@difizen/mana-core
Version:
491 lines (462 loc) • 14.1 kB
text/typescript
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable no-param-reassign */
/* eslint-disable no-restricted-syntax */
/* eslint-disable class-methods-use-this */
/* eslint-disable no-plusplus */
import type { Event } from '@difizen/mana-common';
import { Emitter, isWindows } from '@difizen/mana-common';
import { inject, optional, singleton } from '@difizen/mana-syringe';
import type { IWindowsKeyMapping } from 'native-keymap';
import type { NativeKeyboardLayout } from './keyboard-protocol';
import {
KeyboardLayoutProvider,
KeyboardLayoutChangeNotifier,
KeyValidator,
} from './keyboard-protocol';
import { KeyCode, Key } from './keys';
export type KeyboardLayout = {
/**
* Mapping of standard US keyboard keys to the actual key codes to use.
* See `KeyboardLayoutService.getCharacterIndex` for the index computation.
*/
readonly key2KeyCode: KeyCode[];
/**
* Mapping of KeyboardEvent codes to the characters shown on the user's keyboard
* for the respective keys.
*/
readonly code2Character: Record<string, string>;
};
@singleton()
export class KeyboardLayoutService {
protected readonly layoutProvider: KeyboardLayoutProvider;
protected readonly layoutChangeNotifier: KeyboardLayoutChangeNotifier;
protected readonly keyValidator?: KeyValidator | undefined;
constructor(
@inject(KeyboardLayoutProvider) layoutProvider: KeyboardLayoutProvider,
@inject(KeyboardLayoutChangeNotifier)
layoutChangeNotifier: KeyboardLayoutChangeNotifier,
@inject(KeyValidator) @optional() keyValidator?: KeyValidator,
) {
this.layoutProvider = layoutProvider;
this.layoutChangeNotifier = layoutChangeNotifier;
this.keyValidator = keyValidator;
}
private currentLayout?: KeyboardLayout;
protected updateLayout(newLayout: NativeKeyboardLayout): KeyboardLayout {
const transformed = this.transformNativeLayout(newLayout);
this.currentLayout = transformed;
this.keyboardLayoutChanged.fire(transformed);
return transformed;
}
protected keyboardLayoutChanged = new Emitter<KeyboardLayout>();
get onKeyboardLayoutChanged(): Event<KeyboardLayout> {
return this.keyboardLayoutChanged.event;
}
async initialize(): Promise<void> {
this.layoutChangeNotifier.onDidChangeNativeLayout(
(newLayout: NativeKeyboardLayout) => this.updateLayout(newLayout),
);
const initialLayout = await this.layoutProvider.getNativeLayout();
this.updateLayout(initialLayout);
}
/**
* Resolve a KeyCode of a keybinding using the current keyboard layout.
* If no keyboard layout has been detected or the layout does not contain the
* key used in the KeyCode, the KeyCode is returned unchanged.
*/
resolveKeyCode(inCode: KeyCode): KeyCode {
const layout = this.currentLayout;
if (layout && inCode.key) {
for (let shift = 0; shift <= 1; shift++) {
const index = this.getCharacterIndex(inCode.key, !!shift);
const mappedCode = layout.key2KeyCode[index];
if (mappedCode) {
const transformed = this.transformKeyCode(inCode, mappedCode, !!shift);
if (transformed) {
return transformed;
}
}
}
}
return inCode;
}
/**
* Return the character shown on the user's keyboard for the given key.
* Use this to determine UI representations of keybindings.
*/
getKeyboardCharacter(key: Key): string {
const layout = this.currentLayout;
if (layout) {
const value = layout.code2Character[key.code];
if (value && value.replace(/[\n\r\t]/g, '')) {
return value;
}
}
return key.easyString;
}
/**
* Called when a KeyboardEvent is processed by the KeybindingRegistry.
* The KeyValidator may trigger a keyboard layout change.
*/
validateKeyCode(keyCode: KeyCode): void {
if (this.keyValidator && keyCode.key && keyCode.character) {
this.keyValidator.validateKey({
code: keyCode.key.code,
character: keyCode.character,
shiftKey: keyCode.shift,
ctrlKey: keyCode.ctrl,
altKey: keyCode.alt,
});
}
}
protected transformKeyCode(
inCode: KeyCode,
mappedCode: KeyCode,
keyNeedsShift: boolean,
): KeyCode | undefined {
if (!inCode.shift && keyNeedsShift) {
return undefined;
}
if (
mappedCode.alt &&
(inCode.alt || inCode.ctrl || (inCode.shift && !keyNeedsShift))
) {
return undefined;
}
return new KeyCode({
key: mappedCode.key,
meta: inCode.meta,
ctrl: inCode.ctrl || mappedCode.alt,
shift: (inCode.shift && !keyNeedsShift) || mappedCode.shift,
alt: inCode.alt || mappedCode.alt,
});
}
protected transformNativeLayout(nativeLayout: NativeKeyboardLayout): KeyboardLayout {
const key2KeyCode: KeyCode[] = new Array(2 * (Key.MAX_KEY_CODE + 1));
const code2Character: Record<string, string> = {};
const { mapping } = nativeLayout;
for (const code in mapping) {
if (Object.prototype.hasOwnProperty.call(mapping, code)) {
const keyMapping = mapping[code];
const mappedKey = Key.getKey(code);
if (mappedKey && this.shouldIncludeKey(code)) {
if (isWindows) {
this.addWindowsKeyMapping(
key2KeyCode,
mappedKey,
(keyMapping as IWindowsKeyMapping).vkey,
keyMapping.value,
);
} else {
if (keyMapping.value) {
this.addKeyMapping(
key2KeyCode,
mappedKey,
keyMapping.value,
false,
false,
);
}
if (keyMapping.withShift) {
this.addKeyMapping(
key2KeyCode,
mappedKey,
keyMapping.withShift,
true,
false,
);
}
if (keyMapping.withAltGr) {
this.addKeyMapping(
key2KeyCode,
mappedKey,
keyMapping.withAltGr,
false,
true,
);
}
if (keyMapping.withShiftAltGr) {
this.addKeyMapping(
key2KeyCode,
mappedKey,
keyMapping.withShiftAltGr,
true,
true,
);
}
}
}
if (keyMapping.value) {
code2Character[code] = keyMapping.value;
}
}
}
return { key2KeyCode, code2Character };
}
protected shouldIncludeKey(code: string): boolean {
// Exclude all numpad keys because they produce values that are already found elsewhere on the keyboard.
// This can cause problems, e.g. if `Numpad3` maps to `PageDown` then commands bound to `PageDown` would
// be resolved to `Digit3` (`Numpad3` is associated with `Key.DIGIT3`), effectively blocking the user
// from typing `3` in an editor.
return !code.startsWith('Numpad');
}
private addKeyMapping(
key2KeyCode: KeyCode[],
mappedKey: Key,
value: string,
shift: boolean,
alt: boolean,
): void {
const key = VALUE_TO_KEY[value];
if (key) {
const index = this.getCharacterIndex(key.key, key.shift);
if (key2KeyCode[index] === undefined) {
key2KeyCode[index] = new KeyCode({
key: mappedKey,
shift,
alt,
character: value,
});
}
}
}
private addWindowsKeyMapping(
key2KeyCode: KeyCode[],
mappedKey: Key,
vkey: string,
value: string,
): void {
const key = VKEY_TO_KEY[vkey];
if (key) {
const index = this.getCharacterIndex(key);
if (key2KeyCode[index] === undefined) {
key2KeyCode[index] = new KeyCode({
key: mappedKey,
character: value,
});
}
}
}
protected getCharacterIndex(key: Key, shift?: boolean): number {
if (shift) {
return Key.MAX_KEY_CODE + key.keyCode + 1;
}
return key.keyCode;
}
}
/**
* Mapping of character values to the corresponding keys on a standard US keyboard layout.
*/
const VALUE_TO_KEY: Record<string, { key: Key; shift?: boolean }> = {
'`': { key: Key.BACKQUOTE },
'~': { key: Key.BACKQUOTE, shift: true },
'1': { key: Key.DIGIT1 },
'!': { key: Key.DIGIT1, shift: true },
'2': { key: Key.DIGIT2 },
'@': { key: Key.DIGIT2, shift: true },
'3': { key: Key.DIGIT3 },
'#': { key: Key.DIGIT3, shift: true },
'4': { key: Key.DIGIT4 },
$: { key: Key.DIGIT4, shift: true },
'5': { key: Key.DIGIT5 },
'%': { key: Key.DIGIT5, shift: true },
'6': { key: Key.DIGIT6 },
'^': { key: Key.DIGIT6, shift: true },
'7': { key: Key.DIGIT7 },
'&': { key: Key.DIGIT7, shift: true },
'8': { key: Key.DIGIT8 },
'*': { key: Key.DIGIT8, shift: true },
'9': { key: Key.DIGIT9 },
'(': { key: Key.DIGIT9, shift: true },
'0': { key: Key.DIGIT0 },
')': { key: Key.DIGIT0, shift: true },
'-': { key: Key.MINUS },
_: { key: Key.MINUS, shift: true },
'=': { key: Key.EQUAL },
'+': { key: Key.EQUAL, shift: true },
a: { key: Key.KEY_A },
A: { key: Key.KEY_A, shift: true },
b: { key: Key.KEY_B },
B: { key: Key.KEY_B, shift: true },
c: { key: Key.KEY_C },
C: { key: Key.KEY_C, shift: true },
d: { key: Key.KEY_D },
D: { key: Key.KEY_D, shift: true },
e: { key: Key.KEY_E },
E: { key: Key.KEY_E, shift: true },
f: { key: Key.KEY_F },
F: { key: Key.KEY_F, shift: true },
g: { key: Key.KEY_G },
G: { key: Key.KEY_G, shift: true },
h: { key: Key.KEY_H },
H: { key: Key.KEY_H, shift: true },
i: { key: Key.KEY_I },
I: { key: Key.KEY_I, shift: true },
j: { key: Key.KEY_J },
J: { key: Key.KEY_J, shift: true },
k: { key: Key.KEY_K },
K: { key: Key.KEY_K, shift: true },
l: { key: Key.KEY_L },
L: { key: Key.KEY_L, shift: true },
m: { key: Key.KEY_M },
M: { key: Key.KEY_M, shift: true },
n: { key: Key.KEY_N },
N: { key: Key.KEY_N, shift: true },
o: { key: Key.KEY_O },
O: { key: Key.KEY_O, shift: true },
p: { key: Key.KEY_P },
P: { key: Key.KEY_P, shift: true },
q: { key: Key.KEY_Q },
Q: { key: Key.KEY_Q, shift: true },
r: { key: Key.KEY_R },
R: { key: Key.KEY_R, shift: true },
s: { key: Key.KEY_S },
S: { key: Key.KEY_S, shift: true },
t: { key: Key.KEY_T },
T: { key: Key.KEY_T, shift: true },
u: { key: Key.KEY_U },
U: { key: Key.KEY_U, shift: true },
v: { key: Key.KEY_V },
V: { key: Key.KEY_V, shift: true },
w: { key: Key.KEY_W },
W: { key: Key.KEY_W, shift: true },
x: { key: Key.KEY_X },
X: { key: Key.KEY_X, shift: true },
y: { key: Key.KEY_Y },
Y: { key: Key.KEY_Y, shift: true },
z: { key: Key.KEY_Z },
Z: { key: Key.KEY_Z, shift: true },
'[': { key: Key.BRACKET_LEFT },
'{': { key: Key.BRACKET_LEFT, shift: true },
']': { key: Key.BRACKET_RIGHT },
'}': { key: Key.BRACKET_RIGHT, shift: true },
';': { key: Key.SEMICOLON },
':': { key: Key.SEMICOLON, shift: true },
"'": { key: Key.QUOTE },
'"': { key: Key.QUOTE, shift: true },
',': { key: Key.COMMA },
'<': { key: Key.COMMA, shift: true },
'.': { key: Key.PERIOD },
'>': { key: Key.PERIOD, shift: true },
'/': { key: Key.SLASH },
'?': { key: Key.SLASH, shift: true },
'\\': { key: Key.BACKSLASH },
'|': { key: Key.BACKSLASH, shift: true },
'\t': { key: Key.TAB },
'\r': { key: Key.ENTER },
'\n': { key: Key.ENTER },
' ': { key: Key.SPACE },
};
/**
* Mapping of Windows Virtual Keys to the corresponding keys on a standard US keyboard layout.
*/
const VKEY_TO_KEY: Record<string, Key> = {
VK_SHIFT: Key.SHIFT_LEFT,
VK_LSHIFT: Key.SHIFT_LEFT,
VK_RSHIFT: Key.SHIFT_RIGHT,
VK_CONTROL: Key.CONTROL_LEFT,
VK_LCONTROL: Key.CONTROL_LEFT,
VK_RCONTROL: Key.CONTROL_RIGHT,
VK_MENU: Key.ALT_LEFT,
VK_COMMAND: Key.OS_LEFT,
VK_LWIN: Key.OS_LEFT,
VK_RWIN: Key.OS_RIGHT,
VK_0: Key.DIGIT0,
VK_1: Key.DIGIT1,
VK_2: Key.DIGIT2,
VK_3: Key.DIGIT3,
VK_4: Key.DIGIT4,
VK_5: Key.DIGIT5,
VK_6: Key.DIGIT6,
VK_7: Key.DIGIT7,
VK_8: Key.DIGIT8,
VK_9: Key.DIGIT9,
VK_A: Key.KEY_A,
VK_B: Key.KEY_B,
VK_C: Key.KEY_C,
VK_D: Key.KEY_D,
VK_E: Key.KEY_E,
VK_F: Key.KEY_F,
VK_G: Key.KEY_G,
VK_H: Key.KEY_H,
VK_I: Key.KEY_I,
VK_J: Key.KEY_J,
VK_K: Key.KEY_K,
VK_L: Key.KEY_L,
VK_M: Key.KEY_M,
VK_N: Key.KEY_N,
VK_O: Key.KEY_O,
VK_P: Key.KEY_P,
VK_Q: Key.KEY_Q,
VK_R: Key.KEY_R,
VK_S: Key.KEY_S,
VK_T: Key.KEY_T,
VK_U: Key.KEY_U,
VK_V: Key.KEY_V,
VK_W: Key.KEY_W,
VK_X: Key.KEY_X,
VK_Y: Key.KEY_Y,
VK_Z: Key.KEY_Z,
VK_OEM_1: Key.SEMICOLON,
VK_OEM_2: Key.SLASH,
VK_OEM_3: Key.BACKQUOTE,
VK_OEM_4: Key.BRACKET_LEFT,
VK_OEM_5: Key.BACKSLASH,
VK_OEM_6: Key.BRACKET_RIGHT,
VK_OEM_7: Key.QUOTE,
VK_OEM_PLUS: Key.EQUAL,
VK_OEM_COMMA: Key.COMMA,
VK_OEM_MINUS: Key.MINUS,
VK_OEM_PERIOD: Key.PERIOD,
VK_F1: Key.F1,
VK_F2: Key.F2,
VK_F3: Key.F3,
VK_F4: Key.F4,
VK_F5: Key.F5,
VK_F6: Key.F6,
VK_F7: Key.F7,
VK_F8: Key.F8,
VK_F9: Key.F9,
VK_F10: Key.F10,
VK_F11: Key.F11,
VK_F12: Key.F12,
VK_F13: Key.F13,
VK_F14: Key.F14,
VK_F15: Key.F15,
VK_F16: Key.F16,
VK_F17: Key.F17,
VK_F18: Key.F18,
VK_F19: Key.F19,
VK_BACK: Key.BACKSPACE,
VK_TAB: Key.TAB,
VK_RETURN: Key.ENTER,
VK_CAPITAL: Key.CAPS_LOCK,
VK_ESCAPE: Key.ESCAPE,
VK_SPACE: Key.SPACE,
VK_PRIOR: Key.PAGE_UP,
VK_NEXT: Key.PAGE_DOWN,
VK_END: Key.END,
VK_HOME: Key.HOME,
VK_INSERT: Key.INSERT,
VK_DELETE: Key.DELETE,
VK_LEFT: Key.ARROW_LEFT,
VK_UP: Key.ARROW_UP,
VK_RIGHT: Key.ARROW_RIGHT,
VK_DOWN: Key.ARROW_DOWN,
VK_NUMLOCK: Key.NUM_LOCK,
VK_NUMPAD0: Key.DIGIT0,
VK_NUMPAD1: Key.DIGIT1,
VK_NUMPAD2: Key.DIGIT2,
VK_NUMPAD3: Key.DIGIT3,
VK_NUMPAD4: Key.DIGIT4,
VK_NUMPAD5: Key.DIGIT5,
VK_NUMPAD6: Key.DIGIT6,
VK_NUMPAD7: Key.DIGIT7,
VK_NUMPAD8: Key.DIGIT8,
VK_NUMPAD9: Key.DIGIT9,
VK_MULTIPLY: Key.MULTIPLY,
VK_ADD: Key.ADD,
VK_SUBTRACT: Key.SUBTRACT,
VK_DECIMAL: Key.DECIMAL,
VK_DIVIDE: Key.DIVIDE,
};