diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index 83e23225d3105..a6d79bdbcb0a7 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -43,6 +43,8 @@ export class BaseActionItem implements IActionItem { public _context: any; public _action: IAction; + static MNEMONIC_REGEX: RegExp = /&&(.)/g; + private _actionRunner: IActionRunner; constructor(context: any, action: IAction, protected options?: IBaseActionItemOptions) { @@ -167,12 +169,14 @@ export class BaseActionItem implements IActionItem { public focus(): void { if (this.builder) { this.builder.domFocus(); + this.builder.addClass('focused'); } } public blur(): void { if (this.builder) { this.builder.domBlur(); + this.builder.removeClass('focused'); } } @@ -273,7 +277,11 @@ export class ActionItem extends BaseActionItem { public _updateLabel(): void { if (this.options.label) { - this.$e.text(this.getAction().label); + let label = this.getAction().label; + if (label && this.options.isMenu) { + label = label.replace(BaseActionItem.MNEMONIC_REGEX, '$1\u0332'); + } + this.$e.text(label); } } @@ -372,7 +380,6 @@ export class ActionBar implements IActionRunner { // Items public items: IActionItem[]; - private focusedItem: number; private focusTracker: DOM.IFocusTracker; @@ -487,7 +494,7 @@ export class ActionBar implements IActionRunner { this.actionsList = document.createElement('ul'); this.actionsList.className = 'actions-container'; if (this.options.isMenu) { - this.actionsList.setAttribute('role', 'menubar'); + this.actionsList.setAttribute('role', 'menu'); } else { this.actionsList.setAttribute('role', 'toolbar'); } @@ -558,6 +565,15 @@ export class ActionBar implements IActionRunner { return this.domNode; } + private _addMnemonic(action: IAction, actionItemElement: HTMLElement): void { + let matches = BaseActionItem.MNEMONIC_REGEX.exec(action.label); + if (matches && matches.length === 2) { + let mnemonic = matches[1]; + + actionItemElement.accessKey = mnemonic.toLocaleLowerCase(); + } + } + public push(arg: IAction | IAction[], options: IActionOptions = {}): void { const actions: IAction[] = !Array.isArray(arg) ? [arg] : arg; @@ -575,6 +591,10 @@ export class ActionBar implements IActionRunner { e.stopPropagation(); }); + if (options.isMenu) { + this._addMnemonic(action, actionItemElement); + } + let item: IActionItem = null; if (this.options.actionItemProvider) { @@ -808,4 +828,4 @@ export class SelectActionItem extends BaseActionItem { super.dispose(); } -} +} \ No newline at end of file diff --git a/src/vs/base/browser/ui/menu/menu.css b/src/vs/base/browser/ui/menu/menu.css index a7ea384d8882f..554b91d60de0b 100644 --- a/src/vs/base/browser/ui/menu/menu.css +++ b/src/vs/base/browser/ui/menu/menu.css @@ -87,6 +87,10 @@ color: inherit; } +.monaco-menu .monaco-action-bar.vertical .action-label.checked:after { + content: ' \2713'; +} + /* Context Menu */ .context-view.monaco-menu-container { diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index 16c9a2a69a783..512d107f15684 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -18,6 +18,7 @@ export interface IMenuOptions { actionItemProvider?: IActionItemProvider; actionRunner?: IActionRunner; getKeyBinding?: (action: IAction) => ResolvedKeybinding; + ariaLabel?: string; } export class Menu { @@ -27,9 +28,11 @@ export class Menu { constructor(container: HTMLElement, actions: IAction[], options: IMenuOptions = {}) { addClass(container, 'monaco-menu-container'); + container.setAttribute('role', 'presentation'); let menuContainer = document.createElement('div'); addClass(menuContainer, 'monaco-menu'); + menuContainer.setAttribute('role', 'presentation'); container.appendChild(menuContainer); this.actionBar = new ActionBar(menuContainer, { @@ -37,10 +40,11 @@ export class Menu { actionItemProvider: options.actionItemProvider, context: options.context, actionRunner: options.actionRunner, - isMenu: true + isMenu: true, + ariaLabel: options.ariaLabel }); - this.actionBar.push(actions, { icon: true, label: true }); + this.actionBar.push(actions, { icon: true, label: true, isMenu: true }); } public get onDidCancel(): Event { diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index ec6d2f65d5cf5..964e1ed48981a 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -12,7 +12,6 @@ import { IWindowsService, OpenContext, ActiveWindowManager } from 'vs/platform/w import { WindowsChannel } from 'vs/platform/windows/common/windowsIpc'; import { WindowsService } from 'vs/platform/windows/electron-main/windowsService'; import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; -import { CodeMenu } from 'vs/code/electron-main/menus'; import { getShellEnvironment } from 'vs/code/node/shellEnv'; import { IUpdateService } from 'vs/platform/update/common/update'; import { UpdateChannel } from 'vs/platform/update/common/updateIpc'; @@ -61,6 +60,11 @@ import { LogLevelSetterChannel } from 'vs/platform/log/common/logIpc'; import { setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { ElectronURLListener } from 'vs/platform/url/electron-main/electronUrlListener'; import { serve as serveDriver } from 'vs/platform/driver/electron-main/driver'; +import { IMenubarService } from 'vs/platform/menubar/common/menubar'; +import { MenubarService } from 'vs/platform/menubar/electron-main/menubarService'; +import { MenubarChannel } from 'vs/platform/menubar/common/menubarIpc'; +// TODO@sbatten: Remove after conversion to new dynamic menubar +import { CodeMenu } from 'vs/code/electron-main/menus'; export class CodeApplication { @@ -340,6 +344,7 @@ export class CodeApplication { services.set(IWindowsService, new SyncDescriptor(WindowsService, this.sharedProcess)); services.set(ILaunchService, new SyncDescriptor(LaunchService)); services.set(IIssueService, new SyncDescriptor(IssueService, machineId, this.userEnv)); + services.set(IMenubarService, new SyncDescriptor(MenubarService)); // Telemtry if (this.environmentService.isBuilt && !this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { @@ -383,6 +388,10 @@ export class CodeApplication { this.electronIpcServer.registerChannel('windows', windowsChannel); this.sharedProcessClient.done(client => client.registerChannel('windows', windowsChannel)); + const menubarService = accessor.get(IMenubarService); + const menubarChannel = new MenubarChannel(menubarService); + this.electronIpcServer.registerChannel('menubar', menubarChannel); + const urlService = accessor.get(IURLService); const urlChannel = new URLServiceChannel(urlService); this.electronIpcServer.registerChannel('url', urlChannel); @@ -447,7 +456,6 @@ export class CodeApplication { } private afterWindowOpen(accessor: ServicesAccessor): void { - const appInstantiationService = accessor.get(IInstantiationService); const windowsMainService = accessor.get(IWindowsMainService); let windowsMutex: Mutex = null; @@ -487,8 +495,13 @@ export class CodeApplication { } } + // TODO@sbatten: Remove when menu is converted // Install Menu - appInstantiationService.createInstance(CodeMenu); + const instantiationService = accessor.get(IInstantiationService); + const configurationService = accessor.get(IConfigurationService); + if (platform.isMacintosh || configurationService.getValue('window.titleBarStyle') !== 'custom') { + instantiationService.createInstance(CodeMenu); + } // Jump List this.historyMainService.updateWindowsJumpList(); diff --git a/src/vs/code/electron-main/menubar.ts b/src/vs/code/electron-main/menubar.ts new file mode 100644 index 0000000000000..5870ea0ad3169 --- /dev/null +++ b/src/vs/code/electron-main/menubar.ts @@ -0,0 +1,745 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as nls from 'vs/nls'; +import { isMacintosh, language } from 'vs/base/common/platform'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { app, shell, Menu, MenuItem, BrowserWindow } from 'electron'; +import { OpenContext, IRunActionInWindowRequest } from 'vs/platform/windows/common/windows'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IUpdateService, StateType } from 'vs/platform/update/common/update'; +import product from 'vs/platform/node/product'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { mnemonicMenuLabel as baseMnemonicLabel, unmnemonicLabel, getPathLabel } from 'vs/base/common/labels'; +import { KeybindingsResolver } from 'vs/code/electron-main/keyboard'; +import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows'; +import { IHistoryMainService } from 'vs/platform/history/common/history'; +import { IWorkspaceIdentifier, getWorkspaceLabel, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IMenubarData, IMenubarMenuItemAction, IMenubarMenuItemSeparator } from 'vs/platform/menubar/common/menubar'; + +// interface IExtensionViewlet { +// id: string; +// label: string; +// } + +const telemetryFrom = 'menu'; + +export class Menubar { + + private static readonly MAX_MENU_RECENT_ENTRIES = 10; + + // private keys = [ + // 'files.autoSave', + // 'editor.multiCursorModifier', + // 'workbench.sideBar.location', + // 'workbench.statusBar.visible', + // 'workbench.activityBar.visible', + // 'window.enableMenuBarMnemonics', + // 'window.nativeTabs' + // ]; + + private isQuitting: boolean; + private appMenuInstalled: boolean; + + private menuUpdater: RunOnceScheduler; + + private keybindingsResolver: KeybindingsResolver; + + // private extensionViewlets: IExtensionViewlet[]; + + private nativeTabMenuItems: Electron.MenuItem[]; + + private menubarMenus: IMenubarData = {}; + + constructor( + @IUpdateService private updateService: IUpdateService, + @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService private configurationService: IConfigurationService, + @IWindowsMainService private windowsMainService: IWindowsMainService, + @IEnvironmentService private environmentService: IEnvironmentService, + @ITelemetryService private telemetryService: ITelemetryService, + @IHistoryMainService private historyMainService: IHistoryMainService + ) { + // this.extensionViewlets = []; + // this.nativeTabMenuItems = []; + + this.menuUpdater = new RunOnceScheduler(() => this.doUpdateMenu(), 0); + this.keybindingsResolver = instantiationService.createInstance(KeybindingsResolver); + + this.install(); + + this.registerListeners(); + } + + private registerListeners(): void { + + // Keep flag when app quits + app.on('will-quit', () => { + this.isQuitting = true; + }); + + // // Listen to some events from window service to update menu + // this.historyMainService.onRecentlyOpenedChange(() => this.updateMenu()); + this.windowsMainService.onWindowsCountChanged(e => this.onWindowsCountChanged(e)); + // this.windowsMainService.onActiveWindowChanged(() => this.updateWorkspaceMenuItems()); + // this.windowsMainService.onWindowReady(() => this.updateWorkspaceMenuItems()); + // this.windowsMainService.onWindowClose(() => this.updateWorkspaceMenuItems()); + + // Listen to extension viewlets + // ipc.on('vscode:extensionViewlets', (event: any, rawExtensionViewlets: string) => { + // let extensionViewlets: IExtensionViewlet[] = []; + // try { + // extensionViewlets = JSON.parse(rawExtensionViewlets); + // } catch (error) { + // // Should not happen + // } + + // if (extensionViewlets.length) { + // this.extensionViewlets = extensionViewlets; + // this.updateMenu(); + // } + // }); + + // Update when auto save config changes + // this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e)); + + // Listen to update service + // this.updateService.onStateChange(() => this.updateMenu()); + + // Listen to keybindings change + this.keybindingsResolver.onKeybindingsChanged(() => this.scheduleUpdateMenu()); + } + + private get currentEnableMenuBarMnemonics(): boolean { + let enableMenuBarMnemonics = this.configurationService.getValue('window.enableMenuBarMnemonics'); + if (typeof enableMenuBarMnemonics !== 'boolean') { + enableMenuBarMnemonics = true; + } + + return enableMenuBarMnemonics; + } + + private get currentEnableNativeTabs(): boolean { + let enableNativeTabs = this.configurationService.getValue('window.nativeTabs'); + if (typeof enableNativeTabs !== 'boolean') { + enableNativeTabs = false; + } + return enableNativeTabs; + } + + updateMenu(menus: IMenubarData, windowId: number) { + this.menubarMenus = menus; + 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 + if (!this.isQuitting) { + setTimeout(() => { + if (!this.isQuitting) { + this.install(); + } + }, 10 /* delay this because there is an issue with updating a menu when it is open */); + } + } + + 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)) { + this.scheduleUpdateMenu(); + } + + // Update specific items that are dependent on window count + else if (this.currentEnableNativeTabs) { + this.nativeTabMenuItems.forEach(item => { + if (item) { + item.enabled = e.newCount > 1; + } + }); + } + } + + private install(): void { + + // 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); + menubar.append(macApplicationMenuItem); + } + + // Mac: Dock + 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); + } + + // File + const fileMenu = new Menu(); + const fileMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File")), submenu: fileMenu }); + + if (this.shouldDrawMenu('File')) { + if (this.shouldFallback('File')) { + this.setFallbackMenuById(fileMenu, 'File'); + } else { + this.setMenuById(fileMenu, 'File'); + } + + menubar.append(fileMenuItem); + } + + + // Edit + const editMenu = new Menu(); + const editMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit")), submenu: editMenu }); + + if (this.shouldDrawMenu('Edit')) { + this.setMenuById(editMenu, 'Edit'); + menubar.append(editMenuItem); + } + + // Recent + const recentMenu = new Menu(); + const recentMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miRecent', comment: ['&& denotes a mnemonic'] }, "&&Recent")), submenu: recentMenu, enabled: recentMenu.items.length > 0 }); + if (this.shouldDrawMenu('Recent')) { + if (this.shouldFallback('Recent')) { + this.setFallbackMenuById(recentMenu, 'Recent'); + } else { + this.setMenuById(recentMenu, 'Recent'); + } + + menubar.append(recentMenuItem); + } + + // Selection + const selectionMenu = new Menu(); + const selectionMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection")), submenu: selectionMenu }); + + if (this.shouldDrawMenu('Selection')) { + this.setMenuById(selectionMenu, 'Selection'); + menubar.append(selectionMenuItem); + } + + // View + const viewMenu = new Menu(); + const viewMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View")), submenu: viewMenu }); + + if (this.shouldDrawMenu('View')) { + this.setMenuById(viewMenu, 'View'); + menubar.append(viewMenuItem); + } + + // Layout + const layoutMenu = new Menu(); + const layoutMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mLayout', comment: ['&& denotes a mnemonic'] }, "&&Layout")), submenu: layoutMenu }); + + if (this.shouldDrawMenu('Layout')) { + this.setMenuById(layoutMenu, 'Layout'); + menubar.append(layoutMenuItem); + } + + // Go + const gotoMenu = new Menu(); + const gotoMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go")), submenu: gotoMenu }); + + if (this.shouldDrawMenu('Go')) { + this.setMenuById(gotoMenu, 'Go'); + menubar.append(gotoMenuItem); + } + + // Debug + const debugMenu = new Menu(); + const debugMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mDebug', comment: ['&& denotes a mnemonic'] }, "&&Debug")), submenu: debugMenu }); + + if (this.shouldDrawMenu('Debug')) { + this.setMenuById(debugMenu, 'Debug'); + menubar.append(debugMenuItem); + } + + // Tasks + const taskMenu = new Menu(); + const taskMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mTask', comment: ['&& denotes a mnemonic'] }, "&&Tasks")), submenu: taskMenu }); + + if (this.shouldDrawMenu('Task')) { + this.setMenuById(taskMenu, 'Task'); + menubar.append(taskMenuItem); + } + + // Mac: Window + let macWindowMenuItem: Electron.MenuItem; + if (isMacintosh) { + const windowMenu = new Menu(); + macWindowMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize('mWindow', "Window")), submenu: windowMenu, role: 'window' }); + this.setMacWindowMenu(windowMenu); + } + + if (macWindowMenuItem) { + menubar.append(macWindowMenuItem); + } + + // Preferences + const preferencesMenu = new Menu(); + const preferencesMenuItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'mPreferences', comment: ['&& denotes a mnemonic'] }, "&&Preferences")), submenu: preferencesMenu }); + + if (this.shouldDrawMenu('Preferences')) { + if (this.shouldFallback('Preferences')) { + this.setFallbackMenuById(preferencesMenu, 'Preferences'); + } else { + this.setMenuById(preferencesMenu, 'Preferences'); + } + menubar.append(preferencesMenuItem); + } + + // 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' }); + + if (this.shouldDrawMenu('Help')) { + if (this.shouldFallback('Help')) { + this.setFallbackMenuById(helpMenu, 'Help'); + } else { + this.setMenuById(helpMenu, 'Help'); + } + menubar.append(helpMenuItem); + } + + Menu.setApplicationMenu(menubar); + } + + private setMacApplicationMenu(macApplicationMenu: Electron.Menu): void { + const about = new MenuItem({ label: nls.localize('mAbout', "About {0}", product.nameLong), role: 'about' }); + const checkForUpdates = this.getUpdateMenuItems(); + 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: () => { + if (this.windowsMainService.getWindowCount() === 0 || !!BrowserWindow.getFocusedWindow()) { + this.windowsMainService.quit(); // fix for https://github.com/Microsoft/vscode/issues/39191 + } + } + })); + + const actions = [about]; + actions.push(...checkForUpdates); + actions.push(...[ + __separator__(), + services, + __separator__(), + hide, + hideOthers, + showAll, + __separator__(), + quit + ]); + + actions.forEach(i => macApplicationMenu.append(i)); + } + + private shouldDrawMenu(menuId: string): boolean { + switch (menuId) { + case 'File': + case 'Recent': + case 'Help': + return true; + default: + return this.windowsMainService.getWindowCount() > 0 && !!this.menubarMenus[menuId]; + } + } + + private shouldFallback(menuId: string): boolean { + return this.shouldDrawMenu(menuId) && (this.windowsMainService.getWindowCount() === 0 || !this.menubarMenus[menuId]); + } + + private setFallbackMenuById(menu: Electron.Menu, menuId: string): void { + switch (menuId) { + case 'File': + const newFile = new MenuItem(this.likeAction('workbench.action.files.newUntitledFile', { label: this.mnemonicLabel(nls.localize({ key: 'miNewFile', comment: ['&& denotes a mnemonic'] }, "&&New File")), click: () => this.windowsMainService.openNewWindow(OpenContext.MENU) })); + + const newWindow = new MenuItem(this.likeAction('workbench.action.newWindow', { label: this.mnemonicLabel(nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window")), click: () => this.windowsMainService.openNewWindow(OpenContext.MENU) })); + + const open = new MenuItem(this.likeAction('workbench.action.files.openFileFolder', { label: this.mnemonicLabel(nls.localize({ key: 'miOpen', comment: ['&& denotes a mnemonic'] }, "&&Open...")), click: (menuItem, win, event) => this.windowsMainService.pickFileFolderAndOpen({ forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } }) })); + + const openWorkspace = new MenuItem(this.likeAction('workbench.action.openWorkspace', { label: this.mnemonicLabel(nls.localize({ key: 'miOpenWorkspace', comment: ['&& denotes a mnemonic'] }, "Open Wor&&kspace...")), click: (menuItem, win, event) => this.windowsMainService.pickWorkspaceAndOpen({ forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } }) })); + + menu.append(newFile); + menu.append(newWindow); + menu.append(__separator__()); + menu.append(open); + menu.append(openWorkspace); + + break; + + case 'Recent': + menu.append(this.createMenuItem(nls.localize({ key: 'miReopenClosedEditor', comment: ['&& denotes a mnemonic'] }, "&&Reopen Closed Editor"), 'workbench.action.reopenClosedEditor')); + + const { workspaces, files } = this.historyMainService.getRecentlyOpened(); + + // Workspaces + if (workspaces.length > 0) { + menu.append(__separator__()); + + for (let i = 0; i < Menubar.MAX_MENU_RECENT_ENTRIES && i < workspaces.length; i++) { + menu.append(this.createOpenRecentMenuItem(workspaces[i], 'openRecentWorkspace', false)); + } + } + + // Files + if (files.length > 0) { + menu.append(__separator__()); + + for (let i = 0; i < Menubar.MAX_MENU_RECENT_ENTRIES && i < files.length; i++) { + menu.append(this.createOpenRecentMenuItem(files[i], 'openRecentFile', true)); + } + } + + if (workspaces.length || files.length) { + menu.append(__separator__()); + menu.append(this.createMenuItem(nls.localize({ key: 'miMore', comment: ['&& denotes a mnemonic'] }, "&&More..."), 'workbench.action.openRecent')); + menu.append(__separator__()); + menu.append(new MenuItem(this.likeAction('workbench.action.clearRecentFiles', { label: this.mnemonicLabel(nls.localize({ key: 'miClearRecentOpen', comment: ['&& denotes a mnemonic'] }, "&&Clear Recently Opened")), click: () => this.historyMainService.clearRecentlyOpened() }))); + } + + break; + + case 'Help': + let twitterItem: MenuItem; + if (product.twitterUrl) { + twitterItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miTwitter', comment: ['&& denotes a mnemonic'] }, "&&Join us on Twitter")), click: () => this.openUrl(product.twitterUrl, 'openTwitterUrl') }); + } + + let featureRequestsItem: MenuItem; + if (product.requestFeatureUrl) { + featureRequestsItem = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miUserVoice', comment: ['&& denotes a mnemonic'] }, "&&Search Feature Requests")), click: () => this.openUrl(product.requestFeatureUrl, 'openUserVoiceUrl') }); + } + + let reportIssuesItem: MenuItem; + if (product.reportIssueUrl) { + const label = nls.localize({ key: 'miReportIssue', comment: ['&& denotes a mnemonic', 'Translate this to "Report Issue in English" in all languages please!'] }, "Report &&Issue"); + + reportIssuesItem = new MenuItem({ label: this.mnemonicLabel(label), click: () => this.openUrl(product.reportIssueUrl, 'openReportIssues') }); + } + + let licenseItem: MenuItem; + if (product.privacyStatementUrl) { + licenseItem = new MenuItem({ + label: this.mnemonicLabel(nls.localize({ key: 'miLicense', comment: ['&& denotes a mnemonic'] }, "View &&License")), click: () => { + if (language) { + const queryArgChar = product.licenseUrl.indexOf('?') > 0 ? '&' : '?'; + this.openUrl(`${product.licenseUrl}${queryArgChar}lang=${language}`, 'openLicenseUrl'); + } else { + this.openUrl(product.licenseUrl, 'openLicenseUrl'); + } + } + }); + } + + let privacyStatementItem: MenuItem; + if (product.privacyStatementUrl) { + privacyStatementItem = new MenuItem({ + label: this.mnemonicLabel(nls.localize({ key: 'miPrivacyStatement', comment: ['&& denotes a mnemonic'] }, "&&Privacy Statement")), click: () => { + if (language) { + const queryArgChar = product.licenseUrl.indexOf('?') > 0 ? '&' : '?'; + this.openUrl(`${product.privacyStatementUrl}${queryArgChar}lang=${language}`, 'openPrivacyStatement'); + } else { + this.openUrl(product.privacyStatementUrl, 'openPrivacyStatement'); + } + } + }); + } + + const openProcessExplorer = new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miOpenProcessExplorerer', comment: ['&& denotes a mnemonic'] }, "Open &&Process Explorer")), click: () => this.runActionInRenderer('workbench.action.openProcessExplorer') }); + + if (twitterItem) { menu.append(twitterItem); } + if (featureRequestsItem) { menu.append(featureRequestsItem); } + if (reportIssuesItem) { menu.append(reportIssuesItem); } + if (twitterItem || featureRequestsItem || reportIssuesItem) { menu.append(__separator__()); } + if (licenseItem) { menu.append(licenseItem); } + if (privacyStatementItem) { menu.append(privacyStatementItem); } + if (licenseItem || privacyStatementItem) { menu.append(__separator__()); } + menu.append(openProcessExplorer); + + break; + } + } + + private setMenuById(menu: Electron.Menu, menuId: string): void { + console.log(`Attempting to set menu for ${menuId}`); + + // Build dynamic menu + this.menubarMenus[menuId].items.forEach((item: IMenubarMenuItemAction | IMenubarMenuItemSeparator) => { + if (item.id === 'vscode.menubar.separator') { + menu.append(__separator__()); + } else { + let menuItem: Electron.MenuItem; + let action: IMenubarMenuItemAction = item; + menuItem = this.createMenuItem(action.label, action.id, action.enabled, action.checked); + menu.append(menuItem); + } + }); + } + + private createOpenRecentMenuItem(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | string, commandId: string, isFile: boolean): Electron.MenuItem { + let label: string; + let path: string; + if (isSingleFolderWorkspaceIdentifier(workspace) || typeof workspace === 'string') { + label = unmnemonicLabel(getPathLabel(workspace, this.environmentService, null)); + path = workspace; + } else { + label = getWorkspaceLabel(workspace, this.environmentService, { verbose: true }); + path = workspace.configPath; + } + + 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, + pathsToOpen: [path], forceNewWindow: openInNewWindow, + forceOpenWorkspaceAsFile: isFile + }).length > 0; + + if (!success) { + this.historyMainService.removeFromRecentlyOpened([isSingleFolderWorkspaceIdentifier(workspace) ? workspace : workspace.configPath]); + } + } + }, false)); + } + + private isOptionClick(event: Electron.Event): boolean { + return event && ((!isMacintosh && (event.ctrlKey || event.shiftKey)) || (isMacintosh && (event.metaKey || event.altKey))); + } + + 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'); + + this.nativeTabMenuItems = []; + const nativeTabMenuItems: Electron.MenuItem[] = []; + if (this.currentEnableNativeTabs) { + const hasMultipleWindows = this.windowsMainService.getWindowCount() > 1; + + this.nativeTabMenuItems.push(this.createMenuItem(nls.localize('mShowPreviousTab', "Show Previous Tab"), 'workbench.action.showPreviousWindowTab', hasMultipleWindows)); + this.nativeTabMenuItems.push(this.createMenuItem(nls.localize('mShowNextTab', "Show Next Tab"), 'workbench.action.showNextWindowTab', hasMultipleWindows)); + this.nativeTabMenuItems.push(this.createMenuItem(nls.localize('mMoveTabToNewWindow', "Move Tab to New Window"), 'workbench.action.moveWindowTabToNewWindow', hasMultipleWindows)); + this.nativeTabMenuItems.push(this.createMenuItem(nls.localize('mMergeAllWindows', "Merge All Windows"), 'workbench.action.mergeAllWindowTabs', hasMultipleWindows)); + + nativeTabMenuItems.push(__separator__(), ...this.nativeTabMenuItems); + } else { + this.nativeTabMenuItems = []; + } + + [ + 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({ + label: nls.localize('miCheckForUpdates', "Check for Updates..."), click: () => setTimeout(() => { + 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({ + label: nls.localize('miDownloadUpdate', "Download Available Update"), click: () => { + this.updateService.downloadUpdate(); + } + })]; + + case StateType.Downloading: + return [new MenuItem({ label: nls.localize('miDownloadingUpdate', "Downloading Update..."), enabled: false })]; + + case StateType.Downloaded: + return [new MenuItem({ + label: nls.localize('miInstallUpdate', "Install Update..."), click: () => { + 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({ + label: nls.localize('miRestartToUpdate', "Restart to Update..."), click: () => { + this.reportMenuActionTelemetry('RestartToUpdate'); + this.updateService.quitAndInstall(); + } + })]; + } + } + + 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); + const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: Electron.MenuItem, win: Electron.BrowserWindow, event: Electron.Event) => { + 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 + } + + this.runActionInRenderer(commandId); + }; + 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) { + options['type'] = 'checkbox'; + options['checked'] = checked; + } + + let commandId: string; + if (typeof arg2 === 'string') { + commandId = arg2; + } else if (Array.isArray(arg2)) { + commandId = arg2[0]; + } + + return new MenuItem(this.withKeybinding(commandId, options)); + } + + private runActionInRenderer(id: string): void { + // 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 + const activeWindow = this.windowsMainService.getFocusedWindow(); + if (activeWindow) { + this.windowsMainService.sendToFocused('vscode:runAction', { id, from: 'menu' } as IRunActionInWindowRequest); + } + } + + private withKeybinding(commandId: string, options: Electron.MenuItemConstructorOptions): Electron.MenuItemConstructorOptions { + const binding = this.keybindingsResolver.getKeybinding(commandId); + + // Apply binding if there is one + if (binding && binding.label) { + + // if the binding is native, we can just apply it + if (binding.isNative) { + options.accelerator = binding.label; + } + + // 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 + else { + 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 { + options.accelerator = void 0; + } + + 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 { + /* __GDPR__ + "workbenchActionExecuted" : { + "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('workbenchActionExecuted', { id, from: telemetryFrom }); + } + + private mnemonicLabel(label: string): string { + return baseMnemonicLabel(label, !this.currentEnableMenuBarMnemonics); + } +} + +function __separator__(): Electron.MenuItem { + return new MenuItem({ type: 'separator' }); +} diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 9b5e91bd553f9..85abd41d9726e 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -166,11 +166,15 @@ export class CodeWindow implements ICodeWindow { } let useCustomTitleStyle = false; - if (isMacintosh && (!windowConfig || !windowConfig.titleBarStyle || windowConfig.titleBarStyle === 'custom')) { + if (isMacintosh) { + useCustomTitleStyle = !windowConfig || !windowConfig.titleBarStyle || windowConfig.titleBarStyle === 'custom'; // Default to custom on macOS + const isDev = !this.environmentService.isBuilt || !!config.extensionDevelopmentPath; - if (!isDev) { - useCustomTitleStyle = true; // not enabled when developing due to https://github.com/electron/electron/issues/3647 + if (isDev) { + useCustomTitleStyle = false; // not enabled when developing due to https://github.com/electron/electron/issues/3647 } + } else { + useCustomTitleStyle = windowConfig && windowConfig.titleBarStyle === 'custom'; // Must be specified on Windows/Linux } if (useNativeTabs) { @@ -180,6 +184,9 @@ export class CodeWindow implements ICodeWindow { if (useCustomTitleStyle) { options.titleBarStyle = 'hidden'; this.hiddenTitleBarStyle = true; + if (!isMacintosh) { + options.frame = false; + } } // Create the browser window. @@ -376,6 +383,23 @@ export class CodeWindow implements ICodeWindow { this._lastFocusTime = Date.now(); }); + // Window (Un)Maximize + this._win.on('maximize', (e) => { + if (this.currentConfig) { + this.currentConfig.maximized = true; + } + + app.emit('browser-window-maximize', e, this._win); + }); + + this._win.on('unmaximize', (e) => { + if (this.currentConfig) { + this.currentConfig.maximized = false; + } + + app.emit('browser-window-unmaximize', e, this._win); + }); + // Window Fullscreen this._win.on('enter-full-screen', () => { this.sendWhenReady('vscode:enterFullScreen'); @@ -591,6 +615,10 @@ export class CodeWindow implements ICodeWindow { windowConfiguration.baseTheme = this.getBaseTheme(); windowConfiguration.backgroundColor = this.getBackgroundColor(); + // Title style related + windowConfiguration.maximized = this._win.isMaximized(); + windowConfiguration.frameless = this.hasHiddenTitleBarStyle() && !isMacintosh; + // Perf Counters windowConfiguration.perfEntries = exportEntries(); windowConfiguration.perfStartTime = (global).perfStartTime; diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index ec370222870be..23aa9b19f2a10 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -62,6 +62,18 @@ export class MenuId { static readonly ViewItemContext = new MenuId(); static readonly TouchBarContext = new MenuId(); static readonly SearchContext = new MenuId(); + static readonly MenubarFileMenu = new MenuId(); + static readonly MenubarEditMenu = new MenuId(); + static readonly MenubarRecentMenu = new MenuId(); + static readonly MenubarSelectionMenu = new MenuId(); + static readonly MenubarViewMenu = new MenuId(); + static readonly MenubarLayoutMenu = new MenuId(); + static readonly MenubarGoMenu = new MenuId(); + static readonly MenubarDebugMenu = new MenuId(); + static readonly MenubarTasksMenu = new MenuId(); + static readonly MenubarWindowMenu = new MenuId(); + static readonly MenubarPreferencesMenu = new MenuId(); + static readonly MenubarHelpMenu = new MenuId(); readonly id: string = String(MenuId.ID++); } diff --git a/src/vs/platform/menubar/common/menubar.ts b/src/vs/platform/menubar/common/menubar.ts new file mode 100644 index 0000000000000..5fcc3ecec23ed --- /dev/null +++ b/src/vs/platform/menubar/common/menubar.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const IMenubarService = createDecorator('menubarService'); + +export interface IMenubarService { + _serviceBrand: any; + + updateMenubar(windowId: number, menus: IMenubarData): TPromise; +} + +export interface IMenubarData { + 'Files'?: IMenubarMenu; + 'Edit'?: IMenubarMenu; + [id: string]: IMenubarMenu; +} + +export interface IMenubarMenu { + items: Array; +} + +export interface IMenubarMenuItemAction { + id: string; + label: string; + checked: boolean; + enabled: boolean; +} + +export interface IMenubarMenuItemSeparator { + id: 'vscode.menubar.separator'; +} \ No newline at end of file diff --git a/src/vs/platform/menubar/common/menubarIpc.ts b/src/vs/platform/menubar/common/menubarIpc.ts new file mode 100644 index 0000000000000..52b2841d3e843 --- /dev/null +++ b/src/vs/platform/menubar/common/menubarIpc.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IMenubarService, IMenubarData } from 'vs/platform/menubar/common/menubar'; + +export interface IMenubarChannel extends IChannel { + call(command: 'updateMenubar', arg: [number, IMenubarData]): TPromise; + call(command: string, arg?: any): TPromise; +} + +export class MenubarChannel implements IMenubarChannel { + + constructor(private service: IMenubarService) { } + + call(command: string, arg?: any): TPromise { + switch (command) { + case 'updateMenubar': return this.service.updateMenubar(arg[0], arg[1]); + } + return undefined; + } +} + +export class MenubarChannelClient implements IMenubarService { + + _serviceBrand: any; + + constructor(private channel: IMenubarChannel) { } + + updateMenubar(windowId: number, menus: IMenubarData): TPromise { + return this.channel.call('updateMenubar', [windowId, menus]); + } +} \ No newline at end of file diff --git a/src/vs/platform/menubar/electron-main/menubarService.ts b/src/vs/platform/menubar/electron-main/menubarService.ts new file mode 100644 index 0000000000000..d4a846baf26ff --- /dev/null +++ b/src/vs/platform/menubar/electron-main/menubarService.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { IMenubarService, IMenubarData } from 'vs/platform/menubar/common/menubar'; +import { Menubar } from 'vs/code/electron-main/menubar'; +import { ILogService } from 'vs/platform/log/common/log'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { isMacintosh, isWindows } from 'vs/base/common/platform'; + +export class MenubarService implements IMenubarService { + _serviceBrand: any; + + private _menubar: Menubar; + + constructor( + @IInstantiationService private instantiationService: IInstantiationService, + @ILogService private logService: ILogService + ) { + // Install Menu + // TODO@sbatten: Remove if block + if (isMacintosh && isWindows) { + this._menubar = this.instantiationService.createInstance(Menubar); + } + } + + updateMenubar(windowId: number, menus: IMenubarData): TPromise { + this.logService.trace('menubarService#updateMenubar', windowId); + + if (this._menubar) { + this._menubar.updateMenu(menus, windowId); + } + + return TPromise.as(null); + } +} \ No newline at end of file diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index f548718cd7568..fd107b2bfc340 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -103,6 +103,8 @@ export interface IWindowsService { onWindowOpen: Event; onWindowFocus: Event; onWindowBlur: Event; + onWindowMaximize: Event; + onWindowUnmaximize: Event; // Dialogs pickFileFolderAndOpen(options: INativeOpenDialogOptions): TPromise; @@ -131,6 +133,7 @@ export interface IWindowsService { isMaximized(windowId: number): TPromise; maximizeWindow(windowId: number): TPromise; unmaximizeWindow(windowId: number): TPromise; + minimizeWindow(windowId: number): TPromise; onWindowTitleDoubleClick(windowId: number): TPromise; setDocumentEdited(windowId: number, flag: boolean): TPromise; quit(): TPromise; @@ -181,6 +184,7 @@ export interface IWindowService { _serviceBrand: any; onDidChangeFocus: Event; + onDidChangeMaximize: Event; getConfiguration(): IWindowConfiguration; getCurrentWindowId(): number; @@ -203,6 +207,10 @@ export interface IWindowService { openWindow(paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean; }): TPromise; isFocused(): TPromise; setDocumentEdited(flag: boolean): TPromise; + isMaximized(): TPromise; + maximizeWindow(): TPromise; + unmaximizeWindow(): TPromise; + minimizeWindow(): TPromise; onWindowTitleDoubleClick(): TPromise; show(): TPromise; showMessageBox(options: MessageBoxOptions): TPromise; @@ -326,9 +334,11 @@ export interface IWindowConfiguration extends ParsedArgs, IOpenFileRequest { zoomLevel?: number; fullscreen?: boolean; + maximized?: boolean; highContrast?: boolean; baseTheme?: string; backgroundColor?: string; + frameless?: boolean; accessibilitySupport?: boolean; perfEntries: PerformanceEntry[]; diff --git a/src/vs/platform/windows/common/windowsIpc.ts b/src/vs/platform/windows/common/windowsIpc.ts index 86b8cf27dfd87..04deb59213842 100644 --- a/src/vs/platform/windows/common/windowsIpc.ts +++ b/src/vs/platform/windows/common/windowsIpc.ts @@ -49,6 +49,7 @@ export interface IWindowsChannel extends IChannel { call(command: 'isMaximized', arg: number): TPromise; call(command: 'maximizeWindow', arg: number): TPromise; call(command: 'unmaximizeWindow', arg: number): TPromise; + call(command: 'minimizeWindow', arg: number): TPromise; call(command: 'onWindowTitleDoubleClick', arg: number): TPromise; call(command: 'setDocumentEdited', arg: [number, boolean]): TPromise; call(command: 'quit'): TPromise; @@ -73,11 +74,15 @@ export class WindowsChannel implements IWindowsChannel { private onWindowOpen: Event; private onWindowFocus: Event; private onWindowBlur: Event; + private onWindowMaximize: Event; + private onWindowUnmaximize: Event; constructor(private service: IWindowsService) { this.onWindowOpen = buffer(service.onWindowOpen, true); this.onWindowFocus = buffer(service.onWindowFocus, true); this.onWindowBlur = buffer(service.onWindowBlur, true); + this.onWindowMaximize = buffer(service.onWindowMaximize, true); + this.onWindowUnmaximize = buffer(service.onWindowUnmaximize, true); } call(command: string, arg?: any): TPromise { @@ -85,6 +90,8 @@ export class WindowsChannel implements IWindowsChannel { case 'event:onWindowOpen': return eventToCall(this.onWindowOpen); case 'event:onWindowFocus': return eventToCall(this.onWindowFocus); case 'event:onWindowBlur': return eventToCall(this.onWindowBlur); + case 'event:onWindowMaximize': return eventToCall(this.onWindowMaximize); + case 'event:onWindowUnmaximize': return eventToCall(this.onWindowUnmaximize); case 'pickFileFolderAndOpen': return this.service.pickFileFolderAndOpen(arg); case 'pickFileAndOpen': return this.service.pickFileAndOpen(arg); case 'pickFolderAndOpen': return this.service.pickFolderAndOpen(arg); @@ -129,6 +136,7 @@ export class WindowsChannel implements IWindowsChannel { case 'isMaximized': return this.service.isMaximized(arg); case 'maximizeWindow': return this.service.maximizeWindow(arg); case 'unmaximizeWindow': return this.service.unmaximizeWindow(arg); + case 'minimizeWindow': return this.service.minimizeWindow(arg); case 'onWindowTitleDoubleClick': return this.service.onWindowTitleDoubleClick(arg); case 'setDocumentEdited': return this.service.setDocumentEdited(arg[0], arg[1]); case 'openWindow': return this.service.openWindow(arg[0], arg[1], arg[2]); @@ -165,6 +173,12 @@ export class WindowsChannelClient implements IWindowsService { private _onWindowBlur: Event = eventFromCall(this.channel, 'event:onWindowBlur'); get onWindowBlur(): Event { return this._onWindowBlur; } + private _onWindowMaximize: Event = eventFromCall(this.channel, 'event:onWindowMaximize'); + get onWindowMaximize(): Event { return this._onWindowMaximize; } + + private _onWindowUnmaximize: Event = eventFromCall(this.channel, 'event:onWindowUnmaximize'); + get onWindowUnmaximize(): Event { return this._onWindowUnmaximize; } + pickFileFolderAndOpen(options: INativeOpenDialogOptions): TPromise { return this.channel.call('pickFileFolderAndOpen', options); } @@ -285,6 +299,10 @@ export class WindowsChannelClient implements IWindowsService { return this.channel.call('unmaximizeWindow', windowId); } + minimizeWindow(windowId: number): TPromise { + return this.channel.call('minimizeWindow', windowId); + } + onWindowTitleDoubleClick(windowId: number): TPromise { return this.channel.call('onWindowTitleDoubleClick', windowId); } diff --git a/src/vs/platform/windows/electron-browser/windowService.ts b/src/vs/platform/windows/electron-browser/windowService.ts index 785106fbd91c0..ec7940c32e731 100644 --- a/src/vs/platform/windows/electron-browser/windowService.ts +++ b/src/vs/platform/windows/electron-browser/windowService.ts @@ -16,6 +16,7 @@ import { ParsedArgs } from 'vs/platform/environment/common/environment'; export class WindowService implements IWindowService { readonly onDidChangeFocus: Event; + readonly onDidChangeMaximize: Event; _serviceBrand: any; @@ -26,7 +27,10 @@ export class WindowService implements IWindowService { ) { const onThisWindowFocus = mapEvent(filterEvent(windowsService.onWindowFocus, id => id === windowId), _ => true); const onThisWindowBlur = mapEvent(filterEvent(windowsService.onWindowBlur, id => id === windowId), _ => false); + const onThisWindowMaximize = mapEvent(filterEvent(windowsService.onWindowMaximize, id => id === windowId), _ => true); + const onThisWindowUnmaximize = mapEvent(filterEvent(windowsService.onWindowUnmaximize, id => id === windowId), _ => false); this.onDidChangeFocus = anyEvent(onThisWindowFocus, onThisWindowBlur); + this.onDidChangeMaximize = anyEvent(onThisWindowMaximize, onThisWindowUnmaximize); } getCurrentWindowId(): number { @@ -113,6 +117,22 @@ export class WindowService implements IWindowService { return this.windowsService.isFocused(this.windowId); } + isMaximized(): TPromise { + return this.windowsService.isMaximized(this.windowId); + } + + maximizeWindow(): TPromise { + return this.windowsService.maximizeWindow(this.windowId); + } + + unmaximizeWindow(): TPromise { + return this.windowsService.unmaximizeWindow(this.windowId); + } + + minimizeWindow(): TPromise { + return this.windowsService.minimizeWindow(this.windowId); + } + onWindowTitleDoubleClick(): TPromise { return this.windowsService.onWindowTitleDoubleClick(this.windowId); } diff --git a/src/vs/platform/windows/electron-main/windowsService.ts b/src/vs/platform/windows/electron-main/windowsService.ts index 2909bc74d362c..47441d7e6f929 100644 --- a/src/vs/platform/windows/electron-main/windowsService.ts +++ b/src/vs/platform/windows/electron-main/windowsService.ts @@ -39,6 +39,8 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable ); readonly onWindowBlur: Event = filterEvent(fromNodeEventEmitter(app, 'browser-window-blur', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)); + readonly onWindowMaximize: Event = filterEvent(fromNodeEventEmitter(app, 'browser-window-maximize', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)); + readonly onWindowUnmaximize: Event = filterEvent(fromNodeEventEmitter(app, 'browser-window-unmaximize', (_, w: Electron.BrowserWindow) => w.id), id => !!this.windowsMainService.getWindowById(id)); constructor( private sharedProcess: ISharedProcess, @@ -338,6 +340,17 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable return TPromise.as(null); } + minimizeWindow(windowId: number): TPromise { + this.logService.trace('windowsService#minimizeWindow', windowId); + const codeWindow = this.windowsMainService.getWindowById(windowId); + + if (codeWindow) { + codeWindow.win.minimize(); + } + + return TPromise.as(null); + } + onWindowTitleDoubleClick(windowId: number): TPromise { this.logService.trace('windowsService#onWindowTitleDoubleClick', windowId); const codeWindow = this.windowsMainService.getWindowById(windowId); diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index d28eade122655..325b14f8506d5 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -16,6 +16,7 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView import { Disposable } from 'vs/base/common/lifecycle'; import { getZoomFactor } from 'vs/base/browser/browser'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { isMacintosh } from 'vs/base/common/platform'; import { memoize } from 'vs/base/common/decorators'; import { NotificationsCenter } from 'vs/workbench/browser/parts/notifications/notificationsCenter'; import { NotificationsToasts } from 'vs/workbench/browser/parts/notifications/notificationsToasts'; @@ -27,8 +28,9 @@ import { ActivitybarPart } from 'vs/workbench/browser/parts/activitybar/activity import { SidebarPart } from 'vs/workbench/browser/parts/sidebar/sidebarPart'; import { PanelPart } from 'vs/workbench/browser/parts/panel/panelPart'; import { StatusbarPart } from 'vs/workbench/browser/parts/statusbar/statusbarPart'; +import { MenubarPart } from 'vs/workbench/browser/parts/menubar/menubarPart'; -const TITLE_BAR_HEIGHT = 22; +const TITLE_BAR_HEIGHT = isMacintosh ? 22 : 30; const STATUS_BAR_HEIGHT = 22; const ACTIVITY_BAR_WIDTH = 50; @@ -63,6 +65,8 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr private _sidebarWidth: number; private sidebarHeight: number; private titlebarHeight: number; + private menubarHeight: number; + private headingHeight: number; private statusbarHeight: number; private panelSizeBeforeMaximized: number; private panelMaximized: boolean; @@ -75,6 +79,7 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr private workbenchContainer: HTMLElement, private parts: { titlebar: TitlebarPart, + menubar: MenubarPart, activitybar: ActivitybarPart, editor: EditorPart, sidebar: SidebarPart, @@ -219,6 +224,9 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr titlebar: { height: TITLE_BAR_HEIGHT }, + menubar: { + height: TITLE_BAR_HEIGHT + }, activitybar: { width: ACTIVITY_BAR_WIDTH }, @@ -311,7 +319,7 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr if (newSashHeight + HIDE_PANEL_HEIGHT_THRESHOLD < this.partLayoutInfo.panel.minHeight) { let dragCompensation = this.partLayoutInfo.panel.minHeight - HIDE_PANEL_HEIGHT_THRESHOLD; promise = this.partService.setPanelHidden(true); - startY = Math.min(this.sidebarHeight - this.statusbarHeight - this.titlebarHeight, e.currentY + dragCompensation); + startY = Math.min(this.sidebarHeight - this.statusbarHeight - this.headingHeight, e.currentY + dragCompensation); this.panelHeight = startPanelHeight; // when restoring panel, restore to the panel height we started from } @@ -412,6 +420,7 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr const isActivityBarHidden = !this.partService.isVisible(Parts.ACTIVITYBAR_PART); const isTitlebarHidden = !this.partService.isVisible(Parts.TITLEBAR_PART); + const isMenubarHidden = !this.partService.isVisible(Parts.MENUBAR_PART); const isPanelHidden = !this.partService.isVisible(Parts.PANEL_PART); const isStatusbarHidden = !this.partService.isVisible(Parts.STATUSBAR_PART); const isSidebarHidden = !this.partService.isVisible(Parts.SIDEBAR_PART); @@ -425,8 +434,10 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr this.statusbarHeight = isStatusbarHidden ? 0 : this.partLayoutInfo.statusbar.height; this.titlebarHeight = isTitlebarHidden ? 0 : this.partLayoutInfo.titlebar.height / getZoomFactor(); // adjust for zoom prevention + this.menubarHeight = isMenubarHidden ? 0 : this.partLayoutInfo.menubar.height / getZoomFactor(); // adjust for zoom prevention + this.headingHeight = Math.max(this.menubarHeight, this.titlebarHeight); - this.sidebarHeight = this.workbenchSize.height - this.statusbarHeight - this.titlebarHeight; + this.sidebarHeight = this.workbenchSize.height - this.statusbarHeight - this.headingHeight; let sidebarSize = new Dimension(this.sidebarWidth, this.sidebarHeight); // Activity Bar @@ -563,6 +574,14 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr show(titleContainer); } + // Menubar + const menubarContainer = this.parts.menubar.getContainer(); + if (isMenubarHidden) { + hide(menubarContainer); + } else { + show(menubarContainer); + } + // Editor Part and Panel part const editorContainer = this.parts.editor.getContainer(); const panelContainer = this.parts.panel.getContainer(); @@ -571,19 +590,19 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr if (panelPosition === Position.BOTTOM) { if (sidebarPosition === Position.LEFT) { - position(editorContainer, this.titlebarHeight, 0, this.statusbarHeight + panelDimension.height, sidebarSize.width + activityBarSize.width); - position(panelContainer, editorSize.height + this.titlebarHeight, 0, this.statusbarHeight, sidebarSize.width + activityBarSize.width); + position(editorContainer, this.headingHeight, 0, this.statusbarHeight + panelDimension.height, sidebarSize.width + activityBarSize.width); + position(panelContainer, editorSize.height + this.headingHeight, 0, this.statusbarHeight, sidebarSize.width + activityBarSize.width); } else { - position(editorContainer, this.titlebarHeight, sidebarSize.width, this.statusbarHeight + panelDimension.height, 0); - position(panelContainer, editorSize.height + this.titlebarHeight, sidebarSize.width, this.statusbarHeight, 0); + position(editorContainer, this.headingHeight, sidebarSize.width, this.statusbarHeight + panelDimension.height, 0); + position(panelContainer, editorSize.height + this.headingHeight, sidebarSize.width, this.statusbarHeight, 0); } } else { if (sidebarPosition === Position.LEFT) { - position(editorContainer, this.titlebarHeight, panelDimension.width, this.statusbarHeight, sidebarSize.width + activityBarSize.width); - position(panelContainer, this.titlebarHeight, 0, this.statusbarHeight, sidebarSize.width + activityBarSize.width + editorSize.width); + position(editorContainer, this.headingHeight, panelDimension.width, this.statusbarHeight, sidebarSize.width + activityBarSize.width); + position(panelContainer, this.headingHeight, 0, this.statusbarHeight, sidebarSize.width + activityBarSize.width + editorSize.width); } else { - position(editorContainer, this.titlebarHeight, sidebarSize.width + activityBarSize.width + panelWidth, this.statusbarHeight, 0); - position(panelContainer, this.titlebarHeight, sidebarSize.width + activityBarSize.width, this.statusbarHeight, editorSize.width); + position(editorContainer, this.headingHeight, sidebarSize.width + activityBarSize.width + panelWidth, this.statusbarHeight, 0); + position(panelContainer, this.headingHeight, sidebarSize.width + activityBarSize.width, this.statusbarHeight, editorSize.width); } } @@ -592,10 +611,10 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr size(activitybarContainer, null, activityBarSize.height); if (sidebarPosition === Position.LEFT) { this.parts.activitybar.getContainer().style.right = ''; - position(activitybarContainer, this.titlebarHeight, null, 0, 0); + position(activitybarContainer, this.headingHeight, null, 0, 0); } else { this.parts.activitybar.getContainer().style.left = ''; - position(activitybarContainer, this.titlebarHeight, 0, 0, null); + position(activitybarContainer, this.headingHeight, 0, 0, null); } if (isActivityBarHidden) { hide(activitybarContainer); @@ -608,9 +627,9 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr size(sidebarContainer, sidebarSize.width, sidebarSize.height); const editorAndPanelWidth = editorSize.width + (panelPosition === Position.RIGHT ? panelWidth : 0); if (sidebarPosition === Position.LEFT) { - position(sidebarContainer, this.titlebarHeight, editorAndPanelWidth, this.statusbarHeight, activityBarSize.width); + position(sidebarContainer, this.headingHeight, editorAndPanelWidth, this.statusbarHeight, activityBarSize.width); } else { - position(sidebarContainer, this.titlebarHeight, activityBarSize.width, this.statusbarHeight, editorAndPanelWidth); + position(sidebarContainer, this.headingHeight, activityBarSize.width, this.statusbarHeight, editorAndPanelWidth); } // Statusbar Part @@ -646,6 +665,7 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr // Propagate to Part Layouts this.parts.titlebar.layout(new Dimension(this.workbenchSize.width, this.titlebarHeight)); + this.parts.menubar.layout(new Dimension(this.workbenchSize.width, this.menubarHeight)); this.parts.editor.layout(new Dimension(editorSize.width, editorSize.height)); this.parts.sidebar.layout(sidebarSize); this.parts.panel.layout(panelDimension); @@ -656,7 +676,7 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr } getVerticalSashTop(sash: Sash): number { - return this.titlebarHeight; + return this.headingHeight; } getVerticalSashLeft(sash: Sash): number { @@ -683,7 +703,7 @@ export class WorkbenchLayout extends Disposable implements IVerticalSashLayoutPr getHorizontalSashTop(sash: Sash): number { const offset = 2; // Horizontal sash should be a bit lower than the editor area, thus add 2px #5524 - return offset + (this.partService.isVisible(Parts.PANEL_PART) ? this.sidebarHeight - this.panelHeight + this.titlebarHeight : this.sidebarHeight + this.titlebarHeight); + return offset + (this.partService.isVisible(Parts.PANEL_PART) ? this.sidebarHeight - this.panelHeight + this.headingHeight : this.sidebarHeight + this.headingHeight); } getHorizontalSashLeft(sash: Sash): number { diff --git a/src/vs/workbench/browser/parts/menubar/media/menubarpart.css b/src/vs/workbench/browser/parts/menubar/media/menubarpart.css new file mode 100644 index 0000000000000..1fb3533853fb2 --- /dev/null +++ b/src/vs/workbench/browser/parts/menubar/media/menubarpart.css @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench > .part.menubar { + display: flex; + position: absolute; + font-size: 12px; + box-sizing: border-box; + padding-left: 30px; + padding-right: 138px; + height: 30px; +} + +.monaco-workbench.fullscreen > .part.menubar { + position: absolute; + width: 100%; + margin: 0px; + padding: 0px 5px; +} + +.monaco-workbench > .part.menubar > .menubar-menu-button { + display: flex; + flex-shrink: 0; + align-items: center; + box-sizing: border-box; + padding: 0px 5px; + position: relative; + z-index: 10; + cursor: pointer; + -webkit-app-region: no-drag; + zoom: 1; +} + +.monaco-workbench > .part.menubar > .menubar-menu-button.open, +.monaco-workbench > .part.menubar > .menubar-menu-button:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +.monaco-workbench > .part.menubar.light > .menubar-menu-button.open, +.monaco-workbench > .part.menubar.light > .menubar-menu-button:hover { + background-color: rgba(0, 0, 0, 0.1); +} + +.menubar-menu-items-holder { + position: absolute; + left: 0px; + opacity: 1; +} + +.menubar-menu-items-holder.monaco-menu-container { + box-shadow: 0 2px 8px #A8A8A8; +} + +.vs-dark .menubar-menu-items-holder.monaco-menu-container { + box-shadow: 0 2px 8px #000; +} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts b/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts new file mode 100644 index 0000000000000..b551e1158cb9d --- /dev/null +++ b/src/vs/workbench/browser/parts/menubar/menubar.contribution.ts @@ -0,0 +1,1477 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; +import * as menubarCommands from 'vs/workbench/browser/parts/menubar/menubarCommands'; +import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { isMacintosh } from 'vs/base/common/platform'; + +// TODO: Add submenu support to remove layout, preferences, and recent top level +menubarCommands.setup(); +fileMenuRegistration(); +editMenuRegistration(); +recentMenuRegistration(); +selectionMenuRegistration(); +viewMenuRegistration(); +layoutMenuRegistration(); +goMenuRegistration(); +debugMenuRegistration(); +tasksMenuRegistration(); + +if (isMacintosh) { + windowMenuRegistration(); +} + +preferencesMenuRegistration(); +helpMenuRegistration(); + +// Menu registration - File Menu +function fileMenuRegistration() { + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '1_new', + command: { + id: 'workbench.action.files.newUntitledFile', + title: nls.localize({ key: 'miNewFile', comment: ['&& denotes a mnemonic'] }, "&&New File") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '1_new', + command: { + id: 'workbench.action.newWindow', + title: nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '2_open', + command: { + id: 'workbench.action.files.openFile', + title: nls.localize({ key: 'miOpenFile', comment: ['&& denotes a mnemonic'] }, "&&Open File...") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '2_open', + command: { + id: 'workbench.action.files.openFolder', + title: nls.localize({ key: 'miOpenFolder', comment: ['&& denotes a mnemonic'] }, "Open &&Folder...") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '2_open', + command: { + id: 'workbench.action.openWorkspace', + title: nls.localize({ key: 'miOpenWorkspace', comment: ['&& denotes a mnemonic'] }, "Open Wor&&kspace...") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '3_workspace', + command: { + id: 'workbench.action.addRootFolder', + title: nls.localize({ key: 'miAddFolderToWorkspace', comment: ['&& denotes a mnemonic'] }, "A&&dd Folder to Workspace...") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '3_workspace', + command: { + id: 'workbench.action.saveWorkspaceAs', + title: nls.localize('miSaveWorkspaceAs', "Save Workspace As...") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '4_save', + command: { + id: 'workbench.action.files.save', + title: nls.localize({ key: 'miSave', comment: ['&& denotes a mnemonic'] }, "&&Save"), + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '4_save', + command: { + id: 'workbench.action.files.saveAs', + title: nls.localize({ key: 'miSaveAs', comment: ['&& denotes a mnemonic'] }, "Save &&As...") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '4_save', + command: { + id: 'workbench.action.files.saveAll', + title: nls.localize({ key: 'miSaveAll', comment: ['&& denotes a mnemonic'] }, "Save A&&ll") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '5_autosave', + command: { + id: 'workbench.action.toggleAutoSave', + title: nls.localize('miAutoSave', "Auto Save") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '6_close', + command: { + id: '', + title: nls.localize({ key: 'miRevert', comment: ['&& denotes a mnemonic'] }, "Re&&vert File") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '6_close', + command: { + id: 'workbench.action.closeActiveEditor', + title: nls.localize({ key: 'miCloseEditor', comment: ['&& denotes a mnemonic'] }, "&&Close Editor") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '6_close', + command: { + id: 'workbench.action.closeFolder', + title: nls.localize({ key: 'miCloseFolder', comment: ['&& denotes a mnemonic'] }, "Close &&Folder") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '6_close', + command: { + id: 'workbench.action.closeWindow', + title: nls.localize({ key: 'miCloseWindow', comment: ['&& denotes a mnemonic'] }, "Clos&&e Window") + }, + order: 4 + }); + + if (!isMacintosh) { + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: 'z_Exit', + command: { + id: 'workbench.action.quit', + title: nls.localize({ key: 'miExit', comment: ['&& denotes a mnemonic'] }, "E&&xit") + }, + order: 1 + }); + } +} + +function editMenuRegistration() { + MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { + group: '1_do', + command: { + id: 'undo', + title: nls.localize({ key: 'miUndo', comment: ['&& denotes a mnemonic'] }, "&&Undo") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { + group: '1_do', + command: { + id: 'redo', + title: nls.localize({ key: 'miRedo', comment: ['&& denotes a mnemonic'] }, "&&Redo") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { + group: '2_ccp', + command: { + id: 'editor.action.clipboardCutAction', + title: nls.localize({ key: 'miCut', comment: ['&& denotes a mnemonic'] }, "Cu&&t") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { + group: '2_ccp', + command: { + id: 'editor.action.clipboardCopyAction', + title: nls.localize({ key: 'miCopy', comment: ['&& denotes a mnemonic'] }, "&&Copy") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { + group: '2_ccp', + command: { + id: 'editor.action.clipboardPasteAction', + title: nls.localize({ key: 'miPaste', comment: ['&& denotes a mnemonic'] }, "&&Paste") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { + group: '3_find', + command: { + id: 'actions.find', + title: nls.localize({ key: 'miFind', comment: ['&& denotes a mnemonic'] }, "&&Find") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { + group: '3_find', + command: { + id: 'editor.action.startFindReplaceAction', + title: nls.localize({ key: 'miReplace', comment: ['&& denotes a mnemonic'] }, "&&Replace") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { + group: '4_find_global', + command: { + id: 'workbench.action.findInFiles', + title: nls.localize({ key: 'miFindInFiles', comment: ['&& denotes a mnemonic'] }, "Find &&in Files") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { + group: '4_find_global', + command: { + + id: 'workbench.action.replaceInFiles', + title: nls.localize({ key: 'miReplaceInFiles', comment: ['&& denotes a mnemonic'] }, "Replace &&in Files") + }, + order: 2 + }); + + + /// + + + MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { + group: '5_insert', + command: { + id: 'editor.action.commentLine', + title: nls.localize({ key: 'miToggleLineComment', comment: ['&& denotes a mnemonic'] }, "&&Toggle Line Comment") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { + group: '5_insert', + command: { + id: 'editor.action.blockComment', + title: nls.localize({ key: 'miToggleBlockComment', comment: ['&& denotes a mnemonic'] }, "Toggle &&Block Comment") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { + group: '5_insert', + command: { + id: 'editor.emmet.action.expandAbbreviation', + title: nls.localize({ key: 'miEmmetExpandAbbreviation', comment: ['&& denotes a mnemonic'] }, "Emmet: E&&xpand Abbreviation") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { + group: '5_insert', + command: { + id: 'workbench.action.showEmmetCommands', + title: nls.localize({ key: 'miShowEmmetCommands', comment: ['&& denotes a mnemonic'] }, "E&&mmet...") + }, + order: 2 + }); +} + +function selectionMenuRegistration() { + MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { + group: '1_basic', + command: { + id: 'editor.action.selectAll', + title: nls.localize({ key: 'miSelectAll', comment: ['&& denotes a mnemonic'] }, "&&Select All") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { + group: '1_basic', + command: { + id: 'editor.action.smartSelect.grow', + title: nls.localize({ key: 'miSmartSelectGrow', comment: ['&& denotes a mnemonic'] }, "&&Expand Selection") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { + group: '1_basic', + command: { + id: 'editor.action.smartSelect.shrink', + title: nls.localize({ key: 'miSmartSelectShrink', comment: ['&& denotes a mnemonic'] }, "&&Shrink Selection") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { + group: '2_line', + command: { + id: 'editor.action.copyLinesUpAction', + title: nls.localize({ key: 'miCopyLinesUp', comment: ['&& denotes a mnemonic'] }, "&&Copy Line Up") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { + group: '2_line', + command: { + id: 'editor.action.copyLinesDownAction', + title: nls.localize({ key: 'miCopyLinesDown', comment: ['&& denotes a mnemonic'] }, "Co&&py Line Down") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { + group: '2_line', + command: { + id: 'editor.action.moveLinesUpAction', + title: nls.localize({ key: 'miMoveLinesUp', comment: ['&& denotes a mnemonic'] }, "Mo&&ve Line Up") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { + group: '2_line', + command: { + id: 'editor.action.moveLinesDownAction', + title: nls.localize({ key: 'miMoveLinesDown', comment: ['&& denotes a mnemonic'] }, "Move &&Line Down") + }, + order: 4 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { + group: '3_multi', + command: { + id: 'workbench.action.toggleMultiCursorModifier', + title: nls.localize('miMultiCursorAlt', "Switch to Alt+Click for Multi-Cursor") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { + group: '3_multi', + command: { + id: 'editor.action.insertCursorAbove', + title: nls.localize({ key: 'miInsertCursorAbove', comment: ['&& denotes a mnemonic'] }, "&&Add Cursor Above") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { + group: '3_multi', + command: { + id: 'editor.action.insertCursorBelow', + title: nls.localize({ key: 'miInsertCursorBelow', comment: ['&& denotes a mnemonic'] }, "A&&dd Cursor Below") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { + group: '3_multi', + command: { + id: 'editor.action.insertCursorAtEndOfEachLineSelected', + title: nls.localize({ key: 'miInsertCursorAtEndOfEachLineSelected', comment: ['&& denotes a mnemonic'] }, "Add C&&ursors to Line Ends") + }, + order: 4 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { + group: '3_multi', + command: { + id: 'editor.action.addSelectionToNextFindMatch', + title: nls.localize({ key: 'miAddSelectionToNextFindMatch', comment: ['&& denotes a mnemonic'] }, "Add &&Next Occurrence") + }, + order: 5 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { + group: '3_multi', + command: { + id: 'editor.action.addSelectionToPreviousFindMatch', + title: nls.localize({ key: 'miAddSelectionToPreviousFindMatch', comment: ['&& denotes a mnemonic'] }, "Add P&&revious Occurrence") + }, + order: 6 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { + group: '3_multi', + command: { + id: 'editor.action.selectHighlights', + title: nls.localize({ key: 'miSelectHighlights', comment: ['&& denotes a mnemonic'] }, "Select All &&Occurrences") + }, + order: 7 + }); +} + +function recentMenuRegistration() { + // Editor + MenuRegistry.appendMenuItem(MenuId.MenubarRecentMenu, { + group: '1_editor', + command: { + id: 'workbench.action.reopenClosedEditor', + title: nls.localize({ key: 'miReopenClosedEditor', comment: ['&& denotes a mnemonic'] }, "&&Reopen Closed Editor") + }, + order: 1 + }); + + // More + MenuRegistry.appendMenuItem(MenuId.MenubarRecentMenu, { + group: 'y_more', + command: { + id: 'workbench.action.openRecent', + title: nls.localize({ key: 'miMore', comment: ['&& denotes a mnemonic'] }, "&&More...") + }, + order: 1 + }); + + // Clear + MenuRegistry.appendMenuItem(MenuId.MenubarRecentMenu, { + group: 'z_clear', + command: { + id: 'workbench.action.clearRecentFiles', + title: nls.localize({ key: 'miClearRecentOpen', comment: ['&& denotes a mnemonic'] }, "&&Clear Recently Opened") + }, + order: 1 + }); + +} + +function viewMenuRegistration() { + + // Command Palette + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '1_open', + command: { + id: 'workbench.action.showCommands', + title: nls.localize({ key: 'miCommandPalette', comment: ['&& denotes a mnemonic'] }, "&&Command Palette...") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '1_open', + command: { + id: 'workbench.action.openView', + title: nls.localize({ key: 'miOpenView', comment: ['&& denotes a mnemonic'] }, "&&Open View...") + }, + order: 2 + }); + + // Viewlets + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '2_views', + command: { + id: 'workbench.view.explorer', + title: nls.localize({ key: 'miViewExplorer', comment: ['&& denotes a mnemonic'] }, "&&Explorer") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '2_views', + command: { + id: 'workbench.view.search', + title: nls.localize({ key: 'miViewSearch', comment: ['&& denotes a mnemonic'] }, "&&Search") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '2_views', + command: { + id: 'workbench.view.scm', + title: nls.localize({ key: 'miViewSCM', comment: ['&& denotes a mnemonic'] }, "S&&CM") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '2_views', + command: { + id: 'workbench.view.debug', + title: nls.localize({ key: 'miViewDebug', comment: ['&& denotes a mnemonic'] }, "&&Debug") + }, + order: 4 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '2_views', + command: { + id: 'workbench.view.extensions', + title: nls.localize({ key: 'miViewExtensions', comment: ['&& denotes a mnemonic'] }, "E&&xtensions") + }, + order: 5 + }); + + // Panels + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '3_panels', + command: { + id: 'workbench.action.output.toggleOutput', + title: nls.localize({ key: 'miToggleOutput', comment: ['&& denotes a mnemonic'] }, "&&Output") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '3_panels', + command: { + id: 'workbench.debug.action.toggleRepl', + title: nls.localize({ key: 'miToggleDebugConsole', comment: ['&& denotes a mnemonic'] }, "De&&bug Console") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '3_panels', + command: { + id: 'workbench.action.terminal.toggleTerminal', + title: nls.localize({ key: 'miToggleIntegratedTerminal', comment: ['&& denotes a mnemonic'] }, "&&Integrated Terminal") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '3_panels', + command: { + id: 'workbench.actions.view.problems', + title: nls.localize({ key: 'miMarker', comment: ['&& denotes a mnemonic'] }, "&&Problems") + }, + order: 4 + }); + + // Toggle View + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '4_toggle_view', + command: { + id: 'workbench.action.toggleFullScreen', + title: nls.localize({ key: 'miToggleFullScreen', comment: ['&& denotes a mnemonic'] }, "Toggle &&Full Screen") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '4_toggle_view', + command: { + id: 'workbench.action.toggleZenMode', + title: nls.localize('miToggleZenMode', "Toggle Zen Mode") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '4_toggle_view', + command: { + id: 'workbench.action.toggleCenteredLayout', + title: nls.localize('miToggleCenteredLayout', "Toggle Centered Layout") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '4_toggle_view', + command: { + id: 'workbench.action.toggleMenuBar', + title: nls.localize({ key: 'miToggleMenuBar', comment: ['&& denotes a mnemonic'] }, "Toggle Menu &&Bar") + }, + order: 4 + }); + + // TODO: Editor Layout Submenu + + // Workbench Layout + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '6_workbench_layout', + command: { + id: 'workbench.action.toggleSidebarVisibility', + title: nls.localize({ key: 'miToggleSidebar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Side Bar") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '6_workbench_layout', + command: { + id: 'workbench.action.toggleSidebarPosition', + title: nls.localize({ key: 'miMoveSidebarLeftRight', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Left/Right") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '6_workbench_layout', + command: { + id: 'workbench.action.toggleStatusbarVisibility', + title: nls.localize({ key: 'miToggleStatusbar', comment: ['&& denotes a mnemonic'] }, "&&Toggle Status Bar") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '6_workbench_layout', + command: { + id: 'workbench.action.toggleActivityBarVisibility', + title: nls.localize({ key: 'miToggleActivityBar', comment: ['&& denotes a mnemonic'] }, "Toggle &&Activity Bar") + }, + order: 4 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '6_workbench_layout', + command: { + id: 'workbench.action.togglePanel', + title: nls.localize({ key: 'miTogglePanel', comment: ['&& denotes a mnemonic'] }, "Toggle &&Panel") + }, + order: 5 + }); + + // Toggle Editor Settings + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '8_editor', + command: { + id: 'workbench.action.toggleWordWrap', + title: nls.localize({ key: 'miToggleWordWrap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Word Wrap") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '8_editor', + command: { + id: 'workbench.action.toggleMinimap', + title: nls.localize({ key: 'miToggleMinimap', comment: ['&& denotes a mnemonic'] }, "Toggle &&Minimap") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '8_editor', + command: { + id: 'workbench.action.toggleRenderWhitespace', + title: nls.localize({ key: 'miToggleRenderWhitespace', comment: ['&& denotes a mnemonic'] }, "Toggle &&Render Whitespace") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '8_editor', + command: { + id: 'workbench.action.toggleRenderControlCharacters', + title: nls.localize({ key: 'miToggleRenderControlCharacters', comment: ['&& denotes a mnemonic'] }, "Toggle &&Control Characters") + }, + order: 4 + }); + + // Zoom + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '9_zoom', + command: { + id: 'workbench.action.zoomIn', + title: nls.localize({ key: 'miZoomIn', comment: ['&& denotes a mnemonic'] }, "&&Zoom In") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '9_zoom', + command: { + id: 'workbench.action.zoomOut', + title: nls.localize({ key: 'miZoomOut', comment: ['&& denotes a mnemonic'] }, "&&Zoom Out") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { + group: '9_zoom', + command: { + id: 'workbench.action.zoomReset', + title: nls.localize({ key: 'miZoomReset', comment: ['&& denotes a mnemonic'] }, "&&Reset Zoom") + }, + order: 3 + }); +} + +function layoutMenuRegistration() { + // Split + MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '1_split', + command: { + id: 'workbench.action.splitEditorUp', + title: nls.localize({ key: 'miSplitEditorUp', comment: ['&& denotes a mnemonic'] }, "Split &&Up") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '1_split', + command: { + id: 'workbench.action.splitEditorDown', + title: nls.localize({ key: 'miSplitEditorDown', comment: ['&& denotes a mnemonic'] }, "Split &&Down") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '1_split', + command: { + id: 'workbench.action.splitEditorLeft', + title: nls.localize({ key: 'miSplitEditorLeft', comment: ['&& denotes a mnemonic'] }, "Split &&Left") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '1_split', + command: { + id: 'workbench.action.splitEditorRight', + title: nls.localize({ key: 'miSplitEditorRight', comment: ['&& denotes a mnemonic'] }, "Split &&Right") + }, + order: 4 + }); + + // Layouts + MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: 'workbench.action.editorLayoutSingle', + title: nls.localize({ key: 'miSingleColumnEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Single") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: 'workbench.action.editorLayoutCentered', + title: nls.localize({ key: 'miCenteredEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Centered") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: 'workbench.action.editorLayoutTwoColumns', + title: nls.localize({ key: 'miTwoColumnsEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Two Columns") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: 'workbench.action.editorLayoutThreeColumns', + title: nls.localize({ key: 'miThreeColumnsEditorLayout', comment: ['&& denotes a mnemonic'] }, "T&&hree Columns") + }, + order: 4 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: 'workbench.action.editorLayoutTwoRows', + title: nls.localize({ key: 'miTwoRowsEditorLayout', comment: ['&& denotes a mnemonic'] }, "T&&wo Rows") + }, + order: 5 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: 'workbench.action.editorLayoutThreeRows', + title: nls.localize({ key: 'miThreeRowsEditorLayout', comment: ['&& denotes a mnemonic'] }, "Three &&Rows") + }, + order: 6 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: 'workbench.action.editorLayoutTwoByTwoGrid', + title: nls.localize({ key: 'miTwoByTwoGridEditorLayout', comment: ['&& denotes a mnemonic'] }, "&&Grid (2x2)") + }, + order: 7 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: 'workbench.action.editorLayoutTwoColumnsRight', + title: nls.localize({ key: 'miTwoColumnsRightEditorLayout', comment: ['&& denotes a mnemonic'] }, "Two C&&olumns Right") + }, + order: 8 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: '2_layouts', + command: { + id: 'workbench.action.editorLayoutTwoColumnsBottom', + title: nls.localize({ key: 'miTwoColumnsBottomEditorLayout', comment: ['&& denotes a mnemonic'] }, "Two &&Columns Bottom") + }, + order: 9 + }); + + // Flip + MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, { + group: 'z_flip', + command: { + id: 'workbench.action.toggleEditorGroupLayout', + title: nls.localize({ key: 'miToggleEditorLayout', comment: ['&& denotes a mnemonic'] }, "Flip &&Layout") + }, + order: 1 + }); + +} + +function goMenuRegistration() { + // Forward/Back + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '1_fwd_back', + command: { + id: 'workbench.action.navigateBack', + title: nls.localize({ key: 'miBack', comment: ['&& denotes a mnemonic'] }, "&&Back") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '1_fwd_back', + command: { + id: 'workbench.action.navigateForward', + title: nls.localize({ key: 'miForward', comment: ['&& denotes a mnemonic'] }, "&&Forward") + }, + order: 2 + }); + + // Switch Editor + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '2_switch_editor', + command: { + id: 'workbench.action.nextEditor', + title: nls.localize({ key: 'miNextEditor', comment: ['&& denotes a mnemonic'] }, "&&Next Editor") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '2_switch_editor', + command: { + id: 'workbench.action.previousEditor', + title: nls.localize({ key: 'miPreviousEditor', comment: ['&& denotes a mnemonic'] }, "&&Previous Editor") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '2_switch_editor', + command: { + id: 'workbench.action.openNextRecentlyUsedEditorInGroup', + title: nls.localize({ key: 'miNextEditorInGroup', comment: ['&& denotes a mnemonic'] }, "&&Next Used Editor in Group") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '2_switch_editor', + command: { + id: 'workbench.action.openPreviousRecentlyUsedEditorInGroup', + title: nls.localize({ key: 'miPreviousEditorInGroup', comment: ['&& denotes a mnemonic'] }, "&&Previous Used Editor in Group") + }, + order: 4 + }); + + // Switch Group + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '3_switch_group', + command: { + id: 'workbench.action.focusNextGroup', + title: nls.localize({ key: 'miNextGroup', comment: ['&& denotes a mnemonic'] }, "&&Next Group") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '3_switch_group', + command: { + id: 'workbench.action.focusPreviousGroup', + title: nls.localize({ key: 'miPreviousGroup', comment: ['&& denotes a mnemonic'] }, "&&Previous Group") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '3_switch_group', + command: { + id: 'workbench.action.focusLeftGroup', + title: nls.localize({ key: 'miFocusLeftGroup', comment: ['&& denotes a mnemonic'] }, "Group &&Left") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '3_switch_group', + command: { + id: 'workbench.action.focusRightGroup', + title: nls.localize({ key: 'miFocusRightGroup', comment: ['&& denotes a mnemonic'] }, "Group &&Right") + }, + order: 4 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '3_switch_group', + command: { + id: 'workbench.action.focusAboveGroup', + title: nls.localize({ key: 'miFocusAboveGroup', comment: ['&& denotes a mnemonic'] }, "Group &&Above") + }, + order: 5 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: '3_switch_group', + command: { + id: 'workbench.action.focusBelowGroup', + title: nls.localize({ key: 'miFocusBelowGroup', comment: ['&& denotes a mnemonic'] }, "Group &&Below") + }, + order: 6 + }); + + // Go to + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: 'z_go_to', + command: { + id: 'workbench.action.quickOpen', + title: nls.localize({ key: 'miGotoFile', comment: ['&& denotes a mnemonic'] }, "Go to &&File...") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: 'z_go_to', + command: { + id: 'workbench.action.gotoSymbol', + title: nls.localize({ key: 'miGotoSymbolInFile', comment: ['&& denotes a mnemonic'] }, "Go to &&Symbol in File...") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: 'z_go_to', + command: { + id: 'workbench.action.showAllSymbols', + title: nls.localize({ key: 'miGotoSymbolInWorkspace', comment: ['&& denotes a mnemonic'] }, "Go to Symbol in &&Workspace...") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: 'z_go_to', + command: { + id: 'editor.action.goToDeclaration', + title: nls.localize({ key: 'miGotoDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Definition") + }, + order: 4 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: 'z_go_to', + command: { + id: 'editor.action.goToTypeDefinition', + title: nls.localize({ key: 'miGotoDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Definition") + }, + order: 5 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: 'z_go_to', + command: { + id: 'editor.action.goToImplementation', + title: nls.localize({ key: 'miGotoImplementation', comment: ['&& denotes a mnemonic'] }, "Go to &&Implementation") + }, + order: 6 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { + group: 'z_go_to', + command: { + id: 'workbench.action.gotoLine', + title: nls.localize({ key: 'miGotoLine', comment: ['&& denotes a mnemonic'] }, "Go to &&Line...") + }, + order: 7 + }); +} + +function debugMenuRegistration() { + // Start/Stop Debug + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '1_debug', + command: { + id: 'workbench.action.debug.start', + title: nls.localize({ key: 'miStartDebugging', comment: ['&& denotes a mnemonic'] }, "&&Start Debugging") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '1_debug', + command: { + id: 'workbench.action.debug.run', + title: nls.localize({ key: 'miStartWithoutDebugging', comment: ['&& denotes a mnemonic'] }, "Start &&Without Debugging") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '1_debug', + command: { + id: 'workbench.action.debug.stop', + title: nls.localize({ key: 'miStopDebugging', comment: ['&& denotes a mnemonic'] }, "&&Stop Debugging") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '1_debug', + command: { + id: 'workbench.action.debug.restart', + title: nls.localize({ key: 'miRestart Debugging', comment: ['&& denotes a mnemonic'] }, "&&Restart Debugging") + }, + order: 4 + }); + + // Configuration + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '2_configuration', + command: { + id: 'workbench.action.debug.configure', + title: nls.localize({ key: 'miOpenConfigurations', comment: ['&& denotes a mnemonic'] }, "Open &&Configurations") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '2_configuration', + command: { + id: 'debug.addConfiguration', + title: nls.localize({ key: 'miAddConfiguration', comment: ['&& denotes a mnemonic'] }, "Add Configuration...") + }, + order: 2 + }); + + // Step Commands + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '3_step', + command: { + id: 'workbench.action.debug.stepOver', + title: nls.localize({ key: 'miStepOver', comment: ['&& denotes a mnemonic'] }, "Step &&Over") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '3_step', + command: { + id: 'workbench.action.debug.stepInto', + title: nls.localize({ key: 'miStepInto', comment: ['&& denotes a mnemonic'] }, "Step &&Into") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '3_step', + command: { + id: 'workbench.action.debug.stepOut', + title: nls.localize({ key: 'miStepOut', comment: ['&& denotes a mnemonic'] }, "Step O&&ut") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '3_step', + command: { + id: 'workbench.action.debug.continue', + title: nls.localize({ key: 'miContinue', comment: ['&& denotes a mnemonic'] }, "&&Continue") + }, + order: 4 + }); + + // New Breakpoints + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '4_new_breakpoint', + command: { + id: 'editor.debug.action.toggleBreakpoint', + title: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '4_new_breakpoint', + command: { + id: 'editor.debug.action.conditionalBreakpoint', + title: nls.localize({ key: 'miConditionalBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Conditional Breakpoint...") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '4_new_breakpoint', + command: { + id: 'editor.debug.action.toggleInlineBreakpoint', + title: nls.localize({ key: 'miInlineBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle Inline Breakp&&oint") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '4_new_breakpoint', + command: { + id: 'workbench.debug.viewlet.action.addFunctionBreakpointAction', + title: nls.localize({ key: 'miFunctionBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Function Breakpoint...") + }, + order: 4 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '4_new_breakpoint', + command: { + id: 'editor.debug.action.toggleLogPoint', + title: nls.localize({ key: 'miLogPoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Logpoint...") + }, + order: 5 + }); + + // Modify Breakpoints + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '5_breakpoints', + command: { + id: 'workbench.debug.viewlet.action.enableAllBreakpoints', + title: nls.localize({ key: 'miEnableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Enable All Breakpoints") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '5_breakpoints', + command: { + id: 'workbench.debug.viewlet.action.disableAllBreakpoints', + title: nls.localize({ key: 'miDisableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Disable A&&ll Breakpoints") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { + group: '5_breakpoints', + command: { + id: 'workbench.debug.viewlet.action.removeAllBreakpoints', + title: nls.localize({ key: 'miRemoveAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Remove &&All Breakpoints") + }, + order: 3 + }); + +} + +function tasksMenuRegistration() { + // Run Tasks + MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { + group: '1_run', + command: { + id: 'workbench.action.tasks.runTask', + title: nls.localize({ key: 'miRunTask', comment: ['&& denotes a mnemonic'] }, "&&Run Task...") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { + group: '1_run', + command: { + id: 'workbench.action.tasks.build', + title: nls.localize({ key: 'miBuildTask', comment: ['&& denotes a mnemonic'] }, "Run &&Build Task...") + }, + order: 2 + }); + + // Manage Tasks + MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { + group: '2_manage', + command: { + id: 'workbench.action.tasks.showTasks', + title: nls.localize({ key: 'miRunningTask', comment: ['&& denotes a mnemonic'] }, "Show Runnin&&g Tasks...") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { + group: '2_manage', + command: { + id: 'workbench.action.tasks.restartTask', + title: nls.localize({ key: 'miRestartTask', comment: ['&& denotes a mnemonic'] }, "R&&estart Running Task...") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { + group: '2_manage', + command: { + id: 'workbench.action.tasks.terminate', + title: nls.localize({ key: 'miTerminateTask', comment: ['&& denotes a mnemonic'] }, "&&Terminate Task...") + }, + order: 3 + }); + + // Configure Tasks + MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { + group: '3_configure', + command: { + id: 'workbench.action.tasks.configureTaskRunner', + title: nls.localize({ key: 'miConfigureTask', comment: ['&& denotes a mnemonic'] }, "&&Configure Tasks...") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarTasksMenu, { + group: '3_configure', + command: { + id: 'workbench.action.tasks.configureDefaultBuildTask', + title: nls.localize({ key: 'miConfigureBuildTask', comment: ['&& denotes a mnemonic'] }, "Configure De&&fault Build Task...") + }, + order: 2 + }); + +} + +function windowMenuRegistration() { + +} + +function preferencesMenuRegistration() { + MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '1_settings', + command: { + id: 'workbench.action.openSettings', + title: nls.localize({ key: 'miOpenSettings', comment: ['&& denotes a mnemonic'] }, "&&Settings") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '2_keybindings', + command: { + id: 'workbench.action.openGlobalKeybindings', + title: nls.localize({ key: 'miOpenKeymap', comment: ['&& denotes a mnemonic'] }, "&&Keyboard Shortcuts") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '2_keybindings', + command: { + id: 'workbench.extensions.action.showRecommendedKeymapExtensions', + title: nls.localize({ key: 'miOpenKeymapExtensions', comment: ['&& denotes a mnemonic'] }, "&&Keymap Extensions") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '3_snippets', + command: { + id: 'workbench.action.openSnippets', + title: nls.localize({ key: 'miOpenSnippets', comment: ['&& denotes a mnemonic'] }, "User &&Snippets") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '4_themes', + command: { + id: 'workbench.action.selectTheme', + title: nls.localize({ key: 'miSelectColorTheme', comment: ['&& denotes a mnemonic'] }, "&&Color Theme") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '4_themes', + command: { + id: 'workbench.action.selectIconTheme', + title: nls.localize({ key: 'miSelectIconTheme', comment: ['&& denotes a mnemonic'] }, "File &&Icon Theme") + }, + order: 2 + }); +} + +function helpMenuRegistration() { + // Welcome + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '1_welcome', + command: { + id: 'workbench.action.showWelcomePage', + title: nls.localize({ key: 'miWelcome', comment: ['&& denotes a mnemonic'] }, "&&Welcome") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '1_welcome', + command: { + id: 'workbench.action.showInteractivePlayground', + title: nls.localize({ key: 'miInteractivePlayground', comment: ['&& denotes a mnemonic'] }, "&&Interactive Playground") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '1_welcome', + command: { + id: 'workbench.action.openDocumentationUrl', + title: nls.localize({ key: 'miDocumentation', comment: ['&& denotes a mnemonic'] }, "&&Documentation") + }, + order: 3 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '1_welcome', + command: { + id: 'workbench.action.showCurrentReleaseNotes', + title: nls.localize({ key: 'miReleaseNotes', comment: ['&& denotes a mnemonic'] }, "&&Release Notes") + }, + order: 4 + }); + + // Reference + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '2_reference', + command: { + id: 'workbench.action.keybindingsReference', + title: nls.localize({ key: 'miKeyboardShortcuts', comment: ['&& denotes a mnemonic'] }, "&&Keyboard Shortcuts Reference") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '2_reference', + command: { + id: 'workbench.action.openIntroductoryVideosUrl', + title: nls.localize({ key: 'miIntroductoryVideos', comment: ['&& denotes a mnemonic'] }, "Introductory &&Videos") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '2_reference', + command: { + id: 'workbench.action.openTipsAndTricksUrl', + title: nls.localize({ key: 'miTipsAndTricks', comment: ['&& denotes a mnemonic'] }, "&&Tips and Tricks") + }, + order: 3 + }); + + // Feedback + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '3_feedback', + command: { + id: 'openTwitterUrl', + title: nls.localize({ key: 'miTwitter', comment: ['&& denotes a mnemonic'] }, "&&Join us on Twitter") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '3_feedback', + command: { + id: 'openUserVoiceUrl', + title: nls.localize({ key: 'miUserVoice', comment: ['&& denotes a mnemonic'] }, "&&Search Feature Requests") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '3_feedback', + command: { + id: 'openReportIssues', + title: nls.localize({ key: 'miReportIssue', comment: ['&& denotes a mnemonic', 'Translate this to "Report Issue in English" in all languages please!'] }, "Report &&Issue") + }, + order: 3 + }); + + // Legal + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '4_legal', + command: { + id: 'openLicenseUrl', + title: nls.localize({ key: 'miLicense', comment: ['&& denotes a mnemonic'] }, "View &&License") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '4_legal', + command: { + id: 'openPrivacyStatement', + title: nls.localize({ key: 'miPrivacyStatement', comment: ['&& denotes a mnemonic'] }, "&&Privacy Statement") + }, + order: 2 + }); + + // Tools + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '5_tools', + command: { + id: 'workbench.action.toggleDevTools', + title: nls.localize({ key: 'miToggleDevTools', comment: ['&& denotes a mnemonic'] }, "&&Toggle Developer Tools") + }, + order: 1 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '5_tools', + command: { + id: 'workbench.action.openProcessExplorer', + title: nls.localize({ key: 'miOpenProcessExplorerer', comment: ['&& denotes a mnemonic'] }, "Open &&Process Explorer") + }, + order: 2 + }); + + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: '5_tools', + command: { + id: 'accessibilityOptions', + title: nls.localize({ key: 'miAccessibilityOptions', comment: ['&& denotes a mnemonic'] }, "Accessibility &&Options") + }, + order: 3 + }); + + // About + MenuRegistry.appendMenuItem(MenuId.MenubarHelpMenu, { + group: 'z_about', + command: { + id: 'workbench.action.showAboutDialog', + title: nls.localize({ key: 'miAbout', comment: ['&& denotes a mnemonic'] }, "&&About") + }, + order: 1 + }); +} diff --git a/src/vs/workbench/browser/parts/menubar/menubarCommands.ts b/src/vs/workbench/browser/parts/menubar/menubarCommands.ts new file mode 100644 index 0000000000000..5ed135837593a --- /dev/null +++ b/src/vs/workbench/browser/parts/menubar/menubarCommands.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; +import URI from 'vs/base/common/uri'; + +export const FILE_MENU_FAKE_OPEN_FILE_COMMAND_ID = 'workbench.action.fakeOpenFile'; + +export function setup(): void { + registerMenubarCommands(); +} + +function registerMenubarCommands() { + KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: FILE_MENU_FAKE_OPEN_FILE_COMMAND_ID, + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(), + when: void 0, + primary: KeyMod.CtrlCmd | KeyCode.F6, + win: { primary: KeyMod.CtrlCmd | KeyCode.F6 }, + handler: (accessor, resource: URI | object) => { + alert('fake open successful'); + console.log('fake open triggered'); + } + }); +} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/menubar/menubarPart.ts b/src/vs/workbench/browser/parts/menubar/menubarPart.ts new file mode 100644 index 0000000000000..ab7402fff28f5 --- /dev/null +++ b/src/vs/workbench/browser/parts/menubar/menubarPart.ts @@ -0,0 +1,764 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/workbench/browser/parts/menubar/menubar.contribution'; +import 'vs/css!./media/menubarpart'; +import * as nls from 'vs/nls'; +import * as browser from 'vs/base/browser/browser'; +import { Part } from 'vs/workbench/browser/part'; +import { IMenubarService, IMenubarMenu, IMenubarMenuItemAction, IMenubarData } from 'vs/platform/menubar/common/menubar'; +import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IWindowService, MenuBarVisibility } from 'vs/platform/windows/common/windows'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ActionRunner, IActionRunner, IAction } from 'vs/base/common/actions'; +import { Builder, $ } from 'vs/base/browser/builder'; +import { Separator, ActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { EventType, Dimension, toggleClass } from 'vs/base/browser/dom'; +import { TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { isWindows, isMacintosh } from 'vs/base/common/platform'; +import { Menu, IMenuOptions } from 'vs/base/browser/ui/menu/menu'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; +import { Color } from 'vs/base/common/color'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { domEvent } from 'vs/base/browser/event'; + +interface CustomMenu { + title: string; + titleElement: Builder; + actions?: IAction[]; +} + +export class MenubarPart extends Part { + + private keys = [ + 'files.autoSave', + 'window.menuBarVisibility', + 'editor.multiCursorModifier', + 'workbench.sideBar.location', + 'workbench.statusBar.visible', + 'workbench.activityBar.visible', + 'window.enableMenuBarMnemonics', + // 'window.nativeTabs' + ]; + + private topLevelMenus: { + 'File': IMenu; + 'Edit': IMenu; + 'Recent': IMenu; + 'Selection': IMenu; + 'View': IMenu; + 'Layout': IMenu; + 'Go': IMenu; + 'Debug': IMenu; + 'Tasks': IMenu; + 'Window'?: IMenu; + 'Preferences': IMenu; + 'Help': IMenu; + [index: string]: IMenu; + }; + + private topLevelTitles = { + 'File': nls.localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File"), + 'Edit': nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit"), + 'Recent': nls.localize({ key: 'mRecent', comment: ['&& denotes a mnemonic'] }, "&&Recent"), + 'Selection': nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection"), + 'View': nls.localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View"), + 'Layout': nls.localize({ key: 'mLayout', comment: ['&& denotes a mnemonic'] }, "&&Layout"), + 'Go': nls.localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go"), + 'Debug': nls.localize({ key: 'mDebug', comment: ['&& denotes a mnemonic'] }, "&&Debug"), + 'Tasks': nls.localize({ key: 'mTasks', comment: ['&& denotes a mnemonic'] }, "&&Tasks"), + 'Preferences': nls.localize({ key: 'mPreferences', comment: ['&& denotes a mnemonic'] }, "&&Preferences"), + 'Help': nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help") + }; + + private mnemonics: { + [index: number]: number; + } = {}; + + private focusedMenu: { + index: number; + holder: Builder; + widget: Menu; + }; + + private customMenus: CustomMenu[]; + + private actionRunner: IActionRunner; + private container: Builder; + private _isFocused: boolean; + private _onVisibilityChange: Emitter; + + private initialSizing: { + menuButtonPaddingLeftRight?: number; + menubarHeight?: number; + menubarPaddingLeft?: number; + menubarPaddingRight?: number; + menubarFontSize?: number; + } = {}; + + constructor( + id: string, + @IThemeService themeService: IThemeService, + @IMenubarService private menubarService: IMenubarService, + @IMenuService private menuService: IMenuService, + @IWindowService private windowService: IWindowService, + @IContextKeyService private contextKeyService: IContextKeyService, + @IKeybindingService private keybindingService: IKeybindingService, + @IConfigurationService private configurationService: IConfigurationService + ) { + super(id, { hasTitle: false }, themeService); + + this.topLevelMenus = { + 'File': this.menuService.createMenu(MenuId.MenubarFileMenu, this.contextKeyService), + 'Edit': this.menuService.createMenu(MenuId.MenubarEditMenu, this.contextKeyService), + 'Recent': this.menuService.createMenu(MenuId.MenubarRecentMenu, this.contextKeyService), + 'Selection': this.menuService.createMenu(MenuId.MenubarSelectionMenu, this.contextKeyService), + 'View': this.menuService.createMenu(MenuId.MenubarViewMenu, this.contextKeyService), + 'Layout': this.menuService.createMenu(MenuId.MenubarLayoutMenu, this.contextKeyService), + 'Go': this.menuService.createMenu(MenuId.MenubarGoMenu, this.contextKeyService), + 'Debug': this.menuService.createMenu(MenuId.MenubarDebugMenu, this.contextKeyService), + 'Tasks': this.menuService.createMenu(MenuId.MenubarTasksMenu, this.contextKeyService), + 'Preferences': this.menuService.createMenu(MenuId.MenubarPreferencesMenu, this.contextKeyService), + 'Help': this.menuService.createMenu(MenuId.MenubarHelpMenu, this.contextKeyService) + }; + + if (isMacintosh) { + this.topLevelMenus['Window'] = this.menuService.createMenu(MenuId.MenubarWindowMenu, this.contextKeyService); + } + + this.actionRunner = new ActionRunner(); + this.actionRunner.onDidBeforeRun(() => { + if (this.focusedMenu && this.focusedMenu.holder) { + this.focusedMenu.holder.hide(); + } + }); + + this._onVisibilityChange = new Emitter(); + + if (isMacintosh || this.currentTitlebarStyleSetting !== 'custom') { + for (let topLevelMenuName of Object.keys(this.topLevelMenus)) { + this.topLevelMenus[topLevelMenuName].onDidChange(() => this.setupMenubar()); + } + this.setupMenubar(); + } + + this.isFocused = false; + + this.registerListeners(); + } + + private get currentEnableMenuBarMnemonics(): boolean { + let enableMenuBarMnemonics = this.configurationService.getValue('window.enableMenuBarMnemonics'); + if (typeof enableMenuBarMnemonics !== 'boolean') { + enableMenuBarMnemonics = true; + } + + return enableMenuBarMnemonics; + } + + private get currentMultiCursorSetting(): string { + return this.configurationService.getValue('editor.multiCursorModifier'); + } + + private get currentAutoSaveSetting(): string { + return this.configurationService.getValue('files.autoSave'); + } + + private get currentSidebarPosition(): string { + return this.configurationService.getValue('workbench.sideBar.location'); + } + + private get currentStatusBarVisibility(): boolean { + let setting = this.configurationService.getValue('workbench.statusBar.visible'); + if (typeof setting !== 'boolean') { + setting = true; + } + + return setting; + } + + private get currentActivityBarVisibility(): boolean { + let setting = this.configurationService.getValue('workbench.activityBar.visible'); + if (typeof setting !== 'boolean') { + setting = true; + } + + return setting; + } + + private get currentMenubarVisibility(): MenuBarVisibility { + return this.configurationService.getValue('window.menuBarVisibility'); + } + + private get currentTitlebarStyleSetting(): string { + return this.configurationService.getValue('window.titleBarStyle'); + } + + private get isFocused(): boolean { + return this._isFocused; + } + + private set isFocused(value: boolean) { + this._isFocused = value; + + if (!this._isFocused && this.currentMenubarVisibility === 'toggle') { + if (this.container) { + this.hideMenubar(); + } + } + } + + private onDidChangeFullscreen(): void { + this.updateStyles(); + } + + private onConfigurationUpdated(event: IConfigurationChangeEvent): void { + if (this.keys.some(key => event.affectsConfiguration(key))) { + this.setupMenubar(); + } + } + + private hideMenubar(): void { + this._onVisibilityChange.fire(new Dimension(0, 0)); + this.container.style('visibility', 'hidden'); + } + + private showMenubar(): void { + this._onVisibilityChange.fire(this.getMenubarItemsDimensions()); + this.container.style('visibility', null); + } + + private onAltKeyToggled(altKeyDown: boolean): void { + if (this.currentMenubarVisibility === 'toggle') { + if (altKeyDown) { + this.showMenubar(); + } else if (!this.isFocused) { + this.hideMenubar(); + } + } + + if (this.currentEnableMenuBarMnemonics && this.customMenus) { + this.customMenus.forEach(customMenu => { + let child = customMenu.titleElement.child(); + if (child) { + let grandChild = child.child(); + if (grandChild) { + grandChild.style('text-decoration', altKeyDown ? 'underline' : null); + } + } + }); + } + } + + private registerListeners(): void { + browser.onDidChangeFullscreen(() => this.onDidChangeFullscreen()); + + // Update when config changes + this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e)); + + // Listen to update service + // this.updateService.onStateChange(() => this.setupMenubar()); + + // Listen to keybindings change + this.keybindingService.onDidUpdateKeybindings(() => this.setupMenubar()); + + AlternativeKeyEmitter.getInstance().event(this.onAltKeyToggled, this); + } + + private setupMenubar(): void { + if (!isMacintosh && this.currentTitlebarStyleSetting === 'custom') { + this.setupCustomMenubar(); + } else { + this.setupNativeMenubar(); + } + } + + private setupNativeMenubar(): void { + // TODO@sbatten: Remove once native menubar is ready + if (isMacintosh && isWindows) { + this.menubarService.updateMenubar(this.windowService.getCurrentWindowId(), this.getMenubarMenus()); + } + } + + private registerMnemonic(topLevelElement: HTMLElement, mnemonic: string): void { + topLevelElement.accessKey = mnemonic.toLocaleLowerCase(); + } + + private setCheckedStatus(action: IAction | IMenubarMenuItemAction) { + switch (action.id) { + case 'workbench.action.toggleAutoSave': + action.checked = this.currentAutoSaveSetting !== 'off'; + break; + + default: + break; + } + } + + private calculateActionLabel(action: IAction | IMenubarMenuItemAction): string { + let label = action.label; + switch (action.id) { + case 'workbench.action.toggleMultiCursorModifier': + if (this.currentMultiCursorSetting === 'ctrlCmd') { + label = nls.localize('miMultiCursorAlt', "Switch to Alt+Click for Multi-Cursor"); + } else { + label = isMacintosh + ? nls.localize('miMultiCursorCmd', "Switch to Cmd+Click for Multi-Cursor") + : nls.localize('miMultiCursorCtrl', "Switch to Ctrl+Click for Multi-Cursor"); + } + break; + + case 'workbench.action.toggleSidebarPosition': + if (this.currentSidebarPosition !== 'right') { + label = nls.localize({ key: 'miMoveSidebarRight', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Right"); + } else { + label = nls.localize({ key: 'miMoveSidebarLeft', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Left"); + } + break; + + case 'workbench.action.toggleStatusbarVisibility': + if (this.currentStatusBarVisibility) { + label = nls.localize({ key: 'miHideStatusbar', comment: ['&& denotes a mnemonic'] }, "&&Hide Status Bar"); + } else { + label = nls.localize({ key: 'miShowStatusbar', comment: ['&& denotes a mnemonic'] }, "&&Show Status Bar"); + } + break; + + case 'workbench.action.toggleActivityBarVisibility': + if (this.currentActivityBarVisibility) { + label = nls.localize({ key: 'miHideActivityBar', comment: ['&& denotes a mnemonic'] }, "Hide &&Activity Bar"); + } else { + label = nls.localize({ key: 'miShowActivityBar', comment: ['&& denotes a mnemonic'] }, "Show &&Activity Bar"); + } + break; + + default: + break; + } + + return this.currentEnableMenuBarMnemonics ? label : label.replace(/&&(.)/g, '$1'); + } + + private setupCustomMenubar(): void { + this.container.empty(); + this.container.attr('role', 'menubar'); + + this.customMenus = []; + + let idx = 0; + + for (let menuTitle of Object.keys(this.topLevelMenus)) { + const menu: IMenu = this.topLevelMenus[menuTitle]; + let menuIndex = idx++; + + let titleElement = $(this.container).div({ class: 'menubar-menu-button' }); + let displayTitle = this.topLevelTitles[menuTitle].replace(/&&(.)/g, this.currentEnableMenuBarMnemonics ? '$1' : '$1'); + let legibleTitle = this.topLevelTitles[menuTitle].replace(/&&(.)/g, '$1'); + $(titleElement).div({ class: 'menubar-menu-title', 'aria-hidden': true }).innerHtml(displayTitle); + + titleElement.attr('aria-label', legibleTitle); + titleElement.attr('role', 'menu'); + + let mnemonic = (/&&(.)/g).exec(this.topLevelTitles[menuTitle])[1]; + if (mnemonic) { + this.registerMnemonic(titleElement.getHTMLElement(), mnemonic); + } + + this.customMenus.push({ + title: menuTitle, + titleElement: titleElement + }); + + // Update cached actions array for CustomMenus + const updateActions = () => { + this.customMenus[menuIndex].actions = []; + let groups = menu.getActions(); + for (let group of groups) { + const [, actions] = group; + + actions.map((action: IAction) => { + action.label = this.calculateActionLabel(action); + this.setCheckedStatus(action); + }); + + this.customMenus[menuIndex].actions.push(...actions); + this.customMenus[menuIndex].actions.push(new Separator()); + } + + this.customMenus[menuIndex].actions.pop(); + }; + + menu.onDidChange(updateActions); + updateActions(); + + this.customMenus[menuIndex].titleElement.on(EventType.CLICK, (event) => { + this.toggleCustomMenu(menuIndex); + this.isFocused = !this.isFocused; + }); + + this.customMenus[menuIndex].titleElement.getHTMLElement().onmouseenter = () => { + if (this.isFocused && !this.isCurrentMenu(menuIndex)) { + this.toggleCustomMenu(menuIndex); + } + }; + + this.customMenus[menuIndex].titleElement.getHTMLElement().onmouseleave = () => { + if (!this.isFocused) { + this.cleanupCustomMenu(); + } + }; + + this.customMenus[menuIndex].titleElement.getHTMLElement().onblur = () => { + this.cleanupCustomMenu(); + }; + } + + this.container.off(EventType.KEY_DOWN); + this.container.on(EventType.KEY_DOWN, (e) => { + let event = new StandardKeyboardEvent(e as KeyboardEvent); + let eventHandled = true; + + if (event.equals(KeyCode.LeftArrow)) { + this.focusPrevious(); + } else if (event.equals(KeyCode.RightArrow)) { + this.focusNext(); + } else if (event.altKey && event.keyCode && this.mnemonics[event.keyCode] !== undefined && !this.focusedMenu) { + this.toggleCustomMenu(this.mnemonics[event.keyCode]); + } else { + eventHandled = false; + } + + if (eventHandled) { + event.preventDefault(); + event.stopPropagation(); + } + }); + } + + private focusPrevious(): void { + if (!this.focusedMenu) { + return; + } + + let newFocusedIndex = (this.focusedMenu.index - 1 + this.customMenus.length) % this.customMenus.length; + + if (newFocusedIndex === this.focusedMenu.index) { + return; + } + + this.toggleCustomMenu(newFocusedIndex); + } + + private focusNext(): void { + if (!this.focusedMenu) { + return; + } + + let newFocusedIndex = (this.focusedMenu.index + 1) % this.customMenus.length; + + if (newFocusedIndex === this.focusedMenu.index) { + return; + } + + this.toggleCustomMenu(newFocusedIndex); + } + + private getMenubarMenus(): IMenubarData { + let ret: IMenubarData = {}; + + for (let topLevelMenuName of Object.keys(this.topLevelMenus)) { + const menu = this.topLevelMenus[topLevelMenuName]; + let menubarMenu: IMenubarMenu = { items: [] }; + let groups = menu.getActions(); + for (let group of groups) { + const [, actions] = group; + + actions.forEach(menuItemAction => { + let menubarMenuItem: IMenubarMenuItemAction = { + id: menuItemAction.id, + label: menuItemAction.label, + checked: menuItemAction.checked, + enabled: menuItemAction.enabled + }; + + this.setCheckedStatus(menubarMenuItem); + menubarMenuItem.label = this.calculateActionLabel(menubarMenuItem); + + menubarMenu.items.push(menubarMenuItem); + }); + + menubarMenu.items.push({ id: 'vscode.menubar.separator' }); + } + + if (menubarMenu.items.length > 0) { + menubarMenu.items.pop(); + } + + ret[topLevelMenuName] = menubarMenu; + } + + return ret; + } + + private isCurrentMenu(menuIndex: number): boolean { + if (!this.focusedMenu) { + return false; + } + + return this.focusedMenu.index === menuIndex; + } + + private cleanupCustomMenu(): void { + if (this.focusedMenu) { + + if (this.focusedMenu.holder) { + $(this.focusedMenu.holder.getHTMLElement().parentElement).removeClass('open'); + this.focusedMenu.holder.dispose(); + } + + if (this.focusedMenu.widget) { + this.focusedMenu.widget.dispose(); + } + } + + this.focusedMenu = null; + } + + public focusCustomMenu(menuTitle: string): void { + this.toggleCustomMenu(0); + } + + private _getActionItem(action: IAction): ActionItem { + const keybinding = this.keybindingService.lookupKeybinding(action.id); + if (keybinding) { + return new ActionItem(action, action, { label: true, keybinding: keybinding.getLabel(), isMenu: true }); + } + return null; + } + + private toggleCustomMenu(menuIndex: number): void { + const customMenu = this.customMenus[menuIndex]; + + if (this.focusedMenu) { + let hiding: boolean = this.isCurrentMenu(menuIndex); + + // Need to cleanup currently displayed menu + this.cleanupCustomMenu(); + + // Hiding this menu + if (hiding) { + return; + } + } + + customMenu.titleElement.domFocus(); + + let menuHolder = $(customMenu.titleElement).div({ class: 'menubar-menu-items-holder' }); + + $(menuHolder.getHTMLElement().parentElement).addClass('open'); + + menuHolder.addClass('menubar-menu-items-holder-open context-view'); + menuHolder.style({ + 'zoom': `${1 / browser.getZoomFactor()}`, + 'top': `${this.container.getClientArea().height * browser.getZoomFactor()}px` + }); + + let menuOptions: IMenuOptions = { + actionRunner: this.actionRunner, + ariaLabel: 'File', + actionItemProvider: (action) => { return this._getActionItem(action); } + }; + + let menuWidget = new Menu(menuHolder.getHTMLElement(), customMenu.actions, menuOptions); + + menuWidget.onDidCancel(() => { + this.cleanupCustomMenu(); + this.isFocused = false; + }); + + menuWidget.onDidBlur(() => { + setTimeout(() => { + this.cleanupCustomMenu(); + this.isFocused = false; + }, 100); + }); + + menuWidget.focus(); + + this.focusedMenu = { + index: menuIndex, + holder: menuHolder, + widget: menuWidget + }; + } + + updateStyles(): void { + super.updateStyles(); + + // Part container + if (this.container) { + const fgColor = this.getColor(TITLE_BAR_ACTIVE_FOREGROUND); + const bgColor = this.getColor(TITLE_BAR_ACTIVE_BACKGROUND); + + this.container.style('color', fgColor); + if (browser.isFullscreen()) { + this.container.style('background-color', bgColor); + } else { + this.container.style('background-color', null); + } + + toggleClass(this.container.getHTMLElement(), 'light', Color.fromHex(bgColor).isLighter()); + } + } + + public get onVisibilityChange(): Event { + return this._onVisibilityChange.event; + } + + public layout(dimension: Dimension): Dimension[] { + // To prevent zooming we need to adjust the font size with the zoom factor + if (this.customMenus) { + if (typeof this.initialSizing.menubarFontSize !== 'number') { + this.initialSizing.menubarFontSize = parseInt(this.container.getComputedStyle().fontSize, 10); + } + + if (typeof this.initialSizing.menubarHeight !== 'number') { + this.initialSizing.menubarHeight = parseInt(this.container.getComputedStyle().height, 10); + } + + if (typeof this.initialSizing.menubarPaddingLeft !== 'number') { + this.initialSizing.menubarPaddingLeft = parseInt(this.container.getComputedStyle().paddingLeft, 10); + } + + if (typeof this.initialSizing.menubarPaddingRight !== 'number') { + this.initialSizing.menubarPaddingRight = parseInt(this.container.getComputedStyle().paddingRight, 10); + } + + if (typeof this.initialSizing.menuButtonPaddingLeftRight !== 'number') { + this.initialSizing.menuButtonPaddingLeftRight = parseInt(this.customMenus[0].titleElement.getComputedStyle().paddingLeft, 10); + } + + this.container.style({ + height: `${this.initialSizing.menubarHeight / browser.getZoomFactor()}px`, + 'padding-left': `${this.initialSizing.menubarPaddingLeft / browser.getZoomFactor()}px`, + 'padding-right': `${this.initialSizing.menubarPaddingRight / browser.getZoomFactor()}px`, + 'font-size': `${this.initialSizing.menubarFontSize / browser.getZoomFactor()}px`, + }); + + this.customMenus.forEach(customMenu => { + customMenu.titleElement.style({ + 'padding': `0 ${this.initialSizing.menuButtonPaddingLeftRight / browser.getZoomFactor()}px` + }); + }); + } + + if (this.currentMenubarVisibility === 'toggle') { + this.hideMenubar(); + } else { + this.showMenubar(); + } + + return super.layout(dimension); + } + + public getMenubarItemsDimensions(): Dimension { + if (this.customMenus) { + const left = this.customMenus[0].titleElement.getHTMLElement().getBoundingClientRect().left; + const right = this.customMenus[this.customMenus.length - 1].titleElement.getHTMLElement().getBoundingClientRect().right; + return new Dimension(right - left, this.container.getClientArea().height); + } + + return new Dimension(0, 0); + } + + public createContentArea(parent: HTMLElement): HTMLElement { + this.container = $(parent); + + if (!isWindows) { + return this.container.getHTMLElement(); + } + + // Build the menubar + if (this.container) { + this.setupMenubar(); + } + + return this.container.getHTMLElement(); + } +} + +class AlternativeKeyEmitter extends Emitter { + + private _subscriptions: IDisposable[] = []; + private _isPressed: boolean; + private static instance: AlternativeKeyEmitter; + private _suppressAltKeyUp: boolean = false; + + private constructor() { + super(); + + this._subscriptions.push(domEvent(document.body, 'keydown')(e => { + if (e.altKey) { + this.isPressed = true; + } + })); + this._subscriptions.push(domEvent(document.body, 'keyup')(e => { + if (this.isPressed && !e.altKey) { + if (this._suppressAltKeyUp) { + e.preventDefault(); + } + + this._suppressAltKeyUp = false; + this.isPressed = false; + } + })); + this._subscriptions.push(domEvent(document.body, 'mouseleave')(e => { + if (this.isPressed) { + this.isPressed = false; + } + })); + + this._subscriptions.push(domEvent(document.body, 'blur')(e => { + if (this.isPressed) { + this.isPressed = false; + } + })); + } + + get isPressed(): boolean { + return this._isPressed; + } + + set isPressed(value: boolean) { + this._isPressed = value; + this.fire(this._isPressed); + } + + suppressAltKeyUp() { + // Sometimes the native alt behavior needs to be suppresed since the alt was already used as an alternative key + // Example: windows behavior to toggle tha top level menu #44396 + this._suppressAltKeyUp = true; + } + + static getInstance() { + if (!AlternativeKeyEmitter.instance) { + AlternativeKeyEmitter.instance = new AlternativeKeyEmitter(); + } + + return AlternativeKeyEmitter.instance; + } + + dispose() { + super.dispose(); + this._subscriptions = dispose(this._subscriptions); + } +} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/chrome-close-dark.svg b/src/vs/workbench/browser/parts/titlebar/media/chrome-close-dark.svg new file mode 100644 index 0000000000000..bb243036bb574 --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/media/chrome-close-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/chrome-close.svg b/src/vs/workbench/browser/parts/titlebar/media/chrome-close.svg new file mode 100644 index 0000000000000..7abec27cd9767 --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/media/chrome-close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/chrome-maximize-dark.svg b/src/vs/workbench/browser/parts/titlebar/media/chrome-maximize-dark.svg new file mode 100644 index 0000000000000..b6645e8c82945 --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/media/chrome-maximize-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/chrome-maximize.svg b/src/vs/workbench/browser/parts/titlebar/media/chrome-maximize.svg new file mode 100644 index 0000000000000..781322be05f43 --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/media/chrome-maximize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/chrome-minimize-dark.svg b/src/vs/workbench/browser/parts/titlebar/media/chrome-minimize-dark.svg new file mode 100644 index 0000000000000..1f6a7016f8512 --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/media/chrome-minimize-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/chrome-minimize.svg b/src/vs/workbench/browser/parts/titlebar/media/chrome-minimize.svg new file mode 100644 index 0000000000000..80ecf45c9abdc --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/media/chrome-minimize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/chrome-restore-dark.svg b/src/vs/workbench/browser/parts/titlebar/media/chrome-restore-dark.svg new file mode 100644 index 0000000000000..d9f814370b0f6 --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/media/chrome-restore-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/chrome-restore.svg b/src/vs/workbench/browser/parts/titlebar/media/chrome-restore.svg new file mode 100644 index 0000000000000..3ab78151c1794 --- /dev/null +++ b/src/vs/workbench/browser/parts/titlebar/media/chrome-restore.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 12c110e78fe61..fc61158b914e9 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -24,8 +24,127 @@ flex: 0 1 auto; overflow: hidden; white-space: nowrap; - line-height: 22px; text-overflow: ellipsis; -webkit-app-region: drag; zoom: 1; /* prevent zooming */ +} + +/* Windows/Linux: Rules for custom title (icon, window controls) */ + +.monaco-workbench.windows > .part.titlebar, +.monaco-workbench.linux > .part.titlebar { + padding: 0; + height: 30px; + line-height: 30px; + justify-content: space-between; +} + +.monaco-workbench.windows > .part.titlebar > .resizer, +.monaco-workbench.linux > .part.titlebar > .resizer { + -webkit-app-region: no-drag; + position: absolute; + top: 0; + width: 100%; + height: 1px; +} + +.monaco-workbench.windows > .part.titlebar > .window-title, +.monaco-workbench.linux > .part.titlebar > .window-title { + order: 2; +} + +.monaco-workbench > .part.titlebar > .window-appicon { + width: 15px; + padding-left: 10px; + margin-right: 113px; + -webkit-app-region: no-drag; + position: relative; + z-index: 99; + order: 1; + image-rendering: crisp-edges; +} + +.monaco-workbench > .part.titlebar > .window-controls-container { + display: flex; + flex-grow: 0; + flex-shrink: 0; + text-align: center; + position: relative; + z-index: 99; + -webkit-app-region: no-drag; + height: 100%; + width: 138px; + order: 3; +} + +.monaco-workbench > .part.titlebar > .window-controls-container > .window-icon { + display: inline-block; + -webkit-app-region: no-drag; + -webkit-transition: background-color .2s; + transition: background-color .2s; + height: 100%; + width: 33.34%; + background-size: 20%; + background-position: center center; + background-repeat: no-repeat; +} + + +.monaco-workbench > .part.titlebar > .window-controls-container > .window-icon svg { + shape-rendering: crispEdges; + text-align: center; +} + +.monaco-workbench > .part.titlebar.titlebar > .window-controls-container > .window-close { + background-image: url('chrome-close-dark.svg'); + order: 3; +} + +.monaco-workbench > .part.titlebar.titlebar.light > .window-controls-container > .window-close { + background-image: url('chrome-close.svg'); + order: 3; +} + +.monaco-workbench > .part.titlebar.titlebar > .window-controls-container > .window-unmaximize { + background-image: url('chrome-restore-dark.svg'); + order: 2; +} + +.monaco-workbench > .part.titlebar.titlebar.light > .window-controls-container > .window-unmaximize { + background-image: url('chrome-restore.svg'); + order: 2; +} + +.monaco-workbench > .part.titlebar > .window-controls-container > .window-maximize { + background-image: url('chrome-maximize-dark.svg'); + order: 2; +} + +.monaco-workbench > .part.titlebar.light > .window-controls-container > .window-maximize { + background-image: url('chrome-maximize.svg'); + order: 2; +} + +.monaco-workbench > .part.titlebar > .window-controls-container > .window-minimize { + background-image: url('chrome-minimize-dark.svg'); + order: 1; +} + +.monaco-workbench > .part.titlebar.light > .window-controls-container > .window-minimize { + background-image: url('chrome-minimize.svg'); + order: 1; +} + +.monaco-workbench > .part.titlebar > .window-controls-container > .window-icon:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +.monaco-workbench > .part.titlebar.light > .window-controls-container > .window-icon:hover { + background-color: rgba(0, 0, 0, 0.1); +} + +.monaco-workbench > .part.titlebar > .window-controls-container > .window-close:hover, + .monaco-workbench > .part.titlebar.light > .window-controls-container > .window-close:hover { + background-color: rgba(232, 17, 35, 0.9); + background-image: url('chrome-close-dark.svg'); } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index cd7cf25b757cd..229ef8bfe919c 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -12,7 +12,7 @@ import * as paths from 'vs/base/common/paths'; import { Part } from 'vs/workbench/browser/part'; import { ITitleService, ITitleProperties } from 'vs/workbench/services/title/common/titleService'; import { getZoomFactor } from 'vs/base/browser/browser'; -import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; +import { IWindowService, IWindowsService, MenuBarVisibility } from 'vs/platform/windows/common/windows'; import * as errors from 'vs/base/common/errors'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -27,10 +27,12 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_BACKGROUND, TITLE_BAR_BORDER } from 'vs/workbench/common/theme'; -import { isMacintosh, isWindows } from 'vs/base/common/platform'; +import { isMacintosh, isWindows, isLinux } from 'vs/base/common/platform'; import URI from 'vs/base/common/uri'; +import { Color } from 'vs/base/common/color'; import { trim } from 'vs/base/common/strings'; -import { addDisposableListener, EventType, EventHelper, Dimension } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, EventHelper, Dimension, addClass, removeClass } from 'vs/base/browser/dom'; +import { IPartService } from 'vs/workbench/services/part/common/partService'; export class TitlebarPart extends Part implements ITitleService { @@ -44,9 +46,20 @@ export class TitlebarPart extends Part implements ITitleService { private titleContainer: Builder; private title: Builder; + private windowControls: Builder; + private appIcon: Builder; + private pendingTitle: string; - private initialTitleFontSize: number; private representedFileName: string; + private menubarWidth: number; + + private initialSizing: { + titleFontSize?: number; + titlebarHeight?: number; + controlsWidth?: number; + appIconWidth?: number; + appIconLeftPadding?: number; + } = Object.create(null); private isInactive: boolean; @@ -62,6 +75,7 @@ export class TitlebarPart extends Part implements ITitleService { @IEditorService private editorService: IEditorService, @IEnvironmentService private environmentService: IEnvironmentService, @IWorkspaceContextService private contextService: IWorkspaceContextService, + @IPartService private partService: IPartService, @IThemeService themeService: IThemeService ) { super(id, { hasTitle: false }, themeService); @@ -80,6 +94,7 @@ export class TitlebarPart extends Part implements ITitleService { this.toUnbind.push(this.contextService.onDidChangeWorkspaceFolders(() => this.setTitle(this.getWindowTitle()))); this.toUnbind.push(this.contextService.onDidChangeWorkbenchState(() => this.setTitle(this.getWindowTitle()))); this.toUnbind.push(this.contextService.onDidChangeWorkspaceName(() => this.setTitle(this.getWindowTitle()))); + this.toUnbind.push(this.partService.onMenubarVisibilityChange(this.onMenubarVisibilityChanged, this)); } private onBlur(): void { @@ -98,6 +113,12 @@ export class TitlebarPart extends Part implements ITitleService { } } + private onMenubarVisibilityChanged(dimension: Dimension): void { + this.menubarWidth = dimension.width; + + this.updateLayout(); + } + private onActiveEditorChange(): void { // Dispose old listeners @@ -228,6 +249,21 @@ export class TitlebarPart extends Part implements ITitleService { public createContentArea(parent: HTMLElement): HTMLElement { this.titleContainer = $(parent); + // App Icon (Windows/Linux) + if (!isMacintosh) { + this.appIcon = $(this.titleContainer).img({ + class: 'window-appicon', + src: paths.join(this.environmentService.appRoot, isWindows ? 'resources/win32/code.ico' : 'resources/linux/code.png') + }).on(EventType.DBLCLICK, e => { + EventHelper.stop(e, true); + + this.windowService.closeWindow().then(null, errors.onUnexpectedError); + }); + + // Resizer + $(this.titleContainer).div({ class: 'resizer' }); + } + // Title this.title = $(this.titleContainer).div({ class: 'window-title' }); if (this.pendingTitle) { @@ -250,6 +286,36 @@ export class TitlebarPart extends Part implements ITitleService { } }); + // Window Controls (Windows/Linux) + if (!isMacintosh) { + this.windowControls = $(this.titleContainer).div({ class: 'window-controls-container' }); + + // Minimize + $(this.windowControls).div({ class: 'window-icon window-minimize' }).on(EventType.CLICK, () => { + this.windowService.minimizeWindow().then(null, errors.onUnexpectedError); + }); + + // Restore + $(this.windowControls).div({ class: 'window-icon window-max-restore' }).on(EventType.CLICK, () => { + this.windowService.isMaximized().then((maximized) => { + if (maximized) { + return this.windowService.unmaximizeWindow(); + } + + return this.windowService.maximizeWindow(); + }).then(null, errors.onUnexpectedError); + }); + + // Close + $(this.windowControls).div({ class: 'window-icon window-close' }).on(EventType.CLICK, () => { + this.windowService.closeWindow().then(null, errors.onUnexpectedError); + }); + + const isMaximized = this.windowService.getConfiguration().maximized ? true : false; + this.onDidChangeMaximized(isMaximized); + this.windowService.onDidChangeMaximize(this.onDidChangeMaximized, this); + } + // Since the title area is used to drag the window, we do not want to steal focus from the // currently active element. So we restore focus after a timeout back to where it was. this.titleContainer.on([EventType.MOUSE_DOWN], () => { @@ -264,13 +330,36 @@ export class TitlebarPart extends Part implements ITitleService { return this.titleContainer.getHTMLElement(); } + private onDidChangeMaximized(maximized: boolean) { + const element = $(this.titleContainer).getHTMLElement().querySelector('.window-max-restore') as HTMLElement; + if (!element) { + return; + } + + if (maximized) { + removeClass(element, 'window-maximize'); + addClass(element, 'window-unmaximize'); + } else { + removeClass(element, 'window-unmaximize'); + addClass(element, 'window-maximize'); + } + } + protected updateStyles(): void { super.updateStyles(); // Part container if (this.titleContainer) { - this.titleContainer.style('color', this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_FOREGROUND : TITLE_BAR_ACTIVE_FOREGROUND)); - this.titleContainer.style('background-color', this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_BACKGROUND : TITLE_BAR_ACTIVE_BACKGROUND)); + const titleBackground = this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_BACKGROUND : TITLE_BAR_ACTIVE_BACKGROUND); + this.titleContainer.style('background-color', titleBackground); + if (Color.fromHex(titleBackground).isLighter()) { + this.titleContainer.addClass('light'); + } else { + this.titleContainer.removeClass('light'); + } + + const titleForeground = this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_FOREGROUND : TITLE_BAR_ACTIVE_FOREGROUND); + this.titleContainer.style('color', titleForeground); const titleBorder = this.getColor(TITLE_BAR_BORDER); this.titleContainer.style('border-bottom', titleBorder ? `1px solid ${titleBorder}` : null); @@ -340,13 +429,85 @@ export class TitlebarPart extends Part implements ITitleService { } } - public layout(dimension: Dimension): Dimension[] { + private updateLayout() { // To prevent zooming we need to adjust the font size with the zoom factor - if (typeof this.initialTitleFontSize !== 'number') { - this.initialTitleFontSize = parseInt(this.titleContainer.getComputedStyle().fontSize, 10); + if (typeof this.initialSizing.titleFontSize !== 'number') { + this.initialSizing.titleFontSize = parseInt(this.titleContainer.getComputedStyle().fontSize, 10); + } + + if (typeof this.initialSizing.titlebarHeight !== 'number') { + this.initialSizing.titlebarHeight = parseInt(this.titleContainer.getComputedStyle().height, 10); + } + + // Set font size and line height + const newHeight = this.initialSizing.titlebarHeight / getZoomFactor(); + this.titleContainer.style({ + fontSize: `${this.initialSizing.titleFontSize / getZoomFactor()}px`, + 'line-height': `${newHeight}px` + }); + + // Windows/Linux specific layout + if (isWindows || isLinux) { + if (typeof this.initialSizing.controlsWidth !== 'number') { + this.initialSizing.controlsWidth = parseInt(this.windowControls.getComputedStyle().width, 10); + } + + if (typeof this.initialSizing.appIconWidth !== 'number') { + this.initialSizing.appIconWidth = parseInt(this.appIcon.getComputedStyle().width, 10); + } + + if (typeof this.initialSizing.appIconLeftPadding !== 'number') { + this.initialSizing.appIconLeftPadding = parseInt(this.appIcon.getComputedStyle().paddingLeft, 10); + } + + const currentAppIconHeight = parseInt(this.appIcon.getComputedStyle().height, 10); + const newControlsWidth = this.initialSizing.controlsWidth / getZoomFactor(); + const newAppIconWidth = this.initialSizing.appIconWidth / getZoomFactor(); + const newAppIconPaddingLeft = this.initialSizing.appIconLeftPadding / getZoomFactor(); + + if (!this.menubarWidth) { + this.menubarWidth = 0; + } + + // If we can center the title in the titlebar, we should + const fullWidth = parseInt(this.titleContainer.getComputedStyle().width, 10); + const titleWidth = parseInt(this.title.getComputedStyle().width, 10); + const freeSpace = fullWidth - newAppIconWidth - newControlsWidth - titleWidth; + const leftSideTitle = newAppIconWidth + (freeSpace / 2); + + let bufferWidth = this.menubarWidth; + if (newAppIconWidth + this.menubarWidth < leftSideTitle) { + bufferWidth = 0; + } + + // Adjust app icon mimic menubar + this.appIcon.style({ + width: `${newAppIconWidth}px`, + 'padding-left': `${newAppIconPaddingLeft}px`, + 'margin-right': `${newControlsWidth - newAppIconWidth - newAppIconPaddingLeft + bufferWidth}px`, + 'padding-top': `${(newHeight - currentAppIconHeight) / 2.0}px`, + 'padding-bottom': `${(newHeight - currentAppIconHeight) / 2.0}px` + }); + + // Adjust windows controls + this.windowControls.style({ + 'width': `${newControlsWidth}px` + }); + + // Hide title when toggling menu bar + let menubarToggled = this.configurationService.getValue('window.menuBarVisibility') === 'toggle'; + if (menubarToggled && this.menubarWidth) { + this.title.style('visibility', 'hidden'); + } else { + this.title.style('visibility', null); + } } - this.titleContainer.style({ fontSize: `${this.initialTitleFontSize / getZoomFactor()}px` }); + } + + public layout(dimension: Dimension): Dimension[] { + + this.updateLayout(); return super.layout(dimension); } diff --git a/src/vs/workbench/electron-browser/main.contribution.ts b/src/vs/workbench/electron-browser/main.contribution.ts index 0462a5e555edc..1beaca3cbd98d 100644 --- a/src/vs/workbench/electron-browser/main.contribution.ts +++ b/src/vs/workbench/electron-browser/main.contribution.ts @@ -436,10 +436,9 @@ configurationRegistry.registerConfiguration({ 'window.titleBarStyle': { 'type': 'string', 'enum': ['native', 'custom'], - 'default': 'custom', + 'default': isMacintosh ? 'custom' : 'native', 'scope': ConfigurationScope.APPLICATION, - 'description': nls.localize('titleBarStyle', "Adjust the appearance of the window title bar. Changes require a full restart to apply."), - 'included': isMacintosh + 'description': nls.localize('titleBarStyle', "Adjust the appearance of the window title bar. Changes require a full restart to apply.") }, 'window.nativeTabs': { 'type': 'boolean', diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/main.ts index 087bbe40d2448..a51785aea26ff 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/main.ts @@ -47,6 +47,8 @@ import { IssueChannelClient } from 'vs/platform/issue/common/issueIpc'; import { IIssueService } from 'vs/platform/issue/common/issue'; import { LogLevelSetterChannelClient, FollowerLogService } from 'vs/platform/log/common/logIpc'; import { RelayURLService } from 'vs/platform/url/common/urlService'; +import { MenubarChannelClient } from 'vs/platform/menubar/common/menubarIpc'; +import { IMenubarService } from 'vs/platform/menubar/common/menubar'; gracefulFs.gracefulify(fs); // enable gracefulFs @@ -227,6 +229,9 @@ function createMainProcessServices(mainProcessClient: ElectronIPCClient, configu const issueChannel = mainProcessClient.getChannel('issue'); serviceCollection.set(IIssueService, new SyncDescriptor(IssueChannelClient, issueChannel)); + const menubarChannel = mainProcessClient.getChannel('menubar'); + serviceCollection.set(IMenubarService, new SyncDescriptor(MenubarChannelClient, menubarChannel)); + const workspacesChannel = mainProcessClient.getChannel('workspaces'); serviceCollection.set(IWorkspacesService, new WorkspacesChannelClient(workspacesChannel)); diff --git a/src/vs/workbench/electron-browser/media/shell.css b/src/vs/workbench/electron-browser/media/shell.css index 60dff58c2ab4f..6748e7384c3e2 100644 --- a/src/vs/workbench/electron-browser/media/shell.css +++ b/src/vs/workbench/electron-browser/media/shell.css @@ -65,6 +65,15 @@ cursor: pointer; } +.monaco-shell .monaco-menu .monaco-action-bar.vertical { + padding: .5em 0; +} + +.monaco-shell .monaco-menu .monaco-action-bar.vertical .action-label, +.monaco-shell .monaco-menu .monaco-action-bar.vertical .keybinding { + padding: 0.5em 2em; +} + /* START Keyboard Focus Indication Styles */ .monaco-shell [tabindex="0"]:focus, diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index eb99329ae51c4..397fde34ad5e3 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -30,6 +30,7 @@ import { SidebarPart } from 'vs/workbench/browser/parts/sidebar/sidebarPart'; import { PanelPart } from 'vs/workbench/browser/parts/panel/panelPart'; import { StatusbarPart } from 'vs/workbench/browser/parts/statusbar/statusbarPart'; import { TitlebarPart } from 'vs/workbench/browser/parts/titlebar/titlebarPart'; +import { MenubarPart } from 'vs/workbench/browser/parts/menubar/menubarPart'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { WorkbenchLayout } from 'vs/workbench/browser/layout'; import { IActionBarRegistry, Extensions as ActionBarExtensions } from 'vs/workbench/browser/actions'; @@ -41,7 +42,8 @@ import { getServices } from 'vs/platform/instantiation/common/extensions'; import { Position, Parts, IPartService, ILayoutOptions, IDimension } from 'vs/workbench/services/part/common/partService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { ContextMenuService } from 'vs/workbench/services/contextview/electron-browser/contextmenuService'; +import { ContextMenuService as NativeContextMenuService } from 'vs/workbench/services/contextview/electron-browser/contextmenuService'; +import { ContextMenuService as HTMLContextMenuService } from 'vs/platform/contextview/browser/contextMenuService'; import { WorkbenchKeybindingService } from 'vs/workbench/services/keybinding/electron-browser/keybindingService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { WorkspaceService, DefaultConfigurationExportHelper } from 'vs/workbench/services/configuration/node/configurationService'; @@ -77,11 +79,11 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; import { LifecycleService } from 'vs/platform/lifecycle/electron-browser/lifecycleService'; -import { IWindowService, IWindowConfiguration as IWindowSettings, IWindowConfiguration, IPath } from 'vs/platform/windows/common/windows'; +import { IWindowService, IWindowConfiguration as IWindowSettings, IWindowConfiguration, IPath, MenuBarVisibility } from 'vs/platform/windows/common/windows'; import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; import { IMenuService, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; import { MenuService } from 'vs/workbench/services/actions/common/menuService'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { OpenRecentAction, ToggleDevToolsAction, ReloadWindowAction, ShowPreviousWindowTab, MoveWindowTabToNewWindow, MergeAllWindowTabs, ShowNextWindowTab, ToggleWindowTabsBar, ReloadWindowWithExtensionsDisabledAction } from 'vs/workbench/electron-browser/actions'; @@ -111,6 +113,9 @@ import { IEditorService, IResourceEditor } from 'vs/workbench/services/editor/co import { IEditorGroupsService, GroupDirection, preferredSideBySideGroupDirection, GroupOrientation } from 'vs/workbench/services/group/common/editorGroupsService'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IExtensionUrlHandler, ExtensionUrlHandler } from 'vs/platform/url/electron-browser/inactiveExtensionUrlHandler'; +import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; import { WorkbenchThemeService } from 'vs/workbench/services/themes/electron-browser/workbenchThemeService'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; @@ -146,7 +151,8 @@ const Identifiers = { SIDEBAR_PART: 'workbench.parts.sidebar', PANEL_PART: 'workbench.parts.panel', EDITOR_PART: 'workbench.parts.editor', - STATUSBAR_PART: 'workbench.parts.statusbar' + STATUSBAR_PART: 'workbench.parts.statusbar', + MENUBAR_PART: 'workbench.parts.menubar' }; function getWorkbenchStateString(state: WorkbenchState): string { @@ -169,6 +175,7 @@ interface IZenMode { export class Workbench extends Disposable implements IPartService { private static readonly sidebarHiddenStorageKey = 'workbench.sidebar.hidden'; + private static readonly menubarVisibilityConfigurationKey = 'window.menuBarVisibility'; private static readonly sidebarRestoreStorageKey = 'workbench.sidebar.restore'; private static readonly panelHiddenStorageKey = 'workbench.panel.hidden'; private static readonly zenModeActiveStorageKey = 'workbench.zenmode.active'; @@ -202,6 +209,7 @@ export class Workbench extends Disposable implements IPartService { private workbenchLayout: WorkbenchLayout; private titlebarPart: TitlebarPart; + private menubarPart: MenubarPart; private activitybarPart: ActivitybarPart; private sidebarPart: SidebarPart; private panelPart: PanelPart; @@ -217,6 +225,7 @@ export class Workbench extends Disposable implements IPartService { private sideBarPosition: Position; private panelPosition: Position; private panelHidden: boolean; + private menubarHidden: boolean; private zenMode: IZenMode; private centeredEditorLayoutActive: boolean; private fontAliasing: FontAliasingOption; @@ -241,7 +250,9 @@ export class Workbench extends Disposable implements IPartService { @IWorkbenchThemeService private themeService: WorkbenchThemeService, @IEnvironmentService private environmentService: IEnvironmentService, @IWindowService private windowService: IWindowService, - @INotificationService private notificationService: NotificationService + @INotificationService private notificationService: NotificationService, + @IContextViewService private contextViewService: ContextViewService, + @ITelemetryService private telemetryService: TelemetryService ) { super(); @@ -345,8 +356,12 @@ export class Workbench extends Disposable implements IPartService { // List serviceCollection.set(IListService, this.instantiationService.createInstance(ListService)); - // Context Menu - serviceCollection.set(IContextMenuService, new SyncDescriptor(ContextMenuService)); + // Use themable context menus when custom titlebar is enabled to match custom menubar + if (!isMacintosh && this.getCustomTitleBarStyle() === 'custom') { + serviceCollection.set(IContextMenuService, new SyncDescriptor(HTMLContextMenuService, null, this.telemetryService, this.notificationService, this.contextViewService)); + } else { + serviceCollection.set(IContextMenuService, new SyncDescriptor(NativeContextMenuService)); + } // Menus/Actions serviceCollection.set(IMenuService, new SyncDescriptor(MenuService)); @@ -393,9 +408,13 @@ export class Workbench extends Disposable implements IPartService { this.titlebarPart = this.instantiationService.createInstance(TitlebarPart, Identifiers.TITLEBAR_PART); this._register(toDisposable(() => this.titlebarPart.shutdown())); serviceCollection.set(ITitleService, this.titlebarPart); + // History serviceCollection.set(IHistoryService, new SyncDescriptor(HistoryService)); + // Menubar + this.menubarPart = this.instantiationService.createInstance(MenubarPart, Identifiers.MENUBAR_PART); + // Backup File Service this.backupFileService = this.instantiationService.createInstance(BackupFileService, this.workbenchParams.configuration.backupPath); serviceCollection.set(IBackupFileService, this.backupFileService); @@ -562,6 +581,9 @@ export class Workbench extends Disposable implements IPartService { this.setActivityBarHidden(newActivityBarHiddenValue, skipLayout); } } + + const newMenubarVisibility = this.configurationService.getValue(Workbench.menubarVisibilityConfigurationKey); + this.setMenubarHidden(newMenubarVisibility, skipLayout); } //#endregion @@ -826,6 +848,10 @@ export class Workbench extends Disposable implements IPartService { // Panel position this.setPanelPositionFromStorageOrConfig(); + // Menubar visibility + const menuBarVisibility = this.configurationService.getValue(Workbench.menubarVisibilityConfigurationKey); + this.setMenubarHidden(menuBarVisibility, true); + // Statusbar visibility const statusBarVisible = this.configurationService.getValue(Workbench.statusbarVisibleConfigurationKey); this.statusBarHidden = !statusBarVisible; @@ -858,12 +884,8 @@ export class Workbench extends Disposable implements IPartService { } private getCustomTitleBarStyle(): 'custom' { - if (!isMacintosh) { - return null; // custom title bar is only supported on Mac currently - } - const isDev = !this.environmentService.isBuilt || this.environmentService.isExtensionDevelopment; - if (isDev) { + if (isMacintosh && isDev) { return null; // not enabled when developing due to https://github.com/electron/electron/issues/3647 } @@ -918,6 +940,7 @@ export class Workbench extends Disposable implements IPartService { this.workbench.getHTMLElement(), { titlebar: this.titlebarPart, + menubar: this.menubarPart, activitybar: this.activitybarPart, editor: this.editorPart, sidebar: this.sidebarPart, @@ -960,6 +983,7 @@ export class Workbench extends Disposable implements IPartService { // Create Parts this.createTitlebarPart(); + this.createMenubarPart(); this.createActivityBarPart(); this.createSidebarPart(); this.createEditorPart(); @@ -983,6 +1007,20 @@ export class Workbench extends Disposable implements IPartService { this.titlebarPart.create(titlebarContainer.getHTMLElement()); } + private createMenubarPart(): void { + const menubarContainer = $(this.workbench).div({ + 'class': ['part', 'menubar'], + id: Identifiers.MENUBAR_PART, + role: 'menubar' + }); + + this.menubarPart.create(menubarContainer.getHTMLElement()); + + this._register(this.menubarPart.onVisibilityChange((dimension => { + this._onMenubarVisibilityChange.fire(dimension); + }))); + } + private createActivityBarPart(): void { const activitybarPartContainer = $(this.workbench) .div({ @@ -1094,9 +1132,12 @@ export class Workbench extends Disposable implements IPartService { //#region IPartService - private _onTitleBarVisibilityChange: Emitter = new Emitter(); + private _onTitleBarVisibilityChange: Emitter = this._register(new Emitter()); get onTitleBarVisibilityChange(): Event { return this._onTitleBarVisibilityChange.event; } + private _onMenubarVisibilityChange: Emitter = this._register(new Emitter()); + get onMenubarVisibilityChange(): Event { return this._onMenubarVisibilityChange.event; } + get onEditorLayout(): Event { return this.editorPart.onDidLayout; } isCreated(): boolean { @@ -1119,6 +1160,9 @@ export class Workbench extends Disposable implements IPartService { case Parts.TITLEBAR_PART: container = this.titlebarPart.getContainer(); break; + case Parts.MENUBAR_PART: + container = this.menubarPart.getContainer(); + break; case Parts.ACTIVITYBAR_PART: container = this.activitybarPart.getContainer(); break; @@ -1142,7 +1186,9 @@ export class Workbench extends Disposable implements IPartService { isVisible(part: Parts): boolean { switch (part) { case Parts.TITLEBAR_PART: - return this.getCustomTitleBarStyle() && !browser.isFullscreen(); + return this.getCustomTitleBarStyle() === 'custom' && !browser.isFullscreen(); + case Parts.MENUBAR_PART: + return this.getCustomTitleBarStyle() === 'custom' && !this.menubarHidden; case Parts.SIDEBAR_PART: return !this.sideBarHidden; case Parts.PANEL_PART: @@ -1436,6 +1482,14 @@ export class Workbench extends Disposable implements IPartService { this.workbenchLayout.layout(); } + setMenubarHidden(visibility: MenuBarVisibility, skipLayout: boolean): void { + this.menubarHidden = visibility === 'hidden' || (visibility === 'default' && browser.isFullscreen()); + + if (!skipLayout) { + this.workbenchLayout.layout(); + } + } + getPanelPosition(): Position { return this.panelPosition; } diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts index d445eaa0684ec..59221606c5112 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.contribution.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; -import { GlobalNewUntitledFileAction, ShowOpenedFileInNewWindow, CopyPathAction, FocusOpenEditorsView, FocusFilesExplorer, GlobalCompareResourcesAction, SaveAllAction, ShowActiveFileInExplorer, CollapseExplorerView, RefreshExplorerView, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler } from 'vs/workbench/parts/files/electron-browser/fileActions'; +import { ToggleAutoSaveAction, GlobalNewUntitledFileAction, ShowOpenedFileInNewWindow, CopyPathAction, FocusOpenEditorsView, FocusFilesExplorer, GlobalCompareResourcesAction, SaveAllAction, ShowActiveFileInExplorer, CollapseExplorerView, RefreshExplorerView, CompareWithClipboardAction, NEW_FILE_COMMAND_ID, NEW_FILE_LABEL, NEW_FOLDER_COMMAND_ID, NEW_FOLDER_LABEL, TRIGGER_RENAME_LABEL, MOVE_FILE_TO_TRASH_LABEL, COPY_FILE_LABEL, PASTE_FILE_LABEL, FileCopiedContext, renameHandler, moveFileToTrashHandler, copyFileHandler, pasteFileHandler, deleteFileHandler } from 'vs/workbench/parts/files/electron-browser/fileActions'; import { revertLocalChangesCommand, acceptLocalChangesCommand, CONFLICT_RESOLUTION_CONTEXT } from 'vs/workbench/parts/files/electron-browser/saveErrorHandler'; import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; @@ -40,6 +40,7 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(RefreshExplorerView, R registry.registerWorkbenchAction(new SyncActionDescriptor(GlobalNewUntitledFileAction, GlobalNewUntitledFileAction.ID, GlobalNewUntitledFileAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_N }), 'File: New Untitled File', category); registry.registerWorkbenchAction(new SyncActionDescriptor(ShowOpenedFileInNewWindow, ShowOpenedFileInNewWindow.ID, ShowOpenedFileInNewWindow.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_O) }), 'File: Open Active File in New Window', category); registry.registerWorkbenchAction(new SyncActionDescriptor(CompareWithClipboardAction, CompareWithClipboardAction.ID, CompareWithClipboardAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_C) }), 'File: Compare Active File with Clipboard', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(ToggleAutoSaveAction, ToggleAutoSaveAction.ID, ToggleAutoSaveAction.LABEL), 'File: Toggle Auto Save', category); // Commands CommandsRegistry.registerCommand('_files.windowOpen', openWindowCommand); diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index 40cd32a481dfb..405be9a3a7ce3 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -8,6 +8,7 @@ import 'vs/css!./media/fileactions'; import { TPromise } from 'vs/base/common/winjs.base'; import * as nls from 'vs/nls'; +import * as types from 'vs/base/common/types'; import { isWindows, isLinux } from 'vs/base/common/platform'; import { sequence, ITask, always } from 'vs/base/common/async'; import * as paths from 'vs/base/common/paths'; @@ -24,7 +25,7 @@ import { ITree, IHighlightEvent } from 'vs/base/parts/tree/browser/tree'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { VIEWLET_ID } from 'vs/workbench/parts/files/common/files'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IFileService, IFileStat } from 'vs/platform/files/common/files'; +import { IFileService, IFileStat, AutoSaveConfiguration } from 'vs/platform/files/common/files'; import { toResource, IUntitledResourceInput } from 'vs/workbench/common/editor'; import { ExplorerItem, Model, NewStatPlaceholder } from 'vs/workbench/parts/files/common/explorerModel'; import { ExplorerView } from 'vs/workbench/parts/files/electron-browser/views/explorerView'; @@ -1185,6 +1186,36 @@ export class RefreshViewExplorerAction extends Action { } } +export class ToggleAutoSaveAction extends Action { + public static readonly ID = 'workbench.action.toggleAutoSave'; + public static readonly LABEL = nls.localize('toggleAutoSave', "Toggle Auto Save"); + + constructor( + id: string, + label: string, + @IConfigurationService private configurationService: IConfigurationService + ) { + super(id, label); + } + + public run(): TPromise { + const setting = this.configurationService.inspect('files.autoSave'); + let userAutoSaveConfig = setting.user; + if (types.isUndefinedOrNull(userAutoSaveConfig)) { + userAutoSaveConfig = setting.default; // use default if setting not defined + } + + let newAutoSaveValue: string; + if ([AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE].some(s => s === userAutoSaveConfig)) { + newAutoSaveValue = AutoSaveConfiguration.OFF; + } else { + newAutoSaveValue = AutoSaveConfiguration.AFTER_DELAY; + } + + return this.configurationService.updateValue('files.autoSave', newAutoSaveValue, ConfigurationTarget.USER); + } +} + export abstract class BaseSaveAllAction extends BaseErrorReportingAction { private toDispose: IDisposable[]; private lastIsDirty: boolean; diff --git a/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts b/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts index 9981d15cb9883..cc504052f716d 100644 --- a/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts +++ b/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts @@ -77,8 +77,8 @@ export class SettingsChangeRelauncher implements IWorkbenchContribution { private onConfigurationChange(config: IConfiguration, notify: boolean): void { let changed = false; - // macOS: Titlebar style - if (isMacintosh && config.window && config.window.titleBarStyle !== this.titleBarStyle && (config.window.titleBarStyle === 'native' || config.window.titleBarStyle === 'custom')) { + // Titlebar style + if (config.window && config.window.titleBarStyle !== this.titleBarStyle && (config.window.titleBarStyle === 'native' || config.window.titleBarStyle === 'custom')) { this.titleBarStyle = config.window.titleBarStyle; changed = true; } diff --git a/src/vs/workbench/services/part/common/partService.ts b/src/vs/workbench/services/part/common/partService.ts index c96d2e0ed701d..e157b37687f5c 100644 --- a/src/vs/workbench/services/part/common/partService.ts +++ b/src/vs/workbench/services/part/common/partService.ts @@ -14,7 +14,8 @@ export enum Parts { PANEL_PART, EDITOR_PART, STATUSBAR_PART, - TITLEBAR_PART + TITLEBAR_PART, + MENUBAR_PART } export enum Position { @@ -43,6 +44,11 @@ export interface IPartService { */ onTitleBarVisibilityChange: Event; + /** + * Emits when the visibility of the menubar changes. + */ + onMenubarVisibilityChange: Event; + /** * Emits when the editor part's layout changes. */ diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index e2dffbee86292..bbd574dd76c02 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -73,6 +73,7 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDecorationRenderOptions } from 'vs/editor/common/editorCommon'; import { EditorGroup } from 'vs/workbench/common/editor/editorGroup'; +import { Dimension } from 'vs/base/browser/dom'; export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, void 0); @@ -374,12 +375,17 @@ export class TestPartService implements IPartService { public _serviceBrand: any; private _onTitleBarVisibilityChange = new Emitter(); + private _onMenubarVisibilityChange = new Emitter(); private _onEditorLayout = new Emitter(); public get onTitleBarVisibilityChange(): Event { return this._onTitleBarVisibilityChange.event; } + public get onMenubarVisibilityChange(): Event { + return this._onMenubarVisibilityChange.event; + } + public get onEditorLayout(): Event { return this._onEditorLayout.event; } @@ -958,11 +964,16 @@ export class TestWindowService implements IWindowService { public _serviceBrand: any; onDidChangeFocus: Event = new Emitter().event; + onDidChangeMaximize: Event; isFocused(): TPromise { return TPromise.as(false); } + isMaximized(): TPromise { + return TPromise.as(false); + } + getConfiguration(): IWindowConfiguration { return Object.create(null); } @@ -1027,6 +1038,18 @@ export class TestWindowService implements IWindowService { return TPromise.as(void 0); } + maximizeWindow(): TPromise { + return TPromise.as(void 0); + } + + unmaximizeWindow(): TPromise { + return TPromise.as(void 0); + } + + minimizeWindow(): TPromise { + return TPromise.as(void 0); + } + openWindow(paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean, forceOpenWorkspaceAsFile?: boolean }): TPromise { return TPromise.as(void 0); } @@ -1104,6 +1127,8 @@ export class TestWindowsService implements IWindowsService { onWindowOpen: Event; onWindowFocus: Event; onWindowBlur: Event; + onWindowMaximize: Event; + onWindowUnmaximize: Event; isFocused(windowId: number): TPromise { return TPromise.as(false); @@ -1189,6 +1214,10 @@ export class TestWindowsService implements IWindowsService { return TPromise.as(void 0); } + minimizeWindow(windowId: number): TPromise { + return TPromise.as(void 0); + } + unmaximizeWindow(windowId: number): TPromise { return TPromise.as(void 0); }