Path: blob/main/src/vs/platform/menubar/electron-main/menubar.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 { 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 = new RunOnceScheduler(() => this.doUpdateMenu(), 0);8586this.menuGC = 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 ? false : true, !!item.checked));531}532} else {533menu.append(this.createMenuItem(item.label, item.id, item.enabled === false ? false : true, !!item.checked));534}535}536});537}538539private setMenuById(menu: Menu, menuId: string): void {540if (this.menubarMenus && 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: any): 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();646}647})];648649case StateType.Downloading:650return [new MenuItem({ label: nls.localize('miDownloadingUpdate', "Downloading Update..."), enabled: false })];651652case StateType.Downloaded:653return isMacintosh ? [] : [new MenuItem({654label: this.mnemonicLabel(nls.localize('miInstallUpdate', "Install &&Update...")), click: () => {655this.reportMenuActionTelemetry('InstallUpdate');656this.updateService.applyUpdate();657}658})];659660case StateType.Updating:661return [new MenuItem({ label: nls.localize('miInstallingUpdate', "Installing Update..."), enabled: false })];662663case StateType.Ready:664return [new MenuItem({665label: this.mnemonicLabel(nls.localize('miRestartToUpdate', "Restart to &&Update")), click: () => {666this.reportMenuActionTelemetry('RestartToUpdate');667this.updateService.quitAndInstall();668}669})];670671default:672return [];673}674}675676private createMenuItem(label: string, commandId: string | string[], enabled?: boolean, checked?: boolean): MenuItem;677private createMenuItem(label: string, click: () => void, enabled?: boolean, checked?: boolean): MenuItem;678private createMenuItem(arg1: string, arg2: any, arg3?: boolean, arg4?: boolean): MenuItem {679const label = this.mnemonicLabel(arg1);680const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: MenuItem & IMenuItemWithKeybinding, win: BrowserWindow, event: KeyboardEvent) => {681const userSettingsLabel = menuItem ? menuItem.userSettingsLabel : null;682let commandId = arg2;683if (Array.isArray(arg2)) {684commandId = this.isOptionClick(event) ? arg2[1] : arg2[0]; // support alternative action if we got multiple action Ids and the option key was pressed while invoking685}686687if (userSettingsLabel && event.triggeredByAccelerator) {688this.runActionInRenderer({ type: 'keybinding', userSettingsLabel });689} else {690this.runActionInRenderer({ type: 'commandId', commandId });691}692};693const enabled = typeof arg3 === 'boolean' ? arg3 : this.windowsMainService.getWindowCount() > 0;694const checked = typeof arg4 === 'boolean' ? arg4 : false;695696const options: MenuItemConstructorOptions = {697label,698click,699enabled700};701702if (checked) {703options.type = 'checkbox';704options.checked = checked;705}706707let commandId: string | undefined;708if (typeof arg2 === 'string') {709commandId = arg2;710} else if (Array.isArray(arg2)) {711commandId = arg2[0];712}713714if (isMacintosh) {715716// Add role for special case menu items717if (commandId === 'editor.action.clipboardCutAction') {718options.role = 'cut';719} else if (commandId === 'editor.action.clipboardCopyAction') {720options.role = 'copy';721} else if (commandId === 'editor.action.clipboardPasteAction') {722options.role = 'paste';723}724725// Add context aware click handlers for special case menu items726if (commandId === 'undo') {727options.click = this.makeContextAwareClickHandler(click, {728inDevTools: devTools => devTools.undo(),729inNoWindow: () => Menu.sendActionToFirstResponder('undo:')730});731} else if (commandId === 'redo') {732options.click = this.makeContextAwareClickHandler(click, {733inDevTools: devTools => devTools.redo(),734inNoWindow: () => Menu.sendActionToFirstResponder('redo:')735});736} else if (commandId === 'editor.action.selectAll') {737options.click = this.makeContextAwareClickHandler(click, {738inDevTools: devTools => devTools.selectAll(),739inNoWindow: () => Menu.sendActionToFirstResponder('selectAll:')740});741}742}743744return new MenuItem(this.withKeybinding(commandId, options));745}746747private makeContextAwareClickHandler(click: (menuItem: MenuItem, win: BaseWindow, event: KeyboardEvent) => void, contextSpecificHandlers: IMenuItemClickHandler): (menuItem: MenuItem, win: BaseWindow | undefined, event: KeyboardEvent) => void {748return (menuItem: MenuItem, win: BaseWindow | undefined, event: KeyboardEvent) => {749750// No Active Window751const activeWindow = BrowserWindow.getFocusedWindow();752if (!activeWindow) {753return contextSpecificHandlers.inNoWindow();754}755756// DevTools focused757if (activeWindow.webContents.isDevToolsFocused() &&758activeWindow.webContents.devToolsWebContents) {759return contextSpecificHandlers.inDevTools(activeWindow.webContents.devToolsWebContents);760}761762// Finally execute command in Window763click(menuItem, win || activeWindow, event);764};765}766767private runActionInRenderer(invocation: IMenuItemInvocation): boolean {768769// We want to support auxililary windows that may have focus by770// returning their parent windows as target to support running771// actions via the main window.772let activeBrowserWindow = BrowserWindow.getFocusedWindow();773if (activeBrowserWindow) {774const auxiliaryWindowCandidate = this.auxiliaryWindowsMainService.getWindowByWebContents(activeBrowserWindow.webContents);775if (auxiliaryWindowCandidate) {776activeBrowserWindow = this.windowsMainService.getWindowById(auxiliaryWindowCandidate.parentId)?.win ?? null;777}778}779780// We make sure to not run actions when the window has no focus, this helps781// for https://github.com/microsoft/vscode/issues/25907 and specifically for782// https://github.com/microsoft/vscode/issues/11928783// Still allow to run when the last active window is minimized though for784// https://github.com/microsoft/vscode/issues/63000785if (!activeBrowserWindow) {786const lastActiveWindow = this.windowsMainService.getLastActiveWindow();787if (lastActiveWindow?.win?.isMinimized()) {788activeBrowserWindow = lastActiveWindow.win;789}790}791792const activeWindow = activeBrowserWindow ? this.windowsMainService.getWindowById(activeBrowserWindow.id) : undefined;793if (activeWindow) {794this.logService.trace('menubar#runActionInRenderer', invocation);795796if (isMacintosh && !this.environmentMainService.isBuilt && !activeWindow.isReady) {797if ((invocation.type === 'commandId' && invocation.commandId === 'workbench.action.toggleDevTools') || (invocation.type !== 'commandId' && invocation.userSettingsLabel === 'alt+cmd+i')) {798// prevent this action from running twice on macOS (https://github.com/microsoft/vscode/issues/62719)799// we already register a keybinding in workbench.ts for opening developer tools in case something800// goes wrong and that keybinding is only removed when the application has loaded (= window ready).801return false;802}803}804805if (invocation.type === 'commandId') {806const runActionPayload: INativeRunActionInWindowRequest = { id: invocation.commandId, from: 'menu' };807activeWindow.sendWhenReady('vscode:runAction', CancellationToken.None, runActionPayload);808} else {809const runKeybindingPayload: INativeRunKeybindingInWindowRequest = { userSettingsLabel: invocation.userSettingsLabel };810activeWindow.sendWhenReady('vscode:runKeybinding', CancellationToken.None, runKeybindingPayload);811}812813return true;814} else {815this.logService.trace('menubar#runActionInRenderer: no active window found', invocation);816817return false;818}819}820821private withKeybinding(commandId: string | undefined, options: MenuItemConstructorOptions & IMenuItemWithKeybinding): MenuItemConstructorOptions {822const binding = typeof commandId === 'string' ? this.keybindings[commandId] : undefined;823824// Apply binding if there is one825if (binding?.label) {826827// if the binding is native, we can just apply it828if (binding.isNative !== false) {829options.accelerator = binding.label;830options.userSettingsLabel = binding.userSettingsLabel;831}832833// the keybinding is not native so we cannot show it as part of the accelerator of834// the menu item. we fallback to a different strategy so that we always display it835else if (typeof options.label === 'string') {836const bindingIndex = options.label.indexOf('[');837if (bindingIndex >= 0) {838options.label = `${options.label.substr(0, bindingIndex)} [${binding.label}]`;839} else {840options.label = `${options.label} [${binding.label}]`;841}842}843}844845// Unset bindings if there is none846else {847options.accelerator = undefined;848}849850return options;851}852853private likeAction(commandId: string, options: MenuItemConstructorOptions, setAccelerator = !options.accelerator): MenuItemConstructorOptions {854if (setAccelerator) {855options = this.withKeybinding(commandId, options);856}857858const originalClick = options.click;859options.click = (item, window, event) => {860this.reportMenuActionTelemetry(commandId);861originalClick?.(item, window, event);862};863864return options;865}866867private openUrl(url: string, id: string): void {868this.nativeHostMainService.openExternal(undefined, url);869this.reportMenuActionTelemetry(id);870}871872private reportMenuActionTelemetry(id: string): void {873this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id, from: telemetryFrom });874}875876private mnemonicLabel(label: string): string {877return mnemonicMenuLabel(label, !this.currentEnableMenuBarMnemonics);878}879}880881function __separator__(): MenuItem {882return new MenuItem({ type: 'separator' });883}884885886