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
This commit is contained in:
Thomas Nind
2025-05-09 15:47:04 +01:00
committed by GitHub
parent b46b7781f1
commit d0bfa5ef13
5 changed files with 236 additions and 4 deletions

View File

@@ -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"));

View File

@@ -10,6 +10,7 @@ namespace Terminal.Gui;
public class FileDialogStyle
{
private readonly IFileSystem _fileSystem;
private bool _preserveFilenameOnDirectoryChanges;
/// <summary>Creates a new instance of the <see cref="FileDialogStyle"/> class.</summary>
public FileDialogStyle (IFileSystem fileSystem)
@@ -144,6 +145,21 @@ public class FileDialogStyle
/// </summary>
public string WrongFileTypeFeedback { get; set; } = Strings.fdWrongFileTypeFeedback;
/// <summary>
/// <para>
/// Gets or sets a flag that determines behaviour when opening (double click/enter) or selecting a
/// directory in a <see cref="FileDialog"/>.
/// </para>
/// <para>If <see langword="false"/> (the default) then the <see cref="FileDialog.Path"/> is simply
/// updated to the new directory path.</para>
/// <para>If <see langword="true"/> then any typed or previously selected file
/// name is preserved (e.g. "c:/hello.csv" when opening "temp" becomes "c:/temp/hello.csv").
/// </para>
/// </summary>
public bool PreserveFilenameOnDirectoryChanges { get; set; }
[UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
private Dictionary<IDirectoryInfo, string> DefaultTreeRootGetter ()
{

View File

@@ -2,6 +2,8 @@ using System.IO.Abstractions;
using System.Text.RegularExpressions;
using Terminal.Gui.Resources;
#nullable enable
namespace Terminal.Gui;
/// <summary>
@@ -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<IFileSystemInfo> 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 ()

View File

@@ -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<TextField> (_=>true)
// Clear selection by pressing right in 'file path' text box
.RaiseKeyDownEvent (Key.CursorRight)
.AssertIsType <TextField>(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<Button> (b => b.Text == "►►")
.ScreenShot ("After pop tree", _out)
.Focus<TreeView<IFileSystemInfo>> (_ => true)
.Right ()
.ScreenShot ("After expand tree", _out)
// Because of PreserveFilenameOnDirectoryChanges we should select the new dir but keep the filename
.AssertEndsWith ("hello", sd.Path)
.Down ()
.ScreenShot ("After navigate down in tree", _out)
// Because of PreserveFilenameOnDirectoryChanges we should select the new dir but keep the filename
.AssertContains ("empty-dir",sd.Path)
.AssertEndsWith ("hello", sd.Path)
.Enter ()
.WaitIteration ()
.Then (() => Assert.False (sd.Canceled))
.AssertContains ("empty-dir", sd.FileName)
.WriteOutLogs (_out)
.Stop ();
}
[Theory]
[ClassData (typeof (V2TestDrivers))]
public void SaveFileDialog_PopTree_AndNavigate_PreserveFilenameOnDirectoryChanges_False (V2TestDriver d)
{
var sd = new SaveDialog (CreateExampleFileSystem ()) { Modal = false };
sd.Style.PreserveFilenameOnDirectoryChanges = false;
using var c = With.A (sd, 100, 20, d)
.ScreenShot ("Save dialog", _out)
.Then (() => Assert.True (sd.Canceled))
.Focus<TextField> (_ => true)
// Clear selection by pressing right in 'file path' text box
.RaiseKeyDownEvent (Key.CursorRight)
.AssertIsType<TextField> (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<Button> (b => b.Text == "►►")
.ScreenShot ("After pop tree", _out)
.Focus<TreeView<IFileSystemInfo>> (_ => true)
.Right ()
.ScreenShot ("After expand tree", _out)
.Down ()
.ScreenShot ("After navigate down in tree", _out)
// PreserveFilenameOnDirectoryChanges is false so just select new path
.AssertEndsWith ("empty-dir", sd.Path)
.AssertDoesNotContain ("hello", sd.Path)
.Enter ()
.WaitIteration ()
.Then (() => Assert.False (sd.Canceled))
.AssertContains ("empty-dir", sd.FileName)
.WriteOutLogs (_out)
.Stop ();
}
[Theory]
[ClassData (typeof (V2TestDrivers_WithTrueFalseParameter))]
public void SaveFileDialog_TableView_UpDown_PreserveFilenameOnDirectoryChanges_True (V2TestDriver d, bool preserve)
{
var sd = new SaveDialog (CreateExampleFileSystem ()) { Modal = false };
sd.Style.PreserveFilenameOnDirectoryChanges = preserve;
using var c = With.A (sd, 100, 20, d)
.ScreenShot ("Save dialog", _out)
.Then (() => Assert.True (sd.Canceled))
.Focus<TextField> (_ => true)
// Clear selection by pressing right in 'file path' text box
.RaiseKeyDownEvent (Key.CursorRight)
.AssertIsType<TextField> (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)
.Focus<TableView> (_ => true)
.ScreenShot ("After focus table", _out)
.Down ()
.ScreenShot ("After down in table", _out);
if (preserve)
{
c.AssertContains ("logs", sd.Path)
.AssertEndsWith ("hello", sd.Path);
}
else
{
c.AssertContains ("logs", sd.Path)
.AssertDoesNotContain ("hello", sd.Path);
}
c.Up ()
.ScreenShot ("After up in table", _out);
if (preserve)
{
c.AssertContains ("empty-dir", sd.Path)
.AssertEndsWith ("hello", sd.Path);
}
else
{
c.AssertContains ("empty-dir", sd.Path)
.AssertDoesNotContain ("hello", sd.Path);
}
c.Enter ()
.ScreenShot ("After enter in table", _out); ;
if (preserve)
{
c.AssertContains ("empty-dir", sd.Path)
.AssertEndsWith ("hello", sd.Path);
}
else
{
c.AssertContains ("empty-dir", sd.Path)
.AssertDoesNotContain ("hello", sd.Path);
}
c.LeftClick<Button> (b => b.Text == "_Save");
c.AssertFalse (sd.Canceled);
if (preserve)
{
c.AssertContains ("empty-dir", sd.Path)
.AssertEndsWith ("hello", sd.Path);
}
else
{
c.AssertContains ("empty-dir", sd.Path)
.AssertDoesNotContain ("hello", sd.Path);
}
c.WriteOutLogs (_out)
.Stop ();
}
}

View File

@@ -13,3 +13,20 @@ public class V2TestDrivers : IEnumerable<object []>
IEnumerator IEnumerable.GetEnumerator () => GetEnumerator ();
}
/// <summary>
/// Test cases for functions with signature <code>V2TestDriver d, bool someFlag</code>
/// that enumerates all variations
/// </summary>
public class V2TestDrivers_WithTrueFalseParameter : IEnumerable<object []>
{
public IEnumerator<object []> GetEnumerator ()
{
yield return new object [] { V2TestDriver.V2Win,false };
yield return new object [] { V2TestDriver.V2Net,false };
yield return new object [] { V2TestDriver.V2Win,true };
yield return new object [] { V2TestDriver.V2Net,true };
}
IEnumerator IEnumerable.GetEnumerator () => GetEnumerator ();
}