mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-09 20:36:32 +00:00
feat: Create remote sketch
Closes #1580 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
parent
6984c52b92
commit
7d6a2d5e33
@ -335,6 +335,8 @@ import { UserFields } from './contributions/user-fields';
|
|||||||
import { UpdateIndexes } from './contributions/update-indexes';
|
import { UpdateIndexes } from './contributions/update-indexes';
|
||||||
import { InterfaceScale } from './contributions/interface-scale';
|
import { InterfaceScale } from './contributions/interface-scale';
|
||||||
import { OpenHandler } from '@theia/core/lib/browser/opener-service';
|
import { OpenHandler } from '@theia/core/lib/browser/opener-service';
|
||||||
|
import { NewCloudSketch } from './contributions/new-cloud-sketch';
|
||||||
|
import { SketchbookCompositeWidget } from './widgets/sketchbook/sketchbook-composite-widget';
|
||||||
|
|
||||||
const registerArduinoThemes = () => {
|
const registerArduinoThemes = () => {
|
||||||
const themes: MonacoThemeJson[] = [
|
const themes: MonacoThemeJson[] = [
|
||||||
@ -751,6 +753,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
Contribution.configure(bind, DeleteSketch);
|
Contribution.configure(bind, DeleteSketch);
|
||||||
Contribution.configure(bind, UpdateIndexes);
|
Contribution.configure(bind, UpdateIndexes);
|
||||||
Contribution.configure(bind, InterfaceScale);
|
Contribution.configure(bind, InterfaceScale);
|
||||||
|
Contribution.configure(bind, NewCloudSketch);
|
||||||
|
|
||||||
bindContributionProvider(bind, StartupTaskProvider);
|
bindContributionProvider(bind, StartupTaskProvider);
|
||||||
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
|
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
|
||||||
@ -905,6 +908,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
id: 'arduino-sketchbook-widget',
|
id: 'arduino-sketchbook-widget',
|
||||||
createWidget: () => container.get(SketchbookWidget),
|
createWidget: () => container.get(SketchbookWidget),
|
||||||
}));
|
}));
|
||||||
|
bind(SketchbookCompositeWidget).toSelf();
|
||||||
|
bind<WidgetFactory>(WidgetFactory).toDynamicValue((ctx) => ({
|
||||||
|
id: 'sketchbook-composite-widget',
|
||||||
|
createWidget: () => ctx.container.get(SketchbookCompositeWidget),
|
||||||
|
}));
|
||||||
|
|
||||||
bind(CloudSketchbookWidget).toSelf();
|
bind(CloudSketchbookWidget).toSelf();
|
||||||
rebind(SketchbookWidget).toService(CloudSketchbookWidget);
|
rebind(SketchbookWidget).toService(CloudSketchbookWidget);
|
||||||
|
@ -65,7 +65,7 @@ export class Close extends SketchContribution {
|
|||||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||||
commandId: Close.Commands.CLOSE.id,
|
commandId: Close.Commands.CLOSE.id,
|
||||||
label: nls.localize('vscode/editor.contribution/close', 'Close'),
|
label: nls.localize('vscode/editor.contribution/close', 'Close'),
|
||||||
order: '5',
|
order: '6',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,247 @@
|
|||||||
|
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||||
|
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
|
||||||
|
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
|
||||||
|
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||||
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||||
|
import type { AuthenticationSession } from '../../node/auth/types';
|
||||||
|
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 { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
|
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 { 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 widgetContribution: SketchbookWidgetContribution;
|
||||||
|
@inject(AuthenticationClientService)
|
||||||
|
private readonly authenticationService: AuthenticationClientService;
|
||||||
|
@inject(MainMenuManager)
|
||||||
|
private readonly mainMenuManager: MainMenuManager;
|
||||||
|
|
||||||
|
private readonly toDispose = new DisposableCollection();
|
||||||
|
private _session: AuthenticationSession | undefined;
|
||||||
|
private _enabled: boolean;
|
||||||
|
|
||||||
|
override onReady(): void {
|
||||||
|
this.toDispose.pushAll([
|
||||||
|
this.authenticationService.onSessionDidChange((session) => {
|
||||||
|
const oldSession = this._session;
|
||||||
|
this._session = session;
|
||||||
|
if (!!oldSession !== !!this._session) {
|
||||||
|
this.mainMenuManager.update();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
|
||||||
|
if (preferenceName === 'arduino.cloud.enabled') {
|
||||||
|
const oldEnabled = this._enabled;
|
||||||
|
this._enabled = Boolean(newValue);
|
||||||
|
if (this._enabled !== oldEnabled) {
|
||||||
|
this.mainMenuManager.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
this._enabled = this.preferences['arduino.cloud.enabled'];
|
||||||
|
this._session = this.authenticationService.session;
|
||||||
|
if (this._session) {
|
||||||
|
this.mainMenuManager.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onStop(): void {
|
||||||
|
this.toDispose.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerCommands(registry: CommandRegistry): void {
|
||||||
|
registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, {
|
||||||
|
execute: () => this.createNewSketch(),
|
||||||
|
isEnabled: () => !!this._session,
|
||||||
|
isVisible: () => this._enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
|
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||||
|
commandId: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
|
||||||
|
label: nls.localize('arduino/cloudSketch/new', 'New Remote Sketch'),
|
||||||
|
order: '1',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override registerKeybindings(registry: KeybindingRegistry): void {
|
||||||
|
registry.registerKeybinding({
|
||||||
|
command: NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
|
||||||
|
keybinding: 'CtrlCmd+Alt+N',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createNewSketch(
|
||||||
|
initialValue?: string | undefined
|
||||||
|
): Promise<URI | undefined> {
|
||||||
|
const widget = await this.widgetContribution.widget;
|
||||||
|
const treeModel = this.treeModelFrom(widget);
|
||||||
|
if (!treeModel) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const rootNode = CompositeTreeNode.is(treeModel.root)
|
||||||
|
? treeModel.root
|
||||||
|
: undefined;
|
||||||
|
if (!rootNode) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSketchName = await this.newSketchName(rootNode, initialValue);
|
||||||
|
if (!newSketchName) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let result: Create.Sketch | undefined | 'conflict';
|
||||||
|
try {
|
||||||
|
result = await this.createApi.createSketch(newSketchName);
|
||||||
|
} catch (err) {
|
||||||
|
if (isConflict(err)) {
|
||||||
|
result = 'conflict';
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (result) {
|
||||||
|
await treeModel.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result === 'conflict') {
|
||||||
|
return this.createNewSketch(newSketchName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
return this.open(treeModel, result);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async open(
|
||||||
|
treeModel: CloudSketchbookTreeModel,
|
||||||
|
newSketch: Create.Sketch
|
||||||
|
): Promise<URI | undefined> {
|
||||||
|
const id = CreateUri.toUri(newSketch).path.toString();
|
||||||
|
const node = treeModel.getNode(id);
|
||||||
|
if (!node) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not find remote sketchbook tree node with Tree node ID: ${id}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!CloudSketchbookTree.CloudSketchDirNode.is(node)) {
|
||||||
|
throw new Error(
|
||||||
|
`Remote sketchbook tree node expected to represent a directory but it did not. Tree node ID: ${id}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await treeModel.sketchbookTree().pull({ node });
|
||||||
|
} catch (err) {
|
||||||
|
if (isNotFound(err)) {
|
||||||
|
await treeModel.refresh();
|
||||||
|
this.messageService.error(
|
||||||
|
nls.localize(
|
||||||
|
'arduino/newCloudSketch/notFound',
|
||||||
|
"Could not pull the remote sketch '{0}'. It does not exist.",
|
||||||
|
newSketch.name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return this.commandService.executeCommand(
|
||||||
|
SketchbookCommands.OPEN_NEW_WINDOW.id,
|
||||||
|
{ node }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private treeModelFrom(
|
||||||
|
widget: SketchbookWidget
|
||||||
|
): CloudSketchbookTreeModel | undefined {
|
||||||
|
const treeWidget = widget.getTreeWidget();
|
||||||
|
if (treeWidget instanceof CloudSketchbookTreeWidget) {
|
||||||
|
const model = treeWidget.model;
|
||||||
|
if (model instanceof CloudSketchbookTreeModel) {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async newSketchName(
|
||||||
|
rootNode: CompositeTreeNode,
|
||||||
|
initialValue?: string | undefined
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const existingNames = rootNode.children
|
||||||
|
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
|
||||||
|
.map(({ fileStat }) => fileStat.name);
|
||||||
|
return new WorkspaceInputDialog(
|
||||||
|
{
|
||||||
|
title: nls.localize(
|
||||||
|
'arduino/newCloudSketch/newSketchTitle',
|
||||||
|
'Name of a new Remote Sketch'
|
||||||
|
),
|
||||||
|
parentUri: CreateUri.root,
|
||||||
|
initialValue,
|
||||||
|
validate: (input) => {
|
||||||
|
if (existingNames.includes(input)) {
|
||||||
|
return nls.localize(
|
||||||
|
'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,36}$/.test(input)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return nls.localize(
|
||||||
|
'arduino/newCloudSketch/invalidSketchName',
|
||||||
|
'The name must consist of basic letters, numbers, or underscores. The maximum length is 36 characters.'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
this.labelProvider
|
||||||
|
).open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export namespace NewCloudSketch {
|
||||||
|
export namespace Commands {
|
||||||
|
export const NEW_CLOUD_SKETCH: Command = {
|
||||||
|
id: 'arduino-new-cloud-sketch',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
@ -21,7 +21,7 @@ export class NewSketch extends SketchContribution {
|
|||||||
override registerMenus(registry: MenuModelRegistry): void {
|
override registerMenus(registry: MenuModelRegistry): void {
|
||||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||||
commandId: NewSketch.Commands.NEW_SKETCH.id,
|
commandId: NewSketch.Commands.NEW_SKETCH.id,
|
||||||
label: nls.localize('arduino/sketch/new', 'New'),
|
label: nls.localize('arduino/sketch/new', 'New Sketch'),
|
||||||
order: '0',
|
order: '0',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ export class OpenSketch extends SketchContribution {
|
|||||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||||
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
commandId: OpenSketch.Commands.OPEN_SKETCH.id,
|
||||||
label: nls.localize('vscode/workspaceActions/openFileFolder', 'Open...'),
|
label: nls.localize('vscode/workspaceActions/openFileFolder', 'Open...'),
|
||||||
order: '1',
|
order: '2',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ export class SaveSketch extends SketchContribution {
|
|||||||
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
registry.registerMenuAction(ArduinoMenus.FILE__SKETCH_GROUP, {
|
||||||
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
|
commandId: SaveSketch.Commands.SAVE_SKETCH.id,
|
||||||
label: nls.localize('vscode/fileCommands/save', 'Save'),
|
label: nls.localize('vscode/fileCommands/save', 'Save'),
|
||||||
order: '6',
|
order: '7',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,9 @@ export namespace CreateUri {
|
|||||||
export const scheme = 'arduino-create';
|
export const scheme = 'arduino-create';
|
||||||
export const root = toUri(posix.sep);
|
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 =
|
const posixPath =
|
||||||
typeof posixPathOrResource === 'string'
|
typeof posixPathOrResource === 'string'
|
||||||
? posixPathOrResource
|
? posixPathOrResource
|
||||||
|
@ -34,7 +34,6 @@ export class LocalCacheFsProvider
|
|||||||
@inject(AuthenticationClientService)
|
@inject(AuthenticationClientService)
|
||||||
protected readonly authenticationService: AuthenticationClientService;
|
protected readonly authenticationService: AuthenticationClientService;
|
||||||
|
|
||||||
// TODO: do we need this? Cannot we `await` on the `init` call from `registerFileSystemProviders`?
|
|
||||||
readonly ready = new Deferred<void>();
|
readonly ready = new Deferred<void>();
|
||||||
|
|
||||||
private _localCacheRoot: URI;
|
private _localCacheRoot: URI;
|
||||||
@ -153,7 +152,7 @@ export class LocalCacheFsProvider
|
|||||||
return uri;
|
return uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
private toUri(session: AuthenticationSession): URI {
|
toUri(session: AuthenticationSession): URI {
|
||||||
// Hack: instead of getting the UUID only, we get `auth0|UUID` after the authentication. `|` cannot be part of filesystem path or filename.
|
// Hack: instead of getting the UUID only, we get `auth0|UUID` after the authentication. `|` cannot be part of filesystem path or filename.
|
||||||
return this._localCacheRoot.resolve(session.id.split('|')[1]);
|
return this._localCacheRoot.resolve(session.id.split('|')[1]);
|
||||||
}
|
}
|
||||||
|
@ -80,10 +80,8 @@
|
|||||||
opacity: .4;
|
opacity: .4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media only screen and (max-height: 560px) {
|
@media only screen and (max-height: 560px) {
|
||||||
.p-Widget.dialogOverlay .dialogBlock {
|
.p-Widget.dialogOverlay .dialogBlock {
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -33,6 +33,22 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sketchbook-trees-container .create-new {
|
||||||
|
min-height: 58px;
|
||||||
|
height: 58px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
By default, theia-button has a left-margin. IDE2 does not need the left margin
|
||||||
|
for the _New Remote? Sketch_. Otherwise, the button does not fit the default
|
||||||
|
widget width.
|
||||||
|
*/
|
||||||
|
.sketchbook-trees-container .create-new .theia-button {
|
||||||
|
margin-left: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.sketchbook-tree__opts {
|
.sketchbook-tree__opts {
|
||||||
background-color: var(--theia-foreground);
|
background-color: var(--theia-foreground);
|
||||||
-webkit-mask: url(./sketchbook-opts-icon.svg);
|
-webkit-mask: url(./sketchbook-opts-icon.svg);
|
||||||
|
@ -1,78 +1,78 @@
|
|||||||
import * as React from '@theia/core/shared/react';
|
import * as React from '@theia/core/shared/react';
|
||||||
import * as ReactDOM from '@theia/core/shared/react-dom';
|
import * as ReactDOM from '@theia/core/shared/react-dom';
|
||||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
import {
|
||||||
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
inject,
|
||||||
import { Message, MessageLoop } from '@theia/core/shared/@phosphor/messaging';
|
injectable,
|
||||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
postConstruct,
|
||||||
import { BaseWidget } from '@theia/core/lib/browser/widgets/widget';
|
} from '@theia/core/shared/inversify';
|
||||||
import { UserStatus } from './cloud-user-status';
|
import { UserStatus } from './cloud-user-status';
|
||||||
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
import { CloudSketchbookTreeWidget } from './cloud-sketchbook-tree-widget';
|
import { CloudSketchbookTreeWidget } from './cloud-sketchbook-tree-widget';
|
||||||
import { AuthenticationClientService } from '../../auth/authentication-client-service';
|
import { AuthenticationClientService } from '../../auth/authentication-client-service';
|
||||||
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
|
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
|
||||||
import { nls } from '@theia/core/lib/common';
|
import { BaseSketchbookCompositeWidget } from '../sketchbook/sketchbook-composite-widget';
|
||||||
|
import { CreateNew } from '../sketchbook/create-new';
|
||||||
|
import { AuthenticationSession } from '../../../node/auth/types';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class CloudSketchbookCompositeWidget extends BaseWidget {
|
export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidget<CloudSketchbookTreeWidget> {
|
||||||
@inject(AuthenticationClientService)
|
@inject(AuthenticationClientService)
|
||||||
protected readonly authenticationService: AuthenticationClientService;
|
private readonly authenticationService: AuthenticationClientService;
|
||||||
|
|
||||||
@inject(CloudSketchbookTreeWidget)
|
@inject(CloudSketchbookTreeWidget)
|
||||||
protected readonly cloudSketchbookTreeWidget: CloudSketchbookTreeWidget;
|
private readonly cloudSketchbookTreeWidget: CloudSketchbookTreeWidget;
|
||||||
|
private _session: AuthenticationSession | undefined;
|
||||||
private compositeNode: HTMLElement;
|
|
||||||
private cloudUserStatusNode: HTMLElement;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.compositeNode = document.createElement('div');
|
this.id = 'cloud-sketchbook-composite-widget';
|
||||||
this.compositeNode.classList.add('composite-node');
|
|
||||||
this.cloudUserStatusNode = document.createElement('div');
|
|
||||||
this.cloudUserStatusNode.classList.add('cloud-status-node');
|
|
||||||
this.compositeNode.appendChild(this.cloudUserStatusNode);
|
|
||||||
this.node.appendChild(this.compositeNode);
|
|
||||||
this.title.caption = nls.localize(
|
this.title.caption = nls.localize(
|
||||||
'arduino/cloud/remoteSketchbook',
|
'arduino/cloud/remoteSketchbook',
|
||||||
'Remote Sketchbook'
|
'Remote Sketchbook'
|
||||||
);
|
);
|
||||||
this.title.iconClass = 'cloud-sketchbook-tree-icon';
|
this.title.iconClass = 'cloud-sketchbook-tree-icon';
|
||||||
this.title.closable = false;
|
|
||||||
this.id = 'cloud-sketchbook-composite-widget';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTreeWidget(): CloudSketchbookTreeWidget {
|
@postConstruct()
|
||||||
|
protected init(): void {
|
||||||
|
this.toDispose.push(
|
||||||
|
this.authenticationService.onSessionDidChange((session) => {
|
||||||
|
const oldSession = this._session;
|
||||||
|
this._session = session;
|
||||||
|
if (!!oldSession !== !!this._session) {
|
||||||
|
this.updateFooter();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get treeWidget(): CloudSketchbookTreeWidget {
|
||||||
return this.cloudSketchbookTreeWidget;
|
return this.cloudSketchbookTreeWidget;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override onAfterAttach(message: Message): void {
|
protected renderFooter(footerNode: HTMLElement): void {
|
||||||
super.onAfterAttach(message);
|
|
||||||
Widget.attach(this.cloudSketchbookTreeWidget, this.compositeNode);
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<UserStatus
|
<>
|
||||||
model={this.cloudSketchbookTreeWidget.model as CloudSketchbookTreeModel}
|
{this._session && (
|
||||||
authenticationService={this.authenticationService}
|
<CreateNew
|
||||||
/>,
|
label={nls.localize(
|
||||||
this.cloudUserStatusNode
|
'arduino/sketchbook/newRemoteSketch',
|
||||||
);
|
'New Remote Sketch'
|
||||||
this.toDisposeOnDetach.push(
|
)}
|
||||||
Disposable.create(() => Widget.detach(this.cloudSketchbookTreeWidget))
|
onClick={this.onDidClickCreateNew}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<UserStatus
|
||||||
|
model={
|
||||||
|
this.cloudSketchbookTreeWidget.model as CloudSketchbookTreeModel
|
||||||
|
}
|
||||||
|
authenticationService={this.authenticationService}
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
footerNode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override onActivateRequest(msg: Message): void {
|
private onDidClickCreateNew: () => void = () => {
|
||||||
super.onActivateRequest(msg);
|
this.commandService.executeCommand('arduino-new-cloud-sketch');
|
||||||
|
};
|
||||||
/*
|
|
||||||
Sending a resize message is needed because otherwise the cloudSketchbookTreeWidget
|
|
||||||
would render empty
|
|
||||||
*/
|
|
||||||
this.onResize(Widget.ResizeMessage.UnknownSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override onResize(message: Widget.ResizeMessage): void {
|
|
||||||
super.onResize(message);
|
|
||||||
MessageLoop.sendMessage(
|
|
||||||
this.cloudSketchbookTreeWidget,
|
|
||||||
Widget.ResizeMessage.UnknownSize
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,26 @@
|
|||||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
import {
|
||||||
import { TreeNode } from '@theia/core/lib/browser/tree';
|
inject,
|
||||||
|
injectable,
|
||||||
|
postConstruct,
|
||||||
|
} from '@theia/core/shared/inversify';
|
||||||
|
import { CompositeTreeNode, TreeNode } from '@theia/core/lib/browser/tree';
|
||||||
import { posixSegments, splitSketchPath } from '../../create/create-paths';
|
import { posixSegments, splitSketchPath } from '../../create/create-paths';
|
||||||
import { CreateApi } from '../../create/create-api';
|
import { CreateApi } from '../../create/create-api';
|
||||||
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
|
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
|
||||||
import { AuthenticationClientService } from '../../auth/authentication-client-service';
|
import { AuthenticationClientService } from '../../auth/authentication-client-service';
|
||||||
import { SketchbookTreeModel } from '../sketchbook/sketchbook-tree-model';
|
import { SketchbookTreeModel } from '../sketchbook/sketchbook-tree-model';
|
||||||
import { ArduinoPreferences } from '../../arduino-preferences';
|
|
||||||
import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree';
|
import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree';
|
||||||
import { CreateUri } from '../../create/create-uri';
|
import { CreateUri } from '../../create/create-uri';
|
||||||
import { FileStat } from '@theia/filesystem/lib/common/files';
|
import { FileChangesEvent, FileStat } from '@theia/filesystem/lib/common/files';
|
||||||
import { LocalCacheFsProvider } from '../../local-cache/local-cache-fs-provider';
|
import {
|
||||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
LocalCacheFsProvider,
|
||||||
|
LocalCacheUri,
|
||||||
|
} from '../../local-cache/local-cache-fs-provider';
|
||||||
import URI from '@theia/core/lib/common/uri';
|
import URI from '@theia/core/lib/common/uri';
|
||||||
import { SketchCache } from './cloud-sketch-cache';
|
import { SketchCache } from './cloud-sketch-cache';
|
||||||
import { Create } from '../../create/typings';
|
import { Create } from '../../create/typings';
|
||||||
import { nls } from '@theia/core/lib/common';
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||||
|
|
||||||
export function sketchBaseDir(sketch: Create.Sketch): FileStat {
|
export function sketchBaseDir(sketch: Create.Sketch): FileStat {
|
||||||
// extract the sketch path
|
// extract the sketch path
|
||||||
@ -52,26 +58,16 @@ export function sketchesToFileStats(sketches: Create.Sketch[]): FileStat[] {
|
|||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class CloudSketchbookTreeModel extends SketchbookTreeModel {
|
export class CloudSketchbookTreeModel extends SketchbookTreeModel {
|
||||||
@inject(FileService)
|
|
||||||
protected override readonly fileService: FileService;
|
|
||||||
|
|
||||||
@inject(AuthenticationClientService)
|
|
||||||
protected readonly authenticationService: AuthenticationClientService;
|
|
||||||
|
|
||||||
@inject(CreateApi)
|
@inject(CreateApi)
|
||||||
protected readonly createApi: CreateApi;
|
private readonly createApi: CreateApi;
|
||||||
|
@inject(AuthenticationClientService)
|
||||||
@inject(CloudSketchbookTree)
|
private readonly authenticationService: AuthenticationClientService;
|
||||||
protected readonly cloudSketchbookTree: CloudSketchbookTree;
|
|
||||||
|
|
||||||
@inject(ArduinoPreferences)
|
|
||||||
protected override readonly arduinoPreferences: ArduinoPreferences;
|
|
||||||
|
|
||||||
@inject(LocalCacheFsProvider)
|
@inject(LocalCacheFsProvider)
|
||||||
protected readonly localCacheFsProvider: LocalCacheFsProvider;
|
private readonly localCacheFsProvider: LocalCacheFsProvider;
|
||||||
|
|
||||||
@inject(SketchCache)
|
@inject(SketchCache)
|
||||||
protected readonly sketchCache: SketchCache;
|
private readonly sketchCache: SketchCache;
|
||||||
|
|
||||||
|
private _localCacheFsProviderReady: Deferred<void> | undefined;
|
||||||
|
|
||||||
@postConstruct()
|
@postConstruct()
|
||||||
protected override init(): void {
|
protected override init(): void {
|
||||||
@ -81,6 +77,50 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override *getNodesByUri(uri: URI): IterableIterator<TreeNode> {
|
||||||
|
if (uri.scheme === LocalCacheUri.scheme) {
|
||||||
|
const workspace = this.root;
|
||||||
|
const { session } = this.authenticationService;
|
||||||
|
if (session && WorkspaceNode.is(workspace)) {
|
||||||
|
const currentUri = this.localCacheFsProvider.to(uri);
|
||||||
|
if (currentUri) {
|
||||||
|
const rootPath = this.localCacheFsProvider
|
||||||
|
.toUri(session)
|
||||||
|
.path.toString();
|
||||||
|
const currentPath = currentUri.path.toString();
|
||||||
|
if (rootPath === currentPath) {
|
||||||
|
return workspace;
|
||||||
|
}
|
||||||
|
if (currentPath.startsWith(rootPath)) {
|
||||||
|
const id = currentPath.substring(rootPath.length);
|
||||||
|
const node = this.getNode(id);
|
||||||
|
if (node) {
|
||||||
|
yield node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override isRootAffected(changes: FileChangesEvent): boolean {
|
||||||
|
return changes.changes
|
||||||
|
.map(({ resource }) => resource)
|
||||||
|
.some(
|
||||||
|
(uri) => uri.parent.toString().startsWith(LocalCacheUri.root.toString()) // all files under the root might affect the tree
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async refresh(
|
||||||
|
parent?: Readonly<CompositeTreeNode>
|
||||||
|
): Promise<CompositeTreeNode | undefined> {
|
||||||
|
if (parent) {
|
||||||
|
return super.refresh(parent);
|
||||||
|
}
|
||||||
|
await this.updateRoot();
|
||||||
|
return super.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
override async createRoot(): Promise<TreeNode | undefined> {
|
override async createRoot(): Promise<TreeNode | undefined> {
|
||||||
const { session } = this.authenticationService;
|
const { session } = this.authenticationService;
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@ -89,7 +129,10 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
|
|||||||
}
|
}
|
||||||
this.createApi.init(this.authenticationService, this.arduinoPreferences);
|
this.createApi.init(this.authenticationService, this.arduinoPreferences);
|
||||||
this.sketchCache.init();
|
this.sketchCache.init();
|
||||||
const sketches = await this.createApi.sketches();
|
const [sketches] = await Promise.all([
|
||||||
|
this.createApi.sketches(),
|
||||||
|
this.ensureLocalFsProviderReady(),
|
||||||
|
]);
|
||||||
const rootFileStats = sketchesToFileStats(sketches);
|
const rootFileStats = sketchesToFileStats(sketches);
|
||||||
if (this.workspaceService.opened) {
|
if (this.workspaceService.opened) {
|
||||||
const workspaceNode = WorkspaceNode.createRoot(
|
const workspaceNode = WorkspaceNode.createRoot(
|
||||||
@ -108,7 +151,9 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
|
|||||||
return this.tree as CloudSketchbookTree;
|
return this.tree as CloudSketchbookTree;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override recursivelyFindSketchRoot(node: TreeNode): any {
|
protected override recursivelyFindSketchRoot(
|
||||||
|
node: TreeNode
|
||||||
|
): TreeNode | false {
|
||||||
if (node && CloudSketchbookTree.CloudSketchDirNode.is(node)) {
|
if (node && CloudSketchbookTree.CloudSketchDirNode.is(node)) {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
@ -122,13 +167,25 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async revealFile(uri: URI): Promise<TreeNode | undefined> {
|
override async revealFile(uri: URI): Promise<TreeNode | undefined> {
|
||||||
|
await this.localCacheFsProvider.ready.promise;
|
||||||
// we use remote uris as keys for the tree
|
// we use remote uris as keys for the tree
|
||||||
// convert local URIs
|
// convert local URIs
|
||||||
const remoteuri = this.localCacheFsProvider.from(uri);
|
const remoteUri = this.localCacheFsProvider.from(uri);
|
||||||
if (remoteuri) {
|
if (remoteUri) {
|
||||||
return super.revealFile(remoteuri);
|
return super.revealFile(remoteUri);
|
||||||
} else {
|
} else {
|
||||||
return super.revealFile(uri);
|
return super.revealFile(uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ensureLocalFsProviderReady(): Promise<void> {
|
||||||
|
if (this._localCacheFsProviderReady) {
|
||||||
|
return this._localCacheFsProviderReady.promise;
|
||||||
|
}
|
||||||
|
this._localCacheFsProviderReady = new Deferred();
|
||||||
|
this.fileService
|
||||||
|
.access(LocalCacheUri.root)
|
||||||
|
.then(() => this._localCacheFsProviderReady?.resolve());
|
||||||
|
return this._localCacheFsProviderReady.promise;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as React from '@theia/core/shared/react';
|
import * as React from '@theia/core/shared/react';
|
||||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { TreeModel } from '@theia/core/lib/browser/tree/tree-model';
|
import { TreeModel } from '@theia/core/lib/browser/tree/tree-model';
|
||||||
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
|
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
|
||||||
import { AuthenticationClientService } from '../../auth/authentication-client-service';
|
import { AuthenticationClientService } from '../../auth/authentication-client-service';
|
||||||
@ -27,12 +27,6 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
|
|||||||
@inject(CloudSketchbookTree)
|
@inject(CloudSketchbookTree)
|
||||||
protected readonly cloudSketchbookTree: CloudSketchbookTree;
|
protected readonly cloudSketchbookTree: CloudSketchbookTree;
|
||||||
|
|
||||||
@postConstruct()
|
|
||||||
protected override async init(): Promise<void> {
|
|
||||||
await super.init();
|
|
||||||
this.addClass('tree-container'); // Adds `height: 100%` to the tree. Otherwise you cannot see it.
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override renderTree(model: TreeModel): React.ReactNode {
|
protected override renderTree(model: TreeModel): React.ReactNode {
|
||||||
if (this.shouldShowWelcomeView()) return this.renderViewWelcome();
|
if (this.shouldShowWelcomeView()) return this.renderViewWelcome();
|
||||||
if (this.shouldShowEmptyView()) return this.renderEmptyView();
|
if (this.shouldShowEmptyView()) return this.renderEmptyView();
|
||||||
|
@ -136,7 +136,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.runWithState(node, 'pulling', async (node) => {
|
return this.runWithState(node, 'pulling', async (node) => {
|
||||||
const commandsCopy = node.commands;
|
const commandsCopy = node.commands;
|
||||||
node.commands = [];
|
node.commands = [];
|
||||||
|
|
||||||
@ -196,7 +196,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.runWithState(node, 'pushing', async (node) => {
|
return this.runWithState(node, 'pushing', async (node) => {
|
||||||
if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
|
if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
nls.localize(
|
nls.localize(
|
||||||
@ -269,7 +269,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
|||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
// do not map "do_not_sync" files/directoris and their descendants
|
// do not map "do_not_sync" files/directories and their descendants
|
||||||
const segments = path[1].split(posix.sep) || [];
|
const segments = path[1].split(posix.sep) || [];
|
||||||
if (
|
if (
|
||||||
segments.some((segment) => Create.do_not_sync_files.includes(segment))
|
segments.some((segment) => Create.do_not_sync_files.includes(segment))
|
||||||
|
@ -2,6 +2,7 @@ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'
|
|||||||
import { CloudSketchbookCompositeWidget } from './cloud-sketchbook-composite-widget';
|
import { CloudSketchbookCompositeWidget } from './cloud-sketchbook-composite-widget';
|
||||||
import { SketchbookWidget } from '../sketchbook/sketchbook-widget';
|
import { SketchbookWidget } from '../sketchbook/sketchbook-widget';
|
||||||
import { ArduinoPreferences } from '../../arduino-preferences';
|
import { ArduinoPreferences } from '../../arduino-preferences';
|
||||||
|
import { BaseSketchbookCompositeWidget } from '../sketchbook/sketchbook-composite-widget';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class CloudSketchbookWidget extends SketchbookWidget {
|
export class CloudSketchbookWidget extends SketchbookWidget {
|
||||||
@ -19,8 +20,8 @@ export class CloudSketchbookWidget extends SketchbookWidget {
|
|||||||
override getTreeWidget(): any {
|
override getTreeWidget(): any {
|
||||||
const widget: any = this.sketchbookTreesContainer.selectedWidgets().next();
|
const widget: any = this.sketchbookTreesContainer.selectedWidgets().next();
|
||||||
|
|
||||||
if (widget && typeof widget.getTreeWidget !== 'undefined') {
|
if (widget instanceof BaseSketchbookCompositeWidget) {
|
||||||
return (widget as CloudSketchbookCompositeWidget).getTreeWidget();
|
return widget.treeWidget;
|
||||||
}
|
}
|
||||||
return widget;
|
return widget;
|
||||||
}
|
}
|
||||||
@ -30,7 +31,7 @@ export class CloudSketchbookWidget extends SketchbookWidget {
|
|||||||
this.sketchbookTreesContainer.activateWidget(this.widget);
|
this.sketchbookTreesContainer.activateWidget(this.widget);
|
||||||
} else {
|
} else {
|
||||||
this.sketchbookTreesContainer.activateWidget(
|
this.sketchbookTreesContainer.activateWidget(
|
||||||
this.localSketchbookTreeWidget
|
this.sketchbookCompositeWidget
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.setDocumentMode();
|
this.setDocumentMode();
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
import * as React from '@theia/core/shared/react';
|
||||||
|
|
||||||
|
export class CreateNew extends React.Component<CreateNew.Props> {
|
||||||
|
override render(): React.ReactNode {
|
||||||
|
return (
|
||||||
|
<div className="create-new">
|
||||||
|
<button className="theia-button secondary" onClick={this.props.onClick}>
|
||||||
|
{this.props.label}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace CreateNew {
|
||||||
|
export interface Props {
|
||||||
|
readonly label: string;
|
||||||
|
readonly onClick: () => void;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
import * as React from '@theia/core/shared/react';
|
||||||
|
import * as ReactDOM from '@theia/core/shared/react-dom';
|
||||||
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
|
import { 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 { CommandService } from '@theia/core/lib/common/command';
|
||||||
|
import { SketchbookTreeWidget } from './sketchbook-tree-widget';
|
||||||
|
import { CreateNew } from '../sketchbook/create-new';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export abstract class BaseSketchbookCompositeWidget<
|
||||||
|
TW extends SketchbookTreeWidget
|
||||||
|
> extends BaseWidget {
|
||||||
|
@inject(CommandService)
|
||||||
|
protected readonly commandService: CommandService;
|
||||||
|
|
||||||
|
private readonly compositeNode: HTMLElement;
|
||||||
|
private readonly footerNode: HTMLElement;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.compositeNode = document.createElement('div');
|
||||||
|
this.compositeNode.classList.add('composite-node');
|
||||||
|
this.footerNode = document.createElement('div');
|
||||||
|
this.footerNode.classList.add('footer-node');
|
||||||
|
this.compositeNode.appendChild(this.footerNode);
|
||||||
|
this.node.appendChild(this.compositeNode);
|
||||||
|
this.title.closable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract get treeWidget(): TW;
|
||||||
|
protected abstract renderFooter(footerNode: HTMLElement): void;
|
||||||
|
protected updateFooter(): void {
|
||||||
|
this.renderFooter(this.footerNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override onAfterAttach(message: Message): void {
|
||||||
|
super.onAfterAttach(message);
|
||||||
|
Widget.attach(this.treeWidget, this.compositeNode);
|
||||||
|
this.renderFooter(this.footerNode);
|
||||||
|
this.toDisposeOnDetach.push(
|
||||||
|
Disposable.create(() => Widget.detach(this.treeWidget))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override onActivateRequest(message: Message): void {
|
||||||
|
super.onActivateRequest(message);
|
||||||
|
// Sending a resize message is needed because otherwise the tree widget would render empty
|
||||||
|
this.onResize(Widget.ResizeMessage.UnknownSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override onResize(message: Widget.ResizeMessage): void {
|
||||||
|
super.onResize(message);
|
||||||
|
MessageLoop.sendMessage(this.treeWidget, Widget.ResizeMessage.UnknownSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class SketchbookCompositeWidget extends BaseSketchbookCompositeWidget<SketchbookTreeWidget> {
|
||||||
|
@inject(SketchbookTreeWidget)
|
||||||
|
private readonly sketchbookTreeWidget: SketchbookTreeWidget;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.id = 'sketchbook-composite-widget';
|
||||||
|
this.title.caption = nls.localize(
|
||||||
|
'arduino/sketch/titleLocalSketchbook',
|
||||||
|
'Local Sketchbook'
|
||||||
|
);
|
||||||
|
this.title.iconClass = 'sketchbook-tree-icon';
|
||||||
|
}
|
||||||
|
|
||||||
|
get treeWidget(): SketchbookTreeWidget {
|
||||||
|
return this.sketchbookTreeWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderFooter(footerNode: HTMLElement): void {
|
||||||
|
ReactDOM.render(
|
||||||
|
<CreateNew
|
||||||
|
label={nls.localize('arduino/sketchbook/newSketch', 'New Sketch')}
|
||||||
|
onClick={this.onDidClickCreateNew}
|
||||||
|
/>,
|
||||||
|
footerNode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDidClickCreateNew: () => void = () => {
|
||||||
|
this.commandService.executeCommand('arduino-new-sketch');
|
||||||
|
};
|
||||||
|
}
|
@ -59,6 +59,7 @@ export class SketchbookTreeWidget extends FileTreeWidget {
|
|||||||
'Local Sketchbook'
|
'Local Sketchbook'
|
||||||
);
|
);
|
||||||
this.title.closable = false;
|
this.title.closable = false;
|
||||||
|
this.addClass('tree-container'); // Adds `height: 100%` to the tree. Otherwise you cannot see it.
|
||||||
}
|
}
|
||||||
|
|
||||||
@postConstruct()
|
@postConstruct()
|
||||||
|
@ -11,15 +11,21 @@ import { Disposable } from '@theia/core/lib/common/disposable';
|
|||||||
import { BaseWidget } from '@theia/core/lib/browser/widgets/widget';
|
import { BaseWidget } from '@theia/core/lib/browser/widgets/widget';
|
||||||
import { SketchbookTreeWidget } from './sketchbook-tree-widget';
|
import { SketchbookTreeWidget } from './sketchbook-tree-widget';
|
||||||
import { nls } from '@theia/core/lib/common';
|
import { nls } from '@theia/core/lib/common';
|
||||||
import { CloudSketchbookCompositeWidget } from '../cloud-sketchbook/cloud-sketchbook-composite-widget';
|
|
||||||
import { URI } from '../../contributions/contribution';
|
import { URI } from '../../contributions/contribution';
|
||||||
|
import {
|
||||||
|
BaseSketchbookCompositeWidget,
|
||||||
|
SketchbookCompositeWidget,
|
||||||
|
} from './sketchbook-composite-widget';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class SketchbookWidget extends BaseWidget {
|
export class SketchbookWidget extends BaseWidget {
|
||||||
static LABEL = nls.localize('arduino/sketch/titleSketchbook', 'Sketchbook');
|
static readonly LABEL = nls.localize(
|
||||||
|
'arduino/sketch/titleSketchbook',
|
||||||
|
'Sketchbook'
|
||||||
|
);
|
||||||
|
|
||||||
@inject(SketchbookTreeWidget)
|
@inject(SketchbookCompositeWidget)
|
||||||
protected readonly localSketchbookTreeWidget: SketchbookTreeWidget;
|
protected readonly sketchbookCompositeWidget: SketchbookCompositeWidget;
|
||||||
|
|
||||||
protected readonly sketchbookTreesContainer: DockPanel;
|
protected readonly sketchbookTreesContainer: DockPanel;
|
||||||
|
|
||||||
@ -36,7 +42,7 @@ export class SketchbookWidget extends BaseWidget {
|
|||||||
|
|
||||||
@postConstruct()
|
@postConstruct()
|
||||||
protected init(): void {
|
protected init(): void {
|
||||||
this.sketchbookTreesContainer.addWidget(this.localSketchbookTreeWidget);
|
this.sketchbookTreesContainer.addWidget(this.sketchbookCompositeWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override onAfterAttach(message: Message): void {
|
protected override onAfterAttach(message: Message): void {
|
||||||
@ -48,7 +54,7 @@ export class SketchbookWidget extends BaseWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTreeWidget(): SketchbookTreeWidget {
|
getTreeWidget(): SketchbookTreeWidget {
|
||||||
return this.localSketchbookTreeWidget;
|
return this.sketchbookCompositeWidget.treeWidget;
|
||||||
}
|
}
|
||||||
|
|
||||||
activeTreeWidgetId(): string | undefined {
|
activeTreeWidgetId(): string | undefined {
|
||||||
@ -80,8 +86,8 @@ export class SketchbookWidget extends BaseWidget {
|
|||||||
if (widget instanceof SketchbookTreeWidget) {
|
if (widget instanceof SketchbookTreeWidget) {
|
||||||
return widget;
|
return widget;
|
||||||
}
|
}
|
||||||
if (widget instanceof CloudSketchbookCompositeWidget) {
|
if (widget instanceof BaseSketchbookCompositeWidget) {
|
||||||
return widget.getTreeWidget();
|
return widget.treeWidget;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
@ -2,8 +2,10 @@ import { inject, injectable } from '@theia/core/shared/inversify';
|
|||||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||||
import { isOSX } from '@theia/core/lib/common/os';
|
import { isOSX } from '@theia/core/lib/common/os';
|
||||||
import {
|
import {
|
||||||
|
ActionMenuNode,
|
||||||
CompositeMenuNode,
|
CompositeMenuNode,
|
||||||
MAIN_MENU_BAR,
|
MAIN_MENU_BAR,
|
||||||
|
MenuNode,
|
||||||
MenuPath,
|
MenuPath,
|
||||||
} from '@theia/core/lib/common/menu';
|
} from '@theia/core/lib/common/menu';
|
||||||
import {
|
import {
|
||||||
@ -134,7 +136,7 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override handleElectronDefault(
|
protected override handleElectronDefault(
|
||||||
menuNode: CompositeMenuNode,
|
menuNode: MenuNode,
|
||||||
args: any[] = [],
|
args: any[] = [],
|
||||||
options?: ElectronMenuOptions
|
options?: ElectronMenuOptions
|
||||||
): Electron.MenuItemConstructorOptions[] {
|
): Electron.MenuItemConstructorOptions[] {
|
||||||
@ -149,4 +151,119 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
|
|||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copied from 1.25.0 Theia as is to customize the enablement of the menu items.
|
||||||
|
// Source: https://github.com/eclipse-theia/theia/blob/ca417a31e402bd35717d3314bf6254049d1dae44/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts#L125-L220
|
||||||
|
// See https://github.com/arduino/arduino-ide/issues/1533
|
||||||
|
protected override fillMenuTemplate(
|
||||||
|
items: Electron.MenuItemConstructorOptions[],
|
||||||
|
menuModel: CompositeMenuNode,
|
||||||
|
args: any[] = [],
|
||||||
|
options?: ElectronMenuOptions
|
||||||
|
): Electron.MenuItemConstructorOptions[] {
|
||||||
|
const showDisabled =
|
||||||
|
options?.showDisabled === undefined ? true : options?.showDisabled;
|
||||||
|
for (const menu of menuModel.children) {
|
||||||
|
if (menu instanceof CompositeMenuNode) {
|
||||||
|
if (menu.children.length > 0) {
|
||||||
|
// do not render empty nodes
|
||||||
|
|
||||||
|
if (menu.isSubmenu) {
|
||||||
|
// submenu node
|
||||||
|
|
||||||
|
const submenu = this.fillMenuTemplate([], menu, args, options);
|
||||||
|
if (submenu.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
label: menu.label,
|
||||||
|
submenu,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// group node
|
||||||
|
|
||||||
|
// process children
|
||||||
|
const submenu = this.fillMenuTemplate([], menu, args, options);
|
||||||
|
if (submenu.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length > 0) {
|
||||||
|
// do not put a separator above the first group
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
type: 'separator',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// render children
|
||||||
|
items.push(...submenu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (menu instanceof ActionMenuNode) {
|
||||||
|
const node =
|
||||||
|
menu.altNode && this.context.altPressed ? menu.altNode : menu;
|
||||||
|
const commandId = node.action.commandId;
|
||||||
|
|
||||||
|
// That is only a sanity check at application startup.
|
||||||
|
if (!this.commandRegistry.getCommand(commandId)) {
|
||||||
|
console.debug(
|
||||||
|
`Skipping menu item with missing command: "${commandId}".`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.commandRegistry.isVisible(commandId, ...args) ||
|
||||||
|
(!!node.action.when &&
|
||||||
|
!this.contextKeyService.match(node.action.when))
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should omit rendering context-menu items which are disabled.
|
||||||
|
if (
|
||||||
|
!showDisabled &&
|
||||||
|
!this.commandRegistry.isEnabled(commandId, ...args)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bindings =
|
||||||
|
this.keybindingRegistry.getKeybindingsForCommand(commandId);
|
||||||
|
|
||||||
|
const accelerator = bindings[0] && this.acceleratorFor(bindings[0]);
|
||||||
|
|
||||||
|
const menuItem: Electron.MenuItemConstructorOptions = {
|
||||||
|
id: node.id,
|
||||||
|
label: node.label,
|
||||||
|
type: this.commandRegistry.getToggledHandler(commandId, ...args)
|
||||||
|
? 'checkbox'
|
||||||
|
: 'normal',
|
||||||
|
checked: this.commandRegistry.isToggled(commandId, ...args),
|
||||||
|
enabled: this.commandRegistry.isEnabled(commandId, ...args), // Unlike Theia https://github.com/eclipse-theia/theia/blob/ca417a31e402bd35717d3314bf6254049d1dae44/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts#L197
|
||||||
|
visible: true,
|
||||||
|
accelerator,
|
||||||
|
click: () => this.execute(commandId, args),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isOSX) {
|
||||||
|
const role = this.roleFor(node.id);
|
||||||
|
if (role) {
|
||||||
|
menuItem.role = role;
|
||||||
|
delete menuItem.click;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.push(menuItem);
|
||||||
|
|
||||||
|
if (this.commandRegistry.getToggledHandler(commandId, ...args)) {
|
||||||
|
this._toggledCommands.add(commandId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items.push(...this.handleElectronDefault(menu, args, options));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
15
i18n/en.json
15
i18n/en.json
@ -119,6 +119,9 @@
|
|||||||
"syncEditSketches": "Sync and edit your Arduino Cloud Sketches",
|
"syncEditSketches": "Sync and edit your Arduino Cloud Sketches",
|
||||||
"visitArduinoCloud": "Visit Arduino Cloud to create Cloud Sketches."
|
"visitArduinoCloud": "Visit Arduino Cloud to create Cloud Sketches."
|
||||||
},
|
},
|
||||||
|
"cloudSketch": {
|
||||||
|
"new": "New Remote Sketch"
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"contributed": "Contributed",
|
"contributed": "Contributed",
|
||||||
@ -299,6 +302,12 @@
|
|||||||
"unableToCloseWebSocket": "Unable to close websocket",
|
"unableToCloseWebSocket": "Unable to close websocket",
|
||||||
"unableToConnectToWebSocket": "Unable to connect to websocket"
|
"unableToConnectToWebSocket": "Unable to connect to websocket"
|
||||||
},
|
},
|
||||||
|
"newCloudSketch": {
|
||||||
|
"invalidSketchName": "The name must consist of basic letters, numbers, or underscores. The maximum length is 36 characters.",
|
||||||
|
"newSketchTitle": "Name of a new Remote Sketch",
|
||||||
|
"notFound": "Could not pull the remote sketch '{0}'. It does not exist.",
|
||||||
|
"sketchAlreadyExists": "Remote sketch '{0}' already exists."
|
||||||
|
},
|
||||||
"portProtocol": {
|
"portProtocol": {
|
||||||
"network": "Network",
|
"network": "Network",
|
||||||
"serial": "Serial"
|
"serial": "Serial"
|
||||||
@ -388,7 +397,7 @@
|
|||||||
"exportBinary": "Export Compiled Binary",
|
"exportBinary": "Export Compiled Binary",
|
||||||
"moving": "Moving",
|
"moving": "Moving",
|
||||||
"movingMsg": "The file \"{0}\" needs to be inside a sketch folder named \"{1}\".\nCreate this folder, move the file, and continue?",
|
"movingMsg": "The file \"{0}\" needs to be inside a sketch folder named \"{1}\".\nCreate this folder, move the file, and continue?",
|
||||||
"new": "New",
|
"new": "New Sketch",
|
||||||
"openFolder": "Open Folder",
|
"openFolder": "Open Folder",
|
||||||
"openRecent": "Open Recent",
|
"openRecent": "Open Recent",
|
||||||
"openSketchInNewWindow": "Open Sketch in New Window",
|
"openSketchInNewWindow": "Open Sketch in New Window",
|
||||||
@ -407,6 +416,10 @@
|
|||||||
"verify": "Verify",
|
"verify": "Verify",
|
||||||
"verifyOrCompile": "Verify/Compile"
|
"verifyOrCompile": "Verify/Compile"
|
||||||
},
|
},
|
||||||
|
"sketchbook": {
|
||||||
|
"newRemoteSketch": "New Remote Sketch",
|
||||||
|
"newSketch": "New Sketch"
|
||||||
|
},
|
||||||
"survey": {
|
"survey": {
|
||||||
"answerSurvey": "Answer survey",
|
"answerSurvey": "Answer survey",
|
||||||
"dismissSurvey": "Don't show again",
|
"dismissSurvey": "Don't show again",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user