diff --git a/arduino-ide-extension/src/browser/contributions/board-selection.ts b/arduino-ide-extension/src/browser/contributions/board-selection.ts index 57a7dc01..3373e20c 100644 --- a/arduino-ide-extension/src/browser/contributions/board-selection.ts +++ b/arduino-ide-extension/src/browser/contributions/board-selection.ts @@ -2,12 +2,13 @@ import { inject, injectable } from 'inversify'; import { remote } from 'electron'; import { MenuModelRegistry } from '@theia/core/lib/common/menu'; import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable'; +import { firstToUpperCase } from '../../common/utils'; import { BoardsConfig } from '../boards/boards-config'; import { MainMenuManager } from '../../common/main-menu-manager'; import { BoardsListWidget } from '../boards/boards-list-widget'; import { NotificationCenter } from '../notification-center'; 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 { SketchContribution, Command, CommandRegistry } from './contribution'; @@ -150,39 +151,61 @@ PID: ${PID}`; } // Installed ports - for (const address of Object.keys(availablePorts)) { - if (!!availablePorts[address]) { - const [port, boards] = availablePorts[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 + const registerPorts = (ports: AvailablePorts) => { + const addresses = Object.keys(ports); + if (!addresses.length) { + return; + } + + // Register placeholder for protocol + const [port] = ports[addresses[0]]; + const protocol = port.protocol; + const menuPath = [...portsSubmenuPath, protocol]; + const placeholder = new PlaceholderMenuNode(menuPath, `${firstToUpperCase(port.protocol)} ports`); + this.menuModelRegistry.registerMenuNode(menuPath, placeholder); + this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.menuModelRegistry.unregisterMenuNode(placeholder.id))); + + for (const address of addresses) { + if (!!ports[address]) { + 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) - }; - const menuAction = { - commandId: id, - label: `${address}${name ? ` (${name})` : ''}` - }; - this.commandRegistry.registerCommand(command, handler); - this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.commandRegistry.unregisterCommand(command))); - this.menuModelRegistry.registerMenuAction(portsSubmenuPath, menuAction); + }, + isToggled: () => Port.equals(port, this.boardsServiceProvider.boardsConfig.selectedPort) + }; + const label = `${address}${name ? ` (${name})` : ''}`; + const menuAction = { + commandId: id, + 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.menuModelRegistry.registerMenuAction(menuPath, menuAction); + } } } } + const { serial, network, unknown } = AvailablePorts.groupByProtocol(availablePorts); + registerPorts(serial); + registerPorts(network); + registerPorts(unknown); + this.mainMenuManager.update(); } diff --git a/arduino-ide-extension/src/browser/contributions/examples.ts b/arduino-ide-extension/src/browser/contributions/examples.ts index 960bc231..4b1d5f1a 100644 --- a/arduino-ide-extension/src/browser/contributions/examples.ts +++ b/arduino-ide-extension/src/browser/contributions/examples.ts @@ -3,12 +3,13 @@ import { inject, injectable, postConstruct } from 'inversify'; import { MenuPath, CompositeMenuNode } from '@theia/core/lib/common/menu'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; 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 { BoardsServiceProvider } from '../boards/boards-service-provider'; import { ExamplesService, ExampleContainer } from '../../common/protocol/examples-service'; import { SketchContribution, CommandRegistry, MenuModelRegistry } from './contribution'; import { NotificationCenter } from '../notification-center'; +import { Board } from '../../common/protocol'; @injectable() export abstract class Examples extends SketchContribution { @@ -32,10 +33,10 @@ export abstract class Examples extends SketchContribution { @postConstruct() 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 } @@ -58,27 +59,33 @@ export abstract class Examples extends SketchContribution { } registerRecursively( - exampleContainer: ExampleContainer, + exampleContainerOrPlaceholder: ExampleContainer | string, menuPath: MenuPath, pushToDispose: DisposableCollection = new DisposableCollection()): void { - const { label, sketches, children } = exampleContainer; - const submenuPath = [...menuPath, label]; - this.menuRegistry.registerSubmenu(submenuPath, label); - children.forEach(child => this.registerRecursively(child, submenuPath, pushToDispose)); - for (const sketch of sketches) { - const { uri } = sketch; - const commandId = `arduino-open-example-${submenuPath.join(':')}--${uri}`; - const command = { id: commandId }; - const handler = { - execute: async () => { - const sketch = await this.sketchService.cloneExample(uri); - 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))); + if (typeof exampleContainerOrPlaceholder === 'string') { + const placeholder = new PlaceholderMenuNode(menuPath, exampleContainerOrPlaceholder); + this.menuRegistry.registerMenuNode(menuPath, placeholder); + pushToDispose.push(Disposable.create(() => this.menuRegistry.unregisterMenuNode(placeholder.id))); + } else { + const { label, sketches, children } = exampleContainerOrPlaceholder; + const submenuPath = [...menuPath, label]; + this.menuRegistry.registerSubmenu(submenuPath, label); + children.forEach(child => this.registerRecursively(child, submenuPath, pushToDispose)); + for (const sketch of sketches) { + const { uri } = sketch; + const commandId = `arduino-open-example-${submenuPath.join(':')}--${uri}`; + const command = { id: commandId }; + const handler = { + execute: async () => { + const sketch = await this.sketchService.cloneExample(uri); + 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; } 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.menuManager.update(); + // TODO: remove + console.log(typeof this.menuRegistry); } } @@ -123,17 +132,27 @@ export class LibraryExamples extends Examples { this.notificationCenter.onLibraryUninstalled(() => this.register()); } - protected handleBoardChanged(fqbn: string | undefined): void { - this.register(fqbn); + protected handleBoardChanged(board: Board | undefined): void { + 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 () => { this.toDispose.dispose(); - if (!fqbn) { + if (!board || !board.fqbn) { return; } + const { fqbn, name } = board; 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) { this.registerRecursively(container, ArduinoMenus.EXAMPLES__USER_LIBS_GROUP, this.toDispose); } diff --git a/arduino-ide-extension/src/browser/contributions/include-library.ts b/arduino-ide-extension/src/browser/contributions/include-library.ts index 6ad8756a..079e6b2a 100644 --- a/arduino-ide-extension/src/browser/contributions/include-library.ts +++ b/arduino-ide-extension/src/browser/contributions/include-library.ts @@ -5,8 +5,8 @@ import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; import { EditorManager } from '@theia/editor/lib/browser'; import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; -import { ArduinoMenus } from '../menu/arduino-menus'; -import { LibraryPackage, LibraryLocation, LibraryService } from '../../common/protocol'; +import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus'; +import { LibraryPackage, LibraryService } from '../../common/protocol'; import { MainMenuManager } from '../../common/main-menu-manager'; import { LibraryListWidget } from '../library/library-list-widget'; import { BoardsServiceProvider } from '../boards/boards-service-provider'; @@ -84,19 +84,35 @@ export class IncludeLibrary extends SketchContribution { // `Arduino libraries` const packageMenuPath = [...includeLibMenuPath, '2_arduino']; const userMenuPath = [...includeLibMenuPath, '3_contributed']; - for (const library of libraries) { - this.toDispose.push(this.registerLibrary(library, library.location === LibraryLocation.USER ? userMenuPath : packageMenuPath)); + const { user, rest } = LibraryPackage.groupByLocation(libraries); + 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(); }); } - protected registerLibrary(library: LibraryPackage, menuPath: MenuPath): Disposable { - const commandId = `arduino-include-library--${library.name}:${library.author}`; + protected registerLibrary(libraryOrPlaceholder: LibraryPackage | string, menuPath: MenuPath): Disposable { + 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 handler = { execute: () => this.commandRegistry.executeCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY.id, library) }; - const menuAction = { commandId, label: library.name }; + const handler = { execute: () => this.commandRegistry.executeCommand(IncludeLibrary.Commands.INCLUDE_LIBRARY.id, libraryOrPlaceholder) }; + const menuAction = { commandId, label: libraryOrPlaceholder.name }; this.menuRegistry.registerMenuAction(menuPath, menuAction); return new DisposableCollection( this.commandRegistry.registerCommand(command, handler), diff --git a/arduino-ide-extension/src/browser/menu/arduino-menus.ts b/arduino-ide-extension/src/browser/menu/arduino-menus.ts index 6a61fbc6..6ef51e95 100644 --- a/arduino-ide-extension/src/browser/menu/arduino-menus.ts +++ b/arduino-ide-extension/src/browser/menu/arduino-menus.ts @@ -1,6 +1,6 @@ import { isOSX } from '@theia/core/lib/common/os'; 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 { @@ -99,3 +99,24 @@ export function unregisterSubmenu(menuPath: string[], menuRegistry: MenuModelReg } (parent.children as Array).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('-'); + } + +} diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts index a1ede3a1..218b588b 100644 --- a/arduino-ide-extension/src/common/protocol/boards-service.ts +++ b/arduino-ide-extension/src/common/protocol/boards-service.ts @@ -5,6 +5,25 @@ import { Installable } from './installable'; import { ArduinoComponent } from './arduino-component'; export type AvailablePorts = Record]>; +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 { readonly oldState: Readonly<{ boards: Board[], ports: Port[] }>; diff --git a/arduino-ide-extension/src/common/protocol/library-service.ts b/arduino-ide-extension/src/common/protocol/library-service.ts index 04bb4ee6..efbb37d6 100644 --- a/arduino-ide-extension/src/common/protocol/library-service.ts +++ b/arduino-ide-extension/src/common/protocol/library-service.ts @@ -69,4 +69,17 @@ export namespace LibraryPackage { 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 }; + } + } diff --git a/arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts b/arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts index 268a6d12..0341dc45 100644 --- a/arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts +++ b/arduino-ide-extension/src/electron-browser/theia/core/electron-main-menu-factory.ts @@ -1,8 +1,9 @@ import { injectable } from 'inversify' import { remote } from 'electron'; import { Keybinding } from '@theia/core/lib/common/keybinding'; -import { ElectronMainMenuFactory as TheiaElectronMainMenuFactory } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory'; -import { ArduinoMenus } from '../../../browser/menu/arduino-menus'; +import { CompositeMenuNode } from '@theia/core/lib/common/menu'; +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() export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory { @@ -42,4 +43,15 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory { 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 []; + } + }