Path: blob/main/src/vs/platform/actionWidget/browser/actionWidget.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import * as dom from '../../../base/browser/dom.js';5import { ActionBar } from '../../../base/browser/ui/actionbar/actionbar.js';6import { IAnchor } from '../../../base/browser/ui/contextview/contextview.js';7import { IAction } from '../../../base/common/actions.js';8import { KeyCode, KeyMod } from '../../../base/common/keyCodes.js';9import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js';10import './actionWidget.css';11import { localize, localize2 } from '../../../nls.js';12import { acceptSelectedActionCommand, ActionList, IActionListDelegate, IActionListItem, previewSelectedActionCommand } from './actionList.js';13import { Action2, registerAction2 } from '../../actions/common/actions.js';14import { IContextKeyService, RawContextKey } from '../../contextkey/common/contextkey.js';15import { IContextViewService } from '../../contextview/browser/contextView.js';16import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';17import { createDecorator, IInstantiationService, ServicesAccessor } from '../../instantiation/common/instantiation.js';18import { KeybindingWeight } from '../../keybinding/common/keybindingsRegistry.js';19import { inputActiveOptionBackground, registerColor } from '../../theme/common/colorRegistry.js';20import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js';21import { IListAccessibilityProvider } from '../../../base/browser/ui/list/listWidget.js';2223registerColor(24'actionBar.toggledBackground',25inputActiveOptionBackground,26localize('actionBar.toggledBackground', 'Background color for toggled action items in action bar.')27);2829const ActionWidgetContextKeys = {30Visible: new RawContextKey<boolean>('codeActionMenuVisible', false, localize('codeActionMenuVisible', "Whether the action widget list is visible"))31};3233export const IActionWidgetService = createDecorator<IActionWidgetService>('actionWidgetService');3435export interface IActionWidgetService {36readonly _serviceBrand: undefined;3738show<T>(user: string, supportsPreview: boolean, items: readonly IActionListItem<T>[], delegate: IActionListDelegate<T>, anchor: HTMLElement | StandardMouseEvent | IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[], accessibilityProvider?: Partial<IListAccessibilityProvider<IActionListItem<T>>>): void;3940hide(didCancel?: boolean): void;4142readonly isVisible: boolean;43}4445class ActionWidgetService extends Disposable implements IActionWidgetService {46declare readonly _serviceBrand: undefined;4748get isVisible() {49return ActionWidgetContextKeys.Visible.getValue(this._contextKeyService) || false;50}5152private readonly _list = this._register(new MutableDisposable<ActionList<unknown>>());5354constructor(55@IContextViewService private readonly _contextViewService: IContextViewService,56@IContextKeyService private readonly _contextKeyService: IContextKeyService,57@IInstantiationService private readonly _instantiationService: IInstantiationService58) {59super();60}6162show<T>(user: string, supportsPreview: boolean, items: readonly IActionListItem<T>[], delegate: IActionListDelegate<T>, anchor: HTMLElement | StandardMouseEvent | IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[], accessibilityProvider?: Partial<IListAccessibilityProvider<IActionListItem<T>>>): void {63const visibleContext = ActionWidgetContextKeys.Visible.bindTo(this._contextKeyService);6465const list = this._instantiationService.createInstance(ActionList, user, supportsPreview, items, delegate, accessibilityProvider);66this._contextViewService.showContextView({67getAnchor: () => anchor,68render: (container: HTMLElement) => {69visibleContext.set(true);70return this._renderWidget(container, list, actionBarActions ?? []);71},72onHide: (didCancel) => {73visibleContext.reset();74this._onWidgetClosed(didCancel);75},76}, container, false);77}7879acceptSelected(preview?: boolean) {80this._list.value?.acceptSelected(preview);81}8283focusPrevious() {84this._list?.value?.focusPrevious();85}8687focusNext() {88this._list?.value?.focusNext();89}9091hide(didCancel?: boolean) {92this._list.value?.hide(didCancel);93this._list.clear();94}9596clear() {97this._list.clear();98}99100private _renderWidget(element: HTMLElement, list: ActionList<unknown>, actionBarActions: readonly IAction[]): IDisposable {101const widget = document.createElement('div');102widget.classList.add('action-widget');103element.appendChild(widget);104105this._list.value = list;106if (this._list.value) {107widget.appendChild(this._list.value.domNode);108} else {109throw new Error('List has no value');110}111const renderDisposables = new DisposableStore();112113// Invisible div to block mouse interaction in the rest of the UI114const menuBlock = document.createElement('div');115const block = element.appendChild(menuBlock);116block.classList.add('context-view-block');117renderDisposables.add(dom.addDisposableListener(block, dom.EventType.MOUSE_DOWN, e => e.stopPropagation()));118119// Invisible div to block mouse interaction with the menu120const pointerBlockDiv = document.createElement('div');121const pointerBlock = element.appendChild(pointerBlockDiv);122pointerBlock.classList.add('context-view-pointerBlock');123124// Removes block on click INSIDE widget or ANY mouse movement125renderDisposables.add(dom.addDisposableListener(pointerBlock, dom.EventType.POINTER_MOVE, () => pointerBlock.remove()));126renderDisposables.add(dom.addDisposableListener(pointerBlock, dom.EventType.MOUSE_DOWN, () => pointerBlock.remove()));127128// Action bar129let actionBarWidth = 0;130if (actionBarActions.length) {131const actionBar = this._createActionBar('.action-widget-action-bar', actionBarActions);132if (actionBar) {133widget.appendChild(actionBar.getContainer().parentElement!);134renderDisposables.add(actionBar);135actionBarWidth = actionBar.getContainer().offsetWidth;136}137}138139const width = this._list.value?.layout(actionBarWidth);140widget.style.width = `${width}px`;141142const focusTracker = renderDisposables.add(dom.trackFocus(element));143renderDisposables.add(focusTracker.onDidBlur(() => this.hide(true)));144145return renderDisposables;146}147148private _createActionBar(className: string, actions: readonly IAction[]): ActionBar | undefined {149if (!actions.length) {150return undefined;151}152153const container = dom.$(className);154const actionBar = new ActionBar(container);155actionBar.push(actions, { icon: false, label: true });156return actionBar;157}158159private _onWidgetClosed(didCancel?: boolean): void {160this._list.value?.hide(didCancel);161}162}163164registerSingleton(IActionWidgetService, ActionWidgetService, InstantiationType.Delayed);165166const weight = KeybindingWeight.EditorContrib + 1000;167168registerAction2(class extends Action2 {169constructor() {170super({171id: 'hideCodeActionWidget',172title: localize2('hideCodeActionWidget.title', "Hide action widget"),173precondition: ActionWidgetContextKeys.Visible,174keybinding: {175weight,176primary: KeyCode.Escape,177secondary: [KeyMod.Shift | KeyCode.Escape]178},179});180}181182run(accessor: ServicesAccessor): void {183accessor.get(IActionWidgetService).hide(true);184}185});186187registerAction2(class extends Action2 {188constructor() {189super({190id: 'selectPrevCodeAction',191title: localize2('selectPrevCodeAction.title', "Select previous action"),192precondition: ActionWidgetContextKeys.Visible,193keybinding: {194weight,195primary: KeyCode.UpArrow,196secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow],197mac: { primary: KeyCode.UpArrow, secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow, KeyMod.WinCtrl | KeyCode.KeyP] },198}199});200}201202run(accessor: ServicesAccessor): void {203const widgetService = accessor.get(IActionWidgetService);204if (widgetService instanceof ActionWidgetService) {205widgetService.focusPrevious();206}207}208});209210registerAction2(class extends Action2 {211constructor() {212super({213id: 'selectNextCodeAction',214title: localize2('selectNextCodeAction.title', "Select next action"),215precondition: ActionWidgetContextKeys.Visible,216keybinding: {217weight,218primary: KeyCode.DownArrow,219secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow],220mac: { primary: KeyCode.DownArrow, secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow, KeyMod.WinCtrl | KeyCode.KeyN] }221}222});223}224225run(accessor: ServicesAccessor): void {226const widgetService = accessor.get(IActionWidgetService);227if (widgetService instanceof ActionWidgetService) {228widgetService.focusNext();229}230}231});232233registerAction2(class extends Action2 {234constructor() {235super({236id: acceptSelectedActionCommand,237title: localize2('acceptSelected.title', "Accept selected action"),238precondition: ActionWidgetContextKeys.Visible,239keybinding: {240weight,241primary: KeyCode.Enter,242secondary: [KeyMod.CtrlCmd | KeyCode.Period],243}244});245}246247run(accessor: ServicesAccessor): void {248const widgetService = accessor.get(IActionWidgetService);249if (widgetService instanceof ActionWidgetService) {250widgetService.acceptSelected();251}252}253});254255registerAction2(class extends Action2 {256constructor() {257super({258id: previewSelectedActionCommand,259title: localize2('previewSelected.title', "Preview selected action"),260precondition: ActionWidgetContextKeys.Visible,261keybinding: {262weight,263primary: KeyMod.CtrlCmd | KeyCode.Enter,264}265});266}267268run(accessor: ServicesAccessor): void {269const widgetService = accessor.get(IActionWidgetService);270if (widgetService instanceof ActionWidgetService) {271widgetService.acceptSelected(true);272}273}274});275276277