TreeView.cs 47 KB

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