refactor(file-menu): regroup the export and report commands

The File menu ran six output commands together with no separators, and
"Export" / "Export All" didn't say what they exported. Group them by what
they produce:

- Save Copy as .nest File... (was Export .nest...) moves next to Save As,
  since it writes the nest file rather than exporting a plate.
- Export Plate... / Export All Plates... (were Export / Export All) get their
  own group and the "..." their save dialogs call for.
- Nest Report... / Part Printouts... (were View ...) form a reports group.
- Storage Mode... gets its own separator above Exit.

Export Nest Report... is removed: the report viewer already has Save As
(available even without WebView2), so it was a second path to the same PDF.
Its guard and failure tests now drive the viewer path, and the atomic-write
test calls WriteNestReport directly. The Avalonia prototype's matching
command is renamed too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-11-12 08:41:12 -05:00
1 parent 755f021092
commit ecd0bba4eb
10 files changed
+90 -170

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@
<Separator /> <Separator />
<MenuItem x:Name="SaveMenuItem" Header="_Save" InputGesture="Ctrl+S" Click="OnSave" /> <MenuItem x:Name="SaveMenuItem" Header="_Save" InputGesture="Ctrl+S" Click="OnSave" />
<MenuItem x:Name="SaveAsMenuItem" Header="Save _As..." Click="OnSaveAs" /> <MenuItem x:Name="SaveAsMenuItem" Header="Save _As..." Click="OnSaveAs" />
<MenuItem x:Name="ExportNestMenuItem" Header="_Export .nest..." Click="OnExportNest" /> <MenuItem x:Name="ExportNestMenuItem" Header="Save _Copy as .nest File..." Click="OnExportNest" />
<Separator /> <Separator />
<MenuItem x:Name="CloseMenuItem" Header="_Close" Click="OnCloseNest" /> <MenuItem x:Name="CloseMenuItem" Header="_Close" Click="OnCloseNest" />
<MenuItem x:Name="CloseAllMenuItem" Header="Close A_ll" Click="OnCloseAll" /> <MenuItem x:Name="CloseAllMenuItem" Header="Close A_ll" Click="OnCloseAll" />
+6 -6
View File
@@ -426,25 +426,25 @@ namespace OpenNest.Avalonia
writingFile = true; writingFile = true;
try try
{ {
var path = await PickNestFileAsync(tab, "Export .nest"); var path = await PickNestFileAsync(tab, "Save Copy as .nest File");
if (path == null || IsNesting(tab, "exporting")) if (path == null || IsNesting(tab, "exporting"))
return; return;
try try
{ {
// Export never changes the nest's file, name or unsaved mark. // Export never changes the nest's file, name or unsaved mark.
tabs.ExportCopy(tab, path); tabs.ExportCopy(tab, path);
ShowNotice($"Exported .nest to {path}"); ShowNotice($"Saved a copy to {path}");
} }
catch (SaveTargetRefusedException ex) catch (SaveTargetRefusedException ex)
{ {
await ConfirmDialog.ShowAsync( await ConfirmDialog.ShowAsync(
this, "Export .nest", $"The nest was not exported. {ex.Message}"); this, "Save Copy", $"The copy was not saved. {ex.Message}");
} }
catch (Exception ex) catch (Exception ex)
{ {
await ConfirmDialog.ShowAsync(this, "Export .nest", await ConfirmDialog.ShowAsync(this, "Save Copy",
$"The nest was not exported.\n\n{path}\n\n{ex.Message}\n\nChoose " $"The copy was not saved.\n\n{path}\n\n{ex.Message}\n\nChoose "
+ "File > Export .nest again to pick another folder or name."); + "File > Save Copy as .nest File again to pick another folder or name.");
} }
} }
finally finally
@@ -47,25 +47,21 @@ public class NestReportExportTests
{ {
using var host = new TestMainForm(); using var host = new TestMainForm();
host.Show(); host.Show();
var item = Menu(host, "mnuFileExportNestReport");
var view = Menu(host, "mnuFileViewNestReport"); var view = Menu(host, "mnuFileViewNestReport");
Assert.False(item.Enabled);
Assert.False(view.Enabled); Assert.False(view.Enabled);
using var form = new EditNestForm(new Nest("enabled job")) { MdiParent = host }; using var form = new EditNestForm(new Nest("enabled job")) { MdiParent = host };
form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here. form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
form.Show(); form.Show();
Assert.True(item.Enabled);
Assert.True(view.Enabled); Assert.True(view.Enabled);
form.Close(); form.Close();
host.Activate(); host.Activate();
Assert.False(item.Enabled);
Assert.False(view.Enabled); Assert.False(view.Enabled);
}); });
[Fact] [Fact]
public void JobBusyRejectsBeforeAnyDialog() => RunSta(() => public void JobBusyRejectsBeforeRendering() => RunSta(() =>
{ {
using var host = new TestMainForm(); using var host = new TestMainForm();
host.Show(); host.Show();
@@ -75,14 +71,14 @@ public class NestReportExportTests
SetField(host, "nestingInProgress", true); SetField(host, "nestingInProgress", true);
try try
{ {
Menu(host, "mnuFileExportNestReport").PerformClick(); Menu(host, "mnuFileViewNestReport").PerformClick();
} }
finally finally
{ {
SetField(host, "nestingInProgress", false); SetField(host, "nestingInProgress", false);
} }
Assert.Empty(host.DialogRequests); Assert.Empty(host.Viewed);
var failure = Assert.Single(host.Failures); var failure = Assert.Single(host.Failures);
Assert.Contains("busy job", failure); Assert.Contains("busy job", failure);
Assert.Contains("Finish or cancel", failure); Assert.Contains("Finish or cancel", failure);
@@ -99,8 +95,8 @@ public class NestReportExportTests
SetBackingField(form.PlateView, "IsFillInProgress", true); SetBackingField(form.PlateView, "IsFillInProgress", true);
try try
{ {
Menu(host, "mnuFileExportNestReport").PerformClick(); Menu(host, "mnuFileViewNestReport").PerformClick();
Assert.Empty(host.DialogRequests); Assert.Empty(host.Viewed);
Assert.Contains("Finish or cancel", Assert.Single(host.Failures)); Assert.Contains("Finish or cancel", Assert.Single(host.Failures));
} }
finally finally
@@ -108,9 +104,9 @@ public class NestReportExportTests
SetBackingField(form.PlateView, "IsFillInProgress", false); SetBackingField(form.PlateView, "IsFillInProgress", false);
} }
// Cleared guard: the command now reaches the (test-intercepted) dialog. // Cleared guard: the command now reaches the (test-intercepted) viewer.
Menu(host, "mnuFileExportNestReport").PerformClick(); Menu(host, "mnuFileViewNestReport").PerformClick();
Assert.Single(host.DialogRequests); NestReportViewerForm.DeleteReportFolder(Assert.Single(host.Viewed));
}); });
[Fact] [Fact]
@@ -126,8 +122,8 @@ public class NestReportExportTests
Assert.True(form.PlateView.Actions.CurrentAction!.IsBusy()); Assert.True(form.PlateView.Actions.CurrentAction!.IsBusy());
try try
{ {
Menu(host, "mnuFileExportNestReport").PerformClick(); Menu(host, "mnuFileViewNestReport").PerformClick();
Assert.Empty(host.DialogRequests); Assert.Empty(host.Viewed);
Assert.Contains("Finish or cancel", Assert.Single(host.Failures)); Assert.Contains("Finish or cancel", Assert.Single(host.Failures));
} }
finally finally
@@ -138,7 +134,7 @@ public class NestReportExportTests
}); });
[Fact] [Fact]
public void InvalidNestNameRejectsBeforeAnyDialog() => RunSta(() => public void InvalidNestNameRejectsBeforeRendering() => RunSta(() =>
{ {
using var host = new TestMainForm(); using var host = new TestMainForm();
host.Show(); host.Show();
@@ -146,87 +142,58 @@ public class NestReportExportTests
form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here. form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
form.Show(); form.Show();
Menu(host, "mnuFileExportNestReport").PerformClick(); Menu(host, "mnuFileViewNestReport").PerformClick();
Assert.Empty(host.DialogRequests); Assert.Empty(host.Viewed);
Assert.Contains("bad/name", Assert.Single(host.Failures)); Assert.Contains("bad/name", Assert.Single(host.Failures));
}); });
[Fact] [Fact]
public void CancelWritesNothingAndNotifiesNothing() => RunSta(() => public void ViewLeavesJobStateUntouched() => RunSta(() =>
{ {
using var host = new TestMainForm { NextDialogPath = null }; using var host = new TestMainForm();
host.Show(); host.Show();
using var form = new EditNestForm(CreateJob("cancel job")) { MdiParent = host }; var nest = CreateJob("state job");
form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
form.Show();
var path = Path.Combine(Path.GetTempPath(), $"opennest-report-cancel-{Guid.NewGuid():N}.pdf");
host.NextDialogPath = path;
host.NextDialogResult = DialogResult.Cancel;
Menu(host, "mnuFileExportNestReport").PerformClick();
Assert.False(File.Exists(path));
Assert.Empty(host.Failures);
Assert.Empty(host.Successes);
});
[Fact]
public void SuccessfulExportWritesPdfAndLeavesJobStateUntouched() => RunSta(() =>
{
using var host = new TestMainForm { NextDialogResult = DialogResult.OK };
host.Show();
var nest = CreateJob("export job");
using var form = new EditNestForm(nest) { MdiParent = host }; using var form = new EditNestForm(nest) { MdiParent = host };
form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here. form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
form.Show(); form.Show();
form.PlateManager.LoadAt(1); form.PlateManager.LoadAt(1);
// EnsureSentinel already appended a trailing empty plate behind the two loaded here. // EnsureSentinel already appended a trailing empty plate behind the two loaded here.
var plateCountBeforeExport = nest.Plates.Count; var plateCountBeforeView = nest.Plates.Count;
var path = Path.Combine(Path.GetTempPath(), $"opennest-report-ok-{Guid.NewGuid():N}.pdf");
Menu(host, "mnuFileViewNestReport").PerformClick();
var path = Assert.Single(host.Viewed);
try try
{ {
host.NextDialogPath = path;
Menu(host, "mnuFileExportNestReport").PerformClick();
Assert.Equal(path, Assert.Single(host.Successes));
var bytes = File.ReadAllBytes(path);
Assert.Equal("%PDF-", System.Text.Encoding.ASCII.GetString(bytes, 0, 5));
Assert.True(bytes.Length > 1000);
// Selected plate and job contents are untouched by reporting. // Selected plate and job contents are untouched by reporting.
Assert.Equal(1, form.PlateManager.CurrentIndex); Assert.Equal(1, form.PlateManager.CurrentIndex);
Assert.Equal(plateCountBeforeExport, nest.Plates.Count); Assert.Equal(plateCountBeforeView, nest.Plates.Count);
Assert.Single(nest.Plates[0].Parts); Assert.Single(nest.Plates[0].Parts);
Assert.Equal(2, nest.Drawings.Count); Assert.Equal(2, nest.Drawings.Count);
} }
finally finally
{ {
File.Delete(path); NestReportViewerForm.DeleteReportFolder(path);
} }
}); });
[Fact] [Fact]
public void RenderFailureLeavesExistingDestinationUnchanged() => RunSta(() => public void RenderFailureLeavesExistingDestinationUnchanged() => RunSta(() =>
{ {
using var host = new TestMainForm { NextDialogResult = DialogResult.OK };
host.Show();
var nest = CreateJob("failure job"); var nest = CreateJob("failure job");
// Outside the report's supported text contract: fails font validation before layout. // Outside the report's supported text contract: fails font validation before layout.
nest.Notes = "em\u2014dash"; nest.Notes = "em\u2014dash";
using var form = new EditNestForm(nest) { MdiParent = host }; using var form = new EditNestForm(nest);
form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here. form.PlateView.SetOverlapAutoCheck(null);
form.Show(); var targets = form.CaptureReportTargets(() => false);
var path = Path.Combine(Path.GetTempPath(), $"opennest-report-fail-{Guid.NewGuid():N}.pdf"); var path = Path.Combine(Path.GetTempPath(), $"opennest-report-fail-{Guid.NewGuid():N}.pdf");
var original = new byte[] { 1, 2, 3, 4 }; var original = new byte[] { 1, 2, 3, 4 };
File.WriteAllBytes(path, original); File.WriteAllBytes(path, original);
try try
{ {
host.NextDialogPath = path; Assert.ThrowsAny<Exception>(() => form.WriteNestReport(targets, path, () => false));
Menu(host, "mnuFileExportNestReport").PerformClick();
Assert.Empty(host.Successes);
var failure = Assert.Single(host.Failures);
Assert.Contains("failure job", failure);
Assert.Equal(original, File.ReadAllBytes(path)); Assert.Equal(original, File.ReadAllBytes(path));
Assert.Empty(Directory.GetFiles(Path.GetDirectoryName(path)!, ".*report-fail*.tmp")); Assert.Empty(Directory.GetFiles(Path.GetDirectoryName(path)!, ".*report-fail*.tmp"));
} }
@@ -248,7 +215,6 @@ public class NestReportExportTests
Menu(host, "mnuFileViewNestReport").PerformClick(); Menu(host, "mnuFileViewNestReport").PerformClick();
Assert.Empty(host.DialogRequests);
Assert.Empty(host.Failures); Assert.Empty(host.Failures);
var path = Assert.Single(host.Viewed); var path = Assert.Single(host.Viewed);
try try
@@ -421,28 +387,16 @@ public class NestReportExportTests
// Same reflection precedent PlateViewFillLifetimeTests uses for fill-lifetime state. // Same reflection precedent PlateViewFillLifetimeTests uses for fill-lifetime state.
private sealed class TestMainForm : MainForm private sealed class TestMainForm : MainForm
{ {
public List<string> DialogRequests { get; } = [];
public string? NextDialogPath { get; set; }
public DialogResult NextDialogResult { get; set; } = DialogResult.OK;
public List<string> Failures { get; } = []; public List<string> Failures { get; } = [];
public List<string> Successes { get; } = [];
public List<string> Viewed { get; } = []; public List<string> Viewed { get; } = [];
// No startup migrations/automatic new document, and no settings save at close. // No startup migrations/automatic new document, and no settings save at close.
protected override void OnLoad(EventArgs e) { } protected override void OnLoad(EventArgs e) { }
protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { } protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { }
internal override string? ShowReportSaveDialog(string suggestedFileName)
{
DialogRequests.Add(suggestedFileName);
return NextDialogResult == DialogResult.OK ? NextDialogPath : null;
}
internal override void ReportFailure(EditNestForm.NestReportTargets? targets, Exception error) => internal override void ReportFailure(EditNestForm.NestReportTargets? targets, Exception error) =>
Failures.Add($"{targets?.Nest.Name}|{error.Message}"); Failures.Add($"{targets?.Nest.Name}|{error.Message}");
internal override void ReportSuccess(string destination) => Successes.Add(destination);
internal override void ShowReportViewer(string pdfPath, EditNestForm.NestReportTargets targets) => internal override void ShowReportViewer(string pdfPath, EditNestForm.NestReportTargets targets) =>
Viewed.Add(pdfPath); Viewed.Add(pdfPath);
+4 -4
View File
@@ -815,16 +815,16 @@ namespace OpenNest.Forms
if (isJobBusy() || IsReportTargetBusy(nest)) if (isJobBusy() || IsReportTargetBusy(nest))
throw new InvalidOperationException( throw new InvalidOperationException(
$"Finish or cancel the current nesting or plate action before exporting the report for '{nest.Name}'." $"Finish or cancel the current nesting or plate action before creating the report for '{nest.Name}'."
); );
return new NestReportTargets(nest, $"{nest.Name}.report.pdf"); return new NestReportTargets(nest, $"{nest.Name}.report.pdf");
} }
/// <summary> /// <summary>
/// Revalidate the fixed target after the dialog closed, then synchronously capture the /// Revalidate the fixed target, then synchronously capture the snapshot on the UI thread
/// snapshot on the UI thread and write it. The dialog pumps messages, so an async fill /// and write it. A caller that pumps messages between capture and write (a dialog) lets an
/// could have committed or started meanwhile; once capture begins nothing yields. /// async fill commit or start meanwhile; once capture begins nothing yields.
/// </summary> /// </summary>
internal void WriteNestReport( internal void WriteNestReport(
NestReportTargets targets, NestReportTargets targets,
+12 -15
View File
@@ -51,7 +51,7 @@ namespace OpenNest.Forms
mnuFileExport = new System.Windows.Forms.ToolStripMenuItem(); mnuFileExport = new System.Windows.Forms.ToolStripMenuItem();
mnuFileExportAll = new System.Windows.Forms.ToolStripMenuItem(); mnuFileExportAll = new System.Windows.Forms.ToolStripMenuItem();
mnuFileViewNestReport = new System.Windows.Forms.ToolStripMenuItem(); mnuFileViewNestReport = new System.Windows.Forms.ToolStripMenuItem();
mnuFileExportNestReport = new System.Windows.Forms.ToolStripMenuItem(); mnuFileReportsSeparator = new System.Windows.Forms.ToolStripSeparator();
mnuFileViewPartPrintouts = new System.Windows.Forms.ToolStripMenuItem(); mnuFileViewPartPrintouts = new System.Windows.Forms.ToolStripMenuItem();
toolStripMenuItem3 = new System.Windows.Forms.ToolStripSeparator(); toolStripMenuItem3 = new System.Windows.Forms.ToolStripSeparator();
mnuFileExit = new System.Windows.Forms.ToolStripMenuItem(); mnuFileExit = new System.Windows.Forms.ToolStripMenuItem();
@@ -222,7 +222,7 @@ namespace OpenNest.Forms
// //
// mnuFile // mnuFile
// //
mnuFile.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuFileNew, mnuFileOpen, mnuFileImportBom, mnuFileDrawingLibrary, mnuFileDrawingSets, mnuFileReleases, toolStripMenuItem1, mnuFileSave, mnuFileSaveAs, toolStripMenuItem2, mnuFileExport, mnuFileExportAll, mnuFileViewNestReport, mnuFileExportNestReport, mnuFileViewPartPrintouts, toolStripMenuItem3, mnuFileExit }); mnuFile.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuFileNew, mnuFileOpen, mnuFileImportBom, mnuFileDrawingLibrary, mnuFileDrawingSets, mnuFileReleases, toolStripMenuItem1, mnuFileSave, mnuFileSaveAs, toolStripMenuItem2, mnuFileExport, mnuFileExportAll, mnuFileReportsSeparator, mnuFileViewNestReport, mnuFileViewPartPrintouts, toolStripMenuItem3, mnuFileExit });
mnuFile.Name = "mnuFile"; mnuFile.Name = "mnuFile";
mnuFile.DropDownOpening += FileMenu_Opening; mnuFile.DropDownOpening += FileMenu_Opening;
mnuFile.Size = new System.Drawing.Size(37, 20); mnuFile.Size = new System.Drawing.Size(37, 20);
@@ -307,38 +307,35 @@ namespace OpenNest.Forms
// //
mnuFileExport.Name = "mnuFileExport"; mnuFileExport.Name = "mnuFileExport";
mnuFileExport.Size = new System.Drawing.Size(146, 22); mnuFileExport.Size = new System.Drawing.Size(146, 22);
mnuFileExport.Text = "Export"; mnuFileExport.Text = "Export Plate...";
mnuFileExport.Click += Export_Click; mnuFileExport.Click += Export_Click;
// //
// mnuFileExportAll // mnuFileExportAll
// //
mnuFileExportAll.Name = "mnuFileExportAll"; mnuFileExportAll.Name = "mnuFileExportAll";
mnuFileExportAll.Size = new System.Drawing.Size(146, 22); mnuFileExportAll.Size = new System.Drawing.Size(146, 22);
mnuFileExportAll.Text = "Export All"; mnuFileExportAll.Text = "Export All Plates...";
mnuFileExportAll.Click += ExportAll_Click; mnuFileExportAll.Click += ExportAll_Click;
// //
// mnuFileReportsSeparator
//
mnuFileReportsSeparator.Name = "mnuFileReportsSeparator";
mnuFileReportsSeparator.Size = new System.Drawing.Size(143, 6);
//
// mnuFileViewNestReport // mnuFileViewNestReport
// //
mnuFileViewNestReport.Enabled = false; mnuFileViewNestReport.Enabled = false;
mnuFileViewNestReport.Name = "mnuFileViewNestReport"; mnuFileViewNestReport.Name = "mnuFileViewNestReport";
mnuFileViewNestReport.Size = new System.Drawing.Size(146, 22); mnuFileViewNestReport.Size = new System.Drawing.Size(146, 22);
mnuFileViewNestReport.Text = "View Nest Report..."; mnuFileViewNestReport.Text = "Nest Report...";
mnuFileViewNestReport.Click += ViewNestReport_Click; mnuFileViewNestReport.Click += ViewNestReport_Click;
// //
// mnuFileExportNestReport
//
mnuFileExportNestReport.Enabled = false;
mnuFileExportNestReport.Name = "mnuFileExportNestReport";
mnuFileExportNestReport.Size = new System.Drawing.Size(146, 22);
mnuFileExportNestReport.Text = "Export Nest Report...";
mnuFileExportNestReport.Click += ExportNestReport_Click;
//
// mnuFileViewPartPrintouts // mnuFileViewPartPrintouts
// //
mnuFileViewPartPrintouts.Enabled = false; mnuFileViewPartPrintouts.Enabled = false;
mnuFileViewPartPrintouts.Name = "mnuFileViewPartPrintouts"; mnuFileViewPartPrintouts.Name = "mnuFileViewPartPrintouts";
mnuFileViewPartPrintouts.Size = new System.Drawing.Size(146, 22); mnuFileViewPartPrintouts.Size = new System.Drawing.Size(146, 22);
mnuFileViewPartPrintouts.Text = "View Part Printouts..."; mnuFileViewPartPrintouts.Text = "Part Printouts...";
mnuFileViewPartPrintouts.Click += ViewPartPrintouts_Click; mnuFileViewPartPrintouts.Click += ViewPartPrintouts_Click;
// //
// toolStripMenuItem3 // toolStripMenuItem3
@@ -1538,7 +1535,7 @@ namespace OpenNest.Forms
private System.Windows.Forms.ToolStripStatusLabel plateUtilStatusLabel; private System.Windows.Forms.ToolStripStatusLabel plateUtilStatusLabel;
private System.Windows.Forms.ToolStripMenuItem mnuFileExportAll; private System.Windows.Forms.ToolStripMenuItem mnuFileExportAll;
private System.Windows.Forms.ToolStripMenuItem mnuFileViewNestReport; private System.Windows.Forms.ToolStripMenuItem mnuFileViewNestReport;
private System.Windows.Forms.ToolStripMenuItem mnuFileExportNestReport; private System.Windows.Forms.ToolStripSeparator mnuFileReportsSeparator;
private System.Windows.Forms.ToolStripMenuItem mnuFileViewPartPrintouts; private System.Windows.Forms.ToolStripMenuItem mnuFileViewPartPrintouts;
private System.Windows.Forms.ToolStripMenuItem openNestToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem openNestToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem pEPToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem pEPToolStripMenuItem;
+7 -39
View File
@@ -304,7 +304,7 @@ namespace OpenNest.Forms
{ {
// Export never changes the document's save path, remote id, or nest name. // Export never changes the document's save path, remote id, or nest name.
new NestWriter(form.Nest) { BestFitStore = BestFitCacheStore.Shared }.Write(dlg.FileName); new NestWriter(form.Nest) { BestFitStore = BestFitCacheStore.Shared }.Write(dlg.FileName);
statusLabel1.Text = $"Exported .nest to {dlg.FileName}"; statusLabel1.Text = $"Saved a copy to {dlg.FileName}";
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -316,23 +316,10 @@ namespace OpenNest.Forms
private void ExportAll_Click(object sender, EventArgs e) => WithActive(f => f.ExportAll()); private void ExportAll_Click(object sender, EventArgs e) => WithActive(f => f.ExportAll());
private void ExportNestReport_Click(object sender, EventArgs e) /// <summary>
{ /// Renders the report to a private temporary file and opens it in a viewer to save or
var form = activeForm; /// print. The viewer's Save As is the only way to keep the PDF, with or without WebView2.
if (form == null || !TryCaptureReportTargets(form, out var targets)) /// </summary>
return;
var destination = ShowReportSaveDialog(targets.SuggestedFileName);
if (destination == null)
return; // Cancel: nothing written, job untouched.
if (!TryWriteNestReport(form, targets, destination))
return;
statusLabel1.Text = $"Saved nest report to {destination}";
ReportSuccess(destination);
}
/// <summary>Renders the report to a private temporary file and opens it in a viewer to save or print.</summary>
private void ViewNestReport_Click(object sender, EventArgs e) private void ViewNestReport_Click(object sender, EventArgs e)
{ {
var form = activeForm; var form = activeForm;
@@ -467,32 +454,13 @@ namespace OpenNest.Forms
viewer.Show(this); viewer.Show(this);
} }
/// <summary>Returns the chosen path, or null on cancel. Overwrite consent is the dialog prompt.</summary>
internal virtual string ShowReportSaveDialog(string suggestedFileName)
{
using var dlg = new SaveFileDialog
{
Filter = "PDF report (*.pdf)|*.pdf",
FileName = suggestedFileName,
AddExtension = true,
DefaultExt = ".pdf",
OverwritePrompt = true,
};
return dlg.ShowDialog(this) == DialogResult.OK ? dlg.FileName : null;
}
/// <summary>Failure notice naming the fixed target; internal so tests observe it without dialogs.</summary> /// <summary>Failure notice naming the fixed target; internal so tests observe it without dialogs.</summary>
internal virtual void ReportFailure( internal virtual void ReportFailure(
EditNestForm.NestReportTargets targets, EditNestForm.NestReportTargets targets,
Exception error) => Exception error) =>
MessageBox.Show(this, MessageBox.Show(this,
$"Could not export the nest report{(targets == null ? "" : $" for '{targets.Nest.Name}'")}: {error.Message}", $"Could not create the nest report{(targets == null ? "" : $" for '{targets.Nest.Name}'")}: {error.Message}",
"Export Nest Report", MessageBoxButtons.OK, MessageBoxIcon.Warning); "Nest Report", MessageBoxButtons.OK, MessageBoxIcon.Warning);
/// <summary>Success notice; internal so tests observe completion without dialogs.</summary>
internal virtual void ReportSuccess(string destination) =>
MessageBox.Show(this, $"Nest report saved to:\n\n{destination}", "Export Nest Report",
MessageBoxButtons.OK, MessageBoxIcon.Information);
private void Exit_Click(object sender, EventArgs e) private void Exit_Click(object sender, EventArgs e)
{ {
+1 -2
View File
@@ -22,8 +22,7 @@ namespace OpenNest.Forms
exportNestMenu.Enabled = hasValue; exportNestMenu.Enabled = hasValue;
mnuFileExport.Enabled = hasValue; mnuFileExport.Enabled = hasValue;
mnuFileExportAll.Enabled = hasValue; mnuFileExportAll.Enabled = hasValue;
mnuFileExportNestReport.Enabled = hasValue && !storage.IsSaving; mnuFileViewNestReport.Enabled = hasValue && !storage.IsSaving;
mnuFileViewNestReport.Enabled = mnuFileExportNestReport.Enabled;
mnuFileViewPartPrintouts.Enabled = hasValue; mnuFileViewPartPrintouts.Enabled = hasValue;
mnuEdit.Visible = hasValue; mnuEdit.Visible = hasValue;
mnuView.Visible = hasValue; mnuView.Visible = hasValue;
+6 -3
View File
@@ -25,7 +25,7 @@ namespace OpenNest.Forms
private CancellationTokenSource nestingCts; private CancellationTokenSource nestingCts;
private readonly NestStorageController storage; private readonly NestStorageController storage;
private readonly ToolStripMenuItem storageModeMenu = new("Storage Mode..."); private readonly ToolStripMenuItem storageModeMenu = new("Storage Mode...");
private readonly ToolStripMenuItem exportNestMenu = new("Export .nest..."); private readonly ToolStripMenuItem exportNestMenu = new("Save Copy as .nest File...");
private readonly ToolStripButton btnCutPlanner; private readonly ToolStripButton btnCutPlanner;
private readonly ToolStripButton btnSimulateCutting; private readonly ToolStripButton btnSimulateCutting;
private readonly ToolStripButton btnSequenceByPath; private readonly ToolStripButton btnSequenceByPath;
@@ -131,8 +131,11 @@ namespace OpenNest.Forms
toolStrip1.Items.Add(postButton); toolStrip1.Items.Add(postButton);
storageModeMenu.Click += StorageMode_Click; storageModeMenu.Click += StorageMode_Click;
exportNestMenu.Click += ExportNest_Click; exportNestMenu.Click += ExportNest_Click;
mnuFile.DropDownItems.Insert(mnuFile.DropDownItems.IndexOf(mnuFileExport), exportNestMenu); // A local .nest copy is a save, not a plate export, so it sits with Save and Save As.
mnuFile.DropDownItems.Insert(mnuFile.DropDownItems.IndexOf(mnuFileExit), storageModeMenu); mnuFile.DropDownItems.Insert(mnuFile.DropDownItems.IndexOf(mnuFileSaveAs) + 1, exportNestMenu);
var exitIndex = mnuFile.DropDownItems.IndexOf(mnuFileExit);
mnuFile.DropDownItems.Insert(exitIndex, new ToolStripSeparator());
mnuFile.DropDownItems.Insert(exitIndex, storageModeMenu);
InitializeAlignMenu(); InitializeAlignMenu();
InitializeHelpMenu(); InitializeHelpMenu();
UpdateOverlapMenu(); UpdateOverlapMenu();
+23 -24
View File
@@ -1,23 +1,23 @@
# Nest report PDF export # Nest report PDF export
Desktop: **File -> Export Nest Report...** writes `<nest-name>.report.pdf` for the Desktop: **File -> Nest Report...** renders `<nest-name>.report.pdf` for the
active nest's whole job through an overwrite-confirming save dialog. The command active nest's whole job. The command is disabled without an open document and
is disabled without an open document and during a background database save, and during a background database save, and it refuses to run while whole-job
it refuses to run while whole-job nesting, an open progress window, interactive nesting, an open progress window, interactive fill or a busy plate action holds
fill or a busy plate action holds the nest or any second window sharing it. After the nest or any second window sharing it. The target and those conditions are
the dialog closes, the target and those conditions are revalidated; the snapshot revalidated before the write; the snapshot is then captured synchronously on the
is then captured synchronously on the UI thread and only that detached snapshot UI thread and only that detached snapshot reaches the renderer. Reporting never
reaches the renderer. Export never changes the selected plate, dirty state, changes the selected plate, dirty state, timestamps or quantities, and it never
timestamps or quantities, and it never touches post selection, verification or touches post selection, verification or CNC output. There are no persisted report
CNC output. There are no persisted report settings or templates. settings or templates.
**File -> View Nest Report...** runs the same guards and capture without a dialog, The report renders to a private folder under `%TEMP%\OpenNest\Reports\` and opens
renders to a private folder under `%TEMP%\OpenNest\Reports\` and opens a modeless a modeless viewer window (WebView2's PDF viewer, sharing Help's WebView2 data
viewer window (WebView2's PDF viewer, sharing Help's WebView2 data folder) with folder) with **Save As...**, which copies the rendered file to a chosen path, and
**Save As...**, which copies the rendered file to a chosen path, and **Print...**, **Print...**, the WebView2 print dialog. Save As is the only way to keep the PDF;
the WebView2 print dialog. Closing the window deletes its temporary folder; a there is no separate direct-save command. Closing the window deletes its temporary
render failure opens no viewer and leaves no folder. Without the WebView2 Runtime folder; a render failure opens no viewer and leaves no folder. Without the WebView2
the window still offers Save As. Runtime the window still offers Save As.
The same two calls back the command and any future integration: The same two calls back the command and any future integration:
@@ -31,7 +31,7 @@ the selected plate, or certify that a layout passed geometry/pre-post checks.
## Part printouts ## Part printouts
**File -> View Part Printouts...** prints every non-cutoff drawing of the active **File -> Part Printouts...** prints every non-cutoff drawing of the active
nest; **View Part Printout...** on the drawing list's right-click menu prints the nest; **View Part Printout...** on the drawing list's right-click menu prints the
selected one. Both render to `%TEMP%\OpenNest\Reports\` and open the report viewer, selected one. Both render to `%TEMP%\OpenNest\Reports\` and open the report viewer,
suggesting `<nest-name>.printout.pdf` or `<drawing-name>.printout.pdf`. Each part suggesting `<nest-name>.printout.pdf` or `<drawing-name>.printout.pdf`. Each part
@@ -191,7 +191,7 @@ concurrent calls cannot lay out text differently from a sequential export.
## Estimated cut time ## Estimated cut time
When the caller passes `CutParameters` (the desktop export always does, from the When the caller passes `CutParameters` (the desktop always does, from the
settings last used in Calculate Cut Time or the simulator), each plate gets an settings last used in Calculate Cut Time or the simulator), each plate gets an
estimated time per copy from `CutSimulation`, plus clamp-removal minutes, the same estimated time per copy from `CutSimulation`, plus clamp-removal minutes, the same
as Calculate Cut Time (see [cut-time estimates](cut-time-estimates.md)). The summary as Calculate Cut Time (see [cut-time estimates](cut-time-estimates.md)). The summary
@@ -253,10 +253,9 @@ Pixel inspection supplements geometry assertions; it is not a CNC-validation
result. result.
Windows compilation is not runtime acceptance. `OpenNest.WinForms.Tests/Forms/NestReportExportTests.cs` Windows compilation is not runtime acceptance. `OpenNest.WinForms.Tests/Forms/NestReportExportTests.cs`
covers enablement, busy/cancel/failure/success adapter behavior, but it only covers enablement, busy/failure/viewer adapter behavior, but it only
compiles on Linux. Windows runtime acceptance still owes: exporting a real nest compiles on Linux. Windows runtime acceptance still owes: reporting a real nest
(and the packaged application, so bundled fonts/notices are verified), checking (and the packaged application, so bundled fonts/notices are verified), checking
page sizes/labels/copies and embedded fonts with `pdffonts`, a tabbed part with a page sizes/labels/copies and embedded fonts with `pdffonts`, a tabbed part with a
hole, opening and printing the PDF, and exercising fill/nesting rejection, cancel, hole, saving and printing the PDF from the viewer, and exercising fill/nesting
overwrite and invalid-path behavior with job state and any existing destination rejection with job state unchanged.
unchanged.
+3 -3
View File
@@ -44,9 +44,9 @@ The desktop app can save nests two ways:
the record from ordinary browsing but retains every archive; **Deleted nests...** the record from ordinary browsing but retains every archive; **Deleted nests...**
opens an explicit recovery view. Neither action purges data. An old desktop opens an explicit recovery view. Neither action purges data. An old desktop
without a revision precondition cannot update or delete on an upgraded server. without a revision precondition cannot update or delete on an upgraded server.
A separate **File > Export .nest...** command is always available (in both A separate **File > Save Copy as .nest File...** command is always available
modes) for producing a local file to share or back up. Export does not change (in both modes) for producing a local file to share or back up. The copy does
the document's file save path or database record association. not change the document's file save path or database record association.
Dragging one or more `.nest` files onto the main window (or onto an open Dragging one or more `.nest` files onto the main window (or onto an open
plate) opens them from disk directly, in either mode — this is the only way plate) opens them from disk directly, in either mode — this is the only way