Skip to content

Commit

Permalink
Refactor menu nodes
Browse files Browse the repository at this point in the history
Fixes eclipse-theia#14217

Makes menu nodes active object that can decide on visibility,
enablement, etc. themselves.

Contributed on behalf of STMicroelectronics

Signed-off-by: Thomas Mäder <[email protected]>
  • Loading branch information
tsmaeder committed Oct 30, 2024
1 parent ea62f67 commit dbcfe42
Show file tree
Hide file tree
Showing 68 changed files with 1,829 additions and 1,951 deletions.
6 changes: 1 addition & 5 deletions examples/api-samples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@
"frontend": "lib/browser/api-samples-frontend-module",
"backend": "lib/node/api-samples-backend-module"
},
{
"frontend": "lib/browser/menu/sample-browser-menu-module",
"frontendElectron": "lib/electron-browser/menu/sample-electron-menu-module"
},
{
"electronMain": "lib/electron-main/update/sample-updater-main-module",
"frontendElectron": "lib/electron-browser/updater/sample-updater-frontend-module"
Expand Down Expand Up @@ -60,4 +56,4 @@
"devDependencies": {
"@theia/ext-scripts": "1.54.0"
}
}
}

This file was deleted.

39 changes: 26 additions & 13 deletions examples/api-samples/src/browser/menu/sample-menu-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import { ConfirmDialog, Dialog, QuickInputService } from '@theia/core/lib/browse
import { ReactDialog } from '@theia/core/lib/browser/dialogs/react-dialog';
import { SelectComponent } from '@theia/core/lib/browser/widgets/select-component';
import {
Command, CommandContribution, CommandRegistry, MAIN_MENU_BAR,
MenuContribution, MenuModelRegistry, MenuNode, MessageService, SubMenuOptions
Command, CommandContribution, CommandMenu, CommandRegistry, ContextExpressionMatcher, MAIN_MENU_BAR,
MenuContribution, MenuModelRegistry, MessageService
} from '@theia/core/lib/common';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
Expand Down Expand Up @@ -226,9 +226,8 @@ export class SampleCommandContribution implements CommandContribution {
export class SampleMenuContribution implements MenuContribution {
registerMenus(menus: MenuModelRegistry): void {
const subMenuPath = [...MAIN_MENU_BAR, 'sample-menu'];
menus.registerSubmenu(subMenuPath, 'Sample Menu', {
order: '2' // that should put the menu right next to the File menu
});
menus.registerSubmenu(subMenuPath, 'Sample Menu', '2'); // that should put the menu right next to the File menu

menus.registerMenuAction(subMenuPath, {
commandId: SampleCommand.id,
order: '0'
Expand All @@ -238,7 +237,7 @@ export class SampleMenuContribution implements MenuContribution {
order: '2'
});
const subSubMenuPath = [...subMenuPath, 'sample-sub-menu'];
menus.registerSubmenu(subSubMenuPath, 'Sample sub menu', { order: '2' });
menus.registerSubmenu(subSubMenuPath, 'Sample sub menu', '2');
menus.registerMenuAction(subSubMenuPath, {
commandId: SampleCommand.id,
order: '1'
Expand All @@ -247,8 +246,8 @@ export class SampleMenuContribution implements MenuContribution {
commandId: SampleCommand2.id,
order: '3'
});
const placeholder = new PlaceholderMenuNode([...subSubMenuPath, 'placeholder'].join('-'), 'Placeholder', { order: '0' });
menus.registerMenuNode(subSubMenuPath, placeholder);
const placeholder = new PlaceholderMenuNode([...subSubMenuPath, 'placeholder'].join('-'), 'Placeholder', '0');
menus.registerCommandMenu(subSubMenuPath, placeholder);

/**
* Register an action menu with an invalid command (un-registered and without a label) in order
Expand All @@ -262,16 +261,30 @@ export class SampleMenuContribution implements MenuContribution {
/**
* Special menu node that is not backed by any commands and is always disabled.
*/
export class PlaceholderMenuNode implements MenuNode {
export class PlaceholderMenuNode implements CommandMenu {

constructor(readonly id: string, public readonly label: string, protected options?: SubMenuOptions) { }
constructor(readonly id: string, public readonly label: string, readonly order?: string, readonly icon?: string) { }

isEnabled(...args: any[]): boolean {
return false;
}

get icon(): string | undefined {
return this.options?.iconClass;
isToggled(): boolean {
return false;
}
run(...args: any[]): Promise<void> {
throw new Error('Should never happen');
}
getAccelerator(context: HTMLElement | undefined): string[] {
return [];
}

get sortString(): string {
return this.options?.order || this.label;
return this.order || this.label;
}

isVisible(contextMatcher: ContextExpressionMatcher, ...args: any[]): boolean {
return true;
}

}
Expand Down

This file was deleted.

28 changes: 14 additions & 14 deletions packages/core/src/browser/common-frontend-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import debounce = require('lodash.debounce');
import { injectable, inject, optional } from 'inversify';
import { MAIN_MENU_BAR, MANAGE_MENU, MenuContribution, MenuModelRegistry, ACCOUNTS_MENU } from '../common/menu';
import { MAIN_MENU_BAR, MANAGE_MENU, MenuContribution, MenuModelRegistry, ACCOUNTS_MENU, CompoundMenuNode, CommandMenu, Group, Submenu } from '../common/menu';
import { KeybindingContribution, KeybindingRegistry } from './keybinding';
import { FrontendApplication } from './frontend-application';
import { FrontendApplicationContribution, OnWillStopAction } from './frontend-application-contribution';
Expand Down Expand Up @@ -83,7 +83,7 @@ export namespace CommonMenus {
export const FILE_SETTINGS_SUBMENU_THEME = [...FILE_SETTINGS_SUBMENU, '2_settings_submenu_theme'];
export const FILE_CLOSE = [...FILE, '6_close'];

export const FILE_NEW_CONTRIBUTIONS = 'file/newFile';
export const FILE_NEW_CONTRIBUTIONS = ['file', 'newFile'];

export const EDIT = [...MAIN_MENU_BAR, '2_edit'];
export const EDIT_UNDO = [...EDIT, '1_undo'];
Expand Down Expand Up @@ -606,7 +606,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
registry.registerSubmenu(CommonMenus.HELP, nls.localizeByDefault('Help'));

// For plugins contributing create new file commands/menu-actions
registry.registerIndependentSubmenu(CommonMenus.FILE_NEW_CONTRIBUTIONS, nls.localizeByDefault('New File...'));
registry.registerSubmenu(CommonMenus.FILE_NEW_CONTRIBUTIONS, nls.localizeByDefault('New File...'));

registry.registerMenuAction(CommonMenus.FILE_SAVE, {
commandId: CommonCommands.SAVE.id
Expand Down Expand Up @@ -747,7 +747,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
commandId: CommonCommands.SELECT_ICON_THEME.id
});

registry.registerSubmenu(CommonMenus.MANAGE_SETTINGS_THEMES, nls.localizeByDefault('Themes'), { order: 'a50' });
registry.registerSubmenu(CommonMenus.MANAGE_SETTINGS_THEMES, nls.localizeByDefault('Themes'), 'a50');
registry.registerMenuAction(CommonMenus.MANAGE_SETTINGS_THEMES, {
commandId: CommonCommands.SELECT_COLOR_THEME.id,
order: '0'
Expand Down Expand Up @@ -1452,7 +1452,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
* @todo https://github.com/eclipse-theia/theia/issues/12824
*/
protected async showNewFilePicker(): Promise<void> {
const newFileContributions = this.menuRegistry.getMenuNode(CommonMenus.FILE_NEW_CONTRIBUTIONS); // Add menus
const newFileContributions = this.menuRegistry.getMenuNode(CommonMenus.FILE_NEW_CONTRIBUTIONS) as Submenu; // Add menus
const items: QuickPickItemOrSeparator[] = [
{
label: nls.localizeByDefault('New Text File'),
Expand All @@ -1461,22 +1461,22 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
},
...newFileContributions.children
.flatMap(node => {
if (node.children && node.children.length > 0) {
if (CompoundMenuNode.is(node) && node.children.length > 0) {
return node.children;
}
return node;
})
.filter(node => node.role || node.command)
.filter(node => Group.is(node) || CommandMenu.is(node))
.map(node => {
if (node.role) {
if (Group.is(node)) {
return { type: 'separator' } as QuickPickSeparator;
} else {
const item = node as CommandMenu;
return {
label: item.label,
execute: () => item.run()
};
}
const command = this.commandRegistry.getCommand(node.command!);
return {
label: command!.label!,
execute: async () => this.commandRegistry.executeCommand(command!.id!)
};

})
];

Expand Down
29 changes: 23 additions & 6 deletions packages/core/src/browser/context-menu-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@

/* eslint-disable @typescript-eslint/no-explicit-any */

import { injectable } from 'inversify';
import { MenuPath } from '../common/menu';
import { injectable, inject } from 'inversify';
import { CompoundMenuNode, MenuModelRegistry, MenuPath } from '../common/menu';
import { Disposable, DisposableCollection } from '../common/disposable';
import { ContextMatcher } from './context-key-service';
import { ContextKeyService, ContextMatcher } from './context-key-service';

export interface Coordinate { x: number; y: number; }
export const Coordinate = Symbol('Coordinate');
Expand Down Expand Up @@ -53,6 +53,10 @@ export abstract class ContextMenuAccess implements Disposable {
@injectable()
export abstract class ContextMenuRenderer {

@inject(MenuModelRegistry) menuRegistry: MenuModelRegistry;
@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;

protected _current: ContextMenuAccess | undefined;
protected readonly toDisposeOnSetCurrent = new DisposableCollection();
/**
Expand All @@ -77,13 +81,26 @@ export abstract class ContextMenuRenderer {
}

render(options: RenderContextMenuOptions): ContextMenuAccess {
let menu = CompoundMenuNode.is(options.menuPath) ? options.menuPath : this.menuRegistry.getMenu(options.menuPath);

const resolvedOptions = this.resolve(options);
const access = this.doRender(resolvedOptions);

if (resolvedOptions.skipSingleRootNode) {
menu = MenuModelRegistry.removeSingleRootNode(menu);
}

const access = this.doRender(menu, resolvedOptions.anchor, options.contextKeyService || this.contextKeyService, resolvedOptions.args, resolvedOptions.context, resolvedOptions.onHide);
this.setCurrent(access);
return access;
}

protected abstract doRender(options: RenderContextMenuOptions): ContextMenuAccess;
protected abstract doRender(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() : [];
Expand All @@ -99,7 +116,7 @@ export abstract class ContextMenuRenderer {
}

export interface RenderContextMenuOptions {
menuPath: MenuPath;
menuPath: CompoundMenuNode | MenuPath;
anchor: Anchor;
args?: any[];
/**
Expand Down
Loading

0 comments on commit dbcfe42

Please sign in to comment.