mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-15 13:35:34 -05:00
Tools > Set Offset Increment (F5) and Set Rotation Increment (F6) open the WinForms SetValueForm's dialog: an "Increment :" box shown with four decimals and a Set button that Enter also presses; Esc or closing the window changes nothing. - The offset dialog has the "Calculated distance (selection size + part spacing)" check box, which turns the value box off; with it on, arrow keys step the selection by its size plus the part spacing. - The rotation increment goes up to 360 degrees; Shift+wheel turns by it and Ctrl+Shift+wheel by a tenth of it. - A typed value out of range is taken to the nearest limit and an emptied box keeps the last value it held, as in WinForms. Each nest keeps its own increments for the session, starting at 10, as each WinForms nest window does; they are not saved. With no nest open both commands are off (WinForms hides them).
952 lines
38 KiB
C#
952 lines
38 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)) });
|
|
KeyBindings.Add(new KeyBinding
|
|
{
|
|
Gesture = new KeyGesture(Key.F5),
|
|
Command = new Command(() => OnSetOffsetIncrement(null, null)),
|
|
});
|
|
KeyBindings.Add(new KeyBinding
|
|
{
|
|
Gesture = new KeyGesture(Key.F6),
|
|
Command = new Command(() => OnSetRotationIncrement(null, null)),
|
|
});
|
|
// 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;
|
|
SetOffsetIncrementMenuItem.IsEnabled = view != null;
|
|
SetRotationIncrementMenuItem.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;
|
|
// As WinForms behind its modal progress window: the nest's plate takes no edits
|
|
// while Auto Nest works on it.
|
|
var lockedView = views.GetValueOrDefault(tab);
|
|
if (lockedView != null)
|
|
lockedView.PlateView.EditsLocked = true;
|
|
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;
|
|
if (lockedView != null)
|
|
lockedView.PlateView.EditsLocked = false;
|
|
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 > Set Offset Increment (F5), as WinForms: how far an arrow key nudges the active
|
|
// nest's selection, or the calculated step (selection size plus part spacing). Each nest
|
|
// keeps its own increments for the session, as each WinForms nest window does.
|
|
private async void OnSetOffsetIncrement(object sender, RoutedEventArgs e)
|
|
{
|
|
if (ActiveView is not { } view)
|
|
return;
|
|
var edits = view.PlateView.Edits;
|
|
var result = await SetValueDialog.ShowAsync(this, "Set Offset Increment",
|
|
edits.OffsetIncrement,
|
|
optionText: "Calculated distance (selection size + part spacing)",
|
|
optionChecked: edits.CalculatedOffset);
|
|
if (result == null)
|
|
return;
|
|
edits.OffsetIncrement = result.Value;
|
|
edits.CalculatedOffset = result.OptionChecked;
|
|
}
|
|
|
|
// Tools > Set Rotation Increment (F6): the Shift+wheel turn, at most 360 degrees.
|
|
private async void OnSetRotationIncrement(object sender, RoutedEventArgs e)
|
|
{
|
|
if (ActiveView is not { } view)
|
|
return;
|
|
var edits = view.PlateView.Edits;
|
|
var result = await SetValueDialog.ShowAsync(this, "Set Rotation Increment",
|
|
edits.RotateIncrementDegrees, maximum: 360);
|
|
if (result == null)
|
|
return;
|
|
edits.RotateIncrementDegrees = result.Value;
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|
|
}
|