TableView.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  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) : base ()
  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. }
  78. ///<inheritdoc/>
  79. public override void Redraw (Rect bounds)
  80. {
  81. Move (0, 0);
  82. var frame = Frame;
  83. // What columns to render at what X offset in viewport
  84. Dictionary<DataColumn, int> columnsToRender = CalculateViewport (bounds);
  85. Driver.SetAttribute (ColorScheme.Normal);
  86. //invalidate current row (prevents scrolling around leaving old characters in the frame
  87. Driver.AddStr (new string (' ', bounds.Width));
  88. // Render the headers
  89. foreach (var kvp in columnsToRender) {
  90. Move (kvp.Value, 0);
  91. Driver.AddStr (Truncate (kvp.Key.ColumnName + SeparatorSymbol, bounds.Width - kvp.Value));
  92. }
  93. //render the cells
  94. for (int line = 1; line < frame.Height; line++) {
  95. //invalidate current row (prevents scrolling around leaving old characters in the frame
  96. Move (0, line);
  97. Driver.SetAttribute (ColorScheme.Normal);
  98. Driver.AddStr (new string (' ', bounds.Width));
  99. //work out what Row to render
  100. var rowToRender = RowOffset + (line - 1);
  101. //if we have run off the end of the table
  102. if ( Table == null || rowToRender >= Table.Rows.Count)
  103. continue;
  104. foreach (var kvp in columnsToRender) {
  105. Move (kvp.Value, line);
  106. bool isSelectedCell = rowToRender == SelectedRow && kvp.Key.Ordinal == SelectedColumn;
  107. Driver.SetAttribute (isSelectedCell ? ColorScheme.HotFocus : ColorScheme.Normal);
  108. var valueToRender = GetRenderedVal (Table.Rows [rowToRender] [kvp.Key]) + SeparatorSymbol;
  109. Driver.AddStr (Truncate (valueToRender, bounds.Width - kvp.Value));
  110. }
  111. }
  112. }
  113. /// <summary>
  114. /// Truncates <paramref name="valueToRender"/> so that it occupies a maximum of <paramref name="availableHorizontalSpace"/>
  115. /// </summary>
  116. /// <param name="valueToRender"></param>
  117. /// <param name="availableHorizontalSpace"></param>
  118. /// <returns></returns>
  119. private ustring Truncate (string valueToRender, int availableHorizontalSpace)
  120. {
  121. if (string.IsNullOrEmpty (valueToRender) || valueToRender.Length < availableHorizontalSpace)
  122. return valueToRender;
  123. return valueToRender.Substring (0, availableHorizontalSpace);
  124. }
  125. /// <inheritdoc/>
  126. public override bool ProcessKey (KeyEvent keyEvent)
  127. {
  128. switch (keyEvent.Key) {
  129. case Key.CursorLeft:
  130. SelectedColumn--;
  131. Update ();
  132. break;
  133. case Key.CursorRight:
  134. SelectedColumn++;
  135. Update ();
  136. break;
  137. case Key.CursorDown:
  138. SelectedRow++;
  139. Update ();
  140. break;
  141. case Key.CursorUp:
  142. SelectedRow--;
  143. Update ();
  144. break;
  145. case Key.PageUp:
  146. SelectedRow -= Frame.Height;
  147. Update ();
  148. break;
  149. case Key.PageDown:
  150. SelectedRow += Frame.Height;
  151. Update ();
  152. break;
  153. case Key.Home | Key.CtrlMask:
  154. SelectedRow = 0;
  155. SelectedColumn = 0;
  156. Update ();
  157. break;
  158. case Key.Home:
  159. SelectedColumn = 0;
  160. Update ();
  161. break;
  162. case Key.End | Key.CtrlMask:
  163. //jump to end of table
  164. SelectedRow = Table == null ? 0 : Table.Rows.Count - 1;
  165. SelectedColumn = Table == null ? 0 : Table.Columns.Count - 1;
  166. Update ();
  167. break;
  168. case Key.End:
  169. //jump to end of row
  170. SelectedColumn = Table == null ? 0 : Table.Columns.Count - 1;
  171. Update ();
  172. break;
  173. }
  174. PositionCursor ();
  175. return true;
  176. }
  177. /// <summary>
  178. /// Updates the view to reflect changes to <see cref="Table"/> and to (<see cref="ColumnOffset"/> / <see cref="RowOffset"/>) etc
  179. /// </summary>
  180. /// <remarks>This always calls <see cref="View.SetNeedsDisplay()"/></remarks>
  181. public void Update()
  182. {
  183. if(Table == null) {
  184. SetNeedsDisplay ();
  185. return;
  186. }
  187. //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)
  188. ColumnOffset = Math.Max(Math.Min(ColumnOffset,Table.Columns.Count -1),0);
  189. RowOffset = Math.Max(Math.Min(RowOffset,Table.Rows.Count -1),0);
  190. SelectedColumn = Math.Max(Math.Min(SelectedColumn,Table.Columns.Count -1),0);
  191. SelectedRow = Math.Max(Math.Min(SelectedRow,Table.Rows.Count -1),0);
  192. Dictionary<DataColumn, int> columnsToRender = CalculateViewport (Bounds);
  193. //if we have scrolled too far to the left
  194. if (SelectedColumn < columnsToRender.Keys.Min (col => col.Ordinal)) {
  195. ColumnOffset = SelectedColumn;
  196. }
  197. //if we have scrolled too far to the right
  198. if (SelectedColumn > columnsToRender.Keys.Max (col => col.Ordinal)) {
  199. ColumnOffset = SelectedColumn;
  200. }
  201. //if we have scrolled too far down
  202. if (SelectedRow > RowOffset + Bounds.Height - 1) {
  203. RowOffset = SelectedRow;
  204. }
  205. //if we have scrolled too far up
  206. if (SelectedRow < RowOffset) {
  207. RowOffset = SelectedRow;
  208. }
  209. SetNeedsDisplay ();
  210. }
  211. /// <summary>
  212. /// Calculates which columns should be rendered given the <paramref name="bounds"/> in which to display and the <see cref="ColumnOffset"/>
  213. /// </summary>
  214. /// <param name="bounds"></param>
  215. /// <param name="padding"></param>
  216. /// <returns></returns>
  217. private Dictionary<DataColumn, int> CalculateViewport (Rect bounds, int padding = 1)
  218. {
  219. Dictionary<DataColumn, int> toReturn = new Dictionary<DataColumn, int> ();
  220. if(Table == null)
  221. return toReturn;
  222. int usedSpace = 0;
  223. int availableHorizontalSpace = bounds.Width;
  224. int rowsToRender = bounds.Height - 1; //1 reserved for the headers row
  225. foreach (var col in Table.Columns.Cast<DataColumn> ().Skip (ColumnOffset)) {
  226. toReturn.Add (col, usedSpace);
  227. usedSpace += CalculateMaxRowSize (col, rowsToRender) + padding;
  228. if (usedSpace > availableHorizontalSpace)
  229. return toReturn;
  230. }
  231. return toReturn;
  232. }
  233. /// <summary>
  234. /// 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"/>
  235. /// </summary>
  236. /// <param name="col"></param>
  237. /// <param name="rowsToRender"></param>
  238. /// <returns></returns>
  239. private int CalculateMaxRowSize (DataColumn col, int rowsToRender)
  240. {
  241. int spaceRequired = col.ColumnName.Length;
  242. for (int i = RowOffset; i < RowOffset + rowsToRender && i < Table.Rows.Count; i++) {
  243. //expand required space if cell is bigger than the last biggest cell or header
  244. spaceRequired = Math.Max (spaceRequired, GetRenderedVal (Table.Rows [i] [col]).Length);
  245. }
  246. return spaceRequired;
  247. }
  248. /// <summary>
  249. /// Returns the value that should be rendered to best represent a strongly typed <paramref name="value"/> read from <see cref="Table"/>
  250. /// </summary>
  251. /// <param name="value"></param>
  252. /// <returns></returns>
  253. private string GetRenderedVal (object value)
  254. {
  255. if (value == null || value == DBNull.Value) {
  256. return NullSymbol;
  257. }
  258. var representation = value.ToString ();
  259. //if it is too long to fit
  260. if (representation.Length > MaximumCellWidth)
  261. return representation.Substring (0, MaximumCellWidth);
  262. return representation;
  263. }
  264. }
  265. }