diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9eac42cfee5ce..6901259230332 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,10 @@
[Breaking Changes:](#breaking_changes_1.19.0)
- [view-container] `ViewContainerPart` constructor takes new 2 parameters: `originalContainerId` and `originalContainerTitle`. The existing `viewContainerId` parameter has been renamed to `currentContainerId` to enable drag & drop views. [#9644](https://github.com/eclipse-theia/theia/pull/9644)
+- [electron] `ElectronMainMenuFactory` now inherits from `BrowserMainMenuFactory` and its methods have been renamed. [#10044](https://github.com/eclipse-theia/theia/pull/10044)
+ - renamed `handleDefault` to `handleElectronDefault`
+ - renamed `createContextMenu` to `createElectronContextMenu`
+ - renamed `createMenuBar` to `createElectronMenuBar`
## v1.18.0 - 9/30/2021
diff --git a/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts b/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts
index 286a21484e134..7f40836b6288f 100644
--- a/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts
+++ b/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts
@@ -27,7 +27,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
class SampleElectronMainMenuFactory extends ElectronMainMenuFactory {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- protected handleDefault(menuNode: CompositeMenuNode, args: any[] = [], options?: ElectronMenuOptions): Electron.MenuItemConstructorOptions[] {
+ protected handleElectronDefault(menuNode: CompositeMenuNode, args: any[] = [], options?: ElectronMenuOptions): Electron.MenuItemConstructorOptions[] {
if (menuNode instanceof PlaceholderMenuNode) {
return [{
label: menuNode.label,
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 ed4baca3c1231..7fc6c1913a66e 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 | null = this.factory.createMenuBar(), electronWindow: BrowserWindow = remote.getCurrentWindow()): void {
+ private setMenu(menu: Menu | null = this.factory.createElectronMenuBar(), electronWindow: BrowserWindow = remote.getCurrentWindow()): void {
if (isOSX) {
remote.Menu.setApplicationMenu(menu);
} else {
diff --git a/packages/core/src/browser/menu/browser-context-menu-renderer.ts b/packages/core/src/browser/menu/browser-context-menu-renderer.ts
index 2b664b75f0238..c549094700e78 100644
--- a/packages/core/src/browser/menu/browser-context-menu-renderer.ts
+++ b/packages/core/src/browser/menu/browser-context-menu-renderer.ts
@@ -36,7 +36,7 @@ export class BrowserContextMenuRenderer extends ContextMenuRenderer {
super();
}
- protected doRender({ menuPath, anchor, args, onHide }: RenderContextMenuOptions): BrowserContextMenuAccess {
+ protected doRender({ menuPath, anchor, args, onHide }: RenderContextMenuOptions): ContextMenuAccess {
const contextMenu = this.menuFactory.createContextMenu(menuPath, args);
const { x, y } = coordinateFromAnchor(anchor);
if (onHide) {
diff --git a/packages/core/src/browser/menu/browser-menu-plugin.ts b/packages/core/src/browser/menu/browser-menu-plugin.ts
index 452da0acbf9ae..f393a301264c0 100644
--- a/packages/core/src/browser/menu/browser-menu-plugin.ts
+++ b/packages/core/src/browser/menu/browser-menu-plugin.ts
@@ -18,7 +18,7 @@ import { injectable, inject } from 'inversify';
import { MenuBar, Menu as MenuWidget, Widget } from '@phosphor/widgets';
import { CommandRegistry as PhosphorCommandRegistry } from '@phosphor/commands';
import {
- CommandRegistry, ActionMenuNode, CompositeMenuNode,
+ CommandRegistry, ActionMenuNode, CompositeMenuNode, environment,
MenuModelRegistry, MAIN_MENU_BAR, MenuPath, DisposableCollection, Disposable, MenuNode
} from '../../common';
import { KeybindingRegistry } from '../keybinding';
@@ -28,6 +28,7 @@ import { ContextMenuContext } from './context-menu-context';
import { waitForRevealed } from '../widgets';
import { ApplicationShell } from '../shell';
import { CorePreferences } from '../core-preferences';
+import { PreferenceService } from '../preferences/preference-service';
export abstract class MenuBarWidget extends MenuBar {
abstract activateMenu(label: string, ...labels: string[]): Promise;
@@ -371,21 +372,40 @@ export class BrowserMenuBarContribution implements FrontendApplicationContributi
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;
+ @inject(PreferenceService)
+ protected readonly preferenceService: PreferenceService;
+
constructor(
@inject(BrowserMainMenuFactory) protected readonly factory: BrowserMainMenuFactory
) { }
onStart(app: FrontendApplication): void {
- const logo = this.createLogo();
- app.shell.addWidget(logo, { area: 'top' });
- const menu = this.factory.createMenuBar();
- app.shell.addWidget(menu, { area: 'top' });
+ this.appendMenu(app.shell);
}
get menuBar(): MenuBarWidget | undefined {
return this.shell.topPanel.widgets.find(w => w instanceof MenuBarWidget) as MenuBarWidget | undefined;
}
+ protected appendMenu(shell: ApplicationShell): void {
+ const logo = this.createLogo();
+ shell.addWidget(logo, { area: 'top' });
+ const menu = this.factory.createMenuBar();
+ shell.addWidget(menu, { area: 'top' });
+ // Hiding the menu is only necessary in electron
+ // In the browser we hide the whole top panel
+ if (environment.electron.is()) {
+ this.preferenceService.ready.then(() => {
+ menu.setHidden(['compact', 'hidden'].includes(this.preferenceService.get('window.menuBarVisibility', '')));
+ });
+ this.preferenceService.onPreferenceChanged(change => {
+ if (change.preferenceName === 'window.menuBarVisibility') {
+ menu.setHidden(['compact', 'hidden'].includes(change.newValue));
+ }
+ });
+ }
+ }
+
protected createLogo(): Widget {
const logo = new Widget();
logo.id = 'theia:icon';
diff --git a/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts b/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts
index c73b72d4565d2..792b40386c997 100644
--- a/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts
+++ b/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts
@@ -17,11 +17,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as electron from '../../../shared/electron';
-import { inject, injectable } from 'inversify';
-import { ContextMenuRenderer, RenderContextMenuOptions, ContextMenuAccess, FrontendApplicationContribution, CommonCommands, coordinateFromAnchor } from '../../browser';
+import { inject, injectable, postConstruct } from 'inversify';
+import {
+ ContextMenuRenderer, RenderContextMenuOptions, ContextMenuAccess, FrontendApplicationContribution, CommonCommands, coordinateFromAnchor, PreferenceService
+} from '../../browser';
import { ElectronMainMenuFactory } from './electron-main-menu-factory';
import { ContextMenuContext } from '../../browser/menu/context-menu-context';
import { MenuPath, MenuContribution, MenuModelRegistry } from '../../common';
+import { BrowserContextMenuRenderer } from '../../browser/menu/browser-context-menu-renderer';
+import { RequestTitleBarStyle, TitleBarStyleAtStartup } from '../../electron-common/messaging/electron-messages';
export class ElectronContextMenuAccess extends ContextMenuAccess {
constructor(readonly menu: electron.Menu) {
@@ -73,27 +77,45 @@ export class ElectronTextInputContextMenuContribution implements FrontendApplica
}
@injectable()
-export class ElectronContextMenuRenderer extends ContextMenuRenderer {
+export class ElectronContextMenuRenderer extends BrowserContextMenuRenderer {
@inject(ContextMenuContext)
protected readonly context: ContextMenuContext;
- constructor(@inject(ElectronMainMenuFactory) private menuFactory: ElectronMainMenuFactory) {
- super();
+ @inject(PreferenceService)
+ protected readonly preferenceService: PreferenceService;
+
+ protected useNativeStyle: boolean = true;
+
+ constructor(@inject(ElectronMainMenuFactory) private electronMenuFactory: ElectronMainMenuFactory) {
+ super(electronMenuFactory);
+ }
+
+ @postConstruct()
+ protected async init(): Promise {
+ electron.ipcRenderer.on(TitleBarStyleAtStartup, (_event, style: string) => {
+ this.useNativeStyle = style === 'native';
+ });
+ electron.ipcRenderer.send(RequestTitleBarStyle);
}
- protected doRender({ menuPath, anchor, args, onHide }: RenderContextMenuOptions): ElectronContextMenuAccess {
- const menu = this.menuFactory.createContextMenu(menuPath, args);
- const { x, y } = coordinateFromAnchor(anchor);
- const zoom = electron.webFrame.getZoomFactor();
- // x and y values must be Ints or else there is a conversion error
- menu.popup({ x: Math.round(x * zoom), y: Math.round(y * zoom) });
- // native context menu stops the event loop, so there is no keyboard events
- this.context.resetAltPressed();
- if (onHide) {
- menu.once('menu-will-close', () => onHide());
+ protected doRender(options: RenderContextMenuOptions): ContextMenuAccess {
+ if (this.useNativeStyle) {
+ const { menuPath, anchor, args, onHide } = options;
+ const menu = this.electronMenuFactory.createElectronContextMenu(menuPath, args);
+ const { x, y } = coordinateFromAnchor(anchor);
+ const zoom = electron.webFrame.getZoomFactor();
+ // x and y values must be Ints or else there is a conversion error
+ menu.popup({ x: Math.round(x * zoom), y: Math.round(y * zoom) });
+ // native context menu stops the event loop, so there is no keyboard events
+ this.context.resetAltPressed();
+ if (onHide) {
+ menu.once('menu-will-close', () => onHide());
+ }
+ return new ElectronContextMenuAccess(menu);
+ } else {
+ return super.doRender(options);
}
- return new ElectronContextMenuAccess(menu);
}
}
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 51fa1f305a107..536110a33aad0 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
@@ -24,10 +24,9 @@ import {
} from '../../common';
import { Keybinding } from '../../common/keybinding';
import { PreferenceService, KeybindingRegistry, CommonCommands } from '../../browser';
-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';
+import { BrowserMainMenuFactory } from '../../browser/menu/browser-menu-plugin';
/**
* Representation of possible electron menu options.
@@ -55,23 +54,18 @@ export type ElectronMenuItemRole = ('undo' | 'redo' | 'cut' | 'copy' | 'paste' |
'moveTabToNewWindow' | 'windowMenu');
@injectable()
-export class ElectronMainMenuFactory {
+export class ElectronMainMenuFactory extends BrowserMainMenuFactory {
protected _menu: Electron.Menu | undefined;
protected _toggledCommands: Set = new Set();
- @inject(ContextKeyService)
- protected readonly contextKeyService: ContextKeyService;
-
- @inject(ContextMenuContext)
- protected readonly context: ContextMenuContext;
-
constructor(
@inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry,
@inject(PreferenceService) protected readonly preferencesService: PreferenceService,
@inject(MenuModelRegistry) protected readonly menuProvider: MenuModelRegistry,
@inject(KeybindingRegistry) protected readonly keybindingRegistry: KeybindingRegistry
) {
+ super();
preferencesService.onPreferenceChanged(
debounce(e => {
if (e.preferenceName === 'window.menuBarVisibility') {
@@ -92,15 +86,16 @@ export class ElectronMainMenuFactory {
async setMenuBar(): Promise {
await this.preferencesService.ready;
- const createdMenuBar = this.createMenuBar();
if (isOSX) {
+ const createdMenuBar = this.createElectronMenuBar();
electron.remote.Menu.setApplicationMenu(createdMenuBar);
- } else {
+ } else if (this.preferencesService.get('window.titleBarStyle') === 'native') {
+ const createdMenuBar = this.createElectronMenuBar();
electron.remote.getCurrentWindow().setMenu(createdMenuBar);
}
}
- createMenuBar(): Electron.Menu | null {
+ createElectronMenuBar(): 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)) {
@@ -118,7 +113,7 @@ export class ElectronMainMenuFactory {
return null;
}
- createContextMenu(menuPath: MenuPath, args?: any[]): Electron.Menu {
+ createElectronContextMenu(menuPath: MenuPath, args?: any[]): Electron.Menu {
const menuModel = this.menuProvider.getMenu(menuPath);
const template = this.fillMenuTemplate([], menuModel, args, { showDisabled: false });
return electron.remote.Menu.buildFromTemplate(template);
@@ -221,13 +216,13 @@ export class ElectronMainMenuFactory {
this._toggledCommands.add(commandId);
}
} else {
- items.push(...this.handleDefault(menu, args, options));
+ items.push(...this.handleElectronDefault(menu, args, options));
}
}
return items;
}
- protected handleDefault(menuNode: MenuNode, args: any[] = [], options?: ElectronMenuOptions): Electron.MenuItemConstructorOptions[] {
+ protected handleElectronDefault(menuNode: MenuNode, args: any[] = [], options?: ElectronMenuOptions): Electron.MenuItemConstructorOptions[] {
return [];
}
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 aaf1926e0c99a..ec989a10ed2ca 100644
--- a/packages/core/src/electron-browser/menu/electron-menu-contribution.ts
+++ b/packages/core/src/electron-browser/menu/electron-menu-contribution.ts
@@ -20,11 +20,15 @@ import {
Command, CommandContribution, CommandRegistry,
isOSX, isWindows, MenuModelRegistry, MenuContribution, Disposable
} from '../../common';
-import { ApplicationShell, KeybindingContribution, KeybindingRegistry, PreferenceScope, PreferenceService } from '../../browser';
+import { ApplicationShell, codicon, ConfirmDialog, KeybindingContribution, KeybindingRegistry, PreferenceScope, Widget } from '../../browser';
import { FrontendApplication, FrontendApplicationContribution, CommonMenus } from '../../browser';
import { ElectronMainMenuFactory } from './electron-main-menu-factory';
import { FrontendApplicationStateService, FrontendApplicationState } from '../../browser/frontend-application-state';
+import { RequestTitleBarStyle, Restart, TitleBarStyleAtStartup, TitleBarStyleChanged } from '../../electron-common/messaging/electron-messages';
import { ZoomLevel } from '../window/electron-window-preferences';
+import { BrowserMenuBarContribution } from '../../browser/menu/browser-menu-plugin';
+
+import '../../../src/electron-browser/menu/electron-menu-style.css';
export namespace ElectronCommands {
export const TOGGLE_DEVELOPER_TOOLS: Command = {
@@ -72,38 +76,33 @@ export namespace ElectronMenus {
}
@injectable()
-export class ElectronMenuContribution implements FrontendApplicationContribution, CommandContribution, MenuContribution, KeybindingContribution {
+export class ElectronMenuContribution extends BrowserMenuBarContribution implements FrontendApplicationContribution, CommandContribution, MenuContribution, KeybindingContribution {
@inject(FrontendApplicationStateService)
protected readonly stateService: FrontendApplicationStateService;
- @inject(PreferenceService)
- protected readonly preferenceService: PreferenceService;
+ protected titleBarStyleChangeFlag = false;
+ protected titleBarStyle?: string;
constructor(
@inject(ElectronMainMenuFactory) protected readonly factory: ElectronMainMenuFactory,
@inject(ApplicationShell) protected shell: ApplicationShell
- ) { }
+ ) {
+ super(factory);
+ }
onStart(app: FrontendApplication): void {
- this.hideTopPanel(app);
- this.preferenceService.ready.then(() => {
- this.setMenu();
- electron.remote.getCurrentWindow().setMenuBarVisibility(true);
- });
+ this.handleTitleBarStyling(app);
if (isOSX) {
// OSX: Recreate the menus when changing windows.
// OSX only has one menu bar for all windows, so we need to swap
// between them as the user switches windows.
- electron.remote.getCurrentWindow().on('focus', () => this.setMenu());
+ electron.remote.getCurrentWindow().on('focus', () => this.setMenu(app));
}
// 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();
@@ -119,6 +118,30 @@ export class ElectronMenuContribution implements FrontendApplicationContribution
});
}
+ handleTitleBarStyling(app: FrontendApplication): void {
+ this.hideTopPanel(app);
+ electron.ipcRenderer.on(TitleBarStyleAtStartup, (_event, style: string) => {
+ this.titleBarStyle = style;
+ this.preferenceService.ready.then(() => {
+ this.preferenceService.set('window.titleBarStyle', this.titleBarStyle, PreferenceScope.User);
+ });
+ });
+ electron.ipcRenderer.send(RequestTitleBarStyle);
+ this.preferenceService.ready.then(() => {
+ this.setMenu(app);
+ electron.remote.getCurrentWindow().setMenuBarVisibility(['classic', 'visible'].includes(this.preferenceService.get('window.menuBarVisibility', 'classic')));
+ });
+ this.preferenceService.onPreferenceChanged(change => {
+ if (change.preferenceName === 'window.titleBarStyle') {
+ if (this.titleBarStyleChangeFlag && this.titleBarStyle !== change.newValue && electron.remote.getCurrentWindow().isFocused()) {
+ electron.ipcRenderer.send(TitleBarStyleChanged, change.newValue);
+ this.handleRequiredRestart();
+ }
+ this.titleBarStyleChangeFlag = true;
+ }
+ });
+ }
+
handleToggleMaximized(): void {
const preference = this.preferenceService.get('window.menuBarVisibility');
if (preference === 'classic') {
@@ -127,31 +150,87 @@ export class ElectronMenuContribution implements FrontendApplicationContribution
}
/**
- * Makes the `theia-top-panel` hidden as it is unused for the electron-based application.
+ * Hides the `theia-top-panel` depending on the selected `titleBarStyle`.
* The `theia-top-panel` is used as the container of the main, application menu-bar for the
- * browser. Electron has it's own.
+ * browser. Native Electron has it's own.
* By default, this method is called on application `onStart`.
*/
protected hideTopPanel(app: FrontendApplication): void {
const itr = app.shell.children();
let child = itr.next();
while (child) {
- // Top panel for the menu contribution is not required for Electron.
+ // Top panel for the menu contribution is not required for native Electron title bar.
if (child.id === 'theia-top-panel') {
- child.setHidden(true);
- child = undefined;
+ child.setHidden(this.titleBarStyle !== 'custom');
+ break;
} else {
child = itr.next();
}
}
}
- private setMenu(menu: electron.Menu | null = this.factory.createMenuBar(), electronWindow: electron.BrowserWindow = electron.remote.getCurrentWindow()): void {
+ protected setMenu(app: FrontendApplication, electronMenu: electron.Menu | null = this.factory.createElectronMenuBar(),
+ electronWindow: electron.BrowserWindow = electron.remote.getCurrentWindow()): void {
if (isOSX) {
- electron.remote.Menu.setApplicationMenu(menu);
+ electron.remote.Menu.setApplicationMenu(electronMenu);
} else {
+ this.hideTopPanel(app);
+ if (this.titleBarStyle === 'custom' && !this.menuBar) {
+ this.createCustomTitleBar(app, electronWindow);
+ }
// Unix/Windows: Set the per-window menus
- electronWindow.setMenu(menu);
+ electronWindow.setMenu(electronMenu);
+ }
+ }
+
+ protected createCustomTitleBar(app: FrontendApplication, electronWindow: electron.BrowserWindow): void {
+ const dragPanel = new Widget();
+ dragPanel.id = 'theia-drag-panel';
+ app.shell.addWidget(dragPanel, { area: 'top' });
+ this.appendMenu(app.shell);
+ const controls = document.createElement('div');
+ controls.id = 'window-controls';
+ controls.append(
+ this.createControlButton('minimize', () => electronWindow.minimize()),
+ this.createControlButton('maximize', () => electronWindow.maximize()),
+ this.createControlButton('restore', () => electronWindow.unmaximize()),
+ this.createControlButton('close', () => electronWindow.close())
+ );
+ app.shell.topPanel.node.append(controls);
+ this.handleWindowControls(electronWindow);
+ }
+
+ protected handleWindowControls(electronWindow: electron.BrowserWindow): void {
+ toggleControlButtons();
+ electronWindow.on('maximize', toggleControlButtons);
+ electronWindow.on('unmaximize', toggleControlButtons);
+
+ function toggleControlButtons(): void {
+ if (electronWindow.isMaximized()) {
+ document.body.classList.add('maximized');
+ } else {
+ document.body.classList.remove('maximized');
+ }
+ }
+ }
+
+ protected createControlButton(id: string, handler: () => void): HTMLElement {
+ const button = document.createElement('div');
+ button.id = `${id}-button`;
+ button.className = `control-button ${codicon(`chrome-${id}`)}`;
+ button.addEventListener('click', handler);
+ return button;
+ }
+
+ protected async handleRequiredRestart(): Promise {
+ const dialog = new ConfirmDialog({
+ title: 'A setting has changed that requires a restart to take effect',
+ msg: 'Press the restart button to restart the application and enable the setting.',
+ ok: 'Restart',
+ cancel: 'Cancel'
+ });
+ if (await dialog.open()) {
+ electron.ipcRenderer.send(Restart);
}
}
diff --git a/packages/core/src/electron-browser/menu/electron-menu-style.css b/packages/core/src/electron-browser/menu/electron-menu-style.css
new file mode 100644
index 0000000000000..61f137863276e
--- /dev/null
+++ b/packages/core/src/electron-browser/menu/electron-menu-style.css
@@ -0,0 +1,84 @@
+/********************************************************************************
+ * Copyright (C) 2021 TypeFox 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
+ ********************************************************************************/
+
+#theia-drag-panel {
+ position: absolute;
+ display: block;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: calc(100% - 4px);
+ margin: 4px;
+ -webkit-app-region: drag !important;
+}
+
+#theia-top-panel > * {
+ -webkit-app-region: no-drag;
+}
+
+#window-controls {
+ display: grid;
+ grid-template-columns: repeat(3, 48px);
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 100%;
+}
+
+#window-controls .control-button {
+ display: flex;
+ line-height: 30px;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+}
+
+#minimize-button {
+ grid-column: 1;
+}
+
+#maximize-button, #restore-button {
+ grid-column: 2;
+}
+
+#close-button {
+ grid-column: 3;
+}
+
+#window-controls .control-button {
+ user-select: none;
+}
+
+#window-controls .control-button:hover {
+ background: rgba(50%, 50%, 50%, 0.2);
+}
+
+#window-controls #close-button:hover {
+ background: #E81123;
+}
+
+#window-controls #close-button:hover:before {
+ color: white;
+}
+
+body:not(.maximized) #restore-button {
+ display: none;
+}
+
+body.maximized #maximize-button {
+ display: none;
+}
diff --git a/packages/core/src/electron-browser/window/electron-window-preferences.ts b/packages/core/src/electron-browser/window/electron-window-preferences.ts
index b7d97e543b84a..9c97bc732b5a5 100644
--- a/packages/core/src/electron-browser/window/electron-window-preferences.ts
+++ b/packages/core/src/electron-browser/window/electron-window-preferences.ts
@@ -16,6 +16,7 @@
import { interfaces } from 'inversify';
import { createPreferenceProxy, PreferenceContribution, PreferenceProxy, PreferenceSchema, PreferenceService } from '../../browser/preferences';
+import { isOSX, isWindows } from '../../common';
export namespace ZoomLevel {
export const DEFAULT = 0;
@@ -38,11 +39,25 @@ export const electronWindowPreferencesSchema: PreferenceSchema = {
// eslint-disable-next-line max-len
'description': 'Adjust the zoom level of the window. The original size is 0 and each increment above (e.g. 1.0) or below (e.g. -1.0) represents zooming 20% larger or smaller. You can also enter decimals to adjust the zoom level with a finer granularity.'
},
+ 'window.titleBarStyle': {
+ type: 'string',
+ enum: ['native', 'custom'],
+ markdownEnumDescriptions: [
+ 'Native title bar is displayed.',
+ 'Custom title bar is displayed.'
+ ],
+ default: isWindows ? 'custom' : 'native',
+ scope: 'application',
+ // eslint-disable-next-line max-len
+ markdownDescription: 'Adjust the appearance of the window title bar. On Linux and Windows, this setting also affects the application and context menu appearances. Changes require a full restart to apply.',
+ included: !isOSX
+ },
}
};
export class ElectronWindowConfiguration {
'window.zoomLevel': number;
+ 'window.titleBarStyle': 'native' | 'custom';
}
export const ElectronWindowPreferenceContribution = Symbol('ElectronWindowPreferenceContribution');
diff --git a/packages/core/src/electron-common/messaging/electron-messages.ts b/packages/core/src/electron-common/messaging/electron-messages.ts
new file mode 100644
index 0000000000000..b64f3db3d9e92
--- /dev/null
+++ b/packages/core/src/electron-common/messaging/electron-messages.ts
@@ -0,0 +1,20 @@
+/********************************************************************************
+ * Copyright (C) 2021 TypeFox 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
+ ********************************************************************************/
+
+export const RequestTitleBarStyle = 'requestTitleBarStyle';
+export const TitleBarStyleChanged = 'titleBarStyleChanged';
+export const TitleBarStyleAtStartup = 'titleBarStyleAtStartup';
+export const Restart = 'restart';
diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts
index 5c4a3fa35c8bf..a9fca66448382 100644
--- a/packages/core/src/electron-main/electron-main-application.ts
+++ b/packages/core/src/electron-main/electron-main-application.ts
@@ -15,7 +15,7 @@
********************************************************************************/
import { inject, injectable, named } from 'inversify';
-import { screen, globalShortcut, app, BrowserWindow, BrowserWindowConstructorOptions, Event as ElectronEvent } from '../../shared/electron';
+import { screen, globalShortcut, ipcMain, app, BrowserWindow, BrowserWindowConstructorOptions, Event as ElectronEvent } from '../../shared/electron';
import * as path from 'path';
import { Argv } from 'yargs';
import { AddressInfo } from 'net';
@@ -32,6 +32,8 @@ import { ElectronSecurityToken } from '../electron-common/electron-token';
import Storage = require('electron-store');
// eslint-disable-next-line @theia/runtime-import-check
import { DEFAULT_WINDOW_HASH } from '../browser/window/window-service';
+import { isOSX, isWindows } from '../common';
+import { RequestTitleBarStyle, Restart, TitleBarStyleAtStartup, TitleBarStyleChanged } from '../electron-common/messaging/electron-messages';
const createYargs: (argv?: string[], cwd?: string) => Argv = require('yargs/yargs');
@@ -182,6 +184,10 @@ export class ElectronMainApplication {
readonly backendPort = this._backendPort.promise;
protected _config: FrontendApplicationConfig | undefined;
+ protected useNativeWindowFrame: boolean = true;
+ protected didUseNativeWindowFrameOnStart = new Map();
+ protected restarting = false;
+
get config(): FrontendApplicationConfig {
if (!this._config) {
throw new Error('You have to start the application first.');
@@ -190,6 +196,7 @@ export class ElectronMainApplication {
}
async start(config: FrontendApplicationConfig): Promise {
+ this.useNativeWindowFrame = this.getTitleBarStyle(config) === 'native';
this._config = config;
this.hookApplicationEvents();
const port = await this.startBackend();
@@ -204,6 +211,23 @@ export class ElectronMainApplication {
});
}
+ protected getTitleBarStyle(config: FrontendApplicationConfig): 'native' | 'custom' {
+ if (isOSX) {
+ return 'native';
+ }
+ const storedFrame = this.electronStore.get('windowstate')?.frame;
+ if (storedFrame !== undefined) {
+ return !!storedFrame ? 'native' : 'custom';
+ }
+ if (config.preferences && config.preferences['window.titleBarStyle']) {
+ const titleBarStyle = config.preferences['window.titleBarStyle'];
+ if (titleBarStyle === 'native' || titleBarStyle === 'custom') {
+ return titleBarStyle;
+ }
+ }
+ return isWindows ? 'custom' : 'native';
+ }
+
protected async launch(params: ElectronMainExecutionParams): Promise {
createYargs(params.argv, params.cwd)
.command('$0 [file]', false,
@@ -233,6 +257,7 @@ export class ElectronMainApplication {
async getLastWindowOptions(): Promise {
const windowState: TheiaBrowserWindowOptions | undefined = this.electronStore.get('windowstate') || this.getDefaultTheiaWindowOptions();
return {
+ frame: this.useNativeWindowFrame,
...windowState,
...this.getDefaultOptions()
};
@@ -327,6 +352,7 @@ export class ElectronMainApplication {
const y = Math.round(bounds.y + (bounds.height - height) / 2);
const x = Math.round(bounds.x + (bounds.width - width) / 2);
return {
+ frame: this.useNativeWindowFrame,
isFullScreen: false,
isMaximized: false,
width,
@@ -348,31 +374,41 @@ export class ElectronMainApplication {
* Save the window geometry state on every change.
*/
protected attachSaveWindowState(electronWindow: BrowserWindow): void {
- const saveWindowState = () => {
- try {
- const bounds = electronWindow.getBounds();
- this.electronStore.set('windowstate', {
- isFullScreen: electronWindow.isFullScreen(),
- isMaximized: electronWindow.isMaximized(),
- width: bounds.width,
- height: bounds.height,
- x: bounds.x,
- y: bounds.y
- });
- } catch (e) {
- console.error('Error while saving window state:', e);
- }
- };
let delayedSaveTimeout: NodeJS.Timer | undefined;
const saveWindowStateDelayed = () => {
if (delayedSaveTimeout) {
clearTimeout(delayedSaveTimeout);
}
- delayedSaveTimeout = setTimeout(saveWindowState, 1000);
+ delayedSaveTimeout = setTimeout(() => this.saveWindowState(electronWindow), 1000);
};
- electronWindow.on('close', saveWindowState);
+ electronWindow.on('close', () => {
+ this.saveWindowState(electronWindow);
+ this.didUseNativeWindowFrameOnStart.delete(electronWindow.id);
+ });
electronWindow.on('resize', saveWindowStateDelayed);
electronWindow.on('move', saveWindowStateDelayed);
+ this.didUseNativeWindowFrameOnStart.set(electronWindow.id, this.useNativeWindowFrame);
+ }
+
+ protected saveWindowState(electronWindow: BrowserWindow): void {
+ // In some circumstances the `electronWindow` can be `null`
+ if (!electronWindow) {
+ return;
+ }
+ try {
+ const bounds = electronWindow.getBounds();
+ this.electronStore.set('windowstate', {
+ isFullScreen: electronWindow.isFullScreen(),
+ isMaximized: electronWindow.isMaximized(),
+ width: bounds.width,
+ height: bounds.height,
+ x: bounds.x,
+ y: bounds.y,
+ frame: this.useNativeWindowFrame
+ });
+ } catch (e) {
+ console.error('Error while saving window state:', e);
+ }
}
/**
@@ -477,6 +513,19 @@ export class ElectronMainApplication {
app.on('will-quit', this.onWillQuit.bind(this));
app.on('second-instance', this.onSecondInstance.bind(this));
app.on('window-all-closed', this.onWindowAllClosed.bind(this));
+
+ ipcMain.on(TitleBarStyleChanged, ({ sender }, titleBarStyle: string) => {
+ this.useNativeWindowFrame = titleBarStyle === 'native';
+ this.saveWindowState(BrowserWindow.fromId(sender.id));
+ });
+
+ ipcMain.on(Restart, ({ sender }) => {
+ this.restart(sender.id);
+ });
+
+ ipcMain.on(RequestTitleBarStyle, ({ sender }) => {
+ sender.send(TitleBarStyleAtStartup, this.didUseNativeWindowFrameOnStart.get(sender.id) ? 'native' : 'custom');
+ });
}
protected onWillQuit(event: ElectronEvent): void {
@@ -495,7 +544,23 @@ export class ElectronMainApplication {
}
protected onWindowAllClosed(event: ElectronEvent): void {
- this.requestStop();
+ if (!this.restarting) {
+ this.requestStop();
+ }
+ }
+
+ protected restart(id: number): void {
+ this.restarting = true;
+ const window = BrowserWindow.fromId(id);
+ window.on('closed', async () => {
+ await this.launch({
+ secondInstance: false,
+ argv: this.processArgv.getProcessArgvWithoutBin(process.argv),
+ cwd: process.cwd()
+ });
+ this.restarting = false;
+ });
+ window.close();
}
protected async startContributions(): Promise {