Skip to content

Commit f51258b

Browse files
authored
Optional TreeItem Checkbox (#158250)
1 parent daf5eb2 commit f51258b

File tree

12 files changed

+281
-12
lines changed

12 files changed

+281
-12
lines changed

extensions/vscode-api-tests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"textSearchProvider",
4343
"timeline",
4444
"tokenInformation",
45+
"treeItemCheckbox",
4546
"treeViewReveal",
4647
"workspaceTrust",
4748
"telemetry"

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { Disposable } from 'vs/base/common/lifecycle';
7-
import { ExtHostContext, MainThreadTreeViewsShape, ExtHostTreeViewsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol';
7+
import { ExtHostContext, MainThreadTreeViewsShape, ExtHostTreeViewsShape, MainContext, CheckboxUpdate } from 'vs/workbench/api/common/extHost.protocol';
88
import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions, ResolvableTreeItem, ITreeViewDragAndDropController, IViewBadge } from 'vs/workbench/common/views';
99
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
1010
import { distinct } from 'vs/base/common/arrays';
@@ -170,6 +170,11 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie
170170
this._register(treeView.onDidChangeSelection(items => this._proxy.$setSelection(treeViewId, items.map(({ handle }) => handle))));
171171
this._register(treeView.onDidChangeFocus(item => this._proxy.$setFocus(treeViewId, item.handle)));
172172
this._register(treeView.onDidChangeVisibility(isVisible => this._proxy.$setVisible(treeViewId, isVisible)));
173+
this._register(treeView.onDidChangeCheckboxState(items => {
174+
this._proxy.$changeCheckboxState(treeViewId, <CheckboxUpdate[]>items.map(item => {
175+
return { treeItemHandle: item.handle, newState: item.checkboxChecked ?? false };
176+
}));
177+
}));
173178
}
174179

175180
private getTreeView(treeViewId: string): ITreeView | null {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,6 +1295,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
12951295
ThemeColor: extHostTypes.ThemeColor,
12961296
ThemeIcon: extHostTypes.ThemeIcon,
12971297
TreeItem: extHostTypes.TreeItem,
1298+
TreeItem2: extHostTypes.TreeItem,
1299+
TreeItemCheckboxState: extHostTypes.TreeItemCheckboxState,
12981300
TreeItemCollapsibleState: extHostTypes.TreeItemCollapsibleState,
12991301
TypeHierarchyItem: extHostTypes.TypeHierarchyItem,
13001302
UIKind: UIKind,

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,6 +1399,11 @@ export interface DataTransferDTO {
13991399
readonly items: Array<[/* type */string, DataTransferItemDTO]>;
14001400
}
14011401

1402+
export interface CheckboxUpdate {
1403+
treeItemHandle: string;
1404+
newState: boolean;
1405+
}
1406+
14021407
export interface ExtHostTreeViewsShape {
14031408
$getChildren(treeViewId: string, treeItemHandle?: string): Promise<ITreeItem[] | undefined>;
14041409
$handleDrop(destinationViewId: string, requestId: number, treeDataTransfer: DataTransferDTO, targetHandle: string | undefined, token: CancellationToken, operationUuid?: string, sourceViewId?: string, sourceTreeItemHandles?: string[]): Promise<void>;
@@ -1407,6 +1412,7 @@ export interface ExtHostTreeViewsShape {
14071412
$setSelection(treeViewId: string, treeItemHandles: string[]): void;
14081413
$setFocus(treeViewId: string, treeItemHandle: string): void;
14091414
$setVisible(treeViewId: string, visible: boolean): void;
1415+
$changeCheckboxState(treeViewId: string, checkboxUpdates: CheckboxUpdate[]): void;
14101416
$hasResolve(treeViewId: string): Promise<boolean>;
14111417
$resolve(treeViewId: string, treeItemHandle: string, token: CancellationToken): Promise<ITreeItem | undefined>;
14121418
}

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

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import { basename } from 'vs/base/common/resources';
1010
import { URI } from 'vs/base/common/uri';
1111
import { Emitter, Event } from 'vs/base/common/event';
1212
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
13-
import { DataTransferDTO, ExtHostTreeViewsShape, MainThreadTreeViewsShape } from './extHost.protocol';
13+
import { CheckboxUpdate, DataTransferDTO, ExtHostTreeViewsShape, MainThreadTreeViewsShape } from './extHost.protocol';
1414
import { ITreeItem, TreeViewItemHandleArg, ITreeItemLabel, IRevealOptions, TreeCommand, TreeViewPaneHandleArg } from 'vs/workbench/common/views';
1515
import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/common/extHostCommands';
1616
import { asPromise } from 'vs/base/common/async';
17-
import { TreeItemCollapsibleState, ThemeIcon, MarkdownString as MarkdownStringType, TreeItem } from 'vs/workbench/api/common/extHostTypes';
17+
import { TreeItemCollapsibleState, TreeItemCheckboxState, ThemeIcon, MarkdownString as MarkdownStringType, TreeItem } from 'vs/workbench/api/common/extHostTypes';
1818
import { isUndefinedOrNull, isString } from 'vs/base/common/types';
1919
import { equals, coalesce } from 'vs/base/common/arrays';
2020
import { ILogService } from 'vs/platform/log/common/log';
@@ -23,6 +23,7 @@ import { MarkdownString, ViewBadge, DataTransfer } from 'vs/workbench/api/common
2323
import { IMarkdownString } from 'vs/base/common/htmlContent';
2424
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
2525
import { ITreeViewsService, TreeviewsService } from 'vs/workbench/services/views/common/treeViewsService';
26+
import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
2627

2728
type TreeItemHandle = string;
2829

@@ -99,6 +100,7 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
99100
get onDidChangeSelection() { return treeView.onDidChangeSelection; },
100101
get visible() { return treeView.visible; },
101102
get onDidChangeVisibility() { return treeView.onDidChangeVisibility; },
103+
get onDidChangeTreeCheckbox() { checkProposedApiEnabled(extension, 'treeItemCheckbox'); return treeView.onDidChangeTreeCheckbox; },
102104
get message() { return treeView.message; },
103105
set message(message: string) {
104106
treeView.message = message;
@@ -234,6 +236,14 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
234236
treeView.setVisible(isVisible);
235237
}
236238

239+
$changeCheckboxState(treeViewId: string, checkboxUpdate: CheckboxUpdate[]): void {
240+
const treeView = this.treeViews.get(treeViewId);
241+
if (!treeView) {
242+
throw new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId));
243+
}
244+
treeView.setCheckboxState(checkboxUpdate);
245+
}
246+
237247
private createExtHostTreeView<T>(id: string, options: vscode.TreeViewOptions<T>, extension: IExtensionDescription): ExtHostTreeView<T> {
238248
const treeView = new ExtHostTreeView<T>(id, options, this._proxy, this.commands.converter, this.logService, extension);
239249
this.treeViews.set(id, treeView);
@@ -296,6 +306,9 @@ class ExtHostTreeView<T> extends Disposable {
296306
private _onDidChangeVisibility: Emitter<vscode.TreeViewVisibilityChangeEvent> = this._register(new Emitter<vscode.TreeViewVisibilityChangeEvent>());
297307
readonly onDidChangeVisibility: Event<vscode.TreeViewVisibilityChangeEvent> = this._onDidChangeVisibility.event;
298308

309+
private _onDidChangeTreeCheckbox = this._register(new Emitter<vscode.TreeCheckboxChangeEvent<T>>());
310+
readonly onDidChangeTreeCheckbox: Event<vscode.TreeCheckboxChangeEvent<T>> = this._onDidChangeTreeCheckbox.event;
311+
299312
private _onDidChangeData: Emitter<TreeData<T>> = this._register(new Emitter<TreeData<T>>());
300313

301314
private refreshPromise: Promise<void> = Promise.resolve();
@@ -474,6 +487,26 @@ class ExtHostTreeView<T> extends Disposable {
474487
}
475488
}
476489

490+
async setCheckboxState(checkboxUpdates: CheckboxUpdate[]) {
491+
const items = (await Promise.all(checkboxUpdates.map(async checkboxUpdate => {
492+
const extensionItem = this.getExtensionElement(checkboxUpdate.treeItemHandle);
493+
if (extensionItem) {
494+
return {
495+
extensionItem: extensionItem,
496+
treeItem: await this.dataProvider.getTreeItem(extensionItem),
497+
newState: checkboxUpdate.newState ? TreeItemCheckboxState.Checked : TreeItemCheckboxState.Unchecked
498+
};
499+
}
500+
return Promise.resolve(undefined);
501+
}))).filter((item) => item !== undefined) as { extensionItem: T; treeItem: vscode.TreeItem2; newState: TreeItemCheckboxState }[];
502+
503+
items.forEach(item => {
504+
item.treeItem.checkboxState = item.newState ? TreeItemCheckboxState.Checked : TreeItemCheckboxState.Unchecked;
505+
});
506+
507+
this._onDidChangeTreeCheckbox.fire({ items: items.map(item => [item.extensionItem, item.newState]) });
508+
}
509+
477510
async handleDrag(sourceTreeItemHandles: TreeItemHandle[], treeDataTransfer: vscode.DataTransfer, token: CancellationToken): Promise<vscode.DataTransfer | undefined> {
478511
const extensionTreeItems: T[] = [];
479512
for (const sourceHandle of sourceTreeItemHandles) {
@@ -717,8 +750,13 @@ class ExtHostTreeView<T> extends Disposable {
717750
return command ? { ...this.commands.toInternal(command, disposable), originalId: command.command } : undefined;
718751
}
719752

753+
private getCheckbox(extensionTreeItem: vscode.TreeItem2): boolean | undefined {
754+
return (extensionTreeItem.checkboxState !== undefined) ?
755+
extensionTreeItem.checkboxState === TreeItemCheckboxState.Checked : undefined;
756+
}
757+
720758
private validateTreeItem(extensionTreeItem: vscode.TreeItem) {
721-
if (!TreeItem.isTreeItem(extensionTreeItem)) {
759+
if (!TreeItem.isTreeItem(extensionTreeItem, this.extension)) {
722760
throw new Error(`Extension ${this.extension.identifier.value} has provided an invalid tree item.`);
723761
}
724762
}
@@ -741,7 +779,8 @@ class ExtHostTreeView<T> extends Disposable {
741779
iconDark: this.getDarkIconPath(extensionTreeItem) || icon,
742780
themeIcon: this.getThemeIcon(extensionTreeItem),
743781
collapsibleState: isUndefinedOrNull(extensionTreeItem.collapsibleState) ? TreeItemCollapsibleState.None : extensionTreeItem.collapsibleState,
744-
accessibilityInformation: extensionTreeItem.accessibilityInformation
782+
accessibilityInformation: extensionTreeItem.accessibilityInformation,
783+
checkboxChecked: this.getCheckbox(extensionTreeItem)
745784
};
746785

747786
return {

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ import { nextCharLength } from 'vs/base/common/strings';
1313
import { isString, isStringArray } from 'vs/base/common/types';
1414
import { URI } from 'vs/base/common/uri';
1515
import { generateUuid } from 'vs/base/common/uuid';
16+
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
1617
import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files';
1718
import { RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver';
1819
import { IRelativePatternDto } from 'vs/workbench/api/common/extHost.protocol';
1920
import { CellEditType, ICellPartialMetadataEdit, IDocumentMetadataEdit } from 'vs/workbench/contrib/notebook/common/notebookCommon';
21+
import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
2022
import type * as vscode from 'vscode';
2123

2224
/**
@@ -2413,12 +2415,13 @@ export class TreeItem {
24132415
command?: vscode.Command;
24142416
contextValue?: string;
24152417
tooltip?: string | vscode.MarkdownString;
2418+
checkboxState?: vscode.TreeItemCheckboxState;
24162419

2417-
static isTreeItem(thing: any): thing is TreeItem {
2420+
static isTreeItem(thing: any, extension: IExtensionDescription): thing is TreeItem {
24182421
if (thing instanceof TreeItem) {
24192422
return true;
24202423
}
2421-
const treeItemThing = thing as vscode.TreeItem;
2424+
const treeItemThing = thing as vscode.TreeItem2;
24222425
if (treeItemThing.label !== undefined && !isString(treeItemThing.label) && !(treeItemThing.label?.label)) {
24232426
console.log('INVALID tree item, invalid label', treeItemThing.label);
24242427
return false;
@@ -2462,6 +2465,13 @@ export class TreeItem {
24622465
console.log('INVALID tree item, invalid accessibilityInformation', treeItemThing.accessibilityInformation);
24632466
return false;
24642467
}
2468+
if (treeItemThing.checkboxState !== undefined) {
2469+
checkProposedApiEnabled(extension, 'treeItemCheckbox');
2470+
if (treeItemThing.checkboxState !== TreeItemCheckboxState.Checked && treeItemThing.checkboxState !== TreeItemCheckboxState.Unchecked) {
2471+
console.log('INVALID tree item, invalid checkboxState', treeItemThing.checkboxState);
2472+
return false;
2473+
}
2474+
}
24652475

24662476
return true;
24672477
}
@@ -2484,6 +2494,11 @@ export enum TreeItemCollapsibleState {
24842494
Expanded = 2
24852495
}
24862496

2497+
export enum TreeItemCheckboxState {
2498+
Unchecked = 0,
2499+
Checked = 1
2500+
}
2501+
24872502
@es5ClassCompat
24882503
export class DataTransferItem {
24892504

src/vs/workbench/browser/checkbox.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as DOM from 'vs/base/browser/dom';
7+
import { Toggle } from 'vs/base/browser/ui/toggle/toggle';
8+
import { Codicon } from 'vs/base/common/codicons';
9+
import { Emitter, Event } from 'vs/base/common/event';
10+
import { Disposable } from 'vs/base/common/lifecycle';
11+
import { localize } from 'vs/nls';
12+
import { attachToggleStyler } from 'vs/platform/theme/common/styler';
13+
import { IThemeService } from 'vs/platform/theme/common/themeService';
14+
import { ITreeItem } from 'vs/workbench/common/views';
15+
16+
export class CheckboxStateHandler extends Disposable {
17+
private readonly _onDidChangeCheckboxState = this._register(new Emitter<ITreeItem[]>());
18+
readonly onDidChangeCheckboxState: Event<ITreeItem[]> = this._onDidChangeCheckboxState.event;
19+
20+
public setCheckboxState(node: ITreeItem) {
21+
this._onDidChangeCheckboxState.fire([node]);
22+
}
23+
}
24+
25+
export class TreeItemCheckbox extends Disposable {
26+
public toggle: Toggle | undefined;
27+
private checkboxContainer: HTMLDivElement;
28+
public isDisposed = false;
29+
30+
public static readonly checkboxClass = 'custom-view-tree-node-item-checkbox';
31+
32+
private readonly _onDidChangeState = new Emitter<boolean>();
33+
readonly onDidChangeState: Event<boolean> = this._onDidChangeState.event;
34+
35+
constructor(container: HTMLElement, private checkboxStateHandler: CheckboxStateHandler, private themeService: IThemeService) {
36+
super();
37+
this.checkboxContainer = <HTMLDivElement>container;
38+
}
39+
40+
public render(node: ITreeItem) {
41+
if (node.checkboxChecked !== undefined) {
42+
if (!this.toggle) {
43+
this.createCheckbox(node);
44+
}
45+
else {
46+
this.toggle.checked = node.checkboxChecked;
47+
this.toggle.setIcon(this.toggle.checked ? Codicon.check : undefined);
48+
}
49+
}
50+
}
51+
52+
private createCheckbox(node: ITreeItem) {
53+
if (node.checkboxChecked !== undefined) {
54+
this.toggle = new Toggle({
55+
isChecked: node.checkboxChecked,
56+
title: localize('check', "Check"),
57+
icon: node.checkboxChecked ? Codicon.check : undefined
58+
});
59+
60+
this.toggle.domNode.classList.add(TreeItemCheckbox.checkboxClass);
61+
DOM.append(this.checkboxContainer, this.toggle.domNode);
62+
this.registerListener(node);
63+
}
64+
}
65+
66+
private registerListener(node: ITreeItem) {
67+
if (this.toggle) {
68+
this._register({ dispose: () => this.removeCheckbox() });
69+
this._register(this.toggle);
70+
this._register(this.toggle.onChange(() => {
71+
this.setCheckbox(node);
72+
}));
73+
this._register(attachToggleStyler(this.toggle, this.themeService));
74+
}
75+
}
76+
77+
private setCheckbox(node: ITreeItem) {
78+
if (this.toggle && node.checkboxChecked !== undefined) {
79+
node.checkboxChecked = this.toggle.checked;
80+
this.toggle.setIcon(this.toggle.checked ? Codicon.check : undefined);
81+
this.toggle.checked = this.toggle.checked;
82+
this.checkboxStateHandler.setCheckboxState(node);
83+
}
84+
}
85+
86+
private removeCheckbox() {
87+
const children = this.checkboxContainer.children;
88+
for (const child of children) {
89+
this.checkboxContainer.removeChild(child);
90+
}
91+
}
92+
}

src/vs/workbench/browser/parts/views/media/views.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@
120120
padding-left: 3px;
121121
}
122122

123+
.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .custom-view-tree-node-item-checkbox {
124+
width: 16px;
125+
height: 16px;
126+
margin: 3px 6px 3px 0px;
127+
padding: 0px;
128+
border: 1px solid var(--vscode-checkbox-border);
129+
opacity: 1;
130+
background-color: var(--vscode-checkbox-background);
131+
}
132+
133+
.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .custom-view-tree-node-item-checkbox.codicon {
134+
font-size: 13px;
135+
line-height: 15px;
136+
}
123137
.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .monaco-inputbox {
124138
line-height: normal;
125139
flex: 1;

0 commit comments

Comments
 (0)