diff --git a/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-contribution.ts b/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-contribution.ts index 9c9bff21aa780..ed4baca3c1231 100644 --- a/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-contribution.ts +++ b/examples/api-samples/src/electron-browser/updater/sample-updater-frontend-contribution.ts @@ -90,7 +90,7 @@ export class ElectronMenuUpdater { this.setMenu(); } - private setMenu(menu: Menu = this.factory.createMenuBar(), electronWindow: BrowserWindow = remote.getCurrentWindow()): void { + private setMenu(menu: Menu | null = this.factory.createMenuBar(), electronWindow: BrowserWindow = remote.getCurrentWindow()): void { if (isOSX) { remote.Menu.setApplicationMenu(menu); } else { diff --git a/packages/core/src/browser/common-frontend-contribution.ts b/packages/core/src/browser/common-frontend-contribution.ts index 3abcb7012c0ab..d764ca7dc9c81 100644 --- a/packages/core/src/browser/common-frontend-contribution.ts +++ b/packages/core/src/browser/common-frontend-contribution.ts @@ -354,16 +354,38 @@ export class CommonFrontendContribution implements FrontendApplicationContributi this.updateThemeFromPreference('workbench.colorTheme'); this.updateThemeFromPreference('workbench.iconTheme'); this.preferences.onPreferenceChanged(e => { - if (e.preferenceName === 'workbench.editor.highlightModifiedTabs') { - this.updateStyles(); - } else if (e.preferenceName === 'workbench.colorTheme' || e.preferenceName === 'workbench.iconTheme') { - this.updateThemeFromPreference(e.preferenceName); + switch (e.preferenceName) { + case 'workbench.editor.highlightModifiedTabs': { + this.updateStyles(); + break; + } + case 'workbench.colorTheme': + case 'workbench.iconTheme': { + this.updateThemeFromPreference(e.preferenceName); + break; + } + case 'window.menuBarVisibility': { + const { newValue } = e; + const mainMenuId = 'main-menu'; + if (newValue === 'compact') { + app.shell.leftPanelHandler.addTopMenu({ + id: mainMenuId, + iconClass: 'codicon codicon-menu', + title: 'Application Menu', + menuPath: ['menubar'], + order: 0, + }); + } else { + app.shell.leftPanelHandler.removeTopMenu(mainMenuId); + } + break; + } } }); this.themeService.onDidColorThemeChange(() => this.updateThemePreference('workbench.colorTheme')); this.iconThemes.onDidChangeCurrent(() => this.updateThemePreference('workbench.iconTheme')); - app.shell.leftPanelHandler.addMenu({ + app.shell.leftPanelHandler.addBottomMenu({ id: 'settings-menu', iconClass: 'codicon codicon-settings-gear', title: 'Settings', @@ -378,11 +400,11 @@ export class CommonFrontendContribution implements FrontendApplicationContributi order: 1, }; this.authenticationService.onDidRegisterAuthenticationProvider(() => { - app.shell.leftPanelHandler.addMenu(accountsMenu); + app.shell.leftPanelHandler.addBottomMenu(accountsMenu); }); this.authenticationService.onDidUnregisterAuthenticationProvider(() => { if (this.authenticationService.getProviderIds().length === 0) { - app.shell.leftPanelHandler.removeMenu(accountsMenu.id); + app.shell.leftPanelHandler.removeBottomMenu(accountsMenu.id); } }); } diff --git a/packages/core/src/browser/core-preferences.ts b/packages/core/src/browser/core-preferences.ts index 1888d91d0438d..1505725da3f50 100644 --- a/packages/core/src/browser/core-preferences.ts +++ b/packages/core/src/browser/core-preferences.ts @@ -97,6 +97,20 @@ export const corePreferenceSchema: PreferenceSchema = { default: 'code', description: 'Whether to interpret keypresses by the `code` of the physical key, or by the `keyCode` provided by the OS.' }, + 'window.menuBarVisibility': { + type: 'string', + enum: ['classic', 'visible', 'hidden', 'compact'], + markdownEnumDescriptions: [ + 'Menu is displayed at the top of the window and only hidden in full screen mode.', + 'Menu is always visible at the top of the window even in full screen mode.', + 'Menu is always hidden.', + 'Menu is displayed as a compact button in the sidebar. This value is ignored when `#window.titleBarStyle#` is `native`.' + ], + default: 'classic', + scope: 'application', + markdownDescription: `Control the visibility of the menu bar. A setting of 'toggle' means that the menu bar is hidden and a single press of the Alt key will show it. + A setting of 'compact' will move the menu into the sidebar.`, + }, } }; @@ -112,6 +126,7 @@ export interface CoreConfiguration { 'workbench.silentNotifications': boolean; 'files.encoding': string 'workbench.tree.renderIndentGuides': 'onHover' | 'none' | 'always'; + 'window.menuBarVisibility': 'classic' | 'visible' | 'hidden' | 'compact'; } export const CorePreferences = Symbol('CorePreferences'); diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index 8ab0b4dd709fc..9283ce978f2fa 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -42,7 +42,7 @@ import { ApplicationShell, ApplicationShellOptions, DockPanelRenderer, TabBarRenderer, TabBarRendererFactory, ShellLayoutRestorer, SidePanelHandler, SidePanelHandlerFactory, - SidebarBottomMenuWidget, SidebarBottomMenuWidgetFactory, + SidebarMenuWidget, SidebarMenuWidgetFactory, SplitPositionHandler, DockPanelRendererFactory, ApplicationShellLayoutMigration, ApplicationShellLayoutMigrationError } from './shell'; import { StatusBar, StatusBarImpl } from './status-bar/status-bar'; @@ -131,8 +131,8 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo bind(ApplicationShell).toSelf().inSingletonScope(); bind(SidePanelHandlerFactory).toAutoFactory(SidePanelHandler); bind(SidePanelHandler).toSelf(); - bind(SidebarBottomMenuWidgetFactory).toAutoFactory(SidebarBottomMenuWidget); - bind(SidebarBottomMenuWidget).toSelf(); + bind(SidebarMenuWidgetFactory).toAutoFactory(SidebarMenuWidget); + bind(SidebarMenuWidget).toSelf(); bind(SplitPositionHandler).toSelf().inSingletonScope(); bindContributionProvider(bind, TabBarToolbarContribution); diff --git a/packages/core/src/browser/menu/browser-menu-plugin.ts b/packages/core/src/browser/menu/browser-menu-plugin.ts index 9187a01355430..f7990bb20e5fa 100644 --- a/packages/core/src/browser/menu/browser-menu-plugin.ts +++ b/packages/core/src/browser/menu/browser-menu-plugin.ts @@ -27,6 +27,7 @@ import { ContextKeyService } from '../context-key-service'; import { ContextMenuContext } from './context-menu-context'; import { waitForRevealed } from '../widgets'; import { ApplicationShell } from '../shell'; +import { CorePreferences } from '../core-preferences'; export abstract class MenuBarWidget extends MenuBar { abstract activateMenu(label: string, ...labels: string[]): Promise; @@ -45,6 +46,9 @@ export class BrowserMainMenuFactory implements MenuWidgetFactory { @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; + @inject(CorePreferences) + protected readonly corePreferences: CorePreferences; + @inject(KeybindingRegistry) protected readonly keybindingRegistry: KeybindingRegistry; @@ -54,15 +58,31 @@ export class BrowserMainMenuFactory implements MenuWidgetFactory { createMenuBar(): MenuBarWidget { const menuBar = new DynamicMenuBarWidget(); menuBar.id = 'theia:menubar'; - this.fillMenuBar(menuBar); - const listener = this.keybindingRegistry.onKeybindingsChanged(() => { - menuBar.clearMenus(); - this.fillMenuBar(menuBar); + const preferenceListener = this.corePreferences.onPreferenceChanged(preference => { + if (preference.preferenceName === 'window.menuBarVisibility') { + this.showMenuBar(menuBar, preference.newValue); + } + }); + const keybindingListener = this.keybindingRegistry.onKeybindingsChanged(() => { + const preference = this.corePreferences['window.menuBarVisibility']; + this.showMenuBar(menuBar, preference); + }); + menuBar.disposed.connect(() => { + preferenceListener.dispose(); + keybindingListener.dispose(); }); - menuBar.disposed.connect(() => listener.dispose()); return menuBar; } + protected showMenuBar(menuBar: DynamicMenuBarWidget, preference: string | undefined): void { + if (preference && ['classic', 'visible'].includes(preference)) { + menuBar.clearMenus(); + this.fillMenuBar(menuBar); + } else { + menuBar.clearMenus(); + } + } + protected fillMenuBar(menuBar: MenuBarWidget): void { const menuModel = this.menuProvider.getMenu(MAIN_MENU_BAR); const menuCommandRegistry = this.createMenuCommandRegistry(menuModel); @@ -348,6 +368,9 @@ export class BrowserMenuBarContribution implements FrontendApplicationContributi @inject(ApplicationShell) protected readonly shell: ApplicationShell; + @inject(CorePreferences) + protected readonly corePreferences: CorePreferences; + constructor( @inject(BrowserMainMenuFactory) protected readonly factory: BrowserMainMenuFactory ) { } diff --git a/packages/core/src/browser/shell/index.ts b/packages/core/src/browser/shell/index.ts index b376c3c672c70..c24c9f758e3b1 100644 --- a/packages/core/src/browser/shell/index.ts +++ b/packages/core/src/browser/shell/index.ts @@ -17,7 +17,7 @@ export * from './application-shell'; export * from './shell-layout-restorer'; export * from './side-panel-handler'; -export * from './sidebar-bottom-menu-widget'; +export * from './sidebar-menu-widget'; export * from './split-panels'; export * from './tab-bars'; export * from './view-contribution'; diff --git a/packages/core/src/browser/shell/side-panel-handler.ts b/packages/core/src/browser/shell/side-panel-handler.ts index 66b8cf3f03d46..7d577bbb54a03 100644 --- a/packages/core/src/browser/shell/side-panel-handler.ts +++ b/packages/core/src/browser/shell/side-panel-handler.ts @@ -21,7 +21,7 @@ import { MimeData } from '@phosphor/coreutils'; import { Drag } from '@phosphor/dragdrop'; import { AttachedProperty } from '@phosphor/properties'; import { TabBarRendererFactory, TabBarRenderer, SHELL_TABBAR_CONTEXT_MENU, SideTabBar } from './tab-bars'; -import { SidebarBottomMenuWidget, SidebarBottomMenuWidgetFactory, SidebarBottomMenu } from './sidebar-bottom-menu-widget'; +import { SidebarMenuWidget, SidebarMenuWidgetFactory, SidebarMenu } from './sidebar-menu-widget'; import { SplitPositionHandler, SplitPositionOptions } from './split-panels'; import { animationFrame } from '../browser'; import { FrontendApplicationStateService } from '../frontend-application-state'; @@ -65,12 +65,18 @@ export class SidePanelHandler { * tab bar itself remains visible as long as there is at least one widget. */ tabBar: SideTabBar; + /** + * The menu placed on the sidebar top. + * Displayed as icons. + * Open menus when on clicks. + */ + topMenu: SidebarMenuWidget; /** * The menu placed on the sidebar bottom. * Displayed as icons. * Open menus when on clicks. */ - bottomMenu: SidebarBottomMenuWidget; + bottomMenu: SidebarMenuWidget; /** * A tool bar, which displays a title and widget specific command buttons. */ @@ -107,7 +113,7 @@ export class SidePanelHandler { @inject(TabBarToolbarRegistry) protected tabBarToolBarRegistry: TabBarToolbarRegistry; @inject(TabBarToolbarFactory) protected tabBarToolBarFactory: () => TabBarToolbar; @inject(TabBarRendererFactory) protected tabBarRendererFactory: () => TabBarRenderer; - @inject(SidebarBottomMenuWidgetFactory) protected sidebarBottomWidgetFactory: () => SidebarBottomMenuWidget; + @inject(SidebarMenuWidgetFactory) protected sidebarWidgetFactory: () => SidebarMenuWidget; @inject(SplitPositionHandler) protected splitPositionHandler: SplitPositionHandler; @inject(FrontendApplicationStateService) protected readonly applicationStateService: FrontendApplicationStateService; @@ -120,8 +126,9 @@ export class SidePanelHandler { create(side: 'left' | 'right', options: SidePanel.Options): void { this.side = side; this.options = options; + this.topMenu = this.createSidebarMenu(); this.tabBar = this.createSideBar(); - this.bottomMenu = this.createSidebarBottomMenu(); + this.bottomMenu = this.createSidebarMenu(); this.toolBar = this.createToolbar(); this.dockPanel = this.createSidePanel(); this.container = this.createContainer(); @@ -187,9 +194,9 @@ export class SidePanelHandler { return toolbar; } - protected createSidebarBottomMenu(): SidebarBottomMenuWidget { - const bottomMenu = this.sidebarBottomWidgetFactory(); - bottomMenu.addClass('theia-sidebar-bottom-menu'); + protected createSidebarMenu(): SidebarMenuWidget { + const bottomMenu = this.sidebarWidgetFactory(); + bottomMenu.addClass('theia-sidebar-menu'); return bottomMenu; } @@ -232,6 +239,7 @@ export class SidePanelHandler { const sidebarContainerLayout = new PanelLayout(); const sidebarContainer = new Panel({ layout: sidebarContainerLayout }); sidebarContainer.addClass('theia-app-sidebar-container'); + sidebarContainerLayout.addWidget(this.topMenu); sidebarContainerLayout.addWidget(this.tabBar); sidebarContainerLayout.addWidget(this.bottomMenu); @@ -385,12 +393,30 @@ export class SidePanelHandler { this.dockPanel.addWidget(widget); } + /** + * Add a menu to the sidebar top. + * + * If the menu is already added, it will be ignored. + */ + addTopMenu(menu: SidebarMenu): void { + this.topMenu.addMenu(menu); + } + + /** + * Remove a menu from the sidebar top. + * + * @param menuId id of the menu to remove + */ + removeTopMenu(menuId: string): void { + this.topMenu.removeMenu(menuId); + } + /** * Add a menu to the sidebar bottom. * * If the menu is already added, it will be ignored. */ - addMenu(menu: SidebarBottomMenu): void { + addBottomMenu(menu: SidebarMenu): void { this.bottomMenu.addMenu(menu); } @@ -399,7 +425,7 @@ export class SidePanelHandler { * * @param menuId id of the menu to remove */ - removeMenu(menuId: string): void { + removeBottomMenu(menuId: string): void { this.bottomMenu.removeMenu(menuId); } diff --git a/packages/core/src/browser/shell/sidebar-bottom-menu-widget.tsx b/packages/core/src/browser/shell/sidebar-menu-widget.tsx similarity index 78% rename from packages/core/src/browser/shell/sidebar-bottom-menu-widget.tsx rename to packages/core/src/browser/shell/sidebar-menu-widget.tsx index f85573635bb87..710cdbf424990 100644 --- a/packages/core/src/browser/shell/sidebar-bottom-menu-widget.tsx +++ b/packages/core/src/browser/shell/sidebar-menu-widget.tsx @@ -20,9 +20,9 @@ import { ReactWidget } from '../widgets'; import { ContextMenuRenderer } from '../context-menu-renderer'; import { MenuPath } from '../../common/menu'; -export const SidebarBottomMenuWidgetFactory = Symbol('SidebarBottomMenuWidgetFactory'); +export const SidebarMenuWidgetFactory = Symbol('SidebarMenuWidgetFactory'); -export interface SidebarBottomMenu { +export interface SidebarMenu { id: string; iconClass: string; title: string; @@ -31,11 +31,11 @@ export interface SidebarBottomMenu { } /** - * The menu widget placed on the bottom of the sidebar. + * The menu widget placed on the bottom or top of the sidebar. */ @injectable() -export class SidebarBottomMenuWidget extends ReactWidget { - protected readonly menus: SidebarBottomMenu[]; +export class SidebarMenuWidget extends ReactWidget { + protected readonly menus: SidebarMenu[]; @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer; @@ -45,7 +45,7 @@ export class SidebarBottomMenuWidget extends ReactWidget { this.menus = []; } - addMenu(menu: SidebarBottomMenu): void { + addMenu(menu: SidebarMenu): void { const exists = this.menus.find(m => m.id === menu.id); if (exists) { return; @@ -55,14 +55,14 @@ export class SidebarBottomMenuWidget extends ReactWidget { } removeMenu(menuId: string): void { - const menu = this.menus.find(m => m.id === menuId); - if (menu) { - const index = this.menus.indexOf(menu); - if (index !== -1) { - this.menus.splice(index, 1); - this.update(); - } + const menu = this.menus.find(m => m.id === menuId); + if (menu) { + const index = this.menus.indexOf(menu); + if (index !== -1) { + this.menus.splice(index, 1); + this.update(); } + } } protected onClick(e: React.MouseEvent, menuPath: MenuPath): void { diff --git a/packages/core/src/browser/style/sidepanel.css b/packages/core/src/browser/style/sidepanel.css index 77e34a6901c0a..1ccdc62b838cd 100644 --- a/packages/core/src/browser/style/sidepanel.css +++ b/packages/core/src/browser/style/sidepanel.css @@ -186,17 +186,17 @@ flex-grow: 1; } -.theia-app-sidebar-container .theia-sidebar-bottom-menu { +.theia-app-sidebar-container .theia-sidebar-menu { flex-shrink: 0; } -.p-Widget.theia-sidebar-bottom-menu { +.p-Widget.theia-sidebar-menu { background-color: var(--theia-activityBar-background); display: flex; flex-direction: column-reverse; } -.p-Widget.theia-sidebar-bottom-menu i { +.p-Widget.theia-sidebar-menu i { padding: var(--theia-private-sidebar-tab-padding-top-and-bottom) var(--theia-private-sidebar-tab-padding-left-and-right); display: flex; justify-content: center; @@ -207,10 +207,14 @@ font-size: var(--theia-private-sidebar-icon-size); } -.theia-sidebar-bottom-menu i:hover { +.theia-sidebar-menu i:hover { color: var(--theia-activityBar-foreground); } +.theia-sidebar-menu > i.codicon-menu{ + font-size: calc(var(--theia-private-sidebar-icon-size)*0.7); +} + /*----------------------------------------------------------------------------- | Perfect scrollbar |----------------------------------------------------------------------------*/ 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 e0d22f1973605..74625511f0976 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 @@ -71,33 +71,49 @@ export class ElectronMainMenuFactory { @inject(MenuModelRegistry) protected readonly menuProvider: MenuModelRegistry, @inject(KeybindingRegistry) protected readonly keybindingRegistry: KeybindingRegistry ) { - preferencesService.onPreferenceChanged(debounce(() => { - if (this._menu) { - for (const item of this._toggledCommands) { - this._menu.getMenuItemById(item).checked = this.commandRegistry.isToggled(item); + preferencesService.onPreferenceChanged( + debounce(e => { + if (e.preferenceName === 'window.menuBarVisibility') { + this.setMenuBar(); } - electron.remote.getCurrentWindow().setMenu(this._menu); - } - }, 10)); + if (this._menu) { + for (const item of this._toggledCommands) { + this._menu.getMenuItemById(item).checked = this.commandRegistry.isToggled(item); + } + electron.remote.getCurrentWindow().setMenu(this._menu); + } + }, 10) + ); keybindingRegistry.onKeybindingsChanged(() => { - const createdMenuBar = this.createMenuBar(); - if (isOSX) { - electron.remote.Menu.setApplicationMenu(createdMenuBar); - } else { - electron.remote.getCurrentWindow().setMenu(createdMenuBar); - } + this.setMenuBar(); }); } - createMenuBar(): Electron.Menu { - const menuModel = this.menuProvider.getMenu(MAIN_MENU_BAR); - const template = this.fillMenuTemplate([], menuModel); + setMenuBar(): void { + const createdMenuBar = this.createMenuBar(); if (isOSX) { - template.unshift(this.createOSXMenu()); + electron.remote.Menu.setApplicationMenu(createdMenuBar); + } else { + electron.remote.getCurrentWindow().setMenu(createdMenuBar); + } + } + + createMenuBar(): Electron.Menu | null { + let preference = this.preferencesService.get('window.menuBarVisibility'); + preference = preference ? preference : 'classic'; + if (preference && ['classic', 'visible'].includes(preference)) { + const menuModel = this.menuProvider.getMenu(MAIN_MENU_BAR); + const template = this.fillMenuTemplate([], menuModel); + if (isOSX) { + template.unshift(this.createOSXMenu()); + } + const menu = electron.remote.Menu.buildFromTemplate(template); + this._menu = menu; + return this._menu; } - const menu = electron.remote.Menu.buildFromTemplate(template); - this._menu = menu; - return menu; + this._menu = undefined; + // eslint-disable-next-line no-null/no-null + return null; } createContextMenu(menuPath: MenuPath, args?: any[]): Electron.Menu { 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 da71787d08ad4..8e9231e7428de 100644 --- a/packages/core/src/electron-browser/menu/electron-menu-contribution.ts +++ b/packages/core/src/electron-browser/menu/electron-menu-contribution.ts @@ -129,7 +129,7 @@ export class ElectronMenuContribution implements FrontendApplicationContribution } } - private setMenu(menu: electron.Menu = this.factory.createMenuBar(), electronWindow: electron.BrowserWindow = electron.remote.getCurrentWindow()): void { + private setMenu(menu: electron.Menu | null = this.factory.createMenuBar(), electronWindow: electron.BrowserWindow = electron.remote.getCurrentWindow()): void { if (isOSX) { electron.remote.Menu.setApplicationMenu(menu); } else {