From 474dfbb579754fd6c55804476f91e98167d00044 Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 19 Nov 2020 12:33:14 +0000 Subject: [PATCH 01/47] Added basic table viewing --- Terminal.Gui/Views/TableView.cs | 190 +++++++++++++++++++++++++++++ UICatalog/Scenarios/TableEditor.cs | 78 ++++++++++++ 2 files changed, 268 insertions(+) create mode 100644 Terminal.Gui/Views/TableView.cs create mode 100644 UICatalog/Scenarios/TableEditor.cs diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs new file mode 100644 index 000000000..3bf49df02 --- /dev/null +++ b/Terminal.Gui/Views/TableView.cs @@ -0,0 +1,190 @@ +ο»Ώusing System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Terminal.Gui.Views { + + /// + /// View for tabular data based on a + /// + public class TableView : View { + + private int columnOffset; + private int rowOffset; + + public DataTable Table { get; private set; } + + /// + /// Zero indexed offset for the upper left to display in . + /// + /// This property allows very wide tables to be rendered with horizontal scrolling + public int ColumnOffset { + get => columnOffset; + + //try to prevent this being set to an out of bounds column + set => columnOffset = Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); + } + + + /// + /// Zero indexed offset for the to display in on line 2 of the control (first line being headers) + /// + /// This property allows very wide tables to be rendered with horizontal scrolling + public int RowOffset { + get => rowOffset; + set => rowOffset = Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); + } + + /// + /// The maximum number of characters to render in any given column. This prevents one long column from pushing out all the others + /// + public int MaximumCellWidth {get;set;} = 100; + + /// + /// The text representation that should be rendered for cells with the value + /// + public string NullSymbol {get;set;} = "-"; + + /// + /// Initialzies a class using layout. + /// + /// The table to display in the control + public TableView (DataTable table) : base () + { + this.Table = table ?? throw new ArgumentNullException (nameof (table)); + } + /// + public override void Redraw (Rect bounds) + { + Attribute currentAttribute; + var current = ColorScheme.Focus; + Driver.SetAttribute (current); + Move (0, 0); + + var frame = Frame; + + int activeColor = ColorScheme.HotNormal; + int trackingColor = ColorScheme.HotFocus; + + // What columns to render at what X offset in viewport + Dictionary columnsToRender = CalculateViewport(bounds); + + Driver.SetAttribute (ColorScheme.HotNormal); + + // Render the headers + foreach(var kvp in columnsToRender) { + + Move (kvp.Value,0); + Driver.AddStr(kvp.Key.ColumnName); + } + + //render the cells + for (int line = 1; line < frame.Height; line++) { + + //work out what Row to render + var rowToRender = RowOffset + (line-1); + if(rowToRender >= Table.Rows.Count) + break; + + foreach(var kvp in columnsToRender) { + Move (kvp.Value,line); + Driver.AddStr(GetRenderedVal(Table.Rows[rowToRender][kvp.Key])); + } + } + + /* + + for (int line = 1; line < frame.Height; line++) { + var lineRect = new Rect (0, line, frame.Width, 1); + if (!bounds.Contains (lineRect)) + continue; + + Move (0, line); + Driver.SetAttribute (ColorScheme.HotNormal); + Driver.AddStr ("test"); + + currentAttribute = ColorScheme.HotNormal; + SetAttribute (ColorScheme.Normal); + }*/ + + void SetAttribute (Attribute attribute) + { + if (currentAttribute != attribute) { + currentAttribute = attribute; + Driver.SetAttribute (attribute); + } + } + + } + + /// + /// Calculates which columns should be rendered given the in which to display and the + /// + /// + /// + /// + private Dictionary CalculateViewport(Rect bounds, int padding = 1) + { + Dictionary toReturn = new Dictionary(); + + int usedSpace = 0; + int availableHorizontalSpace = bounds.Width; + int rowsToRender = bounds.Height-1; //1 reserved for the headers row + + foreach(var col in Table.Columns.Cast().Skip(ColumnOffset)) { + + toReturn.Add(col,usedSpace); + usedSpace += CalculateMaxRowSize(col,rowsToRender) + padding; + + if(usedSpace > availableHorizontalSpace) + return toReturn; + + } + + return toReturn; + } + + /// + /// Returns the maximum of the name and the maximum length of data that will be rendered starting at and rendering + /// + /// + /// + /// + private int CalculateMaxRowSize (DataColumn col, int rowsToRender) + { + int spaceRequired = col.ColumnName.Length; + + for(int i = RowOffset; i + /// Returns the value that should be rendered to best represent a strongly typed read from + /// + /// + /// + private string GetRenderedVal (object value) + { + if(value == null || value == DBNull.Value) + { + return NullSymbol; + } + + var representation = value.ToString(); + + //if it is too long to fit + if(representation.Length > MaximumCellWidth) + return representation.Substring(0,MaximumCellWidth); + + return representation; + } + } +} diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs new file mode 100644 index 000000000..2d76fb60e --- /dev/null +++ b/UICatalog/Scenarios/TableEditor.cs @@ -0,0 +1,78 @@ +ο»Ώusing System; +using System.Collections.Generic; +using System.Data; +using Terminal.Gui; +using Terminal.Gui.Views; + +namespace UICatalog.Scenarios { + + [ScenarioMetadata (Name: "TableEditor", Description: "A Terminal.Gui DataTable editor via TableView")] + [ScenarioCategory ("Controls")] + [ScenarioCategory ("Dialogs")] + [ScenarioCategory ("Text")] + [ScenarioCategory ("Dialogs")] + [ScenarioCategory ("TopLevel")] + public class TableEditor : Scenario + { + TableView tableView; + + public override void Setup () + { + var dt = BuildDemoDataTable(30,1000); + + Win.Title = this.GetName() + "-" + dt.TableName ?? "Untitled"; + Win.Y = 1; // menu + Win.Height = Dim.Fill (1); // status bar + Top.LayoutSubviews (); + + this.tableView = new TableView (dt) { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill (), + }; + tableView.CanFocus = true; + Win.Add (tableView); + } + + /// + /// Generates a new demo with the given number of (min 4) and + /// + /// + /// + /// + public static DataTable BuildDemoDataTable(int cols, int rows) + { + var dt = new DataTable(); + + dt.Columns.Add(new DataColumn("StrCol",typeof(string))); + dt.Columns.Add(new DataColumn("DateCol",typeof(DateTime))); + dt.Columns.Add(new DataColumn("IntCol",typeof(int))); + dt.Columns.Add(new DataColumn("DoubleCol",typeof(double))); + + for(int i=0;i< cols -4; i++) { + dt.Columns.Add("Column" + (i+4)); + } + + var r = new Random(100); + + for(int i=0;i< rows;i++) { + + List row = new List(){ + "Some long text with unicode 'πŸ˜€'", + new DateTime(2000+i,12,25), + r.Next(i), + r.NextDouble()*i + }; + + for(int j=0;j< cols -4; j++) { + row.Add("SomeValue" + r.Next(100)); + } + + dt.Rows.Add(row.ToArray()); + } + + return dt; + } + } +} From dcb020ab147bd14f08c40a8312eb184e2168c6e8 Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 19 Nov 2020 13:09:08 +0000 Subject: [PATCH 02/47] Added keyboard navigation and fixed layout/rendering issues --- Terminal.Gui/Views/TableView.cs | 114 ++++++++++++++++++++++------- UICatalog/Scenarios/TableEditor.cs | 10 ++- 2 files changed, 94 insertions(+), 30 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 3bf49df02..7040e51e2 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Data; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Terminal.Gui.Views { @@ -22,10 +20,21 @@ namespace Terminal.Gui.Views { /// /// This property allows very wide tables to be rendered with horizontal scrolling public int ColumnOffset { - get => columnOffset; + get { + return columnOffset; + } //try to prevent this being set to an out of bounds column - set => columnOffset = Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); + set { + //the value before we changed it + var origValue = columnOffset; + + columnOffset = Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); + + //if value actually changed we must update UI + if(columnOffset != origValue) + SetNeedsDisplay(); + } } @@ -34,9 +43,20 @@ namespace Terminal.Gui.Views { /// /// This property allows very wide tables to be rendered with horizontal scrolling public int RowOffset { - get => rowOffset; - set => rowOffset = Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); + get { + return rowOffset; } + set { + //the value before we changed it + var origValue = rowOffset; + + rowOffset = Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); + + //if value actually changed we must update UI + if(rowOffset != origValue) + SetNeedsDisplay(); + } + } /// /// The maximum number of characters to render in any given column. This prevents one long column from pushing out all the others @@ -48,6 +68,11 @@ namespace Terminal.Gui.Views { /// public string NullSymbol {get;set;} = "-"; + /// + /// The symbol to add after each cell value and header value to visually seperate values + /// + public char SeparatorSymbol {get;set; } = ' '; + /// /// Initialzies a class using layout. /// @@ -74,42 +99,36 @@ namespace Terminal.Gui.Views { Driver.SetAttribute (ColorScheme.HotNormal); + //invalidate current row (prevents scrolling around leaving old characters in the frame + Driver.AddStr(new string (' ',bounds.Width)); + // Render the headers foreach(var kvp in columnsToRender) { Move (kvp.Value,0); - Driver.AddStr(kvp.Key.ColumnName); + Driver.AddStr(kvp.Key.ColumnName+ SeparatorSymbol); } //render the cells for (int line = 1; line < frame.Height; line++) { + //invalidate current row (prevents scrolling around leaving old characters in the frame + Move (0,line); + Driver.AddStr(new string (' ',bounds.Width)); + //work out what Row to render var rowToRender = RowOffset + (line-1); + + //if we have run off the end of the table if(rowToRender >= Table.Rows.Count) - break; + continue; foreach(var kvp in columnsToRender) { Move (kvp.Value,line); - Driver.AddStr(GetRenderedVal(Table.Rows[rowToRender][kvp.Key])); + Driver.AddStr(GetRenderedVal(Table.Rows[rowToRender][kvp.Key]) + SeparatorSymbol); } } - /* - - for (int line = 1; line < frame.Height; line++) { - var lineRect = new Rect (0, line, frame.Width, 1); - if (!bounds.Contains (lineRect)) - continue; - - Move (0, line); - Driver.SetAttribute (ColorScheme.HotNormal); - Driver.AddStr ("test"); - - currentAttribute = ColorScheme.HotNormal; - SetAttribute (ColorScheme.Normal); - }*/ - void SetAttribute (Attribute attribute) { if (currentAttribute != attribute) { @@ -119,7 +138,50 @@ namespace Terminal.Gui.Views { } } - + + /// + public override bool ProcessKey (KeyEvent keyEvent) + { + switch (keyEvent.Key) { + case Key.CursorLeft: + ColumnOffset--; + break; + case Key.CursorRight: + ColumnOffset++; + break; + case Key.CursorDown: + RowOffset++; + break; + case Key.CursorUp: + RowOffset--; + break; + case Key.PageUp: + RowOffset -= Frame.Height; + break; + case Key.V | Key.CtrlMask: + case Key.PageDown: + RowOffset += Frame.Height; + break; + case Key.Home | Key.CtrlMask: + RowOffset = 0; + ColumnOffset = 0; + break; + case Key.Home: + ColumnOffset = 0; + break; + case Key.End | Key.CtrlMask: + //jump to end of table + RowOffset = Table.Rows.Count-1; + ColumnOffset = Table.Columns.Count-1; + break; + case Key.End: + //jump to end of row + ColumnOffset = Table.Columns.Count-1; + break; + } + PositionCursor (); + return true; + } /// /// Calculates which columns should be rendered given the in which to display and the /// @@ -157,7 +219,7 @@ namespace Terminal.Gui.Views { { int spaceRequired = col.ColumnName.Length; - for(int i = RowOffset; i - /// Generates a new demo with the given number of (min 4) and + /// Generates a new demo with the given number of (min 5) and /// /// /// @@ -49,8 +49,9 @@ namespace UICatalog.Scenarios { dt.Columns.Add(new DataColumn("DateCol",typeof(DateTime))); dt.Columns.Add(new DataColumn("IntCol",typeof(int))); dt.Columns.Add(new DataColumn("DoubleCol",typeof(double))); + dt.Columns.Add(new DataColumn("NullsCol",typeof(string))); - for(int i=0;i< cols -4; i++) { + for(int i=0;i< cols -5; i++) { dt.Columns.Add("Column" + (i+4)); } @@ -62,10 +63,11 @@ namespace UICatalog.Scenarios { "Some long text with unicode 'πŸ˜€'", new DateTime(2000+i,12,25), r.Next(i), - r.NextDouble()*i + r.NextDouble()*i, + DBNull.Value }; - for(int j=0;j< cols -4; j++) { + for(int j=0;j< cols -5; j++) { row.Add("SomeValue" + r.Next(100)); } From bfefc724dfacb7195ec6e7dc953960de00ec434e Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 19 Nov 2020 13:50:23 +0000 Subject: [PATCH 03/47] Added selected cell properties --- Terminal.Gui/Views/TableView.cs | 148 ++++++++++++++++++++++---------- 1 file changed, 101 insertions(+), 47 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 7040e51e2..f2c88bf60 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -1,4 +1,5 @@ -ο»Ώusing System; +ο»Ώusing NStack; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -12,6 +13,8 @@ namespace Terminal.Gui.Views { private int columnOffset; private int rowOffset; + private int selectedRow; + private int selectedColumn; public DataTable Table { get; private set; } @@ -20,42 +23,37 @@ namespace Terminal.Gui.Views { /// /// This property allows very wide tables to be rendered with horizontal scrolling public int ColumnOffset { - get { - return columnOffset; - } + get => columnOffset; //try to prevent this being set to an out of bounds column - set { - //the value before we changed it - var origValue = columnOffset; - - columnOffset = Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); - - //if value actually changed we must update UI - if(columnOffset != origValue) - SetNeedsDisplay(); - } + set => columnOffset = Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); } - /// /// Zero indexed offset for the to display in on line 2 of the control (first line being headers) /// /// This property allows very wide tables to be rendered with horizontal scrolling public int RowOffset { - get { - return rowOffset; - } - set { - //the value before we changed it - var origValue = rowOffset; + get => rowOffset; + set => rowOffset = Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); + } - rowOffset = Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); + /// + /// The index of in that the user has currently selected + /// + public int SelectedColumn { + get => selectedColumn; - //if value actually changed we must update UI - if(rowOffset != origValue) - SetNeedsDisplay(); - } + //try to prevent this being set to an out of bounds column + set => selectedColumn = Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); + } + + /// + /// The index of in that the user has currently selected + /// + public int SelectedRow { + get => selectedRow; + set => selectedRow = Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); } /// @@ -91,22 +89,19 @@ namespace Terminal.Gui.Views { var frame = Frame; - int activeColor = ColorScheme.HotNormal; - int trackingColor = ColorScheme.HotFocus; - // What columns to render at what X offset in viewport Dictionary columnsToRender = CalculateViewport(bounds); - Driver.SetAttribute (ColorScheme.HotNormal); + Driver.SetAttribute (ColorScheme.Normal); //invalidate current row (prevents scrolling around leaving old characters in the frame Driver.AddStr(new string (' ',bounds.Width)); - + // Render the headers foreach(var kvp in columnsToRender) { Move (kvp.Value,0); - Driver.AddStr(kvp.Key.ColumnName+ SeparatorSymbol); + Driver.AddStr(Truncate(kvp.Key.ColumnName+ SeparatorSymbol,bounds.Width - kvp.Value)); } //render the cells @@ -114,6 +109,7 @@ namespace Terminal.Gui.Views { //invalidate current row (prevents scrolling around leaving old characters in the frame Move (0,line); + Driver.SetAttribute(ColorScheme.Normal); Driver.AddStr(new string (' ',bounds.Width)); //work out what Row to render @@ -125,7 +121,14 @@ namespace Terminal.Gui.Views { foreach(var kvp in columnsToRender) { Move (kvp.Value,line); - Driver.AddStr(GetRenderedVal(Table.Rows[rowToRender][kvp.Key]) + SeparatorSymbol); + + bool isSelectedCell = rowToRender == SelectedRow && kvp.Key.Ordinal == SelectedColumn; + + Driver.SetAttribute(isSelectedCell? ColorScheme.HotFocus: ColorScheme.Normal); + + + var valueToRender = GetRenderedVal(Table.Rows[rowToRender][kvp.Key]) + SeparatorSymbol; + Driver.AddStr(Truncate(valueToRender,bounds.Width - kvp.Value )); } } @@ -138,50 +141,101 @@ namespace Terminal.Gui.Views { } } - + + private ustring Truncate (string valueToRender, int availableHorizontalSpace) + { + if(string.IsNullOrEmpty(valueToRender) || valueToRender.Length < availableHorizontalSpace) + return valueToRender; + + return valueToRender.Substring(0,availableHorizontalSpace); + } + /// public override bool ProcessKey (KeyEvent keyEvent) { switch (keyEvent.Key) { case Key.CursorLeft: - ColumnOffset--; + SelectedColumn--; + RefreshViewport(); break; case Key.CursorRight: - ColumnOffset++; + SelectedColumn++; + RefreshViewport(); break; case Key.CursorDown: - RowOffset++; + SelectedRow++; + RefreshViewport(); break; case Key.CursorUp: - RowOffset--; + SelectedRow--; + RefreshViewport(); break; case Key.PageUp: - RowOffset -= Frame.Height; + SelectedRow -= Frame.Height; + RefreshViewport(); break; - case Key.V | Key.CtrlMask: case Key.PageDown: - RowOffset += Frame.Height; + SelectedRow += Frame.Height; + RefreshViewport(); break; case Key.Home | Key.CtrlMask: - RowOffset = 0; - ColumnOffset = 0; + SelectedRow = 0; + SelectedColumn = 0; + RefreshViewport(); break; case Key.Home: - ColumnOffset = 0; + SelectedColumn = 0; + RefreshViewport(); break; case Key.End | Key.CtrlMask: //jump to end of table - RowOffset = Table.Rows.Count-1; - ColumnOffset = Table.Columns.Count-1; + SelectedRow = Table.Rows.Count-1; + SelectedColumn = Table.Columns.Count-1; + RefreshViewport(); break; case Key.End: //jump to end of row - ColumnOffset = Table.Columns.Count-1; + SelectedColumn = Table.Columns.Count-1; + RefreshViewport(); break; } PositionCursor (); return true; } + + /// + /// Updates the viewport ( / ) to ensure that the users selected cell is visible and redraws control + /// + /// This always calls + public void RefreshViewport () + { + //TODO: implement + + Dictionary columnsToRender = CalculateViewport(Bounds); + + + //if we have scrolled too far to the left + if(SelectedColumn < columnsToRender.Keys.Min(col=>col.Ordinal)) { + ColumnOffset = SelectedColumn; + } + + //if we have scrolled too far to the right + if(SelectedColumn > columnsToRender.Keys.Max(col=>col.Ordinal)) { + ColumnOffset = SelectedColumn; + } + + //if we have scrolled too far down + if(SelectedRow > RowOffset + Bounds.Height-1) { + RowOffset = SelectedRow; + } + //if we have scrolled too far up + if(SelectedRow < RowOffset) { + RowOffset = SelectedRow; + } + + SetNeedsDisplay(); + } + /// /// Calculates which columns should be rendered given the in which to display and the /// From 6744e0604114ff39ab92bbb84ba6985f13e37036 Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 19 Nov 2020 14:11:18 +0000 Subject: [PATCH 04/47] Added edit cell values into the example in UICatalog --- UICatalog/Scenarios/TableEditor.cs | 56 +++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 1c9c23a58..fe5196a30 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -32,9 +32,63 @@ namespace UICatalog.Scenarios { Height = Dim.Fill (), }; tableView.CanFocus = true; + + tableView.KeyPress += KeyPressed; + Win.Add (tableView); } + private void KeyPressed (View.KeyEventEventArgs obj) + { + if(obj.KeyEvent.Key == Key.Enter) { + EditCurrentCell(); + } + + } + + private void EditCurrentCell () + { + var oldValue = tableView.Table.Rows[tableView.SelectedRow][tableView.SelectedColumn].ToString(); + bool okPressed = false; + + var ok = new Button ("Ok", is_default: true); + ok.Clicked += () => { okPressed = true; Application.RequestStop (); }; + var cancel = new Button ("Cancel"); + cancel.Clicked += () => { Application.RequestStop (); }; + var d = new Dialog ("Enter new value", 60, 20, ok, cancel); + + var lbl = new Label() { + X = 0, + Y = 1, + Text = tableView.Table.Columns[tableView.SelectedColumn].ColumnName + }; + + var tf = new TextField() + { + Text = oldValue, + X = 0, + Y = 2, + Width = Dim.Fill() + }; + + d.Add (lbl,tf); + tf.SetFocus(); + + Application.Run (d); + + if(okPressed) { + + try { + tableView.Table.Rows[tableView.SelectedRow][tableView.SelectedColumn] = tf.Text; + } + catch(Exception ex) { + MessageBox.ErrorQuery(60,20,"Failed to set text", ex.Message,"Ok"); + } + + tableView.RefreshViewport(); + } + } + /// /// Generates a new demo with the given number of (min 5) and /// @@ -60,7 +114,7 @@ namespace UICatalog.Scenarios { for(int i=0;i< rows;i++) { List row = new List(){ - "Some long text with unicode 'πŸ˜€'", + "Some long text that is super cool", new DateTime(2000+i,12,25), r.Next(i), r.NextDouble()*i, From 85f0e9667c8d05f2b0eb946cc47e656810ca1522 Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 19 Nov 2020 14:12:45 +0000 Subject: [PATCH 05/47] Support for setting the value to null in example --- UICatalog/Scenarios/TableEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index fe5196a30..1d346fb75 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -79,7 +79,7 @@ namespace UICatalog.Scenarios { if(okPressed) { try { - tableView.Table.Rows[tableView.SelectedRow][tableView.SelectedColumn] = tf.Text; + tableView.Table.Rows[tableView.SelectedRow][tableView.SelectedColumn] = string.IsNullOrWhiteSpace(tf.Text.ToString()) ? DBNull.Value : (object)tf.Text; } catch(Exception ex) { MessageBox.ErrorQuery(60,20,"Failed to set text", ex.Message,"Ok"); From cdbc37ca90a094589ccbc6d5609889f51b35d219 Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 19 Nov 2020 15:08:50 +0000 Subject: [PATCH 06/47] Standardisation (blank constructor, menu in example etc) - Added blank constructor (Table is now optional and can be null, in which case control will be blank) - Moved edit to be an F key and follow pattern of open/close seen in HexEditor --- Terminal.Gui/Views/TableView.cs | 165 ++++++++++++++++------------- UICatalog/Scenarios/TableEditor.cs | 47 ++++++-- 2 files changed, 125 insertions(+), 87 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index f2c88bf60..7ea5b7de0 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -15,8 +15,9 @@ namespace Terminal.Gui.Views { private int rowOffset; private int selectedRow; private int selectedColumn; + private DataTable table; - public DataTable Table { get; private set; } + public DataTable Table { get => table; set {table = value; Update(); } } /// /// Zero indexed offset for the upper left to display in . @@ -26,16 +27,16 @@ namespace Terminal.Gui.Views { get => columnOffset; //try to prevent this being set to an out of bounds column - set => columnOffset = Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); + set => columnOffset = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); } /// /// Zero indexed offset for the to display in on line 2 of the control (first line being headers) /// /// This property allows very wide tables to be rendered with horizontal scrolling - public int RowOffset { - get => rowOffset; - set => rowOffset = Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); + public int RowOffset { + get => rowOffset; + set => rowOffset = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); } /// @@ -45,31 +46,31 @@ namespace Terminal.Gui.Views { get => selectedColumn; //try to prevent this being set to an out of bounds column - set => selectedColumn = Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); + set => selectedColumn = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); } /// /// The index of in that the user has currently selected /// - public int SelectedRow { - get => selectedRow; - set => selectedRow = Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); + public int SelectedRow { + get => selectedRow; + set => selectedRow = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); } /// /// The maximum number of characters to render in any given column. This prevents one long column from pushing out all the others /// - public int MaximumCellWidth {get;set;} = 100; + public int MaximumCellWidth { get; set; } = 100; /// /// The text representation that should be rendered for cells with the value /// - public string NullSymbol {get;set;} = "-"; + public string NullSymbol { get; set; } = "-"; /// /// The symbol to add after each cell value and header value to visually seperate values /// - public char SeparatorSymbol {get;set; } = ' '; + public char SeparatorSymbol { get; set; } = ' '; /// /// Initialzies a class using layout. @@ -77,8 +78,16 @@ namespace Terminal.Gui.Views { /// The table to display in the control public TableView (DataTable table) : base () { - this.Table = table ?? throw new ArgumentNullException (nameof (table)); + this.Table = table; } + + /// + /// Initialzies a class using layout. Set the property to begin editing + /// + public TableView () : base () + { + } + /// public override void Redraw (Rect bounds) { @@ -90,45 +99,45 @@ namespace Terminal.Gui.Views { var frame = Frame; // What columns to render at what X offset in viewport - Dictionary columnsToRender = CalculateViewport(bounds); + Dictionary columnsToRender = CalculateViewport (bounds); Driver.SetAttribute (ColorScheme.Normal); //invalidate current row (prevents scrolling around leaving old characters in the frame - Driver.AddStr(new string (' ',bounds.Width)); - + Driver.AddStr (new string (' ', bounds.Width)); + // Render the headers - foreach(var kvp in columnsToRender) { - - Move (kvp.Value,0); - Driver.AddStr(Truncate(kvp.Key.ColumnName+ SeparatorSymbol,bounds.Width - kvp.Value)); + foreach (var kvp in columnsToRender) { + + Move (kvp.Value, 0); + Driver.AddStr (Truncate (kvp.Key.ColumnName + SeparatorSymbol, bounds.Width - kvp.Value)); } //render the cells for (int line = 1; line < frame.Height; line++) { - + //invalidate current row (prevents scrolling around leaving old characters in the frame - Move (0,line); - Driver.SetAttribute(ColorScheme.Normal); - Driver.AddStr(new string (' ',bounds.Width)); + Move (0, line); + Driver.SetAttribute (ColorScheme.Normal); + Driver.AddStr (new string (' ', bounds.Width)); //work out what Row to render - var rowToRender = RowOffset + (line-1); + var rowToRender = RowOffset + (line - 1); //if we have run off the end of the table - if(rowToRender >= Table.Rows.Count) + if ( Table == null || rowToRender >= Table.Rows.Count) continue; - foreach(var kvp in columnsToRender) { - Move (kvp.Value,line); + foreach (var kvp in columnsToRender) { + Move (kvp.Value, line); bool isSelectedCell = rowToRender == SelectedRow && kvp.Key.Ordinal == SelectedColumn; - Driver.SetAttribute(isSelectedCell? ColorScheme.HotFocus: ColorScheme.Normal); + Driver.SetAttribute (isSelectedCell ? ColorScheme.HotFocus : ColorScheme.Normal); - - var valueToRender = GetRenderedVal(Table.Rows[rowToRender][kvp.Key]) + SeparatorSymbol; - Driver.AddStr(Truncate(valueToRender,bounds.Width - kvp.Value )); + + var valueToRender = GetRenderedVal (Table.Rows [rowToRender] [kvp.Key]) + SeparatorSymbol; + Driver.AddStr (Truncate (valueToRender, bounds.Width - kvp.Value)); } } @@ -144,10 +153,10 @@ namespace Terminal.Gui.Views { private ustring Truncate (string valueToRender, int availableHorizontalSpace) { - if(string.IsNullOrEmpty(valueToRender) || valueToRender.Length < availableHorizontalSpace) + if (string.IsNullOrEmpty (valueToRender) || valueToRender.Length < availableHorizontalSpace) return valueToRender; - return valueToRender.Substring(0,availableHorizontalSpace); + return valueToRender.Substring (0, availableHorizontalSpace); } /// @@ -156,47 +165,47 @@ namespace Terminal.Gui.Views { switch (keyEvent.Key) { case Key.CursorLeft: SelectedColumn--; - RefreshViewport(); + Update (); break; case Key.CursorRight: SelectedColumn++; - RefreshViewport(); + Update (); break; case Key.CursorDown: SelectedRow++; - RefreshViewport(); + Update (); break; case Key.CursorUp: SelectedRow--; - RefreshViewport(); + Update (); break; case Key.PageUp: SelectedRow -= Frame.Height; - RefreshViewport(); + Update (); break; case Key.PageDown: SelectedRow += Frame.Height; - RefreshViewport(); + Update (); break; case Key.Home | Key.CtrlMask: SelectedRow = 0; SelectedColumn = 0; - RefreshViewport(); + Update (); break; case Key.Home: SelectedColumn = 0; - RefreshViewport(); + Update (); break; case Key.End | Key.CtrlMask: //jump to end of table - SelectedRow = Table.Rows.Count-1; - SelectedColumn = Table.Columns.Count-1; - RefreshViewport(); + SelectedRow = Table == null ? 0 : Table.Rows.Count - 1; + SelectedColumn = Table == null ? 0 : Table.Columns.Count - 1; + Update (); break; case Key.End: //jump to end of row - SelectedColumn = Table.Columns.Count-1; - RefreshViewport(); + SelectedColumn = Table == null ? 0 : Table.Columns.Count - 1; + Update (); break; } PositionCursor (); @@ -204,36 +213,38 @@ namespace Terminal.Gui.Views { } /// - /// Updates the viewport ( / ) to ensure that the users selected cell is visible and redraws control + /// Updates the view to reflect changes to and to ( / ) etc /// /// This always calls - public void RefreshViewport () + public void Update() { - //TODO: implement - - Dictionary columnsToRender = CalculateViewport(Bounds); + if(Table == null) { + SetNeedsDisplay (); + return; + } + Dictionary columnsToRender = CalculateViewport (Bounds); //if we have scrolled too far to the left - if(SelectedColumn < columnsToRender.Keys.Min(col=>col.Ordinal)) { + if (SelectedColumn < columnsToRender.Keys.Min (col => col.Ordinal)) { ColumnOffset = SelectedColumn; } //if we have scrolled too far to the right - if(SelectedColumn > columnsToRender.Keys.Max(col=>col.Ordinal)) { + if (SelectedColumn > columnsToRender.Keys.Max (col => col.Ordinal)) { ColumnOffset = SelectedColumn; } //if we have scrolled too far down - if(SelectedRow > RowOffset + Bounds.Height-1) { + if (SelectedRow > RowOffset + Bounds.Height - 1) { RowOffset = SelectedRow; } //if we have scrolled too far up - if(SelectedRow < RowOffset) { + if (SelectedRow < RowOffset) { RowOffset = SelectedRow; } - SetNeedsDisplay(); + SetNeedsDisplay (); } /// @@ -242,24 +253,27 @@ namespace Terminal.Gui.Views { /// /// /// - private Dictionary CalculateViewport(Rect bounds, int padding = 1) + private Dictionary CalculateViewport (Rect bounds, int padding = 1) { - Dictionary toReturn = new Dictionary(); + Dictionary toReturn = new Dictionary (); + + if(Table == null) + return toReturn; int usedSpace = 0; int availableHorizontalSpace = bounds.Width; - int rowsToRender = bounds.Height-1; //1 reserved for the headers row - - foreach(var col in Table.Columns.Cast().Skip(ColumnOffset)) { - - toReturn.Add(col,usedSpace); - usedSpace += CalculateMaxRowSize(col,rowsToRender) + padding; + int rowsToRender = bounds.Height - 1; //1 reserved for the headers row - if(usedSpace > availableHorizontalSpace) + foreach (var col in Table.Columns.Cast ().Skip (ColumnOffset)) { + + toReturn.Add (col, usedSpace); + usedSpace += CalculateMaxRowSize (col, rowsToRender) + padding; + + if (usedSpace > availableHorizontalSpace) return toReturn; - + } - + return toReturn; } @@ -273,10 +287,10 @@ namespace Terminal.Gui.Views { { int spaceRequired = col.ColumnName.Length; - for(int i = RowOffset; i private string GetRenderedVal (object value) { - if(value == null || value == DBNull.Value) - { + if (value == null || value == DBNull.Value) { return NullSymbol; } - - var representation = value.ToString(); + + var representation = value.ToString (); //if it is too long to fit - if(representation.Length > MaximumCellWidth) - return representation.Substring(0,MaximumCellWidth); + if (representation.Length > MaximumCellWidth) + return representation.Substring (0, MaximumCellWidth); return representation; } diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 1d346fb75..5bae8cd3f 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -18,14 +18,30 @@ namespace UICatalog.Scenarios { public override void Setup () { - var dt = BuildDemoDataTable(30,1000); - - Win.Title = this.GetName() + "-" + dt.TableName ?? "Untitled"; + Win.Title = this.GetName(); Win.Y = 1; // menu Win.Height = Dim.Fill (1); // status bar Top.LayoutSubviews (); - this.tableView = new TableView (dt) { + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_File", new MenuItem [] { + new MenuItem ("_OpenExample", "", () => OpenExample()), + new MenuItem ("_CloseExample", "", () => CloseExample()), + new MenuItem ("_Quit", "", () => Quit()), + }), + }); + Top.Add (menu); + + var statusBar = new StatusBar (new StatusItem [] { + //new StatusItem(Key.Enter, "~ENTER~ ApplyEdits", () => { _hexView.ApplyEdits(); }), + new StatusItem(Key.F2, "~F2~ OpenExample", () => OpenExample()), + new StatusItem(Key.F3, "~F3~ EditCell", () => EditCurrentCell()), + new StatusItem(Key.F4, "~F4~ CloseExample", () => CloseExample()), + new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()), + }); + Top.Add (statusBar); + + this.tableView = new TableView () { X = 0, Y = 0, Width = Dim.Fill (), @@ -33,21 +49,30 @@ namespace UICatalog.Scenarios { }; tableView.CanFocus = true; - tableView.KeyPress += KeyPressed; Win.Add (tableView); } - private void KeyPressed (View.KeyEventEventArgs obj) + private void CloseExample () { - if(obj.KeyEvent.Key == Key.Enter) { - EditCurrentCell(); - } - + tableView.Table = null; + } + + private void Quit () + { + Application.RequestStop (); + } + + private void OpenExample () + { + tableView.Table = BuildDemoDataTable(30,1000); } private void EditCurrentCell () { + if(tableView.Table == null) + return; + var oldValue = tableView.Table.Rows[tableView.SelectedRow][tableView.SelectedColumn].ToString(); bool okPressed = false; @@ -85,7 +110,7 @@ namespace UICatalog.Scenarios { MessageBox.ErrorQuery(60,20,"Failed to set text", ex.Message,"Ok"); } - tableView.RefreshViewport(); + tableView.Update(); } } From 39b7ec4da9e377ef870165ad8730623815785e56 Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 19 Nov 2020 15:33:54 +0000 Subject: [PATCH 07/47] Fixed indexes when closing a a large table and then opening a small table --- Terminal.Gui/Views/TableView.cs | 6 ++++++ UICatalog/Scenarios/TableEditor.cs | 9 +++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 7ea5b7de0..0296e7fb4 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -223,6 +223,12 @@ namespace Terminal.Gui.Views { return; } + //if user opened a large table scrolled down a lot then opened a smaller table (or API deleted a bunch of columns without telling anyone) + ColumnOffset = Math.Max(Math.Min(ColumnOffset,Table.Columns.Count -1),0); + RowOffset = Math.Max(Math.Min(RowOffset,Table.Rows.Count -1),0); + SelectedColumn = Math.Max(Math.Min(SelectedColumn,Table.Columns.Count -1),0); + SelectedRow = Math.Max(Math.Min(SelectedRow,Table.Rows.Count -1),0); + Dictionary columnsToRender = CalculateViewport (Bounds); //if we have scrolled too far to the left diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 5bae8cd3f..d5da0ad55 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -25,7 +25,8 @@ namespace UICatalog.Scenarios { var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { - new MenuItem ("_OpenExample", "", () => OpenExample()), + new MenuItem ("_OpenBigExample", "", () => OpenExample(true)), + new MenuItem ("_OpenSmallExample", "", () => OpenExample(false)), new MenuItem ("_CloseExample", "", () => CloseExample()), new MenuItem ("_Quit", "", () => Quit()), }), @@ -34,7 +35,7 @@ namespace UICatalog.Scenarios { var statusBar = new StatusBar (new StatusItem [] { //new StatusItem(Key.Enter, "~ENTER~ ApplyEdits", () => { _hexView.ApplyEdits(); }), - new StatusItem(Key.F2, "~F2~ OpenExample", () => OpenExample()), + new StatusItem(Key.F2, "~F2~ OpenExample", () => OpenExample(true)), new StatusItem(Key.F3, "~F3~ EditCell", () => EditCurrentCell()), new StatusItem(Key.F4, "~F4~ CloseExample", () => CloseExample()), new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()), @@ -63,9 +64,9 @@ namespace UICatalog.Scenarios { Application.RequestStop (); } - private void OpenExample () + private void OpenExample (bool big) { - tableView.Table = BuildDemoDataTable(30,1000); + tableView.Table = BuildDemoDataTable(big ? 30 : 5, big ? 1000 : 5); } private void EditCurrentCell () From 10d3781c2e2b97254f6bcf007fe1635ecbb96118 Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 19 Nov 2020 16:05:34 +0000 Subject: [PATCH 08/47] Added comments and removed unused variables/method --- Terminal.Gui/Views/TableView.cs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 0296e7fb4..5042727c9 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -17,6 +17,9 @@ namespace Terminal.Gui.Views { private int selectedColumn; private DataTable table; + /// + /// 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(); } } /// @@ -91,11 +94,7 @@ namespace Terminal.Gui.Views { /// public override void Redraw (Rect bounds) { - Attribute currentAttribute; - var current = ColorScheme.Focus; - Driver.SetAttribute (current); Move (0, 0); - var frame = Frame; // What columns to render at what X offset in viewport @@ -141,16 +140,14 @@ namespace Terminal.Gui.Views { } } - void SetAttribute (Attribute attribute) - { - if (currentAttribute != attribute) { - currentAttribute = attribute; - Driver.SetAttribute (attribute); - } - } - } + /// + /// Truncates so that it occupies a maximum of + /// + /// + /// + /// private ustring Truncate (string valueToRender, int availableHorizontalSpace) { if (string.IsNullOrEmpty (valueToRender) || valueToRender.Length < availableHorizontalSpace) From 74d4d1b895e3352f50ca8f56cc7c8443cc431047 Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 23 Nov 2020 09:32:24 +0000 Subject: [PATCH 09/47] Fixed CanFocus not being true by default for TableView --- Terminal.Gui/Views/TableView.cs | 3 ++- UICatalog/Scenarios/TableEditor.cs | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 5042727c9..0ed95a557 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -79,7 +79,7 @@ namespace Terminal.Gui.Views { /// Initialzies a class using layout. /// /// The table to display in the control - public TableView (DataTable table) : base () + public TableView (DataTable table) : this () { this.Table = table; } @@ -89,6 +89,7 @@ namespace Terminal.Gui.Views { /// public TableView () : base () { + CanFocus = true; } /// diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index d5da0ad55..03d4c7801 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -48,8 +48,6 @@ namespace UICatalog.Scenarios { Width = Dim.Fill (), Height = Dim.Fill (), }; - tableView.CanFocus = true; - Win.Add (tableView); } From 1416f2f047445b6a76ddde59ef846b836268cf54 Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 8 Dec 2020 11:57:41 +0000 Subject: [PATCH 10/47] Fixed always swallowing keystrokes in ProcessKey in TableView --- Terminal.Gui/Views/TableView.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 0ed95a557..10a50701b 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -205,6 +205,9 @@ namespace Terminal.Gui.Views { SelectedColumn = Table == null ? 0 : Table.Columns.Count - 1; Update (); break; + default: + // Not a keystroke we care about + return false; } PositionCursor (); return true; From 185f4ed4cde26480b26b01223437837600bdbea2 Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 14 Dec 2020 10:28:41 +0000 Subject: [PATCH 11/47] Added gridlines and fixed partial column rendering --- Terminal.Gui/Views/TableView.cs | 300 +++++++++++++++++++++++++---- UICatalog/Scenarios/TableEditor.cs | 88 +++++++++ 2 files changed, 350 insertions(+), 38 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 10a50701b..a1cbfe819 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -6,6 +6,37 @@ using System.Linq; namespace Terminal.Gui.Views { + /// + /// Defines rendering options that affect how the table is displayed + /// + public class TableStyle { + + /// + /// When scrolling down always lock the column headers in place as the first row of the table + /// + public bool AlwaysShowHeaders {get;set;} = false; + + /// + /// True to render a solid line above the headers + /// + public bool ShowHorizontalHeaderOverline {get;set;} = true; + + /// + /// True to render a solid line under the headers + /// + public bool ShowHorizontalHeaderUnderline {get;set;} = true; + + /// + /// True to render a solid line vertical line between cells + /// + public bool ShowVerticalCellLines {get;set;} = true; + + /// + /// True to render a solid line vertical line between headers + /// + public bool ShowVerticalHeaderLines {get;set;} = true; + } + /// /// View for tabular data based on a /// @@ -16,12 +47,18 @@ namespace Terminal.Gui.Views { private int selectedRow; private int selectedColumn; private DataTable table; + private TableStyle style = new TableStyle(); /// /// 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(); } } - + + /// + /// Contains options for changing how the table is rendered + /// + public TableStyle Style { get => style; set {style = value; Update(); } } + /// /// Zero indexed offset for the upper left to display in . /// @@ -71,7 +108,7 @@ namespace Terminal.Gui.Views { public string NullSymbol { get; set; } = "-"; /// - /// The symbol to add after each cell value and header value to visually seperate values + /// The symbol to add after each cell value and header value to visually seperate values (if not using vertical gridlines) /// public char SeparatorSymbol { get; set; } = ' '; @@ -102,45 +139,204 @@ namespace Terminal.Gui.Views { Dictionary columnsToRender = CalculateViewport (bounds); Driver.SetAttribute (ColorScheme.Normal); - + //invalidate current row (prevents scrolling around leaving old characters in the frame Driver.AddStr (new string (' ', bounds.Width)); - // Render the headers - foreach (var kvp in columnsToRender) { + int line = 0; - Move (kvp.Value, 0); - Driver.AddStr (Truncate (kvp.Key.ColumnName + SeparatorSymbol, bounds.Width - kvp.Value)); - } + if(ShouldRenderHeaders()){ + // Render something like: + /* + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ArithmeticComparatorβ”‚chi β”‚Healthboardβ”‚Interpretationβ”‚Labnumberβ”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + */ + if(Style.ShowHorizontalHeaderOverline){ + RenderHeaderOverline(line,bounds.Width,columnsToRender); + line++; + } - //render the cells - for (int line = 1; line < frame.Height; line++) { + RenderHeaderMidline(line,bounds.Width,columnsToRender); + line++; - //invalidate current row (prevents scrolling around leaving old characters in the frame - Move (0, line); - Driver.SetAttribute (ColorScheme.Normal); - Driver.AddStr (new string (' ', bounds.Width)); - - //work out what Row to render - var rowToRender = RowOffset + (line - 1); - - //if we have run off the end of the table - if ( Table == null || rowToRender >= Table.Rows.Count) - continue; - - foreach (var kvp in columnsToRender) { - Move (kvp.Value, line); - - bool isSelectedCell = rowToRender == SelectedRow && kvp.Key.Ordinal == SelectedColumn; - - Driver.SetAttribute (isSelectedCell ? ColorScheme.HotFocus : ColorScheme.Normal); - - - var valueToRender = GetRenderedVal (Table.Rows [rowToRender] [kvp.Key]) + SeparatorSymbol; - Driver.AddStr (Truncate (valueToRender, bounds.Width - kvp.Value)); + if(Style.ShowHorizontalHeaderUnderline){ + RenderHeaderUnderline(line,bounds.Width,columnsToRender); + line++; } } + + //render the cells + for (; line < frame.Height; line++) { + ClearLine(line,bounds.Width); + + //work out what Row to render + var rowToRender = RowOffset + (line - GetHeaderHeight()); + + //if we have run off the end of the table + if ( Table == null || rowToRender >= Table.Rows.Count || rowToRender < 0) + continue; + + RenderRow(line,bounds.Width,rowToRender,columnsToRender); + } + } + + /// + /// Clears a line of the console by filling it with spaces + /// + /// + /// + private void ClearLine(int row, int width) + { + Move (0, row); + Driver.SetAttribute (ColorScheme.Normal); + Driver.AddStr (new string (' ', width)); + } + + /// + /// Returns the amount of vertical space required to display the header + /// + /// + private int GetHeaderHeight() + { + int heightRequired = 1; + + if(Style.ShowHorizontalHeaderOverline) + heightRequired++; + + if(Style.ShowHorizontalHeaderUnderline) + heightRequired++; + + return heightRequired; + } + + private void RenderHeaderOverline(int row,int availableWidth, Dictionary columnsToRender) + { + // Renders a line above table headers (when visible) like: + // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + + for(int c = 0;c< availableWidth;c++) { + + var rune = Driver.HLine; + + if (Style.ShowVerticalHeaderLines){ + + if(c == 0){ + rune = Driver.ULCorner; + } + // if the next column is the start of a header + else if(columnsToRender.Values.Contains(c+1)){ + rune = Driver.TopTee; + } + else if(c == availableWidth -1){ + rune = Driver.URCorner; + } + } + + AddRuneAt(Driver,c,row,rune); + } + } + + private void RenderHeaderMidline(int row,int availableWidth, Dictionary columnsToRender) + { + // Renders something like: + // β”‚ArithmeticComparatorβ”‚chi β”‚Healthboardβ”‚Interpretationβ”‚Labnumberβ”‚ + + ClearLine(row,availableWidth); + + //render start of line + if(style.ShowVerticalHeaderLines) + AddRune(0,row,Driver.VLine); + + foreach (var kvp in columnsToRender) { + + //where the header should start + var col = kvp.Value; + + RenderSeparator(col-1,row); + + Move (col, row); + Driver.AddStr(Truncate (kvp.Key.ColumnName, availableWidth - kvp.Value)); + + } + + //render end of line + if(style.ShowVerticalHeaderLines) + AddRune(availableWidth-1,row,Driver.VLine); + } + + private void RenderHeaderUnderline(int row,int availableWidth, Dictionary columnsToRender) + { + // Renders a line below the table headers (when visible) like: + // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ + + for(int c = 0;c< availableWidth;c++) { + + var rune = Driver.HLine; + + if (Style.ShowVerticalHeaderLines){ + if(c == 0){ + rune = Style.ShowVerticalCellLines ? Driver.LeftTee : Driver.LLCorner; + } + // if the next column is the start of a header + else if(columnsToRender.Values.Contains(c+1)){ + + /*TODO: is β”Ό symbol in Driver?*/ + rune = Style.ShowVerticalCellLines ? 'β”Ό' :Driver.BottomTee; + } + else if(c == availableWidth -1){ + rune = Style.ShowVerticalCellLines ? Driver.RightTee : Driver.LRCorner; + } + } + + AddRuneAt(Driver,c,row,rune); + } + + } + private void RenderRow(int row, int availableWidth, int rowToRender, Dictionary columnsToRender) + { + //render start of line + if(style.ShowVerticalHeaderLines) + AddRune(0,row,Driver.VLine); + + // Render cells for each visible header for the current row + foreach (var kvp in columnsToRender) { + + // move to start of cell (in line with header positions) + Move (kvp.Value, row); + + // Set color scheme based on whether the current cell is the selected one + bool isSelectedCell = rowToRender == SelectedRow && kvp.Key.Ordinal == SelectedColumn; + Driver.SetAttribute (isSelectedCell ? ColorScheme.HotFocus : ColorScheme.Normal); + + // Render the (possibly truncated) cell value + var valueToRender = GetRenderedVal (Table.Rows [rowToRender] [kvp.Key]); + Driver.AddStr (Truncate (valueToRender, availableWidth - kvp.Value)); + + // Reset color scheme to normal and render the vertical line (or space) at the end of the cell + Driver.SetAttribute (ColorScheme.Normal); + RenderSeparator(kvp.Value-1,row); + } + + //render end of line + if(style.ShowVerticalHeaderLines) + AddRune(availableWidth-1,row,Driver.VLine); + } + + private void RenderSeparator(int col, int row) + { + if(col<0) + return; + + Rune symbol = style.ShowVerticalHeaderLines ? Driver.VLine : SeparatorSymbol; + AddRune(col,row,symbol); + } + + void AddRuneAt (ConsoleDriver d,int col, int row, Rune ch) + { + Move (col, row); + d.AddRune (ch); } /// @@ -231,6 +427,7 @@ namespace Terminal.Gui.Views { SelectedRow = Math.Max(Math.Min(SelectedRow,Table.Rows.Count -1),0); Dictionary columnsToRender = CalculateViewport (Bounds); + var headerHeight = GetHeaderHeight(); //if we have scrolled too far to the left if (SelectedColumn < columnsToRender.Keys.Min (col => col.Ordinal)) { @@ -243,7 +440,7 @@ namespace Terminal.Gui.Views { } //if we have scrolled too far down - if (SelectedRow > RowOffset + Bounds.Height - 1) { + if (SelectedRow >= RowOffset + (Bounds.Height - headerHeight)) { RowOffset = SelectedRow; } //if we have scrolled too far up @@ -266,24 +463,46 @@ namespace Terminal.Gui.Views { if(Table == null) return toReturn; - + int usedSpace = 0; + + //if horizontal space is required at the start of the line (before the first header) + if(Style.ShowVerticalHeaderLines || Style.ShowVerticalCellLines) + usedSpace+=2; + int availableHorizontalSpace = bounds.Width; - int rowsToRender = bounds.Height - 1; //1 reserved for the headers row + int rowsToRender = bounds.Height; - foreach (var col in Table.Columns.Cast ().Skip (ColumnOffset)) { + // reserved for the headers row + if(ShouldRenderHeaders()) + rowsToRender -= GetHeaderHeight(); - toReturn.Add (col, usedSpace); + bool first = true; + + foreach (var col in Table.Columns.Cast().Skip (ColumnOffset)) { + + int startingIdxForCurrentHeader = usedSpace; + + // is there enough space for this column (and it's data)? usedSpace += CalculateMaxRowSize (col, rowsToRender) + padding; - if (usedSpace > availableHorizontalSpace) + // no (don't render it) unless its the only column we are render (that must be one massively wide column!) + if (!first && usedSpace > availableHorizontalSpace) return toReturn; + // there is space + toReturn.Add (col, startingIdxForCurrentHeader); + first=false; } return toReturn; } + private bool ShouldRenderHeaders() + { + return Style.AlwaysShowHeaders || rowOffset == 0; + } + /// /// Returns the maximum of the name and the maximum length of data that will be rendered starting at and rendering /// @@ -294,6 +513,11 @@ namespace Terminal.Gui.Views { { int spaceRequired = col.ColumnName.Length; + // if table has no rows + if(RowOffset < 0) + return spaceRequired; + + for (int i = RowOffset; i < RowOffset + rowsToRender && i < Table.Rows.Count; i++) { //expand required space if cell is bigger than the last biggest cell or header diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 03d4c7801..8b3eac99f 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -30,6 +30,15 @@ namespace UICatalog.Scenarios { new MenuItem ("_CloseExample", "", () => CloseExample()), new MenuItem ("_Quit", "", () => Quit()), }), + new MenuBarItem ("_View", new MenuItem [] { + new MenuItem ("_AlwaysShowHeaders", "", () => ToggleAlwaysShowHeader()), + new MenuItem ("_HeaderOverLine", "", () => ToggleOverline()), + new MenuItem ("_HeaderMidLine", "", () => ToggleHeaderMidline()), + new MenuItem ("_HeaderUnderLine", "", () => ToggleUnderline()), + new MenuItem ("_CellLines", "", () => ToggleCellLines()), + new MenuItem ("_AllLines", "", () => ToggleAllCellLines()), + new MenuItem ("_NoLines", "", () => ToggleNoCellLines()), + }), }); Top.Add (menu); @@ -38,6 +47,7 @@ namespace UICatalog.Scenarios { new StatusItem(Key.F2, "~F2~ OpenExample", () => OpenExample(true)), new StatusItem(Key.F3, "~F3~ EditCell", () => EditCurrentCell()), new StatusItem(Key.F4, "~F4~ CloseExample", () => CloseExample()), + new StatusItem(Key.F5, "~F5~ OpenSimple", () => OpenSimple(true)), new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()), }); Top.Add (statusBar); @@ -52,6 +62,52 @@ namespace UICatalog.Scenarios { Win.Add (tableView); } + + + private void ToggleAlwaysShowHeader () + { + tableView.Style.AlwaysShowHeaders = !tableView.Style.AlwaysShowHeaders; + tableView.Update(); + } + + private void ToggleOverline () + { + tableView.Style.ShowHorizontalHeaderOverline = !tableView.Style.ShowHorizontalHeaderOverline; + tableView.Update(); + } + private void ToggleHeaderMidline () + { + tableView.Style.ShowVerticalHeaderLines = !tableView.Style.ShowVerticalHeaderLines; + tableView.Update(); + } + private void ToggleUnderline () + { + tableView.Style.ShowHorizontalHeaderUnderline = !tableView.Style.ShowHorizontalHeaderUnderline; + tableView.Update(); + } + private void ToggleCellLines() + { + tableView.Style.ShowVerticalCellLines = !tableView.Style.ShowVerticalCellLines; + tableView.Update(); + } + private void ToggleAllCellLines() + { + tableView.Style.ShowHorizontalHeaderOverline = true; + tableView.Style.ShowVerticalHeaderLines = true; + tableView.Style.ShowHorizontalHeaderUnderline = true; + tableView.Style.ShowVerticalCellLines = true; + tableView.Update(); + } + private void ToggleNoCellLines() + { + tableView.Style.ShowHorizontalHeaderOverline = false; + tableView.Style.ShowVerticalHeaderLines = false; + tableView.Style.ShowHorizontalHeaderUnderline = false; + tableView.Style.ShowVerticalCellLines = false; + tableView.Update(); + } + + private void CloseExample () { tableView.Table = null; @@ -66,6 +122,11 @@ namespace UICatalog.Scenarios { { tableView.Table = BuildDemoDataTable(big ? 30 : 5, big ? 1000 : 5); } + private void OpenSimple (bool big) + { + + tableView.Table = BuildSimpleDataTable(big ? 30 : 5, big ? 1000 : 5); + } private void EditCurrentCell () { @@ -154,5 +215,32 @@ namespace UICatalog.Scenarios { return dt; } + + /// + /// Builds a simple table in which cell values contents are the index of the cell. This helps testing that scrolling etc is working correctly and not skipping out any rows/columns when paging + /// + /// + /// + /// + public static DataTable BuildSimpleDataTable(int cols, int rows) + { + var dt = new DataTable(); + + for(int c = 0; c < cols; c++) { + dt.Columns.Add("Col"+c); + } + + for(int r = 0; r < rows; r++) { + var newRow = dt.NewRow(); + + for(int c = 0; c < cols; c++) { + newRow[c] = $"R{r}C{c}"; + } + + dt.Rows.Add(newRow); + } + + return dt; + } } } From 700e097e4bf55852c80ba902035bafda595a81cc Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 14 Dec 2020 10:45:32 +0000 Subject: [PATCH 12/47] Fixed start of line rendering and line flag checks --- Terminal.Gui/Views/TableView.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index a1cbfe819..9dcf9f722 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -254,7 +254,7 @@ namespace Terminal.Gui.Views { //where the header should start var col = kvp.Value; - RenderSeparator(col-1,row); + RenderSeparator(col-1,row,true); Move (col, row); Driver.AddStr(Truncate (kvp.Key.ColumnName, availableWidth - kvp.Value)); @@ -297,7 +297,7 @@ namespace Terminal.Gui.Views { private void RenderRow(int row, int availableWidth, int rowToRender, Dictionary columnsToRender) { //render start of line - if(style.ShowVerticalHeaderLines) + if(style.ShowVerticalCellLines) AddRune(0,row,Driver.VLine); // Render cells for each visible header for the current row @@ -316,20 +316,22 @@ namespace Terminal.Gui.Views { // Reset color scheme to normal and render the vertical line (or space) at the end of the cell Driver.SetAttribute (ColorScheme.Normal); - RenderSeparator(kvp.Value-1,row); + RenderSeparator(kvp.Value-1,row,false); } //render end of line - if(style.ShowVerticalHeaderLines) + if(style.ShowVerticalCellLines) AddRune(availableWidth-1,row,Driver.VLine); } - private void RenderSeparator(int col, int row) + private void RenderSeparator(int col, int row,bool isHeader) { if(col<0) return; + + var renderLines = isHeader ? style.ShowVerticalHeaderLines : style.ShowVerticalCellLines; - Rune symbol = style.ShowVerticalHeaderLines ? Driver.VLine : SeparatorSymbol; + Rune symbol = renderLines ? Driver.VLine : SeparatorSymbol; AddRune(col,row,symbol); } @@ -468,7 +470,7 @@ namespace Terminal.Gui.Views { //if horizontal space is required at the start of the line (before the first header) if(Style.ShowVerticalHeaderLines || Style.ShowVerticalCellLines) - usedSpace+=2; + usedSpace+=1; int availableHorizontalSpace = bounds.Width; int rowsToRender = bounds.Height; @@ -500,6 +502,9 @@ namespace Terminal.Gui.Views { private bool ShouldRenderHeaders() { + if(Table == null || Table.Columns.Count == 0) + return false; + return Style.AlwaysShowHeaders || rowOffset == 0; } From ace6251414ae27b5d0370b1948dd7767a534a709 Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 14 Dec 2020 13:45:34 +0000 Subject: [PATCH 13/47] Support for column styles (alignment and format) --- Terminal.Gui/Views/TableView.cs | 265 ++++++++++++++++++++++++----- UICatalog/Scenarios/TableEditor.cs | 49 +++++- 2 files changed, 265 insertions(+), 49 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 9dcf9f722..be09d4abe 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -6,6 +6,59 @@ using System.Linq; namespace Terminal.Gui.Views { + public class ColumnStyle { + + /// + /// Defines the default alignment for all values rendered in this column. For custom alignment based on cell contents use . + /// + public TextAlignment Alignment {get;set;} + + /// + /// Defines a delegate for returning custom alignment per cell based on cell values. When specified this will override + /// + public Func AlignmentGetter; + + /// + /// Defines a delegate for returning custom representations of cell values. If not set then is used. Return values from your delegate may be truncated e.g. based on + /// + public Func RepresentationGetter; + + /// + /// Set the maximum width of the column in characters. This value will be ignored if more than the tables . Defaults to + /// + public int MaxWidth {get;set;} = TableView.DefaultMaxCellWidth; + + /// + /// Set the minimum width of the column in characters. This value will be ignored if more than the tables or the + /// + public int MinWidth {get;set;} + + /// + /// Returns the alignment for the cell based on and / + /// + /// + /// + public TextAlignment GetAlignment(object cellValue) + { + if(AlignmentGetter != null) + return AlignmentGetter(cellValue); + + return Alignment; + } + + /// + /// Returns the full string to render (which may be truncated if too long) that the current style says best represents the given + /// + /// + /// + public string GetRepresentation (object value) + { + if(RepresentationGetter != null) + return RepresentationGetter(value); + + return value?.ToString(); + } + } /// /// Defines rendering options that affect how the table is displayed /// @@ -35,6 +88,21 @@ namespace Terminal.Gui.Views { /// True to render a solid line vertical line between headers /// public bool ShowVerticalHeaderLines {get;set;} = true; + + /// + /// Collection of columns for which you want special rendering (e.g. custom column lengths, text alignment etc) + /// + public Dictionary ColumnStyles {get;set; } = new Dictionary(); + + /// + /// Returns the entry from for the given or null if no custom styling is defined for it + /// + /// + /// + public ColumnStyle GetColumnStyleIfAny (DataColumn col) + { + return ColumnStyles.TryGetValue(col,out ColumnStyle result) ? result : null; + } } /// @@ -49,6 +117,11 @@ namespace Terminal.Gui.Views { private DataTable table; private TableStyle style = new TableStyle(); + /// + /// The default maximum cell width for and + /// + public const int DefaultMaxCellWidth = 100; + /// /// The data table to render in the view. Setting this property automatically updates and redraws the control. /// @@ -100,7 +173,7 @@ namespace Terminal.Gui.Views { /// /// The maximum number of characters to render in any given column. This prevents one long column from pushing out all the others /// - public int MaximumCellWidth { get; set; } = 100; + public int MaxCellWidth { get; set; } = DefaultMaxCellWidth; /// /// The text representation that should be rendered for cells with the value @@ -136,7 +209,7 @@ namespace Terminal.Gui.Views { var frame = Frame; // What columns to render at what X offset in viewport - Dictionary columnsToRender = CalculateViewport (bounds); + var columnsToRender = CalculateViewport(bounds).ToArray(); Driver.SetAttribute (ColorScheme.Normal); @@ -211,7 +284,7 @@ namespace Terminal.Gui.Views { return heightRequired; } - private void RenderHeaderOverline(int row,int availableWidth, Dictionary columnsToRender) + private void RenderHeaderOverline(int row,int availableWidth, ColumnToRender[] columnsToRender) { // Renders a line above table headers (when visible) like: // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β” @@ -226,7 +299,7 @@ namespace Terminal.Gui.Views { rune = Driver.ULCorner; } // if the next column is the start of a header - else if(columnsToRender.Values.Contains(c+1)){ + else if(columnsToRender.Any(r=>r.X == c+1)){ rune = Driver.TopTee; } else if(c == availableWidth -1){ @@ -238,7 +311,7 @@ namespace Terminal.Gui.Views { } } - private void RenderHeaderMidline(int row,int availableWidth, Dictionary columnsToRender) + private void RenderHeaderMidline(int row,int availableWidth, ColumnToRender[] columnsToRender) { // Renders something like: // β”‚ArithmeticComparatorβ”‚chi β”‚Healthboardβ”‚Interpretationβ”‚Labnumberβ”‚ @@ -249,15 +322,19 @@ namespace Terminal.Gui.Views { if(style.ShowVerticalHeaderLines) AddRune(0,row,Driver.VLine); - foreach (var kvp in columnsToRender) { + for(int i =0 ; i columnsToRender) + /// + /// Calculates how much space is available to render index of the given the remaining horizontal space + /// + /// + /// + /// + private int GetCellWidth (ColumnToRender [] columnsToRender, int i,int availableWidth) + { + var current = columnsToRender[i]; + var next = i+1 < columnsToRender.Length ? columnsToRender[i+1] : null; + + if(next == null) { + // cell can fill to end of the line + return availableWidth - current.X; + } + else { + // cell can fill up to next cell start + return next.X - current.X; + } + + } + + private void RenderHeaderUnderline(int row,int availableWidth, ColumnToRender[] columnsToRender) { // Renders a line below the table headers (when visible) like: // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ @@ -280,7 +379,7 @@ namespace Terminal.Gui.Views { rune = Style.ShowVerticalCellLines ? Driver.LeftTee : Driver.LLCorner; } // if the next column is the start of a header - else if(columnsToRender.Values.Contains(c+1)){ + else if(columnsToRender.Any(r=>r.X == c+1)){ /*TODO: is β”Ό symbol in Driver?*/ rune = Style.ShowVerticalCellLines ? 'β”Ό' :Driver.BottomTee; @@ -294,29 +393,37 @@ namespace Terminal.Gui.Views { } } - private void RenderRow(int row, int availableWidth, int rowToRender, Dictionary columnsToRender) + private void RenderRow(int row, int availableWidth, int rowToRender, ColumnToRender[] columnsToRender) { //render start of line if(style.ShowVerticalCellLines) AddRune(0,row,Driver.VLine); // Render cells for each visible header for the current row - foreach (var kvp in columnsToRender) { + for(int i=0;i< columnsToRender.Length ;i++) { + + var current = columnsToRender[i]; + var availableWidthForCell = GetCellWidth(columnsToRender,i,availableWidth); + + var colStyle = Style.GetColumnStyleIfAny(current.Column); // move to start of cell (in line with header positions) - Move (kvp.Value, row); + Move (current.X, row); // Set color scheme based on whether the current cell is the selected one - bool isSelectedCell = rowToRender == SelectedRow && kvp.Key.Ordinal == SelectedColumn; + bool isSelectedCell = rowToRender == SelectedRow && current.Column.Ordinal == SelectedColumn; Driver.SetAttribute (isSelectedCell ? ColorScheme.HotFocus : ColorScheme.Normal); + var val = Table.Rows [rowToRender][current.Column]; + // Render the (possibly truncated) cell value - var valueToRender = GetRenderedVal (Table.Rows [rowToRender] [kvp.Key]); - Driver.AddStr (Truncate (valueToRender, availableWidth - kvp.Value)); + var representation = GetRepresentation(val,colStyle); + + Driver.AddStr (TruncateOrPad(val,representation,availableWidthForCell,colStyle)); // Reset color scheme to normal and render the vertical line (or space) at the end of the cell Driver.SetAttribute (ColorScheme.Normal); - RenderSeparator(kvp.Value-1,row,false); + RenderSeparator(current.X-1,row,false); } //render end of line @@ -342,17 +449,49 @@ namespace Terminal.Gui.Views { } /// - /// Truncates so that it occupies a maximum of + /// Truncates or pads so that it occupies a exactly using the alignment specified in (or left if no style is defined) /// - /// + /// The object in this cell of the + /// The string representation of /// + /// Optional style indicating custom alignment for the cell /// - private ustring Truncate (string valueToRender, int availableHorizontalSpace) + private ustring TruncateOrPad (object originalCellValue,string representation, int availableHorizontalSpace, ColumnStyle colStyle) { - if (string.IsNullOrEmpty (valueToRender) || valueToRender.Length < availableHorizontalSpace) - return valueToRender; + if (string.IsNullOrEmpty (representation)) + return representation; - return valueToRender.Substring (0, availableHorizontalSpace); + // if value is not wide enough + if(representation.Length < availableHorizontalSpace) { + + // pad it out with spaces to the given alignment + int toPad = availableHorizontalSpace - representation.Length; + + switch(colStyle?.GetAlignment(originalCellValue) ?? TextAlignment.Left) { + + case TextAlignment.Left : + representation = representation.PadRight(toPad); + break; + case TextAlignment.Right : + representation = representation.PadLeft(toPad); + break; + + // TODO: With single line cells, centered and justified are the same right? + case TextAlignment.Centered : + case TextAlignment.Justified : + //round down + representation = representation.PadRight((int)Math.Floor(toPad/2.0)); + //round up + representation = representation.PadLeft((int)Math.Ceiling(toPad/2.0)); + break; + + } + + return representation; + } + + // value is too wide + return representation.Substring (0, availableHorizontalSpace); } /// @@ -428,16 +567,16 @@ namespace Terminal.Gui.Views { SelectedColumn = Math.Max(Math.Min(SelectedColumn,Table.Columns.Count -1),0); SelectedRow = Math.Max(Math.Min(SelectedRow,Table.Rows.Count -1),0); - Dictionary columnsToRender = CalculateViewport (Bounds); + var columnsToRender = CalculateViewport (Bounds).ToArray(); var headerHeight = GetHeaderHeight(); //if we have scrolled too far to the left - if (SelectedColumn < columnsToRender.Keys.Min (col => col.Ordinal)) { + if (SelectedColumn < columnsToRender.Min (r => r.Column.Ordinal)) { ColumnOffset = SelectedColumn; } //if we have scrolled too far to the right - if (SelectedColumn > columnsToRender.Keys.Max (col => col.Ordinal)) { + if (SelectedColumn > columnsToRender.Max (r=> r.Column.Ordinal)) { ColumnOffset = SelectedColumn; } @@ -459,12 +598,10 @@ namespace Terminal.Gui.Views { /// /// /// - private Dictionary CalculateViewport (Rect bounds, int padding = 1) + private IEnumerable CalculateViewport (Rect bounds, int padding = 1) { - Dictionary toReturn = new Dictionary (); - if(Table == null) - return toReturn; + yield break; int usedSpace = 0; @@ -484,20 +621,19 @@ namespace Terminal.Gui.Views { foreach (var col in Table.Columns.Cast().Skip (ColumnOffset)) { int startingIdxForCurrentHeader = usedSpace; + var colStyle = Style.GetColumnStyleIfAny(col); // is there enough space for this column (and it's data)? - usedSpace += CalculateMaxRowSize (col, rowsToRender) + padding; + usedSpace += CalculateMaxCellWidth (col, rowsToRender,colStyle) + padding; // no (don't render it) unless its the only column we are render (that must be one massively wide column!) if (!first && usedSpace > availableHorizontalSpace) - return toReturn; + yield break; // there is space - toReturn.Add (col, startingIdxForCurrentHeader); + yield return new ColumnToRender(col, startingIdxForCurrentHeader); first=false; } - - return toReturn; } private bool ShouldRenderHeaders() @@ -513,8 +649,9 @@ namespace Terminal.Gui.Views { /// /// /// + /// /// - private int CalculateMaxRowSize (DataColumn col, int rowsToRender) + private int CalculateMaxCellWidth(DataColumn col, int rowsToRender,ColumnStyle colStyle) { int spaceRequired = col.ColumnName.Length; @@ -526,9 +663,28 @@ namespace Terminal.Gui.Views { for (int i = RowOffset; i < RowOffset + rowsToRender && i < Table.Rows.Count; i++) { //expand required space if cell is bigger than the last biggest cell or header - spaceRequired = Math.Max (spaceRequired, GetRenderedVal (Table.Rows [i] [col]).Length); + spaceRequired = Math.Max (spaceRequired, GetRepresentation(Table.Rows [i][col],colStyle).Length); } + // Don't require more space than the style allows + if(colStyle != null){ + + // enforce maximum cell width based on style + if(spaceRequired > colStyle.MaxWidth) { + spaceRequired = colStyle.MaxWidth; + } + + // enforce minimum cell width based on style + if(spaceRequired < colStyle.MinWidth) { + spaceRequired = colStyle.MinWidth; + } + } + + // enforce maximum cell width based on global table style + if(spaceRequired > MaxCellWidth) + spaceRequired = MaxCellWidth; + + return spaceRequired; } @@ -536,20 +692,37 @@ namespace Terminal.Gui.Views { /// Returns the value that should be rendered to best represent a strongly typed read from /// /// + /// Optional style defining how to represent cell values /// - private string GetRenderedVal (object value) + private string GetRepresentation(object value,ColumnStyle colStyle) { if (value == null || value == DBNull.Value) { return NullSymbol; } - var representation = value.ToString (); + return colStyle != null ? colStyle.GetRepresentation(value): value.ToString(); + } + } - //if it is too long to fit - if (representation.Length > MaximumCellWidth) - return representation.Substring (0, MaximumCellWidth); + /// + /// Describes a desire to render a column at a given horizontal position in the UI + /// + internal class ColumnToRender { - return representation; + /// + /// The column to render + /// + public DataColumn Column {get;set;} + + /// + /// The horizontal position to begin rendering the column at + /// + public int X{get;set;} + + public ColumnToRender (DataColumn col, int x) + { + Column = col; + X = x; } } } diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 8b3eac99f..3ee7f34aa 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -38,6 +38,7 @@ namespace UICatalog.Scenarios { new MenuItem ("_CellLines", "", () => ToggleCellLines()), new MenuItem ("_AllLines", "", () => ToggleAllCellLines()), new MenuItem ("_NoLines", "", () => ToggleNoCellLines()), + new MenuItem ("_ClearColumnStyles", "", () => ClearColumnStyles()), }), }); Top.Add (menu); @@ -62,7 +63,11 @@ namespace UICatalog.Scenarios { Win.Add (tableView); } - + private void ClearColumnStyles () + { + tableView.Style.ColumnStyles.Clear(); + tableView.Update(); + } private void ToggleAlwaysShowHeader () { @@ -121,10 +126,48 @@ namespace UICatalog.Scenarios { private void OpenExample (bool big) { tableView.Table = BuildDemoDataTable(big ? 30 : 5, big ? 1000 : 5); + SetDemoTableStyles(); } + + private void SetDemoTableStyles () + { + var alignMid = new ColumnStyle() { + Alignment = TextAlignment.Centered + }; + var alignRight = new ColumnStyle() { + Alignment = TextAlignment.Right + }; + + var dateFormatStyle = new ColumnStyle() { + Alignment = TextAlignment.Right, + RepresentationGetter = (v)=> v is DateTime d ? d.ToString("yyyy-MM-dd"):v.ToString() + }; + + var negativeRight = new ColumnStyle() { + + RepresentationGetter = (v)=> v is double d ? + d.ToString("0.##"): + v.ToString(), + MinWidth = 10, + AlignmentGetter = (v)=>v is double d ? + // align negative values right + d < 0 ? TextAlignment.Right : + // align positive values left + TextAlignment.Left: + // not a double + TextAlignment.Left + }; + + 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.Update(); + } + private void OpenSimple (bool big) { - tableView.Table = BuildSimpleDataTable(big ? 30 : 5, big ? 1000 : 5); } @@ -202,7 +245,7 @@ namespace UICatalog.Scenarios { "Some long text that is super cool", new DateTime(2000+i,12,25), r.Next(i), - r.NextDouble()*i, + (r.NextDouble()*i)-0.5 /*add some negatives to demo styles*/, DBNull.Value }; From 30251c8bafbf31c64ddd58655c438053f2cb0e6f Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 14 Dec 2020 14:01:29 +0000 Subject: [PATCH 14/47] Fixed alignment padding and made better use of Bounds.Width --- Terminal.Gui/Views/TableView.cs | 45 ++++++++++++++------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index be09d4abe..eccc83605 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -230,7 +230,7 @@ namespace Terminal.Gui.Views { line++; } - RenderHeaderMidline(line,bounds.Width,columnsToRender); + RenderHeaderMidline(line,columnsToRender); line++; if(Style.ShowHorizontalHeaderUnderline){ @@ -251,7 +251,7 @@ namespace Terminal.Gui.Views { if ( Table == null || rowToRender >= Table.Rows.Count || rowToRender < 0) continue; - RenderRow(line,bounds.Width,rowToRender,columnsToRender); + RenderRow(line,rowToRender,columnsToRender); } } @@ -311,12 +311,12 @@ namespace Terminal.Gui.Views { } } - private void RenderHeaderMidline(int row,int availableWidth, ColumnToRender[] columnsToRender) + private void RenderHeaderMidline(int row, ColumnToRender[] columnsToRender) { // Renders something like: // β”‚ArithmeticComparatorβ”‚chi β”‚Healthboardβ”‚Interpretationβ”‚Labnumberβ”‚ - ClearLine(row,availableWidth); + ClearLine(row,Bounds.Width); //render start of line if(style.ShowVerticalHeaderLines) @@ -325,7 +325,7 @@ namespace Terminal.Gui.Views { for(int i =0 ; i @@ -348,15 +348,14 @@ namespace Terminal.Gui.Views { /// /// /// - /// - private int GetCellWidth (ColumnToRender [] columnsToRender, int i,int availableWidth) + private int GetCellWidth (ColumnToRender [] columnsToRender, int i) { var current = columnsToRender[i]; var next = i+1 < columnsToRender.Length ? columnsToRender[i+1] : null; if(next == null) { // cell can fill to end of the line - return availableWidth - current.X; + return Bounds.Width - current.X; } else { // cell can fill up to next cell start @@ -393,7 +392,7 @@ namespace Terminal.Gui.Views { } } - private void RenderRow(int row, int availableWidth, int rowToRender, ColumnToRender[] columnsToRender) + private void RenderRow(int row, int rowToRender, ColumnToRender[] columnsToRender) { //render start of line if(style.ShowVerticalCellLines) @@ -403,7 +402,7 @@ namespace Terminal.Gui.Views { for(int i=0;i< columnsToRender.Length ;i++) { var current = columnsToRender[i]; - var availableWidthForCell = GetCellWidth(columnsToRender,i,availableWidth); + var availableWidthForCell = GetCellWidth(columnsToRender,i); var colStyle = Style.GetColumnStyleIfAny(current.Column); @@ -428,7 +427,7 @@ namespace Terminal.Gui.Views { //render end of line if(style.ShowVerticalCellLines) - AddRune(availableWidth-1,row,Driver.VLine); + AddRune(Bounds.Width-1,row,Driver.VLine); } private void RenderSeparator(int col, int row,bool isHeader) @@ -456,7 +455,7 @@ namespace Terminal.Gui.Views { /// /// Optional style indicating custom alignment for the cell /// - private ustring TruncateOrPad (object originalCellValue,string representation, int availableHorizontalSpace, ColumnStyle colStyle) + private string TruncateOrPad (object originalCellValue,string representation, int availableHorizontalSpace, ColumnStyle colStyle) { if (string.IsNullOrEmpty (representation)) return representation; @@ -465,29 +464,23 @@ namespace Terminal.Gui.Views { if(representation.Length < availableHorizontalSpace) { // pad it out with spaces to the given alignment - int toPad = availableHorizontalSpace - representation.Length; + int toPad = availableHorizontalSpace - (representation.Length+1 /*leave 1 space for cell boundary*/); switch(colStyle?.GetAlignment(originalCellValue) ?? TextAlignment.Left) { case TextAlignment.Left : - representation = representation.PadRight(toPad); - break; + return representation + new string(' ',toPad); case TextAlignment.Right : - representation = representation.PadLeft(toPad); - break; + return new string(' ',toPad) + representation; // TODO: With single line cells, centered and justified are the same right? case TextAlignment.Centered : case TextAlignment.Justified : - //round down - representation = representation.PadRight((int)Math.Floor(toPad/2.0)); - //round up - representation = representation.PadLeft((int)Math.Ceiling(toPad/2.0)); - break; - + return + new string(' ',(int)Math.Floor(toPad/2.0)) + // round down + representation + + new string(' ',(int)Math.Ceiling(toPad/2.0)) ; // round up } - - return representation; } // value is too wide From 7cf34777d370bb456cc9dc02a34c4fa7ef57b5f8 Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 14 Dec 2020 15:49:29 +0000 Subject: [PATCH 15/47] Fixed missing xmldoc --- Terminal.Gui/Views/TableView.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index eccc83605..0a9a8cc0c 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -6,6 +6,9 @@ using System.Linq; namespace Terminal.Gui.Views { + /// + /// Describes how to render a given column in a including and textual representation of cells (e.g. date formats) + /// public class ColumnStyle { /// @@ -448,7 +451,7 @@ namespace Terminal.Gui.Views { } /// - /// Truncates or pads so that it occupies a exactly using the alignment specified in (or left if no style is defined) + /// Truncates or pads so that it occupies a exactly using the alignment specified in (or left if no style is defined) /// /// The object in this cell of the /// The string representation of From e1b60fb9fa26d35ba0b82dadfdb21826afd1ed2d Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 16 Dec 2020 19:47:15 +0000 Subject: [PATCH 16/47] Added Format property to ColumnStyle --- Terminal.Gui/Views/TableView.cs | 12 ++++++++++++ UICatalog/Scenarios/TableEditor.cs | 4 +--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 0a9a8cc0c..7323e2772 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -26,6 +26,11 @@ namespace Terminal.Gui.Views { /// public Func RepresentationGetter; + /// + /// Defines the format for values e.g. "yyyy-MM-dd" for dates + /// + public string Format{get;set;} + /// /// Set the maximum width of the column in characters. This value will be ignored if more than the tables . Defaults to /// @@ -56,6 +61,13 @@ namespace Terminal.Gui.Views { /// public string GetRepresentation (object value) { + if(!string.IsNullOrWhiteSpace(Format)) { + + if(value is IFormattable f) + return f.ToString(Format,null); + } + + if(RepresentationGetter != null) return RepresentationGetter(value); diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 3ee7f34aa..1767e8691 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -145,9 +145,7 @@ namespace UICatalog.Scenarios { var negativeRight = new ColumnStyle() { - RepresentationGetter = (v)=> v is double d ? - d.ToString("0.##"): - v.ToString(), + Format = "0.##", MinWidth = 10, AlignmentGetter = (v)=>v is double d ? // align negative values right From 94c4f2aab1e63e0578057b7ab9c265f71035c28a Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 28 Dec 2020 20:05:01 +0000 Subject: [PATCH 17/47] Mouse support for selecting cells --- Terminal.Gui/Views/TableView.cs | 47 +++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 7323e2772..7df3a44dd 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -558,6 +558,53 @@ namespace Terminal.Gui.Views { return true; } + /// + public override bool MouseEvent (MouseEvent me) + { + if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) && + me.Flags != MouseFlags.WheeledDown && me.Flags != MouseFlags.WheeledUp) + return false; + + if (!HasFocus && CanFocus) { + SetFocus (); + } + + if (Table == null) { + return false; + } + + if (me.Flags == MouseFlags.WheeledDown) { + + RowOffset++; + Update (); + return true; + } else if (me.Flags == MouseFlags.WheeledUp) { + RowOffset--; + return true; + } + + if(me.Flags == MouseFlags.Button1Clicked) { + + var viewPort = CalculateViewport(Bounds); + + var headerHeight = GetHeaderHeight(); + + var col = viewPort.LastOrDefault(c=>c.X < me.OfX); + + var rowIdx = RowOffset - headerHeight + me.OfY; + + if(col != null && rowIdx >= 0) { + + SelectedRow = rowIdx; + SelectedColumn = col.Column.Ordinal; + + Update(); + } + } + + return false; + } + /// /// Updates the view to reflect changes to and to ( / ) etc /// From 0045bed69221950d0a6bf0df36bf0061fa619aad Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 28 Dec 2020 20:07:57 +0000 Subject: [PATCH 18/47] Added missing update on scroll wheel up --- Terminal.Gui/Views/TableView.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 7df3a44dd..03c1de17e 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -580,6 +580,7 @@ namespace Terminal.Gui.Views { return true; } else if (me.Flags == MouseFlags.WheeledUp) { RowOffset--; + Update (); return true; } From 2404a48fc65885b7dd07a01a69b776ad6a906db3 Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 29 Dec 2020 05:57:06 +0000 Subject: [PATCH 19/47] Fixed mouse scrolling (wheel) away from selected cell --- Terminal.Gui/Views/TableView.cs | 46 ++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 03c1de17e..6cc4a6c92 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -576,11 +576,17 @@ namespace Terminal.Gui.Views { if (me.Flags == MouseFlags.WheeledDown) { RowOffset++; - Update (); + + EnsureValidScrollOffsets(); + SetNeedsDisplay(); + return true; } else if (me.Flags == MouseFlags.WheeledUp) { RowOffset--; - Update (); + + EnsureValidScrollOffsets(); + SetNeedsDisplay(); + return true; } @@ -617,11 +623,8 @@ namespace Terminal.Gui.Views { return; } - //if user opened a large table scrolled down a lot then opened a smaller table (or API deleted a bunch of columns without telling anyone) - ColumnOffset = Math.Max(Math.Min(ColumnOffset,Table.Columns.Count -1),0); - RowOffset = Math.Max(Math.Min(RowOffset,Table.Rows.Count -1),0); - SelectedColumn = Math.Max(Math.Min(SelectedColumn,Table.Columns.Count -1),0); - SelectedRow = Math.Max(Math.Min(SelectedRow,Table.Rows.Count -1),0); + EnsureValidScrollOffsets(); + EnsureValidSelection(); var columnsToRender = CalculateViewport (Bounds).ToArray(); var headerHeight = GetHeaderHeight(); @@ -648,6 +651,35 @@ namespace Terminal.Gui.Views { SetNeedsDisplay (); } + /// + /// Updates and where they are outside the bounds of the table (by adjusting them to the nearest existing cell). Has no effect if has not been set. + /// + /// Changes will not be immediately visible in the display until you call + public void EnsureValidScrollOffsets () + { + if(Table == null){ + return; + } + + ColumnOffset = Math.Max(Math.Min(ColumnOffset,Table.Columns.Count -1),0); + RowOffset = Math.Max(Math.Min(RowOffset,Table.Rows.Count -1),0); + } + + + /// + /// Updates and where they are outside the bounds of the table (by adjusting them to the nearest existing cell). Has no effect if has not been set. + /// + /// Changes will not be immediately visible in the display until you call + public void EnsureValidSelection () + { + if(Table == null){ + return; + } + + SelectedColumn = Math.Max(Math.Min(SelectedColumn,Table.Columns.Count -1),0); + SelectedRow = Math.Max(Math.Min(SelectedRow,Table.Rows.Count -1),0); + } + /// /// Calculates which columns should be rendered given the in which to display and the /// From d3ec8b2f03b10958d31fd6310c6e298f37c0263f Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 29 Dec 2020 06:08:41 +0000 Subject: [PATCH 20/47] Refactored Update method. Includes new public method EnsureSelectedCellIsVisible --- Terminal.Gui/Views/TableView.cs | 55 ++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 6cc4a6c92..141816d96 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -626,27 +626,7 @@ namespace Terminal.Gui.Views { EnsureValidScrollOffsets(); EnsureValidSelection(); - var columnsToRender = CalculateViewport (Bounds).ToArray(); - var headerHeight = GetHeaderHeight(); - - //if we have scrolled too far to the left - if (SelectedColumn < columnsToRender.Min (r => r.Column.Ordinal)) { - ColumnOffset = SelectedColumn; - } - - //if we have scrolled too far to the right - if (SelectedColumn > columnsToRender.Max (r=> r.Column.Ordinal)) { - ColumnOffset = SelectedColumn; - } - - //if we have scrolled too far down - if (SelectedRow >= RowOffset + (Bounds.Height - headerHeight)) { - RowOffset = SelectedRow; - } - //if we have scrolled too far up - if (SelectedRow < RowOffset) { - RowOffset = SelectedRow; - } + EnsureSelectedCellIsVisible(); SetNeedsDisplay (); } @@ -680,6 +660,39 @@ namespace Terminal.Gui.Views { SelectedRow = Math.Max(Math.Min(SelectedRow,Table.Rows.Count -1),0); } + /// + /// Updates scroll offsets to ensure that the selected cell is visible. Has no effect if has not been set. + /// + /// Changes will not be immediately visible in the display until you call + public void EnsureSelectedCellIsVisible () + { + if(Table == null){ + return; + } + + var columnsToRender = CalculateViewport (Bounds).ToArray(); + var headerHeight = GetHeaderHeight(); + + //if we have scrolled too far to the left + if (SelectedColumn < columnsToRender.Min (r => r.Column.Ordinal)) { + ColumnOffset = SelectedColumn; + } + + //if we have scrolled too far to the right + if (SelectedColumn > columnsToRender.Max (r=> r.Column.Ordinal)) { + ColumnOffset = SelectedColumn; + } + + //if we have scrolled too far down + if (SelectedRow >= RowOffset + (Bounds.Height - headerHeight)) { + RowOffset = SelectedRow; + } + //if we have scrolled too far up + if (SelectedRow < RowOffset) { + RowOffset = SelectedRow; + } + } + /// /// Calculates which columns should be rendered given the in which to display and the /// From 52af2a609e77bfacf43615a86c45818ce8efe536 Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 29 Dec 2020 06:47:51 +0000 Subject: [PATCH 21/47] Fixed namespace, comments and added tests --- Terminal.Gui/Views/TableView.cs | 13 ++-- UICatalog/Scenarios/TableEditor.cs | 1 - UnitTests/TableViewTests.cs | 98 ++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 UnitTests/TableViewTests.cs diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 141816d96..7b3125fd0 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Data; using System.Linq; -namespace Terminal.Gui.Views { +namespace Terminal.Gui { /// /// Describes how to render a given column in a including and textual representation of cells (e.g. date formats) @@ -148,23 +148,22 @@ namespace Terminal.Gui.Views { public TableStyle Style { get => style; set {style = value; Update(); } } /// - /// Zero indexed offset for the upper left to display in . + /// Horizontal scroll offset. The index of the first column in to display when when rendering the view. /// /// This property allows very wide tables to be rendered with horizontal scrolling public int ColumnOffset { get => columnOffset; //try to prevent this being set to an out of bounds column - set => columnOffset = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); + set => columnOffset = Table == null ? 0 :Math.Max (0,Math.Min (Table.Columns.Count - 1, value)); } /// - /// Zero indexed offset for the to display in on line 2 of the control (first line being headers) + /// Vertical scroll offset. The index of the first row in to display in the first non header line of the control when rendering the view. /// - /// This property allows very wide tables to be rendered with horizontal scrolling public int RowOffset { get => rowOffset; - set => rowOffset = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); + set => rowOffset = Table == null ? 0 : Math.Max (0,Math.Min (Table.Rows.Count - 1, value)); } /// @@ -666,7 +665,7 @@ namespace Terminal.Gui.Views { /// Changes will not be immediately visible in the display until you call public void EnsureSelectedCellIsVisible () { - if(Table == null){ + if(Table == null || Table.Columns.Count <= 0){ return; } diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 1767e8691..cce0349b1 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Data; using Terminal.Gui; -using Terminal.Gui.Views; namespace UICatalog.Scenarios { diff --git a/UnitTests/TableViewTests.cs b/UnitTests/TableViewTests.cs new file mode 100644 index 000000000..c4819d251 --- /dev/null +++ b/UnitTests/TableViewTests.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Terminal.Gui; +using Xunit; + +namespace UnitTests { + public class TableViewTests + { + + [Fact] + public void EnsureValidScrollOffsets_WithNoCells() + { + var tableView = new TableView(); + + Assert.Equal(0,tableView.RowOffset); + Assert.Equal(0,tableView.ColumnOffset); + + // Set empty table + tableView.Table = new DataTable(); + + // Since table has no rows or columns scroll offset should default to 0 + tableView.EnsureValidScrollOffsets(); + Assert.Equal(0,tableView.RowOffset); + Assert.Equal(0,tableView.ColumnOffset); + } + + + + [Fact] + public void EnsureValidScrollOffsets_LoadSmallerTable() + { + var tableView = new TableView(); + tableView.Bounds = new Rect(0,0,25,10); + + Assert.Equal(0,tableView.RowOffset); + Assert.Equal(0,tableView.ColumnOffset); + + // Set big table + tableView.Table = BuildTable(25,50); + + // Scroll down and along + tableView.RowOffset = 20; + tableView.ColumnOffset = 10; + + tableView.EnsureValidScrollOffsets(); + + // The scroll should be valid at the moment + Assert.Equal(20,tableView.RowOffset); + Assert.Equal(10,tableView.ColumnOffset); + + // Set small table + tableView.Table = BuildTable(2,2); + + // Setting a small table should automatically trigger fixing the scroll offsets to ensure valid cells + Assert.Equal(0,tableView.RowOffset); + Assert.Equal(0,tableView.ColumnOffset); + + + // Trying to set invalid indexes should not be possible + tableView.RowOffset = 20; + tableView.ColumnOffset = 10; + + Assert.Equal(1,tableView.RowOffset); + Assert.Equal(1,tableView.ColumnOffset); + } + + /// + /// Builds a simple table of string columns with the requested number of columns and rows + /// + /// + /// + /// + public static DataTable BuildTable(int cols, int rows) + { + var dt = new DataTable(); + + for(int c = 0; c < cols; c++) { + dt.Columns.Add("Col"+c); + } + + for(int r = 0; r < rows; r++) { + var newRow = dt.NewRow(); + + for(int c = 0; c < cols; c++) { + newRow[c] = $"R{r}C{c}"; + } + + dt.Rows.Add(newRow); + } + + return dt; + } + } +} \ No newline at end of file From 81edb73785fc70b26786876806e9a53a4fb8f69f Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 29 Dec 2020 07:18:12 +0000 Subject: [PATCH 22/47] Added SelectedCellChanged event --- Terminal.Gui/Views/TableView.cs | 34 +++++++++++++++++++++++++++--- UICatalog/Scenarios/TableEditor.cs | 17 +++++++++++++-- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 7b3125fd0..55504e14a 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -172,8 +172,15 @@ namespace Terminal.Gui { public int SelectedColumn { get => selectedColumn; - //try to prevent this being set to an out of bounds column - set => selectedColumn = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); + set { + var oldValue = selectedColumn; + + //try to prevent this being set to an out of bounds column + selectedColumn = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); + + if(oldValue != selectedColumn) + OnSelectedCellChanged(); + } } /// @@ -181,7 +188,15 @@ namespace Terminal.Gui { /// public int SelectedRow { get => selectedRow; - set => selectedRow = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); + set { + + var oldValue = selectedColumn; + + selectedRow = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); + + if(oldValue != selectedRow) + OnSelectedCellChanged(); + } } /// @@ -199,6 +214,11 @@ namespace Terminal.Gui { /// public char SeparatorSymbol { get; set; } = ' '; + /// + /// This event is raised when the selected cell in the table changes. + /// + public event Action SelectedCellChanged; + /// /// Initialzies a class using layout. /// @@ -692,6 +712,14 @@ namespace Terminal.Gui { } } + /// + /// Invokes the event + /// + protected virtual void OnSelectedCellChanged() + { + SelectedCellChanged?.Invoke(new EventArgs()); + } + /// /// Calculates which columns should be rendered given the in which to display and the /// diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index cce0349b1..8e168af60 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -56,10 +56,23 @@ namespace UICatalog.Scenarios { X = 0, Y = 0, Width = Dim.Fill (), - Height = Dim.Fill (), + Height = Dim.Fill (1), }; Win.Add (tableView); + + var selectedCellLabel = new Label(){ + X = 0, + Y = Pos.Bottom(tableView), + Text = "0,0", + Width = Dim.Fill(), + TextAlignment = TextAlignment.Right + + }; + + Win.Add(selectedCellLabel); + + tableView.SelectedCellChanged += (s,e)=>{selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}";}; } private void ClearColumnStyles () @@ -231,7 +244,7 @@ namespace UICatalog.Scenarios { dt.Columns.Add(new DataColumn("NullsCol",typeof(string))); for(int i=0;i< cols -5; i++) { - dt.Columns.Add("Column" + (i+4)); + dt.Columns.Add("Column" + (i+5)); } var r = new Random(100); From 4bb9d9ac66b9afd5e0a21263e27191ad3d6d4746 Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 29 Dec 2020 07:20:44 +0000 Subject: [PATCH 23/47] Fixed typo changing EventHandler to Action in UICatalog --- UICatalog/Scenarios/TableEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 8e168af60..1abd247d9 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -72,7 +72,7 @@ namespace UICatalog.Scenarios { Win.Add(selectedCellLabel); - tableView.SelectedCellChanged += (s,e)=>{selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}";}; + tableView.SelectedCellChanged += (e)=>{selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}";}; } private void ClearColumnStyles () From 65806b1ba2fc4aa8b8642be9f5d046b2062ba0ef Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 29 Dec 2020 09:54:50 +0000 Subject: [PATCH 24/47] Added SelectedCellChangedEventArgs and tests --- Terminal.Gui/Views/TableView.cs | 69 ++++++++++++++++++++++++++++++--- UnitTests/TableViewTests.cs | 60 ++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 6 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 55504e14a..5395e70b6 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -179,7 +179,7 @@ namespace Terminal.Gui { selectedColumn = Table == null ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); if(oldValue != selectedColumn) - OnSelectedCellChanged(); + OnSelectedCellChanged(new SelectedCellChangedEventArgs(Table,oldValue,SelectedColumn,SelectedRow,SelectedRow)); } } @@ -190,12 +190,12 @@ namespace Terminal.Gui { get => selectedRow; set { - var oldValue = selectedColumn; + var oldValue = selectedRow; selectedRow = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); if(oldValue != selectedRow) - OnSelectedCellChanged(); + OnSelectedCellChanged(new SelectedCellChangedEventArgs(Table,SelectedColumn,SelectedColumn,oldValue,selectedRow)); } } @@ -217,7 +217,7 @@ namespace Terminal.Gui { /// /// This event is raised when the selected cell in the table changes. /// - public event Action SelectedCellChanged; + public event Action SelectedCellChanged; /// /// Initialzies a class using layout. @@ -715,9 +715,9 @@ namespace Terminal.Gui { /// /// Invokes the event /// - protected virtual void OnSelectedCellChanged() + protected virtual void OnSelectedCellChanged(SelectedCellChangedEventArgs args) { - SelectedCellChanged?.Invoke(new EventArgs()); + SelectedCellChanged?.Invoke(args); } /// @@ -853,4 +853,61 @@ namespace Terminal.Gui { X = x; } } + + /// + /// Defines the event arguments for + /// + 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;} + + + /// + /// 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 + /// + /// + public int OldCol {get;} + + + /// + /// The newly selected column index. + /// + /// + public int NewCol {get;} + + + /// + /// The previous selected row index. May be invalid e.g. when the selection has been changed as a result of deleting rows from the table + /// + /// + public int OldRow {get;} + + + /// + /// The newly selected row index. + /// + /// + public int NewRow {get;} + + /// + /// Creates a new instance of arguments describing a change in selected cell in a + /// + /// + /// + /// + /// + /// + public SelectedCellChangedEventArgs(DataTable t, int oldCol, int newCol, int oldRow, int newRow) + { + Table = t; + OldCol = oldCol; + NewCol = newCol; + OldRow = oldRow; + NewRow = newRow; + } + } } diff --git a/UnitTests/TableViewTests.cs b/UnitTests/TableViewTests.cs index c4819d251..481facd06 100644 --- a/UnitTests/TableViewTests.cs +++ b/UnitTests/TableViewTests.cs @@ -67,6 +67,66 @@ namespace UnitTests { Assert.Equal(1,tableView.RowOffset); Assert.Equal(1,tableView.ColumnOffset); } + + [Fact] + public void SelectedCellChanged_NotFiredForSameValue() + { + var tableView = new TableView(){ + Table = BuildTable(25,50) + }; + + bool called = false; + tableView.SelectedCellChanged += (e)=>{called=true;}; + + Assert.Equal(0,tableView.SelectedColumn); + Assert.False(called); + + // Changing value to same as it already was should not raise an event + tableView.SelectedColumn = 0; + + Assert.False(called); + + tableView.SelectedColumn = 10; + Assert.True(called); + } + + + + [Fact] + public void SelectedCellChanged_SelectedColumnIndexesCorrect() + { + var tableView = new TableView(){ + Table = BuildTable(25,50) + }; + + bool called = false; + tableView.SelectedCellChanged += (e)=>{ + called=true; + Assert.Equal(0,e.OldCol); + Assert.Equal(10,e.NewCol); + }; + + tableView.SelectedColumn = 10; + Assert.True(called); + } + + [Fact] + public void SelectedCellChanged_SelectedRowIndexesCorrect() + { + var tableView = new TableView(){ + Table = BuildTable(25,50) + }; + + bool called = false; + tableView.SelectedCellChanged += (e)=>{ + called=true; + Assert.Equal(0,e.OldRow); + Assert.Equal(10,e.NewRow); + }; + + tableView.SelectedRow = 10; + Assert.True(called); + } /// /// Builds a simple table of string columns with the requested number of columns and rows From a237e1a8e4bf5ddc8bdacc6ba2b301fe9bd82f0f Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 30 Dec 2020 07:53:52 +0000 Subject: [PATCH 25/47] Fixed draw/scroll bugs when header is not visible due to scrolling --- Terminal.Gui/Views/TableView.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 5395e70b6..547a234ba 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -273,13 +273,15 @@ namespace Terminal.Gui { } } + int headerLinesConsumed = line; + //render the cells for (; line < frame.Height; line++) { ClearLine(line,bounds.Width); //work out what Row to render - var rowToRender = RowOffset + (line - GetHeaderHeight()); + var rowToRender = RowOffset + (line - headerLinesConsumed); //if we have run off the end of the table if ( Table == null || rowToRender >= Table.Rows.Count || rowToRender < 0) @@ -613,7 +615,7 @@ namespace Terminal.Gui { var viewPort = CalculateViewport(Bounds); - var headerHeight = GetHeaderHeight(); + var headerHeight = ShouldRenderHeaders()? GetHeaderHeight():0; var col = viewPort.LastOrDefault(c=>c.X < me.OfX); @@ -690,7 +692,7 @@ namespace Terminal.Gui { } var columnsToRender = CalculateViewport (Bounds).ToArray(); - var headerHeight = GetHeaderHeight(); + var headerHeight = ShouldRenderHeaders()? GetHeaderHeight() : 0; //if we have scrolled too far to the left if (SelectedColumn < columnsToRender.Min (r => r.Column.Ordinal)) { From d4f14f81d2b0241703c196a94ffdf54651296bff Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 30 Dec 2020 08:05:58 +0000 Subject: [PATCH 26/47] Changed TableEditor in UICatalog to use checked menus --- UICatalog/Scenarios/TableEditor.cs | 58 +++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 1abd247d9..e18e09bc8 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -14,6 +14,11 @@ namespace UICatalog.Scenarios { public class TableEditor : Scenario { TableView tableView; + private MenuItem miAlwaysShowHeaders; + private MenuItem miHeaderOverline; + private MenuItem miHeaderMidline; + private MenuItem miHeaderUnderline; + private MenuItem miCellLines; public override void Setup () { @@ -22,6 +27,13 @@ namespace UICatalog.Scenarios { Win.Height = Dim.Fill (1); // status bar Top.LayoutSubviews (); + this.tableView = new TableView () { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill (1), + }; + var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { new MenuItem ("_OpenBigExample", "", () => OpenExample(true)), @@ -30,11 +42,11 @@ namespace UICatalog.Scenarios { new MenuItem ("_Quit", "", () => Quit()), }), new MenuBarItem ("_View", new MenuItem [] { - new MenuItem ("_AlwaysShowHeaders", "", () => ToggleAlwaysShowHeader()), - new MenuItem ("_HeaderOverLine", "", () => ToggleOverline()), - new MenuItem ("_HeaderMidLine", "", () => ToggleHeaderMidline()), - new MenuItem ("_HeaderUnderLine", "", () => ToggleUnderline()), - new MenuItem ("_CellLines", "", () => ToggleCellLines()), + miAlwaysShowHeaders = new MenuItem ("_AlwaysShowHeaders", "", () => ToggleAlwaysShowHeader()){Checked = tableView.Style.AlwaysShowHeaders, CheckType = MenuItemCheckStyle.Checked }, + miHeaderOverline = new MenuItem ("_HeaderOverLine", "", () => ToggleOverline()){Checked = tableView.Style.ShowHorizontalHeaderOverline, CheckType = MenuItemCheckStyle.Checked }, + miHeaderMidline = new MenuItem ("_HeaderMidLine", "", () => ToggleHeaderMidline()){Checked = tableView.Style.ShowVerticalHeaderLines, CheckType = MenuItemCheckStyle.Checked }, + miHeaderUnderline =new MenuItem ("_HeaderUnderLine", "", () => ToggleUnderline()){Checked = tableView.Style.ShowHorizontalHeaderUnderline, CheckType = MenuItemCheckStyle.Checked }, + miCellLines =new MenuItem ("_CellLines", "", () => ToggleCellLines()){Checked = tableView.Style.ShowVerticalCellLines, CheckType = MenuItemCheckStyle.Checked }, new MenuItem ("_AllLines", "", () => ToggleAllCellLines()), new MenuItem ("_NoLines", "", () => ToggleNoCellLines()), new MenuItem ("_ClearColumnStyles", "", () => ClearColumnStyles()), @@ -42,6 +54,8 @@ namespace UICatalog.Scenarios { }); Top.Add (menu); + + var statusBar = new StatusBar (new StatusItem [] { //new StatusItem(Key.Enter, "~ENTER~ ApplyEdits", () => { _hexView.ApplyEdits(); }), new StatusItem(Key.F2, "~F2~ OpenExample", () => OpenExample(true)), @@ -52,13 +66,6 @@ namespace UICatalog.Scenarios { }); Top.Add (statusBar); - this.tableView = new TableView () { - X = 0, - Y = 0, - Width = Dim.Fill (), - Height = Dim.Fill (1), - }; - Win.Add (tableView); var selectedCellLabel = new Label(){ @@ -83,28 +90,33 @@ namespace UICatalog.Scenarios { private void ToggleAlwaysShowHeader () { - tableView.Style.AlwaysShowHeaders = !tableView.Style.AlwaysShowHeaders; + miAlwaysShowHeaders.Checked = !miAlwaysShowHeaders.Checked; + tableView.Style.AlwaysShowHeaders = miAlwaysShowHeaders.Checked; tableView.Update(); } private void ToggleOverline () { - tableView.Style.ShowHorizontalHeaderOverline = !tableView.Style.ShowHorizontalHeaderOverline; + miHeaderOverline.Checked = !miHeaderOverline.Checked; + tableView.Style.ShowHorizontalHeaderOverline = miHeaderOverline.Checked; tableView.Update(); } private void ToggleHeaderMidline () { - tableView.Style.ShowVerticalHeaderLines = !tableView.Style.ShowVerticalHeaderLines; + miHeaderMidline.Checked = !miHeaderMidline.Checked; + tableView.Style.ShowVerticalHeaderLines = miHeaderMidline.Checked; tableView.Update(); } private void ToggleUnderline () { - tableView.Style.ShowHorizontalHeaderUnderline = !tableView.Style.ShowHorizontalHeaderUnderline; + miHeaderUnderline.Checked = !miHeaderUnderline.Checked; + tableView.Style.ShowHorizontalHeaderUnderline = miHeaderUnderline.Checked; tableView.Update(); } private void ToggleCellLines() { - tableView.Style.ShowVerticalCellLines = !tableView.Style.ShowVerticalCellLines; + miCellLines.Checked = !miCellLines.Checked; + tableView.Style.ShowVerticalCellLines = miCellLines.Checked; tableView.Update(); } private void ToggleAllCellLines() @@ -113,6 +125,12 @@ namespace UICatalog.Scenarios { tableView.Style.ShowVerticalHeaderLines = true; tableView.Style.ShowHorizontalHeaderUnderline = true; tableView.Style.ShowVerticalCellLines = true; + + miHeaderOverline.Checked = true; + miHeaderMidline.Checked = true; + miHeaderUnderline.Checked = true; + miCellLines.Checked = true; + tableView.Update(); } private void ToggleNoCellLines() @@ -121,6 +139,12 @@ namespace UICatalog.Scenarios { tableView.Style.ShowVerticalHeaderLines = false; tableView.Style.ShowHorizontalHeaderUnderline = false; tableView.Style.ShowVerticalCellLines = false; + + miHeaderOverline.Checked = false; + miHeaderMidline.Checked = false; + miHeaderUnderline.Checked = false; + miCellLines.Checked = false; + tableView.Update(); } From 448bc3af3be9b30d0a289a0d46576b56a5d58be3 Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 30 Dec 2020 08:14:47 +0000 Subject: [PATCH 27/47] Fixed cell selection when clicking near cell border --- Terminal.Gui/Views/TableView.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 547a234ba..bae21e1ac 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -617,8 +617,12 @@ namespace Terminal.Gui { var headerHeight = ShouldRenderHeaders()? GetHeaderHeight():0; - var col = viewPort.LastOrDefault(c=>c.X < me.OfX); + var col = viewPort.LastOrDefault(c=>c.X <= me.OfX); + // Click is on the header section of rendered UI + if(me.OfY < headerHeight) + return false; + var rowIdx = RowOffset - headerHeight + me.OfY; if(col != null && rowIdx >= 0) { From f610cc841610633df98d79fdce942fcb1d57956e Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 31 Dec 2020 10:06:49 +0000 Subject: [PATCH 28/47] better handling of unicode in table view --- Terminal.Gui/Views/TableView.cs | 10 +++++----- UICatalog/Scenarios/TableEditor.cs | 12 ++++++++---- UnitTests/TableViewTests.cs | 15 ++++++++++++++- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index bae21e1ac..267ef2d21 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -497,10 +497,10 @@ namespace Terminal.Gui { return representation; // if value is not wide enough - if(representation.Length < availableHorizontalSpace) { + if(representation.Sum(c=>Rune.ColumnWidth(c)) < availableHorizontalSpace) { // pad it out with spaces to the given alignment - int toPad = availableHorizontalSpace - (representation.Length+1 /*leave 1 space for cell boundary*/); + int toPad = availableHorizontalSpace - (representation.Sum(c=>Rune.ColumnWidth(c)) +1 /*leave 1 space for cell boundary*/); switch(colStyle?.GetAlignment(originalCellValue) ?? TextAlignment.Left) { @@ -520,7 +520,7 @@ namespace Terminal.Gui { } // value is too wide - return representation.Substring (0, availableHorizontalSpace); + return new string(representation.TakeWhile(c=>(availableHorizontalSpace-= Rune.ColumnWidth(c))>0).ToArray()); } /// @@ -787,7 +787,7 @@ namespace Terminal.Gui { /// private int CalculateMaxCellWidth(DataColumn col, int rowsToRender,ColumnStyle colStyle) { - int spaceRequired = col.ColumnName.Length; + int spaceRequired = col.ColumnName.Sum(c=>Rune.ColumnWidth(c)); // if table has no rows if(RowOffset < 0) @@ -797,7 +797,7 @@ namespace Terminal.Gui { for (int i = RowOffset; i < RowOffset + rowsToRender && i < Table.Rows.Count; i++) { //expand required space if cell is bigger than the last biggest cell or header - spaceRequired = Math.Max (spaceRequired, GetRepresentation(Table.Rows [i][col],colStyle).Length); + spaceRequired = Math.Max (spaceRequired, GetRepresentation(Table.Rows [i][col],colStyle).Sum(c=>Rune.ColumnWidth(c))); } // Don't require more space than the style allows diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index e18e09bc8..04354baee 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data; using Terminal.Gui; +using System.Globalization; namespace UICatalog.Scenarios { @@ -261,14 +262,16 @@ namespace UICatalog.Scenarios { { var dt = new DataTable(); + int explicitCols = 6; dt.Columns.Add(new DataColumn("StrCol",typeof(string))); dt.Columns.Add(new DataColumn("DateCol",typeof(DateTime))); dt.Columns.Add(new DataColumn("IntCol",typeof(int))); dt.Columns.Add(new DataColumn("DoubleCol",typeof(double))); dt.Columns.Add(new DataColumn("NullsCol",typeof(string))); + dt.Columns.Add(new DataColumn("Unicode",typeof(string))); - for(int i=0;i< cols -5; i++) { - dt.Columns.Add("Column" + (i+5)); + for(int i=0;i< cols -explicitCols; i++) { + dt.Columns.Add("Column" + (i+explicitCols)); } var r = new Random(100); @@ -280,10 +283,11 @@ namespace UICatalog.Scenarios { new DateTime(2000+i,12,25), r.Next(i), (r.NextDouble()*i)-0.5 /*add some negatives to demo styles*/, - DBNull.Value + DBNull.Value, + "Les Mise" + Char.ConvertFromUtf32(Int32.Parse("0301", NumberStyles.HexNumber)) + "rables" }; - for(int j=0;j< cols -5; j++) { + for(int j=0;j< cols -explicitCols; j++) { row.Add("SomeValue" + r.Next(100)); } diff --git a/UnitTests/TableViewTests.cs b/UnitTests/TableViewTests.cs index 481facd06..b702d3334 100644 --- a/UnitTests/TableViewTests.cs +++ b/UnitTests/TableViewTests.cs @@ -2,10 +2,10 @@ using System; using System.Collections.Generic; using System.Data; using System.Linq; -using System.Text; using System.Threading.Tasks; using Terminal.Gui; using Xunit; +using System.Globalization; namespace UnitTests { public class TableViewTests @@ -127,6 +127,19 @@ namespace UnitTests { tableView.SelectedRow = 10; Assert.True(called); } + + [Fact] + public void Test_SumColumnWidth_UnicodeLength() + { + Assert.Equal(11,"hello there".Sum(c=>Rune.ColumnWidth(c))); + + // Creates a string with the peculiar (french?) r symbol + String surrogate = "Les Mise" + Char.ConvertFromUtf32(Int32.Parse("0301", NumberStyles.HexNumber)) + "rables"; + + // The unicode width of this string is shorter than the string length! + Assert.Equal(14,surrogate.Sum(c=>Rune.ColumnWidth(c))); + Assert.Equal(15,surrogate.Length); + } /// /// Builds a simple table of string columns with the requested number of columns and rows From d35b3b8c3d7c228bdc6a18c13985fe83f9c323e1 Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 31 Dec 2020 10:18:05 +0000 Subject: [PATCH 29/47] Added wheel left/right for horizontal scrolling --- Terminal.Gui/Views/TableView.cs | 40 +++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 267ef2d21..7ce5b0e0d 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -583,7 +583,8 @@ namespace Terminal.Gui { public override bool MouseEvent (MouseEvent me) { if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) && - me.Flags != MouseFlags.WheeledDown && me.Flags != MouseFlags.WheeledUp) + me.Flags != MouseFlags.WheeledDown && me.Flags != MouseFlags.WheeledUp && + me.Flags != MouseFlags.WheeledLeft && me.Flags != MouseFlags.WheeledRight) return false; if (!HasFocus && CanFocus) { @@ -594,21 +595,32 @@ namespace Terminal.Gui { return false; } - if (me.Flags == MouseFlags.WheeledDown) { - - RowOffset++; - - EnsureValidScrollOffsets(); - SetNeedsDisplay(); + // Scroll wheel flags + switch(me.Flags) + { + case MouseFlags.WheeledDown: + RowOffset++; + EnsureValidScrollOffsets(); + SetNeedsDisplay(); + return true; - return true; - } else if (me.Flags == MouseFlags.WheeledUp) { - RowOffset--; - - EnsureValidScrollOffsets(); - SetNeedsDisplay(); + case MouseFlags.WheeledUp: + RowOffset--; + EnsureValidScrollOffsets(); + SetNeedsDisplay(); + return true; - return true; + case MouseFlags.WheeledRight: + ColumnOffset++; + EnsureValidScrollOffsets(); + SetNeedsDisplay(); + return true; + + case MouseFlags.WheeledLeft: + ColumnOffset--; + EnsureValidScrollOffsets(); + SetNeedsDisplay(); + return true; } if(me.Flags == MouseFlags.Button1Clicked) { From 913d3a247ed22eb3cd4d18fcc59bfc9eb2e462be Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 31 Dec 2020 13:02:09 +0000 Subject: [PATCH 30/47] Added PositionCursor implementation - Added new methods ScreenToCell and CellToScreen --- Terminal.Gui/Views/TableView.cs | 97 ++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 15 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 7ce5b0e0d..db18988ea 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -579,6 +579,22 @@ namespace Terminal.Gui { return true; } + /// + /// Positions the cursor in the area of the screen in which the start of the active cell is rendered. Calls base implementation if active cell is not visible due to scrolling or table is loaded etc + /// + public override void PositionCursor() + { + if(Table == null) { + base.PositionCursor(); + return; + } + + var screenPoint = CellToScreen(SelectedColumn,SelectedRow); + + if(screenPoint != null) + Move(screenPoint.Value.X,screenPoint.Value.Y); + } + /// public override bool MouseEvent (MouseEvent me) { @@ -625,23 +641,12 @@ namespace Terminal.Gui { if(me.Flags == MouseFlags.Button1Clicked) { - var viewPort = CalculateViewport(Bounds); - - var headerHeight = ShouldRenderHeaders()? GetHeaderHeight():0; + var hit = ScreenToCell(me.OfX,me.OfY); - var col = viewPort.LastOrDefault(c=>c.X <= me.OfX); - - // Click is on the header section of rendered UI - if(me.OfY < headerHeight) - return false; - - var rowIdx = RowOffset - headerHeight + me.OfY; - - if(col != null && rowIdx >= 0) { + if(hit != null) { - SelectedRow = rowIdx; - SelectedColumn = col.Column.Ordinal; - + SelectedRow = hit.Value.Y; + SelectedColumn = hit.Value.X; Update(); } } @@ -649,6 +654,68 @@ namespace Terminal.Gui { return false; } + /// + /// Returns the column and row of that corresponds to a given point on the screen (relative to the control client area). Returns null if the point is in the header, no table is loaded or outside the control bounds + /// + /// X offset from the top left of the control + /// Y offset from the top left of the control + /// + public Point? ScreenToCell (int clientX, int clientY) + { + if(Table == null) + return null; + + var viewPort = CalculateViewport(Bounds); + + var headerHeight = ShouldRenderHeaders()? GetHeaderHeight():0; + + var col = viewPort.LastOrDefault(c=>c.X <= clientX); + + // Click is on the header section of rendered UI + if(clientY < headerHeight) + return null; + + var rowIdx = RowOffset - headerHeight + clientY; + + if(col != null && rowIdx >= 0) { + + return new Point(col.Column.Ordinal,rowIdx); + } + + return null; + } + + /// + /// 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 row in that you are looking for + /// + public Point? CellToScreen (int tableColumn, int tableRow) + { + if(Table == null) + return null; + + var viewPort = CalculateViewport(Bounds); + + var headerHeight = ShouldRenderHeaders()? GetHeaderHeight():0; + + var colHit = viewPort.FirstOrDefault(c=>c.Column.Ordinal == tableColumn); + + // current column is outside the scroll area + if(colHit == null) + return null; + + // the cell is too far up above the current scroll area + if(RowOffset > tableRow) + return null; + + // the cell is way down below the scroll area and off the screen + if(tableRow > RowOffset + (Bounds.Height - headerHeight)) + return null; + + return new Point(colHit.X,tableRow + headerHeight - RowOffset); + } /// /// Updates the view to reflect changes to and to ( / ) etc /// From 31bdec453509e9eeee8ca1694e0d7f6c86749c34 Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 31 Dec 2020 13:09:31 +0000 Subject: [PATCH 31/47] Added FullRowSelect property --- Terminal.Gui/Views/TableView.cs | 15 ++++++++++++--- UICatalog/Scenarios/TableEditor.cs | 8 ++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index db18988ea..d631b5d92 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -147,6 +147,11 @@ namespace Terminal.Gui { /// public TableStyle Style { get => style; set {style = value; Update(); } } + /// + /// True to select the entire row at once. False to select individual cells. Defaults to false + /// + public bool FullRowSelect {get;set;} + /// /// Horizontal scroll offset. The index of the first column in to display when when rendering the view. /// @@ -446,7 +451,9 @@ namespace Terminal.Gui { Move (current.X, row); // Set color scheme based on whether the current cell is the selected one - bool isSelectedCell = rowToRender == SelectedRow && current.Column.Ordinal == SelectedColumn; + bool isSelectedCell = rowToRender == SelectedRow && + (current.Column.Ordinal == SelectedColumn || FullRowSelect); + Driver.SetAttribute (isSelectedCell ? ColorScheme.HotFocus : ColorScheme.Normal); var val = Table.Rows [rowToRender][current.Column]; @@ -456,8 +463,10 @@ namespace Terminal.Gui { Driver.AddStr (TruncateOrPad(val,representation,availableWidthForCell,colStyle)); - // Reset color scheme to normal and render the vertical line (or space) at the end of the cell - Driver.SetAttribute (ColorScheme.Normal); + // If not in full row select mode always, reset color scheme to normal and render the vertical line (or space) at the end of the cell + if(!FullRowSelect) + Driver.SetAttribute (ColorScheme.Normal); + RenderSeparator(current.X-1,row,false); } diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 04354baee..863dc774b 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -20,6 +20,7 @@ namespace UICatalog.Scenarios { private MenuItem miHeaderMidline; private MenuItem miHeaderUnderline; private MenuItem miCellLines; + private MenuItem miFullRowSelect; public override void Setup () { @@ -47,6 +48,7 @@ namespace UICatalog.Scenarios { miHeaderOverline = new MenuItem ("_HeaderOverLine", "", () => ToggleOverline()){Checked = tableView.Style.ShowHorizontalHeaderOverline, CheckType = MenuItemCheckStyle.Checked }, miHeaderMidline = new MenuItem ("_HeaderMidLine", "", () => ToggleHeaderMidline()){Checked = tableView.Style.ShowVerticalHeaderLines, CheckType = MenuItemCheckStyle.Checked }, miHeaderUnderline =new MenuItem ("_HeaderUnderLine", "", () => ToggleUnderline()){Checked = tableView.Style.ShowHorizontalHeaderUnderline, CheckType = MenuItemCheckStyle.Checked }, + miFullRowSelect =new MenuItem ("_FullRowSelect", "", () => ToggleFullRowSelect()){Checked = tableView.FullRowSelect, CheckType = MenuItemCheckStyle.Checked }, miCellLines =new MenuItem ("_CellLines", "", () => ToggleCellLines()){Checked = tableView.Style.ShowVerticalCellLines, CheckType = MenuItemCheckStyle.Checked }, new MenuItem ("_AllLines", "", () => ToggleAllCellLines()), new MenuItem ("_NoLines", "", () => ToggleNoCellLines()), @@ -114,6 +116,12 @@ namespace UICatalog.Scenarios { tableView.Style.ShowHorizontalHeaderUnderline = miHeaderUnderline.Checked; tableView.Update(); } + private void ToggleFullRowSelect () + { + miFullRowSelect.Checked = !miFullRowSelect.Checked; + tableView.FullRowSelect= miFullRowSelect.Checked; + tableView.Update(); + } private void ToggleCellLines() { miCellLines.Checked = !miCellLines.Checked; From df3e191a72dfd67dede5b029abb2a1d384bf4825 Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 31 Dec 2020 13:38:15 +0000 Subject: [PATCH 32/47] Added CellActivated event which occurs on Enter or double click --- Terminal.Gui/Views/TableView.cs | 69 ++++++++++++++++++++++++++++++ UICatalog/Scenarios/TableEditor.cs | 16 +++---- 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index d631b5d92..8b185bc93 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -224,6 +224,16 @@ namespace Terminal.Gui { /// public event Action SelectedCellChanged; + /// + /// This event is raised when a cell is activated e.g. by double clicking or pressing + /// + public event Action CellActivated; + + /// + /// The key which when pressed should trigger event. Defaults to Enter. + /// + public Key CellActivationKey {get;set;} = Key.Enter; + /// /// Initialzies a class using layout. /// @@ -535,6 +545,12 @@ namespace Terminal.Gui { /// public override bool ProcessKey (KeyEvent keyEvent) { + if(keyEvent.Key == CellActivationKey && Table != null) { + OnCellActivated(new CellActivatedEventArgs(Table,SelectedColumn,SelectedRow)); + return true; + } + + switch (keyEvent.Key) { case Key.CursorLeft: SelectedColumn--; @@ -660,6 +676,14 @@ namespace Terminal.Gui { } } + // Double clicking a cell activates + if(me.Flags == MouseFlags.Button1DoubleClicked) { + var hit = ScreenToCell(me.OfX,me.OfY); + if(hit!= null) { + OnCellActivated(new CellActivatedEventArgs(Table,hit.Value.X,hit.Value.Y)); + } + } + return false; } @@ -813,6 +837,15 @@ namespace Terminal.Gui { { SelectedCellChanged?.Invoke(args); } + + /// + /// Invokes the event + /// + /// + protected virtual void OnCellActivated (CellActivatedEventArgs args) + { + CellActivated?.Invoke(args); + } /// /// Calculates which columns should be rendered given the in which to display and the @@ -1004,4 +1037,40 @@ namespace Terminal.Gui { NewRow = newRow; } } + + /// + /// Defines the event arguments for event + /// + 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;} + + + /// + /// The column index of the cell that is being activated + /// + /// + public int Col {get;} + + /// + /// The row index of the cell that is being activated + /// + /// + public int Row {get;} + + /// + /// Creates a new instance of arguments describing a cell being activated in + /// + /// + public CellActivatedEventArgs(DataTable t, int col, int row) + { + Table = t; + Col = col; + Row = row; + } + } } diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 863dc774b..a07212030 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -62,9 +62,8 @@ namespace UICatalog.Scenarios { var statusBar = new StatusBar (new StatusItem [] { //new StatusItem(Key.Enter, "~ENTER~ ApplyEdits", () => { _hexView.ApplyEdits(); }), new StatusItem(Key.F2, "~F2~ OpenExample", () => OpenExample(true)), - new StatusItem(Key.F3, "~F3~ EditCell", () => EditCurrentCell()), - new StatusItem(Key.F4, "~F4~ CloseExample", () => CloseExample()), - new StatusItem(Key.F5, "~F5~ OpenSimple", () => OpenSimple(true)), + new StatusItem(Key.F3, "~F3~ CloseExample", () => CloseExample()), + new StatusItem(Key.F4, "~F4~ OpenSimple", () => OpenSimple(true)), new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()), }); Top.Add (statusBar); @@ -83,6 +82,7 @@ namespace UICatalog.Scenarios { Win.Add(selectedCellLabel); tableView.SelectedCellChanged += (e)=>{selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}";}; + tableView.CellActivated += EditCurrentCell; } private void ClearColumnStyles () @@ -214,12 +214,12 @@ namespace UICatalog.Scenarios { tableView.Table = BuildSimpleDataTable(big ? 30 : 5, big ? 1000 : 5); } - private void EditCurrentCell () + private void EditCurrentCell (CellActivatedEventArgs e) { - if(tableView.Table == null) + if(e.Table == null) return; - var oldValue = tableView.Table.Rows[tableView.SelectedRow][tableView.SelectedColumn].ToString(); + var oldValue = e.Table.Rows[e.Row][e.Col].ToString(); bool okPressed = false; var ok = new Button ("Ok", is_default: true); @@ -231,7 +231,7 @@ namespace UICatalog.Scenarios { var lbl = new Label() { X = 0, Y = 1, - Text = tableView.Table.Columns[tableView.SelectedColumn].ColumnName + Text = e.Table.Columns[e.Col].ColumnName }; var tf = new TextField() @@ -250,7 +250,7 @@ namespace UICatalog.Scenarios { if(okPressed) { try { - tableView.Table.Rows[tableView.SelectedRow][tableView.SelectedColumn] = string.IsNullOrWhiteSpace(tf.Text.ToString()) ? DBNull.Value : (object)tf.Text; + e.Table.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"); From 54eab6eb56b5a44182da3d4b858caf2473f1fb8d Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 2 Jan 2021 15:57:41 +0000 Subject: [PATCH 33/47] Added multi select mode --- Terminal.Gui/Views/TableView.cs | 125 ++++++++++++++++++++++++++++++-- 1 file changed, 119 insertions(+), 6 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index d631b5d92..b7168c638 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -152,6 +152,18 @@ namespace Terminal.Gui { /// public bool FullRowSelect {get;set;} + /// + /// True to allow regions to be selected + /// + /// + public bool MultiSelect {get;set;} = true; + + /// + /// When is enabled this property contain all rectangles of selected cells. Rectangles describe column/rows selected in (not screen coordinates) + /// + /// + public Stack MultiSelectedRegions {get;} = new Stack(); + /// /// Horizontal scroll offset. The index of the first column in to display when when rendering the view. /// @@ -451,8 +463,7 @@ namespace Terminal.Gui { Move (current.X, row); // Set color scheme based on whether the current cell is the selected one - bool isSelectedCell = rowToRender == SelectedRow && - (current.Column.Ordinal == SelectedColumn || FullRowSelect); + bool isSelectedCell = IsSelected(current.Column.Ordinal,rowToRender); Driver.SetAttribute (isSelectedCell ? ColorScheme.HotFocus : ColorScheme.Normal); @@ -537,19 +548,23 @@ namespace Terminal.Gui { { switch (keyEvent.Key) { case Key.CursorLeft: - SelectedColumn--; + case Key.ShiftMask | Key.CursorLeft: + ChangeSelectionByOffset(-1,0,keyEvent.Key.HasFlag(Key.ShiftMask)); Update (); break; case Key.CursorRight: - SelectedColumn++; + case Key.ShiftMask | Key.CursorRight: + ChangeSelectionByOffset(1,0,keyEvent.Key.HasFlag(Key.ShiftMask)); Update (); break; case Key.CursorDown: - SelectedRow++; + case Key.ShiftMask | Key.CursorDown: + ChangeSelectionByOffset(0,1,keyEvent.Key.HasFlag(Key.ShiftMask)); Update (); break; case Key.CursorUp: - SelectedRow--; + case Key.ShiftMask | Key.CursorUp: + ChangeSelectionByOffset(0,-1,keyEvent.Key.HasFlag(Key.ShiftMask)); Update (); break; case Key.PageUp: @@ -588,6 +603,74 @@ namespace Terminal.Gui { return true; } + private void ChangeSelectionByOffset (int offsetX, int offsetY, bool extendExistingSelection) + { + if(!MultiSelect || !extendExistingSelection) + MultiSelectedRegions.Clear(); + + if(extendExistingSelection) + { + // If we are extending current selection but there isn't one + if(MultiSelectedRegions.Count == 0) + { + // Create a new region between the old active cell and the new offset + var rect = CreateTableSelection(SelectedColumn,SelectedRow,SelectedColumn+offsetX,SelectedRow+offsetY); + MultiSelectedRegions.Push(rect); + } + else + { + // Extend the current head selection to include the new offset + var head = MultiSelectedRegions.Pop(); + var newRect = CreateTableSelection(head.Origin.X,head.Origin.Y,SelectedColumn + offsetX,SelectedRow + offsetY); + MultiSelectedRegions.Push(newRect); + } + } + + SelectedColumn += offsetX; + SelectedRow += offsetY; + + } + + + /// + /// Returns a new rectangle between the two points with positive width/height regardless of relative positioning of the points. pt1 is always considered the point + /// + /// Origin point for the selection in X + /// Origin point for the selection in Y + /// End point for the selection in X + /// End point for the selection in Y + /// + private TableSelection CreateTableSelection (int pt1X, int pt1Y, int pt2X, int pt2Y) + { + var top = Math.Min(pt1Y,pt2Y); + var bot = Math.Max(pt1Y,pt2Y); + + var left = Math.Min(pt1X,pt2X); + var right = Math.Max(pt1X,pt2X); + + // Rect class is inclusive of Top Left but exclusive of Bottom Right so extend by 1 + return new TableSelection(new Point(pt1X,pt1Y),new Rect(left,top,right-left + 1,bot-top + 1)); + } + + /// + /// Returns true if the given cell is selected either because it is the active cell or part of a multi cell selection (e.g. ) + /// + /// + /// + /// + public bool IsSelected(int col, int row) + { + // Cell is also selected if + if(MultiSelect && MultiSelectedRegions.Any(r=>r.Rect.Contains(col,row))) + return true; + + return row == SelectedRow && + (col == SelectedColumn || FullRowSelect); + } + + + + /// /// Positions the cursor in the area of the screen in which the start of the active cell is rendered. Calls base implementation if active cell is not visible due to scrolling or table is loaded etc /// @@ -1004,4 +1087,34 @@ namespace Terminal.Gui { NewRow = newRow; } } + + /// + /// Describes a selected region of the table + /// + public class TableSelection{ + + /// + /// Corner of the where selection began + /// + /// + public Point Origin{get;} + + /// + /// Area selected + /// + /// + public Rect Rect { get; } + + /// + /// Creates a new selected area starting at the origin corner and covering the provided rectangular area + /// + /// + /// + public TableSelection(Point origin, Rect rect) + { + Origin = origin; + Rect = rect; + } + + } } From b313dd8396dc6e426839975e8535300b3fd695c3 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 2 Jan 2021 18:58:36 +0000 Subject: [PATCH 34/47] Mouse, Home, End etc work properly with multi select now --- Terminal.Gui/Views/TableView.cs | 86 +++++++++++++++++++++------------ 1 file changed, 56 insertions(+), 30 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index b7168c638..9c8e8734e 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -546,53 +546,64 @@ namespace Terminal.Gui { /// public override bool ProcessKey (KeyEvent keyEvent) { + if(Table == null){ + PositionCursor (); + return true; + } + switch (keyEvent.Key) { case Key.CursorLeft: - case Key.ShiftMask | Key.CursorLeft: + case Key.CursorLeft | Key.ShiftMask: ChangeSelectionByOffset(-1,0,keyEvent.Key.HasFlag(Key.ShiftMask)); Update (); break; case Key.CursorRight: - case Key.ShiftMask | Key.CursorRight: + case Key.CursorRight | Key.ShiftMask: ChangeSelectionByOffset(1,0,keyEvent.Key.HasFlag(Key.ShiftMask)); Update (); break; case Key.CursorDown: - case Key.ShiftMask | Key.CursorDown: + case Key.CursorDown | Key.ShiftMask: ChangeSelectionByOffset(0,1,keyEvent.Key.HasFlag(Key.ShiftMask)); Update (); break; case Key.CursorUp: - case Key.ShiftMask | Key.CursorUp: + case Key.CursorUp | Key.ShiftMask: ChangeSelectionByOffset(0,-1,keyEvent.Key.HasFlag(Key.ShiftMask)); Update (); break; case Key.PageUp: - SelectedRow -= Frame.Height; + case Key.PageUp | Key.ShiftMask: + ChangeSelectionByOffset(0,-Frame.Height,keyEvent.Key.HasFlag(Key.ShiftMask)); Update (); break; case Key.PageDown: - SelectedRow += Frame.Height; + case Key.PageDown | Key.ShiftMask: + ChangeSelectionByOffset(0,Frame.Height,keyEvent.Key.HasFlag(Key.ShiftMask)); Update (); break; case Key.Home | Key.CtrlMask: - SelectedRow = 0; - SelectedColumn = 0; + case Key.Home | Key.CtrlMask | Key.ShiftMask: + // jump to table origin + SetSelection(0,0,keyEvent.Key.HasFlag(Key.ShiftMask)); Update (); break; case Key.Home: - SelectedColumn = 0; + case Key.Home | Key.ShiftMask: + // jump to start of line + SetSelection(0,SelectedRow,keyEvent.Key.HasFlag(Key.ShiftMask)); Update (); break; case Key.End | Key.CtrlMask: - //jump to end of table - SelectedRow = Table == null ? 0 : Table.Rows.Count - 1; - SelectedColumn = Table == null ? 0 : Table.Columns.Count - 1; + case Key.End | Key.CtrlMask | Key.ShiftMask: + // jump to end of table + SetSelection(Table.Columns.Count - 1, Table.Rows.Count - 1, keyEvent.Key.HasFlag(Key.ShiftMask)); Update (); break; case Key.End: + case Key.End | Key.ShiftMask: //jump to end of row - SelectedColumn = Table == null ? 0 : Table.Columns.Count - 1; + SetSelection(Table.Columns.Count - 1,SelectedRow, keyEvent.Key.HasFlag(Key.ShiftMask)); Update (); break; default: @@ -603,7 +614,13 @@ namespace Terminal.Gui { return true; } - private void ChangeSelectionByOffset (int offsetX, int offsetY, bool extendExistingSelection) + /// + /// Moves the and to the given col/row in . Optionally starting a box selection (see ) + /// + /// + /// + /// True to create a multi cell selection or adjust an existing one + public void SetSelection (int col, int row, bool extendExistingSelection) { if(!MultiSelect || !extendExistingSelection) MultiSelectedRegions.Clear(); @@ -613,22 +630,32 @@ namespace Terminal.Gui { // If we are extending current selection but there isn't one if(MultiSelectedRegions.Count == 0) { - // Create a new region between the old active cell and the new offset - var rect = CreateTableSelection(SelectedColumn,SelectedRow,SelectedColumn+offsetX,SelectedRow+offsetY); + // Create a new region between the old active cell and the new cell + var rect = CreateTableSelection(SelectedColumn,SelectedRow,col,row); MultiSelectedRegions.Push(rect); } else { - // Extend the current head selection to include the new offset + // Extend the current head selection to include the new cell var head = MultiSelectedRegions.Pop(); - var newRect = CreateTableSelection(head.Origin.X,head.Origin.Y,SelectedColumn + offsetX,SelectedRow + offsetY); + var newRect = CreateTableSelection(head.Origin.X,head.Origin.Y,col,row); MultiSelectedRegions.Push(newRect); } } - SelectedColumn += offsetX; - SelectedRow += offsetY; - + SelectedColumn = col; + SelectedRow = row; + } + + /// + /// Moves the and by the provided offsets. Optionally starting a box selection (see ) + /// + /// Offset in number of columns + /// Offset in number of rows + /// True to create a multi cell selection or adjust an existing one + public void ChangeSelectionByOffset (int offsetX, int offsetY, bool extendExistingSelection) + { + SetSelection(SelectedColumn + offsetX, SelectedRow + offsetY,extendExistingSelection); } @@ -660,17 +687,18 @@ namespace Terminal.Gui { /// public bool IsSelected(int col, int row) { - // Cell is also selected if + // Cell is also selected if in any multi selection region if(MultiSelect && MultiSelectedRegions.Any(r=>r.Rect.Contains(col,row))) return true; + // Cell is also selected if Y axis appears in any region (when FullRowSelect is enabled) + if(FullRowSelect && MultiSelect && MultiSelectedRegions.Any(r=>r.Rect.Bottom> row && r.Rect.Top <= row)) + return true; + return row == SelectedRow && (col == SelectedColumn || FullRowSelect); } - - - /// /// Positions the cursor in the area of the screen in which the start of the active cell is rendered. Calls base implementation if active cell is not visible due to scrolling or table is loaded etc /// @@ -731,14 +759,12 @@ namespace Terminal.Gui { return true; } - if(me.Flags == MouseFlags.Button1Clicked) { + if(me.Flags.HasFlag(MouseFlags.Button1Clicked)) { var hit = ScreenToCell(me.OfX,me.OfY); - if(hit != null) { - - SelectedRow = hit.Value.Y; - SelectedColumn = hit.Value.X; + // TODO : if shift is held down extend selection + SetSelection(hit.Value.X,hit.Value.Y,false ); Update(); } } From 0c2685bcbe1628ee3ba75283569623a251543428 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 2 Jan 2021 19:20:00 +0000 Subject: [PATCH 35/47] Added test for box selection --- UnitTests/TableViewTests.cs | 84 +++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/UnitTests/TableViewTests.cs b/UnitTests/TableViewTests.cs index b702d3334..9a2d0a791 100644 --- a/UnitTests/TableViewTests.cs +++ b/UnitTests/TableViewTests.cs @@ -140,6 +140,90 @@ namespace UnitTests { Assert.Equal(14,surrogate.Sum(c=>Rune.ColumnWidth(c))); Assert.Equal(15,surrogate.Length); } + + [Fact] + public void IsSelected_MultiSelectionOn_Vertical() + { + var tableView = new TableView(){ + Table = BuildTable(25,50), + MultiSelect = true + }; + + // 3 cell vertical selection + tableView.SetSelection(1,1,false); + tableView.SetSelection(1,3,true); + + Assert.False(tableView.IsSelected(0,0)); + Assert.False(tableView.IsSelected(1,0)); + Assert.False(tableView.IsSelected(2,0)); + + Assert.False(tableView.IsSelected(0,1)); + Assert.True(tableView.IsSelected(1,1)); + Assert.False(tableView.IsSelected(2,1)); + + Assert.False(tableView.IsSelected(0,2)); + Assert.True(tableView.IsSelected(1,2)); + Assert.False(tableView.IsSelected(2,2)); + + Assert.False(tableView.IsSelected(0,3)); + Assert.True(tableView.IsSelected(1,3)); + Assert.False(tableView.IsSelected(2,3)); + + Assert.False(tableView.IsSelected(0,4)); + Assert.False(tableView.IsSelected(1,4)); + Assert.False(tableView.IsSelected(2,4)); + } + + + [Fact] + public void IsSelected_MultiSelectionOn_Horizontal() + { + var tableView = new TableView(){ + Table = BuildTable(25,50), + MultiSelect = true + }; + + // 2 cell horizontal selection + tableView.SetSelection(1,0,false); + tableView.SetSelection(2,0,true); + + Assert.False(tableView.IsSelected(0,0)); + Assert.True(tableView.IsSelected(1,0)); + Assert.True(tableView.IsSelected(2,0)); + Assert.False(tableView.IsSelected(3,0)); + + Assert.False(tableView.IsSelected(0,1)); + Assert.False(tableView.IsSelected(1,1)); + Assert.False(tableView.IsSelected(2,1)); + Assert.False(tableView.IsSelected(3,1)); + } + + + + [Fact] + public void IsSelected_MultiSelectionOn_BoxSelection() + { + var tableView = new TableView(){ + Table = BuildTable(25,50), + MultiSelect = true + }; + + // 4 cell horizontal in box 2x2 + tableView.SetSelection(0,0,false); + tableView.SetSelection(1,1,true); + + Assert.True(tableView.IsSelected(0,0)); + Assert.True(tableView.IsSelected(1,0)); + Assert.False(tableView.IsSelected(2,0)); + + Assert.True(tableView.IsSelected(0,1)); + Assert.True(tableView.IsSelected(1,1)); + Assert.False(tableView.IsSelected(2,1)); + + Assert.False(tableView.IsSelected(0,2)); + Assert.False(tableView.IsSelected(1,2)); + Assert.False(tableView.IsSelected(2,2)); + } /// /// Builds a simple table of string columns with the requested number of columns and rows From b2e54ec83dcebbfa78adfa802f6faf7b656dd1a2 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 3 Jan 2021 10:28:27 +0000 Subject: [PATCH 36/47] Fixed page up/down offset and added test --- Terminal.Gui/Views/TableView.cs | 23 ++++++++++++++++------- UnitTests/TableViewTests.cs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index c7ed425d1..23bf89b99 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -330,6 +330,15 @@ namespace Terminal.Gui { Driver.AddStr (new string (' ', width)); } + /// + /// Returns the amount of vertical space currently occupied by the header or 0 if it is not visible. + /// + /// + private int GetHeaderHeightIfAny() + { + return ShouldRenderHeaders()? GetHeaderHeight():0; + } + /// /// Returns the amount of vertical space required to display the header /// @@ -589,12 +598,12 @@ namespace Terminal.Gui { break; case Key.PageUp: case Key.PageUp | Key.ShiftMask: - ChangeSelectionByOffset(0,-Frame.Height,keyEvent.Key.HasFlag(Key.ShiftMask)); + ChangeSelectionByOffset(0,-(Bounds.Height - GetHeaderHeightIfAny()),keyEvent.Key.HasFlag(Key.ShiftMask)); Update (); break; case Key.PageDown: case Key.PageDown | Key.ShiftMask: - ChangeSelectionByOffset(0,Frame.Height,keyEvent.Key.HasFlag(Key.ShiftMask)); + ChangeSelectionByOffset(0,Bounds.Height - GetHeaderHeightIfAny(),keyEvent.Key.HasFlag(Key.ShiftMask)); Update (); break; case Key.Home | Key.CtrlMask: @@ -778,8 +787,8 @@ namespace Terminal.Gui { var hit = ScreenToCell(me.OfX,me.OfY); if(hit != null) { - // TODO : if shift is held down extend selection - SetSelection(hit.Value.X,hit.Value.Y,false ); + + SetSelection(hit.Value.X,hit.Value.Y,me.Flags.HasFlag(MouseFlags.ButtonShift)); Update(); } } @@ -808,7 +817,7 @@ namespace Terminal.Gui { var viewPort = CalculateViewport(Bounds); - var headerHeight = ShouldRenderHeaders()? GetHeaderHeight():0; + var headerHeight = GetHeaderHeightIfAny(); var col = viewPort.LastOrDefault(c=>c.X <= clientX); @@ -839,7 +848,7 @@ namespace Terminal.Gui { var viewPort = CalculateViewport(Bounds); - var headerHeight = ShouldRenderHeaders()? GetHeaderHeight():0; + var headerHeight = GetHeaderHeightIfAny(); var colHit = viewPort.FirstOrDefault(c=>c.Column.Ordinal == tableColumn); @@ -916,7 +925,7 @@ namespace Terminal.Gui { } var columnsToRender = CalculateViewport (Bounds).ToArray(); - var headerHeight = ShouldRenderHeaders()? GetHeaderHeight() : 0; + var headerHeight = GetHeaderHeightIfAny(); //if we have scrolled too far to the left if (SelectedColumn < columnsToRender.Min (r => r.Column.Ordinal)) { diff --git a/UnitTests/TableViewTests.cs b/UnitTests/TableViewTests.cs index 9a2d0a791..143397908 100644 --- a/UnitTests/TableViewTests.cs +++ b/UnitTests/TableViewTests.cs @@ -224,6 +224,39 @@ namespace UnitTests { Assert.False(tableView.IsSelected(1,2)); Assert.False(tableView.IsSelected(2,2)); } + + [Fact] + public void PageDown_ExcludesHeaders() + { + + var driver = new FakeDriver (); + Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true))); + driver.Init (() => { }); + + + var tableView = new TableView(){ + Table = BuildTable(25,50), + MultiSelect = true, + Bounds = new Rect(0,0,10,5) + }; + + // Header should take up 2 lines + tableView.Style.ShowHorizontalHeaderOverline = false; + tableView.Style.ShowHorizontalHeaderUnderline = true; + tableView.Style.AlwaysShowHeaders = false; + + Assert.Equal(0,tableView.RowOffset); + + tableView.ProcessKey(new KeyEvent(Key.PageDown,new KeyModifiers())); + + // window height is 5 rows 2 are header so page down should give 3 new rows + Assert.Equal(3,tableView.RowOffset); + + // header is no longer visible so page down should give 5 new rows + tableView.ProcessKey(new KeyEvent(Key.PageDown,new KeyModifiers())); + + Assert.Equal(8,tableView.RowOffset); + } /// /// Builds a simple table of string columns with the requested number of columns and rows From d988a3444437d5c4aa9603e45bbfb1c41f7a6406 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 3 Jan 2021 21:57:00 +0000 Subject: [PATCH 37/47] Added SelectAll, better selection iteration and validation --- Terminal.Gui/Views/TableView.cs | 111 +++++++++++++++++++- UICatalog/Scenarios/TableEditor.cs | 29 +++++- UnitTests/TableViewTests.cs | 159 +++++++++++++++++++++++++++++ 3 files changed, 294 insertions(+), 5 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 23bf89b99..67f897dc2 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -624,6 +624,10 @@ namespace Terminal.Gui { SetSelection(Table.Columns.Count - 1, Table.Rows.Count - 1, keyEvent.Key.HasFlag(Key.ShiftMask)); Update (); break; + case Key.A | Key.CtrlMask: + SelectAll(); + Update (); + break; case Key.End: case Key.End | Key.ShiftMask: //jump to end of row @@ -682,6 +686,71 @@ namespace Terminal.Gui { SetSelection(SelectedColumn + offsetX, SelectedRow + offsetY,extendExistingSelection); } + /// + /// When is on, creates selection over all cells in the table (replacing any old selection regions) + /// + public void SelectAll() + { + if(Table == null || !MultiSelect || Table.Rows.Count == 0) + return; + + MultiSelectedRegions.Clear(); + + // 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))); + Update(); + } + + /// + /// Returns all cells in any (if is enabled) and the selected cell + /// + /// + public IEnumerable GetAllSelectedCells() + { + if(Table == null || Table.Rows.Count == 0) + yield break; + + EnsureValidSelection(); + + // If there are one or more rectangular selections + if(MultiSelect && MultiSelectedRegions.Any()){ + + // Quiz any cells for whether they are selected. For performance we only need to check those between the top left and lower right vertex of selection regions + var yMin = MultiSelectedRegions.Min(r=>r.Rect.Top); + 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); + + for(int y = yMin ; y < yMax ; y++) + { + for(int x = xMin ; x < xMax ; x++) + { + if(IsSelected(x,y)){ + yield return new Point(x,y); + } + } + } + } + else{ + + // if there are no region selections then it is just the active cell + + // if we are selecting the full row + if(FullRowSelect) + { + // all cells in active row are selected + for(int x =0;x /// Returns a new rectangle between the two points with positive width/height regardless of relative positioning of the points. pt1 is always considered the point @@ -901,17 +970,51 @@ namespace Terminal.Gui { /// - /// Updates and where they are outside the bounds of the table (by adjusting them to the nearest existing cell). Has no effect if has not been set. + /// Updates , and where they are outside the bounds of the table (by adjusting them to the nearest existing cell). Has no effect if has not been set. /// /// Changes will not be immediately visible in the display until you call - public void EnsureValidSelection () + public void EnsureValidSelection() { if(Table == null){ + + // Table doesn't exist, we should probably clear those selections + MultiSelectedRegions.Clear(); return; } SelectedColumn = Math.Max(Math.Min(SelectedColumn,Table.Columns.Count -1),0); SelectedRow = Math.Max(Math.Min(SelectedRow,Table.Rows.Count -1),0); + + var oldRegions = MultiSelectedRegions.ToArray().Reverse(); + + MultiSelectedRegions.Clear(); + + // evaluate + foreach(var region in oldRegions) + { + // ignore regions entirely below current table state + if(region.Rect.Top >= Table.Rows.Count) + continue; + + // ignore regions entirely too far right of table columns + if(region.Rect.Left >= Table.Columns.Count) + 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)); + + // 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) + ); + + MultiSelectedRegions.Push(region); + } + } /// @@ -1165,13 +1268,13 @@ namespace Terminal.Gui { /// Corner of the where selection began /// /// - public Point Origin{get;} + public Point Origin{get;set;} /// /// Area selected /// /// - public Rect Rect { get; } + public Rect Rect { get; set;} /// /// Creates a new selected area starting at the origin corner and covering the provided rectangular area diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index a07212030..839a95013 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data; using Terminal.Gui; +using System.Linq; using System.Globalization; namespace UICatalog.Scenarios { @@ -60,7 +61,6 @@ namespace UICatalog.Scenarios { var statusBar = new StatusBar (new StatusItem [] { - //new StatusItem(Key.Enter, "~ENTER~ ApplyEdits", () => { _hexView.ApplyEdits(); }), new StatusItem(Key.F2, "~F2~ OpenExample", () => OpenExample(true)), new StatusItem(Key.F3, "~F3~ CloseExample", () => CloseExample()), new StatusItem(Key.F4, "~F4~ OpenSimple", () => OpenSimple(true)), @@ -83,6 +83,33 @@ namespace UICatalog.Scenarios { tableView.SelectedCellChanged += (e)=>{selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}";}; tableView.CellActivated += EditCurrentCell; + tableView.KeyPress += TableViewKeyPress; + } + + private void TableViewKeyPress (View.KeyEventEventArgs e) + { + if(e.KeyEvent.Key == Key.DeleteChar){ + + 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); + } + else{ + + // otherwise set all selected cells to null + foreach(var pt in tableView.GetAllSelectedCells()) + { + tableView.Table.Rows[pt.Y][pt.X] = DBNull.Value; + } + } + + tableView.Update(); + e.Handled = true; + } + + } private void ClearColumnStyles () diff --git a/UnitTests/TableViewTests.cs b/UnitTests/TableViewTests.cs index 143397908..fa39627fd 100644 --- a/UnitTests/TableViewTests.cs +++ b/UnitTests/TableViewTests.cs @@ -257,7 +257,166 @@ namespace UnitTests { Assert.Equal(8,tableView.RowOffset); } + + [Fact] + public void DeleteRow_SelectAll_AdjustsSelectionToPreventOverrun() + { + // create a 4 by 4 table + var tableView = new TableView(){ + Table = BuildTable(4,4), + MultiSelect = true, + Bounds = new Rect(0,0,10,5) + }; + + tableView.SelectAll(); + Assert.Equal(16,tableView.GetAllSelectedCells().Count()); + + // delete one of the columns + tableView.Table.Columns.RemoveAt(2); + + // table should now be 3x4 + Assert.Equal(12,tableView.GetAllSelectedCells().Count()); + + // remove a row + tableView.Table.Rows.RemoveAt(1); + + // table should now be 3x3 + Assert.Equal(9,tableView.GetAllSelectedCells().Count()); + } + + + [Fact] + public void DeleteRow_SelectLastRow_AdjustsSelectionToPreventOverrun() + { + // create a 4 by 4 table + var tableView = new TableView(){ + Table = BuildTable(4,4), + MultiSelect = true, + Bounds = new Rect(0,0,10,5) + }; + + // select the last row + tableView.MultiSelectedRegions.Clear(); + tableView.MultiSelectedRegions.Push(new TableSelection(new Point(0,3), new Rect(0,3,4,1))); + + Assert.Equal(4,tableView.GetAllSelectedCells().Count()); + + // remove a row + tableView.Table.Rows.RemoveAt(0); + + tableView.EnsureValidSelection(); + + // since the selection no longer exists it should be removed + Assert.Empty(tableView.MultiSelectedRegions); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GetAllSelectedCells_SingleCellSelected_ReturnsOne(bool multiSelect) + { + var tableView = new TableView(){ + Table = BuildTable(3,3), + MultiSelect = multiSelect, + Bounds = new Rect(0,0,10,5) + }; + + tableView.SetSelection(1,1,false); + + Assert.Single(tableView.GetAllSelectedCells()); + Assert.Equal(new Point(1,1),tableView.GetAllSelectedCells().Single()); + } + + + [Fact] + public void GetAllSelectedCells_SquareSelection_ReturnsFour() + { + var tableView = new TableView(){ + Table = BuildTable(3,3), + MultiSelect = true, + Bounds = new Rect(0,0,10,5) + }; + + // move cursor to 1,1 + tableView.SetSelection(1,1,false); + // spread selection across to 2,2 (e.g. shift+right then shift+down) + tableView.SetSelection(2,2,true); + + var selected = tableView.GetAllSelectedCells().ToArray(); + + Assert.Equal(4,selected.Length); + Assert.Equal(new Point(1,1),selected[0]); + Assert.Equal(new Point(2,1),selected[1]); + Assert.Equal(new Point(1,2),selected[2]); + Assert.Equal(new Point(2,2),selected[3]); + } + + [Fact] + public void GetAllSelectedCells_SquareSelection_FullRowSelect() + { + var tableView = new TableView(){ + Table = BuildTable(3,3), + MultiSelect = true, + FullRowSelect = true, + Bounds = new Rect(0,0,10,5) + }; + + // move cursor to 1,1 + tableView.SetSelection(1,1,false); + // spread selection across to 2,2 (e.g. shift+right then shift+down) + tableView.SetSelection(2,2,true); + + var selected = tableView.GetAllSelectedCells().ToArray(); + + Assert.Equal(6,selected.Length); + Assert.Equal(new Point(0,1),selected[0]); + Assert.Equal(new Point(1,1),selected[1]); + Assert.Equal(new Point(2,1),selected[2]); + Assert.Equal(new Point(0,2),selected[3]); + Assert.Equal(new Point(1,2),selected[4]); + Assert.Equal(new Point(2,2),selected[5]); + } + + + [Fact] + public void GetAllSelectedCells_TwoIsolatedSelections_ReturnsSix() + { + var tableView = new TableView(){ + Table = BuildTable(20,20), + MultiSelect = true, + Bounds = new Rect(0,0,10,5) + }; + + /* + Sets up disconnected selections like: + + 00000000000 + 01100000000 + 01100000000 + 00000001100 + 00000000000 + */ + + tableView.MultiSelectedRegions.Clear(); + tableView.MultiSelectedRegions.Push(new TableSelection(new Point(1,1),new Rect(1,1,2,2))); + tableView.MultiSelectedRegions.Push(new TableSelection(new Point(7,3),new Rect(7,3,2,1))); + + tableView.SelectedColumn = 8; + tableView.SelectedRow = 3; + + var selected = tableView.GetAllSelectedCells().ToArray(); + + Assert.Equal(6,selected.Length); + + Assert.Equal(new Point(1,1),selected[0]); + Assert.Equal(new Point(2,1),selected[1]); + Assert.Equal(new Point(1,2),selected[2]); + Assert.Equal(new Point(2,2),selected[3]); + Assert.Equal(new Point(7,3),selected[4]); + Assert.Equal(new Point(8,3),selected[5]); + } + /// /// Builds a simple table of string columns with the requested number of columns and rows /// From 1549d775c548a4990119edd62c1b843fb0d4479e Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 27 Jan 2021 08:29:12 +0000 Subject: [PATCH 38/47] Added vertical scrollbars --- UICatalog/Scenarios/TableEditor.cs | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 839a95013..9e32e9ca9 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -84,6 +84,38 @@ namespace UICatalog.Scenarios { tableView.SelectedCellChanged += (e)=>{selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}";}; tableView.CellActivated += EditCurrentCell; tableView.KeyPress += TableViewKeyPress; + + SetupScrollBar(); + } + + private void SetupScrollBar () + { + var _scrollBar = new ScrollBarView (tableView, true); + + _scrollBar.ChangedPosition += () => { + tableView.RowOffset = _scrollBar.Position; + if (tableView.RowOffset != _scrollBar.Position) { + _scrollBar.Position = tableView.RowOffset; + } + tableView.SetNeedsDisplay (); + }; + /* + _scrollBar.OtherScrollBarView.ChangedPosition += () => { + _listView.LeftItem = _scrollBar.OtherScrollBarView.Position; + if (_listView.LeftItem != _scrollBar.OtherScrollBarView.Position) { + _scrollBar.OtherScrollBarView.Position = _listView.LeftItem; + } + _listView.SetNeedsDisplay (); + };*/ + + tableView.DrawContent += (e) => { + _scrollBar.Size = tableView.Table?.Rows?.Count ??0; + _scrollBar.Position = tableView.RowOffset; + // _scrollBar.OtherScrollBarView.Size = _listView.Maxlength - 1; + // _scrollBar.OtherScrollBarView.Position = _listView.LeftItem; + _scrollBar.Refresh (); + }; + } private void TableViewKeyPress (View.KeyEventEventArgs e) From 5bff06405a05352f6b11ebb1d29508eb20cbc04e Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 27 Jan 2021 08:58:34 +0000 Subject: [PATCH 39/47] Fixed checking OfX instead of X on mouse event handler --- Terminal.Gui/Views/TableView.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 67f897dc2..6d30a9f1a 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -854,7 +854,7 @@ namespace Terminal.Gui { if(me.Flags.HasFlag(MouseFlags.Button1Clicked)) { - var hit = ScreenToCell(me.OfX,me.OfY); + var hit = ScreenToCell(me.X,me.Y); if(hit != null) { SetSelection(hit.Value.X,hit.Value.Y,me.Flags.HasFlag(MouseFlags.ButtonShift)); @@ -864,7 +864,7 @@ namespace Terminal.Gui { // Double clicking a cell activates if(me.Flags == MouseFlags.Button1DoubleClicked) { - var hit = ScreenToCell(me.OfX,me.OfY); + var hit = ScreenToCell(me.X,me.Y); if(hit!= null) { OnCellActivated(new CellActivatedEventArgs(Table,hit.Value.X,hit.Value.Y)); } From cbcd55771079669a605bd7c204ffea29fcd350f7 Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 27 Jan 2021 12:18:47 +0000 Subject: [PATCH 40/47] Added a CSV editor Scenario and an article introducing table view and how to use it --- UICatalog/Scenarios/CsvEditor.cs | 428 +++++++++++++++++++++++++++++++ docfx/articles/tableview.md | 57 ++++ 2 files changed, 485 insertions(+) create mode 100644 UICatalog/Scenarios/CsvEditor.cs create mode 100644 docfx/articles/tableview.md diff --git a/UICatalog/Scenarios/CsvEditor.cs b/UICatalog/Scenarios/CsvEditor.cs new file mode 100644 index 000000000..3df6f687c --- /dev/null +++ b/UICatalog/Scenarios/CsvEditor.cs @@ -0,0 +1,428 @@ +ο»Ώusing System; +using System.Collections.Generic; +using System.Data; +using Terminal.Gui; +using System.Linq; +using System.Globalization; +using System.IO; +using System.Text; + +namespace UICatalog.Scenarios { + + [ScenarioMetadata (Name: "Csv Editor", Description: "Open and edit simple CSV files")] + [ScenarioCategory ("Controls")] + [ScenarioCategory ("Dialogs")] + [ScenarioCategory ("Text")] + [ScenarioCategory ("Dialogs")] + [ScenarioCategory ("TopLevel")] + public class CsvEditor : Scenario + { + TableView tableView; + private string currentFile; + + public override void Setup () + { + Win.Title = this.GetName(); + Win.Y = 1; // menu + Win.Height = Dim.Fill (1); // status bar + Top.LayoutSubviews (); + + this.tableView = new TableView () { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill (1), + }; + + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_File", new MenuItem [] { + new MenuItem ("_Open CSV", "", () => Open()), + new MenuItem ("_Save", "", () => Save()), + new MenuItem ("_Quit", "", () => Quit()), + }), + new MenuBarItem ("_Edit", new MenuItem [] { + new MenuItem ("_Rename Column", "", () => RenameColumn()), + }), + new MenuBarItem ("_Insert", new MenuItem [] { + new MenuItem ("_New Column", "", () => AddColumn()), + new MenuItem ("_New Row", "", () => AddRow()), + }) + }); + Top.Add (menu); + + var statusBar = new StatusBar (new StatusItem [] { + new StatusItem(Key.CtrlMask | Key.O, "~^O~ Open", () => Open()), + new StatusItem(Key.CtrlMask | Key.S, "~^S~ Save", () => Save()), + new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()), + }); + Top.Add (statusBar); + + Win.Add (tableView); + + var selectedCellLabel = new Label(){ + X = 0, + Y = Pos.Bottom(tableView), + Text = "0,0", + Width = Dim.Fill(), + TextAlignment = TextAlignment.Right + + }; + + Win.Add(selectedCellLabel); + + tableView.SelectedCellChanged += (e)=>{selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}";}; + tableView.CellActivated += EditCurrentCell; + tableView.KeyPress += TableViewKeyPress; + + SetupScrollBar(); + } + + private void RenameColumn () + { + if(NoTableLoaded()) { + return; + } + + var currentCol = tableView.Table.Columns[tableView.SelectedColumn]; + + if(GetText("Rename Column","Name:",currentCol.ColumnName,out string newName)) { + currentCol.ColumnName = newName; + tableView.Update(); + } + + + } + + private bool NoTableLoaded () + { + if(tableView.Table == null) { + MessageBox.ErrorQuery("No Table Loaded","No table has currently be opened","Ok"); + return true; + } + + return false; + } + + private void AddRow () + { + if(NoTableLoaded()) { + return; + } + + tableView.Table.Rows.Add(); + tableView.Update(); + } + + private void AddColumn () + { + if(NoTableLoaded()) { + return; + } + + if(GetText("Enter column name","Name:","",out string colName)) { + tableView.Table.Columns.Add(new DataColumn(colName)); + tableView.Update(); + } + + } + + private void Save() + { + if(tableView.Table == null || string.IsNullOrWhiteSpace(currentFile)) { + MessageBox.ErrorQuery("No file loaded","No file is currently loaded","Ok"); + return; + } + + var sb = new StringBuilder(); + + sb.AppendLine(string.Join(",",tableView.Table.Columns.Cast().Select(c=>c.ColumnName))); + + foreach(DataRow row in tableView.Table.Rows) { + sb.AppendLine(string.Join(",",row.ItemArray)); + } + + File.WriteAllText(currentFile,sb.ToString()); + } + + private void Open() + { + var ofd = new FileDialog("Select File","Open","File","Select a CSV file to open (does not support newlines, escaping etc)"); + ofd.AllowedFileTypes = new string[]{".csv" }; + + Application.Run(ofd); + + if(!string.IsNullOrWhiteSpace(ofd.FilePath?.ToString())) + { + Open(ofd.FilePath.ToString()); + } + } + + private void Open(string filename) + { + + int lineNumber = 0; + currentFile = null; + + try { + var dt = new DataTable(); + var lines = File.ReadAllLines(filename); + + foreach(var h in lines[0].Split(',')){ + dt.Columns.Add(h); + } + + + foreach(var line in lines.Skip(1)) { + lineNumber++; + dt.Rows.Add(line.Split(',')); + } + + tableView.Table = dt; + + // Only set the current filename if we succesfully loaded the entire file + currentFile = filename; + } + catch(Exception ex) { + MessageBox.ErrorQuery("Open Failed",$"Error on line {lineNumber}{Environment.NewLine}{ex.Message}","Ok"); + } + } + private void SetupScrollBar () + { + var _scrollBar = new ScrollBarView (tableView, true); + + _scrollBar.ChangedPosition += () => { + tableView.RowOffset = _scrollBar.Position; + if (tableView.RowOffset != _scrollBar.Position) { + _scrollBar.Position = tableView.RowOffset; + } + tableView.SetNeedsDisplay (); + }; + /* + _scrollBar.OtherScrollBarView.ChangedPosition += () => { + _listView.LeftItem = _scrollBar.OtherScrollBarView.Position; + if (_listView.LeftItem != _scrollBar.OtherScrollBarView.Position) { + _scrollBar.OtherScrollBarView.Position = _listView.LeftItem; + } + _listView.SetNeedsDisplay (); + };*/ + + tableView.DrawContent += (e) => { + _scrollBar.Size = tableView.Table?.Rows?.Count ??0; + _scrollBar.Position = tableView.RowOffset; + // _scrollBar.OtherScrollBarView.Size = _listView.Maxlength - 1; + // _scrollBar.OtherScrollBarView.Position = _listView.LeftItem; + _scrollBar.Refresh (); + }; + + } + + private void TableViewKeyPress (View.KeyEventEventArgs e) + { + if(e.KeyEvent.Key == Key.DeleteChar){ + + 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); + } + else{ + + // otherwise set all selected cells to null + foreach(var pt in tableView.GetAllSelectedCells()) + { + tableView.Table.Rows[pt.Y][pt.X] = DBNull.Value; + } + } + + tableView.Update(); + e.Handled = true; + } + } + + private void ClearColumnStyles () + { + tableView.Style.ColumnStyles.Clear(); + tableView.Update(); + } + + + private void CloseExample () + { + tableView.Table = null; + } + + private void Quit () + { + Application.RequestStop (); + } + + private void OpenExample (bool big) + { + tableView.Table = BuildDemoDataTable(big ? 30 : 5, big ? 1000 : 5); + SetDemoTableStyles(); + } + + private void SetDemoTableStyles () + { + var alignMid = new ColumnStyle() { + Alignment = TextAlignment.Centered + }; + var alignRight = new ColumnStyle() { + Alignment = TextAlignment.Right + }; + + var dateFormatStyle = new ColumnStyle() { + Alignment = TextAlignment.Right, + RepresentationGetter = (v)=> v is DateTime d ? d.ToString("yyyy-MM-dd"):v.ToString() + }; + + var negativeRight = new ColumnStyle() { + + Format = "0.##", + MinWidth = 10, + AlignmentGetter = (v)=>v is double d ? + // align negative values right + d < 0 ? TextAlignment.Right : + // align positive values left + TextAlignment.Left: + // not a double + TextAlignment.Left + }; + + 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.Update(); + } + + private void OpenSimple (bool big) + { + tableView.Table = BuildSimpleDataTable(big ? 30 : 5, big ? 1000 : 5); + } + private bool GetText(string title, string label, string initialText, out string enteredText) + { + bool okPressed = false; + + var ok = new Button ("Ok", is_default: true); + ok.Clicked += () => { okPressed = true; Application.RequestStop (); }; + var cancel = new Button ("Cancel"); + cancel.Clicked += () => { Application.RequestStop (); }; + var d = new Dialog (title, 60, 20, ok, cancel); + + var lbl = new Label() { + X = 0, + Y = 1, + Text = label + }; + + var tf = new TextField() + { + Text = initialText, + X = 0, + Y = 2, + Width = Dim.Fill() + }; + + d.Add (lbl,tf); + tf.SetFocus(); + + Application.Run (d); + + enteredText = okPressed? tf.Text.ToString() : null; + return okPressed; + } + private void EditCurrentCell (CellActivatedEventArgs e) + { + if(e.Table == null) + return; + + var oldValue = e.Table.Rows[e.Row][e.Col].ToString(); + + if(GetText("Enter new value",e.Table.Columns[e.Col].ColumnName,oldValue, out string newText)) { + try { + e.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"); + } + + tableView.Update(); + } + } + + /// + /// Generates a new demo with the given number of (min 5) and + /// + /// + /// + /// + public static DataTable BuildDemoDataTable(int cols, int rows) + { + var dt = new DataTable(); + + int explicitCols = 6; + dt.Columns.Add(new DataColumn("StrCol",typeof(string))); + dt.Columns.Add(new DataColumn("DateCol",typeof(DateTime))); + dt.Columns.Add(new DataColumn("IntCol",typeof(int))); + dt.Columns.Add(new DataColumn("DoubleCol",typeof(double))); + dt.Columns.Add(new DataColumn("NullsCol",typeof(string))); + dt.Columns.Add(new DataColumn("Unicode",typeof(string))); + + for(int i=0;i< cols -explicitCols; i++) { + dt.Columns.Add("Column" + (i+explicitCols)); + } + + var r = new Random(100); + + for(int i=0;i< rows;i++) { + + List row = new List(){ + "Some long text that is super cool", + new DateTime(2000+i,12,25), + r.Next(i), + (r.NextDouble()*i)-0.5 /*add some negatives to demo styles*/, + DBNull.Value, + "Les Mise" + Char.ConvertFromUtf32(Int32.Parse("0301", NumberStyles.HexNumber)) + "rables" + }; + + for(int j=0;j< cols -explicitCols; j++) { + row.Add("SomeValue" + r.Next(100)); + } + + dt.Rows.Add(row.ToArray()); + } + + return dt; + } + + /// + /// Builds a simple table in which cell values contents are the index of the cell. This helps testing that scrolling etc is working correctly and not skipping out any rows/columns when paging + /// + /// + /// + /// + public static DataTable BuildSimpleDataTable(int cols, int rows) + { + var dt = new DataTable(); + + for(int c = 0; c < cols; c++) { + dt.Columns.Add("Col"+c); + } + + for(int r = 0; r < rows; r++) { + var newRow = dt.NewRow(); + + for(int c = 0; c < cols; c++) { + newRow[c] = $"R{r}C{c}"; + } + + dt.Rows.Add(newRow); + } + + return dt; + } + } +} diff --git a/docfx/articles/tableview.md b/docfx/articles/tableview.md new file mode 100644 index 000000000..75955f7bd --- /dev/null +++ b/docfx/articles/tableview.md @@ -0,0 +1,57 @@ +# Table View + +This control supports viewing and editing tabular data. It provides a view of a [System.DataTable](https://docs.microsoft.com/en-us/dotnet/api/system.data.datatable?view=net-5.0). + +System.DataTable is a core class of .net standard and can be created very easily + +## Csv Example + +You can create a DataTable from a CSV file by creating a new instance and adding columns and rows as you read them. For a robust solution however you might want to look into a CSV parser library that deals with escaping, multi line rows etc. + +```csharp +var dt = new DataTable(); +var lines = File.ReadAllLines(filename); + +foreach(var h in lines[0].Split(',')){ + dt.Columns.Add(h); +} + + +foreach(var line in lines.Skip(1)) { + lineNumber++; + dt.Rows.Add(line.Split(',')); +} +``` + +## Database Example + +All Ado.net database providers (Oracle, MySql, SqlServer etc) support reading data as DataTables for example: + +```csharp +var dt = new DataTable(); + +using(var con = new SqlConnection("Server=myServerAddress;Database=myDataBase;Trusted_Connection=True;")) +{ + con.Open(); + var cmd = new SqlCommand("select * from myTable;",con); + var adapter = new SqlDataAdapter(cmd); + + adapter.Fill(dt); +} +``` + +## Displaying the table + +Once you have set up your data table set it in the view: + +```csharp +tableView = new TableView () { + X = 0, + Y = 0, + Width = 50, + Height = 10, +}; + +tableView.Table = yourDataTable; +``` + From 20920313909b88dea77fb79444627adaf229e4d0 Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 27 Jan 2021 14:07:46 +0000 Subject: [PATCH 41/47] Added alignment and format functions to CsvEditor --- Terminal.Gui/Views/TableView.cs | 13 ++ UICatalog/Scenarios/CsvEditor.cs | 225 ++++++++++++++----------------- 2 files changed, 114 insertions(+), 124 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 6d30a9f1a..d3d390209 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -118,6 +118,19 @@ namespace Terminal.Gui { { return ColumnStyles.TryGetValue(col,out ColumnStyle result) ? result : null; } + + /// + /// Returns an existing for the given or creates a new one with default options + /// + /// + /// + public ColumnStyle GetOrCreateColumnStyle (DataColumn col) + { + if(!ColumnStyles.ContainsKey(col)) + ColumnStyles.Add(col,new ColumnStyle()); + + return ColumnStyles[col]; + } } /// diff --git a/UICatalog/Scenarios/CsvEditor.cs b/UICatalog/Scenarios/CsvEditor.cs index 3df6f687c..6972c759f 100644 --- a/UICatalog/Scenarios/CsvEditor.cs +++ b/UICatalog/Scenarios/CsvEditor.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Globalization; using System.IO; using System.Text; +using NStack; namespace UICatalog.Scenarios { @@ -19,6 +20,10 @@ namespace UICatalog.Scenarios { { TableView tableView; private string currentFile; + private MenuItem miLeft; + private MenuItem miRight; + private MenuItem miCentered; + private Label selectedCellLabel; public override void Setup () { @@ -41,11 +46,18 @@ namespace UICatalog.Scenarios { new MenuItem ("_Quit", "", () => Quit()), }), new MenuBarItem ("_Edit", new MenuItem [] { - new MenuItem ("_Rename Column", "", () => RenameColumn()), - }), - new MenuBarItem ("_Insert", new MenuItem [] { new MenuItem ("_New Column", "", () => AddColumn()), new MenuItem ("_New Row", "", () => AddRow()), + new MenuItem ("_Rename Column", "", () => RenameColumn()), + new MenuItem ("_Delete Column", "", () => DeleteColum()), + }), + new MenuBarItem ("_View", new MenuItem [] { + miLeft = new MenuItem ("_Align Left", "", () => Align(TextAlignment.Left)), + miRight = new MenuItem ("_Align Right", "", () => Align(TextAlignment.Right)), + miCentered = new MenuItem ("_Align Centered", "", () => Align(TextAlignment.Centered)), + + // Format requires hard typed data table, when we read a CSV everything is untyped (string) so this only works for new columns in this demo + miCentered = new MenuItem ("_Set Format Pattern", "", () => SetFormat()), }) }); Top.Add (menu); @@ -59,7 +71,7 @@ namespace UICatalog.Scenarios { Win.Add (tableView); - var selectedCellLabel = new Label(){ + selectedCellLabel = new Label(){ X = 0, Y = Pos.Bottom(tableView), Text = "0,0", @@ -70,13 +82,29 @@ namespace UICatalog.Scenarios { Win.Add(selectedCellLabel); - tableView.SelectedCellChanged += (e)=>{selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}";}; + tableView.SelectedCellChanged += OnSelectedCellChanged; tableView.CellActivated += EditCurrentCell; tableView.KeyPress += TableViewKeyPress; SetupScrollBar(); } + private void OnSelectedCellChanged (SelectedCellChangedEventArgs e) + { + selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}"; + + if(tableView.Table == null) + return; + + var col = tableView.Table.Columns[tableView.SelectedColumn]; + + var style = tableView.Style.GetColumnStyleIfAny(col); + + miLeft.Checked = style?.Alignment == TextAlignment.Left; + miRight.Checked = style?.Alignment == TextAlignment.Right; + miCentered.Checked = style?.Alignment == TextAlignment.Centered; + } + private void RenameColumn () { if(NoTableLoaded()) { @@ -89,8 +117,55 @@ namespace UICatalog.Scenarios { currentCol.ColumnName = newName; tableView.Update(); } + } + private void DeleteColum() + { + if(NoTableLoaded()) { + return; + } + tableView.Table.Columns.RemoveAt(tableView.SelectedColumn); + tableView.Update(); + } + + private void Align (TextAlignment newAlignment) + { + if (NoTableLoaded ()) { + return; + } + + var col = tableView.Table.Columns[tableView.SelectedColumn]; + + var style = tableView.Style.GetOrCreateColumnStyle(col); + style.Alignment = newAlignment; + + miLeft.Checked = style.Alignment == TextAlignment.Left; + miRight.Checked = style.Alignment == TextAlignment.Right; + miCentered.Checked = style.Alignment == TextAlignment.Centered; + + tableView.Update(); + } + + private void SetFormat() + { + if (NoTableLoaded ()) { + return; + } + + var col = tableView.Table.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); + + if(GetText("Format","Pattern:",style.Format ?? "",out string newPattern)) { + style.Format = newPattern; + tableView.Update(); + } } private bool NoTableLoaded () @@ -120,9 +195,29 @@ namespace UICatalog.Scenarios { } if(GetText("Enter column name","Name:","",out string colName)) { - tableView.Table.Columns.Add(new DataColumn(colName)); + + var col = new DataColumn(colName); + + int result = MessageBox.Query(40,15,"Column Type","Pick a data type for the column",new ustring[]{"Date","Integer","Double","Text","Cancel"}); + + if(result <= -1 || result >= 4) + return; + switch(result) { + case 0: col.DataType = typeof(DateTime); + break; + case 1: col.DataType = typeof(int); + break; + case 2: col.DataType = typeof(double); + break; + case 3: col.DataType = typeof(string); + break; + } + + tableView.Table.Columns.Add(col); tableView.Update(); } + + } @@ -256,52 +351,6 @@ namespace UICatalog.Scenarios { { Application.RequestStop (); } - - private void OpenExample (bool big) - { - tableView.Table = BuildDemoDataTable(big ? 30 : 5, big ? 1000 : 5); - SetDemoTableStyles(); - } - - private void SetDemoTableStyles () - { - var alignMid = new ColumnStyle() { - Alignment = TextAlignment.Centered - }; - var alignRight = new ColumnStyle() { - Alignment = TextAlignment.Right - }; - - var dateFormatStyle = new ColumnStyle() { - Alignment = TextAlignment.Right, - RepresentationGetter = (v)=> v is DateTime d ? d.ToString("yyyy-MM-dd"):v.ToString() - }; - - var negativeRight = new ColumnStyle() { - - Format = "0.##", - MinWidth = 10, - AlignmentGetter = (v)=>v is double d ? - // align negative values right - d < 0 ? TextAlignment.Right : - // align positive values left - TextAlignment.Left: - // not a double - TextAlignment.Left - }; - - 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.Update(); - } - - private void OpenSimple (bool big) - { - tableView.Table = BuildSimpleDataTable(big ? 30 : 5, big ? 1000 : 5); - } private bool GetText(string title, string label, string initialText, out string enteredText) { bool okPressed = false; @@ -352,77 +401,5 @@ namespace UICatalog.Scenarios { tableView.Update(); } } - - /// - /// Generates a new demo with the given number of (min 5) and - /// - /// - /// - /// - public static DataTable BuildDemoDataTable(int cols, int rows) - { - var dt = new DataTable(); - - int explicitCols = 6; - dt.Columns.Add(new DataColumn("StrCol",typeof(string))); - dt.Columns.Add(new DataColumn("DateCol",typeof(DateTime))); - dt.Columns.Add(new DataColumn("IntCol",typeof(int))); - dt.Columns.Add(new DataColumn("DoubleCol",typeof(double))); - dt.Columns.Add(new DataColumn("NullsCol",typeof(string))); - dt.Columns.Add(new DataColumn("Unicode",typeof(string))); - - for(int i=0;i< cols -explicitCols; i++) { - dt.Columns.Add("Column" + (i+explicitCols)); - } - - var r = new Random(100); - - for(int i=0;i< rows;i++) { - - List row = new List(){ - "Some long text that is super cool", - new DateTime(2000+i,12,25), - r.Next(i), - (r.NextDouble()*i)-0.5 /*add some negatives to demo styles*/, - DBNull.Value, - "Les Mise" + Char.ConvertFromUtf32(Int32.Parse("0301", NumberStyles.HexNumber)) + "rables" - }; - - for(int j=0;j< cols -explicitCols; j++) { - row.Add("SomeValue" + r.Next(100)); - } - - dt.Rows.Add(row.ToArray()); - } - - return dt; - } - - /// - /// Builds a simple table in which cell values contents are the index of the cell. This helps testing that scrolling etc is working correctly and not skipping out any rows/columns when paging - /// - /// - /// - /// - public static DataTable BuildSimpleDataTable(int cols, int rows) - { - var dt = new DataTable(); - - for(int c = 0; c < cols; c++) { - dt.Columns.Add("Col"+c); - } - - for(int r = 0; r < rows; r++) { - var newRow = dt.NewRow(); - - for(int c = 0; c < cols; c++) { - newRow[c] = $"R{r}C{c}"; - } - - dt.Rows.Add(newRow); - } - - return dt; - } } } From 656e9b5159fe766ebc8943c2a5ae22127d84f15b Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 27 Jan 2021 14:40:57 +0000 Subject: [PATCH 42/47] Fixed Scenario for deleting last column in table --- UICatalog/Scenarios/CsvEditor.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/UICatalog/Scenarios/CsvEditor.cs b/UICatalog/Scenarios/CsvEditor.cs index 6972c759f..e595265e2 100644 --- a/UICatalog/Scenarios/CsvEditor.cs +++ b/UICatalog/Scenarios/CsvEditor.cs @@ -93,7 +93,7 @@ namespace UICatalog.Scenarios { { selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}"; - if(tableView.Table == null) + if(tableView.Table == null || tableView.SelectedColumn == -1) return; var col = tableView.Table.Columns[tableView.SelectedColumn]; @@ -125,8 +125,20 @@ namespace UICatalog.Scenarios { return; } - tableView.Table.Columns.RemoveAt(tableView.SelectedColumn); - tableView.Update(); + if(tableView.SelectedColumn == -1) { + + MessageBox.ErrorQuery("No Column","No column selected", "Ok"); + return; + } + + + try { + tableView.Table.Columns.RemoveAt(tableView.SelectedColumn); + tableView.Update(); + + } catch (Exception ex) { + MessageBox.ErrorQuery("Could not remove column",ex.Message, "Ok"); + } } private void Align (TextAlignment newAlignment) From 63c20164ab6fff2f03442b8d3ace22f972190b96 Mon Sep 17 00:00:00 2001 From: Thomas Nind <31306100+tznind@users.noreply.github.com> Date: Wed, 27 Jan 2021 16:09:59 +0000 Subject: [PATCH 43/47] Update tableview.md Removed reference to lineNumber in example code --- docfx/articles/tableview.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docfx/articles/tableview.md b/docfx/articles/tableview.md index 75955f7bd..5bb34fa4b 100644 --- a/docfx/articles/tableview.md +++ b/docfx/articles/tableview.md @@ -18,7 +18,6 @@ foreach(var h in lines[0].Split(',')){ foreach(var line in lines.Skip(1)) { - lineNumber++; dt.Rows.Add(line.Split(',')); } ``` From 22c30fbb554865e68ad9e1b7ff56da26e4ab92b6 Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 27 Jan 2021 18:59:51 +0000 Subject: [PATCH 44/47] Added MoveColumn command to Scenario --- UICatalog/Scenarios/CsvEditor.cs | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/UICatalog/Scenarios/CsvEditor.cs b/UICatalog/Scenarios/CsvEditor.cs index e595265e2..a5d84c414 100644 --- a/UICatalog/Scenarios/CsvEditor.cs +++ b/UICatalog/Scenarios/CsvEditor.cs @@ -50,6 +50,7 @@ namespace UICatalog.Scenarios { new MenuItem ("_New Row", "", () => AddRow()), new MenuItem ("_Rename Column", "", () => RenameColumn()), new MenuItem ("_Delete Column", "", () => DeleteColum()), + new MenuItem ("_Move Column", "", () => MoveColumn()), }), new MenuBarItem ("_View", new MenuItem [] { miLeft = new MenuItem ("_Align Left", "", () => Align(TextAlignment.Left)), @@ -89,6 +90,7 @@ namespace UICatalog.Scenarios { SetupScrollBar(); } + private void OnSelectedCellChanged (SelectedCellChangedEventArgs e) { selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}"; @@ -141,6 +143,39 @@ namespace UICatalog.Scenarios { } } + private void MoveColumn () + { + if(NoTableLoaded()) { + return; + } + + if(tableView.SelectedColumn == -1) { + + MessageBox.ErrorQuery("No Column","No column selected", "Ok"); + return; + } + + try{ + + var currentCol = tableView.Table.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); + + currentCol.SetOrdinal(newIdx); + + tableView.SetSelection(newIdx,tableView.SelectedRow,false); + tableView.EnsureSelectedCellIsVisible(); + tableView.SetNeedsDisplay(); + } + + }catch(Exception ex) + { + MessageBox.ErrorQuery("Error moving column",ex.Message, "Ok"); + } + } + private void Align (TextAlignment newAlignment) { if (NoTableLoaded ()) { From 0817fa3d6c68607b15625f62ad267b0df540caa0 Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 27 Jan 2021 19:22:55 +0000 Subject: [PATCH 45/47] Added Move Column and Sort --- UICatalog/Scenarios/CsvEditor.cs | 66 ++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/UICatalog/Scenarios/CsvEditor.cs b/UICatalog/Scenarios/CsvEditor.cs index a5d84c414..63eac225e 100644 --- a/UICatalog/Scenarios/CsvEditor.cs +++ b/UICatalog/Scenarios/CsvEditor.cs @@ -51,6 +51,9 @@ namespace UICatalog.Scenarios { new MenuItem ("_Rename Column", "", () => RenameColumn()), new MenuItem ("_Delete Column", "", () => DeleteColum()), new MenuItem ("_Move Column", "", () => MoveColumn()), + new MenuItem ("_Move Row", "", () => MoveRow()), + new MenuItem ("_Sort Asc", "", () => Sort(true)), + new MenuItem ("_Sort Desc", "", () => Sort(false)), }), new MenuBarItem ("_View", new MenuItem [] { miLeft = new MenuItem ("_Align Left", "", () => Align(TextAlignment.Left)), @@ -175,6 +178,69 @@ namespace UICatalog.Scenarios { MessageBox.ErrorQuery("Error moving column",ex.Message, "Ok"); } } + private void Sort (bool asc) + { + + if(NoTableLoaded()) { + return; + } + + if(tableView.SelectedColumn == -1) { + + MessageBox.ErrorQuery("No Column","No column selected", "Ok"); + return; + } + + var colName = tableView.Table.Columns[tableView.SelectedColumn].ColumnName; + + tableView.Table.DefaultView.Sort = colName + (asc ? " asc" : " desc"); + tableView.Table = tableView.Table.DefaultView.ToTable(); + } + + private void MoveRow () + { + if(NoTableLoaded()) { + return; + } + + if(tableView.SelectedRow == -1) { + + MessageBox.ErrorQuery("No Rows","No row selected", "Ok"); + return; + } + + try{ + + int oldIdx = tableView.SelectedRow; + + var currentRow = tableView.Table.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); + + + if(newIdx == oldIdx) + return; + + var arrayItems = currentRow.ItemArray; + tableView.Table.Rows.Remove(currentRow); + + var newRow = tableView.Table.NewRow(); + newRow.ItemArray = arrayItems; + + tableView.Table.Rows.InsertAt(newRow,newIdx); + + tableView.SetSelection(tableView.SelectedColumn,newIdx,false); + tableView.EnsureSelectedCellIsVisible(); + tableView.SetNeedsDisplay(); + } + + }catch(Exception ex) + { + MessageBox.ErrorQuery("Error moving column",ex.Message, "Ok"); + } + } private void Align (TextAlignment newAlignment) { From c5c477596c3082d4638d3155cf2592d7a2bd36e0 Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 27 Jan 2021 19:43:41 +0000 Subject: [PATCH 46/47] Made New Row/Col location adjacent to current selected cell --- UICatalog/Scenarios/CsvEditor.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/UICatalog/Scenarios/CsvEditor.cs b/UICatalog/Scenarios/CsvEditor.cs index 63eac225e..56f9d3e7c 100644 --- a/UICatalog/Scenarios/CsvEditor.cs +++ b/UICatalog/Scenarios/CsvEditor.cs @@ -225,7 +225,8 @@ namespace UICatalog.Scenarios { var arrayItems = currentRow.ItemArray; tableView.Table.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(); newRow.ItemArray = arrayItems; @@ -297,7 +298,11 @@ namespace UICatalog.Scenarios { return; } - tableView.Table.Rows.Add(); + var newRow = tableView.Table.NewRow(); + + var newRowIdx = Math.Min(Math.Max(0,tableView.SelectedRow+1),tableView.Table.Rows.Count); + + tableView.Table.Rows.InsertAt(newRow,newRowIdx); tableView.Update(); } @@ -311,6 +316,8 @@ namespace UICatalog.Scenarios { var col = new DataColumn(colName); + var newColIdx = Math.Min(Math.Max(0,tableView.SelectedColumn + 1),tableView.Table.Columns.Count); + int result = MessageBox.Query(40,15,"Column Type","Pick a data type for the column",new ustring[]{"Date","Integer","Double","Text","Cancel"}); if(result <= -1 || result >= 4) @@ -327,6 +334,7 @@ namespace UICatalog.Scenarios { } tableView.Table.Columns.Add(col); + col.SetOrdinal(newColIdx); tableView.Update(); } From e68df90d27a2a1193df5ff989d56db74c1b8c5d3 Mon Sep 17 00:00:00 2001 From: tznind Date: Wed, 10 Feb 2021 09:44:47 +0000 Subject: [PATCH 47/47] Fixed TableView ProcessKey return value when no Table is loaded --- Terminal.Gui/Views/TableView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index d3d390209..256dafc18 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -580,7 +580,7 @@ namespace Terminal.Gui { { if(Table == null){ PositionCursor (); - return true; + return false; } if(keyEvent.Key == CellActivationKey && Table != null) {