From 631913145b8c6ee93919a4d5e7732eebb5114c01 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Sun, 14 Jan 2024 22:11:23 +0800 Subject: [PATCH] feat: finish int implementation. --- demo/Ursa.Demo/Pages/NumericUpDownDemo.axaml | 3 +- .../Controls/NumericUpDown.axaml | 31 +- src/Ursa/Controls/NumericUpDown/IntUpDown.cs | 44 +- .../NumericUpDown/NumericUpDownBase.cs | 430 +++++++++++++++++- .../NumericUpDown/ValueChangedEventArgs.cs | 15 + 5 files changed, 470 insertions(+), 53 deletions(-) create mode 100644 src/Ursa/Controls/NumericUpDown/ValueChangedEventArgs.cs diff --git a/demo/Ursa.Demo/Pages/NumericUpDownDemo.axaml b/demo/Ursa.Demo/Pages/NumericUpDownDemo.axaml index a16edcd..6a3cfe5 100644 --- a/demo/Ursa.Demo/Pages/NumericUpDownDemo.axaml +++ b/demo/Ursa.Demo/Pages/NumericUpDownDemo.axaml @@ -9,6 +9,7 @@ d:DesignWidth="800" mc:Ignorable="d"> - + + diff --git a/src/Ursa.Themes.Semi/Controls/NumericUpDown.axaml b/src/Ursa.Themes.Semi/Controls/NumericUpDown.axaml index 53f60ce..eff46a6 100644 --- a/src/Ursa.Themes.Semi/Controls/NumericUpDown.axaml +++ b/src/Ursa.Themes.Semi/Controls/NumericUpDown.axaml @@ -3,9 +3,36 @@ xmlns:u="https://irihi.tech/ursa"> + + - - + + + + + + diff --git a/src/Ursa/Controls/NumericUpDown/IntUpDown.cs b/src/Ursa/Controls/NumericUpDown/IntUpDown.cs index b951372..6aba69b 100644 --- a/src/Ursa/Controls/NumericUpDown/IntUpDown.cs +++ b/src/Ursa/Controls/NumericUpDown/IntUpDown.cs @@ -1,48 +1,32 @@ -using Avalonia.Utilities; +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Utilities; namespace Ursa.Controls; -public class IntUpDown: NumericUpDownBase +public class IntUpDown : NumericUpDownBase { protected override Type StyleKeyOverride { get; } = typeof(NumericUpDown); static IntUpDown() { MaximumProperty.OverrideDefaultValue(int.MaxValue); + MinimumProperty.OverrideDefaultValue(int.MinValue); StepProperty.OverrideDefaultValue(1); } - - protected override void Increase() + + protected override bool ParseText(string? text, out int? number) { - Value += Step; + var result = int.TryParse(text, ParsingNumberStyle, NumberFormat, out var value); + number = value; + return result; } - protected override void Decrease() - { - Value -= Step; - } - - protected override void UpdateTextToValue(string x) - { - if (int.TryParse(x, out var value)) - { - Value = value; - } - } + protected override string? ValueToString(int? value) => value?.ToString(FormatString, NumberFormat); - protected override bool CommitInput() - { - // throw new NotImplementedException(); - return true; - } + protected override int Zero => 0; - protected override void SyncTextAndValue() - { - // throw new NotImplementedException(); - } + protected override int? Add(int? a, int? b) => a + b; - protected override int Clamp() - { - return MathUtilities.Clamp(Value, Maximum, Minimum); - } + protected override int? Minus(int? a, int? b) => a - b; } \ No newline at end of file diff --git a/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs b/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs index 659aeaa..1dc9a24 100644 --- a/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs +++ b/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs @@ -1,7 +1,10 @@ -using Avalonia; +using System.Globalization; +using System.Net.Mime; +using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +using Avalonia.Data.Converters; using Avalonia.Input; using Avalonia.Interactivity; @@ -21,7 +24,7 @@ public abstract class NumericUpDown : TemplatedControl protected internal Panel? _dragPanel; private Point? _point; - private bool _updateFromTextInput; + protected internal bool _updateFromTextInput; public static readonly StyledProperty TextEditableProperty = AvaloniaProperty.Register( nameof(TextEditable), defaultValue: true); @@ -58,8 +61,94 @@ public abstract class NumericUpDown : TemplatedControl get => GetValue(WatermarkProperty); set => SetValue(WatermarkProperty, value); } + + public static readonly StyledProperty NumberFormatProperty = AvaloniaProperty.Register( + nameof(NumberFormat), defaultValue: NumberFormatInfo.CurrentInfo); + + public NumberFormatInfo? NumberFormat + { + get => GetValue(NumberFormatProperty); + set => SetValue(NumberFormatProperty, value); + } + + public static readonly StyledProperty FormatStringProperty = AvaloniaProperty.Register( + nameof(FormatString), string.Empty); + + public string FormatString + { + get => GetValue(FormatStringProperty); + set => SetValue(FormatStringProperty, value); + } + + public static readonly StyledProperty ParsingNumberStyleProperty = AvaloniaProperty.Register( + nameof(ParsingNumberStyle)); + + public NumberStyles ParsingNumberStyle + { + get => GetValue(ParsingNumberStyleProperty); + set => SetValue(ParsingNumberStyleProperty, value); + } + + public static readonly StyledProperty TextConverterProperty = AvaloniaProperty.Register( + nameof(TextConverter)); + + public IValueConverter? TextConverter + { + get => GetValue(TextConverterProperty); + set => SetValue(TextConverterProperty, value); + } + + public static readonly StyledProperty AllowSpinProperty = AvaloniaProperty.Register( + nameof(AllowSpin), true); + + public bool AllowSpin + { + get => GetValue(AllowSpinProperty); + set => SetValue(AllowSpinProperty, value); + } + + public static readonly StyledProperty AllowMouseWheelProperty = AvaloniaProperty.Register( + nameof(AllowMouseWheel)); + + public bool AllowMouseWheel + { + get => GetValue(AllowMouseWheelProperty); + set => SetValue(AllowMouseWheelProperty, value); + } + + public event EventHandler? Spinned; - + static NumericUpDown() + { + NumberFormatProperty.Changed.AddClassHandler((o, e) => o.OnFormatChange(e)); + FormatStringProperty.Changed.AddClassHandler((o, e) => o.OnFormatChange(e)); + IsReadOnlyProperty.Changed.AddClassHandler((o,e)=>o.ChangeToSetSpinDirection(e)); + TextConverterProperty.Changed.AddClassHandler((o, e) => o.OnFormatChange(e)); + } + + protected void ChangeToSetSpinDirection(AvaloniaPropertyChangedEventArgs avaloniaPropertyChangedEventArgs, bool afterInitialization = false) + { + if (afterInitialization) + { + if (IsInitialized) + { + SetValidSpinDirection(); + } + } + else + { + SetValidSpinDirection(); + } + } + + protected virtual void OnFormatChange(AvaloniaPropertyChangedEventArgs arg) + { + if (IsInitialized) + { + SyncTextAndValue(false, null); + } + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); @@ -70,6 +159,7 @@ public abstract class NumericUpDown : TemplatedControl if(_textBox is not null) { _textBox.TextChanged -= OnTextChange; + } if (_dragPanel is not null) @@ -101,7 +191,7 @@ public abstract class NumericUpDown : TemplatedControl protected override void OnLostFocus(RoutedEventArgs e) { - CommitInput(); + CommitInput(true); base.OnLostFocus(e); } @@ -134,46 +224,65 @@ public abstract class NumericUpDown : TemplatedControl } } + [Obsolete] private void OnTextChange(object sender, TextChangedEventArgs e) { _updateFromTextInput = true; - UpdateTextToValue(_textBox?.Text ?? string.Empty); + SyncTextAndValue(); _updateFromTextInput = false; } private void OnSpin(object sender, SpinEventArgs e) { - if (e.Direction == SpinDirection.Increase) + if (AllowSpin && !IsReadOnly) { - Increase(); - } - else - { - Decrease(); + var spin = !e.UsingMouseWheel; + spin |= _textBox is { IsFocused: true }; + if (spin) + { + e.Handled = true; + var handler = Spinned; + handler?.Invoke(this, e); + if (e.Direction == SpinDirection.Increase) + { + Increase(); + } + else + { + Decrease(); + } + } } } + protected abstract void SetValidSpinDirection(); + protected abstract void Increase(); protected abstract void Decrease(); - protected abstract void UpdateTextToValue(string x); - protected abstract bool CommitInput(); - protected abstract void SyncTextAndValue(); + + protected virtual bool CommitInput(bool forceTextUpdate = false) + { + return SyncTextAndValue(true, _textBox?.Text, forceTextUpdate); + } + + protected abstract bool SyncTextAndValue(bool fromTextToValue = false, string? text = null, + bool forceTextUpdate = false); } public abstract class NumericUpDownBase: NumericUpDown where T: struct, IComparable { - public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register, T>( + public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register, T?>( nameof(Value)); - public T Value + public T? Value { get => GetValue(ValueProperty); set => SetValue(ValueProperty, value); } public static readonly StyledProperty MaximumProperty = AvaloniaProperty.Register, T>( - nameof(Maximum)); - + nameof(Maximum), coerce: CoerceMaximum); + public T Maximum { get => GetValue(MaximumProperty); @@ -181,13 +290,54 @@ public abstract class NumericUpDownBase: NumericUpDown where T: struct, IComp } public static readonly StyledProperty MinimumProperty = AvaloniaProperty.Register, T>( - nameof(Minimum)); + nameof(Minimum), coerce: CoerceMinimum); public T Minimum { get => GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); } + + #region Max and Min Coerce + private static T CoerceMaximum(AvaloniaObject instance, T value) + { + if (instance is NumericUpDownBase n) + { + return n.CoerceMaximum(value); + } + + return value; + } + + private T CoerceMaximum(T value) + { + if (value.CompareTo(Minimum) < 0) + { + return Minimum; + } + return value; + } + + private static T CoerceMinimum(AvaloniaObject instance, T value) + { + if (instance is NumericUpDownBase n) + { + return n.CoerceMinimum(value); + } + + return value; + } + + private T CoerceMinimum(T value) + { + if (value.CompareTo(Maximum) > 0) + { + return Maximum; + } + return value; + } + + #endregion public static readonly StyledProperty StepProperty = AvaloniaProperty.Register, T>( nameof(Step)); @@ -198,5 +348,245 @@ public abstract class NumericUpDownBase: NumericUpDown where T: struct, IComp set => SetValue(StepProperty, value); } - protected abstract T Clamp(); + public static readonly StyledProperty EmptyInputValueProperty = + AvaloniaProperty.Register, T?>( + nameof(EmptyInputValue), defaultValue: null); + + public T? EmptyInputValue + { + get => GetValue(EmptyInputValueProperty); + set => SetValue(EmptyInputValueProperty, value); + } + + /// + /// Defines the event. + /// + public static readonly RoutedEvent> ValueChangedEvent = + RoutedEvent.Register>(nameof(ValueChanged), RoutingStrategies.Bubble); + + /// + /// Raised when the changes. + /// + public event EventHandler>? ValueChanged + { + add => AddHandler(ValueChangedEvent, value); + remove => RemoveHandler(ValueChangedEvent, value); + } + + static NumericUpDownBase() + { + StepProperty.Changed.AddClassHandler>((o, e) => o.ChangeToSetSpinDirection(e)); + MaximumProperty.Changed.AddClassHandler>((o, e) => o.OnConstraintChanged(e)); + MinimumProperty.Changed.AddClassHandler>((o, e) => o.OnConstraintChanged(e)); + ValueProperty.Changed.AddClassHandler>((o, e) => o.OnValueChanged(e) ); + } + + private void OnConstraintChanged(AvaloniaPropertyChangedEventArgs avaloniaPropertyChangedEventArgs) + { + if (IsInitialized) + { + SetValidSpinDirection(); + } + if (Value.HasValue) + { + SetCurrentValue(ValueProperty, Clamp(Value, Maximum, Minimum)); + } + } + + private void OnValueChanged(AvaloniaPropertyChangedEventArgs args) + { + if (IsInitialized) + { + SyncTextAndValue(false, null, true); + } + SetValidSpinDirection(); + T? oldValue = args.GetOldValue(); + T? newValue = args.GetNewValue(); + var e = new ValueChangedEventArgs(ValueChangedEvent, oldValue, newValue); + RaiseEvent(e); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + if (_textBox != null) + { + _textBox.Text = ConvertValueToText(Value); + } + } + + protected virtual T? Clamp(T? value, T max, T min) + { + if (value is null) + { + return null; + } + if (value.Value.CompareTo(max) > 0) + { + return Maximum; + } + if (value.Value.CompareTo(min) < 0) + { + return Minimum; + } + return value; + } + + protected override void SetValidSpinDirection() + { + var validDirection = ValidSpinDirections.None; + if (!IsReadOnly) + { + if (Value is null) + { + validDirection = ValidSpinDirections.Increase | ValidSpinDirections.Decrease; + } + if (Value.HasValue && Value.Value.CompareTo(Maximum) < 0) + { + validDirection |= ValidSpinDirections.Increase; + } + + if (Value.HasValue && Value.Value.CompareTo(Minimum) > 0) + { + validDirection |= ValidSpinDirections.Decrease; + } + } + if (_spinner != null) + { + _spinner.ValidSpinDirection = validDirection; + } + } + + private bool _isSyncingTextAndValue; + + protected override bool SyncTextAndValue(bool fromTextToValue = false, string? text = null, bool forceTextUpdate = false) + { + if (_isSyncingTextAndValue) return true; + _isSyncingTextAndValue = true; + var parsedTextIsValid = true; + try + { + if (fromTextToValue) + { + try + { + // TODO + var newValue = ConvertTextToValue(text); + if (EmptyInputValue is not null && newValue is null) + { + newValue = EmptyInputValue; + } + if (!Equals(newValue, Value)) + { + SetCurrentValue(ValueProperty, newValue); + } + } + catch + { + parsedTextIsValid = false; + } + } + + if (!_updateFromTextInput) + { + if (forceTextUpdate) + { + var newText = ConvertValueToText(Value); + if (_textBox!= null && !Equals(_textBox.Text, newText)) + { + _textBox.Text = newText; + } + } + } + + if (_updateFromTextInput && !parsedTextIsValid) + { + if (_spinner is not null) + { + _spinner.ValidSpinDirection = ValidSpinDirections.None; + } + } + else + { + SetValidSpinDirection(); + } + } + finally + { + _isSyncingTextAndValue = false; + } + return parsedTextIsValid; + } + + protected virtual T? ConvertTextToValue(string? text) + { + T? result; + if (string.IsNullOrWhiteSpace(text)) return null; + if (TextConverter != null) + { + var valueFromText = TextConverter.Convert(text, typeof(T?), null, CultureInfo.CurrentCulture); + return (T?)valueFromText; + } + else + { + if (!ParseText(text, out var outputValue)) + { + throw new InvalidDataException("Input string was not in a correct format."); + } + + result = outputValue; + } + return result; + } + + protected virtual string? ConvertValueToText(T? value) + { + if (TextConverter is not null) + { + return TextConverter.ConvertBack(Value, typeof(int), null, CultureInfo.CurrentCulture)?.ToString(); + } + + if (FormatString.Contains("{0")) + { + return string.Format(NumberFormat, FormatString, value); + } + + return ValueToString(Value); + } + + protected override void Increase() + { + T? value; + if (Value is not null) + { + value = Add(Value.Value, Step); + } + else + { + value = IsSet(MinimumProperty) ? Minimum : Zero; + } + SetCurrentValue(ValueProperty, Clamp(value, Maximum, Minimum)); + } + + protected override void Decrease() + { + T? value; + if (Value is not null) + { + value = Minus(Value.Value, Step); + } + else + { + value = IsSet(MaximumProperty) ? Maximum : Zero; + } + + SetCurrentValue(ValueProperty, Clamp(value, Maximum, Minimum)); + } + + protected abstract bool ParseText(string? text, out T? number); + protected abstract string? ValueToString(T? value); + protected abstract T Zero { get; } + protected abstract T? Add(T? a, T? b); + protected abstract T? Minus(T? a, T? b); + } \ No newline at end of file diff --git a/src/Ursa/Controls/NumericUpDown/ValueChangedEventArgs.cs b/src/Ursa/Controls/NumericUpDown/ValueChangedEventArgs.cs new file mode 100644 index 0000000..f5929c1 --- /dev/null +++ b/src/Ursa/Controls/NumericUpDown/ValueChangedEventArgs.cs @@ -0,0 +1,15 @@ +using Avalonia.Interactivity; + +namespace Ursa.Controls; + +public class ValueChangedEventArgs: RoutedEventArgs where T: struct, IComparable +{ + public ValueChangedEventArgs(RoutedEvent routedEvent, T? oldValue, T? newValue): base(routedEvent) + { + OldValue = oldValue; + NewValue = newValue; + } + public T? OldValue { get; } + public T? NewValue { get; } + +} \ No newline at end of file