Skip to content

Commit

Permalink
Implement VS Code tree view checkbox API. Fixes #12695
Browse files Browse the repository at this point in the history
Contributed on behalf of STMicroelectronics

Signed-off-by: Thomas Mäder <[email protected]>
  • Loading branch information
tsmaeder committed Aug 16, 2023
1 parent 8ef8e2d commit d365b4c
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 11 deletions.
8 changes: 8 additions & 0 deletions packages/core/src/browser/tree/tree-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,14 @@ export class TreeModelImpl implements TreeModel, SelectionProvider<ReadonlyArray
return this.tree.markAsBusy(node, ms, token);
}

get onDidUpdate(): Event<TreeNode[]> {
return this.tree.onDidUpdate;
}

markAsChecked(node: TreeNode, checked: boolean): void {
this.tree.markAsChecked(node, checked);
}

}
export namespace TreeModelImpl {
export interface State {
Expand Down
37 changes: 37 additions & 0 deletions packages/core/src/browser/tree/tree-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
this.model.onSelectionChanged(() => this.scheduleUpdateScrollToRow({ resize: false })),
this.focusService.onDidChangeFocus(() => this.scheduleUpdateScrollToRow({ resize: false })),
this.model.onDidChangeBusy(() => this.update()),
this.model.onDidUpdate(() => this.update()),
this.model.onNodeRefreshed(() => this.updateDecorations()),
this.model.onExpansionChanged(() => this.updateDecorations()),
this.decoratorService,
Expand Down Expand Up @@ -578,6 +579,41 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
</div>;
}

/**
* Render the node expansion toggle.
* @param node the tree node.
* @param props the node properties.
*/
protected renderCheckbox(node: TreeNode, props: NodeProps): React.ReactNode {
if (node.checkboxInfo === undefined) {
// eslint-disable-next-line no-null/no-null
return null;
}
return <input data-node-id={node.id}
readOnly
type='checkbox'
checked={!!node.checkboxInfo.checked}
title={node.checkboxInfo.tooltip}
aria-label={node.checkboxInfo.accessibilityInformation?.label}
role={node.checkboxInfo.accessibilityInformation?.role}
className='theia-input'
onClick={event => this.toggleChecked(event)} />

}

protected toggleChecked(event: React.MouseEvent<HTMLElement>) {
const nodeId = event.currentTarget.getAttribute('data-node-id');
if (nodeId) {
const node = this.model.getNode(nodeId);
if (node) {
this.model.markAsChecked(node, !node.checkboxInfo!.checked);
} else {
this.handleClickEvent(node, event);
}
}
event.preventDefault();
event.stopPropagation();
}
/**
* Render the tree node caption given the node properties.
* @param node the tree node.
Expand Down Expand Up @@ -905,6 +941,7 @@ export class TreeWidget extends ReactWidget implements StatefulWidget {
const attributes = this.createNodeAttributes(node, props);
const content = <div className={TREE_NODE_CONTENT_CLASS}>
{this.renderExpansionToggle(node, props)}
{this.renderCheckbox(node, props)}
{this.decorateIcon(node, this.renderIcon(node, props))}
{this.renderCaptionAffixes(node, props, 'captionPrefixes')}
{this.renderCaption(node, props)}
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/browser/tree/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Disposable, DisposableCollection } from '../../common/disposable';
import { CancellationToken, CancellationTokenSource } from '../../common/cancellation';
import { timeout } from '../../common/promise-util';
import { isObject, Mutable } from '../../common';
import { AccessibilityInformation } from '../../common/accessibility';

export const Tree = Symbol('Tree');

Expand Down Expand Up @@ -70,6 +71,19 @@ export interface Tree extends Disposable {
* A token source of the given token should be canceled to unmark.
*/
markAsBusy(node: Readonly<TreeNode>, ms: number, token: CancellationToken): Promise<void>;

/**
* An update to the tree node occurred, but the tree structure remains unchanged
*/
readonly onDidUpdate: Event<TreeNode[]>;

markAsChecked(node: TreeNode, checked: boolean): void;
}

export interface TreeViewItemCheckboxInfo {
checked: boolean;
tooltip?: string;
accessibilityInformation?: AccessibilityInformation
}

/**
Expand Down Expand Up @@ -120,6 +134,11 @@ export interface TreeNode {
* Whether this node is busy. Greater than 0 then busy; otherwise not.
*/
readonly busy?: number;

/**
* Whether this node is checked.
*/
readonly checkboxInfo?: TreeViewItemCheckboxInfo;
}

export namespace TreeNode {
Expand Down Expand Up @@ -238,6 +257,8 @@ export class TreeImpl implements Tree {

protected readonly onDidChangeBusyEmitter = new Emitter<TreeNode>();
readonly onDidChangeBusy = this.onDidChangeBusyEmitter.event;
protected readonly onDidUpdateEmitter = new Emitter<TreeNode[]>();
readonly onDidUpdate = this.onDidUpdateEmitter.event;

protected nodes: {
[id: string]: Mutable<TreeNode> | undefined
Expand Down Expand Up @@ -368,6 +389,12 @@ export class TreeImpl implements Tree {
await this.doMarkAsBusy(node, ms, token);
}
}

markAsChecked(node: Mutable<TreeNode>, checked: boolean) {
node.checkboxInfo!.checked = checked;
this.onDidUpdateEmitter.fire([node]);
}

protected async doMarkAsBusy(node: Mutable<TreeNode>, ms: number, token: CancellationToken): Promise<void> {
try {
await timeout(ms, token);
Expand Down
11 changes: 11 additions & 0 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ import { isString, isObject, PickOptions, QuickInputButtonHandle } from '@theia/
import { Severity } from '@theia/core/lib/common/severity';
import { DebugConfiguration, DebugSessionOptions } from '@theia/debug/lib/common/debug-configuration';
import { LanguagePackBundle } from './language-pack-service';
import { AccessibilityInformation } from '@theia/core/lib/common/accessibility';

export interface PreferenceData {
[scope: number]: any;
Expand Down Expand Up @@ -736,6 +737,7 @@ export interface DialogsMain {
}

export interface RegisterTreeDataProviderOptions {
manageCheckboxStateManually?: boolean;
showCollapseAll?: boolean
canSelectMany?: boolean
dragMimeTypes?: string[]
Expand Down Expand Up @@ -768,6 +770,7 @@ export class DataTransferFileDTO {
}

export interface TreeViewsExt {
$checkStateChanged(treeViewId: string, itemIds: { id: string, checked: boolean }[]): unknown;
$dragStarted(treeViewId: string, treeItemIds: string[], token: CancellationToken): Promise<UriComponents[] | undefined>;
$dragEnd(treeViewId: string): Promise<void>;
$drop(treeViewId: string, treeItemId: string | undefined, dataTransferItems: [string, string | DataTransferFileDTO][], token: CancellationToken): Promise<void>;
Expand All @@ -779,6 +782,12 @@ export interface TreeViewsExt {
$setVisible(treeViewId: string, visible: boolean): Promise<void>;
}

export interface TreeViewItemCheckboxInfo {
checked: boolean;
tooltip?: string;
accessibilityInformation?: AccessibilityInformation
}

export interface TreeViewItem {

id: string;
Expand All @@ -801,6 +810,8 @@ export interface TreeViewItem {

collapsibleState?: TreeViewItemCollapsibleState;

checkboxInfo?: TreeViewItemCheckboxInfo;

contextValue?: string;

command?: Command;
Expand Down
41 changes: 39 additions & 2 deletions packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import { AccessibilityInformation } from '@theia/plugin';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { DecoratedTreeNode } from '@theia/core/lib/browser/tree/tree-decorator';
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
import { CancellationTokenSource, CancellationToken } from '@theia/core/lib/common';
import { CancellationTokenSource, CancellationToken, Mutable } from '@theia/core/lib/common';
import { mixin } from '../../../common/types';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { DnDFileContentStore } from './dnd-file-content-store';
Expand Down Expand Up @@ -165,6 +165,7 @@ export namespace CompositeTreeViewNode {
@injectable()
export class TreeViewWidgetOptions {
id: string;
manageCheckboxStateManually: boolean | undefined;
showCollapseAll: boolean | undefined;
multiSelect: boolean | undefined;
dragMimeTypes: string[] | undefined;
Expand Down Expand Up @@ -272,6 +273,40 @@ export class PluginTree extends TreeImpl {
}, update);
}

override markAsChecked(node: Mutable<TreeNode>, checked: boolean): void {
function findParentsToChange(child: TreeNode, nodes: TreeNode[]): void {
if (child.parent) {
if (child.parent.checkboxInfo !== undefined && child.parent.checkboxInfo.checked !== checked) {
if (!checked || child.parent.children.every(candidate => candidate.checkboxInfo?.checked || candidate.checkboxInfo === undefined || candidate === child)) {
nodes.push(child.parent);
findParentsToChange(child.parent, nodes);
}
}
}
}

function findChildrenToChange(parent: TreeNode, nodes: TreeNode[]): void {
if (CompositeTreeNode.is(parent)) {
parent.children.forEach(child => {
if (child.checkboxInfo !== undefined && child.checkboxInfo.checked !== checked) {
nodes.push(child);
}
findChildrenToChange(child, nodes);
});
}
}

const nodesToChange = [node];
if (!this.options.manageCheckboxStateManually) {
findParentsToChange(node, nodesToChange);
findChildrenToChange(node, nodesToChange);

}
nodesToChange.forEach(n => n.checkboxInfo!.checked = checked)
this.onDidUpdateEmitter.fire(nodesToChange);
this.proxy?.$checkStateChanged(this.options.id, [{ id: node.id, checked: checked }]);
}

/** Creates a resolvable tree node. If a node already exists, reset it because the underlying TreeViewItem might have been disposed in the backend. */
protected createResolvableTreeNode(item: TreeViewItem, parent: CompositeTreeNode): TreeNode {
const update: Partial<TreeViewNode> = this.createTreeNodeUpdate(item);
Expand Down Expand Up @@ -328,6 +363,7 @@ export class PluginTree extends TreeImpl {
tooltip: item.tooltip,
contextValue: item.contextValue,
command: item.command,
checkboxInfo: item.checkboxInfo,
accessibilityInformation: item.accessibilityInformation,
};
}
Expand Down Expand Up @@ -496,6 +532,7 @@ export class TreeViewWidget extends TreeViewWelcomeWidget {
...attrs,
onMouseLeave: () => source?.cancel(),
onMouseEnter: async event => {
const target = event.currentTarget; // event.currentTarget will be null after awaiting node resolve()
if (configuredTip) {
if (MarkdownString.is(node.tooltip)) {
this.hoverService.requestHover({
Expand Down Expand Up @@ -524,7 +561,7 @@ export class TreeViewWidget extends TreeViewWelcomeWidget {
const title = node.tooltip ||
(node.resourceUri && this.labelProvider.getLongName(new URI(node.resourceUri)))
|| this.toNodeName(node);
event.currentTarget.title = title;
target.title = title;
}
configuredTip = true;
}
Expand Down
10 changes: 10 additions & 0 deletions packages/plugin-ext/src/main/browser/view/tree-views-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
manageCheckboxStateManually: $options.manageCheckboxStateManually,
showCollapseAll: $options.showCollapseAll,
multiSelect: $options.canSelectMany,
dragMimeTypes: $options.dragMimeTypes,
Expand Down Expand Up @@ -183,6 +184,15 @@ export class TreeViewsMainImpl implements TreeViewsMain, Disposable {
}
}

async setChecked(treeViewWidget: TreeViewWidget, changedNodes: TreeViewNode[]) {
this.proxy.$checkStateChanged(treeViewWidget.id, changedNodes.map(node => {
return {
id: node.id,
checked: !!node.checkboxInfo?.checked
}
}));
}

protected handleTreeEvents(treeViewId: string, treeViewWidget: TreeViewWidget): void {
this.toDispose.push(treeViewWidget.model.onExpansionChanged(event => {
this.proxy.$setExpanded(treeViewId, event.id, event.expanded);
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-ext/src/plugin/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import {
DataTransfer,
TreeItem,
TreeItemCollapsibleState,
TreeItemCheckboxState,
DocumentSymbol,
SymbolTag,
WorkspaceEdit,
Expand Down Expand Up @@ -1291,6 +1292,7 @@ export function createAPIFactory(
DataTransfer,
TreeItem,
TreeItemCollapsibleState,
TreeItemCheckboxState,
SymbolKind,
SymbolTag,
DocumentSymbol,
Expand Down
Loading

0 comments on commit d365b4c

Please sign in to comment.