TreeView.cs 54 KB

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