TreeView.cs 15 KB

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