Add NativeResolvedKeybinding & tests

This commit is contained in:
Alex Dima
2017-03-14 17:55:04 +01:00
parent 279e9e5db1
commit 560d5e0c19
3 changed files with 368 additions and 34 deletions
@@ -410,4 +410,6 @@ export const enum KeyboardEventCode {
MailReply,
MailForward,
MailSend,
MAX_VALUE
}
@@ -6,10 +6,12 @@
'use strict';
import { OperatingSystem } from 'vs/base/common/platform';
import { SimpleKeybinding, KeyCode } from 'vs/base/common/keyCodes';
import { SimpleKeybinding, KeyCode, ResolvedKeybinding, Keybinding, KeyCodeUtils } from 'vs/base/common/keyCodes';
import { KeyboardEventCode, KeyboardEventCodeUtils } from 'vs/workbench/services/keybinding/common/keyboardEventCode';
import { CharCode } from 'vs/base/common/charCode';
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/abstractKeybindingService';
import { IHTMLContentElement } from 'vs/base/common/htmlContent';
import { PrintableKeypress, UILabelProvider, AriaLabelProvider } from 'vs/platform/keybinding/common/keybindingLabels';
export interface IKeyMapping {
value: string;
@@ -43,6 +45,7 @@ function cannotMapSimpleKeybinding(keybinding: SimpleKeybinding, OS: OperatingSy
* -1 if a KeyCode => keyboardEvent.code mapping depends on kb layout.
*/
const IMMUTABLE_KEY_CODE_TO_CODE: KeyboardEventCode[] = [];
const IMMUTABLE_CODE_TO_KEY_CODE: KeyCode[] = [];
/**
* Chars that will be remapped.
@@ -130,7 +133,7 @@ const enum ModifierState {
ShiftAltGr = 3
}
export class HardwareKeyPress {
export class HardwareKeypress {
readonly ctrlKey: boolean;
readonly shiftKey: boolean;
readonly altKey: boolean;
@@ -144,35 +147,131 @@ export class HardwareKeyPress {
this.metaKey = metaKey;
this.code = code;
}
public toPrintableKeypress(key: string): PrintableKeypress {
return new PrintableKeypress(this.ctrlKey, this.shiftKey, this.altKey, this.metaKey, key);
}
}
export class NativeResolvedKeybinding extends ResolvedKeybinding {
private readonly _mapper: KeyboardMapper;
private readonly _OS: OperatingSystem;
private readonly _firstPart: HardwareKeypress;
private readonly _chordPart: HardwareKeypress;
constructor(mapper: KeyboardMapper, OS: OperatingSystem, firstPart: HardwareKeypress, chordPart: HardwareKeypress) {
super();
this._mapper = mapper;
this._OS = OS;
this._firstPart = firstPart;
this._chordPart = chordPart;
}
public getLabel(): string {
let firstPart = this._firstPart.toPrintableKeypress(this._mapper.getUILabelForHardwareCode(this._firstPart.code));
let chordPart = this._chordPart ? this._chordPart.toPrintableKeypress(this._mapper.getUILabelForHardwareCode(this._chordPart.code)) : null;
return UILabelProvider.toLabel2(firstPart, chordPart, this._OS);
}
public getAriaLabel(): string {
let firstPart = this._firstPart.toPrintableKeypress(this._mapper.getAriaLabelForHardwareCode(this._firstPart.code));
let chordPart = this._chordPart ? this._chordPart.toPrintableKeypress(this._mapper.getAriaLabelForHardwareCode(this._chordPart.code)) : null;
return AriaLabelProvider.toLabel2(firstPart, chordPart, this._OS);
}
public getHTMLLabel(): IHTMLContentElement[] {
let firstPart = this._firstPart.toPrintableKeypress(this._mapper.getUILabelForHardwareCode(this._firstPart.code));
let chordPart = this._chordPart ? this._chordPart.toPrintableKeypress(this._mapper.getUILabelForHardwareCode(this._chordPart.code)) : null;
return UILabelProvider.toHTMLLabel2(firstPart, chordPart, this._OS);
}
public getElectronAccelerator(): string {
throw new Error('TODO!');
// const usResolvedKeybinding = new USLayoutResolvedKeybinding(this._actual, OS);
// if (OS === OperatingSystem.Windows) {
// // electron menus always do the correct rendering on Windows
// return usResolvedKeybinding.getElectronAccelerator();
// }
// let usLabel = usResolvedKeybinding.getLabel();
// let label = this.getLabel();
// if (usLabel !== label) {
// // electron menus are incorrect in rendering (linux) and in rendering and interpreting (mac)
// // for non US standard keyboard layouts
// return null;
// }
// return usResolvedKeybinding.getElectronAccelerator();
}
public getUserSettingsLabel(): string {
throw new Error('TODO!');
// return KeybindingIO.writeKeybinding(this._actual, OS);
}
}
interface IHardwareCodeMapping {
value: number;
withShift: number;
withAltGr: number;
withShiftAltGr: number;
valueIsDeadKey: boolean;
withShiftIsDeadKey: boolean;
withAltGrIsDeadKey: boolean;
withShiftAltGrIsDeadKey: boolean;
}
export class KeyboardMapper {
private readonly _OS: OperatingSystem;
private readonly _remapChars2: HardwareKeyPress[][];
private readonly _remapChars: HardwareKeypress[][];
private readonly _mappings: IHardwareCodeMapping[];
constructor(mapping: IKeyboardMapping, OS: OperatingSystem) {
constructor(mappings: IKeyboardMapping, OS: OperatingSystem) {
this._remapChars2 = [];
this._remapChars = [];
let maxCharCode = REMAP_CHARS.reduce((prev, curr) => Math.max(prev, curr));
for (let i = 0; i <= maxCharCode; i++) {
this._remapChars2[i] = null;
this._remapChars[i] = null;
}
for (let strCode in mapping) {
if (mapping.hasOwnProperty(strCode)) {
this._mappings = [];
for (let strCode in mappings) {
if (mappings.hasOwnProperty(strCode)) {
const code = KeyboardEventCodeUtils.toEnum(strCode);
if (code === KeyboardEventCode.None) {
log(`Unknown code ${strCode} in mapping.`);
continue;
}
const results = mapping[strCode];
const mapping = mappings[strCode];
const value = KeyboardMapper._getCharCode(mapping.value);
const withShift = KeyboardMapper._getCharCode(mapping.withShift);
const withAltGr = KeyboardMapper._getCharCode(mapping.withAltGr);
const withShiftAltGr = KeyboardMapper._getCharCode(mapping.withShiftAltGr);
this._register(code, false, false, false, KeyboardMapper._getCharCode(results.value));
this._register(code, false, true, false, KeyboardMapper._getCharCode(results.withShift));
this._register(code, true, false, true, KeyboardMapper._getCharCode(results.withAltGr));
this._register(code, true, true, true, KeyboardMapper._getCharCode(results.withShiftAltGr));
this._mappings[code] = {
value: value,
withShift: withShift,
withAltGr: withAltGr,
withShiftAltGr: withShiftAltGr,
valueIsDeadKey: mapping.valueIsDeadKey,
withShiftIsDeadKey: mapping.withShiftIsDeadKey,
withAltGrIsDeadKey: mapping.withAltGrIsDeadKey,
withShiftAltGrIsDeadKey: mapping.withShiftAltGrIsDeadKey,
};
this._register(code, false, false, false, value);
this._register(code, false, true, false, withShift);
this._register(code, true, false, true, withAltGr);
this._register(code, true, true, true, withShiftAltGr);
}
}
@@ -180,7 +279,7 @@ export class KeyboardMapper {
for (let i = 0; i < REMAP_CHARS.length; i++) {
const charCode = REMAP_CHARS[i];
let combos = this._remapChars2[charCode];
let combos = this._remapChars[charCode];
if (combos === null) {
log(`Could not find any key combination producing '${String.fromCharCode(charCode)}'`);
} else if (combos.length > 1) {
@@ -238,15 +337,15 @@ export class KeyboardMapper {
return;
}
let entry = new HardwareKeyPress(ctrlKey, shiftKey, altKey, false, code);
let entry = new HardwareKeypress(ctrlKey, shiftKey, altKey, false, code);
if (this._remapChars2[charCode] === null) {
if (this._remapChars[charCode] === null) {
// no duplicates so far
this._remapChars2[charCode] = [entry];
this._remapChars[charCode] = [entry];
return;
}
const list = this._remapChars2[charCode];
const list = this._remapChars[charCode];
// Do not register if it already sits under the same code
for (let i = 0, len = list.length; i < len; i++) {
if (list[i].code === code) {
@@ -256,7 +355,7 @@ export class KeyboardMapper {
list.push(entry);
}
private _doMapSimpleKeybinding(source: SimpleKeybinding, keyCombo: HardwareKeyPress, ctrlKey: boolean, altKey: boolean, metaKey: boolean, charCode: number): HardwareKeyPress {
private _doMapSimpleKeybinding(source: SimpleKeybinding, keyCombo: HardwareKeypress, ctrlKey: boolean, altKey: boolean, metaKey: boolean, charCode: number): HardwareKeypress {
if ((keyCombo.ctrlKey && ctrlKey) || (keyCombo.altKey && altKey)) {
cannotMapSimpleKeybinding(source, this._OS, `ctrl or alt modifiers are needed to produce '${String.fromCharCode(charCode)}'`);
return null;
@@ -270,13 +369,13 @@ export class KeyboardMapper {
altKey = true;
}
return new HardwareKeyPress(ctrlKey, shiftKey, altKey, metaKey, keyCombo.code);
return new HardwareKeypress(ctrlKey, shiftKey, altKey, metaKey, keyCombo.code);
}
private _mapSimpleKeybinding(source: SimpleKeybinding, ctrlKey: boolean, altKey: boolean, metaKey: boolean, charCode: number): HardwareKeyPress[] {
const keyCombos = this._remapChars2[charCode];
private _mapSimpleKeybinding(source: SimpleKeybinding, ctrlKey: boolean, altKey: boolean, metaKey: boolean, charCode: number): HardwareKeypress[] {
const keyCombos = this._remapChars[charCode];
let result: HardwareKeyPress[] = [], resultLen = 0;
let result: HardwareKeypress[] = [], resultLen = 0;
if (keyCombos !== null) {
for (let i = 0, len = keyCombos.length; i < len; i++) {
const keyCombo = keyCombos[i];
@@ -292,7 +391,7 @@ export class KeyboardMapper {
return result;
}
public mapSimpleKeybinding(keybinding: SimpleKeybinding): HardwareKeyPress[] {
public mapSimpleKeybinding(keybinding: SimpleKeybinding): HardwareKeypress[] {
const ctrlCmd = keybinding.hasCtrlCmd();
const winCtrl = keybinding.hasWinCtrl();
@@ -304,7 +403,7 @@ export class KeyboardMapper {
if (IMMUTABLE_KEY_CODE_TO_CODE[keyCode] !== -1) {
const keyboardEventCode = IMMUTABLE_KEY_CODE_TO_CODE[keyCode];
return [new HardwareKeyPress(ctrlKey, shiftKey, altKey, metaKey, keyboardEventCode)];
return [new HardwareKeypress(ctrlKey, shiftKey, altKey, metaKey, keyboardEventCode)];
}
let desiredCharCode = 0;
@@ -362,18 +461,90 @@ export class KeyboardMapper {
return this._mapSimpleKeybinding(keybinding, ctrlKey, altKey, metaKey, desiredCharCode);
}
public getUILabelForHardwareCode(code: KeyboardEventCode): string {
return this._getLabelForHardwareCode(code, true);
}
public getAriaLabelForHardwareCode(code: KeyboardEventCode): string {
return this._getLabelForHardwareCode(code, false);
}
private _getLabelForHardwareCode(code: KeyboardEventCode, isUI: boolean): string {
if (isUI && this._OS === OperatingSystem.Macintosh) {
switch (code) {
case KeyboardEventCode.ArrowLeft:
return '←';
case KeyboardEventCode.ArrowUp:
return '↑';
case KeyboardEventCode.ArrowRight:
return '→';
case KeyboardEventCode.ArrowDown:
return '↓';
}
}
if (IMMUTABLE_CODE_TO_KEY_CODE[code] !== -1) {
const keyCode = IMMUTABLE_CODE_TO_KEY_CODE[code];
return KeyCodeUtils.toString(keyCode);
}
const mapping = this._mappings[code];
if (!mapping) {
// uh-oh
return 'Unknown';
}
if (mapping.value >= CharCode.a && mapping.value <= CharCode.z) {
return String.fromCharCode(CharCode.A + (mapping.value - CharCode.a));
}
if (mapping.value) {
return String.fromCharCode(mapping.value);
}
throw new Error('TODO!');
}
public resolveKeybinding(keybinding: Keybinding): NativeResolvedKeybinding[] {
let result: NativeResolvedKeybinding[] = [], resultLen = 0;
if (keybinding.isChord()) {
const firstParts = this.mapSimpleKeybinding(keybinding.extractFirstPart());
const chordParts = this.mapSimpleKeybinding(keybinding.extractChordPart());
for (let i = 0, len = firstParts.length; i < len; i++) {
const firstPart = firstParts[i];
for (let j = 0, lenJ = chordParts.length; j < lenJ; j++) {
const chordPart = chordParts[j];
result[resultLen++] = new NativeResolvedKeybinding(this, this._OS, firstPart, chordPart);
}
}
} else {
const firstParts = this.mapSimpleKeybinding(keybinding);
for (let i = 0, len = firstParts.length; i < len; i++) {
const firstPart = firstParts[i];
result[resultLen++] = new NativeResolvedKeybinding(this, this._OS, firstPart, null);
}
}
return result;
}
}
(function () {
let currentLength = 0;
for (let i = 0; i <= KeyCode.MAX_VALUE; i++) {
IMMUTABLE_KEY_CODE_TO_CODE[i] = -1;
}
for (let i = 0; i <= KeyboardEventCode.MAX_VALUE; i++) {
IMMUTABLE_CODE_TO_KEY_CODE[i] = -1;
}
function d(keyCode: KeyCode, code: KeyboardEventCode): void {
if (keyCode > currentLength) {
for (let i = currentLength; i < keyCode; i++) {
IMMUTABLE_KEY_CODE_TO_CODE[i] = -1;
}
}
IMMUTABLE_KEY_CODE_TO_CODE[keyCode] = code;
currentLength = keyCode + 1;
IMMUTABLE_CODE_TO_KEY_CODE[code] = keyCode;
}
// Unknown = 0,
@@ -8,12 +8,13 @@
import * as assert from 'assert';
import { mac_de_ch } from 'vs/workbench/services/keybinding/test/mac_de_ch';
import { linux_de_ch } from 'vs/workbench/services/keybinding/test/linux_de_ch';
import { KeyMod, KeyCode, SimpleKeybinding, createKeybinding } from 'vs/base/common/keyCodes';
import { KeyMod, KeyCode, SimpleKeybinding, createKeybinding, Keybinding } from 'vs/base/common/keyCodes';
import { KeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper';
import { OperatingSystem } from 'vs/base/common/platform';
import { UserSettingsLabelProvider, PrintableKeypress } from 'vs/platform/keybinding/common/keybindingLabels';
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/abstractKeybindingService';
import { KeyboardEventCodeUtils } from "vs/workbench/services/keybinding/common/keyboardEventCode";
import { KeyboardEventCodeUtils } from 'vs/workbench/services/keybinding/common/keyboardEventCode';
import { IHTMLContentElement } from "vs/base/common/htmlContent";
function _assertKeybindingTranslation(mapper: KeyboardMapper, OS: OperatingSystem, kb: number, expected: string[]): void {
let actualHardwareKeypresses = mapper.mapSimpleKeybinding(new SimpleKeybinding(kb));
@@ -325,6 +326,104 @@ suite('keyboardMapper - MAC de_ch', () => {
// OEM_8
// OEM_102
});
test('resolveKeybinding - all labels', () => {
function _assertAllLabels(keybinding: Keybinding, labels: string[], ariaLabels: string[], htmlLabel: IHTMLContentElement[][]): void {
const kb = mapper.resolveKeybinding(keybinding);
let actualLabels = kb.map(k => k.getLabel());
assert.deepEqual(actualLabels, labels);
let actualAriaLabels = kb.map(k => k.getAriaLabel());
assert.deepEqual(actualAriaLabels, ariaLabels);
let actualHTMLLabels = kb.map(k => k.getHTMLLabel());
assert.deepEqual(actualHTMLLabels, htmlLabel);
}
function assertAllLabels(keybinding: Keybinding, label: string | string[], ariaLabel: string | string[], htmlLabel: IHTMLContentElement[][]): void {
let _labels = (typeof label === 'string' ? [label] : label);
let _ariaLabels = (typeof ariaLabel === 'string' ? [ariaLabel] : ariaLabel);
_assertAllLabels(keybinding, _labels, _ariaLabels, htmlLabel);
}
// TODO: ElectronAccelerator, UserSettings
assertAllLabels(
createKeybinding(KeyMod.CtrlCmd | KeyCode.KEY_Z),
'⌘Z',
'Command+Z',
[[{
tagName: 'span',
className: 'monaco-kb',
children: [
{ tagName: 'span', className: 'monaco-kbkey', text: '⌘' },
{ tagName: 'span', className: 'monaco-kbkey', text: 'Z' },
]
}]]
);
});
test('resolveKeybinding - aria labels', () => {
function assertAriaLabels(keybinding: number, label: string | string[]): void {
let _labels = (typeof label === 'string' ? [label] : label);
const kb = mapper.resolveKeybinding(createKeybinding(keybinding));
let actualLabels = kb.map(k => k.getAriaLabel());
assert.deepEqual(actualLabels, _labels);
}
assertAriaLabels(KeyMod.CtrlCmd | KeyCode.KEY_1, 'Command+1');
assertAriaLabels(KeyMod.CtrlCmd | KeyCode.KEY_B, 'Command+B');
assertAriaLabels(KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_B, 'Shift+Command+B');
assertAriaLabels(KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_B, 'Control+Shift+Alt+Command+B');
assertAriaLabels(KeyMod.CtrlCmd | KeyCode.KEY_Z, 'Command+Z');
assertAriaLabels(KeyMod.CtrlCmd | KeyCode.KEY_Y, 'Command+Y');
// ;
assertAriaLabels(KeyCode.US_SEMICOLON, 'Shift+,');
// :
assertAriaLabels(KeyMod.Shift | KeyCode.US_SEMICOLON, 'Shift+.');
// =
assertAriaLabels(KeyCode.US_EQUAL, 'Shift+0');
// +
assertAriaLabels(KeyMod.Shift | KeyCode.US_EQUAL, 'Shift+1');
// ,
assertAriaLabels(KeyCode.US_COMMA, ',');
// <
assertAriaLabels(KeyMod.Shift | KeyCode.US_COMMA, '<');
// -
assertAriaLabels(KeyCode.US_MINUS, '-');
// _
assertAriaLabels(KeyMod.Shift | KeyCode.US_MINUS, 'Shift+-');
// .
assertAriaLabels(KeyCode.US_DOT, '.');
// >
assertAriaLabels(KeyMod.Shift | KeyCode.US_DOT, 'Shift+<');
// /
assertAriaLabels(KeyCode.US_SLASH, 'Shift+7');
// ?
assertAriaLabels(KeyMod.Shift | KeyCode.US_SLASH, 'Shift+\'');
// `
assertAriaLabels(KeyCode.US_BACKTICK, 'Shift+^');
// ~
assertAriaLabels(KeyMod.Shift | KeyCode.US_BACKTICK, 'Control+Alt+N');
// [
assertAriaLabels(KeyCode.US_OPEN_SQUARE_BRACKET, 'Control+Alt+5');
// {
assertAriaLabels(KeyMod.Shift | KeyCode.US_OPEN_SQUARE_BRACKET, 'Control+Alt+8');
// \
assertAriaLabels(KeyCode.US_BACKSLASH, 'Control+Shift+Alt+7');
// |
assertAriaLabels(KeyMod.Shift | KeyCode.US_BACKSLASH, 'Control+Alt+7');
// ]
assertAriaLabels(KeyCode.US_CLOSE_SQUARE_BRACKET, 'Control+Alt+6');
// }
assertAriaLabels(KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET, 'Control+Alt+9');
// '
assertAriaLabels(KeyCode.US_QUOTE, '\'');
// "
assertAriaLabels(KeyMod.Shift | KeyCode.US_QUOTE, 'Shift+2');
});
});
@@ -626,4 +725,66 @@ suite('keyboardMapper - LINUX de_ch', () => {
// OEM_8
// OEM_102
});
test('resolveKeybinding - aria labels', () => {
function assertAriaLabels(keybinding: number, label: string | string[]): void {
let _labels = (typeof label === 'string' ? [label] : label);
const kb = mapper.resolveKeybinding(createKeybinding(keybinding));
let actualLabels = kb.map(k => k.getAriaLabel());
assert.deepEqual(actualLabels, _labels);
}
assertAriaLabels(KeyMod.CtrlCmd | KeyCode.KEY_1, 'Control+1');
assertAriaLabels(KeyMod.CtrlCmd | KeyCode.KEY_B, 'Control+B');
assertAriaLabels(KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_B, 'Control+Shift+B');
assertAriaLabels(KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.KEY_B, 'Control+Shift+Alt+Windows+B');
assertAriaLabels(KeyMod.CtrlCmd | KeyCode.KEY_Z, 'Control+Z');
assertAriaLabels(KeyMod.CtrlCmd | KeyCode.KEY_Y, 'Control+Y');
// ;
assertAriaLabels(KeyCode.US_SEMICOLON, 'Shift+,');
// :
assertAriaLabels(KeyMod.Shift | KeyCode.US_SEMICOLON, 'Shift+.');
// =
assertAriaLabels(KeyCode.US_EQUAL, 'Shift+0');
// +
assertAriaLabels(KeyMod.Shift | KeyCode.US_EQUAL, 'Shift+1');
// ,
assertAriaLabels(KeyCode.US_COMMA, ',');
// <
assertAriaLabels(KeyMod.Shift | KeyCode.US_COMMA, ['<', 'Control+Shift+Alt+Y']);
// -
assertAriaLabels(KeyCode.US_MINUS, '-');
// _
assertAriaLabels(KeyMod.Shift | KeyCode.US_MINUS, 'Shift+-');
// .
assertAriaLabels(KeyCode.US_DOT, '.');
// >
assertAriaLabels(KeyMod.Shift | KeyCode.US_DOT, ['Shift+<', 'Control+Shift+Alt+X']);
// /
assertAriaLabels(KeyCode.US_SLASH, 'Shift+7');
// ?
assertAriaLabels(KeyMod.Shift | KeyCode.US_SLASH, 'Shift+\'');
// `
assertAriaLabels(KeyCode.US_BACKTICK, 'Shift+^');
// ~
assertAriaLabels(KeyMod.Shift | KeyCode.US_BACKTICK, []);
// [
assertAriaLabels(KeyCode.US_OPEN_SQUARE_BRACKET, 'Control+Alt+ü');
// {
assertAriaLabels(KeyMod.Shift | KeyCode.US_OPEN_SQUARE_BRACKET, 'Control+Alt+ä');
// \
assertAriaLabels(KeyCode.US_BACKSLASH, 'Control+Alt+<');
// |
assertAriaLabels(KeyMod.Shift | KeyCode.US_BACKSLASH, ['Control+Alt+1', 'Control+Alt+7']);
// ]
assertAriaLabels(KeyCode.US_CLOSE_SQUARE_BRACKET, ['Control+Alt+9', 'Control+Alt+¨']);
// }
assertAriaLabels(KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET, ['Control+Alt+0', 'Control+Alt+$']);
// '
assertAriaLabels(KeyCode.US_QUOTE, '\'');
// "
assertAriaLabels(KeyMod.Shift | KeyCode.US_QUOTE, 'Shift+2');
});
});