Implemented the board discovery with polling.

Signed-off-by: Akos Kitta <kittaakos@typefox.io>
This commit is contained in:
Akos Kitta
2019-07-26 08:12:34 +02:00
parent db78c8ac2d
commit b78ddbeb64
23 changed files with 855 additions and 874 deletions

View File

@@ -0,0 +1,55 @@
import * as React from 'react';
import { injectable, inject } from 'inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { ReactWidget, Message } from '@theia/core/lib/browser';
import { BoardsService } from '../../common/protocol/boards-service';
import { BoardsConfig } from './boards-config';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
@injectable()
export class BoardsConfigDialogWidget extends ReactWidget {
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected readonly onBoardConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
readonly onBoardConfigChanged = this.onBoardConfigChangedEmitter.event;
protected focusNode: HTMLElement | undefined;
constructor() {
super();
this.id = 'select-board-dialog';
}
protected fireConfigChanged = (config: BoardsConfig.Config) => {
this.onBoardConfigChangedEmitter.fire(config);
}
protected setFocusNode = (element: HTMLElement | undefined) => {
this.focusNode = element;
}
protected render(): React.ReactNode {
return <div className='selectBoardContainer'>
<BoardsConfig
boardsService={this.boardsService}
boardsServiceClient={this.boardsServiceClient}
onConfigChange={this.fireConfigChanged}
onFocusNodeSet={this.setFocusNode} />
</div>;
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
if (this.focusNode instanceof HTMLInputElement) {
this.focusNode.select();
}
(this.focusNode || this.node).focus();
}
}

View File

@@ -0,0 +1,113 @@
import { injectable, inject, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { AbstractDialog, DialogProps, Widget, DialogError } from '@theia/core/lib/browser';
import { BoardsService } from '../../common/protocol/boards-service';
import { BoardsConfig } from './boards-config';
import { BoardsConfigDialogWidget } from './boards-config-dialog-widget';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
@injectable()
export class BoardsConfigDialogProps extends DialogProps {
}
@injectable()
export class BoardsConfigDialog extends AbstractDialog<BoardsConfig.Config> {
@inject(BoardsConfigDialogWidget)
protected readonly widget: BoardsConfigDialogWidget;
@inject(BoardsService)
protected readonly boardService: BoardsService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
protected config: BoardsConfig.Config = {};
constructor(@inject(BoardsConfigDialogProps) protected readonly props: BoardsConfigDialogProps) {
super(props);
this.contentNode.classList.add('select-board-dialog');
this.contentNode.appendChild(this.createDescription());
this.appendCloseButton('CANCEL');
this.appendAcceptButton('OK');
}
@postConstruct()
protected init(): void {
this.toDispose.push(this.boardsServiceClient.onBoardsConfigChanged(config => {
this.config = config;
this.update();
}));
}
protected createDescription(): HTMLElement {
const head = document.createElement('div');
head.classList.add('head');
const title = document.createElement('div');
title.textContent = 'Select Other Board & Port';
title.classList.add('title');
head.appendChild(title);
const text = document.createElement('div');
text.classList.add('text');
head.appendChild(text);
for (const paragraph of [
'Select both a Board and a Port if you want to upload a sketch.',
'If you only select a Board you will be able just to compile, but not to upload your sketch.'
]) {
const p = document.createElement('p');
p.textContent = paragraph;
text.appendChild(p);
}
return head;
}
protected onAfterAttach(msg: Message): void {
if (this.widget.isAttached) {
Widget.detach(this.widget);
}
Widget.attach(this.widget, this.contentNode);
this.toDisposeOnDetach.push(this.widget.onBoardConfigChanged(config => {
this.config = config;
this.update();
}));
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message) {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLTextAreaElement) {
return false;
}
}
protected isValid(value: BoardsConfig.Config): DialogError {
if (!value.selectedBoard) {
if (value.selectedPort) {
return 'Please pick a board connected to the port you have selected.';
}
return false;
}
return '';
}
get value(): BoardsConfig.Config {
return this.config;
}
}

View File

@@ -0,0 +1,240 @@
import * as React from 'react';
import { DisposableCollection } from '@theia/core';
import { BoardsService, Board, AttachedSerialBoard } from '../../common/protocol/boards-service';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
export namespace BoardsConfig {
export interface Config {
selectedBoard?: Board;
selectedPort?: string;
}
export interface Props {
readonly boardsService: BoardsService;
readonly boardsServiceClient: BoardsServiceClientImpl;
readonly onConfigChange: (config: Config) => void;
readonly onFocusNodeSet: (element: HTMLElement | undefined) => void;
}
export interface State extends Config {
searchResults: Board[];
knownPorts: string[];
}
}
export abstract class Item<T> extends React.Component<{
item: T,
name: string,
selected: boolean,
onClick: (item: T) => void,
missing?: boolean }> {
render(): React.ReactNode {
const { selected, name, missing } = this.props;
const classNames = ['item'];
if (selected) {
classNames.push('selected');
}
if (missing === true) {
classNames.push('missing')
}
return <div onClick={this.onClick} className={classNames.join(' ')}>
{name}
{selected ? <i className='fa fa-check'></i> : ''}
</div>;
}
protected onClick = () => {
this.props.onClick(this.props.item);
}
}
export class BoardsConfig extends React.Component<BoardsConfig.Props, BoardsConfig.State> {
protected toDispose = new DisposableCollection();
constructor(props: BoardsConfig.Props) {
super(props);
const { boardsConfig } = props.boardsServiceClient;
this.state = {
searchResults: [],
knownPorts: [],
...boardsConfig
}
}
componentDidMount() {
this.updateBoards();
this.props.boardsService.getAttachedBoards().then(({ boards }) => this.updatePorts(boards));
const { boardsServiceClient: client } = this.props;
this.toDispose.pushAll([
client.onBoardsChanged(event => this.updatePorts(event.newState.boards)),
client.onBoardsConfigChanged(({ selectedBoard, selectedPort }) => {
this.setState({ selectedBoard, selectedPort }, () => this.fireConfigChanged());
})
]);
}
componentWillUnmount(): void {
this.toDispose.dispose();
}
protected fireConfigChanged() {
const { selectedBoard, selectedPort } = this.state;
this.props.onConfigChange({ selectedBoard, selectedPort });
}
protected updateBoards = (eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = '') => {
const query = (typeof eventOrQuery === 'string'
? eventOrQuery
: eventOrQuery.target.value.toLowerCase()
).trim();
this.queryBoards({ query }).then(({ searchResults }) => this.setState({ searchResults }));
}
protected updatePorts = (boards: Board[] = []) => {
this.queryPorts(Promise.resolve({ boards })).then(({ knownPorts }) => {
let { selectedPort } = this.state;
if (!!selectedPort && knownPorts.indexOf(selectedPort) === -1) {
selectedPort = undefined;
}
this.setState({ knownPorts, selectedPort }, () => this.fireConfigChanged());
});
}
protected queryBoards = (options: { query?: string } = {}): Promise<{ searchResults: Board[] }> => {
const { boardsService } = this.props;
const query = (options.query || '').toLocaleLowerCase();
return new Promise<{ searchResults: Board[] }>(resolve => {
boardsService.search(options)
.then(({ items }) => items
.map(item => item.boards)
.reduce((acc, curr) => acc.concat(curr), [])
.filter(board => board.name.toLocaleLowerCase().indexOf(query) !== -1)
.sort(Board.compare))
.then(searchResults => resolve({ searchResults }));
});
}
protected get attachedBoards(): Promise<{ boards: Board[] }> {
return this.props.boardsService.getAttachedBoards();
}
protected queryPorts = (attachedBoards: Promise<{ boards: Board[] }> = this.attachedBoards) => {
return new Promise<{ knownPorts: string[] }>(resolve => {
attachedBoards
.then(({ boards }) => boards
.filter(AttachedSerialBoard.is)
.map(({ port }) => port)
.sort())
.then(knownPorts => resolve({ knownPorts }));
});
}
protected selectPort = (selectedPort: string | undefined) => {
this.setState({ selectedPort }, () => this.fireConfigChanged());
}
protected selectBoard = (selectedBoard: Board | undefined) => {
this.setState({ selectedBoard }, () => this.fireConfigChanged());
}
protected focusNodeSet = (element: HTMLElement | null) => {
this.props.onFocusNodeSet(element || undefined);
}
render(): React.ReactNode {
return <div className='body'>
{this.renderContainer('boards', this.renderBoards.bind(this))}
{this.renderContainer('ports', this.renderPorts.bind(this))}
</div>;
}
protected renderContainer(title: string, contentRenderer: () => React.ReactNode): React.ReactNode {
return <div className='container'>
<div className='content'>
<div className='title'>
{title}
</div>
{contentRenderer()}
</div>
</div>;
}
protected renderBoards(): React.ReactNode {
const { selectedBoard } = this.state;
return <React.Fragment>
<div className='search'>
<input type='search' 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, index) => <Item<Board>
key={`${board.name}-${index}`}
item={board}
name={board.name}
selected={!!selectedBoard && Board.equals(board, selectedBoard)}
onClick={this.selectBoard}
missing={!Board.installed(board)}
/>)}
</div>
</React.Fragment>;
}
protected renderPorts(): React.ReactNode {
return !this.state.knownPorts.length ?
(
<div className='loading noselect'>
No ports discovered
</div>
) :
(
<div className='ports list'>
{this.state.knownPorts.map(port => <Item<string>
key={port}
item={port}
name={port}
selected={this.state.selectedPort === port}
onClick={this.selectPort}
/>)}
</div>
);
}
}
export namespace BoardsConfig {
export namespace Config {
export function sameAs(config: Config, other: Config | AttachedSerialBoard): boolean {
const { selectedBoard, selectedPort } = config;
if (AttachedSerialBoard.is(other)) {
return !!selectedBoard
&& Board.equals(other, selectedBoard)
&& selectedPort === other.port;
}
return sameAs(config, other);
}
export function equals(left: Config, right: Config): boolean {
return left.selectedBoard === right.selectedBoard
&& left.selectedPort === right.selectedPort;
}
export function toString(config: Config, options: { default: string } = { default: '' }): string {
const { selectedBoard, selectedPort: port } = config;
if (!selectedBoard) {
return options.default;
}
const { name } = selectedBoard;
return `${name}${port ? ' at ' + port : ''}`;
}
}
}

View File

@@ -13,4 +13,4 @@ export class BoardsListWidget extends ListWidget {
}
}
}
}

View File

@@ -0,0 +1,71 @@
import { injectable, inject, postConstruct } from 'inversify';
import { Emitter, ILogger } from '@theia/core';
import { BoardsServiceClient, AttachedBoardsChangeEvent, BoardInstalledEvent, AttachedSerialBoard } from '../../common/protocol/boards-service';
import { BoardsConfig } from './boards-config';
import { LocalStorageService } from '@theia/core/lib/browser';
@injectable()
export class BoardsServiceClientImpl implements BoardsServiceClient {
@inject(ILogger)
protected logger: ILogger;
@inject(LocalStorageService)
protected storageService: LocalStorageService;
protected readonly onAttachedBoardsChangedEmitter = new Emitter<AttachedBoardsChangeEvent>();
protected readonly onBoardInstalledEmitter = new Emitter<BoardInstalledEvent>();
protected readonly onSelectedBoardsConfigChangedEmitter = new Emitter<BoardsConfig.Config>();
protected _boardsConfig: BoardsConfig.Config = {};
readonly onBoardsChanged = this.onAttachedBoardsChangedEmitter.event;
readonly onBoardInstalled = this.onBoardInstalledEmitter.event;
readonly onBoardsConfigChanged = this.onSelectedBoardsConfigChangedEmitter.event;
@postConstruct()
protected init(): void {
this.loadState();
}
notifyAttachedBoardsChanged(event: AttachedBoardsChangeEvent): void {
this.logger.info('Attached boards changed: ', JSON.stringify(event));
const { boards } = event.newState;
const { selectedPort, selectedBoard } = this.boardsConfig;
this.onAttachedBoardsChangedEmitter.fire(event);
// Dynamically unset the port if there is not corresponding attached boards for it.
if (!!selectedPort && boards.filter(AttachedSerialBoard.is).map(({ port }) => port).indexOf(selectedPort) === -1) {
this.boardsConfig = {
selectedBoard,
selectedPort: undefined
};
}
}
notifyBoardInstalled(event: BoardInstalledEvent): void {
this.logger.info('Board installed: ', JSON.stringify(event));
this.onBoardInstalledEmitter.fire(event);
}
set boardsConfig(config: BoardsConfig.Config) {
this.logger.info('Board config changed: ', JSON.stringify(config));
this._boardsConfig = config;
this.saveState().then(() => this.onSelectedBoardsConfigChangedEmitter.fire(this._boardsConfig));
}
get boardsConfig(): BoardsConfig.Config {
return this._boardsConfig;
}
protected saveState(): Promise<void> {
return this.storageService.setData('boards-config', this.boardsConfig);
}
protected async loadState(): Promise<void> {
const boardsConfig = await this.storageService.getData<BoardsConfig.Config>('boards-config');
if (boardsConfig) {
this.boardsConfig = boardsConfig;
}
}
}

View File

@@ -1,61 +1,42 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { CommandRegistry, DisposableCollection } from '@theia/core';
import { BoardsService, Board, AttachedSerialBoard } from '../../common/protocol/boards-service';
import { ContextMenuRenderer, StatusBar, StatusBarAlignment } from '@theia/core/lib/browser';
import { BoardsNotificationService } from '../boards-notification-service';
import { Command, CommandRegistry } from '@theia/core';
import { ArduinoCommands } from '../arduino-commands';
import ReactDOM = require('react-dom');
import { BoardsServiceClientImpl } from './boards-service-client-impl';
import { BoardsConfig } from './boards-config';
export interface BoardsDropdownItem {
label: string;
commandExecutor: () => void;
isSelected: () => boolean;
}
export interface BoardsDropDownListCoord {
top: number;
left: number;
width: number;
paddingTop: number;
}
export namespace BoardsDropdownItemComponent {
export interface Props {
label: string;
onClick: () => void;
isSelected: boolean;
}
}
export class BoardsDropdownItemComponent extends React.Component<BoardsDropdownItemComponent.Props> {
render() {
return <div className={`arduino-boards-dropdown-item ${this.props.isSelected ? 'selected' : ''}`} onClick={this.props.onClick}>
<div>{this.props.label}</div>
{this.props.isSelected ? <span className='fa fa-check'></span> : ''}
</div>;
}
export interface BoardsDropDownListCoords {
readonly top: number;
readonly left: number;
readonly width: number;
readonly paddingTop: number;
}
export namespace BoardsDropDown {
export interface Props {
readonly coords: BoardsDropDownListCoord;
readonly isOpen: boolean;
readonly dropDownItems: BoardsDropdownItem[];
readonly openDialog: () => void;
readonly coords: BoardsDropDownListCoords | 'hidden';
readonly items: Item[];
readonly openBoardsConfig: () => void;
}
export interface Item {
readonly label: string;
readonly selected: boolean;
readonly onClick: () => void;
}
}
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
protected dropdownId: string = 'boards-dropdown-container';
protected dropdownElement: HTMLElement;
constructor(props: BoardsDropDown.Props) {
super(props);
let list = document.getElementById(this.dropdownId);
let list = document.getElementById('boards-dropdown-container');
if (!list) {
list = document.createElement('div');
list.id = this.dropdownId;
list.id = 'boards-dropdown-container';
document.body.appendChild(list);
this.dropdownElement = list;
}
@@ -65,179 +46,149 @@ export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
}
renderNode(): React.ReactNode {
if (this.props.isOpen) {
return <div className='arduino-boards-dropdown-list'
style={{
position: 'absolute',
top: this.props.coords.top,
left: this.props.coords.left,
width: this.props.coords.width,
paddingTop: this.props.coords.paddingTop
}}>
{
this.props.dropDownItems.map(item => {
return <React.Fragment key={item.label}>
<BoardsDropdownItemComponent isSelected={item.isSelected()} label={item.label} onClick={item.commandExecutor}></BoardsDropdownItemComponent>
</React.Fragment>;
})
}
<BoardsDropdownItemComponent isSelected={false} label={'Select Other Board & Port'} onClick={this.props.openDialog}></BoardsDropdownItemComponent>
</div>
} else {
protected renderNode(): React.ReactNode {
const { coords, items } = this.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)}
</div>
}
protected renderItem(item: BoardsDropDown.Item): React.ReactNode {
const { label, selected, onClick } = item;
return <div key={label} className={`arduino-boards-dropdown-item ${selected ? 'selected' : ''}`} onClick={onClick}>
<div>
{label}
</div>
{selected ? <span className='fa fa-check'/> : ''}
</div>
}
}
export namespace BoardsToolBarItem {
export interface Props {
readonly contextMenuRenderer: ContextMenuRenderer;
readonly boardsNotificationService: BoardsNotificationService;
readonly boardService: BoardsService;
readonly boardsServiceClient: BoardsServiceClientImpl;
readonly commands: CommandRegistry;
readonly statusBar: StatusBar;
}
export interface State {
selectedBoard?: Board;
selectedIsAttached: boolean;
boardItems: BoardsDropdownItem[];
isOpen: boolean;
boardsConfig: BoardsConfig.Config;
attachedBoards: Board[];
coords: BoardsDropDownListCoords | 'hidden';
}
}
export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props, BoardsToolBarItem.State> {
protected attachedBoards: Board[];
protected dropDownListCoord: BoardsDropDownListCoord;
static TOOLBAR_ID: 'boards-toolbar';
protected readonly toDispose: DisposableCollection = new DisposableCollection();
constructor(props: BoardsToolBarItem.Props) {
super(props);
this.state = {
selectedBoard: undefined,
selectedIsAttached: true,
boardItems: [],
isOpen: false
boardsConfig: this.props.boardsServiceClient.boardsConfig,
attachedBoards: [],
coords: 'hidden'
};
document.addEventListener('click', () => {
this.setState({ isOpen: false });
this.setState({ coords: 'hidden' });
});
}
componentDidMount() {
this.setAttachedBoards();
}
setSelectedBoard(board: Board) {
if (this.attachedBoards && this.attachedBoards.length) {
this.setState({ selectedIsAttached: !!this.attachedBoards.find(attachedBoard => attachedBoard.name === board.name) });
}
this.setState({ selectedBoard: board });
}
protected async setAttachedBoards() {
this.props.boardService.getAttachedBoards().then(attachedBoards => {
this.attachedBoards = attachedBoards.boards;
if (this.attachedBoards.length) {
this.createBoardDropdownItems();
this.props.boardService.selectBoard(this.attachedBoards[0]).then(() => this.setSelectedBoard(this.attachedBoards[0]));
}
})
}
protected createBoardDropdownItems() {
const boardItems: BoardsDropdownItem[] = [];
this.attachedBoards.forEach(board => {
const { commands } = this.props;
const port = this.getPort(board);
const command: Command = {
id: 'selectBoard' + port
}
commands.registerCommand(command, {
execute: () => {
commands.executeCommand(ArduinoCommands.SELECT_BOARD.id, board);
this.setState({ isOpen: false, selectedBoard: board });
}
});
boardItems.push({
commandExecutor: () => commands.executeCommand(command.id),
label: board.name + ' at ' + port,
isSelected: () => this.doIsSelectedBoard(board)
});
const { boardsServiceClient: client, boardService } = this.props;
this.toDispose.pushAll([
client.onBoardsConfigChanged(boardsConfig => this.setState({ boardsConfig })),
client.onBoardsChanged(({ newState }) => this.setState({ attachedBoards: newState.boards }))
]);
boardService.getAttachedBoards().then(({ boards: attachedBoards }) => {
this.setState({ attachedBoards })
});
this.setState({ boardItems });
}
protected doIsSelectedBoard = (board: Board) => this.isSelectedBoard(board);
protected isSelectedBoard(board: Board): boolean {
return AttachedSerialBoard.is(board) &&
!!this.state.selectedBoard &&
AttachedSerialBoard.is(this.state.selectedBoard) &&
board.port === this.state.selectedBoard.port &&
board.fqbn === this.state.selectedBoard.fqbn;
componentWillUnmount(): void {
this.toDispose.dispose();
}
protected getPort(board: Board): string {
if (AttachedSerialBoard.is(board)) {
return board.port;
protected readonly show = (event: React.MouseEvent<HTMLElement>) => {
const { currentTarget: element } = event;
if (element instanceof HTMLElement) {
if (this.state.coords === 'hidden') {
const rect = element.getBoundingClientRect();
this.setState({
coords: {
top: rect.top,
left: rect.left,
width: rect.width,
paddingTop: rect.height
}
});
} else {
this.setState({ coords: 'hidden'});
}
}
return '';
}
protected readonly doShowSelectBoardsMenu = (event: React.MouseEvent<HTMLElement>) => {
this.showSelectBoardsMenu(event);
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
};
protected showSelectBoardsMenu(event: React.MouseEvent<HTMLElement>) {
const el = (event.currentTarget as HTMLElement);
if (el) {
this.dropDownListCoord = {
top: el.getBoundingClientRect().top,
left: el.getBoundingClientRect().left,
paddingTop: el.getBoundingClientRect().height,
width: el.getBoundingClientRect().width
}
this.setState({ isOpen: !this.state.isOpen });
}
}
render(): React.ReactNode {
const selectedBoard = this.state.selectedBoard;
const port = selectedBoard ? this.getPort(selectedBoard) : undefined;
const boardTxt = selectedBoard && `${selectedBoard.name}${port ? ' at ' + port : ''}` || '';
this.props.statusBar.setElement('arduino-selected-board', {
alignment: StatusBarAlignment.RIGHT,
text: boardTxt
});
const { boardsConfig, coords, attachedBoards } = this.state;
const boardsConfigText = BoardsConfig.Config.toString(boardsConfig, { default: 'no board selected' });
const configuredBoard = attachedBoards
.filter(AttachedSerialBoard.is)
.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: board.port
}
}));
return <React.Fragment>
<div className='arduino-boards-toolbar-item-container'>
<div className='arduino-boards-toolbar-item' title={boardTxt}>
<div className='inner-container' onClick={this.doShowSelectBoardsMenu}>
<span className={!selectedBoard || !this.state.selectedIsAttached ? 'fa fa-times notAttached' : ''}></span>
<div className='arduino-boards-toolbar-item' title={boardsConfigText}>
<div className='inner-container' onClick={this.show}>
<span className={!configuredBoard ? 'fa fa-times notAttached' : ''}/>
<div className='label noWrapInfo'>
<div className='noWrapInfo noselect'>
{selectedBoard ? boardTxt : 'no board selected'}
{boardsConfigText}
</div>
</div>
<span className='fa fa-caret-down caret'></span>
<span className='fa fa-caret-down caret'/>
</div>
</div>
</div>
<BoardsDropDown
isOpen={this.state.isOpen}
coords={this.dropDownListCoord}
dropDownItems={this.state.boardItems}
openDialog={this.openDialog}>
coords={coords}
items={items}
openBoardsConfig={this.openDialog}>
</BoardsDropDown>
</React.Fragment>;
}
protected openDialog = () => {
this.props.commands.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id);
this.setState({ isOpen: false });
this.setState({ coords: 'hidden' });
};
}
}

View File

@@ -3,10 +3,9 @@ import { inject, injectable, postConstruct } from 'inversify';
import { Message } from '@phosphor/messaging';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import { BoardsService } from '../../common/protocol/boards-service';
import { FilterableListContainer } from '../components/component-list/filterable-list-container';
import { BoardsService, Board, BoardPackage } from '../../common/protocol/boards-service';
import { BoardsNotificationService } from '../boards-notification-service';
import { LibraryService } from '../../common/protocol/library-service';
import { BoardsServiceClientImpl } from './boards-service-client-impl';
@injectable()
export abstract class ListWidget extends ReactWidget {
@@ -17,8 +16,8 @@ export abstract class ListWidget extends ReactWidget {
@inject(WindowService)
protected readonly windowService: WindowService;
@inject(BoardsNotificationService)
protected readonly boardsNotificationService: BoardsNotificationService;
@inject(BoardsServiceClientImpl)
protected readonly boardsServiceClient: BoardsServiceClientImpl;
constructor() {
super();
@@ -51,19 +50,8 @@ export abstract class ListWidget extends ReactWidget {
}
render(): React.ReactNode {
const boardsServiceDelegate = this.boardsService;
const boardsService: BoardsService = {
getAttachedBoards: () => boardsServiceDelegate.getAttachedBoards(),
selectBoard: (board: Board) => boardsServiceDelegate.selectBoard(board),
getSelectBoard: () => boardsServiceDelegate.getSelectBoard(),
search: (options: { query?: string, props?: LibraryService.Search.Props }) => boardsServiceDelegate.search(options),
install: async (item: BoardPackage) => {
await boardsServiceDelegate.install(item);
this.boardsNotificationService.notifyBoardsInstalled();
}
}
return <FilterableListContainer
service={boardsService}
service={this.boardsService}
windowService={this.windowService}
/>;
}
@@ -85,4 +73,4 @@ export namespace ListWidget {
export const LIST_WIDGET_CLASS = 'arduino-list-widget'
}
}
}

View File

@@ -1,305 +0,0 @@
import * as React from 'react';
import { ReactWidget } from '@theia/core/lib/browser';
import { injectable, inject } from 'inversify';
import { BoardsService, Board, BoardPackage, AttachedSerialBoard } from '../../common/protocol/boards-service';
import { BoardsNotificationService } from '../boards-notification-service';
import { Emitter, Event } from '@theia/core';
export interface BoardAndPortSelection {
board?: Board;
port?: string;
}
export namespace BoardAndPortSelectableItem {
export interface Props {
item: BoardAndPortSelection,
selected: boolean,
onSelect: (selection: BoardAndPortSelection) => void
}
}
export class BoardAndPortSelectableItem extends React.Component<BoardAndPortSelectableItem.Props> {
render(): React.ReactNode {
if (this.props.item.board || this.props.item.port) {
return <div onClick={this.select} className={`item ${this.props.selected ? 'selected' : ''}`}>
{this.props.item.board ? this.props.item.board.name : this.props.item.port}
{this.props.selected ? <i className='fa fa-check'></i> : ''}
</div>;
}
}
protected readonly select = (() => {
this.props.onSelect({ board: this.props.item.board, port: this.props.item.port })
}).bind(this);
}
export namespace BoardAndPortSelectionList {
export interface Props {
type: 'boards' | 'ports';
list: BoardAndPortSelection[];
onSelect: (selection: BoardAndPortSelection) => void;
}
export interface State {
selection: BoardAndPortSelection
}
}
export class BoardAndPortSelectionList extends React.Component<BoardAndPortSelectionList.Props, BoardAndPortSelectionList.State> {
constructor(props: BoardAndPortSelectionList.Props) {
super(props);
this.state = {
selection: {}
}
}
reset(): void {
this.setState({ selection: {} });
}
render(): React.ReactNode {
return <div className={`${this.props.type} list`}>
{this.props.list.map((item, idx) => <BoardAndPortSelectableItem
key={(item.board ? item.board.name : item.port || '') + idx}
onSelect={this.doSelect}
item={item}
selected={this.isSelectedItem(item)}
/>)}
</div>
}
protected readonly doSelect = (boardAndPortSelection: BoardAndPortSelection) => {
this.setState({ selection: boardAndPortSelection });
this.props.onSelect(boardAndPortSelection);
}
protected readonly isSelectedItem = ((item: BoardAndPortSelection) => {
if (this.state.selection.board) {
return (this.state.selection.board === item.board);
} else if (this.state.selection.port) {
return (this.state.selection.port === item.port);
}
return false;
});
protected readonly isSelectedPort = ((port: string) => {
return (this.state.selection.port && this.state.selection.port === port) || false;
});
}
export namespace BoardAndPortSelectionComponent {
export interface Props {
boardsService: BoardsService;
onSelect: (selection: BoardAndPortSelection) => void;
}
export interface State {
boards: Board[];
ports: string[];
selection: BoardAndPortSelection;
}
}
export class BoardAndPortSelectionComponent extends React.Component<BoardAndPortSelectionComponent.Props, BoardAndPortSelectionComponent.State> {
protected allBoards: Board[] = [];
protected boardListComponent: BoardAndPortSelectionList | null;
protected portListComponent: BoardAndPortSelectionList | null;
constructor(props: BoardAndPortSelectionComponent.Props) {
super(props);
this.state = {
boards: [],
ports: [],
selection: {}
}
}
componentDidMount() {
this.searchAvailableBoards();
this.setPorts();
}
reset(): void {
if (this.boardListComponent) {
this.boardListComponent.reset();
}
if (this.portListComponent) {
this.portListComponent.reset();
}
this.setState({ selection: {} });
}
render(): React.ReactNode {
return <React.Fragment>
<div className='body'>
<div className='left container'>
<div className='content'>
<div className='title'>
BOARDS
</div>
<div className='search'>
<input type='search' placeholder='SEARCH BOARD' onChange={this.doFilter} />
<i className='fa fa-search'></i>
</div>
<BoardAndPortSelectionList
ref={ref => { this.boardListComponent = ref }}
type='boards'
onSelect={this.doSelect}
list={this.state.boards.map<BoardAndPortSelection>(board => ({ board }))} />
</div>
</div>
<div className='right container'>
<div className='content'>
<div className='title'>
PORTS
</div>
{
this.state.ports.length ?
<BoardAndPortSelectionList
ref={ref => { this.portListComponent = ref }}
type='ports'
onSelect={this.doSelect}
list={this.state.ports.map<BoardAndPortSelection>(port => ({ port }))} /> : 'loading ports...'
}
</div>
</div>
</div>
</React.Fragment>
}
protected sort(items: Board[]): Board[] {
return items.sort((a, b) => {
if (a.name < b.name) {
return -1;
} else if (a.name === b.name) {
return 0;
} else {
return 1;
}
});
}
protected readonly doSelect = (boardAndPortSelection: BoardAndPortSelection) => {
const selection = this.state.selection;
if (boardAndPortSelection.board) {
selection.board = boardAndPortSelection.board;
}
if (boardAndPortSelection.port) {
selection.port = boardAndPortSelection.port;
}
this.setState({ selection });
this.props.onSelect(this.state.selection);
}
protected readonly doFilter = (event: React.ChangeEvent<HTMLInputElement>) => {
const boards = this.allBoards.filter(board => board.name.toLowerCase().indexOf(event.target.value.toLowerCase()) >= 0);
this.setState({ boards })
}
protected async searchAvailableBoards() {
const boardPkg = await this.props.boardsService.search({});
const boards = [].concat.apply([], boardPkg.items.map<Board[]>(item => item.boards)) as Board[];
this.allBoards = this.sort(boards);
this.setState({ boards: this.allBoards });
}
protected async setPorts() {
const ports: string[] = [];
const { boards } = await this.props.boardsService.getAttachedBoards();
boards.forEach(board => {
if (AttachedSerialBoard.is(board)) {
ports.push(board.port);
}
});
this.setState({ ports });
}
}
@injectable()
export class SelectBoardDialogWidget extends ReactWidget {
@inject(BoardsService)
protected readonly boardsService: BoardsService;
@inject(BoardsNotificationService)
protected readonly boardsNotificationService: BoardsNotificationService;
protected readonly onChangedEmitter = new Emitter<BoardAndPortSelection>();
protected boardAndPortSelectionComponent: BoardAndPortSelectionComponent | null;
protected attachedBoards: Promise<{ boards: Board[] }>;
boardAndPort: BoardAndPortSelection = {};
constructor() {
super();
this.id = 'select-board-dialog';
this.toDispose.push(this.onChangedEmitter);
}
get onChanged(): Event<BoardAndPortSelection> {
return this.onChangedEmitter.event;
}
reset(): void {
if (this.boardAndPortSelectionComponent) {
this.boardAndPortSelectionComponent.reset();
}
this.boardAndPort = {};
}
setAttachedBoards(attachedBoards: Promise<{ boards: Board[] }>): void {
this.attachedBoards = attachedBoards;
}
protected fireChanged(boardAndPort: BoardAndPortSelection): void {
this.onChangedEmitter.fire(boardAndPort);
}
protected render(): React.ReactNode {
let content: React.ReactNode;
const boardsServiceDelegate = this.boardsService;
const attachedBoards = this.attachedBoards;
const boardsService: BoardsService = {
getAttachedBoards: () => attachedBoards,
selectBoard: (board: Board) => boardsServiceDelegate.selectBoard(board),
getSelectBoard: () => boardsServiceDelegate.getSelectBoard(),
search: (options: { query?: string }) => boardsServiceDelegate.search(options),
install: async (item: BoardPackage) => {
await boardsServiceDelegate.install(item);
this.boardsNotificationService.notifyBoardsInstalled();
}
}
content = <React.Fragment>
<div className='selectBoardContainer'>
<div className='head'>
<div className='title'>
Select Other Board &amp; Port
</div>
<div className='text'>
<p>Select both a BOARD and a PORT if you want to upload a sketch.</p>
<p>If you only select a BOARD you will be able just to compile,</p>
<p>but not to upload your sketch.</p>
</div>
</div>
<BoardAndPortSelectionComponent
ref={ref => this.boardAndPortSelectionComponent = ref}
boardsService={boardsService}
onSelect={this.onSelect} />
</div>
</React.Fragment>
return content;
}
protected readonly onSelect = (selection: BoardAndPortSelection) => { this.doOnSelect(selection) };
protected doOnSelect(selection: BoardAndPortSelection) {
this.boardAndPort = selection;
this.fireChanged(this.boardAndPort);
}
}

View File

@@ -1,113 +0,0 @@
import { AbstractDialog, DialogProps, Widget, Panel, DialogError } from '@theia/core/lib/browser';
import { injectable, inject } from 'inversify';
import { SelectBoardDialogWidget, BoardAndPortSelection } from './select-board-dialog-widget';
import { Message } from '@phosphor/messaging';
import { Disposable } from '@theia/core';
import { Board, BoardsService, AttachedSerialBoard } from '../../common/protocol/boards-service';
@injectable()
export class SelectBoardDialogProps extends DialogProps {
}
@injectable()
export class SelectBoardDialog extends AbstractDialog<BoardAndPortSelection> {
protected readonly dialogPanel: Panel;
protected attachedBoards: Board[];
constructor(
@inject(SelectBoardDialogProps) protected readonly props: SelectBoardDialogProps,
@inject(SelectBoardDialogWidget) protected readonly widget: SelectBoardDialogWidget,
@inject(BoardsService) protected readonly boardService: BoardsService
) {
super({ title: props.title });
this.dialogPanel = new Panel();
this.dialogPanel.addWidget(this.widget);
this.contentNode.classList.add('select-board-dialog');
this.toDispose.push(this.widget.onChanged(() => this.update()));
this.toDispose.push(this.dialogPanel);
this.attachedBoards = [];
this.init();
this.appendCloseButton('CANCEL');
this.appendAcceptButton('OK');
}
protected init() {
const boards = this.boardService.getAttachedBoards();
boards.then(b => this.attachedBoards = b.boards);
this.widget.setAttachedBoards(boards);
}
protected onAfterAttach(msg: Message): void {
Widget.attach(this.dialogPanel, this.contentNode);
this.toDisposeOnDetach.push(Disposable.create(() => {
Widget.detach(this.dialogPanel);
}))
super.onAfterAttach(msg);
this.update();
}
protected onUpdateRequest(msg: Message) {
super.onUpdateRequest(msg);
this.widget.update();
}
protected onActivateRequest(msg: Message): void {
this.widget.activate();
}
protected handleEnter(event: KeyboardEvent): boolean | void {
if (event.target instanceof HTMLTextAreaElement) {
return false;
}
}
protected isValid(value: BoardAndPortSelection): DialogError {
if (!value.board) {
if (value.port) {
return 'Please pick the Board connected to the Port you have selected';
}
return false;
}
return '';
}
get value(): BoardAndPortSelection {
const boardAndPortSelection = this.widget.boardAndPort;
if (this.attachedBoards.length) {
boardAndPortSelection.board = this.attachedBoards.find(b => {
const isAttachedBoard = !!boardAndPortSelection.board &&
b.name === boardAndPortSelection.board.name &&
b.fqbn === boardAndPortSelection.board.fqbn;
if (boardAndPortSelection.port) {
return isAttachedBoard &&
AttachedSerialBoard.is(b) &&
b.port === boardAndPortSelection.port;
} else {
return isAttachedBoard;
}
})
|| boardAndPortSelection.board;
}
return boardAndPortSelection;
}
close(): void {
this.widget.reset();
super.close();
}
onAfterDetach(msg: Message) {
this.widget.reset();
super.onAfterDetach(msg);
}
}