Skip to content

Commit 05fe6a0

Browse files
committed
Support vscode's titleBarStyle
1 parent 5c5b5ba commit 05fe6a0

File tree

10 files changed

+321
-72
lines changed

10 files changed

+321
-72
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
<a name="breaking_changes_1.18.0">[Breaking Changes:](#breaking_changes_1.18.0)</a>
88

99
- [core] added `BreadcrumbsRendererFactory` to constructor arguments of `DockPanelRenderer` and `ToolbarAwareTabBar`. [#9920](https://github.com/eclipse-theia/theia/pull/9920)
10-
10+
- [electron] `ElectronMainMenuFactory` now inherits from `BrowserMainMenuFactory` and had its methods renamed. [#10044](https://github.com/eclipse-theia/theia/pull/10044)
11+
- renamed `handleDefault` to `handleElectronDefault`
12+
- renamed `createContextMenu` to `createElectronContextMenu`
13+
- renamed `createMenuBar` to `createElectronMenuBar`
1114
## v1.17.2 - 9/1/2021
1215

1316
[1.17.2 Milestone](https://github.com/eclipse-theia/theia/milestone/27)

examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
2727
class SampleElectronMainMenuFactory extends ElectronMainMenuFactory {
2828

2929
// eslint-disable-next-line @typescript-eslint/no-explicit-any
30-
protected handleDefault(menuNode: CompositeMenuNode, args: any[] = [], options?: ElectronMenuOptions): Electron.MenuItemConstructorOptions[] {
30+
protected handleElectronDefault(menuNode: CompositeMenuNode, args: any[] = [], options?: ElectronMenuOptions): Electron.MenuItemConstructorOptions[] {
3131
if (menuNode instanceof PlaceholderMenuNode) {
3232
return [{
3333
label: menuNode.label,

examples/api-samples/src/electron-browser/updater/sample-updater-frontend-contribution.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export class ElectronMenuUpdater {
9090
this.setMenu();
9191
}
9292

93-
private setMenu(menu: Menu | null = this.factory.createMenuBar(), electronWindow: BrowserWindow = remote.getCurrentWindow()): void {
93+
private setMenu(menu: Menu | null = this.factory.createElectronMenuBar(), electronWindow: BrowserWindow = remote.getCurrentWindow()): void {
9494
if (isOSX) {
9595
remote.Menu.setApplicationMenu(menu);
9696
} else {

packages/core/src/browser/menu/browser-context-menu-renderer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class BrowserContextMenuRenderer extends ContextMenuRenderer {
3636
super();
3737
}
3838

39-
protected doRender({ menuPath, anchor, args, onHide }: RenderContextMenuOptions): BrowserContextMenuAccess {
39+
protected doRender({ menuPath, anchor, args, onHide }: RenderContextMenuOptions): ContextMenuAccess {
4040
const contextMenu = this.menuFactory.createContextMenu(menuPath, args);
4141
const { x, y } = coordinateFromAnchor(anchor);
4242
if (onHide) {

packages/core/src/electron-browser/menu/electron-context-menu-renderer.ts

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@
1717
/* eslint-disable @typescript-eslint/no-explicit-any */
1818

1919
import * as electron from '../../../shared/electron';
20-
import { inject, injectable } from 'inversify';
21-
import { ContextMenuRenderer, RenderContextMenuOptions, ContextMenuAccess, FrontendApplicationContribution, CommonCommands, coordinateFromAnchor } from '../../browser';
20+
import { inject, injectable, postConstruct } from 'inversify';
21+
import {
22+
ContextMenuRenderer, RenderContextMenuOptions, ContextMenuAccess, FrontendApplicationContribution, CommonCommands, coordinateFromAnchor, PreferenceService
23+
} from '../../browser';
2224
import { ElectronMainMenuFactory } from './electron-main-menu-factory';
2325
import { ContextMenuContext } from '../../browser/menu/context-menu-context';
2426
import { MenuPath, MenuContribution, MenuModelRegistry } from '../../common';
27+
import { BrowserContextMenuRenderer } from '../../browser/menu/browser-context-menu-renderer';
2528

2629
export class ElectronContextMenuAccess extends ContextMenuAccess {
2730
constructor(readonly menu: electron.Menu) {
@@ -73,27 +76,48 @@ export class ElectronTextInputContextMenuContribution implements FrontendApplica
7376
}
7477

7578
@injectable()
76-
export class ElectronContextMenuRenderer extends ContextMenuRenderer {
79+
export class ElectronContextMenuRenderer extends BrowserContextMenuRenderer {
7780

7881
@inject(ContextMenuContext)
7982
protected readonly context: ContextMenuContext;
8083

81-
constructor(@inject(ElectronMainMenuFactory) private menuFactory: ElectronMainMenuFactory) {
82-
super();
84+
@inject(PreferenceService)
85+
protected readonly preferenceService: PreferenceService;
86+
87+
protected customTitleBarStyle: boolean = false;
88+
89+
constructor(@inject(ElectronMainMenuFactory) private electronMenuFactory: ElectronMainMenuFactory) {
90+
super(electronMenuFactory);
91+
}
92+
93+
@postConstruct()
94+
protected async init(): Promise<void> {
95+
const isCustom = (style?: string): boolean => (style || this.preferenceService.get('window.titleBarStyle')) === 'custom';
96+
await this.preferenceService.ready;
97+
this.customTitleBarStyle = isCustom();
98+
electron.ipcRenderer.on('original-titleBarStyle', (_event, style: string) => {
99+
this.customTitleBarStyle = isCustom(style);
100+
});
101+
electron.ipcRenderer.send('request-titleBarStyle');
83102
}
84103

85-
protected doRender({ menuPath, anchor, args, onHide }: RenderContextMenuOptions): ElectronContextMenuAccess {
86-
const menu = this.menuFactory.createContextMenu(menuPath, args);
87-
const { x, y } = coordinateFromAnchor(anchor);
88-
const zoom = electron.webFrame.getZoomFactor();
89-
// x and y values must be Ints or else there is a conversion error
90-
menu.popup({ x: Math.round(x * zoom), y: Math.round(y * zoom) });
91-
// native context menu stops the event loop, so there is no keyboard events
92-
this.context.resetAltPressed();
93-
if (onHide) {
94-
menu.once('menu-will-close', () => onHide());
104+
protected doRender(options: RenderContextMenuOptions): ContextMenuAccess {
105+
if (this.customTitleBarStyle) {
106+
return super.doRender(options);
107+
} else {
108+
const { menuPath, anchor, args, onHide } = options;
109+
const menu = this.electronMenuFactory.createElectronContextMenu(menuPath, args);
110+
const { x, y } = coordinateFromAnchor(anchor);
111+
const zoom = electron.webFrame.getZoomFactor();
112+
// x and y values must be Ints or else there is a conversion error
113+
menu.popup({ x: Math.round(x * zoom), y: Math.round(y * zoom) });
114+
// native context menu stops the event loop, so there is no keyboard events
115+
this.context.resetAltPressed();
116+
if (onHide) {
117+
menu.once('menu-will-close', () => onHide());
118+
}
119+
return new ElectronContextMenuAccess(menu);
95120
}
96-
return new ElectronContextMenuAccess(menu);
97121
}
98122

99123
}

packages/core/src/electron-browser/menu/electron-main-menu-factory.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,9 @@ import {
2424
} from '../../common';
2525
import { Keybinding } from '../../common/keybinding';
2626
import { PreferenceService, KeybindingRegistry, CommonCommands } from '../../browser';
27-
import { ContextKeyService } from '../../browser/context-key-service';
2827
import debounce = require('lodash.debounce');
29-
import { ContextMenuContext } from '../../browser/menu/context-menu-context';
3028
import { MAXIMIZED_CLASS } from '../../browser/shell/theia-dock-panel';
29+
import { BrowserMainMenuFactory } from '../../browser/menu/browser-menu-plugin';
3130

3231
/**
3332
* Representation of possible electron menu options.
@@ -55,23 +54,18 @@ export type ElectronMenuItemRole = ('undo' | 'redo' | 'cut' | 'copy' | 'paste' |
5554
'moveTabToNewWindow' | 'windowMenu');
5655

5756
@injectable()
58-
export class ElectronMainMenuFactory {
57+
export class ElectronMainMenuFactory extends BrowserMainMenuFactory {
5958

6059
protected _menu: Electron.Menu | undefined;
6160
protected _toggledCommands: Set<string> = new Set();
6261

63-
@inject(ContextKeyService)
64-
protected readonly contextKeyService: ContextKeyService;
65-
66-
@inject(ContextMenuContext)
67-
protected readonly context: ContextMenuContext;
68-
6962
constructor(
7063
@inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry,
7164
@inject(PreferenceService) protected readonly preferencesService: PreferenceService,
7265
@inject(MenuModelRegistry) protected readonly menuProvider: MenuModelRegistry,
7366
@inject(KeybindingRegistry) protected readonly keybindingRegistry: KeybindingRegistry
7467
) {
68+
super();
7569
preferencesService.onPreferenceChanged(
7670
debounce(e => {
7771
if (e.preferenceName === 'window.menuBarVisibility') {
@@ -92,15 +86,16 @@ export class ElectronMainMenuFactory {
9286

9387
async setMenuBar(): Promise<void> {
9488
await this.preferencesService.ready;
95-
const createdMenuBar = this.createMenuBar();
9689
if (isOSX) {
90+
const createdMenuBar = this.createElectronMenuBar();
9791
electron.remote.Menu.setApplicationMenu(createdMenuBar);
98-
} else {
92+
} else if (this.preferencesService.get('window.titleBarStyle') === 'native') {
93+
const createdMenuBar = this.createElectronMenuBar();
9994
electron.remote.getCurrentWindow().setMenu(createdMenuBar);
10095
}
10196
}
10297

103-
createMenuBar(): Electron.Menu | null {
98+
createElectronMenuBar(): Electron.Menu | null {
10499
const preference = this.preferencesService.get<string>('window.menuBarVisibility') || 'classic';
105100
const maxWidget = document.getElementsByClassName(MAXIMIZED_CLASS);
106101
if (preference === 'visible' || (preference === 'classic' && maxWidget.length === 0)) {
@@ -118,7 +113,7 @@ export class ElectronMainMenuFactory {
118113
return null;
119114
}
120115

121-
createContextMenu(menuPath: MenuPath, args?: any[]): Electron.Menu {
116+
createElectronContextMenu(menuPath: MenuPath, args?: any[]): Electron.Menu {
122117
const menuModel = this.menuProvider.getMenu(menuPath);
123118
const template = this.fillMenuTemplate([], menuModel, args, { showDisabled: false });
124119
return electron.remote.Menu.buildFromTemplate(template);
@@ -221,13 +216,13 @@ export class ElectronMainMenuFactory {
221216
this._toggledCommands.add(commandId);
222217
}
223218
} else {
224-
items.push(...this.handleDefault(menu, args, options));
219+
items.push(...this.handleElectronDefault(menu, args, options));
225220
}
226221
}
227222
return items;
228223
}
229224

230-
protected handleDefault(menuNode: MenuNode, args: any[] = [], options?: ElectronMenuOptions): Electron.MenuItemConstructorOptions[] {
225+
protected handleElectronDefault(menuNode: MenuNode, args: any[] = [], options?: ElectronMenuOptions): Electron.MenuItemConstructorOptions[] {
231226
return [];
232227
}
233228

packages/core/src/electron-browser/menu/electron-menu-contribution.ts

Lines changed: 102 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@ import {
2020
Command, CommandContribution, CommandRegistry,
2121
isOSX, isWindows, MenuModelRegistry, MenuContribution, Disposable
2222
} from '../../common';
23-
import { ApplicationShell, KeybindingContribution, KeybindingRegistry, PreferenceScope, PreferenceService } from '../../browser';
23+
import { ApplicationShell, codicon, ConfirmDialog, KeybindingContribution, KeybindingRegistry, PreferenceScope, PreferenceService, Widget } from '../../browser';
2424
import { FrontendApplication, FrontendApplicationContribution, CommonMenus } from '../../browser';
2525
import { ElectronMainMenuFactory } from './electron-main-menu-factory';
2626
import { FrontendApplicationStateService, FrontendApplicationState } from '../../browser/frontend-application-state';
2727
import { ZoomLevel } from '../window/electron-window-preferences';
28+
import { BrowserMenuBarContribution } from '../../browser/menu/browser-menu-plugin';
29+
30+
import '../../../src/electron-browser/menu/electron-menu-style.css';
2831

2932
export namespace ElectronCommands {
3033
export const TOGGLE_DEVELOPER_TOOLS: Command = {
@@ -72,38 +75,36 @@ export namespace ElectronMenus {
7275
}
7376

7477
@injectable()
75-
export class ElectronMenuContribution implements FrontendApplicationContribution, CommandContribution, MenuContribution, KeybindingContribution {
78+
export class ElectronMenuContribution extends BrowserMenuBarContribution implements FrontendApplicationContribution, CommandContribution, MenuContribution, KeybindingContribution {
7679

7780
@inject(FrontendApplicationStateService)
7881
protected readonly stateService: FrontendApplicationStateService;
7982

8083
@inject(PreferenceService)
8184
protected readonly preferenceService: PreferenceService;
8285

86+
protected titleBarStyleChangeFlag = false;
87+
protected titleBarStyle?: string;
88+
8389
constructor(
8490
@inject(ElectronMainMenuFactory) protected readonly factory: ElectronMainMenuFactory,
8591
@inject(ApplicationShell) protected shell: ApplicationShell
86-
) { }
92+
) {
93+
super(factory);
94+
}
8795

8896
onStart(app: FrontendApplication): void {
89-
this.hideTopPanel(app);
90-
this.preferenceService.ready.then(() => {
91-
this.setMenu();
92-
electron.remote.getCurrentWindow().setMenuBarVisibility(true);
93-
});
97+
this.handleTitleBarStyling(app);
9498
if (isOSX) {
9599
// OSX: Recreate the menus when changing windows.
96100
// OSX only has one menu bar for all windows, so we need to swap
97101
// between them as the user switches windows.
98-
electron.remote.getCurrentWindow().on('focus', () => this.setMenu());
102+
electron.remote.getCurrentWindow().on('focus', () => this.setMenu(app));
99103
}
100104
// Make sure the application menu is complete, once the frontend application is ready.
101105
// https://github.com/theia-ide/theia/issues/5100
102106
let onStateChange: Disposable | undefined = undefined;
103107
const stateServiceListener = (state: FrontendApplicationState) => {
104-
if (state === 'ready') {
105-
this.setMenu();
106-
}
107108
if (state === 'closing_window') {
108109
if (!!onStateChange) {
109110
onStateChange.dispose();
@@ -119,6 +120,28 @@ export class ElectronMenuContribution implements FrontendApplicationContribution
119120
});
120121
}
121122

123+
handleTitleBarStyling(app: FrontendApplication): void {
124+
this.hideTopPanel(app);
125+
electron.ipcRenderer.on('original-titleBarStyle', (_event, style: string) => {
126+
this.titleBarStyle = style;
127+
});
128+
electron.ipcRenderer.send('request-titleBarStyle');
129+
this.preferenceService.ready.then(() => {
130+
this.titleBarStyle = this.titleBarStyle ?? this.preferenceService.get('window.titleBarStyle');
131+
this.setMenu(app);
132+
electron.remote.getCurrentWindow().setMenuBarVisibility(true);
133+
setTimeout(() => {
134+
this.titleBarStyleChangeFlag = true;
135+
}, 1000);
136+
});
137+
this.preferenceService.onPreferenceChanged(change => {
138+
if (change.preferenceName === 'window.titleBarStyle' && this.titleBarStyleChangeFlag && electron.remote.getCurrentWindow().isFocused()) {
139+
electron.ipcRenderer.send('titleBarStyle-changed', change.newValue);
140+
this.handleRequiredRestart();
141+
}
142+
});
143+
}
144+
122145
handleToggleMaximized(): void {
123146
const preference = this.preferenceService.get('window.menuBarVisibility');
124147
if (preference === 'classic') {
@@ -129,29 +152,90 @@ export class ElectronMenuContribution implements FrontendApplicationContribution
129152
/**
130153
* Makes the `theia-top-panel` hidden as it is unused for the electron-based application.
131154
* The `theia-top-panel` is used as the container of the main, application menu-bar for the
132-
* browser. Electron has it's own.
155+
* browser. Native Electron has it's own.
133156
* By default, this method is called on application `onStart`.
134157
*/
135158
protected hideTopPanel(app: FrontendApplication): void {
136159
const itr = app.shell.children();
137160
let child = itr.next();
138161
while (child) {
139-
// Top panel for the menu contribution is not required for Electron.
162+
// Top panel for the menu contribution is not required for native Electron title bar.
140163
if (child.id === 'theia-top-panel') {
141-
child.setHidden(true);
164+
child.setHidden(this.titleBarStyle !== 'custom');
142165
child = undefined;
143166
} else {
144167
child = itr.next();
145168
}
146169
}
147170
}
148171

149-
private setMenu(menu: electron.Menu | null = this.factory.createMenuBar(), electronWindow: electron.BrowserWindow = electron.remote.getCurrentWindow()): void {
172+
protected setMenu(app: FrontendApplication, electronMenu: electron.Menu | null = this.factory.createElectronMenuBar(),
173+
electronWindow: electron.BrowserWindow = electron.remote.getCurrentWindow()): void {
150174
if (isOSX) {
151-
electron.remote.Menu.setApplicationMenu(menu);
175+
electron.remote.Menu.setApplicationMenu(electronMenu);
152176
} else {
177+
this.hideTopPanel(app);
178+
if (this.titleBarStyle === 'custom' && !this.menuBar) {
179+
const dragPanel = new Widget();
180+
dragPanel.id = 'theia-drag-panel';
181+
app.shell.addWidget(dragPanel, { area: 'top' });
182+
const logo = this.createLogo();
183+
app.shell.addWidget(logo, { area: 'top' });
184+
const menu = this.factory.createMenuBar();
185+
app.shell.addWidget(menu, { area: 'top' });
186+
menu.setHidden(['compact', 'hidden'].includes(this.preferenceService.get('window.menuBarVisibility', '')));
187+
this.preferenceService.onPreferenceChanged(change => {
188+
if (change.preferenceName === 'window.menuBarVisibility') {
189+
menu.setHidden(['compact', 'hidden'].includes(change.newValue));
190+
}
191+
});
192+
const controls = document.createElement('div');
193+
controls.id = 'window-controls';
194+
controls.append(
195+
this.createControlButton('minimize', () => electronWindow.minimize()),
196+
this.createControlButton('maximize', () => electronWindow.maximize()),
197+
this.createControlButton('restore', () => electronWindow.unmaximize()),
198+
this.createControlButton('close', () => electronWindow.close())
199+
);
200+
app.shell.topPanel.node.append(controls);
201+
this.handleWindowControls(electronWindow);
202+
}
153203
// Unix/Windows: Set the per-window menus
154-
electronWindow.setMenu(menu);
204+
electronWindow.setMenu(electronMenu);
205+
}
206+
}
207+
208+
protected handleWindowControls(electronWindow: electron.BrowserWindow): void {
209+
toggleControlButtons();
210+
electronWindow.on('maximize', toggleControlButtons);
211+
electronWindow.on('unmaximize', toggleControlButtons);
212+
213+
function toggleControlButtons(): void {
214+
if (electronWindow.isMaximized()) {
215+
document.body.classList.add('maximized');
216+
} else {
217+
document.body.classList.remove('maximized');
218+
}
219+
}
220+
}
221+
222+
protected createControlButton(id: string, handler: () => void): HTMLElement {
223+
const button = document.createElement('div');
224+
button.id = `${id}-button`;
225+
button.className = `control-button ${codicon(`chrome-${id}`)}`;
226+
button.addEventListener('click', handler);
227+
return button;
228+
}
229+
230+
protected async handleRequiredRestart(): Promise<void> {
231+
const dialog = new ConfirmDialog({
232+
title: 'A setting has changed that requires a restart to take effect',
233+
msg: 'Press the restart button to restart the application and enable the setting.',
234+
ok: 'Restart',
235+
cancel: 'Cancel'
236+
});
237+
if (await dialog.open()) {
238+
electron.ipcRenderer.send('restart');
155239
}
156240
}
157241

0 commit comments

Comments
 (0)