浏览代码

Extracted TreeView classes (builders, delegates, event args etc) into seperate files in namespace Terminal.Gui.Trees (#1246)

Thomas Nind 4 年之前
父节点
当前提交
444a01ae46

+ 29 - 0
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<T> (this IReadOnlyCollection<T> self, Func<T, bool> predicate)
+		{
+			int i = 0;
+			foreach (T element in self) {
+				if (predicate (element))
+					return i;
+				i++;
+			}
+			return -1;
+		}
+		public static int IndexOf<T> (this IReadOnlyCollection<T> self, T toFind)
+		{
+			int i = 0;
+			foreach (T element in self) {
+				if (Equals (element, toFind))
+					return i;
+				i++;
+			}
+			return -1;
+		}
+	}
+}

+ 11 - 0
Terminal.Gui/Core/Trees/AspectGetterDelegate.cs

@@ -0,0 +1,11 @@
+
+namespace Terminal.Gui.Trees {
+
+	/// <summary>
+	/// Delegates of this type are used to fetch string representations of user's model objects
+	/// </summary>
+	/// <param name="toRender">The object that is being rendered</param>
+	/// <returns></returns>
+	public delegate string AspectGetterDelegate<T> (T toRender) where T : class;
+
+}

+ 428 - 0
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<T> where T : class {
+		/// <summary>
+		/// True if the branch is expanded to reveal child branches
+		/// </summary>
+		public bool IsExpanded { get; set; }
+
+		/// <summary>
+		/// The users object that is being displayed by this branch of the tree
+		/// </summary>
+		public T Model { get; private set; }
+
+		/// <summary>
+		/// The depth of the current branch.  Depth of 0 indicates root level branches
+		/// </summary>
+		public int Depth { get; private set; } = 0;
+
+		/// <summary>
+		/// The children of the current branch.  This is null until the first call to 
+		/// <see cref="FetchChildren"/> to avoid enumerating the entire underlying hierarchy
+		/// </summary>
+		public Dictionary<T, Branch<T>> ChildBranches { get; set; }
+
+		/// <summary>
+		/// The parent <see cref="Branch{T}"/> or null if it is a root.
+		/// </summary>
+		public Branch<T> Parent { get; private set; }
+
+		private TreeView<T> tree;
+
+		/// <summary>
+		/// Declares a new branch of <paramref name="tree"/> in which the users object 
+		/// <paramref name="model"/> is presented
+		/// </summary>
+		/// <param name="tree">The UI control in which the branch resides</param>
+		/// <param name="parentBranchIfAny">Pass null for root level branches, otherwise
+		/// pass the parent</param>
+		/// <param name="model">The user's object that should be displayed</param>
+		public Branch (TreeView<T> tree, Branch<T> parentBranchIfAny, T model)
+		{
+			this.tree = tree;
+			this.Model = model;
+
+			if (parentBranchIfAny != null) {
+				Depth = parentBranchIfAny.Depth + 1;
+				Parent = parentBranchIfAny;
+			}
+		}
+
+
+		/// <summary>
+		/// Fetch the children of this branch. This method populates <see cref="ChildBranches"/>
+		/// </summary>
+		public virtual void FetchChildren ()
+		{
+			if (tree.TreeBuilder == null) {
+				return;
+			}
+
+			var children = tree.TreeBuilder.GetChildren (this.Model) ?? Enumerable.Empty<T> ();
+
+			this.ChildBranches = children.ToDictionary (k => k, val => new Branch<T> (tree, this, val));
+		}
+
+		/// <summary>
+		/// Returns the width of the line including prefix and the results 
+		/// of <see cref="TreeView{T}.AspectGetter"/> (the line body).
+		/// </summary>
+		/// <returns></returns>
+		public virtual int GetWidth (ConsoleDriver driver)
+		{
+			return
+				GetLinePrefix (driver).Sum (Rune.ColumnWidth) +
+				Rune.ColumnWidth (GetExpandableSymbol (driver)) +
+				(tree.AspectGetter (Model) ?? "").Length;
+		}
+
+		/// <summary>
+		/// Renders the current <see cref="Model"/> on the specified line <paramref name="y"/>
+		/// </summary>
+		/// <param name="driver"></param>
+		/// <param name="colorScheme"></param>
+		/// <param name="y"></param>
+		/// <param name="availableWidth"></param>
+		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);
+		}
+
+		/// <summary>
+		/// Gets all characters to render prior to the current branches line.  This includes indentation
+		/// whitespace and any tree branches (if enabled)
+		/// </summary>
+		/// <param name="driver"></param>
+		/// <returns></returns>
+		private IEnumerable<Rune> 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;
+			}
+		}
+
+		/// <summary>
+		/// Returns all parents starting with the immediate parent and ending at the root
+		/// </summary>
+		/// <returns></returns>
+		private IEnumerable<Branch<T>> GetParentBranches ()
+		{
+			var cur = Parent;
+
+			while (cur != null) {
+				yield return cur;
+				cur = cur.Parent;
+			}
+		}
+
+		/// <summary>
+		/// Returns an appropriate symbol for displaying next to the string representation of 
+		/// the <see cref="Model"/> object to indicate whether it <see cref="IsExpanded"/> or
+		/// not (or it is a leaf)
+		/// </summary>
+		/// <param name="driver"></param>
+		/// <returns></returns>
+		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;
+		}
+
+		/// <summary>
+		/// Returns true if the current branch can be expanded according to 
+		/// the <see cref="TreeBuilder{T}"/> or cached children already fetched
+		/// </summary>
+		/// <returns></returns>
+		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 ();
+		}
+
+		/// <summary>
+		/// Expands the current branch if possible
+		/// </summary>
+		public void Expand ()
+		{
+			if (ChildBranches == null) {
+				FetchChildren ();
+			}
+
+			if (ChildBranches.Any ()) {
+				IsExpanded = true;
+			}
+		}
+
+		/// <summary>
+		/// Marks the branch as collapsed (<see cref="IsExpanded"/> false)
+		/// </summary>
+		public void Collapse ()
+		{
+			IsExpanded = false;
+		}
+
+		/// <summary>
+		/// Refreshes cached knowledge in this branch e.g. what children an object has
+		/// </summary>
+		/// <param name="startAtTop">True to also refresh all <see cref="Parent"/> 
+		/// branches (starting with the root)</param>
+		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<T> ();
+
+				// 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<T> (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;
+					}
+				}
+			}
+
+		}
+
+		/// <summary>
+		/// Calls <see cref="Refresh(bool)"/> on the current branch and all expanded children
+		/// </summary>
+		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;
+				}
+			}
+
+		}
+
+		/// <summary>
+		/// Returns true if this branch has parents and it is the last node of it's parents 
+		/// branches (or last root of the tree)
+		/// </summary>
+		/// <returns></returns>
+		private bool IsLast ()
+		{
+			if (Parent == null) {
+				return this == tree.roots.Values.LastOrDefault ();
+			}
+
+			return Parent.ChildBranches.Values.LastOrDefault () == this;
+		}
+
+		/// <summary>
+		/// 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
+		/// </summary>
+		/// <param name="driver"></param>
+		/// <param name="x"></param>
+		/// <returns></returns>
+		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;
+		}
+
+		/// <summary>
+		/// Expands the current branch and all children branches
+		/// </summary>
+		internal void ExpandAll ()
+		{
+			Expand ();
+
+			if (ChildBranches != null) {
+				foreach (var child in ChildBranches) {
+					child.Value.ExpandAll ();
+				}
+			}
+		}
+
+		/// <summary>
+		/// Collapses the current branch and all children branches (even though those branches are 
+		/// no longer visible they retain collapse/expansion state)
+		/// </summary>
+		internal void CollapseAll ()
+		{
+			Collapse ();
+
+			if (ChildBranches != null) {
+				foreach (var child in ChildBranches) {
+					child.Value.CollapseAll ();
+				}
+			}
+		}
+	}
+}

+ 57 - 0
Terminal.Gui/Core/Trees/DelegateTreeBuilder.cs

@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+
+namespace Terminal.Gui.Trees {
+	/// <summary>
+	/// Implementation of <see cref="ITreeBuilder{T}"/> that uses user defined functions
+	/// </summary>
+	public class DelegateTreeBuilder<T> : TreeBuilder<T> {
+		private Func<T, IEnumerable<T>> childGetter;
+		private Func<T, bool> canExpand;
+
+		/// <summary>
+		/// Constructs an implementation of <see cref="ITreeBuilder{T}"/> that calls the user 
+		/// defined method <paramref name="childGetter"/> to determine children
+		/// </summary>
+		/// <param name="childGetter"></param>
+		/// <returns></returns>
+		public DelegateTreeBuilder (Func<T, IEnumerable<T>> childGetter) : base (false)
+		{
+			this.childGetter = childGetter;
+		}
+
+		/// <summary>
+		/// Constructs an implementation of <see cref="ITreeBuilder{T}"/> that calls the user 
+		/// defined method <paramref name="childGetter"/> to determine children 
+		/// and <paramref name="canExpand"/> to determine expandability
+		/// </summary>
+		/// <param name="childGetter"></param>
+		/// <param name="canExpand"></param>
+		/// <returns></returns>
+		public DelegateTreeBuilder (Func<T, IEnumerable<T>> childGetter, Func<T, bool> canExpand) : base (true)
+		{
+			this.childGetter = childGetter;
+			this.canExpand = canExpand;
+		}
+
+		/// <summary>
+		/// Returns whether a node can be expanded based on the delegate passed during construction
+		/// </summary>
+		/// <param name="toExpand"></param>
+		/// <returns></returns>
+		public override bool CanExpand (T toExpand)
+		{
+			return canExpand?.Invoke (toExpand) ?? base.CanExpand (toExpand);
+		}
+
+		/// <summary>
+		/// Returns children using the delegate method passed during construction
+		/// </summary>
+		/// <param name="forObject"></param>
+		/// <returns></returns>
+		public override IEnumerable<T> GetChildren (T forObject)
+		{
+			return childGetter.Invoke (forObject);
+		}
+	}
+}

+ 36 - 0
Terminal.Gui/Core/Trees/ITreeBuilder.cs

@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+
+namespace Terminal.Gui.Trees {
+	/// <summary>
+	/// Interface for supplying data to a <see cref="TreeView{T}"/> on demand as root level nodes
+	/// are expanded by the user
+	/// </summary>
+	public interface ITreeBuilder<T> {
+		/// <summary>
+		/// Returns true if <see cref="CanExpand"/> is implemented by this class
+		/// </summary>
+		/// <value></value>
+		bool SupportsCanExpand { get; }
+
+		/// <summary>
+		/// Returns true/false for whether a model has children.  This method should be implemented
+		/// when <see cref="GetChildren"/> is an expensive operation otherwise 
+		/// <see cref="SupportsCanExpand"/> should return false (in which case this method will not
+		/// be called)
+		/// </summary>
+		/// <remarks>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)
+		/// </remarks>
+		/// <param name="toExpand"></param>
+		/// <returns></returns>
+		bool CanExpand (T toExpand);
+
+		/// <summary>
+		/// Returns all children of a given <paramref name="forObject"/> which should be added to the 
+		/// tree as new branches underneath it
+		/// </summary>
+		/// <param name="forObject"></param>
+		/// <returns></returns>
+		IEnumerable<T> GetChildren (T forObject);
+	}
+}

+ 32 - 0
Terminal.Gui/Core/Trees/ObjectActivatedEventArgs.cs

@@ -0,0 +1,32 @@
+namespace Terminal.Gui.Trees {
+	/// <summary>
+	/// Event args for the <see cref="TreeView{T}.ObjectActivated"/> event
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	public class ObjectActivatedEventArgs<T> where T : class {
+
+		/// <summary>
+		/// The tree in which the activation occurred
+		/// </summary>
+		/// <value></value>
+		public TreeView<T> Tree { get; }
+
+		/// <summary>
+		/// The object that was selected at the time of activation
+		/// </summary>
+		/// <value></value>
+		public T ActivatedObject { get; }
+
+
+		/// <summary>
+		/// Creates a new instance documenting activation of the <paramref name="activated"/> object
+		/// </summary>
+		/// <param name="tree">Tree in which the activation is happening</param>
+		/// <param name="activated">What object is being activated</param>
+		public ObjectActivatedEventArgs (TreeView<T> tree, T activated)
+		{
+			Tree = tree;
+			ActivatedObject = activated;
+		}
+	}
+}

+ 37 - 0
Terminal.Gui/Core/Trees/SelectionChangedEventArgs.cs

@@ -0,0 +1,37 @@
+using System;
+
+namespace Terminal.Gui.Trees {
+	/// <summary>
+	/// Event arguments describing a change in selected object in a tree view
+	/// </summary>
+	public class SelectionChangedEventArgs<T> : EventArgs where T : class {
+		/// <summary>
+		/// The view in which the change occurred
+		/// </summary>
+		public TreeView<T> Tree { get; }
+
+		/// <summary>
+		/// The previously selected value (can be null)
+		/// </summary>
+		public T OldValue { get; }
+
+		/// <summary>
+		/// The newly selected value in the <see cref="Tree"/> (can be null)
+		/// </summary>
+		public T NewValue { get; }
+
+		/// <summary>
+		/// Creates a new instance of event args describing a change of selection 
+		/// in <paramref name="tree"/>
+		/// </summary>
+		/// <param name="tree"></param>
+		/// <param name="oldValue"></param>
+		/// <param name="newValue"></param>
+		public SelectionChangedEventArgs (TreeView<T> tree, T oldValue, T newValue)
+		{
+			Tree = tree;
+			OldValue = oldValue;
+			NewValue = newValue;
+		}
+	}
+}

+ 41 - 0
Terminal.Gui/Core/Trees/TreeBuilder.cs

@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Terminal.Gui.Trees {
+
+	/// <summary>
+	/// Abstract implementation of <see cref="ITreeBuilder{T}"/>.
+	/// </summary>
+	public abstract class TreeBuilder<T> : ITreeBuilder<T> {
+
+		/// <inheritdoc/>
+		public bool SupportsCanExpand { get; protected set; } = false;
+
+		/// <summary>
+		/// Override this method to return a rapid answer as to whether <see cref="GetChildren(T)"/> 
+		/// returns results.  If you are implementing this method ensure you passed true in base 
+		/// constructor or set <see cref="SupportsCanExpand"/>
+		/// </summary>
+		/// <param name="toExpand"></param>
+		/// <returns></returns>
+		public virtual bool CanExpand (T toExpand)
+		{
+
+			return GetChildren (toExpand).Any ();
+		}
+
+		/// <inheritdoc/>
+		public abstract IEnumerable<T> GetChildren (T forObject);
+
+		/// <summary>
+		/// Constructs base and initializes <see cref="SupportsCanExpand"/>
+		/// </summary>
+		/// <param name="supportsCanExpand">Pass true if you intend to 
+		/// implement <see cref="CanExpand(T)"/> otherwise false</param>
+		public TreeBuilder (bool supportsCanExpand)
+		{
+			SupportsCanExpand = supportsCanExpand;
+		}
+	}
+}

+ 1 - 1
Terminal.Gui/Views/TreeNode.cs → Terminal.Gui/Core/Trees/TreeNode.cs

@@ -1,6 +1,6 @@
 using System.Collections.Generic;
 
-namespace Terminal.Gui {
+namespace Terminal.Gui.Trees {
 		
 	/// <summary>
 	/// Interface to implement when you want the regular (non generic) <see cref="TreeView"/>

+ 28 - 0
Terminal.Gui/Core/Trees/TreeNodeBuilder.cs

@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+
+namespace Terminal.Gui.Trees {
+	/// <summary>
+	/// <see cref="ITreeBuilder{T}"/> implementation for <see cref="ITreeNode"/> objects
+	/// </summary>
+	public class TreeNodeBuilder : TreeBuilder<ITreeNode> {
+
+		/// <summary>
+		/// Initialises a new instance of builder for any model objects of 
+		/// Type <see cref="ITreeNode"/>
+		/// </summary>
+		public TreeNodeBuilder () : base (false)
+		{
+
+		}
+
+		/// <summary>
+		/// Returns <see cref="ITreeNode.Children"/> from <paramref name="model"/>
+		/// </summary>
+		/// <param name="model"></param>
+		/// <returns></returns>
+		public override IEnumerable<ITreeNode> GetChildren (ITreeNode model)
+		{
+			return model.Children;
+		}
+	}
+}

+ 1 - 1
Terminal.Gui/Views/TreeStyle.cs → Terminal.Gui/Core/Trees/TreeStyle.cs

@@ -1,6 +1,6 @@
 using System;
 
-namespace Terminal.Gui {
+namespace Terminal.Gui.Trees {
 	/// <summary>
 	/// Defines rendering options that affect how the tree is displayed
 	/// </summary>

+ 0 - 155
Terminal.Gui/Views/TreeBuilder.cs

@@ -1,155 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace Terminal.Gui {
-
-	/// <summary>
-	/// Interface for supplying data to a <see cref="TreeView{T}"/> on demand as root level nodes
-	/// are expanded by the user
-	/// </summary>
-	public interface ITreeBuilder<T> {
-		/// <summary>
-		/// Returns true if <see cref="CanExpand"/> is implemented by this class
-		/// </summary>
-		/// <value></value>
-		bool SupportsCanExpand { get; }
-
-		/// <summary>
-		/// Returns true/false for whether a model has children.  This method should be implemented
-		/// when <see cref="GetChildren"/> is an expensive operation otherwise 
-		/// <see cref="SupportsCanExpand"/> should return false (in which case this method will not
-		/// be called)
-		/// </summary>
-		/// <remarks>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)
-		/// </remarks>
-		/// <param name="toExpand"></param>
-		/// <returns></returns>
-		bool CanExpand (T toExpand);
-
-		/// <summary>
-		/// Returns all children of a given <paramref name="forObject"/> which should be added to the 
-		/// tree as new branches underneath it
-		/// </summary>
-		/// <param name="forObject"></param>
-		/// <returns></returns>
-		IEnumerable<T> GetChildren (T forObject);
-	}
-
-	/// <summary>
-	/// Abstract implementation of <see cref="ITreeBuilder{T}"/>.
-	/// </summary>
-	public abstract class TreeBuilder<T> : ITreeBuilder<T> {
-
-		/// <inheritdoc/>
-		public bool SupportsCanExpand { get; protected set; } = false;
-
-		/// <summary>
-		/// Override this method to return a rapid answer as to whether <see cref="GetChildren(T)"/> 
-		/// returns results.  If you are implementing this method ensure you passed true in base 
-		/// constructor or set <see cref="SupportsCanExpand"/>
-		/// </summary>
-		/// <param name="toExpand"></param>
-		/// <returns></returns>
-		public virtual bool CanExpand (T toExpand)
-		{
-
-			return GetChildren (toExpand).Any ();
-		}
-
-		/// <inheritdoc/>
-		public abstract IEnumerable<T> GetChildren (T forObject);
-
-		/// <summary>
-		/// Constructs base and initializes <see cref="SupportsCanExpand"/>
-		/// </summary>
-		/// <param name="supportsCanExpand">Pass true if you intend to 
-		/// implement <see cref="CanExpand(T)"/> otherwise false</param>
-		public TreeBuilder (bool supportsCanExpand)
-		{
-			SupportsCanExpand = supportsCanExpand;
-		}
-	}
-
-	
-
-	/// <summary>
-	/// <see cref="ITreeBuilder{T}"/> implementation for <see cref="ITreeNode"/> objects
-	/// </summary>
-	public class TreeNodeBuilder : TreeBuilder<ITreeNode> {
-
-		/// <summary>
-		/// Initialises a new instance of builder for any model objects of 
-		/// Type <see cref="ITreeNode"/>
-		/// </summary>
-		public TreeNodeBuilder () : base (false)
-		{
-
-		}
-
-		/// <summary>
-		/// Returns <see cref="ITreeNode.Children"/> from <paramref name="model"/>
-		/// </summary>
-		/// <param name="model"></param>
-		/// <returns></returns>
-		public override IEnumerable<ITreeNode> GetChildren (ITreeNode model)
-		{
-			return model.Children;
-		}
-	}
-
-	
-	/// <summary>
-	/// Implementation of <see cref="ITreeBuilder{T}"/> that uses user defined functions
-	/// </summary>
-	public class DelegateTreeBuilder<T> : TreeBuilder<T> {
-		private Func<T, IEnumerable<T>> childGetter;
-		private Func<T, bool> canExpand;
-
-		/// <summary>
-		/// Constructs an implementation of <see cref="ITreeBuilder{T}"/> that calls the user 
-		/// defined method <paramref name="childGetter"/> to determine children
-		/// </summary>
-		/// <param name="childGetter"></param>
-		/// <returns></returns>
-		public DelegateTreeBuilder (Func<T, IEnumerable<T>> childGetter) : base (false)
-		{
-			this.childGetter = childGetter;
-		}
-
-		/// <summary>
-		/// Constructs an implementation of <see cref="ITreeBuilder{T}"/> that calls the user 
-		/// defined method <paramref name="childGetter"/> to determine children 
-		/// and <paramref name="canExpand"/> to determine expandability
-		/// </summary>
-		/// <param name="childGetter"></param>
-		/// <param name="canExpand"></param>
-		/// <returns></returns>
-		public DelegateTreeBuilder (Func<T, IEnumerable<T>> childGetter, Func<T, bool> canExpand) : base (true)
-		{
-			this.childGetter = childGetter;
-			this.canExpand = canExpand;
-		}
-
-		/// <summary>
-		/// Returns whether a node can be expanded based on the delegate passed during construction
-		/// </summary>
-		/// <param name="toExpand"></param>
-		/// <returns></returns>
-		public override bool CanExpand (T toExpand)
-		{
-			return canExpand?.Invoke (toExpand) ?? base.CanExpand (toExpand);
-		}
-
-		/// <summary>
-		/// Returns children using the delegate method passed during construction
-		/// </summary>
-		/// <param name="forObject"></param>
-		/// <returns></returns>
-		public override IEnumerable<T> GetChildren (T forObject)
-		{
-			return childGetter.Invoke (forObject);
-		}
-	}
-}

+ 1 - 518
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 {
 		}
 	}
 
-	/// <summary>
-	/// Event args for the <see cref="TreeView{T}.ObjectActivated"/> event
-	/// </summary>
-	/// <typeparam name="T"></typeparam>
-	public class ObjectActivatedEventArgs<T> where T : class {
-
-		/// <summary>
-		/// The tree in which the activation occurred
-		/// </summary>
-		/// <value></value>
-		public TreeView<T> Tree { get; }
-
-		/// <summary>
-		/// The object that was selected at the time of activation
-		/// </summary>
-		/// <value></value>
-		public T ActivatedObject { get; }
-
-
-		/// <summary>
-		/// Creates a new instance documenting activation of the <paramref name="activated"/> object
-		/// </summary>
-		/// <param name="tree">Tree in which the activation is happening</param>
-		/// <param name="activated">What object is being activated</param>
-		public ObjectActivatedEventArgs (TreeView<T> tree, T activated)
-		{
-			Tree = tree;
-			ActivatedObject = activated;
-		}
-	}
-
 	class TreeSelection<T> where T : class {
 
 		public Branch<T> Origin { get; }
@@ -1281,491 +1251,4 @@ namespace Terminal.Gui {
 		}
 	}
 
-	class Branch<T> where T : class {
-		/// <summary>
-		/// True if the branch is expanded to reveal child branches
-		/// </summary>
-		public bool IsExpanded { get; set; }
-
-		/// <summary>
-		/// The users object that is being displayed by this branch of the tree
-		/// </summary>
-		public T Model { get; private set; }
-
-		/// <summary>
-		/// The depth of the current branch.  Depth of 0 indicates root level branches
-		/// </summary>
-		public int Depth { get; private set; } = 0;
-
-		/// <summary>
-		/// The children of the current branch.  This is null until the first call to 
-		/// <see cref="FetchChildren"/> to avoid enumerating the entire underlying hierarchy
-		/// </summary>
-		public Dictionary<T, Branch<T>> ChildBranches { get; set; }
-
-		/// <summary>
-		/// The parent <see cref="Branch{T}"/> or null if it is a root.
-		/// </summary>
-		public Branch<T> Parent { get; private set; }
-
-		private TreeView<T> tree;
-
-		/// <summary>
-		/// Declares a new branch of <paramref name="tree"/> in which the users object 
-		/// <paramref name="model"/> is presented
-		/// </summary>
-		/// <param name="tree">The UI control in which the branch resides</param>
-		/// <param name="parentBranchIfAny">Pass null for root level branches, otherwise
-		/// pass the parent</param>
-		/// <param name="model">The user's object that should be displayed</param>
-		public Branch (TreeView<T> tree, Branch<T> parentBranchIfAny, T model)
-		{
-			this.tree = tree;
-			this.Model = model;
-
-			if (parentBranchIfAny != null) {
-				Depth = parentBranchIfAny.Depth + 1;
-				Parent = parentBranchIfAny;
-			}
-		}
-
-
-		/// <summary>
-		/// Fetch the children of this branch. This method populates <see cref="ChildBranches"/>
-		/// </summary>
-		public virtual void FetchChildren ()
-		{
-			if (tree.TreeBuilder == null) {
-				return;
-			}
-
-			var children = tree.TreeBuilder.GetChildren (this.Model) ?? Enumerable.Empty<T> ();
-
-			this.ChildBranches = children.ToDictionary (k => k, val => new Branch<T> (tree, this, val));
-		}
-
-		/// <summary>
-		/// Returns the width of the line including prefix and the results 
-		/// of <see cref="TreeView{T}.AspectGetter"/> (the line body).
-		/// </summary>
-		/// <returns></returns>
-		public virtual int GetWidth (ConsoleDriver driver)
-		{
-			return
-				GetLinePrefix (driver).Sum (Rune.ColumnWidth) +
-				Rune.ColumnWidth (GetExpandableSymbol (driver)) +
-				(tree.AspectGetter (Model) ?? "").Length;
-		}
-
-		/// <summary>
-		/// Renders the current <see cref="Model"/> on the specified line <paramref name="y"/>
-		/// </summary>
-		/// <param name="driver"></param>
-		/// <param name="colorScheme"></param>
-		/// <param name="y"></param>
-		/// <param name="availableWidth"></param>
-		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);
-		}
-
-		/// <summary>
-		/// Gets all characters to render prior to the current branches line.  This includes indentation
-		/// whitespace and any tree branches (if enabled)
-		/// </summary>
-		/// <param name="driver"></param>
-		/// <returns></returns>
-		private IEnumerable<Rune> 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;
-			}
-		}
-
-		/// <summary>
-		/// Returns all parents starting with the immediate parent and ending at the root
-		/// </summary>
-		/// <returns></returns>
-		private IEnumerable<Branch<T>> GetParentBranches ()
-		{
-			var cur = Parent;
-
-			while (cur != null) {
-				yield return cur;
-				cur = cur.Parent;
-			}
-		}
-
-		/// <summary>
-		/// Returns an appropriate symbol for displaying next to the string representation of 
-		/// the <see cref="Model"/> object to indicate whether it <see cref="IsExpanded"/> or
-		/// not (or it is a leaf)
-		/// </summary>
-		/// <param name="driver"></param>
-		/// <returns></returns>
-		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;
-		}
-
-		/// <summary>
-		/// Returns true if the current branch can be expanded according to 
-		/// the <see cref="TreeBuilder{T}"/> or cached children already fetched
-		/// </summary>
-		/// <returns></returns>
-		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 ();
-		}
-
-		/// <summary>
-		/// Expands the current branch if possible
-		/// </summary>
-		public void Expand ()
-		{
-			if (ChildBranches == null) {
-				FetchChildren ();
-			}
-
-			if (ChildBranches.Any ()) {
-				IsExpanded = true;
-			}
-		}
-
-		/// <summary>
-		/// Marks the branch as collapsed (<see cref="IsExpanded"/> false)
-		/// </summary>
-		public void Collapse ()
-		{
-			IsExpanded = false;
-		}
-
-		/// <summary>
-		/// Refreshes cached knowledge in this branch e.g. what children an object has
-		/// </summary>
-		/// <param name="startAtTop">True to also refresh all <see cref="Parent"/> 
-		/// branches (starting with the root)</param>
-		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<T> ();
-
-				// 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<T> (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;
-					}
-				}
-			}
-
-		}
-
-		/// <summary>
-		/// Calls <see cref="Refresh(bool)"/> on the current branch and all expanded children
-		/// </summary>
-		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;
-				}
-			}
-
-		}
-
-		/// <summary>
-		/// Returns true if this branch has parents and it is the last node of it's parents 
-		/// branches (or last root of the tree)
-		/// </summary>
-		/// <returns></returns>
-		private bool IsLast ()
-		{
-			if (Parent == null) {
-				return this == tree.roots.Values.LastOrDefault ();
-			}
-
-			return Parent.ChildBranches.Values.LastOrDefault () == this;
-		}
-
-		/// <summary>
-		/// 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
-		/// </summary>
-		/// <param name="driver"></param>
-		/// <param name="x"></param>
-		/// <returns></returns>
-		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;
-		}
-
-		/// <summary>
-		/// Expands the current branch and all children branches
-		/// </summary>
-		internal void ExpandAll ()
-		{
-			Expand ();
-
-			if (ChildBranches != null) {
-				foreach (var child in ChildBranches) {
-					child.Value.ExpandAll ();
-				}
-			}
-		}
-
-		/// <summary>
-		/// Collapses the current branch and all children branches (even though those branches are 
-		/// no longer visible they retain collapse/expansion state)
-		/// </summary>
-		internal void CollapseAll ()
-		{
-			Collapse ();
-
-			if (ChildBranches != null) {
-				foreach (var child in ChildBranches) {
-					child.Value.CollapseAll ();
-				}
-			}
-		}
-	}
-
-	/// <summary>
-	/// Delegates of this type are used to fetch string representations of user's model objects
-	/// </summary>
-	/// <param name="toRender">The object that is being rendered</param>
-	/// <returns></returns>
-	public delegate string AspectGetterDelegate<T> (T toRender) where T : class;
-
-	/// <summary>
-	/// Event arguments describing a change in selected object in a tree view
-	/// </summary>
-	public class SelectionChangedEventArgs<T> : EventArgs where T : class {
-		/// <summary>
-		/// The view in which the change occurred
-		/// </summary>
-		public TreeView<T> Tree { get; }
-
-		/// <summary>
-		/// The previously selected value (can be null)
-		/// </summary>
-		public T OldValue { get; }
-
-		/// <summary>
-		/// The newly selected value in the <see cref="Tree"/> (can be null)
-		/// </summary>
-		public T NewValue { get; }
-
-		/// <summary>
-		/// Creates a new instance of event args describing a change of selection 
-		/// in <paramref name="tree"/>
-		/// </summary>
-		/// <param name="tree"></param>
-		/// <param name="oldValue"></param>
-		/// <param name="newValue"></param>
-		public SelectionChangedEventArgs (TreeView<T> tree, T oldValue, T newValue)
-		{
-			Tree = tree;
-			OldValue = oldValue;
-			NewValue = newValue;
-		}
-	}
-
-	static class ReadOnlyCollectionExtensions {
-		
-		public static int IndexOf<T> (this IReadOnlyCollection<T> self, Func<T,bool> predicate)
-		{
-			int i = 0;
-			foreach (T element in self) {
-				if (predicate(element))
-					return i;
-				i++;
-			}
-			return -1;
-		}
-		public static int IndexOf<T> (this IReadOnlyCollection<T> self, T toFind)
-		{
-			int i = 0;
-			foreach (T element in self) {
-				if (Equals(element,toFind))
-					return i;
-				i++;
-			}
-			return -1;
-		}
-	}
 }

+ 1 - 0
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 {
 

+ 1 - 0
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;

+ 1 - 0
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 {

+ 1 - 0
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")]

+ 1 - 0
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")]

+ 1 - 0
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 {