ATL-938: Added menu group categories.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
Akos Kitta 2021-02-10 16:41:20 +01:00 committed by Akos Kitta
parent 3e92567d52
commit ba8885c8c8
7 changed files with 189 additions and 66 deletions

View File

@ -2,12 +2,13 @@ import { inject, injectable } from 'inversify';
import { remote } from 'electron'; import { remote } from 'electron';
import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable'; import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable';
import { firstToUpperCase } from '../../common/utils';
import { BoardsConfig } from '../boards/boards-config'; import { BoardsConfig } from '../boards/boards-config';
import { MainMenuManager } from '../../common/main-menu-manager'; import { MainMenuManager } from '../../common/main-menu-manager';
import { BoardsListWidget } from '../boards/boards-list-widget'; import { BoardsListWidget } from '../boards/boards-list-widget';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { ArduinoMenus, unregisterSubmenu } from '../menu/arduino-menus'; import { ArduinoMenus, PlaceholderMenuNode, unregisterSubmenu } from '../menu/arduino-menus';
import { BoardsService, InstalledBoardWithPackage, AvailablePorts, Port } from '../../common/protocol'; import { BoardsService, InstalledBoardWithPackage, AvailablePorts, Port } from '../../common/protocol';
import { SketchContribution, Command, CommandRegistry } from './contribution'; import { SketchContribution, Command, CommandRegistry } from './contribution';
@ -150,39 +151,61 @@ PID: ${PID}`;
} }
// Installed ports // Installed ports
for (const address of Object.keys(availablePorts)) { const registerPorts = (ports: AvailablePorts) => {
if (!!availablePorts[address]) { const addresses = Object.keys(ports);
const [port, boards] = availablePorts[address]; if (!addresses.length) {
if (!boards.length) { return;
boards.push({ }
name: ''
}); // Register placeholder for protocol
} const [port] = ports[addresses[0]];
for (const { name, fqbn } of boards) { const protocol = port.protocol;
const id = `arduino-select-port--${address}${fqbn ? `--${fqbn}` : ''}`; const menuPath = [...portsSubmenuPath, protocol];
const command = { id }; const placeholder = new PlaceholderMenuNode(menuPath, `${firstToUpperCase(port.protocol)} ports`);
const handler = { this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
execute: () => { this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.menuModelRegistry.unregisterMenuNode(placeholder.id)));
if (!Port.equals(port, this.boardsServiceProvider.boardsConfig.selectedPort)) {
this.boardsServiceProvider.boardsConfig = { for (const address of addresses) {
selectedBoard: this.boardsServiceProvider.boardsConfig.selectedBoard, if (!!ports[address]) {
selectedPort: port const [port, boards] = ports[address];
if (!boards.length) {
boards.push({
name: ''
});
}
for (const { name, fqbn } of boards) {
const id = `arduino-select-port--${address}${fqbn ? `--${fqbn}` : ''}`;
const command = { id };
const handler = {
execute: () => {
if (!Port.equals(port, this.boardsServiceProvider.boardsConfig.selectedPort)) {
this.boardsServiceProvider.boardsConfig = {
selectedBoard: this.boardsServiceProvider.boardsConfig.selectedBoard,
selectedPort: port
}
} }
} },
}, isToggled: () => Port.equals(port, this.boardsServiceProvider.boardsConfig.selectedPort)
isToggled: () => Port.equals(port, this.boardsServiceProvider.boardsConfig.selectedPort) };
}; const label = `${address}${name ? ` (${name})` : ''}`;
const menuAction = { const menuAction = {
commandId: id, commandId: id,
label: `${address}${name ? ` (${name})` : ''}` label,
}; order: `1${label}` // `1` comes after the placeholder which has order `0`
this.commandRegistry.registerCommand(command, handler); };
this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.commandRegistry.unregisterCommand(command))); this.commandRegistry.registerCommand(command, handler);
this.menuModelRegistry.registerMenuAction(portsSubmenuPath, menuAction); this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.commandRegistry.unregisterCommand(command)));
this.menuModelRegistry.registerMenuAction(menuPath, menuAction);
}
} }
} }
} }
const { serial, network, unknown } = AvailablePorts.groupByProtocol(availablePorts);
registerPorts(serial);
registerPorts(network);
registerPorts(unknown);
this.mainMenuManager.update(); this.mainMenuManager.update();
} }

View File

@ -3,12 +3,13 @@ import { inject, injectable, postConstruct } from 'inversify';
import { MenuPath, CompositeMenuNode } from '@theia/core/lib/common/menu'; import { MenuPath, CompositeMenuNode } from '@theia/core/lib/common/menu';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { OpenSketch } from './open-sketch'; import { OpenSketch } from './open-sketch';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { MainMenuManager } from '../../common/main-menu-manager'; import { MainMenuManager } from '../../common/main-menu-manager';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { ExamplesService, ExampleContainer } from '../../common/protocol/examples-service'; import { ExamplesService, ExampleContainer } from '../../common/protocol/examples-service';
import { SketchContribution, CommandRegistry, MenuModelRegistry } from './contribution'; import { SketchContribution, CommandRegistry, MenuModelRegistry } from './contribution';
import { NotificationCenter } from '../notification-center'; import { NotificationCenter } from '../notification-center';
import { Board } from '../../common/protocol';
@injectable() @injectable()
export abstract class Examples extends SketchContribution { export abstract class Examples extends SketchContribution {
@ -32,10 +33,10 @@ export abstract class Examples extends SketchContribution {
@postConstruct() @postConstruct()
init(): void { init(): void {
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.handleBoardChanged(selectedBoard?.fqbn)); this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.handleBoardChanged(selectedBoard));
} }
protected handleBoardChanged(fqbn: string | undefined): void { protected handleBoardChanged(board: Board | undefined): void {
// NOOP // NOOP
} }
@ -58,27 +59,33 @@ export abstract class Examples extends SketchContribution {
} }
registerRecursively( registerRecursively(
exampleContainer: ExampleContainer, exampleContainerOrPlaceholder: ExampleContainer | string,
menuPath: MenuPath, menuPath: MenuPath,
pushToDispose: DisposableCollection = new DisposableCollection()): void { pushToDispose: DisposableCollection = new DisposableCollection()): void {
const { label, sketches, children } = exampleContainer; if (typeof exampleContainerOrPlaceholder === 'string') {
const submenuPath = [...menuPath, label]; const placeholder = new PlaceholderMenuNode(menuPath, exampleContainerOrPlaceholder);
this.menuRegistry.registerSubmenu(submenuPath, label); this.menuRegistry.registerMenuNode(menuPath, placeholder);
children.forEach(child => this.registerRecursively(child, submenuPath, pushToDispose)); pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id)));
for (const sketch of sketches) { } else {
const { uri } = sketch; const { label, sketches, children } = exampleContainerOrPlaceholder;
const commandId = `arduino-open-example-${submenuPath.join(':')}--${uri}`; const submenuPath = [...menuPath, label];
const command = { id: commandId }; this.menuRegistry.registerSubmenu(submenuPath, label);
const handler = { children.forEach(child => this.registerRecursively(child, submenuPath, pushToDispose));
execute: async () => { for (const sketch of sketches) {
const sketch = await this.sketchService.cloneExample(uri); const { uri } = sketch;
this.commandService.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch); const commandId = `arduino-open-example-${submenuPath.join(':')}--${uri}`;
} const command = { id: commandId };
}; const handler = {
pushToDispose.push(this.commandRegistry.registerCommand(command, handler)); execute: async () => {
this.menuRegistry.registerMenuAction(submenuPath, { commandId, label: sketch.name }); const sketch = await this.sketchService.cloneExample(uri);
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command))); this.commandService.executeCommand(OpenSketch.Commands.OPEN_SKETCH.id, sketch);
}
};
pushToDispose.push(this.commandRegistry.registerCommand(command, handler));
this.menuRegistry.registerMenuAction(submenuPath, { commandId, label: sketch.name });
pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuAction(command)));
}
} }
} }
@ -101,10 +108,12 @@ export class BuiltInExamples extends Examples {
return; return;
} }
this.toDispose.dispose(); this.toDispose.dispose();
for (const container of exampleContainers) { for (const container of ['Built-in examples', ...exampleContainers]) {
this.registerRecursively(container, ArduinoMenus.EXAMPLES__BUILT_IN_GROUP, this.toDispose); this.registerRecursively(container, ArduinoMenus.EXAMPLES__BUILT_IN_GROUP, this.toDispose);
} }
this.menuManager.update(); this.menuManager.update();
// TODO: remove
console.log(typeof this.menuRegistry);
} }
} }
@ -123,17 +132,27 @@ export class LibraryExamples extends Examples {
this.notificationCenter.onLibraryUninstalled(() => this.register()); this.notificationCenter.onLibraryUninstalled(() => this.register());
} }
protected handleBoardChanged(fqbn: string | undefined): void { protected handleBoardChanged(board: Board | undefined): void {
this.register(fqbn); this.register(board);
} }
protected async register(fqbn: string | undefined = this.boardsServiceClient.boardsConfig.selectedBoard?.fqbn) { protected async register(board: Board | undefined = this.boardsServiceClient.boardsConfig.selectedBoard) {
return this.queue.add(async () => { return this.queue.add(async () => {
this.toDispose.dispose(); this.toDispose.dispose();
if (!fqbn) { if (!board || !board.fqbn) {
return; return;
} }
const { fqbn, name } = board;
const { user, current, any } = await this.examplesService.installed({ fqbn }); const { user, current, any } = await this.examplesService.installed({ fqbn });
if (user.length) {
(user as any).unshift('Examples from Custom Libraries');
}
if (current.length) {
(current as any).unshift(`Examples for ${name}`);
}
if (any.length) {
(any as any).unshift('Examples for any board');
}
for (const container of user) { for (const container of user) {
this.registerRecursively(container, ArduinoMenus.EXAMPLES__USER_LIBS_GROUP, this.toDispose); this.registerRecursively(container, ArduinoMenus.EXAMPLES__USER_LIBS_GROUP, this.toDispose);
} }

View File

@ -5,8 +5,8 @@ import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { EditorManager } from '@theia/editor/lib/browser'; import { EditorManager } from '@theia/editor/lib/browser';
import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu'; import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
import { LibraryPackage, LibraryLocation, LibraryService } from '../../common/protocol'; import { LibraryPackage, LibraryService } from '../../common/protocol';
import { MainMenuManager } from '../../common/main-menu-manager'; import { MainMenuManager } from '../../common/main-menu-manager';
import { LibraryListWidget } from '../library/library-list-widget'; import { LibraryListWidget } from '../library/library-list-widget';
import { BoardsServiceProvider } from '../boards/boards-service-provider'; import { BoardsServiceProvider } from '../boards/boards-service-provider';
@ -84,19 +84,35 @@ export class IncludeLibrary extends SketchContribution {
// `Arduino libraries` // `Arduino libraries`
const packageMenuPath = [...includeLibMenuPath, '2_arduino']; const packageMenuPath = [...includeLibMenuPath, '2_arduino'];
const userMenuPath = [...includeLibMenuPath, '3_contributed']; const userMenuPath = [...includeLibMenuPath, '3_contributed'];
for (const library of libraries) { const { user, rest } = LibraryPackage.groupByLocation(libraries);
this.toDispose.push(this.registerLibrary(library, library.location === LibraryLocation.USER ? userMenuPath : packageMenuPath)); if (rest.length) {
(rest as any).unshift('Arduino libraries');
}
if (user.length) {
(user as any).unshift('Contributed libraries');
}
for (const library of user) {
this.toDispose.push(this.registerLibrary(library, userMenuPath));
}
for (const library of rest) {
this.toDispose.push(this.registerLibrary(library, packageMenuPath));
} }
this.mainMenuManager.update(); this.mainMenuManager.update();
}); });
} }
protected registerLibrary(library: LibraryPackage, menuPath: MenuPath): Disposable { protected registerLibrary(libraryOrPlaceholder: LibraryPackage | string, menuPath: MenuPath): Disposable {
const commandId = `arduino-include-library--${library.name}:${library.author}`; if (typeof libraryOrPlaceholder === 'string') {
const placeholder = new PlaceholderMenuNode(menuPath, libraryOrPlaceholder);
this.menuRegistry.registerMenuNode(menuPath, placeholder);
return Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id));
}
const commandId = `arduino-include-library--${libraryOrPlaceholder.name}:${libraryOrPlaceholder.author}`;
const command = { id: commandId }; const command = { id: commandId };
const handler = { execute: () => this.commandRegistry.executeCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY.id, library) }; const handler = { execute: () => this.commandRegistry.executeCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY.id, libraryOrPlaceholder) };
const menuAction = { commandId, label: library.name }; const menuAction = { commandId, label: libraryOrPlaceholder.name };
this.menuRegistry.registerMenuAction(menuPath, menuAction); this.menuRegistry.registerMenuAction(menuPath, menuAction);
return new DisposableCollection( return new DisposableCollection(
this.commandRegistry.registerCommand(command, handler), this.commandRegistry.registerCommand(command, handler),

View File

@ -1,6 +1,6 @@
import { isOSX } from '@theia/core/lib/common/os'; import { isOSX } from '@theia/core/lib/common/os';
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution'; import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { MAIN_MENU_BAR, MenuModelRegistry, MenuNode } from '@theia/core/lib/common/menu'; import { MAIN_MENU_BAR, MenuModelRegistry, MenuNode, MenuPath, SubMenuOptions } from '@theia/core/lib/common/menu';
export namespace ArduinoMenus { export namespace ArduinoMenus {
@ -99,3 +99,24 @@ export function unregisterSubmenu(menuPath: string[], menuRegistry: MenuModelReg
} }
(parent.children as Array<MenuNode>).splice(index, 1); (parent.children as Array<MenuNode>).splice(index, 1);
} }
/**
* Special menu node that is not backed by any commands and is always disabled.
*/
export class PlaceholderMenuNode implements MenuNode {
constructor(protected readonly menuPath: MenuPath, readonly label: string, protected options: SubMenuOptions = { order: '0' }) { }
get icon(): string | undefined {
return this.options?.iconClass;
}
get sortString(): string {
return this.options?.order || this.label;
}
get id(): string {
return [...this.menuPath, 'placeholder'].join('-');
}
}

View File

@ -5,6 +5,25 @@ import { Installable } from './installable';
import { ArduinoComponent } from './arduino-component'; import { ArduinoComponent } from './arduino-component';
export type AvailablePorts = Record<string, [Port, Array<Board>]>; export type AvailablePorts = Record<string, [Port, Array<Board>]>;
export namespace AvailablePorts {
export function groupByProtocol(availablePorts: AvailablePorts): { serial: AvailablePorts, network: AvailablePorts, unknown: AvailablePorts } {
const serial: AvailablePorts = {};
const network: AvailablePorts = {};
const unknown: AvailablePorts = {};
for (const key of Object.keys(availablePorts)) {
const [port, boards] = availablePorts[key];
const { protocol } = port;
if (protocol === 'serial') {
serial[key] = [port, boards];
} else if (protocol === 'network') {
network[key] = [port, boards];
} else {
unknown[key] = [port, boards];
}
}
return { serial, network, unknown };
}
}
export interface AttachedBoardsChangeEvent { export interface AttachedBoardsChangeEvent {
readonly oldState: Readonly<{ boards: Board[], ports: Port[] }>; readonly oldState: Readonly<{ boards: Board[], ports: Port[] }>;

View File

@ -69,4 +69,17 @@ export namespace LibraryPackage {
return left.name === right.name && left.author === right.author; return left.name === right.name && left.author === right.author;
} }
export function groupByLocation(packages: LibraryPackage[]): { user: LibraryPackage[], rest: LibraryPackage[] } {
const user: LibraryPackage[] = [];
const rest: LibraryPackage[] = [];
for (const pkg of packages) {
if (pkg.location === LibraryLocation.USER) {
user.push(pkg);
} else {
rest.push(pkg);
}
}
return { user, rest };
}
} }

View File

@ -1,8 +1,9 @@
import { injectable } from 'inversify' import { injectable } from 'inversify'
import { remote } from 'electron'; import { remote } from 'electron';
import { Keybinding } from '@theia/core/lib/common/keybinding'; import { Keybinding } from '@theia/core/lib/common/keybinding';
import { ElectronMainMenuFactory as TheiaElectronMainMenuFactory } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory'; import { CompositeMenuNode } from '@theia/core/lib/common/menu';
import { ArduinoMenus } from '../../../browser/menu/arduino-menus'; import { ElectronMainMenuFactory as TheiaElectronMainMenuFactory, ElectronMenuOptions } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory';
import { ArduinoMenus, PlaceholderMenuNode } from '../../../browser/menu/arduino-menus';
@injectable() @injectable()
export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory { export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
@ -42,4 +43,15 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
return { label, submenu }; return { label, submenu };
} }
protected handleDefault(menuNode: CompositeMenuNode, args: any[] = [], options?: ElectronMenuOptions): Electron.MenuItemConstructorOptions[] {
if (menuNode instanceof PlaceholderMenuNode) {
return [{
label: menuNode.label,
enabled: false,
visible: true
}];
}
return [];
}
} }