]> git.proxmox.com Git - mirror_novnc.git/blob - core/input/keyboard.js
Better detection of AltGr on Windows
[mirror_novnc.git] / core / input / keyboard.js
1 /*
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2012 Joel Martin
4 * Copyright (C) 2013 Samuel Mannehed for Cendio AB
5 * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
6 */
7
8 import * as Log from '../util/logging.js';
9 import { stopEvent } from '../util/events.js';
10 import * as KeyboardUtil from "./util.js";
11 import KeyTable from "./keysym.js";
12 import * as browser from "../util/browser.js";
13
14 //
15 // Keyboard event handler
16 //
17
18 export default function Keyboard(target) {
19 this._target = target || null;
20
21 this._keyDownList = {}; // List of depressed keys
22 // (even if they are happy)
23 this._pendingKey = null; // Key waiting for keypress
24 this._altGrArmed = false; // Windows AltGr detection
25
26 // keep these here so we can refer to them later
27 this._eventHandlers = {
28 'keyup': this._handleKeyUp.bind(this),
29 'keydown': this._handleKeyDown.bind(this),
30 'keypress': this._handleKeyPress.bind(this),
31 'blur': this._allKeysUp.bind(this)
32 };
33 };
34
35 Keyboard.prototype = {
36 // ===== EVENT HANDLERS =====
37
38 onkeyevent: function () {}, // Handler for key press/release
39
40 // ===== PRIVATE METHODS =====
41
42 _sendKeyEvent: function (keysym, code, down) {
43 if (down) {
44 this._keyDownList[code] = keysym;
45 } else {
46 // Do we really think this key is down?
47 if (!(code in this._keyDownList)) {
48 return;
49 }
50 delete this._keyDownList[code];
51 }
52
53 Log.Debug("onkeyevent " + (down ? "down" : "up") +
54 ", keysym: " + keysym, ", code: " + code);
55 this.onkeyevent(keysym, code, down);
56 },
57
58 _getKeyCode: function (e) {
59 var code = KeyboardUtil.getKeycode(e);
60 if (code !== 'Unidentified') {
61 return code;
62 }
63
64 // Unstable, but we don't have anything else to go on
65 // (don't use it for 'keypress' events thought since
66 // WebKit sets it to the same as charCode)
67 if (e.keyCode && (e.type !== 'keypress')) {
68 // 229 is used for composition events
69 if (e.keyCode !== 229) {
70 return 'Platform' + e.keyCode;
71 }
72 }
73
74 // A precursor to the final DOM3 standard. Unfortunately it
75 // is not layout independent, so it is as bad as using keyCode
76 if (e.keyIdentifier) {
77 // Non-character key?
78 if (e.keyIdentifier.substr(0, 2) !== 'U+') {
79 return e.keyIdentifier;
80 }
81
82 var codepoint = parseInt(e.keyIdentifier.substr(2), 16);
83 var char = String.fromCharCode(codepoint);
84 // Some implementations fail to uppercase the symbols
85 char = char.toUpperCase();
86
87 return 'Platform' + char.charCodeAt();
88 }
89
90 return 'Unidentified';
91 },
92
93 _handleKeyDown: function (e) {
94 var code = this._getKeyCode(e);
95 var keysym = KeyboardUtil.getKeysym(e);
96
97 // Windows doesn't have a proper AltGr, but handles it using
98 // fake Ctrl+Alt. However the remote end might not be Windows,
99 // so we need to merge those in to a single AltGr event. We
100 // detect this case by seeing the two key events directly after
101 // each other with a very short time between them (<50ms).
102 if (this._altGrArmed) {
103 this._altGrArmed = false;
104 clearTimeout(this._altGrTimeout);
105
106 if ((code === "AltRight") &&
107 ((e.timeStamp - this._altGrCtrlTime) < 50)) {
108 // FIXME: We fail to detect this if either Ctrl key is
109 // first manually pressed as Windows then no
110 // longer sends the fake Ctrl down event. It
111 // does however happily send real Ctrl events
112 // even when AltGr is already down. Some
113 // browsers detect this for us though and set the
114 // key to "AltGraph".
115 keysym = KeyTable.XK_ISO_Level3_Shift;
116 } else {
117 this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
118 }
119 }
120
121 // We cannot handle keys we cannot track, but we also need
122 // to deal with virtual keyboards which omit key info
123 // (iOS omits tracking info on keyup events, which forces us to
124 // special treat that platform here)
125 if ((code === 'Unidentified') || browser.isIOS()) {
126 if (keysym) {
127 // If it's a virtual keyboard then it should be
128 // sufficient to just send press and release right
129 // after each other
130 this._sendKeyEvent(keysym, code, true);
131 this._sendKeyEvent(keysym, code, false);
132 }
133
134 stopEvent(e);
135 return;
136 }
137
138 // Alt behaves more like AltGraph on macOS, so shuffle the
139 // keys around a bit to make things more sane for the remote
140 // server. This method is used by RealVNC and TigerVNC (and
141 // possibly others).
142 if (browser.isMac()) {
143 switch (keysym) {
144 case KeyTable.XK_Super_L:
145 keysym = KeyTable.XK_Alt_L;
146 break;
147 case KeyTable.XK_Super_R:
148 keysym = KeyTable.XK_Super_L;
149 break;
150 case KeyTable.XK_Alt_L:
151 keysym = KeyTable.XK_Mode_switch;
152 break;
153 case KeyTable.XK_Alt_R:
154 keysym = KeyTable.XK_ISO_Level3_Shift;
155 break;
156 }
157 }
158
159 // Is this key already pressed? If so, then we must use the
160 // same keysym or we'll confuse the server
161 if (code in this._keyDownList) {
162 keysym = this._keyDownList[code];
163 }
164
165 // macOS doesn't send proper key events for modifiers, only
166 // state change events. That gets extra confusing for CapsLock
167 // which toggles on each press, but not on release. So pretend
168 // it was a quick press and release of the button.
169 if (browser.isMac() && (code === 'CapsLock')) {
170 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
171 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
172 stopEvent(e);
173 return;
174 }
175
176 // If this is a legacy browser then we'll need to wait for
177 // a keypress event as well
178 // (IE and Edge has a broken KeyboardEvent.key, so we can't
179 // just check for the presence of that field)
180 if (!keysym && (!e.key || browser.isIE() || browser.isEdge())) {
181 this._pendingKey = code;
182 // However we might not get a keypress event if the key
183 // is non-printable, which needs some special fallback
184 // handling
185 setTimeout(this._handleKeyPressTimeout.bind(this), 10, e);
186 return;
187 }
188
189 this._pendingKey = null;
190 stopEvent(e);
191
192 // Possible start of AltGr sequence? (see above)
193 if ((code === "ControlLeft") && browser.isWindows() &&
194 !("ControlLeft" in this._keyDownList)) {
195 this._altGrArmed = true;
196 this._altGrTimeout = setTimeout(this._handleAltGrTimeout.bind(this), 100);
197 this._altGrCtrlTime = e.timeStamp;
198 return;
199 }
200
201 this._sendKeyEvent(keysym, code, true);
202 },
203
204 // Legacy event for browsers without code/key
205 _handleKeyPress: function (e) {
206 stopEvent(e);
207
208 // Are we expecting a keypress?
209 if (this._pendingKey === null) {
210 return;
211 }
212
213 var code = this._getKeyCode(e);
214 var keysym = KeyboardUtil.getKeysym(e);
215
216 // The key we were waiting for?
217 if ((code !== 'Unidentified') && (code != this._pendingKey)) {
218 return;
219 }
220
221 code = this._pendingKey;
222 this._pendingKey = null;
223
224 if (!keysym) {
225 Log.Info('keypress with no keysym:', e);
226 return;
227 }
228
229 this._sendKeyEvent(keysym, code, true);
230 },
231 _handleKeyPressTimeout: function (e) {
232 // Did someone manage to sort out the key already?
233 if (this._pendingKey === null) {
234 return;
235 }
236
237 var code, keysym;
238
239 code = this._pendingKey;
240 this._pendingKey = null;
241
242 // We have no way of knowing the proper keysym with the
243 // information given, but the following are true for most
244 // layouts
245 if ((e.keyCode >= 0x30) && (e.keyCode <= 0x39)) {
246 // Digit
247 keysym = e.keyCode;
248 } else if ((e.keyCode >= 0x41) && (e.keyCode <= 0x5a)) {
249 // Character (A-Z)
250 var char = String.fromCharCode(e.keyCode);
251 // A feeble attempt at the correct case
252 if (e.shiftKey)
253 char = char.toUpperCase();
254 else
255 char = char.toLowerCase();
256 keysym = char.charCodeAt();
257 } else {
258 // Unknown, give up
259 keysym = 0;
260 }
261
262 this._sendKeyEvent(keysym, code, true);
263 },
264
265 _handleKeyUp: function (e) {
266 stopEvent(e);
267
268 var code = this._getKeyCode(e);
269
270 // We can't get a release in the middle of an AltGr sequence, so
271 // abort that detection
272 if (this._altGrArmed) {
273 this._altGrArmed = false;
274 clearTimeout(this._altGrTimeout);
275 this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
276 }
277
278 // See comment in _handleKeyDown()
279 if (browser.isMac() && (code === 'CapsLock')) {
280 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
281 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
282 return;
283 }
284
285 this._sendKeyEvent(this._keyDownList[code], code, false);
286 },
287
288 _handleAltGrTimeout: function () {
289 this._altGrArmed = false;
290 clearTimeout(this._altGrTimeout);
291 this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
292 },
293
294 _allKeysUp: function () {
295 Log.Debug(">> Keyboard.allKeysUp");
296 for (var code in this._keyDownList) {
297 this._sendKeyEvent(this._keyDownList[code], code, false);
298 };
299 Log.Debug("<< Keyboard.allKeysUp");
300 },
301
302 // ===== PUBLIC METHODS =====
303
304 grab: function () {
305 //Log.Debug(">> Keyboard.grab");
306 var c = this._target;
307
308 c.addEventListener('keydown', this._eventHandlers.keydown);
309 c.addEventListener('keyup', this._eventHandlers.keyup);
310 c.addEventListener('keypress', this._eventHandlers.keypress);
311
312 // Release (key up) if window loses focus
313 window.addEventListener('blur', this._eventHandlers.blur);
314
315 //Log.Debug("<< Keyboard.grab");
316 },
317
318 ungrab: function () {
319 //Log.Debug(">> Keyboard.ungrab");
320 var c = this._target;
321
322 c.removeEventListener('keydown', this._eventHandlers.keydown);
323 c.removeEventListener('keyup', this._eventHandlers.keyup);
324 c.removeEventListener('keypress', this._eventHandlers.keypress);
325 window.removeEventListener('blur', this._eventHandlers.blur);
326
327 // Release (key up) all keys that are in a down state
328 this._allKeysUp();
329
330 //Log.Debug(">> Keyboard.ungrab");
331 },
332 };