diff --git a/arduino-ide-extension/src/browser/arduino-file-menu.ts b/arduino-ide-extension/src/browser/arduino-file-menu.ts index 389eada0..8dbca73f 100644 --- a/arduino-ide-extension/src/browser/arduino-file-menu.ts +++ b/arduino-ide-extension/src/browser/arduino-file-menu.ts @@ -62,11 +62,11 @@ export class ArduinoToolbarMenuContribution implements MenuContribution { protected getPort(board: Board): string { - if(AttachedSerialBoard.is(board)){ + if (AttachedSerialBoard.is(board)) { return board.port; } - if(AttachedNetworkBoard.is(board)) { - return 'netport' + board.port; + if (AttachedNetworkBoard.is(board)) { + return 'netport: ' + board.port; } return ''; } @@ -95,7 +95,7 @@ export class ArduinoToolbarMenuContribution implements MenuContribution { registerMenus(registry: MenuModelRegistry) { registry.registerMenuAction([...CommonMenus.FILE, '0_new_sletch'], { commandId: ArduinoCommands.NEW_SKETCH.id - }) + }); registry.registerMenuAction(ArduinoToolbarContextMenu.OPEN_GROUP, { commandId: ArduinoCommands.OPEN_FILE_NAVIGATOR.id, diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index e526b91f..1f582ef3 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -27,7 +27,8 @@ import { ArduinoToolbarContextMenu } from './arduino-file-menu'; import { Sketch, SketchesService } from '../common/protocol/sketches-service'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { CommonCommands } from '@theia/core/lib/browser/common-frontend-contribution' -import { BoardsToolBarItem } from './components/boards-toolbar-item'; +import { BoardsToolBarItem } from './boards/boards-toolbar-item'; +import { SelectBoardsDialog } from './boards/select-board-dialog'; @injectable() export class ArduinoFrontendContribution implements TabBarToolbarContribution, CommandContribution { @@ -86,6 +87,9 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C @inject(SketchesService) protected readonly sketches: SketchesService; + @inject(SelectBoardsDialog) + protected readonly selectBoardsDialog: SelectBoardsDialog; + @postConstruct() protected async init(): Promise { // This is a hack. Otherwise, the backend services won't bind. @@ -227,13 +231,26 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C registry.registerCommand(ArduinoCommands.SELECT_BOARD, { isEnabled: () => true, execute: (board: Board) => { - console.log("SELECT BOARD HERE", board); - } + this.boardService.selectBoard(board).then(() => { + return this.boardService.getSelectBoard(); + }).then(board => { + console.log("and the selected board is", board); + }) + } }) registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, { isEnabled: () => true, - execute: () => { - console.log("OPEN THE DIALOG HERE"); + execute: async () => { + const boardAndPort = await this.selectBoardsDialog.open(); + if(boardAndPort && boardAndPort.board){ + const selectedBoard = { + fqbn: boardAndPort.board.fqbn, + name: boardAndPort.board.name, + port: boardAndPort.port + } + this.boardService.selectBoard(selectedBoard); + + } } }) } diff --git a/arduino-ide-extension/src/browser/arduino-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-frontend-module.ts index 0df09de3..7cec9282 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-frontend-module.ts @@ -48,6 +48,8 @@ import { CustomApplicationShell } from './customization/custom-application-shell import { CustomFrontendApplication } from './customization/custom-frontend-application'; import { EditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory'; import { CustomEditorWidgetFactory } from './customization/custom-editor-widget-factory'; +import { SelectBoardsDialog, SelectBoardsDialogProps } from './boards/select-board-dialog'; +import { SelectBoardDialogWidget } from './boards/select-board-dialog-widget'; export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => { // Commands and toolbar items @@ -94,6 +96,13 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un })); bind(FrontendApplicationContribution).toService(BoardsListWidgetFrontendContribution); + // Board select dialog + bind(SelectBoardDialogWidget).toSelf().inSingletonScope(); + bind(SelectBoardsDialog).toSelf().inSingletonScope(); + bind(SelectBoardsDialogProps).toConstantValue({ + title: 'Select Board' + }) + // Core service bind(CoreService) .toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, CoreServicePath)) diff --git a/arduino-ide-extension/src/browser/components/boards-toolbar-item.tsx b/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx similarity index 100% rename from arduino-ide-extension/src/browser/components/boards-toolbar-item.tsx rename to arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx diff --git a/arduino-ide-extension/src/browser/boards/select-board-dialog-widget.tsx b/arduino-ide-extension/src/browser/boards/select-board-dialog-widget.tsx new file mode 100644 index 00000000..9dee9256 --- /dev/null +++ b/arduino-ide-extension/src/browser/boards/select-board-dialog-widget.tsx @@ -0,0 +1,230 @@ +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 SelectableBoardsItem { + export interface Props { + board: Board, + selected: boolean, + onClick: (selection: BoardAndPortSelection) => void + } +} + +export class SelectableBoardsItem extends React.Component { + + render(): React.ReactNode { + return
{this.props.board.name}
+ } + + protected readonly select = (() => { + this.props.onClick({ board: this.props.board }) + }).bind(this); +} + +export namespace SelectablePortsItem { + export interface Props { + port: string, + selected: boolean, + onClick: (selection: BoardAndPortSelection) => void + } +} + +export class SelectablePortsItem extends React.Component { + + render(): React.ReactNode { + return
this.props.onClick({ port: this.props.port })} className={`item ${this.props.selected ? 'selected': ''}`}>{this.props.port}
+ } + + protected readonly select = (() => { + this.props.onClick({ port: this.props.port }) + }).bind(this); +} + +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 { + + protected allBoards: Board[] = []; + + constructor(props: BoardAndPortSelectionComponent.Props) { + super(props); + + this.state = { + boards: [], + ports: [], + selection: {} + } + } + + componentDidMount() { + this.searchAvailableBoards(); + this.setPorts(); + } + + render(): React.ReactNode { + return +
+
+
+ BOARDS +
+
+ +
+
+ {this.state.boards.map(board => )} +
+
+
+
+ PORTS +
+
+ {this.state.ports.map(port => )} +
+
+
+
+ } + + protected readonly isSelectedBoard = ((board: Board) => { + return (this.state.selection.board && this.state.selection.board === board) || false; + }); + + protected readonly isSelectedPort = ((port: string) => { + return (this.state.selection.port && this.state.selection.port === port) || false; + }); + + 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 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 doFilter = (event: React.ChangeEvent) => { + 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(item => item.boards)) as Board[]; + this.allBoards = this.sort(boards); + this.setState({ boards: this.allBoards }); + } + + protected async setPorts() { + const ports: string[] = []; + const attached = await this.props.boardsService.getAttachedBoards(); + attached.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(); + + boardAndPort: BoardAndPortSelection = {}; + + constructor() { + super(); + this.id = 'select-board-dialog'; + + this.toDispose.push(this.onChangedEmitter); + } + + get onChanged(): Event { + return this.onChangedEmitter.event; + } + + protected fireChanged(boardAndPort: BoardAndPortSelection): void { + this.onChangedEmitter.fire(boardAndPort); + } + + protected render(): React.ReactNode { + let content: 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 }) => boardsServiceDelegate.search(options), + install: async (item: BoardPackage) => { + await boardsServiceDelegate.install(item); + this.boardsNotificationService.notifyBoardsInstalled(); + } + } + + content = +
+
+
+ Select Other Board & Port +
+
+ 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. +
+
+ +
+
+ + return content; + } + + protected readonly onSelect = (selection: BoardAndPortSelection) => { this.doOnSelect(selection) }; + protected doOnSelect(selection: BoardAndPortSelection) { + this.boardAndPort = selection; + this.fireChanged(this.boardAndPort); + } +} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/boards/select-board-dialog.ts b/arduino-ide-extension/src/browser/boards/select-board-dialog.ts new file mode 100644 index 00000000..55278bfb --- /dev/null +++ b/arduino-ide-extension/src/browser/boards/select-board-dialog.ts @@ -0,0 +1,78 @@ +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'; + +@injectable() +export class SelectBoardsDialogProps extends DialogProps { + +} + +@injectable() +export class SelectBoardsDialog extends AbstractDialog { + + protected readonly dialogPanel: Panel; + + constructor( + @inject(SelectBoardsDialogProps) protected readonly props: SelectBoardsDialogProps, + @inject(SelectBoardDialogWidget) protected readonly widget: SelectBoardDialogWidget + ) { + super({ title: props.title }); + + this.dialogPanel = new Panel(); + this.dialogPanel.addWidget(this.widget); + + this.toDispose.push(this.widget.onChanged(() => this.update())); + + this.toDispose.push(this.dialogPanel); + + this.appendCloseButton('CANCEL'); + this.appendAcceptButton('OK'); + } + + 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 { + return this.widget.boardAndPort; + } + + protected async accept(): Promise { + super.accept(); + } +} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/style/main.css b/arduino-ide-extension/src/browser/style/main.css index 29d43d35..681eb841 100644 --- a/arduino-ide-extension/src/browser/style/main.css +++ b/arduino-ide-extension/src/browser/style/main.css @@ -96,4 +96,16 @@ display: flex; align-items: center; color: var(--theia-ui-font-color3); +} + +div#select-board-dialog .selectBoardContainer .body { + display: flex; +} + +div#select-board-dialog .selectBoardContainer .head { + margin-bottom: 10px; +} + +div#select-board-dialog .selectBoardContainer .body .list .item.selected { + background: #aaaaaa; } \ No newline at end of file diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts index 16654df1..b35861c2 100644 --- a/arduino-ide-extension/src/common/protocol/boards-service.ts +++ b/arduino-ide-extension/src/common/protocol/boards-service.ts @@ -4,8 +4,8 @@ export const BoardsServicePath = '/services/boards-service'; export const BoardsService = Symbol('BoardsService'); export interface BoardsService { getAttachedBoards(): Promise<{ boards: Board[] }>; - selectBoard(board: Board): Promise; - getSelectBoard(): Promise; + selectBoard(board: Board | AttachedSerialBoard | AttachedNetworkBoard): Promise; + getSelectBoard(): Promise; search(options: { query?: string }): Promise<{ items: BoardPackage[] }>; install(item: BoardPackage): Promise; @@ -23,31 +23,29 @@ export interface Board { export interface AttachedSerialBoard extends Board { port: string; - serialNumber: string; - productID: string; - vendorID: string; + type: 'serial'; + serialNumber?: string; + productID?: string; + vendorID?: string; } export namespace AttachedSerialBoard { export function is(b: Board): b is AttachedSerialBoard { - return 'port' in b - && 'serialNumber' in b - && 'productID' in b - && 'vendorID' in b; + return 'type' in b && (b as Board & { type: any }).type === 'serial' && + 'port' in b && !!(b as Board & { port: any }).port && typeof (b as Board & { port: any }).port === 'string'; } } export interface AttachedNetworkBoard extends Board { - info: string; - address: string; + info?: string; + address?: string; port: number; + type: 'network'; } export namespace AttachedNetworkBoard { export function is(b: Board): b is AttachedNetworkBoard { - return 'name' in b - && 'info' in b - && 'address' in b - && 'port' in b; + return 'type' in b && (b as Board & { type: any }).type === 'network' && + 'port' in b && !!(b as Board & { port: any }).port && typeof (b as Board & { port: any }).port === 'number'; } } diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index 18b6c1b0..50df0265 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -31,6 +31,7 @@ export class BoardsServiceImpl implements BoardsService { name: b.getName() || "unknown", fqbn: b.getFqbn(), port: b.getPort(), + type: 'serial', serialNumber: b.getSerialnumber(), productID: b.getProductid(), vendorID: b.getVendorid() @@ -41,6 +42,7 @@ export class BoardsServiceImpl implements BoardsService { address: b.getAddress(), info: b.getInfo(), port: b.getPort(), + type: 'network' }); return { boards: serialBoards.concat(networkBoards) };