diff --git a/Terminal.Gui/FileServices/FileDialogState.cs b/Terminal.Gui/FileServices/FileDialogState.cs index 807b46856..677409665 100644 --- a/Terminal.Gui/FileServices/FileDialogState.cs +++ b/Terminal.Gui/FileServices/FileDialogState.cs @@ -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 () { diff --git a/Terminal.Gui/FileServices/FileDialogStyle.cs b/Terminal.Gui/FileServices/FileDialogStyle.cs index 6f1535b43..2bb58c62a 100644 --- a/Terminal.Gui/FileServices/FileDialogStyle.cs +++ b/Terminal.Gui/FileServices/FileDialogStyle.cs @@ -40,7 +40,7 @@ namespace Terminal.Gui { /// /// Gets or sets the culture to use (e.g. for number formatting). /// Defaults to . - /// + /// public CultureInfo Culture {get;set;} = CultureInfo.CurrentUICulture; /// diff --git a/Terminal.Gui/FileServices/FileSystemInfoStats.cs b/Terminal.Gui/FileServices/FileSystemInfoStats.cs index d2e37ab11..f850bf2e2 100644 --- a/Terminal.Gui/FileServices/FileSystemInfoStats.cs +++ b/Terminal.Gui/FileServices/FileSystemInfoStats.cs @@ -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) { diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index 6d18bebad..c92102943 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -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 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; /// /// 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 () - .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 () - .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 { /// 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 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 (); + } + /// /// State representing a recursive search from /// downwards. diff --git a/Terminal.Gui/Views/FileDialogTableSource.cs b/Terminal.Gui/Views/FileDialogTableSource.cs new file mode 100644 index 000000000..24a7fcf1d --- /dev/null +++ b/Terminal.Gui/Views/FileDialogTableSource.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/UnitTests/FileServices/FileDialogTests.cs b/UnitTests/FileServices/FileDialogTests.cs index 28206f87e..1d73f0ad4 100644 --- a/UnitTests/FileServices/FileDialogTests.cs +++ b/UnitTests/FileServices/FileDialogTests.cs @@ -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(dlg.MostFocused); + Assert.IsType (dlg.MostFocused); Send ('v', ConsoleKey.DownArrow, false); - Assert.IsType(dlg.MostFocused); + Assert.IsType (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 eventMultiSelected = null; - dlg.FilesSelected += (s,e)=> - { - eventMultiSelected = e.Dialog.MultiSelected; + dlg.FilesSelected += (s, e) => { + eventMultiSelected = e.Dialog.MultiSelected; }; - Assert.IsType(dlg.MostFocused); + Assert.IsType (dlg.MostFocused); Send ('v', ConsoleKey.DownArrow, false); - Assert.IsType(dlg.MostFocused); + Assert.IsType (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(dlg.MostFocused); + Assert.IsType (dlg.MostFocused); Send ('v', ConsoleKey.DownArrow, false); - Assert.IsType(dlg.MostFocused); - + Assert.IsType (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 eventMultiSelected = null; - dlg.FilesSelected += (s,e)=> - { - eventMultiSelected = e.Dialog.MultiSelected; + dlg.FilesSelected += (s, e) => { + eventMultiSelected = e.Dialog.MultiSelected; }; - Assert.IsType(dlg.MostFocused); + Assert.IsType (dlg.MostFocused); Send ('v', ConsoleKey.DownArrow, false); - Assert.IsType(dlg.MostFocused); + Assert.IsType (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 eventMultiSelected = null; - dlg.FilesSelected += (s,e)=> - { - eventMultiSelected = e.Dialog.MultiSelected; + dlg.FilesSelected += (s, e) => { + eventMultiSelected = e.Dialog.MultiSelected; }; - Assert.IsType(dlg.MostFocused); + Assert.IsType (dlg.MostFocused); Send ('v', ConsoleKey.DownArrow, false); - Assert.IsType(dlg.MostFocused); + Assert.IsType (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 (), @"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 (), "/"); diff --git a/UnitTests/TestHelpers.cs b/UnitTests/TestHelpers.cs index 8a9a59dcf..67d39c84b 100644 --- a/UnitTests/TestHelpers.cs +++ b/UnitTests/TestHelpers.cs @@ -305,7 +305,44 @@ class TestHelpers { r++; } } + /// + /// Verifies the console used all the 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. + /// + /// + internal static void AssertDriverUsedColors (params Attribute [] expectedColors) + { + var driver = ((FakeDriver)Application.Driver); + var contents = driver.Contents; + + var toFind = expectedColors.ToList (); + + var colorsUsed = new HashSet (); + + 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);