quickInput.ts 52.1 KB
Newer Older
C
Christof Marti 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

B
Benjamin Pasero 已提交
6
import 'vs/css!./media/quickInput';
7
import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent, NO_KEY_MODS, ItemActivation } from 'vs/base/parts/quickinput/common/quickInput';
C
Christof Marti 已提交
8
import * as dom from 'vs/base/browser/dom';
9
import { CancellationToken } from 'vs/base/common/cancellation';
B
Benjamin Pasero 已提交
10
import { QuickInputList, QuickInputListFocus } from './quickInputList';
11
import { QuickInputBox } from './quickInputBox';
12
import { KeyCode } from 'vs/base/common/keyCodes';
C
Christof Marti 已提交
13
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
14
import { localize } from 'vs/nls';
15 16
import { CountBadge, ICountBadgetyles } from 'vs/base/browser/ui/countBadge/countBadge';
import { ProgressBar, IProgressBarStyles } from 'vs/base/browser/ui/progressbar/progressbar';
J
Joao Moreno 已提交
17
import { Emitter, Event } from 'vs/base/common/event';
18
import { Button, IButtonStyles } from 'vs/base/browser/ui/button/button';
19
import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
C
Christof Marti 已提交
20
import Severity from 'vs/base/common/severity';
21
import { ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
C
Christof Marti 已提交
22
import { Action } from 'vs/base/common/actions';
23
import { equals } from 'vs/base/common/arrays';
24
import { TimeoutTimer } from 'vs/base/common/async';
25 26 27 28 29
import { getIconClass } from 'vs/base/parts/quickinput/browser/quickInputUtils';
import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list';
import { List, IListOptions, IListStyles } from 'vs/base/browser/ui/list/listWidget';
import { IInputBoxStyles } from 'vs/base/browser/ui/inputbox/inputBox';
import { Color } from 'vs/base/common/color';
M
Martin Aeschlimann 已提交
30
import { registerIcon, Codicon } from 'vs/base/common/codicons';
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61

export interface IQuickInputOptions {
	idPrefix: string;
	container: HTMLElement;
	ignoreFocusOut(): boolean;
	isScreenReaderOptimized(): boolean;
	backKeybindingLabel(): string | undefined;
	setContextKey(id?: string): void;
	returnFocus(): void;
	createList<T>(
		user: string,
		container: HTMLElement,
		delegate: IListVirtualDelegate<T>,
		renderers: IListRenderer<T, any>[],
		options: IListOptions<T>,
	): List<T>;
	styles: IQuickInputStyles;
}

export interface IQuickInputStyles {
	widget: IQuickInputWidgetStyles;
	inputBox: IInputBoxStyles;
	countBadge: ICountBadgetyles;
	button: IButtonStyles;
	progressBar: IProgressBarStyles;
	list: IListStyles & { listInactiveFocusForeground?: Color; pickerGroupBorder?: Color; pickerGroupForeground?: Color; };
}

export interface IQuickInputWidgetStyles {
	quickInputBackground?: Color;
	quickInputForeground?: Color;
62
	quickInputTitleBackground?: Color;
63 64 65
	contrastBorder?: Color;
	widgetShadow?: Color;
}
C
Christof Marti 已提交
66 67 68

const $ = dom.$;

C
Christof Marti 已提交
69 70
type Writeable<T> = { -readonly [P in keyof T]: T[P] };

M
Martin Aeschlimann 已提交
71 72 73

const backButtonIcon = registerIcon('quick-input-back', Codicon.arrowLeft);

C
Christof Marti 已提交
74
const backButton = {
M
Martin Aeschlimann 已提交
75
	iconClass: backButtonIcon.classNames,
C
Christof Marti 已提交
76 77 78 79
	tooltip: localize('quickInput.back', "Back"),
	handle: -1 // TODO
};

80
interface QuickInputUI {
C
Christof Marti 已提交
81
	container: HTMLElement;
82
	styleSheet: HTMLStyleElement;
C
Christof Marti 已提交
83
	leftActionBar: ActionBar;
84
	titleBar: HTMLElement;
C
Christof Marti 已提交
85
	title: HTMLElement;
86
	description: HTMLElement;
C
Christof Marti 已提交
87
	rightActionBar: ActionBar;
88
	checkAll: HTMLInputElement;
89
	filterContainer: HTMLElement;
90
	inputBox: QuickInputBox;
91
	visibleCountContainer: HTMLElement;
92
	visibleCount: CountBadge;
93
	countContainer: HTMLElement;
94
	count: CountBadge;
95 96
	okContainer: HTMLElement;
	ok: Button;
97
	message: HTMLElement;
98
	customButtonContainer: HTMLElement;
99
	customButton: Button;
100
	progressBar: ProgressBar;
101
	list: QuickInputList;
102
	onDidAccept: Event<void>;
103
	onDidCustom: Event<void>;
C
Christof Marti 已提交
104
	onDidTriggerButton: Event<IQuickInputButton>;
105
	ignoreFocusOut: boolean;
C
Christof Marti 已提交
106
	keyMods: Writeable<IKeyMods>;
107
	isScreenReaderOptimized(): boolean;
108 109
	show(controller: QuickInput): void;
	setVisibilities(visibilities: Visibilities): void;
110
	setComboboxAccessibility(enabled: boolean): void;
C
Christof Marti 已提交
111
	setEnabled(enabled: boolean): void;
C
Christof Marti 已提交
112
	setContextKey(contextKey?: string): void;
113
	hide(): void;
114 115
}

116
type Visibilities = {
C
Christof Marti 已提交
117
	title?: boolean;
118
	description?: boolean;
119 120
	checkAll?: boolean;
	inputBox?: boolean;
121
	visibleCount?: boolean;
122 123 124 125
	count?: boolean;
	message?: boolean;
	list?: boolean;
	ok?: boolean;
126
	customButton?: boolean;
127
	progressBar?: boolean;
128 129
};

130
class QuickInput extends Disposable implements IQuickInput {
131

132
	private _title: string | undefined;
133
	private _description: string | undefined;
134 135
	private _steps: number | undefined;
	private _totalSteps: number | undefined;
136 137
	protected visible = false;
	private _enabled = true;
138
	private _contextKey: string | undefined;
139 140
	private _busy = false;
	private _ignoreFocusOut = false;
C
Christof Marti 已提交
141 142
	private _buttons: IQuickInputButton[] = [];
	private buttonsUpdated = false;
143 144 145
	private readonly onDidTriggerButtonEmitter = this._register(new Emitter<IQuickInputButton>());
	private readonly onDidHideEmitter = this._register(new Emitter<void>());
	private readonly onDisposeEmitter = this._register(new Emitter<void>());
146

147
	protected readonly visibleDisposables = this._register(new DisposableStore());
148

149
	private busyDelay: TimeoutTimer | undefined;
150

151 152 153 154
	constructor(
		protected ui: QuickInputUI
	) {
		super();
155 156
	}

C
Christof Marti 已提交
157 158 159 160
	get title() {
		return this._title;
	}

161
	set title(title: string | undefined) {
C
Christof Marti 已提交
162 163 164 165
		this._title = title;
		this.update();
	}

166 167 168 169 170 171 172 173 174
	get description() {
		return this._description;
	}

	set description(description: string | undefined) {
		this._description = description;
		this.update();
	}

C
Christof Marti 已提交
175 176 177 178
	get step() {
		return this._steps;
	}

179
	set step(step: number | undefined) {
C
Christof Marti 已提交
180 181 182 183 184 185 186 187
		this._steps = step;
		this.update();
	}

	get totalSteps() {
		return this._totalSteps;
	}

188
	set totalSteps(totalSteps: number | undefined) {
C
Christof Marti 已提交
189 190 191 192
		this._totalSteps = totalSteps;
		this.update();
	}

193 194 195 196 197 198
	get enabled() {
		return this._enabled;
	}

	set enabled(enabled: boolean) {
		this._enabled = enabled;
C
Christof Marti 已提交
199
		this.update();
200 201
	}

C
Christof Marti 已提交
202 203 204 205
	get contextKey() {
		return this._contextKey;
	}

206
	set contextKey(contextKey: string | undefined) {
C
Christof Marti 已提交
207 208 209 210
		this._contextKey = contextKey;
		this.update();
	}

211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
	get busy() {
		return this._busy;
	}

	set busy(busy: boolean) {
		this._busy = busy;
		this.update();
	}

	get ignoreFocusOut() {
		return this._ignoreFocusOut;
	}

	set ignoreFocusOut(ignoreFocusOut: boolean) {
		this._ignoreFocusOut = ignoreFocusOut;
		this.update();
	}

C
Christof Marti 已提交
229 230 231 232 233 234 235 236 237 238
	get buttons() {
		return this._buttons;
	}

	set buttons(buttons: IQuickInputButton[]) {
		this._buttons = buttons;
		this.buttonsUpdated = true;
		this.update();
	}

B
Benjamin Pasero 已提交
239
	readonly onDidTriggerButton = this.onDidTriggerButtonEmitter.event;
C
Christof Marti 已提交
240

241 242 243 244
	show(): void {
		if (this.visible) {
			return;
		}
245
		this.visibleDisposables.add(
C
Christof Marti 已提交
246 247 248 249 250 251
			this.ui.onDidTriggerButton(button => {
				if (this.buttons.indexOf(button) !== -1) {
					this.onDidTriggerButtonEmitter.fire(button);
				}
			}),
		);
252 253 254 255 256 257 258 259 260 261 262 263 264 265
		this.ui.show(this);
		this.visible = true;
		this.update();
	}

	hide(): void {
		if (!this.visible) {
			return;
		}
		this.ui.hide();
	}

	didHide(): void {
		this.visible = false;
266
		this.visibleDisposables.clear();
267 268 269
		this.onDidHideEmitter.fire();
	}

B
Benjamin Pasero 已提交
270
	readonly onDidHide = this.onDidHideEmitter.event;
271 272 273 274 275

	protected update() {
		if (!this.visible) {
			return;
		}
C
Christof Marti 已提交
276
		const title = this.getTitle();
277
		if (title && this.ui.title.textContent !== title) {
C
Christof Marti 已提交
278
			this.ui.title.textContent = title;
279 280
		} else if (!title && this.ui.title.innerHTML !== '&nbsp;') {
			this.ui.title.innerHTML = '&nbsp;';
C
Christof Marti 已提交
281
		}
282 283 284 285
		const description = this.getDescription();
		if (this.ui.description.textContent !== description) {
			this.ui.description.textContent = description;
		}
286
		if (this.busy && !this.busyDelay) {
287 288
			this.busyDelay = new TimeoutTimer();
			this.busyDelay.setIfNotSet(() => {
C
Christof Marti 已提交
289 290 291
				if (this.visible) {
					this.ui.progressBar.infinite();
				}
292
			}, 800);
293 294 295 296
		}
		if (!this.busy && this.busyDelay) {
			this.ui.progressBar.stop();
			this.busyDelay.cancel();
297
			this.busyDelay = undefined;
298
		}
C
Christof Marti 已提交
299 300
		if (this.buttonsUpdated) {
			this.buttonsUpdated = false;
C
Christof Marti 已提交
301 302 303
			this.ui.leftActionBar.clear();
			const leftButtons = this.buttons.filter(button => button === backButton);
			this.ui.leftActionBar.push(leftButtons.map((button, index) => {
304
				const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, async () => {
M
Matt Bierner 已提交
305 306
					this.onDidTriggerButtonEmitter.fire(button);
				});
307
				action.tooltip = button.tooltip || '';
C
Christof Marti 已提交
308 309 310 311 312
				return action;
			}), { icon: true, label: false });
			this.ui.rightActionBar.clear();
			const rightButtons = this.buttons.filter(button => button !== backButton);
			this.ui.rightActionBar.push(rightButtons.map((button, index) => {
313
				const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, async () => {
M
Matt Bierner 已提交
314 315
					this.onDidTriggerButtonEmitter.fire(button);
				});
316
				action.tooltip = button.tooltip || '';
C
Christof Marti 已提交
317 318 319
				return action;
			}), { icon: true, label: false });
		}
320
		this.ui.ignoreFocusOut = this.ignoreFocusOut;
C
Christof Marti 已提交
321
		this.ui.setEnabled(this.enabled);
C
Christof Marti 已提交
322
		this.ui.setContextKey(this.contextKey);
323 324
	}

C
Christof Marti 已提交
325 326
	private getTitle() {
		if (this.title && this.step) {
C
Christof Marti 已提交
327
			return `${this.title} (${this.getSteps()})`;
C
Christof Marti 已提交
328 329 330 331 332 333 334 335 336 337
		}
		if (this.title) {
			return this.title;
		}
		if (this.step) {
			return this.getSteps();
		}
		return '';
	}

338 339 340 341
	private getDescription() {
		return this.description || '';
	}

C
Christof Marti 已提交
342 343
	private getSteps() {
		if (this.step && this.totalSteps) {
C
Christof Marti 已提交
344
			return localize('quickInput.steps', "{0}/{1}", this.step, this.totalSteps);
C
Christof Marti 已提交
345 346 347 348 349 350 351
		}
		if (this.step) {
			return String(this.step);
		}
		return '';
	}

352 353 354 355
	protected showMessageDecoration(severity: Severity) {
		this.ui.inputBox.showDecoration(severity);
		if (severity === Severity.Error) {
			const styles = this.ui.inputBox.stylesForType(severity);
356
			this.ui.message.style.color = styles.foreground ? `${styles.foreground}` : '';
M
Matt Bierner 已提交
357 358
			this.ui.message.style.backgroundColor = styles.background ? `${styles.background}` : '';
			this.ui.message.style.border = styles.border ? `1px solid ${styles.border}` : '';
A
Alex Ross 已提交
359
			this.ui.message.style.paddingBottom = '4px';
360
		} else {
361
			this.ui.message.style.color = '';
362 363
			this.ui.message.style.backgroundColor = '';
			this.ui.message.style.border = '';
A
Alex Ross 已提交
364
			this.ui.message.style.paddingBottom = '';
365 366 367
		}
	}

B
Benjamin Pasero 已提交
368 369
	readonly onDispose = this.onDisposeEmitter.event;

370
	dispose(): void {
371
		this.hide();
B
Benjamin Pasero 已提交
372 373
		this.onDisposeEmitter.fire();

374
		super.dispose();
375
	}
376 377
}

378
class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPick<T> {
379

380
	private static readonly DEFAULT_ARIA_LABEL = localize('quickInputBox.ariaLabel', "Type to narrow down results.");
381

382
	private _value = '';
383
	private _ariaLabel = QuickPick.DEFAULT_ARIA_LABEL;
384
	private _placeholder: string | undefined;
385
	private readonly onDidChangeValueEmitter = this._register(new Emitter<string>());
386
	private readonly onDidAcceptEmitter = this._register(new Emitter<IQuickPickAcceptEvent>());
387
	private readonly onDidCustomEmitter = this._register(new Emitter<void>());
388
	private _items: Array<T | IQuickPickSeparator> = [];
389 390
	private itemsUpdated = false;
	private _canSelectMany = false;
391
	private _canAcceptInBackground = false;
C
Christof Marti 已提交
392 393
	private _matchOnDescription = false;
	private _matchOnDetail = false;
A
Alex Ross 已提交
394
	private _matchOnLabel = true;
395
	private _sortByLabel = true;
A
Alex Ross 已提交
396
	private _autoFocusOnList = true;
B
Benjamin Pasero 已提交
397
	private _itemActivation = this.ui.isScreenReaderOptimized() ? ItemActivation.NONE /* https://github.com/microsoft/vscode/issues/57501 */ : ItemActivation.FIRST;
398
	private _activeItems: T[] = [];
399
	private activeItemsUpdated = false;
400
	private activeItemsToConfirm: T[] | null = [];
401
	private readonly onDidChangeActiveEmitter = this._register(new Emitter<T[]>());
402
	private _selectedItems: T[] = [];
403
	private selectedItemsUpdated = false;
404
	private selectedItemsToConfirm: T[] | null = [];
405 406
	private readonly onDidChangeSelectionEmitter = this._register(new Emitter<T[]>());
	private readonly onDidTriggerItemButtonEmitter = this._register(new Emitter<IQuickPickItemButtonEvent<T>>());
407
	private _valueSelection: Readonly<[number, number]> | undefined;
A
Alex Ross 已提交
408
	private valueSelectionUpdated = true;
409
	private _validationMessage: string | undefined;
410
	private _ok: boolean | 'default' = 'default';
411 412 413
	private _customButton = false;
	private _customButtonLabel: string | undefined;
	private _customButtonHover: string | undefined;
414
	private _quickNavigate: IQuickNavigateConfiguration | undefined;
415
	private _hideInput: boolean | undefined;
C
Christof Marti 已提交
416

417 418 419 420 421 422 423 424
	get quickNavigate() {
		return this._quickNavigate;
	}

	set quickNavigate(quickNavigate: IQuickNavigateConfiguration | undefined) {
		this._quickNavigate = quickNavigate;
		this.update();
	}
425

426 427 428
	get value() {
		return this._value;
	}
429

430 431 432 433
	set value(value: string) {
		this._value = value || '';
		this.update();
	}
434

435 436
	filterValue = (value: string) => value;

437 438 439 440 441 442 443 444 445
	set ariaLabel(ariaLabel: string) {
		this._ariaLabel = ariaLabel || QuickPick.DEFAULT_ARIA_LABEL;
		this.update();
	}

	get ariaLabel() {
		return this._ariaLabel;
	}

446 447 448 449
	get placeholder() {
		return this._placeholder;
	}

450
	set placeholder(placeholder: string | undefined) {
451
		this._placeholder = placeholder;
452 453
		this.update();
	}
454

455
	onDidChangeValue = this.onDidChangeValueEmitter.event;
456 457 458

	onDidAccept = this.onDidAcceptEmitter.event;

459 460
	onDidCustom = this.onDidCustomEmitter.event;

461 462 463 464
	get items() {
		return this._items;
	}

465
	set items(items: Array<T | IQuickPickSeparator>) {
466 467 468 469 470 471 472 473 474 475 476 477 478 479
		this._items = items;
		this.itemsUpdated = true;
		this.update();
	}

	get canSelectMany() {
		return this._canSelectMany;
	}

	set canSelectMany(canSelectMany: boolean) {
		this._canSelectMany = canSelectMany;
		this.update();
	}

480 481 482 483 484 485 486 487
	get canAcceptInBackground() {
		return this._canAcceptInBackground;
	}

	set canAcceptInBackground(canAcceptInBackground: boolean) {
		this._canAcceptInBackground = canAcceptInBackground;
	}

488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505
	get matchOnDescription() {
		return this._matchOnDescription;
	}

	set matchOnDescription(matchOnDescription: boolean) {
		this._matchOnDescription = matchOnDescription;
		this.update();
	}

	get matchOnDetail() {
		return this._matchOnDetail;
	}

	set matchOnDetail(matchOnDetail: boolean) {
		this._matchOnDetail = matchOnDetail;
		this.update();
	}

A
Alex Ross 已提交
506 507 508 509 510 511 512 513 514
	get matchOnLabel() {
		return this._matchOnLabel;
	}

	set matchOnLabel(matchOnLabel: boolean) {
		this._matchOnLabel = matchOnLabel;
		this.update();
	}

515 516 517 518 519 520 521 522 523
	get sortByLabel() {
		return this._sortByLabel;
	}

	set sortByLabel(sortByLabel: boolean) {
		this._sortByLabel = sortByLabel;
		this.update();
	}

A
Alex Ross 已提交
524 525 526 527 528 529 530 531 532
	get autoFocusOnList() {
		return this._autoFocusOnList;
	}

	set autoFocusOnList(autoFocusOnList: boolean) {
		this._autoFocusOnList = autoFocusOnList;
		this.update();
	}

533 534
	get itemActivation() {
		return this._itemActivation;
535 536
	}

537 538
	set itemActivation(itemActivation: ItemActivation) {
		this._itemActivation = itemActivation;
539 540
	}

541 542 543 544
	get activeItems() {
		return this._activeItems;
	}

545
	set activeItems(activeItems: T[]) {
546 547 548 549 550
		this._activeItems = activeItems;
		this.activeItemsUpdated = true;
		this.update();
	}

551 552 553 554 555 556
	onDidChangeActive = this.onDidChangeActiveEmitter.event;

	get selectedItems() {
		return this._selectedItems;
	}

557
	set selectedItems(selectedItems: T[]) {
558 559 560 561 562
		this._selectedItems = selectedItems;
		this.selectedItemsUpdated = true;
		this.update();
	}

C
Christof Marti 已提交
563
	get keyMods() {
564 565 566 567 568 569 570
		if (this._quickNavigate) {
			// Disable keyMods when quick navigate is enabled
			// because in this model the interaction is purely
			// keyboard driven and Ctrl/Alt are typically
			// pressed and hold during this interaction.
			return NO_KEY_MODS;
		}
C
Christof Marti 已提交
571 572 573
		return this.ui.keyMods;
	}

A
Alex Ross 已提交
574 575 576 577 578 579
	set valueSelection(valueSelection: Readonly<[number, number]>) {
		this._valueSelection = valueSelection;
		this.valueSelectionUpdated = true;
		this.update();
	}

580 581 582 583
	get validationMessage() {
		return this._validationMessage;
	}

584
	set validationMessage(validationMessage: string | undefined) {
585 586 587 588
		this._validationMessage = validationMessage;
		this.update();
	}

589 590 591 592 593 594 595 596 597 598 599 600 601
	get customButton() {
		return this._customButton;
	}

	set customButton(showCustomButton: boolean) {
		this._customButton = showCustomButton;
		this.update();
	}

	get customLabel() {
		return this._customButtonLabel;
	}

602
	set customLabel(label: string | undefined) {
603 604 605 606
		this._customButtonLabel = label;
		this.update();
	}

A
Alex Ross 已提交
607 608 609 610
	get customHover() {
		return this._customButtonHover;
	}

611
	set customHover(hover: string | undefined) {
A
Alex Ross 已提交
612 613 614 615
		this._customButtonHover = hover;
		this.update();
	}

616 617 618 619
	get ok() {
		return this._ok;
	}

620
	set ok(showOkButton: boolean | 'default') {
621 622 623 624
		this._ok = showOkButton;
		this.update();
	}

625
	inputHasFocus(): boolean {
626 627 628
		return this.visible ? this.ui.inputBox.hasFocus() : false;
	}

629
	focusOnInput() {
630 631 632
		this.ui.inputBox.setFocus();
	}

633 634 635 636 637 638 639 640 641
	get hideInput() {
		return !!this._hideInput;
	}

	set hideInput(hideInput: boolean) {
		this._hideInput = hideInput;
		this.update();
	}

642 643
	onDidChangeSelection = this.onDidChangeSelectionEmitter.event;

C
Christof Marti 已提交
644 645
	onDidTriggerItemButton = this.onDidTriggerItemButtonEmitter.event;

A
Alex Ross 已提交
646 647
	private trySelectFirst() {
		if (this.autoFocusOnList) {
B
Benjamin Pasero 已提交
648
			if (!this.canSelectMany) {
B
Benjamin Pasero 已提交
649
				this.ui.list.focus(QuickInputListFocus.First);
A
Alex Ross 已提交
650 651 652 653
			}
		}
	}

654 655
	show() {
		if (!this.visible) {
656
			this.visibleDisposables.add(
657 658 659
				this.ui.inputBox.onDidChange(value => {
					if (value === this.value) {
						return;
660
					}
661
					this._value = value;
662 663 664 665
					const didFilter = this.ui.list.filter(this.filterValue(this.ui.inputBox.value));
					if (didFilter) {
						this.trySelectFirst();
					}
666
					this.onDidChangeValueEmitter.fire(value);
667 668 669 670 671 672
				}));
			this.visibleDisposables.add(this.ui.inputBox.onMouseDown(event => {
				if (!this.autoFocusOnList) {
					this.ui.list.clearFocus();
				}
			}));
B
Benjamin Pasero 已提交
673
			this.visibleDisposables.add((this._hideInput ? this.ui.list : this.ui.inputBox).onKeyDown((event: KeyboardEvent | StandardKeyboardEvent) => {
674 675
				switch (event.keyCode) {
					case KeyCode.DownArrow:
B
Benjamin Pasero 已提交
676
						this.ui.list.focus(QuickInputListFocus.Next);
677 678
						if (this.canSelectMany) {
							this.ui.list.domFocus();
679
						}
680
						dom.EventHelper.stop(event, true);
681 682 683
						break;
					case KeyCode.UpArrow:
						if (this.ui.list.getFocusedElements().length) {
B
Benjamin Pasero 已提交
684
							this.ui.list.focus(QuickInputListFocus.Previous);
685
						} else {
B
Benjamin Pasero 已提交
686
							this.ui.list.focus(QuickInputListFocus.Last);
687 688 689 690
						}
						if (this.canSelectMany) {
							this.ui.list.domFocus();
						}
691
						dom.EventHelper.stop(event, true);
692 693
						break;
					case KeyCode.PageDown:
694
						this.ui.list.focus(QuickInputListFocus.NextPage);
695 696 697
						if (this.canSelectMany) {
							this.ui.list.domFocus();
						}
698
						dom.EventHelper.stop(event, true);
699 700
						break;
					case KeyCode.PageUp:
701
						this.ui.list.focus(QuickInputListFocus.PreviousPage);
702 703 704
						if (this.canSelectMany) {
							this.ui.list.domFocus();
						}
705
						dom.EventHelper.stop(event, true);
706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721
						break;
					case KeyCode.RightArrow:
						if (!this._canAcceptInBackground) {
							return; // needs to be enabled
						}

						if (!this.ui.inputBox.isSelectionAtEnd()) {
							return; // ensure input box selection at end
						}

						if (this.activeItems[0]) {
							this._selectedItems = [this.activeItems[0]];
							this.onDidChangeSelectionEmitter.fire(this.selectedItems);
							this.onDidAcceptEmitter.fire({ inBackground: true });
						}

722 723
						break;
					case KeyCode.Home:
C
Christof Marti 已提交
724
						if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
725
							this.ui.list.focus(QuickInputListFocus.First);
726
							dom.EventHelper.stop(event, true);
727 728 729
						}
						break;
					case KeyCode.End:
C
Christof Marti 已提交
730
						if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
731
							this.ui.list.focus(QuickInputListFocus.Last);
732
							dom.EventHelper.stop(event, true);
733
						}
734 735 736 737 738 739 740 741
						break;
				}
			}));
			this.visibleDisposables.add(this.ui.onDidAccept(() => {
				if (!this.canSelectMany && this.activeItems[0]) {
					this._selectedItems = [this.activeItems[0]];
					this.onDidChangeSelectionEmitter.fire(this.selectedItems);
				}
742
				this.onDidAcceptEmitter.fire({ inBackground: false });
743 744
			}));
			this.visibleDisposables.add(this.ui.onDidCustom(() => {
745
				this.onDidCustomEmitter.fire();
746 747 748 749 750 751 752 753 754 755 756
			}));
			this.visibleDisposables.add(this.ui.list.onDidChangeFocus(focusedItems => {
				if (this.activeItemsUpdated) {
					return; // Expect another event.
				}
				if (this.activeItemsToConfirm !== this._activeItems && equals(focusedItems, this._activeItems, (a, b) => a === b)) {
					return;
				}
				this._activeItems = focusedItems as T[];
				this.onDidChangeActiveEmitter.fire(focusedItems as T[]);
			}));
757
			this.visibleDisposables.add(this.ui.list.onDidChangeSelection(({ items: selectedItems, event }) => {
758
				if (this.canSelectMany) {
759
					if (selectedItems.length) {
760
						this.ui.list.setSelectedElements([]);
761
					}
762 763 764 765 766 767 768 769
					return;
				}
				if (this.selectedItemsToConfirm !== this._selectedItems && equals(selectedItems, this._selectedItems, (a, b) => a === b)) {
					return;
				}
				this._selectedItems = selectedItems as T[];
				this.onDidChangeSelectionEmitter.fire(selectedItems as T[]);
				if (selectedItems.length) {
770
					this.onDidAcceptEmitter.fire({ inBackground: event instanceof MouseEvent && event.button === 1 /* mouse middle click */ });
771 772 773 774 775 776 777 778 779 780 781 782 783 784
				}
			}));
			this.visibleDisposables.add(this.ui.list.onChangedCheckedElements(checkedItems => {
				if (!this.canSelectMany) {
					return;
				}
				if (this.selectedItemsToConfirm !== this._selectedItems && equals(checkedItems, this._selectedItems, (a, b) => a === b)) {
					return;
				}
				this._selectedItems = checkedItems as T[];
				this.onDidChangeSelectionEmitter.fire(checkedItems as T[]);
			}));
			this.visibleDisposables.add(this.ui.list.onButtonTriggered(event => this.onDidTriggerItemButtonEmitter.fire(event as IQuickPickItemButtonEvent<T>)));
			this.visibleDisposables.add(this.registerQuickNavigation());
A
Alex Ross 已提交
785
			this.valueSelectionUpdated = true;
786
		}
787
		super.show(); // TODO: Why have show() bubble up while update() trickles down? (Could move setComboboxAccessibility() here.)
788 789
	}

C
Christof Marti 已提交
790
	private registerQuickNavigation() {
791
		return dom.addDisposableListener(this.ui.container, dom.EventType.KEY_UP, e => {
B
Benjamin Pasero 已提交
792
			if (this.canSelectMany || !this._quickNavigate) {
C
Christof Marti 已提交
793 794 795
				return;
			}

796
			const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
C
Christof Marti 已提交
797 798 799
			const keyCode = keyboardEvent.keyCode;

			// Select element when keys are pressed that signal it
B
Benjamin Pasero 已提交
800
			const quickNavKeys = this._quickNavigate.keybindings;
C
Christof Marti 已提交
801
			const wasTriggerKeyPressed = quickNavKeys.some(k => {
C
Christof Marti 已提交
802 803 804 805 806 807 808
				const [firstPart, chordPart] = k.getParts();
				if (chordPart) {
					return false;
				}

				if (firstPart.shiftKey && keyCode === KeyCode.Shift) {
					if (keyboardEvent.ctrlKey || keyboardEvent.altKey || keyboardEvent.metaKey) {
B
Benjamin Pasero 已提交
809
						return false; // this is an optimistic check for the shift key being used to navigate back in quick input
C
Christof Marti 已提交
810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829
					}

					return true;
				}

				if (firstPart.altKey && keyCode === KeyCode.Alt) {
					return true;
				}

				if (firstPart.ctrlKey && keyCode === KeyCode.Ctrl) {
					return true;
				}

				if (firstPart.metaKey && keyCode === KeyCode.Meta) {
					return true;
				}

				return false;
			});

830 831 832 833 834 835 836 837 838 839
			if (wasTriggerKeyPressed) {
				if (this.activeItems[0]) {
					this._selectedItems = [this.activeItems[0]];
					this.onDidChangeSelectionEmitter.fire(this.selectedItems);
					this.onDidAcceptEmitter.fire({ inBackground: false });
				}
				// Unset quick navigate after press. It is only valid once
				// and should not result in any behaviour change afterwards
				// if the picker remains open because there was no active item
				this._quickNavigate = undefined;
C
Christof Marti 已提交
840 841 842 843
			}
		});
	}

844 845 846 847
	protected update() {
		if (!this.visible) {
			return;
		}
848 849 850 851 852 853 854 855 856
		let hideInput = false;
		let inputShownJustForScreenReader = false;
		if (!!this._hideInput && this._items.length > 0) {
			if (this.ui.isScreenReaderOptimized()) {
				// Always show input if screen reader attached https://github.com/microsoft/vscode/issues/94360
				inputShownJustForScreenReader = true;
			} else {
				hideInput = true;
			}
857
		}
858 859
		dom.toggleClass(this.ui.container, 'hidden-input', hideInput);
		const visibilities: Visibilities = {
860
			title: !!this.title || !!this.step || !!this.buttons.length,
861 862 863 864 865 866 867 868 869 870 871
			description: !!this.description,
			checkAll: this.canSelectMany,
			inputBox: !hideInput,
			progressBar: !hideInput,
			visibleCount: true,
			count: this.canSelectMany,
			ok: this.ok === 'default' ? this.canSelectMany : this.ok,
			list: true,
			message: !!this.validationMessage,
			customButton: this.customButton
		};
872
		this.ui.setVisibilities(visibilities);
873
		super.update();
874 875 876
		if (this.ui.inputBox.value !== this.value) {
			this.ui.inputBox.value = this.value;
		}
A
Alex Ross 已提交
877 878 879 880
		if (this.valueSelectionUpdated) {
			this.valueSelectionUpdated = false;
			this.ui.inputBox.select(this._valueSelection && { start: this._valueSelection[0], end: this._valueSelection[1] });
		}
881 882
		if (this.ui.inputBox.placeholder !== (this.placeholder || '')) {
			this.ui.inputBox.placeholder = (this.placeholder || '');
883
		}
884 885 886
		if (inputShownJustForScreenReader) {
			this.ui.inputBox.ariaLabel = '';
		} else if (this.ui.inputBox.ariaLabel !== this.ariaLabel) {
887 888
			this.ui.inputBox.ariaLabel = this.ariaLabel;
		}
889 890 891 892
		this.ui.list.matchOnDescription = this.matchOnDescription;
		this.ui.list.matchOnDetail = this.matchOnDetail;
		this.ui.list.matchOnLabel = this.matchOnLabel;
		this.ui.list.sortByLabel = this.sortByLabel;
893
		if (this.itemsUpdated) {
894
			this.itemsUpdated = false;
895
			this.ui.list.setElements(this.items);
896
			this.ui.list.filter(this.filterValue(this.ui.inputBox.value));
897
			this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked();
898
			this.ui.visibleCount.setCount(this.ui.list.getVisibleCount());
899
			this.ui.count.setCount(this.ui.list.getCheckedCount());
900
			switch (this._itemActivation) {
B
Benjamin Pasero 已提交
901 902 903
				case ItemActivation.NONE:
					this._itemActivation = ItemActivation.FIRST; // only valid once, then unset
					break;
904 905 906 907 908 909 910 911 912 913 914
				case ItemActivation.SECOND:
					this.ui.list.focus(QuickInputListFocus.Second);
					this._itemActivation = ItemActivation.FIRST; // only valid once, then unset
					break;
				case ItemActivation.LAST:
					this.ui.list.focus(QuickInputListFocus.Last);
					this._itemActivation = ItemActivation.FIRST; // only valid once, then unset
					break;
				default:
					this.trySelectFirst();
					break;
915
			}
916
		}
917
		if (this.ui.container.classList.contains('show-checkboxes') !== !!this.canSelectMany) {
918 919
			if (this.canSelectMany) {
				this.ui.list.clearFocus();
A
Alex Ross 已提交
920 921
			} else {
				this.trySelectFirst();
922 923
			}
		}
924 925
		if (this.activeItemsUpdated) {
			this.activeItemsUpdated = false;
926
			this.activeItemsToConfirm = this._activeItems;
927
			this.ui.list.setFocusedElements(this.activeItems);
928 929 930
			if (this.activeItemsToConfirm === this._activeItems) {
				this.activeItemsToConfirm = null;
			}
931 932 933
		}
		if (this.selectedItemsUpdated) {
			this.selectedItemsUpdated = false;
934
			this.selectedItemsToConfirm = this._selectedItems;
935 936 937 938 939
			if (this.canSelectMany) {
				this.ui.list.setCheckedElements(this.selectedItems);
			} else {
				this.ui.list.setSelectedElements(this.selectedItems);
			}
940 941 942
			if (this.selectedItemsToConfirm === this._selectedItems) {
				this.selectedItemsToConfirm = null;
			}
943
		}
944 945
		if (this.validationMessage) {
			this.ui.message.textContent = this.validationMessage;
946
			this.showMessageDecoration(Severity.Error);
947 948
		} else {
			this.ui.message.textContent = null;
949
			this.showMessageDecoration(Severity.Ignore);
950
		}
951 952
		this.ui.customButton.label = this.customLabel || '';
		this.ui.customButton.element.title = this.customHover || '';
953
		this.ui.setComboboxAccessibility(true);
954 955 956 957 958
		if (!visibilities.inputBox) {
			// we need to move focus into the tree to detect keybindings
			// properly when the input box is not visible (quick nav)
			this.ui.list.domFocus();
		}
959
	}
960
}
C
Christof Marti 已提交
961

962 963
class InputBox extends QuickInput implements IInputBox {

964
	private static readonly noPromptMessage = localize('inputModeEntry', "Press 'Enter' to confirm your input or 'Escape' to cancel");
965 966

	private _value = '';
967
	private _valueSelection: Readonly<[number, number]> | undefined;
968
	private valueSelectionUpdated = true;
969
	private _placeholder: string | undefined;
970
	private _password = false;
971
	private _prompt: string | undefined;
972
	private noValidationMessage = InputBox.noPromptMessage;
973
	private _validationMessage: string | undefined;
974 975
	private readonly onDidValueChangeEmitter = this._register(new Emitter<string>());
	private readonly onDidAcceptEmitter = this._register(new Emitter<void>());
976

977 978 979
	get value() {
		return this._value;
	}
980

981 982 983 984
	set value(value: string) {
		this._value = value || '';
		this.update();
	}
985

986 987 988 989
	set valueSelection(valueSelection: Readonly<[number, number]>) {
		this._valueSelection = valueSelection;
		this.valueSelectionUpdated = true;
		this.update();
990
	}
991

992 993
	get placeholder() {
		return this._placeholder;
994
	}
995

996
	set placeholder(placeholder: string | undefined) {
997
		this._placeholder = placeholder;
998 999 1000 1001 1002 1003 1004 1005
		this.update();
	}

	get password() {
		return this._password;
	}

	set password(password: boolean) {
1006
		this._password = password;
1007 1008 1009 1010 1011 1012 1013
		this.update();
	}

	get prompt() {
		return this._prompt;
	}

1014
	set prompt(prompt: string | undefined) {
1015
		this._prompt = prompt;
1016 1017 1018 1019 1020 1021 1022 1023 1024 1025
		this.noValidationMessage = prompt
			? localize('inputModeEntryDescription', "{0} (Press 'Enter' to confirm or 'Escape' to cancel)", prompt)
			: InputBox.noPromptMessage;
		this.update();
	}

	get validationMessage() {
		return this._validationMessage;
	}

1026
	set validationMessage(validationMessage: string | undefined) {
1027
		this._validationMessage = validationMessage;
1028 1029 1030
		this.update();
	}

1031
	readonly onDidChangeValue = this.onDidValueChangeEmitter.event;
1032

1033
	readonly onDidAccept = this.onDidAcceptEmitter.event;
1034 1035 1036

	show() {
		if (!this.visible) {
1037
			this.visibleDisposables.add(
1038 1039 1040
				this.ui.inputBox.onDidChange(value => {
					if (value === this.value) {
						return;
1041
					}
1042 1043
					this._value = value;
					this.onDidValueChangeEmitter.fire(value);
1044
				}));
1045
			this.visibleDisposables.add(this.ui.onDidAccept(() => this.onDidAcceptEmitter.fire()));
1046
			this.valueSelectionUpdated = true;
1047
		}
1048
		super.show();
1049 1050
	}

1051 1052 1053 1054
	protected update() {
		if (!this.visible) {
			return;
		}
1055 1056 1057 1058 1059 1060
		const visibilities: Visibilities = {
			title: !!this.title || !!this.step || !!this.buttons.length,
			description: !!this.description || !!this.step,
			inputBox: true, message: true
		};
		this.ui.setVisibilities(visibilities);
1061
		super.update();
1062 1063 1064 1065 1066
		if (this.ui.inputBox.value !== this.value) {
			this.ui.inputBox.value = this.value;
		}
		if (this.valueSelectionUpdated) {
			this.valueSelectionUpdated = false;
1067
			this.ui.inputBox.select(this._valueSelection && { start: this._valueSelection[0], end: this._valueSelection[1] });
1068
		}
1069 1070
		if (this.ui.inputBox.placeholder !== (this.placeholder || '')) {
			this.ui.inputBox.placeholder = (this.placeholder || '');
1071 1072 1073 1074 1075 1076
		}
		if (this.ui.inputBox.password !== this.password) {
			this.ui.inputBox.password = this.password;
		}
		if (!this.validationMessage && this.ui.message.textContent !== this.noValidationMessage) {
			this.ui.message.textContent = this.noValidationMessage;
1077
			this.showMessageDecoration(Severity.Ignore);
1078 1079 1080
		}
		if (this.validationMessage && this.ui.message.textContent !== this.validationMessage) {
			this.ui.message.textContent = this.validationMessage;
1081
			this.showMessageDecoration(Severity.Error);
1082
		}
1083 1084 1085
	}
}

1086
export class QuickInputController extends Disposable {
B
Benjamin Pasero 已提交
1087
	private static readonly MAX_WIDTH = 600; // Max total width of quick input widget
C
Christof Marti 已提交
1088

1089
	private idPrefix: string;
1090
	private ui: QuickInputUI | undefined;
B
Benjamin Pasero 已提交
1091
	private dimension?: dom.IDimension;
1092
	private titleBarOffset?: number;
1093
	private comboboxAccessibility = false;
C
Christof Marti 已提交
1094
	private enabled = true;
1095 1096 1097
	private readonly onDidAcceptEmitter = this._register(new Emitter<void>());
	private readonly onDidCustomEmitter = this._register(new Emitter<void>());
	private readonly onDidTriggerButtonEmitter = this._register(new Emitter<IQuickInputButton>());
1098
	private keyMods: Writeable<IKeyMods> = { ctrlCmd: false, alt: false };
C
Christof Marti 已提交
1099

1100
	private controller: QuickInput | null = null;
C
Christof Marti 已提交
1101

1102 1103
	private parentElement: HTMLElement;
	private styles: IQuickInputStyles;
1104

C
Christof Marti 已提交
1105
	private onShowEmitter = this._register(new Emitter<void>());
1106 1107
	readonly onShow = this.onShowEmitter.event;

C
Christof Marti 已提交
1108
	private onHideEmitter = this._register(new Emitter<void>());
1109
	readonly onHide = this.onHideEmitter.event;
C
Christof Marti 已提交
1110

C
Christof Marti 已提交
1111 1112
	private previousFocusElement?: HTMLElement;

1113 1114 1115 1116 1117 1118
	constructor(private options: IQuickInputOptions) {
		super();
		this.idPrefix = options.idPrefix;
		this.parentElement = options.container;
		this.styles = options.styles;
		this.registerKeyModsListeners();
C
Christof Marti 已提交
1119 1120
	}

1121
	private registerKeyModsListeners() {
1122 1123 1124 1125 1126 1127 1128
		const listener = (e: KeyboardEvent | MouseEvent) => {
			this.keyMods.ctrlCmd = e.ctrlKey || e.metaKey;
			this.keyMods.alt = e.altKey;
		};
		this._register(dom.addDisposableListener(window, dom.EventType.KEY_DOWN, listener, true));
		this._register(dom.addDisposableListener(window, dom.EventType.KEY_UP, listener, true));
		this._register(dom.addDisposableListener(window, dom.EventType.MOUSE_DOWN, listener, true));
1129 1130
	}

1131
	private getUI() {
C
Christof Marti 已提交
1132
		if (this.ui) {
1133
			return this.ui;
C
Christof Marti 已提交
1134 1135
		}

1136
		const container = dom.append(this.parentElement, $('.quick-input-widget.show-file-icons'));
C
Christof Marti 已提交
1137 1138
		container.tabIndex = -1;
		container.style.display = 'none';
C
Christof Marti 已提交
1139

1140 1141
		const styleSheet = dom.createStyleSheet(container);

1142
		const titleBar = dom.append(container, $('.quick-input-titlebar'));
1143

1144
		const leftActionBar = this._register(new ActionBar(titleBar));
C
Christof Marti 已提交
1145 1146
		leftActionBar.domNode.classList.add('quick-input-left-action-bar');

1147
		const title = dom.append(titleBar, $('.quick-input-title'));
1148

1149
		const rightActionBar = this._register(new ActionBar(titleBar));
C
Christof Marti 已提交
1150
		rightActionBar.domNode.classList.add('quick-input-right-action-bar');
C
Christof Marti 已提交
1151

1152 1153
		const description = dom.append(container, $('.quick-input-description'));

C
Christof Marti 已提交
1154
		const headerContainer = dom.append(container, $('.quick-input-header'));
1155

1156 1157
		const checkAll = <HTMLInputElement>dom.append(headerContainer, $('input.quick-input-check-all'));
		checkAll.type = 'checkbox';
B
Benjamin Pasero 已提交
1158
		this._register(dom.addStandardDisposableListener(checkAll, dom.EventType.CHANGE, e => {
1159
			const checked = checkAll.checked;
1160
			list.setAllVisibleChecked(checked);
C
Christof Marti 已提交
1161
		}));
B
Benjamin Pasero 已提交
1162
		this._register(dom.addDisposableListener(checkAll, dom.EventType.CLICK, e => {
1163
			if (e.x || e.y) { // Avoid 'click' triggered by 'space'...
1164
				inputBox.setFocus();
1165 1166
			}
		}));
C
Christof Marti 已提交
1167

1168
		const extraContainer = dom.append(headerContainer, $('.quick-input-and-message'));
1169
		const filterContainer = dom.append(extraContainer, $('.quick-input-filter'));
1170

1171
		const inputBox = this._register(new QuickInputBox(filterContainer));
1172
		inputBox.setAttribute('aria-describedby', `${this.idPrefix}message`);
1173

1174 1175 1176 1177
		const visibleCountContainer = dom.append(filterContainer, $('.quick-input-visible-count'));
		visibleCountContainer.setAttribute('aria-live', 'polite');
		visibleCountContainer.setAttribute('aria-atomic', 'true');
		const visibleCount = new CountBadge(visibleCountContainer, { countFormat: localize({ key: 'quickInput.visibleCount', comment: ['This tells the user how many items are shown in a list of items to select from. The items can be anything. Currently not visible, but read by screen readers.'] }, "{0} Results") });
1178

1179 1180 1181
		const countContainer = dom.append(filterContainer, $('.quick-input-count'));
		countContainer.setAttribute('aria-live', 'polite');
		const count = new CountBadge(countContainer, { countFormat: localize({ key: 'quickInput.countSelected', comment: ['This tells the user how many items are selected in a list of items to select from. The items can be anything.'] }, "{0} Selected") });
1182

1183 1184 1185 1186
		const okContainer = dom.append(headerContainer, $('.quick-input-action'));
		const ok = new Button(okContainer);
		ok.label = localize('ok', "OK");
		this._register(ok.onDidClick(e => {
C
Christof Marti 已提交
1187
			this.onDidAcceptEmitter.fire();
C
Christof Marti 已提交
1188 1189
		}));

1190 1191
		const customButtonContainer = dom.append(headerContainer, $('.quick-input-action'));
		const customButton = new Button(customButtonContainer);
1192 1193 1194 1195 1196
		customButton.label = localize('custom', "Custom");
		this._register(customButton.onDidClick(e => {
			this.onDidCustomEmitter.fire();
		}));

1197
		const message = dom.append(extraContainer, $(`#${this.idPrefix}message.quick-input-message`));
1198

1199 1200
		const progressBar = new ProgressBar(container);
		dom.addClass(progressBar.getContainer(), 'quick-input-progress');
1201

1202
		const list = this._register(new QuickInputList(container, this.idPrefix + 'list', this.options));
B
Benjamin Pasero 已提交
1203
		this._register(list.onChangedAllVisibleChecked(checked => {
1204
			checkAll.checked = checked;
C
Christof Marti 已提交
1205
		}));
1206 1207 1208
		this._register(list.onChangedVisibleCount(c => {
			visibleCount.setCount(c);
		}));
B
Benjamin Pasero 已提交
1209
		this._register(list.onChangedCheckedCount(c => {
1210
			count.setCount(c);
C
Christof Marti 已提交
1211
		}));
B
Benjamin Pasero 已提交
1212
		this._register(list.onLeave(() => {
1213 1214
			// Defer to avoid the input field reacting to the triggering key.
			setTimeout(() => {
1215
				inputBox.setFocus();
C
Christof Marti 已提交
1216 1217 1218
				if (this.controller instanceof QuickPick && this.controller.canSelectMany) {
					list.clearFocus();
				}
1219 1220
			}, 0);
		}));
1221 1222
		this._register(list.onDidChangeFocus(() => {
			if (this.comboboxAccessibility) {
1223
				this.getUI().inputBox.setAttribute('aria-activedescendant', this.getUI().list.getActiveDescendant() || '');
1224 1225
			}
		}));
C
Christof Marti 已提交
1226

1227 1228
		const focusTracker = dom.trackFocus(container);
		this._register(focusTracker);
C
Christof Marti 已提交
1229 1230 1231
		this._register(dom.addDisposableListener(container, dom.EventType.FOCUS, e => {
			this.previousFocusElement = e.relatedTarget instanceof HTMLElement ? e.relatedTarget : undefined;
		}, true));
1232
		this._register(focusTracker.onDidBlur(() => {
1233
			if (!this.getUI().ignoreFocusOut && !this.options.ignoreFocusOut()) {
1234
				this.hide();
1235
			}
1236
			this.previousFocusElement = undefined;
C
Christof Marti 已提交
1237
		}));
1238 1239 1240
		this._register(dom.addDisposableListener(container, dom.EventType.FOCUS, (e: FocusEvent) => {
			inputBox.setFocus();
		}));
B
Benjamin Pasero 已提交
1241
		this._register(dom.addDisposableListener(container, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
C
Christof Marti 已提交
1242 1243 1244
			const event = new StandardKeyboardEvent(e);
			switch (event.keyCode) {
				case KeyCode.Enter:
1245 1246
					dom.EventHelper.stop(e, true);
					this.onDidAcceptEmitter.fire();
C
Christof Marti 已提交
1247 1248 1249
					break;
				case KeyCode.Escape:
					dom.EventHelper.stop(e, true);
1250
					this.hide();
C
Christof Marti 已提交
1251
					break;
1252 1253
				case KeyCode.Tab:
					if (!event.altKey && !event.ctrlKey && !event.metaKey) {
C
Christof Marti 已提交
1254
						const selectors = ['.action-label.codicon'];
C
Christof Marti 已提交
1255
						if (container.classList.contains('show-checkboxes')) {
1256
							selectors.push('input');
C
Christof Marti 已提交
1257
						} else {
1258
							selectors.push('input[type=text]');
C
Christof Marti 已提交
1259
						}
1260
						if (this.getUI().list.isDisplayed()) {
1261 1262 1263 1264
							selectors.push('.monaco-list');
						}
						const stops = container.querySelectorAll<HTMLElement>(selectors.join(', '));
						if (event.shiftKey && event.target === stops[0]) {
1265
							dom.EventHelper.stop(e, true);
1266 1267
							stops[stops.length - 1].focus();
						} else if (!event.shiftKey && event.target === stops[stops.length - 1]) {
1268
							dom.EventHelper.stop(e, true);
1269
							stops[0].focus();
1270 1271 1272
						}
					}
					break;
C
Christof Marti 已提交
1273 1274
			}
		}));
1275

1276 1277
		this.ui = {
			container,
1278
			styleSheet,
C
Christof Marti 已提交
1279
			leftActionBar,
1280
			titleBar,
C
Christof Marti 已提交
1281
			title,
1282
			description,
C
Christof Marti 已提交
1283
			rightActionBar,
1284
			checkAll,
1285
			filterContainer,
1286
			inputBox,
1287
			visibleCountContainer,
1288
			visibleCount,
1289
			countContainer,
1290
			count,
1291 1292
			okContainer,
			ok,
1293
			message,
1294
			customButtonContainer,
1295
			customButton,
1296 1297 1298
			progressBar,
			list,
			onDidAccept: this.onDidAcceptEmitter.event,
1299
			onDidCustom: this.onDidCustomEmitter.event,
C
Christof Marti 已提交
1300
			onDidTriggerButton: this.onDidTriggerButtonEmitter.event,
1301
			ignoreFocusOut: false,
1302
			keyMods: this.keyMods,
1303
			isScreenReaderOptimized: () => this.options.isScreenReaderOptimized(),
1304 1305
			show: controller => this.show(controller),
			hide: () => this.hide(),
C
Christof Marti 已提交
1306
			setVisibilities: visibilities => this.setVisibilities(visibilities),
1307
			setComboboxAccessibility: enabled => this.setComboboxAccessibility(enabled),
C
Christof Marti 已提交
1308
			setEnabled: enabled => this.setEnabled(enabled),
1309
			setContextKey: contextKey => this.options.setContextKey(contextKey),
1310
		};
1311
		this.updateStyles();
1312
		return this.ui;
C
Christof Marti 已提交
1313 1314
	}

1315 1316 1317 1318
	pick<T extends IQuickPickItem, O extends IPickOptions<T>>(picks: Promise<QuickPickInput<T>[]> | QuickPickInput<T>[], options: O = <O>{}, token: CancellationToken = CancellationToken.None): Promise<(O extends { canPickMany: true } ? T[] : T) | undefined> {
		type R = (O extends { canPickMany: true } ? T[] : T) | undefined;
		return new Promise<R>((doResolve, reject) => {
			let resolve = (result: R) => {
C
Christof Marti 已提交
1319 1320 1321 1322 1323 1324
				resolve = doResolve;
				if (options.onKeyMods) {
					options.onKeyMods(input.keyMods);
				}
				doResolve(result);
			};
1325 1326 1327
			if (token.isCancellationRequested) {
				resolve(undefined);
				return;
1328
			}
1329
			const input = this.createQuickPick<T>();
1330
			let activeItem: T | undefined;
1331 1332 1333 1334
			const disposables = [
				input,
				input.onDidAccept(() => {
					if (input.canSelectMany) {
1335
						resolve(<R>input.selectedItems.slice());
1336 1337 1338 1339
						input.hide();
					} else {
						const result = input.activeItems[0];
						if (result) {
1340
							resolve(<R>result);
1341 1342 1343 1344 1345 1346
							input.hide();
						}
					}
				}),
				input.onDidChangeActive(items => {
					const focused = items[0];
J
Joao Moreno 已提交
1347 1348
					if (focused && options.onDidFocus) {
						options.onDidFocus(focused);
1349 1350 1351 1352 1353 1354
					}
				}),
				input.onDidChangeSelection(items => {
					if (!input.canSelectMany) {
						const result = items[0];
						if (result) {
1355
							resolve(<R>result);
1356 1357 1358 1359
							input.hide();
						}
					}
				}),
C
Christof Marti 已提交
1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370
				input.onDidTriggerItemButton(event => options.onDidTriggerItemButton && options.onDidTriggerItemButton({
					...event,
					removeItem: () => {
						const index = input.items.indexOf(event.item);
						if (index !== -1) {
							const items = input.items.slice();
							items.splice(index, 1);
							input.items = items;
						}
					}
				})),
1371 1372 1373 1374 1375
				input.onDidChangeValue(value => {
					if (activeItem && !value && (input.activeItems.length !== 1 || input.activeItems[0] !== activeItem)) {
						input.activeItems = [activeItem];
					}
				}),
1376 1377 1378 1379 1380 1381 1382 1383
				token.onCancellationRequested(() => {
					input.hide();
				}),
				input.onDidHide(() => {
					dispose(disposables);
					resolve(undefined);
				}),
			];
1384
			input.canSelectMany = !!options.canPickMany;
1385
			input.placeholder = options.placeHolder;
1386 1387 1388
			if (options.placeHolder) {
				input.ariaLabel = options.placeHolder;
			}
1389 1390 1391
			input.ignoreFocusOut = !!options.ignoreFocusLost;
			input.matchOnDescription = !!options.matchOnDescription;
			input.matchOnDetail = !!options.matchOnDetail;
A
Alex Ross 已提交
1392 1393
			input.matchOnLabel = (options.matchOnLabel === undefined) || options.matchOnLabel; // default to true
			input.autoFocusOnList = (options.autoFocusOnList === undefined) || options.autoFocusOnList; // default to true
C
Christof Marti 已提交
1394
			input.quickNavigate = options.quickNavigate;
C
Christof Marti 已提交
1395
			input.contextKey = options.contextKey;
1396
			input.busy = true;
1397
			Promise.all<QuickPickInput<T>[], T | undefined>([picks, options.activeItem])
1398 1399
				.then(([items, _activeItem]) => {
					activeItem = _activeItem;
C
Christof Marti 已提交
1400 1401 1402
					input.busy = false;
					input.items = items;
					if (input.canSelectMany) {
C
Christof Marti 已提交
1403
						input.selectedItems = items.filter(item => item.type !== 'separator' && item.picked) as T[];
C
Christof Marti 已提交
1404 1405 1406 1407 1408
					}
					if (activeItem) {
						input.activeItems = [activeItem];
					}
				});
1409
			input.show();
R
Rob Lourens 已提交
1410
			Promise.resolve(picks).then(undefined, err => {
1411 1412 1413 1414
				reject(err);
				input.hide();
			});
		});
C
Christof Marti 已提交
1415 1416
	}

1417
	input(options: IInputOptions = {}, token: CancellationToken = CancellationToken.None): Promise<string | undefined> {
C
Christof Marti 已提交
1418
		return new Promise<string>((resolve, reject) => {
1419 1420 1421 1422 1423
			if (token.isCancellationRequested) {
				resolve(undefined);
				return;
			}
			const input = this.createInputBox();
J
Johannes Rieken 已提交
1424
			const validateInput = options.validateInput || (() => <Promise<undefined>>Promise.resolve(undefined));
J
Joao Moreno 已提交
1425
			const onDidValueChange = Event.debounce(input.onDidChangeValue, (last, cur) => cur, 100);
1426
			let validationValue = options.value || '';
C
Christof Marti 已提交
1427
			let validation = Promise.resolve(validateInput(validationValue));
1428 1429 1430 1431
			const disposables = [
				input,
				onDidValueChange(value => {
					if (value !== validationValue) {
C
Christof Marti 已提交
1432
						validation = Promise.resolve(validateInput(value));
1433
						validationValue = value;
1434 1435
					}
					validation.then(result => {
1436
						if (value === validationValue) {
1437
							input.validationMessage = result || undefined;
1438
						}
1439 1440 1441 1442 1443
					});
				}),
				input.onDidAccept(() => {
					const value = input.value;
					if (value !== validationValue) {
C
Christof Marti 已提交
1444
						validation = Promise.resolve(validateInput(value));
1445
						validationValue = value;
1446 1447 1448 1449 1450
					}
					validation.then(result => {
						if (!result) {
							resolve(value);
							input.hide();
1451 1452
						} else if (value === validationValue) {
							input.validationMessage = result;
1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463
						}
					});
				}),
				token.onCancellationRequested(() => {
					input.hide();
				}),
				input.onDidHide(() => {
					dispose(disposables);
					resolve(undefined);
				}),
			];
1464
			input.value = options.value || '';
1465 1466 1467
			input.valueSelection = options.valueSelection;
			input.prompt = options.prompt;
			input.placeholder = options.placeHolder;
1468 1469
			input.password = !!options.password;
			input.ignoreFocusOut = !!options.ignoreFocusLost;
1470 1471
			input.show();
		});
1472 1473
	}

C
Christof Marti 已提交
1474
	backButton = backButton;
C
Christof Marti 已提交
1475

1476
	createQuickPick<T extends IQuickPickItem>(): IQuickPick<T> {
1477 1478
		const ui = this.getUI();
		return new QuickPick<T>(ui);
C
Christof Marti 已提交
1479 1480
	}

1481
	createInputBox(): IInputBox {
1482 1483
		const ui = this.getUI();
		return new InputBox(ui);
1484 1485
	}

1486
	private show(controller: QuickInput) {
1487
		const ui = this.getUI();
1488
		this.onShowEmitter.fire();
1489 1490 1491 1492
		const oldController = this.controller;
		this.controller = controller;
		if (oldController) {
			oldController.didHide();
C
Christof Marti 已提交
1493
		}
C
Christof Marti 已提交
1494

C
Christof Marti 已提交
1495
		this.setEnabled(true);
1496 1497
		ui.leftActionBar.clear();
		ui.title.textContent = '';
1498
		ui.description.textContent = '';
1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512
		ui.rightActionBar.clear();
		ui.checkAll.checked = false;
		// ui.inputBox.value = ''; Avoid triggering an event.
		ui.inputBox.placeholder = '';
		ui.inputBox.password = false;
		ui.inputBox.showDecoration(Severity.Ignore);
		ui.visibleCount.setCount(0);
		ui.count.setCount(0);
		ui.message.textContent = '';
		ui.progressBar.stop();
		ui.list.setElements([]);
		ui.list.matchOnDescription = false;
		ui.list.matchOnDetail = false;
		ui.list.matchOnLabel = true;
1513
		ui.list.sortByLabel = true;
1514
		ui.ignoreFocusOut = false;
1515
		this.setComboboxAccessibility(false);
1516
		ui.inputBox.ariaLabel = '';
1517

1518 1519
		const backKeybindingLabel = this.options.backKeybindingLabel();
		backButton.tooltip = backKeybindingLabel ? localize('quickInput.backWithKeybinding', "Back ({0})", backKeybindingLabel) : localize('quickInput.back', "Back");
C
Christof Marti 已提交
1520

1521
		ui.container.style.display = '';
C
Christof Marti 已提交
1522
		this.updateLayout();
1523
		ui.inputBox.setFocus();
C
Christof Marti 已提交
1524 1525
	}

1526
	private setVisibilities(visibilities: Visibilities) {
1527 1528
		const ui = this.getUI();
		ui.title.style.display = visibilities.title ? '' : 'none';
1529
		ui.description.style.display = visibilities.description ? '' : 'none';
1530 1531 1532 1533 1534 1535 1536
		ui.checkAll.style.display = visibilities.checkAll ? '' : 'none';
		ui.filterContainer.style.display = visibilities.inputBox ? '' : 'none';
		ui.visibleCountContainer.style.display = visibilities.visibleCount ? '' : 'none';
		ui.countContainer.style.display = visibilities.count ? '' : 'none';
		ui.okContainer.style.display = visibilities.ok ? '' : 'none';
		ui.customButtonContainer.style.display = visibilities.customButton ? '' : 'none';
		ui.message.style.display = visibilities.message ? '' : 'none';
1537
		ui.progressBar.getContainer().style.display = visibilities.progressBar ? '' : 'none';
1538 1539
		ui.list.display(!!visibilities.list);
		ui.container.classList[visibilities.checkAll ? 'add' : 'remove']('show-checkboxes');
1540
		this.updateLayout(); // TODO
1541 1542
	}

1543 1544
	private setComboboxAccessibility(enabled: boolean) {
		if (enabled !== this.comboboxAccessibility) {
1545
			const ui = this.getUI();
1546 1547
			this.comboboxAccessibility = enabled;
			if (this.comboboxAccessibility) {
1548 1549 1550 1551
				ui.inputBox.setAttribute('role', 'combobox');
				ui.inputBox.setAttribute('aria-haspopup', 'true');
				ui.inputBox.setAttribute('aria-autocomplete', 'list');
				ui.inputBox.setAttribute('aria-activedescendant', ui.list.getActiveDescendant() || '');
1552
			} else {
1553 1554 1555 1556
				ui.inputBox.removeAttribute('role');
				ui.inputBox.removeAttribute('aria-haspopup');
				ui.inputBox.removeAttribute('aria-autocomplete');
				ui.inputBox.removeAttribute('aria-activedescendant');
1557 1558 1559 1560
			}
		}
	}

C
Christof Marti 已提交
1561 1562 1563
	private setEnabled(enabled: boolean) {
		if (enabled !== this.enabled) {
			this.enabled = enabled;
1564
			for (const item of this.getUI().leftActionBar.viewItems) {
1565
				(item as ActionViewItem).getAction().enabled = enabled;
C
Christof Marti 已提交
1566
			}
1567
			for (const item of this.getUI().rightActionBar.viewItems) {
1568
				(item as ActionViewItem).getAction().enabled = enabled;
C
Christof Marti 已提交
1569
			}
1570 1571 1572 1573
			this.getUI().checkAll.disabled = !enabled;
			// this.getUI().inputBox.enabled = enabled; Avoid loosing focus.
			this.getUI().ok.enabled = enabled;
			this.getUI().list.enabled = enabled;
C
Christof Marti 已提交
1574 1575 1576
		}
	}

1577
	hide() {
1578 1579
		const controller = this.controller;
		if (controller) {
1580
			const focusChanged = !this.ui?.container.contains(document.activeElement);
1581
			this.controller = null;
1582
			this.onHideEmitter.fire();
1583
			this.getUI().container.style.display = 'none';
1584
			if (!focusChanged) {
C
Christof Marti 已提交
1585 1586 1587 1588 1589 1590
				if (this.previousFocusElement && this.previousFocusElement.offsetParent) {
					this.previousFocusElement.focus();
					this.previousFocusElement = undefined;
				} else {
					this.options.returnFocus();
				}
1591 1592
			}
			controller.didHide();
C
Christof Marti 已提交
1593 1594 1595
		}
	}

C
Christof Marti 已提交
1596
	focus() {
C
Christof Marti 已提交
1597
		if (this.isDisplayed()) {
1598
			this.getUI().inputBox.setFocus();
C
Christof Marti 已提交
1599 1600 1601
		}
	}

C
Christof Marti 已提交
1602
	toggle() {
1603
		if (this.isDisplayed() && this.controller instanceof QuickPick && this.controller.canSelectMany) {
1604
			this.getUI().list.toggleCheckbox();
C
Christof Marti 已提交
1605 1606 1607
		}
	}

C
Christof Marti 已提交
1608
	navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration) {
1609
		if (this.isDisplayed() && this.getUI().list.isDisplayed()) {
B
Benjamin Pasero 已提交
1610
			this.getUI().list.focus(next ? QuickInputListFocus.Next : QuickInputListFocus.Previous);
1611
			if (quickNavigate && this.controller instanceof QuickPick) {
C
Christof Marti 已提交
1612
				this.controller.quickNavigate = quickNavigate;
C
Christof Marti 已提交
1613
			}
1614 1615 1616
		}
	}

1617 1618 1619 1620 1621 1622 1623 1624
	async accept(keyMods: IKeyMods = { alt: false, ctrlCmd: false }) {
		// When accepting the item programmatically, it is important that
		// we update `keyMods` either from the provided set or unset it
		// because the accept did not happen from mouse or keyboard
		// interaction on the list itself
		this.keyMods.alt = keyMods.alt;
		this.keyMods.ctrlCmd = keyMods.ctrlCmd;

1625
		this.onDidAcceptEmitter.fire();
C
Christof Marti 已提交
1626 1627
	}

1628
	async back() {
C
Christof Marti 已提交
1629 1630 1631
		this.onDidTriggerButtonEmitter.fire(this.backButton);
	}

1632
	async cancel() {
1633
		this.hide();
C
Christof Marti 已提交
1634 1635
	}

B
Benjamin Pasero 已提交
1636
	layout(dimension: dom.IDimension, titleBarOffset: number): void {
C
Christof Marti 已提交
1637
		this.dimension = dimension;
1638
		this.titleBarOffset = titleBarOffset;
C
Christof Marti 已提交
1639 1640 1641 1642
		this.updateLayout();
	}

	private updateLayout() {
1643
		if (this.ui) {
1644
			this.ui.container.style.top = `${this.titleBarOffset}px`;
C
Christof Marti 已提交
1645

C
Christof Marti 已提交
1646
			const style = this.ui.container.style;
1647
			const width = Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH);
C
Christof Marti 已提交
1648 1649 1650
			style.width = width + 'px';
			style.marginLeft = '-' + (width / 2) + 'px';

1651
			this.ui.inputBox.layout();
1652
			this.ui.list.layout(this.dimension && this.dimension.height * 0.4);
C
Christof Marti 已提交
1653 1654
		}
	}
1655

1656
	applyStyles(styles: IQuickInputStyles) {
1657 1658 1659 1660 1661
		this.styles = styles;
		this.updateStyles();
	}

	private updateStyles() {
1662
		if (this.ui) {
1663
			const {
1664
				quickInputTitleBackground,
1665 1666 1667 1668 1669
				quickInputBackground,
				quickInputForeground,
				contrastBorder,
				widgetShadow,
			} = this.styles.widget;
1670
			this.ui.titleBar.style.backgroundColor = quickInputTitleBackground ? quickInputTitleBackground.toString() : '';
M
Matt Bierner 已提交
1671
			this.ui.container.style.backgroundColor = quickInputBackground ? quickInputBackground.toString() : '';
M
Matt Bierner 已提交
1672
			this.ui.container.style.color = quickInputForeground ? quickInputForeground.toString() : '';
1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696
			this.ui.container.style.border = contrastBorder ? `1px solid ${contrastBorder}` : '';
			this.ui.container.style.boxShadow = widgetShadow ? `0 5px 8px ${widgetShadow}` : '';
			this.ui.inputBox.style(this.styles.inputBox);
			this.ui.count.style(this.styles.countBadge);
			this.ui.ok.style(this.styles.button);
			this.ui.customButton.style(this.styles.button);
			this.ui.progressBar.style(this.styles.progressBar);
			this.ui.list.style(this.styles.list);

			const content: string[] = [];
			if (this.styles.list.listInactiveFocusForeground) {
				content.push(`.monaco-list .monaco-list-row.focused { color:  ${this.styles.list.listInactiveFocusForeground}; }`);
				content.push(`.monaco-list .monaco-list-row.focused:hover { color:  ${this.styles.list.listInactiveFocusForeground}; }`); // overwrite :hover style in this case!
			}
			if (this.styles.list.pickerGroupBorder) {
				content.push(`.quick-input-list .quick-input-list-entry { border-top-color:  ${this.styles.list.pickerGroupBorder}; }`);
			}
			if (this.styles.list.pickerGroupForeground) {
				content.push(`.quick-input-list .quick-input-list-separator { color:  ${this.styles.list.pickerGroupForeground}; }`);
			}
			const newStyles = content.join('\n');
			if (newStyles !== this.ui.styleSheet.innerHTML) {
				this.ui.styleSheet.innerHTML = newStyles;
			}
1697
		}
1698
	}
1699 1700

	private isDisplayed() {
C
Christof Marti 已提交
1701
		return this.ui && this.ui.container.style.display !== 'none';
1702
	}
C
Christof Marti 已提交
1703
}