TableView.cs 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. using NStack;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Data;
  5. using System.Linq;
  6. namespace Terminal.Gui.Views {
  7. /// <summary>
  8. /// View for tabular data based on a <see cref="DataTable"/>
  9. /// </summary>
  10. public class TableView : View {
  11. private int columnOffset;
  12. private int rowOffset;
  13. private int selectedRow;
  14. private int selectedColumn;
  15. private DataTable table;
  16. /// <summary>
  17. /// The data table to render in the view. Setting this property automatically updates and redraws the control.
  18. /// </summary>
  19. public DataTable Table { get => table; set {table = value; Update(); } }
  20. /// <summary>
  21. /// Zero indexed offset for the upper left <see cref="DataColumn"/> to display in <see cref="Table"/>.
  22. /// </summary>
  23. /// <remarks>This property allows very wide tables to be rendered with horizontal scrolling</remarks>
  24. public int ColumnOffset {
  25. get => columnOffset;
  26. //try to prevent this being set to an out of bounds column
  27. set => columnOffset = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value));
  28. }
  29. /// <summary>
  30. /// Zero indexed offset for the <see cref="DataRow"/> to display in <see cref="Table"/> on line 2 of the control (first line being headers)
  31. /// </summary>
  32. /// <remarks>This property allows very wide tables to be rendered with horizontal scrolling</remarks>
  33. public int RowOffset {
  34. get => rowOffset;
  35. set => rowOffset = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value));
  36. }
  37. /// <summary>
  38. /// The index of <see cref="DataTable.Columns"/> in <see cref="Table"/> that the user has currently selected
  39. /// </summary>
  40. public int SelectedColumn {
  41. get => selectedColumn;
  42. //try to prevent this being set to an out of bounds column
  43. set => selectedColumn = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value));
  44. }
  45. /// <summary>
  46. /// The index of <see cref="DataTable.Rows"/> in <see cref="Table"/> that the user has currently selected
  47. /// </summary>
  48. public int SelectedRow {
  49. get => selectedRow;
  50. set => selectedRow = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value));
  51. }
  52. /// <summary>
  53. /// The maximum number of characters to render in any given column. This prevents one long column from pushing out all the others
  54. /// </summary>
  55. public int MaximumCellWidth { get; set; } = 100;
  56. /// <summary>
  57. /// The text representation that should be rendered for cells with the value <see cref="DBNull.Value"/>
  58. /// </summary>
  59. public string NullSymbol { get; set; } = "-";
  60. /// <summary>
  61. /// The symbol to add after each cell value and header value to visually seperate values
  62. /// </summary>
  63. public char SeparatorSymbol { get; set; } = ' ';
  64. /// <summary>
  65. /// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout.
  66. /// </summary>
  67. /// <param name="table">The table to display in the control</param>
  68. public TableView (DataTable table) : this ()
  69. {
  70. this.Table = table;
  71. }
  72. /// <summary>
  73. /// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout. Set the <see cref="Table"/> property to begin editing
  74. /// </summary>
  75. public TableView () : base ()
  76. {
  77. CanFocus = true;
  78. }
  79. ///<inheritdoc/>
  80. public override void Redraw (Rect bounds)
  81. {
  82. Move (0, 0);
  83. var frame = Frame;
  84. // What columns to render at what X offset in viewport
  85. Dictionary<DataColumn, int> columnsToRender = CalculateViewport (bounds);
  86. Driver.SetAttribute (ColorScheme.Normal);
  87. //invalidate current row (prevents scrolling around leaving old characters in the frame
  88. Driver.AddStr (new string (' ', bounds.Width));
  89. // Render the headers
  90. foreach (var kvp in columnsToRender) {
  91. Move (kvp.Value, 0);
  92. Driver.AddStr (Truncate (kvp.Key.ColumnName + SeparatorSymbol, bounds.Width - kvp.Value));
  93. }
  94. //render the cells
  95. for (int line = 1; line < frame.Height; line++) {
  96. //invalidate current row (prevents scrolling around leaving old characters in the frame
  97. Move (0, line);
  98. Driver.SetAttribute (ColorScheme.Normal);
  99. Driver.AddStr (new string (' ', bounds.Width));
  100. //work out what Row to render
  101. var rowToRender = RowOffset + (line - 1);
  102. //if we have run off the end of the table
  103. if ( Table == null || rowToRender >= Table.Rows.Count)
  104. continue;
  105. foreach (var kvp in columnsToRender) {
  106. Move (kvp.Value, line);
  107. bool isSelectedCell = rowToRender == SelectedRow && kvp.Key.Ordinal == SelectedColumn;
  108. Driver.SetAttribute (isSelectedCell ? ColorScheme.HotFocus : ColorScheme.Normal);
  109. var valueToRender = GetRenderedVal (Table.Rows [rowToRender] [kvp.Key]) + SeparatorSymbol;
  110. Driver.AddStr (Truncate (valueToRender, bounds.Width - kvp.Value));
  111. }
  112. }
  113. }
  114. /// <summary>
  115. /// Truncates <paramref name="valueToRender"/> so that it occupies a maximum of <paramref name="availableHorizontalSpace"/>
  116. /// </summary>
  117. /// <param name="valueToRender"></param>
  118. /// <param name="availableHorizontalSpace"></param>
  119. /// <returns></returns>
  120. private ustring Truncate (string valueToRender, int availableHorizontalSpace)
  121. {
  122. if (string.IsNullOrEmpty (valueToRender) || valueToRender.Length < availableHorizontalSpace)
  123. return valueToRender;
  124. return valueToRender.Substring (0, availableHorizontalSpace);
  125. }
  126. /// <inheritdoc/>
  127. public override bool ProcessKey (KeyEvent keyEvent)
  128. {
  129. switch (keyEvent.Key) {
  130. case Key.CursorLeft:
  131. SelectedColumn--;
  132. Update ();
  133. break;
  134. case Key.CursorRight:
  135. SelectedColumn++;
  136. Update ();
  137. break;
  138. case Key.CursorDown:
  139. SelectedRow++;
  140. Update ();
  141. break;
  142. case Key.CursorUp:
  143. SelectedRow--;
  144. Update ();
  145. break;
  146. case Key.PageUp:
  147. SelectedRow -= Frame.Height;
  148. Update ();
  149. break;
  150. case Key.PageDown:
  151. SelectedRow += Frame.Height;
  152. Update ();
  153. break;
  154. case Key.Home | Key.CtrlMask:
  155. SelectedRow = 0;
  156. SelectedColumn = 0;
  157. Update ();
  158. break;
  159. case Key.Home:
  160. SelectedColumn = 0;
  161. Update ();
  162. break;
  163. case Key.End | Key.CtrlMask:
  164. //jump to end of table
  165. SelectedRow = Table == null ? 0 : Table.Rows.Count - 1;
  166. SelectedColumn = Table == null ? 0 : Table.Columns.Count - 1;
  167. Update ();
  168. break;
  169. case Key.End:
  170. //jump to end of row
  171. SelectedColumn = Table == null ? 0 : Table.Columns.Count - 1;
  172. Update ();
  173. break;
  174. }
  175. PositionCursor ();
  176. return true;
  177. }
  178. /// <summary>
  179. /// Updates the view to reflect changes to <see cref="Table"/> and to (<see cref="ColumnOffset"/> / <see cref="RowOffset"/>) etc
  180. /// </summary>
  181. /// <remarks>This always calls <see cref="View.SetNeedsDisplay()"/></remarks>
  182. public void Update()
  183. {
  184. if(Table == null) {
  185. SetNeedsDisplay ();
  186. return;
  187. }
  188. //if user opened a large table scrolled down a lot then opened a smaller table (or API deleted a bunch of columns without telling anyone)
  189. ColumnOffset = Math.Max(Math.Min(ColumnOffset,Table.Columns.Count -1),0);
  190. RowOffset = Math.Max(Math.Min(RowOffset,Table.Rows.Count -1),0);
  191. SelectedColumn = Math.Max(Math.Min(SelectedColumn,Table.Columns.Count -1),0);
  192. SelectedRow = Math.Max(Math.Min(SelectedRow,Table.Rows.Count -1),0);
  193. Dictionary<DataColumn, int> columnsToRender = CalculateViewport (Bounds);
  194. //if we have scrolled too far to the left
  195. if (SelectedColumn < columnsToRender.Keys.Min (col => col.Ordinal)) {
  196. ColumnOffset = SelectedColumn;
  197. }
  198. //if we have scrolled too far to the right
  199. if (SelectedColumn > columnsToRender.Keys.Max (col => col.Ordinal)) {
  200. ColumnOffset = SelectedColumn;
  201. }
  202. //if we have scrolled too far down
  203. if (SelectedRow > RowOffset + Bounds.Height - 1) {
  204. RowOffset = SelectedRow;
  205. }
  206. //if we have scrolled too far up
  207. if (SelectedRow < RowOffset) {
  208. RowOffset = SelectedRow;
  209. }
  210. SetNeedsDisplay ();
  211. }
  212. /// <summary>
  213. /// Calculates which columns should be rendered given the <paramref name="bounds"/> in which to display and the <see cref="ColumnOffset"/>
  214. /// </summary>
  215. /// <param name="bounds"></param>
  216. /// <param name="padding"></param>
  217. /// <returns></returns>
  218. private Dictionary<DataColumn, int> CalculateViewport (Rect bounds, int padding = 1)
  219. {
  220. Dictionary<DataColumn, int> toReturn = new Dictionary<DataColumn, int> ();
  221. if(Table == null)
  222. return toReturn;
  223. int usedSpace = 0;
  224. int availableHorizontalSpace = bounds.Width;
  225. int rowsToRender = bounds.Height - 1; //1 reserved for the headers row
  226. foreach (var col in Table.Columns.Cast<DataColumn> ().Skip (ColumnOffset)) {
  227. toReturn.Add (col, usedSpace);
  228. usedSpace += CalculateMaxRowSize (col, rowsToRender) + padding;
  229. if (usedSpace > availableHorizontalSpace)
  230. return toReturn;
  231. }
  232. return toReturn;
  233. }
  234. /// <summary>
  235. /// Returns the maximum of the <paramref name="col"/> name and the maximum length of data that will be rendered starting at <see cref="RowOffset"/> and rendering <paramref name="rowsToRender"/>
  236. /// </summary>
  237. /// <param name="col"></param>
  238. /// <param name="rowsToRender"></param>
  239. /// <returns></returns>
  240. private int CalculateMaxRowSize (DataColumn col, int rowsToRender)
  241. {
  242. int spaceRequired = col.ColumnName.Length;
  243. for (int i = RowOffset; i < RowOffset + rowsToRender && i < Table.Rows.Count; i++) {
  244. //expand required space if cell is bigger than the last biggest cell or header
  245. spaceRequired = Math.Max (spaceRequired, GetRenderedVal (Table.Rows [i] [col]).Length);
  246. }
  247. return spaceRequired;
  248. }
  249. /// <summary>
  250. /// Returns the value that should be rendered to best represent a strongly typed <paramref name="value"/> read from <see cref="Table"/>
  251. /// </summary>
  252. /// <param name="value"></param>
  253. /// <returns></returns>
  254. private string GetRenderedVal (object value)
  255. {
  256. if (value == null || value == DBNull.Value) {
  257. return NullSymbol;
  258. }
  259. var representation = value.ToString ();
  260. //if it is too long to fit
  261. if (representation.Length > MaximumCellWidth)
  262. return representation.Substring (0, MaximumCellWidth);
  263. return representation;
  264. }
  265. }
  266. }