Skip to content

Commit

Permalink
Merge pull request #65826 from toumorokoshi/feature/keychords
Browse files Browse the repository at this point in the history
Modifying ChordKeybinding to support multiple chords
  • Loading branch information
alexdima authored Feb 15, 2019
2 parents 37efd5c + 555739f commit 871a6fe
Show file tree
Hide file tree
Showing 22 changed files with 367 additions and 389 deletions.
74 changes: 45 additions & 29 deletions src/vs/base/common/keyCodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { OperatingSystem } from 'vs/base/common/platform';
import { illegalArgument } from 'vs/base/common/errors';

/**
* Virtual Key Codes, the value does not hold any inherent meaning.
Expand Down Expand Up @@ -417,12 +418,12 @@ export function createKeybinding(keybinding: number, OS: OperatingSystem): Keybi
const firstPart = (keybinding & 0x0000FFFF) >>> 0;
const chordPart = (keybinding & 0xFFFF0000) >>> 16;
if (chordPart !== 0) {
return new ChordKeybinding(
return new ChordKeybinding([
createSimpleKeybinding(firstPart, OS),
createSimpleKeybinding(chordPart, OS),
);
createSimpleKeybinding(chordPart, OS)
]);
}
return createSimpleKeybinding(firstPart, OS);
return new ChordKeybinding([createSimpleKeybinding(firstPart, OS)]);
}

export function createSimpleKeybinding(keybinding: number, OS: OperatingSystem): SimpleKeybinding {
Expand All @@ -439,14 +440,7 @@ export function createSimpleKeybinding(keybinding: number, OS: OperatingSystem):
return new SimpleKeybinding(ctrlKey, shiftKey, altKey, metaKey, keyCode);
}

export const enum KeybindingType {
Simple = 1,
Chord = 2
}

export class SimpleKeybinding {
public readonly type = KeybindingType.Simple;

public readonly ctrlKey: boolean;
public readonly shiftKey: boolean;
public readonly altKey: boolean;
Expand All @@ -461,10 +455,7 @@ export class SimpleKeybinding {
this.keyCode = keyCode;
}

public equals(other: Keybinding): boolean {
if (other.type !== KeybindingType.Simple) {
return false;
}
public equals(other: SimpleKeybinding): boolean {
return (
this.ctrlKey === other.ctrlKey
&& this.shiftKey === other.shiftKey
Expand Down Expand Up @@ -492,6 +483,10 @@ export class SimpleKeybinding {
);
}

public toChord(): ChordKeybinding {
return new ChordKeybinding([this]);
}

/**
* Does this keybinding refer to the key code of a modifier and it also has the modifier flag?
*/
Expand All @@ -506,22 +501,43 @@ export class SimpleKeybinding {
}

export class ChordKeybinding {
public readonly type = KeybindingType.Chord;

public readonly firstPart: SimpleKeybinding;
public readonly chordPart: SimpleKeybinding;
public readonly parts: SimpleKeybinding[];

constructor(firstPart: SimpleKeybinding, chordPart: SimpleKeybinding) {
this.firstPart = firstPart;
this.chordPart = chordPart;
constructor(parts: SimpleKeybinding[]) {
if (parts.length === 0) {
throw illegalArgument(`parts`);
}
this.parts = parts;
}

public getHashCode(): string {
return `${this.firstPart.getHashCode()};${this.chordPart.getHashCode()}`;
let result = '';
for (let i = 0, len = this.parts.length; i < len; i++) {
if (i !== 0) {
result += ';';
}
result += this.parts[i].getHashCode();
}
return result;
}

public equals(other: ChordKeybinding | null): boolean {
if (other === null) {
return false;
}
if (this.parts.length !== other.parts.length) {
return false;
}
for (let i = 0; i < this.parts.length; i++) {
if (!this.parts[i].equals(other.parts[i])) {
return false;
}
}
return true;
}
}

export type Keybinding = SimpleKeybinding | ChordKeybinding;
export type Keybinding = ChordKeybinding;

export class ResolvedKeybindingPart {
readonly ctrlKey: boolean;
Expand Down Expand Up @@ -574,12 +590,12 @@ export abstract class ResolvedKeybinding {
public abstract isChord(): boolean;

/**
* Returns the firstPart, chordPart that should be used for dispatching.
* Returns the parts that should be used for dispatching.
*/
public abstract getDispatchParts(): [string | null, string | null];
public abstract getDispatchParts(): (string | null)[];
/**
* Returns the firstPart, chordPart of the keybinding.
* For simple keybindings, the second element will be null.
* Returns the parts that comprise of the keybinding.
* Simple keybindings return one element.
*/
public abstract getParts(): [ResolvedKeybindingPart, ResolvedKeybindingPart | null];
public abstract getParts(): ResolvedKeybindingPart[];
}
32 changes: 18 additions & 14 deletions src/vs/base/common/keybindingLabels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export interface Modifiers {
readonly metaKey: boolean;
}

export interface KeyLabelProvider<T extends Modifiers> {
(keybinding: T): string | null;
}

export class ModifierLabelProvider {

public readonly modifierLabels: ModifierLabels[];
Expand All @@ -32,11 +36,22 @@ export class ModifierLabelProvider {
this.modifierLabels[OperatingSystem.Linux] = linux;
}

public toLabel(firstPartMod: Modifiers | null, firstPartKey: string | null, chordPartMod: Modifiers | null, chordPartKey: string | null, OS: OperatingSystem): string | null {
if (firstPartMod === null || firstPartKey === null) {
public toLabel<T extends Modifiers>(OS: OperatingSystem, parts: T[], keyLabelProvider: KeyLabelProvider<T>): string | null {
if (parts.length === 0) {
return null;
}
return _asString(firstPartMod, firstPartKey, chordPartMod, chordPartKey, this.modifierLabels[OS]);

let result: string[] = [];
for (let i = 0, len = parts.length; i < len; i++) {
const part = parts[i];
const keyLabel = keyLabelProvider(part);
if (keyLabel === null) {
// this keybinding cannot be expressed...
return null;
}
result[i] = _simpleAsString(part, keyLabel, this.modifierLabels[OS]);
}
return result.join(' ');
}
}

Expand Down Expand Up @@ -171,14 +186,3 @@ function _simpleAsString(modifiers: Modifiers, key: string, labels: ModifierLabe

return result.join(labels.separator);
}

function _asString(firstPartMod: Modifiers, firstPartKey: string, chordPartMod: Modifiers | null, chordPartKey: string | null, labels: ModifierLabels): string {
let result = _simpleAsString(firstPartMod, firstPartKey, labels);

if (chordPartMod !== null && chordPartKey !== null) {
result += ' ';
result += _simpleAsString(chordPartMod, chordPartKey, labels);
}

return result;
}
17 changes: 8 additions & 9 deletions src/vs/base/common/keybindingParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,14 @@ export class KeybindingParser {
return null;
}

let [firstPart, remains] = this.parseSimpleKeybinding(input);
let chordPart: SimpleKeybinding | null = null;
if (remains.length > 0) {
[chordPart] = this.parseSimpleKeybinding(remains);
}
let parts: SimpleKeybinding[] = [];
let part: SimpleKeybinding;

if (chordPart) {
return new ChordKeybinding(firstPart, chordPart);
}
return firstPart;
do {
[part, input] = this.parseSimpleKeybinding(input);
parts.push(part);
} while (input.length > 0);
return new ChordKeybinding(parts);
}

private static parseSimpleUserBinding(input: string): [SimpleKeybinding | ScanCodeBinding, string] {
Expand All @@ -110,6 +108,7 @@ export class KeybindingParser {
}

static parseUserBinding(input: string): [SimpleKeybinding | ScanCodeBinding | null, SimpleKeybinding | ScanCodeBinding | null] {
// TODO@chords: allow users to define N chords
if (!input) {
return [null, null];
}
Expand Down
18 changes: 10 additions & 8 deletions src/vs/base/parts/tree/browser/treeDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import * as mouse from 'vs/base/browser/mouseEvent';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import * as _ from 'vs/base/parts/tree/browser/tree';
import { IDragAndDropData } from 'vs/base/browser/dnd';
import { KeyCode, KeyMod, Keybinding, SimpleKeybinding, createSimpleKeybinding } from 'vs/base/common/keyCodes';
import { KeyCode, KeyMod, Keybinding, SimpleKeybinding, createKeybinding } from 'vs/base/common/keyCodes';

export interface IKeyBindingCallback {
(tree: _.ITree, event: IKeyboardEvent): void;
Expand Down Expand Up @@ -49,7 +49,7 @@ export interface IControllerOptions {
}

interface IKeybindingDispatcherItem {
keybinding: Keybinding;
keybinding: Keybinding | null;
callback: IKeyBindingCallback;
}

Expand All @@ -62,18 +62,20 @@ export class KeybindingDispatcher {
}

public has(keybinding: KeyCode): boolean {
let target = createSimpleKeybinding(keybinding, platform.OS);
for (const a of this._arr) {
if (target.equals(a.keybinding)) {
return true;
let target = createKeybinding(keybinding, platform.OS);
if (target !== null) {
for (const a of this._arr) {
if (target.equals(a.keybinding)) {
return true;
}
}
}
return false;
}

public set(keybinding: number, callback: IKeyBindingCallback) {
this._arr.push({
keybinding: createSimpleKeybinding(keybinding, platform.OS),
keybinding: createKeybinding(keybinding, platform.OS),
callback: callback
});
}
Expand All @@ -82,7 +84,7 @@ export class KeybindingDispatcher {
// Loop from the last to the first to handle overwrites
for (let i = this._arr.length - 1; i >= 0; i--) {
let item = this._arr[i];
if (keybinding.equals(item.keybinding)) {
if (keybinding.toChord().equals(item.keybinding)) {
return item.callback;
}
}
Expand Down
80 changes: 40 additions & 40 deletions src/vs/base/test/common/keyCodes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,35 +20,35 @@ suite('keyCodes', () => {
}

test(null, 0);
test(new SimpleKeybinding(false, false, false, false, KeyCode.Enter), KeyCode.Enter);
test(new SimpleKeybinding(true, false, false, false, KeyCode.Enter), KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, false, true, false, KeyCode.Enter), KeyMod.Alt | KeyCode.Enter);
test(new SimpleKeybinding(true, false, true, false, KeyCode.Enter), KeyMod.Alt | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, true, false, false, KeyCode.Enter), KeyMod.Shift | KeyCode.Enter);
test(new SimpleKeybinding(true, true, false, false, KeyCode.Enter), KeyMod.Shift | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, true, true, false, KeyCode.Enter), KeyMod.Shift | KeyMod.Alt | KeyCode.Enter);
test(new SimpleKeybinding(true, true, true, false, KeyCode.Enter), KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, false, false, true, KeyCode.Enter), KeyMod.CtrlCmd | KeyCode.Enter);
test(new SimpleKeybinding(true, false, false, true, KeyCode.Enter), KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, false, true, true, KeyCode.Enter), KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Enter);
test(new SimpleKeybinding(true, false, true, true, KeyCode.Enter), KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, true, false, true, KeyCode.Enter), KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter);
test(new SimpleKeybinding(true, true, false, true, KeyCode.Enter), KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, true, true, true, KeyCode.Enter), KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.Enter);
test(new SimpleKeybinding(true, true, true, true, KeyCode.Enter), KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, false, false, false, KeyCode.Enter).toChord(), KeyCode.Enter);
test(new SimpleKeybinding(true, false, false, false, KeyCode.Enter).toChord(), KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, false, true, false, KeyCode.Enter).toChord(), KeyMod.Alt | KeyCode.Enter);
test(new SimpleKeybinding(true, false, true, false, KeyCode.Enter).toChord(), KeyMod.Alt | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, true, false, false, KeyCode.Enter).toChord(), KeyMod.Shift | KeyCode.Enter);
test(new SimpleKeybinding(true, true, false, false, KeyCode.Enter).toChord(), KeyMod.Shift | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, true, true, false, KeyCode.Enter).toChord(), KeyMod.Shift | KeyMod.Alt | KeyCode.Enter);
test(new SimpleKeybinding(true, true, true, false, KeyCode.Enter).toChord(), KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, false, false, true, KeyCode.Enter).toChord(), KeyMod.CtrlCmd | KeyCode.Enter);
test(new SimpleKeybinding(true, false, false, true, KeyCode.Enter).toChord(), KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, false, true, true, KeyCode.Enter).toChord(), KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Enter);
test(new SimpleKeybinding(true, false, true, true, KeyCode.Enter).toChord(), KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, true, false, true, KeyCode.Enter).toChord(), KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter);
test(new SimpleKeybinding(true, true, false, true, KeyCode.Enter).toChord(), KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, true, true, true, KeyCode.Enter).toChord(), KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.Enter);
test(new SimpleKeybinding(true, true, true, true, KeyCode.Enter).toChord(), KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.Enter);

test(
new ChordKeybinding(
new ChordKeybinding([
new SimpleKeybinding(false, false, false, false, KeyCode.Enter),
new SimpleKeybinding(false, false, false, false, KeyCode.Tab)
),
]),
KeyChord(KeyCode.Enter, KeyCode.Tab)
);
test(
new ChordKeybinding(
new ChordKeybinding([
new SimpleKeybinding(false, false, false, true, KeyCode.KEY_Y),
new SimpleKeybinding(false, false, false, false, KeyCode.KEY_Z)
),
]),
KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_Y, KeyCode.KEY_Z)
);
});
Expand All @@ -62,35 +62,35 @@ suite('keyCodes', () => {
}

test(null, 0);
test(new SimpleKeybinding(false, false, false, false, KeyCode.Enter), KeyCode.Enter);
test(new SimpleKeybinding(false, false, false, true, KeyCode.Enter), KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, false, true, false, KeyCode.Enter), KeyMod.Alt | KeyCode.Enter);
test(new SimpleKeybinding(false, false, true, true, KeyCode.Enter), KeyMod.Alt | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, true, false, false, KeyCode.Enter), KeyMod.Shift | KeyCode.Enter);
test(new SimpleKeybinding(false, true, false, true, KeyCode.Enter), KeyMod.Shift | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, true, true, false, KeyCode.Enter), KeyMod.Shift | KeyMod.Alt | KeyCode.Enter);
test(new SimpleKeybinding(false, true, true, true, KeyCode.Enter), KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(true, false, false, false, KeyCode.Enter), KeyMod.CtrlCmd | KeyCode.Enter);
test(new SimpleKeybinding(true, false, false, true, KeyCode.Enter), KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(true, false, true, false, KeyCode.Enter), KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Enter);
test(new SimpleKeybinding(true, false, true, true, KeyCode.Enter), KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(true, true, false, false, KeyCode.Enter), KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter);
test(new SimpleKeybinding(true, true, false, true, KeyCode.Enter), KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(true, true, true, false, KeyCode.Enter), KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.Enter);
test(new SimpleKeybinding(true, true, true, true, KeyCode.Enter), KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, false, false, false, KeyCode.Enter).toChord(), KeyCode.Enter);
test(new SimpleKeybinding(false, false, false, true, KeyCode.Enter).toChord(), KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, false, true, false, KeyCode.Enter).toChord(), KeyMod.Alt | KeyCode.Enter);
test(new SimpleKeybinding(false, false, true, true, KeyCode.Enter).toChord(), KeyMod.Alt | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, true, false, false, KeyCode.Enter).toChord(), KeyMod.Shift | KeyCode.Enter);
test(new SimpleKeybinding(false, true, false, true, KeyCode.Enter).toChord(), KeyMod.Shift | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(false, true, true, false, KeyCode.Enter).toChord(), KeyMod.Shift | KeyMod.Alt | KeyCode.Enter);
test(new SimpleKeybinding(false, true, true, true, KeyCode.Enter).toChord(), KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(true, false, false, false, KeyCode.Enter).toChord(), KeyMod.CtrlCmd | KeyCode.Enter);
test(new SimpleKeybinding(true, false, false, true, KeyCode.Enter).toChord(), KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(true, false, true, false, KeyCode.Enter).toChord(), KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Enter);
test(new SimpleKeybinding(true, false, true, true, KeyCode.Enter).toChord(), KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(true, true, false, false, KeyCode.Enter).toChord(), KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter);
test(new SimpleKeybinding(true, true, false, true, KeyCode.Enter).toChord(), KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.WinCtrl | KeyCode.Enter);
test(new SimpleKeybinding(true, true, true, false, KeyCode.Enter).toChord(), KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.Enter);
test(new SimpleKeybinding(true, true, true, true, KeyCode.Enter).toChord(), KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyMod.WinCtrl | KeyCode.Enter);

test(
new ChordKeybinding(
new ChordKeybinding([
new SimpleKeybinding(false, false, false, false, KeyCode.Enter),
new SimpleKeybinding(false, false, false, false, KeyCode.Tab)
),
]),
KeyChord(KeyCode.Enter, KeyCode.Tab)
);
test(
new ChordKeybinding(
new ChordKeybinding([
new SimpleKeybinding(true, false, false, false, KeyCode.KEY_Y),
new SimpleKeybinding(false, false, false, false, KeyCode.KEY_Z)
),
]),
KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_Y, KeyCode.KEY_Z)
);

Expand Down
2 changes: 1 addition & 1 deletion src/vs/editor/standalone/browser/simpleServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ export class StandaloneKeybindingService extends AbstractKeybindingService {
keyboardEvent.altKey,
keyboardEvent.metaKey,
keyboardEvent.keyCode
);
).toChord();
return new USLayoutResolvedKeybinding(keybinding, OS);
}

Expand Down
Loading

0 comments on commit 871a6fe

Please sign in to comment.