using NStack;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
namespace Terminal.Gui.Views {
///
/// View for tabular data based on a
///
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(); } }
///
/// Zero indexed offset for the upper left to display in .
///
/// This property allows very wide tables to be rendered with horizontal scrolling
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));
}
///
/// Zero indexed offset for the to display in on line 2 of the control (first line being headers)
///
/// This property allows very wide tables to be rendered with horizontal scrolling
public int RowOffset {
get => rowOffset;
set => rowOffset = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value));
}
///
/// The index of in that the user has currently selected
///
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));
}
///
/// The index of in that the user has currently selected
///
public int SelectedRow {
get => selectedRow;
set => selectedRow = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value));
}
///
/// The maximum number of characters to render in any given column. This prevents one long column from pushing out all the others
///
public int MaximumCellWidth { get; set; } = 100;
///
/// The text representation that should be rendered for cells with the value
///
public string NullSymbol { get; set; } = "-";
///
/// The symbol to add after each cell value and header value to visually seperate values
///
public char SeparatorSymbol { get; set; } = ' ';
///
/// Initialzies a class using layout.
///
/// The table to display in the control
public TableView (DataTable table) : base ()
{
this.Table = table;
}
///
/// Initialzies a class using layout. Set the property to begin editing
///
public TableView () : base ()
{
}
///
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 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);
}
///
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;
}
///
/// Updates the view to reflect changes to and to ( / ) etc
///
/// This always calls
public void Update()
{
if(Table == null) {
SetNeedsDisplay ();
return;
}
Dictionary 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 ();
}
///
/// Calculates which columns should be rendered given the in which to display and the
///
///
///
///
private Dictionary CalculateViewport (Rect bounds, int padding = 1)
{
Dictionary toReturn = new Dictionary ();
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 ().Skip (ColumnOffset)) {
toReturn.Add (col, usedSpace);
usedSpace += CalculateMaxRowSize (col, rowsToRender) + padding;
if (usedSpace > availableHorizontalSpace)
return toReturn;
}
return toReturn;
}
///
/// Returns the maximum of the name and the maximum length of data that will be rendered starting at and rendering
///
///
///
///
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;
}
///
/// Returns the value that should be rendered to best represent a strongly typed read from
///
///
///
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;
}
}
}