diff --git a/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts b/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts index df0cbe3acfb71..746a45dffd1cf 100644 --- a/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts +++ b/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts @@ -18,7 +18,7 @@ import * as electron from 'electron'; import { inject, injectable } from 'inversify'; import { CommandRegistry, isOSX, ActionMenuNode, CompositeMenuNode, - MAIN_MENU_BAR, MenuModelRegistry, MenuPath + MAIN_MENU_BAR, MenuModelRegistry, MenuPath, ILogger } from '../../common'; import { PreferenceService, KeybindingRegistry, Keybinding } from '../../browser'; import { ContextKeyService } from '../../browser/context-key-service'; @@ -30,6 +30,9 @@ export class ElectronMainMenuFactory { protected _menu: Electron.Menu; protected _toggledCommands: Set = new Set(); + @inject(ILogger) + protected readonly logger: ILogger; + @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService; @@ -57,7 +60,13 @@ export class ElectronMainMenuFactory { createMenuBar(): Electron.Menu { const menuModel = this.menuProvider.getMenu(MAIN_MENU_BAR); + const start = Date.now(); const template = this.fillMenuTemplate([], menuModel); + this.logger.isDebug().then(debug => { + if (debug) { + this.logger.debug(`Recalculated the menu templates in ${Date.now() - start} ms.`); + } + }); if (isOSX) { template.unshift(this.createOSXMenu()); } @@ -122,10 +131,14 @@ export class ElectronMainMenuFactory { throw new Error(`Unknown command with ID: ${commandId}.`); } - const args = anchor ? [anchor] : []; - if (!this.commandRegistry.isVisible(commandId, ...args) - || (!!menu.action.when && !this.contextKeyService.match(menu.action.when))) { - continue; + // Do not render empty groups for the context menu only. + // But we show all disabled items in the main application menu. + if (anchor) { + const args = anchor ? [anchor] : []; + if (!this.commandRegistry.isVisible(commandId, ...args) + || (!!menu.action.when && !this.contextKeyService.match(menu.action.when))) { + continue; + } } const bindings = this.keybindingRegistry.getKeybindingsForCommand(commandId); @@ -143,8 +156,8 @@ export class ElectronMainMenuFactory { label: menu.label, type: this.commandRegistry.getToggledHandler(commandId) ? 'checkbox' : 'normal', checked: this.commandRegistry.isToggled(commandId), - enabled: true, // https://github.com/theia-ide/theia/issues/446 - visible: true, + enabled: this.commandRegistry.isEnabled(commandId), + visible: anchor ? this.commandRegistry.isVisible(commandId) : true, // We filter items for the context menu only. click: () => this.execute(commandId, anchor), accelerator }); diff --git a/packages/core/src/electron-browser/menu/electron-menu-contribution.ts b/packages/core/src/electron-browser/menu/electron-menu-contribution.ts index a92c61d3e7608..fd2e0b5f57a99 100644 --- a/packages/core/src/electron-browser/menu/electron-menu-contribution.ts +++ b/packages/core/src/electron-browser/menu/electron-menu-contribution.ts @@ -18,12 +18,13 @@ import * as electron from 'electron'; import { inject, injectable } from 'inversify'; import { Command, CommandContribution, CommandRegistry, - isOSX, MenuModelRegistry, MenuContribution, Disposable + isOSX, MenuModelRegistry, MenuContribution, Disposable, DisposableCollection } from '../../common'; import { KeybindingContribution, KeybindingRegistry } from '../../browser'; import { FrontendApplication, FrontendApplicationContribution, CommonMenus } from '../../browser'; import { ElectronMainMenuFactory } from './electron-main-menu-factory'; import { FrontendApplicationStateService, FrontendApplicationState } from '../../browser/frontend-application-state'; +import { SelectionService } from '../../common/selection-service'; export namespace ElectronCommands { export const TOGGLE_DEVELOPER_TOOLS: Command = { @@ -71,6 +72,9 @@ export class ElectronMenuContribution implements FrontendApplicationContribution @inject(FrontendApplicationStateService) protected readonly stateService: FrontendApplicationStateService; + @inject(SelectionService) + protected readonly selectionService: SelectionService; + constructor( @inject(ElectronMainMenuFactory) protected readonly factory: ElectronMainMenuFactory ) { } @@ -96,20 +100,33 @@ export class ElectronMenuContribution implements FrontendApplicationContribution // between them as the user switches windows. electron.remote.getCurrentWindow().on('focus', () => this.setMenu()); } + + // Poor man's way to update the Electron menu items dynamically. + // https://github.com/theia-ide/theia/issues/446 + // https://github.com/microsoft/vscode/blob/a6774a6961a802453d7ae985d4f555b8f3e0bb88/src/vs/platform/menubar/electron-main/menubar.ts#L200-L202 + const toDisposeOnClosingWindow = new DisposableCollection(); + + // Listen on current widget changes. + const currentChangedListener = () => this.setMenu(); + app.shell.currentChanged.connect(currentChangedListener); + toDisposeOnClosingWindow.push(Disposable.create(() => app.shell.currentChanged.disconnect(currentChangedListener))); + + // Listen on the selection service. To be able to update the items on the `Navigator` selections, for instance. + toDisposeOnClosingWindow.push(this.selectionService.onSelectionChanged(() => this.setMenu())); + // Make sure the application menu is complete, once the frontend application is ready. // https://github.com/theia-ide/theia/issues/5100 - let onStateChange: Disposable | undefined = undefined; const stateServiceListener = (state: FrontendApplicationState) => { if (state === 'ready') { this.setMenu(); } if (state === 'closing_window') { - if (!!onStateChange) { - onStateChange.dispose(); + if (!toDisposeOnClosingWindow.disposed) { + toDisposeOnClosingWindow.dispose(); } } }; - onStateChange = this.stateService.onStateChanged(stateServiceListener); + toDisposeOnClosingWindow.push(this.stateService.onStateChanged(stateServiceListener)); } private setMenu(menu: electron.Menu = this.factory.createMenuBar(), window: electron.BrowserWindow = electron.remote.getCurrentWindow()): void {