TableView.cs 9.0 KB

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