TreeView.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. // This code is based on http://objectlistview.sourceforge.net (GPLv3 tree/list controls by [email protected])
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. namespace Terminal.Gui {
  6. /// <summary>
  7. /// Hierarchical tree view with expandable branches. Branch objects are dynamically determined when expanded using a user defined <see cref="ChildrenGetterDelegate"/>
  8. /// </summary>
  9. public class TreeView : View
  10. {
  11. /// <summary>
  12. /// Default implementation of a <see cref="ChildrenGetterDelegate"/>, returns an empty collection (i.e. no children)
  13. /// </summary>
  14. static ChildrenGetterDelegate DefaultChildrenGetter = (s)=>{return new object[0];};
  15. /// <summary>
  16. /// This is the delegate that will be used to fetch the children of a model object
  17. /// </summary>
  18. public ChildrenGetterDelegate ChildrenGetter {
  19. get { return childrenGetter ?? DefaultChildrenGetter; }
  20. set { childrenGetter = value; }
  21. }
  22. private ChildrenGetterDelegate childrenGetter;
  23. private CanExpandGetterDelegate canExpandGetter;
  24. private int scrollOffset;
  25. /// <summary>
  26. /// Optional delegate where <see cref="ChildrenGetter"/> is expensive. This should quickly return true/false for whether an object is expandable. (e.g. indicating to a user that all folders can be expanded because they are folders without having to calculate contents)
  27. /// </summary>
  28. /// <remarks>When this is null <see cref="ChildrenGetter"/> is used directly to determine if a node should be expandable</remarks>
  29. public CanExpandGetterDelegate CanExpandGetter {
  30. get { return canExpandGetter; }
  31. set { canExpandGetter = value; }
  32. }
  33. /// <summary>
  34. /// private variable for <see cref="SelectedObject"/>
  35. /// </summary>
  36. object selectedObject;
  37. /// <summary>
  38. /// The currently selected object in the tree
  39. /// </summary>
  40. public object SelectedObject {
  41. get => selectedObject;
  42. set {
  43. var oldValue = selectedObject;
  44. selectedObject = value;
  45. if(!ReferenceEquals(oldValue,value))
  46. SelectionChanged?.Invoke(this,new SelectionChangedEventArgs(this,oldValue,value));
  47. }
  48. }
  49. /// <summary>
  50. /// Called when the <see cref="SelectedObject"/> changes
  51. /// </summary>
  52. public event EventHandler<SelectionChangedEventArgs> SelectionChanged;
  53. /// <summary>
  54. /// Refreshes the state of the object <paramref name="o"/> in the tree. This will recompute children, string representation etc
  55. /// </summary>
  56. /// <remarks>This has no effect if the object is not exposed in the tree.</remarks>
  57. /// <param name="o"></param>
  58. /// <param name="startAtTop">True to also refresh all ancestors of the objects branch (starting with the root). False to refresh only the passed node</param>
  59. public void RefreshObject (object o, bool startAtTop = false)
  60. {
  61. var branch = ObjectToBranch(o);
  62. if(branch != null) {
  63. branch.Refresh(startAtTop);
  64. SetNeedsDisplay();
  65. }
  66. }
  67. /// <summary>
  68. /// The root objects in the tree, note that this collection is of root objects only
  69. /// </summary>
  70. public IEnumerable<object> Objects {get=>roots.Keys;}
  71. /// <summary>
  72. /// Map of root objects to the branches under them. All objects have a <see cref="Branch"/> even if that branch has no children
  73. /// </summary>
  74. Dictionary<object,Branch> roots {get; set;} = new Dictionary<object, Branch>();
  75. /// <summary>
  76. /// The amount of tree view that has been scrolled off the top of the screen (by the user scrolling down)
  77. /// </summary>
  78. /// <remarks>Setting a value of less than 0 will result in a ScrollOffset of 0. To see changes in the UI call <see cref="View.SetNeedsDisplay()"/></remarks>
  79. public int ScrollOffset {
  80. get => scrollOffset;
  81. set {
  82. scrollOffset = Math.Max(0,value);
  83. }
  84. }
  85. /// <summary>
  86. /// Creates a new tree view with absolute positioning. Use <see cref="AddObjects(IEnumerable{object})"/> to set set root objects for the tree
  87. /// </summary>
  88. public TreeView ():base()
  89. {
  90. CanFocus = true;
  91. }
  92. /// <summary>
  93. /// Adds a new root level object unless it is already a root of the tree
  94. /// </summary>
  95. /// <param name="o"></param>
  96. public void AddObject(object o)
  97. {
  98. if(!roots.ContainsKey(o)) {
  99. roots.Add(o,new Branch(this,null,o));
  100. SetNeedsDisplay();
  101. }
  102. }
  103. /// <summary>
  104. /// Removes all objects from the tree and clears <see cref="SelectedObject"/>
  105. /// </summary>
  106. public void ClearObjects()
  107. {
  108. SelectedObject = null;
  109. roots = new Dictionary<object, Branch>();
  110. SetNeedsDisplay();
  111. }
  112. /// <summary>
  113. /// Removes the given root object from the tree
  114. /// </summary>
  115. /// <remarks>If <paramref name="o"/> is the currently <see cref="SelectedObject"/> then the selection is cleared</remarks>
  116. /// <param name="o"></param>
  117. public void Remove(object o)
  118. {
  119. if(roots.ContainsKey(o)) {
  120. roots.Remove(o);
  121. SetNeedsDisplay();
  122. if(Equals(SelectedObject,o))
  123. SelectedObject = null;
  124. }
  125. }
  126. /// <summary>
  127. /// Adds many new root level objects. Objects that are already root objects are ignored
  128. /// </summary>
  129. /// <param name="collection">Objects to add as new root level objects</param>
  130. public void AddObjects(IEnumerable<object> collection)
  131. {
  132. bool objectsAdded = false;
  133. foreach(var o in collection) {
  134. if (!roots.ContainsKey (o)) {
  135. roots.Add(o,new Branch(this,null,o));
  136. objectsAdded = true;
  137. }
  138. }
  139. if(objectsAdded)
  140. SetNeedsDisplay();
  141. }
  142. /// <summary>
  143. /// Returns the currently expanded children of the passed object. Returns an empty collection if the branch is not exposed or not expanded
  144. /// </summary>
  145. /// <param name="o">An object in the tree</param>
  146. /// <returns></returns>
  147. public IEnumerable<object> GetChildren (object o)
  148. {
  149. var branch = ObjectToBranch(o);
  150. if(branch == null || !branch.IsExpanded)
  151. return new object[0];
  152. return branch.ChildBranches?.Values?.Select(b=>b.Model)?.ToArray() ?? new object[0];
  153. }
  154. /// <summary>
  155. /// Returns the parent object of <paramref name="o"/> in the tree. Returns null if the object is not exposed in the tree
  156. /// </summary>
  157. /// <param name="o">An object in the tree</param>
  158. /// <returns></returns>
  159. public object GetParent (object o)
  160. {
  161. return ObjectToBranch(o)?.Parent?.Model;
  162. }
  163. /// <summary>
  164. /// Returns the string representation of model objects hosted in the tree. Default implementation is to call <see cref="object.ToString"/>
  165. /// </summary>
  166. /// <value></value>
  167. public AspectGetterDelegate AspectGetter {get;set;} = (o)=>o.ToString();
  168. ///<inheritdoc/>
  169. public override void Redraw (Rect bounds)
  170. {
  171. if(roots == null)
  172. return;
  173. var map = BuildLineMap();
  174. for(int line = 0 ; line < bounds.Height; line++){
  175. var idxToRender = ScrollOffset + line;
  176. // Is there part of the tree view to render?
  177. if(idxToRender < map.Length) {
  178. // Render the line
  179. map[idxToRender].Draw(Driver,ColorScheme,line,bounds.Width);
  180. } else {
  181. // Else clear the line to prevent stale symbols due to scrolling etc
  182. Move(0,line);
  183. Driver.SetAttribute(ColorScheme.Normal);
  184. Driver.AddStr(new string(' ',bounds.Width));
  185. }
  186. }
  187. }
  188. /// <summary>
  189. /// Returns the index of the object <paramref name="o"/> if it is currently exposed (it's parent(s) have been expanded). This can be used with <see cref="ScrollOffset"/> and <see cref="View.SetNeedsDisplay()"/> to scroll to a specific object
  190. /// </summary>
  191. /// <remarks>Uses the Equals method and returns the first index at which the object is found or -1 if it is not found</remarks>
  192. /// <param name="o">An object that appears in your tree and is currently exposed</param>
  193. /// <returns>The index the object was found at or -1 if it is not currently revealed or not in the tree at all</returns>
  194. public int GetScrollOffsetOf(object o)
  195. {
  196. var map = BuildLineMap();
  197. for (int i = 0; i < map.Length; i++)
  198. {
  199. if (map[i].Model.Equals(o))
  200. return i;
  201. }
  202. //object not found
  203. return -1;
  204. }
  205. /// <summary>
  206. /// Calculates all currently visible/expanded branches (including leafs) and outputs them by index from the top of the screen
  207. /// </summary>
  208. /// <remarks>Index 0 of the returned array is the first item that should be visible in the top of the control, index 1 is the next etc.</remarks>
  209. /// <returns></returns>
  210. private Branch[] BuildLineMap()
  211. {
  212. List<Branch> toReturn = new List<Branch>();
  213. foreach(var root in roots.Values) {
  214. toReturn.AddRange(AddToLineMap(root));
  215. }
  216. return toReturn.ToArray();
  217. }
  218. private IEnumerable<Branch> AddToLineMap (Branch currentBranch)
  219. {
  220. yield return currentBranch;
  221. if(currentBranch.IsExpanded){
  222. foreach(var subBranch in currentBranch.ChildBranches.Values){
  223. foreach(var sub in AddToLineMap(subBranch)) {
  224. yield return sub;
  225. }
  226. }
  227. }
  228. }
  229. /// <summary>
  230. /// Symbol to use for expanded branch nodes to indicate to the user that they can be collapsed. Defaults to '-'
  231. /// </summary>
  232. public char ExpandedSymbol {get;set;} = '-';
  233. /// <summary>
  234. /// Symbol to use for branch nodes that can be expanded to indicate this to the user. Defaults to '+'
  235. /// </summary>
  236. public char ExpandableSymbol {get;set;} = '+';
  237. /// <summary>
  238. /// Symbol to use for branch nodes that cannot be expanded (as they have no children). Defaults to space ' '
  239. /// </summary>
  240. public char LeafSymbol {get;set;} = ' ';
  241. /// <inheritdoc/>
  242. public override bool ProcessKey (KeyEvent keyEvent)
  243. {
  244. switch (keyEvent.Key) {
  245. case Key.CursorRight:
  246. Expand(SelectedObject);
  247. break;
  248. case Key.CursorLeft:
  249. Collapse(SelectedObject);
  250. break;
  251. case Key.CursorUp:
  252. AdjustSelection(-1);
  253. break;
  254. case Key.CursorDown:
  255. AdjustSelection(1);
  256. break;
  257. case Key.PageUp:
  258. AdjustSelection(-Bounds.Height);
  259. break;
  260. case Key.PageDown:
  261. AdjustSelection(Bounds.Height);
  262. break;
  263. case Key.Home:
  264. GoToFirst();
  265. break;
  266. case Key.End:
  267. GoToEnd();
  268. break;
  269. default:
  270. // we don't care about this keystroke
  271. return false;
  272. }
  273. PositionCursor ();
  274. return true;
  275. }
  276. /// <summary>
  277. /// Changes the <see cref="SelectedObject"/> to the first root object and resets the <see cref="ScrollOffset"/> to 0
  278. /// </summary>
  279. public void GoToFirst()
  280. {
  281. ScrollOffset = 0;
  282. SelectedObject = roots.Keys.FirstOrDefault();
  283. SetNeedsDisplay();
  284. }
  285. /// <summary>
  286. /// Changes the <see cref="SelectedObject"/> to the last object in the tree and scrolls so that it is visible
  287. /// </summary>
  288. public void GoToEnd ()
  289. {
  290. var map = BuildLineMap();
  291. ScrollOffset = Math.Max(0,map.Length - Bounds.Height +1);
  292. SelectedObject = map.Last().Model;
  293. SetNeedsDisplay();
  294. }
  295. /// <summary>
  296. /// Changes the selected object by a number of screen lines
  297. /// </summary>
  298. /// <remarks>If nothing is currently selected the first root is selected. If the selected object is no longer in the tree the first object is selected</remarks>
  299. /// <param name="offset"></param>
  300. private void AdjustSelection (int offset)
  301. {
  302. if(SelectedObject == null){
  303. SelectedObject = roots.Keys.FirstOrDefault();
  304. }
  305. else {
  306. var map = BuildLineMap();
  307. var idx = Array.FindIndex(map,b=>b.Model.Equals(SelectedObject));
  308. if(idx == -1) {
  309. // The current selection has disapeared!
  310. SelectedObject = roots.Keys.FirstOrDefault();
  311. }
  312. else {
  313. var newIdx = Math.Min(Math.Max(0,idx+offset),map.Length-1);
  314. SelectedObject = map[newIdx].Model;
  315. if(newIdx < ScrollOffset) {
  316. //if user has scrolled up too far to see their selection
  317. ScrollOffset = newIdx;
  318. }
  319. else if(newIdx >= ScrollOffset + Bounds.Height){
  320. //if user has scrolled off bottom of visible tree
  321. ScrollOffset = Math.Max(0,(newIdx+1) - Bounds.Height);
  322. }
  323. }
  324. }
  325. SetNeedsDisplay();
  326. }
  327. /// <summary>
  328. /// Expands the supplied object if it is contained in the tree (either as a root object or as an exposed branch object)
  329. /// </summary>
  330. /// <param name="toExpand">The object to expand</param>
  331. public void Expand(object toExpand)
  332. {
  333. if(toExpand == null)
  334. return;
  335. ObjectToBranch(toExpand)?.Expand();
  336. SetNeedsDisplay();
  337. }
  338. /// <summary>
  339. /// Returns true if the given object <paramref name="o"/> is exposed in the tree and expanded otherwise false
  340. /// </summary>
  341. /// <param name="o"></param>
  342. /// <returns></returns>
  343. public bool IsExpanded(object o)
  344. {
  345. return ObjectToBranch(o)?.IsExpanded ?? false;
  346. }
  347. /// <summary>
  348. /// Collapses the supplied object if it is currently expanded
  349. /// </summary>
  350. /// <param name="toCollapse">The object to collapse</param>
  351. public void Collapse(object toCollapse)
  352. {
  353. if(toCollapse == null)
  354. return;
  355. ObjectToBranch(toCollapse)?.Collapse();
  356. SetNeedsDisplay();
  357. }
  358. /// <summary>
  359. /// Returns the corresponding <see cref="Branch"/> in the tree for <paramref name="toFind"/>. This will not work for objects hidden by their parent being collapsed
  360. /// </summary>
  361. /// <param name="toFind"></param>
  362. /// <returns>The branch for <paramref name="toFind"/> or null if it is not currently exposed in the tree</returns>
  363. private Branch ObjectToBranch(object toFind)
  364. {
  365. return BuildLineMap().FirstOrDefault(o=>o.Model.Equals(toFind));
  366. }
  367. }
  368. class Branch
  369. {
  370. /// <summary>
  371. /// True if the branch is expanded to reveal child branches
  372. /// </summary>
  373. public bool IsExpanded {get;set;}
  374. /// <summary>
  375. /// The users object that is being displayed by this branch of the tree
  376. /// </summary>
  377. public object Model {get;private set;}
  378. /// <summary>
  379. /// The depth of the current branch. Depth of 0 indicates root level branches
  380. /// </summary>
  381. public int Depth {get;private set;} = 0;
  382. /// <summary>
  383. /// The children of the current branch. This is null until the first call to <see cref="FetchChildren"/> to avoid enumerating the entire underlying hierarchy
  384. /// </summary>
  385. public Dictionary<object,Branch> ChildBranches {get;set;}
  386. /// <summary>
  387. /// The parent <see cref="Branch"/> or null if it is a root.
  388. /// </summary>
  389. public Branch Parent {get; private set;}
  390. private TreeView tree;
  391. /// <summary>
  392. /// Declares a new branch of <paramref name="tree"/> in which the users object <paramref name="model"/> is presented
  393. /// </summary>
  394. /// <param name="tree">The UI control in which the branch resides</param>
  395. /// <param name="parentBranchIfAny">Pass null for root level branches, otherwise pass the parent</param>
  396. /// <param name="model">The user's object that should be displayed</param>
  397. public Branch(TreeView tree,Branch parentBranchIfAny,object model)
  398. {
  399. this.tree = tree;
  400. this.Model = model;
  401. if(parentBranchIfAny != null) {
  402. Depth = parentBranchIfAny.Depth +1;
  403. Parent = parentBranchIfAny;
  404. }
  405. }
  406. /// <summary>
  407. /// Fetch the children of this branch. This method populates <see cref="ChildBranches"/>
  408. /// </summary>
  409. public virtual void FetchChildren()
  410. {
  411. if (tree.ChildrenGetter == null)
  412. return;
  413. var children = tree.ChildrenGetter(this.Model) ?? new object[0];
  414. this.ChildBranches = children.ToDictionary(k=>k,val=>new Branch(tree,this,val));
  415. }
  416. /// <summary>
  417. /// Renders the current <see cref="Model"/> on the specified line <paramref name="y"/>
  418. /// </summary>
  419. /// <param name="driver"></param>
  420. /// <param name="colorScheme"></param>
  421. /// <param name="y"></param>
  422. /// <param name="availableWidth"></param>
  423. public virtual void Draw(ConsoleDriver driver,ColorScheme colorScheme, int y, int availableWidth)
  424. {
  425. string representation = new string(' ',Depth) + GetExpandableIcon() + tree.AspectGetter(Model);
  426. tree.Move(0,y);
  427. driver.SetAttribute(tree.SelectedObject == Model ?
  428. colorScheme.HotFocus :
  429. colorScheme.Normal);
  430. driver.AddStr(representation.PadRight(availableWidth));
  431. }
  432. /// <summary>
  433. /// Returns an appropriate symbol for displaying next to the string representation of the <see cref="Model"/> object to indicate whether it <see cref="IsExpanded"/> or not (or it is a leaf)
  434. /// </summary>
  435. /// <returns></returns>
  436. public char GetExpandableIcon()
  437. {
  438. if(IsExpanded)
  439. return tree.ExpandedSymbol;
  440. if(ChildBranches == null) {
  441. //if there is a rapid method for determining whether there are children
  442. if(tree.CanExpandGetter != null) {
  443. return tree.CanExpandGetter(Model) ? tree.ExpandableSymbol : tree.LeafSymbol;
  444. }
  445. //there is no way of knowing whether we can expand without fetching the children
  446. FetchChildren();
  447. }
  448. //we fetched or already know the children, so return whether we are a leaf or a expandable branch
  449. return ChildBranches.Any() ? tree.ExpandableSymbol : tree.LeafSymbol;
  450. }
  451. /// <summary>
  452. /// Expands the current branch if possible
  453. /// </summary>
  454. public void Expand()
  455. {
  456. if(ChildBranches == null) {
  457. FetchChildren();
  458. }
  459. if (ChildBranches.Any ()) {
  460. IsExpanded = true;
  461. }
  462. }
  463. /// <summary>
  464. /// Marks the branch as collapsed (<see cref="IsExpanded"/> false)
  465. /// </summary>
  466. public void Collapse ()
  467. {
  468. IsExpanded = false;
  469. }
  470. /// <summary>
  471. /// Refreshes cached knowledge in this branch e.g. what children an object has
  472. /// </summary>
  473. /// <param name="startAtTop">True to also refresh all <see cref="Parent"/> branches (starting with the root)</param>
  474. public void Refresh (bool startAtTop)
  475. {
  476. // if we must go up and refresh from the top down
  477. if(startAtTop)
  478. Parent?.Refresh(true);
  479. // we don't want to loose the state of our children so lets be selective about how we refresh
  480. //if we don't know about any children yet just use the normal method
  481. if(ChildBranches == null)
  482. FetchChildren();
  483. else {
  484. // we already knew about some children so preserve the state of the old children
  485. // first gather the new Children
  486. var newChildren = tree.ChildrenGetter(this.Model) ?? new object[0];
  487. // Children who no longer appear need to go
  488. foreach(var toRemove in ChildBranches.Keys.Except(newChildren).ToArray())
  489. {
  490. ChildBranches.Remove(toRemove);
  491. //also if the user has this node selected (its disapearing) so lets change selection to us (the parent object) to be helpful
  492. if(Equals(tree.SelectedObject ,toRemove))
  493. tree.SelectedObject = Model;
  494. }
  495. // New children need to be added
  496. foreach(var newChild in newChildren)
  497. {
  498. // If we don't know about the child yet we need a new branch
  499. if (!ChildBranches.ContainsKey (newChild)) {
  500. ChildBranches.Add(newChild,new Branch(tree,this,newChild));
  501. }
  502. else{
  503. //we already have this object but update the reference anyway incase Equality match but the references are new
  504. ChildBranches[newChild].Model = newChild;
  505. }
  506. }
  507. }
  508. }
  509. }
  510. /// <summary>
  511. /// Delegates of this type are used to fetch the children of the given model object
  512. /// </summary>
  513. /// <param name="model">The parent whose children should be fetched</param>
  514. /// <returns>An enumerable over the children</returns>
  515. public delegate IEnumerable<object> ChildrenGetterDelegate(object model);
  516. /// <summary>
  517. /// Delegates of this type are used to fetch string representations of user's model objects
  518. /// </summary>
  519. /// <param name="model"></param>
  520. /// <returns></returns>
  521. public delegate string AspectGetterDelegate(object model);
  522. /// <summary>
  523. /// Delegates of this type are used to quickly display to the user whether a given user object can be expanded when fetching it's children is expensive (e.g. indicating to a user that all 1000 folders can be expanded because they are folders without having to calculate contents)
  524. /// </summary>
  525. /// <param name="model"></param>
  526. /// <returns></returns>
  527. public delegate bool CanExpandGetterDelegate(object model);
  528. /// <summary>
  529. /// Event arguments describing a change in selected object in a tree view
  530. /// </summary>
  531. public class SelectionChangedEventArgs : EventArgs
  532. {
  533. /// <summary>
  534. /// The view in which the change occurred
  535. /// </summary>
  536. public TreeView Tree { get; }
  537. /// <summary>
  538. /// The previously selected value (can be null)
  539. /// </summary>
  540. public object OldValue { get; }
  541. /// <summary>
  542. /// The newly selected value in the <see cref="Tree"/> (can be null)
  543. /// </summary>
  544. public object NewValue { get; }
  545. /// <summary>
  546. /// Creates a new instance of event args describing a change of selection in <paramref name="tree"/>
  547. /// </summary>
  548. /// <param name="tree"></param>
  549. /// <param name="oldValue"></param>
  550. /// <param name="newValue"></param>
  551. public SelectionChangedEventArgs(TreeView tree, object oldValue, object newValue)
  552. {
  553. Tree = tree;
  554. OldValue = oldValue;
  555. NewValue = newValue;
  556. }
  557. }
  558. }