Files
OpenNest/OpenNest.Avalonia/MainWindow.axaml.cs
T
aj e2a27df7ef feat(avalonia): open a new nest on startup and convert the old nest template
Once the main window opens, the Avalonia app runs the shared start-up
steps as the WinForms main window does on load: a legacy nest template
(NestTemplatePath) becomes the nest defaults file, and Tools > Options >
Create a new nest on startup (on unless turned off) opens a new nest,
which until now that checkbox only saved.

A failed template conversion shows the WinForms "Nest Template" warning
through the queue the load warnings use; the new nest opens only once
every queued warning has closed. File > New's "Nest Defaults" warning
now goes through the same queue, so it never stacks on a load warning,
and the queue stops once the window closes. New beside WinForms, which
takes no nest paths on its command line and converts synchronously: no
new nest is added when a nest is already open by then (named on the
command line, or opened or dropped while the template converted), start-
up stops when the window closes (checked again just before the nest
number is taken) or asks about unsaved nests meanwhile, and --snapshot
runs neither step, so an unattended render changes no settings and takes
no nest number.
2026-11-14 19:32:24 -05:00

902 lines
36 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using OpenNest.Desktop;
using OpenNest.Engine;
using OpenNest.Engine.Jobs;
using OpenNest.Forms;
using OpenNest.IO;
namespace OpenNest.Avalonia
{
public partial class MainWindow : Window
{
private static readonly IBrush Accent = Brush.Parse("#3368A0");
private static readonly IBrush Muted = Brush.Parse("#6B7280");
private static readonly IBrush InactiveTab = Brush.Parse("#DDE1E5");
private readonly NestTabs tabs = new();
private readonly Dictionary<NestTab, NestView> views = new();
private string notice;
private CancellationTokenSource nesting;
private NestTab nestingTab;
private Task autoNest = Task.CompletedTask;
private bool closeConfirmed;
// A File > New waiting on the machines or the nest number.
private bool creatingNest;
private bool snapshotMode;
// A save or export waiting on its file picker or write; another one waits for it.
private bool writingFile;
// True while Exit asks about unsaved nests, so a second close request is ignored.
private bool askingToExit;
// Load and start-up warnings shown one dialog at a time, once the window is open.
private readonly Queue<(string Title, string Message)> pendingWarnings = new();
private bool showingWarnings;
private Task warningsShown = Task.CompletedTask;
// The window is closing (its close went ahead) or closed.
private bool closed;
public MainWindow()
{
InitializeComponent();
// As the desktop app: plug-in engines first, then the saved engine selection.
NestingEngineRegistry.LoadPlugins(Path.Combine(AppContext.BaseDirectory, "Engines"));
var engineWarning = EngineSelection.LoadSavedSelection();
if (!string.IsNullOrEmpty(engineWarning))
notice = engineWarning;
// Post processors from Posts/ (their materials registered), then the machines,
// reported in the status line as the WinForms app reports them in dialogs.
if (DesktopSettings.LoadPosts(Path.Combine(AppContext.BaseDirectory, "Posts")) is { } postWarning)
notice = notice == null ? postWarning : $"{notice} {postWarning}";
MachinesLoaded = LoadMachinesAsync();
// The colour scheme and fill strategies chosen in Tools > Options (either app's), from
// the shared user settings; applying a scheme later recolours the open nests.
var userSettings = Data.UserSettingsFile.Read(Data.UserSettingsFile.DefaultPath);
ColorSchemeRegistry.ApplySaved(userSettings);
FillStrategyOptions.ApplySaved(userSettings);
ColorSchemeRegistry.Applied += OnColorSchemeApplied;
Closed += (_, _) => ColorSchemeRegistry.Applied -= OnColorSchemeApplied;
Closed += (_, _) => closed = true;
tabs.Changed += (_, _) => ShowActiveTab();
KeyBindings.Add(new KeyBinding
{
Gesture = new KeyGesture(Key.N, KeyModifiers.Control),
Command = new Command(() => OnNew(null, null)),
});
KeyBindings.Add(new KeyBinding
{
Gesture = new KeyGesture(Key.O, KeyModifiers.Control),
Command = new Command(() => OnOpen(null, null)),
});
KeyBindings.Add(new KeyBinding
{
Gesture = new KeyGesture(Key.S, KeyModifiers.Control),
Command = new Command(() => OnSave(null, null)),
});
KeyBindings.Add(new KeyBinding
{
Gesture = new KeyGesture(Key.Q, KeyModifiers.Control),
Command = new Command(() => OnExit(null, null)),
});
KeyBindings.Add(new KeyBinding
{
Gesture = new KeyGesture(Key.A, KeyModifiers.Control),
Command = new Command(() => OnSelectAll(null, null)),
});
KeyBindings.Add(new KeyBinding { Gesture = new KeyGesture(Key.PageUp), Command = new Command(() => StepPlate(-1)) });
KeyBindings.Add(new KeyBinding { Gesture = new KeyGesture(Key.PageDown), Command = new Command(() => StepPlate(1)) });
// Ctrl+Tab before focus navigation takes the Tab key.
AddHandler(KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel);
// .nest files dropped anywhere on the window, the nest view included, open in tabs.
DragDrop.SetAllowDrop(this, true);
AddHandler(DragDrop.DragEnterEvent, OnDragOver);
AddHandler(DragDrop.DragOverEvent, OnDragOver);
AddHandler(DragDrop.DropEvent, OnDrop);
Closing += OnWindowClosing;
#if DEBUG
StartUnrecordedChangeCheck();
#endif
Opened += (_, _) =>
{
if (!snapshotMode)
_ = ShowLoadWarningsAsync();
};
ShowActiveTab();
}
// Null while a new tab's view is still being created.
private NestView ActiveView =>
tabs.Active != null && views.TryGetValue(tabs.Active, out var view) ? view : null;
/// <summary>Opens the nest in a new tab, or switches to its tab if open.</summary>
public void OpenNest(string path)
{
try
{
IReadOnlyList<string> warnings = Array.Empty<string>();
var tab = tabs.Open(path, file =>
{
var reader = new NestReader(file);
var document = new Document { Nest = reader.Read() };
warnings = reader.Warnings;
// The view follows the application's scheme, not the one the file was saved under.
ColorSchemeRegistry.RecolorDrawings(document.Nest);
return document;
});
if (!views.ContainsKey(tab))
AddView(tab);
if (warnings.Count > 0)
{
var count = warnings.Count == 1
? "1 load warning"
: $"{warnings.Count} load warnings";
ShowNotice($"{tab.Name}: {count}");
pendingWarnings.Enqueue(("Nest Load Warnings",
$"{tab.Name}:\n\n" + string.Join(Environment.NewLine, warnings)));
if (IsVisible && !snapshotMode)
_ = ShowLoadWarningsAsync();
}
}
catch (Exception ex)
{
ShowNotice($"Could not open {Path.GetFileName(path)}: {ex.Message}");
}
}
/// <summary>
/// Opens a new nest from the nest defaults, the default machine with its clamps and
/// support grid, and the year's next name, in a new tab, as File > New does in WinForms.
/// </summary>
public async Task NewNestAsync()
{
// One at a time, so two nests never take the same number.
if (creatingNest)
return;
creatingNest = true;
NewNest created;
try
{
// The machines finish loading first; reading them and the nest number may wait
// on the nest server, so neither runs on the UI thread.
await MachinesLoaded;
created = await Task.Run(() => DesktopSettings.NewNests.CreateAsync());
}
catch (Exception ex)
{
ShowNotice($"Could not create a new nest: {ex.Message}");
return;
}
finally
{
creatingNest = false;
}
AddView(tabs.Add(new Document { Nest = created.Nest }));
if (created.Warning != null)
{
// Through the queue, so it never stacks on a load warning already showing.
pendingWarnings.Enqueue(("Nest Defaults", created.Warning));
await ShowLoadWarningsAsync();
}
}
private void AddView(NestTab tab)
{
var view = new NestView(tab);
RestoreViewToggles(view);
view.ViewChanged += (_, _) => { if (view == ActiveView) UpdateStatus(); };
views[tab] = view;
WatchHistory(tab);
notice = null;
ShowActiveTab();
}
/// <summary>
/// Restores the window's size, position and maximized state from the user settings the
/// WinForms app shares, and saves them on close. Not used for --snapshot, whose image
/// must not depend on the user's settings.
/// </summary>
public void RememberPlacement() =>
WindowPlacementBinding.Remember(
this, WindowPlacement.MainWindow, Data.UserSettingsFile.DefaultPath);
/// <summary>
/// Once the window opens, converts a legacy nest template into the nest defaults and
/// opens a new nest when Tools > Options asks for one, as the WinForms main window does
/// on load. No new nest beside nests already open (from the command line, or opened or
/// dropped while the template converted). Not used for --snapshot, which must neither
/// change settings nor take a nest number.
/// </summary>
public void RunStartupOnOpen() => Opened += async (_, _) => await RunStartupAsync();
// The window is closing, or asking about unsaved nests before exit: start-up stops.
private bool StartupInterrupted => closed || closeConfirmed || askingToExit;
private async Task RunStartupAsync()
{
DesktopStartupResult startup;
try
{
// Reads the template's nest and writes the defaults file: off the UI thread.
startup = await Task.Run(() => DesktopStartup.Run(
Data.UserSettingsFile.DefaultPath, Data.NestDefaults.DefaultPath,
Engine.BestFit.BestFitCacheStore.Shared));
}
catch (Exception ex)
{
ShowNotice(
$"Start-up could not finish: {ex.Message} Use File > New to start a nest.");
return;
}
// On the UI thread, as the window's other user-settings writers are.
if (startup.ClearTemplateSetting)
DesktopStartup.ClearTemplateSetting(Data.UserSettingsFile.DefaultPath);
if (StartupInterrupted)
return;
if (startup.TemplateWarning != null)
{
pendingWarnings.Enqueue(("Nest Template", startup.TemplateWarning));
// Waits for every queued warning, including ones already showing.
await ShowLoadWarningsAsync();
}
if (!startup.CreateNewNest)
return;
// The machines load first, so the check below is close to the nest-number request.
await MachinesLoaded;
// Closed meanwhile (a window closed in code takes its dialogs with it), or nests
// opened from the command line, or opened or dropped meanwhile: no new nest.
if (StartupInterrupted || tabs.Tabs.Count > 0)
return;
await NewNestAsync();
}
/// <summary>Runs Auto Nest once the window opens, for --auto-nest.</summary>
public void AutoNestOnOpen() => Opened += (_, _) => autoNest = RunAutoNestAsync();
public void SaveSnapshotAndClose(string path, int plateNumber)
{
// No dialogs: the image is taken and the window closes unattended.
snapshotMode = true;
UseDefaultViewToggles();
Opened += async (_, _) =>
{
ActiveView?.ShowPlate(plateNumber - 1);
await autoNest;
await Task.Delay(500);
var size = new PixelSize((int)Bounds.Width, (int)Bounds.Height);
using var bitmap = new global::Avalonia.Media.Imaging.RenderTargetBitmap(size);
bitmap.Render(this);
bitmap.Save(path);
// The status line beside the image, so headless checks can read it.
File.WriteAllText(path + ".txt", Status.Text);
closeConfirmed = true;
Close();
};
}
private void ShowActiveTab()
{
var view = ActiveView;
NestHost.Content = view;
EmptyHint.IsVisible = view == null;
CloseMenuItem.IsEnabled = view != null;
SaveMenuItem.IsEnabled = view != null;
SaveAsMenuItem.IsEnabled = view != null;
ExportNestMenuItem.IsEnabled = view != null;
SaveButton.IsEnabled = view != null;
SaveAsButton.IsEnabled = view != null;
CloseAllMenuItem.IsEnabled = view != null;
UpdateExportCommands(view != null);
Title = view == null
? "OpenNest (Avalonia prototype)"
: $"{view.Tab.Name} - OpenNest (Avalonia prototype)";
RebuildTabStrip();
UpdateViewToggles();
// The Edit menu follows the active nest's history.
UpdateUndoMenu();
UpdateStatus();
}
private void RebuildTabStrip()
{
TabStrip.Children.Clear();
for (var i = 0; i < tabs.Tabs.Count; i++)
TabStrip.Children.Add(TabButton(tabs.Tabs[i], i == tabs.ActiveIndex));
var add = new Button
{
Content = "+",
FontSize = 15,
Padding = new Thickness(10, 2),
Margin = new Thickness(4, 4, 0, 0),
Background = Brushes.Transparent,
VerticalAlignment = VerticalAlignment.Top,
};
ToolTip.SetTip(add, "New nest");
add.Click += OnNew;
TabStrip.Children.Add(add);
}
private Control TabButton(NestTab tab, bool active)
{
var name = new TextBlock
{
Text = tab.Modified ? tab.Name + " \u2022" : tab.Name,
FontSize = 13,
FontWeight = active ? FontWeight.SemiBold : FontWeight.Normal,
Foreground = active ? Brushes.Black : Muted,
VerticalAlignment = VerticalAlignment.Center,
};
var close = new Button
{
Content = "\u2715",
FontSize = 10,
Padding = new Thickness(5, 1),
Background = Brushes.Transparent,
Foreground = Muted,
VerticalAlignment = VerticalAlignment.Center,
};
ToolTip.SetTip(close, "Close");
close.Click += async (_, e) =>
{
e.Handled = true;
await CloseTabAsync(tab);
};
var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
row.Children.Add(name);
row.Children.Add(close);
var border = new Border
{
Child = row,
Padding = new Thickness(14, 5, 6, 5),
Margin = new Thickness(0, 0, 2, 0),
CornerRadius = new CornerRadius(0, 0, 6, 6),
Background = active ? Brushes.White : InactiveTab,
BorderBrush = active ? Accent : Brushes.Transparent,
BorderThickness = new Thickness(0, 0, 0, 2),
Cursor = new Cursor(StandardCursorType.Hand),
};
ToolTip.SetTip(border, tab.Path);
border.PointerPressed += (_, e) =>
{
if (e.GetCurrentPoint(border).Properties.IsLeftButtonPressed)
tabs.Activate(tabs.Tabs.ToList().IndexOf(tab));
};
return border;
}
private async Task<bool> CloseTabAsync(NestTab tab)
{
if (tab == nestingTab)
{
ShowNotice($"Auto Nest is running on {tab.Name}. Stop it before closing the nest.");
return false;
}
if (writingFile)
{
ShowNotice("A nest is being saved or exported. Close the nest once that finishes.");
return false;
}
if (!await ConfirmUnsavedAsync(tab))
return false;
UnwatchHistory(tab);
tabs.Close(tab);
if (views.Remove(tab, out var closed))
SaveViewToggles(closed);
return true;
}
private async void OnCloseNest(object sender, RoutedEventArgs e)
{
if (tabs.Active != null)
await CloseTabAsync(tabs.Active);
}
private async void OnCloseAll(object sender, RoutedEventArgs e)
{
foreach (var tab in tabs.Tabs.Reverse().ToList())
{
if (!await CloseTabAsync(tab))
return;
}
}
private async void OnWindowClosing(object sender, WindowClosingEventArgs e)
{
if (closeConfirmed || (!tabs.AnyModified && !writingFile))
{
// Set before Avalonia closes the owned dialogs, whose awaiters then resume.
closed = true;
nesting?.Cancel();
SaveAllViewToggles();
return;
}
e.Cancel = true;
if (askingToExit)
return;
if (writingFile)
{
ShowNotice("A nest is being saved or exported. Exit again once that finishes.");
return;
}
// As closing each WinForms nest window: ask about every unsaved nest in turn,
// showing it; Cancel, or a save that does not happen, keeps the app open. The list
// is read again after each answer: Auto Nest can finish while a question is open.
askingToExit = true;
try
{
var answered = new HashSet<NestTab>();
while (tabs.Tabs.FirstOrDefault(t => t.Modified && !answered.Contains(t))
is { } tab)
{
tabs.Activate(tabs.Tabs.ToList().IndexOf(tab));
if (tab == nestingTab)
{
await ConfirmDialog.ShowAsync(this, "Exit OpenNest",
$"Auto Nest is running on {tab.Name}, which has unsaved changes. "
+ "Stop Auto Nest (Nest > Stop Auto Nest) or let it finish, then "
+ "exit again.");
return;
}
if (!await ConfirmUnsavedAsync(tab))
return;
answered.Add(tab);
}
}
finally
{
askingToExit = false;
}
closeConfirmed = true;
Close();
}
/// <summary>
/// For a nest with unsaved changes, asks Save / Don't Save / Cancel. True when it may
/// close: saved, or the user chose not to save.
/// </summary>
private async Task<bool> ConfirmUnsavedAsync(NestTab tab)
{
if (!tab.Modified)
return true;
var choice = await ConfirmDialog.ChooseAsync(this, "Unsaved Changes",
$"Save changes to '{tab.Name}' before closing?", "Save", "Don't Save");
return choice switch
{
0 => await SaveAsync(tab, askForFile: false),
1 => true,
_ => false,
};
}
private async void OnSave(object sender, RoutedEventArgs e)
{
if (tabs.Active != null)
await SaveAsync(tabs.Active, askForFile: false);
}
private async void OnSaveAs(object sender, RoutedEventArgs e)
{
if (tabs.Active != null)
await SaveAsync(tabs.Active, askForFile: true);
}
/// <summary>
/// Saves the nest to the file it was saved to this session, else (or for Save As) to a
/// file the user picks. True when it was saved.
/// </summary>
private async Task<bool> SaveAsync(NestTab tab, bool askForFile)
{
if (!CanWrite(tab, "saving"))
return false;
writingFile = true;
try
{
var path = askForFile ? null : tab.SavePath;
path ??= await PickNestFileAsync(tab, "Save Nest As");
// Auto Nest may have started on it while the file picker was open.
if (path == null || IsNesting(tab, "saving"))
return false;
try
{
tabs.SaveAs(tab, path);
}
catch (SaveTargetRefusedException ex)
{
await ConfirmDialog.ShowAsync(
this, "Save Nest", $"The nest was not saved. {ex.Message}");
return false;
}
catch (Exception ex)
{
await ConfirmDialog.ShowAsync(this, "Save Nest",
"The nest was not saved. Any copy already at that path was left as it "
+ $"was.\n\n{path}\n\n{ex.Message}\n\nChoose File > Save As to save "
+ "it to another folder or name.");
return false;
}
ShowNotice($"Saved {tab.Name} to {path}");
return true;
}
finally
{
writingFile = false;
}
}
private async void OnExportNest(object sender, RoutedEventArgs e)
{
var tab = tabs.Active;
if (tab == null || !CanWrite(tab, "exporting"))
return;
writingFile = true;
try
{
var path = await PickNestFileAsync(tab, "Save Copy as .nest File");
if (path == null || IsNesting(tab, "exporting"))
return;
try
{
// Export never changes the nest's file, name or unsaved mark.
tabs.ExportCopy(tab, path);
ShowNotice($"Saved a copy to {path}");
}
catch (SaveTargetRefusedException ex)
{
await ConfirmDialog.ShowAsync(
this, "Save Copy", $"The copy was not saved. {ex.Message}");
}
catch (Exception ex)
{
await ConfirmDialog.ShowAsync(this, "Save Copy",
$"The copy was not saved.\n\n{path}\n\n{ex.Message}\n\nChoose "
+ "File > Save Copy as .nest File again to pick another folder or name.");
}
}
finally
{
writingFile = false;
}
}
// Whether the tab's nest can be written now: not while Auto Nest works on it, and one
// save or export at a time.
private bool CanWrite(NestTab tab, string doing)
{
if (IsNesting(tab, doing))
return false;
if (writingFile)
{
ShowNotice("A nest is already being saved or exported. "
+ $"Finish that before {doing}.");
return false;
}
return true;
}
private bool IsNesting(NestTab tab, string doing)
{
if (tab != nestingTab)
return false;
ShowNotice($"Auto Nest is running on {tab.Name}. "
+ $"Stop it or let it finish before {doing}.");
return true;
}
// A .nest file to write, named after the nest, starting in the folder of its file.
private async Task<string> PickNestFileAsync(NestTab tab, string title)
{
var folder = tab.Path != null ? Path.GetDirectoryName(tab.Path) : null;
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = title,
SuggestedFileName = tab.Name,
DefaultExtension = "nest",
ShowOverwritePrompt = true,
FileTypeChoices = new[] { NestFiles },
SuggestedStartLocation = folder != null
? await StorageProvider.TryGetFolderFromPathAsync(folder)
: null,
});
return file?.TryGetLocalPath();
}
private static readonly FilePickerFileType NestFiles =
new("Nest files") { Patterns = new[] { "*.nest" } };
/// <summary>Shows the queued warnings one at a time; finishes when the queue is empty,
/// also when called while they are already showing.</summary>
private Task ShowLoadWarningsAsync()
{
if (!showingWarnings)
warningsShown = ShowQueuedWarningsAsync();
return warningsShown;
}
private async Task ShowQueuedWarningsAsync()
{
showingWarnings = true;
try
{
// None once the window has closed: a dialog needs a live owner.
while (!closed && pendingWarnings.TryDequeue(out var item))
await ConfirmDialog.ShowAsync(this, item.Title, item.Message);
}
finally
{
showingWarnings = false;
}
}
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
if (ProcessUndoKeys(e))
{
e.Handled = true;
return;
}
if (e.Key == Key.Tab && e.KeyModifiers.HasFlag(KeyModifiers.Control))
{
StepNest(e.KeyModifiers.HasFlag(KeyModifiers.Shift) ? -1 : 1);
e.Handled = true;
}
}
private void OnNextNest(object sender, RoutedEventArgs e) => StepNest(1);
private void OnPreviousNest(object sender, RoutedEventArgs e) => StepNest(-1);
private void StepNest(int step)
{
var count = tabs.Tabs.Count;
if (count > 1)
tabs.Activate(((tabs.ActiveIndex + step) % count + count) % count);
}
private async void OnNew(object sender, RoutedEventArgs e) => await NewNestAsync();
private async void OnOpen(object sender, RoutedEventArgs e)
{
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Open Nest",
AllowMultiple = true,
FileTypeFilter = new[] { NestFiles },
});
foreach (var path in files.Select(f => f.TryGetLocalPath()).Where(p => p != null))
OpenNest(path);
}
// As the WinForms main window: a drop is taken only when it holds a .nest file.
private void OnDragOver(object sender, DragEventArgs e)
{
e.DragEffects = DroppedNestFiles.Any(DroppedPaths(e))
? DragDropEffects.Copy
: DragDropEffects.None;
e.Handled = true;
}
// Each dropped .nest file opens in a tab (or shows its tab); other files are ignored.
// As File > Open, this works while Auto Nest runs.
private void OnDrop(object sender, DragEventArgs e)
{
var paths = DroppedNestFiles.Select(DroppedPaths(e));
e.DragEffects = paths.Count > 0 ? DragDropEffects.Copy : DragDropEffects.None;
e.Handled = true;
foreach (var path in paths)
OpenNest(path);
}
private static IEnumerable<string> DroppedPaths(DragEventArgs e) =>
e.Data.GetFiles()?.Select(file => file.TryGetLocalPath())
?? Enumerable.Empty<string>();
private void OnExit(object sender, RoutedEventArgs e) => Close();
private void OnAutoNest(object sender, RoutedEventArgs e)
{
// Stop always cancels and discards the whole proposal.
if (nesting != null)
{
nesting.Cancel();
return;
}
autoNest = RunAutoNestAsync();
}
/// <summary>
/// Nests every drawing's remaining quantity of the active nest on sheets like its shown
/// plate, through the shared pipeline off the UI thread. Other nests can be opened and
/// viewed meanwhile; the nest being nested cannot be closed until it finishes.
/// </summary>
private async Task RunAutoNestAsync()
{
var view = ActiveView;
if (view == null || nesting != null)
return;
var tab = view.Tab;
var nest = tab.Document.Nest;
var engineName = EngineSelection.EngineName;
var template = view.PlateView.Plate
?? nest.Plates.FirstOrDefault()
?? nest.PlateDefaults.CreateNew();
var request = AutoNestJob.ForRemaining(nest, template, engineName);
if (request == null)
{
ShowNotice("Auto Nest: nothing has a remaining quantity to nest.");
return;
}
nesting = new CancellationTokenSource();
nestingTab = tab;
var token = nesting.Token;
SetNesting(true);
ShowNotice($"{engineName}: starting...");
var progress = new Progress<NestProgress>(p =>
{
if (!token.IsCancellationRequested)
ShowNotice(p.Description ?? $"{engineName}: plate {p.PlateNumber}");
});
try
{
var jobProgress = JobEngineNest.CreateProgress(engineName, progress);
var result = await Task.Run(() => NestPipeline.Run(request, jobProgress, token));
// Engines that ignore cancellation still have their proposal discarded.
token.ThrowIfCancellationRequested();
var firstNew = nest.Plates.Count(p => p.Parts.Count > 0);
// One undo step for the whole result, as in the WinForms app; the history change
// updates the tab's unsaved mark.
NestRenestResult kept;
using (var change = tab.Undo.BeginChange("Auto Nest"))
{
try
{
kept = AutoNestJob.Keep(nest, result);
}
catch
{
// A commit that failed part way is rolled back.
change.MarkChanged();
throw;
}
if (kept.Nested)
{
change.MarkChanged();
view.Refresh(firstNew);
}
change.Complete();
}
ShowNotice(kept.Nested
? $"{tab.Name}: {engineName}: {kept.Note}"
: AutoNestJob.RefusalReason(result));
}
catch (OperationCanceledException) when (token.IsCancellationRequested)
{
ShowNotice("Auto Nest stopped; nothing was changed.");
}
catch (Exception ex)
{
ShowNotice($"Auto Nest failed: {ex.Message}");
}
finally
{
nesting.Dispose();
nesting = null;
nestingTab = null;
SetNesting(false);
}
}
private void SetNesting(bool running)
{
AutoNestButton.Content = running ? "Stop" : "Auto Nest";
AutoNestMenuItem.Header = running ? "_Stop Auto Nest" : "_Auto Nest";
UpdateUndoMenu();
}
/// <summary>Completes once the machines are read (see <see cref="DesktopSettings.LoadMachinesAsync"/>).</summary>
internal Task MachinesLoaded { get; }
private async Task LoadMachinesAsync()
{
string warning;
try
{
warning = await DesktopSettings.LoadMachinesAsync();
}
catch (Exception ex)
{
warning = $"The machines could not be read: {ex.Message}";
}
if (warning != null)
ShowNotice(warning);
}
private void ShowNotice(string text)
{
notice = text;
UpdateStatus();
}
private void OnFit(object sender, RoutedEventArgs e) => ActiveView?.PlateView.ZoomToFit();
private void OnSelectAll(object sender, RoutedEventArgs e) => ActiveView?.PlateView.SelectAll();
private void OnPreviousPlate(object sender, RoutedEventArgs e) => StepPlate(-1);
private void OnNextPlate(object sender, RoutedEventArgs e) => StepPlate(1);
private void StepPlate(int step) => ActiveView?.StepPlate(step);
// Tools > Options. Settings for features the app does not have yet (Database mode, the
// update feed) are still saved, for the WinForms app and later milestones.
private async void OnOptions(object sender, RoutedEventArgs e) =>
await OptionsDialog.ShowAsync(this, Data.UserSettingsFile.DefaultPath);
// As the WinForms nest windows: every open nest takes the new scheme's drawing colours.
private void OnColorSchemeApplied(object sender, ColorScheme scheme)
{
foreach (var (tab, view) in views)
{
ColorSchemeRegistry.RecolorDrawings(tab.Document.Nest);
view.PlateView.InvalidateVisual();
}
}
private void UpdateStatus()
{
var view = ActiveView;
var canvas = view?.PlateView;
if (canvas?.Plate == null)
{
var empty = view == null
? "Create a new nest or open a .nest file to begin."
: $"{view.Tab.Name} No plates";
Status.Text = notice ?? empty;
return;
}
// The active tool's name and hint first, as the WinForms status label shows them.
var text = $"{canvas.Tools.Status} {view.Tab.Name} Plate {view.PlateIndex + 1} of {view.PlateCount}"
+ $" Zoom {canvas.Zoom:0.##} px/unit";
if (canvas.CursorWorld is { } at)
text += $" X {at.X:0.000} Y {at.Y:0.000}";
var selected = canvas.Selection.Parts.Where(canvas.Plate.Parts.Contains).ToList();
if (selected.Count > 0)
text += " " + Desktop.Interaction.SelectionStatus.Text(selected);
if (canvas.HoveredPart is { } part)
text += $" Over: {part.BaseDrawing.Name}";
if (notice != null)
text += $" | {notice}";
Status.Text = text;
}
private sealed class Command : System.Windows.Input.ICommand
{
private readonly Action action;
public Command(Action action) => this.action = action;
public event EventHandler CanExecuteChanged { add { } remove { } }
public bool CanExecute(object parameter) => true;
public void Execute(object parameter) => action();
}
}
}