mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 02:41:06 -04:00
feat(desktop): redesign the pre-post verification form for quick reading
Replace the plain-text report with a colored status banner, per-check count tiles and a findings list grouped by check type, with the selected finding shown in full below it. A Copy Report button keeps the full text report available. Consent, cancellation and failure blocking are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0ca4e64189
commit
11b6032459
3 files changed
+461
-62
No files matched your search
@@ -23,6 +23,9 @@ public class PostVerificationFormTests
|
||||
var post = Control<Button>(form, "postButton");
|
||||
Assert.False(consent.Checked);
|
||||
Assert.False(post.Enabled);
|
||||
var list = Control<ListView>(form, "findingsList");
|
||||
Assert.NotEmpty(list.Items);
|
||||
Assert.All(list.Items.Cast<ListViewItem>(), item => Assert.NotNull(item.Group));
|
||||
Invoke(form, "PostButton_Click", null, EventArgs.Empty);
|
||||
Assert.NotEqual(DialogResult.OK, form.DialogResult);
|
||||
consent.Checked = true;
|
||||
@@ -54,7 +57,8 @@ public class PostVerificationFormTests
|
||||
using var form = new PostVerificationForm(nest,
|
||||
(_, _) => Task.FromResult(PostVerificationAnalyzer.Analyze(nest)));
|
||||
form.VerifyAsync().GetAwaiter().GetResult();
|
||||
Assert.NotEmpty(Control<TextBox>(form, "reportBox").Text);
|
||||
Assert.Equal("No warnings found", Control<Label>(form, "headlineLabel").Text);
|
||||
Assert.Empty(Control<ListView>(form, "findingsList").Items);
|
||||
Assert.True(Control<Button>(form, "postButton").Enabled);
|
||||
Assert.False(Control<CheckBox>(form, "acknowledgeBox").Enabled);
|
||||
Assert.Equal("Continue Posting", Control<Button>(form, "postButton").Text);
|
||||
@@ -73,7 +77,7 @@ public class PostVerificationFormTests
|
||||
form.VerifyAsync().GetAwaiter().GetResult();
|
||||
Control<CheckBox>(form, "acknowledgeBox").Checked = true;
|
||||
Assert.False(Control<Button>(form, "postButton").Enabled);
|
||||
Assert.Contains("test failure", Control<TextBox>(form, "reportBox").Text);
|
||||
Assert.Contains("test failure", Control<Label>(form, "detailLabel").Text);
|
||||
Invoke(form, "PostButton_Click", null, EventArgs.Empty);
|
||||
Assert.NotEqual(DialogResult.OK, form.DialogResult);
|
||||
});
|
||||
|
||||
+208
-46
@@ -6,11 +6,26 @@ namespace OpenNest.Forms;
|
||||
partial class PostVerificationForm
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
private TableLayoutPanel banner;
|
||||
private Label statusIcon;
|
||||
private Label headlineLabel;
|
||||
private Label detailLabel;
|
||||
private ProgressBar progressBar;
|
||||
private TableLayoutPanel layout;
|
||||
private Label summaryLabel;
|
||||
private TextBox reportBox;
|
||||
private FlowLayoutPanel tilesPanel;
|
||||
private Panel findingsHost;
|
||||
private ListView findingsList;
|
||||
private ColumnHeader plateColumn;
|
||||
private ColumnHeader partColumn;
|
||||
private ColumnHeader otherPartColumn;
|
||||
private ColumnHeader messageColumn;
|
||||
private Label emptyLabel;
|
||||
private Label findingDetailLabel;
|
||||
private Label footnoteLabel;
|
||||
private Panel acknowledgePanel;
|
||||
private CheckBox acknowledgeBox;
|
||||
private FlowLayoutPanel buttons;
|
||||
private TableLayoutPanel buttonRow;
|
||||
private Button copyButton;
|
||||
private Button postButton;
|
||||
private Button cancelButton;
|
||||
private bool disposedResources;
|
||||
@@ -29,96 +44,243 @@ partial class PostVerificationForm
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
banner = new TableLayoutPanel();
|
||||
statusIcon = new Label();
|
||||
headlineLabel = new Label();
|
||||
detailLabel = new Label();
|
||||
progressBar = new ProgressBar();
|
||||
layout = new TableLayoutPanel();
|
||||
summaryLabel = new Label();
|
||||
reportBox = new TextBox();
|
||||
tilesPanel = new FlowLayoutPanel();
|
||||
findingsHost = new Panel();
|
||||
findingsList = new ListView();
|
||||
plateColumn = new ColumnHeader();
|
||||
partColumn = new ColumnHeader();
|
||||
otherPartColumn = new ColumnHeader();
|
||||
messageColumn = new ColumnHeader();
|
||||
emptyLabel = new Label();
|
||||
findingDetailLabel = new Label();
|
||||
footnoteLabel = new Label();
|
||||
acknowledgePanel = new Panel();
|
||||
acknowledgeBox = new CheckBox();
|
||||
buttons = new FlowLayoutPanel();
|
||||
buttonRow = new TableLayoutPanel();
|
||||
copyButton = new Button();
|
||||
postButton = new Button();
|
||||
cancelButton = new Button();
|
||||
banner.SuspendLayout();
|
||||
layout.SuspendLayout();
|
||||
buttons.SuspendLayout();
|
||||
findingsHost.SuspendLayout();
|
||||
acknowledgePanel.SuspendLayout();
|
||||
buttonRow.SuspendLayout();
|
||||
SuspendLayout();
|
||||
|
||||
banner.Name = "banner";
|
||||
banner.Dock = DockStyle.Top;
|
||||
banner.AutoSize = true;
|
||||
banner.Padding = new Padding(16, 14, 16, 14);
|
||||
banner.ColumnCount = 2;
|
||||
banner.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
banner.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
banner.RowCount = 3;
|
||||
banner.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
banner.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
banner.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
banner.Controls.Add(statusIcon, 0, 0);
|
||||
banner.SetRowSpan(statusIcon, 3);
|
||||
banner.Controls.Add(headlineLabel, 1, 0);
|
||||
banner.Controls.Add(detailLabel, 1, 1);
|
||||
banner.Controls.Add(progressBar, 1, 2);
|
||||
|
||||
statusIcon.Name = "statusIcon";
|
||||
statusIcon.AutoSize = true;
|
||||
statusIcon.Font = new Font("Segoe MDL2 Assets", 22F, FontStyle.Regular, GraphicsUnit.Point);
|
||||
statusIcon.Margin = new Padding(0, 2, 14, 0);
|
||||
statusIcon.AccessibleRole = AccessibleRole.Graphic;
|
||||
|
||||
headlineLabel.Name = "headlineLabel";
|
||||
headlineLabel.AutoSize = true;
|
||||
headlineLabel.Dock = DockStyle.Fill;
|
||||
headlineLabel.Font = new Font("Segoe UI Semibold", 13F, FontStyle.Regular, GraphicsUnit.Point);
|
||||
headlineLabel.Margin = new Padding(0);
|
||||
|
||||
detailLabel.Name = "detailLabel";
|
||||
detailLabel.AutoSize = true;
|
||||
detailLabel.Dock = DockStyle.Fill;
|
||||
detailLabel.Margin = new Padding(1, 4, 0, 0);
|
||||
|
||||
progressBar.Name = "progressBar";
|
||||
progressBar.Style = ProgressBarStyle.Marquee;
|
||||
progressBar.MarqueeAnimationSpeed = 30;
|
||||
progressBar.Height = 6;
|
||||
progressBar.Dock = DockStyle.Fill;
|
||||
progressBar.Margin = new Padding(1, 10, 0, 0);
|
||||
|
||||
layout.Name = "layout";
|
||||
layout.ColumnCount = 1;
|
||||
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
layout.RowCount = 4;
|
||||
layout.RowCount = 6;
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
layout.Dock = DockStyle.Fill;
|
||||
layout.Padding = new Padding(12);
|
||||
layout.Controls.Add(summaryLabel, 0, 0);
|
||||
layout.Controls.Add(reportBox, 0, 1);
|
||||
layout.Controls.Add(acknowledgeBox, 0, 2);
|
||||
layout.Controls.Add(buttons, 0, 3);
|
||||
layout.Padding = new Padding(12, 10, 12, 12);
|
||||
layout.Controls.Add(tilesPanel, 0, 0);
|
||||
layout.Controls.Add(findingsHost, 0, 1);
|
||||
layout.Controls.Add(findingDetailLabel, 0, 2);
|
||||
layout.Controls.Add(footnoteLabel, 0, 3);
|
||||
layout.Controls.Add(acknowledgePanel, 0, 4);
|
||||
layout.Controls.Add(buttonRow, 0, 5);
|
||||
|
||||
summaryLabel.Name = "summaryLabel";
|
||||
summaryLabel.AutoSize = true;
|
||||
summaryLabel.Dock = DockStyle.Fill;
|
||||
summaryLabel.Margin = new Padding(3, 3, 3, 10);
|
||||
summaryLabel.Text = "Checking all plates: overlaps, missing lead-ins, and rapid crossings...";
|
||||
summaryLabel.TabIndex = 0;
|
||||
tilesPanel.Name = "tilesPanel";
|
||||
tilesPanel.AutoSize = true;
|
||||
tilesPanel.Dock = DockStyle.Fill;
|
||||
tilesPanel.WrapContents = true;
|
||||
tilesPanel.Margin = new Padding(0, 0, 0, 6);
|
||||
|
||||
reportBox.Name = "reportBox";
|
||||
reportBox.Dock = DockStyle.Fill;
|
||||
reportBox.Multiline = true;
|
||||
reportBox.ReadOnly = true;
|
||||
reportBox.ScrollBars = ScrollBars.Vertical;
|
||||
reportBox.WordWrap = true;
|
||||
reportBox.MaxLength = 0;
|
||||
reportBox.TabIndex = 1;
|
||||
reportBox.AccessibleName = "Pre-post verification findings";
|
||||
reportBox.Text = "Verification is running. No CNC output has been written.";
|
||||
findingsHost.Name = "findingsHost";
|
||||
findingsHost.Dock = DockStyle.Fill;
|
||||
findingsHost.Margin = new Padding(3);
|
||||
findingsHost.Controls.Add(findingsList);
|
||||
findingsHost.Controls.Add(emptyLabel);
|
||||
|
||||
findingsList.Name = "findingsList";
|
||||
findingsList.Dock = DockStyle.Fill;
|
||||
findingsList.View = View.Details;
|
||||
findingsList.FullRowSelect = true;
|
||||
findingsList.MultiSelect = false;
|
||||
findingsList.HideSelection = false;
|
||||
findingsList.ShowGroups = true;
|
||||
findingsList.ShowItemToolTips = true;
|
||||
findingsList.HeaderStyle = ColumnHeaderStyle.Nonclickable;
|
||||
findingsList.TabIndex = 0;
|
||||
findingsList.AccessibleName = "Pre-post verification findings";
|
||||
findingsList.Columns.AddRange(new[] { plateColumn, partColumn, otherPartColumn, messageColumn });
|
||||
findingsList.SelectedIndexChanged += FindingsList_SelectedIndexChanged;
|
||||
findingsList.Resize += FindingsList_Resize;
|
||||
|
||||
plateColumn.Text = "Plate";
|
||||
plateColumn.Width = 60;
|
||||
partColumn.Text = "Part";
|
||||
partColumn.Width = 55;
|
||||
otherPartColumn.Text = "Other part";
|
||||
otherPartColumn.Width = 75;
|
||||
messageColumn.Text = "Finding";
|
||||
messageColumn.Width = 600;
|
||||
|
||||
emptyLabel.Name = "emptyLabel";
|
||||
emptyLabel.Dock = DockStyle.Fill;
|
||||
emptyLabel.TextAlign = ContentAlignment.MiddleCenter;
|
||||
emptyLabel.ForeColor = Color.FromArgb(110, 110, 110);
|
||||
emptyLabel.BorderStyle = BorderStyle.FixedSingle;
|
||||
emptyLabel.BackColor = Color.FromArgb(250, 250, 250);
|
||||
emptyLabel.Visible = false;
|
||||
|
||||
findingDetailLabel.Name = "findingDetailLabel";
|
||||
findingDetailLabel.AutoSize = true;
|
||||
findingDetailLabel.Dock = DockStyle.Fill;
|
||||
findingDetailLabel.BackColor = Color.FromArgb(245, 245, 245);
|
||||
findingDetailLabel.ForeColor = Color.FromArgb(51, 51, 51);
|
||||
findingDetailLabel.Padding = new Padding(8, 6, 8, 6);
|
||||
findingDetailLabel.Margin = new Padding(3, 0, 3, 6);
|
||||
findingDetailLabel.MinimumSize = new Size(0, 44);
|
||||
findingDetailLabel.Visible = false;
|
||||
|
||||
footnoteLabel.Name = "footnoteLabel";
|
||||
footnoteLabel.AutoSize = true;
|
||||
footnoteLabel.Dock = DockStyle.Fill;
|
||||
footnoteLabel.Font = new Font("Segoe UI", 8.25F, FontStyle.Regular, GraphicsUnit.Point);
|
||||
footnoteLabel.ForeColor = Color.FromArgb(110, 110, 110);
|
||||
footnoteLabel.Margin = new Padding(3, 2, 3, 8);
|
||||
footnoteLabel.Text = "These checks cover direct XY rapids in plate/program order; the post may change order, " +
|
||||
"routing or retracts. Inspect the posted machine program and machine setup before cutting, " +
|
||||
"and confirm actual contour gaps give adequate retention.";
|
||||
|
||||
acknowledgePanel.Name = "acknowledgePanel";
|
||||
acknowledgePanel.AutoSize = true;
|
||||
acknowledgePanel.Dock = DockStyle.Fill;
|
||||
acknowledgePanel.Padding = new Padding(10, 8, 10, 8);
|
||||
acknowledgePanel.Margin = new Padding(3, 0, 3, 10);
|
||||
acknowledgePanel.BackColor = Color.FromArgb(255, 246, 224);
|
||||
acknowledgePanel.Visible = false;
|
||||
acknowledgePanel.Controls.Add(acknowledgeBox);
|
||||
|
||||
acknowledgeBox.Name = "acknowledgeBox";
|
||||
acknowledgeBox.AutoSize = true;
|
||||
acknowledgeBox.Dock = DockStyle.Fill;
|
||||
acknowledgeBox.Margin = new Padding(3, 12, 3, 12);
|
||||
acknowledgeBox.Enabled = false;
|
||||
acknowledgeBox.Checked = false;
|
||||
acknowledgeBox.ForeColor = Color.FromArgb(110, 60, 0);
|
||||
acknowledgeBox.Text = "I understand the listed risks, including possible head crashes and machine or material damage.\r\nI choose to bypass these warnings and continue posting this nest.";
|
||||
acknowledgeBox.TabIndex = 2;
|
||||
acknowledgeBox.TabIndex = 0;
|
||||
acknowledgeBox.CheckedChanged += AcknowledgeBox_CheckedChanged;
|
||||
|
||||
buttons.Dock = DockStyle.Fill;
|
||||
buttons.AutoSize = true;
|
||||
buttons.FlowDirection = FlowDirection.RightToLeft;
|
||||
buttons.Controls.Add(cancelButton);
|
||||
buttons.Controls.Add(postButton);
|
||||
buttons.TabIndex = 3;
|
||||
buttonRow.Name = "buttonRow";
|
||||
buttonRow.AutoSize = true;
|
||||
buttonRow.Dock = DockStyle.Fill;
|
||||
buttonRow.Margin = new Padding(0);
|
||||
buttonRow.ColumnCount = 4;
|
||||
buttonRow.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
buttonRow.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
buttonRow.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
buttonRow.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
buttonRow.RowCount = 1;
|
||||
buttonRow.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
buttonRow.Controls.Add(copyButton, 0, 0);
|
||||
buttonRow.Controls.Add(postButton, 2, 0);
|
||||
buttonRow.Controls.Add(cancelButton, 3, 0);
|
||||
|
||||
cancelButton.Name = "cancelButton";
|
||||
cancelButton.Text = "Cancel";
|
||||
cancelButton.AutoSize = true;
|
||||
cancelButton.DialogResult = DialogResult.Cancel;
|
||||
cancelButton.TabIndex = 0;
|
||||
copyButton.Name = "copyButton";
|
||||
copyButton.Text = "Copy Report";
|
||||
copyButton.AutoSize = true;
|
||||
copyButton.MinimumSize = new Size(100, 30);
|
||||
copyButton.Enabled = false;
|
||||
copyButton.TabIndex = 0;
|
||||
copyButton.Click += CopyButton_Click;
|
||||
|
||||
postButton.Name = "postButton";
|
||||
postButton.Text = "Continue Posting";
|
||||
postButton.AutoSize = true;
|
||||
postButton.MinimumSize = new Size(120, 30);
|
||||
postButton.Enabled = false;
|
||||
postButton.TabIndex = 1;
|
||||
postButton.Click += PostButton_Click;
|
||||
|
||||
cancelButton.Name = "cancelButton";
|
||||
cancelButton.Text = "Cancel";
|
||||
cancelButton.AutoSize = true;
|
||||
cancelButton.MinimumSize = new Size(100, 30);
|
||||
cancelButton.DialogResult = DialogResult.Cancel;
|
||||
cancelButton.TabIndex = 2;
|
||||
|
||||
AcceptButton = cancelButton;
|
||||
CancelButton = cancelButton;
|
||||
AutoScaleDimensions = new SizeF(7F, 15F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(850, 560);
|
||||
MinimumSize = new Size(700, 440);
|
||||
BackColor = Color.White;
|
||||
ClientSize = new Size(880, 600);
|
||||
MinimumSize = new Size(720, 480);
|
||||
Font = new Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point);
|
||||
// Fill first, then the top banner, so the banner docks ahead of the content.
|
||||
Controls.Add(layout);
|
||||
Controls.Add(banner);
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
Name = "PostVerificationForm";
|
||||
ShowInTaskbar = false;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
Text = "Verify Nest Before Posting";
|
||||
banner.ResumeLayout(false);
|
||||
banner.PerformLayout();
|
||||
findingsHost.ResumeLayout(false);
|
||||
acknowledgePanel.ResumeLayout(false);
|
||||
acknowledgePanel.PerformLayout();
|
||||
buttonRow.ResumeLayout(false);
|
||||
buttonRow.PerformLayout();
|
||||
layout.ResumeLayout(false);
|
||||
layout.PerformLayout();
|
||||
buttons.ResumeLayout(false);
|
||||
buttons.PerformLayout();
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
@@ -12,6 +14,31 @@ namespace OpenNest.Forms;
|
||||
/// </summary>
|
||||
public partial class PostVerificationForm : Form
|
||||
{
|
||||
private enum Status { Running, Clear, Warning, Failed }
|
||||
|
||||
// Most severe first; this order drives both the tiles and the list groups.
|
||||
private static readonly (PostVerificationKind Kind, string Label)[] Categories =
|
||||
{
|
||||
(PostVerificationKind.Incomplete, "Incomplete checks"),
|
||||
(PostVerificationKind.Overlap, "Overlaps"),
|
||||
(PostVerificationKind.RapidCrossing, "Rapid crossings"),
|
||||
(PostVerificationKind.MissingLeadIn, "Missing lead-ins"),
|
||||
(PostVerificationKind.NestedCutOrder, "Nested part order"),
|
||||
(PostVerificationKind.NestedLead, "Nested part leads"),
|
||||
(PostVerificationKind.FixedLead, "Fixed leads"),
|
||||
(PostVerificationKind.PierceZone, "Pierce zones"),
|
||||
(PostVerificationKind.PartSpacing, "Part spacing"),
|
||||
};
|
||||
|
||||
// Checks every plate always runs, so their tiles show even at zero.
|
||||
private static readonly PostVerificationKind[] CoreChecks =
|
||||
{
|
||||
PostVerificationKind.Overlap, PostVerificationKind.RapidCrossing, PostVerificationKind.MissingLeadIn,
|
||||
};
|
||||
|
||||
private static readonly Color ClearAccent = Color.FromArgb(22, 128, 61);
|
||||
private static readonly Color WarningAccent = Color.FromArgb(194, 94, 0);
|
||||
|
||||
private readonly Nest nest;
|
||||
private readonly Func<Nest, CancellationToken, Task<PostVerificationReport>> analyze;
|
||||
private readonly CancellationTokenSource cancellation = new();
|
||||
@@ -31,6 +58,10 @@ public partial class PostVerificationForm : Form
|
||||
this.nest = nest ?? throw new ArgumentNullException(nameof(nest));
|
||||
this.analyze = analyze ?? throw new ArgumentNullException(nameof(analyze));
|
||||
InitializeComponent();
|
||||
SetStatus(Status.Running, "Checking nest…",
|
||||
"Looking for overlaps, missing lead-ins and rapid crossings on every plate. " +
|
||||
"No CNC output has been written.");
|
||||
ShowEmpty("Findings will appear here when the checks finish.");
|
||||
}
|
||||
|
||||
protected override async void OnShown(EventArgs e)
|
||||
@@ -51,16 +82,7 @@ public partial class PostVerificationForm : Form
|
||||
if (closeRequested || IsDisposed)
|
||||
return;
|
||||
report = result;
|
||||
reportBox.Text = report.ToDisplayText();
|
||||
reportBox.SelectionStart = 0;
|
||||
reportBox.SelectionLength = 0;
|
||||
summaryLabel.Text = report.HasWarnings
|
||||
? "Warnings found. Review every finding before deciding whether to post."
|
||||
: "Verification complete. No warnings found by these checks.";
|
||||
acknowledgeBox.Visible = report.HasWarnings;
|
||||
acknowledgeBox.Enabled = report.HasWarnings;
|
||||
acknowledgeBox.Checked = false;
|
||||
postButton.Text = report.HasWarnings ? "Post Anyway" : "Continue Posting";
|
||||
ShowReport();
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
|
||||
{
|
||||
@@ -71,9 +93,10 @@ public partial class PostVerificationForm : Form
|
||||
if (!IsDisposed)
|
||||
{
|
||||
report = null;
|
||||
summaryLabel.Text = "Verification failed. Posting is blocked.";
|
||||
reportBox.Text = "The verification could not finish. Cancel, correct the problem, and try again."
|
||||
+ Environment.NewLine + Environment.NewLine + ex.Message;
|
||||
SetStatus(Status.Failed, "Verification failed — posting is blocked",
|
||||
"The verification could not finish. Cancel, correct the problem, and try again."
|
||||
+ Environment.NewLine + ex.Message);
|
||||
ShowEmpty("No findings are available because the verification did not finish.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -91,6 +114,215 @@ public partial class PostVerificationForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowReport()
|
||||
{
|
||||
var findings = report.Findings;
|
||||
if (!report.HasWarnings)
|
||||
{
|
||||
SetStatus(Status.Clear, "No warnings found",
|
||||
"Every plate passed these checks. You can continue posting.");
|
||||
}
|
||||
else if (findings.Any(finding => finding.Kind == PostVerificationKind.Incomplete))
|
||||
{
|
||||
SetStatus(Status.Warning, "Verification incomplete",
|
||||
"Some checks could not finish. Review every finding before deciding whether to post.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var plates = findings.Where(finding => finding.PlateNumber > 0)
|
||||
.Select(finding => finding.PlateNumber).Distinct().Count();
|
||||
var headline = Plural(findings.Count, "warning")
|
||||
+ (plates > 0 ? $" on {Plural(plates, "plate")}" : "");
|
||||
SetStatus(Status.Warning, headline, "Review every finding before deciding whether to post.");
|
||||
}
|
||||
|
||||
BuildTiles();
|
||||
BuildList();
|
||||
acknowledgePanel.Visible = report.HasWarnings;
|
||||
acknowledgeBox.Enabled = report.HasWarnings;
|
||||
acknowledgeBox.Checked = false;
|
||||
postButton.Text = report.HasWarnings ? "Post Anyway" : "Continue Posting";
|
||||
copyButton.Enabled = true;
|
||||
}
|
||||
|
||||
private void SetStatus(Status status, string headline, string detail)
|
||||
{
|
||||
var (back, fore, glyph) = status switch
|
||||
{
|
||||
Status.Clear => (Color.FromArgb(225, 244, 230), Color.FromArgb(20, 83, 45), ""),
|
||||
Status.Warning => (Color.FromArgb(255, 241, 208), Color.FromArgb(124, 63, 0), ""),
|
||||
Status.Failed => (Color.FromArgb(253, 228, 228), Color.FromArgb(145, 24, 24), ""),
|
||||
_ => (Color.FromArgb(230, 239, 250), Color.FromArgb(28, 70, 128), ""),
|
||||
};
|
||||
banner.BackColor = back;
|
||||
headlineLabel.ForeColor = fore;
|
||||
detailLabel.ForeColor = fore;
|
||||
statusIcon.ForeColor = fore;
|
||||
statusIcon.Text = glyph;
|
||||
headlineLabel.Text = headline;
|
||||
detailLabel.Text = detail;
|
||||
progressBar.Visible = status == Status.Running;
|
||||
}
|
||||
|
||||
private void BuildTiles()
|
||||
{
|
||||
tilesPanel.SuspendLayout();
|
||||
tilesPanel.Controls.Clear();
|
||||
foreach (var (kind, label) in Categories)
|
||||
{
|
||||
var count = report.Findings.Count(finding => finding.Kind == kind);
|
||||
if (count == 0 && !CoreChecks.Contains(kind))
|
||||
continue;
|
||||
tilesPanel.Controls.Add(CreateTile(kind, label, count));
|
||||
}
|
||||
tilesPanel.ResumeLayout();
|
||||
}
|
||||
|
||||
private Control CreateTile(PostVerificationKind kind, string label, int count)
|
||||
{
|
||||
var accent = count == 0 ? ClearAccent : WarningAccent;
|
||||
var tile = new TableLayoutPanel
|
||||
{
|
||||
AutoSize = true,
|
||||
ColumnCount = 1,
|
||||
RowCount = 2,
|
||||
MinimumSize = new Size(118, 0),
|
||||
Margin = new Padding(3, 3, 8, 3),
|
||||
Padding = new Padding(10, 6, 10, 8),
|
||||
BackColor = count == 0 ? Color.FromArgb(242, 248, 244) : Color.FromArgb(255, 246, 228),
|
||||
Cursor = count == 0 ? Cursors.Default : Cursors.Hand,
|
||||
AccessibleName = $"{label}: {count}",
|
||||
};
|
||||
var number = new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
Text = count == 0 ? "✓ 0" : count.ToString(),
|
||||
Font = new Font("Segoe UI Semibold", 16F, FontStyle.Regular, GraphicsUnit.Point),
|
||||
ForeColor = accent,
|
||||
Margin = new Padding(0),
|
||||
};
|
||||
var caption = new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
Text = label,
|
||||
ForeColor = Color.FromArgb(70, 70, 70),
|
||||
Margin = new Padding(1, 0, 0, 0),
|
||||
};
|
||||
tile.Controls.Add(number, 0, 0);
|
||||
tile.Controls.Add(caption, 0, 1);
|
||||
if (count != 0)
|
||||
{
|
||||
EventHandler jump = (_, _) => SelectFirst(kind);
|
||||
tile.Click += jump;
|
||||
number.Click += jump;
|
||||
caption.Click += jump;
|
||||
number.Cursor = caption.Cursor = Cursors.Hand;
|
||||
}
|
||||
return tile;
|
||||
}
|
||||
|
||||
private void BuildList()
|
||||
{
|
||||
findingsList.BeginUpdate();
|
||||
findingsList.Items.Clear();
|
||||
findingsList.Groups.Clear();
|
||||
foreach (var (kind, label) in Categories)
|
||||
{
|
||||
var findings = report.Findings.Where(finding => finding.Kind == kind).ToList();
|
||||
if (findings.Count == 0)
|
||||
continue;
|
||||
var group = new ListViewGroup($"{label} ({findings.Count})")
|
||||
{
|
||||
Name = kind.ToString(),
|
||||
CollapsedState = ListViewGroupCollapsedState.Expanded,
|
||||
};
|
||||
findingsList.Groups.Add(group);
|
||||
foreach (var finding in findings)
|
||||
{
|
||||
var item = new ListViewItem(new[]
|
||||
{
|
||||
finding.PlateNumber == 0 ? "Post" : finding.PlateNumber.ToString(),
|
||||
finding.PartNumber?.ToString() ?? "",
|
||||
finding.OtherPartNumber?.ToString() ?? "",
|
||||
finding.Message,
|
||||
}, group)
|
||||
{
|
||||
Tag = finding,
|
||||
ToolTipText = finding.Message,
|
||||
};
|
||||
findingsList.Items.Add(item);
|
||||
}
|
||||
}
|
||||
findingsList.EndUpdate();
|
||||
FitMessageColumn();
|
||||
|
||||
if (report.HasWarnings)
|
||||
{
|
||||
findingsList.Visible = true;
|
||||
emptyLabel.Visible = false;
|
||||
findingDetailLabel.Visible = true;
|
||||
findingDetailLabel.Text = "Select a finding to read it in full.";
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowEmpty("✓ Nothing to review.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowEmpty(string text)
|
||||
{
|
||||
findingsList.Visible = false;
|
||||
findingDetailLabel.Visible = false;
|
||||
emptyLabel.Text = text;
|
||||
emptyLabel.Visible = true;
|
||||
}
|
||||
|
||||
private void SelectFirst(PostVerificationKind kind)
|
||||
{
|
||||
var item = findingsList.Items.Cast<ListViewItem>()
|
||||
.FirstOrDefault(candidate => ((PostVerificationFinding)candidate.Tag).Kind == kind);
|
||||
if (item == null)
|
||||
return;
|
||||
findingsList.SelectedItems.Clear();
|
||||
item.Selected = true;
|
||||
item.Focused = true;
|
||||
item.EnsureVisible();
|
||||
findingsList.Focus();
|
||||
}
|
||||
|
||||
private void FindingsList_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (findingsList.SelectedItems.Count == 0 ||
|
||||
findingsList.SelectedItems[0].Tag is not PostVerificationFinding finding)
|
||||
{
|
||||
findingDetailLabel.Text = "Select a finding to read it in full.";
|
||||
return;
|
||||
}
|
||||
var where = finding.PlateNumber == 0 ? "Post processor" : $"Plate {finding.PlateNumber}";
|
||||
if (finding.PartNumber is { } part)
|
||||
where += $", part {part}";
|
||||
if (finding.OtherPartNumber is { } other)
|
||||
where += $" and part {other}";
|
||||
findingDetailLabel.Text = $"{where}: {finding.Message}";
|
||||
}
|
||||
|
||||
private void FindingsList_Resize(object sender, EventArgs e) => FitMessageColumn();
|
||||
|
||||
private void FitMessageColumn()
|
||||
{
|
||||
var used = plateColumn.Width + partColumn.Width + otherPartColumn.Width;
|
||||
messageColumn.Width = System.Math.Max(200,
|
||||
findingsList.ClientSize.Width - used - SystemInformation.VerticalScrollBarWidth);
|
||||
}
|
||||
|
||||
private void CopyButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (report != null)
|
||||
Clipboard.SetText(report.ToDisplayText());
|
||||
}
|
||||
|
||||
private static string Plural(int count, string noun) => count == 1 ? $"1 {noun}" : $"{count} {noun}s";
|
||||
|
||||
private void UpdatePostPermission() =>
|
||||
postButton.Enabled = !analyzing && !closeRequested
|
||||
&& report?.CanPost(acknowledgeBox.Checked) == true;
|
||||
@@ -114,7 +346,8 @@ public partial class PostVerificationForm : Form
|
||||
cancellation.Cancel();
|
||||
postButton.Enabled = false;
|
||||
cancelButton.Enabled = false;
|
||||
summaryLabel.Text = "Canceling verification...";
|
||||
SetStatus(Status.Running, "Canceling verification…",
|
||||
"Waiting for the running checks to stop. No CNC output has been written.");
|
||||
e.Cancel = true;
|
||||
}
|
||||
base.OnFormClosing(e);
|
||||
|
||||
Reference in new issue
Block a user