Merge pull request #596 from danielhb/master

QEMU RFB extension

Fixes #21 🎉 (again)
This commit is contained in:
Solly Ross 2016-08-29 13:28:15 -04:00 committed by GitHub
commit f4f4e8993d
12 changed files with 379 additions and 20 deletions

View File

@ -17,6 +17,7 @@ is not limited to):
include/util.js include/util.js
include/websock.js include/websock.js
include/webutil.js include/webutil.js
include/xtscancodes.js
The HTML, CSS, font and images files that included with the noVNC The HTML, CSS, font and images files that included with the noVNC
source distibution (or repository) are not considered part of the source distibution (or repository) are not considered part of the

View File

@ -51,10 +51,18 @@ var Keyboard, Mouse;
if (this._onKeyPress) { if (this._onKeyPress) {
Util.Debug("onKeyPress " + (e.type == 'keydown' ? "down" : "up") + Util.Debug("onKeyPress " + (e.type == 'keydown' ? "down" : "up") +
", keysym: " + e.keysym.keysym + "(" + e.keysym.keyname + ")"); ", 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) { _handleKeyDown: function (e) {
if (!this._focused) { return true; } if (!this._focused) { return true; }

View File

@ -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: // Takes a DOM keyboard event and:
// - determines which keysym it represents // - determines which keysym it represents
// - determines a keyId identifying the key that was pressed (corresponding to the key/keyCode properties on the DOM event) // - determines a keyId identifying the key that was pressed (corresponding to the key/keyCode properties on the DOM event)

View File

@ -58,7 +58,8 @@ var RFB;
['ExtendedDesktopSize', -308 ], ['ExtendedDesktopSize', -308 ],
['xvp', -309 ], ['xvp', -309 ],
['Fence', -312 ], ['Fence', -312 ],
['ContinuousUpdates', -313 ] ['ContinuousUpdates', -313 ],
['QEMUExtendedKeyEvent', -258 ]
]; ];
this._encHandlers = {}; this._encHandlers = {};
@ -129,6 +130,9 @@ var RFB;
this._viewportDragPos = {}; this._viewportDragPos = {};
this._viewportHasMoved = false; this._viewportHasMoved = false;
// QEMU Extended Key Event support - default to false
this._qemuExtKeyEventSupported = false;
// set the default value on user-facing properties // set the default value on user-facing properties
Util.set_defaults(this, defaults, { Util.set_defaults(this, defaults, {
'target': 'null', // VNC display rendering Canvas object '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 if (this._view_only) { return; } // View only, skip keyboard, events
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); RFB.messages.keyEvent(this._sock, keysym, down);
}
}, },
_handleMouseButton: function (x, y, down, bmask) { _handleMouseButton: function (x, y, down, bmask) {
@ -1348,6 +1365,42 @@ var RFB;
sock.flush(); 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) { pointerEvent: function (sock, x, y, mask) {
var buff = sock._sQ; var buff = sock._sQ;
var offset = sock._sQlen; var offset = sock._sQlen;
@ -2259,6 +2312,16 @@ var RFB;
compress_lo: function () { compress_lo: function () {
Util.Error("Server sent compress level pseudo-encoding"); 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();
} }
},
}; };
})(); })();

View File

@ -18,8 +18,9 @@ var UI;
// Load supporting scripts // Load supporting scripts
window.onscriptsload = function () { UI.load(); }; window.onscriptsload = function () { UI.load(); };
Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
"keysymdef.js", "keyboard.js", "input.js", "display.js", "keysymdef.js", "xtscancodes.js", "keyboard.js",
"rfb.js", "keysym.js", "inflator.js"]); "input.js", "display.js", "rfb.js", "keysym.js",
"inflator.js"]);
UI = { UI = {

146
include/xtscancodes.js Normal file
View File

@ -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;

View File

@ -115,6 +115,7 @@ module.exports = function(config) {
'include/base64.js', 'include/base64.js',
'include/keysym.js', 'include/keysym.js',
'include/keysymdef.js', 'include/keysymdef.js',
'include/xtscancodes.js',
'include/keyboard.js', 'include/keyboard.js',
'include/input.js', 'include/input.js',
'include/websock.js', 'include/websock.js',

View File

@ -28,6 +28,7 @@
<script src="../include/base64.js"></script> <script src="../include/base64.js"></script>
<script src="../include/keysym.js"></script> <script src="../include/keysym.js"></script>
<script src="../include/keysymdef.js"></script> <script src="../include/keysymdef.js"></script>
<script src="../include/xtscancodes.js"></script>
<script src="../include/keyboard.js"></script> <script src="../include/keyboard.js"></script>
<script src="../include/input.js"></script> <script src="../include/input.js"></script>
<script src="../include/display.js"></script> <script src="../include/display.js"></script>

View File

@ -1927,7 +1927,11 @@ describe('Remote Frame Buffer Protocol Client', function() {
}); });
it('should send a key message on a key press', function () { it('should send a key message on a key press', function () {
client._keyboard._onKeyPress(1234, 1); var keyevent = {};
keyevent.type = 'keydown';
keyevent.keysym = {};
keyevent.keysym.keysym = 1234;
client._keyboard._onKeyPress(keyevent);
var key_msg = {_sQ: new Uint8Array(8), _sQlen: 0, flush: function () {}}; var key_msg = {_sQ: new Uint8Array(8), _sQlen: 0, flush: function () {}};
RFB.messages.keyEvent(key_msg, 1234, 1); RFB.messages.keyEvent(key_msg, 1234, 1);
expect(client._sock).to.have.sent(key_msg._sQ); expect(client._sock).to.have.sent(key_msg._sQ);

View File

@ -50,8 +50,9 @@
// Load supporting scripts // Load supporting scripts
Util.load_scripts(["base64.js", "websock.js", "des.js", "keysym.js", Util.load_scripts(["base64.js", "websock.js", "des.js", "keysym.js",
"keysymdef.js", "keyboard.js", "input.js", "display.js", "keysymdef.js", "xtscancodes.js", "keyboard.js",
"rfb.js", "playback.js", "inflator.js", fname]); "input.js", "display.js", "rfb.js", "playback.js",
"inflator.js", fname]);
} else { } else {
msg("Must specifiy data=FOO.js in query string."); msg("Must specifiy data=FOO.js in query string.");
} }

View File

@ -60,8 +60,9 @@
message("Loading " + fname); message("Loading " + fname);
// Load supporting scripts // Load supporting scripts
Util.load_scripts(["base64.js", "websock.js", "des.js", "keysym.js", Util.load_scripts(["base64.js", "websock.js", "des.js", "keysym.js",
"keysymdef.js", "keyboard.js", "input.js", "display.js", "keysymdef.js", "xtscancodes.js", "keyboard.js",
"rfb.js", "playback.js", "inflator.js", fname]); "input.js", "display.js", "rfb.js", "playback.js",
"inflator.js", fname]);
} else { } else {
message("Must specify data=FOO in query string."); message("Must specify data=FOO in query string.");

View File

@ -78,8 +78,9 @@
// Load supporting scripts // Load supporting scripts
Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
"keysymdef.js", "keyboard.js", "input.js", "display.js", "keysymdef.js", "xtscancodes.js", "keyboard.js",
"inflator.js", "rfb.js", "keysym.js"]); "input.js", "display.js", "inflator.js", "rfb.js",
"keysym.js"]);
var rfb; var rfb;
var resizeTimeout; var resizeTimeout;