From e832ef7558c9dce5cab17d48644cd42d9ad5a731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=A4der?= Date: Tue, 23 May 2023 09:32:16 +0200 Subject: [PATCH] Show "Collapse All" command in tree view toolbar. (#12514) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contributed on behalf of STMicroelectronics Signed-off-by: Thomas Mäder --- packages/core/src/browser/view-container.ts | 21 ++++++++++ .../plugin-ext/src/common/plugin-api-rpc.ts | 1 + .../browser/plugin-ext-frontend-module.ts | 2 + .../browser/plugin-frontend-contribution.ts | 39 ++++++++++++++++++- .../main/browser/view/plugin-view-widget.ts | 11 +++++- .../main/browser/view/tree-view-widget.tsx | 5 +++ .../src/main/browser/view/tree-views-main.ts | 1 + .../plugin-ext/src/plugin/tree/tree-views.ts | 2 +- 8 files changed, 77 insertions(+), 5 deletions(-) diff --git a/packages/core/src/browser/view-container.ts b/packages/core/src/browser/view-container.ts index 459822d4272fe..b9553cd92a6f6 100644 --- a/packages/core/src/browser/view-container.ts +++ b/packages/core/src/browser/view-container.ts @@ -76,6 +76,20 @@ export namespace BadgeWidget { } } +/** + * A widget that may change it's internal structure dynamically. Current use is for + * updating the toolbar when a contributed view is contructed "lazily" + */ +export interface DynamicToolbarWidget { + onDidChangeToolbarItems: CommonEvent; +} + +export namespace DynamicToolbarWidget { + export function is(arg: unknown): arg is DynamicToolbarWidget { + return isObject(arg) && 'onDidChangeToolbarItems' in arg; + } +} + /** * A view container holds an arbitrary number of widgets inside a split panel. * Each widget is wrapped in a _part_ that displays the widget title and toolbar @@ -970,6 +984,13 @@ export class ViewContainerPart extends BaseWidget { this.wrapped.onDidChangeBadgeTooltip(() => this.onDidChangeBadgeTooltipEmitter.fire(), undefined, this.toDispose); } + if (DynamicToolbarWidget.is(this.wrapped)) { + this.wrapped.onDidChangeToolbarItems(() => { + this.toolbar.updateTarget(this.wrapped); + this.viewContainer?.update(); + }); + } + const { header, body, disposable } = this.createContent(); this.header = header; this.body = body; diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index cedcc7f047c2e..d5d279e00ce7d 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -728,6 +728,7 @@ export interface DialogsMain { } export interface RegisterTreeDataProviderOptions { + showCollapseAll?: boolean canSelectMany?: boolean dragMimeTypes?: string[] dropMimeTypes?: string[] diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts index fdc35afaa70c3..0eb5aaf3a99d3 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts +++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts @@ -84,6 +84,7 @@ import { PluginTerminalRegistry } from './plugin-terminal-registry'; import { DnDFileContentStore } from './view/dnd-file-content-store'; import { WebviewContextKeys } from './webview/webview-context-keys'; import { LanguagePackService, languagePackServicePath } from '../../common/language-pack-service'; +import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; export default new ContainerModule((bind, unbind, isBound, rebind) => { @@ -110,6 +111,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(OpenUriCommandHandler).toSelf().inSingletonScope(); bind(PluginApiFrontendContribution).toSelf().inSingletonScope(); bind(CommandContribution).toService(PluginApiFrontendContribution); + bind(TabBarToolbarContribution).toService(PluginApiFrontendContribution); bind(EditorModelService).toSelf().inSingletonScope(); diff --git a/packages/plugin-ext/src/main/browser/plugin-frontend-contribution.ts b/packages/plugin-ext/src/main/browser/plugin-frontend-contribution.ts index ddd600fe17839..47aa6ac37e4b2 100644 --- a/packages/plugin-ext/src/main/browser/plugin-frontend-contribution.ts +++ b/packages/plugin-ext/src/main/browser/plugin-frontend-contribution.ts @@ -15,21 +15,56 @@ // ***************************************************************************** import { injectable, inject } from '@theia/core/shared/inversify'; -import { CommandRegistry, CommandContribution } from '@theia/core/lib/common'; +import { CommandRegistry, CommandContribution, Command } from '@theia/core/lib/common'; import { OpenUriCommandHandler } from './commands'; import URI from '@theia/core/lib/common/uri'; +import { TreeViewWidget } from './view/tree-view-widget'; +import { CompositeTreeNode, Widget, codicon } from '@theia/core/lib/browser'; +import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { PluginViewWidget } from './view/plugin-view-widget'; @injectable() -export class PluginApiFrontendContribution implements CommandContribution { +export class PluginApiFrontendContribution implements CommandContribution, TabBarToolbarContribution { @inject(OpenUriCommandHandler) protected readonly openUriCommandHandler: OpenUriCommandHandler; + static readonly COLLAPSE_ALL_COMMAND = Command.toDefaultLocalizedCommand({ + id: 'treeviews.collapseAll', + iconClass: codicon('collapse-all'), + label: 'Collapse All' + }); + registerCommands(commands: CommandRegistry): void { commands.registerCommand(OpenUriCommandHandler.COMMAND_METADATA, { execute: (arg: URI) => this.openUriCommandHandler.execute(arg), isVisible: () => false }); + commands.registerCommand(PluginApiFrontendContribution.COLLAPSE_ALL_COMMAND, { + execute: (widget: Widget) => { + if (widget instanceof PluginViewWidget && widget.widgets[0] instanceof TreeViewWidget) { + const model = widget.widgets[0].model; + if (CompositeTreeNode.is(model.root)) { + for (const child of model.root.children) { + if (CompositeTreeNode.is(child)) { + model.collapseAll(child); + } + } + } + } + }, + isVisible: (widget: Widget) => widget instanceof PluginViewWidget && widget.widgets[0] instanceof TreeViewWidget && widget.widgets[0].showCollapseAll + }); + } + registerToolbarItems(registry: TabBarToolbarRegistry): void { + registry.registerItem({ + id: PluginApiFrontendContribution.COLLAPSE_ALL_COMMAND.id, + command: PluginApiFrontendContribution.COLLAPSE_ALL_COMMAND.id, + tooltip: PluginApiFrontendContribution.COLLAPSE_ALL_COMMAND.label, + icon: PluginApiFrontendContribution.COLLAPSE_ALL_COMMAND.iconClass, + priority: 1000 + }); + } } diff --git a/packages/plugin-ext/src/main/browser/view/plugin-view-widget.ts b/packages/plugin-ext/src/main/browser/view/plugin-view-widget.ts index a7c2c45929fcf..14a6561ac5081 100644 --- a/packages/plugin-ext/src/main/browser/view/plugin-view-widget.ts +++ b/packages/plugin-ext/src/main/browser/view/plugin-view-widget.ts @@ -21,7 +21,7 @@ import { CommandRegistry } from '@theia/core/lib/common/command'; import { StatefulWidget } from '@theia/core/lib/browser/shell/shell-layout-restorer'; import { Message } from '@theia/core/shared/@phosphor/messaging'; import { TreeViewWidget } from './tree-view-widget'; -import { BadgeWidget, DescriptionWidget } from '@theia/core/lib/browser/view-container'; +import { BadgeWidget, DescriptionWidget, DynamicToolbarWidget } from '@theia/core/lib/browser/view-container'; import { DisposableCollection, Emitter, Event } from '@theia/core/lib/common'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; @@ -32,7 +32,7 @@ export class PluginViewWidgetIdentifier { } @injectable() -export class PluginViewWidget extends Panel implements StatefulWidget, DescriptionWidget, BadgeWidget { +export class PluginViewWidget extends Panel implements StatefulWidget, DescriptionWidget, BadgeWidget, DynamicToolbarWidget { currentViewContainerId?: string; @@ -46,6 +46,11 @@ export class PluginViewWidget extends Panel implements StatefulWidget, Descripti protected onDidChangeBadgeEmitter = new Emitter(); protected onDidChangeBadgeTooltipEmitter = new Emitter(); protected toDispose = new DisposableCollection(this.onDidChangeDescriptionEmitter, this.onDidChangeBadgeEmitter, this.onDidChangeBadgeTooltipEmitter); + protected readonly onDidChangeToolbarItemsEmitter = new Emitter(); + + get onDidChangeToolbarItems(): Event { + return this.onDidChangeToolbarItemsEmitter.event; + } @inject(MenuModelRegistry) protected readonly menus: MenuModelRegistry; @@ -192,11 +197,13 @@ export class PluginViewWidget extends Panel implements StatefulWidget, Descripti widget.onDidChangeBadgeTooltip(() => this.onDidChangeBadgeTooltipEmitter.fire()); } this.updateWidgetMessage(); + this.onDidChangeToolbarItemsEmitter.fire(); } override insertWidget(index: number, widget: Widget): void { super.insertWidget(index, widget); this.updateWidgetMessage(); + this.onDidChangeToolbarItemsEmitter.fire(); } override dispose(): void { diff --git a/packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx b/packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx index 8a95a78310e95..f1ee1790743c7 100644 --- a/packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx +++ b/packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx @@ -164,6 +164,7 @@ export namespace CompositeTreeViewNode { @injectable() export class TreeViewWidgetOptions { id: string; + showCollapseAll: boolean | undefined; multiSelect: boolean | undefined; dragMimeTypes: string[] | undefined; dropMimeTypes: string[] | undefined; @@ -443,6 +444,10 @@ export class TreeViewWidget extends TreeViewWelcomeWidget { this.treeDragType = `application/vnd.code.tree.${this.id.toLowerCase()}`; } + get showCollapseAll(): boolean { + return this.options.showCollapseAll || false; + } + protected override renderIcon(node: TreeNode, props: NodeProps): React.ReactNode { const icon = this.toNodeIcon(node); if (icon) { diff --git a/packages/plugin-ext/src/main/browser/view/tree-views-main.ts b/packages/plugin-ext/src/main/browser/view/tree-views-main.ts index eddd6d559af23..3b80a7d50a463 100644 --- a/packages/plugin-ext/src/main/browser/view/tree-views-main.ts +++ b/packages/plugin-ext/src/main/browser/view/tree-views-main.ts @@ -63,6 +63,7 @@ export class TreeViewsMainImpl implements TreeViewsMain, Disposable { this.treeViewProviders.set(treeViewId, this.viewRegistry.registerViewDataProvider(treeViewId, async ({ state, viewInfo }) => { const options: TreeViewWidgetOptions = { id: treeViewId, + showCollapseAll: $options.showCollapseAll, multiSelect: $options.canSelectMany, dragMimeTypes: $options.dragMimeTypes, dropMimeTypes: $options.dropMimeTypes diff --git a/packages/plugin-ext/src/plugin/tree/tree-views.ts b/packages/plugin-ext/src/plugin/tree/tree-views.ts index 8d548811621f7..6e60ce35ab2fb 100644 --- a/packages/plugin-ext/src/plugin/tree/tree-views.ts +++ b/packages/plugin-ext/src/plugin/tree/tree-views.ts @@ -234,7 +234,7 @@ class TreeViewExtImpl implements Disposable { // make copies of optionally provided MIME types: const dragMimeTypes = options.dragAndDropController?.dragMimeTypes?.slice(); const dropMimeTypes = options.dragAndDropController?.dropMimeTypes?.slice(); - proxy.$registerTreeDataProvider(treeViewId, { canSelectMany: options.canSelectMany, dragMimeTypes, dropMimeTypes }); + proxy.$registerTreeDataProvider(treeViewId, { showCollapseAll: options.showCollapseAll, canSelectMany: options.canSelectMany, dragMimeTypes, dropMimeTypes }); this.toDispose.push(Disposable.create(() => this.proxy.$unregisterTreeDataProvider(treeViewId))); options.treeDataProvider.onDidChangeTreeData?.(() => { this.pendingRefresh = proxy.$refresh(treeViewId);