Browse Source

added ShowLines and TreeNode support

tznind 4 years ago
parent
commit
6af7cd85d1
2 changed files with 265 additions and 19 deletions
  1. 170 18
      Terminal.Gui/Views/TreeView.cs
  2. 95 1
      UICatalog/Scenarios/TreeViewFileSystem.cs

+ 170 - 18
Terminal.Gui/Views/TreeView.cs

@@ -6,15 +6,77 @@ using System.Linq;
 
 
 namespace Terminal.Gui {
 namespace Terminal.Gui {
 
 
+	/// <summary>
+	/// Interface to implement when you want <see cref="TreeView"/> to automatically determine children for your class
+	/// </summary>
+	public interface ITreeNode
+	{
+		/// <summary>
+		/// The children of your class which should be rendered underneath it when expanded
+		/// </summary>
+		/// <value></value>
+		IList<ITreeNode> Children {get;}
+
+		/// <summary>
+		/// The textual representation to be rendered when your class is visible in the tree
+		/// </summary>
+		/// <value></value>
+		string Text {get;}
+	}
+
+	/// <summary>
+	/// Simple class for representing nodes of a <see cref="TreeView"/>
+	/// </summary>
+	public class TreeNode : ITreeNode
+	{
+		/// <summary>
+		/// Children of the current node
+		/// </summary>
+		/// <returns></returns>
+		public IList<ITreeNode> Children {get;set;} = new List<ITreeNode>();
+		
+		/// <summary>
+		/// Text to display in tree node for current entry
+		/// </summary>
+		/// <value></value>
+		public string Text {get;set;}
+
+		/// <summary>
+		/// returns <see cref="Text"/>
+		/// </summary>
+		/// <returns></returns>
+		public override string ToString()
+		{
+			return Text;
+		}
+
+		/// <summary>
+		/// Initialises a new instance with no <see cref="Text"/>
+		/// </summary>
+		public TreeNode()
+		{
+			
+		}
+		/// <summary>
+		/// Initialises a new instance and sets starting <see cref="Text"/>
+		/// </summary>
+		public TreeNode(string text)
+		{
+			Text = text;
+		}
+
+
+	}
+
 	/// <summary>
 	/// <summary>
 	/// Hierarchical tree view with expandable branches.  Branch objects are dynamically determined when expanded using a user defined <see cref="ChildrenGetterDelegate"/>
 	/// Hierarchical tree view with expandable branches.  Branch objects are dynamically determined when expanded using a user defined <see cref="ChildrenGetterDelegate"/>
 	/// </summary>
 	/// </summary>
 	public class TreeView : View
 	public class TreeView : View
 	{   
 	{   
 		/// <summary>
 		/// <summary>
-		/// Default implementation of a <see cref="ChildrenGetterDelegate"/>, returns an empty collection (i.e. no children)
+		/// Default implementation of a <see cref="ChildrenGetterDelegate"/>.  Supports returning children of <see cref="ITreeNode"/> or otherwise returns an empty collection (i.e. no children)
 		/// </summary>
 		/// </summary>
-		static ChildrenGetterDelegate DefaultChildrenGetter = (s)=>{return new object[0];};
+		static ChildrenGetterDelegate DefaultChildrenGetter = (s)=>{return s is ITreeNode n ? n.Children : Enumerable.Empty<object>();};
 
 
 		/// <summary>
 		/// <summary>
 		/// This is the delegate that will be used to fetch the children of a model object
 		/// This is the delegate that will be used to fetch the children of a model object
@@ -28,6 +90,12 @@ namespace Terminal.Gui {
 		private CanExpandGetterDelegate canExpandGetter;
 		private CanExpandGetterDelegate canExpandGetter;
 		private int scrollOffset;
 		private int scrollOffset;
 
 
+		/// <summary>
+		/// True to render vertical lines under expanded nodes to show which node belongs to which parent.  False to use only whitespace
+		/// </summary>
+		/// <value></value>
+		public bool ShowBranchLines {get;set;} = true;
+
 		/// <summary>
 		/// <summary>
 		/// Optional delegate where <see cref="ChildrenGetter"/> is expensive.  This should quickly return true/false for whether an object is expandable.  (e.g. indicating to a user that all folders can be expanded because they are folders without having to calculate contents)
 		/// Optional delegate where <see cref="ChildrenGetter"/> is expensive.  This should quickly return true/false for whether an object is expandable.  (e.g. indicating to a user that all folders can be expanded because they are folders without having to calculate contents)
 		/// </summary>
 		/// </summary>
@@ -96,7 +164,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// <summary>
 		/// Map of root objects to the branches under them.  All objects have a <see cref="Branch"/> even if that branch has no children
 		/// Map of root objects to the branches under them.  All objects have a <see cref="Branch"/> even if that branch has no children
 		/// </summary>
 		/// </summary>
-		Dictionary<object,Branch> roots {get; set;} = new Dictionary<object, Branch>();
+		internal Dictionary<object,Branch> roots {get; set;} = new Dictionary<object, Branch>();
 
 
 		/// <summary>
 		/// <summary>
 		/// The amount of tree view that has been scrolled off the top of the screen (by the user scrolling down)
 		/// The amount of tree view that has been scrolled off the top of the screen (by the user scrolling down)
@@ -284,17 +352,12 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// <summary>
 		/// Symbol to use for expanded branch nodes to indicate to the user that they can be collapsed.  Defaults to '-'
 		/// Symbol to use for expanded branch nodes to indicate to the user that they can be collapsed.  Defaults to '-'
 		/// </summary>
 		/// </summary>
-		public char ExpandedSymbol {get;set;} = '-';
+		public Rune ExpandedSymbol {get;set;} = '-';
 
 
 		/// <summary>
 		/// <summary>
 		/// Symbol to use for branch nodes that can be expanded to indicate this to the user.  Defaults to '+'
 		/// Symbol to use for branch nodes that can be expanded to indicate this to the user.  Defaults to '+'
 		/// </summary>
 		/// </summary>
-		public char ExpandableSymbol {get;set;} = '+';
-
-		/// <summary>
-		/// Symbol to use for branch nodes that cannot be expanded (as they have no children).  Defaults to space ' '
-		/// </summary>
-		public char LeafSymbol {get;set;} = ' ';
+		public Rune ExpandableSymbol {get;set;} = '+';
 
 
 		/// <inheritdoc/>
 		/// <inheritdoc/>
 		public override bool ProcessKey (KeyEvent keyEvent)
 		public override bool ProcessKey (KeyEvent keyEvent)
@@ -304,7 +367,7 @@ namespace Terminal.Gui {
 					Expand(SelectedObject);
 					Expand(SelectedObject);
 				break;
 				break;
 				case Key.CursorLeft:
 				case Key.CursorLeft:
-					Collapse(SelectedObject);
+					CursorLeft();
 				break;
 				break;
 			
 			
 				case Key.CursorUp:
 				case Key.CursorUp:
@@ -336,6 +399,23 @@ namespace Terminal.Gui {
 			return true;
 			return true;
 		}
 		}
 
 
+		/// <summary>
+		/// Determines systems behaviour when the left arrow key is pressed.  Default behaviour is to collapse the current tree node if possible otherwise changes selection to current branches parent
+		/// </summary>
+		protected virtual void CursorLeft()
+		{
+			if(IsExpanded(SelectedObject))
+				Collapse(SelectedObject);
+			else
+			{
+				var parent = GetParent(SelectedObject);
+				if(parent != null){
+					SelectedObject = parent;
+					SetNeedsDisplay();
+				}
+			}
+		}
+
 		/// <summary>
 		/// <summary>
 		/// Changes the <see cref="SelectedObject"/> to the first root object and resets the <see cref="ScrollOffset"/> to 0
 		/// Changes the <see cref="SelectedObject"/> to the first root object and resets the <see cref="ScrollOffset"/> to 0
 		/// </summary>
 		/// </summary>
@@ -476,7 +556,7 @@ namespace Terminal.Gui {
 		public Branch Parent {get; private set;}
 		public Branch Parent {get; private set;}
 
 
 		private TreeView tree;
 		private TreeView tree;
-
+		
 		/// <summary>
 		/// <summary>
 		/// Declares a new branch of <paramref name="tree"/> in which the users object <paramref name="model"/> is presented
 		/// Declares a new branch of <paramref name="tree"/> in which the users object <paramref name="model"/> is presented
 		/// </summary>
 		/// </summary>
@@ -517,31 +597,91 @@ namespace Terminal.Gui {
 		/// <param name="availableWidth"></param>
 		/// <param name="availableWidth"></param>
 		public virtual void Draw(ConsoleDriver driver,ColorScheme colorScheme, int y, int availableWidth)
 		public virtual void Draw(ConsoleDriver driver,ColorScheme colorScheme, int y, int availableWidth)
 		{
 		{
-			string representation = new string(' ',Depth) + GetExpandableIcon() + tree.AspectGetter(Model);
-            
+
+			// Everything on line before the expansion run and branch text
+			string prefix = GetLinePrefix(driver);
+			var expansion = GetExpandableIcon(driver);
+			string lineBody = tree.AspectGetter(Model);
+
+			var remainingWidth = availableWidth - (prefix.Length + 1 + lineBody.Length);
+			            
 			tree.Move(0,y);
 			tree.Move(0,y);
 
 
+			driver.SetAttribute(colorScheme.Normal);
+
+			driver.AddStr(prefix + expansion);
+
 			driver.SetAttribute(tree.SelectedObject == Model ?
 			driver.SetAttribute(tree.SelectedObject == Model ?
 				colorScheme.HotFocus :
 				colorScheme.HotFocus :
 				colorScheme.Normal);
 				colorScheme.Normal);
 
 
-			driver.AddStr(representation.PadRight(availableWidth));
+			driver.AddStr(lineBody);
+
+			driver.SetAttribute(colorScheme.Normal);
+
+			if(remainingWidth > 0)
+				driver.AddStr(new string(' ',remainingWidth));
+		}
+
+		/// <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 string GetLinePrefix (ConsoleDriver driver)
+		{
+			// If not showing line branches or this is a root object
+			if(!tree.ShowBranchLines || Parent == null)
+				return new string(' ',Depth);
+
+			string prefix = "";
+
+			foreach(var cur in GetParentBranches().Reverse())
+			{
+				if(cur.IsLast())
+					prefix += " ";
+				else
+					prefix += driver.VLine;
+			}
+
+			if(IsLast())
+				return prefix + driver.LLCorner;
+			else
+				return prefix + driver.LeftTee;
+		}
+
+		/// <summary>
+		/// Returns all parents starting with the immediate parent and ending at the root
+		/// </summary>
+		/// <returns></returns>
+		private IEnumerable<Branch> GetParentBranches()
+		{
+			var cur = Parent;
+
+			while(cur != null)
+			{
+				yield return cur;
+				cur = cur.Parent;
+			}
 		}
 		}
 
 
 		/// <summary>
 		/// <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)
 		/// 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>
 		/// </summary>
+		/// <param name="driver"></param>
 		/// <returns></returns>
 		/// <returns></returns>
-		public char GetExpandableIcon()
+		public Rune GetExpandableIcon(ConsoleDriver driver)
 		{
 		{
 			if(IsExpanded)
 			if(IsExpanded)
 				return tree.ExpandedSymbol;
 				return tree.ExpandedSymbol;
 
 
+			var leafSymbol = tree.ShowBranchLines ? driver.HLine : ' ';
+
 			if(ChildBranches == null) {
 			if(ChildBranches == null) {
 			
 			
 				//if there is a rapid method for determining whether there are children
 				//if there is a rapid method for determining whether there are children
 				if(tree.CanExpandGetter != null) {
 				if(tree.CanExpandGetter != null) {
-					return tree.CanExpandGetter(Model) ? tree.ExpandableSymbol : tree.LeafSymbol;
+					return tree.CanExpandGetter(Model) ? tree.ExpandableSymbol : leafSymbol;
 				}
 				}
 				
 				
 				//there is no way of knowing whether we can expand without fetching the children
 				//there is no way of knowing whether we can expand without fetching the children
@@ -549,7 +689,7 @@ namespace Terminal.Gui {
 			}
 			}
 
 
 			//we fetched or already know the children, so return whether we are a leaf or a expandable branch
 			//we fetched or already know the children, so return whether we are a leaf or a expandable branch
-			return ChildBranches.Any() ? tree.ExpandableSymbol : tree.LeafSymbol;
+			return ChildBranches.Any() ? tree.ExpandableSymbol : leafSymbol;
 		}
 		}
 
 
 		/// <summary>
 		/// <summary>
@@ -643,6 +783,18 @@ namespace Terminal.Gui {
 			}
 			}
 				
 				
 		}
 		}
+
+		/// <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>
 	/// <summary>

+ 95 - 1
UICatalog/Scenarios/TreeViewFileSystem.cs

@@ -27,6 +27,9 @@ namespace UICatalog.Scenarios {
 				new MenuBarItem ("_File", new MenuItem [] {
 				new MenuBarItem ("_File", new MenuItem [] {
 					new MenuItem ("_Quit", "", () => Quit()),
 					new MenuItem ("_Quit", "", () => Quit()),
 				}),
 				}),
+				new MenuBarItem ("_View", new MenuItem [] {
+					new MenuItem ("_ShowLines", "", () => ShowLines()),
+				}),
 			});
 			});
 			Top.Add (menu);
 			Top.Add (menu);
 
 
@@ -34,6 +37,7 @@ namespace UICatalog.Scenarios {
 				new StatusItem(Key.F2, "~F2~ Add Root Drives", () => AddRootDrives()),
 				new StatusItem(Key.F2, "~F2~ Add Root Drives", () => AddRootDrives()),
 				new StatusItem(Key.F3, "~F3~ Remove Root Object", () => RemoveRoot()),
 				new StatusItem(Key.F3, "~F3~ Remove Root Object", () => RemoveRoot()),
 				new StatusItem(Key.F4, "~F4~ Clear Objects", () => ClearObjects()),
 				new StatusItem(Key.F4, "~F4~ Clear Objects", () => ClearObjects()),
+				new StatusItem(Key.F5, "~F5~ Simple Tree", () => AddSimpleTree()),
 				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
 				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
 			});
 			});
 			Top.Add (statusBar);
 			Top.Add (statusBar);
@@ -54,6 +58,22 @@ namespace UICatalog.Scenarios {
 				return;
 				return;
 			}
 			}
 
 
+
+			Win.Add (_treeView);
+		}
+
+		private void ShowLines ()
+		{
+			_treeView.ShowBranchLines = !_treeView.ShowBranchLines;
+			_treeView.SetNeedsDisplay();
+		}
+
+		/// <summary>
+		/// Sets up children getter delegates that return subfolders/files from directories
+		/// </summary>
+		private void SetupFileSystemDelegates ()
+		{
+			
 			// As a shortcut to enumerating half the file system, tell tree that all directories are expandable (even if they turn out to be empty later on)
 			// As a shortcut to enumerating half the file system, tell tree that all directories are expandable (even if they turn out to be empty later on)
 			_treeView.CanExpandGetter = (o)=>o is DirectoryInfo;
 			_treeView.CanExpandGetter = (o)=>o is DirectoryInfo;
 
 
@@ -62,8 +82,80 @@ namespace UICatalog.Scenarios {
 
 
 			// Determines how to represent objects as strings on the screen
 			// Determines how to represent objects as strings on the screen
 			_treeView.AspectGetter = AspectGetter;
 			_treeView.AspectGetter = AspectGetter;
+		}
 
 
-			Win.Add (_treeView);
+		private void AddSimpleTree ()
+		{
+			ClearObjects();
+		
+			// Clear any previous delegates
+			_treeView.CanExpandGetter = null;
+			_treeView.ChildrenGetter = null;
+
+			// Add 2 root nodes with simple set of subfolders
+			_treeView.AddObject(CreateSimpleRoot());
+			_treeView.AddObject(CreateSimpleRoot());
+		}
+
+		private ITreeNode CreateSimpleRoot ()
+		{
+			
+			return new TreeNode("Root"){
+				Children = new List<ITreeNode>()
+				{
+					new TreeNode("Folder_1"){
+					Children = new List<ITreeNode>()
+					{
+						new TreeNode("Folder_1.1"){
+							Children = new List<ITreeNode>()
+							{
+								new TreeNode("File_1.1.1"),
+								new TreeNode("File_1.1.2")
+							}},
+						new TreeNode("Folder_1.2"){
+							Children = new List<ITreeNode>()
+							{
+								new TreeNode("File_1.2.1"),
+								new TreeNode("File_1.2.2")
+							}},
+						new TreeNode("File_1.1")
+					}},
+					new TreeNode("Folder_2"){
+					Children = new List<ITreeNode>()
+					{
+						new TreeNode("Folder_2.1"){
+							Children = new List<ITreeNode>()
+							{
+								new TreeNode("File_2.1.1"),
+								new TreeNode("File_2.1.2")
+							}},
+						new TreeNode("Folder_2.2"){
+							Children = new List<ITreeNode>()
+							{
+								new TreeNode("File_2.2.1"),
+								new TreeNode("File_2.2.2")
+							}},
+						new TreeNode("File_2.1")
+					}},
+					new TreeNode("Folder_3"){
+					Children = new List<ITreeNode>()
+					{
+						new TreeNode("Folder_3.1"){
+							Children = new List<ITreeNode>()
+							{
+								new TreeNode("File_3.1.1"),
+								new TreeNode("File_3.1.2")
+							}},
+						new TreeNode("Folder_3.2"){
+							Children = new List<ITreeNode>()
+							{
+								new TreeNode("File_3.2.1"),
+								new TreeNode("File_3.2.2")
+							}},
+						new TreeNode("File_3.1")
+					}}
+				}
+			};
 		}
 		}
 
 
 		private void ClearObjects()
 		private void ClearObjects()
@@ -72,6 +164,8 @@ namespace UICatalog.Scenarios {
 		}
 		}
 		private void AddRootDrives()
 		private void AddRootDrives()
 		{
 		{
+			SetupFileSystemDelegates();
+
 			_treeView.AddObjects(DriveInfo.GetDrives().Select(d=>d.RootDirectory));
 			_treeView.AddObjects(DriveInfo.GetDrives().Select(d=>d.RootDirectory));
 		}
 		}
 		private void RemoveRoot()
 		private void RemoveRoot()