From a08ea41b91194f482d607bec692b486ff442f11a Mon Sep 17 00:00:00 2001 From: Thomas Nind <31306100+tznind@users.noreply.github.com> Date: Fri, 9 May 2025 15:47:04 +0100 Subject: [PATCH] Fixes #4035 - FileDialog keeps path when selecting folder (optionally) (#4065) * WIP keep path * Make new 'sticky filename' behaviour optional * Tests for new behaviour when selecting in TreeView * Add more tests, this time for table view navigation * Add the new style option into UICatalog scenario --- .../UICatalog/Scenarios/FileDialogExamples.cs | 7 + Terminal.Gui/FileServices/FileDialogStyle.cs | 16 ++ Terminal.Gui/Views/FileDialog.cs | 30 +++- .../FluentTests/FileDialogFluentTests.cs | 170 ++++++++++++++++++ .../FluentTests/V2TestDrivers.cs | 17 ++ 5 files changed, 236 insertions(+), 4 deletions(-) diff --git a/Examples/UICatalog/Scenarios/FileDialogExamples.cs b/Examples/UICatalog/Scenarios/FileDialogExamples.cs index 4fcbbcd2f..646989b30 100644 --- a/Examples/UICatalog/Scenarios/FileDialogExamples.cs +++ b/Examples/UICatalog/Scenarios/FileDialogExamples.cs @@ -16,6 +16,7 @@ public class FileDialogExamples : Scenario private CheckBox _cbAlwaysTableShowHeaders; private CheckBox _cbCaseSensitive; private CheckBox _cbDrivesOnlyInTree; + private CheckBox _cbPreserveFilenameOnDirectoryChanges; private CheckBox _cbFlipButtonOrder; private CheckBox _cbMustExist; private CheckBox _cbShowTreeBranchLines; @@ -55,6 +56,9 @@ public class FileDialogExamples : Scenario _cbDrivesOnlyInTree = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "Only Show _Drives" }; win.Add (_cbDrivesOnlyInTree); + _cbPreserveFilenameOnDirectoryChanges = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "Preserve Filename" }; + win.Add (_cbPreserveFilenameOnDirectoryChanges); + y = 0; x = 24; @@ -198,6 +202,9 @@ public class FileDialogExamples : Scenario fd.Style.TreeRootGetter = () => { return Environment.GetLogicalDrives ().ToDictionary (dirInfoFactory.New, k => k); }; } + fd.Style.PreserveFilenameOnDirectoryChanges = _cbPreserveFilenameOnDirectoryChanges.CheckedState == CheckState.Checked; + + if (_rgAllowedTypes.SelectedItem > 0) { fd.AllowedTypes.Add (new AllowedType ("Data File", ".csv", ".tsv")); diff --git a/Terminal.Gui/FileServices/FileDialogStyle.cs b/Terminal.Gui/FileServices/FileDialogStyle.cs index 34804775e..761af2e64 100644 --- a/Terminal.Gui/FileServices/FileDialogStyle.cs +++ b/Terminal.Gui/FileServices/FileDialogStyle.cs @@ -10,6 +10,7 @@ namespace Terminal.Gui; public class FileDialogStyle { private readonly IFileSystem _fileSystem; + private bool _preserveFilenameOnDirectoryChanges; /// Creates a new instance of the class. public FileDialogStyle (IFileSystem fileSystem) @@ -144,6 +145,21 @@ public class FileDialogStyle /// public string WrongFileTypeFeedback { get; set; } = Strings.fdWrongFileTypeFeedback; + + /// + /// + /// Gets or sets a flag that determines behaviour when opening (double click/enter) or selecting a + /// directory in a . + /// + /// If (the default) then the is simply + /// updated to the new directory path. + /// If then any typed or previously selected file + /// name is preserved (e.g. "c:/hello.csv" when opening "temp" becomes "c:/temp/hello.csv"). + /// + /// + public bool PreserveFilenameOnDirectoryChanges { get; set; } + + [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] private Dictionary DefaultTreeRootGetter () { diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index e20ee3310..c715a9986 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -2,6 +2,8 @@ using System.IO.Abstractions; using System.Text.RegularExpressions; using Terminal.Gui.Resources; +#nullable enable + namespace Terminal.Gui; /// @@ -1135,7 +1137,7 @@ public class FileDialog : Dialog, IDesignable } else if (setPathText) { - Path = newState.Directory.FullName; + SetPathToSelectedObject (newState.Directory); } State = newState; @@ -1393,7 +1395,7 @@ public class FileDialog : Dialog, IDesignable { _pushingState = true; - Path = dest.FullName; + SetPathToSelectedObject (dest); State.Selected = stats; _tbPath.Autocomplete.ClearSuggestions (); } @@ -1405,12 +1407,32 @@ public class FileDialog : Dialog, IDesignable private void TreeView_SelectionChanged (object sender, SelectionChangedEventArgs e) { - if (e.NewValue is null) + SetPathToSelectedObject (e.NewValue); + } + + private void SetPathToSelectedObject (IFileSystemInfo? selected) + { + if (selected is null) { return; } - Path = e.NewValue.FullName; + if (selected is IDirectoryInfo && Style.PreserveFilenameOnDirectoryChanges) + { + if (!string.IsNullOrWhiteSpace (Path) && !_fileSystem.Directory.Exists (Path)) + { + var currentFile = _fileSystem.Path.GetFileName (Path); + + if (!string.IsNullOrWhiteSpace (currentFile)) + { + Path = _fileSystem.Path.Combine (selected.FullName, currentFile); + + return; + } + } + } + + Path = selected.FullName; } private bool TryAcceptMulti () diff --git a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs index b1a3ced79..067b25327 100644 --- a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs @@ -189,4 +189,174 @@ public class FileDialogFluentTests .WriteOutLogs (_out) .Stop (); } + + [Theory] + [ClassData (typeof (V2TestDrivers))] + public void SaveFileDialog_PopTree_AndNavigate_PreserveFilenameOnDirectoryChanges_True (V2TestDriver d) + { + var sd = new SaveDialog (CreateExampleFileSystem ()) { Modal = false }; + sd.Style.PreserveFilenameOnDirectoryChanges = true; + + using var c = With.A (sd, 100, 20, d) + .ScreenShot ("Save dialog", _out) + .Then (() => Assert.True (sd.Canceled)) + .Focus (_=>true) + // Clear selection by pressing right in 'file path' text box + .RaiseKeyDownEvent (Key.CursorRight) + .AssertIsType (sd.Focused) + // Type a filename into the dialog + .RaiseKeyDownEvent (Key.H) + .RaiseKeyDownEvent (Key.E) + .RaiseKeyDownEvent (Key.L) + .RaiseKeyDownEvent (Key.L) + .RaiseKeyDownEvent (Key.O) + .WaitIteration () + .ScreenShot ("After typing filename 'hello'", _out) + .AssertEndsWith ("hello", sd.Path) + .LeftClick