From ccd44c2665cb5d209dba3d90d0b743d8ee356977 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Tue, 12 Oct 2021 16:06:23 +0200 Subject: [PATCH 01/11] Rebuild gRPC protocol interfaces --- .../cc/arduino/cli/commands/v1/commands_grpc_pb.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb.d.ts b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb.d.ts index 9f34ff24f..25add2fc3 100644 --- a/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb.d.ts +++ b/arduino-ide-extension/src/node/cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb.d.ts @@ -680,7 +680,6 @@ export class ArduinoCoreServiceClient extends grpc.Client implements IArduinoCor public libraryList(request: cc_arduino_cli_commands_v1_lib_pb.LibraryListRequest, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_lib_pb.LibraryListResponse) => void): grpc.ClientUnaryCall; public libraryList(request: cc_arduino_cli_commands_v1_lib_pb.LibraryListRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_lib_pb.LibraryListResponse) => void): grpc.ClientUnaryCall; public libraryList(request: cc_arduino_cli_commands_v1_lib_pb.LibraryListRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_lib_pb.LibraryListResponse) => void): grpc.ClientUnaryCall; - public monitor(options?: Partial): grpc.ClientDuplexStream; public monitor(metadata?: grpc.Metadata, options?: Partial): grpc.ClientDuplexStream; public enumerateMonitorPortSettings(request: cc_arduino_cli_commands_v1_monitor_pb.EnumerateMonitorPortSettingsRequest, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_monitor_pb.EnumerateMonitorPortSettingsResponse) => void): grpc.ClientUnaryCall; public enumerateMonitorPortSettings(request: cc_arduino_cli_commands_v1_monitor_pb.EnumerateMonitorPortSettingsRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: cc_arduino_cli_commands_v1_monitor_pb.EnumerateMonitorPortSettingsResponse) => void): grpc.ClientUnaryCall; From 10d96fc7546eaf556d967af0f83e0f3b3cfe14b4 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Tue, 12 Oct 2021 16:08:30 +0200 Subject: [PATCH 02/11] Implement methods to get user fields for board/port combination --- .../browser/boards/boards-service-provider.ts | 85 ++++++++++++++----- .../src/common/protocol/boards-service.ts | 9 ++ .../src/node/boards-service-impl.ts | 32 +++++++ 3 files changed, 103 insertions(+), 23 deletions(-) diff --git a/arduino-ide-extension/src/browser/boards/boards-service-provider.ts b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts index 97c660e87..295891c8f 100644 --- a/arduino-ide-extension/src/browser/boards/boards-service-provider.ts +++ b/arduino-ide-extension/src/browser/boards/boards-service-provider.ts @@ -12,6 +12,7 @@ import { BoardsPackage, AttachedBoardsChangeEvent, BoardWithPackage, + BoardUserField, } from '../../common/protocol'; import { BoardsConfig } from './boards-config'; import { naturalCompare } from '../../common/utils'; @@ -68,7 +69,8 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { * This event is also emitted when the board package for the currently selected board was uninstalled. */ readonly onBoardsConfigChanged = this.onBoardsConfigChangedEmitter.event; - readonly onAvailableBoardsChanged = this.onAvailableBoardsChangedEmitter.event; + readonly onAvailableBoardsChanged = + this.onAvailableBoardsChangedEmitter.event; readonly onAvailablePortsChanged = this.onAvailablePortsChangedEmitter.event; onStart(): void { @@ -183,8 +185,8 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { const selectedAvailableBoard = AvailableBoard.is(selectedBoard) ? selectedBoard : this._availableBoards.find((availableBoard) => - Board.sameAs(availableBoard, selectedBoard) - ); + Board.sameAs(availableBoard, selectedBoard) + ); if ( selectedAvailableBoard && selectedAvailableBoard.selected && @@ -274,6 +276,18 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { return boards; } + async selectedBoardUserFields(): Promise { + if (!this._boardsConfig.selectedBoard || !this._boardsConfig.selectedPort) { + return []; + } + const fqbn = this._boardsConfig.selectedBoard.fqbn; + if (!fqbn) { + return []; + } + const protocol = this._boardsConfig.selectedPort.protocol; + return await this.boardsService.getBoardUserFields({ fqbn, protocol }); + } + /** * `true` if the `config.selectedBoard` is defined; hence can compile against the board. Otherwise, `false`. */ @@ -361,14 +375,14 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { const timeoutTask = !!timeout && timeout > 0 ? new Promise((_, reject) => - setTimeout( - () => reject(new Error(`Timeout after ${timeout} ms.`)), - timeout + setTimeout( + () => reject(new Error(`Timeout after ${timeout} ms.`)), + timeout + ) ) - ) : new Promise(() => { - /* never */ - }); + /* never */ + }); const waitUntilTask = new Promise((resolve) => { let candidate = find(what, this.availableBoards); if (candidate) { @@ -406,7 +420,7 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { const availableBoards: AvailableBoard[] = []; const attachedBoards = this._attachedBoards.filter(({ port }) => !!port); const availableBoardPorts = availablePorts.filter((port) => { - if (port.protocol === "serial") { + if (port.protocol === 'serial') { // We always show all serial ports, even if there // is no recognized board connected to it return true; @@ -424,8 +438,12 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { }); for (const boardPort of availableBoardPorts) { - let board = attachedBoards.find(({ port }) => Port.sameAs(boardPort, port)); - const lastSelectedBoard = await this.getLastSelectedBoardOnPort(boardPort); + const board = attachedBoards.find(({ port }) => + Port.sameAs(boardPort, port) + ); + const lastSelectedBoard = await this.getLastSelectedBoardOnPort( + boardPort + ); let availableBoard = {} as AvailableBoard; if (board) { @@ -454,11 +472,16 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { availableBoards.push(availableBoard); } - if (boardsConfig.selectedBoard && !availableBoards.some(({ selected }) => selected)) { + if ( + boardsConfig.selectedBoard && + !availableBoards.some(({ selected }) => selected) + ) { // If the selected board has the same port of an unknown board // that is already in availableBoards we might get a duplicate port. // So we remove the one already in the array and add the selected one. - const found = availableBoards.findIndex(board => board.port?.address === boardsConfig.selectedPort?.address); + const found = availableBoards.findIndex( + (board) => board.port?.address === boardsConfig.selectedPort?.address + ); if (found >= 0) { availableBoards.splice(found, 1); } @@ -475,7 +498,9 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { let hasChanged = availableBoards.length !== currentAvailableBoards.length; for (let i = 0; !hasChanged && i < availableBoards.length; i++) { const [left, right] = [availableBoards[i], currentAvailableBoards[i]]; - hasChanged = !!AvailableBoard.compare(left, right) || left.selected !== right.selected; + hasChanged = + !!AvailableBoard.compare(left, right) || + left.selected !== right.selected; } if (hasChanged) { this._availableBoards = availableBoards; @@ -483,7 +508,9 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { } } - protected async getLastSelectedBoardOnPort(port: Port): Promise { + protected async getLastSelectedBoardOnPort( + port: Port + ): Promise { const key = this.getLastSelectedBoardOnPortKey(port); return this.getData(key); } @@ -504,8 +531,11 @@ export class BoardsServiceProvider implements FrontendApplicationContribution { ]); } - protected getLastSelectedBoardOnPortKey(port: Port): string { - return `last-selected-board-on-port:${Port.toString(port)}`; + protected getLastSelectedBoardOnPortKey(port: Port | string): string { + // TODO: we lose the port's `protocol` info (`serial`, `network`, etc.) here if the `port` is a `string`. + return `last-selected-board-on-port:${ + typeof port === 'string' ? port : Port.toString(port) + }`; } protected async loadState(): Promise { @@ -596,13 +626,22 @@ export namespace AvailableBoard { // 4. Network with recognized boards // 5. Other protocols with recognized boards export const compare = (left: AvailableBoard, right: AvailableBoard) => { - if (left.port?.protocol === "serial" && right.port?.protocol !== "serial") { + if (left.port?.protocol === 'serial' && right.port?.protocol !== 'serial') { return -1; - } else if (left.port?.protocol !== "serial" && right.port?.protocol === "serial") { + } else if ( + left.port?.protocol !== 'serial' && + right.port?.protocol === 'serial' + ) { return 1; - } else if (left.port?.protocol === "network" && right.port?.protocol !== "network") { + } else if ( + left.port?.protocol === 'network' && + right.port?.protocol !== 'network' + ) { return -1; - } else if (left.port?.protocol !== "network" && right.port?.protocol === "network") { + } else if ( + left.port?.protocol !== 'network' && + right.port?.protocol === 'network' + ) { return 1; } else if (left.port?.protocol === right.port?.protocol) { // We show all ports, including those that have guessed @@ -614,5 +653,5 @@ export namespace AvailableBoard { } } return naturalCompare(left.port?.address!, right.port?.address!); - } + }; } diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts index 714b00374..b0ef9fe75 100644 --- a/arduino-ide-extension/src/common/protocol/boards-service.ts +++ b/arduino-ide-extension/src/common/protocol/boards-service.ts @@ -143,6 +143,7 @@ export interface BoardsService fqbn: string; }): Promise; searchBoards({ query }: { query?: string }): Promise; + getBoardUserFields(options: { fqbn: string, protocol: string }): Promise; } export interface Port { @@ -251,6 +252,14 @@ export interface Board { readonly port?: Port; } +export interface BoardUserField { + readonly toolId: string; + readonly name: string; + readonly label: string; + readonly secret: boolean; + value: string; +} + export interface BoardWithPackage extends Board { readonly packageName: string; readonly packageId: string; diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index 20dfdc6ef..acb4a7315 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -16,6 +16,7 @@ import { NotificationServiceServer, AvailablePorts, BoardWithPackage, + BoardUserField, } from '../common/protocol'; import { PlatformInstallRequest, @@ -36,6 +37,8 @@ import { import { ListProgrammersAvailableForUploadRequest, ListProgrammersAvailableForUploadResponse, + SupportedUserFieldsRequest, + SupportedUserFieldsResponse, } from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb'; import { InstallWithProgress } from './grpc-installable'; @@ -244,6 +247,35 @@ export class BoardsServiceImpl return boards; } + async getBoardUserFields(options: { fqbn: string, protocol: string }): Promise { + await this.coreClientProvider.initialized; + const coreClient = await this.coreClient(); + const { client, instance } = coreClient; + + const supportedUserFieldsReq = new SupportedUserFieldsRequest(); + supportedUserFieldsReq.setInstance(instance); + supportedUserFieldsReq.setFqbn(options.fqbn); + supportedUserFieldsReq.setProtocol(options.protocol); + + const supportedUserFieldsResp = await new Promise( + (resolve, reject) => { + client.supportedUserFields(supportedUserFieldsReq, (err, resp) => { + (!!err ? reject : resolve)(!!err ? err : resp) + }) + } + ); + return supportedUserFieldsResp.getUserFieldsList().map(e => { + return { + toolId: e.getToolId(), + name: e.getName(), + label: e.getLabel(), + secret: e.getSecret(), + value: "", + }; + }); + } + + async search(options: { query?: string }): Promise { await this.coreClientProvider.initialized; const coreClient = await this.coreClient(); From 33de26eb34c32038236b5092acad80788f88c1b5 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Mon, 18 Oct 2021 10:10:58 +0200 Subject: [PATCH 03/11] Implement dialog to input board user fields --- .../browser/arduino-ide-frontend-module.ts | 11 ++ .../user-fields/user-fields-component.tsx | 90 +++++++++++++ .../user-fields/user-fields-dialog.tsx | 121 ++++++++++++++++++ .../src/browser/style/index.css | 81 ++++++------ .../src/browser/style/user-fields-dialog.css | 28 ++++ 5 files changed, 293 insertions(+), 38 deletions(-) create mode 100644 arduino-ide-extension/src/browser/dialogs/user-fields/user-fields-component.tsx create mode 100644 arduino-ide-extension/src/browser/dialogs/user-fields/user-fields-dialog.tsx create mode 100644 arduino-ide-extension/src/browser/style/user-fields-dialog.css diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 459e6be39..00a1ae5ad 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -254,6 +254,11 @@ import { UploadCertificateDialogWidget, } from './dialogs/certificate-uploader/certificate-uploader-dialog'; import { PlotterFrontendContribution } from './serial/plotter/plotter-frontend-contribution'; +import { + UserFieldsDialog, + UserFieldsDialogProps, + UserFieldsDialogWidget, +} from './dialogs/user-fields/user-fields-dialog'; import { nls } from '@theia/core/lib/browser/nls'; const ElementQueries = require('css-element-queries/src/ElementQueries'); @@ -739,4 +744,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(UploadCertificateDialogProps).toConstantValue({ title: 'UploadCertificate', }); + + bind(UserFieldsDialogWidget).toSelf().inSingletonScope(); + bind(UserFieldsDialog).toSelf().inSingletonScope(); + bind(UserFieldsDialogProps).toConstantValue({ + title: 'UserFields', + }); }); diff --git a/arduino-ide-extension/src/browser/dialogs/user-fields/user-fields-component.tsx b/arduino-ide-extension/src/browser/dialogs/user-fields/user-fields-component.tsx new file mode 100644 index 000000000..b9fbc4700 --- /dev/null +++ b/arduino-ide-extension/src/browser/dialogs/user-fields/user-fields-component.tsx @@ -0,0 +1,90 @@ +import * as React from 'react'; +import { BoardUserField } from '../../../common/protocol'; +import { nls } from '@theia/core/lib/browser/nls'; + +export const UserFieldsComponent = ({ + initialBoardUserFields, + updateUserFields, + cancel, + accept, +}: { + initialBoardUserFields: BoardUserField[]; + updateUserFields: (userFields: BoardUserField[]) => void; + cancel: () => void; + accept: () => Promise; +}): React.ReactElement => { + const [boardUserFields, setBoardUserFields] = React.useState< + BoardUserField[] + >(initialBoardUserFields); + + const [uploadButtonDisabled, setUploadButtonDisabled] = + React.useState(true); + + React.useEffect(() => { + setBoardUserFields(initialBoardUserFields); + }, [initialBoardUserFields]); + + const updateUserField = + (index: number) => (e: React.ChangeEvent) => { + let newBoardUserFields = [...boardUserFields]; + newBoardUserFields[index].value = e.target.value; + setBoardUserFields(newBoardUserFields); + }; + + const allFieldsHaveValues = (userFields: BoardUserField[]): boolean => { + return userFields + .map((field: BoardUserField): boolean => { + return field.value.length > 0; + }) + .reduce((previous: boolean, current: boolean): boolean => { + return previous && current; + }); + }; + + React.useEffect(() => { + updateUserFields(boardUserFields); + setUploadButtonDisabled(!allFieldsHaveValues(boardUserFields)); + }, [boardUserFields]); + + return ( +
+ {boardUserFields.map((field, index) => { + return ( +
+
+ +
+
+ +
+
+ ); + })} +
+
+ + +
+
+
+ ); +}; diff --git a/arduino-ide-extension/src/browser/dialogs/user-fields/user-fields-dialog.tsx b/arduino-ide-extension/src/browser/dialogs/user-fields/user-fields-dialog.tsx new file mode 100644 index 000000000..aa1bd2646 --- /dev/null +++ b/arduino-ide-extension/src/browser/dialogs/user-fields/user-fields-dialog.tsx @@ -0,0 +1,121 @@ +import * as React from 'react'; +import { inject, injectable } from 'inversify'; +import { + AbstractDialog, + DialogProps, + ReactWidget, +} from '@theia/core/lib/browser'; +import { Widget } from '@phosphor/widgets'; +import { Message } from '@phosphor/messaging'; +import { UploadSketch } from '../../contributions/upload-sketch'; +import { UserFieldsComponent } from './user-fields-component'; +import { BoardUserField } from '../../../common/protocol'; + +@injectable() +export class UserFieldsDialogWidget extends ReactWidget { + protected _currentUserFields: BoardUserField[] = []; + + constructor(private cancel: () => void, private accept: () => Promise) { + super(); + } + + set currentUserFields(userFields: BoardUserField[]) { + this.setUserFields(userFields); + } + + get currentUserFields(): BoardUserField[] { + return this._currentUserFields; + } + + resetUserFieldsValue(): void { + this._currentUserFields = this._currentUserFields.map((field) => { + field.value = ''; + return field; + }); + } + + protected setUserFields(userFields: BoardUserField[]): void { + this._currentUserFields = userFields; + } + + protected render(): React.ReactNode { + return ( +
+ + + ); + } +} + +@injectable() +export class UserFieldsDialogProps extends DialogProps {} + +@injectable() +export class UserFieldsDialog extends AbstractDialog { + protected readonly widget: UserFieldsDialogWidget; + + constructor( + @inject(UserFieldsDialogProps) + protected readonly props: UserFieldsDialogProps + ) { + super({ + title: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label || '', + }); + this.titleNode.classList.add('user-fields-dialog-title'); + this.contentNode.classList.add('user-fields-dialog-content'); + this.acceptButton = undefined; + this.widget = new UserFieldsDialogWidget( + this.close.bind(this), + this.accept.bind(this) + ); + } + + set value(userFields: BoardUserField[]) { + this.widget.currentUserFields = userFields; + } + + get value(): BoardUserField[] { + return this.widget.currentUserFields; + } + + protected onAfterAttach(msg: Message): void { + if (this.widget.isAttached) { + Widget.detach(this.widget); + } + Widget.attach(this.widget, this.contentNode); + super.onAfterAttach(msg); + this.update(); + } + + protected onUpdateRequest(msg: Message): void { + super.onUpdateRequest(msg); + this.widget.update(); + } + + protected onActivateRequest(msg: Message): void { + super.onActivateRequest(msg); + this.widget.activate(); + } + + protected async accept(): Promise { + // If the user presses enter and at least + // a field is empty don't accept the input + for (const field of this.value) { + if (field.value.length === 0) { + return; + } + } + return super.accept(); + } + + close(): void { + this.widget.resetUserFieldsValue(); + this.widget.close(); + super.close(); + } +} diff --git a/arduino-ide-extension/src/browser/style/index.css b/arduino-ide-extension/src/browser/style/index.css index 9ce6b8062..9e52aeb07 100644 --- a/arduino-ide-extension/src/browser/style/index.css +++ b/arduino-ide-extension/src/browser/style/index.css @@ -10,6 +10,7 @@ @import './settings-dialog.css'; @import './firmware-uploader-dialog.css'; @import './certificate-uploader-dialog.css'; +@import './user-fields-dialog.css'; @import './debug.css'; @import './sketchbook.css'; @import './cloud-sketchbook.css'; @@ -17,87 +18,91 @@ @import './custom-codicon.css'; .theia-input.warning:focus { - outline-width: 1px; - outline-style: solid; - outline-offset: -1px; - opacity: 1 !important; - color: var(--theia-warningForeground); - background-color: var(--theia-warningBackground); + outline-width: 1px; + outline-style: solid; + outline-offset: -1px; + opacity: 1 !important; + color: var(--theia-warningForeground); + background-color: var(--theia-warningBackground); } .theia-input.warning { - background-color: var(--theia-warningBackground); + background-color: var(--theia-warningBackground); } -.theia-input.warning::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ - color: var(--theia-warningForeground); - background-color: var(--theia-warningBackground); - opacity: 1; /* Firefox */ +.theia-input.warning::placeholder { + /* Chrome, Firefox, Opera, Safari 10.1+ */ + color: var(--theia-warningForeground); + background-color: var(--theia-warningBackground); + opacity: 1; /* Firefox */ } -.theia-input.warning:-ms-input-placeholder { /* Internet Explorer 10-11 */ - color: var(--theia-warningForeground); - background-color: var(--theia-warningBackground); +.theia-input.warning:-ms-input-placeholder { + /* Internet Explorer 10-11 */ + color: var(--theia-warningForeground); + background-color: var(--theia-warningBackground); } -.theia-input.warning::-ms-input-placeholder { /* Microsoft Edge */ - color: var(--theia-warningForeground); - background-color: var(--theia-warningBackground); +.theia-input.warning::-ms-input-placeholder { + /* Microsoft Edge */ + color: var(--theia-warningForeground); + background-color: var(--theia-warningBackground); } -/* Makes the sidepanel a bit wider when opening the widget */ +/* Makes the sidepanel a bit wider when opening the widget */ .p-DockPanel-widget { - min-width: 200px; - min-height: 200px; + min-width: 200px; + min-height: 200px; } /* Overrule the default Theia CSS button styles. */ button.theia-button, .theia-button { - border: 1px solid var(--theia-dropdown-border); + border: 1px solid var(--theia-dropdown-border); } button.theia-button:hover, .theia-button:hover { - border: 1px solid var(--theia-focusBorder); + border: 1px solid var(--theia-focusBorder); } button.theia-button { - height: 31px; + height: 31px; } button.theia-button.secondary { - background-color: var(--theia-secondaryButton-background); - color: var(--theia-secondaryButton-foreground); + background-color: var(--theia-secondaryButton-background); + color: var(--theia-secondaryButton-foreground); } button.theia-button.main { - color: var(--theia-button-foreground); + color: var(--theia-button-foreground); } /* To make the progress-bar slightly thicker, and use the color from the status bar */ .theia-progress-bar-container { - width: 100%; - height: 4px; + width: 100%; + height: 4px; } .theia-progress-bar { - height: 4px; - width: 3%; - animation: progress-animation 1.3s 0s infinite cubic-bezier(0.645, 0.045, 0.355, 1); + height: 4px; + width: 3%; + animation: progress-animation 1.3s 0s infinite + cubic-bezier(0.645, 0.045, 0.355, 1); } .theia-notification-item-progressbar { - height: 4px; - width: 66%; + height: 4px; + width: 66%; } .flex-line { - display: flex; - align-items: center; - white-space: nowrap; + display: flex; + align-items: center; + white-space: nowrap; } .fa-reload { - font-size: 14px; -} \ No newline at end of file + font-size: 14px; +} diff --git a/arduino-ide-extension/src/browser/style/user-fields-dialog.css b/arduino-ide-extension/src/browser/style/user-fields-dialog.css new file mode 100644 index 000000000..4924f9a47 --- /dev/null +++ b/arduino-ide-extension/src/browser/style/user-fields-dialog.css @@ -0,0 +1,28 @@ +.user-fields-dialog-title { + font-family: Open Sans; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 27px; + letter-spacing: 0.01em; + text-align: left; +} + +.user-fields-dialog-content { + width: 408px; +} + +.user-fields-dialog-content .field-label { + color: #2c353a; + font-family: Open Sans; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 21px; + letter-spacing: 0.01em; + text-align: left; +} + +.user-fields-dialog-content .theia-input { + flex-grow: 1; +} From 53b4f17b7ace3dcd002e14c276d6850c423c2043 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Mon, 18 Oct 2021 10:13:53 +0200 Subject: [PATCH 04/11] Add configure and upload step when uploading to board requiring user fields --- .../browser/contributions/upload-sketch.ts | 91 ++++++++++++++++++- .../src/common/protocol/core-service.ts | 2 + .../src/node/core-service-impl.ts | 5 + 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts index c464ec811..1eacb3a2e 100644 --- a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts @@ -1,6 +1,6 @@ -import { inject, injectable } from 'inversify'; +import { inject, injectable, postConstruct } from 'inversify'; import { Emitter } from '@theia/core/lib/common/event'; -import { CoreService } from '../../common/protocol'; +import { BoardUserField, CoreService } from '../../common/protocol'; import { ArduinoMenus } from '../menu/arduino-menus'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { BoardsDataStore } from '../boards/boards-data-store'; @@ -14,6 +14,7 @@ import { KeybindingRegistry, TabBarToolbarRegistry, } from './contribution'; +import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog'; import { nls } from '@theia/core/lib/browser/nls'; @injectable() @@ -30,16 +31,81 @@ export class UploadSketch extends SketchContribution { @inject(BoardsServiceProvider) protected readonly boardsServiceClientImpl: BoardsServiceProvider; + @inject(UserFieldsDialog) + protected readonly userFieldsDialog: UserFieldsDialog; + + protected cachedUserFields: Map = new Map(); + protected readonly onDidChangeEmitter = new Emitter>(); readonly onDidChange = this.onDidChangeEmitter.event; protected uploadInProgress = false; + protected boardRequiresUserFields = false; + + @postConstruct() + protected init(): void { + this.boardsServiceClientImpl.onBoardsConfigChanged(async () => { + const userFields = await this.boardsServiceClientImpl.selectedBoardUserFields(); + this.boardRequiresUserFields = userFields.length > 0; + }) + } + + private selectedFqbnAddress(): string { + const { boardsConfig } = this.boardsServiceClientImpl; + const fqbn = boardsConfig.selectedBoard?.fqbn; + if (!fqbn) { + return ""; + } + const address = boardsConfig.selectedBoard?.port?.address + if (!address) { + return ""; + } + return fqbn + "|" + address; + } registerCommands(registry: CommandRegistry): void { registry.registerCommand(UploadSketch.Commands.UPLOAD_SKETCH, { - execute: () => this.uploadSketch(), + execute: async () => { + const key = this.selectedFqbnAddress(); + if (!key) { + return; + } + if (this.boardRequiresUserFields && !this.cachedUserFields.has(key)) { + // Deep clone the array of board fields to avoid editing the cached ones + this.userFieldsDialog.value = (await this.boardsServiceClientImpl.selectedBoardUserFields()).map(f => ({ ...f })); + const result = await this.userFieldsDialog.open(); + if (!result) { + return; + } + this.cachedUserFields.set(key, result); + } + this.uploadSketch(); + }, isEnabled: () => !this.uploadInProgress, }); + registry.registerCommand( + UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION, + { + execute: async () => { + const key = this.selectedFqbnAddress(); + if (!key) { + return; + } + + const cached = this.cachedUserFields.get(key); + // Deep clone the array of board fields to avoid editing the cached ones + this.userFieldsDialog.value = (cached ?? await this.boardsServiceClientImpl.selectedBoardUserFields()).map(f => ({ ...f })); + + const result = await this.userFieldsDialog.open() + if (!result) { + return; + } + this.cachedUserFields.set(key, result); + this.uploadSketch(); + }, + isEnabled: () => !this.uploadInProgress && this.boardRequiresUserFields, + } + ); registry.registerCommand( UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER, { @@ -63,13 +129,18 @@ export class UploadSketch extends SketchContribution { label: nls.localize('arduino/sketch/upload', 'Upload'), order: '1', }); + registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { + commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id, + label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label, + order: '2', + }); registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id, label: nls.localize( 'arduino/sketch/uploadUsingProgrammer', 'Upload Using Programmer' ), - order: '2', + order: '3', }); } @@ -127,6 +198,11 @@ export class UploadSketch extends SketchContribution { const optimizeForDebug = this.editorMode.compileForDebug; const { selectedPort } = boardsConfig; const port = selectedPort; + const userFields = this.cachedUserFields.get(this.selectedFqbnAddress()); + if (!userFields) { + this.messageService.error(nls.localize('arduino/sketch/userFieldsNotFoundError', "Can't find user fields for connected board")); + return; + } if (usingProgrammer) { const programmer = selectedProgrammer; @@ -139,6 +215,7 @@ export class UploadSketch extends SketchContribution { verbose, verify, sourceOverride, + userFields, }; } else { options = { @@ -149,6 +226,7 @@ export class UploadSketch extends SketchContribution { verbose, verify, sourceOverride, + userFields, }; } this.outputChannelManager.getChannel('Arduino').clear(); @@ -197,6 +275,11 @@ export namespace UploadSketch { export const UPLOAD_SKETCH: Command = { id: 'arduino-upload-sketch', }; + export const UPLOAD_WITH_CONFIGURATION: Command = { + id: 'arduino-upload-with-configuration-sketch', + label: nls.localize('arduino/sketch/configureAndUpload', 'Configure And Upload'), + category: 'Arduino', + } export const UPLOAD_SKETCH_USING_PROGRAMMER: Command = { id: 'arduino-upload-sketch-using-programmer', }; diff --git a/arduino-ide-extension/src/common/protocol/core-service.ts b/arduino-ide-extension/src/common/protocol/core-service.ts index 7e252bc2a..f8216f504 100644 --- a/arduino-ide-extension/src/common/protocol/core-service.ts +++ b/arduino-ide-extension/src/common/protocol/core-service.ts @@ -1,3 +1,4 @@ +import { BoardUserField } from '.'; import { Port } from '../../common/protocol/boards-service'; import { Programmer } from './boards-service'; @@ -44,6 +45,7 @@ export namespace CoreService { readonly port?: Port | undefined; readonly programmer?: Programmer | undefined; readonly verify: boolean; + readonly userFields: BoardUserField[]; } } diff --git a/arduino-ide-extension/src/node/core-service-impl.ts b/arduino-ide-extension/src/node/core-service-impl.ts index f5ebe270c..59535662b 100644 --- a/arduino-ide-extension/src/node/core-service-impl.ts +++ b/arduino-ide-extension/src/node/core-service-impl.ts @@ -153,6 +153,11 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { } req.setVerbose(options.verbose); req.setVerify(options.verify); + + options.userFields.forEach(e => { + req.getUserFieldsMap().set(e.name, e.value); + }); + const result = responseHandler(client, req); try { From 97399cf2dd02a1ae97adf1dab87cc9e02e83b267 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Mon, 18 Oct 2021 14:07:39 +0200 Subject: [PATCH 05/11] Disable Sketch > Configure and Upload menu if board doesn't support user fields --- .../browser/arduino-frontend-contribution.tsx | 2 +- .../browser/contributions/upload-sketch.ts | 140 +++++++++++------- .../browser/contributions/verify-sketch.ts | 2 +- 3 files changed, 92 insertions(+), 52 deletions(-) diff --git a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx index f9ec9ccbf..55e70a2c2 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx +++ b/arduino-ide-extension/src/browser/arduino-frontend-contribution.tsx @@ -445,7 +445,7 @@ export class ArduinoFrontendContribution 'arduino/debug/optimizeForDebugging', 'Optimize for Debugging' ), - order: '4', + order: '5', }); } diff --git a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts index 1eacb3a2e..c475e85b3 100644 --- a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts @@ -1,7 +1,7 @@ import { inject, injectable, postConstruct } from 'inversify'; import { Emitter } from '@theia/core/lib/common/event'; import { BoardUserField, CoreService } from '../../common/protocol'; -import { ArduinoMenus } from '../menu/arduino-menus'; +import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus'; import { ArduinoToolbar } from '../toolbar/arduino-toolbar'; import { BoardsDataStore } from '../boards/boards-data-store'; import { SerialConnectionManager } from '../serial/serial-connection-manager'; @@ -16,6 +16,7 @@ import { } from './contribution'; import { UserFieldsDialog } from '../dialogs/user-fields/user-fields-dialog'; import { nls } from '@theia/core/lib/browser/nls'; +import { DisposableCollection } from '@theia/core'; @injectable() export class UploadSketch extends SketchContribution { @@ -25,6 +26,9 @@ export class UploadSketch extends SketchContribution { @inject(SerialConnectionManager) protected readonly serialConnection: SerialConnectionManager; + @inject(MenuModelRegistry) + protected readonly menuRegistry: MenuModelRegistry; + @inject(BoardsDataStore) protected readonly boardsDataStore: BoardsDataStore; @@ -42,25 +46,29 @@ export class UploadSketch extends SketchContribution { protected uploadInProgress = false; protected boardRequiresUserFields = false; + protected readonly menuActionsDisposables = new DisposableCollection(); + @postConstruct() protected init(): void { this.boardsServiceClientImpl.onBoardsConfigChanged(async () => { - const userFields = await this.boardsServiceClientImpl.selectedBoardUserFields(); + const userFields = + await this.boardsServiceClientImpl.selectedBoardUserFields(); this.boardRequiresUserFields = userFields.length > 0; - }) + this.registerMenus(this.menuRegistry); + }); } private selectedFqbnAddress(): string { const { boardsConfig } = this.boardsServiceClientImpl; const fqbn = boardsConfig.selectedBoard?.fqbn; if (!fqbn) { - return ""; + return ''; } - const address = boardsConfig.selectedBoard?.port?.address + const address = boardsConfig.selectedBoard?.port?.address; if (!address) { - return ""; + return ''; } - return fqbn + "|" + address; + return fqbn + '|' + address; } registerCommands(registry: CommandRegistry): void { @@ -72,7 +80,9 @@ export class UploadSketch extends SketchContribution { } if (this.boardRequiresUserFields && !this.cachedUserFields.has(key)) { // Deep clone the array of board fields to avoid editing the cached ones - this.userFieldsDialog.value = (await this.boardsServiceClientImpl.selectedBoardUserFields()).map(f => ({ ...f })); + this.userFieldsDialog.value = ( + await this.boardsServiceClientImpl.selectedBoardUserFields() + ).map((f) => ({ ...f })); const result = await this.userFieldsDialog.open(); if (!result) { return; @@ -83,29 +93,29 @@ export class UploadSketch extends SketchContribution { }, isEnabled: () => !this.uploadInProgress, }); - registry.registerCommand( - UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION, - { - execute: async () => { - const key = this.selectedFqbnAddress(); - if (!key) { - return; - } + registry.registerCommand(UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION, { + execute: async () => { + const key = this.selectedFqbnAddress(); + if (!key) { + return; + } - const cached = this.cachedUserFields.get(key); - // Deep clone the array of board fields to avoid editing the cached ones - this.userFieldsDialog.value = (cached ?? await this.boardsServiceClientImpl.selectedBoardUserFields()).map(f => ({ ...f })); + const cached = this.cachedUserFields.get(key); + // Deep clone the array of board fields to avoid editing the cached ones + this.userFieldsDialog.value = ( + cached ?? + (await this.boardsServiceClientImpl.selectedBoardUserFields()) + ).map((f) => ({ ...f })); - const result = await this.userFieldsDialog.open() - if (!result) { - return; - } - this.cachedUserFields.set(key, result); - this.uploadSketch(); - }, - isEnabled: () => !this.uploadInProgress && this.boardRequiresUserFields, - } - ); + const result = await this.userFieldsDialog.open(); + if (!result) { + return; + } + this.cachedUserFields.set(key, result); + this.uploadSketch(); + }, + isEnabled: () => !this.uploadInProgress && this.boardRequiresUserFields, + }); registry.registerCommand( UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER, { @@ -124,24 +134,46 @@ export class UploadSketch extends SketchContribution { } registerMenus(registry: MenuModelRegistry): void { - registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { - commandId: UploadSketch.Commands.UPLOAD_SKETCH.id, - label: nls.localize('arduino/sketch/upload', 'Upload'), - order: '1', - }); - registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { - commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id, - label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label, - order: '2', - }); - registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { - commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id, - label: nls.localize( - 'arduino/sketch/uploadUsingProgrammer', - 'Upload Using Programmer' - ), - order: '3', - }); + this.menuActionsDisposables.dispose(); + + this.menuActionsDisposables.push( + registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { + commandId: UploadSketch.Commands.UPLOAD_SKETCH.id, + label: nls.localize('arduino/sketch/upload', 'Upload'), + order: '1', + }) + ); + if (this.boardRequiresUserFields) { + this.menuActionsDisposables.push( + registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { + commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id, + label: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label, + order: '2', + }) + ); + } else { + this.menuActionsDisposables.push( + registry.registerMenuNode( + ArduinoMenus.SKETCH__MAIN_GROUP, + new PlaceholderMenuNode( + ArduinoMenus.SKETCH__MAIN_GROUP, + // commandId: UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.id, + UploadSketch.Commands.UPLOAD_WITH_CONFIGURATION.label!, + { order: '2' } + ) + ) + ); + } + this.menuActionsDisposables.push( + registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, { + commandId: UploadSketch.Commands.UPLOAD_SKETCH_USING_PROGRAMMER.id, + label: nls.localize( + 'arduino/sketch/uploadUsingProgrammer', + 'Upload Using Programmer' + ), + order: '3', + }) + ); } registerKeybindings(registry: KeybindingRegistry): void { @@ -200,7 +232,12 @@ export class UploadSketch extends SketchContribution { const port = selectedPort; const userFields = this.cachedUserFields.get(this.selectedFqbnAddress()); if (!userFields) { - this.messageService.error(nls.localize('arduino/sketch/userFieldsNotFoundError', "Can't find user fields for connected board")); + this.messageService.error( + nls.localize( + 'arduino/sketch/userFieldsNotFoundError', + "Can't find user fields for connected board" + ) + ); return; } @@ -277,9 +314,12 @@ export namespace UploadSketch { }; export const UPLOAD_WITH_CONFIGURATION: Command = { id: 'arduino-upload-with-configuration-sketch', - label: nls.localize('arduino/sketch/configureAndUpload', 'Configure And Upload'), + label: nls.localize( + 'arduino/sketch/configureAndUpload', + 'Configure And Upload' + ), category: 'Arduino', - } + }; export const UPLOAD_SKETCH_USING_PROGRAMMER: Command = { id: 'arduino-upload-sketch-using-programmer', }; diff --git a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts index f8e1e1a00..299655087 100644 --- a/arduino-ide-extension/src/browser/contributions/verify-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/verify-sketch.ts @@ -62,7 +62,7 @@ export class VerifySketch extends SketchContribution { 'arduino/sketch/exportBinary', 'Export Compiled Binary' ), - order: '3', + order: '4', }); } From 624abbe0b0315f4e432f8fcda86bbaed8fc926d2 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Wed, 20 Oct 2021 15:04:54 +0200 Subject: [PATCH 06/11] Fix serial upload not working with all boards --- .../src/browser/contributions/upload-sketch.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts index c475e85b3..af3e033f5 100644 --- a/arduino-ide-extension/src/browser/contributions/upload-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/upload-sketch.ts @@ -230,8 +230,9 @@ export class UploadSketch extends SketchContribution { const optimizeForDebug = this.editorMode.compileForDebug; const { selectedPort } = boardsConfig; const port = selectedPort; - const userFields = this.cachedUserFields.get(this.selectedFqbnAddress()); - if (!userFields) { + const userFields = + this.cachedUserFields.get(this.selectedFqbnAddress()) ?? []; + if (userFields.length === 0 && this.boardRequiresUserFields) { this.messageService.error( nls.localize( 'arduino/sketch/userFieldsNotFoundError', From 6f782b5cd7e51ff29e3b812f664b06d5ecf89f79 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Mon, 8 Nov 2021 11:46:11 +0100 Subject: [PATCH 07/11] Update i18n source file --- i18n/en.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/i18n/en.json b/i18n/en.json index 0e3c12b68..84e472688 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -180,8 +180,10 @@ "sketchbook": "Sketchbook", "upload": "Upload", "uploadUsingProgrammer": "Upload Using Programmer", + "userFieldsNotFoundError": "Can't find user fields for connected board", "doneUploading": "Done uploading.", "couldNotConnectToSerial": "Could not reconnect to serial port. {0}", + "configureAndUpload": "Configure And Upload", "verifyOrCompile": "Verify/Compile", "exportBinary": "Export Compiled Binary", "verify": "Verify", From 286a687bdbd585e78d9237fac24679f712626da9 Mon Sep 17 00:00:00 2001 From: Alberto Iannaccone Date: Wed, 24 Nov 2021 15:17:31 +0100 Subject: [PATCH 08/11] fix user fields UI --- .../user-fields/user-fields-component.tsx | 44 ++++++++++--------- .../src/browser/style/user-fields-dialog.css | 22 ++++++---- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/arduino-ide-extension/src/browser/dialogs/user-fields/user-fields-component.tsx b/arduino-ide-extension/src/browser/dialogs/user-fields/user-fields-component.tsx index b9fbc4700..ea5280234 100644 --- a/arduino-ide-extension/src/browser/dialogs/user-fields/user-fields-component.tsx +++ b/arduino-ide-extension/src/browser/dialogs/user-fields/user-fields-component.tsx @@ -26,7 +26,7 @@ export const UserFieldsComponent = ({ const updateUserField = (index: number) => (e: React.ChangeEvent) => { - let newBoardUserFields = [...boardUserFields]; + const newBoardUserFields = [...boardUserFields]; newBoardUserFields[index].value = e.target.value; setBoardUserFields(newBoardUserFields); }; @@ -48,26 +48,30 @@ export const UserFieldsComponent = ({ return (
- {boardUserFields.map((field, index) => { - return ( -
-
- -
-
- -
-
- ); - })} +
+
+ {boardUserFields.map((field, index) => { + return ( +
+
+ +
+
+ +
+
+ ); + })} +
+
-
+