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 {