Files
arduino-ide/arduino-ide-extension/src/browser/contributions/board-selection.ts

410 lines
14 KiB
TypeScript

import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { MenuModelRegistry } from '@theia/core/lib/common/menu/menu-model-registry';
import type { MenuPath } from '@theia/core/lib/common/menu/menu-types';
import { nls } from '@theia/core/lib/common/nls';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MainMenuManager } from '../../common/main-menu-manager';
import {
BoardsService,
BoardWithPackage,
createPlatformIdentifier,
getBoardInfo,
InstalledBoardWithPackage,
platformIdentifierEquals,
Port,
serializePlatformIdentifier,
} from '../../common/protocol';
import type { BoardList } from '../../common/protocol/board-list';
import { BoardsListWidget } from '../boards/boards-list-widget';
import { BoardsDataStore } from '../boards/boards-data-store';
import { BoardsServiceProvider } from '../boards/boards-service-provider';
import {
ArduinoMenus,
PlaceholderMenuNode,
unregisterSubmenu,
} from '../menu/arduino-menus';
import { NotificationCenter } from '../notification-center';
import { Command, CommandRegistry, SketchContribution } from './contribution';
@injectable()
export class BoardSelection extends SketchContribution {
@inject(CommandRegistry)
private readonly commandRegistry: CommandRegistry;
@inject(MainMenuManager)
private readonly mainMenuManager: MainMenuManager;
@inject(MenuModelRegistry)
private readonly menuModelRegistry: MenuModelRegistry;
@inject(NotificationCenter)
private readonly notificationCenter: NotificationCenter;
@inject(BoardsDataStore)
private readonly boardsDataStore: BoardsDataStore;
@inject(BoardsService)
private readonly boardsService: BoardsService;
@inject(BoardsServiceProvider)
private readonly boardsServiceProvider: BoardsServiceProvider;
private readonly toDisposeBeforeMenuRebuild = new DisposableCollection();
// do not query installed platforms on every change
private _installedBoards: Deferred<InstalledBoardWithPackage[]> | undefined;
override registerCommands(registry: CommandRegistry): void {
registry.registerCommand(BoardSelection.Commands.GET_BOARD_INFO, {
execute: async () => {
const boardInfo = await getBoardInfo(
this.boardsServiceProvider.boardList
);
if (typeof boardInfo === 'string') {
this.messageService.info(boardInfo);
return;
}
const { BN, VID, PID, SN } = boardInfo;
const detail = `
BN: ${BN}
VID: ${VID}
PID: ${PID}
SN: ${SN}
`.trim();
await this.dialogService.showMessageBox({
message: nls.localize('arduino/board/boardInfo', 'Board Info'),
title: nls.localize('arduino/board/boardInfo', 'Board Info'),
type: 'info',
detail,
buttons: [nls.localize('vscode/issueMainService/ok', 'OK')],
});
},
});
registry.registerCommand(BoardSelection.Commands.RELOAD_BOARD_DATA, {
execute: async () => {
const selectedFqbn =
this.boardsServiceProvider.boardList.boardsConfig.selectedBoard?.fqbn;
let message: string;
if (selectedFqbn) {
await this.boardsDataStore.reloadBoardData(selectedFqbn);
message = nls.localize(
'arduino/board/boardDataReloaded',
'Board data reloaded.'
);
} else {
message = nls.localize(
'arduino/board/selectBoardToReload',
'Please select a board first.'
);
}
this.messageService.info(message, { timeout: 2000 });
},
});
}
override onStart(): void {
this.notificationCenter.onPlatformDidInstall(() => this.updateMenus(true));
this.notificationCenter.onPlatformDidUninstall(() =>
this.updateMenus(true)
);
this.boardsServiceProvider.onBoardListDidChange(() => this.updateMenus());
}
override async onReady(): Promise<void> {
this.updateMenus();
}
private async updateMenus(discardCache = false): Promise<void> {
if (discardCache) {
this._installedBoards?.reject();
this._installedBoards = undefined;
}
if (!this._installedBoards) {
this._installedBoards = new Deferred();
this.installedBoards().then((installedBoards) =>
this._installedBoards?.resolve(installedBoards)
);
}
const installedBoards = await this._installedBoards.promise;
this.rebuildMenus(installedBoards, this.boardsServiceProvider.boardList);
}
private rebuildMenus(
installedBoards: InstalledBoardWithPackage[],
boardList: BoardList
): void {
this.toDisposeBeforeMenuRebuild.dispose();
// Boards submenu
const boardsSubmenuPath = [
...ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
'1_boards',
];
const { selectedBoard, selectedPort } = boardList.boardsConfig;
const boardsSubmenuLabel = 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,
nls.localize(
'arduino/board/board',
'Board{0}',
!!boardsSubmenuLabel ? `: "${boardsSubmenuLabel}"` : ''
),
{ order: '100' }
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
unregisterSubmenu(boardsSubmenuPath, this.menuModelRegistry)
)
);
// Ports submenu
const portsSubmenuPath = ArduinoMenus.TOOLS__PORTS_SUBMENU;
const portsSubmenuLabel = selectedPort?.address;
this.menuModelRegistry.registerSubmenu(
portsSubmenuPath,
nls.localize(
'arduino/board/port',
'Port{0}',
portsSubmenuLabel ? `: "${portsSubmenuLabel}"` : ''
),
{ order: '101' }
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
unregisterSubmenu(portsSubmenuPath, this.menuModelRegistry)
)
);
const reloadBoardData = {
commandId: BoardSelection.Commands.RELOAD_BOARD_DATA.id,
label: nls.localize('arduino/board/reloadBoardData', 'Reload Board Data'),
order: '102',
};
this.menuModelRegistry.registerMenuAction(
ArduinoMenus.TOOLS__BOARD_SELECTION_GROUP,
reloadBoardData
);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.menuModelRegistry.unregisterMenuAction(reloadBoardData)
)
);
const getBoardInfo = {
commandId: BoardSelection.Commands.GET_BOARD_INFO.id,
label: nls.localize('arduino/board/getBoardInfo', '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: `${BoardsListWidget.WIDGET_LABEL}...`,
});
const selectedBoardPlatformId = selectedBoard
? createPlatformIdentifier(selectedBoard)
: undefined;
// Keys are the vendor IDs
type BoardsPerVendor = Record<string, BoardWithPackage[]>;
// Group boards by their platform names. The keys are the platform names as menu labels.
// If there is a platform name (menu label) collision, refine the menu label with the vendor ID.
const groupedBoards = new Map<string, BoardsPerVendor>();
for (const board of installedBoards) {
const { packageId, packageName } = board;
const { vendorId } = packageId;
let boardsPerPackageName = groupedBoards.get(packageName);
if (!boardsPerPackageName) {
boardsPerPackageName = {} as BoardsPerVendor;
groupedBoards.set(packageName, boardsPerPackageName);
}
let boardPerVendor: BoardWithPackage[] | undefined =
boardsPerPackageName[vendorId];
if (!boardPerVendor) {
boardPerVendor = [];
boardsPerPackageName[vendorId] = boardPerVendor;
}
boardPerVendor.push(board);
}
// Installed boards
Array.from(groupedBoards.entries()).forEach(
([packageName, boardsPerPackage]) => {
const useVendorSuffix = Object.keys(boardsPerPackage).length > 1;
Object.entries(boardsPerPackage).forEach(([vendorId, boards]) => {
let platformMenuPath: MenuPath | undefined = undefined;
boards.forEach((board, index) => {
const { packageId, fqbn, name, manuallyInstalled } = board;
// create the platform submenu once.
// creating and registering the same submenu twice in Theia is a noop, though.
if (!platformMenuPath) {
let packageLabel =
packageName +
`${
manuallyInstalled
? nls.localize(
'arduino/board/inSketchbook',
' (in Sketchbook)'
)
: ''
}`;
if (
selectedBoardPlatformId &&
platformIdentifierEquals(packageId, selectedBoardPlatformId)
) {
packageLabel = `${packageLabel}`;
}
if (useVendorSuffix) {
packageLabel += ` (${vendorId})`;
}
// Platform submenu
platformMenuPath = [
...boardsPackagesGroup,
serializePlatformIdentifier(packageId),
];
this.menuModelRegistry.registerSubmenu(
platformMenuPath,
packageLabel,
{
order: packageName.toLowerCase(),
}
);
}
const id = `arduino-select-board--${fqbn}`;
const command = { id };
const handler = {
execute: () =>
this.boardsServiceProvider.updateConfig({
name: name,
fqbn: fqbn,
}),
isToggled: () => fqbn === selectedBoard?.fqbn,
};
// Board menu
const menuAction = {
commandId: id,
label: name,
order: String(index).padStart(4), // pads with leading zeros for alphanumeric sort where order is 1, 2, 11, and NOT 1, 11, 2
};
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.
});
});
}
);
// Detected ports
const registerPorts = (
protocol: string,
ports: ReturnType<BoardList['ports']>,
protocolOrder: number
) => {
if (!ports.length) {
return;
}
// Register placeholder for protocol
const menuPath = [
...portsSubmenuPath,
`${protocolOrder.toString()}_${protocol}`,
];
const placeholder = new PlaceholderMenuNode(
menuPath,
nls.localize(
'arduino/board/typeOfPorts',
'{0} ports',
Port.Protocols.protocolLabel(protocol)
),
{ order: protocolOrder.toString().padStart(4) }
);
this.menuModelRegistry.registerMenuNode(menuPath, placeholder);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.menuModelRegistry.unregisterMenuNode(placeholder.id)
)
);
for (let i = 0; i < ports.length; i++) {
const { port, boards } = ports[i];
const portKey = Port.keyOf(port);
let label = `${port.addressLabel}`;
if (boards?.length) {
const boardsList = boards.map((board) => board.name).join(', ');
label = `${label} (${boardsList})`;
}
const id = `arduino-select-port--${portKey}`;
const command = { id };
const handler = {
execute: () => {
this.boardsServiceProvider.updateConfig({
protocol: port.protocol,
address: port.address,
});
},
isToggled: () => {
return i === ports.matchingIndex;
},
};
const menuAction = {
commandId: id,
label,
order: String(protocolOrder + i + 1).padStart(4),
};
this.commandRegistry.registerCommand(command, handler);
this.toDisposeBeforeMenuRebuild.push(
Disposable.create(() =>
this.commandRegistry.unregisterCommand(command)
)
);
this.menuModelRegistry.registerMenuAction(menuPath, menuAction);
}
};
const groupedPorts = boardList.portsGroupedByProtocol();
let protocolOrder = 100;
Object.entries(groupedPorts).forEach(([protocol, ports]) => {
registerPorts(protocol, ports, protocolOrder);
protocolOrder += 100;
});
this.mainMenuManager.update();
}
protected async installedBoards(): Promise<InstalledBoardWithPackage[]> {
const allBoards = await this.boardsService.getInstalledBoards();
return allBoards.filter(InstalledBoardWithPackage.is);
}
}
export namespace BoardSelection {
export namespace Commands {
export const GET_BOARD_INFO: Command = { id: 'arduino-get-board-info' };
export const RELOAD_BOARD_DATA: Command = {
id: 'arduino-reload-board-data',
};
}
}