123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319 |
- using NStack;
- using System;
- using System.Collections.Generic;
- using System.Data;
- using System.Linq;
- namespace Terminal.Gui.Views {
- /// <summary>
- /// View for tabular data based on a <see cref="DataTable"/>
- /// </summary>
- public class TableView : View {
- private int columnOffset;
- private int rowOffset;
- private int selectedRow;
- private int selectedColumn;
- private DataTable table;
- public DataTable Table { get => table; set {table = value; Update(); } }
- /// <summary>
- /// Zero indexed offset for the upper left <see cref="DataColumn"/> to display in <see cref="Table"/>.
- /// </summary>
- /// <remarks>This property allows very wide tables to be rendered with horizontal scrolling</remarks>
- public int ColumnOffset {
- get => columnOffset;
- //try to prevent this being set to an out of bounds column
- set => columnOffset = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value));
- }
- /// <summary>
- /// Zero indexed offset for the <see cref="DataRow"/> to display in <see cref="Table"/> on line 2 of the control (first line being headers)
- /// </summary>
- /// <remarks>This property allows very wide tables to be rendered with horizontal scrolling</remarks>
- public int RowOffset {
- get => rowOffset;
- set => rowOffset = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value));
- }
- /// <summary>
- /// The index of <see cref="DataTable.Columns"/> in <see cref="Table"/> that the user has currently selected
- /// </summary>
- public int SelectedColumn {
- get => selectedColumn;
- //try to prevent this being set to an out of bounds column
- set => selectedColumn = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value));
- }
- /// <summary>
- /// The index of <see cref="DataTable.Rows"/> in <see cref="Table"/> that the user has currently selected
- /// </summary>
- public int SelectedRow {
- get => selectedRow;
- set => selectedRow = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value));
- }
- /// <summary>
- /// The maximum number of characters to render in any given column. This prevents one long column from pushing out all the others
- /// </summary>
- public int MaximumCellWidth { get; set; } = 100;
- /// <summary>
- /// The text representation that should be rendered for cells with the value <see cref="DBNull.Value"/>
- /// </summary>
- public string NullSymbol { get; set; } = "-";
- /// <summary>
- /// The symbol to add after each cell value and header value to visually seperate values
- /// </summary>
- public char SeparatorSymbol { get; set; } = ' ';
- /// <summary>
- /// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout.
- /// </summary>
- /// <param name="table">The table to display in the control</param>
- public TableView (DataTable table) : base ()
- {
- this.Table = table;
- }
- /// <summary>
- /// Initialzies a <see cref="TableView"/> class using <see cref="LayoutStyle.Computed"/> layout. Set the <see cref="Table"/> property to begin editing
- /// </summary>
- public TableView () : base ()
- {
- }
- ///<inheritdoc/>
- public override void Redraw (Rect bounds)
- {
- Attribute currentAttribute;
- var current = ColorScheme.Focus;
- Driver.SetAttribute (current);
- Move (0, 0);
- var frame = Frame;
- // What columns to render at what X offset in viewport
- Dictionary<DataColumn, int> columnsToRender = CalculateViewport (bounds);
- Driver.SetAttribute (ColorScheme.Normal);
- //invalidate current row (prevents scrolling around leaving old characters in the frame
- Driver.AddStr (new string (' ', bounds.Width));
- // Render the headers
- foreach (var kvp in columnsToRender) {
- Move (kvp.Value, 0);
- Driver.AddStr (Truncate (kvp.Key.ColumnName + SeparatorSymbol, bounds.Width - kvp.Value));
- }
- //render the cells
- for (int line = 1; line < frame.Height; line++) {
- //invalidate current row (prevents scrolling around leaving old characters in the frame
- Move (0, line);
- Driver.SetAttribute (ColorScheme.Normal);
- Driver.AddStr (new string (' ', bounds.Width));
- //work out what Row to render
- var rowToRender = RowOffset + (line - 1);
- //if we have run off the end of the table
- if ( Table == null || rowToRender >= Table.Rows.Count)
- continue;
- foreach (var kvp in columnsToRender) {
- Move (kvp.Value, line);
- bool isSelectedCell = rowToRender == SelectedRow && kvp.Key.Ordinal == SelectedColumn;
- Driver.SetAttribute (isSelectedCell ? ColorScheme.HotFocus : ColorScheme.Normal);
- var valueToRender = GetRenderedVal (Table.Rows [rowToRender] [kvp.Key]) + SeparatorSymbol;
- Driver.AddStr (Truncate (valueToRender, bounds.Width - kvp.Value));
- }
- }
- void SetAttribute (Attribute attribute)
- {
- if (currentAttribute != attribute) {
- currentAttribute = attribute;
- Driver.SetAttribute (attribute);
- }
- }
- }
- private ustring Truncate (string valueToRender, int availableHorizontalSpace)
- {
- if (string.IsNullOrEmpty (valueToRender) || valueToRender.Length < availableHorizontalSpace)
- return valueToRender;
- return valueToRender.Substring (0, availableHorizontalSpace);
- }
- /// <inheritdoc/>
- public override bool ProcessKey (KeyEvent keyEvent)
- {
- switch (keyEvent.Key) {
- case Key.CursorLeft:
- SelectedColumn--;
- Update ();
- break;
- case Key.CursorRight:
- SelectedColumn++;
- Update ();
- break;
- case Key.CursorDown:
- SelectedRow++;
- Update ();
- break;
- case Key.CursorUp:
- SelectedRow--;
- Update ();
- break;
- case Key.PageUp:
- SelectedRow -= Frame.Height;
- Update ();
- break;
- case Key.PageDown:
- SelectedRow += Frame.Height;
- Update ();
- break;
- case Key.Home | Key.CtrlMask:
- SelectedRow = 0;
- SelectedColumn = 0;
- Update ();
- break;
- case Key.Home:
- SelectedColumn = 0;
- Update ();
- break;
- case Key.End | Key.CtrlMask:
- //jump to end of table
- SelectedRow = Table == null ? 0 : Table.Rows.Count - 1;
- SelectedColumn = Table == null ? 0 : Table.Columns.Count - 1;
- Update ();
- break;
- case Key.End:
- //jump to end of row
- SelectedColumn = Table == null ? 0 : Table.Columns.Count - 1;
- Update ();
- break;
- }
- PositionCursor ();
- return true;
- }
- /// <summary>
- /// Updates the view to reflect changes to <see cref="Table"/> and to (<see cref="ColumnOffset"/> / <see cref="RowOffset"/>) etc
- /// </summary>
- /// <remarks>This always calls <see cref="View.SetNeedsDisplay()"/></remarks>
- public void Update()
- {
- if(Table == null) {
- SetNeedsDisplay ();
- return;
- }
- Dictionary<DataColumn, int> columnsToRender = CalculateViewport (Bounds);
- //if we have scrolled too far to the left
- if (SelectedColumn < columnsToRender.Keys.Min (col => col.Ordinal)) {
- ColumnOffset = SelectedColumn;
- }
- //if we have scrolled too far to the right
- if (SelectedColumn > columnsToRender.Keys.Max (col => col.Ordinal)) {
- ColumnOffset = SelectedColumn;
- }
- //if we have scrolled too far down
- if (SelectedRow > RowOffset + Bounds.Height - 1) {
- RowOffset = SelectedRow;
- }
- //if we have scrolled too far up
- if (SelectedRow < RowOffset) {
- RowOffset = SelectedRow;
- }
- SetNeedsDisplay ();
- }
- /// <summary>
- /// Calculates which columns should be rendered given the <paramref name="bounds"/> in which to display and the <see cref="ColumnOffset"/>
- /// </summary>
- /// <param name="bounds"></param>
- /// <param name="padding"></param>
- /// <returns></returns>
- private Dictionary<DataColumn, int> CalculateViewport (Rect bounds, int padding = 1)
- {
- Dictionary<DataColumn, int> toReturn = new Dictionary<DataColumn, int> ();
- if(Table == null)
- return toReturn;
- int usedSpace = 0;
- int availableHorizontalSpace = bounds.Width;
- int rowsToRender = bounds.Height - 1; //1 reserved for the headers row
- foreach (var col in Table.Columns.Cast<DataColumn> ().Skip (ColumnOffset)) {
- toReturn.Add (col, usedSpace);
- usedSpace += CalculateMaxRowSize (col, rowsToRender) + padding;
- if (usedSpace > availableHorizontalSpace)
- return toReturn;
- }
- return toReturn;
- }
- /// <summary>
- /// 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"/>
- /// </summary>
- /// <param name="col"></param>
- /// <param name="rowsToRender"></param>
- /// <returns></returns>
- private int CalculateMaxRowSize (DataColumn col, int rowsToRender)
- {
- int spaceRequired = col.ColumnName.Length;
- for (int i = RowOffset; i < RowOffset + rowsToRender && i < Table.Rows.Count; i++) {
- //expand required space if cell is bigger than the last biggest cell or header
- spaceRequired = Math.Max (spaceRequired, GetRenderedVal (Table.Rows [i] [col]).Length);
- }
- return spaceRequired;
- }
- /// <summary>
- /// Returns the value that should be rendered to best represent a strongly typed <paramref name="value"/> read from <see cref="Table"/>
- /// </summary>
- /// <param name="value"></param>
- /// <returns></returns>
- private string GetRenderedVal (object value)
- {
- if (value == null || value == DBNull.Value) {
- return NullSymbol;
- }
- var representation = value.ToString ();
- //if it is too long to fit
- if (representation.Length > MaximumCellWidth)
- return representation.Substring (0, MaximumCellWidth);
- return representation;
- }
- }
- }
|