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 3, 2021
1 parent ce50d67 commit 1212bf4
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 35 deletions.
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
15 changes: 14 additions & 1 deletion packages/core/src/browser/core-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { interfaces } from 'inversify';
import { createPreferenceProxy, PreferenceProxy, PreferenceService, PreferenceContribution, PreferenceSchema } from './preferences';
import { SUPPORTED_ENCODINGS } from './supported-encodings';
import { FrontendApplicationConfigProvider } from './frontend-application-config-provider';
import { isOSX } from '../common/os';
import { isOSX, isWindows } from '../common/os';

export const corePreferenceSchema: PreferenceSchema = {
'type': 'object',
Expand Down Expand Up @@ -72,6 +72,18 @@ export const corePreferenceSchema: PreferenceSchema = {
A setting of 'compact' will move the menu into the sidebar.`,
included: !isOSX
},
'window.titleBarStyle': {
type: 'string',
enum: ['native', 'custom'],
markdownEnumDescriptions: [
'Native title bar is displayed.',
'Custom title bar is displayed.'
],
default: isWindows ? 'custom' : 'native',
scope: 'application',
markdownDescription: 'Adjust the appearance of the window title bar. Changes require a full restart to apply',
included: !isOSX
},
'workbench.list.openMode': {
type: 'string',
enum: [
Expand Down Expand Up @@ -128,6 +140,7 @@ export interface CoreConfiguration {
'files.encoding': string
'keyboard.dispatch': 'code' | 'keyCode';
'window.menuBarVisibility': 'classic' | 'visible' | 'hidden' | 'compact';
'window.titleBarStyle': 'native' | 'custom';
'workbench.list.openMode': 'singleClick' | 'doubleClick';
'workbench.commandPalette.history': number;
'workbench.editor.highlightModifiedTabs': boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export class ElectronContextMenuRenderer extends ContextMenuRenderer {
}

protected doRender({ menuPath, anchor, args, onHide }: RenderContextMenuOptions): ElectronContextMenuAccess {
const menu = this.menuFactory.createContextMenu(menuPath, args);
const menu = this.menuFactory.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
Expand Down
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
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, 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,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;
Expand All @@ -83,27 +86,31 @@ export class ElectronMenuContribution implements FrontendApplicationContribution
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();
this.setMenu(app);
electron.remote.getCurrentWindow().setMenuBarVisibility(true);
});
this.preferenceService.onPreferenceChanged(change => {
if (change.preferenceName === 'window.titleBarStyle') {
electron.ipcRenderer.send('titleBarStyle-changed', change.newValue);
}
});
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 @@ -129,32 +136,82 @@ 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.preferenceService.get('window.titleBarStyle') !== 'custom');
child = undefined;
} else {
child = itr.next();
}
}
}

private setMenu(menu: electron.Menu | null = this.factory.createMenuBar(), electronWindow: electron.BrowserWindow = electron.remote.getCurrentWindow()): void {
private 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);
const preference = this.preferenceService.get('window.titleBarStyle');
if (preference === '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('min', 'chrome-minimize', () => electronWindow.minimize()),
this.createControlButton('max', 'chrome-maximize', () => electronWindow.maximize()),
this.createControlButton('restore', 'chrome-restore', () => electronWindow.unmaximize()),
this.createControlButton('close', 'chrome-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);
}
}

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

private createControlButton(id: string, codicon: string, handler: () => void): HTMLElement {
const button = document.createElement('div');
button.id = `${id}-button`;
button.className = `control-button codicon codicon-${codicon}`;
button.addEventListener('click', handler);
return button;
}

registerCommands(registry: CommandRegistry): void {

const currentWindow = electron.remote.getCurrentWindow();
Expand Down
87 changes: 87 additions & 0 deletions packages/core/src/electron-browser/menu/electron-menu-style.css
Original file line number Diff line number Diff line change
@@ -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%;
}

#min-button {
grid-column: 1;
}
#max-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;
}
Loading

0 comments on commit 1212bf4

Please sign in to comment.