TableView.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931
  1. using NStack;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Data;
  5. using System.Linq;
  6. namespace Terminal.Gui {
  7. /// <summary>
  8. /// Describes how to render a given column in a <see cref="TableView"/> including <see cref="Alignment"/> and textual representation of cells (e.g. date formats)
  9. /// </summary>
  10. public class ColumnStyle {
  11. /// <summary>
  12. /// Defines the default alignment for all values rendered in this column. For custom alignment based on cell contents use <see cref="AlignmentGetter"/>.
  13. /// </summary>
  14. public TextAlignment Alignment {get;set;}
  15. /// <summary>
  16. /// Defines a delegate for returning custom alignment per cell based on cell values. When specified this will override <see cref="Alignment"/>
  17. /// </summary>
  18. public Func<object,TextAlignment> AlignmentGetter;
  19. /// <summary>
  20. /// Defines a delegate for returning custom representations of cell values. If not set then <see cref="object.ToString()"/> is used. Return values from your delegate may be truncated e.g. based on <see cref="MaxWidth"/>
  21. /// </summary>
  22. public Func<object,string> RepresentationGetter;
  23. /// <summary>
  24. /// Defines the format for values e.g. "yyyy-MM-dd" for dates
  25. /// </summary>
  26. public string Format{get;set;}
  27. /// <summary>
  28. /// Set the maximum width of the column in characters. This value will be ignored if more than the tables <see cref="TableView.MaxCellWidth"/>. Defaults to <see cref="TableView.DefaultMaxCellWidth"/>
  29. /// </summary>
  30. public int MaxWidth {get;set;} = TableView.DefaultMaxCellWidth;
  31. /// <summary>
  32. /// Set the minimum width of the column in characters. This value will be ignored if more than the tables <see cref="TableView.MaxCellWidth"/> or the <see cref="MaxWidth"/>
  33. /// </summary>
  34. public int MinWidth {get;set;}
  35. /// <summary>
  36. /// Returns the alignment for the cell based on <paramref name="cellValue"/> and <see cref="AlignmentGetter"/>/<see cref="Alignment"/>
  37. /// </summary>
  38. /// <param name="cellValue"></param>
  39. /// <returns></returns>
  40. public TextAlignment GetAlignment(object cellValue)
  41. {
  42. if(AlignmentGetter != null)
  43. return AlignmentGetter(cellValue);
  44. return Alignment;
  45. }
  46. /// <summary>
  47. /// Returns the full string to render (which may be truncated if too long) that the current style says best represents the given <paramref name="value"/>
  48. /// </summary>
  49. /// <param name="value"></param>
  50. /// <returns></returns>
  51. public string GetRepresentation (object value)
  52. {
  53. if(!string.IsNullOrWhiteSpace(Format)) {
  54. if(value is IFormattable f)
  55. return f.ToString(Format,null);
  56. }
  57. if(RepresentationGetter != null)
  58. return RepresentationGetter(value);
  59. return value?.ToString();
  60. }
  61. }
  62. /// <summary>
  63. /// Defines rendering options that affect how the table is displayed
  64. /// </summary>
  65. public class TableStyle {
  66. /// <summary>
  67. /// When scrolling down always lock the column headers in place as the first row of the table
  68. /// </summary>
  69. public bool AlwaysShowHeaders {get;set;} = false;
  70. /// <summary>
  71. /// True to render a solid line above the headers
  72. /// </summary>
  73. public bool ShowHorizontalHeaderOverline {get;set;} = true;
  74. /// <summary>
  75. /// True to render a solid line under the headers
  76. /// </summary>
  77. public bool ShowHorizontalHeaderUnderline {get;set;} = true;
  78. /// <summary>
  79. /// True to render a solid line vertical line between cells
  80. /// </summary>
  81. public bool ShowVerticalCellLines {get;set;} = true;
  82. /// <summary>
  83. /// True to render a solid line vertical line between headers
  84. /// </summary>
  85. public bool ShowVerticalHeaderLines {get;set;} = true;
  86. /// <summary>
  87. /// Collection of columns for which you want special rendering (e.g. custom column lengths, text alignment etc)
  88. /// </summary>
  89. public Dictionary<DataColumn,ColumnStyle> ColumnStyles {get;set; } = new Dictionary<DataColumn, ColumnStyle>();
  90. /// <summary>
  91. /// Returns the entry from <see cref="ColumnStyles"/> for the given <paramref name="col"/> or null if no custom styling is defined for it
  92. /// </summary>
  93. /// <param name="col"></param>
  94. /// <returns></returns>
  95. public ColumnStyle GetColumnStyleIfAny (DataColumn col)
  96. {
  97. return ColumnStyles.TryGetValue(col,out ColumnStyle result) ? result : null;
  98. }
  99. }
  100. /// <summary>
  101. /// View for tabular data based on a <see cref="DataTable"/>
  102. /// </summary>
  103. public class TableView : View {
  104. private int columnOffset;
  105. private int rowOffset;
  106. private int selectedRow;
  107. private int selectedColumn;
  108. private DataTable table;
  109. private TableStyle style = new TableStyle();
  110. /// <summary>
  111. /// The default maximum cell width for <see cref="TableView.MaxCellWidth"/> and <see cref="ColumnStyle.MaxWidth"/>
  112. /// </summary>
  113. public const int DefaultMaxCellWidth = 100;
  114. /// <summary>
  115. /// The data table to render in the view. Setting this property automatically updates and redraws the control.
  116. /// </summary>
  117. public DataTable Table { get => table; set {table = value; Update(); } }
  118. /// <summary>
  119. /// Contains options for changing how the table is rendered
  120. /// </summary>
  121. public TableStyle Style { get => style; set {style = value; Update(); } }
  122. /// <summary>
  123. /// Horizontal scroll offset. The index of the first column in <see cref="Table"/> to display when when rendering the view.
  124. /// </summary>
  125. /// <remarks>This property allows very wide tables to be rendered with horizontal scrolling</remarks>
  126. public int ColumnOffset {
  127. get => columnOffset;
  128. //try to prevent this being set to an out of bounds column
  129. set => columnOffset = Table == null ? 0 :Math.Max (0,Math.Min (Table.Columns.Count - 1, value));
  130. }
  131. /// <summary>
  132. /// Vertical scroll offset. The index of the first row in <see cref="Table"/> to display in the first non header line of the control when rendering the view.
  133. /// </summary>
  134. public int RowOffset {
  135. get => rowOffset;
  136. set => rowOffset = Table == null ? 0 : Math.Max (0,Math.Min (Table.Rows.Count - 1, value));
  137. }
  138. /// <summary>
  139. /// The index of <see cref="DataTable.Columns"/> in <see cref="Table"/> that the user has currently selected
  140. /// </summary>
  141. public int SelectedColumn {
  142. get => selectedColumn;
  143. set {
  144. var oldValue = selectedColumn;
  145. //try to prevent this being set to an out of bounds column
  146. selectedColumn = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value));
  147. if(oldValue != selectedColumn)
  148. OnSelectedCellChanged(new SelectedCellChangedEventArgs(Table,oldValue,SelectedColumn,SelectedRow,SelectedRow));
  149. }
  150. }
  151. /// <summary>
  152. /// The index of <see cref="DataTable.Rows"/> in <see cref="Table"/> that the user has currently selected
  153. /// </summary>
  154. public int SelectedRow {
  155. get => selectedRow;
  156. set {
  157. var oldValue = selectedRow;
  158. selectedRow = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value));
  159. if(oldValue != selectedRow)
  160. OnSelectedCellChanged(new SelectedCellChangedEventArgs(Table,SelectedColumn,SelectedColumn,oldValue,selectedRow));
  161. }
  162. }
  163. /// <summary>
  164. /// The maximum number of characters to render in any given column. This prevents one long column from pushing out all the others
  165. /// </summary>
  166. public int MaxCellWidth { get; set; } = DefaultMaxCellWidth;
  167. /// <summary>
  168. /// The text representation that should be rendered for cells with the value <see cref="DBNull.Value"/>
  169. /// </summary>
  170. public string NullSymbol { get; set; } = "-";
  171. /// <summary>
  172. /// The symbol to add after each cell value and header value to visually seperate values (if not using vertical gridlines)
  173. /// </summary>
  174. public char SeparatorSymbol { get; set; } = ' ';
  175. /// <summary>
  176. /// This event is raised when the selected cell in the table changes.
  177. /// </summary>
  178. public event Action<SelectedCellChangedEventArgs> SelectedCellChanged;
  179. /// <summary>
  180. /// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout.
  181. /// </summary>
  182. /// <param name="table">The table to display in the control</param>
  183. public TableView (DataTable table) : this ()
  184. {
  185. this.Table = table;
  186. }
  187. /// <summary>
  188. /// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout. Set the <see cref="Table"/> property to begin editing
  189. /// </summary>
  190. public TableView () : base ()
  191. {
  192. CanFocus = true;
  193. }
  194. ///<inheritdoc/>
  195. public override void Redraw (Rect bounds)
  196. {
  197. Move (0, 0);
  198. var frame = Frame;
  199. // What columns to render at what X offset in viewport
  200. var columnsToRender = CalculateViewport(bounds).ToArray();
  201. Driver.SetAttribute (ColorScheme.Normal);
  202. //invalidate current row (prevents scrolling around leaving old characters in the frame
  203. Driver.AddStr (new string (' ', bounds.Width));
  204. int line = 0;
  205. if(ShouldRenderHeaders()){
  206. // Render something like:
  207. /*
  208. ┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐
  209. │ArithmeticComparator│chi │Healthboard│Interpretation│Labnumber│
  210. └────────────────────┴──────────┴───────────┴──────────────┴─────────┘
  211. */
  212. if(Style.ShowHorizontalHeaderOverline){
  213. RenderHeaderOverline(line,bounds.Width,columnsToRender);
  214. line++;
  215. }
  216. RenderHeaderMidline(line,columnsToRender);
  217. line++;
  218. if(Style.ShowHorizontalHeaderUnderline){
  219. RenderHeaderUnderline(line,bounds.Width,columnsToRender);
  220. line++;
  221. }
  222. }
  223. int headerLinesConsumed = line;
  224. //render the cells
  225. for (; line < frame.Height; line++) {
  226. ClearLine(line,bounds.Width);
  227. //work out what Row to render
  228. var rowToRender = RowOffset + (line - headerLinesConsumed);
  229. //if we have run off the end of the table
  230. if ( Table == null || rowToRender >= Table.Rows.Count || rowToRender < 0)
  231. continue;
  232. RenderRow(line,rowToRender,columnsToRender);
  233. }
  234. }
  235. /// <summary>
  236. /// Clears a line of the console by filling it with spaces
  237. /// </summary>
  238. /// <param name="row"></param>
  239. /// <param name="width"></param>
  240. private void ClearLine(int row, int width)
  241. {
  242. Move (0, row);
  243. Driver.SetAttribute (ColorScheme.Normal);
  244. Driver.AddStr (new string (' ', width));
  245. }
  246. /// <summary>
  247. /// Returns the amount of vertical space required to display the header
  248. /// </summary>
  249. /// <returns></returns>
  250. private int GetHeaderHeight()
  251. {
  252. int heightRequired = 1;
  253. if(Style.ShowHorizontalHeaderOverline)
  254. heightRequired++;
  255. if(Style.ShowHorizontalHeaderUnderline)
  256. heightRequired++;
  257. return heightRequired;
  258. }
  259. private void RenderHeaderOverline(int row,int availableWidth, ColumnToRender[] columnsToRender)
  260. {
  261. // Renders a line above table headers (when visible) like:
  262. // ┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐
  263. for(int c = 0;c< availableWidth;c++) {
  264. var rune = Driver.HLine;
  265. if (Style.ShowVerticalHeaderLines){
  266. if(c == 0){
  267. rune = Driver.ULCorner;
  268. }
  269. // if the next column is the start of a header
  270. else if(columnsToRender.Any(r=>r.X == c+1)){
  271. rune = Driver.TopTee;
  272. }
  273. else if(c == availableWidth -1){
  274. rune = Driver.URCorner;
  275. }
  276. }
  277. AddRuneAt(Driver,c,row,rune);
  278. }
  279. }
  280. private void RenderHeaderMidline(int row, ColumnToRender[] columnsToRender)
  281. {
  282. // Renders something like:
  283. // │ArithmeticComparator│chi │Healthboard│Interpretation│Labnumber│
  284. ClearLine(row,Bounds.Width);
  285. //render start of line
  286. if(style.ShowVerticalHeaderLines)
  287. AddRune(0,row,Driver.VLine);
  288. for(int i =0 ; i<columnsToRender.Length;i++) {
  289. var current = columnsToRender[i];
  290. var availableWidthForCell = GetCellWidth(columnsToRender,i);
  291. var colStyle = Style.GetColumnStyleIfAny(current.Column);
  292. var colName = current.Column.ColumnName;
  293. RenderSeparator(current.X-1,row,true);
  294. Move (current.X, row);
  295. Driver.AddStr(TruncateOrPad(colName,colName,availableWidthForCell ,colStyle));
  296. }
  297. //render end of line
  298. if(style.ShowVerticalHeaderLines)
  299. AddRune(Bounds.Width-1,row,Driver.VLine);
  300. }
  301. /// <summary>
  302. /// Calculates how much space is available to render index <paramref name="i"/> of the <paramref name="columnsToRender"/> given the remaining horizontal space
  303. /// </summary>
  304. /// <param name="columnsToRender"></param>
  305. /// <param name="i"></param>
  306. private int GetCellWidth (ColumnToRender [] columnsToRender, int i)
  307. {
  308. var current = columnsToRender[i];
  309. var next = i+1 < columnsToRender.Length ? columnsToRender[i+1] : null;
  310. if(next == null) {
  311. // cell can fill to end of the line
  312. return Bounds.Width - current.X;
  313. }
  314. else {
  315. // cell can fill up to next cell start
  316. return next.X - current.X;
  317. }
  318. }
  319. private void RenderHeaderUnderline(int row,int availableWidth, ColumnToRender[] columnsToRender)
  320. {
  321. // Renders a line below the table headers (when visible) like:
  322. // ├──────────┼───────────┼───────────────────┼──────────┼────────┼─────────────┤
  323. for(int c = 0;c< availableWidth;c++) {
  324. var rune = Driver.HLine;
  325. if (Style.ShowVerticalHeaderLines){
  326. if(c == 0){
  327. rune = Style.ShowVerticalCellLines ? Driver.LeftTee : Driver.LLCorner;
  328. }
  329. // if the next column is the start of a header
  330. else if(columnsToRender.Any(r=>r.X == c+1)){
  331. /*TODO: is ┼ symbol in Driver?*/
  332. rune = Style.ShowVerticalCellLines ? '┼' :Driver.BottomTee;
  333. }
  334. else if(c == availableWidth -1){
  335. rune = Style.ShowVerticalCellLines ? Driver.RightTee : Driver.LRCorner;
  336. }
  337. }
  338. AddRuneAt(Driver,c,row,rune);
  339. }
  340. }
  341. private void RenderRow(int row, int rowToRender, ColumnToRender[] columnsToRender)
  342. {
  343. //render start of line
  344. if(style.ShowVerticalCellLines)
  345. AddRune(0,row,Driver.VLine);
  346. // Render cells for each visible header for the current row
  347. for(int i=0;i< columnsToRender.Length ;i++) {
  348. var current = columnsToRender[i];
  349. var availableWidthForCell = GetCellWidth(columnsToRender,i);
  350. var colStyle = Style.GetColumnStyleIfAny(current.Column);
  351. // move to start of cell (in line with header positions)
  352. Move (current.X, row);
  353. // Set color scheme based on whether the current cell is the selected one
  354. bool isSelectedCell = rowToRender == SelectedRow && current.Column.Ordinal == SelectedColumn;
  355. Driver.SetAttribute (isSelectedCell ? ColorScheme.HotFocus : ColorScheme.Normal);
  356. var val = Table.Rows [rowToRender][current.Column];
  357. // Render the (possibly truncated) cell value
  358. var representation = GetRepresentation(val,colStyle);
  359. Driver.AddStr (TruncateOrPad(val,representation,availableWidthForCell,colStyle));
  360. // Reset color scheme to normal and render the vertical line (or space) at the end of the cell
  361. Driver.SetAttribute (ColorScheme.Normal);
  362. RenderSeparator(current.X-1,row,false);
  363. }
  364. //render end of line
  365. if(style.ShowVerticalCellLines)
  366. AddRune(Bounds.Width-1,row,Driver.VLine);
  367. }
  368. private void RenderSeparator(int col, int row,bool isHeader)
  369. {
  370. if(col<0)
  371. return;
  372. var renderLines = isHeader ? style.ShowVerticalHeaderLines : style.ShowVerticalCellLines;
  373. Rune symbol = renderLines ? Driver.VLine : SeparatorSymbol;
  374. AddRune(col,row,symbol);
  375. }
  376. void AddRuneAt (ConsoleDriver d,int col, int row, Rune ch)
  377. {
  378. Move (col, row);
  379. d.AddRune (ch);
  380. }
  381. /// <summary>
  382. /// Truncates or pads <paramref name="representation"/> so that it occupies a exactly <paramref name="availableHorizontalSpace"/> using the alignment specified in <paramref name="colStyle"/> (or left if no style is defined)
  383. /// </summary>
  384. /// <param name="originalCellValue">The object in this cell of the <see cref="Table"/></param>
  385. /// <param name="representation">The string representation of <paramref name="originalCellValue"/></param>
  386. /// <param name="availableHorizontalSpace"></param>
  387. /// <param name="colStyle">Optional style indicating custom alignment for the cell</param>
  388. /// <returns></returns>
  389. private string TruncateOrPad (object originalCellValue,string representation, int availableHorizontalSpace, ColumnStyle colStyle)
  390. {
  391. if (string.IsNullOrEmpty (representation))
  392. return representation;
  393. // if value is not wide enough
  394. if(representation.Sum(c=>Rune.ColumnWidth(c)) < availableHorizontalSpace) {
  395. // pad it out with spaces to the given alignment
  396. int toPad = availableHorizontalSpace - (representation.Sum(c=>Rune.ColumnWidth(c)) +1 /*leave 1 space for cell boundary*/);
  397. switch(colStyle?.GetAlignment(originalCellValue) ?? TextAlignment.Left) {
  398. case TextAlignment.Left :
  399. return representation + new string(' ',toPad);
  400. case TextAlignment.Right :
  401. return new string(' ',toPad) + representation;
  402. // TODO: With single line cells, centered and justified are the same right?
  403. case TextAlignment.Centered :
  404. case TextAlignment.Justified :
  405. return
  406. new string(' ',(int)Math.Floor(toPad/2.0)) + // round down
  407. representation +
  408. new string(' ',(int)Math.Ceiling(toPad/2.0)) ; // round up
  409. }
  410. }
  411. // value is too wide
  412. return new string(representation.TakeWhile(c=>(availableHorizontalSpace-= Rune.ColumnWidth(c))>0).ToArray());
  413. }
  414. /// <inheritdoc/>
  415. public override bool ProcessKey (KeyEvent keyEvent)
  416. {
  417. switch (keyEvent.Key) {
  418. case Key.CursorLeft:
  419. SelectedColumn--;
  420. Update ();
  421. break;
  422. case Key.CursorRight:
  423. SelectedColumn++;
  424. Update ();
  425. break;
  426. case Key.CursorDown:
  427. SelectedRow++;
  428. Update ();
  429. break;
  430. case Key.CursorUp:
  431. SelectedRow--;
  432. Update ();
  433. break;
  434. case Key.PageUp:
  435. SelectedRow -= Frame.Height;
  436. Update ();
  437. break;
  438. case Key.PageDown:
  439. SelectedRow += Frame.Height;
  440. Update ();
  441. break;
  442. case Key.Home | Key.CtrlMask:
  443. SelectedRow = 0;
  444. SelectedColumn = 0;
  445. Update ();
  446. break;
  447. case Key.Home:
  448. SelectedColumn = 0;
  449. Update ();
  450. break;
  451. case Key.End | Key.CtrlMask:
  452. //jump to end of table
  453. SelectedRow = Table == null ? 0 : Table.Rows.Count - 1;
  454. SelectedColumn = Table == null ? 0 : Table.Columns.Count - 1;
  455. Update ();
  456. break;
  457. case Key.End:
  458. //jump to end of row
  459. SelectedColumn = Table == null ? 0 : Table.Columns.Count - 1;
  460. Update ();
  461. break;
  462. default:
  463. // Not a keystroke we care about
  464. return false;
  465. }
  466. PositionCursor ();
  467. return true;
  468. }
  469. ///<inheritdoc/>
  470. public override bool MouseEvent (MouseEvent me)
  471. {
  472. if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) &&
  473. me.Flags != MouseFlags.WheeledDown && me.Flags != MouseFlags.WheeledUp &&
  474. me.Flags != MouseFlags.WheeledLeft && me.Flags != MouseFlags.WheeledRight)
  475. return false;
  476. if (!HasFocus && CanFocus) {
  477. SetFocus ();
  478. }
  479. if (Table == null) {
  480. return false;
  481. }
  482. // Scroll wheel flags
  483. switch(me.Flags)
  484. {
  485. case MouseFlags.WheeledDown:
  486. RowOffset++;
  487. EnsureValidScrollOffsets();
  488. SetNeedsDisplay();
  489. return true;
  490. case MouseFlags.WheeledUp:
  491. RowOffset--;
  492. EnsureValidScrollOffsets();
  493. SetNeedsDisplay();
  494. return true;
  495. case MouseFlags.WheeledRight:
  496. ColumnOffset++;
  497. EnsureValidScrollOffsets();
  498. SetNeedsDisplay();
  499. return true;
  500. case MouseFlags.WheeledLeft:
  501. ColumnOffset--;
  502. EnsureValidScrollOffsets();
  503. SetNeedsDisplay();
  504. return true;
  505. }
  506. if(me.Flags == MouseFlags.Button1Clicked) {
  507. var viewPort = CalculateViewport(Bounds);
  508. var headerHeight = ShouldRenderHeaders()? GetHeaderHeight():0;
  509. var col = viewPort.LastOrDefault(c=>c.X <= me.OfX);
  510. // Click is on the header section of rendered UI
  511. if(me.OfY < headerHeight)
  512. return false;
  513. var rowIdx = RowOffset - headerHeight + me.OfY;
  514. if(col != null && rowIdx >= 0) {
  515. SelectedRow = rowIdx;
  516. SelectedColumn = col.Column.Ordinal;
  517. Update();
  518. }
  519. }
  520. return false;
  521. }
  522. /// <summary>
  523. /// Updates the view to reflect changes to <see cref="Table"/> and to (<see cref="ColumnOffset"/> / <see cref="RowOffset"/>) etc
  524. /// </summary>
  525. /// <remarks>This always calls <see cref="View.SetNeedsDisplay()"/></remarks>
  526. public void Update()
  527. {
  528. if(Table == null) {
  529. SetNeedsDisplay ();
  530. return;
  531. }
  532. EnsureValidScrollOffsets();
  533. EnsureValidSelection();
  534. EnsureSelectedCellIsVisible();
  535. SetNeedsDisplay ();
  536. }
  537. /// <summary>
  538. /// Updates <see cref="ColumnOffset"/> and <see cref="RowOffset"/> where they are outside the bounds of the table (by adjusting them to the nearest existing cell). Has no effect if <see cref="Table"/> has not been set.
  539. /// </summary>
  540. /// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
  541. public void EnsureValidScrollOffsets ()
  542. {
  543. if(Table == null){
  544. return;
  545. }
  546. ColumnOffset = Math.Max(Math.Min(ColumnOffset,Table.Columns.Count -1),0);
  547. RowOffset = Math.Max(Math.Min(RowOffset,Table.Rows.Count -1),0);
  548. }
  549. /// <summary>
  550. /// Updates <see cref="SelectedColumn"/> and <see cref="SelectedRow"/> where they are outside the bounds of the table (by adjusting them to the nearest existing cell). Has no effect if <see cref="Table"/> has not been set.
  551. /// </summary>
  552. /// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
  553. public void EnsureValidSelection ()
  554. {
  555. if(Table == null){
  556. return;
  557. }
  558. SelectedColumn = Math.Max(Math.Min(SelectedColumn,Table.Columns.Count -1),0);
  559. SelectedRow = Math.Max(Math.Min(SelectedRow,Table.Rows.Count -1),0);
  560. }
  561. /// <summary>
  562. /// Updates scroll offsets to ensure that the selected cell is visible. Has no effect if <see cref="Table"/> has not been set.
  563. /// </summary>
  564. /// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
  565. public void EnsureSelectedCellIsVisible ()
  566. {
  567. if(Table == null || Table.Columns.Count <= 0){
  568. return;
  569. }
  570. var columnsToRender = CalculateViewport (Bounds).ToArray();
  571. var headerHeight = ShouldRenderHeaders()? GetHeaderHeight() : 0;
  572. //if we have scrolled too far to the left
  573. if (SelectedColumn < columnsToRender.Min (r => r.Column.Ordinal)) {
  574. ColumnOffset = SelectedColumn;
  575. }
  576. //if we have scrolled too far to the right
  577. if (SelectedColumn > columnsToRender.Max (r=> r.Column.Ordinal)) {
  578. ColumnOffset = SelectedColumn;
  579. }
  580. //if we have scrolled too far down
  581. if (SelectedRow >= RowOffset + (Bounds.Height - headerHeight)) {
  582. RowOffset = SelectedRow;
  583. }
  584. //if we have scrolled too far up
  585. if (SelectedRow < RowOffset) {
  586. RowOffset = SelectedRow;
  587. }
  588. }
  589. /// <summary>
  590. /// Invokes the <see cref="SelectedCellChanged"/> event
  591. /// </summary>
  592. protected virtual void OnSelectedCellChanged(SelectedCellChangedEventArgs args)
  593. {
  594. SelectedCellChanged?.Invoke(args);
  595. }
  596. /// <summary>
  597. /// Calculates which columns should be rendered given the <paramref name="bounds"/> in which to display and the <see cref="ColumnOffset"/>
  598. /// </summary>
  599. /// <param name="bounds"></param>
  600. /// <param name="padding"></param>
  601. /// <returns></returns>
  602. private IEnumerable<ColumnToRender> CalculateViewport (Rect bounds, int padding = 1)
  603. {
  604. if(Table == null)
  605. yield break;
  606. int usedSpace = 0;
  607. //if horizontal space is required at the start of the line (before the first header)
  608. if(Style.ShowVerticalHeaderLines || Style.ShowVerticalCellLines)
  609. usedSpace+=1;
  610. int availableHorizontalSpace = bounds.Width;
  611. int rowsToRender = bounds.Height;
  612. // reserved for the headers row
  613. if(ShouldRenderHeaders())
  614. rowsToRender -= GetHeaderHeight();
  615. bool first = true;
  616. foreach (var col in Table.Columns.Cast<DataColumn>().Skip (ColumnOffset)) {
  617. int startingIdxForCurrentHeader = usedSpace;
  618. var colStyle = Style.GetColumnStyleIfAny(col);
  619. // is there enough space for this column (and it's data)?
  620. usedSpace += CalculateMaxCellWidth (col, rowsToRender,colStyle) + padding;
  621. // no (don't render it) unless its the only column we are render (that must be one massively wide column!)
  622. if (!first && usedSpace > availableHorizontalSpace)
  623. yield break;
  624. // there is space
  625. yield return new ColumnToRender(col, startingIdxForCurrentHeader);
  626. first=false;
  627. }
  628. }
  629. private bool ShouldRenderHeaders()
  630. {
  631. if(Table == null || Table.Columns.Count == 0)
  632. return false;
  633. return Style.AlwaysShowHeaders || rowOffset == 0;
  634. }
  635. /// <summary>
  636. /// 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"/>
  637. /// </summary>
  638. /// <param name="col"></param>
  639. /// <param name="rowsToRender"></param>
  640. /// <param name="colStyle"></param>
  641. /// <returns></returns>
  642. private int CalculateMaxCellWidth(DataColumn col, int rowsToRender,ColumnStyle colStyle)
  643. {
  644. int spaceRequired = col.ColumnName.Sum(c=>Rune.ColumnWidth(c));
  645. // if table has no rows
  646. if(RowOffset < 0)
  647. return spaceRequired;
  648. for (int i = RowOffset; i < RowOffset + rowsToRender && i < Table.Rows.Count; i++) {
  649. //expand required space if cell is bigger than the last biggest cell or header
  650. spaceRequired = Math.Max (spaceRequired, GetRepresentation(Table.Rows [i][col],colStyle).Sum(c=>Rune.ColumnWidth(c)));
  651. }
  652. // Don't require more space than the style allows
  653. if(colStyle != null){
  654. // enforce maximum cell width based on style
  655. if(spaceRequired > colStyle.MaxWidth) {
  656. spaceRequired = colStyle.MaxWidth;
  657. }
  658. // enforce minimum cell width based on style
  659. if(spaceRequired < colStyle.MinWidth) {
  660. spaceRequired = colStyle.MinWidth;
  661. }
  662. }
  663. // enforce maximum cell width based on global table style
  664. if(spaceRequired > MaxCellWidth)
  665. spaceRequired = MaxCellWidth;
  666. return spaceRequired;
  667. }
  668. /// <summary>
  669. /// Returns the value that should be rendered to best represent a strongly typed <paramref name="value"/> read from <see cref="Table"/>
  670. /// </summary>
  671. /// <param name="value"></param>
  672. /// <param name="colStyle">Optional style defining how to represent cell values</param>
  673. /// <returns></returns>
  674. private string GetRepresentation(object value,ColumnStyle colStyle)
  675. {
  676. if (value == null || value == DBNull.Value) {
  677. return NullSymbol;
  678. }
  679. return colStyle != null ? colStyle.GetRepresentation(value): value.ToString();
  680. }
  681. }
  682. /// <summary>
  683. /// Describes a desire to render a column at a given horizontal position in the UI
  684. /// </summary>
  685. internal class ColumnToRender {
  686. /// <summary>
  687. /// The column to render
  688. /// </summary>
  689. public DataColumn Column {get;set;}
  690. /// <summary>
  691. /// The horizontal position to begin rendering the column at
  692. /// </summary>
  693. public int X{get;set;}
  694. public ColumnToRender (DataColumn col, int x)
  695. {
  696. Column = col;
  697. X = x;
  698. }
  699. }
  700. /// <summary>
  701. /// Defines the event arguments for <see cref="TableView.SelectedCellChanged"/>
  702. /// </summary>
  703. public class SelectedCellChangedEventArgs : EventArgs
  704. {
  705. /// <summary>
  706. /// The current table to which the new indexes refer. May be null e.g. if selection change is the result of clearing the table from the view
  707. /// </summary>
  708. /// <value></value>
  709. public DataTable Table {get;}
  710. /// <summary>
  711. /// The previous selected column index. May be invalid e.g. when the selection has been changed as a result of replacing the existing Table with a smaller one
  712. /// </summary>
  713. /// <value></value>
  714. public int OldCol {get;}
  715. /// <summary>
  716. /// The newly selected column index.
  717. /// </summary>
  718. /// <value></value>
  719. public int NewCol {get;}
  720. /// <summary>
  721. /// The previous selected row index. May be invalid e.g. when the selection has been changed as a result of deleting rows from the table
  722. /// </summary>
  723. /// <value></value>
  724. public int OldRow {get;}
  725. /// <summary>
  726. /// The newly selected row index.
  727. /// </summary>
  728. /// <value></value>
  729. public int NewRow {get;}
  730. /// <summary>
  731. /// Creates a new instance of arguments describing a change in selected cell in a <see cref="TableView"/>
  732. /// </summary>
  733. /// <param name="t"></param>
  734. /// <param name="oldCol"></param>
  735. /// <param name="newCol"></param>
  736. /// <param name="oldRow"></param>
  737. /// <param name="newRow"></param>
  738. public SelectedCellChangedEventArgs(DataTable t, int oldCol, int newCol, int oldRow, int newRow)
  739. {
  740. Table = t;
  741. OldCol = oldCol;
  742. NewCol = newCol;
  743. OldRow = oldRow;
  744. NewRow = newRow;
  745. }
  746. }
  747. }