/********************************************************************************
 * 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 { injectable , inject } from 'inversify' ; import { MenuBar , Menu as MenuWidget , Widget } from '@phosphor/widgets' ; import { CommandRegistry as PhosphorCommandRegistry } from '@phosphor/commands' ; import { CommandRegistry , ActionMenuNode , CompositeMenuNode , environment , MenuModelRegistry , MAIN_MENU_BAR , MenuPath , DisposableCollection , Disposable , MenuNode } from '../../common' ; import { KeybindingRegistry } from '../keybinding' ; import { FrontendApplicationContribution , FrontendApplication } from '../frontend-application' ; import { ContextKeyService } from '../context-key-service' ; import { ContextMenuContext } from './context-menu-context' ; import { waitForRevealed } from '../widgets' ; import { ApplicationShell } from '../shell' ; import { CorePreferences } from '../core-preferences' ; import { PreferenceService } from '../preferences/preference-service' ; export abstract class MenuBarWidget extends MenuBar { abstract activateMenu ( label , ... labels ) ; abstract triggerMenuItem ( label , ... labels ) ; } @ injectable ( ) export class BrowserMainMenuFactory implements MenuWidgetFactory { @ inject ( ContextKeyService ) protected readonly contextKeyService : ContextKeyService ; @ inject ( ContextMenuContext ) protected readonly context : ContextMenuContext ; @ inject ( CommandRegistry ) protected readonly commandRegistry : CommandRegistry ; @ inject ( CorePreferences ) protected readonly corePreferences : CorePreferences ; @ inject ( KeybindingRegistry ) protected readonly keybindingRegistry : KeybindingRegistry ; @ inject ( MenuModelRegistry ) protected readonly menuProvider : MenuModelRegistry ; createMenuBar ( ) { const menuBar = new DynamicMenuBarWidget ( ) ; menuBar . id = 'theia:menubar' ; this . corePreferences . ready . then ( ( ) => { this . showMenuBar ( menuBar , this . corePreferences . get ( 'window.menuBarVisibility' , 'classic' ) ) ; } ) ; const preferenceListener = this . corePreferences . onPreferenceChanged ( preference => { if ( preference . preferenceName === 'window.menuBarVisibility' ) { this . showMenuBar ( menuBar , preference . newValue ) ; } } ) ; const keybindingListener = this . keybindingRegistry . onKeybindingsChanged ( ( ) => { const preference = this . corePreferences [ 'window.menuBarVisibility' ] ; this . showMenuBar ( menuBar , preference ) ; } ) ; menuBar . disposed . connect ( ( ) => { preferenceListener . dispose ( ) ; keybindingListener . dispose ( ) ; } ) ; return menuBar ; } protected showMenuBar ( menuBar , preference ) { if ( preference && [ 'classic' , 'visible' ] . includes ( preference ) ) { menuBar . clearMenus ( ) ; this . fillMenuBar ( menuBar ) ; } else { menuBar . clearMenus ( ) ; } } protected fillMenuBar ( menuBar ) { const menuModel = this . menuProvider . getMenu ( MAIN_MENU_BAR ) ; const menuCommandRegistry = this . createMenuCommandRegistry ( menuModel ) ; for ( const menu of menuModel . children ) { if ( menu instanceof CompositeMenuNode ) { const menuWidget = this . createMenuWidget ( menu , { commands : menuCommandRegistry } ) ; menuBar . addMenu ( menuWidget ) ; } } } createContextMenu ( path , args ? ) { const menuModel = this . menuProvider . getMenu ( path ) ; const menuCommandRegistry = this . createMenuCommandRegistry ( menuModel , args ) . snapshot ( ) ; const contextMenu = this . createMenuWidget ( menuModel , { commands : menuCommandRegistry } ) ; return contextMenu ; } createMenuWidget ( menu , options ) { return new DynamicMenuWidget ( menu , options , this . services ) ; } protected createMenuCommandRegistry ( menu , args = [ ] ) { const menuCommandRegistry = new MenuCommandRegistry ( this . services ) ; this . registerMenu ( menuCommandRegistry , menu , args ) ; return menuCommandRegistry ; } protected registerMenu ( menuCommandRegistry , menu , args ) { for ( const child of menu . children ) { if ( child instanceof ActionMenuNode ) { menuCommandRegistry . registerActionMenu ( child , args ) ; if ( child . altNode ) { menuCommandRegistry . registerActionMenu ( child . altNode , args ) ; } } else if ( child instanceof CompositeMenuNode ) { this . registerMenu ( menuCommandRegistry , child , args ) ; } else { this . handleDefault ( menuCommandRegistry , child , args ) ; } } } protected handleDefault ( menuCommandRegistry , menuNode , args ) { } protected get services ( ) : MenuServices { return { context : this . context , contextKeyService : this . contextKeyService , commandRegistry : this . commandRegistry , keybindingRegistry : this . keybindingRegistry , menuWidgetFactory : this } ; } } export class DynamicMenuBarWidget extends MenuBarWidget { /**
     * We want to restore the focus after the menu closes.
     */ protected previousFocusedElement : HTMLElement | undefined ; constructor ( ) { super ( ) ; DynamicMenuBarWidget . prototype [ '_openChildMenu' ] = ( ) => { if ( this . activeMenu instanceof DynamicMenuWidget ) { if ( ! this . childMenu ) { const { activeElement } = document ; if ( activeElement instanceof HTMLElement ) { this . previousFocusedElement = activeElement ; } } this . activeMenu . aboutToShow ( { previousFocusedElement : this . previousFocusedElement } ) ; } super [ '_openChildMenu' ] ( ) ; } ; } async activateMenu ( label , ... labels ) { const menu = this . menus . find ( m => m . title . label === label ) ; if ( ! menu ) { throw new Error ( `could not find '${ label }' menu` ) ; } this . activeMenu = menu ; this . openActiveMenu ( ) ; await waitForRevealed ( menu ) ; const menuPath = [ label ] ; let current = menu ; for ( const itemLabel of labels ) { const item = current . items . find ( i => i . label === itemLabel ) ; if ( ! item || ! item . submenu ) { throw new Error ( `could not find '${ label }' submenu in ${ menuPath . map ( l => "'" + l + "'" ) . join ( ' -> ' ) } menu` ) ; } current . activeItem = item ; current . triggerActiveItem ( ) ; current = item . submenu ; await waitForRevealed ( current ) ; } return current ; } async triggerMenuItem ( label , ... labels ) { if ( ! labels . length ) { throw new Error ( 'menu item label is not specified' ) ; } const menuPath = [ label , ... labels . slice ( 0 , labels . length - 1 ) ] ; const menu = await this . activateMenu ( menuPath [ 0 ] , ... menuPath . slice ( 1 ) ) ; const item = menu . items . find ( i => i . label === labels [ labels . length - 1 ] ) ; if ( ! item ) { throw new Error ( `could not find '${ label }' item in ${ menuPath . map ( l => "'" + l + "'" ) . join ( ' -> ' ) } menu` ) ; } menu . activeItem = item ; menu . triggerActiveItem ( ) ; return item ; } } export class MenuServices { readonly commandRegistry : CommandRegistry ; readonly keybindingRegistry : KeybindingRegistry ; readonly contextKeyService : ContextKeyService ; readonly context : ContextMenuContext ; readonly menuWidgetFactory : MenuWidgetFactory ; } export interface MenuWidgetFactory { createMenuWidget ( menu , options ) : MenuWidget ; } /**
 * A menu widget that would recompute its items on update.
 */ export class DynamicMenuWidget extends MenuWidget { /**
     * We want to restore the focus after the menu closes.
     */ protected previousFocusedElement : HTMLElement | undefined ; constructor ( protected menu , protected options , protected services ) { super ( options ) ; if ( menu . label ) { this . title . label = menu . label ; } if ( menu . iconClass ) { this . title . iconClass = menu . iconClass ; } this . updateSubMenus ( this , this . menu , this . options . commands ) ; } public aboutToShow ( { previousFocusedElement } ) { this . preserveFocusedElement ( previousFocusedElement ) ; this . clearItems ( ) ; this . runWithPreservedFocusContext ( ( ) => { this . options . commands . snapshot ( ) ; this . updateSubMenus ( this , this . menu , this . options . commands ) ; } ) ; } public open ( x , y , options ? ) { const cb = ( ) => { this . restoreFocusedElement ( ) ; this . aboutToClose . disconnect ( cb ) ; } ; this . aboutToClose . connect ( cb ) ; super . open ( x , y , options ) ; } private updateSubMenus ( parent , menu , commands ) { const items = this . buildSubMenus ( [ ] , menu , commands ) ; for ( const item of items ) { parent . addItem ( item ) ; } } private buildSubMenus ( items , menu , commands ) { for ( const item of menu . children ) { if ( item instanceof CompositeMenuNode ) { if ( item . children . length ) { if ( item . isSubmenu ) { const submenu = this . services . menuWidgetFactory . createMenuWidget ( item , this . options ) ; if ( ! submenu . items . length ) { continue ; } items . push ( { type : 'submenu' , submenu , } ) ; } else { const submenu = this . buildSubMenus ( [ ] , item , commands ) ; if ( ! submenu . length ) { continue ; } if ( items . length ) { items . push ( { type : 'separator' } ) ; } items . push ( ... submenu ) ; } } } else if ( item instanceof ActionMenuNode ) { const { context , contextKeyService } = this . services ; const node = item . altNode && context . altPressed ? item . altNode : item ; const { when } = node . action ; if ( ! ( commands . isVisible ( node . action . commandId ) && ( ! when || contextKeyService . match ( when ) ) ) ) { continue ; } items . push ( { command : node . action . commandId , type : 'command' } ) ; } else { items . push ( ... this . handleDefault ( item ) ) ; } } return items ; } protected handleDefault ( menuNode ) { return [ ] ; } protected preserveFocusedElement ( previousFocusedElement = document . activeElement ) { if ( ! this . previousFocusedElement && previousFocusedElement instanceof HTMLElement ) { this . previousFocusedElement = previousFocusedElement ; return true ; } return false ; } protected restoreFocusedElement ( ) { if ( this . previousFocusedElement ) { this . previousFocusedElement . focus ( { preventScroll : true } ) ; this . previousFocusedElement = undefined ; return true ; } return false ; } protected runWithPreservedFocusContext ( what ) { let focusToRestore = undefined ; const { activeElement } = document ; if ( this . previousFocusedElement && activeElement instanceof HTMLElement && this . previousFocusedElement !== activeElement ) { focusToRestore = activeElement ; this . previousFocusedElement . focus ( { preventScroll : true } ) ; } try { what ( ) ; } finally { if ( focusToRestore ) { focusToRestore . focus ( { preventScroll : true } ) ; } } } } @ injectable ( ) export class BrowserMenuBarContribution implements FrontendApplicationContribution { @ inject ( ApplicationShell ) protected readonly shell : ApplicationShell ; @ inject ( PreferenceService ) protected readonly preferenceService : PreferenceService ; constructor ( @ inject ( BrowserMainMenuFactory ) protected readonly factory ) { } onStart ( app ) { this . appendMenu ( app . shell ) ; } get menuBar ( ) : MenuBarWidget | undefined { return this . shell . topPanel . widgets . find ( w => w instanceof MenuBarWidget ) as MenuBarWidget | undefined ; } protected appendMenu ( shell ) { const logo = this . createLogo ( ) ; shell . addWidget ( logo , { area : 'top' } ) ; const menu = this . factory . createMenuBar ( ) ; shell . addWidget ( menu , { area : 'top' } ) ; if ( environment . electron . is ( ) ) { this . preferenceService . ready . then ( ( ) => { 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 ) ) ; } } ) ; } } protected createLogo ( ) { const logo = new Widget ( ) ; logo . id = 'theia:icon' ; logo . addClass ( 'theia-icon' ) ; return logo ; } } /**
 * Stores Theia-specific action menu nodes instead of PhosphorJS commands with their handlers.
 */ export class MenuCommandRegistry extends PhosphorCommandRegistry { protected actions = new Map < string , [ ActionMenuNode , any [ ] ] > ( ) ; protected toDispose = new DisposableCollection ( ) ; constructor ( protected services ) { super ( ) ; } registerActionMenu ( menu , args ) { const { commandId } = menu . action ; const { commandRegistry } = this . services ; const command = commandRegistry . getCommand ( commandId ) ; if ( ! command ) { return ; } const { id } = command ; if ( this . actions . has ( id ) ) { return ; } this . actions . set ( id , [ menu , args ] ) ; } snapshot ( ) { this . toDispose . dispose ( ) ; for ( const [ menu , args ] of this . actions . values ( ) ) { this . toDispose . push ( this . registerCommand ( menu , args ) ) ; } return this ; } protected registerCommand ( menu , args ) { const { commandRegistry , keybindingRegistry } = this . services ; const command = commandRegistry . getCommand ( menu . action . commandId ) ; if ( ! command ) { return Disposable . NULL ; } const { id } = command ; if ( this . hasCommand ( id ) ) { return Disposable . NULL ; } const enabled = commandRegistry . isEnabled ( id , ... args ) ; const visible = commandRegistry . isVisible ( id , ... args ) ; const toggled = commandRegistry . isToggled ( id , ... args ) ; const unregisterCommand = this . addCommand ( id , { execute : ( ) => commandRegistry . executeCommand ( id , ... args ) , label : menu . label , icon : menu . icon , isEnabled : ( ) => enabled , isVisible : ( ) => visible , isToggled : ( ) => toggled } ) ; const bindings = keybindingRegistry . getKeybindingsForCommand ( id ) ; if ( bindings . length ) { const binding = bindings [ 0 ] ; const keys = keybindingRegistry . acceleratorFor ( binding ) ; this . addKeyBinding ( { command : id , keys , selector : '.p-Widget' } ) ; } return Disposable . create ( ( ) => unregisterCommand . dispose ( ) ) ; } }