| 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 | |
| 7 | import * as Log from '../util/logging.js'; |
| 8 | import { stopEvent } from '../util/events.js'; |
| 9 | import * as KeyboardUtil from "./util.js"; |
| 10 | import KeyTable from "./keysym.js"; |
| 11 | import * as browser from "../util/browser.js"; |
| 12 | |
| 13 | // |
| 14 | // Keyboard event handler |
| 15 | // |
| 16 | |
| 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) |
| 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._handleAltGrTimeout.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 | if (this._altGrArmed) { |
| 222 | this._altGrArmed = false; |
| 223 | clearTimeout(this._altGrTimeout); |
| 224 | this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); |
| 225 | } |
| 226 | |
| 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); |
| 231 | return; |
| 232 | } |
| 233 | |
| 234 | this._sendKeyEvent(this._keyDownList[code], code, false); |
| 235 | |
| 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 |
| 238 | // pressed |
| 239 | if (browser.isWindows() && ((code === 'ShiftLeft') || |
| 240 | (code === 'ShiftRight'))) { |
| 241 | if ('ShiftRight' in this._keyDownList) { |
| 242 | this._sendKeyEvent(this._keyDownList['ShiftRight'], |
| 243 | 'ShiftRight', false); |
| 244 | } |
| 245 | if ('ShiftLeft' in this._keyDownList) { |
| 246 | this._sendKeyEvent(this._keyDownList['ShiftLeft'], |
| 247 | 'ShiftLeft', false); |
| 248 | } |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | _handleAltGrTimeout() { |
| 253 | this._altGrArmed = false; |
| 254 | clearTimeout(this._altGrTimeout); |
| 255 | this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); |
| 256 | } |
| 257 | |
| 258 | _allKeysUp() { |
| 259 | Log.Debug(">> Keyboard.allKeysUp"); |
| 260 | for (let code in this._keyDownList) { |
| 261 | this._sendKeyEvent(this._keyDownList[code], code, false); |
| 262 | } |
| 263 | Log.Debug("<< Keyboard.allKeysUp"); |
| 264 | } |
| 265 | |
| 266 | // ===== PUBLIC METHODS ===== |
| 267 | |
| 268 | grab() { |
| 269 | //Log.Debug(">> Keyboard.grab"); |
| 270 | |
| 271 | this._target.addEventListener('keydown', this._eventHandlers.keydown); |
| 272 | this._target.addEventListener('keyup', this._eventHandlers.keyup); |
| 273 | |
| 274 | // Release (key up) if window loses focus |
| 275 | window.addEventListener('blur', this._eventHandlers.blur); |
| 276 | |
| 277 | //Log.Debug("<< Keyboard.grab"); |
| 278 | } |
| 279 | |
| 280 | ungrab() { |
| 281 | //Log.Debug(">> Keyboard.ungrab"); |
| 282 | |
| 283 | this._target.removeEventListener('keydown', this._eventHandlers.keydown); |
| 284 | this._target.removeEventListener('keyup', this._eventHandlers.keyup); |
| 285 | window.removeEventListener('blur', this._eventHandlers.blur); |
| 286 | |
| 287 | // Release (key up) all keys that are in a down state |
| 288 | this._allKeysUp(); |
| 289 | |
| 290 | //Log.Debug(">> Keyboard.ungrab"); |
| 291 | } |
| 292 | } |