Clean up AltGraph handling

It doesn't need to be this general as the issue is mostly about
Windows. Also use the same modifier shuffle that RealVNC and
TigerVNC uses to get macOS working well.
This commit is contained in:
Pierre Ossman 2017-01-27 12:52:24 +01:00
parent ae82053366
commit bf43c26319
3 changed files with 132 additions and 190 deletions

View File

@ -13,6 +13,7 @@ import { isTouchDevice } from '../util/browsers.js'
import { setCapture, releaseCapture, stopEvent, getPointerEvent } from '../util/events.js'; import { setCapture, releaseCapture, stopEvent, getPointerEvent } from '../util/events.js';
import { set_defaults, make_properties } from '../util/properties.js'; import { set_defaults, make_properties } from '../util/properties.js';
import * as KeyboardUtil from "./util.js"; import * as KeyboardUtil from "./util.js";
import KeyTable from "./keysym.js";
// //
// Keyboard event handler // Keyboard event handler
@ -23,8 +24,6 @@ const Keyboard = function (defaults) {
// (even if they are happy) // (even if they are happy)
this._pendingKey = null; // Key waiting for keypress this._pendingKey = null; // Key waiting for keypress
this._modifierState = KeyboardUtil.ModifierSync();
set_defaults(this, defaults, { set_defaults(this, defaults, {
'target': document, 'target': document,
'focused': true 'focused': true
@ -39,6 +38,13 @@ const Keyboard = function (defaults) {
}; };
}; };
function isMac() {
return navigator && !!(/mac/i).exec(navigator.platform);
}
function isWindows() {
return navigator && !!(/win/i).exec(navigator.platform);
}
Keyboard.prototype = { Keyboard.prototype = {
// private methods // private methods
@ -50,7 +56,32 @@ Keyboard.prototype = {
Log.Debug("onKeyEvent " + (down ? "down" : "up") + Log.Debug("onKeyEvent " + (down ? "down" : "up") +
", keysym: " + keysym, ", code: " + code); ", 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 && 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); this._onKeyEvent(keysym, code, down);
if (fakeAltGraph) {
this._onKeyEvent(this._keyDownList['ControlLeft'],
'ControlLeft', true);
this._onKeyEvent(this._keyDownList['AltRight'],
'AltRight', true);
}
}, },
_getKeyCode: function (e) { _getKeyCode: function (e) {
@ -70,8 +101,6 @@ Keyboard.prototype = {
_handleKeyDown: function (e) { _handleKeyDown: function (e) {
if (!this._focused) { return; } if (!this._focused) { return; }
this._modifierState.keydown(e);
var code = this._getKeyCode(e); var code = this._getKeyCode(e);
var keysym = KeyboardUtil.getKeysym(e); var keysym = KeyboardUtil.getKeysym(e);
@ -90,6 +119,27 @@ Keyboard.prototype = {
return; return;
} }
// Alt behaves more like AltGraph on macOS, so shuffle the
// keys around a bit to make things more sane for the remote
// server. This method is used by RealVNC and TigerVNC (and
// possibly others).
if (isMac()) {
switch (keysym) {
case KeyTable.XK_Super_L:
keysym = KeyTable.XK_Alt_L;
break;
case KeyTable.XK_Super_R:
keysym = KeyTable.XK_Super_L;
break;
case KeyTable.XK_Alt_L:
keysym = KeyTable.XK_Mode_switch;
break;
case KeyTable.XK_Alt_R:
keysym = KeyTable.XK_ISO_Level3_Shift;
break;
}
}
// Is this key already pressed? If so, then we must use the // Is this key already pressed? If so, then we must use the
// same keysym or we'll confuse the server // same keysym or we'll confuse the server
if (code in this._keyDownList) { if (code in this._keyDownList) {
@ -106,45 +156,9 @@ Keyboard.prototype = {
this._pendingKey = null; this._pendingKey = null;
stopEvent(e); 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) {
var isCharModifier = false;
for (var i = 0; i < active.length; ++i) {
if (active[i] === keysym) {
isCharModifier = true;
}
}
if (!isCharModifier) {
var escape = this._modifierState.activeCharModifier();
}
}
this._keyDownList[code] = keysym; this._keyDownList[code] = 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); 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 // Legacy event for browsers without code/key
@ -169,27 +183,6 @@ Keyboard.prototype = {
code = this._pendingKey; code = this._pendingKey;
this._pendingKey = null; this._pendingKey = null;
// 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();
}
}
if (!keysym) { if (!keysym) {
console.log('keypress with no keysym:', e); console.log('keypress with no keysym:', e);
return; return;
@ -197,22 +190,7 @@ Keyboard.prototype = {
this._keyDownList[code] = keysym; this._keyDownList[code] = 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); 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) { _handleKeyUp: function (e) {
@ -220,8 +198,6 @@ Keyboard.prototype = {
stopEvent(e); stopEvent(e);
this._modifierState.keyup(e);
var code = this._getKeyCode(e); var code = this._getKeyCode(e);
// Do we really think this key is down? // Do we really think this key is down?

View File

@ -6,105 +6,6 @@ import fixedkeys from "./fixedkeys.js";
function isMac() { function isMac() {
return navigator && !!(/mac/i).exec(navigator.platform); return navigator && !!(/mac/i).exec(navigator.platform);
} }
function isWindows() {
return navigator && !!(/win/i).exec(navigator.platform);
}
function isLinux() {
return navigator && !!(/linux/i).exec(navigator.platform);
}
// Return true if the specified char modifier is currently down
export function hasCharModifier(charModifier, currentModifiers) {
if (charModifier.length === 0) { return false; }
for (var i = 0; i < charModifier.length; ++i) {
if (!currentModifiers[charModifier[i]]) {
return false;
}
}
return true;
}
// Helper object tracking modifier key state
// and generates fake key events to compensate if it gets out of sync
export function ModifierSync(charModifier) {
if (!charModifier) {
if (isMac()) {
// on Mac, Option (AKA Alt) is used as a char modifier
charModifier = [KeyTable.XK_Alt_L];
}
else if (isWindows()) {
// on Windows, Ctrl+Alt is used as a char modifier
charModifier = [KeyTable.XK_Alt_L, KeyTable.XK_Control_L];
}
else if (isLinux()) {
// on Linux, ISO Level 3 Shift (AltGr) is used as a char modifier
charModifier = [KeyTable.XK_ISO_Level3_Shift];
}
else {
charModifier = [];
}
}
var state = {};
state[KeyTable.XK_Control_L] = false;
state[KeyTable.XK_Alt_L] = false;
state[KeyTable.XK_ISO_Level3_Shift] = false;
state[KeyTable.XK_Shift_L] = false;
state[KeyTable.XK_Meta_L] = false;
function sync(evt, keysym) {
var result = [];
function syncKey(keysym) {
return {keysym: keysym, type: state[keysym] ? 'keydown' : 'keyup'};
}
if (evt.ctrlKey !== undefined &&
evt.ctrlKey !== state[KeyTable.XK_Control_L] && keysym !== KeyTable.XK_Control_L) {
state[KeyTable.XK_Control_L] = evt.ctrlKey;
result.push(syncKey(KeyTable.XK_Control_L));
}
if (evt.altKey !== undefined &&
evt.altKey !== state[KeyTable.XK_Alt_L] && keysym !== KeyTable.XK_Alt_L) {
state[KeyTable.XK_Alt_L] = evt.altKey;
result.push(syncKey(KeyTable.XK_Alt_L));
}
if (evt.altGraphKey !== undefined &&
evt.altGraphKey !== state[KeyTable.XK_ISO_Level3_Shift] && keysym !== KeyTable.XK_ISO_Level3_Shift) {
state[KeyTable.XK_ISO_Level3_Shift] = evt.altGraphKey;
result.push(syncKey(KeyTable.XK_ISO_Level3_Shift));
}
if (evt.shiftKey !== undefined &&
evt.shiftKey !== state[KeyTable.XK_Shift_L] && keysym !== KeyTable.XK_Shift_L) {
state[KeyTable.XK_Shift_L] = evt.shiftKey;
result.push(syncKey(KeyTable.XK_Shift_L));
}
if (evt.metaKey !== undefined &&
evt.metaKey !== state[KeyTable.XK_Meta_L] && keysym !== KeyTable.XK_Meta_L) {
state[KeyTable.XK_Meta_L] = evt.metaKey;
result.push(syncKey(KeyTable.XK_Meta_L));
}
return result;
}
function syncKeyEvent(evt, down) {
var keysym = getKeysym(evt);
// first, apply the event itself, if relevant
if (keysym !== null && state[keysym] !== undefined) {
state[keysym] = down;
}
return sync(evt, keysym);
}
return {
// sync on the appropriate keyboard event
keydown: function(evt) { return syncKeyEvent(evt, true);},
keyup: function(evt) { return syncKeyEvent(evt, false);},
// if a char modifier is down, return the keys it consists of, otherwise return null
activeCharModifier: function() { return hasCharModifier(charModifier, state) ? charModifier : null; }
};
}
// Get 'KeyboardEvent.code', handling legacy browsers // Get 'KeyboardEvent.code', handling legacy browsers
export function getKeycode(evt){ export function getKeycode(evt){

View File

@ -146,7 +146,72 @@ describe('Key Event Handling', function() {
}); });
}); });
describe('Escape Modifiers', function() { describe('Shuffle modifiers on macOS', 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 = "Mac x86_64";
});
afterEach(function () {
Object.defineProperty(window, "navigator", origNavigator);
});
it('should change Alt to AltGraph', function() {
var count = 0;
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
switch (count++) {
case 0:
expect(keysym).to.be.equal(0xFF7E);
expect(code).to.be.equal('AltLeft');
break;
case 1:
expect(keysym).to.be.equal(0xFE03);
expect(code).to.be.equal('AltRight');
break;
}
}});
kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt'}));
kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt'}));
expect(count).to.be.equal(2);
});
it('should change left Super to Alt', function(done) {
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
expect(keysym).to.be.equal(0xFFE9);
expect(code).to.be.equal('MetaLeft');
done();
}});
kbd._handleKeyDown(keyevent('keydown', {code: 'MetaLeft', key: 'Meta'}));
});
it('should change right Super to left Super', function(done) {
var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) {
expect(keysym).to.be.equal(0xFFEB);
expect(code).to.be.equal('MetaRight');
done();
}});
kbd._handleKeyDown(keyevent('keydown', {code: 'MetaRight', key: 'Meta'}));
});
});
describe('Escape AltGraph on Windows', function() {
var origNavigator; var origNavigator;
beforeEach(function () { beforeEach(function () {
// window.navigator is a protected read-only property in many // window.navigator is a protected read-only property in many
@ -172,7 +237,7 @@ describe('Key Event Handling', function() {
Object.defineProperty(window, "navigator", origNavigator); Object.defineProperty(window, "navigator", origNavigator);
}); });
it('should generate fake undo/redo events on press when a char modifier is down', function() { it('should generate fake undo/redo events on press when AltGraph is down', function() {
var times_called = 0; var times_called = 0;
var kbd = new Keyboard({ var kbd = new Keyboard({
onKeyEvent: function(keysym, code, down) { onKeyEvent: function(keysym, code, down) {
@ -183,18 +248,18 @@ describe('Key Event Handling', function() {
expect(down).to.be.equal(true); expect(down).to.be.equal(true);
break; break;
case 1: case 1:
expect(keysym).to.be.equal(0xFFE9); expect(keysym).to.be.equal(0xFFEA);
expect(code).to.be.equal('AltLeft'); expect(code).to.be.equal('AltRight');
expect(down).to.be.equal(true); expect(down).to.be.equal(true);
break; break;
case 2: case 2:
expect(keysym).to.be.equal(0xFFE9); expect(keysym).to.be.equal(0xFFEA);
expect(code).to.be.equal('Unidentified'); expect(code).to.be.equal('AltRight');
expect(down).to.be.equal(false); expect(down).to.be.equal(false);
break; break;
case 3: case 3:
expect(keysym).to.be.equal(0xFFE3); expect(keysym).to.be.equal(0xFFE3);
expect(code).to.be.equal('Unidentified'); expect(code).to.be.equal('ControlLeft');
expect(down).to.be.equal(false); expect(down).to.be.equal(false);
break; break;
case 4: case 4:
@ -203,20 +268,20 @@ describe('Key Event Handling', function() {
expect(down).to.be.equal(true); expect(down).to.be.equal(true);
break; break;
case 5: case 5:
expect(keysym).to.be.equal(0xFFE9); expect(keysym).to.be.equal(0xFFE3);
expect(code).to.be.equal('Unidentified'); expect(code).to.be.equal('ControlLeft');
expect(down).to.be.equal(true); expect(down).to.be.equal(true);
break; break;
case 6: case 6:
expect(keysym).to.be.equal(0xFFE3); expect(keysym).to.be.equal(0xFFEA);
expect(code).to.be.equal('Unidentified'); expect(code).to.be.equal('AltRight');
expect(down).to.be.equal(true); expect(down).to.be.equal(true);
break; break;
} }
}}); }});
// First the modifier combo // First the modifier combo
kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control'})); kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control'}));
kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt'})); kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt'}));
// Next a normal character // Next a normal character
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
expect(times_called).to.be.equal(7); expect(times_called).to.be.equal(7);
@ -235,7 +300,7 @@ describe('Key Event Handling', function() {
}}); }});
// First the modifier combo // First the modifier combo
kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control'})); kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control'}));
kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt'})); kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt'}));
// Next a normal character // Next a normal character
kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'}));
kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'})); kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'}));