Path: blob/main/src/vs/platform/contextview/browser/contextMenuHandler.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*--------------------------------------------------------------------------------------------*/45import { IContextMenuDelegate } from '../../../base/browser/contextmenu.js';6import { $, addDisposableListener, EventType, getActiveElement, getWindow, isAncestor, isHTMLElement } from '../../../base/browser/dom.js';7import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js';8import { Menu } from '../../../base/browser/ui/menu/menu.js';9import { ActionRunner, IRunEvent, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../base/common/actions.js';10import { isCancellationError } from '../../../base/common/errors.js';11import { combinedDisposable, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';12import { IContextViewService } from './contextView.js';13import { IKeybindingService } from '../../keybinding/common/keybinding.js';14import { INotificationService } from '../../notification/common/notification.js';15import { ITelemetryService } from '../../telemetry/common/telemetry.js';16import { defaultMenuStyles } from '../../theme/browser/defaultStyles.js';171819export interface IContextMenuHandlerOptions {20blockMouse: boolean;21}2223export class ContextMenuHandler {24private focusToReturn: HTMLElement | null = null;25private lastContainer: HTMLElement | null = null;26private block: HTMLElement | null = null;27private blockDisposable: IDisposable | null = null;28private options: IContextMenuHandlerOptions = { blockMouse: true };2930constructor(31private contextViewService: IContextViewService,32private telemetryService: ITelemetryService,33private notificationService: INotificationService,34private keybindingService: IKeybindingService,35) { }3637configure(options: IContextMenuHandlerOptions): void {38this.options = options;39}4041showContextMenu(delegate: IContextMenuDelegate): void {42const actions = delegate.getActions();43if (!actions.length) {44return; // Don't render an empty context menu45}4647this.focusToReturn = getActiveElement() as HTMLElement;4849let menu: Menu | undefined;5051const shadowRootElement = isHTMLElement(delegate.domForShadowRoot) ? delegate.domForShadowRoot : undefined;52this.contextViewService.showContextView({53getAnchor: () => delegate.getAnchor(),54canRelayout: false,55anchorAlignment: delegate.anchorAlignment,56anchorAxisAlignment: delegate.anchorAxisAlignment,57layer: delegate.layer,58render: (container) => {59this.lastContainer = container;60const className = delegate.getMenuClassName ? delegate.getMenuClassName() : '';6162if (className) {63container.className += ' ' + className;64}6566// Render invisible div to block mouse interaction in the rest of the UI67if (this.options.blockMouse) {68this.block = container.appendChild($('.context-view-block'));69this.block.style.position = 'fixed';70this.block.style.cursor = 'initial';71this.block.style.left = '0';72this.block.style.top = '0';73this.block.style.width = '100%';74this.block.style.height = '100%';75this.block.style.zIndex = '-1';7677this.blockDisposable?.dispose();78this.blockDisposable = addDisposableListener(this.block, EventType.MOUSE_DOWN, e => e.stopPropagation());79}8081const menuDisposables = new DisposableStore();8283const actionRunner = delegate.actionRunner || menuDisposables.add(new ActionRunner());84actionRunner.onWillRun(evt => this.onActionRun(evt, !delegate.skipTelemetry), this, menuDisposables);85actionRunner.onDidRun(this.onDidActionRun, this, menuDisposables);86menu = new Menu(container, actions, {87actionViewItemProvider: delegate.getActionViewItem,88context: delegate.getActionsContext ? delegate.getActionsContext() : null,89actionRunner,90getKeyBinding: delegate.getKeyBinding ? delegate.getKeyBinding : action => this.keybindingService.lookupKeybinding(action.id)91},92defaultMenuStyles93);9495menu.onDidCancel(() => this.contextViewService.hideContextView(true), null, menuDisposables);96menu.onDidBlur(() => this.contextViewService.hideContextView(true), null, menuDisposables);97const targetWindow = getWindow(container);98menuDisposables.add(addDisposableListener(targetWindow, EventType.BLUR, () => this.contextViewService.hideContextView(true)));99menuDisposables.add(addDisposableListener(targetWindow, EventType.MOUSE_DOWN, (e: MouseEvent) => {100if (e.defaultPrevented) {101return;102}103104const event = new StandardMouseEvent(targetWindow, e);105let element: HTMLElement | null = event.target;106107// Don't do anything as we are likely creating a context menu108if (event.rightButton) {109return;110}111112while (element) {113if (element === container) {114return;115}116117element = element.parentElement;118}119120this.contextViewService.hideContextView(true);121}));122123return combinedDisposable(menuDisposables, menu);124},125126focus: () => {127menu?.focus(!!delegate.autoSelectFirstItem);128},129130onHide: (didCancel?: boolean) => {131delegate.onHide?.(!!didCancel);132133if (this.block) {134this.block.remove();135this.block = null;136}137138this.blockDisposable?.dispose();139this.blockDisposable = null;140141if (!!this.lastContainer && (getActiveElement() === this.lastContainer || isAncestor(getActiveElement(), this.lastContainer))) {142this.focusToReturn?.focus();143}144145this.lastContainer = null;146}147}, shadowRootElement, !!shadowRootElement);148}149150private onActionRun(e: IRunEvent, logTelemetry: boolean): void {151if (logTelemetry) {152this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: e.action.id, from: 'contextMenu' });153}154155this.contextViewService.hideContextView(false);156}157158private onDidActionRun(e: IRunEvent): void {159if (e.error && !isCancellationError(e.error)) {160this.notificationService.error(e.error);161}162}163}164165166