diff --git a/OpenNest.Avalonia/MainWindow.axaml b/OpenNest.Avalonia/MainWindow.axaml index effc42f7..1ae9afaa 100644 --- a/OpenNest.Avalonia/MainWindow.axaml +++ b/OpenNest.Avalonia/MainWindow.axaml @@ -59,6 +59,9 @@ + + + diff --git a/OpenNest.Avalonia/MainWindow.axaml.cs b/OpenNest.Avalonia/MainWindow.axaml.cs index f2eb6c51..4f967e05 100644 --- a/OpenNest.Avalonia/MainWindow.axaml.cs +++ b/OpenNest.Avalonia/MainWindow.axaml.cs @@ -95,6 +95,16 @@ 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)) }); + 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. @@ -295,6 +305,8 @@ namespace OpenNest.Avalonia 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)" @@ -849,6 +861,37 @@ namespace OpenNest.Avalonia 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) => diff --git a/OpenNest.Avalonia/SetValueDialog.cs b/OpenNest.Avalonia/SetValueDialog.cs new file mode 100644 index 00000000..1231fe19 --- /dev/null +++ b/OpenNest.Avalonia/SetValueDialog.cs @@ -0,0 +1,116 @@ +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; + +namespace OpenNest.Avalonia +{ + /// + /// One number to set, as the WinForms SetValueForm: an "Increment :" box from 0 to a maximum + /// shown with four decimals, an optional check box that disables the box, and a Set button + /// that is also Enter. Esc or closing the window changes nothing. + /// + internal sealed class SetValueDialog : Window + { + public const double DefaultMaximum = 999999; + + /// What the user set: the value and the option check box. + public sealed record Result(double Value, bool OptionChecked); + + private readonly NumericUpDown value; + private readonly CheckBox option; + // The last value the box held, which an emptied box keeps, as the WinForms box does. + private double lastValid; + private readonly double maximum; + + private SetValueDialog( + string title, double initialValue, double maximum, string optionText, + bool optionChecked) + { + Title = title; + // Wide enough for the option's text on one line, as the WinForms form widens. + MinWidth = 300; + SizeToContent = SizeToContent.WidthAndHeight; + CanResize = false; + ShowInTaskbar = false; + WindowStartupLocation = WindowStartupLocation.CenterOwner; + this.maximum = maximum; + lastValid = System.Math.Clamp(initialValue, 0, maximum); + + value = new NumericUpDown + { + Name = "Value", + Minimum = 0, + Maximum = (decimal)maximum, + Increment = 1, + // A typed value out of range goes to the nearest limit, as in WinForms, rather + // than being refused. + ClipValueToMinMax = true, + FormatString = "0.0000", + Value = (decimal)lastValid, + HorizontalAlignment = HorizontalAlignment.Stretch, + }; + value.ValueChanged += (_, e) => + { + if (e.NewValue is { } v) + lastValid = (double)v; + }; + var set = new Button { Name = "Set", Content = "Set", IsDefault = true }; + set.Click += (_, _) => Close(new Result(Current, option.IsChecked == true)); + + var row = new Grid + { + ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto"), + ColumnSpacing = 8, + }; + var label = new TextBlock + { + Text = "Increment :", + VerticalAlignment = VerticalAlignment.Center, + }; + row.Children.Add(label); + Grid.SetColumn(value, 1); + row.Children.Add(value); + Grid.SetColumn(set, 2); + row.Children.Add(set); + + option = new CheckBox + { + Name = "Option", + Content = optionText, + IsChecked = optionChecked, + IsVisible = !string.IsNullOrEmpty(optionText), + }; + value.IsEnabled = !optionChecked; + option.IsCheckedChanged += (_, _) => value.IsEnabled = option.IsChecked != true; + + var panel = new StackPanel { Margin = new Thickness(12), Spacing = 10 }; + panel.Children.Add(row); + panel.Children.Add(option); + Content = panel; + + AddHandler(KeyDownEvent, (_, e) => + { + if (e.Key != Key.Escape) + return; + e.Handled = true; + Close(null); + }, RoutingStrategies.Tunnel); + Opened += (_, _) => value.Focus(); + } + + // A value out of range is taken to the nearest limit, as the WinForms box does. + private double Current => System.Math.Clamp(lastValid, 0, maximum); + + /// + /// The value and option the user set, or null when they closed the dialog without Set. + /// + public static Task ShowAsync( + Window owner, string title, double value, double maximum = DefaultMaximum, + string optionText = null, bool optionChecked = false) => + new SetValueDialog(title, value, maximum, optionText, optionChecked) + .ShowDialog(owner); + } +}