@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
359 lines • 14.5 kB
JavaScript
"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