diff --git a/ChangeLog b/ChangeLog index d7ea959..2e216b0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2010-09-04 Markus Gutschke + + * 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 * Fix some scaling related issues. This fix is thanks to some diff --git a/Makefile.am b/Makefile.am index 3a07cd3..2f28df2 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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: diff --git a/Makefile.in b/Makefile.in index 50c8272..018f234 100644 --- a/Makefile.in +++ b/Makefile.in @@ -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: diff --git a/config.h b/config.h index e9fc3ac..547512c 100644 --- a/config.h +++ b/config.h @@ -153,7 +153,7 @@ #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" diff --git a/configure b/configure index 801ff0a..ffd537e 100755 --- 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 diff --git a/configure.ac b/configure.ac index 53fa250..64d38bb 100644 --- a/configure.ac +++ b/configure.ac @@ -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 index 0000000..feef519 Binary files /dev/null and b/demo/keyboard.png differ diff --git a/demo/styles.css b/demo/styles.css index 7f7f683..d7d5d24 100644 --- a/demo/styles.css +++ b/demo/styles.css @@ -1,156 +1,233 @@ #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; } } diff --git a/demo/vt100.js b/demo/vt100.js index f9345b9..5457b61 100644 --- a/demo/vt100.js +++ b/demo/vt100.js @@ -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') { + //   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) { '' + '' + + '
' + + '
EscF1F2F3F4F5F6F7F8F9F10F11F12
`~1!2@3#4$5%6^7&8*9(0)-_=+ ← 
TabQWERTYUIOP[{]}\|
Tab  ASDFGHJKL;:'"Enter
  ShiftZXCVBNM,<.>/?Shift
XXXCtrlAlt 
   
InsDelHomeEnd
 
 
Ins   
Ins 
' + + '
' + '
' + + '' + + '' + + '' + + '' + + '
     
' + '
 
' + '
' +
                            '
' +
@@ -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) {
           '
  • ' + (this.visualBell ? '' : '') + 'Visual Bell
  • '+ + '
  • ' + + (this.softKeyboard ? '' : '') + + 'Onscreen Keyboard
  • ' + '
  • ' + (this.blinkingCursor ? '' : '') + 'Blinking Cursor
  • '+ @@ -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 index 0000000..b1932f9 --- /dev/null +++ b/shellinabox/keyboard-layout.html @@ -0,0 +1,59 @@ +
    +  
    + EscF1F2F3 + F4F5F6F7 + F8F9F10F11 + F12
    + `~ + 1! + 2@ + 3# + 4$ + 5% + 6^ + 7& + 8* + 9( + 0) + -_ + =+ +  ←  +
    + Tab + QWERTYUIO + P + [{ + ]} + \| +
    + Tab   + ASDFGHJKL + ;: + '" + Enter +
    +    + Shift + ZXCVBNM + ,< + .> + /? + Shift +
    + XXX + Ctrl + Alt +   +
    +     +
    + InsDelHomeEnd +
    +  
    +  
    + Ins    +
    + Ins +   +
    +
    diff --git a/shellinabox/keyboard.png b/shellinabox/keyboard.png new file mode 100644 index 0000000..feef519 Binary files /dev/null and b/shellinabox/keyboard.png differ diff --git a/shellinabox/shell_in_a_box.js b/shellinabox/shell_in_a_box.js index 523d7e0..babce31 100644 --- a/shellinabox/shell_in_a_box.js +++ b/shellinabox/shell_in_a_box.js @@ -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 ? diff --git a/shellinabox/shellinaboxd.c b/shellinabox/shellinaboxd.c index 0b56377..c432ffe 100644 --- a/shellinabox/shellinaboxd.c +++ b/shellinabox/shellinaboxd.c @@ -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. diff --git a/shellinabox/styles.css b/shellinabox/styles.css index 79ad9b2..d96855b 100644 --- a/shellinabox/styles.css +++ b/shellinabox/styles.css @@ -1,140 +1,217 @@ #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. @@ -177,19 +254,19 @@ @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; } } diff --git a/shellinabox/vt100.js b/shellinabox/vt100.js index f9345b9..5457b61 100644 --- a/shellinabox/vt100.js +++ b/shellinabox/vt100.js @@ -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') { + //   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) { '' + '' + + '
    ' + + '
    EscF1F2F3F4F5F6F7F8F9F10F11F12
    `~1!2@3#4$5%6^7&8*9(0)-_=+ ← 
    TabQWERTYUIOP[{]}\|
    Tab  ASDFGHJKL;:'"Enter
      ShiftZXCVBNM,<.>/?Shift
    XXXCtrlAlt 
       
    InsDelHomeEnd
     
     
    Ins   
    Ins 
    ' + + '
    ' + '
    ' + + '' + + '' + + '' + + '' + + '
         
    ' + '
     
    ' + '
    ' +
                                '
    ' +
    @@ -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) {
               '
  • ' + (this.visualBell ? '' : '') + 'Visual Bell
  • '+ + '
  • ' + + (this.softKeyboard ? '' : '') + + 'Onscreen Keyboard
  • ' + '
  • ' + (this.blinkingCursor ? '' : '') + 'Blinking Cursor
  • '+ @@ -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/vt100.jspp b/shellinabox/vt100.jspp index d5c2898..b1a0240 100644 --- a/shellinabox/vt100.jspp +++ b/shellinabox/vt100.jspp @@ -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') { + //   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) { '' + '' + + '
    ' + + KEYBOARD + + '
    ' + '
    ' + + '' + + '' + + '' + + '' + + '
         
    ' + '
     
    ' + '
    ' +
                                '
    ' +
    @@ -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) {
               '
  • ' + (this.visualBell ? '' : '') + 'Visual Bell
  • '+ + '
  • ' + + (this.softKeyboard ? '' : '') + + 'Onscreen Keyboard
  • ' + '
  • ' + (this.blinkingCursor ? '' : '') + 'Blinking Cursor
  • '+ @@ -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(); }