]> andersk Git - test.git/commitdiff
Added an optional on-screen keyboard. Must be activated by the user by selecting...
authorMarkus Gutschke <markus@shellinabox.com>
Sat, 4 Sep 2010 17:50:11 +0000 (17:50 +0000)
committerMarkus Gutschke <markus@shellinabox.com>
Sat, 4 Sep 2010 17:50:11 +0000 (17:50 +0000)
16 files changed:
ChangeLog
Makefile.am
Makefile.in
config.h
configure
configure.ac
demo/keyboard.png [new file with mode: 0644]
demo/styles.css
demo/vt100.js
shellinabox/keyboard-layout.html [new file with mode: 0644]
shellinabox/keyboard.png [new file with mode: 0644]
shellinabox/shell_in_a_box.js
shellinabox/shellinaboxd.c
shellinabox/styles.css
shellinabox/vt100.js
shellinabox/vt100.jspp

index d7ea95971712d7f5eb1fcbd4485cb96db963fc34..2e216b0d64b1026dfb712a52e3b6ea73d51bbaf8 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,8 @@
+2010-09-04  Markus Gutschke  <markus@shellinabox.com>
+
+       * Added an optional on-screen keyboard. Must be activated by the
+       user by selecting the option in the context-menu.
+
 2010-09-03  Markus Gutschke  <markus@shellinabox.com>
 
        * Fix some scaling related issues. This fix is thanks to some
index 3a07cd3eb130ff362659bcc838280ac1ac1cf23d..2f28df253011a95bd53efddaa2e462f4e5f556a5 100644 (file)
@@ -32,6 +32,7 @@ EXTRA_DIST           = INSTALL.Debian                                         \
                        demo/demo.jspp                                         \
                        demo/demo.xml                                          \
                        demo/enabled.gif                                       \
+                       demo/keyboard.png                                      \
                        demo/styles.css                                        \
                        demo/print-styles.css                                  \
                        demo/vt100.js                                          \
@@ -106,6 +107,8 @@ shellinaboxd_SOURCES = shellinabox/shellinaboxd.c                             \
                        shellinabox/print-styles.css                           \
                        shellinabox/enabled.gif                                \
                        shellinabox/favicon.ico                                \
+                       shellinabox/keyboard.png                               \
+                       shellinabox/keyboard-layout.html                       \
                        shellinabox/beep.wav                                   \
                        config.h
 shellinaboxd_LDADD   = liblogging.la                                          \
@@ -148,7 +151,9 @@ libtool: $(LIBTOOL_DEPS)
 
 ${top_srcdir}/demo/demo.js: ${top_srcdir}/demo/beep.wav                       \
                             ${top_srcdir}/demo/demo.jspp                      \
+                            ${top_srcdir}/demo/enabled.gif                    \
                             ${top_srcdir}/demo/favicon.ico                    \
+                            ${top_srcdir}/demo/keyboard.png                   \
                             ${top_srcdir}/demo/styles.css                     \
                             ${top_srcdir}/demo/print-styles.css               \
                             ${top_srcdir}/demo/vt100.js                       \
@@ -169,6 +174,10 @@ ${top_srcdir}/demo/favicon.ico: ${top_srcdir}/shellinabox/favicon.ico
        @rm -f "$@"
        ln "$?" "$@"
 
+${top_srcdir}/demo/keyboard.png: ${top_srcdir}/shellinabox/keyboard.png
+       @rm -f "$@"
+       ln "$?" "$@"
+
 ${top_srcdir}/demo/styles.css: ${top_srcdir}/shellinabox/styles.css
        @rm -f "$@"
        sed -e '/\[if DEFINES_COLORS\]/,/\[endif DEFINES_COLORS\]/d' "$?" >"$@"
@@ -197,7 +206,8 @@ ${top_srcdir}/demo/vt100.js: ${top_srcdir}/shellinabox/vt100.js
        @rm -f "$@"
        ln "$?" "$@"
 
-shellinaboxd.1: shellinabox/shellinaboxd.man.in config.h
+shellinaboxd.1: ${top_srcdir}/shellinabox/shellinaboxd.man.in                 \
+               ${top_srcdir}/config.h
        @src="${top_srcdir}/shellinabox/shellinaboxd.man.in";                 \
        echo preprocess  "$$src" '>'"$@";                                     \
        if sed -e 's/^#define \([^ ]*\).*/\1/' -e t -e d config.h |           \
@@ -253,6 +263,13 @@ clean-local:
          $(OBJCOPY) --add-section .note.GNU-stack=GNU-stack "$@";            \
          rm -f GNU-stack
 
+.png.o:
+       @echo $(OBJCOPY) "$<" "$@"
+       @$(OBJCOPY) -I binary `$(objcopyflags)` `echo "$<" | $(renamesymbols)`\
+         "$<" "$@"
+       @-printf '\000' >GNU-stack &&                                         \
+         $(OBJCOPY) --add-section .note.GNU-stack=GNU-stack "$@";            \
+         rm -f GNU-stack
 
 .html.o:
        @echo $(OBJCOPY) "$<" "$@"
@@ -272,15 +289,23 @@ clean-local:
          rm -f GNU-stack
 
 
-shellinabox/shell_in_a_box.o: shellinabox/shell_in_a_box.js config.h
+shellinabox/shell_in_a_box.o: ${top_srcdir}/shellinabox/shell_in_a_box.js     \
+               ${top_srcdir}/config.h
+
+${top_srcdir}/shellinabox/vt100.js: ${top_srcdir}/shellinabox/vt100.jspp      \
+               ${top_srcdir}/shellinabox/keyboard-layout.html
 
 .jspp.js:
        @echo preprocess "$<" "$@"
-       @sed -e "`sed -e 's/^#define *\([^ ]*\) *\(.*\)/\/^[^#]\/s\/\1\/\2 \\\\\/* \1 *\\\\\/\/g/' \
+       @kbd=`while read i; do                                                \
+               printf '%s' "\`echo "$$i" | sed 's/&/\\\\\\&/g'\`";           \
+             done <${top_srcdir}/shellinabox/keyboard-layout.html`;          \
+       sed -e "`sed -e 's/^#define *\([^ ]*\) *\(.*\)/\/^[^#]\/s\/\1\/\2 \\\\\/* \1 *\\\\\/\/g/' \
                     -e t                                                     \
                     -e d "$<"`"                                              \
             -e "s/^#/\/\/ #/"                                                \
             -e "s/VERSION/\"@VERSION@ (revision @VCS_REVISION@)\"/g"         \
+            -e "s%KEYBOARD%'$${kbd}'%"                                       \
             "$<" >"$@"
 
 .js.o:
index 50c827236dde5c802f4e85d592efb90f01ae6d39..018f2349f5f07734faf52d23fe8b66a2a40032fd 100644 (file)
@@ -82,6 +82,8 @@ am_shellinaboxd_OBJECTS = shellinaboxd.$(OBJEXT) \
        shellinabox/styles.$(OBJEXT) \
        shellinabox/print-styles.$(OBJEXT) \
        shellinabox/enabled.$(OBJEXT) shellinabox/favicon.$(OBJEXT) \
+       shellinabox/keyboard.$(OBJEXT) \
+       shellinabox/keyboard-layout.$(OBJEXT) \
        shellinabox/beep.$(OBJEXT)
 shellinaboxd_OBJECTS = $(am_shellinaboxd_OBJECTS)
 shellinaboxd_DEPENDENCIES = liblogging.la libhttp.la
@@ -288,6 +290,7 @@ EXTRA_DIST = INSTALL.Debian                                         \
                        demo/demo.jspp                                         \
                        demo/demo.xml                                          \
                        demo/enabled.gif                                       \
+                       demo/keyboard.png                                      \
                        demo/styles.css                                        \
                        demo/print-styles.css                                  \
                        demo/vt100.js                                          \
@@ -366,6 +369,8 @@ shellinaboxd_SOURCES = shellinabox/shellinaboxd.c                             \
                        shellinabox/print-styles.css                           \
                        shellinabox/enabled.gif                                \
                        shellinabox/favicon.ico                                \
+                       shellinabox/keyboard.png                               \
+                       shellinabox/keyboard-layout.html                       \
                        shellinabox/beep.wav                                   \
                        config.h
 
@@ -407,7 +412,7 @@ all: config.h
        $(MAKE) $(AM_MAKEFLAGS) all-am
 
 .SUFFIXES:
-.SUFFIXES: .c .css .gif .html .ico .js .jspp .lo .o .obj .wav
+.SUFFIXES: .c .css .gif .html .ico .js .jspp .lo .o .obj .png .wav
 am--refresh:
        @:
 $(srcdir)/Makefile.in:  $(srcdir)/Makefile.am  $(am__configure_deps)
@@ -537,6 +542,10 @@ shellinabox/enabled.$(OBJEXT): shellinabox/$(am__dirstamp) \
        shellinabox/$(DEPDIR)/$(am__dirstamp)
 shellinabox/favicon.$(OBJEXT): shellinabox/$(am__dirstamp) \
        shellinabox/$(DEPDIR)/$(am__dirstamp)
+shellinabox/keyboard.$(OBJEXT): shellinabox/$(am__dirstamp) \
+       shellinabox/$(DEPDIR)/$(am__dirstamp)
+shellinabox/keyboard-layout.$(OBJEXT): shellinabox/$(am__dirstamp) \
+       shellinabox/$(DEPDIR)/$(am__dirstamp)
 shellinabox/beep.$(OBJEXT): shellinabox/$(am__dirstamp) \
        shellinabox/$(DEPDIR)/$(am__dirstamp)
 shellinaboxd$(EXEEXT): $(shellinaboxd_OBJECTS) $(shellinaboxd_DEPENDENCIES) 
@@ -549,6 +558,8 @@ mostlyclean-compile:
        -rm -f shellinabox/cgi_root.$(OBJEXT)
        -rm -f shellinabox/enabled.$(OBJEXT)
        -rm -f shellinabox/favicon.$(OBJEXT)
+       -rm -f shellinabox/keyboard-layout.$(OBJEXT)
+       -rm -f shellinabox/keyboard.$(OBJEXT)
        -rm -f shellinabox/print-styles.$(OBJEXT)
        -rm -f shellinabox/root_page.$(OBJEXT)
        -rm -f shellinabox/shell_in_a_box.$(OBJEXT)
@@ -1162,7 +1173,9 @@ libtool: $(LIBTOOL_DEPS)
 
 ${top_srcdir}/demo/demo.js: ${top_srcdir}/demo/beep.wav                       \
                             ${top_srcdir}/demo/demo.jspp                      \
+                            ${top_srcdir}/demo/enabled.gif                    \
                             ${top_srcdir}/demo/favicon.ico                    \
+                            ${top_srcdir}/demo/keyboard.png                   \
                             ${top_srcdir}/demo/styles.css                     \
                             ${top_srcdir}/demo/print-styles.css               \
                             ${top_srcdir}/demo/vt100.js                       \
@@ -1183,6 +1196,10 @@ ${top_srcdir}/demo/favicon.ico: ${top_srcdir}/shellinabox/favicon.ico
        @rm -f "$@"
        ln "$?" "$@"
 
+${top_srcdir}/demo/keyboard.png: ${top_srcdir}/shellinabox/keyboard.png
+       @rm -f "$@"
+       ln "$?" "$@"
+
 ${top_srcdir}/demo/styles.css: ${top_srcdir}/shellinabox/styles.css
        @rm -f "$@"
        sed -e '/\[if DEFINES_COLORS\]/,/\[endif DEFINES_COLORS\]/d' "$?" >"$@"
@@ -1211,7 +1228,8 @@ ${top_srcdir}/demo/vt100.js: ${top_srcdir}/shellinabox/vt100.js
        @rm -f "$@"
        ln "$?" "$@"
 
-shellinaboxd.1: shellinabox/shellinaboxd.man.in config.h
+shellinaboxd.1: ${top_srcdir}/shellinabox/shellinaboxd.man.in                 \
+               ${top_srcdir}/config.h
        @src="${top_srcdir}/shellinabox/shellinaboxd.man.in";                 \
        echo preprocess  "$$src" '>'"$@";                                     \
        if sed -e 's/^#define \([^ ]*\).*/\1/' -e t -e d config.h |           \
@@ -1267,6 +1285,14 @@ clean-local:
          $(OBJCOPY) --add-section .note.GNU-stack=GNU-stack "$@";            \
          rm -f GNU-stack
 
+.png.o:
+       @echo $(OBJCOPY) "$<" "$@"
+       @$(OBJCOPY) -I binary `$(objcopyflags)` `echo "$<" | $(renamesymbols)`\
+         "$<" "$@"
+       @-printf '\000' >GNU-stack &&                                         \
+         $(OBJCOPY) --add-section .note.GNU-stack=GNU-stack "$@";            \
+         rm -f GNU-stack
+
 .html.o:
        @echo $(OBJCOPY) "$<" "$@"
        @$(OBJCOPY) -I binary `$(objcopyflags)` `echo "$<" | $(renamesymbols)`\
@@ -1283,15 +1309,23 @@ clean-local:
          $(OBJCOPY) --add-section .note.GNU-stack=GNU-stack "$@";            \
          rm -f GNU-stack
 
-shellinabox/shell_in_a_box.o: shellinabox/shell_in_a_box.js config.h
+shellinabox/shell_in_a_box.o: ${top_srcdir}/shellinabox/shell_in_a_box.js     \
+               ${top_srcdir}/config.h
+
+${top_srcdir}/shellinabox/vt100.js: ${top_srcdir}/shellinabox/vt100.jspp      \
+               ${top_srcdir}/shellinabox/keyboard-layout.html
 
 .jspp.js:
        @echo preprocess "$<" "$@"
-       @sed -e "`sed -e 's/^#define *\([^ ]*\) *\(.*\)/\/^[^#]\/s\/\1\/\2 \\\\\/* \1 *\\\\\/\/g/' \
+       @kbd=`while read i; do                                                \
+               printf '%s' "\`echo "$$i" | sed 's/&/\\\\\\&/g'\`";           \
+             done <${top_srcdir}/shellinabox/keyboard-layout.html`;          \
+       sed -e "`sed -e 's/^#define *\([^ ]*\) *\(.*\)/\/^[^#]\/s\/\1\/\2 \\\\\/* \1 *\\\\\/\/g/' \
                     -e t                                                     \
                     -e d "$<"`"                                              \
             -e "s/^#/\/\/ #/"                                                \
             -e "s/VERSION/\"@VERSION@ (revision @VCS_REVISION@)\"/g"         \
+            -e "s%KEYBOARD%'$${kbd}'%"                                       \
             "$<" >"$@"
 
 .js.o:
index e9fc3ac6ffe3647932aa3dc02ee97aa611c308f8..547512cd4e6bc4429711d61b1f4f737c9cde59ba 100644 (file)
--- a/config.h
+++ b/config.h
 #define STDC_HEADERS 1
 
 /* Most recent revision number in the version control system */
-#define VCS_REVISION "220"
+#define VCS_REVISION "221"
 
 /* Version number of package */
 #define VERSION "2.10"
index 801ff0a7c22541bce08cbec69754d213ac733928..ffd537eac697bbf447a7add09e022c6db3559cb8 100755 (executable)
--- a/configure
+++ b/configure
@@ -2328,7 +2328,7 @@ ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $
 ac_compiler_gnu=$ac_cv_c_compiler_gnu
 
 
-VCS_REVISION=220
+VCS_REVISION=221
 
 
 cat >>confdefs.h <<_ACEOF
index 53fa250eb0019c295ce81c489cbd0605d7ba0640..64d38bb037df29d29ea5d322503999c2edfd0d18 100644 (file)
@@ -2,7 +2,7 @@ AC_PREREQ(2.57)
 
 dnl This is the one location where the authoritative version number is stored
 AC_INIT(shellinabox, 2.10, markus@shellinabox.com)
-VCS_REVISION=220
+VCS_REVISION=221
 AC_SUBST(VCS_REVISION)
 AC_DEFINE_UNQUOTED(VCS_REVISION, "${VCS_REVISION}",
                    [Most recent revision number in the version control system])
diff --git a/demo/keyboard.png b/demo/keyboard.png
new file mode 100644 (file)
index 0000000..feef519
Binary files /dev/null and b/demo/keyboard.png differ
index 7f7f6836e4409b505a1874420a9479f805e3d254..d7d5d2420deb48840eb5ff3bfed9da8b3bfee25d 100644 (file)
 #vt100 a { 
-  text-decoration:  none;
-  color:            inherit;
+  text-decoration:      none;
+  color:                inherit;
 }
 
 #vt100 a:hover { 
-  text-decoration:  underline;
+  text-decoration:      underline;
 }
 
 #vt100 #reconnect {
-  position:         absolute;
-  z-index:          2;
+  position:             absolute;
+  z-index:              2;
 }
 
 #vt100 #reconnect input { 
-  padding:          1ex;
-  font-weight:      bold;
-  font-size:        x-large;
+  padding:              1ex;
+  font-weight:          bold;
+  font-size:            x-large;
 }
 
 #vt100 #cursize {
-  background:       #EEEEEE;
-  border:           1px solid black;
-  font-family:      sans-serif;
-  font-size:        large;
-  font-weight:      bold;
-  padding:          1ex;
-  position:         absolute;
-  z-index:          2;
+  background:           #EEEEEE;
+  border:               1px solid black;
+  font-family:          sans-serif;
+  font-size:            large;
+  font-weight:          bold;
+  padding:              1ex;
+  position:             absolute;
+  z-index:              2;
 }
 
 #vt100 pre { 
-  margin:           0px;
+  margin:               0px;
 }
 
 #vt100 pre pre {
-  overflow:         hidden;
+  overflow:             hidden;
 }
 
 #vt100 #scrollable {
-  overflow-x:       hidden;
-  overflow-y:       scroll;
-  position:         relative;
-  padding:          1px;
+  overflow-x:           hidden;
+  overflow-y:           scroll;
+  position:             relative;
+  padding:              1px;
 }
 
 #vt100 #console, #vt100 #alt_console, #vt100 #cursor, #vt100 #lineheight, #vt100 .hidden pre { 
-  font-family:      "DejaVu Sans Mono", "Everson Mono", FreeMono, "Andale Mono", "Lucida Console", monospace;
+  font-family:          "DejaVu Sans Mono", "Everson Mono", FreeMono, "Andale Mono", "Lucida Console", monospace;
 }
 
 #vt100 #lineheight { 
-  position:         absolute;
-  visibility:       hidden;
+  position:             absolute;
+  visibility:           hidden;
 }
 
 #vt100 #cursor {
-  position:         absolute;
-  left:             0px;
-  top:              0px;
-  overflow:         hidden;
-  z-index:          1;
+  position:             absolute;
+  left:                 0px;
+  top:                  0px;
+  overflow:             hidden;
+  z-index:              1;
 }
 
 #vt100 #cursor.bright { 
-  background-color: #e60000;
-  color:            white;
+  background-color:     #e60000;
+  color:                white;
 }
 
 #vt100 #cursor.dim {
-  visibility:       hidden;
+  visibility:           hidden;
 }
 
 #vt100 #cursor.inactive {
-  border:           1px solid #e60000;
-  margin:           -1px;
+  border:               1px solid #e60000;
+  margin:               -1px;
 }
 
 #vt100 #padding { 
-  visibility:       hidden;
-  width:            1px;
-  height:           0px;
-  overflow:         hidden;
+  visibility:           hidden;
+  width:                1px;
+  height:               0px;
+  overflow:             hidden;
 }
 
 #vt100 .hidden {
-  position:         absolute;
-  top:              -10000px;
-  left:             -10000px;
-  width:            0px;
-  height:           0px;
+  position:             absolute;
+  top:                  -10000px;
+  left:                 -10000px;
+  width:                0px;
+  height:               0px;
 }
 
 #vt100 #menu { 
-  overflow:         visible;
-  position:         absolute;
-  z-index:          3;
+  overflow:             visible;
+  position:             absolute;
+  z-index:              3;
 }
 
 #vt100 #menu .popup {
-  background-color: #EEEEEE;
-  border:           1px solid black;
-  font-family:      sans-serif;
-  position:         absolute;
+  background-color:     #EEEEEE;
+  border:               1px solid black;
+  font-family:          sans-serif;
+  position:             absolute;
 }
 
 #vt100 #menu .popup ul { 
-  list-style-type:  none;
-  padding:          0px;
-  margin:           0px;
-  min-width:        10em;
+  list-style-type:      none;
+  padding:              0px;
+  margin:               0px;
+  min-width:            10em;
 }
 
 #vt100 #menu .popup li { 
-  padding:          3px 0.5ex 3px 0.5ex;
+  padding:              3px 0.5ex 3px 0.5ex;
 }
 
 #vt100 #menu .popup li.hover {
-  background-color: #444444;
-  color:            white;
+  background-color:     #444444;
+  color:                white;
 }
 
 #vt100 #menu .popup li.disabled {
-  color:            #AAAAAA;
+  color:                #AAAAAA;
 }
 
 #vt100 #menu .popup hr { 
-  margin:           0.5ex 0px 0.5ex 0px;
+  margin:               0.5ex 0px 0.5ex 0px;
 }
 
 #vt100 #menu img { 
-  margin-right:     0.5ex;
-  width:            1ex;
-  height:           1ex;
+  margin-right:         0.5ex;
+  width:                1ex;
+  height:               1ex;
 }
 
 #vt100 #scrollable.inverted { color:            #ffffff;
                               background-color: #000000; }
 
+#vt100 #kbd_button { 
+  float:                left;
+  position:             fixed;
+  z-index:              0;
+  visibility:           hidden;
+}
+
+#vt100 #keyboard {
+  z-index:              3;
+  position:             absolute;
+}
+
+#vt100 #keyboard .box {
+  font-family:          sans-serif;
+  background-color:     #cccccc;
+  padding:              .8em;
+  float:                left;
+  position:             absolute;
+  border-radius:        10px;
+  -moz-border-radius:   10px;
+  box-shadow:           4px 4px 6px #222222;
+  -webkit-box-shadow:   4px 4px 6px #222222;
+  /* Don't set the -moz-box-shadow. It doesn't properly scale when CSS
+   * transforms are in effect. Once Firefox supports box-shadow, it should
+   * automatically do the right thing. Until then, leave shadows disabled
+   * for Firefox.
+   */
+  opacity:              0.85;
+  -moz-opacity:         0.85;
+  filter:               alpha(opacity=85);
+}
+
+#vt100 #keyboard .box * {
+  vertical-align:       top;
+  display:              inline-block;
+}
+
+#vt100 #keyboard b, #vt100 #keyboard i, #vt100 #keyboard s, #vt100 #keyboard u {
+  font-style:           normal;
+  font-weight:          bold;
+  border-radius:        5px;
+  -moz-border-radius:   5px;
+  background-color:     #555555;
+  color:                #eeeeee;
+  box-shadow:           2px 2px 3px #222222;
+  -webkit-box-shadow:   2px 2px 3px #222222;
+  padding:              4px;
+  margin:               2px;
+  height:               2ex;
+  display:              inline-block;
+  text-align:           center;
+  text-decoration:      none;
+}
+
+#vt100 #keyboard b, #vt100 #keyboard s {
+  width:                2ex;
+}
+
+#vt100 #keyboard u, #vt100 #keyboard s {
+  visibility:           hidden;
+}
+
+#vt100 #keyboard .shifted { 
+  display:              none;
+}
+
+#vt100 #keyboard .selected {
+  color:                #888888;
+  background-color:     #eeeeee;
+  box-shadow:           0px 0px 3px #222222;
+  -webkit-box-shadow:   0px 0px 3px #222222;
+  position:             relative;
+  top:                  1px;
+  left:                 1px;
+}
+
+
 @media print {
   #vt100 .scrollback {
-    display:        none;
+    display:            none;
   }
 
-  #vt100 #reconnect, #vt100 #cursor, #vt100 #menu { 
-    visibility:     hidden;
+  #vt100 #reconnect, #vt100 #cursor, #vt100 #menu, #vt100 #kbd_button, #vt100 #keyboard { 
+    visibility:         hidden;
   }
 
   #vt100 #scrollable { 
-    overflow:       hidden;
+    overflow:           hidden;
   }
 
   #vt100 #console, #vt100 #alt_console { 
-    overflow:       hidden;
-    width:          1000000ex;
+    overflow:           hidden;
+    width:              1000000ex;
   }
 }
index f9345b9d3952ac5e769e779f1441b51ff13e8200..5457b612a4c32416f13e0d3dcc5fff7aa72a20ea 100644 (file)
@@ -238,22 +238,16 @@ VT100.prototype.reset = function(clearHistory) {
   this.enableAlternateScreen(false);
 
   var wasCompressed                                  = false;
-  var styles                                         = [ 'transform',
-                                                         'WebkitTransform',
-                                                         'MozTransform',
-                                                         'filter' ];
-  for (var i = 0; i < styles.length; ++i) {
-    if (typeof this.console[0].style[styles[i]] != 'undefined') {
-      for (var j = 0; j < 1; ++j) {
-        wasCompressed                |= this.console[j].style[styles[i]] != '';
-        this.console[j].style[styles[i]]             = '';
-      }
-      this.cursor.style[styles[i]]                   = '';
-      this.space.style[styles[i]]                    = '';
-      if (styles[i] == 'filter') {
-        this.console[this.currentScreen].style.width = '';
-      }
-      break;
+  var transform                                      = this.getTransformName();
+  if (transform) {
+    for (var i = 0; i < 2; ++i) {
+      wasCompressed                  |= this.console[i].style[transform] != '';
+      this.console[i].style[transform]               = '';
+    }
+    this.cursor.style[transform]                     = '';
+    this.space.style[transform]                      = '';
+    if (transform == 'filter') {
+      this.console[this.currentScreen].style.width   = '';
     }
   }
   this.scale                                         = 1.0;
@@ -270,10 +264,13 @@ VT100.prototype.reset = function(clearHistory) {
 };
 
 VT100.prototype.addListener = function(elem, event, listener) {
-  if (elem.addEventListener) {
-    elem.addEventListener(event, listener, false);
-  } else {
-    elem.attachEvent('on' + event, listener);
+  try {
+    if (elem.addEventListener) {
+      elem.addEventListener(event, listener, false);
+    } else {
+      elem.attachEvent('on' + event, listener);
+    }
+  } catch (e) {
   }
 };
 
@@ -281,11 +278,12 @@ VT100.prototype.getUserSettings = function() {
   // Compute hash signature to identify the entries in the userCSS menu.
   // If the menu is unchanged from last time, default values can be
   // looked up in a cookie associated with this page.
-  this.signature            = 2;
+  this.signature            = 3;
   this.utfPreferred         = true;
   this.visualBell           = typeof suppressAllAudio != 'undefined' &&
                               suppressAllAudio;
   this.autoprint            = true;
+  this.softKeyboard         = false;
   this.blinkingCursor       = true;
   if (this.visualBell) {
     this.signature          = Math.floor(16807*this.signature + 1) %
@@ -311,15 +309,16 @@ VT100.prototype.getUserSettings = function() {
   if (settings >= 0) {
     settings                = document.cookie.substr(settings + key.length).
                                                    replace(/([0-1]*).*/, "$1");
-    if (settings.length == 3 + (typeof userCSSList == 'undefined' ?
+    if (settings.length == 5 + (typeof userCSSList == 'undefined' ?
                                 0 : userCSSList.length)) {
       this.utfPreferred     = settings.charAt(0) != '0';
       this.visualBell       = settings.charAt(1) != '0';
       this.autoprint        = settings.charAt(2) != '0';
-      this.blinkingCursor   = settings.charAt(3) != '0';
+      this.softKeyboard     = settings.charAt(3) != '0';
+      this.blinkingCursor   = settings.charAt(4) != '0';
       if (typeof userCSSList != 'undefined') {
         for (var i = 0; i < userCSSList.length; ++i) {
-          userCSSList[i][2] = settings.charAt(i + 3) != '0';
+          userCSSList[i][2] = settings.charAt(i + 5) != '0';
         }
       }
     }
@@ -332,6 +331,7 @@ VT100.prototype.storeUserSettings = function() {
                   (this.utfEnabled     ? '1' : '0') +
                   (this.visualBell     ? '1' : '0') +
                   (this.autoprint      ? '1' : '0') +
+                  (this.softKeyboard   ? '1' : '0') +
                   (this.blinkingCursor ? '1' : '0');
   if (typeof userCSSList != 'undefined') {
     for (var i = 0; i < userCSSList.length; ++i) {
@@ -413,7 +413,7 @@ VT100.prototype.initializeUserCSSStyles = function() {
                         label.textContent= label.textContent;
                       }
 
-                      // User style sheets are number sequentially
+                      // User style sheets are numbered sequentially
                       var sheet          = document.getElementById(
                                                                'usercss-' + i);
                       if (i == current) {
@@ -470,6 +470,328 @@ VT100.prototype.initializeUserCSSStyles = function() {
   }
 };
 
+VT100.prototype.resetLastSelectedKey = function(e) {
+  var key                          = this.lastSelectedKey;
+  if (!key) {
+    return false;
+  }
+
+  var position                     = this.mousePosition(e);
+
+  // We don't get all the necessary events to reliably reselect a key
+  // if we moved away from it and then back onto it. We approximate the
+  // behavior by remembering the key until either we release the mouse
+  // button (we might never get this event if the mouse has since left
+  // the window), or until we move away too far.
+  var box                          = this.keyboard.firstChild;
+  if (position[0] <  box.offsetLeft + key.offsetWidth ||
+      position[1] <  box.offsetTop + key.offsetHeight ||
+      position[0] >= box.offsetLeft + box.offsetWidth - key.offsetWidth ||
+      position[1] >= box.offsetTop + box.offsetHeight - key.offsetHeight ||
+      position[0] <  box.offsetLeft + key.offsetLeft - key.offsetWidth ||
+      position[1] <  box.offsetTop + key.offsetTop - key.offsetHeight ||
+      position[0] >= box.offsetLeft + key.offsetLeft + 2*key.offsetWidth ||
+      position[1] >= box.offsetTop + key.offsetTop + 2*key.offsetHeight) {
+    if (this.lastSelectedKey.className) log.console('reset: deselecting');
+    this.lastSelectedKey.className = '';
+    this.lastSelectedKey           = undefined;
+  }
+  return false;
+};
+
+VT100.prototype.showShiftState = function(state) {
+  var style              = document.getElementById('shift_state');
+  if (state) {
+    this.setTextContentRaw(style,
+                           '#vt100 #keyboard .shifted {' +
+                             'display: inline }' +
+                           '#vt100 #keyboard .unshifted {' +
+                             'display: none }');
+  } else {
+    this.setTextContentRaw(style, '');
+  }
+  var elems              = this.keyboard.getElementsByTagName('I');
+  for (var i = 0; i < elems.length; ++i) {
+    if (elems[i].id == '16') {
+      elems[i].className = state ? 'selected' : '';
+    }
+  }
+};
+
+VT100.prototype.showCtrlState = function(state) {
+  var ctrl         = this.getChildById(this.keyboard, '17' /* Ctrl */);
+  if (ctrl) {
+    ctrl.className = state ? 'selected' : '';
+  }
+};
+
+VT100.prototype.showAltState = function(state) {
+  var alt         = this.getChildById(this.keyboard, '18' /* Alt */);
+  if (alt) {
+    alt.className = state ? 'selected' : '';
+  }
+};
+
+VT100.prototype.clickedKeyboard = function(e, elem, ch, key, shift, ctrl, alt){
+  var fake      = [ ];
+  fake.charCode = ch;
+  fake.keyCode  = key;
+  fake.ctrlKey  = ctrl;
+  fake.shiftKey = shift;
+  fake.altKey   = alt;
+  fake.metaKey  = alt;
+  return this.handleKey(fake);
+};
+
+VT100.prototype.addKeyBinding = function(elem, ch, key, CH, KEY) {
+  if (elem == undefined) {
+    return;
+  }
+  if (ch == '\u00A0') {
+    // &nbsp; should be treated as a regular space character.
+    ch                                  = ' ';
+  }
+  if (ch != undefined && CH == undefined) {
+    // For letter keys, we automatically compute the uppercase character code
+    // from the lowercase one.
+    CH                                  = ch.toUpperCase();
+  }
+  if (KEY == undefined && key != undefined) {
+    // Most keys have identically key codes for both lowercase and uppercase
+    // keypresses. Normally, only function keys would have distinct key codes,
+    // whereas regular keys have character codes.
+    KEY                                 = key;
+  } else if (KEY == undefined && CH != undefined) {
+    // For regular keys, copy the character code to the key code.
+    KEY                                 = CH.charCodeAt(0);
+  }
+  if (key == undefined && ch != undefined) {
+    // For regular keys, copy the character code to the key code.
+    key                                 = ch.charCodeAt(0);
+  }
+  // Convert characters to numeric character codes. If the character code
+  // is undefined (i.e. this is a function key), set it to zero.
+  ch                                    = ch ? ch.charCodeAt(0) : 0;
+  CH                                    = CH ? CH.charCodeAt(0) : 0;
+
+  // Mouse down events high light the key. We also set lastSelectedKey. This
+  // is needed to that mouseout/mouseover can keep track of the key that
+  // is currently being clicked.
+  this.addListener(elem, 'mousedown',
+    function(vt100, elem, key) { return function(e) {
+      if ((e.which || e.button) == 1) {
+        if (vt100.lastSelectedKey) {       
+          vt100.lastSelectedKey.className= '';
+        }
+        // Highlight the key while the mouse button is held down.
+        if (key == 16 /* Shift */) {
+          if (!elem.className != vt100.isShift) {
+            vt100.showShiftState(!vt100.isShift);
+          }
+        } else if (key == 17 /* Ctrl */) {
+          if (!elem.className != vt100.isCtrl) {
+            vt100.showCtrlState(!vt100.isCtrl);
+          }
+        } else if (key == 18 /* Alt */) {
+          if (!elem.className != vt100.isAlt) {
+            vt100.showAltState(!vt100.isAlt);
+          }
+        } else {
+          elem.className                  = 'selected';
+        }
+        vt100.lastSelectedKey             = elem;
+      }
+      return false; }; }(this, elem, key));
+  var clicked                           =
+    // Modifier keys update the state of the keyboard, but do not generate
+    // any key clicks that get forwarded to the application.
+    key >= 16 /* Shift */ && key <= 18 /* Alt */ ?
+    function(vt100, elem) { return function(e) {
+      if (elem == vt100.lastSelectedKey) {
+        if (key == 16 /* Shift */) {
+          // The user clicked the Shift key
+          vt100.isShift                 = !vt100.isShift;
+          vt100.showShiftState(vt100.isShift);
+        } else if (key == 17 /* Ctrl */) {
+          vt100.isCtrl                  = !vt100.isCtrl;
+          vt100.showCtrlState(vt100.isCtrl);
+        } else if (key == 18 /* Alt */) {
+          vt100.isAlt                   = !vt100.isAlt;
+          vt100.showAltState(vt100.isAlt);
+        }
+        vt100.lastSelectedKey           = undefined;
+      }
+      if (vt100.lastSelectedKey) {
+        vt100.lastSelectedKey.className = '';
+        vt100.lastSelectedKey           = undefined;
+      }
+      return false; }; }(this, elem) :
+    // Regular keys generate key clicks, when the mouse button is released or
+    // when a mouse click event is received.
+    function(vt100, elem, ch, key, CH, KEY) { return function(e) {
+      if (vt100.lastSelectedKey) {
+        if (elem == vt100.lastSelectedKey) {
+          // The user clicked a key.
+          if (vt100.isShift) {
+            vt100.clickedKeyboard(e, elem, CH, KEY,
+                                  true, vt100.isCtrl, vt100.isAlt);
+          } else {
+            vt100.clickedKeyboard(e, elem, ch, key,
+                                  false, vt100.isCtrl, vt100.isAlt);
+          }
+          vt100.isShift                 = false;
+          vt100.showShiftState(false);
+          vt100.isCtrl                  = false;
+          vt100.showCtrlState(false);
+          vt100.isAlt                   = false;
+          vt100.showAltState(false);
+        }
+        vt100.lastSelectedKey.className = '';
+        vt100.lastSelectedKey           = undefined;
+      }
+      elem.className                    = '';
+      return false; }; }(this, elem, ch, key, CH, KEY);
+  this.addListener(elem, 'mouseup', clicked);
+  this.addListener(elem, 'click', clicked);
+
+  // When moving the mouse away from a key, check if any keys need to be
+  // deselected.
+  this.addListener(elem, 'mouseout',
+    function(vt100, elem, key) { return function(e) {
+      if (key == 16 /* Shift */) {
+        if (!elem.className == vt100.isShift) {
+          vt100.showShiftState(vt100.isShift);
+        }
+      } else if (key == 17 /* Ctrl */) {
+        if (!elem.className == vt100.isCtrl) {
+          vt100.showCtrlState(vt100.isCtrl);
+        }
+      } else if (key == 18 /* Alt */) {
+        if (!elem.className == vt100.isAlt) {
+          vt100.showAltState(vt100.isAlt);
+        }
+      } else if (elem.className) {
+        elem.className                  = '';
+        vt100.lastSelectedKey           = elem;
+      } else if (vt100.lastSelectedKey) {
+        vt100.resetLastSelectedKey(e);
+      }
+      return false; }; }(this, elem, key));
+
+  // When moving the mouse over a key, select it if the user is still holding
+  // the mouse button down (i.e. elem == lastSelectedKey)
+  this.addListener(elem, 'mouseover',
+    function(vt100, elem, key) { return function(e) {
+      if (elem == vt100.lastSelectedKey) {
+        if (key == 16 /* Shift */) {
+          if (!elem.className != vt100.isShift) {
+            vt100.showShiftState(!vt100.isShift);
+          }
+        } else if (key == 17 /* Ctrl */) {
+          if (!elem.className != vt100.isCtrl) {
+            vt100.showCtrlState(!vt100.isCtrl);
+          }
+        } else if (key == 18 /* Alt */) {
+          if (!elem.className != vt100.isAlt) {
+            vt100.showAltState(!vt100.isAlt);
+          }
+        } else if (!elem.className) {
+          elem.className                = 'selected';
+        }
+      } else {
+        vt100.resetLastSelectedKey(e);
+      }
+      return false; }; }(this, elem, key));
+};
+
+VT100.prototype.initializeKeyBindings = function(elem) {
+  if (elem) {
+    if (elem.nodeName == "I" || elem.nodeName == "B") {
+      if (elem.id) {
+        // Function keys. The Javascript keycode is part of the "id"
+        var i     = parseInt(elem.id);
+        if (i) {
+          // If the id does not parse as a number, it is not a keycode.
+          this.addKeyBinding(elem, undefined, i);
+        }
+      } else {
+        var child = elem.firstChild;
+        if (child.nodeName == "#text") {
+          // If the key only has a text node as a child, then it is a letter.
+          // Automatically compute the lower and upper case version of the key.
+          this.addKeyBinding(elem, this.getTextContent(child).toLowerCase());
+        } else {
+          // If the key has two children, they are the lower and upper case
+          // character code, respectively.
+          this.addKeyBinding(elem, this.getTextContent(child), undefined,
+                             this.getTextContent(child.nextSibling));
+        }
+      }
+    }
+  }
+  // Recursively parse all other child nodes.
+  for (elem = elem.firstChild; elem; elem = elem.nextSibling) {
+    this.initializeKeyBindings(elem);
+  }
+};
+
+VT100.prototype.initializeKeyboard = function() {
+  // Configure mouse event handlers for button that displays/hides keyboard
+  var box                               = this.keyboard.firstChild;
+  this.hideSoftKeyboard();
+  this.addListener(this.keyboardImage, 'click',
+    function(vt100) { return function(e) {
+      if (vt100.keyboard.style.display != '') {
+        if (vt100.reconnectBtn.style.visibility != '') {
+          vt100.showSoftKeyboard();
+        }
+      } else {
+        vt100.hideSoftKeyboard();
+        vt100.input.focus();
+      }
+      return false; }; }(this));
+
+  // Enable button that displays keyboard
+  if (this.softKeyboard) {
+    this.keyboardImage.style.visibility = 'visible';
+  }
+
+  // Configure mouse event handlers for on-screen keyboard
+  this.addListener(this.keyboard, 'click',
+    function(vt100) { return function(e) {
+      vt100.hideSoftKeyboard();
+      vt100.input.focus();
+      return false; }; }(this));
+  this.addListener(this.keyboard, 'selectstart', this.cancelEvent);
+  this.addListener(box, 'click', this.cancelEvent);
+  this.addListener(box, 'mouseup',
+    function(vt100) { return function(e) {
+      if (vt100.lastSelectedKey) {
+        vt100.lastSelectedKey.className = '';
+        vt100.lastSelectedKey           = undefined;
+      }
+      return false; }; }(this));
+  this.addListener(box, 'mouseout',
+    function(vt100) { return function(e) {
+      return vt100.resetLastSelectedKey(e); }; }(this));
+  this.addListener(box, 'mouseover',
+    function(vt100) { return function(e) {
+      return vt100.resetLastSelectedKey(e); }; }(this));
+
+  // Configure SHIFT key behavior
+  var style                             = document.createElement('style');
+  var id                                = document.createAttribute('id');
+  id.nodeValue                          = 'shift_state';
+  style.setAttributeNode(id);
+  var type                              = document.createAttribute('type');
+  type.nodeValue                        = 'text/css';
+  style.setAttributeNode(type);
+  document.getElementsByTagName('head')[0].appendChild(style);
+
+  // Set up key bindings
+  this.initializeKeyBindings(box);
+};
+
 VT100.prototype.initializeElements = function(container) {
   // If the necessary objects have not already been defined in the HTML
   // page, create them now.
@@ -483,6 +805,9 @@ VT100.prototype.initializeElements = function(container) {
 
   if (!this.getChildById(this.container, 'reconnect')   ||
       !this.getChildById(this.container, 'menu')        ||
+      !this.getChildById(this.container, 'keyboard')    ||
+      !this.getChildById(this.container, 'kbd_button')  ||
+      !this.getChildById(this.container, 'kbd_img')     ||
       !this.getChildById(this.container, 'scrollable')  ||
       !this.getChildById(this.container, 'console')     ||
       !this.getChildById(this.container, 'alt_console') ||
@@ -525,7 +850,15 @@ VT100.prototype.initializeElements = function(container) {
                        '<div id="cursize" style="visibility: hidden">' +
                        '</div>' +
                        '<div id="menu"></div>' +
+                       '<div id="keyboard" unselectable="on">' +
+                         '<pre class="box"><div><i id="27">Esc</i><i id="112">F1</i><i id="113">F2</i><i id="114">F3</i><i id="115">F4</i><i id="116">F5</i><i id="117">F6</i><i id="118">F7</i><i id="119">F8</i><i id="120">F9</i><i id="121">F10</i><i id="122">F11</i><i id="123">F12</i><br /><b><span class="unshifted">`</span><span class="shifted">~</span></b><b><span class="unshifted">1</span><span class="shifted">!</span></b><b><span class="unshifted">2</span><span class="shifted">@</span></b><b><span class="unshifted">3</span><span class="shifted">#</span></b><b><span class="unshifted">4</span><span class="shifted">&#36;</span></b><b><span class="unshifted">5</span><span class="shifted">&#37;</span></b><b><span class="unshifted">6</span><span class="shifted">^</span></b><b><span class="unshifted">7</span><span class="shifted">&amp;</span></b><b><span class="unshifted">8</span><span class="shifted">*</span></b><b><span class="unshifted">9</span><span class="shifted">(</span></b><b><span class="unshifted">0</span><span class="shifted">)</span></b><b><span class="unshifted">-</span><span class="shifted">_</span></b><b><span class="unshifted">=</span><span class="shifted">+</span></b><i id="8">&nbsp;&larr;&nbsp;</i><br /><i id="9">Tab</i><b>Q</b><b>W</b><b>E</b><b>R</b><b>T</b><b>Y</b><b>U</b><b>I</b><b>O</b><b>P</b><b><span class="unshifted">[</span><span class="shifted">{</span></b><b><span class="unshifted">]</span><span class="shifted">}</span></b><b><span class="unshifted">&#92;</span><span class="shifted">|</span></b><br /><u>Tab&nbsp;&nbsp;</u><b>A</b><b>S</b><b>D</b><b>F</b><b>G</b><b>H</b><b>J</b><b>K</b><b>L</b><b><span class="unshifted">;</span><span class="shifted">:</span></b><b><span class="unshifted">&#39;</span><span class="shifted">"</span></b><i id="13">Enter</i><br /><u>&nbsp;&nbsp;</u><i id="16">Shift</i><b>Z</b><b>X</b><b>C</b><b>V</b><b>B</b><b>N</b><b>M</b><b><span class="unshifted">,</span><span class="shifted">&lt;</span></b><b><span class="unshifted">.</span><span class="shifted">&gt;</span></b><b><span class="unshifted">/</span><span class="shifted">?</span></b><i id="16">Shift</i><br /><u>XXX</u><i id="17">Ctrl</i><i id="18">Alt</i><i style="width: 25ex">&nbsp</i></div>&nbsp;&nbsp;&nbsp;<div><i id="45">Ins</i><i id="46">Del</i><i id="36">Home</i><i id="35">End</i><br /><u>&nbsp;</u><br /><u>&nbsp;</u><br /><u>Ins</u><s>&nbsp;</s><b id="38">&uarr;</b><s>&nbsp;</s><u>&nbsp;</u><b id="33">&uArr;</b><br /><u>Ins</u><b id="37">&larr;</b><b id="40">&darr;</b><b id="39">&rarr;</b><u>&nbsp;</u><b id="34">&dArr;</b></div></pre>' +
+                       '</div>' +
                        '<div id="scrollable">' +
+                         '<table id="kbd_button">' +
+                           '<tr><td width="100%">&nbsp;</td>' +
+                           '<td><img id="kbd_img" src="keyboard.png" /></td>' +
+                           '<td>&nbsp;&nbsp;&nbsp;&nbsp;</td></tr>' +
+                         '</table>' +
                          '<pre id="lineheight">&nbsp;</pre>' +
                          '<pre id="console">' +
                            '<pre></pre>' +
@@ -566,6 +899,8 @@ VT100.prototype.initializeElements = function(container) {
   this.reconnectBtn            = this.getChildById(this.container,'reconnect');
   this.curSizeBox              = this.getChildById(this.container, 'cursize');
   this.menu                    = this.getChildById(this.container, 'menu');
+  this.keyboard                = this.getChildById(this.container, 'keyboard');
+  this.keyboardImage           = this.getChildById(this.container, 'kbd_img');
   this.scrollable              = this.getChildById(this.container,
                                                                  'scrollable');
   this.lineheight              = this.getChildById(this.container,
@@ -646,6 +981,9 @@ VT100.prototype.initializeElements = function(container) {
   // Hide context menu
   this.hideContextMenu();
 
+  // Set up onscreen soft keyboard
+  this.initializeKeyboard();
+
   // Add listener to reconnect button
   this.addListener(this.reconnectBtn.firstChild, 'click',
                    function(vt100) {
@@ -733,6 +1071,7 @@ VT100.prototype.reconnect = function() {
 
 VT100.prototype.showReconnect = function(state) {
   if (state) {
+    this.hideSoftKeyboard();
     this.reconnectBtn.style.visibility = '';
   } else {
     this.reconnectBtn.style.visibility = 'hidden';
@@ -766,6 +1105,9 @@ VT100.prototype.resized = function(w, h) {
 };
 
 VT100.prototype.resizer = function() {
+  // Hide onscreen soft keyboard
+  this.hideSoftKeyboard();
+
   // The cursor can get corrupted if the print-preview is displayed in Firefox.
   // Recreating it, will repair it.
   var newCursor                = document.createElement('pre');
@@ -945,6 +1287,17 @@ VT100.prototype.cancelEvent = function(event) {
   return false;
 };
 
+VT100.prototype.mousePosition = function(event) {
+  var offsetX      = this.container.offsetLeft;
+  var offsetY      = this.container.offsetTop;
+  for (var e = this.container; e = e.offsetParent; ) {
+    offsetX       += e.offsetLeft;
+    offsetY       += e.offsetTop;
+  }
+  return [ event.clientX - offsetX,
+           event.clientY - offsetY ];
+};
+
 VT100.prototype.mouseEvent = function(event, type) {
   // If any text is currently selected, do not move the focus as that would
   // invalidate the selection.
@@ -954,15 +1307,10 @@ VT100.prototype.mouseEvent = function(event, type) {
   }
 
   // Compute mouse position in characters.
-  var offsetX      = this.container.offsetLeft;
-  var offsetY      = this.container.offsetTop;
-  for (var e = this.container; e = e.offsetParent; ) {
-    offsetX       += e.offsetLeft;
-    offsetY       += e.offsetTop;
-  }
-  var x            = (event.clientX - offsetX) / this.cursorWidth;
-  var y            = ((event.clientY - offsetY) + this.scrollable.offsetTop) /
-                     this.cursorHeight - this.numScrollbackLines;
+  var position     = this.mousePosition(event);
+  var x            = Math.floor(position[0] / this.cursorWidth);
+  var y            = Math.floor((position[1] + this.scrollable.scrollTop) /
+                                this.cursorHeight) - this.numScrollbackLines;
   var inside       = true;
   if (x >= this.terminalWidth) {
     x              = this.terminalWidth - 1;
@@ -1022,7 +1370,7 @@ VT100.prototype.mouseEvent = function(event, type) {
   // Bring up context menu.
   if (button == 2 && !event.shiftKey) {
     if (type == 0 /* MOUSE_DOWN */) {
-      this.showContextMenu(event.clientX - offsetX, event.clientY - offsetY);
+      this.showContextMenu(position[0], position[1]);
     }
     return this.cancelEvent(event);
   }
@@ -1058,6 +1406,29 @@ VT100.prototype.getTextContent = function(elem) {
          (typeof elem.textContent == 'undefined' ? elem.innerText : '');
 };
 
+VT100.prototype.setTextContentRaw = function(elem, s) {
+  // Updating the content of an element is an expensive operation. It actually
+  // pays off to first check whether the element is still unchanged.
+  if (typeof elem.textContent == 'undefined') {
+    if (elem.innerText != s) {
+      try {
+        elem.innerText = s;
+      } catch (e) {
+        // Very old versions of IE do not allow setting innerText. Instead,
+        // remove all children, by setting innerHTML and then set the text
+        // using DOM methods.
+        elem.innerHTML = '';
+        elem.appendChild(document.createTextNode(
+                                          this.replaceChar(s, ' ', '\u00A0')));
+      }
+    }
+  } else {
+    if (elem.textContent != s) {
+      elem.textContent = s;
+    }
+  }
+};
+
 VT100.prototype.setTextContent = function(elem, s) {
   // Check if we find any URLs in the text. If so, automatically convert them
   // to links.
@@ -1103,26 +1474,7 @@ VT100.prototype.setTextContent = function(elem, s) {
     return;
   }
 
-  // Updating the content of an element is an expensive operation. It actually
-  // pays off to first check whether the element is still unchanged.
-  if (typeof elem.textContent == 'undefined') {
-    if (elem.innerText != s) {
-      try {
-        elem.innerText = s;
-      } catch (e) {
-        // Very old versions of IE do not allow setting innerText. Instead,
-        // remove all children, by setting innerHTML and then set the text
-        // using DOM methods.
-        elem.innerHTML = '';
-        elem.appendChild(document.createTextNode(
-                                          this.replaceChar(s, ' ', '\u00A0')));
-      }
-    }
-  } else {
-    if (elem.textContent != s) {
-      elem.textContent = s;
-    }
-  }
+  this.setTextContentRaw(elem, s);
 };
 
 VT100.prototype.insertBlankLine = function(y, color, style) {
@@ -1578,27 +1930,21 @@ VT100.prototype.enableAlternateScreen = function(state) {
   this.console[this.currentScreen].style.display     = '';
 
   // Select appropriate character pitch.
-  var styles                                         = [ 'transform',
-                                                         'WebkitTransform',
-                                                         'MozTransform',
-                                                         'filter' ];
-  for (var i = 0; i < styles.length; ++i) {
-    if (typeof this.console[0].style[styles[i]] != 'undefined') {
-      if (state) {
-        // Upon enabling the alternate screen, we switch to 80 column mode. But
-        // upon returning to the regular screen, we restore the mode that was
-        // in effect previously.
-        this.console[1].style[styles[i]]             = '';
-      }
-      var style                                      =
-                             this.console[this.currentScreen].style[styles[i]];
-      this.cursor.style[styles[i]]                   = style;
-      this.space.style[styles[i]]                    = style;
-      this.scale                                     = style == '' ? 1.0:1.65;
-      if (styles[i] == 'filter') {
-        this.console[this.currentScreen].style.width = style == '' ? '165%':'';
-      }
-      break;
+  var transform                                      = this.getTransformName();
+  if (transform) {
+    if (state) {
+      // Upon enabling the alternate screen, we switch to 80 column mode. But
+      // upon returning to the regular screen, we restore the mode that was
+      // in effect previously.
+      this.console[1].style[transform]               = '';
+    }
+    var style                                        =
+                             this.console[this.currentScreen].style[transform];
+    this.cursor.style[transform]                     = style;
+    this.space.style[transform]                      = style;
+    this.scale                                       = style == '' ? 1.0:1.65;
+    if (transform == 'filter') {
+       this.console[this.currentScreen].style.width  = style == '' ? '165%':'';
     }
   }
   this.resizer();
@@ -1969,12 +2315,76 @@ VT100.prototype.toggleBell = function() {
   this.visualBell = !this.visualBell;
 };
 
+VT100.prototype.toggleSoftKeyboard = function() {
+  this.softKeyboard = !this.softKeyboard;
+  this.keyboardImage.style.visibility = this.softKeyboard ? 'visible' : '';
+};
+
+VT100.prototype.deselectKeys = function(elem) {
+  if (elem && elem.className == 'selected') {
+    elem.className = '';
+  }
+  for (elem = elem.firstChild; elem; elem = elem.nextSibling) {
+    this.deselectKeys(elem);
+  }
+};
+
+VT100.prototype.showSoftKeyboard = function() {
+  // Make sure no key is currently selected
+  this.lastSelectedKey           = undefined;
+  this.deselectKeys(this.keyboard);
+  this.isShift                   = false;
+  this.showShiftState(false);
+  this.isCtrl                    = false;
+  this.showCtrlState(false);
+  this.isAlt                     = false;
+  this.showAltState(false);
+
+  this.keyboard.style.left       = '0px';
+  this.keyboard.style.top        = '0px';
+  this.keyboard.style.width      = this.container.offsetWidth  + 'px';
+  this.keyboard.style.height     = this.container.offsetHeight + 'px';
+  this.keyboard.style.visibility = 'hidden';
+  this.keyboard.style.display    = '';
+
+  var kbd                        = this.keyboard.firstChild;
+  var scale                      = 1.0;
+  var transform                  = this.getTransformName();
+  if (transform) {
+    kbd.style[transform]         = '';
+    if (kbd.offsetWidth > 0.9 * this.container.offsetWidth) {
+      scale                      = (kbd.offsetWidth/
+                                    this.container.offsetWidth)/0.9;
+    }
+    if (kbd.offsetHeight > 0.9 * this.container.offsetHeight) {
+      scale                      = Math.max((kbd.offsetHeight/
+                                             this.container.offsetHeight)/0.9);
+    }
+    var style                    = this.getTransformStyle(transform,
+                                              scale > 1.0 ? scale : undefined);
+    kbd.style[transform]         = style;
+  }
+  if (transform == 'filter') {
+    scale                        = 1.0;
+  }
+  kbd.style.left                 = ((this.container.offsetWidth -
+                                     kbd.offsetWidth/scale)/2) + 'px';
+  kbd.style.top                  = ((this.container.offsetHeight -
+                                     kbd.offsetHeight/scale)/2) + 'px';
+
+  this.keyboard.style.visibility = 'visible';
+};
+
+VT100.prototype.hideSoftKeyboard = function() {
+  this.keyboard.style.display    = 'none';
+};
+
 VT100.prototype.toggleCursorBlinking = function() {
   this.blinkingCursor = !this.blinkingCursor;
 };
 
 VT100.prototype.about = function() {
-  alert("VT100 Terminal Emulator " + "2.10 (revision 220)" +
+  alert("VT100 Terminal Emulator " + "2.10 (revision 221)" +
         "\nCopyright 2008-2010 by Markus Gutschke\n" +
         "For more information check http://shellinabox.com");
 };
@@ -2007,6 +2417,9 @@ VT100.prototype.showContextMenu = function(x, y) {
           '<li>' +
              (this.visualBell ? '<img src="enabled.gif" />' : '') +
              'Visual Bell</li>'+
+          '<li>' +
+             (this.softKeyboard ? '<img src="enabled.gif" />' : '') +
+             'Onscreen Keyboard</li>' +
           '<li id="endconfig">' +
              (this.blinkingCursor ? '<img src="enabled.gif" />' : '') +
              'Blinking Cursor</li>'+
@@ -2038,6 +2451,7 @@ VT100.prototype.showContextMenu = function(x, y) {
   // Actions for default items
   var actions                 = [ this.copyLast, p, this.reset,
                                   this.toggleUTF, this.toggleBell,
+                                  this.toggleSoftKeyboard,
                                   this.toggleCursorBlinking ];
 
   // Actions for user CSS styles (if any)
@@ -2093,26 +2507,30 @@ VT100.prototype.showContextMenu = function(x, y) {
   }
 
   // Position menu next to the mouse pointer
-  if (x + popup.clientWidth > this.container.offsetWidth) {
-    x                         = this.container.offsetWidth - popup.clientWidth;
+  this.menu.style.left        = '0px';
+  this.menu.style.top         = '0px';
+  this.menu.style.width       =  this.container.offsetWidth  + 'px';
+  this.menu.style.height      =  this.container.offsetHeight + 'px';
+  popup.style.left            = '0px';
+  popup.style.top             = '0px';
+  
+  var margin                  = 2;
+  if (x + popup.clientWidth >= this.container.offsetWidth - margin) {
+    x              = this.container.offsetWidth-popup.clientWidth - margin - 1;
   }
-  if (x < 0) {
-    x                         = 0;
+  if (x < margin) {
+    x                         = margin;
   }
-  if (y + popup.clientHeight > this.container.offsetHeight) {
-    y                         = this.container.offsetHeight-popup.clientHeight;
+  if (y + popup.clientHeight >= this.container.offsetHeight - margin) {
+    y            = this.container.offsetHeight-popup.clientHeight - margin - 1;
   }
-  if (y < 0) {
-    y                         = 0;
+  if (y < margin) {
+    y                         = margin;
   }
   popup.style.left            = x + 'px';
   popup.style.top             = y + 'px';
 
   // Block all other interactions with the terminal emulator
-  this.menu.style.left        = '0px';
-  this.menu.style.top         = '0px';
-  this.menu.style.width       =  this.container.offsetWidth  + 'px';
-  this.menu.style.height      =  this.container.offsetHeight + 'px';
   this.addListener(this.menu, 'click', function(vt100) {
                                          return function() {
                                            vt100.hideContextMenu();
@@ -2895,39 +3313,42 @@ VT100.prototype.restoreCursor = function() {
               this.savedY[this.currentScreen]);
 };
 
-VT100.prototype.set80_132Mode = function(state) {
-  var transform                                       = undefined;
-  var styles                                          = [ 'transform',
-                                                          'WebkitTransform',
-                                                          'MozTransform',
-                                                          'filter'
-                                                        ];
+VT100.prototype.getTransformName = function() {
+  var styles = [ 'transform', 'WebkitTransform', 'MozTransform', 'filter' ];
   for (var i = 0; i < styles.length; ++i) {
     if (typeof this.console[0].style[styles[i]] != 'undefined') {
-      transform                                       = styles[i];
-      break;
+      return styles[i];
     }
   }
+  return undefined;
+};
 
+VT100.prototype.getTransformStyle = function(transform, scale) {
+  return scale && scale != 1.0
+    ? transform == 'filter'
+      ? 'progid:DXImageTransform.Microsoft.Matrix(' +
+                                 'M11=' + (1.0/scale) + ',M12=0,M21=0,M22=1,' +
+                                 "sizingMethod='auto expand')"
+      : 'translateX(-50%) ' +
+        'scaleX(' + (1.0/scale) + ') ' +
+        'translateX(50%)'
+    : '';
+};
+
+VT100.prototype.set80_132Mode = function(state) {
+  var transform                  = this.getTransformName();
   if (transform) {
     if ((this.console[this.currentScreen].style[transform] != '') == state) {
       return;
     }
-    var style                                         =
-      state ? transform == 'filter'
-            ? 'progid:DXImageTransform.Microsoft.Matrix(' +
-                             'M11=0.606060606060606060606,M12=0,M21=0,M22=1,' +
-                             "sizingMethod='auto expand')"
-            : 'translateX(-50%) ' +
-              'scaleX(0.606060606060606060606) ' +
-              'translateX(50%)'
-            : '';
+    var style                    = state ?
+                                   this.getTransformStyle(transform, 1.65):'';
     this.console[this.currentScreen].style[transform] = style;
-    this.cursor.style[transform]                      = style;
-    this.space.style[transform]                       = style;
-    this.scale                                        = state ? 1.65 : 1.0;
+    this.cursor.style[transform] = style;
+    this.space.style[transform]  = style;
+    this.scale                   = state ? 1.65 : 1.0;
     if (transform == 'filter') {
-      this.console[this.currentScreen].style.width    = state ? '165%' : '';
+      this.console[this.currentScreen].style.width = state ? '165%' : '';
     }
     this.resizer();
   }
diff --git a/shellinabox/keyboard-layout.html b/shellinabox/keyboard-layout.html
new file mode 100644 (file)
index 0000000..b1932f9
--- /dev/null
@@ -0,0 +1,59 @@
+<pre class="box">
+  <div>
+    <i id="27">Esc</i><i id="112">F1</i><i id="113">F2</i><i id="114">F3</i>
+     <i id="115">F4</i><i id="116">F5</i><i id="117">F6</i><i id="118">F7</i>
+     <i id="119">F8</i><i id="120">F9</i><i id="121">F10</i><i id="122">F11</i>
+     <i id="123">F12</i><br />
+    <b><span class="unshifted">`</span><span class="shifted">~</span></b>
+      <b><span class="unshifted">1</span><span class="shifted">!</span></b>
+      <b><span class="unshifted">2</span><span class="shifted">@</span></b>
+      <b><span class="unshifted">3</span><span class="shifted">#</span></b>
+      <b><span class="unshifted">4</span><span class="shifted">&#36;</span></b>
+      <b><span class="unshifted">5</span><span class="shifted">&#37;</span></b>
+      <b><span class="unshifted">6</span><span class="shifted">^</span></b>
+      <b><span class="unshifted">7</span><span class="shifted">&amp;</span></b>
+      <b><span class="unshifted">8</span><span class="shifted">*</span></b>
+      <b><span class="unshifted">9</span><span class="shifted">(</span></b>
+      <b><span class="unshifted">0</span><span class="shifted">)</span></b>
+      <b><span class="unshifted">-</span><span class="shifted">_</span></b>
+      <b><span class="unshifted">=</span><span class="shifted">+</span></b>
+      <i id="8">&nbsp;&larr;&nbsp;</i>
+      <br />
+    <i id="9">Tab</i>
+      <b>Q</b><b>W</b><b>E</b><b>R</b><b>T</b><b>Y</b><b>U</b><b>I</b><b>O</b>
+      <b>P</b>
+      <b><span class="unshifted">[</span><span class="shifted">{</span></b>
+      <b><span class="unshifted">]</span><span class="shifted">}</span></b>
+      <b><span class="unshifted">&#92;</span><span class="shifted">|</span></b>
+      <br />
+    <u>Tab&nbsp;&nbsp;</u>
+      <b>A</b><b>S</b><b>D</b><b>F</b><b>G</b><b>H</b><b>J</b><b>K</b><b>L</b>
+      <b><span class="unshifted">;</span><span class="shifted">:</span></b>
+      <b><span class="unshifted">&#39;</span><span class="shifted">"</span></b>
+      <i id="13">Enter</i>
+      <br />
+    <u>&nbsp;&nbsp;</u>
+      <i id="16">Shift</i>
+      <b>Z</b><b>X</b><b>C</b><b>V</b><b>B</b><b>N</b><b>M</b>
+      <b><span class="unshifted">,</span><span class="shifted">&lt;</span></b>
+      <b><span class="unshifted">.</span><span class="shifted">&gt;</span></b>
+      <b><span class="unshifted">/</span><span class="shifted">?</span></b>
+      <i id="16">Shift</i>
+      <br />
+    <u>XXX</u>
+      <i id="17">Ctrl</i>
+      <i id="18">Alt</i>
+      <i style="width: 25ex">&nbsp</i>
+  </div>
+  &nbsp;&nbsp;&nbsp;
+  <div>
+    <i id="45">Ins</i><i id="46">Del</i><i id="36">Home</i><i id="35">End</i>
+    <br />
+    <u>&nbsp;</u><br />
+    <u>&nbsp;</u><br />
+    <u>Ins</u><s>&nbsp;</s><b id="38">&uarr;</b><s>&nbsp;</s><u>&nbsp;</u>
+      <b id="33">&uArr;</b><br />
+    <u>Ins</u><b id="37">&larr;</b><b id="40">&darr;</b>
+      <b id="39">&rarr;</b><u>&nbsp;</u><b id="34">&dArr;</b>
+  </div>
+</pre>
diff --git a/shellinabox/keyboard.png b/shellinabox/keyboard.png
new file mode 100644 (file)
index 0000000..feef519
Binary files /dev/null and b/shellinabox/keyboard.png differ
index 523d7e0c6eeda15ea55b99804f2e83a3fa95a8c7..babce31686603013866f8f0d634cf0d2935bfb0f 100644 (file)
@@ -358,7 +358,7 @@ ShellInABox.prototype.extendContextMenu = function(entries, actions) {
 };
 
 ShellInABox.prototype.about = function() {
-  alert("Shell In A Box version " + "2.10 (revision 220)" +
+  alert("Shell In A Box version " + "2.10 (revision 221)" +
         "\nCopyright 2008-2010 by Markus Gutschke\n" +
         "For more information check http://shellinabox.com" +
         (typeof serverSupportsSSL != 'undefined' && serverSupportsSSL ?
index 0b5637714cf98e177da3e54a055454a20f2e2d14..c432ffeb6b68bffa55fa86841c2ddc0e8c1bae9a 100644 (file)
@@ -641,6 +641,11 @@ static int shellInABoxHttpHandler(HttpConnection *http, void *arg,
     extern char faviconStart[];
     extern char faviconEnd[];
     serveStaticFile(http, "image/x-icon", faviconStart, faviconEnd);
+  } else if (pathInfoLength == 12 && !memcmp(pathInfo, "keyboard.png", 11)) {
+    // Serve the keyboard icon
+    extern char keyboardStart[];
+    extern char keyboardEnd[];
+    serveStaticFile(http, "image/png", keyboardStart, keyboardEnd);
   } else if (pathInfoLength == 14 && !memcmp(pathInfo, "ShellInABox.js", 14)) {
     // Serve both vt100.js and shell_in_a_box.js in the same transaction.
     // Also, indicate to the client whether the server is SSL enabled.
index 79ad9b271ac5b1371297e4ed48a84534c6b08e26..d96855b708065bb332caa9edad92cb1cb62bc24e 100644 (file)
 #vt100 a { 
-  text-decoration:  none;
-  color:            inherit;
+  text-decoration:      none;
+  color:                inherit;
 }
 
 #vt100 a:hover { 
-  text-decoration:  underline;
+  text-decoration:      underline;
 }
 
 #vt100 #reconnect {
-  position:         absolute;
-  z-index:          2;
+  position:             absolute;
+  z-index:              2;
 }
 
 #vt100 #reconnect input { 
-  padding:          1ex;
-  font-weight:      bold;
-  font-size:        x-large;
+  padding:              1ex;
+  font-weight:          bold;
+  font-size:            x-large;
 }
 
 #vt100 #cursize {
-  background:       #EEEEEE;
-  border:           1px solid black;
-  font-family:      sans-serif;
-  font-size:        large;
-  font-weight:      bold;
-  padding:          1ex;
-  position:         absolute;
-  z-index:          2;
+  background:           #EEEEEE;
+  border:               1px solid black;
+  font-family:          sans-serif;
+  font-size:            large;
+  font-weight:          bold;
+  padding:              1ex;
+  position:             absolute;
+  z-index:              2;
 }
 
 #vt100 pre { 
-  margin:           0px;
+  margin:               0px;
 }
 
 #vt100 pre pre {
-  overflow:         hidden;
+  overflow:             hidden;
 }
 
 #vt100 #scrollable {
-  overflow-x:       hidden;
-  overflow-y:       scroll;
-  position:         relative;
-  padding:          1px;
+  overflow-x:           hidden;
+  overflow-y:           scroll;
+  position:             relative;
+  padding:              1px;
 }
 
 #vt100 #console, #vt100 #alt_console, #vt100 #cursor, #vt100 #lineheight, #vt100 .hidden pre { 
-  font-family:      "DejaVu Sans Mono", "Everson Mono", FreeMono, "Andale Mono", "Lucida Console", monospace;
+  font-family:          "DejaVu Sans Mono", "Everson Mono", FreeMono, "Andale Mono", "Lucida Console", monospace;
 }
 
 #vt100 #lineheight { 
-  position:         absolute;
-  visibility:       hidden;
+  position:             absolute;
+  visibility:           hidden;
 }
 
 #vt100 #cursor {
-  position:         absolute;
-  left:             0px;
-  top:              0px;
-  overflow:         hidden;
-  z-index:          1;
+  position:             absolute;
+  left:                 0px;
+  top:                  0px;
+  overflow:             hidden;
+  z-index:              1;
 }
 
 #vt100 #cursor.bright { 
-  background-color: #e60000;
-  color:            white;
+  background-color:     #e60000;
+  color:                white;
 }
 
 #vt100 #cursor.dim {
-  visibility:       hidden;
+  visibility:           hidden;
 }
 
 #vt100 #cursor.inactive {
-  border:           1px solid #e60000;
-  margin:           -1px;
+  border:               1px solid #e60000;
+  margin:               -1px;
 }
 
 #vt100 #padding { 
-  visibility:       hidden;
-  width:            1px;
-  height:           0px;
-  overflow:         hidden;
+  visibility:           hidden;
+  width:                1px;
+  height:               0px;
+  overflow:             hidden;
 }
 
 #vt100 .hidden {
-  position:         absolute;
-  top:              -10000px;
-  left:             -10000px;
-  width:            0px;
-  height:           0px;
+  position:             absolute;
+  top:                  -10000px;
+  left:                 -10000px;
+  width:                0px;
+  height:               0px;
 }
 
 #vt100 #menu { 
-  overflow:         visible;
-  position:         absolute;
-  z-index:          3;
+  overflow:             visible;
+  position:             absolute;
+  z-index:              3;
 }
 
 #vt100 #menu .popup {
-  background-color: #EEEEEE;
-  border:           1px solid black;
-  font-family:      sans-serif;
-  position:         absolute;
+  background-color:     #EEEEEE;
+  border:               1px solid black;
+  font-family:          sans-serif;
+  position:             absolute;
 }
 
 #vt100 #menu .popup ul { 
-  list-style-type:  none;
-  padding:          0px;
-  margin:           0px;
-  min-width:        10em;
+  list-style-type:      none;
+  padding:              0px;
+  margin:               0px;
+  min-width:            10em;
 }
 
 #vt100 #menu .popup li { 
-  padding:          3px 0.5ex 3px 0.5ex;
+  padding:              3px 0.5ex 3px 0.5ex;
 }
 
 #vt100 #menu .popup li.hover {
-  background-color: #444444;
-  color:            white;
+  background-color:     #444444;
+  color:                white;
 }
 
 #vt100 #menu .popup li.disabled {
-  color:            #AAAAAA;
+  color:                #AAAAAA;
 }
 
 #vt100 #menu .popup hr { 
-  margin:           0.5ex 0px 0.5ex 0px;
+  margin:               0.5ex 0px 0.5ex 0px;
 }
 
 #vt100 #menu img { 
-  margin-right:     0.5ex;
-  width:            1ex;
-  height:           1ex;
+  margin-right:         0.5ex;
+  width:                1ex;
+  height:               1ex;
 }
 
 #vt100 #scrollable.inverted { color:            #ffffff;
                               background-color: #000000; }
+
+#vt100 #kbd_button { 
+  float:                left;
+  position:             fixed;
+  z-index:              0;
+  visibility:           hidden;
+}
+
+#vt100 #keyboard {
+  z-index:              3;
+  position:             absolute;
+}
+
+#vt100 #keyboard .box {
+  font-family:          sans-serif;
+  background-color:     #cccccc;
+  padding:              .8em;
+  float:                left;
+  position:             absolute;
+  border-radius:        10px;
+  -moz-border-radius:   10px;
+  box-shadow:           4px 4px 6px #222222;
+  -webkit-box-shadow:   4px 4px 6px #222222;
+  /* Don't set the -moz-box-shadow. It doesn't properly scale when CSS
+   * transforms are in effect. Once Firefox supports box-shadow, it should
+   * automatically do the right thing. Until then, leave shadows disabled
+   * for Firefox.
+   */
+  opacity:              0.85;
+  -moz-opacity:         0.85;
+  filter:               alpha(opacity=85);
+}
+
+#vt100 #keyboard .box * {
+  vertical-align:       top;
+  display:              inline-block;
+}
+
+#vt100 #keyboard b, #vt100 #keyboard i, #vt100 #keyboard s, #vt100 #keyboard u {
+  font-style:           normal;
+  font-weight:          bold;
+  border-radius:        5px;
+  -moz-border-radius:   5px;
+  background-color:     #555555;
+  color:                #eeeeee;
+  box-shadow:           2px 2px 3px #222222;
+  -webkit-box-shadow:   2px 2px 3px #222222;
+  padding:              4px;
+  margin:               2px;
+  height:               2ex;
+  display:              inline-block;
+  text-align:           center;
+  text-decoration:      none;
+}
+
+#vt100 #keyboard b, #vt100 #keyboard s {
+  width:                2ex;
+}
+
+#vt100 #keyboard u, #vt100 #keyboard s {
+  visibility:           hidden;
+}
+
+#vt100 #keyboard .shifted { 
+  display:              none;
+}
+
+#vt100 #keyboard .selected {
+  color:                #888888;
+  background-color:     #eeeeee;
+  box-shadow:           0px 0px 3px #222222;
+  -webkit-box-shadow:   0px 0px 3px #222222;
+  position:             relative;
+  top:                  1px;
+  left:                 1px;
+}
+
 [if DEFINES_COLORS]
 /* IE cannot properly handle "inherit" properties. So, the monochrome.css/
  * color.css style sheets cannot work, if we define colors in styles.css.
 
 @media print {
   #vt100 .scrollback {
-    display:        none;
+    display:            none;
   }
 
-  #vt100 #reconnect, #vt100 #cursor, #vt100 #menu { 
-    visibility:     hidden;
+  #vt100 #reconnect, #vt100 #cursor, #vt100 #menu, #vt100 #kbd_button, #vt100 #keyboard { 
+    visibility:         hidden;
   }
 
   #vt100 #scrollable { 
-    overflow:       hidden;
+    overflow:           hidden;
   }
 
   #vt100 #console, #vt100 #alt_console { 
-    overflow:       hidden;
-    width:          1000000ex;
+    overflow:           hidden;
+    width:              1000000ex;
   }
 }
index f9345b9d3952ac5e769e779f1441b51ff13e8200..5457b612a4c32416f13e0d3dcc5fff7aa72a20ea 100644 (file)
@@ -238,22 +238,16 @@ VT100.prototype.reset = function(clearHistory) {
   this.enableAlternateScreen(false);
 
   var wasCompressed                                  = false;
-  var styles                                         = [ 'transform',
-                                                         'WebkitTransform',
-                                                         'MozTransform',
-                                                         'filter' ];
-  for (var i = 0; i < styles.length; ++i) {
-    if (typeof this.console[0].style[styles[i]] != 'undefined') {
-      for (var j = 0; j < 1; ++j) {
-        wasCompressed                |= this.console[j].style[styles[i]] != '';
-        this.console[j].style[styles[i]]             = '';
-      }
-      this.cursor.style[styles[i]]                   = '';
-      this.space.style[styles[i]]                    = '';
-      if (styles[i] == 'filter') {
-        this.console[this.currentScreen].style.width = '';
-      }
-      break;
+  var transform                                      = this.getTransformName();
+  if (transform) {
+    for (var i = 0; i < 2; ++i) {
+      wasCompressed                  |= this.console[i].style[transform] != '';
+      this.console[i].style[transform]               = '';
+    }
+    this.cursor.style[transform]                     = '';
+    this.space.style[transform]                      = '';
+    if (transform == 'filter') {
+      this.console[this.currentScreen].style.width   = '';
     }
   }
   this.scale                                         = 1.0;
@@ -270,10 +264,13 @@ VT100.prototype.reset = function(clearHistory) {
 };
 
 VT100.prototype.addListener = function(elem, event, listener) {
-  if (elem.addEventListener) {
-    elem.addEventListener(event, listener, false);
-  } else {
-    elem.attachEvent('on' + event, listener);
+  try {
+    if (elem.addEventListener) {
+      elem.addEventListener(event, listener, false);
+    } else {
+      elem.attachEvent('on' + event, listener);
+    }
+  } catch (e) {
   }
 };
 
@@ -281,11 +278,12 @@ VT100.prototype.getUserSettings = function() {
   // Compute hash signature to identify the entries in the userCSS menu.
   // If the menu is unchanged from last time, default values can be
   // looked up in a cookie associated with this page.
-  this.signature            = 2;
+  this.signature            = 3;
   this.utfPreferred         = true;
   this.visualBell           = typeof suppressAllAudio != 'undefined' &&
                               suppressAllAudio;
   this.autoprint            = true;
+  this.softKeyboard         = false;
   this.blinkingCursor       = true;
   if (this.visualBell) {
     this.signature          = Math.floor(16807*this.signature + 1) %
@@ -311,15 +309,16 @@ VT100.prototype.getUserSettings = function() {
   if (settings >= 0) {
     settings                = document.cookie.substr(settings + key.length).
                                                    replace(/([0-1]*).*/, "$1");
-    if (settings.length == 3 + (typeof userCSSList == 'undefined' ?
+    if (settings.length == 5 + (typeof userCSSList == 'undefined' ?
                                 0 : userCSSList.length)) {
       this.utfPreferred     = settings.charAt(0) != '0';
       this.visualBell       = settings.charAt(1) != '0';
       this.autoprint        = settings.charAt(2) != '0';
-      this.blinkingCursor   = settings.charAt(3) != '0';
+      this.softKeyboard     = settings.charAt(3) != '0';
+      this.blinkingCursor   = settings.charAt(4) != '0';
       if (typeof userCSSList != 'undefined') {
         for (var i = 0; i < userCSSList.length; ++i) {
-          userCSSList[i][2] = settings.charAt(i + 3) != '0';
+          userCSSList[i][2] = settings.charAt(i + 5) != '0';
         }
       }
     }
@@ -332,6 +331,7 @@ VT100.prototype.storeUserSettings = function() {
                   (this.utfEnabled     ? '1' : '0') +
                   (this.visualBell     ? '1' : '0') +
                   (this.autoprint      ? '1' : '0') +
+                  (this.softKeyboard   ? '1' : '0') +
                   (this.blinkingCursor ? '1' : '0');
   if (typeof userCSSList != 'undefined') {
     for (var i = 0; i < userCSSList.length; ++i) {
@@ -413,7 +413,7 @@ VT100.prototype.initializeUserCSSStyles = function() {
                         label.textContent= label.textContent;
                       }
 
-                      // User style sheets are number sequentially
+                      // User style sheets are numbered sequentially
                       var sheet          = document.getElementById(
                                                                'usercss-' + i);
                       if (i == current) {
@@ -470,6 +470,328 @@ VT100.prototype.initializeUserCSSStyles = function() {
   }
 };
 
+VT100.prototype.resetLastSelectedKey = function(e) {
+  var key                          = this.lastSelectedKey;
+  if (!key) {
+    return false;
+  }
+
+  var position                     = this.mousePosition(e);
+
+  // We don't get all the necessary events to reliably reselect a key
+  // if we moved away from it and then back onto it. We approximate the
+  // behavior by remembering the key until either we release the mouse
+  // button (we might never get this event if the mouse has since left
+  // the window), or until we move away too far.
+  var box                          = this.keyboard.firstChild;
+  if (position[0] <  box.offsetLeft + key.offsetWidth ||
+      position[1] <  box.offsetTop + key.offsetHeight ||
+      position[0] >= box.offsetLeft + box.offsetWidth - key.offsetWidth ||
+      position[1] >= box.offsetTop + box.offsetHeight - key.offsetHeight ||
+      position[0] <  box.offsetLeft + key.offsetLeft - key.offsetWidth ||
+      position[1] <  box.offsetTop + key.offsetTop - key.offsetHeight ||
+      position[0] >= box.offsetLeft + key.offsetLeft + 2*key.offsetWidth ||
+      position[1] >= box.offsetTop + key.offsetTop + 2*key.offsetHeight) {
+    if (this.lastSelectedKey.className) log.console('reset: deselecting');
+    this.lastSelectedKey.className = '';
+    this.lastSelectedKey           = undefined;
+  }
+  return false;
+};
+
+VT100.prototype.showShiftState = function(state) {
+  var style              = document.getElementById('shift_state');
+  if (state) {
+    this.setTextContentRaw(style,
+                           '#vt100 #keyboard .shifted {' +
+                             'display: inline }' +
+                           '#vt100 #keyboard .unshifted {' +
+                             'display: none }');
+  } else {
+    this.setTextContentRaw(style, '');
+  }
+  var elems              = this.keyboard.getElementsByTagName('I');
+  for (var i = 0; i < elems.length; ++i) {
+    if (elems[i].id == '16') {
+      elems[i].className = state ? 'selected' : '';
+    }
+  }
+};
+
+VT100.prototype.showCtrlState = function(state) {
+  var ctrl         = this.getChildById(this.keyboard, '17' /* Ctrl */);
+  if (ctrl) {
+    ctrl.className = state ? 'selected' : '';
+  }
+};
+
+VT100.prototype.showAltState = function(state) {
+  var alt         = this.getChildById(this.keyboard, '18' /* Alt */);
+  if (alt) {
+    alt.className = state ? 'selected' : '';
+  }
+};
+
+VT100.prototype.clickedKeyboard = function(e, elem, ch, key, shift, ctrl, alt){
+  var fake      = [ ];
+  fake.charCode = ch;
+  fake.keyCode  = key;
+  fake.ctrlKey  = ctrl;
+  fake.shiftKey = shift;
+  fake.altKey   = alt;
+  fake.metaKey  = alt;
+  return this.handleKey(fake);
+};
+
+VT100.prototype.addKeyBinding = function(elem, ch, key, CH, KEY) {
+  if (elem == undefined) {
+    return;
+  }
+  if (ch == '\u00A0') {
+    // &nbsp; should be treated as a regular space character.
+    ch                                  = ' ';
+  }
+  if (ch != undefined && CH == undefined) {
+    // For letter keys, we automatically compute the uppercase character code
+    // from the lowercase one.
+    CH                                  = ch.toUpperCase();
+  }
+  if (KEY == undefined && key != undefined) {
+    // Most keys have identically key codes for both lowercase and uppercase
+    // keypresses. Normally, only function keys would have distinct key codes,
+    // whereas regular keys have character codes.
+    KEY                                 = key;
+  } else if (KEY == undefined && CH != undefined) {
+    // For regular keys, copy the character code to the key code.
+    KEY                                 = CH.charCodeAt(0);
+  }
+  if (key == undefined && ch != undefined) {
+    // For regular keys, copy the character code to the key code.
+    key                                 = ch.charCodeAt(0);
+  }
+  // Convert characters to numeric character codes. If the character code
+  // is undefined (i.e. this is a function key), set it to zero.
+  ch                                    = ch ? ch.charCodeAt(0) : 0;
+  CH                                    = CH ? CH.charCodeAt(0) : 0;
+
+  // Mouse down events high light the key. We also set lastSelectedKey. This
+  // is needed to that mouseout/mouseover can keep track of the key that
+  // is currently being clicked.
+  this.addListener(elem, 'mousedown',
+    function(vt100, elem, key) { return function(e) {
+      if ((e.which || e.button) == 1) {
+        if (vt100.lastSelectedKey) {       
+          vt100.lastSelectedKey.className= '';
+        }
+        // Highlight the key while the mouse button is held down.
+        if (key == 16 /* Shift */) {
+          if (!elem.className != vt100.isShift) {
+            vt100.showShiftState(!vt100.isShift);
+          }
+        } else if (key == 17 /* Ctrl */) {
+          if (!elem.className != vt100.isCtrl) {
+            vt100.showCtrlState(!vt100.isCtrl);
+          }
+        } else if (key == 18 /* Alt */) {
+          if (!elem.className != vt100.isAlt) {
+            vt100.showAltState(!vt100.isAlt);
+          }
+        } else {
+          elem.className                  = 'selected';
+        }
+        vt100.lastSelectedKey             = elem;
+      }
+      return false; }; }(this, elem, key));
+  var clicked                           =
+    // Modifier keys update the state of the keyboard, but do not generate
+    // any key clicks that get forwarded to the application.
+    key >= 16 /* Shift */ && key <= 18 /* Alt */ ?
+    function(vt100, elem) { return function(e) {
+      if (elem == vt100.lastSelectedKey) {
+        if (key == 16 /* Shift */) {
+          // The user clicked the Shift key
+          vt100.isShift                 = !vt100.isShift;
+          vt100.showShiftState(vt100.isShift);
+        } else if (key == 17 /* Ctrl */) {
+          vt100.isCtrl                  = !vt100.isCtrl;
+          vt100.showCtrlState(vt100.isCtrl);
+        } else if (key == 18 /* Alt */) {
+          vt100.isAlt                   = !vt100.isAlt;
+          vt100.showAltState(vt100.isAlt);
+        }
+        vt100.lastSelectedKey           = undefined;
+      }
+      if (vt100.lastSelectedKey) {
+        vt100.lastSelectedKey.className = '';
+        vt100.lastSelectedKey           = undefined;
+      }
+      return false; }; }(this, elem) :
+    // Regular keys generate key clicks, when the mouse button is released or
+    // when a mouse click event is received.
+    function(vt100, elem, ch, key, CH, KEY) { return function(e) {
+      if (vt100.lastSelectedKey) {
+        if (elem == vt100.lastSelectedKey) {
+          // The user clicked a key.
+          if (vt100.isShift) {
+            vt100.clickedKeyboard(e, elem, CH, KEY,
+                                  true, vt100.isCtrl, vt100.isAlt);
+          } else {
+            vt100.clickedKeyboard(e, elem, ch, key,
+                                  false, vt100.isCtrl, vt100.isAlt);
+          }
+          vt100.isShift                 = false;
+          vt100.showShiftState(false);
+          vt100.isCtrl                  = false;
+          vt100.showCtrlState(false);
+          vt100.isAlt                   = false;
+          vt100.showAltState(false);
+        }
+        vt100.lastSelectedKey.className = '';
+        vt100.lastSelectedKey           = undefined;
+      }
+      elem.className                    = '';
+      return false; }; }(this, elem, ch, key, CH, KEY);
+  this.addListener(elem, 'mouseup', clicked);
+  this.addListener(elem, 'click', clicked);
+
+  // When moving the mouse away from a key, check if any keys need to be
+  // deselected.
+  this.addListener(elem, 'mouseout',
+    function(vt100, elem, key) { return function(e) {
+      if (key == 16 /* Shift */) {
+        if (!elem.className == vt100.isShift) {
+          vt100.showShiftState(vt100.isShift);
+        }
+      } else if (key == 17 /* Ctrl */) {
+        if (!elem.className == vt100.isCtrl) {
+          vt100.showCtrlState(vt100.isCtrl);
+        }
+      } else if (key == 18 /* Alt */) {
+        if (!elem.className == vt100.isAlt) {
+          vt100.showAltState(vt100.isAlt);
+        }
+      } else if (elem.className) {
+        elem.className                  = '';
+        vt100.lastSelectedKey           = elem;
+      } else if (vt100.lastSelectedKey) {
+        vt100.resetLastSelectedKey(e);
+      }
+      return false; }; }(this, elem, key));
+
+  // When moving the mouse over a key, select it if the user is still holding
+  // the mouse button down (i.e. elem == lastSelectedKey)
+  this.addListener(elem, 'mouseover',
+    function(vt100, elem, key) { return function(e) {
+      if (elem == vt100.lastSelectedKey) {
+        if (key == 16 /* Shift */) {
+          if (!elem.className != vt100.isShift) {
+            vt100.showShiftState(!vt100.isShift);
+          }
+        } else if (key == 17 /* Ctrl */) {
+          if (!elem.className != vt100.isCtrl) {
+            vt100.showCtrlState(!vt100.isCtrl);
+          }
+        } else if (key == 18 /* Alt */) {
+          if (!elem.className != vt100.isAlt) {
+            vt100.showAltState(!vt100.isAlt);
+          }
+        } else if (!elem.className) {
+          elem.className                = 'selected';
+        }
+      } else {
+        vt100.resetLastSelectedKey(e);
+      }
+      return false; }; }(this, elem, key));
+};
+
+VT100.prototype.initializeKeyBindings = function(elem) {
+  if (elem) {
+    if (elem.nodeName == "I" || elem.nodeName == "B") {
+      if (elem.id) {
+        // Function keys. The Javascript keycode is part of the "id"
+        var i     = parseInt(elem.id);
+        if (i) {
+          // If the id does not parse as a number, it is not a keycode.
+          this.addKeyBinding(elem, undefined, i);
+        }
+      } else {
+        var child = elem.firstChild;
+        if (child.nodeName == "#text") {
+          // If the key only has a text node as a child, then it is a letter.
+          // Automatically compute the lower and upper case version of the key.
+          this.addKeyBinding(elem, this.getTextContent(child).toLowerCase());
+        } else {
+          // If the key has two children, they are the lower and upper case
+          // character code, respectively.
+          this.addKeyBinding(elem, this.getTextContent(child), undefined,
+                             this.getTextContent(child.nextSibling));
+        }
+      }
+    }
+  }
+  // Recursively parse all other child nodes.
+  for (elem = elem.firstChild; elem; elem = elem.nextSibling) {
+    this.initializeKeyBindings(elem);
+  }
+};
+
+VT100.prototype.initializeKeyboard = function() {
+  // Configure mouse event handlers for button that displays/hides keyboard
+  var box                               = this.keyboard.firstChild;
+  this.hideSoftKeyboard();
+  this.addListener(this.keyboardImage, 'click',
+    function(vt100) { return function(e) {
+      if (vt100.keyboard.style.display != '') {
+        if (vt100.reconnectBtn.style.visibility != '') {
+          vt100.showSoftKeyboard();
+        }
+      } else {
+        vt100.hideSoftKeyboard();
+        vt100.input.focus();
+      }
+      return false; }; }(this));
+
+  // Enable button that displays keyboard
+  if (this.softKeyboard) {
+    this.keyboardImage.style.visibility = 'visible';
+  }
+
+  // Configure mouse event handlers for on-screen keyboard
+  this.addListener(this.keyboard, 'click',
+    function(vt100) { return function(e) {
+      vt100.hideSoftKeyboard();
+      vt100.input.focus();
+      return false; }; }(this));
+  this.addListener(this.keyboard, 'selectstart', this.cancelEvent);
+  this.addListener(box, 'click', this.cancelEvent);
+  this.addListener(box, 'mouseup',
+    function(vt100) { return function(e) {
+      if (vt100.lastSelectedKey) {
+        vt100.lastSelectedKey.className = '';
+        vt100.lastSelectedKey           = undefined;
+      }
+      return false; }; }(this));
+  this.addListener(box, 'mouseout',
+    function(vt100) { return function(e) {
+      return vt100.resetLastSelectedKey(e); }; }(this));
+  this.addListener(box, 'mouseover',
+    function(vt100) { return function(e) {
+      return vt100.resetLastSelectedKey(e); }; }(this));
+
+  // Configure SHIFT key behavior
+  var style                             = document.createElement('style');
+  var id                                = document.createAttribute('id');
+  id.nodeValue                          = 'shift_state';
+  style.setAttributeNode(id);
+  var type                              = document.createAttribute('type');
+  type.nodeValue                        = 'text/css';
+  style.setAttributeNode(type);
+  document.getElementsByTagName('head')[0].appendChild(style);
+
+  // Set up key bindings
+  this.initializeKeyBindings(box);
+};
+
 VT100.prototype.initializeElements = function(container) {
   // If the necessary objects have not already been defined in the HTML
   // page, create them now.
@@ -483,6 +805,9 @@ VT100.prototype.initializeElements = function(container) {
 
   if (!this.getChildById(this.container, 'reconnect')   ||
       !this.getChildById(this.container, 'menu')        ||
+      !this.getChildById(this.container, 'keyboard')    ||
+      !this.getChildById(this.container, 'kbd_button')  ||
+      !this.getChildById(this.container, 'kbd_img')     ||
       !this.getChildById(this.container, 'scrollable')  ||
       !this.getChildById(this.container, 'console')     ||
       !this.getChildById(this.container, 'alt_console') ||
@@ -525,7 +850,15 @@ VT100.prototype.initializeElements = function(container) {
                        '<div id="cursize" style="visibility: hidden">' +
                        '</div>' +
                        '<div id="menu"></div>' +
+                       '<div id="keyboard" unselectable="on">' +
+                         '<pre class="box"><div><i id="27">Esc</i><i id="112">F1</i><i id="113">F2</i><i id="114">F3</i><i id="115">F4</i><i id="116">F5</i><i id="117">F6</i><i id="118">F7</i><i id="119">F8</i><i id="120">F9</i><i id="121">F10</i><i id="122">F11</i><i id="123">F12</i><br /><b><span class="unshifted">`</span><span class="shifted">~</span></b><b><span class="unshifted">1</span><span class="shifted">!</span></b><b><span class="unshifted">2</span><span class="shifted">@</span></b><b><span class="unshifted">3</span><span class="shifted">#</span></b><b><span class="unshifted">4</span><span class="shifted">&#36;</span></b><b><span class="unshifted">5</span><span class="shifted">&#37;</span></b><b><span class="unshifted">6</span><span class="shifted">^</span></b><b><span class="unshifted">7</span><span class="shifted">&amp;</span></b><b><span class="unshifted">8</span><span class="shifted">*</span></b><b><span class="unshifted">9</span><span class="shifted">(</span></b><b><span class="unshifted">0</span><span class="shifted">)</span></b><b><span class="unshifted">-</span><span class="shifted">_</span></b><b><span class="unshifted">=</span><span class="shifted">+</span></b><i id="8">&nbsp;&larr;&nbsp;</i><br /><i id="9">Tab</i><b>Q</b><b>W</b><b>E</b><b>R</b><b>T</b><b>Y</b><b>U</b><b>I</b><b>O</b><b>P</b><b><span class="unshifted">[</span><span class="shifted">{</span></b><b><span class="unshifted">]</span><span class="shifted">}</span></b><b><span class="unshifted">&#92;</span><span class="shifted">|</span></b><br /><u>Tab&nbsp;&nbsp;</u><b>A</b><b>S</b><b>D</b><b>F</b><b>G</b><b>H</b><b>J</b><b>K</b><b>L</b><b><span class="unshifted">;</span><span class="shifted">:</span></b><b><span class="unshifted">&#39;</span><span class="shifted">"</span></b><i id="13">Enter</i><br /><u>&nbsp;&nbsp;</u><i id="16">Shift</i><b>Z</b><b>X</b><b>C</b><b>V</b><b>B</b><b>N</b><b>M</b><b><span class="unshifted">,</span><span class="shifted">&lt;</span></b><b><span class="unshifted">.</span><span class="shifted">&gt;</span></b><b><span class="unshifted">/</span><span class="shifted">?</span></b><i id="16">Shift</i><br /><u>XXX</u><i id="17">Ctrl</i><i id="18">Alt</i><i style="width: 25ex">&nbsp</i></div>&nbsp;&nbsp;&nbsp;<div><i id="45">Ins</i><i id="46">Del</i><i id="36">Home</i><i id="35">End</i><br /><u>&nbsp;</u><br /><u>&nbsp;</u><br /><u>Ins</u><s>&nbsp;</s><b id="38">&uarr;</b><s>&nbsp;</s><u>&nbsp;</u><b id="33">&uArr;</b><br /><u>Ins</u><b id="37">&larr;</b><b id="40">&darr;</b><b id="39">&rarr;</b><u>&nbsp;</u><b id="34">&dArr;</b></div></pre>' +
+                       '</div>' +
                        '<div id="scrollable">' +
+                         '<table id="kbd_button">' +
+                           '<tr><td width="100%">&nbsp;</td>' +
+                           '<td><img id="kbd_img" src="keyboard.png" /></td>' +
+                           '<td>&nbsp;&nbsp;&nbsp;&nbsp;</td></tr>' +
+                         '</table>' +
                          '<pre id="lineheight">&nbsp;</pre>' +
                          '<pre id="console">' +
                            '<pre></pre>' +
@@ -566,6 +899,8 @@ VT100.prototype.initializeElements = function(container) {
   this.reconnectBtn            = this.getChildById(this.container,'reconnect');
   this.curSizeBox              = this.getChildById(this.container, 'cursize');
   this.menu                    = this.getChildById(this.container, 'menu');
+  this.keyboard                = this.getChildById(this.container, 'keyboard');
+  this.keyboardImage           = this.getChildById(this.container, 'kbd_img');
   this.scrollable              = this.getChildById(this.container,
                                                                  'scrollable');
   this.lineheight              = this.getChildById(this.container,
@@ -646,6 +981,9 @@ VT100.prototype.initializeElements = function(container) {
   // Hide context menu
   this.hideContextMenu();
 
+  // Set up onscreen soft keyboard
+  this.initializeKeyboard();
+
   // Add listener to reconnect button
   this.addListener(this.reconnectBtn.firstChild, 'click',
                    function(vt100) {
@@ -733,6 +1071,7 @@ VT100.prototype.reconnect = function() {
 
 VT100.prototype.showReconnect = function(state) {
   if (state) {
+    this.hideSoftKeyboard();
     this.reconnectBtn.style.visibility = '';
   } else {
     this.reconnectBtn.style.visibility = 'hidden';
@@ -766,6 +1105,9 @@ VT100.prototype.resized = function(w, h) {
 };
 
 VT100.prototype.resizer = function() {
+  // Hide onscreen soft keyboard
+  this.hideSoftKeyboard();
+
   // The cursor can get corrupted if the print-preview is displayed in Firefox.
   // Recreating it, will repair it.
   var newCursor                = document.createElement('pre');
@@ -945,6 +1287,17 @@ VT100.prototype.cancelEvent = function(event) {
   return false;
 };
 
+VT100.prototype.mousePosition = function(event) {
+  var offsetX      = this.container.offsetLeft;
+  var offsetY      = this.container.offsetTop;
+  for (var e = this.container; e = e.offsetParent; ) {
+    offsetX       += e.offsetLeft;
+    offsetY       += e.offsetTop;
+  }
+  return [ event.clientX - offsetX,
+           event.clientY - offsetY ];
+};
+
 VT100.prototype.mouseEvent = function(event, type) {
   // If any text is currently selected, do not move the focus as that would
   // invalidate the selection.
@@ -954,15 +1307,10 @@ VT100.prototype.mouseEvent = function(event, type) {
   }
 
   // Compute mouse position in characters.
-  var offsetX      = this.container.offsetLeft;
-  var offsetY      = this.container.offsetTop;
-  for (var e = this.container; e = e.offsetParent; ) {
-    offsetX       += e.offsetLeft;
-    offsetY       += e.offsetTop;
-  }
-  var x            = (event.clientX - offsetX) / this.cursorWidth;
-  var y            = ((event.clientY - offsetY) + this.scrollable.offsetTop) /
-                     this.cursorHeight - this.numScrollbackLines;
+  var position     = this.mousePosition(event);
+  var x            = Math.floor(position[0] / this.cursorWidth);
+  var y            = Math.floor((position[1] + this.scrollable.scrollTop) /
+                                this.cursorHeight) - this.numScrollbackLines;
   var inside       = true;
   if (x >= this.terminalWidth) {
     x              = this.terminalWidth - 1;
@@ -1022,7 +1370,7 @@ VT100.prototype.mouseEvent = function(event, type) {
   // Bring up context menu.
   if (button == 2 && !event.shiftKey) {
     if (type == 0 /* MOUSE_DOWN */) {
-      this.showContextMenu(event.clientX - offsetX, event.clientY - offsetY);
+      this.showContextMenu(position[0], position[1]);
     }
     return this.cancelEvent(event);
   }
@@ -1058,6 +1406,29 @@ VT100.prototype.getTextContent = function(elem) {
          (typeof elem.textContent == 'undefined' ? elem.innerText : '');
 };
 
+VT100.prototype.setTextContentRaw = function(elem, s) {
+  // Updating the content of an element is an expensive operation. It actually
+  // pays off to first check whether the element is still unchanged.
+  if (typeof elem.textContent == 'undefined') {
+    if (elem.innerText != s) {
+      try {
+        elem.innerText = s;
+      } catch (e) {
+        // Very old versions of IE do not allow setting innerText. Instead,
+        // remove all children, by setting innerHTML and then set the text
+        // using DOM methods.
+        elem.innerHTML = '';
+        elem.appendChild(document.createTextNode(
+                                          this.replaceChar(s, ' ', '\u00A0')));
+      }
+    }
+  } else {
+    if (elem.textContent != s) {
+      elem.textContent = s;
+    }
+  }
+};
+
 VT100.prototype.setTextContent = function(elem, s) {
   // Check if we find any URLs in the text. If so, automatically convert them
   // to links.
@@ -1103,26 +1474,7 @@ VT100.prototype.setTextContent = function(elem, s) {
     return;
   }
 
-  // Updating the content of an element is an expensive operation. It actually
-  // pays off to first check whether the element is still unchanged.
-  if (typeof elem.textContent == 'undefined') {
-    if (elem.innerText != s) {
-      try {
-        elem.innerText = s;
-      } catch (e) {
-        // Very old versions of IE do not allow setting innerText. Instead,
-        // remove all children, by setting innerHTML and then set the text
-        // using DOM methods.
-        elem.innerHTML = '';
-        elem.appendChild(document.createTextNode(
-                                          this.replaceChar(s, ' ', '\u00A0')));
-      }
-    }
-  } else {
-    if (elem.textContent != s) {
-      elem.textContent = s;
-    }
-  }
+  this.setTextContentRaw(elem, s);
 };
 
 VT100.prototype.insertBlankLine = function(y, color, style) {
@@ -1578,27 +1930,21 @@ VT100.prototype.enableAlternateScreen = function(state) {
   this.console[this.currentScreen].style.display     = '';
 
   // Select appropriate character pitch.
-  var styles                                         = [ 'transform',
-                                                         'WebkitTransform',
-                                                         'MozTransform',
-                                                         'filter' ];
-  for (var i = 0; i < styles.length; ++i) {
-    if (typeof this.console[0].style[styles[i]] != 'undefined') {
-      if (state) {
-        // Upon enabling the alternate screen, we switch to 80 column mode. But
-        // upon returning to the regular screen, we restore the mode that was
-        // in effect previously.
-        this.console[1].style[styles[i]]             = '';
-      }
-      var style                                      =
-                             this.console[this.currentScreen].style[styles[i]];
-      this.cursor.style[styles[i]]                   = style;
-      this.space.style[styles[i]]                    = style;
-      this.scale                                     = style == '' ? 1.0:1.65;
-      if (styles[i] == 'filter') {
-        this.console[this.currentScreen].style.width = style == '' ? '165%':'';
-      }
-      break;
+  var transform                                      = this.getTransformName();
+  if (transform) {
+    if (state) {
+      // Upon enabling the alternate screen, we switch to 80 column mode. But
+      // upon returning to the regular screen, we restore the mode that was
+      // in effect previously.
+      this.console[1].style[transform]               = '';
+    }
+    var style                                        =
+                             this.console[this.currentScreen].style[transform];
+    this.cursor.style[transform]                     = style;
+    this.space.style[transform]                      = style;
+    this.scale                                       = style == '' ? 1.0:1.65;
+    if (transform == 'filter') {
+       this.console[this.currentScreen].style.width  = style == '' ? '165%':'';
     }
   }
   this.resizer();
@@ -1969,12 +2315,76 @@ VT100.prototype.toggleBell = function() {
   this.visualBell = !this.visualBell;
 };
 
+VT100.prototype.toggleSoftKeyboard = function() {
+  this.softKeyboard = !this.softKeyboard;
+  this.keyboardImage.style.visibility = this.softKeyboard ? 'visible' : '';
+};
+
+VT100.prototype.deselectKeys = function(elem) {
+  if (elem && elem.className == 'selected') {
+    elem.className = '';
+  }
+  for (elem = elem.firstChild; elem; elem = elem.nextSibling) {
+    this.deselectKeys(elem);
+  }
+};
+
+VT100.prototype.showSoftKeyboard = function() {
+  // Make sure no key is currently selected
+  this.lastSelectedKey           = undefined;
+  this.deselectKeys(this.keyboard);
+  this.isShift                   = false;
+  this.showShiftState(false);
+  this.isCtrl                    = false;
+  this.showCtrlState(false);
+  this.isAlt                     = false;
+  this.showAltState(false);
+
+  this.keyboard.style.left       = '0px';
+  this.keyboard.style.top        = '0px';
+  this.keyboard.style.width      = this.container.offsetWidth  + 'px';
+  this.keyboard.style.height     = this.container.offsetHeight + 'px';
+  this.keyboard.style.visibility = 'hidden';
+  this.keyboard.style.display    = '';
+
+  var kbd                        = this.keyboard.firstChild;
+  var scale                      = 1.0;
+  var transform                  = this.getTransformName();
+  if (transform) {
+    kbd.style[transform]         = '';
+    if (kbd.offsetWidth > 0.9 * this.container.offsetWidth) {
+      scale                      = (kbd.offsetWidth/
+                                    this.container.offsetWidth)/0.9;
+    }
+    if (kbd.offsetHeight > 0.9 * this.container.offsetHeight) {
+      scale                      = Math.max((kbd.offsetHeight/
+                                             this.container.offsetHeight)/0.9);
+    }
+    var style                    = this.getTransformStyle(transform,
+                                              scale > 1.0 ? scale : undefined);
+    kbd.style[transform]         = style;
+  }
+  if (transform == 'filter') {
+    scale                        = 1.0;
+  }
+  kbd.style.left                 = ((this.container.offsetWidth -
+                                     kbd.offsetWidth/scale)/2) + 'px';
+  kbd.style.top                  = ((this.container.offsetHeight -
+                                     kbd.offsetHeight/scale)/2) + 'px';
+
+  this.keyboard.style.visibility = 'visible';
+};
+
+VT100.prototype.hideSoftKeyboard = function() {
+  this.keyboard.style.display    = 'none';
+};
+
 VT100.prototype.toggleCursorBlinking = function() {
   this.blinkingCursor = !this.blinkingCursor;
 };
 
 VT100.prototype.about = function() {
-  alert("VT100 Terminal Emulator " + "2.10 (revision 220)" +
+  alert("VT100 Terminal Emulator " + "2.10 (revision 221)" +
         "\nCopyright 2008-2010 by Markus Gutschke\n" +
         "For more information check http://shellinabox.com");
 };
@@ -2007,6 +2417,9 @@ VT100.prototype.showContextMenu = function(x, y) {
           '<li>' +
              (this.visualBell ? '<img src="enabled.gif" />' : '') +
              'Visual Bell</li>'+
+          '<li>' +
+             (this.softKeyboard ? '<img src="enabled.gif" />' : '') +
+             'Onscreen Keyboard</li>' +
           '<li id="endconfig">' +
              (this.blinkingCursor ? '<img src="enabled.gif" />' : '') +
              'Blinking Cursor</li>'+
@@ -2038,6 +2451,7 @@ VT100.prototype.showContextMenu = function(x, y) {
   // Actions for default items
   var actions                 = [ this.copyLast, p, this.reset,
                                   this.toggleUTF, this.toggleBell,
+                                  this.toggleSoftKeyboard,
                                   this.toggleCursorBlinking ];
 
   // Actions for user CSS styles (if any)
@@ -2093,26 +2507,30 @@ VT100.prototype.showContextMenu = function(x, y) {
   }
 
   // Position menu next to the mouse pointer
-  if (x + popup.clientWidth > this.container.offsetWidth) {
-    x                         = this.container.offsetWidth - popup.clientWidth;
+  this.menu.style.left        = '0px';
+  this.menu.style.top         = '0px';
+  this.menu.style.width       =  this.container.offsetWidth  + 'px';
+  this.menu.style.height      =  this.container.offsetHeight + 'px';
+  popup.style.left            = '0px';
+  popup.style.top             = '0px';
+  
+  var margin                  = 2;
+  if (x + popup.clientWidth >= this.container.offsetWidth - margin) {
+    x              = this.container.offsetWidth-popup.clientWidth - margin - 1;
   }
-  if (x < 0) {
-    x                         = 0;
+  if (x < margin) {
+    x                         = margin;
   }
-  if (y + popup.clientHeight > this.container.offsetHeight) {
-    y                         = this.container.offsetHeight-popup.clientHeight;
+  if (y + popup.clientHeight >= this.container.offsetHeight - margin) {
+    y            = this.container.offsetHeight-popup.clientHeight - margin - 1;
   }
-  if (y < 0) {
-    y                         = 0;
+  if (y < margin) {
+    y                         = margin;
   }
   popup.style.left            = x + 'px';
   popup.style.top             = y + 'px';
 
   // Block all other interactions with the terminal emulator
-  this.menu.style.left        = '0px';
-  this.menu.style.top         = '0px';
-  this.menu.style.width       =  this.container.offsetWidth  + 'px';
-  this.menu.style.height      =  this.container.offsetHeight + 'px';
   this.addListener(this.menu, 'click', function(vt100) {
                                          return function() {
                                            vt100.hideContextMenu();
@@ -2895,39 +3313,42 @@ VT100.prototype.restoreCursor = function() {
               this.savedY[this.currentScreen]);
 };
 
-VT100.prototype.set80_132Mode = function(state) {
-  var transform                                       = undefined;
-  var styles                                          = [ 'transform',
-                                                          'WebkitTransform',
-                                                          'MozTransform',
-                                                          'filter'
-                                                        ];
+VT100.prototype.getTransformName = function() {
+  var styles = [ 'transform', 'WebkitTransform', 'MozTransform', 'filter' ];
   for (var i = 0; i < styles.length; ++i) {
     if (typeof this.console[0].style[styles[i]] != 'undefined') {
-      transform                                       = styles[i];
-      break;
+      return styles[i];
     }
   }
+  return undefined;
+};
 
+VT100.prototype.getTransformStyle = function(transform, scale) {
+  return scale && scale != 1.0
+    ? transform == 'filter'
+      ? 'progid:DXImageTransform.Microsoft.Matrix(' +
+                                 'M11=' + (1.0/scale) + ',M12=0,M21=0,M22=1,' +
+                                 "sizingMethod='auto expand')"
+      : 'translateX(-50%) ' +
+        'scaleX(' + (1.0/scale) + ') ' +
+        'translateX(50%)'
+    : '';
+};
+
+VT100.prototype.set80_132Mode = function(state) {
+  var transform                  = this.getTransformName();
   if (transform) {
     if ((this.console[this.currentScreen].style[transform] != '') == state) {
       return;
     }
-    var style                                         =
-      state ? transform == 'filter'
-            ? 'progid:DXImageTransform.Microsoft.Matrix(' +
-                             'M11=0.606060606060606060606,M12=0,M21=0,M22=1,' +
-                             "sizingMethod='auto expand')"
-            : 'translateX(-50%) ' +
-              'scaleX(0.606060606060606060606) ' +
-              'translateX(50%)'
-            : '';
+    var style                    = state ?
+                                   this.getTransformStyle(transform, 1.65):'';
     this.console[this.currentScreen].style[transform] = style;
-    this.cursor.style[transform]                      = style;
-    this.space.style[transform]                       = style;
-    this.scale                                        = state ? 1.65 : 1.0;
+    this.cursor.style[transform] = style;
+    this.space.style[transform]  = style;
+    this.scale                   = state ? 1.65 : 1.0;
     if (transform == 'filter') {
-      this.console[this.currentScreen].style.width    = state ? '165%' : '';
+      this.console[this.currentScreen].style.width = state ? '165%' : '';
     }
     this.resizer();
   }
index d5c289828b831ef5d2dd1e6c30df32c59c07f92c..b1a024079cba0a81c775c097b8257cb1a58419f9 100644 (file)
@@ -238,22 +238,16 @@ VT100.prototype.reset = function(clearHistory) {
   this.enableAlternateScreen(false);
 
   var wasCompressed                                  = false;
-  var styles                                         = [ 'transform',
-                                                         'WebkitTransform',
-                                                         'MozTransform',
-                                                         'filter' ];
-  for (var i = 0; i < styles.length; ++i) {
-    if (typeof this.console[0].style[styles[i]] != 'undefined') {
-      for (var j = 0; j < 1; ++j) {
-        wasCompressed                |= this.console[j].style[styles[i]] != '';
-        this.console[j].style[styles[i]]             = '';
-      }
-      this.cursor.style[styles[i]]                   = '';
-      this.space.style[styles[i]]                    = '';
-      if (styles[i] == 'filter') {
-        this.console[this.currentScreen].style.width = '';
-      }
-      break;
+  var transform                                      = this.getTransformName();
+  if (transform) {
+    for (var i = 0; i < 2; ++i) {
+      wasCompressed                  |= this.console[i].style[transform] != '';
+      this.console[i].style[transform]               = '';
+    }
+    this.cursor.style[transform]                     = '';
+    this.space.style[transform]                      = '';
+    if (transform == 'filter') {
+      this.console[this.currentScreen].style.width   = '';
     }
   }
   this.scale                                         = 1.0;
@@ -270,10 +264,13 @@ VT100.prototype.reset = function(clearHistory) {
 };
 
 VT100.prototype.addListener = function(elem, event, listener) {
-  if (elem.addEventListener) {
-    elem.addEventListener(event, listener, false);
-  } else {
-    elem.attachEvent('on' + event, listener);
+  try {
+    if (elem.addEventListener) {
+      elem.addEventListener(event, listener, false);
+    } else {
+      elem.attachEvent('on' + event, listener);
+    }
+  } catch (e) {
   }
 };
 
@@ -281,11 +278,12 @@ VT100.prototype.getUserSettings = function() {
   // Compute hash signature to identify the entries in the userCSS menu.
   // If the menu is unchanged from last time, default values can be
   // looked up in a cookie associated with this page.
-  this.signature            = 2;
+  this.signature            = 3;
   this.utfPreferred         = true;
   this.visualBell           = typeof suppressAllAudio != 'undefined' &&
                               suppressAllAudio;
   this.autoprint            = true;
+  this.softKeyboard         = false;
   this.blinkingCursor       = true;
   if (this.visualBell) {
     this.signature          = Math.floor(16807*this.signature + 1) %
@@ -311,15 +309,16 @@ VT100.prototype.getUserSettings = function() {
   if (settings >= 0) {
     settings                = document.cookie.substr(settings + key.length).
                                                    replace(/([0-1]*).*/, "$1");
-    if (settings.length == 3 + (typeof userCSSList == 'undefined' ?
+    if (settings.length == 5 + (typeof userCSSList == 'undefined' ?
                                 0 : userCSSList.length)) {
       this.utfPreferred     = settings.charAt(0) != '0';
       this.visualBell       = settings.charAt(1) != '0';
       this.autoprint        = settings.charAt(2) != '0';
-      this.blinkingCursor   = settings.charAt(3) != '0';
+      this.softKeyboard     = settings.charAt(3) != '0';
+      this.blinkingCursor   = settings.charAt(4) != '0';
       if (typeof userCSSList != 'undefined') {
         for (var i = 0; i < userCSSList.length; ++i) {
-          userCSSList[i][2] = settings.charAt(i + 3) != '0';
+          userCSSList[i][2] = settings.charAt(i + 5) != '0';
         }
       }
     }
@@ -332,6 +331,7 @@ VT100.prototype.storeUserSettings = function() {
                   (this.utfEnabled     ? '1' : '0') +
                   (this.visualBell     ? '1' : '0') +
                   (this.autoprint      ? '1' : '0') +
+                  (this.softKeyboard   ? '1' : '0') +
                   (this.blinkingCursor ? '1' : '0');
   if (typeof userCSSList != 'undefined') {
     for (var i = 0; i < userCSSList.length; ++i) {
@@ -413,7 +413,7 @@ VT100.prototype.initializeUserCSSStyles = function() {
                         label.textContent= label.textContent;
                       }
 
-                      // User style sheets are number sequentially
+                      // User style sheets are numbered sequentially
                       var sheet          = document.getElementById(
                                                                'usercss-' + i);
                       if (i == current) {
@@ -470,6 +470,328 @@ VT100.prototype.initializeUserCSSStyles = function() {
   }
 };
 
+VT100.prototype.resetLastSelectedKey = function(e) {
+  var key                          = this.lastSelectedKey;
+  if (!key) {
+    return false;
+  }
+
+  var position                     = this.mousePosition(e);
+
+  // We don't get all the necessary events to reliably reselect a key
+  // if we moved away from it and then back onto it. We approximate the
+  // behavior by remembering the key until either we release the mouse
+  // button (we might never get this event if the mouse has since left
+  // the window), or until we move away too far.
+  var box                          = this.keyboard.firstChild;
+  if (position[0] <  box.offsetLeft + key.offsetWidth ||
+      position[1] <  box.offsetTop + key.offsetHeight ||
+      position[0] >= box.offsetLeft + box.offsetWidth - key.offsetWidth ||
+      position[1] >= box.offsetTop + box.offsetHeight - key.offsetHeight ||
+      position[0] <  box.offsetLeft + key.offsetLeft - key.offsetWidth ||
+      position[1] <  box.offsetTop + key.offsetTop - key.offsetHeight ||
+      position[0] >= box.offsetLeft + key.offsetLeft + 2*key.offsetWidth ||
+      position[1] >= box.offsetTop + key.offsetTop + 2*key.offsetHeight) {
+    if (this.lastSelectedKey.className) log.console('reset: deselecting');
+    this.lastSelectedKey.className = '';
+    this.lastSelectedKey           = undefined;
+  }
+  return false;
+};
+
+VT100.prototype.showShiftState = function(state) {
+  var style              = document.getElementById('shift_state');
+  if (state) {
+    this.setTextContentRaw(style,
+                           '#vt100 #keyboard .shifted {' +
+                             'display: inline }' +
+                           '#vt100 #keyboard .unshifted {' +
+                             'display: none }');
+  } else {
+    this.setTextContentRaw(style, '');
+  }
+  var elems              = this.keyboard.getElementsByTagName('I');
+  for (var i = 0; i < elems.length; ++i) {
+    if (elems[i].id == '16') {
+      elems[i].className = state ? 'selected' : '';
+    }
+  }
+};
+
+VT100.prototype.showCtrlState = function(state) {
+  var ctrl         = this.getChildById(this.keyboard, '17' /* Ctrl */);
+  if (ctrl) {
+    ctrl.className = state ? 'selected' : '';
+  }
+};
+
+VT100.prototype.showAltState = function(state) {
+  var alt         = this.getChildById(this.keyboard, '18' /* Alt */);
+  if (alt) {
+    alt.className = state ? 'selected' : '';
+  }
+};
+
+VT100.prototype.clickedKeyboard = function(e, elem, ch, key, shift, ctrl, alt){
+  var fake      = [ ];
+  fake.charCode = ch;
+  fake.keyCode  = key;
+  fake.ctrlKey  = ctrl;
+  fake.shiftKey = shift;
+  fake.altKey   = alt;
+  fake.metaKey  = alt;
+  return this.handleKey(fake);
+};
+
+VT100.prototype.addKeyBinding = function(elem, ch, key, CH, KEY) {
+  if (elem == undefined) {
+    return;
+  }
+  if (ch == '\u00A0') {
+    // &nbsp; should be treated as a regular space character.
+    ch                                  = ' ';
+  }
+  if (ch != undefined && CH == undefined) {
+    // For letter keys, we automatically compute the uppercase character code
+    // from the lowercase one.
+    CH                                  = ch.toUpperCase();
+  }
+  if (KEY == undefined && key != undefined) {
+    // Most keys have identically key codes for both lowercase and uppercase
+    // keypresses. Normally, only function keys would have distinct key codes,
+    // whereas regular keys have character codes.
+    KEY                                 = key;
+  } else if (KEY == undefined && CH != undefined) {
+    // For regular keys, copy the character code to the key code.
+    KEY                                 = CH.charCodeAt(0);
+  }
+  if (key == undefined && ch != undefined) {
+    // For regular keys, copy the character code to the key code.
+    key                                 = ch.charCodeAt(0);
+  }
+  // Convert characters to numeric character codes. If the character code
+  // is undefined (i.e. this is a function key), set it to zero.
+  ch                                    = ch ? ch.charCodeAt(0) : 0;
+  CH                                    = CH ? CH.charCodeAt(0) : 0;
+
+  // Mouse down events high light the key. We also set lastSelectedKey. This
+  // is needed to that mouseout/mouseover can keep track of the key that
+  // is currently being clicked.
+  this.addListener(elem, 'mousedown',
+    function(vt100, elem, key) { return function(e) {
+      if ((e.which || e.button) == 1) {
+        if (vt100.lastSelectedKey) {       
+          vt100.lastSelectedKey.className= '';
+        }
+        // Highlight the key while the mouse button is held down.
+        if (key == 16 /* Shift */) {
+          if (!elem.className != vt100.isShift) {
+            vt100.showShiftState(!vt100.isShift);
+          }
+        } else if (key == 17 /* Ctrl */) {
+          if (!elem.className != vt100.isCtrl) {
+            vt100.showCtrlState(!vt100.isCtrl);
+          }
+        } else if (key == 18 /* Alt */) {
+          if (!elem.className != vt100.isAlt) {
+            vt100.showAltState(!vt100.isAlt);
+          }
+        } else {
+          elem.className                  = 'selected';
+        }
+        vt100.lastSelectedKey             = elem;
+      }
+      return false; }; }(this, elem, key));
+  var clicked                           =
+    // Modifier keys update the state of the keyboard, but do not generate
+    // any key clicks that get forwarded to the application.
+    key >= 16 /* Shift */ && key <= 18 /* Alt */ ?
+    function(vt100, elem) { return function(e) {
+      if (elem == vt100.lastSelectedKey) {
+        if (key == 16 /* Shift */) {
+          // The user clicked the Shift key
+          vt100.isShift                 = !vt100.isShift;
+          vt100.showShiftState(vt100.isShift);
+        } else if (key == 17 /* Ctrl */) {
+          vt100.isCtrl                  = !vt100.isCtrl;
+          vt100.showCtrlState(vt100.isCtrl);
+        } else if (key == 18 /* Alt */) {
+          vt100.isAlt                   = !vt100.isAlt;
+          vt100.showAltState(vt100.isAlt);
+        }
+        vt100.lastSelectedKey           = undefined;
+      }
+      if (vt100.lastSelectedKey) {
+        vt100.lastSelectedKey.className = '';
+        vt100.lastSelectedKey           = undefined;
+      }
+      return false; }; }(this, elem) :
+    // Regular keys generate key clicks, when the mouse button is released or
+    // when a mouse click event is received.
+    function(vt100, elem, ch, key, CH, KEY) { return function(e) {
+      if (vt100.lastSelectedKey) {
+        if (elem == vt100.lastSelectedKey) {
+          // The user clicked a key.
+          if (vt100.isShift) {
+            vt100.clickedKeyboard(e, elem, CH, KEY,
+                                  true, vt100.isCtrl, vt100.isAlt);
+          } else {
+            vt100.clickedKeyboard(e, elem, ch, key,
+                                  false, vt100.isCtrl, vt100.isAlt);
+          }
+          vt100.isShift                 = false;
+          vt100.showShiftState(false);
+          vt100.isCtrl                  = false;
+          vt100.showCtrlState(false);
+          vt100.isAlt                   = false;
+          vt100.showAltState(false);
+        }
+        vt100.lastSelectedKey.className = '';
+        vt100.lastSelectedKey           = undefined;
+      }
+      elem.className                    = '';
+      return false; }; }(this, elem, ch, key, CH, KEY);
+  this.addListener(elem, 'mouseup', clicked);
+  this.addListener(elem, 'click', clicked);
+
+  // When moving the mouse away from a key, check if any keys need to be
+  // deselected.
+  this.addListener(elem, 'mouseout',
+    function(vt100, elem, key) { return function(e) {
+      if (key == 16 /* Shift */) {
+        if (!elem.className == vt100.isShift) {
+          vt100.showShiftState(vt100.isShift);
+        }
+      } else if (key == 17 /* Ctrl */) {
+        if (!elem.className == vt100.isCtrl) {
+          vt100.showCtrlState(vt100.isCtrl);
+        }
+      } else if (key == 18 /* Alt */) {
+        if (!elem.className == vt100.isAlt) {
+          vt100.showAltState(vt100.isAlt);
+        }
+      } else if (elem.className) {
+        elem.className                  = '';
+        vt100.lastSelectedKey           = elem;
+      } else if (vt100.lastSelectedKey) {
+        vt100.resetLastSelectedKey(e);
+      }
+      return false; }; }(this, elem, key));
+
+  // When moving the mouse over a key, select it if the user is still holding
+  // the mouse button down (i.e. elem == lastSelectedKey)
+  this.addListener(elem, 'mouseover',
+    function(vt100, elem, key) { return function(e) {
+      if (elem == vt100.lastSelectedKey) {
+        if (key == 16 /* Shift */) {
+          if (!elem.className != vt100.isShift) {
+            vt100.showShiftState(!vt100.isShift);
+          }
+        } else if (key == 17 /* Ctrl */) {
+          if (!elem.className != vt100.isCtrl) {
+            vt100.showCtrlState(!vt100.isCtrl);
+          }
+        } else if (key == 18 /* Alt */) {
+          if (!elem.className != vt100.isAlt) {
+            vt100.showAltState(!vt100.isAlt);
+          }
+        } else if (!elem.className) {
+          elem.className                = 'selected';
+        }
+      } else {
+        vt100.resetLastSelectedKey(e);
+      }
+      return false; }; }(this, elem, key));
+};
+
+VT100.prototype.initializeKeyBindings = function(elem) {
+  if (elem) {
+    if (elem.nodeName == "I" || elem.nodeName == "B") {
+      if (elem.id) {
+        // Function keys. The Javascript keycode is part of the "id"
+        var i     = parseInt(elem.id);
+        if (i) {
+          // If the id does not parse as a number, it is not a keycode.
+          this.addKeyBinding(elem, undefined, i);
+        }
+      } else {
+        var child = elem.firstChild;
+        if (child.nodeName == "#text") {
+          // If the key only has a text node as a child, then it is a letter.
+          // Automatically compute the lower and upper case version of the key.
+          this.addKeyBinding(elem, this.getTextContent(child).toLowerCase());
+        } else {
+          // If the key has two children, they are the lower and upper case
+          // character code, respectively.
+          this.addKeyBinding(elem, this.getTextContent(child), undefined,
+                             this.getTextContent(child.nextSibling));
+        }
+      }
+    }
+  }
+  // Recursively parse all other child nodes.
+  for (elem = elem.firstChild; elem; elem = elem.nextSibling) {
+    this.initializeKeyBindings(elem);
+  }
+};
+
+VT100.prototype.initializeKeyboard = function() {
+  // Configure mouse event handlers for button that displays/hides keyboard
+  var box                               = this.keyboard.firstChild;
+  this.hideSoftKeyboard();
+  this.addListener(this.keyboardImage, 'click',
+    function(vt100) { return function(e) {
+      if (vt100.keyboard.style.display != '') {
+        if (vt100.reconnectBtn.style.visibility != '') {
+          vt100.showSoftKeyboard();
+        }
+      } else {
+        vt100.hideSoftKeyboard();
+        vt100.input.focus();
+      }
+      return false; }; }(this));
+
+  // Enable button that displays keyboard
+  if (this.softKeyboard) {
+    this.keyboardImage.style.visibility = 'visible';
+  }
+
+  // Configure mouse event handlers for on-screen keyboard
+  this.addListener(this.keyboard, 'click',
+    function(vt100) { return function(e) {
+      vt100.hideSoftKeyboard();
+      vt100.input.focus();
+      return false; }; }(this));
+  this.addListener(this.keyboard, 'selectstart', this.cancelEvent);
+  this.addListener(box, 'click', this.cancelEvent);
+  this.addListener(box, 'mouseup',
+    function(vt100) { return function(e) {
+      if (vt100.lastSelectedKey) {
+        vt100.lastSelectedKey.className = '';
+        vt100.lastSelectedKey           = undefined;
+      }
+      return false; }; }(this));
+  this.addListener(box, 'mouseout',
+    function(vt100) { return function(e) {
+      return vt100.resetLastSelectedKey(e); }; }(this));
+  this.addListener(box, 'mouseover',
+    function(vt100) { return function(e) {
+      return vt100.resetLastSelectedKey(e); }; }(this));
+
+  // Configure SHIFT key behavior
+  var style                             = document.createElement('style');
+  var id                                = document.createAttribute('id');
+  id.nodeValue                          = 'shift_state';
+  style.setAttributeNode(id);
+  var type                              = document.createAttribute('type');
+  type.nodeValue                        = 'text/css';
+  style.setAttributeNode(type);
+  document.getElementsByTagName('head')[0].appendChild(style);
+
+  // Set up key bindings
+  this.initializeKeyBindings(box);
+};
+
 VT100.prototype.initializeElements = function(container) {
   // If the necessary objects have not already been defined in the HTML
   // page, create them now.
@@ -483,6 +805,9 @@ VT100.prototype.initializeElements = function(container) {
 
   if (!this.getChildById(this.container, 'reconnect')   ||
       !this.getChildById(this.container, 'menu')        ||
+      !this.getChildById(this.container, 'keyboard')    ||
+      !this.getChildById(this.container, 'kbd_button')  ||
+      !this.getChildById(this.container, 'kbd_img')     ||
       !this.getChildById(this.container, 'scrollable')  ||
       !this.getChildById(this.container, 'console')     ||
       !this.getChildById(this.container, 'alt_console') ||
@@ -525,7 +850,15 @@ VT100.prototype.initializeElements = function(container) {
                        '<div id="cursize" style="visibility: hidden">' +
                        '</div>' +
                        '<div id="menu"></div>' +
+                       '<div id="keyboard" unselectable="on">' +
+                         KEYBOARD +
+                       '</div>' +
                        '<div id="scrollable">' +
+                         '<table id="kbd_button">' +
+                           '<tr><td width="100%">&nbsp;</td>' +
+                           '<td><img id="kbd_img" src="keyboard.png" /></td>' +
+                           '<td>&nbsp;&nbsp;&nbsp;&nbsp;</td></tr>' +
+                         '</table>' +
                          '<pre id="lineheight">&nbsp;</pre>' +
                          '<pre id="console">' +
                            '<pre></pre>' +
@@ -566,6 +899,8 @@ VT100.prototype.initializeElements = function(container) {
   this.reconnectBtn            = this.getChildById(this.container,'reconnect');
   this.curSizeBox              = this.getChildById(this.container, 'cursize');
   this.menu                    = this.getChildById(this.container, 'menu');
+  this.keyboard                = this.getChildById(this.container, 'keyboard');
+  this.keyboardImage           = this.getChildById(this.container, 'kbd_img');
   this.scrollable              = this.getChildById(this.container,
                                                                  'scrollable');
   this.lineheight              = this.getChildById(this.container,
@@ -646,6 +981,9 @@ VT100.prototype.initializeElements = function(container) {
   // Hide context menu
   this.hideContextMenu();
 
+  // Set up onscreen soft keyboard
+  this.initializeKeyboard();
+
   // Add listener to reconnect button
   this.addListener(this.reconnectBtn.firstChild, 'click',
                    function(vt100) {
@@ -733,6 +1071,7 @@ VT100.prototype.reconnect = function() {
 
 VT100.prototype.showReconnect = function(state) {
   if (state) {
+    this.hideSoftKeyboard();
     this.reconnectBtn.style.visibility = '';
   } else {
     this.reconnectBtn.style.visibility = 'hidden';
@@ -766,6 +1105,9 @@ VT100.prototype.resized = function(w, h) {
 };
 
 VT100.prototype.resizer = function() {
+  // Hide onscreen soft keyboard
+  this.hideSoftKeyboard();
+
   // The cursor can get corrupted if the print-preview is displayed in Firefox.
   // Recreating it, will repair it.
   var newCursor                = document.createElement('pre');
@@ -945,6 +1287,17 @@ VT100.prototype.cancelEvent = function(event) {
   return false;
 };
 
+VT100.prototype.mousePosition = function(event) {
+  var offsetX      = this.container.offsetLeft;
+  var offsetY      = this.container.offsetTop;
+  for (var e = this.container; e = e.offsetParent; ) {
+    offsetX       += e.offsetLeft;
+    offsetY       += e.offsetTop;
+  }
+  return [ event.clientX - offsetX,
+           event.clientY - offsetY ];
+};
+
 VT100.prototype.mouseEvent = function(event, type) {
   // If any text is currently selected, do not move the focus as that would
   // invalidate the selection.
@@ -954,15 +1307,10 @@ VT100.prototype.mouseEvent = function(event, type) {
   }
 
   // Compute mouse position in characters.
-  var offsetX      = this.container.offsetLeft;
-  var offsetY      = this.container.offsetTop;
-  for (var e = this.container; e = e.offsetParent; ) {
-    offsetX       += e.offsetLeft;
-    offsetY       += e.offsetTop;
-  }
-  var x            = (event.clientX - offsetX) / this.cursorWidth;
-  var y            = ((event.clientY - offsetY) + this.scrollable.offsetTop) /
-                     this.cursorHeight - this.numScrollbackLines;
+  var position     = this.mousePosition(event);
+  var x            = Math.floor(position[0] / this.cursorWidth);
+  var y            = Math.floor((position[1] + this.scrollable.scrollTop) /
+                                this.cursorHeight) - this.numScrollbackLines;
   var inside       = true;
   if (x >= this.terminalWidth) {
     x              = this.terminalWidth - 1;
@@ -1022,7 +1370,7 @@ VT100.prototype.mouseEvent = function(event, type) {
   // Bring up context menu.
   if (button == 2 && !event.shiftKey) {
     if (type == MOUSE_DOWN) {
-      this.showContextMenu(event.clientX - offsetX, event.clientY - offsetY);
+      this.showContextMenu(position[0], position[1]);
     }
     return this.cancelEvent(event);
   }
@@ -1058,6 +1406,29 @@ VT100.prototype.getTextContent = function(elem) {
          (typeof elem.textContent == 'undefined' ? elem.innerText : '');
 };
 
+VT100.prototype.setTextContentRaw = function(elem, s) {
+  // Updating the content of an element is an expensive operation. It actually
+  // pays off to first check whether the element is still unchanged.
+  if (typeof elem.textContent == 'undefined') {
+    if (elem.innerText != s) {
+      try {
+        elem.innerText = s;
+      } catch (e) {
+        // Very old versions of IE do not allow setting innerText. Instead,
+        // remove all children, by setting innerHTML and then set the text
+        // using DOM methods.
+        elem.innerHTML = '';
+        elem.appendChild(document.createTextNode(
+                                          this.replaceChar(s, ' ', '\u00A0')));
+      }
+    }
+  } else {
+    if (elem.textContent != s) {
+      elem.textContent = s;
+    }
+  }
+};
+
 VT100.prototype.setTextContent = function(elem, s) {
   // Check if we find any URLs in the text. If so, automatically convert them
   // to links.
@@ -1103,26 +1474,7 @@ VT100.prototype.setTextContent = function(elem, s) {
     return;
   }
 
-  // Updating the content of an element is an expensive operation. It actually
-  // pays off to first check whether the element is still unchanged.
-  if (typeof elem.textContent == 'undefined') {
-    if (elem.innerText != s) {
-      try {
-        elem.innerText = s;
-      } catch (e) {
-        // Very old versions of IE do not allow setting innerText. Instead,
-        // remove all children, by setting innerHTML and then set the text
-        // using DOM methods.
-        elem.innerHTML = '';
-        elem.appendChild(document.createTextNode(
-                                          this.replaceChar(s, ' ', '\u00A0')));
-      }
-    }
-  } else {
-    if (elem.textContent != s) {
-      elem.textContent = s;
-    }
-  }
+  this.setTextContentRaw(elem, s);
 };
 
 VT100.prototype.insertBlankLine = function(y, color, style) {
@@ -1578,27 +1930,21 @@ VT100.prototype.enableAlternateScreen = function(state) {
   this.console[this.currentScreen].style.display     = '';
 
   // Select appropriate character pitch.
-  var styles                                         = [ 'transform',
-                                                         'WebkitTransform',
-                                                         'MozTransform',
-                                                         'filter' ];
-  for (var i = 0; i < styles.length; ++i) {
-    if (typeof this.console[0].style[styles[i]] != 'undefined') {
-      if (state) {
-        // Upon enabling the alternate screen, we switch to 80 column mode. But
-        // upon returning to the regular screen, we restore the mode that was
-        // in effect previously.
-        this.console[1].style[styles[i]]             = '';
-      }
-      var style                                      =
-                             this.console[this.currentScreen].style[styles[i]];
-      this.cursor.style[styles[i]]                   = style;
-      this.space.style[styles[i]]                    = style;
-      this.scale                                     = style == '' ? 1.0:1.65;
-      if (styles[i] == 'filter') {
-        this.console[this.currentScreen].style.width = style == '' ? '165%':'';
-      }
-      break;
+  var transform                                      = this.getTransformName();
+  if (transform) {
+    if (state) {
+      // Upon enabling the alternate screen, we switch to 80 column mode. But
+      // upon returning to the regular screen, we restore the mode that was
+      // in effect previously.
+      this.console[1].style[transform]               = '';
+    }
+    var style                                        =
+                             this.console[this.currentScreen].style[transform];
+    this.cursor.style[transform]                     = style;
+    this.space.style[transform]                      = style;
+    this.scale                                       = style == '' ? 1.0:1.65;
+    if (transform == 'filter') {
+       this.console[this.currentScreen].style.width  = style == '' ? '165%':'';
     }
   }
   this.resizer();
@@ -1969,6 +2315,70 @@ VT100.prototype.toggleBell = function() {
   this.visualBell = !this.visualBell;
 };
 
+VT100.prototype.toggleSoftKeyboard = function() {
+  this.softKeyboard = !this.softKeyboard;
+  this.keyboardImage.style.visibility = this.softKeyboard ? 'visible' : '';
+};
+
+VT100.prototype.deselectKeys = function(elem) {
+  if (elem && elem.className == 'selected') {
+    elem.className = '';
+  }
+  for (elem = elem.firstChild; elem; elem = elem.nextSibling) {
+    this.deselectKeys(elem);
+  }
+};
+
+VT100.prototype.showSoftKeyboard = function() {
+  // Make sure no key is currently selected
+  this.lastSelectedKey           = undefined;
+  this.deselectKeys(this.keyboard);
+  this.isShift                   = false;
+  this.showShiftState(false);
+  this.isCtrl                    = false;
+  this.showCtrlState(false);
+  this.isAlt                     = false;
+  this.showAltState(false);
+
+  this.keyboard.style.left       = '0px';
+  this.keyboard.style.top        = '0px';
+  this.keyboard.style.width      = this.container.offsetWidth  + 'px';
+  this.keyboard.style.height     = this.container.offsetHeight + 'px';
+  this.keyboard.style.visibility = 'hidden';
+  this.keyboard.style.display    = '';
+
+  var kbd                        = this.keyboard.firstChild;
+  var scale                      = 1.0;
+  var transform                  = this.getTransformName();
+  if (transform) {
+    kbd.style[transform]         = '';
+    if (kbd.offsetWidth > 0.9 * this.container.offsetWidth) {
+      scale                      = (kbd.offsetWidth/
+                                    this.container.offsetWidth)/0.9;
+    }
+    if (kbd.offsetHeight > 0.9 * this.container.offsetHeight) {
+      scale                      = Math.max((kbd.offsetHeight/
+                                             this.container.offsetHeight)/0.9);
+    }
+    var style                    = this.getTransformStyle(transform,
+                                              scale > 1.0 ? scale : undefined);
+    kbd.style[transform]         = style;
+  }
+  if (transform == 'filter') {
+    scale                        = 1.0;
+  }
+  kbd.style.left                 = ((this.container.offsetWidth -
+                                     kbd.offsetWidth/scale)/2) + 'px';
+  kbd.style.top                  = ((this.container.offsetHeight -
+                                     kbd.offsetHeight/scale)/2) + 'px';
+
+  this.keyboard.style.visibility = 'visible';
+};
+
+VT100.prototype.hideSoftKeyboard = function() {
+  this.keyboard.style.display    = 'none';
+};
+
 VT100.prototype.toggleCursorBlinking = function() {
   this.blinkingCursor = !this.blinkingCursor;
 };
@@ -2007,6 +2417,9 @@ VT100.prototype.showContextMenu = function(x, y) {
           '<li>' +
              (this.visualBell ? '<img src="enabled.gif" />' : '') +
              'Visual Bell</li>'+
+          '<li>' +
+             (this.softKeyboard ? '<img src="enabled.gif" />' : '') +
+             'Onscreen Keyboard</li>' +
           '<li id="endconfig">' +
              (this.blinkingCursor ? '<img src="enabled.gif" />' : '') +
              'Blinking Cursor</li>'+
@@ -2038,6 +2451,7 @@ VT100.prototype.showContextMenu = function(x, y) {
   // Actions for default items
   var actions                 = [ this.copyLast, p, this.reset,
                                   this.toggleUTF, this.toggleBell,
+                                  this.toggleSoftKeyboard,
                                   this.toggleCursorBlinking ];
 
   // Actions for user CSS styles (if any)
@@ -2093,26 +2507,30 @@ VT100.prototype.showContextMenu = function(x, y) {
   }
 
   // Position menu next to the mouse pointer
-  if (x + popup.clientWidth > this.container.offsetWidth) {
-    x                         = this.container.offsetWidth - popup.clientWidth;
+  this.menu.style.left        = '0px';
+  this.menu.style.top         = '0px';
+  this.menu.style.width       =  this.container.offsetWidth  + 'px';
+  this.menu.style.height      =  this.container.offsetHeight + 'px';
+  popup.style.left            = '0px';
+  popup.style.top             = '0px';
+  
+  var margin                  = 2;
+  if (x + popup.clientWidth >= this.container.offsetWidth - margin) {
+    x              = this.container.offsetWidth-popup.clientWidth - margin - 1;
   }
-  if (x < 0) {
-    x                         = 0;
+  if (x < margin) {
+    x                         = margin;
   }
-  if (y + popup.clientHeight > this.container.offsetHeight) {
-    y                         = this.container.offsetHeight-popup.clientHeight;
+  if (y + popup.clientHeight >= this.container.offsetHeight - margin) {
+    y            = this.container.offsetHeight-popup.clientHeight - margin - 1;
   }
-  if (y < 0) {
-    y                         = 0;
+  if (y < margin) {
+    y                         = margin;
   }
   popup.style.left            = x + 'px';
   popup.style.top             = y + 'px';
 
   // Block all other interactions with the terminal emulator
-  this.menu.style.left        = '0px';
-  this.menu.style.top         = '0px';
-  this.menu.style.width       =  this.container.offsetWidth  + 'px';
-  this.menu.style.height      =  this.container.offsetHeight + 'px';
   this.addListener(this.menu, 'click', function(vt100) {
                                          return function() {
                                            vt100.hideContextMenu();
@@ -2895,39 +3313,42 @@ VT100.prototype.restoreCursor = function() {
               this.savedY[this.currentScreen]);
 };
 
-VT100.prototype.set80_132Mode = function(state) {
-  var transform                                       = undefined;
-  var styles                                          = [ 'transform',
-                                                          'WebkitTransform',
-                                                          'MozTransform',
-                                                          'filter'
-                                                        ];
+VT100.prototype.getTransformName = function() {
+  var styles = [ 'transform', 'WebkitTransform', 'MozTransform', 'filter' ];
   for (var i = 0; i < styles.length; ++i) {
     if (typeof this.console[0].style[styles[i]] != 'undefined') {
-      transform                                       = styles[i];
-      break;
+      return styles[i];
     }
   }
+  return undefined;
+};
 
+VT100.prototype.getTransformStyle = function(transform, scale) {
+  return scale && scale != 1.0
+    ? transform == 'filter'
+      ? 'progid:DXImageTransform.Microsoft.Matrix(' +
+                                 'M11=' + (1.0/scale) + ',M12=0,M21=0,M22=1,' +
+                                 "sizingMethod='auto expand')"
+      : 'translateX(-50%) ' +
+        'scaleX(' + (1.0/scale) + ') ' +
+        'translateX(50%)'
+    : '';
+};
+
+VT100.prototype.set80_132Mode = function(state) {
+  var transform                  = this.getTransformName();
   if (transform) {
     if ((this.console[this.currentScreen].style[transform] != '') == state) {
       return;
     }
-    var style                                         =
-      state ? transform == 'filter'
-            ? 'progid:DXImageTransform.Microsoft.Matrix(' +
-                             'M11=0.606060606060606060606,M12=0,M21=0,M22=1,' +
-                             "sizingMethod='auto expand')"
-            : 'translateX(-50%) ' +
-              'scaleX(0.606060606060606060606) ' +
-              'translateX(50%)'
-            : '';
+    var style                    = state ?
+                                   this.getTransformStyle(transform, 1.65):'';
     this.console[this.currentScreen].style[transform] = style;
-    this.cursor.style[transform]                      = style;
-    this.space.style[transform]                       = style;
-    this.scale                                        = state ? 1.65 : 1.0;
+    this.cursor.style[transform] = style;
+    this.space.style[transform]  = style;
+    this.scale                   = state ? 1.65 : 1.0;
     if (transform == 'filter') {
-      this.console[this.currentScreen].style.width    = state ? '165%' : '';
+      this.console[this.currentScreen].style.width = state ? '165%' : '';
     }
     this.resizer();
   }
This page took 0.182863 seconds and 5 git commands to generate.