mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-15 08:55:37 -05:00
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.
902 lines
36 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|