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

Optional TreeItem Checkbox #158250

Merged
merged 16 commits into from
Sep 6, 2022
7 changes: 6 additions & 1 deletion src/vs/workbench/api/browser/mainThreadTreeViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { Disposable } from 'vs/base/common/lifecycle';
import { ExtHostContext, MainThreadTreeViewsShape, ExtHostTreeViewsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostContext, MainThreadTreeViewsShape, ExtHostTreeViewsShape, MainContext, CheckboxUpdate } from 'vs/workbench/api/common/extHost.protocol';
import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions, ResolvableTreeItem, ITreeViewDragAndDropController, IViewBadge } from 'vs/workbench/common/views';
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { distinct } from 'vs/base/common/arrays';
Expand Down Expand Up @@ -169,6 +169,11 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie
this._register(treeView.onDidCollapseItem(item => this._proxy.$setExpanded(treeViewId, item.handle, false)));
this._register(treeView.onDidChangeSelection(items => this._proxy.$setSelection(treeViewId, items.map(({ handle }) => handle))));
this._register(treeView.onDidChangeVisibility(isVisible => this._proxy.$setVisible(treeViewId, isVisible)));
this._register(treeView.onDidChangeCheckboxState(items => {
this._proxy.$changeCheckboxState(treeViewId, <CheckboxUpdate[]>items.map(item => {
return { treeItemHandle: item.handle, newState: item.checkboxChecked ?? false };
}));
}));
}

private getTreeView(treeViewId: string): ITreeView | null {
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
ThemeColor: extHostTypes.ThemeColor,
ThemeIcon: extHostTypes.ThemeIcon,
TreeItem: extHostTypes.TreeItem,
TreeItem2: extHostTypes.TreeItem,
TreeItemCheckboxState: extHostTypes.TreeItemCheckboxState,
TreeItemCollapsibleState: extHostTypes.TreeItemCollapsibleState,
TypeHierarchyItem: extHostTypes.TypeHierarchyItem,
UIKind: UIKind,
Expand Down
6 changes: 6 additions & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1399,13 +1399,19 @@ export interface DataTransferDTO {
readonly items: Array<[/* type */string, DataTransferItemDTO]>;
}

export interface CheckboxUpdate {
treeItemHandle: string;
newState: boolean;
}

export interface ExtHostTreeViewsShape {
$getChildren(treeViewId: string, treeItemHandle?: string): Promise<ITreeItem[] | undefined>;
$handleDrop(destinationViewId: string, requestId: number, treeDataTransfer: DataTransferDTO, targetHandle: string | undefined, token: CancellationToken, operationUuid?: string, sourceViewId?: string, sourceTreeItemHandles?: string[]): Promise<void>;
$handleDrag(sourceViewId: string, sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise<DataTransferDTO | undefined>;
$setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void;
$setSelection(treeViewId: string, treeItemHandles: string[]): void;
$setVisible(treeViewId: string, visible: boolean): void;
$changeCheckboxState(treeViewId: string, checkboxUpdates: CheckboxUpdate[]): void;
$hasResolve(treeViewId: string): Promise<boolean>;
$resolve(treeViewId: string, treeItemHandle: string, token: CancellationToken): Promise<ITreeItem | undefined>;
}
Expand Down
47 changes: 43 additions & 4 deletions src/vs/workbench/api/common/extHostTreeViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import { basename } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { DataTransferDTO, ExtHostTreeViewsShape, MainThreadTreeViewsShape } from './extHost.protocol';
import { CheckboxUpdate, DataTransferDTO, ExtHostTreeViewsShape, MainThreadTreeViewsShape } from './extHost.protocol';
import { ITreeItem, TreeViewItemHandleArg, ITreeItemLabel, IRevealOptions, TreeCommand } from 'vs/workbench/common/views';
import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/common/extHostCommands';
import { asPromise } from 'vs/base/common/async';
import { TreeItemCollapsibleState, ThemeIcon, MarkdownString as MarkdownStringType, TreeItem } from 'vs/workbench/api/common/extHostTypes';
import { TreeItemCollapsibleState, TreeItemCheckboxState, ThemeIcon, MarkdownString as MarkdownStringType, TreeItem } from 'vs/workbench/api/common/extHostTypes';
import { isUndefinedOrNull, isString } from 'vs/base/common/types';
import { equals, coalesce } from 'vs/base/common/arrays';
import { ILogService } from 'vs/platform/log/common/log';
Expand All @@ -23,6 +23,7 @@ import { MarkdownString, ViewBadge, DataTransfer } from 'vs/workbench/api/common
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { ITreeViewsService, TreeviewsService } from 'vs/workbench/services/views/common/treeViewsService';
import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';

type TreeItemHandle = string;

Expand Down Expand Up @@ -99,6 +100,7 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
get onDidChangeSelection() { return treeView.onDidChangeSelection; },
get visible() { return treeView.visible; },
get onDidChangeVisibility() { return treeView.onDidChangeVisibility; },
get onDidChangeTreeCheckbox() { checkProposedApiEnabled(extension, 'treeItemCheckbox'); return treeView.onDidChangeTreeCheckbox; },
get message() { return treeView.message; },
set message(message: string) {
treeView.message = message;
Expand Down Expand Up @@ -226,6 +228,14 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
treeView.setVisible(isVisible);
}

$changeCheckboxState(treeViewId: string, checkboxUpdate: CheckboxUpdate[]): void {
const treeView = this.treeViews.get(treeViewId);
if (!treeView) {
throw new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId));
}
treeView.setCheckboxState(checkboxUpdate);
}

private createExtHostTreeView<T>(id: string, options: vscode.TreeViewOptions<T>, extension: IExtensionDescription): ExtHostTreeView<T> {
const treeView = new ExtHostTreeView<T>(id, options, this._proxy, this.commands.converter, this.logService, extension);
this.treeViews.set(id, treeView);
Expand Down Expand Up @@ -279,6 +289,9 @@ class ExtHostTreeView<T> extends Disposable {
private _onDidChangeVisibility: Emitter<vscode.TreeViewVisibilityChangeEvent> = this._register(new Emitter<vscode.TreeViewVisibilityChangeEvent>());
readonly onDidChangeVisibility: Event<vscode.TreeViewVisibilityChangeEvent> = this._onDidChangeVisibility.event;

private _onDidChangeTreeCheckbox = this._register(new Emitter<vscode.TreeCheckboxChangeEvent<T>>());
readonly onDidChangeTreeCheckbox: Event<vscode.TreeCheckboxChangeEvent<T>> = this._onDidChangeTreeCheckbox.event;

private _onDidChangeData: Emitter<TreeData<T>> = this._register(new Emitter<TreeData<T>>());

private refreshPromise: Promise<void> = Promise.resolve();
Expand Down Expand Up @@ -453,6 +466,26 @@ class ExtHostTreeView<T> extends Disposable {
}
}

async setCheckboxState(checkboxUpdates: CheckboxUpdate[]) {
const items = (await Promise.all(checkboxUpdates.map(async checkboxUpdate => {
const extensionItem = this.getExtensionElement(checkboxUpdate.treeItemHandle);
if (extensionItem) {
return {
extensionItem: extensionItem,
treeItem: await this.dataProvider.getTreeItem(extensionItem),
newState: checkboxUpdate.newState ? TreeItemCheckboxState.Checked : TreeItemCheckboxState.Unchecked
};
}
return Promise.resolve(undefined);
}))).filter((item) => item !== undefined) as { extensionItem: T; treeItem: vscode.TreeItem2; newState: TreeItemCheckboxState }[];

items.forEach(item => {
item.treeItem.checkboxState = item.newState ? TreeItemCheckboxState.Checked : TreeItemCheckboxState.Unchecked;
});

this._onDidChangeTreeCheckbox.fire({ items: items.map(item => [item.extensionItem, item.newState]) });
}

async handleDrag(sourceTreeItemHandles: TreeItemHandle[], treeDataTransfer: vscode.DataTransfer, token: CancellationToken): Promise<vscode.DataTransfer | undefined> {
const extensionTreeItems: T[] = [];
for (const sourceHandle of sourceTreeItemHandles) {
Expand Down Expand Up @@ -696,8 +729,13 @@ class ExtHostTreeView<T> extends Disposable {
return command ? { ...this.commands.toInternal(command, disposable), originalId: command.command } : undefined;
}

private getCheckbox(extensionTreeItem: vscode.TreeItem2): boolean | undefined {
return (extensionTreeItem.checkboxState !== undefined) ?
extensionTreeItem.checkboxState === TreeItemCheckboxState.Checked : undefined;
}

private validateTreeItem(extensionTreeItem: vscode.TreeItem) {
if (!TreeItem.isTreeItem(extensionTreeItem)) {
if (!TreeItem.isTreeItem(extensionTreeItem, this.extension)) {
throw new Error(`Extension ${this.extension.identifier.value} has provided an invalid tree item.`);
}
}
Expand All @@ -720,7 +758,8 @@ class ExtHostTreeView<T> extends Disposable {
iconDark: this.getDarkIconPath(extensionTreeItem) || icon,
themeIcon: this.getThemeIcon(extensionTreeItem),
collapsibleState: isUndefinedOrNull(extensionTreeItem.collapsibleState) ? TreeItemCollapsibleState.None : extensionTreeItem.collapsibleState,
accessibilityInformation: extensionTreeItem.accessibilityInformation
accessibilityInformation: extensionTreeItem.accessibilityInformation,
checkboxChecked: this.getCheckbox(extensionTreeItem)
};

return {
Expand Down
19 changes: 17 additions & 2 deletions src/vs/workbench/api/common/extHostTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import { nextCharLength } from 'vs/base/common/strings';
import { isString, isStringArray } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files';
import { RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { IRelativePatternDto } from 'vs/workbench/api/common/extHost.protocol';
import { CellEditType, ICellPartialMetadataEdit, IDocumentMetadataEdit } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import type * as vscode from 'vscode';

/**
Expand Down Expand Up @@ -2413,12 +2415,13 @@ export class TreeItem {
command?: vscode.Command;
contextValue?: string;
tooltip?: string | vscode.MarkdownString;
checkboxState?: vscode.TreeItemCheckboxState;

static isTreeItem(thing: any): thing is TreeItem {
static isTreeItem(thing: any, extension: IExtensionDescription): thing is TreeItem {
if (thing instanceof TreeItem) {
return true;
}
const treeItemThing = thing as vscode.TreeItem;
const treeItemThing = thing as vscode.TreeItem2;
if (treeItemThing.label !== undefined && !isString(treeItemThing.label) && !(treeItemThing.label?.label)) {
console.log('INVALID tree item, invalid label', treeItemThing.label);
return false;
Expand Down Expand Up @@ -2462,6 +2465,13 @@ export class TreeItem {
console.log('INVALID tree item, invalid accessibilityInformation', treeItemThing.accessibilityInformation);
return false;
}
if (treeItemThing.checkboxState !== undefined) {
checkProposedApiEnabled(extension, 'treeItemCheckbox');
if (treeItemThing.checkboxState !== TreeItemCheckboxState.Checked && treeItemThing.checkboxState !== TreeItemCheckboxState.Unchecked) {
console.log('INVALID tree item, invalid checkboxState', treeItemThing.checkboxState);
return false;
}
}

return true;
}
Expand All @@ -2484,6 +2494,11 @@ export enum TreeItemCollapsibleState {
Expanded = 2
}

export enum TreeItemCheckboxState {
Unchecked = 0,
Checked = 1
}

@es5ClassCompat
export class DataTransferItem {

Expand Down
101 changes: 101 additions & 0 deletions src/vs/workbench/browser/checkbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as DOM from 'vs/base/browser/dom';
import { Toggle } from 'vs/base/browser/ui/toggle/toggle';
import { Codicon } from 'vs/base/common/codicons';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { localize } from 'vs/nls';
import { attachToggleStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ITreeItem } from 'vs/workbench/common/views';

export class CheckboxStateHandler {
private readonly _onDidChangeCheckboxState = new Emitter<ITreeItem[]>();
readonly onDidChangeCheckboxState: Event<ITreeItem[]> = this._onDidChangeCheckboxState.event;

constructor() { }

public setCheckboxState(node: ITreeItem) {
this._onDidChangeCheckboxState.fire([node]);
}
}

export class TreeItemCheckbox extends Disposable {
public toggle: Toggle | undefined;
private checkboxContainer: HTMLDivElement;
public isDisposed = false;

public static readonly checkboxClass = 'custom-view-tree-node-item-checkbox';

private readonly _onDidChangeState = new Emitter<boolean>();
readonly onDidChangeState: Event<boolean> = this._onDidChangeState.event;

constructor(container: HTMLElement, private checkboxStateHandler: CheckboxStateHandler, private themeService: IThemeService) {
super();
this.checkboxContainer = <HTMLDivElement>container;
}

public render(node: ITreeItem) {
if (node.checkboxChecked !== undefined) {
if (!this.toggle) {
this.createCheckbox(node);
this.registerListener(node);
}
else {
this.toggle.checked = node.checkboxChecked;
this.toggle.setIcon(this.toggle.checked ? Codicon.check : undefined);
}
}
}

private createCheckbox(node: ITreeItem) {
if (node.checkboxChecked !== undefined) {
this.toggle = new Toggle({
isChecked: node.checkboxChecked,
title: localize('check', "Check"),
icon: node.checkboxChecked ? Codicon.check : undefined
});

this.toggle.domNode.classList.add(TreeItemCheckbox.checkboxClass);
DOM.append(this.checkboxContainer, this.toggle.domNode);
this.registerListener(node);
}
}

private registerListener(node: ITreeItem) {
if (this.toggle) {
this._register(this.toggle);
this._register(this.toggle.onChange(() => {
this.setCheckbox(node);
}));
this._register(attachToggleStyler(this.toggle, this.themeService));
}
}

private setCheckbox(node: ITreeItem) {
if (this.toggle && node.checkboxChecked !== undefined) {
node.checkboxChecked = this.toggle.checked;
this.toggle.setIcon(this.toggle.checked ? Codicon.check : undefined);
this.toggle.checked = this.toggle.checked;
this.checkboxStateHandler.setCheckboxState(node);
}
}

private removeCheckbox() {
const children = this.checkboxContainer.children;
for (const child of children) {
this.checkboxContainer.removeChild(child);
}
this.toggle = undefined;
}

public override dispose() {
super.dispose();
this.removeCheckbox();
this.isDisposed = true;
}
}
14 changes: 14 additions & 0 deletions src/vs/workbench/browser/parts/views/media/views.css
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,20 @@
padding-left: 3px;
}

.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .custom-view-tree-node-item-checkbox {
width: 16px;
height: 16px;
margin: 3px 6px 3px 0px;
padding: 0px;
border: 1px solid var(--vscode-checkbox-border);
opacity: 1;
background-color: var(--vscode-checkbox-background);
}

.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .custom-view-tree-node-item-checkbox.codicon {
font-size: 13px;
line-height: 15px;
}
.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .monaco-inputbox {
line-height: normal;
flex: 1;
Expand Down
Loading