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)
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";
14// Keyboard event handler
17export default class Keyboard {
19 this._target = target || null;
21 this._keyDownList = {}; // List of depressed keys
22 // (even if they are happy)
23 this._altGrArmed = false; // Windows AltGr detection
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),
32 // ===== EVENT HANDLERS =====
34 this.onkeyevent = () => {}; // Handler for key press/release
37 // ===== PRIVATE METHODS =====
39 _sendKeyEvent(keysym, code, down, numlock = null, capslock = null) {
41 this._keyDownList[code] = keysym;
43 // Do we really think this key is down?
44 if (!(code in this._keyDownList)) {
47 delete this._keyDownList[code];
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);
57 const code = KeyboardUtil.getKeycode(e);
58 if (code !== 'Unidentified') {
62 // Unstable, but we don't have anything else to go on
64 // 229 is used for composition events
65 if (e.keyCode !== 229) {
66 return 'Platform' + e.keyCode;
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) {
74 if (e.keyIdentifier.substr(0, 2) !== 'U+') {
75 return e.keyIdentifier;
78 const codepoint = parseInt(e.keyIdentifier.substr(2), 16);
79 const char = String.fromCharCode(codepoint).toUpperCase();
81 return 'Platform' + char.charCodeAt();
84 return 'Unidentified';
88 const code = this._getKeyCode(e);
89 let keysym = KeyboardUtil.getKeysym(e);
90 let numlock = e.getModifierState('NumLock');
91 let capslock = e.getModifierState('CapsLock');
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()) {
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);
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;
119 this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true, numlock, capslock);
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') {
127 // If it's a virtual keyboard then it should be
128 // sufficient to just send press and release right
130 this._sendKeyEvent(keysym, code, true, numlock, capslock);
131 this._sendKeyEvent(keysym, code, false, numlock, capslock);
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
142 if (browser.isMac() || browser.isIOS()) {
144 case KeyTable.XK_Super_L:
145 keysym = KeyTable.XK_Alt_L;
147 case KeyTable.XK_Super_R:
148 keysym = KeyTable.XK_Super_L;
150 case KeyTable.XK_Alt_L:
151 keysym = KeyTable.XK_Mode_switch;
153 case KeyTable.XK_Alt_R:
154 keysym = KeyTable.XK_ISO_Level3_Shift;
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];
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);
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);
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);
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._handleAltGrTimeout.bind(this), 100);
207 this._altGrCtrlTime = e.timeStamp;
211 this._sendKeyEvent(keysym, code, true, numlock, capslock);
217 const code = this._getKeyCode(e);
219 // We can't get a release in the middle of an AltGr sequence, so
220 // abort that detection
221 if (this._altGrArmed) {
222 this._altGrArmed = false;
223 clearTimeout(this._altGrTimeout);
224 this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
227 // See comment in _handleKeyDown()
228 if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {
229 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
230 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
234 this._sendKeyEvent(this._keyDownList[code], code, false);
236 // Windows has a rather nasty bug where it won't send key
237 // release events for a Shift button if the other Shift is still
239 if (browser.isWindows() && ((code === 'ShiftLeft') ||
240 (code === 'ShiftRight'))) {
241 if ('ShiftRight' in this._keyDownList) {
242 this._sendKeyEvent(this._keyDownList['ShiftRight'],
243 'ShiftRight', false);
245 if ('ShiftLeft' in this._keyDownList) {
246 this._sendKeyEvent(this._keyDownList['ShiftLeft'],
252 _handleAltGrTimeout() {
253 this._altGrArmed = false;
254 clearTimeout(this._altGrTimeout);
255 this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
259 Log.Debug(">> Keyboard.allKeysUp");
260 for (let code in this._keyDownList) {
261 this._sendKeyEvent(this._keyDownList[code], code, false);
263 Log.Debug("<< Keyboard.allKeysUp");
266 // ===== PUBLIC METHODS =====
269 //Log.Debug(">> Keyboard.grab");
271 this._target.addEventListener('keydown', this._eventHandlers.keydown);
272 this._target.addEventListener('keyup', this._eventHandlers.keyup);
274 // Release (key up) if window loses focus
275 window.addEventListener('blur', this._eventHandlers.blur);
277 //Log.Debug("<< Keyboard.grab");
281 //Log.Debug(">> Keyboard.ungrab");
283 this._target.removeEventListener('keydown', this._eventHandlers.keydown);
284 this._target.removeEventListener('keyup', this._eventHandlers.keyup);
285 window.removeEventListener('blur', this._eventHandlers.blur);
287 // Release (key up) all keys that are in a down state
290 //Log.Debug(">> Keyboard.ungrab");