Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multi-selection of tree items in tree views #12088

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ import type {
import { SerializableEnvironmentVariableCollection } from '@theia/terminal/lib/common/base-terminal-protocol';
import { ThemeType } from '@theia/core/lib/common/theme';
import { Disposable } from '@theia/core/lib/common/disposable';
import { isObject, PickOptions, QuickInputButtonHandle } from '@theia/core/lib/common';
import { isString, isObject, PickOptions, QuickInputButtonHandle } from '@theia/core/lib/common';
import { Severity } from '@theia/core/lib/common/severity';
import { DebugConfiguration, DebugSessionOptions } from '@theia/debug/lib/common/debug-configuration';

Expand Down Expand Up @@ -727,7 +727,7 @@ export interface TreeViewRevealOptions {
}

export interface TreeViewsMain {
$registerTreeDataProvider(treeViewId: string, dragMimetypes: string[] | undefined, dropMimetypes: string[] | undefined): void;
$registerTreeDataProvider(treeViewId: string, canSelectMany: boolean | undefined, dragMimetypes: string[] | undefined, dropMimetypes: string[] | undefined): void;
$readDroppedFile(contentId: string): Promise<BinaryBuffer>;
$unregisterTreeDataProvider(treeViewId: string): void;
$refresh(treeViewId: string): Promise<void>;
Expand Down Expand Up @@ -785,13 +785,13 @@ export interface TreeViewItem {

}

export interface TreeViewSelection {
treeViewId: string
treeItemId: string
export interface TreeViewItemReference {
viewId: string
itemId: string
}
export namespace TreeViewSelection {
export function is(arg: unknown): arg is TreeViewSelection {
return isObject(arg) && 'treeViewId' in arg && 'treeItemId' in arg;
export namespace TreeViewItemReference {
export function is(arg: unknown): arg is TreeViewItemReference {
return isObject(arg) && isString(arg.viewId) && isString(arg.itemId);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { TreeWidgetSelection } from '@theia/core/lib/browser/tree/tree-widget-se
import { ScmRepository } from '@theia/scm/lib/browser/scm-repository';
import { ScmService } from '@theia/scm/lib/browser/scm-service';
import { TimelineItem } from '@theia/timeline/lib/common/timeline-model';
import { ScmCommandArg, TimelineCommandArg, TreeViewSelection } from '../../../common';
import { ScmCommandArg, TimelineCommandArg, TreeViewItemReference } from '../../../common';
import { PluginScmProvider, PluginScmResource, PluginScmResourceGroup } from '../scm-main';
import { TreeViewWidget } from '../view/tree-view-widget';
import { CodeEditorWidgetUtil, codeToTheiaMappings, ContributionPoint } from './vscode-theia-menu-mappings';
Expand Down Expand Up @@ -238,19 +238,21 @@ export class PluginMenuCommandAdapter implements MenuCommandAdapter {
protected toTreeArgs(...args: any[]): any[] {
const treeArgs: any[] = [];
for (const arg of args) {
if (TreeViewSelection.is(arg)) {
if (TreeViewItemReference.is(arg)) {
treeArgs.push(arg);
} else if (Array.isArray(arg)) {
treeArgs.push(arg.filter(TreeViewItemReference.is));
}
}
return treeArgs;
}

protected getSelectedResources(): [CodeUri | TreeViewSelection | undefined, CodeUri[] | undefined] {
protected getSelectedResources(): [CodeUri | TreeViewItemReference | undefined, CodeUri[] | undefined] {
const selection = this.selectionService.selection;
const resourceKey = this.resourceContextKey.get();
const resourceUri = resourceKey ? CodeUri.parse(resourceKey) : undefined;
const firstMember = TreeWidgetSelection.is(selection) && selection.source instanceof TreeViewWidget && selection[0]
? selection.source.toTreeViewSelection(selection[0])
? selection.source.toTreeViewItemReference(selection[0])
: UriSelection.getUri(selection)?.['codeUri'] ?? resourceUri;
const secondMember = TreeWidgetSelection.is(selection)
? UriSelection.getUris(selection).map(uri => uri['codeUri'])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(DnDFileContentStore).toSelf().inSingletonScope();
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: PLUGIN_VIEW_DATA_FACTORY_ID,
createWidget: (identifier: TreeViewWidgetOptions) => {
createWidget: (options: TreeViewWidgetOptions) => {
const props = {
contextMenuPath: VIEW_ITEM_CONTEXT_MENU,
expandOnlyOnExpansionToggleClick: true,
expansionTogglePadding: 22,
globalSelection: true,
leftPadding: 8,
search: true
search: true,
multiSelect: options.multiSelect
};
const child = createTreeContainer(container, {
props,
Expand All @@ -164,7 +165,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
widget: TreeViewWidget,
decoratorService: TreeViewDecoratorService
});
child.bind(TreeViewWidgetOptions).toConstantValue(identifier);
child.bind(TreeViewWidgetOptions).toConstantValue(options);
return child.get(TreeWidget);
}
})).inSingletonScope();
Expand Down
36 changes: 20 additions & 16 deletions packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// *****************************************************************************

import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { TreeViewsExt, TreeViewItemCollapsibleState, TreeViewItem, TreeViewSelection, ThemeIcon, DataTransferFileDTO } from '../../../common/plugin-api-rpc';
import { TreeViewsExt, TreeViewItemCollapsibleState, TreeViewItem, TreeViewItemReference, ThemeIcon, DataTransferFileDTO } from '../../../common/plugin-api-rpc';
import { Command } from '../../../common/plugin-api-rpc-model';
import {
TreeNode,
Expand Down Expand Up @@ -164,6 +164,7 @@ export namespace CompositeTreeViewNode {
@injectable()
export class TreeViewWidgetOptions {
id: string;
multiSelect: boolean | undefined;
dragMimeTypes: string[] | undefined;
dropMimeTypes: string[] | undefined;
}
Expand Down Expand Up @@ -696,41 +697,44 @@ export class TreeViewWidget extends TreeViewWelcomeWidget {
}
}

protected override renderTailDecorations(node: TreeViewNode, props: NodeProps): React.ReactNode {
return this.contextKeys.with({ view: this.id, viewItem: node.contextValue }, () => {
protected override renderTailDecorations(treeViewNode: TreeViewNode, props: NodeProps): React.ReactNode {
paul-marechal marked this conversation as resolved.
Show resolved Hide resolved
return this.contextKeys.with({ view: this.id, viewItem: treeViewNode.contextValue }, () => {
const menu = this.menus.getMenu(VIEW_ITEM_INLINE_MENU);
const arg = this.toTreeViewSelection(node);
const args = this.toContextMenuArgs(treeViewNode);
const inlineCommands = menu.children.filter((item): item is ActionMenuNode => item instanceof ActionMenuNode);
const tailDecorations = super.renderTailDecorations(node, props);
const tailDecorations = super.renderTailDecorations(treeViewNode, props);
return <React.Fragment>
{inlineCommands.length > 0 && <div className={TREE_NODE_SEGMENT_CLASS + ' flex'}>
{inlineCommands.map((item, index) => this.renderInlineCommand(item, index, this.focusService.hasFocus(node), arg))}
{inlineCommands.map((item, index) => this.renderInlineCommand(item, index, this.focusService.hasFocus(treeViewNode), args))}
</div>}
{tailDecorations !== undefined && <div className={TREE_NODE_SEGMENT_CLASS + ' flex'}>{tailDecorations}</div>}
</React.Fragment>;
});
}

toTreeViewSelection(node: TreeNode): TreeViewSelection {
return { treeViewId: this.id, treeItemId: node.id };
toTreeViewItemReference(treeNode: TreeNode): TreeViewItemReference {
return { viewId: this.id, itemId: treeNode.id };
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected renderInlineCommand(node: ActionMenuNode, index: number, tabbable: boolean, arg: any): React.ReactNode {
const { icon } = node;
if (!icon || !this.commands.isVisible(node.command, arg) || !node.when || !this.contextKeys.match(node.when)) {
protected renderInlineCommand(actionMenuNode: ActionMenuNode, index: number, tabbable: boolean, args: any[]): React.ReactNode {
if (!actionMenuNode.icon || !this.commands.isVisible(actionMenuNode.command, ...args) || !actionMenuNode.when || !this.contextKeys.match(actionMenuNode.when)) {
return false;
}
const className = [TREE_NODE_SEGMENT_CLASS, TREE_NODE_TAIL_CLASS, icon, ACTION_ITEM, 'theia-tree-view-inline-action'].join(' ');
const className = [TREE_NODE_SEGMENT_CLASS, TREE_NODE_TAIL_CLASS, actionMenuNode.icon, ACTION_ITEM, 'theia-tree-view-inline-action'].join(' ');
const tabIndex = tabbable ? 0 : undefined;
return <div key={index} className={className} title={node.label} tabIndex={tabIndex} onClick={e => {
return <div key={index} className={className} title={actionMenuNode.label} tabIndex={tabIndex} onClick={e => {
e.stopPropagation();
this.commands.executeCommand(node.command, arg);
this.commands.executeCommand(actionMenuNode.command, ...args);
}} />;
}

protected override toContextMenuArgs(node: SelectableTreeNode): [TreeViewSelection] {
return [this.toTreeViewSelection(node)];
protected override toContextMenuArgs(target: SelectableTreeNode): [TreeViewItemReference, TreeViewItemReference[]] | [TreeViewItemReference] {
if (this.options.multiSelect) {
return [this.toTreeViewItemReference(target), this.model.selectedNodes.map(node => this.toTreeViewItemReference(node))];
} else {
return [this.toTreeViewItemReference(target)];
}
}
paul-marechal marked this conversation as resolved.
Show resolved Hide resolved

override setFlag(flag: Widget.Flag): void {
Expand Down
3 changes: 2 additions & 1 deletion packages/plugin-ext/src/main/browser/view/tree-views-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ export class TreeViewsMainImpl implements TreeViewsMain, Disposable {
this.toDispose.dispose();
}

async $registerTreeDataProvider(treeViewId: string, dragMimeTypes: string[] | undefined, dropMimeTypes: string[] | undefined): Promise<void> {
async $registerTreeDataProvider(treeViewId: string, canSelectMany: boolean | undefined, dragMimeTypes: string[] | undefined, dropMimeTypes: string[] | undefined): Promise<void> {
this.treeViewProviders.set(treeViewId, this.viewRegistry.registerViewDataProvider(treeViewId, async ({ state, viewInfo }) => {
const options: TreeViewWidgetOptions = {
id: treeViewId,
multiSelect: canSelectMany,
dragMimeTypes,
dropMimeTypes
};
Expand Down
Loading