/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ITreeOptions, ComposedTreeDelegate, createComposedTreeListOptions, ITreeEvent, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/abstractTree'; import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeElement, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Emitter, Event, mapEvent } from 'vs/base/common/event'; import { timeout } from 'vs/base/common/async'; export interface IDataSource> { hasChildren(element: T | null): boolean; getChildren(element: T | null): Thenable; } enum DataTreeNodeState { Uninitialized, Loaded, Loading, Slow } interface IDataTreeNode> { readonly element: T | null; readonly parent: IDataTreeNode | null; state: DataTreeNodeState; } interface IDataTreeListTemplateData { templateData: T; } function unpack(node: ITreeNode, TFilterData>): ITreeNode { return new Proxy(Object.create(null), { get: (_: any, name: string) => { switch (name) { case 'element': return node.element.element; default: return node[name]; } } }); } class DataTreeRenderer implements ITreeRenderer, TFilterData, IDataTreeListTemplateData> { readonly templateId: string; private renderedNodes = new Map, IDataTreeListTemplateData>(); private disposables: IDisposable[] = []; constructor( private renderer: ITreeRenderer, readonly onDidChangeTwistieState: Event> ) { this.templateId = renderer.templateId; } renderTemplate(container: HTMLElement): IDataTreeListTemplateData { const templateData = this.renderer.renderTemplate(container); return { templateData }; } renderElement(element: ITreeNode, TFilterData>, index: number, templateData: IDataTreeListTemplateData): void { this.renderer.renderElement(unpack(element), index, templateData.templateData); } renderTwistie(element: IDataTreeNode, twistieElement: HTMLElement): boolean { if (element.state === DataTreeNodeState.Slow) { twistieElement.innerText = '🤨'; return true; } return false; } disposeElement(element: ITreeNode, TFilterData>, index: number, templateData: IDataTreeListTemplateData): void { this.renderer.disposeElement(unpack(element), index, templateData.templateData); } disposeTemplate(templateData: IDataTreeListTemplateData): void { this.renderer.disposeTemplate(templateData.templateData); } dispose(): void { this.renderedNodes.clear(); this.disposables = dispose(this.disposables); } } function asTreeEvent(e: ITreeEvent>): ITreeEvent { return { browserEvent: e.browserEvent, elements: e.elements.map(e => e.element) }; } function asTreeContextMenuEvent(e: ITreeContextMenuEvent>): ITreeContextMenuEvent { return { browserEvent: e.browserEvent, element: e.element.element, anchor: e.anchor }; } export class DataTree, TFilterData = void> implements IDisposable { private tree: ObjectTree, TFilterData>; private root: IDataTreeNode; private nodes = new Map>(); private _onDidChangeNodeState = new Emitter>(); private disposables: IDisposable[] = []; get onDidChangeFocus(): Event> { return mapEvent(this.tree.onDidChangeFocus, asTreeEvent); } get onDidChangeSelection(): Event> { return mapEvent(this.tree.onDidChangeSelection, asTreeEvent); } get onContextMenu(): Event> { return mapEvent(this.tree.onContextMenu, asTreeContextMenuEvent); } get onDidDOMFocus(): Event { return this.tree.onDidFocus; } get onDidDOMBlur(): Event { return this.tree.onDidBlur; } get onDidDispose(): Event { return this.tree.onDidDispose; } constructor( container: HTMLElement, delegate: IListVirtualDelegate, renderers: ITreeRenderer[], private dataSource: IDataSource, options?: ITreeOptions ) { const objectTreeDelegate = new ComposedTreeDelegate>(delegate); const objectTreeRenderers = renderers.map(r => new DataTreeRenderer(r, this._onDidChangeNodeState.event)); const objectTreeOptions = createComposedTreeListOptions>(options); this.tree = new ObjectTree(container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions); this.root = { element: null, parent: null, state: DataTreeNodeState.Uninitialized, }; this.nodes.set(null, this.root); this.tree.onDidChangeCollapseState(this._onDidChangeCollapseState, this, this.disposables); } layout(height?: number): void { this.tree.layout(height); } refresh(element: T | null): Thenable { return this.refreshNode(this.getNode(element)); } private getNode(element: T | null): IDataTreeNode { const node: IDataTreeNode = this.nodes.get(element); if (typeof node === 'undefined') { throw new Error(`Data tree node not found: ${element}`); } return node; } private refreshNode(node: IDataTreeNode): Thenable { const hasChildren = this.dataSource.hasChildren(node.element); if (!hasChildren) { this.tree.setChildren(node === this.root ? null : node); return Promise.resolve(); } else { node.state = DataTreeNodeState.Loading; this._onDidChangeNodeState.fire(node); const slowTimeout = timeout(800); slowTimeout.then(() => { node.state = DataTreeNodeState.Slow; this._onDidChangeNodeState.fire(node); }); return this.dataSource.getChildren(node.element) .then(children => { slowTimeout.cancel(); node.state = DataTreeNodeState.Loaded; this._onDidChangeNodeState.fire(node); const createTreeElement = (element: T): ITreeElement> => { const collapsible = this.dataSource.hasChildren(element); return { element: { element: element, state: DataTreeNodeState.Uninitialized, parent: node }, collapsible, collapsed: true }; }; const nodeChildren = children.map>>(createTreeElement); this.tree.setChildren(node === this.root ? null : node, nodeChildren); }, err => { slowTimeout.cancel(); node.state = DataTreeNodeState.Uninitialized; this._onDidChangeNodeState.fire(node); if (node !== this.root) { this.tree.collapse(node); } return Promise.reject(err); }); } } private _onDidChangeCollapseState(treeNode: ITreeNode, any>): void { if (!treeNode.collapsed && treeNode.element.state === DataTreeNodeState.Uninitialized) { this.refreshNode(treeNode.element); } } // Tree collapse(element: T): boolean { return this.tree.collapse(this.getNode(element)); } expand(element: T): boolean { return this.tree.expand(this.getNode(element)); } toggleCollapsed(element: T): void { this.tree.toggleCollapsed(this.getNode(element)); } collapseAll(): void { this.tree.collapseAll(); } isCollapsed(element: T): boolean { return this.tree.isCollapsed(this.getNode(element)); } isExpanded(element: T): boolean { return this.tree.isExpanded(this.getNode(element)); } refilter(): void { this.tree.refilter(); } setSelection(elements: T[], browserEvent?: UIEvent): void { const nodes = elements.map(e => this.getNode(e)); this.tree.setSelection(nodes, browserEvent); } getSelection(): T[] { const nodes = this.tree.getSelection(); return nodes.map(n => n.element!); } setFocus(elements: T[], browserEvent?: UIEvent): void { const nodes = elements.map(e => this.getNode(e)); this.tree.setFocus(nodes, browserEvent); } focusNext(n = 1, loop = false, browserEvent?: UIEvent): void { this.tree.focusNext(n, loop, browserEvent); } focusPrevious(n = 1, loop = false, browserEvent?: UIEvent): void { this.tree.focusPrevious(n, loop, browserEvent); } focusNextPage(browserEvent?: UIEvent): void { this.tree.focusNextPage(browserEvent); } focusPreviousPage(browserEvent?: UIEvent): void { this.tree.focusPreviousPage(browserEvent); } focusLast(browserEvent?: UIEvent): void { this.tree.focusLast(browserEvent); } focusFirst(browserEvent?: UIEvent): void { this.tree.focusFirst(browserEvent); } getFocus(): T[] { const nodes = this.tree.getFocus(); return nodes.map(n => n.element!); } open(elements: T[]): void { const nodes = elements.map(e => this.getNode(e)); this.tree.open(nodes); } reveal(element: T, relativeTop?: number): void { this.tree.reveal(this.getNode(element), relativeTop); } getRelativeTop(element: T): number | null { return this.tree.getRelativeTop(this.getNode(element)); } dispose(): void { this.disposables = dispose(this.disposables); } }