]>
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 | ||
9fce233d PO |
167 | stopEvent(e); |
168 | ||
b22c9ef9 PO |
169 | // Possible start of AltGr sequence? (see above) |
170 | if ((code === "ControlLeft") && browser.isWindows() && | |
171 | !("ControlLeft" in this._keyDownList)) { | |
172 | this._altGrArmed = true; | |
173 | this._altGrTimeout = setTimeout(this._handleAltGrTimeout.bind(this), 100); | |
174 | this._altGrCtrlTime = e.timeStamp; | |
175 | return; | |
176 | } | |
f7363fd2 | 177 | |
f7363fd2 | 178 | this._sendKeyEvent(keysym, code, true); |
0e4808bf | 179 | } |
d6e281ba | 180 | |
0e4808bf | 181 | _handleKeyUp(e) { |
f7363fd2 PO |
182 | stopEvent(e); |
183 | ||
2b5f94fa | 184 | const code = this._getKeyCode(e); |
f7363fd2 | 185 | |
b22c9ef9 PO |
186 | // We can't get a release in the middle of an AltGr sequence, so |
187 | // abort that detection | |
188 | if (this._altGrArmed) { | |
189 | this._altGrArmed = false; | |
190 | clearTimeout(this._altGrTimeout); | |
191 | this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); | |
192 | } | |
193 | ||
634cc1ba | 194 | // See comment in _handleKeyDown() |
a6304f91 | 195 | if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) { |
634cc1ba PO |
196 | this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); |
197 | this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); | |
198 | return; | |
199 | } | |
200 | ||
ae820533 | 201 | this._sendKeyEvent(this._keyDownList[code], code, false); |
ccb511a5 PO |
202 | |
203 | // Windows has a rather nasty bug where it won't send key | |
204 | // release events for a Shift button if the other Shift is still | |
205 | // pressed | |
206 | if (browser.isWindows() && ((code === 'ShiftLeft') || | |
207 | (code === 'ShiftRight'))) { | |
208 | if ('ShiftRight' in this._keyDownList) { | |
209 | this._sendKeyEvent(this._keyDownList['ShiftRight'], | |
210 | 'ShiftRight', false); | |
211 | } | |
212 | if ('ShiftLeft' in this._keyDownList) { | |
213 | this._sendKeyEvent(this._keyDownList['ShiftLeft'], | |
214 | 'ShiftLeft', false); | |
215 | } | |
216 | } | |
0e4808bf | 217 | } |
ae820533 | 218 | |
0e4808bf | 219 | _handleAltGrTimeout() { |
b22c9ef9 PO |
220 | this._altGrArmed = false; |
221 | clearTimeout(this._altGrTimeout); | |
222 | this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); | |
0e4808bf | 223 | } |
d3796c14 | 224 | |
0e4808bf | 225 | _allKeysUp() { |
6d6f0db0 | 226 | Log.Debug(">> Keyboard.allKeysUp"); |
2b5f94fa | 227 | for (let code in this._keyDownList) { |
ae820533 | 228 | this._sendKeyEvent(this._keyDownList[code], code, false); |
8727f598 | 229 | } |
6d6f0db0 | 230 | Log.Debug("<< Keyboard.allKeysUp"); |
0e4808bf | 231 | } |
d6e281ba | 232 | |
747b4623 | 233 | // ===== PUBLIC METHODS ===== |
d6e281ba | 234 | |
0e4808bf | 235 | grab() { |
6d6f0db0 | 236 | //Log.Debug(">> Keyboard.grab"); |
d6e281ba | 237 | |
2b5f94fa JD |
238 | this._target.addEventListener('keydown', this._eventHandlers.keydown); |
239 | this._target.addEventListener('keyup', this._eventHandlers.keyup); | |
d6e281ba | 240 | |
6d6f0db0 SR |
241 | // Release (key up) if window loses focus |
242 | window.addEventListener('blur', this._eventHandlers.blur); | |
d6e281ba | 243 | |
6d6f0db0 | 244 | //Log.Debug("<< Keyboard.grab"); |
0e4808bf | 245 | } |
d6e281ba | 246 | |
0e4808bf | 247 | ungrab() { |
6d6f0db0 | 248 | //Log.Debug(">> Keyboard.ungrab"); |
d6e281ba | 249 | |
2b5f94fa JD |
250 | this._target.removeEventListener('keydown', this._eventHandlers.keydown); |
251 | this._target.removeEventListener('keyup', this._eventHandlers.keyup); | |
6d6f0db0 | 252 | window.removeEventListener('blur', this._eventHandlers.blur); |
d6e281ba | 253 | |
6d6f0db0 SR |
254 | // Release (key up) all keys that are in a down state |
255 | this._allKeysUp(); | |
d3796c14 | 256 | |
6d6f0db0 | 257 | //Log.Debug(">> Keyboard.ungrab"); |
0e4808bf JD |
258 | } |
259 | } |