Fixes #3696 Remove custom navigation in FileDialog (#3712)

* Remove custom navigation in FileDialog and change Ok/Cancel to use AddButton

* Fix most tests

* Update drawing tests to use contents asserts instead

* Update positioning to be less absolute

* TableView to not swallow cursors if no selection change manifests

* Make SetSelection cleverer about how it decides selection changed

* Refactor TableView key handling/Tab to be simpler and faster

* Tests for TableView selection moving out

* Add test for TableView selection change swallow cursor

* Fix formatting

* Fix split container height when buttons have shadows

* Fix alignment of buttons and input boxes
This commit is contained in:
Thomas Nind
2024-09-01 21:57:41 +01:00
committed by GitHub
parent 0626da3011
commit 5b2e10e6d0
4 changed files with 408 additions and 240 deletions

View File

@@ -10,6 +10,9 @@ namespace Terminal.Gui;
/// </summary>
public class FileDialog : Dialog
{
private const int alignmentGroupInput = 32;
private const int alignmentGroupComplete = 55;
/// <summary>Gets the Path separators for the operating system</summary>
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<IFileSystemInfo> { 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);
}
/// <summary>
@@ -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<IFileSystemInfo> e)
{
if (e.NewValue is null)

View File

@@ -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);
}
/// <summary>
/// Private override of <see cref="ChangeSelectionByOffset"/> 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.
/// </summary>
/// <param name="offsetX"></param>
/// <param name="offsetY"></param>
/// <returns></returns>
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);
/// <summary>
/// Moves the <see cref="SelectedRow"/> and <see cref="SelectedColumn"/> by the provided offsets. Optionally
/// starting a box selection (see <see cref="MultiSelect"/>)

View File

@@ -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<TextField> (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<TextField> (dlg.MostFocused);
Send ('v', ConsoleKey.DownArrow);
var tv = GetTableView(dlg);
tv.SetFocus ();
Assert.IsType<TableView> (dlg.MostFocused);
// ".." should be the first thing selected
@@ -177,8 +182,10 @@ public class FileDialogTests (ITestOutputHelper output)
IReadOnlyCollection<string> eventMultiSelected = null;
dlg.FilesSelected += (s, e) => { eventMultiSelected = e.Dialog.MultiSelected; };
Assert.IsType<TextField> (dlg.MostFocused);
Send ('v', ConsoleKey.DownArrow);
var tv = GetTableView (dlg);
tv.SetFocus ();
Assert.IsType<TableView> (dlg.MostFocused);
// Try to toggle '..'
@@ -232,8 +239,9 @@ public class FileDialogTests (ITestOutputHelper output)
IReadOnlyCollection<string> eventMultiSelected = null;
dlg.FilesSelected += (s, e) => { eventMultiSelected = e.Dialog.MultiSelected; };
Assert.IsType<TextField> (dlg.MostFocused);
Send ('v', ConsoleKey.DownArrow);
var tv = GetTableView (dlg);
tv.SetFocus ();
Assert.IsType<TableView> (dlg.MostFocused);
// Move selection to subfolder
@@ -284,8 +292,9 @@ public class FileDialogTests (ITestOutputHelper output)
IReadOnlyCollection<string> eventMultiSelected = null;
dlg.FilesSelected += (s, e) => { eventMultiSelected = e.Dialog.MultiSelected; };
Assert.IsType<TextField> (dlg.MostFocused);
Send ('v', ConsoleKey.DownArrow);
var tv = GetTableView (dlg);
tv.SetFocus ();
Assert.IsType<TableView> (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<TextField> (dlg.MostFocused);
Send ('v', ConsoleKey.DownArrow);
var tv = GetTableView (dlg);
tv.SetFocus ();
Assert.IsType<TableView> (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 ││
│├────────────┼──────────┼──────────────────────────────┼────────────────┤
││.. │<Directory> ││
││/subfolder │ │2002-01-01T22:42:10 │<Directory>
││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 ││
│├────────────┼──────────┼──────────────────────────────┼────────────────┤
││.. │ │ │<Directory> │
││/subfolder │ │2002-01-01T22:42:10<Directory> ││
││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 ("<Directory>", tv.Style.GetOrCreateColumnStyle (3).GetRepresentation (tv.Table [0, 3]));
Assert.Equal ("<Directory>", 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 ││
│├────────────┼──────────┼──────────────────────────────┼────────────────┤
││.. │<Directory> ││
││\subfolder │ │2002-01-01T22:42:10<Directory> ││
││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 ││
│├────────────┼──────────┼──────────────────────────────┼────────────────┤
││.. │ │ │<Directory> │
││\subfolder │ │2002-01-01T22:42:10<Directory> ││
││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 ("<Directory>", tv.Style.GetOrCreateColumnStyle (3).GetRepresentation (tv.Table [0, 3]));
Assert.Equal ("<Directory>", 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<TextField> ().ElementAt (0);
case FileDialogPart.SearchField:
return dlg.Subviews.OfType<TextField> ().ElementAt (1);
break;
default:
throw new ArgumentOutOfRangeException (nameof (part), part, null);
}
}
private TableView GetTableView (FileDialog dlg)
{
var tile = dlg.Subviews.OfType<TileView> ().Single ();
return (TableView)tile.Tiles.ElementAt (1).ContentView.Subviews.ElementAt(0);
}
private enum FileDialogPart
{
Path,
SearchField,
}
}

View File

@@ -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 ();
}
/// <summary>
/// Creates 3 views on <see cref="Application.Current"/> with the focus in the
/// <see cref="TableView"/>. This is a helper method to setup tests that want to
/// explore moving input focus out of a tableview.
/// </summary>
/// <param name="tv"></param>
/// <param name="tf1"></param>
/// <param name="tf2"></param>
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 ();