| /* |
| * noVNC: HTML5 VNC client |
| * Copyright (C) 2019 The noVNC authors |
| * Licensed under MPL 2.0 or any later version (see LICENSE.txt) |
| */ |
| |
| import * as Log from '../util/logging.js'; |
| import { stopEvent } from '../util/events.js'; |
| import * as KeyboardUtil from "./util.js"; |
| import KeyTable from "./keysym.js"; |
| import * as browser from "../util/browser.js"; |
| |
| // |
| // Keyboard event handler |
| // |
| |
| export default class Keyboard { |
| constructor(target) { |
| this._target = target || null; |
| |
| this._keyDownList = {}; // List of depressed keys |
| // (even if they are happy) |
| this._altGrArmed = false; // Windows AltGr detection |
| |
| // keep these here so we can refer to them later |
| this._eventHandlers = { |
| 'keyup': this._handleKeyUp.bind(this), |
| 'keydown': this._handleKeyDown.bind(this), |
| 'blur': this._allKeysUp.bind(this), |
| }; |
| |
| // ===== EVENT HANDLERS ===== |
| |
| this.onkeyevent = () => {}; // Handler for key press/release |
| } |
| |
| // ===== PRIVATE METHODS ===== |
| |
| _sendKeyEvent(keysym, code, down, numlock = null, capslock = null) { |
| if (down) { |
| this._keyDownList[code] = keysym; |
| } else { |
| // Do we really think this key is down? |
| if (!(code in this._keyDownList)) { |
| return; |
| } |
| delete this._keyDownList[code]; |
| } |
| |
| Log.Debug("onkeyevent " + (down ? "down" : "up") + |
| ", keysym: " + keysym, ", code: " + code + |
| ", numlock: " + numlock + ", capslock: " + capslock); |
| this.onkeyevent(keysym, code, down, numlock, capslock); |
| } |
| |
| _getKeyCode(e) { |
| const code = KeyboardUtil.getKeycode(e); |
| if (code !== 'Unidentified') { |
| return code; |
| } |
| |
| // Unstable, but we don't have anything else to go on |
| if (e.keyCode) { |
| // 229 is used for composition events |
| if (e.keyCode !== 229) { |
| return 'Platform' + e.keyCode; |
| } |
| } |
| |
| // A precursor to the final DOM3 standard. Unfortunately it |
| // is not layout independent, so it is as bad as using keyCode |
| if (e.keyIdentifier) { |
| // Non-character key? |
| if (e.keyIdentifier.substr(0, 2) !== 'U+') { |
| return e.keyIdentifier; |
| } |
| |
| const codepoint = parseInt(e.keyIdentifier.substr(2), 16); |
| const char = String.fromCharCode(codepoint).toUpperCase(); |
| |
| return 'Platform' + char.charCodeAt(); |
| } |
| |
| return 'Unidentified'; |
| } |
| |
| _handleKeyDown(e) { |
| const code = this._getKeyCode(e); |
| let keysym = KeyboardUtil.getKeysym(e); |
| let numlock = e.getModifierState('NumLock'); |
| let capslock = e.getModifierState('CapsLock'); |
| |
| // getModifierState for NumLock is not supported on mac and ios and always returns false. |
| // Set to null to indicate unknown/unsupported instead. |
| if (browser.isMac() || browser.isIOS()) { |
| numlock = null; |
| } |
| |
| // Windows doesn't have a proper AltGr, but handles it using |
| // fake Ctrl+Alt. However the remote end might not be Windows, |
| // so we need to merge those in to a single AltGr event. We |
| // detect this case by seeing the two key events directly after |
| // each other with a very short time between them (<50ms). |
| if (this._altGrArmed) { |
| this._altGrArmed = false; |
| clearTimeout(this._altGrTimeout); |
| |
| if ((code === "AltRight") && |
| ((e.timeStamp - this._altGrCtrlTime) < 50)) { |
| // FIXME: We fail to detect this if either Ctrl key is |
| // first manually pressed as Windows then no |
| // longer sends the fake Ctrl down event. It |
| // does however happily send real Ctrl events |
| // even when AltGr is already down. Some |
| // browsers detect this for us though and set the |
| // key to "AltGraph". |
| keysym = KeyTable.XK_ISO_Level3_Shift; |
| } else { |
| this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true, numlock, capslock); |
| } |
| } |
| |
| // We cannot handle keys we cannot track, but we also need |
| // to deal with virtual keyboards which omit key info |
| if (code === 'Unidentified') { |
| if (keysym) { |
| // If it's a virtual keyboard then it should be |
| // sufficient to just send press and release right |
| // after each other |
| this._sendKeyEvent(keysym, code, true, numlock, capslock); |
| this._sendKeyEvent(keysym, code, false, numlock, capslock); |
| } |
| |
| stopEvent(e); |
| return; |
| } |
| |
| // Alt behaves more like AltGraph on macOS, so shuffle the |
| // keys around a bit to make things more sane for the remote |
| // server. This method is used by RealVNC and TigerVNC (and |
| // possibly others). |
| if (browser.isMac() || browser.isIOS()) { |
| switch (keysym) { |
| case KeyTable.XK_Super_L: |
| keysym = KeyTable.XK_Alt_L; |
| break; |
| case KeyTable.XK_Super_R: |
| keysym = KeyTable.XK_Super_L; |
| break; |
| case KeyTable.XK_Alt_L: |
| keysym = KeyTable.XK_Mode_switch; |
| break; |
| case KeyTable.XK_Alt_R: |
| keysym = KeyTable.XK_ISO_Level3_Shift; |
| break; |
| } |
| } |
| |
| // Is this key already pressed? If so, then we must use the |
| // same keysym or we'll confuse the server |
| if (code in this._keyDownList) { |
| keysym = this._keyDownList[code]; |
| } |
| |
| // macOS doesn't send proper key releases if a key is pressed |
| // while meta is held down |
| if ((browser.isMac() || browser.isIOS()) && |
| (e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight')) { |
| this._sendKeyEvent(keysym, code, true, numlock, capslock); |
| this._sendKeyEvent(keysym, code, false, numlock, capslock); |
| stopEvent(e); |
| return; |
| } |
| |
| // macOS doesn't send proper key events for modifiers, only |
| // state change events. That gets extra confusing for CapsLock |
| // which toggles on each press, but not on release. So pretend |
| // it was a quick press and release of the button. |
| if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) { |
| this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true, numlock, capslock); |
| this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false, numlock, capslock); |
| stopEvent(e); |
| return; |
| } |
| |
| // Windows doesn't send proper key releases for a bunch of |
| // Japanese IM keys so we have to fake the release right away |
| const jpBadKeys = [ KeyTable.XK_Zenkaku_Hankaku, |
| KeyTable.XK_Eisu_toggle, |
| KeyTable.XK_Katakana, |
| KeyTable.XK_Hiragana, |
| KeyTable.XK_Romaji ]; |
| if (browser.isWindows() && jpBadKeys.includes(keysym)) { |
| this._sendKeyEvent(keysym, code, true, numlock, capslock); |
| this._sendKeyEvent(keysym, code, false, numlock, capslock); |
| stopEvent(e); |
| return; |
| } |
| |
| stopEvent(e); |
| |
| // Possible start of AltGr sequence? (see above) |
| if ((code === "ControlLeft") && browser.isWindows() && |
| !("ControlLeft" in this._keyDownList)) { |
| this._altGrArmed = true; |
| this._altGrTimeout = setTimeout(this._interruptAltGrSequence.bind(this), 100); |
| this._altGrCtrlTime = e.timeStamp; |
| return; |
| } |
| |
| this._sendKeyEvent(keysym, code, true, numlock, capslock); |
| } |
| |
| _handleKeyUp(e) { |
| stopEvent(e); |
| |
| const code = this._getKeyCode(e); |
| |
| // We can't get a release in the middle of an AltGr sequence, so |
| // abort that detection |
| this._interruptAltGrSequence(); |
| |
| // See comment in _handleKeyDown() |
| if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) { |
| this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); |
| this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); |
| return; |
| } |
| |
| this._sendKeyEvent(this._keyDownList[code], code, false); |
| |
| // Windows has a rather nasty bug where it won't send key |
| // release events for a Shift button if the other Shift is still |
| // pressed |
| if (browser.isWindows() && ((code === 'ShiftLeft') || |
| (code === 'ShiftRight'))) { |
| if ('ShiftRight' in this._keyDownList) { |
| this._sendKeyEvent(this._keyDownList['ShiftRight'], |
| 'ShiftRight', false); |
| } |
| if ('ShiftLeft' in this._keyDownList) { |
| this._sendKeyEvent(this._keyDownList['ShiftLeft'], |
| 'ShiftLeft', false); |
| } |
| } |
| } |
| |
| _interruptAltGrSequence() { |
| if (this._altGrArmed) { |
| this._altGrArmed = false; |
| clearTimeout(this._altGrTimeout); |
| this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); |
| } |
| } |
| |
| _allKeysUp() { |
| Log.Debug(">> Keyboard.allKeysUp"); |
| |
| // Prevent control key being processed after losing focus. |
| this._interruptAltGrSequence(); |
| |
| for (let code in this._keyDownList) { |
| this._sendKeyEvent(this._keyDownList[code], code, false); |
| } |
| Log.Debug("<< Keyboard.allKeysUp"); |
| } |
| |
| // ===== PUBLIC METHODS ===== |
| |
| grab() { |
| //Log.Debug(">> Keyboard.grab"); |
| |
| this._target.addEventListener('keydown', this._eventHandlers.keydown); |
| this._target.addEventListener('keyup', this._eventHandlers.keyup); |
| |
| // Release (key up) if window loses focus |
| window.addEventListener('blur', this._eventHandlers.blur); |
| |
| //Log.Debug("<< Keyboard.grab"); |
| } |
| |
| ungrab() { |
| //Log.Debug(">> Keyboard.ungrab"); |
| |
| this._target.removeEventListener('keydown', this._eventHandlers.keydown); |
| this._target.removeEventListener('keyup', this._eventHandlers.keyup); |
| window.removeEventListener('blur', this._eventHandlers.blur); |
| |
| // Release (key up) all keys that are in a down state |
| this._allKeysUp(); |
| |
| //Log.Debug(">> Keyboard.ungrab"); |
| } |
| } |