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

Implement VS Code tree view checkbox API. #12836

Merged
merged 3 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
- [Previous Changelogs](https://github.com/eclipse-theia/theia/tree/master/doc/changelogs/)
## v1.41.0 -
- [application-package] Quit Electron app when back end fails to start [#12778](https://github.com/eclipse-theia/theia/pull/12778) - Contributed on behalf of STMicroelectronics.

- [vscode] added support for tree checkbox api [#12836](https://github.com/eclipse-theia/theia/pull/12836) - Contributed on behalf of STMicroelectronics
## v1.40.0 - 07/27/2023

- [application-package] bumped the default supported VS Code API from `1.78.0` to `1.79.0` [#12764](https://github.com/eclipse-theia/theia/pull/12764) - Contributed on behalf of STMicroelectronics.
Expand Down
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
36 changes: 36 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,40 @@ 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>): void {
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 +940,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): void {
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 }[]): Promise<void>;
$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);
}
}
}
}
msujew marked this conversation as resolved.
Show resolved Hide resolved

function findChildrenToChange(parent: TreeNode, nodes: TreeNode[]): void {
if (CompositeTreeNode.is(parent)) {
parent.children.forEach(child => {
if (child.checkboxInfo !== undefined && child.checkboxInfo.checked !== checked) {
msujew marked this conversation as resolved.
Show resolved Hide resolved
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
8 changes: 8 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,13 @@ export class TreeViewsMainImpl implements TreeViewsMain, Disposable {
}
}

async setChecked(treeViewWidget: TreeViewWidget, changedNodes: TreeViewNode[]): Promise<void> {
await this.proxy.$checkStateChanged(treeViewWidget.id, changedNodes.map(node => ({
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
Loading