Path: blob/main/src/vs/workbench/services/keybinding/common/windowsKeyboardMapper.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CharCode } from '../../../../base/common/charCode.js';6import { KeyCode, KeyCodeUtils, IMMUTABLE_CODE_TO_KEY_CODE, ScanCode, ScanCodeUtils, NATIVE_WINDOWS_KEY_CODE_TO_KEY_CODE } from '../../../../base/common/keyCodes.js';7import { ResolvedKeybinding, KeyCodeChord, SingleModifierChord, ScanCodeChord, Keybinding, Chord } from '../../../../base/common/keybindings.js';8import { UILabelProvider } from '../../../../base/common/keybindingLabels.js';9import { OperatingSystem } from '../../../../base/common/platform.js';10import { IKeyboardEvent } from '../../../../platform/keybinding/common/keybinding.js';11import { IKeyboardMapper } from '../../../../platform/keyboardLayout/common/keyboardMapper.js';12import { BaseResolvedKeybinding } from '../../../../platform/keybinding/common/baseResolvedKeybinding.js';13import { toEmptyArrayIfContainsNull } from '../../../../platform/keybinding/common/resolvedKeybindingItem.js';14import { IWindowsKeyboardMapping } from '../../../../platform/keyboardLayout/common/keyboardLayout.js';1516const LOG = false;17function log(str: string): void {18if (LOG) {19console.info(str);20}21}222324export interface IScanCodeMapping {25scanCode: ScanCode;26keyCode: KeyCode;27value: string;28withShift: string;29withAltGr: string;30withShiftAltGr: string;31}3233export class WindowsNativeResolvedKeybinding extends BaseResolvedKeybinding<KeyCodeChord> {3435private readonly _mapper: WindowsKeyboardMapper;3637constructor(mapper: WindowsKeyboardMapper, chords: KeyCodeChord[]) {38super(OperatingSystem.Windows, chords);39this._mapper = mapper;40}4142protected _getLabel(chord: KeyCodeChord): string | null {43if (chord.isDuplicateModifierCase()) {44return '';45}46return this._mapper.getUILabelForKeyCode(chord.keyCode);47}4849private _getUSLabelForKeybinding(chord: KeyCodeChord): string | null {50if (chord.isDuplicateModifierCase()) {51return '';52}53return KeyCodeUtils.toString(chord.keyCode);54}5556public getUSLabel(): string | null {57return UILabelProvider.toLabel(this._os, this._chords, (keybinding) => this._getUSLabelForKeybinding(keybinding));58}5960protected _getAriaLabel(chord: KeyCodeChord): string | null {61if (chord.isDuplicateModifierCase()) {62return '';63}64return this._mapper.getAriaLabelForKeyCode(chord.keyCode);65}6667protected _getElectronAccelerator(chord: KeyCodeChord): string | null {68return this._mapper.getElectronAcceleratorForKeyBinding(chord);69}7071protected _getUserSettingsLabel(chord: KeyCodeChord): string | null {72if (chord.isDuplicateModifierCase()) {73return '';74}75const result = this._mapper.getUserSettingsLabelForKeyCode(chord.keyCode);76return (result ? result.toLowerCase() : result);77}7879protected _isWYSIWYG(chord: KeyCodeChord): boolean {80return this.__isWYSIWYG(chord.keyCode);81}8283private __isWYSIWYG(keyCode: KeyCode): boolean {84if (85keyCode === KeyCode.LeftArrow86|| keyCode === KeyCode.UpArrow87|| keyCode === KeyCode.RightArrow88|| keyCode === KeyCode.DownArrow89) {90return true;91}92const ariaLabel = this._mapper.getAriaLabelForKeyCode(keyCode);93const userSettingsLabel = this._mapper.getUserSettingsLabelForKeyCode(keyCode);94return (ariaLabel === userSettingsLabel);95}9697protected _getChordDispatch(chord: KeyCodeChord): string | null {98if (chord.isModifierKey()) {99return null;100}101let result = '';102103if (chord.ctrlKey) {104result += 'ctrl+';105}106if (chord.shiftKey) {107result += 'shift+';108}109if (chord.altKey) {110result += 'alt+';111}112if (chord.metaKey) {113result += 'meta+';114}115result += KeyCodeUtils.toString(chord.keyCode);116117return result;118}119120protected _getSingleModifierChordDispatch(chord: KeyCodeChord): SingleModifierChord | null {121if (chord.keyCode === KeyCode.Ctrl && !chord.shiftKey && !chord.altKey && !chord.metaKey) {122return 'ctrl';123}124if (chord.keyCode === KeyCode.Shift && !chord.ctrlKey && !chord.altKey && !chord.metaKey) {125return 'shift';126}127if (chord.keyCode === KeyCode.Alt && !chord.ctrlKey && !chord.shiftKey && !chord.metaKey) {128return 'alt';129}130if (chord.keyCode === KeyCode.Meta && !chord.ctrlKey && !chord.shiftKey && !chord.altKey) {131return 'meta';132}133return null;134}135136private static getProducedCharCode(chord: ScanCodeChord, mapping: IScanCodeMapping): string | null {137if (!mapping) {138return null;139}140if (chord.ctrlKey && chord.shiftKey && chord.altKey) {141return mapping.withShiftAltGr;142}143if (chord.ctrlKey && chord.altKey) {144return mapping.withAltGr;145}146if (chord.shiftKey) {147return mapping.withShift;148}149return mapping.value;150}151152public static getProducedChar(chord: ScanCodeChord, mapping: IScanCodeMapping): string {153const char = this.getProducedCharCode(chord, mapping);154if (char === null || char.length === 0) {155return ' --- ';156}157return ' ' + char + ' ';158}159}160161export class WindowsKeyboardMapper implements IKeyboardMapper {162163private readonly _codeInfo: IScanCodeMapping[];164private readonly _scanCodeToKeyCode: KeyCode[];165private readonly _keyCodeToLabel: Array<string | null> = [];166private readonly _keyCodeExists: boolean[];167168constructor(169private readonly _isUSStandard: boolean,170rawMappings: IWindowsKeyboardMapping,171private readonly _mapAltGrToCtrlAlt: boolean172) {173this._scanCodeToKeyCode = [];174this._keyCodeToLabel = [];175this._keyCodeExists = [];176this._keyCodeToLabel[KeyCode.Unknown] = KeyCodeUtils.toString(KeyCode.Unknown);177178for (let scanCode = ScanCode.None; scanCode < ScanCode.MAX_VALUE; scanCode++) {179const immutableKeyCode = IMMUTABLE_CODE_TO_KEY_CODE[scanCode];180if (immutableKeyCode !== KeyCode.DependsOnKbLayout) {181this._scanCodeToKeyCode[scanCode] = immutableKeyCode;182this._keyCodeToLabel[immutableKeyCode] = KeyCodeUtils.toString(immutableKeyCode);183this._keyCodeExists[immutableKeyCode] = true;184}185}186187const producesLetter: boolean[] = [];188let producesLetters = false;189190this._codeInfo = [];191for (const strCode in rawMappings) {192if (rawMappings.hasOwnProperty(strCode)) {193const scanCode = ScanCodeUtils.toEnum(strCode);194if (scanCode === ScanCode.None) {195log(`Unknown scanCode ${strCode} in mapping.`);196continue;197}198const rawMapping = rawMappings[strCode];199200const immutableKeyCode = IMMUTABLE_CODE_TO_KEY_CODE[scanCode];201if (immutableKeyCode !== KeyCode.DependsOnKbLayout) {202const keyCode = NATIVE_WINDOWS_KEY_CODE_TO_KEY_CODE[rawMapping.vkey] || KeyCode.Unknown;203if (keyCode === KeyCode.Unknown || immutableKeyCode === keyCode) {204continue;205}206if (scanCode !== ScanCode.NumpadComma) {207// Looks like ScanCode.NumpadComma doesn't always map to KeyCode.NUMPAD_SEPARATOR208// e.g. on POR - PTB209continue;210}211}212213const value = rawMapping.value;214const withShift = rawMapping.withShift;215const withAltGr = rawMapping.withAltGr;216const withShiftAltGr = rawMapping.withShiftAltGr;217const keyCode = NATIVE_WINDOWS_KEY_CODE_TO_KEY_CODE[rawMapping.vkey] || KeyCode.Unknown;218219const mapping: IScanCodeMapping = {220scanCode: scanCode,221keyCode: keyCode,222value: value,223withShift: withShift,224withAltGr: withAltGr,225withShiftAltGr: withShiftAltGr,226};227this._codeInfo[scanCode] = mapping;228this._scanCodeToKeyCode[scanCode] = keyCode;229230if (keyCode === KeyCode.Unknown) {231continue;232}233this._keyCodeExists[keyCode] = true;234235if (value.length === 0) {236// This key does not produce strings237this._keyCodeToLabel[keyCode] = null;238}239240else if (value.length > 1) {241// This key produces a letter representable with multiple UTF-16 code units.242this._keyCodeToLabel[keyCode] = value;243}244245else {246const charCode = value.charCodeAt(0);247248if (charCode >= CharCode.a && charCode <= CharCode.z) {249const upperCaseValue = CharCode.A + (charCode - CharCode.a);250producesLetter[upperCaseValue] = true;251producesLetters = true;252this._keyCodeToLabel[keyCode] = String.fromCharCode(CharCode.A + (charCode - CharCode.a));253}254255else if (charCode >= CharCode.A && charCode <= CharCode.Z) {256producesLetter[charCode] = true;257producesLetters = true;258this._keyCodeToLabel[keyCode] = value;259}260261else {262this._keyCodeToLabel[keyCode] = value;263}264}265}266}267268// Handle keyboard layouts where latin characters are not produced e.g. Cyrillic269const _registerLetterIfMissing = (charCode: CharCode, keyCode: KeyCode): void => {270if (!producesLetter[charCode]) {271this._keyCodeToLabel[keyCode] = String.fromCharCode(charCode);272}273};274_registerLetterIfMissing(CharCode.A, KeyCode.KeyA);275_registerLetterIfMissing(CharCode.B, KeyCode.KeyB);276_registerLetterIfMissing(CharCode.C, KeyCode.KeyC);277_registerLetterIfMissing(CharCode.D, KeyCode.KeyD);278_registerLetterIfMissing(CharCode.E, KeyCode.KeyE);279_registerLetterIfMissing(CharCode.F, KeyCode.KeyF);280_registerLetterIfMissing(CharCode.G, KeyCode.KeyG);281_registerLetterIfMissing(CharCode.H, KeyCode.KeyH);282_registerLetterIfMissing(CharCode.I, KeyCode.KeyI);283_registerLetterIfMissing(CharCode.J, KeyCode.KeyJ);284_registerLetterIfMissing(CharCode.K, KeyCode.KeyK);285_registerLetterIfMissing(CharCode.L, KeyCode.KeyL);286_registerLetterIfMissing(CharCode.M, KeyCode.KeyM);287_registerLetterIfMissing(CharCode.N, KeyCode.KeyN);288_registerLetterIfMissing(CharCode.O, KeyCode.KeyO);289_registerLetterIfMissing(CharCode.P, KeyCode.KeyP);290_registerLetterIfMissing(CharCode.Q, KeyCode.KeyQ);291_registerLetterIfMissing(CharCode.R, KeyCode.KeyR);292_registerLetterIfMissing(CharCode.S, KeyCode.KeyS);293_registerLetterIfMissing(CharCode.T, KeyCode.KeyT);294_registerLetterIfMissing(CharCode.U, KeyCode.KeyU);295_registerLetterIfMissing(CharCode.V, KeyCode.KeyV);296_registerLetterIfMissing(CharCode.W, KeyCode.KeyW);297_registerLetterIfMissing(CharCode.X, KeyCode.KeyX);298_registerLetterIfMissing(CharCode.Y, KeyCode.KeyY);299_registerLetterIfMissing(CharCode.Z, KeyCode.KeyZ);300301if (!producesLetters) {302// Since this keyboard layout produces no latin letters at all, most of the UI will use the303// US kb layout equivalent for UI labels, so also try to render other keys with the US labels304// for consistency...305const _registerLabel = (keyCode: KeyCode, charCode: CharCode): void => {306// const existingLabel = this._keyCodeToLabel[keyCode];307// const existingCharCode = (existingLabel ? existingLabel.charCodeAt(0) : CharCode.Null);308// if (existingCharCode < 32 || existingCharCode > 126) {309this._keyCodeToLabel[keyCode] = String.fromCharCode(charCode);310// }311};312_registerLabel(KeyCode.Semicolon, CharCode.Semicolon);313_registerLabel(KeyCode.Equal, CharCode.Equals);314_registerLabel(KeyCode.Comma, CharCode.Comma);315_registerLabel(KeyCode.Minus, CharCode.Dash);316_registerLabel(KeyCode.Period, CharCode.Period);317_registerLabel(KeyCode.Slash, CharCode.Slash);318_registerLabel(KeyCode.Backquote, CharCode.BackTick);319_registerLabel(KeyCode.BracketLeft, CharCode.OpenSquareBracket);320_registerLabel(KeyCode.Backslash, CharCode.Backslash);321_registerLabel(KeyCode.BracketRight, CharCode.CloseSquareBracket);322_registerLabel(KeyCode.Quote, CharCode.SingleQuote);323}324}325326public dumpDebugInfo(): string {327const result: string[] = [];328329const immutableSamples = [330ScanCode.ArrowUp,331ScanCode.Numpad0332];333334let cnt = 0;335result.push(`-----------------------------------------------------------------------------------------------------------------------------------------`);336for (let scanCode = ScanCode.None; scanCode < ScanCode.MAX_VALUE; scanCode++) {337if (IMMUTABLE_CODE_TO_KEY_CODE[scanCode] !== KeyCode.DependsOnKbLayout) {338if (immutableSamples.indexOf(scanCode) === -1) {339continue;340}341}342343if (cnt % 6 === 0) {344result.push(`| HW Code combination | Key | KeyCode combination | UI label | User settings | WYSIWYG |`);345result.push(`-----------------------------------------------------------------------------------------------------------------------------------------`);346}347cnt++;348349const mapping = this._codeInfo[scanCode];350const strCode = ScanCodeUtils.toString(scanCode);351352const mods = [0b000, 0b010, 0b101, 0b111];353for (const mod of mods) {354const ctrlKey = (mod & 0b001) ? true : false;355const shiftKey = (mod & 0b010) ? true : false;356const altKey = (mod & 0b100) ? true : false;357const scanCodeChord = new ScanCodeChord(ctrlKey, shiftKey, altKey, false, scanCode);358const keyCodeChord = this._resolveChord(scanCodeChord);359const strKeyCode = (keyCodeChord ? KeyCodeUtils.toString(keyCodeChord.keyCode) : null);360const resolvedKb = (keyCodeChord ? new WindowsNativeResolvedKeybinding(this, [keyCodeChord]) : null);361362const outScanCode = `${ctrlKey ? 'Ctrl+' : ''}${shiftKey ? 'Shift+' : ''}${altKey ? 'Alt+' : ''}${strCode}`;363const ariaLabel = (resolvedKb ? resolvedKb.getAriaLabel() : null);364const outUILabel = (ariaLabel ? ariaLabel.replace(/Control\+/, 'Ctrl+') : null);365const outUserSettings = (resolvedKb ? resolvedKb.getUserSettingsLabel() : null);366const outKey = WindowsNativeResolvedKeybinding.getProducedChar(scanCodeChord, mapping);367const outKb = (strKeyCode ? `${ctrlKey ? 'Ctrl+' : ''}${shiftKey ? 'Shift+' : ''}${altKey ? 'Alt+' : ''}${strKeyCode}` : null);368const isWYSIWYG = (resolvedKb ? resolvedKb.isWYSIWYG() : false);369const outWYSIWYG = (isWYSIWYG ? ' ' : ' NO ');370result.push(`| ${this._leftPad(outScanCode, 30)} | ${outKey} | ${this._leftPad(outKb, 25)} | ${this._leftPad(outUILabel, 25)} | ${this._leftPad(outUserSettings, 25)} | ${outWYSIWYG} |`);371}372result.push(`-----------------------------------------------------------------------------------------------------------------------------------------`);373}374375376return result.join('\n');377}378379private _leftPad(str: string | null, cnt: number): string {380if (str === null) {381str = 'null';382}383while (str.length < cnt) {384str = ' ' + str;385}386return str;387}388389public getUILabelForKeyCode(keyCode: KeyCode): string {390return this._getLabelForKeyCode(keyCode);391}392393public getAriaLabelForKeyCode(keyCode: KeyCode): string {394return this._getLabelForKeyCode(keyCode);395}396397public getUserSettingsLabelForKeyCode(keyCode: KeyCode): string {398if (this._isUSStandard) {399return KeyCodeUtils.toUserSettingsUS(keyCode);400}401return KeyCodeUtils.toUserSettingsGeneral(keyCode);402}403404public getElectronAcceleratorForKeyBinding(chord: KeyCodeChord): string | null {405return KeyCodeUtils.toElectronAccelerator(chord.keyCode);406}407408private _getLabelForKeyCode(keyCode: KeyCode): string {409return this._keyCodeToLabel[keyCode] || KeyCodeUtils.toString(KeyCode.Unknown);410}411412public resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): WindowsNativeResolvedKeybinding {413const ctrlKey = keyboardEvent.ctrlKey || (this._mapAltGrToCtrlAlt && keyboardEvent.altGraphKey);414const altKey = keyboardEvent.altKey || (this._mapAltGrToCtrlAlt && keyboardEvent.altGraphKey);415const chord = new KeyCodeChord(ctrlKey, keyboardEvent.shiftKey, altKey, keyboardEvent.metaKey, keyboardEvent.keyCode);416return new WindowsNativeResolvedKeybinding(this, [chord]);417}418419private _resolveChord(chord: Chord | null): KeyCodeChord | null {420if (!chord) {421return null;422}423if (chord instanceof KeyCodeChord) {424if (!this._keyCodeExists[chord.keyCode]) {425return null;426}427return chord;428}429const keyCode = this._scanCodeToKeyCode[chord.scanCode] || KeyCode.Unknown;430if (keyCode === KeyCode.Unknown || !this._keyCodeExists[keyCode]) {431return null;432}433return new KeyCodeChord(chord.ctrlKey, chord.shiftKey, chord.altKey, chord.metaKey, keyCode);434}435436public resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[] {437const chords: KeyCodeChord[] = toEmptyArrayIfContainsNull(keybinding.chords.map(chord => this._resolveChord(chord)));438if (chords.length > 0) {439return [new WindowsNativeResolvedKeybinding(this, chords)];440}441return [];442}443}444445446