]> git.proxmox.com Git - mirror_novnc.git/blob - core/input/keyboard.js
Update zh_CN.po
[mirror_novnc.git] / core / input / keyboard.js
1 /*
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2019 The noVNC Authors
4 * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
5 */
6
7 import * as Log from '../util/logging.js';
8 import { stopEvent } from '../util/events.js';
9 import * as KeyboardUtil from "./util.js";
10 import KeyTable from "./keysym.js";
11 import * as browser from "../util/browser.js";
12
13 //
14 // Keyboard event handler
15 //
16
17 export default class Keyboard {
18 constructor(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 'checkalt': this._checkAlt.bind(this),
33 };
34
35 // ===== EVENT HANDLERS =====
36
37 this.onkeyevent = () => {}; // Handler for key press/release
38 }
39
40 // ===== PRIVATE METHODS =====
41
42 _sendKeyEvent(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(e) {
59 const 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 const codepoint = parseInt(e.keyIdentifier.substr(2), 16);
83 const char = String.fromCharCode(codepoint).toUpperCase();
84
85 return 'Platform' + char.charCodeAt();
86 }
87
88 return 'Unidentified';
89 }
90
91 _handleKeyDown(e) {
92 const code = this._getKeyCode(e);
93 let keysym = KeyboardUtil.getKeysym(e);
94
95 // Windows doesn't have a proper AltGr, but handles it using
96 // fake Ctrl+Alt. However the remote end might not be Windows,
97 // so we need to merge those in to a single AltGr event. We
98 // detect this case by seeing the two key events directly after
99 // each other with a very short time between them (<50ms).
100 if (this._altGrArmed) {
101 this._altGrArmed = false;
102 clearTimeout(this._altGrTimeout);
103
104 if ((code === "AltRight") &&
105 ((e.timeStamp - this._altGrCtrlTime) < 50)) {
106 // FIXME: We fail to detect this if either Ctrl key is
107 // first manually pressed as Windows then no
108 // longer sends the fake Ctrl down event. It
109 // does however happily send real Ctrl events
110 // even when AltGr is already down. Some
111 // browsers detect this for us though and set the
112 // key to "AltGraph".
113 keysym = KeyTable.XK_ISO_Level3_Shift;
114 } else {
115 this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
116 }
117 }
118
119 // We cannot handle keys we cannot track, but we also need
120 // to deal with virtual keyboards which omit key info
121 if (code === 'Unidentified') {
122 if (keysym) {
123 // If it's a virtual keyboard then it should be
124 // sufficient to just send press and release right
125 // after each other
126 this._sendKeyEvent(keysym, code, true);
127 this._sendKeyEvent(keysym, code, false);
128 }
129
130 stopEvent(e);
131 return;
132 }
133
134 // Alt behaves more like AltGraph on macOS, so shuffle the
135 // keys around a bit to make things more sane for the remote
136 // server. This method is used by RealVNC and TigerVNC (and
137 // possibly others).
138 if (browser.isMac() || browser.isIOS()) {
139 switch (keysym) {
140 case KeyTable.XK_Super_L:
141 keysym = KeyTable.XK_Alt_L;
142 break;
143 case KeyTable.XK_Super_R:
144 keysym = KeyTable.XK_Super_L;
145 break;
146 case KeyTable.XK_Alt_L:
147 keysym = KeyTable.XK_Mode_switch;
148 break;
149 case KeyTable.XK_Alt_R:
150 keysym = KeyTable.XK_ISO_Level3_Shift;
151 break;
152 }
153 }
154
155 // Is this key already pressed? If so, then we must use the
156 // same keysym or we'll confuse the server
157 if (code in this._keyDownList) {
158 keysym = this._keyDownList[code];
159 }
160
161 // macOS doesn't send proper key events for modifiers, only
162 // state change events. That gets extra confusing for CapsLock
163 // which toggles on each press, but not on release. So pretend
164 // it was a quick press and release of the button.
165 if (browser.isMac() && (code === 'CapsLock')) {
166 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
167 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
168 stopEvent(e);
169 return;
170 }
171
172 // If this is a legacy browser then we'll need to wait for
173 // a keypress event as well
174 // (IE and Edge has a broken KeyboardEvent.key, so we can't
175 // just check for the presence of that field)
176 if (!keysym && (!e.key || browser.isIE() || browser.isEdge())) {
177 this._pendingKey = code;
178 // However we might not get a keypress event if the key
179 // is non-printable, which needs some special fallback
180 // handling
181 setTimeout(this._handleKeyPressTimeout.bind(this), 10, e);
182 return;
183 }
184
185 this._pendingKey = null;
186 stopEvent(e);
187
188 // Possible start of AltGr sequence? (see above)
189 if ((code === "ControlLeft") && browser.isWindows() &&
190 !("ControlLeft" in this._keyDownList)) {
191 this._altGrArmed = true;
192 this._altGrTimeout = setTimeout(this._handleAltGrTimeout.bind(this), 100);
193 this._altGrCtrlTime = e.timeStamp;
194 return;
195 }
196
197 this._sendKeyEvent(keysym, code, true);
198 }
199
200 // Legacy event for browsers without code/key
201 _handleKeyPress(e) {
202 stopEvent(e);
203
204 // Are we expecting a keypress?
205 if (this._pendingKey === null) {
206 return;
207 }
208
209 let code = this._getKeyCode(e);
210 const keysym = KeyboardUtil.getKeysym(e);
211
212 // The key we were waiting for?
213 if ((code !== 'Unidentified') && (code != this._pendingKey)) {
214 return;
215 }
216
217 code = this._pendingKey;
218 this._pendingKey = null;
219
220 if (!keysym) {
221 Log.Info('keypress with no keysym:', e);
222 return;
223 }
224
225 this._sendKeyEvent(keysym, code, true);
226 }
227
228 _handleKeyPressTimeout(e) {
229 // Did someone manage to sort out the key already?
230 if (this._pendingKey === null) {
231 return;
232 }
233
234 let keysym;
235
236 const code = this._pendingKey;
237 this._pendingKey = null;
238
239 // We have no way of knowing the proper keysym with the
240 // information given, but the following are true for most
241 // layouts
242 if ((e.keyCode >= 0x30) && (e.keyCode <= 0x39)) {
243 // Digit
244 keysym = e.keyCode;
245 } else if ((e.keyCode >= 0x41) && (e.keyCode <= 0x5a)) {
246 // Character (A-Z)
247 let char = String.fromCharCode(e.keyCode);
248 // A feeble attempt at the correct case
249 if (e.shiftKey) {
250 char = char.toUpperCase();
251 } else {
252 char = char.toLowerCase();
253 }
254 keysym = char.charCodeAt();
255 } else {
256 // Unknown, give up
257 keysym = 0;
258 }
259
260 this._sendKeyEvent(keysym, code, true);
261 }
262
263 _handleKeyUp(e) {
264 stopEvent(e);
265
266 const code = this._getKeyCode(e);
267
268 // We can't get a release in the middle of an AltGr sequence, so
269 // abort that detection
270 if (this._altGrArmed) {
271 this._altGrArmed = false;
272 clearTimeout(this._altGrTimeout);
273 this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
274 }
275
276 // See comment in _handleKeyDown()
277 if (browser.isMac() && (code === 'CapsLock')) {
278 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
279 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
280 return;
281 }
282
283 this._sendKeyEvent(this._keyDownList[code], code, false);
284
285 // Windows has a rather nasty bug where it won't send key
286 // release events for a Shift button if the other Shift is still
287 // pressed
288 if (browser.isWindows() && ((code === 'ShiftLeft') ||
289 (code === 'ShiftRight'))) {
290 if ('ShiftRight' in this._keyDownList) {
291 this._sendKeyEvent(this._keyDownList['ShiftRight'],
292 'ShiftRight', false);
293 }
294 if ('ShiftLeft' in this._keyDownList) {
295 this._sendKeyEvent(this._keyDownList['ShiftLeft'],
296 'ShiftLeft', false);
297 }
298 }
299 }
300
301 _handleAltGrTimeout() {
302 this._altGrArmed = false;
303 clearTimeout(this._altGrTimeout);
304 this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
305 }
306
307 _allKeysUp() {
308 Log.Debug(">> Keyboard.allKeysUp");
309 for (let code in this._keyDownList) {
310 this._sendKeyEvent(this._keyDownList[code], code, false);
311 }
312 Log.Debug("<< Keyboard.allKeysUp");
313 }
314
315 // Alt workaround for Firefox on Windows, see below
316 _checkAlt(e) {
317 if (e.skipCheckAlt) {
318 return;
319 }
320 if (e.altKey) {
321 return;
322 }
323
324 const target = this._target;
325 const downList = this._keyDownList;
326 ['AltLeft', 'AltRight'].forEach((code) => {
327 if (!(code in downList)) {
328 return;
329 }
330
331 const event = new KeyboardEvent('keyup',
332 { key: downList[code],
333 code: code });
334 event.skipCheckAlt = true;
335 target.dispatchEvent(event);
336 });
337 }
338
339 // ===== PUBLIC METHODS =====
340
341 grab() {
342 //Log.Debug(">> Keyboard.grab");
343
344 this._target.addEventListener('keydown', this._eventHandlers.keydown);
345 this._target.addEventListener('keyup', this._eventHandlers.keyup);
346 this._target.addEventListener('keypress', this._eventHandlers.keypress);
347
348 // Release (key up) if window loses focus
349 window.addEventListener('blur', this._eventHandlers.blur);
350
351 // Firefox on Windows has broken handling of Alt, so we need to
352 // poll as best we can for releases (still doesn't prevent the
353 // menu from popping up though as we can't call
354 // preventDefault())
355 if (browser.isWindows() && browser.isFirefox()) {
356 const handler = this._eventHandlers.checkalt;
357 ['mousedown', 'mouseup', 'mousemove', 'wheel',
358 'touchstart', 'touchend', 'touchmove',
359 'keydown', 'keyup'].forEach(type =>
360 document.addEventListener(type, handler,
361 { capture: true,
362 passive: true }));
363 }
364
365 //Log.Debug("<< Keyboard.grab");
366 }
367
368 ungrab() {
369 //Log.Debug(">> Keyboard.ungrab");
370
371 if (browser.isWindows() && browser.isFirefox()) {
372 const handler = this._eventHandlers.checkalt;
373 ['mousedown', 'mouseup', 'mousemove', 'wheel',
374 'touchstart', 'touchend', 'touchmove',
375 'keydown', 'keyup'].forEach(type => document.removeEventListener(type, handler));
376 }
377
378 this._target.removeEventListener('keydown', this._eventHandlers.keydown);
379 this._target.removeEventListener('keyup', this._eventHandlers.keyup);
380 this._target.removeEventListener('keypress', this._eventHandlers.keypress);
381 window.removeEventListener('blur', this._eventHandlers.blur);
382
383 // Release (key up) all keys that are in a down state
384 this._allKeysUp();
385
386 //Log.Debug(">> Keyboard.ungrab");
387 }
388 }