Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #16 - Add columned lists to TableView (with ListTableSource) #2593

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions Terminal.Gui/Views/TableView/DataTableSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,30 @@ namespace Terminal.Gui {
/// </summary>
public class DataTableSource : ITableSource
{
private readonly DataTable table;
/// <summary>
/// The data table this source wraps.
/// </summary>
public DataTable DataTable { get; private set; }

/// <summary>
/// Creates a new instance based on the data in <paramref name="table"/>.
/// </summary>
/// <param name="table"></param>
public DataTableSource(DataTable table)
{
this.table = table;
this.DataTable = table;
}

/// <inheritdoc/>
public object this [int row, int col] => table.Rows[row][col];
public object this [int row, int col] => DataTable.Rows[row][col];

/// <inheritdoc/>
public int Rows => table.Rows.Count;
public int Rows => DataTable.Rows.Count;

/// <inheritdoc/>
public int Columns => table.Columns.Count;
public int Columns => DataTable.Columns.Count;

/// <inheritdoc/>
public string [] ColumnNames => table.Columns.Cast<DataColumn>().Select (c => c.ColumnName).ToArray ();
public string [] ColumnNames => DataTable.Columns.Cast<DataColumn>().Select (c => c.ColumnName).ToArray ();
}
}
166 changes: 166 additions & 0 deletions Terminal.Gui/Views/TableView/ListColumnView.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using NStack;
using System;
using System.Collections;

namespace Terminal.Gui {

/// <summary>
/// View for columned data based on an <see cref="IList"/>.
///
/// <a href="https://gui-cs.github.io/Terminal.Gui/articles/tableview.html">See TableView Deep Dive for more information</a>.
/// </summary>
public partial class ListColumnView : TableView {

private IList listData;

/// <summary>
/// Returns the number of elements to display
/// </summary>
public int Count { get => ListData.Count; }

/// <summary>
/// Returns the maximum length of elements to display
/// </summary>
public int Length { get => CalculateMaxLength (); }

private ListColumnStyle listStyle = new ListColumnStyle ();

/// <summary>
/// A list to render in the view. Setting this property automatically updates and redraws the control.
/// </summary>
public IList ListData { get => listData; set { listData = value; ListUpdate (); } }

/// <summary>
/// Contains options for changing how the table is rendered when set via <see cref="ListColumnView.ListData"/>
/// </summary>
public ListColumnStyle ListStyle { get => listStyle; set { listStyle = value; ListUpdate (); } }

/// <summary>
/// Initializes a <see cref="ListColumnView"/> class using <see cref="LayoutStyle.Computed"/> layout.
/// </summary>
/// <param name="list">The list of items to display in columns in the control</param>
public ListColumnView (ListTableSource list) : this ()
{
for (var i = 0; i < list.Rows; i++) {
this.ListData.Add (list [i, 0]);
}
this.Table = (ITableSource)list.DataTable;
}

/// <summary>
/// Initializes a <see cref="ListColumnView"/> class using <see cref="LayoutStyle.Computed"/> layout.
/// Set the <see cref="ListData"/> property to begin editing.
/// </summary>
public ListColumnView () : base ()
{
ListData?.Clear ();
Update ();
}

/// <summary>
/// Returns the size in characters of the longest value read from <see cref="ListColumnView.ListData"/>
/// </summary>
/// <returns></returns>
private int CalculateMaxLength ()
{
if (listData == null || listData?.Count == 0) {
return 0;
}

int maxLength = 0;
for (int i = 0; i < listData.Count; i++) {
var t = listData [i];
int l;
if (t is ustring u) {
l = TextFormatter.GetTextWidth (u);
} else if (t is string s) {
l = s.Length;
} else {
l = t.ToString ().Length;
}

if (l > maxLength) {
maxLength = l;
}
}

return maxLength;
}

/// <summary>
/// Creates a columned list from an IList to display in a <see cref="TableView"/>
/// </summary>
public void FlowTable ()
{
if (listData == null || listData?.Count == 0) {
return;
}
int colWidth = CalculateMaxLength ();
if (colWidth == 0) {
return;
}

if (colWidth > MaxCellWidth) {
colWidth = MaxCellWidth;
}

if (MinCellWidth > 0 && colWidth < MinCellWidth) {
if (MinCellWidth > MaxCellWidth) {
colWidth = MaxCellWidth;
} else {
colWidth = MinCellWidth;
}
}

int itemsPerSublist = 1;
if (listStyle.VerticalOrientation && listStyle.ScrollParallel) {
decimal m = ((decimal)(this.Bounds.Width - 1) / colWidth) - 2;
itemsPerSublist = (int)Math.Ceiling (listData.Count / m);
} else if (listStyle.VerticalOrientation && !listStyle.ScrollParallel) {
itemsPerSublist = this.Bounds.Height - GetHeaderHeight ();
} else if (!listStyle.VerticalOrientation && listStyle.ScrollParallel) {
decimal m = (decimal)this.Bounds.Height - GetHeaderHeight ();
itemsPerSublist = (int)Math.Ceiling (listData.Count / m);
} else if (!listStyle.VerticalOrientation && !listStyle.ScrollParallel) {
itemsPerSublist = ((int)Math.Ceiling (((decimal)this.Bounds.Width - 1) / colWidth)) - 2;
}
if (itemsPerSublist < 1) itemsPerSublist = 1;

if (listStyle.VerticalOrientation) {
this.Table = new ListTableSource (listData, itemsPerSublist, true);
} else {
this.Table = new ListTableSource (listData, itemsPerSublist, false);
}
}

/// <summary>
/// Updates the view to reflect changes to <see cref="ListData"/>
/// </summary>
public void ListUpdate ()
{
FlowTable ();
Update ();
}

/// TODO: Update TableView Deep Dive
/// <summary>
/// Defines rendering options that affect how the view is displayed.
///
/// <a href="https://gui-cs.github.io/Terminal.Gui/articles/tableview.html">See TableView Deep Dive for more information</a>.
/// </summary>
public class ListColumnStyle {

/// <summary>
/// Gets or sets a flag indicating whether to populate data of a <see cref="ListColumnView"/> down its columns rather than across its rows.
/// Defaults to <see langword="false"/>.
/// </summary>
public bool VerticalOrientation { get; set; } = false;

/// <summary>
/// Gets or sets a flag indicating whether to scroll a <see cref="ListColumnView"/> in the same direction as item population.
/// Defaults to <see langword="false"/>.
/// </summary>
public bool ScrollParallel { get; set; } = false;
}
}
}
124 changes: 124 additions & 0 deletions Terminal.Gui/Views/TableView/ListTableSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System.Collections;
using System.Collections.Generic;
using System.Data;

namespace Terminal.Gui {
/// <summary>
/// <see cref="ITableSource"/> implementation that wraps
/// a <see cref="System.Collections.IList"/>. This class is
/// mutable: changes are permitted to the wrapped <see cref="IList"/>.
/// </summary>
public class ListTableSource : ITableSource
{
/// <summary>
/// The list this source wraps.
/// </summary>
public IList List;

/// <summary>
/// The data table this source creates.
/// </summary>
public DataTable DataTable;

/// <summary>
/// Creates a new table instance based on the data in <paramref name="list"/>.
/// </summary>
/// <param name="list"></param>
public ListTableSource (IList list)
{
this.List = list;
this.DataTable = CreateTable ();
}

/// <summary>
/// Creates a new table instance based on the data in <paramref name="list"/>.
/// </summary>
/// <param name="list"></param>
/// <param name="cols"></param>
/// <param name="transpose"></param>
public ListTableSource (IList list, int cols = 1, bool transpose = false)
{
this.List = list;
this.DataTable = transpose ? CreateTransposedTable (cols) : CreateTable (cols);
}

/*
/// <inheritdoc/>
public object this [int cell] => list [cell];
*/

/// <inheritdoc/>
public object this [int row, int col] => DataTable.Rows[row][col];

/// <inheritdoc/>
public int Count => List.Count;

/// <inheritdoc/>
public int Rows => DataTable.Rows.Count;

/// <inheritdoc/>
public int Columns => DataTable.Columns.Count;

/// <inheritdoc/>
public string [] ColumnNames => GenerateColumnNumbers ();

private string [] GenerateColumnNumbers()
{
string [] names = new string [Count];
for (int i = 0; i < Count; i++) {
names [i] = i.ToString ();
}
return names;
}

/// <summary>
/// Creates a DataTable from an IList to display in a <see cref="TableView"/>
/// </summary>
protected DataTable CreateTable (int cols = 1)
{
var newTable = new DataTable ();

int i;
for (i = 0; i < cols; i++) {
newTable.Columns.Add (new DataColumn (i.ToString ()));
}

var sublist = new List<string> ();
int j;
for (j = 0; j < List.Count; j++) {
if (j % cols == 0 && sublist.Count > 0) {
newTable.Rows.Add (sublist.ToArray ());
sublist.Clear ();
}
sublist.Add (List [j].ToString ());
}
newTable.Rows.Add (sublist.ToArray ());

return newTable;
}

private DataTable CreateTransposedTable (int cols = 1, bool includeColumnNames = false)
{
var it = CreateTable (cols);
var tt = new DataTable ();
int offset = 0;
if (includeColumnNames) {
tt.Columns.Add (new DataColumn ("0"));
offset++;
}
for (int i = 0; i < it.Columns.Count; i++) {
DataRow row = tt.NewRow ();
if (includeColumnNames) {
row [0] = it.Columns [i].ColumnName;
}
for (int j = offset; j < it.Rows.Count + offset; j++) {
if (tt.Columns.Count < it.Rows.Count + offset)
tt.Columns.Add (new DataColumn (j.ToString ()));
row [j] = it.Rows [j - offset] [i];
}
tt.Rows.Add (row);
}
return tt;
}
}
}
21 changes: 19 additions & 2 deletions Terminal.Gui/Views/TableView/TableView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ public int SelectedRow {
}
}

/// <summary>
/// The minimum number of characters to render in any given column.
/// </summary>
public int MinCellWidth { get; set; }

/// <summary>
/// The maximum number of characters to render in any given column. This prevents one long column from pushing out all the others
/// </summary>
Expand Down Expand Up @@ -329,7 +334,7 @@ private void ClearLine (int row, int width)
/// Returns the amount of vertical space currently occupied by the header or 0 if it is not visible.
/// </summary>
/// <returns></returns>
private int GetHeaderHeightIfAny ()
protected int GetHeaderHeightIfAny ()
{
return ShouldRenderHeaders () ? GetHeaderHeight () : 0;
}
Expand All @@ -338,7 +343,7 @@ private int GetHeaderHeightIfAny ()
/// Returns the amount of vertical space required to display the header
/// </summary>
/// <returns></returns>
private int GetHeaderHeight ()
protected int GetHeaderHeight ()
{
int heightRequired = Style.ShowHeaders ? 1 : 0;

Expand Down Expand Up @@ -1619,6 +1624,18 @@ private IEnumerable<ColumnToRender> CalculateViewport (Rect bounds, int padding
// is there enough space for this column (and it's data)?
colWidth = CalculateMaxCellWidth (col, rowsToRender, colStyle) + padding;

if (colWidth > MaxCellWidth) {
colWidth = MaxCellWidth;
}

if (MinCellWidth > 0 && colWidth < MinCellWidth) {
if (MinCellWidth > MaxCellWidth) {
colWidth = MaxCellWidth;
} else {
colWidth = MinCellWidth;
}
}

// there is not enough space for this columns
// visible content
if (usedSpace + colWidth > availableHorizontalSpace) {
Expand Down
Loading