diff options
author | Pierre Ossman <ossman@cendio.se> | 2017-01-27 10:36:10 +0100 |
---|---|---|
committer | Pierre Ossman <ossman@cendio.se> | 2017-05-04 12:13:47 +0200 |
commit | f7363fd26dd7d9fced3bce4c629f5119d8b476ae (patch) | |
tree | deceba4bc76ddcb1feb49244e93aa2c33bd310df | |
parent | 9e6f71cb753405507f8977ed8cb64f11189ac4c2 (diff) | |
download | novnc-f7363fd26dd7d9fced3bce4c629f5119d8b476ae.tar.gz |
Move keyboard handling in to Keyboard class
Replace the multi stage pipeline system with something simpler.
That level of abstraction is not needed.
-rw-r--r-- | core/input/devices.js | 214 | ||||
-rw-r--r-- | core/input/util.js | 184 | ||||
-rw-r--r-- | tests/test.keyboard.js | 807 |
3 files changed, 414 insertions, 791 deletions
diff --git a/core/input/devices.js b/core/input/devices.js index 2308be9..f981c6f 100644 --- a/core/input/devices.js +++ b/core/input/devices.js @@ -22,18 +22,13 @@ const Keyboard = function (defaults) { this._keyDownList = []; // List of depressed keys // (even if they are happy) + this._modifierState = KeyboardUtil.ModifierSync(); + set_defaults(this, defaults, { 'target': document, 'focused': true }); - // create the keyboard handler - this._handler = new KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), - KeyboardUtil.TrackKeyState( - KeyboardUtil.EscapeModifiers(this._handleRfbEvent.bind(this)) - ) - ); - // keep these here so we can refer to them later this._eventHandlers = { 'keyup': this._handleKeyUp.bind(this), @@ -46,47 +41,220 @@ const Keyboard = function (defaults) { Keyboard.prototype = { // private methods - _handleRfbEvent: function (e) { - if (this._onKeyEvent) { - Log.Debug("onKeyEvent " + (e.type == 'keydown' ? "down" : "up") + - ", keysym: " + e.keysym); - this._onKeyEvent(e.keysym, e.code, e.type == 'keydown'); + _sendKeyEvent: function (keysym, code, down) { + if (!this._onKeyEvent) { + return; + } + + Log.Debug("onKeyEvent " + (down ? "down" : "up") + + ", keysym: " + keysym, ", code: " + code); + + this._onKeyEvent(keysym, code, down); + }, + + _getKeyCode: function (e) { + var code = KeyboardUtil.getKeycode(e); + if (code === 'Unidentified') { + // Unstable, but we don't have anything else to go on + // (don't use it for 'keypress' events thought since + // WebKit sets it to the same as charCode) + if (e.keyCode && (e.type !== 'keypress')) { + code = 'Platform' + e.keyCode; + } } + + return code; }, _handleKeyDown: function (e) { if (!this._focused) { return; } - if (this._handler.keydown(e)) { - // Suppress bubbling/default actions + this._modifierState.keydown(e); + + var code = this._getKeyCode(e); + var keysym = KeyboardUtil.getKeysym(e); + + // If this is a legacy browser then we'll need to wait for + // a keypress event as well. Otherwise we supress the + // browser's handling at this point + if (keysym) { stopEvent(e); + } + + // if a char modifier is pressed, get the keys it consists + // of (on Windows, AltGr is equivalent to Ctrl+Alt) + var active = this._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) { + isCharModifier = true; + } + } + if (!isCharModifier) { + var escape = this._modifierState.activeCharModifier(); + } + } + + var last; + if (this._keyDownList.length === 0) { + last = null; } else { - // Allow the event to bubble and become a keyPress event which - // will have the character code translated + last = this._keyDownList[this._keyDownList.length-1]; + } + + // insert a new entry if last seen key was different. + if (!last || code === 'Unidentified' || last.code !== code) { + last = {code: code, keysyms: {}}; + this._keyDownList.push(last); + } + + // Wait for keypress? + if (!keysym) { + return; + } + + // 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[keysym] = keysym; + last.ignoreKeyPress = true; + + // undo modifiers + if (escape) { + for (var i = 0; i < escape.length; ++i) { + this._sendKeyEvent(escape[i], 'Unidentified', false); + } + } + + // send the character event + this._sendKeyEvent(keysym, code, true); + + // redo modifiers + if (escape) { + for (i = 0; i < escape.length; ++i) { + this._sendKeyEvent(escape[i], 'Unidentified', true); + } } }, + // Legacy event for browsers without code/key _handleKeyPress: function (e) { if (!this._focused) { return; } - if (this._handler.keypress(e)) { - // Suppress bubbling/default actions - stopEvent(e); + stopEvent(e); + + var code = this._getKeyCode(e); + var keysym = KeyboardUtil.getKeysym(e); + + // if a char modifier is pressed, get the keys it consists + // of (on Windows, AltGr is equivalent to Ctrl+Alt) + var active = this._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) { + isCharModifier = true; + } + } + if (!isCharModifier) { + var escape = this._modifierState.activeCharModifier(); + } + } + + var last; + if (this._keyDownList.length === 0) { + last = null; + } else { + last = this._keyDownList[this._keyDownList.length-1]; + } + + if (!last) { + last = {code: code, keysyms: {}}; + this._keyDownList.push(last); + } + if (!keysym) { + console.log('keypress with no keysym:', e); + return; + } + + // 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 (last.ignoreKeyPress) { + return; + } + + last.keysyms[keysym] = keysym; + + // undo modifiers + if (escape) { + for (var i = 0; i < escape.length; ++i) { + this._sendKeyEvent(escape[i], 'Unidentified', false); + } + } + + // send the character event + this._sendKeyEvent(keysym, code, true); + + // redo modifiers + if (escape) { + for (i = 0; i < escape.length; ++i) { + this._sendKeyEvent(escape[i], 'Unidentified', true); + } } }, _handleKeyUp: function (e) { if (!this._focused) { return; } - if (this._handler.keyup(e)) { - // Suppress bubbling/default actions - stopEvent(e); + stopEvent(e); + + this._modifierState.keyup(e); + + var code = this._getKeyCode(e); + + if (this._keyDownList.length === 0) { + return; + } + var idx = null; + // do we have a matching key tracked as being down? + for (var i = 0; i !== this._keyDownList.length; ++i) { + if (this._keyDownList[i].code === code) { + idx = i; + break; + } + } + // if we couldn't find a match (it happens), assume it was the last key pressed + if (idx === null) { + idx = this._keyDownList.length - 1; + } + + var item = this._keyDownList.splice(idx, 1)[0]; + for (var key in item.keysyms) { + this._sendKeyEvent(item.keysyms[key], code, false); } }, _allKeysUp: function () { Log.Debug(">> Keyboard.allKeysUp"); - this._handler.releaseAll(); + for (var i = 0; i < this._keyDownList.length; i++) { + var item = this._keyDownList[i]; + for (var key in item.keysyms) { + this._sendKeyEvent(item.keysyms[key], 'Unidentified', false); + } + }; + this._keyDownList = []; Log.Debug("<< Keyboard.allKeysUp"); }, diff --git a/core/input/util.js b/core/input/util.js index e5363a5..110526a 100644 --- a/core/input/util.js +++ b/core/input/util.js @@ -254,187 +254,3 @@ export function getKeysym(evt){ return null; } - -// Takes a DOM keyboard event and: -// - determines which keysym it represents -// - determines a code identifying the key that was pressed (corresponding to the code/keyCode properties on the DOM event) -// - marks each event with an 'escape' property if a modifier was down which should be "escaped" -// This information is collected into an object which is passed to the next() function. (one call per event) -export function KeyEventDecoder (modifierState, next) { - "use strict"; - function process(evt, type) { - var result = {type: type}; - var code = getKeycode(evt); - if (code === 'Unidentified') { - // Unstable, but we don't have anything else to go on - // (don't use it for 'keypress' events thought since - // WebKit sets it to the same as charCode) - if (evt.keyCode && (evt.type !== 'keypress')) { - code = 'Platform' + evt.keyCode; - } - } - result.code = code; - - var keysym = getKeysym(evt); - - // 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) { - result.keysym = keysym; - } - - // 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 = type !== 'keydown' || !!keysym; - - // 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) { - isCharModifier = true; - } - } - if (type === 'keypress' && !isCharModifier) { - result.escape = modifierState.activeCharModifier(); - } - } - - next(result); - - return suppress; - } - - return { - keydown: function(evt) { - modifierState.keydown(evt); - return process(evt, 'keydown'); - }, - keypress: function(evt) { - return process(evt, 'keypress'); - }, - keyup: function(evt) { - modifierState.keyup(evt); - return process(evt, 'keyup'); - }, - releaseAll: function() { next({type: 'releaseall'}); } - }; -}; - -// 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 -export function TrackKeyState (next) { - "use strict"; - 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.code === 'Unidentified' || last.code !== evt.code) { - last = {code: evt.code, 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] = evt.keysym; - last.ignoreKeyPress = true; - next(evt); - } - break; - case 'keypress': - if (!last) { - last = {code: evt.code, 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] = 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].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) { - 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 - var clone = (function(){ - function Clone(){} - return function (obj) { Clone.prototype=obj; return new Clone(); }; - }()); - for (var key in item.keysyms) { - var out = clone(evt); - out.keysym = item.keysyms[key]; - next(out); - } - break; - case 'releaseall': - /* jshint shadow: true */ - for (var i = 0; i < state.length; ++i) { - for (var key in state[i].keysyms) { - var keysym = state[i].keysyms[key]; - next({code: 'Unidentified', keysym: keysym, type: 'keyup'}); - } - } - /* jshint shadow: false */ - 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. -export function EscapeModifiers (next) { - "use strict"; - 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', code: 'Unidentified', keysym: evt.escape[i]}); - } - // send the character event - next(evt); - // redo modifiers - /* jshint shadow: true */ - for (var i = 0; i < evt.escape.length; ++i) { - next({type: 'keydown', code: 'Unidentified', keysym: evt.escape[i]}); - } - /* jshint shadow: false */ - }; -}; diff --git a/tests/test.keyboard.js b/tests/test.keyboard.js index 7ecfcaf..e4ee503 100644 --- a/tests/test.keyboard.js +++ b/tests/test.keyboard.js @@ -1,612 +1,251 @@ var assert = chai.assert; var expect = chai.expect; +import { Keyboard } from '../core/input/devices.js'; import keysyms from '../core/input/keysymdef.js'; import * as KeyboardUtil from '../core/input/util.js'; /* jshint newcap: false, expr: true */ -describe('Key Event Pipeline Stages', function() { +describe('Key Event Handling', function() { "use strict"; - describe('Decode Keyboard Events', function() { - it('should pass events to the next stage', function(done) { - KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) { - expect(evt).to.be.an.object; - done(); - }).keydown({code: 'KeyA', key: 'a'}); - }); - it('should pass the right keysym through', function(done) { - KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) { - expect(evt.keysym).to.be.deep.equal(0x61); - done(); - }).keypress({code: 'KeyA', key: 'a'}); - }); - it('should pass the right keyid through', function(done) { - KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) { - expect(evt).to.have.property('code', 'KeyA'); - done(); - }).keydown({code: 'KeyA', key: 'a'}); - }); - it('should forward keydown events with the right type', function(done) { - KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) { - expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keydown'}); - done(); - }).keydown({code: 'KeyA', key: 'a'}); - }); - it('should forward keyup events with the right type', function(done) { - KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) { - expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keyup'}); - done(); - }).keyup({code: 'KeyA', key: 'a'}); - }); - it('should forward keypress events with the right type', function(done) { - KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) { - expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keypress'}); - done(); - }).keypress({code: 'KeyA', key: 'a'}); - }); - describe('suppress the right events at the right time', function() { - it('should suppress anything while a shortcut modifier is down', function() { - var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {}); - obj.keydown({code: 'ControlLeft'}); - expect(obj.keydown({code: 'KeyA', key: 'a'})).to.be.true; - expect(obj.keydown({code: 'Space', key: ' '})).to.be.true; - expect(obj.keydown({code: 'Digit1', key: '1'})).to.be.true; - expect(obj.keydown({code: 'IntlBackslash', key: '<'})).to.be.true; - expect(obj.keydown({code: 'Semicolon', key: 'ø'})).to.be.true; - }); - it('should suppress non-character keys', function() { - var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {}); + // The real KeyboardEvent constructor might not work everywhere we + // want to run these tests + function keyevent(typeArg, KeyboardEventInit) { + var e = { type: typeArg }; + for (var key in KeyboardEventInit) { + e[key] = KeyboardEventInit[key]; + } + e.stopPropagation = sinon.spy(); + e.preventDefault = sinon.spy(); + return e; + }; - expect(obj.keydown({code: 'Backspace'}), 'a').to.be.true; - expect(obj.keydown({code: 'Tab'}), 'b').to.be.true; - expect(obj.keydown({code: 'ControlLeft'}), 'd').to.be.true; - expect(obj.keydown({code: 'AltLeft'}), 'e').to.be.true; - }); - it('should generate event for shift keydown', function() { - var called = false; - var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) { - expect(evt).to.have.property('keysym'); - called = true; - }).keydown({code: 'ShiftLeft'}); - expect(called).to.be.true; - }); - it('should suppress character keys with key', function() { - var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {}); - - expect(obj.keydown({code: 'KeyA', key: 'a'})).to.be.true; - expect(obj.keydown({code: 'Digit1', key: '1'})).to.be.true; - expect(obj.keydown({code: 'IntlBackslash', key: '<'})).to.be.true; - expect(obj.keydown({code: 'Semicolon', key: 'ø'})).to.be.true; - }); - it('should not suppress character keys without key', function() { - var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {}); - - expect(obj.keydown({code: 'KeyA'})).to.be.false; - expect(obj.keydown({code: 'Digit1'})).to.be.false; - expect(obj.keydown({code: 'IntlBackslash'})).to.be.false; - expect(obj.keydown({code: 'Semicolon'})).to.be.false; - }); - }); - describe('Keypress and keyup events', function() { - it('should always suppress event propagation', function() { - var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {}); - - expect(obj.keypress({code: 'KeyA', key: 'a'})).to.be.true; - expect(obj.keypress({code: 'IntlBackslash', key: '<'})).to.be.true; - expect(obj.keypress({code: 'ControlLeft', key: 'Control'})).to.be.true; - - expect(obj.keyup({code: 'KeyA', key: 'a'})).to.be.true; - expect(obj.keyup({code: 'IntlBackslash', key: '<'})).to.be.true; - expect(obj.keyup({code: 'ControlLeft', key: 'Control'})).to.be.true; + describe('Decode Keyboard Events', function() { + it('should decode keydown events', function(done) { + var kbd = new Keyboard({ + onKeyEvent: function(keysym, code, down) { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + done(); + }}); + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); + }); + it('should decode keyup events', function(done) { + var calls = 0; + var kbd = new Keyboard({ + onKeyEvent: function(keysym, code, down) { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + if (calls++ === 1) { + expect(down).to.be.equal(false); + done(); + } + }}); + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); + kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'})); + }); + + describe('Legacy keypress Events', function() { + it('should wait for keypress when needed', function() { + var callback = sinon.spy(); + var kbd = new Keyboard({onKeyEvent: callback}); + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41})); + expect(callback).to.not.have.been.called; + }); + it('should decode keypress events', function(done) { + var kbd = new Keyboard({ + onKeyEvent: function(keysym, code, down) { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + done(); + }}); + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41})); + kbd._handleKeyPress(keyevent('keypress', {code: 'KeyA', charCode: 0x61})); }); }); - 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 = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync([0xfe03]), function(evt) { - switch (times_called++) { - case 0: //altgr - break; - case 1: // 'a' - expect(evt).to.not.have.property('escape'); - break; - } - }); - - obj.keydown({code: 'AltRight', key: 'AltGraph'}) - obj.keydown({code: 'KeyA', key: 'a'}); - }); - - it('should indicate on events if a single-key char modifier is down', function(done) { - var times_called = 0; - var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync([0xfe03]), function(evt) { - switch (times_called++) { - case 0: //altgr - break; - case 1: // 'a' - expect(evt).to.be.deep.equal({ - type: 'keypress', - code: 'KeyA', - keysym: 0x61, - escape: [0xfe03] - }); - done(); - return; - } - }); - - obj.keydown({code: 'AltRight', key: 'AltGraph'}) - obj.keypress({code: 'KeyA', key: 'a'}); - }); - it('should indicate on events if a multi-key char modifier is down', function(done) { - var times_called = 0; - var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.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', - code: 'KeyA', - keysym: 0x61, - escape: [0xffe9, 0xffe3] - }); - done(); - return; - } - }); - - obj.keydown({code: 'ControlLeft'}); - obj.keydown({code: 'AltLeft'}); - obj.keypress({code: 'KeyA', key: 'a'}); - }); - it('should not consider a char modifier to be down on the modifier key itself', function() { - var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync([0xfe03]), function(evt) { - expect(evt).to.not.have.property('escape'); - }); - - obj.keydown({code: 'AltRight', key: 'AltGraph'}) + describe('suppress the right events at the right time', function() { + it('should suppress anything with a valid key', function() { + var kbd = new Keyboard({}); + var evt = keyevent('keydown', {code: 'KeyA', key: 'a'}); + kbd._handleKeyDown(evt); + expect(evt.preventDefault).to.have.been.called; + evt = keyevent('keyup', {code: 'KeyA', key: 'a'}); + kbd._handleKeyUp(evt); + expect(evt.preventDefault).to.have.been.called; + }); + it('should not suppress keys without key', function() { + var kbd = new Keyboard({}); + var evt = keyevent('keydown', {code: 'KeyA', keyCode: 0x41}); + kbd._handleKeyDown(evt); + expect(evt.preventDefault).to.not.have.been.called; + }); + it('should suppress the following keypress event', function() { + var kbd = new Keyboard({}); + var evt = keyevent('keydown', {code: 'KeyA', keyCode: 0x41}); + kbd._handleKeyDown(evt); + var evt = keyevent('keypress', {code: 'KeyA', charCode: 0x41}); + kbd._handleKeyPress(evt); + expect(evt.preventDefault).to.have.been.called; }); }); }); describe('Track Key State', function() { - it('should do nothing on keyup events if no keys are down', function() { - var obj = KeyboardUtil.TrackKeyState(function(evt) { - expect(true).to.be.false; - }); - obj({type: 'keyup', code: 'KeyA'}); + it('should send release using the same keysym as the press', function(done) { + var kbd = new Keyboard({ + onKeyEvent: function(keysym, code, down) { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + if (!down) { + done(); + } + }}); + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); + kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'b'})); }); - 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 = KeyboardUtil.TrackKeyState(function(evt) { - ++times_called; - if (elem.type == 'keyup') { - expect(evt).to.have.property('keysym'); - expect (keysymsdown[evt.keysym]).to.not.be.undefined; - delete keysymsdown[evt.keysym]; - } - else { - expect(evt).to.be.deep.equal(elem); - expect (keysymsdown[evt.keysym]).to.not.be.undefined; - } - elem = null; - }); - - expect(elem).to.be.null; - elem = {type: 'keydown', code: 'KeyA', keysym: 0x42}; - keysymsdown[0x42] = true; - obj(elem); - expect(elem).to.be.null; - elem = {type: 'keyup', code: 'KeyA'}; - 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 = KeyboardUtil.TrackKeyState(function(evt) { - ++times_called; - if (elem.type == 'keyup') { - expect(evt).to.have.property('keysym'); - expect (keysymsdown[evt.keysym]).to.not.be.undefined; - delete keysymsdown[evt.keysym]; - } - else { - expect(evt).to.be.deep.equal(elem); - expect (keysymsdown[evt.keysym]).to.not.be.undefined; - } - elem = null; - }); - - expect(elem).to.be.null; - elem = {type: 'keypress', code: 'KeyA', keysym: 0x42}; - keysymsdown[0x42] = true; - obj(elem); - expect(elem).to.be.null; - elem = {type: 'keyup', code: 'KeyA'}; - obj(elem); - expect(elem).to.be.null; - expect(times_called).to.be.equal(2); - }); - it('should add keysym to last key entry if code matches', function() { - // this implies that a single keyup will release both keysyms - var times_called = 0; - var elem = null; - var keysymsdown = {}; - var obj = KeyboardUtil.TrackKeyState(function(evt) { - ++times_called; - if (elem.type == 'keyup') { - expect(evt).to.have.property('keysym'); - expect (keysymsdown[evt.keysym]).to.not.be.undefined; - delete keysymsdown[evt.keysym]; - } - else { - expect(evt).to.be.deep.equal(elem); - expect (keysymsdown[evt.keysym]).to.not.be.undefined; - elem = null; - } - }); - - expect(elem).to.be.null; - elem = {type: 'keypress', code: 'KeyA', keysym: 0x42}; - keysymsdown[0x42] = true; - obj(elem); - expect(elem).to.be.null; - elem = {type: 'keypress', code: 'KeyA', keysym: 0x43}; - keysymsdown[0x43] = true; - obj(elem); - expect(elem).to.be.null; - elem = {type: 'keyup', code: 'KeyA'}; - obj(elem); - expect(times_called).to.be.equal(4); - }); - it('should create new key entry if code 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 = KeyboardUtil.TrackKeyState(function(evt) { - ++times_called; - if (elem.type == 'keyup') { - expect(evt).to.have.property('keysym'); - expect (keysymsdown[evt.keysym]).to.not.be.undefined; - delete keysymsdown[evt.keysym]; - } - else { - expect(evt).to.be.deep.equal(elem); - expect (keysymsdown[evt.keysym]).to.not.be.undefined; - elem = null; - } - }); - - expect(elem).to.be.null; - elem = {type: 'keydown', code: 'Unidentified', keysym: 0x42}; - keysymsdown[0x42] = true; - obj(elem); - expect(elem).to.be.null; - elem = {type: 'keydown', code: 'Unidentified', keysym: 0x43}; - keysymsdown[0x43] = true; - obj(elem); - expect(times_called).to.be.equal(2); - expect(elem).to.be.null; - elem = {type: 'keyup', code: 'Unidentified'}; - obj(elem); - expect(times_called).to.be.equal(3); - elem = {type: 'keyup', code: 'Unidentified'}; - obj(elem); - expect(times_called).to.be.equal(4); + it('should do nothing on keyup events if no keys are down', function() { + var callback = sinon.spy(); + var kbd = new Keyboard({onKeyEvent: callback}); + kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'})); + expect(callback).to.not.have.been.called; + }); + it('should send a key release for each key press with the same code', function() { + var callback = sinon.spy(); + var kbd = new Keyboard({onKeyEvent: callback}); + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'b'})); + kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA'})); + expect(callback.callCount).to.be.equal(4); }); - it('should merge key entry if codes 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 = KeyboardUtil.TrackKeyState(function(evt) { - ++times_called; - if (elem.type == 'keyup') { - expect(evt).to.have.property('keysym'); - expect (keysymsdown[evt.keysym]).to.not.be.undefined; - delete keysymsdown[evt.keysym]; - } - else { - expect(evt).to.be.deep.equal(elem); - expect (keysymsdown[evt.keysym]).to.not.be.undefined; - elem = null; - } - }); + }); - expect(elem).to.be.null; - elem = {type: 'keydown', code: 'Unidentified', keysym: 0x42}; - keysymsdown[0x42] = true; - obj(elem); - expect(elem).to.be.null; - elem = {type: 'keydown', code: 'Unidentified', keysym: 0x42}; - keysymsdown[0x42] = true; - obj(elem); - expect(times_called).to.be.equal(2); - expect(elem).to.be.null; - elem = {type: 'keyup', code: 'Unidentified'}; - obj(elem); - expect(times_called).to.be.equal(3); - }); - it('should add keysym as separate entry if code does not match last event', function() { - // this implies that separate keyups are required + describe('Escape Modifiers', function() { + var origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); + if (origNavigator === undefined) { + // Object.getOwnPropertyDescriptor() doesn't work + // properly in any version of IE + this.skip(); + } + + Object.defineProperty(window, "navigator", {value: {}}); + if (window.navigator.platform !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } + + window.navigator.platform = "Windows x86_64"; + }); + afterEach(function () { + Object.defineProperty(window, "navigator", origNavigator); + }); + + it('should generate fake undo/redo events on press when a char modifier is down', function() { var times_called = 0; - var elem = null; - var keysymsdown = {}; - var obj = KeyboardUtil.TrackKeyState(function(evt) { - ++times_called; - if (elem.type == 'keyup') { - expect(evt).to.have.property('keysym'); - expect (keysymsdown[evt.keysym]).to.not.be.undefined; - delete keysymsdown[evt.keysym]; - } - else { - expect(evt).to.be.deep.equal(elem); - expect (keysymsdown[evt.keysym]).to.not.be.undefined; - elem = null; - } - }); - - expect(elem).to.be.null; - elem = {type: 'keypress', code: 'KeyA', keysym: 0x42}; - keysymsdown[0x42] = true; - obj(elem); - expect(elem).to.be.null; - elem = {type: 'keypress', code: 'KeyB', keysym: 0x43}; - keysymsdown[0x43] = true; - obj(elem); - expect(elem).to.be.null; - elem = {type: 'keyup', code: 'KeyA'}; - obj(elem); - expect(times_called).to.be.equal(4); - elem = {type: 'keyup', code: 'KeyB'}; - obj(elem); - expect(times_called).to.be.equal(4); - }); - it('should add keysym as separate entry if code does not match last event and first is zero', function() { - // this implies that separate keyups are required + var kbd = new Keyboard({ + onKeyEvent: function(keysym, code, down) { + switch(times_called++) { + case 0: + expect(keysym).to.be.equal(0xFFE3); + expect(code).to.be.equal('ControlLeft'); + expect(down).to.be.equal(true); + break; + case 1: + expect(keysym).to.be.equal(0xFFE9); + expect(code).to.be.equal('AltLeft'); + expect(down).to.be.equal(true); + break; + case 2: + expect(keysym).to.be.equal(0xFFE9); + expect(code).to.be.equal('Unidentified'); + expect(down).to.be.equal(false); + break; + case 3: + expect(keysym).to.be.equal(0xFFE3); + expect(code).to.be.equal('Unidentified'); + expect(down).to.be.equal(false); + break; + case 4: + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + break; + case 5: + expect(keysym).to.be.equal(0xFFE9); + expect(code).to.be.equal('Unidentified'); + expect(down).to.be.equal(true); + break; + case 6: + expect(keysym).to.be.equal(0xFFE3); + expect(code).to.be.equal('Unidentified'); + expect(down).to.be.equal(true); + break; + } + }}); + // First the modifier combo + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control'})); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt'})); + // Next a normal character + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); + expect(times_called).to.be.equal(7); + }); + it('should no do anything on key release', function() { var times_called = 0; - var elem = null; - var keysymsdown = {}; - var obj = KeyboardUtil.TrackKeyState(function(evt) { - ++times_called; - if (elem.type == 'keyup') { - expect(evt).to.have.property('keysym'); - expect (keysymsdown[evt.keysym]).to.not.be.undefined; - delete keysymsdown[evt.keysym]; - } - else { - expect(evt).to.be.deep.equal(elem); - expect (keysymsdown[evt.keysym]).to.not.be.undefined; - elem = null; - } - }); - - expect(elem).to.be.null; - elem = {type: 'keydown', code: 'Unidentified', keysym: 0x42}; - keysymsdown[0x42] = true; - obj(elem); - expect(elem).to.be.null; - elem = {type: 'keydown', code: 'KeyB', keysym: 0x43}; - keysymsdown[0x43] = true; - obj(elem); - expect(elem).to.be.null; - expect(times_called).to.be.equal(2); - elem = {type: 'keyup', code: 'Unidentified'}; - obj(elem); - expect(times_called).to.be.equal(3); - elem = {type: 'keyup', code: 'KeyB'}; - obj(elem); - expect(times_called).to.be.equal(4); - }); - it('should add keysym as separate entry if code does not match last event and second is zero', function() { - // this implies that a separate keyups are required + var kbd = new Keyboard({ + onKeyEvent: function(keysym, code, down) { + switch(times_called++) { + case 7: + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(false); + break; + } + }}); + // First the modifier combo + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control'})); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt'})); + // Next a normal character + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); + kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'})); + expect(times_called).to.be.equal(8); + }); + it('should not consider a char modifier to be down on the modifier key itself', function() { var times_called = 0; - var elem = null; - var keysymsdown = {}; - var obj = KeyboardUtil.TrackKeyState(function(evt) { - ++times_called; - if (elem.type == 'keyup') { - expect(evt).to.have.property('keysym'); - expect (keysymsdown[evt.keysym]).to.not.be.undefined; - delete keysymsdown[evt.keysym]; - } - else { - expect(evt).to.be.deep.equal(elem); - expect (keysymsdown[evt.keysym]).to.not.be.undefined; - elem = null; - } - }); - - expect(elem).to.be.null; - elem = {type: 'keydown', code: 'KeyA', keysym: 0x42}; - keysymsdown[0x42] = true; - obj(elem); - expect(elem).to.be.null; - elem = {type: 'keydown', code: 'Unidentified', keysym: 0x43}; - keysymsdown[0x43] = true; - obj(elem); - expect(elem).to.be.null; - elem = {type: 'keyup', code: 'KeyA'}; - obj(elem); + var kbd = new Keyboard({ + onKeyEvent: function(keysym, code, down) { + switch(times_called++) { + case 0: + expect(keysym).to.be.equal(0xFFE3); + expect(code).to.be.equal('ControlLeft'); + expect(down).to.be.equal(true); + break; + case 1: + expect(keysym).to.be.equal(0xFFE9); + expect(code).to.be.equal('AltLeft'); + expect(down).to.be.equal(true); + break; + case 2: + expect(keysym).to.be.equal(0xFFE3); + expect(code).to.be.equal('ControlLeft'); + expect(down).to.be.equal(true); + break; + } + }}); + // First the modifier combo + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control'})); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt'})); + // Then one of the keys again + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control'})); expect(times_called).to.be.equal(3); - elem = {type: 'keyup', code: 'Unidentified'}; - obj(elem); - expect(times_called).to.be.equal(4); - }); - it('should pop matching key event on keyup', function() { - var times_called = 0; - var obj = KeyboardUtil.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', code: 'KeyB', keysym: 0x62}); - break; - } - }); - - obj({type: 'keydown', code: 'KeyA', keysym: 0x61}); - obj({type: 'keydown', code: 'KeyB', keysym: 0x62}); - obj({type: 'keydown', code: 'KeyC', keysym: 0x63}); - obj({type: 'keyup', code: 'KeyB'}); - expect(times_called).to.equal(4); - }); - it('should pop the first zero keyevent on keyup with zero code', function() { - var times_called = 0; - var obj = KeyboardUtil.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', code: 'Unidentified', keysym: 0x61}); - break; - } - }); - - obj({type: 'keydown', code: 'Unidentified', keysym: 0x61}); - obj({type: 'keydown', code: 'Unidentified', keysym: 0x62}); - obj({type: 'keydown', code: 'KeyA', keysym: 0x63}); - obj({type: 'keyup', code: 'Unidentified'}); - expect(times_called).to.equal(4); - }); - it('should pop the last keyevents keysym if no match is found for code', function() { - var times_called = 0; - var obj = KeyboardUtil.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', code: 'KeyD', keysym: 0x63}); - break; - } - }); - - obj({type: 'keydown', code: 'KeyA', keysym: 0x61}); - obj({type: 'keydown', code: 'KeyB', keysym: 0x62}); - obj({type: 'keydown', code: 'KeyC', keysym: 0x63}); - obj({type: 'keyup', code: 'KeyD'}); - 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 = KeyboardUtil.TrackKeyState(function(evt) { - expect(times_called).to.be.equal(0); - ++times_called; - }); - - obj({type: 'keydown', code: 'KeyA', keysym: 0x42}); - expect(times_called).to.be.equal(1); - obj({type: 'keypress', code: 'KeyA', keysym: 0x43}); - }); - }); - describe('releaseAll', function() { - it('should do nothing if no keys have been pressed', function() { - var times_called = 0; - var obj = KeyboardUtil.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 = KeyboardUtil.TrackKeyState(function(evt) { - switch (times_called++) { - case 2: - expect(evt).to.be.deep.equal({type: 'keyup', code: 'Unidentified', keysym: 0x41}); - break; - case 3: - expect(evt).to.be.deep.equal({type: 'keyup', code: 'Unidentified', keysym: 0x42}); - break; - } - }); - obj({type: 'keydown', code: 'KeyA', keysym: 0x41}); - obj({type: 'keydown', code: 'KeyB', keysym: 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; - KeyboardUtil.EscapeModifiers(function(evt) { - expect(times_called).to.be.equal(0); - ++times_called; - expect(evt).to.be.deep.equal({type: 'keydown', code: 'KeyA', keysym: 0x42}); - })({type: 'keydown', code: 'KeyA', keysym: 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; - KeyboardUtil.EscapeModifiers(function(evt) { - switch(times_called++) { - case 0: - expect(evt).to.be.deep.equal({type: 'keyup', code: 'Unidentified', keysym: 0xffe9}); - break; - case 1: - expect(evt).to.be.deep.equal({type: 'keyup', code: 'Unidentified', keysym: 0xffe3}); - break; - case 2: - expect(evt).to.be.deep.equal({type: 'keydown', code: 'KeyA', keysym: 0x42, escape: [0xffe9, 0xffe3]}); - break; - case 3: - expect(evt).to.be.deep.equal({type: 'keydown', code: 'Unidentified', keysym: 0xffe9}); - break; - case 4: - expect(evt).to.be.deep.equal({type: 'keydown', code: 'Unidentified', keysym: 0xffe3}); - break; - } - })({type: 'keydown', code: 'KeyA', keysym: 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; - KeyboardUtil.EscapeModifiers(function(evt) { - expect(times_called).to.be.equal(0); - ++times_called; - expect(evt).to.be.deep.equal({type: 'keyup', code: 'KeyA', keysym: 0x42, escape: [0xfe03]}); - })({type: 'keyup', code: 'KeyA', keysym: 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; - KeyboardUtil.EscapeModifiers(function(evt) { - expect(times_called).to.be.equal(0); - ++times_called; - expect(evt).to.be.deep.equal({type: 'keyup', code: 'KeyA', keysym: 0x42}); - })({type: 'keyup', code: 'KeyA', keysym: 0x42}); - expect(times_called).to.be.equal(1); - }); }); }); }); |