menubar.ts 30.6 KB
Newer Older
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as nls from 'vs/nls';
7
import { isMacintosh, language } from 'vs/base/common/platform';
8
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
9
import { app, shell, Menu, MenuItem, BrowserWindow } from 'electron';
M
Martin Aeschlimann 已提交
10
import { OpenContext, IRunActionInWindowRequest, getTitleBarStyle, IRunKeybindingInWindowRequest, IURIToOpen } from 'vs/platform/windows/common/windows';
11
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
12 13
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IUpdateService, StateType } from 'vs/platform/update/common/update';
14
import product from 'vs/platform/product/node/product';
15 16
import { RunOnceScheduler } from 'vs/base/common/async';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
17
import { mnemonicMenuLabel as baseMnemonicLabel } from 'vs/base/common/labels';
18 19
import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows';
import { IHistoryMainService } from 'vs/platform/history/common/history';
20
import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemUriAction } from 'vs/platform/menubar/common/menubar';
21
import { URI } from 'vs/base/common/uri';
22
import { IStateService } from 'vs/platform/state/common/state';
B
Benjamin Pasero 已提交
23
import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain';
24
import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions';
25 26 27

const telemetryFrom = 'menu';

28 29 30 31 32
interface IMenuItemClickHandler {
	inDevTools: (contents: Electron.WebContents) => void;
	inNoWindow: () => void;
}

33 34 35 36 37 38 39 40 41
type IMenuItemInvocation = (
	{ type: 'commandId'; commandId: string; }
	| { type: 'keybinding'; userSettingsLabel: string; }
);

interface IMenuItemWithKeybinding {
	userSettingsLabel?: string;
}

42 43
export class Menubar {

44
	private static readonly lastKnownMenubarStorageKey = 'lastKnownMenubarData';
45

B
Benjamin Pasero 已提交
46
	private willShutdown: boolean;
47
	private appMenuInstalled: boolean;
48
	private closedLastWindow: boolean;
49 50

	private menuUpdater: RunOnceScheduler;
51 52 53 54 55
	private menuGC: RunOnceScheduler;

	// Array to keep menus around so that GC doesn't cause crash as explained in #55347
	// TODO@sbatten Remove this when fixed upstream by Electron
	private oldMenus: Menu[];
56

57
	private menubarMenus: { [id: string]: IMenubarMenu };
58

S
SteVen Batten 已提交
59
	private keybindings: { [commandId: string]: IMenubarKeybinding };
S
SteVen Batten 已提交
60

61 62
	private fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BrowserWindow, event: Electron.Event) => void } = {};

63
	constructor(
64
		@IUpdateService private readonly updateService: IUpdateService,
65
		@IInstantiationService instantiationService: IInstantiationService,
66 67 68 69 70 71 72
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
		@IEnvironmentService private readonly environmentService: IEnvironmentService,
		@ITelemetryService private readonly telemetryService: ITelemetryService,
		@IHistoryMainService private readonly historyMainService: IHistoryMainService,
		@IStateService private readonly stateService: IStateService,
		@ILifecycleService private readonly lifecycleService: ILifecycleService
73 74
	) {
		this.menuUpdater = new RunOnceScheduler(() => this.doUpdateMenu(), 0);
S
SteVen Batten 已提交
75

76 77
		this.menuGC = new RunOnceScheduler(() => { this.oldMenus = []; }, 10000);

78 79
		this.menubarMenus = Object.create(null);
		this.keybindings = Object.create(null);
80

B
Benjamin Pasero 已提交
81
		if (isMacintosh || getTitleBarStyle(this.configurationService, this.environmentService) === 'native') {
82 83
			this.restoreCachedMenubarData();
		}
84 85

		this.addFallbackHandlers();
86

87 88
		this.closedLastWindow = false;

89 90
		this.oldMenus = [];

91 92 93 94 95
		this.install();

		this.registerListeners();
	}

96 97 98 99 100 101 102 103 104 105 106 107 108
	private restoreCachedMenubarData() {
		const menubarData = this.stateService.getItem<IMenubarData>(Menubar.lastKnownMenubarStorageKey);
		if (menubarData) {
			if (menubarData.menus) {
				this.menubarMenus = menubarData.menus;
			}

			if (menubarData.keybindings) {
				this.keybindings = menubarData.keybindings;
			}
		}
	}

109
	private addFallbackHandlers(): void {
110

111 112 113 114 115 116 117 118 119 120 121
		// File Menu Items
		this.fallbackMenuHandlers['workbench.action.files.newUntitledFile'] = () => this.windowsMainService.openNewWindow(OpenContext.MENU);
		this.fallbackMenuHandlers['workbench.action.newWindow'] = () => this.windowsMainService.openNewWindow(OpenContext.MENU);
		this.fallbackMenuHandlers['workbench.action.files.openFileFolder'] = (menuItem, win, event) => this.windowsMainService.pickFileFolderAndOpen({ forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } });
		this.fallbackMenuHandlers['workbench.action.openWorkspace'] = (menuItem, win, event) => this.windowsMainService.pickWorkspaceAndOpen({ forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } });

		// Recent Menu Items
		this.fallbackMenuHandlers['workbench.action.clearRecentFiles'] = () => this.historyMainService.clearRecentlyOpened();

		// Help Menu Items
		if (product.twitterUrl) {
S
SteVen Batten 已提交
122
			this.fallbackMenuHandlers['workbench.action.openTwitterUrl'] = () => this.openUrl(product.twitterUrl, 'openTwitterUrl');
123 124 125 126 127 128 129 130 131 132 133
		}

		if (product.requestFeatureUrl) {
			this.fallbackMenuHandlers['workbench.action.openRequestFeatureUrl'] = () => this.openUrl(product.requestFeatureUrl, 'openUserVoiceUrl');
		}

		if (product.reportIssueUrl) {
			this.fallbackMenuHandlers['workbench.action.openIssueReporter'] = () => this.openUrl(product.reportIssueUrl, 'openReportIssues');
		}

		if (product.licenseUrl) {
S
SteVen Batten 已提交
134
			this.fallbackMenuHandlers['workbench.action.openLicenseUrl'] = () => {
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
				if (language) {
					const queryArgChar = product.licenseUrl.indexOf('?') > 0 ? '&' : '?';
					this.openUrl(`${product.licenseUrl}${queryArgChar}lang=${language}`, 'openLicenseUrl');
				} else {
					this.openUrl(product.licenseUrl, 'openLicenseUrl');
				}
			};
		}

		if (product.privacyStatementUrl) {
			this.fallbackMenuHandlers['workbench.action.openPrivacyStatementUrl'] = () => {
				if (language) {
					const queryArgChar = product.licenseUrl.indexOf('?') > 0 ? '&' : '?';
					this.openUrl(`${product.privacyStatementUrl}${queryArgChar}lang=${language}`, 'openPrivacyStatement');
				} else {
					this.openUrl(product.privacyStatementUrl, 'openPrivacyStatement');
				}
			};
		}
	}

156 157
	private registerListeners(): void {
		// Keep flag when app quits
B
Benjamin Pasero 已提交
158
		this.lifecycleService.onWillShutdown(() => this.willShutdown = true);
159 160

		// // Listen to some events from window service to update menu
161
		this.windowsMainService.onWindowsCountChanged(e => this.onWindowsCountChanged(e));
162 163 164 165 166 167 168
	}

	private get currentEnableMenuBarMnemonics(): boolean {
		let enableMenuBarMnemonics = this.configurationService.getValue<boolean>('window.enableMenuBarMnemonics');
		if (typeof enableMenuBarMnemonics !== 'boolean') {
			enableMenuBarMnemonics = true;
		}
169

170 171 172 173
		return enableMenuBarMnemonics;
	}

	private get currentEnableNativeTabs(): boolean {
S
SteVen Batten 已提交
174 175 176 177
		if (!isMacintosh) {
			return false;
		}

178 179 180 181 182 183 184
		let enableNativeTabs = this.configurationService.getValue<boolean>('window.nativeTabs');
		if (typeof enableNativeTabs !== 'boolean') {
			enableNativeTabs = false;
		}
		return enableNativeTabs;
	}

185 186 187
	updateMenu(menubarData: IMenubarData, windowId: number) {
		this.menubarMenus = menubarData.menus;
		this.keybindings = menubarData.keybindings;
S
SteVen Batten 已提交
188

189
		// Save off new menu and keybindings
190
		this.stateService.setItem(Menubar.lastKnownMenubarStorageKey, menubarData);
191

192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
		this.scheduleUpdateMenu();
	}


	private scheduleUpdateMenu(): void {
		this.menuUpdater.schedule(); // buffer multiple attempts to update the menu
	}

	private doUpdateMenu(): void {

		// Due to limitations in Electron, it is not possible to update menu items dynamically. The suggested
		// workaround from Electron is to set the application menu again.
		// See also https://github.com/electron/electron/issues/846
		//
		// Run delayed to prevent updating menu while it is open
B
Benjamin Pasero 已提交
207
		if (!this.willShutdown) {
208
			setTimeout(() => {
B
Benjamin Pasero 已提交
209
				if (!this.willShutdown) {
210 211 212 213 214 215
					this.install();
				}
			}, 10 /* delay this because there is an issue with updating a menu when it is open */);
		}
	}

216 217 218 219 220 221 222
	private onWindowsCountChanged(e: IWindowsCountChangedEvent): void {
		if (!isMacintosh) {
			return;
		}

		// Update menu if window count goes from N > 0 or 0 > N to update menu item enablement
		if ((e.oldCount === 0 && e.newCount > 0) || (e.oldCount > 0 && e.newCount === 0)) {
223
			this.closedLastWindow = e.newCount === 0;
224 225 226
			this.scheduleUpdateMenu();
		}
	}
227 228

	private install(): void {
229 230 231 232 233 234
		// Store old menu in our array to avoid GC to collect the menu and crash. See #55347
		// TODO@sbatten Remove this when fixed upstream by Electron
		const oldMenu = Menu.getApplicationMenu();
		if (oldMenu) {
			this.oldMenus.push(oldMenu);
		}
235

236 237 238 239 240 241 242
		// If we don't have a menu yet, set it to null to avoid the electron menu.
		// This should only happen on the first launch ever
		if (Object.keys(this.menubarMenus).length === 0) {
			Menu.setApplicationMenu(isMacintosh ? new Menu() : null);
			return;
		}

243 244 245 246 247 248 249 250 251
		// Menus
		const menubar = new Menu();

		// Mac: Application
		let macApplicationMenuItem: Electron.MenuItem;
		if (isMacintosh) {
			const applicationMenu = new Menu();
			macApplicationMenuItem = new MenuItem({ label: product.nameShort, submenu: applicationMenu });
			this.setMacApplicationMenu(applicationMenu);
252
			menubar.append(macApplicationMenuItem);
253 254
		}

255
		// Mac: Dock
256 257 258 259 260 261 262 263 264
		if (isMacintosh && !this.appMenuInstalled) {
			this.appMenuInstalled = true;

			const dockMenu = new Menu();
			dockMenu.append(new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window")), click: () => this.windowsMainService.openNewWindow(OpenContext.DOCK) }));

			app.dock.setMenu(dockMenu);
		}

265 266 267 268
		// File
		const fileMenu = new Menu();
		const fileMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File")), submenu: fileMenu });

269 270
		this.setMenuById(fileMenu, 'File');
		menubar.append(fileMenuItem);
271

272 273 274 275
		// Edit
		const editMenu = new Menu();
		const editMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit")), submenu: editMenu });

276 277
		this.setMenuById(editMenu, 'Edit');
		menubar.append(editMenuItem);
278 279 280 281

		// Selection
		const selectionMenu = new Menu();
		const selectionMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection")), submenu: selectionMenu });
282

283 284
		this.setMenuById(selectionMenu, 'Selection');
		menubar.append(selectionMenuItem);
285 286

		// View
287 288 289
		const viewMenu = new Menu();
		const viewMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View")), submenu: viewMenu });

290 291
		this.setMenuById(viewMenu, 'View');
		menubar.append(viewMenuItem);
292

293
		// Go
294 295
		const gotoMenu = new Menu();
		const gotoMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go")), submenu: gotoMenu });
296

297 298
		this.setMenuById(gotoMenu, 'Go');
		menubar.append(gotoMenuItem);
299

300 301 302 303
		// Debug
		const debugMenu = new Menu();
		const debugMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mDebug', comment: ['&& denotes a mnemonic'] }, "&&Debug")), submenu: debugMenu });

304 305
		this.setMenuById(debugMenu, 'Debug');
		menubar.append(debugMenuItem);
306

307 308 309
		// Terminal
		const terminalMenu = new Menu();
		const terminalMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal")), submenu: terminalMenu });
310

311 312
		this.setMenuById(terminalMenu, 'Terminal');
		menubar.append(terminalMenuItem);
313

314
		// Mac: Window
M
Matt Bierner 已提交
315
		let macWindowMenuItem: Electron.MenuItem | undefined;
S
SteVen Batten 已提交
316
		if (this.shouldDrawMenu('Window')) {
317 318 319 320
			const windowMenu = new Menu();
			macWindowMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize('mWindow', "Window")), submenu: windowMenu, role: 'window' });
			this.setMacWindowMenu(windowMenu);
		}
321 322 323 324 325

		if (macWindowMenuItem) {
			menubar.append(macWindowMenuItem);
		}

326 327 328 329
		// Help
		const helpMenu = new Menu();
		const helpMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help")), submenu: helpMenu, role: 'help' });

330 331
		this.setMenuById(helpMenu, 'Help');
		menubar.append(helpMenuItem);
332

S
SteVen Batten 已提交
333 334 335 336 337
		if (menubar.items && menubar.items.length > 0) {
			Menu.setApplicationMenu(menubar);
		} else {
			Menu.setApplicationMenu(null);
		}
338 339 340

		// Dispose of older menus after some time
		this.menuGC.schedule();
341 342 343
	}

	private setMacApplicationMenu(macApplicationMenu: Electron.Menu): void {
344
		const about = this.createMenuItem(nls.localize('mAbout', "About {0}", product.nameLong), 'workbench.action.showAboutDialog');
345
		const checkForUpdates = this.getUpdateMenuItems();
S
SteVen Batten 已提交
346 347 348 349 350 351 352 353

		let preferences;
		if (this.shouldDrawMenu('Preferences')) {
			const preferencesMenu = new Menu();
			this.setMenuById(preferencesMenu, 'Preferences');
			preferences = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miPreferences', comment: ['&& denotes a mnemonic'] }, "&&Preferences")), submenu: preferencesMenu });
		}

354 355 356 357 358 359 360
		const servicesMenu = new Menu();
		const services = new MenuItem({ label: nls.localize('mServices', "Services"), role: 'services', submenu: servicesMenu });
		const hide = new MenuItem({ label: nls.localize('mHide', "Hide {0}", product.nameLong), role: 'hide', accelerator: 'Command+H' });
		const hideOthers = new MenuItem({ label: nls.localize('mHideOthers', "Hide Others"), role: 'hideothers', accelerator: 'Command+Alt+H' });
		const showAll = new MenuItem({ label: nls.localize('mShowAll', "Show All"), role: 'unhide' });
		const quit = new MenuItem(this.likeAction('workbench.action.quit', {
			label: nls.localize('miQuit', "Quit {0}", product.nameLong), click: () => {
B
Benjamin Pasero 已提交
361 362 363
				if (
					this.windowsMainService.getWindowCount() === 0 || 			// allow to quit when no more windows are open
					!!this.windowsMainService.getFocusedWindow() ||				// allow to quit when window has focus (fix for https://github.com/Microsoft/vscode/issues/39191)
M
Matt Bierner 已提交
364
					this.windowsMainService.getLastActiveWindow()!.isMinimized()	// allow to quit when window has no focus but is minimized (https://github.com/Microsoft/vscode/issues/63000)
B
Benjamin Pasero 已提交
365 366
				) {
					this.windowsMainService.quit();
367 368 369 370 371 372
				}
			}
		}));

		const actions = [about];
		actions.push(...checkForUpdates);
S
SteVen Batten 已提交
373 374 375 376 377 378 379 380

		if (preferences) {
			actions.push(...[
				__separator__(),
				preferences
			]);
		}

381 382 383 384 385 386 387 388 389 390 391 392 393 394
		actions.push(...[
			__separator__(),
			services,
			__separator__(),
			hide,
			hideOthers,
			showAll,
			__separator__(),
			quit
		]);

		actions.forEach(i => macApplicationMenu.append(i));
	}

395
	private shouldDrawMenu(menuId: string): boolean {
S
SteVen Batten 已提交
396
		// We need to draw an empty menu to override the electron default
B
Benjamin Pasero 已提交
397
		if (!isMacintosh && getTitleBarStyle(this.configurationService, this.environmentService) === 'custom') {
S
SteVen Batten 已提交
398 399 400
			return false;
		}

401 402 403
		switch (menuId) {
			case 'File':
			case 'Help':
S
SteVen Batten 已提交
404
				if (isMacintosh) {
405 406
					return (this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) || (!!this.menubarMenus && !!this.menubarMenus[menuId]);
				}
S
SteVen Batten 已提交
407

408 409 410
			case 'Window':
				if (isMacintosh) {
					return (this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) || !!this.menubarMenus;
S
SteVen Batten 已提交
411
				}
S
SteVen Batten 已提交
412

413
			default:
414
				return this.windowsMainService.getWindowCount() > 0 && (!!this.menubarMenus && !!this.menubarMenus[menuId]);
415 416 417 418
		}
	}


419 420 421
	private setMenu(menu: Electron.Menu, items: Array<MenubarMenuItem>) {
		items.forEach((item: MenubarMenuItem) => {
			if (isMenubarMenuItemSeparator(item)) {
422
				menu.append(__separator__());
423 424 425 426 427
			} else if (isMenubarMenuItemSubmenu(item)) {
				const submenu = new Menu();
				const submenuItem = new MenuItem({ label: this.mnemonicLabel(item.label), submenu: submenu });
				this.setMenu(submenu, item.submenu.items);
				menu.append(submenuItem);
428
			} else if (isMenubarMenuItemUriAction(item)) {
429
				menu.append(this.createOpenRecentMenuItem(item.uri, item.label, item.id));
430
			} else if (isMenubarMenuItemAction(item)) {
431
				if (item.id === 'workbench.action.showAboutDialog') {
S
SteVen Batten 已提交
432
					this.insertCheckForUpdatesItems(menu);
433 434
				}

435 436 437 438 439 440 441 442 443
				if (isMacintosh) {
					if (this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) {
						// In the fallback scenario, we are either disabled or using a fallback handler
						if (this.fallbackMenuHandlers[item.id]) {
							menu.append(new MenuItem(this.likeAction(item.id, { label: this.mnemonicLabel(item.label), click: this.fallbackMenuHandlers[item.id] })));
						} else {
							menu.append(this.createMenuItem(item.label, item.id, false, item.checked));
						}
					} else {
444
						menu.append(this.createMenuItem(item.label, item.id, item.enabled === false ? false : true, !!item.checked));
445 446
					}
				} else {
447
					menu.append(this.createMenuItem(item.label, item.id, item.enabled === false ? false : true, !!item.checked));
448
				}
449
			}
450 451 452
		});
	}

453
	private setMenuById(menu: Electron.Menu, menuId: string): void {
454
		if (this.menubarMenus && this.menubarMenus[menuId]) {
S
SteVen Batten 已提交
455 456
			this.setMenu(menu, this.menubarMenus[menuId].items);
		}
457 458
	}

S
SteVen Batten 已提交
459 460 461 462 463 464 465 466
	private insertCheckForUpdatesItems(menu: Electron.Menu) {
		const updateItems = this.getUpdateMenuItems();
		if (updateItems.length) {
			updateItems.forEach(i => menu.append(i));
			menu.append(__separator__());
		}
	}

467
	private createOpenRecentMenuItem(uri: URI, label: string, commandId: string): Electron.MenuItem {
468
		const revivedUri = URI.revive(uri);
M
Martin Aeschlimann 已提交
469 470 471
		const uriToOpen: IURIToOpen =
			(commandId === 'openRecentFile') ? { fileUri: revivedUri } :
				(commandId === 'openRecentWorkspace') ? { workspaceUri: revivedUri } : { folderUri: revivedUri };
472 473 474 475 476 477 478 479

		return new MenuItem(this.likeAction(commandId, {
			label,
			click: (menuItem, win, event) => {
				const openInNewWindow = this.isOptionClick(event);
				const success = this.windowsMainService.open({
					context: OpenContext.MENU,
					cli: this.environmentService.args,
M
Martin Aeschlimann 已提交
480 481
					urisToOpen: [uriToOpen],
					forceNewWindow: openInNewWindow
482 483 484
				}).length > 0;

				if (!success) {
485
					this.historyMainService.removeFromRecentlyOpened([revivedUri]);
486 487 488 489 490 491
				}
			}
		}, false));
	}

	private isOptionClick(event: Electron.Event): boolean {
M
Matt Bierner 已提交
492
		return !!(event && ((!isMacintosh && (event.ctrlKey || event.shiftKey)) || (isMacintosh && (event.metaKey || event.altKey))));
493 494
	}

495 496 497 498 499 500 501 502 503 504
	private createRoleMenuItem(label: string, commandId: string, role: any): Electron.MenuItem {
		const options: Electron.MenuItemConstructorOptions = {
			label: this.mnemonicLabel(label),
			role,
			enabled: true
		};

		return new MenuItem(this.withKeybinding(commandId, options));
	}

505 506 507 508 509 510 511 512
	private setMacWindowMenu(macWindowMenu: Electron.Menu): void {
		const minimize = new MenuItem({ label: nls.localize('mMinimize', "Minimize"), role: 'minimize', accelerator: 'Command+M', enabled: this.windowsMainService.getWindowCount() > 0 });
		const zoom = new MenuItem({ label: nls.localize('mZoom', "Zoom"), role: 'zoom', enabled: this.windowsMainService.getWindowCount() > 0 });
		const bringAllToFront = new MenuItem({ label: nls.localize('mBringToFront', "Bring All to Front"), role: 'front', enabled: this.windowsMainService.getWindowCount() > 0 });
		const switchWindow = this.createMenuItem(nls.localize({ key: 'miSwitchWindow', comment: ['&& denotes a mnemonic'] }, "Switch &&Window..."), 'workbench.action.switchWindow');

		const nativeTabMenuItems: Electron.MenuItem[] = [];
		if (this.currentEnableNativeTabs) {
513 514
			nativeTabMenuItems.push(__separator__());

B
Benjamin Pasero 已提交
515
			nativeTabMenuItems.push(this.createMenuItem(nls.localize('mNewTab', "New Tab"), 'workbench.action.newWindowTab'));
516

B
Benjamin Pasero 已提交
517 518 519 520
			nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mShowPreviousTab', "Show Previous Tab"), 'workbench.action.showPreviousWindowTab', 'selectPreviousTab'));
			nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mShowNextTab', "Show Next Tab"), 'workbench.action.showNextWindowTab', 'selectNextTab'));
			nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mMoveTabToNewWindow', "Move Tab to New Window"), 'workbench.action.moveWindowTabToNewWindow', 'moveTabToNewWindow'));
			nativeTabMenuItems.push(this.createRoleMenuItem(nls.localize('mMergeAllWindows', "Merge All Windows"), 'workbench.action.mergeAllWindowTabs', 'mergeAllWindows'));
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541
		}

		[
			minimize,
			zoom,
			switchWindow,
			...nativeTabMenuItems,
			__separator__(),
			bringAllToFront
		].forEach(item => macWindowMenu.append(item));
	}

	private getUpdateMenuItems(): Electron.MenuItem[] {
		const state = this.updateService.state;

		switch (state.type) {
			case StateType.Uninitialized:
				return [];

			case StateType.Idle:
				return [new MenuItem({
S
SteVen Batten 已提交
542
					label: this.mnemonicLabel(nls.localize('miCheckForUpdates', "Check for &&Updates...")), click: () => setTimeout(() => {
543 544 545 546 547 548 549 550 551 552 553 554 555
						this.reportMenuActionTelemetry('CheckForUpdate');

						const focusedWindow = this.windowsMainService.getFocusedWindow();
						const context = focusedWindow ? { windowId: focusedWindow.id } : null;
						this.updateService.checkForUpdates(context);
					}, 0)
				})];

			case StateType.CheckingForUpdates:
				return [new MenuItem({ label: nls.localize('miCheckingForUpdates', "Checking For Updates..."), enabled: false })];

			case StateType.AvailableForDownload:
				return [new MenuItem({
S
SteVen Batten 已提交
556
					label: this.mnemonicLabel(nls.localize('miDownloadUpdate', "D&&ownload Available Update")), click: () => {
557 558 559 560 561 562 563 564 565
						this.updateService.downloadUpdate();
					}
				})];

			case StateType.Downloading:
				return [new MenuItem({ label: nls.localize('miDownloadingUpdate', "Downloading Update..."), enabled: false })];

			case StateType.Downloaded:
				return [new MenuItem({
S
SteVen Batten 已提交
566
					label: this.mnemonicLabel(nls.localize('miInstallUpdate', "Install &&Update...")), click: () => {
567 568 569 570 571 572 573 574 575 576
						this.reportMenuActionTelemetry('InstallUpdate');
						this.updateService.applyUpdate();
					}
				})];

			case StateType.Updating:
				return [new MenuItem({ label: nls.localize('miInstallingUpdate', "Installing Update..."), enabled: false })];

			case StateType.Ready:
				return [new MenuItem({
J
Joao Moreno 已提交
577
					label: this.mnemonicLabel(nls.localize('miRestartToUpdate', "Restart to &&Update")), click: () => {
578 579 580 581 582 583 584
						this.reportMenuActionTelemetry('RestartToUpdate');
						this.updateService.quitAndInstall();
					}
				})];
		}
	}

585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611
	private static _menuItemIsTriggeredViaKeybinding(event: Electron.Event, userSettingsLabel: string): boolean {
		// The event coming in from Electron will inform us only about the modifier keys pressed.
		// The strategy here is to check if the modifier keys match those of the keybinding,
		// since it is highly unlikely to use modifier keys when clicking with the mouse
		if (!userSettingsLabel) {
			// There is no keybinding
			return false;
		}

		let ctrlRequired = /ctrl/.test(userSettingsLabel);
		let shiftRequired = /shift/.test(userSettingsLabel);
		let altRequired = /alt/.test(userSettingsLabel);
		let metaRequired = /cmd/.test(userSettingsLabel) || /super/.test(userSettingsLabel);

		if (!ctrlRequired && !shiftRequired && !altRequired && !metaRequired) {
			// This keybinding does not use any modifier keys, so we cannot use this heuristic
			return false;
		}

		return (
			ctrlRequired === event.ctrlKey
			&& shiftRequired === event.shiftKey
			&& altRequired === event.altKey
			&& metaRequired === event.metaKey
		);
	}

612 613 614 615
	private createMenuItem(label: string, commandId: string | string[], enabled?: boolean, checked?: boolean): Electron.MenuItem;
	private createMenuItem(label: string, click: () => void, enabled?: boolean, checked?: boolean): Electron.MenuItem;
	private createMenuItem(arg1: string, arg2: any, arg3?: boolean, arg4?: boolean): Electron.MenuItem {
		const label = this.mnemonicLabel(arg1);
616
		const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: Electron.MenuItem & IMenuItemWithKeybinding, win: Electron.BrowserWindow, event: Electron.Event) => {
A
Alex Dima 已提交
617
			const userSettingsLabel = menuItem ? menuItem.userSettingsLabel : null;
618 619 620 621 622
			let commandId = arg2;
			if (Array.isArray(arg2)) {
				commandId = this.isOptionClick(event) ? arg2[1] : arg2[0]; // support alternative action if we got multiple action Ids and the option key was pressed while invoking
			}

623
			if (userSettingsLabel && Menubar._menuItemIsTriggeredViaKeybinding(event, userSettingsLabel)) {
624 625 626 627
				this.runActionInRenderer({ type: 'keybinding', userSettingsLabel });
			} else {
				this.runActionInRenderer({ type: 'commandId', commandId });
			}
628 629 630 631 632 633 634 635 636 637 638
		};
		const enabled = typeof arg3 === 'boolean' ? arg3 : this.windowsMainService.getWindowCount() > 0;
		const checked = typeof arg4 === 'boolean' ? arg4 : false;

		const options: Electron.MenuItemConstructorOptions = {
			label,
			click,
			enabled
		};

		if (checked) {
639 640
			options.type = 'checkbox';
			options.checked = checked;
641 642
		}

M
Matt Bierner 已提交
643
		let commandId: string | undefined;
644 645 646 647 648 649
		if (typeof arg2 === 'string') {
			commandId = arg2;
		} else if (Array.isArray(arg2)) {
			commandId = arg2[0];
		}

S
SteVen Batten 已提交
650
		if (isMacintosh) {
651

652
			// Add role for special case menu items
S
SteVen Batten 已提交
653
			if (commandId === 'editor.action.clipboardCutAction') {
654
				options.role = 'cut';
S
SteVen Batten 已提交
655
			} else if (commandId === 'editor.action.clipboardCopyAction') {
656
				options.role = 'copy';
S
SteVen Batten 已提交
657
			} else if (commandId === 'editor.action.clipboardPasteAction') {
658
				options.role = 'paste';
S
SteVen Batten 已提交
659
			}
660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677

			// Add context aware click handlers for special case menu items
			if (commandId === 'undo') {
				options.click = this.makeContextAwareClickHandler(click, {
					inDevTools: devTools => devTools.undo(),
					inNoWindow: () => Menu.sendActionToFirstResponder('undo:')
				});
			} else if (commandId === 'redo') {
				options.click = this.makeContextAwareClickHandler(click, {
					inDevTools: devTools => devTools.redo(),
					inNoWindow: () => Menu.sendActionToFirstResponder('redo:')
				});
			} else if (commandId === 'editor.action.selectAll') {
				options.click = this.makeContextAwareClickHandler(click, {
					inDevTools: devTools => devTools.selectAll(),
					inNoWindow: () => Menu.sendActionToFirstResponder('selectAll:')
				});
			}
S
SteVen Batten 已提交
678 679
		}

680 681 682
		return new MenuItem(this.withKeybinding(commandId, options));
	}

683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700
	private makeContextAwareClickHandler(click: () => void, contextSpecificHandlers: IMenuItemClickHandler): () => void {
		return () => {
			// No Active Window
			const activeWindow = this.windowsMainService.getFocusedWindow();
			if (!activeWindow) {
				return contextSpecificHandlers.inNoWindow();
			}

			// DevTools focused
			if (activeWindow.win.webContents.isDevToolsFocused()) {
				return contextSpecificHandlers.inDevTools(activeWindow.win.webContents.devToolsWebContents);
			}

			// Finally execute command in Window
			click();
		};
	}

701
	private runActionInRenderer(invocation: IMenuItemInvocation): void {
702 703 704
		// We make sure to not run actions when the window has no focus, this helps
		// for https://github.com/Microsoft/vscode/issues/25907 and specifically for
		// https://github.com/Microsoft/vscode/issues/11928
B
Benjamin Pasero 已提交
705 706 707 708 709
		// Still allow to run when the last active window is minimized though for
		// https://github.com/Microsoft/vscode/issues/63000
		let activeWindow = this.windowsMainService.getFocusedWindow();
		if (!activeWindow) {
			const lastActiveWindow = this.windowsMainService.getLastActiveWindow();
M
Matt Bierner 已提交
710
			if (lastActiveWindow && lastActiveWindow.isMinimized()) {
B
Benjamin Pasero 已提交
711 712 713 714
				activeWindow = lastActiveWindow;
			}
		}

715
		if (activeWindow) {
B
Benjamin Pasero 已提交
716 717
			if (isMacintosh && !this.environmentService.isBuilt && !activeWindow.isReady) {
				if ((invocation.type === 'commandId' && invocation.commandId === 'workbench.action.toggleDevTools') || (invocation.type !== 'commandId' && invocation.userSettingsLabel === 'alt+cmd+i')) {
718 719 720 721 722
					// prevent this action from running twice on macOS (https://github.com/Microsoft/vscode/issues/62719)
					// we already register a keybinding in bootstrap-window.js for opening developer tools in case something
					// goes wrong and that keybinding is only removed when the application has loaded (= window ready).
					return;
				}
B
Benjamin Pasero 已提交
723
			}
B
Benjamin Pasero 已提交
724

B
Benjamin Pasero 已提交
725 726 727
			if (invocation.type === 'commandId') {
				this.windowsMainService.sendToFocused('vscode:runAction', { id: invocation.commandId, from: 'menu' } as IRunActionInWindowRequest);
			} else {
728
				this.windowsMainService.sendToFocused('vscode:runKeybinding', { userSettingsLabel: invocation.userSettingsLabel } as IRunKeybindingInWindowRequest);
B
Benjamin Pasero 已提交
729
			}
730 731 732
		}
	}

733
	private withKeybinding(commandId: string | undefined, options: Electron.MenuItemConstructorOptions & IMenuItemWithKeybinding): Electron.MenuItemConstructorOptions {
M
Matt Bierner 已提交
734
		const binding = typeof commandId === 'string' ? this.keybindings[commandId] : undefined;
735 736 737 738 739

		// Apply binding if there is one
		if (binding && binding.label) {

			// if the binding is native, we can just apply it
740
			if (binding.isNative !== false) {
741
				options.accelerator = binding.label;
742
				options.userSettingsLabel = binding.userSettingsLabel;
743 744 745 746
			}

			// the keybinding is not native so we cannot show it as part of the accelerator of
			// the menu item. we fallback to a different strategy so that we always display it
M
Matt Bierner 已提交
747
			else if (typeof options.label === 'string') {
748 749 750 751 752 753 754 755 756 757 758
				const bindingIndex = options.label.indexOf('[');
				if (bindingIndex >= 0) {
					options.label = `${options.label.substr(0, bindingIndex)} [${binding.label}]`;
				} else {
					options.label = `${options.label} [${binding.label}]`;
				}
			}
		}

		// Unset bindings if there is none
		else {
R
Rob Lourens 已提交
759
			options.accelerator = undefined;
760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786
		}

		return options;
	}

	private likeAction(commandId: string, options: Electron.MenuItemConstructorOptions, setAccelerator = !options.accelerator): Electron.MenuItemConstructorOptions {
		if (setAccelerator) {
			options = this.withKeybinding(commandId, options);
		}

		const originalClick = options.click;
		options.click = (item, window, event) => {
			this.reportMenuActionTelemetry(commandId);
			if (originalClick) {
				originalClick(item, window, event);
			}
		};

		return options;
	}

	private openUrl(url: string, id: string): void {
		shell.openExternal(url);
		this.reportMenuActionTelemetry(id);
	}

	private reportMenuActionTelemetry(id: string): void {
787
		this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id, from: telemetryFrom });
788 789 790 791 792 793 794 795 796 797
	}

	private mnemonicLabel(label: string): string {
		return baseMnemonicLabel(label, !this.currentEnableMenuBarMnemonics);
	}
}

function __separator__(): Electron.MenuItem {
	return new MenuItem({ type: 'separator' });
}