summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPierre Ossman <ossman@cendio.se>2017-01-27 10:36:10 +0100
committerPierre Ossman <ossman@cendio.se>2017-05-04 12:13:47 +0200
commitf7363fd26dd7d9fced3bce4c629f5119d8b476ae (patch)
treedeceba4bc76ddcb1feb49244e93aa2c33bd310df
parent9e6f71cb753405507f8977ed8cb64f11189ac4c2 (diff)
downloadnovnc-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.js214
-rw-r--r--core/input/util.js184
-rw-r--r--tests/test.keyboard.js807
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);
- });
});
});
});