diff --git a/Examples/UICatalog/Scenarios/FileDialogExamples.cs b/Examples/UICatalog/Scenarios/FileDialogExamples.cs index 5681307de3..87b8a45545 100644 --- a/Examples/UICatalog/Scenarios/FileDialogExamples.cs +++ b/Examples/UICatalog/Scenarios/FileDialogExamples.cs @@ -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 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); } diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs index 77786871c2..e74db10c65 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs @@ -14,7 +14,32 @@ public partial class FileDialog public bool IsCompatibleWithAllowedExtensions (IFileInfo file) => AllowedTypes.Count == 0 || MatchesAllowedTypes (file); /// - 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; + } + + if (Accept (true)) + { + return base.OnAccepting (args); + } + + return false; + } private void Accept (IEnumerable toMultiAccept) { @@ -112,7 +137,15 @@ private bool FinishAccept () MultiSelected = string.IsNullOrWhiteSpace (Path) ? Enumerable.Empty ().ToList ().AsReadOnly () : new List { 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 ().AsReadOnly () : new List { Path }.AsReadOnly (); + } if (!IsModal) { diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.cs index 3e96a4e54c..c30621228c 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialog.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui.Views; /// /// The base-class for and /// -public partial class FileDialog : Dialog, IDesignable +public partial class FileDialog : Dialog?>, IDesignable { /// Gets the Path separators for the operating system @@ -31,9 +31,15 @@ public partial class FileDialog : Dialog, IDesignable private readonly Button _btnCancel; /// - /// Gets the index of the cancel button for the dialog. This is useful for checking if the user canceled the dialog by - /// comparing - /// the to the index of this button in the array. + /// Gets whether the dialog was canceled (i.e., the user dismissed it without accepting a selection). + /// + /// + /// Returns if is . + /// + public bool Canceled => Result is null; + + /// + /// Gets the index of the cancel button for the dialog. /// public int CancelButtonIndex => Buttons.IndexOf (_btnCancel); diff --git a/Terminal.Gui/Views/FileDialogs/OpenDialog.cs b/Terminal.Gui/Views/FileDialogs/OpenDialog.cs index c6df91ee9b..f0a587670f 100644 --- a/Terminal.Gui/Views/FileDialogs/OpenDialog.cs +++ b/Terminal.Gui/Views/FileDialogs/OpenDialog.cs @@ -24,7 +24,7 @@ public OpenDialog () { } /// Returns the selected files, or an empty list if nothing has been selected /// The file paths. public IReadOnlyList FilePaths => - ((IRunnable)this).Result is null || Result == CancelButtonIndex ? Enumerable.Empty ().ToList ().AsReadOnly () : + Result is null ? Enumerable.Empty ().ToList ().AsReadOnly () : AllowsMultipleSelection ? MultiSelected : new ReadOnlyCollection ([Path]); /// diff --git a/Terminal.Gui/Views/FileDialogs/SaveDialog.cs b/Terminal.Gui/Views/FileDialogs/SaveDialog.cs index b0af109e7c..daf56ba568 100644 --- a/Terminal.Gui/Views/FileDialogs/SaveDialog.cs +++ b/Terminal.Gui/Views/FileDialogs/SaveDialog.cs @@ -25,7 +25,7 @@ public class SaveDialog : FileDialog /// . /// /// The name of the file. - public string? FileName => (this as IRunnable).Result is null || Result == CancelButtonIndex ? null : Path; + public string? FileName => Result is null ? null : Path; /// Gets the default title for the . /// diff --git a/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs b/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs new file mode 100644 index 0000000000..acb1d037b2 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs @@ -0,0 +1,173 @@ +// Copilot + +using System.IO.Abstractions.TestingHelpers; + +namespace UnitTests.Views; + +/// +/// Tests for refactored to inherit from +/// where TResult is IReadOnlyList<string>?. +/// +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?>> (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 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); + } + + /// Testable subclass that exposes the internal file-system constructor. + private sealed class TestableFileDialog : FileDialog + { + public TestableFileDialog (MockFileSystem fs) : base (fs) { } + } + + /// Testable subclass for OpenDialog. + private sealed class TestableOpenDialog : OpenDialog + { + public TestableOpenDialog () { } + } + + /// Testable subclass for SaveDialog. + private sealed class TestableSaveDialog : SaveDialog + { + public TestableSaveDialog (MockFileSystem fs) : base (fs) { } + } +}