mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-10 07:34:14 -04:00
The plate preview read DataGridView.CurrentRow in SelectionChanged, but that event runs before CurrentCell moves, so CurrentRow still named the previous row. Choosing a row showed the plate that had been selected before it, and the next-plate button snapped the preview back to the old plate. Read the selected row instead. The Windows job of run 37341610182 caught it: PlatePreview_StepsThroughThePlates_AndFollowsThePlatesTable expected "Plate 2 of 2" after the next-plate button and got "Plate 1 of 2". The test now also checks that the selected row follows the button.
716 lines
27 KiB
C#
716 lines
27 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Drawing;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Forms;
|
|
using OpenNest.Controls;
|
|
using OpenNest.Data;
|
|
using OpenNest.IO;
|
|
using Timer = System.Windows.Forms.Timer;
|
|
|
|
namespace OpenNest.Forms;
|
|
|
|
/// <summary>
|
|
/// File > Open (Database mode). The upper list browses saved nests on the shared nest
|
|
/// server one bounded page at a time; filtering, sorting and paging run on the server
|
|
/// through <see cref="NestBrowseSession"/>. The lower tabs show the plates and drawings of
|
|
/// the highlighted nest, read from its archive through <see cref="NestDetailsSession"/>,
|
|
/// beside a preview of one of its plates. This form only renders their state.
|
|
/// </summary>
|
|
public sealed class SavedNestsForm : Form
|
|
{
|
|
private const string IdColumn = "Id";
|
|
private const string PlateSizeColumn = "PlateSize";
|
|
private const string AreaColumn = "Area";
|
|
private const string LoadingText = "Loading...";
|
|
private const string LoadingDetailsText = "Loading details...";
|
|
private const string WindowTitle = "Open Nest — Database";
|
|
|
|
private static readonly Font SectionFont = new(SystemFonts.MessageBoxFont.FontFamily, 14f);
|
|
|
|
private readonly INestRepository repository;
|
|
private readonly string serverUrl;
|
|
private readonly NestBrowseSession session;
|
|
private readonly NestDetailsSession details;
|
|
private readonly SplitContainer split;
|
|
private readonly DataGridView nestGrid;
|
|
private readonly DataGridView platesGrid;
|
|
private readonly DataGridView drawingsGrid;
|
|
private readonly TabControl detailsTabs;
|
|
private readonly SplitContainer detailsSplit;
|
|
private readonly PlateView platePreview;
|
|
private readonly Button previousPlateButton;
|
|
private readonly Button nextPlateButton;
|
|
private readonly Label plateLabel;
|
|
private readonly Label detailsStatus;
|
|
private readonly TextBox searchBox;
|
|
private readonly Timer searchTimer;
|
|
private readonly Timer detailsTimer;
|
|
private readonly Button previousButton;
|
|
private readonly Button nextButton;
|
|
private readonly Label summaryLabel;
|
|
private readonly ToolStripMenuItem openMenuItem;
|
|
private readonly ToolStripMenuItem deleteMenuItem;
|
|
private bool deleting;
|
|
private IReadOnlyList<Plate> previewPlates = Array.Empty<Plate>();
|
|
private int previewIndex = -1;
|
|
|
|
/// <summary>Set to the chosen record's id when the dialog closes with OK.</summary>
|
|
public Guid SelectedId { get; private set; }
|
|
|
|
public SavedNestsForm(INestRepository repository, string serverUrl = null)
|
|
{
|
|
this.repository = repository;
|
|
this.serverUrl = serverUrl ?? "";
|
|
session = new NestBrowseSession(repository);
|
|
details = new NestDetailsSession(LoadDetailsAsync);
|
|
|
|
Text = WindowTitle;
|
|
StartPosition = FormStartPosition.CenterParent;
|
|
ClientSize = new Size(1280, 800);
|
|
MinimumSize = new Size(800, 560);
|
|
MinimizeBox = false;
|
|
ShowInTaskbar = false;
|
|
|
|
// Nests: one server page, sorted by the server when a header is clicked.
|
|
nestGrid = CreateGrid();
|
|
nestGrid.RowHeadersWidth = 64;
|
|
AddNestColumn("Nest name", NestSortField.Name, 170);
|
|
AddNestColumn("Status", NestSortField.Status, 100);
|
|
AddNestColumn("Modified", NestSortField.DateModified, 140);
|
|
AddNestColumn("Plates", NestSortField.PlateCount, 60, alignRight: true);
|
|
AddNestColumn("Parts", NestSortField.PartCount, 60, alignRight: true);
|
|
AddNestColumn("Comment", NestSortField.Comments, 260);
|
|
AddNestColumn("Customer", NestSortField.Customer, 160);
|
|
AddNestColumn("Made by", NestSortField.MadeBy, 90);
|
|
AddNestColumn("Material", NestSortField.Material, 150);
|
|
AddNestColumn("Thickness", NestSortField.Thickness, 80, alignRight: true);
|
|
AddNestColumn("Created", NestSortField.DateCreated, 140);
|
|
AddNestColumn("Saved", NestSortField.SavedAt, 140);
|
|
AddNestColumn("File size", NestSortField.FileSize, 80, alignRight: true);
|
|
nestGrid.Columns.Add(new DataGridViewTextBoxColumn
|
|
{
|
|
Name = IdColumn,
|
|
ValueType = typeof(Guid),
|
|
Visible = false,
|
|
SortMode = DataGridViewColumnSortMode.NotSortable,
|
|
});
|
|
// Sorting a page locally would misrepresent the archive; the server sorts all matches.
|
|
nestGrid.ColumnHeaderMouseClick += async (_, e) =>
|
|
{
|
|
if (e.Button == MouseButtons.Left && nestGrid.Columns[e.ColumnIndex].Tag is NestSortField field)
|
|
await RunAsync(() => session.SortByAsync(field));
|
|
};
|
|
nestGrid.CellDoubleClick += (_, e) =>
|
|
{
|
|
if (e.RowIndex >= 0)
|
|
OpenSelected();
|
|
};
|
|
nestGrid.CellMouseDown += (_, e) =>
|
|
{
|
|
// Right-click highlights the row under the pointer before its menu opens.
|
|
if (e.Button == MouseButtons.Right && e.RowIndex >= 0)
|
|
nestGrid.CurrentCell = nestGrid.Rows[e.RowIndex].Cells[0];
|
|
};
|
|
nestGrid.SelectionChanged += (_, _) =>
|
|
{
|
|
UpdateCommands();
|
|
ScheduleDetails();
|
|
};
|
|
|
|
var menu = new ContextMenuStrip();
|
|
openMenuItem = new ToolStripMenuItem("Open", null, (_, _) => OpenSelected()) { ShortcutKeyDisplayString = "Enter" };
|
|
deleteMenuItem = new ToolStripMenuItem("Delete...", null, async (_, _) => await DeleteSelectedAsync());
|
|
var refreshMenuItem = new ToolStripMenuItem("Refresh", null, async (_, _) => await RunAsync(session.RefreshAsync));
|
|
menu.Items.AddRange(new ToolStripItem[] { openMenuItem, deleteMenuItem, new ToolStripSeparator(), refreshMenuItem });
|
|
nestGrid.ContextMenuStrip = menu;
|
|
|
|
// Details of the highlighted nest.
|
|
platesGrid = CreateGrid();
|
|
platesGrid.RowHeadersVisible = false;
|
|
AddColumn(platesGrid, "Number", "#", 50, typeof(int), alignRight: true);
|
|
AddColumn(platesGrid, "Duplicates", "Duplicates", 90, typeof(int), alignRight: true);
|
|
AddColumn(platesGrid, PlateSizeColumn, "Plate size", 140, typeof(string));
|
|
AddColumn(platesGrid, "Parts", "Parts", 70, typeof(int), alignRight: true);
|
|
AddColumn(platesGrid, "Drawings", "Drawings", 80, typeof(int), alignRight: true);
|
|
AddColumn(platesGrid, "Utilization", "Utilization", 90, typeof(double), alignRight: true, format: "P1");
|
|
platesGrid.SelectionChanged += (_, _) =>
|
|
{
|
|
// Read the selected row: while this event runs, CurrentRow still names the
|
|
// previously current row.
|
|
if (SelectedPlateRow() is var row and >= 0)
|
|
ShowPlate(row);
|
|
};
|
|
|
|
drawingsGrid = CreateGrid();
|
|
drawingsGrid.RowHeadersVisible = false;
|
|
AddColumn(drawingsGrid, "Drawing", "Drawing", 220, typeof(string));
|
|
AddColumn(drawingsGrid, "Customer", "Customer", 160, typeof(string));
|
|
AddColumn(drawingsGrid, "Required", "Required", 80, typeof(int), alignRight: true);
|
|
AddColumn(drawingsGrid, "Nested", "Nested", 80, typeof(int), alignRight: true);
|
|
AddColumn(drawingsGrid, "Remaining", "Remaining", 90, typeof(int), alignRight: true);
|
|
AddColumn(drawingsGrid, AreaColumn, "Area", 100, typeof(double), alignRight: true, format: "0.###");
|
|
|
|
detailsTabs = new TabControl { Dock = DockStyle.Fill };
|
|
detailsTabs.TabPages.Add(CreateTab("Plates", platesGrid));
|
|
detailsTabs.TabPages.Add(CreateTab("Drawings", drawingsGrid));
|
|
|
|
// Preview of one plate beside the tables; the buttons below it step through the plates.
|
|
platePreview = new PlateView
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
AllowSelect = false,
|
|
AllowDrop = false,
|
|
DrawOrigin = false,
|
|
Cursor = Cursors.Default,
|
|
TabStop = false,
|
|
Visible = false,
|
|
};
|
|
platePreview.SizeChanged += (_, _) =>
|
|
{
|
|
if (platePreview.Visible)
|
|
platePreview.ZoomToFit();
|
|
};
|
|
previousPlateButton = new Button { Text = "\u25C0", Width = 32, Enabled = false, AccessibleName = "Previous plate" };
|
|
previousPlateButton.Click += (_, _) => ShowPlate(previewIndex - 1);
|
|
nextPlateButton = new Button { Text = "\u25B6", Width = 32, Enabled = false, AccessibleName = "Next plate" };
|
|
nextPlateButton.Click += (_, _) => ShowPlate(previewIndex + 1);
|
|
plateLabel = new Label { Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleCenter, AutoEllipsis = true };
|
|
|
|
var plateBar = new TableLayoutPanel { Dock = DockStyle.Bottom, AutoSize = true, ColumnCount = 3, RowCount = 1 };
|
|
plateBar.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
|
plateBar.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
|
plateBar.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
|
plateBar.Controls.Add(previousPlateButton, 0, 0);
|
|
plateBar.Controls.Add(plateLabel, 1, 0);
|
|
plateBar.Controls.Add(nextPlateButton, 2, 0);
|
|
|
|
detailsSplit = new SplitContainer
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
Orientation = Orientation.Vertical,
|
|
SplitterWidth = 6,
|
|
};
|
|
detailsSplit.Panel1.Controls.Add(detailsTabs);
|
|
detailsSplit.Panel2.Controls.Add(platePreview);
|
|
detailsSplit.Panel2.Controls.Add(plateBar);
|
|
|
|
detailsStatus = new Label
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
TextAlign = ContentAlignment.MiddleLeft,
|
|
ForeColor = SystemColors.GrayText,
|
|
AutoEllipsis = true,
|
|
};
|
|
var detailsHeader = new Panel { Dock = DockStyle.Top, Height = 32 };
|
|
detailsHeader.Controls.Add(detailsStatus);
|
|
detailsHeader.Controls.Add(SectionLabel("Details", DockStyle.Left));
|
|
|
|
split = new SplitContainer
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
Orientation = Orientation.Horizontal,
|
|
SplitterWidth = 6,
|
|
};
|
|
split.Panel1.Controls.Add(nestGrid);
|
|
split.Panel1.Controls.Add(SectionLabel("Nests", DockStyle.Top));
|
|
split.Panel2.Controls.Add(detailsSplit);
|
|
split.Panel2.Controls.Add(detailsHeader);
|
|
|
|
// Find bar and paging.
|
|
searchBox = new TextBox
|
|
{
|
|
Width = 320,
|
|
MaxLength = NestQuery.MaxSearchLength,
|
|
PlaceholderText = "Name, customer, material, made by, comment or status",
|
|
};
|
|
searchTimer = new Timer { Interval = 300 };
|
|
searchTimer.Tick += async (_, _) =>
|
|
{
|
|
searchTimer.Stop();
|
|
await RunAsync(() => session.SetSearchAsync(searchBox.Text));
|
|
};
|
|
searchBox.TextChanged += (_, _) =>
|
|
{
|
|
searchTimer.Stop();
|
|
searchTimer.Start();
|
|
};
|
|
detailsTimer = new Timer { Interval = 250 };
|
|
detailsTimer.Tick += async (_, _) =>
|
|
{
|
|
detailsTimer.Stop();
|
|
if (SelectedNestId() is Guid id)
|
|
await ShowDetailsAsync(id);
|
|
};
|
|
|
|
var refreshButton = new Button { Text = "Refresh", AutoSize = true };
|
|
refreshButton.Click += async (_, _) => await RunAsync(session.RefreshAsync);
|
|
previousButton = new Button { Text = "\u25C0", Width = 32, Enabled = false };
|
|
previousButton.Click += async (_, _) => await RunAsync(session.PreviousPageAsync);
|
|
nextButton = new Button { Text = "\u25B6", Width = 32, Enabled = false };
|
|
nextButton.Click += async (_, _) => await RunAsync(session.NextPageAsync);
|
|
summaryLabel = new Label { AutoSize = true, Margin = new Padding(12, 8, 3, 0) };
|
|
|
|
var findBar = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill, WrapContents = false };
|
|
findBar.Controls.Add(new Label { Text = "Find:", AutoSize = true, Margin = new Padding(3, 8, 0, 0) });
|
|
findBar.Controls.Add(searchBox);
|
|
findBar.Controls.Add(refreshButton);
|
|
findBar.Controls.Add(previousButton);
|
|
findBar.Controls.Add(nextButton);
|
|
findBar.Controls.Add(summaryLabel);
|
|
|
|
var bottom = new TableLayoutPanel
|
|
{
|
|
Dock = DockStyle.Bottom,
|
|
AutoSize = true,
|
|
ColumnCount = 1,
|
|
Padding = new Padding(3, 3, 3, 6),
|
|
};
|
|
bottom.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
|
bottom.Controls.Add(findBar);
|
|
|
|
Controls.Add(split);
|
|
Controls.Add(bottom);
|
|
|
|
Load += (_, _) =>
|
|
{
|
|
// Minimum sizes are applied once the container has its real height; setting them
|
|
// on the default-sized container throws.
|
|
split.Panel1MinSize = 120;
|
|
split.Panel2MinSize = 120;
|
|
split.SplitterDistance = System.Math.Max(split.Panel1MinSize, split.Height * 3 / 5);
|
|
detailsSplit.Panel1MinSize = 240;
|
|
detailsSplit.Panel2MinSize = 160;
|
|
detailsSplit.SplitterDistance = System.Math.Max(detailsSplit.Panel1MinSize, detailsSplit.Width * 3 / 5);
|
|
};
|
|
Shown += async (_, _) =>
|
|
{
|
|
nestGrid.Focus();
|
|
await RunAsync(session.RefreshAsync);
|
|
};
|
|
UpdateCommands();
|
|
}
|
|
|
|
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
|
{
|
|
switch (keyData)
|
|
{
|
|
case Keys.Enter when searchBox.Focused:
|
|
// Run the typed search now instead of opening a row from the previous results.
|
|
searchTimer.Stop();
|
|
_ = RunAsync(() => session.SetSearchAsync(searchBox.Text));
|
|
nestGrid.Focus();
|
|
return true;
|
|
case Keys.Enter:
|
|
OpenSelected();
|
|
return true;
|
|
case Keys.Escape:
|
|
DialogResult = DialogResult.Cancel;
|
|
return true;
|
|
}
|
|
|
|
return base.ProcessCmdKey(ref msg, keyData);
|
|
}
|
|
|
|
private static DataGridView CreateGrid()
|
|
{
|
|
var grid = new DataGridView
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
ReadOnly = true,
|
|
AllowUserToAddRows = false,
|
|
AllowUserToDeleteRows = false,
|
|
AllowUserToResizeRows = false,
|
|
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
|
|
MultiSelect = false,
|
|
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None,
|
|
BackgroundColor = Color.White,
|
|
BorderStyle = BorderStyle.FixedSingle,
|
|
GridColor = Color.Silver,
|
|
EnableHeadersVisualStyles = false,
|
|
ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize,
|
|
RowHeadersWidthSizeMode = DataGridViewRowHeadersWidthSizeMode.DisableResizing,
|
|
StandardTab = true,
|
|
};
|
|
grid.RowTemplate.Height = 22;
|
|
grid.DefaultCellStyle.SelectionBackColor = Color.Black;
|
|
grid.DefaultCellStyle.SelectionForeColor = Color.White;
|
|
grid.ColumnHeadersDefaultCellStyle.BackColor = SystemColors.Control;
|
|
grid.ColumnHeadersDefaultCellStyle.SelectionBackColor = SystemColors.Control;
|
|
grid.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter;
|
|
grid.RowHeadersDefaultCellStyle.BackColor = SystemColors.Control;
|
|
grid.RowHeadersDefaultCellStyle.SelectionBackColor = Color.Black;
|
|
grid.RowHeadersDefaultCellStyle.SelectionForeColor = Color.White;
|
|
grid.RowHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight;
|
|
return grid;
|
|
}
|
|
|
|
private static DataGridViewTextBoxColumn AddColumn(
|
|
DataGridView grid,
|
|
string name,
|
|
string header,
|
|
int width,
|
|
Type valueType,
|
|
bool alignRight = false,
|
|
string format = null)
|
|
{
|
|
var column = new DataGridViewTextBoxColumn
|
|
{
|
|
Name = name,
|
|
HeaderText = header,
|
|
Width = width,
|
|
ValueType = valueType,
|
|
};
|
|
if (alignRight)
|
|
column.DefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleRight;
|
|
if (format != null)
|
|
column.DefaultCellStyle.Format = format;
|
|
grid.Columns.Add(column);
|
|
return column;
|
|
}
|
|
|
|
private void AddNestColumn(string header, NestSortField sortField, int width, bool alignRight = false)
|
|
{
|
|
// Cells hold display text: the server has already sorted the page.
|
|
var column = AddColumn(nestGrid, header, header, width, typeof(string), alignRight);
|
|
column.SortMode = DataGridViewColumnSortMode.Programmatic;
|
|
column.Tag = sortField;
|
|
}
|
|
|
|
private static TabPage CreateTab(string text, Control content)
|
|
{
|
|
var page = new TabPage(text) { Padding = new Padding(0) };
|
|
page.Controls.Add(content);
|
|
return page;
|
|
}
|
|
|
|
private static Label SectionLabel(string text, DockStyle dock) =>
|
|
new()
|
|
{
|
|
Text = text,
|
|
Font = SectionFont,
|
|
Dock = dock,
|
|
AutoSize = dock == DockStyle.Left,
|
|
Height = 32,
|
|
TextAlign = ContentAlignment.MiddleLeft,
|
|
Padding = new Padding(0, 0, 12, 0),
|
|
};
|
|
|
|
/// <summary>
|
|
/// Sends one browse request and renders its page. A request superseded by a newer one
|
|
/// renders nothing; a failure clears the rows and shows the error in the summary line.
|
|
/// Navigation follows only the latest request (<see cref="NestBrowseSession.IsLoading"/>).
|
|
/// </summary>
|
|
private async Task RunAsync(Func<Task<bool>> request)
|
|
{
|
|
summaryLabel.Text = LoadingText;
|
|
try
|
|
{
|
|
var started = request();
|
|
UpdateNavigation();
|
|
if (await started && !IsDisposed)
|
|
Populate(session.Page);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!IsDisposed)
|
|
{
|
|
nestGrid.Rows.Clear();
|
|
summaryLabel.Text = $"Could not load nests from the server: {ex.Message}";
|
|
UpdateTitle("");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (!IsDisposed)
|
|
{
|
|
UpdateNavigation();
|
|
// A request that was declined or superseded renders nothing of its own.
|
|
if (!session.IsLoading && summaryLabel.Text == LoadingText)
|
|
summaryLabel.Text = session.Summary;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateNavigation()
|
|
{
|
|
previousButton.Enabled = !session.IsLoading && session.CanGoPrevious;
|
|
nextButton.Enabled = !session.IsLoading && session.CanGoNext;
|
|
}
|
|
|
|
private void UpdateCommands()
|
|
{
|
|
var hasSelection = SelectedNestId() != null;
|
|
openMenuItem.Enabled = hasSelection;
|
|
deleteMenuItem.Enabled = hasSelection && !deleting;
|
|
}
|
|
|
|
private void UpdateTitle(string summary)
|
|
{
|
|
var parts = new[] { WindowTitle, serverUrl, summary };
|
|
Text = string.Join(" ", Array.FindAll(parts, part => part.Length > 0));
|
|
}
|
|
|
|
private void Populate(NestPage page)
|
|
{
|
|
nestGrid.Rows.Clear();
|
|
for (var i = 0; i < page.Items.Count; i++)
|
|
{
|
|
var record = page.Items[i];
|
|
var index = nestGrid.Rows.Add(
|
|
record.Name,
|
|
FormatStatus(record.Status),
|
|
FormatDate(record.DateModified),
|
|
record.PlateCount.ToString("N0", CultureInfo.CurrentCulture),
|
|
record.PartCount.ToString("N0", CultureInfo.CurrentCulture),
|
|
record.Comments,
|
|
record.Customer,
|
|
record.MadeBy,
|
|
record.Material,
|
|
record.Thickness.ToString("0.####", CultureInfo.CurrentCulture),
|
|
FormatDate(record.DateCreated),
|
|
FormatDate(record.SavedAt),
|
|
FormatFileSize(record.FileSize),
|
|
record.Id
|
|
);
|
|
// Row numbers count through the whole filtered list, not just this page.
|
|
nestGrid.Rows[index].HeaderCell.Value = (page.Offset + i + 1).ToString("N0", CultureInfo.CurrentCulture);
|
|
}
|
|
|
|
foreach (DataGridViewColumn column in nestGrid.Columns)
|
|
{
|
|
column.HeaderCell.SortGlyphDirection = column.Tag is NestSortField field && field == session.Sort
|
|
? session.Descending ? SortOrder.Descending : SortOrder.Ascending
|
|
: SortOrder.None;
|
|
}
|
|
|
|
if (nestGrid.Rows.Count > 0 && nestGrid.CurrentCell == null)
|
|
nestGrid.CurrentCell = nestGrid.Rows[0].Cells[0];
|
|
|
|
summaryLabel.Text = session.Summary;
|
|
UpdateTitle(session.Summary);
|
|
}
|
|
|
|
private static string FormatStatus(NestStatus status) => status switch
|
|
{
|
|
NestStatus.Quote => "Quote",
|
|
NestStatus.ToBeCut => "To Be Cut",
|
|
NestStatus.HasBeenCut => "Has Been Cut",
|
|
_ => status.ToString(),
|
|
};
|
|
|
|
private static string FormatDate(DateTime value) =>
|
|
value == default ? "" : value.ToString("g", CultureInfo.CurrentCulture);
|
|
|
|
private static string FormatFileSize(long bytes) =>
|
|
$"{System.Math.Ceiling(bytes / 1024.0).ToString("N0", CultureInfo.CurrentCulture)} KB";
|
|
|
|
private Guid? SelectedNestId() =>
|
|
nestGrid.SelectedRows.Count > 0 && nestGrid.SelectedRows[0].Cells[IdColumn].Value is Guid id ? id : null;
|
|
|
|
/// <summary>
|
|
/// Shows the highlighted nest's details after a short pause, so arrowing through the
|
|
/// list downloads only the nest the highlight settles on.
|
|
/// </summary>
|
|
private void ScheduleDetails()
|
|
{
|
|
detailsTimer.Stop();
|
|
var id = SelectedNestId();
|
|
if (id == null)
|
|
{
|
|
details.Clear();
|
|
ShowDetailsMessage("");
|
|
return;
|
|
}
|
|
|
|
if (id == details.NestId)
|
|
{
|
|
// Back on the nest already shown or loading (for example after arrowing away and
|
|
// back before the pause ended): keep that result rather than downloading it again.
|
|
if (details.Details != null)
|
|
{
|
|
PopulateDetails(details.Details);
|
|
return;
|
|
}
|
|
|
|
if (details.IsLoading)
|
|
{
|
|
ShowDetailsMessage(LoadingDetailsText);
|
|
return;
|
|
}
|
|
}
|
|
|
|
ShowDetailsMessage(LoadingDetailsText);
|
|
detailsTimer.Start();
|
|
}
|
|
|
|
private async Task ShowDetailsAsync(Guid id)
|
|
{
|
|
try
|
|
{
|
|
if (await details.LoadAsync(id) && !IsDisposed)
|
|
PopulateDetails(details.Details);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!IsDisposed)
|
|
ShowDetailsMessage($"Could not load details: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>Downloads one nest's archive and reads its plates and drawings off the UI thread.</summary>
|
|
private async Task<NestDetails> LoadDetailsAsync(Guid id, CancellationToken cancellationToken)
|
|
{
|
|
var file = await repository.GetFileAsync(id, cancellationToken)
|
|
?? throw new InvalidOperationException("This nest no longer exists on the server.");
|
|
return await Task.Run(
|
|
() =>
|
|
{
|
|
using var stream = new MemoryStream(file);
|
|
return NestDetails.FromNest(new NestReader(stream).Read());
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
private void ShowDetailsMessage(string message)
|
|
{
|
|
previewPlates = Array.Empty<Plate>();
|
|
previewIndex = -1;
|
|
platePreview.Visible = false;
|
|
plateLabel.Text = "";
|
|
previousPlateButton.Enabled = false;
|
|
nextPlateButton.Enabled = false;
|
|
|
|
platesGrid.Rows.Clear();
|
|
drawingsGrid.Rows.Clear();
|
|
detailsStatus.Text = message;
|
|
}
|
|
|
|
private void PopulateDetails(NestDetails nestDetails)
|
|
{
|
|
var unit = nestDetails.Units == Units.Millimeters ? "mm" : "in";
|
|
platesGrid.Columns[PlateSizeColumn].HeaderText = $"Plate size ({unit})";
|
|
drawingsGrid.Columns[AreaColumn].HeaderText = $"Area ({unit}\u00B2)";
|
|
|
|
ShowDetailsMessage(
|
|
$"{nestDetails.Plates.Count:N0} plate(s), {nestDetails.Drawings.Count:N0} drawing(s)");
|
|
previewPlates = nestDetails.PlateLayouts;
|
|
foreach (var plate in nestDetails.Plates)
|
|
{
|
|
platesGrid.Rows.Add(
|
|
plate.Number,
|
|
plate.Duplicates,
|
|
$"{plate.Width.ToString("0.###", CultureInfo.CurrentCulture)} x {plate.Length.ToString("0.###", CultureInfo.CurrentCulture)}",
|
|
plate.PartCount,
|
|
plate.DrawingCount,
|
|
plate.Utilization);
|
|
}
|
|
|
|
foreach (var drawing in nestDetails.Drawings)
|
|
{
|
|
drawingsGrid.Rows.Add(
|
|
drawing.Name,
|
|
drawing.Customer,
|
|
drawing.Required,
|
|
drawing.Nested,
|
|
drawing.Remaining,
|
|
drawing.Area);
|
|
}
|
|
|
|
ShowPlate(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows one plate of the highlighted nest in the preview and highlights its row in the
|
|
/// Plates table. Plate buttons and row selection both come through here.
|
|
/// </summary>
|
|
private void ShowPlate(int index)
|
|
{
|
|
if (index < 0 || index >= previewPlates.Count)
|
|
return;
|
|
|
|
if (index != previewIndex)
|
|
{
|
|
previewIndex = index;
|
|
platePreview.Plate = previewPlates[index];
|
|
platePreview.Visible = true;
|
|
platePreview.ZoomToFit();
|
|
}
|
|
|
|
plateLabel.Text = $"Plate {index + 1:N0} of {previewPlates.Count:N0}";
|
|
previousPlateButton.Enabled = index > 0;
|
|
nextPlateButton.Enabled = index < previewPlates.Count - 1;
|
|
if (index < platesGrid.Rows.Count && SelectedPlateRow() != index)
|
|
platesGrid.CurrentCell = platesGrid.Rows[index].Cells[0];
|
|
}
|
|
|
|
private int SelectedPlateRow() => platesGrid.SelectedRows.Count > 0 ? platesGrid.SelectedRows[0].Index : -1;
|
|
|
|
private void OpenSelected()
|
|
{
|
|
if (SelectedNestId() is not Guid id)
|
|
return;
|
|
SelectedId = id;
|
|
DialogResult = DialogResult.OK;
|
|
}
|
|
|
|
private async Task DeleteSelectedAsync()
|
|
{
|
|
if (deleting || SelectedNestId() is not Guid id)
|
|
return;
|
|
|
|
var name = nestGrid.SelectedRows[0].Cells[0].Value?.ToString() ?? "";
|
|
var confirm = MessageBox.Show(
|
|
this,
|
|
$"Delete '{name}' from the server? This cannot be undone.",
|
|
"Delete Nest",
|
|
MessageBoxButtons.YesNo,
|
|
MessageBoxIcon.Warning
|
|
);
|
|
if (confirm != DialogResult.Yes)
|
|
return;
|
|
|
|
deleting = true;
|
|
UpdateCommands();
|
|
try
|
|
{
|
|
await repository.DeleteAsync(id);
|
|
if (!IsDisposed)
|
|
await RunAsync(session.RefreshAsync);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!IsDisposed)
|
|
MessageBox.Show(
|
|
this,
|
|
$"Could not delete the nest: {ex.Message}",
|
|
"Delete Nest",
|
|
MessageBoxButtons.OK,
|
|
MessageBoxIcon.Error
|
|
);
|
|
}
|
|
finally
|
|
{
|
|
deleting = false;
|
|
if (!IsDisposed)
|
|
UpdateCommands();
|
|
}
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
searchTimer.Dispose();
|
|
detailsTimer.Dispose();
|
|
session.Dispose();
|
|
details.Dispose();
|
|
}
|
|
|
|
base.Dispose(disposing);
|
|
}
|
|
}
|