Browse Source

Fixed number input behaviour and brightness tool settings

Krzysztof Krysiński 1 year ago
parent
commit
a806de6e4f

+ 10 - 12
src/PixiEditor.AvaloniaUI/Helpers/Behaviours/TextBoxFocusBehavior.cs

@@ -2,6 +2,7 @@
 using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Interactivity;
+using Avalonia.Threading;
 using Avalonia.Xaml.Interactivity;
 
 namespace PixiEditor.AvaloniaUI.Helpers.Behaviours;
@@ -77,16 +78,9 @@ internal class TextBoxFocusBehavior : Behavior<TextBox>
 
     private void RemoveFocus()
     {
-        if (!FocusNext)
-        {
-            //TODO: FocusManager is private api
-            //FocusHelper.MoveFocusToParent(AssociatedObject);
-        }
-        else
-        {
-            //TODO: Idk if it works
-            AssociatedObject.Focus(NavigationMethod.Directional);
-        }
+        var next = KeyboardNavigationHandler.GetNext(AssociatedObject, NavigationDirection.Next);
+        NavigationMethod nextMethod = FocusNext ? NavigationMethod.Directional : NavigationMethod.Unspecified;
+        next?.Focus(nextMethod);
     }
 
     private void AssociatedObjectGotKeyboardFocus(
@@ -94,14 +88,18 @@ internal class TextBoxFocusBehavior : Behavior<TextBox>
         GotFocusEventArgs e)
     {
         if (SelectOnMouseClick || e.NavigationMethod == NavigationMethod.Tab)
-            AssociatedObject.SelectAll();
+        {
+            Dispatcher.UIThread.Post(() => AssociatedObject?.SelectAll(), DispatcherPriority.Input);
+        }
     }
 
     private void AssociatedObjectGotMouseCapture(
         object? sender, PointerPressedEventArgs pointerPressedEventArgs)
     {
         if (SelectOnMouseClick)
-            AssociatedObject.SelectAll();
+        {
+            Dispatcher.UIThread.Post(() => AssociatedObject?.SelectAll(), DispatcherPriority.Input);
+        }
     }
 
     private void AssociatedObject_LostFocus(object sender, RoutedEventArgs e)

+ 3 - 2
src/PixiEditor.AvaloniaUI/Models/DocumentModels/UpdateableChangeExecutors/BrightnessToolExecutor.cs

@@ -1,5 +1,6 @@
 using Avalonia.Input;
 using PixiEditor.AvaloniaUI.Models.Handlers;
+using PixiEditor.AvaloniaUI.Models.Handlers.Toolbars;
 using PixiEditor.AvaloniaUI.Models.Handlers.Tools;
 using PixiEditor.AvaloniaUI.Models.Tools;
 using PixiEditor.ChangeableDocument.Actions.Generated;
@@ -18,14 +19,14 @@ internal class BrightnessToolExecutor : UpdateableChangeExecutor
     {
         IStructureMemberHandler? member = document!.SelectedStructureMember;
         IBrightnessToolHandler? tool = GetHandler<IBrightnessToolHandler>();
-        if (tool is null || member is null)
+        if (tool is null || member is null || tool.Toolbar is not IBasicToolbar toolbar)
             return ExecutionState.Error;
         if (member is not ILayerHandler layer || layer.ShouldDrawOnMask)
             return ExecutionState.Error;
 
         guidValue = member.GuidValue;
         repeat = tool.BrightnessMode == BrightnessMode.Repeat;
-        toolSize = tool.ToolSize;
+        toolSize = toolbar.ToolSize;
         correctionFactor = tool.Darken || tool.UsedWith == MouseButton.Right ? -tool.CorrectionFactor : tool.CorrectionFactor;
 
         ChangeBrightness_Action action = new(guidValue, controller!.LastPixelPosition, correctionFactor, toolSize, repeat);

+ 2 - 3
src/PixiEditor.AvaloniaUI/Models/Handlers/Tools/IBrightnessToolHandler.cs

@@ -5,9 +5,8 @@ namespace PixiEditor.AvaloniaUI.Models.Handlers.Tools;
 
 internal interface IBrightnessToolHandler : IToolHandler
 {
-    public BrightnessMode BrightnessMode { get; set; }
-    public int ToolSize { get; set; }
+    public BrightnessMode BrightnessMode { get; }
     public bool Darken { get; }
-    public MouseButton UsedWith { get; set; }
+    public MouseButton UsedWith { get; }
     public int CorrectionFactor { get; }
 }

+ 8 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Tools/ToolSettings/Toolbars/SettingAttributes.cs

@@ -41,9 +41,17 @@ public static class Settings
     /// </summary>
     public class FloatAttribute : SettingsAttribute
     {
+        public float Min { get; set; } = float.NegativeInfinity;
+        public float Max { get; set; } = float.PositiveInfinity;
+
         public FloatAttribute(string labelKey) : base(labelKey) { }
 
         public FloatAttribute(string labelKey, float defaultValue) : base(labelKey, defaultValue) { }
+        public FloatAttribute(string labelKey, float defaultValue, float min, float max) : base(labelKey, defaultValue)
+        {
+            Min = min;
+            Max = max;
+        }
     }
 
     /// <summary>

+ 2 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Tools/ToolSettings/Toolbars/ToolbarFactory.cs

@@ -59,7 +59,8 @@ internal static class ToolbarFactory
             Settings.ColorAttribute => new ColorSetting(name,
                 ((Color)(attribute.DefaultValue ?? Colors.White)).ToColor(), label),
             Settings.EnumAttribute => GetEnumSetting(propertyType, name, attribute),
-            Settings.FloatAttribute => new FloatSetting(name, (float)(attribute.DefaultValue ?? 0f), label),
+            Settings.FloatAttribute floatAttribute => new FloatSetting(name, (float)(attribute.DefaultValue ?? 0f), label,
+                floatAttribute.Min, floatAttribute.Max),
             Settings.SizeAttribute => new SizeSetting(name, label),
             _ => throw new NotImplementedException(
                 $"SettingsAttribute of type '{attribute.GetType().FullName}' has not been implemented")

+ 5 - 3
src/PixiEditor.AvaloniaUI/ViewModels/Tools/Tools/BrightnessToolViewModel.cs

@@ -27,13 +27,15 @@ internal class BrightnessToolViewModel : ToolViewModel, IBrightnessToolHandler
 
     public override BrushShape BrushShape => BrushShape.Circle;
 
-    BrightnessMode IBrightnessToolHandler.BrightnessMode { get; set; } = BrightnessMode.Default;
-    int IBrightnessToolHandler.ToolSize { get; set; } = 1;
+    BrightnessMode IBrightnessToolHandler.BrightnessMode
+    {
+        get => BrightnessMode;
+    }
 
     [Settings.Inherited]
     public int ToolSize => GetValue<int>();
     
-    [Settings.Float("STRENGTH_LABEL", 5)]
+    [Settings.Float("STRENGTH_LABEL", 5, 0, 50)]
     public float CorrectionFactor => GetValue<float>();
 
     [Settings.Enum("MODE_LABEL")]

+ 6 - 5
src/PixiEditor.AvaloniaUI/Views/Input/NumberInput.axaml

@@ -6,17 +6,18 @@
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
     xmlns:behaviours="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Behaviours"
+    xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
     mc:Ignorable="d"
-             d:DesignHeight="20" d:DesignWidth="40" x:Name="numberInput" Focusable="True">
-    <TextBox TextAlignment="Center" Focusable="True"
+             d:DesignHeight="20" d:DesignWidth="40" x:Name="numberInput" Focusable="False">
+    <TextBox TextAlignment="Center" Focusable="True" Name="TextBox"
              PointerWheelChanged="TextBox_MouseWheel"
-             TextInput="TextBox_PreviewTextInput"
-             Text="{Binding ElementName=numberInput, Path=Value}"
+             LostFocus="TextBox_OnLostFocus"
+             Text="{Binding ElementName=numberInput, Path=FormattedValue, Mode=TwoWay}"
              Padding="0" VerticalContentAlignment="Center">
         <Interaction.Behaviors>
+            <behaviours:GlobalShortcutFocusBehavior/>
             <behaviours:TextBoxFocusBehavior SelectOnMouseClick="True" ConfirmOnEnter="True"
                                              FocusNext="{Binding ElementName=numberInput, Path=FocusNext}"/>
-            <behaviours:GlobalShortcutFocusBehavior/>
         </Interaction.Behaviors>
     </TextBox>
 </UserControl>

+ 82 - 15
src/PixiEditor.AvaloniaUI/Views/Input/NumberInput.axaml.cs

@@ -1,7 +1,10 @@
-using System.Text.RegularExpressions;
+using System.Globalization;
+using System.Linq;
+using System.Text.RegularExpressions;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Input;
+using Avalonia.Interactivity;
 
 namespace PixiEditor.AvaloniaUI.Views.Input;
 
@@ -19,7 +22,16 @@ internal partial class NumberInput : UserControl
         AvaloniaProperty.Register<NumberInput, double>(
             nameof(Max), double.PositiveInfinity);
 
-    private readonly Regex regex = new Regex("^[.][0-9]+$|^[0-9]*[.]{0,1}[0-9]*$", RegexOptions.Compiled);
+    public static readonly StyledProperty<string> FormattedValueProperty = AvaloniaProperty.Register<NumberInput, string>(
+        nameof(FormattedValue), "0");
+
+    public string FormattedValue
+    {
+        get => GetValue(FormattedValueProperty);
+        set => SetValue(FormattedValueProperty, value);
+    }
+
+    private static Regex regex;
 
     public int Decimals
     {
@@ -39,16 +51,6 @@ internal partial class NumberInput : UserControl
     public static readonly StyledProperty<Action> OnScrollActionProperty =
         AvaloniaProperty.Register<NumberInput, Action>(nameof(OnScrollAction));
 
-    static NumberInput()
-    {
-        ValueProperty.Changed.Subscribe(OnValueChanged);
-    }
-
-    public NumberInput()
-    {
-        InitializeComponent();
-    }
-
     public double Value
     {
         get => (double)GetValue(ValueProperty);
@@ -77,20 +79,72 @@ internal partial class NumberInput : UserControl
         set { SetValue(FocusNextProperty, value); }
     }
 
+    static NumberInput()
+    {
+        ValueProperty.Changed.Subscribe(OnValueChanged);
+        FormattedValueProperty.Changed.Subscribe(FormattedValueChanged);
+    }
+
+    public NumberInput()
+    {
+        InitializeComponent();
+    }
+
     private static void OnValueChanged(AvaloniaPropertyChangedEventArgs<double> e)
     {
         NumberInput input = (NumberInput)e.Sender;
         input.Value = (float)Math.Round(Math.Clamp(e.NewValue.Value, input.Min, input.Max), input.Decimals);
+
+        var preFormatted = FormatValue(input.Value, input.Decimals);
+        input.FormattedValue = preFormatted;
     }
 
-    private void TextBox_PreviewTextInput(object sender, TextInputEventArgs e)
+    private static string FormatValue(double value, int decimals)
     {
-        e.Handled = !regex.IsMatch((sender as TextBox).Text.Insert((sender as TextBox).SelectionStart, e.Text));
+        string separator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
+        string decimalString = ((float)value).ToString(CultureInfo.CurrentCulture);
+
+        string preFormatted = decimalString;
+        if (preFormatted.Contains(separator))
+        {
+            if (preFormatted.Split(separator)[1].Length > decimals)
+            {
+                preFormatted =
+                    preFormatted[
+                        ..(preFormatted.LastIndexOf(separator, StringComparison.InvariantCulture) + decimals +
+                           1)];
+            }
+
+            preFormatted = preFormatted.TrimEnd('0');
+            preFormatted = preFormatted.TrimEnd(separator.ToCharArray());
+        }
+
+        return preFormatted;
+    }
+
+    private static bool TryParse(string s, out double value)
+    {
+        s = s.Replace(",", ".");
+        return double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out value);
+    }
+
+    private static void FormattedValueChanged(AvaloniaPropertyChangedEventArgs<string> e)
+    {
+        NumberInput input = (NumberInput)e.Sender;
+        if(ContainsInvalidCharacter(e.NewValue.Value))
+        {
+            input.FormattedValue = e.OldValue.Value;
+        }
+    }
+
+    private static bool ContainsInvalidCharacter(string text)
+    {
+        return text.Any(c => !char.IsDigit(c) && c != '.' && c != ',');
     }
 
     private void TextBox_MouseWheel(object sender, PointerWheelEventArgs e)
     {
-        int step = (int)e.Delta.Y / 100;
+        int step = (int)e.Delta.Y;
 
         double newValue = Value;
         if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
@@ -111,4 +165,17 @@ internal partial class NumberInput : UserControl
 
         OnScrollAction?.Invoke();
     }
+
+    private void TextBox_OnLostFocus(object? sender, RoutedEventArgs e)
+    {
+        if (TryParse(FormattedValue, out double value))
+        {
+            Value = (float)Math.Round(Math.Clamp(value, Min, Max), Decimals);
+            FormattedValue = FormatValue(Value, Decimals);
+        }
+        else
+        {
+            FormattedValue = FormatValue(Value, Decimals);
+        }
+    }
 }