Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Examples/UICatalog/Scenarios/FileDialogExamples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,12 +239,12 @@ private void CreateDialog (IApplication app)

fd.Path = Environment.GetFolderPath (Environment.SpecialFolder.UserProfile);

var result = app.Run (fd) as int?;
app.Run (fd);

IReadOnlyList<string> multiSelected = fd.MultiSelected;
string path = fd.Path;

if (result is null or 1)
if (fd.Canceled)
{
MessageBox.Query (app, "Canceled", "You canceled navigation and did not pick anything", Strings.btnOk);
}
Expand Down
35 changes: 33 additions & 2 deletions Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,30 @@ public partial class FileDialog
public bool IsCompatibleWithAllowedExtensions (IFileInfo file) => AllowedTypes.Count == 0 || MatchesAllowedTypes (file);

/// <inheritdoc/>
protected override bool OnAccepting (CommandEventArgs args) => Accept (true) && base.OnAccepting (args);
protected override bool OnAccepting (CommandEventArgs args)
{
// Check if the source is the Cancel button - if so, leave Result as null and stop
View? sourceView = null;
args.Context?.Source?.TryGetTarget (out sourceView);

if (sourceView == _btnCancel)
{
// Result stays null → Canceled == true
if (IsModal)
{
App?.RequestStop ();
}

return true;
Comment on lines +19 to +33

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Ensure there's a test that fails befor fixing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 106e296. Added FileDialog_Cancel_ClearsResult_WhenPreviouslyAccepted test that:

  • First accepts the dialog (setting Result to ["/testdir/file1.txt"])
  • Then invokes Cancel via the button
  • Asserts Result is null and Canceled is true

Verified the test fails without the Result = null line (Result retains the previously accepted value ["/testdir/file1.txt"]) and passes with it.

}

if (Accept (true))
{
return true;
Comment thread
tig marked this conversation as resolved.
Outdated
}

return false;
}

private void Accept (IEnumerable<FileSystemInfoStats> toMultiAccept)
{
Expand Down Expand Up @@ -112,7 +135,15 @@ private bool FinishAccept ()
MultiSelected = string.IsNullOrWhiteSpace (Path) ? Enumerable.Empty<string> ().ToList ().AsReadOnly () : new List<string> { Path }.AsReadOnly ();
}

Result = 2; // Ok button index
// Set the typed result to the selected paths
if (AllowsMultipleSelection)
{
Result = MultiSelected;
}
else
{
Result = string.IsNullOrWhiteSpace (Path) ? new List<string> ().AsReadOnly () : new List<string> { Path }.AsReadOnly ();
}

if (!IsModal)
{
Expand Down
14 changes: 10 additions & 4 deletions Terminal.Gui/Views/FileDialogs/FileDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Terminal.Gui.Views;
/// <summary>
/// The base-class for <see cref="OpenDialog"/> and <see cref="SaveDialog"/>
/// </summary>
public partial class FileDialog : Dialog, IDesignable
public partial class FileDialog : Dialog<IReadOnlyList<string>?>, IDesignable
{
/// <summary>Gets the Path separators for the operating system</summary>

Expand All @@ -31,9 +31,15 @@ public partial class FileDialog : Dialog, IDesignable
private readonly Button _btnCancel;

/// <summary>
/// Gets the index of the cancel button for the dialog. This is useful for checking if the user canceled the dialog by
/// comparing
/// the <see cref="Dialog.Result"/> to the index of this button in the <see cref="Dialog{TResult}.Buttons"/> array.
/// Gets whether the dialog was canceled (i.e., the user dismissed it without accepting a selection).
/// </summary>
/// <remarks>
/// Returns <see langword="true"/> if <see cref="IRunnable{TResult}.Result"/> is <see langword="null"/>.
/// </remarks>
public bool Canceled => Result is null;

/// <summary>
/// Gets the index of the cancel button for the dialog.
/// </summary>
public int CancelButtonIndex => Buttons.IndexOf (_btnCancel);

Expand Down
2 changes: 1 addition & 1 deletion Terminal.Gui/Views/FileDialogs/OpenDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public OpenDialog () { }
/// <summary>Returns the selected files, or an empty list if nothing has been selected</summary>
/// <value>The file paths.</value>
public IReadOnlyList<string> FilePaths =>
((IRunnable)this).Result is null || Result == CancelButtonIndex ? Enumerable.Empty<string> ().ToList ().AsReadOnly () :
Result is null ? Enumerable.Empty<string> ().ToList ().AsReadOnly () :
AllowsMultipleSelection ? MultiSelected : new ReadOnlyCollection<string> ([Path]);

/// <inheritdoc/>
Expand Down
2 changes: 1 addition & 1 deletion Terminal.Gui/Views/FileDialogs/SaveDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class SaveDialog : FileDialog
/// <see cref="SaveDialog"/>.
/// </summary>
/// <value>The name of the file.</value>
public string? FileName => (this as IRunnable).Result is null || Result == CancelButtonIndex ? null : Path;
public string? FileName => Result is null ? null : Path;

/// <summary>Gets the default title for the <see cref="SaveDialog"/>.</summary>
/// <returns></returns>
Expand Down
142 changes: 142 additions & 0 deletions Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copilot

using System.IO.Abstractions.TestingHelpers;

namespace UnitTests.Views;

/// <summary>
/// Tests for <see cref="FileDialog"/> refactored to inherit from <see cref="Dialog{TResult}"/>
/// where TResult is <c>IReadOnlyList&lt;string&gt;?</c>.
/// </summary>
public class FileDialogResultTests
{
[Fact]
public void FileDialog_Result_IsNull_WhenNotAccepted ()
{
// Arrange
MockFileSystem fs = new ();
fs.AddDirectory ("/testdir");
using FileDialog fd = new TestableFileDialog (fs);

// Assert - Result should be null before any acceptance
Assert.Null (fd.Result);
Assert.True (fd.Canceled);
}

[Fact]
public void FileDialog_Result_IsNull_BeforeAcceptance ()
{
// Arrange
MockFileSystem fs = new ();
fs.AddFile ("/testdir/file1.txt", new MockFileData ("hello"));
using SaveDialog sd = new TestableSaveDialog (fs);
sd.Path = "/testdir/file1.txt";

// Assert - Result should be null before any acceptance
Assert.True (sd.Canceled);
Assert.Null (sd.Result);
}

[Fact]
public void FileDialog_Canceled_IsTrue_WhenResultIsNull ()
{
// Arrange
MockFileSystem fs = new ();
fs.AddDirectory ("/testdir");
using FileDialog fd = new TestableFileDialog (fs);

// Assert
Assert.True (fd.Canceled);
}

[Fact]
public void FileDialog_Canceled_IsFalse_WhenResultIsSet ()
{
// Arrange
MockFileSystem fs = new ();
fs.AddDirectory ("/testdir");
using FileDialog fd = new TestableFileDialog (fs);

// Act
fd.Result = new List<string> { "/testdir" }.AsReadOnly ();

// Assert
Assert.False (fd.Canceled);
Assert.Single (fd.Result);
Assert.Equal ("/testdir", fd.Result [0]);
Comment thread
tig marked this conversation as resolved.
Outdated
}

[Fact]
public void FileDialog_InheritsFromDialogOfReadOnlyListString ()
{
// Arrange
MockFileSystem fs = new ();
fs.AddDirectory ("/testdir");
using FileDialog fd = new TestableFileDialog (fs);

// Assert - verify the new base type
Assert.IsAssignableFrom<Dialog<IReadOnlyList<string>?>> (fd);
}

[Fact]
public void OpenDialog_FilePaths_IsEmpty_WhenCanceled ()
{
// Arrange
using OpenDialog od = new TestableOpenDialog ();

// Assert - Result is null → Canceled → FilePaths empty
Assert.True (od.Canceled);
Assert.Empty (od.FilePaths);
}

[Fact]
public void SaveDialog_FileName_IsNull_WhenCanceled ()
{
// Arrange
MockFileSystem fs = new ();
fs.AddDirectory ("/testdir");
using SaveDialog sd = new TestableSaveDialog (fs);

// Assert
Assert.True (sd.Canceled);
Assert.Null (sd.FileName);
}

[Fact]
public void FileDialog_Result_MultiSelection_IsPopulated ()
{
// Arrange
MockFileSystem fs = new ();
fs.AddFile ("/testdir/file1.txt", new MockFileData ("a"));
fs.AddFile ("/testdir/file2.txt", new MockFileData ("b"));
using FileDialog fd = new TestableFileDialog (fs);

// Act - directly set Result as would happen after multi-select acceptance
List<string> paths = ["/testdir/file1.txt", "/testdir/file2.txt"];
fd.Result = paths.AsReadOnly ();

// Assert
Assert.False (fd.Canceled);
Assert.Equal (2, fd.Result.Count);
Assert.Contains ("/testdir/file1.txt", fd.Result);
Assert.Contains ("/testdir/file2.txt", fd.Result);
}

/// <summary>Testable subclass that exposes the internal file-system constructor.</summary>
private sealed class TestableFileDialog : FileDialog
{
public TestableFileDialog (MockFileSystem fs) : base (fs) { }
}

/// <summary>Testable subclass for OpenDialog.</summary>
private sealed class TestableOpenDialog : OpenDialog
{
public TestableOpenDialog () { }
}

/// <summary>Testable subclass for SaveDialog.</summary>
private sealed class TestableSaveDialog : SaveDialog
{
public TestableSaveDialog (MockFileSystem fs) : base (fs) { }
}
}