// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using FlaxEngine; using FlaxEngine.Assertions; using FlaxEngine.GUI; namespace FlaxEditor.GUI.Tree { /// /// Tree control. /// /// [HideInEditor] public class Tree : ContainerControl { /// /// The key updates timeout in seconds. /// public static float KeyUpdateTimeout = 0.12f; /// /// Delegate for selected tree nodes collection change. /// /// The before state. /// The after state. public delegate void SelectionChangedDelegate(List before, List after); /// /// Delegate for node click events. /// /// The node. /// The location. public delegate void NodeClickDelegate(TreeNode node, Vector2 location); private float _keyUpdateTime; private readonly bool _supportMultiSelect; private Margin _margin; private bool _autoSize = true; /// /// Action fired when tree nodes selection gets changed. /// public event SelectionChangedDelegate SelectedChanged; /// /// Action fired when mouse goes right click up on node. /// public event NodeClickDelegate RightClick; /// /// List with all selected nodes /// [HideInEditor, NoSerialize] public readonly List Selection = new List(); /// /// Gets the first selected node or null. /// public TreeNode SelectedNode => Selection.Count > 0 ? Selection[0] : null; /// /// Gets or sets the margin for the child tree nodes. /// [EditorOrder(0), Tooltip("The margin applied to the child tree nodes.")] public Margin Margin { get => _margin; set { _margin = value; PerformLayout(); } } /// /// Gets or sets the value indicating whenever the tree will auto-size to the tree nodes dimensions. /// [EditorOrder(10), Tooltip("If checked, the tree will auto-size to the tree nodes dimensions.")] public bool AutoSize { get => _autoSize; set { _autoSize = value; PerformLayout(); } } /// /// Initializes a new instance of the class. /// public Tree() : this(false) { } /// /// Initializes a new instance of the class. /// /// True if support multi selection for tree nodes, otherwise false. public Tree(bool supportMultiSelect) : base(0, 0, 100, 100) { IsScrollable = true; AutoFocus = false; _supportMultiSelect = supportMultiSelect; _keyUpdateTime = KeyUpdateTimeout * 10; } internal void OnRightClickInternal(TreeNode node, ref Vector2 location) { RightClick?.Invoke(node, location); } /// /// Selects single tree node. /// /// Node to select. public void Select(TreeNode node) { if (node == null) throw new ArgumentNullException(); // Check if won't change if (Selection.Count == 1 && SelectedNode == node) return; // Cache previous state var prev = new List(Selection); // Update selection Selection.Clear(); Selection.Add(node); // Ensure that node can be visible (all it's parents are expanded) node.ExpandAllParents(); node.Focus(); // Fire event SelectedChanged?.Invoke(prev, Selection); } /// /// Selects tree nodes. /// /// Nodes to select. public void Select(List nodes) { if (nodes == null) throw new ArgumentNullException(); // Check if won't change if (Selection.Count == nodes.Count && Selection.SequenceEqual(nodes)) return; // Cache previous state var prev = new List(Selection); // Update selection Selection.Clear(); if (_supportMultiSelect) Selection.AddRange(nodes); else if (nodes.Count > 0) Selection.Add(nodes[0]); // Ensure that every selected node can be visible (all it's parents are expanded) // TODO: maybe use faster tree walk or faster algorythm? for (int i = 0; i < Selection.Count; i++) { Selection[i].ExpandAllParents(); } // Fire event SelectedChanged?.Invoke(prev, Selection); } /// /// Clears the selection. /// public void Deselect() { // Check if won't change if (Selection.Count == 0) return; // Cache previous state var prev = new List(Selection); // Update selection Selection.Clear(); // Fire event SelectedChanged?.Invoke(prev, Selection); } /// /// Adds or removes node to/from the selection /// /// The node. public void AddOrRemoveSelection(TreeNode node) { // Cache previous state var prev = new List(Selection); // Check if is selected int index = Selection.IndexOf(node); if (index != -1) { // Remove Selection.RemoveAt(index); } else { if (!_supportMultiSelect) Selection.Clear(); // Add Selection.Add(node); } // Fire event SelectedChanged?.Invoke(prev, Selection); } private void WalkSelectRangeExpandedTree(List selection, TreeNode node, ref Rectangle range) { for (int i = 0; i < node.ChildrenCount; i++) { if (node.GetChild(i) is TreeNode child) { Vector2 pos = child.PointToParent(this, Vector2.One); if (range.Contains(pos)) { selection.Add(child); } var nodeArea = new Rectangle(pos, child.Size); if (child.IsExpanded && range.Intersects(ref nodeArea)) WalkSelectRangeExpandedTree(selection, child, ref range); } } } private Rectangle CalcNodeRangeRect(TreeNode node) { Vector2 pos = node.PointToParent(this, Vector2.One); return new Rectangle(pos, new Vector2(10000, 4)); } /// /// Selects tree nodes range (used to select part of the tree using Shift+Mouse). /// /// End range node public void SelectRange(TreeNode endNode) { if (_supportMultiSelect && Selection.Count > 0) { // Cache previous state var prev = new List(Selection); // Update selection var selectionRect = CalcNodeRangeRect(Selection[0]); for (int i = 1; i < Selection.Count; i++) { selectionRect = Rectangle.Union(selectionRect, CalcNodeRangeRect(Selection[i])); } var endNodeRect = CalcNodeRangeRect(endNode); if (endNodeRect.Top - Mathf.Epsilon <= selectionRect.Top) { float diff = selectionRect.Top - endNodeRect.Top; selectionRect.Location.Y -= diff; selectionRect.Size.Y += diff; } else if (endNodeRect.Bottom + Mathf.Epsilon >= selectionRect.Bottom) { float diff = endNodeRect.Bottom - selectionRect.Bottom; selectionRect.Size.Y += diff; } Selection.Clear(); WalkSelectRangeExpandedTree(Selection, _children[0] as TreeNode, ref selectionRect); // Check if changed if (Selection.Count != prev.Count || !Selection.SequenceEqual(prev)) { // Fire event SelectedChanged?.Invoke(prev, Selection); } } else { Select(endNode); } } private void walkSelectExpandedTree(List selection, TreeNode node) { for (int i = 0; i < node.ChildrenCount; i++) { if (node.GetChild(i) is TreeNode child) { selection.Add(child); if (child.IsExpanded) walkSelectExpandedTree(selection, child); } } } /// /// Select all expanded nodes /// public void SelectAllExpanded() { if (_supportMultiSelect) { // Cache previous state var prev = new List(Selection); // Update selection Selection.Clear(); walkSelectExpandedTree(Selection, _children[0] as TreeNode); // Check if changed if (Selection.Count != prev.Count || !Selection.SequenceEqual(prev)) { // Fire event SelectedChanged?.Invoke(prev, Selection); } } } /// /// Updates the tree size. /// public void UpdateSize() { if (!_autoSize) return; // Use max of parent clint area width and root node width float width = 0; if (HasParent) width = Parent.GetClientArea().Width; var rightBottom = Vector2.Zero; for (int i = 0; i < _children.Count; i++) { if (_children[i] is TreeNode node && node.Visible) { width = Mathf.Max(width, node.MinimumWidth); rightBottom = Vector2.Max(rightBottom, node.BottomRight); } } Size = new Vector2(width + _margin.Width, rightBottom.Y + _margin.Bottom); } /// public override void Update(float deltaTime) { var node = SelectedNode; // Check if has focus and if any node is focused and it isn't a root if (ContainsFocus && node != null && !node.IsRoot) { var window = Root; // Check if can perform update if (_keyUpdateTime >= KeyUpdateTimeout) { bool keyUpArrow = window.GetKeyDown(KeyboardKeys.ArrowUp); bool keyDownArrow = window.GetKeyDown(KeyboardKeys.ArrowDown); // Check if arrow flags are different if (keyDownArrow != keyUpArrow) { var nodeParent = node.Parent; var parentNode = nodeParent as TreeNode; var myIndex = nodeParent.GetChildIndex(node); Assert.AreNotEqual(-1, myIndex); // Up if (keyUpArrow) { TreeNode toSelect = null; if (myIndex == 0) { // Select parent (if it exists and it isn't a root) if (parentNode != null && !parentNode.IsRoot) toSelect = parentNode; } else { // Select previous parent child toSelect = nodeParent.GetChild(myIndex - 1) as TreeNode; // Check if is valid and expanded and has any children if (toSelect != null && toSelect.IsExpanded && toSelect.HasAnyVisibleChild) { // Select last child toSelect = toSelect.GetChild(toSelect.ChildrenCount - 1) as TreeNode; } } if (toSelect != null) { // Select Select(toSelect); toSelect.Focus(); } } // Down else { TreeNode toSelect = null; if (node.IsExpanded && node.HasAnyVisibleChild) { // Select the first child toSelect = node.GetChild(0) as TreeNode; } else if (myIndex == nodeParent.ChildrenCount - 1) { // Select next node after parent if (parentNode != null) { int parentIndex = parentNode.IndexInParent; if (parentIndex != -1 && parentIndex < parentNode.Parent.ChildrenCount - 1) { toSelect = parentNode.Parent.GetChild(parentIndex + 1) as TreeNode; } } } else { // Select next parent child toSelect = nodeParent.GetChild(myIndex + 1) as TreeNode; } if (toSelect != null) { // Select Select(toSelect); toSelect.Focus(); } } // Reset time _keyUpdateTime = 0.0f; } } else { // Update time _keyUpdateTime += deltaTime; } if (window.GetKeyDown(KeyboardKeys.ArrowRight)) { if (node.IsExpanded) { // Select first child if has if (node.HasAnyVisibleChild) { Select(node.GetChild(0) as TreeNode); node.Focus(); } } else { // Expand selected node node.Expand(); } } else if (window.GetKeyDown(KeyboardKeys.ArrowLeft)) { if (node.IsCollapsed) { // Select parent if has and is not a root if (node.HasParent && node.Parent is TreeNode nodeParentNode && !nodeParentNode.IsRoot) { Select(nodeParentNode); nodeParentNode.Focus(); } } else { // Collapse selected node node.Collapse(); } } } base.Update(deltaTime); } /// public override bool OnKeyDown(KeyboardKeys key) { // Check if can use multi selection if (_supportMultiSelect) { bool isCtrlDown = Root.GetKey(KeyboardKeys.Control); // Select all expanded nodes if (key == KeyboardKeys.A && isCtrlDown) { SelectAllExpanded(); return true; } } return base.OnKeyDown(key); } /// public override void OnGotFocus() { // Reset timer _keyUpdateTime = 0; base.OnGotFocus(); } /// public override void OnChildResized(Control control) { UpdateSize(); base.OnChildResized(control); } /// public override void OnParentResized() { UpdateSize(); base.OnParentResized(); } /// protected override void PerformLayoutAfterChildren() { // Arrange children float y = _margin.Top; for (int i = 0; i < _children.Count; i++) { if (_children[i] is TreeNode node && node.Visible) { node.Location = new Vector2(_margin.Left, y); y += node.Height + TreeNode.DefaultNodeOffsetY; } } UpdateSize(); } } }