using System.Collections; using System.Data; using System.Globalization; using System.Reflection; using Xunit.Abstractions; namespace Terminal.Gui.ViewsTests; public class TableViewTests (ITestOutputHelper output) { /// Builds a simple list with the requested number of string items /// /// public static IList BuildList (int items) { List list = new (); for (var i = 0; i < items; i++) { list.Add ("Item " + i); } return list.ToArray (); } public static DataTableSource BuildTable (int cols, int rows) { return BuildTable (cols, rows, out _); } /// Builds a simple table of string columns with the requested number of columns and rows /// /// /// public static DataTableSource BuildTable (int cols, int rows, out DataTable dt) { dt = new (); for (var c = 0; c < cols; c++) { dt.Columns.Add ("Col" + c); } for (var r = 0; r < rows; r++) { DataRow newRow = dt.NewRow (); for (var c = 0; c < cols; c++) { newRow [c] = $"R{r}C{c}"; } dt.Rows.Add (newRow); } return new (dt); } [Fact] [AutoInitShutdown] public void CellEventsBackgroundFill () { var tv = new TableView { Width = 20, Height = 4 }; var dt = new DataTable (); dt.Columns.Add ("C1"); dt.Columns.Add ("C2"); dt.Columns.Add ("C3"); dt.Rows.Add ("Hello", DBNull.Value, "f"); tv.Table = new DataTableSource (dt); tv.NullSymbol = string.Empty; var top = new Toplevel (); top.Add (tv); Application.Begin (top); tv.Draw (); var expected = @" ┌─────┬──┬─────────┐ │C1 │C2│C3 │ ├─────┼──┼─────────┤ │Hello│ │f │ "; TestHelpers.AssertDriverContentsAre (expected, output); var color = new Attribute (Color.Magenta, Color.BrightBlue); var scheme = new ColorScheme { Normal = color, HotFocus = color, Focus = color, Disabled = color, HotNormal = color }; // Now the thing we really want to test is the styles! // All cells in the column have a column style that says // the cell is pink! for (var i = 0; i < dt.Columns.Count; i++) { ColumnStyle style = tv.Style.GetOrCreateColumnStyle (i); style.ColorGetter = e => { return scheme; }; } tv.Draw (); expected = @" 00000000000000000000 00000000000000000000 00000000000000000000 01111101101111111110 "; TestHelpers.AssertDriverAttributesAre (expected, Application.Driver, tv.ColorScheme.Normal, color); top.Dispose (); } [Fact] public void DeleteRow_SelectAll_AdjustsSelectionToPreventOverrun () { // create a 4 by 4 table var tableView = new TableView { Table = BuildTable (4, 4, out DataTable dt), MultiSelect = true, Viewport = new (0, 0, 10, 5) }; tableView.BeginInit (); tableView.EndInit (); tableView.SelectAll (); Assert.Equal (16, tableView.GetAllSelectedCells ().Count ()); // delete one of the columns dt.Columns.RemoveAt (2); // table should now be 3x4 Assert.Equal (12, tableView.GetAllSelectedCells ().Count ()); // remove a row dt.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, out DataTable dt), MultiSelect = true, Viewport = new (0, 0, 10, 5) }; tableView.BeginInit (); tableView.EndInit (); tableView.ChangeSelectionToEndOfTable (false); // select the last row tableView.MultiSelectedRegions.Clear (); tableView.MultiSelectedRegions.Push (new (new (0, 3), new (0, 3, 4, 1))); Assert.Equal (4, tableView.GetAllSelectedCells ().Count ()); // remove a row dt.Rows.RemoveAt (0); tableView.EnsureValidSelection (); // since the selection no longer exists it should be removed Assert.Empty (tableView.MultiSelectedRegions); } [Fact] public void EnsureValidScrollOffsets_LoadSmallerTable () { var tableView = new TableView (); tableView.BeginInit (); tableView.EndInit (); tableView.Viewport = new (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); } [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 DataTableSource (new ()); // 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); } [Theory] [InlineData (true)] [InlineData (false)] public void GetAllSelectedCells_SingleCellSelected_ReturnsOne (bool multiSelect) { var tableView = new TableView { Table = BuildTable (3, 3), MultiSelect = multiSelect, Viewport = new (0, 0, 10, 5) }; tableView.BeginInit (); tableView.EndInit (); tableView.SetSelection (1, 1, false); Assert.Single (tableView.GetAllSelectedCells ()); Assert.Equal (new (1, 1), tableView.GetAllSelectedCells ().Single ()); } [Fact] public void GetAllSelectedCells_SquareSelection_FullRowSelect () { var tableView = new TableView { Table = BuildTable (3, 3), MultiSelect = true, FullRowSelect = true, Viewport = new (0, 0, 10, 5) }; tableView.BeginInit (); tableView.EndInit (); // 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); Point [] selected = tableView.GetAllSelectedCells ().ToArray (); Assert.Equal (6, selected.Length); Assert.Equal (new (0, 1), selected [0]); Assert.Equal (new (1, 1), selected [1]); Assert.Equal (new (2, 1), selected [2]); Assert.Equal (new (0, 2), selected [3]); Assert.Equal (new (1, 2), selected [4]); Assert.Equal (new (2, 2), selected [5]); } [Fact] public void GetAllSelectedCells_SquareSelection_ReturnsFour () { var tableView = new TableView { Table = BuildTable (3, 3), MultiSelect = true, Viewport = new (0, 0, 10, 5) }; tableView.BeginInit (); tableView.EndInit (); // 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); Point [] selected = tableView.GetAllSelectedCells ().ToArray (); Assert.Equal (4, selected.Length); Assert.Equal (new (1, 1), selected [0]); Assert.Equal (new (2, 1), selected [1]); Assert.Equal (new (1, 2), selected [2]); Assert.Equal (new (2, 2), selected [3]); } [Fact] public void GetAllSelectedCells_TwoIsolatedSelections_ReturnsSix () { var tableView = new TableView { Table = BuildTable (20, 20), MultiSelect = true, Viewport = new (0, 0, 10, 5) }; tableView.BeginInit (); tableView.EndInit (); /* Sets up disconnected selections like: 00000000000 01100000000 01100000000 00000001100 00000000000 */ tableView.MultiSelectedRegions.Clear (); tableView.MultiSelectedRegions.Push (new (new (1, 1), new (1, 1, 2, 2))); tableView.MultiSelectedRegions.Push (new (new (7, 3), new (7, 3, 2, 1))); tableView.SelectedColumn = 8; tableView.SelectedRow = 3; Point [] selected = tableView.GetAllSelectedCells ().ToArray (); Assert.Equal (6, selected.Length); Assert.Equal (new (1, 1), selected [0]); Assert.Equal (new (2, 1), selected [1]); Assert.Equal (new (1, 2), selected [2]); Assert.Equal (new (2, 2), selected [3]); Assert.Equal (new (7, 3), selected [4]); Assert.Equal (new (8, 3), selected [5]); } [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)); } [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_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] [AutoInitShutdown] public void LongColumnTest () { var tableView = new TableView (); var top = new Toplevel (); top.Add (tableView); Application.Begin (top); tableView.ColorScheme = Colors.ColorSchemes ["TopLevel"]; // 25 characters can be printed into table tableView.Viewport = new (0, 0, 25, 5); tableView.Style.ShowHorizontalHeaderUnderline = true; tableView.Style.ShowHorizontalHeaderOverline = false; tableView.Style.AlwaysShowHeaders = true; tableView.Style.SmoothHorizontalScrolling = true; var dt = new DataTable (); dt.Columns.Add ("A"); dt.Columns.Add ("B"); dt.Columns.Add ("Very Long Column"); dt.Rows.Add (1, 2, new string ('a', 500)); dt.Rows.Add (1, 2, "aaa"); tableView.Table = new DataTableSource (dt); tableView.LayoutSubviews (); tableView.Draw (); // default behaviour of TableView is not to render // columns unless there is sufficient space var expected = @" │A│B │ ├─┼─────────────────────► │1│2 │ │1│2 │ "; TestHelpers.AssertDriverContentsAre (expected, output); // get a style for the long column ColumnStyle style = tableView.Style.GetOrCreateColumnStyle (2); // one way the API user can fix this for long columns // is to specify a MinAcceptableWidth for the column style.MaxWidth = 10; tableView.LayoutSubviews (); tableView.Draw (); expected = @" │A│B│Very Long Column │ ├─┼─┼───────────────────┤ │1│2│aaaaaaaaaaaaaaaaaaa│ │1│2│aaa │ "; TestHelpers.AssertDriverContentsAre (expected, output); // revert the style change style.MaxWidth = TableView.DefaultMaxCellWidth; // another way API user can fix problem is to implement // RepresentationGetter and apply max length there style.RepresentationGetter = s => { return s.ToString ().Length < 15 ? s.ToString () : s.ToString ().Substring (0, 13) + "..."; }; tableView.LayoutSubviews (); tableView.Draw (); expected = @" │A│B│Very Long Column │ ├─┼─┼───────────────────┤ │1│2│aaaaaaaaaaaaa... │ │1│2│aaa │ "; TestHelpers.AssertDriverContentsAre (expected, output); // revert style change style.RepresentationGetter = null; // Both of the above methods rely on having a fixed // size limit for the column. These are awkward if a // table is resizeable e.g. Dim.Fill(). Ideally we want // to render in any space available and truncate the content // of the column dynamically so it fills the free space at // the end of the table. // We can now specify that the column can be any length // (Up to MaxWidth) but the renderer can accept using // less space down to this limit style.MinAcceptableWidth = 5; tableView.LayoutSubviews (); tableView.Draw (); expected = @" │A│B│Very Long Column │ ├─┼─┼───────────────────┤ │1│2│aaaaaaaaaaaaaaaaaaa│ │1│2│aaa │ "; TestHelpers.AssertDriverContentsAre (expected, output); // Now test making the width too small for the MinAcceptableWidth // the Column won't fit so should not be rendered var driver = (FakeDriver)Application.Driver; driver.ClearContents (); tableView.Viewport = new (0, 0, 9, 5); tableView.LayoutSubviews (); tableView.Draw (); expected = @" │A│B │ ├─┼─────► │1│2 │ │1│2 │ "; TestHelpers.AssertDriverContentsAre (expected, output); // setting width to 10 leaves just enough space for the column to // meet MinAcceptableWidth of 5. Column width includes terminator line // symbol (e.g. ┤ or │) tableView.Viewport = new (0, 0, 10, 5); tableView.LayoutSubviews (); tableView.Draw (); expected = @" │A│B│Very│ ├─┼─┼────┤ │1│2│aaaa│ │1│2│aaa │ "; TestHelpers.AssertDriverContentsAre (expected, output); tableView.Viewport = new (0, 0, 25, 5); // revert style change style.MinAcceptableWidth = TableView.DefaultMinAcceptableWidth; // Now let's test the global MaxCellWidth and MinCellWidth tableView.Style.ExpandLastColumn = false; tableView.MaxCellWidth = 10; tableView.MinCellWidth = 3; tableView.LayoutSubviews (); tableView.Draw (); expected = @" │A │B │Very Long │ │ ├───┼───┼──────────┼────┤ │1 │2 │aaaaaaaaaa│ │ │1 │2 │aaa │ │ "; TestHelpers.AssertDriverContentsAre (expected, output); // MaxCellWidth limits MinCellWidth tableView.MaxCellWidth = 5; tableView.MinCellWidth = 10; tableView.LayoutSubviews (); tableView.Draw (); expected = @" │A │B │Very │ │ ├─────┼─────┼─────┼─────┤ │1 │2 │aaaaa│ │ │1 │2 │aaa │ │ "; TestHelpers.AssertDriverContentsAre (expected, output); top.Dispose (); Application.Shutdown (); top.Dispose (); } [AutoInitShutdown] [Fact] public void PageDown_ExcludesHeaders () { var tableView = new TableView { Table = BuildTable (25, 50), MultiSelect = true, Viewport = new (0, 0, 10, 5) }; // Header should take up 2 lines tableView.Style.ShowHorizontalHeaderOverline = false; tableView.Style.ShowHorizontalHeaderUnderline = true; tableView.Style.AlwaysShowHeaders = false; // ensure that TableView has the input focus var top = new Toplevel (); top.Add (tableView); Application.Begin (top); top.FocusFirst (null); Assert.True (tableView.HasFocus); Assert.Equal (0, tableView.RowOffset); tableView.NewKeyDownEvent (Key.PageDown); // window height is 5 rows 2 are header so page down should give 3 new rows Assert.Equal (3, tableView.SelectedRow); Assert.Equal (1, tableView.RowOffset); // header is no longer visible so page down should give 5 new rows tableView.NewKeyDownEvent (Key.PageDown); Assert.Equal (8, tableView.SelectedRow); Assert.Equal (4, tableView.RowOffset); top.Dispose (); } [Fact] [SetupFakeDriver] public void Redraw_EmptyTable () { var tableView = new TableView (); tableView.ColorScheme = new (); tableView.Viewport = new (0, 0, 25, 10); // Set a table with 1 column tableView.Table = BuildTable (1, 50, out DataTable dt); tableView.Draw (); dt.Columns.Remove (dt.Columns [0]); tableView.Draw (); } [Fact] public void ScrollDown_OneLineAtATime () { var tableView = new TableView (); tableView.BeginInit (); tableView.EndInit (); // Set big table tableView.Table = BuildTable (25, 50); // 1 header + 4 rows visible tableView.Viewport = new (0, 0, 25, 5); tableView.Style.ShowHorizontalHeaderUnderline = false; tableView.Style.ShowHorizontalHeaderOverline = false; tableView.Style.AlwaysShowHeaders = true; // select last row tableView.SelectedRow = 3; // row is 0 indexed so this is the 4th visible row // Scroll down tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorDown }); // Scrolled off the page by 1 row so it should only have moved down 1 line of RowOffset Assert.Equal (4, tableView.SelectedRow); Assert.Equal (1, tableView.RowOffset); } [Fact] [SetupFakeDriver] public void ScrollIndicators () { var tableView = new TableView (); tableView.BeginInit (); tableView.EndInit (); tableView.ColorScheme = Colors.ColorSchemes ["TopLevel"]; // 3 columns are visibile tableView.Viewport = new (0, 0, 7, 5); tableView.Style.ShowHorizontalHeaderUnderline = true; tableView.Style.ShowHorizontalHeaderOverline = false; tableView.Style.AlwaysShowHeaders = true; tableView.Style.SmoothHorizontalScrolling = true; var 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 = new DataTableSource (dt); // select last visible column tableView.SelectedColumn = 2; // column C tableView.Draw (); // user can only scroll right so sees right indicator // Because first column in table is A var expected = @" │A│B│C│ ├─┼─┼─► │1│2│3│"; TestHelpers.AssertDriverContentsAre (expected, output); // Scroll right tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); // since A is now pushed off screen we get indicator showing // that user can scroll left to see first column tableView.Draw (); expected = @" │B│C│D│ ◄─┼─┼─► │2│3│4│"; TestHelpers.AssertDriverContentsAre (expected, output); // Scroll right twice more (to end of columns) tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); tableView.Draw (); expected = @" │D│E│F│ ◄─┼─┼─┤ │4│5│6│"; TestHelpers.AssertDriverContentsAre (expected, output); // Shutdown must be called to safely clean up Application if Init has been called Application.Shutdown (); } [Fact] [SetupFakeDriver] public void ScrollRight_SmoothScrolling () { var tableView = new TableView (); tableView.BeginInit (); tableView.EndInit (); tableView.ColorScheme = Colors.ColorSchemes ["TopLevel"]; tableView.LayoutSubviews (); // 3 columns are visibile tableView.Viewport = new (0, 0, 7, 5); tableView.Style.ShowHorizontalHeaderUnderline = false; tableView.Style.ShowHorizontalHeaderOverline = false; tableView.Style.AlwaysShowHeaders = true; tableView.Style.SmoothHorizontalScrolling = true; var 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 = new DataTableSource (dt); // select last visible column tableView.SelectedColumn = 2; // column C tableView.Draw (); var expected = @" │A│B│C│ │1│2│3│"; TestHelpers.AssertDriverContentsAre (expected, output); // Scroll right tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); tableView.Draw (); // Note that with SmoothHorizontalScrolling only a single new column // is exposed when scrolling right. This is not always the case though // sometimes if the leftmost column is long (i.e. A is a long column) // then when A is pushed off the screen multiple new columns could be exposed // (not just D but also E and F). This is because TableView never shows // 'half cells' or scrolls by console unit (scrolling is done by table row/column increments). expected = @" │B│C│D│ │2│3│4│"; TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] [SetupFakeDriver] public void ScrollRight_WithoutSmoothScrolling () { var tableView = new TableView (); tableView.BeginInit (); tableView.EndInit (); tableView.ColorScheme = Colors.ColorSchemes ["TopLevel"]; // 3 columns are visibile tableView.Viewport = new (0, 0, 7, 5); tableView.Style.ShowHorizontalHeaderUnderline = false; tableView.Style.ShowHorizontalHeaderOverline = false; tableView.Style.AlwaysShowHeaders = true; tableView.Style.SmoothHorizontalScrolling = false; var 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 = new DataTableSource (dt); // select last visible column tableView.SelectedColumn = 2; // column C tableView.Draw (); var expected = @" │A│B│C│ │1│2│3│"; TestHelpers.AssertDriverContentsAre (expected, output); // Scroll right tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); tableView.Draw (); // notice that without smooth scrolling we just update the first column // rendered in the table to the newly exposed column (D). This is fast // since we don't have to worry about repeatedly measuring the content // area as we scroll until the new column (D) is exposed. But it makes // the view 'jump' to expose all new columns expected = @" │D│E│F│ │4│5│6│"; TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] public void SelectedCellChanged_NotFiredForSameValue () { var tableView = new TableView { Table = BuildTable (25, 50) }; var called = false; tableView.SelectedCellChanged += (s, 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) }; var called = false; tableView.SelectedCellChanged += (s, 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) }; var called = false; tableView.SelectedCellChanged += (s, e) => { called = true; Assert.Equal (0, e.OldRow); Assert.Equal (10, e.NewRow); }; tableView.SelectedRow = 10; Assert.True (called); } [Fact] [SetupFakeDriver] public void ShowHorizontalBottomLine_NoCellLines () { TableView tableView = GetABCDEFTableView (out _); tableView.ColorScheme = Colors.ColorSchemes ["TopLevel"]; // 3 columns are visibile tableView.Viewport = new (0, 0, 7, 5); tableView.Style.ShowHorizontalHeaderUnderline = true; tableView.Style.ShowHorizontalHeaderOverline = false; tableView.Style.AlwaysShowHeaders = true; tableView.Style.SmoothHorizontalScrolling = true; tableView.Style.ShowHorizontalBottomline = true; tableView.Style.ShowVerticalCellLines = false; tableView.Draw (); // user can only scroll right so sees right indicator // Because first column in table is A var expected = @" │A│B│C│ └─┴─┴─► 1 2 3 ───────"; TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] [SetupFakeDriver] public void ShowHorizontalBottomLine_WithVerticalCellLines () { TableView tableView = GetABCDEFTableView (out _); tableView.ColorScheme = Colors.ColorSchemes ["TopLevel"]; // 3 columns are visibile tableView.Viewport = new (0, 0, 7, 5); tableView.Style.ShowHorizontalHeaderUnderline = true; tableView.Style.ShowHorizontalHeaderOverline = false; tableView.Style.AlwaysShowHeaders = true; tableView.Style.SmoothHorizontalScrolling = true; tableView.Style.ShowHorizontalBottomline = true; tableView.Draw (); // user can only scroll right so sees right indicator // Because first column in table is A var expected = @" │A│B│C│ ├─┼─┼─► │1│2│3│ └─┴─┴─┘"; TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] [AutoInitShutdown] public void TableView_Activate () { string activatedValue = null; var tv = new TableView (BuildTable (1, 1)); tv.CellActivated += (s, c) => activatedValue = c.Table [c.Row, c.Col].ToString (); var top = new Toplevel (); top.Add (tv); Application.Begin (top); // pressing enter should activate the first cell (selected cell) tv.NewKeyDownEvent (Key.Enter); Assert.Equal ("R0C0", activatedValue); // reset the test activatedValue = null; // clear keybindings and ensure that Enter does not trigger the event anymore tv.KeyBindings.Clear (); tv.NewKeyDownEvent (Key.Enter); Assert.Null (activatedValue); // New method for changing the activation key tv.KeyBindings.Add (Key.Z, Command.Accept); tv.NewKeyDownEvent (Key.Z); Assert.Equal ("R0C0", activatedValue); // reset the test activatedValue = null; tv.KeyBindings.Clear (); // Old method for changing the activation key tv.CellActivationKey = KeyCode.Z; tv.NewKeyDownEvent (Key.Z); Assert.Equal ("R0C0", activatedValue); top.Dispose (); } [Theory] [SetupFakeDriver] [InlineData (false)] [InlineData (true)] public void TableView_ColorsTest_ColorGetter (bool focused) { TableView tv = SetUpMiniTable (out DataTable dt); tv.LayoutSubviews (); // width exactly matches the max col widths tv.Viewport = new (0, 0, 5, 4); // Create a style for column B ColumnStyle bStyle = tv.Style.GetOrCreateColumnStyle (1); // when B is 2 use the custom highlight color var cellHighlight = new ColorScheme { Normal = new (Color.BrightCyan, Color.DarkGray), HotNormal = new (Color.Green, Color.Blue), Focus = new (Color.Cyan, Color.Magenta), // Not used by TableView HotFocus = new (Color.BrightYellow, Color.White) }; bStyle.ColorGetter = a => Convert.ToInt32 (a.CellValue) == 2 ? cellHighlight : null; // private method for forcing the view to be focused/not focused MethodInfo setFocusMethod = typeof (View).GetMethod ("SetHasFocus", BindingFlags.Instance | BindingFlags.NonPublic); // when the view is/isn't focused setFocusMethod.Invoke (tv, new object [] { focused, tv, true }); tv.Draw (); var expected = @" ┌─┬─┐ │A│B│ ├─┼─┤ │1│2│ "; TestHelpers.AssertDriverContentsAre (expected, output); var expectedColors = @" 00000 00000 00000 01020 "; TestHelpers.AssertDriverAttributesAre ( expectedColors, Application.Driver, tv.ColorScheme.Normal, focused ? tv.ColorScheme.Focus : tv.ColorScheme.HotNormal, cellHighlight.Normal ); // change the value in the table so that // it no longer matches the ColorGetter // delegate conditional ( which checks for // the value 2) dt.Rows [0] [1] = 5; tv.Draw (); expected = @" ┌─┬─┐ │A│B│ ├─┼─┤ │1│5│ "; TestHelpers.AssertDriverContentsAre (expected, output); expectedColors = @" 00000 00000 00000 01000 "; // now we only see 2 colors used (the selected cell color and Normal // cellHighlight should no longer be used because the delegate returned null // (now that the cell value is 5 - which does not match the conditional) TestHelpers.AssertDriverAttributesAre ( expectedColors, Application.Driver, tv.ColorScheme.Normal, focused ? tv.ColorScheme.Focus : tv.ColorScheme.HotNormal ); } [Theory] [SetupFakeDriver] [InlineData (false)] [InlineData (true)] public void TableView_ColorsTest_RowColorGetter (bool focused) { TableView tv = SetUpMiniTable (out DataTable dt); tv.LayoutSubviews (); // width exactly matches the max col widths tv.Viewport = new (0, 0, 5, 4); var rowHighlight = new ColorScheme { Normal = new (Color.BrightCyan, Color.DarkGray), HotNormal = new (Color.Green, Color.Blue), Focus = new (Color.BrightYellow, Color.White), // Not used by TableView HotFocus = new (Color.Cyan, Color.Magenta) }; // when B is 2 use the custom highlight color for the row tv.Style.RowColorGetter += e => Convert.ToInt32 (e.Table [e.RowIndex, 1]) == 2 ? rowHighlight : null; // private method for forcing the view to be focused/not focused MethodInfo setFocusMethod = typeof (View).GetMethod ("SetHasFocus", BindingFlags.Instance | BindingFlags.NonPublic); // when the view is/isn't focused setFocusMethod.Invoke (tv, new object [] { focused, tv, true }); tv.Draw (); var expected = @" ┌─┬─┐ │A│B│ ├─┼─┤ │1│2│ "; TestHelpers.AssertDriverContentsAre (expected, output); var expectedColors = @" 00000 00000 00000 21222 "; TestHelpers.AssertDriverAttributesAre ( expectedColors, Application.Driver, tv.ColorScheme.Normal, focused ? rowHighlight.Focus : rowHighlight.HotNormal, rowHighlight.Normal ); // change the value in the table so that // it no longer matches the RowColorGetter // delegate conditional ( which checks for // the value 2) dt.Rows [0] [1] = 5; tv.Draw (); expected = @" ┌─┬─┐ │A│B│ ├─┼─┤ │1│5│ "; TestHelpers.AssertDriverContentsAre (expected, output); expectedColors = @" 00000 00000 00000 01000 "; // now we only see 2 colors used (the selected cell color and Normal // rowHighlight should no longer be used because the delegate returned null // (now that the cell value is 5 - which does not match the conditional) TestHelpers.AssertDriverAttributesAre ( expectedColors, Application.Driver, tv.ColorScheme.Normal, focused ? tv.ColorScheme.Focus : tv.ColorScheme.HotNormal ); } [Theory] [SetupFakeDriver] [InlineData (false)] [InlineData (true)] public void TableView_ColorTests_FocusedOrNot (bool focused) { TableView tv = SetUpMiniTable (); tv.LayoutSubviews (); // width exactly matches the max col widths tv.Viewport = new (0, 0, 5, 4); // private method for forcing the view to be focused/not focused MethodInfo setFocusMethod = typeof (View).GetMethod ("SetHasFocus", BindingFlags.Instance | BindingFlags.NonPublic); // when the view is/isn't focused setFocusMethod.Invoke (tv, new object [] { focused, tv, true }); tv.Draw (); var expected = @" ┌─┬─┐ │A│B│ ├─┼─┤ │1│2│ "; TestHelpers.AssertDriverContentsAre (expected, output); var expectedColors = @" 00000 00000 00000 01000 "; TestHelpers.AssertDriverAttributesAre ( expectedColors, Application.Driver, tv.ColorScheme.Normal, focused ? tv.ColorScheme.Focus : tv.ColorScheme.HotNormal ); } [Theory] [SetupFakeDriver] [InlineData (false)] [InlineData (true)] public void TableView_ColorTests_InvertSelectedCellFirstCharacter (bool focused) { TableView tv = SetUpMiniTable (); tv.Style.InvertSelectedCellFirstCharacter = true; tv.LayoutSubviews (); // width exactly matches the max col widths tv.Viewport = new (0, 0, 5, 4); // private method for forcing the view to be focused/not focused MethodInfo setFocusMethod = typeof (View).GetMethod ("SetHasFocus", BindingFlags.Instance | BindingFlags.NonPublic); // when the view is/isn't focused setFocusMethod.Invoke (tv, new object [] { focused, tv, true }); tv.Draw (); var expected = @" ┌─┬─┐ │A│B│ ├─┼─┤ │1│2│ "; TestHelpers.AssertDriverContentsAre (expected, output); var expectedColors = @" 00000 00000 00000 01000 "; var invertFocus = new Attribute (tv.ColorScheme.Focus.Background, tv.ColorScheme.Focus.Foreground); var invertHotNormal = new Attribute (tv.ColorScheme.HotNormal.Background, tv.ColorScheme.HotNormal.Foreground); TestHelpers.AssertDriverAttributesAre ( expectedColors, Application.Driver, tv.ColorScheme.Normal, focused ? invertFocus : invertHotNormal ); } [Fact] [SetupFakeDriver] public void TableView_ExpandLastColumn_False () { TableView tv = SetUpMiniTable (); // the thing we are testing tv.Style.ExpandLastColumn = false; tv.Draw (); var expected = @" ┌─┬─┬────┐ │A│B│ │ ├─┼─┼────┤ │1│2│ │ "; TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] [SetupFakeDriver] public void TableView_ExpandLastColumn_False_ExactBounds () { TableView tv = SetUpMiniTable (); // the thing we are testing tv.Style.ExpandLastColumn = false; // width exactly matches the max col widths tv.Viewport = new (0, 0, 5, 4); tv.Draw (); var expected = @" ┌─┬─┐ │A│B│ ├─┼─┤ │1│2│ "; TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] [SetupFakeDriver] public void TableView_ExpandLastColumn_True () { TableView tv = SetUpMiniTable (); // the thing we are testing tv.Style.ExpandLastColumn = true; tv.Draw (); var expected = @" ┌─┬──────┐ │A│B │ ├─┼──────┤ │1│2 │ "; TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] [SetupFakeDriver] public void TableView_ShowHeadersFalse_AllLines () { TableView tv = GetABCDEFTableView (out _); tv.Viewport = new (0, 0, 5, 5); tv.Style.ShowHeaders = false; tv.Style.ShowHorizontalHeaderOverline = true; tv.Style.ShowHorizontalHeaderUnderline = true; // Horizontal scrolling option is part of the underline tv.Style.ShowHorizontalScrollIndicators = true; tv.Draw (); var expected = @" ┌─┬─┐ ├─┼─► │1│2│ "; TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] [SetupFakeDriver] public void TableView_ShowHeadersFalse_AndNoHeaderLines () { TableView tv = GetABCDEFTableView (out _); tv.Viewport = new (0, 0, 5, 5); tv.Style.ShowHeaders = false; tv.Style.ShowHorizontalHeaderOverline = false; tv.Style.ShowHorizontalHeaderUnderline = false; tv.Draw (); var expected = @" │1│2│ "; TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] [SetupFakeDriver] public void TableView_ShowHeadersFalse_OverlineTrue () { TableView tv = GetABCDEFTableView (out _); tv.Viewport = new (0, 0, 5, 5); tv.Style.ShowHeaders = false; tv.Style.ShowHorizontalHeaderOverline = true; tv.Style.ShowHorizontalHeaderUnderline = false; tv.Draw (); var expected = @" ┌─┬─┐ │1│2│ "; TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] [SetupFakeDriver] public void TableView_ShowHeadersFalse_UnderlineTrue () { TableView tv = GetABCDEFTableView (out _); tv.Viewport = new (0, 0, 5, 5); tv.Style.ShowHeaders = false; tv.Style.ShowHorizontalHeaderOverline = false; tv.Style.ShowHorizontalHeaderUnderline = true; // Horizontal scrolling option is part of the underline tv.Style.ShowHorizontalScrollIndicators = true; tv.Draw (); var expected = @" ├─┼─► │1│2│ "; TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] [SetupFakeDriver] public void TableViewMultiSelect_CannotFallOffBottom () { TableView tv = SetUpMiniTable (out DataTable dt); dt.Rows.Add (1, 2); // add another row (brings us to 2 rows) tv.MultiSelect = true; tv.SelectedColumn = 0; tv.SelectedRow = 0; tv.NewKeyDownEvent (Key.CursorRight.WithShift); tv.NewKeyDownEvent (Key.CursorDown.WithShift); Assert.Equal (new (0, 0, 2, 2), tv.MultiSelectedRegions.Single ().Rectangle); // this next moves should be ignored because we already selected the whole table tv.NewKeyDownEvent (Key.CursorRight.WithShift); tv.NewKeyDownEvent (Key.CursorDown.WithShift); Assert.Equal (new (0, 0, 2, 2), tv.MultiSelectedRegions.Single ().Rectangle); Assert.Equal (1, tv.SelectedColumn); Assert.Equal (1, tv.SelectedRow); } [Fact] [SetupFakeDriver] public void TableViewMultiSelect_CannotFallOffLeft () { TableView tv = SetUpMiniTable (out DataTable dt); dt.Rows.Add (1, 2); // add another row (brings us to 2 rows) tv.MultiSelect = true; tv.SelectedColumn = 1; tv.SelectedRow = 1; tv.NewKeyDownEvent (Key.CursorLeft.WithShift); Assert.Equal (new (0, 1, 2, 1), tv.MultiSelectedRegions.Single ().Rectangle); // this next shift left should be ignored because we are already at the bounds tv.NewKeyDownEvent (Key.CursorLeft.WithShift); Assert.Equal (new (0, 1, 2, 1), tv.MultiSelectedRegions.Single ().Rectangle); Assert.Equal (0, tv.SelectedColumn); Assert.Equal (1, tv.SelectedRow); } [Fact] [SetupFakeDriver] public void TableViewMultiSelect_CannotFallOffRight () { TableView tv = SetUpMiniTable (out DataTable dt); dt.Rows.Add (1, 2); // add another row (brings us to 2 rows) tv.MultiSelect = true; tv.SelectedColumn = 0; tv.SelectedRow = 1; tv.NewKeyDownEvent (Key.CursorRight.WithShift); Assert.Equal (new (0, 1, 2, 1), tv.MultiSelectedRegions.Single ().Rectangle); // this next shift right should be ignored because we are already at the right bounds tv.NewKeyDownEvent (Key.CursorRight.WithShift); Assert.Equal (new (0, 1, 2, 1), tv.MultiSelectedRegions.Single ().Rectangle); Assert.Equal (1, tv.SelectedColumn); Assert.Equal (1, tv.SelectedRow); } [Fact] [SetupFakeDriver] public void TableViewMultiSelect_CannotFallOffTop () { TableView tv = SetUpMiniTable (out DataTable dt); dt.Rows.Add (1, 2); // add another row (brings us to 2 rows) tv.LayoutSubviews (); tv.MultiSelect = true; tv.SelectedColumn = 1; tv.SelectedRow = 1; tv.NewKeyDownEvent (Key.CursorLeft.WithShift); tv.NewKeyDownEvent (Key.CursorUp.WithShift); Assert.Equal (new (0, 0, 2, 2), tv.MultiSelectedRegions.Single ().Rectangle); // this next moves should be ignored because we already selected the whole table tv.NewKeyDownEvent (Key.CursorLeft.WithShift); tv.NewKeyDownEvent (Key.CursorUp.WithShift); Assert.Equal (new (0, 0, 2, 2), tv.MultiSelectedRegions.Single ().Rectangle); Assert.Equal (0, tv.SelectedColumn); Assert.Equal (0, tv.SelectedRow); } [Fact] [AutoInitShutdown] public void Test_CollectionNavigator () { var tv = new TableView (); tv.ColorScheme = Colors.ColorSchemes ["TopLevel"]; tv.Viewport = new (0, 0, 50, 7); tv.Table = new EnumerableTableSource ( new [] { "fish", "troll", "trap", "zoo" }, new() { { "Name", t => t }, { "EndsWith", t => t.Last () } } ); tv.LayoutSubviews (); tv.Draw (); var expected = @" ┌─────┬──────────────────────────────────────────┐ │Name │EndsWith │ ├─────┼──────────────────────────────────────────┤ │fish │h │ │troll│l │ │trap │p │ │zoo │o │"; TestHelpers.AssertDriverContentsAre (expected, output); Assert.Equal (0, tv.SelectedRow); // this test assumes no focus Assert.False (tv.HasFocus); // already on fish tv.NewKeyDownEvent (new() { KeyCode = KeyCode.F }); Assert.Equal (0, tv.SelectedRow); // not focused tv.NewKeyDownEvent (new() { KeyCode = KeyCode.Z }); Assert.Equal (0, tv.SelectedRow); // ensure that TableView has the input focus var top = new Toplevel (); top.Add (tv); Application.Begin (top); top.FocusFirst (null); Assert.True (tv.HasFocus); // already on fish tv.NewKeyDownEvent (new() { KeyCode = KeyCode.F }); Assert.Equal (0, tv.SelectedRow); // move to zoo tv.NewKeyDownEvent (new() { KeyCode = KeyCode.Z }); Assert.Equal (3, tv.SelectedRow); // move to troll tv.NewKeyDownEvent (new() { KeyCode = KeyCode.T }); Assert.Equal (1, tv.SelectedRow); // move to trap tv.NewKeyDownEvent (new() { KeyCode = KeyCode.T }); Assert.Equal (2, tv.SelectedRow); // change columns to navigate by column 2 Assert.Equal (0, tv.SelectedColumn); Assert.Equal (2, tv.SelectedRow); tv.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); Assert.Equal (1, tv.SelectedColumn); Assert.Equal (2, tv.SelectedRow); // nothing ends with t so stay where you are tv.NewKeyDownEvent (new() { KeyCode = KeyCode.T }); Assert.Equal (2, tv.SelectedRow); //jump to fish which ends in h tv.NewKeyDownEvent (new() { KeyCode = KeyCode.H }); Assert.Equal (0, tv.SelectedRow); // jump to zoo which ends in o tv.NewKeyDownEvent (new() { KeyCode = KeyCode.O }); Assert.Equal (3, tv.SelectedRow); top.Dispose (); } [Fact] [SetupFakeDriver] public void Test_ScreenToCell () { TableView tableView = GetTwoRowSixColumnTable (); tableView.BeginInit (); tableView.EndInit (); tableView.LayoutSubviews (); tableView.Draw (); // user can only scroll right so sees right indicator // Because first column in table is A var expected = @" │A│B│C│ ├─┼─┼─► │1│2│3│ │1│2│3│"; TestHelpers.AssertDriverContentsAre (expected, output); // ---------------- X=0 ----------------------- // click is before first cell Assert.Null (tableView.ScreenToCell (0, 0)); Assert.Null (tableView.ScreenToCell (0, 1)); Assert.Null (tableView.ScreenToCell (0, 2)); Assert.Null (tableView.ScreenToCell (0, 3)); Assert.Null (tableView.ScreenToCell (0, 4)); // ---------------- X=1 ----------------------- // click in header Assert.Null (tableView.ScreenToCell (1, 0)); // click in header row line Assert.Null (tableView.ScreenToCell (1, 1)); // click in cell 0,0 Assert.Equal (Point.Empty, tableView.ScreenToCell (1, 2)); // click in cell 0,1 Assert.Equal (new Point (0, 1), tableView.ScreenToCell (1, 3)); // after last row Assert.Null (tableView.ScreenToCell (1, 4)); // ---------------- X=2 ----------------------- // ( even though there is a horizontal dividing line here we treat it as a hit on the cell before) // click in header Assert.Null (tableView.ScreenToCell (2, 0)); // click in header row line Assert.Null (tableView.ScreenToCell (2, 1)); // click in cell 0,0 Assert.Equal (Point.Empty, tableView.ScreenToCell (2, 2)); // click in cell 0,1 Assert.Equal (new Point (0, 1), tableView.ScreenToCell (2, 3)); // after last row Assert.Null (tableView.ScreenToCell (2, 4)); // ---------------- X=3 ----------------------- // click in header Assert.Null (tableView.ScreenToCell (3, 0)); // click in header row line Assert.Null (tableView.ScreenToCell (3, 1)); // click in cell 1,0 Assert.Equal (new Point (1, 0), tableView.ScreenToCell (3, 2)); // click in cell 1,1 Assert.Equal (new Point (1, 1), tableView.ScreenToCell (3, 3)); // after last row Assert.Null (tableView.ScreenToCell (3, 4)); } [Fact] [SetupFakeDriver] public void Test_ScreenToCell_DataColumnOverload () { TableView tableView = GetTwoRowSixColumnTable (); tableView.LayoutSubviews (); tableView.Draw (); // user can only scroll right so sees right indicator // Because first column in table is A var expected = @" │A│B│C│ ├─┼─┼─► │1│2│3│ │1│2│3│"; TestHelpers.AssertDriverContentsAre (expected, output); int? col; // ---------------- X=0 ----------------------- // click is before first cell Assert.Null (tableView.ScreenToCell (0, 0, out col)); Assert.Null (col); Assert.Null (tableView.ScreenToCell (0, 1, out col)); Assert.Null (col); Assert.Null (tableView.ScreenToCell (0, 2, out col)); Assert.Null (col); Assert.Null (tableView.ScreenToCell (0, 3, out col)); Assert.Null (col); Assert.Null (tableView.ScreenToCell (0, 4, out col)); Assert.Null (col); // ---------------- X=1 ----------------------- // click in header Assert.Null (tableView.ScreenToCell (1, 0, out col)); Assert.Equal ("A", tableView.Table.ColumnNames [col.Value]); // click in header row line (click in the horizontal line below header counts as click in header above - consistent with the column hit box) Assert.Null (tableView.ScreenToCell (1, 1, out col)); Assert.Equal ("A", tableView.Table.ColumnNames [col.Value]); // click in cell 0,0 Assert.Equal (Point.Empty, tableView.ScreenToCell (1, 2, out col)); Assert.Null (col); // click in cell 0,1 Assert.Equal (new Point (0, 1), tableView.ScreenToCell (1, 3, out col)); Assert.Null (col); // after last row Assert.Null (tableView.ScreenToCell (1, 4, out col)); Assert.Null (col); // ---------------- X=2 ----------------------- // click in header Assert.Null (tableView.ScreenToCell (2, 0, out col)); Assert.Equal ("A", tableView.Table.ColumnNames [col.Value]); // click in header row line Assert.Null (tableView.ScreenToCell (2, 1, out col)); Assert.Equal ("A", tableView.Table.ColumnNames [col.Value]); // click in cell 0,0 Assert.Equal (Point.Empty, tableView.ScreenToCell (2, 2, out col)); Assert.Null (col); // click in cell 0,1 Assert.Equal (new Point (0, 1), tableView.ScreenToCell (2, 3, out col)); Assert.Null (col); // after last row Assert.Null (tableView.ScreenToCell (2, 4, out col)); Assert.Null (col); // ---------------- X=3 ----------------------- // click in header Assert.Null (tableView.ScreenToCell (3, 0, out col)); Assert.Equal ("B", tableView.Table.ColumnNames [col.Value]); // click in header row line Assert.Null (tableView.ScreenToCell (3, 1, out col)); Assert.Equal ("B", tableView.Table.ColumnNames [col.Value]); // click in cell 1,0 Assert.Equal (new Point (1, 0), tableView.ScreenToCell (3, 2, out col)); Assert.Null (col); // click in cell 1,1 Assert.Equal (new Point (1, 1), tableView.ScreenToCell (3, 3, out col)); Assert.Null (col); // after last row Assert.Null (tableView.ScreenToCell (3, 4, out col)); Assert.Null (col); } [Fact] public void Test_SumColumnWidth_UnicodeLength () { Assert.Equal (11, "hello there".EnumerateRunes ().Sum (c => c.GetColumns ())); // Creates a string with the peculiar (french?) r symbol string surrogate = "Les Mise" + char.ConvertFromUtf32 (int.Parse ("0301", NumberStyles.HexNumber)) + "rables"; // The unicode width of this string is shorter than the string length! Assert.Equal (14, surrogate.EnumerateRunes ().Sum (c => c.GetColumns ())); Assert.Equal (15, surrogate.Length); } [Fact] [SetupFakeDriver] public void TestColumnStyle_AllColumnsVisibleFalse_BehavesAsTableNull () { TableView tableView = GetABCDEFTableView (out DataTable dt); for (var i = 0; i < 6; i++) { tableView.Style.GetOrCreateColumnStyle (i).Visible = false; } tableView.LayoutSubviews (); // expect nothing to be rendered when all columns are invisible var expected = @" "; tableView.Draw (); TestHelpers.AssertDriverContentsAre (expected, output); // expect behavior to match when Table is null tableView.Table = null; tableView.Draw (); TestHelpers.AssertDriverContentsAre (expected, output); } [InlineData (true)] [InlineData (false)] [Theory] [SetupFakeDriver] public void TestColumnStyle_FirstColumnVisibleFalse_CursorStaysAt1 (bool useHome) { TableView tableView = GetABCDEFTableView (out DataTable dt); tableView.LayoutSubviews (); tableView.Style.GetOrCreateColumnStyle (0).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.NewKeyDownEvent ( new() { KeyCode = useHome ? KeyCode.Home : KeyCode.CursorLeft } ); // Expect the cursor to stay at 1 Assert.Equal (1, tableView.SelectedColumn); } [Fact] [SetupFakeDriver] public void TestColumnStyle_FirstColumnVisibleFalse_IsNotRendered () { TableView tableView = GetABCDEFTableView (out DataTable dt); tableView.Style.ShowHorizontalScrollIndicators = true; tableView.Style.ShowHorizontalHeaderUnderline = true; tableView.Style.GetOrCreateColumnStyle (0).Visible = false; tableView.LayoutSubviews (); tableView.Draw (); var expected = @" │B│C│D│ ├─┼─┼─► │2│3│4│"; TestHelpers.AssertDriverContentsAre (expected, output); } [InlineData (true)] [InlineData (false)] [Theory] [SetupFakeDriver] public void TestColumnStyle_LastColumnVisibleFalse_CursorStaysAt2 (bool useEnd) { TableView tableView = GetABCDEFTableView (out DataTable dt); tableView.LayoutSubviews (); // select D tableView.SelectedColumn = 3; Assert.Equal (3, tableView.SelectedColumn); tableView.Style.GetOrCreateColumnStyle (3).Visible = false; tableView.Style.GetOrCreateColumnStyle (4).Visible = false; tableView.Style.GetOrCreateColumnStyle (5).Visible = false; // column D is invisible so this method should move to 2 (C) tableView.EnsureValidSelection (); Assert.Equal (2, tableView.SelectedColumn); tableView.NewKeyDownEvent ( new() { KeyCode = useEnd ? KeyCode.End : KeyCode.CursorRight } ); // Expect the cursor to stay at 2 Assert.Equal (2, tableView.SelectedColumn); } [Fact] [SetupFakeDriver] public void TestColumnStyle_PreceedingColumnsInvisible_NoScrollIndicator () { TableView tableView = GetABCDEFTableView (out DataTable dt); tableView.Style.ShowHorizontalScrollIndicators = true; tableView.Style.ShowHorizontalHeaderUnderline = true; tableView.ColumnOffset = 1; tableView.LayoutSubviews (); tableView.Draw (); // normally we should have scroll indicators because A,E and F are of screen var 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 (4).Visible = false; tableView.Style.GetOrCreateColumnStyle (5).Visible = false; expected = @" │B│C│D│ ◄─┼─┼─┤ │2│3│4│"; tableView.Draw (); TestHelpers.AssertDriverContentsAre (expected, output); // now also A is invisible so we cannot scroll in either direction tableView.Style.GetOrCreateColumnStyle (0).Visible = false; expected = @" │B│C│D│ ├─┼─┼─┤ │2│3│4│"; tableView.Draw (); TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] [SetupFakeDriver] public void TestColumnStyle_RemainingColumnsInvisible_NoScrollIndicator () { TableView tableView = GetABCDEFTableView (out DataTable dt); tableView.Style.ShowHorizontalScrollIndicators = true; tableView.Style.ShowHorizontalHeaderUnderline = true; tableView.LayoutSubviews (); tableView.Draw (); // normally we should have scroll indicators because DEF are of screen var 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 (3).Visible = false; tableView.Style.GetOrCreateColumnStyle (4).Visible = false; tableView.Style.GetOrCreateColumnStyle (5).Visible = false; expected = @" │A│B│C│ ├─┼─┼─┤ │1│2│3│"; tableView.Draw (); TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] [SetupFakeDriver] public void TestColumnStyle_VisibleFalse_CursorStepsOverInvisibleColumns () { TableView tableView = GetABCDEFTableView (out DataTable dt); tableView.LayoutSubviews (); tableView.Style.GetOrCreateColumnStyle (1).Visible = false; tableView.SelectedColumn = 0; tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); // Expect the cursor navigation to skip over the invisible column(s) Assert.Equal (2, tableView.SelectedColumn); tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorLeft }); // Expect the cursor navigation backwards to skip over invisible column too Assert.Equal (0, tableView.SelectedColumn); } [Theory] [SetupFakeDriver] [InlineData (true, true)] [InlineData (false, true)] [InlineData (true, false)] [InlineData (false, false)] public void TestColumnStyle_VisibleFalse_DoesNotEffect_EnsureSelectedCellIsVisible ( bool smooth, bool invisibleCol ) { TableView tableView = GetABCDEFTableView (out DataTable dt); tableView.LayoutSubviews (); tableView.Style.SmoothHorizontalScrolling = smooth; if (invisibleCol) { tableView.Style.GetOrCreateColumnStyle (3).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] [SetupFakeDriver] public void TestColumnStyle_VisibleFalse_IsNotRendered () { TableView tableView = GetABCDEFTableView (out _); tableView.Style.GetOrCreateColumnStyle (1).Visible = false; tableView.LayoutSubviews (); tableView.Draw (); var expected = @" │A│C│D│ │1│3│4│"; TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] [SetupFakeDriver] public void TestColumnStyle_VisibleFalse_MultiSelected () { TableView tableView = GetABCDEFTableView (out DataTable dt); tableView.LayoutSubviews (); // user has rectangular selection tableView.MultiSelectedRegions.Push ( new ( Point.Empty, new (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 (1).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 (1, 0), tableView.GetAllSelectedCells ()); } [Fact] [SetupFakeDriver] public void TestColumnStyle_VisibleFalse_MultiSelectingStepsOverInvisibleColumns () { TableView tableView = GetABCDEFTableView (out _); tableView.LayoutSubviews (); // if middle column is invisible tableView.Style.GetOrCreateColumnStyle (1).Visible = false; tableView.NewKeyDownEvent (Key.CursorRight.WithShift); // 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 (1, 0), tableView.GetAllSelectedCells ()); } [Fact] [SetupFakeDriver] public void TestControlClick_MultiSelect_ThreeRowTable_FullRowSelect () { TableView tv = GetTwoRowSixColumnTable (out DataTable dt); dt.Rows.Add (1, 2, 3, 4, 5, 6); tv.LayoutSubviews (); tv.MultiSelect = true; // Clicking in bottom row tv.NewMouseEvent ( new() { Position = new (1, 4), Flags = MouseFlags.Button1Clicked } ); // should select that row Assert.Equal (2, tv.SelectedRow); // shift clicking top row tv.NewMouseEvent ( new() { Position = new (1, 2), Flags = MouseFlags.Button1Clicked | MouseFlags.ButtonCtrl } ); // should extend the selection // to include bottom and top row but not middle Assert.Equal (0, tv.SelectedRow); Point [] selected = tv.GetAllSelectedCells ().ToArray (); Assert.Contains (Point.Empty, selected); Assert.DoesNotContain (new (0, 1), selected); Assert.Contains (new (0, 2), selected); } [Fact] [SetupFakeDriver] public void TestEnumerableDataSource_BasicTypes () { ((FakeDriver)Application.Driver!).SetBufferSize(100,100); var tv = new TableView (); tv.ColorScheme = Colors.ColorSchemes ["TopLevel"]; tv.Viewport = new (0, 0, 50, 6); tv.Table = new EnumerableTableSource ( new [] { typeof (string), typeof (int), typeof (float) }, new() { { "Name", t => t.Name }, { "Namespace", t => t.Namespace }, { "BaseType", t => t.BaseType } } ); tv.LayoutSubviews (); tv.Draw (); var expected = @" ┌──────┬─────────┬───────────────────────────────┐ │Name │Namespace│BaseType │ ├──────┼─────────┼───────────────────────────────┤ │String│System │System.Object │ │Int32 │System │System.ValueType │ │Single│System │System.ValueType │"; TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] [SetupFakeDriver] public void TestFullRowSelect_AlwaysUseNormalColorForVerticalCellLines () { TableView tv = GetTwoRowSixColumnTable (out DataTable dt); dt.Rows.Add (1, 2, 3, 4, 5, 6); tv.Viewport = new (0, 0, 7, 6); tv.Frame = new (0, 0, 7, 6); tv.LayoutSubviews (); tv.FullRowSelect = true; tv.Style.ShowHorizontalBottomline = true; tv.Style.AlwaysUseNormalColorForVerticalCellLines = true; // Clicking in bottom row tv.NewMouseEvent ( new() { Position = new (1, 4), Flags = MouseFlags.Button1Clicked } ); // should select that row Assert.Equal (2, tv.SelectedRow); tv.OnDrawContent (tv.Viewport); var expected = @" │A│B│C│ ├─┼─┼─► │1│2│3│ │1│2│3│ │1│2│3│ └─┴─┴─┘"; TestHelpers.AssertDriverContentsAre (expected, output); Attribute normal = tv.ColorScheme.Normal; tv.ColorScheme = new (tv.ColorScheme) { Focus = new (Color.Magenta, Color.White) }; Attribute focus = tv.ColorScheme.Focus; tv.Draw (); // Focus color (1) should be used for cells only because // AlwaysUseNormalColorForVerticalCellLines is true expected = @" 0000000 0000000 0000000 0000000 0101010 0000000"; TestHelpers.AssertDriverAttributesAre (expected, Application.Driver, normal, focus); } [Fact] [SetupFakeDriver] public void TestFullRowSelect_SelectionColorDoesNotStop_WhenShowVerticalCellLinesIsFalse () { TableView tv = GetTwoRowSixColumnTable (out DataTable dt); dt.Rows.Add (1, 2, 3, 4, 5, 6); tv.LayoutSubviews (); tv.Viewport = new (0, 0, 7, 6); tv.FullRowSelect = true; tv.Style.ShowVerticalCellLines = false; tv.Style.ShowVerticalHeaderLines = false; // Clicking in bottom row tv.NewMouseEvent ( new() { Position = new (1, 4), Flags = MouseFlags.Button1Clicked } ); // should select that row Assert.Equal (2, tv.SelectedRow); tv.Draw (); var expected = @" A B C ─────── 1 2 3 1 2 3 1 2 3"; TestHelpers.AssertDriverContentsAre (expected, output); Attribute normal = tv.ColorScheme.Normal; tv.ColorScheme = new (tv.ColorScheme) { Focus = new (Color.Magenta, Color.White) }; Attribute focus = tv.ColorScheme.Focus; tv.Draw (); // Focus color (1) should be used for rendering the selected line // Note that because there are no vertical cell lines we use the focus // color for the whole row expected = @" 000000 000000 000000 000000 111111"; TestHelpers.AssertDriverAttributesAre (expected, Application.Driver, normal, focus); } [Fact] [SetupFakeDriver] public void TestFullRowSelect_SelectionColorStopsAtTableEdge_WithCellLines () { TableView tv = GetTwoRowSixColumnTable (out DataTable dt); dt.Rows.Add (1, 2, 3, 4, 5, 6); tv.Viewport = new (0, 0, 7, 6); tv.Frame = new (0, 0, 7, 6); tv.LayoutSubviews (); tv.FullRowSelect = true; tv.Style.ShowHorizontalBottomline = true; // Clicking in bottom row tv.NewMouseEvent ( new() { Position = new (1, 4), Flags = MouseFlags.Button1Clicked } ); // should select that row Assert.Equal (2, tv.SelectedRow); tv.OnDrawContent (tv.Viewport); var expected = @" │A│B│C│ ├─┼─┼─► │1│2│3│ │1│2│3│ │1│2│3│ └─┴─┴─┘"; TestHelpers.AssertDriverContentsAre (expected, output); Attribute normal = tv.ColorScheme.Normal; tv.ColorScheme = new (tv.ColorScheme) { Focus = new (Color.Magenta, Color.White) }; Attribute focus = tv.ColorScheme.Focus; tv.Draw (); // Focus color (1) should be used for rendering the selected line // But should not spill into the borders. Normal color (0) should be // used for the rest. expected = @" 0000000 0000000 0000000 0000000 0111110 0000000"; TestHelpers.AssertDriverAttributesAre (expected, Application.Driver, normal, focus); } [Theory] [SetupFakeDriver] [InlineData (Orientation.Horizontal, false)] [InlineData (Orientation.Vertical, false)] [InlineData (Orientation.Horizontal, true)] [InlineData (Orientation.Vertical, true)] public void TestListTableSource (Orientation orient, bool parallel) { IList list = BuildList (16); var tv = new TableView (); //tv.BeginInit (); tv.EndInit (); tv.ColorScheme = Colors.ColorSchemes ["TopLevel"]; tv.Viewport = new (0, 0, 25, 4); tv.Style = new() { ShowHeaders = false, ShowHorizontalHeaderOverline = false, ShowHorizontalHeaderUnderline = false }; var listStyle = new ListColumnStyle { Orientation = orient, ScrollParallel = parallel }; tv.Table = new ListTableSource (list, tv, listStyle); tv.LayoutSubviews (); tv.Draw (); var horizPerpExpected = @" │Item 0│Item 1 │ │Item 2│Item 3 │ │Item 4│Item 5 │ │Item 6│Item 7 │"; var horizParaExpected = @" │Item 0 │Item 1 │Item 2 │ │Item 4 │Item 5 │Item 6 │ │Item 8 │Item 9 │Item 10│ │Item 12│Item 13│Item 14│"; var vertPerpExpected = @" │Item 0│Item 4│Item 8 │ │Item 1│Item 5│Item 9 │ │Item 2│Item 6│Item 10 │ │Item 3│Item 7│Item 11 │"; var vertParaExpected = @" │Item 0│Item 8 │ │Item 1│Item 9 │ │Item 2│Item 10 │ │Item 3│Item 11 │"; string expected; if (orient == Orientation.Vertical) { if (parallel) { expected = vertParaExpected; } else { expected = vertPerpExpected; } } else { if (parallel) { expected = horizParaExpected; } else { expected = horizPerpExpected; } } TestHelpers.AssertDriverContentsAre (expected, output); } [InlineData (true)] [InlineData (false)] [Theory] [SetupFakeDriver] public void TestMoveStartEnd_WithFullRowSelect (bool withFullRowSelect) { TableView tableView = GetTwoRowSixColumnTable (); tableView.LayoutSubviews (); tableView.FullRowSelect = withFullRowSelect; tableView.SelectedRow = 1; tableView.SelectedColumn = 1; tableView.NewKeyDownEvent (Key.Home.WithCtrl); if (withFullRowSelect) { // Should not be any horizontal movement when // using navigate to Start/End and FullRowSelect Assert.Equal (1, tableView.SelectedColumn); Assert.Equal (0, tableView.SelectedRow); } else { Assert.Equal (0, tableView.SelectedColumn); Assert.Equal (0, tableView.SelectedRow); } tableView.NewKeyDownEvent ( new ( KeyCode.End | KeyCode.CtrlMask ) ); if (withFullRowSelect) { Assert.Equal (1, tableView.SelectedColumn); Assert.Equal (1, tableView.SelectedRow); } else { Assert.Equal (5, tableView.SelectedColumn); Assert.Equal (1, tableView.SelectedRow); } } [Fact] [SetupFakeDriver] public void TestShiftClick_MultiSelect_TwoRowTable_FullRowSelect () { TableView tv = GetTwoRowSixColumnTable (); tv.LayoutSubviews (); tv.MultiSelect = true; // Clicking in bottom row tv.NewMouseEvent ( new() { Position = new (1, 3), Flags = MouseFlags.Button1Clicked } ); // should select that row Assert.Equal (1, tv.SelectedRow); // shift clicking top row tv.NewMouseEvent ( new() { Position = new (1, 2), Flags = MouseFlags.Button1Clicked | MouseFlags.ButtonShift } ); // should extend the selection Assert.Equal (0, tv.SelectedRow); Point [] selected = tv.GetAllSelectedCells ().ToArray (); Assert.Contains (Point.Empty, selected); Assert.Contains (new (0, 1), selected); } [Fact] [SetupFakeDriver] public void TestTableViewCheckboxes_ByObject () { TableView tv = GetPetTable (out EnumerableTableSource source); tv.LayoutSubviews (); IReadOnlyCollection pets = source.Data; CheckBoxTableSourceWrapperByObject wrapper = new ( tv, source, p => p.IsPicked, (p, b) => p.IsPicked = b ); tv.Table = wrapper; tv.Draw (); var expected = @" ┌─┬───────┬─────────────┐ │ │Name │Kind │ ├─┼───────┼─────────────┤ │☐│Tammy │Cat │ │☐│Tibbles│Cat │ │☐│Ripper │Dog │"; TestHelpers.AssertDriverContentsAre (expected, output); Assert.Empty (pets.Where (p => p.IsPicked)); tv.NewKeyDownEvent (Key.Space); Assert.True (pets.First ().IsPicked); tv.Draw (); expected = @" ┌─┬───────┬─────────────┐ │ │Name │Kind │ ├─┼───────┼─────────────┤ │☑│Tammy │Cat │ │☐│Tibbles│Cat │ │☐│Ripper │Dog │"; TestHelpers.AssertDriverContentsAre (expected, output); tv.NewKeyDownEvent (Key.CursorDown); tv.NewKeyDownEvent (Key.Space); Assert.True (pets.ElementAt (0).IsPicked); Assert.True (pets.ElementAt (1).IsPicked); Assert.False (pets.ElementAt (2).IsPicked); tv.Draw (); expected = @" ┌─┬───────┬─────────────┐ │ │Name │Kind │ ├─┼───────┼─────────────┤ │☑│Tammy │Cat │ │☑│Tibbles│Cat │ │☐│Ripper │Dog │"; TestHelpers.AssertDriverContentsAre (expected, output); tv.NewKeyDownEvent (Key.CursorUp); tv.NewKeyDownEvent (Key.Space); Assert.False (pets.ElementAt (0).IsPicked); Assert.True (pets.ElementAt (1).IsPicked); Assert.False (pets.ElementAt (2).IsPicked); tv.Draw (); expected = @" ┌─┬───────┬─────────────┐ │ │Name │Kind │ ├─┼───────┼─────────────┤ │☐│Tammy │Cat │ │☑│Tibbles│Cat │ │☐│Ripper │Dog │"; TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] [SetupFakeDriver] public void TestTableViewCheckboxes_MultiSelectIsUnion_WhenToggling () { TableView tv = GetTwoRowSixColumnTable (out DataTable dt); dt.Rows.Add (1, 2, 3, 4, 5, 6); tv.LayoutSubviews (); var wrapper = new CheckBoxTableSourceWrapperByIndex (tv, tv.Table); tv.Table = wrapper; wrapper.CheckedRows.Add (0); wrapper.CheckedRows.Add (2); tv.Draw (); var expected = @" │ │A│B│ ├─┼─┼─► │☑│1│2│ │☐│1│2│ │☑│1│2│"; //toggle top two at once tv.NewKeyDownEvent (Key.CursorDown.WithShift); Assert.True (tv.IsSelected (0, 0)); Assert.True (tv.IsSelected (0, 1)); tv.NewKeyDownEvent (Key.Space); // Because at least 1 of the rows is not yet ticked we toggle them all to ticked TestHelpers.AssertDriverContentsAre (expected, output); Assert.Contains (0, wrapper.CheckedRows); Assert.Contains (1, wrapper.CheckedRows); Assert.Contains (2, wrapper.CheckedRows); Assert.Equal (3, wrapper.CheckedRows.Count); tv.Draw (); expected = @" │ │A│B│ ├─┼─┼─► │☑│1│2│ │☑│1│2│ │☑│1│2│"; TestHelpers.AssertDriverContentsAre (expected, output); // Untoggle the top 2 tv.NewKeyDownEvent (Key.Space); tv.Draw (); expected = @" │ │A│B│ ├─┼─┼─► │☐│1│2│ │☐│1│2│ │☑│1│2│"; TestHelpers.AssertDriverContentsAre (expected, output); Assert.Single (wrapper.CheckedRows, 2); } [Fact] [SetupFakeDriver] public void TestTableViewCheckboxes_SelectAllToggle () { TableView tv = GetTwoRowSixColumnTable (out DataTable dt); dt.Rows.Add (1, 2, 3, 4, 5, 6); tv.LayoutSubviews (); var wrapper = new CheckBoxTableSourceWrapperByIndex (tv, tv.Table); tv.Table = wrapper; //toggle all cells tv.NewKeyDownEvent (Key.A.WithCtrl); tv.NewKeyDownEvent (Key.Space); tv.Draw (); var expected = @" │ │A│B│ ├─┼─┼─► │☑│1│2│ │☑│1│2│ │☑│1│2│"; TestHelpers.AssertDriverContentsAre (expected, output); Assert.Contains (0, wrapper.CheckedRows); Assert.Contains (1, wrapper.CheckedRows); Assert.Contains (2, wrapper.CheckedRows); Assert.Equal (3, wrapper.CheckedRows.Count); // Untoggle all again tv.NewKeyDownEvent (Key.Space); tv.Draw (); expected = @" │ │A│B│ ├─┼─┼─► │☐│1│2│ │☐│1│2│ │☐│1│2│"; TestHelpers.AssertDriverContentsAre (expected, output); Assert.Empty (wrapper.CheckedRows); } [Fact] [SetupFakeDriver] public void TestTableViewCheckboxes_SelectAllToggle_ByObject () { TableView tv = GetPetTable (out EnumerableTableSource source); tv.LayoutSubviews (); IReadOnlyCollection pets = source.Data; CheckBoxTableSourceWrapperByObject wrapper = new ( tv, source, p => p.IsPicked, (p, b) => p.IsPicked = b ); tv.Table = wrapper; Assert.DoesNotContain (pets, p => p.IsPicked); //toggle all cells tv.NewKeyDownEvent (Key.A.WithCtrl); tv.NewKeyDownEvent (Key.Space); Assert.True (pets.All (p => p.IsPicked)); tv.Draw (); var expected = @" ┌─┬───────┬─────────────┐ │ │Name │Kind │ ├─┼───────┼─────────────┤ │☑│Tammy │Cat │ │☑│Tibbles│Cat │ │☑│Ripper │Dog │"; TestHelpers.AssertDriverContentsAre (expected, output); tv.NewKeyDownEvent (Key.Space); Assert.Empty (pets.Where (p => p.IsPicked)); tv.Draw (); expected = @" ┌─┬───────┬─────────────┐ │ │Name │Kind │ ├─┼───────┼─────────────┤ │☐│Tammy │Cat │ │☐│Tibbles│Cat │ │☐│Ripper │Dog │ "; TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] [SetupFakeDriver] public void TestTableViewCheckboxes_Simple () { TableView tv = GetTwoRowSixColumnTable (out DataTable dt); dt.Rows.Add (1, 2, 3, 4, 5, 6); tv.LayoutSubviews (); var wrapper = new CheckBoxTableSourceWrapperByIndex (tv, tv.Table); tv.Table = wrapper; tv.Draw (); var expected = @" │ │A│B│ ├─┼─┼─► │☐│1│2│ │☐│1│2│ │☐│1│2│"; TestHelpers.AssertDriverContentsAre (expected, output); Assert.Empty (wrapper.CheckedRows); //toggle the top cell tv.NewKeyDownEvent (Key.Space); Assert.Single (wrapper.CheckedRows, 0); tv.Draw (); expected = @" │ │A│B│ ├─┼─┼─► │☑│1│2│ │☐│1│2│ │☐│1│2│"; TestHelpers.AssertDriverContentsAre (expected, output); tv.NewKeyDownEvent (Key.CursorDown); tv.NewKeyDownEvent (Key.Space); Assert.Contains (0, wrapper.CheckedRows); Assert.Contains (1, wrapper.CheckedRows); Assert.Equal (2, wrapper.CheckedRows.Count); tv.Draw (); expected = @" │ │A│B│ ├─┼─┼─► │☑│1│2│ │☑│1│2│ │☐│1│2│"; TestHelpers.AssertDriverContentsAre (expected, output); // untoggle top one tv.NewKeyDownEvent (Key.CursorUp); tv.NewKeyDownEvent (Key.Space); Assert.Single (wrapper.CheckedRows, 1); tv.Draw (); expected = @" │ │A│B│ ├─┼─┼─► │☐│1│2│ │☑│1│2│ │☐│1│2│"; TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] [SetupFakeDriver] public void TestTableViewRadioBoxes_Simple_ByObject () { TableView tv = GetPetTable (out EnumerableTableSource source); tv.LayoutSubviews (); IReadOnlyCollection pets = source.Data; CheckBoxTableSourceWrapperByObject wrapper = new ( tv, source, p => p.IsPicked, (p, b) => p.IsPicked = b ); wrapper.UseRadioButtons = true; tv.Table = wrapper; tv.Draw (); var expected = @" ┌─┬───────┬─────────────┐ │ │Name │Kind │ ├─┼───────┼─────────────┤ │○│Tammy │Cat │ │○│Tibbles│Cat │ │○│Ripper │Dog │ "; TestHelpers.AssertDriverContentsAre (expected, output); Assert.Empty (pets.Where (p => p.IsPicked)); tv.NewKeyDownEvent (Key.Space); Assert.True (pets.First ().IsPicked); tv.Draw (); expected = @" ┌─┬───────┬─────────────┐ │ │Name │Kind │ ├─┼───────┼─────────────┤ │◉│Tammy │Cat │ │○│Tibbles│Cat │ │○│Ripper │Dog │"; TestHelpers.AssertDriverContentsAre (expected, output); tv.NewKeyDownEvent (Key.CursorDown); tv.NewKeyDownEvent (Key.Space); Assert.False (pets.ElementAt (0).IsPicked); Assert.True (pets.ElementAt (1).IsPicked); Assert.False (pets.ElementAt (2).IsPicked); tv.Draw (); expected = @" ┌─┬───────┬─────────────┐ │ │Name │Kind │ ├─┼───────┼─────────────┤ │○│Tammy │Cat │ │◉│Tibbles│Cat │ │○│Ripper │Dog │"; TestHelpers.AssertDriverContentsAre (expected, output); tv.NewKeyDownEvent (Key.CursorUp); tv.NewKeyDownEvent (Key.Space); Assert.True (pets.ElementAt (0).IsPicked); Assert.False (pets.ElementAt (1).IsPicked); Assert.False (pets.ElementAt (2).IsPicked); tv.Draw (); expected = @" ┌─┬───────┬─────────────┐ │ │Name │Kind │ ├─┼───────┼─────────────┤ │◉│Tammy │Cat │ │○│Tibbles│Cat │ │○│Ripper │Dog │"; TestHelpers.AssertDriverContentsAre (expected, output); } [Fact] public void TestToggleCells_MultiSelectOn () { // 2 row table TableView tableView = GetABCDEFTableView (out DataTable dt); tableView.LayoutSubviews (); dt.Rows.Add (1, 2, 3, 4, 5, 6); tableView.MultiSelect = true; tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Select); Point selectedCell = tableView.GetAllSelectedCells ().Single (); Assert.Equal (0, selectedCell.X); Assert.Equal (0, selectedCell.Y); // Go Right tableView.NewKeyDownEvent (Key.CursorRight); selectedCell = tableView.GetAllSelectedCells ().Single (); Assert.Equal (1, selectedCell.X); Assert.Equal (0, selectedCell.Y); // Toggle Select tableView.NewKeyDownEvent (Key.Space); TableSelection m = tableView.MultiSelectedRegions.Single (); Assert.True (m.IsToggled); Assert.Equal (1, m.Origin.X); Assert.Equal (0, m.Origin.Y); selectedCell = tableView.GetAllSelectedCells ().Single (); Assert.Equal (1, selectedCell.X); Assert.Equal (0, selectedCell.Y); // Go Left tableView.NewKeyDownEvent (Key.CursorLeft); // Both Toggled and Moved to should be selected Assert.Equal (2, tableView.GetAllSelectedCells ().Count ()); Point s1 = tableView.GetAllSelectedCells ().ElementAt (0); Point s2 = tableView.GetAllSelectedCells ().ElementAt (1); Assert.Equal (1, s1.X); Assert.Equal (0, s1.Y); Assert.Equal (0, s2.X); Assert.Equal (0, s2.Y); // Go Down tableView.NewKeyDownEvent (Key.CursorDown); // Both Toggled and Moved to should be selected but not 0,0 // which we moved down from Assert.Equal (2, tableView.GetAllSelectedCells ().Count ()); s1 = tableView.GetAllSelectedCells ().ElementAt (0); s2 = tableView.GetAllSelectedCells ().ElementAt (1); Assert.Equal (1, s1.X); Assert.Equal (0, s1.Y); Assert.Equal (0, s2.X); Assert.Equal (1, s2.Y); // Go back to the toggled cell tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorUp }); // Toggle off tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.Space }); // Go Left tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorLeft }); selectedCell = tableView.GetAllSelectedCells ().Single (); Assert.Equal (0, selectedCell.X); Assert.Equal (0, selectedCell.Y); } [Fact] public void TestToggleCells_MultiSelectOn_FullRowSelect () { // 2 row table TableView tableView = GetABCDEFTableView (out DataTable dt); tableView.LayoutSubviews (); dt.Rows.Add (1, 2, 3, 4, 5, 6); tableView.FullRowSelect = true; tableView.MultiSelect = true; tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Select); // Toggle Select Cell 0,0 tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.Space }); // Go Down tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorDown }); TableSelection m = tableView.MultiSelectedRegions.Single (); Assert.True (m.IsToggled); Assert.Equal (0, m.Origin.X); Assert.Equal (0, m.Origin.Y); //First row toggled and Second row active = 12 selected cells Assert.Equal (12, tableView.GetAllSelectedCells ().Count ()); tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorUp }); Assert.Single (tableView.MultiSelectedRegions.Where (r => r.IsToggled)); // Can untoggle at 1,0 even though 0,0 was initial toggle because FullRowSelect is on tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.Space }); Assert.Empty (tableView.MultiSelectedRegions.Where (r => r.IsToggled)); } [Fact] public void TestToggleCells_MultiSelectOn_SquareSelectToggled () { // 3 row table TableView tableView = GetABCDEFTableView (out DataTable dt); tableView.LayoutSubviews (); dt.Rows.Add (1, 2, 3, 4, 5, 6); dt.Rows.Add (1, 2, 3, 4, 5, 6); tableView.MultiSelect = true; tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Select); // Make a square selection tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.ShiftMask | KeyCode.CursorDown }); tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.ShiftMask | KeyCode.CursorRight }); Assert.Equal (4, tableView.GetAllSelectedCells ().Count ()); // Toggle the square selected region on tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.Space }); // Go Right tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorRight }); //Toggled on square + the active cell (x=2,y=1) Assert.Equal (5, tableView.GetAllSelectedCells ().Count ()); Assert.Equal (2, tableView.SelectedColumn); Assert.Equal (1, tableView.SelectedRow); // Untoggle the rectangular region by hitting toggle in // any cell in that rect tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorUp }); tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorLeft }); Assert.Equal (4, tableView.GetAllSelectedCells ().Count ()); tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.Space }); Assert.Single (tableView.GetAllSelectedCells ()); } [Fact] public void TestToggleCells_MultiSelectOn_Two_SquareSelects_BothToggled () { // 6 row table TableView tableView = GetABCDEFTableView (out DataTable dt); tableView.LayoutSubviews (); dt.Rows.Add (1, 2, 3, 4, 5, 6); dt.Rows.Add (1, 2, 3, 4, 5, 6); dt.Rows.Add (1, 2, 3, 4, 5, 6); dt.Rows.Add (1, 2, 3, 4, 5, 6); dt.Rows.Add (1, 2, 3, 4, 5, 6); tableView.MultiSelect = true; tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Select); // Make first square selection (0,0 to 1,1) tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.ShiftMask | KeyCode.CursorDown }); tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.ShiftMask | KeyCode.CursorRight }); tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.Space }); Assert.Equal (4, tableView.GetAllSelectedCells ().Count ()); // Make second square selection leaving 1 unselected line between them tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorLeft }); tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorDown }); tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.CursorDown }); tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.ShiftMask | KeyCode.CursorDown }); tableView.NewKeyDownEvent (new() { KeyCode = KeyCode.ShiftMask | KeyCode.CursorRight }); // 2 square selections Assert.Equal (8, tableView.GetAllSelectedCells ().Count ()); } private TableView GetABCDEFTableView (out DataTable dt) { var tableView = new TableView (); tableView.BeginInit (); tableView.EndInit (); tableView.ColorScheme = Colors.ColorSchemes ["TopLevel"]; // 3 columns are visible tableView.Viewport = new (0, 0, 7, 5); tableView.Style.ShowHorizontalHeaderUnderline = false; tableView.Style.ShowHorizontalHeaderOverline = false; tableView.Style.AlwaysShowHeaders = true; tableView.Style.SmoothHorizontalScrolling = false; dt = new (); 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 = new DataTableSource (dt); return tableView; } private TableView GetPetTable (out EnumerableTableSource source) { var tv = new TableView (); tv.ColorScheme = Colors.ColorSchemes ["TopLevel"]; tv.Viewport = new (0, 0, 25, 6); List pets = new () { new (false, "Tammy", "Cat"), new (false, "Tibbles", "Cat"), new (false, "Ripper", "Dog") }; tv.Table = source = new ( pets, new() { { "Name", p => p.Name }, { "Kind", p => p.Kind } } ); tv.LayoutSubviews (); return tv; } private TableView GetTwoRowSixColumnTable () { return GetTwoRowSixColumnTable (out _); } private TableView GetTwoRowSixColumnTable (out DataTable dt) { var tableView = new TableView (); tableView.ColorScheme = Colors.ColorSchemes ["TopLevel"]; // 3 columns are visible tableView.Viewport = new (0, 0, 7, 5); tableView.Style.ShowHorizontalHeaderUnderline = true; tableView.Style.ShowHorizontalHeaderOverline = false; tableView.Style.AlwaysShowHeaders = true; tableView.Style.SmoothHorizontalScrolling = true; dt = new (); 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); dt.Rows.Add (1, 2, 3, 4, 5, 6); tableView.Table = new DataTableSource (dt); return tableView; } private TableView SetUpMiniTable () { return SetUpMiniTable (out _); } private TableView SetUpMiniTable (out DataTable dt) { var tv = new TableView (); tv.BeginInit (); tv.EndInit (); tv.Viewport = new (0, 0, 10, 4); dt = new (); dt.Columns.Add ("A"); dt.Columns.Add ("B"); dt.Rows.Add (1, 2); tv.Table = new DataTableSource (dt); tv.Style.GetOrCreateColumnStyle (0).MinWidth = 1; tv.Style.GetOrCreateColumnStyle (0).MinWidth = 1; tv.Style.GetOrCreateColumnStyle (1).MaxWidth = 1; tv.Style.GetOrCreateColumnStyle (1).MaxWidth = 1; tv.ColorScheme = Colors.ColorSchemes ["Base"]; return tv; } private class PickablePet { public PickablePet (bool isPicked, string name, string kind) { IsPicked = isPicked; Name = name; Kind = kind; } public bool IsPicked { get; set; } public string Kind { get; } public string Name { get; } } }