diff --git a/Terminal.Gui/Resources/Strings.Designer.cs b/Terminal.Gui/Resources/Strings.Designer.cs index 13fcfe02c..ac857b4e6 100644 --- a/Terminal.Gui/Resources/Strings.Designer.cs +++ b/Terminal.Gui/Resources/Strings.Designer.cs @@ -19,7 +19,7 @@ namespace Terminal.Gui.Resources { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Strings { @@ -682,7 +682,7 @@ namespace Terminal.Gui.Resources { } /// - /// Looks up a localized string similar to Enter Search. + /// Looks up a localized string similar to Find. /// internal static string fdSearchCaption { get { @@ -717,6 +717,15 @@ namespace Terminal.Gui.Resources { } } + /// + /// Looks up a localized string similar to _Tree. + /// + internal static string fdTree { + get { + return ResourceManager.GetString("fdTree", resourceCulture); + } + } + /// /// Looks up a localized string similar to Type. /// diff --git a/Terminal.Gui/Resources/Strings.resx b/Terminal.Gui/Resources/Strings.resx index 68eb7abcd..a05bc2eb5 100644 --- a/Terminal.Gui/Resources/Strings.resx +++ b/Terminal.Gui/Resources/Strings.resx @@ -195,7 +195,7 @@ Enter Path - _Find: + Find Size @@ -355,4 +355,8 @@ New file + + _Tree + Show/Hide Tree View + \ No newline at end of file diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index 41f6ac7ab..b6fc4af5e 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -1,4 +1,5 @@ -namespace Terminal.Gui.Views; +#nullable enable +namespace Terminal.Gui.Views; /// /// A . Supports a simple API for adding s @@ -14,46 +15,6 @@ /// public class Dialog : Window { - /// The default for . - /// This property can be set in a Theme. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static Alignment DefaultButtonAlignment { get; set; } = Alignment.End; - - /// The default for . - /// This property can be set in a Theme. - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static AlignmentModes DefaultButtonAlignmentModes { get; set; } = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems; - - /// - /// Defines the default minimum Dialog width, as a percentage of the container width. Can be configured via - /// . - /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static int DefaultMinimumWidth { get; set; } = 80; - - /// - /// Defines the default minimum Dialog height, as a percentage of the container width. Can be configured via - /// . - /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static int DefaultMinimumHeight { get; set; } = 80; - - /// - /// Gets or sets whether all s are shown with a shadow effect by default. - /// - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public new static ShadowStyle DefaultShadow { get; set; } = ShadowStyle.Transparent; - - /// - /// Defines the default border styling for . Can be configured via - /// . - /// - - [ConfigurationProperty (Scope = typeof (ThemeScope))] - public new static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Heavy; - - private readonly List public class FileDialog : Dialog, IDesignable { - private const int ALIGNMENT_GROUP_INPUT = 32; private const int ALIGNMENT_GROUP_COMPLETE = 55; /// Gets the Path separators for the operating system @@ -35,6 +34,7 @@ public class FileDialog : Dialog, IDesignable private readonly Button _btnForward; private readonly Button _btnOk; private readonly Button _btnUp; + private readonly Button _btnTreeToggle; private readonly IFileSystem? _fileSystem; private readonly FileDialogHistory _history; private readonly SpinnerView _spinnerView; @@ -43,13 +43,13 @@ public class FileDialog : Dialog, IDesignable private readonly TextField _tbFind; private readonly TextField _tbPath; private readonly TreeView _treeView; - private MenuBarItem _allowedTypeMenu; - private MenuBar _allowedTypeMenuBar; - private MenuItem [] _allowedTypeMenuItems; + private MenuBarItem? _allowedTypeMenu; + private MenuBar? _allowedTypeMenuBar; + private MenuItem []? _allowedTypeMenuItems; private int _currentSortColumn; private bool _currentSortIsAsc = true; private bool _disposed; - private string _feedback; + private string? _feedback; private bool _loaded; private bool _pushingState; @@ -86,6 +86,7 @@ public class FileDialog : Dialog, IDesignable } Accept (true); + e.Handled = true; }; _btnCancel = new () @@ -110,6 +111,19 @@ public class FileDialog : Dialog, IDesignable } }; + // Tree toggle button - shares alignment group with OK/Cancel + _btnTreeToggle = new () + { + X = 0,//Pos.Align (Alignment.End, AlignmentModes.AddSpaceBetweenItems, ALIGNMENT_GROUP_COMPLETE), + Y = Pos.AnchorEnd (), + NoPadding = true + }; + _btnTreeToggle.Accepting += (s, e) => + { + e.Handled = true; + ToggleTreeVisibility (); + }; + _btnUp = new () { X = 0, Y = 1, NoPadding = true }; _btnUp.Text = GetUpButtonText (); _btnUp.Accepting += (s, e) => @@ -134,7 +148,7 @@ public class FileDialog : Dialog, IDesignable e.Handled = true; }; - _tbPath = new () { Width = Dim.Fill (), CaptionColor = new (Color.Black) }; + _tbPath = new () { Width = Dim.Fill (),/* CaptionColor = new (Color.Black)*/ }; _tbPath.KeyDown += (s, k) => { @@ -149,13 +163,13 @@ public class FileDialog : Dialog, IDesignable _tbPath.Autocomplete.SuggestionGenerator = new FilepathSuggestionGenerator (); // Create tree view container (left pane) - View treeViewContainer = new () + _treeView = new () { - X = -1, + X = 0, Y = Pos.Bottom (_btnBack), - Width = Dim.Fill (Dim.Func (_ => IsInitialized ? _tableViewContainer!.Frame.Width - 1 : 1)), + Width = Dim.Fill (Dim.Func (_ => IsInitialized ? _tableViewContainer!.Frame.Width - 30 : 30)), Height = Dim.Fill (Dim.Func (_ => IsInitialized ? _btnOk.Frame.Height : 1)), - CanFocus = true, + Visible = false }; // Create table view container (right pane) @@ -168,20 +182,20 @@ public class FileDialog : Dialog, IDesignable Arrangement = ViewArrangement.LeftResizable, BorderStyle = LineStyle.Dashed, SuperViewRendersLineCanvas = true, - CanFocus = true + CanFocus = true, + Id = "_tableViewContainer" }; - _tableViewContainer.Border!.Thickness = new (1, 0, 0, 0); _tableView = new () { Width = Dim.Fill (), - Height = Dim.Fill (), + Height = Dim.Fill (1), FullRowSelect = true, + Id = "_tableView" }; _tableView.CollectionNavigator = new FileDialogCollectionNavigator (this, _tableView); _tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Select); _tableView.MouseClick += OnTableViewMouseClick; - _tableView.Style.InvertSelectedCellFirstCharacter = true; Style.TableStyle = _tableView.Style; ColumnStyle nameStyle = Style.TableStyle.GetOrCreateColumnStyle (0); @@ -200,8 +214,6 @@ public class FileDialog : Dialog, IDesignable typeStyle.MinWidth = 6; typeStyle.ColorGetter = ColorGetter; - _treeView = new () { Width = Dim.Fill (), Height = Dim.Fill () }; - var fileDialogTreeBuilder = new FileSystemTreeBuilder (); _treeView.TreeBuilder = fileDialogTreeBuilder; _treeView.AspectGetter = AspectGetter; @@ -209,40 +221,8 @@ public class FileDialog : Dialog, IDesignable _treeView.SelectionChanged += TreeView_SelectionChanged; - treeViewContainer.Add (_treeView); _tableViewContainer.Add (_tableView); - _tbFind = new () - { - X = Pos.Align (Alignment.Start, AlignmentModes.AddSpaceBetweenItems, ALIGNMENT_GROUP_INPUT), - CaptionColor = new (Color.Black), - Width = 30, - Y = Pos.Top (_btnOk), - HotKey = Key.F.WithAlt - }; - - _spinnerView = new () - { X = Pos.Align (Alignment.Start, AlignmentModes.AddSpaceBetweenItems, ALIGNMENT_GROUP_INPUT), Y = Pos.AnchorEnd (1), Visible = false }; - - _tbFind.TextChanged += (s, o) => RestartSearch (); - - _tbFind.KeyDown += (s, o) => - { - if (o.KeyCode == KeyCode.Enter) - { - RestartSearch (); - o.Handled = true; - } - - if (o.KeyCode == KeyCode.Esc) - { - if (CancelSearch ()) - { - o.Handled = true; - } - } - }; - _tableView.Style.ShowHorizontalHeaderOverline = true; _tableView.Style.ShowVerticalCellLines = true; _tableView.Style.ShowVerticalHeaderLines = true; @@ -263,6 +243,41 @@ public class FileDialog : Dialog, IDesignable _tableView.KeyBindings.ReplaceCommands (Key.Home.WithShift, Command.StartExtend); _tableView.KeyBindings.ReplaceCommands (Key.End.WithShift, Command.EndExtend); + _tbFind = new () + { + X = 0, + Width = Dim.Fill (), + Y = Pos.AnchorEnd (), + HotKey = Key.F.WithAlt, + Id = "_tbFind", + }; + + _spinnerView = new () + { + // The spinner view is positioned over the last column of _tbFind + X = Pos.Right (_tbFind) - 1, + Y = Pos.Top (_tbFind), + Visible = false + }; + + _tbFind.TextChanged += (s, o) => RestartSearch (); + + _tbFind.KeyDown += (s, o) => + { + if (o.KeyCode == KeyCode.Enter) + { + RestartSearch (); + o.Handled = true; + } + + if (o.KeyCode == KeyCode.Esc) + { + if (CancelSearch ()) + { + o.Handled = true; + } + } + }; AllowsMultipleSelection = false; UpdateNavigationVisibility (); @@ -271,13 +286,18 @@ public class FileDialog : Dialog, IDesignable base.Add (_btnUp); base.Add (_btnBack); base.Add (_btnForward); - base.Add (treeViewContainer); + base.Add (_treeView); base.Add (_tableViewContainer); - base.Add (_tbFind); - base.Add (_spinnerView); + _tableViewContainer.Add (_tbFind); + _tableViewContainer.Add (_spinnerView); + // Add the toggle along with OK/Cancel so they align as a group + base.Add (_btnTreeToggle); base.Add (_btnOk); base.Add (_btnCancel); + + // Default: Tree hidden and splitter hidden + SetTreeVisible (false); } /// @@ -301,7 +321,7 @@ public class FileDialog : Dialog, IDesignable } /// The UI selected from combo box. May be null. - public IAllowedType CurrentFilter { get; private set; } + public IAllowedType? CurrentFilter { get; private set; } /// /// Gets or sets behavior of the when the user attempts to delete a selected file(s). Set @@ -364,13 +384,13 @@ public class FileDialog : Dialog, IDesignable public FileDialogStyle Style { get; } /// Gets the currently open directory and known children presented in the dialog. - internal FileDialogState State { get; private set; } + internal FileDialogState? State { get; private set; } /// /// Event fired when user attempts to confirm a selection (or multi selection). Allows you to cancel the selection /// or undertake alternative behavior e.g. open a dialog "File already exists, Overwrite? yes/no". /// - public event EventHandler FilesSelected; + public event EventHandler? FilesSelected; /// /// Returns true if there are no or one of them agrees that @@ -425,6 +445,8 @@ public class FileDialog : Dialog, IDesignable return; } + Arrangement |= ViewArrangement.Resizable; + _loaded = true; // May have been updated after instance was constructed @@ -515,6 +537,9 @@ public class FileDialog : Dialog, IDesignable MoveSubViewTowardsStart (_btnCancel); } + // Ensure toggle button text matches current state after sizing + SetTreeVisible (false); + SetNeedsDraw (); SetNeedsLayout (); } @@ -558,7 +583,7 @@ public class FileDialog : Dialog, IDesignable internal void ApplySort () { - FileSystemInfoStats [] stats = State?.Children ?? new FileSystemInfoStats [0]; + FileSystemInfoStats [] stats = State?.Children ?? []; // This portion is never reordered (always .. at top then folders) IOrderedEnumerable forcedOrder = stats @@ -577,7 +602,7 @@ public class FileDialog : Dialog, IDesignable FileDialogTableSource.GetRawColumnValue (_currentSortColumn, f) ); - State.Children = ordered.ToArray (); + State!.Children = ordered.ToArray (); _tableView.Update (); } @@ -620,7 +645,7 @@ public class FileDialog : Dialog, IDesignable /// internal void RestoreSelection (IFileSystemInfo toRestore) { - _tableView.SelectedRow = State.Children.IndexOf (r => r.FileSystemInfo == toRestore); + _tableView.SelectedRow = State!.Children.IndexOf (r => r.FileSystemInfo == toRestore); _tableView.EnsureSelectedCellIsVisible (); } @@ -709,17 +734,17 @@ public class FileDialog : Dialog, IDesignable for (var i = 0; i < AllowedTypes.Count; i++) { - _allowedTypeMenuItems [i].Checked = i == idx; + _allowedTypeMenuItems! [i].Checked = i == idx; } - _allowedTypeMenu.Title = allow.ToString ()!; + _allowedTypeMenu!.Title = allow.ToString ()!; CurrentFilter = allow; _tbPath.ClearAllSelection (); _tbPath.Autocomplete.ClearSuggestions (); - State.RefreshChildren (); + State!.RefreshChildren (); WriteStateToTableView (); } @@ -815,7 +840,7 @@ public class FileDialog : Dialog, IDesignable private void Delete () { - IFileSystemInfo [] toDelete = GetFocusedFiles (); + IFileSystemInfo [] toDelete = GetFocusedFiles ()!; if (FileOperationsHandler.Delete (toDelete)) { @@ -853,9 +878,9 @@ public class FileDialog : Dialog, IDesignable private string GetBackButtonText () { return Glyphs.LeftArrow + "-"; } - private IFileSystemInfo [] GetFocusedFiles () + private IFileSystemInfo []? GetFocusedFiles () { - if (!_tableView.HasFocus || !_tableView.CanFocus || FileOperationsHandler is null) + if (!_tableView.HasFocus || !_tableView.CanFocus) { return null; } @@ -915,7 +940,7 @@ public class FileDialog : Dialog, IDesignable private bool IsCompatibleWithOpenMode (string s, out string reason) { - reason = null; + reason = string.Empty; if (string.IsNullOrWhiteSpace (s)) { @@ -1007,7 +1032,7 @@ public class FileDialog : Dialog, IDesignable private void New () { { - IFileSystemInfo created = FileOperationsHandler.New (_fileSystem, State.Directory); + IFileSystemInfo created = FileOperationsHandler.New (_fileSystem, State!.Directory); if (created is { }) { @@ -1138,13 +1163,13 @@ public class FileDialog : Dialog, IDesignable private void RefreshState () { - State.RefreshChildren (); + State!.RefreshChildren (); PushState (State, false, false, false); } private void Rename () { - IFileSystemInfo [] toRename = GetFocusedFiles (); + IFileSystemInfo [] toRename = GetFocusedFiles ()!; if (toRename?.Length == 1) { @@ -1328,7 +1353,7 @@ public class FileDialog : Dialog, IDesignable if (stats.IsParent) { - dest = State.Directory; + dest = State!.Directory; } else { @@ -1340,7 +1365,7 @@ public class FileDialog : Dialog, IDesignable _pushingState = true; SetPathToSelectedObject (dest); - State.Selected = stats; + State!.Selected = stats; _tbPath.Autocomplete.ClearSuggestions (); } finally @@ -1363,7 +1388,7 @@ public class FileDialog : Dialog, IDesignable if (selected is IDirectoryInfo && Style.PreserveFilenameOnDirectoryChanges) { - if (!string.IsNullOrWhiteSpace (Path) && !_fileSystem.Directory.Exists (Path)) + if (!string.IsNullOrWhiteSpace (Path) && !_fileSystem!.Directory.Exists (Path)) { var currentFile = _fileSystem.Path.GetFileName (Path); @@ -1384,19 +1409,21 @@ public class FileDialog : Dialog, IDesignable IEnumerable multi = MultiRowToStats (); string? reason = null; - if (!multi.Any ()) + IEnumerable fileSystemInfoStatsEnumerable = multi as FileSystemInfoStats [] ?? multi.ToArray (); + + if (!fileSystemInfoStatsEnumerable.Any ()) { return false; } - if (multi.All ( - m => IsCompatibleWithOpenMode ( - m.FileSystemInfo.FullName, - out reason - ) - )) + if (fileSystemInfoStatsEnumerable.All ( + m => IsCompatibleWithOpenMode ( + m.FileSystemInfo.FullName, + out reason + ) + )) { - Accept (multi); + Accept (fileSystemInfoStatsEnumerable); return true; } @@ -1426,6 +1453,49 @@ public class FileDialog : Dialog, IDesignable _tableView.Update (); } + // --- Tree visibility management --- + + private void ToggleTreeVisibility () + { + SetTreeVisible (!_treeView.Visible); + } + + private void SetTreeVisible (bool visible) + { + _treeView.Enabled = visible; + _treeView.Visible = visible; + + if (visible) + { + // When visible, the table view's left edge is a splitter next to the tree + _treeView.Width = Dim.Fill (Dim.Func (_ => IsInitialized ? _tableViewContainer!.Frame.Width - 30 : 30)); + _tableViewContainer.X = 30; + _tableViewContainer.Arrangement = ViewArrangement.LeftResizable; + _tableViewContainer.Border!.Thickness = new (1, 0, 0, 0); + } + else + { + // When hidden, table occupies full width and splitter is hidden/disabled + _treeView.Width = 0; + _tableViewContainer.X = 0; + _tableViewContainer.Width = Dim.Fill (); + _tableViewContainer.Arrangement = ViewArrangement.Fixed; + _tableViewContainer.Border!.Thickness = new (0, 0, 0, 0); + } + _btnTreeToggle.Text = GetTreeToggleText (visible); + + SetNeedsLayout (); + SetNeedsDraw (); + } + + private string GetTreeToggleText (bool visible) + { + return visible + ? $"{Glyphs.LeftArrow}{Strings.fdTree}" + : $"{Glyphs.RightArrow}{Strings.fdTree}"; + + } + /// State representing a recursive search from downwards. internal class SearchState : FileDialogState { @@ -1470,7 +1540,7 @@ public class FileDialog : Dialog, IDesignable } ); - Task.Run (() => { UpdateChildren (); }); + Task.Run (UpdateChildren); } private void RecursiveFind (IDirectoryInfo directory) diff --git a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs index 9ca6384fc..3dd21f3ad 100644 --- a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs @@ -74,7 +74,7 @@ public class FileDialogFluentTests using var c = With.A (() => NewSaveDialog (out sd,modal:false), 100, 20, d) .ScreenShot ("Save dialog", _out) .Focus