]> git.proxmox.com Git - mirror_novnc.git/commitdiff
Better detection of AltGr on Windows
authorPierre Ossman <ossman@cendio.se>
Fri, 9 Mar 2018 11:14:23 +0000 (12:14 +0100)
committerPierre Ossman <ossman@cendio.se>
Fri, 9 Mar 2018 11:14:23 +0000 (12:14 +0100)
Try to properly detect the fake CtrlL+AltR sequence Windows sends
when pressing AltGr. This allows us to send more accurate key
events over to the server.

core/input/keyboard.js
tests/test.keyboard.js

index 872c9b9a90cdd309f129f855c7e4c3118df0f3c3..93cb53c0840e6102a1fe6e5e49cc5b307edbffad 100644 (file)
@@ -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 = {
@@ -51,33 +52,7 @@ Keyboard.prototype = {
 
         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) {
@@ -119,6 +94,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
@@ -190,6 +189,15 @@ Keyboard.prototype = {
         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);
     },
 
@@ -259,6 +267,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);
@@ -269,6 +285,12 @@ Keyboard.prototype = {
         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) {
index 1c78fd68da581ed1ce451627ee9618088acf974e..78749f3919a2784e428f675e530673ed5a3cc98c 100644 (file)
@@ -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: '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);
         });
     });
 });