2
0

TreeView.cs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966
  1. // This code is based on http://objectlistview.sourceforge.net (GPLv3 tree/list controls by [email protected]). Phillip has explicitly granted permission for his design and code to be used in this library under the MIT license.
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. namespace Terminal.Gui {
  6. /// <summary>
  7. /// Interface to implement when you want <see cref="TreeView"/> to automatically determine children for your class
  8. /// </summary>
  9. public interface ITreeNode
  10. {
  11. /// <summary>
  12. /// The children of your class which should be rendered underneath it when expanded
  13. /// </summary>
  14. /// <value></value>
  15. IList<ITreeNode> Children {get;}
  16. /// <summary>
  17. /// The textual representation to be rendered when your class is visible in the tree
  18. /// </summary>
  19. /// <value></value>
  20. string Text {get;}
  21. }
  22. /// <summary>
  23. /// Simple class for representing nodes of a <see cref="TreeView"/>
  24. /// </summary>
  25. public class TreeNode : ITreeNode
  26. {
  27. /// <summary>
  28. /// Children of the current node
  29. /// </summary>
  30. /// <returns></returns>
  31. public IList<ITreeNode> Children {get;set;} = new List<ITreeNode>();
  32. /// <summary>
  33. /// Text to display in tree node for current entry
  34. /// </summary>
  35. /// <value></value>
  36. public string Text {get;set;}
  37. /// <summary>
  38. /// returns <see cref="Text"/>
  39. /// </summary>
  40. /// <returns></returns>
  41. public override string ToString()
  42. {
  43. return Text;
  44. }
  45. /// <summary>
  46. /// Initialises a new instance with no <see cref="Text"/>
  47. /// </summary>
  48. public TreeNode()
  49. {
  50. }
  51. /// <summary>
  52. /// Initialises a new instance and sets starting <see cref="Text"/>
  53. /// </summary>
  54. public TreeNode(string text)
  55. {
  56. Text = text;
  57. }
  58. }
  59. /// <summary>
  60. /// Interface for supplying data to a <see cref="TreeView"/> on demand as root level nodes are expanded by the user
  61. /// </summary>
  62. public interface ITreeBuilder
  63. {
  64. /// <summary>
  65. /// Returns true if <see cref="CanExpand"/> is implemented by this class
  66. /// </summary>
  67. /// <value></value>
  68. bool SupportsCanExpand {get;}
  69. /// <summary>
  70. /// 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)
  71. /// </summary>
  72. /// <param name="model"></param>
  73. /// <returns></returns>
  74. bool CanExpand(object model);
  75. /// <summary>
  76. /// Returns all children of a given <paramref name="model"/> which should be added to the tree as new branches underneath it
  77. /// </summary>
  78. /// <param name="model"></param>
  79. /// <returns></returns>
  80. IEnumerable<object> GetChildren(object model);
  81. }
  82. /// <summary>
  83. /// Abstract implementation of <see cref="ITreeBuilder"/>
  84. /// </summary>
  85. public abstract class TreeBuilder : ITreeBuilder {
  86. /// <inheritdoc/>
  87. public bool SupportsCanExpand { get; protected set;} = false;
  88. /// <summary>
  89. /// Override this method to return a rapid answer as to whether <see cref="GetChildren(object)"/> returns results.
  90. /// </summary>
  91. /// <param name="model"></param>
  92. /// <returns></returns>
  93. public virtual bool CanExpand (object model){
  94. return GetChildren(model).Any();
  95. }
  96. /// <inheritdoc/>
  97. public abstract IEnumerable<object> GetChildren (object model);
  98. /// <summary>
  99. /// Constructs base and initializes <see cref="SupportsCanExpand"/>
  100. /// </summary>
  101. /// <param name="supportsCanExpand">Pass true if you intend to implement <see cref="CanExpand(object)"/> otherwise false</param>
  102. public TreeBuilder(bool supportsCanExpand)
  103. {
  104. SupportsCanExpand = supportsCanExpand;
  105. }
  106. }
  107. /// <summary>
  108. /// <see cref="ITreeBuilder"/> implementation for <see cref="TreeNode"/> objects
  109. /// </summary>
  110. public class TreeNodeBuilder : TreeBuilder {
  111. /// <summary>
  112. /// Initialises a new instance of builder for any model objects of Type <see cref="ITreeNode"/>
  113. /// </summary>
  114. public TreeNodeBuilder():base(false)
  115. {
  116. }
  117. /// <summary>
  118. /// Returns <see cref="ITreeNode.Children"/> from <paramref name="model"/>
  119. /// </summary>
  120. /// <param name="model"></param>
  121. /// <returns></returns>
  122. public override IEnumerable<object> GetChildren (object model)
  123. {
  124. return model is ITreeNode n ? n.Children : Enumerable.Empty<object>();
  125. }
  126. }
  127. /// <summary>
  128. /// Implementation of <see cref="ITreeBuilder"/> that uses user defined functions
  129. /// </summary>
  130. public class DelegateTreeBuilder : TreeBuilder
  131. {
  132. private Func<object,IEnumerable<object>> childGetter;
  133. private Func<object,bool> canExpand;
  134. /// <summary>
  135. /// Constructs an implementation of <see cref="ITreeBuilder"/> that calls the user defined method <paramref name="childGetter"/> to determine children
  136. /// </summary>
  137. /// <param name="childGetter"></param>
  138. /// <returns></returns>
  139. public DelegateTreeBuilder(Func<object,IEnumerable<object>> childGetter) : base(false)
  140. {
  141. this.childGetter = childGetter;
  142. }
  143. /// <summary>
  144. /// Constructs an implementation of <see cref="ITreeBuilder"/> that calls the user defined method <paramref name="childGetter"/> to determine children and <paramref name="canExpand"/> to determine expandability
  145. /// </summary>
  146. /// <param name="childGetter"></param>
  147. /// <param name="canExpand"></param>
  148. /// <returns></returns>
  149. public DelegateTreeBuilder(Func<object,IEnumerable<object>> childGetter, Func<object,bool> canExpand) : base(true)
  150. {
  151. this.childGetter = childGetter;
  152. this.canExpand = canExpand;
  153. }
  154. /// <summary>
  155. /// Returns whether a node can be expanded based on the delegate passed during construction
  156. /// </summary>
  157. /// <param name="model"></param>
  158. /// <returns></returns>
  159. public override bool CanExpand (object model)
  160. {
  161. return canExpand?.Invoke(model) ?? base.CanExpand (model);
  162. }
  163. /// <summary>
  164. /// Returns children using the delegate method passed during construction
  165. /// </summary>
  166. /// <param name="model"></param>
  167. /// <returns></returns>
  168. public override IEnumerable<object> GetChildren (object model)
  169. {
  170. return childGetter.Invoke(model);
  171. }
  172. }
  173. /// <summary>
  174. /// Hierarchical tree view with expandable branches. Branch objects are dynamically determined when expanded using a user defined <see cref="ITreeBuilder"/>
  175. /// </summary>
  176. public class TreeView : View
  177. {
  178. private int scrollOffset;
  179. /// <summary>
  180. /// True to render vertical lines under expanded nodes to show which node belongs to which parent. False to use only whitespace
  181. /// </summary>
  182. /// <value></value>
  183. public bool ShowBranchLines {get;set;} = true;
  184. /// <summary>
  185. /// Determines how sub branches of the tree are dynamically built at runtime as the user expands root nodes
  186. /// </summary>
  187. /// <value></value>
  188. public ITreeBuilder TreeBuilder { get;set;}
  189. /// <summary>
  190. /// private variable for <see cref="SelectedObject"/>
  191. /// </summary>
  192. object selectedObject;
  193. /// <summary>
  194. /// The currently selected object in the tree
  195. /// </summary>
  196. public object SelectedObject {
  197. get => selectedObject;
  198. set {
  199. var oldValue = selectedObject;
  200. selectedObject = value;
  201. if(!ReferenceEquals(oldValue,value))
  202. SelectionChanged?.Invoke(this,new SelectionChangedEventArgs(this,oldValue,value));
  203. }
  204. }
  205. /// <summary>
  206. /// Called when the <see cref="SelectedObject"/> changes
  207. /// </summary>
  208. public event EventHandler<SelectionChangedEventArgs> SelectionChanged;
  209. /// <summary>
  210. /// Refreshes the state of the object <paramref name="o"/> in the tree. This will recompute children, string representation etc
  211. /// </summary>
  212. /// <remarks>This has no effect if the object is not exposed in the tree.</remarks>
  213. /// <param name="o"></param>
  214. /// <param name="startAtTop">True to also refresh all ancestors of the objects branch (starting with the root). False to refresh only the passed node</param>
  215. public void RefreshObject (object o, bool startAtTop = false)
  216. {
  217. var branch = ObjectToBranch(o);
  218. if(branch != null) {
  219. branch.Refresh(startAtTop);
  220. SetNeedsDisplay();
  221. }
  222. }
  223. /// <summary>
  224. /// Rebuilds the tree structure for all exposed objects starting with the root objects. Call this method when you know there are changes to the tree but don't know which objects have changed (otherwise use <see cref="RefreshObject(object, bool)"/>)
  225. /// </summary>
  226. public void RebuildTree()
  227. {
  228. foreach(var branch in roots.Values)
  229. branch.Rebuild();
  230. SetNeedsDisplay();
  231. }
  232. /// <summary>
  233. /// The root objects in the tree, note that this collection is of root objects only
  234. /// </summary>
  235. public IEnumerable<object> Objects {get=>roots.Keys;}
  236. /// <summary>
  237. /// Map of root objects to the branches under them. All objects have a <see cref="Branch"/> even if that branch has no children
  238. /// </summary>
  239. internal Dictionary<object,Branch> roots {get; set;} = new Dictionary<object, Branch>();
  240. /// <summary>
  241. /// The amount of tree view that has been scrolled off the top of the screen (by the user scrolling down)
  242. /// </summary>
  243. /// <remarks>Setting a value of less than 0 will result in a ScrollOffset of 0. To see changes in the UI call <see cref="View.SetNeedsDisplay()"/></remarks>
  244. public int ScrollOffset {
  245. get => scrollOffset;
  246. set {
  247. scrollOffset = Math.Max(0,value);
  248. }
  249. }
  250. /// <summary>
  251. /// Creates a new tree view with absolute positioning. Use <see cref="AddObjects(IEnumerable{object})"/> to set set root objects for the tree.
  252. /// </summary>
  253. public TreeView():base()
  254. {
  255. CanFocus = true;
  256. TreeBuilder = new TreeNodeBuilder();
  257. }
  258. /// <summary>
  259. /// Initialises <see cref="TreeBuilder"/>.Creates a new tree view with absolute positioning. Use <see cref="AddObjects(IEnumerable{object})"/> to set set root objects for the tree.
  260. /// </summary>
  261. public TreeView(ITreeBuilder builder) : this()
  262. {
  263. TreeBuilder = builder;
  264. }
  265. /// <summary>
  266. /// Adds a new root level object unless it is already a root of the tree
  267. /// </summary>
  268. /// <param name="o"></param>
  269. public void AddObject(object o)
  270. {
  271. if(!roots.ContainsKey(o)) {
  272. roots.Add(o,new Branch(this,null,o));
  273. SetNeedsDisplay();
  274. }
  275. }
  276. /// <summary>
  277. /// Removes all objects from the tree and clears <see cref="SelectedObject"/>
  278. /// </summary>
  279. public void ClearObjects()
  280. {
  281. SelectedObject = null;
  282. roots = new Dictionary<object, Branch>();
  283. SetNeedsDisplay();
  284. }
  285. /// <summary>
  286. /// Removes the given root object from the tree
  287. /// </summary>
  288. /// <remarks>If <paramref name="o"/> is the currently <see cref="SelectedObject"/> then the selection is cleared</remarks>
  289. /// <param name="o"></param>
  290. public void Remove(object o)
  291. {
  292. if(roots.ContainsKey(o)) {
  293. roots.Remove(o);
  294. SetNeedsDisplay();
  295. if(Equals(SelectedObject,o))
  296. SelectedObject = null;
  297. }
  298. }
  299. /// <summary>
  300. /// Adds many new root level objects. Objects that are already root objects are ignored
  301. /// </summary>
  302. /// <param name="collection">Objects to add as new root level objects</param>
  303. public void AddObjects(IEnumerable<object> collection)
  304. {
  305. bool objectsAdded = false;
  306. foreach(var o in collection) {
  307. if (!roots.ContainsKey (o)) {
  308. roots.Add(o,new Branch(this,null,o));
  309. objectsAdded = true;
  310. }
  311. }
  312. if(objectsAdded)
  313. SetNeedsDisplay();
  314. }
  315. /// <summary>
  316. /// Returns the currently expanded children of the passed object. Returns an empty collection if the branch is not exposed or not expanded
  317. /// </summary>
  318. /// <param name="o">An object in the tree</param>
  319. /// <returns></returns>
  320. public IEnumerable<object> GetChildren (object o)
  321. {
  322. var branch = ObjectToBranch(o);
  323. if(branch == null || !branch.IsExpanded)
  324. return new object[0];
  325. return branch.ChildBranches?.Values?.Select(b=>b.Model)?.ToArray() ?? new object[0];
  326. }
  327. /// <summary>
  328. /// Returns the parent object of <paramref name="o"/> in the tree. Returns null if the object is not exposed in the tree
  329. /// </summary>
  330. /// <param name="o">An object in the tree</param>
  331. /// <returns></returns>
  332. public object GetParent (object o)
  333. {
  334. return ObjectToBranch(o)?.Parent?.Model;
  335. }
  336. /// <summary>
  337. /// Returns the string representation of model objects hosted in the tree. Default implementation is to call <see cref="object.ToString"/>
  338. /// </summary>
  339. /// <value></value>
  340. public AspectGetterDelegate AspectGetter {get;set;} = (o)=>o.ToString();
  341. ///<inheritdoc/>
  342. public override void Redraw (Rect bounds)
  343. {
  344. if(roots == null)
  345. return;
  346. var map = BuildLineMap();
  347. for(int line = 0 ; line < bounds.Height; line++){
  348. var idxToRender = ScrollOffset + line;
  349. // Is there part of the tree view to render?
  350. if(idxToRender < map.Length) {
  351. // Render the line
  352. map[idxToRender].Draw(Driver,ColorScheme,line,bounds.Width);
  353. } else {
  354. // Else clear the line to prevent stale symbols due to scrolling etc
  355. Move(0,line);
  356. Driver.SetAttribute(ColorScheme.Normal);
  357. Driver.AddStr(new string(' ',bounds.Width));
  358. }
  359. }
  360. }
  361. /// <summary>
  362. /// Returns the index of the object <paramref name="o"/> if it is currently exposed (it's parent(s) have been expanded). This can be used with <see cref="ScrollOffset"/> and <see cref="View.SetNeedsDisplay()"/> to scroll to a specific object
  363. /// </summary>
  364. /// <remarks>Uses the Equals method and returns the first index at which the object is found or -1 if it is not found</remarks>
  365. /// <param name="o">An object that appears in your tree and is currently exposed</param>
  366. /// <returns>The index the object was found at or -1 if it is not currently revealed or not in the tree at all</returns>
  367. public int GetScrollOffsetOf(object o)
  368. {
  369. var map = BuildLineMap();
  370. for (int i = 0; i < map.Length; i++)
  371. {
  372. if (map[i].Model.Equals(o))
  373. return i;
  374. }
  375. //object not found
  376. return -1;
  377. }
  378. /// <summary>
  379. /// Calculates all currently visible/expanded branches (including leafs) and outputs them by index from the top of the screen
  380. /// </summary>
  381. /// <remarks>Index 0 of the returned array is the first item that should be visible in the top of the control, index 1 is the next etc.</remarks>
  382. /// <returns></returns>
  383. private Branch[] BuildLineMap()
  384. {
  385. List<Branch> toReturn = new List<Branch>();
  386. foreach(var root in roots.Values) {
  387. toReturn.AddRange(AddToLineMap(root));
  388. }
  389. return toReturn.ToArray();
  390. }
  391. private IEnumerable<Branch> AddToLineMap (Branch currentBranch)
  392. {
  393. yield return currentBranch;
  394. if(currentBranch.IsExpanded){
  395. foreach(var subBranch in currentBranch.ChildBranches.Values){
  396. foreach(var sub in AddToLineMap(subBranch)) {
  397. yield return sub;
  398. }
  399. }
  400. }
  401. }
  402. /// <summary>
  403. /// Symbol to use for expanded branch nodes to indicate to the user that they can be collapsed. Defaults to '-'
  404. /// </summary>
  405. public Rune ExpandedSymbol {get;set;} = '-';
  406. /// <summary>
  407. /// Symbol to use for branch nodes that can be expanded to indicate this to the user. Defaults to '+'
  408. /// </summary>
  409. public Rune ExpandableSymbol {get;set;} = '+';
  410. /// <inheritdoc/>
  411. public override bool ProcessKey (KeyEvent keyEvent)
  412. {
  413. switch (keyEvent.Key) {
  414. case Key.CursorRight:
  415. Expand(SelectedObject);
  416. break;
  417. case Key.CursorLeft:
  418. CursorLeft();
  419. break;
  420. case Key.CursorUp:
  421. AdjustSelection(-1);
  422. break;
  423. case Key.CursorDown:
  424. AdjustSelection(1);
  425. break;
  426. case Key.PageUp:
  427. AdjustSelection(-Bounds.Height);
  428. break;
  429. case Key.PageDown:
  430. AdjustSelection(Bounds.Height);
  431. break;
  432. case Key.Home:
  433. GoToFirst();
  434. break;
  435. case Key.End:
  436. GoToEnd();
  437. break;
  438. default:
  439. // we don't care about this keystroke
  440. return false;
  441. }
  442. PositionCursor ();
  443. return true;
  444. }
  445. /// <summary>
  446. /// 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
  447. /// </summary>
  448. protected virtual void CursorLeft()
  449. {
  450. if(IsExpanded(SelectedObject))
  451. Collapse(SelectedObject);
  452. else
  453. {
  454. var parent = GetParent(SelectedObject);
  455. if(parent != null){
  456. SelectedObject = parent;
  457. SetNeedsDisplay();
  458. }
  459. }
  460. }
  461. /// <summary>
  462. /// Changes the <see cref="SelectedObject"/> to the first root object and resets the <see cref="ScrollOffset"/> to 0
  463. /// </summary>
  464. public void GoToFirst()
  465. {
  466. ScrollOffset = 0;
  467. SelectedObject = roots.Keys.FirstOrDefault();
  468. SetNeedsDisplay();
  469. }
  470. /// <summary>
  471. /// Changes the <see cref="SelectedObject"/> to the last object in the tree and scrolls so that it is visible
  472. /// </summary>
  473. public void GoToEnd ()
  474. {
  475. var map = BuildLineMap();
  476. ScrollOffset = Math.Max(0,map.Length - Bounds.Height +1);
  477. SelectedObject = map.Last().Model;
  478. SetNeedsDisplay();
  479. }
  480. /// <summary>
  481. /// Changes the selected object by a number of screen lines
  482. /// </summary>
  483. /// <remarks>If nothing is currently selected the first root is selected. If the selected object is no longer in the tree the first object is selected</remarks>
  484. /// <param name="offset"></param>
  485. private void AdjustSelection (int offset)
  486. {
  487. if(SelectedObject == null){
  488. SelectedObject = roots.Keys.FirstOrDefault();
  489. }
  490. else {
  491. var map = BuildLineMap();
  492. var idx = Array.FindIndex(map,b=>b.Model.Equals(SelectedObject));
  493. if(idx == -1) {
  494. // The current selection has disapeared!
  495. SelectedObject = roots.Keys.FirstOrDefault();
  496. }
  497. else {
  498. var newIdx = Math.Min(Math.Max(0,idx+offset),map.Length-1);
  499. SelectedObject = map[newIdx].Model;
  500. if(newIdx < ScrollOffset) {
  501. //if user has scrolled up too far to see their selection
  502. ScrollOffset = newIdx;
  503. }
  504. else if(newIdx >= ScrollOffset + Bounds.Height){
  505. //if user has scrolled off bottom of visible tree
  506. ScrollOffset = Math.Max(0,(newIdx+1) - Bounds.Height);
  507. }
  508. }
  509. }
  510. SetNeedsDisplay();
  511. }
  512. /// <summary>
  513. /// Expands the supplied object if it is contained in the tree (either as a root object or as an exposed branch object)
  514. /// </summary>
  515. /// <param name="toExpand">The object to expand</param>
  516. public void Expand(object toExpand)
  517. {
  518. if(toExpand == null)
  519. return;
  520. ObjectToBranch(toExpand)?.Expand();
  521. SetNeedsDisplay();
  522. }
  523. /// <summary>
  524. /// Returns true if the given object <paramref name="o"/> is exposed in the tree and expanded otherwise false
  525. /// </summary>
  526. /// <param name="o"></param>
  527. /// <returns></returns>
  528. public bool IsExpanded(object o)
  529. {
  530. return ObjectToBranch(o)?.IsExpanded ?? false;
  531. }
  532. /// <summary>
  533. /// Collapses the supplied object if it is currently expanded
  534. /// </summary>
  535. /// <param name="toCollapse">The object to collapse</param>
  536. public void Collapse(object toCollapse)
  537. {
  538. if(toCollapse == null)
  539. return;
  540. ObjectToBranch(toCollapse)?.Collapse();
  541. SetNeedsDisplay();
  542. }
  543. /// <summary>
  544. /// 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
  545. /// </summary>
  546. /// <param name="toFind"></param>
  547. /// <returns>The branch for <paramref name="toFind"/> or null if it is not currently exposed in the tree</returns>
  548. private Branch ObjectToBranch(object toFind)
  549. {
  550. return BuildLineMap().FirstOrDefault(o=>o.Model.Equals(toFind));
  551. }
  552. }
  553. class Branch
  554. {
  555. /// <summary>
  556. /// True if the branch is expanded to reveal child branches
  557. /// </summary>
  558. public bool IsExpanded {get;set;}
  559. /// <summary>
  560. /// The users object that is being displayed by this branch of the tree
  561. /// </summary>
  562. public object Model {get;private set;}
  563. /// <summary>
  564. /// The depth of the current branch. Depth of 0 indicates root level branches
  565. /// </summary>
  566. public int Depth {get;private set;} = 0;
  567. /// <summary>
  568. /// The children of the current branch. This is null until the first call to <see cref="FetchChildren"/> to avoid enumerating the entire underlying hierarchy
  569. /// </summary>
  570. public Dictionary<object,Branch> ChildBranches {get;set;}
  571. /// <summary>
  572. /// The parent <see cref="Branch"/> or null if it is a root.
  573. /// </summary>
  574. public Branch Parent {get; private set;}
  575. private TreeView tree;
  576. /// <summary>
  577. /// Declares a new branch of <paramref name="tree"/> in which the users object <paramref name="model"/> is presented
  578. /// </summary>
  579. /// <param name="tree">The UI control in which the branch resides</param>
  580. /// <param name="parentBranchIfAny">Pass null for root level branches, otherwise pass the parent</param>
  581. /// <param name="model">The user's object that should be displayed</param>
  582. public Branch(TreeView tree,Branch parentBranchIfAny,object model)
  583. {
  584. this.tree = tree;
  585. this.Model = model;
  586. if(parentBranchIfAny != null) {
  587. Depth = parentBranchIfAny.Depth +1;
  588. Parent = parentBranchIfAny;
  589. }
  590. }
  591. /// <summary>
  592. /// Fetch the children of this branch. This method populates <see cref="ChildBranches"/>
  593. /// </summary>
  594. public virtual void FetchChildren()
  595. {
  596. if (tree.TreeBuilder == null)
  597. return;
  598. var children = tree.TreeBuilder.GetChildren(this.Model) ?? Enumerable.Empty<object>();
  599. this.ChildBranches = children.ToDictionary(k=>k,val=>new Branch(tree,this,val));
  600. }
  601. /// <summary>
  602. /// Renders the current <see cref="Model"/> on the specified line <paramref name="y"/>
  603. /// </summary>
  604. /// <param name="driver"></param>
  605. /// <param name="colorScheme"></param>
  606. /// <param name="y"></param>
  607. /// <param name="availableWidth"></param>
  608. public virtual void Draw(ConsoleDriver driver,ColorScheme colorScheme, int y, int availableWidth)
  609. {
  610. // Everything on line before the expansion run and branch text
  611. Rune[] prefix = GetLinePrefix(driver).ToArray();
  612. Rune expansion = GetExpandableIcon(driver);
  613. string lineBody = tree.AspectGetter(Model);
  614. var remainingWidth = availableWidth - (prefix.Length + 1 + lineBody.Length);
  615. tree.Move(0,y);
  616. driver.SetAttribute(colorScheme.Normal);
  617. foreach(Rune r in prefix)
  618. driver.AddRune(r);
  619. driver.AddRune(expansion);
  620. driver.SetAttribute(tree.SelectedObject == Model ?
  621. colorScheme.HotFocus :
  622. colorScheme.Normal);
  623. driver.AddStr(lineBody);
  624. driver.SetAttribute(colorScheme.Normal);
  625. if(remainingWidth > 0)
  626. driver.AddStr(new string(' ',remainingWidth));
  627. }
  628. /// <summary>
  629. /// Gets all characters to render prior to the current branches line. This includes indentation whitespace and any tree branches (if enabled)
  630. /// </summary>
  631. /// <param name="driver"></param>
  632. /// <returns></returns>
  633. private IEnumerable<Rune> GetLinePrefix (ConsoleDriver driver)
  634. {
  635. // If not showing line branches or this is a root object
  636. if (!tree.ShowBranchLines) {
  637. for(int i = 0; i < Depth; i++) {
  638. yield return new Rune(' ');
  639. }
  640. }
  641. // yield indentations with runes appropriate to the state of the parents
  642. foreach(var cur in GetParentBranches().Reverse())
  643. {
  644. if(cur.IsLast())
  645. yield return new Rune(' ');
  646. else
  647. yield return driver.VLine;
  648. }
  649. if(IsLast())
  650. yield return driver.LLCorner;
  651. else
  652. yield return driver.LeftTee;
  653. }
  654. /// <summary>
  655. /// Returns all parents starting with the immediate parent and ending at the root
  656. /// </summary>
  657. /// <returns></returns>
  658. private IEnumerable<Branch> GetParentBranches()
  659. {
  660. var cur = Parent;
  661. while(cur != null)
  662. {
  663. yield return cur;
  664. cur = cur.Parent;
  665. }
  666. }
  667. /// <summary>
  668. /// 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)
  669. /// </summary>
  670. /// <param name="driver"></param>
  671. /// <returns></returns>
  672. public Rune GetExpandableIcon(ConsoleDriver driver)
  673. {
  674. if(IsExpanded)
  675. return tree.ExpandedSymbol;
  676. var leafSymbol = tree.ShowBranchLines ? driver.HLine : ' ';
  677. if(ChildBranches == null) {
  678. //if there is a rapid method for determining whether there are children
  679. if(tree.TreeBuilder.SupportsCanExpand) {
  680. return tree.TreeBuilder.CanExpand(Model) ? tree.ExpandableSymbol : leafSymbol;
  681. }
  682. //there is no way of knowing whether we can expand without fetching the children
  683. FetchChildren();
  684. }
  685. //we fetched or already know the children, so return whether we are a leaf or a expandable branch
  686. return ChildBranches.Any() ? tree.ExpandableSymbol : leafSymbol;
  687. }
  688. /// <summary>
  689. /// Expands the current branch if possible
  690. /// </summary>
  691. public void Expand()
  692. {
  693. if(ChildBranches == null) {
  694. FetchChildren();
  695. }
  696. if (ChildBranches.Any ()) {
  697. IsExpanded = true;
  698. }
  699. }
  700. /// <summary>
  701. /// Marks the branch as collapsed (<see cref="IsExpanded"/> false)
  702. /// </summary>
  703. public void Collapse ()
  704. {
  705. IsExpanded = false;
  706. }
  707. /// <summary>
  708. /// Refreshes cached knowledge in this branch e.g. what children an object has
  709. /// </summary>
  710. /// <param name="startAtTop">True to also refresh all <see cref="Parent"/> branches (starting with the root)</param>
  711. public void Refresh (bool startAtTop)
  712. {
  713. // if we must go up and refresh from the top down
  714. if(startAtTop)
  715. Parent?.Refresh(true);
  716. // we don't want to loose the state of our children so lets be selective about how we refresh
  717. //if we don't know about any children yet just use the normal method
  718. if(ChildBranches == null)
  719. FetchChildren();
  720. else {
  721. // we already knew about some children so preserve the state of the old children
  722. // first gather the new Children
  723. var newChildren = tree.TreeBuilder?.GetChildren(this.Model) ?? Enumerable.Empty<object>();
  724. // Children who no longer appear need to go
  725. foreach(var toRemove in ChildBranches.Keys.Except(newChildren).ToArray())
  726. {
  727. ChildBranches.Remove(toRemove);
  728. //also if the user has this node selected (its disapearing) so lets change selection to us (the parent object) to be helpful
  729. if(Equals(tree.SelectedObject ,toRemove))
  730. tree.SelectedObject = Model;
  731. }
  732. // New children need to be added
  733. foreach(var newChild in newChildren)
  734. {
  735. // If we don't know about the child yet we need a new branch
  736. if (!ChildBranches.ContainsKey (newChild)) {
  737. ChildBranches.Add(newChild,new Branch(tree,this,newChild));
  738. }
  739. else{
  740. //we already have this object but update the reference anyway incase Equality match but the references are new
  741. ChildBranches[newChild].Model = newChild;
  742. }
  743. }
  744. }
  745. }
  746. /// <summary>
  747. /// Calls <see cref="Refresh(bool)"/> on the current branch and all expanded children
  748. /// </summary>
  749. internal void Rebuild()
  750. {
  751. Refresh(false);
  752. // if we know about our children
  753. if(ChildBranches != null) {
  754. if(IsExpanded) {
  755. //if we are expanded we need to updatethe visible children
  756. foreach(var child in ChildBranches) {
  757. child.Value.Refresh(false);
  758. }
  759. }
  760. else {
  761. // we are not expanded so should forget about children because they may not exist anymore
  762. ChildBranches = null;
  763. }
  764. }
  765. }
  766. /// <summary>
  767. /// Returns true if this branch has parents and it is the last node of it's parents branches (or last root of the tree)
  768. /// </summary>
  769. /// <returns></returns>
  770. private bool IsLast()
  771. {
  772. if(Parent == null)
  773. return this == tree.roots.Values.LastOrDefault();
  774. return Parent.ChildBranches.Values.LastOrDefault() == this;
  775. }
  776. }
  777. /// <summary>
  778. /// Delegates of this type are used to fetch string representations of user's model objects
  779. /// </summary>
  780. /// <param name="model"></param>
  781. /// <returns></returns>
  782. public delegate string AspectGetterDelegate(object model);
  783. /// <summary>
  784. /// Event arguments describing a change in selected object in a tree view
  785. /// </summary>
  786. public class SelectionChangedEventArgs : EventArgs
  787. {
  788. /// <summary>
  789. /// The view in which the change occurred
  790. /// </summary>
  791. public TreeView Tree { get; }
  792. /// <summary>
  793. /// The previously selected value (can be null)
  794. /// </summary>
  795. public object OldValue { get; }
  796. /// <summary>
  797. /// The newly selected value in the <see cref="Tree"/> (can be null)
  798. /// </summary>
  799. public object NewValue { get; }
  800. /// <summary>
  801. /// Creates a new instance of event args describing a change of selection in <paramref name="tree"/>
  802. /// </summary>
  803. /// <param name="tree"></param>
  804. /// <param name="oldValue"></param>
  805. /// <param name="newValue"></param>
  806. public SelectionChangedEventArgs(TreeView tree, object oldValue, object newValue)
  807. {
  808. Tree = tree;
  809. OldValue = oldValue;
  810. NewValue = newValue;
  811. }
  812. }
  813. }