TreeTableSource.cs 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data;
  4. using System.Linq;
  5. using System.Text;
  6. namespace Terminal.Gui;
  7. /// <summary>
  8. /// An <see cref="ITableSource"/> with expandable rows.
  9. /// </summary>
  10. /// <typeparam name="T"></typeparam>
  11. public class TreeTableSource<T> : IEnumerableTableSource<T>, IDisposable where T : class {
  12. private TreeView<T> _tree;
  13. private string [] _cols;
  14. private Dictionary<string, Func<T, object>> _lamdas;
  15. private TableView _tableView;
  16. /// <summary>
  17. /// Creates a new instance of <see cref="TreeTableSource{T}"/> presenting the given
  18. /// <paramref name="tree"/>. This source should only be used with <paramref name="table"/>.
  19. /// </summary>
  20. /// <param name="table">The table this source will provide data for.</param>
  21. /// <param name="firstColumnName">Column name to use for the first column of the table (where
  22. /// the tree branches/leaves will be rendered.</param>
  23. /// <param name="tree">The tree data to render. This should be a new view and not used
  24. /// elsewhere (e.g. via <see cref="View.Add(View)"/>).</param>
  25. /// <param name="subsequentColumns">
  26. /// Getter methods for each additional property you want to present in the table. For example:
  27. /// <code>
  28. /// new () {
  29. /// { "Colname1", (t)=>t.SomeField},
  30. /// { "Colname2", (t)=>t.SomeOtherField}
  31. ///}
  32. /// </code></param>
  33. public TreeTableSource (TableView table, string firstColumnName, TreeView<T> tree, Dictionary<string, Func<T, object>> subsequentColumns)
  34. {
  35. _tableView = table;
  36. _tree = tree;
  37. _tableView.KeyDown += Table_KeyPress;
  38. _tableView.MouseClick += Table_MouseClick;
  39. var colList = subsequentColumns.Keys.ToList ();
  40. colList.Insert (0, firstColumnName);
  41. _cols = colList.ToArray ();
  42. _lamdas = subsequentColumns;
  43. }
  44. /// <inheritdoc/>
  45. public object this [int row, int col] =>
  46. col == 0 ? GetColumnZeroRepresentationFromTree (row) :
  47. _lamdas [ColumnNames [col]] (RowToObject (row));
  48. /// <inheritdoc/>
  49. public int Rows => _tree.BuildLineMap ().Count;
  50. /// <inheritdoc/>
  51. public int Columns => _lamdas.Count + 1;
  52. /// <inheritdoc/>
  53. public string [] ColumnNames => _cols;
  54. /// <inheritdoc/>
  55. public void Dispose ()
  56. {
  57. _tableView.KeyDown -= Table_KeyPress;
  58. _tableView.MouseClick -= Table_MouseClick;
  59. _tree.Dispose ();
  60. }
  61. /// <summary>
  62. /// Returns the tree model object rendering on the given <paramref name="row"/>
  63. /// of the table.
  64. /// </summary>
  65. /// <param name="row">Row in table.</param>
  66. /// <returns></returns>
  67. public T RowToObject (int row)
  68. {
  69. return _tree.BuildLineMap ().ElementAt (row).Model;
  70. }
  71. private string GetColumnZeroRepresentationFromTree (int row)
  72. {
  73. var branch = RowToBranch (row);
  74. // Everything on line before the expansion run and branch text
  75. Rune [] prefix = branch.GetLinePrefix (Application.Driver).ToArray ();
  76. Rune expansion = branch.GetExpandableSymbol (Application.Driver);
  77. string lineBody = _tree.AspectGetter (branch.Model) ?? "";
  78. var sb = new StringBuilder ();
  79. foreach (var p in prefix) {
  80. sb.Append (p);
  81. }
  82. sb.Append (expansion);
  83. sb.Append (lineBody);
  84. return sb.ToString ();
  85. }
  86. private void Table_KeyPress (object sender, Key e)
  87. {
  88. if (!IsInTreeColumn (_tableView.SelectedColumn, true)) {
  89. return;
  90. }
  91. var obj = _tree.GetObjectOnRow (_tableView.SelectedRow);
  92. if (obj == null) {
  93. return;
  94. }
  95. if (e.KeyCode == KeyCode.CursorLeft) {
  96. if (_tree.IsExpanded (obj)) {
  97. _tree.Collapse (obj);
  98. e.Handled = true;
  99. }
  100. }
  101. if (e.KeyCode == KeyCode.CursorRight) {
  102. if (_tree.CanExpand (obj) && !_tree.IsExpanded (obj)) {
  103. _tree.Expand (obj);
  104. e.Handled = true;
  105. }
  106. }
  107. if (e.Handled) {
  108. _tree.InvalidateLineMap ();
  109. _tableView.SetNeedsDisplay ();
  110. }
  111. }
  112. private void Table_MouseClick (object sender, MouseEventEventArgs e)
  113. {
  114. var hit = _tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out var headerIfAny, out var offsetX);
  115. if (hit == null || headerIfAny != null || !IsInTreeColumn (hit.Value.X, false) || offsetX == null) {
  116. return;
  117. }
  118. var branch = RowToBranch (hit.Value.Y);
  119. if (branch.IsHitOnExpandableSymbol (Application.Driver, offsetX.Value)) {
  120. var m = branch.Model;
  121. if (_tree.CanExpand (m) && !_tree.IsExpanded (m)) {
  122. _tree.Expand (m);
  123. e.Handled = true;
  124. } else if (_tree.IsExpanded (m)) {
  125. _tree.Collapse (m);
  126. e.Handled = true;
  127. }
  128. }
  129. if (e.Handled) {
  130. _tree.InvalidateLineMap ();
  131. _tableView.SetNeedsDisplay ();
  132. }
  133. }
  134. private Branch<T> RowToBranch (int row)
  135. {
  136. return _tree.BuildLineMap ().ElementAt (row);
  137. }
  138. private bool IsInTreeColumn (int column, bool isKeyboard)
  139. {
  140. var colNames = _tableView.Table.ColumnNames;
  141. if (column < 0 || column >= colNames.Length) {
  142. return false;
  143. }
  144. // if full row is selected then it is hard to tell which sub cell in the tree
  145. // has focus so we should typically just always respond with expand/collapse
  146. if (_tableView.FullRowSelect && isKeyboard) {
  147. return true;
  148. }
  149. // we cannot just check that SelectedColumn is 0 because source may
  150. // be wrapped e.g. with a CheckBoxTableSourceWrapperBase
  151. return colNames [column] == _cols [0];
  152. }
  153. /// <inheritdoc/>
  154. public T GetObjectOnRow (int row)
  155. {
  156. return RowToObject (row);
  157. }
  158. /// <inheritdoc/>
  159. public IEnumerable<T> GetAllObjects ()
  160. {
  161. return _tree.BuildLineMap ().Select (b => b.Model);
  162. }
  163. }