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