]>
Commit | Line | Data |
---|---|---|
d3796c14 JM |
1 | /* |
2 | * noVNC: HTML5 VNC client | |
1d728ace | 3 | * Copyright (C) 2012 Joel Martin |
b2f1961a | 4 | * Copyright (C) 2013 Samuel Mannehed for Cendio AB |
1d728ace | 5 | * Licensed under MPL 2.0 or any later version (see LICENSE.txt) |
d3796c14 JM |
6 | */ |
7 | ||
6d6f0db0 | 8 | import * as Log from '../util/logging.js'; |
c1e2785f | 9 | import { stopEvent } from '../util/events.js'; |
6d6f0db0 | 10 | import * as KeyboardUtil from "./util.js"; |
bf43c263 | 11 | import KeyTable from "./keysym.js"; |
59ef2916 | 12 | import * as browser from "../util/browser.js"; |
6d6f0db0 SR |
13 | |
14 | // | |
15 | // Keyboard event handler | |
16 | // | |
17 | ||
0e4808bf JD |
18 | export default class Keyboard { |
19 | constructor(target) { | |
20 | this._target = target || null; | |
21 | ||
22 | this._keyDownList = {}; // List of depressed keys | |
23 | // (even if they are happy) | |
24 | this._pendingKey = null; // Key waiting for keypress | |
25 | this._altGrArmed = false; // Windows AltGr detection | |
26 | ||
27 | // keep these here so we can refer to them later | |
28 | this._eventHandlers = { | |
29 | 'keyup': this._handleKeyUp.bind(this), | |
30 | 'keydown': this._handleKeyDown.bind(this), | |
31 | 'keypress': this._handleKeyPress.bind(this), | |
32 | 'blur': this._allKeysUp.bind(this), | |
33 | 'checkalt': this._checkAlt.bind(this), | |
34 | }; | |
d6e281ba | 35 | |
0e4808bf | 36 | // ===== EVENT HANDLERS ===== |
d6e281ba | 37 | |
0e4808bf JD |
38 | this.onkeyevent = () => {}; // Handler for key press/release |
39 | } | |
f7363fd2 | 40 | |
747b4623 PO |
41 | // ===== PRIVATE METHODS ===== |
42 | ||
0e4808bf | 43 | _sendKeyEvent(keysym, code, down) { |
d6ae4457 PO |
44 | if (down) { |
45 | this._keyDownList[code] = keysym; | |
46 | } else { | |
47 | // Do we really think this key is down? | |
48 | if (!(code in this._keyDownList)) { | |
49 | return; | |
bf43c263 | 50 | } |
d6ae4457 | 51 | delete this._keyDownList[code]; |
bf43c263 PO |
52 | } |
53 | ||
747b4623 | 54 | Log.Debug("onkeyevent " + (down ? "down" : "up") + |
f7363fd2 | 55 | ", keysym: " + keysym, ", code: " + code); |
747b4623 | 56 | this.onkeyevent(keysym, code, down); |
0e4808bf | 57 | } |
f7363fd2 | 58 | |
0e4808bf | 59 | _getKeyCode(e) { |
2b5f94fa | 60 | const code = KeyboardUtil.getKeycode(e); |
7e79dfe4 PO |
61 | if (code !== 'Unidentified') { |
62 | return code; | |
63 | } | |
64 | ||
65 | // Unstable, but we don't have anything else to go on | |
66 | // (don't use it for 'keypress' events thought since | |
67 | // WebKit sets it to the same as charCode) | |
68 | if (e.keyCode && (e.type !== 'keypress')) { | |
4093c37f PO |
69 | // 229 is used for composition events |
70 | if (e.keyCode !== 229) { | |
71 | return 'Platform' + e.keyCode; | |
72 | } | |
7e79dfe4 PO |
73 | } |
74 | ||
75 | // A precursor to the final DOM3 standard. Unfortunately it | |
76 | // is not layout independent, so it is as bad as using keyCode | |
77 | if (e.keyIdentifier) { | |
78 | // Non-character key? | |
79 | if (e.keyIdentifier.substr(0, 2) !== 'U+') { | |
80 | return e.keyIdentifier; | |
f7363fd2 | 81 | } |
7e79dfe4 | 82 | |
2b5f94fa JD |
83 | const codepoint = parseInt(e.keyIdentifier.substr(2), 16); |
84 | const char = String.fromCharCode(codepoint).toUpperCase(); | |
7e79dfe4 PO |
85 | |
86 | return 'Platform' + char.charCodeAt(); | |
f7363fd2 PO |
87 | } |
88 | ||
7e79dfe4 | 89 | return 'Unidentified'; |
0e4808bf | 90 | } |
d6e281ba | 91 | |
0e4808bf | 92 | _handleKeyDown(e) { |
2b5f94fa JD |
93 | const code = this._getKeyCode(e); |
94 | let keysym = KeyboardUtil.getKeysym(e); | |
f7363fd2 | 95 | |
b22c9ef9 PO |
96 | // Windows doesn't have a proper AltGr, but handles it using |
97 | // fake Ctrl+Alt. However the remote end might not be Windows, | |
98 | // so we need to merge those in to a single AltGr event. We | |
99 | // detect this case by seeing the two key events directly after | |
100 | // each other with a very short time between them (<50ms). | |
101 | if (this._altGrArmed) { | |
102 | this._altGrArmed = false; | |
103 | clearTimeout(this._altGrTimeout); | |
104 | ||
105 | if ((code === "AltRight") && | |
106 | ((e.timeStamp - this._altGrCtrlTime) < 50)) { | |
107 | // FIXME: We fail to detect this if either Ctrl key is | |
108 | // first manually pressed as Windows then no | |
109 | // longer sends the fake Ctrl down event. It | |
110 | // does however happily send real Ctrl events | |
111 | // even when AltGr is already down. Some | |
112 | // browsers detect this for us though and set the | |
113 | // key to "AltGraph". | |
114 | keysym = KeyTable.XK_ISO_Level3_Shift; | |
115 | } else { | |
116 | this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); | |
117 | } | |
118 | } | |
119 | ||
ae820533 PO |
120 | // We cannot handle keys we cannot track, but we also need |
121 | // to deal with virtual keyboards which omit key info | |
9e99ce12 PO |
122 | // (iOS omits tracking info on keyup events, which forces us to |
123 | // special treat that platform here) | |
59ef2916 | 124 | if ((code === 'Unidentified') || browser.isIOS()) { |
ae820533 PO |
125 | if (keysym) { |
126 | // If it's a virtual keyboard then it should be | |
127 | // sufficient to just send press and release right | |
128 | // after each other | |
9e99ce12 PO |
129 | this._sendKeyEvent(keysym, code, true); |
130 | this._sendKeyEvent(keysym, code, false); | |
ae820533 PO |
131 | } |
132 | ||
133 | stopEvent(e); | |
134 | return; | |
135 | } | |
136 | ||
bf43c263 PO |
137 | // Alt behaves more like AltGraph on macOS, so shuffle the |
138 | // keys around a bit to make things more sane for the remote | |
139 | // server. This method is used by RealVNC and TigerVNC (and | |
140 | // possibly others). | |
59ef2916 | 141 | if (browser.isMac()) { |
bf43c263 PO |
142 | switch (keysym) { |
143 | case KeyTable.XK_Super_L: | |
144 | keysym = KeyTable.XK_Alt_L; | |
145 | break; | |
146 | case KeyTable.XK_Super_R: | |
147 | keysym = KeyTable.XK_Super_L; | |
148 | break; | |
149 | case KeyTable.XK_Alt_L: | |
150 | keysym = KeyTable.XK_Mode_switch; | |
151 | break; | |
152 | case KeyTable.XK_Alt_R: | |
153 | keysym = KeyTable.XK_ISO_Level3_Shift; | |
154 | break; | |
155 | } | |
156 | } | |
157 | ||
ae820533 PO |
158 | // Is this key already pressed? If so, then we must use the |
159 | // same keysym or we'll confuse the server | |
160 | if (code in this._keyDownList) { | |
161 | keysym = this._keyDownList[code]; | |
162 | } | |
163 | ||
634cc1ba PO |
164 | // macOS doesn't send proper key events for modifiers, only |
165 | // state change events. That gets extra confusing for CapsLock | |
166 | // which toggles on each press, but not on release. So pretend | |
167 | // it was a quick press and release of the button. | |
59ef2916 | 168 | if (browser.isMac() && (code === 'CapsLock')) { |
634cc1ba PO |
169 | this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); |
170 | this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); | |
171 | stopEvent(e); | |
172 | return; | |
173 | } | |
174 | ||
f7363fd2 | 175 | // If this is a legacy browser then we'll need to wait for |
9fce233d | 176 | // a keypress event as well |
844e9839 PO |
177 | // (IE and Edge has a broken KeyboardEvent.key, so we can't |
178 | // just check for the presence of that field) | |
59ef2916 | 179 | if (!keysym && (!e.key || browser.isIE() || browser.isEdge())) { |
9fce233d | 180 | this._pendingKey = code; |
7cac5c8e PO |
181 | // However we might not get a keypress event if the key |
182 | // is non-printable, which needs some special fallback | |
183 | // handling | |
184 | setTimeout(this._handleKeyPressTimeout.bind(this), 10, e); | |
9fce233d | 185 | return; |
f7363fd2 PO |
186 | } |
187 | ||
9fce233d PO |
188 | this._pendingKey = null; |
189 | stopEvent(e); | |
190 | ||
b22c9ef9 PO |
191 | // Possible start of AltGr sequence? (see above) |
192 | if ((code === "ControlLeft") && browser.isWindows() && | |
193 | !("ControlLeft" in this._keyDownList)) { | |
194 | this._altGrArmed = true; | |
195 | this._altGrTimeout = setTimeout(this._handleAltGrTimeout.bind(this), 100); | |
196 | this._altGrCtrlTime = e.timeStamp; | |
197 | return; | |
198 | } | |
f7363fd2 | 199 | |
f7363fd2 | 200 | this._sendKeyEvent(keysym, code, true); |
0e4808bf | 201 | } |
d6e281ba | 202 | |
f7363fd2 | 203 | // Legacy event for browsers without code/key |
0e4808bf | 204 | _handleKeyPress(e) { |
f7363fd2 PO |
205 | stopEvent(e); |
206 | ||
9fce233d PO |
207 | // Are we expecting a keypress? |
208 | if (this._pendingKey === null) { | |
209 | return; | |
210 | } | |
211 | ||
2b5f94fa JD |
212 | let code = this._getKeyCode(e); |
213 | const keysym = KeyboardUtil.getKeysym(e); | |
f7363fd2 | 214 | |
9fce233d PO |
215 | // The key we were waiting for? |
216 | if ((code !== 'Unidentified') && (code != this._pendingKey)) { | |
217 | return; | |
218 | } | |
219 | ||
220 | code = this._pendingKey; | |
221 | this._pendingKey = null; | |
222 | ||
f7363fd2 | 223 | if (!keysym) { |
a0035359 | 224 | Log.Info('keypress with no keysym:', e); |
f7363fd2 PO |
225 | return; |
226 | } | |
227 | ||
f7363fd2 | 228 | this._sendKeyEvent(keysym, code, true); |
0e4808bf JD |
229 | } |
230 | ||
231 | _handleKeyPressTimeout(e) { | |
7cac5c8e PO |
232 | // Did someone manage to sort out the key already? |
233 | if (this._pendingKey === null) { | |
234 | return; | |
235 | } | |
236 | ||
2b5f94fa | 237 | let keysym; |
7cac5c8e | 238 | |
2b5f94fa | 239 | const code = this._pendingKey; |
7cac5c8e PO |
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) | |
2b5f94fa | 250 | let char = String.fromCharCode(e.keyCode); |
7cac5c8e PO |
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 | ||
7cac5c8e | 262 | this._sendKeyEvent(keysym, code, true); |
0e4808bf | 263 | } |
d3796c14 | 264 | |
0e4808bf | 265 | _handleKeyUp(e) { |
f7363fd2 PO |
266 | stopEvent(e); |
267 | ||
2b5f94fa | 268 | const code = this._getKeyCode(e); |
f7363fd2 | 269 | |
b22c9ef9 PO |
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 | ||
634cc1ba | 278 | // See comment in _handleKeyDown() |
59ef2916 | 279 | if (browser.isMac() && (code === 'CapsLock')) { |
634cc1ba PO |
280 | this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); |
281 | this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); | |
282 | return; | |
283 | } | |
284 | ||
ae820533 | 285 | this._sendKeyEvent(this._keyDownList[code], code, false); |
0e4808bf | 286 | } |
ae820533 | 287 | |
0e4808bf | 288 | _handleAltGrTimeout() { |
b22c9ef9 PO |
289 | this._altGrArmed = false; |
290 | clearTimeout(this._altGrTimeout); | |
291 | this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); | |
0e4808bf | 292 | } |
d3796c14 | 293 | |
0e4808bf | 294 | _allKeysUp() { |
6d6f0db0 | 295 | Log.Debug(">> Keyboard.allKeysUp"); |
2b5f94fa | 296 | for (let code in this._keyDownList) { |
ae820533 | 297 | this._sendKeyEvent(this._keyDownList[code], code, false); |
8727f598 | 298 | } |
6d6f0db0 | 299 | Log.Debug("<< Keyboard.allKeysUp"); |
0e4808bf | 300 | } |
d6e281ba | 301 | |
3a7c0c67 | 302 | // Firefox Alt workaround, see below |
0e4808bf | 303 | _checkAlt(e) { |
3a7c0c67 PO |
304 | if (e.altKey) { |
305 | return; | |
306 | } | |
307 | ||
2b5f94fa JD |
308 | const target = this._target; |
309 | const downList = this._keyDownList; | |
651c23ec | 310 | ['AltLeft', 'AltRight'].forEach((code) => { |
3a7c0c67 PO |
311 | if (!(code in downList)) { |
312 | return; | |
313 | } | |
314 | ||
2b5f94fa | 315 | const event = new KeyboardEvent('keyup', |
3a7c0c67 PO |
316 | { key: downList[code], |
317 | code: code }); | |
318 | target.dispatchEvent(event); | |
319 | }); | |
0e4808bf | 320 | } |
3a7c0c67 | 321 | |
747b4623 | 322 | // ===== PUBLIC METHODS ===== |
d6e281ba | 323 | |
0e4808bf | 324 | grab() { |
6d6f0db0 | 325 | //Log.Debug(">> Keyboard.grab"); |
d6e281ba | 326 | |
2b5f94fa JD |
327 | this._target.addEventListener('keydown', this._eventHandlers.keydown); |
328 | this._target.addEventListener('keyup', this._eventHandlers.keyup); | |
329 | this._target.addEventListener('keypress', this._eventHandlers.keypress); | |
d6e281ba | 330 | |
6d6f0db0 SR |
331 | // Release (key up) if window loses focus |
332 | window.addEventListener('blur', this._eventHandlers.blur); | |
d6e281ba | 333 | |
3a7c0c67 PO |
334 | // Firefox has broken handling of Alt, so we need to poll as |
335 | // best we can for releases (still doesn't prevent the menu | |
336 | // from popping up though as we can't call preventDefault()) | |
337 | if (browser.isWindows() && browser.isFirefox()) { | |
2b5f94fa | 338 | const handler = this._eventHandlers.checkalt; |
3a7c0c67 PO |
339 | ['mousedown', 'mouseup', 'mousemove', 'wheel', |
340 | 'touchstart', 'touchend', 'touchmove', | |
651c23ec | 341 | 'keydown', 'keyup'].forEach(type => |
3a7c0c67 PO |
342 | document.addEventListener(type, handler, |
343 | { capture: true, | |
651c23ec | 344 | passive: true })); |
3a7c0c67 PO |
345 | } |
346 | ||
6d6f0db0 | 347 | //Log.Debug("<< Keyboard.grab"); |
0e4808bf | 348 | } |
d6e281ba | 349 | |
0e4808bf | 350 | ungrab() { |
6d6f0db0 | 351 | //Log.Debug(">> Keyboard.ungrab"); |
d6e281ba | 352 | |
3a7c0c67 | 353 | if (browser.isWindows() && browser.isFirefox()) { |
2b5f94fa | 354 | const handler = this._eventHandlers.checkalt; |
3a7c0c67 PO |
355 | ['mousedown', 'mouseup', 'mousemove', 'wheel', |
356 | 'touchstart', 'touchend', 'touchmove', | |
651c23ec | 357 | 'keydown', 'keyup'].forEach(type => document.removeEventListener(type, handler)); |
3a7c0c67 PO |
358 | } |
359 | ||
2b5f94fa JD |
360 | this._target.removeEventListener('keydown', this._eventHandlers.keydown); |
361 | this._target.removeEventListener('keyup', this._eventHandlers.keyup); | |
362 | this._target.removeEventListener('keypress', this._eventHandlers.keypress); | |
6d6f0db0 | 363 | window.removeEventListener('blur', this._eventHandlers.blur); |
d6e281ba | 364 | |
6d6f0db0 SR |
365 | // Release (key up) all keys that are in a down state |
366 | this._allKeysUp(); | |
d3796c14 | 367 | |
6d6f0db0 | 368 | //Log.Debug(">> Keyboard.ungrab"); |
0e4808bf JD |
369 | } |
370 | } |