From 4d0393371f8137bd1ab9a27c1ef6b5e455b1ee8f Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 17 May 2023 14:22:09 +0200 Subject: [PATCH] Manage checkbox state by default --- .../api/browser/mainThreadTreeViews.ts | 7 +- .../workbench/api/common/extHost.protocol.ts | 2 +- .../workbench/api/common/extHostTreeViews.ts | 3 +- .../test/browser/mainThreadTreeViews.test.ts | 2 +- .../workbench/browser/parts/views/treeView.ts | 75 ++++++++++++++++++- src/vs/workbench/common/views.ts | 4 + .../vscode.proposed.treeItemCheckbox.d.ts | 17 ++++- 7 files changed, 100 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index bc2c467a8bffa..6b6a75aaf60db 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -37,7 +37,7 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTreeViews); } - async $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean; canSelectMany: boolean; dropMimeTypes: string[]; dragMimeTypes: string[]; hasHandleDrag: boolean; hasHandleDrop: boolean }): Promise { + async $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean; canSelectMany: boolean; dropMimeTypes: string[]; dragMimeTypes: string[]; hasHandleDrag: boolean; hasHandleDrop: boolean; manuallyManageCheckboxes: boolean }): Promise { this.logService.trace('MainThreadTreeViews#$registerTreeViewDataProvider', treeViewId, options); this.extensionService.whenInstalledExtensionsRegistered().then(() => { @@ -49,8 +49,9 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie if (viewer) { // Order is important here. The internal tree isn't created until the dataProvider is set. // Set all other properties first! - viewer.showCollapseAllAction = !!options.showCollapseAll; - viewer.canSelectMany = !!options.canSelectMany; + viewer.showCollapseAllAction = options.showCollapseAll; + viewer.canSelectMany = options.canSelectMany; + viewer.manuallyManageCheckboxes = options.manuallyManageCheckboxes; viewer.dragAndDropController = dndController; if (dndController) { this._dndControllers.set(treeViewId, dndController); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index a5008ef018b9f..de05bdd41d0c8 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -276,7 +276,7 @@ export interface MainThreadTextEditorsShape extends IDisposable { } export interface MainThreadTreeViewsShape extends IDisposable { - $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean; canSelectMany: boolean; dropMimeTypes: readonly string[]; dragMimeTypes: readonly string[]; hasHandleDrag: boolean; hasHandleDrop: boolean }): Promise; + $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean; canSelectMany: boolean; dropMimeTypes: readonly string[]; dragMimeTypes: readonly string[]; hasHandleDrag: boolean; hasHandleDrop: boolean; manuallyManageCheckboxes: boolean }): Promise; $refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem }): Promise; $reveal(treeViewId: string, itemInfo: { item: ITreeItem; parentChain: ITreeItem[] } | undefined, options: IRevealOptions): Promise; $setMessage(treeViewId: string, message: string): void; diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index f23294a143c11..c4c0b1bfbbcbc 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -92,7 +92,8 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { const hasHandleDrag = !!options.dragAndDropController?.handleDrag; const hasHandleDrop = !!options.dragAndDropController?.handleDrop; const treeView = this.createExtHostTreeView(viewId, options, extension); - const registerPromise = this._proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany, dropMimeTypes, dragMimeTypes, hasHandleDrag, hasHandleDrop }); + const proxyOptions = { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany, dropMimeTypes, dragMimeTypes, hasHandleDrag, hasHandleDrop, manuallyManageCheckboxes: !!options.manuallyManageCheckboxSelection }; + const registerPromise = this._proxy.$registerTreeViewDataProvider(viewId, proxyOptions); return { get onDidCollapseElement() { return treeView.onDidCollapseElement; }, get onDidExpandElement() { return treeView.onDidExpandElement; }, diff --git a/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts b/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts index a747e0d592dc3..421d9eecd4ad7 100644 --- a/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts @@ -75,7 +75,7 @@ suite('MainThreadHostTreeView', function () { } drain(): any { return null; } }, new TestViewsService(), new TestNotificationService(), testExtensionService, new NullLogService()); - mainThreadTreeViews.$registerTreeViewDataProvider(testTreeViewId, { showCollapseAll: false, canSelectMany: false, dropMimeTypes: [], dragMimeTypes: [], hasHandleDrag: false, hasHandleDrop: false }); + mainThreadTreeViews.$registerTreeViewDataProvider(testTreeViewId, { showCollapseAll: false, canSelectMany: false, dropMimeTypes: [], dragMimeTypes: [], hasHandleDrag: false, hasHandleDrop: false, manuallyManageCheckboxes: false }); await testExtensionService.whenInstalledExtensionsRegistered(); }); diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 2bd2880b01fa8..e9269419448b6 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -206,6 +206,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { private treeContainer: HTMLElement | undefined; private _messageValue: string | undefined; private _canSelectMany: boolean = false; + private _manuallyManageCheckboxes: boolean = false; private messageElement: HTMLElement | undefined; private tree: Tree | undefined; private treeLabels: ResourceLabels | undefined; @@ -349,6 +350,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { node = node ?? self.root; node.children = await (node instanceof Root ? dataProvider.getChildren() : dataProvider.getChildren(node)); children = node.children ?? []; + children.forEach(child => child.parent = node); } if (node instanceof Root) { const oldEmpty = this._isEmpty; @@ -447,6 +449,14 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { } } + get manuallyManageCheckboxes(): boolean { + return this._manuallyManageCheckboxes; + } + + set manuallyManageCheckboxes(manuallyManageCheckboxes: boolean) { + this._manuallyManageCheckboxes = manuallyManageCheckboxes; + } + get hasIconForParentNode(): boolean { return this._hasIconForParentNode; } @@ -610,6 +620,68 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { this._register(focusTracker.onDidBlur(() => this.focused = false)); } + private updateCheckboxes(items: ITreeItem[]) { + const additionalItems: ITreeItem[] = []; + + if (!this.manuallyManageCheckboxes) { + for (const item of items) { + if (item.checkbox !== undefined) { + + function checkChildren(currentItem: ITreeItem) { + for (const child of (currentItem.children ?? [])) { + if (child.checkbox !== undefined && currentItem.checkbox !== undefined) { + child.checkbox.isChecked = currentItem.checkbox.isChecked; + additionalItems.push(child); + checkChildren(child); + } + } + } + checkChildren(item); + + const visitedParents: Set = new Set(); + function checkParents(currentItem: ITreeItem) { + if (currentItem.parent && (currentItem.parent.checkbox !== undefined) && currentItem.parent.children) { + if (visitedParents.has(currentItem.parent)) { + return; + } else { + visitedParents.add(currentItem.parent); + } + + let someUnchecked = false; + let someChecked = false; + for (const child of currentItem.parent.children) { + if (someUnchecked && someChecked) { + break; + } + if (child.checkbox !== undefined) { + if (child.checkbox.isChecked) { + someChecked = true; + } else { + someUnchecked = true; + } + } + } + if (someChecked && !someUnchecked) { + currentItem.parent.checkbox.isChecked = true; + additionalItems.push(currentItem.parent); + checkParents(currentItem.parent); + } else if (someUnchecked && !someChecked) { + currentItem.parent.checkbox.isChecked = false; + additionalItems.push(currentItem.parent); + checkParents(currentItem.parent); + } + } + } + checkParents(item); + } + } + } + items = items.concat(additionalItems); + items.forEach(item => this.tree?.rerender(item)); + this._onDidChangeCheckboxState.fire(items); + } + + protected createTree() { const actionViewItemProvider = createActionViewItem.bind(undefined, this.instantiationService); const treeMenus = this._register(this.instantiationService.createInstance(TreeMenus, this.id)); @@ -618,8 +690,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { const aligner = new Aligner(this.themeService); const checkboxStateHandler = this._register(new CheckboxStateHandler()); this._register(checkboxStateHandler.onDidChangeCheckboxState(items => { - items.forEach(item => this.tree?.rerender(item)); - this._onDidChangeCheckboxState.fire(items); + this.updateCheckboxes(items); })); const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner, checkboxStateHandler); const widgetAriaLabel = this._title; diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 46b1315470a4c..8b38396e36c39 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -651,6 +651,8 @@ export interface ITreeView extends IDisposable { canSelectMany: boolean; + manuallyManageCheckboxes: boolean; + message?: string; title: string; @@ -784,6 +786,8 @@ export interface ITreeItem { children?: ITreeItem[]; + parent?: ITreeItem; + accessibilityInformation?: IAccessibilityInformation; checkbox?: ITreeItemCheckboxState; diff --git a/src/vscode-dts/vscode.proposed.treeItemCheckbox.d.ts b/src/vscode-dts/vscode.proposed.treeItemCheckbox.d.ts index 74337f3990b78..ca818268bc62b 100644 --- a/src/vscode-dts/vscode.proposed.treeItemCheckbox.d.ts +++ b/src/vscode-dts/vscode.proposed.treeItemCheckbox.d.ts @@ -7,9 +7,10 @@ declare module 'vscode' { export class TreeItem2 extends TreeItem { /** - * [TreeItemCheckboxState](#TreeItemCheckboxState) of the tree item. + * {@link TreeItemCheckboxState TreeItemCheckboxState} of the tree item. + * {@link TreeDataProvider.onDidChangeTreeData onDidChangeTreeData} should be fired when {@link TreeItem2.checkboxState checkboxState} changes. */ - checkboxState?: TreeItemCheckboxState | { readonly state: TreeItemCheckboxState; readonly tooltip?: string }; + checkboxState?: TreeItemCheckboxState | { readonly state: TreeItemCheckboxState; readonly tooltip?: string; readonly accessibilityInformation?: AccessibilityInformation }; } /** @@ -42,4 +43,16 @@ declare module 'vscode' { */ readonly items: ReadonlyArray<[T, TreeItemCheckboxState]>; } + + /** + * Options for creating a {@link TreeView} + */ + export interface TreeViewOptions { + /** + * 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. + * If the tree item is collapsed by default (meaning that the children haven't yet been fetched) then child checkboxes will not be updated. + * To override this behavior and manage child and parent checkbox state in the extension, set this to `true`. + */ + manuallyManageCheckboxSelection?: boolean; + } }