Path: blob/main/src/vs/platform/actionWidget/browser/actionWidget.ts
5221 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(() => {144// Don't hide if focus moved to a hover that belongs to this action widget145const activeElement = dom.getActiveElement();146if (activeElement?.closest('.action-widget-hover')) {147return;148}149this.hide(true);150}));151152return renderDisposables;153}154155private _createActionBar(className: string, actions: readonly IAction[]): ActionBar | undefined {156if (!actions.length) {157return undefined;158}159160const container = dom.$(className);161const actionBar = new ActionBar(container);162actionBar.push(actions, { icon: false, label: true });163return actionBar;164}165166private _onWidgetClosed(didCancel?: boolean): void {167this._list.value?.hide(didCancel);168}169}170171registerSingleton(IActionWidgetService, ActionWidgetService, InstantiationType.Delayed);172173const weight = KeybindingWeight.EditorContrib + 1000;174175registerAction2(class extends Action2 {176constructor() {177super({178id: 'hideCodeActionWidget',179title: localize2('hideCodeActionWidget.title', "Hide action widget"),180precondition: ActionWidgetContextKeys.Visible,181keybinding: {182weight,183primary: KeyCode.Escape,184secondary: [KeyMod.Shift | KeyCode.Escape]185},186});187}188189run(accessor: ServicesAccessor): void {190accessor.get(IActionWidgetService).hide(true);191}192});193194registerAction2(class extends Action2 {195constructor() {196super({197id: 'selectPrevCodeAction',198title: localize2('selectPrevCodeAction.title', "Select previous action"),199precondition: ActionWidgetContextKeys.Visible,200keybinding: {201weight,202primary: KeyCode.UpArrow,203secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow],204mac: { primary: KeyCode.UpArrow, secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow, KeyMod.WinCtrl | KeyCode.KeyP] },205}206});207}208209run(accessor: ServicesAccessor): void {210const widgetService = accessor.get(IActionWidgetService);211if (widgetService instanceof ActionWidgetService) {212widgetService.focusPrevious();213}214}215});216217registerAction2(class extends Action2 {218constructor() {219super({220id: 'selectNextCodeAction',221title: localize2('selectNextCodeAction.title', "Select next action"),222precondition: ActionWidgetContextKeys.Visible,223keybinding: {224weight,225primary: KeyCode.DownArrow,226secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow],227mac: { primary: KeyCode.DownArrow, secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow, KeyMod.WinCtrl | KeyCode.KeyN] }228}229});230}231232run(accessor: ServicesAccessor): void {233const widgetService = accessor.get(IActionWidgetService);234if (widgetService instanceof ActionWidgetService) {235widgetService.focusNext();236}237}238});239240registerAction2(class extends Action2 {241constructor() {242super({243id: acceptSelectedActionCommand,244title: localize2('acceptSelected.title', "Accept selected action"),245precondition: ActionWidgetContextKeys.Visible,246keybinding: {247weight,248primary: KeyCode.Enter,249secondary: [KeyMod.CtrlCmd | KeyCode.Period],250}251});252}253254run(accessor: ServicesAccessor): void {255const widgetService = accessor.get(IActionWidgetService);256if (widgetService instanceof ActionWidgetService) {257widgetService.acceptSelected();258}259}260});261262registerAction2(class extends Action2 {263constructor() {264super({265id: previewSelectedActionCommand,266title: localize2('previewSelected.title', "Preview selected action"),267precondition: ActionWidgetContextKeys.Visible,268keybinding: {269weight,270primary: KeyMod.CtrlCmd | KeyCode.Enter,271}272});273}274275run(accessor: ServicesAccessor): void {276const widgetService = accessor.get(IActionWidgetService);277if (widgetService instanceof ActionWidgetService) {278widgetService.acceptSelected(true);279}280}281});282283284