diff --git a/src/vs/base/browser/ui/list/list.ts b/src/vs/base/browser/ui/list/list.ts index 37ff5439232..53651ccc2f9 100644 --- a/src/vs/base/browser/ui/list/list.ts +++ b/src/vs/base/browser/ui/list/list.ts @@ -10,6 +10,7 @@ export interface IVirtualDelegate { getTemplateId(element: T): string; } +// TODO@joao rename to IListRenderer export interface IRenderer { templateId: string; renderTemplate(container: HTMLElement): TTemplateData; diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 6e98a36ad68..a4f8a098827 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -60,11 +60,10 @@ export class ComposedTreeDelegate implements IVirtu interface ITreeListTemplateData { twistie: HTMLElement; - count: HTMLElement; templateData: T; } -function renderTwistie(node: ITreeNode, twistie: HTMLElement): void { +function renderDefaultTwistie(node: ITreeNode, twistie: HTMLElement): void { if (node.children.length === 0 && !node.collapsible) { twistie.innerText = ''; } else { @@ -72,36 +71,46 @@ function renderTwistie(node: ITreeNode, twistie: HTMLElement): void { } } +export interface ITreeRenderer extends IRenderer { + renderTwistie?(element: TElement, twistieElement: HTMLElement): boolean; + onDidChangeTwistieState?: Event; +} + class TreeRenderer implements IRenderer, ITreeListTemplateData> { readonly templateId: string; + private renderedElements = new Map>(); private renderedNodes = new Map, ITreeListTemplateData>(); private disposables: IDisposable[] = []; constructor( - private renderer: IRenderer, + private renderer: ITreeRenderer, onDidChangeCollapseState: Event> ) { this.templateId = renderer.templateId; - onDidChangeCollapseState(this.onDidChangeCollapseState, this, this.disposables); + + onDidChangeCollapseState(this.onDidChangeNodeTwistieState, this, this.disposables); + + if (renderer.onDidChangeTwistieState) { + renderer.onDidChangeTwistieState(this.onDidChangeTwistieState, this, this.disposables); + } } renderTemplate(container: HTMLElement): ITreeListTemplateData { const el = append(container, $('.monaco-tl-row')); const twistie = append(el, $('.tl-twistie')); const contents = append(el, $('.tl-contents')); - const count = append(el, $('.tl-count')); const templateData = this.renderer.renderTemplate(contents); - return { twistie, count, templateData }; + return { twistie, templateData }; } renderElement(node: ITreeNode, index: number, templateData: ITreeListTemplateData): void { this.renderedNodes.set(node, templateData); + this.renderedElements.set(node.element, node); templateData.twistie.style.width = `${10 + node.depth * 10}px`; - renderTwistie(node, templateData.twistie); - templateData.count.textContent = `${node.revealedCount}`; + this.renderTwistie(node, templateData.twistie); this.renderer.renderElement(node.element, index, templateData.templateData); } @@ -109,25 +118,44 @@ class TreeRenderer implements IRenderer, index: number, templateData: ITreeListTemplateData): void { this.renderer.disposeElement(node.element, index, templateData.templateData); this.renderedNodes.delete(node); + this.renderedElements.set(node.element); } disposeTemplate(templateData: ITreeListTemplateData): void { this.renderer.disposeTemplate(templateData.templateData); } - private onDidChangeCollapseState(node: ITreeNode): void { + private onDidChangeTwistieState(element: T): void { + const node = this.renderedElements.get(element); + + if (!node) { + return; + } + + this.onDidChangeNodeTwistieState(node); + } + + private onDidChangeNodeTwistieState(node: ITreeNode): void { const templateData = this.renderedNodes.get(node); if (!templateData) { return; } - renderTwistie(node, templateData.twistie); - templateData.count.textContent = `${node.revealedCount}`; + this.renderTwistie(node, templateData.twistie); + } + + private renderTwistie(node: ITreeNode, twistieElement: HTMLElement) { + if (this.renderer.renderTwistie && this.renderer.renderTwistie(node.element, twistieElement)) { + return; + } + + renderDefaultTwistie(node, twistieElement); } dispose(): void { this.renderedNodes.clear(); + this.renderedElements.clear(); this.disposables = dispose(this.disposables); } } @@ -149,7 +177,7 @@ export abstract class AbstractTree implements IDisposable constructor( container: HTMLElement, delegate: IVirtualDelegate, - renderers: IRenderer[], + renderers: ITreeRenderer[], options?: ITreeOptions ) { const treeDelegate = new ComposedTreeDelegate>(delegate); diff --git a/src/vs/base/browser/ui/tree/dataTree.ts b/src/vs/base/browser/ui/tree/dataTree.ts index 6874371673e..edb99d42585 100644 --- a/src/vs/base/browser/ui/tree/dataTree.ts +++ b/src/vs/base/browser/ui/tree/dataTree.ts @@ -3,11 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITreeOptions, ComposedTreeDelegate, createComposedTreeListOptions } from 'vs/base/browser/ui/tree/abstractTree'; +import { ITreeOptions, ComposedTreeDelegate, createComposedTreeListOptions, ITreeRenderer } from 'vs/base/browser/ui/tree/abstractTree'; import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; import { ITreeElement, ITreeNode } from 'vs/base/browser/ui/tree/tree'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import { timeout } from 'vs/base/common/async'; export interface IDataTreeElement { readonly element: T; @@ -23,7 +25,8 @@ export interface IDataSource> { enum DataTreeNodeState { Uninitialized, Loaded, - Loading + Loading, + Slow } interface IDataTreeNode> { @@ -36,17 +39,21 @@ interface IDataTreeListTemplateData { templateData: T; } -class DataTreeRenderer implements IRenderer, IDataTreeListTemplateData> { +class DataTreeRenderer implements ITreeRenderer, IDataTreeListTemplateData> { readonly templateId: string; + private renderedNodes = new Map, IDataTreeListTemplateData>(); + private disposables: IDisposable[] = []; - constructor(private renderer: IRenderer) { + constructor( + private renderer: IRenderer, + readonly onDidChangeTwistieState: Event> + ) { this.templateId = renderer.templateId; } renderTemplate(container: HTMLElement): IDataTreeListTemplateData { const templateData = this.renderer.renderTemplate(container); - return { templateData }; } @@ -54,6 +61,15 @@ class DataTreeRenderer implements IRenderer, this.renderer.renderElement(node.element, index, templateData.templateData); } + renderTwistie(element: IDataTreeNode, twistieElement: HTMLElement): boolean { + if (element.state === DataTreeNodeState.Slow) { + twistieElement.innerText = '🤨'; + return true; + } + + return false; + } + disposeElement(node: IDataTreeNode, index: number, templateData: IDataTreeListTemplateData): void { this.renderer.disposeElement(node.element, index, templateData.templateData); } @@ -61,6 +77,11 @@ class DataTreeRenderer implements IRenderer, disposeTemplate(templateData: IDataTreeListTemplateData): void { this.renderer.disposeTemplate(templateData.templateData); } + + dispose(): void { + this.renderedNodes.clear(); + this.disposables = dispose(this.disposables); + } } export class DataTree, TFilterData = void> implements IDisposable { @@ -69,17 +90,19 @@ export class DataTree, TFilterData = void> implements private root: IDataTreeNode; private nodes = new Map>(); + private _onDidChangeNodeState = new Emitter>(); + private disposables: IDisposable[] = []; constructor( container: HTMLElement, delegate: IVirtualDelegate, - renderers: IRenderer[], + renderers: ITreeRenderer[], private dataSource: IDataSource, options?: ITreeOptions ) { const treeDelegate = new ComposedTreeDelegate>(delegate); - const treeRenderers = renderers.map(r => new DataTreeRenderer(r)); + const treeRenderers = renderers.map(r => new DataTreeRenderer(r, this._onDidChangeNodeState.event)); const treeOptions = createComposedTreeListOptions>(options); this.tree = new ObjectTree(container, treeDelegate, treeRenderers, treeOptions); @@ -112,10 +135,20 @@ export class DataTree, TFilterData = void> implements return Promise.resolve(null); } 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 = (el: IDataTreeElement): ITreeElement> => { return { @@ -133,7 +166,9 @@ export class DataTree, TFilterData = void> implements 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); diff --git a/test/tree/public/index.html b/test/tree/public/index.html index 09a9767983e..3657c63774a 100644 --- a/test/tree/public/index.html +++ b/test/tree/public/index.html @@ -17,6 +17,10 @@ .tl-contents { flex: 1; } + + .monaco-list-row:hover:not(.selected):not(.focused) { + background: gainsboro !important; + } @@ -133,7 +137,7 @@ element, collapsible: element.type === 'dir' })); - c(els); + setTimeout(() => c(els), 2500); } }; }); diff --git a/test/tree/server.js b/test/tree/server.js index 85ec2637416..7e9084bf18a 100644 --- a/test/tree/server.js +++ b/test/tree/server.js @@ -46,6 +46,14 @@ async function readdir(relativePath) { } } + result.sort((a, b) => { + if (a.type === b.type) { + return a.name < b.name ? -1 : 1; + } + + return a.type === 'dir' ? -1 : 1; + }); + return result; }