2
0
tznind 4 жил өмнө
parent
commit
de06dd1de3

+ 155 - 17
Terminal.Gui/Views/TreeView.cs

@@ -6,6 +6,9 @@ using System.Linq;
 
 namespace Terminal.Gui {
 
+	/// <summary>
+	/// Hierarchical tree view with expandable branches.  Branch objects are dynamically determined when expanded using a user defined <see cref="ChildrenGetterDelegate"/>
+	/// </summary>
 	public class TreeView : View
 	{   
 		/// <summary>
@@ -43,7 +46,10 @@ namespace Terminal.Gui {
 		/// </summary>
 		public int ScrollOffset {get; private set;}
 
-		public TreeView ()
+		/// <summary>
+		/// Creates a new tree view with absolute positioning.  Use <see cref="AddObjects(IEnumerable{object})"/> to set set root objects for the tree
+		/// </summary>
+		public TreeView ():base()
 		{
 			CanFocus = true;
 		}
@@ -59,11 +65,37 @@ namespace Terminal.Gui {
 				SetNeedsDisplay();
 			}
 		}
+
+		/// <summary>
+		/// Removes all objects from the tree and clears <see cref="SelectedObject"/>
+		/// </summary>
+		public void ClearObjects()
+		{
+			SelectedObject = null;
+			roots = new Dictionary<object, Branch>();
+			SetNeedsDisplay();
+		}
+
+		/// <summary>
+		/// Removes the given root object from the tree
+		/// </summary>
+		/// <remarks>If <paramref name="o"/> is the currently <see cref="SelectedObject"/> then the selection is cleared</remarks>
+		/// <param name="o"></param>
+		public void Remove(object o)
+		{
+			if(roots.ContainsKey(o)) {
+				roots.Remove(o);
+				SetNeedsDisplay();
+
+				if(Equals(SelectedObject,o))
+					SelectedObject = null;
+			}
+		}
 		
 		/// <summary>
 		/// Adds many new root level objects.  Objects that are already root objects are ignored
 		/// </summary>
-		/// <param name="o"></param>
+		/// <param name="collection">Objects to add as new root level objects</param>
 		public void AddObjects(IEnumerable<object> collection)
 		{
 			bool objectsAdded = false;
@@ -83,7 +115,7 @@ namespace Terminal.Gui {
 		/// Returns the string representation of model objects hosted in the tree.  Default implementation is to call <see cref="object.ToString"/>
 		/// </summary>
 		/// <value></value>
-		public Func<object,string> AspectGetter {get;set;} = (o)=>o.ToString();
+		public AspectGetterDelegate AspectGetter {get;set;} = (o)=>o.ToString();
 
 		///<inheritdoc/>
 		public override void Redraw (Rect bounds)
@@ -142,8 +174,19 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// Symbol to use for expanded branch nodes to indicate to the user that they can be collapsed.  Defaults to '-'
+		/// </summary>
 		public char ExpandedSymbol {get;set;} = '-';
+
+		/// <summary>
+		/// Symbol to use for branch nodes that can be expanded to indicate this to the user.  Defaults to '+'
+		/// </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;} = ' ';
 
 		/// <inheritdoc/>
@@ -163,12 +206,48 @@ namespace Terminal.Gui {
 				case Key.CursorDown:
 					AdjustSelection(1);
 				break;
+				case Key.PageUp:
+					AdjustSelection(-Bounds.Height);
+				break;
+				
+				case Key.PageDown:
+					AdjustSelection(Bounds.Height);
+				break;
+				case Key.Home:
+					GoToFirst();
+				break;
+				case Key.End:
+					GoToEnd();
+				break;
 			}
 
 			PositionCursor ();
 			return true;
 		}
 
+		/// <summary>
+		/// Changes the <see cref="SelectedObject"/> to the first root object and resets the <see cref="ScrollOffset"/> to 0
+		/// </summary>
+		public void GoToFirst()
+		{
+			ScrollOffset = 0;
+			SelectedObject = roots.Keys.FirstOrDefault();
+
+			SetNeedsDisplay();
+		}
+
+		/// <summary>
+		/// Changes the <see cref="SelectedObject"/> to the last object in the tree and scrolls so that it is visible
+		/// </summary>
+		public void GoToEnd ()
+		{
+			var map = BuildLineMap();
+			ScrollOffset = Math.Max(0,map.Length - Bounds.Height +1);
+			SelectedObject = map.Last().Model;
+						
+			SetNeedsDisplay();
+		}
+
 		/// <summary>
 		/// Changes the selected object by a number of screen lines
 		/// </summary>
@@ -201,7 +280,7 @@ namespace Terminal.Gui {
 					else if(newIdx >= ScrollOffset + Bounds.Height){
 						
 						//if user has scrolled off bottom of visible tree
-						ScrollOffset = Math.Max(0,newIdx - Bounds.Height);
+						ScrollOffset = Math.Max(0,(newIdx+1) - Bounds.Height);
 
 					}
 				}
@@ -211,21 +290,29 @@ namespace Terminal.Gui {
 			SetNeedsDisplay();
 		}
 
-		private void Expand(object selectedObject)
+		/// <summary>
+		/// Expands the supplied object if it is contained in the tree (either as a root object or as an exposed branch object)
+		/// </summary>
+		/// <param name="toExpand">The object to expand</param>
+		public void Expand(object toExpand)
 		{
-			if(selectedObject == null)
-			return;
-
-			ObjectToBranch(selectedObject).IsExpanded = true;
+			if(toExpand == null)
+				return;
+			
+			ObjectToBranch(toExpand)?.Expand();
 			SetNeedsDisplay();
 		}
 
-		private void Collapse(object selectedObject)
+		/// <summary>
+		/// Collapses the supplied object if it is currently expanded 
+		/// </summary>
+		/// <param name="toCollapse">The object to collapse</param>
+		public void Collapse(object toCollapse)
 		{
-			if(selectedObject == null)
-			return;
+			if(toCollapse == null)
+				return;
 
-			ObjectToBranch(selectedObject).IsExpanded = false;
+			ObjectToBranch(toCollapse)?.Collapse();
 			SetNeedsDisplay();
 		}
 
@@ -233,7 +320,7 @@ namespace Terminal.Gui {
 		/// Returns the corresponding <see cref="Branch"/> in the tree for <paramref name="toFind"/>.  This will not work for objects hidden by their parent being collapsed
 		/// </summary>
 		/// <param name="toFind"></param>
-		/// <returns></returns>
+		/// <returns>The branch for <paramref name="toFind"/> or null if it is not currently exposed in the tree</returns>
 		private Branch ObjectToBranch(object toFind)
 		{
 			return BuildLineMap().FirstOrDefault(o=>o.Model.Equals(toFind));
@@ -242,15 +329,36 @@ namespace Terminal.Gui {
 
 	class Branch
 	{
+		/// <summary>
+		/// True if the branch is expanded to reveal child branches
+		/// </summary>
 		public bool IsExpanded {get;set;}
-		public Object Model{get;set;}
+
+		/// <summary>
+		/// The users object that is being displayed by this branch of the tree
+		/// </summary>
+		public object Model {get;set;}
 		
+		/// <summary>
+		/// The depth of the current branch.  Depth of 0 indicates root level branches
+		/// </summary>
 		public int Depth {get;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<object,Branch> ChildBranches {get;set;}
 
+
 		private TreeView tree;
 
-		public Branch(TreeView tree,Branch parentBranchIfAny,Object model)
+		/// <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 tree,Branch parentBranchIfAny,object model)
 		{
 			this.tree  = tree;
 			this.Model = model;
@@ -292,7 +400,11 @@ namespace Terminal.Gui {
 			driver.AddStr(representation.PadRight(availableWidth));
 		}
 
-		char GetExpandableIcon()
+		/// <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>
+		/// <returns></returns>
+		public char GetExpandableIcon()
 		{
 			if(IsExpanded)
 				return tree.ExpandedSymbol;
@@ -302,6 +414,25 @@ namespace Terminal.Gui {
 
 			return ChildBranches.Any() ? tree.ExpandableSymbol : tree.LeafSymbol;
 		}
+
+		/// <summary>
+		/// Expands the current branch if possible
+		/// </summary>
+		public void Expand()
+		{
+			if(ChildBranches == null) {
+				FetchChildren();
+			}
+
+			if (ChildBranches.Any ()) {
+				IsExpanded = true;
+			}
+		}
+
+		internal void Collapse ()
+		{
+			IsExpanded = false;
+		}
 	}
    
 	/// <summary>
@@ -310,4 +441,11 @@ namespace Terminal.Gui {
 	/// <param name="model">The parent whose children should be fetched</param>
 	/// <returns>An enumerable over the children</returns>
 	public delegate IEnumerable<object> ChildrenGetterDelegate(object model);
+
+	/// <summary>
+	/// Delegates of this type are used to fetch string representations of user's model objects
+	/// </summary>
+	/// <param name="model"></param>
+	/// <returns></returns>
+	public delegate string AspectGetterDelegate(object model);
 }

+ 46 - 15
UICatalog/Scenarios/TreeViewFileSystem.cs

@@ -6,7 +6,7 @@ using System.Text;
 using Terminal.Gui;
 
 namespace UICatalog.Scenarios {
-	[ScenarioMetadata (Name: "TreeViewFileSystem", Description: "A Terminal.Gui tree view file system explorer")]
+	[ScenarioMetadata (Name: "TreeViewFileSystem", Description: "Hierarchical file system explorer based on TreeView")]
 	[ScenarioCategory ("Controls")]
 	[ScenarioCategory ("Dialogs")]
 	[ScenarioCategory ("Text")]
@@ -31,6 +31,9 @@ namespace UICatalog.Scenarios {
 			Top.Add (menu);
 
 			var statusBar = new StatusBar (new StatusItem [] {
+				new StatusItem(Key.F2, "~F2~ Add Root Drives", () => AddRootDrives()),
+				new StatusItem(Key.F3, "~F3~ Remove Root Object", () => RemoveRoot()),
+				new StatusItem(Key.F4, "~F4~ Clear Objects", () => ClearObjects()),
 				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
 			});
 			Top.Add (statusBar);
@@ -51,31 +54,59 @@ namespace UICatalog.Scenarios {
 				return;
 			}
 
+			// Determines how to compute children of any given branch
 			_treeView.ChildrenGetter = GetChildren;
-			_treeView.AddObject(new DirectoryInfo(root));
-			
+
+			// Determines how to represent objects as strings on the screen
+			_treeView.AspectGetter = AspectGetter;
+
 			Win.Add (_treeView);
 		}
 
-        private IEnumerable<object> GetChildren(object model)
-        {
-		// If it is a directory it's children are all contained files and dirs
-		if(model is DirectoryInfo d) {
-			try {
-				return d.GetDirectories().Cast<object>().Union(d.GetFileSystemInfos());
+		private void ClearObjects()
+		{
+			_treeView.ClearObjects();
+		}
+		private void AddRootDrives()
+		{
+			_treeView.AddObjects(DriveInfo.GetDrives().Select(d=>d.RootDirectory));
+		}
+		private void RemoveRoot()
+		{
+			if(_treeView.SelectedObject == null)
+				MessageBox.ErrorQuery(10,5,"Error","No object selected","ok");
+			else {
+				_treeView.Remove(_treeView.SelectedObject);
 			}
-			catch(SystemException ex) {
-				return new []{ex};
+		}
+
+		private IEnumerable<object> GetChildren(object model)
+		{
+			// If it is a directory it's children are all contained files and dirs
+			if(model is DirectoryInfo d) {
+				try {
+					return d.GetDirectories().Cast<object>().Union(d.GetFileSystemInfos());
+				}
+				catch(SystemException ex) {
+					return new []{ex};
+				}
 			}
+
+		    return new object[0];
 		}
+		private string AspectGetter(object model)
+		{
+			if(model is DirectoryInfo d)
+				return d.Name;
+			if(model is FileInfo f)
+				return f.Name;
 
-	    return new object[0];
-        }
+			return model.ToString();
+		}
 
-        private void Quit ()
+		private void Quit ()
 		{
 			Application.RequestStop ();
 		}
-
 	}
 }