quickOpenWidget.ts 24.9 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');
9
import {TPromise} from 'vs/base/common/winjs.base';
E
Erich Gamma 已提交
10 11 12 13 14
import platform = require('vs/base/common/platform');
import browser = require('vs/base/browser/browser');
import {EventType} from 'vs/base/common/events';
import types = require('vs/base/common/types');
import errors = require('vs/base/common/errors');
15
import {IQuickNavigateConfiguration, IAutoFocus, IEntryRunContext, IModel, Mode} from 'vs/base/parts/quickopen/common/quickOpen';
16
import {Filter, Renderer, DataSource, IModelProvider, AccessibilityProvider} from 'vs/base/parts/quickopen/browser/quickOpenViewer';
E
Erich Gamma 已提交
17
import {Dimension, Builder, $} from 'vs/base/browser/builder';
J
Joao Moreno 已提交
18
import {ISelectionEvent, IFocusEvent, ITree, ContextMenuEvent} from 'vs/base/parts/tree/browser/tree';
19 20
import {InputBox, MessageType} from 'vs/base/browser/ui/inputbox/inputBox';
import Severity from 'vs/base/common/severity';
E
Erich Gamma 已提交
21 22 23 24 25 26
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';
import DOM = require('vs/base/browser/dom');
import {IActionProvider} from 'vs/base/parts/tree/browser/actionsRenderer';
27
import {KeyCode, KeyMod} from 'vs/base/common/keyCodes';
B
Benjamin Pasero 已提交
28
import {IDisposable, dispose} from 'vs/base/common/lifecycle';
29
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
162
					else if (keyboardEvent.keyCode === KeyCode.Tab || 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 169 170 171
						DOM.EventHelper.stop(e, true);

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

					// Select element on Enter
					else if (keyboardEvent.keyCode === KeyCode.Enter) {
						DOM.EventHelper.stop(e, true);

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

					// 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 404 405 406 407 408
		this.model.runner.run(value, Mode.PREVIEW, context);
	}

	private elementSelected(value: any, event?: any): void {
		let hide = true;

		// Trigger open of element on selection
		if (this.isVisible()) {
409
			const context: IEntryRunContext = { event, keymods: this.extractKeyMods(event), quickNavigateConfiguration: this.quickNavigateConfiguration };
E
Erich Gamma 已提交
410 411 412
			hide = this.model.runner.run(value, Mode.OPEN, context);
		}

P
Pascal Borreli 已提交
413
		// 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 已提交
414 415
		// to measure the rate of the success and the relevance of the order
		if (this.usageLogger) {
B
Benjamin Pasero 已提交
416 417
			const indexOfAcceptedElement = this.model.entries.indexOf(value);
			const entriesCount = this.model.entries.length;
E
Erich Gamma 已提交
418 419 420 421 422
			this.usageLogger.publicLog('quickOpenWidgetItemAccepted', { index: indexOfAcceptedElement, count: entriesCount, isQuickNavigate: this.quickNavigateConfiguration ? true : false });
		}

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

	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 已提交
431
	}
E
Erich Gamma 已提交
432

B
Benjamin Pasero 已提交
433 434 435
	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 已提交
436 437
		this.visible = true;
		this.isLoosingFocus = false;
B
Benjamin Pasero 已提交
438
		this.quickNavigateConfiguration = options ? options.quickNavigateConfiguration : void 0;
E
Erich Gamma 已提交
439 440 441 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

		// 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 已提交
467
			this.doShowWithInput(param, options && options.autoFocus ? options.autoFocus : {});
E
Erich Gamma 已提交
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484
		}

		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 {
485
		this.treeContainer.style({ height: `${this.getHeight(input)}px` });
E
Erich Gamma 已提交
486

487 488
		this.tree.setInput(null).then(() => {
			this.model = input;
E
Erich Gamma 已提交
489

490 491
			// ARIA
			this.inputElement.setAttribute('aria-haspopup', String(input && input.entries && input.entries.length > 0));
492

493 494
			return this.tree.setInput(input);
		}).done(() => {
495

496 497
			// Indicate entries to tree
			this.tree.layout();
E
Erich Gamma 已提交
498

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

	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 已提交
521 522
			const prefix = autoFocus.autoFocusPrefixMatch;
			const lowerCasePrefix = prefix.toLowerCase();
E
Erich Gamma 已提交
523
			for (let i = 0; i < entries.length; i++) {
B
Benjamin Pasero 已提交
524
				const entry = entries[i];
E
Erich Gamma 已提交
525 526 527 528 529 530 531 532 533 534 535 536 537
				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 已提交
538
			const entryToFocus = caseSensitiveMatch || caseInsensitiveMatch;
E
Erich Gamma 已提交
539 540
			if (entryToFocus) {
				this.tree.setFocus(entryToFocus);
541
				this.tree.reveal(entryToFocus, 0).done(null, errors.onUnexpectedError);
E
Erich Gamma 已提交
542 543 544 545 546 547 548 549

				return;
			}
		}

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

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

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

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

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

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

585 586
			// Indicate entries to tree
			this.tree.layout();
E
Erich Gamma 已提交
587

588 589 590 591
			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 已提交
592

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

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

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

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

		return height;
	}

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

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

		// report failure cases
644
		if (reason === HideReason.CANCELED) {
E
Erich Gamma 已提交
645
			if (this.model) {
B
Benjamin Pasero 已提交
646
				const entriesCount = this.model.entries.filter(e => this.isElementVisible(this.model, e)).length;
E
Erich Gamma 已提交
647 648 649 650 651 652 653 654 655 656
				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);

657 658 659
		// ARIA
		this.inputElement.setAttribute('aria-haspopup', 'false');

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

		// 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
674
		if (reason === HideReason.ELEMENT_SELECTED) {
E
Erich Gamma 已提交
675
			this.callbacks.onOk();
676 677
		} else {
			this.callbacks.onCancel();
E
Erich Gamma 已提交
678 679 680
		}

		if (this.callbacks.onHide) {
681
			this.callbacks.onHide(reason);
E
Erich Gamma 已提交
682 683 684
		}
	}

685 686
	public getQuickNavigateConfiguration(): IQuickNavigateConfiguration {
		return this.quickNavigateConfiguration;
687 688
	}

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

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

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

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

715 716 717 718 719
		// If the input changes, indicate this to the tree
		if (!!this.getInput()) {
			this.onInputChanging();
		}

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

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

729 730 731 732 733 734 735 736 737 738 739 740 741 742
	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 已提交
743 744 745 746
	public getInput(): IModel<any> {
		return this.tree.getInput();
	}

747 748 749 750 751 752 753 754 755 756 757 758
	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();
		}
	}

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

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

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

	public setExtraClass(clazz: string): void {
B
Benjamin Pasero 已提交
779
		const previousClass = this.builder.getProperty('extra-class');
E
Erich Gamma 已提交
780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799
		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 已提交
800
		const quickOpenWidth = Math.min(this.layoutDimensions.width * 0.62 /* golden cut */, QuickOpenWidget.MAX_WIDTH);
E
Erich Gamma 已提交
801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826
		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())) {
827
			return; // user clicked somewhere into quick open widget, do not close thereby
E
Erich Gamma 已提交
828 829 830
		}

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

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

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

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