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