@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
159 lines (136 loc) • 5.36 kB
text/typescript
// *****************************************************************************
// 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
// *****************************************************************************
/* eslint-disable @typescript-eslint/no-explicit-any */
import { injectable, inject } from 'inversify';
import { CompoundMenuNode, GroupImpl, MenuModelRegistry, MenuPath } from '../common/menu';
import { Disposable, DisposableCollection } from '../common/disposable';
import { ContextKeyService, ContextMatcher } from './context-key-service';
export interface Coordinate { x: number; y: number; }
export const Coordinate = Symbol('Coordinate');
export type Anchor = MouseEvent | Coordinate;
export function coordinateFromAnchor(anchor: Anchor): Coordinate {
const { x, y } = anchor instanceof MouseEvent ? { x: anchor.clientX, y: anchor.clientY } : anchor;
return { x, y };
}
export class ContextMenuAccess implements Disposable {
protected readonly toDispose = new DisposableCollection();
readonly onDispose = this.toDispose.onDispose;
constructor(toClose: Disposable) {
this.toDispose.push(toClose);
}
get disposed(): boolean {
return this.toDispose.disposed;
}
dispose(): void {
this.toDispose.dispose();
}
}
()
export abstract class ContextMenuRenderer {
(MenuModelRegistry) menuRegistry: MenuModelRegistry;
(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;
protected _current: ContextMenuAccess | undefined;
protected readonly toDisposeOnSetCurrent = new DisposableCollection();
/**
* Currently opened context menu.
* Rendering a new context menu will close the current.
*/
get current(): ContextMenuAccess | undefined {
return this._current;
}
set current(current: ContextMenuAccess | undefined) {
this.setCurrent(current);
}
protected setCurrent(current: ContextMenuAccess | undefined): void {
if (this._current === current) {
return;
}
this.toDisposeOnSetCurrent.dispose();
this._current = current;
if (current) {
this.toDisposeOnSetCurrent.push(current.onDispose(() => {
this._current = undefined;
}));
this.toDisposeOnSetCurrent.push(current);
}
}
render(options: RenderContextMenuOptions): ContextMenuAccess {
let menu = options.menu;
if (!menu) {
menu = this.menuRegistry.getMenu(options.menuPath) || new GroupImpl('emtpyContextMenu');
}
const resolvedOptions = this.resolve(options);
if (resolvedOptions.skipSingleRootNode) {
menu = MenuModelRegistry.removeSingleRootNode(menu);
}
const access = this.doRender({
menuPath: options.menuPath,
menu,
anchor: resolvedOptions.anchor,
contextMatcher: options.contextKeyService || this.contextKeyService,
args: resolvedOptions.args,
context: resolvedOptions.context,
onHide: resolvedOptions.onHide
});
this.setCurrent(access);
return access;
}
protected abstract doRender(params: {
menuPath: MenuPath,
menu: CompoundMenuNode,
anchor: Anchor,
contextMatcher: ContextMatcher,
args?: any[],
context?: HTMLElement,
onHide?: () => void
}): ContextMenuAccess;
protected resolve(options: RenderContextMenuOptions): RenderContextMenuOptions {
const args: any[] = options.args ? options.args.slice() : [];
if (options.includeAnchorArg !== false) {
args.push(options.anchor);
}
return {
...options,
args
};
}
}
export interface RenderContextMenuOptions {
menu?: CompoundMenuNode,
menuPath: MenuPath;
anchor: Anchor;
args?: any[];
/**
* Whether the anchor should be passed as an argument to the handlers of commands for this context menu.
* If true, the anchor will be appended to the list of arguments or passed as the only argument if no other
* arguments are supplied.
* Default is `true`.
*/
includeAnchorArg?: boolean;
/**
* A DOM context for the menu to be shown
* Will be used to attach the menu to a window and to evaluate enablement ("when"-clauses)
*/
context: HTMLElement;
contextKeyService?: ContextMatcher;
onHide?: () => void;
/**
* If true a single submenu in the context menu is not rendered but its children are rendered on the top level.
* Default is `false`.
*/
skipSingleRootNode?: boolean;
}