Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support dynamic menu contributions #13720

Merged
merged 2 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions examples/api-tests/src/menus.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ describe('Menus', function () {

const container = window.theia.container;
const shell = container.get(ApplicationShell);
/** @type {BrowserMenuBarContribution} */
const menuBarContribution = container.get(BrowserMenuBarContribution);
const menuBar = /** @type {import('@theia/core/lib/browser/menu/browser-menu-plugin').MenuBarWidget} */ (menuBarContribution.menuBar);
const pluginService = container.get(HostedPluginSupport);
const menus = container.get(MenuModelRegistry);
const commands = container.get(CommandRegistry);
Expand All @@ -54,6 +54,9 @@ describe('Menus', function () {
before(async function () {
await pluginService.didStart;
await pluginService.activateByViewContainer('explorer');
// Updating the menu interferes with our ability to programmatically test it
// We simply disable the menu updating
menus.isReady = false;
});

const toTearDown = new DisposableCollection();
Expand All @@ -73,7 +76,7 @@ describe('Menus', function () {
]) {
it(`should toggle '${contribution.viewLabel}' view`, async () => {
await contribution.closeView();
await menuBar.triggerMenuItem('View', contribution.viewLabel);
await menuBarContribution.menuBar.triggerMenuItem('View', contribution.viewLabel);
await shell.waitForActivation(contribution.viewId);
});
}
Expand Down
41 changes: 23 additions & 18 deletions packages/core/src/browser/menu/browser-menu-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,25 +71,30 @@ export class BrowserMainMenuFactory implements MenuWidgetFactory {
const menuBar = new DynamicMenuBarWidget();
menuBar.id = 'theia:menubar';
this.corePreferences.ready.then(() => {
this.showMenuBar(menuBar, this.corePreferences.get('window.menuBarVisibility', 'classic'));
});
const preferenceListener = this.corePreferences.onPreferenceChanged(preference => {
if (preference.preferenceName === 'window.menuBarVisibility') {
this.showMenuBar(menuBar, preference.newValue);
}
});
const keybindingListener = this.keybindingRegistry.onKeybindingsChanged(() => {
const preference = this.corePreferences['window.menuBarVisibility'];
this.showMenuBar(menuBar, preference);
});
menuBar.disposed.connect(() => {
preferenceListener.dispose();
keybindingListener.dispose();
this.showMenuBar(menuBar);
});
const disposable = new DisposableCollection(
this.corePreferences.onPreferenceChanged(change => {
if (change.preferenceName === 'window.menuBarVisibility') {
this.showMenuBar(menuBar, change.newValue);
}
}),
this.keybindingRegistry.onKeybindingsChanged(() => {
this.showMenuBar(menuBar);
}),
this.menuProvider.onDidChange(() => {
this.showMenuBar(menuBar);
})
);
menuBar.disposed.connect(() => disposable.dispose());
return menuBar;
}

protected showMenuBar(menuBar: DynamicMenuBarWidget, preference: string | undefined): void {
protected getMenuBarVisibility(): string {
return this.corePreferences.get('window.menuBarVisibility', 'classic');
}

protected showMenuBar(menuBar: DynamicMenuBarWidget, preference = this.getMenuBarVisibility()): void {
if (preference && ['classic', 'visible'].includes(preference)) {
menuBar.clearMenus();
this.fillMenuBar(menuBar);
Expand Down Expand Up @@ -187,13 +192,13 @@ export class DynamicMenuBarWidget extends MenuBarWidget {
this.openActiveMenu();
await waitForRevealed(menu);

const menuPath = [label];
const menuPath = [label, ...labels];

let current = menu;
for (const itemLabel of labels) {
const item = current.items.find(i => i.label === itemLabel);
if (!item || !item.submenu) {
throw new Error(`could not find '${label}' submenu in ${menuPath.map(l => "'" + l + "'").join(' -> ')} menu`);
throw new Error(`could not find '${itemLabel}' submenu in ${menuPath.map(l => "'" + l + "'").join(' -> ')} menu`);
}
current.activeItem = item;
current.triggerActiveItem();
Expand All @@ -211,7 +216,7 @@ export class DynamicMenuBarWidget extends MenuBarWidget {
const menu = await this.activateMenu(menuPath[0], ...menuPath.slice(1));
const item = menu.items.find(i => i.label === labels[labels.length - 1]);
if (!item) {
throw new Error(`could not find '${label}' item in ${menuPath.map(l => "'" + l + "'").join(' -> ')} menu`);
throw new Error(`could not find '${labels[labels.length - 1]}' item in ${menuPath.map(l => "'" + l + "'").join(' -> ')} menu`);
}
menu.activeItem = item;
menu.triggerActiveItem();
Expand Down
41 changes: 36 additions & 5 deletions packages/core/src/common/menu/menu-model-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { inject, injectable, named } from 'inversify';
import { Command, CommandRegistry } from '../command';
import { ContributionProvider } from '../contribution-provider';
import { Disposable } from '../disposable';
import { Emitter, Event } from '../event';
import { ActionMenuNode } from './action-menu-node';
import { CompositeMenuNode, CompositeMenuNodeWrapper } from './composite-menu-node';
import { CompoundMenuNode, MenuAction, MenuNode, MenuNodeMetadata, MenuPath, MutableCompoundMenuNode, SubMenuOptions } from './menu-types';
Expand Down Expand Up @@ -68,6 +69,14 @@ export class MenuModelRegistry {
protected readonly root = new CompositeMenuNode('');
protected readonly independentSubmenus = new Map<string, MutableCompoundMenuNode>();

protected readonly onDidChangeEmitter = new Emitter<void>();

get onDidChange(): Event<void> {
return this.onDidChangeEmitter.event;
}

protected isReady = false;

constructor(
@inject(ContributionProvider) @named(MenuContribution)
protected readonly contributions: ContributionProvider<MenuContribution>,
Expand All @@ -78,6 +87,7 @@ export class MenuModelRegistry {
for (const contrib of this.contributions.getContributions()) {
contrib.registerMenus(this);
}
this.isReady = true;
}

/**
Expand All @@ -97,7 +107,9 @@ export class MenuModelRegistry {
*/
registerMenuNode(menuPath: MenuPath | string, menuNode: MenuNode, group?: string): Disposable {
const parent = this.getMenuNode(menuPath, group);
return parent.addNode(menuNode);
const disposable = parent.addNode(menuNode);
this.fireChangeEvent();
return this.changeEventOnDispose(disposable);
}

getMenuNode(menuPath: MenuPath | string, group?: string): MutableCompoundMenuNode {
Expand Down Expand Up @@ -137,21 +149,23 @@ export class MenuModelRegistry {
const groupPath = index === 0 ? [] : menuPath.slice(0, index);
const parent = this.findGroup(groupPath, options);
let groupNode = this.findSubMenu(parent, menuId, options);
let disposable = Disposable.NULL;
if (!groupNode) {
groupNode = new CompositeMenuNode(menuId, label, options, parent);
return parent.addNode(groupNode);
disposable = this.changeEventOnDispose(parent.addNode(groupNode));
} else {
groupNode.updateOptions({ ...options, label });
return Disposable.NULL;
}
this.fireChangeEvent();
return disposable;
}

registerIndependentSubmenu(id: string, label: string, options?: SubMenuOptions): Disposable {
if (this.independentSubmenus.has(id)) {
console.debug(`Independent submenu with path ${id} registered, but given ID already exists.`);
}
this.independentSubmenus.set(id, new CompositeMenuNode(id, label, options));
return { dispose: () => this.independentSubmenus.delete(id) };
return this.changeEventOnDispose(Disposable.create(() => this.independentSubmenus.delete(id)));
}

linkSubmenu(parentPath: MenuPath | string, childId: string | MenuPath, options?: SubMenuOptions, group?: string): Disposable {
Expand All @@ -175,7 +189,9 @@ export class MenuModelRegistry {
}

const wrapper = new CompositeMenuNodeWrapper(child, parent, options);
return parent.addNode(wrapper);
const disposable = parent.addNode(wrapper);
this.fireChangeEvent();
return this.changeEventOnDispose(disposable);
}

/**
Expand Down Expand Up @@ -207,6 +223,7 @@ export class MenuModelRegistry {
if (menuPath) {
const parent = this.findGroup(menuPath);
parent.removeNode(id);
this.fireChangeEvent();
return;
}

Expand All @@ -228,6 +245,7 @@ export class MenuModelRegistry {
});
};
recurse(this.root);
this.fireChangeEvent();
}

/**
Expand Down Expand Up @@ -321,6 +339,19 @@ export class MenuModelRegistry {
return true;
}

protected changeEventOnDispose(disposable: Disposable): Disposable {
return Disposable.create(() => {
disposable.dispose();
this.fireChangeEvent();
});
}

protected fireChangeEvent(): void {
if (this.isReady) {
this.onDidChangeEmitter.fire();
}
}

/**
* Returns the {@link MenuPath path} at which a given menu node can be accessed from this registry, if it can be determined.
* Returns `undefined` if the `parent` of any node in the chain is unknown.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory {
this.keybindingRegistry.onKeybindingsChanged(() => {
this.setMenuBar();
});
this.menuProvider.onDidChange(() => {
this.setMenuBar();
});
}

async setMenuBar(): Promise<void> {
Expand Down
Loading