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