mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-10-14 22:08:32 +00:00
410 lines
14 KiB
TypeScript
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',
|
|
};
|
|
}
|
|
}
|