keybindingWidgets.ts 12.7 KB
Newer Older
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import 'vs/css!./media/keybindings';
import * as nls from 'vs/nls';
8
import { OS } from 'vs/base/common/platform';
M
Matt Bierner 已提交
9
import { Disposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
M
Matt Bierner 已提交
10
import { Event, Emitter } from 'vs/base/common/event';
11
import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel';
12 13 14
import { Widget } from 'vs/base/browser/ui/widget';
import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes';
import * as dom from 'vs/base/browser/dom';
S
Sandeep Somavarapu 已提交
15
import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
16 17 18 19
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
20
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser';
21
import { attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler';
B
Benjamin Pasero 已提交
22
import { IThemeService } from 'vs/platform/theme/common/themeService';
23
import { editorWidgetBackground, editorWidgetForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
24
import { ScrollType } from 'vs/editor/common/editorCommon';
25
import { SearchWidget, SearchOptions } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets';
B
Benjamin Pasero 已提交
26
import { withNullAsUndefined } from 'vs/base/common/types';
27

S
Sandeep Somavarapu 已提交
28 29
export interface KeybindingsSearchOptions extends SearchOptions {
	recordEnter?: boolean;
S
Sandeep Somavarapu 已提交
30
	quoteRecordedKeys?: boolean;
S
Sandeep Somavarapu 已提交
31
}
32

S
Sandeep Somavarapu 已提交
33
export class KeybindingsSearchWidget extends SearchWidget {
34

A
Alex Dima 已提交
35 36
	private _firstPart: ResolvedKeybinding | null;
	private _chordPart: ResolvedKeybinding | null;
A
Alex Dima 已提交
37
	private _inputValue: string;
38

M
Matt Bierner 已提交
39
	private readonly recordDisposables = this._register(new DisposableStore());
S
Sandeep Somavarapu 已提交
40

A
Alex Dima 已提交
41 42
	private _onKeybinding = this._register(new Emitter<[ResolvedKeybinding | null, ResolvedKeybinding | null]>());
	readonly onKeybinding: Event<[ResolvedKeybinding | null, ResolvedKeybinding | null]> = this._onKeybinding.event;
43 44

	private _onEnter = this._register(new Emitter<void>());
R
Rob Lourens 已提交
45
	readonly onEnter: Event<void> = this._onEnter.event;
46 47

	private _onEscape = this._register(new Emitter<void>());
R
Rob Lourens 已提交
48
	readonly onEscape: Event<void> = this._onEscape.event;
49

A
Alex Dima 已提交
50
	private _onBlur = this._register(new Emitter<void>());
R
Rob Lourens 已提交
51
	readonly onBlur: Event<void> = this._onBlur.event;
A
Alex Dima 已提交
52

S
Sandeep Somavarapu 已提交
53 54
	constructor(parent: HTMLElement, options: SearchOptions,
		@IContextViewService contextViewService: IContextViewService,
55
		@IKeybindingService private readonly keybindingService: IKeybindingService,
S
Sandeep Somavarapu 已提交
56
		@IInstantiationService instantiationService: IInstantiationService,
B
Benjamin Pasero 已提交
57
		@IThemeService themeService: IThemeService
58
	) {
S
Sandeep Somavarapu 已提交
59
		super(parent, options, contextViewService, instantiationService, themeService);
B
Benjamin Pasero 已提交
60
		this._register(attachInputBoxStyler(this.inputBox, themeService));
S
Sandeep Somavarapu 已提交
61
		this._register(toDisposable(() => this.stopRecordingKeys()));
62 63 64
		this._firstPart = null;
		this._chordPart = null;
		this._inputValue = '';
S
Sandeep Somavarapu 已提交
65 66 67

		this._reset();
	}
A
Alex Dima 已提交
68

S
Sandeep Somavarapu 已提交
69 70 71 72 73 74
	clear(): void {
		this._reset();
		super.clear();
	}

	startRecordingKeys(): void {
M
Matt Bierner 已提交
75 76 77
		this.recordDisposables.add(dom.addDisposableListener(this.inputBox.inputElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => this._onKeyDown(new StandardKeyboardEvent(e))));
		this.recordDisposables.add(dom.addDisposableListener(this.inputBox.inputElement, dom.EventType.BLUR, () => this._onBlur.fire()));
		this.recordDisposables.add(dom.addDisposableListener(this.inputBox.inputElement, dom.EventType.INPUT, () => {
A
Alex Dima 已提交
78 79
			// Prevent other characters from showing up
			this.setInputValue(this._inputValue);
S
Sandeep Somavarapu 已提交
80 81
		}));
	}
A
Alex Dima 已提交
82

S
Sandeep Somavarapu 已提交
83 84
	stopRecordingKeys(): void {
		this._reset();
S
Sandeep Somavarapu 已提交
85
		this.recordDisposables.clear();
86 87
	}

R
Rob Lourens 已提交
88
	setInputValue(value: string): void {
A
Alex Dima 已提交
89 90 91 92
		this._inputValue = value;
		this.inputBox.value = this._inputValue;
	}

S
Sandeep Somavarapu 已提交
93
	private _reset() {
S
Sandeep Somavarapu 已提交
94 95 96 97
		this._firstPart = null;
		this._chordPart = null;
	}

98
	private _onKeyDown(keyboardEvent: IKeyboardEvent): void {
99 100
		keyboardEvent.preventDefault();
		keyboardEvent.stopPropagation();
S
Sandeep Somavarapu 已提交
101 102
		const options = this.options as KeybindingsSearchOptions;
		if (!options.recordEnter && keyboardEvent.equals(KeyCode.Enter)) {
A
Alex Dima 已提交
103 104 105 106 107 108
			this._onEnter.fire();
			return;
		}
		if (keyboardEvent.equals(KeyCode.Escape)) {
			this._onEscape.fire();
			return;
109 110 111 112 113
		}
		this.printKeybinding(keyboardEvent);
	}

	private printKeybinding(keyboardEvent: IKeyboardEvent): void {
114
		const keybinding = this.keybindingService.resolveKeyboardEvent(keyboardEvent);
115
		const info = `code: ${keyboardEvent.browserEvent.code}, keyCode: ${keyboardEvent.browserEvent.keyCode}, key: ${keyboardEvent.browserEvent.key} => UI: ${keybinding.getAriaLabel()}, user settings: ${keybinding.getUserSettingsLabel()}, dispatch: ${keybinding.getDispatchParts()[0]}`;
S
Sandeep Somavarapu 已提交
116
		const options = this.options as KeybindingsSearchOptions;
117

S
Sandeep Somavarapu 已提交
118 119 120 121
		const hasFirstPart = (this._firstPart && this._firstPart.getDispatchParts()[0] !== null);
		const hasChordPart = (this._chordPart && this._chordPart.getDispatchParts()[0] !== null);
		if (hasFirstPart && hasChordPart) {
			// Reset
122
			this._firstPart = keybinding;
S
Sandeep Somavarapu 已提交
123 124 125 126 127
			this._chordPart = null;
		} else if (!hasFirstPart) {
			this._firstPart = keybinding;
		} else {
			this._chordPart = keybinding;
128 129 130 131
		}

		let value = '';
		if (this._firstPart) {
A
Alex Dima 已提交
132
			value = (this._firstPart.getUserSettingsLabel() || '');
133 134 135 136
		}
		if (this._chordPart) {
			value = value + ' ' + this._chordPart.getUserSettingsLabel();
		}
S
Sandeep Somavarapu 已提交
137
		this.setInputValue(options.quoteRecordedKeys ? `"${value}"` : value);
138 139 140

		this.inputBox.inputElement.title = info;
		this._onKeybinding.fire([this._firstPart, this._chordPart]);
141 142 143 144 145
	}
}

export class DefineKeybindingWidget extends Widget {

146
	private static readonly WIDTH = 400;
147
	private static readonly HEIGHT = 110;
148 149

	private _domNode: FastDomNode<HTMLElement>;
S
Sandeep Somavarapu 已提交
150
	private _keybindingInputWidget: KeybindingsSearchWidget;
151
	private _outputNode: HTMLElement;
S
Sandeep Somavarapu 已提交
152
	private _showExistingKeybindingsNode: HTMLElement;
153

154 155
	private _firstPart: ResolvedKeybinding | null = null;
	private _chordPart: ResolvedKeybinding | null = null;
156 157 158 159
	private _isVisible: boolean = false;

	private _onHide = this._register(new Emitter<void>());

A
Alex Dima 已提交
160 161
	private _onDidChange = this._register(new Emitter<string>());
	onDidChange: Event<string> = this._onDidChange.event;
162

A
Alex Dima 已提交
163 164
	private _onShowExistingKeybindings = this._register(new Emitter<string | null>());
	readonly onShowExistingKeybidings: Event<string | null> = this._onShowExistingKeybindings.event;
165

166
	constructor(
167
		parent: HTMLElement | null,
168 169
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@IThemeService private readonly themeService: IThemeService
170 171
	) {
		super();
172 173 174 175 176 177 178 179 180 181 182 183 184 185

		this._domNode = createFastDomNode(document.createElement('div'));
		this._domNode.setDisplay('none');
		this._domNode.setClassName('defineKeybindingWidget');
		this._domNode.setWidth(DefineKeybindingWidget.WIDTH);
		this._domNode.setHeight(DefineKeybindingWidget.HEIGHT);

		const message = nls.localize('defineKeybinding.initial', "Press desired key combination and then press ENTER.");
		dom.append(this._domNode.domNode, dom.$('.message', undefined, message));

		this._register(attachStylerCallback(this.themeService, { editorWidgetBackground, editorWidgetForeground, widgetShadow }, colors => {
			if (colors.editorWidgetBackground) {
				this._domNode.domNode.style.backgroundColor = colors.editorWidgetBackground.toString();
			} else {
M
Matt Bierner 已提交
186
				this._domNode.domNode.style.backgroundColor = '';
187 188 189 190 191 192 193 194 195 196
			}
			if (colors.editorWidgetForeground) {
				this._domNode.domNode.style.color = colors.editorWidgetForeground.toString();
			} else {
				this._domNode.domNode.style.color = null;
			}

			if (colors.widgetShadow) {
				this._domNode.domNode.style.boxShadow = `0 2px 8px ${colors.widgetShadow}`;
			} else {
M
Matt Bierner 已提交
197
				this._domNode.domNode.style.boxShadow = '';
198 199 200 201 202 203 204 205 206 207 208 209 210
			}
		}));

		this._keybindingInputWidget = this._register(this.instantiationService.createInstance(KeybindingsSearchWidget, this._domNode.domNode, { ariaLabel: message }));
		this._keybindingInputWidget.startRecordingKeys();
		this._register(this._keybindingInputWidget.onKeybinding(keybinding => this.onKeybinding(keybinding)));
		this._register(this._keybindingInputWidget.onEnter(() => this.hide()));
		this._register(this._keybindingInputWidget.onEscape(() => this.onCancel()));
		this._register(this._keybindingInputWidget.onBlur(() => this.onCancel()));

		this._outputNode = dom.append(this._domNode.domNode, dom.$('.output'));
		this._showExistingKeybindingsNode = dom.append(this._domNode.domNode, dom.$('.existing'));

211 212 213 214 215 216 217
		if (parent) {
			dom.append(parent, this._domNode.domNode);
		}
	}

	get domNode(): HTMLElement {
		return this._domNode.domNode;
218 219
	}

A
Alex Dima 已提交
220
	define(): Promise<string | null> {
S
Sandeep Somavarapu 已提交
221
		this._keybindingInputWidget.clear();
A
Alex Dima 已提交
222
		return new Promise<string | null>((c) => {
223 224 225 226
			if (!this._isVisible) {
				this._isVisible = true;
				this._domNode.setDisplay('block');

227 228
				this._firstPart = null;
				this._chordPart = null;
A
Alex Dima 已提交
229
				this._keybindingInputWidget.setInputValue('');
230
				dom.clearNode(this._outputNode);
S
Sandeep Somavarapu 已提交
231
				dom.clearNode(this._showExistingKeybindingsNode);
A
Alex Dima 已提交
232
				this._keybindingInputWidget.focus();
233 234
			}
			const disposable = this._onHide.event(() => {
S
Sandeep Somavarapu 已提交
235
				c(this.getUserSettingsLabel());
236 237 238 239 240
				disposable.dispose();
			});
		});
	}

241
	layout(layout: dom.Dimension): void {
R
Rob Lourens 已提交
242
		const top = Math.round((layout.height - DefineKeybindingWidget.HEIGHT) / 2);
243 244
		this._domNode.setTop(top);

R
Rob Lourens 已提交
245
		const left = Math.round((layout.width - DefineKeybindingWidget.WIDTH) / 2);
246 247 248
		this._domNode.setLeft(left);
	}

S
Sandeep Somavarapu 已提交
249 250
	printExisting(numberOfExisting: number): void {
		if (numberOfExisting > 0) {
S
Sandeep Somavarapu 已提交
251 252 253 254 255 256 257
			const existingElement = dom.$('span.existingText');
			const text = numberOfExisting === 1 ? nls.localize('defineKeybinding.oneExists', "1 existing command has this keybinding", numberOfExisting) : nls.localize('defineKeybinding.existing', "{0} existing commands have this keybinding", numberOfExisting);
			dom.append(existingElement, document.createTextNode(text));
			this._showExistingKeybindingsNode.appendChild(existingElement);
			existingElement.onmousedown = (e) => { e.preventDefault(); };
			existingElement.onmouseup = (e) => { e.preventDefault(); };
			existingElement.onclick = () => { this._onShowExistingKeybindings.fire(this.getUserSettingsLabel()); };
S
Sandeep Somavarapu 已提交
258 259 260
		}
	}

A
Alex Dima 已提交
261
	private onKeybinding(keybinding: [ResolvedKeybinding | null, ResolvedKeybinding | null]): void {
262 263 264
		const [firstPart, chordPart] = keybinding;
		this._firstPart = firstPart;
		this._chordPart = chordPart;
265
		dom.clearNode(this._outputNode);
S
Sandeep Somavarapu 已提交
266
		dom.clearNode(this._showExistingKeybindingsNode);
B
Benjamin Pasero 已提交
267
		new KeybindingLabel(this._outputNode, OS).set(withNullAsUndefined(this._firstPart));
268
		if (this._chordPart) {
269
			this._outputNode.appendChild(document.createTextNode(nls.localize('defineKeybinding.chordsTo', "chord to")));
270
			new KeybindingLabel(this._outputNode, OS).set(this._chordPart);
271
		}
S
Sandeep Somavarapu 已提交
272 273 274 275
		const label = this.getUserSettingsLabel();
		if (label) {
			this._onDidChange.fire(label);
		}
276 277
	}

A
Alex Dima 已提交
278 279
	private getUserSettingsLabel(): string | null {
		let label: string | null = null;
S
Sandeep Somavarapu 已提交
280 281
		if (this._firstPart) {
			label = this._firstPart.getUserSettingsLabel();
282
			if (this._chordPart) {
S
Sandeep Somavarapu 已提交
283
				label = label + ' ' + this._chordPart.getUserSettingsLabel();
284
			}
285
		}
S
Sandeep Somavarapu 已提交
286
		return label;
287 288
	}

289
	private onCancel(): void {
290 291
		this._firstPart = null;
		this._chordPart = null;
292 293 294 295
		this.hide();
	}

	private hide(): void {
296
		this._domNode.setDisplay('none');
297 298 299
		this._isVisible = false;
		this._onHide.fire();
	}
300 301 302 303
}

export class DefineKeybindingOverlayWidget extends Disposable implements IOverlayWidget {

304
	private static readonly ID = 'editor.contrib.defineKeybindingWidget';
305 306 307 308 309 310 311 312 313 314 315 316

	private readonly _widget: DefineKeybindingWidget;

	constructor(private _editor: ICodeEditor,
		@IInstantiationService instantiationService: IInstantiationService
	) {
		super();

		this._widget = instantiationService.createInstance(DefineKeybindingWidget, null);
		this._editor.addOverlayWidget(this);
	}

R
Rob Lourens 已提交
317
	getId(): string {
318 319 320
		return DefineKeybindingOverlayWidget.ID;
	}

R
Rob Lourens 已提交
321
	getDomNode(): HTMLElement {
322 323 324
		return this._widget.domNode;
	}

R
Rob Lourens 已提交
325
	getPosition(): IOverlayWidgetPosition {
326 327 328 329 330
		return {
			preference: null
		};
	}

R
Rob Lourens 已提交
331
	dispose(): void {
332 333 334 335
		this._editor.removeOverlayWidget(this);
		super.dispose();
	}

A
Alex Dima 已提交
336 337 338 339
	start(): Promise<string | null> {
		if (this._editor.hasModel()) {
			this._editor.revealPositionInCenterIfOutsideViewport(this._editor.getPosition(), ScrollType.Smooth);
		}
340
		const layoutInfo = this._editor.getLayoutInfo();
341
		this._widget.layout(new dom.Dimension(layoutInfo.width, layoutInfo.height));
342 343
		return this._widget.define();
	}
A
Alex Dima 已提交
344
}