TableView.cs 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348
  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"/>
  9. /// and textual representation of cells (e.g. date formats)
  10. ///
  11. /// <a href="https://migueldeicaza.github.io/gui.cs/articles/tableview.html">See TableView Deep Dive for more information</a>.
  12. /// </summary>
  13. public class ColumnStyle {
  14. /// <summary>
  15. /// Defines the default alignment for all values rendered in this column. For custom alignment based on cell contents use <see cref="AlignmentGetter"/>.
  16. /// </summary>
  17. public TextAlignment Alignment {get;set;}
  18. /// <summary>
  19. /// Defines a delegate for returning custom alignment per cell based on cell values. When specified this will override <see cref="Alignment"/>
  20. /// </summary>
  21. public Func<object,TextAlignment> AlignmentGetter;
  22. /// <summary>
  23. /// 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"/>
  24. /// </summary>
  25. public Func<object,string> RepresentationGetter;
  26. /// <summary>
  27. /// Defines the format for values e.g. "yyyy-MM-dd" for dates
  28. /// </summary>
  29. public string Format{get;set;}
  30. /// <summary>
  31. /// 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"/>
  32. /// </summary>
  33. public int MaxWidth {get;set;} = TableView.DefaultMaxCellWidth;
  34. /// <summary>
  35. /// 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"/>
  36. /// </summary>
  37. public int MinWidth {get;set;}
  38. /// <summary>
  39. /// Returns the alignment for the cell based on <paramref name="cellValue"/> and <see cref="AlignmentGetter"/>/<see cref="Alignment"/>
  40. /// </summary>
  41. /// <param name="cellValue"></param>
  42. /// <returns></returns>
  43. public TextAlignment GetAlignment(object cellValue)
  44. {
  45. if(AlignmentGetter != null)
  46. return AlignmentGetter(cellValue);
  47. return Alignment;
  48. }
  49. /// <summary>
  50. /// 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"/>
  51. /// </summary>
  52. /// <param name="value"></param>
  53. /// <returns></returns>
  54. public string GetRepresentation (object value)
  55. {
  56. if(!string.IsNullOrWhiteSpace(Format)) {
  57. if(value is IFormattable f)
  58. return f.ToString(Format,null);
  59. }
  60. if(RepresentationGetter != null)
  61. return RepresentationGetter(value);
  62. return value?.ToString();
  63. }
  64. }
  65. /// <summary>
  66. /// Defines rendering options that affect how the table is displayed.
  67. ///
  68. /// <a href="https://migueldeicaza.github.io/gui.cs/articles/tableview.html">See TableView Deep Dive for more information</a>.
  69. /// </summary>
  70. public class TableStyle {
  71. /// <summary>
  72. /// When scrolling down always lock the column headers in place as the first row of the table
  73. /// </summary>
  74. public bool AlwaysShowHeaders {get;set;} = false;
  75. /// <summary>
  76. /// True to render a solid line above the headers
  77. /// </summary>
  78. public bool ShowHorizontalHeaderOverline {get;set;} = true;
  79. /// <summary>
  80. /// True to render a solid line under the headers
  81. /// </summary>
  82. public bool ShowHorizontalHeaderUnderline {get;set;} = true;
  83. /// <summary>
  84. /// True to render a solid line vertical line between cells
  85. /// </summary>
  86. public bool ShowVerticalCellLines {get;set;} = true;
  87. /// <summary>
  88. /// True to render a solid line vertical line between headers
  89. /// </summary>
  90. public bool ShowVerticalHeaderLines {get;set;} = true;
  91. /// <summary>
  92. /// Collection of columns for which you want special rendering (e.g. custom column lengths, text alignment etc)
  93. /// </summary>
  94. public Dictionary<DataColumn,ColumnStyle> ColumnStyles {get;set; } = new Dictionary<DataColumn, ColumnStyle>();
  95. /// <summary>
  96. /// Returns the entry from <see cref="ColumnStyles"/> for the given <paramref name="col"/> or null if no custom styling is defined for it
  97. /// </summary>
  98. /// <param name="col"></param>
  99. /// <returns></returns>
  100. public ColumnStyle GetColumnStyleIfAny (DataColumn col)
  101. {
  102. return ColumnStyles.TryGetValue(col,out ColumnStyle result) ? result : null;
  103. }
  104. /// <summary>
  105. /// Returns an existing <see cref="ColumnStyle"/> for the given <paramref name="col"/> or creates a new one with default options
  106. /// </summary>
  107. /// <param name="col"></param>
  108. /// <returns></returns>
  109. public ColumnStyle GetOrCreateColumnStyle (DataColumn col)
  110. {
  111. if(!ColumnStyles.ContainsKey(col))
  112. ColumnStyles.Add(col,new ColumnStyle());
  113. return ColumnStyles[col];
  114. }
  115. }
  116. /// <summary>
  117. /// View for tabular data based on a <see cref="DataTable"/>.
  118. ///
  119. /// <a href="https://migueldeicaza.github.io/gui.cs/articles/tableview.html">See TableView Deep Dive for more information</a>.
  120. /// </summary>
  121. public class TableView : View {
  122. private int columnOffset;
  123. private int rowOffset;
  124. private int selectedRow;
  125. private int selectedColumn;
  126. private DataTable table;
  127. private TableStyle style = new TableStyle();
  128. /// <summary>
  129. /// The default maximum cell width for <see cref="TableView.MaxCellWidth"/> and <see cref="ColumnStyle.MaxWidth"/>
  130. /// </summary>
  131. public const int DefaultMaxCellWidth = 100;
  132. /// <summary>
  133. /// The data table to render in the view. Setting this property automatically updates and redraws the control.
  134. /// </summary>
  135. public DataTable Table { get => table; set {table = value; Update(); } }
  136. /// <summary>
  137. /// Contains options for changing how the table is rendered
  138. /// </summary>
  139. public TableStyle Style { get => style; set {style = value; Update(); } }
  140. /// <summary>
  141. /// True to select the entire row at once. False to select individual cells. Defaults to false
  142. /// </summary>
  143. public bool FullRowSelect {get;set;}
  144. /// <summary>
  145. /// True to allow regions to be selected
  146. /// </summary>
  147. /// <value></value>
  148. public bool MultiSelect {get;set;} = true;
  149. /// <summary>
  150. /// When <see cref="MultiSelect"/> is enabled this property contain all rectangles of selected cells. Rectangles describe column/rows selected in <see cref="Table"/> (not screen coordinates)
  151. /// </summary>
  152. /// <returns></returns>
  153. public Stack<TableSelection> MultiSelectedRegions {get;} = new Stack<TableSelection>();
  154. /// <summary>
  155. /// Horizontal scroll offset. The index of the first column in <see cref="Table"/> to display when when rendering the view.
  156. /// </summary>
  157. /// <remarks>This property allows very wide tables to be rendered with horizontal scrolling</remarks>
  158. public int ColumnOffset {
  159. get => columnOffset;
  160. //try to prevent this being set to an out of bounds column
  161. set => columnOffset = Table == null ? 0 :Math.Max (0,Math.Min (Table.Columns.Count - 1, value));
  162. }
  163. /// <summary>
  164. /// 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.
  165. /// </summary>
  166. public int RowOffset {
  167. get => rowOffset;
  168. set => rowOffset = Table == null ? 0 : Math.Max (0,Math.Min (Table.Rows.Count - 1, value));
  169. }
  170. /// <summary>
  171. /// The index of <see cref="DataTable.Columns"/> in <see cref="Table"/> that the user has currently selected
  172. /// </summary>
  173. public int SelectedColumn {
  174. get => selectedColumn;
  175. set {
  176. var oldValue = selectedColumn;
  177. //try to prevent this being set to an out of bounds column
  178. selectedColumn = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value));
  179. if(oldValue != selectedColumn)
  180. OnSelectedCellChanged(new SelectedCellChangedEventArgs(Table,oldValue,SelectedColumn,SelectedRow,SelectedRow));
  181. }
  182. }
  183. /// <summary>
  184. /// The index of <see cref="DataTable.Rows"/> in <see cref="Table"/> that the user has currently selected
  185. /// </summary>
  186. public int SelectedRow {
  187. get => selectedRow;
  188. set {
  189. var oldValue = selectedRow;
  190. selectedRow = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value));
  191. if(oldValue != selectedRow)
  192. OnSelectedCellChanged(new SelectedCellChangedEventArgs(Table,SelectedColumn,SelectedColumn,oldValue,selectedRow));
  193. }
  194. }
  195. /// <summary>
  196. /// The maximum number of characters to render in any given column. This prevents one long column from pushing out all the others
  197. /// </summary>
  198. public int MaxCellWidth { get; set; } = DefaultMaxCellWidth;
  199. /// <summary>
  200. /// The text representation that should be rendered for cells with the value <see cref="DBNull.Value"/>
  201. /// </summary>
  202. public string NullSymbol { get; set; } = "-";
  203. /// <summary>
  204. /// The symbol to add after each cell value and header value to visually seperate values (if not using vertical gridlines)
  205. /// </summary>
  206. public char SeparatorSymbol { get; set; } = ' ';
  207. /// <summary>
  208. /// This event is raised when the selected cell in the table changes.
  209. /// </summary>
  210. public event Action<SelectedCellChangedEventArgs> SelectedCellChanged;
  211. /// <summary>
  212. /// This event is raised when a cell is activated e.g. by double clicking or pressing <see cref="CellActivationKey"/>
  213. /// </summary>
  214. public event Action<CellActivatedEventArgs> CellActivated;
  215. /// <summary>
  216. /// The key which when pressed should trigger <see cref="CellActivated"/> event. Defaults to Enter.
  217. /// </summary>
  218. public Key CellActivationKey {get;set;} = Key.Enter;
  219. /// <summary>
  220. /// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout.
  221. /// </summary>
  222. /// <param name="table">The table to display in the control</param>
  223. public TableView (DataTable table) : this ()
  224. {
  225. this.Table = table;
  226. }
  227. /// <summary>
  228. /// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout. Set the <see cref="Table"/> property to begin editing
  229. /// </summary>
  230. public TableView () : base ()
  231. {
  232. CanFocus = true;
  233. }
  234. ///<inheritdoc/>
  235. public override void Redraw (Rect bounds)
  236. {
  237. Move (0, 0);
  238. var frame = Frame;
  239. // What columns to render at what X offset in viewport
  240. var columnsToRender = CalculateViewport(bounds).ToArray();
  241. Driver.SetAttribute (ColorScheme.Normal);
  242. //invalidate current row (prevents scrolling around leaving old characters in the frame
  243. Driver.AddStr (new string (' ', bounds.Width));
  244. int line = 0;
  245. if(ShouldRenderHeaders()){
  246. // Render something like:
  247. /*
  248. ┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐
  249. │ArithmeticComparator│chi │Healthboard│Interpretation│Labnumber│
  250. └────────────────────┴──────────┴───────────┴──────────────┴─────────┘
  251. */
  252. if(Style.ShowHorizontalHeaderOverline){
  253. RenderHeaderOverline(line,bounds.Width,columnsToRender);
  254. line++;
  255. }
  256. RenderHeaderMidline(line,columnsToRender);
  257. line++;
  258. if(Style.ShowHorizontalHeaderUnderline){
  259. RenderHeaderUnderline(line,bounds.Width,columnsToRender);
  260. line++;
  261. }
  262. }
  263. int headerLinesConsumed = line;
  264. //render the cells
  265. for (; line < frame.Height; line++) {
  266. ClearLine(line,bounds.Width);
  267. //work out what Row to render
  268. var rowToRender = RowOffset + (line - headerLinesConsumed);
  269. //if we have run off the end of the table
  270. if ( Table == null || rowToRender >= Table.Rows.Count || rowToRender < 0)
  271. continue;
  272. RenderRow(line,rowToRender,columnsToRender);
  273. }
  274. }
  275. /// <summary>
  276. /// Clears a line of the console by filling it with spaces
  277. /// </summary>
  278. /// <param name="row"></param>
  279. /// <param name="width"></param>
  280. private void ClearLine(int row, int width)
  281. {
  282. Move (0, row);
  283. Driver.SetAttribute (ColorScheme.Normal);
  284. Driver.AddStr (new string (' ', width));
  285. }
  286. /// <summary>
  287. /// Returns the amount of vertical space currently occupied by the header or 0 if it is not visible.
  288. /// </summary>
  289. /// <returns></returns>
  290. private int GetHeaderHeightIfAny()
  291. {
  292. return ShouldRenderHeaders()? GetHeaderHeight():0;
  293. }
  294. /// <summary>
  295. /// Returns the amount of vertical space required to display the header
  296. /// </summary>
  297. /// <returns></returns>
  298. private int GetHeaderHeight()
  299. {
  300. int heightRequired = 1;
  301. if(Style.ShowHorizontalHeaderOverline)
  302. heightRequired++;
  303. if(Style.ShowHorizontalHeaderUnderline)
  304. heightRequired++;
  305. return heightRequired;
  306. }
  307. private void RenderHeaderOverline(int row,int availableWidth, ColumnToRender[] columnsToRender)
  308. {
  309. // Renders a line above table headers (when visible) like:
  310. // ┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐
  311. for(int c = 0;c< availableWidth;c++) {
  312. var rune = Driver.HLine;
  313. if (Style.ShowVerticalHeaderLines){
  314. if(c == 0){
  315. rune = Driver.ULCorner;
  316. }
  317. // if the next column is the start of a header
  318. else if(columnsToRender.Any(r=>r.X == c+1)){
  319. rune = Driver.TopTee;
  320. }
  321. else if(c == availableWidth -1){
  322. rune = Driver.URCorner;
  323. }
  324. }
  325. AddRuneAt(Driver,c,row,rune);
  326. }
  327. }
  328. private void RenderHeaderMidline(int row, ColumnToRender[] columnsToRender)
  329. {
  330. // Renders something like:
  331. // │ArithmeticComparator│chi │Healthboard│Interpretation│Labnumber│
  332. ClearLine(row,Bounds.Width);
  333. //render start of line
  334. if(style.ShowVerticalHeaderLines)
  335. AddRune(0,row,Driver.VLine);
  336. for(int i =0 ; i<columnsToRender.Length;i++) {
  337. var current = columnsToRender[i];
  338. var availableWidthForCell = GetCellWidth(columnsToRender,i);
  339. var colStyle = Style.GetColumnStyleIfAny(current.Column);
  340. var colName = current.Column.ColumnName;
  341. RenderSeparator(current.X-1,row,true);
  342. Move (current.X, row);
  343. Driver.AddStr(TruncateOrPad(colName,colName,availableWidthForCell ,colStyle));
  344. }
  345. //render end of line
  346. if(style.ShowVerticalHeaderLines)
  347. AddRune(Bounds.Width-1,row,Driver.VLine);
  348. }
  349. /// <summary>
  350. /// Calculates how much space is available to render index <paramref name="i"/> of the <paramref name="columnsToRender"/> given the remaining horizontal space
  351. /// </summary>
  352. /// <param name="columnsToRender"></param>
  353. /// <param name="i"></param>
  354. private int GetCellWidth (ColumnToRender [] columnsToRender, int i)
  355. {
  356. var current = columnsToRender[i];
  357. var next = i+1 < columnsToRender.Length ? columnsToRender[i+1] : null;
  358. if(next == null) {
  359. // cell can fill to end of the line
  360. return Bounds.Width - current.X;
  361. }
  362. else {
  363. // cell can fill up to next cell start
  364. return next.X - current.X;
  365. }
  366. }
  367. private void RenderHeaderUnderline(int row,int availableWidth, ColumnToRender[] columnsToRender)
  368. {
  369. // Renders a line below the table headers (when visible) like:
  370. // ├──────────┼───────────┼───────────────────┼──────────┼────────┼─────────────┤
  371. for(int c = 0;c< availableWidth;c++) {
  372. var rune = Driver.HLine;
  373. if (Style.ShowVerticalHeaderLines){
  374. if(c == 0){
  375. rune = Style.ShowVerticalCellLines ? Driver.LeftTee : Driver.LLCorner;
  376. }
  377. // if the next column is the start of a header
  378. else if(columnsToRender.Any(r=>r.X == c+1)){
  379. /*TODO: is ┼ symbol in Driver?*/
  380. rune = Style.ShowVerticalCellLines ? '┼' :Driver.BottomTee;
  381. }
  382. else if(c == availableWidth -1){
  383. rune = Style.ShowVerticalCellLines ? Driver.RightTee : Driver.LRCorner;
  384. }
  385. }
  386. AddRuneAt(Driver,c,row,rune);
  387. }
  388. }
  389. private void RenderRow(int row, int rowToRender, ColumnToRender[] columnsToRender)
  390. {
  391. //render start of line
  392. if(style.ShowVerticalCellLines)
  393. AddRune(0,row,Driver.VLine);
  394. // Render cells for each visible header for the current row
  395. for(int i=0;i< columnsToRender.Length ;i++) {
  396. var current = columnsToRender[i];
  397. var availableWidthForCell = GetCellWidth(columnsToRender,i);
  398. var colStyle = Style.GetColumnStyleIfAny(current.Column);
  399. // move to start of cell (in line with header positions)
  400. Move (current.X, row);
  401. // Set color scheme based on whether the current cell is the selected one
  402. bool isSelectedCell = IsSelected(current.Column.Ordinal,rowToRender);
  403. Driver.SetAttribute (isSelectedCell ? ColorScheme.HotFocus : ColorScheme.Normal);
  404. var val = Table.Rows [rowToRender][current.Column];
  405. // Render the (possibly truncated) cell value
  406. var representation = GetRepresentation(val,colStyle);
  407. Driver.AddStr (TruncateOrPad(val,representation,availableWidthForCell,colStyle));
  408. // If not in full row select mode always, reset color scheme to normal and render the vertical line (or space) at the end of the cell
  409. if(!FullRowSelect)
  410. Driver.SetAttribute (ColorScheme.Normal);
  411. RenderSeparator(current.X-1,row,false);
  412. }
  413. //render end of line
  414. if(style.ShowVerticalCellLines)
  415. AddRune(Bounds.Width-1,row,Driver.VLine);
  416. }
  417. private void RenderSeparator(int col, int row,bool isHeader)
  418. {
  419. if(col<0)
  420. return;
  421. var renderLines = isHeader ? style.ShowVerticalHeaderLines : style.ShowVerticalCellLines;
  422. Rune symbol = renderLines ? Driver.VLine : SeparatorSymbol;
  423. AddRune(col,row,symbol);
  424. }
  425. void AddRuneAt (ConsoleDriver d,int col, int row, Rune ch)
  426. {
  427. Move (col, row);
  428. d.AddRune (ch);
  429. }
  430. /// <summary>
  431. /// 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)
  432. /// </summary>
  433. /// <param name="originalCellValue">The object in this cell of the <see cref="Table"/></param>
  434. /// <param name="representation">The string representation of <paramref name="originalCellValue"/></param>
  435. /// <param name="availableHorizontalSpace"></param>
  436. /// <param name="colStyle">Optional style indicating custom alignment for the cell</param>
  437. /// <returns></returns>
  438. private string TruncateOrPad (object originalCellValue,string representation, int availableHorizontalSpace, ColumnStyle colStyle)
  439. {
  440. if (string.IsNullOrEmpty (representation))
  441. return representation;
  442. // if value is not wide enough
  443. if(representation.Sum(c=>Rune.ColumnWidth(c)) < availableHorizontalSpace) {
  444. // pad it out with spaces to the given alignment
  445. int toPad = availableHorizontalSpace - (representation.Sum(c=>Rune.ColumnWidth(c)) +1 /*leave 1 space for cell boundary*/);
  446. switch(colStyle?.GetAlignment(originalCellValue) ?? TextAlignment.Left) {
  447. case TextAlignment.Left :
  448. return representation + new string(' ',toPad);
  449. case TextAlignment.Right :
  450. return new string(' ',toPad) + representation;
  451. // TODO: With single line cells, centered and justified are the same right?
  452. case TextAlignment.Centered :
  453. case TextAlignment.Justified :
  454. return
  455. new string(' ',(int)Math.Floor(toPad/2.0)) + // round down
  456. representation +
  457. new string(' ',(int)Math.Ceiling(toPad/2.0)) ; // round up
  458. }
  459. }
  460. // value is too wide
  461. return new string(representation.TakeWhile(c=>(availableHorizontalSpace-= Rune.ColumnWidth(c))>0).ToArray());
  462. }
  463. /// <inheritdoc/>
  464. public override bool ProcessKey (KeyEvent keyEvent)
  465. {
  466. if(Table == null){
  467. PositionCursor ();
  468. return false;
  469. }
  470. if(keyEvent.Key == CellActivationKey && Table != null) {
  471. OnCellActivated(new CellActivatedEventArgs(Table,SelectedColumn,SelectedRow));
  472. return true;
  473. }
  474. switch (keyEvent.Key) {
  475. case Key.CursorLeft:
  476. case Key.CursorLeft | Key.ShiftMask:
  477. ChangeSelectionByOffset(-1,0,keyEvent.Key.HasFlag(Key.ShiftMask));
  478. Update ();
  479. break;
  480. case Key.CursorRight:
  481. case Key.CursorRight | Key.ShiftMask:
  482. ChangeSelectionByOffset(1,0,keyEvent.Key.HasFlag(Key.ShiftMask));
  483. Update ();
  484. break;
  485. case Key.CursorDown:
  486. case Key.CursorDown | Key.ShiftMask:
  487. ChangeSelectionByOffset(0,1,keyEvent.Key.HasFlag(Key.ShiftMask));
  488. Update ();
  489. break;
  490. case Key.CursorUp:
  491. case Key.CursorUp | Key.ShiftMask:
  492. ChangeSelectionByOffset(0,-1,keyEvent.Key.HasFlag(Key.ShiftMask));
  493. Update ();
  494. break;
  495. case Key.PageUp:
  496. case Key.PageUp | Key.ShiftMask:
  497. ChangeSelectionByOffset(0,-(Bounds.Height - GetHeaderHeightIfAny()),keyEvent.Key.HasFlag(Key.ShiftMask));
  498. Update ();
  499. break;
  500. case Key.PageDown:
  501. case Key.PageDown | Key.ShiftMask:
  502. ChangeSelectionByOffset(0,Bounds.Height - GetHeaderHeightIfAny(),keyEvent.Key.HasFlag(Key.ShiftMask));
  503. Update ();
  504. break;
  505. case Key.Home | Key.CtrlMask:
  506. case Key.Home | Key.CtrlMask | Key.ShiftMask:
  507. // jump to table origin
  508. SetSelection(0,0,keyEvent.Key.HasFlag(Key.ShiftMask));
  509. Update ();
  510. break;
  511. case Key.Home:
  512. case Key.Home | Key.ShiftMask:
  513. // jump to start of line
  514. SetSelection(0,SelectedRow,keyEvent.Key.HasFlag(Key.ShiftMask));
  515. Update ();
  516. break;
  517. case Key.End | Key.CtrlMask:
  518. case Key.End | Key.CtrlMask | Key.ShiftMask:
  519. // jump to end of table
  520. SetSelection(Table.Columns.Count - 1, Table.Rows.Count - 1, keyEvent.Key.HasFlag(Key.ShiftMask));
  521. Update ();
  522. break;
  523. case Key.A | Key.CtrlMask:
  524. SelectAll();
  525. Update ();
  526. break;
  527. case Key.End:
  528. case Key.End | Key.ShiftMask:
  529. //jump to end of row
  530. SetSelection(Table.Columns.Count - 1,SelectedRow, keyEvent.Key.HasFlag(Key.ShiftMask));
  531. Update ();
  532. break;
  533. default:
  534. // Not a keystroke we care about
  535. return false;
  536. }
  537. PositionCursor ();
  538. return true;
  539. }
  540. /// <summary>
  541. /// Moves the <see cref="SelectedRow"/> and <see cref="SelectedColumn"/> to the given col/row in <see cref="Table"/>. Optionally starting a box selection (see <see cref="MultiSelect"/>)
  542. /// </summary>
  543. /// <param name="col"></param>
  544. /// <param name="row"></param>
  545. /// <param name="extendExistingSelection">True to create a multi cell selection or adjust an existing one</param>
  546. public void SetSelection (int col, int row, bool extendExistingSelection)
  547. {
  548. if(!MultiSelect || !extendExistingSelection)
  549. MultiSelectedRegions.Clear();
  550. if(extendExistingSelection)
  551. {
  552. // If we are extending current selection but there isn't one
  553. if(MultiSelectedRegions.Count == 0)
  554. {
  555. // Create a new region between the old active cell and the new cell
  556. var rect = CreateTableSelection(SelectedColumn,SelectedRow,col,row);
  557. MultiSelectedRegions.Push(rect);
  558. }
  559. else
  560. {
  561. // Extend the current head selection to include the new cell
  562. var head = MultiSelectedRegions.Pop();
  563. var newRect = CreateTableSelection(head.Origin.X,head.Origin.Y,col,row);
  564. MultiSelectedRegions.Push(newRect);
  565. }
  566. }
  567. SelectedColumn = col;
  568. SelectedRow = row;
  569. }
  570. /// <summary>
  571. /// Moves the <see cref="SelectedRow"/> and <see cref="SelectedColumn"/> by the provided offsets. Optionally starting a box selection (see <see cref="MultiSelect"/>)
  572. /// </summary>
  573. /// <param name="offsetX">Offset in number of columns</param>
  574. /// <param name="offsetY">Offset in number of rows</param>
  575. /// <param name="extendExistingSelection">True to create a multi cell selection or adjust an existing one</param>
  576. public void ChangeSelectionByOffset (int offsetX, int offsetY, bool extendExistingSelection)
  577. {
  578. SetSelection(SelectedColumn + offsetX, SelectedRow + offsetY,extendExistingSelection);
  579. }
  580. /// <summary>
  581. /// When <see cref="MultiSelect"/> is on, creates selection over all cells in the table (replacing any old selection regions)
  582. /// </summary>
  583. public void SelectAll()
  584. {
  585. if(Table == null || !MultiSelect || Table.Rows.Count == 0)
  586. return;
  587. MultiSelectedRegions.Clear();
  588. // Create a single region over entire table, set the origin of the selection to the active cell so that a followup spread selection e.g. shift-right behaves properly
  589. MultiSelectedRegions.Push(new TableSelection(new Point(SelectedColumn,SelectedRow), new Rect(0,0,Table.Columns.Count,table.Rows.Count)));
  590. Update();
  591. }
  592. /// <summary>
  593. /// Returns all cells in any <see cref="MultiSelectedRegions"/> (if <see cref="MultiSelect"/> is enabled) and the selected cell
  594. /// </summary>
  595. /// <returns></returns>
  596. public IEnumerable<Point> GetAllSelectedCells()
  597. {
  598. if(Table == null || Table.Rows.Count == 0)
  599. yield break;
  600. EnsureValidSelection();
  601. // If there are one or more rectangular selections
  602. if(MultiSelect && MultiSelectedRegions.Any()){
  603. // Quiz any cells for whether they are selected. For performance we only need to check those between the top left and lower right vertex of selection regions
  604. var yMin = MultiSelectedRegions.Min(r=>r.Rect.Top);
  605. var yMax = MultiSelectedRegions.Max(r=>r.Rect.Bottom);
  606. var xMin = FullRowSelect ? 0 : MultiSelectedRegions.Min(r=>r.Rect.Left);
  607. var xMax = FullRowSelect ? Table.Columns.Count : MultiSelectedRegions.Max(r=>r.Rect.Right);
  608. for(int y = yMin ; y < yMax ; y++)
  609. {
  610. for(int x = xMin ; x < xMax ; x++)
  611. {
  612. if(IsSelected(x,y)){
  613. yield return new Point(x,y);
  614. }
  615. }
  616. }
  617. }
  618. else{
  619. // if there are no region selections then it is just the active cell
  620. // if we are selecting the full row
  621. if(FullRowSelect)
  622. {
  623. // all cells in active row are selected
  624. for(int x =0;x<Table.Columns.Count;x++){
  625. yield return new Point(x,SelectedRow);
  626. }
  627. }
  628. else
  629. {
  630. // Not full row select and no multi selections
  631. yield return new Point(SelectedColumn,SelectedRow);
  632. }
  633. }
  634. }
  635. /// <summary>
  636. /// Returns a new rectangle between the two points with positive width/height regardless of relative positioning of the points. pt1 is always considered the <see cref="TableSelection.Origin"/> point
  637. /// </summary>
  638. /// <param name="pt1X">Origin point for the selection in X</param>
  639. /// <param name="pt1Y">Origin point for the selection in Y</param>
  640. /// <param name="pt2X">End point for the selection in X</param>
  641. /// <param name="pt2Y">End point for the selection in Y</param>
  642. /// <returns></returns>
  643. private TableSelection CreateTableSelection (int pt1X, int pt1Y, int pt2X, int pt2Y)
  644. {
  645. var top = Math.Min(pt1Y,pt2Y);
  646. var bot = Math.Max(pt1Y,pt2Y);
  647. var left = Math.Min(pt1X,pt2X);
  648. var right = Math.Max(pt1X,pt2X);
  649. // Rect class is inclusive of Top Left but exclusive of Bottom Right so extend by 1
  650. return new TableSelection(new Point(pt1X,pt1Y),new Rect(left,top,right-left + 1,bot-top + 1));
  651. }
  652. /// <summary>
  653. /// Returns true if the given cell is selected either because it is the active cell or part of a multi cell selection (e.g. <see cref="FullRowSelect"/>)
  654. /// </summary>
  655. /// <param name="col"></param>
  656. /// <param name="row"></param>
  657. /// <returns></returns>
  658. public bool IsSelected(int col, int row)
  659. {
  660. // Cell is also selected if in any multi selection region
  661. if(MultiSelect && MultiSelectedRegions.Any(r=>r.Rect.Contains(col,row)))
  662. return true;
  663. // Cell is also selected if Y axis appears in any region (when FullRowSelect is enabled)
  664. if(FullRowSelect && MultiSelect && MultiSelectedRegions.Any(r=>r.Rect.Bottom> row && r.Rect.Top <= row))
  665. return true;
  666. return row == SelectedRow &&
  667. (col == SelectedColumn || FullRowSelect);
  668. }
  669. /// <summary>
  670. /// Positions the cursor in the area of the screen in which the start of the active cell is rendered. Calls base implementation if active cell is not visible due to scrolling or table is loaded etc
  671. /// </summary>
  672. public override void PositionCursor()
  673. {
  674. if(Table == null) {
  675. base.PositionCursor();
  676. return;
  677. }
  678. var screenPoint = CellToScreen(SelectedColumn,SelectedRow);
  679. if(screenPoint != null)
  680. Move(screenPoint.Value.X,screenPoint.Value.Y);
  681. }
  682. ///<inheritdoc/>
  683. public override bool MouseEvent (MouseEvent me)
  684. {
  685. if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) &&
  686. me.Flags != MouseFlags.WheeledDown && me.Flags != MouseFlags.WheeledUp &&
  687. me.Flags != MouseFlags.WheeledLeft && me.Flags != MouseFlags.WheeledRight)
  688. return false;
  689. if (!HasFocus && CanFocus) {
  690. SetFocus ();
  691. }
  692. if (Table == null) {
  693. return false;
  694. }
  695. // Scroll wheel flags
  696. switch(me.Flags)
  697. {
  698. case MouseFlags.WheeledDown:
  699. RowOffset++;
  700. EnsureValidScrollOffsets();
  701. SetNeedsDisplay();
  702. return true;
  703. case MouseFlags.WheeledUp:
  704. RowOffset--;
  705. EnsureValidScrollOffsets();
  706. SetNeedsDisplay();
  707. return true;
  708. case MouseFlags.WheeledRight:
  709. ColumnOffset++;
  710. EnsureValidScrollOffsets();
  711. SetNeedsDisplay();
  712. return true;
  713. case MouseFlags.WheeledLeft:
  714. ColumnOffset--;
  715. EnsureValidScrollOffsets();
  716. SetNeedsDisplay();
  717. return true;
  718. }
  719. if(me.Flags.HasFlag(MouseFlags.Button1Clicked)) {
  720. var hit = ScreenToCell(me.X,me.Y);
  721. if(hit != null) {
  722. SetSelection(hit.Value.X,hit.Value.Y,me.Flags.HasFlag(MouseFlags.ButtonShift));
  723. Update();
  724. }
  725. }
  726. // Double clicking a cell activates
  727. if(me.Flags == MouseFlags.Button1DoubleClicked) {
  728. var hit = ScreenToCell(me.X,me.Y);
  729. if(hit!= null) {
  730. OnCellActivated(new CellActivatedEventArgs(Table,hit.Value.X,hit.Value.Y));
  731. }
  732. }
  733. return false;
  734. }
  735. /// <summary>
  736. /// Returns the column and row of <see cref="Table"/> that corresponds to a given point on the screen (relative to the control client area). Returns null if the point is in the header, no table is loaded or outside the control bounds
  737. /// </summary>
  738. /// <param name="clientX">X offset from the top left of the control</param>
  739. /// <param name="clientY">Y offset from the top left of the control</param>
  740. /// <returns></returns>
  741. public Point? ScreenToCell (int clientX, int clientY)
  742. {
  743. if(Table == null)
  744. return null;
  745. var viewPort = CalculateViewport(Bounds);
  746. var headerHeight = GetHeaderHeightIfAny();
  747. var col = viewPort.LastOrDefault(c=>c.X <= clientX);
  748. // Click is on the header section of rendered UI
  749. if(clientY < headerHeight)
  750. return null;
  751. var rowIdx = RowOffset - headerHeight + clientY;
  752. if(col != null && rowIdx >= 0) {
  753. return new Point(col.Column.Ordinal,rowIdx);
  754. }
  755. return null;
  756. }
  757. /// <summary>
  758. /// Returns the screen position (relative to the control client area) that the given cell is rendered or null if it is outside the current scroll area or no table is loaded
  759. /// </summary>
  760. /// <param name="tableColumn">The index of the <see cref="Table"/> column you are looking for, use <see cref="DataColumn.Ordinal"/></param>
  761. /// <param name="tableRow">The index of the row in <see cref="Table"/> that you are looking for</param>
  762. /// <returns></returns>
  763. public Point? CellToScreen (int tableColumn, int tableRow)
  764. {
  765. if(Table == null)
  766. return null;
  767. var viewPort = CalculateViewport(Bounds);
  768. var headerHeight = GetHeaderHeightIfAny();
  769. var colHit = viewPort.FirstOrDefault(c=>c.Column.Ordinal == tableColumn);
  770. // current column is outside the scroll area
  771. if(colHit == null)
  772. return null;
  773. // the cell is too far up above the current scroll area
  774. if(RowOffset > tableRow)
  775. return null;
  776. // the cell is way down below the scroll area and off the screen
  777. if(tableRow > RowOffset + (Bounds.Height - headerHeight))
  778. return null;
  779. return new Point(colHit.X,tableRow + headerHeight - RowOffset);
  780. }
  781. /// <summary>
  782. /// Updates the view to reflect changes to <see cref="Table"/> and to (<see cref="ColumnOffset"/> / <see cref="RowOffset"/>) etc
  783. /// </summary>
  784. /// <remarks>This always calls <see cref="View.SetNeedsDisplay()"/></remarks>
  785. public void Update()
  786. {
  787. if(Table == null) {
  788. SetNeedsDisplay ();
  789. return;
  790. }
  791. EnsureValidScrollOffsets();
  792. EnsureValidSelection();
  793. EnsureSelectedCellIsVisible();
  794. SetNeedsDisplay ();
  795. }
  796. /// <summary>
  797. /// 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.
  798. /// </summary>
  799. /// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
  800. public void EnsureValidScrollOffsets ()
  801. {
  802. if(Table == null){
  803. return;
  804. }
  805. ColumnOffset = Math.Max(Math.Min(ColumnOffset,Table.Columns.Count -1),0);
  806. RowOffset = Math.Max(Math.Min(RowOffset,Table.Rows.Count -1),0);
  807. }
  808. /// <summary>
  809. /// Updates <see cref="SelectedColumn"/>, <see cref="SelectedRow"/> and <see cref="MultiSelectedRegions"/> 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.
  810. /// </summary>
  811. /// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
  812. public void EnsureValidSelection()
  813. {
  814. if(Table == null){
  815. // Table doesn't exist, we should probably clear those selections
  816. MultiSelectedRegions.Clear();
  817. return;
  818. }
  819. SelectedColumn = Math.Max(Math.Min(SelectedColumn,Table.Columns.Count -1),0);
  820. SelectedRow = Math.Max(Math.Min(SelectedRow,Table.Rows.Count -1),0);
  821. var oldRegions = MultiSelectedRegions.ToArray().Reverse();
  822. MultiSelectedRegions.Clear();
  823. // evaluate
  824. foreach(var region in oldRegions)
  825. {
  826. // ignore regions entirely below current table state
  827. if(region.Rect.Top >= Table.Rows.Count)
  828. continue;
  829. // ignore regions entirely too far right of table columns
  830. if(region.Rect.Left >= Table.Columns.Count)
  831. continue;
  832. // ensure region's origin exists
  833. region.Origin = new Point(
  834. Math.Max(Math.Min(region.Origin.X,Table.Columns.Count -1),0),
  835. Math.Max(Math.Min(region.Origin.Y,Table.Rows.Count -1),0));
  836. // ensure regions do not go over edge of table bounds
  837. region.Rect = Rect.FromLTRB(region.Rect.Left,
  838. region.Rect.Top,
  839. Math.Max(Math.Min(region.Rect.Right, Table.Columns.Count ),0),
  840. Math.Max(Math.Min(region.Rect.Bottom,Table.Rows.Count),0)
  841. );
  842. MultiSelectedRegions.Push(region);
  843. }
  844. }
  845. /// <summary>
  846. /// Updates scroll offsets to ensure that the selected cell is visible. Has no effect if <see cref="Table"/> has not been set.
  847. /// </summary>
  848. /// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
  849. public void EnsureSelectedCellIsVisible ()
  850. {
  851. if(Table == null || Table.Columns.Count <= 0){
  852. return;
  853. }
  854. var columnsToRender = CalculateViewport (Bounds).ToArray();
  855. var headerHeight = GetHeaderHeightIfAny();
  856. //if we have scrolled too far to the left
  857. if (SelectedColumn < columnsToRender.Min (r => r.Column.Ordinal)) {
  858. ColumnOffset = SelectedColumn;
  859. }
  860. //if we have scrolled too far to the right
  861. if (SelectedColumn > columnsToRender.Max (r=> r.Column.Ordinal)) {
  862. ColumnOffset = SelectedColumn;
  863. }
  864. //if we have scrolled too far down
  865. if (SelectedRow >= RowOffset + (Bounds.Height - headerHeight)) {
  866. RowOffset = SelectedRow;
  867. }
  868. //if we have scrolled too far up
  869. if (SelectedRow < RowOffset) {
  870. RowOffset = SelectedRow;
  871. }
  872. }
  873. /// <summary>
  874. /// Invokes the <see cref="SelectedCellChanged"/> event
  875. /// </summary>
  876. protected virtual void OnSelectedCellChanged(SelectedCellChangedEventArgs args)
  877. {
  878. SelectedCellChanged?.Invoke(args);
  879. }
  880. /// <summary>
  881. /// Invokes the <see cref="CellActivated"/> event
  882. /// </summary>
  883. /// <param name="args"></param>
  884. protected virtual void OnCellActivated (CellActivatedEventArgs args)
  885. {
  886. CellActivated?.Invoke(args);
  887. }
  888. /// <summary>
  889. /// Calculates which columns should be rendered given the <paramref name="bounds"/> in which to display and the <see cref="ColumnOffset"/>
  890. /// </summary>
  891. /// <param name="bounds"></param>
  892. /// <param name="padding"></param>
  893. /// <returns></returns>
  894. private IEnumerable<ColumnToRender> CalculateViewport (Rect bounds, int padding = 1)
  895. {
  896. if(Table == null)
  897. yield break;
  898. int usedSpace = 0;
  899. //if horizontal space is required at the start of the line (before the first header)
  900. if(Style.ShowVerticalHeaderLines || Style.ShowVerticalCellLines)
  901. usedSpace+=1;
  902. int availableHorizontalSpace = bounds.Width;
  903. int rowsToRender = bounds.Height;
  904. // reserved for the headers row
  905. if(ShouldRenderHeaders())
  906. rowsToRender -= GetHeaderHeight();
  907. bool first = true;
  908. foreach (var col in Table.Columns.Cast<DataColumn>().Skip (ColumnOffset)) {
  909. int startingIdxForCurrentHeader = usedSpace;
  910. var colStyle = Style.GetColumnStyleIfAny(col);
  911. // is there enough space for this column (and it's data)?
  912. usedSpace += CalculateMaxCellWidth (col, rowsToRender,colStyle) + padding;
  913. // no (don't render it) unless its the only column we are render (that must be one massively wide column!)
  914. if (!first && usedSpace > availableHorizontalSpace)
  915. yield break;
  916. // there is space
  917. yield return new ColumnToRender(col, startingIdxForCurrentHeader);
  918. first=false;
  919. }
  920. }
  921. private bool ShouldRenderHeaders()
  922. {
  923. if(Table == null || Table.Columns.Count == 0)
  924. return false;
  925. return Style.AlwaysShowHeaders || rowOffset == 0;
  926. }
  927. /// <summary>
  928. /// 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"/>
  929. /// </summary>
  930. /// <param name="col"></param>
  931. /// <param name="rowsToRender"></param>
  932. /// <param name="colStyle"></param>
  933. /// <returns></returns>
  934. private int CalculateMaxCellWidth(DataColumn col, int rowsToRender,ColumnStyle colStyle)
  935. {
  936. int spaceRequired = col.ColumnName.Sum(c=>Rune.ColumnWidth(c));
  937. // if table has no rows
  938. if(RowOffset < 0)
  939. return spaceRequired;
  940. for (int i = RowOffset; i < RowOffset + rowsToRender && i < Table.Rows.Count; i++) {
  941. //expand required space if cell is bigger than the last biggest cell or header
  942. spaceRequired = Math.Max (spaceRequired, GetRepresentation(Table.Rows [i][col],colStyle).Sum(c=>Rune.ColumnWidth(c)));
  943. }
  944. // Don't require more space than the style allows
  945. if(colStyle != null){
  946. // enforce maximum cell width based on style
  947. if(spaceRequired > colStyle.MaxWidth) {
  948. spaceRequired = colStyle.MaxWidth;
  949. }
  950. // enforce minimum cell width based on style
  951. if(spaceRequired < colStyle.MinWidth) {
  952. spaceRequired = colStyle.MinWidth;
  953. }
  954. }
  955. // enforce maximum cell width based on global table style
  956. if(spaceRequired > MaxCellWidth)
  957. spaceRequired = MaxCellWidth;
  958. return spaceRequired;
  959. }
  960. /// <summary>
  961. /// Returns the value that should be rendered to best represent a strongly typed <paramref name="value"/> read from <see cref="Table"/>
  962. /// </summary>
  963. /// <param name="value"></param>
  964. /// <param name="colStyle">Optional style defining how to represent cell values</param>
  965. /// <returns></returns>
  966. private string GetRepresentation(object value,ColumnStyle colStyle)
  967. {
  968. if (value == null || value == DBNull.Value) {
  969. return NullSymbol;
  970. }
  971. return colStyle != null ? colStyle.GetRepresentation(value): value.ToString();
  972. }
  973. }
  974. /// <summary>
  975. /// Describes a desire to render a column at a given horizontal position in the UI
  976. /// </summary>
  977. internal class ColumnToRender {
  978. /// <summary>
  979. /// The column to render
  980. /// </summary>
  981. public DataColumn Column {get;set;}
  982. /// <summary>
  983. /// The horizontal position to begin rendering the column at
  984. /// </summary>
  985. public int X{get;set;}
  986. public ColumnToRender (DataColumn col, int x)
  987. {
  988. Column = col;
  989. X = x;
  990. }
  991. }
  992. /// <summary>
  993. /// Defines the event arguments for <see cref="TableView.SelectedCellChanged"/>
  994. /// </summary>
  995. public class SelectedCellChangedEventArgs : EventArgs
  996. {
  997. /// <summary>
  998. /// 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
  999. /// </summary>
  1000. /// <value></value>
  1001. public DataTable Table {get;}
  1002. /// <summary>
  1003. /// 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
  1004. /// </summary>
  1005. /// <value></value>
  1006. public int OldCol {get;}
  1007. /// <summary>
  1008. /// The newly selected column index.
  1009. /// </summary>
  1010. /// <value></value>
  1011. public int NewCol {get;}
  1012. /// <summary>
  1013. /// 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
  1014. /// </summary>
  1015. /// <value></value>
  1016. public int OldRow {get;}
  1017. /// <summary>
  1018. /// The newly selected row index.
  1019. /// </summary>
  1020. /// <value></value>
  1021. public int NewRow {get;}
  1022. /// <summary>
  1023. /// Creates a new instance of arguments describing a change in selected cell in a <see cref="TableView"/>
  1024. /// </summary>
  1025. /// <param name="t"></param>
  1026. /// <param name="oldCol"></param>
  1027. /// <param name="newCol"></param>
  1028. /// <param name="oldRow"></param>
  1029. /// <param name="newRow"></param>
  1030. public SelectedCellChangedEventArgs(DataTable t, int oldCol, int newCol, int oldRow, int newRow)
  1031. {
  1032. Table = t;
  1033. OldCol = oldCol;
  1034. NewCol = newCol;
  1035. OldRow = oldRow;
  1036. NewRow = newRow;
  1037. }
  1038. }
  1039. /// <summary>
  1040. /// Describes a selected region of the table
  1041. /// </summary>
  1042. public class TableSelection
  1043. {
  1044. /// <summary>
  1045. /// Corner of the <see cref="Rect"/> where selection began
  1046. /// </summary>
  1047. /// <value></value>
  1048. public Point Origin{get;set;}
  1049. /// <summary>
  1050. /// Area selected
  1051. /// </summary>
  1052. /// <value></value>
  1053. public Rect Rect { get; set;}
  1054. /// <summary>
  1055. /// Creates a new selected area starting at the origin corner and covering the provided rectangular area
  1056. /// </summary>
  1057. /// <param name="origin"></param>
  1058. /// <param name="rect"></param>
  1059. public TableSelection(Point origin, Rect rect)
  1060. {
  1061. Origin = origin;
  1062. Rect = rect;
  1063. }
  1064. }
  1065. /// <summary>
  1066. /// Defines the event arguments for <see cref="TableView.CellActivated"/> event
  1067. /// </summary>
  1068. public class CellActivatedEventArgs : EventArgs
  1069. {
  1070. /// <summary>
  1071. /// 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
  1072. /// </summary>
  1073. /// <value></value>
  1074. public DataTable Table {get;}
  1075. /// <summary>
  1076. /// The column index of the <see cref="Table"/> cell that is being activated
  1077. /// </summary>
  1078. /// <value></value>
  1079. public int Col {get;}
  1080. /// <summary>
  1081. /// The row index of the <see cref="Table"/> cell that is being activated
  1082. /// </summary>
  1083. /// <value></value>
  1084. public int Row {get;}
  1085. /// <summary>
  1086. /// Creates a new instance of arguments describing a cell being activated in <see cref="TableView"/>
  1087. /// </summary>
  1088. /// <param name="t"></param>
  1089. /// <param name="col"></param>
  1090. /// <param name="row"></param>
  1091. public CellActivatedEventArgs(DataTable t, int col, int row)
  1092. {
  1093. Table = t;
  1094. Col = col;
  1095. Row = row;
  1096. }
  1097. }
  1098. }