Skip to content

Commit e6ec82f

Browse files
We can do it without transparent menu nodes, but it isn't as fun
1 parent a22d00e commit e6ec82f

File tree

9 files changed

+136
-153
lines changed

9 files changed

+136
-153
lines changed

packages/core/src/browser/menu/browser-menu-node-factory.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ export class BrowserMenuNodeFactory implements MenuNodeFactory {
3838
createCommandMenu(item: MenuAction): CommandMenu {
3939
return new ActionMenuNode(item, this.commandRegistry, this.keybindingRegistry, this.contextKeyService);
4040
}
41-
createSubmenu(id: string, label: string, contextKeyOverlays: Record<string, string> | undefined, orderString?: string, icon?: string, when?: string, transparent?: boolean):
41+
createSubmenu(id: string, label: string, contextKeyOverlays: Record<string, string> | undefined, orderString?: string, icon?: string, when?: string):
4242
Submenu & MutableCompoundMenuNode {
43-
return new SubmenuImpl(id, label, contextKeyOverlays, orderString, icon, when, transparent);
43+
return new SubmenuImpl(id, label, contextKeyOverlays, orderString, icon, when);
4444
}
4545
createSubmenuLink(delegate: Submenu, sortString?: string, when?: string, argumentAdapter?: (...args: unknown[]) => unknown[]): MenuNode {
4646
return new SubMenuLink(delegate, sortString, when, argumentAdapter);

packages/core/src/browser/menu/browser-menu-plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ export class DynamicMenuWidget extends MenuWidget {
306306

307307
protected updateSubMenus(parent: MenuWidget, menu: CompoundMenuNode, commands: LuminoCommandRegistry,
308308
contextMatcher: ContextMatcher, context?: HTMLElement | undefined): void {
309-
const items = this.createItems(CompoundMenuNode.flatten(menu), commands, contextMatcher, context);
309+
const items = this.createItems(menu.children, commands, contextMatcher, context);
310310
while (items[items.length - 1]?.type === 'separator') {
311311
items.pop();
312312
}

packages/core/src/browser/menu/composite-menu-node.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,32 @@
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
1616

17-
import { CommandMenu, CompoundMenuNode, ContextExpressionMatcher, Group, MenuNode, MenuPath, MutableCompoundMenuNode, RenderedMenuNode, Submenu } from '../../common/menu/menu-types';
18-
// import { Event } from '../../common';
17+
import {
18+
CommandMenu,
19+
CompoundMenuNode,
20+
ContextExpressionMatcher,
21+
Group,
22+
MenuNode,
23+
MenuPath,
24+
MutableCompoundMenuNode,
25+
RenderedMenuNode,
26+
Submenu
27+
} from '../../common/menu/menu-types';
1928

2029
export class SubMenuLink implements CompoundMenuNode {
2130
constructor(private readonly delegate: CompoundMenuNode & Partial<RenderedMenuNode>, private readonly _sortString?: string, private readonly _when?: string,
2231
private readonly argumentAdapter?: (...args: unknown[]) => unknown[]) { }
2332

2433
get id(): string { return this.delegate.id; };
25-
get transparent(): boolean | undefined { return this.delegate.transparent; }
2634
get children(): MenuNode[] {
2735
const { argumentAdapter } = this;
2836
if (!argumentAdapter) { return this.delegate.children; }
2937
return this.delegate.children.map(child =>
30-
CommandMenu.is(child) ? new DelegatingAction(child, argumentAdapter) : CompoundMenuNode.is(child) ? new SubMenuLink(child, child.sortString, undefined, argumentAdapter) : child
38+
CommandMenu.is(child)
39+
? new DelegatingAction(child, argumentAdapter)
40+
: CompoundMenuNode.is(child)
41+
? new SubMenuLink(child, child.sortString, undefined, argumentAdapter)
42+
: child
3143
);
3244
}
3345
get contextKeyOverlays(): Record<string, string> | undefined { return this.delegate.contextKeyOverlays; }
@@ -167,7 +179,6 @@ export class SubmenuImpl extends AbstractCompoundMenuImpl implements Submenu {
167179
orderString?: string,
168180
readonly icon?: string,
169181
when?: string,
170-
readonly transparent?: boolean,
171182
) {
172183
super(id, orderString, when);
173184
}

packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.ts

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -136,24 +136,21 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution {
136136
}
137137

138138
for (const delegate of this.menuDelegates.values()) {
139-
if (delegate.isVisible(widget)) {
140-
const menu = this.menuRegistry.getMenu(delegate.menuPath);
141-
if (menu) {
142-
for (const child of CompoundMenuNode.flatten(menu)) {
143-
if (child.isVisible(this.contextKeyService, widget.node)) {
144-
if (CompoundMenuNode.is(child)) {
145-
for (const grandchild of CompoundMenuNode.flatten(child)) {
146-
if (grandchild.isVisible(this.contextKeyService, widget.node) && RenderedMenuNode.is(grandchild)) {
147-
result.push(new ToolbarMenuNodeWrapper(this.commandRegistry, this.menuRegistry,
148-
this.contextKeyService, this.contextMenuRenderer, grandchild, child.id, delegate.menuPath));
149-
}
150-
}
151-
} else if (CommandMenu.is(child)) {
152-
result.push(new ToolbarMenuNodeWrapper(this.commandRegistry, this.menuRegistry,
153-
this.contextKeyService, this.contextMenuRenderer, child, undefined, delegate.menuPath));
154-
}
139+
if (!delegate.isVisible(widget)) { continue; }
140+
const menu = this.menuRegistry.getMenu(delegate.menuPath);
141+
if (!menu) { continue; }
142+
for (const child of menu.children) {
143+
if (!child.isVisible(this.contextKeyService, widget.node)) { continue; }
144+
if (CompoundMenuNode.is(child)) {
145+
for (const grandchild of child.children) {
146+
if (grandchild.isVisible(this.contextKeyService, widget.node) && RenderedMenuNode.is(grandchild)) {
147+
result.push(new ToolbarMenuNodeWrapper(this.commandRegistry, this.menuRegistry,
148+
this.contextKeyService, this.contextMenuRenderer, grandchild, child.id, delegate.menuPath));
155149
}
156150
}
151+
} else if (CommandMenu.is(child)) {
152+
result.push(new ToolbarMenuNodeWrapper(this.commandRegistry, this.menuRegistry,
153+
this.contextKeyService, this.contextMenuRenderer, child, undefined, delegate.menuPath));
157154
}
158155
}
159156
}

packages/core/src/common/menu/menu-model-registry.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,18 @@ export interface MenuNodeFactory {
8686
createGroup(id: string, orderString?: string, when?: string): Group & MutableCompoundMenuNode;
8787
createCommandMenu(item: MenuAction): CommandMenu;
8888
createSubmenu(id: string, label: string, contextKeyOverlays: Record<string, string> | undefined,
89-
orderString?: string, icon?: string, when?: string, transparent?: boolean): Submenu & MutableCompoundMenuNode
89+
orderString?: string, icon?: string, when?: string): Submenu & MutableCompoundMenuNode
9090
createSubmenuLink(delegate: Submenu, sortString?: string, when?: string, argumentAdapter?: (...args: unknown[]) => unknown[]): MenuNode;
9191
}
9292

93+
export interface LinkedSubmenuOptions {
94+
newParentPath: MenuPath;
95+
submenuPath: MenuPath;
96+
order?: string;
97+
when?: string;
98+
argumentAdapter?: (...args: unknown[]) => unknown[];
99+
}
100+
93101
/**
94102
* The MenuModelRegistry allows to register and unregister menus, submenus and actions
95103
* via strings and {@link MenuAction}s without the need to access the underlying UI
@@ -176,14 +184,14 @@ export class MenuModelRegistry {
176184
* will be returned.
177185
*/
178186
registerSubmenu(menuPath: MenuPath, label: string,
179-
options: { sortString?: string, icon?: string, when?: string, contextKeyOverlay?: Record<string, string>, transparent?: boolean } = {}): Disposable {
180-
const { contextKeyOverlay, sortString, icon, when, transparent } = options;
187+
options: { sortString?: string, icon?: string, when?: string, contextKeyOverlay?: Record<string, string> } = {}): Disposable {
188+
const { contextKeyOverlay, sortString, icon, when } = options;
181189

182190
const parent = this.root.getOrCreate(menuPath, 0, menuPath.length - 1);
183191
const existing = parent.children.find(node => node.id === menuPath[menuPath.length - 1]);
184192
if (Group.is(existing)) {
185193
parent.removeNode(existing);
186-
const newMenu = this.menuNodeFactory.createSubmenu(menuPath[menuPath.length - 1], label, contextKeyOverlay, sortString, icon, when, transparent);
194+
const newMenu = this.menuNodeFactory.createSubmenu(menuPath[menuPath.length - 1], label, contextKeyOverlay, sortString, icon, when);
187195
newMenu.addNode(...existing.children);
188196
parent.addNode(newMenu);
189197
this.fireChangeEvent({
@@ -199,7 +207,7 @@ export class MenuModelRegistry {
199207
});
200208
});
201209
} else {
202-
const newMenu = this.menuNodeFactory.createSubmenu(menuPath[menuPath.length - 1], label, contextKeyOverlay, sortString, icon, when, transparent);
210+
const newMenu = this.menuNodeFactory.createSubmenu(menuPath[menuPath.length - 1], label, contextKeyOverlay, sortString, icon, when);
203211
parent.addNode(newMenu);
204212
this.fireChangeEvent({
205213
kind: ChangeKind.ADDED,
@@ -217,7 +225,7 @@ export class MenuModelRegistry {
217225
}
218226
}
219227

220-
linkCompoundMenuNode(params: { newParentPath: MenuPath, submenuPath: MenuPath, order?: string, when?: string, argumentAdapter?: (...args: unknown[]) => unknown[] }): Disposable {
228+
linkCompoundMenuNode(params: LinkedSubmenuOptions): Disposable {
221229
const { newParentPath, submenuPath, order, when, argumentAdapter } = params;
222230
// add a wrapper here
223231
let i = 0;

packages/core/src/common/menu/menu-types.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,7 @@ export type Submenu = CompoundMenuNode & RenderedMenuNode;
122122
export interface CompoundMenuNode extends MenuNode {
123123
children: MenuNode[];
124124
contextKeyOverlays?: Record<string, string>;
125-
/** If true, the menu node's children should be rendered as though they were direct children of the submenu's parent. */
126-
transparent?: boolean;
125+
127126
/**
128127
* Whether the group or submenu contains any visible children
129128
*
@@ -148,19 +147,6 @@ export namespace CompoundMenuNode {
148147
return m1.sortString.localeCompare(m2.sortString);
149148
}
150149

151-
const _flatten = (acc: MenuNode[], curr: MenuNode): MenuNode[] => {
152-
if (is(curr) && curr.transparent) {
153-
acc.push(...curr.children.reduce(_flatten, acc))
154-
} else {
155-
acc.push(curr);
156-
}
157-
return acc;
158-
}
159-
160-
export function flatten(parent: CompoundMenuNode): MenuNode[] {
161-
return parent.children.reduce(_flatten, []).sort(sortChildren);
162-
}
163-
164150
/**
165151
* Indicates whether the given node is the special `navigation` menu.
166152
*

packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts

Lines changed: 54 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,24 @@
1717
/* eslint-disable @typescript-eslint/no-explicit-any */
1818

1919
import { inject, injectable, optional } from '@theia/core/shared/inversify';
20-
import { CommandRegistry, Disposable, DisposableCollection, nls, CommandMenu, AcceleratorSource, ContextExpressionMatcher } from '@theia/core';
20+
import { MenuPath, CommandRegistry, Disposable, DisposableCollection, nls, CommandMenu, AcceleratorSource, ContextExpressionMatcher } from '@theia/core';
2121
import { MenuModelRegistry } from '@theia/core/lib/common';
2222
import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
2323
import { DeployedPlugin, IconUrl, Menu } from '../../../common';
2424
import { ScmWidget } from '@theia/scm/lib/browser/scm-widget';
2525
import { KeybindingRegistry, QuickCommandService } from '@theia/core/lib/browser';
2626
import {
27-
CodeEditorWidgetUtil, PLUGIN_EDITOR_TITLE_MENU, PLUGIN_EDITOR_TITLE_RUN_MENU, PLUGIN_SCM_TITLE_MENU, PLUGIN_VIEW_TITLE_MENU
27+
CodeEditorWidgetUtil, codeToTheiaMappings, PLUGIN_EDITOR_TITLE_MENU, PLUGIN_EDITOR_TITLE_RUN_MENU, PLUGIN_SCM_TITLE_MENU, PLUGIN_VIEW_TITLE_MENU
2828
} from './vscode-theia-menu-mappings';
2929
import { PluginMenuCommandAdapter } from './plugin-menu-command-adapter';
3030
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
3131
import { PluginSharedStyle } from '../plugin-shared-style';
3232
import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables';
3333

34+
function identity(...args: unknown[]): unknown[] {
35+
return args;
36+
}
37+
3438
@injectable()
3539
export class MenusContributionPointHandler {
3640

@@ -63,6 +67,9 @@ export class MenusContributionPointHandler {
6367
this.tabBarToolbar.registerMenuDelegate(PLUGIN_VIEW_TITLE_MENU, widget => !CodeEditorWidgetUtil.is(widget));
6468
}
6569

70+
private getMatchingTheiaMenuPaths(contributionPoint: string): MenuPath[] | undefined {
71+
return (codeToTheiaMappings as Map<string, MenuPath[]>).get(contributionPoint);
72+
}
6673

6774
handle(plugin: DeployedPlugin): Disposable {
6875
const allMenus = plugin.contributes?.menus;
@@ -85,61 +92,66 @@ export class MenusContributionPointHandler {
8592
if (contributionPoint === 'commandPalette') {
8693
toDispose.push(this.registerCommandPaletteAction(item));
8794
} else {
88-
const target = [this.pluginMenuCommandAdapter.toProbablyUniquePath(contributionPoint)];
95+
const targets = this.getMatchingTheiaMenuPaths(contributionPoint) ?? [[contributionPoint]];
8996
const { group, order } = this.parseGroup(item.group);
9097
const { submenu, command } = item;
9198
if (submenu && command) {
9299
console.warn(
93100
`Menu item ${command} from plugin ${plugin.metadata.model.id} contributed both submenu and command. Only command will be registered.`
94101
);
95102
}
103+
const contributionPointAdapter = this.pluginMenuCommandAdapter.getArgumentAdapter(contributionPoint);
96104
if (command) {
97-
const menuPath = group ? [...target, group] : target;
98-
99-
const cmd = this.commandRegistry.getCommand(command);
100-
if (!cmd) {
101-
console.debug(`No label for action menu node: No command "${command}" exists.`);
102-
continue;
103-
}
104-
const label = cmd.label || cmd.id;
105-
const icon = cmd.iconClass;
106-
const action: CommandMenu & AcceleratorSource = {
107-
id: command,
108-
sortString: order || '',
109-
isVisible: <T>(contextMatcher: ContextExpressionMatcher<T>, context: T | undefined, ...args: any[]): boolean => {
110-
if (item.when && !contextMatcher.match(item.when, context)) {
111-
return false;
112-
}
105+
const actionAdapter = contributionPointAdapter ?? identity;
106+
targets.forEach(target => {
107+
const menuPath = group ? [...target, group] : target;
113108

114-
return this.commandRegistry.isVisible(command, ...args);
115-
},
116-
icon: icon,
117-
label: label,
118-
isEnabled: (...args: any[]): boolean =>
119-
this.commandRegistry.isEnabled(command, ...args),
120-
run: (...args: any[]): Promise<void> =>
121-
this.commandRegistry.executeCommand(command, ...args),
122-
isToggled: () => false,
123-
getAccelerator: (context: HTMLElement | undefined): string[] => {
124-
const bindings = this.keybindingRegistry.getKeybindingsForCommand(command);
125-
// Only consider the first active keybinding.
126-
if (bindings.length) {
127-
const binding = bindings.find(b => this.keybindingRegistry.isEnabledInScope(b, context));
128-
if (binding) {
129-
return this.keybindingRegistry.acceleratorFor(binding, '+', true);
109+
const cmd = this.commandRegistry.getCommand(command);
110+
if (!cmd) {
111+
console.debug(`No label for action menu node: No command "${command}" exists.`);
112+
return;
113+
}
114+
const label = cmd.label || cmd.id;
115+
const icon = cmd.iconClass;
116+
const action: CommandMenu & AcceleratorSource = {
117+
id: command,
118+
sortString: order || '',
119+
isVisible: <T>(contextMatcher: ContextExpressionMatcher<T>, context: T | undefined, ...args: any[]): boolean => {
120+
if (item.when && !contextMatcher.match(item.when, context)) {
121+
return false;
122+
}
123+
124+
return this.commandRegistry.isVisible(command, ...actionAdapter(...args));
125+
},
126+
icon: icon,
127+
label: label,
128+
isEnabled: (...args: any[]): boolean =>
129+
this.commandRegistry.isEnabled(command, ...actionAdapter(...args)),
130+
run: (...args: any[]): Promise<void> =>
131+
this.commandRegistry.executeCommand(command, ...actionAdapter(...args)),
132+
isToggled: () => false,
133+
getAccelerator: (context: HTMLElement | undefined): string[] => {
134+
const bindings = this.keybindingRegistry.getKeybindingsForCommand(command);
135+
// Only consider the first active keybinding.
136+
if (bindings.length) {
137+
const binding = bindings.find(b => this.keybindingRegistry.isEnabledInScope(b, context));
138+
if (binding) {
139+
return this.keybindingRegistry.acceleratorFor(binding, '+', true);
140+
}
130141
}
142+
return [];
131143
}
132-
return [];
133-
}
134-
};
135-
toDispose.push(this.menuRegistry.registerCommandMenu(menuPath, action));
144+
};
145+
toDispose.push(this.menuRegistry.registerCommandMenu(menuPath, action));
146+
});
136147
} else if (submenu) {
137-
toDispose.push(this.menuRegistry.linkCompoundMenuNode({
148+
targets.forEach(target => toDispose.push(this.menuRegistry.linkCompoundMenuNode({
138149
newParentPath: group ? [...target, group] : target,
139-
submenuPath: [submenu],
150+
submenuPath: [submenu!],
140151
order: order,
141152
when: item.when,
142-
}));
153+
argumentAdapter: contributionPointAdapter
154+
})));
143155
}
144156
}
145157
} catch (error) {

0 commit comments

Comments
 (0)