From e84d7f0e720197600949c5ce22921b1b2ef4f40a Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 30 Sep 2026 12:30:39 -0400 Subject: [PATCH] fix(ui): traverse nested active controls when restoring focus --- .../Forms/EditNestFocusTraversalTests.cs | 162 ++++++++++++++++++ OpenNest/Forms/EditNestForm.cs | 4 +- 2 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 OpenNest.WinForms.Tests/Forms/EditNestFocusTraversalTests.cs diff --git a/OpenNest.WinForms.Tests/Forms/EditNestFocusTraversalTests.cs b/OpenNest.WinForms.Tests/Forms/EditNestFocusTraversalTests.cs new file mode 100644 index 0000000..7b18133 --- /dev/null +++ b/OpenNest.WinForms.Tests/Forms/EditNestFocusTraversalTests.cs @@ -0,0 +1,162 @@ +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Windows.Forms; +using OpenNest.Forms; + +namespace OpenNest.WinForms.Tests.Forms; + +[Collection("Fill operation lifetime")] +public class EditNestFocusTraversalTests +{ + [Fact] + public void NestedActiveContainersReturnTheirLeaf() => RunSta(() => + { + using var form = CreateForm(); + var outer = new UserControl(); + var inner = new UserControl(); + var leaf = new TextBox(); + form.Controls.Add(outer); + outer.Controls.Add(inner); + inner.Controls.Add(leaf); + inner.ActiveControl = leaf; + outer.ActiveControl = inner; + form.ActiveControl = outer; + + Assert.Same(outer, form.ActiveControl); + Assert.Same(inner, outer.ActiveControl); + Assert.Same(leaf, inner.ActiveControl); + Assert.Same(leaf, GetFocusedControl(form)); + }); + + [Fact] + public void OrdinaryActiveControlIsReturnedUnchanged() => RunSta(() => + { + using var form = CreateForm(); + var leaf = new TextBox(); + form.Controls.Add(leaf); + form.ActiveControl = leaf; + + Assert.Same(leaf, form.ActiveControl); + Assert.Same(leaf, GetFocusedControl(form)); + }); + + [Fact] + public void NoActiveControlReturnsTheForm() => RunSta(() => + { + using var form = CreateForm(); + form.ActiveControl = null; + + Assert.Null(form.ActiveControl); + Assert.Same(form, GetFocusedControl(form)); + }); + + [Fact] + public void NestedContainerWithoutActiveControlIsReturned() => RunSta(() => + { + using var form = CreateForm(); + var outer = new UserControl(); + var inner = new UserControl(); + form.Controls.Add(outer); + outer.Controls.Add(inner); + inner.ActiveControl = null; + outer.ActiveControl = inner; + form.ActiveControl = outer; + + Assert.Same(outer, form.ActiveControl); + Assert.Same(inner, outer.ActiveControl); + Assert.Null(inner.ActiveControl); + Assert.Same(inner, GetFocusedControl(form)); + }); + + [Fact] + public void PlateListRefreshRestoresTheOriginalNestedLeaf() => RunSta(() => + { + using var form = CreateForm(); + form.Width = 1100; + form.Height = 600; + form.Show(); + var outer = new UserControl(); + var inner = new UserControl { Dock = DockStyle.Fill }; + var original = new TextBox { Top = 10 }; + var replacement = new TextBox { Top = 50 }; + inner.Controls.AddRange(new Control[] { original, replacement }); + outer.Controls.Add(inner); + form.ShowSidePanel(outer, "Focus regression"); + var plates = PlateList(form); + Assert.True(original.Focus()); + Assert.True(form.ContainsFocus); + Assert.Same(original, inner.ActiveControl); + + // Force a different leaf to become active during the real refresh. Merely + // focusing the old outer container would now restore this replacement. + var focusWasDisplaced = false; + plates.SelectedIndexChanged += (_, _) => + { + Assert.True(replacement.Focus()); + focusWasDisplaced = true; + }; + + form.UpdatePlateList(); + + Assert.True(focusWasDisplaced); + Assert.True(original.Focused); + Assert.False(replacement.Focused); + Assert.Same(original, GetFocusedControl(form)); + Assert.Equal(form.Nest.Plates.Count, plates.Items.Count); + Assert.Equal(form.PlateManager.CurrentIndex, Assert.Single(plates.SelectedIndices.Cast())); + }); + + [Fact] + public void PlateListRefreshKeepsFocusOnThePlateList() => RunSta(() => + { + using var form = CreateForm(); + form.Show(); + var plates = PlateList(form); + Assert.True(plates.Focus()); + + form.UpdatePlateList(); + + Assert.True(plates.Focused); + Assert.Same(plates, GetFocusedControl(form)); + Assert.Equal(form.Nest.Plates.Count, plates.Items.Count); + }); + + private static EditNestForm CreateForm() + { + var form = new EditNestForm(new Nest("focus traversal")); + form.PlateView.SetOverlapAutoCheck(null); + return form; + } + + private static Control GetFocusedControl(EditNestForm form) + { + var method = typeof(EditNestForm).GetMethod("GetFocusedControl", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(method); + return Assert.IsAssignableFrom(method.Invoke(form, null)); + } + + private static ListView PlateList(EditNestForm form) => + Assert.IsType(Assert.Single(form.Controls.Find("platesListView", true))); + + private static void RunSta(System.Action testBody) + { + Exception? testFailure = null; + var staThread = new Thread(() => + { + try + { + testBody(); + } + catch (Exception error) + { + testFailure = error; + } + }) + { IsBackground = true }; + staThread.SetApartmentState(ApartmentState.STA); + staThread.Start(); + Assert.True(staThread.Join(TimeSpan.FromSeconds(60)), "The focus traversal STA test did not complete."); + if (testFailure != null) + ExceptionDispatchInfo.Capture(testFailure).Throw(); + } +} diff --git a/OpenNest/Forms/EditNestForm.cs b/OpenNest/Forms/EditNestForm.cs index fe9cde7..cbe84d4 100644 --- a/OpenNest/Forms/EditNestForm.cs +++ b/OpenNest/Forms/EditNestForm.cs @@ -317,9 +317,9 @@ namespace OpenNest.Forms private Control GetFocusedControl() { - var ctrl = this; + Control ctrl = this; while (ctrl is ContainerControl container && container.ActiveControl != null) - return container.ActiveControl; + ctrl = container.ActiveControl; return ctrl; }