mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-06-15 16:46:32 +00:00
feat: support updates in lib/boards widget
- can show badge with updates count, - better hover for libraries and platforms, - save/restore widget state (Closes #1398), - fixed `sentence` and `paragraph` order (Ref #1611) Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
parent
9b49712669
commit
fa4626bf14
@ -9,7 +9,10 @@ import {
|
|||||||
FrontendApplicationContribution,
|
FrontendApplicationContribution,
|
||||||
FrontendApplication as TheiaFrontendApplication,
|
FrontendApplication as TheiaFrontendApplication,
|
||||||
} from '@theia/core/lib/browser/frontend-application';
|
} 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 { ArduinoFrontendContribution } from './arduino-frontend-contribution';
|
||||||
import {
|
import {
|
||||||
LibraryService,
|
LibraryService,
|
||||||
@ -25,7 +28,10 @@ import {
|
|||||||
} from '../common/protocol/sketches-service';
|
} from '../common/protocol/sketches-service';
|
||||||
import { SketchesServiceClientImpl } from './sketches-service-client-impl';
|
import { SketchesServiceClientImpl } from './sketches-service-client-impl';
|
||||||
import { CoreService, CoreServicePath } from '../common/protocol/core-service';
|
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 { BoardsListWidgetFrontendContribution } from './boards/boards-widget-frontend-contribution';
|
||||||
import { BoardsServiceProvider } from './boards/boards-service-provider';
|
import { BoardsServiceProvider } from './boards/boards-service-provider';
|
||||||
import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
import { WorkspaceService as TheiaWorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||||
@ -73,7 +79,10 @@ import {
|
|||||||
} from '../common/protocol/config-service';
|
} from '../common/protocol/config-service';
|
||||||
import { MonitorWidget } from './serial/monitor/monitor-widget';
|
import { MonitorWidget } from './serial/monitor/monitor-widget';
|
||||||
import { MonitorViewContribution } from './serial/monitor/monitor-view-contribution';
|
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 { TabBarDecoratorService } from './theia/core/tab-bar-decorator';
|
||||||
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
|
import { ProblemManager as TheiaProblemManager } from '@theia/markers/lib/browser';
|
||||||
import { ProblemManager } from './theia/markers/problem-manager';
|
import { ProblemManager } from './theia/markers/problem-manager';
|
||||||
@ -313,10 +322,10 @@ import { PreferencesEditorWidget } from './theia/preferences/preference-editor-w
|
|||||||
import { PreferencesWidget } from '@theia/preferences/lib/browser/views/preference-widget';
|
import { PreferencesWidget } from '@theia/preferences/lib/browser/views/preference-widget';
|
||||||
import { createPreferencesWidgetContainer } from '@theia/preferences/lib/browser/views/preference-widget-bindings';
|
import { createPreferencesWidgetContainer } from '@theia/preferences/lib/browser/views/preference-widget-bindings';
|
||||||
import {
|
import {
|
||||||
BoardsFilterRenderer,
|
CheckForUpdates,
|
||||||
LibraryFilterRenderer,
|
BoardsUpdates,
|
||||||
} from './widgets/component-list/filter-renderer';
|
LibraryUpdates,
|
||||||
import { CheckForUpdates } from './contributions/check-for-updates';
|
} from './contributions/check-for-updates';
|
||||||
import { OutputEditorFactory } from './theia/output/output-editor-factory';
|
import { OutputEditorFactory } from './theia/output/output-editor-factory';
|
||||||
import { StartupTaskProvider } from '../electron-common/startup-task';
|
import { StartupTaskProvider } from '../electron-common/startup-task';
|
||||||
import { DeleteSketch } from './contributions/delete-sketch';
|
import { DeleteSketch } from './contributions/delete-sketch';
|
||||||
@ -356,6 +365,11 @@ import { Account } from './contributions/account';
|
|||||||
import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget';
|
import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget';
|
||||||
import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/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 { 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) => {
|
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||||
// Commands and toolbar items
|
// Commands and toolbar items
|
||||||
@ -371,8 +385,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
|
|
||||||
// Renderer for both the library and the core widgets.
|
// Renderer for both the library and the core widgets.
|
||||||
bind(ListItemRenderer).toSelf().inSingletonScope();
|
bind(ListItemRenderer).toSelf().inSingletonScope();
|
||||||
bind(LibraryFilterRenderer).toSelf().inSingletonScope();
|
|
||||||
bind(BoardsFilterRenderer).toSelf().inSingletonScope();
|
|
||||||
|
|
||||||
// Library service
|
// Library service
|
||||||
bind(LibraryService)
|
bind(LibraryService)
|
||||||
@ -395,6 +407,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
LibraryListWidgetFrontendContribution
|
LibraryListWidgetFrontendContribution
|
||||||
);
|
);
|
||||||
bind(OpenHandler).toService(LibraryListWidgetFrontendContribution);
|
bind(OpenHandler).toService(LibraryListWidgetFrontendContribution);
|
||||||
|
bind(TabBarToolbarContribution).toService(
|
||||||
|
LibraryListWidgetFrontendContribution
|
||||||
|
);
|
||||||
|
bind(CommandContribution).toService(LibraryListWidgetFrontendContribution);
|
||||||
|
bind(LibraryListWidgetSearchOptions).toSelf().inSingletonScope();
|
||||||
|
|
||||||
// Sketch list service
|
// Sketch list service
|
||||||
bind(SketchesService)
|
bind(SketchesService)
|
||||||
@ -464,6 +481,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
BoardsListWidgetFrontendContribution
|
BoardsListWidgetFrontendContribution
|
||||||
);
|
);
|
||||||
bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
|
bind(OpenHandler).toService(BoardsListWidgetFrontendContribution);
|
||||||
|
bind(TabBarToolbarContribution).toService(
|
||||||
|
BoardsListWidgetFrontendContribution
|
||||||
|
);
|
||||||
|
bind(CommandContribution).toService(BoardsListWidgetFrontendContribution);
|
||||||
|
bind(BoardsListWidgetSearchOptions).toSelf().inSingletonScope();
|
||||||
|
|
||||||
// Board select dialog
|
// Board select dialog
|
||||||
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
|
bind(BoardsConfigDialogWidget).toSelf().inSingletonScope();
|
||||||
@ -1034,4 +1056,20 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
bind(FrontendApplicationContribution).toService(DaemonPort);
|
bind(FrontendApplicationContribution).toService(DaemonPort);
|
||||||
bind(IsOnline).toSelf().inSingletonScope();
|
bind(IsOnline).toSelf().inSingletonScope();
|
||||||
bind(FrontendApplicationContribution).toService(IsOnline);
|
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
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { nls } from '@theia/core/lib/common';
|
||||||
import {
|
import {
|
||||||
inject,
|
inject,
|
||||||
injectable,
|
injectable,
|
||||||
@ -8,10 +9,18 @@ import {
|
|||||||
BoardsPackage,
|
BoardsPackage,
|
||||||
BoardsService,
|
BoardsService,
|
||||||
} from '../../common/protocol/boards-service';
|
} from '../../common/protocol/boards-service';
|
||||||
import { ListWidget } from '../widgets/component-list/list-widget';
|
|
||||||
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
||||||
import { nls } from '@theia/core/lib/common';
|
import {
|
||||||
import { BoardsFilterRenderer } from '../widgets/component-list/filter-renderer';
|
ListWidget,
|
||||||
|
ListWidgetSearchOptions,
|
||||||
|
} from '../widgets/component-list/list-widget';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class BoardsListWidgetSearchOptions extends ListWidgetSearchOptions<BoardSearch> {
|
||||||
|
get defaultOptions(): Required<BoardSearch> {
|
||||||
|
return { query: '', type: 'All' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
|
export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
|
||||||
@ -21,7 +30,8 @@ export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
|
|||||||
constructor(
|
constructor(
|
||||||
@inject(BoardsService) service: BoardsService,
|
@inject(BoardsService) service: BoardsService,
|
||||||
@inject(ListItemRenderer) itemRenderer: ListItemRenderer<BoardsPackage>,
|
@inject(ListItemRenderer) itemRenderer: ListItemRenderer<BoardsPackage>,
|
||||||
@inject(BoardsFilterRenderer) filterRenderer: BoardsFilterRenderer
|
@inject(BoardsListWidgetSearchOptions)
|
||||||
|
searchOptions: BoardsListWidgetSearchOptions
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
id: BoardsListWidget.WIDGET_ID,
|
id: BoardsListWidget.WIDGET_ID,
|
||||||
@ -31,8 +41,7 @@ export class BoardsListWidget extends ListWidget<BoardsPackage, BoardSearch> {
|
|||||||
installable: service,
|
installable: service,
|
||||||
itemLabel: (item: BoardsPackage) => item.name,
|
itemLabel: (item: BoardsPackage) => item.name,
|
||||||
itemRenderer,
|
itemRenderer,
|
||||||
filterRenderer,
|
searchOptions,
|
||||||
defaultSearchOptions: { query: '', type: 'All' },
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
import {
|
||||||
BoardSearch,
|
BoardSearch,
|
||||||
BoardsPackage,
|
BoardsPackage,
|
||||||
} from '../../common/protocol/boards-service';
|
} from '../../common/protocol/boards-service';
|
||||||
import { URI } from '../contributions/contribution';
|
import { URI } from '../contributions/contribution';
|
||||||
|
import { MenuActionTemplate, SubmenuTemplate } from '../menu/register-menu';
|
||||||
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
||||||
import { BoardsListWidget } from './boards-list-widget';
|
import {
|
||||||
|
BoardsListWidget,
|
||||||
|
BoardsListWidgetSearchOptions,
|
||||||
|
} from './boards-list-widget';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
||||||
BoardsPackage,
|
BoardsPackage,
|
||||||
BoardSearch
|
BoardSearch
|
||||||
> {
|
> {
|
||||||
|
@inject(BoardsListWidgetSearchOptions)
|
||||||
|
protected readonly searchOptions: BoardsListWidgetSearchOptions;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
widgetId: BoardsListWidget.WIDGET_ID,
|
widgetId: BoardsListWidget.WIDGET_ID,
|
||||||
@ -37,4 +48,51 @@ export class BoardsListWidgetFrontendContribution extends ListWidgetFrontendCont
|
|||||||
protected parse(uri: URI): BoardSearch | undefined {
|
protected parse(uri: URI): BoardSearch | undefined {
|
||||||
return BoardSearch.UriParser.parse(uri);
|
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...'),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 type { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||||
import { nls } from '@theia/core/lib/common/nls';
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { InstallManually, Later } from '../../common/nls';
|
import { InstallManually, Later } from '../../common/nls';
|
||||||
import {
|
import {
|
||||||
ArduinoComponent,
|
ArduinoComponent,
|
||||||
|
BoardSearch,
|
||||||
BoardsPackage,
|
BoardsPackage,
|
||||||
BoardsService,
|
BoardsService,
|
||||||
LibraryPackage,
|
LibraryPackage,
|
||||||
|
LibrarySearch,
|
||||||
LibraryService,
|
LibraryService,
|
||||||
ResponseServiceClient,
|
ResponseServiceClient,
|
||||||
Searchable,
|
Searchable,
|
||||||
|
Updatable,
|
||||||
} from '../../common/protocol';
|
} from '../../common/protocol';
|
||||||
import { Installable } from '../../common/protocol/installable';
|
import { Installable } from '../../common/protocol/installable';
|
||||||
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
import { ExecuteWithProgress } from '../../common/protocol/progressible';
|
||||||
import { BoardsListWidgetFrontendContribution } from '../boards/boards-widget-frontend-contribution';
|
import { BoardsListWidgetFrontendContribution } from '../boards/boards-widget-frontend-contribution';
|
||||||
import { LibraryListWidgetFrontendContribution } from '../library/library-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 { WindowServiceExt } from '../theia/core/window-service-ext';
|
||||||
import type { ListWidget } from '../widgets/component-list/list-widget';
|
import type { ListWidget } from '../widgets/component-list/list-widget';
|
||||||
import { Command, CommandRegistry, Contribution } from './contribution';
|
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',
|
'arduino/checkForUpdates/noUpdates',
|
||||||
'There are no recent updates available.'
|
'There are no recent updates available.'
|
||||||
);
|
);
|
||||||
const PromptUpdateBoards = nls.localize(
|
const promptUpdateBoards = nls.localize(
|
||||||
'arduino/checkForUpdates/promptUpdateBoards',
|
'arduino/checkForUpdates/promptUpdateBoards',
|
||||||
'Updates are available for some of your boards.'
|
'Updates are available for some of your boards.'
|
||||||
);
|
);
|
||||||
const PromptUpdateLibraries = nls.localize(
|
const promptUpdateLibraries = nls.localize(
|
||||||
'arduino/checkForUpdates/promptUpdateLibraries',
|
'arduino/checkForUpdates/promptUpdateLibraries',
|
||||||
'Updates are available for some of your libraries.'
|
'Updates are available for some of your libraries.'
|
||||||
);
|
);
|
||||||
const UpdatingBoards = nls.localize(
|
const updatingBoards = nls.localize(
|
||||||
'arduino/checkForUpdates/updatingBoards',
|
'arduino/checkForUpdates/updatingBoards',
|
||||||
'Updating boards...'
|
'Updating boards...'
|
||||||
);
|
);
|
||||||
const UpdatingLibraries = nls.localize(
|
const updatingLibraries = nls.localize(
|
||||||
'arduino/checkForUpdates/updatingLibraries',
|
'arduino/checkForUpdates/updatingLibraries',
|
||||||
'Updating libraries...'
|
'Updating libraries...'
|
||||||
);
|
);
|
||||||
const InstallAll = nls.localize(
|
const installAll = nls.localize(
|
||||||
'arduino/checkForUpdates/installAll',
|
'arduino/checkForUpdates/installAll',
|
||||||
'Install All'
|
'Install All'
|
||||||
);
|
);
|
||||||
@ -49,7 +59,24 @@ interface Task<T extends ArduinoComponent> {
|
|||||||
readonly item: T;
|
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()
|
@injectable()
|
||||||
export class CheckForUpdates extends Contribution {
|
export class CheckForUpdates extends Contribution {
|
||||||
@ -70,6 +97,37 @@ export class CheckForUpdates extends Contribution {
|
|||||||
register.registerCommand(CheckForUpdates.Commands.CHECK_FOR_UPDATES, {
|
register.registerCommand(CheckForUpdates.Commands.CHECK_FOR_UPDATES, {
|
||||||
execute: () => this.checkForUpdates(false),
|
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> {
|
override async onReady(): Promise<void> {
|
||||||
@ -85,13 +143,13 @@ export class CheckForUpdates extends Contribution {
|
|||||||
|
|
||||||
private async checkForUpdates(silent = true) {
|
private async checkForUpdates(silent = true) {
|
||||||
const [boardsPackages, libraryPackages] = await Promise.all([
|
const [boardsPackages, libraryPackages] = await Promise.all([
|
||||||
this.boardsService.search(Updatable),
|
this.boardsService.search(updatableBoardsSearchOption),
|
||||||
this.libraryService.search(Updatable),
|
this.libraryService.search(updatableLibrariesSearchOption),
|
||||||
]);
|
]);
|
||||||
this.promptUpdateBoards(boardsPackages);
|
this.promptUpdateBoards(boardsPackages);
|
||||||
this.promptUpdateLibraries(libraryPackages);
|
this.promptUpdateLibraries(libraryPackages);
|
||||||
if (!libraryPackages.length && !boardsPackages.length && !silent) {
|
if (!libraryPackages.length && !boardsPackages.length && !silent) {
|
||||||
this.messageService.info(NoUpdates);
|
this.messageService.info(noUpdates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,9 +158,9 @@ export class CheckForUpdates extends Contribution {
|
|||||||
items,
|
items,
|
||||||
installable: this.boardsService,
|
installable: this.boardsService,
|
||||||
viewContribution: this.boardsContribution,
|
viewContribution: this.boardsContribution,
|
||||||
viewSearchOptions: { query: '', ...Updatable },
|
viewSearchOptions: updatableBoardsSearchOption,
|
||||||
promptMessage: PromptUpdateBoards,
|
promptMessage: promptUpdateBoards,
|
||||||
updatingMessage: UpdatingBoards,
|
updatingMessage: updatingBoards,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,9 +169,9 @@ export class CheckForUpdates extends Contribution {
|
|||||||
items,
|
items,
|
||||||
installable: this.libraryService,
|
installable: this.libraryService,
|
||||||
viewContribution: this.librariesContribution,
|
viewContribution: this.librariesContribution,
|
||||||
viewSearchOptions: { query: '', topic: 'All', ...Updatable },
|
viewSearchOptions: updatableLibrariesSearchOption,
|
||||||
promptMessage: PromptUpdateLibraries,
|
promptMessage: promptUpdateLibraries,
|
||||||
updatingMessage: UpdatingLibraries,
|
updatingMessage: updatingLibraries,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,21 +199,30 @@ export class CheckForUpdates extends Contribution {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.messageService
|
this.messageService
|
||||||
.info(message, Later, InstallManually, InstallAll)
|
.info(message, Later, InstallManually, installAll)
|
||||||
.then((answer) => {
|
.then((answer) => {
|
||||||
if (answer === InstallAll) {
|
if (answer === installAll) {
|
||||||
const tasks = items.map((item) =>
|
const tasks = items.map((item) =>
|
||||||
this.createInstallTask(item, installable)
|
this.createInstallTask(item, installable)
|
||||||
);
|
);
|
||||||
this.executeTasks(updatingMessage, tasks);
|
return this.executeTasks(updatingMessage, tasks);
|
||||||
} else if (answer === InstallManually) {
|
} else if (answer === InstallManually) {
|
||||||
viewContribution
|
return this.showUpdatableItems(viewContribution, viewSearchOptions);
|
||||||
.openView({ reveal: true })
|
|
||||||
.then((widget) => widget.refresh(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(
|
private async executeTasks(
|
||||||
message: string,
|
message: string,
|
||||||
tasks: Task<ArduinoComponent>[]
|
tasks: Task<ArduinoComponent>[]
|
||||||
@ -217,5 +284,127 @@ export namespace CheckForUpdates {
|
|||||||
},
|
},
|
||||||
'arduino/checkForUpdates/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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,10 @@ import {
|
|||||||
import { nls } from '@theia/core/lib/common/nls';
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
import { WorkspaceCommands } from '@theia/workspace/lib/browser/workspace-commands';
|
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 { CurrentSketch } from '../sketches-service-client-impl';
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@ -119,7 +122,7 @@ export class SketchControl extends SketchContribution {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const options = {
|
const options = showDisabledContextMenuOptions({
|
||||||
menuPath: ArduinoMenus.SKETCH_CONTROL__CONTEXT,
|
menuPath: ArduinoMenus.SKETCH_CONTROL__CONTEXT,
|
||||||
anchor: {
|
anchor: {
|
||||||
x: parentElement.getBoundingClientRect().left,
|
x: parentElement.getBoundingClientRect().left,
|
||||||
@ -127,8 +130,7 @@ export class SketchControl extends SketchContribution {
|
|||||||
parentElement.getBoundingClientRect().top +
|
parentElement.getBoundingClientRect().top +
|
||||||
parentElement.offsetHeight,
|
parentElement.offsetHeight,
|
||||||
},
|
},
|
||||||
showDisabled: true,
|
});
|
||||||
};
|
|
||||||
this.contextMenuRenderer.render(options);
|
this.contextMenuRenderer.render(options);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,8 @@
|
|||||||
"activityBar.foreground": "#dae3e3",
|
"activityBar.foreground": "#dae3e3",
|
||||||
"activityBar.inactiveForeground": "#4e5b61",
|
"activityBar.inactiveForeground": "#4e5b61",
|
||||||
"activityBar.activeBorder": "#0ca1a6",
|
"activityBar.activeBorder": "#0ca1a6",
|
||||||
"statusBar.background": "#171e21",
|
"activityBarBadge.background": "#008184",
|
||||||
|
"statusBar.background": "#0ca1a6",
|
||||||
"secondaryButton.background": "#ff000000",
|
"secondaryButton.background": "#ff000000",
|
||||||
"secondaryButton.foreground": "#dae3e3",
|
"secondaryButton.foreground": "#dae3e3",
|
||||||
"secondaryButton.hoverBackground": "#ffffff1a",
|
"secondaryButton.hoverBackground": "#ffffff1a",
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
"activityBar.foreground": "#4e5b61",
|
"activityBar.foreground": "#4e5b61",
|
||||||
"activityBar.inactiveForeground": "#bdc7c7",
|
"activityBar.inactiveForeground": "#bdc7c7",
|
||||||
"activityBar.activeBorder": "#008184",
|
"activityBar.activeBorder": "#008184",
|
||||||
|
"activityBarBadge.background": "#008184",
|
||||||
"statusBar.background": "#006d70",
|
"statusBar.background": "#006d70",
|
||||||
"secondaryButton.background": "#ff000000",
|
"secondaryButton.background": "#ff000000",
|
||||||
"secondaryButton.foreground": "#008184",
|
"secondaryButton.foreground": "#008184",
|
||||||
|
@ -1,25 +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 {
|
import {
|
||||||
|
inject,
|
||||||
injectable,
|
injectable,
|
||||||
postConstruct,
|
postConstruct,
|
||||||
inject,
|
|
||||||
} from '@theia/core/shared/inversify';
|
} from '@theia/core/shared/inversify';
|
||||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
import { Installable } from '../../common/protocol';
|
||||||
import { addEventListener } from '@theia/core/lib/browser/widgets/widget';
|
|
||||||
import { DialogProps } from '@theia/core/lib/browser/dialogs';
|
|
||||||
import { AbstractDialog } from '../theia/dialogs/dialogs';
|
|
||||||
import {
|
import {
|
||||||
LibraryPackage,
|
LibraryPackage,
|
||||||
LibrarySearch,
|
LibrarySearch,
|
||||||
LibraryService,
|
LibraryService,
|
||||||
} from '../../common/protocol/library-service';
|
} from '../../common/protocol/library-service';
|
||||||
|
import { AbstractDialog } from '../theia/dialogs/dialogs';
|
||||||
|
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
||||||
import {
|
import {
|
||||||
ListWidget,
|
ListWidget,
|
||||||
|
ListWidgetSearchOptions,
|
||||||
UserAbortError,
|
UserAbortError,
|
||||||
} from '../widgets/component-list/list-widget';
|
} from '../widgets/component-list/list-widget';
|
||||||
import { Installable } from '../../common/protocol';
|
|
||||||
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
|
@injectable()
|
||||||
import { nls } from '@theia/core/lib/common';
|
export class LibraryListWidgetSearchOptions extends ListWidgetSearchOptions<LibrarySearch> {
|
||||||
import { LibraryFilterRenderer } from '../widgets/component-list/filter-renderer';
|
get defaultOptions(): Required<LibrarySearch> {
|
||||||
|
return { query: '', type: 'All', topic: 'All' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class LibraryListWidget extends ListWidget<
|
export class LibraryListWidget extends ListWidget<
|
||||||
@ -35,7 +42,8 @@ export class LibraryListWidget extends ListWidget<
|
|||||||
constructor(
|
constructor(
|
||||||
@inject(LibraryService) private service: LibraryService,
|
@inject(LibraryService) private service: LibraryService,
|
||||||
@inject(ListItemRenderer) itemRenderer: ListItemRenderer<LibraryPackage>,
|
@inject(ListItemRenderer) itemRenderer: ListItemRenderer<LibraryPackage>,
|
||||||
@inject(LibraryFilterRenderer) filterRenderer: LibraryFilterRenderer
|
@inject(LibraryListWidgetSearchOptions)
|
||||||
|
searchOptions: LibraryListWidgetSearchOptions
|
||||||
) {
|
) {
|
||||||
super({
|
super({
|
||||||
id: LibraryListWidget.WIDGET_ID,
|
id: LibraryListWidget.WIDGET_ID,
|
||||||
@ -45,8 +53,7 @@ export class LibraryListWidget extends ListWidget<
|
|||||||
installable: service,
|
installable: service,
|
||||||
itemLabel: (item: LibraryPackage) => item.name,
|
itemLabel: (item: LibraryPackage) => item.name,
|
||||||
itemRenderer,
|
itemRenderer,
|
||||||
filterRenderer,
|
searchOptions,
|
||||||
defaultSearchOptions: { query: '', type: 'All', topic: 'All' },
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,17 +1,30 @@
|
|||||||
import { nls } from '@theia/core/lib/common';
|
import { Command } from '@theia/core/lib/common/command';
|
||||||
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
|
import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
|
||||||
import { injectable } from '@theia/core/shared/inversify';
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
import { LibraryPackage, LibrarySearch } from '../../common/protocol';
|
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 { URI } from '../contributions/contribution';
|
||||||
import { ArduinoMenus } from '../menu/arduino-menus';
|
import { ArduinoMenus } from '../menu/arduino-menus';
|
||||||
|
import { MenuActionTemplate, SubmenuTemplate } from '../menu/register-menu';
|
||||||
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
import { ListWidgetFrontendContribution } from '../widgets/component-list/list-widget-frontend-contribution';
|
||||||
import { LibraryListWidget } from './library-list-widget';
|
import {
|
||||||
|
LibraryListWidget,
|
||||||
|
LibraryListWidgetSearchOptions,
|
||||||
|
} from './library-list-widget';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
export class LibraryListWidgetFrontendContribution extends ListWidgetFrontendContribution<
|
||||||
LibraryPackage,
|
LibraryPackage,
|
||||||
LibrarySearch
|
LibrarySearch
|
||||||
> {
|
> {
|
||||||
|
@inject(LibraryListWidgetSearchOptions)
|
||||||
|
protected readonly searchOptions: LibraryListWidgetSearchOptions;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
widgetId: LibraryListWidget.WIDGET_ID,
|
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 {
|
try {
|
||||||
LibrarySearch.UriParser.parse(uri);
|
LibrarySearch.UriParser.parse(uri);
|
||||||
return true;
|
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);
|
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,3 +1,4 @@
|
|||||||
|
import { RenderContextMenuOptions } from '@theia/core/lib/browser';
|
||||||
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
|
import { CommonMenus } from '@theia/core/lib/browser/common-frontend-contribution';
|
||||||
import {
|
import {
|
||||||
MAIN_MENU_BAR,
|
MAIN_MENU_BAR,
|
||||||
@ -244,3 +245,13 @@ export class PlaceholderMenuNode implements MenuNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const examplesLabel = nls.localize('arduino/examples/menu', 'Examples');
|
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
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}`;
|
||||||
|
}
|
||||||
|
}
|
82
arduino-ide-extension/src/browser/style/hover-service.css
Normal file
82
arduino-ide-extension/src/browser/style/hover-service.css
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theia-hover a:hover {
|
||||||
|
color: var(--theia-textLink-active-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--arduino-button-height: 28px;
|
--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. */
|
/* Revive of the `--theia-icon-loading`. The variable has been removed from Theia while IDE2 still uses is. */
|
||||||
@ -68,9 +69,9 @@ body.theia-dark {
|
|||||||
|
|
||||||
/* Makes the sidepanel a bit wider when opening the widget */
|
/* Makes the sidepanel a bit wider when opening the widget */
|
||||||
.p-DockPanel-widget {
|
.p-DockPanel-widget {
|
||||||
min-width: 220px;
|
min-width: var(--arduino-side-panel-min-width);
|
||||||
min-height: 20px;
|
min-height: 20px;
|
||||||
height: 220px;
|
height: var(--arduino-side-panel-min-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Overrule the default Theia CSS button styles. */
|
/* Overrule the default Theia CSS button styles. */
|
||||||
|
225
arduino-ide-extension/src/browser/theia/core/hover-service.ts
Normal file
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;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
@ -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,11 @@ import { ExecuteWithProgress } from '../../../common/protocol/progressible';
|
|||||||
import { Installable } from '../../../common/protocol/installable';
|
import { Installable } from '../../../common/protocol/installable';
|
||||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||||
import { SearchBar } from './search-bar';
|
import { SearchBar } from './search-bar';
|
||||||
import { ListWidget } from './list-widget';
|
import { ListWidget, ListWidgetSearchOptions } from './list-widget';
|
||||||
import { ComponentList } from './component-list';
|
import { ComponentList } from './component-list';
|
||||||
import { ListItemRenderer } from './list-item-renderer';
|
import { ListItemRenderer } from './list-item-renderer';
|
||||||
import { ResponseServiceClient } from '../../../common/protocol';
|
import { ResponseServiceClient } from '../../../common/protocol';
|
||||||
import { nls } from '@theia/core/lib/common';
|
import { nls } from '@theia/core/lib/common';
|
||||||
import { FilterRenderer } from './filter-renderer';
|
|
||||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||||
|
|
||||||
export class FilterableListContainer<
|
export class FilterableListContainer<
|
||||||
@ -29,7 +28,7 @@ export class FilterableListContainer<
|
|||||||
constructor(props: Readonly<FilterableListContainer.Props<T, S>>) {
|
constructor(props: Readonly<FilterableListContainer.Props<T, S>>) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
searchOptions: props.defaultSearchOptions,
|
searchOptions: props.searchOptions.options,
|
||||||
items: [],
|
items: [],
|
||||||
};
|
};
|
||||||
this.toDispose = new DisposableCollection();
|
this.toDispose = new DisposableCollection();
|
||||||
@ -39,7 +38,7 @@ export class FilterableListContainer<
|
|||||||
this.search = debounce(this.search, 500, { trailing: true });
|
this.search = debounce(this.search, 500, { trailing: true });
|
||||||
this.search(this.state.searchOptions);
|
this.search(this.state.searchOptions);
|
||||||
this.toDispose.pushAll([
|
this.toDispose.pushAll([
|
||||||
this.props.searchOptionsDidChange((newSearchOptions) => {
|
this.props.searchOptions.onDidChange((newSearchOptions) => {
|
||||||
const { searchOptions } = this.state;
|
const { searchOptions } = this.state;
|
||||||
this.setSearchOptionsAndUpdate({
|
this.setSearchOptionsAndUpdate({
|
||||||
...searchOptions,
|
...searchOptions,
|
||||||
@ -64,7 +63,6 @@ export class FilterableListContainer<
|
|||||||
return (
|
return (
|
||||||
<div className={'filterable-list-container'}>
|
<div className={'filterable-list-container'}>
|
||||||
{this.renderSearchBar()}
|
{this.renderSearchBar()}
|
||||||
{this.renderSearchFilter()}
|
|
||||||
<div className="filterable-list-container">
|
<div className="filterable-list-container">
|
||||||
{this.renderComponentList()}
|
{this.renderComponentList()}
|
||||||
</div>
|
</div>
|
||||||
@ -72,17 +70,6 @@ export class FilterableListContainer<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected renderSearchFilter(): React.ReactNode {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{this.props.filterRenderer.render(
|
|
||||||
this.state.searchOptions,
|
|
||||||
this.handlePropChange.bind(this)
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected renderSearchBar(): React.ReactNode {
|
protected renderSearchBar(): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<SearchBar
|
<SearchBar
|
||||||
@ -115,7 +102,7 @@ export class FilterableListContainer<
|
|||||||
...this.state.searchOptions,
|
...this.state.searchOptions,
|
||||||
[prop]: value,
|
[prop]: value,
|
||||||
};
|
};
|
||||||
this.setSearchOptionsAndUpdate(searchOptions);
|
this.props.searchOptions.update(searchOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
private setSearchOptionsAndUpdate(searchOptions: S) {
|
private setSearchOptionsAndUpdate(searchOptions: S) {
|
||||||
@ -180,14 +167,12 @@ export namespace FilterableListContainer {
|
|||||||
T extends ArduinoComponent,
|
T extends ArduinoComponent,
|
||||||
S extends Searchable.Options
|
S extends Searchable.Options
|
||||||
> {
|
> {
|
||||||
readonly defaultSearchOptions: S;
|
readonly searchOptions: ListWidgetSearchOptions<S>;
|
||||||
readonly container: ListWidget<T, S>;
|
readonly container: ListWidget<T, S>;
|
||||||
readonly searchable: Searchable<T, S>;
|
readonly searchable: Searchable<T, S>;
|
||||||
readonly itemLabel: (item: T) => string;
|
readonly itemLabel: (item: T) => string;
|
||||||
readonly itemRenderer: ListItemRenderer<T>;
|
readonly itemRenderer: ListItemRenderer<T>;
|
||||||
readonly filterRenderer: FilterRenderer<S>;
|
|
||||||
readonly resolveFocus: (element: HTMLElement | undefined) => void;
|
readonly resolveFocus: (element: HTMLElement | undefined) => void;
|
||||||
readonly searchOptionsDidChange: Event<Partial<S> | undefined>;
|
|
||||||
readonly messageService: MessageService;
|
readonly messageService: MessageService;
|
||||||
readonly responseService: ResponseServiceClient;
|
readonly responseService: ResponseServiceClient;
|
||||||
readonly onDidShow: Event<void>;
|
readonly onDidShow: Event<void>;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { ApplicationError } from '@theia/core';
|
|
||||||
import {
|
import {
|
||||||
Anchor,
|
Anchor,
|
||||||
ContextMenuRenderer,
|
ContextMenuRenderer,
|
||||||
@ -6,20 +5,14 @@ import {
|
|||||||
import { TabBarToolbar } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
import { TabBarToolbar } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||||
import { codicon } from '@theia/core/lib/browser/widgets/widget';
|
import { codicon } from '@theia/core/lib/browser/widgets/widget';
|
||||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||||
|
import { ApplicationError } from '@theia/core/lib/common/application-error';
|
||||||
import {
|
import {
|
||||||
CommandHandler,
|
|
||||||
CommandRegistry,
|
CommandRegistry,
|
||||||
CommandService,
|
CommandService,
|
||||||
} from '@theia/core/lib/common/command';
|
} from '@theia/core/lib/common/command';
|
||||||
import {
|
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||||
Disposable,
|
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
|
||||||
DisposableCollection,
|
import { MenuModelRegistry, MenuPath } from '@theia/core/lib/common/menu';
|
||||||
} from '@theia/core/lib/common/disposable';
|
|
||||||
import {
|
|
||||||
MenuModelRegistry,
|
|
||||||
MenuPath,
|
|
||||||
SubMenuOptions,
|
|
||||||
} from '@theia/core/lib/common/menu';
|
|
||||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||||
import { nls } from '@theia/core/lib/common/nls';
|
import { nls } from '@theia/core/lib/common/nls';
|
||||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||||
@ -33,6 +26,7 @@ import {
|
|||||||
SketchContainer,
|
SketchContainer,
|
||||||
SketchesService,
|
SketchesService,
|
||||||
SketchRef,
|
SketchRef,
|
||||||
|
TopicLabel,
|
||||||
} from '../../../common/protocol';
|
} from '../../../common/protocol';
|
||||||
import type { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
import type { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
||||||
import { Installable } from '../../../common/protocol/installable';
|
import { Installable } from '../../../common/protocol/installable';
|
||||||
@ -40,8 +34,14 @@ import { openClonedExample } from '../../contributions/examples';
|
|||||||
import {
|
import {
|
||||||
ArduinoMenus,
|
ArduinoMenus,
|
||||||
examplesLabel,
|
examplesLabel,
|
||||||
unregisterSubmenu,
|
showDisabledContextMenuOptions,
|
||||||
} from '../../menu/arduino-menus';
|
} 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 moreInfoLabel = nls.localize('arduino/component/moreInfo', 'More info');
|
||||||
const otherVersionsLabel = nls.localize(
|
const otherVersionsLabel = nls.localize(
|
||||||
@ -63,9 +63,6 @@ function installVersionLabel(selectedVersion: string) {
|
|||||||
const updateLabel = nls.localize('arduino/component/update', 'Update');
|
const updateLabel = nls.localize('arduino/component/update', 'Update');
|
||||||
const removeLabel = nls.localize('arduino/component/remove', 'Remove');
|
const removeLabel = nls.localize('arduino/component/remove', 'Remove');
|
||||||
const byLabel = nls.localize('arduino/component/by', 'by');
|
const byLabel = nls.localize('arduino/component/by', 'by');
|
||||||
function nameAuthorLabel(name: string, author: string) {
|
|
||||||
return nls.localize('arduino/component/title', '{0} by {1}', name, author);
|
|
||||||
}
|
|
||||||
function installedLabel(installedVersion: string) {
|
function installedLabel(installedVersion: string) {
|
||||||
return nls.localize(
|
return nls.localize(
|
||||||
'arduino/component/installed',
|
'arduino/component/installed',
|
||||||
@ -81,39 +78,6 @@ function clickToOpenInBrowserLabel(href: string): string | undefined {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MenuTemplate {
|
|
||||||
readonly menuLabel: string;
|
|
||||||
}
|
|
||||||
interface MenuActionTemplate extends MenuTemplate {
|
|
||||||
readonly menuPath: MenuPath;
|
|
||||||
readonly handler: CommandHandler;
|
|
||||||
/**
|
|
||||||
* If not defined the insertion oder will be the order string.
|
|
||||||
*/
|
|
||||||
readonly order?: string;
|
|
||||||
}
|
|
||||||
interface SubmenuTemplate extends MenuTemplate {
|
|
||||||
readonly menuLabel: string;
|
|
||||||
readonly submenuPath: MenuPath;
|
|
||||||
readonly options?: SubMenuOptions;
|
|
||||||
}
|
|
||||||
function isMenuTemplate(arg: unknown): arg is MenuTemplate {
|
|
||||||
return (
|
|
||||||
typeof arg === 'object' &&
|
|
||||||
(arg as MenuTemplate).menuLabel !== undefined &&
|
|
||||||
typeof (arg as MenuTemplate).menuLabel === 'string'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class ArduinoComponentContextMenuRenderer {
|
export class ArduinoComponentContextMenuRenderer {
|
||||||
@inject(CommandRegistry)
|
@inject(CommandRegistry)
|
||||||
@ -124,54 +88,26 @@ export class ArduinoComponentContextMenuRenderer {
|
|||||||
private readonly contextMenuRenderer: ContextMenuRenderer;
|
private readonly contextMenuRenderer: ContextMenuRenderer;
|
||||||
|
|
||||||
private readonly toDisposeBeforeRender = new DisposableCollection();
|
private readonly toDisposeBeforeRender = new DisposableCollection();
|
||||||
private menuIndexCounter = 0;
|
|
||||||
|
|
||||||
async render(
|
async render(
|
||||||
anchor: Anchor,
|
anchor: Anchor,
|
||||||
...templates: (MenuActionTemplate | SubmenuTemplate)[]
|
...templates: Array<MenuActionTemplate | SubmenuTemplate>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.toDisposeBeforeRender.dispose();
|
this.toDisposeBeforeRender.dispose();
|
||||||
this.toDisposeBeforeRender.pushAll([
|
this.toDisposeBeforeRender.push(
|
||||||
Disposable.create(() => (this.menuIndexCounter = 0)),
|
registerMenus({
|
||||||
...templates.map((template) => this.registerMenu(template)),
|
contextId: 'component',
|
||||||
]);
|
commandRegistry: this.commandRegistry,
|
||||||
const options = {
|
menuRegistry: this.menuRegistry,
|
||||||
|
templates,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const options = showDisabledContextMenuOptions({
|
||||||
menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT,
|
menuPath: ArduinoMenus.ARDUINO_COMPONENT__CONTEXT,
|
||||||
anchor,
|
anchor,
|
||||||
showDisabled: true,
|
});
|
||||||
};
|
|
||||||
this.contextMenuRenderer.render(options);
|
this.contextMenuRenderer.render(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerMenu(
|
|
||||||
template: MenuActionTemplate | SubmenuTemplate
|
|
||||||
): Disposable {
|
|
||||||
if (isMenuActionTemplate(template)) {
|
|
||||||
const { menuLabel, menuPath, handler, order } = template;
|
|
||||||
const id = this.generateCommandId(menuLabel, menuPath);
|
|
||||||
const index = this.menuIndexCounter++;
|
|
||||||
return new DisposableCollection(
|
|
||||||
this.commandRegistry.registerCommand({ id }, handler),
|
|
||||||
this.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(
|
|
||||||
this.menuRegistry.registerSubmenu(submenuPath, menuLabel, options),
|
|
||||||
Disposable.create(() =>
|
|
||||||
unregisterSubmenu(submenuPath, this.menuRegistry)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateCommandId(menuLabel: string, menuPath: MenuPath): string {
|
|
||||||
return `arduino--component-context-${menuPath.join('-')}-${menuLabel}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListItemRendererParams<T extends ArduinoComponent> {
|
interface ListItemRendererParams<T extends ArduinoComponent> {
|
||||||
@ -201,6 +137,8 @@ export class ListItemRenderer<T extends ArduinoComponent> {
|
|||||||
private readonly messageService: MessageService;
|
private readonly messageService: MessageService;
|
||||||
@inject(CommandService)
|
@inject(CommandService)
|
||||||
private readonly commandService: CommandService;
|
private readonly commandService: CommandService;
|
||||||
|
@inject(HoverService)
|
||||||
|
private readonly hoverService: HoverService;
|
||||||
@inject(CoreService)
|
@inject(CoreService)
|
||||||
private readonly coreService: CoreService;
|
private readonly coreService: CoreService;
|
||||||
@inject(ExamplesService)
|
@inject(ExamplesService)
|
||||||
@ -216,12 +154,26 @@ export class ListItemRenderer<T extends ArduinoComponent> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private readonly showHover = (
|
||||||
|
event: React.MouseEvent<HTMLElement>,
|
||||||
|
markdown: string
|
||||||
|
) => {
|
||||||
|
this.hoverService.requestHover({
|
||||||
|
content: new MarkdownStringImpl(markdown),
|
||||||
|
target: event.currentTarget,
|
||||||
|
position: 'right',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
renderItem(params: ListItemRendererParams<T>): React.ReactNode {
|
renderItem(params: ListItemRendererParams<T>): React.ReactNode {
|
||||||
const action = this.action(params);
|
const action = this.action(params);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="component-list-item noselect">
|
<div
|
||||||
|
className="component-list-item noselect"
|
||||||
|
onMouseEnter={(event) => this.showHover(event, this.markdown(params))}
|
||||||
|
>
|
||||||
<Header
|
<Header
|
||||||
params={params}
|
params={params}
|
||||||
action={action}
|
action={action}
|
||||||
@ -247,6 +199,30 @@ export class ListItemRenderer<T extends ArduinoComponent> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 !== 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 {
|
private get services(): ListItemRendererServices {
|
||||||
return {
|
return {
|
||||||
windowService: this.windowService,
|
windowService: this.windowService,
|
||||||
@ -361,7 +337,7 @@ class Toolbar<T extends ArduinoComponent> extends React.Component<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private get examples(): Promise<(MenuActionTemplate | SubmenuTemplate)[]> {
|
private get examples(): Promise<Array<MenuActionTemplate | SubmenuTemplate>> {
|
||||||
const {
|
const {
|
||||||
params: {
|
params: {
|
||||||
item,
|
item,
|
||||||
@ -394,8 +370,8 @@ class Toolbar<T extends ArduinoComponent> extends React.Component<
|
|||||||
container: SketchContainer,
|
container: SketchContainer,
|
||||||
menuPath: MenuPath,
|
menuPath: MenuPath,
|
||||||
depth = 0
|
depth = 0
|
||||||
): (MenuActionTemplate | SubmenuTemplate)[] {
|
): Array<MenuActionTemplate | SubmenuTemplate> {
|
||||||
const templates: (MenuActionTemplate | SubmenuTemplate)[] = [];
|
const templates: Array<MenuActionTemplate | SubmenuTemplate> = [];
|
||||||
const { label } = container;
|
const { label } = container;
|
||||||
if (depth > 0) {
|
if (depth > 0) {
|
||||||
menuPath = [...menuPath, label];
|
menuPath = [...menuPath, label];
|
||||||
@ -464,7 +440,7 @@ class Toolbar<T extends ArduinoComponent> extends React.Component<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private get otherVersions(): (MenuActionTemplate | SubmenuTemplate)[] {
|
private get otherVersions(): Array<MenuActionTemplate | SubmenuTemplate> {
|
||||||
const {
|
const {
|
||||||
params: {
|
params: {
|
||||||
item: { availableVersions },
|
item: { availableVersions },
|
||||||
@ -566,10 +542,8 @@ class Title<T extends ArduinoComponent> extends React.Component<
|
|||||||
> {
|
> {
|
||||||
override render(): React.ReactNode {
|
override render(): React.ReactNode {
|
||||||
const { name, author } = this.props.params.item;
|
const { name, author } = this.props.params.item;
|
||||||
const title =
|
|
||||||
name && author ? nameAuthorLabel(name, author) : name ? name : Unknown;
|
|
||||||
return (
|
return (
|
||||||
<div className="title" title={title}>
|
<div className="title">
|
||||||
{name && author ? (
|
{name && author ? (
|
||||||
<>
|
<>
|
||||||
{<span className="name">{name}</span>}{' '}
|
{<span className="name">{name}</span>}{' '}
|
||||||
@ -627,7 +601,7 @@ class Content<T extends ArduinoComponent> extends React.Component<
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
const content = [summary, description].filter(Boolean).join(' ');
|
const content = [summary, description].filter(Boolean).join(' ');
|
||||||
return (
|
return (
|
||||||
<div className="content" title={content}>
|
<div className="content">
|
||||||
<p>{content}</p>
|
<p>{content}</p>
|
||||||
<MoreInfo {...this.props} />
|
<MoreInfo {...this.props} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -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 { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
|
||||||
import {
|
import {
|
||||||
OpenerOptions,
|
OpenerOptions,
|
||||||
OpenHandler,
|
OpenHandler,
|
||||||
} from '@theia/core/lib/browser/opener-service';
|
} 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 { 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 { 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 { Searchable } from '../../../common/protocol';
|
||||||
import { ArduinoComponent } from '../../../common/protocol/arduino-component';
|
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()
|
@injectable()
|
||||||
export abstract class ListWidgetFrontendContribution<
|
export abstract class ListWidgetFrontendContribution<
|
||||||
@ -17,14 +41,32 @@ export abstract class ListWidgetFrontendContribution<
|
|||||||
S extends Searchable.Options
|
S extends Searchable.Options
|
||||||
>
|
>
|
||||||
extends AbstractViewContribution<ListWidget<T, S>>
|
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}`;
|
readonly id: string = `http-opener-${this.viewId}`;
|
||||||
|
|
||||||
async initializeLayout(): Promise<void> {
|
async initializeLayout(): Promise<void> {
|
||||||
this.openView();
|
this.openView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onStop(): void {
|
||||||
|
this.toDisposeBeforeShowContextMenu.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
override registerMenus(_: MenuModelRegistry): void {
|
override registerMenus(_: MenuModelRegistry): void {
|
||||||
// NOOP
|
// NOOP
|
||||||
@ -62,4 +104,131 @@ export abstract class ListWidgetFrontendContribution<
|
|||||||
|
|
||||||
protected abstract canParse(uri: URI): boolean;
|
protected abstract canParse(uri: URI): boolean;
|
||||||
protected abstract parse(uri: URI): S | undefined;
|
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';
|
} from '@theia/core/shared/inversify';
|
||||||
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
import { Widget } from '@theia/core/shared/@phosphor/widgets';
|
||||||
import { Message } from '@theia/core/shared/@phosphor/messaging';
|
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 { Deferred } from '@theia/core/lib/common/promise-util';
|
||||||
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
|
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
|
||||||
import { CommandService } from '@theia/core/lib/common/command';
|
import { CommandService } from '@theia/core/lib/common/command';
|
||||||
@ -20,13 +20,16 @@ import {
|
|||||||
import { FilterableListContainer } from './filterable-list-container';
|
import { FilterableListContainer } from './filterable-list-container';
|
||||||
import { ListItemRenderer } from './list-item-renderer';
|
import { ListItemRenderer } from './list-item-renderer';
|
||||||
import { NotificationCenter } from '../../notification-center';
|
import { NotificationCenter } from '../../notification-center';
|
||||||
import { FilterRenderer } from './filter-renderer';
|
import { StatefulWidget } from '@theia/core/lib/browser';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export abstract class ListWidget<
|
export abstract class ListWidget<
|
||||||
T extends ArduinoComponent,
|
T extends ArduinoComponent,
|
||||||
S extends Searchable.Options
|
S extends Searchable.Options
|
||||||
> extends ReactWidget {
|
>
|
||||||
|
extends ReactWidget
|
||||||
|
implements StatefulWidget
|
||||||
|
{
|
||||||
@inject(MessageService)
|
@inject(MessageService)
|
||||||
protected readonly messageService: MessageService;
|
protected readonly messageService: MessageService;
|
||||||
@inject(NotificationCenter)
|
@inject(NotificationCenter)
|
||||||
@ -41,9 +44,7 @@ export abstract class ListWidget<
|
|||||||
*/
|
*/
|
||||||
private focusNode: HTMLElement | undefined;
|
private focusNode: HTMLElement | undefined;
|
||||||
private readonly didReceiveFirstFocus = new Deferred();
|
private readonly didReceiveFirstFocus = new Deferred();
|
||||||
private readonly searchOptionsChangeEmitter = new Emitter<
|
private readonly searchOptions: ListWidgetSearchOptions<S>;
|
||||||
Partial<S> | undefined
|
|
||||||
>();
|
|
||||||
private readonly onDidShowEmitter = new Emitter<void>();
|
private readonly onDidShowEmitter = new Emitter<void>();
|
||||||
/**
|
/**
|
||||||
* Instead of running an `update` from the `postConstruct` `init` method,
|
* Instead of running an `update` from the `postConstruct` `init` method,
|
||||||
@ -53,7 +54,7 @@ export abstract class ListWidget<
|
|||||||
|
|
||||||
constructor(protected options: ListWidget.Options<T, S>) {
|
constructor(protected options: ListWidget.Options<T, S>) {
|
||||||
super();
|
super();
|
||||||
const { id, label, iconClass } = options;
|
const { id, label, iconClass, searchOptions } = options;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.title.label = label;
|
this.title.label = label;
|
||||||
this.title.caption = label;
|
this.title.caption = label;
|
||||||
@ -62,10 +63,8 @@ export abstract class ListWidget<
|
|||||||
this.addClass('arduino-list-widget');
|
this.addClass('arduino-list-widget');
|
||||||
this.node.tabIndex = 0; // To be able to set the focus on the widget.
|
this.node.tabIndex = 0; // To be able to set the focus on the widget.
|
||||||
this.scrollOptions = undefined;
|
this.scrollOptions = undefined;
|
||||||
this.toDispose.pushAll([
|
this.searchOptions = searchOptions;
|
||||||
this.searchOptionsChangeEmitter,
|
this.toDispose.push(this.onDidShowEmitter);
|
||||||
this.onDidShowEmitter,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@postConstruct()
|
@postConstruct()
|
||||||
@ -79,6 +78,16 @@ 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 {
|
protected override onAfterShow(message: Message): void {
|
||||||
this.maybeUpdateOnFirstRender();
|
this.maybeUpdateOnFirstRender();
|
||||||
super.onAfterShow(message);
|
super.onAfterShow(message);
|
||||||
@ -141,7 +150,7 @@ export abstract class ListWidget<
|
|||||||
override render(): React.ReactNode {
|
override render(): React.ReactNode {
|
||||||
return (
|
return (
|
||||||
<FilterableListContainer<T, S>
|
<FilterableListContainer<T, S>
|
||||||
defaultSearchOptions={this.options.defaultSearchOptions}
|
searchOptions={this.searchOptions}
|
||||||
container={this}
|
container={this}
|
||||||
resolveFocus={this.onFocusResolved}
|
resolveFocus={this.onFocusResolved}
|
||||||
searchable={this.options.searchable}
|
searchable={this.options.searchable}
|
||||||
@ -149,8 +158,6 @@ export abstract class ListWidget<
|
|||||||
uninstall={this.uninstall.bind(this)}
|
uninstall={this.uninstall.bind(this)}
|
||||||
itemLabel={this.options.itemLabel}
|
itemLabel={this.options.itemLabel}
|
||||||
itemRenderer={this.options.itemRenderer}
|
itemRenderer={this.options.itemRenderer}
|
||||||
filterRenderer={this.options.filterRenderer}
|
|
||||||
searchOptionsDidChange={this.searchOptionsChangeEmitter.event}
|
|
||||||
messageService={this.messageService}
|
messageService={this.messageService}
|
||||||
commandService={this.commandService}
|
commandService={this.commandService}
|
||||||
responseService={this.responseService}
|
responseService={this.responseService}
|
||||||
@ -164,9 +171,13 @@ export abstract class ListWidget<
|
|||||||
* If it is `undefined`, updates the view state by re-running the search with the current `filterText` term.
|
* If it is `undefined`, updates the view state by re-running the search with the current `filterText` term.
|
||||||
*/
|
*/
|
||||||
refresh(searchOptions: Partial<S> | undefined): void {
|
refresh(searchOptions: Partial<S> | undefined): void {
|
||||||
this.didReceiveFirstFocus.promise.then(() =>
|
this.didReceiveFirstFocus.promise.then(() => {
|
||||||
this.searchOptionsChangeEmitter.fire(searchOptions)
|
if (searchOptions) {
|
||||||
);
|
this.searchOptions.update(searchOptions);
|
||||||
|
} else {
|
||||||
|
this.searchOptions.options = this.searchOptions.options; // triggers a refresh. TODO fix this!
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateScrollBar(): void {
|
updateScrollBar(): void {
|
||||||
@ -188,8 +199,7 @@ export namespace ListWidget {
|
|||||||
readonly searchable: Searchable<T, S>;
|
readonly searchable: Searchable<T, S>;
|
||||||
readonly itemLabel: (item: T) => string;
|
readonly itemLabel: (item: T) => string;
|
||||||
readonly itemRenderer: ListItemRenderer<T>;
|
readonly itemRenderer: ListItemRenderer<T>;
|
||||||
readonly filterRenderer: FilterRenderer<S>;
|
readonly searchOptions: ListWidgetSearchOptions<S>;
|
||||||
readonly defaultSearchOptions: S;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,3 +209,57 @@ export class UserAbortError extends Error {
|
|||||||
Object.setPrototypeOf(this, UserAbortError.prototype);
|
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>;
|
||||||
|
}
|
||||||
|
@ -3,6 +3,10 @@ import { nls } from '@theia/core/lib/common/nls';
|
|||||||
export const Unknown = nls.localize('arduino/common/unknown', 'Unknown');
|
export const Unknown = nls.localize('arduino/common/unknown', 'Unknown');
|
||||||
export const Later = nls.localize('arduino/common/later', 'Later');
|
export const Later = nls.localize('arduino/common/later', 'Later');
|
||||||
export const Updatable = nls.localize('arduino/common/updateable', 'Updatable');
|
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 All = nls.localize('arduino/common/all', 'All');
|
||||||
export const Type = nls.localize('arduino/common/type', 'Type');
|
export const Type = nls.localize('arduino/common/type', 'Type');
|
||||||
export const Partner = nls.localize('arduino/common/partner', 'Partner');
|
export const Partner = nls.localize('arduino/common/partner', 'Partner');
|
||||||
|
@ -6,6 +6,7 @@ import { nls } from '@theia/core/lib/common/nls';
|
|||||||
import {
|
import {
|
||||||
All,
|
All,
|
||||||
Contributed,
|
Contributed,
|
||||||
|
Installed,
|
||||||
Partner,
|
Partner,
|
||||||
Type as TypeLabel,
|
Type as TypeLabel,
|
||||||
Updatable,
|
Updatable,
|
||||||
@ -174,6 +175,7 @@ export namespace BoardSearch {
|
|||||||
export const TypeLiterals = [
|
export const TypeLiterals = [
|
||||||
'All',
|
'All',
|
||||||
'Updatable',
|
'Updatable',
|
||||||
|
'Installed',
|
||||||
'Arduino',
|
'Arduino',
|
||||||
'Contributed',
|
'Contributed',
|
||||||
'Arduino Certified',
|
'Arduino Certified',
|
||||||
@ -189,6 +191,7 @@ export namespace BoardSearch {
|
|||||||
export const TypeLabels: Record<Type, string> = {
|
export const TypeLabels: Record<Type, string> = {
|
||||||
All: All,
|
All: All,
|
||||||
Updatable: Updatable,
|
Updatable: Updatable,
|
||||||
|
Installed: Installed,
|
||||||
Arduino: 'Arduino',
|
Arduino: 'Arduino',
|
||||||
Contributed: Contributed,
|
Contributed: Contributed,
|
||||||
'Arduino Certified': nls.localize(
|
'Arduino Certified': nls.localize(
|
||||||
|
@ -5,6 +5,7 @@ import { nls } from '@theia/core/lib/common/nls';
|
|||||||
import {
|
import {
|
||||||
All,
|
All,
|
||||||
Contributed,
|
Contributed,
|
||||||
|
Installed,
|
||||||
Partner,
|
Partner,
|
||||||
Recommended,
|
Recommended,
|
||||||
Retired,
|
Retired,
|
||||||
@ -13,6 +14,11 @@ import {
|
|||||||
} from '../nls';
|
} from '../nls';
|
||||||
import URI from '@theia/core/lib/common/uri';
|
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 LibraryServicePath = '/services/library-service';
|
||||||
export const LibraryService = Symbol('LibraryService');
|
export const LibraryService = Symbol('LibraryService');
|
||||||
export interface LibraryService
|
export interface LibraryService
|
||||||
@ -76,7 +82,7 @@ export namespace LibrarySearch {
|
|||||||
export const TypeLabels: Record<Type, string> = {
|
export const TypeLabels: Record<Type, string> = {
|
||||||
All: All,
|
All: All,
|
||||||
Updatable: Updatable,
|
Updatable: Updatable,
|
||||||
Installed: nls.localize('arduino/libraryType/installed', 'Installed'),
|
Installed: Installed,
|
||||||
Arduino: 'Arduino',
|
Arduino: 'Arduino',
|
||||||
Partner: Partner,
|
Partner: Partner,
|
||||||
Recommended: Recommended,
|
Recommended: Recommended,
|
||||||
@ -137,7 +143,7 @@ export namespace LibrarySearch {
|
|||||||
keyof Omit<LibrarySearch, 'query'>,
|
keyof Omit<LibrarySearch, 'query'>,
|
||||||
string
|
string
|
||||||
> = {
|
> = {
|
||||||
topic: nls.localize('arduino/librarySearchProperty/topic', 'Topic'),
|
topic: TopicLabel,
|
||||||
type: TypeLabel,
|
type: TypeLabel,
|
||||||
};
|
};
|
||||||
export namespace UriParser {
|
export namespace UriParser {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import URI from '@theia/core/lib/common/uri';
|
import URI from '@theia/core/lib/common/uri';
|
||||||
import type { ArduinoComponent } from './arduino-component';
|
import type { ArduinoComponent } from './arduino-component';
|
||||||
|
|
||||||
|
export const Updatable = { type: 'Updatable' } as const;
|
||||||
|
|
||||||
export interface Searchable<T, O extends Searchable.Options> {
|
export interface Searchable<T, O extends Searchable.Options> {
|
||||||
search(options: O): Promise<T[]>;
|
search(options: O): Promise<T[]>;
|
||||||
}
|
}
|
||||||
|
@ -402,8 +402,8 @@ export class BoardsServiceImpl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filter = this.typePredicate(options);
|
const typeFilter = this.typePredicate(options);
|
||||||
const boardsPackages = [...packages.values()].filter(filter);
|
const boardsPackages = [...packages.values()].filter(typeFilter);
|
||||||
return sortComponents(boardsPackages, boardsPackageSortGroup);
|
return sortComponents(boardsPackages, boardsPackageSortGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -415,6 +415,8 @@ export class BoardsServiceImpl
|
|||||||
return () => true;
|
return () => true;
|
||||||
}
|
}
|
||||||
switch (options.type) {
|
switch (options.type) {
|
||||||
|
case 'Installed':
|
||||||
|
return Installable.Installed;
|
||||||
case 'Updatable':
|
case 'Updatable':
|
||||||
return Installable.Updateable;
|
return Installable.Updateable;
|
||||||
case 'Arduino':
|
case 'Arduino':
|
||||||
|
@ -221,8 +221,8 @@ export class LibraryServiceImpl
|
|||||||
{
|
{
|
||||||
name: library.getName(),
|
name: library.getName(),
|
||||||
installedVersion,
|
installedVersion,
|
||||||
description: library.getSentence(),
|
description: library.getParagraph(),
|
||||||
summary: library.getParagraph(),
|
summary: library.getSentence(),
|
||||||
moreInfoLink: library.getWebsite(),
|
moreInfoLink: library.getWebsite(),
|
||||||
includes: library.getProvidesIncludesList(),
|
includes: library.getProvidesIncludesList(),
|
||||||
location: this.mapLocation(library.getLocation()),
|
location: this.mapLocation(library.getLocation()),
|
||||||
@ -462,9 +462,9 @@ function toLibrary(
|
|||||||
author: lib.getAuthor(),
|
author: lib.getAuthor(),
|
||||||
availableVersions,
|
availableVersions,
|
||||||
includes: lib.getProvidesIncludesList(),
|
includes: lib.getProvidesIncludesList(),
|
||||||
description: lib.getSentence(),
|
description: lib.getParagraph(),
|
||||||
moreInfoLink: lib.getWebsite(),
|
moreInfoLink: lib.getWebsite(),
|
||||||
summary: lib.getParagraph(),
|
summary: lib.getSentence(),
|
||||||
category: lib.getCategory(),
|
category: lib.getCategory(),
|
||||||
types: lib.getTypesList(),
|
types: lib.getTypesList(),
|
||||||
};
|
};
|
||||||
|
14
i18n/en.json
14
i18n/en.json
@ -46,6 +46,9 @@
|
|||||||
"typeOfPorts": "{0} ports",
|
"typeOfPorts": "{0} ports",
|
||||||
"unknownBoard": "Unknown board"
|
"unknownBoard": "Unknown board"
|
||||||
},
|
},
|
||||||
|
"boards": {
|
||||||
|
"filterBoards": "Filter Boards..."
|
||||||
|
},
|
||||||
"boardsManager": "Boards Manager",
|
"boardsManager": "Boards Manager",
|
||||||
"boardsType": {
|
"boardsType": {
|
||||||
"arduinoCertified": "Arduino Certified"
|
"arduinoCertified": "Arduino Certified"
|
||||||
@ -81,6 +84,10 @@
|
|||||||
"noUpdates": "There are no recent updates available.",
|
"noUpdates": "There are no recent updates available.",
|
||||||
"promptUpdateBoards": "Updates are available for some of your boards.",
|
"promptUpdateBoards": "Updates are available for some of your boards.",
|
||||||
"promptUpdateLibraries": "Updates are available for some of your libraries.",
|
"promptUpdateLibraries": "Updates are available for some of your libraries.",
|
||||||
|
"showBoardsUpdates": "Boards Updates",
|
||||||
|
"showInstalledBoards": "Installed Boards",
|
||||||
|
"showInstalledLibraries": "Installed Libraries",
|
||||||
|
"showLibraryUpdates": "Library Updates",
|
||||||
"updatingBoards": "Updating boards...",
|
"updatingBoards": "Updating boards...",
|
||||||
"updatingLibraries": "Updating libraries..."
|
"updatingLibraries": "Updating libraries..."
|
||||||
},
|
},
|
||||||
@ -169,7 +176,6 @@
|
|||||||
"moreInfo": "More info",
|
"moreInfo": "More info",
|
||||||
"otherVersions": "Other Versions",
|
"otherVersions": "Other Versions",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"title": "{0} by {1}",
|
|
||||||
"uninstall": "Uninstall",
|
"uninstall": "Uninstall",
|
||||||
"uninstallMsg": "Do you want to uninstall {0}?",
|
"uninstallMsg": "Do you want to uninstall {0}?",
|
||||||
"update": "Update"
|
"update": "Update"
|
||||||
@ -242,6 +248,9 @@
|
|||||||
"forAny": "Examples for any board",
|
"forAny": "Examples for any board",
|
||||||
"menu": "Examples"
|
"menu": "Examples"
|
||||||
},
|
},
|
||||||
|
"filter": {
|
||||||
|
"clearAll": "Clear All Filters"
|
||||||
|
},
|
||||||
"firmware": {
|
"firmware": {
|
||||||
"checkUpdates": "Check Updates",
|
"checkUpdates": "Check Updates",
|
||||||
"failedInstall": "Installation failed. Please try again.",
|
"failedInstall": "Installation failed. Please try again.",
|
||||||
@ -282,6 +291,9 @@
|
|||||||
"updateAvailable": "Update Available",
|
"updateAvailable": "Update Available",
|
||||||
"versionDownloaded": "Arduino IDE {0} has been downloaded."
|
"versionDownloaded": "Arduino IDE {0} has been downloaded."
|
||||||
},
|
},
|
||||||
|
"libraries": {
|
||||||
|
"filterLibraries": "Filter Libraries..."
|
||||||
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"addZip": "Add .ZIP Library...",
|
"addZip": "Add .ZIP Library...",
|
||||||
"arduinoLibraries": "Arduino libraries",
|
"arduinoLibraries": "Arduino libraries",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user