From 7b8538e5c81967335e33648dbcb95f634a714151 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Thu, 9 Feb 2023 11:34:06 +0100 Subject: [PATCH] fix: restored `window.titleBarStyle` Ref: #1733 Signed-off-by: Akos Kitta --- .../browser/arduino-ide-frontend-module.ts | 6 ++ .../src/browser/style/main.css | 13 ++++ .../browser/theia/core/application-shell.ts | 61 +++++++++++++++- .../browser/theia/core/core-preferences.ts | 34 +++++++++ .../toolbar/arduino-toolbar-contribution.ts | 13 ++-- .../theia/core/electron-main-menu-factory.ts | 24 ++----- .../theia/core/electron-menu-contribution.ts | 70 +++++++++++-------- .../theia/core/electron-window-preferences.ts | 35 ++++++++++ .../theia/electron-main-application.ts | 37 ++++++++-- 9 files changed, 230 insertions(+), 63 deletions(-) create mode 100644 arduino-ide-extension/src/browser/theia/core/core-preferences.ts create mode 100644 arduino-ide-extension/src/electron-browser/theia/core/electron-window-preferences.ts diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 284d5be8d..401b2af4f 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -348,6 +348,8 @@ import { ValidateSketch } from './contributions/validate-sketch'; import { RenameCloudSketch } from './contributions/rename-cloud-sketch'; import { CreateFeatures } from './create/create-features'; import { NativeImageCache } from './native-image-cache'; +import { rebindWindowPreferences } from '../electron-browser/theia/core/electron-window-preferences'; +import { rebindCorePreferences } from './theia/core/core-preferences'; export default new ContainerModule((bind, unbind, isBound, rebind) => { // Commands and toolbar items @@ -1019,4 +1021,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // manages native images for the electron menu icons bind(NativeImageCache).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(NativeImageCache); + // enable `'window.titleBarStyle'` on all supported OSs + rebindWindowPreferences(bind, rebind); + // enable `'window.menuBarVisibility'` on all supported OSs + rebindCorePreferences(bind, rebind); }); diff --git a/arduino-ide-extension/src/browser/style/main.css b/arduino-ide-extension/src/browser/style/main.css index 438277dee..6afa1027f 100644 --- a/arduino-ide-extension/src/browser/style/main.css +++ b/arduino-ide-extension/src/browser/style/main.css @@ -100,6 +100,19 @@ background-color: var(--theia-titleBar-activeBackground); } +#arduino-toolbar-panel { + background: var(--theia-titleBar-activeBackground); + color: var(--theia-titleBar-activeForeground); + display: flex; + min-height: var(--theia-private-menubar-height); + border-bottom: 1px solid var(--theia-titleBar-border); + } +#arduino-toolbar-panel:window-inactive, +#arduino-toolbar-panel:-moz-window-inactive { +background: var(--theia-titleBar-inactiveBackground); +color: var(--theia-titleBar-inactiveForeground); +} + #arduino-toolbar-container { display: flex; width: 100%; diff --git a/arduino-ide-extension/src/browser/theia/core/application-shell.ts b/arduino-ide-extension/src/browser/theia/core/application-shell.ts index 9f0ac6b74..6f61861bc 100644 --- a/arduino-ide-extension/src/browser/theia/core/application-shell.ts +++ b/arduino-ide-extension/src/browser/theia/core/application-shell.ts @@ -7,6 +7,8 @@ import { SHELL_TABBAR_CONTEXT_MENU, TabBar, Widget, + Layout, + SplitPanel, } from '@theia/core/lib/browser'; import { ConnectionStatus, @@ -17,6 +19,11 @@ import { MessageService } from '@theia/core/lib/common/message-service'; import { inject, injectable } from '@theia/core/shared/inversify'; import { ToolbarAwareTabBar } from './tab-bars'; +interface WidgetOptions + extends Omit { + area?: TheiaApplicationShell.Area | 'toolbar'; +} + @injectable() export class ApplicationShell extends TheiaApplicationShell { @inject(MessageService) @@ -24,10 +31,11 @@ export class ApplicationShell extends TheiaApplicationShell { @inject(ConnectionStatusService) private readonly connectionStatusService: ConnectionStatusService; + private toolbarPanel: Panel; override async addWidget( widget: Widget, - options: Readonly = {} + options: Readonly = {} ): Promise { // By default, Theia open a widget **next** to the currently active in the target area. // Instead of this logic, we want to open the new widget after the last of the target area. @@ -37,8 +45,12 @@ export class ApplicationShell extends TheiaApplicationShell { ); return; } + if (options.area === 'toolbar') { + this.toolbarPanel.addWidget(widget); + return; + } + const area = options.area || 'main'; let ref: Widget | undefined = options.ref; - const area: TheiaApplicationShell.Area = options.area || 'main'; if (!ref && (area === 'main' || area === 'bottom')) { const tabBar = this.getTabBarFor(area); if (tabBar) { @@ -48,7 +60,10 @@ export class ApplicationShell extends TheiaApplicationShell { } } } - return super.addWidget(widget, { ...options, ref }); + return super.addWidget(widget, { + ...(options), + ref, + }); } override handleEvent(): boolean { @@ -56,6 +71,46 @@ export class ApplicationShell extends TheiaApplicationShell { return false; } + protected override initializeShell(): void { + this.toolbarPanel = this.createToolbarPanel(); + super.initializeShell(); + } + + private createToolbarPanel(): Panel { + const toolbarPanel = new Panel(); + toolbarPanel.id = 'arduino-toolbar-panel'; + toolbarPanel.show(); + return toolbarPanel; + } + + protected override createLayout(): Layout { + const bottomSplitLayout = this.createSplitLayout( + [this.mainPanel, this.bottomPanel], + [1, 0], + { orientation: 'vertical', spacing: 0 } + ); + const panelForBottomArea = new SplitPanel({ layout: bottomSplitLayout }); + panelForBottomArea.id = 'theia-bottom-split-panel'; + + const leftRightSplitLayout = this.createSplitLayout( + [ + this.leftPanelHandler.container, + panelForBottomArea, + this.rightPanelHandler.container, + ], + [0, 1, 0], + { orientation: 'horizontal', spacing: 0 } + ); + const panelForSideAreas = new SplitPanel({ layout: leftRightSplitLayout }); + panelForSideAreas.id = 'theia-left-right-split-panel'; + + return this.createBoxLayout( + [this.topPanel, this.toolbarPanel, panelForSideAreas, this.statusBar], + [0, 0, 1, 0], + { direction: 'top-to-bottom', spacing: 0 } + ); + } + // Avoid hiding top panel as we use it for arduino toolbar protected override createTopPanel(): Panel { const topPanel = super.createTopPanel(); diff --git a/arduino-ide-extension/src/browser/theia/core/core-preferences.ts b/arduino-ide-extension/src/browser/theia/core/core-preferences.ts new file mode 100644 index 000000000..fd02d541c --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/core/core-preferences.ts @@ -0,0 +1,34 @@ +import { + CorePreferenceContribution, + CorePreferences, + corePreferenceSchema, + createCorePreferences, +} from '@theia/core/lib/browser/core-preferences'; +import { PreferenceContribution } from '@theia/core/lib/browser/preferences/preference-contribution'; +import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service'; +import { deepClone } from '@theia/core/lib/common/objects'; +import { PreferenceSchema } from '@theia/core/lib/common/preferences/preference-schema'; +import { interfaces } from '@theia/core/shared/inversify'; + +const copy = deepClone(corePreferenceSchema); +copy.properties['window.menuBarVisibility'].included = true; // https://github.com/arduino/arduino-ide/issues/1733#issuecomment-1424186676 +const corePreferencesSchema: PreferenceSchema = copy; + +export function rebindCorePreferences( + bind: interfaces.Bind, + rebind: interfaces.Rebind +): void { + rebind(CorePreferences) + .toDynamicValue((ctx) => { + const preferences = + ctx.container.get(PreferenceService); + const contribution = ctx.container.get( + CorePreferenceContribution + ); + return createCorePreferences(preferences, contribution.schema); + }) + .inSingletonScope(); + rebind(CorePreferenceContribution).toConstantValue({ + schema: corePreferencesSchema, + }); +} diff --git a/arduino-ide-extension/src/browser/toolbar/arduino-toolbar-contribution.ts b/arduino-ide-extension/src/browser/toolbar/arduino-toolbar-contribution.ts index 09c125e9a..668f8bed7 100644 --- a/arduino-ide-extension/src/browser/toolbar/arduino-toolbar-contribution.ts +++ b/arduino-ide-extension/src/browser/toolbar/arduino-toolbar-contribution.ts @@ -19,7 +19,8 @@ export class ArduinoToolbarContainer extends Widget { this.toolbars = toolbars; } - override onAfterAttach(msg: Message) { + override onAfterAttach(msg: Message): void { + super.onAfterAttach(msg); for (const toolbar of this.toolbars) { Widget.attach(toolbar, this.node); } @@ -56,9 +57,11 @@ export class ArduinoToolbarContribution ); } - onStart(app: FrontendApplication) { - app.shell.addWidget(this.arduinoToolbarContainer, { - area: 'top', - }); + onStart(app: FrontendApplication): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const options = { + area: 'toolbar', + }; + app.shell.addWidget(this.arduinoToolbarContainer, options); } } diff --git a/arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts b/arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts index 4a2f2faf4..369901654 100644 --- a/arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts +++ b/arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts @@ -7,7 +7,6 @@ import { CommandMenuNode, CompoundMenuNode, CompoundMenuNodeRole, - MAIN_MENU_BAR, MenuNode, MenuPath, } from '@theia/core/lib/common/menu'; @@ -28,7 +27,7 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory { @inject(FrontendApplicationStateService) private readonly appStateService: FrontendApplicationStateService; - private appReady = false; + appReady = false; private updateWhenReady = false; override postConstruct(): void { @@ -41,18 +40,9 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory { }); } - override createElectronMenuBar(): Electron.Menu { + override createElectronMenuBar(): Electron.Menu | null { this._toggledCommands.clear(); // https://github.com/eclipse-theia/theia/issues/8977 - const menuModel = this.menuProvider.getMenu(MAIN_MENU_BAR); - const template = this.fillMenuTemplate([], menuModel, [], { - rootMenuPath: MAIN_MENU_BAR, - }); - if (isOSX) { - template.unshift(this.createOSXMenu()); - } - const menu = remote.Menu.buildFromTemplate(this.escapeAmpersand(template)); - this._menu = menu; - return menu; + return super.createElectronMenuBar(); } override async setMenuBar(): Promise { @@ -63,13 +53,7 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory { this.updateWhenReady = true; return; } - await this.preferencesService.ready; - const createdMenuBar = this.createElectronMenuBar(); - if (isOSX) { - remote.Menu.setApplicationMenu(createdMenuBar); - } else { - remote.getCurrentWindow().setMenu(createdMenuBar); - } + return super.setMenuBar(); } override createElectronContextMenu( diff --git a/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-contribution.ts b/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-contribution.ts index 9327637d7..77119e04d 100644 --- a/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-contribution.ts +++ b/arduino-ide-extension/src/electron-browser/theia/core/electron-menu-contribution.ts @@ -1,53 +1,61 @@ -import { inject, injectable } from '@theia/core/shared/inversify'; +import { + getCurrentWebContents, + getCurrentWindow, + Menu, +} from '@theia/core/electron-shared/@electron/remote'; +import { + BrowserWindow, + Menu as ElectronMenu, +} from '@theia/core/electron-shared/electron'; +import { FrontendApplication } from '@theia/core/lib/browser/frontend-application'; +import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding'; +import { PreferenceScope } from '@theia/core/lib/browser/preferences/preference-scope'; import { CommandRegistry } from '@theia/core/lib/common/command'; import { MenuModelRegistry } from '@theia/core/lib/common/menu'; -import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding'; +import { isOSX } from '@theia/core/lib/common/os'; import { - ElectronMenuContribution as TheiaElectronMenuContribution, ElectronCommands, + ElectronMenuContribution as TheiaElectronMenuContribution, } from '@theia/core/lib/electron-browser/menu/electron-menu-contribution'; -import { MainMenuManager } from '../../../common/main-menu-manager'; -import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; -import { FrontendApplication } from '@theia/core/lib/browser/frontend-application'; import { ZoomLevel } from '@theia/core/lib/electron-browser/window/electron-window-preferences'; -import { PreferenceScope } from '@theia/core/lib/browser/preferences/preference-scope'; -import { - getCurrentWindow, - getCurrentWebContents, -} from '@theia/core/electron-shared/@electron/remote'; +import { injectable } from '@theia/core/shared/inversify'; +import { MainMenuManager } from '../../../common/main-menu-manager'; @injectable() export class ElectronMenuContribution extends TheiaElectronMenuContribution implements MainMenuManager { - @inject(FrontendApplicationStateService) - private readonly appStateService: FrontendApplicationStateService; - - // private appReady = false; - // private updateWhenReady = false; + private app: FrontendApplication; override onStart(app: FrontendApplication): void { + this.app = app; super.onStart(app); - this.appStateService.reachedState('ready').then(() => { - // this.appReady = true; - // if (this.updateWhenReady) { - // this.update(); - // } - }); } - protected override hideTopPanel(): void { - // NOOP - // We reuse the `div` for the Arduino toolbar. + update(): void { + // no menu updates before `onStart` + if (!this.app) { + return; + } + this.setMenu(this.app); } - update(): void { - // if (this.appReady) { - (this as any).setMenu(); - // } else { - // this.updateWhenReady = true; - // } + protected override setMenu( + app: FrontendApplication, + electronMenu: ElectronMenu | null = this.factory.createElectronMenuBar(), + electronWindow: BrowserWindow = getCurrentWindow() + ): void { + this.hideTopPanel(app); // Updates the top panel's visibility on all operating systems. + if (this.titleBarStyle === 'custom' && !this.menuBar) { + this.createCustomTitleBar(app, electronWindow); + } + if (isOSX) { + Menu.setApplicationMenu(electronMenu); + } else { + // Unix/Windows: Set the per-window menus + electronWindow.setMenu(electronMenu); + } } override registerCommands(registry: CommandRegistry): void { diff --git a/arduino-ide-extension/src/electron-browser/theia/core/electron-window-preferences.ts b/arduino-ide-extension/src/electron-browser/theia/core/electron-window-preferences.ts new file mode 100644 index 000000000..1de3b11f2 --- /dev/null +++ b/arduino-ide-extension/src/electron-browser/theia/core/electron-window-preferences.ts @@ -0,0 +1,35 @@ +import { PreferenceContribution } from '@theia/core/lib/browser/preferences/preference-contribution'; +import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service'; +import { deepClone } from '@theia/core/lib/common/objects'; +import { PreferenceSchema } from '@theia/core/lib/common/preferences/preference-schema'; +import { + createElectronWindowPreferences, + ElectronWindowPreferenceContribution, + ElectronWindowPreferences, + electronWindowPreferencesSchema as theiaElectronWindowPreferencesSchema, +} from '@theia/core/lib/electron-browser/window/electron-window-preferences'; +import { interfaces } from '@theia/core/shared/inversify'; + +const copy = deepClone(theiaElectronWindowPreferencesSchema); +copy.properties['window.titleBarStyle'].included = true; // https://github.com/eclipse-theia/theia/pull/10044#issuecomment-1423841453 +copy.properties['window.titleBarStyle'].default = 'custom'; +const electronWindowPreferencesSchema: PreferenceSchema = copy; + +export function rebindWindowPreferences( + bind: interfaces.Bind, + rebind: interfaces.Rebind +): void { + rebind(ElectronWindowPreferences) + .toDynamicValue((ctx) => { + const preferences = + ctx.container.get(PreferenceService); + const contribution = ctx.container.get( + ElectronWindowPreferenceContribution + ); + return createElectronWindowPreferences(preferences, contribution.schema); + }) + .inSingletonScope(); + rebind(ElectronWindowPreferenceContribution).toConstantValue({ + schema: electronWindowPreferencesSchema, + }); +} diff --git a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts index 84736969e..787f94ee6 100644 --- a/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts +++ b/arduino-ide-extension/src/electron-main/theia/electron-main-application.ts @@ -20,7 +20,12 @@ import { import { URI } from '@theia/core/shared/vscode-uri'; import { Deferred } from '@theia/core/lib/common/promise-util'; import * as os from '@theia/core/lib/common/os'; -import { Restart } from '@theia/core/lib/electron-common/messaging/electron-messages'; +import { + RequestTitleBarStyle, + Restart, + TitleBarStyleAtStartup, + TitleBarStyleChanged, +} from '@theia/core/lib/electron-common/messaging/electron-messages'; import { TheiaBrowserWindowOptions } from '@theia/core/lib/electron-main/theia-electron-window'; import { IsTempSketch } from '../../node/is-temp-sketch'; import { @@ -330,10 +335,19 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { } protected override getTitleBarStyle( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _config: FrontendApplicationConfig + config: FrontendApplicationConfig ): 'native' | 'custom' { - 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 'custom'; } protected override hookApplicationEvents(): void { @@ -351,6 +365,21 @@ export class ElectronMainApplication extends TheiaElectronMainApplication { this.delete(sketch); } }); + ipcMain.on(TitleBarStyleChanged, ({ sender }, titleBarStyle: string) => { + this.useNativeWindowFrame = titleBarStyle === 'native'; // XXX: No `isOSX || ` check + const browserWindow = BrowserWindow.fromId(sender.id); + if (browserWindow) { + this.saveWindowState(browserWindow); + } else { + console.warn(`no BrowserWindow with id: ${sender.id}`); + } + }); + ipcMain.on(RequestTitleBarStyle, ({ sender }) => { + sender.send( + TitleBarStyleAtStartup, + this.didUseNativeWindowFrameOnStart.get(sender.id) ? 'native' : 'custom' + ); + }); } protected override async onSecondInstance(