UNPKG

sussudio

Version:

An unofficial VS Code Internal API

1,238 lines (1,237 loc) 66.1 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as dom from "../../../browser/dom.mjs"; import { StandardKeyboardEvent } from "../../../browser/keyboardEvent.mjs"; import { ActionBar } from "../../../browser/ui/actionbar/actionbar.mjs"; import { Button } from "../../../browser/ui/button/button.mjs"; import { CountBadge } from "../../../browser/ui/countBadge/countBadge.mjs"; import { renderLabelWithIcons } from "../../../browser/ui/iconLabel/iconLabels.mjs"; import { ProgressBar } from "../../../browser/ui/progressbar/progressbar.mjs"; import { Toggle } from "../../../browser/ui/toggle/toggle.mjs"; import { Action } from "../../../common/actions.mjs"; import { equals } from "../../../common/arrays.mjs"; import { TimeoutTimer } from "../../../common/async.mjs"; import { CancellationToken } from "../../../common/cancellation.mjs"; import { Codicon } from "../../../common/codicons.mjs"; import { Emitter, Event } from "../../../common/event.mjs"; import { Disposable, DisposableStore, dispose } from "../../../common/lifecycle.mjs"; import { isIOS } from "../../../common/platform.mjs"; import Severity from "../../../common/severity.mjs"; import { isString, withNullAsUndefined } from "../../../common/types.mjs"; import { getIconClass } from "./quickInputUtils.mjs"; import { ItemActivation, NO_KEY_MODS, QuickInputHideReason } from "../common/quickInput.mjs"; import "../../../../css!./media/quickInput.mjs"; import { localize } from "../../../../nls.mjs"; import { QuickInputBox } from './quickInputBox'; import { QuickInputList, QuickInputListFocus } from './quickInputList'; const $ = dom.$; const backButton = { iconClass: Codicon.quickInputBack.classNames, tooltip: localize('quickInput.back', "Back"), handle: -1 // TODO }; class QuickInput extends Disposable { ui; static noPromptMessage = localize('inputModeEntry', "Press 'Enter' to confirm your input or 'Escape' to cancel"); _title; _description; _steps; _totalSteps; visible = false; _enabled = true; _contextKey; _busy = false; _ignoreFocusOut = false; _buttons = []; buttonsUpdated = false; _toggles = []; togglesUpdated = false; noValidationMessage = QuickInput.noPromptMessage; _validationMessage; _lastValidationMessage; _severity = Severity.Ignore; _lastSeverity; onDidTriggerButtonEmitter = this._register(new Emitter()); onDidHideEmitter = this._register(new Emitter()); onDisposeEmitter = this._register(new Emitter()); visibleDisposables = this._register(new DisposableStore()); busyDelay; constructor(ui) { super(); this.ui = ui; } get title() { return this._title; } set title(title) { this._title = title; this.update(); } get description() { return this._description; } set description(description) { this._description = description; this.update(); } get step() { return this._steps; } set step(step) { this._steps = step; this.update(); } get totalSteps() { return this._totalSteps; } set totalSteps(totalSteps) { this._totalSteps = totalSteps; this.update(); } get enabled() { return this._enabled; } set enabled(enabled) { this._enabled = enabled; this.update(); } get contextKey() { return this._contextKey; } set contextKey(contextKey) { this._contextKey = contextKey; this.update(); } get busy() { return this._busy; } set busy(busy) { this._busy = busy; this.update(); } get ignoreFocusOut() { return this._ignoreFocusOut; } set ignoreFocusOut(ignoreFocusOut) { const shouldUpdate = this._ignoreFocusOut !== ignoreFocusOut && !isIOS; this._ignoreFocusOut = ignoreFocusOut && !isIOS; if (shouldUpdate) { this.update(); } } get buttons() { return this._buttons; } set buttons(buttons) { this._buttons = buttons; this.buttonsUpdated = true; this.update(); } get toggles() { return this._toggles; } set toggles(toggles) { this._toggles = toggles ?? []; this.togglesUpdated = true; this.update(); } get validationMessage() { return this._validationMessage; } set validationMessage(validationMessage) { this._validationMessage = validationMessage; this.update(); } get severity() { return this._severity; } set severity(severity) { this._severity = severity; this.update(); } onDidTriggerButton = this.onDidTriggerButtonEmitter.event; show() { if (this.visible) { return; } this.visibleDisposables.add(this.ui.onDidTriggerButton(button => { if (this.buttons.indexOf(button) !== -1) { this.onDidTriggerButtonEmitter.fire(button); } })); this.ui.show(this); // update properties in the controller that get reset in the ui.show() call this.visible = true; // This ensures the message/prompt gets rendered this._lastValidationMessage = undefined; // This ensures the input box has the right severity applied this._lastSeverity = undefined; if (this.buttons.length) { // if there are buttons, the ui.show() clears them out of the UI so we should // rerender them. this.buttonsUpdated = true; } if (this.toggles.length) { // if there are toggles, the ui.show() clears them out of the UI so we should // rerender them. this.togglesUpdated = true; } this.update(); } hide() { if (!this.visible) { return; } this.ui.hide(); } didHide(reason = QuickInputHideReason.Other) { this.visible = false; this.visibleDisposables.clear(); this.onDidHideEmitter.fire({ reason }); } onDidHide = this.onDidHideEmitter.event; update() { if (!this.visible) { return; } const title = this.getTitle(); if (title && this.ui.title.textContent !== title) { this.ui.title.textContent = title; } else if (!title && this.ui.title.innerHTML !== ' ') { this.ui.title.innerText = '\u00a0'; } const description = this.getDescription(); if (this.ui.description1.textContent !== description) { this.ui.description1.textContent = description; } if (this.ui.description2.textContent !== description) { this.ui.description2.textContent = description; } if (this.busy && !this.busyDelay) { this.busyDelay = new TimeoutTimer(); this.busyDelay.setIfNotSet(() => { if (this.visible) { this.ui.progressBar.infinite(); } }, 800); } if (!this.busy && this.busyDelay) { this.ui.progressBar.stop(); this.busyDelay.cancel(); this.busyDelay = undefined; } if (this.buttonsUpdated) { this.buttonsUpdated = false; this.ui.leftActionBar.clear(); const leftButtons = this.buttons.filter(button => button === backButton); this.ui.leftActionBar.push(leftButtons.map((button, index) => { const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, async () => { this.onDidTriggerButtonEmitter.fire(button); }); action.tooltip = button.tooltip || ''; 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) => { const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, async () => { this.onDidTriggerButtonEmitter.fire(button); }); action.tooltip = button.tooltip || ''; return action; }), { icon: true, label: false }); } if (this.togglesUpdated) { this.togglesUpdated = false; // HACK: Filter out toggles here that are not concrete Toggle objects. This is to workaround // a layering issue as quick input's interface is in common but Toggle is in browser and // it requires a HTMLElement on its interface const concreteToggles = this.toggles?.filter(opts => opts instanceof Toggle) ?? []; this.ui.inputBox.toggles = concreteToggles; } this.ui.ignoreFocusOut = this.ignoreFocusOut; this.ui.setEnabled(this.enabled); this.ui.setContextKey(this.contextKey); const validationMessage = this.validationMessage || this.noValidationMessage; if (this._lastValidationMessage !== validationMessage) { this._lastValidationMessage = validationMessage; dom.reset(this.ui.message, ...renderLabelWithIcons(validationMessage)); } if (this._lastSeverity !== this.severity) { this._lastSeverity = this.severity; this.showMessageDecoration(this.severity); } } getTitle() { if (this.title && this.step) { return `${this.title} (${this.getSteps()})`; } if (this.title) { return this.title; } if (this.step) { return this.getSteps(); } return ''; } getDescription() { return this.description || ''; } getSteps() { if (this.step && this.totalSteps) { return localize('quickInput.steps', "{0}/{1}", this.step, this.totalSteps); } if (this.step) { return String(this.step); } return ''; } showMessageDecoration(severity) { this.ui.inputBox.showDecoration(severity); if (severity !== Severity.Ignore) { const styles = this.ui.inputBox.stylesForType(severity); this.ui.message.style.color = styles.foreground ? `${styles.foreground}` : ''; this.ui.message.style.backgroundColor = styles.background ? `${styles.background}` : ''; this.ui.message.style.border = styles.border ? `1px solid ${styles.border}` : ''; this.ui.message.style.marginBottom = '-2px'; } else { this.ui.message.style.color = ''; this.ui.message.style.backgroundColor = ''; this.ui.message.style.border = ''; this.ui.message.style.marginBottom = ''; } } onDispose = this.onDisposeEmitter.event; dispose() { this.hide(); this.onDisposeEmitter.fire(); super.dispose(); } } class QuickPick extends QuickInput { static DEFAULT_ARIA_LABEL = localize('quickInputBox.ariaLabel', "Type to narrow down results."); _value = ''; _ariaLabel; _placeholder; onDidChangeValueEmitter = this._register(new Emitter()); onWillAcceptEmitter = this._register(new Emitter()); onDidAcceptEmitter = this._register(new Emitter()); onDidCustomEmitter = this._register(new Emitter()); _items = []; itemsUpdated = false; _canSelectMany = false; _canAcceptInBackground = false; _matchOnDescription = false; _matchOnDetail = false; _matchOnLabel = true; _matchOnLabelMode = 'fuzzy'; _sortByLabel = true; _autoFocusOnList = true; _keepScrollPosition = false; _itemActivation = this.ui.isScreenReaderOptimized() ? ItemActivation.NONE /* https://github.com/microsoft/vscode/issues/57501 */ : ItemActivation.FIRST; _activeItems = []; activeItemsUpdated = false; activeItemsToConfirm = []; onDidChangeActiveEmitter = this._register(new Emitter()); _selectedItems = []; selectedItemsUpdated = false; selectedItemsToConfirm = []; onDidChangeSelectionEmitter = this._register(new Emitter()); onDidTriggerItemButtonEmitter = this._register(new Emitter()); onDidTriggerSeparatorButtonEmitter = this._register(new Emitter()); _valueSelection; valueSelectionUpdated = true; _ok = 'default'; _customButton = false; _customButtonLabel; _customButtonHover; _quickNavigate; _hideInput; _hideCheckAll; get quickNavigate() { return this._quickNavigate; } set quickNavigate(quickNavigate) { this._quickNavigate = quickNavigate; this.update(); } get value() { return this._value; } set value(value) { this.doSetValue(value); } doSetValue(value, skipUpdate) { if (this._value !== value) { this._value = value; if (!skipUpdate) { this.update(); } if (this.visible) { const didFilter = this.ui.list.filter(this.filterValue(this._value)); if (didFilter) { this.trySelectFirst(); } } this.onDidChangeValueEmitter.fire(this._value); } } filterValue = (value) => value; set ariaLabel(ariaLabel) { this._ariaLabel = ariaLabel; this.update(); } get ariaLabel() { return this._ariaLabel; } get placeholder() { return this._placeholder; } set placeholder(placeholder) { this._placeholder = placeholder; this.update(); } onDidChangeValue = this.onDidChangeValueEmitter.event; onWillAccept = this.onWillAcceptEmitter.event; onDidAccept = this.onDidAcceptEmitter.event; onDidCustom = this.onDidCustomEmitter.event; get items() { return this._items; } get scrollTop() { return this.ui.list.scrollTop; } set scrollTop(scrollTop) { this.ui.list.scrollTop = scrollTop; } set items(items) { this._items = items; this.itemsUpdated = true; this.update(); } get canSelectMany() { return this._canSelectMany; } set canSelectMany(canSelectMany) { this._canSelectMany = canSelectMany; this.update(); } get canAcceptInBackground() { return this._canAcceptInBackground; } set canAcceptInBackground(canAcceptInBackground) { this._canAcceptInBackground = canAcceptInBackground; } get matchOnDescription() { return this._matchOnDescription; } set matchOnDescription(matchOnDescription) { this._matchOnDescription = matchOnDescription; this.update(); } get matchOnDetail() { return this._matchOnDetail; } set matchOnDetail(matchOnDetail) { this._matchOnDetail = matchOnDetail; this.update(); } get matchOnLabel() { return this._matchOnLabel; } set matchOnLabel(matchOnLabel) { this._matchOnLabel = matchOnLabel; this.update(); } get matchOnLabelMode() { return this._matchOnLabelMode; } set matchOnLabelMode(matchOnLabelMode) { this._matchOnLabelMode = matchOnLabelMode; this.update(); } get sortByLabel() { return this._sortByLabel; } set sortByLabel(sortByLabel) { this._sortByLabel = sortByLabel; this.update(); } get autoFocusOnList() { return this._autoFocusOnList; } set autoFocusOnList(autoFocusOnList) { this._autoFocusOnList = autoFocusOnList; this.update(); } get keepScrollPosition() { return this._keepScrollPosition; } set keepScrollPosition(keepScrollPosition) { this._keepScrollPosition = keepScrollPosition; } get itemActivation() { return this._itemActivation; } set itemActivation(itemActivation) { this._itemActivation = itemActivation; } get activeItems() { return this._activeItems; } set activeItems(activeItems) { this._activeItems = activeItems; this.activeItemsUpdated = true; this.update(); } onDidChangeActive = this.onDidChangeActiveEmitter.event; get selectedItems() { return this._selectedItems; } set selectedItems(selectedItems) { this._selectedItems = selectedItems; this.selectedItemsUpdated = true; this.update(); } get keyMods() { 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; } return this.ui.keyMods; } set valueSelection(valueSelection) { this._valueSelection = valueSelection; this.valueSelectionUpdated = true; this.update(); } get customButton() { return this._customButton; } set customButton(showCustomButton) { this._customButton = showCustomButton; this.update(); } get customLabel() { return this._customButtonLabel; } set customLabel(label) { this._customButtonLabel = label; this.update(); } get customHover() { return this._customButtonHover; } set customHover(hover) { this._customButtonHover = hover; this.update(); } get ok() { return this._ok; } set ok(showOkButton) { this._ok = showOkButton; this.update(); } inputHasFocus() { return this.visible ? this.ui.inputBox.hasFocus() : false; } focusOnInput() { this.ui.inputBox.setFocus(); } get hideInput() { return !!this._hideInput; } set hideInput(hideInput) { this._hideInput = hideInput; this.update(); } get hideCheckAll() { return !!this._hideCheckAll; } set hideCheckAll(hideCheckAll) { this._hideCheckAll = hideCheckAll; this.update(); } onDidChangeSelection = this.onDidChangeSelectionEmitter.event; onDidTriggerItemButton = this.onDidTriggerItemButtonEmitter.event; onDidTriggerSeparatorButton = this.onDidTriggerSeparatorButtonEmitter.event; trySelectFirst() { if (this.autoFocusOnList) { if (!this.canSelectMany) { this.ui.list.focus(QuickInputListFocus.First); } } } show() { if (!this.visible) { this.visibleDisposables.add(this.ui.inputBox.onDidChange(value => { this.doSetValue(value, true /* skip update since this originates from the UI */); })); this.visibleDisposables.add(this.ui.inputBox.onMouseDown(event => { if (!this.autoFocusOnList) { this.ui.list.clearFocus(); } })); this.visibleDisposables.add((this._hideInput ? this.ui.list : this.ui.inputBox).onKeyDown((event) => { switch (event.keyCode) { case 18 /* KeyCode.DownArrow */: this.ui.list.focus(QuickInputListFocus.Next); if (this.canSelectMany) { this.ui.list.domFocus(); } dom.EventHelper.stop(event, true); break; case 16 /* KeyCode.UpArrow */: if (this.ui.list.getFocusedElements().length) { this.ui.list.focus(QuickInputListFocus.Previous); } else { this.ui.list.focus(QuickInputListFocus.Last); } if (this.canSelectMany) { this.ui.list.domFocus(); } dom.EventHelper.stop(event, true); break; case 12 /* KeyCode.PageDown */: this.ui.list.focus(QuickInputListFocus.NextPage); if (this.canSelectMany) { this.ui.list.domFocus(); } dom.EventHelper.stop(event, true); break; case 11 /* KeyCode.PageUp */: this.ui.list.focus(QuickInputListFocus.PreviousPage); if (this.canSelectMany) { this.ui.list.domFocus(); } dom.EventHelper.stop(event, true); break; case 17 /* 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.handleAccept(true); } break; case 14 /* KeyCode.Home */: if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) { this.ui.list.focus(QuickInputListFocus.First); dom.EventHelper.stop(event, true); } break; case 13 /* KeyCode.End */: if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) { this.ui.list.focus(QuickInputListFocus.Last); dom.EventHelper.stop(event, true); } break; } })); this.visibleDisposables.add(this.ui.onDidAccept(() => { if (this.canSelectMany) { // if there are no checked elements, it means that an onDidChangeSelection never fired to overwrite // `_selectedItems`. In that case, we should emit one with an empty array to ensure that // `.selectedItems` is up to date. if (!this.ui.list.getCheckedElements().length) { this._selectedItems = []; this.onDidChangeSelectionEmitter.fire(this.selectedItems); } } else if (this.activeItems[0]) { // For single-select, we set `selectedItems` to the item that was accepted. this._selectedItems = [this.activeItems[0]]; this.onDidChangeSelectionEmitter.fire(this.selectedItems); } this.handleAccept(false); })); this.visibleDisposables.add(this.ui.onDidCustom(() => { this.onDidCustomEmitter.fire(); })); 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; this.onDidChangeActiveEmitter.fire(focusedItems); })); this.visibleDisposables.add(this.ui.list.onDidChangeSelection(({ items: selectedItems, event }) => { if (this.canSelectMany) { if (selectedItems.length) { this.ui.list.setSelectedElements([]); } return; } if (this.selectedItemsToConfirm !== this._selectedItems && equals(selectedItems, this._selectedItems, (a, b) => a === b)) { return; } this._selectedItems = selectedItems; this.onDidChangeSelectionEmitter.fire(selectedItems); if (selectedItems.length) { this.handleAccept(event instanceof MouseEvent && event.button === 1 /* mouse middle click */); } })); 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; this.onDidChangeSelectionEmitter.fire(checkedItems); })); this.visibleDisposables.add(this.ui.list.onButtonTriggered(event => this.onDidTriggerItemButtonEmitter.fire(event))); this.visibleDisposables.add(this.ui.list.onSeparatorButtonTriggered(event => this.onDidTriggerSeparatorButtonEmitter.fire(event))); this.visibleDisposables.add(this.registerQuickNavigation()); this.valueSelectionUpdated = true; } super.show(); // TODO: Why have show() bubble up while update() trickles down? (Could move setComboboxAccessibility() here.) } handleAccept(inBackground) { // Figure out veto via `onWillAccept` event let veto = false; this.onWillAcceptEmitter.fire({ veto: () => veto = true }); // Continue with `onDidAccept` if no veto if (!veto) { this.onDidAcceptEmitter.fire({ inBackground }); } } registerQuickNavigation() { return dom.addDisposableListener(this.ui.container, dom.EventType.KEY_UP, e => { if (this.canSelectMany || !this._quickNavigate) { return; } const keyboardEvent = new StandardKeyboardEvent(e); const keyCode = keyboardEvent.keyCode; // Select element when keys are pressed that signal it const quickNavKeys = this._quickNavigate.keybindings; const wasTriggerKeyPressed = quickNavKeys.some(k => { const [firstChord, secondChord] = k.getChords(); // TODO@chords if (secondChord) { return false; } if (firstChord.shiftKey && keyCode === 4 /* 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 input } return true; } if (firstChord.altKey && keyCode === 6 /* KeyCode.Alt */) { return true; } if (firstChord.ctrlKey && keyCode === 5 /* KeyCode.Ctrl */) { return true; } if (firstChord.metaKey && keyCode === 57 /* KeyCode.Meta */) { return true; } return false; }); if (wasTriggerKeyPressed) { if (this.activeItems[0]) { this._selectedItems = [this.activeItems[0]]; this.onDidChangeSelectionEmitter.fire(this.selectedItems); this.handleAccept(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; } }); } update() { if (!this.visible) { return; } // store the scrollTop before it is reset const scrollTopBefore = this.keepScrollPosition ? this.scrollTop : 0; const hasDescription = !!this.description; const visibilities = { title: !!this.title || !!this.step || !!this.buttons.length, description: hasDescription, checkAll: this.canSelectMany && !this._hideCheckAll, checkBox: this.canSelectMany, inputBox: !this._hideInput, progressBar: !this._hideInput || hasDescription, visibleCount: true, count: this.canSelectMany, ok: this.ok === 'default' ? this.canSelectMany : this.ok, list: true, message: !!this.validationMessage, customButton: this.customButton }; this.ui.setVisibilities(visibilities); super.update(); if (this.ui.inputBox.value !== this.value) { this.ui.inputBox.value = this.value; } if (this.valueSelectionUpdated) { this.valueSelectionUpdated = false; this.ui.inputBox.select(this._valueSelection && { start: this._valueSelection[0], end: this._valueSelection[1] }); } if (this.ui.inputBox.placeholder !== (this.placeholder || '')) { this.ui.inputBox.placeholder = (this.placeholder || ''); } let ariaLabel = this.ariaLabel; if (!ariaLabel) { ariaLabel = this.placeholder || QuickPick.DEFAULT_ARIA_LABEL; // If we have a title, include it in the aria label. if (this.title) { ariaLabel += ` - ${this.title}`; } } if (this.ui.inputBox.ariaLabel !== ariaLabel) { this.ui.inputBox.ariaLabel = ariaLabel; } this.ui.list.matchOnDescription = this.matchOnDescription; this.ui.list.matchOnDetail = this.matchOnDetail; this.ui.list.matchOnLabel = this.matchOnLabel; this.ui.list.matchOnLabelMode = this.matchOnLabelMode; this.ui.list.sortByLabel = this.sortByLabel; if (this.itemsUpdated) { this.itemsUpdated = false; this.ui.list.setElements(this.items); this.ui.list.filter(this.filterValue(this.ui.inputBox.value)); this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked(); this.ui.visibleCount.setCount(this.ui.list.getVisibleCount()); this.ui.count.setCount(this.ui.list.getCheckedCount()); switch (this._itemActivation) { case ItemActivation.NONE: this._itemActivation = ItemActivation.FIRST; // only valid once, then unset break; 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; } } if (this.ui.container.classList.contains('show-checkboxes') !== !!this.canSelectMany) { if (this.canSelectMany) { this.ui.list.clearFocus(); } else { this.trySelectFirst(); } } if (this.activeItemsUpdated) { this.activeItemsUpdated = false; this.activeItemsToConfirm = this._activeItems; this.ui.list.setFocusedElements(this.activeItems); if (this.activeItemsToConfirm === this._activeItems) { this.activeItemsToConfirm = null; } } if (this.selectedItemsUpdated) { this.selectedItemsUpdated = false; this.selectedItemsToConfirm = this._selectedItems; if (this.canSelectMany) { this.ui.list.setCheckedElements(this.selectedItems); } else { this.ui.list.setSelectedElements(this.selectedItems); } if (this.selectedItemsToConfirm === this._selectedItems) { this.selectedItemsToConfirm = null; } } this.ui.customButton.label = this.customLabel || ''; this.ui.customButton.element.title = this.customHover || ''; this.ui.setComboboxAccessibility(true); 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(); // Focus the first element in the list if multiselect is enabled if (this.canSelectMany) { this.ui.list.focus(QuickInputListFocus.First); } } // Set the scroll position to what it was before updating the items if (this.keepScrollPosition) { this.scrollTop = scrollTopBefore; } } } class InputBox extends QuickInput { _value = ''; _valueSelection; valueSelectionUpdated = true; _placeholder; _password = false; _prompt; onDidValueChangeEmitter = this._register(new Emitter()); onDidAcceptEmitter = this._register(new Emitter()); get value() { return this._value; } set value(value) { this._value = value || ''; this.update(); } set valueSelection(valueSelection) { this._valueSelection = valueSelection; this.valueSelectionUpdated = true; this.update(); } get placeholder() { return this._placeholder; } set placeholder(placeholder) { this._placeholder = placeholder; this.update(); } get password() { return this._password; } set password(password) { this._password = password; this.update(); } get prompt() { return this._prompt; } set prompt(prompt) { this._prompt = prompt; this.noValidationMessage = prompt ? localize('inputModeEntryDescription', "{0} (Press 'Enter' to confirm or 'Escape' to cancel)", prompt) : QuickInput.noPromptMessage; this.update(); } onDidChangeValue = this.onDidValueChangeEmitter.event; onDidAccept = this.onDidAcceptEmitter.event; show() { if (!this.visible) { this.visibleDisposables.add(this.ui.inputBox.onDidChange(value => { if (value === this.value) { return; } this._value = value; this.onDidValueChangeEmitter.fire(value); })); this.visibleDisposables.add(this.ui.onDidAccept(() => this.onDidAcceptEmitter.fire())); this.valueSelectionUpdated = true; } super.show(); } update() { if (!this.visible) { return; } this.ui.container.classList.remove('hidden-input'); const visibilities = { title: !!this.title || !!this.step || !!this.buttons.length, description: !!this.description || !!this.step, inputBox: true, message: true }; this.ui.setVisibilities(visibilities); super.update(); if (this.ui.inputBox.value !== this.value) { this.ui.inputBox.value = this.value; } if (this.valueSelectionUpdated) { this.valueSelectionUpdated = false; this.ui.inputBox.select(this._valueSelection && { start: this._valueSelection[0], end: this._valueSelection[1] }); } if (this.ui.inputBox.placeholder !== (this.placeholder || '')) { this.ui.inputBox.placeholder = (this.placeholder || ''); } if (this.ui.inputBox.password !== this.password) { this.ui.inputBox.password = this.password; } } } export class QuickInputController extends Disposable { options; static MAX_WIDTH = 600; // Max total width of quick input widget idPrefix; ui; dimension; titleBarOffset; comboboxAccessibility = false; enabled = true; onDidAcceptEmitter = this._register(new Emitter()); onDidCustomEmitter = this._register(new Emitter()); onDidTriggerButtonEmitter = this._register(new Emitter()); keyMods = { ctrlCmd: false, alt: false }; controller = null; parentElement; styles; onShowEmitter = this._register(new Emitter()); onShow = this.onShowEmitter.event; onHideEmitter = this._register(new Emitter()); onHide = this.onHideEmitter.event; previousFocusElement; constructor(options) { super(); this.options = options; this.idPrefix = options.idPrefix; this.parentElement = options.container; this.styles = options.styles; this.registerKeyModsListeners(); } registerKeyModsListeners() { const listener = (e) => { 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)); } getUI() { if (this.ui) { return this.ui; } const container = dom.append(this.parentElement, $('.quick-input-widget.show-file-icons')); container.tabIndex = -1; container.style.display = 'none'; const styleSheet = dom.createStyleSheet(container); const titleBar = dom.append(container, $('.quick-input-titlebar')); const leftActionBar = this._register(new ActionBar(titleBar)); leftActionBar.domNode.classList.add('quick-input-left-action-bar'); const title = dom.append(titleBar, $('.quick-input-title')); const rightActionBar = this._register(new ActionBar(titleBar)); rightActionBar.domNode.classList.add('quick-input-right-action-bar'); const description1 = dom.append(container, $('.quick-input-description')); const headerContainer = dom.append(container, $('.quick-input-header')); const checkAll = dom.append(headerContainer, $('input.quick-input-check-all')); checkAll.type = 'checkbox'; checkAll.setAttribute('aria-label', localize('quickInput.checkAll', "Toggle all checkboxes")); this._register(dom.addStandardDisposableListener(checkAll, dom.EventType.CHANGE, e => { const checked = checkAll.checked; list.setAllVisibleChecked(checked); })); this._register(dom.addDisposableListener(checkAll, dom.EventType.CLICK, e => { if (e.x || e.y) { // Avoid 'click' triggered by 'space'... inputBox.setFocus(); } })); const description2 = dom.append(headerContainer, $('.quick-input-description')); const extraContainer = dom.append(headerContainer, $('.quick-input-and-message')); const filterContainer = dom.append(extraContainer, $('.quick-input-filter')); const inputBox = this._register(new QuickInputBox(filterContainer, this.styles.inputBox, this.styles.toggle)); inputBox.setAttribute('aria-describedby', `${this.idPrefix}message`); 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") }, this.styles.countBadge); 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") }, this.styles.countBadge); const okContainer = dom.append(headerContainer, $('.quick-input-action')); const ok = new Button(okContainer, this.styles.button); ok.label = localize('ok', "OK"); this._register(ok.onDidClick(e => { this.onDidAcceptEmitter.fire(); })); const customButtonContainer = dom.append(headerContainer, $('.quick-input-action')); const customButton = new Button(customButtonContainer, this.styles.button); customButton.label = localize('custom', "Custom"); this._register(customButton.onDidClick(e => { this.onDidCustomEmitter.fire(); })); const message = dom.append(extraContainer, $(`#${this.idPrefix}message.quick-input-message`)); const progressBar = new ProgressBar(container, this.styles.progressBar); progressBar.getContainer().classList.add('quick-input-progress'); const list = this._register(new QuickInputList(container, this.idPrefix + 'list', this.options)); this._register(list.onChangedAllVisibleChecked(checked => { checkAll.checked = checked; })); this._register(list.onChangedVisibleCount(c => { visibleCount.setCount(c); })); this._register(list.onChangedCheckedCount(c => { count.setCount(c); })); this._register(list.onLeave(() => { // Defer to avoid the input field reacting to the triggering key. setTimeout(() => { inputBox.setFocus(); if (this.controller instanceof QuickPick && this.controller.canSelectMany) { list.clearFocus(); } }, 0); })); this._register(list.onDidChangeFocus(() => { if (this.comboboxAccessibility) { this.getUI().inputBox.setAttribute('aria-activedescendant', this.getUI().list.getActiveDescendant() || ''); } })); const focusTracker = dom.trackFocus(container); this._register(focusTracker); this._register(dom.addDisposableListener(container, dom.EventType.FOCUS, e => { this.previousFocusElement = e.relatedTarget instanceof HTMLElement ? e.relatedTarget : undefined; }, true)); this._register(focusTracker.onDidBlur(() => { if (!this.getUI().ignoreFocusOut && !this.options.ignoreFocusOut()) { this.hide(QuickInputHideReason.Blur); } this.previousFocusElement = undefined; })); this._register(dom.addDisposableListener(container, dom.EventType.FOCUS, (e) => { inputBox.setFocus(); })); this._register(dom.addDisposableListener(container, dom.EventType.KEY_DOWN, (e) => { const event = new StandardKeyboardEvent(e); switch (event.keyCode) { case 3 /* KeyCode.Enter */: dom.EventHelper.stop(e, true); if (this.enabled) { this.onDidAcceptEmitter.fire(); } break; case 9 /* KeyCode.Escape */: dom.EventHelper.stop(e, true); this.hide(QuickInputHideReason.Gesture); break; case 2 /* KeyCode.Tab */: if (!event.altKey && !event.ctrlKey && !event.metaKey) { const selectors = ['.action-label.codicon']; if (container.classList.contains('show-checkboxes')) { selectors.push('input'); } else { selectors.push('input[type=text]'); } if (this.getUI().list.isDisplayed()) { selectors.push('.monaco-list'); } const stops = container.querySelectorAll(selectors.join(', ')); if (event.shiftKey && event.target === stops[0]) { dom.EventHelper.stop(e, true); stops[stops.length - 1].focus(); } else if (!event.shiftKey && event.target === stops[stops.length - 1]) { dom.EventHelper.stop(e, true); stops[0].focus(); } } break; } })); this.ui = { container, styleSheet, leftActionBar, titleBar, title, description1, description2, rightActionBar, checkAll, filterContainer, inputBox, visibleCountContainer, visibleCount, countContainer, count, okContainer, ok, message, customButtonContainer, customButton, list, progressBar, onDidAccept: this.onDidAcceptEmitter.event, onDidCustom: this.onDidCustomEmitter.event, onDidTriggerButton: this.onDidTriggerButtonEmitter.event, ignoreFocusOut: false, keyMods: this.keyMods, isScreenReaderOptimized: () => this.options.isScreenReaderOptimized(), show: controller => this.show(controller), hide: () => this.hide(), setVisibilities: visibilities => this.setVisibilities(visibilities), setComboboxAccessibility: enabled => this.setComboboxAccessibility(enabled), setEnabled: enabled => this.setEnabled(enabled), setContextKey: contextKey => this.options.setContextKey(contextKey), }; this.updateStyles(); return this.ui; } pick(picks, options = {}, token = CancellationToken.None) { return new Promise((doResolve, reject) => { let resolve = (result) => { resolve = doResolve; options.onKeyMods?.(input.keyMods); doResolve(result); }; if (token.isCancellationRequested) { resolve(undefined); return; } const input = this.createQuickPick(); let activeItem; const disposables = [ input, input.onDidAccept(() => { if (input.canSelectMany) { resolve(input.selectedItems.slice()); input.hide(); } else { const result = input.activeItems[0]; if (result) { resolve(result); input.hide(); } } }), input.onDidChangeActive(items => { const focused = items[0]; if (focused && options.onDidFocus) { options.onDidFocus(focused); } }), input.onDidChangeSelection(items => { if (!input.canSelectMany) { const result = items[0]; if (result) { resolve(result); input.hide(); } } }), input.onDidTriggerItemButton(event => options.onDidTriggerItemButton && options.onDidTriggerItemButton({ ...event, removeItem: () => { const index = input.items.indexOf(event.item); if (index !== -1) { const items = input.items.slice(); const removed = items.splice(index, 1); const activeItems = input.activeItems.filter(activeItem => activeItem !== removed[0]); const keepScrollPositionBefore = input.keepScrollPosition; input.keepScrollPosition = true; input.items = items; if (activeItems) { input.activeItems = activeItems; } input.keepScrollPosition = keepScrollPositionBefore; } } })),