mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-04 14:51:37 -05:00
feat(reporting): view nest reports in a window to save or print
Export Nest Report only wrote a file, so checking or printing a report meant finding it and opening another program. View Nest Report renders the same snapshot to a private temp folder and opens it in a WebView2 viewer with Save As and Print; closing the window deletes the temp copy. Export and View share the capture and write guards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
54cea0c325
commit
9d4d024d60
6 files changed
+279
-22
No files matched your search
@@ -48,16 +48,20 @@ public class NestReportExportTests
|
||||
using var host = new TestMainForm();
|
||||
host.Show();
|
||||
var item = Menu(host, "mnuFileExportNestReport");
|
||||
var view = Menu(host, "mnuFileViewNestReport");
|
||||
Assert.False(item.Enabled);
|
||||
Assert.False(view.Enabled);
|
||||
|
||||
using var form = new EditNestForm(new Nest("enabled job")) { MdiParent = host };
|
||||
form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
|
||||
form.Show();
|
||||
Assert.True(item.Enabled);
|
||||
Assert.True(view.Enabled);
|
||||
|
||||
form.Close();
|
||||
host.Activate();
|
||||
Assert.False(item.Enabled);
|
||||
Assert.False(view.Enabled);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
@@ -232,6 +236,55 @@ public class NestReportExportTests
|
||||
}
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void ViewRendersATemporaryPdfForTheViewer() => RunSta(() =>
|
||||
{
|
||||
using var host = new TestMainForm();
|
||||
host.Show();
|
||||
var nest = CreateJob("view job");
|
||||
using var form = new EditNestForm(nest) { MdiParent = host };
|
||||
form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
|
||||
form.Show();
|
||||
|
||||
Menu(host, "mnuFileViewNestReport").PerformClick();
|
||||
|
||||
Assert.Empty(host.DialogRequests);
|
||||
Assert.Empty(host.Failures);
|
||||
var path = Assert.Single(host.Viewed);
|
||||
try
|
||||
{
|
||||
Assert.Equal("view job.report.pdf", Path.GetFileName(path));
|
||||
var bytes = File.ReadAllBytes(path);
|
||||
Assert.Equal("%PDF-", System.Text.Encoding.ASCII.GetString(bytes, 0, 5));
|
||||
Assert.Equal(2, nest.Drawings.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
NestReportViewerForm.DeleteReportFolder(path);
|
||||
}
|
||||
Assert.False(Directory.Exists(Path.GetDirectoryName(path)));
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void ViewRenderFailureOpensNoViewerAndLeavesNoTempFolder() => RunSta(() =>
|
||||
{
|
||||
using var host = new TestMainForm();
|
||||
host.Show();
|
||||
var nest = CreateJob("view failure");
|
||||
nest.Notes = "em—dash"; // Fails font validation before layout.
|
||||
using var form = new EditNestForm(nest) { MdiParent = host };
|
||||
form.PlateView.SetOverlapAutoCheck(null); // Background overlap workers are irrelevant here.
|
||||
form.Show();
|
||||
var reports = Path.Combine(Path.GetTempPath(), "OpenNest", "Reports");
|
||||
var before = Directory.Exists(reports) ? Directory.GetDirectories(reports).Length : 0;
|
||||
|
||||
Menu(host, "mnuFileViewNestReport").PerformClick();
|
||||
|
||||
Assert.Empty(host.Viewed);
|
||||
Assert.Contains("view failure", Assert.Single(host.Failures));
|
||||
Assert.Equal(before, Directory.Exists(reports) ? Directory.GetDirectories(reports).Length : 0);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void BusyAfterDialogRejectsWriteWithoutTouchingDestination() => RunSta(() =>
|
||||
{
|
||||
@@ -295,6 +348,7 @@ public class NestReportExportTests
|
||||
public DialogResult NextDialogResult { get; set; } = DialogResult.OK;
|
||||
public List<string> Failures { get; } = [];
|
||||
public List<string> Successes { get; } = [];
|
||||
public List<string> Viewed { get; } = [];
|
||||
|
||||
// No startup migrations/automatic new document, and no settings save at close.
|
||||
protected override void OnLoad(EventArgs e) { }
|
||||
@@ -310,6 +364,9 @@ public class NestReportExportTests
|
||||
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) =>
|
||||
Viewed.Add(pdfPath);
|
||||
}
|
||||
|
||||
private static void RunSta(System.Action action) =>
|
||||
|
||||
Generated
+11
-1
@@ -50,6 +50,7 @@ namespace OpenNest.Forms
|
||||
toolStripMenuItem2 = new System.Windows.Forms.ToolStripSeparator();
|
||||
mnuFileExport = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuFileExportAll = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuFileViewNestReport = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuFileExportNestReport = new System.Windows.Forms.ToolStripMenuItem();
|
||||
toolStripMenuItem3 = new System.Windows.Forms.ToolStripSeparator();
|
||||
mnuFileExit = new System.Windows.Forms.ToolStripMenuItem();
|
||||
@@ -217,7 +218,7 @@ namespace OpenNest.Forms
|
||||
//
|
||||
// mnuFile
|
||||
//
|
||||
mnuFile.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuFileNew, mnuFileOpen, mnuFileImportBom, mnuFileDrawingLibrary, mnuFileDrawingSets, mnuFileReleases, toolStripMenuItem1, mnuFileSave, mnuFileSaveAs, toolStripMenuItem2, mnuFileExport, mnuFileExportAll, mnuFileExportNestReport, toolStripMenuItem3, mnuFileExit });
|
||||
mnuFile.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuFileNew, mnuFileOpen, mnuFileImportBom, mnuFileDrawingLibrary, mnuFileDrawingSets, mnuFileReleases, toolStripMenuItem1, mnuFileSave, mnuFileSaveAs, toolStripMenuItem2, mnuFileExport, mnuFileExportAll, mnuFileViewNestReport, mnuFileExportNestReport, toolStripMenuItem3, mnuFileExit });
|
||||
mnuFile.Name = "mnuFile";
|
||||
mnuFile.DropDownOpening += FileMenu_Opening;
|
||||
mnuFile.Size = new System.Drawing.Size(37, 20);
|
||||
@@ -312,6 +313,14 @@ namespace OpenNest.Forms
|
||||
mnuFileExportAll.Text = "Export All";
|
||||
mnuFileExportAll.Click += ExportAll_Click;
|
||||
//
|
||||
// mnuFileViewNestReport
|
||||
//
|
||||
mnuFileViewNestReport.Enabled = false;
|
||||
mnuFileViewNestReport.Name = "mnuFileViewNestReport";
|
||||
mnuFileViewNestReport.Size = new System.Drawing.Size(146, 22);
|
||||
mnuFileViewNestReport.Text = "View Nest Report...";
|
||||
mnuFileViewNestReport.Click += ViewNestReport_Click;
|
||||
//
|
||||
// mnuFileExportNestReport
|
||||
//
|
||||
mnuFileExportNestReport.Enabled = false;
|
||||
@@ -1500,6 +1509,7 @@ namespace OpenNest.Forms
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuCloseAll;
|
||||
private System.Windows.Forms.ToolStripStatusLabel plateUtilStatusLabel;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuFileExportAll;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuFileViewNestReport;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuFileExportNestReport;
|
||||
private System.Windows.Forms.ToolStripMenuItem openNestToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem pEPToolStripMenuItem;
|
||||
|
||||
@@ -315,30 +315,69 @@ namespace OpenNest.Forms
|
||||
private void ExportNestReport_Click(object sender, EventArgs e)
|
||||
{
|
||||
var form = activeForm;
|
||||
if (form == null)
|
||||
if (form == null || !TryCaptureReportTargets(form, out var targets))
|
||||
return;
|
||||
|
||||
// The report never depends on a selected post or NC output; it only needs a
|
||||
// quiescent nest so the snapshot is coherent.
|
||||
Func<bool> isJobBusy = IsNestJobBusy;
|
||||
EditNestForm.NestReportTargets targets;
|
||||
try
|
||||
{
|
||||
targets = form.CaptureReportTargets(isJobBusy);
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidOperationException or ArgumentException)
|
||||
{
|
||||
ReportFailure(null, ex);
|
||||
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)
|
||||
{
|
||||
var form = activeForm;
|
||||
if (form == null || !TryCaptureReportTargets(form, out var targets))
|
||||
return;
|
||||
|
||||
var path = Path.Combine(Path.GetTempPath(), "OpenNest", "Reports",
|
||||
Guid.NewGuid().ToString("N"), targets.SuggestedFileName);
|
||||
try
|
||||
{
|
||||
form.WriteNestReport(targets, destination, isJobBusy);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
ReportFailure(targets, ex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryWriteNestReport(form, targets, path))
|
||||
{
|
||||
NestReportViewerForm.DeleteReportFolder(path);
|
||||
return;
|
||||
}
|
||||
ShowReportViewer(path, targets);
|
||||
}
|
||||
|
||||
// The report never depends on a selected post or NC output; it only needs a
|
||||
// quiescent nest so the snapshot is coherent.
|
||||
private bool TryCaptureReportTargets(EditNestForm form, out EditNestForm.NestReportTargets targets)
|
||||
{
|
||||
try
|
||||
{
|
||||
targets = form.CaptureReportTargets(IsNestJobBusy);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidOperationException or ArgumentException)
|
||||
{
|
||||
ReportFailure(null, ex);
|
||||
targets = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryWriteNestReport(EditNestForm form, EditNestForm.NestReportTargets targets, string destination)
|
||||
{
|
||||
try
|
||||
{
|
||||
form.WriteNestReport(targets, destination, IsNestJobBusy);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
when (ex is InvalidOperationException
|
||||
@@ -350,10 +389,15 @@ namespace OpenNest.Forms
|
||||
{
|
||||
// Failures describe the captured target; an active-document switch is irrelevant.
|
||||
ReportFailure(targets, ex);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
statusLabel1.Text = $"Saved nest report to {destination}";
|
||||
ReportSuccess(destination);
|
||||
}
|
||||
|
||||
/// <summary>Opens the rendered report; internal so tests observe it without a WebView2 window.</summary>
|
||||
internal virtual void ShowReportViewer(string pdfPath, EditNestForm.NestReportTargets targets)
|
||||
{
|
||||
var viewer = new NestReportViewerForm(pdfPath, targets.Nest.Name, targets.SuggestedFileName);
|
||||
viewer.Show(this);
|
||||
}
|
||||
|
||||
/// <summary>Returns the chosen path, or null on cancel. Overwrite consent is the dialog prompt.</summary>
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace OpenNest.Forms
|
||||
mnuFileExport.Enabled = hasValue;
|
||||
mnuFileExportAll.Enabled = hasValue;
|
||||
mnuFileExportNestReport.Enabled = hasValue && !storage.IsSaving;
|
||||
mnuFileViewNestReport.Enabled = mnuFileExportNestReport.Enabled;
|
||||
mnuEdit.Visible = hasValue;
|
||||
mnuView.Visible = hasValue;
|
||||
mnuNest.Visible = hasValue;
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Forms;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Microsoft.Web.WebView2.WinForms;
|
||||
|
||||
namespace OpenNest.Forms
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows a rendered nest report PDF in WebView2 with Save As and Print. The PDF is a private
|
||||
/// temporary copy owned by this window: saving copies it, and closing deletes it.
|
||||
/// </summary>
|
||||
public sealed class NestReportViewerForm : Form
|
||||
{
|
||||
private readonly string pdfPath;
|
||||
private readonly string suggestedFileName;
|
||||
private readonly WebView2 browser = new() { Dock = DockStyle.Fill };
|
||||
private readonly ToolStripButton printButton;
|
||||
|
||||
public NestReportViewerForm(string pdfPath, string nestName, string suggestedFileName)
|
||||
{
|
||||
this.pdfPath = pdfPath ?? throw new ArgumentNullException(nameof(pdfPath));
|
||||
this.suggestedFileName = suggestedFileName;
|
||||
Text = $"Nest Report - {nestName}";
|
||||
Icon = Properties.Resources.app_icon;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
var area = Screen.FromPoint(Cursor.Position).WorkingArea;
|
||||
Size = new Size(System.Math.Min(1000, area.Width), System.Math.Min(900, area.Height));
|
||||
|
||||
var saveButton = new ToolStripButton("Save As...") { Name = "btnSaveAs" };
|
||||
saveButton.Click += (_, _) => SaveAs();
|
||||
printButton = new ToolStripButton("Print...") { Name = "btnPrint", Enabled = false };
|
||||
printButton.Click += (_, _) => Print();
|
||||
var toolbar = new ToolStrip { GripStyle = ToolStripGripStyle.Hidden };
|
||||
toolbar.Items.AddRange(new ToolStripItem[] { saveButton, printButton });
|
||||
|
||||
Controls.Add(browser);
|
||||
Controls.Add(toolbar);
|
||||
}
|
||||
|
||||
/// <summary>The report file this window shows and deletes on close.</summary>
|
||||
public string PdfPath => pdfPath;
|
||||
|
||||
protected override async void OnLoad(EventArgs e)
|
||||
{
|
||||
base.OnLoad(e);
|
||||
try
|
||||
{
|
||||
var dataFolder = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"OpenNest", "WebView2");
|
||||
var environment = await CoreWebView2Environment.CreateAsync(null, dataFolder);
|
||||
await browser.EnsureCoreWebView2Async(environment);
|
||||
}
|
||||
catch (Exception ex) when (ex is WebView2RuntimeNotFoundException or COMException or IOException
|
||||
or UnauthorizedAccessException)
|
||||
{
|
||||
// Saving needs no viewer; only the preview and Print are unavailable.
|
||||
MessageBox.Show(this,
|
||||
"The report preview needs the Microsoft Edge WebView2 Runtime, which could not be started. "
|
||||
+ "Use Save As to keep the report.\n\n" + ex.Message,
|
||||
Text, MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsDisposed)
|
||||
return;
|
||||
var core = browser.CoreWebView2;
|
||||
core.Settings.AreDevToolsEnabled = false;
|
||||
core.Settings.IsStatusBarEnabled = false;
|
||||
// The report is the only page this window shows.
|
||||
core.NewWindowRequested += (_, args) => args.Handled = true;
|
||||
core.Navigate(new Uri(pdfPath).AbsoluteUri);
|
||||
printButton.Enabled = true;
|
||||
}
|
||||
|
||||
private void SaveAs()
|
||||
{
|
||||
using var dlg = new SaveFileDialog
|
||||
{
|
||||
Filter = "PDF report (*.pdf)|*.pdf",
|
||||
FileName = suggestedFileName,
|
||||
AddExtension = true,
|
||||
DefaultExt = ".pdf",
|
||||
OverwritePrompt = true,
|
||||
};
|
||||
if (dlg.ShowDialog(this) != DialogResult.OK)
|
||||
return;
|
||||
try
|
||||
{
|
||||
// Overwrite consent is the dialog prompt.
|
||||
File.Copy(pdfPath, dlg.FileName, true);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException)
|
||||
{
|
||||
MessageBox.Show(this, $"Could not save the nest report: {ex.Message}", Text,
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
private void Print()
|
||||
{
|
||||
try
|
||||
{
|
||||
browser.CoreWebView2?.ShowPrintUI(CoreWebView2PrintDialogKind.Browser);
|
||||
}
|
||||
catch (Exception ex) when (ex is COMException or InvalidOperationException)
|
||||
{
|
||||
MessageBox.Show(this, $"Could not print the nest report: {ex.Message}", Text,
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnFormClosed(FormClosedEventArgs e)
|
||||
{
|
||||
// Release WebView2's hold on the file before deleting its private folder.
|
||||
browser.Dispose();
|
||||
base.OnFormClosed(e);
|
||||
DeleteReportFolder(pdfPath);
|
||||
}
|
||||
|
||||
/// <summary>Best effort: a leftover temp report is harmless.</summary>
|
||||
internal static void DeleteReportFolder(string pdfPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var folder = Path.GetDirectoryName(pdfPath);
|
||||
if (folder != null && Directory.Exists(folder))
|
||||
Directory.Delete(folder, true);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,15 @@ the dialog closes, the target and those conditions are revalidated; the snapshot
|
||||
is then captured synchronously on the UI thread and only that detached snapshot
|
||||
reaches the renderer. Export never changes the selected plate, dirty state,
|
||||
timestamps or quantities, and it never touches post selection, verification or
|
||||
CNC output. Opening and printing use any normal PDF viewer; the report includes
|
||||
no printer controls and there are no persisted report settings or templates.
|
||||
CNC output. There are no persisted report settings or templates.
|
||||
|
||||
**File -> View Nest Report...** runs the same guards and capture without a dialog,
|
||||
renders to a private folder under `%TEMP%\OpenNest\Reports\` and opens a modeless
|
||||
viewer window (WebView2's PDF viewer, sharing Help's WebView2 data folder) with
|
||||
**Save As...**, which copies the rendered file to a chosen path, and **Print...**,
|
||||
the WebView2 print dialog. Closing the window deletes its temporary folder; a
|
||||
render failure opens no viewer and leaves no folder. Without the WebView2 Runtime
|
||||
the window still offers Save As.
|
||||
|
||||
The same two calls back the command and any future integration:
|
||||
|
||||
|
||||
Reference in new issue
Block a user