import { TabBarToolbar } from '@theia/core/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar'; import { codicon } from '@theia/core/lib/browser/widgets/widget'; import { CommandRegistry } from '@theia/core/lib/common/command'; import { Disposable, DisposableCollection, } from '@theia/core/lib/common/disposable'; import { nls } from '@theia/core/lib/common/nls'; import React from '@theia/core/shared/react'; import ReactDOM from '@theia/core/shared/react-dom'; import classNames from 'classnames'; import { boardIdentifierLabel, Port } from '../../common/protocol'; import { BoardListItemUI } from '../../common/protocol/board-list'; import { assertUnreachable } from '../../common/utils'; import type { BoardListUI, BoardsServiceProvider, } from './boards-service-provider'; export interface BoardsDropDownListCoords { readonly top: number; readonly left: number; readonly width: number; readonly paddingTop: number; } export namespace BoardsDropDown { export interface Props { readonly coords: BoardsDropDownListCoords | 'hidden'; readonly boardList: BoardListUI; readonly openBoardsConfig: () => void; readonly hide: () => void; } } export class BoardListDropDown extends React.Component { private dropdownElement: HTMLElement; private listRef: React.RefObject; constructor(props: BoardsDropDown.Props) { super(props); this.listRef = React.createRef(); let list = document.getElementById('boards-dropdown-container'); if (!list) { list = document.createElement('div'); list.id = 'boards-dropdown-container'; document.body.appendChild(list); this.dropdownElement = list; } } override componentDidUpdate(prevProps: BoardsDropDown.Props): void { if (prevProps.coords === 'hidden' && this.listRef.current) { this.listRef.current.focus(); } } override render(): React.ReactNode { return ReactDOM.createPortal( this.renderBoardListItems(), this.dropdownElement ); } private renderBoardListItems(): React.ReactNode { const { coords, boardList } = this.props; if (coords === 'hidden') { return ''; } const footerLabel = nls.localize( 'arduino/board/openBoardsConfig', 'Select other board and port…' ); return (
{boardList.items.map((item, index) => this.renderBoardListItem({ item, selected: index === boardList.selectedIndex, }) )}
this.props.openBoardsConfig()} >
{footerLabel}
); } private readonly onDefaultAction = (item: BoardListItemUI): unknown => { const { boardList, hide } = this.props; const { type, params } = item.defaultAction; hide(); switch (type) { case 'select-boards-config': { return boardList.select(params); } case 'edit-boards-config': { return boardList.edit(params); } default: return assertUnreachable(type); } }; private renderBoardListItem({ item, selected, }: { item: BoardListItemUI; selected: boolean; }): React.ReactNode { const { boardLabel, portLabel, portProtocol, tooltip } = item.labels; const port = item.port; const onKeyUp = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { this.onDefaultAction(item); } }; return (
this.onDefaultAction(item)} onKeyUp={onKeyUp} tabIndex={0} >
{boardLabel}
{portLabel}
{this.renderActions(item)}
); } private renderActions(item: BoardListItemUI): React.ReactNode { const { boardList, hide } = this.props; const { revert, edit } = item.otherActions; if (!edit && !revert) { return undefined; } const handleOnClick = ( event: React.MouseEvent, callback: () => void ) => { event.preventDefault(); event.stopPropagation(); hide(); callback(); }; return (
{edit && (
{
handleOnClick(event, () => boardList.edit(edit.params)) } /> }
)} {revert && (
{
handleOnClick(event, () => boardList.select(revert.params)) } /> }
)}
); } } export class BoardsToolBarItem extends React.Component< BoardsToolBarItem.Props, BoardsToolBarItem.State > { static TOOLBAR_ID: 'boards-toolbar'; private readonly toDispose: DisposableCollection; constructor(props: BoardsToolBarItem.Props) { super(props); const { boardList } = props.boardsServiceProvider; this.state = { boardList, coords: 'hidden', }; const listener = () => this.setState({ coords: 'hidden' }); document.addEventListener('click', listener); this.toDispose = new DisposableCollection( Disposable.create(() => document.removeEventListener('click', listener)) ); } override componentDidMount(): void { this.toDispose.push( this.props.boardsServiceProvider.onBoardListDidChange((boardList) => this.setState({ boardList }) ) ); } override componentWillUnmount(): void { this.toDispose.dispose(); } private readonly show = (event: React.MouseEvent): void => { 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' }); } } event.stopPropagation(); event.nativeEvent.stopImmediatePropagation(); }; private readonly hide = () => { this.setState({ coords: 'hidden' }); }; override render(): React.ReactNode { const { coords, boardList } = this.state; const { boardLabel, selected, portProtocol, tooltip } = boardList.labels; const protocolIcon = portProtocol ? iconNameFromProtocol(portProtocol) : null; const protocolIconClassNames = classNames( 'arduino-boards-toolbar-item--protocol', 'fa', protocolIcon ); return (
{protocolIcon &&
}
{boardLabel}
boardList.edit({ query: '' })} hide={this.hide} /> ); } } export namespace BoardsToolBarItem { export interface Props { readonly boardsServiceProvider: BoardsServiceProvider; readonly commands: CommandRegistry; } export interface State { boardList: BoardListUI; coords: BoardsDropDownListCoords | 'hidden'; } } function iconNameFromProtocol(protocol: string): string { switch (protocol) { case 'serial': return 'fa-arduino-technology-usb'; case 'network': return 'fa-arduino-technology-connection'; // it is fine to assign dedicated icons to the protocols used by the official boards, // but other than that it is best to avoid implementing any special handling // for specific protocols in the IDE codebase. default: return 'fa-arduino-technology-3dimensionscube'; } }