Skip to content

Commit d0bfa5e

Browse files
authored
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
1 parent b46b778 commit d0bfa5e

File tree

5 files changed

+236
-4
lines changed

5 files changed

+236
-4
lines changed

Examples/UICatalog/Scenarios/FileDialogExamples.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class FileDialogExamples : Scenario
1616
private CheckBox _cbAlwaysTableShowHeaders;
1717
private CheckBox _cbCaseSensitive;
1818
private CheckBox _cbDrivesOnlyInTree;
19+
private CheckBox _cbPreserveFilenameOnDirectoryChanges;
1920
private CheckBox _cbFlipButtonOrder;
2021
private CheckBox _cbMustExist;
2122
private CheckBox _cbShowTreeBranchLines;
@@ -55,6 +56,9 @@ public override void Main ()
5556
_cbDrivesOnlyInTree = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "Only Show _Drives" };
5657
win.Add (_cbDrivesOnlyInTree);
5758

59+
_cbPreserveFilenameOnDirectoryChanges = new CheckBox { CheckedState = CheckState.UnChecked, Y = y++, X = x, Text = "Preserve Filename" };
60+
win.Add (_cbPreserveFilenameOnDirectoryChanges);
61+
5862
y = 0;
5963
x = 24;
6064

@@ -198,6 +202,9 @@ private void CreateDialog ()
198202
fd.Style.TreeRootGetter = () => { return Environment.GetLogicalDrives ().ToDictionary (dirInfoFactory.New, k => k); };
199203
}
200204

205+
fd.Style.PreserveFilenameOnDirectoryChanges = _cbPreserveFilenameOnDirectoryChanges.CheckedState == CheckState.Checked;
206+
207+
201208
if (_rgAllowedTypes.SelectedItem > 0)
202209
{
203210
fd.AllowedTypes.Add (new AllowedType ("Data File", ".csv", ".tsv"));

Terminal.Gui/FileServices/FileDialogStyle.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ namespace Terminal.Gui;
1010
public class FileDialogStyle
1111
{
1212
private readonly IFileSystem _fileSystem;
13+
private bool _preserveFilenameOnDirectoryChanges;
1314

1415
/// <summary>Creates a new instance of the <see cref="FileDialogStyle"/> class.</summary>
1516
public FileDialogStyle (IFileSystem fileSystem)
@@ -144,6 +145,21 @@ public FileDialogStyle (IFileSystem fileSystem)
144145
/// </summary>
145146
public string WrongFileTypeFeedback { get; set; } = Strings.fdWrongFileTypeFeedback;
146147

148+
149+
/// <summary>
150+
/// <para>
151+
/// Gets or sets a flag that determines behaviour when opening (double click/enter) or selecting a
152+
/// directory in a <see cref="FileDialog"/>.
153+
/// </para>
154+
/// <para>If <see langword="false"/> (the default) then the <see cref="FileDialog.Path"/> is simply
155+
/// updated to the new directory path.</para>
156+
/// <para>If <see langword="true"/> then any typed or previously selected file
157+
/// name is preserved (e.g. "c:/hello.csv" when opening "temp" becomes "c:/temp/hello.csv").
158+
/// </para>
159+
/// </summary>
160+
public bool PreserveFilenameOnDirectoryChanges { get; set; }
161+
162+
147163
[UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
148164
private Dictionary<IDirectoryInfo, string> DefaultTreeRootGetter ()
149165
{

Terminal.Gui/Views/FileDialog.cs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using System.Text.RegularExpressions;
33
using Terminal.Gui.Resources;
44

5+
#nullable enable
6+
57
namespace Terminal.Gui;
68

79
/// <summary>
@@ -1135,7 +1137,7 @@ private void PushState (
11351137
}
11361138
else if (setPathText)
11371139
{
1138-
Path = newState.Directory.FullName;
1140+
SetPathToSelectedObject (newState.Directory);
11391141
}
11401142

11411143
State = newState;
@@ -1393,7 +1395,7 @@ private void TableView_SelectedCellChanged (object sender, SelectedCellChangedEv
13931395
{
13941396
_pushingState = true;
13951397

1396-
Path = dest.FullName;
1398+
SetPathToSelectedObject (dest);
13971399
State.Selected = stats;
13981400
_tbPath.Autocomplete.ClearSuggestions ();
13991401
}
@@ -1405,12 +1407,32 @@ private void TableView_SelectedCellChanged (object sender, SelectedCellChangedEv
14051407

14061408
private void TreeView_SelectionChanged (object sender, SelectionChangedEventArgs<IFileSystemInfo> e)
14071409
{
1408-
if (e.NewValue is null)
1410+
SetPathToSelectedObject (e.NewValue);
1411+
}
1412+
1413+
private void SetPathToSelectedObject (IFileSystemInfo? selected)
1414+
{
1415+
if (selected is null)
14091416
{
14101417
return;
14111418
}
14121419

1413-
Path = e.NewValue.FullName;
1420+
if (selected is IDirectoryInfo && Style.PreserveFilenameOnDirectoryChanges)
1421+
{
1422+
if (!string.IsNullOrWhiteSpace (Path) && !_fileSystem.Directory.Exists (Path))
1423+
{
1424+
var currentFile = _fileSystem.Path.GetFileName (Path);
1425+
1426+
if (!string.IsNullOrWhiteSpace (currentFile))
1427+
{
1428+
Path = _fileSystem.Path.Combine (selected.FullName, currentFile);
1429+
1430+
return;
1431+
}
1432+
}
1433+
}
1434+
1435+
Path = selected.FullName;
14141436
}
14151437

14161438
private bool TryAcceptMulti ()

Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,174 @@ public void SaveFileDialog_PopTree_AndNavigate (V2TestDriver d)
189189
.WriteOutLogs (_out)
190190
.Stop ();
191191
}
192+
193+
[Theory]
194+
[ClassData (typeof (V2TestDrivers))]
195+
public void SaveFileDialog_PopTree_AndNavigate_PreserveFilenameOnDirectoryChanges_True (V2TestDriver d)
196+
{
197+
var sd = new SaveDialog (CreateExampleFileSystem ()) { Modal = false };
198+
sd.Style.PreserveFilenameOnDirectoryChanges = true;
199+
200+
using var c = With.A (sd, 100, 20, d)
201+
.ScreenShot ("Save dialog", _out)
202+
.Then (() => Assert.True (sd.Canceled))
203+
.Focus<TextField> (_=>true)
204+
// Clear selection by pressing right in 'file path' text box
205+
.RaiseKeyDownEvent (Key.CursorRight)
206+
.AssertIsType <TextField>(sd.Focused)
207+
// Type a filename into the dialog
208+
.RaiseKeyDownEvent (Key.H)
209+
.RaiseKeyDownEvent (Key.E)
210+
.RaiseKeyDownEvent (Key.L)
211+
.RaiseKeyDownEvent (Key.L)
212+
.RaiseKeyDownEvent (Key.O)
213+
.WaitIteration ()
214+
.ScreenShot ("After typing filename 'hello'", _out)
215+
.AssertEndsWith ("hello", sd.Path)
216+
.LeftClick<Button> (b => b.Text == "►►")
217+
.ScreenShot ("After pop tree", _out)
218+
.Focus<TreeView<IFileSystemInfo>> (_ => true)
219+
.Right ()
220+
.ScreenShot ("After expand tree", _out)
221+
// Because of PreserveFilenameOnDirectoryChanges we should select the new dir but keep the filename
222+
.AssertEndsWith ("hello", sd.Path)
223+
.Down ()
224+
.ScreenShot ("After navigate down in tree", _out)
225+
// Because of PreserveFilenameOnDirectoryChanges we should select the new dir but keep the filename
226+
.AssertContains ("empty-dir",sd.Path)
227+
.AssertEndsWith ("hello", sd.Path)
228+
.Enter ()
229+
.WaitIteration ()
230+
.Then (() => Assert.False (sd.Canceled))
231+
.AssertContains ("empty-dir", sd.FileName)
232+
.WriteOutLogs (_out)
233+
.Stop ();
234+
}
235+
236+
[Theory]
237+
[ClassData (typeof (V2TestDrivers))]
238+
public void SaveFileDialog_PopTree_AndNavigate_PreserveFilenameOnDirectoryChanges_False (V2TestDriver d)
239+
{
240+
var sd = new SaveDialog (CreateExampleFileSystem ()) { Modal = false };
241+
sd.Style.PreserveFilenameOnDirectoryChanges = false;
242+
243+
using var c = With.A (sd, 100, 20, d)
244+
.ScreenShot ("Save dialog", _out)
245+
.Then (() => Assert.True (sd.Canceled))
246+
.Focus<TextField> (_ => true)
247+
// Clear selection by pressing right in 'file path' text box
248+
.RaiseKeyDownEvent (Key.CursorRight)
249+
.AssertIsType<TextField> (sd.Focused)
250+
// Type a filename into the dialog
251+
.RaiseKeyDownEvent (Key.H)
252+
.RaiseKeyDownEvent (Key.E)
253+
.RaiseKeyDownEvent (Key.L)
254+
.RaiseKeyDownEvent (Key.L)
255+
.RaiseKeyDownEvent (Key.O)
256+
.WaitIteration ()
257+
.ScreenShot ("After typing filename 'hello'", _out)
258+
.AssertEndsWith ("hello", sd.Path)
259+
.LeftClick<Button> (b => b.Text == "►►")
260+
.ScreenShot ("After pop tree", _out)
261+
.Focus<TreeView<IFileSystemInfo>> (_ => true)
262+
.Right ()
263+
.ScreenShot ("After expand tree", _out)
264+
.Down ()
265+
.ScreenShot ("After navigate down in tree", _out)
266+
// PreserveFilenameOnDirectoryChanges is false so just select new path
267+
.AssertEndsWith ("empty-dir", sd.Path)
268+
.AssertDoesNotContain ("hello", sd.Path)
269+
.Enter ()
270+
.WaitIteration ()
271+
.Then (() => Assert.False (sd.Canceled))
272+
.AssertContains ("empty-dir", sd.FileName)
273+
.WriteOutLogs (_out)
274+
.Stop ();
275+
}
276+
277+
[Theory]
278+
[ClassData (typeof (V2TestDrivers_WithTrueFalseParameter))]
279+
public void SaveFileDialog_TableView_UpDown_PreserveFilenameOnDirectoryChanges_True (V2TestDriver d, bool preserve)
280+
{
281+
var sd = new SaveDialog (CreateExampleFileSystem ()) { Modal = false };
282+
sd.Style.PreserveFilenameOnDirectoryChanges = preserve;
283+
284+
using var c = With.A (sd, 100, 20, d)
285+
.ScreenShot ("Save dialog", _out)
286+
.Then (() => Assert.True (sd.Canceled))
287+
.Focus<TextField> (_ => true)
288+
// Clear selection by pressing right in 'file path' text box
289+
.RaiseKeyDownEvent (Key.CursorRight)
290+
.AssertIsType<TextField> (sd.Focused)
291+
// Type a filename into the dialog
292+
.RaiseKeyDownEvent (Key.H)
293+
.RaiseKeyDownEvent (Key.E)
294+
.RaiseKeyDownEvent (Key.L)
295+
.RaiseKeyDownEvent (Key.L)
296+
.RaiseKeyDownEvent (Key.O)
297+
.WaitIteration ()
298+
.ScreenShot ("After typing filename 'hello'", _out)
299+
.AssertEndsWith ("hello", sd.Path)
300+
.Focus<TableView> (_ => true)
301+
.ScreenShot ("After focus table", _out)
302+
.Down ()
303+
.ScreenShot ("After down in table", _out);
304+
305+
if (preserve)
306+
{
307+
c.AssertContains ("logs", sd.Path)
308+
.AssertEndsWith ("hello", sd.Path);
309+
}
310+
else
311+
{
312+
c.AssertContains ("logs", sd.Path)
313+
.AssertDoesNotContain ("hello", sd.Path);
314+
}
315+
316+
c.Up ()
317+
.ScreenShot ("After up in table", _out);
318+
319+
if (preserve)
320+
{
321+
c.AssertContains ("empty-dir", sd.Path)
322+
.AssertEndsWith ("hello", sd.Path);
323+
}
324+
else
325+
{
326+
c.AssertContains ("empty-dir", sd.Path)
327+
.AssertDoesNotContain ("hello", sd.Path);
328+
}
329+
330+
c.Enter ()
331+
.ScreenShot ("After enter in table", _out); ;
332+
333+
334+
if (preserve)
335+
{
336+
c.AssertContains ("empty-dir", sd.Path)
337+
.AssertEndsWith ("hello", sd.Path);
338+
}
339+
else
340+
{
341+
c.AssertContains ("empty-dir", sd.Path)
342+
.AssertDoesNotContain ("hello", sd.Path);
343+
}
344+
345+
c.LeftClick<Button> (b => b.Text == "_Save");
346+
c.AssertFalse (sd.Canceled);
347+
348+
if (preserve)
349+
{
350+
c.AssertContains ("empty-dir", sd.Path)
351+
.AssertEndsWith ("hello", sd.Path);
352+
}
353+
else
354+
{
355+
c.AssertContains ("empty-dir", sd.Path)
356+
.AssertDoesNotContain ("hello", sd.Path);
357+
}
358+
359+
c.WriteOutLogs (_out)
360+
.Stop ();
361+
}
192362
}

Tests/IntegrationTests/FluentTests/V2TestDrivers.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,20 @@ public IEnumerator<object []> GetEnumerator ()
1313

1414
IEnumerator IEnumerable.GetEnumerator () => GetEnumerator ();
1515
}
16+
17+
/// <summary>
18+
/// Test cases for functions with signature <code>V2TestDriver d, bool someFlag</code>
19+
/// that enumerates all variations
20+
/// </summary>
21+
public class V2TestDrivers_WithTrueFalseParameter : IEnumerable<object []>
22+
{
23+
public IEnumerator<object []> GetEnumerator ()
24+
{
25+
yield return new object [] { V2TestDriver.V2Win,false };
26+
yield return new object [] { V2TestDriver.V2Net,false };
27+
yield return new object [] { V2TestDriver.V2Win,true };
28+
yield return new object [] { V2TestDriver.V2Net,true };
29+
}
30+
31+
IEnumerator IEnumerable.GetEnumerator () => GetEnumerator ();
32+
}

0 commit comments

Comments
 (0)