mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-11 19:11:10 -05:00
feat(avalonia): open several nests in tabs along the bottom
Each open nest gets a tab along the bottom of the window, above the status bar: the shown one is white with a blue line, a dot marks unsaved Auto Nest results, x closes it and + opens another. Inside a tab, Plates and Drawings tabs sit at the top of the left panel (Drawings lists each drawing's nested and required quantity) and the plate header with first/previous/next/last sits over the plate view. Each nest keeps its own plate and zoom when you switch. File > Open takes several files, and a file already open switches to its tab. File > Close and Close All, View > Next/Previous Nest and Ctrl+Tab/Ctrl+Shift+Tab switch or close nests. This version cannot save, so closing a nest with Auto Nest results, or exiting with any, asks first (Cancel is the default). Other nests can be opened and viewed while Auto Nest runs; the nest being nested cannot be closed until it finishes. Several nest paths on the command line open in tabs.
This commit is contained in:
1 parent
c32f3e5df3
commit
b329dd6c6b
6 files changed
+445
-99
No files matched your search
@@ -15,8 +15,13 @@ namespace OpenNest.Avalonia
|
||||
var window = new MainWindow();
|
||||
desktop.MainWindow = window;
|
||||
var args = desktop.Args ?? System.Array.Empty<string>();
|
||||
if (args.Length > 0)
|
||||
window.OpenNest(args[0]);
|
||||
// Each nest path before the first --option opens in its own tab; the last is shown.
|
||||
foreach (var path in args)
|
||||
{
|
||||
if (path.StartsWith("--"))
|
||||
break;
|
||||
window.OpenNest(path);
|
||||
}
|
||||
// --auto-nest: run Auto Nest on the opened nest (with --snapshot, before rendering).
|
||||
if (System.Array.IndexOf(args, "--auto-nest") >= 0)
|
||||
window.AutoNestOnOpen();
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace OpenNest.Avalonia
|
||||
{
|
||||
/// <summary>
|
||||
/// A two-button question. Cancel is the default and Esc, so a stray Enter never discards work.
|
||||
/// </summary>
|
||||
internal sealed class ConfirmDialog : Window
|
||||
{
|
||||
private ConfirmDialog(string title, string message, string confirmText)
|
||||
{
|
||||
Title = title;
|
||||
Width = 460;
|
||||
SizeToContent = SizeToContent.Height;
|
||||
CanResize = false;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
|
||||
var confirm = new Button { Content = confirmText };
|
||||
var cancel = new Button { Content = "Cancel", IsDefault = true, IsCancel = true };
|
||||
confirm.Click += (_, _) => Close(true);
|
||||
cancel.Click += (_, _) => Close(false);
|
||||
|
||||
var buttons = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Spacing = 8,
|
||||
};
|
||||
buttons.Children.Add(confirm);
|
||||
buttons.Children.Add(cancel);
|
||||
|
||||
var panel = new StackPanel { Margin = new Thickness(18), Spacing = 18 };
|
||||
panel.Children.Add(new TextBlock { Text = message, TextWrapping = TextWrapping.Wrap });
|
||||
panel.Children.Add(buttons);
|
||||
Content = panel;
|
||||
Opened += (_, _) => cancel.Focus();
|
||||
}
|
||||
|
||||
/// <summary>True when the user chose <paramref name="confirmText"/>.</summary>
|
||||
public static Task<bool> AskAsync(
|
||||
Window owner, string title, string message, string confirmText) =>
|
||||
new ConfirmDialog(title, message, confirmText).ShowDialog<bool>(owner);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:c="using:OpenNest.Avalonia.Controls"
|
||||
xmlns:local="using:OpenNest.Avalonia"
|
||||
x:Class="OpenNest.Avalonia.MainWindow"
|
||||
Title="OpenNest (Avalonia prototype)"
|
||||
Width="1400" Height="860">
|
||||
@@ -9,6 +7,8 @@
|
||||
<Menu DockPanel.Dock="Top">
|
||||
<MenuItem Header="_File">
|
||||
<MenuItem x:Name="OpenMenuItem" Header="_Open..." InputGesture="Ctrl+O" Click="OnOpen" />
|
||||
<MenuItem x:Name="CloseMenuItem" Header="_Close" Click="OnCloseNest" />
|
||||
<MenuItem x:Name="CloseAllMenuItem" Header="Close A_ll" Click="OnCloseAll" />
|
||||
<Separator />
|
||||
<MenuItem Header="E_xit" Click="OnExit" />
|
||||
</MenuItem>
|
||||
@@ -19,6 +19,9 @@
|
||||
<MenuItem Header="Zoom to _Fit" InputGesture="F" Click="OnFit" />
|
||||
<MenuItem Header="_Previous Plate" InputGesture="PageUp" Click="OnPreviousPlate" />
|
||||
<MenuItem Header="_Next Plate" InputGesture="PageDown" Click="OnNextPlate" />
|
||||
<Separator />
|
||||
<MenuItem Header="Next N_est" InputGesture="Ctrl+Tab" Click="OnNextNest" />
|
||||
<MenuItem Header="Previous Ne_st" InputGesture="Ctrl+Shift+Tab" Click="OnPreviousNest" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
@@ -36,25 +39,17 @@
|
||||
<TextBlock x:Name="Status" Text="Open a .nest file to begin." />
|
||||
</Border>
|
||||
|
||||
<Grid ColumnDefinitions="260,4,*,4,320">
|
||||
<DockPanel Grid.Column="0">
|
||||
<TextBlock DockPanel.Dock="Top" Margin="6,6,6,2" FontWeight="SemiBold" Text="Plates" />
|
||||
<ListBox x:Name="PlateList" SelectionChanged="OnPlateSelected" />
|
||||
</DockPanel>
|
||||
<GridSplitter Grid.Column="1" ResizeDirection="Columns" />
|
||||
<c:PlateCanvas Grid.Column="2" x:Name="Canvas" />
|
||||
<GridSplitter Grid.Column="3" ResizeDirection="Columns" />
|
||||
<DockPanel Grid.Column="4">
|
||||
<TextBlock DockPanel.Dock="Top" Margin="6,6,6,2" FontWeight="SemiBold" Text="Drawings on plate" />
|
||||
<DataGrid x:Name="DrawingGrid" x:DataType="local:DrawingRow" IsReadOnly="True"
|
||||
AutoGenerateColumns="False" GridLinesVisibility="Horizontal" CanUserSortColumns="True">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Drawing" Binding="{Binding Name}" Width="*" />
|
||||
<DataGridTextColumn Header="Qty" Binding="{Binding Count}" />
|
||||
<DataGridTextColumn Header="Size" Binding="{Binding Size}" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
<!-- One tab per open nest, along the bottom of the window. -->
|
||||
<Border DockPanel.Dock="Bottom" Background="#E4E7EA">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<StackPanel x:Name="TabStrip" Orientation="Horizontal" Margin="6,0,6,6" />
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<Panel>
|
||||
<TextBlock x:Name="EmptyHint" Text="Open a .nest file to begin." Foreground="#6B7280"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="15" />
|
||||
<ContentControl x:Name="NestHost" />
|
||||
</Panel>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -1,11 +1,15 @@
|
||||
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;
|
||||
@@ -15,17 +19,19 @@ using OpenNest.IO;
|
||||
|
||||
namespace OpenNest.Avalonia
|
||||
{
|
||||
public sealed record DrawingRow(string Name, int Count, string Size);
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private Document document;
|
||||
private string fileName;
|
||||
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 Nest nest => document?.Nest;
|
||||
private bool closeConfirmed;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
@@ -35,7 +41,7 @@ namespace OpenNest.Avalonia
|
||||
var engineWarning = EngineSelection.LoadSavedSelection();
|
||||
if (!string.IsNullOrEmpty(engineWarning))
|
||||
notice = engineWarning;
|
||||
Canvas.ViewChanged += (_, _) => UpdateStatus();
|
||||
tabs.Changed += (_, _) => ShowActiveTab();
|
||||
KeyBindings.Add(new KeyBinding
|
||||
{
|
||||
Gesture = new KeyGesture(Key.O, KeyModifiers.Control),
|
||||
@@ -43,35 +49,36 @@ namespace OpenNest.Avalonia
|
||||
});
|
||||
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);
|
||||
Closing += OnWindowClosing;
|
||||
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)
|
||||
{
|
||||
if (nesting != null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
document = new Document { Nest = new NestReader(path).Read() };
|
||||
var tab = tabs.Open(
|
||||
path, file => new Document { Nest = new NestReader(file).Read() });
|
||||
if (!views.ContainsKey(tab))
|
||||
{
|
||||
var view = new NestView(tab);
|
||||
view.ViewChanged += (_, _) => { if (view == ActiveView) UpdateStatus(); };
|
||||
views[tab] = view;
|
||||
notice = null;
|
||||
ShowActiveTab();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Status.Text = $"Could not open {Path.GetFileName(path)}: {ex.Message}";
|
||||
return;
|
||||
ShowNotice($"Could not open {Path.GetFileName(path)}: {ex.Message}");
|
||||
}
|
||||
|
||||
fileName = Path.GetFileName(path);
|
||||
Title = $"{fileName} - OpenNest (Avalonia prototype)";
|
||||
notice = null;
|
||||
Canvas.ClearCache();
|
||||
RefreshPlateList(0);
|
||||
}
|
||||
|
||||
private void RefreshPlateList(int selectIndex)
|
||||
{
|
||||
PlateList.ItemsSource = nest.Plates
|
||||
.Select((plate, i) => $"Plate {i + 1} {plate.Size.Length:0.###} x {plate.Size.Width:0.###} ({plate.Parts.Count} parts)")
|
||||
.ToList();
|
||||
PlateList.SelectedIndex = nest.Plates.Count > 0 ? System.Math.Clamp(selectIndex, 0, nest.Plates.Count - 1) : -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -90,32 +97,187 @@ namespace OpenNest.Avalonia
|
||||
{
|
||||
Opened += async (_, _) =>
|
||||
{
|
||||
if (nest?.Plates.Count > 0)
|
||||
PlateList.SelectedIndex = System.Math.Clamp(plateNumber - 1, 0, nest.Plates.Count - 1);
|
||||
ActiveView?.ShowPlate(plateNumber - 1);
|
||||
await autoNest;
|
||||
await System.Threading.Tasks.Task.Delay(500);
|
||||
var size = new global::Avalonia.PixelSize((int)Bounds.Width, (int)Bounds.Height);
|
||||
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;
|
||||
CloseAllMenuItem.IsEnabled = view != null;
|
||||
Title = view == null
|
||||
? "OpenNest (Avalonia prototype)"
|
||||
: $"{view.Tab.Name} - OpenNest (Avalonia prototype)";
|
||||
RebuildTabStrip();
|
||||
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, "Open a nest");
|
||||
add.Click += OnOpen;
|
||||
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 (tab.Modified && !await ConfirmDialog.AskAsync(this, "Close Nest",
|
||||
$"{tab.Name} has Auto Nest results that are not saved. This version of "
|
||||
+ "OpenNest cannot save nests yet, so closing it discards them.",
|
||||
"Discard and Close"))
|
||||
return false;
|
||||
|
||||
tabs.Close(tab);
|
||||
views.Remove(tab);
|
||||
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)
|
||||
{
|
||||
nesting?.Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
e.Cancel = true;
|
||||
var count = tabs.Tabs.Count(t => t.Modified);
|
||||
var what = count == 1 ? "One open nest has" : $"{count} open nests have";
|
||||
if (await ConfirmDialog.AskAsync(this, "Exit OpenNest",
|
||||
$"{what} Auto Nest results that are not saved. This version of OpenNest "
|
||||
+ "cannot save nests yet, so exiting discards them.",
|
||||
"Discard and Exit"))
|
||||
{
|
||||
closeConfirmed = true;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
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 OnOpen(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (nesting != null)
|
||||
return;
|
||||
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Open Nest",
|
||||
AllowMultiple = false,
|
||||
AllowMultiple = true,
|
||||
FileTypeFilter = new[] { new FilePickerFileType("Nest files") { Patterns = new[] { "*.nest" } } },
|
||||
});
|
||||
var path = files.FirstOrDefault()?.TryGetLocalPath();
|
||||
if (path != null)
|
||||
foreach (var path in files.Select(f => f.TryGetLocalPath()).Where(p => p != null))
|
||||
OpenNest(path);
|
||||
}
|
||||
|
||||
@@ -133,16 +295,21 @@ namespace OpenNest.Avalonia
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Nests every drawing's remaining quantity on sheets like the shown plate through the
|
||||
/// shared pipeline, off the UI thread. Open is refused until it finishes, so the nest
|
||||
/// and its drawings stay stable from the request to the commit.
|
||||
/// 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()
|
||||
{
|
||||
if (nest == null || nesting != null)
|
||||
var view = ActiveView;
|
||||
if (view == null || nesting != null)
|
||||
return;
|
||||
var tab = view.Tab;
|
||||
var nest = tab.Document.Nest;
|
||||
var engineName = EngineSelection.EngineName;
|
||||
var template = Canvas.Plate ?? nest.Plates.FirstOrDefault() ?? nest.PlateDefaults.CreateNew();
|
||||
var template = view.PlateView.Plate
|
||||
?? nest.Plates.FirstOrDefault()
|
||||
?? nest.PlateDefaults.CreateNew();
|
||||
var request = AutoNestJob.ForRemaining(nest, template, engineName);
|
||||
if (request == null)
|
||||
{
|
||||
@@ -151,6 +318,7 @@ namespace OpenNest.Avalonia
|
||||
}
|
||||
|
||||
nesting = new CancellationTokenSource();
|
||||
nestingTab = tab;
|
||||
var token = nesting.Token;
|
||||
SetNesting(true);
|
||||
ShowNotice($"{engineName}: starting...");
|
||||
@@ -168,8 +336,14 @@ namespace OpenNest.Avalonia
|
||||
var firstNew = nest.Plates.Count(p => p.Parts.Count > 0);
|
||||
var kept = AutoNestJob.Keep(nest, result);
|
||||
if (kept.Nested)
|
||||
RefreshPlateList(firstNew);
|
||||
ShowNotice(kept.Nested ? $"{engineName}: {kept.Note}" : AutoNestJob.RefusalReason(result));
|
||||
{
|
||||
tab.MarkModified();
|
||||
view.Refresh(firstNew);
|
||||
RebuildTabStrip();
|
||||
}
|
||||
ShowNotice(kept.Nested
|
||||
? $"{tab.Name}: {engineName}: {kept.Note}"
|
||||
: AutoNestJob.RefusalReason(result));
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
@@ -183,6 +357,7 @@ namespace OpenNest.Avalonia
|
||||
{
|
||||
nesting.Dispose();
|
||||
nesting = null;
|
||||
nestingTab = null;
|
||||
SetNesting(false);
|
||||
}
|
||||
}
|
||||
@@ -191,58 +366,45 @@ namespace OpenNest.Avalonia
|
||||
{
|
||||
AutoNestButton.Content = running ? "Stop" : "Auto Nest";
|
||||
AutoNestMenuItem.Header = running ? "_Stop Auto Nest" : "_Auto Nest";
|
||||
OpenButton.IsEnabled = !running;
|
||||
OpenMenuItem.IsEnabled = !running;
|
||||
}
|
||||
|
||||
private void ShowNotice(string text)
|
||||
{
|
||||
notice = text;
|
||||
UpdateStatus();
|
||||
if (Canvas.Plate == null)
|
||||
Status.Text = text;
|
||||
}
|
||||
|
||||
private void OnFit(object sender, RoutedEventArgs e) => Canvas.ZoomToFit();
|
||||
private void OnFit(object sender, RoutedEventArgs e) => ActiveView?.PlateView.ZoomToFit();
|
||||
|
||||
private void OnPreviousPlate(object sender, RoutedEventArgs e) => StepPlate(-1);
|
||||
|
||||
private void OnNextPlate(object sender, RoutedEventArgs e) => StepPlate(1);
|
||||
|
||||
private void StepPlate(int step)
|
||||
{
|
||||
if (nest == null || nest.Plates.Count == 0)
|
||||
return;
|
||||
PlateList.SelectedIndex = System.Math.Clamp(PlateList.SelectedIndex + step, 0, nest.Plates.Count - 1);
|
||||
}
|
||||
|
||||
private void OnPlateSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
var index = PlateList.SelectedIndex;
|
||||
var plate = nest != null && index >= 0 ? nest.Plates[index] : null;
|
||||
Canvas.Plate = plate;
|
||||
DrawingGrid.ItemsSource = plate?.Parts
|
||||
.Where(part => !part.BaseDrawing.IsCutOff)
|
||||
.GroupBy(part => part.BaseDrawing)
|
||||
.Select(group => new DrawingRow(group.Key.Name, group.Count(), SizeOf(group.First())))
|
||||
.OrderByDescending(row => row.Count)
|
||||
.ToList();
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
private static string SizeOf(Part part) =>
|
||||
$"{part.BoundingBox.Length:0.###} x {part.BoundingBox.Width:0.###}";
|
||||
private void StepPlate(int step) => ActiveView?.StepPlate(step);
|
||||
|
||||
private void UpdateStatus()
|
||||
{
|
||||
if (Canvas.Plate == null)
|
||||
var view = ActiveView;
|
||||
var canvas = view?.PlateView;
|
||||
if (canvas?.Plate == null)
|
||||
{
|
||||
var empty = view == null
|
||||
? "Open a .nest file to begin."
|
||||
: $"{view.Tab.Name} No plates";
|
||||
Status.Text = notice ?? empty;
|
||||
return;
|
||||
var text = $"{fileName} Plate {PlateList.SelectedIndex + 1} of {nest.Plates.Count} Zoom {Canvas.Zoom:0.##} px/unit";
|
||||
if (Canvas.CursorWorld is { } at)
|
||||
}
|
||||
|
||||
var text = $"{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 part = Canvas.HoveredPart ?? Canvas.SelectedPart;
|
||||
var part = canvas.HoveredPart ?? canvas.SelectedPart;
|
||||
if (part != null)
|
||||
text += $" {(ReferenceEquals(part, Canvas.SelectedPart) ? "Selected" : "Over")}: {part.BaseDrawing.Name}";
|
||||
{
|
||||
var how = ReferenceEquals(part, canvas.SelectedPart) ? "Selected" : "Over";
|
||||
text += $" {how}: {part.BaseDrawing.Name}";
|
||||
}
|
||||
if (notice != null)
|
||||
text += $" | {notice}";
|
||||
Status.Text = text;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:c="using:OpenNest.Avalonia.Controls"
|
||||
xmlns:local="using:OpenNest.Avalonia"
|
||||
x:Class="OpenNest.Avalonia.NestView">
|
||||
<Grid ColumnDefinitions="290,4,*">
|
||||
<Border Grid.Column="0" Background="White">
|
||||
<TabControl x:Name="SideTabs" Padding="0">
|
||||
<TabItem Header="Plates" FontSize="14" MinHeight="34">
|
||||
<ListBox x:Name="PlateList" SelectionChanged="OnPlateSelected" />
|
||||
</TabItem>
|
||||
<TabItem Header="Drawings" FontSize="14" MinHeight="34">
|
||||
<DataGrid x:Name="DrawingGrid" x:DataType="local:DrawingRow" IsReadOnly="True"
|
||||
FontSize="13" AutoGenerateColumns="False"
|
||||
GridLinesVisibility="Horizontal" CanUserSortColumns="True">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Drawing" Binding="{Binding Name}" Width="*" />
|
||||
<DataGridTextColumn Header="Nested" Binding="{Binding Nested}" />
|
||||
<DataGridTextColumn Header="Required" Binding="{Binding Required}" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Border>
|
||||
<GridSplitter Grid.Column="1" ResizeDirection="Columns" Background="#D0D4D8" />
|
||||
<DockPanel Grid.Column="2">
|
||||
<Border DockPanel.Dock="Top" Padding="8,4" Background="White"
|
||||
BorderBrush="#D0D4D8" BorderThickness="0,0,0,1">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Content="⏮" ToolTip.Tip="First plate" Click="OnFirstPlate" />
|
||||
<Button Content="◀" ToolTip.Tip="Previous plate (Page Up)"
|
||||
Click="OnPreviousPlate" />
|
||||
<TextBlock x:Name="PlateLabel" VerticalAlignment="Center" FontSize="13" Margin="6,0" />
|
||||
<Button Content="▶" ToolTip.Tip="Next plate (Page Down)" Click="OnNextPlate" />
|
||||
<Button Content="⏭" ToolTip.Tip="Last plate" Click="OnLastPlate" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<c:PlateCanvas x:Name="Canvas" />
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using OpenNest.Avalonia.Controls;
|
||||
using OpenNest.Desktop;
|
||||
|
||||
namespace OpenNest.Avalonia
|
||||
{
|
||||
public sealed record DrawingRow(string Name, int Nested, int Required);
|
||||
|
||||
/// <summary>
|
||||
/// One open nest: Plates and Drawings tabs on the left, the plate header and plate view on
|
||||
/// the right. Each tab keeps its own view, so switching nests keeps each one's plate and zoom.
|
||||
/// </summary>
|
||||
public partial class NestView : UserControl
|
||||
{
|
||||
public NestView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public NestView(NestTab tab)
|
||||
: this()
|
||||
{
|
||||
Tab = tab;
|
||||
Canvas.ViewChanged += (_, _) => ViewChanged?.Invoke(this, EventArgs.Empty);
|
||||
Refresh(0);
|
||||
}
|
||||
|
||||
/// <summary>Raised when the shown plate, zoom, hover or selection changes.</summary>
|
||||
public event EventHandler ViewChanged;
|
||||
|
||||
public NestTab Tab { get; }
|
||||
|
||||
public PlateCanvas PlateView => Canvas;
|
||||
|
||||
public int PlateIndex => PlateList.SelectedIndex;
|
||||
|
||||
public int PlateCount => Tab.Document.Nest.Plates.Count;
|
||||
|
||||
/// <summary>Rebuilds both lists and shows plate <paramref name="selectIndex"/>.</summary>
|
||||
public void Refresh(int selectIndex)
|
||||
{
|
||||
var nest = Tab.Document.Nest;
|
||||
Canvas.ClearCache();
|
||||
PlateList.ItemsSource = nest.Plates
|
||||
.Select((plate, i) =>
|
||||
$"Plate {i + 1} {SizeText(plate)} ({plate.Parts.Count} parts)")
|
||||
.ToList();
|
||||
PlateList.SelectedIndex = nest.Plates.Count > 0
|
||||
? System.Math.Clamp(selectIndex, 0, nest.Plates.Count - 1)
|
||||
: -1;
|
||||
DrawingGrid.ItemsSource = nest.Drawings
|
||||
.Where(d => !d.IsCutOff)
|
||||
.Select(d => new DrawingRow(d.Name, d.Quantity.Nested, d.Quantity.Required))
|
||||
.ToList();
|
||||
ShowPlate();
|
||||
}
|
||||
|
||||
public void ShowPlate(int index)
|
||||
{
|
||||
if (PlateCount > 0)
|
||||
PlateList.SelectedIndex = System.Math.Clamp(index, 0, PlateCount - 1);
|
||||
}
|
||||
|
||||
public void StepPlate(int step) => ShowPlate(PlateList.SelectedIndex + step);
|
||||
|
||||
private void OnPlateSelected(object sender, SelectionChangedEventArgs e) => ShowPlate();
|
||||
|
||||
private void ShowPlate()
|
||||
{
|
||||
var nest = Tab.Document.Nest;
|
||||
var index = PlateList.SelectedIndex;
|
||||
var plate = index >= 0 && index < nest.Plates.Count ? nest.Plates[index] : null;
|
||||
Canvas.Plate = plate;
|
||||
PlateLabel.Text = plate == null
|
||||
? "No plates"
|
||||
: $"Plate {index + 1} of {nest.Plates.Count} "
|
||||
+ $"{SizeText(plate)} qty {plate.Quantity}";
|
||||
ViewChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private static string SizeText(Plate plate) =>
|
||||
$"{plate.Size.Length:0.###} x {plate.Size.Width:0.###}";
|
||||
|
||||
private void OnFirstPlate(object sender, RoutedEventArgs e) => ShowPlate(0);
|
||||
|
||||
private void OnPreviousPlate(object sender, RoutedEventArgs e) => StepPlate(-1);
|
||||
|
||||
private void OnNextPlate(object sender, RoutedEventArgs e) => StepPlate(1);
|
||||
|
||||
private void OnLastPlate(object sender, RoutedEventArgs e) => ShowPlate(PlateCount - 1);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user