TreeView.cs 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597
  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. using NStack;
  6. namespace Terminal.Gui {
  7. /// <summary>
  8. /// Interface to implement when you want the regular (non generic) <see cref="TreeView"/> to automatically determine children for your class (without having to specify a <see cref="ITreeBuilder{T}"/>)
  9. /// </summary>
  10. public interface ITreeNode
  11. {
  12. /// <summary>
  13. /// The children of your class which should be rendered underneath it when expanded
  14. /// </summary>
  15. /// <value></value>
  16. IList<ITreeNode> Children {get;}
  17. }
  18. /// <summary>
  19. /// Simple class for representing nodes, use with regular (non generic) <see cref="TreeView"/>.
  20. /// </summary>
  21. public class TreeNode : ITreeNode
  22. {
  23. /// <summary>
  24. /// Children of the current node
  25. /// </summary>
  26. /// <returns></returns>
  27. public IList<ITreeNode> Children {get;set;} = new List<ITreeNode>();
  28. /// <summary>
  29. /// Text to display in tree node for current entry
  30. /// </summary>
  31. /// <value></value>
  32. public string Text {get;set;}
  33. /// <summary>
  34. /// returns <see cref="Text"/>
  35. /// </summary>
  36. /// <returns></returns>
  37. public override string ToString()
  38. {
  39. return Text ?? "Unamed Node";
  40. }
  41. /// <summary>
  42. /// Initialises a new instance with no <see cref="Text"/>
  43. /// </summary>
  44. public TreeNode()
  45. {
  46. }
  47. /// <summary>
  48. /// Initialises a new instance and sets starting <see cref="Text"/>
  49. /// </summary>
  50. public TreeNode(string text)
  51. {
  52. Text = text;
  53. }
  54. }
  55. /// <summary>
  56. /// Interface for supplying data to a <see cref="TreeView{T}"/> on demand as root level nodes are expanded by the user
  57. /// </summary>
  58. public interface ITreeBuilder<T>
  59. {
  60. /// <summary>
  61. /// Returns true if <see cref="CanExpand"/> is implemented by this class
  62. /// </summary>
  63. /// <value></value>
  64. bool SupportsCanExpand {get;}
  65. /// <summary>
  66. /// 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)
  67. /// </summary>
  68. /// <remarks>Only implement this method if you have a very fast way of determining whether an object can have children e.g. checking a Type (directories can always be expanded)</remarks>
  69. /// <param name="model"></param>
  70. /// <returns></returns>
  71. bool CanExpand(T model);
  72. /// <summary>
  73. /// Returns all children of a given <paramref name="model"/> which should be added to the tree as new branches underneath it
  74. /// </summary>
  75. /// <param name="model"></param>
  76. /// <returns></returns>
  77. IEnumerable<T> GetChildren(T model);
  78. }
  79. /// <summary>
  80. /// Abstract implementation of <see cref="ITreeBuilder{T}"/>.
  81. /// </summary>
  82. public abstract class TreeBuilder<T> : ITreeBuilder<T> {
  83. /// <inheritdoc/>
  84. public bool SupportsCanExpand { get; protected set;} = false;
  85. /// <summary>
  86. /// Override this method to return a rapid answer as to whether <see cref="GetChildren(T)"/> returns results. If you are implementing this method ensure you passed true in base constructor or set <see cref="SupportsCanExpand"/>
  87. /// </summary>
  88. /// <param name="model"></param>
  89. /// <returns></returns>
  90. public virtual bool CanExpand (T model){
  91. return GetChildren(model).Any();
  92. }
  93. /// <inheritdoc/>
  94. public abstract IEnumerable<T> GetChildren (T model);
  95. /// <summary>
  96. /// Constructs base and initializes <see cref="SupportsCanExpand"/>
  97. /// </summary>
  98. /// <param name="supportsCanExpand">Pass true if you intend to implement <see cref="CanExpand(T)"/> otherwise false</param>
  99. public TreeBuilder(bool supportsCanExpand)
  100. {
  101. SupportsCanExpand = supportsCanExpand;
  102. }
  103. }
  104. /// <summary>
  105. /// <see cref="ITreeBuilder{T}"/> implementation for <see cref="ITreeNode"/> objects
  106. /// </summary>
  107. public class TreeNodeBuilder : TreeBuilder<ITreeNode>
  108. {
  109. /// <summary>
  110. /// Initialises a new instance of builder for any model objects of Type <see cref="ITreeNode"/>
  111. /// </summary>
  112. public TreeNodeBuilder():base(false)
  113. {
  114. }
  115. /// <summary>
  116. /// Returns <see cref="ITreeNode.Children"/> from <paramref name="model"/>
  117. /// </summary>
  118. /// <param name="model"></param>
  119. /// <returns></returns>
  120. public override IEnumerable<ITreeNode> GetChildren (ITreeNode model)
  121. {
  122. return model.Children;
  123. }
  124. }
  125. /// <summary>
  126. /// Implementation of <see cref="ITreeBuilder{T}"/> that uses user defined functions
  127. /// </summary>
  128. public class DelegateTreeBuilder<T> : TreeBuilder<T>
  129. {
  130. private Func<T,IEnumerable<T>> childGetter;
  131. private Func<T,bool> canExpand;
  132. /// <summary>
  133. /// Constructs an implementation of <see cref="ITreeBuilder{T}"/> that calls the user defined method <paramref name="childGetter"/> to determine children
  134. /// </summary>
  135. /// <param name="childGetter"></param>
  136. /// <returns></returns>
  137. public DelegateTreeBuilder(Func<T,IEnumerable<T>> childGetter) : base(false)
  138. {
  139. this.childGetter = childGetter;
  140. }
  141. /// <summary>
  142. /// 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
  143. /// </summary>
  144. /// <param name="childGetter"></param>
  145. /// <param name="canExpand"></param>
  146. /// <returns></returns>
  147. public DelegateTreeBuilder(Func<T,IEnumerable<T>> childGetter, Func<T,bool> canExpand) : base(true)
  148. {
  149. this.childGetter = childGetter;
  150. this.canExpand = canExpand;
  151. }
  152. /// <summary>
  153. /// Returns whether a node can be expanded based on the delegate passed during construction
  154. /// </summary>
  155. /// <param name="model"></param>
  156. /// <returns></returns>
  157. public override bool CanExpand (T model)
  158. {
  159. return canExpand?.Invoke(model) ?? base.CanExpand (model);
  160. }
  161. /// <summary>
  162. /// Returns children using the delegate method passed during construction
  163. /// </summary>
  164. /// <param name="model"></param>
  165. /// <returns></returns>
  166. public override IEnumerable<T> GetChildren (T model)
  167. {
  168. return childGetter.Invoke(model);
  169. }
  170. }
  171. /// <summary>
  172. /// Interface for all non generic members of <see cref="TreeView{T}"/>
  173. /// </summary>
  174. public interface ITreeView {
  175. /// <summary>
  176. /// Contains options for changing how the tree is rendered
  177. /// </summary>
  178. TreeStyle Style{get;set;}
  179. /// <summary>
  180. /// Removes all objects from the tree and clears selection
  181. /// </summary>
  182. void ClearObjects ();
  183. /// <summary>
  184. /// Sets a flag indicating this view needs to be redisplayed because its state has changed.
  185. /// </summary>
  186. void SetNeedsDisplay ();
  187. }
  188. /// <summary>
  189. /// Convenience implementation of generic <see cref="TreeView{T}"/> for any tree were all nodes implement <see cref="ITreeNode"/>
  190. /// </summary>
  191. public class TreeView : TreeView<ITreeNode> {
  192. /// <summary>
  193. /// Creates a new instance of the tree control with absolute positioning and initialises <see cref="TreeBuilder{T}"/> with default <see cref="ITreeNode"/> based builder
  194. /// </summary>
  195. public TreeView ()
  196. {
  197. TreeBuilder = new TreeNodeBuilder();
  198. }
  199. }
  200. /// <summary>
  201. /// Defines rendering options that affect how the tree is displayed
  202. /// </summary>
  203. public class TreeStyle {
  204. /// <summary>
  205. /// True to render vertical lines under expanded nodes to show which node belongs to which parent. False to use only whitespace
  206. /// </summary>
  207. /// <value></value>
  208. public bool ShowBranchLines {get;set;} = true;
  209. /// <summary>
  210. /// Symbol to use for branch nodes that can be expanded to indicate this to the user. Defaults to '+'. Set to null to hide
  211. /// </summary>
  212. public Rune? ExpandableSymbol {get;set;} = '+';
  213. /// <summary>
  214. /// Symbol to use for branch nodes that can be collapsed (are currently expanded). Defaults to '-'. Set to null to hide
  215. /// </summary>
  216. public Rune? CollapseableSymbol {get;set;} = '-';
  217. /// <summary>
  218. /// Set to true to highlight expand/collapse symbols in hot key color
  219. /// </summary>
  220. public bool ColorExpandSymbol {get;set;}
  221. /// <summary>
  222. /// Invert console colours used to render the expand symbol
  223. /// </summary>
  224. public bool InvertExpandSymbolColors {get;set;}
  225. /// <summary>
  226. /// True to leave the last row of the control free for overwritting (e.g. by a scrollbar). When True scrolling will be triggered on the second last row of the control rather than the last.
  227. /// </summary>
  228. /// <value></value>
  229. public bool LeaveLastRow {get;set;}
  230. }
  231. /// <summary>
  232. /// Hierarchical tree view with expandable branches. Branch objects are dynamically determined when expanded using a user defined <see cref="ITreeBuilder{T}"/>
  233. /// </summary>
  234. public class TreeView<T> : View, ITreeView where T:class
  235. {
  236. private int scrollOffsetVertical;
  237. private int scrollOffsetHorizontal;
  238. /// <summary>
  239. /// Determines how sub branches of the tree are dynamically built at runtime as the user expands root nodes
  240. /// </summary>
  241. /// <value></value>
  242. public ITreeBuilder<T> TreeBuilder { get;set;}
  243. /// <summary>
  244. /// private variable for <see cref="SelectedObject"/>
  245. /// </summary>
  246. T selectedObject;
  247. /// <summary>
  248. /// Contains options for changing how the tree is rendered
  249. /// </summary>
  250. public TreeStyle Style {get;set;} = new TreeStyle();
  251. /// <summary>
  252. /// True to allow multiple objects to be selected at once
  253. /// </summary>
  254. /// <value></value>
  255. public bool MultiSelect {get;set;} = true;
  256. /// <summary>
  257. /// The currently selected object in the tree. When <see cref="MultiSelect"/> is true this is the object at which the cursor is at
  258. /// </summary>
  259. public T SelectedObject {
  260. get => selectedObject;
  261. set {
  262. var oldValue = selectedObject;
  263. selectedObject = value;
  264. if(!ReferenceEquals(oldValue,value))
  265. OnSelectionChanged(new SelectionChangedEventArgs<T>(this,oldValue,value));
  266. }
  267. }
  268. /// <summary>
  269. /// Secondary selected regions of tree when <see cref="MultiSelect"/> is true
  270. /// </summary>
  271. private Stack<TreeSelection<T>> multiSelectedRegions = new Stack<TreeSelection<T>>();
  272. /// <summary>
  273. /// Cached result of <see cref="BuildLineMap"/>
  274. /// </summary>
  275. private Branch<T>[] cachedLineMap;
  276. /// <summary>
  277. /// Error message to display when the control is not properly initialized at draw time (nodes added but no tree builder set)
  278. /// </summary>
  279. public static ustring NoBuilderError = "ERROR: Builder Not Set";
  280. /// <summary>
  281. /// Called when the <see cref="SelectedObject"/> changes
  282. /// </summary>
  283. public event EventHandler<SelectionChangedEventArgs<T>> SelectionChanged;
  284. /// <summary>
  285. /// The root objects in the tree, note that this collection is of root objects only
  286. /// </summary>
  287. public IEnumerable<T> Objects {get=>roots.Keys;}
  288. /// <summary>
  289. /// Map of root objects to the branches under them. All objects have a <see cref="Branch{T}"/> even if that branch has no children
  290. /// </summary>
  291. internal Dictionary<T,Branch<T>> roots {get; set;} = new Dictionary<T, Branch<T>>();
  292. /// <summary>
  293. /// The amount of tree view that has been scrolled off the top of the screen (by the user scrolling down)
  294. /// </summary>
  295. /// <remarks>Setting a value of less than 0 will result in a offset of 0. To see changes in the UI call <see cref="View.SetNeedsDisplay()"/></remarks>
  296. public int ScrollOffsetVertical {
  297. get => scrollOffsetVertical;
  298. set {
  299. scrollOffsetVertical = Math.Max(0,value);
  300. }
  301. }
  302. /// <summary>
  303. /// The amount of tree view that has been scrolled to the right (horizontally)
  304. /// </summary>
  305. /// <remarks>Setting a value of less than 0 will result in a offset of 0. To see changes in the UI call <see cref="View.SetNeedsDisplay()"/></remarks>
  306. public int ScrollOffsetHorizontal {
  307. get => scrollOffsetHorizontal;
  308. set {
  309. scrollOffsetHorizontal = Math.Max(0,value);
  310. }
  311. }
  312. /// <summary>
  313. /// The current number of rows in the tree (ignoring the controls bounds)
  314. /// </summary>
  315. public int ContentHeight => BuildLineMap().Count();
  316. /// <summary>
  317. /// Returns the string representation of model objects hosted in the tree. Default implementation is to call <see cref="object.ToString"/>
  318. /// </summary>
  319. /// <value></value>
  320. public AspectGetterDelegate<T> AspectGetter {get;set;} = (o)=>o.ToString() ?? "";
  321. /// <summary>
  322. /// 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"/>
  323. /// </summary>
  324. public TreeView():base()
  325. {
  326. CanFocus = true;
  327. }
  328. /// <summary>
  329. /// 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.
  330. /// </summary>
  331. public TreeView(ITreeBuilder<T> builder) : this()
  332. {
  333. TreeBuilder = builder;
  334. }
  335. /// <summary>
  336. /// Adds a new root level object unless it is already a root of the tree
  337. /// </summary>
  338. /// <param name="o"></param>
  339. public void AddObject(T o)
  340. {
  341. if(!roots.ContainsKey(o)) {
  342. roots.Add(o,new Branch<T>(this,null,o));
  343. InvalidateLineMap();
  344. SetNeedsDisplay();
  345. }
  346. }
  347. /// <summary>
  348. /// Removes all objects from the tree and clears <see cref="SelectedObject"/>
  349. /// </summary>
  350. public void ClearObjects()
  351. {
  352. SelectedObject = default(T);
  353. multiSelectedRegions.Clear();
  354. roots = new Dictionary<T, Branch<T>>();
  355. InvalidateLineMap();
  356. SetNeedsDisplay();
  357. }
  358. /// <summary>
  359. /// Removes the given root object from the tree
  360. /// </summary>
  361. /// <remarks>If <paramref name="o"/> is the currently <see cref="SelectedObject"/> then the selection is cleared</remarks>
  362. /// <param name="o"></param>
  363. public void Remove(T o)
  364. {
  365. if(roots.ContainsKey(o)) {
  366. roots.Remove(o);
  367. InvalidateLineMap();
  368. SetNeedsDisplay();
  369. if(Equals(SelectedObject,o))
  370. SelectedObject = default(T);
  371. }
  372. }
  373. /// <summary>
  374. /// Adds many new root level objects. Objects that are already root objects are ignored
  375. /// </summary>
  376. /// <param name="collection">Objects to add as new root level objects</param>
  377. public void AddObjects(IEnumerable<T> collection)
  378. {
  379. bool objectsAdded = false;
  380. foreach(var o in collection) {
  381. if (!roots.ContainsKey (o)) {
  382. roots.Add(o,new Branch<T>(this,null,o));
  383. objectsAdded = true;
  384. }
  385. }
  386. if (objectsAdded) {
  387. InvalidateLineMap();
  388. SetNeedsDisplay();
  389. }
  390. }
  391. /// <summary>
  392. /// Refreshes the state of the object <paramref name="o"/> in the tree. This will recompute children, string representation etc
  393. /// </summary>
  394. /// <remarks>This has no effect if the object is not exposed in the tree.</remarks>
  395. /// <param name="o"></param>
  396. /// <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>
  397. public void RefreshObject (T o, bool startAtTop = false)
  398. {
  399. var branch = ObjectToBranch(o);
  400. if(branch != null) {
  401. branch.Refresh(startAtTop);
  402. InvalidateLineMap();
  403. SetNeedsDisplay();
  404. }
  405. }
  406. /// <summary>
  407. /// 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)"/>)
  408. /// </summary>
  409. public void RebuildTree()
  410. {
  411. foreach(var branch in roots.Values)
  412. branch.Rebuild();
  413. InvalidateLineMap();
  414. SetNeedsDisplay();
  415. }
  416. /// <summary>
  417. /// Returns the currently expanded children of the passed object. Returns an empty collection if the branch is not exposed or not expanded
  418. /// </summary>
  419. /// <param name="o">An object in the tree</param>
  420. /// <returns></returns>
  421. public IEnumerable<T> GetChildren (T o)
  422. {
  423. var branch = ObjectToBranch(o);
  424. if(branch == null || !branch.IsExpanded)
  425. return new T[0];
  426. return branch.ChildBranches?.Values?.Select(b=>b.Model)?.ToArray() ?? new T[0];
  427. }
  428. /// <summary>
  429. /// Returns the parent object of <paramref name="o"/> in the tree. Returns null if the object is not exposed in the tree
  430. /// </summary>
  431. /// <param name="o">An object in the tree</param>
  432. /// <returns></returns>
  433. public T GetParent (T o)
  434. {
  435. return ObjectToBranch(o)?.Parent?.Model;
  436. }
  437. ///<inheritdoc/>
  438. public override void Redraw (Rect bounds)
  439. {
  440. if(roots == null)
  441. return;
  442. if(TreeBuilder == null) {
  443. Move(0,0);
  444. Driver.AddStr(NoBuilderError);
  445. return;
  446. }
  447. var map = BuildLineMap();
  448. for(int line = 0 ; line < bounds.Height; line++){
  449. var idxToRender = ScrollOffsetVertical + line;
  450. // Is there part of the tree view to render?
  451. if(idxToRender < map.Length) {
  452. // Render the line
  453. map[idxToRender].Draw(Driver,ColorScheme,line,bounds.Width);
  454. } else {
  455. // Else clear the line to prevent stale symbols due to scrolling etc
  456. Move(0,line);
  457. Driver.SetAttribute(ColorScheme.Normal);
  458. Driver.AddStr(new string(' ',bounds.Width));
  459. }
  460. }
  461. }
  462. /// <summary>
  463. /// 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="ScrollOffsetVertical"/> and <see cref="View.SetNeedsDisplay()"/> to scroll to a specific object
  464. /// </summary>
  465. /// <remarks>Uses the Equals method and returns the first index at which the object is found or -1 if it is not found</remarks>
  466. /// <param name="o">An object that appears in your tree and is currently exposed</param>
  467. /// <returns>The index the object was found at or -1 if it is not currently revealed or not in the tree at all</returns>
  468. public int GetScrollOffsetOf(T o)
  469. {
  470. var map = BuildLineMap();
  471. for (int i = 0; i < map.Length; i++)
  472. {
  473. if (map[i].Model.Equals(o))
  474. return i;
  475. }
  476. //object not found
  477. return -1;
  478. }
  479. /// <summary>
  480. /// Returns the maximum width line in the tree including prefix and expansion symbols
  481. /// </summary>
  482. /// <param name="visible">True to consider only rows currently visible (based on window bounds and <see cref="ScrollOffsetVertical"/>. False to calculate the width of every exposed branch in the tree</param>
  483. /// <returns></returns>
  484. public int GetContentWidth(bool visible){
  485. var map = BuildLineMap();
  486. if(map.Length == 0)
  487. return 0;
  488. if(visible){
  489. //Somehow we managed to scroll off the end of the control
  490. if(ScrollOffsetVertical >= map.Length)
  491. return 0;
  492. // If control has no height to it then there is no visible area for content
  493. if(Bounds.Height == 0)
  494. return 0;
  495. return map.Skip(ScrollOffsetVertical).Take(Bounds.Height).Max(b=>b.GetWidth(Driver));
  496. }
  497. else{
  498. return map.Max(b=>b.GetWidth(Driver));
  499. }
  500. }
  501. /// <summary>
  502. /// Calculates all currently visible/expanded branches (including leafs) and outputs them by index from the top of the screen
  503. /// </summary>
  504. /// <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>
  505. /// <returns></returns>
  506. private Branch<T>[] BuildLineMap()
  507. {
  508. if(cachedLineMap != null)
  509. return cachedLineMap;
  510. List<Branch<T>> toReturn = new List<Branch<T>>();
  511. foreach(var root in roots.Values) {
  512. toReturn.AddRange(AddToLineMap(root));
  513. }
  514. return cachedLineMap = toReturn.ToArray();
  515. }
  516. private IEnumerable<Branch<T>> AddToLineMap (Branch<T> currentBranch)
  517. {
  518. yield return currentBranch;
  519. if(currentBranch.IsExpanded){
  520. foreach(var subBranch in currentBranch.ChildBranches.Values){
  521. foreach(var sub in AddToLineMap(subBranch)) {
  522. yield return sub;
  523. }
  524. }
  525. }
  526. }
  527. /// <inheritdoc/>
  528. public override bool ProcessKey (KeyEvent keyEvent)
  529. {
  530. switch (keyEvent.Key) {
  531. case Key.CursorRight:
  532. Expand(SelectedObject);
  533. break;
  534. case Key.CursorRight | Key.CtrlMask:
  535. ExpandAll(SelectedObject);
  536. break;
  537. case Key.CursorLeft:
  538. case Key.CursorLeft | Key.CtrlMask:
  539. CursorLeft(keyEvent.Key.HasFlag(Key.CtrlMask));
  540. break;
  541. case Key.CursorUp:
  542. case Key.CursorUp | Key.ShiftMask:
  543. AdjustSelection(-1,keyEvent.Key.HasFlag(Key.ShiftMask));
  544. break;
  545. case Key.CursorDown:
  546. case Key.CursorDown | Key.ShiftMask:
  547. AdjustSelection(1,keyEvent.Key.HasFlag(Key.ShiftMask));
  548. break;
  549. case Key.PageUp:
  550. case Key.PageUp | Key.ShiftMask:
  551. AdjustSelection(-Bounds.Height,keyEvent.Key.HasFlag(Key.ShiftMask));
  552. break;
  553. case Key.PageDown:
  554. case Key.PageDown | Key.ShiftMask:
  555. AdjustSelection(Bounds.Height,keyEvent.Key.HasFlag(Key.ShiftMask));
  556. break;
  557. case Key.A | Key.CtrlMask:
  558. SelectAll();
  559. break;
  560. case Key.Home:
  561. GoToFirst();
  562. break;
  563. case Key.End:
  564. GoToEnd();
  565. break;
  566. default:
  567. // we don't care about this keystroke
  568. return false;
  569. }
  570. PositionCursor ();
  571. return true;
  572. }
  573. ///<inheritdoc/>
  574. public override bool MouseEvent (MouseEvent me)
  575. {
  576. if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) &&
  577. me.Flags != MouseFlags.WheeledDown && me.Flags != MouseFlags.WheeledUp && me.Flags != MouseFlags.WheeledRight&& me.Flags != MouseFlags.WheeledLeft)
  578. return false;
  579. if (!HasFocus && CanFocus) {
  580. SetFocus ();
  581. }
  582. if (me.Flags == MouseFlags.WheeledDown) {
  583. ScrollOffsetVertical++;
  584. SetNeedsDisplay();
  585. return true;
  586. } else if (me.Flags == MouseFlags.WheeledUp) {
  587. ScrollOffsetVertical--;
  588. SetNeedsDisplay();
  589. return true;
  590. }
  591. if (me.Flags == MouseFlags.WheeledRight) {
  592. ScrollOffsetHorizontal++;
  593. SetNeedsDisplay();
  594. return true;
  595. } else if (me.Flags == MouseFlags.WheeledLeft) {
  596. ScrollOffsetHorizontal--;
  597. SetNeedsDisplay();
  598. return true;
  599. }
  600. if(me.Flags.HasFlag(MouseFlags.Button1Clicked)) {
  601. var map = BuildLineMap();
  602. var idx = me.Y + ScrollOffsetVertical;
  603. // click is outside any visible nodes
  604. if(idx < 0 || idx >= map.Length) {
  605. return false;
  606. }
  607. // The line they clicked on
  608. var clickedBranch = map[idx];
  609. bool isExpandToggleAttempt = clickedBranch.IsHitOnExpandableSymbol(Driver,me.X);
  610. // If we are already selected (double click)
  611. if(Equals(SelectedObject,clickedBranch.Model))
  612. isExpandToggleAttempt = true;
  613. // if they clicked on the +/- expansion symbol
  614. if( isExpandToggleAttempt) {
  615. if (clickedBranch.IsExpanded) {
  616. clickedBranch.Collapse();
  617. }
  618. else
  619. if(clickedBranch.CanExpand())
  620. clickedBranch.Expand();
  621. else {
  622. SelectedObject = clickedBranch.Model; // It is a leaf node
  623. multiSelectedRegions.Clear();
  624. }
  625. }
  626. else {
  627. // It is a first click somewhere in the current line that doesn't look like an expansion/collapse attempt
  628. SelectedObject = clickedBranch.Model;
  629. multiSelectedRegions.Clear();
  630. }
  631. SetNeedsDisplay();
  632. return true;
  633. }
  634. return false;
  635. }
  636. /// <summary>
  637. /// Positions the cursor at the start of the selected objects line (if visible)
  638. /// </summary>
  639. public override void PositionCursor()
  640. {
  641. if (CanFocus && HasFocus && Visible && SelectedObject != null)
  642. {
  643. var map = BuildLineMap();
  644. var idx = Array.FindIndex(map,b=>b.Model.Equals(SelectedObject));
  645. // if currently selected line is visible
  646. if(idx - ScrollOffsetVertical >= 0 && idx - ScrollOffsetVertical < Bounds.Height)
  647. Move(0,idx - ScrollOffsetVertical);
  648. else
  649. base.PositionCursor();
  650. } else {
  651. base.PositionCursor();
  652. }
  653. }
  654. /// <summary>
  655. /// 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
  656. /// </summary>
  657. protected virtual void CursorLeft(bool ctrl)
  658. {
  659. if(IsExpanded(SelectedObject)) {
  660. if(ctrl)
  661. CollapseAll(SelectedObject);
  662. else
  663. Collapse(SelectedObject);
  664. }
  665. else
  666. {
  667. var parent = GetParent(SelectedObject);
  668. if(parent != null){
  669. SelectedObject = parent;
  670. AdjustSelection(0);
  671. SetNeedsDisplay();
  672. }
  673. }
  674. }
  675. /// <summary>
  676. /// Changes the <see cref="SelectedObject"/> to the first root object and resets the <see cref="ScrollOffsetVertical"/> to 0
  677. /// </summary>
  678. public void GoToFirst()
  679. {
  680. ScrollOffsetVertical = 0;
  681. SelectedObject = roots.Keys.FirstOrDefault();
  682. SetNeedsDisplay();
  683. }
  684. /// <summary>
  685. /// Changes the <see cref="SelectedObject"/> to the last object in the tree and scrolls so that it is visible
  686. /// </summary>
  687. public void GoToEnd ()
  688. {
  689. var map = BuildLineMap();
  690. ScrollOffsetVertical = Math.Max(0,map.Length - Bounds.Height +1);
  691. SelectedObject = map.Last().Model;
  692. SetNeedsDisplay();
  693. }
  694. /// <summary>
  695. /// Changes the selected object by a number of screen lines
  696. /// </summary>
  697. /// <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>
  698. /// <param name="offset"></param>
  699. /// <param name="expandSelection">True to expand the selection (assuming <see cref="MultiSelect"/> is enabled). False to replace</param>
  700. public void AdjustSelection (int offset, bool expandSelection = false)
  701. {
  702. // if it is not a shift click or we don't allow multi select
  703. if(!expandSelection || !MultiSelect)
  704. multiSelectedRegions.Clear();
  705. if(SelectedObject == null){
  706. SelectedObject = roots.Keys.FirstOrDefault();
  707. }
  708. else {
  709. var map = BuildLineMap();
  710. var idx = Array.FindIndex(map,b=>b.Model.Equals(SelectedObject));
  711. if(idx == -1) {
  712. // The current selection has disapeared!
  713. SelectedObject = roots.Keys.FirstOrDefault();
  714. }
  715. else {
  716. var newIdx = Math.Min(Math.Max(0,idx+offset),map.Length-1);
  717. var newBranch = map[newIdx];
  718. // If it is a multi selection
  719. if(expandSelection && MultiSelect)
  720. {
  721. if(multiSelectedRegions.Any())
  722. {
  723. // expand the existing head selection
  724. var head = multiSelectedRegions.Pop();
  725. multiSelectedRegions.Push(new TreeSelection<T>(head.Origin,newIdx,map));
  726. }
  727. else
  728. {
  729. // or start a new multi selection region
  730. multiSelectedRegions.Push(new TreeSelection<T>(map[idx],newIdx,map));
  731. }
  732. }
  733. SelectedObject = newBranch.Model;
  734. /*this -1 allows for possible horizontal scroll bar in the last row of the control*/
  735. int leaveSpace = Style.LeaveLastRow ? 1 :0;
  736. if(newIdx < ScrollOffsetVertical) {
  737. //if user has scrolled up too far to see their selection
  738. ScrollOffsetVertical = newIdx;
  739. }
  740. else if(newIdx >= ScrollOffsetVertical + Bounds.Height - leaveSpace){
  741. //if user has scrolled off bottom of visible tree
  742. ScrollOffsetVertical = Math.Max(0,(newIdx+1) - (Bounds.Height-leaveSpace));
  743. }
  744. }
  745. }
  746. InvalidateLineMap();
  747. SetNeedsDisplay();
  748. }
  749. /// <summary>
  750. /// Expands the supplied object if it is contained in the tree (either as a root object or as an exposed branch object)
  751. /// </summary>
  752. /// <param name="toExpand">The object to expand</param>
  753. public void Expand(T toExpand)
  754. {
  755. if(toExpand == null)
  756. return;
  757. ObjectToBranch(toExpand)?.Expand();
  758. InvalidateLineMap();
  759. SetNeedsDisplay();
  760. }
  761. /// <summary>
  762. /// Expands the supplied object and all child objects
  763. /// </summary>
  764. /// <param name="toExpand">The object to expand</param>
  765. public void ExpandAll(T toExpand)
  766. {
  767. if(toExpand == null)
  768. return;
  769. ObjectToBranch(toExpand)?.ExpandAll();
  770. InvalidateLineMap();
  771. SetNeedsDisplay();
  772. }
  773. /// <summary>
  774. /// Fully expands all nodes in the tree, if the tree is very big and built dynamically this may take a while (e.g. for file system)
  775. /// </summary>
  776. public void ExpandAll()
  777. {
  778. foreach (var item in roots) {
  779. item.Value.ExpandAll();
  780. }
  781. InvalidateLineMap();
  782. SetNeedsDisplay();
  783. }
  784. /// <summary>
  785. /// Returns true if the given object <paramref name="o"/> is exposed in the tree and can be expanded otherwise false
  786. /// </summary>
  787. /// <param name="o"></param>
  788. /// <returns></returns>
  789. public bool CanExpand(T o)
  790. {
  791. return ObjectToBranch(o)?.CanExpand() ?? false;
  792. }
  793. /// <summary>
  794. /// Returns true if the given object <paramref name="o"/> is exposed in the tree and expanded otherwise false
  795. /// </summary>
  796. /// <param name="o"></param>
  797. /// <returns></returns>
  798. public bool IsExpanded(T o)
  799. {
  800. return ObjectToBranch(o)?.IsExpanded ?? false;
  801. }
  802. /// <summary>
  803. /// Collapses the supplied object if it is currently expanded
  804. /// </summary>
  805. /// <param name="toCollapse">The object to collapse</param>
  806. public void Collapse(T toCollapse)
  807. {
  808. CollapseImpl(toCollapse,false);
  809. }
  810. /// <summary>
  811. /// Collapses the supplied object if it is currently expanded. Also collapses all children branches (this will only become apparent when/if the user expands it again)
  812. /// </summary>
  813. /// <param name="toCollapse">The object to collapse</param>
  814. public void CollapseAll(T toCollapse)
  815. {
  816. CollapseImpl(toCollapse,true);
  817. }
  818. /// <summary>
  819. /// Collapses all root nodes in the tree
  820. /// </summary>
  821. public void CollapseAll()
  822. {
  823. foreach (var item in roots) {
  824. item.Value.Collapse();
  825. }
  826. InvalidateLineMap();
  827. SetNeedsDisplay();
  828. }
  829. /// <summary>
  830. /// Implementation of <see cref="Collapse(T)"/> and <see cref="CollapseAll(T)"/>. Performs operation and updates selection if disapeared
  831. /// </summary>
  832. /// <param name="toCollapse"></param>
  833. /// <param name="all"></param>
  834. protected void CollapseImpl(T toCollapse, bool all)
  835. {
  836. if(toCollapse == null)
  837. return;
  838. var branch = ObjectToBranch(toCollapse);
  839. // Nothing to collapse
  840. if(branch == null)
  841. return;
  842. if (all) {
  843. branch.CollapseAll();
  844. } else {
  845. branch.Collapse();
  846. }
  847. if(SelectedObject != null && ObjectToBranch(SelectedObject) == null)
  848. {
  849. // If the old selection suddenly became invalid then clear it
  850. SelectedObject = null;
  851. }
  852. InvalidateLineMap();
  853. SetNeedsDisplay();
  854. }
  855. /// <summary>
  856. /// Clears any cached results of <see cref="BuildLineMap"/>
  857. /// </summary>
  858. protected void InvalidateLineMap()
  859. {
  860. cachedLineMap = null;
  861. }
  862. /// <summary>
  863. /// 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
  864. /// </summary>
  865. /// <param name="toFind"></param>
  866. /// <returns>The branch for <paramref name="toFind"/> or null if it is not currently exposed in the tree</returns>
  867. private Branch<T> ObjectToBranch(T toFind)
  868. {
  869. return BuildLineMap().FirstOrDefault(o=>o.Model.Equals(toFind));
  870. }
  871. /// <summary>
  872. /// Returns true if the <paramref name="model"/> is either the <see cref="SelectedObject"/> or part of a <see cref="MultiSelect"/>
  873. /// </summary>
  874. /// <param name="model"></param>
  875. /// <returns></returns>
  876. public bool IsSelected (T model)
  877. {
  878. return Equals(SelectedObject , model) ||
  879. (MultiSelect && multiSelectedRegions.Any(s=>s.Contains(model)));
  880. }
  881. /// <summary>
  882. /// Returns <see cref="SelectedObject"/> (if not null) and all multi selected objects if <see cref="MultiSelect"/> is true
  883. /// </summary>
  884. /// <returns></returns>
  885. public IEnumerable<T> GetAllSelectedObjects()
  886. {
  887. var map = BuildLineMap();
  888. if(SelectedObject != null)
  889. yield return SelectedObject;
  890. // To determine multi selected objects, start with the line map, that avoids yielding hidden nodes that were selected then the parent collapsed e.g. programmatically or with mouse click
  891. if(MultiSelect){
  892. foreach(var m in map.Select(b=>b.Model).Where(IsSelected)){
  893. if(m != SelectedObject){
  894. yield return m;
  895. }
  896. }
  897. }
  898. }
  899. /// <summary>
  900. /// Selects all objects in the tree when <see cref="MultiSelect"/> is enabled otherwise does nothing
  901. /// </summary>
  902. public void SelectAll()
  903. {
  904. if(!MultiSelect)
  905. return;
  906. multiSelectedRegions.Clear();
  907. var map = BuildLineMap();
  908. if(map.Length == 0)
  909. return;
  910. multiSelectedRegions.Push(new TreeSelection<T>(map[0],map.Length,map));
  911. SetNeedsDisplay();
  912. OnSelectionChanged(new SelectionChangedEventArgs<T>(this,SelectedObject,SelectedObject));
  913. }
  914. /// <summary>
  915. /// Raises the SelectionChanged event
  916. /// </summary>
  917. /// <param name="e"></param>
  918. protected virtual void OnSelectionChanged (SelectionChangedEventArgs<T> e)
  919. {
  920. SelectionChanged?.Invoke(this,e);
  921. }
  922. }
  923. class TreeSelection<T> where T : class {
  924. public Branch<T> Origin {get;}
  925. private HashSet<T> included = new HashSet<T>();
  926. /// <summary>
  927. /// Creates a new selection between two branches in the tree
  928. /// </summary>
  929. /// <param name="from"></param>
  930. /// <param name="toIndex"></param>
  931. /// <param name="map"></param>
  932. public TreeSelection(Branch<T> from, int toIndex, Branch<T>[] map )
  933. {
  934. Origin = from;
  935. included.Add(Origin.Model);
  936. var oldIdx = Array.IndexOf(map,from);
  937. var lowIndex = Math.Min(oldIdx,toIndex);
  938. var highIndex = Math.Max(oldIdx,toIndex);
  939. // Select everything between the old and new indexes
  940. foreach(var alsoInclude in map.Skip(lowIndex).Take(highIndex-lowIndex)){
  941. included.Add(alsoInclude.Model);
  942. }
  943. }
  944. public bool Contains(T model)
  945. {
  946. return included.Contains(model);
  947. }
  948. }
  949. class Branch<T> where T:class
  950. {
  951. /// <summary>
  952. /// True if the branch is expanded to reveal child branches
  953. /// </summary>
  954. public bool IsExpanded {get;set;}
  955. /// <summary>
  956. /// The users object that is being displayed by this branch of the tree
  957. /// </summary>
  958. public T Model {get;private set;}
  959. /// <summary>
  960. /// The depth of the current branch. Depth of 0 indicates root level branches
  961. /// </summary>
  962. public int Depth {get;private set;} = 0;
  963. /// <summary>
  964. /// The children of the current branch. This is null until the first call to <see cref="FetchChildren"/> to avoid enumerating the entire underlying hierarchy
  965. /// </summary>
  966. public Dictionary<T,Branch<T>> ChildBranches {get;set;}
  967. /// <summary>
  968. /// The parent <see cref="Branch{T}"/> or null if it is a root.
  969. /// </summary>
  970. public Branch<T> Parent {get; private set;}
  971. private TreeView<T> tree;
  972. /// <summary>
  973. /// Declares a new branch of <paramref name="tree"/> in which the users object <paramref name="model"/> is presented
  974. /// </summary>
  975. /// <param name="tree">The UI control in which the branch resides</param>
  976. /// <param name="parentBranchIfAny">Pass null for root level branches, otherwise pass the parent</param>
  977. /// <param name="model">The user's object that should be displayed</param>
  978. public Branch(TreeView<T> tree,Branch<T> parentBranchIfAny,T model)
  979. {
  980. this.tree = tree;
  981. this.Model = model;
  982. if(parentBranchIfAny != null) {
  983. Depth = parentBranchIfAny.Depth +1;
  984. Parent = parentBranchIfAny;
  985. }
  986. }
  987. /// <summary>
  988. /// Fetch the children of this branch. This method populates <see cref="ChildBranches"/>
  989. /// </summary>
  990. public virtual void FetchChildren()
  991. {
  992. if (tree.TreeBuilder == null)
  993. return;
  994. var children = tree.TreeBuilder.GetChildren(this.Model) ?? Enumerable.Empty<T>();
  995. this.ChildBranches = children.ToDictionary(k=>k,val=>new Branch<T>(tree,this,val));
  996. }
  997. /// <summary>
  998. /// Returns the width of the line including prefix and the results of <see cref="TreeView{T}.AspectGetter"/> (the line body).
  999. /// </summary>
  1000. /// <returns></returns>
  1001. public virtual int GetWidth (ConsoleDriver driver)
  1002. {
  1003. return
  1004. GetLinePrefix(driver).Sum(Rune.ColumnWidth) +
  1005. Rune.ColumnWidth(GetExpandableSymbol(driver)) +
  1006. (tree.AspectGetter(Model) ?? "").Length;
  1007. }
  1008. /// <summary>
  1009. /// Renders the current <see cref="Model"/> on the specified line <paramref name="y"/>
  1010. /// </summary>
  1011. /// <param name="driver"></param>
  1012. /// <param name="colorScheme"></param>
  1013. /// <param name="y"></param>
  1014. /// <param name="availableWidth"></param>
  1015. public virtual void Draw(ConsoleDriver driver,ColorScheme colorScheme, int y, int availableWidth)
  1016. {
  1017. // true if the current line of the tree is the selected one and control has focus
  1018. bool isSelected = tree.IsSelected(Model) && tree.HasFocus;
  1019. Attribute lineColor = isSelected? colorScheme.Focus : colorScheme.Normal;
  1020. driver.SetAttribute(lineColor);
  1021. // Everything on line before the expansion run and branch text
  1022. Rune[] prefix = GetLinePrefix(driver).ToArray();
  1023. Rune expansion = GetExpandableSymbol(driver);
  1024. string lineBody = tree.AspectGetter(Model) ?? "";
  1025. tree.Move(0,y);
  1026. // if we have scrolled to the right then bits of the prefix will have dispeared off the screen
  1027. int toSkip = tree.ScrollOffsetHorizontal;
  1028. // Draw the line prefix (all paralell lanes or whitespace and an expand/collapse/leaf symbol)
  1029. foreach(Rune r in prefix){
  1030. if(toSkip > 0){
  1031. toSkip--;
  1032. }
  1033. else{
  1034. driver.AddRune(r);
  1035. availableWidth -= Rune.ColumnWidth(r);
  1036. }
  1037. }
  1038. // pick color for expanded symbol
  1039. if(tree.Style.ColorExpandSymbol || tree.Style.InvertExpandSymbolColors)
  1040. {
  1041. Attribute color;
  1042. if(tree.Style.ColorExpandSymbol)
  1043. color = isSelected ? tree.ColorScheme.HotFocus : tree.ColorScheme.HotNormal;
  1044. else
  1045. color = lineColor;
  1046. if(tree.Style.InvertExpandSymbolColors)
  1047. color = new Attribute(color.Background,color.Foreground);
  1048. driver.SetAttribute(color);
  1049. }
  1050. if(toSkip > 0){
  1051. toSkip--;
  1052. }
  1053. else{
  1054. driver.AddRune(expansion);
  1055. availableWidth -= Rune.ColumnWidth(expansion);
  1056. }
  1057. // horizontal scrolling has already skipped the prefix but now must also skip some of the line body
  1058. if(toSkip > 0)
  1059. {
  1060. if(toSkip > lineBody.Length){
  1061. lineBody = "";
  1062. }
  1063. else{
  1064. lineBody = lineBody.Substring(toSkip);
  1065. }
  1066. }
  1067. // If body of line is too long
  1068. if(lineBody.Sum(l=>Rune.ColumnWidth(l)) > availableWidth)
  1069. {
  1070. // remaining space is zero and truncate the line
  1071. lineBody = new string(lineBody.TakeWhile(c=>(availableWidth -= Rune.ColumnWidth(c)) >= 0).ToArray());
  1072. availableWidth = 0;
  1073. }
  1074. else{
  1075. // line is short so remaining width will be whatever comes after the line body
  1076. availableWidth -= lineBody.Length;
  1077. }
  1078. //reset the line color if it was changed for rendering expansion symbol
  1079. driver.SetAttribute(lineColor);
  1080. driver.AddStr(lineBody);
  1081. if(availableWidth > 0)
  1082. driver.AddStr(new string(' ',availableWidth));
  1083. driver.SetAttribute(colorScheme.Normal);
  1084. }
  1085. /// <summary>
  1086. /// Gets all characters to render prior to the current branches line. This includes indentation whitespace and any tree branches (if enabled)
  1087. /// </summary>
  1088. /// <param name="driver"></param>
  1089. /// <returns></returns>
  1090. private IEnumerable<Rune> GetLinePrefix (ConsoleDriver driver)
  1091. {
  1092. // If not showing line branches or this is a root object
  1093. if (!tree.Style.ShowBranchLines) {
  1094. for(int i = 0; i < Depth; i++) {
  1095. yield return new Rune(' ');
  1096. }
  1097. yield break;
  1098. }
  1099. // yield indentations with runes appropriate to the state of the parents
  1100. foreach(var cur in GetParentBranches().Reverse())
  1101. {
  1102. if(cur.IsLast())
  1103. yield return new Rune(' ');
  1104. else
  1105. yield return driver.VLine;
  1106. yield return new Rune(' ');
  1107. }
  1108. if(IsLast())
  1109. yield return driver.LLCorner;
  1110. else
  1111. yield return driver.LeftTee;
  1112. }
  1113. /// <summary>
  1114. /// Returns all parents starting with the immediate parent and ending at the root
  1115. /// </summary>
  1116. /// <returns></returns>
  1117. private IEnumerable<Branch<T>> GetParentBranches()
  1118. {
  1119. var cur = Parent;
  1120. while(cur != null)
  1121. {
  1122. yield return cur;
  1123. cur = cur.Parent;
  1124. }
  1125. }
  1126. /// <summary>
  1127. /// 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)
  1128. /// </summary>
  1129. /// <param name="driver"></param>
  1130. /// <returns></returns>
  1131. public Rune GetExpandableSymbol(ConsoleDriver driver)
  1132. {
  1133. var leafSymbol = tree.Style.ShowBranchLines ? driver.HLine : ' ';
  1134. if(IsExpanded)
  1135. return tree.Style.CollapseableSymbol ?? leafSymbol;
  1136. if(CanExpand())
  1137. return tree.Style.ExpandableSymbol ?? leafSymbol;
  1138. return leafSymbol;
  1139. }
  1140. /// <summary>
  1141. /// Returns true if the current branch can be expanded according to the <see cref="TreeBuilder{T}"/> or cached children already fetched
  1142. /// </summary>
  1143. /// <returns></returns>
  1144. public bool CanExpand ()
  1145. {
  1146. // if we do not know the children yet
  1147. if(ChildBranches == null) {
  1148. //if there is a rapid method for determining whether there are children
  1149. if(tree.TreeBuilder.SupportsCanExpand) {
  1150. return tree.TreeBuilder.CanExpand(Model);
  1151. }
  1152. //there is no way of knowing whether we can expand without fetching the children
  1153. FetchChildren();
  1154. }
  1155. //we fetched or already know the children, so return whether we have any
  1156. return ChildBranches.Any();
  1157. }
  1158. /// <summary>
  1159. /// Expands the current branch if possible
  1160. /// </summary>
  1161. public void Expand()
  1162. {
  1163. if(ChildBranches == null) {
  1164. FetchChildren();
  1165. }
  1166. if (ChildBranches.Any ()) {
  1167. IsExpanded = true;
  1168. }
  1169. }
  1170. /// <summary>
  1171. /// Marks the branch as collapsed (<see cref="IsExpanded"/> false)
  1172. /// </summary>
  1173. public void Collapse ()
  1174. {
  1175. IsExpanded = false;
  1176. }
  1177. /// <summary>
  1178. /// Refreshes cached knowledge in this branch e.g. what children an object has
  1179. /// </summary>
  1180. /// <param name="startAtTop">True to also refresh all <see cref="Parent"/> branches (starting with the root)</param>
  1181. public void Refresh (bool startAtTop)
  1182. {
  1183. // if we must go up and refresh from the top down
  1184. if(startAtTop)
  1185. Parent?.Refresh(true);
  1186. // we don't want to loose the state of our children so lets be selective about how we refresh
  1187. //if we don't know about any children yet just use the normal method
  1188. if(ChildBranches == null)
  1189. FetchChildren();
  1190. else {
  1191. // we already knew about some children so preserve the state of the old children
  1192. // first gather the new Children
  1193. var newChildren = tree.TreeBuilder?.GetChildren(this.Model) ?? Enumerable.Empty<T>();
  1194. // Children who no longer appear need to go
  1195. foreach(var toRemove in ChildBranches.Keys.Except(newChildren).ToArray())
  1196. {
  1197. ChildBranches.Remove(toRemove);
  1198. //also if the user has this node selected (its disapearing) so lets change selection to us (the parent object) to be helpful
  1199. if(Equals(tree.SelectedObject ,toRemove))
  1200. tree.SelectedObject = Model;
  1201. }
  1202. // New children need to be added
  1203. foreach(var newChild in newChildren)
  1204. {
  1205. // If we don't know about the child yet we need a new branch
  1206. if (!ChildBranches.ContainsKey (newChild)) {
  1207. ChildBranches.Add(newChild,new Branch<T>(tree,this,newChild));
  1208. }
  1209. else{
  1210. //we already have this object but update the reference anyway incase Equality match but the references are new
  1211. ChildBranches[newChild].Model = newChild;
  1212. }
  1213. }
  1214. }
  1215. }
  1216. /// <summary>
  1217. /// Calls <see cref="Refresh(bool)"/> on the current branch and all expanded children
  1218. /// </summary>
  1219. internal void Rebuild()
  1220. {
  1221. Refresh(false);
  1222. // if we know about our children
  1223. if(ChildBranches != null) {
  1224. if(IsExpanded) {
  1225. //if we are expanded we need to updatethe visible children
  1226. foreach(var child in ChildBranches) {
  1227. child.Value.Rebuild();
  1228. }
  1229. }
  1230. else {
  1231. // we are not expanded so should forget about children because they may not exist anymore
  1232. ChildBranches = null;
  1233. }
  1234. }
  1235. }
  1236. /// <summary>
  1237. /// Returns true if this branch has parents and it is the last node of it's parents branches (or last root of the tree)
  1238. /// </summary>
  1239. /// <returns></returns>
  1240. private bool IsLast()
  1241. {
  1242. if(Parent == null)
  1243. return this == tree.roots.Values.LastOrDefault();
  1244. return Parent.ChildBranches.Values.LastOrDefault() == this;
  1245. }
  1246. /// <summary>
  1247. /// Returns true if the given x offset on the branch line is the +/- symbol. Returns false if not showing expansion symbols or leaf node etc
  1248. /// </summary>
  1249. /// <param name="driver"></param>
  1250. /// <param name="x"></param>
  1251. /// <returns></returns>
  1252. internal bool IsHitOnExpandableSymbol (ConsoleDriver driver, int x)
  1253. {
  1254. // if leaf node then we cannot expand
  1255. if(!CanExpand())
  1256. return false;
  1257. // if we could theoretically expand
  1258. if(!IsExpanded && tree.Style.ExpandableSymbol != null) {
  1259. return x == GetLinePrefix(driver).Count();
  1260. }
  1261. // if we could theoretically collapse
  1262. if(IsExpanded && tree.Style.CollapseableSymbol != null) {
  1263. return x == GetLinePrefix(driver).Count();
  1264. }
  1265. return false;
  1266. }
  1267. /// <summary>
  1268. /// Expands the current branch and all children branches
  1269. /// </summary>
  1270. internal void ExpandAll ()
  1271. {
  1272. Expand();
  1273. if(ChildBranches != null)
  1274. foreach (var child in ChildBranches) {
  1275. child.Value.ExpandAll();
  1276. }
  1277. }
  1278. /// <summary>
  1279. /// Collapses the current branch and all children branches (even though those branches are no longer visible they retain collapse/expansion state)
  1280. /// </summary>
  1281. internal void CollapseAll ()
  1282. {
  1283. Collapse();
  1284. if(ChildBranches != null)
  1285. foreach (var child in ChildBranches) {
  1286. child.Value.CollapseAll();
  1287. }
  1288. }
  1289. }
  1290. /// <summary>
  1291. /// Delegates of this type are used to fetch string representations of user's model objects
  1292. /// </summary>
  1293. /// <param name="model"></param>
  1294. /// <returns></returns>
  1295. public delegate string AspectGetterDelegate<T>(T model) where T:class;
  1296. /// <summary>
  1297. /// Event arguments describing a change in selected object in a tree view
  1298. /// </summary>
  1299. public class SelectionChangedEventArgs<T> : EventArgs where T:class
  1300. {
  1301. /// <summary>
  1302. /// The view in which the change occurred
  1303. /// </summary>
  1304. public TreeView<T> Tree { get; }
  1305. /// <summary>
  1306. /// The previously selected value (can be null)
  1307. /// </summary>
  1308. public T OldValue { get; }
  1309. /// <summary>
  1310. /// The newly selected value in the <see cref="Tree"/> (can be null)
  1311. /// </summary>
  1312. public T NewValue { get; }
  1313. /// <summary>
  1314. /// Creates a new instance of event args describing a change of selection in <paramref name="tree"/>
  1315. /// </summary>
  1316. /// <param name="tree"></param>
  1317. /// <param name="oldValue"></param>
  1318. /// <param name="newValue"></param>
  1319. public SelectionChangedEventArgs(TreeView<T> tree, T oldValue, T newValue)
  1320. {
  1321. Tree = tree;
  1322. OldValue = oldValue;
  1323. NewValue = newValue;
  1324. }
  1325. }
  1326. }