import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { Event } from '@theia/core/lib/common/event'; import { FrontendApplicationState } from '@theia/core/lib/common/frontend-application-state'; import { nls } from '@theia/core/lib/common/nls'; import React from '@theia/core/shared/react'; import { EditBoardsConfigActionParams } from '../../common/protocol/board-list'; import { Board, BoardIdentifier, BoardWithPackage, DetectedPort, findMatchingPortIndex, Port, PortIdentifier, } from '../../common/protocol/boards-service'; import type { Defined } from '../../common/types'; import { NotificationCenter } from '../notification-center'; import { BoardsConfigDialogState } from './boards-config-dialog'; namespace BoardsConfigComponent { export interface Props { /** * This is not the real config, it's only living in the dialog. Users can change it without update and can cancel any modifications. */ readonly boardsConfig: BoardsConfigDialogState; readonly searchSet: BoardIdentifier[] | undefined; readonly notificationCenter: NotificationCenter; readonly appState: FrontendApplicationState; readonly onFocusNodeSet: (element: HTMLElement | undefined) => void; readonly onFilteredTextDidChangeEvent: Event< Defined<EditBoardsConfigActionParams['query']> >; readonly onAppStateDidChange: Event<FrontendApplicationState>; readonly onBoardSelected: (board: BoardIdentifier) => void; readonly onPortSelected: (port: PortIdentifier) => void; readonly searchBoards: (query?: { query?: string; }) => Promise<BoardWithPackage[]>; readonly ports: ( predicate?: (port: DetectedPort) => boolean ) => readonly DetectedPort[]; } export interface State { searchResults: Array<BoardWithPackage>; showAllPorts: boolean; query: string; } } class Item<T> extends React.Component<{ item: T; label: string; selected: boolean; onClick: (item: T) => void; missing?: boolean; details?: string; title?: string | ((item: T) => string); }> { override render(): React.ReactNode { const { selected, label, missing, details, item } = this.props; const classNames = ['item']; if (selected) { classNames.push('selected'); } if (missing === true) { classNames.push('missing'); } let title = this.props.title ?? `${label}${!details ? '' : details}`; if (typeof title === 'function') { title = title(item); } return ( <div onClick={this.onClick} className={classNames.join(' ')} title={title} > <div className="label">{label}</div> {!details ? '' : <div className="details">{details}</div>} {!selected ? ( '' ) : ( <div className="selected-icon"> <i className="fa fa-check" /> </div> )} </div> ); } private readonly onClick = () => { this.props.onClick(this.props.item); }; } export class BoardsConfigComponent extends React.Component< BoardsConfigComponent.Props, BoardsConfigComponent.State > { private readonly toDispose: DisposableCollection; constructor(props: BoardsConfigComponent.Props) { super(props); this.state = { searchResults: [], showAllPorts: false, query: '', }; this.toDispose = new DisposableCollection(); } override componentDidMount(): void { this.toDispose.pushAll([ this.props.onAppStateDidChange(async (state) => { if (state === 'ready') { const searchResults = await this.queryBoards({}); this.setState({ searchResults }); } }), this.props.notificationCenter.onPlatformDidInstall(() => this.updateBoards(this.state.query) ), this.props.notificationCenter.onPlatformDidUninstall(() => this.updateBoards(this.state.query) ), this.props.notificationCenter.onIndexUpdateDidComplete(() => this.updateBoards(this.state.query) ), this.props.notificationCenter.onDaemonDidStart(() => this.updateBoards(this.state.query) ), this.props.notificationCenter.onDaemonDidStop(() => this.setState({ searchResults: [] }) ), this.props.onFilteredTextDidChangeEvent((query) => { if (typeof query === 'string') { this.setState({ query }, () => this.updateBoards(this.state.query)); } }), ]); } override componentWillUnmount(): void { this.toDispose.dispose(); } private readonly updateBoards = ( eventOrQuery: React.ChangeEvent<HTMLInputElement> | string = '' ) => { const query = typeof eventOrQuery === 'string' ? eventOrQuery : eventOrQuery.target.value.toLowerCase(); this.setState({ query }); this.queryBoards({ query }).then((searchResults) => this.setState({ searchResults }) ); }; private readonly queryBoards = async ( options: { query?: string } = {} ): Promise<Array<BoardWithPackage>> => { const result = await this.props.searchBoards(options); const { searchSet } = this.props; if (searchSet) { return result.filter((board) => searchSet.some( (restriction) => restriction.fqbn === board.fqbn || restriction.name === board.fqbn ) ); } return result; }; private readonly toggleFilterPorts = () => { this.setState({ showAllPorts: !this.state.showAllPorts }); }; private readonly selectPort = (selectedPort: PortIdentifier) => { this.props.onPortSelected(selectedPort); }; private readonly selectBoard = (selectedBoard: BoardWithPackage) => { this.props.onBoardSelected(selectedBoard); }; private readonly focusNodeSet = (element: HTMLElement | null) => { this.props.onFocusNodeSet(element || undefined); }; override render(): React.ReactNode { return ( <> {this.renderContainer( nls.localize('arduino/board/boards', 'boards'), this.renderBoards.bind(this) )} {this.renderContainer( nls.localize('arduino/board/ports', 'ports'), this.renderPorts.bind(this), this.renderPortsFooter.bind(this) )} </> ); } private renderContainer( title: string, contentRenderer: () => React.ReactNode, footerRenderer?: () => React.ReactNode ): React.ReactNode { return ( <div className="container"> <div className="content"> <div className="title">{title}</div> {contentRenderer()} <div className="footer">{footerRenderer ? footerRenderer() : ''}</div> </div> </div> ); } private renderBoards(): React.ReactNode { const { boardsConfig } = this.props; const { searchResults, query } = this.state; // Board names are not unique per core https://github.com/arduino/arduino-pro-ide/issues/262#issuecomment-661019560 // It is tricky when the core is not yet installed, no FQBNs are available. const distinctBoards = new Map<string, Board.Detailed>(); const toKey = ({ name, packageName, fqbn }: Board.Detailed) => !!fqbn ? `${name}-${packageName}-${fqbn}` : `${name}-${packageName}`; for (const board of Board.decorateBoards( boardsConfig.selectedBoard, searchResults )) { const key = toKey(board); if (!distinctBoards.has(key)) { distinctBoards.set(key, board); } } const title = (board: Board.Detailed): string => { const { details, manuallyInstalled } = board; let label = board.name; if (details) { label += details; } if (manuallyInstalled) { label += nls.localize('arduino/board/inSketchbook', ' (in Sketchbook)'); } return label; }; const boardsList = Array.from(distinctBoards.values()).map((board) => ( <Item<Board.Detailed> key={toKey(board)} item={board} label={board.name} details={board.details} selected={board.selected} onClick={this.selectBoard} missing={board.missing} title={title} /> )); return ( <React.Fragment> <div className="search"> <input type="search" value={query} className="theia-input" placeholder={nls.localize( 'arduino/board/searchBoard', 'Search board' )} onChange={this.updateBoards} ref={this.focusNodeSet} /> <i className="fa fa-search"></i> </div> {boardsList.length > 0 ? ( <div className="boards list">{boardsList}</div> ) : ( <div className="no-result"> {nls.localize( 'arduino/board/noBoardsFound', 'No boards found for "{0}"', query )} </div> )} </React.Fragment> ); } private renderPorts(): React.ReactNode { const predicate = this.state.showAllPorts ? undefined : Port.isVisiblePort; const detectedPorts = this.props.ports(predicate); const matchingIndex = findMatchingPortIndex( this.props.boardsConfig.selectedPort, detectedPorts ); return !detectedPorts.length ? ( <div className="no-result"> {nls.localize('arduino/board/noPortsDiscovered', 'No ports discovered')} </div> ) : ( <div className="ports list"> {detectedPorts.map((detectedPort, index) => ( <Item<Port> key={`${Port.keyOf(detectedPort.port)}`} item={detectedPort.port} label={Port.toString(detectedPort.port)} selected={index === matchingIndex} onClick={this.selectPort} /> ))} </div> ); } private renderPortsFooter(): React.ReactNode { return ( <div className="noselect"> <label title={nls.localize( 'arduino/board/showAllAvailablePorts', 'Shows all available ports when enabled' )} > <input type="checkbox" defaultChecked={this.state.showAllPorts} onChange={this.toggleFilterPorts} /> <span> {nls.localize('arduino/board/showAllPorts', 'Show all ports')} </span> </label> </div> ); } }