feat(desktop): adjust nest quantities from the drawing library dialog

After adding library drawings the operator had to close the dialog to see what
the nest now needed, and fixing a quantity meant leaving it for Edit Drawing
one part at a time. The new In This Nest tab lists the nest's drawings (cutoffs
excluded) with their library revision and required, nested and remaining
quantities, refreshes after each add, and edits Required in place.

An edit applies to the nest immediately, like an add, and the nest window
refreshes its drawing list when the dialog closes. Out-of-range values are
refused both when the cell is validated and when they are applied, because
DataGridView only validates on leaving the cell and EndEdit commits without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-22 11:57:30 -04:00
1 parent 8fd1fabc54
commit 939d7bfff2
5 files changed
+363 -3

No files matched your search

@@ -91,6 +91,70 @@ public class DrawingLibraryFormTests
"The drawing library preview test did not complete.");
}
[Fact]
public void InThisNestTab_ListsTheNestsDrawings_AndEditsTheirRequiredQuantity()
{
StaTestThread.Run(
() =>
{
var library = new FakeLibrary();
var record = library.Add("SYN-606", "Synthetic Steel", "Grade A", 0.25, Units.Inches, "");
var nest = MatchingNest();
var local = new RectangleShape { Name = "LOCAL-1", Length = 3, Width = 1 }.GetDrawing();
local.Quantity.Required = 4;
nest.Drawings.Add(local);
var cutoff = new RectangleShape { Name = "CUTOFF", Length = 1, Width = 1 }.GetDrawing();
cutoff.IsCutOff = true;
nest.Drawings.Add(cutoff);
using var form = new DrawingLibraryForm(library, Server, nest);
form.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
form.LoadPreview = (_, _) => Task.FromResult<Drawing>(null!);
form.Show();
var grid = Field<DataGridView>(form, "drawingGrid");
var nestGrid = Field<DataGridView>(form, "nestGrid");
var nestTab = Field<TabPage>(form, "nestTab");
Assert.Equal("In This Nest (1)", nestTab.Text);
Assert.Equal("LOCAL-1", Assert.Single(nestGrid.Rows.Cast<DataGridViewRow>()).Cells["nestNameColumn"].Value);
Assert.Equal("Not from the library", nestGrid.Rows[0].Cells["nestLibraryColumn"].Value);
PumpUntil(() => grid.Rows.Count == 1, "the library page");
Field<NumericUpDown>(form, "quantityBox").Value = 2;
Run(form.AddSelectedAsync());
Assert.Equal("In This Nest (2)", nestTab.Text);
Assert.Equal($"Revision {record.Revision}", nestGrid.Rows[1].Cells["nestLibraryColumn"].Value);
Assert.Equal(2, nestGrid.Rows[1].Cells["nestRequiredColumn"].Value);
Field<TabControl>(form, "tabControl").SelectedTab = nestTab;
var required = nestGrid.Rows[0].Cells["nestRequiredColumn"];
nestGrid.CurrentCell = required;
Assert.True(nestGrid.BeginEdit(true));
nestGrid.EditingControl!.Text = "-1";
nestGrid.NotifyCurrentCellDirty(true);
// Leaving the cell validates it and keeps the editor open on a bad number.
Assert.Throws<InvalidOperationException>(() => nestGrid.CurrentCell = nestGrid.Rows[0].Cells["nestNameColumn"]);
Assert.NotEmpty(nestGrid.Rows[0].ErrorText);
// A commit that skips validation is refused when it is applied.
nestGrid.EndEdit();
Assert.Equal(4, local.Quantity.Required);
Assert.Equal(4, required.Value);
Assert.False(form.QuantitiesChanged);
Assert.True(nestGrid.BeginEdit(true));
nestGrid.EditingControl!.Text = "9";
nestGrid.NotifyCurrentCellDirty(true);
Assert.True(nestGrid.EndEdit());
Assert.Equal(9, local.Quantity.Required);
Assert.Equal(9, nestGrid.Rows[0].Cells["nestRemainingColumn"].Value);
Assert.True(form.QuantitiesChanged);
Assert.Contains("11 parts required", Field<Label>(form, "nestSummaryLabel").Text);
},
TestTimeout,
"The In This Nest tab test did not complete.");
}
[Fact]
public void MismatchWarning_IsShownBeforeTheAdd_AndCancelLeavesTheNestAlone()
{
+156 -1
View File
@@ -23,7 +23,20 @@ namespace OpenNest.Forms
{
components = new System.ComponentModel.Container();
System.Windows.Forms.DataGridViewCellStyle rightAligned = new System.Windows.Forms.DataGridViewCellStyle();
System.Windows.Forms.DataGridViewCellStyle editableQuantity = new System.Windows.Forms.DataGridViewCellStyle();
rootTable = new System.Windows.Forms.TableLayoutPanel();
tabControl = new System.Windows.Forms.TabControl();
drawingsTab = new System.Windows.Forms.TabPage();
nestTab = new System.Windows.Forms.TabPage();
nestTable = new System.Windows.Forms.TableLayoutPanel();
nestHintLabel = new System.Windows.Forms.Label();
nestGrid = new System.Windows.Forms.DataGridView();
nestNameColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
nestLibraryColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
nestRequiredColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
nestNestedColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
nestRemainingColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
nestSummaryLabel = new System.Windows.Forms.Label();
footerPanel = new System.Windows.Forms.FlowLayoutPanel();
layoutTable = new System.Windows.Forms.TableLayoutPanel();
filterTable = new System.Windows.Forms.TableLayoutPanel();
@@ -80,6 +93,11 @@ namespace OpenNest.Forms
releaseMenuItem = new System.Windows.Forms.ToolStripMenuItem();
releasesMenuItem = new System.Windows.Forms.ToolStripMenuItem();
rootTable.SuspendLayout();
tabControl.SuspendLayout();
drawingsTab.SuspendLayout();
nestTab.SuspendLayout();
nestTable.SuspendLayout();
((System.ComponentModel.ISupportInitialize)nestGrid).BeginInit();
footerPanel.SuspendLayout();
layoutTable.SuspendLayout();
filterTable.SuspendLayout();
@@ -103,7 +121,7 @@ namespace OpenNest.Forms
//
rootTable.ColumnCount = 1;
rootTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
rootTable.Controls.Add(layoutTable, 0, 0);
rootTable.Controls.Add(tabControl, 0, 0);
rootTable.Controls.Add(footerPanel, 0, 1);
rootTable.Dock = System.Windows.Forms.DockStyle.Fill;
rootTable.Location = new System.Drawing.Point(0, 0);
@@ -115,6 +133,124 @@ namespace OpenNest.Forms
rootTable.Size = new System.Drawing.Size(1184, 721);
rootTable.TabIndex = 0;
//
// tabControl
//
tabControl.Controls.Add(drawingsTab);
tabControl.Controls.Add(nestTab);
tabControl.Dock = System.Windows.Forms.DockStyle.Fill;
tabControl.Margin = new System.Windows.Forms.Padding(0);
tabControl.Name = "tabControl";
tabControl.SelectedIndex = 0;
tabControl.TabIndex = 0;
//
// drawingsTab
//
drawingsTab.Controls.Add(layoutTable);
drawingsTab.Name = "drawingsTab";
drawingsTab.Text = "Drawings";
drawingsTab.UseVisualStyleBackColor = true;
//
// nestTab
//
nestTab.Controls.Add(nestTable);
nestTab.Name = "nestTab";
nestTab.Text = "In This Nest";
nestTab.UseVisualStyleBackColor = true;
//
// nestTable
//
nestTable.ColumnCount = 1;
nestTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
nestTable.Controls.Add(nestHintLabel, 0, 0);
nestTable.Controls.Add(nestGrid, 0, 1);
nestTable.Controls.Add(nestSummaryLabel, 0, 2);
nestTable.Dock = System.Windows.Forms.DockStyle.Fill;
nestTable.Name = "nestTable";
nestTable.Padding = new System.Windows.Forms.Padding(8);
nestTable.RowCount = 3;
nestTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
nestTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
nestTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
nestTable.TabIndex = 0;
//
// nestHintLabel
//
nestHintLabel.AutoSize = true;
nestHintLabel.ForeColor = System.Drawing.SystemColors.GrayText;
nestHintLabel.Margin = new System.Windows.Forms.Padding(3, 0, 3, 6);
nestHintLabel.Name = "nestHintLabel";
nestHintLabel.TabIndex = 0;
nestHintLabel.Text = "The parts this nest needs. Edit Required to change a quantity; it applies to the nest right away.";
//
// nestGrid
//
nestGrid.AllowUserToAddRows = false;
nestGrid.AllowUserToDeleteRows = false;
nestGrid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { nestNameColumn, nestLibraryColumn, nestRequiredColumn, nestNestedColumn, nestRemainingColumn });
nestGrid.Dock = System.Windows.Forms.DockStyle.Fill;
nestGrid.EditMode = System.Windows.Forms.DataGridViewEditMode.EditOnKeystrokeOrF2;
nestGrid.Margin = new System.Windows.Forms.Padding(0);
nestGrid.MultiSelect = false;
nestGrid.Name = "nestGrid";
nestGrid.RowHeadersVisible = false;
nestGrid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.CellSelect;
nestGrid.StandardTab = true;
nestGrid.TabIndex = 1;
nestGrid.CellClick += NestGrid_CellClick;
nestGrid.CellValidating += NestGrid_CellValidating;
nestGrid.CellValueChanged += NestGrid_CellValueChanged;
//
// nestNameColumn
//
nestNameColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
nestNameColumn.HeaderText = "Name";
nestNameColumn.MinimumWidth = 160;
nestNameColumn.Name = "nestNameColumn";
nestNameColumn.ReadOnly = true;
//
// nestLibraryColumn
//
nestLibraryColumn.HeaderText = "Library";
nestLibraryColumn.Name = "nestLibraryColumn";
nestLibraryColumn.ReadOnly = true;
nestLibraryColumn.Width = 160;
//
// nestRequiredColumn
//
editableQuantity.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleRight;
editableQuantity.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Bold);
nestRequiredColumn.DefaultCellStyle = editableQuantity;
nestRequiredColumn.HeaderText = "Required";
nestRequiredColumn.Name = "nestRequiredColumn";
nestRequiredColumn.ValueType = typeof(int);
nestRequiredColumn.Width = 90;
//
// nestNestedColumn
//
nestNestedColumn.DefaultCellStyle = rightAligned;
nestNestedColumn.HeaderText = "Nested";
nestNestedColumn.Name = "nestNestedColumn";
nestNestedColumn.ReadOnly = true;
nestNestedColumn.Width = 80;
//
// nestRemainingColumn
//
nestRemainingColumn.DefaultCellStyle = rightAligned;
nestRemainingColumn.HeaderText = "Remaining";
nestRemainingColumn.Name = "nestRemainingColumn";
nestRemainingColumn.ReadOnly = true;
nestRemainingColumn.Width = 90;
//
// nestSummaryLabel
//
nestSummaryLabel.AutoEllipsis = true;
nestSummaryLabel.Dock = System.Windows.Forms.DockStyle.Fill;
nestSummaryLabel.ForeColor = System.Drawing.SystemColors.GrayText;
nestSummaryLabel.Margin = new System.Windows.Forms.Padding(3, 6, 3, 0);
nestSummaryLabel.Name = "nestSummaryLabel";
nestSummaryLabel.Size = new System.Drawing.Size(100, 20);
nestSummaryLabel.TabIndex = 2;
//
// footerPanel
//
footerPanel.AutoSize = true;
@@ -134,6 +270,7 @@ namespace OpenNest.Forms
layoutTable.Controls.Add(statusLabel, 0, 2);
layoutTable.Dock = System.Windows.Forms.DockStyle.Fill;
layoutTable.Margin = new System.Windows.Forms.Padding(0);
layoutTable.Padding = new System.Windows.Forms.Padding(8);
layoutTable.Name = "layoutTable";
layoutTable.RowCount = 3;
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
@@ -684,6 +821,12 @@ namespace OpenNest.Forms
Shown += DrawingLibraryForm_Shown;
rootTable.ResumeLayout(false);
rootTable.PerformLayout();
tabControl.ResumeLayout(false);
drawingsTab.ResumeLayout(false);
nestTab.ResumeLayout(false);
nestTable.ResumeLayout(false);
nestTable.PerformLayout();
((System.ComponentModel.ISupportInitialize)nestGrid).EndInit();
footerPanel.ResumeLayout(false);
footerPanel.PerformLayout();
layoutTable.ResumeLayout(false);
@@ -716,6 +859,18 @@ namespace OpenNest.Forms
#endregion
private System.Windows.Forms.TableLayoutPanel rootTable;
private System.Windows.Forms.TabControl tabControl;
private System.Windows.Forms.TabPage drawingsTab;
private System.Windows.Forms.TabPage nestTab;
private System.Windows.Forms.TableLayoutPanel nestTable;
private System.Windows.Forms.Label nestHintLabel;
private System.Windows.Forms.DataGridView nestGrid;
private System.Windows.Forms.DataGridViewTextBoxColumn nestNameColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn nestLibraryColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn nestRequiredColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn nestNestedColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn nestRemainingColumn;
private System.Windows.Forms.Label nestSummaryLabel;
private System.Windows.Forms.FlowLayoutPanel footerPanel;
private System.Windows.Forms.TableLayoutPanel layoutTable;
private System.Windows.Forms.TableLayoutPanel filterTable;
+134 -1
View File
@@ -25,13 +25,15 @@ namespace OpenNest.Forms
/// the releases that had it on its own and, once release is enabled, releases it; sets
/// themselves are browsed and released in <see cref="DrawingSetsForm"/>.
/// The details pane previews the selected revision's geometry, downloaded shortly after
/// the selection settles and cached per revision for the dialog's lifetime.
/// the selection settles and cached per revision for the dialog's lifetime. The In This
/// Nest tab lists the nest's drawings and edits their required quantities in place.
/// </summary>
public partial class DrawingLibraryForm : Form
{
private const string LoadingText = "Loading...";
private const int DetailsWidth = 380;
private const int PreviewCacheLimit = 64;
private const int MaxQuantity = 99999;
private readonly IDrawingLibraryStore library;
private readonly string serverUrl;
@@ -42,6 +44,7 @@ namespace OpenNest.Forms
private CancellationTokenSource previewCancel;
private (Guid Id, int Revision) previewKey;
private Task<Drawing> previewLoad;
private bool populatingNest;
private bool adding;
/// <summary>
@@ -80,6 +83,12 @@ namespace OpenNest.Forms
/// </summary>
public List<Drawing> AddedDrawings { get; } = new();
/// <summary>
/// True once the In This Nest tab changed a drawing's required quantity; like an add,
/// the change is already applied to the nest when the dialog closes.
/// </summary>
public bool QuantitiesChanged { get; private set; }
/// <param name="sets">Enables Used in Sets and Add to Set; null hides them.</param>
public DrawingLibraryForm(IDrawingLibraryStore library, string serverUrl, Nest nest, IDrawingSetStore sets = null)
{
@@ -122,6 +131,7 @@ namespace OpenNest.Forms
};
GridStyle.Apply(drawingGrid);
GridStyle.Apply(nestGrid);
nameColumn.Tag = DrawingSortField.Name;
customerColumn.Tag = DrawingSortField.Customer;
materialColumn.Tag = DrawingSortField.MaterialName;
@@ -139,6 +149,7 @@ namespace OpenNest.Forms
UpdateFilterLabel();
UpdateNavigation();
ShowDetails();
RefreshNestTab();
}
private async void DrawingLibraryForm_Shown(object sender, EventArgs e) =>
@@ -629,6 +640,7 @@ namespace OpenNest.Forms
insert.Apply();
if (!AddedDrawings.Contains(insert.Drawing))
AddedDrawings.Add(insert.Drawing);
RefreshNestTab();
statusLabel.Text = insert.Kind == LibraryInsertKind.AddQuantity
? $"Added {quantity} to '{insert.Drawing.Name}' (now {insert.Drawing.Quantity.Required})."
: $"Added '{insert.Drawing.Name}' revision {record.Revision}, quantity {quantity}.";
@@ -756,6 +768,127 @@ namespace OpenNest.Forms
private static string FormatThickness(double value) =>
value.ToString("0.####", CultureInfo.CurrentCulture);
/// <summary>
/// Lists the nest's drawings (cutoffs excluded) with their required, nested and
/// remaining quantities, and counts them on the tab.
/// </summary>
private void RefreshNestTab()
{
if (nestGrid.IsCurrentCellInEditMode)
nestGrid.CancelEdit();
populatingNest = true;
try
{
nestGrid.Rows.Clear();
foreach (var drawing in nest.Drawings.Where(drawing => !drawing.IsCutOff))
{
var index = nestGrid.Rows.Add(
drawing.Name,
LibraryText(drawing),
drawing.Quantity.Required,
drawing.Quantity.Nested,
drawing.Quantity.Remaining);
nestGrid.Rows[index].Tag = drawing;
}
}
finally
{
populatingNest = false;
}
nestTab.Text = $"In This Nest ({nestGrid.Rows.Count:N0})";
UpdateNestSummary();
}
// One click on a Required cell starts editing it; the other columns are read-only.
private void NestGrid_CellClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex >= 0 && e.ColumnIndex == nestRequiredColumn.Index)
nestGrid.BeginEdit(true);
}
private void NestGrid_CellValidating(object sender, DataGridViewCellValidatingEventArgs e)
{
if (e.RowIndex < 0 || e.ColumnIndex != nestRequiredColumn.Index)
return;
var valid = int.TryParse(Convert.ToString(e.FormattedValue, CultureInfo.CurrentCulture),
NumberStyles.Integer, CultureInfo.CurrentCulture, out var value)
&& value >= 0 && value <= MaxQuantity;
nestGrid.Rows[e.RowIndex].ErrorText = valid ? "" : $"Enter a whole number from 0 to {MaxQuantity:N0}.";
e.Cancel = !valid;
}
private void NestGrid_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
if (populatingNest || e.RowIndex < 0 || e.ColumnIndex != nestRequiredColumn.Index)
return;
var row = nestGrid.Rows[e.RowIndex];
if (row.Tag is not Drawing drawing)
return;
// CellValidating runs only when focus leaves the cell; a commit by EndEdit skips it.
if (row.Cells[e.ColumnIndex].Value is int value && value >= 0 && value <= MaxQuantity)
{
SetRequiredQuantity(row, drawing, value);
return;
}
populatingNest = true;
try
{
row.Cells[e.ColumnIndex].Value = drawing.Quantity.Required;
}
finally
{
populatingNest = false;
}
}
private void SetRequiredQuantity(DataGridViewRow row, Drawing drawing, int value)
{
if (drawing.Quantity.Required == value)
return;
var before = drawing.Quantity.Required;
drawing.Quantity.Required = value;
QuantitiesChanged = true;
populatingNest = true;
try
{
row.Cells[nestRemainingColumn.Index].Value = drawing.Quantity.Remaining;
}
finally
{
populatingNest = false;
}
statusLabel.Text = $"'{drawing.Name}' now requires {value:N0} (was {before:N0}).";
UpdateNestSummary();
}
private void UpdateNestSummary()
{
var drawings = nest.Drawings.Where(drawing => !drawing.IsCutOff).ToList();
nestSummaryLabel.Text = drawings.Count == 0
? "This nest has no drawings yet. Add them from the Drawings tab."
: $"{drawings.Count:N0} drawing{(drawings.Count == 1 ? "" : "s")}: "
+ $"{drawings.Sum(drawing => drawing.Quantity.Required):N0} parts required, "
+ $"{drawings.Sum(drawing => drawing.Quantity.Nested):N0} nested, "
+ $"{drawings.Sum(drawing => drawing.Quantity.Remaining):N0} remaining.";
}
private string LibraryText(Drawing drawing)
{
if (drawing.Library == null)
return "Not from the library";
return string.Equals(drawing.Library.ServerKey, serverUrl, StringComparison.OrdinalIgnoreCase)
? $"Revision {drawing.Library.Revision}"
: $"Revision {drawing.Library.Revision} (other server)";
}
private static string FormatRevision(DrawingLibraryRecord record)
{
var text = record.Revision.ToString(CultureInfo.CurrentCulture);
+1 -1
View File
@@ -1289,7 +1289,7 @@ namespace OpenNest.Forms
if (MdiParent is MainForm main)
form.EnableRelease(NestStorage.CreateRepository(), main.OpenServerNest, main.IsServerNestOpen);
form.ShowDialog(this);
if (form.AddedDrawings.Count > 0)
if (form.AddedDrawings.Count > 0 || form.QuantitiesChanged)
UpdateDrawingList();
}
+8
View File
@@ -302,6 +302,14 @@ id, revision and a snapshot of the library details) that is saved with the nest
in the nest never changes the library. The copy takes its name, customer,
material and nesting settings from the library record, not from the package.
The **In This Nest** tab (its title counts them) lists the nest's drawings,
library or not, cutoffs excluded: name, library revision ("Not from the library"
or "(other server)" when it is linked elsewhere), and the Required, Nested and
Remaining quantities, with totals below. It refreshes after every add. Required
is edited in place (0 to 99,999; one click starts editing, a bad number keeps
the editor open with an error) and applies to the nest at once, like an add;
the main window's drawing list refreshes when the dialog closes.
The browser's **Cut first** column shows the setting. **Edit Settings...**
(`OpenNest.Data.DrawingLibraryEditSession`) edits the selected active drawing's
Cut first flag, priority, rotation step/start/end (degrees), 180° equivalent,