/********************************************************************************
 * 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 , named } from 'inversify' ; import { Disposable } from './disposable' ; import { CommandRegistry , Command } from './command' ; import { ContributionProvider } from './contribution-provider' ; /**
 * A menu entry representing an action, e.g. "New File".
 */ export interface MenuAction { /**
     * The command to execute.
     */ commandId : string ; /**
     * In addition to the mandatory command property, an alternative command can be defined.
     * It will be shown and invoked when pressing Alt while opening a menu.
     */ alt ? : string ; /**
     * A specific label for this action. If not specified the command label or command id will be used.
     */ label ? : string ; /**
     * Icon class(es). If not specified the icon class associated with the specified command
     * (i.e. `command.iconClass`) will be used if it exists.
     */ icon ? : string ; /**
     * Menu entries are sorted in ascending order based on their `order` strings. If omitted the determined
     * label will be used instead.
     */ order ? : string ; /**
     * Optional expression which will be evaluated by the  ContextKeyService to determine visibility
     * of the action, e.g. `resourceLangId == markdown`.
      when ? : string ; } export namespace MenuAction { export function is ( arg ) { return ! ! arg && arg === Object ( arg ) && 'commandId' in arg ; } } /**
 * Additional options when creating a new submenu.
 */ export interface SubMenuOptions { /**
     * The class to use for the submenu icon.
     */ iconClass ? : string ; /**
     * Menu entries are sorted in ascending order based on their `order` strings. If omitted the determined
     * label will be used instead.
     */ order ? : string ; } export type MenuPath = string [ ] ; export const MAIN_MENU_BAR = [ 'menubar' ] ; export const SETTINGS_MENU = [ 'settings_menu' ] ; export const ACCOUNTS_MENU = [ 'accounts_menu' ] ; export const ACCOUNTS_SUBMENU = [ ... ACCOUNTS_MENU , '1_accounts_submenu' ] ; export const MenuContribution = Symbol ( 'MenuContribution' ) ; /**
 * Representation of a menu contribution.
 *
 * Note that there are also convenience classes which combine multiple contributions into one.
 * For example to register a view together with a menu and keybinding you could use
 *  AbstractViewContribution instead.
 *
 * ### Example usage
 *
 * ```ts
 * import { MenuContribution, MenuModelRegistry, MAIN_MENU_BAR } from '@theia/core';
 *
 *  injectable export interface MenuContribution { param menus registerMenus ( menus ) : void ; } /**
 * The MenuModelRegistry allows to register and unregister menus, submenus and actions
 * via strings and  MenuAction s without the need to access the underlying UI
 * representation.
  @ injectable ( ) export class MenuModelRegistry { protected readonly root = new CompositeMenuNode ( '' ) ; constructor ( @ inject ( ContributionProvider ) @ named ( MenuContribution ) protected readonly contributions , @ inject ( CommandRegistry ) protected readonly commands ) { } onStart ( ) { for ( const contrib of this . contributions . getContributions ( ) ) { contrib . registerMenus ( this ) ; } } returns registerMenuAction ( menuPath , item ) { const menuNode = new ActionMenuNode ( item , this . commands ) ; return this . registerMenuNode ( menuPath , menuNode ) ; } returns registerMenuNode ( menuPath , menuNode ) { const parent = this . findGroup ( menuPath ) ; return parent . addNode ( menuNode ) ; } param menuPath param label param options returns registerSubmenu ( menuPath , label , options ? ) { if ( menuPath . length === 0 ) { throw new Error ( 'The sub menu path cannot be empty.' ) ; } const index = menuPath . length - 1 ; const menuId = menuPath [ index ] ; const groupPath = index === 0 ? [ ] : menuPath . slice ( 0 , index ) ; const parent = this . findGroup ( groupPath , options ) ; let groupNode = this . findSubMenu ( parent , menuId , options ) ; if ( ! groupNode ) { groupNode = new CompositeMenuNode ( menuId , label , options ) ; return parent . addNode ( groupNode ) ; } else { if ( ! groupNode . label ) { groupNode . label = label ; } else if ( groupNode . label !== label ) { throw new Error ( "The group '" + menuPath . join ( '/' ) + "' already has a different label." ) ; } if ( options ) { if ( ! groupNode . iconClass ) { groupNode . iconClass = options . iconClass ; } if ( ! groupNode . order ) { groupNode . order = options . order ; } } return { dispose : ( ) => { } } ; } } param item param menuPath unregisterMenuAction ( item , menuPath ? ) ; param command param menuPath unregisterMenuAction ( command , menuPath ? ) ; param id param menuPath unregisterMenuAction ( id , menuPath ? ) ; unregisterMenuAction ( itemOrCommandOrId , menuPath ? ) { const id = MenuAction . is ( itemOrCommandOrId ) ? itemOrCommandOrId . commandId : Command . is ( itemOrCommandOrId ) ? itemOrCommandOrId . id : itemOrCommandOrId ; if ( menuPath ) { const parent = this . findGroup ( menuPath ) ; parent . removeNode ( id ) ; return ; } this . unregisterMenuNode ( id ) ; } param id unregisterMenuNode ( id ) { const recurse = ( root ) => { root . children . forEach ( node => { if ( node instanceof CompositeMenuNode ) { node . removeNode ( id ) ; recurse ( node ) ; } } ) ; } ; recurse ( this . root ) ; } protected findGroup ( menuPath , options ? ) { let currentMenu = this . root ; for ( const segment of menuPath ) { currentMenu = this . findSubMenu ( currentMenu , segment , options ) ; } return currentMenu ; } protected findSubMenu ( current , menuId , options ? ) { const sub = current . children . find ( e => e . id === menuId ) ; if ( sub instanceof CompositeMenuNode ) { return sub ; } if ( sub ) { throw new Error ( `'${ menuId }' is not a menu group.` ) ; } const newSub = new CompositeMenuNode ( menuId , undefined , options ) ; current . addNode ( newSub ) ; return newSub ; } param menuPath returns getMenu ( menuPath = [ ] ) { return this . findGroup ( menuPath ) ; } } /**
 * Base interface of the nodes used in the menu tree structure.
 */ export interface MenuNode { /**
     * the optional label for this specific node.
     */ readonly label ? : string ; /**
     * technical identifier.
     */ readonly id : string ; /**
     * Menu nodes are sorted in ascending order based on their `sortString`.
     */ readonly sortString : string ; } /**
 * Node representing a (sub)menu in the menu tree structure.
 */ export class CompositeMenuNode implements MenuNode { protected readonly _children : MenuNode [ ] = [ ] ; public iconClass ? : string ; public order ? : string ; constructor ( public readonly id , public label ? , options ? ) { if ( options ) { this . iconClass = options . iconClass ; this . order = options . order ; } } get children ( ) : ReadonlyArray < MenuNode > { return this . _children ; } returns public addNode ( node ) { this . _children . push ( node ) ; this . _children . sort ( ( m1 , m2 ) => { if ( CompositeMenuNode . isNavigationGroup ( m1 ) ) { return - 1 ; } if ( CompositeMenuNode . isNavigationGroup ( m2 ) ) { return 1 ; } if ( m1 . sortString < m2 . sortString ) { return - 1 ; } else if ( m1 . sortString > m2 . sortString ) { return 1 ; } else { return 0 ; } } ) ; return { dispose : ( ) => { const idx = this . _children . indexOf ( node ) ; if ( idx >= 0 ) { this . _children . splice ( idx , 1 ) ; } } } ; } param id public removeNode ( id ) { const node = this . _children . find ( n => n . id === id ) ; if ( node ) { const idx = this . _children . indexOf ( node ) ; if ( idx >= 0 ) { this . _children . splice ( idx , 1 ) ; } } } get sortString ( ) : string { return this . order || this . id ; } get isSubmenu ( ) : boolean { return this . label !== undefined ; } param node returns `true` when the given node is a  CompositeMenuNode with id `navigation`,
     * `false` otherwise.
      static isNavigationGroup ( node ) { return node instanceof CompositeMenuNode && node . id === 'navigation' ; } } /**
 * Node representing an action in the menu tree structure.
 * It's based on  MenuAction for which it tries to determine the
 * best label, icon and sortString with the given data.
  export class ActionMenuNode implements MenuNode { readonly altNode : ActionMenuNode | undefined ; constructor ( public readonly action , protected readonly commands ) { if ( action . alt ) { this . altNode = new ActionMenuNode ( { commandId : action . alt } , commands ) ; } } get id ( ) : string { return this . action . commandId ; } get label ( ) : string { if ( this . action . label ) { return this . action . label ; } const cmd = this . commands . getCommand ( this . action . commandId ) ; if ( ! cmd ) { console . debug ( `No label for action menu node: No command "${ this . action . commandId }" exists.` ) ; return '' ; } return cmd . label || cmd . id ; } get icon ( ) : string | undefined { if ( this . action . icon ) { return this . action . icon ; } const command = this . commands . getCommand ( this . action . commandId ) ; return command && command . iconClass ; } get sortString ( ) : string { return this . action . order || this . label ; } }