diff --git a/CHANGELOG.md b/CHANGELOG.md index 71b707da44857..ab7409ee755d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ - [vsx-registry] `VSXExtensionsContribution` no longer implements `TabBarToolbarContribution` and is not bound as such. Extensions of the class that expect such behavior should reimplement it with caution. See caveats in PR. [#9798](https://github.com/eclipse-theia/theia/pull/9798) - [core] `handleExpansionToggleDblClickEvent` in `TreeWidget` can no longer be overridden. Instead, `doHandleExpansionToggleDblClickEvent` can be overridden. [#9877](https://github.com/eclipse-theia/theia/pull/9877) - [core] `ViewContainerPart` methods and properties related to hiding and showing toolbar removed: `toHideToolbar`, `hideToolbar`, `showToolbar`, `toolbarHidden`. `ViewContainerPart` toolbars are now hidden or shown using CSS properties. [#9935](https://github.com/eclipse-theia/theia/pull/9935) +- [core] `SidePanelHandler.addMenu` and `SidePanelHandler.removeMenu` no longer exists, instead added `addBottomMenu` and `addTopMenu` for adding menu, `removeTopMenu` and `removeBottomMenu` for removing menu. + - `SidebarBottomMenu` interface is renamed `SidebarMenu` and handles not only bottom menu's. + - Changed style class name from `theia-sidebar-bottom-menu` to `theia-sidebar-menu` +- [preferences] `TheiaDockPanel` constructor takes a new parameter `preferences`. +[#9830](https://github.com/eclipse-theia/theia/pull/9830) ## v1.16.0 - 7/29/2021 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 33c53f504b638..0afbb5e5f7d2c 100644 --- a/packages/core/src/browser/common-frontend-contribution.ts +++ b/packages/core/src/browser/common-frontend-contribution.ts @@ -43,9 +43,9 @@ import { environment } from '@theia/application-package/lib/environment'; import { IconThemeService } from './icon-theme-service'; import { ColorContribution } from './color-application-contribution'; import { ColorRegistry, Color } from './color-registry'; -import { CorePreferences } from './core-preferences'; +import { CoreConfiguration, CorePreferences } from './core-preferences'; import { ThemeService } from './theming'; -import { PreferenceService, PreferenceScope } from './preferences'; +import { PreferenceService, PreferenceScope, PreferenceChangeEvent } from './preferences'; import { ClipboardService } from './clipboard-service'; import { EncodingRegistry } from './encoding-registry'; import { UTF8 } from '../common/encodings'; @@ -358,17 +358,11 @@ export class CommonFrontendContribution implements FrontendApplicationContributi this.updateStyles(); 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); - } - }); + this.preferences.onPreferenceChanged(e => this.handlePreferenceChange(e, app)); 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', @@ -383,11 +377,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); } }); } @@ -425,6 +419,36 @@ export class CommonFrontendContribution implements FrontendApplicationContributi } } + protected handlePreferenceChange(e: PreferenceChangeEvent, app: FrontendApplication): void { + 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') { + this.shell.leftPanelHandler.addTopMenu({ + id: mainMenuId, + iconClass: 'codicon codicon-menu', + title: 'Application Menu', + menuPath: ['menubar'], + order: 0, + }); + } else { + app.shell.leftPanelHandler.removeTopMenu(mainMenuId); + } + break; + } + } + } + onStart(): void { this.storageService.getData<{ recent: Command[] }>(RECENT_COMMANDS_STORAGE_KEY, { recent: [] }) .then(tasks => this.commandRegistry.recent = tasks.recent); diff --git a/packages/core/src/browser/core-preferences.ts b/packages/core/src/browser/core-preferences.ts index e69a7784a2b15..8d2b1b6e7e33e 100644 --- a/packages/core/src/browser/core-preferences.ts +++ b/packages/core/src/browser/core-preferences.ts @@ -18,6 +18,7 @@ import { interfaces } from 'inversify'; import { createPreferenceProxy, PreferenceProxy, PreferenceService, PreferenceContribution, PreferenceSchema } from './preferences'; import { SUPPORTED_ENCODINGS } from './supported-encodings'; import { FrontendApplicationConfigProvider } from './frontend-application-config-provider'; +import { isOSX } from '../common/os'; export const corePreferenceSchema: PreferenceSchema = { 'type': 'object', @@ -97,6 +98,21 @@ 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.' + ], + default: 'classic', + scope: 'application', + markdownDescription: `Control the visibility of the menu bar. + A setting of 'compact' will move the menu into the sidebar.`, + included: !isOSX + }, } }; @@ -112,6 +128,7 @@ export interface CoreConfiguration { 'workbench.silentNotifications': boolean; 'files.encoding': string 'workbench.tree.renderIndentGuides': 'onHover' | 'none' | 'always'; + 'window.menuBarVisibility': 'classic' | 'visible' | 'hidden' | 'compact'; } export const CorePreferenceContribution = Symbol('CorePreferenceContribution'); diff --git a/packages/core/src/browser/frontend-application-module.ts b/packages/core/src/browser/frontend-application-module.ts index b868bbacc4791..aa0cbc50e3af6 100644 --- a/packages/core/src/browser/frontend-application-module.ts +++ b/packages/core/src/browser/frontend-application-module.ts @@ -43,8 +43,8 @@ import { ApplicationShell, ApplicationShellOptions, DockPanelRenderer, TabBarRenderer, TabBarRendererFactory, ShellLayoutRestorer, SidePanelHandler, SidePanelHandlerFactory, - SidebarBottomMenuWidget, SidebarBottomMenuWidgetFactory, - SplitPositionHandler, DockPanelRendererFactory, ApplicationShellLayoutMigration, ApplicationShellLayoutMigrationError + SidebarMenuWidget, SidebarTopMenuWidgetFactory, + SplitPositionHandler, DockPanelRendererFactory, ApplicationShellLayoutMigration, ApplicationShellLayoutMigrationError, SidebarBottomMenuWidgetFactory } from './shell'; import { StatusBar, StatusBarImpl } from './status-bar/status-bar'; import { LabelParser } from './label-parser'; @@ -104,6 +104,7 @@ import { } from './quick-input'; import { QuickAccessContribution } from './quick-input/quick-access'; import { QuickCommandService } from './quick-input/quick-command-service'; +import { SidebarBottomMenuWidget } from './shell/sidebar-bottom-menu-widget'; export { bindResourceProvider, bindMessageService, bindPreferenceService }; @@ -133,8 +134,10 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo bind(ApplicationShell).toSelf().inSingletonScope(); bind(SidePanelHandlerFactory).toAutoFactory(SidePanelHandler); bind(SidePanelHandler).toSelf(); - bind(SidebarBottomMenuWidgetFactory).toAutoFactory(SidebarBottomMenuWidget); + bind(SidebarTopMenuWidgetFactory).toAutoFactory(SidebarMenuWidget); + bind(SidebarMenuWidget).toSelf(); bind(SidebarBottomMenuWidget).toSelf(); + bind(SidebarBottomMenuWidgetFactory).toAutoFactory(SidebarBottomMenuWidget); 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..3549ba9b66a5f 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); diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts index 39f4455bd9e83..0732c39415eba 100644 --- a/packages/core/src/browser/shell/application-shell.ts +++ b/packages/core/src/browser/shell/application-shell.ts @@ -36,6 +36,8 @@ import { TabBarToolbarRegistry, TabBarToolbarFactory, TabBarToolbar } from './ta import { ContextKeyService } from '../context-key-service'; import { Emitter } from '../../common/event'; import { waitForRevealed, waitForClosed } from '../widgets'; +import { CorePreferences } from '../core-preferences'; +import { environment } from '../../common'; /** The class name added to ApplicationShell instances. */ const APPLICATION_SHELL_CLASS = 'theia-ApplicationShell'; @@ -204,7 +206,8 @@ export class ApplicationShell extends Widget { @inject(SidePanelHandlerFactory) sidePanelHandlerFactory: () => SidePanelHandler, @inject(SplitPositionHandler) protected splitPositionHandler: SplitPositionHandler, @inject(FrontendApplicationStateService) protected readonly applicationStateService: FrontendApplicationStateService, - @inject(ApplicationShellOptions) @optional() options: RecursivePartial = {} + @inject(ApplicationShellOptions) @optional() options: RecursivePartial = {}, + @inject(CorePreferences) protected readonly corePreferences: CorePreferences ) { super(options as Widget.IOptions); this.addClass(APPLICATION_SHELL_CLASS); @@ -250,6 +253,17 @@ export class ApplicationShell extends Widget { protected init(): void { this.initSidebarVisibleKeyContext(); this.initFocusKeyContexts(); + + if (!environment.electron.is()) { + this.corePreferences.ready.then(() => { + this.setTopPanelVisibily(this.corePreferences['window.menuBarVisibility']); + }); + this.corePreferences.onPreferenceChanged(preference => { + if (preference.preferenceName === 'window.menuBarVisibility') { + this.setTopPanelVisibily(preference.newValue); + } + }); + } } protected initSidebarVisibleKeyContext(): void { @@ -279,6 +293,11 @@ export class ApplicationShell extends Widget { this.activeChanged.connect(updateFocusContextKeys); } + protected setTopPanelVisibily(preference: string): void { + const hiddenPreferences = ['compact', 'hidden']; + this.topPanel.setHidden(hiddenPreferences.includes(preference)); + } + protected onBeforeAttach(msg: Message): void { document.addEventListener('p-dragenter', this, true); document.addEventListener('p-dragover', this, true); @@ -448,7 +467,7 @@ export class ApplicationShell extends Widget { mode: 'multiple-document', renderer, spacing: 0 - }); + }, this.corePreferences); dockPanel.id = MAIN_AREA_ID; dockPanel.widgetAdded.connect((_, widget) => this.fireDidAddWidget(widget)); dockPanel.widgetRemoved.connect((_, widget) => this.fireDidRemoveWidget(widget)); @@ -466,7 +485,7 @@ export class ApplicationShell extends Widget { mode: 'multiple-document', renderer, spacing: 0 - }); + }, this.corePreferences); dockPanel.id = BOTTOM_AREA_ID; dockPanel.widgetAdded.connect((sender, widget) => { this.refreshBottomPanelToggleButton(); @@ -493,6 +512,7 @@ export class ApplicationShell extends Widget { protected createTopPanel(): Panel { const topPanel = new Panel(); topPanel.id = 'theia-top-panel'; + topPanel.hide(); return topPanel; } 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..2d5a3ebacccce 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, SidebarMenu, SidebarBottomMenuWidgetFactory, SidebarTopMenuWidgetFactory } from './sidebar-menu-widget'; import { SplitPositionHandler, SplitPositionOptions } from './split-panels'; import { animationFrame } from '../browser'; import { FrontendApplicationStateService } from '../frontend-application-state'; @@ -31,6 +31,8 @@ import { TabBarToolbarRegistry, TabBarToolbarFactory, TabBarToolbar } from './ta import { DisposableCollection, Disposable } from '../../common/disposable'; import { ContextMenuRenderer } from '../context-menu-renderer'; import { MenuPath } from '../../common/menu'; +import { SidebarBottomMenuWidget } from './sidebar-bottom-menu-widget'; +import { SidebarTopMenuWidget } from './sidebar-top-menu-widget'; /** The class name added to the left and right area panels. */ export const LEFT_RIGHT_AREA_CLASS = 'theia-app-sides'; @@ -65,12 +67,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,6 +115,7 @@ export class SidePanelHandler { @inject(TabBarToolbarRegistry) protected tabBarToolBarRegistry: TabBarToolbarRegistry; @inject(TabBarToolbarFactory) protected tabBarToolBarFactory: () => TabBarToolbar; @inject(TabBarRendererFactory) protected tabBarRendererFactory: () => TabBarRenderer; + @inject(SidebarTopMenuWidgetFactory) protected sidebarTopWidgetFactory: () => SidebarTopMenuWidget; @inject(SidebarBottomMenuWidgetFactory) protected sidebarBottomWidgetFactory: () => SidebarBottomMenuWidget; @inject(SplitPositionHandler) protected splitPositionHandler: SplitPositionHandler; @inject(FrontendApplicationStateService) protected readonly applicationStateService: FrontendApplicationStateService; @@ -120,6 +129,7 @@ export class SidePanelHandler { create(side: 'left' | 'right', options: SidePanel.Options): void { this.side = side; this.options = options; + this.topMenu = this.createSidebarTopMenu(); this.tabBar = this.createSideBar(); this.bottomMenu = this.createSidebarBottomMenu(); this.toolBar = this.createToolbar(); @@ -187,10 +197,18 @@ export class SidePanelHandler { return toolbar; } + protected createSidebarTopMenu(): SidebarTopMenuWidget { + return this.createSidebarMenu(this.sidebarTopWidgetFactory); + } + protected createSidebarBottomMenu(): SidebarBottomMenuWidget { - const bottomMenu = this.sidebarBottomWidgetFactory(); - bottomMenu.addClass('theia-sidebar-bottom-menu'); - return bottomMenu; + return this.createSidebarMenu(this.sidebarBottomWidgetFactory); + } + + protected createSidebarMenu(factory: () => T): T { + const menu = factory(); + menu.addClass('theia-sidebar-menu'); + return menu; } protected showContextMenu(e: MouseEvent): void { @@ -232,6 +250,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 +404,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 +436,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-bottom-menu-widget.tsx index f85573635bb87..c8bb12dbf6e93 100644 --- a/packages/core/src/browser/shell/sidebar-bottom-menu-widget.tsx +++ b/packages/core/src/browser/shell/sidebar-bottom-menu-widget.tsx @@ -14,75 +14,25 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable, inject } from 'inversify'; -import * as React from 'react'; -import { ReactWidget } from '../widgets'; -import { ContextMenuRenderer } from '../context-menu-renderer'; +import { SidebarMenuWidget } from './sidebar-menu-widget'; import { MenuPath } from '../../common/menu'; - -export const SidebarBottomMenuWidgetFactory = Symbol('SidebarBottomMenuWidgetFactory'); - -export interface SidebarBottomMenu { - id: string; - iconClass: string; - title: string; - menuPath: MenuPath; - order: number; // smaller one place lower -} +import { injectable } from 'inversify'; /** * The menu widget placed on the bottom of the sidebar. */ @injectable() -export class SidebarBottomMenuWidget extends ReactWidget { - protected readonly menus: SidebarBottomMenu[]; - - @inject(ContextMenuRenderer) - protected readonly contextMenuRenderer: ContextMenuRenderer; - - constructor() { - super(); - this.menus = []; - } - - addMenu(menu: SidebarBottomMenu): void { - const exists = this.menus.find(m => m.id === menu.id); - if (exists) { - return; +export class SidebarBottomMenuWidget extends SidebarMenuWidget { + + protected onClick(e: React.MouseEvent, menuPath: MenuPath): void { + const button = e.currentTarget.getBoundingClientRect(); + this.contextMenuRenderer.render({ + menuPath, + anchor: { + x: button.left + button.width, + y: button.top + button.height, + } + }); } - this.menus.push(menu); - this.update(); - } - - 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(); - } - } - } - - protected onClick(e: React.MouseEvent, menuPath: MenuPath): void { - this.contextMenuRenderer.render({ - menuPath, - anchor: { - x: e.clientX, - y: e.clientY, - } - }); - } - protected render(): React.ReactNode { - return - {this.menus.sort((a, b) => a.order - b.order).map(menu => this.onClick(e, menu.menuPath)} - />)} - ; - } } diff --git a/packages/core/src/browser/shell/sidebar-menu-widget.tsx b/packages/core/src/browser/shell/sidebar-menu-widget.tsx new file mode 100644 index 0000000000000..18726a1182a44 --- /dev/null +++ b/packages/core/src/browser/shell/sidebar-menu-widget.tsx @@ -0,0 +1,93 @@ +/******************************************************************************** + * Copyright (C) 2020 Alibaba Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject } from 'inversify'; +import * as React from 'react'; +import { ReactWidget } from '../widgets'; +import { ContextMenuRenderer } from '../context-menu-renderer'; +import { MenuPath } from '../../common/menu'; + +export const SidebarTopMenuWidgetFactory = Symbol('SidebarTopMenuWidgetFactory'); +export const SidebarBottomMenuWidgetFactory = Symbol('SidebarBottomMenuWidgetFactory'); + +export interface SidebarMenu { + id: string; + iconClass: string; + title: string; + menuPath: MenuPath; + /* + * Used to sort menus. The lower the value the lower they are placed in the sidebar. + */ + order: number; +} + +/** + * The menu widget placed on the sidebar. + */ +@injectable() +export class SidebarMenuWidget extends ReactWidget { + protected readonly menus: SidebarMenu[]; + + @inject(ContextMenuRenderer) + protected readonly contextMenuRenderer: ContextMenuRenderer; + + constructor() { + super(); + this.menus = []; + } + + addMenu(menu: SidebarMenu): void { + const exists = this.menus.find(m => m.id === menu.id); + if (exists) { + return; + } + this.menus.push(menu); + this.update(); + } + + 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(); + } + } + } + + protected onClick(e: React.MouseEvent, menuPath: MenuPath): void { + const button = e.currentTarget.getBoundingClientRect(); + this.contextMenuRenderer.render({ + menuPath, + anchor: { + x: button.left + button.width, + y: button.top, + } + }); + } + + protected render(): React.ReactNode { + return + {this.menus.sort((a, b) => a.order - b.order).map(menu => this.onClick(e, menu.menuPath)} + />)} + ; + } +} diff --git a/packages/core/src/browser/shell/sidebar-top-menu-widget.tsx b/packages/core/src/browser/shell/sidebar-top-menu-widget.tsx new file mode 100644 index 0000000000000..31deb7d79cbdd --- /dev/null +++ b/packages/core/src/browser/shell/sidebar-top-menu-widget.tsx @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (C) 2020 Alibaba Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { SidebarMenuWidget } from './sidebar-menu-widget'; +import { injectable } from 'inversify'; + +/** + * The menu widget placed on the top of the sidebar. + */ +@injectable() +export class SidebarTopMenuWidget extends SidebarMenuWidget { + +} diff --git a/packages/core/src/browser/shell/theia-dock-panel.ts b/packages/core/src/browser/shell/theia-dock-panel.ts index efbef03ba8258..9bc4fc314f827 100644 --- a/packages/core/src/browser/shell/theia-dock-panel.ts +++ b/packages/core/src/browser/shell/theia-dock-panel.ts @@ -19,8 +19,12 @@ import { TabBar, Widget, DockPanel, Title, DockLayout } from '@phosphor/widgets' import { Signal } from '@phosphor/signaling'; import { Disposable, DisposableCollection } from '../../common/disposable'; import { MessageLoop } from '../widgets'; +import { CorePreferences } from '../core-preferences'; +import { inject } from 'inversify'; +import { Emitter, environment } from '../../common'; -const MAXIMIZED_CLASS = 'theia-maximized'; +export const MAXIMIZED_CLASS = 'theia-maximized'; +const VISIBLE_MENU_MAXIMIZED_CLASS = 'theia-visible-menu-maximized'; export const MAIN_AREA_ID = 'theia-main-content-panel'; export const BOTTOM_AREA_ID = 'theia-bottom-content-panel'; @@ -44,7 +48,12 @@ export class TheiaDockPanel extends DockPanel { */ readonly widgetRemoved = new Signal(this); - constructor(options?: DockPanel.IOptions) { + protected readonly onDidToggleMaximizedEmitter = new Emitter(); + readonly onDidToggleMaximized = this.onDidToggleMaximizedEmitter.event; + + constructor(options?: DockPanel.IOptions, + @inject(CorePreferences) protected readonly preferences?: CorePreferences + ) { super(options); this['_onCurrentChanged'] = (sender: TabBar, args: TabBar.ICurrentChangedArgs) => { this.markAsCurrent(args.currentTitle || undefined); @@ -54,6 +63,30 @@ export class TheiaDockPanel extends DockPanel { this.markAsCurrent(args.title); super['_onTabActivateRequested'](sender, args); }; + if (preferences) { + preferences.onPreferenceChanged(preference => { + if (!this.isElectron() && preference.preferenceName === 'window.menuBarVisibility' && (preference.newValue === 'visible' || preference.oldValue === 'visible')) { + this.handleMenuBarVisibility(preference.newValue); + } + }); + } + } + + isElectron(): boolean { + return environment.electron.is(); + } + + protected handleMenuBarVisibility(newValue: string): void { + const areaContainer = this.node.parentElement; + const maximizedElement = this.getMaximizedElement(); + + if (areaContainer === maximizedElement) { + if (newValue === 'visible') { + this.addClass(VISIBLE_MENU_MAXIMIZED_CLASS); + } else { + this.removeClass(VISIBLE_MENU_MAXIMIZED_CLASS); + } + } } protected _currentTitle: Title | undefined; @@ -148,13 +181,22 @@ export class TheiaDockPanel extends DockPanel { } maximizedElement.style.display = 'block'; this.addClass(MAXIMIZED_CLASS); + const preference = this.preferences?.get('window.menuBarVisibility'); + if (!this.isElectron() && preference === 'visible') { + this.addClass(VISIBLE_MENU_MAXIMIZED_CLASS); + } MessageLoop.sendMessage(this, Widget.Msg.BeforeAttach); maximizedElement.appendChild(this.node); MessageLoop.sendMessage(this, Widget.Msg.AfterAttach); this.fit(); + this.onDidToggleMaximizedEmitter.fire(this); this.toDisposeOnToggleMaximized.push(Disposable.create(() => { maximizedElement.style.display = 'none'; this.removeClass(MAXIMIZED_CLASS); + this.onDidToggleMaximizedEmitter.fire(this); + if (!this.isElectron()) { + this.removeClass(VISIBLE_MENU_MAXIMIZED_CLASS); + } if (this.isAttached) { MessageLoop.sendMessage(this, Widget.Msg.BeforeDetach); this.node.remove(); diff --git a/packages/core/src/browser/style/index.css b/packages/core/src/browser/style/index.css index 11eac0003e6d1..e4e8f65c61476 100644 --- a/packages/core/src/browser/style/index.css +++ b/packages/core/src/browser/style/index.css @@ -78,6 +78,10 @@ blockquote { background: var(--theia-editor-background); } +.theia-visible-menu-maximized { + top: var(--theia-private-menubar-height) !important; +} + .theia-ApplicationShell { position: absolute; top: 0; diff --git a/packages/core/src/browser/style/sidepanel.css b/packages/core/src/browser/style/sidepanel.css index 77e34a6901c0a..9bf6061c0d406 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 6658e4470ffa4..51fa1f305a107 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 @@ -27,6 +27,7 @@ import { PreferenceService, KeybindingRegistry, CommonCommands } from '../../bro import { ContextKeyService } from '../../browser/context-key-service'; import debounce = require('lodash.debounce'); import { ContextMenuContext } from '../../browser/menu/context-menu-context'; +import { MAXIMIZED_CLASS } from '../../browser/shell/theia-dock-panel'; /** * Representation of possible electron menu options. @@ -71,33 +72,50 @@ 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); + async setMenuBar(): Promise { + await this.preferencesService.ready; + 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 { + const preference = this.preferencesService.get('window.menuBarVisibility') || 'classic'; + const maxWidget = document.getElementsByClassName(MAXIMIZED_CLASS); + if (preference === 'visible' || (preference === 'classic' && maxWidget.length === 0)) { + 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..aaf1926e0c99a 100644 --- a/packages/core/src/electron-browser/menu/electron-menu-contribution.ts +++ b/packages/core/src/electron-browser/menu/electron-menu-contribution.ts @@ -20,7 +20,7 @@ import { Command, CommandContribution, CommandRegistry, isOSX, isWindows, MenuModelRegistry, MenuContribution, Disposable } from '../../common'; -import { KeybindingContribution, KeybindingRegistry, PreferenceScope, PreferenceService } from '../../browser'; +import { ApplicationShell, KeybindingContribution, KeybindingRegistry, PreferenceScope, PreferenceService } from '../../browser'; import { FrontendApplication, FrontendApplicationContribution, CommonMenus } from '../../browser'; import { ElectronMainMenuFactory } from './electron-main-menu-factory'; import { FrontendApplicationStateService, FrontendApplicationState } from '../../browser/frontend-application-state'; @@ -81,12 +81,16 @@ export class ElectronMenuContribution implements FrontendApplicationContribution protected readonly preferenceService: PreferenceService; constructor( - @inject(ElectronMainMenuFactory) protected readonly factory: ElectronMainMenuFactory + @inject(ElectronMainMenuFactory) protected readonly factory: ElectronMainMenuFactory, + @inject(ApplicationShell) protected shell: ApplicationShell ) { } onStart(app: FrontendApplication): void { this.hideTopPanel(app); - this.setMenu(); + this.preferenceService.ready.then(() => { + this.setMenu(); + electron.remote.getCurrentWindow().setMenuBarVisibility(true); + }); if (isOSX) { // OSX: Recreate the menus when changing windows. // OSX only has one menu bar for all windows, so we need to swap @@ -107,6 +111,19 @@ export class ElectronMenuContribution implements FrontendApplicationContribution } }; onStateChange = this.stateService.onStateChanged(stateServiceListener); + this.shell.mainPanel.onDidToggleMaximized(() => { + this.handleToggleMaximized(); + }); + this.shell.bottomPanel.onDidToggleMaximized(() => { + this.handleToggleMaximized(); + }); + } + + handleToggleMaximized(): void { + const preference = this.preferenceService.get('window.menuBarVisibility'); + if (preference === 'classic') { + this.factory.setMenuBar(); + } } /** @@ -129,7 +146,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 { diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts index 8ed01ca7556a1..6d0da0cced7a5 100644 --- a/packages/core/src/electron-main/electron-main-application.ts +++ b/packages/core/src/electron-main/electron-main-application.ts @@ -219,6 +219,7 @@ export class ElectronMainApplication { let options = await asyncOptions; options = this.avoidOverlap(options); const electronWindow = new BrowserWindow(options); + electronWindow.setMenuBarVisibility(false); this.attachReadyToShow(electronWindow); this.attachSaveWindowState(electronWindow); this.attachGlobalShortcuts(electronWindow);