quickOpenWidget.ts 25.1 KB
Newer Older
E
Erich Gamma 已提交
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.
 *--------------------------------------------------------------------------------------------*/
'use strict';

import 'vs/css!./quickopen';
B
Benjamin Pasero 已提交
8
import nls = require('vs/nls');
J
Johannes Rieken 已提交
9
import { TPromise } from 'vs/base/common/winjs.base';
E
Erich Gamma 已提交
10 11
import platform = require('vs/base/common/platform');
import browser = require('vs/base/browser/browser');
J
Johannes Rieken 已提交
12
import { EventType } from 'vs/base/common/events';
E
Erich Gamma 已提交
13 14
import types = require('vs/base/common/types');
import errors = require('vs/base/common/errors');
J
Johannes Rieken 已提交
15 16 17 18 19
import { IQuickNavigateConfiguration, IAutoFocus, IEntryRunContext, IModel, Mode } from 'vs/base/parts/quickopen/common/quickOpen';
import { Filter, Renderer, DataSource, IModelProvider, AccessibilityProvider } from 'vs/base/parts/quickopen/browser/quickOpenViewer';
import { Dimension, Builder, $ } from 'vs/base/browser/builder';
import { ISelectionEvent, IFocusEvent, ITree, ContextMenuEvent } from 'vs/base/parts/tree/browser/tree';
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
20
import Severity from 'vs/base/common/severity';
J
Johannes Rieken 已提交
21 22 23 24
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { DefaultController, ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults';
E
Erich Gamma 已提交
25
import DOM = require('vs/base/browser/dom');
J
Johannes Rieken 已提交
26 27 28 29
import { IActionProvider } from 'vs/base/parts/tree/browser/actionsRenderer';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
E
Erich Gamma 已提交
30 31 32 33 34 35

export interface IQuickOpenCallbacks {
	onOk: () => void;
	onCancel: () => void;
	onType: (value: string) => void;
	onShow?: () => void;
36
	onHide?: (reason: HideReason) => void;
E
Erich Gamma 已提交
37 38 39 40 41 42 43 44 45 46 47
	onFocusLost?: () => boolean /* veto close */;
}

export interface IQuickOpenOptions {
	minItemsToShow?: number;
	maxItemsToShow?: number;
	inputPlaceHolder: string;
	inputAriaLabel?: string;
	actionProvider?: IActionProvider;
}

B
Benjamin Pasero 已提交
48 49 50 51 52
export interface IShowOptions {
	quickNavigateConfiguration?: IQuickNavigateConfiguration;
	autoFocus?: IAutoFocus;
}

E
Erich Gamma 已提交
53 54 55 56
export interface IQuickOpenUsageLogger {
	publicLog(eventName: string, data?: any): void;
}

57 58
export class QuickOpenController extends DefaultController {

59
	public onContextMenu(tree: ITree, element: any, event: ContextMenuEvent): boolean {
60 61 62 63 64 65 66 67
		if (platform.isMacintosh) {
			return this.onLeftClick(tree, element, event); // https://github.com/Microsoft/vscode/issues/1011
		}

		return super.onContextMenu(tree, element, event);
	}
}

B
Benjamin Pasero 已提交
68
export enum HideReason {
69 70 71 72 73
	ELEMENT_SELECTED,
	FOCUS_LOST,
	CANCELED
}

74 75
const DEFAULT_INPUT_ARIA_LABEL = nls.localize('quickOpenAriaLabel', "Quick picker. Type to narrow down results.");

E
Erich Gamma 已提交
76 77
export class QuickOpenWidget implements IModelProvider {

B
Benjamin Pasero 已提交
78 79
	private static MAX_WIDTH = 600;				// Max total width of quick open widget
	private static MAX_ITEMS_HEIGHT = 20 * 22;	// Max height of item list below input field
E
Erich Gamma 已提交
80 81 82 83 84 85 86 87 88 89 90 91

	private options: IQuickOpenOptions;
	private builder: Builder;
	private tree: ITree;
	private inputBox: InputBox;
	private inputContainer: Builder;
	private helpText: Builder;
	private treeContainer: Builder;
	private progressBar: ProgressBar;
	private visible: boolean;
	private isLoosingFocus: boolean;
	private callbacks: IQuickOpenCallbacks;
A
Alex Dima 已提交
92
	private toUnbind: IDisposable[];
E
Erich Gamma 已提交
93 94
	private quickNavigateConfiguration: IQuickNavigateConfiguration;
	private container: HTMLElement;
95 96
	private treeElement: HTMLElement;
	private inputElement: HTMLElement;
E
Erich Gamma 已提交
97 98 99
	private usageLogger: IQuickOpenUsageLogger;
	private layoutDimensions: Dimension;
	private model: IModel<any>;
100
	private inputChangingTimeoutHandle: number;
E
Erich Gamma 已提交
101 102 103 104 105 106 107 108 109 110

	constructor(container: HTMLElement, callbacks: IQuickOpenCallbacks, options: IQuickOpenOptions, usageLogger?: IQuickOpenUsageLogger) {
		this.toUnbind = [];
		this.container = container;
		this.callbacks = callbacks;
		this.options = options;
		this.usageLogger = usageLogger;
		this.model = null;
	}

B
Benjamin Pasero 已提交
111
	public getModel(): IModel<any> {
E
Erich Gamma 已提交
112 113 114 115 116 117 118
		return this.model;
	}

	public setCallbacks(callbacks: IQuickOpenCallbacks): void {
		this.callbacks = callbacks;
	}

B
Benjamin Pasero 已提交
119
	public create(): HTMLElement {
E
Erich Gamma 已提交
120 121 122 123
		this.builder = $().div((div: Builder) => {

			// Eventing
			div.on(DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
B
Benjamin Pasero 已提交
124
				const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
E
Erich Gamma 已提交
125 126 127
				if (keyboardEvent.keyCode === KeyCode.Escape) {
					DOM.EventHelper.stop(e, true);

128
					this.hide(HideReason.CANCELED);
E
Erich Gamma 已提交
129 130
				}
			})
B
Benjamin Pasero 已提交
131 132 133
				.on(DOM.EventType.CONTEXT_MENU, (e: Event) => DOM.EventHelper.stop(e, true)) // Do this to fix an issue on Mac where the menu goes into the way
				.on(DOM.EventType.FOCUS, (e: Event) => this.gainingFocus(), null, true)
				.on(DOM.EventType.BLUR, (e: Event) => this.loosingFocus(e), null, true);
E
Erich Gamma 已提交
134 135 136 137 138 139 140 141 142 143

			// Progress Bar
			this.progressBar = new ProgressBar(div.clone());
			this.progressBar.getContainer().hide();

			// Input Field
			div.div({ 'class': 'quick-open-input' }, (inputContainer) => {
				this.inputContainer = inputContainer;
				this.inputBox = new InputBox(inputContainer.getHTMLElement(), null, {
					placeholder: this.options.inputPlaceHolder || '',
144
					ariaLabel: DEFAULT_INPUT_ARIA_LABEL
E
Erich Gamma 已提交
145
				});
146 147 148 149 150 151 152

				// ARIA
				this.inputElement = this.inputBox.inputElement;
				this.inputElement.setAttribute('role', 'combobox');
				this.inputElement.setAttribute('aria-haspopup', 'false');
				this.inputElement.setAttribute('aria-autocomplete', 'list');

E
Erich Gamma 已提交
153
				DOM.addDisposableListener(this.inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
B
Benjamin Pasero 已提交
154
					const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
E
Erich Gamma 已提交
155

156 157 158 159 160
					// Do not handle Tab: It is used to navigate between elements without mouse
					if (keyboardEvent.keyCode === KeyCode.Tab) {
						return;
					}

E
Erich Gamma 已提交
161
					// Pass tree navigation keys to the tree but leave focus in input field
D
Dirk Baeumer 已提交
162
					else if (keyboardEvent.keyCode === KeyCode.DownArrow || keyboardEvent.keyCode === KeyCode.UpArrow || keyboardEvent.keyCode === KeyCode.PageDown || keyboardEvent.keyCode === KeyCode.PageUp) {
E
Erich Gamma 已提交
163 164 165 166 167 168
						DOM.EventHelper.stop(e, true);

						this.navigateInTree(keyboardEvent.keyCode, keyboardEvent.shiftKey);
					}

					// Select element on Enter
W
Will Prater 已提交
169
					else if (keyboardEvent.keyCode === KeyCode.Enter || keyboardEvent.keyCode === KeyCode.RightArrow) {
E
Erich Gamma 已提交
170 171
						DOM.EventHelper.stop(e, true);

B
Benjamin Pasero 已提交
172
						const focus = this.tree.getFocus();
E
Erich Gamma 已提交
173
						if (focus) {
174
							this.elementSelected(focus, keyboardEvent, keyboardEvent.keyCode === KeyCode.RightArrow ? Mode.OPEN_IN_BACKGROUND : Mode.OPEN);
E
Erich Gamma 已提交
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
						}
					}

					// Bug in IE 9: onInput is not fired for Backspace or Delete keys
					else if (browser.isIE9 && (keyboardEvent.keyCode === KeyCode.Backspace || keyboardEvent.keyCode === KeyCode.Delete)) {
						this.onType();
					}
				});

				DOM.addDisposableListener(this.inputBox.inputElement, DOM.EventType.INPUT, (e: Event) => {
					this.onType();
				});
			});

			// Tree
			this.treeContainer = div.div({
				'class': 'quick-open-tree'
			}, (div: Builder) => {
				this.tree = new Tree(div.getHTMLElement(), {
					dataSource: new DataSource(this),
195
					controller: new QuickOpenController({ clickBehavior: ClickBehavior.ON_MOUSE_UP }),
E
Erich Gamma 已提交
196
					renderer: new Renderer(this),
197 198
					filter: new Filter(this),
					accessibilityProvider: new AccessibilityProvider(this)
E
Erich Gamma 已提交
199
				}, {
B
Benjamin Pasero 已提交
200 201 202 203 204 205
						twistiePixels: 11,
						indentPixels: 0,
						alwaysFocused: true,
						verticalScrollMode: ScrollbarVisibility.Visible,
						ariaLabel: nls.localize('treeAriaLabel', "Quick Picker")
					});
E
Erich Gamma 已提交
206

207 208
				this.treeElement = this.tree.getHTMLElement();

E
Erich Gamma 已提交
209
				// Handle Focus and Selection event
A
Alex Dima 已提交
210
				this.toUnbind.push(this.tree.addListener2(EventType.FOCUS, (event: IFocusEvent) => {
E
Erich Gamma 已提交
211 212 213
					this.elementFocused(event.focus, event);
				}));

A
Alex Dima 已提交
214
				this.toUnbind.push(this.tree.addListener2(EventType.SELECTION, (event: ISelectionEvent) => {
E
Erich Gamma 已提交
215 216 217 218 219
					if (event.selection && event.selection.length > 0) {
						this.elementSelected(event.selection[0], event);
					}
				}));
			}).
B
Benjamin Pasero 已提交
220
				on(DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
B
Benjamin Pasero 已提交
221
					const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
E
Erich Gamma 已提交
222

B
Benjamin Pasero 已提交
223 224 225 226
					// Only handle when in quick navigation mode
					if (!this.quickNavigateConfiguration) {
						return;
					}
227

B
Benjamin Pasero 已提交
228 229 230
					// Support keyboard navigation in quick navigation mode
					if (keyboardEvent.keyCode === KeyCode.DownArrow || keyboardEvent.keyCode === KeyCode.UpArrow || keyboardEvent.keyCode === KeyCode.PageDown || keyboardEvent.keyCode === KeyCode.PageUp) {
						DOM.EventHelper.stop(e, true);
231

B
Benjamin Pasero 已提交
232
						this.navigateInTree(keyboardEvent.keyCode);
E
Erich Gamma 已提交
233
					}
B
Benjamin Pasero 已提交
234 235
				}).
				on(DOM.EventType.KEY_UP, (e: KeyboardEvent) => {
B
Benjamin Pasero 已提交
236 237
					const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
					const keyCode = keyboardEvent.keyCode;
E
Erich Gamma 已提交
238

B
Benjamin Pasero 已提交
239 240 241
					// Only handle when in quick navigation mode
					if (!this.quickNavigateConfiguration) {
						return;
242
					}
243

B
Benjamin Pasero 已提交
244
					// Select element when keys are pressed that signal it
B
Benjamin Pasero 已提交
245 246
					const quickNavKeys = this.quickNavigateConfiguration.keybindings;
					const wasTriggerKeyPressed = keyCode === KeyCode.Enter || quickNavKeys.some((k) => {
B
Benjamin Pasero 已提交
247 248 249 250 251
						if (k.hasShift() && keyCode === KeyCode.Shift) {
							if (keyboardEvent.ctrlKey || keyboardEvent.altKey || keyboardEvent.metaKey) {
								return false; // this is an optimistic check for the shift key being used to navigate back in quick open
							}

E
Erich Gamma 已提交
252 253 254
							return true;
						}

B
Benjamin Pasero 已提交
255
						if (k.hasAlt() && keyCode === KeyCode.Alt) {
E
Erich Gamma 已提交
256 257 258
							return true;
						}

B
Benjamin Pasero 已提交
259 260 261 262 263 264 265 266 267
						// Mac is a bit special
						if (platform.isMacintosh) {
							if (k.hasCtrlCmd() && keyCode === KeyCode.Meta) {
								return true;
							}

							if (k.hasWinCtrl() && keyCode === KeyCode.Ctrl) {
								return true;
							}
E
Erich Gamma 已提交
268 269
						}

B
Benjamin Pasero 已提交
270 271 272 273 274 275 276 277 278
						// Windows/Linux are not :)
						else {
							if (k.hasCtrlCmd() && keyCode === KeyCode.Ctrl) {
								return true;
							}

							if (k.hasWinCtrl() && keyCode === KeyCode.Meta) {
								return true;
							}
E
Erich Gamma 已提交
279 280
						}

B
Benjamin Pasero 已提交
281 282
						return false;
					});
E
Erich Gamma 已提交
283

B
Benjamin Pasero 已提交
284
					if (wasTriggerKeyPressed) {
B
Benjamin Pasero 已提交
285
						const focus = this.tree.getFocus();
B
Benjamin Pasero 已提交
286 287 288
						if (focus) {
							this.elementSelected(focus, e);
						}
E
Erich Gamma 已提交
289
					}
B
Benjamin Pasero 已提交
290 291
				}).
				clone();
E
Erich Gamma 已提交
292 293
		})

B
Benjamin Pasero 已提交
294 295 296 297
			// Widget Attributes
			.addClass('quick-open-widget')
			.addClass((browser.isIE10orEarlier) ? ' no-shadow' : '')
			.build(this.container);
E
Erich Gamma 已提交
298 299 300 301 302

		// Support layout
		if (this.layoutDimensions) {
			this.layout(this.layoutDimensions);
		}
B
Benjamin Pasero 已提交
303 304

		return this.builder.getHTMLElement();
E
Erich Gamma 已提交
305 306 307
	}

	private onType(): void {
B
Benjamin Pasero 已提交
308
		const value = this.inputBox.value;
E
Erich Gamma 已提交
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339

		// Adjust help text as needed if present
		if (this.helpText) {
			if (value) {
				this.helpText.hide();
			} else {
				this.helpText.show();
			}
		}

		// Send to callbacks
		this.callbacks.onType(value);
	}

	public quickNavigate(configuration: IQuickNavigateConfiguration, next: boolean): void {
		if (this.isVisible) {

			// Transition into quick navigate mode if not yet done
			if (!this.quickNavigateConfiguration) {
				this.quickNavigateConfiguration = configuration;
				this.tree.DOMFocus();
			}

			// Navigate
			this.navigateInTree(next ? KeyCode.DownArrow : KeyCode.UpArrow);
		}
	}

	private navigateInTree(keyCode: KeyCode, isShift?: boolean): void {
		const model: IModel<any> = this.tree.getInput();
		const entries = model ? model.entries : [];
340
		const oldFocus = this.tree.getFocus();
E
Erich Gamma 已提交
341

342 343 344 345 346
		// Normal Navigation
		switch (keyCode) {
			case KeyCode.DownArrow:
				this.tree.focusNext();
				break;
E
Erich Gamma 已提交
347

348 349 350
			case KeyCode.UpArrow:
				this.tree.focusPrevious();
				break;
E
Erich Gamma 已提交
351

352 353 354
			case KeyCode.PageDown:
				this.tree.focusNextPage();
				break;
E
Erich Gamma 已提交
355

356 357 358
			case KeyCode.PageUp:
				this.tree.focusPreviousPage();
				break;
E
Erich Gamma 已提交
359

360 361
			case KeyCode.Tab:
				if (isShift) {
E
Erich Gamma 已提交
362
					this.tree.focusPrevious();
363 364 365 366 367
				} else {
					this.tree.focusNext();
				}
				break;
		}
E
Erich Gamma 已提交
368

369
		let newFocus = this.tree.getFocus();
E
Erich Gamma 已提交
370

371 372
		// Support cycle-through navigation if focus did not change
		if (entries.length > 1 && oldFocus === newFocus) {
E
Erich Gamma 已提交
373

374 375 376 377 378 379 380 381
			// Up from no entry or first entry goes down to last
			if (keyCode === KeyCode.UpArrow || (keyCode === KeyCode.Tab && isShift)) {
				this.tree.focusLast();
			}

			// Down from last entry goes to up to first
			else if (keyCode === KeyCode.DownArrow || keyCode === KeyCode.Tab && !isShift) {
				this.tree.focusFirst();
E
Erich Gamma 已提交
382 383 384 385
			}
		}

		// Reveal
386 387 388
		newFocus = this.tree.getFocus();
		if (newFocus) {
			this.tree.reveal(newFocus).done(null, errors.onUnexpectedError);
E
Erich Gamma 已提交
389 390 391 392 393 394 395 396
		}
	}

	private elementFocused(value: any, event?: any): void {
		if (!value || !this.isVisible()) {
			return;
		}

397 398 399
		// ARIA
		this.inputElement.setAttribute('aria-activedescendant', this.treeElement.getAttribute('aria-activedescendant'));

400
		const context: IEntryRunContext = { event: event, keymods: this.extractKeyMods(event), quickNavigateConfiguration: this.quickNavigateConfiguration };
E
Erich Gamma 已提交
401 402 403
		this.model.runner.run(value, Mode.PREVIEW, context);
	}

404
	private elementSelected(value: any, event?: any, preferredMode?: Mode): void {
E
Erich Gamma 已提交
405 406 407 408
		let hide = true;

		// Trigger open of element on selection
		if (this.isVisible()) {
409
			let mode = preferredMode || Mode.OPEN;
W
Will Prater 已提交
410

411
			const context: IEntryRunContext = { event, keymods: this.extractKeyMods(event), quickNavigateConfiguration: this.quickNavigateConfiguration };
W
Will Prater 已提交
412 413

			hide = this.model.runner.run(value, mode, context);
E
Erich Gamma 已提交
414 415
		}

P
Pascal Borreli 已提交
416
		// add telemetry when an item is accepted, logging the index of the item in the list and the length of the list
E
Erich Gamma 已提交
417 418
		// to measure the rate of the success and the relevance of the order
		if (this.usageLogger) {
B
Benjamin Pasero 已提交
419 420
			const indexOfAcceptedElement = this.model.entries.indexOf(value);
			const entriesCount = this.model.entries.length;
E
Erich Gamma 已提交
421 422 423 424 425
			this.usageLogger.publicLog('quickOpenWidgetItemAccepted', { index: indexOfAcceptedElement, count: entriesCount, isQuickNavigate: this.quickNavigateConfiguration ? true : false });
		}

		// Hide if command was run successfully
		if (hide) {
426
			this.hide(HideReason.ELEMENT_SELECTED);
E
Erich Gamma 已提交
427 428
		}
	}
429 430 431 432 433

	private extractKeyMods(event: any): number[] {
		const isCtrlCmd = event && (event.ctrlKey || event.metaKey || (event.payload && event.payload.originalEvent && (event.payload.originalEvent.ctrlKey || event.payload.originalEvent.metaKey)));

		return isCtrlCmd ? [KeyMod.CtrlCmd] : [];
B
Benjamin Pasero 已提交
434
	}
E
Erich Gamma 已提交
435

B
Benjamin Pasero 已提交
436 437 438
	public show(prefix: string, options?: IShowOptions): void;
	public show(input: IModel<any>, options?: IShowOptions): void;
	public show(param: any, options?: IShowOptions): void {
E
Erich Gamma 已提交
439 440
		this.visible = true;
		this.isLoosingFocus = false;
B
Benjamin Pasero 已提交
441
		this.quickNavigateConfiguration = options ? options.quickNavigateConfiguration : void 0;
E
Erich Gamma 已提交
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469

		// Adjust UI for quick navigate mode
		if (this.quickNavigateConfiguration) {
			this.inputContainer.hide();
			this.builder.show();
			this.tree.DOMFocus();
		}

		// Otherwise use normal UI
		else {
			this.inputContainer.show();
			this.builder.show();
			this.inputBox.focus();
		}

		// Adjust Help text for IE
		if (this.helpText) {
			if (this.quickNavigateConfiguration || types.isString(param)) {
				this.helpText.hide();
			} else {
				this.helpText.show();
			}
		}

		// Show based on param
		if (types.isString(param)) {
			this.doShowWithPrefix(param);
		} else {
B
Benjamin Pasero 已提交
470
			this.doShowWithInput(param, options && options.autoFocus ? options.autoFocus : {});
E
Erich Gamma 已提交
471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487
		}

		if (this.callbacks.onShow) {
			this.callbacks.onShow();
		}
	}

	private doShowWithPrefix(prefix: string): void {
		this.inputBox.value = prefix;
		this.callbacks.onType(prefix);
	}

	private doShowWithInput(input: IModel<any>, autoFocus: IAutoFocus): void {
		this.setInput(input, autoFocus);
	}

	private setInputAndLayout(input: IModel<any>, autoFocus: IAutoFocus): void {
488
		this.treeContainer.style({ height: `${this.getHeight(input)}px` });
E
Erich Gamma 已提交
489

490 491
		this.tree.setInput(null).then(() => {
			this.model = input;
E
Erich Gamma 已提交
492

493 494
			// ARIA
			this.inputElement.setAttribute('aria-haspopup', String(input && input.entries && input.entries.length > 0));
495

496 497
			return this.tree.setInput(input);
		}).done(() => {
498

499 500
			// Indicate entries to tree
			this.tree.layout();
E
Erich Gamma 已提交
501

502 503 504
			// Handle auto focus
			if (input && input.entries.some(e => this.isElementVisible(input, e))) {
				this.autoFocus(input, autoFocus);
E
Erich Gamma 已提交
505
			}
506
		}, errors.onUnexpectedError);
E
Erich Gamma 已提交
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523
	}

	private isElementVisible<T>(input: IModel<T>, e: T): boolean {
		if (!input.filter) {
			return true;
		}

		return input.filter.isVisible(e);
	}

	private autoFocus(input: IModel<any>, autoFocus: IAutoFocus = {}): void {
		const entries = input.entries.filter(e => this.isElementVisible(input, e));

		// First check for auto focus of prefix matches
		if (autoFocus.autoFocusPrefixMatch) {
			let caseSensitiveMatch: any;
			let caseInsensitiveMatch: any;
B
Benjamin Pasero 已提交
524 525
			const prefix = autoFocus.autoFocusPrefixMatch;
			const lowerCasePrefix = prefix.toLowerCase();
E
Erich Gamma 已提交
526
			for (let i = 0; i < entries.length; i++) {
B
Benjamin Pasero 已提交
527
				const entry = entries[i];
E
Erich Gamma 已提交
528 529 530 531 532 533 534 535 536 537 538 539 540
				const label = input.dataSource.getLabel(entry);

				if (!caseSensitiveMatch && label.indexOf(prefix) === 0) {
					caseSensitiveMatch = entry;
				} else if (!caseInsensitiveMatch && label.toLowerCase().indexOf(lowerCasePrefix) === 0) {
					caseInsensitiveMatch = entry;
				}

				if (caseSensitiveMatch && caseInsensitiveMatch) {
					break;
				}
			}

B
Benjamin Pasero 已提交
541
			const entryToFocus = caseSensitiveMatch || caseInsensitiveMatch;
E
Erich Gamma 已提交
542 543
			if (entryToFocus) {
				this.tree.setFocus(entryToFocus);
544
				this.tree.reveal(entryToFocus, 0).done(null, errors.onUnexpectedError);
E
Erich Gamma 已提交
545 546 547 548 549 550 551 552

				return;
			}
		}

		// Second check for auto focus of first entry
		if (autoFocus.autoFocusFirstEntry) {
			this.tree.focusFirst();
553
			this.tree.reveal(this.tree.getFocus(), 0).done(null, errors.onUnexpectedError);
E
Erich Gamma 已提交
554 555 556 557 558 559
		}

		// Third check for specific index option
		else if (typeof autoFocus.autoFocusIndex === 'number') {
			if (entries.length > autoFocus.autoFocusIndex) {
				this.tree.focusNth(autoFocus.autoFocusIndex);
560
				this.tree.reveal(this.tree.getFocus()).done(null, errors.onUnexpectedError);
E
Erich Gamma 已提交
561 562 563
			}
		}

564
		// Check for auto focus of second entry
E
Erich Gamma 已提交
565 566 567 568 569
		else if (autoFocus.autoFocusSecondEntry) {
			if (entries.length > 1) {
				this.tree.focusNth(1);
			}
		}
570 571 572 573 574 575 576

		// Finally check for auto focus of last entry
		else if (autoFocus.autoFocusLastEntry) {
			if (entries.length > 1) {
				this.tree.focusLast();
			}
		}
E
Erich Gamma 已提交
577 578 579 580 581 582 583 584
	}

	public refresh(input: IModel<any>, autoFocus: IAutoFocus): void {
		if (!this.isVisible()) {
			return;
		}

		// Apply height & Refresh
585 586
		this.treeContainer.style({ height: `${this.getHeight(input)}px` });
		this.tree.refresh().done(() => {
B
Benjamin Pasero 已提交
587

588 589
			// Indicate entries to tree
			this.tree.layout();
E
Erich Gamma 已提交
590

591 592 593 594
			let doAutoFocus = autoFocus && input && input.entries.some(e => this.isElementVisible(input, e));
			if (doAutoFocus && !autoFocus.autoFocusPrefixMatch) {
				doAutoFocus = !this.tree.getFocus(); // if auto focus is not for prefix matches, we do not want to change what the user has focussed already
			}
E
Erich Gamma 已提交
595

596 597 598 599 600
			// Handle auto focus
			if (doAutoFocus) {
				this.autoFocus(input, autoFocus);
			}
		}, errors.onUnexpectedError);
E
Erich Gamma 已提交
601 602 603 604 605 606
	}

	private getHeight(input: IModel<any>): number {
		const renderer = input.renderer;

		if (!input) {
B
Benjamin Pasero 已提交
607
			const itemHeight = renderer.getHeight(null);
E
Erich Gamma 已提交
608 609 610 611 612 613 614 615 616 617 618 619 620 621 622

			return this.options.minItemsToShow ? this.options.minItemsToShow * itemHeight : 0;
		}

		let height = 0;

		let preferredItemsHeight: number;
		if (this.layoutDimensions && this.layoutDimensions.height) {
			preferredItemsHeight = (this.layoutDimensions.height - 50 /* subtract height of input field (30px) and some spacing (drop shadow) to fit */) * 0.40 /* max 40% of screen */;
		}

		if (!preferredItemsHeight || preferredItemsHeight > QuickOpenWidget.MAX_ITEMS_HEIGHT) {
			preferredItemsHeight = QuickOpenWidget.MAX_ITEMS_HEIGHT;
		}

B
Benjamin Pasero 已提交
623 624
		const entries = input.entries.filter(e => this.isElementVisible(input, e));
		const maxEntries = this.options.maxItemsToShow || entries.length;
E
Erich Gamma 已提交
625
		for (let i = 0; i < maxEntries && i < entries.length; i++) {
B
Benjamin Pasero 已提交
626
			const entryHeight = renderer.getHeight(entries[i]);
E
Erich Gamma 已提交
627 628 629 630 631 632 633 634 635 636
			if (height + entryHeight <= preferredItemsHeight) {
				height += entryHeight;
			} else {
				break;
			}
		}

		return height;
	}

637
	public hide(reason?: HideReason): void {
E
Erich Gamma 已提交
638 639 640 641 642 643 644 645 646
		if (!this.isVisible()) {
			return;
		}

		this.visible = false;
		this.builder.hide();
		this.builder.domBlur();

		// report failure cases
647
		if (reason === HideReason.CANCELED) {
E
Erich Gamma 已提交
648
			if (this.model) {
B
Benjamin Pasero 已提交
649
				const entriesCount = this.model.entries.filter(e => this.isElementVisible(this.model, e)).length;
E
Erich Gamma 已提交
650 651 652 653 654 655 656 657 658 659
				if (this.usageLogger) {
					this.usageLogger.publicLog('quickOpenWidgetCancelled', { count: entriesCount, isQuickNavigate: this.quickNavigateConfiguration ? true : false });
				}
			}
		}

		// Clear input field and clear tree
		this.inputBox.value = '';
		this.tree.setInput(null);

660 661 662
		// ARIA
		this.inputElement.setAttribute('aria-haspopup', 'false');

E
Erich Gamma 已提交
663
		// Reset Tree Height
I
isidor 已提交
664
		this.treeContainer.style({ height: (this.options.minItemsToShow ? this.options.minItemsToShow * 22 : 0) + 'px' });
E
Erich Gamma 已提交
665 666 667 668 669 670 671 672 673 674 675 676

		// Clear any running Progress
		this.progressBar.stop().getContainer().hide();

		// Clear Focus
		if (this.tree.isDOMFocused()) {
			this.tree.DOMBlur();
		} else if (this.inputBox.hasFocus()) {
			this.inputBox.blur();
		}

		// Callbacks
677
		if (reason === HideReason.ELEMENT_SELECTED) {
E
Erich Gamma 已提交
678
			this.callbacks.onOk();
679 680
		} else {
			this.callbacks.onCancel();
E
Erich Gamma 已提交
681 682 683
		}

		if (this.callbacks.onHide) {
684
			this.callbacks.onHide(reason);
E
Erich Gamma 已提交
685 686 687
		}
	}

688 689
	public getQuickNavigateConfiguration(): IQuickNavigateConfiguration {
		return this.quickNavigateConfiguration;
690 691
	}

E
Erich Gamma 已提交
692 693 694 695 696 697
	public setPlaceHolder(placeHolder: string): void {
		if (this.inputBox) {
			this.inputBox.setPlaceHolder(placeHolder);
		}
	}

J
Johannes Rieken 已提交
698
	public setValue(value: string, select: boolean): void {
E
Erich Gamma 已提交
699 700
		if (this.inputBox) {
			this.inputBox.value = value;
J
Johannes Rieken 已提交
701 702 703
			if (select) {
				this.inputBox.select();
			}
E
Erich Gamma 已提交
704 705 706 707 708 709 710 711 712
		}
	}

	public setPassword(isPassword: boolean): void {
		if (this.inputBox) {
			this.inputBox.inputElement.type = isPassword ? 'password' : 'text';
		}
	}

713
	public setInput(input: IModel<any>, autoFocus: IAutoFocus, ariaLabel?: string): void {
E
Erich Gamma 已提交
714 715 716 717
		if (!this.isVisible()) {
			return;
		}

718 719 720 721 722
		// If the input changes, indicate this to the tree
		if (!!this.getInput()) {
			this.onInputChanging();
		}

E
Erich Gamma 已提交
723 724
		// Adapt tree height to entries and apply input
		this.setInputAndLayout(input, autoFocus);
725 726 727 728 729

		// Apply ARIA
		if (this.inputBox) {
			this.inputBox.setAriaLabel(ariaLabel || DEFAULT_INPUT_ARIA_LABEL);
		}
E
Erich Gamma 已提交
730 731
	}

732 733 734 735 736 737 738 739 740 741 742 743 744 745
	private onInputChanging(): void {
		if (this.inputChangingTimeoutHandle) {
			clearTimeout(this.inputChangingTimeoutHandle);
			this.inputChangingTimeoutHandle = null;
		}

		// when the input is changing in quick open, we indicate this as CSS class to the widget
		// for a certain timeout. this helps reducing some hectic UI updates when input changes quickly
		this.builder.addClass('content-changing');
		this.inputChangingTimeoutHandle = setTimeout(() => {
			this.builder.removeClass('content-changing');
		}, 500);
	}

E
Erich Gamma 已提交
746 747 748 749
	public getInput(): IModel<any> {
		return this.tree.getInput();
	}

750 751 752 753 754 755 756 757 758 759 760 761
	public showInputDecoration(decoration: Severity): void {
		if (this.inputBox) {
			this.inputBox.showMessage({ type: decoration === Severity.Info ? MessageType.INFO : decoration === Severity.Warning ? MessageType.WARNING : MessageType.ERROR, content: '' });
		}
	}

	public clearInputDecoration(): void {
		if (this.inputBox) {
			this.inputBox.hideMessage();
		}
	}

762 763 764
	public focus(): void {
		if (this.isVisible() && this.inputBox) {
			this.inputBox.focus();
E
Erich Gamma 已提交
765
		}
766
	}
E
Erich Gamma 已提交
767

768 769
	public accept(): void {
		if (this.isVisible()) {
B
Benjamin Pasero 已提交
770
			const focus = this.tree.getFocus();
771 772 773 774
			if (focus) {
				this.elementSelected(focus);
			}
		}
E
Erich Gamma 已提交
775 776 777 778 779 780 781
	}

	public getProgressBar(): ProgressBar {
		return this.progressBar;
	}

	public setExtraClass(clazz: string): void {
B
Benjamin Pasero 已提交
782
		const previousClass = this.builder.getProperty('extra-class');
E
Erich Gamma 已提交
783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802
		if (previousClass) {
			this.builder.removeClass(previousClass);
		}

		if (clazz) {
			this.builder.addClass(clazz);
			this.builder.setProperty('extra-class', clazz);
		} else if (previousClass) {
			this.builder.removeProperty('extra-class');
		}
	}

	public isVisible(): boolean {
		return this.visible;
	}

	public layout(dimension: Dimension): void {
		this.layoutDimensions = dimension;

		// Apply to quick open width (height is dynamic by number of items to show)
B
Benjamin Pasero 已提交
803
		const quickOpenWidth = Math.min(this.layoutDimensions.width * 0.62 /* golden cut */, QuickOpenWidget.MAX_WIDTH);
E
Erich Gamma 已提交
804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829
		if (this.builder) {

			// quick open
			this.builder.style({
				width: quickOpenWidth + 'px',
				marginLeft: '-' + (quickOpenWidth / 2) + 'px'
			});

			// input field
			this.inputContainer.style({
				width: (quickOpenWidth - 12) + 'px'
			});
		}
	}

	private gainingFocus(): void {
		this.isLoosingFocus = false;
	}

	private loosingFocus(e: Event): void {
		if (!this.isVisible()) {
			return;
		}

		const relatedTarget = (<any>e).relatedTarget;
		if (!this.quickNavigateConfiguration && DOM.isAncestor(relatedTarget, this.builder.getHTMLElement())) {
830
			return; // user clicked somewhere into quick open widget, do not close thereby
E
Erich Gamma 已提交
831 832 833
		}

		this.isLoosingFocus = true;
834
		TPromise.timeout(0).then(() => {
E
Erich Gamma 已提交
835 836 837 838 839 840
			if (!this.isLoosingFocus) {
				return;
			}

			const veto = this.callbacks.onFocusLost && this.callbacks.onFocusLost();
			if (!veto) {
841
				this.hide(HideReason.FOCUS_LOST);
E
Erich Gamma 已提交
842 843 844 845 846
			}
		});
	}

	public dispose(): void {
A
Alex Dima 已提交
847
		this.toUnbind = dispose(this.toUnbind);
E
Erich Gamma 已提交
848 849 850 851 852 853

		this.progressBar.dispose();
		this.inputBox.dispose();
		this.tree.dispose();
	}
}