/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as path from 'path'; export enum TreeCheckboxState { Intermediate = 0, Checked = 1, Unchecked = 2 } export interface TreeComponentDataModel { label?: string; children?: TreeComponentDataModel[]; id?: string; checked?: boolean; } export class TreeNode implements azdata.TreeComponentItem { private _onNodeChange = new vscode.EventEmitter(); private _onTreeChange = new vscode.EventEmitter(); private _data: TreeComponentDataModel; private _parent?: TreeNode; private _root: TreeNode; private _isAlwaysLeaf: boolean; private _nodeMap: Map; private _children: TreeNode[]; public readonly onNodeChange: vscode.Event = this._onNodeChange.event; public readonly onTreeChange: vscode.Event = this._onTreeChange.event; /** * Creates new instance of tree node * @param data the underlining data that's bind to the tree node, any change in the tree will affect the same node in data * @param root the root node of the tree. If passed null, the current node will be the root */ constructor(data: TreeComponentDataModel, root: TreeNode) { if (!data) { throw new Error(`Invalid tree node data`); } if (root === undefined) { root = this; root._nodeMap = new Map(); } this._root = root; if (this.findNode(data.id)) { throw new Error(`tree node with id: '${data.id}' already exists`); } this._data = data; } /** * id for TreeNode */ public get id(): string { return this.data.id; } public set id(value: string) { this.data.id = value; } /** * Label to display to the user, describing this node */ public set label(value: string) { this.data.label = value; } public get collapsibleState(): vscode.TreeItemCollapsibleState { if (!this._isAlwaysLeaf) { return vscode.TreeItemCollapsibleState.Expanded; } else { vscode.TreeItemCollapsibleState.None; } } public get label(): string { return this.data.label; } /** * Is this a leaf node (in which case no children can be generated) or is it expandable? */ public get isAlwaysLeaf(): boolean { return this._isAlwaysLeaf; } /** * Parent of this node */ public get parent(): TreeNode { return this._parent; } public get root(): TreeNode { return this._root; } /** * Path identifying this node */ public get nodePath(): string { return `${this.parent ? this.parent.nodePath + '-' : ''}${this.id}`; } public get data(): TreeComponentDataModel { if (this._data === undefined) { this._data = { label: undefined }; } return this._data; } /** * Sets node checked state to the given value. * @param value New checked state for the node. * @param fromParent Flag that indicates whether change is being propagated from child to parent (false), * from parent to child (true), or state change was initiated by the user (undefined). * @returns true if state has changed, otherwise false. */ public changeNodeCheckedState(value: boolean, fromParent?: boolean): boolean { if (value === this.checked) { return false; } if (fromParent !== false && this.children) { // State change is caused by the user or propagation from parent - propagate new state to children this.children.forEach(child => { child.changeNodeCheckedState(value, true); }); } this.checked = value; this._onNodeChange.fire(); if (fromParent !== true) { // State change is caused by the user or propagation from children - notify parent about check state change // Raise change event from here, if it's not coming from parent and parent node didn't change, // otherwise parent node will raise this event if (!this.parent?.refreshState(value) && this.root) { this.root._onTreeChange.fire(this); } } return true; } public set checked(value: boolean) { this.data.checked = value; } /** * Refreshes the state of the node based on the state of the children nodes. * @param childState New check state of the child node that caused the refresh. * @returns true if state has changed, otherwise false. */ public refreshState(childState?: boolean): boolean { if (childState === undefined) { // Child node is changing to partially checked. // In this case we simply follow its state, as we know not all children are selected further down the tree. return this.changeNodeCheckedState(undefined, false); } else { // Child node is changing to fully checked or unchecked. // In this case we can either change to that same state if all children are the same or become partially checked. return this.changeNodeCheckedState( !this.children || this.children.every(c => c.checked === childState) ? childState : undefined, false); } } public get checked(): boolean { return this.data.checked; } public get checkboxState(): TreeCheckboxState { if (this.checked === undefined) { return TreeCheckboxState.Intermediate; } else { return this.checked ? TreeCheckboxState.Checked : TreeCheckboxState.Unchecked; } } public findNode(id: string): TreeNode { if (this.id === id) { return this; } else if (this.root) { return this.root._nodeMap.has(id) ? this.root._nodeMap.get(id) : undefined; } else { let node: TreeNode; if (this.children) { this.children.forEach(child => { node = child.findNode(id); if (node) { return; } }); } return node; } } /** * Children of this node */ public get children(): TreeNode[] { return this._children; } public addChildNode(node: TreeNode): void { if (node) { if (!node.root) { node._root = this.root; } if (!node.parent) { node._parent = this; } if (node.root) { node.root._nodeMap.set(node.id, node); } this._children.push(node); } } public static createNode(nodeData: TreeComponentDataModel, parent?: TreeNode, root?: TreeNode): TreeNode { let rootNode = root || (parent !== undefined ? parent.root : undefined); let treeNode = new TreeNode(nodeData, rootNode); treeNode._parent = parent; return treeNode; } public static createTree(nodeData: TreeComponentDataModel, parent?: TreeNode, root?: TreeNode): TreeNode { if (nodeData) { let treeNode = TreeNode.createNode(nodeData, parent, root); if (nodeData.children && nodeData.children.length > 0) { treeNode._isAlwaysLeaf = false; treeNode._children = []; nodeData.children.forEach(childNode => { if (childNode) { let childTreeNode = TreeNode.createTree(childNode, treeNode, root || treeNode.root); treeNode.addChildNode(childTreeNode); } }); treeNode.refreshState(); } else { treeNode._isAlwaysLeaf = true; } return treeNode; } else { return undefined; } } } export class TreeDataProvider implements azdata.TreeComponentDataProvider { private _onDidChangeTreeData = new vscode.EventEmitter(); constructor(private _root: TreeNode) { if (this._root) { this._root.onTreeChange(node => { this._onDidChangeTreeData.fire(node); }); } } onDidChangeTreeData?: vscode.Event = this._onDidChangeTreeData.event ; /** * Get [TreeItem](#TreeItem) representation of the `element` * * @param element The element for which [TreeItem](#TreeItem) representation is asked for. * @return [TreeItem](#TreeItem) representation of the element */ getTreeItem(element: TreeNode): azdata.TreeComponentItem | Thenable { let item: azdata.TreeComponentItem = {}; item.label = element.label; item.checked = element.checked; item.collapsibleState = element.collapsibleState; item.iconPath = vscode.Uri.file(path.join(__dirname, '..', 'media', 'monitor.svg')); return item; } /** * Get the children of `element` or root if no element is passed. * * @param element The element from which the provider gets children. Can be `undefined`. * @return Children of `element` or root if no element is passed. */ getChildren(element?: TreeNode): vscode.ProviderResult { if (element) { return Promise.resolve(element.children); } else { return Promise.resolve([this._root]); } } /** * Gets the parent of `element`. Returns `null` or `undefined` if `element` is a child of root. * * @param element The element from which the provider gets parent. * @returns Parent of the `element` or undefined. */ getParent(element: TreeNode): vscode.ProviderResult { return Promise.resolve(element.parent); } }