diff --git a/Terminal.Gui/Core/ReadOnlyCollectionExtensions.cs b/Terminal.Gui/Core/ReadOnlyCollectionExtensions.cs new file mode 100644 index 000000000..e1176fb65 --- /dev/null +++ b/Terminal.Gui/Core/ReadOnlyCollectionExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +namespace Terminal.Gui { + + static class ReadOnlyCollectionExtensions { + + public static int IndexOf (this IReadOnlyCollection self, Func predicate) + { + int i = 0; + foreach (T element in self) { + if (predicate (element)) + return i; + i++; + } + return -1; + } + public static int IndexOf (this IReadOnlyCollection self, T toFind) + { + int i = 0; + foreach (T element in self) { + if (Equals (element, toFind)) + return i; + i++; + } + return -1; + } + } +} diff --git a/Terminal.Gui/Core/Trees/AspectGetterDelegate.cs b/Terminal.Gui/Core/Trees/AspectGetterDelegate.cs new file mode 100644 index 000000000..0afe47f90 --- /dev/null +++ b/Terminal.Gui/Core/Trees/AspectGetterDelegate.cs @@ -0,0 +1,11 @@ + +namespace Terminal.Gui.Trees { + + /// + /// Delegates of this type are used to fetch string representations of user's model objects + /// + /// The object that is being rendered + /// + public delegate string AspectGetterDelegate (T toRender) where T : class; + +} diff --git a/Terminal.Gui/Core/Trees/Branch.cs b/Terminal.Gui/Core/Trees/Branch.cs new file mode 100644 index 000000000..10c189320 --- /dev/null +++ b/Terminal.Gui/Core/Trees/Branch.cs @@ -0,0 +1,428 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Terminal.Gui.Trees { + class Branch where T : class { + /// + /// True if the branch is expanded to reveal child branches + /// + public bool IsExpanded { get; set; } + + /// + /// The users object that is being displayed by this branch of the tree + /// + public T Model { get; private set; } + + /// + /// The depth of the current branch. Depth of 0 indicates root level branches + /// + public int Depth { get; private set; } = 0; + + /// + /// The children of the current branch. This is null until the first call to + /// to avoid enumerating the entire underlying hierarchy + /// + public Dictionary> ChildBranches { get; set; } + + /// + /// The parent or null if it is a root. + /// + public Branch Parent { get; private set; } + + private TreeView tree; + + /// + /// Declares a new branch of in which the users object + /// is presented + /// + /// The UI control in which the branch resides + /// Pass null for root level branches, otherwise + /// pass the parent + /// The user's object that should be displayed + public Branch (TreeView tree, Branch parentBranchIfAny, T model) + { + this.tree = tree; + this.Model = model; + + if (parentBranchIfAny != null) { + Depth = parentBranchIfAny.Depth + 1; + Parent = parentBranchIfAny; + } + } + + + /// + /// Fetch the children of this branch. This method populates + /// + public virtual void FetchChildren () + { + if (tree.TreeBuilder == null) { + return; + } + + var children = tree.TreeBuilder.GetChildren (this.Model) ?? Enumerable.Empty (); + + this.ChildBranches = children.ToDictionary (k => k, val => new Branch (tree, this, val)); + } + + /// + /// Returns the width of the line including prefix and the results + /// of (the line body). + /// + /// + public virtual int GetWidth (ConsoleDriver driver) + { + return + GetLinePrefix (driver).Sum (Rune.ColumnWidth) + + Rune.ColumnWidth (GetExpandableSymbol (driver)) + + (tree.AspectGetter (Model) ?? "").Length; + } + + /// + /// Renders the current on the specified line + /// + /// + /// + /// + /// + public virtual void Draw (ConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth) + { + // true if the current line of the tree is the selected one and control has focus + bool isSelected = tree.IsSelected (Model) && tree.HasFocus; + Attribute lineColor = isSelected ? colorScheme.Focus : colorScheme.Normal; + + driver.SetAttribute (lineColor); + + // Everything on line before the expansion run and branch text + Rune [] prefix = GetLinePrefix (driver).ToArray (); + Rune expansion = GetExpandableSymbol (driver); + string lineBody = tree.AspectGetter (Model) ?? ""; + + tree.Move (0, y); + + // if we have scrolled to the right then bits of the prefix will have dispeared off the screen + int toSkip = tree.ScrollOffsetHorizontal; + + // Draw the line prefix (all paralell lanes or whitespace and an expand/collapse/leaf symbol) + foreach (Rune r in prefix) { + + if (toSkip > 0) { + toSkip--; + } else { + driver.AddRune (r); + availableWidth -= Rune.ColumnWidth (r); + } + } + + // pick color for expanded symbol + if (tree.Style.ColorExpandSymbol || tree.Style.InvertExpandSymbolColors) { + Attribute color; + + if (tree.Style.ColorExpandSymbol) { + color = isSelected ? tree.ColorScheme.HotFocus : tree.ColorScheme.HotNormal; + } else { + color = lineColor; + } + + if (tree.Style.InvertExpandSymbolColors) { + color = new Attribute (color.Background, color.Foreground); + } + + driver.SetAttribute (color); + } + + if (toSkip > 0) { + toSkip--; + } else { + driver.AddRune (expansion); + availableWidth -= Rune.ColumnWidth (expansion); + } + + // horizontal scrolling has already skipped the prefix but now must also skip some of the line body + if (toSkip > 0) { + if (toSkip > lineBody.Length) { + lineBody = ""; + } else { + lineBody = lineBody.Substring (toSkip); + } + } + + // If body of line is too long + if (lineBody.Sum (l => Rune.ColumnWidth (l)) > availableWidth) { + // remaining space is zero and truncate the line + lineBody = new string (lineBody.TakeWhile (c => (availableWidth -= Rune.ColumnWidth (c)) >= 0).ToArray ()); + availableWidth = 0; + } else { + + // line is short so remaining width will be whatever comes after the line body + availableWidth -= lineBody.Length; + } + + //reset the line color if it was changed for rendering expansion symbol + driver.SetAttribute (lineColor); + driver.AddStr (lineBody); + + if (availableWidth > 0) { + driver.AddStr (new string (' ', availableWidth)); + } + + driver.SetAttribute (colorScheme.Normal); + } + + /// + /// Gets all characters to render prior to the current branches line. This includes indentation + /// whitespace and any tree branches (if enabled) + /// + /// + /// + private IEnumerable GetLinePrefix (ConsoleDriver driver) + { + // If not showing line branches or this is a root object + if (!tree.Style.ShowBranchLines) { + for (int i = 0; i < Depth; i++) { + yield return new Rune (' '); + } + + yield break; + } + + // yield indentations with runes appropriate to the state of the parents + foreach (var cur in GetParentBranches ().Reverse ()) { + if (cur.IsLast ()) { + yield return new Rune (' '); + } else { + yield return driver.VLine; + } + + yield return new Rune (' '); + } + + if (IsLast ()) { + yield return driver.LLCorner; + } else { + yield return driver.LeftTee; + } + } + + /// + /// Returns all parents starting with the immediate parent and ending at the root + /// + /// + private IEnumerable> GetParentBranches () + { + var cur = Parent; + + while (cur != null) { + yield return cur; + cur = cur.Parent; + } + } + + /// + /// Returns an appropriate symbol for displaying next to the string representation of + /// the object to indicate whether it or + /// not (or it is a leaf) + /// + /// + /// + public Rune GetExpandableSymbol (ConsoleDriver driver) + { + var leafSymbol = tree.Style.ShowBranchLines ? driver.HLine : ' '; + + if (IsExpanded) { + return tree.Style.CollapseableSymbol ?? leafSymbol; + } + + if (CanExpand ()) { + return tree.Style.ExpandableSymbol ?? leafSymbol; + } + + return leafSymbol; + } + + /// + /// Returns true if the current branch can be expanded according to + /// the or cached children already fetched + /// + /// + public bool CanExpand () + { + // if we do not know the children yet + if (ChildBranches == null) { + + //if there is a rapid method for determining whether there are children + if (tree.TreeBuilder.SupportsCanExpand) { + return tree.TreeBuilder.CanExpand (Model); + } + + //there is no way of knowing whether we can expand without fetching the children + FetchChildren (); + } + + //we fetched or already know the children, so return whether we have any + return ChildBranches.Any (); + } + + /// + /// Expands the current branch if possible + /// + public void Expand () + { + if (ChildBranches == null) { + FetchChildren (); + } + + if (ChildBranches.Any ()) { + IsExpanded = true; + } + } + + /// + /// Marks the branch as collapsed ( false) + /// + public void Collapse () + { + IsExpanded = false; + } + + /// + /// Refreshes cached knowledge in this branch e.g. what children an object has + /// + /// True to also refresh all + /// branches (starting with the root) + public void Refresh (bool startAtTop) + { + // if we must go up and refresh from the top down + if (startAtTop) { + Parent?.Refresh (true); + } + + // we don't want to loose the state of our children so lets be selective about how we refresh + //if we don't know about any children yet just use the normal method + if (ChildBranches == null) { + FetchChildren (); + } else { + // we already knew about some children so preserve the state of the old children + + // first gather the new Children + var newChildren = tree.TreeBuilder?.GetChildren (this.Model) ?? Enumerable.Empty (); + + // Children who no longer appear need to go + foreach (var toRemove in ChildBranches.Keys.Except (newChildren).ToArray ()) { + ChildBranches.Remove (toRemove); + + //also if the user has this node selected (its disapearing) so lets change selection to us (the parent object) to be helpful + if (Equals (tree.SelectedObject, toRemove)) { + tree.SelectedObject = Model; + } + } + + // New children need to be added + foreach (var newChild in newChildren) { + // If we don't know about the child yet we need a new branch + if (!ChildBranches.ContainsKey (newChild)) { + ChildBranches.Add (newChild, new Branch (tree, this, newChild)); + } else { + //we already have this object but update the reference anyway incase Equality match but the references are new + ChildBranches [newChild].Model = newChild; + } + } + } + + } + + /// + /// Calls on the current branch and all expanded children + /// + internal void Rebuild () + { + Refresh (false); + + // if we know about our children + if (ChildBranches != null) { + if (IsExpanded) { + //if we are expanded we need to updatethe visible children + foreach (var child in ChildBranches) { + child.Value.Rebuild (); + } + + } else { + // we are not expanded so should forget about children because they may not exist anymore + ChildBranches = null; + } + } + + } + + /// + /// Returns true if this branch has parents and it is the last node of it's parents + /// branches (or last root of the tree) + /// + /// + private bool IsLast () + { + if (Parent == null) { + return this == tree.roots.Values.LastOrDefault (); + } + + return Parent.ChildBranches.Values.LastOrDefault () == this; + } + + /// + /// Returns true if the given x offset on the branch line is the +/- symbol. Returns + /// false if not showing expansion symbols or leaf node etc + /// + /// + /// + /// + internal bool IsHitOnExpandableSymbol (ConsoleDriver driver, int x) + { + // if leaf node then we cannot expand + if (!CanExpand ()) { + return false; + } + + // if we could theoretically expand + if (!IsExpanded && tree.Style.ExpandableSymbol != null) { + return x == GetLinePrefix (driver).Count (); + } + + // if we could theoretically collapse + if (IsExpanded && tree.Style.CollapseableSymbol != null) { + return x == GetLinePrefix (driver).Count (); + } + + return false; + } + + /// + /// Expands the current branch and all children branches + /// + internal void ExpandAll () + { + Expand (); + + if (ChildBranches != null) { + foreach (var child in ChildBranches) { + child.Value.ExpandAll (); + } + } + } + + /// + /// Collapses the current branch and all children branches (even though those branches are + /// no longer visible they retain collapse/expansion state) + /// + internal void CollapseAll () + { + Collapse (); + + if (ChildBranches != null) { + foreach (var child in ChildBranches) { + child.Value.CollapseAll (); + } + } + } + } +} \ No newline at end of file diff --git a/Terminal.Gui/Core/Trees/DelegateTreeBuilder.cs b/Terminal.Gui/Core/Trees/DelegateTreeBuilder.cs new file mode 100644 index 000000000..a277b49cc --- /dev/null +++ b/Terminal.Gui/Core/Trees/DelegateTreeBuilder.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; + +namespace Terminal.Gui.Trees { + /// + /// Implementation of that uses user defined functions + /// + public class DelegateTreeBuilder : TreeBuilder { + private Func> childGetter; + private Func canExpand; + + /// + /// Constructs an implementation of that calls the user + /// defined method to determine children + /// + /// + /// + public DelegateTreeBuilder (Func> childGetter) : base (false) + { + this.childGetter = childGetter; + } + + /// + /// Constructs an implementation of that calls the user + /// defined method to determine children + /// and to determine expandability + /// + /// + /// + /// + public DelegateTreeBuilder (Func> childGetter, Func canExpand) : base (true) + { + this.childGetter = childGetter; + this.canExpand = canExpand; + } + + /// + /// Returns whether a node can be expanded based on the delegate passed during construction + /// + /// + /// + public override bool CanExpand (T toExpand) + { + return canExpand?.Invoke (toExpand) ?? base.CanExpand (toExpand); + } + + /// + /// Returns children using the delegate method passed during construction + /// + /// + /// + public override IEnumerable GetChildren (T forObject) + { + return childGetter.Invoke (forObject); + } + } +} \ No newline at end of file diff --git a/Terminal.Gui/Core/Trees/ITreeBuilder.cs b/Terminal.Gui/Core/Trees/ITreeBuilder.cs new file mode 100644 index 000000000..d23d00f77 --- /dev/null +++ b/Terminal.Gui/Core/Trees/ITreeBuilder.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace Terminal.Gui.Trees { + /// + /// Interface for supplying data to a on demand as root level nodes + /// are expanded by the user + /// + public interface ITreeBuilder { + /// + /// Returns true if is implemented by this class + /// + /// + bool SupportsCanExpand { get; } + + /// + /// Returns true/false for whether a model has children. This method should be implemented + /// when is an expensive operation otherwise + /// should return false (in which case this method will not + /// be called) + /// + /// Only implement this method if you have a very fast way of determining whether + /// an object can have children e.g. checking a Type (directories can always be expanded) + /// + /// + /// + bool CanExpand (T toExpand); + + /// + /// Returns all children of a given which should be added to the + /// tree as new branches underneath it + /// + /// + /// + IEnumerable GetChildren (T forObject); + } +} \ No newline at end of file diff --git a/Terminal.Gui/Core/Trees/ObjectActivatedEventArgs.cs b/Terminal.Gui/Core/Trees/ObjectActivatedEventArgs.cs new file mode 100644 index 000000000..e4dd95af7 --- /dev/null +++ b/Terminal.Gui/Core/Trees/ObjectActivatedEventArgs.cs @@ -0,0 +1,32 @@ +namespace Terminal.Gui.Trees { + /// + /// Event args for the event + /// + /// + public class ObjectActivatedEventArgs where T : class { + + /// + /// The tree in which the activation occurred + /// + /// + public TreeView Tree { get; } + + /// + /// The object that was selected at the time of activation + /// + /// + public T ActivatedObject { get; } + + + /// + /// Creates a new instance documenting activation of the object + /// + /// Tree in which the activation is happening + /// What object is being activated + public ObjectActivatedEventArgs (TreeView tree, T activated) + { + Tree = tree; + ActivatedObject = activated; + } + } +} \ No newline at end of file diff --git a/Terminal.Gui/Core/Trees/SelectionChangedEventArgs.cs b/Terminal.Gui/Core/Trees/SelectionChangedEventArgs.cs new file mode 100644 index 000000000..b84687f4a --- /dev/null +++ b/Terminal.Gui/Core/Trees/SelectionChangedEventArgs.cs @@ -0,0 +1,37 @@ +using System; + +namespace Terminal.Gui.Trees { + /// + /// Event arguments describing a change in selected object in a tree view + /// + public class SelectionChangedEventArgs : EventArgs where T : class { + /// + /// The view in which the change occurred + /// + public TreeView Tree { get; } + + /// + /// The previously selected value (can be null) + /// + public T OldValue { get; } + + /// + /// The newly selected value in the (can be null) + /// + public T NewValue { get; } + + /// + /// Creates a new instance of event args describing a change of selection + /// in + /// + /// + /// + /// + public SelectionChangedEventArgs (TreeView tree, T oldValue, T newValue) + { + Tree = tree; + OldValue = oldValue; + NewValue = newValue; + } + } +} \ No newline at end of file diff --git a/Terminal.Gui/Core/Trees/TreeBuilder.cs b/Terminal.Gui/Core/Trees/TreeBuilder.cs new file mode 100644 index 000000000..15c4e806d --- /dev/null +++ b/Terminal.Gui/Core/Trees/TreeBuilder.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Terminal.Gui.Trees { + + /// + /// Abstract implementation of . + /// + public abstract class TreeBuilder : ITreeBuilder { + + /// + public bool SupportsCanExpand { get; protected set; } = false; + + /// + /// Override this method to return a rapid answer as to whether + /// returns results. If you are implementing this method ensure you passed true in base + /// constructor or set + /// + /// + /// + public virtual bool CanExpand (T toExpand) + { + + return GetChildren (toExpand).Any (); + } + + /// + public abstract IEnumerable GetChildren (T forObject); + + /// + /// Constructs base and initializes + /// + /// Pass true if you intend to + /// implement otherwise false + public TreeBuilder (bool supportsCanExpand) + { + SupportsCanExpand = supportsCanExpand; + } + } +} \ No newline at end of file diff --git a/Terminal.Gui/Views/TreeNode.cs b/Terminal.Gui/Core/Trees/TreeNode.cs similarity index 98% rename from Terminal.Gui/Views/TreeNode.cs rename to Terminal.Gui/Core/Trees/TreeNode.cs index 5ae07ae19..379f8f7f2 100644 --- a/Terminal.Gui/Views/TreeNode.cs +++ b/Terminal.Gui/Core/Trees/TreeNode.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Terminal.Gui { +namespace Terminal.Gui.Trees { /// /// Interface to implement when you want the regular (non generic) diff --git a/Terminal.Gui/Core/Trees/TreeNodeBuilder.cs b/Terminal.Gui/Core/Trees/TreeNodeBuilder.cs new file mode 100644 index 000000000..c81aa8aef --- /dev/null +++ b/Terminal.Gui/Core/Trees/TreeNodeBuilder.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace Terminal.Gui.Trees { + /// + /// implementation for objects + /// + public class TreeNodeBuilder : TreeBuilder { + + /// + /// Initialises a new instance of builder for any model objects of + /// Type + /// + public TreeNodeBuilder () : base (false) + { + + } + + /// + /// Returns from + /// + /// + /// + public override IEnumerable GetChildren (ITreeNode model) + { + return model.Children; + } + } +} \ No newline at end of file diff --git a/Terminal.Gui/Views/TreeStyle.cs b/Terminal.Gui/Core/Trees/TreeStyle.cs similarity index 97% rename from Terminal.Gui/Views/TreeStyle.cs rename to Terminal.Gui/Core/Trees/TreeStyle.cs index a6269aa60..f6cc30e4c 100644 --- a/Terminal.Gui/Views/TreeStyle.cs +++ b/Terminal.Gui/Core/Trees/TreeStyle.cs @@ -1,6 +1,6 @@ using System; -namespace Terminal.Gui { +namespace Terminal.Gui.Trees { /// /// Defines rendering options that affect how the tree is displayed /// diff --git a/Terminal.Gui/Views/TreeBuilder.cs b/Terminal.Gui/Views/TreeBuilder.cs deleted file mode 100644 index a76af982e..000000000 --- a/Terminal.Gui/Views/TreeBuilder.cs +++ /dev/null @@ -1,155 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Terminal.Gui { - - /// - /// Interface for supplying data to a on demand as root level nodes - /// are expanded by the user - /// - public interface ITreeBuilder { - /// - /// Returns true if is implemented by this class - /// - /// - bool SupportsCanExpand { get; } - - /// - /// Returns true/false for whether a model has children. This method should be implemented - /// when is an expensive operation otherwise - /// should return false (in which case this method will not - /// be called) - /// - /// Only implement this method if you have a very fast way of determining whether - /// an object can have children e.g. checking a Type (directories can always be expanded) - /// - /// - /// - bool CanExpand (T toExpand); - - /// - /// Returns all children of a given which should be added to the - /// tree as new branches underneath it - /// - /// - /// - IEnumerable GetChildren (T forObject); - } - - /// - /// Abstract implementation of . - /// - public abstract class TreeBuilder : ITreeBuilder { - - /// - public bool SupportsCanExpand { get; protected set; } = false; - - /// - /// Override this method to return a rapid answer as to whether - /// returns results. If you are implementing this method ensure you passed true in base - /// constructor or set - /// - /// - /// - public virtual bool CanExpand (T toExpand) - { - - return GetChildren (toExpand).Any (); - } - - /// - public abstract IEnumerable GetChildren (T forObject); - - /// - /// Constructs base and initializes - /// - /// Pass true if you intend to - /// implement otherwise false - public TreeBuilder (bool supportsCanExpand) - { - SupportsCanExpand = supportsCanExpand; - } - } - - - - /// - /// implementation for objects - /// - public class TreeNodeBuilder : TreeBuilder { - - /// - /// Initialises a new instance of builder for any model objects of - /// Type - /// - public TreeNodeBuilder () : base (false) - { - - } - - /// - /// Returns from - /// - /// - /// - public override IEnumerable GetChildren (ITreeNode model) - { - return model.Children; - } - } - - - /// - /// Implementation of that uses user defined functions - /// - public class DelegateTreeBuilder : TreeBuilder { - private Func> childGetter; - private Func canExpand; - - /// - /// Constructs an implementation of that calls the user - /// defined method to determine children - /// - /// - /// - public DelegateTreeBuilder (Func> childGetter) : base (false) - { - this.childGetter = childGetter; - } - - /// - /// Constructs an implementation of that calls the user - /// defined method to determine children - /// and to determine expandability - /// - /// - /// - /// - public DelegateTreeBuilder (Func> childGetter, Func canExpand) : base (true) - { - this.childGetter = childGetter; - this.canExpand = canExpand; - } - - /// - /// Returns whether a node can be expanded based on the delegate passed during construction - /// - /// - /// - public override bool CanExpand (T toExpand) - { - return canExpand?.Invoke (toExpand) ?? base.CanExpand (toExpand); - } - - /// - /// Returns children using the delegate method passed during construction - /// - /// - /// - public override IEnumerable GetChildren (T forObject) - { - return childGetter.Invoke (forObject); - } - } -} \ No newline at end of file diff --git a/Terminal.Gui/Views/TreeView.cs b/Terminal.Gui/Views/TreeView.cs index 954968567..ecf67c478 100644 --- a/Terminal.Gui/Views/TreeView.cs +++ b/Terminal.Gui/Views/TreeView.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using NStack; +using Terminal.Gui.Trees; namespace Terminal.Gui { @@ -1216,37 +1217,6 @@ namespace Terminal.Gui { } } - /// - /// Event args for the event - /// - /// - public class ObjectActivatedEventArgs where T : class { - - /// - /// The tree in which the activation occurred - /// - /// - public TreeView Tree { get; } - - /// - /// The object that was selected at the time of activation - /// - /// - public T ActivatedObject { get; } - - - /// - /// Creates a new instance documenting activation of the object - /// - /// Tree in which the activation is happening - /// What object is being activated - public ObjectActivatedEventArgs (TreeView tree, T activated) - { - Tree = tree; - ActivatedObject = activated; - } - } - class TreeSelection where T : class { public Branch Origin { get; } @@ -1281,491 +1251,4 @@ namespace Terminal.Gui { } } - class Branch where T : class { - /// - /// True if the branch is expanded to reveal child branches - /// - public bool IsExpanded { get; set; } - - /// - /// The users object that is being displayed by this branch of the tree - /// - public T Model { get; private set; } - - /// - /// The depth of the current branch. Depth of 0 indicates root level branches - /// - public int Depth { get; private set; } = 0; - - /// - /// The children of the current branch. This is null until the first call to - /// to avoid enumerating the entire underlying hierarchy - /// - public Dictionary> ChildBranches { get; set; } - - /// - /// The parent or null if it is a root. - /// - public Branch Parent { get; private set; } - - private TreeView tree; - - /// - /// Declares a new branch of in which the users object - /// is presented - /// - /// The UI control in which the branch resides - /// Pass null for root level branches, otherwise - /// pass the parent - /// The user's object that should be displayed - public Branch (TreeView tree, Branch parentBranchIfAny, T model) - { - this.tree = tree; - this.Model = model; - - if (parentBranchIfAny != null) { - Depth = parentBranchIfAny.Depth + 1; - Parent = parentBranchIfAny; - } - } - - - /// - /// Fetch the children of this branch. This method populates - /// - public virtual void FetchChildren () - { - if (tree.TreeBuilder == null) { - return; - } - - var children = tree.TreeBuilder.GetChildren (this.Model) ?? Enumerable.Empty (); - - this.ChildBranches = children.ToDictionary (k => k, val => new Branch (tree, this, val)); - } - - /// - /// Returns the width of the line including prefix and the results - /// of (the line body). - /// - /// - public virtual int GetWidth (ConsoleDriver driver) - { - return - GetLinePrefix (driver).Sum (Rune.ColumnWidth) + - Rune.ColumnWidth (GetExpandableSymbol (driver)) + - (tree.AspectGetter (Model) ?? "").Length; - } - - /// - /// Renders the current on the specified line - /// - /// - /// - /// - /// - public virtual void Draw (ConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth) - { - // true if the current line of the tree is the selected one and control has focus - bool isSelected = tree.IsSelected (Model) && tree.HasFocus; - Attribute lineColor = isSelected ? colorScheme.Focus : colorScheme.Normal; - - driver.SetAttribute (lineColor); - - // Everything on line before the expansion run and branch text - Rune [] prefix = GetLinePrefix (driver).ToArray (); - Rune expansion = GetExpandableSymbol (driver); - string lineBody = tree.AspectGetter (Model) ?? ""; - - tree.Move (0, y); - - // if we have scrolled to the right then bits of the prefix will have dispeared off the screen - int toSkip = tree.ScrollOffsetHorizontal; - - // Draw the line prefix (all paralell lanes or whitespace and an expand/collapse/leaf symbol) - foreach (Rune r in prefix) { - - if (toSkip > 0) { - toSkip--; - } else { - driver.AddRune (r); - availableWidth -= Rune.ColumnWidth (r); - } - } - - // pick color for expanded symbol - if (tree.Style.ColorExpandSymbol || tree.Style.InvertExpandSymbolColors) { - Attribute color; - - if (tree.Style.ColorExpandSymbol) { - color = isSelected ? tree.ColorScheme.HotFocus : tree.ColorScheme.HotNormal; - } else { - color = lineColor; - } - - if (tree.Style.InvertExpandSymbolColors) { - color = new Attribute (color.Background, color.Foreground); - } - - driver.SetAttribute (color); - } - - if (toSkip > 0) { - toSkip--; - } else { - driver.AddRune (expansion); - availableWidth -= Rune.ColumnWidth (expansion); - } - - // horizontal scrolling has already skipped the prefix but now must also skip some of the line body - if (toSkip > 0) { - if (toSkip > lineBody.Length) { - lineBody = ""; - } else { - lineBody = lineBody.Substring (toSkip); - } - } - - // If body of line is too long - if (lineBody.Sum (l => Rune.ColumnWidth (l)) > availableWidth) { - // remaining space is zero and truncate the line - lineBody = new string (lineBody.TakeWhile (c => (availableWidth -= Rune.ColumnWidth (c)) >= 0).ToArray ()); - availableWidth = 0; - } else { - - // line is short so remaining width will be whatever comes after the line body - availableWidth -= lineBody.Length; - } - - //reset the line color if it was changed for rendering expansion symbol - driver.SetAttribute (lineColor); - driver.AddStr (lineBody); - - if (availableWidth > 0) { - driver.AddStr (new string (' ', availableWidth)); - } - - driver.SetAttribute (colorScheme.Normal); - } - - /// - /// Gets all characters to render prior to the current branches line. This includes indentation - /// whitespace and any tree branches (if enabled) - /// - /// - /// - private IEnumerable GetLinePrefix (ConsoleDriver driver) - { - // If not showing line branches or this is a root object - if (!tree.Style.ShowBranchLines) { - for (int i = 0; i < Depth; i++) { - yield return new Rune (' '); - } - - yield break; - } - - // yield indentations with runes appropriate to the state of the parents - foreach (var cur in GetParentBranches ().Reverse ()) { - if (cur.IsLast ()) { - yield return new Rune (' '); - } else { - yield return driver.VLine; - } - - yield return new Rune (' '); - } - - if (IsLast ()) { - yield return driver.LLCorner; - } else { - yield return driver.LeftTee; - } - } - - /// - /// Returns all parents starting with the immediate parent and ending at the root - /// - /// - private IEnumerable> GetParentBranches () - { - var cur = Parent; - - while (cur != null) { - yield return cur; - cur = cur.Parent; - } - } - - /// - /// Returns an appropriate symbol for displaying next to the string representation of - /// the object to indicate whether it or - /// not (or it is a leaf) - /// - /// - /// - public Rune GetExpandableSymbol (ConsoleDriver driver) - { - var leafSymbol = tree.Style.ShowBranchLines ? driver.HLine : ' '; - - if (IsExpanded) { - return tree.Style.CollapseableSymbol ?? leafSymbol; - } - - if (CanExpand ()) { - return tree.Style.ExpandableSymbol ?? leafSymbol; - } - - return leafSymbol; - } - - /// - /// Returns true if the current branch can be expanded according to - /// the or cached children already fetched - /// - /// - public bool CanExpand () - { - // if we do not know the children yet - if (ChildBranches == null) { - - //if there is a rapid method for determining whether there are children - if (tree.TreeBuilder.SupportsCanExpand) { - return tree.TreeBuilder.CanExpand (Model); - } - - //there is no way of knowing whether we can expand without fetching the children - FetchChildren (); - } - - //we fetched or already know the children, so return whether we have any - return ChildBranches.Any (); - } - - /// - /// Expands the current branch if possible - /// - public void Expand () - { - if (ChildBranches == null) { - FetchChildren (); - } - - if (ChildBranches.Any ()) { - IsExpanded = true; - } - } - - /// - /// Marks the branch as collapsed ( false) - /// - public void Collapse () - { - IsExpanded = false; - } - - /// - /// Refreshes cached knowledge in this branch e.g. what children an object has - /// - /// True to also refresh all - /// branches (starting with the root) - public void Refresh (bool startAtTop) - { - // if we must go up and refresh from the top down - if (startAtTop) { - Parent?.Refresh (true); - } - - // we don't want to loose the state of our children so lets be selective about how we refresh - //if we don't know about any children yet just use the normal method - if (ChildBranches == null) { - FetchChildren (); - } else { - // we already knew about some children so preserve the state of the old children - - // first gather the new Children - var newChildren = tree.TreeBuilder?.GetChildren (this.Model) ?? Enumerable.Empty (); - - // Children who no longer appear need to go - foreach (var toRemove in ChildBranches.Keys.Except (newChildren).ToArray ()) { - ChildBranches.Remove (toRemove); - - //also if the user has this node selected (its disapearing) so lets change selection to us (the parent object) to be helpful - if (Equals (tree.SelectedObject, toRemove)) { - tree.SelectedObject = Model; - } - } - - // New children need to be added - foreach (var newChild in newChildren) { - // If we don't know about the child yet we need a new branch - if (!ChildBranches.ContainsKey (newChild)) { - ChildBranches.Add (newChild, new Branch (tree, this, newChild)); - } else { - //we already have this object but update the reference anyway incase Equality match but the references are new - ChildBranches [newChild].Model = newChild; - } - } - } - - } - - /// - /// Calls on the current branch and all expanded children - /// - internal void Rebuild () - { - Refresh (false); - - // if we know about our children - if (ChildBranches != null) { - if (IsExpanded) { - //if we are expanded we need to updatethe visible children - foreach (var child in ChildBranches) { - child.Value.Rebuild (); - } - - } else { - // we are not expanded so should forget about children because they may not exist anymore - ChildBranches = null; - } - } - - } - - /// - /// Returns true if this branch has parents and it is the last node of it's parents - /// branches (or last root of the tree) - /// - /// - private bool IsLast () - { - if (Parent == null) { - return this == tree.roots.Values.LastOrDefault (); - } - - return Parent.ChildBranches.Values.LastOrDefault () == this; - } - - /// - /// Returns true if the given x offset on the branch line is the +/- symbol. Returns - /// false if not showing expansion symbols or leaf node etc - /// - /// - /// - /// - internal bool IsHitOnExpandableSymbol (ConsoleDriver driver, int x) - { - // if leaf node then we cannot expand - if (!CanExpand ()) { - return false; - } - - // if we could theoretically expand - if (!IsExpanded && tree.Style.ExpandableSymbol != null) { - return x == GetLinePrefix (driver).Count (); - } - - // if we could theoretically collapse - if (IsExpanded && tree.Style.CollapseableSymbol != null) { - return x == GetLinePrefix (driver).Count (); - } - - return false; - } - - /// - /// Expands the current branch and all children branches - /// - internal void ExpandAll () - { - Expand (); - - if (ChildBranches != null) { - foreach (var child in ChildBranches) { - child.Value.ExpandAll (); - } - } - } - - /// - /// Collapses the current branch and all children branches (even though those branches are - /// no longer visible they retain collapse/expansion state) - /// - internal void CollapseAll () - { - Collapse (); - - if (ChildBranches != null) { - foreach (var child in ChildBranches) { - child.Value.CollapseAll (); - } - } - } - } - - /// - /// Delegates of this type are used to fetch string representations of user's model objects - /// - /// The object that is being rendered - /// - public delegate string AspectGetterDelegate (T toRender) where T : class; - - /// - /// Event arguments describing a change in selected object in a tree view - /// - public class SelectionChangedEventArgs : EventArgs where T : class { - /// - /// The view in which the change occurred - /// - public TreeView Tree { get; } - - /// - /// The previously selected value (can be null) - /// - public T OldValue { get; } - - /// - /// The newly selected value in the (can be null) - /// - public T NewValue { get; } - - /// - /// Creates a new instance of event args describing a change of selection - /// in - /// - /// - /// - /// - public SelectionChangedEventArgs (TreeView tree, T oldValue, T newValue) - { - Tree = tree; - OldValue = oldValue; - NewValue = newValue; - } - } - - static class ReadOnlyCollectionExtensions { - - public static int IndexOf (this IReadOnlyCollection self, Func predicate) - { - int i = 0; - foreach (T element in self) { - if (predicate(element)) - return i; - i++; - } - return -1; - } - public static int IndexOf (this IReadOnlyCollection self, T toFind) - { - int i = 0; - foreach (T element in self) { - if (Equals(element,toFind)) - return i; - i++; - } - return -1; - } - } } \ No newline at end of file diff --git a/UICatalog/Scenarios/ClassExplorer.cs b/UICatalog/Scenarios/ClassExplorer.cs index 9bc22232d..d5bc118ab 100644 --- a/UICatalog/Scenarios/ClassExplorer.cs +++ b/UICatalog/Scenarios/ClassExplorer.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; using Terminal.Gui; +using Terminal.Gui.Trees; namespace UICatalog.Scenarios { diff --git a/UICatalog/Scenarios/CsvEditor.cs b/UICatalog/Scenarios/CsvEditor.cs index 70eded9d1..0ed3051ff 100644 --- a/UICatalog/Scenarios/CsvEditor.cs +++ b/UICatalog/Scenarios/CsvEditor.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data; using Terminal.Gui; +using Terminal.Gui.Trees; using System.Linq; using System.Globalization; using System.IO; diff --git a/UICatalog/Scenarios/InteractiveTree.cs b/UICatalog/Scenarios/InteractiveTree.cs index 496cf9a6a..b01bc0399 100644 --- a/UICatalog/Scenarios/InteractiveTree.cs +++ b/UICatalog/Scenarios/InteractiveTree.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Terminal.Gui; +using Terminal.Gui.Trees; using static UICatalog.Scenario; namespace UICatalog.Scenarios { diff --git a/UICatalog/Scenarios/TreeUseCases.cs b/UICatalog/Scenarios/TreeUseCases.cs index 5a4cb0af4..a2b616d80 100644 --- a/UICatalog/Scenarios/TreeUseCases.cs +++ b/UICatalog/Scenarios/TreeUseCases.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Terminal.Gui; +using Terminal.Gui.Trees; namespace UICatalog.Scenarios { [ScenarioMetadata (Name: "Tree View", Description: "Simple tree view examples")] diff --git a/UICatalog/Scenarios/TreeViewFileSystem.cs b/UICatalog/Scenarios/TreeViewFileSystem.cs index 7119931b1..e6ba59323 100644 --- a/UICatalog/Scenarios/TreeViewFileSystem.cs +++ b/UICatalog/Scenarios/TreeViewFileSystem.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Terminal.Gui; +using Terminal.Gui.Trees; namespace UICatalog.Scenarios { [ScenarioMetadata (Name: "TreeViewFileSystem", Description: "Hierarchical file system explorer based on TreeView")] diff --git a/UnitTests/TreeViewTests.cs b/UnitTests/TreeViewTests.cs index 99360fa57..47a219f25 100644 --- a/UnitTests/TreeViewTests.cs +++ b/UnitTests/TreeViewTests.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Terminal.Gui; +using Terminal.Gui.Trees; using Xunit; namespace Terminal.Gui.Views {