diff --git a/CHANGELOG.md b/CHANGELOG.md
index c158c6621db42..7a837366e7eee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,8 +7,12 @@
[Breaking Changes:](#breaking_changes_1.18.0)
- [core] added `BreadcrumbsRendererFactory` to constructor arguments of `DockPanelRenderer` and `ToolbarAwareTabBar`. [#9920](https://github.com/eclipse-theia/theia/pull/9920)
-- [task] `TaskDefinition.properties.required` is now optional to align with the specification [#10015](https://github.com/eclipse-theia/theia/pull/10015)
- [core] `setTopPanelVisibily` renamed to `setTopPanelVisibility` [#10020](https://github.com/eclipse-theia/theia/pull/10020)
+- [electron] `ElectronMainMenuFactory` now inherits from `BrowserMainMenuFactory` and had its methods renamed. [#10044](https://github.com/eclipse-theia/theia/pull/10044)
+ - renamed `handleDefault` to `handleElectronDefault`
+ - renamed `createContextMenu` to `createElectronContextMenu`
+ - renamed `createMenuBar` to `createElectronMenuBar`
+- [task] `TaskDefinition.properties.required` is now optional to align with the specification [#10015](https://github.com/eclipse-theia/theia/pull/10015)
## v1.17.2 - 9/1/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/electron-browser/menu/electron-context-menu-renderer.ts b/packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts
index c73b72d4565d2..ab58f06aaf905 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,14 @@
/* 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';
export class ElectronContextMenuAccess extends ContextMenuAccess {
constructor(readonly menu: electron.Menu) {
@@ -73,27 +76,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 customTitleBarStyle: boolean = false;
+
+ constructor(@inject(ElectronMainMenuFactory) private electronMenuFactory: ElectronMainMenuFactory) {
+ super(electronMenuFactory);
+ }
+
+ @postConstruct()
+ protected async init(): Promise {
+ electron.ipcRenderer.on('original-titleBarStyle', (_event, style: string) => {
+ this.customTitleBarStyle = style === 'custom';
+ });
+ electron.ipcRenderer.send('request-titleBarStyle');
}
- 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.customTitleBarStyle) {
+ return super.doRender(options);
+ } else {
+ 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);
}
- 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..471a37ed351b5 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,14 @@ 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, PreferenceService, 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 { 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,7 +75,7 @@ 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;
@@ -80,30 +83,28 @@ export class ElectronMenuContribution implements FrontendApplicationContribution
@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 +120,30 @@ export class ElectronMenuContribution implements FrontendApplicationContribution
});
}
+ handleTitleBarStyling(app: FrontendApplication): void {
+ this.hideTopPanel(app);
+ electron.ipcRenderer.on('original-titleBarStyle', (_event, style: string) => {
+ this.titleBarStyle = style;
+ this.preferenceService.ready.then(() => {
+ this.preferenceService.set('window.titleBarStyle', this.titleBarStyle, PreferenceScope.User);
+ });
+ });
+ electron.ipcRenderer.send('request-titleBarStyle');
+ this.preferenceService.ready.then(() => {
+ this.setMenu(app);
+ electron.remote.getCurrentWindow().setMenuBarVisibility(true);
+ });
+ this.preferenceService.onPreferenceChanged(change => {
+ if (change.preferenceName === 'window.titleBarStyle') {
+ if (this.titleBarStyleChangeFlag && this.titleBarStyle !== change.newValue && electron.remote.getCurrentWindow().isFocused()) {
+ electron.ipcRenderer.send('titleBarStyle-changed', change.newValue);
+ this.handleRequiredRestart();
+ }
+ this.titleBarStyleChangeFlag = true;
+ }
+ });
+ }
+
handleToggleMaximized(): void {
const preference = this.preferenceService.get('window.menuBarVisibility');
if (preference === 'classic') {
@@ -129,16 +154,16 @@ export class ElectronMenuContribution implements FrontendApplicationContribution
/**
* Makes the `theia-top-panel` hidden as it is unused for the electron-based application.
* 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.setHidden(this.titleBarStyle !== 'custom');
child = undefined;
} else {
child = itr.next();
@@ -146,12 +171,73 @@ export class ElectronMenuContribution implements FrontendApplicationContribution
}
}
- 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) {
+ const dragPanel = new Widget();
+ dragPanel.id = 'theia-drag-panel';
+ app.shell.addWidget(dragPanel, { area: 'top' });
+ const logo = this.createLogo();
+ app.shell.addWidget(logo, { area: 'top' });
+ const menu = this.factory.createMenuBar();
+ app.shell.addWidget(menu, { area: 'top' });
+ 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));
+ }
+ });
+ 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);
+ }
// Unix/Windows: Set the per-window menus
- electronWindow.setMenu(menu);
+ electronWindow.setMenu(electronMenu);
+ }
+ }
+
+ 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..2e4e20e60db96
--- /dev/null
+++ b/packages/core/src/electron-browser/menu/electron-menu-style.css
@@ -0,0 +1,87 @@
+/********************************************************************************
+ * 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 {
+ grid-row: 1 / span 1;
+ 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);
+}
+
+#close-button:hover {
+ background: #E81123 !important;
+}
+
+#close-button:hover:before {
+ color: white;
+}
+
+#restore-button {
+ display: none !important;
+}
+
+.maximized #restore-button {
+ display: flex !important;
+}
+
+.maximized #max-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-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts
index 67ddc09dbb1f2..71de5368cb2e3 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';
@@ -31,6 +31,7 @@ import { ElectronSecurityTokenService } from './electron-security-token-service'
import { ElectronSecurityToken } from '../electron-common/electron-token';
import Storage = require('electron-store');
import { DEFAULT_WINDOW_HASH } from '../browser/window/window-service';
+import { isOSX, isWindows } from '../common';
const createYargs: (argv?: string[], cwd?: string) => Argv = require('yargs/yargs');
/**
@@ -180,6 +181,10 @@ export class ElectronMainApplication {
readonly backendPort = this._backendPort.promise;
protected _config: FrontendApplicationConfig | undefined;
+ protected frame: boolean = true;
+ protected originalFrames = new Map();
+ protected restarting = false;
+
get config(): FrontendApplicationConfig {
if (!this._config) {
throw new Error('You have to start the application first.');
@@ -188,6 +193,7 @@ export class ElectronMainApplication {
}
async start(config: FrontendApplicationConfig): Promise {
+ this.frame = this.getTitleBarStyle(config) === 'native';
this._config = config;
this.hookApplicationEvents();
const port = await this.startBackend();
@@ -202,6 +208,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,
@@ -231,6 +254,7 @@ export class ElectronMainApplication {
async getLastWindowOptions(): Promise {
const windowState: TheiaBrowserWindowOptions | undefined = this.electronStore.get('windowstate') || this.getDefaultTheiaWindowOptions();
return {
+ frame: this.frame,
...windowState,
...this.getDefaultOptions()
};
@@ -325,6 +349,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.frame,
isFullScreen: false,
isMaximized: false,
width,
@@ -346,31 +371,37 @@ 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.originalFrames.delete(electronWindow.id);
+ });
electronWindow.on('resize', saveWindowStateDelayed);
electronWindow.on('move', saveWindowStateDelayed);
+ this.originalFrames.set(electronWindow.id, this.frame);
+ }
+
+ protected saveWindowState(electronWindow: BrowserWindow): void {
+ 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.frame
+ });
+ } catch (e) {
+ console.error('Error while saving window state:', e);
+ }
}
/**
@@ -475,6 +506,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('titleBarStyle-changed', ({ sender }, titleBarStyle: string) => {
+ this.frame = titleBarStyle === 'native';
+ this.saveWindowState(BrowserWindow.fromId(sender.id));
+ });
+
+ ipcMain.on('restart', ({ sender }) => {
+ this.restart(sender.id);
+ });
+
+ ipcMain.on('request-titleBarStyle', ({ sender }) => {
+ sender.send('original-titleBarStyle', this.originalFrames.get(sender.id) ? 'native' : 'custom');
+ });
}
protected onWillQuit(event: ElectronEvent): void {
@@ -493,7 +537,22 @@ export class ElectronMainApplication {
}
protected onWindowAllClosed(event: ElectronEvent): void {
- this.requestStop();
+ if (!this.restarting) {
+ this.requestStop();
+ }
+ }
+
+ protected restart(id: number): void {
+ this.restarting = true;
+ BrowserWindow.fromId(id).close();
+ setTimeout(async () => {
+ await this.launch({
+ secondInstance: false,
+ argv: this.processArgv.getProcessArgvWithoutBin(process.argv),
+ cwd: process.cwd()
+ });
+ this.restarting = false;
+ }, 500);
}
protected async startContributions(): Promise {