Skip to content

Commit 327bf4d

Browse files
authored
Manage checkbox state by default (microsoft#182750)
1 parent 7062714 commit 327bf4d

File tree

7 files changed

+100
-10
lines changed

7 files changed

+100
-10
lines changed

src/vs/workbench/api/browser/mainThreadTreeViews.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie
3737
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTreeViews);
3838
}
3939

40-
async $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean; canSelectMany: boolean; dropMimeTypes: string[]; dragMimeTypes: string[]; hasHandleDrag: boolean; hasHandleDrop: boolean }): Promise<void> {
40+
async $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean; canSelectMany: boolean; dropMimeTypes: string[]; dragMimeTypes: string[]; hasHandleDrag: boolean; hasHandleDrop: boolean; manuallyManageCheckboxes: boolean }): Promise<void> {
4141
this.logService.trace('MainThreadTreeViews#$registerTreeViewDataProvider', treeViewId, options);
4242

4343
this.extensionService.whenInstalledExtensionsRegistered().then(() => {
@@ -49,8 +49,9 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie
4949
if (viewer) {
5050
// Order is important here. The internal tree isn't created until the dataProvider is set.
5151
// Set all other properties first!
52-
viewer.showCollapseAllAction = !!options.showCollapseAll;
53-
viewer.canSelectMany = !!options.canSelectMany;
52+
viewer.showCollapseAllAction = options.showCollapseAll;
53+
viewer.canSelectMany = options.canSelectMany;
54+
viewer.manuallyManageCheckboxes = options.manuallyManageCheckboxes;
5455
viewer.dragAndDropController = dndController;
5556
if (dndController) {
5657
this._dndControllers.set(treeViewId, dndController);

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ export interface MainThreadTextEditorsShape extends IDisposable {
276276
}
277277

278278
export interface MainThreadTreeViewsShape extends IDisposable {
279-
$registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean; canSelectMany: boolean; dropMimeTypes: readonly string[]; dragMimeTypes: readonly string[]; hasHandleDrag: boolean; hasHandleDrop: boolean }): Promise<void>;
279+
$registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean; canSelectMany: boolean; dropMimeTypes: readonly string[]; dragMimeTypes: readonly string[]; hasHandleDrag: boolean; hasHandleDrop: boolean; manuallyManageCheckboxes: boolean }): Promise<void>;
280280
$refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem }): Promise<void>;
281281
$reveal(treeViewId: string, itemInfo: { item: ITreeItem; parentChain: ITreeItem[] } | undefined, options: IRevealOptions): Promise<void>;
282282
$setMessage(treeViewId: string, message: string): void;

src/vs/workbench/api/common/extHostTreeViews.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
9292
const hasHandleDrag = !!options.dragAndDropController?.handleDrag;
9393
const hasHandleDrop = !!options.dragAndDropController?.handleDrop;
9494
const treeView = this.createExtHostTreeView(viewId, options, extension);
95-
const registerPromise = this._proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany, dropMimeTypes, dragMimeTypes, hasHandleDrag, hasHandleDrop });
95+
const proxyOptions = { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany, dropMimeTypes, dragMimeTypes, hasHandleDrag, hasHandleDrop, manuallyManageCheckboxes: !!options.manuallyManageCheckboxSelection };
96+
const registerPromise = this._proxy.$registerTreeViewDataProvider(viewId, proxyOptions);
9697
return {
9798
get onDidCollapseElement() { return treeView.onDidCollapseElement; },
9899
get onDidExpandElement() { return treeView.onDidExpandElement; },

src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ suite('MainThreadHostTreeView', function () {
7575
}
7676
drain(): any { return null; }
7777
}, new TestViewsService(), new TestNotificationService(), testExtensionService, new NullLogService());
78-
mainThreadTreeViews.$registerTreeViewDataProvider(testTreeViewId, { showCollapseAll: false, canSelectMany: false, dropMimeTypes: [], dragMimeTypes: [], hasHandleDrag: false, hasHandleDrop: false });
78+
mainThreadTreeViews.$registerTreeViewDataProvider(testTreeViewId, { showCollapseAll: false, canSelectMany: false, dropMimeTypes: [], dragMimeTypes: [], hasHandleDrag: false, hasHandleDrop: false, manuallyManageCheckboxes: false });
7979
await testExtensionService.whenInstalledExtensionsRegistered();
8080
});
8181

src/vs/workbench/browser/parts/views/treeView.ts

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView {
206206
private treeContainer: HTMLElement | undefined;
207207
private _messageValue: string | undefined;
208208
private _canSelectMany: boolean = false;
209+
private _manuallyManageCheckboxes: boolean = false;
209210
private messageElement: HTMLElement | undefined;
210211
private tree: Tree | undefined;
211212
private treeLabels: ResourceLabels | undefined;
@@ -349,6 +350,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView {
349350
node = node ?? self.root;
350351
node.children = await (node instanceof Root ? dataProvider.getChildren() : dataProvider.getChildren(node));
351352
children = node.children ?? [];
353+
children.forEach(child => child.parent = node);
352354
}
353355
if (node instanceof Root) {
354356
const oldEmpty = this._isEmpty;
@@ -447,6 +449,14 @@ abstract class AbstractTreeView extends Disposable implements ITreeView {
447449
}
448450
}
449451

452+
get manuallyManageCheckboxes(): boolean {
453+
return this._manuallyManageCheckboxes;
454+
}
455+
456+
set manuallyManageCheckboxes(manuallyManageCheckboxes: boolean) {
457+
this._manuallyManageCheckboxes = manuallyManageCheckboxes;
458+
}
459+
450460
get hasIconForParentNode(): boolean {
451461
return this._hasIconForParentNode;
452462
}
@@ -610,6 +620,68 @@ abstract class AbstractTreeView extends Disposable implements ITreeView {
610620
this._register(focusTracker.onDidBlur(() => this.focused = false));
611621
}
612622

623+
private updateCheckboxes(items: ITreeItem[]) {
624+
const additionalItems: ITreeItem[] = [];
625+
626+
if (!this.manuallyManageCheckboxes) {
627+
for (const item of items) {
628+
if (item.checkbox !== undefined) {
629+
630+
function checkChildren(currentItem: ITreeItem) {
631+
for (const child of (currentItem.children ?? [])) {
632+
if (child.checkbox !== undefined && currentItem.checkbox !== undefined) {
633+
child.checkbox.isChecked = currentItem.checkbox.isChecked;
634+
additionalItems.push(child);
635+
checkChildren(child);
636+
}
637+
}
638+
}
639+
checkChildren(item);
640+
641+
const visitedParents: Set<ITreeItem> = new Set();
642+
function checkParents(currentItem: ITreeItem) {
643+
if (currentItem.parent && (currentItem.parent.checkbox !== undefined) && currentItem.parent.children) {
644+
if (visitedParents.has(currentItem.parent)) {
645+
return;
646+
} else {
647+
visitedParents.add(currentItem.parent);
648+
}
649+
650+
let someUnchecked = false;
651+
let someChecked = false;
652+
for (const child of currentItem.parent.children) {
653+
if (someUnchecked && someChecked) {
654+
break;
655+
}
656+
if (child.checkbox !== undefined) {
657+
if (child.checkbox.isChecked) {
658+
someChecked = true;
659+
} else {
660+
someUnchecked = true;
661+
}
662+
}
663+
}
664+
if (someChecked && !someUnchecked) {
665+
currentItem.parent.checkbox.isChecked = true;
666+
additionalItems.push(currentItem.parent);
667+
checkParents(currentItem.parent);
668+
} else if (someUnchecked && !someChecked) {
669+
currentItem.parent.checkbox.isChecked = false;
670+
additionalItems.push(currentItem.parent);
671+
checkParents(currentItem.parent);
672+
}
673+
}
674+
}
675+
checkParents(item);
676+
}
677+
}
678+
}
679+
items = items.concat(additionalItems);
680+
items.forEach(item => this.tree?.rerender(item));
681+
this._onDidChangeCheckboxState.fire(items);
682+
}
683+
684+
613685
protected createTree() {
614686
const actionViewItemProvider = createActionViewItem.bind(undefined, this.instantiationService);
615687
const treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id));
@@ -618,8 +690,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView {
618690
const aligner = new Aligner(this.themeService);
619691
const checkboxStateHandler = this._register(new CheckboxStateHandler());
620692
this._register(checkboxStateHandler.onDidChangeCheckboxState(items => {
621-
items.forEach(item => this.tree?.rerender(item));
622-
this._onDidChangeCheckboxState.fire(items);
693+
this.updateCheckboxes(items);
623694
}));
624695
const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner, checkboxStateHandler);
625696
const widgetAriaLabel = this._title;

src/vs/workbench/common/views.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,8 @@ export interface ITreeView extends IDisposable {
651651

652652
canSelectMany: boolean;
653653

654+
manuallyManageCheckboxes: boolean;
655+
654656
message?: string;
655657

656658
title: string;
@@ -784,6 +786,8 @@ export interface ITreeItem {
784786

785787
children?: ITreeItem[];
786788

789+
parent?: ITreeItem;
790+
787791
accessibilityInformation?: IAccessibilityInformation;
788792

789793
checkbox?: ITreeItemCheckboxState;

src/vscode-dts/vscode.proposed.treeItemCheckbox.d.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ declare module 'vscode' {
77

88
export class TreeItem2 extends TreeItem {
99
/**
10-
* [TreeItemCheckboxState](#TreeItemCheckboxState) of the tree item.
10+
* {@link TreeItemCheckboxState TreeItemCheckboxState} of the tree item.
11+
* {@link TreeDataProvider.onDidChangeTreeData onDidChangeTreeData} should be fired when {@link TreeItem2.checkboxState checkboxState} changes.
1112
*/
12-
checkboxState?: TreeItemCheckboxState | { readonly state: TreeItemCheckboxState; readonly tooltip?: string };
13+
checkboxState?: TreeItemCheckboxState | { readonly state: TreeItemCheckboxState; readonly tooltip?: string; readonly accessibilityInformation?: AccessibilityInformation };
1314
}
1415

1516
/**
@@ -42,4 +43,16 @@ declare module 'vscode' {
4243
*/
4344
readonly items: ReadonlyArray<[T, TreeItemCheckboxState]>;
4445
}
46+
47+
/**
48+
* Options for creating a {@link TreeView}
49+
*/
50+
export interface TreeViewOptions<T> {
51+
/**
52+
* By default, when the children of a tree item have already been fetched, child checkboxes are automatically managed based on the checked state of the parent tree item.
53+
* If the tree item is collapsed by default (meaning that the children haven't yet been fetched) then child checkboxes will not be updated.
54+
* To override this behavior and manage child and parent checkbox state in the extension, set this to `true`.
55+
*/
56+
manuallyManageCheckboxSelection?: boolean;
57+
}
4558
}

0 commit comments

Comments
 (0)