From 7785b2b4ff4f21ee4ae351a78702a38b42565d06 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Tue, 25 Oct 2022 17:13:43 +0200 Subject: [PATCH 1/3] feat: Create remote sketch Signed-off-by: Akos Kitta --- .../browser/arduino-ide-frontend-module.ts | 2 + .../browser/contributions/new-cloud-sketch.ts | 238 ++++++++++++++++++ .../src/browser/create/create-uri.ts | 4 +- .../src/browser/style/dialogs.css | 6 +- .../cloud-sketchbook/cloud-sketchbook-tree.ts | 4 +- i18n/en.json | 5 + 6 files changed, 254 insertions(+), 5 deletions(-) create mode 100644 arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts 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 5e87af4fc..e479464c9 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -335,6 +335,7 @@ import { UserFields } from './contributions/user-fields'; import { UpdateIndexes } from './contributions/update-indexes'; import { InterfaceScale } from './contributions/interface-scale'; import { OpenHandler } from '@theia/core/lib/browser/opener-service'; +import { NewCloudSketch } from './contributions/new-cloud-sketch'; const registerArduinoThemes = () => { const themes: MonacoThemeJson[] = [ @@ -751,6 +752,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { Contribution.configure(bind, DeleteSketch); Contribution.configure(bind, UpdateIndexes); Contribution.configure(bind, InterfaceScale); + Contribution.configure(bind, NewCloudSketch); bindContributionProvider(bind, StartupTaskProvider); bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window diff --git a/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts b/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts new file mode 100644 index 000000000..11f50f242 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts @@ -0,0 +1,238 @@ +import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { CompositeTreeNode } from '@theia/core/lib/browser/tree'; +import { codicon } from '@theia/core/lib/browser/widgets/widget'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { Emitter } from '@theia/core/lib/common/event'; +import { nls } from '@theia/core/lib/common/nls'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import type { AuthenticationSession } from '../../common/protocol/authentication-service'; +import { AuthenticationClientService } from '../auth/authentication-client-service'; +import { CreateApi } from '../create/create-api'; +import { CreateUri } from '../create/create-uri'; +import { Create } from '../create/typings'; +import { WorkspaceInputDialog } from '../theia/workspace/workspace-input-dialog'; +import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree'; +import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model'; +import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget'; +import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands'; +import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget'; +import { Command, CommandRegistry, Contribution, URI } from './contribution'; + +@injectable() +export class NewCloudSketch extends Contribution { + @inject(CreateApi) + private readonly createApi: CreateApi; + + @inject(AuthenticationClientService) + private readonly authenticationService: AuthenticationClientService; + + private session: AuthenticationSession | undefined; + private treeModel: CloudSketchbookTreeModel | undefined; + private readonly onDidChangeEmitter = new Emitter(); + private readonly toDisposeOnStop = new DisposableCollection( + this.onDidChangeEmitter + ); + + override onReady(): void { + this.toDisposeOnStop.push( + this.authenticationService.onSessionDidChange((session) => { + this.session = session; + this.onDidChangeEmitter.fire(); + }) + ); + this.session = this.authenticationService.session; + if (this.session) { + this.onDidChangeEmitter.fire(); + } + } + + onStop(): void { + this.toDisposeOnStop.dispose(); + } + + override registerCommands(registry: CommandRegistry): void { + registry.registerCommand(NewCloudSketch.Commands.CREATE_SKETCH, { + execute: () => this.createNewSketch(), + isEnabled: () => !!this.session && !!this.treeModel, + }); + + registry.registerCommand(NewCloudSketch.Commands.CREATE_SKETCH_TOOLBAR, { + execute: () => + this.commandService.executeCommand( + NewCloudSketch.Commands.CREATE_SKETCH.id + ), + isVisible: (arg: unknown) => { + if (this.session && arg instanceof SketchbookWidget) { + const treeWidget = arg.getTreeWidget(); + if (treeWidget instanceof CloudSketchbookTreeWidget) { + const model = treeWidget.model; + if (model instanceof CloudSketchbookTreeModel) { + this.treeModel = model; + } + } + return ( + !!this.treeModel && treeWidget instanceof CloudSketchbookTreeWidget + ); + } + return false; + }, + }); + } + + override registerToolbarItems(registry: TabBarToolbarRegistry): void { + registry.registerItem({ + id: NewCloudSketch.Commands.CREATE_SKETCH_TOOLBAR.id, + command: NewCloudSketch.Commands.CREATE_SKETCH_TOOLBAR.id, + tooltip: NewCloudSketch.Commands.CREATE_SKETCH_TOOLBAR.label, + onDidChange: this.onDidChangeEmitter.event, + }); + } + + private async createNewSketch( + initialValue?: string | undefined + ): Promise { + const newSketchName = await this.newSketchName(initialValue); + if (!newSketchName) { + return undefined; + } + const rootNode = this.rootNode(); + if (!rootNode) { + return undefined; + } + + if (!this.treeModel) { + return undefined; + } + + let result: Create.Sketch | undefined | 'conflict'; + try { + result = await this.createApi.createSketch(newSketchName); + } catch (err) { + if ('status' in err && err.status === 409) { + result = 'conflict'; + } else { + throw err; + } + } finally { + if (result) { + await this.treeModel.updateRoot(); + await this.treeModel.refresh(); + } + } + + if (result === 'conflict') { + return this.createNewSketch(newSketchName); + } + + if (result) { + const newSketch = result; + const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes'); + this.messageService + .info( + nls.localize( + 'arduino/cloud/openNewSketch', + 'Do you want to pull the new remote sketch {0} and open it in a new window?', + newSketchName + ), + yes + ) + .then(async (answer) => { + if (!this.treeModel) { + return; + } + if (answer === yes) { + const node = this.treeModel.getNode( + CreateUri.toUri(newSketch).path.toString() + ); + if (!node) { + return; + } + if (CloudSketchbookTree.CloudSketchDirNode.is(node)) { + await this.treeModel.sketchbookTree().pull({ node }); + return this.commandService.executeCommand( + SketchbookCommands.OPEN_NEW_WINDOW.id, + { node } + ); + } + } + }); + } + return undefined; + } + + private async newSketchName( + initialValue?: string | undefined + ): Promise { + const rootNode = this.rootNode(); + if (!rootNode) { + return undefined; + } + const existingNames = rootNode.children + .filter(CloudSketchbookTree.CloudSketchDirNode.is) + .map(({ fileStat }) => fileStat.name); + return new WorkspaceInputDialog( + { + title: nls.localize( + 'arduino/cloud/newSketchTitle', + 'Name of a new remote sketch' + ), + parentUri: CreateUri.root, + initialValue, + validate: (input) => { + if (!input) { + return nls.localize( + 'arduino/cloud/invalidSketchName', + 'The name must consist of basic letters, numbers, or underscores. The maximum length is 37 characters.' + ); + } + if (existingNames.includes(input)) { + return nls.localize( + 'arduino/cloud/sketchAlreadyExists', + "Remote sketch '{0}' already exists.", + input + ); + } + // This is how https://create.arduino.cc/editor/ works when renaming a sketch. + if (/^[0-9a-zA-Z_]{1,37}$/.test(input)) { + return ''; + } + return nls.localize( + 'arduino/cloud/invalidSketchName', + 'The name must consist of basic letters, numbers, or underscores. The maximum length is 37 characters.' + ); + }, + }, + this.labelProvider + ).open(); + } + + private rootNode(): CompositeTreeNode | undefined { + if (!this.session) { + return undefined; + } + if (!this.treeModel) { + return undefined; + } + if (!CompositeTreeNode.is(this.treeModel.root)) { + return undefined; + } + return this.treeModel.root; + } +} +export namespace NewCloudSketch { + export namespace Commands { + export const CREATE_SKETCH = Command.toLocalizedCommand( + { + id: 'arduino-cloud-sketchbook--create-sketch', + label: 'New Remote Sketch...', + }, + 'arduino/cloud/createSketch' + ) as Command & { label: string }; + + export const CREATE_SKETCH_TOOLBAR: Command & { label: string } = { + ...CREATE_SKETCH, + id: `${CREATE_SKETCH.id}-toolbar`, + iconClass: codicon('new-folder'), + }; + } +} diff --git a/arduino-ide-extension/src/browser/create/create-uri.ts b/arduino-ide-extension/src/browser/create/create-uri.ts index 1d60ffff2..658a65ac1 100644 --- a/arduino-ide-extension/src/browser/create/create-uri.ts +++ b/arduino-ide-extension/src/browser/create/create-uri.ts @@ -7,7 +7,9 @@ export namespace CreateUri { export const scheme = 'arduino-create'; export const root = toUri(posix.sep); - export function toUri(posixPathOrResource: string | Create.Resource): URI { + export function toUri( + posixPathOrResource: string | Create.Resource | Create.Sketch + ): URI { const posixPath = typeof posixPathOrResource === 'string' ? posixPathOrResource diff --git a/arduino-ide-extension/src/browser/style/dialogs.css b/arduino-ide-extension/src/browser/style/dialogs.css index 4d56484e8..99b749158 100644 --- a/arduino-ide-extension/src/browser/style/dialogs.css +++ b/arduino-ide-extension/src/browser/style/dialogs.css @@ -29,6 +29,10 @@ min-height: 0; } +.p-Widget.dialogOverlay .dialogBlock .dialogControl .error { + word-break: normal; +} + .p-Widget.dialogOverlay .dialogBlock .dialogContent { padding: 0; overflow: auto; @@ -80,10 +84,8 @@ opacity: .4; } - @media only screen and (max-height: 560px) { .p-Widget.dialogOverlay .dialogBlock { max-height: 400px; } } - \ No newline at end of file diff --git a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree.ts b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree.ts index 7204df632..f03f83071 100644 --- a/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree.ts +++ b/arduino-ide-extension/src/browser/widgets/cloud-sketchbook/cloud-sketchbook-tree.ts @@ -136,7 +136,7 @@ export class CloudSketchbookTree extends SketchbookTree { return; } } - this.runWithState(node, 'pulling', async (node) => { + return this.runWithState(node, 'pulling', async (node) => { const commandsCopy = node.commands; node.commands = []; @@ -196,7 +196,7 @@ export class CloudSketchbookTree extends SketchbookTree { return; } } - this.runWithState(node, 'pushing', async (node) => { + return this.runWithState(node, 'pushing', async (node) => { if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) { throw new Error( nls.localize( diff --git a/i18n/en.json b/i18n/en.json index ea6db769a..21c8013ca 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -86,16 +86,20 @@ "chooseSketchVisibility": "Choose visibility of your Sketch:", "connected": "Connected", "continue": "Continue", + "createSketch": "New Remote Sketch...", "donePulling": "Done pulling ‘{0}’.", "donePushing": "Done pushing ‘{0}’.", "embed": "Embed:", "emptySketchbook": "Your Sketchbook is empty", "goToCloud": "Go to Cloud", + "invalidSketchName": "The name must consist of basic letters, numbers, or underscores. The maximum length is 37 characters.", "learnMore": "Learn more", "link": "Link:", + "newSketchTitle": "Name of a new remote sketch", "notYetPulled": "Cannot push to Cloud. It is not yet pulled.", "offline": "Offline", "openInCloudEditor": "Open in Cloud Editor", + "openNewSketch": "Do you want to pull the new remote sketch {0} and open it in a new window?", "options": "Options...", "privateVisibility": "Private. Only you can view the Sketch.", "profilePicture": "Profile picture", @@ -115,6 +119,7 @@ "signIn": "SIGN IN", "signInToCloud": "Sign in to Arduino Cloud", "signOut": "Sign Out", + "sketchAlreadyExists": "Remote sketch '{0}' already exists.", "sync": "Sync", "syncEditSketches": "Sync and edit your Arduino Cloud Sketches", "visitArduinoCloud": "Visit Arduino Cloud to create Cloud Sketches." From 932aaad60f6f11e55fbdd0738664af55302a10c9 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Wed, 26 Oct 2022 11:40:01 +0200 Subject: [PATCH 2/3] handle error if remote sketch is missing on pull Signed-off-by: Akos Kitta --- .../browser/contributions/new-cloud-sketch.ts | 134 ++++++++++-------- i18n/en.json | 12 +- 2 files changed, 78 insertions(+), 68 deletions(-) diff --git a/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts b/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts index 11f50f242..2e72e645d 100644 --- a/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts @@ -1,12 +1,13 @@ import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { CompositeTreeNode } from '@theia/core/lib/browser/tree'; import { codicon } from '@theia/core/lib/browser/widgets/widget'; -import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { + Disposable, + DisposableCollection, +} from '@theia/core/lib/common/disposable'; import { Emitter } from '@theia/core/lib/common/event'; import { nls } from '@theia/core/lib/common/nls'; import { inject, injectable } from '@theia/core/shared/inversify'; -import type { AuthenticationSession } from '../../common/protocol/authentication-service'; -import { AuthenticationClientService } from '../auth/authentication-client-service'; import { CreateApi } from '../create/create-api'; import { CreateUri } from '../create/create-uri'; import { Create } from '../create/typings'; @@ -23,51 +24,45 @@ export class NewCloudSketch extends Contribution { @inject(CreateApi) private readonly createApi: CreateApi; - @inject(AuthenticationClientService) - private readonly authenticationService: AuthenticationClientService; - - private session: AuthenticationSession | undefined; + private toDisposeOnNewTreeModel: Disposable | undefined; private treeModel: CloudSketchbookTreeModel | undefined; private readonly onDidChangeEmitter = new Emitter(); private readonly toDisposeOnStop = new DisposableCollection( this.onDidChangeEmitter ); - override onReady(): void { - this.toDisposeOnStop.push( - this.authenticationService.onSessionDidChange((session) => { - this.session = session; - this.onDidChangeEmitter.fire(); - }) - ); - this.session = this.authenticationService.session; - if (this.session) { - this.onDidChangeEmitter.fire(); - } - } - onStop(): void { this.toDisposeOnStop.dispose(); + if (this.toDisposeOnNewTreeModel) { + this.toDisposeOnNewTreeModel.dispose(); + } } override registerCommands(registry: CommandRegistry): void { registry.registerCommand(NewCloudSketch.Commands.CREATE_SKETCH, { execute: () => this.createNewSketch(), - isEnabled: () => !!this.session && !!this.treeModel, + isEnabled: () => !!this.treeModel, }); - registry.registerCommand(NewCloudSketch.Commands.CREATE_SKETCH_TOOLBAR, { execute: () => this.commandService.executeCommand( NewCloudSketch.Commands.CREATE_SKETCH.id ), isVisible: (arg: unknown) => { - if (this.session && arg instanceof SketchbookWidget) { + if (arg instanceof SketchbookWidget) { const treeWidget = arg.getTreeWidget(); if (treeWidget instanceof CloudSketchbookTreeWidget) { const model = treeWidget.model; if (model instanceof CloudSketchbookTreeModel) { - this.treeModel = model; + if (this.treeModel !== model) { + this.treeModel = model; + if (this.toDisposeOnNewTreeModel) { + this.toDisposeOnNewTreeModel.dispose(); + this.toDisposeOnNewTreeModel = this.treeModel.onChanged(() => + this.onDidChangeEmitter.fire() + ); + } + } } } return ( @@ -91,24 +86,18 @@ export class NewCloudSketch extends Contribution { private async createNewSketch( initialValue?: string | undefined ): Promise { - const newSketchName = await this.newSketchName(initialValue); - if (!newSketchName) { - return undefined; - } - const rootNode = this.rootNode(); - if (!rootNode) { + if (!this.treeModel) { return undefined; } - - if (!this.treeModel) { + const newSketchName = await this.newSketchName(initialValue); + if (!newSketchName) { return undefined; } - let result: Create.Sketch | undefined | 'conflict'; try { result = await this.createApi.createSketch(newSketchName); } catch (err) { - if ('status' in err && err.status === 409) { + if (isConflict(err)) { result = 'conflict'; } else { throw err; @@ -126,29 +115,43 @@ export class NewCloudSketch extends Contribution { if (result) { const newSketch = result; + const treeModel = this.treeModel; const yes = nls.localize('vscode/extensionsUtils/yes', 'Yes'); this.messageService .info( nls.localize( - 'arduino/cloud/openNewSketch', + 'arduino/newCloudSketch/openNewSketch', 'Do you want to pull the new remote sketch {0} and open it in a new window?', newSketchName ), yes ) .then(async (answer) => { - if (!this.treeModel) { - return; - } if (answer === yes) { - const node = this.treeModel.getNode( + const node = treeModel.getNode( CreateUri.toUri(newSketch).path.toString() ); if (!node) { return; } if (CloudSketchbookTree.CloudSketchDirNode.is(node)) { - await this.treeModel.sketchbookTree().pull({ node }); + try { + await treeModel.sketchbookTree().pull({ node }); + } catch (err) { + if (isNotFound(err)) { + await treeModel.updateRoot(); + await treeModel.refresh(); + this.messageService.error( + nls.localize( + 'arduino/newCloudSketch/notFound', + `Could not pull the remote sketch {0}. It does not exist.`, + newSketchName + ) + ); + return; + } + throw err; + } return this.commandService.executeCommand( SketchbookCommands.OPEN_NEW_WINDOW.id, { node } @@ -173,32 +176,26 @@ export class NewCloudSketch extends Contribution { return new WorkspaceInputDialog( { title: nls.localize( - 'arduino/cloud/newSketchTitle', + 'arduino/newCloudSketch/newSketchTitle', 'Name of a new remote sketch' ), parentUri: CreateUri.root, initialValue, validate: (input) => { - if (!input) { - return nls.localize( - 'arduino/cloud/invalidSketchName', - 'The name must consist of basic letters, numbers, or underscores. The maximum length is 37 characters.' - ); - } if (existingNames.includes(input)) { return nls.localize( - 'arduino/cloud/sketchAlreadyExists', + 'arduino/newCloudSketch/sketchAlreadyExists', "Remote sketch '{0}' already exists.", input ); } // This is how https://create.arduino.cc/editor/ works when renaming a sketch. - if (/^[0-9a-zA-Z_]{1,37}$/.test(input)) { + if (/^[0-9a-zA-Z_]{1,36}$/.test(input)) { return ''; } return nls.localize( - 'arduino/cloud/invalidSketchName', - 'The name must consist of basic letters, numbers, or underscores. The maximum length is 37 characters.' + 'arduino/newCloudSketch/invalidSketchName', + 'The name must consist of basic letters, numbers, or underscores. The maximum length is 36 characters.' ); }, }, @@ -207,16 +204,9 @@ export class NewCloudSketch extends Contribution { } private rootNode(): CompositeTreeNode | undefined { - if (!this.session) { - return undefined; - } - if (!this.treeModel) { - return undefined; - } - if (!CompositeTreeNode.is(this.treeModel.root)) { - return undefined; - } - return this.treeModel.root; + return this.treeModel && CompositeTreeNode.is(this.treeModel.root) + ? this.treeModel.root + : undefined; } } export namespace NewCloudSketch { @@ -225,10 +215,10 @@ export namespace NewCloudSketch { { id: 'arduino-cloud-sketchbook--create-sketch', label: 'New Remote Sketch...', + category: 'Arduino', }, - 'arduino/cloud/createSketch' + 'arduino/newCloudSketch/createSketch' ) as Command & { label: string }; - export const CREATE_SKETCH_TOOLBAR: Command & { label: string } = { ...CREATE_SKETCH, id: `${CREATE_SKETCH.id}-toolbar`, @@ -236,3 +226,21 @@ export namespace NewCloudSketch { }; } } + +function isConflict(err: unknown): boolean { + return isErrorWithStatusOf(err, 409); +} +function isNotFound(err: unknown): boolean { + return isErrorWithStatusOf(err, 404); +} +function isErrorWithStatusOf( + err: unknown, + status: number +): err is Error & { status: number } { + if (err instanceof Error) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const object = err as any; + return 'status' in object && object.status === status; + } + return false; +} diff --git a/i18n/en.json b/i18n/en.json index 21c8013ca..4793b3a0c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -86,20 +86,16 @@ "chooseSketchVisibility": "Choose visibility of your Sketch:", "connected": "Connected", "continue": "Continue", - "createSketch": "New Remote Sketch...", "donePulling": "Done pulling ‘{0}’.", "donePushing": "Done pushing ‘{0}’.", "embed": "Embed:", "emptySketchbook": "Your Sketchbook is empty", "goToCloud": "Go to Cloud", - "invalidSketchName": "The name must consist of basic letters, numbers, or underscores. The maximum length is 37 characters.", "learnMore": "Learn more", "link": "Link:", - "newSketchTitle": "Name of a new remote sketch", "notYetPulled": "Cannot push to Cloud. It is not yet pulled.", "offline": "Offline", "openInCloudEditor": "Open in Cloud Editor", - "openNewSketch": "Do you want to pull the new remote sketch {0} and open it in a new window?", "options": "Options...", "privateVisibility": "Private. Only you can view the Sketch.", "profilePicture": "Profile picture", @@ -119,7 +115,6 @@ "signIn": "SIGN IN", "signInToCloud": "Sign in to Arduino Cloud", "signOut": "Sign Out", - "sketchAlreadyExists": "Remote sketch '{0}' already exists.", "sync": "Sync", "syncEditSketches": "Sync and edit your Arduino Cloud Sketches", "visitArduinoCloud": "Visit Arduino Cloud to create Cloud Sketches." @@ -304,6 +299,13 @@ "unableToCloseWebSocket": "Unable to close websocket", "unableToConnectToWebSocket": "Unable to connect to websocket" }, + "newCloudSketch": { + "createSketch": "New Remote Sketch...", + "invalidSketchName": "The name must consist of basic letters, numbers, or underscores. The maximum length is 36 characters.", + "newSketchTitle": "Name of a new remote sketch", + "openNewSketch": "Do you want to pull the new remote sketch {0} and open it in a new window?", + "sketchAlreadyExists": "Remote sketch '{0}' already exists." + }, "portProtocol": { "network": "Network", "serial": "Serial" From e0a921c7088332e868cbfb94ab2ea0b0bbf590e7 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Wed, 26 Oct 2022 12:56:50 +0200 Subject: [PATCH 3/3] listen on tree changes inside dock panel Signed-off-by: Akos Kitta --- .../browser/contributions/new-cloud-sketch.ts | 21 +++++++++ .../widgets/sketchbook/sketchbook-widget.tsx | 43 ++++++++++++++++--- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts b/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts index 2e72e645d..173caa991 100644 --- a/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts @@ -17,12 +17,15 @@ import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sket import { CloudSketchbookTreeWidget } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-widget'; import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands'; import { SketchbookWidget } from '../widgets/sketchbook/sketchbook-widget'; +import { SketchbookWidgetContribution } from '../widgets/sketchbook/sketchbook-widget-contribution'; import { Command, CommandRegistry, Contribution, URI } from './contribution'; @injectable() export class NewCloudSketch extends Contribution { @inject(CreateApi) private readonly createApi: CreateApi; + @inject(SketchbookWidgetContribution) + private readonly sketchbookWidgetContribution: SketchbookWidgetContribution; private toDisposeOnNewTreeModel: Disposable | undefined; private treeModel: CloudSketchbookTreeModel | undefined; @@ -31,6 +34,24 @@ export class NewCloudSketch extends Contribution { this.onDidChangeEmitter ); + override onReady(): void { + const handleCurrentTreeDidChange = (widget: SketchbookWidget) => { + this.toDisposeOnStop.push( + widget.onCurrentTreeDidChange(() => this.onDidChangeEmitter.fire()) + ); + const treeWidget = widget.getTreeWidget(); + if (treeWidget instanceof CloudSketchbookTreeWidget) { + this.onDidChangeEmitter.fire(); + } + }; + const widget = this.sketchbookWidgetContribution.tryGetWidget(); + if (widget) { + handleCurrentTreeDidChange(widget); + } else { + this.sketchbookWidgetContribution.widget.then(handleCurrentTreeDidChange); + } + } + onStop(): void { this.toDisposeOnStop.dispose(); if (this.toDisposeOnNewTreeModel) { diff --git a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget.tsx b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget.tsx index 0b7b920a1..a4234ba12 100644 --- a/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget.tsx +++ b/arduino-ide-extension/src/browser/widgets/sketchbook/sketchbook-widget.tsx @@ -8,11 +8,13 @@ import { IDragEvent } from '@theia/core/shared/@phosphor/dragdrop'; import { DockPanel, Widget } from '@theia/core/shared/@phosphor/widgets'; import { Message, MessageLoop } from '@theia/core/shared/@phosphor/messaging'; import { Disposable } from '@theia/core/lib/common/disposable'; -import { BaseWidget } from '@theia/core/lib/browser/widgets/widget'; +import { BaseWidget, Title } from '@theia/core/lib/browser/widgets/widget'; import { SketchbookTreeWidget } from './sketchbook-tree-widget'; -import { nls } from '@theia/core/lib/common'; +import { nls } from '@theia/core/lib/common/nls'; +import { Event, Emitter } from '@theia/core/lib/common/event'; import { CloudSketchbookCompositeWidget } from '../cloud-sketchbook/cloud-sketchbook-composite-widget'; import { URI } from '../../contributions/contribution'; +import { TheiaDockPanel } from '@theia/core/lib/browser/shell/theia-dock-panel'; @injectable() export class SketchbookWidget extends BaseWidget { @@ -21,7 +23,8 @@ export class SketchbookWidget extends BaseWidget { @inject(SketchbookTreeWidget) protected readonly localSketchbookTreeWidget: SketchbookTreeWidget; - protected readonly sketchbookTreesContainer: DockPanel; + protected readonly sketchbookTreesContainer: NoopDragOverDockPanel; + private readonly onCurrentTreeDidChangeEmitter: Emitter; constructor() { super(); @@ -32,6 +35,15 @@ export class SketchbookWidget extends BaseWidget { this.title.closable = true; this.node.tabIndex = 0; this.sketchbookTreesContainer = this.createTreesContainer(); + this.onCurrentTreeDidChangeEmitter = new Emitter(); + this.toDispose.pushAll([ + this.onCurrentTreeDidChangeEmitter, + this.sketchbookTreesContainer.onCurrentDidChange((title) => + this.onCurrentTreeDidChangeEmitter.fire( + title?.owner instanceof SketchbookTreeWidget ? title.owner : undefined + ) + ), + ]); } @postConstruct() @@ -47,6 +59,10 @@ export class SketchbookWidget extends BaseWidget { ); } + get onCurrentTreeDidChange(): Event { + return this.onCurrentTreeDidChangeEmitter.event; + } + getTreeWidget(): SketchbookTreeWidget { return this.localSketchbookTreeWidget; } @@ -129,7 +145,7 @@ export class SketchbookWidget extends BaseWidget { this.onResize(Widget.ResizeMessage.UnknownSize); } - protected createTreesContainer(): DockPanel { + protected createTreesContainer(): NoopDragOverDockPanel { const panel = new NoopDragOverDockPanel({ spacing: 0, mode: 'single-document', @@ -140,13 +156,28 @@ export class SketchbookWidget extends BaseWidget { } } -export class NoopDragOverDockPanel extends DockPanel { +class NoopDragOverDockPanel extends TheiaDockPanel { + private readonly onCurrentDidChangeEmitter = new Emitter< + Title | undefined + >(); + readonly onCurrentDidChange = this.onCurrentDidChangeEmitter.event; + constructor(options?: DockPanel.IOptions) { super(options); - NoopDragOverDockPanel.prototype['_evtDragOver'] = (event: IDragEvent) => { + super['_evtDragOver'] = (event: IDragEvent) => { event.preventDefault(); event.stopPropagation(); event.dropAction = 'none'; }; } + + override markAsCurrent(title: Title | undefined): void { + super.markAsCurrent(title); + this.onCurrentDidChangeEmitter.fire(title); + } + + override dispose(): void { + super.dispose(); + this.onCurrentDidChangeEmitter.dispose(); + } }