Improve lookup of special keys

Look up keys that are independent of layout and state first,
followed by keys that are only mild variations in layouts.
This is more robust as there might be multiple physical keys
generating the same symbols, and Keysyms don't map directly to
Unicode in all cases.

At the same time switch over to using the modern, standardised
'code' field for lookup.
This commit is contained in:
Pierre Ossman
2017-01-24 17:18:43 +01:00
parent bfa1b237b9
commit f714f7deae
6 changed files with 466 additions and 254 deletions
+59 -38
View File
@@ -108,26 +108,50 @@ describe('Helpers', function() {
describe('Non-character keys', function() {
it('should recognize the right keys', function() {
expect(KeyboardUtil.getKeysym({keyCode: 0x0d})).to.be.equal(0xFF0D);
expect(KeyboardUtil.getKeysym({keyCode: 0x08})).to.be.equal(0xFF08);
expect(KeyboardUtil.getKeysym({keyCode: 0x09})).to.be.equal(0xFF09);
expect(KeyboardUtil.getKeysym({keyCode: 0x10})).to.be.equal(0xFFE1);
expect(KeyboardUtil.getKeysym({keyCode: 0x11})).to.be.equal(0xFFE3);
expect(KeyboardUtil.getKeysym({keyCode: 0x12})).to.be.equal(0xFFE9);
expect(KeyboardUtil.getKeysym({keyCode: 0xe0})).to.be.equal(0xFFE7);
expect(KeyboardUtil.getKeysym({keyCode: 0xe1})).to.be.equal(0xFE03);
expect(KeyboardUtil.getKeysym({keyCode: 0x1b})).to.be.equal(0xFF1B);
expect(KeyboardUtil.getKeysym({keyCode: 0x26})).to.be.equal(0xFF52);
expect(KeyboardUtil.getKeysym({code: 'Enter'})).to.be.equal(0xFF0D);
expect(KeyboardUtil.getKeysym({code: 'Backspace'})).to.be.equal(0xFF08);
expect(KeyboardUtil.getKeysym({code: 'Tab'})).to.be.equal(0xFF09);
expect(KeyboardUtil.getKeysym({code: 'ShiftLeft'})).to.be.equal(0xFFE1);
expect(KeyboardUtil.getKeysym({code: 'ControlLeft'})).to.be.equal(0xFFE3);
expect(KeyboardUtil.getKeysym({code: 'AltLeft'})).to.be.equal(0xFFE9);
expect(KeyboardUtil.getKeysym({code: 'MetaLeft'})).to.be.equal(0xFFEB);
expect(KeyboardUtil.getKeysym({code: 'Escape'})).to.be.equal(0xFF1B);
expect(KeyboardUtil.getKeysym({code: 'ArrowUp'})).to.be.equal(0xFF52);
});
it('should return null for unknown keycodes', function() {
expect(KeyboardUtil.getKeysym({keyCode: 0xc0})).to.be.null;
expect(KeyboardUtil.getKeysym({keyCode: 0xde})).to.be.null;
it('should handle AltGraph', function() {
expect(KeyboardUtil.getKeysym({code: 'AltRight', key: 'AltRight'})).to.be.equal(0xFFEA);
expect(KeyboardUtil.getKeysym({code: 'AltRight', key: 'AltGraph'})).to.be.equal(0xFE03);
});
it('should return null for unknown codes', function() {
expect(KeyboardUtil.getKeysym({code: 'Semicolon'})).to.be.null;
expect(KeyboardUtil.getKeysym({code: 'BracketRight'})).to.be.null;
});
it('should not recognize character keys', function() {
expect(KeyboardUtil.getKeysym({keyCode: 'A'})).to.be.null;
expect(KeyboardUtil.getKeysym({keyCode: '1'})).to.be.null;
expect(KeyboardUtil.getKeysym({keyCode: '.'})).to.be.null;
expect(KeyboardUtil.getKeysym({keyCode: ' '})).to.be.null;
expect(KeyboardUtil.getKeysym({code: 'KeyA'})).to.be.null;
expect(KeyboardUtil.getKeysym({code: 'Digit1'})).to.be.null;
expect(KeyboardUtil.getKeysym({code: 'Period'})).to.be.null;
expect(KeyboardUtil.getKeysym({code: 'Numpad1'})).to.be.null;
});
});
describe('Numpad', function() {
it('should handle Numpad numbers', function() {
expect(KeyboardUtil.getKeysym({code: 'Digit5', key: '5', location: 0})).to.be.equal(0x0035);
expect(KeyboardUtil.getKeysym({code: 'Numpad5', key: '5', location: 3})).to.be.equal(0xFFB5);
});
it('should handle Numpad non-character keys', function() {
expect(KeyboardUtil.getKeysym({code: 'Home', key: 'Home', location: 0})).to.be.equal(0xFF50);
expect(KeyboardUtil.getKeysym({code: 'Numpad5', key: 'Home', location: 3})).to.be.equal(0xFF95);
expect(KeyboardUtil.getKeysym({code: 'Delete', key: 'Delete', location: 0})).to.be.equal(0xFFFF);
expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: 'Delete', location: 3})).to.be.equal(0xFF9F);
});
it('should handle IE/Edge key names', function() {
expect(KeyboardUtil.getKeysym({code: 'Numpad6', key: 'Right', location: 3})).to.be.equal(0xFF98);
expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: 'Del', location: 3})).to.be.equal(0xFF9F);
});
it('should handle Numpad Decimal key', function() {
expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: '.', location: 3})).to.be.equal(0xFFAE);
expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: ',', location: 3})).to.be.equal(0xFFAC);
});
});
});
@@ -137,7 +161,7 @@ describe('Helpers', function() {
var sync = KeyboardUtil.ModifierSync();
it ('should do nothing if all modifiers are up as expected', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
ctrlKey: false,
altKey: false,
altGraphKey: false,
@@ -147,7 +171,7 @@ describe('Helpers', function() {
});
it ('should synthesize events if all keys are unexpectedly down', function() {
var result = sync.keydown({
keyCode: 0x41,
code: 'KeyA',
ctrlKey: true,
altKey: true,
altGraphKey: true,
@@ -167,7 +191,7 @@ describe('Helpers', function() {
});
it ('should do nothing if all modifiers are down as expected', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
ctrlKey: true,
altKey: true,
altGraphKey: true,
@@ -180,13 +204,13 @@ describe('Helpers', function() {
var sync = KeyboardUtil.ModifierSync();
it('should sync if modifier is suddenly down', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
ctrlKey: true,
})).to.be.deep.equal([{keysym: 0xffe3, type: 'keydown'}]);
});
it('should sync if modifier is suddenly up', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
ctrlKey: false
})).to.be.deep.equal([{keysym: 0xffe3, type: 'keyup'}]);
});
@@ -195,13 +219,13 @@ describe('Helpers', function() {
var sync = KeyboardUtil.ModifierSync();
it('should sync if modifier is suddenly down', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
altKey: true,
})).to.be.deep.equal([{keysym: 0xffe9, type: 'keydown'}]);
});
it('should sync if modifier is suddenly up', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
altKey: false
})).to.be.deep.equal([{keysym: 0xffe9, type: 'keyup'}]);
});
@@ -210,13 +234,13 @@ describe('Helpers', function() {
var sync = KeyboardUtil.ModifierSync();
it('should sync if modifier is suddenly down', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
altGraphKey: true,
})).to.be.deep.equal([{keysym: 0xfe03, type: 'keydown'}]);
});
it('should sync if modifier is suddenly up', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
altGraphKey: false
})).to.be.deep.equal([{keysym: 0xfe03, type: 'keyup'}]);
});
@@ -225,13 +249,13 @@ describe('Helpers', function() {
var sync = KeyboardUtil.ModifierSync();
it('should sync if modifier is suddenly down', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
shiftKey: true,
})).to.be.deep.equal([{keysym: 0xffe1, type: 'keydown'}]);
});
it('should sync if modifier is suddenly up', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
shiftKey: false
})).to.be.deep.equal([{keysym: 0xffe1, type: 'keyup'}]);
});
@@ -240,13 +264,13 @@ describe('Helpers', function() {
var sync = KeyboardUtil.ModifierSync();
it('should sync if modifier is suddenly down', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
metaKey: true,
})).to.be.deep.equal([{keysym: 0xffe7, type: 'keydown'}]);
});
it('should sync if modifier is suddenly up', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
metaKey: false
})).to.be.deep.equal([{keysym: 0xffe7, type: 'keyup'}]);
});
@@ -254,27 +278,27 @@ describe('Helpers', function() {
describe('Modifier keyevents', function() {
it('should not sync a modifier on its own events', function() {
expect(KeyboardUtil.ModifierSync().keydown({
keyCode: 0x11,
code: 'ControlLeft',
ctrlKey: false
})).to.be.deep.equal([]);
expect(KeyboardUtil.ModifierSync().keydown({
keyCode: 0x11,
code: 'ControlLeft',
ctrlKey: true
}), 'B').to.be.deep.equal([]);
})
it('should update state on modifier keyevents', function() {
var sync = KeyboardUtil.ModifierSync();
sync.keydown({
keyCode: 0x11,
code: 'ControlLeft',
});
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
ctrlKey: true,
})).to.be.deep.equal([]);
});
it('should sync other modifiers on ctrl events', function() {
expect(KeyboardUtil.ModifierSync().keydown({
keyCode: 0x11,
code: 'ControlLeft',
altKey: true
})).to.be.deep.equal([{keysym: 0xffe9, type: 'keydown'}]);
})
@@ -287,9 +311,6 @@ describe('Helpers', function() {
});
});
describe('do not treat shift as a modifier key', function() {
it('should not treat shift as a shortcut modifier', function() {
expect(KeyboardUtil.hasShortcutModifier([], {0xffe1 : true})).to.be.false;
});
it('should not treat shift as a char modifier', function() {
expect(KeyboardUtil.hasCharModifier([], {0xffe1 : true})).to.be.false;
});
+34 -92
View File
@@ -51,7 +51,7 @@ describe('Key Event Pipeline Stages', function() {
});
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', type: 'keydown'});
expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keydown'});
done();
}).keydown({code: 'KeyA', key: 'a'});
});
@@ -71,65 +71,57 @@ describe('Key Event Pipeline Stages', function() {
it('should suppress anything while a shortcut modifier is down', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
obj.keydown({keyCode: 0x11}); // press ctrl
expect(obj.keydown({key: 'A'})).to.be.true;
expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.true;
expect(obj.keydown({key: '1'})).to.be.true;
expect(obj.keydown({key: '<'})).to.be.true;
expect(obj.keydown({key: 'ø'})).to.be.true;
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) {});
expect(obj.keydown({keyCode: 0x08}), 'a').to.be.true;
expect(obj.keydown({keyCode: 0x09}), 'b').to.be.true;
expect(obj.keydown({keyCode: 0x11}), 'd').to.be.true;
expect(obj.keydown({keyCode: 0x12}), 'e').to.be.true;
});
it('should not suppress shift', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
expect(obj.keydown({keyCode: 0x10}), 'd').to.be.false;
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({keyCode: 0x10});
}).keydown({code: 'ShiftLeft'});
expect(called).to.be.true;
});
it('should not suppress character keys', function() {
it('should suppress character keys with key', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
expect(obj.keydown({key: 'A'})).to.be.false;
expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.false;
expect(obj.keydown({key: '1'})).to.be.false;
expect(obj.keydown({key: '<'})).to.be.false; // < key on DK Windows
expect(obj.keydown({key: 'ø'})).to.be.false; // Ø key on DK
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 if a char modifier is down', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync([0xfe03]), function(evt) {});
it('should not suppress character keys without key', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
obj.keydown({keyCode: 0xe1}); // press altgr
expect(obj.keydown({key: 'A'})).to.be.false;
expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.false;
expect(obj.keydown({key: '1'})).to.be.false;
expect(obj.keydown({key: '<'})).to.be.false;
expect(obj.keydown({key: 'ø'})).to.be.false;
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({key: 'A'})).to.be.true;
expect(obj.keypress({key: '<'})).to.be.true;
expect(obj.keypress({keyCode: 0x11})).to.be.true;
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({key: 'A'})).to.be.true;
expect(obj.keyup({key: '<'})).to.be.true;
expect(obj.keyup({keyCode: 0x11})).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('mark events if a char modifier is down', function() {
@@ -145,7 +137,7 @@ describe('Key Event Pipeline Stages', function() {
}
});
obj.keydown({keyCode: 0xe1}); // press altgr
obj.keydown({code: 'AltRight', key: 'AltGraph'})
obj.keydown({code: 'KeyA', key: 'a'});
});
@@ -167,7 +159,7 @@ describe('Key Event Pipeline Stages', function() {
}
});
obj.keydown({keyCode: 0xe1}); // press altgr
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) {
@@ -190,8 +182,8 @@ describe('Key Event Pipeline Stages', function() {
}
});
obj.keydown({keyCode: 0x11}); // press ctrl
obj.keydown({keyCode: 0x12}); // press alt
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() {
@@ -199,60 +191,10 @@ describe('Key Event Pipeline Stages', function() {
expect(evt).to.not.have.property('escape');
});
obj.keydown({keyCode: 0xe1}); // press altgr
obj.keydown({code: 'AltRight', key: 'AltGraph'})
});
});
describe('add/remove keysym', function() {
it('should remove keysym from keydown if a char key and no modifier', function() {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt).to.be.deep.equal({code: 'KeyA', type: 'keydown'});
}).keydown({code: 'KeyA', key: 'a'});
});
it('should not remove keysym from keydown if a shortcut modifier is down', function() {
var times_called = 0;
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
switch (times_called++) {
case 1:
expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keydown'});
break;
}
}).keydown({code: 'KeyA', key: 'a', ctrlKey: true});
expect(times_called).to.be.equal(2);
});
it('should not remove keysym from keydown if a char modifier is down', function() {
var times_called = 0;
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync([0xfe03]), function(evt) {
switch (times_called++) {
case 1:
expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keydown'});
break;
}
}).keydown({code: 'KeyA', key: 'a', altGraphKey: true});
expect(times_called).to.be.equal(2);
});
it('should not remove keysym from keydown if key is noncharacter', function() {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt, 'tab').to.be.deep.equal({code: 'Tab', keysym: 0xff09, type: 'keydown'});
}).keydown({keyCode: 0x09});
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt, 'ctrl').to.be.deep.equal({code: 'ControlLeft', keysym: 0xffe3, type: 'keydown'});
}).keydown({keyCode: 0x11});
});
it('should never remove keysym from keypress', function() {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keypress'});
}).keypress({code: 'KeyA', key: 'a'});
});
it('should never remove keysym from keyup', function() {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keyup'});
}).keyup({code: 'KeyA', key: 'a'});
});
});
// on keypress, keyup(?), always set keysym
// on keydown, only do it if we don't expect a keypress: if noncharacter OR modifier is down
});
describe('Track Key State', function() {
+1 -1
View File
@@ -51,7 +51,7 @@
'core': ["base64.js", "websock.js", "des.js", "input/keysym.js",
"input/keysymdef.js", "input/xtscancodes.js", "input/util.js",
"input/devices.js", "display.js", "rfb.js", "inflator.js",
"input/vkeys.js"],
"input/vkeys.js", "input/fixedkeys.js"],
'tests': ["playback.js"],
'recordings': [fname]});
} else {