diff --git a/Terminal.Gui/Views/TreeView.cs b/Terminal.Gui/Views/TreeView.cs index 583ddea3d..c4a2bd15c 100644 --- a/Terminal.Gui/Views/TreeView.cs +++ b/Terminal.Gui/Views/TreeView.cs @@ -6,6 +6,9 @@ using System.Linq; namespace Terminal.Gui { + /// + /// Hierarchical tree view with expandable branches. Branch objects are dynamically determined when expanded using a user defined + /// public class TreeView : View { /// @@ -43,7 +46,10 @@ namespace Terminal.Gui { /// public int ScrollOffset {get; private set;} - public TreeView () + /// + /// Creates a new tree view with absolute positioning. Use to set set root objects for the tree + /// + public TreeView ():base() { CanFocus = true; } @@ -59,11 +65,37 @@ namespace Terminal.Gui { SetNeedsDisplay(); } } + + /// + /// Removes all objects from the tree and clears + /// + public void ClearObjects() + { + SelectedObject = null; + roots = new Dictionary(); + SetNeedsDisplay(); + } + + /// + /// Removes the given root object from the tree + /// + /// If is the currently then the selection is cleared + /// + public void Remove(object o) + { + if(roots.ContainsKey(o)) { + roots.Remove(o); + SetNeedsDisplay(); + + if(Equals(SelectedObject,o)) + SelectedObject = null; + } + } /// /// Adds many new root level objects. Objects that are already root objects are ignored /// - /// + /// Objects to add as new root level objects public void AddObjects(IEnumerable 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 /// /// - public Func AspectGetter {get;set;} = (o)=>o.ToString(); + public AspectGetterDelegate AspectGetter {get;set;} = (o)=>o.ToString(); /// public override void Redraw (Rect bounds) @@ -142,8 +174,19 @@ namespace Terminal.Gui { } } + /// + /// Symbol to use for expanded branch nodes to indicate to the user that they can be collapsed. Defaults to '-' + /// public char ExpandedSymbol {get;set;} = '-'; + + /// + /// Symbol to use for branch nodes that can be expanded to indicate this to the user. Defaults to '+' + /// public char ExpandableSymbol {get;set;} = '+'; + + /// + /// Symbol to use for branch nodes that cannot be expanded (as they have no children). Defaults to space ' ' + /// public char LeafSymbol {get;set;} = ' '; /// @@ -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; } + /// + /// Changes the to the first root object and resets the to 0 + /// + public void GoToFirst() + { + ScrollOffset = 0; + SelectedObject = roots.Keys.FirstOrDefault(); + + SetNeedsDisplay(); + } + + /// + /// Changes the to the last object in the tree and scrolls so that it is visible + /// + public void GoToEnd () + { + var map = BuildLineMap(); + ScrollOffset = Math.Max(0,map.Length - Bounds.Height +1); + SelectedObject = map.Last().Model; + + SetNeedsDisplay(); + } + /// /// Changes the selected object by a number of screen lines /// @@ -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) + /// + /// Expands the supplied object if it is contained in the tree (either as a root object or as an exposed branch object) + /// + /// The object to expand + 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) + /// + /// Collapses the supplied object if it is currently expanded + /// + /// The object to collapse + 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 in the tree for . This will not work for objects hidden by their parent being collapsed /// /// - /// + /// The branch for or null if it is not currently exposed in the tree private Branch ObjectToBranch(object toFind) { return BuildLineMap().FirstOrDefault(o=>o.Model.Equals(toFind)); @@ -242,15 +329,36 @@ namespace Terminal.Gui { class Branch { + /// + /// True if the branch is expanded to reveal child branches + /// public bool IsExpanded {get;set;} - public Object Model{get;set;} + + /// + /// The users object that is being displayed by this branch of the tree + /// + public object Model {get;set;} + /// + /// The depth of the current branch. Depth of 0 indicates root level branches + /// public int Depth {get;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;} + private TreeView tree; - public Branch(TreeView tree,Branch parentBranchIfAny,Object model) + /// + /// 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,object model) { this.tree = tree; this.Model = model; @@ -292,7 +400,11 @@ namespace Terminal.Gui { driver.AddStr(representation.PadRight(availableWidth)); } - char GetExpandableIcon() + /// + /// 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 char GetExpandableIcon() { if(IsExpanded) return tree.ExpandedSymbol; @@ -302,6 +414,25 @@ namespace Terminal.Gui { return ChildBranches.Any() ? tree.ExpandableSymbol : tree.LeafSymbol; } + + /// + /// Expands the current branch if possible + /// + public void Expand() + { + if(ChildBranches == null) { + FetchChildren(); + } + + if (ChildBranches.Any ()) { + IsExpanded = true; + } + } + + internal void Collapse () + { + IsExpanded = false; + } } /// @@ -310,4 +441,11 @@ namespace Terminal.Gui { /// The parent whose children should be fetched /// An enumerable over the children public delegate IEnumerable ChildrenGetterDelegate(object model); + + /// + /// Delegates of this type are used to fetch string representations of user's model objects + /// + /// + /// + public delegate string AspectGetterDelegate(object model); } \ No newline at end of file diff --git a/UICatalog/Scenarios/TreeViewFileSystem.cs b/UICatalog/Scenarios/TreeViewFileSystem.cs index 47a8b93f1..0f2cbbb41 100644 --- a/UICatalog/Scenarios/TreeViewFileSystem.cs +++ b/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 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().Union(d.GetFileSystemInfos()); - } - catch(SystemException ex) { - return new []{ex}; + 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); } } - return new object[0]; - } + private IEnumerable 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().Union(d.GetFileSystemInfos()); + } + catch(SystemException ex) { + return new []{ex}; + } + } - private void Quit () + 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 model.ToString(); + } + + private void Quit () { Application.RequestStop (); } - } }