Added support for 3rd party core settings.

Closes arduino/arduino-pro-ide#10.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
Akos Kitta
2020-03-03 17:03:11 +01:00
parent 5c16f8d6c9
commit 12f2aa35ff
43 changed files with 1905 additions and 895 deletions

View File

@@ -2,7 +2,7 @@ import { injectable, inject } from 'inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service';
import { ArduinoDaemonClient } from '../common/protocol/arduino-daemon';
import { ArduinoDaemonClient } from '../common/protocol';
@injectable()
export class ArduinoDaemonClientImpl implements ArduinoDaemonClient {

View File

@@ -5,9 +5,8 @@ import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
import { MessageService } from '@theia/core/lib/common/message-service';
import { CommandContribution, CommandRegistry, Command, CommandHandler } from '@theia/core/lib/common/command';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { BoardsService } from '../common/protocol/boards-service';
import { BoardsService, BoardsServiceClient, CoreService, Sketch, SketchesService, ToolOutputServiceClient } from '../common/protocol';
import { ArduinoCommands } from './arduino-commands';
import { CoreService } from '../common/protocol/core-service';
import { BoardsServiceClientImpl } from './boards/boards-service-client-impl';
import { WorkspaceRootUriAwareCommandHandler, WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
import { SelectionService, MenuContribution, MenuModelRegistry, MAIN_MENU_BAR, MenuPath } from '@theia/core';
@@ -19,8 +18,6 @@ import {
} from '@theia/core/lib/browser';
import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog';
import { FileSystem, FileStat } from '@theia/filesystem/lib/common';
import { Sketch, SketchesService } from '../common/protocol/sketches-service';
import { ToolOutputServiceClient } from '../common/protocol/tool-output-service';
import { CommonCommands, CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
import { FileSystemCommands } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
import { FileDownloadCommands } from '@theia/filesystem/lib/browser/download/file-download-command-contribution';
@@ -45,6 +42,8 @@ import { ColorContribution } from '@theia/core/lib/browser/color-application-con
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { ArduinoDaemon } from '../common/protocol/arduino-daemon';
import { ConfigService } from '../common/protocol/config-service';
import { BoardsConfigStore } from './boards/boards-config-store';
import { MainMenuManager } from './menu/main-menu-manager';
export namespace ArduinoMenus {
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
@@ -75,7 +74,11 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
protected readonly toolOutputServiceClient: ToolOutputServiceClient;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected readonly boardsServiceClientImpl: BoardsServiceClientImpl;
// Unused but do not remove it. It's required by DI, otherwise `init` method is not called.
@inject(BoardsServiceClient)
protected readonly boardsServiceClient: BoardsServiceClient;
@inject(SelectionService)
protected readonly selectionService: SelectionService;
@@ -143,6 +146,12 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(BoardsConfigStore)
protected readonly boardsConfigStore: BoardsConfigStore;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
protected application: FrontendApplication;
protected wsSketchCount: number = 0; // TODO: this does not belong here, does it?
@@ -154,15 +163,10 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
text: BoardsConfig.Config.toString(config)
});
}
this.boardsServiceClient.onBoardsConfigChanged(updateStatusBar);
updateStatusBar(this.boardsServiceClient.boardsConfig);
this.boardsServiceClientImpl.onBoardsConfigChanged(updateStatusBar);
updateStatusBar(this.boardsServiceClientImpl.boardsConfig);
this.registerSketchesInMenu(this.menuRegistry);
Promise.all([
this.boardsService.getAttachedBoards(),
this.boardsService.getAvailablePorts()
]).then(([{ boards }, { ports }]) => this.boardsServiceClient.tryReconnect(boards, ports));
}
onStart(app: FrontendApplication): void {
@@ -210,8 +214,7 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
render: () => <BoardsToolBarItem
key='boardsToolbarItem'
commands={this.commandRegistry}
boardsServiceClient={this.boardsServiceClient}
boardService={this.boardsService} />,
boardsServiceClient={this.boardsServiceClientImpl} />,
isVisible: widget => ArduinoToolbar.is(widget) && widget.side === 'left',
priority: 2
});
@@ -276,10 +279,7 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
});
registry.registerCommand(ArduinoCommands.TOGGLE_COMPILE_FOR_DEBUG, {
execute: () => {
this.editorMode.toggleCompileForDebug();
this.editorMode.menuContentChanged.fire();
},
execute: () => this.editorMode.toggleCompileForDebug(),
isToggled: () => this.editorMode.compileForDebug
});
@@ -345,7 +345,7 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
execute: async () => {
const boardsConfig = await this.boardsConfigDialog.open();
if (boardsConfig) {
this.boardsServiceClient.boardsConfig = boardsConfig;
this.boardsServiceClientImpl.boardsConfig = boardsConfig;
}
}
});
@@ -377,18 +377,18 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
}
try {
const { boardsConfig } = this.boardsServiceClient;
const { boardsConfig } = this.boardsServiceClientImpl;
if (!boardsConfig || !boardsConfig.selectedBoard) {
throw new Error('No boards selected. Please select a board.');
}
if (!boardsConfig.selectedBoard.fqbn) {
throw new Error(`No core is installed for ${boardsConfig.selectedBoard.name}. Please install the board.`);
throw new Error(`No core is installed for the '${boardsConfig.selectedBoard.name}' board. Please install the core.`);
}
// Reveal the Output view asynchronously (don't await it)
const fqbn = await this.boardsConfigStore.appendConfigToFqbn(boardsConfig.selectedBoard.fqbn);
this.outputContribution.openView({ reveal: true });
await this.coreService.compile({
uri: uri.toString(),
board: boardsConfig.selectedBoard,
sketchUri: uri.toString(),
fqbn,
optimizeForDebug: this.editorMode.compileForDebug
});
} catch (e) {
@@ -413,7 +413,7 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
}
try {
const { boardsConfig } = this.boardsServiceClient;
const { boardsConfig } = this.boardsServiceClientImpl;
if (!boardsConfig || !boardsConfig.selectedBoard) {
throw new Error('No boards selected. Please select a board.');
}
@@ -421,11 +421,14 @@ export class ArduinoFrontendContribution implements FrontendApplicationContribut
if (!selectedPort) {
throw new Error('No ports selected. Please select a port.');
}
// Reveal the Output view asynchronously (don't await it)
if (!boardsConfig.selectedBoard.fqbn) {
throw new Error(`No core is installed for the '${boardsConfig.selectedBoard.name}' board. Please install the core.`);
}
this.outputContribution.openView({ reveal: true });
const fqbn = await this.boardsConfigStore.appendConfigToFqbn(boardsConfig.selectedBoard.fqbn);
await this.coreService.upload({
uri: uri.toString(),
board: boardsConfig.selectedBoard,
sketchUri: uri.toString(),
fqbn,
port: selectedPort.address,
optimizeForDebug: this.editorMode.compileForDebug
});

View File

@@ -75,6 +75,9 @@ import { ArduinoFrontendConnectionStatusService, ArduinoApplicationConnectionSta
import { FrontendConnectionStatusService, ApplicationConnectionStatusContribution } from '@theia/core/lib/browser/connection-status-service';
import { ConfigServiceClientImpl } from './config-service-client-impl';
import { CoreServiceClientImpl } from './core-service-client-impl';
import { BoardsDetailsMenuUpdater } from './boards/boards-details-menu-updater';
import { BoardsConfigStore } from './boards/boards-config-store';
import { ILogger } from '@theia/core';
const ElementQueries = require('css-element-queries/src/ElementQueries');
@@ -144,12 +147,28 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
// Boards service client to receive and delegate notifications from the backend.
bind(BoardsServiceClientImpl).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsServiceClientImpl);
bind(BoardsServiceClient).toDynamicValue(context => {
bind(BoardsServiceClient).toDynamicValue(async context => {
const client = context.container.get(BoardsServiceClientImpl);
const service = context.container.get<BoardsService>(BoardsService);
const [attachedBoards, availablePorts] = await Promise.all([
service.getAttachedBoards(),
service.getAvailablePorts()
]);
client.init({ attachedBoards, availablePorts });
WebSocketConnectionProvider.createProxy(context.container, BoardsServicePath, client);
return client;
}).inSingletonScope();
// To be able to track, and update the menu based on the core settings (aka. board details) of the currently selected board.
bind(FrontendApplicationContribution).to(BoardsDetailsMenuUpdater).inSingletonScope();
bind(BoardsConfigStore).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsConfigStore);
// Logger for the Arduino daemon
bind(ILogger).toDynamicValue(ctx => {
const parentLogger = ctx.container.get<ILogger>(ILogger);
return parentLogger.child('store');
}).inSingletonScope().whenTargetNamed('store');
// Boards auto-installer
bind(BoardsAutoInstaller).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(BoardsAutoInstaller);

View File

@@ -35,9 +35,9 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
protected ensureCoreExists(config: BoardsConfig.Config): void {
const { selectedBoard } = config;
if (selectedBoard) {
this.boardsService.search({}).then(({ items }) => {
const candidates = items
.filter(item => item.boards.some(board => Board.sameAs(board, selectedBoard)))
this.boardsService.search({}).then(packages => {
const candidates = packages
.filter(pkg => pkg.boards.some(board => Board.sameAs(board, selectedBoard)))
.filter(({ installable, installedVersion }) => installable && !installedVersion);
for (const candidate of candidates) {
// tslint:disable-next-line:max-line-length

View File

@@ -0,0 +1,43 @@
import { inject, injectable } from 'inversify';
import { QuickOpenItem, QuickOpenModel } from '@theia/core/lib/common/quick-open-model';
import { QuickOpenService, QuickOpenOptions } from '@theia/core/lib/browser/quick-open/quick-open-service';
import { BoardsService, BoardsServiceClient } from '../../common/protocol';
@injectable()
export class BoardsConfigQuickOpenService {
@inject(QuickOpenService)
protected readonly quickOpenService: QuickOpenService;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClient)
protected readonly boardsServiceClient: BoardsServiceClient;
async selectBoard(): Promise<void> {
}
protected open(items: QuickOpenItem | QuickOpenItem[], placeholder: string): void {
this.quickOpenService.open(this.getModel(Array.isArray(items) ? items : [items]), this.getOptions(placeholder));
}
protected getOptions(placeholder: string, fuzzyMatchLabel: boolean = true, onClose: (canceled: boolean) => void = () => { }): QuickOpenOptions {
return QuickOpenOptions.resolve({
placeholder,
fuzzyMatchLabel,
fuzzySort: false,
onClose
});
}
protected getModel(items: QuickOpenItem | QuickOpenItem[]): QuickOpenModel {
return {
onType(_: string, acceptor: (items: QuickOpenItem[]) => void): void {
acceptor(Array.isArray(items) ? items : [items]);
}
};
}
}

View File

@@ -0,0 +1,161 @@
import { injectable, inject, named } from 'inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { MaybePromise } from '@theia/core/lib/common/types';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { deepClone, notEmpty } from '@theia/core/lib/common/objects';
import { FrontendApplicationContribution, LocalStorageService } from '@theia/core/lib/browser';
import { BoardsService, ConfigOption, Installable, BoardDetails } from '../../common/protocol';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
@injectable()
export class BoardsConfigStore implements FrontendApplicationContribution {
@inject(ILogger)
@named('store')
protected readonly logger: ILogger;
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
@inject(LocalStorageService)
protected readonly storageService: LocalStorageService;
protected readonly onChangedEmitter = new Emitter<void>();
onStart(): void {
this.boardsServiceClient.onBoardsPackageInstalled(async ({ pkg }) => {
const { installedVersion: version } = pkg;
if (!version) {
return;
}
let shouldFireChanged = false;
for (const fqbn of pkg.boards.map(({ fqbn }) => fqbn).filter(notEmpty).filter(fqbn => !!fqbn)) {
const key = this.getStorageKey(fqbn, version);
let data = await this.storageService.getData<ConfigOption[] | undefined>(key);
if (!data || !data.length) {
const details = await this.getBoardDetailsSafe(fqbn);
if (details) {
data = details.configOptions;
if (data.length) {
await this.storageService.setData(key, data);
shouldFireChanged = true;
}
}
}
}
if (shouldFireChanged) {
this.fireChanged();
}
});
}
get onChanged(): Event<void> {
return this.onChangedEmitter.event;
}
async appendConfigToFqbn(
fqbn: string,
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<string> {
const configOptions = await this.getConfig(fqbn, boardsPackageVersion);
return ConfigOption.decorate(fqbn, configOptions);
}
async getConfig(
fqbn: string,
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<ConfigOption[]> {
const version = await boardsPackageVersion;
if (!version) {
return [];
}
const key = this.getStorageKey(fqbn, version);
let configOptions = await this.storageService.getData<ConfigOption[] | undefined>(key, undefined);
if (configOptions) {
return configOptions;
}
const details = await this.getBoardDetailsSafe(fqbn);
if (!details) {
return [];
}
configOptions = details.configOptions;
await this.storageService.setData(key, configOptions);
return configOptions;
}
async setSelected(
{ fqbn, option, selectedValue }: { fqbn: string, option: string, selectedValue: string },
boardsPackageVersion: MaybePromise<Installable.Version | undefined> = this.getBoardsPackageVersion(fqbn)): Promise<boolean> {
const configOptions = deepClone(await this.getConfig(fqbn, boardsPackageVersion));
const configOption = configOptions.find(c => c.option === option);
if (!configOption) {
return false;
}
let updated = false;
for (const value of configOption.values) {
if (value.value === selectedValue) {
(value as any).selected = true;
updated = true;
} else {
(value as any).selected = false;
}
}
if (!updated) {
return false;
}
const version = await boardsPackageVersion;
if (!version) {
return false;
}
await this.setConfig({ fqbn, configOptions, version });
this.fireChanged();
return true;
}
protected async setConfig(
{ fqbn, configOptions, version }: { fqbn: string, configOptions: ConfigOption[], version: Installable.Version }): Promise<void> {
const key = this.getStorageKey(fqbn, version);
return this.storageService.setData(key, configOptions);
}
protected getStorageKey(fqbn: string, version: Installable.Version): string {
return `.arduinoProIDE-configOptions-${version}-${fqbn}`;
}
protected async getBoardDetailsSafe(fqbn: string): Promise<BoardDetails | undefined> {
try {
const details = this.boardsService.getBoardDetails({ fqbn });
return details;
} catch (err) {
if (err instanceof Error && err.message.includes('loading board data') && err.message.includes('is not installed')) {
this.logger.warn(`The boards package is not installed for board with FQBN: ${fqbn}`);
} else {
this.logger.error(`An unexpected error occurred while retrieving the board details for ${fqbn}.`, err);
}
return undefined;
}
}
protected fireChanged(): void {
this.onChangedEmitter.fire();
}
protected async getBoardsPackageVersion(fqbn: string): Promise<Installable.Version | undefined> {
if (!fqbn) {
return undefined;
}
const boardsPackage = await this.boardsService.getContainerBoardPackage({ fqbn });
if (!boardsPackage) {
return undefined;
}
return boardsPackage.installedVersion;
}
}

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { DisposableCollection } from '@theia/core';
import { BoardsService, Board, Port, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
import { BoardsService, Board, Port, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { CoreServiceClientImpl } from '../core-service-client-impl';
import { ArduinoDaemonClientImpl } from '../arduino-daemon-client-impl';
@@ -36,11 +36,11 @@ export abstract class Item<T> extends React.Component<{
selected: boolean,
onClick: (item: T) => void,
missing?: boolean,
detail?: string
details?: string
}> {
render(): React.ReactNode {
const { selected, label, missing, detail } = this.props;
const { selected, label, missing, details } = this.props;
const classNames = ['item'];
if (selected) {
classNames.push('selected');
@@ -48,11 +48,11 @@ export abstract class Item<T> extends React.Component<{
if (missing === true) {
classNames.push('missing')
}
return <div onClick={this.onClick} className={classNames.join(' ')} title={`${label}${!detail ? '' : detail}`}>
return <div onClick={this.onClick} className={classNames.join(' ')} title={`${label}${!details ? '' : details}`}>
<div className='label'>
{label}
</div>
{!detail ? '' : <div className='detail'>{detail}</div>}
{!details ? '' : <div className='details'>{details}</div>}
{!selected ? '' : <div className='selected-icon'><i className='fa fa-check' /></div>}
</div>;
}
@@ -82,13 +82,15 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
componentDidMount() {
this.updateBoards();
this.props.boardsService.getAvailablePorts().then(({ ports }) => this.updatePorts(ports));
this.props.boardsService.getAvailablePorts().then(ports => this.updatePorts(ports));
const { boardsServiceClient, coreServiceClient, daemonClient } = this.props;
this.toDispose.pushAll([
boardsServiceClient.onBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)),
boardsServiceClient.onAttachedBoardsChanged(event => this.updatePorts(event.newState.ports, AttachedBoardsChangeEvent.diff(event).detached.ports)),
boardsServiceClient.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => {
this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged());
}),
boardsServiceClient.onBoardsPackageInstalled(() => this.updateBoards(this.state.query)),
boardsServiceClient.onBoardsPackageUninstalled(() => this.updateBoards(this.state.query)),
coreServiceClient.onIndexUpdated(() => this.updateBoards(this.state.query)),
daemonClient.onDaemonStarted(() => this.updateBoards(this.state.query)),
daemonClient.onDaemonStopped(() => this.setState({ searchResults: [] }))
@@ -110,11 +112,11 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
: eventOrQuery.target.value.toLowerCase()
).trim();
this.setState({ query });
this.queryBoards({ query }).then(({ searchResults }) => this.setState({ searchResults }));
this.queryBoards({ query }).then(searchResults => this.setState({ searchResults }));
}
protected updatePorts = (ports: Port[] = [], removedPorts: Port[] = []) => {
this.queryPorts(Promise.resolve({ ports })).then(({ knownPorts }) => {
this.queryPorts(Promise.resolve(ports)).then(({ knownPorts }) => {
let { selectedPort } = this.state;
// If the currently selected port is not available anymore, unset the selected port.
if (removedPorts.some(port => Port.equals(port, selectedPort))) {
@@ -124,35 +126,17 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
});
}
protected queryBoards = (options: { query?: string } = {}): Promise<{ searchResults: Array<Board & { packageName: string }> }> => {
const { boardsService } = this.props;
const query = (options.query || '').toLocaleLowerCase();
return new Promise<{ searchResults: Array<Board & { packageName: string }> }>(resolve => {
boardsService.search(options)
.then(({ items }) => items
.map(item => item.boards.map(board => ({ ...board, packageName: item.name })))
.reduce((acc, curr) => acc.concat(curr), [])
.filter(board => board.name.toLocaleLowerCase().indexOf(query) !== -1)
.sort(Board.compare))
.then(searchResults => resolve({ searchResults }));
});
protected queryBoards = (options: { query?: string } = {}): Promise<Array<Board & { packageName: string }>> => {
return this.props.boardsService.searchBoards(options);
}
protected get attachedBoards(): Promise<{ boards: Board[] }> {
return this.props.boardsService.getAttachedBoards();
}
protected get availablePorts(): Promise<{ ports: Port[] }> {
protected get availablePorts(): Promise<Port[]> {
return this.props.boardsService.getAvailablePorts();
}
protected queryPorts = (availablePorts: Promise<{ ports: Port[] }> = this.availablePorts) => {
return new Promise<{ knownPorts: Port[] }>(resolve => {
availablePorts
.then(({ ports }) => ports
.sort(Port.compare))
.then(knownPorts => resolve({ knownPorts }));
});
protected queryPorts = async (availablePorts: Promise<Port[]> = this.availablePorts) => {
const ports = await availablePorts;
return { knownPorts: ports.sort(Port.compare) };
}
protected toggleFilterPorts = () => {
@@ -194,41 +178,20 @@ export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConf
protected renderBoards(): React.ReactNode {
const { selectedBoard, searchResults } = this.state;
// Board names are not unique. We show the corresponding core name as a detail.
// https://github.com/arduino/arduino-cli/pull/294#issuecomment-513764948
const distinctBoardNames = new Map<string, number>();
for (const { name } of searchResults) {
const counter = distinctBoardNames.get(name) || 0;
distinctBoardNames.set(name, counter + 1);
}
// Due to the non-unique board names, we have to check the package name as well.
const selected = (board: Board & { packageName: string }) => {
if (!!selectedBoard) {
if (Board.equals(board, selectedBoard)) {
if ('packageName' in selectedBoard) {
return board.packageName === (selectedBoard as any).packageName;
}
return true;
}
}
return false;
}
return <React.Fragment>
<div className='search'>
<input type='search' className='theia-input' placeholder='SEARCH BOARD' onChange={this.updateBoards} ref={this.focusNodeSet} />
<i className='fa fa-search'></i>
</div>
<div className='boards list'>
{this.state.searchResults.map(board => <Item<Board & { packageName: string }>
{Board.decorateBoards(selectedBoard, searchResults).map(board => <Item<Board & { packageName: string }>
key={`${board.name}-${board.packageName}`}
item={board}
label={board.name}
detail={(distinctBoardNames.get(board.name) || 0) > 1 ? ` - ${board.packageName}` : undefined}
selected={selected(board)}
details={board.details}
selected={board.selected}
onClick={this.selectBoard}
missing={!Board.installed(board)}
missing={board.missing}
/>)}
</div>
</React.Fragment>;
@@ -276,9 +239,9 @@ export namespace BoardsConfig {
export namespace Config {
export function sameAs(config: Config, other: Config | AttachedSerialBoard): boolean {
export function sameAs(config: Config, other: Config | Board): boolean {
const { selectedBoard, selectedPort } = config;
if (AttachedSerialBoard.is(other)) {
if (Board.is(other)) {
return !!selectedBoard
&& Board.equals(other, selectedBoard)
&& Port.sameAs(selectedPort, other.port);

View File

@@ -0,0 +1,90 @@
import { inject, injectable } from 'inversify';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry, MenuNode } from '@theia/core/lib/common/menu';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { Board, ConfigOption } from '../../common/protocol';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { ArduinoMenus } from '../arduino-frontend-contribution';
import { BoardsConfigStore } from './boards-config-store';
import { MainMenuManager } from '../menu/main-menu-manager';
@injectable()
export class BoardsDetailsMenuUpdater implements FrontendApplicationContribution {
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
@inject(BoardsConfigStore)
protected readonly boardsConfigStore: BoardsConfigStore;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected readonly toDisposeOnBoardChange = new DisposableCollection();
onStart(): void {
this.boardsConfigStore.onChanged(() => this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard));
this.boardsServiceClient.onBoardsConfigChanged(({ selectedBoard }) => this.updateMenuActions(selectedBoard));
this.updateMenuActions(this.boardsServiceClient.boardsConfig.selectedBoard);
}
protected async updateMenuActions(selectedBoard: Board | undefined): Promise<void> {
if (selectedBoard) {
this.toDisposeOnBoardChange.dispose();
this.mainMenuManager.update();
const { fqbn } = selectedBoard;
if (fqbn) {
const configOptions = await this.boardsConfigStore.getConfig(fqbn);
const boardsConfigMenuPath = [...ArduinoMenus.TOOLS, 'z_boardsConfig']; // `z_` is for ordering.
for (const { label, option, values } of configOptions.sort(ConfigOption.LABEL_COMPARATOR)) {
const menuPath = [...boardsConfigMenuPath, `${option}`];
const commands = new Map<string, Disposable>()
for (const value of values) {
const id = `${fqbn}-${option}--${value.value}`;
const command = { id, label: value.label };
const selectedValue = value.value;
const handler = {
execute: () => this.boardsConfigStore.setSelected({ fqbn, option, selectedValue }),
isToggled: () => value.selected
};
commands.set(id, this.commandRegistry.registerCommand(command, handler));
}
this.menuRegistry.registerSubmenu(menuPath, label);
this.toDisposeOnBoardChange.pushAll([
...commands.values(),
Disposable.create(() => this.unregisterSubmenu(menuPath)), // We cannot dispose submenu entries: https://github.com/eclipse-theia/theia/issues/7299
...Array.from(commands.keys()).map((commandId, index) => {
this.menuRegistry.registerMenuAction(menuPath, { commandId, order: String(index) })
return Disposable.create(() => this.menuRegistry.unregisterMenuAction(commandId))
})
]);
}
this.mainMenuManager.update();
}
}
}
protected unregisterSubmenu(menuPath: string[]): void {
if (menuPath.length < 2) {
throw new Error(`Expected at least two item as a menu-path. Got ${JSON.stringify(menuPath)} instead.`);
}
const toRemove = menuPath[menuPath.length - 1];
const parentMenuPath = menuPath.slice(0, menuPath.length - 1);
// This is unsafe. Calling `getMenu` with a non-existing menu-path will result in a new menu creation.
// https://github.com/eclipse-theia/theia/issues/7300
const parent = this.menuRegistry.getMenu(parentMenuPath);
const index = parent.children.findIndex(({ id }) => id === toRemove);
if (index === -1) {
throw new Error(`Could not find menu with menu-path: ${JSON.stringify(menuPath)}.`);
}
(parent.children as Array<MenuNode>).splice(index, 1);
}
}

View File

@@ -1,17 +1,17 @@
import { inject, injectable } from 'inversify';
import { BoardPackage, BoardsService } from '../../common/protocol/boards-service';
import { BoardsPackage, BoardsService } from '../../common/protocol/boards-service';
import { ListWidget } from '../components/component-list/list-widget';
import { ListItemRenderer } from '../components/component-list/list-item-renderer';
@injectable()
export class BoardsListWidget extends ListWidget<BoardPackage> {
export class BoardsListWidget extends ListWidget<BoardsPackage> {
static WIDGET_ID = 'boards-list-widget';
static WIDGET_LABEL = 'Boards Manager';
constructor(
@inject(BoardsService) protected service: BoardsService,
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<BoardPackage>) {
@inject(ListItemRenderer) protected itemRenderer: ListItemRenderer<BoardsPackage>) {
super({
id: BoardsListWidget.WIDGET_ID,
@@ -19,7 +19,7 @@ export class BoardsListWidget extends ListWidget<BoardPackage> {
iconClass: 'fa fa-microchip',
searchable: service,
installable: service,
itemLabel: (item: BoardPackage) => item.name,
itemLabel: (item: BoardsPackage) => item.name,
itemRenderer
});
}

View File

@@ -1,11 +1,11 @@
import { injectable, inject } from 'inversify';
import { injectable, inject, optional } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { MessageService } from '@theia/core/lib/common/message-service';
import { LocalStorageService } from '@theia/core/lib/browser/storage-service';
import { StorageService } from '@theia/core/lib/browser/storage-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { RecursiveRequired } from '../../common/types';
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard, Board, Port, BoardUninstalledEvent } from '../../common/protocol/boards-service';
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, Board, Port, BoardUninstalledEvent } from '../../common/protocol';
import { BoardsConfig } from './boards-config';
@injectable()
@@ -14,16 +14,18 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
@inject(ILogger)
protected logger: ILogger;
@optional()
@inject(MessageService)
protected messageService: MessageService;
@inject(LocalStorageService)
protected storageService: LocalStorageService;
@inject(StorageService)
protected storageService: StorageService;
protected readonly onBoardInstalledEmitter = new Emitter<BoardInstalledEvent>();
protected readonly onBoardUninstalledEmitter = new Emitter<BoardUninstalledEvent>();
protected readonly onBoardsPackageInstalledEmitter = new Emitter<BoardInstalledEvent>();
protected readonly onBoardsPackageUninstalledEmitter = new Emitter<BoardUninstalledEvent>();
protected readonly onAttachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
protected readonly onSelectedBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
protected readonly onBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
protected readonly onAvailableBoardsChangedEmitter = new Emitter<AvailableBoard[]>();
/**
* Used for the auto-reconnecting. Sometimes, the attached board gets disconnected after uploading something to it.
@@ -34,35 +36,51 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
*/
protected latestValidBoardsConfig: RecursiveRequired<BoardsConfig.Config> | undefined = undefined;
protected _boardsConfig: BoardsConfig.Config = {};
protected _attachedBoards: Board[] = []; // This does not contain the `Unknown` boards. They're visible from the available ports only.
protected _availablePorts: Port[] = [];
protected _availableBoards: AvailableBoard[] = [];
readonly onBoardsChanged = this.onAttachedBoardsChangedEmitter.event;
readonly onBoardInstalled = this.onBoardInstalledEmitter.event;
readonly onBoardUninstalled = this.onBoardUninstalledEmitter.event;
readonly onBoardsConfigChanged = this.onSelectedBoardsConfigChangedEmitter.event;
/**
* Event when the state of the attached/detached boards has changed. For instance, the user have detached a physical board.
*/
readonly onAttachedBoardsChanged = this.onAttachedBoardsChangedEmitter.event;
readonly onBoardsPackageInstalled = this.onBoardsPackageInstalledEmitter.event;
readonly onBoardsPackageUninstalled = this.onBoardsPackageUninstalledEmitter.event;
/**
* Unlike `onAttachedBoardsChanged` this even fires when the user modifies the selected board in the IDE.\
* This even also fires, when the boards package was not available for the currently selected board,
* and the user installs the board package. Note: installing a board package will set the `fqbn` of the
* currently selected board.\
* This even also emitted when the board package for the currently selected board was uninstalled.
*/
readonly onBoardsConfigChanged = this.onBoardsConfigChangedEmitter.event;
readonly onAvailableBoardsChanged = this.onAvailableBoardsChangedEmitter.event;
async onStart(): Promise<void> {
return this.loadState();
}
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
this.logger.info('Attached boards and available ports changed: ', JSON.stringify(event));
const { detached, attached } = AttachedBoardsChangeEvent.diff(event);
const { selectedPort, selectedBoard } = this.boardsConfig;
this.onAttachedBoardsChangedEmitter.fire(event);
// Dynamically unset the port if is not available anymore. A port can be "detached" when removing a board.
if (detached.ports.some(port => Port.equals(selectedPort, port))) {
this.boardsConfig = {
selectedBoard,
selectedPort: undefined
};
}
// Try to reconnect.
this.tryReconnect(attached.boards, attached.ports);
/**
* When the FE connects to the BE, the BE stets the known boards and ports.\
* This is a DI workaround for not being able to inject the service into the client.
*/
init({ attachedBoards, availablePorts }: { attachedBoards: Board[], availablePorts: Port[] }): void {
this._attachedBoards = attachedBoards;
this._availablePorts = availablePorts;
this.reconcileAvailableBoards().then(() => this.tryReconnect());
}
async tryReconnect(attachedBoards: Board[], availablePorts: Port[]): Promise<boolean> {
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
this.logger.info('Attached boards and available ports changed: ', JSON.stringify(event));
this._attachedBoards = event.newState.boards;
this.onAttachedBoardsChangedEmitter.fire(event);
this._availablePorts = event.newState.ports;
this.reconcileAvailableBoards().then(() => this.tryReconnect());
}
protected async tryReconnect(): Promise<boolean> {
if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) {
for (const board of attachedBoards.filter(AttachedSerialBoard.is)) {
for (const board of this.availableBoards.filter(({ state }) => state !== AvailableBoard.State.incomplete)) {
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
&& this.latestValidBoardsConfig.selectedBoard.name === board.name
&& Port.sameAs(this.latestValidBoardsConfig.selectedPort, board.port)) {
@@ -73,13 +91,13 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
}
// If we could not find an exact match, we compare the board FQBN-name pairs and ignore the port, as it might have changed.
// See documentation on `latestValidBoardsConfig`.
for (const board of attachedBoards.filter(AttachedSerialBoard.is)) {
for (const board of this.availableBoards.filter(({ state }) => state !== AvailableBoard.State.incomplete)) {
if (this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn
&& this.latestValidBoardsConfig.selectedBoard.name === board.name) {
this.boardsConfig = {
...this.latestValidBoardsConfig,
selectedPort: availablePorts.find(port => Port.sameAs(port, board.port))
selectedPort: board.port
};
return true;
}
@@ -90,21 +108,52 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
notifyBoardInstalled(event: BoardInstalledEvent): void {
this.logger.info('Board installed: ', JSON.stringify(event));
this.onBoardInstalledEmitter.fire(event);
this.onBoardsPackageInstalledEmitter.fire(event);
const { selectedBoard } = this.boardsConfig;
const { installedVersion, id } = event.pkg;
if (selectedBoard) {
const installedBoard = event.pkg.boards.find(({ name }) => name === selectedBoard.name);
if (installedBoard && (!selectedBoard.fqbn || selectedBoard.fqbn === installedBoard.fqbn)) {
this.logger.info(`Board package ${id}[${installedVersion}] was installed. Updating the FQBN of the currently selected ${selectedBoard.name} board. [FQBN: ${installedBoard.fqbn}]`);
this.boardsConfig = {
...this.boardsConfig,
selectedBoard: installedBoard
};
}
}
}
notifyBoardUninstalled(event: BoardUninstalledEvent): void {
this.logger.info('Board uninstalled: ', JSON.stringify(event));
this.onBoardUninstalledEmitter.fire(event);
this.onBoardsPackageUninstalledEmitter.fire(event);
const { selectedBoard } = this.boardsConfig;
if (selectedBoard && selectedBoard.fqbn) {
const uninstalledBoard = event.pkg.boards.find(({ name }) => name === selectedBoard.name);
if (uninstalledBoard && uninstalledBoard.fqbn === selectedBoard.fqbn) {
this.logger.info(`Board package ${event.pkg.id} was uninstalled. Discarding the FQBN of the currently selected ${selectedBoard.name} board.`);
const selectedBoardWithoutFqbn = {
name: selectedBoard.name
// No FQBN
};
this.boardsConfig = {
...this.boardsConfig,
selectedBoard: selectedBoardWithoutFqbn
};
}
}
}
set boardsConfig(config: BoardsConfig.Config) {
this.doSetBoardsConfig(config);
this.saveState().finally(() => this.reconcileAvailableBoards().finally(() => this.onBoardsConfigChangedEmitter.fire(this._boardsConfig)));
}
protected doSetBoardsConfig(config: BoardsConfig.Config): void {
this.logger.info('Board config changed: ', JSON.stringify(config));
this._boardsConfig = config;
if (this.canUploadTo(this._boardsConfig)) {
this.latestValidBoardsConfig = this._boardsConfig;
}
this.saveState().then(() => this.onSelectedBoardsConfigChangedEmitter.fire(this._boardsConfig));
}
get boardsConfig(): BoardsConfig.Config {
@@ -123,7 +172,7 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
}
if (!config.selectedBoard) {
if (!options.silent) {
if (!options.silent && this.messageService) {
this.messageService.warn('No boards selected.', { timeout: 3000 });
}
return false;
@@ -133,7 +182,7 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
}
/**
* `true` if the `canVerify` and the `config.selectedPort` is also set with FQBN, hence can upload to board. Otherwise, `false`.
* `true` if `canVerify`, the board has an FQBN and the `config.selectedPort` is also set, hence can upload to board. Otherwise, `false`.
*/
canUploadTo(
config: BoardsConfig.Config | undefined = this.boardsConfig,
@@ -145,14 +194,14 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
const { name } = config.selectedBoard;
if (!config.selectedPort) {
if (!options.silent) {
if (!options.silent && this.messageService) {
this.messageService.warn(`No ports selected for board: '${name}'.`, { timeout: 3000 });
}
return false;
}
if (!config.selectedBoard.fqbn) {
if (!options.silent) {
if (!options.silent && this.messageService) {
this.messageService.warn(`The FQBN is not available for the selected board ${name}. Do you have the corresponding core installed?`, { timeout: 3000 });
}
return false;
@@ -161,8 +210,93 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
return true;
}
protected saveState(): Promise<void> {
return this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig);
get availableBoards(): AvailableBoard[] {
return this._availableBoards;
}
protected async reconcileAvailableBoards(): Promise<void> {
const attachedBoards = this._attachedBoards;
const availablePorts = this._availablePorts;
// Unset the port on the user's config, if it is not available anymore.
if (this.boardsConfig.selectedPort && !availablePorts.some(port => Port.sameAs(port, this.boardsConfig.selectedPort))) {
this.doSetBoardsConfig({ selectedBoard: this.boardsConfig.selectedBoard, selectedPort: undefined });
this.onBoardsConfigChangedEmitter.fire(this._boardsConfig);
}
const boardsConfig = this.boardsConfig;
const currentAvailableBoards = this._availableBoards;
const availableBoards: AvailableBoard[] = [];
const availableBoardPorts = availablePorts.filter(Port.isBoardPort);
const attachedSerialBoards = attachedBoards.filter(({ port }) => !!port);
for (const boardPort of availableBoardPorts) {
let state = AvailableBoard.State.incomplete; // Initial pessimism.
let board = attachedSerialBoards.find(({ port }) => Port.sameAs(boardPort, port));
if (board) {
state = AvailableBoard.State.recognized;
} else {
// If the selected board is not recognized because it is a 3rd party board: https://github.com/arduino/arduino-cli/issues/623
// We still want to show it without the red X in the boards toolbar: https://github.com/arduino/arduino-pro-ide/issues/198#issuecomment-599355836
const lastSelectedBoard = await this.getLastSelectedBoardOnPort(boardPort);
if (lastSelectedBoard) {
board = {
...lastSelectedBoard,
port: boardPort
};
state = AvailableBoard.State.guessed;
}
}
if (!board) {
availableBoards.push({ name: 'Unknown', port: boardPort, state });
} else {
const selected = BoardsConfig.Config.sameAs(boardsConfig, board);
availableBoards.push({ ...board, state, selected, port: boardPort });
}
}
if (boardsConfig.selectedBoard && !availableBoards.some(({ selected }) => selected)) {
availableBoards.push({
...boardsConfig.selectedBoard,
port: boardsConfig.selectedPort,
selected: true,
state: AvailableBoard.State.incomplete
});
}
const sortedAvailableBoards = availableBoards.sort(AvailableBoard.COMPARATOR);
let hasChanged = sortedAvailableBoards.length !== currentAvailableBoards.length;
for (let i = 0; !hasChanged && i < sortedAvailableBoards.length; i++) {
hasChanged = AvailableBoard.COMPARATOR(sortedAvailableBoards[i], currentAvailableBoards[i]) !== 0;
}
if (hasChanged) {
this._availableBoards = sortedAvailableBoards;
this.onAvailableBoardsChangedEmitter.fire(this._availableBoards);
}
}
protected async getLastSelectedBoardOnPort(port: Port | string | undefined): Promise<Board | undefined> {
if (!port) {
return undefined;
}
const key = this.getLastSelectedBoardOnPortKey(port);
return this.storageService.getData<Board>(key);
}
protected async saveState(): Promise<void> {
// We save the port with the selected board name/FQBN, to be able to guess a better board name.
// Required when the attached board belongs to a 3rd party boards package, and neither the name, nor
// the FQBN can be retrieved with a `board list` command.
// https://github.com/arduino/arduino-cli/issues/623
const { selectedBoard, selectedPort } = this.boardsConfig;
if (selectedBoard && selectedPort) {
const key = this.getLastSelectedBoardOnPortKey(selectedPort);
await this.storageService.setData(key, selectedBoard);
}
await this.storageService.setData('latest-valid-boards-config', this.latestValidBoardsConfig);
}
protected getLastSelectedBoardOnPortKey(port: Port | string): string {
// TODO: we lose the port's `protocol` info (`serial`, `network`, etc.) here if the `port` is a `string`.
return `last-selected-board-on-port:${typeof port === 'string' ? port : Port.toString(port)}`;
}
protected async loadState(): Promise<void> {
@@ -176,3 +310,63 @@ export class BoardsServiceClientImpl implements BoardsServiceClient, FrontendApp
}
}
/**
* Representation of a ready-to-use board, configured by the user. Not all of the available boards are
* necessarily recognized by the CLI (e.g.: it is a 3rd party board) or correctly configured but ready for `verify`.
* If it has the selected board and a associated port, it can be used for `upload`.
*/
export interface AvailableBoard extends Board {
readonly state: AvailableBoard.State;
readonly selected?: boolean;
readonly port?: Port;
}
export namespace AvailableBoard {
export enum State {
/**
* Retrieved from the CLI via the `board list` command.
*/
'recognized',
/**
* Guessed the name/FQBN of the board from the available board ports (3rd party).
*/
'guessed',
/**
* We do not know anything about this board, probably a 3rd party. The user has not selected a board for this port yet.
*/
'incomplete'
}
export function isWithPort(board: AvailableBoard): board is AvailableBoard & { port: Port } {
return !!board.port;
}
export const COMPARATOR = (left: AvailableBoard, right: AvailableBoard) => {
let result = left.name.localeCompare(right.name);
if (result !== 0) {
return result;
}
if (left.fqbn && right.fqbn) {
result = left.name.localeCompare(right.name);
if (result !== 0) {
return result;
}
}
if (left.port && right.port) {
result = Port.compare(left.port, right.port);
if (result !== 0) {
return result;
}
}
if (!!left.selected && !right.selected) {
return -1;
}
if (!!right.selected && !left.selected) {
return 1;
}
return left.state - right.state;
}
}

View File

@@ -1,10 +1,11 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { CommandRegistry, DisposableCollection } from '@theia/core';
import { BoardsService, Board, AttachedSerialBoard, Port } from '../../common/protocol/boards-service';
import { ArduinoCommands } from '../arduino-commands';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Port } from '../../common/protocol';
import { BoardsConfig } from './boards-config';
import { ArduinoCommands } from '../arduino-commands';
import { BoardsServiceClientImpl, AvailableBoard } from './boards-service-client-impl';
export interface BoardsDropDownListCoords {
readonly top: number;
@@ -16,14 +17,9 @@ export interface BoardsDropDownListCoords {
export namespace BoardsDropDown {
export interface Props {
readonly coords: BoardsDropDownListCoords | 'hidden';
readonly items: Item[];
readonly items: Array<AvailableBoard & { onClick: () => void, port: Port }>;
readonly openBoardsConfig: () => void;
}
export interface Item {
readonly label: string;
readonly selected: boolean;
readonly onClick: () => void;
}
}
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
@@ -51,48 +47,30 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
if (coords === 'hidden') {
return '';
}
items.push({
label: 'Select Other Board & Port',
selected: false,
onClick: () => this.props.openBoardsConfig()
})
return <div className='arduino-boards-dropdown-list'
style={{
position: 'absolute',
...coords
}}>
{items.map(this.renderItem)}
{this.renderItem({
label: 'Select Other Board & Port',
onClick: () => this.props.openBoardsConfig()
})}
{items.map(({ name, port, selected, onClick }) => ({ label: `${name} at ${Port.toString(port)}`, selected, onClick })).map(this.renderItem)}
</div>
}
protected renderItem(item: BoardsDropDown.Item): React.ReactNode {
const { label, selected, onClick } = item;
protected renderItem({ label, selected, onClick }: { label: string, selected?: boolean, onClick: () => void }): React.ReactNode {
return <div key={label} className={`arduino-boards-dropdown-item ${selected ? 'selected' : ''}`} onClick={onClick}>
<div>
{label}
</div>
{selected ? <span className='fa fa-check'/> : ''}
{selected ? <span className='fa fa-check' /> : ''}
</div>
}
}
export namespace BoardsToolBarItem {
export interface Props {
readonly boardService: BoardsService;
readonly boardsServiceClient: BoardsServiceClientImpl;
readonly commands: CommandRegistry;
}
export interface State {
boardsConfig: BoardsConfig.Config;
attachedBoards: Board[];
availablePorts: Port[];
coords: BoardsDropDownListCoords | 'hidden';
}
}
export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props, BoardsToolBarItem.State> {
static TOOLBAR_ID: 'boards-toolbar';
@@ -102,10 +80,9 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
constructor(props: BoardsToolBarItem.Props) {
super(props);
const { availableBoards } = props.boardsServiceClient;
this.state = {
boardsConfig: this.props.boardsServiceClient.boardsConfig,
attachedBoards: [],
availablePorts: [],
availableBoards,
coords: 'hidden'
};
@@ -115,17 +92,7 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
}
componentDidMount() {
const { boardsServiceClient: client, boardService } = this.props;
this.toDispose.pushAll([
client.onBoardsConfigChanged(boardsConfig => this.setState({ boardsConfig })),
client.onBoardsChanged(({ newState }) => this.setState({ attachedBoards: newState.boards, availablePorts: newState.ports }))
]);
Promise.all([
boardService.getAttachedBoards(),
boardService.getAvailablePorts()
]).then(([{boards: attachedBoards}, { ports: availablePorts }]) => {
this.setState({ attachedBoards, availablePorts })
});
this.props.boardsServiceClient.onAvailableBoardsChanged(availableBoards => this.setState({ availableBoards }));
}
componentWillUnmount(): void {
@@ -146,7 +113,7 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
}
});
} else {
this.setState({ coords: 'hidden'});
this.setState({ coords: 'hidden' });
}
}
event.stopPropagation();
@@ -154,41 +121,52 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
};
render(): React.ReactNode {
const { boardsConfig, coords, attachedBoards, availablePorts } = this.state;
const { coords, availableBoards } = this.state;
const boardsConfig = this.props.boardsServiceClient.boardsConfig;
const title = BoardsConfig.Config.toString(boardsConfig, { default: 'no board selected' });
const configuredBoard = attachedBoards
.filter(AttachedSerialBoard.is)
.filter(board => availablePorts.some(port => Port.sameAs(port, board.port)))
.filter(board => BoardsConfig.Config.sameAs(boardsConfig, board)).shift();
const items = attachedBoards.filter(AttachedSerialBoard.is).map(board => ({
label: `${board.name} at ${board.port}`,
selected: configuredBoard === board,
onClick: () => {
this.props.boardsServiceClient.boardsConfig = {
selectedBoard: board,
selectedPort: availablePorts.find(port => Port.sameAs(port, board.port))
}
const decorator = (() => {
const selectedBoard = availableBoards.find(({ selected }) => selected);
if (!selectedBoard || !selectedBoard.port) {
return 'fa fa-times notAttached'
}
}));
if (selectedBoard.state === AvailableBoard.State.guessed) {
return 'fa fa-exclamation-triangle guessed'
}
return ''
})();
return <React.Fragment>
<div className='arduino-boards-toolbar-item-container'>
<div className='arduino-boards-toolbar-item' title={title}>
<div className='inner-container' onClick={this.show}>
<span className={!configuredBoard ? 'fa fa-times notAttached' : ''}/>
<span className={decorator} />
<div className='label noWrapInfo'>
<div className='noWrapInfo noselect'>
{title}
</div>
</div>
<span className='fa fa-caret-down caret'/>
<span className='fa fa-caret-down caret' />
</div>
</div>
</div>
<BoardsDropDown
coords={coords}
items={items}
items={availableBoards.filter(AvailableBoard.isWithPort).map(board => ({
...board,
onClick: () => {
if (board.state === AvailableBoard.State.incomplete) {
this.props.boardsServiceClient.boardsConfig = {
selectedPort: board.port
};
this.openDialog();
} else {
this.props.boardsServiceClient.boardsConfig = {
selectedBoard: board,
selectedPort: board.port
}
}
}
}))}
openBoardsConfig={this.openDialog}>
</BoardsDropDown>
</React.Fragment>;
@@ -200,3 +178,16 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
};
}
export namespace BoardsToolBarItem {
export interface Props {
readonly boardsServiceClient: BoardsServiceClientImpl;
readonly commands: CommandRegistry;
}
export interface State {
availableBoards: AvailableBoard[];
coords: BoardsDropDownListCoords | 'hidden';
}
}

View File

@@ -2,11 +2,11 @@ import { injectable } from 'inversify';
import { MenuModelRegistry } from '@theia/core';
import { BoardsListWidget } from './boards-list-widget';
import { ArduinoMenus } from '../arduino-frontend-contribution';
import { BoardPackage } from '../../common/protocol/boards-service';
import { BoardsPackage } from '../../common/protocol/boards-service';
import { ListWidgetFrontendContribution } from '../components/component-list/list-widget-frontend-contribution';
@injectable()
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardPackage> {
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<BoardsPackage> {
static readonly OPEN_MANAGER = `${BoardsListWidget.WIDGET_ID}:toggle`;

View File

@@ -72,12 +72,7 @@ export class FilterableListContainer<T extends ArduinoComponent> extends React.C
protected search(query: string): void {
const { searchable } = this.props;
searchable.search({ query: query.trim() }).then(result => {
const { items } = result;
this.setState({
items: this.sort(items)
});
});
searchable.search({ query: query.trim() }).then(items => this.setState({ items: this.sort(items) }));
}
protected sort(items: T[]): T[] {
@@ -91,7 +86,7 @@ export class FilterableListContainer<T extends ArduinoComponent> extends React.C
dialog.open();
try {
await installable.install({ item, version });
const { items } = await searchable.search({ query: this.state.filterText });
const items = await searchable.search({ query: this.state.filterText });
this.setState({ items: this.sort(items) });
} finally {
dialog.close();
@@ -113,7 +108,7 @@ export class FilterableListContainer<T extends ArduinoComponent> extends React.C
dialog.open();
try {
await installable.uninstall({ item });
const { items } = await searchable.search({ query: this.state.filterText });
const items = await searchable.search({ query: this.state.filterText });
this.setState({ items: this.sort(items) });
} finally {
dialog.close();

View File

@@ -1,16 +1,17 @@
import { injectable } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { injectable, inject } from 'inversify';
import { ApplicationShell, FrontendApplicationContribution, FrontendApplication, Widget } from '@theia/core/lib/browser';
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
import { EditorWidget } from '@theia/editor/lib/browser';
import { ArduinoShellLayoutRestorer } from './shell/arduino-shell-layout-restorer';
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
import { MainMenuManager } from './menu/main-menu-manager';
import { BoardsListWidget } from './boards/boards-list-widget';
import { LibraryListWidget } from './library/library-list-widget';
import { ArduinoShellLayoutRestorer } from './shell/arduino-shell-layout-restorer';
@injectable()
export class EditorMode implements FrontendApplicationContribution {
readonly menuContentChanged = new Emitter<void>();
@inject(MainMenuManager)
protected readonly mainMenuManager: MainMenuManager;
protected app: FrontendApplication;
@@ -62,6 +63,7 @@ export class EditorMode implements FrontendApplicationContribution {
const oldState = this.compileForDebug;
const newState = !oldState;
window.localStorage.setItem(EditorMode.COMPILE_FOR_DEBUG_KEY, String(newState));
this.mainMenuManager.update();
}
}

View File

@@ -2,7 +2,6 @@ import { injectable, inject, postConstruct } from 'inversify';
import { BaseLanguageClientContribution } from '@theia/languages/lib/browser';
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
import { BoardsConfig } from '../boards/boards-config';
import { Board, BoardPackage } from '../../common/protocol/boards-service';
@injectable()
export class ArduinoLanguageClientContribution extends BaseLanguageClientContribution {
@@ -26,18 +25,6 @@ export class ArduinoLanguageClientContribution extends BaseLanguageClientContrib
@postConstruct()
protected init() {
this.boardsServiceClient.onBoardsConfigChanged(this.selectBoard.bind(this));
const restartIfAffected = (pkg: BoardPackage) => {
if (!this.boardConfig) {
this.restart();
return;
}
const { selectedBoard } = this.boardConfig;
if (selectedBoard && pkg.boards.some(board => Board.sameAs(board, selectedBoard))) {
this.restart();
}
}
this.boardsServiceClient.onBoardInstalled(({ pkg }) => restartIfAffected(pkg));
this.boardsServiceClient.onBoardUninstalled(({ pkg }) => restartIfAffected(pkg));
}
selectBoard(config: BoardsConfig.Config): void {

View File

@@ -0,0 +1,22 @@
import { injectable } from 'inversify';
import { BrowserMainMenuFactory, MenuBarWidget } from '@theia/core/lib/browser/menu/browser-menu-plugin';
import { MainMenuManager } from './main-menu-manager';
@injectable()
export class ArduinoBrowserMainMenuFactory extends BrowserMainMenuFactory implements MainMenuManager {
protected menuBar: MenuBarWidget | undefined;
createMenuBar(): MenuBarWidget {
this.menuBar = super.createMenuBar();
return this.menuBar;
}
update() {
if (this.menuBar) {
this.menuBar.clearMenus();
this.fillMenuBar(this.menuBar);
}
}
}

View File

@@ -1,10 +1,14 @@
import { BrowserMenuBarContribution } from '@theia/core/lib/browser/menu/browser-menu-plugin';
import { ArduinoMenuContribution } from './arduino-menu-contribution';
import { ContainerModule, interfaces } from 'inversify';
import '../../../src/browser/style/browser-menu.css'
import { ContainerModule } from 'inversify';
import { BrowserMenuBarContribution, BrowserMainMenuFactory } from '@theia/core/lib/browser/menu/browser-menu-plugin';
import { MainMenuManager } from './main-menu-manager';
import { ArduinoMenuContribution } from './arduino-menu-contribution';
import { ArduinoBrowserMainMenuFactory } from './arduino-browser-main-menu-factory';
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind) => {
unbind(BrowserMenuBarContribution);
bind(BrowserMenuBarContribution).to(ArduinoMenuContribution).inSingletonScope();
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ArduinoBrowserMainMenuFactory).toSelf().inSingletonScope();
bind(MainMenuManager).toService(ArduinoBrowserMainMenuFactory);
rebind(BrowserMainMenuFactory).toService(ArduinoBrowserMainMenuFactory);
rebind(BrowserMenuBarContribution).to(ArduinoMenuContribution).inSingletonScope();
});

View File

@@ -0,0 +1,8 @@
export const MainMenuManager = Symbol('MainMenuManager');
export interface MainMenuManager {
/**
* Call this method if you have changed the content of the main menu (updated a toggle flag, removed/added new groups or menu items)
* and you want to re-render it from scratch. Works for electron too.
*/
update(): void;
}

View File

@@ -4,7 +4,7 @@ import { MessageService } from '@theia/core/lib/common/message-service';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { MonitorService, MonitorConfig, MonitorError, Status, MonitorReadEvent } from '../../common/protocol/monitor-service';
import { BoardsServiceClientImpl } from '../boards/boards-service-client-impl';
import { Port, Board, BoardsService, AttachedSerialBoard, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
import { Port, Board, BoardsService, AttachedBoardsChangeEvent } from '../../common/protocol/boards-service';
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
import { BoardsConfig } from '../boards/boards-config';
import { MonitorModel } from './monitor-model';
@@ -110,12 +110,12 @@ export class MonitorConnection {
}
});
this.boardsServiceClient.onBoardsConfigChanged(this.handleBoardConfigChange.bind(this));
this.boardsServiceClient.onBoardsChanged(event => {
this.boardsServiceClient.onAttachedBoardsChanged(event => {
if (this.autoConnect && this.connected) {
const { boardsConfig } = this.boardsServiceClient;
if (this.boardsServiceClient.canUploadTo(boardsConfig, { silent: false })) {
const { attached } = AttachedBoardsChangeEvent.diff(event);
if (attached.boards.some(board => AttachedSerialBoard.is(board) && BoardsConfig.Config.sameAs(boardsConfig, board))) {
if (attached.boards.some(board => !!board.port && BoardsConfig.Config.sameAs(boardsConfig, board))) {
const { selectedBoard: board, selectedPort: port } = boardsConfig;
const { baudRate } = this.monitorModel;
this.disconnect()
@@ -225,7 +225,7 @@ export class MonitorConnection {
if (this.boardsServiceClient.canUploadTo(boardsConfig, { silent: false })) {
// Instead of calling `getAttachedBoards` and filtering for `AttachedSerialBoard` we have to check the available ports.
// The connected board might be unknown. See: https://github.com/arduino/arduino-pro-ide/issues/127#issuecomment-563251881
this.boardsService.getAvailablePorts().then(({ ports }) => {
this.boardsService.getAvailablePorts().then(ports => {
if (ports.some(port => Port.equals(port, boardsConfig.selectedPort))) {
new Promise<void>(resolve => {
// First, disconnect if connected.

View File

@@ -178,7 +178,7 @@ export class MonitorWidget extends ReactWidget {
this.monitorModel.lineEnding = option.value;
}
protected readonly onChangeBaudRate = async (option: SelectOption<MonitorConfig.BaudRate>) => {
protected readonly onChangeBaudRate = (option: SelectOption<MonitorConfig.BaudRate>) => {
this.monitorModel.baudRate = option.value;
}

View File

@@ -97,7 +97,7 @@ div#select-board-dialog .selectBoardContainer .body .list .item.selected i {
margin-left: auto;
}
#select-board-dialog .selectBoardContainer .body .list .item .detail {
#select-board-dialog .selectBoardContainer .body .list .item .details {
font-size: var(--theia-ui-font-size1);
opacity: var(--theia-mod-disabled-opacity);
width: 155px; /* used heuristics for the calculation */
@@ -169,6 +169,13 @@ button.theia-button.main {
margin: 0 5px;
}
.arduino-boards-toolbar-item-container .arduino-boards-toolbar-item .inner-container .guessed {
width: 10px;
height: 10px;
color: var(--theia-warningBackground);
margin: 0 5px;
}
.arduino-boards-toolbar-item-container {
display: flex;
align-items: center;

View File

@@ -46,7 +46,7 @@
See above: `.filterable-list-container .items-container > div:nth-child(odd|event)`.
We have to increase `z-index` of the scroll-bar thumb. Otherwise, the thumb is not visible.
https://github.com/arduino/arduino-pro-ide/issues/82 */
.arduino-list-widget .ps__rail-y > .ps__thumb-y {
.arduino-list-widget .filterable-list-container .items-container .ps__rail-y {
z-index: 1;
}