Skip to content

Commit

Permalink
Added support for menu item placeholders.
Browse files Browse the repository at this point in the history
Placeholders are always visible, alway disabled menu nodes.
One can use them to "add a label" to menu groups.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
  • Loading branch information
Akos Kitta committed Aug 19, 2020
1 parent 9d2dcd0 commit 366972b
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 66 deletions.
21 changes: 11 additions & 10 deletions examples/api-samples/src/browser/menu/sample-menu-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,28 +46,29 @@ export class SampleCommandContribution implements CommandContribution {
@injectable()
export class SampleMenuContribution implements MenuContribution {
registerMenus(menus: MenuModelRegistry): void {
const subMenuPath = [...MAIN_MENU_BAR, 'sample-menu'];
menus.registerSubmenu(subMenuPath, 'Sample Menu', {
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.registerMenuAction(subMenuPath, {
menus.registerMenuAction(submenuPath, {
commandId: SampleCommand.id,
order: '0'
});
menus.registerMenuAction(subMenuPath, {
menus.registerMenuAction(submenuPath, {
commandId: SampleCommand2.id,
order: '2'
});
const subSubMenuPath = [...subMenuPath, 'sample-sub-menu'];
menus.registerSubmenu(subSubMenuPath, 'Sample sub menu', { order: '1' });
menus.registerMenuAction(subSubMenuPath, {
const subSubmenuPath = [...submenuPath, 'sample-submenu'];
menus.registerSubmenu(subSubmenuPath, 'Sample submenu', { order: '2' });
menus.registerMenuAction(subSubmenuPath, {
commandId: SampleCommand.id,
order: '0'
order: '1'
});
menus.registerMenuAction(subSubMenuPath, {
menus.registerMenuAction(subSubmenuPath, {
commandId: SampleCommand2.id,
order: '2'
order: '3'
});
menus.registerMenuPlaceholder(subSubmenuPath, 'Placeholder', { order: '0' });
}
}

Expand Down
130 changes: 80 additions & 50 deletions packages/core/src/browser/menu/browser-menu-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { MenuBar, Menu as MenuWidget, Widget } from '@phosphor/widgets';
import { CommandRegistry as PhosphorCommandRegistry } from '@phosphor/commands';
import {
CommandRegistry, ActionMenuNode, CompositeMenuNode,
MenuModelRegistry, MAIN_MENU_BAR, MenuPath, DisposableCollection, Disposable
MenuModelRegistry, MAIN_MENU_BAR, MenuPath, DisposableCollection, Disposable, PlaceholderMenuNode
} from '../../common';
import { KeybindingRegistry } from '../keybinding';
import { FrontendApplicationContribution, FrontendApplication } from '../frontend-application';
Expand Down Expand Up @@ -92,10 +92,12 @@ export class BrowserMainMenuFactory {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected registerMenu(menuCommandRegistry: MenuCommandRegistry, menu: CompositeMenuNode, args: any[]): void {
for (const child of menu.children) {
if (child instanceof ActionMenuNode) {
menuCommandRegistry.registerActionMenu(child, args);
if (child instanceof PlaceholderMenuNode) {
menuCommandRegistry.registerMenuNode(child, args);
} else if (child instanceof ActionMenuNode) {
menuCommandRegistry.registerMenuNode(child, args);
if (child.altNode) {
menuCommandRegistry.registerActionMenu(child.altNode, args);
menuCommandRegistry.registerMenuNode(child.altNode, args);
}
} else if (child instanceof CompositeMenuNode) {
this.registerMenu(menuCommandRegistry, child, args);
Expand Down Expand Up @@ -268,6 +270,11 @@ class DynamicMenuWidget extends MenuWidget {
items.push(...submenu); // render children
}
}
} else if (item instanceof PlaceholderMenuNode) {
items.push({
command: item.id,
type: 'command'
});
} else if (item instanceof ActionMenuNode) {
const { context, contextKeyService } = this.services;
const node = item.altNode && context.altPressed ? item.altNode : item;
Expand Down Expand Up @@ -354,26 +361,34 @@ export class BrowserMenuBarContribution implements FrontendApplicationContributi
class MenuCommandRegistry extends PhosphorCommandRegistry {

// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected actions = new Map<string, [ActionMenuNode, any[]]>();
protected actions = new Map<string, [ActionMenuNode | PlaceholderMenuNode, any[]]>();
protected toDispose = new DisposableCollection();

constructor(protected services: MenuServices) {
super();
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
registerActionMenu(menu: ActionMenuNode, args: any[]): void {
const { commandId } = menu.action;
const { commandRegistry } = this.services;
const command = commandRegistry.getCommand(commandId);
if (!command) {
return;
}
const { id } = command;
if (this.actions.has(id)) {
return;
registerMenuNode(menu: ActionMenuNode | PlaceholderMenuNode, args: any[]): void {
if (menu instanceof PlaceholderMenuNode) {
const { id } = menu;
if (this.actions.has(id)) {
return;
}
this.actions.set(id, [menu, []]);
} else {
const { commandId } = menu.action;
const { commandRegistry } = this.services;
const command = commandRegistry.getCommand(commandId);
if (!command) {
return;
}
const { id } = command;
if (this.actions.has(id)) {
return;
}
this.actions.set(id, [menu, args]);
}
this.actions.set(id, [menu, args]);
}

snapshot(): this {
Expand All @@ -385,43 +400,58 @@ class MenuCommandRegistry extends PhosphorCommandRegistry {
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected registerCommand(menu: ActionMenuNode, args: any[]): Disposable {
const { commandRegistry, keybindingRegistry } = this.services;
const command = commandRegistry.getCommand(menu.action.commandId);
if (!command) {
return Disposable.NULL;
}
const { id } = command;
if (this.hasCommand(id)) {
// several menu items can be registered for the same command in different contexts
return Disposable.NULL;
}

// We freeze the `isEnabled`, `isVisible`, and `isToggled` states so they won't change.
const enabled = commandRegistry.isEnabled(id, ...args);
const visible = commandRegistry.isVisible(id, ...args);
const toggled = commandRegistry.isToggled(id, ...args);
const unregisterCommand = this.addCommand(id, {
execute: () => commandRegistry.executeCommand(id, ...args),
label: menu.label,
icon: menu.icon,
isEnabled: () => enabled,
isVisible: () => visible,
isToggled: () => toggled
});
protected registerCommand(menu: ActionMenuNode | PlaceholderMenuNode, args: any[]): Disposable {
if (menu instanceof PlaceholderMenuNode) {
const { id } = menu;
if (this.hasCommand(id)) {
return Disposable.NULL;
}
const unregisterCommand = this.addCommand(id, {
execute: () => { /* NOOP */ },
label: menu.label,
icon: menu.icon,
isEnabled: () => false,
isVisible: () => true
});
return Disposable.create(() => unregisterCommand.dispose());
} else {
const { commandRegistry, keybindingRegistry } = this.services;
const command = commandRegistry.getCommand(menu.action.commandId);
if (!command) {
return Disposable.NULL;
}
const { id } = command;
if (this.hasCommand(id)) {
// several menu items can be registered for the same command in different contexts
return Disposable.NULL;
}

const bindings = keybindingRegistry.getKeybindingsForCommand(id);
// Only consider the first keybinding.
if (bindings.length) {
const binding = bindings[0];
const keys = keybindingRegistry.acceleratorFor(binding);
this.addKeyBinding({
command: id,
keys,
selector: '.p-Widget' // We have the PhosphorJS dependency anyway.
// We freeze the `isEnabled`, `isVisible`, and `isToggled` states so they won't change.
const enabled = commandRegistry.isEnabled(id, ...args);
const visible = commandRegistry.isVisible(id, ...args);
const toggled = commandRegistry.isToggled(id, ...args);
const unregisterCommand = this.addCommand(id, {
execute: () => commandRegistry.executeCommand(id, ...args),
label: menu.label,
icon: menu.icon,
isEnabled: () => enabled,
isVisible: () => visible,
isToggled: () => toggled
});

const bindings = keybindingRegistry.getKeybindingsForCommand(id);
// Only consider the first keybinding.
if (bindings.length) {
const binding = bindings[0];
const keys = keybindingRegistry.acceleratorFor(binding);
this.addKeyBinding({
command: id,
keys,
selector: '.p-Widget' // We have the PhosphorJS dependency anyway.
});
}
return Disposable.create(() => unregisterCommand.dispose());
}
return Disposable.create(() => unregisterCommand.dispose());
}

}
37 changes: 33 additions & 4 deletions packages/core/src/common/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,21 @@ export class MenuModelRegistry {
}
}

registerMenuAction(menuPath: MenuPath, item: MenuAction): Disposable {
const parent = this.findGroup(menuPath);
registerMenuAction(menuPath: MenuPath, item: MenuAction, options?: SubMenuOptions): Disposable {
const parent = this.findGroup(menuPath, options);
const actionNode = new ActionMenuNode(item, this.commands);
return parent.addNode(actionNode);
}

registerMenuPlaceholder(menuPath: MenuPath, label: string, options?: SubMenuOptions): Disposable {
const parent = this.findGroup(menuPath, options);
const placeholderNode = new PlaceholderMenuNode(menuPath, label, options);
return parent.addNode(placeholderNode);
}

registerSubmenu(menuPath: MenuPath, label: string, options?: SubMenuOptions): Disposable {
if (menuPath.length === 0) {
throw new Error('The sub menu path cannot be empty.');
throw new Error('The submenu path cannot be empty.');
}
const index = menuPath.length - 1;
const menuId = menuPath[index];
Expand Down Expand Up @@ -262,6 +268,29 @@ export class CompositeMenuNode implements MenuNode {
}
}

/**
* Special menu node that is not backed by any commands and is always disabled.
*/
export class PlaceholderMenuNode implements MenuNode {

private static readonly UUID = 'd365a814-2c2d-4d29-a42c-c6842655785e'; // To avoid command ID collision.

constructor(protected readonly menuPath: MenuPath, public readonly label: string, protected options?: SubMenuOptions) { }

get icon(): string | undefined {
return this.options?.iconClass;
}

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

get id(): string {
return [PlaceholderMenuNode.UUID, ...this.menuPath, this.label].join('-');
}

}

export class ActionMenuNode implements MenuNode {

readonly altNode: ActionMenuNode | undefined;
Expand All @@ -285,7 +314,7 @@ export class ActionMenuNode implements MenuNode {
}
const cmd = this.commands.getCommand(this.action.commandId);
if (!cmd) {
throw new Error(`A command with id '${this.action.commandId}' does not exist.`);
throw new Error(`A command with ID '${this.action.commandId}' does not exist.`);
}
return cmd.label || cmd.id;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ import * as electron from 'electron';
import { inject, injectable } from 'inversify';
import {
CommandRegistry, isOSX, ActionMenuNode, CompositeMenuNode,
MAIN_MENU_BAR, MenuModelRegistry, MenuPath
MAIN_MENU_BAR, MenuModelRegistry, MenuPath, PlaceholderMenuNode
} from '../../common';
import { PreferenceService, KeybindingRegistry, Keybinding } from '../../browser';
import { Keybinding } from '../../common/keybinding';
import { PreferenceService, KeybindingRegistry } from '../../browser';
import { ContextKeyService } from '../../browser/context-key-service';
import debounce = require('lodash.debounce');
import { ContextMenuContext } from '../../browser/menu/context-menu-context';
Expand Down Expand Up @@ -176,6 +177,13 @@ export class ElectronMainMenuFactory {
if (this.commandRegistry.getToggledHandler(commandId, ...args)) {
this._toggledCommands.add(commandId);
}
} else if (menu instanceof PlaceholderMenuNode) {
items.push({
id: menu.id,
label: menu.label,
enabled: false,
visible: true
});
}
}
return items;
Expand Down

0 comments on commit 366972b

Please sign in to comment.