mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-22 11:03:40 +00:00
34fa96723d
Implement keyboard mode options (Legacy, Map, Auto) and a reset keyboard feature in the remote viewer. Update UI to include buttons for selecting keyboard mode and resetting the keyboard state, enhancing user control over remote input settings. Update localization files for new strings across multiple languages.
252 lines
7.3 KiB
JavaScript
252 lines
7.3 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const vm = require('vm');
|
|
|
|
function loadBrowserScripts(relativePaths, extraGlobals) {
|
|
const sandbox = {
|
|
console,
|
|
window: {},
|
|
globalThis: {},
|
|
...extraGlobals,
|
|
};
|
|
if (!extraGlobals || extraGlobals.window === undefined) {
|
|
sandbox.window = sandbox;
|
|
}
|
|
sandbox.globalThis = sandbox.window;
|
|
for (const rel of relativePaths) {
|
|
const filename = path.join(__dirname, '..', rel);
|
|
vm.runInNewContext(fs.readFileSync(filename, 'utf8'), sandbox, { filename });
|
|
}
|
|
return sandbox;
|
|
}
|
|
|
|
function makeDocument() {
|
|
const documentListeners = {
|
|
keydown: [],
|
|
keyup: [],
|
|
pointerlockchange: [],
|
|
visibilitychange: [],
|
|
};
|
|
return {
|
|
activeElement: null,
|
|
visibilityState: 'visible',
|
|
addEventListener(type, fn) {
|
|
if (documentListeners[type]) documentListeners[type].push(fn);
|
|
},
|
|
removeEventListener(type, fn) {
|
|
if (!documentListeners[type]) return;
|
|
documentListeners[type] = documentListeners[type].filter((h) => h !== fn);
|
|
},
|
|
exitPointerLock() {},
|
|
_listeners: documentListeners,
|
|
_dispatch(type, event) {
|
|
for (const fn of documentListeners[type] || []) fn(event);
|
|
},
|
|
};
|
|
}
|
|
|
|
function makeInputHarness(extraGlobals) {
|
|
const document = makeDocument();
|
|
const windowListeners = { blur: [] };
|
|
const win = {
|
|
addEventListener(type, fn) {
|
|
if (windowListeners[type]) windowListeners[type].push(fn);
|
|
},
|
|
removeEventListener(type, fn) {
|
|
if (!windowListeners[type]) return;
|
|
windowListeners[type] = windowListeners[type].filter((h) => h !== fn);
|
|
},
|
|
_dispatch(type, event) {
|
|
for (const fn of windowListeners[type] || []) fn(event);
|
|
},
|
|
};
|
|
|
|
const sandbox = loadBrowserScripts([
|
|
'public/js/rdclient/keyboard-scancode.js',
|
|
'public/js/rdclient/input.js',
|
|
], {
|
|
document,
|
|
window: win,
|
|
RDProtocol: {},
|
|
...extraGlobals,
|
|
});
|
|
|
|
function makeInput(sendMessage) {
|
|
const canvas = {
|
|
tabIndex: -1,
|
|
listeners: {},
|
|
addEventListener(type, fn) { this.listeners[type] = fn; },
|
|
removeEventListener(type, fn) { if (this.listeners[type] === fn) delete this.listeners[type]; },
|
|
focus() { document.activeElement = this; },
|
|
};
|
|
const renderer = {
|
|
remoteWidth: 1920,
|
|
remoteHeight: 1080,
|
|
canvasToRemote: (x, y) => ({ x, y }),
|
|
};
|
|
return new sandbox.window.RDInput(canvas, renderer, sendMessage);
|
|
}
|
|
|
|
return { sandbox, document, win, makeInput };
|
|
}
|
|
|
|
describe('RDKeyboardScancode', () => {
|
|
let RDKeyboardScancode;
|
|
|
|
beforeAll(() => {
|
|
const sandbox = loadBrowserScripts(['public/js/rdclient/keyboard-scancode.js'], {});
|
|
RDKeyboardScancode = sandbox.RDKeyboardScancode;
|
|
});
|
|
|
|
it('maps Digit1 and KeyN to distinct Windows scancodes', () => {
|
|
expect(RDKeyboardScancode.codeToScancode('Digit1', 'Windows')).toBe(0x02);
|
|
expect(RDKeyboardScancode.codeToScancode('KeyN', 'Windows')).toBe(0x31);
|
|
});
|
|
});
|
|
|
|
describe('RDInput keyboard release sync', () => {
|
|
let makeInput;
|
|
let RDInput;
|
|
|
|
beforeAll(() => {
|
|
const harness = makeInputHarness({});
|
|
makeInput = harness.makeInput;
|
|
RDInput = harness.sandbox.window.RDInput;
|
|
});
|
|
|
|
it('sends keyup for held keys when stop() is called', () => {
|
|
const sent = [];
|
|
const input = makeInput((msg) => sent.push(msg));
|
|
input.start();
|
|
|
|
input._handleKeyDown({
|
|
code: 'ShiftLeft',
|
|
key: 'Shift',
|
|
repeat: false,
|
|
preventDefault() {},
|
|
stopPropagation() {},
|
|
ctrlKey: false,
|
|
altKey: false,
|
|
metaKey: false,
|
|
shiftKey: true,
|
|
});
|
|
input._handleKeyDown({
|
|
code: 'KeyA',
|
|
key: 'a',
|
|
repeat: false,
|
|
preventDefault() {},
|
|
stopPropagation() {},
|
|
ctrlKey: false,
|
|
altKey: false,
|
|
metaKey: false,
|
|
shiftKey: true,
|
|
});
|
|
|
|
sent.length = 0;
|
|
input.stop();
|
|
|
|
const keyups = sent.filter((m) => m.keyEvent && m.keyEvent.down === false);
|
|
expect(keyups.length).toBeGreaterThanOrEqual(2);
|
|
expect(input.pressedKeys.size).toBe(0);
|
|
});
|
|
|
|
it('releases keys on window blur while capture is active', () => {
|
|
const sent = [];
|
|
const harness = makeInputHarness({});
|
|
const input = harness.makeInput((msg) => sent.push(msg));
|
|
input.start();
|
|
input._handleKeyDown({
|
|
code: 'ControlLeft',
|
|
key: 'Control',
|
|
repeat: false,
|
|
preventDefault() {},
|
|
stopPropagation() {},
|
|
ctrlKey: true,
|
|
altKey: false,
|
|
metaKey: false,
|
|
shiftKey: false,
|
|
});
|
|
|
|
sent.length = 0;
|
|
harness.win._dispatch('blur', new Event('blur'));
|
|
|
|
expect(sent.some((m) => m.keyEvent && m.keyEvent.down === false)).toBe(true);
|
|
expect(input.pressedKeys.size).toBe(0);
|
|
});
|
|
|
|
it('resetKeyboard() releases modifier keys on the remote', () => {
|
|
const sent = [];
|
|
const input = makeInput((msg) => sent.push(msg));
|
|
input.start();
|
|
input.setPeerPlatform('Windows');
|
|
input.setKeyboardMode('Legacy');
|
|
|
|
sent.length = 0;
|
|
input.resetKeyboard();
|
|
|
|
const releases = sent.filter((m) =>
|
|
m.keyEvent &&
|
|
m.keyEvent.down === false &&
|
|
m.keyEvent.controlKey
|
|
);
|
|
expect(releases.length).toBeGreaterThanOrEqual(4);
|
|
});
|
|
});
|
|
|
|
describe('RDInput KeyboardMode.Map', () => {
|
|
let makeInput;
|
|
|
|
beforeAll(() => {
|
|
({ makeInput } = makeInputHarness({}));
|
|
});
|
|
|
|
it('Auto mode sends Map scancode chr for Windows peers', () => {
|
|
const sent = [];
|
|
const input = makeInput((msg) => sent.push(msg));
|
|
input.setPeerPlatform('Windows');
|
|
input.setKeyboardMode('Auto');
|
|
input.start();
|
|
|
|
input._handleKeyDown({
|
|
code: 'Digit1',
|
|
key: '1',
|
|
repeat: false,
|
|
preventDefault() {},
|
|
stopPropagation() {},
|
|
ctrlKey: false,
|
|
altKey: false,
|
|
metaKey: false,
|
|
shiftKey: false,
|
|
});
|
|
|
|
expect(sent[0].keyEvent.mode).toBe('Map');
|
|
expect(sent[0].keyEvent.chr).toBe(0x02);
|
|
expect(sent[0].keyEvent.controlKey).toBeUndefined();
|
|
});
|
|
|
|
it('Legacy mode sends character chr for printable keys', () => {
|
|
const sent = [];
|
|
const input = makeInput((msg) => sent.push(msg));
|
|
input.setPeerPlatform('Linux');
|
|
input.setKeyboardMode('Legacy');
|
|
input.start();
|
|
|
|
input._handleKeyDown({
|
|
code: 'Digit1',
|
|
key: '1',
|
|
repeat: false,
|
|
preventDefault() {},
|
|
stopPropagation() {},
|
|
ctrlKey: false,
|
|
altKey: false,
|
|
metaKey: false,
|
|
shiftKey: false,
|
|
});
|
|
|
|
expect(sent[0].keyEvent.mode).toBe('Legacy');
|
|
expect(sent[0].keyEvent.chr).toBe(49);
|
|
});
|
|
});
|