TableView.cs 70 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096
  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. /// View for tabular data based on a <see cref="DataTable"/>.
  9. ///
  10. /// <a href="https://gui-cs.github.io/Terminal.Gui/articles/tableview.html">See TableView Deep Dive for more information</a>.
  11. /// </summary>
  12. public class TableView : View {
  13. /// <summary>
  14. /// Defines the event arguments for <see cref="TableView.CellActivated"/> event
  15. /// </summary>
  16. public class CellActivatedEventArgs : EventArgs {
  17. /// <summary>
  18. /// 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
  19. /// </summary>
  20. /// <value></value>
  21. public DataTable Table { get; }
  22. /// <summary>
  23. /// The column index of the <see cref="Table"/> cell that is being activated
  24. /// </summary>
  25. /// <value></value>
  26. public int Col { get; }
  27. /// <summary>
  28. /// The row index of the <see cref="Table"/> cell that is being activated
  29. /// </summary>
  30. /// <value></value>
  31. public int Row { get; }
  32. /// <summary>
  33. /// Creates a new instance of arguments describing a cell being activated in <see cref="TableView"/>
  34. /// </summary>
  35. /// <param name="t"></param>
  36. /// <param name="col"></param>
  37. /// <param name="row"></param>
  38. public CellActivatedEventArgs (DataTable t, int col, int row)
  39. {
  40. Table = t;
  41. Col = col;
  42. Row = row;
  43. }
  44. }
  45. private int columnOffset;
  46. private int rowOffset;
  47. private int selectedRow;
  48. private int selectedColumn;
  49. private DataTable table;
  50. private TableStyle style = new TableStyle ();
  51. private Key cellActivationKey = Key.Enter;
  52. Point? scrollLeftPoint;
  53. Point? scrollRightPoint;
  54. /// <summary>
  55. /// The default maximum cell width for <see cref="TableView.MaxCellWidth"/> and <see cref="ColumnStyle.MaxWidth"/>
  56. /// </summary>
  57. public const int DefaultMaxCellWidth = 100;
  58. /// <summary>
  59. /// The default minimum cell width for <see cref="ColumnStyle.MinAcceptableWidth"/>
  60. /// </summary>
  61. public const int DefaultMinAcceptableWidth = 100;
  62. /// <summary>
  63. /// The data table to render in the view. Setting this property automatically updates and redraws the control.
  64. /// </summary>
  65. public DataTable Table { get => table; set { table = value; Update (); } }
  66. /// <summary>
  67. /// Contains options for changing how the table is rendered
  68. /// </summary>
  69. public TableStyle Style { get => style; set { style = value; Update (); } }
  70. /// <summary>
  71. /// True to select the entire row at once. False to select individual cells. Defaults to false
  72. /// </summary>
  73. public bool FullRowSelect { get; set; }
  74. /// <summary>
  75. /// True to allow regions to be selected
  76. /// </summary>
  77. /// <value></value>
  78. public bool MultiSelect { get; set; } = true;
  79. /// <summary>
  80. /// 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)
  81. /// </summary>
  82. /// <returns></returns>
  83. public Stack<TableSelection> MultiSelectedRegions { get; private set; } = new Stack<TableSelection> ();
  84. /// <summary>
  85. /// Horizontal scroll offset. The index of the first column in <see cref="Table"/> to display when when rendering the view.
  86. /// </summary>
  87. /// <remarks>This property allows very wide tables to be rendered with horizontal scrolling</remarks>
  88. public int ColumnOffset {
  89. get => columnOffset;
  90. //try to prevent this being set to an out of bounds column
  91. set => columnOffset = TableIsNullOrInvisible () ? 0 : Math.Max (0, Math.Min (Table.Columns.Count - 1, value));
  92. }
  93. /// <summary>
  94. /// 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.
  95. /// </summary>
  96. public int RowOffset {
  97. get => rowOffset;
  98. set => rowOffset = TableIsNullOrInvisible () ? 0 : Math.Max (0, Math.Min (Table.Rows.Count - 1, value));
  99. }
  100. /// <summary>
  101. /// The index of <see cref="DataTable.Columns"/> in <see cref="Table"/> that the user has currently selected
  102. /// </summary>
  103. public int SelectedColumn {
  104. get => selectedColumn;
  105. set {
  106. var oldValue = selectedColumn;
  107. //try to prevent this being set to an out of bounds column
  108. selectedColumn = TableIsNullOrInvisible () ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value));
  109. if (oldValue != selectedColumn)
  110. OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, oldValue, SelectedColumn, SelectedRow, SelectedRow));
  111. }
  112. }
  113. /// <summary>
  114. /// The index of <see cref="DataTable.Rows"/> in <see cref="Table"/> that the user has currently selected
  115. /// </summary>
  116. public int SelectedRow {
  117. get => selectedRow;
  118. set {
  119. var oldValue = selectedRow;
  120. selectedRow = TableIsNullOrInvisible () ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value));
  121. if (oldValue != selectedRow)
  122. OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, SelectedColumn, SelectedColumn, oldValue, selectedRow));
  123. }
  124. }
  125. /// <summary>
  126. /// The maximum number of characters to render in any given column. This prevents one long column from pushing out all the others
  127. /// </summary>
  128. public int MaxCellWidth { get; set; } = DefaultMaxCellWidth;
  129. /// <summary>
  130. /// The text representation that should be rendered for cells with the value <see cref="DBNull.Value"/>
  131. /// </summary>
  132. public string NullSymbol { get; set; } = "-";
  133. /// <summary>
  134. /// The symbol to add after each cell value and header value to visually seperate values (if not using vertical gridlines)
  135. /// </summary>
  136. public char SeparatorSymbol { get; set; } = ' ';
  137. /// <summary>
  138. /// This event is raised when the selected cell in the table changes.
  139. /// </summary>
  140. public event Action<SelectedCellChangedEventArgs> SelectedCellChanged;
  141. /// <summary>
  142. /// This event is raised when a cell is activated e.g. by double clicking or pressing <see cref="CellActivationKey"/>
  143. /// </summary>
  144. public event Action<CellActivatedEventArgs> CellActivated;
  145. /// <summary>
  146. /// The key which when pressed should trigger <see cref="CellActivated"/> event. Defaults to Enter.
  147. /// </summary>
  148. public Key CellActivationKey {
  149. get => cellActivationKey;
  150. set {
  151. if (cellActivationKey != value) {
  152. ReplaceKeyBinding (cellActivationKey, value);
  153. // of API user is mixing and matching old and new methods of keybinding then they may have lost
  154. // the old binding (e.g. with ClearKeybindings) so ReplaceKeyBinding alone will fail
  155. AddKeyBinding (value, Command.Accept);
  156. cellActivationKey = value;
  157. }
  158. }
  159. }
  160. /// <summary>
  161. /// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout.
  162. /// </summary>
  163. /// <param name="table">The table to display in the control</param>
  164. public TableView (DataTable table) : this ()
  165. {
  166. this.Table = table;
  167. }
  168. /// <summary>
  169. /// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout. Set the <see cref="Table"/> property to begin editing
  170. /// </summary>
  171. public TableView () : base ()
  172. {
  173. CanFocus = true;
  174. // Things this view knows how to do
  175. AddCommand (Command.Right, () => { ChangeSelectionByOffset (1, 0, false); return true; });
  176. AddCommand (Command.Left, () => { ChangeSelectionByOffset (-1, 0, false); return true; });
  177. AddCommand (Command.LineUp, () => { ChangeSelectionByOffset (0, -1, false); return true; });
  178. AddCommand (Command.LineDown, () => { ChangeSelectionByOffset (0, 1, false); return true; });
  179. AddCommand (Command.PageUp, () => { PageUp (false); return true; });
  180. AddCommand (Command.PageDown, () => { PageDown (false); return true; });
  181. AddCommand (Command.LeftHome, () => { ChangeSelectionToStartOfRow (false); return true; });
  182. AddCommand (Command.RightEnd, () => { ChangeSelectionToEndOfRow (false); return true; });
  183. AddCommand (Command.TopHome, () => { ChangeSelectionToStartOfTable (false); return true; });
  184. AddCommand (Command.BottomEnd, () => { ChangeSelectionToEndOfTable (false); return true; });
  185. AddCommand (Command.RightExtend, () => { ChangeSelectionByOffset (1, 0, true); return true; });
  186. AddCommand (Command.LeftExtend, () => { ChangeSelectionByOffset (-1, 0, true); return true; });
  187. AddCommand (Command.LineUpExtend, () => { ChangeSelectionByOffset (0, -1, true); return true; });
  188. AddCommand (Command.LineDownExtend, () => { ChangeSelectionByOffset (0, 1, true); return true; });
  189. AddCommand (Command.PageUpExtend, () => { PageUp (true); return true; });
  190. AddCommand (Command.PageDownExtend, () => { PageDown (true); return true; });
  191. AddCommand (Command.LeftHomeExtend, () => { ChangeSelectionToStartOfRow (true); return true; });
  192. AddCommand (Command.RightEndExtend, () => { ChangeSelectionToEndOfRow (true); return true; });
  193. AddCommand (Command.TopHomeExtend, () => { ChangeSelectionToStartOfTable (true); return true; });
  194. AddCommand (Command.BottomEndExtend, () => { ChangeSelectionToEndOfTable (true); return true; });
  195. AddCommand (Command.SelectAll, () => { SelectAll (); return true; });
  196. AddCommand (Command.Accept, () => { OnCellActivated (new CellActivatedEventArgs (Table, SelectedColumn, SelectedRow)); return true; });
  197. AddCommand (Command.ToggleChecked, () => { ToggleCurrentCellSelection (); return true; });
  198. // Default keybindings for this view
  199. AddKeyBinding (Key.CursorLeft, Command.Left);
  200. AddKeyBinding (Key.CursorRight, Command.Right);
  201. AddKeyBinding (Key.CursorUp, Command.LineUp);
  202. AddKeyBinding (Key.CursorDown, Command.LineDown);
  203. AddKeyBinding (Key.PageUp, Command.PageUp);
  204. AddKeyBinding (Key.PageDown, Command.PageDown);
  205. AddKeyBinding (Key.Home, Command.LeftHome);
  206. AddKeyBinding (Key.End, Command.RightEnd);
  207. AddKeyBinding (Key.Home | Key.CtrlMask, Command.TopHome);
  208. AddKeyBinding (Key.End | Key.CtrlMask, Command.BottomEnd);
  209. AddKeyBinding (Key.CursorLeft | Key.ShiftMask, Command.LeftExtend);
  210. AddKeyBinding (Key.CursorRight | Key.ShiftMask, Command.RightExtend);
  211. AddKeyBinding (Key.CursorUp | Key.ShiftMask, Command.LineUpExtend);
  212. AddKeyBinding (Key.CursorDown | Key.ShiftMask, Command.LineDownExtend);
  213. AddKeyBinding (Key.PageUp | Key.ShiftMask, Command.PageUpExtend);
  214. AddKeyBinding (Key.PageDown | Key.ShiftMask, Command.PageDownExtend);
  215. AddKeyBinding (Key.Home | Key.ShiftMask, Command.LeftHomeExtend);
  216. AddKeyBinding (Key.End | Key.ShiftMask, Command.RightEndExtend);
  217. AddKeyBinding (Key.Home | Key.CtrlMask | Key.ShiftMask, Command.TopHomeExtend);
  218. AddKeyBinding (Key.End | Key.CtrlMask | Key.ShiftMask, Command.BottomEndExtend);
  219. AddKeyBinding (Key.A | Key.CtrlMask, Command.SelectAll);
  220. AddKeyBinding (CellActivationKey, Command.Accept);
  221. }
  222. ///<inheritdoc/>
  223. public override void Redraw (Rect bounds)
  224. {
  225. Move (0, 0);
  226. var frame = Frame;
  227. scrollRightPoint = null;
  228. scrollLeftPoint = null;
  229. // What columns to render at what X offset in viewport
  230. var columnsToRender = CalculateViewport (bounds).ToArray ();
  231. Driver.SetAttribute (GetNormalColor ());
  232. //invalidate current row (prevents scrolling around leaving old characters in the frame
  233. Driver.AddStr (new string (' ', bounds.Width));
  234. int line = 0;
  235. if (ShouldRenderHeaders ()) {
  236. // Render something like:
  237. /*
  238. ┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐
  239. │ArithmeticComparator│chi │Healthboard│Interpretation│Labnumber│
  240. └────────────────────┴──────────┴───────────┴──────────────┴─────────┘
  241. */
  242. if (Style.ShowHorizontalHeaderOverline) {
  243. RenderHeaderOverline (line, bounds.Width, columnsToRender);
  244. line++;
  245. }
  246. RenderHeaderMidline (line, columnsToRender);
  247. line++;
  248. if (Style.ShowHorizontalHeaderUnderline) {
  249. RenderHeaderUnderline (line, bounds.Width, columnsToRender);
  250. line++;
  251. }
  252. }
  253. int headerLinesConsumed = line;
  254. //render the cells
  255. for (; line < frame.Height; line++) {
  256. ClearLine (line, bounds.Width);
  257. //work out what Row to render
  258. var rowToRender = RowOffset + (line - headerLinesConsumed);
  259. //if we have run off the end of the table
  260. if (TableIsNullOrInvisible () || rowToRender >= Table.Rows.Count || rowToRender < 0)
  261. continue;
  262. RenderRow (line, rowToRender, columnsToRender);
  263. }
  264. }
  265. /// <summary>
  266. /// Clears a line of the console by filling it with spaces
  267. /// </summary>
  268. /// <param name="row"></param>
  269. /// <param name="width"></param>
  270. private void ClearLine (int row, int width)
  271. {
  272. Move (0, row);
  273. Driver.SetAttribute (GetNormalColor ());
  274. Driver.AddStr (new string (' ', width));
  275. }
  276. /// <summary>
  277. /// Returns the amount of vertical space currently occupied by the header or 0 if it is not visible.
  278. /// </summary>
  279. /// <returns></returns>
  280. private int GetHeaderHeightIfAny ()
  281. {
  282. return ShouldRenderHeaders () ? GetHeaderHeight () : 0;
  283. }
  284. /// <summary>
  285. /// Returns the amount of vertical space required to display the header
  286. /// </summary>
  287. /// <returns></returns>
  288. private int GetHeaderHeight ()
  289. {
  290. int heightRequired = 1;
  291. if (Style.ShowHorizontalHeaderOverline)
  292. heightRequired++;
  293. if (Style.ShowHorizontalHeaderUnderline)
  294. heightRequired++;
  295. return heightRequired;
  296. }
  297. private void RenderHeaderOverline (int row, int availableWidth, ColumnToRender [] columnsToRender)
  298. {
  299. // Renders a line above table headers (when visible) like:
  300. // ┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐
  301. for (int c = 0; c < availableWidth; c++) {
  302. var rune = Driver.HLine;
  303. if (Style.ShowVerticalHeaderLines) {
  304. if (c == 0) {
  305. rune = Driver.ULCorner;
  306. }
  307. // if the next column is the start of a header
  308. else if (columnsToRender.Any (r => r.X == c + 1)) {
  309. rune = Driver.TopTee;
  310. } else if (c == availableWidth - 1) {
  311. rune = Driver.URCorner;
  312. }
  313. // if the next console column is the lastcolumns end
  314. else if (Style.ExpandLastColumn == false &&
  315. columnsToRender.Any (r => r.IsVeryLast && r.X + r.Width - 1 == c)) {
  316. rune = Driver.TopTee;
  317. }
  318. }
  319. AddRuneAt (Driver, c, row, rune);
  320. }
  321. }
  322. private void RenderHeaderMidline (int row, ColumnToRender [] columnsToRender)
  323. {
  324. // Renders something like:
  325. // │ArithmeticComparator│chi │Healthboard│Interpretation│Labnumber│
  326. ClearLine (row, Bounds.Width);
  327. //render start of line
  328. if (style.ShowVerticalHeaderLines)
  329. AddRune (0, row, Driver.VLine);
  330. for (int i = 0; i < columnsToRender.Length; i++) {
  331. var current = columnsToRender [i];
  332. var colStyle = Style.GetColumnStyleIfAny (current.Column);
  333. var colName = current.Column.ColumnName;
  334. RenderSeparator (current.X - 1, row, true);
  335. Move (current.X, row);
  336. Driver.AddStr (TruncateOrPad (colName, colName, current.Width, colStyle));
  337. if (Style.ExpandLastColumn == false && current.IsVeryLast) {
  338. RenderSeparator (current.X + current.Width - 1, row, true);
  339. }
  340. }
  341. //render end of line
  342. if (style.ShowVerticalHeaderLines)
  343. AddRune (Bounds.Width - 1, row, Driver.VLine);
  344. }
  345. private void RenderHeaderUnderline (int row, int availableWidth, ColumnToRender [] columnsToRender)
  346. {
  347. /*
  348. * First lets work out if we should be rendering scroll indicators
  349. */
  350. // are there are visible columns to the left that have been pushed
  351. // off the screen due to horizontal scrolling?
  352. bool moreColumnsToLeft = ColumnOffset > 0;
  353. // if we moved left would we find a new column (or are they all invisible?)
  354. if (!TryGetNearestVisibleColumn (ColumnOffset - 1, false, false, out _)) {
  355. moreColumnsToLeft = false;
  356. }
  357. // are there visible columns to the right that have not yet been reached?
  358. // lets find out, what is the column index of the last column we are rendering
  359. int lastColumnIdxRendered = ColumnOffset + columnsToRender.Length - 1;
  360. // are there more valid indexes?
  361. bool moreColumnsToRight = lastColumnIdxRendered < Table.Columns.Count;
  362. // if we went right from the last column would we find a new visible column?
  363. if (!TryGetNearestVisibleColumn (lastColumnIdxRendered + 1, true, false, out _)) {
  364. // no we would not
  365. moreColumnsToRight = false;
  366. }
  367. /*
  368. * Now lets draw the line itself
  369. */
  370. // Renders a line below the table headers (when visible) like:
  371. // ├──────────┼───────────┼───────────────────┼──────────┼────────┼─────────────┤
  372. for (int c = 0; c < availableWidth; c++) {
  373. // Start by assuming we just draw a straight line the
  374. // whole way but update to instead draw a header indicator
  375. // or scroll arrow etc
  376. var rune = Driver.HLine;
  377. if (Style.ShowVerticalHeaderLines) {
  378. if (c == 0) {
  379. // for first character render line
  380. rune = Style.ShowVerticalCellLines ? Driver.LeftTee : Driver.LLCorner;
  381. // unless we have horizontally scrolled along
  382. // in which case render an arrow, to indicate user
  383. // can scroll left
  384. if (Style.ShowHorizontalScrollIndicators && moreColumnsToLeft) {
  385. rune = Driver.LeftArrow;
  386. scrollLeftPoint = new Point (c, row);
  387. }
  388. }
  389. // if the next column is the start of a header
  390. else if (columnsToRender.Any (r => r.X == c + 1)) {
  391. /*TODO: is ┼ symbol in Driver?*/
  392. rune = Style.ShowVerticalCellLines ? '┼' : Driver.BottomTee;
  393. } else if (c == availableWidth - 1) {
  394. // for the last character in the table
  395. rune = Style.ShowVerticalCellLines ? Driver.RightTee : Driver.LRCorner;
  396. // unless there is more of the table we could horizontally
  397. // scroll along to see. In which case render an arrow,
  398. // to indicate user can scroll right
  399. if (Style.ShowHorizontalScrollIndicators && moreColumnsToRight) {
  400. rune = Driver.RightArrow;
  401. scrollRightPoint = new Point (c, row);
  402. }
  403. }
  404. // if the next console column is the lastcolumns end
  405. else if (Style.ExpandLastColumn == false &&
  406. columnsToRender.Any (r => r.IsVeryLast && r.X + r.Width - 1 == c)) {
  407. rune = Style.ShowVerticalCellLines ? '┼' : Driver.BottomTee;
  408. }
  409. }
  410. AddRuneAt (Driver, c, row, rune);
  411. }
  412. }
  413. private void RenderRow (int row, int rowToRender, ColumnToRender [] columnsToRender)
  414. {
  415. var focused = HasFocus;
  416. var rowScheme = (Style.RowColorGetter?.Invoke (
  417. new RowColorGetterArgs (Table, rowToRender))) ?? ColorScheme;
  418. //render start of line
  419. if (style.ShowVerticalCellLines)
  420. AddRune (0, row, Driver.VLine);
  421. //start by clearing the entire line
  422. Move (0, row);
  423. Attribute color;
  424. if (FullRowSelect && IsSelected (0, rowToRender)) {
  425. color = focused ? rowScheme.HotFocus : rowScheme.HotNormal;
  426. } else {
  427. color = Enabled ? rowScheme.Normal : rowScheme.Disabled;
  428. }
  429. Driver.SetAttribute (color);
  430. Driver.AddStr (new string (' ', Bounds.Width));
  431. // Render cells for each visible header for the current row
  432. for (int i = 0; i < columnsToRender.Length; i++) {
  433. var current = columnsToRender [i];
  434. var colStyle = Style.GetColumnStyleIfAny (current.Column);
  435. // move to start of cell (in line with header positions)
  436. Move (current.X, row);
  437. // Set color scheme based on whether the current cell is the selected one
  438. bool isSelectedCell = IsSelected (current.Column.Ordinal, rowToRender);
  439. var val = Table.Rows [rowToRender] [current.Column];
  440. // Render the (possibly truncated) cell value
  441. var representation = GetRepresentation (val, colStyle);
  442. // to get the colour scheme
  443. var colorSchemeGetter = colStyle?.ColorGetter;
  444. ColorScheme scheme;
  445. if (colorSchemeGetter != null) {
  446. // user has a delegate for defining row color per cell, call it
  447. scheme = colorSchemeGetter (
  448. new CellColorGetterArgs (Table, rowToRender, current.Column.Ordinal, val, representation, rowScheme));
  449. // if users custom color getter returned null, use the row scheme
  450. if (scheme == null) {
  451. scheme = rowScheme;
  452. }
  453. } else {
  454. // There is no custom cell coloring delegate so use the scheme for the row
  455. scheme = rowScheme;
  456. }
  457. Attribute cellColor;
  458. if (isSelectedCell) {
  459. cellColor = focused ? scheme.HotFocus : scheme.HotNormal;
  460. } else {
  461. cellColor = Enabled ? scheme.Normal : scheme.Disabled;
  462. }
  463. var render = TruncateOrPad (val, representation, current.Width, colStyle);
  464. // While many cells can be selected (see MultiSelectedRegions) only one cell is the primary (drives navigation etc)
  465. bool isPrimaryCell = current.Column.Ordinal == selectedColumn && rowToRender == selectedRow;
  466. RenderCell (cellColor, render, isPrimaryCell);
  467. // Reset color scheme to normal for drawing separators if we drew text with custom scheme
  468. if (scheme != rowScheme) {
  469. if (isSelectedCell) {
  470. color = focused ? rowScheme.HotFocus : rowScheme.HotNormal;
  471. } else {
  472. color = Enabled ? rowScheme.Normal : rowScheme.Disabled;
  473. }
  474. Driver.SetAttribute (color);
  475. }
  476. // 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
  477. if (!FullRowSelect)
  478. Driver.SetAttribute (Enabled ? rowScheme.Normal : rowScheme.Disabled);
  479. RenderSeparator (current.X - 1, row, false);
  480. if (Style.ExpandLastColumn == false && current.IsVeryLast) {
  481. RenderSeparator (current.X + current.Width - 1, row, false);
  482. }
  483. }
  484. //render end of line
  485. if (style.ShowVerticalCellLines)
  486. AddRune (Bounds.Width - 1, row, Driver.VLine);
  487. }
  488. /// <summary>
  489. /// Override to provide custom multi colouring to cells. Use <see cref="View.Driver"/> to
  490. /// with <see cref="ConsoleDriver.AddStr(ustring)"/>. The driver will already be
  491. /// in the correct place when rendering and you must render the full <paramref name="render"/>
  492. /// or the view will not look right. For simpler provision of color use <see cref="ColumnStyle.ColorGetter"/>
  493. /// For changing the content that is rendered use <see cref="ColumnStyle.RepresentationGetter"/>
  494. /// </summary>
  495. /// <param name="cellColor"></param>
  496. /// <param name="render"></param>
  497. /// <param name="isPrimaryCell"></param>
  498. protected virtual void RenderCell (Attribute cellColor, string render, bool isPrimaryCell)
  499. {
  500. // If the cell is the selected col/row then draw the first rune in inverted colors
  501. // this allows the user to track which cell is the active one during a multi cell
  502. // selection or in full row select mode
  503. if (Style.InvertSelectedCellFirstCharacter && isPrimaryCell) {
  504. if (render.Length > 0) {
  505. // invert the color of the current cell for the first character
  506. Driver.SetAttribute (Driver.MakeAttribute (cellColor.Background, cellColor.Foreground));
  507. Driver.AddRune (render [0]);
  508. if (render.Length > 1) {
  509. Driver.SetAttribute (cellColor);
  510. Driver.AddStr (render.Substring (1));
  511. }
  512. }
  513. } else {
  514. Driver.SetAttribute (cellColor);
  515. Driver.AddStr (render);
  516. }
  517. }
  518. private void RenderSeparator (int col, int row, bool isHeader)
  519. {
  520. if (col < 0)
  521. return;
  522. var renderLines = isHeader ? style.ShowVerticalHeaderLines : style.ShowVerticalCellLines;
  523. Rune symbol = renderLines ? Driver.VLine : SeparatorSymbol;
  524. AddRune (col, row, symbol);
  525. }
  526. void AddRuneAt (ConsoleDriver d, int col, int row, Rune ch)
  527. {
  528. Move (col, row);
  529. d.AddRune (ch);
  530. }
  531. /// <summary>
  532. /// 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)
  533. /// </summary>
  534. /// <param name="originalCellValue">The object in this cell of the <see cref="Table"/></param>
  535. /// <param name="representation">The string representation of <paramref name="originalCellValue"/></param>
  536. /// <param name="availableHorizontalSpace"></param>
  537. /// <param name="colStyle">Optional style indicating custom alignment for the cell</param>
  538. /// <returns></returns>
  539. private string TruncateOrPad (object originalCellValue, string representation, int availableHorizontalSpace, ColumnStyle colStyle)
  540. {
  541. if (string.IsNullOrEmpty (representation))
  542. return representation;
  543. // if value is not wide enough
  544. if (representation.Sum (c => Rune.ColumnWidth (c)) < availableHorizontalSpace) {
  545. // pad it out with spaces to the given alignment
  546. int toPad = availableHorizontalSpace - (representation.Sum (c => Rune.ColumnWidth (c)) + 1 /*leave 1 space for cell boundary*/);
  547. switch (colStyle?.GetAlignment (originalCellValue) ?? TextAlignment.Left) {
  548. case TextAlignment.Left:
  549. return representation + new string (' ', toPad);
  550. case TextAlignment.Right:
  551. return new string (' ', toPad) + representation;
  552. // TODO: With single line cells, centered and justified are the same right?
  553. case TextAlignment.Centered:
  554. case TextAlignment.Justified:
  555. return
  556. new string (' ', (int)Math.Floor (toPad / 2.0)) + // round down
  557. representation +
  558. new string (' ', (int)Math.Ceiling (toPad / 2.0)); // round up
  559. }
  560. }
  561. // value is too wide
  562. return new string (representation.TakeWhile (c => (availableHorizontalSpace -= Rune.ColumnWidth (c)) > 0).ToArray ());
  563. }
  564. /// <inheritdoc/>
  565. public override bool ProcessKey (KeyEvent keyEvent)
  566. {
  567. if (TableIsNullOrInvisible ()) {
  568. PositionCursor ();
  569. return false;
  570. }
  571. var result = InvokeKeybindings (keyEvent);
  572. if (result != null) {
  573. PositionCursor ();
  574. return true;
  575. }
  576. return false;
  577. }
  578. /// <summary>
  579. /// 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"/>)
  580. /// </summary>
  581. /// <param name="col"></param>
  582. /// <param name="row"></param>
  583. /// <param name="extendExistingSelection">True to create a multi cell selection or adjust an existing one</param>
  584. public void SetSelection (int col, int row, bool extendExistingSelection)
  585. {
  586. // if we are trying to increase the column index then
  587. // we are moving right otherwise we are moving left
  588. bool lookRight = col > selectedColumn;
  589. col = GetNearestVisibleColumn (col, lookRight, true);
  590. if (!MultiSelect || !extendExistingSelection) {
  591. ClearMultiSelectedRegions (true);
  592. }
  593. if (extendExistingSelection) {
  594. // If we are extending current selection but there isn't one
  595. if (MultiSelectedRegions.Count == 0 || MultiSelectedRegions.All(m=>m.IsToggled)) {
  596. // Create a new region between the old active cell and the new cell
  597. var rect = CreateTableSelection (SelectedColumn, SelectedRow, col, row);
  598. MultiSelectedRegions.Push (rect);
  599. } else {
  600. // Extend the current head selection to include the new cell
  601. var head = MultiSelectedRegions.Pop ();
  602. var newRect = CreateTableSelection (head.Origin.X, head.Origin.Y, col, row);
  603. MultiSelectedRegions.Push (newRect);
  604. }
  605. }
  606. SelectedColumn = col;
  607. SelectedRow = row;
  608. }
  609. private void ClearMultiSelectedRegions (bool keepToggledSelections)
  610. {
  611. if (!keepToggledSelections) {
  612. MultiSelectedRegions.Clear ();
  613. return;
  614. }
  615. var oldRegions = MultiSelectedRegions.ToArray ().Reverse ();
  616. MultiSelectedRegions.Clear ();
  617. foreach (var region in oldRegions) {
  618. if (region.IsToggled) {
  619. MultiSelectedRegions.Push (region);
  620. }
  621. }
  622. }
  623. /// <summary>
  624. /// Unions the current selected cell (and/or regions) with the provided cell and makes
  625. /// it the active one.
  626. /// </summary>
  627. /// <param name="col"></param>
  628. /// <param name="row"></param>
  629. private void UnionSelection (int col, int row)
  630. {
  631. if (!MultiSelect || TableIsNullOrInvisible ()) {
  632. return;
  633. }
  634. EnsureValidSelection ();
  635. var oldColumn = SelectedColumn;
  636. var oldRow = SelectedRow;
  637. // move us to the new cell
  638. SelectedColumn = col;
  639. SelectedRow = row;
  640. MultiSelectedRegions.Push (
  641. CreateTableSelection (col, row)
  642. );
  643. // if the old cell was not part of a rectangular select
  644. // or otherwise selected we need to retain it in the selection
  645. if (!IsSelected (oldColumn, oldRow)) {
  646. MultiSelectedRegions.Push (
  647. CreateTableSelection (oldColumn, oldRow)
  648. );
  649. }
  650. }
  651. /// <summary>
  652. /// Moves the <see cref="SelectedRow"/> and <see cref="SelectedColumn"/> by the provided offsets. Optionally starting a box selection (see <see cref="MultiSelect"/>)
  653. /// </summary>
  654. /// <param name="offsetX">Offset in number of columns</param>
  655. /// <param name="offsetY">Offset in number of rows</param>
  656. /// <param name="extendExistingSelection">True to create a multi cell selection or adjust an existing one</param>
  657. public void ChangeSelectionByOffset (int offsetX, int offsetY, bool extendExistingSelection)
  658. {
  659. SetSelection (SelectedColumn + offsetX, SelectedRow + offsetY, extendExistingSelection);
  660. Update ();
  661. }
  662. /// <summary>
  663. /// Moves the selection up by one page
  664. /// </summary>
  665. /// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
  666. public void PageUp (bool extend)
  667. {
  668. ChangeSelectionByOffset (0, -(Bounds.Height - GetHeaderHeightIfAny ()), extend);
  669. Update ();
  670. }
  671. /// <summary>
  672. /// Moves the selection down by one page
  673. /// </summary>
  674. /// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
  675. public void PageDown (bool extend)
  676. {
  677. ChangeSelectionByOffset (0, Bounds.Height - GetHeaderHeightIfAny (), extend);
  678. Update ();
  679. }
  680. /// <summary>
  681. /// Moves or extends the selection to the first cell in the table (0,0).
  682. /// If <see cref="FullRowSelect"/> is enabled then selection instead moves
  683. /// to (<see cref="SelectedColumn"/>,0) i.e. no horizontal scrolling.
  684. /// </summary>
  685. /// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
  686. public void ChangeSelectionToStartOfTable (bool extend)
  687. {
  688. SetSelection (FullRowSelect ? SelectedColumn : 0, 0, extend);
  689. Update ();
  690. }
  691. /// <summary>
  692. /// Moves or extends the selection to the final cell in the table (nX,nY).
  693. /// If <see cref="FullRowSelect"/> is enabled then selection instead moves
  694. /// to (<see cref="SelectedColumn"/>,nY) i.e. no horizontal scrolling.
  695. /// </summary>
  696. /// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
  697. public void ChangeSelectionToEndOfTable (bool extend)
  698. {
  699. var finalColumn = Table.Columns.Count - 1;
  700. SetSelection (FullRowSelect ? SelectedColumn : finalColumn, Table.Rows.Count - 1, extend);
  701. Update ();
  702. }
  703. /// <summary>
  704. /// Moves or extends the selection to the last cell in the current row
  705. /// </summary>
  706. /// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
  707. public void ChangeSelectionToEndOfRow (bool extend)
  708. {
  709. SetSelection (Table.Columns.Count - 1, SelectedRow, extend);
  710. Update ();
  711. }
  712. /// <summary>
  713. /// Moves or extends the selection to the first cell in the current row
  714. /// </summary>
  715. /// <param name="extend">true to extend the current selection (if any) instead of replacing</param>
  716. public void ChangeSelectionToStartOfRow (bool extend)
  717. {
  718. SetSelection (0, SelectedRow, extend);
  719. Update ();
  720. }
  721. /// <summary>
  722. /// When <see cref="MultiSelect"/> is on, creates selection over all cells in the table (replacing any old selection regions)
  723. /// </summary>
  724. public void SelectAll ()
  725. {
  726. if (TableIsNullOrInvisible () || !MultiSelect || Table.Rows.Count == 0)
  727. return;
  728. ClearMultiSelectedRegions (true);
  729. // 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
  730. MultiSelectedRegions.Push (new TableSelection (new Point (SelectedColumn, SelectedRow), new Rect (0, 0, Table.Columns.Count, table.Rows.Count)));
  731. Update ();
  732. }
  733. /// <summary>
  734. /// Returns all cells in any <see cref="MultiSelectedRegions"/> (if <see cref="MultiSelect"/> is enabled) and the selected cell
  735. /// </summary>
  736. /// <returns></returns>
  737. public IEnumerable<Point> GetAllSelectedCells ()
  738. {
  739. if (TableIsNullOrInvisible () || Table.Rows.Count == 0)
  740. {
  741. return Enumerable.Empty<Point>();
  742. }
  743. EnsureValidSelection ();
  744. var toReturn = new HashSet<Point>();
  745. // If there are one or more rectangular selections
  746. if (MultiSelect && MultiSelectedRegions.Any ()) {
  747. // 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
  748. var yMin = MultiSelectedRegions.Min (r => r.Rect.Top);
  749. var yMax = MultiSelectedRegions.Max (r => r.Rect.Bottom);
  750. var xMin = FullRowSelect ? 0 : MultiSelectedRegions.Min (r => r.Rect.Left);
  751. var xMax = FullRowSelect ? Table.Columns.Count : MultiSelectedRegions.Max (r => r.Rect.Right);
  752. for (int y = yMin; y < yMax; y++) {
  753. for (int x = xMin; x < xMax; x++) {
  754. if (IsSelected (x, y)) {
  755. toReturn.Add(new Point (x, y));
  756. }
  757. }
  758. }
  759. }
  760. // if there are no region selections then it is just the active cell
  761. // if we are selecting the full row
  762. if (FullRowSelect) {
  763. // all cells in active row are selected
  764. for (int x = 0; x < Table.Columns.Count; x++) {
  765. toReturn.Add(new Point (x, SelectedRow));
  766. }
  767. } else {
  768. // Not full row select and no multi selections
  769. toReturn.Add(new Point (SelectedColumn, SelectedRow));
  770. }
  771. return toReturn;
  772. }
  773. /// <summary>
  774. /// 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
  775. /// </summary>
  776. /// <param name="pt1X">Origin point for the selection in X</param>
  777. /// <param name="pt1Y">Origin point for the selection in Y</param>
  778. /// <param name="pt2X">End point for the selection in X</param>
  779. /// <param name="pt2Y">End point for the selection in Y</param>
  780. /// <param name="toggle">True if selection is result of <see cref="Command.ToggleChecked"/></param>
  781. /// <returns></returns>
  782. private TableSelection CreateTableSelection (int pt1X, int pt1Y, int pt2X, int pt2Y, bool toggle = false)
  783. {
  784. var top = Math.Max (Math.Min (pt1Y, pt2Y), 0);
  785. var bot = Math.Max (Math.Max (pt1Y, pt2Y), 0);
  786. var left = Math.Max (Math.Min (pt1X, pt2X), 0);
  787. var right = Math.Max (Math.Max (pt1X, pt2X), 0);
  788. // Rect class is inclusive of Top Left but exclusive of Bottom Right so extend by 1
  789. return new TableSelection (new Point (pt1X, pt1Y), new Rect (left, top, right - left + 1, bot - top + 1)) {
  790. IsToggled = toggle
  791. };
  792. }
  793. private void ToggleCurrentCellSelection ()
  794. {
  795. if (!MultiSelect) {
  796. return;
  797. }
  798. var regions = GetMultiSelectedRegionsContaining(selectedColumn, selectedRow).ToArray();
  799. var toggles = regions.Where(s=>s.IsToggled).ToArray ();
  800. // Toggle it off
  801. if (toggles.Any ()) {
  802. var oldRegions = MultiSelectedRegions.ToArray ().Reverse ();
  803. MultiSelectedRegions.Clear ();
  804. foreach (var region in oldRegions) {
  805. if (!toggles.Contains (region))
  806. MultiSelectedRegions.Push (region);
  807. }
  808. } else {
  809. // user is toggling selection within a rectangular
  810. // select. So toggle the full region
  811. if(regions.Any())
  812. {
  813. foreach(var r in regions)
  814. {
  815. r.IsToggled = true;
  816. }
  817. }
  818. else{
  819. // Toggle on a single cell selection
  820. MultiSelectedRegions.Push (
  821. CreateTableSelection (selectedColumn, SelectedRow, selectedColumn, selectedRow, true)
  822. );
  823. }
  824. }
  825. }
  826. /// <summary>
  827. /// Returns a single point as a <see cref="TableSelection"/>
  828. /// </summary>
  829. /// <param name="x"></param>
  830. /// <param name="y"></param>
  831. /// <returns></returns>
  832. private TableSelection CreateTableSelection (int x, int y)
  833. {
  834. return CreateTableSelection (x, y, x, y);
  835. }
  836. /// <summary>
  837. /// <para>
  838. /// 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"/>).
  839. /// </para>
  840. /// <remarks>Returns <see langword="false"/> if <see cref="ColumnStyle.Visible"/> is <see langword="false"/>.</remarks>
  841. /// </summary>
  842. /// <param name="col"></param>
  843. /// <param name="row"></param>
  844. /// <returns></returns>
  845. public bool IsSelected (int col, int row)
  846. {
  847. if (!IsColumnVisible (col)) {
  848. return false;
  849. }
  850. if(GetMultiSelectedRegionsContaining(col,row).Any())
  851. {
  852. return true;
  853. }
  854. return row == SelectedRow &&
  855. (col == SelectedColumn || FullRowSelect);
  856. }
  857. private IEnumerable<TableSelection> GetMultiSelectedRegionsContaining(int col, int row)
  858. {
  859. if(!MultiSelect)
  860. {
  861. return Enumerable.Empty<TableSelection>();
  862. }
  863. if(FullRowSelect)
  864. {
  865. return MultiSelectedRegions.Where (r => r.Rect.Bottom > row && r.Rect.Top <= row);
  866. }
  867. else
  868. {
  869. return MultiSelectedRegions.Where (r => r.Rect.Contains (col, row));
  870. }
  871. }
  872. /// <summary>
  873. /// Returns true if the given <paramref name="columnIndex"/> indexes a visible
  874. /// column otherwise false. Returns false for indexes that are out of bounds.
  875. /// </summary>
  876. /// <param name="columnIndex"></param>
  877. /// <returns></returns>
  878. private bool IsColumnVisible (int columnIndex)
  879. {
  880. // if the column index provided is out of bounds
  881. if (columnIndex < 0 || columnIndex >= table.Columns.Count) {
  882. return false;
  883. }
  884. return this.Style.GetColumnStyleIfAny (Table.Columns [columnIndex])?.Visible ?? true;
  885. }
  886. /// <summary>
  887. /// 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
  888. /// </summary>
  889. public override void PositionCursor ()
  890. {
  891. if (TableIsNullOrInvisible ()) {
  892. base.PositionCursor ();
  893. return;
  894. }
  895. var screenPoint = CellToScreen (SelectedColumn, SelectedRow);
  896. if (screenPoint != null)
  897. Move (screenPoint.Value.X, screenPoint.Value.Y);
  898. }
  899. ///<inheritdoc/>
  900. public override bool MouseEvent (MouseEvent me)
  901. {
  902. if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) &&
  903. me.Flags != MouseFlags.WheeledDown && me.Flags != MouseFlags.WheeledUp &&
  904. me.Flags != MouseFlags.WheeledLeft && me.Flags != MouseFlags.WheeledRight)
  905. return false;
  906. if (!HasFocus && CanFocus) {
  907. SetFocus ();
  908. }
  909. if (TableIsNullOrInvisible ()) {
  910. return false;
  911. }
  912. // Scroll wheel flags
  913. switch (me.Flags) {
  914. case MouseFlags.WheeledDown:
  915. RowOffset++;
  916. EnsureValidScrollOffsets ();
  917. SetNeedsDisplay ();
  918. return true;
  919. case MouseFlags.WheeledUp:
  920. RowOffset--;
  921. EnsureValidScrollOffsets ();
  922. SetNeedsDisplay ();
  923. return true;
  924. case MouseFlags.WheeledRight:
  925. ColumnOffset++;
  926. EnsureValidScrollOffsets ();
  927. SetNeedsDisplay ();
  928. return true;
  929. case MouseFlags.WheeledLeft:
  930. ColumnOffset--;
  931. EnsureValidScrollOffsets ();
  932. SetNeedsDisplay ();
  933. return true;
  934. }
  935. if (me.Flags.HasFlag (MouseFlags.Button1Clicked)) {
  936. if (scrollLeftPoint != null
  937. && scrollLeftPoint.Value.X == me.X
  938. && scrollLeftPoint.Value.Y == me.Y) {
  939. ColumnOffset--;
  940. EnsureValidScrollOffsets ();
  941. SetNeedsDisplay ();
  942. }
  943. if (scrollRightPoint != null
  944. && scrollRightPoint.Value.X == me.X
  945. && scrollRightPoint.Value.Y == me.Y) {
  946. ColumnOffset++;
  947. EnsureValidScrollOffsets ();
  948. SetNeedsDisplay ();
  949. }
  950. var hit = ScreenToCell (me.X, me.Y);
  951. if (hit != null) {
  952. if (MultiSelect && HasControlOrAlt (me)) {
  953. UnionSelection (hit.Value.X, hit.Value.Y);
  954. } else {
  955. SetSelection (hit.Value.X, hit.Value.Y, me.Flags.HasFlag (MouseFlags.ButtonShift));
  956. }
  957. Update ();
  958. }
  959. }
  960. // Double clicking a cell activates
  961. if (me.Flags == MouseFlags.Button1DoubleClicked) {
  962. var hit = ScreenToCell (me.X, me.Y);
  963. if (hit != null) {
  964. OnCellActivated (new CellActivatedEventArgs (Table, hit.Value.X, hit.Value.Y));
  965. }
  966. }
  967. return false;
  968. }
  969. private bool HasControlOrAlt (MouseEvent me)
  970. {
  971. return me.Flags.HasFlag (MouseFlags.ButtonAlt) || me.Flags.HasFlag (MouseFlags.ButtonCtrl);
  972. }
  973. /// <summary>.
  974. /// Returns the column and row of <see cref="Table"/> that corresponds to a given point
  975. /// on the screen (relative to the control client area). Returns null if the point is
  976. /// in the header, no table is loaded or outside the control bounds.
  977. /// </summary>
  978. /// <param name="clientX">X offset from the top left of the control.</param>
  979. /// <param name="clientY">Y offset from the top left of the control.</param>
  980. /// <returns>Cell clicked or null.</returns>
  981. public Point? ScreenToCell (int clientX, int clientY)
  982. {
  983. return ScreenToCell (clientX, clientY, out _);
  984. }
  985. /// <inheritdoc cref="ScreenToCell(int, int)"/>
  986. /// <param name="clientX">X offset from the top left of the control.</param>
  987. /// <param name="clientY">Y offset from the top left of the control.</param>
  988. /// <param name="headerIfAny">If the click is in a header this is the column clicked.</param>
  989. public Point? ScreenToCell (int clientX, int clientY, out DataColumn headerIfAny)
  990. {
  991. headerIfAny = null;
  992. if (TableIsNullOrInvisible ())
  993. return null;
  994. var viewPort = CalculateViewport (Bounds);
  995. var headerHeight = GetHeaderHeightIfAny ();
  996. var col = viewPort.LastOrDefault (c => c.X <= clientX);
  997. // Click is on the header section of rendered UI
  998. if (clientY < headerHeight) {
  999. headerIfAny = col?.Column;
  1000. return null;
  1001. }
  1002. var rowIdx = RowOffset - headerHeight + clientY;
  1003. // if click is off bottom of the rows don't give an
  1004. // invalid index back to user!
  1005. if (rowIdx >= Table.Rows.Count) {
  1006. return null;
  1007. }
  1008. if (col != null && rowIdx >= 0) {
  1009. return new Point (col.Column.Ordinal, rowIdx);
  1010. }
  1011. return null;
  1012. }
  1013. /// <summary>
  1014. /// 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
  1015. /// </summary>
  1016. /// <param name="tableColumn">The index of the <see cref="Table"/> column you are looking for, use <see cref="DataColumn.Ordinal"/></param>
  1017. /// <param name="tableRow">The index of the row in <see cref="Table"/> that you are looking for</param>
  1018. /// <returns></returns>
  1019. public Point? CellToScreen (int tableColumn, int tableRow)
  1020. {
  1021. if (TableIsNullOrInvisible ())
  1022. return null;
  1023. var viewPort = CalculateViewport (Bounds);
  1024. var headerHeight = GetHeaderHeightIfAny ();
  1025. var colHit = viewPort.FirstOrDefault (c => c.Column.Ordinal == tableColumn);
  1026. // current column is outside the scroll area
  1027. if (colHit == null)
  1028. return null;
  1029. // the cell is too far up above the current scroll area
  1030. if (RowOffset > tableRow)
  1031. return null;
  1032. // the cell is way down below the scroll area and off the screen
  1033. if (tableRow > RowOffset + (Bounds.Height - headerHeight))
  1034. return null;
  1035. return new Point (colHit.X, tableRow + headerHeight - RowOffset);
  1036. }
  1037. /// <summary>
  1038. /// Updates the view to reflect changes to <see cref="Table"/> and to (<see cref="ColumnOffset"/> / <see cref="RowOffset"/>) etc
  1039. /// </summary>
  1040. /// <remarks>This always calls <see cref="View.SetNeedsDisplay()"/></remarks>
  1041. public void Update ()
  1042. {
  1043. if (TableIsNullOrInvisible ()) {
  1044. SetNeedsDisplay ();
  1045. return;
  1046. }
  1047. EnsureValidScrollOffsets ();
  1048. EnsureValidSelection ();
  1049. EnsureSelectedCellIsVisible ();
  1050. SetNeedsDisplay ();
  1051. }
  1052. /// <summary>
  1053. /// 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.
  1054. /// </summary>
  1055. /// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
  1056. public void EnsureValidScrollOffsets ()
  1057. {
  1058. if (TableIsNullOrInvisible ()) {
  1059. return;
  1060. }
  1061. ColumnOffset = Math.Max (Math.Min (ColumnOffset, Table.Columns.Count - 1), 0);
  1062. RowOffset = Math.Max (Math.Min (RowOffset, Table.Rows.Count - 1), 0);
  1063. }
  1064. /// <summary>
  1065. /// 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.
  1066. /// </summary>
  1067. /// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
  1068. public void EnsureValidSelection ()
  1069. {
  1070. if (TableIsNullOrInvisible ()) {
  1071. // Table doesn't exist, we should probably clear those selections
  1072. ClearMultiSelectedRegions (false);
  1073. return;
  1074. }
  1075. SelectedColumn = Math.Max (Math.Min (SelectedColumn, Table.Columns.Count - 1), 0);
  1076. SelectedRow = Math.Max (Math.Min (SelectedRow, Table.Rows.Count - 1), 0);
  1077. // If SelectedColumn is invisible move it to a visible one
  1078. SelectedColumn = GetNearestVisibleColumn (SelectedColumn, lookRight: true, true);
  1079. var oldRegions = MultiSelectedRegions.ToArray ().Reverse ();
  1080. MultiSelectedRegions.Clear ();
  1081. // evaluate
  1082. foreach (var region in oldRegions) {
  1083. // ignore regions entirely below current table state
  1084. if (region.Rect.Top >= Table.Rows.Count)
  1085. continue;
  1086. // ignore regions entirely too far right of table columns
  1087. if (region.Rect.Left >= Table.Columns.Count)
  1088. continue;
  1089. // ensure region's origin exists
  1090. region.Origin = new Point (
  1091. Math.Max (Math.Min (region.Origin.X, Table.Columns.Count - 1), 0),
  1092. Math.Max (Math.Min (region.Origin.Y, Table.Rows.Count - 1), 0));
  1093. // ensure regions do not go over edge of table bounds
  1094. region.Rect = Rect.FromLTRB (region.Rect.Left,
  1095. region.Rect.Top,
  1096. Math.Max (Math.Min (region.Rect.Right, Table.Columns.Count), 0),
  1097. Math.Max (Math.Min (region.Rect.Bottom, Table.Rows.Count), 0)
  1098. );
  1099. MultiSelectedRegions.Push (region);
  1100. }
  1101. }
  1102. /// <summary>
  1103. /// Returns true if the <see cref="Table"/> is not set or all the
  1104. /// <see cref="DataColumn"/> in the <see cref="Table"/> have an explicit
  1105. /// <see cref="ColumnStyle"/> that marks them <see cref="ColumnStyle.visible"/>
  1106. /// <see langword="false"/>.
  1107. /// </summary>
  1108. /// <returns></returns>
  1109. private bool TableIsNullOrInvisible ()
  1110. {
  1111. return Table == null ||
  1112. Table.Columns.Count <= 0 ||
  1113. Table.Columns.Cast<DataColumn> ().All (
  1114. c => (Style.GetColumnStyleIfAny (c)?.Visible ?? true) == false);
  1115. }
  1116. /// <summary>
  1117. /// Returns <paramref name="columnIndex"/> unless the <see cref="ColumnStyle.Visible"/> is false for
  1118. /// the indexed <see cref="DataColumn"/>. If so then the index returned is nudged to the nearest visible
  1119. /// column.
  1120. /// </summary>
  1121. /// <remarks>Returns <paramref name="columnIndex"/> unchanged if it is invalid (e.g. out of bounds).</remarks>
  1122. /// <param name="columnIndex">The input column index.</param>
  1123. /// <param name="lookRight">When nudging invisible selections look right first.
  1124. /// <see langword="true"/> to look right, <see langword="false"/> to look left.</param>
  1125. /// <param name="allowBumpingInOppositeDirection">If we cannot find anything visible when
  1126. /// looking in direction of <paramref name="lookRight"/> then should we look in the opposite
  1127. /// direction instead? Use true if you want to push a selection to a valid index no matter what.
  1128. /// Use false if you are primarily interested in learning about directional column visibility.</param>
  1129. private int GetNearestVisibleColumn (int columnIndex, bool lookRight, bool allowBumpingInOppositeDirection)
  1130. {
  1131. if (TryGetNearestVisibleColumn (columnIndex, lookRight, allowBumpingInOppositeDirection, out var answer)) {
  1132. return answer;
  1133. }
  1134. return columnIndex;
  1135. }
  1136. private bool TryGetNearestVisibleColumn (int columnIndex, bool lookRight, bool allowBumpingInOppositeDirection, out int idx)
  1137. {
  1138. // if the column index provided is out of bounds
  1139. if (columnIndex < 0 || columnIndex >= table.Columns.Count) {
  1140. idx = columnIndex;
  1141. return false;
  1142. }
  1143. // get the column visibility by index (if no style visible is true)
  1144. bool [] columnVisibility = Table.Columns.Cast<DataColumn> ()
  1145. .Select (c => this.Style.GetColumnStyleIfAny (c)?.Visible ?? true)
  1146. .ToArray ();
  1147. // column is visible
  1148. if (columnVisibility [columnIndex]) {
  1149. idx = columnIndex;
  1150. return true;
  1151. }
  1152. int increment = lookRight ? 1 : -1;
  1153. // move in that direction
  1154. for (int i = columnIndex; i >= 0 && i < columnVisibility.Length; i += increment) {
  1155. // if we find a visible column
  1156. if (columnVisibility [i]) {
  1157. idx = i;
  1158. return true;
  1159. }
  1160. }
  1161. // Caller only wants to look in one direction and we did not find any
  1162. // visible columns in that direction
  1163. if (!allowBumpingInOppositeDirection) {
  1164. idx = columnIndex;
  1165. return false;
  1166. }
  1167. // Caller will let us look in the other direction so
  1168. // now look other way
  1169. increment = -increment;
  1170. for (int i = columnIndex; i >= 0 && i < columnVisibility.Length; i += increment) {
  1171. // if we find a visible column
  1172. if (columnVisibility [i]) {
  1173. idx = i;
  1174. return true;
  1175. }
  1176. }
  1177. // nothing seems to be visible so just return input index
  1178. idx = columnIndex;
  1179. return false;
  1180. }
  1181. /// <summary>
  1182. /// Updates scroll offsets to ensure that the selected cell is visible. Has no effect if <see cref="Table"/> has not been set.
  1183. /// </summary>
  1184. /// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
  1185. public void EnsureSelectedCellIsVisible ()
  1186. {
  1187. if (Table == null || Table.Columns.Count <= 0) {
  1188. return;
  1189. }
  1190. var columnsToRender = CalculateViewport (Bounds).ToArray ();
  1191. var headerHeight = GetHeaderHeightIfAny ();
  1192. //if we have scrolled too far to the left
  1193. if (SelectedColumn < columnsToRender.Min (r => r.Column.Ordinal)) {
  1194. ColumnOffset = SelectedColumn;
  1195. }
  1196. //if we have scrolled too far to the right
  1197. if (SelectedColumn > columnsToRender.Max (r => r.Column.Ordinal)) {
  1198. if (Style.SmoothHorizontalScrolling) {
  1199. // Scroll right 1 column at a time until the users selected column is visible
  1200. while (SelectedColumn > columnsToRender.Max (r => r.Column.Ordinal)) {
  1201. ColumnOffset++;
  1202. columnsToRender = CalculateViewport (Bounds).ToArray ();
  1203. // if we are already scrolled to the last column then break
  1204. // this will prevent any theoretical infinite loop
  1205. if (ColumnOffset >= Table.Columns.Count - 1)
  1206. break;
  1207. }
  1208. } else {
  1209. ColumnOffset = SelectedColumn;
  1210. }
  1211. }
  1212. //if we have scrolled too far down
  1213. if (SelectedRow >= RowOffset + (Bounds.Height - headerHeight)) {
  1214. RowOffset = SelectedRow - (Bounds.Height - headerHeight) + 1;
  1215. }
  1216. //if we have scrolled too far up
  1217. if (SelectedRow < RowOffset) {
  1218. RowOffset = SelectedRow;
  1219. }
  1220. }
  1221. /// <summary>
  1222. /// Invokes the <see cref="SelectedCellChanged"/> event
  1223. /// </summary>
  1224. protected virtual void OnSelectedCellChanged (SelectedCellChangedEventArgs args)
  1225. {
  1226. SelectedCellChanged?.Invoke (args);
  1227. }
  1228. /// <summary>
  1229. /// Invokes the <see cref="CellActivated"/> event
  1230. /// </summary>
  1231. /// <param name="args"></param>
  1232. protected virtual void OnCellActivated (CellActivatedEventArgs args)
  1233. {
  1234. CellActivated?.Invoke (args);
  1235. }
  1236. /// <summary>
  1237. /// Calculates which columns should be rendered given the <paramref name="bounds"/> in which to display and the <see cref="ColumnOffset"/>
  1238. /// </summary>
  1239. /// <param name="bounds"></param>
  1240. /// <param name="padding"></param>
  1241. /// <returns></returns>
  1242. private IEnumerable<ColumnToRender> CalculateViewport (Rect bounds, int padding = 1)
  1243. {
  1244. if (TableIsNullOrInvisible ())
  1245. yield break;
  1246. int usedSpace = 0;
  1247. //if horizontal space is required at the start of the line (before the first header)
  1248. if (Style.ShowVerticalHeaderLines || Style.ShowVerticalCellLines)
  1249. usedSpace += 1;
  1250. int availableHorizontalSpace = bounds.Width;
  1251. int rowsToRender = bounds.Height;
  1252. // reserved for the headers row
  1253. if (ShouldRenderHeaders ())
  1254. rowsToRender -= GetHeaderHeight ();
  1255. bool first = true;
  1256. var lastColumn = Table.Columns.Cast<DataColumn> ().Last ();
  1257. foreach (var col in Table.Columns.Cast<DataColumn> ().Skip (ColumnOffset)) {
  1258. int startingIdxForCurrentHeader = usedSpace;
  1259. var colStyle = Style.GetColumnStyleIfAny (col);
  1260. int colWidth;
  1261. // if column is not being rendered
  1262. if (colStyle?.Visible == false) {
  1263. // do not add it to the returned columns
  1264. continue;
  1265. }
  1266. // is there enough space for this column (and it's data)?
  1267. colWidth = CalculateMaxCellWidth (col, rowsToRender, colStyle) + padding;
  1268. // there is not enough space for this columns
  1269. // visible content
  1270. if (usedSpace + colWidth > availableHorizontalSpace) {
  1271. bool showColumn = false;
  1272. // if this column accepts flexible width rendering and
  1273. // is therefore happy rendering into less space
  1274. if (colStyle != null && colStyle.MinAcceptableWidth > 0 &&
  1275. // is there enough space to meet the MinAcceptableWidth
  1276. (availableHorizontalSpace - usedSpace) >= colStyle.MinAcceptableWidth) {
  1277. // show column and use use whatever space is
  1278. // left for rendering it
  1279. showColumn = true;
  1280. colWidth = availableHorizontalSpace - usedSpace;
  1281. }
  1282. // If its the only column we are able to render then
  1283. // accept it anyway (that must be one massively wide column!)
  1284. if (first) {
  1285. showColumn = true;
  1286. }
  1287. // no special exceptions and we are out of space
  1288. // so stop accepting new columns for the render area
  1289. if (!showColumn)
  1290. break;
  1291. }
  1292. usedSpace += colWidth;
  1293. // there is space
  1294. yield return new ColumnToRender (col, startingIdxForCurrentHeader,
  1295. // required for if we end up here because first == true i.e. we have a single massive width (overspilling bounds) column to present
  1296. Math.Min (availableHorizontalSpace, colWidth),
  1297. lastColumn == col);
  1298. first = false;
  1299. }
  1300. }
  1301. private bool ShouldRenderHeaders ()
  1302. {
  1303. if (TableIsNullOrInvisible ())
  1304. return false;
  1305. return Style.AlwaysShowHeaders || rowOffset == 0;
  1306. }
  1307. /// <summary>
  1308. /// 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"/>
  1309. /// </summary>
  1310. /// <param name="col"></param>
  1311. /// <param name="rowsToRender"></param>
  1312. /// <param name="colStyle"></param>
  1313. /// <returns></returns>
  1314. private int CalculateMaxCellWidth (DataColumn col, int rowsToRender, ColumnStyle colStyle)
  1315. {
  1316. int spaceRequired = col.ColumnName.Sum (c => Rune.ColumnWidth (c));
  1317. // if table has no rows
  1318. if (RowOffset < 0)
  1319. return spaceRequired;
  1320. for (int i = RowOffset; i < RowOffset + rowsToRender && i < Table.Rows.Count; i++) {
  1321. //expand required space if cell is bigger than the last biggest cell or header
  1322. spaceRequired = Math.Max (spaceRequired, GetRepresentation (Table.Rows [i] [col], colStyle).Sum (c => Rune.ColumnWidth (c)));
  1323. }
  1324. // Don't require more space than the style allows
  1325. if (colStyle != null) {
  1326. // enforce maximum cell width based on style
  1327. if (spaceRequired > colStyle.MaxWidth) {
  1328. spaceRequired = colStyle.MaxWidth;
  1329. }
  1330. // enforce minimum cell width based on style
  1331. if (spaceRequired < colStyle.MinWidth) {
  1332. spaceRequired = colStyle.MinWidth;
  1333. }
  1334. }
  1335. // enforce maximum cell width based on global table style
  1336. if (spaceRequired > MaxCellWidth)
  1337. spaceRequired = MaxCellWidth;
  1338. return spaceRequired;
  1339. }
  1340. /// <summary>
  1341. /// Returns the value that should be rendered to best represent a strongly typed <paramref name="value"/> read from <see cref="Table"/>
  1342. /// </summary>
  1343. /// <param name="value"></param>
  1344. /// <param name="colStyle">Optional style defining how to represent cell values</param>
  1345. /// <returns></returns>
  1346. private string GetRepresentation (object value, ColumnStyle colStyle)
  1347. {
  1348. if (value == null || value == DBNull.Value) {
  1349. return NullSymbol;
  1350. }
  1351. return colStyle != null ? colStyle.GetRepresentation (value) : value.ToString ();
  1352. }
  1353. /// <summary>
  1354. /// Delegate for providing color to <see cref="TableView"/> cells based on the value being rendered
  1355. /// </summary>
  1356. /// <param name="args">Contains information about the cell for which color is needed</param>
  1357. /// <returns></returns>
  1358. public delegate ColorScheme CellColorGetterDelegate (CellColorGetterArgs args);
  1359. /// <summary>
  1360. /// Delegate for providing color for a whole row of a <see cref="TableView"/>
  1361. /// </summary>
  1362. /// <param name="args"></param>
  1363. /// <returns></returns>
  1364. public delegate ColorScheme RowColorGetterDelegate (RowColorGetterArgs args);
  1365. #region Nested Types
  1366. /// <summary>
  1367. /// Describes how to render a given column in a <see cref="TableView"/> including <see cref="Alignment"/>
  1368. /// and textual representation of cells (e.g. date formats)
  1369. ///
  1370. /// <a href="https://gui-cs.github.io/Terminal.Gui/articles/tableview.html">See TableView Deep Dive for more information</a>.
  1371. /// </summary>
  1372. public class ColumnStyle {
  1373. /// <summary>
  1374. /// Defines the default alignment for all values rendered in this column. For custom alignment based on cell contents use <see cref="AlignmentGetter"/>.
  1375. /// </summary>
  1376. public TextAlignment Alignment { get; set; }
  1377. /// <summary>
  1378. /// Defines a delegate for returning custom alignment per cell based on cell values. When specified this will override <see cref="Alignment"/>
  1379. /// </summary>
  1380. public Func<object, TextAlignment> AlignmentGetter;
  1381. /// <summary>
  1382. /// 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"/>
  1383. /// </summary>
  1384. public Func<object, string> RepresentationGetter;
  1385. /// <summary>
  1386. /// Defines a delegate for returning a custom color scheme per cell based on cell values.
  1387. /// Return null for the default
  1388. /// </summary>
  1389. public CellColorGetterDelegate ColorGetter;
  1390. private bool visible = true;
  1391. /// <summary>
  1392. /// Defines the format for values e.g. "yyyy-MM-dd" for dates
  1393. /// </summary>
  1394. public string Format { get; set; }
  1395. /// <summary>
  1396. /// 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"/>
  1397. /// </summary>
  1398. public int MaxWidth { get; set; } = TableView.DefaultMaxCellWidth;
  1399. /// <summary>
  1400. /// Set the minimum width of the column in characters. Setting this will ensure that
  1401. /// even when a column has short content/header it still fills a given width of the control.
  1402. ///
  1403. /// <para>This value will be ignored if more than the tables <see cref="TableView.MaxCellWidth"/>
  1404. /// or the <see cref="MaxWidth"/>
  1405. /// </para>
  1406. /// <remarks>
  1407. /// For setting a flexible column width (down to a lower limit) use <see cref="MinAcceptableWidth"/>
  1408. /// instead
  1409. /// </remarks>
  1410. /// </summary>
  1411. public int MinWidth { get; set; }
  1412. /// <summary>
  1413. /// Enables flexible sizing of this column based on available screen space to render into.
  1414. /// </summary>
  1415. public int MinAcceptableWidth { get; set; } = DefaultMinAcceptableWidth;
  1416. /// <summary>
  1417. /// Gets or Sets a value indicating whether the column should be visible to the user.
  1418. /// This affects both whether it is rendered and whether it can be selected. Defaults to
  1419. /// true.
  1420. /// </summary>
  1421. /// <remarks>If <see cref="MaxWidth"/> is 0 then <see cref="Visible"/> will always return false.</remarks>
  1422. public bool Visible { get => MaxWidth >= 0 && visible; set => visible = value; }
  1423. /// <summary>
  1424. /// Returns the alignment for the cell based on <paramref name="cellValue"/> and <see cref="AlignmentGetter"/>/<see cref="Alignment"/>
  1425. /// </summary>
  1426. /// <param name="cellValue"></param>
  1427. /// <returns></returns>
  1428. public TextAlignment GetAlignment (object cellValue)
  1429. {
  1430. if (AlignmentGetter != null)
  1431. return AlignmentGetter (cellValue);
  1432. return Alignment;
  1433. }
  1434. /// <summary>
  1435. /// 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"/>
  1436. /// </summary>
  1437. /// <param name="value"></param>
  1438. /// <returns></returns>
  1439. public string GetRepresentation (object value)
  1440. {
  1441. if (!string.IsNullOrWhiteSpace (Format)) {
  1442. if (value is IFormattable f)
  1443. return f.ToString (Format, null);
  1444. }
  1445. if (RepresentationGetter != null)
  1446. return RepresentationGetter (value);
  1447. return value?.ToString ();
  1448. }
  1449. }
  1450. /// <summary>
  1451. /// Defines rendering options that affect how the table is displayed.
  1452. ///
  1453. /// <a href="https://gui-cs.github.io/Terminal.Gui/articles/tableview.html">See TableView Deep Dive for more information</a>.
  1454. /// </summary>
  1455. public class TableStyle {
  1456. /// <summary>
  1457. /// When scrolling down always lock the column headers in place as the first row of the table
  1458. /// </summary>
  1459. public bool AlwaysShowHeaders { get; set; } = false;
  1460. /// <summary>
  1461. /// True to render a solid line above the headers
  1462. /// </summary>
  1463. public bool ShowHorizontalHeaderOverline { get; set; } = true;
  1464. /// <summary>
  1465. /// True to render a solid line under the headers
  1466. /// </summary>
  1467. public bool ShowHorizontalHeaderUnderline { get; set; } = true;
  1468. /// <summary>
  1469. /// True to render a solid line vertical line between cells
  1470. /// </summary>
  1471. public bool ShowVerticalCellLines { get; set; } = true;
  1472. /// <summary>
  1473. /// True to render a solid line vertical line between headers
  1474. /// </summary>
  1475. public bool ShowVerticalHeaderLines { get; set; } = true;
  1476. /// <summary>
  1477. /// True to render a arrows on the right/left of the table when
  1478. /// there are more column(s) that can be scrolled to. Requires
  1479. /// <see cref="ShowHorizontalHeaderUnderline"/> to be true.
  1480. /// Defaults to true
  1481. /// </summary>
  1482. public bool ShowHorizontalScrollIndicators { get; set; } = true;
  1483. /// <summary>
  1484. /// True to invert the colors of the first symbol of the selected cell in the <see cref="TableView"/>.
  1485. /// This gives the appearance of a cursor for when the <see cref="ConsoleDriver"/> doesn't otherwise show
  1486. /// this
  1487. /// </summary>
  1488. public bool InvertSelectedCellFirstCharacter { get; set; } = false;
  1489. /// <summary>
  1490. /// Collection of columns for which you want special rendering (e.g. custom column lengths, text alignment etc)
  1491. /// </summary>
  1492. public Dictionary<DataColumn, ColumnStyle> ColumnStyles { get; set; } = new Dictionary<DataColumn, ColumnStyle> ();
  1493. /// <summary>
  1494. /// Delegate for coloring specific rows in a different color. For cell color <see cref="ColumnStyle.ColorGetter"/>
  1495. /// </summary>
  1496. /// <value></value>
  1497. public RowColorGetterDelegate RowColorGetter { get; set; }
  1498. /// <summary>
  1499. /// Determines rendering when the last column in the table is visible but it's
  1500. /// content or <see cref="ColumnStyle.MaxWidth"/> is less than the remaining
  1501. /// space in the control. True (the default) will expand the column to fill
  1502. /// the remaining bounds of the control. False will draw a column ending line
  1503. /// and leave a blank column that cannot be selected in the remaining space.
  1504. /// </summary>
  1505. /// <value></value>
  1506. public bool ExpandLastColumn { get; set; } = true;
  1507. /// <summary>
  1508. /// <para>
  1509. /// Determines how <see cref="TableView.ColumnOffset"/> is updated when scrolling
  1510. /// right off the end of the currently visible area.
  1511. /// </para>
  1512. /// <para>
  1513. /// If true then when scrolling right the scroll offset is increased the minimum required to show
  1514. /// the new column. This may be slow if you have an incredibly large number of columns in
  1515. /// your table and/or slow <see cref="ColumnStyle.RepresentationGetter"/> implementations
  1516. /// </para>
  1517. /// <para>
  1518. /// If false then scroll offset is set to the currently selected column (i.e. PageRight).
  1519. /// </para>
  1520. /// </summary>
  1521. public bool SmoothHorizontalScrolling { get; set; } = true;
  1522. /// <summary>
  1523. /// Returns the entry from <see cref="ColumnStyles"/> for the given <paramref name="col"/> or null if no custom styling is defined for it
  1524. /// </summary>
  1525. /// <param name="col"></param>
  1526. /// <returns></returns>
  1527. public ColumnStyle GetColumnStyleIfAny (DataColumn col)
  1528. {
  1529. return ColumnStyles.TryGetValue (col, out ColumnStyle result) ? result : null;
  1530. }
  1531. /// <summary>
  1532. /// Returns an existing <see cref="ColumnStyle"/> for the given <paramref name="col"/> or creates a new one with default options
  1533. /// </summary>
  1534. /// <param name="col"></param>
  1535. /// <returns></returns>
  1536. public ColumnStyle GetOrCreateColumnStyle (DataColumn col)
  1537. {
  1538. if (!ColumnStyles.ContainsKey (col))
  1539. ColumnStyles.Add (col, new ColumnStyle ());
  1540. return ColumnStyles [col];
  1541. }
  1542. }
  1543. /// <summary>
  1544. /// Describes a desire to render a column at a given horizontal position in the UI
  1545. /// </summary>
  1546. internal class ColumnToRender {
  1547. /// <summary>
  1548. /// The column to render
  1549. /// </summary>
  1550. public DataColumn Column { get; set; }
  1551. /// <summary>
  1552. /// The horizontal position to begin rendering the column at
  1553. /// </summary>
  1554. public int X { get; set; }
  1555. /// <summary>
  1556. /// The width that the column should occupy as calculated by <see cref="CalculateViewport(Rect, int)"/>. Note that this includes
  1557. /// space for padding i.e. the separator between columns.
  1558. /// </summary>
  1559. public int Width { get; }
  1560. /// <summary>
  1561. /// True if this column is the very last column in the <see cref="Table"/> (not just the last visible column)
  1562. /// </summary>
  1563. public bool IsVeryLast { get; }
  1564. public ColumnToRender (DataColumn col, int x, int width, bool isVeryLast)
  1565. {
  1566. Column = col;
  1567. X = x;
  1568. Width = width;
  1569. IsVeryLast = isVeryLast;
  1570. }
  1571. }
  1572. /// <summary>
  1573. /// Arguments for a <see cref="CellColorGetterDelegate"/>. Describes a cell for which a rendering
  1574. /// <see cref="ColorScheme"/> is being sought
  1575. /// </summary>
  1576. public class CellColorGetterArgs {
  1577. /// <summary>
  1578. /// The data table hosted by the <see cref="TableView"/> control.
  1579. /// </summary>
  1580. public DataTable Table { get; }
  1581. /// <summary>
  1582. /// The index of the row in <see cref="Table"/> for which color is needed
  1583. /// </summary>
  1584. public int RowIndex { get; }
  1585. /// <summary>
  1586. /// The index of column in <see cref="Table"/> for which color is needed
  1587. /// </summary>
  1588. public int ColIdex { get; }
  1589. /// <summary>
  1590. /// The hard typed value being rendered in the cell for which color is needed
  1591. /// </summary>
  1592. public object CellValue { get; }
  1593. /// <summary>
  1594. /// The textual representation of <see cref="CellValue"/> (what will actually be drawn to the screen)
  1595. /// </summary>
  1596. public string Representation { get; }
  1597. /// <summary>
  1598. /// the color scheme that is going to be used to render the cell if no cell specific color scheme is returned
  1599. /// </summary>
  1600. public ColorScheme RowScheme { get; }
  1601. internal CellColorGetterArgs (DataTable table, int rowIdx, int colIdx, object cellValue, string representation, ColorScheme rowScheme)
  1602. {
  1603. Table = table;
  1604. RowIndex = rowIdx;
  1605. ColIdex = colIdx;
  1606. CellValue = cellValue;
  1607. Representation = representation;
  1608. RowScheme = rowScheme;
  1609. }
  1610. }
  1611. /// <summary>
  1612. /// Arguments for <see cref="RowColorGetterDelegate"/>. Describes a row of data in a <see cref="DataTable"/>
  1613. /// for which <see cref="ColorScheme"/> is sought.
  1614. /// </summary>
  1615. public class RowColorGetterArgs {
  1616. /// <summary>
  1617. /// The data table hosted by the <see cref="TableView"/> control.
  1618. /// </summary>
  1619. public DataTable Table { get; }
  1620. /// <summary>
  1621. /// The index of the row in <see cref="Table"/> for which color is needed
  1622. /// </summary>
  1623. public int RowIndex { get; }
  1624. internal RowColorGetterArgs (DataTable table, int rowIdx)
  1625. {
  1626. Table = table;
  1627. RowIndex = rowIdx;
  1628. }
  1629. }
  1630. /// <summary>
  1631. /// Defines the event arguments for <see cref="TableView.SelectedCellChanged"/>
  1632. /// </summary>
  1633. public class SelectedCellChangedEventArgs : EventArgs {
  1634. /// <summary>
  1635. /// 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
  1636. /// </summary>
  1637. /// <value></value>
  1638. public DataTable Table { get; }
  1639. /// <summary>
  1640. /// 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
  1641. /// </summary>
  1642. /// <value></value>
  1643. public int OldCol { get; }
  1644. /// <summary>
  1645. /// The newly selected column index.
  1646. /// </summary>
  1647. /// <value></value>
  1648. public int NewCol { get; }
  1649. /// <summary>
  1650. /// 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
  1651. /// </summary>
  1652. /// <value></value>
  1653. public int OldRow { get; }
  1654. /// <summary>
  1655. /// The newly selected row index.
  1656. /// </summary>
  1657. /// <value></value>
  1658. public int NewRow { get; }
  1659. /// <summary>
  1660. /// Creates a new instance of arguments describing a change in selected cell in a <see cref="TableView"/>
  1661. /// </summary>
  1662. /// <param name="t"></param>
  1663. /// <param name="oldCol"></param>
  1664. /// <param name="newCol"></param>
  1665. /// <param name="oldRow"></param>
  1666. /// <param name="newRow"></param>
  1667. public SelectedCellChangedEventArgs (DataTable t, int oldCol, int newCol, int oldRow, int newRow)
  1668. {
  1669. Table = t;
  1670. OldCol = oldCol;
  1671. NewCol = newCol;
  1672. OldRow = oldRow;
  1673. NewRow = newRow;
  1674. }
  1675. }
  1676. /// <summary>
  1677. /// Describes a selected region of the table
  1678. /// </summary>
  1679. public class TableSelection {
  1680. /// <summary>
  1681. /// Corner of the <see cref="Rect"/> where selection began
  1682. /// </summary>
  1683. /// <value></value>
  1684. public Point Origin { get; set; }
  1685. /// <summary>
  1686. /// Area selected
  1687. /// </summary>
  1688. /// <value></value>
  1689. public Rect Rect { get; set; }
  1690. /// <summary>
  1691. /// True if the selection was made through <see cref="Command.ToggleChecked"/>
  1692. /// and therefore should persist even through keyboard navigation.
  1693. /// </summary>
  1694. public bool IsToggled { get; set; }
  1695. /// <summary>
  1696. /// Creates a new selected area starting at the origin corner and covering the provided rectangular area
  1697. /// </summary>
  1698. /// <param name="origin"></param>
  1699. /// <param name="rect"></param>
  1700. public TableSelection (Point origin, Rect rect)
  1701. {
  1702. Origin = origin;
  1703. Rect = rect;
  1704. }
  1705. }
  1706. #endregion
  1707. }
  1708. }