Skip to content

Commit 4d2bd87

Browse files
committed
Implemented custom dropdown for board selection
Signed-off-by: jbicker <jan.bicker@typefox.io>
1 parent c2fbccc commit 4d2bd87

File tree

5 files changed

+227
-92
lines changed

5 files changed

+227
-92
lines changed

arduino-ide-extension/src/browser/arduino-file-menu.ts

-9
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@ export namespace ArduinoToolbarContextMenu {
88
export const OPEN_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '1_open'];
99
export const WS_SKETCHES_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '2_sketches'];
1010
export const EXAMPLE_SKETCHES_GROUP: MenuPath = [...OPEN_SKETCH_PATH, '3_examples'];
11-
12-
export const SELECT_BOARDS_PATH: MenuPath = ['arduino-select-boards-context-menu'];
13-
export const CONNECTED_GROUP: MenuPath = [...SELECT_BOARDS_PATH, '1_connected'];
14-
export const OPEN_BOARDS_DIALOG_GROUP: MenuPath = [...SELECT_BOARDS_PATH, '2_open_boards_dialog'];
1511
}
1612

1713
@injectable()
@@ -25,10 +21,5 @@ export class ArduinoToolbarMenuContribution implements MenuContribution {
2521
registry.registerMenuAction([...CommonMenus.FILE, '0_new_sletch'], {
2622
commandId: ArduinoCommands.NEW_SKETCH.id
2723
})
28-
29-
registry.registerMenuAction(ArduinoToolbarContextMenu.OPEN_BOARDS_DIALOG_GROUP, {
30-
commandId: ArduinoCommands.OPEN_BOARDS_DIALOG.id,
31-
label: 'Select Other Board & Port'
32-
});
3324
}
3425
}

arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx

+4-40
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
55
import { MessageService } from '@theia/core/lib/common/message-service';
66
import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common/command';
77
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
8-
import { BoardsService, Board, AttachedSerialBoard } from '../common/protocol/boards-service';
8+
import { BoardsService, Board } from '../common/protocol/boards-service';
99
import { ArduinoCommands } from './arduino-commands';
1010
import { ConnectedBoards } from './components/connected-boards';
1111
import { CoreService } from '../common/protocol/core-service';
@@ -103,8 +103,6 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
103103
protected readonly commands: CommandRegistry;
104104

105105
protected boardsToolbarItem: BoardsToolBarItem | null;
106-
protected attachedBoards: Board[];
107-
protected selectedBoard: Board;
108106
protected wsSketchCount: number = 0;
109107

110108
constructor(@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService) {
@@ -119,41 +117,6 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
119117
protected async init(): Promise<void> {
120118
// This is a hack. Otherwise, the backend services won't bind.
121119
await this.workspaceServiceExt.roots();
122-
const { boards } = await this.boardService.getAttachedBoards();
123-
this.attachedBoards = boards;
124-
this.registerConnectedBoardsInMenu(this.menuRegistry);
125-
}
126-
127-
protected async registerConnectedBoardsInMenu(registry: MenuModelRegistry) {
128-
this.attachedBoards.forEach(board => {
129-
const port = this.getPort(board);
130-
const command: Command = {
131-
id: 'selectBoard' + port
132-
}
133-
this.commands.registerCommand(command, {
134-
execute: () => this.commands.executeCommand(ArduinoCommands.SELECT_BOARD.id, board),
135-
isToggled: () => this.isSelectedBoard(board)
136-
});
137-
registry.registerMenuAction(ArduinoToolbarContextMenu.CONNECTED_GROUP, {
138-
commandId: command.id,
139-
label: board.name + ' at ' + port
140-
});
141-
});
142-
}
143-
144-
protected isSelectedBoard(board: Board): boolean {
145-
return AttachedSerialBoard.is(board) &&
146-
this.selectedBoard &&
147-
AttachedSerialBoard.is(this.selectedBoard) &&
148-
board.port === this.selectedBoard.port &&
149-
board.fqbn === this.selectedBoard.fqbn;
150-
}
151-
152-
protected getPort(board: Board): string {
153-
if (AttachedSerialBoard.is(board)) {
154-
return board.port;
155-
}
156-
return '';
157120
}
158121

159122
registerToolbarItems(registry: TabBarToolbarRegistry): void {
@@ -184,7 +147,9 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
184147
registry.registerItem({
185148
id: ConnectedBoards.TOOLBAR_ID,
186149
render: () => <BoardsToolBarItem
150+
key='boardsToolbarItem'
187151
ref={ref => this.boardsToolbarItem = ref}
152+
commands={this.commands}
188153
contextMenuRenderer={this.contextMenuRenderer}
189154
boardsNotificationService={this.boardsNotificationService}
190155
boardService={this.boardService} />,
@@ -305,11 +270,10 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
305270
}
306271

307272
protected async selectBoard(board: Board) {
308-
await this.boardService.selectBoard(board)
273+
await this.boardService.selectBoard(board);
309274
if (this.boardsToolbarItem) {
310275
this.boardsToolbarItem.setSelectedBoard(board);
311276
}
312-
this.selectedBoard = board;
313277
}
314278

315279
registerMenus(registry: MenuModelRegistry) {
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,237 @@
11
import * as React from 'react';
2-
import { BoardsService, Board } from '../../common/protocol/boards-service';
2+
import { BoardsService, Board, AttachedSerialBoard } from '../../common/protocol/boards-service';
33
import { ContextMenuRenderer } from '@theia/core/lib/browser';
4-
import { ArduinoToolbarContextMenu } from '../arduino-file-menu';
54
import { BoardsNotificationService } from '../boards-notification-service';
5+
import { Command, CommandRegistry } from '@theia/core';
6+
import { ArduinoCommands } from '../arduino-commands';
7+
import ReactDOM = require('react-dom');
8+
9+
export interface BoardsDropdownItem {
10+
label: string;
11+
commandExecutor: () => void;
12+
isSelected: () => boolean;
13+
}
14+
15+
export interface BoardsDropDownListCoord {
16+
top: number;
17+
left: number;
18+
width: number;
19+
paddingTop: number;
20+
}
21+
22+
export namespace BoardsDropdownItemComponent {
23+
export interface Props {
24+
label: string;
25+
onClick: () => void;
26+
isSelected: boolean;
27+
}
28+
}
29+
30+
export class BoardsDropdownItemComponent extends React.Component<BoardsDropdownItemComponent.Props> {
31+
render() {
32+
return <div className={`arduino-boards-dropdown-item ${this.props.isSelected ? 'selected' : ''}`} onClick={this.props.onClick}>
33+
<div>{this.props.label}</div>
34+
{this.props.isSelected ? <span className='fa fa-check'></span> : ''}
35+
</div>;
36+
}
37+
}
38+
39+
export namespace BoardsDropDown {
40+
export interface Props {
41+
readonly coords: BoardsDropDownListCoord;
42+
readonly isOpen: boolean;
43+
readonly dropDownItems: BoardsDropdownItem[];
44+
readonly openDialog: () => void;
45+
}
46+
}
47+
48+
export class BoardsDropDown extends React.Component<BoardsDropDown.Props> {
49+
protected dropdownId: string = 'boards-dropdown-container';
50+
protected dropdownElement: HTMLElement;
51+
52+
constructor(props: BoardsDropDown.Props) {
53+
super(props);
54+
55+
let list = document.getElementById(this.dropdownId);
56+
if (!list) {
57+
list = document.createElement('div');
58+
list.id = this.dropdownId;
59+
document.body.appendChild(list);
60+
this.dropdownElement = list;
61+
}
62+
}
63+
64+
render(): React.ReactNode {
65+
return ReactDOM.createPortal(this.renderNode(), this.dropdownElement);
66+
}
67+
68+
renderNode(): React.ReactNode {
69+
if (this.props.isOpen) {
70+
return <div className='arduino-boards-dropdown-list'
71+
style={{
72+
position: 'absolute',
73+
top: this.props.coords.top,
74+
left: this.props.coords.left,
75+
width: this.props.coords.width,
76+
paddingTop: this.props.coords.paddingTop
77+
}}>
78+
{
79+
this.props.dropDownItems.map(item => {
80+
return <React.Fragment key={item.label}>
81+
<BoardsDropdownItemComponent isSelected={item.isSelected()} label={item.label} onClick={item.commandExecutor}></BoardsDropdownItemComponent>
82+
</React.Fragment>;
83+
})
84+
}
85+
<BoardsDropdownItemComponent isSelected={false} label={'Select Other Board & Port'} onClick={this.props.openDialog}></BoardsDropdownItemComponent>
86+
</div>
87+
} else {
88+
return '';
89+
}
90+
}
91+
}
692

793
export namespace BoardsToolBarItem {
894
export interface Props {
995
readonly contextMenuRenderer: ContextMenuRenderer;
1096
readonly boardsNotificationService: BoardsNotificationService;
1197
readonly boardService: BoardsService;
98+
readonly commands: CommandRegistry;
1299
}
13100

14101
export interface State {
15102
selectedBoard?: Board;
16-
selectedIsAttached: boolean
103+
selectedIsAttached: boolean;
104+
boardItems: BoardsDropdownItem[];
105+
isOpen: boolean;
17106
}
18107
}
19108

20109
export class BoardsToolBarItem extends React.Component<BoardsToolBarItem.Props, BoardsToolBarItem.State> {
21110

22111
protected attachedBoards: Board[];
112+
protected dropDownListCoord: BoardsDropDownListCoord;
23113

24114
constructor(props: BoardsToolBarItem.Props) {
25115
super(props);
26116

27117
this.state = {
28118
selectedBoard: undefined,
29-
selectedIsAttached: true
119+
selectedIsAttached: true,
120+
boardItems: [],
121+
isOpen: false
30122
};
123+
124+
document.addEventListener('click', () => {
125+
this.setState({ isOpen: false });
126+
});
31127
}
32128

33129
componentDidMount() {
34130
this.setAttachedBoards();
35131
}
36132

133+
setSelectedBoard(board: Board) {
134+
if (this.attachedBoards && this.attachedBoards.length) {
135+
this.setState({ selectedIsAttached: !!this.attachedBoards.find(attachedBoard => attachedBoard.name === board.name) });
136+
}
137+
this.setState({ selectedBoard: board });
138+
}
139+
37140
protected async setAttachedBoards() {
38141
const { boards } = await this.props.boardService.getAttachedBoards();
39142
this.attachedBoards = boards;
40143
if (this.attachedBoards.length) {
144+
await this.createBoardDropdownItems();
41145
await this.props.boardService.selectBoard(this.attachedBoards[0]);
42146
this.setSelectedBoard(this.attachedBoards[0]);
43-
}
147+
}
44148
}
45149

46-
setSelectedBoard(board: Board) {
47-
if (this.attachedBoards && this.attachedBoards.length) {
48-
this.setState({ selectedIsAttached: !!this.attachedBoards.find(attachedBoard => attachedBoard.name === board.name) });
150+
protected createBoardDropdownItems() {
151+
const boardItems: BoardsDropdownItem[] = [];
152+
this.attachedBoards.forEach(board => {
153+
const { commands } = this.props;
154+
const port = this.getPort(board);
155+
const command: Command = {
156+
id: 'selectBoard' + port
157+
}
158+
commands.registerCommand(command, {
159+
execute: () => {
160+
commands.executeCommand(ArduinoCommands.SELECT_BOARD.id, board);
161+
this.setState({ isOpen: false, selectedBoard: board });
162+
}
163+
});
164+
boardItems.push({
165+
commandExecutor: () => commands.executeCommand(command.id),
166+
label: board.name + ' at ' + port,
167+
isSelected: () => this.doIsSelectedBoard(board)
168+
});
169+
});
170+
this.setState({ boardItems });
171+
}
172+
173+
protected doIsSelectedBoard = (board: Board) => this.isSelectedBoard(board);
174+
protected isSelectedBoard(board: Board): boolean {
175+
return AttachedSerialBoard.is(board) &&
176+
!!this.state.selectedBoard &&
177+
AttachedSerialBoard.is(this.state.selectedBoard) &&
178+
board.port === this.state.selectedBoard.port &&
179+
board.fqbn === this.state.selectedBoard.fqbn;
180+
}
181+
182+
protected getPort(board: Board): string {
183+
if (AttachedSerialBoard.is(board)) {
184+
return board.port;
49185
}
50-
this.setState({ selectedBoard: board });
186+
return '';
51187
}
52188

53-
protected readonly doShowSelectBoardsMenu = (event: React.MouseEvent<HTMLElement>) => this.showSelectBoardsMenu(event);
189+
protected readonly doShowSelectBoardsMenu = (event: React.MouseEvent<HTMLElement>) => {
190+
this.showSelectBoardsMenu(event);
191+
event.stopPropagation();
192+
event.nativeEvent.stopImmediatePropagation();
193+
};
54194
protected showSelectBoardsMenu(event: React.MouseEvent<HTMLElement>) {
55-
const el = (event.target as HTMLElement).parentElement;
195+
const el = (event.currentTarget as HTMLElement);
56196
if (el) {
57-
this.props.contextMenuRenderer.render({
58-
menuPath: ArduinoToolbarContextMenu.SELECT_BOARDS_PATH,
59-
anchor: {
60-
x: el.getBoundingClientRect().left,
61-
y: el.getBoundingClientRect().top + el.offsetHeight
62-
}
63-
})
197+
this.dropDownListCoord = {
198+
top: el.getBoundingClientRect().top,
199+
left: el.getBoundingClientRect().left,
200+
paddingTop: el.getBoundingClientRect().height,
201+
width: el.getBoundingClientRect().width
202+
}
203+
this.setState({ isOpen: !this.state.isOpen });
64204
}
65205
}
66206

67207
render(): React.ReactNode {
208+
const selectedBoard = this.state.selectedBoard;
209+
const port = selectedBoard ? this.getPort(selectedBoard) : undefined;
68210
return <React.Fragment>
69-
<div className='arduino-boards-toolbar-item-container' onClick={this.doShowSelectBoardsMenu}>
70-
<div className='arduino-boards-toolbar-item'>
71-
<div className='inner-container'>
72-
<span className={!this.state.selectedBoard || !this.state.selectedIsAttached ? 'fa fa-times notAttached' : ''}></span>
73-
<div className='label'>{this.state.selectedBoard ? this.state.selectedBoard.name : 'no board selected'}</div>
74-
<span className='fa fa-caret-down'></span>
211+
<div className='arduino-boards-toolbar-item-container'>
212+
<div className='arduino-boards-toolbar-item' title={selectedBoard && `${selectedBoard.name}${port ? ' at ' + port : ''}`}>
213+
<div className='inner-container' onClick={this.doShowSelectBoardsMenu}>
214+
<span className={!selectedBoard || !this.state.selectedIsAttached ? 'fa fa-times notAttached' : ''}></span>
215+
<div className='label noWrapInfo'>
216+
<div className='noWrapInfo'>
217+
{selectedBoard ? `${selectedBoard.name}${port ? ' at ' + port : ''}` : 'no board selected'}
218+
</div>
219+
</div>
220+
<span className='fa fa-caret-down caret'></span>
75221
</div>
76222
</div>
77223
</div>
224+
<BoardsDropDown
225+
isOpen={this.state.isOpen}
226+
coords={this.dropDownListCoord}
227+
dropDownItems={this.state.boardItems}
228+
openDialog={this.openDialog}>
229+
</BoardsDropDown>
78230
</React.Fragment>;
79231
}
232+
233+
protected openDialog = () => {
234+
this.props.commands.executeCommand(ArduinoCommands.OPEN_BOARDS_DIALOG.id);
235+
this.setState({ isOpen: false });
236+
};
80237
}

0 commit comments

Comments
 (0)