diff --git a/LICENSE.txt b/LICENSE.txt index f217929..2ec4a64 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -17,6 +17,7 @@ is not limited to): include/util.js include/websock.js include/webutil.js + include/xtscancodes.js The HTML, CSS, font and images files that included with the noVNC source distibution (or repository) are not considered part of the @@ -45,7 +46,7 @@ the noVNC core library. Here is a list of those files and the original licenses (all MPL 2.0 compatible): include/base64.js : MPL 2.0 - + include/des.js : Various BSD style licenses include/chrome-app/tcp-stream.js @@ -53,7 +54,7 @@ licenses (all MPL 2.0 compatible): utils/websockify utils/websocket.py : LGPL 3 - + utils/inflator.partial.js include/inflator.js : MIT (for pako) diff --git a/include/input.js b/include/input.js index fa6ba44..eb4d18a 100644 --- a/include/input.js +++ b/include/input.js @@ -51,10 +51,18 @@ var Keyboard, Mouse; if (this._onKeyPress) { Util.Debug("onKeyPress " + (e.type == 'keydown' ? "down" : "up") + ", keysym: " + e.keysym.keysym + "(" + e.keysym.keyname + ")"); - this._onKeyPress(e.keysym.keysym, e.type == 'keydown'); + this._onKeyPress(e); } }, + setQEMUVNCKeyboardHandler: function () { + this._handler = new QEMUKeyEventDecoder(kbdUtil.ModifierSync(), + TrackQEMUKeyState( + this._handleRfbEvent.bind(this) + ) + ); + }, + _handleKeyDown: function (e) { if (!this._focused) { return true; } diff --git a/include/keyboard.js b/include/keyboard.js index 8667031..26543db 100644 --- a/include/keyboard.js +++ b/include/keyboard.js @@ -285,6 +285,137 @@ var kbdUtil = (function() { }; })(); +function QEMUKeyEventDecoder(modifierState, next) { + "use strict"; + + function sendAll(evts) { + for (var i = 0; i < evts.length; ++i) { + next(evts[i]); + } + } + + var numPadCodes = ["Numpad0", "Numpad1", "Numpad2", + "Numpad3", "Numpad4", "Numpad5", "Numpad6", + "Numpad7", "Numpad8", "Numpad9", "NumpadDecimal"]; + + var numLockOnKeySyms = { + "Numpad0": 0xffb0, "Numpad1": 0xffb1, "Numpad2": 0xffb2, + "Numpad3": 0xffb3, "Numpad4": 0xffb4, "Numpad5": 0xffb5, + "Numpad6": 0xffb6, "Numpad7": 0xffb7, "Numpad8": 0xffb8, + "Numpad9": 0xffb9, "NumpadDecimal": 0xffac + }; + + var numLockOnKeyCodes = [96, 97, 98, 99, 100, 101, 102, + 103, 104, 105, 108, 110]; + + function isNumPadMultiKey(evt) { + return (numPadCodes.indexOf(evt.code) !== -1); + } + + function getNumPadKeySym(evt) { + if (numLockOnKeyCodes.indexOf(evt.keyCode) !== -1) { + return numLockOnKeySyms[evt.code]; + } + return 0; + } + + function process(evt, type) { + var result = {type: type}; + result.code = evt.code; + result.keysym = 0; + + if (isNumPadMultiKey(evt)) { + result.keysym = getNumPadKeySym(evt); + } + + var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier(); + var isShift = evt.keyCode === 0x10 || evt.key === 'Shift'; + + var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!kbdUtil.nonCharacterKey(evt)); + + next(result); + return suppress; + } + return { + keydown: function(evt) { + sendAll(modifierState.keydown(evt)); + return process(evt, 'keydown'); + }, + keypress: function(evt) { + return true; + }, + keyup: function(evt) { + sendAll(modifierState.keyup(evt)); + return process(evt, 'keyup'); + }, + syncModifiers: function(evt) { + sendAll(modifierState.syncAny(evt)); + }, + releaseAll: function() { next({type: 'releaseall'}); } + }; +} + +function TrackQEMUKeyState(next) { + "use strict"; + var state = []; + + return function (evt) { + var last = state.length !== 0 ? state[state.length-1] : null; + + switch (evt.type) { + case 'keydown': + + if (!last || last.code !== evt.code) { + last = {code: evt.code}; + + if (state.length > 0 && state[state.length-1].code == 'ControlLeft') { + if (evt.code !== 'AltRight') { + next({code: 'ControlLeft', type: 'keydown', keysym: 0}); + } else { + state.pop(); + } + } + state.push(last); + } + if (evt.code !== 'ControlLeft') { + next(evt); + } + break; + + case 'keyup': + if (state.length === 0) { + return; + } + var idx = null; + // do we have a matching key tracked as being down? + for (var i = 0; i !== state.length; ++i) { + if (state[i].code === evt.code) { + idx = i; + break; + } + } + // if we couldn't find a match (it happens), assume it was the last key pressed + if (idx === null) { + if (evt.code === 'ControlLeft') { + return; + } + idx = state.length - 1; + } + + state.splice(idx, 1); + next(evt); + break; + case 'releaseall': + /* jshint shadow: true */ + for (var i = 0; i < state.length; ++i) { + next({code: state[i].code, keysym: 0, type: 'keyup'}); + } + /* jshint shadow: false */ + state = []; + } + }; +} + // Takes a DOM keyboard event and: // - determines which keysym it represents // - determines a keyId identifying the key that was pressed (corresponding to the key/keyCode properties on the DOM event) diff --git a/include/rfb.js b/include/rfb.js index bc5555a..9935962 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -58,7 +58,8 @@ var RFB; ['ExtendedDesktopSize', -308 ], ['xvp', -309 ], ['Fence', -312 ], - ['ContinuousUpdates', -313 ] + ['ContinuousUpdates', -313 ], + ['QEMUExtendedKeyEvent', -258 ] ]; this._encHandlers = {}; @@ -129,6 +130,9 @@ var RFB; this._viewportDragPos = {}; this._viewportHasMoved = false; + // QEMU Extended Key Event support - default to false + this._qemuExtKeyEventSupported = false; + // set the default value on user-facing properties Util.set_defaults(this, defaults, { 'target': 'null', // VNC display rendering Canvas object @@ -560,9 +564,22 @@ var RFB; } }, - _handleKeyPress: function (keysym, down) { + _handleKeyPress: function (keyevent) { if (this._view_only) { return; } // View only, skip keyboard, events - RFB.messages.keyEvent(this._sock, keysym, down); + + var down = (keyevent.type == 'keydown'); + if (this._qemuExtKeyEventSupported) { + var scancode = XtScancode[keyevent.code]; + if (scancode) { + var keysym = keyevent.keysym; + RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode); + } else { + Util.Error('Unable to find a xt scancode for code = ' + keyevent.code); + } + } else { + keysym = keyevent.keysym.keysym; + RFB.messages.keyEvent(this._sock, keysym, down); + } }, _handleMouseButton: function (x, y, down, bmask) { @@ -1348,6 +1365,42 @@ var RFB; sock.flush(); }, + QEMUExtendedKeyEvent: function (sock, keysym, down, keycode) { + function getRFBkeycode(xt_scancode) { + var upperByte = (keycode >> 8); + var lowerByte = (keycode & 0x00ff); + if (upperByte === 0xe0 && lowerByte < 0x7f) { + lowerByte = lowerByte | 0x80; + return lowerByte; + } + return xt_scancode + } + + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 255; // msg-type + buff[offset + 1] = 0; // sub msg-type + + buff[offset + 2] = (down >> 8); + buff[offset + 3] = down; + + buff[offset + 4] = (keysym >> 24); + buff[offset + 5] = (keysym >> 16); + buff[offset + 6] = (keysym >> 8); + buff[offset + 7] = keysym; + + var RFBkeycode = getRFBkeycode(keycode) + + buff[offset + 8] = (RFBkeycode >> 24); + buff[offset + 9] = (RFBkeycode >> 16); + buff[offset + 10] = (RFBkeycode >> 8); + buff[offset + 11] = RFBkeycode; + + sock._sQlen += 12; + sock.flush(); + }, + pointerEvent: function (sock, x, y, mask) { var buff = sock._sQ; var offset = sock._sQlen; @@ -2259,6 +2312,16 @@ var RFB; compress_lo: function () { Util.Error("Server sent compress level pseudo-encoding"); - } + }, + + QEMUExtendedKeyEvent: function () { + this._FBU.rects--; + + var keyboardEvent = document.createEvent("keyboardEvent"); + if (keyboardEvent.code !== undefined) { + this._qemuExtKeyEventSupported = true; + this._keyboard.setQEMUVNCKeyboardHandler(); + } + }, }; })(); diff --git a/include/ui.js b/include/ui.js index 0386363..d69a4f6 100644 --- a/include/ui.js +++ b/include/ui.js @@ -18,8 +18,9 @@ var UI; // Load supporting scripts window.onscriptsload = function () { UI.load(); }; Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", - "keysymdef.js", "keyboard.js", "input.js", "display.js", - "rfb.js", "keysym.js", "inflator.js"]); + "keysymdef.js", "xtscancodes.js", "keyboard.js", + "input.js", "display.js", "rfb.js", "keysym.js", + "inflator.js"]); UI = { diff --git a/include/xtscancodes.js b/include/xtscancodes.js new file mode 100644 index 0000000..d19a017 --- /dev/null +++ b/include/xtscancodes.js @@ -0,0 +1,146 @@ +var XtScancode = {}; +XtScancode["Escape"] = 0x0001; +XtScancode["Digit1"] = 0x0002; +XtScancode["Digit2"] = 0x0003; +XtScancode["Digit3"] = 0x0004; +XtScancode["Digit4"] = 0x0005; +XtScancode["Digit5"] = 0x0006; +XtScancode["Digit6"] = 0x0007; +XtScancode["Digit7"] = 0x0008; +XtScancode["Digit8"] = 0x0009; +XtScancode["Digit9"] = 0x000A; +XtScancode["Digit0"] = 0x000B; +XtScancode["Minus"] = 0x000C; +XtScancode["Equal"] = 0x000D; +XtScancode["Backspace"] = 0x000E; +XtScancode["Tab"] = 0x000F; +XtScancode["KeyQ"] = 0x0010; +XtScancode["KeyW"] = 0x0011; +XtScancode["KeyE"] = 0x0012; +XtScancode["KeyR"] = 0x0013; +XtScancode["KeyT"] = 0x0014; +XtScancode["KeyY"] = 0x0015; +XtScancode["KeyU"] = 0x0016; +XtScancode["KeyI"] = 0x0017; +XtScancode["KeyO"] = 0x0018; +XtScancode["KeyP"] = 0x0019; +XtScancode["BracketLeft"] = 0x001A; +XtScancode["BracketRight"] = 0x001B; +XtScancode["Enter"] = 0x001C; +XtScancode["ControlLeft"] = 0x001D; +XtScancode["KeyA"] = 0x001E; +XtScancode["KeyS"] = 0x001F; +XtScancode["KeyD"] = 0x0020; +XtScancode["KeyF"] = 0x0021; +XtScancode["KeyG"] = 0x0022; +XtScancode["KeyH"] = 0x0023; +XtScancode["KeyJ"] = 0x0024; +XtScancode["KeyK"] = 0x0025; +XtScancode["KeyL"] = 0x0026; +XtScancode["Semicolon"] = 0x0027; +XtScancode["Quote"] = 0x0028; +XtScancode["Backquote"] = 0x0029; +XtScancode["ShiftLeft"] = 0x002A; +XtScancode["Backslash"] = 0x002B; +XtScancode["KeyZ"] = 0x002C; +XtScancode["KeyX"] = 0x002D; +XtScancode["KeyC"] = 0x002E; +XtScancode["KeyV"] = 0x002F; +XtScancode["KeyB"] = 0x0030; +XtScancode["KeyN"] = 0x0031; +XtScancode["KeyM"] = 0x0032; +XtScancode["Comma"] = 0x0033; +XtScancode["Period"] = 0x0034; +XtScancode["Slash"] = 0x0035; +XtScancode["ShiftRight"] = 0x0036; +XtScancode["NumpadMultiply"] = 0x0037; +XtScancode["AltLeft"] = 0x0038; +XtScancode["Space"] = 0x0039; +XtScancode["CapsLock"] = 0x003A; +XtScancode["F1"] = 0x003B; +XtScancode["F2"] = 0x003C; +XtScancode["F3"] = 0x003D; +XtScancode["F4"] = 0x003E; +XtScancode["F5"] = 0x003F; +XtScancode["F6"] = 0x0040; +XtScancode["F7"] = 0x0041; +XtScancode["F8"] = 0x0042; +XtScancode["F9"] = 0x0043; +XtScancode["F10"] = 0x0044; +XtScancode["Pause"] = 0xE045; +XtScancode["ScrollLock"] = 0x0046; +XtScancode["Numpad7"] = 0x0047; +XtScancode["Numpad8"] = 0x0048; +XtScancode["Numpad9"] = 0x0049; +XtScancode["NumpadSubtract"] = 0x004A; +XtScancode["Numpad4"] = 0x004B; +XtScancode["Numpad5"] = 0x004C; +XtScancode["Numpad6"] = 0x004D; +XtScancode["NumpadAdd"] = 0x004E; +XtScancode["Numpad1"] = 0x004F; +XtScancode["Numpad2"] = 0x0050; +XtScancode["Numpad3"] = 0x0051; +XtScancode["Numpad0"] = 0x0052; +XtScancode["NumpadDecimal"] = 0x0053; +XtScancode["IntlBackslash"] = 0x0056; +XtScancode["F11"] = 0x0057; +XtScancode["F12"] = 0x0058; +XtScancode["IntlYen"] = 0x007D; +XtScancode["MediaTrackPrevious"] = 0xE010; +XtScancode["MediaTrackNext"] = 0xE019; +XtScancode["NumpadEnter"] = 0xE01C; +XtScancode["ControlRight"] = 0xE01D; +XtScancode["VolumeMute"] = 0xE020; +XtScancode["MediaPlayPause"] = 0xE022; +XtScancode["MediaStop"] = 0xE024; +XtScancode["VolumeDown"] = 0xE02E; +XtScancode["VolumeUp"] = 0xE030; +XtScancode["BrowserHome"] = 0xE032; +XtScancode["NumpadDivide"] = 0xE035; +XtScancode["PrintScreen"] = 0xE037; +XtScancode["AltRight"] = 0xE038; +XtScancode["NumLock"] = 0x0045; +XtScancode["Home"] = 0xE047; +XtScancode["ArrowUp"] = 0xE048; +XtScancode["PageUp"] = 0xE049; +XtScancode["ArrowLeft"] = 0xE04B; +XtScancode["ArrowRight"] = 0xE04D; +XtScancode["End"] = 0xE04F; +XtScancode["ArrowDown"] = 0xE050; +XtScancode["PageDown"] = 0xE051; +XtScancode["Insert"] = 0xE052; +XtScancode["Delete"] = 0xE053; +XtScancode["OSLeft"] = 0xE05B; +XtScancode["OSRight"] = 0xE05C; +XtScancode["ContextMenu"] = 0xE05D; +XtScancode["BrowserSearch"] = 0xE065; +XtScancode["BrowserFavorites"] = 0xE066; +XtScancode["BrowserRefresh"] = 0xE067; +XtScancode["BrowserStop"] = 0xE068; +XtScancode["BrowserForward"] = 0xE069; +XtScancode["BrowserBack"] = 0xE06A; +XtScancode["NumpadComma"] = 0x007E; +XtScancode["NumpadEqual"] = 0x0059; +XtScancode["F13"] = 0x0064; +XtScancode["F14"] = 0x0065; +XtScancode["F15"] = 0x0066; +XtScancode["F16"] = 0x0067; +XtScancode["F17"] = 0x0068; +XtScancode["F18"] = 0x0069; +XtScancode["F19"] = 0x006A; +XtScancode["F20"] = 0x006B; +XtScancode["F21"] = 0x006C; +XtScancode["F22"] = 0x006D; +XtScancode["F23"] = 0x006E; +XtScancode["F24"] = 0x0076; +XtScancode["KanaMode"] = 0x0070; +XtScancode["Lang2"] = 0x0071; +XtScancode["Lang1"] = 0x0072; +XtScancode["IntlRo"] = 0x0073; +XtScancode["Convert"] = 0x0079; +XtScancode["NonConvert"] = 0x007B; +XtScancode["LaunchApp2"] = 0xE021; +XtScancode["Power"] = 0xE05E; +XtScancode["LaunchApp1"] = 0xE06B; +XtScancode["LaunchMail"] = 0xE06C; +XtScancode["MediaSelect"] = 0xE06D; diff --git a/karma.conf.js b/karma.conf.js index 870b855..2c49ffc 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -115,6 +115,7 @@ module.exports = function(config) { 'include/base64.js', 'include/keysym.js', 'include/keysymdef.js', + 'include/xtscancodes.js', 'include/keyboard.js', 'include/input.js', 'include/websock.js', diff --git a/tests/input.html b/tests/input.html index 8416379..301a7f8 100644 --- a/tests/input.html +++ b/tests/input.html @@ -20,16 +20,17 @@ - + - - + + +