Skip to content

Commit 905710b

Browse files
committed
microsoft#23368 - Improve keybinding search
- Implement APIs in ResolvedKeybinding to expose parts - Implement APIs in ResolvedKeybinding to get labels without modifiers - Adopt tests to above API changes - Use above APIs to search and highlight keybindings - A new keybinding label widget to render a keybinding - Adopt above widget in Keyboard shortcuts editor and define keybinding widget
1 parent 53e48bb commit 905710b

17 files changed

Lines changed: 683 additions & 366 deletions
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
.monaco-kb {
7+
white-space: nowrap;
8+
}
9+
10+
.monaco-kbkey {
11+
display: inline-block;
12+
border: solid 1px #ccc;
13+
border-bottom-color: #bbb;
14+
border-radius: 3px;
15+
box-shadow: inset 0 -1px 0 #bbb;
16+
background-color: #ddd;
17+
vertical-align: middle;
18+
color: #555;
19+
line-height: 10px;
20+
font-size: 11px;
21+
padding: 3px 5px;
22+
}
23+
24+
.hc-black .monaco-kbkey,
25+
.vs-dark .monaco-kbkey {
26+
background-color: rgba(128, 128, 128, 0.17);
27+
color: #ccc;
28+
border: solid 1px #333;
29+
border-bottom-color: #444;
30+
box-shadow: inset 0 -1px 0 #444;
31+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
'use strict';
6+
7+
import 'vs/css!./keybindingLabel';
8+
import { IDisposable } from 'vs/base/common/lifecycle';
9+
import { equals } from 'vs/base/common/objects';
10+
import { OperatingSystem } from 'vs/base/common/platform';
11+
import { ResolvedKeybinding } from 'vs/base/common/keycodes';
12+
import { UILabelProvider } from 'vs/platform/keybinding/common/keybindingLabels';
13+
import * as dom from 'vs/base/browser/dom';
14+
15+
const $ = dom.$;
16+
17+
export interface PartMatches {
18+
ctrlKey?: boolean;
19+
shiftKey?: boolean;
20+
altKey?: boolean;
21+
metaKey?: boolean;
22+
keyCode?: boolean;
23+
}
24+
25+
export interface Matches {
26+
firstPart: PartMatches;
27+
chordPart: PartMatches;
28+
}
29+
30+
export class KeybindingLabel implements IDisposable {
31+
32+
private domNode: HTMLElement;
33+
private keybinding: ResolvedKeybinding;
34+
private matches: Matches;
35+
private didEverRender: boolean;
36+
37+
constructor(container: HTMLElement, private os: OperatingSystem) {
38+
this.domNode = dom.append(container, $('.htmlkb'));
39+
this.didEverRender = false;
40+
container.appendChild(this.domNode);
41+
}
42+
43+
get element(): HTMLElement {
44+
return this.domNode;
45+
}
46+
47+
set(keybinding: ResolvedKeybinding, matches: Matches) {
48+
if (this.didEverRender && this.keybinding === keybinding && KeybindingLabel.areSame(this.matches, matches)) {
49+
return;
50+
}
51+
52+
this.keybinding = keybinding;
53+
this.matches = matches;
54+
this.render();
55+
}
56+
57+
private render() {
58+
dom.clearNode(this.domNode);
59+
60+
if (this.keybinding) {
61+
let [firstPart, chordPart] = this.keybinding.getParts();
62+
if (firstPart) {
63+
this.renderPart(this.domNode, firstPart, this.matches ? this.matches.firstPart : null);
64+
}
65+
if (chordPart) {
66+
dom.append(this.domNode, $('span', null, ' '));
67+
this.renderPart(this.domNode, chordPart, this.matches ? this.matches.chordPart : null);
68+
}
69+
this.domNode.title = this.keybinding.getAriaLabel();
70+
}
71+
72+
this.didEverRender = true;
73+
}
74+
75+
private renderPart(parent: HTMLElement, part: ResolvedKeybinding, match: PartMatches) {
76+
const modifierLabels = UILabelProvider.modifierLabels[this.os];
77+
if (part.hasCtrlModifier()) {
78+
this.renderKey(parent, modifierLabels.ctrlKey, match && match.ctrlKey, modifierLabels.separator);
79+
}
80+
if (part.hasShiftModifier()) {
81+
this.renderKey(parent, modifierLabels.shiftKey, match && match.shiftKey, modifierLabels.separator);
82+
}
83+
if (part.hasAltModifier()) {
84+
this.renderKey(parent, modifierLabels.altKey, match && match.altKey, modifierLabels.separator);
85+
}
86+
if (part.hasMetaModifier()) {
87+
this.renderKey(parent, modifierLabels.metaKey, match && match.metaKey, modifierLabels.separator);
88+
}
89+
const keyLabel = part.getLabelWithoutModifiers();
90+
if (keyLabel) {
91+
this.renderKey(parent, keyLabel, match && match.keyCode, '');
92+
}
93+
}
94+
95+
private renderKey(parent: HTMLElement, label: string, highlight: boolean, separator: string): void {
96+
dom.append(parent, $('span.monaco-kbkey' + (highlight ? '.highlight' : ''), null, label));
97+
if (separator) {
98+
dom.append(parent, $('span', null, separator));
99+
}
100+
}
101+
102+
dispose() {
103+
this.keybinding = null;
104+
}
105+
106+
private static areSame(a: Matches, b: Matches): boolean {
107+
if (a === b || (!a && !b)) {
108+
return true;
109+
}
110+
return !!a && !!b && equals(a.firstPart, b.firstPart) && equals(a.chordPart, b.chordPart);
111+
}
112+
}

src/vs/base/common/keyCodes.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
'use strict';
77

8-
import { IHTMLContentElement } from 'vs/base/common/htmlContent';
98
import { OperatingSystem } from 'vs/base/common/platform';
109

1110
/**
@@ -550,14 +549,18 @@ export abstract class ResolvedKeybinding {
550549
* This prints the binding in a format suitable for displaying in the UI.
551550
*/
552551
public abstract getLabel(): string;
552+
/**
553+
* Returns the UI label of the binding without modifiers
554+
*/
555+
public abstract getLabelWithoutModifiers(): string;
553556
/**
554557
* This prints the binding in a format suitable for ARIA.
555558
*/
556559
public abstract getAriaLabel(): string;
557560
/**
558-
* This prints the binding in a format suitable for displaying in the UI.
561+
* Returns the ARIA label of the bindings without modifiers
559562
*/
560-
public abstract getHTMLLabel(): IHTMLContentElement[];
563+
public abstract getAriaLabelWithoutModifiers(): string;
561564
/**
562565
* This prints the binding in a format suitable for electron's accelerators.
563566
* See https://github.com/electron/electron/blob/master/docs/api/accelerator.md
@@ -601,4 +604,8 @@ export abstract class ResolvedKeybinding {
601604
* Returns the firstPart, chordPart that should be used for dispatching.
602605
*/
603606
public abstract getDispatchParts(): [string, string];
607+
/**
608+
* Returns the firstPart, chordPart of the keybinding
609+
*/
610+
public abstract getParts(): [ResolvedKeybinding, ResolvedKeybinding];
604611
}

src/vs/platform/keybinding/common/keybindingLabels.ts

Lines changed: 14 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import * as nls from 'vs/nls';
99
import { OperatingSystem } from 'vs/base/common/platform';
10-
import { IHTMLContentElement } from 'vs/base/common/htmlContent';
1110

1211
export interface ModifierLabels {
1312
readonly ctrlKey: string;
@@ -24,29 +23,29 @@ export interface Modifiers {
2423
readonly metaKey: boolean;
2524
}
2625

26+
export const NO_MODIFIERS: Modifiers = {
27+
ctrlKey: false,
28+
shiftKey: false,
29+
altKey: false,
30+
metaKey: false
31+
};
32+
2733
export class ModifierLabelProvider {
2834

29-
private readonly _labels: ModifierLabels[];
35+
public readonly modifierLabels: ModifierLabels[];
3036

3137
constructor(mac: ModifierLabels, windows: ModifierLabels, linux: ModifierLabels = windows) {
32-
this._labels = [null];
33-
this._labels[OperatingSystem.Macintosh] = mac;
34-
this._labels[OperatingSystem.Windows] = windows;
35-
this._labels[OperatingSystem.Linux] = linux;
38+
this.modifierLabels = [null];
39+
this.modifierLabels[OperatingSystem.Macintosh] = mac;
40+
this.modifierLabels[OperatingSystem.Windows] = windows;
41+
this.modifierLabels[OperatingSystem.Linux] = linux;
3642
}
3743

3844
public toLabel(firstPartMod: Modifiers, firstPartKey: string, chordPartMod: Modifiers, chordPartKey: string, OS: OperatingSystem): string {
3945
if (firstPartKey === null && chordPartKey === null) {
4046
return null;
4147
}
42-
return _asString(firstPartMod, firstPartKey, chordPartMod, chordPartKey, this._labels[OS]);
43-
}
44-
45-
public toHTMLLabel(firstPartMod: Modifiers, firstPartKey: string, chordPartMod: Modifiers, chordPartKey: string, OS: OperatingSystem): IHTMLContentElement[] {
46-
if (firstPartKey === null && chordPartKey === null) {
47-
return null;
48-
}
49-
return _asHTML(firstPartMod, firstPartKey, chordPartMod, chordPartKey, this._labels[OS]);
48+
return _asString(firstPartMod, firstPartKey, chordPartMod, chordPartKey, this.modifierLabels[OS]);
5049
}
5150
}
5251

@@ -177,63 +176,4 @@ function _asString(firstPartMod: Modifiers, firstPartKey: string, chordPartMod:
177176
}
178177

179178
return result;
180-
}
181-
182-
function _pushKey(result: IHTMLContentElement[], str: string, append: string): void {
183-
result.push({
184-
tagName: 'span',
185-
className: 'monaco-kbkey',
186-
text: str
187-
});
188-
if (append) {
189-
result.push({
190-
tagName: 'span',
191-
text: '+'
192-
});
193-
}
194-
}
195-
196-
function _simpleAsHTML(result: IHTMLContentElement[], modifiers: Modifiers, key: string, labels: ModifierLabels): void {
197-
if (key === null) {
198-
return;
199-
}
200-
201-
// translate modifier keys: Ctrl-Shift-Alt-Meta
202-
if (modifiers.ctrlKey) {
203-
_pushKey(result, labels.ctrlKey, labels.separator);
204-
}
205-
206-
if (modifiers.shiftKey) {
207-
_pushKey(result, labels.shiftKey, labels.separator);
208-
}
209-
210-
if (modifiers.altKey) {
211-
_pushKey(result, labels.altKey, labels.separator);
212-
}
213-
214-
if (modifiers.metaKey) {
215-
_pushKey(result, labels.metaKey, labels.separator);
216-
}
217-
218-
// the actual key
219-
_pushKey(result, key, null);
220-
}
221-
222-
function _asHTML(firstPartMod: Modifiers, firstPartKey: string, chordPartMod: Modifiers, chordPartKey: string, labels: ModifierLabels): IHTMLContentElement[] {
223-
let result: IHTMLContentElement[] = [];
224-
_simpleAsHTML(result, firstPartMod, firstPartKey, labels);
225-
226-
if (chordPartKey !== null) {
227-
result.push({
228-
tagName: 'span',
229-
text: ' '
230-
});
231-
_simpleAsHTML(result, chordPartMod, chordPartKey, labels);
232-
}
233-
234-
return [{
235-
tagName: 'span',
236-
className: 'monaco-kb',
237-
children: result
238-
}];
239-
}
179+
}

src/vs/platform/keybinding/common/usLayoutResolvedKeybinding.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
*--------------------------------------------------------------------------------------------*/
55
'use strict';
66

7-
import { IHTMLContentElement } from 'vs/base/common/htmlContent';
87
import { ResolvedKeybinding, KeyCode, KeyCodeUtils, USER_SETTINGS, Keybinding, KeybindingType, SimpleKeybinding } from 'vs/base/common/keyCodes';
9-
import { UILabelProvider, AriaLabelProvider, ElectronAcceleratorLabelProvider, UserSettingsLabelProvider } from 'vs/platform/keybinding/common/keybindingLabels';
8+
import { UILabelProvider, AriaLabelProvider, ElectronAcceleratorLabelProvider, UserSettingsLabelProvider, NO_MODIFIERS } from 'vs/platform/keybinding/common/keybindingLabels';
109
import { OperatingSystem } from 'vs/base/common/platform';
1110

1211
/**
@@ -65,6 +64,12 @@ export class USLayoutResolvedKeybinding extends ResolvedKeybinding {
6564
return UILabelProvider.toLabel(this._firstPart, firstPart, this._chordPart, chordPart, this._os);
6665
}
6766

67+
public getLabelWithoutModifiers(): string {
68+
let firstPart = this._getUILabelForKeybinding(this._firstPart);
69+
let chordPart = this._getUILabelForKeybinding(this._chordPart);
70+
return UILabelProvider.toLabel(NO_MODIFIERS, firstPart, NO_MODIFIERS, chordPart, this._os);
71+
}
72+
6873
private _getAriaLabelForKeybinding(keybinding: SimpleKeybinding): string {
6974
if (!keybinding) {
7075
return null;
@@ -81,10 +86,10 @@ export class USLayoutResolvedKeybinding extends ResolvedKeybinding {
8186
return AriaLabelProvider.toLabel(this._firstPart, firstPart, this._chordPart, chordPart, this._os);
8287
}
8388

84-
public getHTMLLabel(): IHTMLContentElement[] {
85-
let firstPart = this._getUILabelForKeybinding(this._firstPart);
86-
let chordPart = this._getUILabelForKeybinding(this._chordPart);
87-
return UILabelProvider.toHTMLLabel(this._firstPart, firstPart, this._chordPart, chordPart, this._os);
89+
public getAriaLabelWithoutModifiers(): string {
90+
let firstPart = this._getAriaLabelForKeybinding(this._firstPart);
91+
let chordPart = this._getAriaLabelForKeybinding(this._chordPart);
92+
return AriaLabelProvider.toLabel(NO_MODIFIERS, firstPart, NO_MODIFIERS, chordPart, this._os);
8893
}
8994

9095
private _keyCodeToElectronAccelerator(keyCode: KeyCode): string {
@@ -180,6 +185,10 @@ export class USLayoutResolvedKeybinding extends ResolvedKeybinding {
180185
return this._firstPart.metaKey;
181186
}
182187

188+
public getParts(): [ResolvedKeybinding, ResolvedKeybinding] {
189+
return [new USLayoutResolvedKeybinding(this._firstPart, this._os), this._chordPart ? new USLayoutResolvedKeybinding(this._chordPart, this._os) : null];
190+
}
191+
183192
public getDispatchParts(): [string, string] {
184193
let firstPart = this._firstPart ? USLayoutResolvedKeybinding.getDispatchStr(this._firstPart) : null;
185194
let chordPart = this._chordPart ? USLayoutResolvedKeybinding.getDispatchStr(this._chordPart) : null;

0 commit comments

Comments
 (0)