Compare commits
25 Commits
#1733
...
show-updat
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5c31c93636 | ||
![]() |
03355903b9 | ||
![]() |
fa4626bf14 | ||
![]() |
9b49712669 | ||
![]() |
0ab28266df | ||
![]() |
b09ae48536 | ||
![]() |
2aad0e3b16 | ||
![]() |
58aac236bf | ||
![]() |
ec24b6813d | ||
![]() |
d398ed1345 | ||
![]() |
fb10de1446 | ||
![]() |
24dc0bbc88 | ||
![]() |
fa9777e529 | ||
![]() |
77213507fb | ||
![]() |
bfec85c352 | ||
![]() |
f3d3d40c75 | ||
![]() |
5bf38d804e | ||
![]() |
9dec9c5a18 | ||
![]() |
43b5d4e22f | ||
![]() |
fe19e0ef26 | ||
![]() |
c0af297f48 | ||
![]() |
c97e34aa04 | ||
![]() |
01ee045beb | ||
![]() |
cf6f83c8a2 | ||
![]() |
4deaf4fb76 |
6
.github/workflows/build.yml
vendored
@@ -29,7 +29,7 @@ on:
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.17"
|
||||
GO_VERSION: "1.19"
|
||||
JOB_TRANSFER_ARTIFACT: build-artifacts
|
||||
CHANGELOG_ARTIFACTS: changelog
|
||||
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
@@ -235,7 +235,7 @@ jobs:
|
||||
echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Publish Release [GitHub]
|
||||
uses: svenstaro/upload-release-action@2.4.1
|
||||
uses: svenstaro/upload-release-action@2.5.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release_name: ${{ steps.tag_name.outputs.TAG_NAME }}
|
||||
|
4
.github/workflows/check-i18n-task.yml
vendored
@@ -2,7 +2,7 @@ name: Check Internationalization
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.17"
|
||||
GO_VERSION: "1.19"
|
||||
|
||||
# See: https://docs.github.com/en/actions/reference/events-that-trigger-workflows
|
||||
on:
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
|
4
.github/workflows/i18n-nightly-push.yml
vendored
@@ -2,7 +2,7 @@ name: i18n-nightly-push
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.17"
|
||||
GO_VERSION: "1.19"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
|
4
.github/workflows/i18n-weekly-pull.yml
vendored
@@ -2,7 +2,7 @@ name: i18n-weekly-pull
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.17"
|
||||
GO_VERSION: "1.19"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
|
4
.github/workflows/themes-weekly-pull.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
env:
|
||||
# See vars.GO_VERSION field of https://github.com/arduino/arduino-cli/blob/master/DistTasks.yml
|
||||
GO_VERSION: "1.17"
|
||||
GO_VERSION: "1.19"
|
||||
NODE_VERSION: 16.x
|
||||
|
||||
jobs:
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "arduino-ide-extension",
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.5",
|
||||
"description": "An extension for Theia building the Arduino IDE",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
@@ -53,12 +53,10 @@
|
||||
"@types/js-yaml": "^3.12.2",
|
||||
"@types/keytar": "^4.4.0",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/ncp": "^2.0.4",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/p-queue": "^2.3.1",
|
||||
"@types/ps-tree": "^1.1.0",
|
||||
"@types/react-tabs": "^2.3.2",
|
||||
"@types/react-virtualized": "^9.21.21",
|
||||
"@types/temp": "^0.8.34",
|
||||
"@types/which": "^1.3.1",
|
||||
"@vscode/debugprotocol": "^1.51.0",
|
||||
@@ -67,6 +65,7 @@
|
||||
"auth0-js": "^9.14.0",
|
||||
"btoa": "^1.2.1",
|
||||
"classnames": "^2.3.1",
|
||||
"cpy": "^8.1.2",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"dateformat": "^3.0.3",
|
||||
"deepmerge": "2.0.1",
|
||||
@@ -77,13 +76,14 @@
|
||||
"glob": "^7.1.6",
|
||||
"google-protobuf": "^3.20.1",
|
||||
"hash.js": "^1.1.7",
|
||||
"is-online": "^9.0.1",
|
||||
"js-yaml": "^3.13.1",
|
||||
"jsonc-parser": "^2.2.0",
|
||||
"just-diff": "^5.1.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"keytar": "7.2.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"minimatch": "^3.1.2",
|
||||
"ncp": "^2.0.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"open": "^8.0.6",
|
||||
"p-debounce": "^2.1.0",
|
||||
@@ -95,7 +95,6 @@
|
||||
"react-perfect-scrollbar": "^1.5.8",
|
||||
"react-select": "^5.6.0",
|
||||
"react-tabs": "^3.1.2",
|
||||
"react-virtualized": "^9.22.3",
|
||||
"react-window": "^1.8.6",
|
||||
"semver": "^7.3.2",
|
||||
"string-natural-compare": "^2.0.3",
|
||||
@@ -121,6 +120,7 @@
|
||||
"mocha": "^7.0.0",
|
||||
"mockdate": "^3.0.5",
|
||||
"moment": "^2.24.0",
|
||||
"ncp": "^2.0.0",
|
||||
"protoc": "^1.0.4",
|
||||
"shelljs": "^0.8.3",
|
||||
"uuid": "^3.2.1",
|
||||
@@ -163,7 +163,11 @@
|
||||
],
|
||||
"arduino": {
|
||||
"cli": {
|
||||
"version": "0.29.0"
|
||||
"version": {
|
||||
"owner": "arduino",
|
||||
"repo": "arduino-cli",
|
||||
"commitish": "71a8576"
|
||||
}
|
||||
},
|
||||
"fwuploader": {
|
||||
"version": "2.2.2"
|
||||
|
@@ -10,10 +10,7 @@ import {
|
||||
MenuContribution,
|
||||
MenuModelRegistry,
|
||||
} from '@theia/core';
|
||||
import {
|
||||
FrontendApplication,
|
||||
FrontendApplicationContribution,
|
||||
} from '@theia/core/lib/browser';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
|
||||
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
@@ -77,7 +74,7 @@ export class ArduinoFrontendContribution
|
||||
}
|
||||
}
|
||||
|
||||
onStart(app: FrontendApplication): void {
|
||||
onStart(): void {
|
||||
this.electronWindowPreferences.onPreferenceChanged((event) => {
|
||||
if (event.newValue !== event.oldValue) {
|
||||
switch (event.preferenceName) {
|
||||
@@ -98,8 +95,6 @@ export class ArduinoFrontendContribution
|
||||
webContents.setZoomLevel(zoomLevel);
|
||||
})
|
||||
);
|
||||
// Removes the _Settings_ (cog) icon from the left sidebar
|
||||
app.shell.leftPanelHandler.removeBottomMenu('settings-menu');
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
|
@@ -9,7 +9,10 @@ import {
|
||||
FrontendApplicationContribution,
|
||||
FrontendApplication as TheiaFrontendApplication,
|
||||
} from '@theia/core/lib/browser/frontend-application';
|
||||
import { LibraryListWidget } from './library/library-list-widget';
|
||||
import {
|
||||
LibraryListWidget,
|
||||
LibraryListWidgetSearchOptions,
|
||||
} from './library/library-list-widget';
|
||||
import { ArduinoFrontendContribution } from './arduino-frontend-contribution';
|
||||
import {
|
||||
LibraryService,
|
||||
@@ -25,7 +28,10 @@ import {
|
||||
} from '../common/protocol/sketches-service';
|
||||
import { SketchesServiceClientImpl } from './sketches-service-client-impl';
|
||||
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
|
||||
import { BoardsListWidget } from './boards/boards-list-widget';
|
||||
import {
|
||||
BoardsListWidget,
|
||||
BoardsListWidgetSearchOptions,
|
||||
} from './boards/boards-list-widget';
|
||||
import { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
|
||||
import { BoardsServiceProvider } from './boards/boards-service-provider';
|
||||
import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
@@ -73,13 +79,19 @@ import {
|
||||
} from '../common/protocol/config-service';
|
||||
import { MonitorWidget } from './serial/monitor/monitor-widget';
|
||||
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
|
||||
import { TabBarDecoratorService as TheiaTabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
|
||||
import {
|
||||
TabBarDecorator,
|
||||
TabBarDecoratorService as TheiaTabBarDecoratorService,
|
||||
} from '@theia/core/lib/browser/shell/tab-bar-decorator';
|
||||
import { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
|
||||
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
|
||||
import { ProblemManager } from './theia/markers/problem-manager';
|
||||
import { BoardsAutoInstaller } from './boards/boards-auto-installer';
|
||||
import { ShellLayoutRestorer } from './theia/core/shell-layout-restorer';
|
||||
import { ListItemRenderer } from './widgets/component-list/list-item-renderer';
|
||||
import {
|
||||
ArduinoComponentContextMenuRenderer,
|
||||
ListItemRenderer,
|
||||
} from './widgets/component-list/list-item-renderer';
|
||||
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
|
||||
|
||||
import {
|
||||
@@ -90,6 +102,8 @@ import { EditorCommandContribution as TheiaEditorCommandContribution } from '@th
|
||||
import {
|
||||
FrontendConnectionStatusService,
|
||||
ApplicationConnectionStatusContribution,
|
||||
DaemonPort,
|
||||
IsOnline,
|
||||
} from './theia/core/connection-status-service';
|
||||
import {
|
||||
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
|
||||
@@ -308,10 +322,10 @@ import { PreferencesEditorWidget } from './theia/preferences/preference-editor-w
|
||||
import { PreferencesWidget } from '@theia/preferences/lib/browser/views/preference-widget';
|
||||
import { createPreferencesWidgetContainer } from '@theia/preferences/lib/browser/views/preference-widget-bindings';
|
||||
import {
|
||||
BoardsFilterRenderer,
|
||||
LibraryFilterRenderer,
|
||||
} from './widgets/component-list/filter-renderer';
|
||||
import { CheckForUpdates } from './contributions/check-for-updates';
|
||||
CheckForUpdates,
|
||||
BoardsUpdates,
|
||||
LibraryUpdates,
|
||||
} from './contributions/check-for-updates';
|
||||
import { OutputEditorFactory } from './theia/output/output-editor-factory';
|
||||
import { StartupTaskProvider } from '../electron-common/startup-task';
|
||||
import { DeleteSketch } from './contributions/delete-sketch';
|
||||
@@ -347,6 +361,15 @@ import { ConfigServiceClient } from './config/config-service-client';
|
||||
import { ValidateSketch } from './contributions/validate-sketch';
|
||||
import { RenameCloudSketch } from './contributions/rename-cloud-sketch';
|
||||
import { CreateFeatures } from './create/create-features';
|
||||
import { Account } from './contributions/account';
|
||||
import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget';
|
||||
import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget';
|
||||
import { CreateCloudCopy } from './contributions/create-cloud-copy';
|
||||
import {
|
||||
BoardsListWidgetTabBarDecorator,
|
||||
LibraryListWidgetTabBarDecorator,
|
||||
} from './widgets/component-list/list-widget-tabbar-decorator';
|
||||
import { HoverService } from './theia/core/hover-service';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
// Commands and toolbar items
|
||||
@@ -362,8 +385,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
|
||||
// Renderer for both the library and the core widgets.
|
||||
bind(ListItemRenderer).toSelf().inSingletonScope();
|
||||
bind(LibraryFilterRenderer).toSelf().inSingletonScope();
|
||||
bind(BoardsFilterRenderer).toSelf().inSingletonScope();
|
||||
|
||||
// Library service
|
||||
bind(LibraryService)
|
||||
@@ -386,6 +407,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
LibraryListWidgetFrontendContribution
|
||||
);
|
||||
bind(OpenHandler).toService(LibraryListWidgetFrontendContribution);
|
||||
bind(TabBarToolbarContribution).toService(
|
||||
LibraryListWidgetFrontendContribution
|
||||
);
|
||||
bind(CommandContribution).toService(LibraryListWidgetFrontendContribution);
|
||||
bind(LibraryListWidgetSearchOptions).toSelf().inSingletonScope();
|
||||
|
||||
// Sketch list service
|
||||
bind(SketchesService)
|
||||
@@ -455,6 +481,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
BoardsListWidgetFrontendContribution
|
||||
);
|
||||
bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
|
||||
bind(TabBarToolbarContribution).toService(
|
||||
BoardsListWidgetFrontendContribution
|
||||
);
|
||||
bind(CommandContribution).toService(BoardsListWidgetFrontendContribution);
|
||||
bind(BoardsListWidgetSearchOptions).toSelf().inSingletonScope();
|
||||
|
||||
// Board select dialog
|
||||
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
|
||||
@@ -734,6 +765,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
Contribution.configure(bind, NewCloudSketch);
|
||||
Contribution.configure(bind, ValidateSketch);
|
||||
Contribution.configure(bind, RenameCloudSketch);
|
||||
Contribution.configure(bind, Account);
|
||||
Contribution.configure(bind, CloudSketchbookContribution);
|
||||
Contribution.configure(bind, CreateCloudCopy);
|
||||
|
||||
bindContributionProvider(bind, StartupTaskProvider);
|
||||
bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window
|
||||
@@ -912,8 +946,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(CreateFsProvider).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(CreateFsProvider);
|
||||
bind(FileServiceContribution).toService(CreateFsProvider);
|
||||
bind(CloudSketchbookContribution).toSelf().inSingletonScope();
|
||||
bind(CommandContribution).toService(CloudSketchbookContribution);
|
||||
bind(LocalCacheFsProvider).toSelf().inSingletonScope();
|
||||
bind(FileServiceContribution).toService(LocalCacheFsProvider);
|
||||
bind(CloudSketchbookCompositeWidget).toSelf();
|
||||
@@ -1014,4 +1046,30 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
},
|
||||
}))
|
||||
.inSingletonScope();
|
||||
|
||||
bind(SidebarBottomMenuWidget).toSelf();
|
||||
rebind(TheiaSidebarBottomMenuWidget).toService(SidebarBottomMenuWidget);
|
||||
|
||||
bind(ArduinoComponentContextMenuRenderer).toSelf().inSingletonScope();
|
||||
|
||||
bind(DaemonPort).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(DaemonPort);
|
||||
bind(IsOnline).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(IsOnline);
|
||||
|
||||
bind(HoverService).toSelf().inSingletonScope();
|
||||
bind(LibraryUpdates).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(LibraryUpdates);
|
||||
bind(LibraryListWidgetTabBarDecorator).toSelf().inSingletonScope();
|
||||
bind(TabBarDecorator).toService(LibraryListWidgetTabBarDecorator);
|
||||
bind(FrontendApplicationContribution).toService(
|
||||
LibraryListWidgetTabBarDecorator
|
||||
);
|
||||
bind(BoardsUpdates).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(BoardsUpdates);
|
||||
bind(BoardsListWidgetTabBarDecorator).toSelf().inSingletonScope();
|
||||
bind(TabBarDecorator).toService(BoardsListWidgetTabBarDecorator);
|
||||
bind(FrontendApplicationContribution).toService(
|
||||
BoardsListWidgetTabBarDecorator
|
||||
);
|
||||
});
|
||||
|
@@ -83,9 +83,13 @@ export class AuthenticationClientService
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(CloudUserCommands.LOGIN, {
|
||||
execute: () => this.service.login(),
|
||||
isEnabled: () => !this._session,
|
||||
isVisible: () => !this._session,
|
||||
});
|
||||
registry.registerCommand(CloudUserCommands.LOGOUT, {
|
||||
execute: () => this.service.logout(),
|
||||
isEnabled: () => !!this._session,
|
||||
isVisible: () => !!this._session,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import { Command } from '@theia/core/lib/common/command';
|
||||
|
||||
export const LEARN_MORE_URL =
|
||||
'https://docs.arduino.cc/software/ide-v2/tutorials/ide-v2-cloud-sketch-sync';
|
||||
|
||||
export namespace CloudUserCommands {
|
||||
export const LOGIN = Command.toLocalizedCommand(
|
||||
{
|
||||
@@ -16,9 +19,4 @@ export namespace CloudUserCommands {
|
||||
},
|
||||
'arduino/cloud/signOut'
|
||||
);
|
||||
|
||||
export const OPEN_PROFILE_CONTEXT_MENU: Command = {
|
||||
id: 'arduino-cloud-sketchbook--open-profile-menu',
|
||||
label: 'Contextual menu',
|
||||
};
|
||||
}
|
||||
|
@@ -174,7 +174,7 @@ export class BoardsAutoInstaller implements FrontendApplicationContribution {
|
||||
// CLI returns the packages already sorted with the deprecated ones at the end of the list
|
||||
// in order to ensure the new ones are preferred
|
||||
const candidates = packagesForBoard.filter(
|
||||
({ installable, installedVersion }) => installable && !installedVersion
|
||||
({ installedVersion }) => !installedVersion
|
||||
);
|
||||
|
||||
return candidates[0];
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
@@ -8,10 +9,18 @@ import {
|
||||
BoardsPackage,
|
||||
BoardsService,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { ListWidget } from '../widgets/component-list/list-widget';
|
||||
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { BoardsFilterRenderer } from '../widgets/component-list/filter-renderer';
|
||||
import {
|
||||
ListWidget,
|
||||
ListWidgetSearchOptions,
|
||||
} from '../widgets/component-list/list-widget';
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidgetSearchOptions extends ListWidgetSearchOptions<BoardSearch> {
|
||||
get defaultOptions(): Required<BoardSearch> {
|
||||
return { query: '', type: 'All' };
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
|
||||
@@ -21,7 +30,8 @@ export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
|
||||
constructor(
|
||||
@inject(BoardsService) service: BoardsService,
|
||||
@inject(ListItemRenderer) itemRenderer: ListItemRenderer<BoardsPackage>,
|
||||
@inject(BoardsFilterRenderer) filterRenderer: BoardsFilterRenderer
|
||||
@inject(BoardsListWidgetSearchOptions)
|
||||
searchOptions: BoardsListWidgetSearchOptions
|
||||
) {
|
||||
super({
|
||||
id: BoardsListWidget.WIDGET_ID,
|
||||
@@ -31,8 +41,7 @@ export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
|
||||
installable: service,
|
||||
itemLabel: (item: BoardsPackage) => item.name,
|
||||
itemRenderer,
|
||||
filterRenderer,
|
||||
defaultSearchOptions: { query: '', type: 'All' },
|
||||
searchOptions,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -158,15 +158,9 @@ export class BoardsServiceProvider
|
||||
this.lastAvailablePortsOnUpload = undefined;
|
||||
}
|
||||
|
||||
private portToAutoSelectCanBeDerived(): boolean {
|
||||
return Boolean(
|
||||
this.lastBoardsConfigOnUpload && this.lastAvailablePortsOnUpload
|
||||
);
|
||||
}
|
||||
|
||||
attemptPostUploadAutoSelect(): void {
|
||||
setTimeout(() => {
|
||||
if (this.portToAutoSelectCanBeDerived()) {
|
||||
if (this.lastBoardsConfigOnUpload && this.lastAvailablePortsOnUpload) {
|
||||
this.attemptAutoSelect({
|
||||
ports: this._availablePorts,
|
||||
boards: this._availableBoards,
|
||||
@@ -185,12 +179,12 @@ export class BoardsServiceProvider
|
||||
private deriveBoardConfigToAutoSelect(
|
||||
newState: AttachedBoardsChangeEvent['newState']
|
||||
): void {
|
||||
if (!this.portToAutoSelectCanBeDerived()) {
|
||||
if (!this.lastBoardsConfigOnUpload || !this.lastAvailablePortsOnUpload) {
|
||||
this.boardConfigToAutoSelect = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const oldPorts = this.lastAvailablePortsOnUpload!;
|
||||
const oldPorts = this.lastAvailablePortsOnUpload;
|
||||
const { ports: newPorts, boards: newBoards } = newState;
|
||||
|
||||
const appearedPorts =
|
||||
@@ -205,20 +199,39 @@ export class BoardsServiceProvider
|
||||
Port.sameAs(board.port, port)
|
||||
);
|
||||
|
||||
const lastBoardsConfigOnUpload = this.lastBoardsConfigOnUpload!;
|
||||
const lastBoardsConfigOnUpload = this.lastBoardsConfigOnUpload;
|
||||
|
||||
if (
|
||||
boardOnAppearedPort &&
|
||||
lastBoardsConfigOnUpload.selectedBoard &&
|
||||
Board.sameAs(
|
||||
if (boardOnAppearedPort && lastBoardsConfigOnUpload.selectedBoard) {
|
||||
const boardIsSameHardware = Board.hardwareIdEquals(
|
||||
boardOnAppearedPort,
|
||||
lastBoardsConfigOnUpload.selectedBoard
|
||||
)
|
||||
) {
|
||||
);
|
||||
|
||||
const boardIsSameFqbn = Board.sameAs(
|
||||
boardOnAppearedPort,
|
||||
lastBoardsConfigOnUpload.selectedBoard
|
||||
);
|
||||
|
||||
if (!boardIsSameHardware && !boardIsSameFqbn) continue;
|
||||
|
||||
let boardToAutoSelect = boardOnAppearedPort;
|
||||
if (boardIsSameHardware && !boardIsSameFqbn) {
|
||||
const { name, fqbn } = lastBoardsConfigOnUpload.selectedBoard;
|
||||
|
||||
boardToAutoSelect = {
|
||||
...boardToAutoSelect,
|
||||
name:
|
||||
boardToAutoSelect.name === Unknown || !boardToAutoSelect.name
|
||||
? name
|
||||
: boardToAutoSelect.name,
|
||||
fqbn: boardToAutoSelect.fqbn || fqbn,
|
||||
};
|
||||
}
|
||||
|
||||
this.clearBoardDiscoverySnapshot();
|
||||
|
||||
this.boardConfigToAutoSelect = {
|
||||
selectedBoard: boardOnAppearedPort,
|
||||
selectedBoard: boardToAutoSelect,
|
||||
selectedPort: port,
|
||||
};
|
||||
return;
|
||||
@@ -326,8 +339,10 @@ export class BoardsServiceProvider
|
||||
// it is just a FQBN, so we need to find the `selected` board among the `AvailableBoards`
|
||||
const selectedAvailableBoard = AvailableBoard.is(selectedBoard)
|
||||
? selectedBoard
|
||||
: this._availableBoards.find((availableBoard) =>
|
||||
Board.sameAs(availableBoard, selectedBoard)
|
||||
: this._availableBoards.find(
|
||||
(availableBoard) =>
|
||||
Board.hardwareIdEquals(availableBoard, selectedBoard) ||
|
||||
Board.sameAs(availableBoard, selectedBoard)
|
||||
);
|
||||
if (
|
||||
selectedAvailableBoard &&
|
||||
@@ -353,9 +368,28 @@ export class BoardsServiceProvider
|
||||
|
||||
protected tryReconnect(): boolean {
|
||||
if (this.latestValidBoardsConfig && !this.canUploadTo(this.boardsConfig)) {
|
||||
// ** Reconnect to a board unplugged from, and plugged back into the same port
|
||||
for (const board of this.availableBoards.filter(
|
||||
({ state }) => state !== AvailableBoard.State.incomplete
|
||||
)) {
|
||||
if (
|
||||
Board.hardwareIdEquals(
|
||||
this.latestValidBoardsConfig.selectedBoard,
|
||||
board
|
||||
)
|
||||
) {
|
||||
const { name, fqbn } = this.latestValidBoardsConfig.selectedBoard;
|
||||
this.boardsConfig = {
|
||||
selectedBoard: {
|
||||
name: board.name === Unknown || !board.name ? name : board.name,
|
||||
fqbn: board.fqbn || fqbn,
|
||||
port: board.port,
|
||||
},
|
||||
selectedPort: board.port,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
this.latestValidBoardsConfig.selectedBoard.fqbn === board.fqbn &&
|
||||
this.latestValidBoardsConfig.selectedBoard.name === board.name &&
|
||||
@@ -365,12 +399,15 @@ export class BoardsServiceProvider
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// **
|
||||
|
||||
// ** Reconnect to a board whose port changed due to an upload
|
||||
if (!this.boardConfigToAutoSelect) return false;
|
||||
|
||||
this.boardsConfig = this.boardConfigToAutoSelect;
|
||||
this.boardConfigToAutoSelect = undefined;
|
||||
return true;
|
||||
// **
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@@ -1,17 +1,28 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { MenuPath } from '@theia/core';
|
||||
import { Command } from '@theia/core/lib/common/command';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Type as TypeLabel } from '../../common/nls';
|
||||
import {
|
||||
BoardSearch,
|
||||
BoardsPackage,
|
||||
} from '../../common/protocol/boards-service';
|
||||
import { URI } from '../contributions/contribution';
|
||||
import { MenuActionTemplate, SubmenuTemplate } from '../menu/register-menu';
|
||||
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
||||
import { BoardsListWidget } from './boards-list-widget';
|
||||
import {
|
||||
BoardsListWidget,
|
||||
BoardsListWidgetSearchOptions,
|
||||
} from './boards-list-widget';
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
||||
BoardsPackage,
|
||||
BoardSearch
|
||||
> {
|
||||
@inject(BoardsListWidgetSearchOptions)
|
||||
protected readonly searchOptions: BoardsListWidgetSearchOptions;
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: BoardsListWidget.WIDGET_ID,
|
||||
@@ -37,4 +48,51 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
|
||||
protected parse(uri: URI): BoardSearch | undefined {
|
||||
return BoardSearch.UriParser.parse(uri);
|
||||
}
|
||||
|
||||
protected buildFilterMenuGroup(
|
||||
menuPath: MenuPath
|
||||
): Array<MenuActionTemplate | SubmenuTemplate> {
|
||||
const typeSubmenuPath = [...menuPath, TypeLabel];
|
||||
return [
|
||||
{
|
||||
submenuPath: typeSubmenuPath,
|
||||
menuLabel: `${TypeLabel}: "${
|
||||
BoardSearch.TypeLabels[this.searchOptions.options.type]
|
||||
}"`,
|
||||
options: { order: String(0) },
|
||||
},
|
||||
...this.buildMenuActions<BoardSearch.Type>(
|
||||
typeSubmenuPath,
|
||||
BoardSearch.TypeLiterals.slice(),
|
||||
(type) => this.searchOptions.options.type === type,
|
||||
(type) => this.searchOptions.update({ type }),
|
||||
(type) => BoardSearch.TypeLabels[type]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
protected get showViewFilterContextMenuCommand(): Command & {
|
||||
label: string;
|
||||
} {
|
||||
return BoardsListWidgetFrontendContribution.Commands
|
||||
.SHOW_BOARDS_LIST_WIDGET_FILTER_CONTEXT_MENU;
|
||||
}
|
||||
|
||||
protected get showInstalledCommandId(): string {
|
||||
return 'arduino-show-installed-boards';
|
||||
}
|
||||
|
||||
protected get showUpdatesCommandId(): string {
|
||||
return 'arduino-show-boards-updates';
|
||||
}
|
||||
}
|
||||
export namespace BoardsListWidgetFrontendContribution {
|
||||
export namespace Commands {
|
||||
export const SHOW_BOARDS_LIST_WIDGET_FILTER_CONTEXT_MENU: Command & {
|
||||
label: string;
|
||||
} = {
|
||||
id: 'arduino-boards-list-widget-show-filter-context-menu',
|
||||
label: nls.localize('arduino/boards/filterBoards', 'Filter Boards...'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
155
arduino-ide-extension/src/browser/contributions/account.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { SidebarMenu } from '@theia/core/lib/browser/shell/sidebar-menu-widget';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { MenuPath } from '@theia/core/lib/common/menu';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CloudUserCommands, LEARN_MORE_URL } from '../auth/cloud-user-commands';
|
||||
import { CreateFeatures } from '../create/create-features';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
|
||||
import {
|
||||
Command,
|
||||
CommandRegistry,
|
||||
Contribution,
|
||||
MenuModelRegistry,
|
||||
} from './contribution';
|
||||
|
||||
export const accountMenu: SidebarMenu = {
|
||||
id: 'arduino-accounts-menu',
|
||||
iconClass: 'codicon codicon-account',
|
||||
title: nls.localize('arduino/account/menuTitle', 'Arduino Cloud'),
|
||||
menuPath: ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT,
|
||||
order: 0,
|
||||
};
|
||||
|
||||
@injectable()
|
||||
export class Account extends Contribution {
|
||||
@inject(WindowService)
|
||||
private readonly windowService: WindowService;
|
||||
@inject(CreateFeatures)
|
||||
private readonly createFeatures: CreateFeatures;
|
||||
@inject(ApplicationConnectionStatusContribution)
|
||||
private readonly connectionStatus: ApplicationConnectionStatusContribution;
|
||||
|
||||
private readonly toDispose = new DisposableCollection();
|
||||
private app: FrontendApplication;
|
||||
|
||||
override onStart(app: FrontendApplication): void {
|
||||
this.app = app;
|
||||
this.updateSidebarCommand();
|
||||
this.toDispose.push(
|
||||
this.createFeatures.onDidChangeEnabled((enabled) =>
|
||||
this.updateSidebarCommand(enabled)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
const openExternal = (url: string) =>
|
||||
this.windowService.openNewWindow(url, { external: true });
|
||||
const loggedIn = () => Boolean(this.createFeatures.session);
|
||||
const loggedInWithInternetConnection = () =>
|
||||
loggedIn() && this.connectionStatus.offlineStatus !== 'internet';
|
||||
registry.registerCommand(Account.Commands.LEARN_MORE, {
|
||||
execute: () => openExternal(LEARN_MORE_URL),
|
||||
isEnabled: () => !loggedIn(),
|
||||
isVisible: () => !loggedIn(),
|
||||
});
|
||||
registry.registerCommand(Account.Commands.GO_TO_PROFILE, {
|
||||
execute: () => openExternal('https://id.arduino.cc/'),
|
||||
isEnabled: () => loggedInWithInternetConnection(),
|
||||
isVisible: () => loggedIn(),
|
||||
});
|
||||
registry.registerCommand(Account.Commands.GO_TO_CLOUD_EDITOR, {
|
||||
execute: () => openExternal('https://create.arduino.cc/editor'),
|
||||
isEnabled: () => loggedInWithInternetConnection(),
|
||||
isVisible: () => loggedIn(),
|
||||
});
|
||||
registry.registerCommand(Account.Commands.GO_TO_IOT_CLOUD, {
|
||||
execute: () => openExternal('https://create.arduino.cc/iot/'),
|
||||
isEnabled: () => loggedInWithInternetConnection(),
|
||||
isVisible: () => loggedIn(),
|
||||
});
|
||||
}
|
||||
|
||||
override registerMenus(registry: MenuModelRegistry): void {
|
||||
const register = (
|
||||
menuPath: MenuPath,
|
||||
...commands: (Command | [command: Command, menuLabel: string])[]
|
||||
) =>
|
||||
commands.forEach((command, index) => {
|
||||
const commandId = Array.isArray(command) ? command[0].id : command.id;
|
||||
const label = Array.isArray(command) ? command[1] : command.label;
|
||||
registry.registerMenuAction(menuPath, {
|
||||
label,
|
||||
commandId,
|
||||
order: String(index),
|
||||
});
|
||||
});
|
||||
|
||||
register(ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__SIGN_IN_GROUP, [
|
||||
CloudUserCommands.LOGIN,
|
||||
nls.localize('arduino/cloud/signInToCloud', 'Sign in to Arduino Cloud'),
|
||||
]);
|
||||
register(ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__LEARN_MORE_GROUP, [
|
||||
Account.Commands.LEARN_MORE,
|
||||
nls.localize('arduino/cloud/learnMore', 'Learn more'),
|
||||
]);
|
||||
register(
|
||||
ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__GO_TO_GROUP,
|
||||
[
|
||||
Account.Commands.GO_TO_PROFILE,
|
||||
nls.localize('arduino/account/goToProfile', 'Go to Profile'),
|
||||
],
|
||||
[
|
||||
Account.Commands.GO_TO_CLOUD_EDITOR,
|
||||
nls.localize('arduino/account/goToCloudEditor', 'Go to Cloud Editor'),
|
||||
],
|
||||
[
|
||||
Account.Commands.GO_TO_IOT_CLOUD,
|
||||
nls.localize('arduino/account/goToIoTCloud', 'Go to IoT Cloud'),
|
||||
]
|
||||
);
|
||||
register(
|
||||
ArduinoMenus.ARDUINO_ACCOUNT__CONTEXT__SIGN_OUT_GROUP,
|
||||
CloudUserCommands.LOGOUT
|
||||
);
|
||||
}
|
||||
|
||||
private updateSidebarCommand(
|
||||
visible: boolean = this.preferences['arduino.cloud.enabled']
|
||||
): void {
|
||||
if (!this.app) {
|
||||
return;
|
||||
}
|
||||
const handler = this.app.shell.leftPanelHandler;
|
||||
if (visible) {
|
||||
handler.addBottomMenu(accountMenu);
|
||||
} else {
|
||||
handler.removeBottomMenu(accountMenu.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Account {
|
||||
export namespace Commands {
|
||||
export const GO_TO_PROFILE: Command = {
|
||||
id: 'arduino-go-to-profile',
|
||||
};
|
||||
export const GO_TO_CLOUD_EDITOR: Command = {
|
||||
id: 'arduino-go-to-cloud-editor',
|
||||
};
|
||||
export const GO_TO_IOT_CLOUD: Command = {
|
||||
id: 'arduino-go-to-iot-cloud',
|
||||
};
|
||||
export const LEARN_MORE: Command = {
|
||||
id: 'arduino-learn-more',
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,45 +1,55 @@
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
||||
import type { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { InstallManually, Later } from '../../common/nls';
|
||||
import {
|
||||
ArduinoComponent,
|
||||
BoardSearch,
|
||||
BoardsPackage,
|
||||
BoardsService,
|
||||
LibraryPackage,
|
||||
LibrarySearch,
|
||||
LibraryService,
|
||||
ResponseServiceClient,
|
||||
Searchable,
|
||||
Updatable,
|
||||
} from '../../common/protocol';
|
||||
import { Installable } from '../../common/protocol/installable';
|
||||
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
||||
import { BoardsListWidgetFrontendContribution } from '../boards/boards-widget-frontend-contribution';
|
||||
import { LibraryListWidgetFrontendContribution } from '../library/library-widget-frontend-contribution';
|
||||
import { NotificationCenter } from '../notification-center';
|
||||
import { WindowServiceExt } from '../theia/core/window-service-ext';
|
||||
import type { ListWidget } from '../widgets/component-list/list-widget';
|
||||
import { Command, CommandRegistry, Contribution } from './contribution';
|
||||
import { Emitter } from '@theia/core';
|
||||
import debounce = require('lodash.debounce');
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
|
||||
const NoUpdates = nls.localize(
|
||||
const noUpdates = nls.localize(
|
||||
'arduino/checkForUpdates/noUpdates',
|
||||
'There are no recent updates available.'
|
||||
);
|
||||
const PromptUpdateBoards = nls.localize(
|
||||
const promptUpdateBoards = nls.localize(
|
||||
'arduino/checkForUpdates/promptUpdateBoards',
|
||||
'Updates are available for some of your boards.'
|
||||
);
|
||||
const PromptUpdateLibraries = nls.localize(
|
||||
const promptUpdateLibraries = nls.localize(
|
||||
'arduino/checkForUpdates/promptUpdateLibraries',
|
||||
'Updates are available for some of your libraries.'
|
||||
);
|
||||
const UpdatingBoards = nls.localize(
|
||||
const updatingBoards = nls.localize(
|
||||
'arduino/checkForUpdates/updatingBoards',
|
||||
'Updating boards...'
|
||||
);
|
||||
const UpdatingLibraries = nls.localize(
|
||||
const updatingLibraries = nls.localize(
|
||||
'arduino/checkForUpdates/updatingLibraries',
|
||||
'Updating libraries...'
|
||||
);
|
||||
const InstallAll = nls.localize(
|
||||
const installAll = nls.localize(
|
||||
'arduino/checkForUpdates/installAll',
|
||||
'Install All'
|
||||
);
|
||||
@@ -49,7 +59,24 @@ interface Task<T extends ArduinoComponent> {
|
||||
readonly item: T;
|
||||
}
|
||||
|
||||
const Updatable = { type: 'Updatable' } as const;
|
||||
const updatableLibrariesSearchOption: LibrarySearch = {
|
||||
query: '',
|
||||
topic: 'All',
|
||||
...Updatable,
|
||||
};
|
||||
const updatableBoardsSearchOption: BoardSearch = {
|
||||
query: '',
|
||||
...Updatable,
|
||||
};
|
||||
const installedLibrariesSearchOptions: LibrarySearch = {
|
||||
query: '',
|
||||
topic: 'All',
|
||||
type: 'Installed',
|
||||
};
|
||||
const installedBoardsSearchOptions: BoardSearch = {
|
||||
query: '',
|
||||
type: 'Installed',
|
||||
};
|
||||
|
||||
@injectable()
|
||||
export class CheckForUpdates extends Contribution {
|
||||
@@ -70,6 +97,37 @@ export class CheckForUpdates extends Contribution {
|
||||
register.registerCommand(CheckForUpdates.Commands.CHECK_FOR_UPDATES, {
|
||||
execute: () => this.checkForUpdates(false),
|
||||
});
|
||||
register.registerCommand(CheckForUpdates.Commands.SHOW_BOARDS_UPDATES, {
|
||||
execute: () =>
|
||||
this.showUpdatableItems(
|
||||
this.boardsContribution,
|
||||
updatableBoardsSearchOption
|
||||
),
|
||||
});
|
||||
register.registerCommand(CheckForUpdates.Commands.SHOW_LIBRARY_UPDATES, {
|
||||
execute: () =>
|
||||
this.showUpdatableItems(
|
||||
this.librariesContribution,
|
||||
updatableLibrariesSearchOption
|
||||
),
|
||||
});
|
||||
register.registerCommand(CheckForUpdates.Commands.SHOW_INSTALLED_BOARDS, {
|
||||
execute: () =>
|
||||
this.showUpdatableItems(
|
||||
this.boardsContribution,
|
||||
installedBoardsSearchOptions
|
||||
),
|
||||
});
|
||||
register.registerCommand(
|
||||
CheckForUpdates.Commands.SHOW_INSTALLED_LIBRARIES,
|
||||
{
|
||||
execute: () =>
|
||||
this.showUpdatableItems(
|
||||
this.librariesContribution,
|
||||
installedLibrariesSearchOptions
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
override async onReady(): Promise<void> {
|
||||
@@ -85,13 +143,13 @@ export class CheckForUpdates extends Contribution {
|
||||
|
||||
private async checkForUpdates(silent = true) {
|
||||
const [boardsPackages, libraryPackages] = await Promise.all([
|
||||
this.boardsService.search(Updatable),
|
||||
this.libraryService.search(Updatable),
|
||||
this.boardsService.search(updatableBoardsSearchOption),
|
||||
this.libraryService.search(updatableLibrariesSearchOption),
|
||||
]);
|
||||
this.promptUpdateBoards(boardsPackages);
|
||||
this.promptUpdateLibraries(libraryPackages);
|
||||
if (!libraryPackages.length && !boardsPackages.length && !silent) {
|
||||
this.messageService.info(NoUpdates);
|
||||
this.messageService.info(noUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,9 +158,9 @@ export class CheckForUpdates extends Contribution {
|
||||
items,
|
||||
installable: this.boardsService,
|
||||
viewContribution: this.boardsContribution,
|
||||
viewSearchOptions: { query: '', ...Updatable },
|
||||
promptMessage: PromptUpdateBoards,
|
||||
updatingMessage: UpdatingBoards,
|
||||
viewSearchOptions: updatableBoardsSearchOption,
|
||||
promptMessage: promptUpdateBoards,
|
||||
updatingMessage: updatingBoards,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -111,9 +169,9 @@ export class CheckForUpdates extends Contribution {
|
||||
items,
|
||||
installable: this.libraryService,
|
||||
viewContribution: this.librariesContribution,
|
||||
viewSearchOptions: { query: '', topic: 'All', ...Updatable },
|
||||
promptMessage: PromptUpdateLibraries,
|
||||
updatingMessage: UpdatingLibraries,
|
||||
viewSearchOptions: updatableLibrariesSearchOption,
|
||||
promptMessage: promptUpdateLibraries,
|
||||
updatingMessage: updatingLibraries,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -141,21 +199,30 @@ export class CheckForUpdates extends Contribution {
|
||||
return;
|
||||
}
|
||||
this.messageService
|
||||
.info(message, Later, InstallManually, InstallAll)
|
||||
.info(message, Later, InstallManually, installAll)
|
||||
.then((answer) => {
|
||||
if (answer === InstallAll) {
|
||||
if (answer === installAll) {
|
||||
const tasks = items.map((item) =>
|
||||
this.createInstallTask(item, installable)
|
||||
);
|
||||
this.executeTasks(updatingMessage, tasks);
|
||||
return this.executeTasks(updatingMessage, tasks);
|
||||
} else if (answer === InstallManually) {
|
||||
viewContribution
|
||||
.openView({ reveal: true })
|
||||
.then((widget) => widget.refresh(viewSearchOptions));
|
||||
return this.showUpdatableItems(viewContribution, viewSearchOptions);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async showUpdatableItems<
|
||||
T extends ArduinoComponent,
|
||||
S extends Searchable.Options
|
||||
>(
|
||||
viewContribution: AbstractViewContribution<ListWidget<T, S>>,
|
||||
viewSearchOptions: S
|
||||
): Promise<void> {
|
||||
const widget = await viewContribution.openView({ reveal: true });
|
||||
widget.refresh(viewSearchOptions);
|
||||
}
|
||||
|
||||
private async executeTasks(
|
||||
message: string,
|
||||
tasks: Task<ArduinoComponent>[]
|
||||
@@ -217,5 +284,127 @@ export namespace CheckForUpdates {
|
||||
},
|
||||
'arduino/checkForUpdates/checkForUpdates'
|
||||
);
|
||||
export const SHOW_BOARDS_UPDATES: Command & { label: string } = {
|
||||
id: 'arduino-show-boards-updates',
|
||||
label: nls.localize(
|
||||
'arduino/checkForUpdates/showBoardsUpdates',
|
||||
'Boards Updates'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const SHOW_LIBRARY_UPDATES: Command & { label: string } = {
|
||||
id: 'arduino-show-library-updates',
|
||||
label: nls.localize(
|
||||
'arduino/checkForUpdates/showLibraryUpdates',
|
||||
'Library Updates'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const SHOW_INSTALLED_BOARDS: Command & { label: string } = {
|
||||
id: 'arduino-show-installed-boards',
|
||||
label: nls.localize(
|
||||
'arduino/checkForUpdates/showInstalledBoards',
|
||||
'Installed Boards'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
export const SHOW_INSTALLED_LIBRARIES: Command & { label: string } = {
|
||||
id: 'arduino-show-installed-libraries',
|
||||
label: nls.localize(
|
||||
'arduino/checkForUpdates/showInstalledLibraries',
|
||||
'Installed Libraries'
|
||||
),
|
||||
category: 'Arduino',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
abstract class ComponentUpdates<T extends ArduinoComponent>
|
||||
implements FrontendApplicationContribution
|
||||
{
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
@inject(ArduinoPreferences)
|
||||
private readonly preferences: ArduinoPreferences;
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
private _updates: T[] | undefined;
|
||||
private readonly onDidChangeEmitter = new Emitter<T[]>();
|
||||
protected readonly toDispose = new DisposableCollection(
|
||||
this.onDidChangeEmitter
|
||||
);
|
||||
|
||||
readonly onDidChange = this.onDidChangeEmitter.event;
|
||||
readonly refresh = debounce(() => this.refreshDebounced(), 200);
|
||||
|
||||
onStart(): void {
|
||||
this.appStateService.reachedState('ready').then(() => this.refresh());
|
||||
this.toDispose.push(
|
||||
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
|
||||
if (
|
||||
preferenceName === 'arduino.checkForUpdates' &&
|
||||
typeof newValue === 'boolean'
|
||||
) {
|
||||
this.refresh();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
get updates(): T[] | undefined {
|
||||
return this._updates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search updatable components (libraries and platforms) via the CLI.
|
||||
*/
|
||||
abstract searchUpdates(): Promise<T[]>;
|
||||
|
||||
private async refreshDebounced(): Promise<void> {
|
||||
const checkForUpdates = this.preferences['arduino.checkForUpdates'];
|
||||
this._updates = checkForUpdates ? await this.searchUpdates() : [];
|
||||
this.onDidChangeEmitter.fire(this._updates.slice());
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class LibraryUpdates extends ComponentUpdates<LibraryPackage> {
|
||||
@inject(LibraryService)
|
||||
private readonly libraryService: LibraryService;
|
||||
|
||||
override onStart(): void {
|
||||
super.onStart();
|
||||
this.toDispose.pushAll([
|
||||
this.notificationCenter.onLibraryDidInstall(() => this.refresh()),
|
||||
this.notificationCenter.onLibraryDidUninstall(() => this.refresh()),
|
||||
]);
|
||||
}
|
||||
|
||||
override searchUpdates(): Promise<LibraryPackage[]> {
|
||||
return this.libraryService.search(updatableLibrariesSearchOption);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class BoardsUpdates extends ComponentUpdates<BoardsPackage> {
|
||||
@inject(BoardsService)
|
||||
private readonly boardsService: BoardsService;
|
||||
|
||||
override onStart(): void {
|
||||
super.onStart();
|
||||
this.toDispose.pushAll([
|
||||
this.notificationCenter.onPlatformDidInstall(() => this.refresh()),
|
||||
this.notificationCenter.onPlatformDidUninstall(() => this.refresh()),
|
||||
this.notificationCenter.onIndexUpdateDidComplete(() => this.refresh()),
|
||||
]);
|
||||
}
|
||||
|
||||
override searchUpdates(): Promise<BoardsPackage[]> {
|
||||
return this.boardsService.search(updatableBoardsSearchOption);
|
||||
}
|
||||
}
|
||||
|
@@ -93,7 +93,7 @@ export abstract class CloudSketchContribution extends SketchContribution {
|
||||
);
|
||||
}
|
||||
try {
|
||||
await treeModel.sketchbookTree().pull({ node });
|
||||
await treeModel.sketchbookTree().pull({ node }, true);
|
||||
return node;
|
||||
} catch (err) {
|
||||
if (isNotFound(err)) {
|
||||
|
@@ -14,7 +14,6 @@ import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
|
||||
|
||||
import {
|
||||
MenuModelRegistry,
|
||||
MenuContribution,
|
||||
@@ -58,7 +57,7 @@ import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
||||
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { BoardsDataStore } from '../boards/boards-data-store';
|
||||
import { NotificationManager } from '../theia/messages/notifications-manager';
|
||||
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
|
||||
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { WorkspaceService } from '../theia/workspace/workspace-service';
|
||||
import { MainMenuManager } from '../../common/main-menu-manager';
|
||||
@@ -295,7 +294,7 @@ export abstract class CoreServiceContribution extends SketchContribution {
|
||||
}
|
||||
|
||||
private notificationId(message: string, ...actions: string[]): string {
|
||||
return this.notificationManager.getMessageId({
|
||||
return this.notificationManager['getMessageId']({
|
||||
text: message,
|
||||
actions,
|
||||
type: MessageType.Error,
|
||||
|
@@ -0,0 +1,118 @@
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell';
|
||||
import type { Command, CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { Progress } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Create } from '../create/typings';
|
||||
import { ApplicationConnectionStatusContribution } from '../theia/core/connection-status-service';
|
||||
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
|
||||
import { SketchbookTree } from '../widgets/sketchbook/sketchbook-tree';
|
||||
import { SketchbookTreeModel } from '../widgets/sketchbook/sketchbook-tree-model';
|
||||
import { CloudSketchContribution, pushingSketch } from './cloud-contribution';
|
||||
import {
|
||||
CreateNewCloudSketchCallback,
|
||||
NewCloudSketch,
|
||||
NewCloudSketchParams,
|
||||
} from './new-cloud-sketch';
|
||||
import { saveOntoCopiedSketch } from './save-as-sketch';
|
||||
|
||||
interface CreateCloudCopyParams {
|
||||
readonly model: SketchbookTreeModel;
|
||||
readonly node: SketchbookTree.SketchDirNode;
|
||||
}
|
||||
function isCreateCloudCopyParams(arg: unknown): arg is CreateCloudCopyParams {
|
||||
return (
|
||||
typeof arg === 'object' &&
|
||||
(<CreateCloudCopyParams>arg).model !== undefined &&
|
||||
(<CreateCloudCopyParams>arg).model instanceof SketchbookTreeModel &&
|
||||
(<CreateCloudCopyParams>arg).node !== undefined &&
|
||||
SketchbookTree.SketchDirNode.is((<CreateCloudCopyParams>arg).node)
|
||||
);
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class CreateCloudCopy extends CloudSketchContribution {
|
||||
@inject(ApplicationConnectionStatusContribution)
|
||||
private readonly connectionStatus: ApplicationConnectionStatusContribution;
|
||||
|
||||
private shell: ApplicationShell;
|
||||
|
||||
override onStart(app: FrontendApplication): void {
|
||||
this.shell = app.shell;
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(CreateCloudCopy.Commands.CREATE_CLOUD_COPY, {
|
||||
execute: (args: CreateCloudCopyParams) => this.createCloudCopy(args),
|
||||
isEnabled: (args: unknown) =>
|
||||
Boolean(this.createFeatures.session) && isCreateCloudCopyParams(args),
|
||||
isVisible: (args: unknown) =>
|
||||
Boolean(this.createFeatures.enabled) &&
|
||||
Boolean(this.createFeatures.session) &&
|
||||
this.connectionStatus.offlineStatus !== 'internet' &&
|
||||
isCreateCloudCopyParams(args),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* - creates new cloud sketch with the name of the params sketch,
|
||||
* - pulls the cloud sketch,
|
||||
* - copies files from params sketch to pulled cloud sketch in the cache folder,
|
||||
* - pushes the cloud sketch, and
|
||||
* - opens in new window.
|
||||
*/
|
||||
private async createCloudCopy(params: CreateCloudCopyParams): Promise<void> {
|
||||
const sketch = await this.sketchesService.loadSketch(
|
||||
params.node.fileStat.resource.toString()
|
||||
);
|
||||
const callback: CreateNewCloudSketchCallback = async (
|
||||
newSketch: Create.Sketch,
|
||||
newNode: CloudSketchbookTree.CloudSketchDirNode,
|
||||
progress: Progress
|
||||
) => {
|
||||
const treeModel = await this.treeModel();
|
||||
if (!treeModel) {
|
||||
throw new Error('Could not retrieve the cloud sketchbook tree model.');
|
||||
}
|
||||
|
||||
progress.report({
|
||||
message: nls.localize(
|
||||
'arduino/createCloudCopy/copyingSketchFilesMessage',
|
||||
'Copying local sketch files...'
|
||||
),
|
||||
});
|
||||
const localCacheFolderUri = newNode.uri.toString();
|
||||
await this.sketchesService.copy(sketch, {
|
||||
destinationUri: localCacheFolderUri,
|
||||
onlySketchFiles: true,
|
||||
});
|
||||
await saveOntoCopiedSketch(
|
||||
sketch,
|
||||
localCacheFolderUri,
|
||||
this.shell,
|
||||
this.editorManager
|
||||
);
|
||||
|
||||
progress.report({ message: pushingSketch(newSketch.name) });
|
||||
await treeModel.sketchbookTree().push(newNode, true, true);
|
||||
};
|
||||
return this.commandService.executeCommand(
|
||||
NewCloudSketch.Commands.NEW_CLOUD_SKETCH.id,
|
||||
<NewCloudSketchParams>{
|
||||
initialValue: params.node.fileStat.name,
|
||||
callback,
|
||||
skipShowErrorMessageOnOpen: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace CreateCloudCopy {
|
||||
export namespace Commands {
|
||||
export const CREATE_CLOUD_COPY: Command = {
|
||||
id: 'arduino-create-cloud-copy',
|
||||
iconClass: 'fa fa-arduino-cloud-upload',
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
import * as PQueue from 'p-queue';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CommandHandler } from '@theia/core/lib/common/command';
|
||||
import { CommandHandler, CommandService } from '@theia/core/lib/common/command';
|
||||
import {
|
||||
MenuPath,
|
||||
CompositeMenuNode,
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { OpenSketch } from './open-sketch';
|
||||
import { ArduinoMenus, PlaceholderMenuNode } from '../menu/arduino-menus';
|
||||
import {
|
||||
ArduinoMenus,
|
||||
examplesLabel,
|
||||
PlaceholderMenuNode,
|
||||
} from '../menu/arduino-menus';
|
||||
import { BoardsServiceProvider } from '../boards/boards-service-provider';
|
||||
import { ExamplesService } from '../../common/protocol/examples-service';
|
||||
import {
|
||||
@@ -25,11 +29,73 @@ import {
|
||||
SketchRef,
|
||||
SketchContainer,
|
||||
SketchesError,
|
||||
Sketch,
|
||||
CoreService,
|
||||
SketchesService,
|
||||
Sketch,
|
||||
} from '../../common/protocol';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { unregisterSubmenu } from '../menu/arduino-menus';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { ApplicationError } from '@theia/core/lib/common/application-error';
|
||||
|
||||
/**
|
||||
* Creates a cloned copy of the example sketch and opens it in a new window.
|
||||
*/
|
||||
export async function openClonedExample(
|
||||
uri: string,
|
||||
services: {
|
||||
sketchesService: SketchesService;
|
||||
commandService: CommandService;
|
||||
},
|
||||
onError: {
|
||||
onDidFailClone?: (
|
||||
err: ApplicationError<
|
||||
number,
|
||||
{
|
||||
uri: string;
|
||||
}
|
||||
>,
|
||||
uri: string
|
||||
) => MaybePromise<unknown>;
|
||||
onDidFailOpen?: (
|
||||
err: ApplicationError<
|
||||
number,
|
||||
{
|
||||
uri: string;
|
||||
}
|
||||
>,
|
||||
sketch: Sketch
|
||||
) => MaybePromise<unknown>;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
const { sketchesService, commandService } = services;
|
||||
const { onDidFailClone, onDidFailOpen } = onError;
|
||||
try {
|
||||
const sketch = await sketchesService.cloneExample(uri);
|
||||
try {
|
||||
await commandService.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
);
|
||||
} catch (openError) {
|
||||
if (SketchesError.NotFound.is(openError)) {
|
||||
if (onDidFailOpen) {
|
||||
await onDidFailOpen(openError, sketch);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw openError;
|
||||
}
|
||||
} catch (cloneError) {
|
||||
if (SketchesError.NotFound.is(cloneError)) {
|
||||
if (onDidFailClone) {
|
||||
await onDidFailClone(cloneError, uri);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw cloneError;
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export abstract class Examples extends SketchContribution {
|
||||
@@ -94,7 +160,7 @@ export abstract class Examples extends SketchContribution {
|
||||
// TODO: unregister submenu? https://github.com/eclipse-theia/theia/issues/7300
|
||||
registry.registerSubmenu(
|
||||
ArduinoMenus.FILE__EXAMPLES_SUBMENU,
|
||||
nls.localize('arduino/examples/menu', 'Examples'),
|
||||
examplesLabel,
|
||||
{
|
||||
order: '4',
|
||||
}
|
||||
@@ -174,47 +240,33 @@ export abstract class Examples extends SketchContribution {
|
||||
}
|
||||
|
||||
protected createHandler(uri: string): CommandHandler {
|
||||
const forceUpdate = () =>
|
||||
this.update({
|
||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||
forceRefresh: true,
|
||||
});
|
||||
return {
|
||||
execute: async () => {
|
||||
const sketch = await this.clone(uri);
|
||||
if (sketch) {
|
||||
try {
|
||||
return this.commandService.executeCommand(
|
||||
OpenSketch.Commands.OPEN_SKETCH.id,
|
||||
sketch
|
||||
);
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
await openClonedExample(
|
||||
uri,
|
||||
{
|
||||
sketchesService: this.sketchesService,
|
||||
commandService: this.commandRegistry,
|
||||
},
|
||||
{
|
||||
onDidFailClone: () => {
|
||||
// Do not toast the error message. It's handled by the `Open Sketch` command.
|
||||
this.update({
|
||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||
forceRefresh: true,
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
forceUpdate();
|
||||
},
|
||||
onDidFailOpen: (err) => {
|
||||
this.messageService.error(err.message);
|
||||
forceUpdate();
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async clone(uri: string): Promise<Sketch | undefined> {
|
||||
try {
|
||||
const sketch = await this.sketchesService.cloneExample(uri);
|
||||
return sketch;
|
||||
} catch (err) {
|
||||
if (SketchesError.NotFound.is(err)) {
|
||||
this.messageService.error(err.message);
|
||||
this.update({
|
||||
board: this.boardsServiceClient.boardsConfig.selectedBoard,
|
||||
forceRefresh: true,
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
|
@@ -6,7 +6,7 @@ import { Progress } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { CreateUri } from '../create/create-uri';
|
||||
import { isConflict } from '../create/typings';
|
||||
import { Create, isConflict } from '../create/typings';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import {
|
||||
TaskFactoryImpl,
|
||||
@@ -15,13 +15,36 @@ import {
|
||||
import { CloudSketchbookTree } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree';
|
||||
import { CloudSketchbookTreeModel } from '../widgets/cloud-sketchbook/cloud-sketchbook-tree-model';
|
||||
import { SketchbookCommands } from '../widgets/sketchbook/sketchbook-commands';
|
||||
import { Command, CommandRegistry, Sketch } from './contribution';
|
||||
import {
|
||||
CloudSketchContribution,
|
||||
pullingSketch,
|
||||
sketchAlreadyExists,
|
||||
synchronizingSketchbook,
|
||||
} from './cloud-contribution';
|
||||
import { Command, CommandRegistry, Sketch } from './contribution';
|
||||
|
||||
export interface CreateNewCloudSketchCallback {
|
||||
(
|
||||
newSketch: Create.Sketch,
|
||||
newNode: CloudSketchbookTree.CloudSketchDirNode,
|
||||
progress: Progress
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export interface NewCloudSketchParams {
|
||||
/**
|
||||
* Value to populate the dialog `<input>` when it opens.
|
||||
*/
|
||||
readonly initialValue?: string | undefined;
|
||||
/**
|
||||
* Additional callback to call when the new cloud sketch has been created.
|
||||
*/
|
||||
readonly callback?: CreateNewCloudSketchCallback;
|
||||
/**
|
||||
* If `true`, the validation error message will not be visible in the input dialog, but the `OK` button will be disabled. Defaults to `true`.
|
||||
*/
|
||||
readonly skipShowErrorMessageOnOpen?: boolean;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class NewCloudSketch extends CloudSketchContribution {
|
||||
@@ -43,7 +66,12 @@ export class NewCloudSketch extends CloudSketchContribution {
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(NewCloudSketch.Commands.NEW_CLOUD_SKETCH, {
|
||||
execute: () => this.createNewSketch(true),
|
||||
execute: (params: NewCloudSketchParams) =>
|
||||
this.createNewSketch(
|
||||
params?.skipShowErrorMessageOnOpen === false ? false : true,
|
||||
params?.initialValue,
|
||||
params?.callback
|
||||
),
|
||||
isEnabled: () => Boolean(this.createFeatures.session),
|
||||
isVisible: () => this.createFeatures.enabled,
|
||||
});
|
||||
@@ -66,7 +94,8 @@ export class NewCloudSketch extends CloudSketchContribution {
|
||||
|
||||
private async createNewSketch(
|
||||
skipShowErrorMessageOnOpen: boolean,
|
||||
initialValue?: string | undefined
|
||||
initialValue?: string | undefined,
|
||||
callback?: CreateNewCloudSketchCallback
|
||||
): Promise<void> {
|
||||
const treeModel = await this.treeModel();
|
||||
if (treeModel) {
|
||||
@@ -75,7 +104,8 @@ export class NewCloudSketch extends CloudSketchContribution {
|
||||
rootNode,
|
||||
treeModel,
|
||||
skipShowErrorMessageOnOpen,
|
||||
initialValue
|
||||
initialValue,
|
||||
callback
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -84,13 +114,14 @@ export class NewCloudSketch extends CloudSketchContribution {
|
||||
rootNode: CompositeTreeNode,
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
skipShowErrorMessageOnOpen: boolean,
|
||||
initialValue?: string | undefined
|
||||
initialValue?: string | undefined,
|
||||
callback?: CreateNewCloudSketchCallback
|
||||
): Promise<void> {
|
||||
const existingNames = rootNode.children
|
||||
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
|
||||
.map(({ fileStat }) => fileStat.name);
|
||||
const taskFactory = new TaskFactoryImpl((value) =>
|
||||
this.createNewSketchWithProgress(treeModel, value)
|
||||
this.createNewSketchWithProgress(treeModel, value, callback)
|
||||
);
|
||||
try {
|
||||
const dialog = new WorkspaceInputDialogWithProgress(
|
||||
@@ -118,7 +149,11 @@ export class NewCloudSketch extends CloudSketchContribution {
|
||||
} catch (err) {
|
||||
if (isConflict(err)) {
|
||||
await treeModel.refresh();
|
||||
return this.createNewSketch(false, taskFactory.value ?? initialValue);
|
||||
return this.createNewSketch(
|
||||
false,
|
||||
taskFactory.value ?? initialValue,
|
||||
callback
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
@@ -126,7 +161,8 @@ export class NewCloudSketch extends CloudSketchContribution {
|
||||
|
||||
private createNewSketchWithProgress(
|
||||
treeModel: CloudSketchbookTreeModel,
|
||||
value: string
|
||||
value: string,
|
||||
callback?: CreateNewCloudSketchCallback
|
||||
): (
|
||||
progress: Progress
|
||||
) => Promise<CloudSketchbookTree.CloudSketchDirNode | undefined> {
|
||||
@@ -143,6 +179,9 @@ export class NewCloudSketch extends CloudSketchContribution {
|
||||
await treeModel.refresh();
|
||||
progress.report({ message: pullingSketch(sketch.name) });
|
||||
const node = await this.pull(sketch);
|
||||
if (callback && node) {
|
||||
await callback(sketch, node, progress);
|
||||
}
|
||||
return node;
|
||||
};
|
||||
}
|
||||
@@ -152,7 +191,7 @@ export class NewCloudSketch extends CloudSketchContribution {
|
||||
): Promise<void> {
|
||||
return this.commandService.executeCommand(
|
||||
SketchbookCommands.OPEN_NEW_WINDOW.id,
|
||||
{ node }
|
||||
{ node, treeWidgetId: 'cloud-sketchbook-composite-widget' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -123,7 +123,7 @@ export class RenameCloudSketch extends CloudSketchContribution {
|
||||
const toPosixPath = params.cloudUri.parent.resolve(value).path.toString();
|
||||
// push
|
||||
progress.report({ message: pushingSketch(params.sketch.name) });
|
||||
await treeModel.sketchbookTree().push(node);
|
||||
await treeModel.sketchbookTree().push(node, true);
|
||||
|
||||
// rename
|
||||
progress.report({
|
||||
|
@@ -6,6 +6,7 @@ import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shel
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { WorkspaceInput } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { StartupTask } from '../../electron-common/startup-task';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
@@ -28,7 +29,7 @@ import {
|
||||
@injectable()
|
||||
export class SaveAsSketch extends CloudSketchContribution {
|
||||
@inject(ApplicationShell)
|
||||
private readonly applicationShell: ApplicationShell;
|
||||
private readonly shell: ApplicationShell;
|
||||
@inject(WindowService)
|
||||
private readonly windowService: WindowService;
|
||||
|
||||
@@ -80,14 +81,17 @@ export class SaveAsSketch extends CloudSketchContribution {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newWorkspaceUri = await this.sketchesService.copy(sketch, {
|
||||
const copiedSketch = await this.sketchesService.copy(sketch, {
|
||||
destinationUri,
|
||||
});
|
||||
if (!newWorkspaceUri) {
|
||||
return false;
|
||||
}
|
||||
const newWorkspaceUri = copiedSketch.uri;
|
||||
|
||||
await this.saveOntoCopiedSketch(sketch, newWorkspaceUri);
|
||||
await saveOntoCopiedSketch(
|
||||
sketch,
|
||||
newWorkspaceUri,
|
||||
this.shell,
|
||||
this.editorManager
|
||||
);
|
||||
if (markAsRecentlyOpened) {
|
||||
this.sketchesService.markAsRecentlyOpened(newWorkspaceUri);
|
||||
}
|
||||
@@ -238,53 +242,6 @@ ${dialogContent.question}`.trim();
|
||||
}
|
||||
return sketchFolderDestinationUri;
|
||||
}
|
||||
|
||||
private async saveOntoCopiedSketch(
|
||||
sketch: Sketch,
|
||||
newSketchFolderUri: string
|
||||
): Promise<void> {
|
||||
const widgets = this.applicationShell.widgets;
|
||||
const snapshots = new Map<string, Saveable.Snapshot>();
|
||||
for (const widget of widgets) {
|
||||
const saveable = Saveable.getDirty(widget);
|
||||
const uri = NavigatableWidget.getUri(widget);
|
||||
if (!uri) {
|
||||
continue;
|
||||
}
|
||||
const uriString = uri.toString();
|
||||
let relativePath: string;
|
||||
if (
|
||||
uriString.includes(sketch.uri) &&
|
||||
saveable &&
|
||||
saveable.createSnapshot
|
||||
) {
|
||||
// The main file will change its name during the copy process
|
||||
// We need to store the new name in the map
|
||||
if (sketch.mainFileUri === uriString) {
|
||||
const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext;
|
||||
relativePath = '/' + lastPart;
|
||||
} else {
|
||||
relativePath = uri.toString().substring(sketch.uri.length);
|
||||
}
|
||||
snapshots.set(relativePath, saveable.createSnapshot());
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
|
||||
const widgetUri = new URI(newSketchFolderUri + path);
|
||||
try {
|
||||
const widget = await this.editorManager.getOrCreateByUri(widgetUri);
|
||||
const saveable = Saveable.get(widget);
|
||||
if (saveable && saveable.applySnapshot) {
|
||||
saveable.applySnapshot(snapshot);
|
||||
await saveable.save();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface InvalidSketchFolderDialogContent {
|
||||
@@ -317,3 +274,48 @@ export namespace SaveAsSketch {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveOntoCopiedSketch(
|
||||
sketch: Sketch,
|
||||
newSketchFolderUri: string,
|
||||
shell: ApplicationShell,
|
||||
editorManager: EditorManager
|
||||
): Promise<void> {
|
||||
const widgets = shell.widgets;
|
||||
const snapshots = new Map<string, Saveable.Snapshot>();
|
||||
for (const widget of widgets) {
|
||||
const saveable = Saveable.getDirty(widget);
|
||||
const uri = NavigatableWidget.getUri(widget);
|
||||
if (!uri) {
|
||||
continue;
|
||||
}
|
||||
const uriString = uri.toString();
|
||||
let relativePath: string;
|
||||
if (uriString.includes(sketch.uri) && saveable && saveable.createSnapshot) {
|
||||
// The main file will change its name during the copy process
|
||||
// We need to store the new name in the map
|
||||
if (sketch.mainFileUri === uriString) {
|
||||
const lastPart = new URI(newSketchFolderUri).path.base + uri.path.ext;
|
||||
relativePath = '/' + lastPart;
|
||||
} else {
|
||||
relativePath = uri.toString().substring(sketch.uri.length);
|
||||
}
|
||||
snapshots.set(relativePath, saveable.createSnapshot());
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
Array.from(snapshots.entries()).map(async ([path, snapshot]) => {
|
||||
const widgetUri = new URI(newSketchFolderUri + path);
|
||||
try {
|
||||
const widget = await editorManager.getOrCreateByUri(widgetUri);
|
||||
const saveable = Saveable.get(widget);
|
||||
if (saveable && saveable.applySnapshot) {
|
||||
saveable.applySnapshot(snapshot);
|
||||
await saveable.save();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@@ -8,7 +8,10 @@ import {
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import {
|
||||
ArduinoMenus,
|
||||
showDisabledContextMenuOptions,
|
||||
} from '../menu/arduino-menus';
|
||||
import { CurrentSketch } from '../sketches-service-client-impl';
|
||||
import {
|
||||
Command,
|
||||
@@ -119,7 +122,7 @@ export class SketchControl extends SketchContribution {
|
||||
)
|
||||
);
|
||||
}
|
||||
const options = {
|
||||
const options = showDisabledContextMenuOptions({
|
||||
menuPath: ArduinoMenus.SKETCH_CONTROL__CONTEXT,
|
||||
anchor: {
|
||||
x: parentElement.getBoundingClientRect().left,
|
||||
@@ -127,8 +130,7 @@ export class SketchControl extends SketchContribution {
|
||||
parentElement.getBoundingClientRect().top +
|
||||
parentElement.offsetHeight,
|
||||
},
|
||||
showDisabled: true,
|
||||
};
|
||||
});
|
||||
this.contextMenuRenderer.render(options);
|
||||
},
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { fetch } from 'cross-fetch';
|
||||
import { SketchesService } from '../../common/protocol';
|
||||
import { uint8ArrayToString } from '../../common/utils';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||
import { SketchCache } from '../widgets/cloud-sketchbook/cloud-sketch-cache';
|
||||
@@ -9,59 +10,16 @@ import * as createPaths from './create-paths';
|
||||
import { posix } from './create-paths';
|
||||
import { Create, CreateError } from './typings';
|
||||
|
||||
export interface ResponseResultProvider {
|
||||
interface ResponseResultProvider {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(response: Response): Promise<any>;
|
||||
}
|
||||
export namespace ResponseResultProvider {
|
||||
namespace ResponseResultProvider {
|
||||
export const NOOP: ResponseResultProvider = async () => undefined;
|
||||
export const TEXT: ResponseResultProvider = (response) => response.text();
|
||||
export const JSON: ResponseResultProvider = (response) => response.json();
|
||||
}
|
||||
|
||||
// TODO: check if this is still needed: https://github.com/electron/electron/issues/18733
|
||||
// The original issue was reported for Electron 5.x and 6.x. Theia uses 15.x
|
||||
export function Utf8ArrayToStr(array: Uint8Array): string {
|
||||
let out, i, c;
|
||||
let char2, char3;
|
||||
|
||||
out = '';
|
||||
const len = array.length;
|
||||
i = 0;
|
||||
while (i < len) {
|
||||
c = array[i++];
|
||||
switch (c >> 4) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
case 6:
|
||||
case 7:
|
||||
// 0xxxxxxx
|
||||
out += String.fromCharCode(c);
|
||||
break;
|
||||
case 12:
|
||||
case 13:
|
||||
// 110x xxxx 10xx xxxx
|
||||
char2 = array[i++];
|
||||
out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f));
|
||||
break;
|
||||
case 14:
|
||||
// 1110 xxxx 10xx xxxx 10xx xxxx
|
||||
char2 = array[i++];
|
||||
char3 = array[i++];
|
||||
out += String.fromCharCode(
|
||||
((c & 0x0f) << 12) | ((char2 & 0x3f) << 6) | ((char3 & 0x3f) << 0)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
type ResourceType = 'f' | 'd';
|
||||
|
||||
@injectable()
|
||||
@@ -330,10 +288,9 @@ export class CreateApi {
|
||||
if (sketch) {
|
||||
const url = new URL(`${this.domain()}/sketches/${sketch.id}`);
|
||||
const headers = await this.headers();
|
||||
|
||||
// parse the secret file
|
||||
const secrets = (
|
||||
typeof content === 'string' ? content : Utf8ArrayToStr(content)
|
||||
typeof content === 'string' ? content : uint8ArrayToString(content)
|
||||
)
|
||||
.split(/\r?\n/)
|
||||
.reduce((prev, curr) => {
|
||||
@@ -397,7 +354,7 @@ export class CreateApi {
|
||||
const headers = await this.headers();
|
||||
|
||||
let data: string =
|
||||
typeof content === 'string' ? content : Utf8ArrayToStr(content);
|
||||
typeof content === 'string' ? content : uint8ArrayToString(content);
|
||||
data = await this.toggleSecretsInclude(posixPath, data, 'remove');
|
||||
|
||||
const payload = { data: btoa(data) };
|
||||
|
@@ -8,6 +8,9 @@ import { AuthenticationSession } from '../../node/auth/types';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import { AuthenticationClientService } from '../auth/authentication-client-service';
|
||||
import { LocalCacheFsProvider } from '../local-cache/local-cache-fs-provider';
|
||||
import { CreateUri } from './create-uri';
|
||||
|
||||
export type CloudSketchState = 'push' | 'pull';
|
||||
|
||||
@injectable()
|
||||
export class CreateFeatures implements FrontendApplicationContribution {
|
||||
@@ -18,13 +21,22 @@ export class CreateFeatures implements FrontendApplicationContribution {
|
||||
@inject(LocalCacheFsProvider)
|
||||
private readonly localCacheFsProvider: LocalCacheFsProvider;
|
||||
|
||||
/**
|
||||
* The keys are the Create URI of the sketches.
|
||||
*/
|
||||
private readonly _cloudSketchStates = new Map<string, CloudSketchState>();
|
||||
private readonly onDidChangeSessionEmitter = new Emitter<
|
||||
AuthenticationSession | undefined
|
||||
>();
|
||||
private readonly onDidChangeEnabledEmitter = new Emitter<boolean>();
|
||||
private readonly onDidChangeCloudSketchStateEmitter = new Emitter<{
|
||||
uri: URI;
|
||||
state: CloudSketchState | undefined;
|
||||
}>();
|
||||
private readonly toDispose = new DisposableCollection(
|
||||
this.onDidChangeSessionEmitter,
|
||||
this.onDidChangeEnabledEmitter
|
||||
this.onDidChangeEnabledEmitter,
|
||||
this.onDidChangeCloudSketchStateEmitter
|
||||
);
|
||||
private _enabled: boolean;
|
||||
private _session: AuthenticationSession | undefined;
|
||||
@@ -64,14 +76,46 @@ export class CreateFeatures implements FrontendApplicationContribution {
|
||||
return this.onDidChangeEnabledEmitter.event;
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
return this._enabled;
|
||||
get onDidChangeCloudSketchState(): Event<{
|
||||
uri: URI;
|
||||
state: CloudSketchState | undefined;
|
||||
}> {
|
||||
return this.onDidChangeCloudSketchStateEmitter.event;
|
||||
}
|
||||
|
||||
get session(): AuthenticationSession | undefined {
|
||||
return this._session;
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
return this._enabled;
|
||||
}
|
||||
|
||||
cloudSketchState(uri: URI): CloudSketchState | undefined {
|
||||
return this._cloudSketchStates.get(uri.toString());
|
||||
}
|
||||
|
||||
setCloudSketchState(uri: URI, state: CloudSketchState | undefined): void {
|
||||
if (uri.scheme !== CreateUri.scheme) {
|
||||
throw new Error(
|
||||
`Expected a URI with '${uri.scheme}' scheme. Got: ${uri.toString()}`
|
||||
);
|
||||
}
|
||||
const key = uri.toString();
|
||||
if (!state) {
|
||||
if (!this._cloudSketchStates.delete(key)) {
|
||||
console.warn(
|
||||
`Could not reset the cloud sketch state of ${key}. No state existed for the the cloud sketch.`
|
||||
);
|
||||
} else {
|
||||
this.onDidChangeCloudSketchStateEmitter.fire({ uri, state: undefined });
|
||||
}
|
||||
} else {
|
||||
this._cloudSketchStates.set(key, state);
|
||||
this.onDidChangeCloudSketchStateEmitter.fire({ uri, state });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` if the sketch is under `directories.data/RemoteSketchbook`. Otherwise, `false`.
|
||||
* Returns with `undefined` if `dataDirUri` is `undefined`.
|
||||
@@ -83,7 +127,10 @@ export class CreateFeatures implements FrontendApplicationContribution {
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return dataDirUri.isEqualOrParent(new URI(sketch.uri));
|
||||
return dataDirUri
|
||||
.resolve('RemoteSketchbook')
|
||||
.resolve('ArduinoCloud')
|
||||
.isEqualOrParent(new URI(sketch.uri));
|
||||
}
|
||||
|
||||
cloudUri(sketch: Sketch): URI | undefined {
|
||||
|
@@ -29,6 +29,7 @@ import { CreateUri } from './create-uri';
|
||||
import { SketchesService } from '../../common/protocol';
|
||||
import { ArduinoPreferences } from '../arduino-preferences';
|
||||
import { Create } from './typings';
|
||||
import { stringToUint8Array } from '../../common/utils';
|
||||
|
||||
@injectable()
|
||||
export class CreateFsProvider
|
||||
@@ -154,7 +155,7 @@ export class CreateFsProvider
|
||||
|
||||
async readFile(uri: URI): Promise<Uint8Array> {
|
||||
const content = await this.getCreateApi.readFile(uri.path.toString());
|
||||
return new TextEncoder().encode(content);
|
||||
return stringToUint8Array(content);
|
||||
}
|
||||
|
||||
async writeFile(
|
||||
|
@@ -38,7 +38,8 @@
|
||||
"activityBar.foreground": "#dae3e3",
|
||||
"activityBar.inactiveForeground": "#4e5b61",
|
||||
"activityBar.activeBorder": "#0ca1a6",
|
||||
"statusBar.background": "#171e21",
|
||||
"activityBarBadge.background": "#008184",
|
||||
"statusBar.background": "#0ca1a6",
|
||||
"secondaryButton.background": "#ff000000",
|
||||
"secondaryButton.foreground": "#dae3e3",
|
||||
"secondaryButton.hoverBackground": "#ffffff1a",
|
||||
|
@@ -38,6 +38,7 @@
|
||||
"activityBar.foreground": "#4e5b61",
|
||||
"activityBar.inactiveForeground": "#bdc7c7",
|
||||
"activityBar.activeBorder": "#008184",
|
||||
"activityBarBadge.background": "#008184",
|
||||
"statusBar.background": "#006d70",
|
||||
"secondaryButton.background": "#ff000000",
|
||||
"secondaryButton.foreground": "#008184",
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.355 3.85509L2.85504 14.3551C2.76026 14.448 2.63281 14.5001 2.50006 14.5001C2.36731 14.5001 2.23986 14.448 2.14508 14.3551C2.0514 14.2607 1.99882 14.1331 1.99882 14.0001C1.99882 13.8671 2.0514 13.7395 2.14508 13.6451L3.82508 11.9651C3.24351 11.8742 2.70645 11.5991 2.29291 11.1802C1.87936 10.7613 1.61116 10.2208 1.52775 9.63811C1.44434 9.05543 1.55012 8.46136 1.82955 7.94328C2.10897 7.4252 2.54728 7.01047 3.08 6.76009C3.20492 6.18251 3.47405 5.64596 3.86232 5.20047C4.25058 4.75498 4.74532 4.41505 5.30042 4.21239C5.85552 4.00972 6.45289 3.9509 7.03686 4.04143C7.62082 4.13196 8.17236 4.36887 8.64004 4.73009C9.01346 4.56809 9.41786 4.48995 9.82475 4.50117C10.2316 4.51239 10.6311 4.6127 10.995 4.79503L12.645 3.14509C12.7392 3.05094 12.8669 2.99805 13 2.99805C13.1332 2.99805 13.2609 3.05094 13.355 3.14509C13.4492 3.23924 13.5021 3.36694 13.5021 3.50009C13.5021 3.63324 13.4492 3.76094 13.355 3.85509V3.85509Z" fill="#7F8C8D"/>
|
||||
<path d="M14.5 9.25047C14.4987 9.97942 14.2086 10.6782 13.6931 11.1936C13.1777 11.709 12.479 11.9992 11.75 12.0005H6.70996L12.355 6.35547C12.38 6.43042 12.4 6.50547 12.4201 6.58044C13.0153 6.72902 13.5436 7.07272 13.9206 7.55669C14.2976 8.04066 14.5016 8.63699 14.5 9.25047V9.25047Z" fill="#7F8C8D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.5 9.24997C14.4987 9.97893 14.2086 10.6777 13.6932 11.1931C13.1777 11.7086 12.479 11.9987 11.75 12H4.25003C3.62476 11.9998 3.01822 11.7866 2.53034 11.3955C2.04247 11.0045 1.70238 10.4589 1.56612 9.84864C1.42986 9.2384 1.50556 8.59995 1.78074 8.0385C2.05593 7.47705 2.51418 7.0261 3.07998 6.75997C3.2049 6.18239 3.47404 5.64584 3.8623 5.20035C4.25056 4.75486 4.74531 4.41494 5.3004 4.21227C5.8555 4.0096 6.45288 3.95078 7.03684 4.04131C7.62081 4.13184 8.17234 4.36875 8.64003 4.72997C8.99025 4.57772 9.36814 4.49942 9.75003 4.49997C10.3635 4.49838 10.9598 4.70238 11.4438 5.07939C11.9278 5.45641 12.2715 5.9847 12.4201 6.57993C13.0153 6.7285 13.5436 7.07221 13.9206 7.55618C14.2976 8.04015 14.5016 8.63649 14.5 9.24997Z" fill="#7F8C8D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 852 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.42 6.58044C12.4 6.50549 12.38 6.43042 12.355 6.35547L11.525 7.18555C11.5575 7.27223 11.6136 7.34811 11.6869 7.40464C11.7603 7.46117 11.8479 7.4961 11.94 7.50549C12.3852 7.55476 12.7947 7.77259 13.0843 8.11428C13.374 8.45597 13.5218 8.89557 13.4975 9.34284C13.4732 9.7901 13.2785 10.2111 12.9536 10.5194C12.6286 10.8276 12.1979 10.9998 11.75 11.0005H7.70996L6.70996 12.0005H11.75C12.421 12.0001 13.0688 11.7545 13.5714 11.3099C14.074 10.8653 14.3969 10.2524 14.4792 9.58644C14.5615 8.92048 14.3977 8.24739 14.0184 7.69379C13.6392 7.14019 13.0708 6.74425 12.42 6.58044V6.58044Z" fill="#7F8C8D"/>
|
||||
<path d="M13.355 3.14532C13.2606 3.05161 13.133 2.99902 13 2.99902C12.867 2.99902 12.7394 3.05161 12.6451 3.14532L10.995 4.79524C10.6311 4.61291 10.2316 4.5126 9.82472 4.50139C9.41783 4.49017 9.01343 4.56832 8.64002 4.73032C8.17233 4.3691 7.6208 4.13219 7.03684 4.04166C6.45287 3.95114 5.85549 4.00995 5.3004 4.21262C4.7453 4.41529 4.25056 4.75521 3.86229 5.2007C3.47403 5.64619 3.2049 6.18274 3.07997 6.76033C2.54726 7.01071 2.10896 7.42543 1.82954 7.9435C1.55013 8.46157 1.44434 9.05564 1.52775 9.63832C1.61115 10.221 1.87935 10.7615 2.29288 11.1804C2.70641 11.5993 3.24346 11.8744 3.82502 11.9653L2.14502 13.6453C2.05133 13.7397 1.99876 13.8673 1.99876 14.0003C1.99876 14.1333 2.05133 14.2609 2.14502 14.3553C2.23979 14.4482 2.36725 14.5003 2.5 14.5003C2.63275 14.5003 2.7602 14.4482 2.85498 14.3553L13.355 3.85528C13.4487 3.7609 13.5012 3.6333 13.5013 3.50031C13.5013 3.36732 13.4487 3.23972 13.355 3.14532V3.14532ZM4.79006 11.0003H4.25002C3.8356 11.0005 3.43458 10.8535 3.11841 10.5856C2.80224 10.3177 2.59145 9.94623 2.52362 9.5374C2.45578 9.12857 2.53529 8.70893 2.74799 8.35326C2.96069 7.99758 3.29275 7.72898 3.68502 7.59529C3.77434 7.56478 3.85319 7.50962 3.91248 7.43617C3.97176 7.36272 4.00904 7.274 4.02002 7.18025C4.09848 6.57783 4.39334 6.0245 4.84963 5.62341C5.30592 5.22233 5.89251 5.00087 6.50002 5.00032C7.1425 4.99652 7.76054 5.24628 8.21999 5.69539C8.30086 5.77275 8.40511 5.8211 8.5164 5.83285C8.6277 5.8446 8.73974 5.8191 8.83499 5.76033C9.10926 5.58886 9.42655 5.4987 9.75002 5.50032C9.9105 5.50127 10.0702 5.5231 10.225 5.56526L4.79006 11.0003Z" fill="#7F8C8D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.75 12H4.25C3.62484 11.9999 3.01838 11.7868 2.53053 11.3959C2.04269 11.0049 1.70257 10.4595 1.56622 9.84934C1.42987 9.23923 1.50542 8.60087 1.78043 8.03945C2.05543 7.47802 2.51348 7.02702 3.0791 6.76076C3.24864 5.97929 3.68041 5.27932 4.3027 4.77712C4.92499 4.27492 5.70035 4.00071 6.5 4.00002C7.27505 3.99715 8.02853 4.25513 8.63916 4.73244C9.00591 4.57154 9.40329 4.49243 9.8037 4.5006C10.2041 4.50877 10.5979 4.60403 10.9578 4.77976C11.3177 4.9555 11.635 5.20748 11.8876 5.51822C12.1403 5.82895 12.3223 6.19097 12.4209 6.57912C13.0715 6.74324 13.6398 7.13939 14.0188 7.69309C14.3979 8.24679 14.5616 8.91989 14.4792 9.58582C14.3967 10.2518 14.0739 10.8646 13.5713 11.3092C13.0687 11.7538 12.421 11.9995 11.75 12ZM6.5 5.00002C5.89213 5.00017 5.30514 5.22179 4.84885 5.62344C4.39257 6.02508 4.09826 6.57921 4.021 7.18215C4.0093 7.27546 3.97153 7.36357 3.91202 7.43638C3.85252 7.50918 3.77369 7.56374 3.68458 7.59377C3.29236 7.72769 2.9604 7.99647 2.7478 8.35224C2.5352 8.70801 2.45576 9.12768 2.52363 9.53654C2.5915 9.9454 2.80227 10.3169 3.11841 10.5849C3.43455 10.8529 3.83555 11 4.25 11H11.75C12.198 10.9996 12.6289 10.8275 12.9539 10.5191C13.279 10.2108 13.4735 9.7896 13.4975 9.34221C13.5215 8.89481 13.3732 8.45522 13.083 8.11384C12.7929 7.77246 12.3829 7.55524 11.9375 7.50686C11.8238 7.4948 11.7176 7.44411 11.6368 7.36325C11.5559 7.28238 11.5052 7.17624 11.4932 7.06252C11.4474 6.63255 11.2439 6.2348 10.9219 5.94619C10.6 5.65758 10.1824 5.49861 9.75 5.50002C9.42739 5.49791 9.11079 5.58731 8.83692 5.75783C8.74185 5.81746 8.62955 5.84352 8.51794 5.83184C8.40633 5.82015 8.30185 5.77141 8.22119 5.69338C7.76046 5.24569 7.14241 4.99672 6.5 5.00002V5.00002Z" fill="#7F8C8D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
@@ -1,22 +1,32 @@
|
||||
import { DialogProps } from '@theia/core/lib/browser/dialogs';
|
||||
import { addEventListener } from '@theia/core/lib/browser/widgets/widget';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
inject,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||
import { addEventListener } from '@theia/core/lib/browser/widgets/widget';
|
||||
import { DialogProps } from '@theia/core/lib/browser/dialogs';
|
||||
import { AbstractDialog } from '../theia/dialogs/dialogs';
|
||||
import { Installable } from '../../common/protocol';
|
||||
import {
|
||||
LibraryPackage,
|
||||
LibrarySearch,
|
||||
LibraryService,
|
||||
} from '../../common/protocol/library-service';
|
||||
import { ListWidget } from '../widgets/component-list/list-widget';
|
||||
import { Installable } from '../../common/protocol';
|
||||
import { AbstractDialog } from '../theia/dialogs/dialogs';
|
||||
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { LibraryFilterRenderer } from '../widgets/component-list/filter-renderer';
|
||||
import {
|
||||
ListWidget,
|
||||
ListWidgetSearchOptions,
|
||||
UserAbortError,
|
||||
} from '../widgets/component-list/list-widget';
|
||||
|
||||
@injectable()
|
||||
export class LibraryListWidgetSearchOptions extends ListWidgetSearchOptions<LibrarySearch> {
|
||||
get defaultOptions(): Required<LibrarySearch> {
|
||||
return { query: '', type: 'All', topic: 'All' };
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class LibraryListWidget extends ListWidget<
|
||||
@@ -32,7 +42,8 @@ export class LibraryListWidget extends ListWidget<
|
||||
constructor(
|
||||
@inject(LibraryService) private service: LibraryService,
|
||||
@inject(ListItemRenderer) itemRenderer: ListItemRenderer<LibraryPackage>,
|
||||
@inject(LibraryFilterRenderer) filterRenderer: LibraryFilterRenderer
|
||||
@inject(LibraryListWidgetSearchOptions)
|
||||
searchOptions: LibraryListWidgetSearchOptions
|
||||
) {
|
||||
super({
|
||||
id: LibraryListWidget.WIDGET_ID,
|
||||
@@ -42,8 +53,7 @@ export class LibraryListWidget extends ListWidget<
|
||||
installable: service,
|
||||
itemLabel: (item: LibraryPackage) => item.name,
|
||||
itemRenderer,
|
||||
filterRenderer,
|
||||
defaultSearchOptions: { query: '', type: 'All', topic: 'All' },
|
||||
searchOptions,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -141,6 +151,8 @@ export class LibraryListWidget extends ListWidget<
|
||||
// All
|
||||
installDependencies = true;
|
||||
}
|
||||
} else {
|
||||
throw new UserAbortError();
|
||||
}
|
||||
} else {
|
||||
// The lib does not have any dependencies.
|
||||
@@ -235,6 +247,21 @@ class MessageBoxDialog extends AbstractDialog<MessageBoxDialog.Result> {
|
||||
this.response = 0;
|
||||
super.handleEnter(event);
|
||||
}
|
||||
|
||||
protected override onAfterAttach(message: Message): void {
|
||||
super.onAfterAttach(message);
|
||||
let buttonToFocus: HTMLButtonElement | undefined = undefined;
|
||||
for (const child of Array.from(this.controlPanel.children)) {
|
||||
if (child instanceof HTMLButtonElement) {
|
||||
if (child.classList.contains('main')) {
|
||||
buttonToFocus = child;
|
||||
break;
|
||||
}
|
||||
buttonToFocus = child;
|
||||
}
|
||||
}
|
||||
buttonToFocus?.focus();
|
||||
}
|
||||
}
|
||||
export namespace MessageBoxDialog {
|
||||
export interface Options extends DialogProps {
|
||||
|
@@ -1,17 +1,30 @@
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { LibraryPackage, LibrarySearch } from '../../common/protocol';
|
||||
import { Command } from '@theia/core/lib/common/command';
|
||||
import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Type as TypeLabel } from '../../common/nls';
|
||||
import {
|
||||
LibraryPackage,
|
||||
LibrarySearch,
|
||||
TopicLabel,
|
||||
} from '../../common/protocol';
|
||||
import { URI } from '../contributions/contribution';
|
||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||
import { MenuActionTemplate, SubmenuTemplate } from '../menu/register-menu';
|
||||
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
||||
import { LibraryListWidget } from './library-list-widget';
|
||||
import {
|
||||
LibraryListWidget,
|
||||
LibraryListWidgetSearchOptions,
|
||||
} from './library-list-widget';
|
||||
|
||||
@injectable()
|
||||
export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
||||
LibraryPackage,
|
||||
LibrarySearch
|
||||
> {
|
||||
@inject(LibraryListWidgetSearchOptions)
|
||||
protected readonly searchOptions: LibraryListWidgetSearchOptions;
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: LibraryListWidget.WIDGET_ID,
|
||||
@@ -38,7 +51,7 @@ export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendCon
|
||||
}
|
||||
}
|
||||
|
||||
protected canParse(uri: URI): boolean {
|
||||
protected override canParse(uri: URI): boolean {
|
||||
try {
|
||||
LibrarySearch.UriParser.parse(uri);
|
||||
return true;
|
||||
@@ -47,7 +60,72 @@ export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendCon
|
||||
}
|
||||
}
|
||||
|
||||
protected parse(uri: URI): LibrarySearch | undefined {
|
||||
protected override parse(uri: URI): LibrarySearch | undefined {
|
||||
return LibrarySearch.UriParser.parse(uri);
|
||||
}
|
||||
|
||||
protected override buildFilterMenuGroup(
|
||||
menuPath: MenuPath
|
||||
): Array<MenuActionTemplate | SubmenuTemplate> {
|
||||
const typeSubmenuPath = [...menuPath, TypeLabel];
|
||||
const topicSubmenuPath = [...menuPath, TopicLabel];
|
||||
return [
|
||||
{
|
||||
submenuPath: typeSubmenuPath,
|
||||
menuLabel: `${TypeLabel}: "${
|
||||
LibrarySearch.TypeLabels[this.searchOptions.options.type]
|
||||
}"`,
|
||||
options: { order: String(0) },
|
||||
},
|
||||
...this.buildMenuActions<LibrarySearch.Type>(
|
||||
typeSubmenuPath,
|
||||
LibrarySearch.TypeLiterals.slice(),
|
||||
(type) => this.searchOptions.options.type === type,
|
||||
(type) => this.searchOptions.update({ type }),
|
||||
(type) => LibrarySearch.TypeLabels[type]
|
||||
),
|
||||
{
|
||||
submenuPath: topicSubmenuPath,
|
||||
menuLabel: `${TopicLabel}: "${
|
||||
LibrarySearch.TopicLabels[this.searchOptions.options.topic]
|
||||
}"`,
|
||||
options: { order: String(1) },
|
||||
},
|
||||
...this.buildMenuActions<LibrarySearch.Topic>(
|
||||
topicSubmenuPath,
|
||||
LibrarySearch.TopicLiterals.slice(),
|
||||
(topic) => this.searchOptions.options.topic === topic,
|
||||
(topic) => this.searchOptions.update({ topic }),
|
||||
(topic) => LibrarySearch.TopicLabels[topic]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
protected override get showViewFilterContextMenuCommand(): Command & {
|
||||
label: string;
|
||||
} {
|
||||
return LibraryListWidgetFrontendContribution.Commands
|
||||
.SHOW_LIBRARY_LIST_WIDGET_FILTER_CONTEXT_MENU;
|
||||
}
|
||||
|
||||
protected get showInstalledCommandId(): string {
|
||||
return 'arduino-show-installed-libraries';
|
||||
}
|
||||
|
||||
protected get showUpdatesCommandId(): string {
|
||||
return 'arduino-show-library-updates';
|
||||
}
|
||||
}
|
||||
export namespace LibraryListWidgetFrontendContribution {
|
||||
export namespace Commands {
|
||||
export const SHOW_LIBRARY_LIST_WIDGET_FILTER_CONTEXT_MENU: Command & {
|
||||
label: string;
|
||||
} = {
|
||||
id: 'arduino-library-list-widget-show-filter-context-menu',
|
||||
label: nls.localize(
|
||||
'arduino/libraries/filterLibraries',
|
||||
'Filter Libraries...'
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
import { RenderContextMenuOptions } from '@theia/core/lib/browser';
|
||||
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||
import {
|
||||
MAIN_MENU_BAR,
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
MenuPath,
|
||||
SubMenuOptions,
|
||||
} from '@theia/core/lib/common/menu';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
|
||||
export namespace ArduinoMenus {
|
||||
// Main menu
|
||||
@@ -154,6 +156,36 @@ export namespace ArduinoMenus {
|
||||
'2_resources',
|
||||
];
|
||||
|
||||
// -- Account
|
||||
export const ARDUINO_ACCOUNT__CONTEXT = ['arduino-account--context'];
|
||||
export const ARDUINO_ACCOUNT__CONTEXT__SIGN_IN_GROUP = [
|
||||
...ARDUINO_ACCOUNT__CONTEXT,
|
||||
'0_sign_in',
|
||||
];
|
||||
export const ARDUINO_ACCOUNT__CONTEXT__LEARN_MORE_GROUP = [
|
||||
...ARDUINO_ACCOUNT__CONTEXT,
|
||||
'1_learn_more',
|
||||
];
|
||||
export const ARDUINO_ACCOUNT__CONTEXT__GO_TO_GROUP = [
|
||||
...ARDUINO_ACCOUNT__CONTEXT,
|
||||
'2_go_to',
|
||||
];
|
||||
export const ARDUINO_ACCOUNT__CONTEXT__SIGN_OUT_GROUP = [
|
||||
...ARDUINO_ACCOUNT__CONTEXT,
|
||||
'3_sign_out',
|
||||
];
|
||||
|
||||
// Context menu from the library and boards manager widget
|
||||
export const ARDUINO_COMPONENT__CONTEXT = ['arduino-component--context'];
|
||||
export const ARDUINO_COMPONENT__CONTEXT__INFO_GROUP = [
|
||||
...ARDUINO_COMPONENT__CONTEXT,
|
||||
'0_info',
|
||||
];
|
||||
export const ARDUINO_COMPONENT__CONTEXT__ACTION_GROUP = [
|
||||
...ARDUINO_COMPONENT__CONTEXT,
|
||||
'1_action',
|
||||
];
|
||||
|
||||
// -- ROOT SSL CERTIFICATES
|
||||
export const ROOT_CERTIFICATES__CONTEXT = [
|
||||
'arduino-root-certificates--context',
|
||||
@@ -211,3 +243,15 @@ export class PlaceholderMenuNode implements MenuNode {
|
||||
return [...this.menuPath, 'placeholder'].join('-');
|
||||
}
|
||||
}
|
||||
|
||||
export const examplesLabel = nls.localize('arduino/examples/menu', 'Examples');
|
||||
|
||||
/**
|
||||
* Helper function to optionally show disabled context menu items in IDE2. They're invisible in Theia.
|
||||
* See `ElectronContextMenuRenderer#showDisabled` for more details.
|
||||
*/
|
||||
export function showDisabledContextMenuOptions(
|
||||
options: RenderContextMenuOptions
|
||||
): RenderContextMenuOptions {
|
||||
return Object.assign(options, { showDisabled: true });
|
||||
}
|
||||
|
151
arduino-ide-extension/src/browser/menu/register-menu.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import {
|
||||
CommandHandler,
|
||||
CommandRegistry,
|
||||
} from '@theia/core/lib/common/command';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import {
|
||||
MenuModelRegistry,
|
||||
MenuPath,
|
||||
SubMenuOptions,
|
||||
} from '@theia/core/lib/common/menu';
|
||||
import { unregisterSubmenu } from './arduino-menus';
|
||||
|
||||
export interface MenuTemplate {
|
||||
readonly menuLabel: string;
|
||||
}
|
||||
|
||||
export function isMenuTemplate(arg: unknown): arg is MenuTemplate {
|
||||
return (
|
||||
typeof arg === 'object' &&
|
||||
(arg as MenuTemplate).menuLabel !== undefined &&
|
||||
typeof (arg as MenuTemplate).menuLabel === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
export interface MenuActionTemplate extends MenuTemplate {
|
||||
readonly menuPath: MenuPath;
|
||||
readonly handler: CommandHandler;
|
||||
/**
|
||||
* If not defined the insertion oder will be the order string.
|
||||
*/
|
||||
readonly order?: string;
|
||||
}
|
||||
|
||||
export function isMenuActionTemplate(
|
||||
arg: MenuTemplate
|
||||
): arg is MenuActionTemplate {
|
||||
return (
|
||||
isMenuTemplate(arg) &&
|
||||
(arg as MenuActionTemplate).handler !== undefined &&
|
||||
typeof (arg as MenuActionTemplate).handler === 'object' &&
|
||||
(arg as MenuActionTemplate).menuPath !== undefined &&
|
||||
Array.isArray((arg as MenuActionTemplate).menuPath)
|
||||
);
|
||||
}
|
||||
|
||||
export function menuActionWithCommandDelegate(
|
||||
template: Omit<MenuActionTemplate, 'handler' | 'menuLabel'> & {
|
||||
command: string;
|
||||
},
|
||||
commandRegistry: CommandRegistry
|
||||
): MenuActionTemplate {
|
||||
const id = template.command;
|
||||
const command = commandRegistry.getCommand(id);
|
||||
if (!command) {
|
||||
throw new Error(`Could not find the registered command with ID: ${id}`);
|
||||
}
|
||||
return {
|
||||
...template,
|
||||
menuLabel: command.label ?? id,
|
||||
handler: {
|
||||
execute: (args) => commandRegistry.executeCommand(id, args),
|
||||
isEnabled: (args) => commandRegistry.isEnabled(id, args),
|
||||
isVisible: (args) => commandRegistry.isVisible(id, args),
|
||||
isToggled: (args) => commandRegistry.isToggled(id, args),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface SubmenuTemplate extends MenuTemplate {
|
||||
readonly menuLabel: string;
|
||||
readonly submenuPath: MenuPath;
|
||||
readonly options?: SubMenuOptions;
|
||||
}
|
||||
|
||||
interface Services {
|
||||
readonly commandRegistry: CommandRegistry;
|
||||
readonly menuRegistry: MenuModelRegistry;
|
||||
}
|
||||
|
||||
class MenuIndexCounter {
|
||||
private _counter: number;
|
||||
constructor(counter = 0) {
|
||||
this._counter = counter;
|
||||
}
|
||||
getAndIncrement(): number {
|
||||
const counter = this._counter;
|
||||
this._counter++;
|
||||
return counter;
|
||||
}
|
||||
}
|
||||
|
||||
export function registerMenus(
|
||||
options: {
|
||||
contextId: string;
|
||||
templates: Array<MenuActionTemplate | SubmenuTemplate>;
|
||||
} & Services
|
||||
): Disposable {
|
||||
const { templates } = options;
|
||||
const menuIndexCounter = new MenuIndexCounter();
|
||||
return new DisposableCollection(
|
||||
...templates.map((template) =>
|
||||
registerMenu({ template, menuIndexCounter, ...options })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function registerMenu(
|
||||
options: {
|
||||
contextId: string;
|
||||
menuIndexCounter: MenuIndexCounter;
|
||||
template: MenuActionTemplate | SubmenuTemplate;
|
||||
} & Services
|
||||
): Disposable {
|
||||
const {
|
||||
template,
|
||||
commandRegistry,
|
||||
menuRegistry,
|
||||
contextId,
|
||||
menuIndexCounter,
|
||||
} = options;
|
||||
if (isMenuActionTemplate(template)) {
|
||||
const { menuLabel, menuPath, handler, order } = template;
|
||||
const id = generateCommandId(contextId, menuLabel, menuPath);
|
||||
const index = menuIndexCounter.getAndIncrement();
|
||||
return new DisposableCollection(
|
||||
commandRegistry.registerCommand({ id }, handler),
|
||||
menuRegistry.registerMenuAction(menuPath, {
|
||||
commandId: id,
|
||||
label: menuLabel,
|
||||
order: typeof order === 'string' ? order : String(index).padStart(4),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const { menuLabel, submenuPath, options } = template;
|
||||
return new DisposableCollection(
|
||||
menuRegistry.registerSubmenu(submenuPath, menuLabel, options),
|
||||
Disposable.create(() => unregisterSubmenu(submenuPath, menuRegistry))
|
||||
);
|
||||
}
|
||||
|
||||
function generateCommandId(
|
||||
contextId: string,
|
||||
menuLabel: string,
|
||||
menuPath: MenuPath
|
||||
): string {
|
||||
return `arduino-${contextId}-context-${menuPath.join('-')}-${menuLabel}`;
|
||||
}
|
||||
}
|
@@ -117,6 +117,11 @@ export class MonitorWidget extends ReactWidget {
|
||||
(this.focusNode || this.node).focus();
|
||||
}
|
||||
|
||||
protected override onAfterShow(msg: Message): void {
|
||||
super.onAfterShow(msg);
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected onFocusResolved = (element: HTMLElement | undefined) => {
|
||||
if (this.closing || !this.isAttached) {
|
||||
return;
|
||||
|
@@ -17,7 +17,7 @@ export class SerialMonitorOutput extends React.Component<
|
||||
* Do not touch it. It is used to be able to "follow" the serial monitor log.
|
||||
*/
|
||||
protected toDisposeBeforeUnmount = new DisposableCollection();
|
||||
private listRef: React.RefObject<any>;
|
||||
private listRef: React.RefObject<List>;
|
||||
|
||||
constructor(props: Readonly<SerialMonitorOutput.Props>) {
|
||||
super(props);
|
||||
@@ -34,12 +34,10 @@ export class SerialMonitorOutput extends React.Component<
|
||||
<List
|
||||
className="serial-monitor-messages"
|
||||
height={this.props.height}
|
||||
itemData={
|
||||
{
|
||||
lines: this.state.lines,
|
||||
timestamp: this.state.timestamp,
|
||||
} as any
|
||||
}
|
||||
itemData={{
|
||||
lines: this.state.lines,
|
||||
timestamp: this.state.timestamp,
|
||||
}}
|
||||
itemCount={this.state.lines.length}
|
||||
itemSize={18}
|
||||
width={'100%'}
|
||||
@@ -65,11 +63,13 @@ export class SerialMonitorOutput extends React.Component<
|
||||
this.state.charCount
|
||||
);
|
||||
const [lines, charCount] = truncateLines(newLines, totalCharCount);
|
||||
this.setState({
|
||||
lines,
|
||||
charCount,
|
||||
});
|
||||
this.scrollToBottom();
|
||||
this.setState(
|
||||
{
|
||||
lines,
|
||||
charCount,
|
||||
},
|
||||
() => this.scrollToBottom()
|
||||
);
|
||||
}),
|
||||
this.props.clearConsoleEvent(() =>
|
||||
this.setState({ lines: [], charCount: 0 })
|
||||
@@ -91,11 +91,11 @@ export class SerialMonitorOutput extends React.Component<
|
||||
this.toDisposeBeforeUnmount.dispose();
|
||||
}
|
||||
|
||||
scrollToBottom = ((): void => {
|
||||
private readonly scrollToBottom = () => {
|
||||
if (this.listRef.current && this.props.monitorModel.autoscroll) {
|
||||
this.listRef.current.scrollToItem(this.state.lines.length, 'end');
|
||||
}
|
||||
}).bind(this);
|
||||
};
|
||||
}
|
||||
|
||||
const _Row = ({
|
||||
|
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" focusable="false" width="1em" height="1em" style="-ms-transform: rotate(360deg); -webkit-transform: rotate(360deg); transform: rotate(360deg);" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16"><g fill="#626262"><path d="M16 7.992C16 3.58 12.416 0 8 0S0 3.58 0 7.992c0 2.43 1.104 4.62 2.832 6.09c.016.016.032.016.032.032c.144.112.288.224.448.336c.08.048.144.111.224.175A7.98 7.98 0 0 0 8.016 16a7.98 7.98 0 0 0 4.48-1.375c.08-.048.144-.111.224-.16c.144-.111.304-.223.448-.335c.016-.016.032-.016.032-.032c1.696-1.487 2.8-3.676 2.8-6.106zm-8 7.001c-1.504 0-2.88-.48-4.016-1.279c.016-.128.048-.255.08-.383a4.17 4.17 0 0 1 .416-.991c.176-.304.384-.576.64-.816c.24-.24.528-.463.816-.639c.304-.176.624-.304.976-.4A4.15 4.15 0 0 1 8 10.342a4.185 4.185 0 0 1 2.928 1.166c.368.368.656.8.864 1.295c.112.288.192.592.24.911A7.03 7.03 0 0 1 8 14.993zm-2.448-7.4a2.49 2.49 0 0 1-.208-1.024c0-.351.064-.703.208-1.023c.144-.32.336-.607.576-.847c.24-.24.528-.431.848-.575c.32-.144.672-.208 1.024-.208c.368 0 .704.064 1.024.208c.32.144.608.336.848.575c.24.24.432.528.576.847c.144.32.208.672.208 1.023c0 .368-.064.704-.208 1.023a2.84 2.84 0 0 1-.576.848a2.84 2.84 0 0 1-.848.575a2.715 2.715 0 0 1-2.064 0a2.84 2.84 0 0 1-.848-.575a2.526 2.526 0 0 1-.56-.848zm7.424 5.306c0-.032-.016-.048-.016-.08a5.22 5.22 0 0 0-.688-1.406a4.883 4.883 0 0 0-1.088-1.135a5.207 5.207 0 0 0-1.04-.608a2.82 2.82 0 0 0 .464-.383a4.2 4.2 0 0 0 .624-.784a3.624 3.624 0 0 0 .528-1.934a3.71 3.71 0 0 0-.288-1.47a3.799 3.799 0 0 0-.816-1.199a3.845 3.845 0 0 0-1.2-.8a3.72 3.72 0 0 0-1.472-.287a3.72 3.72 0 0 0-1.472.288a3.631 3.631 0 0 0-1.2.815a3.84 3.84 0 0 0-.8 1.199a3.71 3.71 0 0 0-.288 1.47c0 .352.048.688.144 1.007c.096.336.224.64.4.927c.16.288.384.544.624.784c.144.144.304.271.48.383a5.12 5.12 0 0 0-1.04.624c-.416.32-.784.703-1.088 1.119a4.999 4.999 0 0 0-.688 1.406c-.016.032-.016.064-.016.08C1.776 11.636.992 9.91.992 7.992C.992 4.14 4.144.991 8 .991s7.008 3.149 7.008 7.001a6.96 6.96 0 0 1-2.032 4.907z"/></g></svg>
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -165,7 +165,7 @@ div#select-board-dialog .selectBoardContainer .list .item.selected i {
|
||||
border: 1px solid var(--theia-arduino-toolbar-dropdown-border);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
height: 28px;
|
||||
height: var(--arduino-button-height);
|
||||
margin: 0 4px;
|
||||
overflow: hidden;
|
||||
padding: 0 10px;
|
||||
|
@@ -15,10 +15,10 @@
|
||||
|
||||
.p-TabBar-tabIcon.cloud-sketchbook-tree-icon {
|
||||
background-color: var(--theia-foreground);
|
||||
-webkit-mask: url(./cloud-sketchbook-tree-icon.svg);
|
||||
-webkit-mask: url(../icons/arduino-cloud.svg);
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
width: var(--theia-icon-size);
|
||||
width: 19px !important;
|
||||
height: var(--theia-icon-size);
|
||||
-webkit-mask-size: 100%;
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
.p-mod-current
|
||||
.cloud-sketchbook-tree-icon {
|
||||
background-color: var(--theia-foreground);
|
||||
-webkit-mask: url(./cloud-sketchbook-tree-icon-filled.svg);
|
||||
-webkit-mask: url(../icons/arduino-cloud-filled.svg);
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-size: 100%;
|
||||
@@ -99,28 +99,9 @@
|
||||
color: var(--theia-textLink-foreground);
|
||||
}
|
||||
|
||||
.pull-sketch-icon {
|
||||
background-color: var(--theia-foreground);
|
||||
-webkit-mask: url(./pull-sketch-icon.svg);
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
width: var(--theia-icon-size);
|
||||
height: var(--theia-icon-size);
|
||||
}
|
||||
|
||||
.push-sketch-icon {
|
||||
background-color: var(--theia-foreground);
|
||||
-webkit-mask: url(./push-sketch-icon.svg);
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
width: var(--theia-icon-size);
|
||||
height: var(--theia-icon-size);
|
||||
}
|
||||
|
||||
.account-icon {
|
||||
background: url("./account-icon.svg") center center no-repeat;
|
||||
width: var(--theia-icon-size);
|
||||
height: var(--theia-icon-size);
|
||||
width: var(--theia-private-sidebar-icon-size);
|
||||
height: var(--theia-private-sidebar-icon-size);
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -199,3 +180,12 @@
|
||||
.arduino-share-sketch-dialog .sketch-link-embed textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.actions.item.flex-line .fa,
|
||||
.theia-file-icons-js.file-icon .fa {
|
||||
font-size: var(--theia-icon-size);
|
||||
}
|
||||
|
||||
.theia-file-icons-js.file-icon.not-in-sync-offline .fa {
|
||||
color: var(--theia-activityBar-inactiveForeground);
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@
|
||||
|
||||
min-width: 424px;
|
||||
max-height: 560px;
|
||||
padding: 0 28px;
|
||||
padding: 0 var(--arduino-button-height);
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogTitle {
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent > input {
|
||||
margin-bottom: 28px;
|
||||
margin-bottom: var(--arduino-button-height);
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent > div {
|
||||
@@ -43,7 +43,7 @@
|
||||
}
|
||||
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection {
|
||||
margin-top: 28px;
|
||||
margin-top: var(--arduino-button-height);
|
||||
}
|
||||
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection:first-child {
|
||||
margin-top: 0;
|
||||
|
@@ -1,14 +1,19 @@
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
src: url('fonts/OpenSans-Regular-webfont.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans Bold';
|
||||
src: url('fonts/OpenSans-Bold-webfont.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'FontAwesome';
|
||||
src:
|
||||
url('fonts/FontAwesome.ttf?2jhpmq') format('truetype'),
|
||||
url('fonts/FontAwesome.woff?2jhpmq') format('woff'),
|
||||
url('fonts/FontAwesome.svg?2jhpmq#FontAwesome') format('svg');
|
||||
url('fonts/FontAwesome.ttf?h959em') format('truetype'),
|
||||
url('fonts/FontAwesome.woff?h959em') format('woff'),
|
||||
url('fonts/FontAwesome.svg?h959em#FontAwesome') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
@@ -674,3 +679,21 @@
|
||||
.fa-microchip:before {
|
||||
content: "\f2db";
|
||||
}
|
||||
.fa-arduino-cloud-download:before {
|
||||
content: "\e910";
|
||||
}
|
||||
.fa-arduino-cloud-upload:before {
|
||||
content: "\e914";
|
||||
}
|
||||
.fa-arduino-cloud:before {
|
||||
content: "\e915";
|
||||
}
|
||||
.fa-arduino-cloud-filled:before {
|
||||
content: "\e912";
|
||||
}
|
||||
.fa-arduino-cloud-offline:before {
|
||||
content: "\e913";
|
||||
}
|
||||
.fa-arduino-cloud-filled-offline:before {
|
||||
content: "\e911";
|
||||
}
|
||||
|
@@ -23,6 +23,12 @@
|
||||
<glyph unicode="" glyph-name="arduino-monitor" horiz-adv-x="1536" d="M651.891 59.977c-92.835 0-179.095 28.493-250.5 77.197l-129.659-129.658c-22.494-22.496-58.964-22.496-81.458 0s-22.494 58.963 0 81.459l124.954 124.954c-67.75 78.157-108.777 180.090-108.777 291.489 0 245.759 199.68 445.439 445.44 445.439s445.44-199.679 445.44-445.439c0-245.761-199.68-445.441-445.44-445.441zM651.891 797.257c-161.28 0-291.84-130.559-291.84-291.839s130.56-291.841 291.84-291.841c160.512 0 291.84 130.561 291.84 291.841 0 160.511-130.56 291.839-291.84 291.839zM1149.562 472.766c0-35.423 28.717-64.138 64.141-64.138s64.134 28.716 64.134 64.138c0 35.423-28.71 64.139-64.134 64.139s-64.141-28.716-64.141-64.139zM64.064 408.62c-35.382 0-64.064 28.682-64.064 64.063s28.682 64.064 64.064 64.064c35.381 0 64.064-28.682 64.064-64.064s-28.683-64.063-64.064-64.063zM1458.707 408.628c-35.418 0-64.134 28.716-64.134 64.138s28.717 64.139 64.134 64.139c35.424 0 64.141-28.716 64.141-64.139s-28.717-64.138-64.141-64.138zM652.659 424.010c-44.961 0-81.408 36.447-81.408 81.407s36.447 81.408 81.408 81.408c44.96 0 81.408-36.447 81.408-81.408s-36.448-81.407-81.408-81.407z" />
|
||||
<glyph unicode="" glyph-name="arduino-sketch-tabs-menu" d="M511.998 347.425c50.495 0 91.432 40.936 91.432 91.432s-40.936 91.432-91.432 91.432c-50.495 0-91.432-40.936-91.432-91.432s40.936-91.432 91.432-91.432zM923.433 347.425c50.494 0 91.432 40.936 91.432 91.432s-40.937 91.432-91.432 91.432c-50.494 0-91.432-40.936-91.432-91.432s40.937-91.432 91.432-91.432zM100.565 347.425c50.495 0 91.432 40.936 91.432 91.432s-40.936 91.432-91.432 91.432c-50.495 0-91.432-40.936-91.432-91.432s40.936-91.432 91.432-91.432z" />
|
||||
<glyph unicode="" glyph-name="arduino-plotter" horiz-adv-x="862" d="M323.368-19.351c-20.263 0-39 11.42-48.21 29.788l-146.789 293.581h-74.474c-29.789 0-53.895 24.107-53.895 53.895s24.105 53.895 53.895 53.895h107.789c20.421 0 39.053-11.528 48.21-29.788l96.527-193.056 180.263 720.949c5.842 23.579 26.737 40.263 51 40.842 23.947 1.579 45.893-15.158 52.894-38.421l150.162-500.526h67.681c29.788 0 53.895-24.107 53.895-53.895s-24.107-53.895-53.895-53.895h-107.789c-23.789 0-44.787 15.629-51.631 38.422l-105.316 351.104-168.052-672.053c-5.474-21.897-23.948-38.055-46.368-40.529-2-0.21-3.947-0.313-5.895-0.313h-0.001z" />
|
||||
<glyph unicode="" glyph-name="arduino-cloud-download" d="M684.256 156.891l-146.286-146.286c-6.932-6.802-16.255-10.606-25.964-10.606s-19.032 3.803-25.964 10.606l-146.286 146.286c-3.41 3.41-6.115 7.458-7.96 11.913s-2.796 9.23-2.796 14.052c-0.001 9.738 3.868 19.079 10.754 25.965s16.226 10.756 25.964 10.756c4.822 0 9.597-0.949 14.052-2.795s8.504-4.549 11.914-7.959l83.749-84.107v423.856c0 9.699 3.853 19.002 10.712 25.86s16.16 10.712 25.86 10.712c9.699 0 19.001-3.853 25.86-10.712s10.712-16.16 10.712-25.86v-423.856l83.749 84.107c6.886 6.886 16.227 10.756 25.966 10.756s19.079-3.869 25.966-10.756c6.886-6.886 10.755-16.227 10.755-25.966s-3.869-19.079-10.755-25.966zM786.286 292.572h-128c-9.699 0-19.001 3.852-25.86 10.711s-10.712 16.161-10.712 25.86c0 9.699 3.853 19.001 10.712 25.86s16.16 10.712 25.86 10.712h128c32.768 0.031 64.285 12.618 88.057 35.172 23.779 22.554 38.005 53.361 39.76 86.085s-9.092 64.877-30.318 89.846c-21.219 24.97-51.207 40.858-83.785 44.396-8.316 0.882-16.084 4.59-21.994 10.505-5.917 5.914-9.626 13.678-10.503 21.996-3.35 31.449-18.235 60.542-41.784 81.652-23.551 21.11-54.092 32.737-85.719 32.634-23.597 0.154-46.754-6.384-66.785-18.857-6.953-4.363-15.168-6.269-23.332-5.414s-15.805 4.42-21.704 10.128c-33.699 32.745-78.905 50.956-125.893 50.714-44.461-0.011-87.395-16.221-120.77-45.598s-54.9-69.908-60.551-114.009c-0.856-6.825-3.618-13.27-7.971-18.595s-10.119-9.315-16.636-11.512c-28.688-9.795-52.969-29.455-68.519-55.477s-21.361-56.718-16.396-86.623c4.964-29.905 20.381-57.078 43.504-76.68s52.454-30.362 82.768-30.363h128c9.699 0 19.002-3.853 25.86-10.712s10.711-16.16 10.711-25.86c0-9.699-3.853-19.002-10.711-25.86s-16.161-10.711-25.86-10.711h-128c-45.726 0.010-90.084 15.596-125.767 44.191s-60.559 68.491-70.532 113.116c-9.973 44.625-4.447 91.317 15.667 132.381s53.618 74.052 94.989 93.527c12.401 57.159 43.982 108.357 89.498 145.089s102.228 56.789 160.717 56.839c56.689 0.21 111.801-18.659 156.464-53.571 26.825 11.769 55.891 17.556 85.178 16.958s58.092-7.565 84.415-20.419c26.323-12.854 49.532-31.284 68.007-54.012 18.483-22.728 31.795-49.208 39.007-77.598 47.587-12.004 89.154-40.98 116.875-81.479 27.728-40.499 39.702-89.732 33.675-138.44-6.034-48.708-29.645-93.536-66.406-126.054s-84.136-50.488-133.215-50.527z" />
|
||||
<glyph unicode="" glyph-name="arduino-cloud-filled-offline" d="M854.72 704.131l-671.997-672.001c-6.066-5.946-14.223-9.28-22.719-9.28s-16.653 3.334-22.719 9.28c-5.996 6.042-9.361 14.208-9.361 22.72s3.365 16.678 9.361 22.72l107.52 107.52c-37.22 5.818-71.592 23.424-98.059 50.234s-43.632 61.402-48.97 98.694c-5.338 37.292 1.432 75.312 19.315 108.469s45.935 59.7 80.029 75.724c7.995 36.965 25.219 71.304 50.068 99.816s56.512 50.267 92.038 63.237c35.526 12.971 73.758 16.735 111.132 10.941s72.672-20.956 102.604-44.074c23.899 10.368 49.78 15.369 75.821 14.651 26.038-0.718 51.606-7.138 74.896-18.807l105.6 105.596c6.029 6.026 14.202 9.411 22.72 9.411 8.525 0 16.698-3.385 22.72-9.411 6.029-6.026 9.414-14.198 9.414-22.72s-3.386-16.694-9.414-22.72v0zM928 358.827c-0.083-46.653-18.65-91.375-51.642-124.36-32.986-32.986-77.702-51.558-124.358-51.642h-322.563l361.283 361.282c1.6-4.797 2.88-9.6 4.166-14.398 38.093-9.509 71.904-31.506 96.032-62.48s37.184-69.139 37.082-108.402v0z" />
|
||||
<glyph unicode="" glyph-name="arduino-cloud-filled" d="M928 358.859c-0.083-46.653-18.65-91.375-51.635-124.36-32.992-32.992-77.709-51.558-124.365-51.642h-479.998c-40.017 0.013-78.836 13.658-110.060 38.688-31.224 25.024-52.989 59.942-61.71 98.999-8.721 39.055-3.876 79.916 13.736 115.849s46.94 64.794 83.151 81.826c7.995 36.965 25.22 71.304 50.068 99.816s56.513 50.266 92.038 63.237c35.526 12.971 73.759 16.735 111.132 10.941s72.672-20.956 102.604-44.074c22.414 9.744 46.599 14.755 71.040 14.72 39.262 0.102 77.425-12.954 108.401-37.083s52.973-57.94 62.483-96.035c38.093-9.508 71.904-31.506 96.032-62.48s37.184-69.14 37.082-108.403z" />
|
||||
<glyph unicode="" glyph-name="arduino-cloud-offline" d="M794.88 529.709c-1.28 4.797-2.56 9.601-4.16 14.398l-53.12-53.125c2.080-5.548 5.67-10.404 10.362-14.022 4.698-3.618 10.304-5.853 16.198-6.454 28.493-3.153 54.701-17.094 73.235-38.963 18.541-21.868 28-50.003 26.445-78.628s-14.016-55.569-34.81-75.3c-20.8-19.725-48.365-30.746-77.030-30.79h-258.563l-64-64h322.563c42.944 0.026 84.403 15.744 116.57 44.198s52.832 67.68 58.099 110.301c5.267 42.621-5.216 85.699-29.491 121.13-24.269 35.43-60.646 60.771-102.298 71.254v0zM854.72 749.557c-6.042 5.997-14.208 9.363-22.72 9.363s-16.678-3.366-22.714-9.363l-105.606-105.595c-23.29 11.669-48.858 18.089-74.898 18.806s-51.923-4.284-75.821-14.652c-29.932 23.118-65.23 38.28-102.604 44.074s-75.606 2.029-111.132-10.941c-35.526-12.971-67.19-34.726-92.039-63.237s-42.073-62.851-50.068-99.816c-34.093-16.024-62.145-42.566-80.028-75.723s-24.653-71.177-19.315-108.468c5.338-37.292 22.502-71.884 48.968-98.693s60.837-44.416 98.057-50.234l-107.52-107.52c-5.996-6.042-9.361-14.208-9.361-22.72s3.364-16.678 9.361-22.72c6.065-5.946 14.223-9.28 22.719-9.28s16.653 3.334 22.719 9.28l672.001 672.001c5.997 6.040 9.357 14.207 9.363 22.718 0 8.511-3.366 16.678-9.363 22.719v0zM306.564 246.838h-34.563c-26.523-0.013-52.188 9.395-72.423 26.541s-33.725 40.92-38.067 67.085c-4.342 26.165 0.747 53.022 14.36 75.785s34.865 39.954 59.97 48.51c5.716 1.953 10.763 5.483 14.557 10.184s6.18 10.379 6.883 16.379c5.021 38.555 23.892 73.968 53.095 99.638s66.744 39.843 105.625 39.878c41.119 0.243 80.673-15.741 110.078-44.484 5.176-4.951 11.848-8.045 18.97-8.797s14.294 0.88 20.39 4.641c17.553 10.974 37.86 16.744 58.562 16.641 10.271-0.061 20.492-1.458 30.399-4.156l-347.836-347.843z" />
|
||||
<glyph unicode="" glyph-name="arduino-cloud-upload" d="M684.258 412.892c-6.932-6.799-16.255-10.607-25.964-10.607s-19.032 3.809-25.964 10.607l-83.751 84.118v-423.867c0-9.699-3.853-19.003-10.711-25.856-6.859-6.861-16.161-10.715-25.86-10.715s-19.001 3.855-25.86 10.715c-6.859 6.853-10.712 16.157-10.712 25.856v423.867l-83.749-84.118c-6.886-6.886-16.227-10.756-25.966-10.756s-19.079 3.869-25.966 10.756c-6.886 6.886-10.755 16.227-10.755 25.966s3.869 19.079 10.755 25.966l146.286 146.286c6.903 6.854 16.236 10.701 25.964 10.701s19.062-3.847 25.964-10.701l146.286-146.286c6.853-6.904 10.7-16.237 10.701-25.965s-3.845-19.062-10.698-25.966zM786.286 256.001h-128c-9.699 0-19.001 3.852-25.86 10.711s-10.712 16.161-10.712 25.86c0 9.699 3.853 19.001 10.712 25.86s16.16 10.712 25.86 10.712h128c32.768 0.031 64.285 12.618 88.057 35.172 23.779 22.554 38.005 53.361 39.76 86.085s-9.092 64.877-30.318 89.846c-21.219 24.97-51.207 40.858-83.785 44.396-8.316 0.882-16.084 4.59-21.994 10.505-5.917 5.914-9.626 13.678-10.503 21.996-3.35 31.449-18.235 60.542-41.784 81.652-23.551 21.11-54.092 32.737-85.719 32.634-23.597 0.154-46.754-6.384-66.785-18.857-6.954-4.362-15.168-6.268-23.331-5.413s-15.805 4.419-21.705 10.127c-33.699 32.745-78.905 50.956-125.893 50.714-44.461-0.011-87.395-16.221-120.77-45.598s-54.9-69.908-60.551-114.009c-0.856-6.825-3.618-13.27-7.971-18.595s-10.119-9.315-16.636-11.512c-28.688-9.795-52.969-29.455-68.519-55.477s-21.361-56.718-16.396-86.623c4.964-29.905 20.381-57.078 43.504-76.68s52.454-30.362 82.768-30.363h128c9.699 0 19.002-3.853 25.86-10.712s10.711-16.16 10.711-25.86c0-9.699-3.853-19.002-10.711-25.86s-16.161-10.711-25.86-10.711h-128c-45.726 0.010-90.084 15.596-125.767 44.191s-60.559 68.491-70.532 113.116c-9.973 44.625-4.447 91.317 15.667 132.381s53.618 74.052 94.989 93.527c12.401 57.159 43.982 108.357 89.498 145.089s102.228 56.789 160.717 56.839c56.689 0.21 111.801-18.659 156.464-53.571 26.825 11.769 55.891 17.556 85.178 16.958s58.092-7.565 84.415-20.419c26.323-12.854 49.532-31.284 68.007-54.012 18.483-22.728 31.795-49.208 39.007-77.598 47.587-12.004 89.154-40.98 116.875-81.479 27.728-40.499 39.702-89.732 33.675-138.44-6.034-48.708-29.645-93.536-66.406-126.054s-84.136-50.488-133.215-50.527z" />
|
||||
<glyph unicode="" glyph-name="arduino-cloud" d="M752 182.857h-480c-40.010 0.006-78.824 13.645-110.046 38.662-31.222 25.024-52.989 59.93-61.716 98.98-8.726 39.047-3.891 79.902 13.709 115.833s46.915 64.796 83.115 81.836c10.851 50.014 38.484 94.812 78.31 126.953s89.45 49.69 140.627 49.734c49.603 0.184 97.826-16.327 136.906-46.875 23.472 10.298 48.904 15.361 74.531 14.838s50.829-6.62 73.862-17.866c23.034-11.247 43.341-27.374 59.507-47.261 16.173-19.887 27.821-43.056 34.131-67.898 41.638-10.504 78.010-35.857 102.266-71.294 24.262-35.437 34.739-78.515 29.466-121.135-5.28-42.623-25.939-81.842-58.106-110.296s-73.619-44.179-116.563-44.211zM416 630.856c-38.904-0.010-76.471-14.193-105.674-39.899s-48.038-61.169-52.982-99.757c-0.749-5.972-3.166-11.611-6.975-16.271s-8.853-8.151-14.556-10.073c-25.102-8.571-46.348-25.773-59.954-48.542s-18.691-49.628-14.347-75.795c4.344-26.167 17.833-49.943 38.066-67.095s45.897-26.566 72.422-26.566h480c28.672 0.026 56.25 11.040 77.050 30.778 20.806 19.731 33.254 46.688 34.79 75.321s-7.955 56.767-26.528 78.616c-18.566 21.848-44.806 35.75-73.312 38.847-7.277 0.772-14.074 4.016-19.245 9.191-5.178 5.176-8.422 11.969-9.19 19.247-2.931 27.518-15.955 52.974-36.563 71.445-20.602 18.471-47.328 28.645-75.002 28.555-20.647 0.135-40.909-5.587-58.437-16.5-6.084-3.816-13.272-5.484-20.415-4.737s-13.83 3.868-18.992 8.861c-29.487 28.652-69.042 44.586-110.156 44.375v0z" />
|
||||
<glyph unicode="" glyph-name="music" horiz-adv-x="878" d="M877.714 822.857v-640c0-80.571-120.571-109.714-182.857-109.714s-182.857 29.143-182.857 109.714 120.571 109.714 182.857 109.714c37.714 0 75.429-6.857 109.714-22.286v306.857l-438.857-135.429v-405.143c0-80.571-120.571-109.714-182.857-109.714s-182.857 29.143-182.857 109.714 120.571 109.714 182.857 109.714c37.714 0 75.429-6.857 109.714-22.286v552.571c0 24 16 45.143 38.857 52.571l475.429 146.286c5.143 1.714 10.286 2.286 16 2.286 30.286 0 54.857-24.571 54.857-54.857z" />
|
||||
<glyph unicode="" glyph-name="search" horiz-adv-x="951" d="M658.286 475.428c0 141.143-114.857 256-256 256s-256-114.857-256-256 114.857-256 256-256 256 114.857 256 256zM950.857 0c0-40-33.143-73.143-73.143-73.143-19.429 0-38.286 8-51.429 21.714l-196 195.429c-66.857-46.286-146.857-70.857-228-70.857-222.286 0-402.286 180-402.286 402.286s180 402.286 402.286 402.286 402.286-180 402.286-402.286c0-81.143-24.571-161.143-70.857-228l196-196c13.143-13.143 21.143-32 21.143-51.429z" />
|
||||
<glyph unicode="" glyph-name="envelope-o" d="M950.857 91.428v438.857c-12-13.714-25.143-26.286-39.429-37.714-81.714-62.857-164-126.857-243.429-193.143-42.857-36-96-80-155.429-80h-1.143c-59.429 0-112.571 44-155.429 80-79.429 66.286-161.714 130.286-243.429 193.143-14.286 11.429-27.429 24-39.429 37.714v-438.857c0-9.714 8.571-18.286 18.286-18.286h841.143c9.714 0 18.286 8.571 18.286 18.286zM950.857 692c0 14.286 3.429 39.429-18.286 39.429h-841.143c-9.714 0-18.286-8.571-18.286-18.286 0-65.143 32.571-121.714 84-162.286 76.571-60 153.143-120.571 229.143-181.143 30.286-24.571 85.143-77.143 125.143-77.143h1.143c40 0 94.857 52.571 125.143 77.143 76 60.571 152.571 121.143 229.143 181.143 37.143 29.143 84 92.571 84 141.143zM1024 713.143v-621.714c0-50.286-41.143-91.429-91.429-91.429h-841.143c-50.286 0-91.429 41.143-91.429 91.429v621.714c0 50.286 41.143 91.429 91.429 91.429h841.143c50.286 0 91.429-41.143 91.429-91.429z" />
|
||||
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 164 KiB |
85
arduino-ide-extension/src/browser/style/hover-service.css
Normal file
@@ -0,0 +1,85 @@
|
||||
/* Copied from https://github.com/eclipse-theia/theia/commit/909f4106e8c15c5c2c320401da4f48f8c6080734 */
|
||||
/* Remove when IDE2 uses 1.32.0 */
|
||||
|
||||
/* Adapted from https://github.com/microsoft/vscode/blob/7d9b1c37f8e5ae3772782ba3b09d827eb3fdd833/src/vs/workbench/services/hover/browser/hoverService.ts */
|
||||
|
||||
:root {
|
||||
--theia-hover-max-width: 200px; /* customized */
|
||||
}
|
||||
|
||||
.theia-hover {
|
||||
font-family: var(--theia-ui-font-family);
|
||||
font-size: var(--theia-ui-font-size1);
|
||||
color: var(--theia-editorHoverWidget-foreground);
|
||||
background-color: var(--theia-editorHoverWidget-background);
|
||||
border: 1px solid var(--theia-editorHoverWidget-border);
|
||||
padding: var(--theia-ui-padding);
|
||||
max-width: var(--theia-hover-max-width);
|
||||
}
|
||||
|
||||
.theia-hover .hover-row:not(:first-child):not(:empty) {
|
||||
border-top: 1px solid var(--theia-editorHoverWidgetInternalBorder);
|
||||
}
|
||||
|
||||
.theia-hover hr {
|
||||
border-top: 1px solid var(--theia-editorHoverWidgetInternalBorder);
|
||||
border-bottom: 0px solid var(--theia-editorHoverWidgetInternalBorder);
|
||||
margin: var(--theia-ui-padding) calc(var(--theia-ui-padding) * -1);
|
||||
}
|
||||
|
||||
.theia-hover a {
|
||||
color: var(--theia-textLink-foreground);
|
||||
word-wrap: break-word; /* customized */
|
||||
cursor: pointer; /* customized */
|
||||
}
|
||||
|
||||
.theia-hover a:hover {
|
||||
/* color: var(--theia-textLink-active-foreground); */
|
||||
text-decoration: underline; /* customized */
|
||||
}
|
||||
|
||||
.theia-hover .hover-row .actions {
|
||||
background-color: var(--theia-editorHoverWidget-statusBarBackground);
|
||||
}
|
||||
|
||||
.theia-hover code {
|
||||
background-color: var(--theia-textCodeBlock-background);
|
||||
font-family: var(--theia-code-font-family);
|
||||
}
|
||||
|
||||
.theia-hover::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.theia-hover.top::before {
|
||||
left: var(--theia-hover-before-position);
|
||||
bottom: -5px;
|
||||
border-top: 5px solid var(--theia-editorHoverWidget-border);
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
}
|
||||
|
||||
.theia-hover.bottom::before {
|
||||
left: var(--theia-hover-before-position);
|
||||
top: -5px;
|
||||
border-bottom: 5px solid var(--theia-editorHoverWidget-border);
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
}
|
||||
|
||||
.theia-hover.left::before {
|
||||
top: var(--theia-hover-before-position);
|
||||
right: -5px;
|
||||
border-left: 5px solid var(--theia-editorHoverWidget-border);
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
}
|
||||
|
||||
.theia-hover.right::before {
|
||||
top: var(--theia-hover-before-position);
|
||||
left: -5px;
|
||||
border-right: 5px solid var(--theia-editorHoverWidget-border);
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
}
|
@@ -15,7 +15,7 @@
|
||||
}
|
||||
|
||||
.ide-updater-dialog--logo-container {
|
||||
margin-right: 28px;
|
||||
margin-right: var(--arduino-button-height);
|
||||
}
|
||||
|
||||
.ide-updater-dialog--logo {
|
||||
@@ -76,7 +76,7 @@
|
||||
.ide-updater-dialog .buttons-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 28px;
|
||||
margin-top: var(--arduino-button-height);
|
||||
}
|
||||
|
||||
.ide-updater-dialog .buttons-container a.theia-button {
|
||||
|
@@ -20,6 +20,11 @@
|
||||
@import './progress-bar.css';
|
||||
@import './settings-step-input.css';
|
||||
|
||||
:root {
|
||||
--arduino-button-height: 28px;
|
||||
--arduino-side-panel-min-width: 220px;
|
||||
}
|
||||
|
||||
/* Revive of the `--theia-icon-loading`. The variable has been removed from Theia while IDE2 still uses is. */
|
||||
/* The SVG icons are still part of Theia (1.31.1) */
|
||||
/* https://github.com/arduino/arduino-ide/pull/1662#issuecomment-1324997134 */
|
||||
@@ -64,9 +69,9 @@ body.theia-dark {
|
||||
|
||||
/* Makes the sidepanel a bit wider when opening the widget */
|
||||
.p-DockPanel-widget {
|
||||
min-width: 200px;
|
||||
min-width: var(--arduino-side-panel-min-width);
|
||||
min-height: 20px;
|
||||
height: 200px;
|
||||
height: var(--arduino-side-panel-min-width);
|
||||
}
|
||||
|
||||
/* Overrule the default Theia CSS button styles. */
|
||||
@@ -74,9 +79,9 @@ button.theia-button,
|
||||
.theia-button {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-family: 'Open Sans',sans-serif;
|
||||
font-family: 'Open Sans Bold',sans-serif;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
@@ -95,7 +100,7 @@ button.theia-button,
|
||||
}
|
||||
|
||||
button.theia-button {
|
||||
height: 28px;
|
||||
height: var(--arduino-button-height);
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
@@ -154,10 +159,6 @@ button.theia-button.message-box-dialog-button {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* High Contrast Theme rules */
|
||||
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
|
||||
.hc-black.hc-theia.theia-hc button.theia-button:hover,
|
||||
|
@@ -44,102 +44,156 @@
|
||||
height: 100%; /* This has top be 100% down to the `scrollContainer`. */
|
||||
}
|
||||
|
||||
.filterable-list-container .items-container > div > div:nth-child(odd) {
|
||||
background-color: var(--theia-sideBar-background);
|
||||
filter: contrast(105%);
|
||||
}
|
||||
|
||||
.filterable-list-container .items-container > div > div:nth-child(even) {
|
||||
background-color: var(--theia-sideBar-background);
|
||||
filter: contrast(95%);
|
||||
}
|
||||
|
||||
.filterable-list-container .items-container > div > div:hover {
|
||||
background-color: var(--theia-sideBar-background);
|
||||
filter: contrast(90%);
|
||||
}
|
||||
|
||||
.component-list-item {
|
||||
padding: 10px 10px 10px 15px;
|
||||
font-size: var(--theia-ui-font-size1);
|
||||
}
|
||||
|
||||
.component-list-item:hover {
|
||||
cursor: pointer;
|
||||
padding: 20px 15px 25px;
|
||||
}
|
||||
|
||||
.component-list-item .header {
|
||||
padding-bottom: 2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: var(--theia-statusBar-height);
|
||||
}
|
||||
|
||||
.component-list-item .header .version-info {
|
||||
.component-list-item .header > div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.component-list-item .header > div .p-TabBar-toolbar {
|
||||
align-self: start;
|
||||
padding: unset;
|
||||
margin-right: unset;
|
||||
}
|
||||
|
||||
.component-list-item:hover .header > div .p-TabBar-toolbar > div {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.component-list-item .header > div .p-TabBar-toolbar > div {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.component-list-item .header .title {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.component-list-item .header .title .name {
|
||||
font-family: 'Open Sans Bold';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.component-list-item .header .version {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.component-list-item .header .name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.component-list-item .header .author {
|
||||
font-weight: bold;
|
||||
color: var(--theia-panelTitle-inactiveForeground);
|
||||
}
|
||||
|
||||
.component-list-item:hover .header .author {
|
||||
color: var(--theia-foreground);
|
||||
}
|
||||
|
||||
.component-list-item .header .version {
|
||||
color: var(--theia-panelTitle-inactiveForeground);
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.component-list-item .footer .theia-button.install {
|
||||
height: auto; /* resets the default Theia button height in the filterable list widget */
|
||||
}
|
||||
|
||||
.component-list-item .header .installed:before {
|
||||
margin-left: 4px;
|
||||
.component-list-item .header .installed-version:before {
|
||||
min-width: 79px;
|
||||
display: inline-block;
|
||||
justify-self: end;
|
||||
background-color: var(--theia-button-background);
|
||||
text-align: center;
|
||||
background-color: var(--theia-arduino-toolbar-dropdown-option-backgroundHover);
|
||||
padding: 2px 4px 2px 4px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
max-height: calc(1em + 4px);
|
||||
color: var(--theia-button-foreground);
|
||||
content: attr(install);
|
||||
}
|
||||
|
||||
.component-list-item .header .installed:hover:before {
|
||||
background-color: var(--theia-button-foreground);
|
||||
color: var(--theia-button-background);
|
||||
content: attr(uninstall);
|
||||
content: attr(version);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.component-list-item[min-width~="170px"] .footer {
|
||||
padding: 5px 5px 0px 0px;
|
||||
min-height: 35px;
|
||||
.component-list-item .header .installed-version:hover:before {
|
||||
content: attr(remove);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.component-list-item .content {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
flex-direction: column;
|
||||
padding-top: 4px;
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.component-list-item .content > p {
|
||||
margin-block-start: unset;
|
||||
margin-block-end: unset;
|
||||
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.component-list-item .content > .info {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.component-list-item .footer {
|
||||
flex-direction: column-reverse;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.component-list-item .footer.scrolling {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.component-list-item .footer > * {
|
||||
display: inline-block;
|
||||
margin: 5px 0px 0px 10px;
|
||||
}
|
||||
|
||||
.filterable-list-container .separator {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.filterable-list-container .separator :last-child,
|
||||
.filterable-list-container .separator :first-child {
|
||||
min-height: 8px;
|
||||
max-height: 8px;
|
||||
min-width: 8px;
|
||||
max-width: 8px;
|
||||
}
|
||||
|
||||
div.filterable-list-container > div > div > div > div:nth-child(1) > div.separator :first-child,
|
||||
div.filterable-list-container > div > div > div > div:nth-child(1) > div.separator :last-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filterable-list-container .separator .line {
|
||||
max-height: 1px;
|
||||
height: 1px;
|
||||
background-color: var(--theia-activityBar-inactiveForeground);
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.component-list-item:hover .footer > label {
|
||||
display: inline-block;
|
||||
align-self: center;
|
||||
margin: 5px 0px 0px 10px;
|
||||
}
|
||||
|
||||
.component-list-item .info a {
|
||||
@@ -151,13 +205,33 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* High Contrast Theme rules */
|
||||
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
|
||||
.hc-black.hc-theia.theia-hc .component-list-item .header .installed:hover:before {
|
||||
background-color: transparent;
|
||||
outline: 1px dashed var(--theia-focusBorder);
|
||||
.component-list-item .theia-button.secondary.no-border {
|
||||
border: 2px solid var(--theia-button-foreground)
|
||||
}
|
||||
|
||||
.hc-black.hc-theia.theia-hc .component-list-item .header .installed:before {
|
||||
.component-list-item .theia-button.secondary.no-border:hover {
|
||||
border: 2px solid var(--theia-secondaryButton-foreground)
|
||||
}
|
||||
|
||||
.component-list-item .theia-button {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.component-list-item .theia-select {
|
||||
height: var(--arduino-button-height);
|
||||
min-height: var(--arduino-button-height);
|
||||
width: 65px;
|
||||
min-width: 65px;
|
||||
}
|
||||
|
||||
/* High Contrast Theme rules */
|
||||
/* TODO: Remove it when the Theia version is upgraded to 1.27.0 and use Theia APIs to implement it*/
|
||||
.hc-black.hc-theia.theia-hc .component-list-item .header .installed-version:hover:before {
|
||||
background-color: transparent;
|
||||
outline: 1px dashed var(--theia-focusBorder);
|
||||
}
|
||||
|
||||
.hc-black.hc-theia.theia-hc .component-list-item .header .installed-version:before {
|
||||
color: var(--theia-button-background);
|
||||
border: 1px solid var(--theia-button-border);
|
||||
}
|
||||
|
@@ -28,8 +28,8 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
height: var(--arduino-button-height);
|
||||
width: var(--arduino-button-height);
|
||||
}
|
||||
|
||||
.p-TabBar-toolbar .item.arduino-tool-item .arduino-upload-sketch--toolbar,
|
||||
@@ -66,8 +66,8 @@
|
||||
}
|
||||
|
||||
.arduino-tool-icon {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
height: var(--arduino-button-height);
|
||||
width: var(--arduino-button-height);
|
||||
}
|
||||
|
||||
.arduino-verify-sketch--toolbar-icon {
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
.arduino-upload-sketch--toolbar-icon {
|
||||
-webkit-mask: url(../icons/upload.svg) center no-repeat;
|
||||
background-color: var(--theia-titleBar-activeBackground);
|
||||
background-color: var(--theia-titleBar-activeBackground);
|
||||
}
|
||||
|
||||
.toggle-serial-monitor-icon {
|
||||
@@ -100,19 +100,6 @@
|
||||
background-color: var(--theia-titleBar-activeBackground);
|
||||
}
|
||||
|
||||
#arduino-toolbar-panel {
|
||||
background: var(--theia-titleBar-activeBackground);
|
||||
color: var(--theia-titleBar-activeForeground);
|
||||
display: flex;
|
||||
min-height: var(--theia-private-menubar-height);
|
||||
border-bottom: 1px solid var(--theia-titleBar-border);
|
||||
}
|
||||
#arduino-toolbar-panel:window-inactive,
|
||||
#arduino-toolbar-panel:-moz-window-inactive {
|
||||
background: var(--theia-titleBar-inactiveBackground);
|
||||
color: var(--theia-titleBar-inactiveForeground);
|
||||
}
|
||||
|
||||
#arduino-toolbar-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
@@ -127,6 +114,10 @@
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.p-TabBar-toolbar .item > div {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:root {
|
||||
--theia-private-menubar-height: 40px; /* set the topbar height */
|
||||
}
|
||||
|
@@ -1,4 +0,0 @@
|
||||
<svg width="8" height="7" viewBox="0 0 8 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.5 6.18999C2.415 6.18999 2.33 6.16999 2.25 6.12499C1.17 5.49999 0.5 4.33999 0.5 3.09499C0.5 1.84999 1.17 0.689992 2.25 0.0649925C2.49 -0.0700075 2.795 0.00999246 2.935 0.249992C3.07 0.489992 2.99 0.794992 2.75 0.934992C1.98 1.37499 1.5 2.20499 1.5 3.09499C1.5 3.98499 1.98 4.81499 2.75 5.25499C2.99 5.39499 3.07 5.69999 2.935 5.93999C2.84 6.09999 2.675 6.18999 2.5 6.18999Z" fill="#008184"/>
|
||||
<path d="M5.49993 6.18999C5.32493 6.18999 5.15993 6.09999 5.06493 5.93999C4.92493 5.69999 5.00993 5.39499 5.24993 5.25499C6.01993 4.81499 6.49993 3.98499 6.49993 3.09499C6.49993 2.20499 6.01993 1.37499 5.24993 0.934992C5.00993 0.794992 4.92993 0.489992 5.06493 0.249992C5.20493 0.00999246 5.50993 -0.0700075 5.74993 0.0649925C6.82993 0.689992 7.49993 1.84999 7.49993 3.09499C7.49993 4.33999 6.82993 5.49999 5.74993 6.12499C5.66993 6.16999 5.58493 6.18999 5.49993 6.18999Z" fill="#008184"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 992 B |
@@ -3,13 +3,6 @@
|
||||
mask: url('./sketchbook.svg');
|
||||
}
|
||||
|
||||
.sketch-folder-icon {
|
||||
background: url('./sketch-folder-icon.svg') center center no-repeat;
|
||||
background-position-x: 1px;
|
||||
width: var(--theia-icon-size);
|
||||
height: var(--theia-icon-size);
|
||||
}
|
||||
|
||||
.p-TabBar-tabIcon.sketchbook-tree-icon {
|
||||
background-color: var(--theia-foreground);
|
||||
-webkit-mask: url(./sketchbook-tree-icon.svg);
|
||||
|
@@ -7,8 +7,6 @@ import {
|
||||
SHELL_TABBAR_CONTEXT_MENU,
|
||||
TabBar,
|
||||
Widget,
|
||||
Layout,
|
||||
SplitPanel,
|
||||
} from '@theia/core/lib/browser';
|
||||
import {
|
||||
ConnectionStatus,
|
||||
@@ -19,11 +17,6 @@ import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { ToolbarAwareTabBar } from './tab-bars';
|
||||
|
||||
interface WidgetOptions
|
||||
extends Omit<TheiaApplicationShell.WidgetOptions, 'area'> {
|
||||
area?: TheiaApplicationShell.Area | 'toolbar';
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ApplicationShell extends TheiaApplicationShell {
|
||||
@inject(MessageService)
|
||||
@@ -31,11 +24,10 @@ export class ApplicationShell extends TheiaApplicationShell {
|
||||
|
||||
@inject(ConnectionStatusService)
|
||||
private readonly connectionStatusService: ConnectionStatusService;
|
||||
private toolbarPanel: Panel;
|
||||
|
||||
override async addWidget(
|
||||
widget: Widget,
|
||||
options: Readonly<WidgetOptions> = {}
|
||||
options: Readonly<TheiaApplicationShell.WidgetOptions> = {}
|
||||
): Promise<void> {
|
||||
// By default, Theia open a widget **next** to the currently active in the target area.
|
||||
// Instead of this logic, we want to open the new widget after the last of the target area.
|
||||
@@ -45,12 +37,8 @@ export class ApplicationShell extends TheiaApplicationShell {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (options.area === 'toolbar') {
|
||||
this.toolbarPanel.addWidget(widget);
|
||||
return;
|
||||
}
|
||||
const area = options.area || 'main';
|
||||
let ref: Widget | undefined = options.ref;
|
||||
const area: TheiaApplicationShell.Area = options.area || 'main';
|
||||
if (!ref && (area === 'main' || area === 'bottom')) {
|
||||
const tabBar = this.getTabBarFor(area);
|
||||
if (tabBar) {
|
||||
@@ -60,10 +48,7 @@ export class ApplicationShell extends TheiaApplicationShell {
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.addWidget(widget, {
|
||||
...(<TheiaApplicationShell.WidgetOptions>options),
|
||||
ref,
|
||||
});
|
||||
return super.addWidget(widget, { ...options, ref });
|
||||
}
|
||||
|
||||
override handleEvent(): boolean {
|
||||
@@ -71,46 +56,6 @@ export class ApplicationShell extends TheiaApplicationShell {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override initializeShell(): void {
|
||||
this.toolbarPanel = this.createToolbarPanel();
|
||||
super.initializeShell();
|
||||
}
|
||||
|
||||
private createToolbarPanel(): Panel {
|
||||
const toolbarPanel = new Panel();
|
||||
toolbarPanel.id = 'arduino-toolbar-panel';
|
||||
toolbarPanel.show();
|
||||
return toolbarPanel;
|
||||
}
|
||||
|
||||
protected override createLayout(): Layout {
|
||||
const bottomSplitLayout = this.createSplitLayout(
|
||||
[this.mainPanel, this.bottomPanel],
|
||||
[1, 0],
|
||||
{ orientation: 'vertical', spacing: 0 }
|
||||
);
|
||||
const panelForBottomArea = new SplitPanel({ layout: bottomSplitLayout });
|
||||
panelForBottomArea.id = 'theia-bottom-split-panel';
|
||||
|
||||
const leftRightSplitLayout = this.createSplitLayout(
|
||||
[
|
||||
this.leftPanelHandler.container,
|
||||
panelForBottomArea,
|
||||
this.rightPanelHandler.container,
|
||||
],
|
||||
[0, 1, 0],
|
||||
{ orientation: 'horizontal', spacing: 0 }
|
||||
);
|
||||
const panelForSideAreas = new SplitPanel({ layout: leftRightSplitLayout });
|
||||
panelForSideAreas.id = 'theia-left-right-split-panel';
|
||||
|
||||
return this.createBoxLayout(
|
||||
[this.topPanel, this.toolbarPanel, panelForSideAreas, this.statusBar],
|
||||
[0, 0, 1, 0],
|
||||
{ direction: 'top-to-bottom', spacing: 0 }
|
||||
);
|
||||
}
|
||||
|
||||
// Avoid hiding top panel as we use it for arduino toolbar
|
||||
protected override createTopPanel(): Panel {
|
||||
const topPanel = super.createTopPanel();
|
||||
|
@@ -1,106 +1,324 @@
|
||||
import {
|
||||
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
|
||||
ConnectionStatus,
|
||||
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
|
||||
} from '@theia/core/lib/browser/connection-status-service';
|
||||
import type { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { MessageType } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import { StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
|
||||
import {
|
||||
FrontendConnectionStatusService as TheiaFrontendConnectionStatusService,
|
||||
ApplicationConnectionStatusContribution as TheiaApplicationConnectionStatusContribution,
|
||||
ConnectionStatus,
|
||||
} from '@theia/core/lib/browser/connection-status-service';
|
||||
import { NotificationManager } from '@theia/messages/lib/browser/notifications-manager';
|
||||
import { ArduinoDaemon } from '../../../common/protocol';
|
||||
import { assertUnreachable } from '../../../common/utils';
|
||||
import { CreateFeatures } from '../../create/create-features';
|
||||
import { NotificationCenter } from '../../notification-center';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import debounce = require('lodash.debounce');
|
||||
import isOnline = require('is-online');
|
||||
|
||||
@injectable()
|
||||
export class FrontendConnectionStatusService extends TheiaFrontendConnectionStatusService {
|
||||
@inject(ArduinoDaemon)
|
||||
protected readonly daemon: ArduinoDaemon;
|
||||
export class IsOnline implements FrontendApplicationContribution {
|
||||
private readonly onDidChangeOnlineEmitter = new Emitter<boolean>();
|
||||
private _online = false;
|
||||
private stopped = false;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
onStart(): void {
|
||||
const checkOnline = async () => {
|
||||
if (!this.stopped) {
|
||||
try {
|
||||
const online = await isOnline();
|
||||
this.setOnline(online);
|
||||
} finally {
|
||||
window.setTimeout(() => checkOnline(), 6_000); // 6 seconds poll interval
|
||||
}
|
||||
}
|
||||
};
|
||||
checkOnline();
|
||||
}
|
||||
|
||||
protected connectedPort: string | undefined;
|
||||
onStop(): void {
|
||||
this.stopped = true;
|
||||
this.onDidChangeOnlineEmitter.dispose();
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected override async init(): Promise<void> {
|
||||
this.schedulePing();
|
||||
try {
|
||||
this.connectedPort = await this.daemon.tryGetPort();
|
||||
} catch {}
|
||||
this.notificationCenter.onDaemonDidStart(
|
||||
(port) => (this.connectedPort = port)
|
||||
);
|
||||
this.notificationCenter.onDaemonDidStop(
|
||||
() => (this.connectedPort = undefined)
|
||||
);
|
||||
const refresh = debounce(() => {
|
||||
this.updateStatus(!!this.connectedPort);
|
||||
this.schedulePing();
|
||||
}, this.options.offlineTimeout - 10);
|
||||
this.wsConnectionProvider.onIncomingMessageActivity(() => refresh());
|
||||
get online(): boolean {
|
||||
return this._online;
|
||||
}
|
||||
|
||||
get onDidChangeOnline(): Event<boolean> {
|
||||
return this.onDidChangeOnlineEmitter.event;
|
||||
}
|
||||
|
||||
private setOnline(online: boolean) {
|
||||
const oldOnline = this._online;
|
||||
this._online = online;
|
||||
if (!this.stopped && this._online !== oldOnline) {
|
||||
this.onDidChangeOnlineEmitter.fire(this._online);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ApplicationConnectionStatusContribution extends TheiaApplicationConnectionStatusContribution {
|
||||
export class DaemonPort implements FrontendApplicationContribution {
|
||||
@inject(ArduinoDaemon)
|
||||
protected readonly daemon: ArduinoDaemon;
|
||||
|
||||
private readonly daemon: ArduinoDaemon;
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
private readonly notificationCenter: NotificationCenter;
|
||||
|
||||
protected connectedPort: string | undefined;
|
||||
private readonly onPortDidChangeEmitter = new Emitter<string | undefined>();
|
||||
private _port: string | undefined;
|
||||
|
||||
onStart(): void {
|
||||
this.daemon.tryGetPort().then(
|
||||
(port) => this.setPort(port),
|
||||
(reason) =>
|
||||
console.warn('Could not retrieve the CLI daemon port.', reason)
|
||||
);
|
||||
this.notificationCenter.onDaemonDidStart((port) => this.setPort(port));
|
||||
this.notificationCenter.onDaemonDidStop(() => this.setPort(undefined));
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.onPortDidChangeEmitter.dispose();
|
||||
}
|
||||
|
||||
get port(): string | undefined {
|
||||
return this._port;
|
||||
}
|
||||
|
||||
get onDidChangePort(): Event<string | undefined> {
|
||||
return this.onPortDidChangeEmitter.event;
|
||||
}
|
||||
|
||||
private setPort(port: string | undefined): void {
|
||||
const oldPort = this._port;
|
||||
this._port = port;
|
||||
if (this._port !== oldPort) {
|
||||
this.onPortDidChangeEmitter.fire(this._port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class FrontendConnectionStatusService extends TheiaFrontendConnectionStatusService {
|
||||
@inject(DaemonPort)
|
||||
private readonly daemonPort: DaemonPort;
|
||||
@inject(IsOnline)
|
||||
private readonly isOnline: IsOnline;
|
||||
|
||||
@postConstruct()
|
||||
protected async init(): Promise<void> {
|
||||
protected override async init(): Promise<void> {
|
||||
this.schedulePing();
|
||||
const refresh = debounce(() => {
|
||||
this.updateStatus(Boolean(this.daemonPort.port) && this.isOnline.online);
|
||||
this.schedulePing();
|
||||
}, this.options.offlineTimeout - 10);
|
||||
this.wsConnectionProvider.onIncomingMessageActivity(() => refresh());
|
||||
}
|
||||
|
||||
protected override async performPingRequest(): Promise<void> {
|
||||
try {
|
||||
this.connectedPort = await this.daemon.tryGetPort();
|
||||
} catch {}
|
||||
this.notificationCenter.onDaemonDidStart(
|
||||
(port) => (this.connectedPort = port)
|
||||
);
|
||||
this.notificationCenter.onDaemonDidStop(
|
||||
() => (this.connectedPort = undefined)
|
||||
);
|
||||
await this.pingService.ping();
|
||||
this.updateStatus(this.isOnline.online);
|
||||
} catch (e) {
|
||||
this.updateStatus(false);
|
||||
this.logger.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const connectionStatusStatusBar = 'connection-status';
|
||||
const theiaOffline = 'theia-mod-offline';
|
||||
|
||||
export type OfflineConnectionStatus =
|
||||
/**
|
||||
* There is no websocket connection between the frontend and the backend.
|
||||
*/
|
||||
| 'backend'
|
||||
/**
|
||||
* The CLI daemon port is not available. Could not establish the gRPC connection between the backend and the CLI.
|
||||
*/
|
||||
| 'daemon'
|
||||
/**
|
||||
* Cloud not connect to the Internet from the browser.
|
||||
*/
|
||||
| 'internet';
|
||||
|
||||
@injectable()
|
||||
export class ApplicationConnectionStatusContribution extends TheiaApplicationConnectionStatusContribution {
|
||||
@inject(DaemonPort)
|
||||
private readonly daemonPort: DaemonPort;
|
||||
@inject(IsOnline)
|
||||
private readonly isOnline: IsOnline;
|
||||
@inject(MessageService)
|
||||
private readonly messageService: MessageService;
|
||||
@inject(NotificationManager)
|
||||
private readonly notificationManager: NotificationManager;
|
||||
@inject(CreateFeatures)
|
||||
private readonly createFeatures: CreateFeatures;
|
||||
|
||||
private readonly offlineStatusDidChangeEmitter = new Emitter<
|
||||
OfflineConnectionStatus | undefined
|
||||
>();
|
||||
private noInternetConnectionNotificationId: string | undefined;
|
||||
private _offlineStatus: OfflineConnectionStatus | undefined;
|
||||
|
||||
get offlineStatus(): OfflineConnectionStatus | undefined {
|
||||
return this._offlineStatus;
|
||||
}
|
||||
|
||||
get onOfflineStatusDidChange(): Event<OfflineConnectionStatus | undefined> {
|
||||
return this.offlineStatusDidChangeEmitter.event;
|
||||
}
|
||||
|
||||
protected override onStateChange(state: ConnectionStatus): void {
|
||||
if (!this.connectedPort && state === ConnectionStatus.ONLINE) {
|
||||
if (
|
||||
(!Boolean(this.daemonPort.port) || !this.isOnline.online) &&
|
||||
state === ConnectionStatus.ONLINE
|
||||
) {
|
||||
return;
|
||||
}
|
||||
super.onStateChange(state);
|
||||
}
|
||||
|
||||
protected override handleOffline(): void {
|
||||
this.statusBar.setElement('connection-status', {
|
||||
const params = {
|
||||
port: this.daemonPort.port,
|
||||
online: this.isOnline.online,
|
||||
};
|
||||
this._offlineStatus = offlineConnectionStatusType(params);
|
||||
const { text, tooltip } = offlineMessage(params);
|
||||
this.statusBar.setElement(connectionStatusStatusBar, {
|
||||
alignment: StatusBarAlignment.LEFT,
|
||||
text: this.connectedPort
|
||||
? nls.localize('theia/core/offline', 'Offline')
|
||||
: '$(bolt) ' +
|
||||
nls.localize('theia/core/daemonOffline', 'CLI Daemon Offline'),
|
||||
tooltip: this.connectedPort
|
||||
? nls.localize(
|
||||
'theia/core/cannotConnectBackend',
|
||||
'Cannot connect to the backend.'
|
||||
)
|
||||
: nls.localize(
|
||||
'theia/core/cannotConnectDaemon',
|
||||
'Cannot connect to the CLI daemon.'
|
||||
),
|
||||
text,
|
||||
tooltip,
|
||||
priority: 5000,
|
||||
});
|
||||
this.toDisposeOnOnline.push(
|
||||
Disposable.create(() => this.statusBar.removeElement('connection-status'))
|
||||
);
|
||||
document.body.classList.add('theia-mod-offline');
|
||||
this.toDisposeOnOnline.push(
|
||||
document.body.classList.add(theiaOffline);
|
||||
this.toDisposeOnOnline.pushAll([
|
||||
Disposable.create(() =>
|
||||
document.body.classList.remove('theia-mod-offline')
|
||||
)
|
||||
);
|
||||
this.statusBar.removeElement(connectionStatusStatusBar)
|
||||
),
|
||||
Disposable.create(() => document.body.classList.remove(theiaOffline)),
|
||||
Disposable.create(() => {
|
||||
this._offlineStatus = undefined;
|
||||
this.fireStatusDidChange();
|
||||
}),
|
||||
]);
|
||||
if (!this.isOnline.online) {
|
||||
const text = nls.localize(
|
||||
'arduino/connectionStatus/connectionLost',
|
||||
"Connection lost. Cloud sketch actions and updates won't be available."
|
||||
);
|
||||
this.noInternetConnectionNotificationId = this.notificationManager[
|
||||
'getMessageId'
|
||||
]({ text, type: MessageType.Warning });
|
||||
if (this.createFeatures.enabled) {
|
||||
this.messageService.warn(text);
|
||||
}
|
||||
this.toDisposeOnOnline.push(
|
||||
Disposable.create(() => this.clearNoInternetConnectionNotification())
|
||||
);
|
||||
}
|
||||
this.fireStatusDidChange();
|
||||
}
|
||||
|
||||
private clearNoInternetConnectionNotification(): void {
|
||||
if (this.noInternetConnectionNotificationId) {
|
||||
this.notificationManager.clear(this.noInternetConnectionNotificationId);
|
||||
this.noInternetConnectionNotificationId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private fireStatusDidChange(): void {
|
||||
if (this.createFeatures.enabled) {
|
||||
return this.offlineStatusDidChangeEmitter.fire(this._offlineStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface OfflineMessageParams {
|
||||
readonly port: string | undefined;
|
||||
readonly online: boolean;
|
||||
}
|
||||
interface OfflineMessage {
|
||||
readonly text: string;
|
||||
readonly tooltip: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* (non-API) exported for testing
|
||||
*
|
||||
* The precedence of the offline states are the following:
|
||||
* - No connection to the Theia backend,
|
||||
* - CLI daemon is offline, and
|
||||
* - There is no Internet connection.
|
||||
*/
|
||||
export function offlineMessage(params: OfflineMessageParams): OfflineMessage {
|
||||
const statusType = offlineConnectionStatusType(params);
|
||||
const text = getOfflineText(statusType);
|
||||
const tooltip = getOfflineTooltip(statusType);
|
||||
return { text, tooltip };
|
||||
}
|
||||
|
||||
function offlineConnectionStatusType(
|
||||
params: OfflineMessageParams
|
||||
): OfflineConnectionStatus {
|
||||
const { port, online } = params;
|
||||
if (port && online) {
|
||||
return 'backend';
|
||||
}
|
||||
if (!port) {
|
||||
return 'daemon';
|
||||
}
|
||||
return 'internet';
|
||||
}
|
||||
|
||||
export const backendOfflineText = nls.localize('theia/core/offline', 'Offline');
|
||||
export const daemonOfflineText = nls.localize(
|
||||
'theia/core/daemonOffline',
|
||||
'CLI Daemon Offline'
|
||||
);
|
||||
export const offlineText = nls.localize('theia/core/offlineText', 'Offline');
|
||||
export const backendOfflineTooltip = nls.localize(
|
||||
'theia/core/cannotConnectBackend',
|
||||
'Cannot connect to the backend.'
|
||||
);
|
||||
export const daemonOfflineTooltip = nls.localize(
|
||||
'theia/core/cannotConnectDaemon',
|
||||
'Cannot connect to the CLI daemon.'
|
||||
);
|
||||
export const offlineTooltip = offlineText;
|
||||
|
||||
function getOfflineText(statusType: OfflineConnectionStatus): string {
|
||||
switch (statusType) {
|
||||
case 'backend':
|
||||
return backendOfflineText;
|
||||
case 'daemon':
|
||||
return '$(bolt) ' + daemonOfflineText;
|
||||
case 'internet':
|
||||
return '$(alert) ' + offlineText;
|
||||
default:
|
||||
assertUnreachable(statusType);
|
||||
}
|
||||
}
|
||||
|
||||
function getOfflineTooltip(statusType: OfflineConnectionStatus): string {
|
||||
switch (statusType) {
|
||||
case 'backend':
|
||||
return backendOfflineTooltip;
|
||||
case 'daemon':
|
||||
return daemonOfflineTooltip;
|
||||
case 'internet':
|
||||
return offlineTooltip;
|
||||
default:
|
||||
assertUnreachable(statusType);
|
||||
}
|
||||
}
|
||||
|
225
arduino-ide-extension/src/browser/theia/core/hover-service.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
// Copied from https://github.com/eclipse-theia/theia/commit/909f4106e8c15c5c2c320401da4f48f8c6080734
|
||||
// Remove when IDE2 uses 1.32.0
|
||||
|
||||
import { animationFrame } from '@theia/core/lib/browser/browser';
|
||||
import {
|
||||
MarkdownRenderer,
|
||||
MarkdownRendererFactory,
|
||||
} from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
|
||||
import { PreferenceService } from '@theia/core/lib/browser/preferences/preference-service';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
disposableTimeout,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering/markdown-string';
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import '../../../../src/browser/style/hover-service.css';
|
||||
|
||||
export type HoverPosition = 'left' | 'right' | 'top' | 'bottom';
|
||||
|
||||
export namespace HoverPosition {
|
||||
export function invertIfNecessary(
|
||||
position: HoverPosition,
|
||||
target: DOMRect,
|
||||
host: DOMRect,
|
||||
totalWidth: number,
|
||||
totalHeight: number
|
||||
): HoverPosition {
|
||||
if (position === 'left') {
|
||||
if (target.left - host.width - 5 < 0) {
|
||||
return 'right';
|
||||
}
|
||||
} else if (position === 'right') {
|
||||
if (target.right + host.width + 5 > totalWidth) {
|
||||
return 'left';
|
||||
}
|
||||
} else if (position === 'top') {
|
||||
if (target.top - host.height - 5 < 0) {
|
||||
return 'bottom';
|
||||
}
|
||||
} else if (position === 'bottom') {
|
||||
if (target.bottom + host.height + 5 > totalHeight) {
|
||||
return 'top';
|
||||
}
|
||||
}
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
||||
export interface HoverRequest {
|
||||
content: string | MarkdownString | HTMLElement;
|
||||
target: HTMLElement;
|
||||
/**
|
||||
* The position where the hover should appear.
|
||||
* Note that the hover service will try to invert the position (i.e. right -> left)
|
||||
* if the specified content does not fit in the window next to the target element
|
||||
*/
|
||||
position: HoverPosition;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class HoverService {
|
||||
protected static hostClassName = 'theia-hover';
|
||||
protected static styleSheetId = 'theia-hover-style';
|
||||
@inject(PreferenceService) protected readonly preferences: PreferenceService;
|
||||
@inject(MarkdownRendererFactory)
|
||||
protected readonly markdownRendererFactory: MarkdownRendererFactory;
|
||||
|
||||
protected _markdownRenderer: MarkdownRenderer | undefined;
|
||||
protected get markdownRenderer(): MarkdownRenderer {
|
||||
this._markdownRenderer ||= this.markdownRendererFactory();
|
||||
return this._markdownRenderer;
|
||||
}
|
||||
|
||||
protected _hoverHost: HTMLElement | undefined;
|
||||
protected get hoverHost(): HTMLElement {
|
||||
if (!this._hoverHost) {
|
||||
this._hoverHost = document.createElement('div');
|
||||
this._hoverHost.classList.add(HoverService.hostClassName);
|
||||
this._hoverHost.style.position = 'absolute';
|
||||
}
|
||||
return this._hoverHost;
|
||||
}
|
||||
protected pendingTimeout: Disposable | undefined;
|
||||
protected hoverTarget: HTMLElement | undefined;
|
||||
protected lastHidHover = Date.now();
|
||||
protected readonly disposeOnHide = new DisposableCollection();
|
||||
|
||||
requestHover(request: HoverRequest): void {
|
||||
if (request.target !== this.hoverTarget) {
|
||||
this.cancelHover();
|
||||
this.pendingTimeout = disposableTimeout(
|
||||
() => this.renderHover(request),
|
||||
this.getHoverDelay()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected getHoverDelay(): number {
|
||||
return Date.now() - this.lastHidHover < 200
|
||||
? 0
|
||||
: this.preferences.get('workbench.hover.delay', isOSX ? 1500 : 500);
|
||||
}
|
||||
|
||||
protected async renderHover(request: HoverRequest): Promise<void> {
|
||||
const host = this.hoverHost;
|
||||
const { target, content, position } = request;
|
||||
this.hoverTarget = target;
|
||||
if (content instanceof HTMLElement) {
|
||||
host.appendChild(content);
|
||||
} else if (typeof content === 'string') {
|
||||
host.textContent = content;
|
||||
} else {
|
||||
const renderedContent = this.markdownRenderer.render(content);
|
||||
this.disposeOnHide.push(renderedContent);
|
||||
host.appendChild(renderedContent.element);
|
||||
}
|
||||
// browsers might insert linebreaks when the hover appears at the edge of the window
|
||||
// resetting the position prevents that
|
||||
host.style.left = '0px';
|
||||
host.style.top = '0px';
|
||||
document.body.append(host);
|
||||
await animationFrame(); // Allow the browser to size the host
|
||||
const updatedPosition = this.setHostPosition(target, host, position);
|
||||
|
||||
this.disposeOnHide.push({
|
||||
dispose: () => {
|
||||
this.lastHidHover = Date.now();
|
||||
host.classList.remove(updatedPosition);
|
||||
},
|
||||
});
|
||||
|
||||
this.listenForMouseOut();
|
||||
}
|
||||
|
||||
protected setHostPosition(
|
||||
target: HTMLElement,
|
||||
host: HTMLElement,
|
||||
position: HoverPosition
|
||||
): HoverPosition {
|
||||
const targetDimensions = target.getBoundingClientRect();
|
||||
const hostDimensions = host.getBoundingClientRect();
|
||||
const documentWidth = document.body.getBoundingClientRect().width;
|
||||
// document.body.getBoundingClientRect().height doesn't work as expected
|
||||
// scrollHeight will always be accurate here: https://stackoverflow.com/a/44077777
|
||||
const documentHeight = document.documentElement.scrollHeight - 22; // --theia-statusBar-height: 22px;
|
||||
position = HoverPosition.invertIfNecessary(
|
||||
position,
|
||||
targetDimensions,
|
||||
hostDimensions,
|
||||
documentWidth,
|
||||
documentHeight
|
||||
);
|
||||
if (position === 'top' || position === 'bottom') {
|
||||
const targetMiddleWidth =
|
||||
targetDimensions.left + targetDimensions.width / 2;
|
||||
const middleAlignment = targetMiddleWidth - hostDimensions.width / 2;
|
||||
const furthestRight = Math.min(
|
||||
documentWidth - hostDimensions.width,
|
||||
middleAlignment
|
||||
);
|
||||
const left = Math.max(0, furthestRight);
|
||||
const top =
|
||||
position === 'top'
|
||||
? targetDimensions.top - hostDimensions.height - 5
|
||||
: targetDimensions.bottom + 5;
|
||||
host.style.setProperty(
|
||||
'--theia-hover-before-position',
|
||||
`${targetMiddleWidth - left - 5}px`
|
||||
);
|
||||
host.style.top = `${top}px`;
|
||||
host.style.left = `${left}px`;
|
||||
} else {
|
||||
const targetMiddleHeight =
|
||||
targetDimensions.top + targetDimensions.height / 2;
|
||||
const middleAlignment = targetMiddleHeight - hostDimensions.height / 2;
|
||||
const furthestTop = Math.min(
|
||||
documentHeight - hostDimensions.height,
|
||||
middleAlignment
|
||||
);
|
||||
const top = Math.max(0, furthestTop);
|
||||
const left =
|
||||
position === 'left'
|
||||
? targetDimensions.left - hostDimensions.width - 5
|
||||
: targetDimensions.right + 5;
|
||||
host.style.setProperty(
|
||||
'--theia-hover-before-position',
|
||||
`${targetMiddleHeight - top - 5}px`
|
||||
);
|
||||
host.style.left = `${left}px`;
|
||||
host.style.top = `${top}px`;
|
||||
}
|
||||
host.classList.add(position);
|
||||
return position;
|
||||
}
|
||||
|
||||
protected listenForMouseOut(): void {
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (
|
||||
e.target instanceof Node &&
|
||||
!this.hoverHost.contains(e.target) &&
|
||||
!this.hoverTarget?.contains(e.target)
|
||||
) {
|
||||
this.cancelHover();
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
this.disposeOnHide.push({
|
||||
dispose: () => document.removeEventListener('mousemove', handleMouseMove),
|
||||
});
|
||||
}
|
||||
|
||||
cancelHover(): void {
|
||||
this.pendingTimeout?.dispose();
|
||||
this.unRenderHover();
|
||||
this.disposeOnHide.dispose();
|
||||
this.hoverTarget = undefined;
|
||||
}
|
||||
|
||||
protected unRenderHover(): void {
|
||||
this.hoverHost.remove();
|
||||
this.hoverHost.replaceChildren();
|
||||
}
|
||||
}
|
@@ -0,0 +1,91 @@
|
||||
import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget';
|
||||
import type { SidebarMenu } from '@theia/core/lib/browser/shell/sidebar-menu-widget';
|
||||
import type { MenuPath } from '@theia/core/lib/common/menu';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { accountMenu } from '../../contributions/account';
|
||||
import { CreateFeatures } from '../../create/create-features';
|
||||
import { ApplicationConnectionStatusContribution } from './connection-status-service';
|
||||
|
||||
@injectable()
|
||||
export class SidebarBottomMenuWidget extends TheiaSidebarBottomMenuWidget {
|
||||
@inject(CreateFeatures)
|
||||
private readonly createFeatures: CreateFeatures;
|
||||
@inject(ApplicationConnectionStatusContribution)
|
||||
private readonly connectionStatue: ApplicationConnectionStatusContribution;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.toDispose.pushAll([
|
||||
this.createFeatures.onDidChangeSession(() => this.update()),
|
||||
this.connectionStatue.onOfflineStatusDidChange(() => this.update()),
|
||||
]);
|
||||
}
|
||||
|
||||
protected override onClick(
|
||||
e: React.MouseEvent<HTMLElement, MouseEvent>,
|
||||
menuPath: MenuPath
|
||||
): void {
|
||||
const button = e.currentTarget.getBoundingClientRect();
|
||||
const options = {
|
||||
menuPath,
|
||||
includeAnchorArg: false,
|
||||
anchor: {
|
||||
x: button.left + button.width,
|
||||
// Bogus y coordinate?
|
||||
// https://github.com/eclipse-theia/theia/discussions/12170
|
||||
y: button.top,
|
||||
},
|
||||
showDisabled: true,
|
||||
};
|
||||
this.contextMenuRenderer.render(options);
|
||||
}
|
||||
|
||||
protected override render(): React.ReactNode {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.menus.map((menu) => this.renderMenu(menu))}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private renderMenu(menu: SidebarMenu): React.ReactNode {
|
||||
// Removes the _Settings_ (cog) icon from the left sidebar
|
||||
if (menu.id === 'settings-menu') {
|
||||
return undefined;
|
||||
}
|
||||
const arduinoAccount = menu.id === accountMenu.id;
|
||||
const picture =
|
||||
arduinoAccount &&
|
||||
this.connectionStatue.offlineStatus !== 'internet' &&
|
||||
this.createFeatures.session?.account.picture;
|
||||
const className = typeof picture === 'string' ? undefined : menu.iconClass;
|
||||
return (
|
||||
<i
|
||||
key={menu.id}
|
||||
className={className}
|
||||
title={menu.title}
|
||||
onClick={(e) => this.onClick(e, menu.menuPath)}
|
||||
onMouseDown={this.onMouseDown}
|
||||
onMouseOut={this.onMouseOut}
|
||||
>
|
||||
{picture && (
|
||||
<div className="account-icon">
|
||||
<img
|
||||
src={picture}
|
||||
alt={nls.localize(
|
||||
'arduino/cloud/profilePicture',
|
||||
'Profile picture'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</i>
|
||||
);
|
||||
}
|
||||
}
|
@@ -23,6 +23,11 @@ export class TabBarRenderer extends TheiaTabBarRenderer {
|
||||
}
|
||||
|
||||
export class ToolbarAwareTabBar extends TheiaToolbarAwareTabBar {
|
||||
protected override async updateBreadcrumbs(): Promise<void> {
|
||||
// NOOP
|
||||
// IDE2 does not use breadcrumbs.
|
||||
}
|
||||
|
||||
private readonly doUpdateToolbar = debounce(() => super.updateToolbar(), 500);
|
||||
protected override updateToolbar(): void {
|
||||
// Unlike Theia, IDE2 debounces the toolbar updates with 500ms
|
||||
|
@@ -1,18 +1,28 @@
|
||||
import * as remote from '@theia/core/electron-shared/@electron/remote';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
||||
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable-types';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { Widget } from '@theia/core/lib/browser/widgets/widget';
|
||||
import { WindowTitleUpdater as TheiaWindowTitleUpdater } from '@theia/core/lib/browser/window/window-title-updater';
|
||||
import { ApplicationServer } from '@theia/core/lib/common/application-protocol';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
import {
|
||||
inject,
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { ConfigServiceClient } from '../../config/config-service-client';
|
||||
import { CreateFeatures } from '../../create/create-features';
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../sketches-service-client-impl';
|
||||
|
||||
@injectable()
|
||||
export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
|
||||
@@ -22,12 +32,22 @@ export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
|
||||
private readonly applicationShell: ApplicationShell;
|
||||
@inject(WorkspaceService)
|
||||
private readonly workspaceService: WorkspaceService;
|
||||
|
||||
private _previousRepresentedFilename: string | undefined;
|
||||
@inject(SketchesServiceClientImpl)
|
||||
private readonly sketchesServiceClient: SketchesServiceClientImpl;
|
||||
@inject(ConfigServiceClient)
|
||||
private readonly configServiceClient: ConfigServiceClient;
|
||||
@inject(CreateFeatures)
|
||||
private readonly createFeatures: CreateFeatures;
|
||||
@inject(EditorManager)
|
||||
private readonly editorManager: EditorManager;
|
||||
|
||||
private readonly applicationName =
|
||||
FrontendApplicationConfigProvider.get().applicationName;
|
||||
private readonly toDispose = new DisposableCollection();
|
||||
|
||||
private previousRepresentedFilename: string | undefined;
|
||||
private applicationVersion: string | undefined;
|
||||
private hasCloudPrefix: boolean | undefined;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
@@ -43,6 +63,22 @@ export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
|
||||
);
|
||||
}
|
||||
|
||||
override onStart(app: FrontendApplication): void {
|
||||
super.onStart(app);
|
||||
this.toDispose.pushAll([
|
||||
this.sketchesServiceClient.onCurrentSketchDidChange(() =>
|
||||
this.maybeSetCloudPrefix()
|
||||
),
|
||||
this.configServiceClient.onDidChangeDataDirUri(() =>
|
||||
this.maybeSetCloudPrefix()
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
protected override handleWidgetChange(widget?: Widget | undefined): void {
|
||||
if (isOSX) {
|
||||
this.maybeUpdateRepresentedFilename(widget);
|
||||
@@ -54,7 +90,7 @@ export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
|
||||
|
||||
protected override updateTitleWidget(widget?: Widget | undefined): void {
|
||||
let activeEditorShort = '';
|
||||
const rootName = this.workspaceService.workspace?.name ?? '';
|
||||
let rootName = this.workspaceService.workspace?.name ?? '';
|
||||
let appName = `${this.applicationName}${
|
||||
this.applicationVersion ? ` ${this.applicationVersion}` : ''
|
||||
}`;
|
||||
@@ -69,6 +105,12 @@ export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
|
||||
activeEditorShort = ` - ${base} `;
|
||||
}
|
||||
}
|
||||
if (this.hasCloudPrefix) {
|
||||
rootName = `[${nls.localize(
|
||||
'arduino/title/cloud',
|
||||
'Cloud'
|
||||
)}] ${rootName}`;
|
||||
}
|
||||
this.windowTitleService.update({ rootName, appName, activeEditorShort });
|
||||
}
|
||||
|
||||
@@ -77,10 +119,32 @@ export class WindowTitleUpdater extends TheiaWindowTitleUpdater {
|
||||
const { uri } = widget.editor;
|
||||
const filename = uri.path.toString();
|
||||
// Do not necessarily require the current window if not needed. It's a synchronous, blocking call.
|
||||
if (this._previousRepresentedFilename !== filename) {
|
||||
if (this.previousRepresentedFilename !== filename) {
|
||||
const currentWindow = remote.getCurrentWindow();
|
||||
currentWindow.setRepresentedFilename(uri.path.toString());
|
||||
this._previousRepresentedFilename = filename;
|
||||
this.previousRepresentedFilename = filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private maybeSetCloudPrefix(): void {
|
||||
if (typeof this.hasCloudPrefix === 'boolean') {
|
||||
return;
|
||||
}
|
||||
const sketch = this.sketchesServiceClient.tryGetCurrentSketch();
|
||||
if (!CurrentSketch.isValid(sketch)) {
|
||||
return;
|
||||
}
|
||||
const dataDirUri = this.configServiceClient.tryGetDataDirUri();
|
||||
if (!dataDirUri) {
|
||||
return;
|
||||
}
|
||||
this.hasCloudPrefix = this.createFeatures.isCloud(sketch, dataDirUri);
|
||||
if (typeof this.hasCloudPrefix === 'boolean') {
|
||||
const editor =
|
||||
this.editorManager.activeEditor ?? this.editorManager.currentEditor;
|
||||
if (editor) {
|
||||
this.updateTitleWidget(editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { CancellationToken } from '@theia/core/lib/common/cancellation';
|
||||
import type {
|
||||
Message,
|
||||
ProgressMessage,
|
||||
ProgressUpdate,
|
||||
} from '@theia/core/lib/common/message-service-protocol';
|
||||
@@ -46,11 +45,4 @@ export class NotificationManager extends TheiaNotificationManager {
|
||||
}
|
||||
return Math.min((update.work.done / update.work.total) * 100, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* For `public` visibility.
|
||||
*/
|
||||
override getMessageId(message: Message): string {
|
||||
return super.getMessageId(message);
|
||||
}
|
||||
}
|
||||
|
@@ -19,8 +19,7 @@ export class ArduinoToolbarContainer extends Widget {
|
||||
this.toolbars = toolbars;
|
||||
}
|
||||
|
||||
override onAfterAttach(msg: Message): void {
|
||||
super.onAfterAttach(msg);
|
||||
override onAfterAttach(msg: Message) {
|
||||
for (const toolbar of this.toolbars) {
|
||||
Widget.attach(toolbar, this.node);
|
||||
}
|
||||
@@ -57,11 +56,9 @@ export class ArduinoToolbarContribution
|
||||
);
|
||||
}
|
||||
|
||||
onStart(app: FrontendApplication): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const options = <any>{
|
||||
area: 'toolbar',
|
||||
};
|
||||
app.shell.addWidget(this.arduinoToolbarContainer, options);
|
||||
onStart(app: FrontendApplication) {
|
||||
app.shell.addWidget(this.arduinoToolbarContainer, {
|
||||
area: 'top',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,88 @@
|
||||
import { TreeNode } from '@theia/core/lib/browser/tree';
|
||||
import { Command } from '@theia/core/lib/common/command';
|
||||
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
|
||||
|
||||
export namespace CloudSketchbookCommands {
|
||||
export interface Arg {
|
||||
model: CloudSketchbookTreeModel;
|
||||
node: TreeNode;
|
||||
event?: MouseEvent;
|
||||
}
|
||||
export namespace Arg {
|
||||
export function is(arg: unknown): arg is Arg {
|
||||
return (
|
||||
typeof arg === 'object' &&
|
||||
(<Arg>arg).model !== undefined &&
|
||||
(<Arg>arg).model instanceof CloudSketchbookTreeModel &&
|
||||
(<Arg>arg).node !== undefined &&
|
||||
TreeNode.is((<Arg>arg).node)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const TOGGLE_CLOUD_SKETCHBOOK = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-cloud-sketchbook--disable',
|
||||
label: 'Show/Hide Cloud Sketchbook',
|
||||
},
|
||||
'arduino/cloud/showHideSketchbook'
|
||||
);
|
||||
|
||||
export const PULL_SKETCH = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-cloud-sketchbook--pull-sketch',
|
||||
label: 'Pull Sketch',
|
||||
iconClass: 'fa fa-arduino-cloud-download',
|
||||
},
|
||||
'arduino/cloud/pullSketch'
|
||||
);
|
||||
|
||||
export const PUSH_SKETCH = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-cloud-sketchbook--push-sketch',
|
||||
label: 'Push Sketch',
|
||||
iconClass: 'fa fa-arduino-cloud-upload',
|
||||
},
|
||||
'arduino/cloud/pullSketch'
|
||||
);
|
||||
|
||||
export const PULL_SKETCH__TOOLBAR = {
|
||||
...PULL_SKETCH,
|
||||
id: `${PULL_SKETCH.id}-toolbar`,
|
||||
};
|
||||
|
||||
export const PUSH_SKETCH__TOOLBAR = {
|
||||
...PUSH_SKETCH,
|
||||
id: `${PUSH_SKETCH.id}-toolbar`,
|
||||
};
|
||||
|
||||
export const OPEN_IN_CLOUD_EDITOR = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-cloud-sketchbook--open-in-cloud-editor',
|
||||
label: 'Open in Cloud Editor',
|
||||
},
|
||||
'arduino/cloud/openInCloudEditor'
|
||||
);
|
||||
|
||||
export const OPEN_SKETCHBOOKSYNC_CONTEXT_MENU = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-sketchbook-sync--open-sketch-context-menu',
|
||||
label: 'Options...',
|
||||
iconClass: 'sketchbook-tree__opts',
|
||||
},
|
||||
'arduino/cloud/options'
|
||||
);
|
||||
|
||||
export const OPEN_SKETCH_SHARE_DIALOG = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-cloud-sketchbook--share-modal',
|
||||
label: 'Share...',
|
||||
},
|
||||
'arduino/cloud/share'
|
||||
);
|
||||
|
||||
export const OPEN_PROFILE_CONTEXT_MENU: Command = {
|
||||
id: 'arduino-cloud-sketchbook--open-profile-menu',
|
||||
label: 'Contextual menu',
|
||||
};
|
||||
}
|
@@ -5,7 +5,7 @@ import {
|
||||
injectable,
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { UserStatus } from './cloud-user-status';
|
||||
import { CloudStatus } from './cloud-status';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { CloudSketchbookTreeWidget } from './cloud-sketchbook-tree-widget';
|
||||
import { AuthenticationClientService } from '../../auth/authentication-client-service';
|
||||
@@ -13,6 +13,7 @@ import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
|
||||
import { BaseSketchbookCompositeWidget } from '../sketchbook/sketchbook-composite-widget';
|
||||
import { CreateNew } from '../sketchbook/create-new';
|
||||
import { AuthenticationSession } from '../../../node/auth/types';
|
||||
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
|
||||
|
||||
@injectable()
|
||||
export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidget<CloudSketchbookTreeWidget> {
|
||||
@@ -20,6 +21,9 @@ export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidge
|
||||
private readonly authenticationService: AuthenticationClientService;
|
||||
@inject(CloudSketchbookTreeWidget)
|
||||
private readonly cloudSketchbookTreeWidget: CloudSketchbookTreeWidget;
|
||||
@inject(ApplicationConnectionStatusContribution)
|
||||
private readonly connectionStatus: ApplicationConnectionStatusContribution;
|
||||
|
||||
private _session: AuthenticationSession | undefined;
|
||||
|
||||
constructor() {
|
||||
@@ -61,11 +65,12 @@ export class CloudSketchbookCompositeWidget extends BaseSketchbookCompositeWidge
|
||||
onClick={this.onDidClickCreateNew}
|
||||
/>
|
||||
)}
|
||||
<UserStatus
|
||||
<CloudStatus
|
||||
model={
|
||||
this.cloudSketchbookTreeWidget.model as CloudSketchbookTreeModel
|
||||
}
|
||||
authenticationService={this.authenticationService}
|
||||
connectionStatus={this.connectionStatus}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@@ -1,156 +1,94 @@
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { TreeNode } from '@theia/core/lib/browser/tree';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { Command, CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import {
|
||||
ContextMenuRenderer,
|
||||
RenderContextMenuOptions,
|
||||
} from '@theia/core/lib/browser';
|
||||
} from '@theia/core/lib/browser/context-menu-renderer';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import {
|
||||
PreferenceScope,
|
||||
PreferenceService,
|
||||
} from '@theia/core/lib/browser/preferences/preference-service';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
|
||||
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
|
||||
import { CloudUserCommands } from '../../auth/cloud-user-commands';
|
||||
import { ShareSketchDialog } from '../../dialogs/cloud-share-sketch-dialog';
|
||||
import { CreateApi } from '../../create/create-api';
|
||||
import {
|
||||
PreferenceService,
|
||||
PreferenceScope,
|
||||
} from '@theia/core/lib/browser/preferences/preference-service';
|
||||
import { ArduinoMenus, PlaceholderMenuNode } from '../../menu/arduino-menus';
|
||||
import { SketchbookCommands } from '../sketchbook/sketchbook-commands';
|
||||
import {
|
||||
CurrentSketch,
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../sketches-service-client-impl';
|
||||
import { Contribution } from '../../contributions/contribution';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { ArduinoPreferences } from '../../arduino-preferences';
|
||||
import { MainMenuManager } from '../../../common/main-menu-manager';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { ConfigServiceClient } from '../../config/config-service-client';
|
||||
import { CloudSketchContribution } from '../../contributions/cloud-contribution';
|
||||
import {
|
||||
Sketch,
|
||||
TabBarToolbarRegistry,
|
||||
} from '../../contributions/contribution';
|
||||
import { ShareSketchDialog } from '../../dialogs/cloud-share-sketch-dialog';
|
||||
import { ArduinoMenus, PlaceholderMenuNode } from '../../menu/arduino-menus';
|
||||
import { CurrentSketch } from '../../sketches-service-client-impl';
|
||||
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
|
||||
import { SketchbookCommands } from '../sketchbook/sketchbook-commands';
|
||||
import { CloudSketchbookCommands } from './cloud-sketchbook-commands';
|
||||
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
|
||||
import { CreateUri } from '../../create/create-uri';
|
||||
|
||||
export const SKETCHBOOKSYNC__CONTEXT = ['arduino-sketchbook-sync--context'];
|
||||
const SKETCHBOOKSYNC__CONTEXT = ['arduino-sketchbook-sync--context'];
|
||||
|
||||
// `Open Folder`, `Open in New Window`
|
||||
export const SKETCHBOOKSYNC__CONTEXT__MAIN_GROUP = [
|
||||
const SKETCHBOOKSYNC__CONTEXT__MAIN_GROUP = [
|
||||
...SKETCHBOOKSYNC__CONTEXT,
|
||||
'0_main',
|
||||
];
|
||||
|
||||
export const CLOUD_USER__CONTEXT = ['arduino-cloud-user--context'];
|
||||
export const CLOUD_USER__CONTEXT__USERNAME = [
|
||||
...CLOUD_USER__CONTEXT,
|
||||
'0_username',
|
||||
];
|
||||
export const CLOUD_USER__CONTEXT__MAIN_GROUP = [
|
||||
...CLOUD_USER__CONTEXT,
|
||||
'1_main',
|
||||
];
|
||||
|
||||
export namespace CloudSketchbookCommands {
|
||||
export interface Arg {
|
||||
model: CloudSketchbookTreeModel;
|
||||
node: TreeNode;
|
||||
event?: MouseEvent;
|
||||
}
|
||||
export namespace Arg {
|
||||
export function is(arg: Partial<Arg> | undefined): arg is Arg {
|
||||
return (
|
||||
!!arg && !!arg.node && arg.model instanceof CloudSketchbookTreeModel
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const TOGGLE_CLOUD_SKETCHBOOK = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-cloud-sketchbook--disable',
|
||||
label: 'Show/Hide Cloud Sketchbook',
|
||||
},
|
||||
'arduino/cloud/showHideSketchbook'
|
||||
);
|
||||
|
||||
export const PULL_SKETCH = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-cloud-sketchbook--pull-sketch',
|
||||
label: 'Pull Sketch',
|
||||
iconClass: 'pull-sketch-icon',
|
||||
},
|
||||
'arduino/cloud/pullSketch'
|
||||
);
|
||||
|
||||
export const PUSH_SKETCH = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-cloud-sketchbook--push-sketch',
|
||||
label: 'Push Sketch',
|
||||
iconClass: 'push-sketch-icon',
|
||||
},
|
||||
'arduino/cloud/pullSketch'
|
||||
);
|
||||
|
||||
export const OPEN_IN_CLOUD_EDITOR = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-cloud-sketchbook--open-in-cloud-editor',
|
||||
label: 'Open in Cloud Editor',
|
||||
},
|
||||
'arduino/cloud/openInCloudEditor'
|
||||
);
|
||||
|
||||
export const OPEN_SKETCHBOOKSYNC_CONTEXT_MENU = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-sketchbook-sync--open-sketch-context-menu',
|
||||
label: 'Options...',
|
||||
iconClass: 'sketchbook-tree__opts',
|
||||
},
|
||||
'arduino/cloud/options'
|
||||
);
|
||||
|
||||
export const OPEN_SKETCH_SHARE_DIALOG = Command.toLocalizedCommand(
|
||||
{
|
||||
id: 'arduino-cloud-sketchbook--share-modal',
|
||||
label: 'Share...',
|
||||
},
|
||||
'arduino/cloud/share'
|
||||
);
|
||||
|
||||
export const OPEN_PROFILE_CONTEXT_MENU: Command = {
|
||||
id: 'arduino-cloud-sketchbook--open-profile-menu',
|
||||
label: 'Contextual menu',
|
||||
};
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class CloudSketchbookContribution extends Contribution {
|
||||
@inject(FileService)
|
||||
protected readonly fileService: FileService;
|
||||
|
||||
export class CloudSketchbookContribution extends CloudSketchContribution {
|
||||
@inject(ContextMenuRenderer)
|
||||
protected readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
private readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
@inject(MenuModelRegistry)
|
||||
protected readonly menuRegistry: MenuModelRegistry;
|
||||
|
||||
@inject(SketchesServiceClientImpl)
|
||||
protected readonly sketchServiceClient: SketchesServiceClientImpl;
|
||||
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
@inject(CommandRegistry)
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
@inject(WindowService)
|
||||
protected readonly windowService: WindowService;
|
||||
|
||||
@inject(CreateApi)
|
||||
protected readonly createApi: CreateApi;
|
||||
|
||||
private readonly windowService: WindowService;
|
||||
@inject(ArduinoPreferences)
|
||||
protected readonly arduinoPreferences: ArduinoPreferences;
|
||||
|
||||
private readonly arduinoPreferences: ArduinoPreferences;
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferenceService: PreferenceService;
|
||||
private readonly preferenceService: PreferenceService;
|
||||
@inject(ConfigServiceClient)
|
||||
private readonly configServiceClient: ConfigServiceClient;
|
||||
@inject(ApplicationConnectionStatusContribution)
|
||||
private readonly connectionStatus: ApplicationConnectionStatusContribution;
|
||||
|
||||
@inject(MainMenuManager)
|
||||
protected readonly mainMenuManager: MainMenuManager;
|
||||
private readonly onDidChangeToolbarEmitter = new Emitter<void>();
|
||||
private readonly toDisposeBeforeNewContextMenu = new DisposableCollection();
|
||||
private readonly toDisposeOnStop = new DisposableCollection(
|
||||
this.onDidChangeToolbarEmitter,
|
||||
this.toDisposeBeforeNewContextMenu
|
||||
);
|
||||
private shell: ApplicationShell | undefined;
|
||||
|
||||
protected readonly toDisposeBeforeNewContextMenu = new DisposableCollection();
|
||||
override onStart(app: FrontendApplication): void {
|
||||
this.shell = app.shell;
|
||||
this.toDisposeOnStop.pushAll([
|
||||
this.connectionStatus.onOfflineStatusDidChange((offlineStatus) => {
|
||||
if (!offlineStatus || offlineStatus === 'internet') {
|
||||
this.fireToolbarChange();
|
||||
}
|
||||
}),
|
||||
this.createFeatures.onDidChangeSession(() => this.fireToolbarChange()),
|
||||
this.createFeatures.onDidChangeEnabled(() => this.fireToolbarChange()),
|
||||
this.createFeatures.onDidChangeCloudSketchState(() =>
|
||||
this.fireToolbarChange()
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDisposeOnStop.dispose();
|
||||
}
|
||||
|
||||
override registerMenus(menus: MenuModelRegistry): void {
|
||||
menus.registerMenuAction(ArduinoMenus.FILE__ADVANCED_SUBMENU, {
|
||||
@@ -160,6 +98,23 @@ export class CloudSketchbookContribution extends Contribution {
|
||||
});
|
||||
}
|
||||
|
||||
override registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: CloudSketchbookCommands.PULL_SKETCH__TOOLBAR.id,
|
||||
command: CloudSketchbookCommands.PULL_SKETCH__TOOLBAR.id,
|
||||
tooltip: CloudSketchbookCommands.PULL_SKETCH__TOOLBAR.label,
|
||||
priority: -2,
|
||||
onDidChange: this.onDidChangeToolbar,
|
||||
});
|
||||
registry.registerItem({
|
||||
id: CloudSketchbookCommands.PUSH_SKETCH__TOOLBAR.id,
|
||||
command: CloudSketchbookCommands.PUSH_SKETCH__TOOLBAR.id,
|
||||
tooltip: CloudSketchbookCommands.PUSH_SKETCH__TOOLBAR.label,
|
||||
priority: -1,
|
||||
onDidChange: this.onDidChangeToolbar,
|
||||
});
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(CloudSketchbookCommands.TOGGLE_CLOUD_SKETCHBOOK, {
|
||||
execute: () => {
|
||||
@@ -169,32 +124,41 @@ export class CloudSketchbookContribution extends Contribution {
|
||||
PreferenceScope.User
|
||||
);
|
||||
},
|
||||
isEnabled: () => true,
|
||||
isVisible: () => true,
|
||||
});
|
||||
|
||||
registry.registerCommand(CloudSketchbookCommands.PULL_SKETCH, {
|
||||
execute: (arg) => arg.model.sketchbookTree().pull(arg),
|
||||
isEnabled: (arg) =>
|
||||
CloudSketchbookCommands.Arg.is(arg) &&
|
||||
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
|
||||
isVisible: (arg) =>
|
||||
CloudSketchbookCommands.Arg.is(arg) &&
|
||||
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
|
||||
isEnabled: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
|
||||
isVisible: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
|
||||
});
|
||||
|
||||
registry.registerCommand(CloudSketchbookCommands.PUSH_SKETCH, {
|
||||
execute: (arg) => arg.model.sketchbookTree().push(arg.node),
|
||||
isEnabled: (arg) =>
|
||||
CloudSketchbookCommands.Arg.is(arg) &&
|
||||
CloudSketchbookTree.CloudSketchDirNode.is(arg.node) &&
|
||||
this.isCloudSketchDirNodeCommandArg(arg) &&
|
||||
CloudSketchbookTree.CloudSketchTreeNode.isSynced(arg.node),
|
||||
isVisible: (arg) =>
|
||||
CloudSketchbookCommands.Arg.is(arg) &&
|
||||
CloudSketchbookTree.CloudSketchDirNode.is(arg.node) &&
|
||||
this.isCloudSketchDirNodeCommandArg(arg) &&
|
||||
CloudSketchbookTree.CloudSketchTreeNode.isSynced(arg.node),
|
||||
});
|
||||
|
||||
registry.registerCommand(CloudSketchbookCommands.PUSH_SKETCH__TOOLBAR, {
|
||||
execute: () =>
|
||||
this.executeDelegateWithCurrentSketch(
|
||||
CloudSketchbookCommands.PUSH_SKETCH.id
|
||||
),
|
||||
isEnabled: (arg) => this.isEnabledCloudSketchToolbar(arg),
|
||||
isVisible: (arg) => this.isVisibleCloudSketchToolbar(arg),
|
||||
});
|
||||
registry.registerCommand(CloudSketchbookCommands.PULL_SKETCH__TOOLBAR, {
|
||||
execute: () =>
|
||||
this.executeDelegateWithCurrentSketch(
|
||||
CloudSketchbookCommands.PULL_SKETCH.id
|
||||
),
|
||||
isEnabled: (arg) => this.isEnabledCloudSketchToolbar(arg),
|
||||
isVisible: (arg) => this.isVisibleCloudSketchToolbar(arg),
|
||||
});
|
||||
|
||||
registry.registerCommand(CloudSketchbookCommands.OPEN_IN_CLOUD_EDITOR, {
|
||||
execute: (arg) => {
|
||||
this.windowService.openNewWindow(
|
||||
@@ -202,12 +166,8 @@ export class CloudSketchbookContribution extends Contribution {
|
||||
{ external: true }
|
||||
);
|
||||
},
|
||||
isEnabled: (arg) =>
|
||||
CloudSketchbookCommands.Arg.is(arg) &&
|
||||
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
|
||||
isVisible: (arg) =>
|
||||
CloudSketchbookCommands.Arg.is(arg) &&
|
||||
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
|
||||
isEnabled: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
|
||||
isVisible: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
|
||||
});
|
||||
|
||||
registry.registerCommand(CloudSketchbookCommands.OPEN_SKETCH_SHARE_DIALOG, {
|
||||
@@ -218,12 +178,8 @@ export class CloudSketchbookContribution extends Contribution {
|
||||
createApi: this.createApi,
|
||||
}).open();
|
||||
},
|
||||
isEnabled: (arg) =>
|
||||
CloudSketchbookCommands.Arg.is(arg) &&
|
||||
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
|
||||
isVisible: (arg) =>
|
||||
CloudSketchbookCommands.Arg.is(arg) &&
|
||||
CloudSketchbookTree.CloudSketchDirNode.is(arg.node),
|
||||
isEnabled: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
|
||||
isVisible: (arg) => this.isCloudSketchDirNodeCommandArg(arg),
|
||||
});
|
||||
|
||||
registry.registerCommand(
|
||||
@@ -327,53 +283,118 @@ export class CloudSketchbookContribution extends Contribution {
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
registry.registerCommand(CloudUserCommands.OPEN_PROFILE_CONTEXT_MENU, {
|
||||
execute: async (arg) => {
|
||||
this.toDisposeBeforeNewContextMenu.dispose();
|
||||
const container = arg.event.target;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
private get currentCloudSketch(): Sketch | undefined {
|
||||
const currentSketch = this.sketchServiceClient.tryGetCurrentSketch();
|
||||
// could not load sketch via CLI
|
||||
if (!CurrentSketch.isValid(currentSketch)) {
|
||||
return undefined;
|
||||
}
|
||||
// cannot determine if the sketch is in the cloud cache folder
|
||||
const dataDirUri = this.configServiceClient.tryGetDataDirUri();
|
||||
if (!dataDirUri) {
|
||||
return undefined;
|
||||
}
|
||||
// sketch is not in the cache folder
|
||||
if (!this.createFeatures.isCloud(currentSketch, dataDirUri)) {
|
||||
return undefined;
|
||||
}
|
||||
return currentSketch;
|
||||
}
|
||||
|
||||
this.menuRegistry.registerMenuAction(CLOUD_USER__CONTEXT__MAIN_GROUP, {
|
||||
commandId: CloudUserCommands.LOGOUT.id,
|
||||
label: CloudUserCommands.LOGOUT.label,
|
||||
});
|
||||
this.toDisposeBeforeNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuAction(CloudUserCommands.LOGOUT)
|
||||
)
|
||||
private isVisibleCloudSketchToolbar(arg: unknown): boolean {
|
||||
// cloud preference is disabled
|
||||
if (!this.createFeatures.enabled) {
|
||||
return false;
|
||||
}
|
||||
if (!this.currentCloudSketch) {
|
||||
return false;
|
||||
}
|
||||
if (arg instanceof Widget) {
|
||||
return !!this.shell && this.shell.getWidgets('main').indexOf(arg) !== -1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private isEnabledCloudSketchToolbar(arg: unknown): boolean {
|
||||
if (!this.isVisibleCloudSketchToolbar(arg)) {
|
||||
return false;
|
||||
}
|
||||
// not logged in
|
||||
if (!this.createFeatures.session) {
|
||||
return false;
|
||||
}
|
||||
// no Internet connection
|
||||
if (this.connectionStatus.offlineStatus === 'internet') {
|
||||
return false;
|
||||
}
|
||||
// no pull/push context for the current cloud sketch
|
||||
const sketch = this.currentCloudSketch;
|
||||
if (sketch) {
|
||||
const cloudUri = this.createFeatures.cloudUri(sketch);
|
||||
if (cloudUri) {
|
||||
return !this.createFeatures.cloudSketchState(
|
||||
CreateUri.toUri(cloudUri.path.toString())
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const placeholder = new PlaceholderMenuNode(
|
||||
CLOUD_USER__CONTEXT__USERNAME,
|
||||
arg.username
|
||||
);
|
||||
this.menuRegistry.registerMenuNode(
|
||||
CLOUD_USER__CONTEXT__USERNAME,
|
||||
placeholder
|
||||
);
|
||||
this.toDisposeBeforeNewContextMenu.push(
|
||||
Disposable.create(() =>
|
||||
this.menuRegistry.unregisterMenuNode(placeholder.id)
|
||||
)
|
||||
);
|
||||
private isCloudSketchDirNodeCommandArg(
|
||||
arg: unknown
|
||||
): arg is CloudSketchbookCommands.Arg & {
|
||||
node: CloudSketchbookTree.CloudSketchDirNode;
|
||||
} {
|
||||
return (
|
||||
CloudSketchbookCommands.Arg.is(arg) &&
|
||||
CloudSketchbookTree.CloudSketchDirNode.is(arg.node) &&
|
||||
!this.createFeatures.cloudSketchState(arg.node.remoteUri)
|
||||
);
|
||||
}
|
||||
|
||||
const options: RenderContextMenuOptions = {
|
||||
menuPath: CLOUD_USER__CONTEXT,
|
||||
anchor: {
|
||||
x: container.getBoundingClientRect().left,
|
||||
y:
|
||||
container.getBoundingClientRect().top -
|
||||
3.5 * container.offsetHeight,
|
||||
},
|
||||
args: [arg],
|
||||
};
|
||||
this.contextMenuRenderer.render(options);
|
||||
},
|
||||
});
|
||||
private async commandArgFromCurrentSketch(): Promise<
|
||||
CloudSketchbookCommands.Arg | undefined
|
||||
> {
|
||||
const sketch = this.currentCloudSketch;
|
||||
if (!sketch) {
|
||||
return undefined;
|
||||
}
|
||||
const model = await this.treeModel();
|
||||
if (!model) {
|
||||
return undefined;
|
||||
}
|
||||
const cloudUri = this.createFeatures.cloudUri(sketch);
|
||||
if (!cloudUri) {
|
||||
return undefined;
|
||||
}
|
||||
const posixPath = cloudUri.path.toString();
|
||||
const node = model.getNode(posixPath);
|
||||
if (CloudSketchbookTree.CloudSketchDirNode.is(node)) {
|
||||
return { model, node };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.registerMenus(this.menuRegistry);
|
||||
private async executeDelegateWithCurrentSketch(id: string): Promise<unknown> {
|
||||
const arg = await this.commandArgFromCurrentSketch();
|
||||
if (!arg) {
|
||||
return;
|
||||
}
|
||||
if (!this.commandRegistry.getActiveHandler(id, arg)) {
|
||||
throw new Error(
|
||||
`No active handler was available for the delegate command: ${id}. Cloud sketch tree node: ${arg.node.id}`
|
||||
);
|
||||
}
|
||||
return this.commandRegistry.executeCommand(id, arg);
|
||||
}
|
||||
|
||||
private fireToolbarChange(): void {
|
||||
this.onDidChangeToolbarEmitter.fire();
|
||||
}
|
||||
|
||||
private get onDidChangeToolbar(): Event<void> {
|
||||
return this.onDidChangeToolbarEmitter.event;
|
||||
}
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ import URI from '@theia/core/lib/common/uri';
|
||||
import { Create } from '../../create/typings';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
|
||||
|
||||
function sketchBaseDir(sketch: Create.Sketch): FileStat {
|
||||
// extract the sketch path
|
||||
@@ -63,15 +64,22 @@ export class CloudSketchbookTreeModel extends SketchbookTreeModel {
|
||||
private readonly authenticationService: AuthenticationClientService;
|
||||
@inject(LocalCacheFsProvider)
|
||||
private readonly localCacheFsProvider: LocalCacheFsProvider;
|
||||
@inject(ApplicationConnectionStatusContribution)
|
||||
private readonly connectionStatus: ApplicationConnectionStatusContribution;
|
||||
|
||||
private _localCacheFsProviderReady: Deferred<void> | undefined;
|
||||
|
||||
@postConstruct()
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.toDispose.push(
|
||||
this.authenticationService.onSessionDidChange(() => this.updateRoot())
|
||||
);
|
||||
this.toDispose.pushAll([
|
||||
this.authenticationService.onSessionDidChange(() => this.updateRoot()),
|
||||
this.connectionStatus.onOfflineStatusDidChange((offlineStatus) => {
|
||||
if (!offlineStatus) {
|
||||
this.updateRoot();
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
override *getNodesByUri(uri: URI): IterableIterator<TreeNode> {
|
||||
|
@@ -5,16 +5,17 @@ import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
|
||||
import { AuthenticationClientService } from '../../auth/authentication-client-service';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { CloudSketchbookTree } from './cloud-sketchbook-tree';
|
||||
import { CloudUserCommands } from '../../auth/cloud-user-commands';
|
||||
import {
|
||||
CloudUserCommands,
|
||||
LEARN_MORE_URL,
|
||||
} from '../../auth/cloud-user-commands';
|
||||
import { NodeProps } from '@theia/core/lib/browser/tree/tree-widget';
|
||||
import { TreeNode } from '@theia/core/lib/browser/tree';
|
||||
import { CompositeTreeNode } from '@theia/core/lib/browser';
|
||||
import { shell } from '@theia/core/electron-shared/@electron/remote';
|
||||
import { SketchbookTreeWidget } from '../sketchbook/sketchbook-tree-widget';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
const LEARN_MORE_URL =
|
||||
'https://docs.arduino.cc/software/ide-v2/tutorials/ide-v2-cloud-sketch-sync';
|
||||
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
|
||||
|
||||
@injectable()
|
||||
export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
|
||||
@@ -27,6 +28,9 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
|
||||
@inject(CloudSketchbookTree)
|
||||
protected readonly cloudSketchbookTree: CloudSketchbookTree;
|
||||
|
||||
@inject(ApplicationConnectionStatusContribution)
|
||||
private readonly connectionStatus: ApplicationConnectionStatusContribution;
|
||||
|
||||
protected override renderTree(model: TreeModel): React.ReactNode {
|
||||
if (this.shouldShowWelcomeView()) return this.renderViewWelcome();
|
||||
if (this.shouldShowEmptyView()) return this.renderEmptyView();
|
||||
@@ -91,10 +95,33 @@ export class CloudSketchbookTreeWidget extends SketchbookTreeWidget {
|
||||
return classNames;
|
||||
}
|
||||
|
||||
protected override renderIcon(
|
||||
node: TreeNode,
|
||||
props: NodeProps
|
||||
): React.ReactNode {
|
||||
if (CloudSketchbookTree.CloudSketchDirNode.is(node)) {
|
||||
const synced = CloudSketchbookTree.CloudSketchTreeNode.isSynced(node);
|
||||
const offline = this.connectionStatus.offlineStatus === 'internet';
|
||||
const icon = `fa fa-arduino-cloud${synced ? '-filled' : ''}${
|
||||
offline ? '-offline' : ''
|
||||
}`;
|
||||
return (
|
||||
<div
|
||||
className={`theia-file-icons-js file-icon${
|
||||
!synced && offline ? ' not-in-sync-offline' : ''
|
||||
}`}
|
||||
>
|
||||
<div className={icon} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return super.renderIcon(node, props);
|
||||
}
|
||||
|
||||
protected override renderInlineCommands(node: any): React.ReactNode {
|
||||
if (CloudSketchbookTree.CloudSketchDirNode.is(node) && node.commands) {
|
||||
return Array.from(new Set(node.commands)).map((command) =>
|
||||
this.renderInlineCommand(command.id, node, {
|
||||
this.renderInlineCommand(command, node, {
|
||||
username: this.authenticationService.session?.account?.label,
|
||||
})
|
||||
);
|
||||
|
@@ -22,15 +22,22 @@ import {
|
||||
LocalCacheFsProvider,
|
||||
LocalCacheUri,
|
||||
} from '../../local-cache/local-cache-fs-provider';
|
||||
import { CloudSketchbookCommands } from './cloud-sketchbook-contributions';
|
||||
import { CloudSketchbookCommands } from './cloud-sketchbook-commands';
|
||||
import { DoNotAskAgainConfirmDialog } from '../../dialogs/do-not-ask-again-dialog';
|
||||
import { SketchbookTree } from '../sketchbook/sketchbook-tree';
|
||||
import { firstToUpperCase } from '../../../common/utils';
|
||||
import { assertUnreachable } from '../../../common/utils';
|
||||
import { FileStat } from '@theia/filesystem/lib/common/files';
|
||||
import { WorkspaceNode } from '@theia/navigator/lib/browser/navigator-tree';
|
||||
import { posix, splitSketchPath } from '../../create/create-paths';
|
||||
import { Create } from '../../create/typings';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
|
||||
import { ExecuteWithProgress } from '../../../common/protocol/progressible';
|
||||
import {
|
||||
pullingSketch,
|
||||
pushingSketch,
|
||||
} from '../../contributions/cloud-contribution';
|
||||
import { CloudSketchState, CreateFeatures } from '../../create/create-features';
|
||||
|
||||
const MESSAGE_TIMEOUT = 5 * 1000;
|
||||
const deepmerge = require('deepmerge').default;
|
||||
@@ -54,6 +61,19 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
@inject(CreateApi)
|
||||
private readonly createApi: CreateApi;
|
||||
|
||||
@inject(ApplicationConnectionStatusContribution)
|
||||
private readonly connectionStatus: ApplicationConnectionStatusContribution;
|
||||
|
||||
@inject(CreateFeatures)
|
||||
private readonly createFeatures: CreateFeatures;
|
||||
|
||||
protected override init(): void {
|
||||
this.toDispose.push(
|
||||
this.connectionStatus.onOfflineStatusDidChange(() => this.refresh())
|
||||
);
|
||||
super.init();
|
||||
}
|
||||
|
||||
async pushPublicWarn(
|
||||
node: CloudSketchbookTree.CloudSketchDirNode
|
||||
): Promise<boolean> {
|
||||
@@ -84,7 +104,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
|
||||
async pull(arg: any): Promise<void> {
|
||||
async pull(arg: any, noProgress = false): Promise<void> {
|
||||
const {
|
||||
// model,
|
||||
node,
|
||||
@@ -118,32 +138,45 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return this.runWithState(node, 'pulling', async (node) => {
|
||||
const commandsCopy = node.commands;
|
||||
node.commands = [];
|
||||
|
||||
const localUri = await this.fileService.toUnderlyingResource(
|
||||
LocalCacheUri.root.resolve(node.remoteUri.path)
|
||||
);
|
||||
await this.sync(node.remoteUri, localUri);
|
||||
|
||||
this.createApi.sketchCache.purgeByPath(node.remoteUri.path.toString());
|
||||
|
||||
node.commands = commandsCopy;
|
||||
this.messageService.info(
|
||||
nls.localize(
|
||||
'arduino/cloud/donePulling',
|
||||
'Done pulling ‘{0}’.',
|
||||
node.fileStat.name
|
||||
),
|
||||
{
|
||||
timeout: MESSAGE_TIMEOUT,
|
||||
}
|
||||
);
|
||||
});
|
||||
return this.runWithState(
|
||||
node,
|
||||
'pull',
|
||||
async (node) => {
|
||||
await this.pullNode(node);
|
||||
},
|
||||
noProgress
|
||||
);
|
||||
}
|
||||
|
||||
async push(node: CloudSketchbookTree.CloudSketchDirNode): Promise<void> {
|
||||
private async pullNode(node: CloudSketchbookTree.CloudSketchDirNode) {
|
||||
const commandsCopy = node.commands;
|
||||
node.commands = [];
|
||||
|
||||
const localUri = await this.fileService.toUnderlyingResource(
|
||||
LocalCacheUri.root.resolve(node.remoteUri.path)
|
||||
);
|
||||
await this.sync(node.remoteUri, localUri);
|
||||
|
||||
this.createApi.sketchCache.purgeByPath(node.remoteUri.path.toString());
|
||||
|
||||
node.commands = commandsCopy;
|
||||
this.messageService.info(
|
||||
nls.localize(
|
||||
'arduino/cloud/donePulling',
|
||||
"Done pulling '{0}'.",
|
||||
node.fileStat.name
|
||||
),
|
||||
{
|
||||
timeout: MESSAGE_TIMEOUT,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async push(
|
||||
node: CloudSketchbookTree.CloudSketchDirNode,
|
||||
noProgress = false,
|
||||
ignorePushWarnings = false
|
||||
): Promise<void> {
|
||||
if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
|
||||
throw new Error(
|
||||
nls.localize(
|
||||
@@ -158,7 +191,8 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
return;
|
||||
}
|
||||
|
||||
const warn = this.arduinoPreferences['arduino.cloud.push.warn'];
|
||||
const warn =
|
||||
!ignorePushWarnings && this.arduinoPreferences['arduino.cloud.push.warn'];
|
||||
|
||||
if (warn) {
|
||||
const ok = await new DoNotAskAgainConfirmDialog({
|
||||
@@ -178,37 +212,46 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return this.runWithState(node, 'pushing', async (node) => {
|
||||
if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
|
||||
throw new Error(
|
||||
nls.localize(
|
||||
'arduino/cloud/pullFirst',
|
||||
'You have to pull first to be able to push to the Cloud.'
|
||||
)
|
||||
);
|
||||
}
|
||||
const commandsCopy = node.commands;
|
||||
node.commands = [];
|
||||
return this.runWithState(
|
||||
node,
|
||||
'push',
|
||||
async (node) => {
|
||||
await this.pushNode(node);
|
||||
},
|
||||
noProgress
|
||||
);
|
||||
}
|
||||
|
||||
const localUri = await this.fileService.toUnderlyingResource(
|
||||
LocalCacheUri.root.resolve(node.remoteUri.path)
|
||||
);
|
||||
await this.sync(localUri, node.remoteUri);
|
||||
|
||||
this.createApi.sketchCache.purgeByPath(node.remoteUri.path.toString());
|
||||
|
||||
node.commands = commandsCopy;
|
||||
this.messageService.info(
|
||||
private async pushNode(node: CloudSketchbookTree.CloudSketchDirNode) {
|
||||
if (!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)) {
|
||||
throw new Error(
|
||||
nls.localize(
|
||||
'arduino/cloud/donePushing',
|
||||
'Done pushing ‘{0}’.',
|
||||
node.fileStat.name
|
||||
),
|
||||
{
|
||||
timeout: MESSAGE_TIMEOUT,
|
||||
}
|
||||
'arduino/cloud/pullFirst',
|
||||
'You have to pull first to be able to push to the Cloud.'
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
const commandsCopy = node.commands;
|
||||
node.commands = [];
|
||||
|
||||
const localUri = await this.fileService.toUnderlyingResource(
|
||||
LocalCacheUri.root.resolve(node.remoteUri.path)
|
||||
);
|
||||
await this.sync(localUri, node.remoteUri);
|
||||
|
||||
this.createApi.sketchCache.purgeByPath(node.remoteUri.path.toString());
|
||||
|
||||
node.commands = commandsCopy;
|
||||
this.messageService.info(
|
||||
nls.localize(
|
||||
'arduino/cloud/donePushing',
|
||||
"Done pushing '{0}'.",
|
||||
node.fileStat.name
|
||||
),
|
||||
{
|
||||
timeout: MESSAGE_TIMEOUT,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async recursiveURIs(uri: URI): Promise<URI[]> {
|
||||
@@ -310,31 +353,37 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
|
||||
private async runWithState<T>(
|
||||
node: CloudSketchbookTree.CloudSketchDirNode & Partial<DecoratedTreeNode>,
|
||||
state: CloudSketchbookTree.CloudSketchDirNode.State,
|
||||
task: (node: CloudSketchbookTree.CloudSketchDirNode) => MaybePromise<T>
|
||||
state: CloudSketchState,
|
||||
task: (node: CloudSketchbookTree.CloudSketchDirNode) => MaybePromise<T>,
|
||||
noProgress = false
|
||||
): Promise<T> {
|
||||
const decoration: WidgetDecoration.TailDecoration = {
|
||||
data: `${firstToUpperCase(state)}...`,
|
||||
fontData: {
|
||||
color: 'var(--theia-list-highlightForeground)',
|
||||
},
|
||||
};
|
||||
this.createFeatures.setCloudSketchState(node.remoteUri, state);
|
||||
try {
|
||||
node.state = state;
|
||||
this.mergeDecoration(node, { tailDecorations: [decoration] });
|
||||
const result = await (noProgress
|
||||
? task(node)
|
||||
: ExecuteWithProgress.withProgress(
|
||||
this.taskMessage(state, node.uri.path.name),
|
||||
this.messageService,
|
||||
async (progress) => {
|
||||
progress.report({ work: { done: 0, total: NaN } });
|
||||
return task(node);
|
||||
}
|
||||
));
|
||||
await this.refresh(node);
|
||||
const result = await task(node);
|
||||
return result;
|
||||
} finally {
|
||||
delete node.state;
|
||||
// TODO: find a better way to attach and detach decorators. Do we need a proper `TreeDecorator` instead?
|
||||
const index = node.decorationData?.tailDecorations?.findIndex(
|
||||
(candidate) => JSON.stringify(decoration) === JSON.stringify(candidate)
|
||||
);
|
||||
if (typeof index === 'number' && index !== -1) {
|
||||
node.decorationData?.tailDecorations?.splice(index, 1);
|
||||
}
|
||||
await this.refresh(node);
|
||||
this.createFeatures.setCloudSketchState(node.remoteUri, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private taskMessage(state: CloudSketchState, input: string): string {
|
||||
switch (state) {
|
||||
case 'pull':
|
||||
return pullingSketch(input);
|
||||
case 'push':
|
||||
return pushingSketch(input);
|
||||
default:
|
||||
assertUnreachable(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,7 +550,7 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
};
|
||||
}
|
||||
|
||||
protected readonly notInSyncDecoration: WidgetDecoration.Data = {
|
||||
protected readonly notInSyncOfflineDecoration: WidgetDecoration.Data = {
|
||||
fontData: {
|
||||
color: 'var(--theia-activityBar-inactiveForeground)',
|
||||
},
|
||||
@@ -522,11 +571,15 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
node.fileStat.resource.path.toString()
|
||||
);
|
||||
|
||||
const commands = [CloudSketchbookCommands.PULL_SKETCH];
|
||||
const commands: Command[] = [];
|
||||
if (this.connectionStatus.offlineStatus !== 'internet') {
|
||||
commands.push(CloudSketchbookCommands.PULL_SKETCH);
|
||||
}
|
||||
|
||||
if (
|
||||
CloudSketchbookTree.CloudSketchTreeNode.is(node) &&
|
||||
CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)
|
||||
CloudSketchbookTree.CloudSketchTreeNode.isSynced(node) &&
|
||||
this.connectionStatus.offlineStatus !== 'internet'
|
||||
) {
|
||||
commands.push(CloudSketchbookCommands.PUSH_SKETCH);
|
||||
}
|
||||
@@ -557,14 +610,15 @@ export class CloudSketchbookTree extends SketchbookTree {
|
||||
}
|
||||
}
|
||||
|
||||
// add style decoration for not-in-sync files
|
||||
// add style decoration for not-in-sync files when offline
|
||||
if (
|
||||
CloudSketchbookTree.CloudSketchTreeNode.is(node) &&
|
||||
!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node)
|
||||
!CloudSketchbookTree.CloudSketchTreeNode.isSynced(node) &&
|
||||
this.connectionStatus.offlineStatus === 'internet'
|
||||
) {
|
||||
this.mergeDecoration(node, this.notInSyncDecoration);
|
||||
this.mergeDecoration(node, this.notInSyncOfflineDecoration);
|
||||
} else {
|
||||
this.removeDecoration(node, this.notInSyncDecoration);
|
||||
this.removeDecoration(node, this.notInSyncOfflineDecoration);
|
||||
}
|
||||
|
||||
return node;
|
||||
@@ -644,7 +698,7 @@ export namespace CloudSketchbookTree {
|
||||
export interface CloudSketchDirNode
|
||||
extends Omit<SketchbookTree.SketchDirNode, 'fileStat'>,
|
||||
CloudSketchTreeNode {
|
||||
state?: CloudSketchDirNode.State;
|
||||
state?: CloudSketchState;
|
||||
isPublic?: boolean;
|
||||
sketchId?: string;
|
||||
commands?: Command[];
|
||||
@@ -653,7 +707,5 @@ export namespace CloudSketchbookTree {
|
||||
export function is(node: TreeNode | undefined): node is CloudSketchDirNode {
|
||||
return SketchbookTree.SketchDirNode.is(node);
|
||||
}
|
||||
|
||||
export type State = 'syncing' | 'pulling' | 'pushing';
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,101 @@
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
|
||||
import { AuthenticationClientService } from '../../auth/authentication-client-service';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { ApplicationConnectionStatusContribution } from '../../theia/core/connection-status-service';
|
||||
|
||||
export class CloudStatus extends React.Component<
|
||||
CloudStatus.Props,
|
||||
CloudStatus.State
|
||||
> {
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
constructor(props: CloudStatus.Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
status: this.status,
|
||||
refreshing: false,
|
||||
};
|
||||
}
|
||||
|
||||
override componentDidMount(): void {
|
||||
this.toDispose.push(
|
||||
this.props.connectionStatus.onOfflineStatusDidChange(() =>
|
||||
this.setState({ status: this.status })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override componentWillUnmount(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
if (!this.props.authenticationService.session) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="cloud-connection-status flex-line">
|
||||
<div className="status item flex-line">
|
||||
<div
|
||||
className={`${
|
||||
this.state.status === 'connected'
|
||||
? 'connected-status-icon'
|
||||
: 'offline-status-icon'
|
||||
}`}
|
||||
/>
|
||||
{this.state.status === 'connected'
|
||||
? nls.localize('arduino/cloud/connected', 'Connected')
|
||||
: nls.localize('arduino/cloud/offline', 'Offline')}
|
||||
</div>
|
||||
<div className="actions item flex-line">
|
||||
{this.props.connectionStatus.offlineStatus === 'internet' ? (
|
||||
<div
|
||||
className="fa fa-arduino-cloud-offline"
|
||||
title={nls.localize('arduino/cloud/offline', 'Offline')}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
title={nls.localize('arduino/cloud/sync', 'Sync')}
|
||||
className={`fa fa-reload ${
|
||||
(this.state.refreshing && 'rotating') || ''
|
||||
}`}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={this.onDidClickRefresh}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onDidClickRefresh = () => {
|
||||
this.setState({ refreshing: true });
|
||||
Promise.all([
|
||||
this.props.model.updateRoot(),
|
||||
new Promise((resolve) => setTimeout(() => resolve(true), 1000)),
|
||||
]).then(() => {
|
||||
this.props.model.sketchbookTree().refresh();
|
||||
this.setState({ refreshing: false });
|
||||
});
|
||||
};
|
||||
|
||||
private get status(): 'connected' | 'offline' {
|
||||
return this.props.connectionStatus.offlineStatus === 'internet'
|
||||
? 'offline'
|
||||
: 'connected';
|
||||
}
|
||||
}
|
||||
|
||||
export namespace CloudStatus {
|
||||
export interface Props {
|
||||
readonly model: CloudSketchbookTreeModel;
|
||||
readonly authenticationService: AuthenticationClientService;
|
||||
readonly connectionStatus: ApplicationConnectionStatusContribution;
|
||||
}
|
||||
export interface State {
|
||||
status: 'connected' | 'offline';
|
||||
refreshing?: boolean;
|
||||
}
|
||||
}
|
@@ -1,134 +0,0 @@
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { CloudSketchbookTreeModel } from './cloud-sketchbook-tree-model';
|
||||
import { AuthenticationClientService } from '../../auth/authentication-client-service';
|
||||
import { CloudUserCommands } from '../../auth/cloud-user-commands';
|
||||
import { AuthenticationSessionAccountInformation } from '../../../common/protocol/authentication-service';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
export class UserStatus extends React.Component<
|
||||
UserStatus.Props,
|
||||
UserStatus.State
|
||||
> {
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
constructor(props: UserStatus.Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
status: this.status,
|
||||
accountInfo: props.authenticationService.session?.account,
|
||||
refreshing: false,
|
||||
};
|
||||
}
|
||||
|
||||
override componentDidMount(): void {
|
||||
const statusListener = () => this.setState({ status: this.status });
|
||||
window.addEventListener('online', statusListener);
|
||||
window.addEventListener('offline', statusListener);
|
||||
this.toDispose.pushAll([
|
||||
this.props.authenticationService.onSessionDidChange((session) =>
|
||||
this.setState({ accountInfo: session?.account })
|
||||
),
|
||||
Disposable.create(() =>
|
||||
window.removeEventListener('online', statusListener)
|
||||
),
|
||||
Disposable.create(() =>
|
||||
window.removeEventListener('offline', statusListener)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
override componentWillUnmount(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
if (!this.props.authenticationService.session) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="cloud-connection-status flex-line">
|
||||
<div className="status item flex-line">
|
||||
<div
|
||||
className={`${
|
||||
this.state.status === 'connected'
|
||||
? 'connected-status-icon'
|
||||
: 'offline-status-icon'
|
||||
}`}
|
||||
/>
|
||||
{this.state.status === 'connected'
|
||||
? nls.localize('arduino/cloud/connected', 'Connected')
|
||||
: nls.localize('arduino/cloud/offline', 'Offline')}
|
||||
</div>
|
||||
<div className="actions item flex-line">
|
||||
<div
|
||||
title={nls.localize('arduino/cloud/sync', 'Sync')}
|
||||
className={`fa fa-reload ${
|
||||
(this.state.refreshing && 'rotating') || ''
|
||||
}`}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={this.onDidClickRefresh}
|
||||
/>
|
||||
</div>
|
||||
<div className="account item flex-line">
|
||||
<div
|
||||
title={nls.localize('arduino/cloud/account', 'Account')}
|
||||
className="account-icon"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.props.model.commandRegistry.executeCommand(
|
||||
CloudUserCommands.OPEN_PROFILE_CONTEXT_MENU.id,
|
||||
{
|
||||
event: event.nativeEvent,
|
||||
username: this.state.accountInfo?.label,
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
{this.state.accountInfo?.picture && (
|
||||
<img
|
||||
src={this.state.accountInfo?.picture}
|
||||
alt={nls.localize(
|
||||
'arduino/cloud/profilePicture',
|
||||
'Profile picture'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onDidClickRefresh = () => {
|
||||
this.setState({ refreshing: true });
|
||||
Promise.all([
|
||||
this.props.model.updateRoot(),
|
||||
new Promise((resolve) => setTimeout(() => resolve(true), 1000)),
|
||||
]).then(() => {
|
||||
this.props.model.sketchbookTree().refresh();
|
||||
this.setState({ refreshing: false });
|
||||
});
|
||||
};
|
||||
|
||||
private get status(): 'connected' | 'offline' {
|
||||
return window.navigator.onLine ? 'connected' : 'offline';
|
||||
}
|
||||
}
|
||||
|
||||
export namespace UserStatus {
|
||||
export interface Props {
|
||||
readonly model: CloudSketchbookTreeModel;
|
||||
readonly authenticationService: AuthenticationClientService;
|
||||
}
|
||||
export interface State {
|
||||
status: 'connected' | 'offline';
|
||||
accountInfo?: AuthenticationSessionAccountInformation;
|
||||
refreshing?: boolean;
|
||||
}
|
||||
}
|
@@ -1,60 +1,77 @@
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import type { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { Installable } from '../../../common/protocol/installable';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { ListItemRenderer } from './list-item-renderer';
|
||||
import type { ListItemRenderer } from './list-item-renderer';
|
||||
import { UserAbortError } from './list-widget';
|
||||
|
||||
export class ComponentListItem<
|
||||
T extends ArduinoComponent
|
||||
> extends React.Component<ComponentListItem.Props<T>, ComponentListItem.State> {
|
||||
constructor(props: ComponentListItem.Props<T>) {
|
||||
super(props);
|
||||
if (props.item.installable) {
|
||||
const version = props.item.availableVersions.filter(
|
||||
(version) => version !== props.item.installedVersion
|
||||
)[0];
|
||||
this.state = {
|
||||
selectedVersion: version,
|
||||
};
|
||||
}
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const { item, itemRenderer } = this.props;
|
||||
const selectedVersion =
|
||||
this.props.edited?.item.name === item.name
|
||||
? this.props.edited.selectedVersion
|
||||
: this.latestVersion;
|
||||
return (
|
||||
<>
|
||||
{itemRenderer.renderItem(
|
||||
Object.assign(this.state, { item }),
|
||||
this.install.bind(this),
|
||||
this.uninstall.bind(this),
|
||||
this.onVersionChange.bind(this)
|
||||
)}
|
||||
{itemRenderer.renderItem({
|
||||
item,
|
||||
selectedVersion,
|
||||
inProgress: this.state.inProgress,
|
||||
isScrolling: this.props.isScrolling,
|
||||
install: (item) => this.install(item),
|
||||
uninstall: (item) => this.uninstall(item),
|
||||
onVersionChange: (version) => this.onVersionChange(version),
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private async install(item: T): Promise<void> {
|
||||
const toInstall = this.state.selectedVersion;
|
||||
const version = this.props.item.availableVersions.filter(
|
||||
(version) => version !== this.state.selectedVersion
|
||||
)[0];
|
||||
this.setState({
|
||||
selectedVersion: version,
|
||||
});
|
||||
try {
|
||||
await this.props.install(item, toInstall);
|
||||
} catch {
|
||||
this.setState({
|
||||
selectedVersion: toInstall,
|
||||
});
|
||||
}
|
||||
await this.withState('installing', () =>
|
||||
this.props.install(
|
||||
item,
|
||||
this.props.edited?.item.name === item.name
|
||||
? this.props.edited.selectedVersion
|
||||
: Installable.latest(this.props.item.availableVersions)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async uninstall(item: T): Promise<void> {
|
||||
await this.props.uninstall(item);
|
||||
await this.withState('uninstalling', () => this.props.uninstall(item));
|
||||
}
|
||||
|
||||
private async withState(
|
||||
inProgress: 'installing' | 'uninstalling',
|
||||
task: () => Promise<unknown>
|
||||
): Promise<void> {
|
||||
this.setState({ inProgress });
|
||||
try {
|
||||
await task();
|
||||
} catch (err) {
|
||||
if (err instanceof UserAbortError) {
|
||||
// No state update when user cancels the task
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
this.setState({ inProgress: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
private onVersionChange(version: Installable.Version): void {
|
||||
this.setState({ selectedVersion: version });
|
||||
this.props.onItemEdit(this.props.item, version);
|
||||
}
|
||||
|
||||
private get latestVersion(): Installable.Version | undefined {
|
||||
return Installable.latest(this.props.item.availableVersions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,10 +80,19 @@ export namespace ComponentListItem {
|
||||
readonly item: T;
|
||||
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
|
||||
readonly uninstall: (item: T) => Promise<void>;
|
||||
readonly edited?: {
|
||||
item: T;
|
||||
selectedVersion: Installable.Version;
|
||||
};
|
||||
readonly onItemEdit: (
|
||||
item: T,
|
||||
selectedVersion: Installable.Version
|
||||
) => void;
|
||||
readonly itemRenderer: ListItemRenderer<T>;
|
||||
readonly isScrolling: boolean;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
selectedVersion?: Installable.Version;
|
||||
inProgress?: 'installing' | 'uninstalling' | undefined;
|
||||
}
|
||||
}
|
||||
|
@@ -1,148 +1,50 @@
|
||||
import 'react-virtualized/styles.css';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
|
||||
import {
|
||||
CellMeasurer,
|
||||
CellMeasurerCache,
|
||||
} from 'react-virtualized/dist/commonjs/CellMeasurer';
|
||||
import type {
|
||||
ListRowProps,
|
||||
ListRowRenderer,
|
||||
} from 'react-virtualized/dist/commonjs/List';
|
||||
import List from 'react-virtualized/dist/commonjs/List';
|
||||
import { Virtuoso } from '@theia/core/shared/react-virtuoso';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { Installable } from '../../../common/protocol/installable';
|
||||
import { HoverService } from '../../theia/core/hover-service';
|
||||
import { ComponentListItem } from './component-list-item';
|
||||
import { ListItemRenderer } from './list-item-renderer';
|
||||
|
||||
function sameAs<T>(
|
||||
left: T[],
|
||||
right: T[],
|
||||
...compareProps: (keyof T)[]
|
||||
): boolean {
|
||||
if (left === right) {
|
||||
return true;
|
||||
}
|
||||
const leftLength = left.length;
|
||||
if (leftLength !== right.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < leftLength; i++) {
|
||||
for (const prop of compareProps) {
|
||||
const leftValue = left[i][prop];
|
||||
const rightValue = right[i][prop];
|
||||
if (leftValue !== rightValue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export class ComponentList<T extends ArduinoComponent> extends React.Component<
|
||||
ComponentList.Props<T>
|
||||
ComponentList.Props<T>,
|
||||
ComponentList.State
|
||||
> {
|
||||
private readonly cache: CellMeasurerCache;
|
||||
private resizeAllFlag: boolean;
|
||||
private list: List | undefined;
|
||||
private mostRecentWidth: number | undefined;
|
||||
|
||||
constructor(props: ComponentList.Props<T>) {
|
||||
constructor(props: Readonly<ComponentList.Props<T>>) {
|
||||
super(props);
|
||||
this.cache = new CellMeasurerCache({
|
||||
defaultHeight: 140,
|
||||
fixedWidth: true,
|
||||
});
|
||||
this.state = {
|
||||
isScrolling: false,
|
||||
};
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ width, height }) => {
|
||||
if (this.mostRecentWidth && this.mostRecentWidth !== width) {
|
||||
this.resizeAllFlag = true;
|
||||
setTimeout(() => this.clearAll(), 0);
|
||||
<Virtuoso
|
||||
data={this.props.items}
|
||||
isScrolling={(isScrolling) => {
|
||||
if (this.state.isScrolling !== isScrolling) {
|
||||
this.setState({ isScrolling });
|
||||
if (isScrolling) {
|
||||
this.props.hoverService.cancelHover();
|
||||
}
|
||||
}
|
||||
this.mostRecentWidth = width;
|
||||
return (
|
||||
<List
|
||||
className={'items-container'}
|
||||
rowRenderer={this.createItem}
|
||||
height={height}
|
||||
width={width}
|
||||
rowCount={this.props.items.length}
|
||||
rowHeight={this.cache.rowHeight}
|
||||
deferredMeasurementCache={this.cache}
|
||||
ref={this.setListRef}
|
||||
estimatedRowSize={140}
|
||||
// If default value, then `react-virtualized` will optimize and list item will not receive a `:hover` event.
|
||||
// Hence, install and version `<select>` won't be visible even if the mouse cursor is over the `<div>`.
|
||||
// See https://github.com/bvaughn/react-virtualized/blob/005be24a608add0344284053dae7633be86053b2/source/Grid/Grid.js#L38-L42
|
||||
scrollingResetTimeInterval={0}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
);
|
||||
}
|
||||
|
||||
override componentDidUpdate(prevProps: ComponentList.Props<T>): void {
|
||||
if (
|
||||
this.resizeAllFlag ||
|
||||
!sameAs(this.props.items, prevProps.items, 'name', 'installedVersion')
|
||||
) {
|
||||
this.clearAll(true);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly setListRef = (ref: List | null): void => {
|
||||
this.list = ref || undefined;
|
||||
};
|
||||
|
||||
private clearAll(scrollToTop = false): void {
|
||||
this.resizeAllFlag = false;
|
||||
this.cache.clearAll();
|
||||
if (this.list) {
|
||||
this.list.recomputeRowHeights();
|
||||
if (scrollToTop) {
|
||||
this.list.scrollToPosition(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly createItem: ListRowRenderer = ({
|
||||
index,
|
||||
parent,
|
||||
key,
|
||||
style,
|
||||
}: ListRowProps): React.ReactNode => {
|
||||
const item = this.props.items[index];
|
||||
return (
|
||||
<CellMeasurer
|
||||
cache={this.cache}
|
||||
columnIndex={0}
|
||||
key={key}
|
||||
rowIndex={index}
|
||||
parent={parent}
|
||||
>
|
||||
{({ registerChild }) => (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
<div ref={registerChild} style={style}>
|
||||
<ComponentListItem<T>
|
||||
key={this.props.itemLabel(item)}
|
||||
item={item}
|
||||
itemRenderer={this.props.itemRenderer}
|
||||
install={this.props.install}
|
||||
uninstall={this.props.uninstall}
|
||||
/>
|
||||
</div>
|
||||
itemContent={(_: number, item: T) => (
|
||||
<ComponentListItem<T>
|
||||
key={this.props.itemLabel(item)}
|
||||
item={item}
|
||||
itemRenderer={this.props.itemRenderer}
|
||||
install={this.props.install}
|
||||
uninstall={this.props.uninstall}
|
||||
edited={this.props.edited}
|
||||
onItemEdit={this.props.onItemEdit}
|
||||
isScrolling={this.state.isScrolling}
|
||||
/>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export namespace ComponentList {
|
||||
export interface Props<T extends ArduinoComponent> {
|
||||
readonly items: T[];
|
||||
@@ -150,5 +52,17 @@ export namespace ComponentList {
|
||||
readonly itemRenderer: ListItemRenderer<T>;
|
||||
readonly install: (item: T, version?: Installable.Version) => Promise<void>;
|
||||
readonly uninstall: (item: T) => Promise<void>;
|
||||
readonly edited?: {
|
||||
item: T;
|
||||
selectedVersion: Installable.Version;
|
||||
};
|
||||
readonly onItemEdit: (
|
||||
item: T,
|
||||
selectedVersion: Installable.Version
|
||||
) => void;
|
||||
readonly hoverService: HoverService;
|
||||
}
|
||||
export interface State {
|
||||
isScrolling: boolean;
|
||||
}
|
||||
}
|
||||
|
@@ -1,121 +0,0 @@
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import {
|
||||
BoardSearch,
|
||||
LibrarySearch,
|
||||
Searchable,
|
||||
} from '../../../common/protocol';
|
||||
|
||||
@injectable()
|
||||
export abstract class FilterRenderer<S extends Searchable.Options> {
|
||||
render(
|
||||
options: S,
|
||||
handlePropChange: (prop: keyof S, value: S[keyof S]) => void
|
||||
): React.ReactNode {
|
||||
const props = this.props();
|
||||
return (
|
||||
<div className="filter-bar">
|
||||
{Object.entries(options)
|
||||
.filter(([prop]) => props.includes(prop as keyof S))
|
||||
.map(([prop, value]) => (
|
||||
<div key={prop} className="filter">
|
||||
<div className="filter-label">
|
||||
{`${this.propertyLabel(prop as keyof S)}:`}
|
||||
</div>
|
||||
<select
|
||||
className="theia-select"
|
||||
value={value}
|
||||
onChange={(event) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
handlePropChange(prop as keyof S, event.target.value as any)
|
||||
}
|
||||
>
|
||||
{this.options(prop as keyof S).map((key) => (
|
||||
<option key={key} value={key}>
|
||||
{this.valueLabel(prop as keyof S, key)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
protected abstract props(): (keyof S)[];
|
||||
protected abstract options(prop: keyof S): string[];
|
||||
protected abstract valueLabel(prop: keyof S, key: string): string;
|
||||
protected abstract propertyLabel(prop: keyof S): string;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class BoardsFilterRenderer extends FilterRenderer<BoardSearch> {
|
||||
protected props(): (keyof BoardSearch)[] {
|
||||
return ['type'];
|
||||
}
|
||||
protected options(prop: keyof BoardSearch): string[] {
|
||||
switch (prop) {
|
||||
case 'type':
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return BoardSearch.TypeLiterals as any;
|
||||
default:
|
||||
throw new Error(`Unexpected prop: ${prop}`);
|
||||
}
|
||||
}
|
||||
protected valueLabel(prop: keyof BoardSearch, key: string): string {
|
||||
switch (prop) {
|
||||
case 'type':
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (BoardSearch.TypeLabels as any)[key];
|
||||
default:
|
||||
throw new Error(`Unexpected key: ${prop}`);
|
||||
}
|
||||
}
|
||||
protected propertyLabel(prop: keyof BoardSearch): string {
|
||||
switch (prop) {
|
||||
case 'type':
|
||||
return BoardSearch.PropertyLabels[prop];
|
||||
default:
|
||||
throw new Error(`Unexpected key: ${prop}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class LibraryFilterRenderer extends FilterRenderer<LibrarySearch> {
|
||||
protected props(): (keyof LibrarySearch)[] {
|
||||
return ['type', 'topic'];
|
||||
}
|
||||
protected options(prop: keyof LibrarySearch): string[] {
|
||||
switch (prop) {
|
||||
case 'type':
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return LibrarySearch.TypeLiterals as any;
|
||||
case 'topic':
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return LibrarySearch.TopicLiterals as any;
|
||||
default:
|
||||
throw new Error(`Unexpected prop: ${prop}`);
|
||||
}
|
||||
}
|
||||
protected propertyLabel(prop: keyof LibrarySearch): string {
|
||||
switch (prop) {
|
||||
case 'type':
|
||||
case 'topic':
|
||||
return LibrarySearch.PropertyLabels[prop];
|
||||
default:
|
||||
throw new Error(`Unexpected key: ${prop}`);
|
||||
}
|
||||
}
|
||||
protected valueLabel(prop: keyof LibrarySearch, key: string): string {
|
||||
switch (prop) {
|
||||
case 'type':
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (LibrarySearch.TypeLabels as any)[key] as any;
|
||||
case 'topic':
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (LibrarySearch.TopicLabels as any)[key] as any;
|
||||
default:
|
||||
throw new Error(`Unexpected prop: ${prop}`);
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,12 +9,13 @@ import { ExecuteWithProgress } from '../../../common/protocol/progressible';
|
||||
import { Installable } from '../../../common/protocol/installable';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { SearchBar } from './search-bar';
|
||||
import { ListWidget } from './list-widget';
|
||||
import { ListWidget, ListWidgetSearchOptions } from './list-widget';
|
||||
import { ComponentList } from './component-list';
|
||||
import { ListItemRenderer } from './list-item-renderer';
|
||||
import { ResponseServiceClient } from '../../../common/protocol';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { FilterRenderer } from './filter-renderer';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { HoverService } from '../../theia/core/hover-service';
|
||||
|
||||
export class FilterableListContainer<
|
||||
T extends ArduinoComponent,
|
||||
@@ -23,21 +24,30 @@ export class FilterableListContainer<
|
||||
FilterableListContainer.Props<T, S>,
|
||||
FilterableListContainer.State<T, S>
|
||||
> {
|
||||
private readonly toDispose: DisposableCollection;
|
||||
|
||||
constructor(props: Readonly<FilterableListContainer.Props<T, S>>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
searchOptions: props.defaultSearchOptions,
|
||||
searchOptions: props.searchOptions.options,
|
||||
items: [],
|
||||
};
|
||||
this.toDispose = new DisposableCollection();
|
||||
}
|
||||
|
||||
override componentDidMount(): void {
|
||||
this.search = debounce(this.search, 500, { trailing: true });
|
||||
this.search(this.state.searchOptions);
|
||||
this.props.searchOptionsDidChange((newSearchOptions) => {
|
||||
const { searchOptions } = this.state;
|
||||
this.setSearchOptionsAndUpdate({ ...searchOptions, ...newSearchOptions });
|
||||
});
|
||||
this.toDispose.pushAll([
|
||||
this.props.searchOptions.onDidChange((newSearchOptions) => {
|
||||
const { searchOptions } = this.state;
|
||||
this.setSearchOptionsAndUpdate({
|
||||
...searchOptions,
|
||||
...newSearchOptions,
|
||||
});
|
||||
}),
|
||||
this.props.onDidShow(() => this.setState({ edited: undefined })),
|
||||
]);
|
||||
}
|
||||
|
||||
override componentDidUpdate(): void {
|
||||
@@ -46,11 +56,14 @@ export class FilterableListContainer<
|
||||
this.props.container.updateScrollBar();
|
||||
}
|
||||
|
||||
override componentWillUnmount(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
return (
|
||||
<div className={'filterable-list-container'}>
|
||||
{this.renderSearchBar()}
|
||||
{this.renderSearchFilter()}
|
||||
<div className="filterable-list-container">
|
||||
{this.renderComponentList()}
|
||||
</div>
|
||||
@@ -58,17 +71,6 @@ export class FilterableListContainer<
|
||||
);
|
||||
}
|
||||
|
||||
protected renderSearchFilter(): React.ReactNode {
|
||||
return (
|
||||
<>
|
||||
{this.props.filterRenderer.render(
|
||||
this.state.searchOptions,
|
||||
this.handlePropChange.bind(this)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
protected renderSearchBar(): React.ReactNode {
|
||||
return (
|
||||
<SearchBar
|
||||
@@ -90,31 +92,33 @@ export class FilterableListContainer<
|
||||
itemRenderer={itemRenderer}
|
||||
install={this.install.bind(this)}
|
||||
uninstall={this.uninstall.bind(this)}
|
||||
edited={this.state.edited}
|
||||
onItemEdit={this.onItemEdit.bind(this)}
|
||||
hoverService={this.props.hoverService}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
protected handlePropChange = (prop: keyof S, value: S[keyof S]): void => {
|
||||
private handlePropChange = (prop: keyof S, value: S[keyof S]): void => {
|
||||
const searchOptions = {
|
||||
...this.state.searchOptions,
|
||||
[prop]: value,
|
||||
};
|
||||
this.setSearchOptionsAndUpdate(searchOptions);
|
||||
this.props.searchOptions.update(searchOptions);
|
||||
};
|
||||
|
||||
private setSearchOptionsAndUpdate(searchOptions: S) {
|
||||
this.setState({ searchOptions }, () => this.search(searchOptions));
|
||||
}
|
||||
|
||||
protected search(searchOptions: S): void {
|
||||
private search(searchOptions: S): void {
|
||||
const { searchable } = this.props;
|
||||
searchable.search(searchOptions).then((items) => this.setState({ items }));
|
||||
searchable
|
||||
.search(searchOptions)
|
||||
.then((items) => this.setState({ items, edited: undefined }));
|
||||
}
|
||||
|
||||
protected async install(
|
||||
item: T,
|
||||
version: Installable.Version
|
||||
): Promise<void> {
|
||||
private async install(item: T, version: Installable.Version): Promise<void> {
|
||||
const { install, searchable } = this.props;
|
||||
await ExecuteWithProgress.doWithProgress({
|
||||
...this.props,
|
||||
@@ -124,10 +128,10 @@ export class FilterableListContainer<
|
||||
run: ({ progressId }) => install({ item, progressId, version }),
|
||||
});
|
||||
const items = await searchable.search(this.state.searchOptions);
|
||||
this.setState({ items });
|
||||
this.setState({ items, edited: undefined });
|
||||
}
|
||||
|
||||
protected async uninstall(item: T): Promise<void> {
|
||||
private async uninstall(item: T): Promise<void> {
|
||||
const ok = await new ConfirmDialog({
|
||||
title: nls.localize('arduino/component/uninstall', 'Uninstall'),
|
||||
msg: nls.localize(
|
||||
@@ -152,7 +156,11 @@ export class FilterableListContainer<
|
||||
run: ({ progressId }) => uninstall({ item, progressId }),
|
||||
});
|
||||
const items = await searchable.search(this.state.searchOptions);
|
||||
this.setState({ items });
|
||||
this.setState({ items, edited: undefined });
|
||||
}
|
||||
|
||||
private onItemEdit(item: T, selectedVersion: Installable.Version): void {
|
||||
this.setState({ edited: { item, selectedVersion } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,16 +169,15 @@ export namespace FilterableListContainer {
|
||||
T extends ArduinoComponent,
|
||||
S extends Searchable.Options
|
||||
> {
|
||||
readonly defaultSearchOptions: S;
|
||||
readonly searchOptions: ListWidgetSearchOptions<S>;
|
||||
readonly container: ListWidget<T, S>;
|
||||
readonly searchable: Searchable<T, S>;
|
||||
readonly itemLabel: (item: T) => string;
|
||||
readonly itemRenderer: ListItemRenderer<T>;
|
||||
readonly filterRenderer: FilterRenderer<S>;
|
||||
readonly resolveFocus: (element: HTMLElement | undefined) => void;
|
||||
readonly searchOptionsDidChange: Event<Partial<S> | undefined>;
|
||||
readonly messageService: MessageService;
|
||||
readonly responseService: ResponseServiceClient;
|
||||
readonly onDidShow: Event<void>;
|
||||
readonly install: ({
|
||||
item,
|
||||
progressId,
|
||||
@@ -188,10 +195,15 @@ export namespace FilterableListContainer {
|
||||
progressId: string;
|
||||
}) => Promise<void>;
|
||||
readonly commandService: CommandService;
|
||||
readonly hoverService: HoverService;
|
||||
}
|
||||
|
||||
export interface State<T, S extends Searchable.Options> {
|
||||
searchOptions: S;
|
||||
items: T[];
|
||||
edited?: {
|
||||
item: T;
|
||||
selectedVersion: Installable.Version;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -1,137 +1,765 @@
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import {
|
||||
Anchor,
|
||||
ContextMenuRenderer,
|
||||
} from '@theia/core/lib/browser/context-menu-renderer';
|
||||
import { TabBarToolbar } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { codicon } from '@theia/core/lib/browser/widgets/widget';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { Installable } from '../../../common/protocol/installable';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { ComponentListItem } from './component-list-item';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
import { ApplicationError } from '@theia/core/lib/common/application-error';
|
||||
import {
|
||||
CommandRegistry,
|
||||
CommandService,
|
||||
} from '@theia/core/lib/common/command';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
|
||||
import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { Unknown } from '../../../common/nls';
|
||||
import {
|
||||
CoreService,
|
||||
ExamplesService,
|
||||
LibraryPackage,
|
||||
Sketch,
|
||||
SketchContainer,
|
||||
SketchesService,
|
||||
SketchRef,
|
||||
TopicLabel,
|
||||
} from '../../../common/protocol';
|
||||
import type { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { Installable } from '../../../common/protocol/installable';
|
||||
import { openClonedExample } from '../../contributions/examples';
|
||||
import {
|
||||
ArduinoMenus,
|
||||
examplesLabel,
|
||||
showDisabledContextMenuOptions,
|
||||
} from '../../menu/arduino-menus';
|
||||
import {
|
||||
MenuActionTemplate,
|
||||
registerMenus,
|
||||
SubmenuTemplate,
|
||||
} from '../../menu/register-menu';
|
||||
import { HoverService } from '../../theia/core/hover-service';
|
||||
|
||||
const moreInfoLabel = nls.localize('arduino/component/moreInfo', 'More info');
|
||||
const otherVersionsLabel = nls.localize(
|
||||
'arduino/component/otherVersions',
|
||||
'Other Versions'
|
||||
);
|
||||
const installLabel = nls.localize('arduino/component/install', 'Install');
|
||||
const installLatestLabel = nls.localize(
|
||||
'arduino/component/installLatest',
|
||||
'Install Latest'
|
||||
);
|
||||
function installVersionLabel(selectedVersion: string) {
|
||||
return nls.localize(
|
||||
'arduino/component/installVersion',
|
||||
'Install {0}',
|
||||
selectedVersion
|
||||
);
|
||||
}
|
||||
const updateLabel = nls.localize('arduino/component/update', 'Update');
|
||||
const removeLabel = nls.localize('arduino/component/remove', 'Remove');
|
||||
const byLabel = nls.localize('arduino/component/by', 'by');
|
||||
function installedLabel(installedVersion: string) {
|
||||
return nls.localize(
|
||||
'arduino/component/installed',
|
||||
'{0} installed',
|
||||
installedVersion
|
||||
);
|
||||
}
|
||||
function clickToOpenInBrowserLabel(href: string): string | undefined {
|
||||
return nls.localize(
|
||||
'arduino/component/clickToOpen',
|
||||
'Click to open in browser: {0}',
|
||||
href
|
||||
);
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ArduinoComponentContextMenuRenderer {
|
||||
@inject(CommandRegistry)
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
@inject(MenuModelRegistry)
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
@inject(ContextMenuRenderer)
|
||||
private readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
|
||||
private readonly toDisposeBeforeRender = new DisposableCollection();
|
||||
|
||||
async render(
|
||||
anchor: Anchor,
|
||||
...templates: Array<MenuActionTemplate | SubmenuTemplate>
|
||||
): Promise<void> {
|
||||
this.toDisposeBeforeRender.dispose();
|
||||
this.toDisposeBeforeRender.push(
|
||||
registerMenus({
|
||||
contextId: 'component',
|
||||
commandRegistry: this.commandRegistry,
|
||||
menuRegistry: this.menuRegistry,
|
||||
templates,
|
||||
})
|
||||
);
|
||||
const options = showDisabledContextMenuOptions({
|
||||
menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT,
|
||||
anchor,
|
||||
});
|
||||
this.contextMenuRenderer.render(options);
|
||||
}
|
||||
}
|
||||
|
||||
interface ListItemRendererParams<T extends ArduinoComponent> {
|
||||
readonly item: T;
|
||||
readonly selectedVersion: Installable.Version | undefined;
|
||||
readonly inProgress?: 'installing' | 'uninstalling' | undefined;
|
||||
readonly isScrolling: boolean;
|
||||
readonly install: (item: T) => Promise<void>;
|
||||
readonly uninstall: (item: T) => Promise<void>;
|
||||
readonly onVersionChange: (version: Installable.Version) => void;
|
||||
}
|
||||
|
||||
interface ListItemRendererServices {
|
||||
readonly windowService: WindowService;
|
||||
readonly messagesService: MessageService;
|
||||
readonly commandService: CommandService;
|
||||
readonly coreService: CoreService;
|
||||
readonly examplesService: ExamplesService;
|
||||
readonly sketchesService: SketchesService;
|
||||
readonly contextMenuRenderer: ArduinoComponentContextMenuRenderer;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ListItemRenderer<T extends ArduinoComponent> {
|
||||
@inject(WindowService)
|
||||
protected windowService: WindowService;
|
||||
private readonly windowService: WindowService;
|
||||
@inject(MessageService)
|
||||
private readonly messageService: MessageService;
|
||||
@inject(CommandService)
|
||||
private readonly commandService: CommandService;
|
||||
@inject(HoverService)
|
||||
private readonly hoverService: HoverService;
|
||||
@inject(CoreService)
|
||||
private readonly coreService: CoreService;
|
||||
@inject(ExamplesService)
|
||||
private readonly examplesService: ExamplesService;
|
||||
@inject(SketchesService)
|
||||
private readonly sketchesService: SketchesService;
|
||||
@inject(ArduinoComponentContextMenuRenderer)
|
||||
private readonly contextMenuRenderer: ArduinoComponentContextMenuRenderer;
|
||||
|
||||
protected onMoreInfoClick = (
|
||||
event: React.SyntheticEvent<HTMLAnchorElement, Event>
|
||||
): void => {
|
||||
const { target } = event.nativeEvent;
|
||||
if (target instanceof HTMLAnchorElement) {
|
||||
this.windowService.openNewWindow(target.href, { external: true });
|
||||
event.nativeEvent.preventDefault();
|
||||
private readonly onMoreInfo = (href: string | undefined): void => {
|
||||
if (href) {
|
||||
this.windowService.openNewWindow(href, { external: true });
|
||||
}
|
||||
};
|
||||
|
||||
renderItem(
|
||||
input: ComponentListItem.State & { item: T },
|
||||
install: (item: T) => Promise<void>,
|
||||
uninstall: (item: T) => Promise<void>,
|
||||
onVersionChange: (version: Installable.Version) => void
|
||||
): React.ReactNode {
|
||||
const { item } = input;
|
||||
let nameAndAuthor: JSX.Element;
|
||||
if (item.name && item.author) {
|
||||
const name = <span className="name">{item.name}</span>;
|
||||
const author = <span className="author">{item.author}</span>;
|
||||
nameAndAuthor = (
|
||||
<span>
|
||||
{name} {nls.localize('arduino/component/by', 'by')} {author}
|
||||
</span>
|
||||
);
|
||||
} else if (item.name) {
|
||||
nameAndAuthor = <span className="name">{item.name}</span>;
|
||||
} else if ((item as any).id) {
|
||||
nameAndAuthor = <span className="name">{(item as any).id}</span>;
|
||||
} else {
|
||||
nameAndAuthor = <span className="name">{Unknown}</span>;
|
||||
private readonly showHover = (
|
||||
event: React.MouseEvent<HTMLElement>,
|
||||
params: ListItemRendererParams<T>
|
||||
) => {
|
||||
if (!params.isScrolling) {
|
||||
const markdown = this.markdown(params);
|
||||
this.hoverService.requestHover({
|
||||
content: new MarkdownStringImpl(markdown),
|
||||
target: event.currentTarget,
|
||||
position: 'right',
|
||||
});
|
||||
}
|
||||
const onClickUninstall = () => uninstall(item);
|
||||
const installedVersion = !!item.installedVersion && (
|
||||
<div className="version-info">
|
||||
<span className="version">
|
||||
{nls.localize(
|
||||
'arduino/component/version',
|
||||
'Version {0}',
|
||||
item.installedVersion
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
className="installed uppercase"
|
||||
onClick={onClickUninstall}
|
||||
{...{
|
||||
install: nls.localize('arduino/component/installed', 'Installed'),
|
||||
uninstall: nls.localize('arduino/component/uninstall', 'Uninstall'),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const summary = <div className="summary">{item.summary}</div>;
|
||||
const description = <div className="summary">{item.description}</div>;
|
||||
|
||||
const moreInfo = !!item.moreInfoLink && (
|
||||
<a href={item.moreInfoLink} onClick={this.onMoreInfoClick}>
|
||||
{nls.localize('arduino/component/moreInfo', 'More info')}
|
||||
</a>
|
||||
);
|
||||
const onClickInstall = () => install(item);
|
||||
const installButton = item.installable && (
|
||||
<button
|
||||
className="theia-button secondary install uppercase"
|
||||
onClick={onClickInstall}
|
||||
>
|
||||
{nls.localize('arduino/component/install', 'Install')}
|
||||
</button>
|
||||
);
|
||||
|
||||
const onSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const version = event.target.value;
|
||||
if (version) {
|
||||
onVersionChange(version);
|
||||
}
|
||||
};
|
||||
|
||||
const versions = (() => {
|
||||
const { availableVersions } = item;
|
||||
if (availableVersions.length === 0) {
|
||||
return undefined;
|
||||
} else if (availableVersions.length === 1) {
|
||||
return <label>{availableVersions[0]}</label>;
|
||||
} else {
|
||||
return (
|
||||
<select
|
||||
className="theia-select"
|
||||
value={input.selectedVersion}
|
||||
onChange={onSelectChange}
|
||||
>
|
||||
{item.availableVersions
|
||||
.filter((version) => version !== item.installedVersion) // Filter the version that is currently installed.
|
||||
.map((version) => (
|
||||
<option value={version} key={version}>
|
||||
{version}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
};
|
||||
renderItem(params: ListItemRendererParams<T>): React.ReactNode {
|
||||
const action = this.action(params);
|
||||
return (
|
||||
<div className="component-list-item noselect">
|
||||
<div className="header">
|
||||
{nameAndAuthor}
|
||||
{installedVersion}
|
||||
</div>
|
||||
<div className="content">
|
||||
{summary}
|
||||
{description}
|
||||
</div>
|
||||
<div className="info">{moreInfo}</div>
|
||||
<div className="footer">
|
||||
{versions}
|
||||
{installButton}
|
||||
<>
|
||||
<Separator />
|
||||
<div
|
||||
className="component-list-item noselect"
|
||||
onMouseOver={(event) => this.showHover(event, params)}
|
||||
>
|
||||
<Header
|
||||
params={params}
|
||||
action={action}
|
||||
services={this.services}
|
||||
onMoreInfo={this.onMoreInfo}
|
||||
/>
|
||||
<Content params={params} onMoreInfo={this.onMoreInfo} />
|
||||
<Footer params={params} action={action} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private action(params: ListItemRendererParams<T>): Installable.Action {
|
||||
const {
|
||||
item: { installedVersion, availableVersions },
|
||||
selectedVersion,
|
||||
} = params;
|
||||
return Installable.action({
|
||||
installed: installedVersion,
|
||||
available: availableVersions,
|
||||
selected: selectedVersion,
|
||||
});
|
||||
}
|
||||
|
||||
private markdown(params: ListItemRendererParams<T>): string {
|
||||
// TODO: dedicated library and boards services for the markdown content generation
|
||||
const {
|
||||
item,
|
||||
item: { name, author, description, summary, installedVersion },
|
||||
} = params;
|
||||
let title = `__${name}__ ${byLabel} ${author}`;
|
||||
if (installedVersion) {
|
||||
title += `\n\n(${installedLabel(`\`${installedVersion}\``)})`;
|
||||
}
|
||||
if (LibraryPackage.is(item)) {
|
||||
let content = `\n\n${summary}`;
|
||||
// do not repeat the same info if paragraph and sentence are the same
|
||||
// example: https://github.com/arduino-libraries/ArduinoCloudThing/blob/8cbcee804e99fed614366c1b87143b1f1634c45f/library.properties#L5-L6
|
||||
if (description && description !== summary) {
|
||||
content += `\n_____\n\n${description}`;
|
||||
}
|
||||
return `${title}\n\n____${content}\n\n____\n${TopicLabel}: \`${item.category}\``;
|
||||
}
|
||||
return `${title}\n\n____\n\n${summary}\n\n - ${description
|
||||
.split(',')
|
||||
.join('\n - ')}`;
|
||||
}
|
||||
|
||||
private get services(): ListItemRendererServices {
|
||||
return {
|
||||
windowService: this.windowService,
|
||||
messagesService: this.messageService,
|
||||
commandService: this.commandService,
|
||||
coreService: this.coreService,
|
||||
sketchesService: this.sketchesService,
|
||||
examplesService: this.examplesService,
|
||||
contextMenuRenderer: this.contextMenuRenderer,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Separator extends React.Component {
|
||||
override render(): React.ReactNode {
|
||||
return (
|
||||
<div className="separator">
|
||||
<div />
|
||||
<div className="line" />
|
||||
<div />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Header<T extends ArduinoComponent> extends React.Component<
|
||||
Readonly<{
|
||||
params: ListItemRendererParams<T>;
|
||||
action: Installable.Action;
|
||||
services: ListItemRendererServices;
|
||||
onMoreInfo: (href: string | undefined) => void;
|
||||
}>
|
||||
> {
|
||||
override render(): React.ReactNode {
|
||||
return (
|
||||
<div className="header">
|
||||
<div>
|
||||
<Title {...this.props} />
|
||||
<Toolbar {...this.props} />
|
||||
</div>
|
||||
<InstalledVersion {...this.props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Toolbar<T extends ArduinoComponent> extends React.Component<
|
||||
Readonly<{
|
||||
params: ListItemRendererParams<T>;
|
||||
action: Installable.Action;
|
||||
services: ListItemRendererServices;
|
||||
onMoreInfo: (href: string | undefined) => void;
|
||||
}>
|
||||
> {
|
||||
private readonly onClick = (event: React.MouseEvent): void => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const anchor = this.toAnchor(event);
|
||||
this.showContextMenu(anchor);
|
||||
};
|
||||
|
||||
override render(): React.ReactNode {
|
||||
return (
|
||||
<div className={TabBarToolbar.Styles.TAB_BAR_TOOLBAR}>
|
||||
<div className={`${TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM} enabled`}>
|
||||
<div
|
||||
id="__more__"
|
||||
className={codicon('ellipsis', true)}
|
||||
title={nls.localizeByDefault('More Actions...')}
|
||||
onClick={this.onClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private toAnchor(event: React.MouseEvent): Anchor {
|
||||
const itemBox = event.currentTarget
|
||||
.closest('.' + TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM)
|
||||
?.getBoundingClientRect();
|
||||
return itemBox
|
||||
? {
|
||||
y: itemBox.bottom + itemBox.height / 2,
|
||||
x: itemBox.left,
|
||||
}
|
||||
: event.nativeEvent;
|
||||
}
|
||||
|
||||
private async showContextMenu(anchor: Anchor): Promise<void> {
|
||||
this.props.services.contextMenuRenderer.render(
|
||||
anchor,
|
||||
this.moreInfo,
|
||||
...(await this.examples),
|
||||
...this.otherVersions,
|
||||
...this.actions
|
||||
);
|
||||
}
|
||||
|
||||
private get moreInfo(): MenuActionTemplate {
|
||||
const {
|
||||
params: {
|
||||
item: { moreInfoLink },
|
||||
},
|
||||
} = this.props;
|
||||
return {
|
||||
menuLabel: moreInfoLabel,
|
||||
menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT,
|
||||
handler: {
|
||||
execute: () => this.props.onMoreInfo(moreInfoLink),
|
||||
isEnabled: () => Boolean(moreInfoLink),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private get examples(): Promise<Array<MenuActionTemplate | SubmenuTemplate>> {
|
||||
const {
|
||||
params: {
|
||||
item,
|
||||
item: { installedVersion, name },
|
||||
},
|
||||
services: { examplesService },
|
||||
} = this.props;
|
||||
// TODO: `LibraryPackage.is` should not be here but it saves one extra `lib list`
|
||||
// gRPC equivalent call with the name of a platform which will result an empty array.
|
||||
if (!LibraryPackage.is(item) || !installedVersion) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const submenuPath = [
|
||||
...ArduinoMenus.ARDUINO_COMPONENT__CONTEXT,
|
||||
'examples',
|
||||
];
|
||||
return examplesService.find({ libraryName: name }).then((containers) => [
|
||||
{
|
||||
submenuPath,
|
||||
menuLabel: examplesLabel,
|
||||
options: { order: String(0) },
|
||||
},
|
||||
...containers
|
||||
.map((container) => this.flattenContainers(container, submenuPath))
|
||||
.reduce((acc, curr) => acc.concat(curr), []),
|
||||
]);
|
||||
}
|
||||
|
||||
private flattenContainers(
|
||||
container: SketchContainer,
|
||||
menuPath: MenuPath,
|
||||
depth = 0
|
||||
): Array<MenuActionTemplate | SubmenuTemplate> {
|
||||
const templates: Array<MenuActionTemplate | SubmenuTemplate> = [];
|
||||
const { label } = container;
|
||||
if (depth > 0) {
|
||||
menuPath = [...menuPath, label];
|
||||
templates.push({
|
||||
submenuPath: menuPath,
|
||||
menuLabel: label,
|
||||
options: { order: label.toLocaleLowerCase() },
|
||||
});
|
||||
}
|
||||
return templates
|
||||
.concat(
|
||||
...container.sketches.map((sketch) =>
|
||||
this.sketchToMenuTemplate(sketch, menuPath)
|
||||
)
|
||||
)
|
||||
.concat(
|
||||
container.children
|
||||
.map((childContainer) =>
|
||||
this.flattenContainers(childContainer, menuPath, ++depth)
|
||||
)
|
||||
.reduce((acc, curr) => acc.concat(curr), [])
|
||||
);
|
||||
}
|
||||
|
||||
private sketchToMenuTemplate(
|
||||
sketch: SketchRef,
|
||||
menuPath: MenuPath
|
||||
): MenuActionTemplate {
|
||||
const { name, uri } = sketch;
|
||||
const { sketchesService, commandService } = this.props.services;
|
||||
return {
|
||||
menuLabel: name,
|
||||
menuPath,
|
||||
handler: {
|
||||
execute: () =>
|
||||
openClonedExample(
|
||||
uri,
|
||||
{ sketchesService, commandService },
|
||||
this.onExampleOpenError
|
||||
),
|
||||
},
|
||||
order: name.toLocaleLowerCase(),
|
||||
};
|
||||
}
|
||||
|
||||
private get onExampleOpenError(): {
|
||||
onDidFailClone: (
|
||||
err: ApplicationError<number, unknown>,
|
||||
uri: string
|
||||
) => unknown;
|
||||
onDidFailOpen: (
|
||||
err: ApplicationError<number, unknown>,
|
||||
sketch: Sketch
|
||||
) => unknown;
|
||||
} {
|
||||
const {
|
||||
services: { messagesService, coreService },
|
||||
} = this.props;
|
||||
const handle = async (err: ApplicationError<number, unknown>) => {
|
||||
messagesService.error(err.message);
|
||||
return coreService.refresh();
|
||||
};
|
||||
return {
|
||||
onDidFailClone: handle,
|
||||
onDidFailOpen: handle,
|
||||
};
|
||||
}
|
||||
|
||||
private get otherVersions(): Array<MenuActionTemplate | SubmenuTemplate> {
|
||||
const {
|
||||
params: {
|
||||
item: { availableVersions },
|
||||
selectedVersion,
|
||||
onVersionChange,
|
||||
},
|
||||
} = this.props;
|
||||
const submenuPath = [
|
||||
...ArduinoMenus.ARDUINO_COMPONENT__CONTEXT,
|
||||
'other-versions',
|
||||
];
|
||||
return [
|
||||
{
|
||||
submenuPath,
|
||||
menuLabel: otherVersionsLabel,
|
||||
options: { order: String(1) },
|
||||
},
|
||||
...availableVersions
|
||||
.filter((version) => version !== selectedVersion)
|
||||
.map((version) => ({
|
||||
menuPath: submenuPath,
|
||||
menuLabel: version,
|
||||
handler: {
|
||||
execute: () => onVersionChange(version),
|
||||
},
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
private get actions(): MenuActionTemplate[] {
|
||||
const {
|
||||
action,
|
||||
params: {
|
||||
item,
|
||||
item: { availableVersions, installedVersion },
|
||||
install,
|
||||
uninstall,
|
||||
selectedVersion,
|
||||
},
|
||||
} = this.props;
|
||||
const removeAction = {
|
||||
menuLabel: removeLabel,
|
||||
menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT__ACTION_GROUP,
|
||||
handler: {
|
||||
execute: () => uninstall(item),
|
||||
},
|
||||
};
|
||||
const installAction = {
|
||||
menuLabel: installVersionLabel(
|
||||
selectedVersion ?? Installable.latest(availableVersions) ?? ''
|
||||
),
|
||||
menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT__ACTION_GROUP,
|
||||
handler: {
|
||||
execute: () => install(item),
|
||||
},
|
||||
};
|
||||
const installLatestAction = {
|
||||
menuLabel: installLatestLabel,
|
||||
menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT__ACTION_GROUP,
|
||||
handler: {
|
||||
execute: () => install(item),
|
||||
},
|
||||
};
|
||||
const updateAction = {
|
||||
menuLabel: updateLabel,
|
||||
menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT__ACTION_GROUP,
|
||||
handler: {
|
||||
execute: () => install(item),
|
||||
},
|
||||
};
|
||||
switch (action) {
|
||||
case 'unknown':
|
||||
return [];
|
||||
case 'remove': {
|
||||
return [removeAction];
|
||||
}
|
||||
case 'update': {
|
||||
return [removeAction, updateAction];
|
||||
}
|
||||
case 'installLatest':
|
||||
return [
|
||||
...(Boolean(installedVersion) ? [removeAction] : []),
|
||||
installLatestAction,
|
||||
];
|
||||
case 'installSelected': {
|
||||
return [
|
||||
...(Boolean(installedVersion) ? [removeAction] : []),
|
||||
installAction,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Title<T extends ArduinoComponent> extends React.Component<
|
||||
Readonly<{
|
||||
params: ListItemRendererParams<T>;
|
||||
}>
|
||||
> {
|
||||
override render(): React.ReactNode {
|
||||
const { name, author } = this.props.params.item;
|
||||
return (
|
||||
<div className="title">
|
||||
{name && author ? (
|
||||
<>
|
||||
{<span className="name">{name}</span>}{' '}
|
||||
{<span className="author">{`${byLabel} ${author}`}</span>}
|
||||
</>
|
||||
) : name ? (
|
||||
<span className="name">{name}</span>
|
||||
) : (
|
||||
<span className="name">{Unknown}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InstalledVersion<T extends ArduinoComponent> extends React.Component<
|
||||
Readonly<{
|
||||
params: ListItemRendererParams<T>;
|
||||
}>
|
||||
> {
|
||||
private readonly onClick = (): void => {
|
||||
this.props.params.uninstall(this.props.params.item);
|
||||
};
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const { installedVersion } = this.props.params.item;
|
||||
return (
|
||||
installedVersion && (
|
||||
<div className="version">
|
||||
<span
|
||||
className="installed-version"
|
||||
onClick={this.onClick}
|
||||
{...{
|
||||
version: installedLabel(installedVersion),
|
||||
remove: removeLabel,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Content<T extends ArduinoComponent> extends React.Component<
|
||||
Readonly<{
|
||||
params: ListItemRendererParams<T>;
|
||||
onMoreInfo: (href: string | undefined) => void;
|
||||
}>
|
||||
> {
|
||||
override render(): React.ReactNode {
|
||||
const {
|
||||
params: {
|
||||
item: { summary, description },
|
||||
},
|
||||
} = this.props;
|
||||
const content = [summary, description].filter(Boolean).join(' ');
|
||||
return (
|
||||
<div className="content">
|
||||
<p>{content}</p>
|
||||
<MoreInfo {...this.props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MoreInfo<T extends ArduinoComponent> extends React.Component<
|
||||
Readonly<{
|
||||
params: ListItemRendererParams<T>;
|
||||
onMoreInfo: (href: string | undefined) => void;
|
||||
}>
|
||||
> {
|
||||
private readonly onClick = (
|
||||
event: React.SyntheticEvent<HTMLAnchorElement, Event>
|
||||
): void => {
|
||||
const { target } = event.nativeEvent;
|
||||
if (target instanceof HTMLAnchorElement) {
|
||||
this.props.onMoreInfo(target.href);
|
||||
event.nativeEvent.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const {
|
||||
params: {
|
||||
item: { moreInfoLink: href },
|
||||
},
|
||||
} = this.props;
|
||||
return (
|
||||
href && (
|
||||
<div className="info" title={clickToOpenInBrowserLabel(href)}>
|
||||
<a href={href} onClick={this.onClick}>
|
||||
{moreInfoLabel}
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Footer<T extends ArduinoComponent> extends React.Component<
|
||||
Readonly<{
|
||||
params: ListItemRendererParams<T>;
|
||||
action: Installable.Action;
|
||||
}>
|
||||
> {
|
||||
override render(): React.ReactNode {
|
||||
const { isScrolling } = this.props.params;
|
||||
const className = ['footer'];
|
||||
if (isScrolling) {
|
||||
className.push('scrolling');
|
||||
}
|
||||
return (
|
||||
<div className={className.join(' ')}>
|
||||
<SelectVersion {...this.props} />
|
||||
<Button {...this.props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectVersion<T extends ArduinoComponent> extends React.Component<
|
||||
Readonly<{
|
||||
params: ListItemRendererParams<T>;
|
||||
action: Installable.Action;
|
||||
}>
|
||||
> {
|
||||
private readonly onChange = (
|
||||
event: React.ChangeEvent<HTMLSelectElement>
|
||||
): void => {
|
||||
const version = event.target.value;
|
||||
if (version) {
|
||||
this.props.params.onVersionChange(version);
|
||||
}
|
||||
};
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const {
|
||||
selectedVersion,
|
||||
item: { availableVersions },
|
||||
} = this.props.params;
|
||||
switch (this.props.action) {
|
||||
case 'installLatest': // fall-through
|
||||
case 'installSelected': // fall-through
|
||||
case 'update': // fall-through
|
||||
case 'remove':
|
||||
return (
|
||||
<select
|
||||
className="theia-select"
|
||||
value={selectedVersion}
|
||||
onChange={this.onChange}
|
||||
>
|
||||
{availableVersions.map((version) => (
|
||||
<option value={version} key={version}>
|
||||
{version}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
case 'unknown':
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Button<T extends ArduinoComponent> extends React.Component<
|
||||
Readonly<{
|
||||
params: ListItemRendererParams<T>;
|
||||
action: Installable.Action;
|
||||
}>
|
||||
> {
|
||||
override render(): React.ReactNode {
|
||||
const {
|
||||
params: { item, install, uninstall, inProgress: state },
|
||||
} = this.props;
|
||||
const classNames = ['theia-button install uppercase'];
|
||||
let onClick;
|
||||
let label;
|
||||
switch (this.props.action) {
|
||||
case 'unknown':
|
||||
return undefined;
|
||||
case 'installLatest': {
|
||||
classNames.push('primary');
|
||||
label = installLabel;
|
||||
onClick = () => install(item);
|
||||
break;
|
||||
}
|
||||
case 'installSelected': {
|
||||
classNames.push('secondary');
|
||||
label = installLabel;
|
||||
onClick = () => install(item);
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
classNames.push('secondary');
|
||||
label = updateLabel;
|
||||
onClick = () => install(item);
|
||||
break;
|
||||
}
|
||||
case 'remove': {
|
||||
classNames.push('secondary', 'no-border');
|
||||
label = removeLabel;
|
||||
onClick = () => uninstall(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<button
|
||||
className={classNames.join(' ')}
|
||||
onClick={onClick}
|
||||
disabled={Boolean(state)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,15 +1,39 @@
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { ContextMenuRenderer } from '@theia/core/lib/browser/context-menu-renderer';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import {
|
||||
OpenerOptions,
|
||||
OpenHandler,
|
||||
} from '@theia/core/lib/browser/opener-service';
|
||||
import {
|
||||
TabBarToolbarContribution,
|
||||
TabBarToolbarRegistry,
|
||||
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import { codicon } from '@theia/core/lib/browser/widgets/widget';
|
||||
import {
|
||||
Command,
|
||||
CommandContribution,
|
||||
CommandRegistry,
|
||||
} from '@theia/core/lib/common/command';
|
||||
import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
|
||||
import { URI } from '@theia/core/lib/common/uri';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Searchable } from '../../../common/protocol';
|
||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||
import { ListWidget } from './list-widget';
|
||||
import { showDisabledContextMenuOptions } from '../../menu/arduino-menus';
|
||||
import {
|
||||
MenuActionTemplate,
|
||||
menuActionWithCommandDelegate,
|
||||
registerMenus,
|
||||
SubmenuTemplate,
|
||||
} from '../../menu/register-menu';
|
||||
import { ListWidget, ListWidgetSearchOptions } from './list-widget';
|
||||
import { Event, nls } from '@theia/core';
|
||||
|
||||
@injectable()
|
||||
export abstract class ListWidgetFrontendContribution<
|
||||
@@ -17,14 +41,32 @@ export abstract class ListWidgetFrontendContribution<
|
||||
S extends Searchable.Options
|
||||
>
|
||||
extends AbstractViewContribution<ListWidget<T, S>>
|
||||
implements FrontendApplicationContribution, OpenHandler
|
||||
implements
|
||||
FrontendApplicationContribution,
|
||||
OpenHandler,
|
||||
TabBarToolbarContribution,
|
||||
CommandContribution
|
||||
{
|
||||
@inject(ContextMenuRenderer)
|
||||
private readonly contextMenuRenderer: ContextMenuRenderer;
|
||||
@inject(CommandRegistry)
|
||||
private readonly commandRegistry: CommandRegistry;
|
||||
@inject(MenuModelRegistry)
|
||||
private readonly menuRegistry: MenuModelRegistry;
|
||||
protected abstract readonly searchOptions: ListWidgetSearchOptions<S>;
|
||||
|
||||
private readonly toDisposeBeforeShowContextMenu = new DisposableCollection();
|
||||
|
||||
readonly id: string = `http-opener-${this.viewId}`;
|
||||
|
||||
async initializeLayout(): Promise<void> {
|
||||
this.openView();
|
||||
}
|
||||
|
||||
onStop(): void {
|
||||
this.toDisposeBeforeShowContextMenu.dispose();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
override registerMenus(_: MenuModelRegistry): void {
|
||||
// NOOP
|
||||
@@ -62,4 +104,131 @@ export abstract class ListWidgetFrontendContribution<
|
||||
|
||||
protected abstract canParse(uri: URI): boolean;
|
||||
protected abstract parse(uri: URI): S | undefined;
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
const filterCommand = this.showViewFilterContextMenuCommand;
|
||||
registry.registerItem({
|
||||
id: filterCommand.id,
|
||||
command: filterCommand.id,
|
||||
icon: () =>
|
||||
codicon(
|
||||
this.searchOptions.hasFilters() ? 'filter-filled' : 'filter',
|
||||
true
|
||||
),
|
||||
onDidChange: this.searchOptions
|
||||
.onDidChange as Event<unknown> as Event<void>,
|
||||
});
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
const filterCommand = this.showViewFilterContextMenuCommand;
|
||||
registry.registerCommand(filterCommand, {
|
||||
execute: () => this.showFilterContextMenu(filterCommand.id),
|
||||
isVisible: (arg: unknown) =>
|
||||
arg instanceof Widget && arg.id === this.viewId,
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract get showViewFilterContextMenuCommand(): Command & {
|
||||
label: string;
|
||||
};
|
||||
|
||||
protected abstract get showInstalledCommandId(): string;
|
||||
|
||||
protected abstract get showUpdatesCommandId(): string;
|
||||
|
||||
protected abstract buildFilterMenuGroup(
|
||||
menuPath: MenuPath
|
||||
): Array<MenuActionTemplate | SubmenuTemplate>;
|
||||
|
||||
private buildQuickFiltersMenuGroup(
|
||||
menuPath: MenuPath
|
||||
): Array<MenuActionTemplate | SubmenuTemplate> {
|
||||
return [
|
||||
menuActionWithCommandDelegate(
|
||||
{
|
||||
menuPath,
|
||||
command: this.showInstalledCommandId,
|
||||
},
|
||||
this.commandRegistry
|
||||
),
|
||||
menuActionWithCommandDelegate(
|
||||
{ menuPath, command: this.showUpdatesCommandId },
|
||||
this.commandRegistry
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private buildActionsMenuGroup(
|
||||
menuPath: MenuPath
|
||||
): Array<MenuActionTemplate | SubmenuTemplate> {
|
||||
if (!this.searchOptions.hasFilters()) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
menuPath,
|
||||
menuLabel: nls.localize('arduino/filter/clearAll', 'Clear All Filters'),
|
||||
handler: {
|
||||
execute: () => this.searchOptions.clearFilters(),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
protected buildMenuActions<T>(
|
||||
menuPath: MenuPath,
|
||||
literals: T[],
|
||||
isSelected: (literal: T) => boolean,
|
||||
select: (literal: T) => void,
|
||||
menuLabelProvider: (literal: T) => string
|
||||
): MenuActionTemplate[] {
|
||||
return literals
|
||||
.map((literal) => ({ literal, label: menuLabelProvider(literal) }))
|
||||
.map(({ literal, label }) => ({
|
||||
menuPath,
|
||||
menuLabel: label,
|
||||
handler: {
|
||||
execute: () => select(literal),
|
||||
isToggled: () => isSelected(literal),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
private showFilterContextMenu(commandId: string): void {
|
||||
this.toDisposeBeforeShowContextMenu.dispose();
|
||||
const element = document.getElementById(commandId);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
const client = element.getBoundingClientRect();
|
||||
const menuPath = [`${this.viewId}-filter-context-menu`];
|
||||
this.toDisposeBeforeShowContextMenu.pushAll([
|
||||
this.registerMenuGroup(
|
||||
this.buildFilterMenuGroup([...menuPath, '0_filter'])
|
||||
),
|
||||
this.registerMenuGroup(
|
||||
this.buildQuickFiltersMenuGroup([...menuPath, '1_quick_filters'])
|
||||
),
|
||||
this.registerMenuGroup(
|
||||
this.buildActionsMenuGroup([...menuPath, '2_actions'])
|
||||
),
|
||||
]);
|
||||
const options = showDisabledContextMenuOptions({
|
||||
menuPath,
|
||||
anchor: { x: client.left, y: client.bottom + client.height / 2 },
|
||||
});
|
||||
this.contextMenuRenderer.render(options);
|
||||
}
|
||||
|
||||
private registerMenuGroup(
|
||||
templates: Array<MenuActionTemplate | SubmenuTemplate>
|
||||
): Disposable {
|
||||
return registerMenus({
|
||||
commandRegistry: this.commandRegistry,
|
||||
menuRegistry: this.menuRegistry,
|
||||
contextId: this.viewId,
|
||||
templates,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,109 @@
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||
import { TabBarDecorator } from '@theia/core/lib/browser/shell/tab-bar-decorator';
|
||||
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { Title, Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { BoardsListWidget } from '../../boards/boards-list-widget';
|
||||
import {
|
||||
BoardsUpdates,
|
||||
LibraryUpdates,
|
||||
} from '../../contributions/check-for-updates';
|
||||
import { LibraryListWidget } from '../../library/library-list-widget';
|
||||
import { NotificationCenter } from '../../notification-center';
|
||||
|
||||
@injectable()
|
||||
abstract class ListWidgetTabBarDecorator
|
||||
implements TabBarDecorator, FrontendApplicationContribution
|
||||
{
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
|
||||
private count = 0;
|
||||
private readonly onDidChangeDecorationsEmitter = new Emitter<void>();
|
||||
protected readonly toDispose = new DisposableCollection(
|
||||
this.onDidChangeDecorationsEmitter
|
||||
);
|
||||
|
||||
abstract readonly id: string;
|
||||
readonly onDidChangeDecorations: Event<void> =
|
||||
this.onDidChangeDecorationsEmitter.event;
|
||||
|
||||
onStop(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
decorate(title: Title<Widget>): WidgetDecoration.Data[] {
|
||||
const { owner } = title;
|
||||
if (this.isListWidget(owner)) {
|
||||
if (this.count > 0) {
|
||||
return [{ badge: this.count }];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
protected async update(count: number): Promise<void> {
|
||||
this.count = count;
|
||||
this.onDidChangeDecorationsEmitter.fire();
|
||||
}
|
||||
|
||||
protected abstract isListWidget(widget: Widget): boolean;
|
||||
|
||||
protected abstract get updatableCount(): number | undefined;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class LibraryListWidgetTabBarDecorator extends ListWidgetTabBarDecorator {
|
||||
@inject(LibraryUpdates)
|
||||
private readonly libraryUpdates: LibraryUpdates;
|
||||
|
||||
readonly id = `${LibraryListWidget.WIDGET_ID}-badge-decorator`;
|
||||
|
||||
onStart(): void {
|
||||
this.toDispose.push(
|
||||
this.libraryUpdates.onDidChange((libraries) =>
|
||||
this.update(libraries.length)
|
||||
)
|
||||
);
|
||||
const count = this.updatableCount;
|
||||
if (count) {
|
||||
this.update(count);
|
||||
}
|
||||
}
|
||||
|
||||
protected isListWidget(widget: Widget): boolean {
|
||||
return widget instanceof LibraryListWidget;
|
||||
}
|
||||
|
||||
protected get updatableCount(): number | undefined {
|
||||
return this.libraryUpdates.updates?.length;
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class BoardsListWidgetTabBarDecorator extends ListWidgetTabBarDecorator {
|
||||
@inject(BoardsUpdates)
|
||||
private readonly boardsUpdates: BoardsUpdates;
|
||||
|
||||
readonly id = `${BoardsListWidget.WIDGET_ID}-badge-decorator`;
|
||||
|
||||
onStart(): void {
|
||||
this.toDispose.push(
|
||||
this.boardsUpdates.onDidChange((boards) => this.update(boards.length))
|
||||
);
|
||||
const count = this.updatableCount;
|
||||
if (count) {
|
||||
this.update(count);
|
||||
}
|
||||
}
|
||||
|
||||
protected isListWidget(widget: Widget): boolean {
|
||||
return widget instanceof BoardsListWidget;
|
||||
}
|
||||
|
||||
protected get updatableCount(): number | undefined {
|
||||
return this.boardsUpdates.updates?.length;
|
||||
}
|
||||
}
|
@@ -6,7 +6,7 @@ import {
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
|
||||
import { CommandService } from '@theia/core/lib/common/command';
|
||||
@@ -20,42 +20,44 @@ import {
|
||||
import { FilterableListContainer } from './filterable-list-container';
|
||||
import { ListItemRenderer } from './list-item-renderer';
|
||||
import { NotificationCenter } from '../../notification-center';
|
||||
import { FilterRenderer } from './filter-renderer';
|
||||
import { StatefulWidget } from '@theia/core/lib/browser';
|
||||
import { HoverService } from '../../theia/core/hover-service';
|
||||
|
||||
@injectable()
|
||||
export abstract class ListWidget<
|
||||
T extends ArduinoComponent,
|
||||
S extends Searchable.Options
|
||||
> extends ReactWidget {
|
||||
T extends ArduinoComponent,
|
||||
S extends Searchable.Options
|
||||
>
|
||||
extends ReactWidget
|
||||
implements StatefulWidget
|
||||
{
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
@inject(CommandService)
|
||||
protected readonly commandService: CommandService;
|
||||
|
||||
@inject(ResponseServiceClient)
|
||||
protected readonly responseService: ResponseServiceClient;
|
||||
|
||||
@inject(NotificationCenter)
|
||||
protected readonly notificationCenter: NotificationCenter;
|
||||
@inject(CommandService)
|
||||
private readonly commandService: CommandService;
|
||||
@inject(ResponseServiceClient)
|
||||
private readonly responseService: ResponseServiceClient;
|
||||
@inject(HoverService)
|
||||
private readonly hoverService: HoverService;
|
||||
|
||||
/**
|
||||
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
|
||||
*/
|
||||
protected focusNode: HTMLElement | undefined;
|
||||
private focusNode: HTMLElement | undefined;
|
||||
private readonly didReceiveFirstFocus = new Deferred();
|
||||
protected readonly searchOptionsChangeEmitter = new Emitter<
|
||||
Partial<S> | undefined
|
||||
>();
|
||||
private readonly searchOptions: ListWidgetSearchOptions<S>;
|
||||
private readonly onDidShowEmitter = new Emitter<void>();
|
||||
/**
|
||||
* Instead of running an `update` from the `postConstruct` `init` method,
|
||||
* we use this variable to track first activate, then run.
|
||||
*/
|
||||
protected firstActivate = true;
|
||||
private firstUpdate = true;
|
||||
|
||||
constructor(protected options: ListWidget.Options<T, S>) {
|
||||
super();
|
||||
const { id, label, iconClass } = options;
|
||||
const { id, label, iconClass, searchOptions } = options;
|
||||
this.id = id;
|
||||
this.title.label = label;
|
||||
this.title.caption = label;
|
||||
@@ -64,7 +66,8 @@ export abstract class ListWidget<
|
||||
this.addClass('arduino-list-widget');
|
||||
this.node.tabIndex = 0; // To be able to set the focus on the widget.
|
||||
this.scrollOptions = undefined;
|
||||
this.toDispose.push(this.searchOptionsChangeEmitter);
|
||||
this.searchOptions = searchOptions;
|
||||
this.toDispose.push(this.onDidShowEmitter);
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
@@ -78,15 +81,27 @@ export abstract class ListWidget<
|
||||
]);
|
||||
}
|
||||
|
||||
storeState(): S | undefined {
|
||||
return this.searchOptions.options;
|
||||
}
|
||||
|
||||
restoreState(oldState: unknown): void {
|
||||
if (oldState) {
|
||||
this.searchOptions.update(oldState as S);
|
||||
}
|
||||
}
|
||||
|
||||
protected override onAfterShow(message: Message): void {
|
||||
this.maybeUpdateOnFirstRender();
|
||||
super.onAfterShow(message);
|
||||
this.onDidShowEmitter.fire();
|
||||
}
|
||||
|
||||
private maybeUpdateOnFirstRender() {
|
||||
if (this.firstActivate) {
|
||||
this.firstActivate = false;
|
||||
if (this.firstUpdate) {
|
||||
this.firstUpdate = false;
|
||||
this.update();
|
||||
this.didReceiveFirstFocus.promise.then(() => this.focusNode?.focus());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +121,9 @@ export abstract class ListWidget<
|
||||
this.updateScrollBar();
|
||||
}
|
||||
|
||||
protected onFocusResolved = (element: HTMLElement | undefined): void => {
|
||||
private readonly onFocusResolved = (
|
||||
element: HTMLElement | undefined
|
||||
): void => {
|
||||
this.focusNode = element;
|
||||
this.didReceiveFirstFocus.resolve();
|
||||
};
|
||||
@@ -133,10 +150,10 @@ export abstract class ListWidget<
|
||||
return this.options.installable.uninstall({ item, progressId });
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
override render(): React.ReactNode {
|
||||
return (
|
||||
<FilterableListContainer<T, S>
|
||||
defaultSearchOptions={this.options.defaultSearchOptions}
|
||||
searchOptions={this.searchOptions}
|
||||
container={this}
|
||||
resolveFocus={this.onFocusResolved}
|
||||
searchable={this.options.searchable}
|
||||
@@ -144,11 +161,11 @@ export abstract class ListWidget<
|
||||
uninstall={this.uninstall.bind(this)}
|
||||
itemLabel={this.options.itemLabel}
|
||||
itemRenderer={this.options.itemRenderer}
|
||||
filterRenderer={this.options.filterRenderer}
|
||||
searchOptionsDidChange={this.searchOptionsChangeEmitter.event}
|
||||
messageService={this.messageService}
|
||||
commandService={this.commandService}
|
||||
responseService={this.responseService}
|
||||
onDidShow={this.onDidShowEmitter.event}
|
||||
hoverService={this.hoverService}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -158,9 +175,13 @@ export abstract class ListWidget<
|
||||
* If it is `undefined`, updates the view state by re-running the search with the current `filterText` term.
|
||||
*/
|
||||
refresh(searchOptions: Partial<S> | undefined): void {
|
||||
this.didReceiveFirstFocus.promise.then(() =>
|
||||
this.searchOptionsChangeEmitter.fire(searchOptions)
|
||||
);
|
||||
this.didReceiveFirstFocus.promise.then(() => {
|
||||
if (searchOptions) {
|
||||
this.searchOptions.update(searchOptions);
|
||||
} else {
|
||||
this.searchOptions.options = this.searchOptions.options; // triggers a refresh. TODO fix this!
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateScrollBar(): void {
|
||||
@@ -182,7 +203,67 @@ export namespace ListWidget {
|
||||
readonly searchable: Searchable<T, S>;
|
||||
readonly itemLabel: (item: T) => string;
|
||||
readonly itemRenderer: ListItemRenderer<T>;
|
||||
readonly filterRenderer: FilterRenderer<S>;
|
||||
readonly defaultSearchOptions: S;
|
||||
readonly searchOptions: ListWidgetSearchOptions<S>;
|
||||
}
|
||||
}
|
||||
|
||||
export class UserAbortError extends Error {
|
||||
constructor(message = 'User abort') {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, UserAbortError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export abstract class ListWidgetSearchOptions<S extends Searchable.Options> {
|
||||
private readonly onDidChangeEmitter = new Emitter<Required<S>>();
|
||||
protected _options: Required<S>;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.options = this.defaultOptions;
|
||||
}
|
||||
|
||||
get onDidChange(): Event<Required<S>> {
|
||||
return this.onDidChangeEmitter.event;
|
||||
}
|
||||
|
||||
get options(): Required<S> {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
set options(options: Required<S>) {
|
||||
this._options = options;
|
||||
this.onDidChangeEmitter.fire({ ...this._options });
|
||||
}
|
||||
|
||||
update(options: Partial<S>): void {
|
||||
this.options = { ...this.options, ...options };
|
||||
}
|
||||
|
||||
clearFilters(): void {
|
||||
const { query } = this.options;
|
||||
this.options = { ...this.defaultOptions, query };
|
||||
}
|
||||
|
||||
/**
|
||||
* `true` if all property values of the `options` object equals with the `defaultOptions` property values. The `query` property is ignored in the comparison.
|
||||
*/
|
||||
hasFilters(): boolean {
|
||||
const defaultOptions = this.defaultOptions;
|
||||
const currentOptions = this.options;
|
||||
for (const key of Object.keys(currentOptions)) {
|
||||
if (key === 'query') {
|
||||
continue;
|
||||
}
|
||||
const defaultValue = (defaultOptions as Record<string, unknown>)[key];
|
||||
const currentValue = (currentOptions as Record<string, unknown>)[key];
|
||||
if (defaultValue !== currentValue) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
abstract get defaultOptions(): Required<S>;
|
||||
}
|
||||
|
@@ -27,17 +27,14 @@ export namespace SketchbookCommands {
|
||||
|
||||
export const OPEN_SKETCHBOOK_CONTEXT_MENU: Command = {
|
||||
id: 'arduino-sketchbook--open-sketch-context-menu',
|
||||
label: 'Contextual menu',
|
||||
iconClass: 'sketchbook-tree__opts',
|
||||
};
|
||||
|
||||
export const SKETCHBOOK_HIDE_FILES: Command = {
|
||||
id: 'arduino-sketchbook--hide-files',
|
||||
label: 'Contextual menu',
|
||||
};
|
||||
|
||||
export const SKETCHBOOK_SHOW_FILES: Command = {
|
||||
id: 'arduino-sketchbook--show-files',
|
||||
label: 'Contextual menu',
|
||||
};
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import {
|
||||
postConstruct,
|
||||
} from '@theia/core/shared/inversify';
|
||||
import { TreeNode } from '@theia/core/lib/browser/tree/tree';
|
||||
import { CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { Command, CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import {
|
||||
NodeProps,
|
||||
TreeProps,
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
SketchesServiceClientImpl,
|
||||
} from '../../sketches-service-client-impl';
|
||||
import { SelectableTreeNode } from '@theia/core/lib/browser/tree/tree-selection';
|
||||
import { Sketch } from '../../contributions/contribution';
|
||||
import { nls } from '@theia/core/lib/common';
|
||||
|
||||
const customTreeProps: TreeProps = {
|
||||
@@ -91,8 +90,8 @@ export class SketchbookTreeWidget extends FileTreeWidget {
|
||||
node: TreeNode,
|
||||
props: NodeProps
|
||||
): React.ReactNode {
|
||||
if (SketchbookTree.SketchDirNode.is(node) || Sketch.isSketchFile(node.id)) {
|
||||
return <div className="sketch-folder-icon file-icon"></div>;
|
||||
if (SketchbookTree.SketchDirNode.is(node)) {
|
||||
return undefined;
|
||||
}
|
||||
const icon = this.toNodeIcon(node);
|
||||
if (icon) {
|
||||
@@ -116,7 +115,6 @@ export class SketchbookTreeWidget extends FileTreeWidget {
|
||||
protected hoveredNodeId: string | undefined;
|
||||
protected setHoverNodeId(id: string | undefined): void {
|
||||
this.hoveredNodeId = id;
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected override createNodeAttributes(
|
||||
@@ -134,26 +132,34 @@ export class SketchbookTreeWidget extends FileTreeWidget {
|
||||
protected renderInlineCommands(node: TreeNode): React.ReactNode {
|
||||
if (SketchbookTree.SketchDirNode.is(node) && node.commands) {
|
||||
return Array.from(new Set(node.commands)).map((command) =>
|
||||
this.renderInlineCommand(command.id, node)
|
||||
this.renderInlineCommand(command, node)
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected renderInlineCommand(
|
||||
commandId: string,
|
||||
command: Command | string | [command: string, label: string],
|
||||
node: SketchbookTree.SketchDirNode,
|
||||
options?: any
|
||||
): React.ReactNode {
|
||||
const command = this.commandRegistry.getCommand(commandId);
|
||||
const icon = command?.iconClass;
|
||||
const commandId = Command.is(command)
|
||||
? command.id
|
||||
: Array.isArray(command)
|
||||
? command[0]
|
||||
: command;
|
||||
const resolvedCommand = this.commandRegistry.getCommand(commandId);
|
||||
const icon = resolvedCommand?.iconClass;
|
||||
const args = { model: this.model, node: node, ...options };
|
||||
if (
|
||||
command &&
|
||||
resolvedCommand &&
|
||||
icon &&
|
||||
this.commandRegistry.isEnabled(commandId, args) &&
|
||||
this.commandRegistry.isVisible(commandId, args)
|
||||
) {
|
||||
const label = Array.isArray(command)
|
||||
? command[1]
|
||||
: resolvedCommand.label ?? resolvedCommand.id;
|
||||
const className = [
|
||||
TREE_NODE_SEGMENT_CLASS,
|
||||
TREE_NODE_TAIL_CLASS,
|
||||
@@ -165,7 +171,7 @@ export class SketchbookTreeWidget extends FileTreeWidget {
|
||||
<div
|
||||
key={`${commandId}--${node.id}`}
|
||||
className={className}
|
||||
title={command?.label || command.id}
|
||||
title={label}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@@ -9,6 +9,7 @@ import {
|
||||
WorkspaceRootNode,
|
||||
} from '@theia/navigator/lib/browser/navigator-tree';
|
||||
import { ArduinoPreferences } from '../../arduino-preferences';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
@injectable()
|
||||
export class SketchbookTree extends FileNavigatorTree {
|
||||
@@ -18,7 +19,9 @@ export class SketchbookTree extends FileNavigatorTree {
|
||||
@inject(ArduinoPreferences)
|
||||
protected readonly arduinoPreferences: ArduinoPreferences;
|
||||
|
||||
override async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
|
||||
override async resolveChildren(
|
||||
parent: CompositeTreeNode
|
||||
): Promise<TreeNode[]> {
|
||||
const showAllFiles =
|
||||
this.arduinoPreferences['arduino.sketchbook.showAllFiles'];
|
||||
|
||||
@@ -71,7 +74,13 @@ export class SketchbookTree extends FileNavigatorTree {
|
||||
protected async augmentSketchNode(node: DirNode): Promise<void> {
|
||||
Object.assign(node, {
|
||||
type: 'sketch',
|
||||
commands: [SketchbookCommands.OPEN_SKETCHBOOK_CONTEXT_MENU],
|
||||
commands: [
|
||||
[
|
||||
'arduino-create-cloud-copy',
|
||||
nls.localize('arduino/createCloudCopy', 'Push Sketch to Cloud'),
|
||||
],
|
||||
SketchbookCommands.OPEN_SKETCHBOOK_CONTEXT_MENU,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -96,7 +105,10 @@ export class SketchbookTree extends FileNavigatorTree {
|
||||
export namespace SketchbookTree {
|
||||
export interface SketchDirNode extends DirNode {
|
||||
readonly type: 'sketch';
|
||||
readonly commands?: Command[];
|
||||
/**
|
||||
* Theia command, the command ID string, or a tuple of command ID and preferred UI label. If the array construct is used, the label is the 1<sup>st</sup> of the array.
|
||||
*/
|
||||
readonly commands?: (Command | string | [string, string])[];
|
||||
}
|
||||
export namespace SketchDirNode {
|
||||
export function is(
|
||||
|
@@ -106,7 +106,7 @@ export class SketchbookWidgetContribution
|
||||
this.revealSketchNode(treeWidgetId, nodeUri),
|
||||
});
|
||||
registry.registerCommand(SketchbookCommands.OPEN_NEW_WINDOW, {
|
||||
execute: (arg) => this.openNewWindow(arg.node),
|
||||
execute: (arg) => this.openNewWindow(arg.node, arg?.treeWidgetId),
|
||||
isEnabled: (arg) =>
|
||||
!!arg && 'node' in arg && SketchbookTree.SketchDirNode.is(arg.node),
|
||||
isVisible: (arg) =>
|
||||
@@ -209,14 +209,20 @@ export class SketchbookWidgetContribution
|
||||
});
|
||||
}
|
||||
|
||||
private openNewWindow(node: SketchbookTree.SketchDirNode): void {
|
||||
const widget = this.tryGetWidget();
|
||||
if (widget) {
|
||||
const treeWidgetId = widget.activeTreeWidgetId();
|
||||
if (!treeWidgetId) {
|
||||
private openNewWindow(
|
||||
node: SketchbookTree.SketchDirNode,
|
||||
treeWidgetId?: string
|
||||
): void {
|
||||
if (!treeWidgetId) {
|
||||
const widget = this.tryGetWidget();
|
||||
if (!widget) {
|
||||
console.warn(`Could not retrieve active sketchbook tree ID.`);
|
||||
return;
|
||||
}
|
||||
treeWidgetId = widget.activeTreeWidgetId();
|
||||
}
|
||||
const widget = this.tryGetWidget();
|
||||
if (widget) {
|
||||
const nodeUri = node.uri.toString();
|
||||
const options: WorkspaceInput = {};
|
||||
Object.assign(options, {
|
||||
|
@@ -3,6 +3,10 @@ import { nls } from '@theia/core/lib/common/nls';
|
||||
export const Unknown = nls.localize('arduino/common/unknown', 'Unknown');
|
||||
export const Later = nls.localize('arduino/common/later', 'Later');
|
||||
export const Updatable = nls.localize('arduino/common/updateable', 'Updatable');
|
||||
export const Installed = nls.localize(
|
||||
'arduino/libraryType/installed', // TODO: rename `libraryType` to `common`?
|
||||
'Installed'
|
||||
);
|
||||
export const All = nls.localize('arduino/common/all', 'All');
|
||||
export const Type = nls.localize('arduino/common/type', 'Type');
|
||||
export const Partner = nls.localize('arduino/common/partner', 'Partner');
|
||||
|
@@ -1,34 +1,35 @@
|
||||
import { Installable } from './installable';
|
||||
import type { Installable } from './installable';
|
||||
|
||||
export interface ArduinoComponent {
|
||||
readonly name: string;
|
||||
readonly deprecated?: boolean;
|
||||
readonly author: string;
|
||||
readonly summary: string;
|
||||
readonly description: string;
|
||||
readonly moreInfoLink?: string;
|
||||
readonly availableVersions: Installable.Version[];
|
||||
readonly installable: boolean;
|
||||
readonly installedVersion?: Installable.Version;
|
||||
/**
|
||||
* This is the `Type` in IDE (1.x) UI.
|
||||
*/
|
||||
readonly types: string[];
|
||||
readonly deprecated?: boolean;
|
||||
readonly moreInfoLink?: string;
|
||||
}
|
||||
export namespace ArduinoComponent {
|
||||
export function is(arg: any): arg is ArduinoComponent {
|
||||
export function is(arg: unknown): arg is ArduinoComponent {
|
||||
return (
|
||||
!!arg &&
|
||||
'name' in arg &&
|
||||
typeof arg['name'] === 'string' &&
|
||||
'author' in arg &&
|
||||
typeof arg['author'] === 'string' &&
|
||||
'summary' in arg &&
|
||||
typeof arg['summary'] === 'string' &&
|
||||
'description' in arg &&
|
||||
typeof arg['description'] === 'string' &&
|
||||
'installable' in arg &&
|
||||
typeof arg['installable'] === 'boolean'
|
||||
typeof arg === 'object' &&
|
||||
(<ArduinoComponent>arg).name !== undefined &&
|
||||
typeof (<ArduinoComponent>arg).name === 'string' &&
|
||||
(<ArduinoComponent>arg).author !== undefined &&
|
||||
typeof (<ArduinoComponent>arg).author === 'string' &&
|
||||
(<ArduinoComponent>arg).summary !== undefined &&
|
||||
typeof (<ArduinoComponent>arg).summary === 'string' &&
|
||||
(<ArduinoComponent>arg).description !== undefined &&
|
||||
typeof (<ArduinoComponent>arg).description === 'string' &&
|
||||
(<ArduinoComponent>arg).availableVersions !== undefined &&
|
||||
Array.isArray((<ArduinoComponent>arg).availableVersions) &&
|
||||
(<ArduinoComponent>arg).types !== undefined &&
|
||||
Array.isArray((<ArduinoComponent>arg).types)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import { nls } from '@theia/core/lib/common/nls';
|
||||
import {
|
||||
All,
|
||||
Contributed,
|
||||
Installed,
|
||||
Partner,
|
||||
Type as TypeLabel,
|
||||
Updatable,
|
||||
@@ -174,6 +175,7 @@ export namespace BoardSearch {
|
||||
export const TypeLiterals = [
|
||||
'All',
|
||||
'Updatable',
|
||||
'Installed',
|
||||
'Arduino',
|
||||
'Contributed',
|
||||
'Arduino Certified',
|
||||
@@ -189,6 +191,7 @@ export namespace BoardSearch {
|
||||
export const TypeLabels: Record<Type, string> = {
|
||||
All: All,
|
||||
Updatable: Updatable,
|
||||
Installed: Installed,
|
||||
Arduino: 'Arduino',
|
||||
Contributed: Contributed,
|
||||
'Arduino Certified': nls.localize(
|
||||
@@ -245,6 +248,7 @@ export interface Port {
|
||||
readonly protocol: string;
|
||||
readonly protocolLabel: string;
|
||||
readonly properties?: Record<string, string>;
|
||||
readonly hardwareId?: string;
|
||||
}
|
||||
export namespace Port {
|
||||
export type Properties = Record<string, string>;
|
||||
@@ -553,6 +557,19 @@ export namespace Board {
|
||||
return left.name === right.name && left.fqbn === right.fqbn;
|
||||
}
|
||||
|
||||
export function hardwareIdEquals(left: Board, right: Board): boolean {
|
||||
if (left.port && right.port) {
|
||||
const { hardwareId: leftHardwareId } = left.port;
|
||||
const { hardwareId: rightHardwareId } = right.port;
|
||||
|
||||
if (leftHardwareId && rightHardwareId) {
|
||||
return leftHardwareId === rightHardwareId;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function sameAs(left: Board, right: string | Board): boolean {
|
||||
// How to associate a selected board with one of the available cores: https://typefox.slack.com/archives/CJJHJCJSJ/p1571142327059200
|
||||
// 1. How to use the FQBN if any and infer the package ID from it: https://typefox.slack.com/archives/CJJHJCJSJ/p1571147549069100
|
||||
|
@@ -9,4 +9,8 @@ export interface ExamplesService {
|
||||
current: SketchContainer[];
|
||||
any: SketchContainer[];
|
||||
}>;
|
||||
/**
|
||||
* Finds example sketch containers for the installed library.
|
||||
*/
|
||||
find(options: { libraryName: string }): Promise<SketchContainer[]>;
|
||||
}
|
||||
|
@@ -51,6 +51,46 @@ export namespace Installable {
|
||||
};
|
||||
}
|
||||
|
||||
export const ActionLiterals = [
|
||||
'installLatest',
|
||||
'installSelected',
|
||||
'update',
|
||||
'remove',
|
||||
'unknown',
|
||||
] as const;
|
||||
export type Action = typeof ActionLiterals[number];
|
||||
|
||||
export function action(params: {
|
||||
installed?: Version | undefined;
|
||||
available: Version[];
|
||||
selected?: Version;
|
||||
}): Action {
|
||||
const { installed, available } = params;
|
||||
const latest = Installable.latest(available);
|
||||
if (!latest || (installed && !available.includes(installed))) {
|
||||
return 'unknown';
|
||||
}
|
||||
const selected = params.selected ?? latest;
|
||||
if (installed === selected) {
|
||||
return 'remove';
|
||||
}
|
||||
if (installed) {
|
||||
return selected === latest && installed !== latest
|
||||
? 'update'
|
||||
: 'installSelected';
|
||||
} else {
|
||||
return selected === latest ? 'installLatest' : 'installSelected';
|
||||
}
|
||||
}
|
||||
|
||||
export function latest(versions: Version[]): Version | undefined {
|
||||
if (!versions.length) {
|
||||
return undefined;
|
||||
}
|
||||
const ordered = versions.slice().sort(Installable.Version.COMPARATOR);
|
||||
return ordered[ordered.length - 1];
|
||||
}
|
||||
|
||||
export const Installed = <T extends ArduinoComponent>({
|
||||
installedVersion,
|
||||
}: T): boolean => {
|
||||
|
@@ -5,6 +5,7 @@ import { nls } from '@theia/core/lib/common/nls';
|
||||
import {
|
||||
All,
|
||||
Contributed,
|
||||
Installed,
|
||||
Partner,
|
||||
Recommended,
|
||||
Retired,
|
||||
@@ -13,6 +14,11 @@ import {
|
||||
} from '../nls';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
|
||||
export const TopicLabel = nls.localize(
|
||||
'arduino/librarySearchProperty/topic',
|
||||
'Topic'
|
||||
);
|
||||
|
||||
export const LibraryServicePath = '/services/library-service';
|
||||
export const LibraryService = Symbol('LibraryService');
|
||||
export interface LibraryService
|
||||
@@ -76,7 +82,7 @@ export namespace LibrarySearch {
|
||||
export const TypeLabels: Record<Type, string> = {
|
||||
All: All,
|
||||
Updatable: Updatable,
|
||||
Installed: nls.localize('arduino/libraryType/installed', 'Installed'),
|
||||
Installed: Installed,
|
||||
Arduino: 'Arduino',
|
||||
Partner: Partner,
|
||||
Recommended: Recommended,
|
||||
@@ -137,7 +143,7 @@ export namespace LibrarySearch {
|
||||
keyof Omit<LibrarySearch, 'query'>,
|
||||
string
|
||||
> = {
|
||||
topic: nls.localize('arduino/librarySearchProperty/topic', 'Topic'),
|
||||
topic: TopicLabel,
|
||||
type: TypeLabel,
|
||||
};
|
||||
export namespace UriParser {
|
||||
@@ -198,6 +204,10 @@ export namespace LibraryService {
|
||||
export namespace List {
|
||||
export interface Options {
|
||||
readonly fqbn?: string | undefined;
|
||||
/**
|
||||
* The name of the library to filter to.
|
||||
*/
|
||||
readonly libraryName?: string | undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,11 +251,15 @@ export interface LibraryPackage extends ArduinoComponent {
|
||||
readonly category: string;
|
||||
}
|
||||
export namespace LibraryPackage {
|
||||
export function is(arg: any): arg is LibraryPackage {
|
||||
export function is(arg: unknown): arg is LibraryPackage {
|
||||
return (
|
||||
ArduinoComponent.is(arg) &&
|
||||
'includes' in arg &&
|
||||
Array.isArray(arg['includes'])
|
||||
(<LibraryPackage>arg).includes !== undefined &&
|
||||
Array.isArray((<LibraryPackage>arg).includes) &&
|
||||
(<LibraryPackage>arg).exampleUris !== undefined &&
|
||||
Array.isArray((<LibraryPackage>arg).exampleUris) &&
|
||||
(<LibraryPackage>arg).location !== undefined &&
|
||||
typeof (<LibraryPackage>arg).location === 'number'
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import type { ArduinoComponent } from './arduino-component';
|
||||
|
||||
export const Updatable = { type: 'Updatable' } as const;
|
||||
|
||||
export interface Searchable<T, O extends Searchable.Options> {
|
||||
search(options: O): Promise<T[]>;
|
||||
}
|
||||
|
@@ -74,12 +74,15 @@ export interface SketchesService {
|
||||
isTemp(sketch: SketchRef): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* If `isTemp` is `true` for the `sketch`, you can call this method to move the sketch from the temp
|
||||
* location to `directories.user`. Resolves with the URI of the sketch after the move. Rejects, when the sketch
|
||||
* was not in the temp folder. This method always overrides. It's the callers responsibility to ask the user whether
|
||||
* the files at the destination can be overwritten or not.
|
||||
* Recursively copies the sketch folder content including all files into the destination folder.
|
||||
* Resolves with the new URI of the sketch after the move. This method always overrides. It's the callers responsibility to ask the user whether
|
||||
* the files at the destination can be overwritten or not. This method copies all filesystem files, if you want to copy only sketch files,
|
||||
* but exclude, for example, language server log file, set the `onlySketchFiles` property to `true`. `onlySketchFiles` is `false` by default.
|
||||
*/
|
||||
copy(sketch: Sketch, options: { destinationUri: string }): Promise<string>;
|
||||
copy(
|
||||
sketch: Sketch,
|
||||
options: { destinationUri: string; onlySketchFiles?: boolean }
|
||||
): Promise<Sketch>;
|
||||
|
||||
/**
|
||||
* Returns with the container sketch for the input `uri`. If the `uri` is not in a sketch folder, the promise resolves to `undefined`.
|
||||
@@ -157,8 +160,6 @@ export namespace Sketch {
|
||||
// (non-API) exported for the tests
|
||||
export const defaultSketchFolderName = 'sketch';
|
||||
// (non-API) exported for the tests
|
||||
export const defaultFallbackFirstChar = '0';
|
||||
// (non-API) exported for the tests
|
||||
export const defaultFallbackChar = '_';
|
||||
// (non-API) exported for the tests
|
||||
export function reservedFilename(name: string): string {
|
||||
@@ -176,11 +177,11 @@ export namespace Sketch {
|
||||
// (non-API) exported for the tests
|
||||
export const invalidSketchFolderNameMessage = nls.localize(
|
||||
'arduino/sketch/invalidSketchName',
|
||||
'The name must start with a letter or number, followed by letters, numbers, dashes, dots and underscores. Maximum length is 63 characters.'
|
||||
'The name must start with a letter, number, or underscore, followed by letters, numbers, dashes, dots and underscores. Maximum length is 63 characters.'
|
||||
);
|
||||
const invalidCloudSketchFolderNameMessage = nls.localize(
|
||||
'arduino/sketch/invalidCloudSketchName',
|
||||
'The name must start with a letter or number, followed by letters, numbers, dashes, dots and underscores. Maximum length is 36 characters.'
|
||||
'The name must start with a letter, number, or underscore, followed by letters, numbers, dashes, dots and underscores. Maximum length is 36 characters.'
|
||||
);
|
||||
/**
|
||||
* `undefined` if the candidate sketch folder name is valid. Otherwise, the validation error message.
|
||||
@@ -193,7 +194,7 @@ export namespace Sketch {
|
||||
if (validFilenameError) {
|
||||
return validFilenameError;
|
||||
}
|
||||
return /^[0-9a-zA-Z]{1}[0-9a-zA-Z_\.-]{0,62}$/.test(candidate)
|
||||
return /^[0-9a-zA-Z_]{1}[0-9a-zA-Z_\.-]{0,62}$/.test(candidate)
|
||||
? undefined
|
||||
: invalidSketchFolderNameMessage;
|
||||
}
|
||||
@@ -208,7 +209,7 @@ export namespace Sketch {
|
||||
if (validFilenameError) {
|
||||
return validFilenameError;
|
||||
}
|
||||
return /^[0-9a-zA-Z]{1}[0-9a-zA-Z_\.-]{0,35}$/.test(candidate)
|
||||
return /^[0-9a-zA-Z_]{1}[0-9a-zA-Z_\.-]{0,35}$/.test(candidate)
|
||||
? undefined
|
||||
: invalidCloudSketchFolderNameMessage;
|
||||
}
|
||||
@@ -252,10 +253,7 @@ export namespace Sketch {
|
||||
return defaultSketchFolderName;
|
||||
}
|
||||
const validName = candidate
|
||||
? candidate
|
||||
.replace(/^[^0-9a-zA-Z]{1}/g, defaultFallbackFirstChar)
|
||||
.replace(/[^0-9a-zA-Z_]/g, defaultFallbackChar)
|
||||
.slice(0, 63)
|
||||
? candidate.replace(/[^0-9a-zA-Z_]/g, defaultFallbackChar).slice(0, 63)
|
||||
: defaultSketchFolderName;
|
||||
if (appendTimestampSuffix) {
|
||||
return `${validName.slice(0, 63 - timestampSuffixLength)}${
|
||||
@@ -283,10 +281,7 @@ export namespace Sketch {
|
||||
return defaultSketchFolderName;
|
||||
}
|
||||
return candidate
|
||||
? candidate
|
||||
.replace(/^[^0-9a-zA-Z]{1}/g, defaultFallbackFirstChar)
|
||||
.replace(/[^0-9a-zA-Z_]/g, defaultFallbackChar)
|
||||
.slice(0, 36)
|
||||
? candidate.replace(/[^0-9a-zA-Z_]/g, defaultFallbackChar).slice(0, 36)
|
||||
: defaultSketchFolderName;
|
||||
}
|
||||
|
||||
|
@@ -20,3 +20,21 @@ export function startsWithUpperCase(what: string): boolean {
|
||||
export function isNullOrUndefined(what: unknown): what is undefined | null {
|
||||
return what === undefined || what === null;
|
||||
}
|
||||
|
||||
// Use it for and exhaustive `switch` statements
|
||||
// https://stackoverflow.com/a/39419171/5529090
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function assertUnreachable(_: never): never {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
// Text encoder can crash in electron browser: https://github.com/arduino/arduino-ide/issues/634#issuecomment-1440039171
|
||||
export function uint8ArrayToString(uint8Array: Uint8Array): string {
|
||||
return uint8Array.reduce(
|
||||
(text, byte) => text + String.fromCharCode(byte),
|
||||
''
|
||||
);
|
||||
}
|
||||
export function stringToUint8Array(text: string): Uint8Array {
|
||||
return Uint8Array.from(text, (char) => char.charCodeAt(0));
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import {
|
||||
CommandMenuNode,
|
||||
CompoundMenuNode,
|
||||
CompoundMenuNodeRole,
|
||||
MAIN_MENU_BAR,
|
||||
MenuNode,
|
||||
MenuPath,
|
||||
} from '@theia/core/lib/common/menu';
|
||||
@@ -38,9 +39,18 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
|
||||
});
|
||||
}
|
||||
|
||||
override createElectronMenuBar(): Electron.Menu | null {
|
||||
override createElectronMenuBar(): Electron.Menu {
|
||||
this._toggledCommands.clear(); // https://github.com/eclipse-theia/theia/issues/8977
|
||||
return super.createElectronMenuBar();
|
||||
const menuModel = this.menuProvider.getMenu(MAIN_MENU_BAR);
|
||||
const template = this.fillMenuTemplate([], menuModel, [], {
|
||||
rootMenuPath: MAIN_MENU_BAR,
|
||||
});
|
||||
if (isOSX) {
|
||||
template.unshift(this.createOSXMenu());
|
||||
}
|
||||
const menu = remote.Menu.buildFromTemplate(this.escapeAmpersand(template));
|
||||
this._menu = menu;
|
||||
return menu;
|
||||
}
|
||||
|
||||
override async setMenuBar(): Promise<void> {
|
||||
@@ -51,7 +61,13 @@ export class ElectronMainMenuFactory extends TheiaElectronMainMenuFactory {
|
||||
this.updateWhenReady = true;
|
||||
return;
|
||||
}
|
||||
return super.setMenuBar();
|
||||
await this.preferencesService.ready;
|
||||
const createdMenuBar = this.createElectronMenuBar();
|
||||
if (isOSX) {
|
||||
remote.Menu.setApplicationMenu(createdMenuBar);
|
||||
} else {
|
||||
remote.getCurrentWindow().setMenu(createdMenuBar);
|
||||
}
|
||||
}
|
||||
|
||||
override createElectronContextMenu(
|
||||
|
@@ -1,38 +1,53 @@
|
||||
import {
|
||||
getCurrentWebContents,
|
||||
getCurrentWindow,
|
||||
} from '@theia/core/electron-shared/@electron/remote';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
|
||||
import { PreferenceScope } from '@theia/core/lib/browser/preferences/preference-scope';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
|
||||
import {
|
||||
ElectronCommands,
|
||||
ElectronMenuContribution as TheiaElectronMenuContribution,
|
||||
ElectronCommands,
|
||||
} from '@theia/core/lib/electron-browser/menu/electron-menu-contribution';
|
||||
import { ZoomLevel } from '@theia/core/lib/electron-browser/window/electron-window-preferences';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { MainMenuManager } from '../../../common/main-menu-manager';
|
||||
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
|
||||
import { FrontendApplication } from '@theia/core/lib/browser/frontend-application';
|
||||
import { ZoomLevel } from '@theia/core/lib/electron-browser/window/electron-window-preferences';
|
||||
import { PreferenceScope } from '@theia/core/lib/browser/preferences/preference-scope';
|
||||
import {
|
||||
getCurrentWindow,
|
||||
getCurrentWebContents,
|
||||
} from '@theia/core/electron-shared/@electron/remote';
|
||||
|
||||
@injectable()
|
||||
export class ElectronMenuContribution
|
||||
extends TheiaElectronMenuContribution
|
||||
implements MainMenuManager
|
||||
{
|
||||
private app: FrontendApplication;
|
||||
@inject(FrontendApplicationStateService)
|
||||
private readonly appStateService: FrontendApplicationStateService;
|
||||
|
||||
// private appReady = false;
|
||||
// private updateWhenReady = false;
|
||||
|
||||
override onStart(app: FrontendApplication): void {
|
||||
this.app = app;
|
||||
super.onStart(app);
|
||||
this.appStateService.reachedState('ready').then(() => {
|
||||
// this.appReady = true;
|
||||
// if (this.updateWhenReady) {
|
||||
// this.update();
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
protected override hideTopPanel(): void {
|
||||
// NOOP
|
||||
// We reuse the `div` for the Arduino toolbar.
|
||||
}
|
||||
|
||||
update(): void {
|
||||
// no menu updates before `onStart`
|
||||
if (!this.app) {
|
||||
return;
|
||||
}
|
||||
this.setMenu(this.app);
|
||||
// if (this.appReady) {
|
||||
(this as any).setMenu();
|
||||
// } else {
|
||||
// this.updateWhenReady = true;
|
||||
// }
|
||||
}
|
||||
|
||||
override registerCommands(registry: CommandRegistry): void {
|
||||
|
@@ -19,13 +19,8 @@ import {
|
||||
} from '@theia/core/lib/electron-main/electron-main-application';
|
||||
import { URI } from '@theia/core/shared/vscode-uri';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { isOSX } from '@theia/core/lib/common/os';
|
||||
import {
|
||||
RequestTitleBarStyle,
|
||||
Restart,
|
||||
TitleBarStyleAtStartup,
|
||||
TitleBarStyleChanged,
|
||||
} from '@theia/core/lib/electron-common/messaging/electron-messages';
|
||||
import * as os from '@theia/core/lib/common/os';
|
||||
import { Restart } from '@theia/core/lib/electron-common/messaging/electron-messages';
|
||||
import { TheiaBrowserWindowOptions } from '@theia/core/lib/electron-main/theia-electron-window';
|
||||
import { IsTempSketch } from '../../node/is-temp-sketch';
|
||||
import {
|
||||
@@ -181,7 +176,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
||||
|
||||
private attachFileAssociations(cwd: string): void {
|
||||
// OSX: register open-file event
|
||||
if (isOSX) {
|
||||
if (os.isOSX) {
|
||||
app.on('open-file', async (event, path) => {
|
||||
event.preventDefault();
|
||||
const resolvedPath = await this.resolvePath(path, cwd);
|
||||
@@ -335,19 +330,10 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
||||
}
|
||||
|
||||
protected override getTitleBarStyle(
|
||||
config: FrontendApplicationConfig
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_config: FrontendApplicationConfig
|
||||
): 'native' | 'custom' {
|
||||
const storedFrame = this.electronStore.get('windowstate')?.frame;
|
||||
if (storedFrame !== undefined) {
|
||||
return !!storedFrame ? 'native' : 'custom';
|
||||
}
|
||||
if (config.preferences && config.preferences['window.titleBarStyle']) {
|
||||
const titleBarStyle = config.preferences['window.titleBarStyle'];
|
||||
if (titleBarStyle === 'native' || titleBarStyle === 'custom') {
|
||||
return titleBarStyle;
|
||||
}
|
||||
}
|
||||
return 'custom';
|
||||
return 'native';
|
||||
}
|
||||
|
||||
protected override hookApplicationEvents(): void {
|
||||
@@ -365,21 +351,6 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
|
||||
this.delete(sketch);
|
||||
}
|
||||
});
|
||||
ipcMain.on(TitleBarStyleChanged, ({ sender }, titleBarStyle: string) => {
|
||||
this.useNativeWindowFrame = isOSX || titleBarStyle === 'native';
|
||||
const browserWindow = BrowserWindow.fromId(sender.id);
|
||||
if (browserWindow) {
|
||||
this.saveWindowState(browserWindow);
|
||||
} else {
|
||||
console.warn(`no BrowserWindow with id: ${sender.id}`);
|
||||
}
|
||||
});
|
||||
ipcMain.on(RequestTitleBarStyle, ({ sender }) => {
|
||||
sender.send(
|
||||
TitleBarStyleAtStartup,
|
||||
this.didUseNativeWindowFrameOnStart.get(sender.id) ? 'native' : 'custom'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
protected override async onSecondInstance(
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { join } from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||
@@ -16,7 +15,7 @@ import { BackendApplicationContribution } from '@theia/core/lib/node/backend-app
|
||||
import { ArduinoDaemon, NotificationServiceServer } from '../common/protocol';
|
||||
import { CLI_CONFIG } from './cli-config';
|
||||
import { getExecPath } from './exec-util';
|
||||
import { ErrnoException } from './utils/errors';
|
||||
import { SettingsReader } from './settings-reader';
|
||||
|
||||
@injectable()
|
||||
export class ArduinoDaemonImpl
|
||||
@@ -32,6 +31,9 @@ export class ArduinoDaemonImpl
|
||||
@inject(NotificationServiceServer)
|
||||
private readonly notificationService: NotificationServiceServer;
|
||||
|
||||
@inject(SettingsReader)
|
||||
private readonly settingsReader: SettingsReader;
|
||||
|
||||
private readonly toDispose = new DisposableCollection();
|
||||
private readonly onDaemonStartedEmitter = new Emitter<string>();
|
||||
private readonly onDaemonStoppedEmitter = new Emitter<void>();
|
||||
@@ -134,8 +136,6 @@ export class ArduinoDaemonImpl
|
||||
const cliConfigPath = join(FileUri.fsPath(configDirUri), CLI_CONFIG);
|
||||
const args = [
|
||||
'daemon',
|
||||
'--format',
|
||||
'jsonmini',
|
||||
'--port',
|
||||
'0',
|
||||
'--config-file',
|
||||
@@ -149,34 +149,12 @@ export class ArduinoDaemonImpl
|
||||
}
|
||||
|
||||
private async debugDaemon(): Promise<boolean> {
|
||||
// Poor man's preferences on the backend. (https://github.com/arduino/arduino-ide/issues/1056#issuecomment-1153975064)
|
||||
const configDirUri = await this.envVariablesServer.getConfigDirUri();
|
||||
const configDirPath = FileUri.fsPath(configDirUri);
|
||||
try {
|
||||
const raw = await fs.readFile(join(configDirPath, 'settings.json'), {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const json = this.tryParse(raw);
|
||||
if (json) {
|
||||
const value = json['arduino.cli.daemon.debug'];
|
||||
return typeof value === 'boolean' && !!value;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
if (ErrnoException.isENOENT(error)) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private tryParse(raw: string): any | undefined {
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return undefined;
|
||||
const settings = await this.settingsReader.read();
|
||||
if (settings) {
|
||||
const value = settings['arduino.cli.daemon.debug'];
|
||||
return value === true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected async spawnDaemonProcess(): Promise<{
|
||||
@@ -197,26 +175,6 @@ export class ArduinoDaemonImpl
|
||||
|
||||
daemon.stdout.on('data', (data) => {
|
||||
const message = data.toString();
|
||||
|
||||
let port = '';
|
||||
let address = '';
|
||||
message
|
||||
.split('\n')
|
||||
.filter((line: string) => line.length)
|
||||
.forEach((line: string) => {
|
||||
try {
|
||||
const parsedLine = JSON.parse(line);
|
||||
if ('Port' in parsedLine) {
|
||||
port = parsedLine.Port;
|
||||
}
|
||||
if ('IP' in parsedLine) {
|
||||
address = parsedLine.IP;
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
this.onData(message);
|
||||
if (!grpcServerIsReady) {
|
||||
const error = DaemonError.parse(message);
|
||||
@@ -225,6 +183,25 @@ export class ArduinoDaemonImpl
|
||||
return;
|
||||
}
|
||||
|
||||
let port = '';
|
||||
let address = '';
|
||||
message
|
||||
.split('\n')
|
||||
.filter((line: string) => line.length)
|
||||
.forEach((line: string) => {
|
||||
try {
|
||||
const parsedLine = JSON.parse(line);
|
||||
if ('Port' in parsedLine) {
|
||||
port = parsedLine.Port;
|
||||
}
|
||||
if ('IP' in parsedLine) {
|
||||
address = parsedLine.IP;
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
if (port.length && address.length) {
|
||||
grpcServerIsReady = true;
|
||||
ready.resolve({ daemon, port });
|
||||
|
@@ -118,6 +118,7 @@ import {
|
||||
LocalDirectoryPluginDeployerResolverWithFallback,
|
||||
PluginDeployer_GH_12064,
|
||||
} from './theia/plugin-ext/plugin-deployer';
|
||||
import { SettingsReader } from './settings-reader';
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(BackendApplication).toSelf().inSingletonScope();
|
||||
@@ -403,6 +404,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
.toSelf()
|
||||
.inSingletonScope();
|
||||
rebind(PluginDeployer).to(PluginDeployer_GH_12064).inSingletonScope();
|
||||
|
||||
bind(SettingsReader).toSelf().inSingletonScope();
|
||||
});
|
||||
|
||||
function bindChildLogger(bind: interfaces.Bind, name: string): void {
|
||||
|
@@ -323,14 +323,14 @@ export class BoardDiscovery
|
||||
}
|
||||
|
||||
private fromRpcPort(rpcPort: RpcPort): Port {
|
||||
const port = {
|
||||
return {
|
||||
address: rpcPort.getAddress(),
|
||||
addressLabel: rpcPort.getLabel(),
|
||||
protocol: rpcPort.getProtocol(),
|
||||
protocolLabel: rpcPort.getProtocolLabel(),
|
||||
properties: Port.Properties.create(rpcPort.getPropertiesMap().toObject()),
|
||||
hardwareId: rpcPort.getHardwareId(),
|
||||
};
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
|