]>
Commit | Line | Data |
---|---|---|
d3796c14 JM |
1 | /* |
2 | * noVNC: HTML5 VNC client | |
412d9306 | 3 | * Copyright (C) 2019 The noVNC Authors |
1d728ace | 4 | * Licensed under MPL 2.0 or any later version (see LICENSE.txt) |
d3796c14 JM |
5 | */ |
6 | ||
6d6f0db0 | 7 | import * as Log from '../util/logging.js'; |
c1e2785f | 8 | import { stopEvent } from '../util/events.js'; |
6d6f0db0 | 9 | import * as KeyboardUtil from "./util.js"; |
bf43c263 | 10 | import KeyTable from "./keysym.js"; |
59ef2916 | 11 | import * as browser from "../util/browser.js"; |
6d6f0db0 SR |
12 | |
13 | // | |
14 | // Keyboard event handler | |
15 | // | |
16 | ||
0e4808bf JD |
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) | |
0e4808bf JD |
23 | this._altGrArmed = false; // Windows AltGr detection |
24 | ||
25 | // keep these here so we can refer to them later | |
26 | this._eventHandlers = { | |
27 | 'keyup': this._handleKeyUp.bind(this), | |
28 | 'keydown': this._handleKeyDown.bind(this), | |
0e4808bf | 29 | 'blur': this._allKeysUp.bind(this), |
0e4808bf | 30 | }; |
d6e281ba | 31 | |
0e4808bf | 32 | // ===== EVENT HANDLERS ===== |
d6e281ba | 33 | |
0e4808bf JD |
34 | this.onkeyevent = () => {}; // Handler for key press/release |
35 | } | |
f7363fd2 | 36 | |
747b4623 PO |
37 | // ===== PRIVATE METHODS ===== |
38 | ||
0e4808bf | 39 | _sendKeyEvent(keysym, code, down) { |
d6ae4457 PO |
40 | if (down) { |
41 | this._keyDownList[code] = keysym; | |
42 | } else { | |
43 | // Do we really think this key is down? | |
44 | if (!(code in this._keyDownList)) { | |
45 | return; | |
bf43c263 | 46 | } |
d6ae4457 | 47 | delete this._keyDownList[code]; |
bf43c263 PO |
48 | } |
49 | ||
747b4623 | 50 | Log.Debug("onkeyevent " + (down ? "down" : "up") + |
f7363fd2 | 51 | ", keysym: " + keysym, ", code: " + code); |
747b4623 | 52 | this.onkeyevent(keysym, code, down); |
0e4808bf | 53 | } |
f7363fd2 | 54 | |
0e4808bf | 55 | _getKeyCode(e) { |
2b5f94fa | 56 | const code = KeyboardUtil.getKeycode(e); |
7e79dfe4 PO |
57 | if (code !== 'Unidentified') { |
58 | return code; | |
59 | } | |
60 | ||
61 | // Unstable, but we don't have anything else to go on | |
1f7e1c75 | 62 | if (e.keyCode) { |
4093c37f PO |
63 | // 229 is used for composition events |
64 | if (e.keyCode !== 229) { | |
65 | return 'Platform' + e.keyCode; | |
66 | } | |
7e79dfe4 PO |
67 | } |
68 | ||
69 | // A precursor to the final DOM3 standard. Unfortunately it | |
70 | // is not layout independent, so it is as bad as using keyCode | |
71 | if (e.keyIdentifier) { | |
72 | // Non-character key? | |
73 | if (e.keyIdentifier.substr(0, 2) !== 'U+') { | |
74 | return e.keyIdentifier; | |
f7363fd2 | 75 | } |
7e79dfe4 | 76 | |
2b5f94fa JD |
77 | const codepoint = parseInt(e.keyIdentifier.substr(2), 16); |
78 | const char = String.fromCharCode(codepoint).toUpperCase(); | |
7e79dfe4 PO |
79 | |
80 | return 'Platform' + char.charCodeAt(); | |
f7363fd2 PO |
81 | } |
82 | ||
7e79dfe4 | 83 | return 'Unidentified'; |
0e4808bf | 84 | } |
d6e281ba | 85 | |
0e4808bf | 86 | _handleKeyDown(e) { |
2b5f94fa JD |
87 | const code = this._getKeyCode(e); |
88 | let keysym = KeyboardUtil.getKeysym(e); | |
f7363fd2 | 89 | |
b22c9ef9 PO |
90 | // Windows doesn't have a proper AltGr, but handles it using |
91 | // fake Ctrl+Alt. However the remote end might not be Windows, | |
92 | // so we need to merge those in to a single AltGr event. We | |
93 | // detect this case by seeing the two key events directly after | |
94 | // each other with a very short time between them (<50ms). | |
95 | if (this._altGrArmed) { | |
96 | this._altGrArmed = false; | |
97 | clearTimeout(this._altGrTimeout); | |
98 | ||
99 | if ((code === "AltRight") && | |
100 | ((e.timeStamp - this._altGrCtrlTime) < 50)) { | |
101 | // FIXME: We fail to detect this if either Ctrl key is | |
102 | // first manually pressed as Windows then no | |
103 | // longer sends the fake Ctrl down event. It | |
104 | // does however happily send real Ctrl events | |
105 | // even when AltGr is already down. Some | |
106 | // browsers detect this for us though and set the | |
107 | // key to "AltGraph". | |
108 | keysym = KeyTable.XK_ISO_Level3_Shift; | |
109 | } else { | |
110 | this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); | |
111 | } | |
112 | } | |
113 | ||
ae820533 PO |
114 | // We cannot handle keys we cannot track, but we also need |
115 | // to deal with virtual keyboards which omit key info | |
8c51e9a8 | 116 | if (code === 'Unidentified') { |
ae820533 PO |
117 | if (keysym) { |
118 | // If it's a virtual keyboard then it should be | |
119 | // sufficient to just send press and release right | |
120 | // after each other | |
9e99ce12 PO |
121 | this._sendKeyEvent(keysym, code, true); |
122 | this._sendKeyEvent(keysym, code, false); | |
ae820533 PO |
123 | } |
124 | ||
125 | stopEvent(e); | |
126 | return; | |
127 | } | |
128 | ||
bf43c263 PO |
129 | // Alt behaves more like AltGraph on macOS, so shuffle the |
130 | // keys around a bit to make things more sane for the remote | |
131 | // server. This method is used by RealVNC and TigerVNC (and | |
132 | // possibly others). | |
175b843b | 133 | if (browser.isMac() || browser.isIOS()) { |
bf43c263 | 134 | switch (keysym) { |
7b536961 PO |
135 | case KeyTable.XK_Super_L: |
136 | keysym = KeyTable.XK_Alt_L; | |
137 | break; | |
138 | case KeyTable.XK_Super_R: | |
139 | keysym = KeyTable.XK_Super_L; | |
140 | break; | |
141 | case KeyTable.XK_Alt_L: | |
142 | keysym = KeyTable.XK_Mode_switch; | |
143 | break; | |
144 | case KeyTable.XK_Alt_R: | |
145 | keysym = KeyTable.XK_ISO_Level3_Shift; | |
146 | break; | |
bf43c263 PO |
147 | } |
148 | } | |
149 | ||
ae820533 PO |
150 | // Is this key already pressed? If so, then we must use the |
151 | // same keysym or we'll confuse the server | |
152 | if (code in this._keyDownList) { | |
153 | keysym = this._keyDownList[code]; | |
154 | } | |
155 | ||
634cc1ba PO |
156 | // macOS doesn't send proper key events for modifiers, only |
157 | // state change events. That gets extra confusing for CapsLock | |
158 | // which toggles on each press, but not on release. So pretend | |
159 | // it was a quick press and release of the button. | |
a6304f91 | 160 | if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) { |
634cc1ba PO |
161 | this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); |
162 | this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); | |
163 | stopEvent(e); | |
164 | return; | |
165 | } | |
166 | ||
0cdf2962 PO |
167 | // Windows doesn't send proper key releases for a bunch of |
168 | // Japanese IM keys so we have to fake the release right away | |
169 | const jpBadKeys = [ KeyTable.XK_Zenkaku_Hankaku, | |
170 | KeyTable.XK_Eisu_toggle, | |
171 | KeyTable.XK_Katakana, | |
172 | KeyTable.XK_Hiragana, | |
173 | KeyTable.XK_Romaji ]; | |
174 | if (browser.isWindows() && jpBadKeys.includes(keysym)) { | |
175 | this._sendKeyEvent(keysym, code, true); | |
176 | this._sendKeyEvent(keysym, code, false); | |
177 | stopEvent(e); | |
178 | return; | |
179 | } | |
180 | ||
9fce233d PO |
181 | stopEvent(e); |
182 | ||
b22c9ef9 PO |
183 | // Possible start of AltGr sequence? (see above) |
184 | if ((code === "ControlLeft") && browser.isWindows() && | |
185 | !("ControlLeft" in this._keyDownList)) { | |
186 | this._altGrArmed = true; | |
187 | this._altGrTimeout = setTimeout(this._handleAltGrTimeout.bind(this), 100); | |
188 | this._altGrCtrlTime = e.timeStamp; | |
189 | return; | |
190 | } | |
f7363fd2 | 191 | |
f7363fd2 | 192 | this._sendKeyEvent(keysym, code, true); |
0e4808bf | 193 | } |
d6e281ba | 194 | |
0e4808bf | 195 | _handleKeyUp(e) { |
f7363fd2 PO |
196 | stopEvent(e); |
197 | ||
2b5f94fa | 198 | const code = this._getKeyCode(e); |
f7363fd2 | 199 | |
b22c9ef9 PO |
200 | // We can't get a release in the middle of an AltGr sequence, so |
201 | // abort that detection | |
202 | if (this._altGrArmed) { | |
203 | this._altGrArmed = false; | |
204 | clearTimeout(this._altGrTimeout); | |
205 | this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); | |
206 | } | |
207 | ||
634cc1ba | 208 | // See comment in _handleKeyDown() |
a6304f91 | 209 | if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) { |
634cc1ba PO |
210 | this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); |
211 | this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); | |
212 | return; | |
213 | } | |
214 | ||
ae820533 | 215 | this._sendKeyEvent(this._keyDownList[code], code, false); |
ccb511a5 PO |
216 | |
217 | // Windows has a rather nasty bug where it won't send key | |
218 | // release events for a Shift button if the other Shift is still | |
219 | // pressed | |
220 | if (browser.isWindows() && ((code === 'ShiftLeft') || | |
221 | (code === 'ShiftRight'))) { | |
222 | if ('ShiftRight' in this._keyDownList) { | |
223 | this._sendKeyEvent(this._keyDownList['ShiftRight'], | |
224 | 'ShiftRight', false); | |
225 | } | |
226 | if ('ShiftLeft' in this._keyDownList) { | |
227 | this._sendKeyEvent(this._keyDownList['ShiftLeft'], | |
228 | 'ShiftLeft', false); | |
229 | } | |
230 | } | |
0e4808bf | 231 | } |
ae820533 | 232 | |
0e4808bf | 233 | _handleAltGrTimeout() { |
b22c9ef9 PO |
234 | this._altGrArmed = false; |
235 | clearTimeout(this._altGrTimeout); | |
236 | this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); | |
0e4808bf | 237 | } |
d3796c14 | 238 | |
0e4808bf | 239 | _allKeysUp() { |
6d6f0db0 | 240 | Log.Debug(">> Keyboard.allKeysUp"); |
2b5f94fa | 241 | for (let code in this._keyDownList) { |
ae820533 | 242 | this._sendKeyEvent(this._keyDownList[code], code, false); |
8727f598 | 243 | } |
6d6f0db0 | 244 | Log.Debug("<< Keyboard.allKeysUp"); |
0e4808bf | 245 | } |
d6e281ba | 246 | |
747b4623 | 247 | // ===== PUBLIC METHODS ===== |
d6e281ba | 248 | |
0e4808bf | 249 | grab() { |
6d6f0db0 | 250 | //Log.Debug(">> Keyboard.grab"); |
d6e281ba | 251 | |
2b5f94fa JD |
252 | this._target.addEventListener('keydown', this._eventHandlers.keydown); |
253 | this._target.addEventListener('keyup', this._eventHandlers.keyup); | |
d6e281ba | 254 | |
6d6f0db0 SR |
255 | // Release (key up) if window loses focus |
256 | window.addEventListener('blur', this._eventHandlers.blur); | |
d6e281ba | 257 | |
6d6f0db0 | 258 | //Log.Debug("<< Keyboard.grab"); |
0e4808bf | 259 | } |
d6e281ba | 260 | |
0e4808bf | 261 | ungrab() { |
6d6f0db0 | 262 | //Log.Debug(">> Keyboard.ungrab"); |
d6e281ba | 263 | |
2b5f94fa JD |
264 | this._target.removeEventListener('keydown', this._eventHandlers.keydown); |
265 | this._target.removeEventListener('keyup', this._eventHandlers.keyup); | |
6d6f0db0 | 266 | window.removeEventListener('blur', this._eventHandlers.blur); |
d6e281ba | 267 | |
6d6f0db0 SR |
268 | // Release (key up) all keys that are in a down state |
269 | this._allKeysUp(); | |
d3796c14 | 270 | |
6d6f0db0 | 271 | //Log.Debug(">> Keyboard.ungrab"); |
0e4808bf JD |
272 | } |
273 | } |