TreeView.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  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 string representation of model objects hosted in the tree. Default implementation is to call <see cref="object.ToString"/>
  144. /// </summary>
  145. /// <value></value>
  146. public AspectGetterDelegate AspectGetter {get;set;} = (o)=>o.ToString();
  147. ///<inheritdoc/>
  148. public override void Redraw (Rect bounds)
  149. {
  150. if(roots == null)
  151. return;
  152. var map = BuildLineMap();
  153. for(int line = 0 ; line < bounds.Height; line++){
  154. var idxToRender = ScrollOffset + line;
  155. // Is there part of the tree view to render?
  156. if(idxToRender < map.Length) {
  157. // Render the line
  158. map[idxToRender].Draw(Driver,ColorScheme,line,bounds.Width);
  159. } else {
  160. // Else clear the line to prevent stale symbols due to scrolling etc
  161. Move(0,line);
  162. Driver.SetAttribute(ColorScheme.Normal);
  163. Driver.AddStr(new string(' ',bounds.Width));
  164. }
  165. }
  166. }
  167. /// <summary>
  168. /// 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
  169. /// </summary>
  170. /// <remarks>Uses the Equals method and returns the first index at which the object is found or -1 if it is not found</remarks>
  171. /// <param name="o">An object that appears in your tree and is currently exposed</param>
  172. /// <returns>The index the object was found at or -1 if it is not currently revealed or not in the tree at all</returns>
  173. public int GetScrollOffsetOf(object o)
  174. {
  175. var map = BuildLineMap();
  176. for (int i = 0; i < map.Length; i++)
  177. {
  178. if (map[i].Model.Equals(o))
  179. return i;
  180. }
  181. //object not found
  182. return -1;
  183. }
  184. /// <summary>
  185. /// Calculates all currently visible/expanded branches (including leafs) and outputs them by index from the top of the screen
  186. /// </summary>
  187. /// <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>
  188. /// <returns></returns>
  189. private Branch[] BuildLineMap()
  190. {
  191. List<Branch> toReturn = new List<Branch>();
  192. foreach(var root in roots.Values) {
  193. toReturn.AddRange(AddToLineMap(root));
  194. }
  195. return toReturn.ToArray();
  196. }
  197. private IEnumerable<Branch> AddToLineMap (Branch currentBranch)
  198. {
  199. yield return currentBranch;
  200. if(currentBranch.IsExpanded){
  201. foreach(var subBranch in currentBranch.ChildBranches.Values){
  202. foreach(var sub in AddToLineMap(subBranch)) {
  203. yield return sub;
  204. }
  205. }
  206. }
  207. }
  208. /// <summary>
  209. /// Symbol to use for expanded branch nodes to indicate to the user that they can be collapsed. Defaults to '-'
  210. /// </summary>
  211. public char ExpandedSymbol {get;set;} = '-';
  212. /// <summary>
  213. /// Symbol to use for branch nodes that can be expanded to indicate this to the user. Defaults to '+'
  214. /// </summary>
  215. public char ExpandableSymbol {get;set;} = '+';
  216. /// <summary>
  217. /// Symbol to use for branch nodes that cannot be expanded (as they have no children). Defaults to space ' '
  218. /// </summary>
  219. public char LeafSymbol {get;set;} = ' ';
  220. /// <inheritdoc/>
  221. public override bool ProcessKey (KeyEvent keyEvent)
  222. {
  223. switch (keyEvent.Key) {
  224. case Key.CursorRight:
  225. Expand(SelectedObject);
  226. break;
  227. case Key.CursorLeft:
  228. Collapse(SelectedObject);
  229. break;
  230. case Key.CursorUp:
  231. AdjustSelection(-1);
  232. break;
  233. case Key.CursorDown:
  234. AdjustSelection(1);
  235. break;
  236. case Key.PageUp:
  237. AdjustSelection(-Bounds.Height);
  238. break;
  239. case Key.PageDown:
  240. AdjustSelection(Bounds.Height);
  241. break;
  242. case Key.Home:
  243. GoToFirst();
  244. break;
  245. case Key.End:
  246. GoToEnd();
  247. break;
  248. default:
  249. // we don't care about this keystroke
  250. return false;
  251. }
  252. PositionCursor ();
  253. return true;
  254. }
  255. /// <summary>
  256. /// Changes the <see cref="SelectedObject"/> to the first root object and resets the <see cref="ScrollOffset"/> to 0
  257. /// </summary>
  258. public void GoToFirst()
  259. {
  260. ScrollOffset = 0;
  261. SelectedObject = roots.Keys.FirstOrDefault();
  262. SetNeedsDisplay();
  263. }
  264. /// <summary>
  265. /// Changes the <see cref="SelectedObject"/> to the last object in the tree and scrolls so that it is visible
  266. /// </summary>
  267. public void GoToEnd ()
  268. {
  269. var map = BuildLineMap();
  270. ScrollOffset = Math.Max(0,map.Length - Bounds.Height +1);
  271. SelectedObject = map.Last().Model;
  272. SetNeedsDisplay();
  273. }
  274. /// <summary>
  275. /// Changes the selected object by a number of screen lines
  276. /// </summary>
  277. /// <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>
  278. /// <param name="offset"></param>
  279. private void AdjustSelection (int offset)
  280. {
  281. if(SelectedObject == null){
  282. SelectedObject = roots.Keys.FirstOrDefault();
  283. }
  284. else {
  285. var map = BuildLineMap();
  286. var idx = Array.FindIndex(map,b=>b.Model.Equals(SelectedObject));
  287. if(idx == -1) {
  288. // The current selection has disapeared!
  289. SelectedObject = roots.Keys.FirstOrDefault();
  290. }
  291. else {
  292. var newIdx = Math.Min(Math.Max(0,idx+offset),map.Length-1);
  293. SelectedObject = map[newIdx].Model;
  294. if(newIdx < ScrollOffset) {
  295. //if user has scrolled up too far to see their selection
  296. ScrollOffset = newIdx;
  297. }
  298. else if(newIdx >= ScrollOffset + Bounds.Height){
  299. //if user has scrolled off bottom of visible tree
  300. ScrollOffset = Math.Max(0,(newIdx+1) - Bounds.Height);
  301. }
  302. }
  303. }
  304. SetNeedsDisplay();
  305. }
  306. /// <summary>
  307. /// Expands the supplied object if it is contained in the tree (either as a root object or as an exposed branch object)
  308. /// </summary>
  309. /// <param name="toExpand">The object to expand</param>
  310. public void Expand(object toExpand)
  311. {
  312. if(toExpand == null)
  313. return;
  314. ObjectToBranch(toExpand)?.Expand();
  315. SetNeedsDisplay();
  316. }
  317. /// <summary>
  318. /// Returns true if the given object <paramref name="o"/> is exposed in the tree and expanded otherwise false
  319. /// </summary>
  320. /// <param name="o"></param>
  321. /// <returns></returns>
  322. public bool IsExpanded(object o)
  323. {
  324. return ObjectToBranch(o)?.IsExpanded ?? false;
  325. }
  326. /// <summary>
  327. /// Collapses the supplied object if it is currently expanded
  328. /// </summary>
  329. /// <param name="toCollapse">The object to collapse</param>
  330. public void Collapse(object toCollapse)
  331. {
  332. if(toCollapse == null)
  333. return;
  334. ObjectToBranch(toCollapse)?.Collapse();
  335. SetNeedsDisplay();
  336. }
  337. /// <summary>
  338. /// 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
  339. /// </summary>
  340. /// <param name="toFind"></param>
  341. /// <returns>The branch for <paramref name="toFind"/> or null if it is not currently exposed in the tree</returns>
  342. private Branch ObjectToBranch(object toFind)
  343. {
  344. return BuildLineMap().FirstOrDefault(o=>o.Model.Equals(toFind));
  345. }
  346. }
  347. class Branch
  348. {
  349. /// <summary>
  350. /// True if the branch is expanded to reveal child branches
  351. /// </summary>
  352. public bool IsExpanded {get;set;}
  353. /// <summary>
  354. /// The users object that is being displayed by this branch of the tree
  355. /// </summary>
  356. public object Model {get;private set;}
  357. /// <summary>
  358. /// The depth of the current branch. Depth of 0 indicates root level branches
  359. /// </summary>
  360. public int Depth {get;private set;} = 0;
  361. /// <summary>
  362. /// The children of the current branch. This is null until the first call to <see cref="FetchChildren"/> to avoid enumerating the entire underlying hierarchy
  363. /// </summary>
  364. public Dictionary<object,Branch> ChildBranches {get;set;}
  365. /// <summary>
  366. /// The parent <see cref="Branch"/> or null if it is a root.
  367. /// </summary>
  368. public Branch Parent {get; private set;}
  369. private TreeView tree;
  370. /// <summary>
  371. /// Declares a new branch of <paramref name="tree"/> in which the users object <paramref name="model"/> is presented
  372. /// </summary>
  373. /// <param name="tree">The UI control in which the branch resides</param>
  374. /// <param name="parentBranchIfAny">Pass null for root level branches, otherwise pass the parent</param>
  375. /// <param name="model">The user's object that should be displayed</param>
  376. public Branch(TreeView tree,Branch parentBranchIfAny,object model)
  377. {
  378. this.tree = tree;
  379. this.Model = model;
  380. if(parentBranchIfAny != null) {
  381. Depth = parentBranchIfAny.Depth +1;
  382. Parent = parentBranchIfAny;
  383. }
  384. }
  385. /// <summary>
  386. /// Fetch the children of this branch. This method populates <see cref="ChildBranches"/>
  387. /// </summary>
  388. public virtual void FetchChildren()
  389. {
  390. if (tree.ChildrenGetter == null)
  391. return;
  392. var children = tree.ChildrenGetter(this.Model) ?? new object[0];
  393. this.ChildBranches = children.ToDictionary(k=>k,val=>new Branch(tree,this,val));
  394. }
  395. /// <summary>
  396. /// Renders the current <see cref="Model"/> on the specified line <paramref name="y"/>
  397. /// </summary>
  398. /// <param name="driver"></param>
  399. /// <param name="colorScheme"></param>
  400. /// <param name="y"></param>
  401. /// <param name="availableWidth"></param>
  402. public virtual void Draw(ConsoleDriver driver,ColorScheme colorScheme, int y, int availableWidth)
  403. {
  404. string representation = new string(' ',Depth) + GetExpandableIcon() + tree.AspectGetter(Model);
  405. tree.Move(0,y);
  406. driver.SetAttribute(tree.SelectedObject == Model ?
  407. colorScheme.HotFocus :
  408. colorScheme.Normal);
  409. driver.AddStr(representation.PadRight(availableWidth));
  410. }
  411. /// <summary>
  412. /// 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)
  413. /// </summary>
  414. /// <returns></returns>
  415. public char GetExpandableIcon()
  416. {
  417. if(IsExpanded)
  418. return tree.ExpandedSymbol;
  419. if(ChildBranches == null) {
  420. //if there is a rapid method for determining whether there are children
  421. if(tree.CanExpandGetter != null) {
  422. return tree.CanExpandGetter(Model) ? tree.ExpandableSymbol : tree.LeafSymbol;
  423. }
  424. //there is no way of knowing whether we can expand without fetching the children
  425. FetchChildren();
  426. }
  427. //we fetched or already know the children, so return whether we are a leaf or a expandable branch
  428. return ChildBranches.Any() ? tree.ExpandableSymbol : tree.LeafSymbol;
  429. }
  430. /// <summary>
  431. /// Expands the current branch if possible
  432. /// </summary>
  433. public void Expand()
  434. {
  435. if(ChildBranches == null) {
  436. FetchChildren();
  437. }
  438. if (ChildBranches.Any ()) {
  439. IsExpanded = true;
  440. }
  441. }
  442. /// <summary>
  443. /// Marks the branch as collapsed (<see cref="IsExpanded"/> false)
  444. /// </summary>
  445. public void Collapse ()
  446. {
  447. IsExpanded = false;
  448. }
  449. /// <summary>
  450. /// Refreshes cached knowledge in this branch e.g. what children an object has
  451. /// </summary>
  452. /// <param name="startAtTop">True to also refresh all <see cref="Parent"/> branches (starting with the root)</param>
  453. public void Refresh (bool startAtTop)
  454. {
  455. // if we must go up and refresh from the top down
  456. if(startAtTop)
  457. Parent?.Refresh(true);
  458. // we don't want to loose the state of our children so lets be selective about how we refresh
  459. //if we don't know about any children yet just use the normal method
  460. if(ChildBranches == null)
  461. FetchChildren();
  462. else {
  463. // we already knew about some children so preserve the state of the old children
  464. // first gather the new Children
  465. var newChildren = tree.ChildrenGetter(this.Model) ?? new object[0];
  466. // Children who no longer appear need to go
  467. foreach(var toRemove in ChildBranches.Keys.Except(newChildren).ToArray())
  468. {
  469. ChildBranches.Remove(toRemove);
  470. //also if the user has this node selected (its disapearing) so lets change selection to us (the parent object) to be helpful
  471. if(Equals(tree.SelectedObject ,toRemove))
  472. tree.SelectedObject = Model;
  473. }
  474. // New children need to be added
  475. foreach(var toAdd in newChildren.Except(ChildBranches.Keys).ToArray())
  476. ChildBranches.Add(toAdd,new Branch(tree,this,toAdd));
  477. }
  478. }
  479. }
  480. /// <summary>
  481. /// Delegates of this type are used to fetch the children of the given model object
  482. /// </summary>
  483. /// <param name="model">The parent whose children should be fetched</param>
  484. /// <returns>An enumerable over the children</returns>
  485. public delegate IEnumerable<object> ChildrenGetterDelegate(object model);
  486. /// <summary>
  487. /// Delegates of this type are used to fetch string representations of user's model objects
  488. /// </summary>
  489. /// <param name="model"></param>
  490. /// <returns></returns>
  491. public delegate string AspectGetterDelegate(object model);
  492. /// <summary>
  493. /// 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)
  494. /// </summary>
  495. /// <param name="model"></param>
  496. /// <returns></returns>
  497. public delegate bool CanExpandGetterDelegate(object model);
  498. /// <summary>
  499. /// Event arguments describing a change in selected object in a tree view
  500. /// </summary>
  501. public class SelectionChangedEventArgs : EventArgs
  502. {
  503. /// <summary>
  504. /// The view in which the change occurred
  505. /// </summary>
  506. public TreeView Tree { get; }
  507. /// <summary>
  508. /// The previously selected value (can be null)
  509. /// </summary>
  510. public object OldValue { get; }
  511. /// <summary>
  512. /// The newly selected value in the <see cref="Tree"/> (can be null)
  513. /// </summary>
  514. public object NewValue { get; }
  515. /// <summary>
  516. /// Creates a new instance of event args describing a change of selection in <paramref name="tree"/>
  517. /// </summary>
  518. /// <param name="tree"></param>
  519. /// <param name="oldValue"></param>
  520. /// <param name="newValue"></param>
  521. public SelectionChangedEventArgs(TreeView tree, object oldValue, object newValue)
  522. {
  523. Tree = tree;
  524. OldValue = oldValue;
  525. NewValue = newValue;
  526. }
  527. }
  528. }