From 31c02f5fa0f72aa89ba11fbb86447e33c0c4dab7 Mon Sep 17 00:00:00 2001 From: Jesper Dam Date: Thu, 24 Oct 2013 11:13:51 +0200 Subject: [PATCH] copy actual keyboard code into input.js Pretty hackish, just copy/pasting everything verbatim from keybard.js and helper.js into input.js --- include/input.js | 496 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 496 insertions(+) diff --git a/include/input.js b/include/input.js index 942f6dc8..b0907cbc 100644 --- a/include/input.js +++ b/include/input.js @@ -44,8 +44,504 @@ Util.conf_defaults(conf, that, defaults, [ // ///////// helper.js +function isMac() { + return navigator && !!(/macintosh/i).exec(navigator.appVersion); +} +function isWindows() { + return navigator && !!(/windows/i).exec(navigator.appVersion); +} +function isLinux() { + return navigator && !!(/linux/i).exec(navigator.appVersion); +} + +// on Mac, Option (AKA Alt) is used as a char modifier +var charModifierListMac = [0xffe9]; +// on Windows, Ctrl+Alt is used as a char modifier +var charModifierListWin = [0xffe9, 0xffe3]; +// on Linux, AltGr is used as a char modifier +var charModifierListLinux = [0xfe03] + +// Return true if a modifier which is not the specified char modifier (and is not shift) is down +function hasShortcutModifier(charModifier, currentModifiers) { + var mods = {}; + for (var key in currentModifiers) { + if (key != 0xffe1) { + mods[key] = currentModifiers[key]; + } + } + + var sum = 0; + for (var k in currentModifiers) { + if (mods[k]) { + ++sum; + } + } + if (hasCharModifier(charModifier, mods)) { + return sum > charModifier.length; + } + else { + return sum > 0; + } +} + +// Return true if the specified char modifier is currently down +function hasCharModifier(charModifier, currentModifiers) { + if (charModifier == []) { return false; } + + for (var i = 0; i < charModifier.length; ++i) { + if (!currentModifiers[charModifier[i]]) { + return false; + } + } + return true; +} + +// Helper object tracking modifier key state +// and generates fake key events to compensate if it gets out of sync +function ModifierSync(charModifier) { + if (!charModifier) { + if (isMac()) { + charModifier = charModifierListMac; + } + else if (isWindows()) { + charModifier = charModifierListWin; + } + else if (isLinux()) { + charModifier = charModifierListLinux; + } + else { + charModifier = []; + } + } + var ctrl = 0xffe3; + var alt = 0xffe9; + var altGr = 0xfe03; + var shift = 0xffe1; + var meta = 0xffe7; + + var state = {}; + state[ctrl] = false; + state[alt] = false; + state[altGr] = false; + state[shift] = false; + state[meta] = false; + + + function sync(evt, keysym) { + var result = []; + function syncKey(keysym) { + return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'}; + } + + if (evt.ctrlKey !== undefined && evt.ctrlKey !== state[ctrl] && keysym !== ctrl) { + state[ctrl] = evt.ctrlKey; + result.push(syncKey(ctrl)); + } + if (evt.altKey !== undefined && evt.altKey !== state[alt] && keysym !== alt) { + state[alt] = evt.altKey; + result.push(syncKey(alt)); + } + if (evt.altGraphKey !== undefined && evt.altGraphKey !== state[altGr] && keysym !== altGr) { + state[altGr] = evt.altGraphKey; + result.push(syncKey(altGr)); + } + if (evt.shiftKey !== undefined && evt.shiftKey !== state[shift] && keysym !== shift) { + state[shift] = evt.shiftKey; + result.push(syncKey(shift)); + } + if (evt.metaKey !== undefined && evt.metaKey !== state[meta] && keysym !== meta) { + state[meta] = evt.metaKey; + result.push(syncKey(meta)); + } + return result; + } + function syncKeyEvent(evt, down) { + var obj = getKeysym(evt); + var keysym = obj ? obj.keysym : null; + + // first, apply the event itself, if relevant + if (keysym != null && state[keysym] !== undefined) { + state[keysym] = down; + } + return sync(evt, keysym); + } + + return { + // sync on the appropriate keyboard event + keydown: function(evt) { return syncKeyEvent(evt, true);}, + keyup: function(evt) { return syncKeyEvent(evt, false);}, + // Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway + syncAny: function(evt) { return sync(evt);}, + + // is a shortcut modifier down? + hasShortcutModifier: function() { return hasShortcutModifier(charModifier, state); }, + // if a char modifier is down, return the keys it consists of, otherwise return null + activeCharModifier: function() { return hasCharModifier(charModifier, state) ? charModifier : null; } + }; +} + +// Get a key ID from a keyboard event +// May be a string or an integer depending on the available properties +function getKey(evt){ + if (evt.key) { + return evt.key; + } + else { + return evt.keyCode; + } +} + +// Get the most reliable keysym value we can get from a key event +// if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which +function getKeysym(evt){ + if (evt.char && evt.char.length === 1) { + var codepoint = evt.char.charCodeAt(); + var res = keysyms.fromUnicode(codepoint); + if (res) { + return res; + } + } + if (evt.charCode) { + var res = keysyms.fromUnicode(evt.charCode); + if (res) { + return res; + } + } + // we could check evt.key here. + // Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list, + // so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key + // so we don't *need* it yet + if (evt.keyCode) { + return keysyms.lookup(keysymFromKeyCode(evt.keyCode, evt.shiftKey)); + } + if (evt.which) { + return keysyms.lookup(keysymFromKeyCode(evt.which, evt.shiftKey)); + } + return null; +} + +// Given a keycode, try to predict which keysym it might be. +// If the keycode is unknown, null is returned. +function keysymFromKeyCode(keycode, shiftPressed) { + if (typeof(keycode) !== 'number') { + return null; + } + // won't be accurate for azerty + if (keycode >= 0x30 && keycode <= 0x39) { + return keycode; // digit + } + if (keycode >= 0x41 && keycode <= 0x5a) { + // remap to lowercase unless shift is down + return shiftPressed ? keycode : keycode + 32; // A-Z + } + if (keycode >= 0x60 && keycode <= 0x69) { + return 0xffb0 + (keycode - 0x60); // numpad 0-9 + } + + switch(keycode) { + case 0x20: return 0x20; // space + case 0x6a: return 0xffaa; // multiply + case 0x6b: return 0xffab; // add + case 0x6c: return 0xffac; // separator + case 0x6d: return 0xffad; // subtract + case 0x6e: return 0xffae; // decimal + case 0x6f: return 0xffaf; // divide + case 0xbb: return 0x2b; // + + case 0xbc: return 0x2c; // , + case 0xbd: return 0x2d; // - + case 0xbe: return 0x2e; // . + } + + return nonCharacterKey({keyCode: keycode}); +} + +// if the key is a known non-character key (any key which doesn't generate character data) +// return its keysym value. Otherwise return null +function nonCharacterKey(evt) { + // evt.key not implemented yet + if (!evt.keyCode) { return null; } + var keycode = evt.keyCode; + + if (keycode >= 0x70 && keycode <= 0x87) { + return 0xffbe + keycode - 0x70; // F1-F24 + } + switch (keycode) { + + case 8 : return 0xFF08; // BACKSPACE + case 13 : return 0xFF0D; // ENTER + + case 9 : return 0xFF09; // TAB + + case 27 : return 0xFF1B; // ESCAPE + case 46 : return 0xFFFF; // DELETE + + case 36 : return 0xFF50; // HOME + case 35 : return 0xFF57; // END + case 33 : return 0xFF55; // PAGE_UP + case 34 : return 0xFF56; // PAGE_DOWN + case 45 : return 0xFF63; // INSERT + + case 37 : return 0xFF51; // LEFT + case 38 : return 0xFF52; // UP + case 39 : return 0xFF53; // RIGHT + case 40 : return 0xFF54; // DOWN + case 16 : return 0xFFE1; // SHIFT + case 17 : return 0xFFE3; // CONTROL + case 18 : return 0xFFE9; // Left ALT (Mac Option) + + case 224 : return 0xFE07; // Meta + case 225 : return 0xFE03; // AltGr + case 91 : return 0xFFEC; // Super_L (Win Key) + case 92 : return 0xFFED; // Super_R (Win Key) + case 93 : return 0xFF67; // Menu (Win Menu), Mac Command + default: return null; + } +} ///////// keyboard.js +// 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) +// - synthesizes events to synchronize modifier key state between which modifiers are actually down, and which we thought were down +// - marks each event with an 'escape' property if a modifier was down which should be "escaped" +// - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown +// This information is collected into an object which is passed to the next() function. (one call per event) +function KeyEventDecoder(modifierState, next) { + function sendAll(evts) { + for (var i = 0; i < evts.length; ++i) { + next(evts[i]); + } + } + function process(evt, type) { + var result = {type: type}; + var keyId = getKey(evt); + if (keyId) { + result['keyId'] = keyId; + } + + var keysym = getKeysym(evt); + + var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier(); + // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress? + // "special" keys like enter, tab or backspace don't send keypress events, + // and some browsers don't send keypresses at all if a modifier is down + if (keysym && (type !== 'keydown' || nonCharacterKey(evt) || hasModifier)) { + result['keysym'] = keysym; + } + + var isShift = evt.keyCode == 0x10 || evt.key == 'Shift'; + + // Should we prevent the browser from handling the event? + // Doing so on a keydown (in most browsers) prevents keypress from being generated + // so only do that if we have to. + var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!nonCharacterKey(evt)); + + // If a char modifier is down on a keydown, we need to insert a stall, + // so VerifyCharModifier knows to wait and see if a keypress is comnig + var stall = type === 'keydown' && modifierState.activeCharModifier() && !nonCharacterKey(evt); + + // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt) + var active = modifierState.activeCharModifier(); + + // If we have a char modifier down, and we're able to determine a keysym reliably + // then (a) we know to treat the modifier as a char modifier, + // and (b) we'll have to "escape" the modifier to undo the modifier when sending the char. + if (active && keysym) { + var isCharModifier = false; + for (var i = 0; i < active.length; ++i) { + if (active[i] == keysym.keysym) { + isCharModifier = true; + } + } + if (type == 'keypress' && !isCharModifier) { + result.escape = modifierState.activeCharModifier(); + } + } + + if (stall) { + // insert a fake "stall" event + next({type: 'stall'}); + } + next(result); + + return suppress; + } + + return { + keydown: function(evt) { + sendAll(modifierState.keydown(evt)); + return process(evt, 'keydown'); + }, + keypress: function(evt) { + return process(evt, 'keypress'); + }, + keyup: function(evt) { + sendAll(modifierState.keyup(evt)); + return process(evt, 'keyup'); + }, + syncModifiers: function(evt) { + sendAll(modifierState.syncAny(evt)); + }, + releaseAll: function() { next({type: 'releaseall'}); } + }; +} + +// Combines keydown and keypress events where necessary to handle char modifiers. +// On some OS'es, a char modifier is sometimes used as a shortcut modifier. +// For example, on Windows, AltGr is synonymous with Ctrl-Alt. On a Danish keyboard layout, AltGr-2 yields a @, but Ctrl-Alt-D does nothing +// so when used with the '2' key, Ctrl-Alt counts as a char modifier (and should be escaped), but when used with 'D', it does not. +// The only way we can distinguish these cases is to wait and see if a keypress event arrives +// When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two +function VerifyCharModifier(next) { + var queue = []; + var timer = null; + function process() { + if (timer) { + return; + } + while (queue.length != 0) { + var cur = queue[0]; + queue = queue.splice(1); + switch (cur.type) { + case 'stall': + // insert a delay before processing available events. + timer = setTimeout(function() { + clearTimeout(timer); + timer = null; + process(); + }, 5); + return; + case 'keydown': + // is the next element a keypress? Then we should merge the two + if (queue.length != 0 && queue[0].type == 'keypress') { + // Firefox sends keypress even when no char is generated. + // so, if keypress keysym is the same as we'd have guessed from keydown, + // the modifier didn't have any effect, and should not be escaped + if (queue[0].escape && (!cur.keysym || cur.keysym.keysym !== queue[0].keysym.keysym)) { + cur.escape = queue[0].escape; + } + cur.keysym = queue[0].keysym; + queue = queue.splice(1); + } + break; + } + + // swallow stall events, and pass all others to the next stage + if (cur.type !== 'stall') { + next(cur); + } + } + } + return function(evt) { + queue.push(evt); + process(); + }; +} + +// Keeps track of which keys we (and the server) believe are down +// When a keyup is received, match it against this list, to determine the corresponding keysym(s) +// in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars +// key repeat events should be merged into a single entry. +// Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess +function TrackKeyState(next) { + var state = []; + + return function (evt) { + var last = state.length != 0 ? state[state.length-1] : null; + + switch (evt.type) { + case 'keydown': + // insert a new entry if last seen key was different. + if (!last || !evt.keyId || last.keyId != evt.keyId) { + last = {keyId: evt.keyId, keysyms: {}}; + state.push(last); + } + if (evt.keysym) { + // make sure last event contains this keysym (a single "logical" keyevent + // can cause multiple key events to be sent to the VNC server) + last.keysyms[evt.keysym.keysym] = evt.keysym; + last.ignoreKeyPress = true; + next(evt); + } + break; + case 'keypress': + if (!last) { + last = {keyId: evt.keyId, keysyms: {}}; + state.push(last); + } + if (!evt.keysym) { + console.log('keypress with no keysym:', evt); + } + + // If we didn't expect a keypress, and already sent a keydown to the VNC server + // based on the keydown, make sure to skip this event. + if (evt.keysym && !last.ignoreKeyPress) { + last.keysyms[evt.keysym.keysym] = evt.keysym; + evt.type = 'keydown'; + 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].keyId === evt.keyId) { + idx = i; + break; + } + } + // if we couldn't find a match (it happens), assume it was the last key pressed + if (idx === null) { + idx = state.length - 1; + } + + var item = state.splice(idx, 1)[0]; + // for each keysym tracked by this key entry, clone the current event and override the keysym + for (var key in item.keysyms) { + var clone = (function(){ + return function (obj) { Clone.prototype=obj; return new Clone() }; + function Clone(){} + }()); + var out = clone(evt); + out.keysym = item.keysyms[key]; + next(out); + } + break; + case 'releaseall': + for (var i = 0; i < state.length; ++i) { + for (var key in state[i].keysyms) { + var keysym = state[i].keysyms[key]; + next({keyId: 0, keysym: keysym, type: 'keyup'}); + } + } + state = []; + } + } +} + +// Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @), +// then the modifier must be "undone" before sending the @, and "redone" afterwards. +function EscapeModifiers(next) { + return function(evt) { + if (evt.type != 'keydown' || evt.escape === undefined) { + next(evt); + return; + } + // undo modifiers + for (var i = 0; i < evt.escape.length; ++i) { + next({type: 'keyup', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); + } + // send the character event + next(evt); + // redo modifiers + for (var i = 0; i < evt.escape.length; ++i) { + next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); + } + } +} /////// setup