@@ -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' ;
2424import { FrontendApplication , FrontendApplicationContribution , CommonMenus } from '../../browser' ;
2525import { ElectronMainMenuFactory } from './electron-main-menu-factory' ;
2626import { FrontendApplicationStateService , FrontendApplicationState } from '../../browser/frontend-application-state' ;
2727import { 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
2932export 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