Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
37 changes: 35 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,32 @@ 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)
{
// Explicitly clear Result so Canceled == true even if the dialog was previously accepted
Result = null;

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 base.OnAccepting (args);
}

return false;
}

private void Accept (IEnumerable<FileSystemInfoStats> toMultiAccept)
{
Expand Down Expand Up @@ -112,7 +137,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
173 changes: 173 additions & 0 deletions Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// 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_WhenAccepted_ThroughCommandPipeline ()
{
// Arrange
MockFileSystem fs = new ();
fs.AddFile ("/testdir/file1.txt", new MockFileData ("hello"));
using SaveDialog sd = new TestableSaveDialog (fs);
sd.Path = "/testdir/file1.txt";

// Act
bool? accepted = sd.InvokeCommand (Command.Accept);

// Assert
Assert.True (accepted is true);
Assert.False (sd.Canceled);
Assert.NotNull (sd.Result);
Assert.Single (sd.Result);
Assert.Equal ("/testdir/file1.txt", sd.Result [0]);
}

[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);
}

[Fact]
public void FileDialog_Cancel_ClearsResult_WhenPreviouslyAccepted ()
{
// Copilot
// This test validates that canceling a dialog after a previous acceptance
// correctly clears Result to null (so Canceled == true).
// Without the explicit `Result = null` in the Cancel path, this test would fail
// because Result would retain the previously accepted value.

// Arrange
MockFileSystem fs = new ();
fs.AddFile ("/testdir/file1.txt", new MockFileData ("hello"));
using SaveDialog sd = new TestableSaveDialog (fs);
sd.Path = "/testdir/file1.txt";

// Act - first accept the dialog to set Result
sd.InvokeCommand (Command.Accept);
Assert.False (sd.Canceled); // Result is set — not canceled

// Act - now simulate pressing Cancel button
Button cancelBtn = sd.Buttons [sd.CancelButtonIndex];
cancelBtn.InvokeCommand (Command.Accept);

// Assert - Result should be cleared, Canceled should be true
Assert.Null (sd.Result);
Assert.True (sd.Canceled);
}

/// <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) { }
}
}
Loading