diff --git a/OpenNest.WinForms.Tests/Forms/AutoNestFormStockOptionsTests.cs b/OpenNest.WinForms.Tests/Forms/AutoNestFormStockOptionsTests.cs new file mode 100644 index 0000000..a360f95 --- /dev/null +++ b/OpenNest.WinForms.Tests/Forms/AutoNestFormStockOptionsTests.cs @@ -0,0 +1,109 @@ +using System.Collections; +using System.ComponentModel; +using System.Runtime.ExceptionServices; +using System.Windows.Forms; +using OpenNest.Forms; + +namespace OpenNest.WinForms.Tests.Forms; + +public class AutoNestFormStockOptionsTests +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public void StockOptionsKeepAnAddRowAfterAddingAPlate(bool loadSavedOptions) + { + RunSta(() => + { + using var form = new AutoNestForm(new Nest()); + if (loadSavedOptions) + form.LoadPlateOptions(new List + { + new() { Width = 48, Length = 96, Cost = 25 }, + }, 0.5); + form.OptimizePlateSize = true; + var grid = GetGrid(form); + var initialCount = form.GetPlateOptions().Count; + + AssertNewRow(grid); + var rows = Assert.IsAssignableFrom(grid.DataSource); + Assert.True(rows.AllowNew); + var added = rows.AddNew()!; + SetValue(added, "Size", "84 x 168"); + SetValue(added, "Cost", 123.5); + ((ICancelAddNew)rows).EndNew(rows.Count - 1); + + var options = form.GetPlateOptions(); + Assert.Equal(initialCount + 1, options.Count); + var option = options.Last(); + Assert.Equal(84, option.Width); + Assert.Equal(168, option.Length); + Assert.Equal(123.5, option.Cost); + AssertNewRow(grid); + }); + } + + [Fact] + public void BlankAndCancelledRowsDoNotBecomeStockOptions() + { + RunSta(() => + { + using var form = new AutoNestForm(new Nest()); + var saved = new List + { + new() { Width = 48, Length = 96, Cost = 25 }, + }; + form.LoadPlateOptions(saved, 0.5); + var grid = GetGrid(form); + var rows = Assert.IsAssignableFrom(grid.DataSource); + var addNew = Assert.IsAssignableFrom(rows); + + rows.AddNew(); + Assert.Single(form.GetPlateOptions()); + addNew.CancelNew(rows.Count - 1); + Assert.Single((IEnumerable)rows); + + rows.Clear(); + Assert.Empty(form.GetPlateOptions()); + AssertNewRow(grid); + Assert.Single(saved); + Assert.Equal(48, saved[0].Width); + Assert.Equal(96, saved[0].Length); + Assert.Equal(25, saved[0].Cost); + }); + } + + private static DataGridView GetGrid(AutoNestForm form) + { + // Binding needs a context even when the test does not show the form. + form.BindingContext = new BindingContext(); + var grid = Assert.IsType(form.Controls.Find("plateGrid", true).Single()); + grid.CreateControl(); + return grid; + } + + private static void AssertNewRow(DataGridView grid) + { + Assert.True(grid.AllowUserToAddRows); + Assert.True(grid.NewRowIndex >= 0); + Assert.True(grid.Rows[grid.NewRowIndex].IsNewRow); + } + + private static void SetValue(object row, string property, object value) => + TypeDescriptor.GetProperties(row)[property]!.SetValue(row, value); + + private static void RunSta(Action action) + { + Exception? error = null; + var thread = new Thread(() => + { + try { action(); } + catch (Exception ex) { error = ex; } + }); + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + Assert.True(thread.Join(TimeSpan.FromSeconds(15))); + if (error != null) + ExceptionDispatchInfo.Capture(error).Throw(); + } +} diff --git a/OpenNest/Forms/AutoNestForm.cs b/OpenNest/Forms/AutoNestForm.cs index 525f50a..4997eb7 100644 --- a/OpenNest/Forms/AutoNestForm.cs +++ b/OpenNest/Forms/AutoNestForm.cs @@ -195,8 +195,9 @@ namespace OpenNest.Forms public List GetPlateOptions() { + plateGrid.EndEdit(); var result = new List(); - var gridItems = plateGrid.DataSource as List; + var gridItems = plateGrid.DataSource as BindingList; if (gridItems == null) return result; @@ -231,7 +232,7 @@ namespace OpenNest.Forms Cost = o.Cost, }) .ToList(); - plateGrid.DataSource = items; + plateGrid.DataSource = new BindingList(items); optimizePlateSizeBox.Checked = true; } SalvageRate = salvageRate; @@ -239,7 +240,8 @@ namespace OpenNest.Forms private void LoadDefaultPlateOptions() { - var items = new List + // A bound DataGridView needs IBindingList.AddNew support for its blank last row. + var items = new BindingList { new() { Size = "48 x 96", Cost = 0 }, new() { Size = "48 x 120", Cost = 0 }, @@ -385,6 +387,8 @@ namespace OpenNest.Forms private class PlateOptionItem { + public PlateOptionItem() { } + public string Size { get; set; } public double Cost { get; set; } } diff --git a/docs/automatic-nesting.md b/docs/automatic-nesting.md index 6ad1a0a..f994db7 100644 --- a/docs/automatic-nesting.md +++ b/docs/automatic-nesting.md @@ -14,7 +14,7 @@ Every engine selected in **Nest > Auto Nest** uses the pipeline. Part-First and its sorting controls are removed. Use interactive Fill Area/remnant tools for leftover space on occupied sheets; these are not whole-job nesting. -Auto Nest starts on empty/new sheets and does not change existing populated plates. Stock Options offers sheet sizes and salvage settings; minimum salvage size is part of that section, not a Part-First option. The maximum remains 100 physical sheets per run. +Auto Nest starts on empty/new sheets and does not change existing populated plates. Stock Options offers sheet sizes and salvage settings; minimum salvage size is part of that section, not a Part-First option. When enabled, its grid keeps a blank last row for adding another size (`W x L`) and cost, including after loading saved options. Unused blank rows are not offered as stock. The maximum remains 100 physical sheets per run. Progress is owned and modal so the input drawings cannot be edited while a worker uses them. Stop or closing progress cancels and discards the whole proposal. The dialog waits for the worker to finish; no engine has an Accept-early button on this path. A plug-in that ignores the cancellation token cannot commit its late result.