diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index ba45c59d7..e738d6d1d 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -10,6 +10,9 @@ namespace Terminal.Gui; /// public class FileDialog : Dialog { + private const int alignmentGroupInput = 32; + private const int alignmentGroupComplete = 55; + /// Gets the Path separators for the operating system internal static char [] Separators = [ @@ -71,24 +74,20 @@ public class FileDialog : Dialog _btnOk = new Button { - Y = Pos.AnchorEnd (1), X = Pos.Func (CalculateOkButtonPosX), IsDefault = true, Text = Style.OkButtonText + X = Pos.Align (Alignment.End, AlignmentModes.AddSpaceBetweenItems, alignmentGroupComplete), + Y = Pos.AnchorEnd (), + IsDefault = true, Text = Style.OkButtonText }; _btnOk.Accept += (s, e) => Accept (true); - _btnOk.KeyDown += (s, k) => - { - NavigateIf (k, KeyCode.CursorLeft, _btnCancel); - NavigateIf (k, KeyCode.CursorUp, _tableView); - }; - _btnCancel = new Button { Y = Pos.AnchorEnd (1), X = Pos.Right (_btnOk) + 1, Text = Strings.btnCancel }; + _btnCancel = new Button + { + X = Pos.Align (Alignment.End, AlignmentModes.AddSpaceBetweenItems, alignmentGroupComplete), + Y = Pos.AnchorEnd(), + Text = Strings.btnCancel + }; - _btnCancel.KeyDown += (s, k) => - { - NavigateIf (k, KeyCode.CursorLeft, _btnToggleSplitterCollapse); - NavigateIf (k, KeyCode.CursorUp, _tableView); - NavigateIf (k, KeyCode.CursorRight, _btnOk); - }; _btnCancel.Accept += (s, e) => { Canceled = true; @@ -121,7 +120,13 @@ public class FileDialog : Dialog _tbPath.Autocomplete = new AppendAutocomplete (_tbPath); _tbPath.Autocomplete.SuggestionGenerator = new FilepathSuggestionGenerator (); - _splitContainer = new TileView { X = 0, Y = 2, Width = Dim.Fill (), Height = Dim.Fill (1) }; + _splitContainer = new TileView + { + X = 0, + Y = Pos.Bottom (_btnBack), + Width = Dim.Fill (), + Height = Dim.Fill (Dim.Func (() => IsInitialized ? _btnOk.Frame.Height : 1)), + }; Initialized += (s, e) => { @@ -129,7 +134,7 @@ public class FileDialog : Dialog _splitContainer.Tiles.ElementAt (0).ContentView.Visible = false; }; - // this.splitContainer.Border.BorderStyle = BorderStyle.None; + // this.splitContainer.Border.BorderStyle = BorderStyle.None; _tableView = new TableView { @@ -158,28 +163,7 @@ public class FileDialog : Dialog ColumnStyle typeStyle = Style.TableStyle.GetOrCreateColumnStyle (3); typeStyle.MinWidth = 6; typeStyle.ColorGetter = ColorGetter; - - _tableView.KeyDown += (s, k) => - { - if (_tableView.SelectedRow <= 0) - { - NavigateIf (k, KeyCode.CursorUp, _tbPath); - } - - if (_tableView.SelectedRow == _tableView.Table.Rows - 1) - { - NavigateIf (k, KeyCode.CursorDown, _btnToggleSplitterCollapse); - } - - if (_splitContainer.Tiles.First ().ContentView.Visible && _tableView.SelectedColumn == 0) - { - NavigateIf (k, KeyCode.CursorLeft, _treeView); - } - - if (k.Handled) - { } - }; - + _treeView = new TreeView { Width = Dim.Fill (), Height = Dim.Fill () }; var fileDialogTreeBuilder = new FileSystemTreeBuilder (); @@ -192,7 +176,11 @@ public class FileDialog : Dialog _splitContainer.Tiles.ElementAt (0).ContentView.Add (_treeView); _splitContainer.Tiles.ElementAt (1).ContentView.Add (_tableView); - _btnToggleSplitterCollapse = new Button { Y = Pos.AnchorEnd (1), Text = GetToggleSplitterText (false) }; + _btnToggleSplitterCollapse = new Button + { + X = Pos.Align (Alignment.Start, AlignmentModes.AddSpaceBetweenItems, alignmentGroupInput), + Y = Pos.AnchorEnd (), Text = GetToggleSplitterText (false) + }; _btnToggleSplitterCollapse.Accept += (s, e) => { @@ -206,13 +194,13 @@ public class FileDialog : Dialog _tbFind = new TextField { - X = Pos.Right (_btnToggleSplitterCollapse) + 1, + X = Pos.Align (Alignment.Start,AlignmentModes.AddSpaceBetweenItems, alignmentGroupInput), CaptionColor = new Color (Color.Black), Width = 30, - Y = Pos.AnchorEnd (1), + Y = Pos.Top (_btnToggleSplitterCollapse), HotKey = Key.F.WithAlt }; - _spinnerView = new SpinnerView { X = Pos.Right (_tbFind) + 1, Y = Pos.AnchorEnd (1), Visible = false }; + _spinnerView = new SpinnerView { X = Pos.Align (Alignment.Start, AlignmentModes.AddSpaceBetweenItems, alignmentGroupInput), Y = Pos.AnchorEnd (1), Visible = false }; _tbFind.TextChanged += (s, o) => RestartSearch (); @@ -231,16 +219,6 @@ public class FileDialog : Dialog o.Handled = true; } } - - if (_tbFind.CursorIsAtEnd ()) - { - NavigateIf (o, KeyCode.CursorRight, _btnCancel); - } - - if (_tbFind.CursorIsAtStart ()) - { - NavigateIf (o, KeyCode.CursorLeft, _btnToggleSplitterCollapse); - } }; _tableView.Style.ShowHorizontalHeaderOverline = true; @@ -262,48 +240,22 @@ public class FileDialog : Dialog _tableView.KeyBindings.ReplaceCommands (Key.End, Command.BottomEnd); _tableView.KeyBindings.ReplaceCommands (Key.Home.WithShift, Command.TopHomeExtend); _tableView.KeyBindings.ReplaceCommands (Key.End.WithShift, Command.BottomEndExtend); - - _treeView.KeyDown += (s, k) => - { - IFileSystemInfo selected = _treeView.SelectedObject; - - if (selected is { }) - { - if (!_treeView.CanExpand (selected) || _treeView.IsExpanded (selected)) - { - NavigateIf (k, KeyCode.CursorRight, _tableView); - } - else if (_treeView.GetObjectRow (selected) == 0) - { - NavigateIf (k, KeyCode.CursorUp, _tbPath); - } - } - - if (k.Handled) - { - return; - } - - k.Handled = TreeView_KeyDown (k); - }; - + AllowsMultipleSelection = false; UpdateNavigationVisibility (); - // BUGBUG: This TabOrder is counter-intuitive. The tab order for a dialog should match the - // order the Views' are presented, left to right, top to bottom. - // Determines tab order - Add (_btnToggleSplitterCollapse); - Add (_tbFind); - Add (_spinnerView); - Add (_btnOk); - Add (_btnCancel); + Add (_tbPath); Add (_btnUp); Add (_btnBack); Add (_btnForward); - Add (_tbPath); Add (_splitContainer); + Add (_btnToggleSplitterCollapse); + Add (_tbFind); + Add (_spinnerView); + + Add(_btnOk); + Add(_btnCancel); } /// @@ -1041,23 +993,6 @@ public class FileDialog : Dialog return toReturn; } - private bool NavigateIf (Key keyEvent, KeyCode isKey, View to) - { - if (keyEvent.KeyCode == isKey) - { - to.FocusDeepest (NavigationDirection.Forward, null); - - if (to == _tbPath) - { - _tbPath.MoveEnd (); - } - - return true; - } - - return false; - } - private void New () { if (State is { }) @@ -1430,19 +1365,6 @@ public class FileDialog : Dialog } } - private bool TreeView_KeyDown (Key keyEvent) - { - if (_treeView.HasFocus && Separators.Contains ((char)keyEvent)) - { - _tbPath.FocusDeepest (NavigationDirection.Forward, null); - - // let that keystroke go through on the tbPath instead - return true; - } - - return false; - } - private void TreeView_SelectionChanged (object sender, SelectionChangedEventArgs e) { if (e.NewValue is null) diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index 10c53513d..ac9ea1113 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -54,47 +54,19 @@ public class TableView : View // Things this view knows how to do AddCommand ( Command.Right, - () => - { - // BUGBUG: SHould return false if selectokn doesn't change (to support nav to next view) - ChangeSelectionByOffset (1, 0, false); - - return true; - } - ); + () => ChangeSelectionByOffsetWithReturn (1, 0)); AddCommand ( Command.Left, - () => - { - // BUGBUG: SHould return false if selectokn doesn't change (to support nav to next view) - ChangeSelectionByOffset (-1, 0, false); - - return true; - } - ); + () => ChangeSelectionByOffsetWithReturn (-1, 0)); AddCommand ( Command.LineUp, - () => - { - // BUGBUG: SHould return false if selectokn doesn't change (to support nav to next view) - ChangeSelectionByOffset (0, -1, false); - - return true; - } - ); + () => ChangeSelectionByOffsetWithReturn (0, -1)); AddCommand ( Command.LineDown, - () => - { - // BUGBUG: SHould return false if selectokn doesn't change (to support nav to next view) - ChangeSelectionByOffset (0, 1, false); - - return true; - } - ); + () => ChangeSelectionByOffsetWithReturn (0, 1)); AddCommand ( Command.PageUp, @@ -519,6 +491,41 @@ public class TableView : View return new Point (colHit.X, tableRow + headerHeight - RowOffset); } + /// + /// Private override of that returns true if the selection has + /// changed as a result of moving the selection. Used by key handling logic to determine whether e.g. + /// the cursor right resulted in a change or should be forwarded on to toggle logic handling. + /// + /// + /// + /// + private bool ChangeSelectionByOffsetWithReturn (int offsetX, int offsetY) + { + var oldSelection = GetSelectionSnapshot (); + SetSelection (SelectedColumn + offsetX, SelectedRow + offsetY, false); + Update (); + + return !SelectionIsSame (oldSelection); + } + + private TableViewSelectionSnapshot GetSelectionSnapshot () + { + return new ( + SelectedColumn, + SelectedRow, + MultiSelectedRegions.Select (s => s.Rectangle).ToArray ()); + } + + private bool SelectionIsSame (TableViewSelectionSnapshot oldSelection) + { + var newSelection = GetSelectionSnapshot (); + + return oldSelection.SelectedColumn == newSelection.SelectedColumn + && oldSelection.SelectedRow == newSelection.SelectedRow + && oldSelection.multiSelection.SequenceEqual (newSelection.multiSelection); + } + private record TableViewSelectionSnapshot (int SelectedColumn, int SelectedRow, Rectangle [] multiSelection); + /// /// Moves the and by the provided offsets. Optionally /// starting a box selection (see ) diff --git a/UnitTests/FileServices/FileDialogTests.cs b/UnitTests/FileServices/FileDialogTests.cs index 8b8b2e19b..afa6c1b63 100644 --- a/UnitTests/FileServices/FileDialogTests.cs +++ b/UnitTests/FileServices/FileDialogTests.cs @@ -99,12 +99,13 @@ public class FileDialogTests (ITestOutputHelper output) string openIn = Path.Combine (Environment.CurrentDirectory, "zz"); Directory.CreateDirectory (openIn); dlg.Path = openIn + Path.DirectorySeparatorChar; - Application.OnKeyDown (Key.Tab); - Application.OnKeyDown (Key.Tab); - Application.OnKeyDown (Key.Tab); + + var tf = GetTextField (dlg, FileDialogPart.SearchField); + tf.SetFocus (); Assert.IsType (dlg.MostFocused); - var tf = (TextField)dlg.MostFocused; + Assert.Same (tf, dlg.MostFocused); + Assert.Equal ("Enter Search", tf.Caption); // Dialog has not yet been confirmed with a choice @@ -140,6 +141,10 @@ public class FileDialogTests (ITestOutputHelper output) Assert.IsType (dlg.MostFocused); Send ('v', ConsoleKey.DownArrow); + + var tv = GetTableView(dlg); + tv.SetFocus (); + Assert.IsType (dlg.MostFocused); // ".." should be the first thing selected @@ -177,8 +182,10 @@ public class FileDialogTests (ITestOutputHelper output) IReadOnlyCollection eventMultiSelected = null; dlg.FilesSelected += (s, e) => { eventMultiSelected = e.Dialog.MultiSelected; }; - Assert.IsType (dlg.MostFocused); - Send ('v', ConsoleKey.DownArrow); + + var tv = GetTableView (dlg); + tv.SetFocus (); + Assert.IsType (dlg.MostFocused); // Try to toggle '..' @@ -232,8 +239,9 @@ public class FileDialogTests (ITestOutputHelper output) IReadOnlyCollection eventMultiSelected = null; dlg.FilesSelected += (s, e) => { eventMultiSelected = e.Dialog.MultiSelected; }; - Assert.IsType (dlg.MostFocused); - Send ('v', ConsoleKey.DownArrow); + var tv = GetTableView (dlg); + tv.SetFocus (); + Assert.IsType (dlg.MostFocused); // Move selection to subfolder @@ -284,8 +292,9 @@ public class FileDialogTests (ITestOutputHelper output) IReadOnlyCollection eventMultiSelected = null; dlg.FilesSelected += (s, e) => { eventMultiSelected = e.Dialog.MultiSelected; }; - Assert.IsType (dlg.MostFocused); - Send ('v', ConsoleKey.DownArrow); + var tv = GetTableView (dlg); + tv.SetFocus (); + Assert.IsType (dlg.MostFocused); // Move selection to subfolder @@ -327,8 +336,9 @@ public class FileDialogTests (ITestOutputHelper output) dlg.OpenMode = openModeMixed ? OpenMode.Mixed : OpenMode.Directory; dlg.AllowsMultipleSelection = multiple; - Assert.IsType (dlg.MostFocused); - Send ('v', ConsoleKey.DownArrow); + var tv = GetTableView (dlg); + tv.SetFocus (); + Assert.IsType (dlg.MostFocused); // Should be selecting .. @@ -421,45 +431,60 @@ public class FileDialogTests (ITestOutputHelper output) fd.Draw (); - var expected = - @$" -┌─────────────────────────────────────────────────────────────────────────┐ -│/demo/ │ -│{ - CM.Glyphs.LeftBracket -}▲{ - CM.Glyphs.RightBracket -} │ -│┌────────────┬──────────┬──────────────────────────────┬────────────────┐│ -││Filename (▲)│Size │Modified │Type ││ -│├────────────┼──────────┼──────────────────────────────┼────────────────┤│ -││.. │ │ │ ││ -││/subfolder │ │2002-01-01T22:42:10 │ ││ -││image.gif │4.00 B │2002-01-01T22:42:10 │.gif ││ -││jQuery.js │7.00 B │2001-01-01T11:44:42 │.js ││ -│ │ -│ │ -│ │ -│{ - CM.Glyphs.LeftBracket -} ►► { - CM.Glyphs.RightBracket -} Enter Search { - CM.Glyphs.LeftBracket -}{ - CM.Glyphs.LeftDefaultIndicator -} OK { - CM.Glyphs.RightDefaultIndicator -}{ - CM.Glyphs.RightBracket -} { - CM.Glyphs.LeftBracket -} Cancel { - CM.Glyphs.RightBracket -} │ -└─────────────────────────────────────────────────────────────────────────┘ -"; - TestHelpers.AssertDriverContentsAre (expected, output, ignoreLeadingWhitespace: true); + /* + * + * + ┌─────────────────────────────────────────────────────────────────────────┐ + │/demo/ │ + │⟦▲⟧ │ + │┌────────────┬──────────┬──────────────────────────────┬────────────────┐│ + ││Filename (▲)│Size │Modified │Type ││ + │├────────────┼──────────┼──────────────────────────────┼────────────────┤│ + ││.. │ │ │ ││ + ││/subfolder │ │2002-01-01T22:42:10 │ ││ + ││image.gif │4.00 B │2002-01-01T22:42:10 │.gif ││ + ││jQuery.js │7.00 B │2001-01-01T11:44:42 │.js ││ + │ │ + │ │ + │ │ + │⟦ ►► ⟧ Enter Search ⟦► OK ◄⟧ ⟦ Cancel ⟧ │ + └─────────────────────────────────────────────────────────────────────────┘ + + * + */ + + var path = GetTextField (fd, FileDialogPart.Path); + Assert.Equal ("/demo/", path.Text); + + var tv = GetTableView (fd); + + // Asserting the headers + Assert.Equal ("Filename (▲)", tv.Table.ColumnNames.ElementAt (0)); + Assert.Equal ("Size", tv.Table.ColumnNames.ElementAt (1)); + Assert.Equal ("Modified", tv.Table.ColumnNames.ElementAt (2)); + Assert.Equal ("Type", tv.Table.ColumnNames.ElementAt (3)); + + // Asserting the table contents + Assert.Equal ("..", tv.Style.GetOrCreateColumnStyle (0).GetRepresentation (tv.Table [0, 0])); + Assert.Equal ("/subfolder", tv.Style.GetOrCreateColumnStyle (0).GetRepresentation (tv.Table [1, 0])); + Assert.Equal ("image.gif", tv.Style.GetOrCreateColumnStyle (0).GetRepresentation (tv.Table [2, 0])); + Assert.Equal ("jQuery.js", tv.Style.GetOrCreateColumnStyle (0).GetRepresentation (tv.Table [3, 0])); + + Assert.Equal ("", tv.Style.GetOrCreateColumnStyle (1).GetRepresentation (tv.Table [0, 1])); + Assert.Equal ("", tv.Style.GetOrCreateColumnStyle (1).GetRepresentation (tv.Table [1, 1])); + Assert.Equal ("4.00 B", tv.Style.GetOrCreateColumnStyle (1).GetRepresentation (tv.Table [2, 1])); + Assert.Equal ("7.00 B", tv.Style.GetOrCreateColumnStyle (1).GetRepresentation (tv.Table [3, 1])); + + Assert.Equal ("", tv.Style.GetOrCreateColumnStyle (2).GetRepresentation (tv.Table [0, 2])); + Assert.Equal ("2002-01-01T22:42:10", tv.Style.GetOrCreateColumnStyle (2).GetRepresentation (tv.Table [1, 2])); + Assert.Equal ("2002-01-01T22:42:10", tv.Style.GetOrCreateColumnStyle (2).GetRepresentation (tv.Table [2, 2])); + Assert.Equal ("2001-01-01T11:44:42", tv.Style.GetOrCreateColumnStyle (2).GetRepresentation (tv.Table [3, 2])); + + Assert.Equal ("", tv.Style.GetOrCreateColumnStyle (3).GetRepresentation (tv.Table [0, 3])); + Assert.Equal ("", tv.Style.GetOrCreateColumnStyle (3).GetRepresentation (tv.Table [1, 3])); + Assert.Equal (".gif", tv.Style.GetOrCreateColumnStyle (3).GetRepresentation (tv.Table [2, 3])); + Assert.Equal (".js", tv.Style.GetOrCreateColumnStyle (3).GetRepresentation (tv.Table [3, 3])); + fd.Dispose (); } @@ -479,45 +504,64 @@ public class FileDialogTests (ITestOutputHelper output) fd.Draw (); - var expected = - @$" -┌─────────────────────────────────────────────────────────────────────────┐ -│c:\demo\ │ -│{ - CM.Glyphs.LeftBracket -}▲{ - CM.Glyphs.RightBracket -} │ -│┌────────────┬──────────┬──────────────────────────────┬────────────────┐│ -││Filename (▲)│Size │Modified │Type ││ -│├────────────┼──────────┼──────────────────────────────┼────────────────┤│ -││.. │ │ │ ││ -││\subfolder │ │2002-01-01T22:42:10 │ ││ -││image.gif │4.00 B │2002-01-01T22:42:10 │.gif ││ -││jQuery.js │7.00 B │2001-01-01T11:44:42 │.js ││ -││mybinary.exe│7.00 B │2001-01-01T11:44:42 │.exe ││ -│ │ -│ │ -│{ - CM.Glyphs.LeftBracket -} ►► { - CM.Glyphs.RightBracket -} Enter Search { - CM.Glyphs.LeftBracket -}{ - CM.Glyphs.LeftDefaultIndicator -} OK { - CM.Glyphs.RightDefaultIndicator -}{ - CM.Glyphs.RightBracket -} { - CM.Glyphs.LeftBracket -} Cancel { - CM.Glyphs.RightBracket -} │ -└─────────────────────────────────────────────────────────────────────────┘ -"; - TestHelpers.AssertDriverContentsAre (expected, output, ignoreLeadingWhitespace: true); + /* + * + * + ┌─────────────────────────────────────────────────────────────────────────┐ + │c:\demo\ │ + │⟦▲⟧ │ + │┌────────────┬──────────┬──────────────────────────────┬────────────────┐│ + ││Filename (▲)│Size │Modified │Type ││ + │├────────────┼──────────┼──────────────────────────────┼────────────────┤│ + ││.. │ │ │ ││ + ││\subfolder │ │2002-01-01T22:42:10 │ ││ + ││image.gif │4.00 B │2002-01-01T22:42:10 │.gif ││ + ││jQuery.js │7.00 B │2001-01-01T11:44:42 │.js ││ + ││mybinary.exe│7.00 B │2001-01-01T11:44:42 │.exe ││ + │ │ + │ │ + │⟦ ►► ⟧ Enter Search ⟦► OK ◄⟧ ⟦ Cancel ⟧ │ + └─────────────────────────────────────────────────────────────────────────┘ + + * + */ + + var path = GetTextField (fd, FileDialogPart.Path); + Assert.Equal ("c:\\demo\\",path.Text); + + var tv = GetTableView (fd); + + // Asserting the headers + Assert.Equal ("Filename (▲)", tv.Table.ColumnNames.ElementAt (0)); + Assert.Equal ("Size", tv.Table.ColumnNames.ElementAt (1)); + Assert.Equal ("Modified", tv.Table.ColumnNames.ElementAt (2)); + Assert.Equal ("Type", tv.Table.ColumnNames.ElementAt (3)); + + // Asserting the table contents + Assert.Equal ("..", tv.Style.GetOrCreateColumnStyle (0).GetRepresentation (tv.Table [0, 0])); + Assert.Equal (@"\subfolder", tv.Style.GetOrCreateColumnStyle (0).GetRepresentation (tv.Table [1, 0])); + Assert.Equal ("image.gif", tv.Style.GetOrCreateColumnStyle (0).GetRepresentation (tv.Table [2, 0])); + Assert.Equal ("jQuery.js", tv.Style.GetOrCreateColumnStyle (0).GetRepresentation (tv.Table [3, 0])); + Assert.Equal ("mybinary.exe", tv.Style.GetOrCreateColumnStyle (0).GetRepresentation (tv.Table [4, 0])); + + Assert.Equal ("", tv.Style.GetOrCreateColumnStyle (1).GetRepresentation (tv.Table [0, 1])); + Assert.Equal ("", tv.Style.GetOrCreateColumnStyle (1).GetRepresentation (tv.Table [1, 1])); + Assert.Equal ("4.00 B", tv.Style.GetOrCreateColumnStyle (1).GetRepresentation (tv.Table [2, 1])); + Assert.Equal ("7.00 B", tv.Style.GetOrCreateColumnStyle (1).GetRepresentation (tv.Table [3, 1])); + Assert.Equal ("7.00 B", tv.Style.GetOrCreateColumnStyle (1).GetRepresentation (tv.Table [4, 1])); + + Assert.Equal ("", tv.Style.GetOrCreateColumnStyle (2).GetRepresentation (tv.Table [0, 2])); + Assert.Equal ("2002-01-01T22:42:10", tv.Style.GetOrCreateColumnStyle (2).GetRepresentation (tv.Table [1, 2])); + Assert.Equal ("2002-01-01T22:42:10", tv.Style.GetOrCreateColumnStyle (2).GetRepresentation (tv.Table [2, 2])); + Assert.Equal ("2001-01-01T11:44:42", tv.Style.GetOrCreateColumnStyle (2).GetRepresentation (tv.Table [3, 2])); + Assert.Equal ("2001-01-01T11:44:42", tv.Style.GetOrCreateColumnStyle (2).GetRepresentation (tv.Table [4, 2])); + + Assert.Equal ("", tv.Style.GetOrCreateColumnStyle (3).GetRepresentation (tv.Table [0, 3])); + Assert.Equal ("", tv.Style.GetOrCreateColumnStyle (3).GetRepresentation (tv.Table [1, 3])); + Assert.Equal (".gif", tv.Style.GetOrCreateColumnStyle (3).GetRepresentation (tv.Table [2, 3])); + Assert.Equal (".js", tv.Style.GetOrCreateColumnStyle (3).GetRepresentation (tv.Table [3, 3])); + Assert.Equal (".exe", tv.Style.GetOrCreateColumnStyle (3).GetRepresentation (tv.Table [4, 3])); + fd.Dispose (); } @@ -734,4 +778,30 @@ public class FileDialogTests (ITestOutputHelper output) Send ('\\', ConsoleKey.Separator); } } + + private TextField GetTextField (FileDialog dlg, FileDialogPart part) + { + switch (part) + { + case FileDialogPart.Path: + return dlg.Subviews.OfType ().ElementAt (0); + case FileDialogPart.SearchField: + return dlg.Subviews.OfType ().ElementAt (1); + break; + default: + throw new ArgumentOutOfRangeException (nameof (part), part, null); + } + } + + private TableView GetTableView (FileDialog dlg) + { + var tile = dlg.Subviews.OfType ().Single (); + return (TableView)tile.Tiles.ElementAt (1).ContentView.Subviews.ElementAt(0); + } + + private enum FileDialogPart + { + Path, + SearchField, + } } diff --git a/UnitTests/Views/TableViewTests.cs b/UnitTests/Views/TableViewTests.cs index f5d03f7bc..33e3ba6f3 100644 --- a/UnitTests/Views/TableViewTests.cs +++ b/UnitTests/Views/TableViewTests.cs @@ -1068,7 +1068,7 @@ public class TableViewTests (ITestOutputHelper output) Application.Begin (top); tv.HasFocus = focused; - Assert.Equal(focused, tv.HasFocus); + Assert.Equal (focused, tv.HasFocus); tv.Draw (); @@ -1155,7 +1155,7 @@ public class TableViewTests (ITestOutputHelper output) // 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; - + var top = new Toplevel (); top.Add (tv); Application.Begin (top); @@ -3169,7 +3169,7 @@ A B C } [Fact] - public void TestDataColumnCaption() + public void TestDataColumnCaption () { var tableView = new TableView (); @@ -3191,6 +3191,175 @@ A B C Assert.Equal ("Column Name 2", cn [1]); } + + [Fact] + public void CanTabOutOfTableViewUsingCursor_Left () + { + GetTableViewWithSiblings (out var tf1, out var tableView, out var tf2); + + // Make the selected cell one in + tableView.SelectedColumn = 1; + + // Pressing left should move us to the first column without changing focus + Application.OnKeyDown (Key.CursorLeft); + Assert.Same (tableView, Application.Current.MostFocused); + Assert.True (tableView.HasFocus); + + // Because we are now on the leftmost cell a further left press should move focus + Application.OnKeyDown (Key.CursorLeft); + + Assert.NotSame (tableView, Application.Current.MostFocused); + Assert.False (tableView.HasFocus); + + Assert.Same (tf1, Application.Current.MostFocused); + Assert.True (tf1.HasFocus); + + Application.Current.Dispose (); + } + + [Fact] + public void CanTabOutOfTableViewUsingCursor_Up () + { + GetTableViewWithSiblings (out var tf1, out var tableView, out var tf2); + + // Make the selected cell one in + tableView.SelectedRow = 1; + + // First press should move us up + Application.OnKeyDown (Key.CursorUp); + Assert.Same (tableView, Application.Current.MostFocused); + Assert.True (tableView.HasFocus); + + // Because we are now on the top row a further press should move focus + Application.OnKeyDown (Key.CursorUp); + + Assert.NotSame (tableView, Application.Current.MostFocused); + Assert.False (tableView.HasFocus); + + Assert.Same (tf1, Application.Current.MostFocused); + Assert.True (tf1.HasFocus); + + Application.Current.Dispose (); + } + [Fact] + public void CanTabOutOfTableViewUsingCursor_Right () + { + GetTableViewWithSiblings (out var tf1, out var tableView, out var tf2); + + // Make the selected cell one in from the rightmost column + tableView.SelectedColumn = tableView.Table.Columns - 2; + + // First press should move us to the rightmost column without changing focus + Application.OnKeyDown (Key.CursorRight); + Assert.Same (tableView, Application.Current.MostFocused); + Assert.True (tableView.HasFocus); + + // Because we are now on the rightmost cell, a further right press should move focus + Application.OnKeyDown (Key.CursorRight); + + Assert.NotSame (tableView, Application.Current.MostFocused); + Assert.False (tableView.HasFocus); + + Assert.Same (tf2, Application.Current.MostFocused); + Assert.True (tf2.HasFocus); + + Application.Current.Dispose (); + } + + [Fact] + public void CanTabOutOfTableViewUsingCursor_Down () + { + GetTableViewWithSiblings (out var tf1, out var tableView, out var tf2); + + // Make the selected cell one in from the bottommost row + tableView.SelectedRow = tableView.Table.Rows - 2; + + // First press should move us to the bottommost row without changing focus + Application.OnKeyDown (Key.CursorDown); + Assert.Same (tableView, Application.Current.MostFocused); + Assert.True (tableView.HasFocus); + + // Because we are now on the bottommost cell, a further down press should move focus + Application.OnKeyDown (Key.CursorDown); + + Assert.NotSame (tableView, Application.Current.MostFocused); + Assert.False (tableView.HasFocus); + + Assert.Same (tf2, Application.Current.MostFocused); + Assert.True (tf2.HasFocus); + + Application.Current.Dispose (); + } + + + [Fact] + public void CanTabOutOfTableViewUsingCursor_Left_ClearsSelectionFirst () + { + GetTableViewWithSiblings (out var tf1, out var tableView, out var tf2); + + // Make the selected cell one in + tableView.SelectedColumn = 1; + + // Pressing shift-left should give us a multi selection + Application.OnKeyDown (Key.CursorLeft.WithShift); + Assert.Same (tableView, Application.Current.MostFocused); + Assert.True (tableView.HasFocus); + Assert.Equal (2, tableView.GetAllSelectedCells ().Count ()); + + // Because we are now on the leftmost cell a further left press would normally move focus + // However there is an ongoing selection so instead the operation clears the selection and + // gets swallowed (not resulting in a focus change) + Application.OnKeyDown (Key.CursorLeft); + + // Selection 'clears' just to the single cell and we remain focused + Assert.Single (tableView.GetAllSelectedCells ()); + Assert.Same (tableView, Application.Current.MostFocused); + Assert.True (tableView.HasFocus); + + // A further left will switch focus + Application.OnKeyDown (Key.CursorLeft); + + Assert.NotSame (tableView, Application.Current.MostFocused); + Assert.False (tableView.HasFocus); + + Assert.Same (tf1, Application.Current.MostFocused); + Assert.True (tf1.HasFocus); + + Application.Current.Dispose (); + } + + /// + /// Creates 3 views on with the focus in the + /// . This is a helper method to setup tests that want to + /// explore moving input focus out of a tableview. + /// + /// + /// + /// + private void GetTableViewWithSiblings (out TextField tf1, out TableView tableView, out TextField tf2) + { + tableView = new TableView (); + tableView.BeginInit (); + tableView.EndInit (); + + Application.Navigation = new (); + Application.Current = new (); + tf1 = new TextField (); + tf2 = new TextField (); + Application.Current.Add (tf1); + Application.Current.Add (tableView); + Application.Current.Add (tf2); + + tableView.SetFocus (); + + Assert.Same (tableView, Application.Current.MostFocused); + Assert.True (tableView.HasFocus); + + + // Set big table + tableView.Table = BuildTable (25, 50); + } + private TableView GetABCDEFTableView (out DataTable dt) { var tableView = new TableView ();