diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index 1fa83a51e1..6d18bebadd 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -107,7 +107,7 @@ public partial class FileDialog : Dialog { private MenuBar allowedTypeMenuBar; private MenuBarItem allowedTypeMenu; private MenuItem [] allowedTypeMenuItems; - private DataColumn filenameColumn; + private int filenameColumn; /// /// Event fired when user attempts to confirm a selection (or multi selection). @@ -226,7 +226,7 @@ public FileDialog (IFileSystem fileSystem) if (this.tableView.SelectedRow <= 0) { this.NavigateIf (k, Key.CursorUp, this.tbPath); } - if (this.tableView.SelectedRow == this.tableView.Table.Rows.Count-1) { + if (this.tableView.SelectedRow == this.tableView.Table.Rows - 1) { this.NavigateIf (k, Key.CursorDown, this.btnToggleSplitterCollapse); } @@ -318,7 +318,7 @@ public FileDialog (IFileSystem fileSystem) this.sorter = new FileDialogSorter (this, this.tableView); this.history = new FileDialogHistory (this); - this.tableView.Table = this.dtFiles; + this.tableView.Table = new DataTableSource(this.dtFiles); this.tbPath.TextChanged += (s, e) => this.PathChanged (); @@ -514,7 +514,7 @@ private void ClearFeedback () private void CycleToNextTableEntryBeginningWith (KeyEventEventArgs keyEvent) { - if (tableView.Table.Rows.Count == 0) { + if (tableView.Table.Rows == 0) { return; } @@ -539,11 +539,10 @@ private void UpdateCollectionNavigator () { tableView.EnsureValidSelection (); var col = tableView.SelectedColumn; - var style = tableView.Style.GetColumnStyleIfAny (tableView.Table.Columns [col]); + var style = tableView.Style.GetColumnStyleIfAny (col); - var collection = tableView - .Table + var collection = dtFiles .Rows .Cast () .Select ((o, idx) => col == 0 ? @@ -897,7 +896,7 @@ private void UpdateNavigationVisibility () private void TableView_SelectedCellChanged (object sender, SelectedCellChangedEventArgs obj) { - if (!this.tableView.HasFocus || obj.NewRow == -1 || obj.Table.Rows.Count == 0) { + if (!this.tableView.HasFocus || obj.NewRow == -1 || obj.Table.Rows == 0) { return; } @@ -969,7 +968,7 @@ private void SetupTableColumns () this.dtFiles = new DataTable (); var nameStyle = this.tableView.Style.GetOrCreateColumnStyle ( - filenameColumn = this.dtFiles.Columns.Add (Style.FilenameColumnName, typeof (int)) + filenameColumn = this.dtFiles.Columns.Add (Style.FilenameColumnName, typeof (int)).Ordinal ); nameStyle.RepresentationGetter = (i) => { @@ -989,11 +988,13 @@ private void SetupTableColumns () nameStyle.MinWidth = 50; - var sizeStyle = this.tableView.Style.GetOrCreateColumnStyle (this.dtFiles.Columns.Add (Style.SizeColumnName, typeof (int))); + var sizeStyle = this.tableView.Style.GetOrCreateColumnStyle ( + this.dtFiles.Columns.Add (Style.SizeColumnName, typeof (int)).Ordinal); sizeStyle.RepresentationGetter = (i) => this.State?.Children [(int)i].HumanReadableLength ?? string.Empty; nameStyle.MinWidth = 10; - var dateModifiedStyle = this.tableView.Style.GetOrCreateColumnStyle (this.dtFiles.Columns.Add (Style.ModifiedColumnName, typeof (int))); + var dateModifiedStyle = this.tableView.Style.GetOrCreateColumnStyle ( + this.dtFiles.Columns.Add (Style.ModifiedColumnName, typeof (int)).Ordinal); dateModifiedStyle.RepresentationGetter = (i) => { var s = this.State?.Children [(int)i]; @@ -1006,7 +1007,8 @@ private void SetupTableColumns () dateModifiedStyle.MinWidth = 30; - var typeStyle = this.tableView.Style.GetOrCreateColumnStyle (this.dtFiles.Columns.Add (Style.TypeColumnName, typeof (int))); + var typeStyle = this.tableView.Style.GetOrCreateColumnStyle ( + this.dtFiles.Columns.Add (Style.TypeColumnName, typeof (int)).Ordinal); typeStyle.RepresentationGetter = (i) => this.State?.Children [(int)i].Type ?? string.Empty; typeStyle.MinWidth = 6; @@ -1237,7 +1239,7 @@ private void WriteStateToTableView () private void BuildRow (int idx) { - this.tableView.Table.Rows.Add (idx, idx, idx, idx); + dtFiles.Rows.Add (idx, idx, idx, idx); } private ColorScheme ColorGetter (TableView.CellColorGetterArgs args) @@ -1277,7 +1279,7 @@ private IEnumerable MultiRowToStats () foreach (var p in this.tableView.GetAllSelectedCells ()) { - var add = this.State?.Children [(int)this.tableView.Table.Rows [p.Y] [0]]; + var add = this.State?.Children [(int)this.tableView.Table[p.Y, 0]]; if (add != null) { toReturn.Add (add); } @@ -1288,7 +1290,7 @@ private IEnumerable MultiRowToStats () } private FileSystemInfoStats RowToStats (int rowIndex) { - return this.State?.Children [(int)this.tableView.Table.Rows [rowIndex] [0]]; + return this.State?.Children [(int)this.tableView.Table[rowIndex,0]]; } private int? StatsToRow (IFileSystemInfo fileSystemInfo) { @@ -1299,7 +1301,7 @@ private FileSystemInfoStats RowToStats (int rowIndex) // find the row number in our DataTable where the cell // contains idx - var match = tableView.Table.Rows + var match = dtFiles.Rows .Cast () .Select ((r, rIdx) => new { row = r, rowIdx = rIdx }) .Where (t => (int)t.row [0] == idx) @@ -1367,7 +1369,7 @@ private class FileDialogSorter { private readonly FileDialog dlg; private TableView tableView; - private DataColumn currentSort = null; + private int? currentSort = null; private bool currentSortIsAsc = true; public FileDialogSorter (FileDialog dlg, TableView tableView) @@ -1378,17 +1380,17 @@ public FileDialogSorter (FileDialog dlg, TableView tableView) // if user clicks the mouse in TableView this.tableView.MouseClick += (s, e) => { - var clickedCell = this.tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out DataColumn clickedCol); + var clickedCell = this.tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out int? clickedCol); if (clickedCol != null) { if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) { // left click in a header - this.SortColumn (clickedCol); + this.SortColumn (clickedCol.Value); } else if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) { // right click in a header - this.ShowHeaderContextMenu (clickedCol, e); + this.ShowHeaderContextMenu (clickedCol.Value, e); } } else { if (clickedCell != null && e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) { @@ -1406,10 +1408,14 @@ internal void ApplySort () { var col = this.currentSort; + if(col == null) { + return; + } + // TODO: Consider preserving selection - this.tableView.Table.Rows.Clear (); + dlg.dtFiles.Rows.Clear (); - var colName = col == null ? null : StripArrows (col.ColumnName); + var colName = col == null ? null : StripArrows (tableView.Table.ColumnNames[col.Value]); var stats = this.dlg.State?.Children ?? new FileSystemInfoStats [0]; @@ -1438,13 +1444,13 @@ internal void ApplySort () this.dlg.BuildRow (o.i); } - foreach (DataColumn c in this.tableView.Table.Columns) { + foreach (DataColumn c in dlg.dtFiles.Columns) { // remove any lingering sort indicator c.ColumnName = StripArrows (c.ColumnName); // add a new one if this the one that is being sorted - if (c == col) { + if (c.Ordinal == col) { c.ColumnName += this.currentSortIsAsc ? " (▲)" : " (▼)"; } } @@ -1458,13 +1464,13 @@ private static string StripArrows (string columnName) return columnName.Replace (" (▼)", string.Empty).Replace (" (▲)", string.Empty); } - private void SortColumn (DataColumn clickedCol) + private void SortColumn (int clickedCol) { this.GetProposedNewSortOrder (clickedCol, out var isAsc); this.SortColumn (clickedCol, isAsc); } - internal void SortColumn (DataColumn col, bool isAsc) + internal void SortColumn (int col, bool isAsc) { // set a sort order this.currentSort = col; @@ -1473,19 +1479,19 @@ internal void SortColumn (DataColumn col, bool isAsc) this.ApplySort (); } - private string GetProposedNewSortOrder (DataColumn clickedCol, out bool isAsc) + private string GetProposedNewSortOrder (int clickedCol, out bool isAsc) { // work out new sort order if (this.currentSort == clickedCol && this.currentSortIsAsc) { isAsc = false; - return $"{clickedCol.ColumnName} DESC"; + return $"{tableView.Table.ColumnNames[clickedCol]} DESC"; } else { isAsc = true; - return $"{clickedCol.ColumnName} ASC"; + return $"{tableView.Table.ColumnNames [clickedCol]} ASC"; } } - private void ShowHeaderContextMenu (DataColumn clickedCol, MouseEventEventArgs e) + private void ShowHeaderContextMenu (int clickedCol, MouseEventEventArgs e) { var sort = this.GetProposedNewSortOrder (clickedCol, out var isAsc); @@ -1494,7 +1500,7 @@ private void ShowHeaderContextMenu (DataColumn clickedCol, MouseEventEventArgs e e.MouseEvent.Y + 1, new MenuBarItem (new MenuItem [] { - new MenuItem($"Hide {StripArrows(clickedCol.ColumnName)}", string.Empty, () => this.HideColumn(clickedCol)), + new MenuItem($"Hide {StripArrows(tableView.Table.ColumnNames[clickedCol])}", string.Empty, () => this.HideColumn(clickedCol)), new MenuItem($"Sort {StripArrows(sort)}",string.Empty, ()=> this.SortColumn(clickedCol,isAsc)), }) ); @@ -1524,7 +1530,7 @@ private void ShowCellContextMenu (Point? clickedCell, MouseEventEventArgs e) contextMenu.Show (); } - private void HideColumn (DataColumn clickedCol) + private void HideColumn (int clickedCol) { var style = this.tableView.Style.GetOrCreateColumnStyle (clickedCol); style.Visible = false; diff --git a/Terminal.Gui/Views/TableView/CellActivatedEventArgs.cs b/Terminal.Gui/Views/TableView/CellActivatedEventArgs.cs index beb5dffe82..ed5e5fcdfd 100644 --- a/Terminal.Gui/Views/TableView/CellActivatedEventArgs.cs +++ b/Terminal.Gui/Views/TableView/CellActivatedEventArgs.cs @@ -11,7 +11,7 @@ public class CellActivatedEventArgs : EventArgs { /// 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 /// /// - public DataTable Table { get; } + public ITableSource Table { get; } /// /// The column index of the cell that is being activated @@ -31,7 +31,7 @@ public class CellActivatedEventArgs : EventArgs { /// /// /// - public CellActivatedEventArgs (DataTable t, int col, int row) + public CellActivatedEventArgs (ITableSource t, int col, int row) { Table = t; Col = col; diff --git a/Terminal.Gui/Views/TableView/DataTableSource.cs b/Terminal.Gui/Views/TableView/DataTableSource.cs new file mode 100644 index 0000000000..c6ea33ee50 --- /dev/null +++ b/Terminal.Gui/Views/TableView/DataTableSource.cs @@ -0,0 +1,35 @@ +using System.Data; +using System.Linq; + +namespace Terminal.Gui { + /// + /// implementation that wraps + /// a . This class is + /// mutable: changes are permitted to the wrapped . + /// + public class DataTableSource : ITableSource + { + private readonly DataTable table; + + /// + /// Creates a new instance based on the data in . + /// + /// + public DataTableSource(DataTable table) + { + this.table = table; + } + + /// + public object this [int row, int col] => table.Rows[row][col]; + + /// + public int Rows => table.Rows.Count; + + /// + public int Columns => table.Columns.Count; + + /// + public string [] ColumnNames => table.Columns.Cast().Select (c => c.ColumnName).ToArray (); + } +} diff --git a/Terminal.Gui/Views/TableView/EnumerableTableSource.cs b/Terminal.Gui/Views/TableView/EnumerableTableSource.cs new file mode 100644 index 0000000000..65be790148 --- /dev/null +++ b/Terminal.Gui/Views/TableView/EnumerableTableSource.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Terminal.Gui { + + /// + /// implementation that wraps arbitrary data. + /// + /// + public class EnumerableTableSource : ITableSource { + private T [] data; + private string [] cols; + private Dictionary> lamdas; + + /// + /// Creates a new instance of the class that presents + /// collection as a table. + /// + /// The elements of the collection are recorded during + /// construction (immutable) but the properties of those objects are permitted to + /// change. + /// The data that you want to present. The members of this collection + /// will be frozen after construction. + /// + /// Getter methods for each property you want to present in the table. For example: + /// + /// new () { + /// { "Colname1", (t)=>t.SomeField}, + /// { "Colname2", (t)=>t.SomeOtherField} + ///} + /// + public EnumerableTableSource (IEnumerable data, Dictionary> columnDefinitions) + { + this.data = data.ToArray (); + this.cols = columnDefinitions.Keys.ToArray (); + this.lamdas = columnDefinitions; + } + + /// + public object this [int row, int col] { + get => this.lamdas [ColumnNames [col]] (this.data [row]); + } + + /// + public int Rows => data.Length; + + /// + public int Columns => cols.Length; + + /// + public string [] ColumnNames => cols; + } +} diff --git a/Terminal.Gui/Views/TableView/ITableSource.cs b/Terminal.Gui/Views/TableView/ITableSource.cs new file mode 100644 index 0000000000..2747f13c89 --- /dev/null +++ b/Terminal.Gui/Views/TableView/ITableSource.cs @@ -0,0 +1,33 @@ +namespace Terminal.Gui { + /// + /// Tabular matrix of data to be displayed in a . + /// + public interface ITableSource + { + /// + /// Gets the number of rows in the table. + /// + int Rows { get; } + + /// + /// Gets the number of columns in the table. + /// + int Columns { get; } + + /// + /// Gets the label for each column. + /// + string[] ColumnNames { get; } + + /// + /// Returns the data at the given indexes of the table (row, column). + /// + /// + /// + /// + object this[int row, int col] + { + get; + } + } +} diff --git a/Terminal.Gui/Views/TableView/SelectedCellChangedEventArgs.cs b/Terminal.Gui/Views/TableView/SelectedCellChangedEventArgs.cs index 67e6c3e3d4..c5e275bee6 100644 --- a/Terminal.Gui/Views/TableView/SelectedCellChangedEventArgs.cs +++ b/Terminal.Gui/Views/TableView/SelectedCellChangedEventArgs.cs @@ -11,7 +11,7 @@ public class SelectedCellChangedEventArgs : EventArgs { /// 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 /// /// - public DataTable Table { get; } + public ITableSource Table { get; } /// /// 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 @@ -45,7 +45,7 @@ public class SelectedCellChangedEventArgs : EventArgs { /// /// /// - public SelectedCellChangedEventArgs (DataTable t, int oldCol, int newCol, int oldRow, int newRow) + public SelectedCellChangedEventArgs (ITableSource t, int oldCol, int newCol, int oldRow, int newRow) { Table = t; OldCol = oldCol; diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index e4f3ebc0c0..30ad804d19 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -7,19 +7,18 @@ namespace Terminal.Gui { - /// - /// View for tabular data based on a . + /// View for tabular data based on a . /// /// See TableView Deep Dive for more information. /// - public partial class TableView : View { + public class TableView : View { private int columnOffset; private int rowOffset; private int selectedRow; private int selectedColumn; - private DataTable table; + private ITableSource table; private TableStyle style = new TableStyle (); private Key cellActivationKey = Key.Enter; @@ -39,7 +38,7 @@ public partial class TableView : View { /// /// The data table to render in the view. Setting this property automatically updates and redraws the control. /// - public DataTable Table { get => table; set { table = value; Update (); } } + public ITableSource Table { get => table; set { table = value; Update (); } } /// /// Contains options for changing how the table is rendered @@ -71,7 +70,7 @@ public int ColumnOffset { get => columnOffset; //try to prevent this being set to an out of bounds column - set => columnOffset = TableIsNullOrInvisible () ? 0 : Math.Max (0, Math.Min (Table.Columns.Count - 1, value)); + set => columnOffset = TableIsNullOrInvisible () ? 0 : Math.Max (0, Math.Min (Table.Columns - 1, value)); } /// @@ -79,7 +78,7 @@ public int ColumnOffset { /// public int RowOffset { get => rowOffset; - set => rowOffset = TableIsNullOrInvisible () ? 0 : Math.Max (0, Math.Min (Table.Rows.Count - 1, value)); + set => rowOffset = TableIsNullOrInvisible () ? 0 : Math.Max (0, Math.Min (Table.Rows - 1, value)); } /// @@ -92,7 +91,7 @@ public int SelectedColumn { var oldValue = selectedColumn; //try to prevent this being set to an out of bounds column - selectedColumn = TableIsNullOrInvisible () ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); + selectedColumn = TableIsNullOrInvisible () ? 0 : Math.Min (Table.Columns - 1, Math.Max (0, value)); if (oldValue != selectedColumn) OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, oldValue, SelectedColumn, SelectedRow, SelectedRow)); @@ -108,7 +107,7 @@ public int SelectedRow { var oldValue = selectedRow; - selectedRow = TableIsNullOrInvisible () ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); + selectedRow = TableIsNullOrInvisible () ? 0 : Math.Min (Table.Rows - 1, Math.Max (0, value)); if (oldValue != selectedRow) OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, SelectedColumn, SelectedColumn, oldValue, selectedRow)); @@ -161,7 +160,7 @@ public Key CellActivationKey { /// Initialzies a class using layout. /// /// The table to display in the control - public TableView (DataTable table) : this () + public TableView (ITableSource table) : this () { this.Table = table; } @@ -231,8 +230,9 @@ public TableView () : base () /// public override void Redraw (Rect bounds) { + base.Redraw (bounds); + Move (0, 0); - var frame = Frame; scrollRightPoint = null; scrollLeftPoint = null; @@ -273,7 +273,7 @@ public override void Redraw (Rect bounds) int headerLinesConsumed = line; //render the cells - for (; line < frame.Height; line++) { + for (; line < Bounds.Height; line++) { ClearLine (line, bounds.Width); @@ -285,11 +285,12 @@ public override void Redraw (Rect bounds) continue; // No more data - if(rowToRender >= Table.Rows.Count) { + if(rowToRender >= Table.Rows) { - if(rowToRender == Table.Rows.Count && Style.ShowHorizontalBottomline) { + if(rowToRender == Table.Rows && Style.ShowHorizontalBottomline) { RenderBottomLine (line, bounds.Width, columnsToRender); } + continue; } @@ -382,7 +383,7 @@ private void RenderHeaderMidline (int row, ColumnToRender [] columnsToRender) var current = columnsToRender [i]; var colStyle = Style.GetColumnStyleIfAny (current.Column); - var colName = current.Column.ColumnName; + var colName = table.ColumnNames[current.Column]; RenderSeparator (current.X - 1, row, true); @@ -420,7 +421,7 @@ private void RenderHeaderUnderline (int row, int availableWidth, ColumnToRender int lastColumnIdxRendered = ColumnOffset + columnsToRender.Length - 1; // are there more valid indexes? - bool moreColumnsToRight = lastColumnIdxRendered < Table.Columns.Count; + bool moreColumnsToRight = lastColumnIdxRendered < Table.Columns; // if we went right from the last column would we find a new visible column? if (!TryGetNearestVisibleColumn (lastColumnIdxRendered + 1, true, false, out _)) { @@ -555,9 +556,9 @@ private void RenderRow (int row, int rowToRender, ColumnToRender [] columnsToRen Move (current.X, row); // Set color scheme based on whether the current cell is the selected one - bool isSelectedCell = IsSelected (current.Column.Ordinal, rowToRender); + bool isSelectedCell = IsSelected (current.Column, rowToRender); - var val = Table.Rows [rowToRender] [current.Column]; + var val = Table [rowToRender, current.Column]; // Render the (possibly truncated) cell value var representation = GetRepresentation (val, colStyle); @@ -569,7 +570,7 @@ private void RenderRow (int row, int rowToRender, ColumnToRender [] columnsToRen if (colorSchemeGetter != null) { // user has a delegate for defining row color per cell, call it scheme = colorSchemeGetter ( - new CellColorGetterArgs (Table, rowToRender, current.Column.Ordinal, val, representation, rowScheme)); + new CellColorGetterArgs (Table, rowToRender, current.Column, val, representation, rowScheme)); // if users custom color getter returned null, use the row scheme if (scheme == null) { @@ -590,7 +591,7 @@ private void RenderRow (int row, int rowToRender, ColumnToRender [] columnsToRen var render = TruncateOrPad (val, representation, current.Width, colStyle); // While many cells can be selected (see MultiSelectedRegions) only one cell is the primary (drives navigation etc) - bool isPrimaryCell = current.Column.Ordinal == selectedColumn && rowToRender == selectedRow; + bool isPrimaryCell = current.Column == selectedColumn && rowToRender == selectedRow; RenderCell (cellColor, render, isPrimaryCell); @@ -880,9 +881,9 @@ public void ChangeSelectionToStartOfTable (bool extend) /// true to extend the current selection (if any) instead of replacing public void ChangeSelectionToEndOfTable (bool extend) { - var finalColumn = Table.Columns.Count - 1; + var finalColumn = Table.Columns - 1; - SetSelection (FullRowSelect ? SelectedColumn : finalColumn, Table.Rows.Count - 1, extend); + SetSelection (FullRowSelect ? SelectedColumn : finalColumn, Table.Rows - 1, extend); Update (); } @@ -892,7 +893,7 @@ public void ChangeSelectionToEndOfTable (bool extend) /// true to extend the current selection (if any) instead of replacing public void ChangeSelectionToEndOfRow (bool extend) { - SetSelection (Table.Columns.Count - 1, SelectedRow, extend); + SetSelection (Table.Columns - 1, SelectedRow, extend); Update (); } @@ -911,13 +912,13 @@ public void ChangeSelectionToStartOfRow (bool extend) /// public void SelectAll () { - if (TableIsNullOrInvisible () || !MultiSelect || Table.Rows.Count == 0) + if (TableIsNullOrInvisible () || !MultiSelect || Table.Rows == 0) return; ClearMultiSelectedRegions (true); // 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 - MultiSelectedRegions.Push (new TableSelection (new Point (SelectedColumn, SelectedRow), new Rect (0, 0, Table.Columns.Count, table.Rows.Count))); + MultiSelectedRegions.Push (new TableSelection (new Point (SelectedColumn, SelectedRow), new Rect (0, 0, Table.Columns, table.Rows))); Update (); } @@ -927,7 +928,7 @@ public void SelectAll () /// public IEnumerable GetAllSelectedCells () { - if (TableIsNullOrInvisible () || Table.Rows.Count == 0) + if (TableIsNullOrInvisible () || Table.Rows == 0) { return Enumerable.Empty(); } @@ -944,7 +945,7 @@ public IEnumerable GetAllSelectedCells () var yMax = MultiSelectedRegions.Max (r => r.Rect.Bottom); var xMin = FullRowSelect ? 0 : MultiSelectedRegions.Min (r => r.Rect.Left); - var xMax = FullRowSelect ? Table.Columns.Count : MultiSelectedRegions.Max (r => r.Rect.Right); + var xMax = FullRowSelect ? Table.Columns : MultiSelectedRegions.Max (r => r.Rect.Right); for (int y = yMin; y < yMax; y++) { for (int x = xMin; x < xMax; x++) { @@ -960,7 +961,7 @@ public IEnumerable GetAllSelectedCells () // if we are selecting the full row if (FullRowSelect) { // all cells in active row are selected - for (int x = 0; x < Table.Columns.Count; x++) { + for (int x = 0; x < Table.Columns; x++) { toReturn.Add(new Point (x, SelectedRow)); } } else { @@ -1094,11 +1095,11 @@ private IEnumerable GetMultiSelectedRegionsContaining(int col, i private bool IsColumnVisible (int columnIndex) { // if the column index provided is out of bounds - if (columnIndex < 0 || columnIndex >= table.Columns.Count) { + if (columnIndex < 0 || columnIndex >= table.Columns) { return false; } - return this.Style.GetColumnStyleIfAny (Table.Columns [columnIndex])?.Visible ?? true; + return this.Style.GetColumnStyleIfAny (columnIndex)?.Visible ?? true; } /// @@ -1160,25 +1161,29 @@ public override bool MouseEvent (MouseEvent me) return true; } + // TODO: Revert this (or not) once #2578 is solved + var boundsX = me.X - GetFramesThickness ().Left; + var boundsY = me.Y - GetFramesThickness ().Top; + if (me.Flags.HasFlag (MouseFlags.Button1Clicked)) { if (scrollLeftPoint != null - && scrollLeftPoint.Value.X == me.X - && scrollLeftPoint.Value.Y == me.Y) { + && scrollLeftPoint.Value.X == boundsX + && scrollLeftPoint.Value.Y == boundsY) { ColumnOffset--; EnsureValidScrollOffsets (); SetNeedsDisplay (); } if (scrollRightPoint != null - && scrollRightPoint.Value.X == me.X - && scrollRightPoint.Value.Y == me.Y) { + && scrollRightPoint.Value.X == boundsX + && scrollRightPoint.Value.Y == boundsY) { ColumnOffset++; EnsureValidScrollOffsets (); SetNeedsDisplay (); } - var hit = ScreenToCell (me.X, me.Y); + var hit = ScreenToCell (boundsX, boundsY); if (hit != null) { if (MultiSelect && HasControlOrAlt (me)) { @@ -1193,7 +1198,7 @@ public override bool MouseEvent (MouseEvent me) // Double clicking a cell activates if (me.Flags == MouseFlags.Button1DoubleClicked) { - var hit = ScreenToCell (me.X, me.Y); + var hit = ScreenToCell (boundsX, boundsY); if (hit != null) { OnCellActivated (new CellActivatedEventArgs (Table, hit.Value.X, hit.Value.Y)); } @@ -1224,7 +1229,7 @@ private bool HasControlOrAlt (MouseEvent me) /// X offset from the top left of the control. /// Y offset from the top left of the control. /// If the click is in a header this is the column clicked. - public Point? ScreenToCell (int clientX, int clientY, out DataColumn headerIfAny) + public Point? ScreenToCell (int clientX, int clientY, out int? headerIfAny) { headerIfAny = null; @@ -1247,13 +1252,13 @@ private bool HasControlOrAlt (MouseEvent me) // if click is off bottom of the rows don't give an // invalid index back to user! - if (rowIdx >= Table.Rows.Count) { + if (rowIdx >= Table.Rows) { return null; } if (col != null && rowIdx >= 0) { - return new Point (col.Column.Ordinal, rowIdx); + return new Point (col.Column, rowIdx); } return null; @@ -1262,7 +1267,7 @@ private bool HasControlOrAlt (MouseEvent me) /// /// 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 /// - /// The index of the column you are looking for, use + /// The index of the column you are looking for /// The index of the row in that you are looking for /// public Point? CellToScreen (int tableColumn, int tableRow) @@ -1274,7 +1279,7 @@ private bool HasControlOrAlt (MouseEvent me) var headerHeight = GetHeaderHeightIfAny (); - var colHit = viewPort.FirstOrDefault (c => c.Column.Ordinal == tableColumn); + var colHit = viewPort.FirstOrDefault (c => c.Column == tableColumn); // current column is outside the scroll area if (colHit == null) @@ -1319,8 +1324,8 @@ public void EnsureValidScrollOffsets () return; } - ColumnOffset = Math.Max (Math.Min (ColumnOffset, Table.Columns.Count - 1), 0); - RowOffset = Math.Max (Math.Min (RowOffset, Table.Rows.Count - 1), 0); + ColumnOffset = Math.Max (Math.Min (ColumnOffset, Table.Columns - 1), 0); + RowOffset = Math.Max (Math.Min (RowOffset, Table.Rows - 1), 0); } /// @@ -1336,8 +1341,8 @@ public void EnsureValidSelection () return; } - SelectedColumn = Math.Max (Math.Min (SelectedColumn, Table.Columns.Count - 1), 0); - SelectedRow = Math.Max (Math.Min (SelectedRow, Table.Rows.Count - 1), 0); + SelectedColumn = Math.Max (Math.Min (SelectedColumn, Table.Columns - 1), 0); + SelectedRow = Math.Max (Math.Min (SelectedRow, Table.Rows - 1), 0); // If SelectedColumn is invisible move it to a visible one SelectedColumn = GetNearestVisibleColumn (SelectedColumn, lookRight: true, true); @@ -1349,23 +1354,23 @@ public void EnsureValidSelection () // evaluate foreach (var region in oldRegions) { // ignore regions entirely below current table state - if (region.Rect.Top >= Table.Rows.Count) + if (region.Rect.Top >= Table.Rows) continue; // ignore regions entirely too far right of table columns - if (region.Rect.Left >= Table.Columns.Count) + if (region.Rect.Left >= Table.Columns) continue; // ensure region's origin exists region.Origin = new Point ( - Math.Max (Math.Min (region.Origin.X, Table.Columns.Count - 1), 0), - Math.Max (Math.Min (region.Origin.Y, Table.Rows.Count - 1), 0)); + Math.Max (Math.Min (region.Origin.X, Table.Columns - 1), 0), + Math.Max (Math.Min (region.Origin.Y, Table.Rows - 1), 0)); // ensure regions do not go over edge of table bounds region.Rect = Rect.FromLTRB (region.Rect.Left, region.Rect.Top, - Math.Max (Math.Min (region.Rect.Right, Table.Columns.Count), 0), - Math.Max (Math.Min (region.Rect.Bottom, Table.Rows.Count), 0) + Math.Max (Math.Min (region.Rect.Right, Table.Columns), 0), + Math.Max (Math.Min (region.Rect.Bottom, Table.Rows), 0) ); MultiSelectedRegions.Push (region); @@ -1374,7 +1379,7 @@ public void EnsureValidSelection () /// /// Returns true if the is not set or all the - /// in the have an explicit + /// columns in the have an explicit /// that marks them /// . /// @@ -1382,14 +1387,14 @@ public void EnsureValidSelection () private bool TableIsNullOrInvisible () { return Table == null || - Table.Columns.Count <= 0 || - Table.Columns.Cast ().All ( + Table.Columns <= 0 || + Enumerable.Range(0,Table.Columns).All ( c => (Style.GetColumnStyleIfAny (c)?.Visible ?? true) == false); } /// /// Returns unless the is false for - /// the indexed . If so then the index returned is nudged to the nearest visible + /// the indexed column. If so then the index returned is nudged to the nearest visible /// column. /// /// Returns unchanged if it is invalid (e.g. out of bounds). @@ -1412,14 +1417,15 @@ private int GetNearestVisibleColumn (int columnIndex, bool lookRight, bool allow private bool TryGetNearestVisibleColumn (int columnIndex, bool lookRight, bool allowBumpingInOppositeDirection, out int idx) { // if the column index provided is out of bounds - if (columnIndex < 0 || columnIndex >= table.Columns.Count) { + if (columnIndex < 0 || columnIndex >= table.Columns) { idx = columnIndex; return false; } // get the column visibility by index (if no style visible is true) - bool [] columnVisibility = Table.Columns.Cast () + bool [] columnVisibility = + Enumerable.Range(0,Table.Columns) .Select (c => this.Style.GetColumnStyleIfAny (c)?.Visible ?? true) .ToArray (); @@ -1470,7 +1476,7 @@ private bool TryGetNearestVisibleColumn (int columnIndex, bool lookRight, bool a /// Changes will not be immediately visible in the display until you call public void EnsureSelectedCellIsVisible () { - if (Table == null || Table.Columns.Count <= 0) { + if (Table == null || Table.Columns <= 0) { return; } @@ -1478,24 +1484,24 @@ public void EnsureSelectedCellIsVisible () var headerHeight = GetHeaderHeightIfAny (); //if we have scrolled too far to the left - if (SelectedColumn < columnsToRender.Min (r => r.Column.Ordinal)) { + if (SelectedColumn < columnsToRender.Min (r => r.Column)) { ColumnOffset = SelectedColumn; } //if we have scrolled too far to the right - if (SelectedColumn > columnsToRender.Max (r => r.Column.Ordinal)) { + if (SelectedColumn > columnsToRender.Max (r => r.Column)) { if (Style.SmoothHorizontalScrolling) { // Scroll right 1 column at a time until the users selected column is visible - while (SelectedColumn > columnsToRender.Max (r => r.Column.Ordinal)) { + while (SelectedColumn > columnsToRender.Max (r => r.Column)) { ColumnOffset++; columnsToRender = CalculateViewport (Bounds).ToArray (); // if we are already scrolled to the last column then break // this will prevent any theoretical infinite loop - if (ColumnOffset >= Table.Columns.Count - 1) + if (ColumnOffset >= Table.Columns - 1) break; } @@ -1559,9 +1565,10 @@ private IEnumerable CalculateViewport (Rect bounds, int padding rowsToRender -= GetHeaderHeight (); bool first = true; - var lastColumn = Table.Columns.Cast ().Last (); + var lastColumn = Table.Columns - 1; - foreach (var col in Table.Columns.Cast ().Skip (ColumnOffset)) { + // TODO : Maybe just a for loop? + foreach (var col in Enumerable.Range(0,Table.Columns).Skip (ColumnOffset)) { int startingIdxForCurrentHeader = usedSpace; var colStyle = Style.GetColumnStyleIfAny (col); @@ -1639,18 +1646,21 @@ private bool ShouldRenderHeaders () /// /// /// - private int CalculateMaxCellWidth (DataColumn col, int rowsToRender, ColumnStyle colStyle) + private int CalculateMaxCellWidth (int col, int rowsToRender, ColumnStyle colStyle) { - int spaceRequired = col.ColumnName.Sum (c => Rune.ColumnWidth (c)); + int spaceRequired = table.ColumnNames[col].Sum (c => Rune.ColumnWidth (c)); // if table has no rows if (RowOffset < 0) return spaceRequired; - for (int i = RowOffset; i < RowOffset + rowsToRender && i < Table.Rows.Count; i++) { + + for (int i = RowOffset; i < RowOffset + rowsToRender && i < Table.Rows; i++) { //expand required space if cell is bigger than the last biggest cell or header - spaceRequired = Math.Max (spaceRequired, GetRepresentation (Table.Rows [i] [col], colStyle).Sum (c => Rune.ColumnWidth (c))); + spaceRequired = Math.Max ( + spaceRequired, + GetRepresentation (Table [i,col], colStyle).Sum (c => Rune.ColumnWidth (c))); } // Don't require more space than the style allows @@ -1874,7 +1884,7 @@ public class TableStyle { /// /// Collection of columns for which you want special rendering (e.g. custom column lengths, text alignment etc) /// - public Dictionary ColumnStyles { get; set; } = new Dictionary (); + public Dictionary ColumnStyles { get; set; } = new Dictionary (); /// /// Delegate for coloring specific rows in a different color. For cell color @@ -1913,7 +1923,7 @@ public class TableStyle { /// /// /// - public ColumnStyle GetColumnStyleIfAny (DataColumn col) + public ColumnStyle GetColumnStyleIfAny (int col) { return ColumnStyles.TryGetValue (col, out ColumnStyle result) ? result : null; } @@ -1923,7 +1933,7 @@ public ColumnStyle GetColumnStyleIfAny (DataColumn col) /// /// /// - public ColumnStyle GetOrCreateColumnStyle (DataColumn col) + public ColumnStyle GetOrCreateColumnStyle (int col) { if (!ColumnStyles.ContainsKey (col)) ColumnStyles.Add (col, new ColumnStyle ()); @@ -1940,7 +1950,7 @@ internal class ColumnToRender { /// /// The column to render /// - public DataColumn Column { get; set; } + public int Column { get; set; } /// /// The horizontal position to begin rendering the column at @@ -1958,7 +1968,7 @@ internal class ColumnToRender { /// public bool IsVeryLast { get; } - public ColumnToRender (DataColumn col, int x, int width, bool isVeryLast) + public ColumnToRender (int col, int x, int width, bool isVeryLast) { Column = col; X = x; @@ -1977,7 +1987,7 @@ public class CellColorGetterArgs { /// /// The data table hosted by the control. /// - public DataTable Table { get; } + public ITableSource Table { get; } /// /// The index of the row in for which color is needed @@ -2004,7 +2014,7 @@ public class CellColorGetterArgs { /// public ColorScheme RowScheme { get; } - internal CellColorGetterArgs (DataTable table, int rowIdx, int colIdx, object cellValue, string representation, ColorScheme rowScheme) + internal CellColorGetterArgs (ITableSource table, int rowIdx, int colIdx, object cellValue, string representation, ColorScheme rowScheme) { Table = table; RowIndex = rowIdx; @@ -2017,7 +2027,7 @@ internal CellColorGetterArgs (DataTable table, int rowIdx, int colIdx, object ce } /// - /// Arguments for . Describes a row of data in a + /// Arguments for . Describes a row of data in a /// for which is sought. /// public class RowColorGetterArgs { @@ -2025,14 +2035,14 @@ public class RowColorGetterArgs { /// /// The data table hosted by the control. /// - public DataTable Table { get; } + public ITableSource Table { get; } /// /// The index of the row in for which color is needed /// public int RowIndex { get; } - internal RowColorGetterArgs (DataTable table, int rowIdx) + internal RowColorGetterArgs (ITableSource table, int rowIdx) { Table = table; RowIndex = rowIdx; diff --git a/UICatalog/Scenarios/CsvEditor.cs b/UICatalog/Scenarios/CsvEditor.cs index 4d61348d93..09e515d2ad 100644 --- a/UICatalog/Scenarios/CsvEditor.cs +++ b/UICatalog/Scenarios/CsvEditor.cs @@ -22,6 +22,7 @@ namespace UICatalog.Scenarios { public class CsvEditor : Scenario { TableView tableView; private string currentFile; + DataTable currentTable; private MenuItem miLeft; private MenuItem miRight; private MenuItem miCentered; @@ -121,9 +122,7 @@ private void OnSelectedCellChanged (object sender, SelectedCellChangedEventArgs if (tableView.Table == null || tableView.SelectedColumn == -1) return; - var col = tableView.Table.Columns [tableView.SelectedColumn]; - - var style = tableView.Style.GetColumnStyleIfAny (col); + var style = tableView.Style.GetColumnStyleIfAny (tableView.SelectedColumn); miLeft.Checked = style?.Alignment == TextAlignment.Left; miRight.Checked = style?.Alignment == TextAlignment.Right; @@ -136,7 +135,7 @@ private void RenameColumn () return; } - var currentCol = tableView.Table.Columns [tableView.SelectedColumn]; + var currentCol = currentTable.Columns [tableView.SelectedColumn]; if (GetText ("Rename Column", "Name:", currentCol.ColumnName, out string newName)) { currentCol.ColumnName = newName; @@ -157,7 +156,7 @@ private void DeleteColum () } try { - tableView.Table.Columns.RemoveAt (tableView.SelectedColumn); + currentTable.Columns.RemoveAt (tableView.SelectedColumn); tableView.Update (); } catch (Exception ex) { @@ -179,11 +178,11 @@ private void MoveColumn () try { - var currentCol = tableView.Table.Columns [tableView.SelectedColumn]; + var currentCol = currentTable.Columns [tableView.SelectedColumn]; if (GetText ("Move Column", "New Index:", currentCol.Ordinal.ToString (), out string newOrdinal)) { - var newIdx = Math.Min (Math.Max (0, int.Parse (newOrdinal)), tableView.Table.Columns.Count - 1); + var newIdx = Math.Min (Math.Max (0, int.Parse (newOrdinal)), tableView.Table.Columns - 1); currentCol.SetOrdinal (newIdx); @@ -209,10 +208,15 @@ private void Sort (bool asc) return; } - var colName = tableView.Table.Columns [tableView.SelectedColumn].ColumnName; + var colName = tableView.Table.ColumnNames [tableView.SelectedColumn]; + + currentTable.DefaultView.Sort = colName + (asc ? " asc" : " desc"); + SetTable(currentTable.DefaultView.ToTable ()); + } - tableView.Table.DefaultView.Sort = colName + (asc ? " asc" : " desc"); - tableView.Table = tableView.Table.DefaultView.ToTable (); + private void SetTable (DataTable dataTable) + { + tableView.Table = new DataTableSource(currentTable = dataTable); } private void MoveRow () @@ -231,23 +235,23 @@ private void MoveRow () int oldIdx = tableView.SelectedRow; - var currentRow = tableView.Table.Rows [oldIdx]; + var currentRow = currentTable.Rows [oldIdx]; if (GetText ("Move Row", "New Row:", oldIdx.ToString (), out string newOrdinal)) { - var newIdx = Math.Min (Math.Max (0, int.Parse (newOrdinal)), tableView.Table.Rows.Count - 1); + var newIdx = Math.Min (Math.Max (0, int.Parse (newOrdinal)), tableView.Table.Rows - 1); if (newIdx == oldIdx) return; var arrayItems = currentRow.ItemArray; - tableView.Table.Rows.Remove (currentRow); + currentTable.Rows.Remove (currentRow); // Removing and Inserting the same DataRow seems to result in it loosing its values so we have to create a new instance - var newRow = tableView.Table.NewRow (); + var newRow = currentTable.NewRow (); newRow.ItemArray = arrayItems; - tableView.Table.Rows.InsertAt (newRow, newIdx); + currentTable.Rows.InsertAt (newRow, newIdx); tableView.SetSelection (tableView.SelectedColumn, newIdx, false); tableView.EnsureSelectedCellIsVisible (); @@ -265,9 +269,7 @@ private void Align (TextAlignment newAlignment) return; } - var col = tableView.Table.Columns [tableView.SelectedColumn]; - - var style = tableView.Style.GetOrCreateColumnStyle (col); + var style = tableView.Style.GetOrCreateColumnStyle (tableView.SelectedColumn); style.Alignment = newAlignment; miLeft.Checked = style.Alignment == TextAlignment.Left; @@ -283,14 +285,14 @@ private void SetFormat () return; } - var col = tableView.Table.Columns [tableView.SelectedColumn]; + var col = currentTable.Columns [tableView.SelectedColumn]; if (col.DataType == typeof (string)) { MessageBox.ErrorQuery ("Cannot Format Column", "String columns cannot be Formatted, try adding a new column to the table with a date/numerical Type", "Ok"); return; } - var style = tableView.Style.GetOrCreateColumnStyle (col); + var style = tableView.Style.GetOrCreateColumnStyle (col.Ordinal); if (GetText ("Format", "Pattern:", style.Format ?? "", out string newPattern)) { style.Format = newPattern; @@ -314,11 +316,11 @@ private void AddRow () return; } - var newRow = tableView.Table.NewRow (); + var newRow = currentTable.NewRow (); - var newRowIdx = Math.Min (Math.Max (0, tableView.SelectedRow + 1), tableView.Table.Rows.Count); + var newRowIdx = Math.Min (Math.Max (0, tableView.SelectedRow + 1), tableView.Table.Rows); - tableView.Table.Rows.InsertAt (newRow, newRowIdx); + currentTable.Rows.InsertAt (newRow, newRowIdx); tableView.Update (); } @@ -332,7 +334,7 @@ private void AddColumn () var col = new DataColumn (colName); - var newColIdx = Math.Min (Math.Max (0, tableView.SelectedColumn + 1), tableView.Table.Columns.Count); + var newColIdx = Math.Min (Math.Max (0, tableView.SelectedColumn + 1), tableView.Table.Columns); int result = MessageBox.Query ("Column Type", "Pick a data type for the column", new ustring [] { "Date", "Integer", "Double", "Text", "Cancel" }); @@ -353,7 +355,7 @@ private void AddColumn () break; } - tableView.Table.Columns.Add (col); + currentTable.Columns.Add (col); col.SetOrdinal (newColIdx); tableView.Update (); } @@ -371,13 +373,13 @@ private void Save () new StreamWriter (File.OpenWrite (currentFile)), CultureInfo.InvariantCulture); - foreach (var col in tableView.Table.Columns.Cast ().Select (c => c.ColumnName)) { + foreach (var col in currentTable.Columns.Cast ().Select (c => c.ColumnName)) { writer.WriteField (col); } writer.NextRecord (); - foreach (DataRow row in tableView.Table.Rows) { + foreach (DataRow row in currentTable.Rows) { foreach (var item in row.ItemArray) { writer.WriteField (item); } @@ -428,7 +430,7 @@ private void Open (string filename) } } - tableView.Table = dt; + SetTable(dt); // Only set the current filename if we successfully loaded the entire file currentFile = filename; @@ -459,7 +461,7 @@ private void SetupScrollBar () };*/ tableView.DrawContent += (s, e) => { - _scrollBar.Size = tableView.Table?.Rows?.Count ?? 0; + _scrollBar.Size = tableView.Table?.Rows ?? 0; _scrollBar.Position = tableView.RowOffset; // _scrollBar.OtherScrollBarView.Size = _listView.Maxlength - 1; // _scrollBar.OtherScrollBarView.Position = _listView.LeftItem; @@ -475,12 +477,12 @@ private void TableViewKeyPress (object sender, KeyEventEventArgs e) if (tableView.FullRowSelect) { // Delete button deletes all rows when in full row mode foreach (int toRemove in tableView.GetAllSelectedCells ().Select (p => p.Y).Distinct ().OrderByDescending (i => i)) - tableView.Table.Rows.RemoveAt (toRemove); + currentTable.Rows.RemoveAt (toRemove); } else { // otherwise set all selected cells to null foreach (var pt in tableView.GetAllSelectedCells ()) { - tableView.Table.Rows [pt.Y] [pt.X] = DBNull.Value; + currentTable.Rows [pt.Y] [pt.X] = DBNull.Value; } } @@ -540,11 +542,11 @@ private void EditCurrentCell (object sender, CellActivatedEventArgs e) if (e.Table == null) return; - var oldValue = e.Table.Rows [e.Row] [e.Col].ToString (); + var oldValue = currentTable.Rows [e.Row] [e.Col].ToString (); - if (GetText ("Enter new value", e.Table.Columns [e.Col].ColumnName, oldValue, out string newText)) { + if (GetText ("Enter new value", currentTable.Columns [e.Col].ColumnName, oldValue, out string newText)) { try { - e.Table.Rows [e.Row] [e.Col] = string.IsNullOrWhiteSpace (newText) ? DBNull.Value : (object)newText; + currentTable.Rows [e.Row] [e.Col] = string.IsNullOrWhiteSpace (newText) ? DBNull.Value : (object)newText; } catch (Exception ex) { MessageBox.ErrorQuery (60, 20, "Failed to set text", ex.Message, "Ok"); } diff --git a/UICatalog/Scenarios/MultiColouredTable.cs b/UICatalog/Scenarios/MultiColouredTable.cs index 85fb25b435..101df3d316 100644 --- a/UICatalog/Scenarios/MultiColouredTable.cs +++ b/UICatalog/Scenarios/MultiColouredTable.cs @@ -10,6 +10,7 @@ namespace UICatalog.Scenarios { [ScenarioCategory ("TableView")] public class MultiColouredTable : Scenario { TableViewColors tableView; + private DataTable table; public override void Setup () { @@ -60,7 +61,7 @@ public override void Setup () Normal = Application.Driver.MakeAttribute (Color.DarkGray, Color.Black) }; - tableView.Table = dt; + tableView.Table = new DataTableSource(this.table = dt); } private void Quit () @@ -103,11 +104,11 @@ private void EditCurrentCell (object sender, CellActivatedEventArgs e) if (e.Table == null) return; - var oldValue = e.Table.Rows [e.Row] [e.Col].ToString (); + var oldValue = e.Table[e.Row, e.Col].ToString (); - if (GetText ("Enter new value", e.Table.Columns [e.Col].ColumnName, oldValue, out string newText)) { + if (GetText ("Enter new value", e.Table.ColumnNames [e.Col], oldValue, out string newText)) { try { - e.Table.Rows [e.Row] [e.Col] = string.IsNullOrWhiteSpace (newText) ? DBNull.Value : (object)newText; + table.Rows [e.Row] [e.Col] = string.IsNullOrWhiteSpace (newText) ? DBNull.Value : (object)newText; } catch (Exception ex) { MessageBox.ErrorQuery (60, 20, "Failed to set text", ex.Message, "Ok"); } diff --git a/UICatalog/Scenarios/ProcessTable.cs b/UICatalog/Scenarios/ProcessTable.cs new file mode 100644 index 0000000000..d07f16bcbd --- /dev/null +++ b/UICatalog/Scenarios/ProcessTable.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Data; +using Terminal.Gui; +using System.Linq; +using System.Globalization; +using static Terminal.Gui.TableView; +using System.Diagnostics; + +namespace UICatalog.Scenarios { + + [ScenarioMetadata (Name: "ProcessTable", Description: "Demonstrates TableView with the currently running processes.")] + [ScenarioCategory ("TableView")] + public class ProcessTable : Scenario { + TableView tableView; + public override void Setup () + { + Win.Title = this.GetName (); + Win.Y = 1; // menu + Win.Height = Dim.Fill (1); // status bar + Application.Top.LayoutSubviews (); + + this.tableView = new TableView () { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill (1), + }; + + // First time + CreateProcessTable (); + + // Then every second + Application.MainLoop.AddTimeout (TimeSpan.FromSeconds (1), + (s) => { + CreateProcessTable (); + return true; + }); + + Win.Add (tableView); + + } + + private void CreateProcessTable () + { + var ro = tableView.RowOffset; + var co = tableView.ColumnOffset; + tableView.Table = new EnumerableTableSource (Process.GetProcesses (), + new Dictionary>() { + { "ID",(p)=>p.Id}, + { "Name",(p)=>p.ProcessName}, + { "Threads",(p)=>p.Threads.Count}, + { "Virtual Memory",(p)=>p.VirtualMemorySize64}, + { "Working Memory",(p)=>p.WorkingSet64}, + }); + + tableView.RowOffset = ro; + tableView.ColumnOffset = co; + tableView.EnsureValidScrollOffsets (); + } + } +} diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index ab67ee2914..bc57cc73c7 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -16,6 +16,7 @@ namespace UICatalog.Scenarios { [ScenarioCategory ("Top Level Windows")] public class TableEditor : Scenario { TableView tableView; + DataTable currentTable; private MenuItem miShowHeaders; private MenuItem miAlwaysShowHeaders; private MenuItem miHeaderOverline; @@ -139,17 +140,17 @@ public override void Setup () // if user clicks the mouse in TableView tableView.MouseClick += (s,e) => { - tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out DataColumn clickedCol); + tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out int? clickedCol); if (clickedCol != null) { if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) { // left click in a header - SortColumn (clickedCol); + SortColumn (clickedCol.Value); } else if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) { // right click in a header - ShowHeaderContextMenu (clickedCol, e); + ShowHeaderContextMenu (clickedCol.Value, e); } } }; @@ -165,32 +166,32 @@ private void ShowAllColumns () tableView.Update (); } - private void SortColumn (DataColumn clickedCol) + private void SortColumn (int clickedCol) { var sort = GetProposedNewSortOrder (clickedCol, out var isAsc); SortColumn (clickedCol, sort, isAsc); } - private void SortColumn (DataColumn clickedCol, string sort, bool isAsc) + private void SortColumn (int clickedCol, string sort, bool isAsc) { // set a sort order - tableView.Table.DefaultView.Sort = sort; + currentTable.DefaultView.Sort = sort; // copy the rows from the view - var sortedCopy = tableView.Table.DefaultView.ToTable (); - tableView.Table.Rows.Clear (); + var sortedCopy = currentTable.DefaultView.ToTable (); + currentTable.Rows.Clear (); foreach (DataRow r in sortedCopy.Rows) { - tableView.Table.ImportRow (r); + currentTable.ImportRow (r); } - foreach (DataColumn col in tableView.Table.Columns) { + foreach (DataColumn col in currentTable.Columns) { // remove any lingering sort indicator col.ColumnName = TrimArrows (col.ColumnName); // add a new one if this the one that is being sorted - if (col == clickedCol) { + if (col.Ordinal == clickedCol) { col.ColumnName += isAsc ? '▲' : '▼'; } } @@ -206,29 +207,31 @@ private string StripArrows (string columnName) { return columnName.Replace ("▼", "").Replace ("▲", ""); } - private string GetProposedNewSortOrder (DataColumn clickedCol, out bool isAsc) + private string GetProposedNewSortOrder (int clickedCol, out bool isAsc) { // work out new sort order - var sort = tableView.Table.DefaultView.Sort; + var sort = currentTable.DefaultView.Sort; + var colName = currentTable.Columns[clickedCol]; if (sort?.EndsWith ("ASC") ?? false) { - sort = $"{clickedCol.ColumnName} DESC"; + sort = $"{colName} DESC"; isAsc = false; } else { - sort = $"{clickedCol.ColumnName} ASC"; + sort = $"{colName} ASC"; isAsc = true; } return sort; } - private void ShowHeaderContextMenu (DataColumn clickedCol, MouseEventEventArgs e) + private void ShowHeaderContextMenu (int clickedCol, MouseEventEventArgs e) { var sort = GetProposedNewSortOrder (clickedCol, out var isAsc); + var colName = tableView.Table.ColumnNames[clickedCol]; var contextMenu = new ContextMenu (e.MouseEvent.X + 1, e.MouseEvent.Y + 1, new MenuBarItem (new MenuItem [] { - new MenuItem ($"Hide {TrimArrows(clickedCol.ColumnName)}", "", () => HideColumn(clickedCol)), + new MenuItem ($"Hide {TrimArrows(colName)}", "", () => HideColumn(clickedCol)), new MenuItem ($"Sort {StripArrows(sort)}","",()=>SortColumn(clickedCol,sort,isAsc)), }) ); @@ -236,7 +239,7 @@ private void ShowHeaderContextMenu (DataColumn clickedCol, MouseEventEventArgs e contextMenu.Show (); } - private void HideColumn (DataColumn clickedCol) + private void HideColumn (int clickedCol) { var style = tableView.Style.GetOrCreateColumnStyle (clickedCol); style.Visible = false; @@ -248,16 +251,16 @@ private DataColumn GetColumn () if (tableView.Table == null) return null; - if (tableView.SelectedColumn < 0 || tableView.SelectedColumn > tableView.Table.Columns.Count) + if (tableView.SelectedColumn < 0 || tableView.SelectedColumn > tableView.Table.Columns) return null; - return tableView.Table.Columns [tableView.SelectedColumn]; + return currentTable.Columns [tableView.SelectedColumn]; } private void SetMinAcceptableWidthToOne () { - foreach (DataColumn c in tableView.Table.Columns) { - var style = tableView.Style.GetOrCreateColumnStyle (c); + foreach (DataColumn c in currentTable.Columns) { + var style = tableView.Style.GetOrCreateColumnStyle (c.Ordinal); style.MinAcceptableWidth = 1; } } @@ -288,7 +291,7 @@ private void RunColumnWidthDialog (DataColumn col, string prompt, Action { Application.RequestStop (); }; var d = new Dialog (ok, cancel) { Title = prompt }; - var style = tableView.Style.GetOrCreateColumnStyle (col); + var style = tableView.Style.GetOrCreateColumnStyle (col.Ordinal); var lbl = new Label () { X = 0, @@ -341,7 +344,7 @@ private void SetupScrollBar () };*/ tableView.DrawContent += (s,e) => { - _scrollBar.Size = tableView.Table?.Rows?.Count ?? 0; + _scrollBar.Size = tableView.Table?.Rows ?? 0; _scrollBar.Position = tableView.RowOffset; // _scrollBar.OtherScrollBarView.Size = _listView.Maxlength - 1; // _scrollBar.OtherScrollBarView.Position = _listView.LeftItem; @@ -357,12 +360,12 @@ private void TableViewKeyPress (object sender, KeyEventEventArgs e) if (tableView.FullRowSelect) { // Delete button deletes all rows when in full row mode foreach (int toRemove in tableView.GetAllSelectedCells ().Select (p => p.Y).Distinct ().OrderByDescending (i => i)) - tableView.Table.Rows.RemoveAt (toRemove); + currentTable.Rows.RemoveAt (toRemove); } else { // otherwise set all selected cells to null foreach (var pt in tableView.GetAllSelectedCells ()) { - tableView.Table.Rows [pt.Y] [pt.X] = DBNull.Value; + currentTable.Rows [pt.Y] [pt.X] = DBNull.Value; } } @@ -520,13 +523,18 @@ private void Quit () private void OpenExample (bool big) { - tableView.Table = BuildDemoDataTable (big ? 30 : 5, big ? 1000 : 5); + SetTable(BuildDemoDataTable (big ? 30 : 5, big ? 1000 : 5)); SetDemoTableStyles (); } + private void SetTable (DataTable dataTable) + { + tableView.Table = new DataTableSource(currentTable = dataTable); + } + private void OpenUnicodeMap () { - tableView.Table = BuildUnicodeMap (); + SetTable(BuildUnicodeMap ()); tableView.Update (); } @@ -538,7 +546,7 @@ private DataTable BuildUnicodeMap () for (int i = 0; i < 10; i++) { var col = dt.Columns.Add (i.ToString (), typeof (uint)); - var style = tableView.Style.GetOrCreateColumnStyle (col); + var style = tableView.Style.GetOrCreateColumnStyle (col.Ordinal); style.RepresentationGetter = (o) => new Rune ((uint)o).ToString (); } @@ -546,7 +554,7 @@ private DataTable BuildUnicodeMap () for (int i = 'a'; i < 'a' + 26; i++) { var col = dt.Columns.Add (((char)i).ToString (), typeof (uint)); - var style = tableView.Style.GetOrCreateColumnStyle (col); + var style = tableView.Style.GetOrCreateColumnStyle (col.Ordinal); style.RepresentationGetter = (o) => new Rune ((uint)o).ToString (); } @@ -727,6 +735,8 @@ public UnicodeRange (uint start, uint end, string category) }; private void SetDemoTableStyles () { + tableView.Style.ColumnStyles.Clear(); + var alignMid = new TableView.ColumnStyle () { Alignment = TextAlignment.Centered }; @@ -760,28 +770,28 @@ private void SetDemoTableStyles () null }; - tableView.Style.ColumnStyles.Add (tableView.Table.Columns ["DateCol"], dateFormatStyle); - tableView.Style.ColumnStyles.Add (tableView.Table.Columns ["DoubleCol"], negativeRight); - tableView.Style.ColumnStyles.Add (tableView.Table.Columns ["NullsCol"], alignMid); - tableView.Style.ColumnStyles.Add (tableView.Table.Columns ["IntCol"], alignRight); + tableView.Style.ColumnStyles.Add (currentTable.Columns ["DateCol"].Ordinal, dateFormatStyle); + tableView.Style.ColumnStyles.Add (currentTable.Columns ["DoubleCol"].Ordinal, negativeRight); + tableView.Style.ColumnStyles.Add (currentTable.Columns ["NullsCol"].Ordinal, alignMid); + tableView.Style.ColumnStyles.Add (currentTable.Columns ["IntCol"].Ordinal, alignRight); tableView.Update (); } private void OpenSimple (bool big) { - tableView.Table = BuildSimpleDataTable (big ? 30 : 5, big ? 1000 : 5); + SetTable(BuildSimpleDataTable (big ? 30 : 5, big ? 1000 : 5)); } private void EditCurrentCell (object sender, CellActivatedEventArgs e) { if (e.Table == null) return; - var o = e.Table.Rows [e.Row] [e.Col]; + var o = currentTable.Rows [e.Row] [e.Col]; var title = o is uint u ? GetUnicodeCategory (u) + $"(0x{o:X4})" : "Enter new value"; - var oldValue = e.Table.Rows [e.Row] [e.Col].ToString (); + var oldValue = currentTable.Rows [e.Row] [e.Col].ToString (); bool okPressed = false; var ok = new Button ("Ok", is_default: true); @@ -793,7 +803,7 @@ private void EditCurrentCell (object sender, CellActivatedEventArgs e) var lbl = new Label () { X = 0, Y = 1, - Text = e.Table.Columns [e.Col].ColumnName + Text = currentTable.Columns [e.Col].ColumnName }; var tf = new TextField () { @@ -811,7 +821,7 @@ private void EditCurrentCell (object sender, CellActivatedEventArgs e) if (okPressed) { try { - e.Table.Rows [e.Row] [e.Col] = string.IsNullOrWhiteSpace (tf.Text.ToString ()) ? DBNull.Value : (object)tf.Text; + currentTable.Rows [e.Row] [e.Col] = string.IsNullOrWhiteSpace (tf.Text.ToString ()) ? DBNull.Value : (object)tf.Text; } catch (Exception ex) { MessageBox.ErrorQuery (60, 20, "Failed to set text", ex.Message, "Ok"); } diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index ece04fe680..a29406585c 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -12,9 +12,10 @@ using System.Threading; using static Terminal.Gui.ConfigurationManager; using System.Text.Json.Serialization; +using static Terminal.Gui.TableView; -#nullable enable +#nullable enable /// /// UI Catalog is a comprehensive sample library for Terminal.Gui. It provides a simple UI for adding to the catalog of scenarios. /// @@ -253,8 +254,8 @@ public class UICatalogTopLevel : Toplevel { public MenuItem? miIsMouseDisabled; public MenuItem? miEnableConsoleScrolling; - public ListView CategoryListView; - public ListView ScenarioListView; + public ListView CategoryList; + public TableView ScenarioList; public StatusItem Capslock; public StatusItem Numlock; @@ -323,7 +324,7 @@ public UICatalogTopLevel () //ContentPane.SetSplitterPos (0, 25); //ContentPane.ShortcutAction = () => ContentPane.SetFocus (); - CategoryListView = new ListView (_categories) { + CategoryList = new ListView (_categories) { X = 0, Y = 1, Width = Dim.Percent (30), @@ -334,37 +335,53 @@ public UICatalogTopLevel () BorderStyle = LineStyle.Single, SuperViewRendersLineCanvas = true }; - CategoryListView.OpenSelectedItem += (s, a) => { - ScenarioListView!.SetFocus (); + CategoryList.OpenSelectedItem += (s, a) => { + ScenarioList!.SetFocus (); }; - CategoryListView.SelectedItemChanged += CategoryListView_SelectedChanged; - - //ContentPane.Tiles.ElementAt (0).Title = "Categories"; - //ContentPane.Tiles.ElementAt (0).MinSize = 2; - //ContentPane.Tiles.ElementAt (0).ContentView.Add (CategoryListView); + CategoryList.SelectedItemChanged += CategoryView_SelectedChanged; - ScenarioListView = new ListView () { - X = Pos.Right (CategoryListView) - 1, + ScenarioList = new TableView () { + X = Pos.Right (CategoryList) - 1, Y = 1, Width = Dim.Fill (0), Height = Dim.Fill (1), - AllowsMarking = false, + //AllowsMarking = false, CanFocus = true, Title = "Scenarios", BorderStyle = LineStyle.Single, SuperViewRendersLineCanvas = true }; - - ScenarioListView.OpenSelectedItem += ScenarioListView_OpenSelectedItem; - - //ContentPane.Tiles.ElementAt (1).Title = "Scenarios"; - //ContentPane.Tiles.ElementAt (1).ContentView.Add (ScenarioListView); - //ContentPane.Tiles.ElementAt (1).MinSize = 2; + ScenarioList.FullRowSelect = true; + //ScenarioList.Style.ShowHeaders = false; + ScenarioList.Style.ShowHorizontalHeaderOverline = false; + //ScenarioList.Style.ShowHorizontalHeaderUnderline = false; + ScenarioList.Style.ShowHorizontalBottomline = false; + ScenarioList.Style.ShowVerticalCellLines = false; + ScenarioList.Style.ShowVerticalHeaderLines = false; + + /* By default TableView lays out columns at render time and only + * measures y rows of data at a time. Where y is the height of the + * console. This is for the following reasons: + * + * - Performance, when tables have a large amount of data + * - Defensive, prevents a single wide cell value pushing other + * columns off screen (requiring horizontal scrolling + * + * In the case of UICatalog here, such an approach is overkill so + * we just measure all the data ourselves and set the appropriate + * max widths as ColumnStyles + */ + + var longestName = _scenarios!.Max (s => s.GetName ().Length); + ScenarioList.Style.ColumnStyles.Add (0, new ColumnStyle () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }); + ScenarioList.Style.ColumnStyles.Add (1, new ColumnStyle () { MaxWidth = 1 }); + + ScenarioList.CellActivated += ScenarioView_OpenSelectedItem; KeyDown += KeyDownHandler; - //Add (ContentPane); - Add (CategoryListView); - Add (ScenarioListView); + + Add (CategoryList); + Add (ScenarioList); Add (MenuBar); Add (StatusBar); @@ -373,8 +390,10 @@ public UICatalogTopLevel () Unloaded += UnloadedHandler; // Restore previous selections - CategoryListView.SelectedItem = _cachedCategoryIndex; - ScenarioListView.SelectedItem = _cachedScenarioIndex; + CategoryList.SelectedItem = _cachedCategoryIndex; + CategoryList.EnsureSelectedItemVisible (); + ScenarioList.SelectedRow = _cachedScenarioIndex; + ScenarioList.EnsureSelectedCellIsVisible (); ConfigurationManager.Applied += ConfigAppliedHandler; } @@ -393,15 +412,15 @@ void LoadedHandler (object? sender, EventArgs? args) _isFirstRunning = false; } if (!_isFirstRunning) { - ScenarioListView.SetFocus (); + ScenarioList.SetFocus (); } StatusBar.VisibleChanged += (s, e) => { UICatalogApp.ShowStatusBar = StatusBar.Visible; var height = (StatusBar.Visible ? 1 : 0); - CategoryListView.Height = Dim.Fill (height); - ScenarioListView.Height = Dim.Fill (height); + CategoryList.Height = Dim.Fill (height); + ScenarioList.Height = Dim.Fill (height); // ContentPane.Height = Dim.Fill (height); LayoutSubviews (); SetSubViewNeedsDisplay (); @@ -425,16 +444,16 @@ void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) /// Launches the selected scenario, setting the global _selectedScenario /// /// - void ScenarioListView_OpenSelectedItem (object? sender, EventArgs? e) + void ScenarioView_OpenSelectedItem (object? sender, EventArgs? e) { if (_selectedScenario is null) { // Save selected item state - _cachedCategoryIndex = CategoryListView.SelectedItem; - _cachedScenarioIndex = ScenarioListView.SelectedItem; - // Create new instance of scenario (even though Scenarios contains instances) - var sourceList = ScenarioListView.Source.ToList (); + _cachedCategoryIndex = CategoryList.SelectedItem; + _cachedScenarioIndex = ScenarioList.SelectedRow; - _selectedScenario = (Scenario)Activator.CreateInstance (ScenarioListView.Source.ToList () [ScenarioListView.SelectedItem]!.GetType ())!; + // Create new instance of scenario (even though Scenarios contains instances) + string selectedScenarioName = (string)ScenarioList.Table [ScenarioList.SelectedRow, 0]; + _selectedScenario = (Scenario)Activator.CreateInstance (_scenarios!.FirstOrDefault (s => s.GetName () == selectedScenarioName)!.GetType ())!; // Tell the main app to stop Application.RequestStop (); @@ -729,18 +748,22 @@ void KeyDownHandler (object? sender, KeyEventEventArgs? a) } } - void CategoryListView_SelectedChanged (object? sender, ListViewItemEventArgs? e) + void CategoryView_SelectedChanged (object? sender, ListViewItemEventArgs? e) { var item = _categories! [e!.Item]; List newlist; if (e.Item == 0) { // First category is "All" newlist = _scenarios!; + newlist = _scenarios!; } else { newlist = _scenarios!.Where (s => s.GetCategories ().Contains (item)).ToList (); } - ScenarioListView.SetSource (newlist.ToList ()); + ScenarioList.Table = new EnumerableTableSource (newlist, new Dictionary> () { + { "Name", (s) => s.GetName() }, + { "Description", (s) => s.GetDescription() }, + }); } } diff --git a/UnitTests/Views/TableViewTests.cs b/UnitTests/Views/TableViewTests.cs index befc6a3d28..acd0b7fa84 100644 --- a/UnitTests/Views/TableViewTests.cs +++ b/UnitTests/Views/TableViewTests.cs @@ -28,7 +28,7 @@ public void EnsureValidScrollOffsets_WithNoCells () Assert.Equal (0, tableView.ColumnOffset); // Set empty table - tableView.Table = new DataTable (); + tableView.Table = new DataTableSource(new DataTable ()); // Since table has no rows or columns scroll offset should default to 0 tableView.EnsureValidScrollOffsets (); @@ -83,10 +83,10 @@ public void Redraw_EmptyTable () tableView.Bounds = new Rect (0, 0, 25, 10); // Set a table with 1 column - tableView.Table = BuildTable (1, 50); + tableView.Table = BuildTable (1, 50, out var dt); tableView.Redraw (tableView.Bounds); - tableView.Table.Columns.Remove (tableView.Table.Columns [0]); + dt.Columns.Remove (dt.Columns [0]); tableView.Redraw (tableView.Bounds); } @@ -286,7 +286,7 @@ public void DeleteRow_SelectAll_AdjustsSelectionToPreventOverrun () { // create a 4 by 4 table var tableView = new TableView () { - Table = BuildTable (4, 4), + Table = BuildTable (4, 4, out var dt), MultiSelect = true, Bounds = new Rect (0, 0, 10, 5) }; @@ -296,13 +296,13 @@ public void DeleteRow_SelectAll_AdjustsSelectionToPreventOverrun () Assert.Equal (16, tableView.GetAllSelectedCells ().Count ()); // delete one of the columns - tableView.Table.Columns.RemoveAt (2); + dt.Columns.RemoveAt (2); // table should now be 3x4 Assert.Equal (12, tableView.GetAllSelectedCells ().Count ()); // remove a row - tableView.Table.Rows.RemoveAt (1); + dt.Rows.RemoveAt (1); // table should now be 3x3 Assert.Equal (9, tableView.GetAllSelectedCells ().Count ()); @@ -313,7 +313,7 @@ public void DeleteRow_SelectLastRow_AdjustsSelectionToPreventOverrun () { // create a 4 by 4 table var tableView = new TableView () { - Table = BuildTable (4, 4), + Table = BuildTable (4, 4, out var dt), MultiSelect = true, Bounds = new Rect (0, 0, 10, 5) }; @@ -328,7 +328,7 @@ public void DeleteRow_SelectLastRow_AdjustsSelectionToPreventOverrun () Assert.Equal (4, tableView.GetAllSelectedCells ().Count ()); // remove a row - tableView.Table.Rows.RemoveAt (0); + dt.Rows.RemoveAt (0); tableView.EnsureValidSelection (); @@ -598,7 +598,7 @@ public void TableView_Activate () { string activatedValue = null; var tv = new TableView (BuildTable (1, 1)); - tv.CellActivated += (s, c) => activatedValue = c.Table.Rows [c.Row] [c.Col].ToString (); + tv.CellActivated += (s, c) => activatedValue = c.Table [c.Row,c.Col].ToString(); Application.Top.Add (tv); Application.Begin (Application.Top); @@ -633,8 +633,8 @@ public void TableView_Activate () [Fact, AutoInitShutdown] public void TableViewMultiSelect_CannotFallOffLeft () { - var tv = SetUpMiniTable (); - tv.Table.Rows.Add (1, 2); // add another row (brings us to 2 rows) + var tv = SetUpMiniTable (out var dt); + dt.Rows.Add (1, 2); // add another row (brings us to 2 rows) tv.MultiSelect = true; tv.SelectedColumn = 1; @@ -656,8 +656,8 @@ public void TableViewMultiSelect_CannotFallOffLeft () [Fact, AutoInitShutdown] public void TableViewMultiSelect_CannotFallOffRight () { - var tv = SetUpMiniTable (); - tv.Table.Rows.Add (1, 2); // add another row (brings us to 2 rows) + var tv = SetUpMiniTable (out var dt); + dt.Rows.Add (1, 2); // add another row (brings us to 2 rows) tv.MultiSelect = true; tv.SelectedColumn = 0; @@ -679,8 +679,8 @@ public void TableViewMultiSelect_CannotFallOffRight () [Fact, AutoInitShutdown] public void TableViewMultiSelect_CannotFallOffBottom () { - var tv = SetUpMiniTable (); - tv.Table.Rows.Add (1, 2); // add another row (brings us to 2 rows) + var tv = SetUpMiniTable (out var dt); + dt.Rows.Add (1, 2); // add another row (brings us to 2 rows) tv.MultiSelect = true; tv.SelectedColumn = 0; @@ -704,8 +704,8 @@ public void TableViewMultiSelect_CannotFallOffBottom () [Fact, AutoInitShutdown] public void TableViewMultiSelect_CannotFallOffTop () { - var tv = SetUpMiniTable (); - tv.Table.Rows.Add (1, 2); // add another row (brings us to 2 rows) + var tv = SetUpMiniTable (out var dt); + dt.Rows.Add (1, 2); // add another row (brings us to 2 rows) tv.LayoutSubviews (); tv.MultiSelect = true; @@ -764,8 +764,8 @@ public void TestShiftClick_MultiSelect_TwoRowTable_FullRowSelect () [Fact, AutoInitShutdown] public void TestControlClick_MultiSelect_ThreeRowTable_FullRowSelect () { - var tv = GetTwoRowSixColumnTable (); - tv.Table.Rows.Add (1, 2, 3, 4, 5, 6); + var tv = GetTwoRowSixColumnTable (out var dt); + dt.Rows.Add (1, 2, 3, 4, 5, 6); tv.LayoutSubviews (); tv.MultiSelect = true; @@ -893,7 +893,7 @@ public void TableView_ColorTests_InvertSelectedCellFirstCharacter (bool focused) [InlineData (true)] public void TableView_ColorsTest_RowColorGetter (bool focused) { - var tv = SetUpMiniTable (); + var tv = SetUpMiniTable (out DataTable dt); tv.LayoutSubviews (); // width exactly matches the max col widths @@ -907,7 +907,7 @@ public void TableView_ColorsTest_RowColorGetter (bool focused) }; // when B is 2 use the custom highlight colour for the row - tv.Style.RowColorGetter += (e) => Convert.ToInt32 (e.Table.Rows [e.RowIndex] [1]) == 2 ? rowHighlight : null; + tv.Style.RowColorGetter += (e) => Convert.ToInt32 (e.Table[e.RowIndex,1]) == 2 ? rowHighlight : null; // private method for forcing the view to be focused/not focused var setFocusMethod = typeof (View).GetMethod ("SetHasFocus", BindingFlags.Instance | BindingFlags.NonPublic); @@ -944,7 +944,7 @@ public void TableView_ColorsTest_RowColorGetter (bool focused) // it no longer matches the RowColorGetter // delegate conditional ( which checks for // the value 2) - tv.Table.Rows [0] [1] = 5; + dt.Rows [0][1] = 5; tv.Redraw (tv.Bounds); expected = @" @@ -980,14 +980,14 @@ public void TableView_ColorsTest_RowColorGetter (bool focused) [InlineData (true)] public void TableView_ColorsTest_ColorGetter (bool focused) { - var tv = SetUpMiniTable (); + var tv = SetUpMiniTable (out var dt); tv.LayoutSubviews (); // width exactly matches the max col widths tv.Bounds = new Rect (0, 0, 5, 4); // Create a style for column B - var bStyle = tv.Style.GetOrCreateColumnStyle (tv.Table.Columns ["B"]); + var bStyle = tv.Style.GetOrCreateColumnStyle (1); // when B is 2 use the custom highlight colour var cellHighlight = new ColorScheme () { @@ -1034,7 +1034,7 @@ public void TableView_ColorsTest_ColorGetter (bool focused) // it no longer matches the ColorGetter // delegate conditional ( which checks for // the value 2) - tv.Table.Rows [0] [1] = 5; + dt.Rows [0] [1] = 5; tv.Redraw (tv.Bounds); expected = @" @@ -1066,21 +1066,25 @@ public void TableView_ColorsTest_ColorGetter (bool focused) } private TableView SetUpMiniTable () + { + return SetUpMiniTable (out _); + } + private TableView SetUpMiniTable (out DataTable dt) { var tv = new TableView (); tv.BeginInit (); tv.EndInit (); tv.Bounds = new Rect (0, 0, 10, 4); - var dt = new DataTable (); - var colA = dt.Columns.Add ("A"); - var colB = dt.Columns.Add ("B"); + dt = new DataTable (); + dt.Columns.Add ("A"); + dt.Columns.Add ("B"); dt.Rows.Add (1, 2); - tv.Table = dt; - tv.Style.GetOrCreateColumnStyle (colA).MinWidth = 1; - tv.Style.GetOrCreateColumnStyle (colA).MinWidth = 1; - tv.Style.GetOrCreateColumnStyle (colB).MaxWidth = 1; - tv.Style.GetOrCreateColumnStyle (colB).MaxWidth = 1; + tv.Table = new DataTableSource(dt); + tv.Style.GetOrCreateColumnStyle (0).MinWidth = 1; + tv.Style.GetOrCreateColumnStyle (0).MinWidth = 1; + tv.Style.GetOrCreateColumnStyle (1).MaxWidth = 1; + tv.Style.GetOrCreateColumnStyle (1).MaxWidth = 1; tv.ColorScheme = Colors.Base; return tv; @@ -1140,7 +1144,7 @@ public void ScrollRight_SmoothScrolling () dt.Rows.Add (1, 2, 3, 4, 5, 6); - tableView.Table = dt; + tableView.Table = new DataTableSource(dt); // select last visible column tableView.SelectedColumn = 2; // column C @@ -1201,7 +1205,7 @@ public void ScrollRight_WithoutSmoothScrolling () dt.Rows.Add (1, 2, 3, 4, 5, 6); - tableView.Table = dt; + tableView.Table = new DataTableSource(dt); // select last visible column tableView.SelectedColumn = 2; // column C @@ -1260,7 +1264,7 @@ private TableView GetABCDEFTableView (out DataTable dt) dt.Columns.Add ("F"); dt.Rows.Add (1, 2, 3, 4, 5, 6); - tableView.Table = dt; + tableView.Table = new DataTableSource(dt); return tableView; } @@ -1268,9 +1272,9 @@ private TableView GetABCDEFTableView (out DataTable dt) [Fact, AutoInitShutdown] public void TestColumnStyle_VisibleFalse_IsNotRendered () { - var tableView = GetABCDEFTableView (out DataTable dt); + var tableView = GetABCDEFTableView (out _); - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (1).Visible = false; tableView.LayoutSubviews (); tableView.Redraw (tableView.Bounds); @@ -1289,7 +1293,7 @@ public void TestColumnStyle_FirstColumnVisibleFalse_IsNotRendered () tableView.Style.ShowHorizontalScrollIndicators = true; tableView.Style.ShowHorizontalHeaderUnderline = true; - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (0).Visible = false; tableView.LayoutSubviews (); tableView.Redraw (tableView.Bounds); @@ -1308,12 +1312,10 @@ public void TestColumnStyle_AllColumnsVisibleFalse_BehavesAsTableNull () { var tableView = GetABCDEFTableView (out DataTable dt); - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false; - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false; - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["C"]).Visible = false; - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false; - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false; - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false; + for (int i = 0; i < 6; i++) { + tableView.Style.GetOrCreateColumnStyle (i).Visible = false; + } + tableView.LayoutSubviews (); // expect nothing to be rendered when all columns are invisible @@ -1351,9 +1353,9 @@ public void TestColumnStyle_RemainingColumnsInvisible_NoScrollIndicator () TestHelpers.AssertDriverContentsAre (expected, output); // but if DEF are invisible we shouldn't be showing the indicator - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false; - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false; - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (3).Visible = false; + tableView.Style.GetOrCreateColumnStyle (4).Visible = false; + tableView.Style.GetOrCreateColumnStyle (5).Visible = false; expected = @" @@ -1386,8 +1388,8 @@ public void TestColumnStyle_PreceedingColumnsInvisible_NoScrollIndicator () TestHelpers.AssertDriverContentsAre (expected, output); // but if E and F are invisible so we shouldn't show right - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false; - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (4).Visible = false; + tableView.Style.GetOrCreateColumnStyle (5).Visible = false; expected = @" @@ -1398,7 +1400,7 @@ public void TestColumnStyle_PreceedingColumnsInvisible_NoScrollIndicator () TestHelpers.AssertDriverContentsAre (expected, output); // now also A is invisible so we cannot scroll in either direction - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (0).Visible = false; expected = @" @@ -1414,7 +1416,7 @@ public void TestColumnStyle_VisibleFalse_CursorStepsOverInvisibleColumns () var tableView = GetABCDEFTableView (out var dt); tableView.LayoutSubviews (); - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (1).Visible = false; tableView.SelectedColumn = 0; tableView.ProcessKey (new KeyEvent { Key = Key.CursorRight }); @@ -1436,7 +1438,7 @@ public void TestColumnStyle_FirstColumnVisibleFalse_CursorStaysAt1 (bool useHome var tableView = GetABCDEFTableView (out var dt); tableView.LayoutSubviews (); - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (0).Visible = false; tableView.SelectedColumn = 0; Assert.Equal (0, tableView.SelectedColumn); @@ -1505,9 +1507,9 @@ public void TestColumnStyle_LastColumnVisibleFalse_CursorStaysAt2 (bool useEnd) tableView.SelectedColumn = 3; Assert.Equal (3, tableView.SelectedColumn); - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false; - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false; - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (3).Visible = false; + tableView.Style.GetOrCreateColumnStyle (4).Visible = false; + tableView.Style.GetOrCreateColumnStyle (5).Visible = false; // column D is invisible so this method should move to 2 (C) tableView.EnsureValidSelection (); @@ -1541,7 +1543,7 @@ public void TestColumnStyle_VisibleFalse_MultiSelected () Assert.False (tableView.IsSelected (3, 0)); // if middle column is invisible - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (1).Visible = false; // it should not be included in the selection Assert.Equal (2, tableView.GetAllSelectedCells ().Count ()); @@ -1556,11 +1558,11 @@ public void TestColumnStyle_VisibleFalse_MultiSelected () [Fact, AutoInitShutdown] public void TestColumnStyle_VisibleFalse_MultiSelectingStepsOverInvisibleColumns () { - var tableView = GetABCDEFTableView (out var dt); + var tableView = GetABCDEFTableView (out _); tableView.LayoutSubviews (); // if middle column is invisible - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (1).Visible = false; tableView.ProcessKey (new KeyEvent { Key = Key.CursorRight | Key.ShiftMask }); @@ -1764,7 +1766,7 @@ public void TestColumnStyle_VisibleFalse_DoesNotEffect_EnsureSelectedCellIsVisib tableView.Style.SmoothHorizontalScrolling = smooth; if (invisibleCol) { - tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (3).Visible = false; } // New TableView should have first cell selected @@ -1812,7 +1814,7 @@ public void LongColumnTest () dt.Rows.Add (1, 2, new string ('a', 500)); dt.Rows.Add (1, 2, "aaa"); - tableView.Table = dt; + tableView.Table = new DataTableSource(dt); tableView.LayoutSubviews (); tableView.Redraw (tableView.Bounds); @@ -1829,7 +1831,7 @@ public void LongColumnTest () TestHelpers.AssertDriverContentsAre (expected, output); // get a style for the long column - var style = tableView.Style.GetOrCreateColumnStyle (dt.Columns [2]); + var style = tableView.Style.GetOrCreateColumnStyle (2); // one way the API user can fix this for long columns // is to specify a MinAcceptableWidth for the column @@ -1955,7 +1957,7 @@ public void ScrollIndicators () dt.Rows.Add (1, 2, 3, 4, 5, 6); - tableView.Table = dt; + tableView.Table = new DataTableSource(dt); // select last visible column tableView.SelectedColumn = 2; // column C @@ -2020,7 +2022,7 @@ public void CellEventsBackgroundFill () dt.Rows.Add ("Hello", DBNull.Value, "f"); - tv.Table = dt; + tv.Table = new DataTableSource(dt); tv.NullSymbol = string.Empty; Application.Top.Add (tv); @@ -2051,8 +2053,9 @@ public void CellEventsBackgroundFill () // Now the thing we really want to test is the styles! // All cells in the column have a column style that says // the cell is pink! - foreach (DataColumn col in dt.Columns) { - var style = tv.Style.GetOrCreateColumnStyle (col); + for (int i = 0; i < dt.Columns.Count; i++) { + + var style = tv.Style.GetOrCreateColumnStyle (i); style.ColorGetter = (e) => { return scheme; }; @@ -2135,12 +2138,13 @@ 1 2 3 [Fact, AutoInitShutdown] public void TestFullRowSelect_SelectionColorStopsAtTableEdge_WithCellLines () { - var tv = GetTwoRowSixColumnTable (); - tv.Table.Rows.Add (1, 2, 3, 4, 5, 6); - tv.LayoutSubviews (); - + var tv = GetTwoRowSixColumnTable (out var dt); + dt.Rows.Add (1, 2, 3, 4, 5, 6); tv.Bounds = new Rect (0, 0, 7, 6); + tv.Frame = new Rect (0, 0, 7, 6); + tv.LayoutSubviews (); + tv.FullRowSelect = true; tv.Style.ShowHorizontalBottomline = true; @@ -2192,12 +2196,12 @@ public void TestFullRowSelect_SelectionColorStopsAtTableEdge_WithCellLines () [Fact, AutoInitShutdown] public void TestFullRowSelect_AlwaysUseNormalColorForVerticalCellLines () { - var tv = GetTwoRowSixColumnTable (); - tv.Table.Rows.Add (1, 2, 3, 4, 5, 6); - tv.LayoutSubviews (); - + var tv = GetTwoRowSixColumnTable (out var dt); + dt.Rows.Add (1, 2, 3, 4, 5, 6); tv.Bounds = new Rect (0, 0, 7, 6); + tv.Frame = new Rect (0, 0, 7, 6); + tv.LayoutSubviews (); tv.FullRowSelect = true; tv.Style.ShowHorizontalBottomline = true; @@ -2248,8 +2252,8 @@ public void TestFullRowSelect_AlwaysUseNormalColorForVerticalCellLines () [Fact, AutoInitShutdown] public void TestFullRowSelect_SelectionColorDoesNotStop_WhenShowVerticalCellLinesIsFalse () { - var tv = GetTwoRowSixColumnTable (); - tv.Table.Rows.Add (1, 2, 3, 4, 5, 6); + var tv = GetTwoRowSixColumnTable (out var dt); + dt.Rows.Add (1, 2, 3, 4, 5, 6); tv.LayoutSubviews (); @@ -2301,15 +2305,20 @@ 1 2 3 TestHelpers.AssertDriverColorsAre (expected, normal, focus); } + public static DataTableSource BuildTable (int cols, int rows) + { + return BuildTable (cols, rows, out _); + } + /// /// Builds a simple table of string columns with the requested number of columns and rows /// /// /// /// - public static DataTable BuildTable (int cols, int rows) + public static DataTableSource BuildTable (int cols, int rows, out DataTable dt) { - var dt = new DataTable (); + dt = new DataTable (); for (int c = 0; c < cols; c++) { dt.Columns.Add ("Col" + c); @@ -2325,7 +2334,7 @@ public static DataTable BuildTable (int cols, int rows) dt.Rows.Add (newRow); } - return dt; + return new DataTableSource(dt); } [Fact, AutoInitShutdown] @@ -2412,7 +2421,7 @@ public void Test_ScreenToCell_DataColumnOverload () │1│2│3│"; TestHelpers.AssertDriverContentsAre (expected, output); - DataColumn col; + int? col; // ---------------- X=0 ----------------------- // click is before first cell @@ -2430,10 +2439,10 @@ public void Test_ScreenToCell_DataColumnOverload () // ---------------- X=1 ----------------------- // click in header Assert.Null (tableView.ScreenToCell (1, 0, out col)); - Assert.Equal ("A", col.ColumnName); + Assert.Equal ("A", tableView.Table.ColumnNames[col.Value]); // click in header row line (click in the horizontal line below header counts as click in header above - consistent with the column hit box) Assert.Null (tableView.ScreenToCell (1, 1, out col)); - Assert.Equal ("A", col.ColumnName); + Assert.Equal ("A", tableView.Table.ColumnNames[col.Value]); // click in cell 0,0 Assert.Equal (new Point (0, 0), tableView.ScreenToCell (1, 2, out col)); Assert.Null (col); @@ -2447,10 +2456,10 @@ public void Test_ScreenToCell_DataColumnOverload () // ---------------- X=2 ----------------------- // click in header Assert.Null (tableView.ScreenToCell (2, 0, out col)); - Assert.Equal ("A", col.ColumnName); + Assert.Equal ("A", tableView.Table.ColumnNames[col.Value]); // click in header row line Assert.Null (tableView.ScreenToCell (2, 1, out col)); - Assert.Equal ("A", col.ColumnName); + Assert.Equal ("A", tableView.Table.ColumnNames [col.Value]); // click in cell 0,0 Assert.Equal (new Point (0, 0), tableView.ScreenToCell (2, 2, out col)); Assert.Null (col); @@ -2464,10 +2473,10 @@ public void Test_ScreenToCell_DataColumnOverload () // ---------------- X=3 ----------------------- // click in header Assert.Null (tableView.ScreenToCell (3, 0, out col)); - Assert.Equal ("B", col.ColumnName); + Assert.Equal ("B", tableView.Table.ColumnNames [col.Value]); // click in header row line Assert.Null (tableView.ScreenToCell (3, 1, out col)); - Assert.Equal ("B", col.ColumnName); + Assert.Equal ("B", tableView.Table.ColumnNames[col.Value]); // click in cell 1,0 Assert.Equal (new Point (1, 0), tableView.ScreenToCell (3, 2, out col)); Assert.Null (col); @@ -2478,7 +2487,42 @@ public void Test_ScreenToCell_DataColumnOverload () Assert.Null (tableView.ScreenToCell (3, 4, out col)); Assert.Null (col); } + + [Fact,AutoInitShutdown] + public void TestEnumerableDataSource_BasicTypes() + { + var tv = new TableView (); + tv.ColorScheme = Colors.TopLevel; + tv.Bounds = new Rect (0, 0, 50, 6); + + tv.Table = new EnumerableTableSource ( + new Type [] { typeof (string), typeof (int), typeof (float) }, + new () { + { "Name", (t)=>t.Name}, + { "Namespace", (t)=>t.Namespace}, + { "BaseType", (t)=>t.BaseType} + }); + + tv.LayoutSubviews (); + + tv.Redraw (tv.Bounds); + + string expected = + @" +┌──────┬─────────┬───────────────────────────────┐ +│Name │Namespace│BaseType │ +├──────┼─────────┼───────────────────────────────┤ +│String│System │System.Object │ +│Int32 │System │System.ValueType │ +│Single│System │System.ValueType │"; + + TestHelpers.AssertDriverContentsAre (expected, output); + } private TableView GetTwoRowSixColumnTable () + { + return GetTwoRowSixColumnTable (out _); + } + private TableView GetTwoRowSixColumnTable (out DataTable dt) { var tableView = new TableView (); tableView.ColorScheme = Colors.TopLevel; @@ -2490,7 +2534,7 @@ private TableView GetTwoRowSixColumnTable () tableView.Style.AlwaysShowHeaders = true; tableView.Style.SmoothHorizontalScrolling = true; - var dt = new DataTable (); + dt = new DataTable (); dt.Columns.Add ("A"); dt.Columns.Add ("B"); dt.Columns.Add ("C"); @@ -2501,7 +2545,7 @@ private TableView GetTwoRowSixColumnTable () dt.Rows.Add (1, 2, 3, 4, 5, 6); dt.Rows.Add (1, 2, 3, 4, 5, 6); - tableView.Table = dt; + tableView.Table = new DataTableSource(dt); return tableView; } } diff --git a/docfx/articles/tableview.md b/docfx/articles/tableview.md index 8ba6c9e583..39aa98e1cc 100644 --- a/docfx/articles/tableview.md +++ b/docfx/articles/tableview.md @@ -52,13 +52,30 @@ tableView = new TableView () { Height = 10, }; -tableView.Table = yourDataTable; +tableView.Table = new DataTableSource(yourDataTable); +``` + +## Object data +If your data objects are not stored in a `System.Data.DataTable` then you can instead +create a table using `EnumerableTableSource` or implementing your own `ITableSource` +class. + +For example to render data for the currently running processes: + +```csharp +tableView.Table = new EnumerableTableDataSource (Process.GetProcesses (), + new Dictionary>() { + { "ID",(p)=>p.Id}, + { "Name",(p)=>p.ProcessName}, + { "Threads",(p)=>p.Threads.Count}, + { "Virtual Memory",(p)=>p.VirtualMemorySize64}, + { "Working Memory",(p)=>p.WorkingSet64}, + }); ``` ## Table Rendering -TableView supports any size of table (limited only by the RAM requirements of `System.DataTable`). You can have -thousands of columns and/or millions of rows if you want. Horizontal and vertical scrolling can be done using -the mouse or keyboard. +TableView supports any size of table. You can have thousands of columns and/or millions of rows if you want. +Horizontal and vertical scrolling can be done using the mouse or keyboard. TableView uses `ColumnOffset` and `RowOffset` to determine the first visible cell of the `System.DataTable`. Rendering then continues until the avaialble console space is exhausted. Updating the `ColumnOffset` and