/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Iterable } from 'vs/base/common/iterator'; import { AbstractTree, IAbstractTreeOptions, IAbstractTreeOptionsUpdate } from 'vs/base/browser/ui/tree/abstractTree'; import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer, ITreeSorter, ICollapseStateChangeEvent } from 'vs/base/browser/ui/tree/tree'; import { ObjectTreeModel, IObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; import { IListVirtualDelegate, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list'; import { Event } from 'vs/base/common/event'; import { CompressibleObjectTreeModel, ElementMapper, ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { memoize } from 'vs/base/common/decorators'; import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; export interface IObjectTreeOptions extends IAbstractTreeOptions { readonly sorter?: ITreeSorter; } export class ObjectTree, TFilterData = void> extends AbstractTree { protected model!: IObjectTreeModel; get onDidChangeCollapseState(): Event> { return this.model.onDidChangeCollapseState; } constructor( user: string, container: HTMLElement, delegate: IListVirtualDelegate, renderers: ITreeRenderer[], options: IObjectTreeOptions = {} ) { super(user, container, delegate, renderers, options as IObjectTreeOptions); } setChildren(element: T | null, children: Iterable> = Iterable.empty()): void { this.model.setChildren(element, children); } rerender(element?: T): void { if (element === undefined) { this.view.rerender(); return; } this.model.rerender(element); } updateElementHeight(element: T, height: number): void { this.model.updateElementHeight(element, height); } resort(element: T, recursive = true): void { this.model.resort(element, recursive); } hasElement(element: T): boolean { return this.model.has(element); } protected createModel(user: string, view: IList>, options: IObjectTreeOptions): ITreeModel { return new ObjectTreeModel(user, view, options); } } interface ICompressedTreeNodeProvider { getCompressedTreeNode(location: T | null): ITreeNode | null, TFilterData>; } export interface ICompressibleTreeRenderer extends ITreeRenderer { renderCompressedElements(node: ITreeNode, TFilterData>, index: number, templateData: TTemplateData, height: number | undefined): void; disposeCompressedElements?(node: ITreeNode, TFilterData>, index: number, templateData: TTemplateData, height: number | undefined): void; } interface CompressibleTemplateData { compressedTreeNode: ITreeNode, TFilterData> | undefined; readonly data: TTemplateData; } class CompressibleRenderer, TFilterData, TTemplateData> implements ITreeRenderer> { readonly templateId: string; readonly onDidChangeTwistieState: Event | undefined; @memoize private get compressedTreeNodeProvider(): ICompressedTreeNodeProvider { return this._compressedTreeNodeProvider(); } constructor(private _compressedTreeNodeProvider: () => ICompressedTreeNodeProvider, private renderer: ICompressibleTreeRenderer) { this.templateId = renderer.templateId; if (renderer.onDidChangeTwistieState) { this.onDidChangeTwistieState = renderer.onDidChangeTwistieState; } } renderTemplate(container: HTMLElement): CompressibleTemplateData { const data = this.renderer.renderTemplate(container); return { compressedTreeNode: undefined, data }; } renderElement(node: ITreeNode, index: number, templateData: CompressibleTemplateData, height: number | undefined): void { const compressedTreeNode = this.compressedTreeNodeProvider.getCompressedTreeNode(node.element) as ITreeNode, TFilterData>; if (compressedTreeNode.element.elements.length === 1) { templateData.compressedTreeNode = undefined; this.renderer.renderElement(node, index, templateData.data, height); } else { templateData.compressedTreeNode = compressedTreeNode; this.renderer.renderCompressedElements(compressedTreeNode, index, templateData.data, height); } } disposeElement(node: ITreeNode, index: number, templateData: CompressibleTemplateData, height: number | undefined): void { if (templateData.compressedTreeNode) { if (this.renderer.disposeCompressedElements) { this.renderer.disposeCompressedElements(templateData.compressedTreeNode, index, templateData.data, height); } } else { if (this.renderer.disposeElement) { this.renderer.disposeElement(node, index, templateData.data, height); } } } disposeTemplate(templateData: CompressibleTemplateData): void { this.renderer.disposeTemplate(templateData.data); } renderTwistie?(element: T, twistieElement: HTMLElement): void { if (this.renderer.renderTwistie) { this.renderer.renderTwistie(element, twistieElement); } } } export interface ICompressibleKeyboardNavigationLabelProvider extends IKeyboardNavigationLabelProvider { getCompressedNodeKeyboardNavigationLabel(elements: T[]): { toString(): string | undefined; } | undefined; } export interface ICompressibleObjectTreeOptions extends IObjectTreeOptions { readonly compressionEnabled?: boolean; readonly elementMapper?: ElementMapper; readonly keyboardNavigationLabelProvider?: ICompressibleKeyboardNavigationLabelProvider; } function asObjectTreeOptions(compressedTreeNodeProvider: () => ICompressedTreeNodeProvider, options?: ICompressibleObjectTreeOptions): IObjectTreeOptions | undefined { return options && { ...options, keyboardNavigationLabelProvider: options.keyboardNavigationLabelProvider && { getKeyboardNavigationLabel(e: T) { let compressedTreeNode: ITreeNode, TFilterData>; try { compressedTreeNode = compressedTreeNodeProvider().getCompressedTreeNode(e) as ITreeNode, TFilterData>; } catch { return options.keyboardNavigationLabelProvider!.getKeyboardNavigationLabel(e); } if (compressedTreeNode.element.elements.length === 1) { return options.keyboardNavigationLabelProvider!.getKeyboardNavigationLabel(e); } else { return options.keyboardNavigationLabelProvider!.getCompressedNodeKeyboardNavigationLabel(compressedTreeNode.element.elements); } } } }; } export interface ICompressibleObjectTreeOptionsUpdate extends IAbstractTreeOptionsUpdate { readonly compressionEnabled?: boolean; } export class CompressibleObjectTree, TFilterData = void> extends ObjectTree implements ICompressedTreeNodeProvider { protected model!: CompressibleObjectTreeModel; constructor( user: string, container: HTMLElement, delegate: IListVirtualDelegate, renderers: ICompressibleTreeRenderer[], options: ICompressibleObjectTreeOptions = {} ) { const compressedTreeNodeProvider = () => this; const compressibleRenderers = renderers.map(r => new CompressibleRenderer(compressedTreeNodeProvider, r)); super(user, container, delegate, compressibleRenderers, asObjectTreeOptions(compressedTreeNodeProvider, options)); } setChildren(element: T | null, children: Iterable> = Iterable.empty()): void { this.model.setChildren(element, children); } protected createModel(user: string, view: IList>, options: ICompressibleObjectTreeOptions): ITreeModel { return new CompressibleObjectTreeModel(user, view, options); } updateOptions(optionsUpdate: ICompressibleObjectTreeOptionsUpdate = {}): void { super.updateOptions(optionsUpdate); if (typeof optionsUpdate.compressionEnabled !== 'undefined') { this.model.setCompressionEnabled(optionsUpdate.compressionEnabled); } } getCompressedTreeNode(element: T | null = null): ITreeNode | null, TFilterData> { return this.model.getCompressedTreeNode(element); } }