Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/menubar/electron-main/menubar.ts
5328 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 { app, BrowserWindow, BaseWindow, KeyboardEvent, Menu, MenuItem, MenuItemConstructorOptions, WebContents } from 'electron';
7
import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../base/common/actions.js';
8
import { RunOnceScheduler } from '../../../base/common/async.js';
9
import { CancellationToken } from '../../../base/common/cancellation.js';
10
import { mnemonicMenuLabel } from '../../../base/common/labels.js';
11
import { isMacintosh, language } from '../../../base/common/platform.js';
12
import { URI } from '../../../base/common/uri.js';
13
import * as nls from '../../../nls.js';
14
import { IAuxiliaryWindowsMainService } from '../../auxiliaryWindow/electron-main/auxiliaryWindows.js';
15
import { IConfigurationService } from '../../configuration/common/configuration.js';
16
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
17
import { ILifecycleMainService } from '../../lifecycle/electron-main/lifecycleMainService.js';
18
import { ILogService } from '../../log/common/log.js';
19
import { IMenubarData, IMenubarKeybinding, IMenubarMenu, IMenubarMenuRecentItemAction, isMenubarMenuItemAction, isMenubarMenuItemRecentAction, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, MenubarMenuItem } from '../common/menubar.js';
20
import { INativeHostMainService } from '../../native/electron-main/nativeHostMainService.js';
21
import { IProductService } from '../../product/common/productService.js';
22
import { IStateService } from '../../state/node/state.js';
23
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
24
import { IUpdateService, StateType } from '../../update/common/update.js';
25
import { INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, IWindowOpenable, hasNativeMenu } from '../../window/common/window.js';
26
import { IWindowsCountChangedEvent, IWindowsMainService, OpenContext } from '../../windows/electron-main/windows.js';
27
import { IWorkspacesHistoryMainService } from '../../workspaces/electron-main/workspacesHistoryMainService.js';
28
import { Disposable } from '../../../base/common/lifecycle.js';
29
30
const telemetryFrom = 'menu';
31
32
interface IMenuItemClickHandler {
33
inDevTools: (contents: WebContents) => void;
34
inNoWindow: () => void;
35
}
36
37
type IMenuItemInvocation = (
38
{ type: 'commandId'; commandId: string }
39
| { type: 'keybinding'; userSettingsLabel: string }
40
);
41
42
interface IMenuItemWithKeybinding {
43
userSettingsLabel?: string;
44
}
45
46
export class Menubar extends Disposable {
47
48
private static readonly lastKnownMenubarStorageKey = 'lastKnownMenubarData';
49
50
private willShutdown: boolean | undefined;
51
private appMenuInstalled: boolean | undefined;
52
private closedLastWindow: boolean;
53
private noActiveMainWindow: boolean;
54
private showNativeMenu: boolean;
55
56
private menuUpdater: RunOnceScheduler;
57
private menuGC: RunOnceScheduler;
58
59
// Array to keep menus around so that GC doesn't cause crash as explained in #55347
60
// TODO@sbatten Remove this when fixed upstream by Electron
61
private oldMenus: Menu[];
62
63
private menubarMenus: { [id: string]: IMenubarMenu };
64
65
private keybindings: { [commandId: string]: IMenubarKeybinding };
66
67
private readonly fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BaseWindow | undefined, event: KeyboardEvent) => void } = Object.create(null);
68
69
constructor(
70
@IUpdateService private readonly updateService: IUpdateService,
71
@IConfigurationService private readonly configurationService: IConfigurationService,
72
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
73
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
74
@ITelemetryService private readonly telemetryService: ITelemetryService,
75
@IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService,
76
@IStateService private readonly stateService: IStateService,
77
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
78
@ILogService private readonly logService: ILogService,
79
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
80
@IProductService private readonly productService: IProductService,
81
@IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService
82
) {
83
super();
84
85
this.menuUpdater = this._register(new RunOnceScheduler(() => this.doUpdateMenu(), 0));
86
87
this.menuGC = this._register(new RunOnceScheduler(() => { this.oldMenus = []; }, 10000));
88
89
this.menubarMenus = Object.create(null);
90
this.keybindings = Object.create(null);
91
this.showNativeMenu = hasNativeMenu(configurationService);
92
93
if (isMacintosh || this.showNativeMenu) {
94
this.restoreCachedMenubarData();
95
}
96
97
this.addFallbackHandlers();
98
99
this.closedLastWindow = false;
100
this.noActiveMainWindow = false;
101
102
this.oldMenus = [];
103
104
this.install();
105
106
this.registerListeners();
107
}
108
109
private restoreCachedMenubarData() {
110
const menubarData = this.stateService.getItem<IMenubarData>(Menubar.lastKnownMenubarStorageKey);
111
if (menubarData) {
112
if (menubarData.menus) {
113
this.menubarMenus = menubarData.menus;
114
}
115
116
if (menubarData.keybindings) {
117
this.keybindings = menubarData.keybindings;
118
}
119
}
120
}
121
122
private addFallbackHandlers(): void {
123
124
// File Menu Items
125
this.fallbackMenuHandlers['workbench.action.files.newUntitledFile'] = (menuItem, win, event) => {
126
if (!this.runActionInRenderer({ type: 'commandId', commandId: 'workbench.action.files.newUntitledFile' })) { // this is one of the few supported actions when aux window has focus
127
this.windowsMainService.openEmptyWindow({ context: OpenContext.MENU, contextWindowId: win?.id });
128
}
129
};
130
this.fallbackMenuHandlers['workbench.action.newWindow'] = (menuItem, win, event) => this.windowsMainService.openEmptyWindow({ context: OpenContext.MENU, contextWindowId: win?.id });
131
this.fallbackMenuHandlers['workbench.action.files.openFileFolder'] = (menuItem, win, event) => this.nativeHostMainService.pickFileFolderAndOpen(undefined, { forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } });
132
this.fallbackMenuHandlers['workbench.action.files.openFolder'] = (menuItem, win, event) => this.nativeHostMainService.pickFolderAndOpen(undefined, { forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } });
133
this.fallbackMenuHandlers['workbench.action.openWorkspace'] = (menuItem, win, event) => this.nativeHostMainService.pickWorkspaceAndOpen(undefined, { forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } });
134
135
// Recent Menu Items
136
this.fallbackMenuHandlers['workbench.action.clearRecentFiles'] = () => this.workspacesHistoryMainService.clearRecentlyOpened({ confirm: true /* ask for confirmation */ });
137
138
// Help Menu Items
139
const youTubeUrl = this.productService.youTubeUrl;
140
if (youTubeUrl) {
141
this.fallbackMenuHandlers['workbench.action.openYouTubeUrl'] = () => this.openUrl(youTubeUrl, 'openYouTubeUrl');
142
}
143
144
const requestFeatureUrl = this.productService.requestFeatureUrl;
145
if (requestFeatureUrl) {
146
this.fallbackMenuHandlers['workbench.action.openRequestFeatureUrl'] = () => this.openUrl(requestFeatureUrl, 'openUserVoiceUrl');
147
}
148
149
const reportIssueUrl = this.productService.reportIssueUrl;
150
if (reportIssueUrl) {
151
this.fallbackMenuHandlers['workbench.action.openIssueReporter'] = () => this.openUrl(reportIssueUrl, 'openReportIssues');
152
}
153
154
const licenseUrl = this.productService.licenseUrl;
155
if (licenseUrl) {
156
this.fallbackMenuHandlers['workbench.action.openLicenseUrl'] = () => {
157
if (language) {
158
const queryArgChar = licenseUrl.indexOf('?') > 0 ? '&' : '?';
159
this.openUrl(`${licenseUrl}${queryArgChar}lang=${language}`, 'openLicenseUrl');
160
} else {
161
this.openUrl(licenseUrl, 'openLicenseUrl');
162
}
163
};
164
}
165
166
const privacyStatementUrl = this.productService.privacyStatementUrl;
167
if (privacyStatementUrl && licenseUrl) {
168
this.fallbackMenuHandlers['workbench.action.openPrivacyStatementUrl'] = () => {
169
this.openUrl(privacyStatementUrl, 'openPrivacyStatement');
170
};
171
}
172
}
173
174
private registerListeners(): void {
175
176
// Keep flag when app quits
177
this._register(this.lifecycleMainService.onWillShutdown(() => this.willShutdown = true));
178
179
// Listen to some events from window service to update menu
180
this._register(this.windowsMainService.onDidChangeWindowsCount(e => this.onDidChangeWindowsCount(e)));
181
this._register(this.nativeHostMainService.onDidBlurMainWindow(() => this.onDidChangeWindowFocus()));
182
this._register(this.nativeHostMainService.onDidFocusMainWindow(() => this.onDidChangeWindowFocus()));
183
}
184
185
private get currentEnableMenuBarMnemonics(): boolean {
186
const enableMenuBarMnemonics = this.configurationService.getValue('window.enableMenuBarMnemonics');
187
if (typeof enableMenuBarMnemonics !== 'boolean') {
188
return true;
189
}
190
191
return enableMenuBarMnemonics;
192
}
193
194
private get currentEnableNativeTabs(): boolean {
195
if (!isMacintosh) {
196
return false;
197
}
198
199
const enableNativeTabs = this.configurationService.getValue('window.nativeTabs');
200
if (typeof enableNativeTabs !== 'boolean') {
201
return false;
202
}
203
return enableNativeTabs;
204
}
205
206
updateMenu(menubarData: IMenubarData, windowId: number) {
207
this.menubarMenus = menubarData.menus;
208
this.keybindings = menubarData.keybindings;
209
210
// Save off new menu and keybindings
211
this.stateService.setItem(Menubar.lastKnownMenubarStorageKey, menubarData);
212
213
this.scheduleUpdateMenu();
214
}
215
216
217
private scheduleUpdateMenu(): void {
218
this.menuUpdater.schedule(); // buffer multiple attempts to update the menu
219
}
220
221
private doUpdateMenu(): void {
222
223
// Due to limitations in Electron, it is not possible to update menu items dynamically. The suggested
224
// workaround from Electron is to set the application menu again.
225
// See also https://github.com/electron/electron/issues/846
226
//
227
// Run delayed to prevent updating menu while it is open
228
if (!this.willShutdown) {
229
setTimeout(() => {
230
if (!this.willShutdown) {
231
this.install();
232
}
233
}, 10 /* delay this because there is an issue with updating a menu when it is open */);
234
}
235
}
236
237
private onDidChangeWindowsCount(e: IWindowsCountChangedEvent): void {
238
if (!isMacintosh) {
239
return;
240
}
241
242
// Update menu if window count goes from N > 0 or 0 > N to update menu item enablement
243
if ((e.oldCount === 0 && e.newCount > 0) || (e.oldCount > 0 && e.newCount === 0)) {
244
this.closedLastWindow = e.newCount === 0;
245
this.scheduleUpdateMenu();
246
}
247
}
248
249
private onDidChangeWindowFocus(): void {
250
if (!isMacintosh) {
251
return;
252
}
253
254
const focusedWindow = BrowserWindow.getFocusedWindow();
255
this.noActiveMainWindow = !focusedWindow || !!this.auxiliaryWindowsMainService.getWindowByWebContents(focusedWindow.webContents);
256
this.scheduleUpdateMenu();
257
}
258
259
private install(): void {
260
// Store old menu in our array to avoid GC to collect the menu and crash. See #55347
261
// TODO@sbatten Remove this when fixed upstream by Electron
262
const oldMenu = Menu.getApplicationMenu();
263
if (oldMenu) {
264
this.oldMenus.push(oldMenu);
265
}
266
267
// If we don't have a menu yet, set it to null to avoid the electron menu.
268
// This should only happen on the first launch ever
269
if (Object.keys(this.menubarMenus).length === 0) {
270
this.doSetApplicationMenu(isMacintosh ? new Menu() : null);
271
return;
272
}
273
274
// Menus
275
const menubar = new Menu();
276
277
// Mac: Application
278
let macApplicationMenuItem: MenuItem;
279
if (isMacintosh) {
280
const applicationMenu = new Menu();
281
macApplicationMenuItem = new MenuItem({ label: this.productService.nameShort, submenu: applicationMenu });
282
this.setMacApplicationMenu(applicationMenu);
283
menubar.append(macApplicationMenuItem);
284
}
285
286
// Mac: Dock
287
if (isMacintosh && !this.appMenuInstalled) {
288
this.appMenuInstalled = true;
289
290
const dockMenu = new Menu();
291
dockMenu.append(new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window")), click: () => this.windowsMainService.openEmptyWindow({ context: OpenContext.DOCK }) }));
292
293
app.dock!.setMenu(dockMenu);
294
}
295
296
// File
297
if (this.shouldDrawMenu('File')) {
298
const fileMenu = new Menu();
299
const fileMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File")), submenu: fileMenu });
300
this.setMenuById(fileMenu, 'File');
301
menubar.append(fileMenuItem);
302
}
303
304
// Edit
305
if (this.shouldDrawMenu('Edit')) {
306
const editMenu = new Menu();
307
const editMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit")), submenu: editMenu });
308
this.setMenuById(editMenu, 'Edit');
309
menubar.append(editMenuItem);
310
}
311
312
// Selection
313
if (this.shouldDrawMenu('Selection')) {
314
const selectionMenu = new Menu();
315
const selectionMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection")), submenu: selectionMenu });
316
this.setMenuById(selectionMenu, 'Selection');
317
menubar.append(selectionMenuItem);
318
}
319
320
// View
321
if (this.shouldDrawMenu('View')) {
322
const viewMenu = new Menu();
323
const viewMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View")), submenu: viewMenu });
324
this.setMenuById(viewMenu, 'View');
325
menubar.append(viewMenuItem);
326
}
327
328
// Go
329
if (this.shouldDrawMenu('Go')) {
330
const gotoMenu = new Menu();
331
const gotoMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go")), submenu: gotoMenu });
332
this.setMenuById(gotoMenu, 'Go');
333
menubar.append(gotoMenuItem);
334
}
335
336
// Debug
337
if (this.shouldDrawMenu('Run')) {
338
const debugMenu = new Menu();
339
const debugMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mRun', comment: ['&& denotes a mnemonic'] }, "&&Run")), submenu: debugMenu });
340
this.setMenuById(debugMenu, 'Run');
341
menubar.append(debugMenuItem);
342
}
343
344
// Terminal
345
if (this.shouldDrawMenu('Terminal')) {
346
const terminalMenu = new Menu();
347
const terminalMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal")), submenu: terminalMenu });
348
this.setMenuById(terminalMenu, 'Terminal');
349
menubar.append(terminalMenuItem);
350
}
351
352
// Mac: Window
353
let macWindowMenuItem: MenuItem | undefined;
354
if (this.shouldDrawMenu('Window')) {
355
const windowMenu = new Menu();
356
macWindowMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize('mWindow', "Window")), submenu: windowMenu, role: 'window' });
357
this.setMacWindowMenu(windowMenu);
358
}
359
360
if (macWindowMenuItem) {
361
menubar.append(macWindowMenuItem);
362
}
363
364
// Help
365
if (this.shouldDrawMenu('Help')) {
366
const helpMenu = new Menu();
367
const helpMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help")), submenu: helpMenu, role: 'help' });
368
this.setMenuById(helpMenu, 'Help');
369
menubar.append(helpMenuItem);
370
}
371
372
if (menubar.items && menubar.items.length > 0) {
373
this.doSetApplicationMenu(menubar);
374
} else {
375
this.doSetApplicationMenu(null);
376
}
377
378
// Dispose of older menus after some time
379
this.menuGC.schedule();
380
}
381
382
private doSetApplicationMenu(menu: (Menu) | (null)): void {
383
384
// Setting the application menu sets it to all opened windows,
385
// but we currently do not support a menu in auxiliary windows,
386
// so we need to unset it there.
387
//
388
// This is a bit ugly but `setApplicationMenu()` has some nice
389
// behaviour we want:
390
// - on macOS it is required because menus are application set
391
// - we use `getApplicationMenu()` to access the current state
392
// - new windows immediately get the same menu when opening
393
// reducing overall flicker for these
394
395
Menu.setApplicationMenu(menu);
396
397
if (menu) {
398
for (const window of this.auxiliaryWindowsMainService.getWindows()) {
399
window.win?.setMenu(null);
400
}
401
}
402
}
403
404
private setMacApplicationMenu(macApplicationMenu: Menu): void {
405
const about = this.createMenuItem(nls.localize('mAbout', "About {0}", this.productService.nameLong), 'workbench.action.showAboutDialog');
406
const checkForUpdates = this.getUpdateMenuItems();
407
408
let preferences;
409
if (this.shouldDrawMenu('Preferences')) {
410
const preferencesMenu = new Menu();
411
this.setMenuById(preferencesMenu, 'Preferences');
412
preferences = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miPreferences', comment: ['&& denotes a mnemonic'] }, "&&Preferences")), submenu: preferencesMenu });
413
}
414
415
const servicesMenu = new Menu();
416
const services = new MenuItem({ label: nls.localize('mServices', "Services"), role: 'services', submenu: servicesMenu });
417
const hide = new MenuItem({ label: nls.localize('mHide', "Hide {0}", this.productService.nameLong), role: 'hide', accelerator: 'Command+H' });
418
const hideOthers = new MenuItem({ label: nls.localize('mHideOthers', "Hide Others"), role: 'hideOthers', accelerator: 'Command+Alt+H' });
419
const showAll = new MenuItem({ label: nls.localize('mShowAll', "Show All"), role: 'unhide' });
420
const quit = new MenuItem(this.likeAction('workbench.action.quit', {
421
label: nls.localize('miQuit', "Quit {0}", this.productService.nameLong), click: async (item, window, event) => {
422
const lastActiveWindow = this.windowsMainService.getLastActiveWindow();
423
if (
424
this.windowsMainService.getWindowCount() === 0 || // allow to quit when no more windows are open
425
!!BrowserWindow.getFocusedWindow() || // allow to quit when window has focus (fix for https://github.com/microsoft/vscode/issues/39191)
426
lastActiveWindow?.win?.isMinimized() // allow to quit when window has no focus but is minimized (https://github.com/microsoft/vscode/issues/63000)
427
) {
428
const confirmed = await this.confirmBeforeQuit(event);
429
if (confirmed) {
430
this.nativeHostMainService.quit(undefined);
431
}
432
}
433
}
434
}));
435
436
const actions = [about];
437
actions.push(...checkForUpdates);
438
439
if (preferences) {
440
actions.push(...[
441
__separator__(),
442
preferences
443
]);
444
}
445
446
actions.push(...[
447
__separator__(),
448
services,
449
__separator__(),
450
hide,
451
hideOthers,
452
showAll,
453
__separator__(),
454
quit
455
]);
456
457
actions.forEach(i => macApplicationMenu.append(i));
458
}
459
460
private async confirmBeforeQuit(event: KeyboardEvent): Promise<boolean> {
461
if (this.windowsMainService.getWindowCount() === 0) {
462
return true; // never confirm when no windows are opened
463
}
464
465
const confirmBeforeClose = this.configurationService.getValue<'always' | 'never' | 'keyboardOnly'>('window.confirmBeforeClose');
466
if (confirmBeforeClose === 'always' || (confirmBeforeClose === 'keyboardOnly' && this.isKeyboardEvent(event))) {
467
const { response } = await this.nativeHostMainService.showMessageBox(this.windowsMainService.getFocusedWindow()?.id, {
468
type: 'question',
469
buttons: [
470
isMacintosh ? nls.localize({ key: 'quit', comment: ['&& denotes a mnemonic'] }, "&&Quit") : nls.localize({ key: 'exit', comment: ['&& denotes a mnemonic'] }, "&&Exit"),
471
nls.localize('cancel', "Cancel")
472
],
473
message: isMacintosh ? nls.localize('quitMessageMac', "Are you sure you want to quit?") : nls.localize('quitMessage', "Are you sure you want to exit?")
474
});
475
476
return response === 0;
477
}
478
479
return true;
480
}
481
482
private shouldDrawMenu(menuId: string): boolean {
483
if (!isMacintosh && !this.showNativeMenu) {
484
return false; // We need to draw an empty menu to override the electron default
485
}
486
487
switch (menuId) {
488
case 'File':
489
case 'Help':
490
if (isMacintosh) {
491
return (this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) || (this.windowsMainService.getWindowCount() > 0 && this.noActiveMainWindow) || (!!this.menubarMenus && !!this.menubarMenus[menuId]);
492
}
493
494
case 'Window':
495
if (isMacintosh) {
496
return (this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) || (this.windowsMainService.getWindowCount() > 0 && this.noActiveMainWindow) || !!this.menubarMenus;
497
}
498
499
default:
500
return this.windowsMainService.getWindowCount() > 0 && (!!this.menubarMenus && !!this.menubarMenus[menuId]);
501
}
502
}
503
504
505
private setMenu(menu: Menu, items: Array<MenubarMenuItem>) {
506
items.forEach((item: MenubarMenuItem) => {
507
if (isMenubarMenuItemSeparator(item)) {
508
menu.append(__separator__());
509
} else if (isMenubarMenuItemSubmenu(item)) {
510
const submenu = new Menu();
511
const submenuItem = new MenuItem({ label: this.mnemonicLabel(item.label), submenu });
512
this.setMenu(submenu, item.submenu.items);
513
menu.append(submenuItem);
514
} else if (isMenubarMenuItemRecentAction(item)) {
515
menu.append(this.createOpenRecentMenuItem(item));
516
} else if (isMenubarMenuItemAction(item)) {
517
if (item.id === 'workbench.action.showAboutDialog') {
518
this.insertCheckForUpdatesItems(menu);
519
}
520
521
if (isMacintosh) {
522
if ((this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) ||
523
(this.windowsMainService.getWindowCount() > 0 && this.noActiveMainWindow)) {
524
// In the fallback scenario, we are either disabled or using a fallback handler
525
if (this.fallbackMenuHandlers[item.id]) {
526
menu.append(new MenuItem(this.likeAction(item.id, { label: this.mnemonicLabel(item.label), click: this.fallbackMenuHandlers[item.id] })));
527
} else {
528
menu.append(this.createMenuItem(item.label, item.id, false, item.checked));
529
}
530
} else {
531
menu.append(this.createMenuItem(item.label, item.id, item.enabled !== false, !!item.checked));
532
}
533
} else {
534
menu.append(this.createMenuItem(item.label, item.id, item.enabled !== false, !!item.checked));
535
}
536
}
537
});
538
}
539
540
private setMenuById(menu: Menu, menuId: string): void {
541
if (this.menubarMenus?.[menuId]) {
542
this.setMenu(menu, this.menubarMenus[menuId].items);
543
}
544
}
545
546
private insertCheckForUpdatesItems(menu: Menu) {
547
const updateItems = this.getUpdateMenuItems();
548
if (updateItems.length) {
549
updateItems.forEach(i => menu.append(i));
550
menu.append(__separator__());
551
}
552
}
553
554
private createOpenRecentMenuItem(item: IMenubarMenuRecentItemAction): MenuItem {
555
const revivedUri = URI.revive(item.uri);
556
const commandId = item.id;
557
const openable: IWindowOpenable =
558
(commandId === 'openRecentFile') ? { fileUri: revivedUri } :
559
(commandId === 'openRecentWorkspace') ? { workspaceUri: revivedUri } : { folderUri: revivedUri };
560
561
return new MenuItem(this.likeAction(commandId, {
562
label: item.label,
563
click: async (menuItem, win, event) => {
564
const openInNewWindow = this.isOptionClick(event);
565
const success = (await this.windowsMainService.open({
566
context: OpenContext.MENU,
567
cli: this.environmentMainService.args,
568
urisToOpen: [openable],
569
forceNewWindow: openInNewWindow,
570
gotoLineMode: false,
571
remoteAuthority: item.remoteAuthority
572
})).length > 0;
573
574
if (!success) {
575
await this.workspacesHistoryMainService.removeRecentlyOpened([revivedUri]);
576
}
577
}
578
}, false));
579
}
580
581
private isOptionClick(event: KeyboardEvent): boolean {
582
return !!(event && ((!isMacintosh && (event.ctrlKey || event.shiftKey)) || (isMacintosh && (event.metaKey || event.altKey))));
583
}
584
585
private isKeyboardEvent(event: KeyboardEvent): boolean {
586
return !!(event.triggeredByAccelerator || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
587
}
588
589
private createRoleMenuItem(label: string, commandId: string, role: 'undo' | 'redo' | 'cut' | 'copy' | 'paste' | 'pasteAndMatchStyle' | 'delete' | 'selectAll' | 'reload' | 'forceReload' | 'toggleDevTools' | 'resetZoom' | 'zoomIn' | 'zoomOut' | 'toggleSpellChecker' | 'togglefullscreen' | 'window' | 'minimize' | 'close' | 'help' | 'about' | 'services' | 'hide' | 'hideOthers' | 'unhide' | 'quit' | 'showSubstitutions' | 'toggleSmartQuotes' | 'toggleSmartDashes' | 'toggleTextReplacement' | 'startSpeaking' | 'stopSpeaking' | 'zoom' | 'front' | 'appMenu' | 'fileMenu' | 'editMenu' | 'viewMenu' | 'shareMenu' | 'recentDocuments' | 'toggleTabBar' | 'selectNextTab' | 'selectPreviousTab' | 'showAllTabs' | 'mergeAllWindows' | 'clearRecentDocuments' | 'moveTabToNewWindow' | 'windowMenu'): MenuItem {
590
const options: MenuItemConstructorOptions = {
591
label: this.mnemonicLabel(label),
592
role,
593
enabled: true
594
};
595
596
return new MenuItem(this.withKeybinding(commandId, options));
597
}
598
599
private setMacWindowMenu(macWindowMenu: Menu): void {
600
const minimize = new MenuItem({ label: nls.localize('mMinimize', "Minimize"), role: 'minimize', accelerator: 'Command+M', enabled: this.windowsMainService.getWindowCount() > 0 });
601
const zoom = new MenuItem({ label: nls.localize('mZoom', "Zoom"), role: 'zoom', enabled: this.windowsMainService.getWindowCount() > 0 });
602
const bringAllToFront = new MenuItem({ label: nls.localize('mBringToFront', "Bring All to Front"), role: 'front', enabled: this.windowsMainService.getWindowCount() > 0 });
603
const switchWindow = this.createMenuItem(nls.localize({ key: 'miSwitchWindow', comment: ['&& denotes a mnemonic'] }, "Switch &&Window..."), 'workbench.action.switchWindow');
604
605
const nativeTabMenuItems: MenuItem[] = [];
606
if (this.currentEnableNativeTabs) {
607
nativeTabMenuItems.push(__separator__());
608
609
nativeTabMenuItems.push(this.createMenuItem(nls.localize('mNewTab', "New Tab"), 'workbench.action.newWindowTab'));
610
611
nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mShowPreviousTab', "Show Previous Tab"), 'workbench.action.showPreviousWindowTab', 'selectPreviousTab'));
612
nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mShowNextTab', "Show Next Tab"), 'workbench.action.showNextWindowTab', 'selectNextTab'));
613
nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mMoveTabToNewWindow', "Move Tab to New Window"), 'workbench.action.moveWindowTabToNewWindow', 'moveTabToNewWindow'));
614
nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mMergeAllWindows', "Merge All Windows"), 'workbench.action.mergeAllWindowTabs', 'mergeAllWindows'));
615
}
616
617
[
618
minimize,
619
zoom,
620
__separator__(),
621
switchWindow,
622
...nativeTabMenuItems,
623
__separator__(),
624
bringAllToFront
625
].forEach(item => macWindowMenu.append(item));
626
}
627
628
private getUpdateMenuItems(): MenuItem[] {
629
const state = this.updateService.state;
630
631
switch (state.type) {
632
case StateType.Idle:
633
return [new MenuItem({
634
label: this.mnemonicLabel(nls.localize('miCheckForUpdates', "Check for &&Updates...")), click: () => setTimeout(() => {
635
this.reportMenuActionTelemetry('CheckForUpdate');
636
this.updateService.checkForUpdates(true);
637
}, 0)
638
})];
639
640
case StateType.CheckingForUpdates:
641
return [new MenuItem({ label: nls.localize('miCheckingForUpdates', "Checking for Updates..."), enabled: false })];
642
643
case StateType.AvailableForDownload:
644
return [new MenuItem({
645
label: this.mnemonicLabel(nls.localize('miDownloadUpdate', "D&&ownload Available Update")), click: () => {
646
this.updateService.downloadUpdate(true);
647
}
648
})];
649
650
case StateType.Downloading:
651
case StateType.Overwriting:
652
return [new MenuItem({ label: nls.localize('miDownloadingUpdate', "Downloading Update..."), enabled: false })];
653
654
case StateType.Downloaded:
655
return isMacintosh ? [] : [new MenuItem({
656
label: this.mnemonicLabel(nls.localize('miInstallUpdate', "Install &&Update...")), click: () => {
657
this.reportMenuActionTelemetry('InstallUpdate');
658
this.updateService.applyUpdate();
659
}
660
})];
661
662
case StateType.Updating:
663
return [new MenuItem({ label: nls.localize('miInstallingUpdate', "Installing Update..."), enabled: false })];
664
665
case StateType.Ready:
666
return [new MenuItem({
667
label: this.mnemonicLabel(nls.localize('miRestartToUpdate', "Restart to &&Update")), click: () => {
668
this.reportMenuActionTelemetry('RestartToUpdate');
669
this.updateService.quitAndInstall();
670
}
671
})];
672
673
default:
674
return [];
675
}
676
}
677
678
private createMenuItem(labelOpt: string, commandId: string, enabledOpt?: boolean, checkedOpt?: boolean): MenuItem {
679
const label = this.mnemonicLabel(labelOpt);
680
const click = (menuItem: MenuItem & IMenuItemWithKeybinding, window: BaseWindow | undefined, event: KeyboardEvent) => {
681
const userSettingsLabel = menuItem ? menuItem.userSettingsLabel : null;
682
if (userSettingsLabel && event.triggeredByAccelerator) {
683
this.runActionInRenderer({ type: 'keybinding', userSettingsLabel });
684
} else {
685
this.runActionInRenderer({ type: 'commandId', commandId });
686
}
687
};
688
const enabled = typeof enabledOpt === 'boolean' ? enabledOpt : this.windowsMainService.getWindowCount() > 0;
689
const checked = typeof checkedOpt === 'boolean' ? checkedOpt : false;
690
691
const options: MenuItemConstructorOptions = {
692
label,
693
click,
694
enabled
695
};
696
697
if (checked) {
698
options.type = 'checkbox';
699
options.checked = checked;
700
}
701
702
if (isMacintosh) {
703
704
// Add role for special case menu items
705
if (commandId === 'editor.action.clipboardCutAction') {
706
options.role = 'cut';
707
} else if (commandId === 'editor.action.clipboardCopyAction') {
708
options.role = 'copy';
709
} else if (commandId === 'editor.action.clipboardPasteAction') {
710
options.role = 'paste';
711
}
712
713
// Add context aware click handlers for special case menu items
714
if (commandId === 'undo') {
715
options.click = this.makeContextAwareClickHandler(click, {
716
inDevTools: devTools => devTools.undo(),
717
inNoWindow: () => Menu.sendActionToFirstResponder('undo:')
718
});
719
} else if (commandId === 'redo') {
720
options.click = this.makeContextAwareClickHandler(click, {
721
inDevTools: devTools => devTools.redo(),
722
inNoWindow: () => Menu.sendActionToFirstResponder('redo:')
723
});
724
} else if (commandId === 'editor.action.selectAll') {
725
options.click = this.makeContextAwareClickHandler(click, {
726
inDevTools: devTools => devTools.selectAll(),
727
inNoWindow: () => Menu.sendActionToFirstResponder('selectAll:')
728
});
729
}
730
}
731
732
return new MenuItem(this.withKeybinding(commandId, options));
733
}
734
735
private makeContextAwareClickHandler(click: (menuItem: MenuItem, win: BaseWindow, event: KeyboardEvent) => void, contextSpecificHandlers: IMenuItemClickHandler): (menuItem: MenuItem, win: BaseWindow | undefined, event: KeyboardEvent) => void {
736
return (menuItem: MenuItem, win: BaseWindow | undefined, event: KeyboardEvent) => {
737
738
// No Active Window
739
const activeWindow = BrowserWindow.getFocusedWindow();
740
if (!activeWindow) {
741
return contextSpecificHandlers.inNoWindow();
742
}
743
744
// DevTools focused
745
if (activeWindow.webContents.isDevToolsFocused() &&
746
activeWindow.webContents.devToolsWebContents) {
747
return contextSpecificHandlers.inDevTools(activeWindow.webContents.devToolsWebContents);
748
}
749
750
// Focus is not in the workbench webContents
751
if (!activeWindow.webContents.isFocused()) {
752
return contextSpecificHandlers.inNoWindow();
753
}
754
755
// Finally execute command in Window
756
click(menuItem, win || activeWindow, event);
757
};
758
}
759
760
private runActionInRenderer(invocation: IMenuItemInvocation): boolean {
761
762
// We want to support auxililary windows that may have focus by
763
// returning their parent windows as target to support running
764
// actions via the main window.
765
let activeBrowserWindow = BrowserWindow.getFocusedWindow();
766
if (activeBrowserWindow) {
767
const auxiliaryWindowCandidate = this.auxiliaryWindowsMainService.getWindowByWebContents(activeBrowserWindow.webContents);
768
if (auxiliaryWindowCandidate) {
769
activeBrowserWindow = this.windowsMainService.getWindowById(auxiliaryWindowCandidate.parentId)?.win ?? null;
770
}
771
}
772
773
// We make sure to not run actions when the window has no focus, this helps
774
// for https://github.com/microsoft/vscode/issues/25907 and specifically for
775
// https://github.com/microsoft/vscode/issues/11928
776
// Still allow to run when the last active window is minimized though for
777
// https://github.com/microsoft/vscode/issues/63000
778
if (!activeBrowserWindow) {
779
const lastActiveWindow = this.windowsMainService.getLastActiveWindow();
780
if (lastActiveWindow?.win?.isMinimized()) {
781
activeBrowserWindow = lastActiveWindow.win;
782
}
783
}
784
785
const activeWindow = activeBrowserWindow ? this.windowsMainService.getWindowById(activeBrowserWindow.id) : undefined;
786
if (activeWindow) {
787
this.logService.trace('menubar#runActionInRenderer', invocation);
788
789
if (isMacintosh && !this.environmentMainService.isBuilt && !activeWindow.isReady) {
790
if ((invocation.type === 'commandId' && invocation.commandId === 'workbench.action.toggleDevTools') || (invocation.type !== 'commandId' && invocation.userSettingsLabel === 'alt+cmd+i')) {
791
// prevent this action from running twice on macOS (https://github.com/microsoft/vscode/issues/62719)
792
// we already register a keybinding in workbench.ts for opening developer tools in case something
793
// goes wrong and that keybinding is only removed when the application has loaded (= window ready).
794
return false;
795
}
796
}
797
798
if (invocation.type === 'commandId') {
799
const runActionPayload: INativeRunActionInWindowRequest = { id: invocation.commandId, from: 'menu' };
800
activeWindow.sendWhenReady('vscode:runAction', CancellationToken.None, runActionPayload);
801
} else {
802
const runKeybindingPayload: INativeRunKeybindingInWindowRequest = { userSettingsLabel: invocation.userSettingsLabel };
803
activeWindow.sendWhenReady('vscode:runKeybinding', CancellationToken.None, runKeybindingPayload);
804
}
805
806
return true;
807
} else {
808
this.logService.trace('menubar#runActionInRenderer: no active window found', invocation);
809
810
return false;
811
}
812
}
813
814
private withKeybinding(commandId: string | undefined, options: MenuItemConstructorOptions & IMenuItemWithKeybinding): MenuItemConstructorOptions {
815
const binding = typeof commandId === 'string' ? this.keybindings[commandId] : undefined;
816
817
// Apply binding if there is one
818
if (binding?.label) {
819
820
// if the binding is native, we can just apply it
821
if (binding.isNative !== false) {
822
options.accelerator = binding.label;
823
options.userSettingsLabel = binding.userSettingsLabel;
824
}
825
826
// the keybinding is not native so we cannot show it as part of the accelerator of
827
// the menu item. we fallback to a different strategy so that we always display it
828
else if (typeof options.label === 'string') {
829
const bindingIndex = options.label.indexOf('[');
830
if (bindingIndex >= 0) {
831
options.label = `${options.label.substr(0, bindingIndex)} [${binding.label}]`;
832
} else {
833
options.label = `${options.label} [${binding.label}]`;
834
}
835
}
836
}
837
838
// Unset bindings if there is none
839
else {
840
options.accelerator = undefined;
841
}
842
843
return options;
844
}
845
846
private likeAction(commandId: string, options: MenuItemConstructorOptions, setAccelerator = !options.accelerator): MenuItemConstructorOptions {
847
if (setAccelerator) {
848
options = this.withKeybinding(commandId, options);
849
}
850
851
const originalClick = options.click;
852
options.click = (item, window, event) => {
853
this.reportMenuActionTelemetry(commandId);
854
originalClick?.(item, window, event);
855
};
856
857
return options;
858
}
859
860
private openUrl(url: string, id: string): void {
861
this.nativeHostMainService.openExternal(undefined, url);
862
this.reportMenuActionTelemetry(id);
863
}
864
865
private reportMenuActionTelemetry(id: string): void {
866
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id, from: telemetryFrom });
867
}
868
869
private mnemonicLabel(label: string): string {
870
return mnemonicMenuLabel(label, !this.currentEnableMenuBarMnemonics);
871
}
872
}
873
874
function __separator__(): MenuItem {
875
return new MenuItem({ type: 'separator' });
876
}
877
878