Merge 31c02f5fa0 into 75d69b9f62
This commit is contained in:
commit
0a087e3d3b
|
|
@ -2,3 +2,4 @@
|
||||||
*.o
|
*.o
|
||||||
tests/data_*.js
|
tests/data_*.js
|
||||||
utils/rebind.so
|
utils/rebind.so
|
||||||
|
node_modules
|
||||||
|
|
|
||||||
2164
include/input.js
2164
include/input.js
File diff suppressed because one or more lines are too long
|
|
@ -222,7 +222,8 @@ function constructor() {
|
||||||
'onKeyPress': keyPress});
|
'onKeyPress': keyPress});
|
||||||
mouse = new Mouse({'target': conf.target,
|
mouse = new Mouse({'target': conf.target,
|
||||||
'onMouseButton': mouseButton,
|
'onMouseButton': mouseButton,
|
||||||
'onMouseMove': mouseMove});
|
'onMouseMove': mouseMove,
|
||||||
|
'notify': keyboard.sync});
|
||||||
|
|
||||||
rmode = display.get_render_mode();
|
rmode = display.get_render_mode();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,253 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,243 @@
|
||||||
|
// 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])});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,61 @@
|
||||||
|
fs = require('fs');
|
||||||
|
|
||||||
|
// Set this to false to omit key names from the generated keysymdef.js
|
||||||
|
// This reduces the file size by around 40kb, but may hinder debugging
|
||||||
|
var USE_KEYNAMES=true;
|
||||||
|
|
||||||
|
var buf = fs.readFileSync("keysymdef.h");
|
||||||
|
var str = buf.toString('utf8');
|
||||||
|
|
||||||
|
var re = /^\#define XK_([a-zA-Z_0-9]+)\s+0x([0-9a-fA-F]+)\s*(\/\*\s*(.*)\s*\*\/)?\s*$/m;
|
||||||
|
|
||||||
|
var arr = str.split('\n');
|
||||||
|
|
||||||
|
var keysyms = {}
|
||||||
|
var codepoints = {}
|
||||||
|
|
||||||
|
for (var i = 0; i < arr.length; ++i) {
|
||||||
|
//console.log(arr[i]);
|
||||||
|
var result = re.exec(arr[i]);
|
||||||
|
if (result){
|
||||||
|
var keyname = result[1];
|
||||||
|
var keysym = parseInt(result[2], 16);
|
||||||
|
var remainder = result[3]
|
||||||
|
|
||||||
|
var val = {keyname: keyname, keysym: keysym};
|
||||||
|
keysyms[keysym] = keyname;
|
||||||
|
|
||||||
|
var unicodeRes = /U\+([0-9a-fA-F]+)/.exec(remainder);
|
||||||
|
if (unicodeRes) {
|
||||||
|
var unicode = parseInt(unicodeRes[1], 16);
|
||||||
|
if (!codepoints[unicode]){
|
||||||
|
codepoints[unicode] = keysym;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("no unicode codepoint found:", arr[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("line is not a keysym:", arr[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var out = "// This file describes mappings from Unicode codepoints to the keysym values\n" +
|
||||||
|
"// (and optionally, key names) expected by the RFB protocol\n" +
|
||||||
|
"// How this file was generated:\n" +
|
||||||
|
"// Install node.js and run the command 'node parse.js'\n" +
|
||||||
|
"var keysyms = (function(){\n" +
|
||||||
|
" var keynames = {keysyms};\n" +
|
||||||
|
" var codepoints = {codepoints};\n" +
|
||||||
|
"\n" +
|
||||||
|
" function lookup(k) { return k ? {keysym: k, keyname: keynames ? keynames[k] : k} : undefined; }\n" +
|
||||||
|
" return {\n" +
|
||||||
|
" fromUnicode : function(u) { return lookup(codepoints[u]); },\n" +
|
||||||
|
" lookup : lookup\n" +
|
||||||
|
" };\n" +
|
||||||
|
"})();\n";
|
||||||
|
out = out.replace('{keysyms}', USE_KEYNAMES ? JSON.stringify(keysyms) : "null");
|
||||||
|
out = out.replace('{codepoints}', JSON.stringify(codepoints));
|
||||||
|
|
||||||
|
fs.writeFileSync("keysymdef.js", out);
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Mocha Tests</title>
|
||||||
|
<link rel="stylesheet" href="node_modules/mocha/mocha.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!--
|
||||||
|
Keyboard handling unit tests
|
||||||
|
To run, I used npm (from node.js) to install the libraries chai and mocha in $(PWD)
|
||||||
|
Installing the libraries in any other old way should work as well, provided you update the script paths below
|
||||||
|
-->
|
||||||
|
<div id="mocha"></div>
|
||||||
|
<script src="node_modules/chai/chai.js"></script>
|
||||||
|
<script src="node_modules/mocha/mocha.js"></script>
|
||||||
|
<script>mocha.setup('bdd')</script>
|
||||||
|
<script src="../src/keysymdef.js"></script>
|
||||||
|
<script src="../src/keyboard.js"></script>
|
||||||
|
<script src="../src/helper.js"></script>
|
||||||
|
<script src="test.keyboard.js"></script>
|
||||||
|
<script src="test.helper.js"></script>
|
||||||
|
<script>
|
||||||
|
mocha.checkLeaks();
|
||||||
|
mocha.globals(['navigator']);
|
||||||
|
mocha.run();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Keytest</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="../src/keysymdef.js"></script>
|
||||||
|
<script src="../src/keyboard.js"></script>
|
||||||
|
<script src="../src/helper.js"></script>
|
||||||
|
<div><em>Type some stuff and see what happens</em></div>
|
||||||
|
<input id="clear" type="button" value="Clear" />
|
||||||
|
<div style="width: 100%; height: 600px; border-style:solid; border-width: 1px;"><pre id="log" /></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var elem = document.getElementById('log');
|
||||||
|
var textProp = (elem.textContent !== undefined ? "textContent" : "innerText");
|
||||||
|
function write() {
|
||||||
|
//console.log(arguments);
|
||||||
|
// for each in args, jsonify and print
|
||||||
|
for (var i = 0; i < arguments.length; ++i) {
|
||||||
|
var val = typeof(arguments[i]) === 'string' ? arguments[i] : JSON.stringify(arguments[i]);
|
||||||
|
elem[textProp] += val + ", ";
|
||||||
|
}
|
||||||
|
if (arguments.length != 0) {
|
||||||
|
elem[textProp] += '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('clear').onclick = function() { elem[textProp] = "";}
|
||||||
|
|
||||||
|
|
||||||
|
var k = KeyEventDecoder(ModifierSync(),
|
||||||
|
VerifyCharModifier(
|
||||||
|
TrackKeyState(
|
||||||
|
EscapeModifiers(function(evt) {
|
||||||
|
write(evt.type, evt.keysym.keyname);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
function suppress(evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
if (window.event) {
|
||||||
|
window.event.cancelBubble = true;
|
||||||
|
}
|
||||||
|
//return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onkeydown = function(evt) {
|
||||||
|
if (k.keydown(evt)) { suppress(evt); }
|
||||||
|
};
|
||||||
|
window.onkeypress = function(evt) {
|
||||||
|
if (k.keypress(evt)) { suppress(evt); }
|
||||||
|
};
|
||||||
|
window.onkeyup = function(evt) {
|
||||||
|
if (k.keyup(evt)) { suppress(evt); }
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
var assert = chai.assert;
|
||||||
|
var expect = chai.expect;
|
||||||
|
|
||||||
|
describe('Helpers', function() {
|
||||||
|
describe('keysymFromKeyCode', function() {
|
||||||
|
it('should map known keycodes to keysyms', function() {
|
||||||
|
expect(keysymFromKeyCode(0x41, false), 'a').to.be.equal(0x61);
|
||||||
|
expect(keysymFromKeyCode(0x41, true), 'A').to.be.equal(0x41);
|
||||||
|
expect(keysymFromKeyCode(0xd, false), 'enter').to.be.equal(0xFF0D);
|
||||||
|
expect(keysymFromKeyCode(0x11, false), 'ctrl').to.be.equal(0xFFE3);
|
||||||
|
expect(keysymFromKeyCode(0x12, false), 'alt').to.be.equal(0xFFE9);
|
||||||
|
expect(keysymFromKeyCode(0xe1, false), 'altgr').to.be.equal(0xFE03);
|
||||||
|
expect(keysymFromKeyCode(0x1b, false), 'esc').to.be.equal(0xFF1B);
|
||||||
|
expect(keysymFromKeyCode(0x26, false), 'up').to.be.equal(0xFF52);
|
||||||
|
});
|
||||||
|
it('should return null for unknown keycodes', function() {
|
||||||
|
expect(keysymFromKeyCode(0xc0, false), 'DK æ').to.be.null;
|
||||||
|
expect(keysymFromKeyCode(0xde, false), 'DK ø').to.be.null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('keysyms.fromUnicode', function() {
|
||||||
|
it('should map ASCII characters to keysyms', function() {
|
||||||
|
expect(keysyms.fromUnicode('a'.charCodeAt())).to.have.property('keysym', 0x61);
|
||||||
|
expect(keysyms.fromUnicode('A'.charCodeAt())).to.have.property('keysym', 0x41);
|
||||||
|
});
|
||||||
|
it('should map Latin-1 characters to keysyms', function() {
|
||||||
|
expect(keysyms.fromUnicode('ø'.charCodeAt())).to.have.property('keysym', 0xf8);
|
||||||
|
|
||||||
|
expect(keysyms.fromUnicode('é'.charCodeAt())).to.have.property('keysym', 0xe9);
|
||||||
|
});
|
||||||
|
it('should map characters that are in Windows-1252 but not in Latin-1 to keysyms', function() {
|
||||||
|
expect(keysyms.fromUnicode('Š'.charCodeAt())).to.have.property('keysym', 0x01a9);
|
||||||
|
});
|
||||||
|
it('should map characters which aren\'t in Latin1 *or* Windows-1252 to keysyms', function() {
|
||||||
|
expect(keysyms.fromUnicode('ŵ'.charCodeAt())).to.have.property('keysym', 0x1000175);
|
||||||
|
});
|
||||||
|
it('should return undefined for unknown codepoints', function() {
|
||||||
|
expect(keysyms.fromUnicode('\n'.charCodeAt())).to.be.undefined;
|
||||||
|
expect(keysyms.fromUnicode('\u1F686'.charCodeAt())).to.be.undefined;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('nonCharacterKey', function() {
|
||||||
|
it('should recognize the right keys', function() {
|
||||||
|
expect(nonCharacterKey({keyCode: 0xd}), 'enter').to.be.defined;
|
||||||
|
expect(nonCharacterKey({keyCode: 0x08}), 'backspace').to.be.defined;
|
||||||
|
expect(nonCharacterKey({keyCode: 0x09}), 'tab').to.be.defined;
|
||||||
|
expect(nonCharacterKey({keyCode: 0x10}), 'shift').to.be.defined;
|
||||||
|
expect(nonCharacterKey({keyCode: 0x11}), 'ctrl').to.be.defined;
|
||||||
|
expect(nonCharacterKey({keyCode: 0x12}), 'alt').to.be.defined;
|
||||||
|
expect(nonCharacterKey({keyCode: 0xe0}), 'meta').to.be.defined;
|
||||||
|
});
|
||||||
|
it('should not recognize character keys', function() {
|
||||||
|
expect(nonCharacterKey({keyCode: 'A'}), 'A').to.be.null;
|
||||||
|
expect(nonCharacterKey({keyCode: '1'}), '1').to.be.null;
|
||||||
|
expect(nonCharacterKey({keyCode: '.'}), '.').to.be.null;
|
||||||
|
expect(nonCharacterKey({keyCode: ' '}), 'space').to.be.null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getKeysym', function() {
|
||||||
|
it('should prefer char', function() {
|
||||||
|
expect(getKeysym({char : 'a', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.have.property('keysym', 0x61);
|
||||||
|
});
|
||||||
|
it('should use charCode if no char', function() {
|
||||||
|
expect(getKeysym({char : '', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.have.property('keysym', 0x01a9);
|
||||||
|
expect(getKeysym({charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.have.property('keysym', 0x01a9);
|
||||||
|
expect(getKeysym({char : 'hello', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.have.property('keysym', 0x01a9);
|
||||||
|
});
|
||||||
|
it('should use keyCode if no charCode', function() {
|
||||||
|
expect(getKeysym({keyCode: 0x42, which: 0x43, shiftKey: false})).to.have.property('keysym', 0x62);
|
||||||
|
expect(getKeysym({keyCode: 0x42, which: 0x43, shiftKey: true})).to.have.property('keysym', 0x42);
|
||||||
|
});
|
||||||
|
it('should use which if no keyCode', function() {
|
||||||
|
expect(getKeysym({which: 0x43, shiftKey: false})).to.have.property('keysym', 0x63);
|
||||||
|
expect(getKeysym({which: 0x43, shiftKey: true})).to.have.property('keysym', 0x43);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Modifier Sync', function() { // return a list of fake events necessary to fix modifier state
|
||||||
|
describe('Toggle all modifiers', function() {
|
||||||
|
var sync = ModifierSync();
|
||||||
|
it ('should do nothing if all modifiers are up as expected', function() {
|
||||||
|
expect(sync.keydown({
|
||||||
|
keyCode: 0x41,
|
||||||
|
ctrlKey: false,
|
||||||
|
altKey: false,
|
||||||
|
altGraphKey: false,
|
||||||
|
shiftKey: false,
|
||||||
|
metaKey: false})
|
||||||
|
).to.have.lengthOf(0);
|
||||||
|
});
|
||||||
|
it ('should synthesize events if all keys are unexpectedly down', function() {
|
||||||
|
var result = sync.keydown({
|
||||||
|
keyCode: 0x41,
|
||||||
|
ctrlKey: true,
|
||||||
|
altKey: true,
|
||||||
|
altGraphKey: true,
|
||||||
|
shiftKey: true,
|
||||||
|
metaKey: true
|
||||||
|
});
|
||||||
|
expect(result).to.have.lengthOf(5);
|
||||||
|
var keysyms = {};
|
||||||
|
for (var i = 0; i < result.length; ++i) {
|
||||||
|
keysyms[result[i].keysym] = (result[i].type == 'keydown');
|
||||||
|
}
|
||||||
|
expect(keysyms[0xffe3]);
|
||||||
|
expect(keysyms[0xffe9]);
|
||||||
|
expect(keysyms[0xfe03]);
|
||||||
|
expect(keysyms[0xffe1]);
|
||||||
|
expect(keysyms[0xffe7]);
|
||||||
|
});
|
||||||
|
it ('should do nothing if all modifiers are down as expected', function() {
|
||||||
|
expect(sync.keydown({
|
||||||
|
keyCode: 0x41,
|
||||||
|
ctrlKey: true,
|
||||||
|
altKey: true,
|
||||||
|
altGraphKey: true,
|
||||||
|
shiftKey: true,
|
||||||
|
metaKey: true
|
||||||
|
})).to.have.lengthOf(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Toggle Ctrl', function() {
|
||||||
|
var sync = ModifierSync();
|
||||||
|
it('should sync if modifier is suddenly down', function() {
|
||||||
|
expect(sync.keydown({
|
||||||
|
keyCode: 0x41,
|
||||||
|
ctrlKey: true,
|
||||||
|
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe3), type: 'keydown'}]);
|
||||||
|
});
|
||||||
|
it('should sync if modifier is suddenly up', function() {
|
||||||
|
expect(sync.keydown({
|
||||||
|
keyCode: 0x41,
|
||||||
|
ctrlKey: false
|
||||||
|
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe3), type: 'keyup'}]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Toggle Alt', function() {
|
||||||
|
var sync = ModifierSync();
|
||||||
|
it('should sync if modifier is suddenly down', function() {
|
||||||
|
expect(sync.keydown({
|
||||||
|
keyCode: 0x41,
|
||||||
|
altKey: true,
|
||||||
|
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe9), type: 'keydown'}]);
|
||||||
|
});
|
||||||
|
it('should sync if modifier is suddenly up', function() {
|
||||||
|
expect(sync.keydown({
|
||||||
|
keyCode: 0x41,
|
||||||
|
altKey: false
|
||||||
|
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe9), type: 'keyup'}]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Toggle AltGr', function() {
|
||||||
|
var sync = ModifierSync();
|
||||||
|
it('should sync if modifier is suddenly down', function() {
|
||||||
|
expect(sync.keydown({
|
||||||
|
keyCode: 0x41,
|
||||||
|
altGraphKey: true,
|
||||||
|
})).to.be.deep.equal([{keysym: keysyms.lookup(0xfe03), type: 'keydown'}]);
|
||||||
|
});
|
||||||
|
it('should sync if modifier is suddenly up', function() {
|
||||||
|
expect(sync.keydown({
|
||||||
|
keyCode: 0x41,
|
||||||
|
altGraphKey: false
|
||||||
|
})).to.be.deep.equal([{keysym: keysyms.lookup(0xfe03), type: 'keyup'}]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Toggle Shift', function() {
|
||||||
|
var sync = ModifierSync();
|
||||||
|
it('should sync if modifier is suddenly down', function() {
|
||||||
|
expect(sync.keydown({
|
||||||
|
keyCode: 0x41,
|
||||||
|
shiftKey: true,
|
||||||
|
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe1), type: 'keydown'}]);
|
||||||
|
});
|
||||||
|
it('should sync if modifier is suddenly up', function() {
|
||||||
|
expect(sync.keydown({
|
||||||
|
keyCode: 0x41,
|
||||||
|
shiftKey: false
|
||||||
|
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe1), type: 'keyup'}]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Toggle Meta', function() {
|
||||||
|
var sync = ModifierSync();
|
||||||
|
it('should sync if modifier is suddenly down', function() {
|
||||||
|
expect(sync.keydown({
|
||||||
|
keyCode: 0x41,
|
||||||
|
metaKey: true,
|
||||||
|
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe7), type: 'keydown'}]);
|
||||||
|
});
|
||||||
|
it('should sync if modifier is suddenly up', function() {
|
||||||
|
expect(sync.keydown({
|
||||||
|
keyCode: 0x41,
|
||||||
|
metaKey: false
|
||||||
|
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe7), type: 'keyup'}]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Modifier keyevents', function() {
|
||||||
|
it('should not sync a modifier on its own events', function() {
|
||||||
|
expect(ModifierSync().keydown({
|
||||||
|
keyCode: 0x11,
|
||||||
|
ctrlKey: false
|
||||||
|
})).to.be.deep.equal([]);
|
||||||
|
expect(ModifierSync().keydown({
|
||||||
|
keyCode: 0x11,
|
||||||
|
ctrlKey: true
|
||||||
|
}), 'B').to.be.deep.equal([]);
|
||||||
|
})
|
||||||
|
it('should update state on modifier keyevents', function() {
|
||||||
|
var sync = ModifierSync();
|
||||||
|
sync.keydown({
|
||||||
|
keyCode: 0x11,
|
||||||
|
});
|
||||||
|
expect(sync.keydown({
|
||||||
|
keyCode: 0x41,
|
||||||
|
ctrlKey: true,
|
||||||
|
})).to.be.deep.equal([]);
|
||||||
|
});
|
||||||
|
it('should sync other modifiers on ctrl events', function() {
|
||||||
|
expect(ModifierSync().keydown({
|
||||||
|
keyCode: 0x11,
|
||||||
|
altKey: true
|
||||||
|
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe9), type: 'keydown'}]);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
describe('Sync on non-key events', function() {
|
||||||
|
it('should generate sync events when receiving non-keyboard events', function() {
|
||||||
|
expect(ModifierSync().syncAny({
|
||||||
|
altKey: true
|
||||||
|
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe9), type: 'keydown'}]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,840 @@
|
||||||
|
var assert = chai.assert;
|
||||||
|
var expect = chai.expect;
|
||||||
|
|
||||||
|
|
||||||
|
describe('Key Event Pipeline Stages', function() {
|
||||||
|
describe('Decode Keyboard Events', function() {
|
||||||
|
it('should pass events to the next stage', function(done) {
|
||||||
|
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||||
|
expect(evt).to.be.an.object;
|
||||||
|
done();
|
||||||
|
}).keydown({keyCode: 0x41});
|
||||||
|
});
|
||||||
|
it('should pass the right keysym through', function(done) {
|
||||||
|
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||||
|
expect(evt.keysym).to.be.deep.equal(keysyms.lookup(0x61));
|
||||||
|
done();
|
||||||
|
}).keypress({keyCode: 0x41});
|
||||||
|
});
|
||||||
|
it('should pass the right keyid through', function(done) {
|
||||||
|
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||||
|
expect(evt).to.have.property('keyId', 0x41);
|
||||||
|
done();
|
||||||
|
}).keydown({keyCode: 0x41});
|
||||||
|
});
|
||||||
|
it('should not sync modifiers on a keypress', function() {
|
||||||
|
// Firefox provides unreliable modifier state on keypress events
|
||||||
|
var count = 0;
|
||||||
|
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||||
|
++count;
|
||||||
|
}).keypress({keyCode: 0x41, ctrlKey: true});
|
||||||
|
expect(count).to.be.equal(1);
|
||||||
|
});
|
||||||
|
it('should sync modifiers if necessary', function(done) {
|
||||||
|
var count = 0;
|
||||||
|
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||||
|
switch (count) {
|
||||||
|
case 0: // fake a ctrl keydown
|
||||||
|
expect(evt).to.be.deep.equal({keysym: keysyms.lookup(0xffe3), type: 'keydown'});
|
||||||
|
++count;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown', keysym: keysyms.lookup(0x61)});
|
||||||
|
done();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}).keydown({keyCode: 0x41, ctrlKey: true});
|
||||||
|
});
|
||||||
|
it('should forward keydown events with the right type', function(done) {
|
||||||
|
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||||
|
expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown'});
|
||||||
|
done();
|
||||||
|
}).keydown({keyCode: 0x41})
|
||||||
|
});
|
||||||
|
it('should forward keyup events with the right type', function(done) {
|
||||||
|
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||||
|
expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keyup'});
|
||||||
|
done();
|
||||||
|
}).keyup({keyCode: 0x41});
|
||||||
|
});
|
||||||
|
it('should forward keypress events with the right type', function(done) {
|
||||||
|
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||||
|
expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keypress'});
|
||||||
|
done();
|
||||||
|
}).keypress({keyCode: 0x41});
|
||||||
|
});
|
||||||
|
it('should generate stalls if a char modifier is down while a key is pressed', function(done) {
|
||||||
|
var count = 0;
|
||||||
|
KeyEventDecoder(ModifierSync([0xfe03]), function(evt) {
|
||||||
|
switch (count) {
|
||||||
|
case 0: // fake altgr
|
||||||
|
expect(evt).to.be.deep.equal({keysym: keysyms.lookup(0xfe03), type: 'keydown'});
|
||||||
|
++count;
|
||||||
|
break;
|
||||||
|
case 1: // stall before processing the 'a' keydown
|
||||||
|
expect(evt).to.be.deep.equal({type: 'stall'});
|
||||||
|
++count;
|
||||||
|
break;
|
||||||
|
case 2: // 'a'
|
||||||
|
expect(evt).to.be.deep.equal({
|
||||||
|
type: 'keydown',
|
||||||
|
keyId: 0x41,
|
||||||
|
keysym: keysyms.lookup(0x61)
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}).keydown({keyCode: 0x41, altGraphKey: true});
|
||||||
|
|
||||||
|
});
|
||||||
|
describe('suppress the right events at the right time', function() {
|
||||||
|
it('should suppress anything while a shortcut modifier is down', function() {
|
||||||
|
var obj = KeyEventDecoder(ModifierSync(), function(evt) {});
|
||||||
|
|
||||||
|
obj.keydown({keyCode: 0x11}); // press ctrl
|
||||||
|
expect(obj.keydown({keyCode: 'A'.charCodeAt()})).to.be.true;
|
||||||
|
expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.true;
|
||||||
|
expect(obj.keydown({keyCode: '1'.charCodeAt()})).to.be.true;
|
||||||
|
expect(obj.keydown({keyCode: 0x3c})).to.be.true; // < key on DK Windows
|
||||||
|
expect(obj.keydown({keyCode: 0xde})).to.be.true; // Ø key on DK
|
||||||
|
});
|
||||||
|
it('should suppress non-character keys', function() {
|
||||||
|
var obj = KeyEventDecoder(ModifierSync(), function(evt) {});
|
||||||
|
|
||||||
|
expect(obj.keydown({keyCode: 0x08}), 'a').to.be.true;
|
||||||
|
expect(obj.keydown({keyCode: 0x09}), 'b').to.be.true;
|
||||||
|
expect(obj.keydown({keyCode: 0x11}), 'd').to.be.true;
|
||||||
|
expect(obj.keydown({keyCode: 0x12}), 'e').to.be.true;
|
||||||
|
});
|
||||||
|
it('should not suppress shift', function() {
|
||||||
|
var obj = KeyEventDecoder(ModifierSync(), function(evt) {});
|
||||||
|
|
||||||
|
expect(obj.keydown({keyCode: 0x10}), 'd').to.be.false;
|
||||||
|
});
|
||||||
|
it('should generate event for shift keydown', function() {
|
||||||
|
var called = false;
|
||||||
|
var obj = KeyEventDecoder(ModifierSync(), function(evt) {
|
||||||
|
expect(evt).to.have.property('keysym');
|
||||||
|
called = true;
|
||||||
|
}).keydown({keyCode: 0x10});
|
||||||
|
expect(called).to.be.true;
|
||||||
|
});
|
||||||
|
it('should not suppress character keys', function() {
|
||||||
|
var obj = KeyEventDecoder(ModifierSync(), function(evt) {});
|
||||||
|
|
||||||
|
expect(obj.keydown({keyCode: 'A'.charCodeAt()})).to.be.false;
|
||||||
|
expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.false;
|
||||||
|
expect(obj.keydown({keyCode: '1'.charCodeAt()})).to.be.false;
|
||||||
|
expect(obj.keydown({keyCode: 0x3c})).to.be.false; // < key on DK Windows
|
||||||
|
expect(obj.keydown({keyCode: 0xde})).to.be.false; // Ø key on DK
|
||||||
|
});
|
||||||
|
it('should not suppress if a char modifier is down', function() {
|
||||||
|
var obj = KeyEventDecoder(ModifierSync([0xfe03]), function(evt) {});
|
||||||
|
|
||||||
|
obj.keydown({keyCode: 0xe1}); // press altgr
|
||||||
|
expect(obj.keydown({keyCode: 'A'.charCodeAt()})).to.be.false;
|
||||||
|
expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.false;
|
||||||
|
expect(obj.keydown({keyCode: '1'.charCodeAt()})).to.be.false;
|
||||||
|
expect(obj.keydown({keyCode: 0x3c})).to.be.false; // < key on DK Windows
|
||||||
|
expect(obj.keydown({keyCode: 0xde})).to.be.false; // Ø key on DK
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Keypress and keyup events', function() {
|
||||||
|
it('should always suppress event propagation', function() {
|
||||||
|
var obj = KeyEventDecoder(ModifierSync(), function(evt) {});
|
||||||
|
|
||||||
|
expect(obj.keypress({keyCode: 'A'.charCodeAt()})).to.be.true;
|
||||||
|
expect(obj.keypress({keyCode: 0x3c})).to.be.true; // < key on DK Windows
|
||||||
|
expect(obj.keypress({keyCode: 0x11})).to.be.true;
|
||||||
|
|
||||||
|
expect(obj.keyup({keyCode: 'A'.charCodeAt()})).to.be.true;
|
||||||
|
expect(obj.keyup({keyCode: 0x3c})).to.be.true; // < key on DK Windows
|
||||||
|
expect(obj.keyup({keyCode: 0x11})).to.be.true;
|
||||||
|
});
|
||||||
|
it('should never generate stalls', function() {
|
||||||
|
var obj = KeyEventDecoder(ModifierSync(), function(evt) {
|
||||||
|
expect(evt.type).to.not.be.equal('stall');
|
||||||
|
});
|
||||||
|
|
||||||
|
obj.keypress({keyCode: 'A'.charCodeAt()});
|
||||||
|
obj.keypress({keyCode: 0x3c});
|
||||||
|
obj.keypress({keyCode: 0x11});
|
||||||
|
|
||||||
|
obj.keyup({keyCode: 'A'.charCodeAt()});
|
||||||
|
obj.keyup({keyCode: 0x3c});
|
||||||
|
obj.keyup({keyCode: 0x11});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('mark events if a char modifier is down', function() {
|
||||||
|
it('should not mark modifiers on a keydown event', function() {
|
||||||
|
var times_called = 0;
|
||||||
|
var obj = KeyEventDecoder(ModifierSync([0xfe03]), function(evt) {
|
||||||
|
switch (times_called++) {
|
||||||
|
case 0: //altgr
|
||||||
|
break;
|
||||||
|
case 1: // 'a'
|
||||||
|
expect(evt).to.not.have.property('escape');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
obj.keydown({keyCode: 0xe1}); // press altgr
|
||||||
|
obj.keydown({keyCode: 'A'.charCodeAt()});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate on events if a single-key char modifier is down', function(done) {
|
||||||
|
var times_called = 0;
|
||||||
|
var obj = KeyEventDecoder(ModifierSync([0xfe03]), function(evt) {
|
||||||
|
switch (times_called++) {
|
||||||
|
case 0: //altgr
|
||||||
|
break;
|
||||||
|
case 1: // 'a'
|
||||||
|
expect(evt).to.be.deep.equal({
|
||||||
|
type: 'keypress',
|
||||||
|
keyId: 'A'.charCodeAt(),
|
||||||
|
keysym: keysyms.lookup('a'.charCodeAt()),
|
||||||
|
escape: [0xfe03]
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
obj.keydown({keyCode: 0xe1}); // press altgr
|
||||||
|
obj.keypress({keyCode: 'A'.charCodeAt()});
|
||||||
|
});
|
||||||
|
it('should indicate on events if a multi-key char modifier is down', function(done) {
|
||||||
|
var times_called = 0;
|
||||||
|
var obj = KeyEventDecoder(ModifierSync([0xffe9, 0xffe3]), function(evt) {
|
||||||
|
switch (times_called++) {
|
||||||
|
case 0: //ctrl
|
||||||
|
break;
|
||||||
|
case 1: //alt
|
||||||
|
break;
|
||||||
|
case 2: // 'a'
|
||||||
|
expect(evt).to.be.deep.equal({
|
||||||
|
type: 'keypress',
|
||||||
|
keyId: 'A'.charCodeAt(),
|
||||||
|
keysym: keysyms.lookup('a'.charCodeAt()),
|
||||||
|
escape: [0xffe9, 0xffe3]
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
obj.keydown({keyCode: 0x11}); // press ctrl
|
||||||
|
obj.keydown({keyCode: 0x12}); // press alt
|
||||||
|
obj.keypress({keyCode: 'A'.charCodeAt()});
|
||||||
|
});
|
||||||
|
it('should not consider a char modifier to be down on the modifier key itself', function() {
|
||||||
|
var obj = KeyEventDecoder(ModifierSync([0xfe03]), function(evt) {
|
||||||
|
expect(evt).to.not.have.property('escape');
|
||||||
|
});
|
||||||
|
|
||||||
|
obj.keydown({keyCode: 0xe1}); // press altgr
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('add/remove keysym', function() {
|
||||||
|
it('should remove keysym from keydown if a char key and no modifier', function() {
|
||||||
|
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||||
|
expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown'});
|
||||||
|
}).keydown({keyCode: 0x41});
|
||||||
|
});
|
||||||
|
it('should not remove keysym from keydown if a shortcut modifier is down', function() {
|
||||||
|
var times_called = 0;
|
||||||
|
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||||
|
switch (times_called++) {
|
||||||
|
case 1:
|
||||||
|
expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keydown'});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}).keydown({keyCode: 0x41, ctrlKey: true});
|
||||||
|
expect(times_called).to.be.equal(2);
|
||||||
|
});
|
||||||
|
it('should not remove keysym from keydown if a char modifier is down', function() {
|
||||||
|
var times_called = 0;
|
||||||
|
KeyEventDecoder(ModifierSync([0xfe03]), function(evt) {
|
||||||
|
switch (times_called++) {
|
||||||
|
case 2:
|
||||||
|
expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keydown'});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}).keydown({keyCode: 0x41, altGraphKey: true});
|
||||||
|
expect(times_called).to.be.equal(3);
|
||||||
|
});
|
||||||
|
it('should not remove keysym from keydown if key is noncharacter', function() {
|
||||||
|
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||||
|
expect(evt, 'bacobjpace').to.be.deep.equal({keyId: 0x09, keysym: keysyms.lookup(0xff09), type: 'keydown'});
|
||||||
|
}).keydown({keyCode: 0x09});
|
||||||
|
|
||||||
|
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||||
|
expect(evt, 'ctrl').to.be.deep.equal({keyId: 0x11, keysym: keysyms.lookup(0xffe3), type: 'keydown'});
|
||||||
|
}).keydown({keyCode: 0x11});
|
||||||
|
});
|
||||||
|
it('should never remove keysym from keypress', function() {
|
||||||
|
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||||
|
expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keypress'});
|
||||||
|
}).keypress({keyCode: 0x41});
|
||||||
|
});
|
||||||
|
it('should never remove keysym from keyup', function() {
|
||||||
|
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||||
|
expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keyup'});
|
||||||
|
}).keyup({keyCode: 0x41});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// on keypress, keyup(?), always set keysym
|
||||||
|
// on keydown, only do it if we don't expect a keypress: if noncharacter OR modifier is down
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Verify that char modifiers are active', function() {
|
||||||
|
it('should pass keydown events through if there is no stall', function(done) {
|
||||||
|
var obj = VerifyCharModifier(function(evt){
|
||||||
|
expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||||
|
done();
|
||||||
|
})({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||||
|
});
|
||||||
|
it('should pass keyup events through if there is no stall', function(done) {
|
||||||
|
var obj = VerifyCharModifier(function(evt){
|
||||||
|
expect(evt).to.deep.equal({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||||
|
done();
|
||||||
|
})({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||||
|
});
|
||||||
|
it('should pass keypress events through if there is no stall', function(done) {
|
||||||
|
var obj = VerifyCharModifier(function(evt){
|
||||||
|
expect(evt).to.deep.equal({type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||||
|
done();
|
||||||
|
})({type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||||
|
});
|
||||||
|
it('should not pass stall events through', function(done){
|
||||||
|
var obj = VerifyCharModifier(function(evt){
|
||||||
|
// should only be called once, for the keydown
|
||||||
|
expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
obj({type: 'stall'});
|
||||||
|
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||||
|
});
|
||||||
|
it('should merge keydown and keypress events if they come after a stall', function(done) {
|
||||||
|
var next_called = false;
|
||||||
|
var obj = VerifyCharModifier(function(evt){
|
||||||
|
// should only be called once, for the keydown
|
||||||
|
expect(next_called).to.be.false;
|
||||||
|
next_called = true;
|
||||||
|
expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x44)});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
obj({type: 'stall'});
|
||||||
|
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||||
|
obj({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x44)});
|
||||||
|
expect(next_called).to.be.false;
|
||||||
|
});
|
||||||
|
it('should preserve modifier attribute when merging if keysyms differ', function(done) {
|
||||||
|
var next_called = false;
|
||||||
|
var obj = VerifyCharModifier(function(evt){
|
||||||
|
// should only be called once, for the keydown
|
||||||
|
expect(next_called).to.be.false;
|
||||||
|
next_called = true;
|
||||||
|
expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x44), escape: [0xffe3]});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
obj({type: 'stall'});
|
||||||
|
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||||
|
obj({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x44), escape: [0xffe3]});
|
||||||
|
expect(next_called).to.be.false;
|
||||||
|
});
|
||||||
|
it('should not preserve modifier attribute when merging if keysyms are the same', function() {
|
||||||
|
var obj = VerifyCharModifier(function(evt){
|
||||||
|
expect(evt).to.not.have.property('escape');
|
||||||
|
});
|
||||||
|
|
||||||
|
obj({type: 'stall'});
|
||||||
|
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||||
|
obj({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x42), escape: [0xffe3]});
|
||||||
|
});
|
||||||
|
it('should not merge keydown and keypress events if there is no stall', function(done) {
|
||||||
|
var times_called = 0;
|
||||||
|
var obj = VerifyCharModifier(function(evt){
|
||||||
|
switch(times_called) {
|
||||||
|
case 0:
|
||||||
|
expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
expect(evt).to.deep.equal({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x44)});
|
||||||
|
done();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
++times_called;
|
||||||
|
});
|
||||||
|
|
||||||
|
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||||
|
obj({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x44)});
|
||||||
|
});
|
||||||
|
it('should not merge keydown and keypress events if separated by another event', function(done) {
|
||||||
|
var times_called = 0;
|
||||||
|
var obj = VerifyCharModifier(function(evt){
|
||||||
|
switch(times_called) {
|
||||||
|
case 0:
|
||||||
|
expect(evt,1).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
expect(evt,2).to.deep.equal({type: 'keyup', keyId: 0x43, keysym: keysyms.lookup(0x44)});
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
expect(evt,3).to.deep.equal({type: 'keypress', keyId: 0x45, keysym: keysyms.lookup(0x46)});
|
||||||
|
done();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
++times_called;
|
||||||
|
});
|
||||||
|
|
||||||
|
obj({type: 'stall'});
|
||||||
|
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||||
|
obj({type: 'keyup', keyId: 0x43, keysym: keysyms.lookup(0x44)});
|
||||||
|
obj({type: 'keypress', keyId: 0x45, keysym: keysyms.lookup(0x46)});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Track Key State', function() {
|
||||||
|
it('should do nothing on keyup events if no keys are down', function() {
|
||||||
|
var obj = TrackKeyState(function(evt) {
|
||||||
|
expect(true).to.be.false;
|
||||||
|
});
|
||||||
|
obj({type: 'keyup', keyId: 0x41});
|
||||||
|
});
|
||||||
|
it('should insert into the queue on keydown if no keys are down', function() {
|
||||||
|
var times_called = 0;
|
||||||
|
var elem = null;
|
||||||
|
var keysymsdown = {};
|
||||||
|
var obj = TrackKeyState(function(evt) {
|
||||||
|
++times_called;
|
||||||
|
if (elem.type == 'keyup') {
|
||||||
|
expect(evt).to.have.property('keysym');
|
||||||
|
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||||
|
delete keysymsdown[evt.keysym.keysym];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expect(evt).to.be.deep.equal(elem);
|
||||||
|
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||||
|
}
|
||||||
|
elem = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)};
|
||||||
|
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||||
|
obj(elem);
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keyup', keyId: 0x41};
|
||||||
|
obj(elem);
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
expect(times_called).to.be.equal(2);
|
||||||
|
});
|
||||||
|
it('should insert into the queue on keypress if no keys are down', function() {
|
||||||
|
var times_called = 0;
|
||||||
|
var elem = null;
|
||||||
|
var keysymsdown = {};
|
||||||
|
var obj = TrackKeyState(function(evt) {
|
||||||
|
++times_called;
|
||||||
|
if (elem.type == 'keyup') {
|
||||||
|
expect(evt).to.have.property('keysym');
|
||||||
|
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||||
|
delete keysymsdown[evt.keysym.keysym];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expect(evt).to.be.deep.equal(elem);
|
||||||
|
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||||
|
}
|
||||||
|
elem = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x42)};
|
||||||
|
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||||
|
obj(elem);
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keyup', keyId: 0x41};
|
||||||
|
obj(elem);
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
expect(times_called).to.be.equal(2);
|
||||||
|
});
|
||||||
|
it('should add keysym to last key entry if keyId matches', function() {
|
||||||
|
// this implies that a single keyup will release both keysyms
|
||||||
|
var times_called = 0;
|
||||||
|
var elem = null;
|
||||||
|
var keysymsdown = {};
|
||||||
|
var obj = TrackKeyState(function(evt) {
|
||||||
|
++times_called;
|
||||||
|
if (elem.type == 'keyup') {
|
||||||
|
expect(evt).to.have.property('keysym');
|
||||||
|
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||||
|
delete keysymsdown[evt.keysym.keysym];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expect(evt).to.be.deep.equal(elem);
|
||||||
|
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||||
|
elem = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x42)};
|
||||||
|
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||||
|
obj(elem);
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x43)};
|
||||||
|
keysymsdown[keysyms.lookup(0x43).keysym] = true;
|
||||||
|
obj(elem);
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keyup', keyId: 0x41};
|
||||||
|
obj(elem);
|
||||||
|
expect(times_called).to.be.equal(4);
|
||||||
|
});
|
||||||
|
it('should create new key entry if keyId matches and keysym does not', function() {
|
||||||
|
// this implies that a single keyup will release both keysyms
|
||||||
|
var times_called = 0;
|
||||||
|
var elem = null;
|
||||||
|
var keysymsdown = {};
|
||||||
|
var obj = TrackKeyState(function(evt) {
|
||||||
|
++times_called;
|
||||||
|
if (elem.type == 'keyup') {
|
||||||
|
expect(evt).to.have.property('keysym');
|
||||||
|
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||||
|
delete keysymsdown[evt.keysym.keysym];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expect(evt).to.be.deep.equal(elem);
|
||||||
|
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||||
|
elem = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x42)};
|
||||||
|
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||||
|
obj(elem);
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x43)};
|
||||||
|
keysymsdown[keysyms.lookup(0x43).keysym] = true;
|
||||||
|
obj(elem);
|
||||||
|
expect(times_called).to.be.equal(2);
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keyup', keyId: 0};
|
||||||
|
obj(elem);
|
||||||
|
expect(times_called).to.be.equal(3);
|
||||||
|
elem = {type: 'keyup', keyId: 0};
|
||||||
|
obj(elem);
|
||||||
|
expect(times_called).to.be.equal(4);
|
||||||
|
});
|
||||||
|
it('should merge key entry if keyIds are zero and keysyms match', function() {
|
||||||
|
// this implies that a single keyup will release both keysyms
|
||||||
|
var times_called = 0;
|
||||||
|
var elem = null;
|
||||||
|
var keysymsdown = {};
|
||||||
|
var obj = TrackKeyState(function(evt) {
|
||||||
|
++times_called;
|
||||||
|
if (elem.type == 'keyup') {
|
||||||
|
expect(evt).to.have.property('keysym');
|
||||||
|
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||||
|
delete keysymsdown[evt.keysym.keysym];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expect(evt).to.be.deep.equal(elem);
|
||||||
|
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||||
|
elem = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x42)};
|
||||||
|
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||||
|
obj(elem);
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x42)};
|
||||||
|
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||||
|
obj(elem);
|
||||||
|
expect(times_called).to.be.equal(2);
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keyup', keyId: 0};
|
||||||
|
obj(elem);
|
||||||
|
expect(times_called).to.be.equal(3);
|
||||||
|
});
|
||||||
|
it('should add keysym as separate entry if keyId does not match last event', function() {
|
||||||
|
// this implies that separate keyups are required
|
||||||
|
var times_called = 0;
|
||||||
|
var elem = null;
|
||||||
|
var keysymsdown = {};
|
||||||
|
var obj = TrackKeyState(function(evt) {
|
||||||
|
++times_called;
|
||||||
|
if (elem.type == 'keyup') {
|
||||||
|
expect(evt).to.have.property('keysym');
|
||||||
|
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||||
|
delete keysymsdown[evt.keysym.keysym];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expect(evt).to.be.deep.equal(elem);
|
||||||
|
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||||
|
elem = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x42)};
|
||||||
|
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||||
|
obj(elem);
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keypress', keyId: 0x42, keysym: keysyms.lookup(0x43)};
|
||||||
|
keysymsdown[keysyms.lookup(0x43).keysym] = true;
|
||||||
|
obj(elem);
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keyup', keyId: 0x41};
|
||||||
|
obj(elem);
|
||||||
|
expect(times_called).to.be.equal(4);
|
||||||
|
elem = {type: 'keyup', keyId: 0x42};
|
||||||
|
obj(elem);
|
||||||
|
expect(times_called).to.be.equal(4);
|
||||||
|
});
|
||||||
|
it('should add keysym as separate entry if keyId does not match last event and first is zero', function() {
|
||||||
|
// this implies that separate keyups are required
|
||||||
|
var times_called = 0;
|
||||||
|
var elem = null;
|
||||||
|
var keysymsdown = {};
|
||||||
|
var obj = TrackKeyState(function(evt) {
|
||||||
|
++times_called;
|
||||||
|
if (elem.type == 'keyup') {
|
||||||
|
expect(evt).to.have.property('keysym');
|
||||||
|
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||||
|
delete keysymsdown[evt.keysym.keysym];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expect(evt).to.be.deep.equal(elem);
|
||||||
|
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||||
|
elem = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x42)};
|
||||||
|
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||||
|
obj(elem);
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keydown', keyId: 0x42, keysym: keysyms.lookup(0x43)};
|
||||||
|
keysymsdown[keysyms.lookup(0x43).keysym] = true;
|
||||||
|
obj(elem);
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
expect(times_called).to.be.equal(2);
|
||||||
|
elem = {type: 'keyup', keyId: 0};
|
||||||
|
obj(elem);
|
||||||
|
expect(times_called).to.be.equal(3);
|
||||||
|
elem = {type: 'keyup', keyId: 0x42};
|
||||||
|
obj(elem);
|
||||||
|
expect(times_called).to.be.equal(4);
|
||||||
|
});
|
||||||
|
it('should add keysym as separate entry if keyId does not match last event and second is zero', function() {
|
||||||
|
// this implies that a separate keyups are required
|
||||||
|
var times_called = 0;
|
||||||
|
var elem = null;
|
||||||
|
var keysymsdown = {};
|
||||||
|
var obj = TrackKeyState(function(evt) {
|
||||||
|
++times_called;
|
||||||
|
if (elem.type == 'keyup') {
|
||||||
|
expect(evt).to.have.property('keysym');
|
||||||
|
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||||
|
delete keysymsdown[evt.keysym.keysym];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expect(evt).to.be.deep.equal(elem);
|
||||||
|
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||||
|
elem = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)};
|
||||||
|
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||||
|
obj(elem);
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x43)};
|
||||||
|
keysymsdown[keysyms.lookup(0x43).keysym] = true;
|
||||||
|
obj(elem);
|
||||||
|
expect(elem).to.be.null;
|
||||||
|
elem = {type: 'keyup', keyId: 0x41};
|
||||||
|
obj(elem);
|
||||||
|
expect(times_called).to.be.equal(3);
|
||||||
|
elem = {type: 'keyup', keyId: 0};
|
||||||
|
obj(elem);
|
||||||
|
expect(times_called).to.be.equal(4);
|
||||||
|
});
|
||||||
|
it('should pop matching key event on keyup', function() {
|
||||||
|
var times_called = 0;
|
||||||
|
var obj = TrackKeyState(function(evt) {
|
||||||
|
switch (times_called++) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
expect(evt.type).to.be.equal('keydown');
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0x42, keysym: keysyms.lookup(0x62)});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x61)});
|
||||||
|
obj({type: 'keydown', keyId: 0x42, keysym: keysyms.lookup(0x62)});
|
||||||
|
obj({type: 'keydown', keyId: 0x43, keysym: keysyms.lookup(0x63)});
|
||||||
|
obj({type: 'keyup', keyId: 0x42});
|
||||||
|
expect(times_called).to.equal(4);
|
||||||
|
});
|
||||||
|
it('should pop the first zero keyevent on keyup with zero keyId', function() {
|
||||||
|
var times_called = 0;
|
||||||
|
var obj = TrackKeyState(function(evt) {
|
||||||
|
switch (times_called++) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
expect(evt.type).to.be.equal('keydown');
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0x61)});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
obj({type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x61)});
|
||||||
|
obj({type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x62)});
|
||||||
|
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x63)});
|
||||||
|
obj({type: 'keyup', keyId: 0x0});
|
||||||
|
expect(times_called).to.equal(4);
|
||||||
|
});
|
||||||
|
it('should pop the last keyevents keysym if no match is found for keyId', function() {
|
||||||
|
var times_called = 0;
|
||||||
|
var obj = TrackKeyState(function(evt) {
|
||||||
|
switch (times_called++) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
expect(evt.type).to.be.equal('keydown');
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0x44, keysym: keysyms.lookup(0x63)});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x61)});
|
||||||
|
obj({type: 'keydown', keyId: 0x42, keysym: keysyms.lookup(0x62)});
|
||||||
|
obj({type: 'keydown', keyId: 0x43, keysym: keysyms.lookup(0x63)});
|
||||||
|
obj({type: 'keyup', keyId: 0x44});
|
||||||
|
expect(times_called).to.equal(4);
|
||||||
|
});
|
||||||
|
describe('Firefox sends keypress even when keydown is suppressed', function() {
|
||||||
|
it('should discard the keypress', function() {
|
||||||
|
var times_called = 0;
|
||||||
|
var obj = TrackKeyState(function(evt) {
|
||||||
|
expect(times_called).to.be.equal(0);
|
||||||
|
++times_called;
|
||||||
|
});
|
||||||
|
|
||||||
|
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||||
|
expect(times_called).to.be.equal(1);
|
||||||
|
obj({type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x43)});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('releaseAll', function() {
|
||||||
|
it('should do nothing if no keys have been pressed', function() {
|
||||||
|
var times_called = 0;
|
||||||
|
var obj = TrackKeyState(function(evt) {
|
||||||
|
++times_called;
|
||||||
|
});
|
||||||
|
obj({type: 'releaseall'});
|
||||||
|
expect(times_called).to.be.equal(0);
|
||||||
|
});
|
||||||
|
it('should release the keys that have been pressed', function() {
|
||||||
|
var times_called = 0;
|
||||||
|
var obj = TrackKeyState(function(evt) {
|
||||||
|
switch (times_called++) {
|
||||||
|
case 2:
|
||||||
|
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0x41)});
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0x42)});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||||
|
obj({type: 'keydown', keyId: 0x42, keysym: keysyms.lookup(0x42)});
|
||||||
|
expect(times_called).to.be.equal(2);
|
||||||
|
obj({type: 'releaseall'});
|
||||||
|
expect(times_called).to.be.equal(4);
|
||||||
|
obj({type: 'releaseall'});
|
||||||
|
expect(times_called).to.be.equal(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Escape Modifiers', function() {
|
||||||
|
describe('Keydown', function() {
|
||||||
|
it('should pass through when a char modifier is not down', function() {
|
||||||
|
var times_called = 0;
|
||||||
|
EscapeModifiers(function(evt) {
|
||||||
|
expect(times_called).to.be.equal(0);
|
||||||
|
++times_called;
|
||||||
|
expect(evt).to.be.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||||
|
})({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||||
|
expect(times_called).to.be.equal(1);
|
||||||
|
});
|
||||||
|
it('should generate fake undo/redo events when a char modifier is down', function() {
|
||||||
|
var times_called = 0;
|
||||||
|
EscapeModifiers(function(evt) {
|
||||||
|
switch(times_called++) {
|
||||||
|
case 0:
|
||||||
|
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0xffe9)});
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0xffe3)});
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
expect(evt).to.be.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42), escape: [0xffe9, 0xffe3]});
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
expect(evt).to.be.deep.equal({type: 'keydown', keyId: 0, keysym: keysyms.lookup(0xffe9)});
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
expect(evt).to.be.deep.equal({type: 'keydown', keyId: 0, keysym: keysyms.lookup(0xffe3)});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
})({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42), escape: [0xffe9, 0xffe3]});
|
||||||
|
expect(times_called).to.be.equal(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Keyup', function() {
|
||||||
|
it('should pass through when a char modifier is down', function() {
|
||||||
|
var times_called = 0;
|
||||||
|
EscapeModifiers(function(evt) {
|
||||||
|
expect(times_called).to.be.equal(0);
|
||||||
|
++times_called;
|
||||||
|
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x42), escape: [0xfe03]});
|
||||||
|
})({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x42), escape: [0xfe03]});
|
||||||
|
expect(times_called).to.be.equal(1);
|
||||||
|
});
|
||||||
|
it('should pass through when a char modifier is not down', function() {
|
||||||
|
var times_called = 0;
|
||||||
|
EscapeModifiers(function(evt) {
|
||||||
|
expect(times_called).to.be.equal(0);
|
||||||
|
++times_called;
|
||||||
|
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||||
|
})({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||||
|
expect(times_called).to.be.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue