]> git.proxmox.com Git - mirror_novnc.git/blame - core/input/keyboard.js
Fake key releases for some Japanese IM keys
[mirror_novnc.git] / core / input / keyboard.js
CommitLineData
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 7import * as Log from '../util/logging.js';
c1e2785f 8import { stopEvent } from '../util/events.js';
6d6f0db0 9import * as KeyboardUtil from "./util.js";
bf43c263 10import KeyTable from "./keysym.js";
59ef2916 11import * as browser from "../util/browser.js";
6d6f0db0
SR
12
13//
14// Keyboard event handler
15//
16
0e4808bf
JD
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)
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}