Implemented custom dropdown for board selection

Signed-off-by: jbicker <jan.bicker@typefox.io>
This commit is contained in:
jbicker 2019-07-19 17:44:27 +02:00
parent c2fbccc9e8
commit 4d2bd87f74
5 changed files with 232 additions and 97 deletions

View File

@ -8,10 +8,6 @@ export namespace ArduinoToolbarContextMenu {
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()
@ -25,10 +21,5 @@ export class ArduinoToolbarMenuContribution implements MenuContribution {
registry.registerMenuAction([...CommonMenus.FILE, '0_new_sletch'], {
commandId: ArduinoCommands.NEW_SKETCH.id
})
registry.registerMenuAction(ArduinoToolbarContextMenu.OPEN_BOARDS_DIALOG_GROUP, {
commandId: ArduinoCommands.OPEN_BOARDS_DIALOG.id,
label: 'Select Other Board & Port'
});
}
}

View File

@ -5,7 +5,7 @@ import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
import { MessageService } from '@theia/core/lib/common/message-service';
import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { BoardsService, Board, AttachedSerialBoard } from '../common/protocol/boards-service';
import { BoardsService, Board } from '../common/protocol/boards-service';
import { ArduinoCommands } from './arduino-commands';
import { ConnectedBoards } from './components/connected-boards';
import { CoreService } from '../common/protocol/core-service';
@ -103,8 +103,6 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
protected readonly commands: CommandRegistry;
protected boardsToolbarItem: BoardsToolBarItem | null;
protected attachedBoards: Board[];
protected selectedBoard: Board;
protected wsSketchCount: number = 0;
constructor(@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService) {
@ -119,41 +117,6 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
protected async init(): Promise<void> {
// 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 {
@ -184,7 +147,9 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
registry.registerItem({
id: ConnectedBoards.TOOLBAR_ID,
render: () => <BoardsToolBarItem
key='boardsToolbarItem'
ref={ref => this.boardsToolbarItem = ref}
commands={this.commands}
contextMenuRenderer={this.contextMenuRenderer}
boardsNotificationService={this.boardsNotificationService}
boardService={this.boardService} />,
@ -305,11 +270,10 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
}
protected async selectBoard(board: Board) {
await this.boardService.selectBoard(board)
await this.boardService.selectBoard(board);
if (this.boardsToolbarItem) {
this.boardsToolbarItem.setSelectedBoard(board);
}
this.selectedBoard = board;
}
registerMenus(registry: MenuModelRegistry) {

View File

@ -1,48 +1,135 @@
import * as React from 'react';
import { BoardsService, Board } from '../../common/protocol/boards-service';
import { BoardsService, Board, AttachedSerialBoard } from '../../common/protocol/boards-service';
import { ContextMenuRenderer } from '@theia/core/lib/browser';
import { ArduinoToolbarContextMenu } from '../arduino-file-menu';
import { BoardsNotificationService } from '../boards-notification-service';
import { Command, CommandRegistry } from '@theia/core';
import { ArduinoCommands } from '../arduino-commands';
import ReactDOM = require('react-dom');
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 namespace BoardsDropDown {
export interface Props {
readonly coords: BoardsDropDownListCoord;
readonly isOpen: boolean;
readonly dropDownItems: BoardsDropdownItem[];
readonly openDialog: () => 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);
if (!list) {
list = document.createElement('div');
list.id = this.dropdownId;
document.body.appendChild(list);
this.dropdownElement = list;
}
}
render(): React.ReactNode {
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 {
return '';
}
}
}
export namespace BoardsToolBarItem {
export interface Props {
readonly contextMenuRenderer: ContextMenuRenderer;
readonly boardsNotificationService: BoardsNotificationService;
readonly boardService: BoardsService;
readonly commands: CommandRegistry;
}
export interface State {
selectedBoard?: Board;
selectedIsAttached: boolean
selectedIsAttached: boolean;
boardItems: BoardsDropdownItem[];
isOpen: boolean;
}
}
export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props, BoardsToolBarItem.State> {
protected attachedBoards: Board[];
protected dropDownListCoord: BoardsDropDownListCoord;
constructor(props: BoardsToolBarItem.Props) {
super(props);
this.state = {
selectedBoard: undefined,
selectedIsAttached: true
selectedIsAttached: true,
boardItems: [],
isOpen: false
};
document.addEventListener('click', () => {
this.setState({ isOpen: false });
});
}
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) });
@ -50,31 +137,101 @@ export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props,
this.setState({ selectedBoard: board });
}
protected readonly doShowSelectBoardsMenu = (event: React.MouseEvent<HTMLElement>) => this.showSelectBoardsMenu(event);
protected showSelectBoardsMenu(event: React.MouseEvent<HTMLElement>) {
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
protected async setAttachedBoards() {
const { boards } = await this.props.boardService.getAttachedBoards();
this.attachedBoards = boards;
if (this.attachedBoards.length) {
await this.createBoardDropdownItems();
await this.props.boardService.selectBoard(this.attachedBoards[0]);
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)
});
});
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;
}
protected getPort(board: Board): string {
if (AttachedSerialBoard.is(board)) {
return board.port;
}
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;
return <React.Fragment>
<div className='arduino-boards-toolbar-item-container' onClick={this.doShowSelectBoardsMenu}>
<div className='arduino-boards-toolbar-item'>
<div className='inner-container'>
<span className={!this.state.selectedBoard || !this.state.selectedIsAttached ? 'fa fa-times notAttached' : ''}></span>
<div className='label'>{this.state.selectedBoard ? this.state.selectedBoard.name : 'no board selected'}</div>
<span className='fa fa-caret-down'></span>
<div className='arduino-boards-toolbar-item-container'>
<div className='arduino-boards-toolbar-item' title={selectedBoard && `${selectedBoard.name}${port ? ' at ' + port : ''}`}>
<div className='inner-container' onClick={this.doShowSelectBoardsMenu}>
<span className={!selectedBoard || !this.state.selectedIsAttached ? 'fa fa-times notAttached' : ''}></span>
<div className='label noWrapInfo'>
<div className='noWrapInfo'>
{selectedBoard ? `${selectedBoard.name}${port ? ' at ' + port : ''}` : 'no board selected'}
</div>
</div>
<span className='fa fa-caret-down caret'></span>
</div>
</div>
</div>
<BoardsDropDown
isOpen={this.state.isOpen}
coords={this.dropDownListCoord}
dropDownItems={this.state.boardItems}
openDialog={this.openDialog}>
</BoardsDropDown>
</React.Fragment>;
}
protected openDialog = () => {
this.props.commands.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id);
this.setState({ isOpen: false });
};
}

View File

@ -123,30 +123,62 @@ button.theia-button.main {
.arduino-boards-toolbar-item-container .arduino-boards-toolbar-item .inner-container {
display: flex;
align-items: baseline;
margin: 0 5px;
width: 100%;
}
.arduino-boards-toolbar-item-container .arduino-boards-toolbar-item .inner-container .notAttached {
width: 10px;
height: 10px;
color: red;
margin-right: 5px;
margin: 0 5px;
}
.arduino-boards-toolbar-item-container {
display: flex;
align-items: center;
width: 220px;
}
.arduino-boards-toolbar-item .label {
height: 100%;
display: flex;
align-items: center;
margin: 0 5px;
width: 100%;
font-weight: bold;
}
.arduino-boards-toolbar-item .caret {
width: 10px;
margin-right: 5px;
}
.arduino-boards-toolbar-item {
background: white;
height: 18px;
height: 22px;
display: flex;
width: 100%;
overflow: hidden;
}
.arduino-boards-dropdown-list {
background: #f7f7f7;
border: 3px solid var(--theia-border-color2);
margin: -3px;
}
.arduino-boards-dropdown-item {
font-size: var(--theia-ui-font-size1);
display: flex;
padding: 10px;
cursor: pointer;
}
.arduino-boards-dropdown-item .fa-check {
color: var(--theia-accent-color2);
}
.arduino-boards-dropdown-item.selected,
.arduino-boards-dropdown-item:hover {
background: var(--theia-ui-button-color-secondary-hover);
}

View File

@ -61,21 +61,6 @@
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-boards-toolbar-item {
background: white;
}
.arduino-tool-item.item.connected-boards select {
line-height: var(--theia-content-line-height);
font-size: var(--theia-ui-font-size1);
@ -105,3 +90,9 @@
border-right: 2px solid var(--theia-border-color1);
box-sizing: border-box;
}
.noWrapInfo {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}