@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
257 lines (235 loc) • 11.4 kB
text/typescript
// *****************************************************************************
// Copyright (C) 2022 Ericsson 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
// *****************************************************************************
import debounce = require('lodash.debounce');
import { inject, injectable, named } from 'inversify';
// eslint-disable-next-line max-len
import { CommandMenuNode, CommandRegistry, CompoundMenuNode, ContributionProvider, Disposable, DisposableCollection, Emitter, Event, MenuModelRegistry, MenuNode, MenuPath } from '../../../common';
import { ContextKeyService } from '../../context-key-service';
import { FrontendApplicationContribution } from '../../frontend-application-contribution';
import { Widget } from '../../widgets';
import { AnyToolbarItem, ConditionalToolbarItem, MenuDelegate, MenuToolbarItem, ReactTabBarToolbarItem, TabBarToolbarItem } from './tab-bar-toolbar-types';
import { ToolbarMenuNodeWrapper } from './tab-bar-toolbar-menu-adapters';
/**
* Clients should implement this interface if they want to contribute to the tab-bar toolbar.
*/
export const TabBarToolbarContribution = Symbol('TabBarToolbarContribution');
/**
* Representation of a tabbar toolbar contribution.
*/
export interface TabBarToolbarContribution {
/**
* Registers toolbar items.
* @param registry the tabbar toolbar registry.
*/
registerToolbarItems(registry: TabBarToolbarRegistry): void;
}
function yes(): true { return true; }
const menuDelegateSeparator = '=@=';
/**
* Main, shared registry for tab-bar toolbar items.
*/
export class TabBarToolbarRegistry implements FrontendApplicationContribution {
protected items = new Map<string, TabBarToolbarItem | ReactTabBarToolbarItem>();
protected menuDelegates = new Map<string, MenuDelegate>();
protected readonly commandRegistry: CommandRegistry;
protected readonly contextKeyService: ContextKeyService;
protected readonly menuRegistry: MenuModelRegistry;
protected readonly contributionProvider: ContributionProvider<TabBarToolbarContribution>;
protected readonly onDidChangeEmitter = new Emitter<void>();
readonly onDidChange: Event<void> = this.onDidChangeEmitter.event;
// debounce in order to avoid to fire more than once in the same tick
protected fireOnDidChange = debounce(() => this.onDidChangeEmitter.fire(undefined), 0);
onStart(): void {
const contributions = this.contributionProvider.getContributions();
for (const contribution of contributions) {
contribution.registerToolbarItems(this);
}
}
/**
* Registers the given item. Throws an error, if the corresponding command cannot be found or an item has been already registered for the desired command.
*
* @param item the item to register.
*/
registerItem(item: TabBarToolbarItem | ReactTabBarToolbarItem): Disposable {
const { id } = item;
if (this.items.has(id)) {
throw new Error(`A toolbar item is already registered with the '${id}' ID.`);
}
this.items.set(id, item);
this.fireOnDidChange();
const toDispose = new DisposableCollection(
Disposable.create(() => this.fireOnDidChange()),
Disposable.create(() => this.items.delete(id))
);
if (item.onDidChange) {
toDispose.push(item.onDidChange(() => this.fireOnDidChange()));
}
return toDispose;
}
/**
* Returns an array of tab-bar toolbar items which are visible when the `widget` argument is the current one.
*
* By default returns with all items where the command is enabled and `item.isVisible` is `true`.
*/
visibleItems(widget: Widget): Array<TabBarToolbarItem | ReactTabBarToolbarItem> {
if (widget.isDisposed) {
return [];
}
const result: Array<TabBarToolbarItem | ReactTabBarToolbarItem> = [];
for (const item of this.items.values()) {
if (this.isItemVisible(item, widget)) {
result.push(item);
}
}
for (const delegate of this.menuDelegates.values()) {
if (delegate.isVisible(widget)) {
const menu = this.menuRegistry.getMenu(delegate.menuPath);
const children = CompoundMenuNode.getFlatChildren(menu.children);
for (const child of children) {
if (!child.when || this.contextKeyService.match(child.when, widget.node)) {
if (child.children) {
for (const grandchild of child.children) {
if (!grandchild.when || this.contextKeyService.match(grandchild.when, widget.node)) {
if (CommandMenuNode.is(grandchild)) {
result.push(new ToolbarMenuNodeWrapper(grandchild, child.id, delegate.menuPath));
} else if (CompoundMenuNode.is(grandchild)) {
let menuPath;
if (menuPath = this.menuRegistry.getPath(grandchild)) {
result.push(new ToolbarMenuNodeWrapper(grandchild, child.id, menuPath));
}
}
}
}
} else if (child.command) {
result.push(new ToolbarMenuNodeWrapper(child, '', delegate.menuPath));
}
}
}
}
}
return result;
}
/**
* Query whether a toolbar `item` should be shown in the toolbar.
* This implementation delegates to item-specific checks according to their type.
*
* @param item a menu toolbar item
* @param widget the widget that is updating the toolbar
* @returns `false` if the `item` should be suppressed, otherwise `true`
*/
protected isItemVisible(item: TabBarToolbarItem | ReactTabBarToolbarItem, widget: Widget): boolean {
if (TabBarToolbarItem.is(item) && item.command && !this.isTabBarToolbarItemVisible(item, widget)) {
return false;
}
if (MenuToolbarItem.is(item) && !this.isMenuToolbarItemVisible(item, widget)) {
return false;
}
if (AnyToolbarItem.isConditional(item) && !this.isConditionalItemVisible(item, widget)) {
return false;
}
// The item is not vetoed. Accept it
return true;
}
/**
* Query whether a conditional toolbar `item` should be shown in the toolbar.
* This implementation delegates to the `item`'s own intrinsic conditionality.
*
* @param item a menu toolbar item
* @param widget the widget that is updating the toolbar
* @returns `false` if the `item` should be suppressed, otherwise `true`
*/
protected isConditionalItemVisible(item: ConditionalToolbarItem, widget: Widget): boolean {
if (item.isVisible && !item.isVisible(widget)) {
return false;
}
if (item.when && !this.contextKeyService.match(item.when, widget.node)) {
return false;
}
return true;
}
/**
* Query whether a tab-bar toolbar `item` that has a command should be shown in the toolbar.
* This implementation returns `false` if the `item`'s command is not visible in the
* `widget` according to the command registry.
*
* @param item a tab-bar toolbar item that has a non-empty `command`
* @param widget the widget that is updating the toolbar
* @returns `false` if the `item` should be suppressed, otherwise `true`
*/
protected isTabBarToolbarItemVisible(item: TabBarToolbarItem, widget: Widget): boolean {
return this.commandRegistry.isVisible(item.command, widget);
}
/**
* Query whether a menu toolbar `item` should be shown in the toolbar.
* This implementation returns `false` if the `item` does not have any actual menu to show.
*
* @param item a menu toolbar item
* @param widget the widget that is updating the toolbar
* @returns `false` if the `item` should be suppressed, otherwise `true`
*/
protected isMenuToolbarItemVisible(item: MenuToolbarItem, widget: Widget): boolean {
const menu = this.menuRegistry.getMenu(item.menuPath);
const isVisible: (node: MenuNode) => boolean = node =>
node.children?.length
// Either the node is a sub-menu that has some visible child ...
? node.children?.some(isVisible)
// ... or there is a command ...
: !!node.command
// ... that is visible ...
&& this.commandRegistry.isVisible(node.command, widget)
// ... and a "when" clause does not suppress the menu node.
&& (!node.when || this.contextKeyService.match(node.when, widget?.node));
return isVisible(menu);
}
unregisterItem(itemOrId: TabBarToolbarItem | ReactTabBarToolbarItem | string): void {
const id = typeof itemOrId === 'string' ? itemOrId : itemOrId.id;
if (this.items.delete(id)) {
this.fireOnDidChange();
}
}
registerMenuDelegate(menuPath: MenuPath, when?: string | ((widget: Widget) => boolean)): Disposable {
const id = this.toElementId(menuPath);
if (!this.menuDelegates.has(id)) {
const isVisible: MenuDelegate['isVisible'] = !when
? yes
: typeof when === 'function'
? when
: widget => this.contextKeyService.match(when, widget?.node);
this.menuDelegates.set(id, { menuPath, isVisible });
this.fireOnDidChange();
return { dispose: () => this.unregisterMenuDelegate(menuPath) };
}
console.warn('Unable to register menu delegate. Delegate has already been registered', menuPath);
return Disposable.NULL;
}
unregisterMenuDelegate(menuPath: MenuPath): void {
if (this.menuDelegates.delete(this.toElementId(menuPath))) {
this.fireOnDidChange();
}
}
/**
* Generate a single ID string from a menu path that
* is likely to be unique amongst the items in the toolbar.
*
* @param menuPath a menubar path
* @returns a likely unique ID based on the path
*/
toElementId(menuPath: MenuPath): string {
return menuPath.join(menuDelegateSeparator);
}
}