UNPKG

monaco-editor

Version:
563 lines (562 loc) • 28.8 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 '../../../base/browser/dom.js'; import { ActionBar } from '../../../base/browser/ui/actionbar/actionbar.js'; import { Button } from '../../../base/browser/ui/button/button.js'; import { CountBadge } from '../../../base/browser/ui/countBadge/countBadge.js'; import { ProgressBar } from '../../../base/browser/ui/progressbar/progressbar.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable, dispose } from '../../../base/common/lifecycle.js'; import Severity from '../../../base/common/severity.js'; import { localize } from '../../../nls.js'; import { QuickInputHideReason } from '../common/quickInput.js'; import { QuickInputBox } from './quickInputBox.js'; import { QuickInputList } from './quickInputList.js'; import { QuickPick, backButton, InputBox } from './quickInput.js'; import { mainWindow } from '../../../base/browser/window.js'; const $ = dom.$; export class QuickInputController extends Disposable { constructor(options, themeService, layoutService) { super(); this.options = options; this.themeService = themeService; this.layoutService = layoutService; this.enabled = true; this.onDidAcceptEmitter = this._register(new Emitter()); this.onDidCustomEmitter = this._register(new Emitter()); this.onDidTriggerButtonEmitter = this._register(new Emitter()); this.keyMods = { ctrlCmd: false, alt: false }; this.controller = null; this.onShowEmitter = this._register(new Emitter()); this.onShow = this.onShowEmitter.event; this.onHideEmitter = this._register(new Emitter()); this.onHide = this.onHideEmitter.event; this.idPrefix = options.idPrefix; this.parentElement = options.container; this.styles = options.styles; this._register(Event.runAndSubscribe(dom.onDidRegisterWindow, ({ window, disposables }) => this.registerKeyModsListeners(window, disposables), { window: mainWindow, disposables: this._store })); this._register(dom.onWillUnregisterWindow(window => { if (this.ui && dom.getWindow(this.ui.container) === window) { // The window this quick input is contained in is about to // close, so we have to make sure to reparent it back to an // existing parent to not loose functionality. // (https://github.com/microsoft/vscode/issues/195870) this.reparentUI(this.layoutService.mainContainer); } })); } registerKeyModsListeners(window, disposables) { const listener = (e) => { this.keyMods.ctrlCmd = e.ctrlKey || e.metaKey; this.keyMods.alt = e.altKey; }; for (const event of [dom.EventType.KEY_DOWN, dom.EventType.KEY_UP, dom.EventType.MOUSE_DOWN]) { disposables.add(dom.addDisposableListener(window, event, listener, true)); } } getUI(showInActiveContainer) { if (this.ui) { // In order to support aux windows, re-parent the controller // if the original event is from a different document if (showInActiveContainer) { if (this.parentElement.ownerDocument !== this.layoutService.activeContainer.ownerDocument) { this.reparentUI(this.layoutService.activeContainer); } } 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, { hoverDelegate: this.options.hoverDelegate })); leftActionBar.domNode.classList.add('quick-input-left-action-bar'); const title = dom.append(titleBar, $('.quick-input-title')); const rightActionBar = this._register(new ActionBar(titleBar, { hoverDelegate: this.options.hoverDelegate })); rightActionBar.domNode.classList.add('quick-input-right-action-bar'); 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 inputContainer = dom.append(headerContainer, $('.quick-input-and-message')); const filterContainer = dom.append(inputContainer, $('.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 = this._register(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 = this._register(new Button(customButtonContainer, { ...this.styles.button, supportIcons: true })); customButton.label = localize('custom', "Custom"); this._register(customButton.onDidClick(e => { this.onDidCustomEmitter.fire(); })); const message = dom.append(inputContainer, $(`#${this.idPrefix}message.quick-input-message`)); const progressBar = this._register(new ProgressBar(container, this.styles.progressBar)); progressBar.getContainer().classList.add('quick-input-progress'); const widget = dom.append(container, $('.quick-input-html-widget')); widget.tabIndex = -1; const description1 = dom.append(container, $('.quick-input-description')); const listId = this.idPrefix + 'list'; const list = this._register(new QuickInputList(container, listId, this.options, this.themeService)); inputBox.setAttribute('aria-controls', listId); this._register(list.onDidChangeFocus(() => { var _a; inputBox.setAttribute('aria-activedescendant', (_a = list.getActiveDescendant()) !== null && _a !== void 0 ? _a : ''); })); 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); })); const focusTracker = dom.trackFocus(container); this._register(focusTracker); this._register(dom.addDisposableListener(container, dom.EventType.FOCUS, e => { // Ignore focus events within container if (dom.isAncestor(e.relatedTarget, container)) { return; } 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(); })); // TODO: Turn into commands instead of handling KEY_DOWN this._register(dom.addStandardDisposableListener(container, dom.EventType.KEY_DOWN, (event) => { if (dom.isAncestor(event.target, widget)) { return; // Ignore event if target is inside widget to allow the widget to handle the event. } switch (event.keyCode) { case 3 /* KeyCode.Enter */: dom.EventHelper.stop(event, true); if (this.enabled) { this.onDidAcceptEmitter.fire(); } break; case 9 /* KeyCode.Escape */: dom.EventHelper.stop(event, true); this.hide(QuickInputHideReason.Gesture); break; case 2 /* KeyCode.Tab */: if (!event.altKey && !event.ctrlKey && !event.metaKey) { // detect only visible actions const selectors = [ '.quick-input-list .monaco-action-bar .always-visible', '.quick-input-list-entry:hover .monaco-action-bar', '.monaco-list-row.focused .monaco-action-bar' ]; if (container.classList.contains('show-checkboxes')) { selectors.push('input'); } else { selectors.push('input[type=text]'); } if (this.getUI().list.isDisplayed()) { selectors.push('.monaco-list'); } // focus links if there are any if (this.getUI().message) { selectors.push('.quick-input-message a'); } if (this.getUI().widget) { if (dom.isAncestor(event.target, this.getUI().widget)) { // let the widget control tab break; } selectors.push('.quick-input-html-widget'); } const stops = container.querySelectorAll(selectors.join(', ')); if (event.shiftKey && event.target === stops[0]) { // Clear the focus from the list in order to allow // screen readers to read operations in the input box. dom.EventHelper.stop(event, true); list.clearFocus(); } else if (!event.shiftKey && dom.isAncestor(event.target, stops[stops.length - 1])) { dom.EventHelper.stop(event, true); stops[0].focus(); } } break; case 10 /* KeyCode.Space */: if (event.ctrlKey) { dom.EventHelper.stop(event, true); this.getUI().list.toggleHover(); } break; } })); this.ui = { container, styleSheet, leftActionBar, titleBar, title, description1, description2, widget, rightActionBar, checkAll, inputContainer, 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, show: controller => this.show(controller), hide: () => this.hide(), setVisibilities: visibilities => this.setVisibilities(visibilities), setEnabled: enabled => this.setEnabled(enabled), setContextKey: contextKey => this.options.setContextKey(contextKey), linkOpenerDelegate: content => this.options.linkOpenerDelegate(content) }; this.updateStyles(); return this.ui; } reparentUI(container) { if (this.ui) { this.parentElement = container; dom.append(this.parentElement, this.ui.container); } } pick(picks, options = {}, token = CancellationToken.None) { return new Promise((doResolve, reject) => { let resolve = (result) => { var _a; resolve = doResolve; (_a = options.onKeyMods) === null || _a === void 0 ? void 0 : _a.call(options, 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; } } })), input.onDidTriggerSeparatorButton(event => { var _a; return (_a = options.onDidTriggerSeparatorButton) === null || _a === void 0 ? void 0 : _a.call(options, event); }), input.onDidChangeValue(value => { if (activeItem && !value && (input.activeItems.length !== 1 || input.activeItems[0] !== activeItem)) { input.activeItems = [activeItem]; } }), token.onCancellationRequested(() => { input.hide(); }), input.onDidHide(() => { dispose(disposables); resolve(undefined); }), ]; input.title = options.title; input.canSelectMany = !!options.canPickMany; input.placeholder = options.placeHolder; input.ignoreFocusOut = !!options.ignoreFocusLost; input.matchOnDescription = !!options.matchOnDescription; input.matchOnDetail = !!options.matchOnDetail; input.matchOnLabel = (options.matchOnLabel === undefined) || options.matchOnLabel; // default to true input.quickNavigate = options.quickNavigate; input.hideInput = !!options.hideInput; input.contextKey = options.contextKey; input.busy = true; Promise.all([picks, options.activeItem]) .then(([items, _activeItem]) => { activeItem = _activeItem; input.busy = false; input.items = items; if (input.canSelectMany) { input.selectedItems = items.filter(item => item.type !== 'separator' && item.picked); } if (activeItem) { input.activeItems = [activeItem]; } }); input.show(); Promise.resolve(picks).then(undefined, err => { reject(err); input.hide(); }); }); } createQuickPick() { const ui = this.getUI(true); return new QuickPick(ui); } createInputBox() { const ui = this.getUI(true); return new InputBox(ui); } show(controller) { const ui = this.getUI(true); this.onShowEmitter.fire(); const oldController = this.controller; this.controller = controller; oldController === null || oldController === void 0 ? void 0 : oldController.didHide(); this.setEnabled(true); ui.leftActionBar.clear(); ui.title.textContent = ''; ui.description1.textContent = ''; ui.description2.textContent = ''; dom.reset(ui.widget); ui.rightActionBar.clear(); ui.checkAll.checked = false; // ui.inputBox.value = ''; Avoid triggering an event. ui.inputBox.placeholder = ''; ui.inputBox.password = false; ui.inputBox.showDecoration(Severity.Ignore); ui.visibleCount.setCount(0); ui.count.setCount(0); dom.reset(ui.message); ui.progressBar.stop(); ui.list.setElements([]); ui.list.matchOnDescription = false; ui.list.matchOnDetail = false; ui.list.matchOnLabel = true; ui.list.sortByLabel = true; ui.ignoreFocusOut = false; ui.inputBox.toggles = undefined; const backKeybindingLabel = this.options.backKeybindingLabel(); backButton.tooltip = backKeybindingLabel ? localize('quickInput.backWithKeybinding', "Back ({0})", backKeybindingLabel) : localize('quickInput.back', "Back"); ui.container.style.display = ''; this.updateLayout(); ui.inputBox.setFocus(); } isVisible() { return !!this.ui && this.ui.container.style.display !== 'none'; } setVisibilities(visibilities) { const ui = this.getUI(); ui.title.style.display = visibilities.title ? '' : 'none'; ui.description1.style.display = visibilities.description && (visibilities.inputBox || visibilities.checkAll) ? '' : 'none'; ui.description2.style.display = visibilities.description && !(visibilities.inputBox || visibilities.checkAll) ? '' : 'none'; ui.checkAll.style.display = visibilities.checkAll ? '' : 'none'; ui.inputContainer.style.display = visibilities.inputBox ? '' : 'none'; ui.filterContainer.style.display = visibilities.inputBox ? '' : 'none'; ui.visibleCountContainer.style.display = visibilities.visibleCount ? '' : 'none'; ui.countContainer.style.display = visibilities.count ? '' : 'none'; ui.okContainer.style.display = visibilities.ok ? '' : 'none'; ui.customButtonContainer.style.display = visibilities.customButton ? '' : 'none'; ui.message.style.display = visibilities.message ? '' : 'none'; ui.progressBar.getContainer().style.display = visibilities.progressBar ? '' : 'none'; ui.list.display(!!visibilities.list); ui.container.classList.toggle('show-checkboxes', !!visibilities.checkBox); ui.container.classList.toggle('hidden-input', !visibilities.inputBox && !visibilities.description); this.updateLayout(); // TODO } setEnabled(enabled) { if (enabled !== this.enabled) { this.enabled = enabled; for (const item of this.getUI().leftActionBar.viewItems) { item.action.enabled = enabled; } for (const item of this.getUI().rightActionBar.viewItems) { item.action.enabled = enabled; } this.getUI().checkAll.disabled = !enabled; this.getUI().inputBox.enabled = enabled; this.getUI().ok.enabled = enabled; this.getUI().list.enabled = enabled; } } hide(reason) { var _a, _b; const controller = this.controller; if (!controller) { return; } const container = (_a = this.ui) === null || _a === void 0 ? void 0 : _a.container; const focusChanged = container && !dom.isAncestorOfActiveElement(container); this.controller = null; this.onHideEmitter.fire(); if (container) { container.style.display = 'none'; } if (!focusChanged) { let currentElement = this.previousFocusElement; while (currentElement && !currentElement.offsetParent) { currentElement = (_b = currentElement.parentElement) !== null && _b !== void 0 ? _b : undefined; } if (currentElement === null || currentElement === void 0 ? void 0 : currentElement.offsetParent) { currentElement.focus(); this.previousFocusElement = undefined; } else { this.options.returnFocus(); } } controller.didHide(reason); } layout(dimension, titleBarOffset) { this.dimension = dimension; this.titleBarOffset = titleBarOffset; this.updateLayout(); } updateLayout() { if (this.ui && this.isVisible()) { this.ui.container.style.top = `${this.titleBarOffset}px`; const style = this.ui.container.style; const width = Math.min(this.dimension.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH); style.width = width + 'px'; style.marginLeft = '-' + (width / 2) + 'px'; this.ui.inputBox.layout(); this.ui.list.layout(this.dimension && this.dimension.height * 0.4); } } applyStyles(styles) { this.styles = styles; this.updateStyles(); } updateStyles() { if (this.ui) { const { quickInputTitleBackground, quickInputBackground, quickInputForeground, widgetBorder, widgetShadow, } = this.styles.widget; this.ui.titleBar.style.backgroundColor = quickInputTitleBackground !== null && quickInputTitleBackground !== void 0 ? quickInputTitleBackground : ''; this.ui.container.style.backgroundColor = quickInputBackground !== null && quickInputBackground !== void 0 ? quickInputBackground : ''; this.ui.container.style.color = quickInputForeground !== null && quickInputForeground !== void 0 ? quickInputForeground : ''; this.ui.container.style.border = widgetBorder ? `1px solid ${widgetBorder}` : ''; this.ui.container.style.boxShadow = widgetShadow ? `0 0 8px 2px ${widgetShadow}` : ''; this.ui.list.style(this.styles.list); const content = []; if (this.styles.pickerGroup.pickerGroupBorder) { content.push(`.quick-input-list .quick-input-list-entry { border-top-color: ${this.styles.pickerGroup.pickerGroupBorder}; }`); } if (this.styles.pickerGroup.pickerGroupForeground) { content.push(`.quick-input-list .quick-input-list-separator { color: ${this.styles.pickerGroup.pickerGroupForeground}; }`); } if (this.styles.pickerGroup.pickerGroupForeground) { content.push(`.quick-input-list .quick-input-list-separator-as-item { color: var(--vscode-descriptionForeground); }`); } if (this.styles.keybindingLabel.keybindingLabelBackground || this.styles.keybindingLabel.keybindingLabelBorder || this.styles.keybindingLabel.keybindingLabelBottomBorder || this.styles.keybindingLabel.keybindingLabelShadow || this.styles.keybindingLabel.keybindingLabelForeground) { content.push('.quick-input-list .monaco-keybinding > .monaco-keybinding-key {'); if (this.styles.keybindingLabel.keybindingLabelBackground) { content.push(`background-color: ${this.styles.keybindingLabel.keybindingLabelBackground};`); } if (this.styles.keybindingLabel.keybindingLabelBorder) { // Order matters here. `border-color` must come before `border-bottom-color`. content.push(`border-color: ${this.styles.keybindingLabel.keybindingLabelBorder};`); } if (this.styles.keybindingLabel.keybindingLabelBottomBorder) { content.push(`border-bottom-color: ${this.styles.keybindingLabel.keybindingLabelBottomBorder};`); } if (this.styles.keybindingLabel.keybindingLabelShadow) { content.push(`box-shadow: inset 0 -1px 0 ${this.styles.keybindingLabel.keybindingLabelShadow};`); } if (this.styles.keybindingLabel.keybindingLabelForeground) { content.push(`color: ${this.styles.keybindingLabel.keybindingLabelForeground};`); } content.push('}'); } const newStyles = content.join('\n'); if (newStyles !== this.ui.styleSheet.textContent) { this.ui.styleSheet.textContent = newStyles; } } } } QuickInputController.MAX_WIDTH = 600; // Max total width of quick input widget