TableView.cs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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. public DataTable Table { get => table; set {table = value; Update(); } }
  17. /// <summary>
  18. /// Zero indexed offset for the upper left <see cref="DataColumn"/> to display in <see cref="Table"/>.
  19. /// </summary>
  20. /// <remarks>This property allows very wide tables to be rendered with horizontal scrolling</remarks>
  21. public int ColumnOffset {
  22. get => columnOffset;
  23. //try to prevent this being set to an out of bounds column
  24. set => columnOffset = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value));
  25. }
  26. /// <summary>
  27. /// Zero indexed offset for the <see cref="DataRow"/> to display in <see cref="Table"/> on line 2 of the control (first line being headers)
  28. /// </summary>
  29. /// <remarks>This property allows very wide tables to be rendered with horizontal scrolling</remarks>
  30. public int RowOffset {
  31. get => rowOffset;
  32. set => rowOffset = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value));
  33. }
  34. /// <summary>
  35. /// The index of <see cref="DataTable.Columns"/> in <see cref="Table"/> that the user has currently selected
  36. /// </summary>
  37. public int SelectedColumn {
  38. get => selectedColumn;
  39. //try to prevent this being set to an out of bounds column
  40. set => selectedColumn = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value));
  41. }
  42. /// <summary>
  43. /// The index of <see cref="DataTable.Rows"/> in <see cref="Table"/> that the user has currently selected
  44. /// </summary>
  45. public int SelectedRow {
  46. get => selectedRow;
  47. set => selectedRow = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value));
  48. }
  49. /// <summary>
  50. /// The maximum number of characters to render in any given column. This prevents one long column from pushing out all the others
  51. /// </summary>
  52. public int MaximumCellWidth { get; set; } = 100;
  53. /// <summary>
  54. /// The text representation that should be rendered for cells with the value <see cref="DBNull.Value"/>
  55. /// </summary>
  56. public string NullSymbol { get; set; } = "-";
  57. /// <summary>
  58. /// The symbol to add after each cell value and header value to visually seperate values
  59. /// </summary>
  60. public char SeparatorSymbol { get; set; } = ' ';
  61. /// <summary>
  62. /// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout.
  63. /// </summary>
  64. /// <param name="table">The table to display in the control</param>
  65. public TableView (DataTable table) : base ()
  66. {
  67. this.Table = table;
  68. }
  69. /// <summary>
  70. /// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout. Set the <see cref="Table"/> property to begin editing
  71. /// </summary>
  72. public TableView () : base ()
  73. {
  74. }
  75. ///<inheritdoc/>
  76. public override void Redraw (Rect bounds)
  77. {
  78. Attribute currentAttribute;
  79. var current = ColorScheme.Focus;
  80. Driver.SetAttribute (current);
  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. void SetAttribute (Attribute attribute)
  113. {
  114. if (currentAttribute != attribute) {
  115. currentAttribute = attribute;
  116. Driver.SetAttribute (attribute);
  117. }
  118. }
  119. }
  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. Dictionary<DataColumn, int> columnsToRender = CalculateViewport (Bounds);
  189. //if we have scrolled too far to the left
  190. if (SelectedColumn < columnsToRender.Keys.Min (col => col.Ordinal)) {
  191. ColumnOffset = SelectedColumn;
  192. }
  193. //if we have scrolled too far to the right
  194. if (SelectedColumn > columnsToRender.Keys.Max (col => col.Ordinal)) {
  195. ColumnOffset = SelectedColumn;
  196. }
  197. //if we have scrolled too far down
  198. if (SelectedRow > RowOffset + Bounds.Height - 1) {
  199. RowOffset = SelectedRow;
  200. }
  201. //if we have scrolled too far up
  202. if (SelectedRow < RowOffset) {
  203. RowOffset = SelectedRow;
  204. }
  205. SetNeedsDisplay ();
  206. }
  207. /// <summary>
  208. /// Calculates which columns should be rendered given the <paramref name="bounds"/> in which to display and the <see cref="ColumnOffset"/>
  209. /// </summary>
  210. /// <param name="bounds"></param>
  211. /// <param name="padding"></param>
  212. /// <returns></returns>
  213. private Dictionary<DataColumn, int> CalculateViewport (Rect bounds, int padding = 1)
  214. {
  215. Dictionary<DataColumn, int> toReturn = new Dictionary<DataColumn, int> ();
  216. if(Table == null)
  217. return toReturn;
  218. int usedSpace = 0;
  219. int availableHorizontalSpace = bounds.Width;
  220. int rowsToRender = bounds.Height - 1; //1 reserved for the headers row
  221. foreach (var col in Table.Columns.Cast<DataColumn> ().Skip (ColumnOffset)) {
  222. toReturn.Add (col, usedSpace);
  223. usedSpace += CalculateMaxRowSize (col, rowsToRender) + padding;
  224. if (usedSpace > availableHorizontalSpace)
  225. return toReturn;
  226. }
  227. return toReturn;
  228. }
  229. /// <summary>
  230. /// 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"/>
  231. /// </summary>
  232. /// <param name="col"></param>
  233. /// <param name="rowsToRender"></param>
  234. /// <returns></returns>
  235. private int CalculateMaxRowSize (DataColumn col, int rowsToRender)
  236. {
  237. int spaceRequired = col.ColumnName.Length;
  238. for (int i = RowOffset; i < RowOffset + rowsToRender && i < Table.Rows.Count; i++) {
  239. //expand required space if cell is bigger than the last biggest cell or header
  240. spaceRequired = Math.Max (spaceRequired, GetRenderedVal (Table.Rows [i] [col]).Length);
  241. }
  242. return spaceRequired;
  243. }
  244. /// <summary>
  245. /// Returns the value that should be rendered to best represent a strongly typed <paramref name="value"/> read from <see cref="Table"/>
  246. /// </summary>
  247. /// <param name="value"></param>
  248. /// <returns></returns>
  249. private string GetRenderedVal (object value)
  250. {
  251. if (value == null || value == DBNull.Value) {
  252. return NullSymbol;
  253. }
  254. var representation = value.ToString ();
  255. //if it is too long to fit
  256. if (representation.Length > MaximumCellWidth)
  257. return representation.Substring (0, MaximumCellWidth);
  258. return representation;
  259. }
  260. }
  261. }