Skip to content

Commit

Permalink
Support vscode's titleBarStyle
Browse files Browse the repository at this point in the history
  • Loading branch information
msujew committed Sep 9, 2021
1 parent 5c5b5ba commit 05fe6a0
Show file tree
Hide file tree
Showing 10 changed files with 321 additions and 72 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
<a name="breaking_changes_1.18.0">[Breaking Changes:](#breaking_changes_1.18.0)</a>

- [core] added `BreadcrumbsRendererFactory` to constructor arguments of `DockPanelRenderer` and `ToolbarAwareTabBar`. [#9920](https://github.com/eclipse-theia/theia/pull/9920)

- [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`
## v1.17.2 - 9/1/2021

[1.17.2 Milestone](https://github.com/eclipse-theia/theia/milestone/27)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -73,27 +76,48 @@ 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<void> {
const isCustom = (style?: string): boolean => (style || this.preferenceService.get('window.titleBarStyle')) === 'custom';
await this.preferenceService.ready;
this.customTitleBarStyle = isCustom();
electron.ipcRenderer.on('original-titleBarStyle', (_event, style: string) => {
this.customTitleBarStyle = isCustom(style);
});
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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<string> = 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') {
Expand All @@ -92,15 +86,16 @@ export class ElectronMainMenuFactory {

async setMenuBar(): Promise<void> {
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<string>('window.menuBarVisibility') || 'classic';
const maxWidget = document.getElementsByClassName(MAXIMIZED_CLASS);
if (preference === 'visible' || (preference === 'classic' && maxWidget.length === 0)) {
Expand All @@ -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);
Expand Down Expand Up @@ -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 [];
}

Expand Down
120 changes: 102 additions & 18 deletions packages/core/src/electron-browser/menu/electron-menu-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -72,38 +75,36 @@ 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();
Expand All @@ -119,6 +120,28 @@ export class ElectronMenuContribution implements FrontendApplicationContribution
});
}

handleTitleBarStyling(app: FrontendApplication): void {
this.hideTopPanel(app);
electron.ipcRenderer.on('original-titleBarStyle', (_event, style: string) => {
this.titleBarStyle = style;
});
electron.ipcRenderer.send('request-titleBarStyle');
this.preferenceService.ready.then(() => {
this.titleBarStyle = this.titleBarStyle ?? this.preferenceService.get('window.titleBarStyle');
this.setMenu(app);
electron.remote.getCurrentWindow().setMenuBarVisibility(true);
setTimeout(() => {
this.titleBarStyleChangeFlag = true;
}, 1000);
});
this.preferenceService.onPreferenceChanged(change => {
if (change.preferenceName === 'window.titleBarStyle' && this.titleBarStyleChangeFlag && electron.remote.getCurrentWindow().isFocused()) {
electron.ipcRenderer.send('titleBarStyle-changed', change.newValue);
this.handleRequiredRestart();
}
});
}

handleToggleMaximized(): void {
const preference = this.preferenceService.get('window.menuBarVisibility');
if (preference === 'classic') {
Expand All @@ -129,29 +152,90 @@ 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();
}
}
}

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<void> {
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');
}
}

Expand Down
Loading

0 comments on commit 05fe6a0

Please sign in to comment.