Path: blob/main/src/vs/platform/menubar/electron-main/menubar.ts
5328 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 { app, BrowserWindow, BaseWindow, KeyboardEvent, Menu, MenuItem, MenuItemConstructorOptions, WebContents } from 'electron';6import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../base/common/actions.js';7import { RunOnceScheduler } from '../../../base/common/async.js';8import { CancellationToken } from '../../../base/common/cancellation.js';9import { mnemonicMenuLabel } from '../../../base/common/labels.js';10import { isMacintosh, language } from '../../../base/common/platform.js';11import { URI } from '../../../base/common/uri.js';12import * as nls from '../../../nls.js';13import { IAuxiliaryWindowsMainService } from '../../auxiliaryWindow/electron-main/auxiliaryWindows.js';14import { IConfigurationService } from '../../configuration/common/configuration.js';15import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';16import { ILifecycleMainService } from '../../lifecycle/electron-main/lifecycleMainService.js';17import { ILogService } from '../../log/common/log.js';18import { IMenubarData, IMenubarKeybinding, IMenubarMenu, IMenubarMenuRecentItemAction, isMenubarMenuItemAction, isMenubarMenuItemRecentAction, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, MenubarMenuItem } from '../common/menubar.js';19import { INativeHostMainService } from '../../native/electron-main/nativeHostMainService.js';20import { IProductService } from '../../product/common/productService.js';21import { IStateService } from '../../state/node/state.js';22import { ITelemetryService } from '../../telemetry/common/telemetry.js';23import { IUpdateService, StateType } from '../../update/common/update.js';24import { INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, IWindowOpenable, hasNativeMenu } from '../../window/common/window.js';25import { IWindowsCountChangedEvent, IWindowsMainService, OpenContext } from '../../windows/electron-main/windows.js';26import { IWorkspacesHistoryMainService } from '../../workspaces/electron-main/workspacesHistoryMainService.js';27import { Disposable } from '../../../base/common/lifecycle.js';2829const telemetryFrom = 'menu';3031interface IMenuItemClickHandler {32inDevTools: (contents: WebContents) => void;33inNoWindow: () => void;34}3536type IMenuItemInvocation = (37{ type: 'commandId'; commandId: string }38| { type: 'keybinding'; userSettingsLabel: string }39);4041interface IMenuItemWithKeybinding {42userSettingsLabel?: string;43}4445export class Menubar extends Disposable {4647private static readonly lastKnownMenubarStorageKey = 'lastKnownMenubarData';4849private willShutdown: boolean | undefined;50private appMenuInstalled: boolean | undefined;51private closedLastWindow: boolean;52private noActiveMainWindow: boolean;53private showNativeMenu: boolean;5455private menuUpdater: RunOnceScheduler;56private menuGC: RunOnceScheduler;5758// Array to keep menus around so that GC doesn't cause crash as explained in #5534759// TODO@sbatten Remove this when fixed upstream by Electron60private oldMenus: Menu[];6162private menubarMenus: { [id: string]: IMenubarMenu };6364private keybindings: { [commandId: string]: IMenubarKeybinding };6566private readonly fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BaseWindow | undefined, event: KeyboardEvent) => void } = Object.create(null);6768constructor(69@IUpdateService private readonly updateService: IUpdateService,70@IConfigurationService private readonly configurationService: IConfigurationService,71@IWindowsMainService private readonly windowsMainService: IWindowsMainService,72@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,73@ITelemetryService private readonly telemetryService: ITelemetryService,74@IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService,75@IStateService private readonly stateService: IStateService,76@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,77@ILogService private readonly logService: ILogService,78@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,79@IProductService private readonly productService: IProductService,80@IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService81) {82super();8384this.menuUpdater = this._register(new RunOnceScheduler(() => this.doUpdateMenu(), 0));8586this.menuGC = this._register(new RunOnceScheduler(() => { this.oldMenus = []; }, 10000));8788this.menubarMenus = Object.create(null);89this.keybindings = Object.create(null);90this.showNativeMenu = hasNativeMenu(configurationService);9192if (isMacintosh || this.showNativeMenu) {93this.restoreCachedMenubarData();94}9596this.addFallbackHandlers();9798this.closedLastWindow = false;99this.noActiveMainWindow = false;100101this.oldMenus = [];102103this.install();104105this.registerListeners();106}107108private restoreCachedMenubarData() {109const menubarData = this.stateService.getItem<IMenubarData>(Menubar.lastKnownMenubarStorageKey);110if (menubarData) {111if (menubarData.menus) {112this.menubarMenus = menubarData.menus;113}114115if (menubarData.keybindings) {116this.keybindings = menubarData.keybindings;117}118}119}120121private addFallbackHandlers(): void {122123// File Menu Items124this.fallbackMenuHandlers['workbench.action.files.newUntitledFile'] = (menuItem, win, event) => {125if (!this.runActionInRenderer({ type: 'commandId', commandId: 'workbench.action.files.newUntitledFile' })) { // this is one of the few supported actions when aux window has focus126this.windowsMainService.openEmptyWindow({ context: OpenContext.MENU, contextWindowId: win?.id });127}128};129this.fallbackMenuHandlers['workbench.action.newWindow'] = (menuItem, win, event) => this.windowsMainService.openEmptyWindow({ context: OpenContext.MENU, contextWindowId: win?.id });130this.fallbackMenuHandlers['workbench.action.files.openFileFolder'] = (menuItem, win, event) => this.nativeHostMainService.pickFileFolderAndOpen(undefined, { forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } });131this.fallbackMenuHandlers['workbench.action.files.openFolder'] = (menuItem, win, event) => this.nativeHostMainService.pickFolderAndOpen(undefined, { forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } });132this.fallbackMenuHandlers['workbench.action.openWorkspace'] = (menuItem, win, event) => this.nativeHostMainService.pickWorkspaceAndOpen(undefined, { forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } });133134// Recent Menu Items135this.fallbackMenuHandlers['workbench.action.clearRecentFiles'] = () => this.workspacesHistoryMainService.clearRecentlyOpened({ confirm: true /* ask for confirmation */ });136137// Help Menu Items138const youTubeUrl = this.productService.youTubeUrl;139if (youTubeUrl) {140this.fallbackMenuHandlers['workbench.action.openYouTubeUrl'] = () => this.openUrl(youTubeUrl, 'openYouTubeUrl');141}142143const requestFeatureUrl = this.productService.requestFeatureUrl;144if (requestFeatureUrl) {145this.fallbackMenuHandlers['workbench.action.openRequestFeatureUrl'] = () => this.openUrl(requestFeatureUrl, 'openUserVoiceUrl');146}147148const reportIssueUrl = this.productService.reportIssueUrl;149if (reportIssueUrl) {150this.fallbackMenuHandlers['workbench.action.openIssueReporter'] = () => this.openUrl(reportIssueUrl, 'openReportIssues');151}152153const licenseUrl = this.productService.licenseUrl;154if (licenseUrl) {155this.fallbackMenuHandlers['workbench.action.openLicenseUrl'] = () => {156if (language) {157const queryArgChar = licenseUrl.indexOf('?') > 0 ? '&' : '?';158this.openUrl(`${licenseUrl}${queryArgChar}lang=${language}`, 'openLicenseUrl');159} else {160this.openUrl(licenseUrl, 'openLicenseUrl');161}162};163}164165const privacyStatementUrl = this.productService.privacyStatementUrl;166if (privacyStatementUrl && licenseUrl) {167this.fallbackMenuHandlers['workbench.action.openPrivacyStatementUrl'] = () => {168this.openUrl(privacyStatementUrl, 'openPrivacyStatement');169};170}171}172173private registerListeners(): void {174175// Keep flag when app quits176this._register(this.lifecycleMainService.onWillShutdown(() => this.willShutdown = true));177178// Listen to some events from window service to update menu179this._register(this.windowsMainService.onDidChangeWindowsCount(e => this.onDidChangeWindowsCount(e)));180this._register(this.nativeHostMainService.onDidBlurMainWindow(() => this.onDidChangeWindowFocus()));181this._register(this.nativeHostMainService.onDidFocusMainWindow(() => this.onDidChangeWindowFocus()));182}183184private get currentEnableMenuBarMnemonics(): boolean {185const enableMenuBarMnemonics = this.configurationService.getValue('window.enableMenuBarMnemonics');186if (typeof enableMenuBarMnemonics !== 'boolean') {187return true;188}189190return enableMenuBarMnemonics;191}192193private get currentEnableNativeTabs(): boolean {194if (!isMacintosh) {195return false;196}197198const enableNativeTabs = this.configurationService.getValue('window.nativeTabs');199if (typeof enableNativeTabs !== 'boolean') {200return false;201}202return enableNativeTabs;203}204205updateMenu(menubarData: IMenubarData, windowId: number) {206this.menubarMenus = menubarData.menus;207this.keybindings = menubarData.keybindings;208209// Save off new menu and keybindings210this.stateService.setItem(Menubar.lastKnownMenubarStorageKey, menubarData);211212this.scheduleUpdateMenu();213}214215216private scheduleUpdateMenu(): void {217this.menuUpdater.schedule(); // buffer multiple attempts to update the menu218}219220private doUpdateMenu(): void {221222// Due to limitations in Electron, it is not possible to update menu items dynamically. The suggested223// workaround from Electron is to set the application menu again.224// See also https://github.com/electron/electron/issues/846225//226// Run delayed to prevent updating menu while it is open227if (!this.willShutdown) {228setTimeout(() => {229if (!this.willShutdown) {230this.install();231}232}, 10 /* delay this because there is an issue with updating a menu when it is open */);233}234}235236private onDidChangeWindowsCount(e: IWindowsCountChangedEvent): void {237if (!isMacintosh) {238return;239}240241// Update menu if window count goes from N > 0 or 0 > N to update menu item enablement242if ((e.oldCount === 0 && e.newCount > 0) || (e.oldCount > 0 && e.newCount === 0)) {243this.closedLastWindow = e.newCount === 0;244this.scheduleUpdateMenu();245}246}247248private onDidChangeWindowFocus(): void {249if (!isMacintosh) {250return;251}252253const focusedWindow = BrowserWindow.getFocusedWindow();254this.noActiveMainWindow = !focusedWindow || !!this.auxiliaryWindowsMainService.getWindowByWebContents(focusedWindow.webContents);255this.scheduleUpdateMenu();256}257258private install(): void {259// Store old menu in our array to avoid GC to collect the menu and crash. See #55347260// TODO@sbatten Remove this when fixed upstream by Electron261const oldMenu = Menu.getApplicationMenu();262if (oldMenu) {263this.oldMenus.push(oldMenu);264}265266// If we don't have a menu yet, set it to null to avoid the electron menu.267// This should only happen on the first launch ever268if (Object.keys(this.menubarMenus).length === 0) {269this.doSetApplicationMenu(isMacintosh ? new Menu() : null);270return;271}272273// Menus274const menubar = new Menu();275276// Mac: Application277let macApplicationMenuItem: MenuItem;278if (isMacintosh) {279const applicationMenu = new Menu();280macApplicationMenuItem = new MenuItem({ label: this.productService.nameShort, submenu: applicationMenu });281this.setMacApplicationMenu(applicationMenu);282menubar.append(macApplicationMenuItem);283}284285// Mac: Dock286if (isMacintosh && !this.appMenuInstalled) {287this.appMenuInstalled = true;288289const dockMenu = new Menu();290dockMenu.append(new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window")), click: () => this.windowsMainService.openEmptyWindow({ context: OpenContext.DOCK }) }));291292app.dock!.setMenu(dockMenu);293}294295// File296if (this.shouldDrawMenu('File')) {297const fileMenu = new Menu();298const fileMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File")), submenu: fileMenu });299this.setMenuById(fileMenu, 'File');300menubar.append(fileMenuItem);301}302303// Edit304if (this.shouldDrawMenu('Edit')) {305const editMenu = new Menu();306const editMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit")), submenu: editMenu });307this.setMenuById(editMenu, 'Edit');308menubar.append(editMenuItem);309}310311// Selection312if (this.shouldDrawMenu('Selection')) {313const selectionMenu = new Menu();314const selectionMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection")), submenu: selectionMenu });315this.setMenuById(selectionMenu, 'Selection');316menubar.append(selectionMenuItem);317}318319// View320if (this.shouldDrawMenu('View')) {321const viewMenu = new Menu();322const viewMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View")), submenu: viewMenu });323this.setMenuById(viewMenu, 'View');324menubar.append(viewMenuItem);325}326327// Go328if (this.shouldDrawMenu('Go')) {329const gotoMenu = new Menu();330const gotoMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go")), submenu: gotoMenu });331this.setMenuById(gotoMenu, 'Go');332menubar.append(gotoMenuItem);333}334335// Debug336if (this.shouldDrawMenu('Run')) {337const debugMenu = new Menu();338const debugMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mRun', comment: ['&& denotes a mnemonic'] }, "&&Run")), submenu: debugMenu });339this.setMenuById(debugMenu, 'Run');340menubar.append(debugMenuItem);341}342343// Terminal344if (this.shouldDrawMenu('Terminal')) {345const terminalMenu = new Menu();346const terminalMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal")), submenu: terminalMenu });347this.setMenuById(terminalMenu, 'Terminal');348menubar.append(terminalMenuItem);349}350351// Mac: Window352let macWindowMenuItem: MenuItem | undefined;353if (this.shouldDrawMenu('Window')) {354const windowMenu = new Menu();355macWindowMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize('mWindow', "Window")), submenu: windowMenu, role: 'window' });356this.setMacWindowMenu(windowMenu);357}358359if (macWindowMenuItem) {360menubar.append(macWindowMenuItem);361}362363// Help364if (this.shouldDrawMenu('Help')) {365const helpMenu = new Menu();366const helpMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help")), submenu: helpMenu, role: 'help' });367this.setMenuById(helpMenu, 'Help');368menubar.append(helpMenuItem);369}370371if (menubar.items && menubar.items.length > 0) {372this.doSetApplicationMenu(menubar);373} else {374this.doSetApplicationMenu(null);375}376377// Dispose of older menus after some time378this.menuGC.schedule();379}380381private doSetApplicationMenu(menu: (Menu) | (null)): void {382383// Setting the application menu sets it to all opened windows,384// but we currently do not support a menu in auxiliary windows,385// so we need to unset it there.386//387// This is a bit ugly but `setApplicationMenu()` has some nice388// behaviour we want:389// - on macOS it is required because menus are application set390// - we use `getApplicationMenu()` to access the current state391// - new windows immediately get the same menu when opening392// reducing overall flicker for these393394Menu.setApplicationMenu(menu);395396if (menu) {397for (const window of this.auxiliaryWindowsMainService.getWindows()) {398window.win?.setMenu(null);399}400}401}402403private setMacApplicationMenu(macApplicationMenu: Menu): void {404const about = this.createMenuItem(nls.localize('mAbout', "About {0}", this.productService.nameLong), 'workbench.action.showAboutDialog');405const checkForUpdates = this.getUpdateMenuItems();406407let preferences;408if (this.shouldDrawMenu('Preferences')) {409const preferencesMenu = new Menu();410this.setMenuById(preferencesMenu, 'Preferences');411preferences = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miPreferences', comment: ['&& denotes a mnemonic'] }, "&&Preferences")), submenu: preferencesMenu });412}413414const servicesMenu = new Menu();415const services = new MenuItem({ label: nls.localize('mServices', "Services"), role: 'services', submenu: servicesMenu });416const hide = new MenuItem({ label: nls.localize('mHide', "Hide {0}", this.productService.nameLong), role: 'hide', accelerator: 'Command+H' });417const hideOthers = new MenuItem({ label: nls.localize('mHideOthers', "Hide Others"), role: 'hideOthers', accelerator: 'Command+Alt+H' });418const showAll = new MenuItem({ label: nls.localize('mShowAll', "Show All"), role: 'unhide' });419const quit = new MenuItem(this.likeAction('workbench.action.quit', {420label: nls.localize('miQuit', "Quit {0}", this.productService.nameLong), click: async (item, window, event) => {421const lastActiveWindow = this.windowsMainService.getLastActiveWindow();422if (423this.windowsMainService.getWindowCount() === 0 || // allow to quit when no more windows are open424!!BrowserWindow.getFocusedWindow() || // allow to quit when window has focus (fix for https://github.com/microsoft/vscode/issues/39191)425lastActiveWindow?.win?.isMinimized() // allow to quit when window has no focus but is minimized (https://github.com/microsoft/vscode/issues/63000)426) {427const confirmed = await this.confirmBeforeQuit(event);428if (confirmed) {429this.nativeHostMainService.quit(undefined);430}431}432}433}));434435const actions = [about];436actions.push(...checkForUpdates);437438if (preferences) {439actions.push(...[440__separator__(),441preferences442]);443}444445actions.push(...[446__separator__(),447services,448__separator__(),449hide,450hideOthers,451showAll,452__separator__(),453quit454]);455456actions.forEach(i => macApplicationMenu.append(i));457}458459private async confirmBeforeQuit(event: KeyboardEvent): Promise<boolean> {460if (this.windowsMainService.getWindowCount() === 0) {461return true; // never confirm when no windows are opened462}463464const confirmBeforeClose = this.configurationService.getValue<'always' | 'never' | 'keyboardOnly'>('window.confirmBeforeClose');465if (confirmBeforeClose === 'always' || (confirmBeforeClose === 'keyboardOnly' && this.isKeyboardEvent(event))) {466const { response } = await this.nativeHostMainService.showMessageBox(this.windowsMainService.getFocusedWindow()?.id, {467type: 'question',468buttons: [469isMacintosh ? nls.localize({ key: 'quit', comment: ['&& denotes a mnemonic'] }, "&&Quit") : nls.localize({ key: 'exit', comment: ['&& denotes a mnemonic'] }, "&&Exit"),470nls.localize('cancel', "Cancel")471],472message: isMacintosh ? nls.localize('quitMessageMac', "Are you sure you want to quit?") : nls.localize('quitMessage', "Are you sure you want to exit?")473});474475return response === 0;476}477478return true;479}480481private shouldDrawMenu(menuId: string): boolean {482if (!isMacintosh && !this.showNativeMenu) {483return false; // We need to draw an empty menu to override the electron default484}485486switch (menuId) {487case 'File':488case 'Help':489if (isMacintosh) {490return (this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) || (this.windowsMainService.getWindowCount() > 0 && this.noActiveMainWindow) || (!!this.menubarMenus && !!this.menubarMenus[menuId]);491}492493case 'Window':494if (isMacintosh) {495return (this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) || (this.windowsMainService.getWindowCount() > 0 && this.noActiveMainWindow) || !!this.menubarMenus;496}497498default:499return this.windowsMainService.getWindowCount() > 0 && (!!this.menubarMenus && !!this.menubarMenus[menuId]);500}501}502503504private setMenu(menu: Menu, items: Array<MenubarMenuItem>) {505items.forEach((item: MenubarMenuItem) => {506if (isMenubarMenuItemSeparator(item)) {507menu.append(__separator__());508} else if (isMenubarMenuItemSubmenu(item)) {509const submenu = new Menu();510const submenuItem = new MenuItem({ label: this.mnemonicLabel(item.label), submenu });511this.setMenu(submenu, item.submenu.items);512menu.append(submenuItem);513} else if (isMenubarMenuItemRecentAction(item)) {514menu.append(this.createOpenRecentMenuItem(item));515} else if (isMenubarMenuItemAction(item)) {516if (item.id === 'workbench.action.showAboutDialog') {517this.insertCheckForUpdatesItems(menu);518}519520if (isMacintosh) {521if ((this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) ||522(this.windowsMainService.getWindowCount() > 0 && this.noActiveMainWindow)) {523// In the fallback scenario, we are either disabled or using a fallback handler524if (this.fallbackMenuHandlers[item.id]) {525menu.append(new MenuItem(this.likeAction(item.id, { label: this.mnemonicLabel(item.label), click: this.fallbackMenuHandlers[item.id] })));526} else {527menu.append(this.createMenuItem(item.label, item.id, false, item.checked));528}529} else {530menu.append(this.createMenuItem(item.label, item.id, item.enabled !== false, !!item.checked));531}532} else {533menu.append(this.createMenuItem(item.label, item.id, item.enabled !== false, !!item.checked));534}535}536});537}538539private setMenuById(menu: Menu, menuId: string): void {540if (this.menubarMenus?.[menuId]) {541this.setMenu(menu, this.menubarMenus[menuId].items);542}543}544545private insertCheckForUpdatesItems(menu: Menu) {546const updateItems = this.getUpdateMenuItems();547if (updateItems.length) {548updateItems.forEach(i => menu.append(i));549menu.append(__separator__());550}551}552553private createOpenRecentMenuItem(item: IMenubarMenuRecentItemAction): MenuItem {554const revivedUri = URI.revive(item.uri);555const commandId = item.id;556const openable: IWindowOpenable =557(commandId === 'openRecentFile') ? { fileUri: revivedUri } :558(commandId === 'openRecentWorkspace') ? { workspaceUri: revivedUri } : { folderUri: revivedUri };559560return new MenuItem(this.likeAction(commandId, {561label: item.label,562click: async (menuItem, win, event) => {563const openInNewWindow = this.isOptionClick(event);564const success = (await this.windowsMainService.open({565context: OpenContext.MENU,566cli: this.environmentMainService.args,567urisToOpen: [openable],568forceNewWindow: openInNewWindow,569gotoLineMode: false,570remoteAuthority: item.remoteAuthority571})).length > 0;572573if (!success) {574await this.workspacesHistoryMainService.removeRecentlyOpened([revivedUri]);575}576}577}, false));578}579580private isOptionClick(event: KeyboardEvent): boolean {581return !!(event && ((!isMacintosh && (event.ctrlKey || event.shiftKey)) || (isMacintosh && (event.metaKey || event.altKey))));582}583584private isKeyboardEvent(event: KeyboardEvent): boolean {585return !!(event.triggeredByAccelerator || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);586}587588private 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 {589const options: MenuItemConstructorOptions = {590label: this.mnemonicLabel(label),591role,592enabled: true593};594595return new MenuItem(this.withKeybinding(commandId, options));596}597598private setMacWindowMenu(macWindowMenu: Menu): void {599const minimize = new MenuItem({ label: nls.localize('mMinimize', "Minimize"), role: 'minimize', accelerator: 'Command+M', enabled: this.windowsMainService.getWindowCount() > 0 });600const zoom = new MenuItem({ label: nls.localize('mZoom', "Zoom"), role: 'zoom', enabled: this.windowsMainService.getWindowCount() > 0 });601const bringAllToFront = new MenuItem({ label: nls.localize('mBringToFront', "Bring All to Front"), role: 'front', enabled: this.windowsMainService.getWindowCount() > 0 });602const switchWindow = this.createMenuItem(nls.localize({ key: 'miSwitchWindow', comment: ['&& denotes a mnemonic'] }, "Switch &&Window..."), 'workbench.action.switchWindow');603604const nativeTabMenuItems: MenuItem[] = [];605if (this.currentEnableNativeTabs) {606nativeTabMenuItems.push(__separator__());607608nativeTabMenuItems.push(this.createMenuItem(nls.localize('mNewTab', "New Tab"), 'workbench.action.newWindowTab'));609610nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mShowPreviousTab', "Show Previous Tab"), 'workbench.action.showPreviousWindowTab', 'selectPreviousTab'));611nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mShowNextTab', "Show Next Tab"), 'workbench.action.showNextWindowTab', 'selectNextTab'));612nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mMoveTabToNewWindow', "Move Tab to New Window"), 'workbench.action.moveWindowTabToNewWindow', 'moveTabToNewWindow'));613nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mMergeAllWindows', "Merge All Windows"), 'workbench.action.mergeAllWindowTabs', 'mergeAllWindows'));614}615616[617minimize,618zoom,619__separator__(),620switchWindow,621...nativeTabMenuItems,622__separator__(),623bringAllToFront624].forEach(item => macWindowMenu.append(item));625}626627private getUpdateMenuItems(): MenuItem[] {628const state = this.updateService.state;629630switch (state.type) {631case StateType.Idle:632return [new MenuItem({633label: this.mnemonicLabel(nls.localize('miCheckForUpdates', "Check for &&Updates...")), click: () => setTimeout(() => {634this.reportMenuActionTelemetry('CheckForUpdate');635this.updateService.checkForUpdates(true);636}, 0)637})];638639case StateType.CheckingForUpdates:640return [new MenuItem({ label: nls.localize('miCheckingForUpdates', "Checking for Updates..."), enabled: false })];641642case StateType.AvailableForDownload:643return [new MenuItem({644label: this.mnemonicLabel(nls.localize('miDownloadUpdate', "D&&ownload Available Update")), click: () => {645this.updateService.downloadUpdate(true);646}647})];648649case StateType.Downloading:650case StateType.Overwriting:651return [new MenuItem({ label: nls.localize('miDownloadingUpdate', "Downloading Update..."), enabled: false })];652653case StateType.Downloaded:654return isMacintosh ? [] : [new MenuItem({655label: this.mnemonicLabel(nls.localize('miInstallUpdate', "Install &&Update...")), click: () => {656this.reportMenuActionTelemetry('InstallUpdate');657this.updateService.applyUpdate();658}659})];660661case StateType.Updating:662return [new MenuItem({ label: nls.localize('miInstallingUpdate', "Installing Update..."), enabled: false })];663664case StateType.Ready:665return [new MenuItem({666label: this.mnemonicLabel(nls.localize('miRestartToUpdate', "Restart to &&Update")), click: () => {667this.reportMenuActionTelemetry('RestartToUpdate');668this.updateService.quitAndInstall();669}670})];671672default:673return [];674}675}676677private createMenuItem(labelOpt: string, commandId: string, enabledOpt?: boolean, checkedOpt?: boolean): MenuItem {678const label = this.mnemonicLabel(labelOpt);679const click = (menuItem: MenuItem & IMenuItemWithKeybinding, window: BaseWindow | undefined, event: KeyboardEvent) => {680const userSettingsLabel = menuItem ? menuItem.userSettingsLabel : null;681if (userSettingsLabel && event.triggeredByAccelerator) {682this.runActionInRenderer({ type: 'keybinding', userSettingsLabel });683} else {684this.runActionInRenderer({ type: 'commandId', commandId });685}686};687const enabled = typeof enabledOpt === 'boolean' ? enabledOpt : this.windowsMainService.getWindowCount() > 0;688const checked = typeof checkedOpt === 'boolean' ? checkedOpt : false;689690const options: MenuItemConstructorOptions = {691label,692click,693enabled694};695696if (checked) {697options.type = 'checkbox';698options.checked = checked;699}700701if (isMacintosh) {702703// Add role for special case menu items704if (commandId === 'editor.action.clipboardCutAction') {705options.role = 'cut';706} else if (commandId === 'editor.action.clipboardCopyAction') {707options.role = 'copy';708} else if (commandId === 'editor.action.clipboardPasteAction') {709options.role = 'paste';710}711712// Add context aware click handlers for special case menu items713if (commandId === 'undo') {714options.click = this.makeContextAwareClickHandler(click, {715inDevTools: devTools => devTools.undo(),716inNoWindow: () => Menu.sendActionToFirstResponder('undo:')717});718} else if (commandId === 'redo') {719options.click = this.makeContextAwareClickHandler(click, {720inDevTools: devTools => devTools.redo(),721inNoWindow: () => Menu.sendActionToFirstResponder('redo:')722});723} else if (commandId === 'editor.action.selectAll') {724options.click = this.makeContextAwareClickHandler(click, {725inDevTools: devTools => devTools.selectAll(),726inNoWindow: () => Menu.sendActionToFirstResponder('selectAll:')727});728}729}730731return new MenuItem(this.withKeybinding(commandId, options));732}733734private makeContextAwareClickHandler(click: (menuItem: MenuItem, win: BaseWindow, event: KeyboardEvent) => void, contextSpecificHandlers: IMenuItemClickHandler): (menuItem: MenuItem, win: BaseWindow | undefined, event: KeyboardEvent) => void {735return (menuItem: MenuItem, win: BaseWindow | undefined, event: KeyboardEvent) => {736737// No Active Window738const activeWindow = BrowserWindow.getFocusedWindow();739if (!activeWindow) {740return contextSpecificHandlers.inNoWindow();741}742743// DevTools focused744if (activeWindow.webContents.isDevToolsFocused() &&745activeWindow.webContents.devToolsWebContents) {746return contextSpecificHandlers.inDevTools(activeWindow.webContents.devToolsWebContents);747}748749// Focus is not in the workbench webContents750if (!activeWindow.webContents.isFocused()) {751return contextSpecificHandlers.inNoWindow();752}753754// Finally execute command in Window755click(menuItem, win || activeWindow, event);756};757}758759private runActionInRenderer(invocation: IMenuItemInvocation): boolean {760761// We want to support auxililary windows that may have focus by762// returning their parent windows as target to support running763// actions via the main window.764let activeBrowserWindow = BrowserWindow.getFocusedWindow();765if (activeBrowserWindow) {766const auxiliaryWindowCandidate = this.auxiliaryWindowsMainService.getWindowByWebContents(activeBrowserWindow.webContents);767if (auxiliaryWindowCandidate) {768activeBrowserWindow = this.windowsMainService.getWindowById(auxiliaryWindowCandidate.parentId)?.win ?? null;769}770}771772// We make sure to not run actions when the window has no focus, this helps773// for https://github.com/microsoft/vscode/issues/25907 and specifically for774// https://github.com/microsoft/vscode/issues/11928775// Still allow to run when the last active window is minimized though for776// https://github.com/microsoft/vscode/issues/63000777if (!activeBrowserWindow) {778const lastActiveWindow = this.windowsMainService.getLastActiveWindow();779if (lastActiveWindow?.win?.isMinimized()) {780activeBrowserWindow = lastActiveWindow.win;781}782}783784const activeWindow = activeBrowserWindow ? this.windowsMainService.getWindowById(activeBrowserWindow.id) : undefined;785if (activeWindow) {786this.logService.trace('menubar#runActionInRenderer', invocation);787788if (isMacintosh && !this.environmentMainService.isBuilt && !activeWindow.isReady) {789if ((invocation.type === 'commandId' && invocation.commandId === 'workbench.action.toggleDevTools') || (invocation.type !== 'commandId' && invocation.userSettingsLabel === 'alt+cmd+i')) {790// prevent this action from running twice on macOS (https://github.com/microsoft/vscode/issues/62719)791// we already register a keybinding in workbench.ts for opening developer tools in case something792// goes wrong and that keybinding is only removed when the application has loaded (= window ready).793return false;794}795}796797if (invocation.type === 'commandId') {798const runActionPayload: INativeRunActionInWindowRequest = { id: invocation.commandId, from: 'menu' };799activeWindow.sendWhenReady('vscode:runAction', CancellationToken.None, runActionPayload);800} else {801const runKeybindingPayload: INativeRunKeybindingInWindowRequest = { userSettingsLabel: invocation.userSettingsLabel };802activeWindow.sendWhenReady('vscode:runKeybinding', CancellationToken.None, runKeybindingPayload);803}804805return true;806} else {807this.logService.trace('menubar#runActionInRenderer: no active window found', invocation);808809return false;810}811}812813private withKeybinding(commandId: string | undefined, options: MenuItemConstructorOptions & IMenuItemWithKeybinding): MenuItemConstructorOptions {814const binding = typeof commandId === 'string' ? this.keybindings[commandId] : undefined;815816// Apply binding if there is one817if (binding?.label) {818819// if the binding is native, we can just apply it820if (binding.isNative !== false) {821options.accelerator = binding.label;822options.userSettingsLabel = binding.userSettingsLabel;823}824825// the keybinding is not native so we cannot show it as part of the accelerator of826// the menu item. we fallback to a different strategy so that we always display it827else if (typeof options.label === 'string') {828const bindingIndex = options.label.indexOf('[');829if (bindingIndex >= 0) {830options.label = `${options.label.substr(0, bindingIndex)} [${binding.label}]`;831} else {832options.label = `${options.label} [${binding.label}]`;833}834}835}836837// Unset bindings if there is none838else {839options.accelerator = undefined;840}841842return options;843}844845private likeAction(commandId: string, options: MenuItemConstructorOptions, setAccelerator = !options.accelerator): MenuItemConstructorOptions {846if (setAccelerator) {847options = this.withKeybinding(commandId, options);848}849850const originalClick = options.click;851options.click = (item, window, event) => {852this.reportMenuActionTelemetry(commandId);853originalClick?.(item, window, event);854};855856return options;857}858859private openUrl(url: string, id: string): void {860this.nativeHostMainService.openExternal(undefined, url);861this.reportMenuActionTelemetry(id);862}863864private reportMenuActionTelemetry(id: string): void {865this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id, from: telemetryFrom });866}867868private mnemonicLabel(label: string): string {869return mnemonicMenuLabel(label, !this.currentEnableMenuBarMnemonics);870}871}872873function __separator__(): MenuItem {874return new MenuItem({ type: 'separator' });875}876877878