From b803654076f2bc9c750cad205dd216e33a8e3522 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 15:28:49 +0000 Subject: [PATCH 1/7] Initial plan From 0866596f6e8687622a0662a90193783f4275c373 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 15:39:00 +0000 Subject: [PATCH 2/7] Refactor FileDialog base type from Dialog to Dialog?> for typed result BREAKING CHANGE: FileDialog now inherits from Dialog?> instead of Dialog (Dialog). The Result property is now a list of selected paths (non-null on Accept, null on Cancel) instead of a button index integer. - Changed FileDialog base class to Dialog?> - Added Canceled property to FileDialog (Result is null) - Updated FinishAccept() to set Result to selected path list - Updated OnAccepting to properly handle Cancel button - Updated OpenDialog.FilePaths to use new Result type - Updated SaveDialog.FileName to use new Result type - Updated FileDialogExamples scenario - Added unit tests for new Result behavior Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/55cd95c8-eece-4112-a2ed-4f12775075ea Co-authored-by: tig <585482+tig@users.noreply.github.com> --- .../UICatalog/Scenarios/FileDialogExamples.cs | 4 +- .../Views/FileDialogs/FileDialog.Commands.cs | 35 ++++- Terminal.Gui/Views/FileDialogs/FileDialog.cs | 14 +- Terminal.Gui/Views/FileDialogs/OpenDialog.cs | 2 +- Terminal.Gui/Views/FileDialogs/SaveDialog.cs | 2 +- .../Views/FileDialogResultTests.cs | 145 ++++++++++++++++++ 6 files changed, 192 insertions(+), 10 deletions(-) create mode 100644 Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs 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..77e5591dc7 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs @@ -14,7 +14,30 @@ 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) + { + // Result stays null → Canceled == true + if (IsModal) + { + App?.RequestStop (); + } + + return true; + } + + if (Accept (true)) + { + return true; + } + + return false; + } private void Accept (IEnumerable toMultiAccept) { @@ -112,7 +135,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..4822796fbd --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs @@ -0,0 +1,145 @@ +// 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_IsPopulated_OnSingleSelection () + { + // Arrange + MockFileSystem fs = new (); + fs.AddFile ("/testdir/file1.txt", new MockFileData ("hello")); + using SaveDialog sd = new TestableSaveDialog (fs); + sd.Path = "/testdir/file1.txt"; + + // Act - simulate acceptance by calling FinishAccept indirectly through reflection + // We set path and call the internal accept logic + 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 { "/testdir" }.AsReadOnly (); + + // Assert + Assert.False (fd.Canceled); + Assert.Single (fd.Result); + Assert.Equal ("/testdir", fd.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 + MockFileSystem fs = new (); + fs.AddDirectory ("/testdir"); + using OpenDialog od = new TestableOpenDialog (fs); + + // 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); + } + + /// 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 (MockFileSystem fs) { } + } + + /// Testable subclass for SaveDialog. + private sealed class TestableSaveDialog : SaveDialog + { + public TestableSaveDialog (MockFileSystem fs) : base (fs) { } + } +} From 5eb9e1ef19832dca69b08fe58f33299b731a33e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 15:41:37 +0000 Subject: [PATCH 3/7] Address review feedback: fix test naming and unused parameter Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/55cd95c8-eece-4112-a2ed-4f12775075ea Co-authored-by: tig <585482+tig@users.noreply.github.com> --- .../Views/FileDialogResultTests.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs b/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs index 4822796fbd..edf4443842 100644 --- a/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs +++ b/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs @@ -24,7 +24,7 @@ public void FileDialog_Result_IsNull_WhenNotAccepted () } [Fact] - public void FileDialog_Result_IsPopulated_OnSingleSelection () + public void FileDialog_Result_IsNull_BeforeAcceptance () { // Arrange MockFileSystem fs = new (); @@ -32,8 +32,7 @@ public void FileDialog_Result_IsPopulated_OnSingleSelection () using SaveDialog sd = new TestableSaveDialog (fs); sd.Path = "/testdir/file1.txt"; - // Act - simulate acceptance by calling FinishAccept indirectly through reflection - // We set path and call the internal accept logic + // Assert - Result should be null before any acceptance Assert.True (sd.Canceled); Assert.Null (sd.Result); } @@ -83,9 +82,7 @@ public void FileDialog_InheritsFromDialogOfReadOnlyListString () public void OpenDialog_FilePaths_IsEmpty_WhenCanceled () { // Arrange - MockFileSystem fs = new (); - fs.AddDirectory ("/testdir"); - using OpenDialog od = new TestableOpenDialog (fs); + using OpenDialog od = new TestableOpenDialog (); // Assert - Result is null → Canceled → FilePaths empty Assert.True (od.Canceled); @@ -134,7 +131,7 @@ public TestableFileDialog (MockFileSystem fs) : base (fs) { } /// Testable subclass for OpenDialog. private sealed class TestableOpenDialog : OpenDialog { - public TestableOpenDialog (MockFileSystem fs) { } + public TestableOpenDialog () { } } /// Testable subclass for SaveDialog. From 102ef3ac1f426e64d153b43a53c87fb393be4c49 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 4 May 2026 10:33:17 -0600 Subject: [PATCH 4/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs index 77e5591dc7..fd79bd4c55 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs @@ -33,7 +33,7 @@ protected override bool OnAccepting (CommandEventArgs args) if (Accept (true)) { - return true; + return base.OnAccepting (args); } return false; From e4550e926ca73a7e3f58d31f7cf2da383cd178ea Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 4 May 2026 10:33:55 -0600 Subject: [PATCH 5/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Views/FileDialogResultTests.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs b/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs index edf4443842..e99930a8e0 100644 --- a/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs +++ b/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs @@ -50,20 +50,23 @@ public void FileDialog_Canceled_IsTrue_WhenResultIsNull () } [Fact] - public void FileDialog_Canceled_IsFalse_WhenResultIsSet () + public void FileDialog_Canceled_IsFalse_WhenAccepted_ThroughCommandPipeline () { // Arrange MockFileSystem fs = new (); - fs.AddDirectory ("/testdir"); - using FileDialog fd = new TestableFileDialog (fs); + fs.AddFile ("/testdir/file1.txt", new MockFileData ("hello")); + using SaveDialog sd = new TestableSaveDialog (fs); + sd.Path = "/testdir/file1.txt"; // Act - fd.Result = new List { "/testdir" }.AsReadOnly (); + bool accepted = sd.InvokeCommand (Command.Accept); // Assert - Assert.False (fd.Canceled); - Assert.Single (fd.Result); - Assert.Equal ("/testdir", fd.Result [0]); + Assert.True (accepted); + Assert.False (sd.Canceled); + Assert.NotNull (sd.Result); + Assert.Single (sd.Result); + Assert.Equal ("/testdir/file1.txt", sd.Result [0]); } [Fact] From 967d0b65b1bacc84fc681826d2dfcd03ea482b43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 17:09:24 +0000 Subject: [PATCH 6/7] Fix CI build error: use bool? for InvokeCommand return type and clear Result on Cancel Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/cb0fa1f2-e1a9-4de0-8a10-94a2f6cc722d Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs | 4 +++- Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs index fd79bd4c55..e74db10c65 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialog.Commands.cs @@ -22,7 +22,9 @@ protected override bool OnAccepting (CommandEventArgs args) if (sourceView == _btnCancel) { - // Result stays null → Canceled == true + // Explicitly clear Result so Canceled == true even if the dialog was previously accepted + Result = null; + if (IsModal) { App?.RequestStop (); diff --git a/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs b/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs index e99930a8e0..3bc66ee040 100644 --- a/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs +++ b/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs @@ -59,10 +59,10 @@ public void FileDialog_Canceled_IsFalse_WhenAccepted_ThroughCommandPipeline () sd.Path = "/testdir/file1.txt"; // Act - bool accepted = sd.InvokeCommand (Command.Accept); + bool? accepted = sd.InvokeCommand (Command.Accept); // Assert - Assert.True (accepted); + Assert.True (accepted is true); Assert.False (sd.Canceled); Assert.NotNull (sd.Result); Assert.Single (sd.Result); From 106e296f96911052c4a12df840076a64f68546cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 4 May 2026 17:27:11 +0000 Subject: [PATCH 7/7] Add test proving Cancel clears Result when dialog was previously accepted Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/0b17b364-95c0-468f-9356-4e56f007914c Co-authored-by: tig <585482+tig@users.noreply.github.com> --- .../Views/FileDialogResultTests.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs b/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs index 3bc66ee040..acb1d037b2 100644 --- a/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs +++ b/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs @@ -125,6 +125,34 @@ public void FileDialog_Result_MultiSelection_IsPopulated () 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 {