From de1caf14514455fef2ad7c3942d5e0755101d9c6 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Wed, 16 Oct 2019 11:00:44 +0200 Subject: [PATCH] PROEDITOR-46: Added a core auto-installer. Signed-off-by: Akos Kitta --- .../src/browser/arduino-frontend-module.ts | 5 ++ .../browser/boards/boards-auto-installer.ts | 63 +++++++++++++++++++ .../filterable-list-container.tsx | 7 ++- .../components/component-list/list-widget.tsx | 10 ++- .../src/common/protocol/boards-service.ts | 11 ++++ 5 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 arduino-ide-extension/src/browser/boards/boards-auto-installer.ts diff --git a/arduino-ide-extension/src/browser/arduino-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-frontend-module.ts index 75814c07..609ae29b 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-frontend-module.ts @@ -67,6 +67,7 @@ import { TabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-de import { ArduinoTabBarDecoratorService } from './shell/arduino-tab-bar-decorator'; import { ProblemManager } from '@theia/markers/lib/browser'; import { ArduinoProblemManager } from './markers/arduino-problem-manager'; +import { BoardsAutoInstaller } from './boards/boards-auto-installer'; const ElementQueries = require('css-element-queries/src/ElementQueries'); export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => { @@ -120,6 +121,10 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un return client; }).inSingletonScope(); + // boards auto-installer + bind(BoardsAutoInstaller).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(BoardsAutoInstaller); + // Boards list widget bind(BoardsListWidget).toSelf(); bindViewContribution(bind, BoardsListWidgetFrontendContribution); diff --git a/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts b/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts new file mode 100644 index 00000000..2ee282d6 --- /dev/null +++ b/arduino-ide-extension/src/browser/boards/boards-auto-installer.ts @@ -0,0 +1,63 @@ +import { injectable, inject } from 'inversify'; +import { MessageService } from '@theia/core/lib/common/message-service'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; +import { BoardsService, Board } from '../../common/protocol/boards-service'; +import { BoardsServiceClientImpl } from './boards-service-client-impl'; +import { BoardsListWidgetFrontendContribution } from './boards-widget-frontend-contribution'; +import { InstallationProgressDialog } from '../components/installation-progress-dialog'; +import { BoardsConfig } from './boards-config'; + + +/** + * Listens on `BoardsConfig.Config` changes, if a board is selected which does not + * have the corresponding core installed, it proposes the user to install the core. + */ +@injectable() +export class BoardsAutoInstaller implements FrontendApplicationContribution { + + @inject(MessageService) + protected readonly messageService: MessageService; + + @inject(BoardsService) + protected readonly boardsService: BoardsService; + + @inject(BoardsServiceClientImpl) + protected readonly boardsServiceClient: BoardsServiceClientImpl; + + @inject(BoardsListWidgetFrontendContribution) + protected readonly boardsManagerFrontendContribution: BoardsListWidgetFrontendContribution; + + onStart(): void { + this.boardsServiceClient.onBoardsConfigChanged(this.ensureCoreExists.bind(this)); + this.ensureCoreExists(this.boardsServiceClient.boardsConfig); + } + + protected ensureCoreExists(config: BoardsConfig.Config): void { + const { selectedBoard } = config; + if (selectedBoard) { + this.boardsService.search({}).then(({ items }) => { + const candidates = items + .filter(item => item.boards.some(board => Board.sameAs(board, selectedBoard))) + .filter(({ installable, installedVersion }) => installable && !installedVersion); + for (const candidate of candidates) { + // tslint:disable-next-line:max-line-length + this.messageService.info(`The \`"${candidate.name}"\` core has to be installed for the currently selected \`"${selectedBoard.name}"\` board. Do you want to install it now?`, 'Yes', 'Install Manually').then(async answer => { + if (answer === 'Yes') { + const dialog = new InstallationProgressDialog(candidate.name); + dialog.open(); + try { + await this.boardsService.install(candidate); + } finally { + dialog.close(); + } + } + if (answer) { + this.boardsManagerFrontendContribution.openView({ reveal: true }).then(widget => widget.refresh(candidate.name.toLocaleLowerCase())); + } + }); + } + }) + } + } + +} \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/components/component-list/filterable-list-container.tsx b/arduino-ide-extension/src/browser/components/component-list/filterable-list-container.tsx index db93c9dd..bb7ea3aa 100644 --- a/arduino-ide-extension/src/browser/components/component-list/filterable-list-container.tsx +++ b/arduino-ide-extension/src/browser/components/component-list/filterable-list-container.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import debounce = require('lodash.debounce'); +import { Event } from '@theia/core/lib/common/event'; import { Searchable } from '../../../common/protocol/searchable'; import { Installable } from '../../../common/protocol/installable'; import { InstallationProgressDialog } from '../installation-progress-dialog'; @@ -20,6 +21,7 @@ export class FilterableListContainer extends React.Component extends React.Component { const { items } = result; @@ -97,6 +99,7 @@ export namespace FilterableListContainer { readonly itemRenderer: ListItemRenderer; readonly resolveContainer: (element: HTMLElement) => void; readonly resolveFocus: (element: HTMLElement | undefined) => void; + readonly filterTextChangeEvent: Event; } export interface State { diff --git a/arduino-ide-extension/src/browser/components/component-list/list-widget.tsx b/arduino-ide-extension/src/browser/components/component-list/list-widget.tsx index 897a76ff..376c4de2 100644 --- a/arduino-ide-extension/src/browser/components/component-list/list-widget.tsx +++ b/arduino-ide-extension/src/browser/components/component-list/list-widget.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { injectable, postConstruct } from 'inversify'; import { Message } from '@phosphor/messaging'; import { Deferred } from '@theia/core/lib/common/promise-util'; +import { Emitter } from '@theia/core/lib/common/event'; import { MaybePromise } from '@theia/core/lib/common/types'; import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget'; import { Installable } from '../../../common/protocol/installable'; @@ -17,6 +18,7 @@ export abstract class ListWidget extends ReactWidget { */ protected focusNode: HTMLElement | undefined; protected readonly deferredContainer = new Deferred(); + protected readonly filterTextChangeEmitter = new Emitter(); constructor(protected options: ListWidget.Options) { super(); @@ -31,6 +33,7 @@ export abstract class ListWidget extends ReactWidget { this.scrollOptions = { suppressScrollX: true } + this.toDispose.push(this.filterTextChangeEmitter); } @postConstruct() @@ -63,7 +66,12 @@ export abstract class ListWidget extends ReactWidget { searchable={this.options.searchable} installable={this.options.installable} itemLabel={this.options.itemLabel} - itemRenderer={this.options.itemRenderer} />; + itemRenderer={this.options.itemRenderer} + filterTextChangeEvent={this.filterTextChangeEmitter.event}/>; + } + + refresh(filterText: string): void { + this.deferredContainer.promise.then(() => this.filterTextChangeEmitter.fire(filterText)); } } diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts index e59a00b4..9a2fd518 100644 --- a/arduino-ide-extension/src/common/protocol/boards-service.ts +++ b/arduino-ide-extension/src/common/protocol/boards-service.ts @@ -59,6 +59,17 @@ export namespace Board { return left.name === right.name && left.fqbn === right.fqbn; } + export function sameAs(left: Board, right: string | Board): boolean { + // How to associate a selected board with one of the available cores: https://typefox.slack.com/archives/CJJHJCJSJ/p1571142327059200 + // 1. How to use the FQBN if any and infer the package ID from it: https://typefox.slack.com/archives/CJJHJCJSJ/p1571147549069100 + // 2. How to trim the `/Genuino` from the name: https://arduino.slack.com/archives/CJJHJCJSJ/p1571146951066800?thread_ts=1571142327.059200&cid=CJJHJCJSJ + const other = typeof right === 'string' ? { name: right } : right; + if (left.fqbn && other.fqbn) { + return left.fqbn === other.fqbn; + } + return left.name.replace('/Genuino', '') === other.name.replace('/Genuino', ''); + } + export function compare(left: Board, right: Board): number { let result = left.name.localeCompare(right.name); if (result === 0) {