TreeView.cs 31 KB

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