/********************************************************************************
 * Copyright (C) 2017 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
 ********************************************************************************/ import * as electron from '../../../shared/electron' ; import { inject , injectable } from 'inversify' ; import { CommandRegistry , isOSX , ActionMenuNode , CompositeMenuNode , MAIN_MENU_BAR , MenuModelRegistry , MenuPath , MenuNode } from '../../common' ; import { Keybinding } from '../../common/keybinding' ; import { PreferenceService , KeybindingRegistry , CommonCommands } from '../../browser' ; import debounce = require ( 'lodash.debounce' ) ; import { MAXIMIZED_CLASS } from '../../browser/shell/theia-dock-panel' ; import { BrowserMainMenuFactory } from '../../browser/menu/browser-menu-plugin' ; /**
 * Representation of possible electron menu options.
 */ export interface ElectronMenuOptions { /**
     * Controls whether to render disabled menu items.
     * Defaults to `true`.
     */ readonly showDisabled ? : boolean ; } /**
 * Define the action of the menu item, when specified the `click` property will
 * be ignored. See [roles](https://www.electronjs.org/docs/api/menu-item#roles).
 */ export type ElectronMenuItemRole = ( 'undo' | 'redo' | 'cut' | 'copy' | 'paste' | 'pasteAndMatchStyle' | 'delete' | 'selectAll' | 'reload' | 'forceReload' | 'toggleDevTools' | 'resetZoom' | 'zoomIn' | 'zoomOut' | 'togglefullscreen' | 'window' | 'minimize' | 'close' | 'help' | 'about' | 'services' | 'hide' | 'hideOthers' | 'unhide' | 'quit' | 'startSpeaking' | 'stopSpeaking' | 'zoom' | 'front' | 'appMenu' | 'fileMenu' | 'editMenu' | 'viewMenu' | 'recentDocuments' | 'toggleTabBar' | 'selectNextTab' | 'selectPreviousTab' | 'mergeAllWindows' | 'clearRecentDocuments' | 'moveTabToNewWindow' | 'windowMenu' ) ; @ injectable ( ) export class ElectronMainMenuFactory extends BrowserMainMenuFactory { protected _menu : Electron . Menu | undefined ; protected _toggledCommands : Set < string > = new Set ( ) ; constructor ( @ inject ( CommandRegistry ) protected readonly commandRegistry , @ inject ( PreferenceService ) protected readonly preferencesService , @ inject ( MenuModelRegistry ) protected readonly menuProvider , @ inject ( KeybindingRegistry ) protected readonly keybindingRegistry ) { super ( ) ; preferencesService . onPreferenceChanged ( debounce ( e => { if ( e . preferenceName === 'window.menuBarVisibility' ) { this . setMenuBar ( ) ; } if ( this . _menu ) { for ( const item of this . _toggledCommands ) { this . _menu . getMenuItemById ( item ) . checked = this . commandRegistry . isToggled ( item ) ; } electron . remote . getCurrentWindow ( ) . setMenu ( this . _menu ) ; } } , 10 ) ) ; keybindingRegistry . onKeybindingsChanged ( ( ) => { this . setMenuBar ( ) ; } ) ; } async setMenuBar ( ) { await this . preferencesService . ready ; if ( isOSX ) { const createdMenuBar = this . createElectronMenuBar ( ) ; electron . remote . Menu . setApplicationMenu ( createdMenuBar ) ; } else if ( this . preferencesService . get ( 'window.titleBarStyle' ) === 'native' ) { const createdMenuBar = this . createElectronMenuBar ( ) ; electron . remote . getCurrentWindow ( ) . setMenu ( createdMenuBar ) ; } } createElectronMenuBar ( ) { const preference = this . preferencesService . get < string > ( 'window.menuBarVisibility' ) || 'classic' ; const maxWidget = document . getElementsByClassName ( MAXIMIZED_CLASS ) ; if ( preference === 'visible' || ( preference === 'classic' && maxWidget . length === 0 ) ) { const menuModel = this . menuProvider . getMenu ( MAIN_MENU_BAR ) ; const template = this . fillMenuTemplate ( [ ] , menuModel ) ; if ( isOSX ) { template . unshift ( this . createOSXMenu ( ) ) ; } const menu = electron . remote . Menu . buildFromTemplate ( template ) ; this . _menu = menu ; return this . _menu ; } this . _menu = undefined ; return null ; } createElectronContextMenu ( menuPath , args ? ) { const menuModel = this . menuProvider . getMenu ( menuPath ) ; const template = this . fillMenuTemplate ( [ ] , menuModel , args , { showDisabled : false } ) ; return electron . remote . Menu . buildFromTemplate ( template ) ; } protected fillMenuTemplate ( items , menuModel , args = [ ] , options ? ) { const showDisabled = ( options ?. showDisabled === undefined ) ? true : options ?. showDisabled ; for ( const menu of menuModel . children ) { if ( menu instanceof CompositeMenuNode ) { if ( menu . children . length > 0 ) { if ( menu . isSubmenu ) { const submenu = this . fillMenuTemplate ( [ ] , menu , args , options ) ; if ( submenu . length === 0 ) { continue ; } items . push ( { label : menu . label , submenu } ) ; } else { const submenu = this . fillMenuTemplate ( [ ] , menu , args , options ) ; if ( submenu . length === 0 ) { continue ; } if ( items . length > 0 ) { items . push ( { type : 'separator' } ) ; } items . push ( ... submenu ) ; } } } else if ( menu instanceof ActionMenuNode ) { const node = menu . altNode && this . context . altPressed ? menu . altNode : menu ; const commandId = node . action . commandId ; if ( ! this . commandRegistry . getCommand ( commandId ) ) { console . debug ( `Skipping menu item with missing command: "${ commandId }".` ) ; continue ; } if ( ! this . commandRegistry . isVisible ( commandId , ... args ) || ( ! ! node . action . when && ! this . contextKeyService . match ( node . action . when ) ) ) { continue ; } if ( ! showDisabled && ! this . commandRegistry . isEnabled ( commandId , ... args ) ) { continue ; } const bindings = this . keybindingRegistry . getKeybindingsForCommand ( commandId ) ; let accelerator ; if ( bindings . length > 0 ) { const binding = bindings [ 0 ] ; accelerator = this . acceleratorFor ( binding ) ; } const menuItem = { id : node . id , label : node . label , type : this . commandRegistry . getToggledHandler ( commandId , ... args ) ? 'checkbox' : 'normal' , checked : this . commandRegistry . isToggled ( commandId , ... args ) , enabled : true , visible : true , accelerator , click : ( ) => this . execute ( commandId , args ) } as Electron . MenuItemConstructorOptions ; if ( isOSX ) { const role = this . roleFor ( node . id ) ; if ( role ) { menuItem . role = role ; delete menuItem . click ; } } items . push ( menuItem ) ; if ( this . commandRegistry . getToggledHandler ( commandId , ... args ) ) { this . _toggledCommands . add ( commandId ) ; } } else { items . push ( ... this . handleElectronDefault ( menu , args , options ) ) ; } } return items ; } protected handleElectronDefault ( menuNode , args = [ ] , options ? ) { return [ ] ; } /**
     * Return a user visible representation of a keybinding.
     */ protected acceleratorFor ( keybinding ) { const bindingKeySequence = this . keybindingRegistry . resolveKeybinding ( keybinding ) ; if ( bindingKeySequence . length > 1 ) { return '' ; } const keyCode = bindingKeySequence [ 0 ] ; return this . keybindingRegistry . acceleratorForKeyCode ( keyCode , '+' ) ; } protected roleFor ( id ) { let role ; switch ( id ) { case CommonCommands . UNDO . id : role = 'undo' ; break ; case CommonCommands . REDO . id : role = 'redo' ; break ; case CommonCommands . CUT . id : role = 'cut' ; break ; case CommonCommands . COPY . id : role = 'copy' ; break ; case CommonCommands . PASTE . id : role = 'paste' ; break ; case CommonCommands . SELECT_ALL . id : role = 'selectAll' ; break ; default : break ; } return role ; } protected async execute ( command , args ) { try { if ( this . commandRegistry . isEnabled ( command , ... args ) ) { await this . commandRegistry . executeCommand ( command , ... args ) ; if ( this . _menu && this . commandRegistry . isVisible ( command , ... args ) ) { this . _menu . getMenuItemById ( command ) . checked = this . commandRegistry . isToggled ( command , ... args ) ; electron . remote . getCurrentWindow ( ) . setMenu ( this . _menu ) ; } } } catch { } } protected createOSXMenu ( ) { return { label : 'Theia' , submenu : [ { role : 'about' } , { type : 'separator' } , { role : 'services' , submenu : [ ] } , { type : 'separator' } , { role : 'hide' } , { role : 'hideOthers' } , { role : 'unhide' } , { type : 'separator' } , { role : 'quit' } ] } ; } }