Fixes #2582 - Refactors FileDialog for cleaner data model (#2583)

* WIP: Add ITableDataSource

* WIP: Refactor TableView

* WIP: Port CSVEditor

* WIP: Port TableEditor

* WIP: Port MultiColouredTable scenario

* Fix bug of adding duplicate column styles

* Update tests to use DataTableSource

* Tidy up

* Add EnumerableTableDataSource<T>

* Add test for EnumerableTableDataSource

* Add test for EnumerableTableDataSource

* Add code example to xmldoc

* Add ProcessTable scenario

* Rename ITableDataSource to ITableSource and update docs

* Rename EnumerableTableDataSource to EnumerableTableSource

* Fixed Frame != Bounds; changed UICat Scenarios list to use tableview!

* Fix scroll resetting in ProcessTable scenario

* Fix unit tests by setting Frame to same as Bounds

* Document why we have to measure our data for use with TableView

* WIP: Simplify FileDialogs use of TableView

* WIP start migrating sorter

* WIP new filedialog table source mostly working

* WIP remove sorter class

* Refactor GetOrderByValue to be adjacent to GetColumnValue

* Fix collection navigator back so it ignores icon

* Fix unit tests

* Tidy up

* Fix UseColors

* Add test for UseColors

---------

Co-authored-by: Tig Kindel <tig@users.noreply.github.com>
This commit is contained in:
Thomas Nind
2023-04-29 11:04:20 +01:00
committed by GitHub
parent 038cf8aa45
commit 130fc5713d
7 changed files with 412 additions and 443 deletions

View File

@@ -28,7 +28,7 @@ namespace Terminal.Gui {
public IDirectoryInfo Directory { get; }
public FileSystemInfoStats [] Children { get; protected set; }
public FileSystemInfoStats [] Children { get; internal set; }
internal virtual void RefreshChildren ()
{

View File

@@ -40,7 +40,7 @@ namespace Terminal.Gui {
/// <summary>
/// Gets or sets the culture to use (e.g. for number formatting).
/// Defaults to <see cref="CultureInfo.CurrentUICulture"/>.
/// <summary>
/// </summary>
public CultureInfo Culture {get;set;} = CultureInfo.CurrentUICulture;
/// <summary>

View File

@@ -73,7 +73,7 @@ namespace Terminal.Gui {
public bool IsImage ()
{
return this.FileSystemInfo is FileSystemInfo f &&
return this.FileSystemInfo is IFileSystemInfo f &&
ImageExtensions.Contains (
f.Extension,
StringComparer.InvariantCultureIgnoreCase);
@@ -82,38 +82,12 @@ namespace Terminal.Gui {
public bool IsExecutable ()
{
// TODO: handle linux executable status
return this.FileSystemInfo is FileSystemInfo f &&
return this.FileSystemInfo is IFileSystemInfo f &&
ExecutableExtensions.Contains (
f.Extension,
StringComparer.InvariantCultureIgnoreCase);
}
internal object GetOrderByValue (FileDialog dlg, string columnName)
{
if (dlg.Style.FilenameColumnName == columnName)
return this.FileSystemInfo.Name;
if (dlg.Style.SizeColumnName == columnName)
return this.MachineReadableLength;
if (dlg.Style.ModifiedColumnName == columnName)
return this.LastWriteTime;
if (dlg.Style.TypeColumnName == columnName)
return this.Type;
throw new ArgumentOutOfRangeException ("Unknown column " + nameof (columnName));
}
internal object GetOrderByDefault ()
{
if (this.IsDir ()) {
return -1;
}
return 100;
}
private static string GetHumanReadableFileSize (long value, CultureInfo culture)
{

View File

@@ -85,10 +85,8 @@ namespace Terminal.Gui {
private IFileSystem fileSystem;
private TextField tbPath;
private FileDialogSorter sorter;
private FileDialogHistory history;
private DataTable dtFiles;
private TableView tableView;
private TreeView<object> treeView;
private TileView splitContainer;
@@ -107,7 +105,10 @@ namespace Terminal.Gui {
private MenuBar allowedTypeMenuBar;
private MenuBarItem allowedTypeMenu;
private MenuItem [] allowedTypeMenuItems;
private int filenameColumn;
private int currentSortColumn;
private bool currentSortIsAsc = true;
/// <summary>
/// Event fired when user attempts to confirm a selection (or multi selection).
@@ -220,8 +221,25 @@ namespace Terminal.Gui {
};
this.tableView.AddKeyBinding (Key.Space, Command.ToggleChecked);
this.tableView.MouseClick += OnTableViewMouseClick;
Style.TableStyle = tableView.Style;
var nameStyle = Style.TableStyle.GetOrCreateColumnStyle (0);
nameStyle.MinWidth = 10;
nameStyle.ColorGetter = this.ColorGetter;
var sizeStyle = Style.TableStyle.GetOrCreateColumnStyle (1);
sizeStyle.MinWidth = 10;
sizeStyle.ColorGetter = this.ColorGetter;
var dateModifiedStyle = Style.TableStyle.GetOrCreateColumnStyle (2);
dateModifiedStyle.MinWidth = 30;
dateModifiedStyle.ColorGetter = this.ColorGetter;
var typeStyle = Style.TableStyle.GetOrCreateColumnStyle (3);
typeStyle.MinWidth = 6;
typeStyle.ColorGetter = this.ColorGetter;
this.tableView.KeyPress += (s, k) => {
if (this.tableView.SelectedRow <= 0) {
this.NavigateIf (k, Key.CursorUp, this.tbPath);
@@ -313,13 +331,8 @@ namespace Terminal.Gui {
this.tableView.Style.ShowHorizontalHeaderUnderline = true;
this.tableView.Style.ShowHorizontalScrollIndicators = true;
this.SetupTableColumns ();
this.sorter = new FileDialogSorter (this, this.tableView);
this.history = new FileDialogHistory (this);
this.tableView.Table = new DataTableSource(this.dtFiles);
this.tbPath.TextChanged += (s, e) => this.PathChanged ();
this.tableView.CellActivated += this.CellActivate;
@@ -366,9 +379,29 @@ namespace Terminal.Gui {
this.Add (this.btnForward);
this.Add (this.tbPath);
this.Add (this.splitContainer);
}
// Default sort order is by name
sorter.SortColumn(this.filenameColumn,true);
private void OnTableViewMouseClick (object sender, MouseEventEventArgs e)
{
var clickedCell = this.tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out int? clickedCol);
if (clickedCol != null) {
if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) {
// left click in a header
this.SortColumn (clickedCol.Value);
} else if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) {
// right click in a header
this.ShowHeaderContextMenu (clickedCol.Value, e);
}
} else {
if (clickedCell != null && e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) {
// right click in rest of table
this.ShowCellContextMenu (clickedCell, e);
}
}
}
private string GetForwardButtonText ()
@@ -541,15 +574,7 @@ namespace Terminal.Gui {
var col = tableView.SelectedColumn;
var style = tableView.Style.GetColumnStyleIfAny (col);
var collection = dtFiles
.Rows
.Cast<DataRow> ()
.Select ((o, idx) => col == 0 ?
RowToStats(idx).FileSystemInfo.Name :
style.GetRepresentation (o [0])?.TrimStart('.'))
.ToArray ();
var collection = State.Children.Select (s=> FileDialogTableSource.GetRawColumnValue(col,s));
collectionNavigator = new CollectionNavigator (collection);
}
@@ -963,61 +988,6 @@ namespace Terminal.Gui {
return false;
}
private void SetupTableColumns ()
{
this.dtFiles = new DataTable ();
var nameStyle = this.tableView.Style.GetOrCreateColumnStyle (
filenameColumn = this.dtFiles.Columns.Add (Style.FilenameColumnName, typeof (int)).Ordinal
);
nameStyle.RepresentationGetter = (i) => {
var stats = this.State?.Children [(int)i];
if (stats == null) {
return string.Empty;
}
var icon = stats.IsParent ? null : Style.IconGetter?.Invoke (stats.FileSystemInfo);
if (icon != null) {
return icon + stats.Name;
}
return stats.Name;
};
nameStyle.MinWidth = 50;
var sizeStyle = this.tableView.Style.GetOrCreateColumnStyle (
this.dtFiles.Columns.Add (Style.SizeColumnName, typeof (int)).Ordinal);
sizeStyle.RepresentationGetter = (i) => this.State?.Children [(int)i].HumanReadableLength ?? string.Empty;
nameStyle.MinWidth = 10;
var dateModifiedStyle = this.tableView.Style.GetOrCreateColumnStyle (
this.dtFiles.Columns.Add (Style.ModifiedColumnName, typeof (int)).Ordinal);
dateModifiedStyle.RepresentationGetter = (i) =>
{
var s = this.State?.Children [(int)i];
if(s == null || s.IsParent || s.LastWriteTime == null)
{
return string.Empty;
}
return s.LastWriteTime.Value.ToString (Style.DateFormat);
};
dateModifiedStyle.MinWidth = 30;
var typeStyle = this.tableView.Style.GetOrCreateColumnStyle (
this.dtFiles.Columns.Add (Style.TypeColumnName, typeof (int)).Ordinal);
typeStyle.RepresentationGetter = (i) => this.State?.Children [(int)i].Type ?? string.Empty;
typeStyle.MinWidth = 6;
foreach(var colStyle in Style.TableStyle.ColumnStyles) {
colStyle.Value.ColorGetter = this.ColorGetter;
}
}
private void CellActivate (object sender, CellActivatedEventArgs obj)
{
if(TryAcceptMulti())
@@ -1225,23 +1195,13 @@ namespace Terminal.Gui {
if (this.State == null) {
return;
}
this.tableView.Table = new FileDialogTableSource (this.State, this.Style, currentSortColumn, currentSortIsAsc);
this.dtFiles.Rows.Clear ();
for (int i = 0; i < this.State.Children.Length; i++) {
this.BuildRow (i);
}
this.sorter.ApplySort ();
this.ApplySort ();
this.tableView.Update ();
UpdateCollectionNavigator ();
}
private void BuildRow (int idx)
{
dtFiles.Rows.Add (idx, idx, idx, idx);
}
private ColorScheme ColorGetter (TableView.CellColorGetterArgs args)
{
var stats = this.RowToStats (args.RowIndex);
@@ -1279,7 +1239,7 @@ namespace Terminal.Gui {
foreach (var p in this.tableView.GetAllSelectedCells ()) {
var add = this.State?.Children [(int)this.tableView.Table[p.Y, 0]];
var add = this.State?.Children [p.Y];
if (add != null) {
toReturn.Add (add);
}
@@ -1290,31 +1250,9 @@ namespace Terminal.Gui {
}
private FileSystemInfoStats RowToStats (int rowIndex)
{
return this.State?.Children [(int)this.tableView.Table[rowIndex,0]];
return this.State?.Children [rowIndex];
}
private int? StatsToRow (IFileSystemInfo fileSystemInfo)
{
// find array index of the current state for the stats
var idx = State?.Children.IndexOf ((f) => f.FileSystemInfo.FullName == fileSystemInfo.FullName);
if (idx != -1 && idx != null) {
// find the row number in our DataTable where the cell
// contains idx
var match = dtFiles.Rows
.Cast<DataRow> ()
.Select ((r, rIdx) => new { row = r, rowIdx = rIdx })
.Where (t => (int)t.row [0] == idx)
.ToArray ();
if (match.Length == 1) {
return match [0].rowIdx;
}
}
return null;
}
private void PathChanged ()
{
// avoid re-entry
@@ -1358,185 +1296,110 @@ namespace Terminal.Gui {
/// <param name="toRestore"></param>
internal void RestoreSelection (IFileSystemInfo toRestore)
{
var toReselect = StatsToRow (toRestore);
tableView.SelectedRow = State.Children.IndexOf (r=>r.FileSystemInfo == toRestore);
tableView.EnsureSelectedCellIsVisible ();
}
if (toReselect.HasValue) {
tableView.SelectedRow = toReselect.Value;
tableView.EnsureSelectedCellIsVisible ();
internal void ApplySort ()
{
var stats = State?.Children ?? new FileSystemInfoStats [0];
// This portion is never reordered (aways .. at top then folders)
var forcedOrder = stats
.OrderByDescending (f => f.IsParent)
.ThenBy (f => f.IsDir() ? -1:100);
// This portion is flexible based on the column clicked (e.g. alphabetical)
var ordered =
this.currentSortIsAsc ?
forcedOrder.ThenBy (f => FileDialogTableSource.GetRawColumnValue(currentSortColumn,f)):
forcedOrder.ThenByDescending (f => FileDialogTableSource.GetRawColumnValue (currentSortColumn, f));
State.Children = ordered.ToArray();
this.tableView.Update ();
UpdateCollectionNavigator ();
}
private void SortColumn (int clickedCol)
{
this.GetProposedNewSortOrder (clickedCol, out var isAsc);
this.SortColumn (clickedCol, isAsc);
this.tableView.Table = new FileDialogTableSource(State,Style,currentSortColumn,currentSortIsAsc);
}
internal void SortColumn (int col, bool isAsc)
{
// set a sort order
this.currentSortColumn = col;
this.currentSortIsAsc = isAsc;
this.ApplySort ();
}
private string GetProposedNewSortOrder (int clickedCol, out bool isAsc)
{
// work out new sort order
if (this.currentSortColumn == clickedCol && this.currentSortIsAsc) {
isAsc = false;
return $"{tableView.Table.ColumnNames[clickedCol]} DESC";
} else {
isAsc = true;
return $"{tableView.Table.ColumnNames [clickedCol]} ASC";
}
}
private class FileDialogSorter {
private readonly FileDialog dlg;
private TableView tableView;
private int? currentSort = null;
private bool currentSortIsAsc = true;
private void ShowHeaderContextMenu (int clickedCol, MouseEventEventArgs e)
{
var sort = this.GetProposedNewSortOrder (clickedCol, out var isAsc);
public FileDialogSorter (FileDialog dlg, TableView tableView)
{
this.dlg = dlg;
this.tableView = tableView;
var contextMenu = new ContextMenu (
e.MouseEvent.X + 1,
e.MouseEvent.Y + 1,
new MenuBarItem (new MenuItem []
{
new MenuItem($"Hide {StripArrows(tableView.Table.ColumnNames[clickedCol])}", string.Empty, () => this.HideColumn(clickedCol)),
new MenuItem($"Sort {StripArrows(sort)}",string.Empty, ()=> this.SortColumn(clickedCol,isAsc)),
})
);
// if user clicks the mouse in TableView
this.tableView.MouseClick += (s, e) => {
var clickedCell = this.tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out int? clickedCol);
if (clickedCol != null) {
if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) {
// left click in a header
this.SortColumn (clickedCol.Value);
} else if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) {
// right click in a header
this.ShowHeaderContextMenu (clickedCol.Value, e);
}
} else {
if (clickedCell != null && e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) {
// right click in rest of table
this.ShowCellContextMenu (clickedCell, e);
}
}
};
}
internal void ApplySort ()
{
var col = this.currentSort;
if(col == null) {
return;
}
// TODO: Consider preserving selection
dlg.dtFiles.Rows.Clear ();
var colName = col == null ? null : StripArrows (tableView.Table.ColumnNames[col.Value]);
var stats = this.dlg.State?.Children ?? new FileSystemInfoStats [0];
// Do we sort on a column or just use the default sort order?
Func<FileSystemInfoStats, object> sortAlgorithm;
if (colName == null) {
sortAlgorithm = (v) => v.GetOrderByDefault ();
this.currentSortIsAsc = true;
} else {
sortAlgorithm = (v) => v.GetOrderByValue (dlg, colName);
}
// This portion is never reordered (aways .. at top then folders)
var forcedOrder = stats.Select ((v, i) => new { v, i })
.OrderByDescending (f => f.v.IsParent)
.ThenBy (f => f.v.IsDir() ? -1:100);
// This portion is flexible based on the column clicked (e.g. alphabetical)
var ordered =
this.currentSortIsAsc ?
forcedOrder.ThenBy (f => sortAlgorithm (f.v)):
forcedOrder.ThenByDescending (f => sortAlgorithm (f.v));
foreach (var o in ordered) {
this.dlg.BuildRow (o.i);
}
foreach (DataColumn c in dlg.dtFiles.Columns) {
// remove any lingering sort indicator
c.ColumnName = StripArrows (c.ColumnName);
// add a new one if this the one that is being sorted
if (c.Ordinal == col) {
c.ColumnName += this.currentSortIsAsc ? " (▲)" : " (▼)";
}
}
this.tableView.Update ();
dlg.UpdateCollectionNavigator ();
}
private static string StripArrows (string columnName)
{
return columnName.Replace (" (▼)", string.Empty).Replace (" (▲)", string.Empty);
}
private void SortColumn (int clickedCol)
{
this.GetProposedNewSortOrder (clickedCol, out var isAsc);
this.SortColumn (clickedCol, isAsc);
}
internal void SortColumn (int col, bool isAsc)
{
// set a sort order
this.currentSort = col;
this.currentSortIsAsc = isAsc;
this.ApplySort ();
}
private string GetProposedNewSortOrder (int clickedCol, out bool isAsc)
{
// work out new sort order
if (this.currentSort == clickedCol && this.currentSortIsAsc) {
isAsc = false;
return $"{tableView.Table.ColumnNames[clickedCol]} DESC";
} else {
isAsc = true;
return $"{tableView.Table.ColumnNames [clickedCol]} ASC";
}
}
private void ShowHeaderContextMenu (int clickedCol, MouseEventEventArgs e)
{
var sort = this.GetProposedNewSortOrder (clickedCol, out var isAsc);
var contextMenu = new ContextMenu (
e.MouseEvent.X + 1,
e.MouseEvent.Y + 1,
new MenuBarItem (new MenuItem []
{
new MenuItem($"Hide {StripArrows(tableView.Table.ColumnNames[clickedCol])}", string.Empty, () => this.HideColumn(clickedCol)),
new MenuItem($"Sort {StripArrows(sort)}",string.Empty, ()=> this.SortColumn(clickedCol,isAsc)),
})
);
contextMenu.Show ();
}
private void ShowCellContextMenu (Point? clickedCell, MouseEventEventArgs e)
{
if (clickedCell == null) {
return;
}
var contextMenu = new ContextMenu (
e.MouseEvent.X + 1,
e.MouseEvent.Y + 1,
new MenuBarItem (new MenuItem []
{
new MenuItem($"New", string.Empty, () => dlg.New()),
new MenuItem($"Rename",string.Empty, ()=> dlg.Rename()),
new MenuItem($"Delete",string.Empty, ()=> dlg.Delete()),
})
);
dlg.tableView.SetSelection (clickedCell.Value.X, clickedCell.Value.Y, false);
contextMenu.Show ();
}
private void HideColumn (int clickedCol)
{
var style = this.tableView.Style.GetOrCreateColumnStyle (clickedCol);
style.Visible = false;
this.tableView.Update ();
}
contextMenu.Show ();
}
private static string StripArrows (string columnName)
{
return columnName.Replace (" (▼)", string.Empty).Replace (" (▲)", string.Empty);
}
private void ShowCellContextMenu (Point? clickedCell, MouseEventEventArgs e)
{
if (clickedCell == null) {
return;
}
var contextMenu = new ContextMenu (
e.MouseEvent.X + 1,
e.MouseEvent.Y + 1,
new MenuBarItem (new MenuItem []
{
new MenuItem($"New", string.Empty, () => New()),
new MenuItem($"Rename",string.Empty, ()=> Rename()),
new MenuItem($"Delete",string.Empty, ()=> Delete()),
})
);
tableView.SetSelection (clickedCell.Value.X, clickedCell.Value.Y, false);
contextMenu.Show ();
}
private void HideColumn (int clickedCol)
{
var style = this.tableView.Style.GetOrCreateColumnStyle (clickedCol);
style.Visible = false;
this.tableView.Update ();
}
/// <summary>
/// State representing a recursive search from <see cref="FileDialogState.Directory"/>
/// downwards.

View File

@@ -0,0 +1,72 @@
using System;
using System.Linq;
namespace Terminal.Gui {
internal class FileDialogTableSource : ITableSource {
readonly FileDialogStyle style;
readonly int currentSortColumn;
readonly bool currentSortIsAsc;
readonly FileDialogState state;
public FileDialogTableSource (FileDialogState state, FileDialogStyle style, int currentSortColumn, bool currentSortIsAsc)
{
this.style = style;
this.currentSortColumn = currentSortColumn;
this.currentSortIsAsc = currentSortIsAsc;
this.state = state;
}
public object this [int row, int col] => GetColumnValue (col, state.Children [row]);
private object GetColumnValue (int col, FileSystemInfoStats stats)
{
switch (col) {
case 0:
var icon = stats.IsParent ? null : style.IconGetter?.Invoke (stats.FileSystemInfo);
return icon + (stats?.Name ?? string.Empty);
case 1:
return stats?.HumanReadableLength ?? string.Empty;
case 2:
if (stats == null || stats.IsParent || stats.LastWriteTime == null) {
return string.Empty;
}
return stats.LastWriteTime.Value.ToString (style.DateFormat);
case 3:
return stats?.Type ?? string.Empty;
default:
throw new ArgumentOutOfRangeException (nameof (col));
}
}
internal static object GetRawColumnValue (int col, FileSystemInfoStats stats)
{
switch (col) {
case 0: return stats.FileSystemInfo.Name;
case 1: return stats.MachineReadableLength;
case 2: return stats.LastWriteTime;
case 3: return stats.Type;
}
throw new ArgumentOutOfRangeException (nameof (col));
}
public int Rows => state.Children.Count ();
public int Columns => 4;
public string [] ColumnNames => new string []{
MaybeAddSortArrows(style.FilenameColumnName,0),
MaybeAddSortArrows(style.SizeColumnName,1),
MaybeAddSortArrows(style.ModifiedColumnName,2),
MaybeAddSortArrows(style.TypeColumnName,3)
};
private string MaybeAddSortArrows (string name, int idx)
{
if (idx == currentSortColumn) {
return name + (currentSortIsAsc ? " (▲)" : " (▼)");
}
return name;
}
}
}

View File

@@ -130,13 +130,13 @@ namespace Terminal.Gui.FileServicesTests {
}
[Theory, AutoInitShutdown]
[InlineData(true,true)]
[InlineData(true,false)]
[InlineData(false,true)]
[InlineData(false,false)]
[InlineData (true, true)]
[InlineData (true, false)]
[InlineData (false, true)]
[InlineData (false, false)]
public void PickDirectory_DirectTyping (bool openModeMixed, bool multiple)
{
var dlg = GetDialog();
var dlg = GetDialog ();
dlg.OpenMode = openModeMixed ? OpenMode.Mixed : OpenMode.Directory;
dlg.AllowsMultipleSelection = multiple;
@@ -144,61 +144,60 @@ namespace Terminal.Gui.FileServicesTests {
// so to add to current path user must press End or right
Send ('>', ConsoleKey.RightArrow, false);
Send("subfolder");
Send ("subfolder");
// Dialog has not yet been confirmed with a choice
Assert.True(dlg.Canceled);
Assert.True (dlg.Canceled);
// Now it has
Send ('\n', ConsoleKey.Enter, false);
Assert.False(dlg.Canceled);
AssertIsTheSubfolder(dlg.Path);
Assert.False (dlg.Canceled);
AssertIsTheSubfolder (dlg.Path);
}
[Theory, AutoInitShutdown]
[InlineData(true,true)]
[InlineData(true,false)]
[InlineData(false,true)]
[InlineData(false,false)]
[InlineData (true, true)]
[InlineData (true, false)]
[InlineData (false, true)]
[InlineData (false, false)]
public void PickDirectory_ArrowNavigation (bool openModeMixed, bool multiple)
{
var dlg = GetDialog();
var dlg = GetDialog ();
dlg.OpenMode = openModeMixed ? OpenMode.Mixed : OpenMode.Directory;
dlg.AllowsMultipleSelection = multiple;
Assert.IsType<TextField>(dlg.MostFocused);
Assert.IsType<TextField> (dlg.MostFocused);
Send ('v', ConsoleKey.DownArrow, false);
Assert.IsType<TableView>(dlg.MostFocused);
Assert.IsType<TableView> (dlg.MostFocused);
// Should be selecting ..
Send ('v', ConsoleKey.DownArrow, false);
// Down to the directory
Assert.True(dlg.Canceled);
Assert.True (dlg.Canceled);
// Alt+O to open (enter would just navigate into the child dir)
Send ('o', ConsoleKey.O, false,true);
Assert.False(dlg.Canceled);
Send ('o', ConsoleKey.O, false, true);
Assert.False (dlg.Canceled);
AssertIsTheSubfolder(dlg.Path);
AssertIsTheSubfolder (dlg.Path);
}
[Theory, AutoInitShutdown]
[InlineData(true)]
[InlineData(false)]
[InlineData (true)]
[InlineData (false)]
public void MultiSelectDirectory_CannotToggleDotDot (bool acceptWithEnter)
{
var dlg = GetDialog();
var dlg = GetDialog ();
dlg.OpenMode = OpenMode.Directory;
dlg.AllowsMultipleSelection = true;
IReadOnlyCollection<string> eventMultiSelected = null;
dlg.FilesSelected += (s,e)=>
{
eventMultiSelected = e.Dialog.MultiSelected;
dlg.FilesSelected += (s, e) => {
eventMultiSelected = e.Dialog.MultiSelected;
};
Assert.IsType<TextField>(dlg.MostFocused);
Assert.IsType<TextField> (dlg.MostFocused);
Send ('v', ConsoleKey.DownArrow, false);
Assert.IsType<TableView>(dlg.MostFocused);
Assert.IsType<TableView> (dlg.MostFocused);
// Try to toggle '..'
Send (' ', ConsoleKey.Spacebar, false);
@@ -206,202 +205,184 @@ namespace Terminal.Gui.FileServicesTests {
// Toggle subfolder
Send (' ', ConsoleKey.Spacebar, false);
Assert.True(dlg.Canceled);
Assert.True (dlg.Canceled);
if(acceptWithEnter)
{
if (acceptWithEnter) {
Send ('\n', ConsoleKey.Enter);
} else {
Send ('o', ConsoleKey.O, false, true);
}
else
{
Send ('o', ConsoleKey.O,false,true);
}
Assert.False(dlg.Canceled);
Assert.False (dlg.Canceled);
Assert.Multiple(
()=>{
Assert.Multiple (
() => {
// Only the subfolder should be selected
Assert.Equal(1,dlg.MultiSelected.Count);
AssertIsTheSubfolder(dlg.Path);
AssertIsTheSubfolder(dlg.MultiSelected.Single());
Assert.Equal (1, dlg.MultiSelected.Count);
AssertIsTheSubfolder (dlg.Path);
AssertIsTheSubfolder (dlg.MultiSelected.Single ());
},
()=>{
() => {
// Event should also agree with the final state
Assert.NotNull(eventMultiSelected);
Assert.Equal(1,eventMultiSelected.Count);
AssertIsTheSubfolder(eventMultiSelected.Single());
Assert.NotNull (eventMultiSelected);
Assert.Equal (1, eventMultiSelected.Count);
AssertIsTheSubfolder (eventMultiSelected.Single ());
}
);
}
[Fact, AutoInitShutdown]
public void DotDot_MovesToRoot_ThenPressBack ()
{
var dlg = GetDialog();
var dlg = GetDialog ();
dlg.OpenMode = OpenMode.Directory;
dlg.AllowsMultipleSelection = true;
bool selected = false;
dlg.FilesSelected += (s,e)=>
{
dlg.FilesSelected += (s, e) => {
selected = true;
};
AssertIsTheStartingDirectory(dlg.Path);
AssertIsTheStartingDirectory (dlg.Path);
Assert.IsType<TextField>(dlg.MostFocused);
Assert.IsType<TextField> (dlg.MostFocused);
Send ('v', ConsoleKey.DownArrow, false);
Assert.IsType<TableView>(dlg.MostFocused);
Assert.IsType<TableView> (dlg.MostFocused);
// ".." should be the first thing selected
// ".." should not mess with the displayed path
AssertIsTheStartingDirectory(dlg.Path);
AssertIsTheStartingDirectory (dlg.Path);
// Accept navigation up a directory
Send ('\n', ConsoleKey.Enter);
AssertIsTheRootDirectory(dlg.Path);
Assert.True(dlg.Canceled);
Assert.False(selected);
AssertIsTheRootDirectory (dlg.Path);
Assert.True (dlg.Canceled);
Assert.False (selected);
// Now press the back button (in table view)
Send ('<', ConsoleKey.Backspace);
// Should move us back to the root
AssertIsTheStartingDirectory(dlg.Path);
AssertIsTheStartingDirectory (dlg.Path);
Assert.True(dlg.Canceled);
Assert.False(selected);
Assert.True (dlg.Canceled);
Assert.False (selected);
}
[Fact, AutoInitShutdown]
public void MultiSelectDirectory_EnterOpensFolder ()
{
var dlg = GetDialog();
var dlg = GetDialog ();
dlg.OpenMode = OpenMode.Directory;
dlg.AllowsMultipleSelection = true;
IReadOnlyCollection<string> eventMultiSelected = null;
dlg.FilesSelected += (s,e)=>
{
eventMultiSelected = e.Dialog.MultiSelected;
dlg.FilesSelected += (s, e) => {
eventMultiSelected = e.Dialog.MultiSelected;
};
Assert.IsType<TextField>(dlg.MostFocused);
Assert.IsType<TextField> (dlg.MostFocused);
Send ('v', ConsoleKey.DownArrow, false);
Assert.IsType<TableView>(dlg.MostFocused);
Assert.IsType<TableView> (dlg.MostFocused);
// Move selection to subfolder
Send ('v', ConsoleKey.DownArrow, false);
Send ('\n', ConsoleKey.Enter);
// Path should update to the newly opened folder
AssertIsTheSubfolder(dlg.Path);
AssertIsTheSubfolder (dlg.Path);
// No selection will have been confirmed
Assert.True(dlg.Canceled);
Assert.Empty(dlg.MultiSelected);
Assert.Null(eventMultiSelected);
Assert.True (dlg.Canceled);
Assert.Empty (dlg.MultiSelected);
Assert.Null (eventMultiSelected);
}
[Theory, AutoInitShutdown]
[InlineData(true)]
[InlineData(false)]
[InlineData (true)]
[InlineData (false)]
public void MultiSelectDirectory_CanToggleThenAccept (bool acceptWithEnter)
{
var dlg = GetDialog();
var dlg = GetDialog ();
dlg.OpenMode = OpenMode.Directory;
dlg.AllowsMultipleSelection = true;
IReadOnlyCollection<string> eventMultiSelected = null;
dlg.FilesSelected += (s,e)=>
{
eventMultiSelected = e.Dialog.MultiSelected;
dlg.FilesSelected += (s, e) => {
eventMultiSelected = e.Dialog.MultiSelected;
};
Assert.IsType<TextField>(dlg.MostFocused);
Assert.IsType<TextField> (dlg.MostFocused);
Send ('v', ConsoleKey.DownArrow, false);
Assert.IsType<TableView>(dlg.MostFocused);
Assert.IsType<TableView> (dlg.MostFocused);
// Move selection to subfolder
Send ('v', ConsoleKey.DownArrow, false);
// Toggle subfolder
Send (' ', ConsoleKey.Spacebar, false);
Assert.True(dlg.Canceled);
Assert.True (dlg.Canceled);
if(acceptWithEnter)
{
if (acceptWithEnter) {
Send ('\n', ConsoleKey.Enter);
} else {
Send ('o', ConsoleKey.O, false, true);
}
else
{
Send ('o', ConsoleKey.O,false,true);
}
Assert.False(dlg.Canceled);
Assert.False (dlg.Canceled);
Assert.Multiple(
()=>{
Assert.Multiple (
() => {
// Only the subfolder should be selected
Assert.Equal(1,dlg.MultiSelected.Count);
AssertIsTheSubfolder(dlg.Path);
AssertIsTheSubfolder(dlg.MultiSelected.Single());
Assert.Equal (1, dlg.MultiSelected.Count);
AssertIsTheSubfolder (dlg.Path);
AssertIsTheSubfolder (dlg.MultiSelected.Single ());
},
()=>{
() => {
// Event should also agree with the final state
Assert.NotNull(eventMultiSelected);
Assert.Equal(1,eventMultiSelected.Count);
AssertIsTheSubfolder(eventMultiSelected.Single());
Assert.NotNull (eventMultiSelected);
Assert.Equal (1, eventMultiSelected.Count);
AssertIsTheSubfolder (eventMultiSelected.Single ());
}
);
}
private void AssertIsTheStartingDirectory (string path)
{
if(IsWindows())
{
Assert.Equal (@"c:\demo\",path);
}
else
{
Assert.Equal ("/demo/",path);
if (IsWindows ()) {
Assert.Equal (@"c:\demo\", path);
} else {
Assert.Equal ("/demo/", path);
}
}
private void AssertIsTheRootDirectory (string path)
{
if(IsWindows())
{
Assert.Equal (@"c:\",path);
}
else
{
Assert.Equal ("/",path);
if (IsWindows ()) {
Assert.Equal (@"c:\", path);
} else {
Assert.Equal ("/", path);
}
}
private void AssertIsTheSubfolder (string path)
{
if(IsWindows())
{
Assert.Equal (@"c:\demo\subfolder",path);
}
else
{
Assert.Equal ("/demo/subfolder",path);
if (IsWindows ()) {
Assert.Equal (@"c:\demo\subfolder", path);
} else {
Assert.Equal ("/demo/subfolder", path);
}
}
[Fact, AutoInitShutdown]
public void TestDirectoryContents_Linux ()
{
if (IsWindows()) {
if (IsWindows ()) {
return;
}
var fd = GetLinuxDialog();
var fd = GetLinuxDialog ();
fd.Title = string.Empty;
fd.Style.Culture = new CultureInfo("en-US");
fd.Style.Culture = new CultureInfo ("en-US");
fd.Redraw (fd.Bounds);
string expected =
@"
┌──────────────────────────────────────────────────────────────────┐
@@ -426,14 +407,14 @@ namespace Terminal.Gui.FileServicesTests {
[Fact, AutoInitShutdown]
public void TestDirectoryContents_Windows ()
{
if (!IsWindows()) {
if (!IsWindows ()) {
return;
}
var fd = GetWindowsDialog();
var fd = GetWindowsDialog ();
fd.Title = string.Empty;
fd.Style.Culture = new CultureInfo("en-US");
fd.Style.Culture = new CultureInfo ("en-US");
fd.Redraw (fd.Bounds);
@@ -450,7 +431,7 @@ namespace Terminal.Gui.FileServicesTests {
││\subfolder │ │2002-01-01T22:42:10 │dir ││
││image.gif │4.00 bytes│2002-01-01T22:42:10 │.gif ││
││jQuery.js │7.00 bytes│2001-01-01T11:44:42 │.js ││
│mybinary.exe│7.00 bytes│2001-01-01T11:44:42 │.exe
│ │
│ │
│[ ►► ] Enter Search [ Cancel ] [ Ok ] │
@@ -459,6 +440,47 @@ namespace Terminal.Gui.FileServicesTests {
TestHelpers.AssertDriverContentsAre (expected, output, true);
}
[Fact, AutoInitShutdown]
public void TestDirectoryContents_Windows_Colors ()
{
if (!IsWindows ()) {
return;
}
var fd = GetWindowsDialog ();
fd.Title = string.Empty;
fd.Style.Culture = new CultureInfo ("en-US");
fd.Style.UseColors = true;
var dir = new Attribute (Color.Magenta);
fd.Style.ColorSchemeDirectory = GetColorScheme (dir);
var img = new Attribute (Color.Cyan);
fd.Style.ColorSchemeImage = GetColorScheme (img);
var other = new Attribute (Color.BrightGreen);
fd.Style.ColorSchemeOther = GetColorScheme (other);
var exe = new Attribute (Color.BrightYellow);
fd.Style.ColorSchemeExeOrRecommended = GetColorScheme (exe);
fd.Redraw (fd.Bounds);
TestHelpers.AssertDriverUsedColors (other,dir,img,exe);
}
private ColorScheme GetColorScheme (Attribute a)
{
return new ColorScheme {
Normal = a,
Focus = a,
Disabled = a,
HotFocus = a,
HotNormal = a,
};
}
[Theory, AutoInitShutdown]
[InlineData (true)]
[InlineData (false)]
@@ -518,17 +540,17 @@ namespace Terminal.Gui.FileServicesTests {
Assert.Equal (@"/bob/fish", tb.Text);
}*/
private bool IsWindows()
private bool IsWindows ()
{
return System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform (System.Runtime.InteropServices.OSPlatform.Windows);
}
private FileDialog GetDialog()
private FileDialog GetDialog ()
{
return IsWindows() ? GetWindowsDialog() : GetLinuxDialog();
return IsWindows () ? GetWindowsDialog () : GetLinuxDialog ();
}
private FileDialog GetWindowsDialog()
private FileDialog GetWindowsDialog ()
{
// Arrange
var fileSystem = new MockFileSystem (new Dictionary<string, MockFileData> (), @"c:\");
@@ -536,6 +558,7 @@ namespace Terminal.Gui.FileServicesTests {
fileSystem.AddFile (@"c:\myfile.txt", new MockFileData ("Testing is meh.") { LastWriteTime = new DateTime (2001, 01, 01, 11, 12, 11) });
fileSystem.AddFile (@"c:\demo\jQuery.js", new MockFileData ("some js") { LastWriteTime = new DateTime (2001, 01, 01, 11, 44, 42) });
fileSystem.AddFile (@"c:\demo\mybinary.exe", new MockFileData ("some js") { LastWriteTime = new DateTime (2001, 01, 01, 11, 44, 42) });
fileSystem.AddFile (@"c:\demo\image.gif", new MockFileData (new byte [] { 0x12, 0x34, 0x56, 0xd2 }) { LastWriteTime = new DateTime (2002, 01, 01, 22, 42, 10) });
var m = (MockDirectoryInfo)fileSystem.DirectoryInfo.New (@"c:\demo\subfolder");
@@ -552,7 +575,7 @@ namespace Terminal.Gui.FileServicesTests {
return fd;
}
private FileDialog GetLinuxDialog()
private FileDialog GetLinuxDialog ()
{
// Arrange
var fileSystem = new MockFileSystem (new Dictionary<string, MockFileData> (), "/");

View File

@@ -305,7 +305,44 @@ class TestHelpers {
r++;
}
}
/// <summary>
/// Verifies the console used all the <paramref name="expectedColors"/> when rendering.
/// If one or more of the expected colors are not used then the failure will output both
/// the colors that were found to be used and which of your expectations was not met.
/// </summary>
/// <param name="expectedColors"></param>
internal static void AssertDriverUsedColors (params Attribute [] expectedColors)
{
var driver = ((FakeDriver)Application.Driver);
var contents = driver.Contents;
var toFind = expectedColors.ToList ();
var colorsUsed = new HashSet<int> ();
for (int r = 0; r < driver.Rows; r++) {
for (int c = 0; c < driver.Cols; c++) {
int val = contents [r, c, 1];
colorsUsed.Add (val);
var match = toFind.FirstOrDefault (e => e.Value == val);
// need to check twice because Attribute is a struct and therefore cannot be null
if (toFind.Any (e => e.Value == val)) {
toFind.Remove (match);
}
}
}
if(toFind.Any()) {
var sb = new StringBuilder ();
sb.AppendLine ("The following colors were not used:" + string.Join ("; ", toFind.Select (a => DescribeColor (a))));
sb.AppendLine ("Colors used were:" + string.Join ("; ", colorsUsed.Select (DescribeColor)));
throw new Exception (sb.ToString());
}
}
private static object DescribeColor (int userExpected)
{
var a = new Attribute (userExpected);