From 2fba17328886df6245bd1ad48c5324e857d57eff Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 26 Nov 2022 22:55:32 +0000 Subject: [PATCH 01/10] Add ColumnStyle.Visible --- Terminal.Gui/Views/TableView.cs | 71 +++++++++++++++++++++++++++++++++ UICatalog/UICatalog.cs | 32 +++++++++++++++ UnitTests/TableViewTests.cs | 63 +++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 74a8862bd..0c8dd88a7 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -705,6 +705,12 @@ namespace Terminal.Gui { /// True to create a multi cell selection or adjust an existing one public void SetSelection (int col, int row, bool extendExistingSelection) { + // if we are trying to increase the column index then + // we are moving right otherwise we are moving left + bool lookRight = col > selectedColumn; + + col = GetNearestVisibleColumn (col, lookRight); + if (!MultiSelect || !extendExistingSelection) MultiSelectedRegions.Clear (); @@ -1137,7 +1143,56 @@ namespace Terminal.Gui { MultiSelectedRegions.Push (region); } + } + /// + /// Returns unless the is false for + /// the indexed . If so then the index returned is nudged to the nearest visible + /// column. + /// + /// Returns unchanged if it is invalid (e.g. out of bounds). + /// The input column index. + /// When nudging invisible selections look right first. + private int GetNearestVisibleColumn (int columnIndex, bool lookRight) + { + // if the column index provided is out of bounds + if (columnIndex < 0 || columnIndex >= table.Columns.Count) { + return columnIndex; + } + + // get the column visibility by index (if no style visible is true) + bool [] columnVisibility = Table.Columns.Cast () + .Select (c => this.Style.GetColumnStyleIfAny (c)?.Visible ?? true) + .ToArray(); + + // column is visible + if (columnVisibility [columnIndex]) { + return columnIndex; + } + + int increment = lookRight ? 1 : -1; + + // move in that direction + for (int i = columnIndex; i >=0 && i < columnVisibility.Length; i += increment) { + // if we find a visible column + if(columnVisibility [i]) + { + return i; + } + } + + // now look other way + increment = -increment; + + for (int i = columnIndex; i >= 0 && i < columnVisibility.Length; i += increment) { + // if we find a visible column + if (columnVisibility [i]) { + return i; + } + } + + // nothing seems to be visible so just return input index + return columnIndex; } /// @@ -1242,6 +1297,12 @@ namespace Terminal.Gui { var colStyle = Style.GetColumnStyleIfAny (col); int colWidth; + // if column is not being rendered + if(colStyle?.Visible == false) { + // do not add it to the returned columns + continue; + } + // is there enough space for this column (and it's data)? colWidth = CalculateMaxCellWidth (col, rowsToRender, colStyle) + padding; @@ -1397,6 +1458,7 @@ namespace Terminal.Gui { /// Return null for the default /// public CellColorGetterDelegate ColorGetter; + private bool visible = true; /// /// Defines the format for values e.g. "yyyy-MM-dd" for dates @@ -1427,6 +1489,15 @@ namespace Terminal.Gui { /// public int MinAcceptableWidth { get; set; } = DefaultMinAcceptableWidth; + /// + /// Gets or Sets a value indicating whether the column should be visible to the user. + /// This affects both whether it is rendered and whether it can be selected. Defaults to + /// true. + /// + /// If is 0 then will always return false. + public bool Visible { get => MaxWidth >= 0 && visible; set => visible = value; } + + /// /// Returns the alignment for the cell based on and / /// diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 979b9eed7..716ab0a79 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -47,6 +47,38 @@ namespace UICatalog { class UICatalogApp { static void Main (string [] args) { + + Application.Init (); + + var win = new Window (); + var mb = new MenuBar (new []{new MenuBarItem( + new []{ + new MenuItem("Click Me","",()=>{ }) + + }){ + Title = "File (F9)"} + }); + win.Add (mb); + + var txt = new TextView { + Y = 1, + Width = Dim.Fill (), + Height = Dim.Fill (), + AllowsTab = false, + WordWrap = true, + }; + + win.Add (txt); + Random r = new Random (); + Application.MainLoop.AddTimeout (TimeSpan.FromSeconds (1), + (m) => { + Application.MainLoop.Invoke (() => + txt.Text = new string ((char)r.Next (255), 999) + ); + return true; + }); + Application.Run (win); + Console.OutputEncoding = Encoding.Default; if (Debugger.IsAttached) { diff --git a/UnitTests/TableViewTests.cs b/UnitTests/TableViewTests.cs index 6d075909b..49b7ca015 100644 --- a/UnitTests/TableViewTests.cs +++ b/UnitTests/TableViewTests.cs @@ -1098,6 +1098,69 @@ namespace Terminal.Gui.Views { Application.Shutdown (); } + private TableView GetABCDEFTableView (out DataTable dt) + { + var tableView = new TableView (); + tableView.ColorScheme = Colors.TopLevel; + + // 3 columns are visible + tableView.Bounds = new Rect (0, 0, 7, 5); + tableView.Style.ShowHorizontalHeaderUnderline = false; + tableView.Style.ShowHorizontalHeaderOverline = false; + tableView.Style.AlwaysShowHeaders = true; + tableView.Style.SmoothHorizontalScrolling = false; + + dt = new DataTable (); + dt.Columns.Add ("A"); + dt.Columns.Add ("B"); + dt.Columns.Add ("C"); + dt.Columns.Add ("D"); + dt.Columns.Add ("E"); + dt.Columns.Add ("F"); + + + dt.Rows.Add (1, 2, 3, 4, 5, 6); + tableView.Table = dt; + + return tableView; + } + + [Fact, AutoInitShutdown] + public void TestColumnStyle_VisibleFalse_IsNotRendered() + { + var tableView = GetABCDEFTableView (out DataTable dt); + + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false; + + tableView.Redraw (tableView.Bounds); + + string expected = + @" +│A│C│D│ +│1│3│4│"; + + TestHelpers.AssertDriverContentsAre (expected, output); + } + + [Fact, AutoInitShutdown] + public void TestColumnStyle_VisibleFalse_CursorStepsOverInvisibleColumns () + { + var tableView = GetABCDEFTableView (out var dt); + + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false; + tableView.SelectedColumn = 0; + + tableView.ProcessKey (new KeyEvent { Key = Key.CursorRight }); + + // Expect the cursor navigation to skip over the invisible column(s) + Assert.Equal(2,tableView.SelectedColumn); + + tableView.ProcessKey (new KeyEvent { Key = Key.CursorLeft }); + + // Expect the cursor navigation backwards to skip over invisible column too + Assert.Equal (0, tableView.SelectedColumn); + } + [Fact] public void LongColumnTest () { From 25f9b4e26772f7919f405ba81214452221412485 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 26 Nov 2022 22:56:45 +0000 Subject: [PATCH 02/10] Revert accidental changes to UICatalog --- UICatalog/UICatalog.cs | 36 ++---------------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 716ab0a79..bdb6dbe21 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -47,38 +47,6 @@ namespace UICatalog { class UICatalogApp { static void Main (string [] args) { - - Application.Init (); - - var win = new Window (); - var mb = new MenuBar (new []{new MenuBarItem( - new []{ - new MenuItem("Click Me","",()=>{ }) - - }){ - Title = "File (F9)"} - }); - win.Add (mb); - - var txt = new TextView { - Y = 1, - Width = Dim.Fill (), - Height = Dim.Fill (), - AllowsTab = false, - WordWrap = true, - }; - - win.Add (txt); - Random r = new Random (); - Application.MainLoop.AddTimeout (TimeSpan.FromSeconds (1), - (m) => { - Application.MainLoop.Invoke (() => - txt.Text = new string ((char)r.Next (255), 999) - ); - return true; - }); - Application.Run (win); - Console.OutputEncoding = Encoding.Default; if (Debugger.IsAttached) { @@ -182,12 +150,12 @@ namespace UICatalog { class UICatalogTopLevel : Toplevel { public MenuItem miIsMouseDisabled; public MenuItem miHeightAsBuffer; - + public FrameView LeftPane; public ListView CategoryListView; public FrameView RightPane; public ListView ScenarioListView; - + public StatusItem Capslock; public StatusItem Numlock; public StatusItem Scrolllock; From de57331e50030534da312d57402223e215fb0ffe Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 26 Nov 2022 23:50:58 +0000 Subject: [PATCH 03/10] ShowHorizontalScrollIndicators now respects ColumnStyle.Visible --- Terminal.Gui/Views/TableView.cs | 84 +++++++++++++++++++++++----- UnitTests/TableViewTests.cs | 98 ++++++++++++++++++++++++++++++++- 2 files changed, 168 insertions(+), 14 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 0c8dd88a7..d8e76598a 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -427,6 +427,36 @@ namespace Terminal.Gui { private void RenderHeaderUnderline (int row, int availableWidth, ColumnToRender [] columnsToRender) { + /* + * First lets work out if we should be rendering scroll indicators + */ + + // are there are visible columns to the left that have been pushed + // off the screen due to horizontal scrolling? + bool moreColumnsToLeft = ColumnOffset > 0; + + // if we moved left would we find a new column (or are they all invisible?) + if(!TryGetNearestVisibleColumn (ColumnOffset-1, false, false, out _)) { + moreColumnsToLeft = false; + } + + // are there visible columns to the right that have not yet been reached? + // lets find out, what is the column index of the last column we are rendering + int lastColumnIdxRendered = ColumnOffset + columnsToRender.Length - 1; + + // are there more valid indexes? + bool moreColumnsToRight = lastColumnIdxRendered < Table.Columns.Count; + + // if we went right from the last column would we find a new visible column? + if(!TryGetNearestVisibleColumn (lastColumnIdxRendered + 1, true, false, out _)) { + // no we would not + moreColumnsToRight = false; + } + + /* + * Now lets draw the line itself + */ + // Renders a line below the table headers (when visible) like: // ├──────────┼───────────┼───────────────────┼──────────┼────────┼─────────────┤ @@ -436,7 +466,7 @@ namespace Terminal.Gui { // whole way but update to instead draw a header indicator // or scroll arrow etc var rune = Driver.HLine; - + if (Style.ShowVerticalHeaderLines) { if (c == 0) { // for first character render line @@ -445,7 +475,7 @@ namespace Terminal.Gui { // unless we have horizontally scrolled along // in which case render an arrow, to indicate user // can scroll left - if(Style.ShowHorizontalScrollIndicators && ColumnOffset > 0) + if(Style.ShowHorizontalScrollIndicators && moreColumnsToLeft) { rune = Driver.LeftArrow; scrollLeftPoint = new Point(c,row); @@ -465,8 +495,7 @@ namespace Terminal.Gui { // unless there is more of the table we could horizontally // scroll along to see. In which case render an arrow, // to indicate user can scroll right - if(Style.ShowHorizontalScrollIndicators && - ColumnOffset + columnsToRender.Length < Table.Columns.Count) + if(Style.ShowHorizontalScrollIndicators && moreColumnsToRight) { rune = Driver.RightArrow; scrollRightPoint = new Point(c,row); @@ -709,7 +738,7 @@ namespace Terminal.Gui { // we are moving right otherwise we are moving left bool lookRight = col > selectedColumn; - col = GetNearestVisibleColumn (col, lookRight); + col = GetNearestVisibleColumn (col, lookRight, true); if (!MultiSelect || !extendExistingSelection) MultiSelectedRegions.Clear (); @@ -1146,18 +1175,35 @@ namespace Terminal.Gui { } /// - /// Returns unless the is false for + /// Returns unless the is false for /// the indexed . If so then the index returned is nudged to the nearest visible /// column. /// /// Returns unchanged if it is invalid (e.g. out of bounds). /// The input column index. - /// When nudging invisible selections look right first. - private int GetNearestVisibleColumn (int columnIndex, bool lookRight) + /// When nudging invisible selections look right first. + /// to look right, to look left. + /// If we cannot find anything visible when + /// looking in direction of then should we look in the opposite + /// direction instead? Use true if you want to push a selection to a valid index no matter what. + /// Use false if you are primarily interested in learning about directional column visibility. + private int GetNearestVisibleColumn (int columnIndex, bool lookRight, bool allowBumpingInOppositeDirection) + { + if(TryGetNearestVisibleColumn(columnIndex,lookRight,allowBumpingInOppositeDirection, out var answer)) + { + return answer; + } + + return columnIndex; + } + + private bool TryGetNearestVisibleColumn (int columnIndex, bool lookRight, bool allowBumpingInOppositeDirection, out int idx) { // if the column index provided is out of bounds if (columnIndex < 0 || columnIndex >= table.Columns.Count) { - return columnIndex; + + idx = columnIndex; + return false; } // get the column visibility by index (if no style visible is true) @@ -1167,7 +1213,8 @@ namespace Terminal.Gui { // column is visible if (columnVisibility [columnIndex]) { - return columnIndex; + idx = columnIndex; + return true; } int increment = lookRight ? 1 : -1; @@ -1177,22 +1224,33 @@ namespace Terminal.Gui { // if we find a visible column if(columnVisibility [i]) { - return i; + idx = i; + return true; } } + // Caller only wants to look in one direction and we did not find any + // visible columns in that direction + if(!allowBumpingInOppositeDirection) { + idx = columnIndex; + return false; + } + + // Caller will let us look in the other direction so // now look other way increment = -increment; for (int i = columnIndex; i >= 0 && i < columnVisibility.Length; i += increment) { // if we find a visible column if (columnVisibility [i]) { - return i; + idx = i; + return true; } } // nothing seems to be visible so just return input index - return columnIndex; + idx = columnIndex; + return false; } /// diff --git a/UnitTests/TableViewTests.cs b/UnitTests/TableViewTests.cs index 49b7ca015..60ef5e1aa 100644 --- a/UnitTests/TableViewTests.cs +++ b/UnitTests/TableViewTests.cs @@ -1141,7 +1141,103 @@ namespace Terminal.Gui.Views { TestHelpers.AssertDriverContentsAre (expected, output); } - + + [Fact, AutoInitShutdown] + public void TestColumnStyle_FirstColumnVisibleFalse_IsNotRendered () + { + var tableView = GetABCDEFTableView (out DataTable dt); + + tableView.Style.ShowHorizontalScrollIndicators = true; + tableView.Style.ShowHorizontalHeaderUnderline = true; + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false; + + tableView.Redraw (tableView.Bounds); + + string expected = + @" +│B│C│D│ +├─┼─┼─► +│2│3│4│"; + + TestHelpers.AssertDriverContentsAre (expected, output); + } + + [Fact, AutoInitShutdown] + public void TestColumnStyle_RemainingColumnsInvisible_NoScrollIndicator () + { + var tableView = GetABCDEFTableView (out DataTable dt); + + tableView.Style.ShowHorizontalScrollIndicators = true; + tableView.Style.ShowHorizontalHeaderUnderline = true; + + tableView.Redraw (tableView.Bounds); + + // normally we should have scroll indicators because DEF are of screen + string expected = + @" +│A│B│C│ +├─┼─┼─► +│1│2│3│"; + + TestHelpers.AssertDriverContentsAre (expected, output); + + // but if DEF are invisible we shouldn't be showing the indicator + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false; + + expected = + @" +│A│B│C│ +├─┼─┼─┤ +│1│2│3│"; + tableView.Redraw (tableView.Bounds); + TestHelpers.AssertDriverContentsAre (expected, output); + } + + [Fact, AutoInitShutdown] + public void TestColumnStyle_PreceedingColumnsInvisible_NoScrollIndicator () + { + var tableView = GetABCDEFTableView (out DataTable dt); + + tableView.Style.ShowHorizontalScrollIndicators = true; + tableView.Style.ShowHorizontalHeaderUnderline = true; + + tableView.ColumnOffset = 1; + tableView.Redraw (tableView.Bounds); + + // normally we should have scroll indicators because A,E and F are of screen + string expected = + @" +│B│C│D│ +◄─┼─┼─► +│2│3│4│"; + + TestHelpers.AssertDriverContentsAre (expected, output); + + // but if E and F are invisible so we shouldn't show right + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false; + + expected = + @" +│B│C│D│ +◄─┼─┼─┤ +│2│3│4│"; + tableView.Redraw (tableView.Bounds); + TestHelpers.AssertDriverContentsAre (expected, output); + + // now also A is invisible so we cannot scroll in either direction + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false; + + expected = + @" +│B│C│D│ +├─┼─┼─┤ +│2│3│4│"; + tableView.Redraw (tableView.Bounds); + TestHelpers.AssertDriverContentsAre (expected, output); + } [Fact, AutoInitShutdown] public void TestColumnStyle_VisibleFalse_CursorStepsOverInvisibleColumns () { From 67386e43c5981402b77e53c226d5d7b8b5c7e6cc Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 27 Nov 2022 00:26:37 +0000 Subject: [PATCH 04/10] EnsureValidSelection now considers ColumnStyle.Visible --- Terminal.Gui/Views/TableView.cs | 3 ++ UnitTests/TableViewTests.cs | 53 +++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index d8e76598a..75d646c92 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -1144,6 +1144,9 @@ namespace Terminal.Gui { SelectedColumn = Math.Max (Math.Min (SelectedColumn, Table.Columns.Count - 1), 0); SelectedRow = Math.Max (Math.Min (SelectedRow, Table.Rows.Count - 1), 0); + // If SelectedColumn is invisible move it to a visible one + SelectedColumn = GetNearestVisibleColumn (SelectedColumn, lookRight: true, true); + var oldRegions = MultiSelectedRegions.ToArray ().Reverse (); MultiSelectedRegions.Clear (); diff --git a/UnitTests/TableViewTests.cs b/UnitTests/TableViewTests.cs index 60ef5e1aa..02728ca58 100644 --- a/UnitTests/TableViewTests.cs +++ b/UnitTests/TableViewTests.cs @@ -1257,6 +1257,59 @@ namespace Terminal.Gui.Views { Assert.Equal (0, tableView.SelectedColumn); } + [InlineData(true)] + [InlineData (false)] + [Theory, AutoInitShutdown] + public void TestColumnStyle_FirstColumnVisibleFalse_CursorStaysAt1(bool useHome) + { + var tableView = GetABCDEFTableView (out var dt); + + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false; + tableView.SelectedColumn = 0; + + Assert.Equal (0, tableView.SelectedColumn); + + // column 0 is invisible so this method should move to 1 + tableView.EnsureValidSelection(); + Assert.Equal (1, tableView.SelectedColumn); + + tableView.ProcessKey (new KeyEvent + { + Key = useHome ? Key.Home : Key.CursorLeft + }); + + // Expect the cursor to stay at 1 + Assert.Equal (1, tableView.SelectedColumn); + } + + [InlineData (true)] + [InlineData (false)] + [Theory, AutoInitShutdown] + public void TestColumnStyle_LastColumnVisibleFalse_CursorStaysAt2 (bool useEnd) + { + var tableView = GetABCDEFTableView (out var dt); + + // select D + tableView.SelectedColumn = 3; + Assert.Equal (3, tableView.SelectedColumn); + + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false; + + // column D is invisible so this method should move to 2 (C) + tableView.EnsureValidSelection (); + Assert.Equal (2, tableView.SelectedColumn); + + tableView.ProcessKey (new KeyEvent { + Key = useEnd ? Key.End : Key.CursorRight + }); + + // Expect the cursor to stay at 2 + Assert.Equal (2, tableView.SelectedColumn); + } + + [Fact] public void LongColumnTest () { From fd1b9ef872cf318868441f0cc197e90de855bc71 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 27 Nov 2022 00:40:50 +0000 Subject: [PATCH 05/10] Added TableIsNullOrInvisible to TableView --- Terminal.Gui/Views/TableView.cs | 49 +++++++++++++++++++++------------ UnitTests/TableViewTests.cs | 30 ++++++++++++++++++++ 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 75d646c92..7f95628ac 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -109,7 +109,7 @@ namespace Terminal.Gui { get => columnOffset; //try to prevent this being set to an out of bounds column - set => columnOffset = Table == null ? 0 : Math.Max (0, Math.Min (Table.Columns.Count - 1, value)); + set => columnOffset = TableIsNullOrInvisible() ? 0 : Math.Max (0, Math.Min (Table.Columns.Count - 1, value)); } /// @@ -117,7 +117,7 @@ namespace Terminal.Gui { /// public int RowOffset { get => rowOffset; - set => rowOffset = Table == null ? 0 : Math.Max (0, Math.Min (Table.Rows.Count - 1, value)); + set => rowOffset = TableIsNullOrInvisible () ? 0 : Math.Max (0, Math.Min (Table.Rows.Count - 1, value)); } /// @@ -130,7 +130,7 @@ namespace Terminal.Gui { 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)); + selectedColumn = TableIsNullOrInvisible () ? 0 : Math.Min (Table.Columns.Count - 1, Math.Max (0, value)); if (oldValue != selectedColumn) OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, oldValue, SelectedColumn, SelectedRow, SelectedRow)); @@ -146,7 +146,7 @@ namespace Terminal.Gui { var oldValue = selectedRow; - selectedRow = Table == null ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); + selectedRow = TableIsNullOrInvisible () ? 0 : Math.Min (Table.Rows.Count - 1, Math.Max (0, value)); if (oldValue != selectedRow) OnSelectedCellChanged (new SelectedCellChangedEventArgs (Table, SelectedColumn, SelectedColumn, oldValue, selectedRow)); @@ -315,7 +315,7 @@ namespace Terminal.Gui { var rowToRender = RowOffset + (line - headerLinesConsumed); //if we have run off the end of the table - if (Table == null || rowToRender >= Table.Rows.Count || rowToRender < 0) + if (TableIsNullOrInvisible () || rowToRender >= Table.Rows.Count || rowToRender < 0) continue; RenderRow (line, rowToRender, columnsToRender); @@ -712,7 +712,7 @@ namespace Terminal.Gui { /// public override bool ProcessKey (KeyEvent keyEvent) { - if (Table == null || Table.Columns.Count <= 0) { + if (TableIsNullOrInvisible ()) { PositionCursor (); return false; } @@ -839,7 +839,7 @@ namespace Terminal.Gui { /// public void SelectAll () { - if (Table == null || !MultiSelect || Table.Rows.Count == 0) + if (TableIsNullOrInvisible() || !MultiSelect || Table.Rows.Count == 0) return; MultiSelectedRegions.Clear (); @@ -855,7 +855,7 @@ namespace Terminal.Gui { /// public IEnumerable GetAllSelectedCells () { - if (Table == null || Table.Rows.Count == 0) + if (TableIsNullOrInvisible () || Table.Rows.Count == 0) yield break; EnsureValidSelection (); @@ -939,7 +939,7 @@ namespace Terminal.Gui { /// public override void PositionCursor () { - if (Table == null) { + if (TableIsNullOrInvisible ()) { base.PositionCursor (); return; } @@ -962,7 +962,7 @@ namespace Terminal.Gui { SetFocus (); } - if (Table == null || Table.Columns.Count <= 0) { + if (TableIsNullOrInvisible ()) { return false; } @@ -1040,7 +1040,7 @@ namespace Terminal.Gui { /// public Point? ScreenToCell (int clientX, int clientY) { - if (Table == null || Table.Columns.Count <= 0) + if (TableIsNullOrInvisible ()) return null; var viewPort = CalculateViewport (Bounds); @@ -1071,7 +1071,7 @@ namespace Terminal.Gui { /// public Point? CellToScreen (int tableColumn, int tableRow) { - if (Table == null || Table.Columns.Count <= 0) + if (TableIsNullOrInvisible ()) return null; var viewPort = CalculateViewport (Bounds); @@ -1100,7 +1100,7 @@ namespace Terminal.Gui { /// This always calls public void Update () { - if (Table == null) { + if (TableIsNullOrInvisible ()) { SetNeedsDisplay (); return; } @@ -1119,7 +1119,7 @@ namespace Terminal.Gui { /// Changes will not be immediately visible in the display until you call public void EnsureValidScrollOffsets () { - if (Table == null) { + if (TableIsNullOrInvisible ()) { return; } @@ -1134,7 +1134,7 @@ namespace Terminal.Gui { /// Changes will not be immediately visible in the display until you call public void EnsureValidSelection () { - if (Table == null) { + if (TableIsNullOrInvisible()) { // Table doesn't exist, we should probably clear those selections MultiSelectedRegions.Clear (); @@ -1177,6 +1177,21 @@ namespace Terminal.Gui { } } + /// + /// Returns true if the is not set or all the + /// in the have an explicit + /// that marks them + /// . + /// + /// + private bool TableIsNullOrInvisible () + { + return Table == null || + Table.Columns.Count <= 0 || + Table.Columns.Cast ().All ( + c => (Style.GetColumnStyleIfAny (c)?.Visible ?? true) == false); + } + /// /// Returns unless the is false for /// the indexed . If so then the index returned is nudged to the nearest visible @@ -1333,7 +1348,7 @@ namespace Terminal.Gui { /// private IEnumerable CalculateViewport (Rect bounds, int padding = 1) { - if (Table == null || Table.Columns.Count <= 0) + if (TableIsNullOrInvisible ()) yield break; int usedSpace = 0; @@ -1411,7 +1426,7 @@ namespace Terminal.Gui { private bool ShouldRenderHeaders () { - if (Table == null || Table.Columns.Count == 0) + if (TableIsNullOrInvisible ()) return false; return Style.AlwaysShowHeaders || rowOffset == 0; diff --git a/UnitTests/TableViewTests.cs b/UnitTests/TableViewTests.cs index 02728ca58..33f61df25 100644 --- a/UnitTests/TableViewTests.cs +++ b/UnitTests/TableViewTests.cs @@ -1162,6 +1162,36 @@ namespace Terminal.Gui.Views { TestHelpers.AssertDriverContentsAre (expected, output); } + + [Fact, AutoInitShutdown] + public void TestColumnStyle_AllColumnsVisibleFalse_BehavesAsTableNull () + { + var tableView = GetABCDEFTableView (out DataTable dt); + + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["C"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false; + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false; + + + // expect nothing to be rendered when all columns are invisible + string expected = + @" +"; + + tableView.Redraw (tableView.Bounds); + TestHelpers.AssertDriverContentsAre (expected, output); + + + // expect behavior to match when Table is null + tableView.Table = null; + + tableView.Redraw (tableView.Bounds); + TestHelpers.AssertDriverContentsAre (expected, output); + } + [Fact, AutoInitShutdown] public void TestColumnStyle_RemainingColumnsInvisible_NoScrollIndicator () { From 6c533d318f5e09d7a253bce28a95896250ad4a6d Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 28 Nov 2022 19:54:27 +0000 Subject: [PATCH 06/10] IsSelected returns false for invisible columns --- Terminal.Gui/Views/TableView.cs | 25 ++++++++++++++++++++++++- UnitTests/TableViewTests.cs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 7f95628ac..c64c9a86c 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -915,13 +915,20 @@ namespace Terminal.Gui { } /// - /// Returns true if the given cell is selected either because it is the active cell or part of a multi cell selection (e.g. ) + /// + /// Returns true if the given cell is selected either because it is the active cell or part of a multi cell selection (e.g. ). + /// + /// Returns if is . /// /// /// /// public bool IsSelected (int col, int row) { + if(!IsColumnVisible(col)) { + return false; + } + // Cell is also selected if in any multi selection region if (MultiSelect && MultiSelectedRegions.Any (r => r.Rect.Contains (col, row))) return true; @@ -934,6 +941,22 @@ namespace Terminal.Gui { (col == SelectedColumn || FullRowSelect); } + /// + /// Returns true if the given indexes a visible + /// column otherwise false. Returns false for indexes that are out of bounds. + /// + /// + /// + private bool IsColumnVisible (int columnIndex) + { + // if the column index provided is out of bounds + if (columnIndex < 0 || columnIndex >= table.Columns.Count) { + return false; + } + + return this.Style.GetColumnStyleIfAny (Table.Columns [columnIndex])?.Visible ?? 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 /// diff --git a/UnitTests/TableViewTests.cs b/UnitTests/TableViewTests.cs index 33f61df25..72b3b4b73 100644 --- a/UnitTests/TableViewTests.cs +++ b/UnitTests/TableViewTests.cs @@ -1339,6 +1339,37 @@ namespace Terminal.Gui.Views { Assert.Equal (2, tableView.SelectedColumn); } + [Fact, AutoInitShutdown] + public void TestColumnStyle_VisibleFalse_MultiSelected () + { + var tableView = GetABCDEFTableView (out var dt); + + // user has rectangular selection + tableView.MultiSelectedRegions.Push ( + new TableView.TableSelection( + new Point(0,0), + new Rect(0, 0, 3, 1)) + ); + + Assert.Equal (3, tableView.GetAllSelectedCells ().Count()); + Assert.True (tableView.IsSelected (0, 0)); + Assert.True (tableView.IsSelected (1, 0)); + Assert.True (tableView.IsSelected (2, 0)); + Assert.False (tableView.IsSelected (3, 0)); + + // if middle column is invisible + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false; + + // it should not be included in the selection + Assert.Equal (2, tableView.GetAllSelectedCells ().Count ()); + Assert.True (tableView.IsSelected (0, 0)); + Assert.False (tableView.IsSelected (1, 0)); + Assert.True (tableView.IsSelected (2, 0)); + Assert.False (tableView.IsSelected (3, 0)); + + Assert.DoesNotContain(new Point(1,0),tableView.GetAllSelectedCells ()); + } + [Fact] public void LongColumnTest () From 42656a5440f1e1466ca2ca63328dfd7c6b39cf14 Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 28 Nov 2022 19:57:10 +0000 Subject: [PATCH 07/10] Test for extending multi selection with keyboard over invisible column --- UnitTests/TableViewTests.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/UnitTests/TableViewTests.cs b/UnitTests/TableViewTests.cs index 72b3b4b73..49cc7cac4 100644 --- a/UnitTests/TableViewTests.cs +++ b/UnitTests/TableViewTests.cs @@ -1370,6 +1370,25 @@ namespace Terminal.Gui.Views { Assert.DoesNotContain(new Point(1,0),tableView.GetAllSelectedCells ()); } + [Fact, AutoInitShutdown] + public void TestColumnStyle_VisibleFalse_MultiSelectingStepsOverInvisibleColumns () + { + var tableView = GetABCDEFTableView (out var dt); + + // if middle column is invisible + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false; + + tableView.ProcessKey (new KeyEvent { Key = Key.CursorRight | Key.ShiftMask }); + + // Selection should extend from A to C but skip B + Assert.Equal (2, tableView.GetAllSelectedCells ().Count ()); + Assert.True (tableView.IsSelected (0, 0)); + Assert.False (tableView.IsSelected (1, 0)); + Assert.True (tableView.IsSelected (2, 0)); + Assert.False (tableView.IsSelected (3, 0)); + + Assert.DoesNotContain (new Point (1, 0), tableView.GetAllSelectedCells ()); + } [Fact] public void LongColumnTest () From 4da071b32862787ad8d6bc17dc68371f1ff7cf05 Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 29 Nov 2022 20:09:35 +0000 Subject: [PATCH 08/10] Added `TestColumnStyle_VisibleFalse_DoesNotEffect_EnsureSelectedCellIsVisible` --- UnitTests/TableViewTests.cs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/UnitTests/TableViewTests.cs b/UnitTests/TableViewTests.cs index 49cc7cac4..107a10494 100644 --- a/UnitTests/TableViewTests.cs +++ b/UnitTests/TableViewTests.cs @@ -1389,7 +1389,40 @@ namespace Terminal.Gui.Views { Assert.DoesNotContain (new Point (1, 0), tableView.GetAllSelectedCells ()); } + + [Theory, AutoInitShutdown] + [InlineData(new object[] { true,true })] + [InlineData (new object[] { false,true })] + [InlineData (new object [] { true, false})] + [InlineData (new object [] { false, false})] + public void TestColumnStyle_VisibleFalse_DoesNotEffect_EnsureSelectedCellIsVisible (bool smooth, bool invisibleCol) + { + var tableView = GetABCDEFTableView (out var dt); + tableView.Style.SmoothHorizontalScrolling = smooth; + + if(invisibleCol) { + tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false; + } + // New TableView should have first cell selected + Assert.Equal (0,tableView.SelectedColumn); + // With no scrolling + Assert.Equal (0, tableView.ColumnOffset); + + // A,B and C are visible on screen at the moment so these should have no effect + tableView.SelectedColumn = 1; + tableView.EnsureSelectedCellIsVisible (); + Assert.Equal (0, tableView.ColumnOffset); + + tableView.SelectedColumn = 2; + tableView.EnsureSelectedCellIsVisible (); + Assert.Equal (0, tableView.ColumnOffset); + + // Selecting D should move the visible table area to fit D onto the screen + tableView.SelectedColumn = 3; + tableView.EnsureSelectedCellIsVisible (); + Assert.Equal (smooth ? 1 : 3, tableView.ColumnOffset); + } [Fact] public void LongColumnTest () { From 6136656327fc6e86dce904e547f41dc9981b2bd5 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 3 Dec 2022 19:49:42 +0000 Subject: [PATCH 09/10] Added context menu for show/hide columns in TableEditorScenario --- UICatalog/Scenarios/TableEditor.cs | 102 +++++++++++++++++++---------- 1 file changed, 69 insertions(+), 33 deletions(-) diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 7e14d11c3..89e390c2c 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -70,6 +70,7 @@ namespace UICatalog.Scenarios { miAlternatingColors = new MenuItem ("Alternating Colors", "", () => ToggleAlternatingColors()){CheckType = MenuItemCheckStyle.Checked}, miCursor = new MenuItem ("Invert Selected Cell First Character", "", () => ToggleInvertSelectedCellFirstCharacter()){Checked = tableView.Style.InvertSelectedCellFirstCharacter,CheckType = MenuItemCheckStyle.Checked}, new MenuItem ("_ClearColumnStyles", "", () => ClearColumnStyles()), + new MenuItem ("Sho_w All Columns", "", ()=>ShowAllColumns()) }), new MenuBarItem ("_Column", new MenuItem [] { new MenuItem ("_Set Max Width", "", SetMaxWidth), @@ -137,45 +138,80 @@ namespace UICatalog.Scenarios { tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out DataColumn clickedCol); if (clickedCol != null) { + if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) { + + // left click in a header + SortColumn (clickedCol); + } else if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) { - // work out new sort order - var sort = tableView.Table.DefaultView.Sort; - bool isAsc; - - if(sort?.EndsWith("ASC") ?? false) { - sort = $"{clickedCol.ColumnName} DESC"; - isAsc = false; - } else { - sort = $"{clickedCol.ColumnName} ASC"; - isAsc = true; + // right click in a header + ShowHeaderContextMenu (clickedCol, e); } - - // set a sort order - tableView.Table.DefaultView.Sort = sort; - - // copy the rows from the view - var sortedCopy = tableView.Table.DefaultView.ToTable (); - tableView.Table.Rows.Clear (); - foreach(DataRow r in sortedCopy.Rows) { - tableView.Table.ImportRow (r); - } - - foreach(DataColumn col in tableView.Table.Columns) { - - // remove any lingering sort indicator - col.ColumnName = col.ColumnName.TrimEnd ('▼', '▲'); - - // add a new one if this the one that is being sorted - if (col == clickedCol) { - col.ColumnName += isAsc ? '▲': '▼'; - } - } - - tableView.Update (); } }; } + private void ShowAllColumns () + { + foreach(var colStyle in tableView.Style.ColumnStyles) { + colStyle.Value.Visible = true; + } + tableView.Update (); + } + + private void SortColumn (DataColumn clickedCol) + { + // work out new sort order + var sort = tableView.Table.DefaultView.Sort; + bool isAsc; + + if (sort?.EndsWith ("ASC") ?? false) { + sort = $"{clickedCol.ColumnName} DESC"; + isAsc = false; + } else { + sort = $"{clickedCol.ColumnName} ASC"; + isAsc = true; + } + + // set a sort order + tableView.Table.DefaultView.Sort = sort; + + // copy the rows from the view + var sortedCopy = tableView.Table.DefaultView.ToTable (); + tableView.Table.Rows.Clear (); + foreach (DataRow r in sortedCopy.Rows) { + tableView.Table.ImportRow (r); + } + + foreach (DataColumn col in tableView.Table.Columns) { + + // remove any lingering sort indicator + col.ColumnName = col.ColumnName.TrimEnd ('▼', '▲'); + + // add a new one if this the one that is being sorted + if (col == clickedCol) { + col.ColumnName += isAsc ? '▲' : '▼'; + } + } + + tableView.Update (); + } + private void ShowHeaderContextMenu (DataColumn clickedCol, View.MouseEventArgs e) + { + var contextMenu = new ContextMenu (e.MouseEvent.X + 1, e.MouseEvent.Y + 1, + new MenuBarItem (new MenuItem [] { + new MenuItem ($"Hide '{clickedCol.ColumnName}'", "", () => HideColumn(clickedCol)), + }) + ); + contextMenu.Show (); + } + + private void HideColumn (DataColumn clickedCol) + { + var style = tableView.Style.GetOrCreateColumnStyle (clickedCol); + style.Visible = false; + tableView.Update (); + } private DataColumn GetColumn () { From f362e40ca0151e64144f5f906f3373ad85401392 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 3 Dec 2022 19:59:52 +0000 Subject: [PATCH 10/10] Add sort to context menu and fix naming of context items when they are sorted --- UICatalog/Scenarios/TableEditor.cs | 48 ++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index 89e390c2c..99200329e 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -161,18 +161,13 @@ namespace UICatalog.Scenarios { private void SortColumn (DataColumn clickedCol) { - // work out new sort order - var sort = tableView.Table.DefaultView.Sort; - bool isAsc; + var sort = GetProposedNewSortOrder (clickedCol, out var isAsc); - if (sort?.EndsWith ("ASC") ?? false) { - sort = $"{clickedCol.ColumnName} DESC"; - isAsc = false; - } else { - sort = $"{clickedCol.ColumnName} ASC"; - isAsc = true; - } + SortColumn (clickedCol, sort, isAsc); + } + private void SortColumn (DataColumn clickedCol, string sort, bool isAsc) + { // set a sort order tableView.Table.DefaultView.Sort = sort; @@ -186,7 +181,7 @@ namespace UICatalog.Scenarios { foreach (DataColumn col in tableView.Table.Columns) { // remove any lingering sort indicator - col.ColumnName = col.ColumnName.TrimEnd ('▼', '▲'); + col.ColumnName = TrimArrows(col.ColumnName); // add a new one if this the one that is being sorted if (col == clickedCol) { @@ -196,13 +191,42 @@ namespace UICatalog.Scenarios { tableView.Update (); } + + private string TrimArrows (string columnName) + { + return columnName.TrimEnd ('▼', '▲'); + } + private string StripArrows (string columnName) + { + return columnName.Replace ("▼", "").Replace ("▲", ""); + } + private string GetProposedNewSortOrder (DataColumn clickedCol, out bool isAsc) + { + // work out new sort order + var sort = tableView.Table.DefaultView.Sort; + + if (sort?.EndsWith ("ASC") ?? false) { + sort = $"{clickedCol.ColumnName} DESC"; + isAsc = false; + } else { + sort = $"{clickedCol.ColumnName} ASC"; + isAsc = true; + } + + return sort; + } + private void ShowHeaderContextMenu (DataColumn clickedCol, View.MouseEventArgs e) { + var sort = GetProposedNewSortOrder (clickedCol, out var isAsc); + var contextMenu = new ContextMenu (e.MouseEvent.X + 1, e.MouseEvent.Y + 1, new MenuBarItem (new MenuItem [] { - new MenuItem ($"Hide '{clickedCol.ColumnName}'", "", () => HideColumn(clickedCol)), + new MenuItem ($"Hide {TrimArrows(clickedCol.ColumnName)}", "", () => HideColumn(clickedCol)), + new MenuItem ($"Sort {StripArrows(sort)}","",()=>SortColumn(clickedCol,sort,isAsc)), }) ); + contextMenu.Show (); }