UNPKG

@theia/core

Version:

Theia is a cloud & desktop IDE framework implemented in TypeScript.

359 lines • 14.5 kB
"use strict"; // ***************************************************************************** // 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-only WITH Classpath-exception-2.0 // ***************************************************************************** Object.defineProperty(exports, "__esModule", { value: true }); exports.MenuModelRegistry = exports.StructuralMenuChange = exports.ChangeKind = exports.MenuContribution = void 0; const tslib_1 = require("tslib"); const inversify_1 = require("inversify"); const command_1 = require("../command"); const contribution_provider_1 = require("../contribution-provider"); const disposable_1 = require("../disposable"); const event_1 = require("../event"); const action_menu_node_1 = require("./action-menu-node"); const composite_menu_node_1 = require("./composite-menu-node"); const menu_types_1 = require("./menu-types"); exports.MenuContribution = Symbol('MenuContribution'); var ChangeKind; (function (ChangeKind) { ChangeKind[ChangeKind["ADDED"] = 0] = "ADDED"; ChangeKind[ChangeKind["REMOVED"] = 1] = "REMOVED"; ChangeKind[ChangeKind["CHANGED"] = 2] = "CHANGED"; ChangeKind[ChangeKind["LINKED"] = 3] = "LINKED"; })(ChangeKind || (exports.ChangeKind = ChangeKind = {})); var StructuralMenuChange; (function (StructuralMenuChange) { function is(evt) { return evt.kind !== ChangeKind.CHANGED; } StructuralMenuChange.is = is; })(StructuralMenuChange || (exports.StructuralMenuChange = StructuralMenuChange = {})); /** * The MenuModelRegistry allows to register and unregister menus, submenus and actions * via strings and {@link MenuAction}s without the need to access the underlying UI * representation. */ let MenuModelRegistry = class MenuModelRegistry { get onDidChange() { return this.onDidChangeEmitter.event; } constructor(contributions, commands) { this.contributions = contributions; this.commands = commands; this.root = new composite_menu_node_1.CompositeMenuNode(''); this.independentSubmenus = new Map(); this.onDidChangeEmitter = new event_1.Emitter(); this.isReady = false; } onStart() { for (const contrib of this.contributions.getContributions()) { contrib.registerMenus(this); } this.isReady = true; } /** * Adds the given menu action to the menu denoted by the given path. * * @returns a disposable which, when called, will remove the menu action again. */ registerMenuAction(menuPath, item) { const menuNode = new action_menu_node_1.ActionMenuNode(item, this.commands); return this.registerMenuNode(menuPath, menuNode); } /** * Adds the given menu node to the menu denoted by the given path. * * @returns a disposable which, when called, will remove the menu node again. */ registerMenuNode(menuPath, menuNode, group) { const parent = this.getMenuNode(menuPath, group); const disposable = parent.addNode(menuNode); const parentPath = this.getParentPath(menuPath, group); this.fireChangeEvent({ kind: ChangeKind.ADDED, path: parentPath, affectedChildId: menuNode.id }); return this.changeEventOnDispose(parentPath, menuNode.id, disposable); } getParentPath(menuPath, group) { if (typeof menuPath === 'string') { return group ? [menuPath, group] : [menuPath]; } else { return group ? menuPath.concat(group) : menuPath; } } getMenuNode(menuPath, group) { if (typeof menuPath === 'string') { const target = this.independentSubmenus.get(menuPath); if (!target) { throw new Error(`Could not find submenu with id ${menuPath}`); } if (group) { return this.findSubMenu(target, group); } return target; } else { return this.findGroup(group ? menuPath.concat(group) : menuPath); } } /** * Register a new menu at the given path with the given label. * (If the menu already exists without a label, iconClass or order this method can be used to set them.) * * @param menuPath the path for which a new submenu shall be registered. * @param label the label to be used for the new submenu. * @param options optionally allows to set an icon class and specify the order of the new menu. * * @returns if the menu was successfully created a disposable will be returned which, * when called, will remove the menu again. If the menu already existed a no-op disposable * will be returned. * * Note that if the menu already existed and was registered with a different label an error * will be thrown. */ 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); let disposable = disposable_1.Disposable.NULL; if (!groupNode) { groupNode = new composite_menu_node_1.CompositeMenuNode(menuId, label, options, parent); disposable = this.changeEventOnDispose(groupPath, menuId, parent.addNode(groupNode)); this.fireChangeEvent({ kind: ChangeKind.ADDED, path: groupPath, affectedChildId: menuId }); } else { this.fireChangeEvent({ kind: ChangeKind.CHANGED, path: groupPath, }); groupNode.updateOptions({ ...options, label }); } return disposable; } registerIndependentSubmenu(id, label, options) { if (this.independentSubmenus.has(id)) { console.debug(`Independent submenu with path ${id} registered, but given ID already exists.`); } this.independentSubmenus.set(id, new composite_menu_node_1.CompositeMenuNode(id, label, options)); return this.changeEventOnDispose([], id, disposable_1.Disposable.create(() => this.independentSubmenus.delete(id))); } linkSubmenu(parentPath, childId, options, group) { const child = this.getMenuNode(childId); const parent = this.getMenuNode(parentPath, group); const affectedPath = this.getParentPath(parentPath, group); const isRecursive = (node, childNode) => { if (node.id === childNode.id) { return true; } if (node.parent) { return isRecursive(node.parent, childNode); } return false; }; // check for menu contribution recursion if (isRecursive(parent, child)) { console.warn(`Recursive menu contribution detected: ${child.id} is already in hierarchy of ${parent.id}.`); return disposable_1.Disposable.NULL; } const wrapper = new composite_menu_node_1.CompositeMenuNodeWrapper(child, parent, options); const disposable = parent.addNode(wrapper); this.fireChangeEvent({ kind: ChangeKind.LINKED, path: affectedPath, affectedChildId: child.id }); return this.changeEventOnDispose(affectedPath, child.id, disposable); } unregisterMenuAction(itemOrCommandOrId, menuPath) { const id = menu_types_1.MenuAction.is(itemOrCommandOrId) ? itemOrCommandOrId.commandId : command_1.Command.is(itemOrCommandOrId) ? itemOrCommandOrId.id : itemOrCommandOrId; if (menuPath) { const parent = this.findGroup(menuPath); parent.removeNode(id); this.fireChangeEvent({ kind: ChangeKind.REMOVED, path: menuPath, affectedChildId: id }); } else { this.unregisterMenuNode(id); } } /** * Recurse all menus, removing any menus matching the `id`. * * @param id technical identifier of the `MenuNode`. */ unregisterMenuNode(id) { const parentPath = []; const recurse = (root) => { root.children.forEach(node => { if (menu_types_1.CompoundMenuNode.isMutable(node)) { if (node.removeNode(id)) { this.fireChangeEvent({ kind: ChangeKind.REMOVED, path: parentPath, affectedChildId: id }); } parentPath.push(node.id); recurse(node); parentPath.pop(); } }); }; recurse(this.root); } /** * Finds a submenu as a descendant of the `root` node. * See {@link MenuModelRegistry.findSubMenu findSubMenu}. */ findGroup(menuPath, options) { let currentMenu = this.root; for (const segment of menuPath) { currentMenu = this.findSubMenu(currentMenu, segment, options); } return currentMenu; } /** * Finds or creates a submenu as an immediate child of `current`. * @throws if a node with the given `menuId` exists but is not a {@link MutableCompoundMenuNode}. */ findSubMenu(current, menuId, options) { const sub = current.children.find(e => e.id === menuId); if (menu_types_1.CompoundMenuNode.isMutable(sub)) { return sub; } if (sub) { throw new Error(`'${menuId}' is not a menu group.`); } const newSub = new composite_menu_node_1.CompositeMenuNode(menuId, undefined, options, current); current.addNode(newSub); return newSub; } /** * Returns the menu at the given path. * * @param menuPath the path specifying the menu to return. If not given the empty path will be used. * * @returns the root menu when `menuPath` is empty. If `menuPath` is not empty the specified menu is * returned if it exists, otherwise an error is thrown. */ getMenu(menuPath = []) { return this.findGroup(menuPath); } /** * Checks the given menu model whether it will show a menu with a single submenu. * * @param fullMenuModel the menu model to analyze * @param menuPath the menu's path * @returns if the menu will show a single submenu this returns a menu that will show the child elements of the submenu, * otherwise the given `fullMenuModel` is return */ removeSingleRootNode(fullMenuModel, menuPath) { // check whether all children are compound menus and that there is only one child that has further children if (!this.allChildrenCompound(fullMenuModel.children)) { return fullMenuModel; } let nonEmptyNode = undefined; for (const child of fullMenuModel.children) { if (!this.isEmpty(child.children || [])) { if (nonEmptyNode === undefined) { nonEmptyNode = child; } else { return fullMenuModel; } } } if (menu_types_1.CompoundMenuNode.is(nonEmptyNode) && nonEmptyNode.children.length === 1 && menu_types_1.CompoundMenuNode.is(nonEmptyNode.children[0])) { nonEmptyNode = nonEmptyNode.children[0]; } return menu_types_1.CompoundMenuNode.is(nonEmptyNode) ? nonEmptyNode : fullMenuModel; } allChildrenCompound(children) { return children.every(menu_types_1.CompoundMenuNode.is); } isEmpty(children) { if (children.length === 0) { return true; } if (!this.allChildrenCompound(children)) { return false; } for (const child of children) { if (!this.isEmpty(child.children || [])) { return false; } } return true; } changeEventOnDispose(path, id, disposable) { return disposable_1.Disposable.create(() => { disposable.dispose(); this.fireChangeEvent({ path, affectedChildId: id, kind: ChangeKind.REMOVED }); }); } fireChangeEvent(evt) { if (this.isReady) { this.onDidChangeEmitter.fire(evt); } } /** * Returns the {@link MenuPath path} at which a given menu node can be accessed from this registry, if it can be determined. * Returns `undefined` if the `parent` of any node in the chain is unknown. */ getPath(node) { const identifiers = []; const visited = []; let next = node; while (next && !visited.includes(next)) { if (next === this.root) { return identifiers.reverse(); } visited.push(next); identifiers.push(next.id); next = next.parent; } return undefined; } }; exports.MenuModelRegistry = MenuModelRegistry; exports.MenuModelRegistry = MenuModelRegistry = tslib_1.__decorate([ (0, inversify_1.injectable)(), tslib_1.__param(0, (0, inversify_1.inject)(contribution_provider_1.ContributionProvider)), tslib_1.__param(0, (0, inversify_1.named)(exports.MenuContribution)), tslib_1.__param(1, (0, inversify_1.inject)(command_1.CommandRegistry)), tslib_1.__metadata("design:paramtypes", [Object, command_1.CommandRegistry]) ], MenuModelRegistry); //# sourceMappingURL=menu-model-registry.js.map