feat(desktop): open drawing sets from File without a nest

Releasing a set creates new nests or merges into draft ones, so tying the
set browser to the Drawing Library dialog (which needs an open nest) put it
behind a document it never uses. File > Drawing Sets... (Database mode)
now opens a standalone window with the set browser and Release, opening
released nests in their own tabs. The library dialog loses its Sets tab
but keeps Used in Sets and Add to Set on a drawing's menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-22 11:37:04 -04:00
1 parent 099a020424
commit 1fc36e9e54
10 files changed
+199 -101

No files matched your search

@@ -14,7 +14,7 @@ public class DrawingSetFormsTests
private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(60);
[Fact]
public void SetsTab_LoadsWhenShown_AndNewSetIsSavedThroughTheEditor()
public void DrawingSetsForm_LoadsWhenShown_AndNewSetIsSavedThroughTheEditor()
{
StaTestThread.Run(
() =>
@@ -24,9 +24,8 @@ public class DrawingSetFormsTests
var gusset = library.Add("SYN-GUSSET", "Synthetic Aluminum", 0.125);
var sets = new InMemoryDrawingSets();
using var form = new DrawingLibraryForm(library, Server, MatchingNest(), sets);
form.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
var browser = form.SetBrowser;
using var form = new DrawingSetsForm(sets, library, Server);
var browser = form.Browser;
browser.ShowMessage = (text, _, _) => throw new Xunit.Sdk.XunitException(text);
browser.EditSet = edit =>
{
@@ -47,13 +46,10 @@ public class DrawingSetFormsTests
Run(editor.SaveAsync());
return editor.Saved;
};
Assert.Empty(sets.Queries);
form.Show();
var tabs = Field<TabControl>(form, "tabControl");
var grid = Field<DataGridView>(browser, "setGrid");
Assert.Equal(new[] { "Drawings", "Sets" }, tabs.TabPages.Cast<TabPage>().Select(t => t.Text));
Assert.Empty(sets.Queries);
tabs.SelectedIndex = 1;
PumpUntil(() => sets.Queries.Count == 1 && !Field<Label>(browser, "summaryLabel").Text.StartsWith("Loading"), "the set list");
Assert.Contains("New Set", Field<Label>(browser, "summaryLabel").Text);
+1 -1
View File
@@ -12,7 +12,7 @@ namespace OpenNest.Controls
/// <summary>
/// Browses the server's drawing sets one bounded page at a time through
/// <see cref="DrawingSetBrowseSession"/>, and creates, edits, archives or restores them and
/// shows their history. The Drawing Library dialog hosts it as its Sets tab. In pick mode
/// shows their history. <see cref="DrawingSetsForm"/> hosts it to manage and release sets. In pick mode
/// (<see cref="ChooseDrawingSetForm"/>) only active sets are listed, and choosing a set or
/// New Set raises <see cref="SetActivated"/> or <see cref="NewSetChosen"/> instead.
/// </summary>
+2 -50
View File
@@ -24,10 +24,6 @@ namespace OpenNest.Forms
components = new System.ComponentModel.Container();
System.Windows.Forms.DataGridViewCellStyle rightAligned = new System.Windows.Forms.DataGridViewCellStyle();
rootTable = new System.Windows.Forms.TableLayoutPanel();
tabControl = new System.Windows.Forms.TabControl();
drawingsTab = new System.Windows.Forms.TabPage();
setsTab = new System.Windows.Forms.TabPage();
setBrowser = new OpenNest.Controls.DrawingSetBrowser();
footerPanel = new System.Windows.Forms.FlowLayoutPanel();
layoutTable = new System.Windows.Forms.TableLayoutPanel();
filterTable = new System.Windows.Forms.TableLayoutPanel();
@@ -81,9 +77,6 @@ namespace OpenNest.Forms
usedInSetsMenuItem = new System.Windows.Forms.ToolStripMenuItem();
addToSetMenuItem = new System.Windows.Forms.ToolStripMenuItem();
rootTable.SuspendLayout();
tabControl.SuspendLayout();
drawingsTab.SuspendLayout();
setsTab.SuspendLayout();
footerPanel.SuspendLayout();
layoutTable.SuspendLayout();
filterTable.SuspendLayout();
@@ -107,7 +100,7 @@ namespace OpenNest.Forms
//
rootTable.ColumnCount = 1;
rootTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
rootTable.Controls.Add(tabControl, 0, 0);
rootTable.Controls.Add(layoutTable, 0, 0);
rootTable.Controls.Add(footerPanel, 0, 1);
rootTable.Dock = System.Windows.Forms.DockStyle.Fill;
rootTable.Location = new System.Drawing.Point(0, 0);
@@ -119,38 +112,6 @@ namespace OpenNest.Forms
rootTable.Size = new System.Drawing.Size(1184, 721);
rootTable.TabIndex = 0;
//
// tabControl
//
tabControl.Controls.Add(drawingsTab);
tabControl.Controls.Add(setsTab);
tabControl.Dock = System.Windows.Forms.DockStyle.Fill;
tabControl.Margin = new System.Windows.Forms.Padding(0);
tabControl.Name = "tabControl";
tabControl.SelectedIndex = 0;
tabControl.TabIndex = 0;
tabControl.SelectedIndexChanged += TabControl_SelectedIndexChanged;
//
// drawingsTab
//
drawingsTab.Controls.Add(layoutTable);
drawingsTab.Name = "drawingsTab";
drawingsTab.Text = "Drawings";
drawingsTab.UseVisualStyleBackColor = true;
//
// setsTab
//
setsTab.Controls.Add(setBrowser);
setsTab.Name = "setsTab";
setsTab.Padding = new System.Windows.Forms.Padding(8);
setsTab.Text = "Sets";
setsTab.UseVisualStyleBackColor = true;
//
// setBrowser
//
setBrowser.Dock = System.Windows.Forms.DockStyle.Fill;
setBrowser.Name = "setBrowser";
setBrowser.TabIndex = 0;
//
// footerPanel
//
footerPanel.AutoSize = true;
@@ -169,14 +130,12 @@ namespace OpenNest.Forms
layoutTable.Controls.Add(splitContainer, 0, 1);
layoutTable.Controls.Add(statusLabel, 0, 2);
layoutTable.Dock = System.Windows.Forms.DockStyle.Fill;
layoutTable.Location = new System.Drawing.Point(0, 0);
layoutTable.Margin = new System.Windows.Forms.Padding(0);
layoutTable.Name = "layoutTable";
layoutTable.Padding = new System.Windows.Forms.Padding(8);
layoutTable.RowCount = 3;
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
layoutTable.Size = new System.Drawing.Size(1160, 650);
layoutTable.TabIndex = 0;
//
// filterTable
@@ -702,9 +661,6 @@ namespace OpenNest.Forms
Shown += DrawingLibraryForm_Shown;
rootTable.ResumeLayout(false);
rootTable.PerformLayout();
tabControl.ResumeLayout(false);
drawingsTab.ResumeLayout(false);
setsTab.ResumeLayout(false);
footerPanel.ResumeLayout(false);
footerPanel.PerformLayout();
layoutTable.ResumeLayout(false);
@@ -737,10 +693,6 @@ 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 setsTab;
private OpenNest.Controls.DrawingSetBrowser setBrowser;
private System.Windows.Forms.FlowLayoutPanel footerPanel;
private System.Windows.Forms.TableLayoutPanel layoutTable;
private System.Windows.Forms.TableLayoutPanel filterTable;
+2 -31
View File
@@ -21,8 +21,8 @@ namespace OpenNest.Forms
/// shows any warning or refusal first and only then changes the nest. Edit Settings
/// changes the selected drawing's notes and nesting settings (Cut first, priority,
/// 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.
/// store, the drawing grid's menu lists the sets a drawing is in and adds it to a set;
/// 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.
/// </summary>
@@ -119,14 +119,7 @@ namespace OpenNest.Forms
Text = $"Add from Drawing Library — {this.serverUrl}";
if (sets != null)
{
drawingGrid.ContextMenuStrip = drawingMenu;
setBrowser.Initialize(sets, library, this.serverUrl);
}
else
{
tabControl.TabPages.Remove(setsTab);
}
showAllCheckBox.Enabled = session.IsFilteredToNest;
UpdateFilterLabel();
UpdateNavigation();
@@ -334,26 +327,6 @@ namespace OpenNest.Forms
private async void AddButton_Click(object sender, EventArgs e) => await AddSelectedAsync();
/// <summary>The Sets tab's browser; loaded the first time the tab is shown.</summary>
internal OpenNest.Controls.DrawingSetBrowser SetBrowser => setBrowser;
/// <summary>
/// Lets the Sets tab release sets: nests are saved through <paramref name="nests"/>
/// and each is opened with <paramref name="openNest"/>; a nest <paramref name="isNestOpen"/>
/// reports as open in a tab is never merged into.
/// </summary>
public void EnableRelease(INestRepository nests, Action<Nest, NestRecord> openNest, Func<Guid, bool> isNestOpen = null)
{
if (setBrowser.IsInitialized)
setBrowser.EnableRelease(nests, openNest, isNestOpen);
}
private async void TabControl_SelectedIndexChanged(object sender, EventArgs e)
{
if (tabControl.SelectedTab == setsTab && setBrowser.IsInitialized && !setBrowser.HasLoaded)
await setBrowser.RefreshAsync();
}
// A right-click selects the row under the pointer, so the menu acts on what was clicked.
private void DrawingGrid_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
{
@@ -478,8 +451,6 @@ namespace OpenNest.Forms
if (saved == null || IsDisposed)
return;
statusLabel.Text = $"Added '{record.Name}' to set '{saved.Name}' (revision {saved.Revision}).";
if (setBrowser.HasLoaded)
await setBrowser.RefreshAsync();
}
private async void EditButton_Click(object sender, EventArgs e) => await EditSelectedAsync();
+99
View File
@@ -0,0 +1,99 @@
namespace OpenNest.Forms
{
partial class DrawingSetsForm
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
private void InitializeComponent()
{
layoutTable = new System.Windows.Forms.TableLayoutPanel();
browser = new OpenNest.Controls.DrawingSetBrowser();
buttonPanel = new System.Windows.Forms.FlowLayoutPanel();
closeButton = new System.Windows.Forms.Button();
layoutTable.SuspendLayout();
buttonPanel.SuspendLayout();
SuspendLayout();
//
// layoutTable
//
layoutTable.ColumnCount = 1;
layoutTable.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
layoutTable.Controls.Add(browser, 0, 0);
layoutTable.Controls.Add(buttonPanel, 0, 1);
layoutTable.Dock = System.Windows.Forms.DockStyle.Fill;
layoutTable.Location = new System.Drawing.Point(0, 0);
layoutTable.Name = "layoutTable";
layoutTable.Padding = new System.Windows.Forms.Padding(8);
layoutTable.RowCount = 2;
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
layoutTable.RowStyles.Add(new System.Windows.Forms.RowStyle());
layoutTable.Size = new System.Drawing.Size(984, 601);
layoutTable.TabIndex = 0;
//
// browser
//
browser.Dock = System.Windows.Forms.DockStyle.Fill;
browser.Margin = new System.Windows.Forms.Padding(0);
browser.Name = "browser";
browser.TabIndex = 0;
//
// buttonPanel
//
buttonPanel.AutoSize = true;
buttonPanel.Controls.Add(closeButton);
buttonPanel.Dock = System.Windows.Forms.DockStyle.Fill;
buttonPanel.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft;
buttonPanel.Margin = new System.Windows.Forms.Padding(0, 6, 0, 0);
buttonPanel.Name = "buttonPanel";
buttonPanel.TabIndex = 1;
//
// closeButton
//
closeButton.AutoSize = true;
closeButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
closeButton.Name = "closeButton";
closeButton.TabIndex = 0;
closeButton.Text = "Close";
closeButton.UseVisualStyleBackColor = true;
//
// DrawingSetsForm
//
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
CancelButton = closeButton;
ClientSize = new System.Drawing.Size(984, 601);
Controls.Add(layoutTable);
Font = new System.Drawing.Font("Segoe UI", 9.75F);
MinimizeBox = false;
MinimumSize = new System.Drawing.Size(640, 380);
Name = "DrawingSetsForm";
ShowIcon = false;
ShowInTaskbar = false;
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "Drawing Sets";
Shown += DrawingSetsForm_Shown;
layoutTable.ResumeLayout(false);
layoutTable.PerformLayout();
buttonPanel.ResumeLayout(false);
buttonPanel.PerformLayout();
ResumeLayout(false);
}
#endregion
private System.Windows.Forms.TableLayoutPanel layoutTable;
private OpenNest.Controls.DrawingSetBrowser browser;
private System.Windows.Forms.FlowLayoutPanel buttonPanel;
private System.Windows.Forms.Button closeButton;
}
}
+34
View File
@@ -0,0 +1,34 @@
using System;
using System.Windows.Forms;
using OpenNest.Data;
namespace OpenNest.Forms
{
/// <summary>
/// File &gt; Drawing Sets (Database mode): browses, edits and releases the server's drawing
/// sets with <see cref="Controls.DrawingSetBrowser"/>. It needs no open nest, since a
/// release creates new nests or merges into draft ones and opens each in its own tab.
/// </summary>
public partial class DrawingSetsForm : Form
{
internal OpenNest.Controls.DrawingSetBrowser Browser => browser;
/// <param name="library">Used by the set editor to pick drawings.</param>
public DrawingSetsForm(IDrawingSetStore sets, IDrawingLibraryStore library, string serverUrl)
{
InitializeComponent();
browser.Initialize(sets, library, serverUrl);
Text = $"Drawing Sets — {serverUrl}";
}
/// <summary>
/// Shows Release: nests are saved through <paramref name="nests"/> and each is opened
/// with <paramref name="openNest"/>; a nest <paramref name="isNestOpen"/> reports as open
/// in a tab is never merged into.
/// </summary>
public void EnableRelease(INestRepository nests, Action<Nest, NestRecord> openNest, Func<Guid, bool> isNestOpen = null) =>
browser.EnableRelease(nests, openNest, isNestOpen);
private async void DrawingSetsForm_Shown(object sender, EventArgs e) => await browser.RefreshAsync();
}
}
-3
View File
@@ -1285,9 +1285,6 @@ namespace OpenNest.Forms
}
using var form = new DrawingLibraryForm(library, NestStorage.Settings.ServerUrl, Nest, sets);
// Released nests open in their own tabs beside this one.
if (MdiParent is MainForm main)
form.EnableRelease(NestStorage.CreateRepository(), main.OpenServerNest, main.IsServerNestOpen);
form.ShowDialog(this);
if (form.AddedDrawings.Count > 0)
UpdateDrawingList();
+11 -1
View File
@@ -38,6 +38,7 @@ namespace OpenNest.Forms
mnuFileNew = new System.Windows.Forms.ToolStripMenuItem();
mnuFileOpen = new System.Windows.Forms.ToolStripMenuItem();
mnuFileImportBom = new System.Windows.Forms.ToolStripMenuItem();
mnuFileDrawingSets = new System.Windows.Forms.ToolStripMenuItem();
toolStripMenuItem1 = new System.Windows.Forms.ToolStripSeparator();
mnuFileSave = new System.Windows.Forms.ToolStripMenuItem();
mnuFileSaveAs = new System.Windows.Forms.ToolStripMenuItem();
@@ -203,8 +204,9 @@ namespace OpenNest.Forms
//
// mnuFile
//
mnuFile.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuFileNew, mnuFileOpen, mnuFileImportBom, toolStripMenuItem1, mnuFileSave, mnuFileSaveAs, toolStripMenuItem2, mnuFileExport, mnuFileExportAll, mnuFileExportNestReport, toolStripMenuItem3, mnuFileExit });
mnuFile.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuFileNew, mnuFileOpen, mnuFileImportBom, mnuFileDrawingSets, toolStripMenuItem1, mnuFileSave, mnuFileSaveAs, toolStripMenuItem2, mnuFileExport, mnuFileExportAll, mnuFileExportNestReport, toolStripMenuItem3, mnuFileExit });
mnuFile.Name = "mnuFile";
mnuFile.DropDownOpening += FileMenu_Opening;
mnuFile.Size = new System.Drawing.Size(37, 20);
mnuFile.Text = "&File";
//
@@ -233,6 +235,13 @@ namespace OpenNest.Forms
mnuFileImportBom.Text = "Import BOM...";
mnuFileImportBom.Click += ImportBom_Click;
//
// mnuFileDrawingSets
//
mnuFileDrawingSets.Name = "mnuFileDrawingSets";
mnuFileDrawingSets.Size = new System.Drawing.Size(180, 22);
mnuFileDrawingSets.Text = "Drawing Sets...";
mnuFileDrawingSets.Click += DrawingSets_Click;
//
// toolStripMenuItem1
//
toolStripMenuItem1.Name = "toolStripMenuItem1";
@@ -1321,6 +1330,7 @@ namespace OpenNest.Forms
private System.Windows.Forms.ToolStripMenuItem mnuFileNew;
private System.Windows.Forms.ToolStripMenuItem mnuFileOpen;
private System.Windows.Forms.ToolStripMenuItem mnuFileImportBom;
private System.Windows.Forms.ToolStripMenuItem mnuFileDrawingSets;
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem1;
private System.Windows.Forms.ToolStripMenuItem mnuFileSave;
private System.Windows.Forms.ToolStripMenuItem mnuFileSaveAs;
+38
View File
@@ -96,6 +96,44 @@ namespace OpenNest.Forms
form.ShowDialog(this);
}
private void FileMenu_Opening(object sender, EventArgs e)
{
// Drawing sets live on the nest server; File mode has nothing to browse.
var databaseMode = NestStorage.Settings.IsDatabaseMode;
mnuFile.DropDown.ShowItemToolTips = true;
mnuFileDrawingSets.Enabled = databaseMode;
mnuFileDrawingSets.ToolTipText = databaseMode ? "" : NestStorage.DatabaseModeOnly;
}
/// <summary>
/// Browses, edits and releases drawing sets; needs no open nest, since a release
/// creates or merges into nests and opens each in its own tab.
/// </summary>
private void DrawingSets_Click(object sender, EventArgs e)
{
if (!NestStorage.Settings.IsDatabaseMode)
return;
IDrawingLibraryStore library;
IDrawingSetStore sets;
INestRepository nests;
try
{
library = NestStorage.CreateDrawingLibrary();
sets = NestStorage.CreateDrawingSetStore();
nests = NestStorage.CreateRepository();
}
catch (ArgumentException ex)
{
MessageBox.Show(this, ex.Message, "Drawing Sets", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
using var form = new DrawingSetsForm(sets, library, NestStorage.Settings.ServerUrl);
form.EnableRelease(nests, OpenServerNest, IsServerNestOpen);
form.ShowDialog(this);
}
private async void Save_Click(object sender, EventArgs e)
{
var form = activeForm;
+8 -7
View File
@@ -345,8 +345,9 @@ revision; save the nest to keep it.
### Drawing sets (desktop)
The Drawing Library dialog has a **Sets** tab beside **Drawings** (Database
mode only, like the dialog). It lists sets a page at a time
**File > Drawing Sets...** (Database mode only) opens the drawing sets window;
it needs no open nest, since releasing a set makes new nests or merges into
draft ones and opens each in its own tab. It lists sets a page at a time
(`OpenNest.Data.DrawingSetBrowseSession`): search by name, customer or notes,
sort by name, customer, revision or saved time, and **Show archived**. Selecting
a set shows its notes.
@@ -356,7 +357,7 @@ a set shows its notes.
grid (drawing, material, grade, thickness, units, cut type, quantity, note).
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**,
like the Drawing Library dialog. 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
@@ -374,7 +375,7 @@ a set shows its notes.
deleted. **History...** lists revisions newest first with the members of the
selected one, shown with their drawings' current library details.
On the Drawings tab, right-click a drawing for **Used in Sets...** (the sets
In the Drawing Library dialog, right-click a drawing for **Used in Sets...** (the sets
whose current revision contains it) or **Add to Set...**: choose an active set
or **New Set**, and the set editor opens with the drawing added at the entered
quantity for review and Save.
@@ -393,7 +394,7 @@ library's latest revision. Anything kept, skipped, archived in the library or
newer there than in the nest is listed first, then the set editor opens for
review and Save; nothing is stored before then.
**Release...** (the Sets tab, when the dialog was opened from a nest) reads the
**Release...** (in the drawing sets window) reads the
selected active set with its members and opens the release dialog with it listed.
More sets are added by typing in **Add a set** (the list of matching active sets
fills as you type; Enter or **Add** takes the picked set, or the one set whose
@@ -462,8 +463,8 @@ A **drawing set** (schema v7) is a named, versioned list of library drawings
with a quantity each: a kit, an assembly or a recurring order. Members name
drawings by id and always mean each drawing's **current** revision; there is no
per-member revision pin. Sets travel as JSON (`application/json`, at most 1 MiB).
The desktop browses and edits them on the Drawing Library dialog's Sets tab
([Drawing sets (desktop)](#drawing-sets-desktop)), where sets are also released.
The desktop browses, edits and releases them in File > Drawing Sets
([Drawing sets (desktop)](#drawing-sets-desktop)).
`OpenNest.Data.DrawingSetRecord` fields: `id` (client-allocated before the
first save), `revision`, `name`, `customer`, `notes`, `archived`, `savedBy`,