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 = {
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) {
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
this._pendingKey = null;
stopEvent(e);
+ // 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);
},
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);
this._sendKeyEvent(this._keyDownList[code], code, false);
},
+ _handleAltGrTimeout: function () {
+ this._altGrArmed = false;
+ clearTimeout(this._altGrTimeout);
+ this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
+ },
+
_allKeysUp: function () {
Log.Debug(">> Keyboard.allKeysUp");
for (var code in this._keyDownList) {
}
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: 'KeyA', key: 'a'}));
- kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'}));
- expect(times_called).to.be.equal(8);
+ 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 consider a char modifier to be down on the modifier key itself', function() {
- var times_called = 0;
+
+ it('should not supress ControlRight', 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.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}));
- kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt', location: 1}));
- // Then one of the keys again
+ 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(times_called).to.be.equal(3);
+ 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(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 generate AltGraph for quick 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(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);
});
});
});