TreeView.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  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. /// The root objects in the tree, note that this collection is of root objects only
  55. /// </summary>
  56. public IEnumerable<object> Objects {get=>roots.Keys;}
  57. /// <summary>
  58. /// Map of root objects to the branches under them. All objects have a <see cref="Branch"/> even if that branch has no children
  59. /// </summary>
  60. Dictionary<object,Branch> roots {get; set;} = new Dictionary<object, Branch>();
  61. /// <summary>
  62. /// The amount of tree view that has been scrolled off the top of the screen (by the user scrolling down)
  63. /// </summary>
  64. /// <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>
  65. public int ScrollOffset {
  66. get => scrollOffset;
  67. set {
  68. scrollOffset = Math.Max(0,value);
  69. }
  70. }
  71. /// <summary>
  72. /// Creates a new tree view with absolute positioning. Use <see cref="AddObjects(IEnumerable{object})"/> to set set root objects for the tree
  73. /// </summary>
  74. public TreeView ():base()
  75. {
  76. CanFocus = true;
  77. }
  78. /// <summary>
  79. /// Adds a new root level object unless it is already a root of the tree
  80. /// </summary>
  81. /// <param name="o"></param>
  82. public void AddObject(object o)
  83. {
  84. if(!roots.ContainsKey(o)) {
  85. roots.Add(o,new Branch(this,null,o));
  86. SetNeedsDisplay();
  87. }
  88. }
  89. /// <summary>
  90. /// Removes all objects from the tree and clears <see cref="SelectedObject"/>
  91. /// </summary>
  92. public void ClearObjects()
  93. {
  94. SelectedObject = null;
  95. roots = new Dictionary<object, Branch>();
  96. SetNeedsDisplay();
  97. }
  98. /// <summary>
  99. /// Removes the given root object from the tree
  100. /// </summary>
  101. /// <remarks>If <paramref name="o"/> is the currently <see cref="SelectedObject"/> then the selection is cleared</remarks>
  102. /// <param name="o"></param>
  103. public void Remove(object o)
  104. {
  105. if(roots.ContainsKey(o)) {
  106. roots.Remove(o);
  107. SetNeedsDisplay();
  108. if(Equals(SelectedObject,o))
  109. SelectedObject = null;
  110. }
  111. }
  112. /// <summary>
  113. /// Adds many new root level objects. Objects that are already root objects are ignored
  114. /// </summary>
  115. /// <param name="collection">Objects to add as new root level objects</param>
  116. public void AddObjects(IEnumerable<object> collection)
  117. {
  118. bool objectsAdded = false;
  119. foreach(var o in collection) {
  120. if (!roots.ContainsKey (o)) {
  121. roots.Add(o,new Branch(this,null,o));
  122. objectsAdded = true;
  123. }
  124. }
  125. if(objectsAdded)
  126. SetNeedsDisplay();
  127. }
  128. /// <summary>
  129. /// Returns the string representation of model objects hosted in the tree. Default implementation is to call <see cref="object.ToString"/>
  130. /// </summary>
  131. /// <value></value>
  132. public AspectGetterDelegate AspectGetter {get;set;} = (o)=>o.ToString();
  133. ///<inheritdoc/>
  134. public override void Redraw (Rect bounds)
  135. {
  136. if(roots == null)
  137. return;
  138. var map = BuildLineMap();
  139. for(int line = 0 ; line < bounds.Height; line++){
  140. var idxToRender = ScrollOffset + line;
  141. // Is there part of the tree view to render?
  142. if(idxToRender < map.Length) {
  143. // Render the line
  144. map[idxToRender].Draw(Driver,ColorScheme,line,bounds.Width);
  145. } else {
  146. // Else clear the line to prevent stale symbols due to scrolling etc
  147. Move(0,line);
  148. Driver.SetAttribute(ColorScheme.Normal);
  149. Driver.AddStr(new string(' ',bounds.Width));
  150. }
  151. }
  152. }
  153. /// <summary>
  154. /// 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
  155. /// </summary>
  156. /// <remarks>Uses the Equals method and returns the first index at which the object is found or -1 if it is not found</remarks>
  157. /// <param name="o">An object that appears in your tree and is currently exposed</param>
  158. /// <returns>The index the object was found at or -1 if it is not currently revealed or not in the tree at all</returns>
  159. public int GetScrollOffsetOf(object o)
  160. {
  161. var map = BuildLineMap();
  162. for (int i = 0; i < map.Length; i++)
  163. {
  164. if (map[i].Model.Equals(o))
  165. return i;
  166. }
  167. //object not found
  168. return -1;
  169. }
  170. /// <summary>
  171. /// Calculates all currently visible/expanded branches (including leafs) and outputs them by index from the top of the screen
  172. /// </summary>
  173. /// <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>
  174. /// <returns></returns>
  175. private Branch[] BuildLineMap()
  176. {
  177. List<Branch> toReturn = new List<Branch>();
  178. foreach(var root in roots.Values) {
  179. toReturn.AddRange(AddToLineMap(root));
  180. }
  181. return toReturn.ToArray();
  182. }
  183. private IEnumerable<Branch> AddToLineMap (Branch currentBranch)
  184. {
  185. yield return currentBranch;
  186. if(currentBranch.IsExpanded){
  187. foreach(var subBranch in currentBranch.ChildBranches.Values){
  188. foreach(var sub in AddToLineMap(subBranch)) {
  189. yield return sub;
  190. }
  191. }
  192. }
  193. }
  194. /// <summary>
  195. /// Symbol to use for expanded branch nodes to indicate to the user that they can be collapsed. Defaults to '-'
  196. /// </summary>
  197. public char ExpandedSymbol {get;set;} = '-';
  198. /// <summary>
  199. /// Symbol to use for branch nodes that can be expanded to indicate this to the user. Defaults to '+'
  200. /// </summary>
  201. public char ExpandableSymbol {get;set;} = '+';
  202. /// <summary>
  203. /// Symbol to use for branch nodes that cannot be expanded (as they have no children). Defaults to space ' '
  204. /// </summary>
  205. public char LeafSymbol {get;set;} = ' ';
  206. /// <inheritdoc/>
  207. public override bool ProcessKey (KeyEvent keyEvent)
  208. {
  209. switch (keyEvent.Key) {
  210. case Key.CursorRight:
  211. Expand(SelectedObject);
  212. break;
  213. case Key.CursorLeft:
  214. Collapse(SelectedObject);
  215. break;
  216. case Key.CursorUp:
  217. AdjustSelection(-1);
  218. break;
  219. case Key.CursorDown:
  220. AdjustSelection(1);
  221. break;
  222. case Key.PageUp:
  223. AdjustSelection(-Bounds.Height);
  224. break;
  225. case Key.PageDown:
  226. AdjustSelection(Bounds.Height);
  227. break;
  228. case Key.Home:
  229. GoToFirst();
  230. break;
  231. case Key.End:
  232. GoToEnd();
  233. break;
  234. default:
  235. // we don't care about this keystroke
  236. return false;
  237. }
  238. PositionCursor ();
  239. return true;
  240. }
  241. /// <summary>
  242. /// Changes the <see cref="SelectedObject"/> to the first root object and resets the <see cref="ScrollOffset"/> to 0
  243. /// </summary>
  244. public void GoToFirst()
  245. {
  246. ScrollOffset = 0;
  247. SelectedObject = roots.Keys.FirstOrDefault();
  248. SetNeedsDisplay();
  249. }
  250. /// <summary>
  251. /// Changes the <see cref="SelectedObject"/> to the last object in the tree and scrolls so that it is visible
  252. /// </summary>
  253. public void GoToEnd ()
  254. {
  255. var map = BuildLineMap();
  256. ScrollOffset = Math.Max(0,map.Length - Bounds.Height +1);
  257. SelectedObject = map.Last().Model;
  258. SetNeedsDisplay();
  259. }
  260. /// <summary>
  261. /// Changes the selected object by a number of screen lines
  262. /// </summary>
  263. /// <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>
  264. /// <param name="offset"></param>
  265. private void AdjustSelection (int offset)
  266. {
  267. if(SelectedObject == null){
  268. SelectedObject = roots.Keys.FirstOrDefault();
  269. }
  270. else {
  271. var map = BuildLineMap();
  272. var idx = Array.FindIndex(map,b=>b.Model.Equals(SelectedObject));
  273. if(idx == -1) {
  274. // The current selection has disapeared!
  275. SelectedObject = roots.Keys.FirstOrDefault();
  276. }
  277. else {
  278. var newIdx = Math.Min(Math.Max(0,idx+offset),map.Length-1);
  279. SelectedObject = map[newIdx].Model;
  280. if(newIdx < ScrollOffset) {
  281. //if user has scrolled up too far to see their selection
  282. ScrollOffset = newIdx;
  283. }
  284. else if(newIdx >= ScrollOffset + Bounds.Height){
  285. //if user has scrolled off bottom of visible tree
  286. ScrollOffset = Math.Max(0,(newIdx+1) - Bounds.Height);
  287. }
  288. }
  289. }
  290. SetNeedsDisplay();
  291. }
  292. /// <summary>
  293. /// Expands the supplied object if it is contained in the tree (either as a root object or as an exposed branch object)
  294. /// </summary>
  295. /// <param name="toExpand">The object to expand</param>
  296. public void Expand(object toExpand)
  297. {
  298. if(toExpand == null)
  299. return;
  300. ObjectToBranch(toExpand)?.Expand();
  301. SetNeedsDisplay();
  302. }
  303. /// <summary>
  304. /// Collapses the supplied object if it is currently expanded
  305. /// </summary>
  306. /// <param name="toCollapse">The object to collapse</param>
  307. public void Collapse(object toCollapse)
  308. {
  309. if(toCollapse == null)
  310. return;
  311. ObjectToBranch(toCollapse)?.Collapse();
  312. SetNeedsDisplay();
  313. }
  314. /// <summary>
  315. /// 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
  316. /// </summary>
  317. /// <param name="toFind"></param>
  318. /// <returns>The branch for <paramref name="toFind"/> or null if it is not currently exposed in the tree</returns>
  319. private Branch ObjectToBranch(object toFind)
  320. {
  321. return BuildLineMap().FirstOrDefault(o=>o.Model.Equals(toFind));
  322. }
  323. }
  324. class Branch
  325. {
  326. /// <summary>
  327. /// True if the branch is expanded to reveal child branches
  328. /// </summary>
  329. public bool IsExpanded {get;set;}
  330. /// <summary>
  331. /// The users object that is being displayed by this branch of the tree
  332. /// </summary>
  333. public object Model {get;set;}
  334. /// <summary>
  335. /// The depth of the current branch. Depth of 0 indicates root level branches
  336. /// </summary>
  337. public int Depth {get;set;} = 0;
  338. /// <summary>
  339. /// The children of the current branch. This is null until the first call to <see cref="FetchChildren"/> to avoid enumerating the entire underlying hierarchy
  340. /// </summary>
  341. public Dictionary<object,Branch> ChildBranches {get;set;}
  342. private TreeView tree;
  343. /// <summary>
  344. /// Declares a new branch of <paramref name="tree"/> in which the users object <paramref name="model"/> is presented
  345. /// </summary>
  346. /// <param name="tree">The UI control in which the branch resides</param>
  347. /// <param name="parentBranchIfAny">Pass null for root level branches, otherwise pass the parent</param>
  348. /// <param name="model">The user's object that should be displayed</param>
  349. public Branch(TreeView tree,Branch parentBranchIfAny,object model)
  350. {
  351. this.tree = tree;
  352. this.Model = model;
  353. if(parentBranchIfAny != null) {
  354. Depth = parentBranchIfAny.Depth +1;
  355. }
  356. }
  357. /// <summary>
  358. /// Fetch the children of this branch. This method populates <see cref="ChildBranches"/>
  359. /// </summary>
  360. public virtual void FetchChildren()
  361. {
  362. if (tree.ChildrenGetter == null)
  363. return;
  364. this.ChildBranches = tree.ChildrenGetter(this.Model).ToDictionary(k=>k,val=>new Branch(tree,this,val));
  365. }
  366. /// <summary>
  367. /// Renders the current <see cref="Model"/> on the specified line <paramref name="y"/>
  368. /// </summary>
  369. /// <param name="driver"></param>
  370. /// <param name="colorScheme"></param>
  371. /// <param name="y"></param>
  372. /// <param name="availableWidth"></param>
  373. public virtual void Draw(ConsoleDriver driver,ColorScheme colorScheme, int y, int availableWidth)
  374. {
  375. string representation = new string(' ',Depth) + GetExpandableIcon() + tree.AspectGetter(Model);
  376. tree.Move(0,y);
  377. driver.SetAttribute(tree.SelectedObject == Model ?
  378. colorScheme.HotFocus :
  379. colorScheme.Normal);
  380. driver.AddStr(representation.PadRight(availableWidth));
  381. }
  382. /// <summary>
  383. /// 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)
  384. /// </summary>
  385. /// <returns></returns>
  386. public char GetExpandableIcon()
  387. {
  388. if(IsExpanded)
  389. return tree.ExpandedSymbol;
  390. if(ChildBranches == null) {
  391. //if there is a rapid method for determining whether there are children
  392. if(tree.CanExpandGetter != null) {
  393. return tree.CanExpandGetter(Model) ? tree.ExpandableSymbol : tree.LeafSymbol;
  394. }
  395. //there is no way of knowing whether we can expand without fetching the children
  396. FetchChildren();
  397. }
  398. //we fetched or already know the children, so return whether we are a leaf or a expandable branch
  399. return ChildBranches.Any() ? tree.ExpandableSymbol : tree.LeafSymbol;
  400. }
  401. /// <summary>
  402. /// Expands the current branch if possible
  403. /// </summary>
  404. public void Expand()
  405. {
  406. if(ChildBranches == null) {
  407. FetchChildren();
  408. }
  409. if (ChildBranches.Any ()) {
  410. IsExpanded = true;
  411. }
  412. }
  413. internal void Collapse ()
  414. {
  415. IsExpanded = false;
  416. }
  417. }
  418. /// <summary>
  419. /// Delegates of this type are used to fetch the children of the given model object
  420. /// </summary>
  421. /// <param name="model">The parent whose children should be fetched</param>
  422. /// <returns>An enumerable over the children</returns>
  423. public delegate IEnumerable<object> ChildrenGetterDelegate(object model);
  424. /// <summary>
  425. /// Delegates of this type are used to fetch string representations of user's model objects
  426. /// </summary>
  427. /// <param name="model"></param>
  428. /// <returns></returns>
  429. public delegate string AspectGetterDelegate(object model);
  430. /// <summary>
  431. /// 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)
  432. /// </summary>
  433. /// <param name="model"></param>
  434. /// <returns></returns>
  435. public delegate bool CanExpandGetterDelegate(object model);
  436. /// <summary>
  437. /// Event arguments describing a change in selected object in a tree view
  438. /// </summary>
  439. public class SelectionChangedEventArgs : EventArgs
  440. {
  441. /// <summary>
  442. /// The view in which the change occurred
  443. /// </summary>
  444. public TreeView Tree { get; }
  445. /// <summary>
  446. /// The previously selected value (can be null)
  447. /// </summary>
  448. public object OldValue { get; }
  449. /// <summary>
  450. /// The newly selected value in the <see cref="Tree"/> (can be null)
  451. /// </summary>
  452. public object NewValue { get; }
  453. public SelectionChangedEventArgs(TreeView tree, object oldValue, object newValue)
  454. {
  455. Tree = tree;
  456. OldValue = oldValue;
  457. NewValue = newValue;
  458. }
  459. }
  460. }