Browse Source

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
Thomas Nind 4 months ago
parent
commit
a08ea41b91

+ 7 - 0
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"));

+ 16 - 0
Terminal.Gui/FileServices/FileDialogStyle.cs

@@ -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 ()
     {

+ 26 - 4
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;
 
 /// <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 ()

+ 170 - 0
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<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 ();
+    }
 }

+ 17 - 0
Tests/IntegrationTests/FluentTests/V2TestDrivers.cs

@@ -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 ();
+}