From c6b125011eeac70346095f3041830341dfc2091e Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Thu, 21 Jan 2021 16:14:00 +0100 Subject: [PATCH] ATL-814: Show boards and ports under Tools menu. Signed-off-by: Akos Kitta --- .../browser/arduino-ide-frontend-module.ts | 3 + .../boards-widget-frontend-contribution.ts | 16 +- .../browser/contributions/board-selection.ts | 199 ++++++++++++++++++ .../src/browser/contributions/examples.ts | 7 +- .../src/browser/menu/arduino-menus.ts | 4 +- .../src/common/protocol/boards-service.ts | 13 ++ .../src/node/boards-service-impl.ts | 13 +- 7 files changed, 234 insertions(+), 21 deletions(-) create mode 100644 arduino-ide-extension/src/browser/contributions/board-selection.ts diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 009ac67b..85fc716a 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -133,6 +133,7 @@ import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/li import { Sketchbook } from './contributions/sketchbook'; import { DebugFrontendApplicationContribution } from './theia/debug/debug-frontend-application-contribution'; import { DebugFrontendApplicationContribution as TheiaDebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution'; +import { BoardSelection } from './contributions/board-selection'; const ElementQueries = require('css-element-queries/src/ElementQueries'); @@ -335,6 +336,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { Contribution.configure(bind, About); Contribution.configure(bind, Debug); Contribution.configure(bind, Sketchbook); + Contribution.configure(bind, BoardSelection); bind(OutputServiceImpl).toSelf().inSingletonScope().onActivation(({ container }, outputService) => { WebSocketConnectionProvider.createProxy(container, OutputServicePath, outputService); @@ -343,6 +345,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(OutputService).toService(OutputServiceImpl); bind(NotificationCenter).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(NotificationCenter); bind(NotificationServiceServer).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, NotificationServicePath)).inSingletonScope(); // Enable the dirty indicator on uncloseable widgets. diff --git a/arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts b/arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts index 9117081e..fdbb290d 100644 --- a/arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts +++ b/arduino-ide-extension/src/browser/boards/boards-widget-frontend-contribution.ts @@ -1,15 +1,11 @@ import { injectable } from 'inversify'; -import { MenuModelRegistry } from '@theia/core'; import { BoardsListWidget } from './boards-list-widget'; import { BoardsPackage } from '../../common/protocol/boards-service'; import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution'; -import { ArduinoMenus } from '../menu/arduino-menus'; @injectable() export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution { - static readonly OPEN_MANAGER = `${BoardsListWidget.WIDGET_ID}:toggle`; - constructor() { super({ widgetId: BoardsListWidget.WIDGET_ID, @@ -18,7 +14,7 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont area: 'left', rank: 600 }, - toggleCommandId: BoardsListWidgetFrontendContribution.OPEN_MANAGER, + toggleCommandId: `${BoardsListWidget.WIDGET_ID}:toggle`, toggleKeybinding: 'CtrlCmd+Shift+B' }); } @@ -27,14 +23,4 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont this.openView(); } - registerMenus(menus: MenuModelRegistry): void { - if (this.toggleCommand) { - menus.registerMenuAction(ArduinoMenus.TOOLS__MAIN_GROUP, { - commandId: this.toggleCommand.id, - label: 'Boards Manager...', - order: '4' - }); - } - } - } diff --git a/arduino-ide-extension/src/browser/contributions/board-selection.ts b/arduino-ide-extension/src/browser/contributions/board-selection.ts new file mode 100644 index 00000000..57a7dc01 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/board-selection.ts @@ -0,0 +1,199 @@ +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 { 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 { BoardsService, InstalledBoardWithPackage, AvailablePorts, Port } from '../../common/protocol'; +import { SketchContribution, Command, CommandRegistry } from './contribution'; + +@injectable() +export class BoardSelection extends SketchContribution { + + @inject(CommandRegistry) + protected readonly commandRegistry: CommandRegistry; + + @inject(MainMenuManager) + protected readonly mainMenuManager: MainMenuManager; + + @inject(MenuModelRegistry) + protected readonly menuModelRegistry: MenuModelRegistry; + + @inject(NotificationCenter) + protected readonly notificationCenter: NotificationCenter; + + @inject(BoardsService) + protected readonly boardsService: BoardsService; + + @inject(BoardsServiceProvider) + protected readonly boardsServiceProvider: BoardsServiceProvider; + + protected readonly toDisposeBeforeMenuRebuild = new DisposableCollection(); + + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, { + execute: async () => { + const { selectedBoard, selectedPort } = this.boardsServiceProvider.boardsConfig; + if (!selectedBoard) { + this.messageService.info('Please select a board to obtain board info.'); + return; + } + if (!selectedBoard.fqbn) { + this.messageService.info(`The platform for the selected '${selectedBoard.name}' board is not installed.`); + return; + } + if (!selectedPort) { + this.messageService.info('Please select a port to obtain board info.'); + return; + } + const boardDetails = await this.boardsService.getBoardDetails({ fqbn: selectedBoard.fqbn }); + if (boardDetails) { + const { VID, PID } = boardDetails; + const detail = `BN: ${selectedBoard.name} +VID: ${VID} +PID: ${PID}`; + await remote.dialog.showMessageBox(remote.getCurrentWindow(), { + message: 'Board Info', + title: 'Board Info', + type: 'info', + detail, + buttons: ['OK'] + }); + } + } + }); + } + + onStart(): void { + this.updateMenus(); + this.notificationCenter.onPlatformInstalled(this.updateMenus.bind(this)); + this.notificationCenter.onPlatformUninstalled(this.updateMenus.bind(this)); + this.boardsServiceProvider.onBoardsConfigChanged(this.updateMenus.bind(this)); + this.boardsServiceProvider.onAvailableBoardsChanged(this.updateMenus.bind(this)); + } + + protected async updateMenus(): Promise { + const [installedBoards, availablePorts, config] = await Promise.all([ + this.installedBoards(), + this.boardsService.getState(), + this.boardsServiceProvider.boardsConfig + ]); + this.rebuildMenus(installedBoards, availablePorts, config); + } + + protected rebuildMenus(installedBoards: InstalledBoardWithPackage[], availablePorts: AvailablePorts, config: BoardsConfig.Config): void { + this.toDisposeBeforeMenuRebuild.dispose(); + + // Boards submenu + const boardsSubmenuPath = [...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, '1_boards']; + const boardsSubmenuLabel = config.selectedBoard?.name; + // Note: The submenu order starts from `100` because `Auto Format`, `Serial Monitor`, etc starts from `0` index. + // The board specific items, and the rest, have order with `z`. We needed something between `0` and `z` with natural-order. + this.menuModelRegistry.registerSubmenu(boardsSubmenuPath, `Board${!!boardsSubmenuLabel ? `: "${boardsSubmenuLabel}"` : ''}`, { order: '100' }); + this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => unregisterSubmenu(boardsSubmenuPath, this.menuModelRegistry))); + + // Ports submenu + const portsSubmenuPath = [...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, '2_ports']; + const portsSubmenuLabel = config.selectedPort?.address; + this.menuModelRegistry.registerSubmenu(portsSubmenuPath, `Port${!!portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''}`, { order: '101' }); + this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => unregisterSubmenu(portsSubmenuPath, this.menuModelRegistry))); + + const getBoardInfo = { commandId: BoardSelection.Commands.GET_BOARD_INFO.id, label: 'Get Board Info', order: '103' }; + this.menuModelRegistry.registerMenuAction(ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP, getBoardInfo); + this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.menuModelRegistry.unregisterMenuAction(getBoardInfo))); + + const boardsManagerGroup = [...boardsSubmenuPath, '0_manager']; + const boardsPackagesGroup = [...boardsSubmenuPath, '1_packages']; + + this.menuModelRegistry.registerMenuAction(boardsManagerGroup, { + commandId: `${BoardsListWidget.WIDGET_ID}:toggle`, + label: 'Boards Manager...' + }); + + // Installed boards + for (const board of installedBoards) { + const { packageId, packageName, fqbn, name } = board; + + // Platform submenu + const platformMenuPath = [...boardsPackagesGroup, packageId]; + // Note: Registering the same submenu twice is a noop. No need to group the boards per platform. + this.menuModelRegistry.registerSubmenu(platformMenuPath, packageName); + + const id = `arduino-select-board--${fqbn}`; + const command = { id }; + const handler = { + execute: () => { + if (fqbn !== this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn) { + this.boardsServiceProvider.boardsConfig = { + selectedBoard: { + name, + fqbn, + port: this.boardsServiceProvider.boardsConfig.selectedBoard?.port // TODO: verify! + }, + selectedPort: this.boardsServiceProvider.boardsConfig.selectedPort + } + } + }, + isToggled: () => fqbn === this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn + }; + + // Board menu + const menuAction = { commandId: id, label: name }; + this.commandRegistry.registerCommand(command, handler); + this.toDisposeBeforeMenuRebuild.push(Disposable.create(() => this.commandRegistry.unregisterCommand(command))); + this.menuModelRegistry.registerMenuAction(platformMenuPath, menuAction); + // Note: we do not dispose the menu actions individually. Calling `unregisterSubmenu` on the parent will wipe the children menu nodes recursively. + } + + // 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 + } + } + }, + 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); + } + } + } + + this.mainMenuManager.update(); + } + + protected async installedBoards(): Promise { + const allBoards = await this.boardsService.allBoards({}); + return allBoards.filter(InstalledBoardWithPackage.is); + } + +} +export namespace BoardSelection { + export namespace Commands { + export const GET_BOARD_INFO: Command = { id: 'arduino-get-board-info' }; + } +} diff --git a/arduino-ide-extension/src/browser/contributions/examples.ts b/arduino-ide-extension/src/browser/contributions/examples.ts index e8a2538d..960bc231 100644 --- a/arduino-ide-extension/src/browser/contributions/examples.ts +++ b/arduino-ide-extension/src/browser/contributions/examples.ts @@ -1,6 +1,6 @@ import * as PQueue from 'p-queue'; import { inject, injectable, postConstruct } from 'inversify'; -import { MenuPath, SubMenuOptions, 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 { OpenSketch } from './open-sketch'; import { ArduinoMenus } from '../menu/arduino-menus'; @@ -60,12 +60,11 @@ export abstract class Examples extends SketchContribution { registerRecursively( exampleContainer: ExampleContainer, menuPath: MenuPath, - pushToDispose: DisposableCollection = new DisposableCollection(), - options?: SubMenuOptions): void { + pushToDispose: DisposableCollection = new DisposableCollection()): void { const { label, sketches, children } = exampleContainer; const submenuPath = [...menuPath, label]; - this.menuRegistry.registerSubmenu(submenuPath, label, options); + this.menuRegistry.registerSubmenu(submenuPath, label); children.forEach(child => this.registerRecursively(child, submenuPath, pushToDispose)); for (const sketch of sketches) { const { uri } = sketch; diff --git a/arduino-ide-extension/src/browser/menu/arduino-menus.ts b/arduino-ide-extension/src/browser/menu/arduino-menus.ts index a4e0931d..501d9c62 100644 --- a/arduino-ide-extension/src/browser/menu/arduino-menus.ts +++ b/arduino-ide-extension/src/browser/menu/arduino-menus.ts @@ -40,8 +40,10 @@ export namespace ArduinoMenus { export const TOOLS = [...MAIN_MENU_BAR, '4_tools']; // `Auto Format`, `Library Manager...`, `Boards Manager...` export const TOOLS__MAIN_GROUP = [...TOOLS, '0_main']; + // `Board`, `Port`, and `Get Board Info`. + export const TOOLS__BOARD_SELECTION_GROUP = [...TOOLS, '2_board_selection']; // Core settings, such as `Processor` and `Programmers` for the board and `Burn Bootloader` - export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '1_board_settings']; + export const TOOLS__BOARD_SETTINGS_GROUP = [...TOOLS, '3_board_settings']; // -- Help // `About` group diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts index 76af168b..a1ede3a1 100644 --- a/arduino-ide-extension/src/common/protocol/boards-service.ts +++ b/arduino-ide-extension/src/common/protocol/boards-service.ts @@ -267,12 +267,25 @@ export namespace BoardWithPackage { } +export interface InstalledBoardWithPackage extends BoardWithPackage { + readonly fqbn: string; +} +export namespace InstalledBoardWithPackage { + + export function is(boardWithPackage: BoardWithPackage): boardWithPackage is InstalledBoardWithPackage { + return !!boardWithPackage.fqbn; + } + +} + export interface BoardDetails { readonly fqbn: string; readonly requiredTools: Tool[]; readonly configOptions: ConfigOption[]; readonly programmers: Programmer[]; readonly debuggingSupported: boolean; + readonly VID: string; + readonly PID: string; } export interface Tool { diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index a8fdb661..27bee4d2 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -1,5 +1,6 @@ import { injectable, inject, named } from 'inversify'; import { ILogger } from '@theia/core/lib/common/logger'; +import { notEmpty } from '@theia/core/lib/common/objects'; import { BoardsService, Installable, @@ -128,12 +129,22 @@ export class BoardsServiceImpl implements BoardsService { platform: p.getPlatform() }); + let VID = 'N/A'; + let PID = 'N/A'; + const usbId = detailsResp.getIdentificationPrefList().map(item => item.getUsbid()).find(notEmpty); + if (usbId) { + VID = usbId.getVid(); + PID = usbId.getPid(); + } + return { fqbn, requiredTools, configOptions, programmers, - debuggingSupported + debuggingSupported, + VID, + PID }; }