Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/contextview/browser/contextMenuHandler.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { IContextMenuDelegate } from '../../../base/browser/contextmenu.js';
7
import { $, addDisposableListener, EventType, getActiveElement, getWindow, isAncestor, isHTMLElement } from '../../../base/browser/dom.js';
8
import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js';
9
import { Menu } from '../../../base/browser/ui/menu/menu.js';
10
import { ActionRunner, IRunEvent, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../base/common/actions.js';
11
import { isCancellationError } from '../../../base/common/errors.js';
12
import { combinedDisposable, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
13
import { IContextViewService } from './contextView.js';
14
import { IKeybindingService } from '../../keybinding/common/keybinding.js';
15
import { INotificationService } from '../../notification/common/notification.js';
16
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
17
import { defaultMenuStyles } from '../../theme/browser/defaultStyles.js';
18
19
20
export interface IContextMenuHandlerOptions {
21
blockMouse: boolean;
22
}
23
24
export class ContextMenuHandler {
25
private focusToReturn: HTMLElement | null = null;
26
private lastContainer: HTMLElement | null = null;
27
private block: HTMLElement | null = null;
28
private blockDisposable: IDisposable | null = null;
29
private options: IContextMenuHandlerOptions = { blockMouse: true };
30
31
constructor(
32
private contextViewService: IContextViewService,
33
private telemetryService: ITelemetryService,
34
private notificationService: INotificationService,
35
private keybindingService: IKeybindingService,
36
) { }
37
38
configure(options: IContextMenuHandlerOptions): void {
39
this.options = options;
40
}
41
42
showContextMenu(delegate: IContextMenuDelegate): void {
43
const actions = delegate.getActions();
44
if (!actions.length) {
45
return; // Don't render an empty context menu
46
}
47
48
this.focusToReturn = getActiveElement() as HTMLElement;
49
50
let menu: Menu | undefined;
51
52
const shadowRootElement = isHTMLElement(delegate.domForShadowRoot) ? delegate.domForShadowRoot : undefined;
53
this.contextViewService.showContextView({
54
getAnchor: () => delegate.getAnchor(),
55
canRelayout: false,
56
anchorAlignment: delegate.anchorAlignment,
57
anchorAxisAlignment: delegate.anchorAxisAlignment,
58
layer: delegate.layer,
59
render: (container) => {
60
this.lastContainer = container;
61
const className = delegate.getMenuClassName ? delegate.getMenuClassName() : '';
62
63
if (className) {
64
container.className += ' ' + className;
65
}
66
67
// Render invisible div to block mouse interaction in the rest of the UI
68
if (this.options.blockMouse) {
69
this.block = container.appendChild($('.context-view-block'));
70
this.block.style.position = 'fixed';
71
this.block.style.cursor = 'initial';
72
this.block.style.left = '0';
73
this.block.style.top = '0';
74
this.block.style.width = '100%';
75
this.block.style.height = '100%';
76
this.block.style.zIndex = '-1';
77
78
this.blockDisposable?.dispose();
79
this.blockDisposable = addDisposableListener(this.block, EventType.MOUSE_DOWN, e => e.stopPropagation());
80
}
81
82
const menuDisposables = new DisposableStore();
83
84
const actionRunner = delegate.actionRunner || menuDisposables.add(new ActionRunner());
85
actionRunner.onWillRun(evt => this.onActionRun(evt, !delegate.skipTelemetry), this, menuDisposables);
86
actionRunner.onDidRun(this.onDidActionRun, this, menuDisposables);
87
menu = new Menu(container, actions, {
88
actionViewItemProvider: delegate.getActionViewItem,
89
context: delegate.getActionsContext ? delegate.getActionsContext() : null,
90
actionRunner,
91
getKeyBinding: delegate.getKeyBinding ? delegate.getKeyBinding : action => this.keybindingService.lookupKeybinding(action.id)
92
},
93
defaultMenuStyles
94
);
95
96
menu.onDidCancel(() => this.contextViewService.hideContextView(true), null, menuDisposables);
97
menu.onDidBlur(() => this.contextViewService.hideContextView(true), null, menuDisposables);
98
const targetWindow = getWindow(container);
99
menuDisposables.add(addDisposableListener(targetWindow, EventType.BLUR, () => this.contextViewService.hideContextView(true)));
100
menuDisposables.add(addDisposableListener(targetWindow, EventType.MOUSE_DOWN, (e: MouseEvent) => {
101
if (e.defaultPrevented) {
102
return;
103
}
104
105
const event = new StandardMouseEvent(targetWindow, e);
106
let element: HTMLElement | null = event.target;
107
108
// Don't do anything as we are likely creating a context menu
109
if (event.rightButton) {
110
return;
111
}
112
113
while (element) {
114
if (element === container) {
115
return;
116
}
117
118
element = element.parentElement;
119
}
120
121
this.contextViewService.hideContextView(true);
122
}));
123
124
return combinedDisposable(menuDisposables, menu);
125
},
126
127
focus: () => {
128
menu?.focus(!!delegate.autoSelectFirstItem);
129
},
130
131
onHide: (didCancel?: boolean) => {
132
delegate.onHide?.(!!didCancel);
133
134
if (this.block) {
135
this.block.remove();
136
this.block = null;
137
}
138
139
this.blockDisposable?.dispose();
140
this.blockDisposable = null;
141
142
if (!!this.lastContainer && (getActiveElement() === this.lastContainer || isAncestor(getActiveElement(), this.lastContainer))) {
143
this.focusToReturn?.focus();
144
}
145
146
this.lastContainer = null;
147
}
148
}, shadowRootElement, !!shadowRootElement);
149
}
150
151
private onActionRun(e: IRunEvent, logTelemetry: boolean): void {
152
if (logTelemetry) {
153
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: e.action.id, from: 'contextMenu' });
154
}
155
156
this.contextViewService.hideContextView(false);
157
}
158
159
private onDidActionRun(e: IRunEvent): void {
160
if (e.error && !isCancellationError(e.error)) {
161
this.notificationService.error(e.error);
162
}
163
}
164
}
165
166