feat(sets): browse the library inside the set editor

Adding drawings to a set meant opening the library dialog, then the set
editor, then a second copy of the library in pick mode, then backing out
again. The set editor now has the library as its left pane (unfiltered,
searchable, sortable, paged): select drawings and Add to Set, double-click
or press Enter. Members already in the set are greyed. The library
dialog's pick mode had no other caller and is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-21 08:41:53 -04:00
1 parent 382abf702b
commit 19809c61d0
5 files changed
+547 -148

No files matched your search

@@ -1,3 +1,4 @@
using System.Drawing;
using System.Reflection;
using System.Windows.Forms;
using OpenNest.Data;
@@ -31,14 +32,17 @@ public class DrawingSetFormsTests
{
using var editor = new EditDrawingSetForm(edit, library, Server);
editor.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
editor.PickDrawings = () => (new[] { bracket, gusset }, 3);
editor.Show();
var save = Field<Button>(editor, "saveButton");
var libraryGrid = Field<DataGridView>(editor, "libraryGrid");
PumpUntil(() => libraryGrid.Rows.Count == 2, "the editor's library pane");
Assert.False(save.Enabled);
Field<TextBox>(editor, "nameBox").Text = "Synthetic kit";
Field<TextBox>(editor, "customerBox").Text = "Synthetic customer";
editor.AddDrawings();
libraryGrid.SelectAll();
Field<NumericUpDown>(editor, "quantityBox").Value = 3;
editor.AddSelectedDrawings();
Assert.True(save.Enabled);
Run(editor.SaveAsync());
return editor.Saved;
@@ -67,36 +71,58 @@ public class DrawingSetFormsTests
}
[Fact]
public void PickMode_HasNoSetsTab_AndReturnsEverySelectedDrawingInListOrder()
public void EditorLibraryPane_ListsEveryDrawing_AddsTheSelectionInListOrder_AndGreysMembers()
{
StaTestThread.Run(
() =>
{
var library = new FakeLibrary();
var a = library.Add("SYN-A", "Synthetic Steel", 0.25);
library.Add("SYN-B", "Synthetic Steel", 0.25);
var b = library.Add("SYN-B", "Synthetic Aluminum", 0.125);
var c = library.Add("SYN-C", "Synthetic Steel", 0.25);
var sets = new InMemoryDrawingSets();
var created = Run(sets.CreateAsync(new DrawingSetRecord
{
Id = Guid.NewGuid(),
Name = "Synthetic kit",
Members = new[] { new DrawingSetMember { DrawingId = b.Id, Quantity = 2, Drawing = b } },
}));
using var form = new DrawingLibraryForm(library, Server);
form.Show();
var grid = Field<DataGridView>(form, "drawingGrid");
PumpUntil(() => grid.Rows.Count == 3, "the unfiltered library page");
using var editor = new EditDrawingSetForm(new DrawingSetEditSession(sets, created, "Synthetic operator"), library, Server);
editor.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
editor.Show();
var libraryGrid = Field<DataGridView>(editor, "libraryGrid");
var memberGrid = Field<DataGridView>(editor, "memberGrid");
var add = Field<Button>(editor, "addButton");
PumpUntil(() => libraryGrid.Rows.Count == 3, "the unfiltered library page");
Assert.True(form.IsPickMode);
Assert.Single(Field<TabControl>(form, "tabControl").TabPages);
Assert.Equal(("", "", (double?)null), (library.Queries[0].MaterialName, library.Queries[0].MaterialGrade, library.Queries[0].Thickness));
grid.ClearSelection();
grid.Rows[2].Selected = true;
grid.Rows[0].Selected = true;
Field<NumericUpDown>(form, "quantityBox").Value = 4;
Run(form.AddSelectedAsync());
Assert.Equal(SystemColors.GrayText, libraryGrid.Rows[1].DefaultCellStyle.ForeColor);
Assert.Equal(Color.Empty, libraryGrid.Rows[0].DefaultCellStyle.ForeColor);
Assert.Equal(DialogResult.OK, form.DialogResult);
Assert.Equal(new[] { a, c }, form.PickedDrawings);
Assert.Equal(4, form.PickedQuantity);
libraryGrid.ClearSelection();
libraryGrid.Rows[2].Selected = true;
libraryGrid.Rows[1].Selected = true;
libraryGrid.Rows[0].Selected = true;
Assert.True(add.Enabled);
Field<NumericUpDown>(editor, "quantityBox").Value = 4;
editor.AddSelectedDrawings();
Assert.Equal(new[] { ("SYN-B", 2), ("SYN-A", 4), ("SYN-C", 4) },
memberGrid.Rows.Cast<DataGridViewRow>().Select(r => ((string)r.Cells["drawingColumn"].Value, (int)r.Cells["quantityColumn"].Value)));
Assert.Contains("Already in the set: 'SYN-B'", Field<Label>(editor, "statusLabel").Text);
Assert.All(libraryGrid.Rows.Cast<DataGridViewRow>(), r => Assert.Equal(SystemColors.GrayText, r.DefaultCellStyle.ForeColor));
memberGrid.CurrentCell = memberGrid.Rows[1].Cells["drawingColumn"];
Field<Button>(editor, "removeButton").PerformClick();
Assert.Equal(Color.Empty, libraryGrid.Rows[0].DefaultCellStyle.ForeColor);
Run(editor.SaveAsync());
var stored = sets.Current.Single();
Assert.Equal(new[] { (b.Id, 2), (c.Id, 4) }, stored.Members.Select(m => (m.DrawingId, m.Quantity)));
},
TestTimeout,
"The pick mode test did not complete.");
"The editor library pane test did not complete.");
}
[Fact]
+4 -70
View File
@@ -22,11 +22,6 @@ namespace OpenNest.Forms
/// rotation, color) on the server as a new revision with the same package. With a set
/// store, the Sets tab browses and edits drawing sets (<see cref="Controls.DrawingSetBrowser"/>),
/// and the drawing grid's menu lists the sets a drawing is in and adds it to a set.
/// <para>
/// Opened without a nest it picks drawings for a drawing set instead: every active
/// drawing is listed, several may be selected, and Add to Set closes the dialog with
/// <see cref="PickedDrawings"/>.
/// </para>
/// </summary>
public partial class DrawingLibraryForm : Form
{
@@ -54,15 +49,6 @@ namespace OpenNest.Forms
/// <summary>Shows the set editor and returns the saved record, or null; replaced by tests.</summary>
internal Func<DrawingSetEditSession, DrawingSetRecord> EditSet { get; set; }
/// <summary>In pick mode, the drawings chosen, in list order, once the dialog closes with OK.</summary>
public IReadOnlyList<DrawingLibraryRecord> PickedDrawings { get; private set; } = Array.Empty<DrawingLibraryRecord>();
/// <summary>In pick mode, the quantity entered for the picked drawings.</summary>
public int PickedQuantity { get; private set; }
/// <summary>True when the dialog picks drawings for a set rather than adding them to a nest.</summary>
public bool IsPickMode => nest == null;
/// <summary>
/// The library drawings this dialog added or raised the quantity of, in order. The
/// dialog stays open after each add, so callers check this rather than the dialog result.
@@ -71,25 +57,13 @@ namespace OpenNest.Forms
/// <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)
: this(library, serverUrl, nest ?? throw new ArgumentNullException(nameof(nest)), sets, pick: false)
{
}
/// <summary>Picks drawings for a drawing set (see <see cref="PickedDrawings"/>).</summary>
public DrawingLibraryForm(IDrawingLibraryStore library, string serverUrl)
: this(library, serverUrl, null, null, pick: true)
{
}
private DrawingLibraryForm(IDrawingLibraryStore library, string serverUrl, Nest nest, IDrawingSetStore sets, bool pick)
{
InitializeComponent();
this.library = library ?? throw new ArgumentNullException(nameof(library));
this.serverUrl = serverUrl ?? "";
this.nest = nest;
this.nest = nest ?? throw new ArgumentNullException(nameof(nest));
this.sets = sets;
// Without a nest there is no material or thickness to filter by.
session = new DrawingLibraryBrowseSession(library, nest ?? new Nest());
session = new DrawingLibraryBrowseSession(library, nest);
ShowMessage = (text, buttons, icon) => MessageBox.Show(this, text, Text, buttons, icon);
EditDrawing = edit =>
{
@@ -117,18 +91,9 @@ namespace OpenNest.Forms
revisionColumn.Tag = DrawingSortField.Revision;
savedColumn.Tag = DrawingSortField.SavedAt;
if (pick)
{
Text = $"Add Drawings to Set — {this.serverUrl}";
addButton.Text = "Add to Set";
drawingGrid.MultiSelect = true;
}
else
{
Text = $"Add from Drawing Library — {this.serverUrl}";
}
Text = $"Add from Drawing Library — {this.serverUrl}";
if (sets != null && !pick)
if (sets != null)
{
drawingGrid.ContextMenuStrip = drawingMenu;
setBrowser.Initialize(sets, library, this.serverUrl);
@@ -376,12 +341,6 @@ namespace OpenNest.Forms
/// </summary>
internal async Task AddSelectedAsync()
{
if (IsPickMode)
{
PickSelected();
return;
}
var record = SelectedRecord();
if (record == null || adding)
return;
@@ -450,25 +409,6 @@ namespace OpenNest.Forms
}
}
// Pick mode: closes with every selected drawing, in list order.
private void PickSelected()
{
var picked = drawingGrid.SelectedRows.Cast<DataGridViewRow>()
.OrderBy(row => row.Index)
.Select(row => row.Tag as DrawingLibraryRecord)
.Where(record => record != null)
.ToList();
if (picked.Count == 0 && SelectedRecord() is { } current)
picked.Add(current);
if (picked.Count == 0)
return;
PickedDrawings = picked;
PickedQuantity = (int)quantityBox.Value;
DialogResult = DialogResult.OK;
Close();
}
private void Fail(string message)
{
if (IsDisposed)
@@ -555,12 +495,6 @@ namespace OpenNest.Forms
private void UpdateFilterLabel(bool showAll = false)
{
if (IsPickMode)
{
filterLabel.Text = "Showing every drawing. Select several with Ctrl or Shift.";
return;
}
if (!session.IsFilteredToNest && !showAll)
{
filterLabel.Text = "This nest has no material or thickness; showing every drawing.";
+314 -32
View File
@@ -17,8 +17,10 @@ namespace OpenNest.Forms
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
System.Windows.Forms.DataGridViewCellStyle rightAligned = new System.Windows.Forms.DataGridViewCellStyle();
System.Windows.Forms.DataGridViewCellStyle quantityStyle = new System.Windows.Forms.DataGridViewCellStyle();
System.Windows.Forms.DataGridViewCellStyle libraryRightAligned = new System.Windows.Forms.DataGridViewCellStyle();
layoutTable = new System.Windows.Forms.TableLayoutPanel();
detailsTable = new System.Windows.Forms.TableLayoutPanel();
nameLabel = new System.Windows.Forms.Label();
@@ -27,6 +29,25 @@ namespace OpenNest.Forms
customerBox = new System.Windows.Forms.TextBox();
notesLabel = new System.Windows.Forms.Label();
notesBox = new System.Windows.Forms.TextBox();
splitContainer = new System.Windows.Forms.SplitContainer();
libraryGroup = new System.Windows.Forms.GroupBox();
libraryTable = new System.Windows.Forms.TableLayoutPanel();
searchBox = new System.Windows.Forms.TextBox();
libraryGrid = new System.Windows.Forms.DataGridView();
libraryNameColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
libraryMaterialColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
libraryGradeColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
libraryThicknessColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
libraryCustomerColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
libraryNavTable = new System.Windows.Forms.TableLayoutPanel();
previousButton = new System.Windows.Forms.Button();
nextButton = new System.Windows.Forms.Button();
librarySummaryLabel = new System.Windows.Forms.Label();
addTable = new System.Windows.Forms.TableLayoutPanel();
quantityLabel = new System.Windows.Forms.Label();
quantityBox = new System.Windows.Forms.NumericUpDown();
addButton = new System.Windows.Forms.Button();
memberGroup = new System.Windows.Forms.GroupBox();
memberTable = new System.Windows.Forms.TableLayoutPanel();
memberGrid = new System.Windows.Forms.DataGridView();
drawingColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
@@ -38,7 +59,6 @@ namespace OpenNest.Forms
quantityColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
warningColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
memberButtons = new System.Windows.Forms.FlowLayoutPanel();
addButton = new System.Windows.Forms.Button();
removeButton = new System.Windows.Forms.Button();
upButton = new System.Windows.Forms.Button();
downButton = new System.Windows.Forms.Button();
@@ -47,8 +67,20 @@ namespace OpenNest.Forms
saveButton = new System.Windows.Forms.Button();
cancelButton = new System.Windows.Forms.Button();
statusLabel = new System.Windows.Forms.Label();
searchTimer = new System.Windows.Forms.Timer(components);
layoutTable.SuspendLayout();
detailsTable.SuspendLayout();
((System.ComponentModel.ISupportInitialize)splitContainer).BeginInit();
splitContainer.Panel1.SuspendLayout();
splitContainer.Panel2.SuspendLayout();
splitContainer.SuspendLayout();
libraryGroup.SuspendLayout();
libraryTable.SuspendLayout();
((System.ComponentModel.ISupportInitialize)libraryGrid).BeginInit();
libraryNavTable.SuspendLayout();
addTable.SuspendLayout();
((System.ComponentModel.ISupportInitialize)quantityBox).BeginInit();
memberGroup.SuspendLayout();
memberTable.SuspendLayout();
((System.ComponentModel.ISupportInitialize)memberGrid).BeginInit();
memberButtons.SuspendLayout();
@@ -60,7 +92,7 @@ namespace OpenNest.Forms
layoutTable.ColumnCount = 1;
layoutTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
layoutTable.Controls.Add(detailsTable, 0, 0);
layoutTable.Controls.Add(memberTable, 0, 1);
layoutTable.Controls.Add(splitContainer, 0, 1);
layoutTable.Controls.Add(bottomTable, 0, 2);
layoutTable.Controls.Add(statusLabel, 0, 3);
layoutTable.Dock = System.Windows.Forms.DockStyle.Fill;
@@ -72,7 +104,7 @@ namespace OpenNest.Forms
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
layoutTable.Size = new System.Drawing.Size(904, 561);
layoutTable.Size = new System.Drawing.Size(1264, 641);
layoutTable.TabIndex = 0;
//
// detailsTable
@@ -95,7 +127,7 @@ namespace OpenNest.Forms
detailsTable.Name = "detailsTable";
detailsTable.RowCount = 2;
detailsTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
detailsTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 60F));
detailsTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 48F));
detailsTable.TabIndex = 0;
//
// nameLabel
@@ -149,6 +181,224 @@ namespace OpenNest.Forms
notesBox.TabIndex = 5;
notesBox.TextChanged += Details_TextChanged;
//
// splitContainer
//
splitContainer.Dock = System.Windows.Forms.DockStyle.Fill;
splitContainer.Margin = new System.Windows.Forms.Padding(0);
splitContainer.Name = "splitContainer";
splitContainer.Panel1.Controls.Add(libraryGroup);
splitContainer.Panel2.Controls.Add(memberGroup);
splitContainer.Size = new System.Drawing.Size(1248, 470);
splitContainer.SplitterDistance = 450;
splitContainer.SplitterWidth = 6;
splitContainer.Panel1MinSize = 320;
splitContainer.Panel2MinSize = 420;
splitContainer.TabIndex = 1;
//
// libraryGroup
//
libraryGroup.Controls.Add(libraryTable);
libraryGroup.Dock = System.Windows.Forms.DockStyle.Fill;
libraryGroup.Name = "libraryGroup";
libraryGroup.Padding = new System.Windows.Forms.Padding(6);
libraryGroup.TabIndex = 0;
libraryGroup.TabStop = false;
libraryGroup.Text = "Drawing library";
//
// libraryTable
//
libraryTable.ColumnCount = 1;
libraryTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
libraryTable.Controls.Add(searchBox, 0, 0);
libraryTable.Controls.Add(libraryGrid, 0, 1);
libraryTable.Controls.Add(libraryNavTable, 0, 2);
libraryTable.Controls.Add(addTable, 0, 3);
libraryTable.Dock = System.Windows.Forms.DockStyle.Fill;
libraryTable.Margin = new System.Windows.Forms.Padding(0);
libraryTable.Name = "libraryTable";
libraryTable.RowCount = 4;
libraryTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
libraryTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
libraryTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
libraryTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
libraryTable.TabIndex = 0;
//
// searchBox
//
searchBox.Dock = System.Windows.Forms.DockStyle.Fill;
searchBox.Margin = new System.Windows.Forms.Padding(0, 0, 0, 6);
searchBox.Name = "searchBox";
searchBox.PlaceholderText = "Search by name, customer or notes";
searchBox.TabIndex = 0;
searchBox.TextChanged += SearchBox_TextChanged;
//
// libraryGrid
//
libraryGrid.AllowUserToAddRows = false;
libraryGrid.AllowUserToDeleteRows = false;
libraryGrid.AllowUserToOrderColumns = false;
libraryGrid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { libraryNameColumn, libraryMaterialColumn, libraryGradeColumn, libraryThicknessColumn, libraryCustomerColumn });
libraryGrid.Dock = System.Windows.Forms.DockStyle.Fill;
libraryGrid.Margin = new System.Windows.Forms.Padding(0);
libraryGrid.MultiSelect = true;
libraryGrid.Name = "libraryGrid";
libraryGrid.ReadOnly = true;
libraryGrid.RowHeadersVisible = false;
libraryGrid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
libraryGrid.StandardTab = true;
libraryGrid.TabIndex = 1;
libraryGrid.CellDoubleClick += LibraryGrid_CellDoubleClick;
libraryGrid.ColumnHeaderMouseClick += LibraryGrid_ColumnHeaderMouseClick;
libraryGrid.SelectionChanged += LibraryGrid_SelectionChanged;
//
// libraryNameColumn
//
libraryNameColumn.HeaderText = "Drawing";
libraryNameColumn.Name = "libraryNameColumn";
libraryNameColumn.ReadOnly = true;
libraryNameColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic;
libraryNameColumn.Width = 140;
//
// libraryMaterialColumn
//
libraryMaterialColumn.HeaderText = "Material";
libraryMaterialColumn.Name = "libraryMaterialColumn";
libraryMaterialColumn.ReadOnly = true;
libraryMaterialColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic;
libraryMaterialColumn.Width = 100;
//
// libraryGradeColumn
//
libraryGradeColumn.HeaderText = "Grade";
libraryGradeColumn.Name = "libraryGradeColumn";
libraryGradeColumn.ReadOnly = true;
libraryGradeColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic;
libraryGradeColumn.Width = 60;
//
// libraryThicknessColumn
//
libraryRightAligned.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleRight;
libraryThicknessColumn.DefaultCellStyle = libraryRightAligned;
libraryThicknessColumn.HeaderText = "Thickness";
libraryThicknessColumn.Name = "libraryThicknessColumn";
libraryThicknessColumn.ReadOnly = true;
libraryThicknessColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic;
libraryThicknessColumn.Width = 70;
//
// libraryCustomerColumn
//
libraryCustomerColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
libraryCustomerColumn.HeaderText = "Customer";
libraryCustomerColumn.MinimumWidth = 60;
libraryCustomerColumn.Name = "libraryCustomerColumn";
libraryCustomerColumn.ReadOnly = true;
libraryCustomerColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Programmatic;
//
// libraryNavTable
//
libraryNavTable.AutoSize = true;
libraryNavTable.ColumnCount = 3;
libraryNavTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
libraryNavTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
libraryNavTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
libraryNavTable.Controls.Add(previousButton, 0, 0);
libraryNavTable.Controls.Add(nextButton, 1, 0);
libraryNavTable.Controls.Add(librarySummaryLabel, 2, 0);
libraryNavTable.Dock = System.Windows.Forms.DockStyle.Fill;
libraryNavTable.Margin = new System.Windows.Forms.Padding(0, 4, 0, 0);
libraryNavTable.Name = "libraryNavTable";
libraryNavTable.RowCount = 1;
libraryNavTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
libraryNavTable.TabIndex = 2;
//
// previousButton
//
previousButton.Enabled = false;
previousButton.Margin = new System.Windows.Forms.Padding(0, 0, 3, 0);
previousButton.Name = "previousButton";
previousButton.Size = new System.Drawing.Size(34, 26);
previousButton.TabIndex = 0;
previousButton.Text = "<";
previousButton.UseVisualStyleBackColor = true;
previousButton.Click += PreviousButton_Click;
//
// nextButton
//
nextButton.Enabled = false;
nextButton.Margin = new System.Windows.Forms.Padding(0, 0, 6, 0);
nextButton.Name = "nextButton";
nextButton.Size = new System.Drawing.Size(34, 26);
nextButton.TabIndex = 1;
nextButton.Text = ">";
nextButton.UseVisualStyleBackColor = true;
nextButton.Click += NextButton_Click;
//
// librarySummaryLabel
//
librarySummaryLabel.Anchor = System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
librarySummaryLabel.AutoEllipsis = true;
librarySummaryLabel.ForeColor = System.Drawing.SystemColors.GrayText;
librarySummaryLabel.Name = "librarySummaryLabel";
librarySummaryLabel.TabIndex = 2;
//
// addTable
//
addTable.AutoSize = true;
addTable.ColumnCount = 4;
addTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
addTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
addTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
addTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle());
addTable.Controls.Add(quantityLabel, 1, 0);
addTable.Controls.Add(quantityBox, 2, 0);
addTable.Controls.Add(addButton, 3, 0);
addTable.Dock = System.Windows.Forms.DockStyle.Fill;
addTable.Margin = new System.Windows.Forms.Padding(0, 6, 0, 0);
addTable.Name = "addTable";
addTable.RowCount = 1;
addTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
addTable.TabIndex = 3;
//
// quantityLabel
//
quantityLabel.Anchor = System.Windows.Forms.AnchorStyles.Right;
quantityLabel.AutoSize = true;
quantityLabel.Name = "quantityLabel";
quantityLabel.TabIndex = 0;
quantityLabel.Text = "Qty per kit:";
//
// quantityBox
//
quantityBox.Anchor = System.Windows.Forms.AnchorStyles.Left;
quantityBox.Maximum = new decimal(new int[] { 100000, 0, 0, 0 });
quantityBox.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
quantityBox.Name = "quantityBox";
quantityBox.Size = new System.Drawing.Size(70, 25);
quantityBox.TabIndex = 1;
quantityBox.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
quantityBox.Value = new decimal(new int[] { 1, 0, 0, 0 });
//
// addButton
//
addButton.Enabled = false;
addButton.Margin = new System.Windows.Forms.Padding(6, 0, 0, 0);
addButton.Name = "addButton";
addButton.Size = new System.Drawing.Size(120, 28);
addButton.TabIndex = 2;
addButton.Text = "Add to Set →";
addButton.UseVisualStyleBackColor = true;
addButton.Click += AddButton_Click;
//
// memberGroup
//
memberGroup.Controls.Add(memberTable);
memberGroup.Dock = System.Windows.Forms.DockStyle.Fill;
memberGroup.Name = "memberGroup";
memberGroup.Padding = new System.Windows.Forms.Padding(6);
memberGroup.TabIndex = 0;
memberGroup.TabStop = false;
memberGroup.Text = "In this set";
//
// memberTable
//
memberTable.ColumnCount = 2;
@@ -161,7 +411,7 @@ namespace OpenNest.Forms
memberTable.Name = "memberTable";
memberTable.RowCount = 1;
memberTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
memberTable.TabIndex = 1;
memberTable.TabIndex = 0;
//
// memberGrid
//
@@ -171,6 +421,7 @@ namespace OpenNest.Forms
memberGrid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { drawingColumn, materialColumn, gradeColumn, thicknessColumn, unitsColumn, cutTypeColumn, quantityColumn, warningColumn });
memberGrid.Dock = System.Windows.Forms.DockStyle.Fill;
memberGrid.EditMode = System.Windows.Forms.DataGridViewEditMode.EditOnKeystrokeOrF2;
memberGrid.Margin = new System.Windows.Forms.Padding(0);
memberGrid.MultiSelect = false;
memberGrid.Name = "memberGrid";
memberGrid.RowHeadersVisible = false;
@@ -188,7 +439,7 @@ namespace OpenNest.Forms
drawingColumn.Name = "drawingColumn";
drawingColumn.ReadOnly = true;
drawingColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
drawingColumn.Width = 200;
drawingColumn.Width = 150;
//
// materialColumn
//
@@ -196,7 +447,7 @@ namespace OpenNest.Forms
materialColumn.Name = "materialColumn";
materialColumn.ReadOnly = true;
materialColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
materialColumn.Width = 140;
materialColumn.Width = 100;
//
// gradeColumn
//
@@ -204,7 +455,7 @@ namespace OpenNest.Forms
gradeColumn.Name = "gradeColumn";
gradeColumn.ReadOnly = true;
gradeColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
gradeColumn.Width = 90;
gradeColumn.Width = 60;
//
// thicknessColumn
//
@@ -214,7 +465,7 @@ namespace OpenNest.Forms
thicknessColumn.Name = "thicknessColumn";
thicknessColumn.ReadOnly = true;
thicknessColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
thicknessColumn.Width = 80;
thicknessColumn.Width = 70;
//
// unitsColumn
//
@@ -222,7 +473,7 @@ namespace OpenNest.Forms
unitsColumn.Name = "unitsColumn";
unitsColumn.ReadOnly = true;
unitsColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
unitsColumn.Width = 60;
unitsColumn.Width = 50;
//
// cutTypeColumn
//
@@ -230,7 +481,7 @@ namespace OpenNest.Forms
cutTypeColumn.Name = "cutTypeColumn";
cutTypeColumn.ReadOnly = true;
cutTypeColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
cutTypeColumn.Width = 75;
cutTypeColumn.Width = 65;
//
// quantityColumn
//
@@ -242,12 +493,13 @@ namespace OpenNest.Forms
quantityColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
quantityColumn.ToolTipText = "Quantity per kit; type to change it";
quantityColumn.ValueType = typeof(int);
quantityColumn.Width = 60;
quantityColumn.Width = 50;
//
// warningColumn
//
warningColumn.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
warningColumn.HeaderText = "Note";
warningColumn.MinimumWidth = 60;
warningColumn.Name = "warningColumn";
warningColumn.ReadOnly = true;
warningColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
@@ -255,7 +507,6 @@ namespace OpenNest.Forms
// memberButtons
//
memberButtons.AutoSize = true;
memberButtons.Controls.Add(addButton);
memberButtons.Controls.Add(removeButton);
memberButtons.Controls.Add(upButton);
memberButtons.Controls.Add(downButton);
@@ -266,21 +517,13 @@ namespace OpenNest.Forms
memberButtons.TabIndex = 1;
memberButtons.WrapContents = false;
//
// addButton
//
addButton.Name = "addButton";
addButton.Size = new System.Drawing.Size(130, 28);
addButton.TabIndex = 0;
addButton.Text = "Add Drawings...";
addButton.UseVisualStyleBackColor = true;
addButton.Click += AddButton_Click;
//
// removeButton
//
removeButton.Enabled = false;
removeButton.Margin = new System.Windows.Forms.Padding(0, 0, 0, 3);
removeButton.Name = "removeButton";
removeButton.Size = new System.Drawing.Size(130, 28);
removeButton.TabIndex = 1;
removeButton.Size = new System.Drawing.Size(100, 28);
removeButton.TabIndex = 0;
removeButton.Text = "Remove";
removeButton.UseVisualStyleBackColor = true;
removeButton.Click += RemoveButton_Click;
@@ -288,10 +531,10 @@ namespace OpenNest.Forms
// upButton
//
upButton.Enabled = false;
upButton.Margin = new System.Windows.Forms.Padding(3, 12, 3, 3);
upButton.Margin = new System.Windows.Forms.Padding(0, 12, 0, 3);
upButton.Name = "upButton";
upButton.Size = new System.Drawing.Size(130, 28);
upButton.TabIndex = 2;
upButton.Size = new System.Drawing.Size(100, 28);
upButton.TabIndex = 1;
upButton.Text = "Move Up";
upButton.UseVisualStyleBackColor = true;
upButton.Click += UpButton_Click;
@@ -299,9 +542,10 @@ namespace OpenNest.Forms
// downButton
//
downButton.Enabled = false;
downButton.Margin = new System.Windows.Forms.Padding(0, 0, 0, 3);
downButton.Name = "downButton";
downButton.Size = new System.Drawing.Size(130, 28);
downButton.TabIndex = 3;
downButton.Size = new System.Drawing.Size(100, 28);
downButton.TabIndex = 2;
downButton.Text = "Move Down";
downButton.UseVisualStyleBackColor = true;
downButton.Click += DownButton_Click;
@@ -358,25 +602,44 @@ namespace OpenNest.Forms
statusLabel.Size = new System.Drawing.Size(100, 20);
statusLabel.TabIndex = 3;
//
// searchTimer
//
searchTimer.Interval = 300;
searchTimer.Tick += SearchTimer_Tick;
//
// EditDrawingSetForm
//
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
CancelButton = cancelButton;
ClientSize = new System.Drawing.Size(904, 561);
ClientSize = new System.Drawing.Size(1264, 641);
Controls.Add(layoutTable);
Font = new System.Drawing.Font("Segoe UI", 9.75F);
MinimizeBox = false;
MinimumSize = new System.Drawing.Size(720, 420);
MinimumSize = new System.Drawing.Size(1000, 480);
Name = "EditDrawingSetForm";
ShowIcon = false;
ShowInTaskbar = false;
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "Drawing Set";
FormClosing += EditDrawingSetForm_FormClosing;
Shown += EditDrawingSetForm_Shown;
layoutTable.ResumeLayout(false);
layoutTable.PerformLayout();
detailsTable.ResumeLayout(false);
detailsTable.PerformLayout();
splitContainer.Panel1.ResumeLayout(false);
splitContainer.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)splitContainer).EndInit();
splitContainer.ResumeLayout(false);
libraryGroup.ResumeLayout(false);
libraryTable.ResumeLayout(false);
libraryTable.PerformLayout();
((System.ComponentModel.ISupportInitialize)libraryGrid).EndInit();
libraryNavTable.ResumeLayout(false);
addTable.ResumeLayout(false);
addTable.PerformLayout();
((System.ComponentModel.ISupportInitialize)quantityBox).EndInit();
memberGroup.ResumeLayout(false);
memberTable.ResumeLayout(false);
memberTable.PerformLayout();
((System.ComponentModel.ISupportInitialize)memberGrid).EndInit();
@@ -396,6 +659,25 @@ namespace OpenNest.Forms
private System.Windows.Forms.TextBox customerBox;
private System.Windows.Forms.Label notesLabel;
private System.Windows.Forms.TextBox notesBox;
private System.Windows.Forms.SplitContainer splitContainer;
private System.Windows.Forms.GroupBox libraryGroup;
private System.Windows.Forms.TableLayoutPanel libraryTable;
private System.Windows.Forms.TextBox searchBox;
private System.Windows.Forms.DataGridView libraryGrid;
private System.Windows.Forms.DataGridViewTextBoxColumn libraryNameColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn libraryMaterialColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn libraryGradeColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn libraryThicknessColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn libraryCustomerColumn;
private System.Windows.Forms.TableLayoutPanel libraryNavTable;
private System.Windows.Forms.Button previousButton;
private System.Windows.Forms.Button nextButton;
private System.Windows.Forms.Label librarySummaryLabel;
private System.Windows.Forms.TableLayoutPanel addTable;
private System.Windows.Forms.Label quantityLabel;
private System.Windows.Forms.NumericUpDown quantityBox;
private System.Windows.Forms.Button addButton;
private System.Windows.Forms.GroupBox memberGroup;
private System.Windows.Forms.TableLayoutPanel memberTable;
private System.Windows.Forms.DataGridView memberGrid;
private System.Windows.Forms.DataGridViewTextBoxColumn drawingColumn;
@@ -407,7 +689,6 @@ namespace OpenNest.Forms
private System.Windows.Forms.DataGridViewTextBoxColumn quantityColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn warningColumn;
private System.Windows.Forms.FlowLayoutPanel memberButtons;
private System.Windows.Forms.Button addButton;
private System.Windows.Forms.Button removeButton;
private System.Windows.Forms.Button upButton;
private System.Windows.Forms.Button downButton;
@@ -416,5 +697,6 @@ namespace OpenNest.Forms
private System.Windows.Forms.Button saveButton;
private System.Windows.Forms.Button cancelButton;
private System.Windows.Forms.Label statusLabel;
private System.Windows.Forms.Timer searchTimer;
}
}
+178 -24
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -13,22 +14,25 @@ namespace OpenNest.Forms
{
/// <summary>
/// Creates or edits one drawing set through <see cref="DrawingSetEditSession"/>: name,
/// customer, notes and the ordered members with a quantity each. Add Drawings picks from
/// the drawing library; a drawing already in the set is not added twice. Saving adds a
/// revision on the server, guarded by the revision the set was read at.
/// customer, notes and the ordered members with a quantity each. The drawing library is
/// browsed in the left pane (every active drawing, a page at a time through
/// <see cref="DrawingLibraryBrowseSession"/>); Add to Set, a double-click or Enter adds the
/// selected drawings at the entered quantity, and a drawing already in the set is greyed
/// and not added twice. Saving adds a revision on the server, guarded by the revision the
/// set was read at.
/// </summary>
public partial class EditDrawingSetForm : Form
{
private const string LoadingText = "Loading...";
private readonly DrawingSetEditSession session;
private readonly DrawingLibraryBrowseSession librarySession;
private bool loading;
private bool saving;
/// <summary>Shows a message and returns the operator's answer; replaced by tests.</summary>
internal Func<string, MessageBoxButtons, MessageBoxIcon, DialogResult> ShowMessage { get; set; }
/// <summary>Picks library drawings and the quantity to add them with (none when cancelled); replaced by tests.</summary>
internal Func<(IReadOnlyList<DrawingLibraryRecord> Drawings, int Quantity)> PickDrawings { get; set; }
/// <summary>The saved revision once the dialog closes with OK; otherwise null.</summary>
public DrawingSetRecord Saved { get; private set; }
@@ -37,16 +41,20 @@ namespace OpenNest.Forms
InitializeComponent();
this.session = session ?? throw new ArgumentNullException(nameof(session));
ArgumentNullException.ThrowIfNull(library);
// A set mixes materials and thicknesses, so the library is never filtered to a nest.
librarySession = new DrawingLibraryBrowseSession(library, new Nest());
Disposed += (_, _) => librarySession.Dispose();
ShowMessage = (text, buttons, icon) => MessageBox.Show(this, text, Text, buttons, icon);
PickDrawings = () =>
{
using var form = new DrawingLibraryForm(library, serverUrl);
return form.ShowDialog(this) == DialogResult.OK
? (form.PickedDrawings, form.PickedQuantity)
: (Array.Empty<DrawingLibraryRecord>(), 0);
};
GridStyle.Apply(memberGrid);
GridStyle.Apply(libraryGrid);
libraryNameColumn.Tag = DrawingSortField.Name;
libraryMaterialColumn.Tag = DrawingSortField.MaterialName;
libraryGradeColumn.Tag = DrawingSortField.MaterialGrade;
libraryThicknessColumn.Tag = DrawingSortField.Thickness;
libraryCustomerColumn.Tag = DrawingSortField.Customer;
quantityBox.Maximum = DrawingSetRules.MaxQuantity;
libraryGroup.Text = string.IsNullOrEmpty(serverUrl) ? "Drawing library" : $"Drawing library — {serverUrl}";
loading = true;
nameBox.Text = session.Name;
customerBox.Text = session.Customer;
@@ -113,7 +121,54 @@ namespace OpenNest.Forms
// Validation refuses bad text before it is parsed; never show the grid's own error dialog.
private void MemberGrid_DataError(object sender, DataGridViewDataErrorEventArgs e) => e.ThrowException = false;
private void AddButton_Click(object sender, EventArgs e) => AddDrawings();
private async void EditDrawingSetForm_Shown(object sender, EventArgs e) =>
await RunLibraryAsync(librarySession.RefreshAsync);
private void SearchBox_TextChanged(object sender, EventArgs e)
{
searchTimer.Stop();
searchTimer.Start();
}
private async void SearchTimer_Tick(object sender, EventArgs e)
{
searchTimer.Stop();
await RunLibraryAsync(() => librarySession.SetSearchAsync(searchBox.Text));
}
private async void LibraryGrid_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e)
{
// Sorting a page locally would misrepresent the library; the server sorts all matches.
if (e.Button == MouseButtons.Left && libraryGrid.Columns[e.ColumnIndex].Tag is DrawingSortField field)
await RunLibraryAsync(() => librarySession.SortByAsync(field));
}
private async void PreviousButton_Click(object sender, EventArgs e) =>
await RunLibraryAsync(librarySession.PreviousPageAsync);
private async void NextButton_Click(object sender, EventArgs e) =>
await RunLibraryAsync(librarySession.NextPageAsync);
private void LibraryGrid_SelectionChanged(object sender, EventArgs e) => UpdateAddCommand();
private void LibraryGrid_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex >= 0)
AddSelectedDrawings();
}
private void AddButton_Click(object sender, EventArgs e) => AddSelectedDrawings();
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.Enter && libraryGrid.ContainsFocus && SelectedLibraryDrawings().Count > 0)
{
AddSelectedDrawings();
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
private void RemoveButton_Click(object sender, EventArgs e)
{
@@ -138,24 +193,122 @@ namespace OpenNest.Forms
UpdateCommands();
}
/// <summary>Adds the drawings picked from the library, skipping those already in the set.</summary>
internal void AddDrawings()
/// <summary>
/// Adds the drawings selected in the library pane, in list order, at the entered
/// quantity, skipping those already in the set.
/// </summary>
internal void AddSelectedDrawings()
{
if (saving || !memberGrid.EndEdit())
return;
var (drawings, quantity) = PickDrawings();
if (drawings == null || drawings.Count == 0 || IsDisposed)
var drawings = SelectedLibraryDrawings();
if (saving || drawings.Count == 0 || !memberGrid.EndEdit())
return;
var result = session.AddDrawings(drawings, quantity);
var result = session.AddDrawings(drawings, (int)quantityBox.Value);
Populate(result.Added.Count > 0 ? session.Members.Count - 1 : SelectedIndex());
UpdateCommands();
var added = result.Added.Count == 1 ? $"Added '{result.Added[0].Name}'." : $"Added {result.Added.Count} drawings.";
var added = result.Added.Count switch
{
0 => "Nothing added.",
1 => $"Added '{result.Added[0].Name}'.",
_ => $"Added {result.Added.Count} drawings.",
};
statusLabel.Text = result.AlreadyMembers.Count == 0
? added
: $"{added} Already in the set: {string.Join(", ", result.AlreadyMembers.Select(d => $"'{d.Name}'"))}.";
}
// Every selected row in list order; the current row alone when nothing is selected.
private IReadOnlyList<DrawingLibraryRecord> SelectedLibraryDrawings()
{
var picked = libraryGrid.SelectedRows.Cast<DataGridViewRow>()
.OrderBy(row => row.Index)
.Select(row => row.Tag as DrawingLibraryRecord)
.Where(record => record != null)
.ToList();
if (picked.Count == 0 && libraryGrid.CurrentRow?.Tag is DrawingLibraryRecord current)
picked.Add(current);
return picked;
}
private async Task RunLibraryAsync(Func<Task<bool>> request)
{
librarySummaryLabel.Text = LoadingText;
try
{
var started = request();
UpdateLibraryNavigation();
if (await started && !IsDisposed)
PopulateLibrary(librarySession.Page);
}
catch (Exception ex)
{
if (!IsDisposed)
{
libraryGrid.Rows.Clear();
librarySummaryLabel.Text = ex is ServerTooOldException
? ex.Message
: $"Could not load the drawing library: {ex.Message}";
}
}
finally
{
if (!IsDisposed)
{
UpdateLibraryNavigation();
// A request that was declined or superseded renders nothing of its own.
if (!librarySession.IsLoading && librarySummaryLabel.Text == LoadingText)
librarySummaryLabel.Text = librarySession.Summary;
UpdateAddCommand();
}
}
}
private void PopulateLibrary(DrawingLibraryPage page)
{
libraryGrid.Rows.Clear();
foreach (var record in page?.Items ?? Array.Empty<DrawingLibraryRecord>())
{
var index = libraryGrid.Rows.Add(
record.Name,
record.MaterialName,
record.MaterialGrade,
record.Thickness.ToString("0.####", CultureInfo.CurrentCulture),
record.Customer);
libraryGrid.Rows[index].Tag = record;
}
foreach (DataGridViewColumn column in libraryGrid.Columns)
{
column.HeaderCell.SortGlyphDirection = column.Tag is DrawingSortField field && field == librarySession.Sort
? librarySession.Descending ? SortOrder.Descending : SortOrder.Ascending
: SortOrder.None;
}
MarkMembersInLibrary();
UpdateAddCommand();
}
// Greys the library rows already in the set, so what is left to add stands out.
private void MarkMembersInLibrary()
{
var members = session.Members.Select(m => m.DrawingId).ToHashSet();
foreach (DataGridViewRow row in libraryGrid.Rows)
{
var inSet = row.Tag is DrawingLibraryRecord record && members.Contains(record.Id);
row.DefaultCellStyle.ForeColor = inSet ? SystemColors.GrayText : Color.Empty;
row.Cells[libraryNameColumn.Index].ToolTipText = inSet ? "Already in this set" : "";
}
}
private void UpdateLibraryNavigation()
{
previousButton.Enabled = !librarySession.IsLoading && librarySession.CanGoPrevious;
nextButton.Enabled = !librarySession.IsLoading && librarySession.CanGoNext;
}
private void UpdateAddCommand() =>
addButton.Enabled = !saving && SelectedLibraryDrawings().Count > 0;
private async void SaveButton_Click(object sender, EventArgs e) => await SaveAsync();
/// <summary>Saves the set as a new revision; closes with OK on success.</summary>
@@ -237,9 +390,10 @@ namespace OpenNest.Forms
else
memberGrid.ClearSelection();
MarkMembersInLibrary();
var parts = session.Members.Sum(m => (long)m.Quantity);
summaryLabel.Text = session.Members.Count == 0
? "No drawings yet. Choose Add Drawings to pick them from the library."
? "No drawings yet. Select drawings in the library on the left and choose Add to Set."
: $"{session.Members.Count:N0} drawing{(session.Members.Count == 1 ? "" : "s")}, {parts:N0} part{(parts == 1 ? "" : "s")} per kit";
}
@@ -260,7 +414,7 @@ namespace OpenNest.Forms
{
var index = SelectedIndex();
var error = session.GetError();
addButton.Enabled = !saving;
UpdateAddCommand();
removeButton.Enabled = !saving && index >= 0;
upButton.Enabled = !saving && index > 0;
downButton.Enabled = !saving && index >= 0 && index < session.Members.Count - 1;
+6 -3
View File
@@ -342,9 +342,12 @@ a set shows its notes.
- **New Set...** and **Edit...** open the set editor
(`OpenNest.Data.DrawingSetEditSession`): name, customer, notes and the member
grid (drawing, material, grade, thickness, units, cut type, quantity, note).
**Add Drawings...** opens the library in pick mode: every active drawing,
several selectable, added with the quantity entered there. A drawing already
in the set is skipped and named; one member per drawing. Quantities are edited
The editor's left pane browses the library in place, with no second dialog:
every active drawing (never filtered to a nest), searched, sorted and paged
like the Drawings tab. Select one or several and choose **Add to Set**,
double-click or press Enter to add them at the entered quantity per kit.
Drawings already in the set are greyed; one picked again is skipped and
named, so there is one member per drawing. Quantities are edited
in the grid (1 to 100,000); **Remove**, **Move Up** and **Move Down** change
the list. The note column flags drawings archived in the library (skipped on
release unless included) and drawings with no material or thickness (cannot be