UNPKG

@eclipse-glsp/theia-integration

Version:

Glue code to integrate GLSP clients into Eclipse Theia

208 lines (176 loc) 7.8 kB
/******************************************************************************** * Copyright (c) 2019-2025 EclipseSource 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 ********************************************************************************/ // based on: https://github.com/eclipse-sprotty/sprotty-theia/blob/v0.12.0/src/sprotty/theia-sprotty-context-menu-service.ts import { Anchor, ClientMenuItem, FocusStateChangedAction, IActionDispatcher, IContextMenuService } from '@eclipse-glsp/client'; import { Command, CommandHandler, CommandRegistry, Disposable, MenuAction, MenuModelRegistry, MenuPath } from '@theia/core'; import { ApplicationShell, ContextMenuRenderer } from '@theia/core/lib/browser'; import { inject, injectable } from 'inversify'; import { getDiagramWidget, GLSPDiagramWidget } from './diagram/glsp-diagram-widget'; export namespace TheiaGLSPContextMenu { export const CONTEXT_MENU: MenuPath = ['theia-glsp-context-menu']; } @injectable() export class TheiaContextMenuService implements IContextMenuService { protected timeout?: number; protected disposables: DisposableItem[] = []; @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer; @inject(MenuModelRegistry) protected readonly menuProvider: MenuModelRegistry; @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; @inject(ApplicationShell) protected readonly shell: ApplicationShell; protected actionDispatcher?: IActionDispatcher; connect(actionDispatcher: IActionDispatcher): void { this.actionDispatcher = actionDispatcher; } get diagramWidget(): GLSPDiagramWidget | undefined { return getDiagramWidget(this.shell); } show(items: ClientMenuItem[], anchor: Anchor, onHide?: () => void): void { const context = this.diagramWidget?.node; if (!context) { console.warn('No context available for context menu'); return; } this.cleanUpNow(); this.disposables = this.register(TheiaGLSPContextMenu.CONTEXT_MENU, items); const renderOptions = { menuPath: TheiaGLSPContextMenu.CONTEXT_MENU, anchor: anchor, context, onHide: () => { if (onHide) { onHide(); } this.scheduleCleanup(); this.restoreFocus(); } }; this.contextMenuRenderer.render(renderOptions); } protected register(menuPath: string[], items: ClientMenuItem[]): DisposableItem[] { const disposables: DisposableItem[] = []; for (const item of items) { if (item.children && item.children.length > 0) { const menuPathOfItem = item.group ? [...menuPath, item.group] : menuPath; disposables.push(this.registerSubmenu(menuPathOfItem, item)); disposables.push(...this.register([...menuPathOfItem, item.id], item.children)); } else { disposables.push(this.registerCommand(menuPath, item)); disposables.push(this.registerMenuAction(menuPath, item)); } } return disposables; } protected registerSubmenu(menuPath: string[], item: ClientMenuItem): DisposableItem { return this.menuProvider.registerSubmenu([...menuPath, item.id], item.label, { icon: item.icon }); } protected registerCommand(menuPath: string[], item: ClientMenuItem): DisposableItem { const command: Command = { id: commandId(menuPath, item), label: item.label, iconClass: item.icon }; const disposable = this.commandRegistry.registerCommand(command, new GLSPCommandHandler(item, this.actionDispatcher)); return new DisposableCommand(command, disposable); } protected registerMenuAction(menuPath: string[], item: ClientMenuItem): DisposableItem { const { label, sortString: order, isEnabled, isToggled, isVisible } = item; const menuAction = { label, order, isEnabled, isToggled, isVisible, commandId: commandId(menuPath, item) }; const menuPathOfItem = item.group ? [...menuPath, item.group] : menuPath; const disposable = this.menuProvider.registerMenuAction(menuPathOfItem, menuAction); return new DisposableMenuAction(menuAction, disposable); } protected cleanUpNow(): void { window.clearTimeout(this.timeout); this.cleanUp(); } protected scheduleCleanup(): void { this.timeout = window.setTimeout(() => { this.cleanUp(); }, 200); } protected cleanUp(): void { this.disposables.forEach(disposable => disposable.dispose(this.menuProvider, this.commandRegistry)); this.disposables = []; } protected restoreFocus(): void { const widget = this.diagramWidget; if (!widget || widget.hasFocus) { return; } widget.actionDispatcher.dispatch(FocusStateChangedAction.create(true)); } } class GLSPCommandHandler implements CommandHandler { constructor( readonly menuItem: ClientMenuItem, readonly actionDispatcher?: IActionDispatcher ) {} execute(...args: any[]): void { if (this.actionDispatcher && this.menuItem.actions) { this.actionDispatcher.dispatchAll(this.menuItem.actions); } } isEnabled(...args: any[]): boolean { return getBooleanValue(this.menuItem.isEnabled, true); } isVisible(...args: any[]): boolean { return getBooleanValue(this.menuItem.isVisible, true); } isToggled(...args: any[]): boolean { return getBooleanValue(this.menuItem.isToggled, false); } } interface DisposableItem { dispose(menuProvider: MenuModelRegistry, commandRegistry: CommandRegistry): void; } class DisposableMenuAction implements DisposableItem { constructor( protected readonly menuAction: MenuAction, protected readonly disposable: Disposable ) {} dispose(menuProvider: MenuModelRegistry, commandRegistry: CommandRegistry): void { menuProvider.unregisterMenuAction(this.menuAction); this.disposable.dispose(); } } class DisposableCommand implements DisposableItem { constructor( protected readonly command: Command, protected readonly disposable: Disposable ) {} dispose(menuProvider: MenuModelRegistry, commandRegistry: CommandRegistry): void { commandRegistry.unregisterCommand(this.command); this.disposable.dispose(); } } function commandId(menuPath: string[], item: any): string { return menuPath.join('.') + '.' + item.id; } function getBooleanValue(value: any, defaultValue: boolean): boolean { let returnVal = defaultValue; if (isFunction(value)) { returnVal = value(); } else if (isBoolean(value)) { returnVal = value; } return returnVal; } function isFunction(value: () => boolean | boolean): value is () => boolean { return !!(value && value.constructor && value.apply); } function isBoolean(value: () => boolean | boolean): boolean { return typeof value === 'boolean'; }