diff --git a/arduino-ide-extension/src/browser/arduino-commands.ts b/arduino-ide-extension/src/browser/arduino-commands.ts index 6c139c3d..d07298d7 100644 --- a/arduino-ide-extension/src/browser/arduino-commands.ts +++ b/arduino-ide-extension/src/browser/arduino-commands.ts @@ -40,4 +40,12 @@ export namespace ArduinoCommands { label: "Refresh attached boards" } + export const SELECT_BOARD: Command = { + id: "arduino-select-board" + } + + export const OPEN_BOARDS_DIALOG: Command = { + id: "arduino-open-boards-dialog" + } + } diff --git a/arduino-ide-extension/src/browser/arduino-file-menu.ts b/arduino-ide-extension/src/browser/arduino-file-menu.ts index e27b28a0..2a0d1663 100644 --- a/arduino-ide-extension/src/browser/arduino-file-menu.ts +++ b/arduino-ide-extension/src/browser/arduino-file-menu.ts @@ -4,16 +4,21 @@ import { CommonMenus } from "@theia/core/lib/browser"; import { ArduinoCommands } from "./arduino-commands"; import { SketchesService, Sketch } from "../common/protocol/sketches-service"; import { AWorkspaceService } from "./arduino-workspace-service"; +import { BoardsService } from "../common/protocol/boards-service"; -export namespace ArduinoOpenSketchContextMenu { - export const PATH: MenuPath = ['arduino-open-sketch-context-menu']; - export const OPEN_GROUP: MenuPath = [...PATH, '1_open']; - export const WS_SKETCHES_GROUP: MenuPath = [...PATH, '2_sketches']; - export const EXAMPLE_SKETCHES_GROUP: MenuPath = [...PATH, '3_examples']; +export namespace ArduinoToolbarContextMenu { + export const OPEN_SKETCH_PATH: MenuPath = ['arduino-open-sketch-context-menu']; + export const OPEN_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '1_open']; + export const WS_SKETCHES_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '2_sketches']; + export const EXAMPLE_SKETCHES_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '3_examples']; + + export const SELECT_BOARDS_PATH: MenuPath = ['arduino-select-boards-context-menu']; + export const CONNECTED_GROUP: MenuPath = [...SELECT_BOARDS_PATH, '1_connected']; + export const OPEN_BOARDS_DIALOG_GROUP: MenuPath = [...SELECT_BOARDS_PATH, '2_open_boards_dialog']; } @injectable() -export class ArduinoFileMenuContribution implements MenuContribution { +export class ArduinoToolbarMenuContribution implements MenuContribution { @inject(CommandRegistry) protected readonly commands: CommandRegistry; @@ -21,6 +26,9 @@ export class ArduinoFileMenuContribution implements MenuContribution { @inject(SketchesService) protected readonly sketches: SketchesService; + @inject(BoardsService) + protected readonly boardsService: BoardsService; + constructor( @inject(AWorkspaceService) protected readonly workspaceService: AWorkspaceService, @inject(MenuModelRegistry) protected readonly menuRegistry: MenuModelRegistry) { @@ -31,21 +39,19 @@ export class ArduinoFileMenuContribution implements MenuContribution { }) } - protected registerSketchesInMenu(registry: MenuModelRegistry) { - this.getWorkspaceSketches().then(sketches => { - sketches.forEach(sketch => { - const command: Command = { - id: 'openSketch' + sketch.name - } - this.commands.registerCommand(command, { - execute: () => this.commands.executeCommand(ArduinoCommands.OPEN_SKETCH.id, sketch) - }); - - registry.registerMenuAction(ArduinoOpenSketchContextMenu.WS_SKETCHES_GROUP, { - commandId: command.id, - label: sketch.name - }); - }) + protected async registerSketchesInMenu(registry: MenuModelRegistry) { + const sketches = await this.getWorkspaceSketches(); + sketches.forEach(sketch => { + const command: Command = { + id: 'openSketch' + sketch.name + } + this.commands.registerCommand(command, { + execute: () => this.commands.executeCommand(ArduinoCommands.OPEN_SKETCH.id, sketch) + }); + registry.registerMenuAction(ArduinoToolbarContextMenu.WS_SKETCHES_GROUP, { + commandId: command.id, + label: sketch.name + }); }) } @@ -57,11 +63,16 @@ export class ArduinoFileMenuContribution implements MenuContribution { registerMenus(registry: MenuModelRegistry) { registry.registerMenuAction([...CommonMenus.FILE, '0_new_sletch'], { commandId: ArduinoCommands.NEW_SKETCH.id - }) + }); - registry.registerMenuAction(ArduinoOpenSketchContextMenu.OPEN_GROUP, { + registry.registerMenuAction(ArduinoToolbarContextMenu.OPEN_GROUP, { commandId: ArduinoCommands.OPEN_FILE_NAVIGATOR.id, label: 'Open...' }); + + registry.registerMenuAction(ArduinoToolbarContextMenu.OPEN_BOARDS_DIALOG_GROUP, { + commandId: ArduinoCommands.OPEN_BOARDS_DIALOG.id, + label: 'Select Other Board & Port' + }); } } \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index be0446f1..97849d20 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -3,9 +3,9 @@ import { injectable, inject, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { EditorWidget } from '@theia/editor/lib/browser/editor-widget'; import { MessageService } from '@theia/core/lib/common/message-service'; -import { CommandContribution, CommandRegistry } from '@theia/core/lib/common/command'; +import { CommandContribution, CommandRegistry, Command } 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, Board, AttachedSerialBoard } from '../common/protocol/boards-service'; import { ArduinoCommands } from './arduino-commands'; import { ConnectedBoards } from './components/connected-boards'; import { CoreService } from '../common/protocol/core-service'; @@ -15,7 +15,7 @@ import { QuickPickService } from '@theia/core/lib/common/quick-pick-service'; import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution'; import { BoardsNotificationService } from './boards-notification-service'; import { WorkspaceRootUriAwareCommandHandler, WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands'; -import { SelectionService } from '@theia/core'; +import { SelectionService, MenuModelRegistry } from '@theia/core'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { SketchFactory } from './sketch-factory'; import { ArduinoToolbar } from './toolbar/arduino-toolbar'; @@ -23,10 +23,12 @@ import { EditorManager } from '@theia/editor/lib/browser'; import { ContextMenuRenderer, OpenerService, Widget } from '@theia/core/lib/browser'; import { OpenFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser/file-dialog'; import { FileSystem } from '@theia/filesystem/lib/common'; -import { ArduinoOpenSketchContextMenu } from './arduino-file-menu'; +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 './boards/boards-toolbar-item'; +import { SelectBoardDialog } from './boards/select-board-dialog'; @injectable() export class ArduinoFrontendContribution implements TabBarToolbarContribution, CommandContribution { @@ -85,10 +87,58 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C @inject(SketchesService) protected readonly sketches: SketchesService; + @inject(SelectBoardDialog) + protected readonly selectBoardsDialog: SelectBoardDialog; + + @inject(MenuModelRegistry) + protected readonly menuRegistry: MenuModelRegistry; + + @inject(CommandRegistry) + protected readonly commands: CommandRegistry; + + protected boardsToolbarItem: BoardsToolBarItem | null; + protected attachedBoards: Board[]; + protected selectedBoard: Board; + @postConstruct() protected async init(): Promise { // This is a hack. Otherwise, the backend services won't bind. await this.workspaceServiceExt.roots(); + const { boards } = await this.boardService.getAttachedBoards(); + this.attachedBoards = boards; + this.registerConnectedBoardsInMenu(this.menuRegistry); + } + + protected async registerConnectedBoardsInMenu(registry: MenuModelRegistry) { + this.attachedBoards.forEach(board => { + const port = this.getPort(board); + const command: Command = { + id: 'selectBoard' + port + } + this.commands.registerCommand(command, { + execute: () => this.commands.executeCommand(ArduinoCommands.SELECT_BOARD.id, board), + isToggled: () => this.isSelectedBoard(board) + }); + registry.registerMenuAction(ArduinoToolbarContextMenu.CONNECTED_GROUP, { + commandId: command.id, + label: board.name + ' at ' + port + }); + }); + } + + protected isSelectedBoard(board: Board): boolean { + return AttachedSerialBoard.is(board) && + this.selectedBoard && + AttachedSerialBoard.is(this.selectedBoard) && + board.port === this.selectedBoard.port && + board.fqbn === this.selectedBoard.fqbn; + } + + protected getPort(board: Board): string { + if (AttachedSerialBoard.is(board)) { + return board.port; + } + return ''; } registerToolbarItems(registry: TabBarToolbarRegistry): void { @@ -118,15 +168,11 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C }); registry.registerItem({ id: ConnectedBoards.TOOLBAR_ID, - // render: () => , - render: () => this.boardsToolbarItem = ref} + contextMenuRenderer={this.contextMenuRenderer} boardsNotificationService={this.boardsNotificationService} - quickPickService={this.quickPickService} - onNoBoardsInstalled={this.onNoBoardsInstalled.bind(this)} - onUnknownBoard={this.onUnknownBoard.bind(this)} />, + boardService={this.boardService} />, isVisible: widget => this.isArduinoToolbar(widget) }) } @@ -180,7 +226,7 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C execute: async (widget: Widget, event: React.MouseEvent) => { const el = (event.target as HTMLElement).parentElement; if (el) { - this.contextMenuRenderer.render(ArduinoOpenSketchContextMenu.PATH, { + this.contextMenuRenderer.render(ArduinoToolbarContextMenu.OPEN_SKETCH_PATH, { x: el.getBoundingClientRect().left, y: el.getBoundingClientRect().top + el.offsetHeight }); @@ -221,7 +267,30 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C registry.registerCommand(ArduinoCommands.REFRESH_BOARDS, { isEnabled: () => true, execute: () => this.boardsNotificationService.notifyBoardsInstalled() + }); + registry.registerCommand(ArduinoCommands.SELECT_BOARD, { + isEnabled: () => true, + execute: async (board: Board) => { + this.selectBoard(board); + } }) + registry.registerCommand(ArduinoCommands.OPEN_BOARDS_DIALOG, { + isEnabled: () => true, + execute: async () => { + const boardAndPort = await this.selectBoardsDialog.open(); + if (boardAndPort && boardAndPort.board) { + this.selectBoard(boardAndPort.board); + } + } + }) + } + + protected async selectBoard(board: Board) { + await this.boardService.selectBoard(board) + if (this.boardsToolbarItem) { + this.boardsToolbarItem.setSelectedBoard(board); + } + this.selectedBoard = board; } protected async openSketchFilesInNewWindow(uri: string) { @@ -278,24 +347,24 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C return widget; } - private async onNoBoardsInstalled() { - const action = await this.messageService.info("You have no boards installed. Use the boards mangager to install one.", "Open Boards Manager"); - if (!action) { - return; - } + // private async onNoBoardsInstalled() { + // const action = await this.messageService.info("You have no boards installed. Use the boards mangager to install one.", "Open Boards Manager"); + // if (!action) { + // return; + // } - this.boardsListWidgetFrontendContribution.openView({ reveal: true }); - } + // this.boardsListWidgetFrontendContribution.openView({ reveal: true }); + // } - private async onUnknownBoard() { - const action = await this.messageService.warn("There's a board connected for which you need to install software." + - " If this were not a PoC we would offer you the right package now.", "Open Boards Manager"); - if (!action) { - return; - } + // private async onUnknownBoard() { + // const action = await this.messageService.warn("There's a board connected for which you need to install software." + + // " If this were not a PoC we would offer you the right package now.", "Open Boards Manager"); + // if (!action) { + // return; + // } - this.boardsListWidgetFrontendContribution.openView({ reveal: true }); - } + // this.boardsListWidgetFrontendContribution.openView({ reveal: true }); + // } private isArduinoToolbar(maybeToolbarWidget: any): boolean { if (maybeToolbarWidget instanceof ArduinoToolbar) { diff --git a/arduino-ide-extension/src/browser/arduino-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-frontend-module.ts index 9dfcdf0b..89483e52 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-frontend-module.ts @@ -27,7 +27,7 @@ import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service import { AWorkspaceService } from './arduino-workspace-service'; import { ThemeService } from '@theia/core/lib/browser/theming'; import { ArduinoTheme } from './arduino-theme'; -import { ArduinoFileMenuContribution } from './arduino-file-menu'; +import { ArduinoToolbarMenuContribution } from './arduino-file-menu'; import { MenuContribution } from '@theia/core'; import { SketchFactory } from './sketch-factory'; import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution'; @@ -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 { SelectBoardDialog, SelectBoardDialogProps } 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 @@ -55,7 +57,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un bind(CommandContribution).toService(ArduinoFrontendContribution); bind(TabBarToolbarContribution).toService(ArduinoFrontendContribution); bind(FrontendApplicationContribution).toService(ArduinoFrontendContribution); - bind(MenuContribution).to(ArduinoFileMenuContribution).inSingletonScope(); + bind(MenuContribution).to(ArduinoToolbarMenuContribution).inSingletonScope(); bind(ArduinoToolbarContribution).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(ArduinoToolbarContribution); @@ -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(SelectBoardDialog).toSelf().inSingletonScope(); + bind(SelectBoardDialogProps).toConstantValue({ + title: 'Select Board' + }) + // Core service bind(CoreService) .toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, CoreServicePath)) diff --git a/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx b/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx new file mode 100644 index 00000000..765daf5f --- /dev/null +++ b/arduino-ide-extension/src/browser/boards/boards-toolbar-item.tsx @@ -0,0 +1,80 @@ +import * as React from 'react'; +import { BoardsService, Board } from '../../common/protocol/boards-service'; +import { ContextMenuRenderer } from '@theia/core/lib/browser'; +import { ArduinoToolbarContextMenu } from '../arduino-file-menu'; +import { BoardsNotificationService } from '../boards-notification-service'; + +export namespace BoardsToolBarItem { + export interface Props { + readonly contextMenuRenderer: ContextMenuRenderer; + readonly boardsNotificationService: BoardsNotificationService; + readonly boardService: BoardsService; + } + + export interface State { + selectedBoard?: Board; + selectedIsAttached: boolean + } +} + +export class BoardsToolBarItem extends React.Component { + + protected attachedBoards: Board[]; + + constructor(props: BoardsToolBarItem.Props) { + super(props); + + this.state = { + selectedBoard: undefined, + selectedIsAttached: true + }; + } + + componentDidMount() { + this.setAttachedBoards(); + } + + protected async setAttachedBoards() { + const { boards } = await this.props.boardService.getAttachedBoards(); + this.attachedBoards = boards; + if (this.attachedBoards.length) { + await this.props.boardService.selectBoard(this.attachedBoards[0]); + this.setSelectedBoard(this.attachedBoards[0]); + } + } + + 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 readonly doShowSelectBoardsMenu = (event: React.MouseEvent) => this.showSelectBoardsMenu(event); + protected showSelectBoardsMenu(event: React.MouseEvent) { + const el = (event.target as HTMLElement).parentElement; + if (el) { + this.props.contextMenuRenderer.render({ + menuPath: ArduinoToolbarContextMenu.SELECT_BOARDS_PATH, + anchor: { + x: el.getBoundingClientRect().left, + y: el.getBoundingClientRect().top + el.offsetHeight + } + }) + } + } + + render(): React.ReactNode { + return +
+
+
+ +
{this.state.selectedBoard ? this.state.selectedBoard.name : 'no board selected'}
+ +
+
+
+
; + } +} \ No newline at end of file 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..d7657303 --- /dev/null +++ b/arduino-ide-extension/src/browser/boards/select-board-dialog-widget.tsx @@ -0,0 +1,305 @@ +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 { + + render(): React.ReactNode { + if (this.props.item.board || this.props.item.port) { + return
+ {this.props.item.board ? this.props.item.board.name : this.props.item.port} + {this.props.selected ? : ''} +
; + } + } + + 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 { + + constructor(props: BoardAndPortSelectionList.Props) { + super(props); + + this.state = { + selection: {} + } + } + + reset(): void { + this.setState({ selection: {} }); + } + + render(): React.ReactNode { + return
+ {this.props.list.map(item => )} +
+ } + + 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 { + + 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 +
+
+
+
+ BOARDS +
+
+ + +
+ { this.boardListComponent = ref }} + type='boards' + onSelect={this.doSelect} + list={this.state.boards.map(board => ({ board }))} /> +
+
+
+
+
+ PORTS +
+ { + this.state.ports.length ? + { this.portListComponent = ref }} + type='ports' + onSelect={this.doSelect} + list={this.state.ports.map(port => ({ port }))} /> : 'loading ports...' + } +
+
+
+
+ } + + 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) => { + 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 { 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(); + 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 { + 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 = +
+
+
+ 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.

+
+
+ this.boardAndPortSelectionComponent = ref} + boardsService={boardsService} + onSelect={this.onSelect} /> +
+
+ + 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..d7c5c81d --- /dev/null +++ b/arduino-ide-extension/src/browser/boards/select-board-dialog.ts @@ -0,0 +1,111 @@ +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 { + + 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.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); + } +} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/components/boards-toolbar-item.tsx b/arduino-ide-extension/src/browser/components/boards-toolbar-item.tsx deleted file mode 100644 index f4ea197c..00000000 --- a/arduino-ide-extension/src/browser/components/boards-toolbar-item.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react'; -import { Board } from '../../common/protocol/boards-service'; - -export namespace BoardsToolBarItem { - export interface Props { - readonly onNoBoardsInstalled: () => void; - readonly onUnknownBoard: (board: Board) => void; - } - - export interface State { - showOpenButton: boolean; - } -} - -export class BoardsToolBarItem extends React.Component { - - constructor(props: BoardsToolBarItem.Props) { - super(props); - - this.state = { - showOpenButton: false - } - } - - render(): React.ReactNode { - - return -
this.setState({ showOpenButton: !this.state.showOpenButton })}> -
-
-
Hallo
- {this.state.showOpenButton ?
OPEN BOARDS DIALOG
: ''} -
-
-
-
; - } -} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/components/connected-boards.tsx b/arduino-ide-extension/src/browser/components/connected-boards.tsx index e90163e4..700c78b0 100644 --- a/arduino-ide-extension/src/browser/components/connected-boards.tsx +++ b/arduino-ide-extension/src/browser/components/connected-boards.tsx @@ -29,14 +29,14 @@ export class ConnectedBoards extends React.Component{label} ]; } - return
- - + { content } - + { !!this.state.otherBoard && } diff --git a/arduino-ide-extension/src/browser/style/arduino.useable.css b/arduino-ide-extension/src/browser/style/arduino.useable.css index 3ad86611..8862821c 100644 --- a/arduino-ide-extension/src/browser/style/arduino.useable.css +++ b/arduino-ide-extension/src/browser/style/arduino.useable.css @@ -86,7 +86,7 @@ is not optimized for dense, information rich UIs. --theia-brand-color3: var(--md-blue-100); /* Secondary Brand colors */ --theia-secondary-brand-color0: var(--md-grey-700); - --theia-secondary-brand-color1: var(--md-grey-500); + --theia-secondary-brand-color1: #b5c8c9; --theia-secondary-brand-color2: var(--md-grey-300); --theia-secondary-brand-color3: var(--md-grey-100); /* Accent colors (dark to bright): Use these to create contrast to layout colors. */ @@ -147,7 +147,7 @@ is not optimized for dense, information rich UIs. /* Menu */ --theia-menu-color0: var(--theia-layout-color3); --theia-menu-color1: var(--theia-layout-color0); - --theia-menu-color2: var(--theia-layout-color3); + --theia-menu-color2: #dae3e3; /* Statusbar */ --theia-statusbar-color: var(--theia-arduino-light); --theia-statusBar-font-color: var(--theia-inverse-ui-font-color0); @@ -157,7 +157,7 @@ is not optimized for dense, information rich UIs. --theia-ui-button-color-hover: var(--theia-arduino-light1); --theia-ui-button-font-color: var(--theia-inverse-ui-font-color0); --theia-ui-button-color-secondary: var(--theia-secondary-brand-color1); - --theia-ui-button-color-secondary-hover: var(--theia-secondary-brand-color0); + --theia-ui-button-color-secondary-hover: var(--theia-menu-color2); --theia-ui-button-font-color-secondary: var(--theia-inverse-ui-font-color0); --theia-ui-button-color-disabled: var(--theia-accent-color3); --theia-ui-button-font-color-disabled: var(--theia-ui-font-color2); @@ -169,8 +169,8 @@ is not optimized for dense, information rich UIs. /* Dialogs */ --theia-ui-dialog-header-color: var(--theia-arduino-light); --theia-ui-dialog-header-font-color: var(--theia-inverse-ui-font-color0); - --theia-ui-dialog-color: var(--theia-layout-color0); - --theia-ui-dialog-font-color: var(--theia-ui-font-color1); + --theia-ui-dialog-color: rgb(236, 241, 241); + --theia-ui-dialog-font-color: black; /* Variables */ --theia-variable-name-color: #9B46B0; --theia-variable-value-color: rgba(108, 108, 108, 0.8); diff --git a/arduino-ide-extension/src/browser/style/board-select-dialog.css b/arduino-ide-extension/src/browser/style/board-select-dialog.css new file mode 100644 index 00000000..ce19946f --- /dev/null +++ b/arduino-ide-extension/src/browser/style/board-select-dialog.css @@ -0,0 +1,152 @@ +div#select-board-dialog { + margin: 5px 20px 50px 20px; +} + +div#select-board-dialog .selectBoardContainer .body { + display: flex; + overflow: hidden; +} + +div#select-board-dialog .selectBoardContainer .head { + margin-bottom: 10px; +} + +div#select-board-dialog .selectBoardContainer .head .title { + font-weight: 400; + letter-spacing: .02em; + font-size: 1.2em; + color: #00979d; + margin: 17px 0; +} + +div#select-board-dialog .selectBoardContainer .head .text { + margin-bottom: 21px; +} + +div#select-board-dialog .selectBoardContainer .body .list .item.selected { + background: var(--theia-ui-button-color-secondary-hover); +} + +div#select-board-dialog .selectBoardContainer .body .list .item.selected i{ + color: var(--theia-arduino-light); +} + +#select-board-dialog .selectBoardContainer .body .search input, +#select-board-dialog .selectBoardContainer .body .boards.list, +#select-board-dialog .selectBoardContainer .body .search, +#select-board-dialog .selectBoardContainer .body .ports.list { + background: white; +} + +#select-board-dialog .selectBoardContainer .body .search input { + border: none; + width: 100%; + height: auto; + max-height: 37px; + padding: 10px 8px; + margin: 0; + vertical-align: top; + display: flex; + color: var(--theia-content-font-color0); +} + +#select-board-dialog .selectBoardContainer .body .search input:focus { + box-shadow: none; +} + +#select-board-dialog .selectBoardContainer .body .container { + flex: 1; +} + +#select-board-dialog .selectBoardContainer .body .left.container .content { + margin: 0 5px 0 0; +} + +#select-board-dialog .selectBoardContainer .body .right.container .content { + margin: 0 0 0 5px; +} + +#select-board-dialog .selectBoardContainer .body .container .content .title{ + color: #7f8c8d; + margin-bottom: 10px; +} + +#select-board-dialog .selectBoardContainer .body .list .item { + padding: 10px 5px 10px 20px; + display: flex; + justify-content: space-between; +} +#select-board-dialog .selectBoardContainer .body .list .item:hover { + background: var(--theia-ui-button-color-secondary-hover); +} + +#select-board-dialog .selectBoardContainer .body .list { + max-height: 265px; + overflow-y: auto; +} + +#select-board-dialog .selectBoardContainer .body .search { + margin-bottom: 10px; + display: flex; + align-items: center; + padding-right: 5px; +} + +.p-Widget.dialogOverlay .dialogBlock { + width: 740px; +} + +button.theia-button { + height: 31px; +} + +button.theia-button.secondary { + background-color: #b5c8c9; + color: #000; + box-shadow: 0 4px #95a5a6; +} + +button.theia-button.main { + color: #fff; + background-color: #00979c; + box-shadow: 0 4px #005c5f; +} + +.dialogControl { + margin: 0 20px 30px 0; +} + +.arduino-boards-toolbar-item-container { + margin-left: 3px; +} + +.arduino-boards-toolbar-item-container .arduino-boards-toolbar-item .inner-container { + display: flex; + align-items: baseline; + margin: 0 5px; +} + +.arduino-boards-toolbar-item-container .arduino-boards-toolbar-item .inner-container .notAttached { + width: 10px; + height: 10px; + color: red; + margin-right: 5px; +} + +.arduino-boards-toolbar-item-container { + display: flex; + align-items: center; +} + +.arduino-boards-toolbar-item .label { + height: 100%; + display: flex; + align-items: center; + margin-right: 5px; +} + +.arduino-boards-toolbar-item { + background: white; + height: 18px; +} + diff --git a/arduino-ide-extension/src/browser/style/index.css b/arduino-ide-extension/src/browser/style/index.css index 4a57acb5..e9e53b25 100644 --- a/arduino-ide-extension/src/browser/style/index.css +++ b/arduino-ide-extension/src/browser/style/index.css @@ -1,3 +1,3 @@ @import './list-widget.css'; -@import './select-board-dialog.css'; -@import './main.css'; \ No newline at end of file +@import './board-select-dialog.css'; +@import './main.css'; diff --git a/arduino-ide-extension/src/browser/style/main.css b/arduino-ide-extension/src/browser/style/main.css index 734cb13c..2d0e9f04 100644 --- a/arduino-ide-extension/src/browser/style/main.css +++ b/arduino-ide-extension/src/browser/style/main.css @@ -53,24 +53,10 @@ opacity: 1; } -.arduino-boards-toolbar-item-container { - display: flex; - align-items: center; -} - -.arduino-boards-toolbar-item .label { - height: 100%; - display: flex; - align-items: center; -} - .arduino-open-boards-button { + background: white; } -.arduino-boards-toolbar-item { - background: white; - height: 18px; -} .arduino-tool-item.item.connected-boards select { line-height: var(--theia-content-line-height); diff --git a/arduino-ide-extension/src/browser/style/select-board-dialog.css b/arduino-ide-extension/src/browser/style/select-board-dialog.css deleted file mode 100644 index 1e3004e8..00000000 --- a/arduino-ide-extension/src/browser/style/select-board-dialog.css +++ /dev/null @@ -1,13 +0,0 @@ - -.select-board-dialog { - width: 600px; -} - -.select-board-dialog input { - width: calc(100% - 8px); - margin-bottom: 5px; -} - -.select-board-dialog select { - width: 100%; -} \ 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) };