Feature/kasm 2335 ime support 2 (#27)
IME support, refactored keyboard input
This commit is contained in:
parent
df9c9d0d96
commit
385a1f99b4
|
@ -936,15 +936,15 @@ select:active {
|
||||||
}
|
}
|
||||||
|
|
||||||
#noVNC_keyboardinput {
|
#noVNC_keyboardinput {
|
||||||
width: 1px;
|
width: 0px;
|
||||||
height: 1px;
|
height: 0px;
|
||||||
background-color: #fff;
|
background-color: #fff0;
|
||||||
color: #fff;
|
color: rgba(5, 5, 5, 0);
|
||||||
border: 0;
|
border: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -40px;
|
left: 35%;
|
||||||
|
top: 40%;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
ime-mode: disabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Default noVNC logo.*/
|
/*Default noVNC logo.*/
|
||||||
|
@ -1021,6 +1021,11 @@ body {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#noVNC_keyboard_control .noVNC_selected {
|
||||||
|
background-color:rgb(15, 36, 153);
|
||||||
|
border: 6px rgb(15, 36, 153) solid;
|
||||||
|
}
|
||||||
|
|
||||||
.keyboard-controls .button.ctrl {
|
.keyboard-controls .button.ctrl {
|
||||||
background-image: url("../images/ctrl.svg");
|
background-image: url("../images/ctrl.svg");
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
|
|
167
app/ui.js
167
app/ui.js
|
@ -65,8 +65,6 @@ const UI = {
|
||||||
controlbarMouseDownClientY: 0,
|
controlbarMouseDownClientY: 0,
|
||||||
controlbarMouseDownOffsetY: 0,
|
controlbarMouseDownOffsetY: 0,
|
||||||
|
|
||||||
lastKeyboardinput: null,
|
|
||||||
defaultKeyboardinputLen: 100,
|
|
||||||
needToCheckClipboardChange: false,
|
needToCheckClipboardChange: false,
|
||||||
|
|
||||||
inhibitReconnect: true,
|
inhibitReconnect: true,
|
||||||
|
@ -135,10 +133,6 @@ const UI = {
|
||||||
UI.addSettingsHandlers();
|
UI.addSettingsHandlers();
|
||||||
document.getElementById("noVNC_status")
|
document.getElementById("noVNC_status")
|
||||||
.addEventListener('click', UI.hideStatus);
|
.addEventListener('click', UI.hideStatus);
|
||||||
|
|
||||||
// Bootstrap fallback input handler
|
|
||||||
UI.keyboardinputReset();
|
|
||||||
|
|
||||||
UI.openControlbar();
|
UI.openControlbar();
|
||||||
|
|
||||||
UI.updateVisualState('init');
|
UI.updateVisualState('init');
|
||||||
|
@ -245,6 +239,9 @@ const UI = {
|
||||||
UI.initSetting('prefer_local_cursor', true);
|
UI.initSetting('prefer_local_cursor', true);
|
||||||
UI.initSetting('toggle_control_panel', false);
|
UI.initSetting('toggle_control_panel', false);
|
||||||
UI.initSetting('enable_perf_stats', false);
|
UI.initSetting('enable_perf_stats', false);
|
||||||
|
UI.initSetting('virtual_keyboard_visible', false);
|
||||||
|
UI.initSetting('enable_ime', false)
|
||||||
|
UI.toggleKeyboardControls();
|
||||||
|
|
||||||
if (WebUtil.isInsideKasmVDI()) {
|
if (WebUtil.isInsideKasmVDI()) {
|
||||||
UI.initSetting('clipboard_up', false);
|
UI.initSetting('clipboard_up', false);
|
||||||
|
@ -371,12 +368,6 @@ const UI = {
|
||||||
addTouchSpecificHandlers() {
|
addTouchSpecificHandlers() {
|
||||||
document.getElementById("noVNC_keyboard_button")
|
document.getElementById("noVNC_keyboard_button")
|
||||||
.addEventListener('click', UI.toggleVirtualKeyboard);
|
.addEventListener('click', UI.toggleVirtualKeyboard);
|
||||||
|
|
||||||
UI.touchKeyboard = new Keyboard(document.getElementById('noVNC_keyboardinput'));
|
|
||||||
UI.touchKeyboard.onkeyevent = UI.keyEvent;
|
|
||||||
UI.touchKeyboard.grab();
|
|
||||||
document.getElementById("noVNC_keyboardinput")
|
|
||||||
.addEventListener('input', UI.keyInput);
|
|
||||||
document.getElementById("noVNC_keyboardinput")
|
document.getElementById("noVNC_keyboardinput")
|
||||||
.addEventListener('focus', UI.onfocusVirtualKeyboard);
|
.addEventListener('focus', UI.onfocusVirtualKeyboard);
|
||||||
document.getElementById("noVNC_keyboardinput")
|
document.getElementById("noVNC_keyboardinput")
|
||||||
|
@ -536,6 +527,10 @@ const UI = {
|
||||||
UI.addSettingChangeHandler('clipboard_seamless');
|
UI.addSettingChangeHandler('clipboard_seamless');
|
||||||
UI.addSettingChangeHandler('clipboard_up');
|
UI.addSettingChangeHandler('clipboard_up');
|
||||||
UI.addSettingChangeHandler('clipboard_down');
|
UI.addSettingChangeHandler('clipboard_down');
|
||||||
|
UI.addSettingChangeHandler('virtual_keyboard_visible');
|
||||||
|
UI.addSettingChangeHandler('virtual_keyboard_visible', UI.toggleKeyboardControls);
|
||||||
|
UI.addSettingChangeHandler('enable_ime');
|
||||||
|
UI.addSettingChangeHandler('enable_ime', UI.toggleIMEMode);
|
||||||
},
|
},
|
||||||
|
|
||||||
addFullscreenHandlers() {
|
addFullscreenHandlers() {
|
||||||
|
@ -1297,7 +1292,9 @@ const UI = {
|
||||||
}
|
}
|
||||||
url += '/' + path;
|
url += '/' + path;
|
||||||
|
|
||||||
UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
|
UI.rfb = new RFB(document.getElementById('noVNC_container'),
|
||||||
|
document.getElementById('noVNC_keyboardinput'),
|
||||||
|
url,
|
||||||
{ shared: UI.getSetting('shared'),
|
{ shared: UI.getSetting('shared'),
|
||||||
repeaterID: UI.getSetting('repeaterID'),
|
repeaterID: UI.getSetting('repeaterID'),
|
||||||
credentials: { password: password } });
|
credentials: { password: password } });
|
||||||
|
@ -1335,6 +1332,7 @@ const UI = {
|
||||||
UI.rfb.clipboardUp = UI.getSetting('clipboard_up');
|
UI.rfb.clipboardUp = UI.getSetting('clipboard_up');
|
||||||
UI.rfb.clipboardDown = UI.getSetting('clipboard_down');
|
UI.rfb.clipboardDown = UI.getSetting('clipboard_down');
|
||||||
UI.rfb.clipboardSeamless = UI.getSetting('clipboard_seamless');
|
UI.rfb.clipboardSeamless = UI.getSetting('clipboard_seamless');
|
||||||
|
UI.rfb.keyboard.enableIME = UI.getSetting('enable_ime');
|
||||||
UI.rfb.clipboardBinary = supportsBinaryClipboard() && UI.rfb.clipboardSeamless;
|
UI.rfb.clipboardBinary = supportsBinaryClipboard() && UI.rfb.clipboardSeamless;
|
||||||
|
|
||||||
//Only explicitly request permission to clipboard on browsers that support binary clipboard access
|
//Only explicitly request permission to clipboard on browsers that support binary clipboard access
|
||||||
|
@ -1544,6 +1542,30 @@ const UI = {
|
||||||
UI.forceSetting('video_quality', parseInt(event.data.value), false);
|
UI.forceSetting('video_quality', parseInt(event.data.value), false);
|
||||||
UI.updateQuality();
|
UI.updateQuality();
|
||||||
break;
|
break;
|
||||||
|
case 'show_keyboard_controls':
|
||||||
|
if (!UI.getSetting('virtual_keyboard_visible')) {
|
||||||
|
UI.forceSetting('virtual_keyboard_visible', true, false);
|
||||||
|
UI.showKeyboardControls();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'hide_keyboard_controls':
|
||||||
|
if (UI.getSetting('virtual_keyboard_visible')) {
|
||||||
|
UI.forceSetting('virtual_keyboard_visible', true, false);
|
||||||
|
UI.hideKeyboardControls();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'enable_ime_mode':
|
||||||
|
if (!UI.getSetting('enable_ime')) {
|
||||||
|
UI.forceSetting('enable_ime', true, false);
|
||||||
|
UI.toggleIMEMode();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'disable_ime_mode':
|
||||||
|
if (UI.getSetting('enable_ime')) {
|
||||||
|
UI.forceSetting('enable_ime', false, false);
|
||||||
|
UI.toggleIMEMode();
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1881,18 +1903,41 @@ const UI = {
|
||||||
UI.rfb.translateShortcuts = UI.getSetting('translate_shortcuts');
|
UI.rfb.translateShortcuts = UI.getSetting('translate_shortcuts');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleKeyboardControls() {
|
||||||
|
if (UI.getSetting('virtual_keyboard_visible')) {
|
||||||
|
UI.showKeyboardControls();
|
||||||
|
} else {
|
||||||
|
UI.hideKeyboardControls();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleIMEMode() {
|
||||||
|
if (UI.rfb) {
|
||||||
|
if (UI.getSetting('enable_ime')) {
|
||||||
|
UI.rfb.keyboard.enableIME = true;
|
||||||
|
} else {
|
||||||
|
UI.rfb.keyboard.enableIME = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
showKeyboardControls() {
|
showKeyboardControls() {
|
||||||
document.querySelector(".keyboard-controls").classList.add("is-visible");
|
document.getElementById('noVNC_keyboard_control').classList.add("is-visible");
|
||||||
},
|
},
|
||||||
|
|
||||||
hideKeyboardControls() {
|
hideKeyboardControls() {
|
||||||
document.querySelector(".keyboard-controls").classList.remove("is-visible");
|
document.getElementById('noVNC_keyboard_control').classList.remove("is-visible");
|
||||||
},
|
},
|
||||||
|
|
||||||
showVirtualKeyboard() {
|
showVirtualKeyboard() {
|
||||||
const input = document.getElementById('noVNC_keyboardinput');
|
const input = document.getElementById('noVNC_keyboardinput');
|
||||||
|
|
||||||
if (document.activeElement == input) return;
|
if (document.activeElement == input || !UI.rfb) return;
|
||||||
|
|
||||||
|
if (UI.getSetting('virtual_keyboard_visible')) {
|
||||||
|
document.getElementById('noVNC_keyboard_control_handle')
|
||||||
|
.classList.add("noVNC_selected");
|
||||||
|
}
|
||||||
|
|
||||||
input.focus();
|
input.focus();
|
||||||
|
|
||||||
|
@ -1916,7 +1961,12 @@ const UI = {
|
||||||
hideVirtualKeyboard() {
|
hideVirtualKeyboard() {
|
||||||
const input = document.getElementById('noVNC_keyboardinput');
|
const input = document.getElementById('noVNC_keyboardinput');
|
||||||
|
|
||||||
if (document.activeElement != input) return;
|
if (document.activeElement != input || !UI.rfb) return;
|
||||||
|
|
||||||
|
if (UI.getSetting('virtual_keyboard_visible')) {
|
||||||
|
document.getElementById('noVNC_keyboard_control_handle')
|
||||||
|
.classList.remove("noVNC_selected");
|
||||||
|
}
|
||||||
|
|
||||||
input.blur();
|
input.blur();
|
||||||
},
|
},
|
||||||
|
@ -1941,6 +1991,12 @@ const UI = {
|
||||||
onblurVirtualKeyboard(event) {
|
onblurVirtualKeyboard(event) {
|
||||||
document.getElementById('noVNC_keyboard_button')
|
document.getElementById('noVNC_keyboard_button')
|
||||||
.classList.remove("noVNC_selected");
|
.classList.remove("noVNC_selected");
|
||||||
|
|
||||||
|
if (UI.getSetting('virtual_keyboard_visible')) {
|
||||||
|
document.getElementById('noVNC_keyboard_control_handle')
|
||||||
|
.classList.remove("noVNC_selected");
|
||||||
|
}
|
||||||
|
|
||||||
if (UI.rfb) {
|
if (UI.rfb) {
|
||||||
UI.rfb.focusOnClick = true;
|
UI.rfb.focusOnClick = true;
|
||||||
}
|
}
|
||||||
|
@ -1974,83 +2030,6 @@ const UI = {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
},
|
},
|
||||||
|
|
||||||
keyboardinputReset() {
|
|
||||||
const kbi = document.getElementById('noVNC_keyboardinput');
|
|
||||||
kbi.value = new Array(UI.defaultKeyboardinputLen).join("_");
|
|
||||||
UI.lastKeyboardinput = kbi.value;
|
|
||||||
},
|
|
||||||
|
|
||||||
keyEvent(keysym, code, down) {
|
|
||||||
if (!UI.rfb) return;
|
|
||||||
|
|
||||||
UI.rfb.sendKey(keysym, code, down);
|
|
||||||
},
|
|
||||||
|
|
||||||
// When normal keyboard events are left uncought, use the input events from
|
|
||||||
// the keyboardinput element instead and generate the corresponding key events.
|
|
||||||
// This code is required since some browsers on Android are inconsistent in
|
|
||||||
// sending keyCodes in the normal keyboard events when using on screen keyboards.
|
|
||||||
keyInput(event) {
|
|
||||||
|
|
||||||
if (!UI.rfb) return;
|
|
||||||
|
|
||||||
const newValue = event.target.value;
|
|
||||||
|
|
||||||
if (!UI.lastKeyboardinput) {
|
|
||||||
UI.keyboardinputReset();
|
|
||||||
}
|
|
||||||
const oldValue = UI.lastKeyboardinput;
|
|
||||||
|
|
||||||
let newLen;
|
|
||||||
try {
|
|
||||||
// Try to check caret position since whitespace at the end
|
|
||||||
// will not be considered by value.length in some browsers
|
|
||||||
newLen = Math.max(event.target.selectionStart, newValue.length);
|
|
||||||
} catch (err) {
|
|
||||||
// selectionStart is undefined in Google Chrome
|
|
||||||
newLen = newValue.length;
|
|
||||||
}
|
|
||||||
const oldLen = oldValue.length;
|
|
||||||
|
|
||||||
let inputs = newLen - oldLen;
|
|
||||||
let backspaces = inputs < 0 ? -inputs : 0;
|
|
||||||
|
|
||||||
// Compare the old string with the new to account for
|
|
||||||
// text-corrections or other input that modify existing text
|
|
||||||
for (let i = 0; i < Math.min(oldLen, newLen); i++) {
|
|
||||||
if (newValue.charAt(i) != oldValue.charAt(i)) {
|
|
||||||
inputs = newLen - i;
|
|
||||||
backspaces = oldLen - i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the key events
|
|
||||||
for (let i = 0; i < backspaces; i++) {
|
|
||||||
UI.rfb.sendKey(KeyTable.XK_BackSpace, "Backspace");
|
|
||||||
}
|
|
||||||
for (let i = newLen - inputs; i < newLen; i++) {
|
|
||||||
UI.rfb.sendKey(keysyms.lookup(newValue.charCodeAt(i)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Control the text content length in the keyboardinput element
|
|
||||||
if (newLen > 2 * UI.defaultKeyboardinputLen) {
|
|
||||||
UI.keyboardinputReset();
|
|
||||||
} else if (newLen < 1) {
|
|
||||||
// There always have to be some text in the keyboardinput
|
|
||||||
// element with which backspace can interact.
|
|
||||||
UI.keyboardinputReset();
|
|
||||||
// This sometimes causes the keyboard to disappear for a second
|
|
||||||
// but it is required for the android keyboard to recognize that
|
|
||||||
// text has been added to the field
|
|
||||||
event.target.blur();
|
|
||||||
// This has to be ran outside of the input handler in order to work
|
|
||||||
setTimeout(event.target.focus.bind(event.target), 0);
|
|
||||||
} else {
|
|
||||||
UI.lastKeyboardinput = newValue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ------^-------
|
/* ------^-------
|
||||||
* /KEYBOARD
|
* /KEYBOARD
|
||||||
* ==============
|
* ==============
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* KasmVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2022 Kasm Technologies Inc
|
||||||
|
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Keys that could be interaction with IME input
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
0x30: 'Digit0',
|
||||||
|
0x31: 'Digit1',
|
||||||
|
0x32: 'Digit2',
|
||||||
|
0x33: 'Digit3',
|
||||||
|
0x34: 'Digit4',
|
||||||
|
0x35: 'Digit5',
|
||||||
|
0x36: 'Digit6',
|
||||||
|
0x37: 'Digit7',
|
||||||
|
0x38: 'Digit8',
|
||||||
|
0x39: 'Digit9',
|
||||||
|
0x60: 'Numpad0',
|
||||||
|
0x61: 'Numpad1',
|
||||||
|
0x62: 'Numpad2',
|
||||||
|
0x63: 'Numpad3',
|
||||||
|
0x64: 'Numpad4',
|
||||||
|
0x65: 'Numpad5',
|
||||||
|
0x66: 'Numpad6',
|
||||||
|
0x67: 'Numpad7',
|
||||||
|
0x68: 'Numpad8',
|
||||||
|
0x69: 'Numpad9'
|
||||||
|
};
|
|
@ -8,16 +8,20 @@ import * as Log from '../util/logging.js';
|
||||||
import { stopEvent } from '../util/events.js';
|
import { stopEvent } from '../util/events.js';
|
||||||
import * as KeyboardUtil from "./util.js";
|
import * as KeyboardUtil from "./util.js";
|
||||||
import KeyTable from "./keysym.js";
|
import KeyTable from "./keysym.js";
|
||||||
|
import keysyms from "./keysymdef.js";
|
||||||
|
import imekeys from "./imekeys.js";
|
||||||
import * as browser from "../util/browser.js";
|
import * as browser from "../util/browser.js";
|
||||||
import UI from '../../app/ui.js';
|
import UI from '../../app/ui.js';
|
||||||
|
import { isChromiumBased } from '../util/browser.js';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Keyboard event handler
|
// Keyboard event handler
|
||||||
//
|
//
|
||||||
|
|
||||||
export default class Keyboard {
|
export default class Keyboard {
|
||||||
constructor(target) {
|
constructor(screenInput, touchInput) {
|
||||||
this._target = target || null;
|
this._screenInput = screenInput;
|
||||||
|
this._touchInput = touchInput;
|
||||||
|
|
||||||
this._keyDownList = {}; // List of depressed keys
|
this._keyDownList = {}; // List of depressed keys
|
||||||
// (even if they are happy)
|
// (even if they are happy)
|
||||||
|
@ -28,11 +32,28 @@ export default class Keyboard {
|
||||||
'keyup': this._handleKeyUp.bind(this),
|
'keyup': this._handleKeyUp.bind(this),
|
||||||
'keydown': this._handleKeyDown.bind(this),
|
'keydown': this._handleKeyDown.bind(this),
|
||||||
'blur': this._allKeysUp.bind(this),
|
'blur': this._allKeysUp.bind(this),
|
||||||
|
'compositionstart' : this._handleCompositionStart.bind(this),
|
||||||
|
'compositionend' : this._handleCompositionEnd.bind(this),
|
||||||
|
'input' : this._handleInput.bind(this)
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== EVENT HANDLERS =====
|
// ===== EVENT HANDLERS =====
|
||||||
|
|
||||||
this.onkeyevent = () => {}; // Handler for key press/release
|
this.onkeyevent = () => {}; // Handler for key press/release
|
||||||
|
|
||||||
|
this._enableIME = false;
|
||||||
|
this._imeHold = false;
|
||||||
|
this._imeInProgress = false;
|
||||||
|
this._lastKeyboardInput = null;
|
||||||
|
this._defaultKeyboardInputLen = 100;
|
||||||
|
this._keyboardInputReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== PUBLIC METHODS =====
|
||||||
|
|
||||||
|
get enableIME() { return this._enableIME; }
|
||||||
|
set enableIME(val) {
|
||||||
|
this._enableIME = val;
|
||||||
|
this.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== PRIVATE METHODS =====
|
// ===== PRIVATE METHODS =====
|
||||||
|
@ -95,10 +116,135 @@ export default class Keyboard {
|
||||||
return 'Unidentified';
|
return 'Unidentified';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleCompositionStart(e) {
|
||||||
|
Log.Debug("composition started");
|
||||||
|
if (this._enableIME) {
|
||||||
|
this._imeHold = true;
|
||||||
|
this._imeInProgress = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleCompositionEnd(e) {
|
||||||
|
Log.Debug("Composition ended");
|
||||||
|
if (this._enableIME) { this._imeInProgress = false; }
|
||||||
|
if (isChromiumBased()) {
|
||||||
|
this._imeHold = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleInput(e) {
|
||||||
|
//input event occurs only when keyup keydown events don't prevent default
|
||||||
|
//IME events will make this happen, for example
|
||||||
|
//IME changes can back out old characters and replace, thus send differential if IME
|
||||||
|
//otherwise send new characters
|
||||||
|
if (this._enableIME && this._imeHold) {
|
||||||
|
Log.Debug("IME input change, sending differential");
|
||||||
|
if (!this._imeInProgress) {
|
||||||
|
this._imeHold = false; //Firefox fires compisitionend before last input change
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldValue = this._lastKeyboardInput;
|
||||||
|
const newValue = e.target.value;
|
||||||
|
let diff_start = 0;
|
||||||
|
|
||||||
|
//find position where difference starts
|
||||||
|
for (let i = 0; i < Math.min(oldValue.length, newValue.length); i++) {
|
||||||
|
if (newValue.charAt(i) != oldValue.charAt(i)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
diff_start++;
|
||||||
|
}
|
||||||
|
|
||||||
|
//send backspaces if needed
|
||||||
|
for (let bs = oldValue.length - diff_start; bs > 0; bs--) {
|
||||||
|
this._sendKeyEvent(KeyTable.XK_BackSpace, "Backspace", true);
|
||||||
|
this._sendKeyEvent(KeyTable.XK_BackSpace, "Backspace", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
//send new keys
|
||||||
|
for (let i = diff_start; i < newValue.length; i++) {
|
||||||
|
this._sendKeyEvent(keysyms.lookup(newValue.charCodeAt(i)), 'Unidentified', true);
|
||||||
|
this._sendKeyEvent(keysyms.lookup(newValue.charCodeAt(i)), 'Unidentified', false);
|
||||||
|
}
|
||||||
|
this._lastKeyboardInput = newValue;
|
||||||
|
} else {
|
||||||
|
Log.Debug("Non-IME input change, sending new characters");
|
||||||
|
const newValue = e.target.value;
|
||||||
|
|
||||||
|
if (!this._lastKeyboardInput) {
|
||||||
|
this._keyboardInputReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldValue = this._lastKeyboardInput;
|
||||||
|
let newLen;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to check caret position since whitespace at the end
|
||||||
|
// will not be considered by value.length in some browsers
|
||||||
|
newLen = Math.max(e.target.selectionStart, newValue.length);
|
||||||
|
} catch (err) {
|
||||||
|
// selectionStart is undefined in Google Chrome
|
||||||
|
newLen = newValue.length;
|
||||||
|
}
|
||||||
|
const oldLen = oldValue.length;
|
||||||
|
|
||||||
|
let inputs = newLen - oldLen;
|
||||||
|
let backspaces = inputs < 0 ? -inputs : 0;
|
||||||
|
|
||||||
|
// Compare the old string with the new to account for
|
||||||
|
// text-corrections or other input that modify existing text
|
||||||
|
for (let i = 0; i < Math.min(oldLen, newLen); i++) {
|
||||||
|
if (newValue.charAt(i) != oldValue.charAt(i)) {
|
||||||
|
inputs = newLen - i;
|
||||||
|
backspaces = oldLen - i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the key events
|
||||||
|
for (let i = 0; i < backspaces; i++) {
|
||||||
|
this._sendKeyEvent(KeyTable.XK_BackSpace, "Backspace", true);
|
||||||
|
this._sendKeyEvent(KeyTable.XK_BackSpace, "Backspace", false);
|
||||||
|
}
|
||||||
|
for (let i = newLen - inputs; i < newLen; i++) {
|
||||||
|
this._sendKeyEvent(keysyms.lookup(newValue.charCodeAt(i)), 'Unidentified', true);
|
||||||
|
this._sendKeyEvent(keysyms.lookup(newValue.charCodeAt(i)), 'Unidentified', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control the text content length in the keyboardinput element
|
||||||
|
if (newLen > 2 * this._defaultKeyboardInputLen) {
|
||||||
|
this._keyboardInputReset();
|
||||||
|
} else if (newLen < 1) {
|
||||||
|
// There always have to be some text in the keyboardinput
|
||||||
|
// element with which backspace can interact.
|
||||||
|
this._keyboardInputReset();
|
||||||
|
// This sometimes causes the keyboard to disappear for a second
|
||||||
|
// but it is required for the android keyboard to recognize that
|
||||||
|
// text has been added to the field
|
||||||
|
e.target.blur();
|
||||||
|
// This has to be ran outside of the input handler in order to work
|
||||||
|
setTimeout(e.target.focus.bind(e.target), 0);
|
||||||
|
} else {
|
||||||
|
this._lastKeyboardInput = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_keyboardInputReset() {
|
||||||
|
this._touchInput.value = new Array(this._defaultKeyboardInputLen).join("_");
|
||||||
|
this._lastKeyboardInput = this._touchInput.value;
|
||||||
|
}
|
||||||
|
|
||||||
_handleKeyDown(e) {
|
_handleKeyDown(e) {
|
||||||
const code = this._getKeyCode(e);
|
const code = this._getKeyCode(e);
|
||||||
let keysym = KeyboardUtil.getKeysym(e);
|
let keysym = KeyboardUtil.getKeysym(e);
|
||||||
|
|
||||||
|
if (this._isIMEInteraction(e)) {
|
||||||
|
//skip event if IME related
|
||||||
|
Log.Debug("Skipping keydown, IME interaction, code: " + code + " keysym: " + keysym + " keycode: " + e.keyCode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Windows doesn't have a proper AltGr, but handles it using
|
// Windows doesn't have a proper AltGr, but handles it using
|
||||||
// fake Ctrl+Alt. However the remote end might not be Windows,
|
// fake Ctrl+Alt. However the remote end might not be Windows,
|
||||||
// so we need to merge those in to a single AltGr event. We
|
// so we need to merge those in to a single AltGr event. We
|
||||||
|
@ -220,10 +366,15 @@ export default class Keyboard {
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleKeyUp(e) {
|
_handleKeyUp(e) {
|
||||||
stopEvent(e);
|
|
||||||
|
|
||||||
const code = this._getKeyCode(e);
|
const code = this._getKeyCode(e);
|
||||||
|
|
||||||
|
if (this._isIMEInteraction(e)) {
|
||||||
|
//skip IME related events
|
||||||
|
Log.Debug("Skipping keyup, IME interaction, code: " + code + " keycode: " + e.keyCode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopEvent(e);
|
||||||
|
|
||||||
// We can't get a release in the middle of an AltGr sequence, so
|
// We can't get a release in the middle of an AltGr sequence, so
|
||||||
// abort that detection
|
// abort that detection
|
||||||
if (this._altGrArmed) {
|
if (this._altGrArmed) {
|
||||||
|
@ -271,13 +422,56 @@ export default class Keyboard {
|
||||||
Log.Debug("<< Keyboard.allKeysUp");
|
Log.Debug("<< Keyboard.allKeysUp");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_isIMEInteraction(e) {
|
||||||
|
//input must come from touchinput (textarea) and ime must be enabled
|
||||||
|
if (e.target != this._touchInput || !this._enableIME) { return false; }
|
||||||
|
|
||||||
|
//keyCode of 229 is IME composition
|
||||||
|
if (e.keyCode == 229) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//unfortunately, IME interactions can come through as events
|
||||||
|
//generally safe to ignore and let them come in as "input" events instead
|
||||||
|
//we can't do that with none character keys though
|
||||||
|
//Firefox does not seem to fire key events for IME interaction but Chrome does
|
||||||
|
//TODO: potentially skip this for Firefox browsers, needs more testing with different IME types
|
||||||
|
if (e.keyCode in imekeys) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// ===== PUBLIC METHODS =====
|
// ===== PUBLIC METHODS =====
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
if (this._enableIME) {
|
||||||
|
this._touchInput.focus();
|
||||||
|
} else {
|
||||||
|
this._screenInput.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blur() {
|
||||||
|
if (this._enableIME) {
|
||||||
|
this._touchInput.blur();
|
||||||
|
} else {
|
||||||
|
this._screenInput.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
grab() {
|
grab() {
|
||||||
//Log.Debug(">> Keyboard.grab");
|
//Log.Debug(">> Keyboard.grab");
|
||||||
|
|
||||||
this._target.addEventListener('keydown', this._eventHandlers.keydown);
|
this._screenInput.addEventListener('keydown', this._eventHandlers.keydown);
|
||||||
this._target.addEventListener('keyup', this._eventHandlers.keyup);
|
this._screenInput.addEventListener('keyup', this._eventHandlers.keyup);
|
||||||
|
|
||||||
|
this._touchInput.addEventListener('keydown', this._eventHandlers.keydown);
|
||||||
|
this._touchInput.addEventListener('keyup', this._eventHandlers.keyup);
|
||||||
|
this._touchInput.addEventListener('compositionstart', this._eventHandlers.compositionstart);
|
||||||
|
this._touchInput.addEventListener('compositionend', this._eventHandlers.compositionend);
|
||||||
|
this._touchInput.addEventListener('input', this._eventHandlers.input);
|
||||||
|
|
||||||
// Release (key up) if window loses focus
|
// Release (key up) if window loses focus
|
||||||
window.addEventListener('blur', this._eventHandlers.blur);
|
window.addEventListener('blur', this._eventHandlers.blur);
|
||||||
|
@ -288,8 +482,15 @@ export default class Keyboard {
|
||||||
ungrab() {
|
ungrab() {
|
||||||
//Log.Debug(">> Keyboard.ungrab");
|
//Log.Debug(">> Keyboard.ungrab");
|
||||||
|
|
||||||
this._target.removeEventListener('keydown', this._eventHandlers.keydown);
|
this._screenInput.removeEventListener('keydown', this._eventHandlers.keydown);
|
||||||
this._target.removeEventListener('keyup', this._eventHandlers.keyup);
|
this._screenInput.removeEventListener('keyup', this._eventHandlers.keyup);
|
||||||
|
|
||||||
|
this._touchInput.removeEventListener('keydown', this._eventHandlers.keydown);
|
||||||
|
this._touchInput.removeEventListener('keyup', this._eventHandlers.keyup);
|
||||||
|
this._touchInput.removeEventListener('compositionstart', this._eventHandlers.compositionstart);
|
||||||
|
this._touchInput.removeEventListener('compositionend', this._eventHandlers.compositionend);
|
||||||
|
this._touchInput.removeEventListener('input', this._eventHandlers.input);
|
||||||
|
|
||||||
window.removeEventListener('blur', this._eventHandlers.blur);
|
window.removeEventListener('blur', this._eventHandlers.blur);
|
||||||
|
|
||||||
// Release (key up) all keys that are in a down state
|
// Release (key up) all keys that are in a down state
|
||||||
|
|
12
core/rfb.js
12
core/rfb.js
|
@ -70,7 +70,7 @@ const extendedClipboardActionNotify = 1 << 27;
|
||||||
const extendedClipboardActionProvide = 1 << 28;
|
const extendedClipboardActionProvide = 1 << 28;
|
||||||
|
|
||||||
export default class RFB extends EventTargetMixin {
|
export default class RFB extends EventTargetMixin {
|
||||||
constructor(target, urlOrChannel, options) {
|
constructor(target, touchInput, urlOrChannel, options) {
|
||||||
if (!target) {
|
if (!target) {
|
||||||
throw new Error("Must specify target");
|
throw new Error("Must specify target");
|
||||||
}
|
}
|
||||||
|
@ -244,7 +244,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
}
|
}
|
||||||
this._display.onflush = this._onFlush.bind(this);
|
this._display.onflush = this._onFlush.bind(this);
|
||||||
|
|
||||||
this._keyboard = new Keyboard(this._canvas);
|
this._keyboard = new Keyboard(this._canvas, touchInput);
|
||||||
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
|
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
|
||||||
|
|
||||||
this._gestures = new GestureHandler();
|
this._gestures = new GestureHandler();
|
||||||
|
@ -333,6 +333,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
|
|
||||||
// ===== PROPERTIES =====
|
// ===== PROPERTIES =====
|
||||||
|
|
||||||
|
get keyboard() { return this._keyboard; }
|
||||||
|
|
||||||
get clipboardBinary() { return this._clipboardMode; }
|
get clipboardBinary() { return this._clipboardMode; }
|
||||||
set clipboardBinary(val) { this._clipboardMode = val; }
|
set clipboardBinary(val) { this._clipboardMode = val; }
|
||||||
|
|
||||||
|
@ -745,11 +747,13 @@ export default class RFB extends EventTargetMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
this._canvas.focus();
|
this._keyboard.focus();
|
||||||
|
//this._canvas.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
blur() {
|
blur() {
|
||||||
this._canvas.blur();
|
this._keyboard.blur();
|
||||||
|
//this._canvas.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
clipboardPasteFrom(text) {
|
clipboardPasteFrom(text) {
|
||||||
|
|
|
@ -97,6 +97,10 @@ export function isSafari() {
|
||||||
navigator.userAgent.indexOf('Chrome') === -1);
|
navigator.userAgent.indexOf('Chrome') === -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isChromiumBased() {
|
||||||
|
return (!!window.chrome);
|
||||||
|
}
|
||||||
|
|
||||||
export function isFirefox() {
|
export function isFirefox() {
|
||||||
return navigator && !!(/firefox/i).exec(navigator.userAgent);
|
return navigator && !!(/firefox/i).exec(navigator.userAgent);
|
||||||
}
|
}
|
||||||
|
|
8
vnc.html
8
vnc.html
|
@ -202,6 +202,10 @@
|
||||||
<label><input id="noVNC_setting_enable_webp" type="checkbox" /> Enable WebP Compression</label></li>
|
<label><input id="noVNC_setting_enable_webp" type="checkbox" /> Enable WebP Compression</label></li>
|
||||||
<li>
|
<li>
|
||||||
<label><input id="noVNC_setting_enable_perf_stats" type="checkbox" /> Enable Performance Stats</label></li>
|
<label><input id="noVNC_setting_enable_perf_stats" type="checkbox" /> Enable Performance Stats</label></li>
|
||||||
|
<li>
|
||||||
|
<label><input id="noVNC_setting_enable_ime" type="checkbox" /> IME Input Mode</label></li>
|
||||||
|
<li>
|
||||||
|
<label><input id="noVNC_setting_virtual_keyboard_visible" type="checkbox" /> Show Virtual Keyboard Control</label></li>
|
||||||
<li>
|
<li>
|
||||||
<label><input id="noVNC_setting_toggle_control_panel" type="checkbox" /> Toggle Control Panel via Keystrokes</label></li>
|
<label><input id="noVNC_setting_toggle_control_panel" type="checkbox" /> Toggle Control Panel via Keystrokes</label></li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -444,7 +448,7 @@
|
||||||
<source src="app/sounds/bell.mp3" type="audio/mpeg">
|
<source src="app/sounds/bell.mp3" type="audio/mpeg">
|
||||||
</audio>
|
</audio>
|
||||||
|
|
||||||
<div class="keyboard-controls">
|
<div id="noVNC_keyboard_control" class="keyboard-controls">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<div class="button ctrl"></div>
|
<div class="button ctrl"></div>
|
||||||
<div class="button alt"></div>
|
<div class="button alt"></div>
|
||||||
|
@ -454,7 +458,7 @@
|
||||||
<div class="button ctrlaltdel"></div>
|
<div class="button ctrlaltdel"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="button keyboard handle"></div>
|
<div id="noVNC_keyboard_control_handle" class="button keyboard handle"></div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in New Issue