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