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