diff --git a/core/input/keyboard.js b/core/input/keyboard.js index 081a00ab..72ff222e 100644 --- a/core/input/keyboard.js +++ b/core/input/keyboard.js @@ -21,6 +21,7 @@ export default function Keyboard(target) { this._keyDownList = {}; // List of depressed keys // (even if they are happy) this._pendingKey = null; // Key waiting for keypress + this._altGrArmed = false; // Windows AltGr detection // keep these here so we can refer to them later this._eventHandlers = { @@ -40,35 +41,19 @@ Keyboard.prototype = { // ===== PRIVATE METHODS ===== _sendKeyEvent: function (keysym, code, down) { + if (down) { + this._keyDownList[code] = keysym; + } else { + // Do we really think this key is down? + if (!(code in this._keyDownList)) { + return; + } + delete this._keyDownList[code]; + } + Log.Debug("onkeyevent " + (down ? "down" : "up") + ", keysym: " + keysym, ", code: " + code); - - // Windows sends CtrlLeft+AltRight when you press - // AltGraph, which tends to confuse the hell out of - // remote systems. Fake a release of these keys until - // there is a way to detect AltGraph properly. - var fakeAltGraph = false; - if (down && browser.isWindows()) { - if ((code !== 'ControlLeft') && - (code !== 'AltRight') && - ('ControlLeft' in this._keyDownList) && - ('AltRight' in this._keyDownList)) { - fakeAltGraph = true; - this.onkeyevent(this._keyDownList['AltRight'], - 'AltRight', false); - this.onkeyevent(this._keyDownList['ControlLeft'], - 'ControlLeft', false); - } - } - this.onkeyevent(keysym, code, down); - - if (fakeAltGraph) { - this.onkeyevent(this._keyDownList['ControlLeft'], - 'ControlLeft', true); - this.onkeyevent(this._keyDownList['AltRight'], - 'AltRight', true); - } }, _getKeyCode: function (e) { @@ -110,6 +95,30 @@ Keyboard.prototype = { var code = this._getKeyCode(e); var keysym = KeyboardUtil.getKeysym(e); + // Windows doesn't have a proper AltGr, but handles it using + // fake Ctrl+Alt. However the remote end might not be Windows, + // so we need to merge those in to a single AltGr event. We + // detect this case by seeing the two key events directly after + // each other with a very short time between them (<50ms). + if (this._altGrArmed) { + this._altGrArmed = false; + clearTimeout(this._altGrTimeout); + + if ((code === "AltRight") && + ((e.timeStamp - this._altGrCtrlTime) < 50)) { + // FIXME: We fail to detect this if either Ctrl key is + // first manually pressed as Windows then no + // longer sends the fake Ctrl down event. It + // does however happily send real Ctrl events + // even when AltGr is already down. Some + // browsers detect this for us though and set the + // key to "AltGraph". + keysym = KeyTable.XK_ISO_Level3_Shift; + } else { + this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); + } + } + // We cannot handle keys we cannot track, but we also need // to deal with virtual keyboards which omit key info // (iOS omits tracking info on keyup events, which forces us to @@ -181,7 +190,14 @@ Keyboard.prototype = { this._pendingKey = null; stopEvent(e); - this._keyDownList[code] = keysym; + // Possible start of AltGr sequence? (see above) + if ((code === "ControlLeft") && browser.isWindows() && + !("ControlLeft" in this._keyDownList)) { + this._altGrArmed = true; + this._altGrTimeout = setTimeout(this._handleAltGrTimeout.bind(this), 100); + this._altGrCtrlTime = e.timeStamp; + return; + } this._sendKeyEvent(keysym, code, true); }, @@ -211,8 +227,6 @@ Keyboard.prototype = { return; } - this._keyDownList[code] = keysym; - this._sendKeyEvent(keysym, code, true); }, _handleKeyPressTimeout: function (e) { @@ -246,8 +260,6 @@ Keyboard.prototype = { keysym = 0; } - this._keyDownList[code] = keysym; - this._sendKeyEvent(keysym, code, true); }, @@ -256,6 +268,14 @@ Keyboard.prototype = { var code = this._getKeyCode(e); + // We can't get a release in the middle of an AltGr sequence, so + // abort that detection + if (this._altGrArmed) { + this._altGrArmed = false; + clearTimeout(this._altGrTimeout); + this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); + } + // See comment in _handleKeyDown() if (browser.isMac() && (code === 'CapsLock')) { this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); @@ -263,14 +283,13 @@ Keyboard.prototype = { return; } - // Do we really think this key is down? - if (!(code in this._keyDownList)) { - return; - } - this._sendKeyEvent(this._keyDownList[code], code, false); + }, - delete this._keyDownList[code]; + _handleAltGrTimeout: function () { + this._altGrArmed = false; + clearTimeout(this._altGrTimeout); + this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); }, _allKeysUp: function () { @@ -278,7 +297,6 @@ Keyboard.prototype = { for (var code in this._keyDownList) { this._sendKeyEvent(this._keyDownList[code], code, false); }; - this._keyDownList = {}; Log.Debug("<< Keyboard.allKeysUp"); }, diff --git a/tests/test.keyboard.js b/tests/test.keyboard.js index 1c78fd68..78749f39 100644 --- a/tests/test.keyboard.js +++ b/tests/test.keyboard.js @@ -386,108 +386,128 @@ describe('Key Event Handling', function() { } window.navigator.platform = "Windows x86_64"; + + this.clock = sinon.useFakeTimers(); }); afterEach(function () { Object.defineProperty(window, "navigator", origNavigator); + this.clock.restore(); }); - it('should generate fake undo/redo events on press when AltGraph is down', function() { - var times_called = 0; + it('should supress ControlLeft until it knows if it is AltGr', function () { var kbd = new Keyboard(document); - kbd.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(0xFFEA); - expect(code).to.be.equal('AltRight'); - expect(down).to.be.equal(true); - break; - case 2: - expect(keysym).to.be.equal(0xFFEA); - expect(code).to.be.equal('AltRight'); - expect(down).to.be.equal(false); - break; - case 3: - expect(keysym).to.be.equal(0xFFE3); - expect(code).to.be.equal('ControlLeft'); - 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(0xFFE3); - expect(code).to.be.equal('ControlLeft'); - expect(down).to.be.equal(true); - break; - case 6: - expect(keysym).to.be.equal(0xFFEA); - expect(code).to.be.equal('AltRight'); - expect(down).to.be.equal(true); - break; - } - }; - // First the modifier combo + kbd.onkeyevent = sinon.spy(); kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); - kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2})); - // Next a normal character - kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); - expect(times_called).to.be.equal(7); + expect(kbd.onkeyevent).to.not.have.been.called; }); - it('should no do anything on key release', function() { - var times_called = 0; + + it('should not trigger on repeating ControlLeft', function () { var kbd = new Keyboard(document); - kbd.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.onkeyevent = sinon.spy(); kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); - kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2})); - // Next a normal character + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); + expect(kbd.onkeyevent).to.have.been.calledTwice; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, "ControlLeft", true); + expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffe3, "ControlLeft", true); + }); + + it('should not supress ControlRight', function () { + var kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlRight', key: 'Control', location: 2})); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent).to.have.been.calledWith(0xffe4, "ControlRight", true); + }); + + it('should release ControlLeft after 100 ms', function () { + var kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); + expect(kbd.onkeyevent).to.not.have.been.called; + this.clock.tick(100); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent).to.have.been.calledWith(0xffe3, "ControlLeft", true); + }); + + it('should release ControlLeft on other key press', function () { + var kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); + expect(kbd.onkeyevent).to.not.have.been.called; kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); + expect(kbd.onkeyevent).to.have.been.calledTwice; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, "ControlLeft", true); + expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0x61, "KeyA", true); + + // Check that the timer is properly dead + kbd.onkeyevent.reset(); + this.clock.tick(100); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + + it('should release ControlLeft on other key release', function () { + var kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0x61, "KeyA", true); kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'})); - expect(times_called).to.be.equal(8); + expect(kbd.onkeyevent).to.have.been.calledThrice; + expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffe3, "ControlLeft", true); + expect(kbd.onkeyevent.thirdCall).to.have.been.calledWith(0x61, "KeyA", false); + + // Check that the timer is properly dead + kbd.onkeyevent.reset(); + this.clock.tick(100); + expect(kbd.onkeyevent).to.not.have.been.called; }); - it('should not consider a char modifier to be down on the modifier key itself', function() { - var times_called = 0; + + it('should generate AltGraph for quick Ctrl+Alt sequence', function () { var kbd = new Keyboard(document); - kbd.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', location: 1})); - kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt', location: 1})); - // Then one of the keys again - kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); - expect(times_called).to.be.equal(3); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1, timeStamp: Date.now()})); + this.clock.tick(20); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2, timeStamp: Date.now()})); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent).to.have.been.calledWith(0xfe03, 'AltRight', true); + + // Check that the timer is properly dead + kbd.onkeyevent.reset(); + this.clock.tick(100); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + + it('should generate Ctrl, Alt for slow Ctrl+Alt sequence', function () { + var kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1, timeStamp: Date.now()})); + this.clock.tick(60); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2, timeStamp: Date.now()})); + expect(kbd.onkeyevent).to.have.been.calledTwice; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, "ControlLeft", true); + expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffea, "AltRight", true); + + // Check that the timer is properly dead + kbd.onkeyevent.reset(); + this.clock.tick(100); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + + it('should pass through single Alt', function () { + var kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2})); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent).to.have.been.calledWith(0xffea, 'AltRight', true); + }); + + it('should pass through single AltGr', function () { + var kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'AltGraph', location: 2})); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent).to.have.been.calledWith(0xfe03, 'AltRight', true); }); }); });