diff --git a/src/Ursa/Controls/NumericUpDown/IntUpDown.cs b/src/Ursa/Controls/NumericUpDown/IntUpDown.cs index eabbe4a..655a7b1 100644 --- a/src/Ursa/Controls/NumericUpDown/IntUpDown.cs +++ b/src/Ursa/Controls/NumericUpDown/IntUpDown.cs @@ -25,9 +25,17 @@ public class NumericIntUpDown : NumericUpDownBase protected override int Zero => 0; - protected override int? Add(int? a, int? b) => a + b; + protected override int? Add(int? a, int? b) + { + var result = a + b; + return result < Value ? Maximum : result; + } - protected override int? Minus(int? a, int? b) => a - b; + protected override int? Minus(int? a, int? b) + { + var result = a - b; + return result > Value ? Minimum : result; + } } public class NumericUIntUpDown : NumericUpDownBase @@ -53,9 +61,17 @@ public class NumericUIntUpDown : NumericUpDownBase protected override uint Zero => 0; - protected override uint? Add(uint? a, uint? b) => a + b; + protected override uint? Add(uint? a, uint? b) + { + var result = a + b; + return result < Value ? Maximum : result; + } - protected override uint? Minus(uint? a, uint? b) => a - b; + protected override uint? Minus(uint? a, uint? b) + { + var result = a - b; + return result > Value ? Minimum : result; + } } public class NumericDoubleUpDown : NumericUpDownBase @@ -99,9 +115,17 @@ public class NumericByteUpDown : NumericUpDownBase protected override byte Zero => 0; - protected override byte? Add(byte? a, byte? b) => (byte?)(a + b); + protected override byte? Add(byte? a, byte? b) + { + var result = a + b; + return (byte?)(result < Value ? Maximum : result); + } - protected override byte? Minus(byte? a, byte? b) => (byte?)(a - b); + protected override byte? Minus(byte? a, byte? b) + { + var result = a - b; + return (byte?)(result > Value ? Minimum : result); + } } public class NumericSByteUpDown : NumericUpDownBase @@ -122,9 +146,17 @@ public class NumericSByteUpDown : NumericUpDownBase protected override sbyte Zero => 0; - protected override sbyte? Add(sbyte? a, sbyte? b) => (sbyte?)(a + b); + protected override sbyte? Add(sbyte? a, sbyte? b) + { + var result = a + b; + return (sbyte?)(result < Value ? Maximum : result); + } - protected override sbyte? Minus(sbyte? a, sbyte? b) => (sbyte?)(a - b); + protected override sbyte? Minus(sbyte? a, sbyte? b) + { + var result = a - b; + return (sbyte?)(result > Value ? Minimum : result); + } } public class NumericShortUpDown : NumericUpDownBase @@ -145,9 +177,17 @@ public class NumericShortUpDown : NumericUpDownBase protected override short Zero => 0; - protected override short? Add(short? a, short? b) => (short?)(a + b); + protected override short? Add(short? a, short? b) + { + var result = a + b; + return (short?)(result < Value ? Maximum : result); + } - protected override short? Minus(short? a, short? b) => (short?)(a - b); + protected override short? Minus(short? a, short? b) + { + var result = a - b; + return (short?)(result > Value ? Minimum : result); + } } public class NumericUShortUpDown : NumericUpDownBase @@ -168,9 +208,17 @@ public class NumericUShortUpDown : NumericUpDownBase protected override ushort Zero => 0; - protected override ushort? Add(ushort? a, ushort? b) => (ushort?)(a + b); + protected override ushort? Add(ushort? a, ushort? b) + { + var result = a + b; + return (ushort?)(result < Value ? Maximum : result); + } - protected override ushort? Minus(ushort? a, ushort? b) => (ushort?)(a - b); + protected override ushort? Minus(ushort? a, ushort? b) + { + var result = a - b; + return (ushort?)(result > Value ? Minimum : result); + } } public class NumericLongUpDown : NumericUpDownBase @@ -191,9 +239,17 @@ public class NumericLongUpDown : NumericUpDownBase protected override long Zero => 0; - protected override long? Add(long? a, long? b) => a + b; + protected override long? Add(long? a, long? b) + { + var result = a + b; + return result < Value ? Maximum : result; + } - protected override long? Minus(long? a, long? b) => a - b; + protected override long? Minus(long? a, long? b) + { + var result = a - b; + return result > Value ? Minimum : result; + } } public class NumericULongUpDown : NumericUpDownBase diff --git a/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs b/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs index 956e065..5d41aaa 100644 --- a/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs +++ b/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs @@ -1,750 +1,751 @@ -using System.Diagnostics; -using System.Globalization; -using System.Net.Mime; -using System.Windows.Input; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Metadata; -using Avalonia.Controls.Primitives; -using Avalonia.Data; -using Avalonia.Data.Converters; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.Layout; -using Irihi.Avalonia.Shared.Contracts; -using Irihi.Avalonia.Shared.Helpers; - -namespace Ursa.Controls; - -[TemplatePart(PART_Spinner, typeof(ButtonSpinner))] -[TemplatePart(PART_TextBox, typeof(TextBox))] -[TemplatePart(PART_DragPanel, typeof(Panel))] -public abstract class NumericUpDown : TemplatedControl, IClearControl -{ - public const string PART_Spinner = "PART_Spinner"; - public const string PART_TextBox = "PART_TextBox"; - public const string PART_DragPanel = "PART_DragPanel"; - - protected internal ButtonSpinner? _spinner; - protected internal TextBox? _textBox; - protected internal Panel? _dragPanel; - - private Point? _point; - protected internal bool _updateFromTextInput; - - - protected internal bool _canIncrease = true; - - protected internal bool _canDecrease = true; - - - public static readonly StyledProperty AllowDragProperty = AvaloniaProperty.Register( - nameof(AllowDrag), defaultBindingMode: BindingMode.TwoWay); - - public bool AllowDrag - { - get => GetValue(AllowDragProperty); - set => SetValue(AllowDragProperty, value); - } - - public static readonly StyledProperty IsReadOnlyProperty = AvaloniaProperty.Register( - nameof(IsReadOnly), defaultBindingMode: BindingMode.TwoWay); - - public bool IsReadOnly - { - get => GetValue(IsReadOnlyProperty); - set => SetValue(IsReadOnlyProperty, value); - } - - public static readonly StyledProperty HorizontalContentAlignmentProperty = - ContentControl.HorizontalContentAlignmentProperty.AddOwner(); - public HorizontalAlignment HorizontalContentAlignment - { - get => GetValue(HorizontalContentAlignmentProperty); - set => SetValue(HorizontalContentAlignmentProperty, value); - } - - public static readonly StyledProperty InnerLeftContentProperty = AvaloniaProperty.Register( - nameof(InnerLeftContent)); - - public object? InnerLeftContent - { - get => GetValue(InnerLeftContentProperty); - set => SetValue(InnerLeftContentProperty, value); - } - - public static readonly StyledProperty WatermarkProperty = AvaloniaProperty.Register( - nameof(Watermark)); - - public string? Watermark - { - 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), defaultValue: NumberStyles.Any); - - 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 ShowButtonSpinnerProperty = - ButtonSpinner.ShowButtonSpinnerProperty.AddOwner(); - - public bool ShowButtonSpinner - { - get => GetValue(ShowButtonSpinnerProperty); - set => SetValue(ShowButtonSpinnerProperty, 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, args) => o.OnIsReadOnlyChanged(args)); - TextConverterProperty.Changed.AddClassHandler((o, e) => o.OnFormatChange(e)); - AllowDragProperty.Changed.AddClassHandler((o, e) => o.OnAllowDragChange(e)); - } - - private void OnAllowDragChange(AvaloniaPropertyChangedEventArgs args) - { - IsVisibleProperty.SetValue(args.NewValue.Value, _dragPanel); - } - - private void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs args) - { - ChangeToSetSpinDirection(args, false); - TextBox.IsReadOnlyProperty.SetValue(args.NewValue.Value, _textBox); - } - - protected void ChangeToSetSpinDirection(AvaloniaPropertyChangedEventArgs e, bool afterInitialization = false) - { - if (afterInitialization) - { - if (IsInitialized) - { - SetValidSpinDirection(); - } - } - else - { - SetValidSpinDirection(); - } - } - - protected virtual void OnFormatChange(AvaloniaPropertyChangedEventArgs arg) - { - if (IsInitialized) - { - SyncTextAndValue(false, null, true);//sync text update while OnFormatChange - } - } - - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) - { - base.OnApplyTemplate(e); - Spinner.SpinEvent.RemoveHandler(OnSpin, _spinner); - PointerPressedEvent.RemoveHandler(OnDragPanelPointerPressed, _dragPanel); - PointerMovedEvent.RemoveHandler(OnDragPanelPointerMoved, _dragPanel); - PointerReleasedEvent.RemoveHandler(OnDragPanelPointerReleased, _dragPanel); - _spinner = e.NameScope.Find(PART_Spinner); - _textBox = e.NameScope.Find(PART_TextBox); - _dragPanel = e.NameScope.Find(PART_DragPanel); - IsVisibleProperty.SetValue(AllowDrag, _dragPanel); - TextBox.IsReadOnlyProperty.SetValue(IsReadOnly, _textBox); - Spinner.SpinEvent.AddHandler(OnSpin, _spinner); - PointerPressedEvent.AddHandler(OnDragPanelPointerPressed, _dragPanel); - PointerMovedEvent.AddHandler(OnDragPanelPointerMoved, _dragPanel); - PointerReleasedEvent.AddHandler(OnDragPanelPointerReleased, _dragPanel); - } - - protected override void OnLostFocus(RoutedEventArgs e) - { - CommitInput(true); - base.OnLostFocus(e); - if (AllowDrag && _dragPanel is not null) - { - _dragPanel.IsVisible = true; - } - } - - protected override void OnKeyDown(KeyEventArgs e) - { - if (e.Key == Key.Enter) - { - var commitSuccess = CommitInput(true); - e.Handled = !commitSuccess; - } - - if (e.Key == Key.Escape) - { - if (AllowDrag && _dragPanel is not null) - { - _dragPanel.IsVisible = true; - // _dragPanel.Focus(); - _textBox?.ClearSelection(); - _spinner?.Focus(); - } - } - } - - private void OnDragPanelPointerPressed(object sender, PointerPressedEventArgs e) - { - _point = e.GetPosition(this); - if (e.ClickCount == 2 && _dragPanel is not null && AllowDrag) - { - IsVisibleProperty.SetValue(false, _dragPanel); - _textBox?.Focus(); - TextBox.IsReadOnlyProperty.SetValue(IsReadOnly, _textBox); - } - else - { - _textBox?.Focus(); - TextBox.IsReadOnlyProperty.SetValue(true, _textBox); - } - } - - protected override void OnTextInput(TextInputEventArgs e) - { - if (IsReadOnly) return; - _textBox?.RaiseEvent(e); - } - - private void OnDragPanelPointerReleased(object sender, PointerReleasedEventArgs e) - { - _point = null; - } - - private void OnDragPanelPointerMoved(object sender, PointerEventArgs e) - { - if (!AllowDrag || IsReadOnly) return; - if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; - var point = e.GetPosition(this); - var delta = point - _point; - if (delta is null) - { - return; - } - int d = GetDelta(delta.Value); - if (d > 0) - { - if (_canIncrease) - Increase(); - } - else if (d < 0) - { - if (_canDecrease) - Decrease(); - } - _point = point; - } - - private int GetDelta(Point point) - { - bool horizontal = Math.Abs(point.X) > Math.Abs(point.Y); - var value = horizontal ? point.X : -point.Y; - return value switch - { - > 0 => 1, - < 0 => -1, - _ => 0 - }; - } - - private void OnSpin(object sender, SpinEventArgs e) - { - if (AllowSpin && !IsReadOnly) - { - 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 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 void Clear(); -} - -public abstract class NumericUpDownBase : NumericUpDown where T : struct, IComparable -{ - protected static string TrimString(string? text, NumberStyles numberStyles) - { - if (text is null) return string.Empty; - text = text.Trim(); - if (text.Contains("_")) // support _ like 0x1024_1024(hex), 10_24 (normal) - { - text = text.Replace("_", ""); - } - - if ((numberStyles & NumberStyles.AllowHexSpecifier) != 0) - { - if (text.StartsWith("0x") || text.StartsWith("0X")) // support 0x hex while user input - { - text = text.Substring(2); - } - else if (text.StartsWith("h'") || text.StartsWith("H'")) // support verilog hex while user input - { - text = text.Substring(2); - } - else if (text.StartsWith("h") || text.StartsWith("H")) // support hex while user input - { - text = text.Substring(1); - } - } -#if NET8_0_OR_GREATER - else if ((numberStyles & NumberStyles.AllowBinarySpecifier) != 0) - { - if (text.StartsWith("0b") || text.StartsWith("0B")) // support 0b bin while user input - { - text = text.Substring(2); - } - else if (text.StartsWith("b'") || text.StartsWith("B'")) // support verilog bin while user input - { - text = text.Substring(2); - } - else if (text.StartsWith("b") || text.StartsWith("B")) // support bin while user input - { - text = text.Substring(1); - } - } - -#endif - return text; - } - - public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register, T?>( - nameof(Value), defaultBindingMode: BindingMode.TwoWay); - - public T? Value - { - get => GetValue(ValueProperty); - set => SetValue(ValueProperty, value); - } - - public static readonly StyledProperty MaximumProperty = AvaloniaProperty.Register, T>( - nameof(Maximum), defaultBindingMode: BindingMode.TwoWay, coerce: CoerceMaximum); - - public T Maximum - { - get => GetValue(MaximumProperty); - set => SetValue(MaximumProperty, value); - } - - public static readonly StyledProperty MinimumProperty = AvaloniaProperty.Register, T>( - nameof(Minimum), defaultBindingMode: BindingMode.TwoWay, 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)); - - public T Step - { - get => GetValue(StepProperty); - set => SetValue(StepProperty, value); - } - - public static readonly StyledProperty EmptyInputValueProperty = - AvaloniaProperty.Register, T?>( - nameof(EmptyInputValue), defaultValue: null); - - public T? EmptyInputValue - { - get => GetValue(EmptyInputValueProperty); - set => SetValue(EmptyInputValueProperty, value); - } - - - public static readonly StyledProperty CommandProperty = AvaloniaProperty.Register( - nameof(Command)); - - public ICommand? Command - { - get => GetValue(CommandProperty); - set => SetValue(CommandProperty, value); - } - - public static readonly StyledProperty CommandParameterProperty = - AvaloniaProperty.Register(nameof(CommandParameter)); - - public object? CommandParameter - { - get => this.GetValue(CommandParameterProperty); - set => this.SetValue(CommandParameterProperty, value); - } - - private void InvokeCommand(object? cp) - { - if (this.Command != null && this.Command.CanExecute(cp)) - { - this.Command.Execute(cp); - } - } - - /// - /// 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); - RaiseEventCommand(e); - } - } - - private void RaiseEventCommand(ValueChangedEventArgs e) - { - InvokeCommand(this.CommandParameter ?? e.NewValue); - RaiseEvent(e); - } - - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) - { - base.OnApplyTemplate(e); - if (_textBox != null) - { - _textBox.Text = ConvertValueToText(Value); - } - SetValidSpinDirection(); - } - - protected virtual T? Clamp(T? value, T max, T min) - { - if (value is null) - { - return null; - } - if (value.Value.CompareTo(max) > 0) - { - return max; - } - if (value.Value.CompareTo(min) < 0) - { - return min; - } - return value; - } - - protected override void SetValidSpinDirection() - { - var validDirection = ValidSpinDirections.None; - _canIncrease = false; - _canDecrease = false; - if (!IsReadOnly) - { - if (Value is null) - { - validDirection = ValidSpinDirections.Increase | ValidSpinDirections.Decrease; - } - if (Value.HasValue && Value.Value.CompareTo(Maximum) < 0) - { - validDirection |= ValidSpinDirections.Increase; - _canIncrease = true; - } - - if (Value.HasValue && Value.Value.CompareTo(Minimum) > 0) - { - validDirection |= ValidSpinDirections.Decrease; - _canDecrease = true; - } - } - 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 - { - var newValue = ConvertTextToValue(text); - if (EmptyInputValue is not null && newValue is null) - { - newValue = EmptyInputValue; - } - if (!Equals(newValue, Value)) - { - if (Equals(Clamp(newValue, Maximum, Minimum), newValue)) - { - SetCurrentValue(ValueProperty, newValue); - } - else - { - parsedTextIsValid = false; - } - } - } - catch - { - parsedTextIsValid = false; - } - } - - if (!_updateFromTextInput) - { - if (forceTextUpdate) - { - var newText = ConvertValueToText(Value); - if (_textBox != null && !Equals(_textBox.Text, newText)) - { - _textBox.Text = newText; - _textBox.CaretIndex = newText?.Length ?? 0; - } - } - } - - 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 - { - text = TrimString(text, ParsingNumberStyle); - 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); - - public override void Clear() - { - SetCurrentValue(ValueProperty, EmptyInputValue); - SyncTextAndValue(false, forceTextUpdate: true); - } +using System.Diagnostics; +using System.Globalization; +using System.Net.Mime; +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Irihi.Avalonia.Shared.Contracts; +using Irihi.Avalonia.Shared.Helpers; + +namespace Ursa.Controls; + +[TemplatePart(PART_Spinner, typeof(ButtonSpinner))] +[TemplatePart(PART_TextBox, typeof(TextBox))] +[TemplatePart(PART_DragPanel, typeof(Panel))] +public abstract class NumericUpDown : TemplatedControl, IClearControl +{ + public const string PART_Spinner = "PART_Spinner"; + public const string PART_TextBox = "PART_TextBox"; + public const string PART_DragPanel = "PART_DragPanel"; + + protected internal ButtonSpinner? _spinner; + protected internal TextBox? _textBox; + protected internal Panel? _dragPanel; + + private Point? _point; + protected internal bool _updateFromTextInput; + + + protected internal bool _canIncrease = true; + + protected internal bool _canDecrease = true; + + + public static readonly StyledProperty AllowDragProperty = AvaloniaProperty.Register( + nameof(AllowDrag), defaultBindingMode: BindingMode.TwoWay); + + public bool AllowDrag + { + get => GetValue(AllowDragProperty); + set => SetValue(AllowDragProperty, value); + } + + public static readonly StyledProperty IsReadOnlyProperty = AvaloniaProperty.Register( + nameof(IsReadOnly), defaultBindingMode: BindingMode.TwoWay); + + public bool IsReadOnly + { + get => GetValue(IsReadOnlyProperty); + set => SetValue(IsReadOnlyProperty, value); + } + + public static readonly StyledProperty HorizontalContentAlignmentProperty = + ContentControl.HorizontalContentAlignmentProperty.AddOwner(); + public HorizontalAlignment HorizontalContentAlignment + { + get => GetValue(HorizontalContentAlignmentProperty); + set => SetValue(HorizontalContentAlignmentProperty, value); + } + + public static readonly StyledProperty InnerLeftContentProperty = AvaloniaProperty.Register( + nameof(InnerLeftContent)); + + public object? InnerLeftContent + { + get => GetValue(InnerLeftContentProperty); + set => SetValue(InnerLeftContentProperty, value); + } + + public static readonly StyledProperty WatermarkProperty = AvaloniaProperty.Register( + nameof(Watermark)); + + public string? Watermark + { + 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), defaultValue: NumberStyles.Any); + + 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 ShowButtonSpinnerProperty = + ButtonSpinner.ShowButtonSpinnerProperty.AddOwner(); + + public bool ShowButtonSpinner + { + get => GetValue(ShowButtonSpinnerProperty); + set => SetValue(ShowButtonSpinnerProperty, 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, args) => o.OnIsReadOnlyChanged(args)); + TextConverterProperty.Changed.AddClassHandler((o, e) => o.OnFormatChange(e)); + AllowDragProperty.Changed.AddClassHandler((o, e) => o.OnAllowDragChange(e)); + } + + private void OnAllowDragChange(AvaloniaPropertyChangedEventArgs args) + { + IsVisibleProperty.SetValue(args.NewValue.Value, _dragPanel); + } + + private void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs args) + { + ChangeToSetSpinDirection(args, false); + TextBox.IsReadOnlyProperty.SetValue(args.NewValue.Value, _textBox); + } + + protected void ChangeToSetSpinDirection(AvaloniaPropertyChangedEventArgs e, bool afterInitialization = false) + { + if (afterInitialization) + { + if (IsInitialized) + { + SetValidSpinDirection(); + } + } + else + { + SetValidSpinDirection(); + } + } + + protected virtual void OnFormatChange(AvaloniaPropertyChangedEventArgs arg) + { + if (IsInitialized) + { + SyncTextAndValue(false, null, true);//sync text update while OnFormatChange + } + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + Spinner.SpinEvent.RemoveHandler(OnSpin, _spinner); + PointerPressedEvent.RemoveHandler(OnDragPanelPointerPressed, _dragPanel); + PointerMovedEvent.RemoveHandler(OnDragPanelPointerMoved, _dragPanel); + PointerReleasedEvent.RemoveHandler(OnDragPanelPointerReleased, _dragPanel); + _spinner = e.NameScope.Find(PART_Spinner); + _textBox = e.NameScope.Find(PART_TextBox); + _dragPanel = e.NameScope.Find(PART_DragPanel); + IsVisibleProperty.SetValue(AllowDrag, _dragPanel); + TextBox.IsReadOnlyProperty.SetValue(IsReadOnly, _textBox); + Spinner.SpinEvent.AddHandler(OnSpin, _spinner); + PointerPressedEvent.AddHandler(OnDragPanelPointerPressed, _dragPanel); + PointerMovedEvent.AddHandler(OnDragPanelPointerMoved, _dragPanel); + PointerReleasedEvent.AddHandler(OnDragPanelPointerReleased, _dragPanel); + } + + protected override void OnLostFocus(RoutedEventArgs e) + { + CommitInput(true); + base.OnLostFocus(e); + if (AllowDrag && _dragPanel is not null) + { + _dragPanel.IsVisible = true; + } + } + + protected override void OnKeyDown(KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + var commitSuccess = CommitInput(true); + e.Handled = !commitSuccess; + } + + if (e.Key == Key.Escape) + { + if (AllowDrag && _dragPanel is not null) + { + _dragPanel.IsVisible = true; + // _dragPanel.Focus(); + _textBox?.ClearSelection(); + _spinner?.Focus(); + } + } + } + + private void OnDragPanelPointerPressed(object sender, PointerPressedEventArgs e) + { + _point = e.GetPosition(this); + if (e.ClickCount == 2 && _dragPanel is not null && AllowDrag) + { + IsVisibleProperty.SetValue(false, _dragPanel); + _textBox?.Focus(); + TextBox.IsReadOnlyProperty.SetValue(IsReadOnly, _textBox); + } + else + { + _textBox?.Focus(); + TextBox.IsReadOnlyProperty.SetValue(true, _textBox); + } + } + + protected override void OnTextInput(TextInputEventArgs e) + { + if (IsReadOnly) return; + _textBox?.RaiseEvent(e); + } + + private void OnDragPanelPointerReleased(object sender, PointerReleasedEventArgs e) + { + _point = null; + } + + private void OnDragPanelPointerMoved(object sender, PointerEventArgs e) + { + if (!AllowDrag || IsReadOnly) return; + if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; + var point = e.GetPosition(this); + var delta = point - _point; + if (delta is null) + { + return; + } + int d = GetDelta(delta.Value); + if (d > 0) + { + if (_canIncrease) + Increase(); + } + else if (d < 0) + { + if (_canDecrease) + Decrease(); + } + _point = point; + } + + private int GetDelta(Point point) + { + bool horizontal = Math.Abs(point.X) > Math.Abs(point.Y); + var value = horizontal ? point.X : -point.Y; + return value switch + { + > 0 => 1, + < 0 => -1, + _ => 0 + }; + } + + private void OnSpin(object sender, SpinEventArgs e) + { + if (AllowSpin && !IsReadOnly) + { + 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 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 void Clear(); +} + +public abstract class NumericUpDownBase : NumericUpDown where T : struct, IComparable +{ + protected static string TrimString(string? text, NumberStyles numberStyles) + { + if (text is null) return string.Empty; + text = text.Trim(); + if (text.Contains("_")) // support _ like 0x1024_1024(hex), 10_24 (normal) + { + text = text.Replace("_", ""); + } + + if ((numberStyles & NumberStyles.AllowHexSpecifier) != 0) + { + if (text.StartsWith("0x") || text.StartsWith("0X")) // support 0x hex while user input + { + text = text.Substring(2); + } + else if (text.StartsWith("h'") || text.StartsWith("H'")) // support verilog hex while user input + { + text = text.Substring(2); + } + else if (text.StartsWith("h") || text.StartsWith("H")) // support hex while user input + { + text = text.Substring(1); + } + } +#if NET8_0_OR_GREATER + else if ((numberStyles & NumberStyles.AllowBinarySpecifier) != 0) + { + if (text.StartsWith("0b") || text.StartsWith("0B")) // support 0b bin while user input + { + text = text.Substring(2); + } + else if (text.StartsWith("b'") || text.StartsWith("B'")) // support verilog bin while user input + { + text = text.Substring(2); + } + else if (text.StartsWith("b") || text.StartsWith("B")) // support bin while user input + { + text = text.Substring(1); + } + } + +#endif + return text; + } + + public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register, T?>( + nameof(Value), defaultBindingMode: BindingMode.TwoWay); + + public T? Value + { + get => GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } + + public static readonly StyledProperty MaximumProperty = AvaloniaProperty.Register, T>( + nameof(Maximum), defaultBindingMode: BindingMode.TwoWay, coerce: CoerceMaximum); + + public T Maximum + { + get => GetValue(MaximumProperty); + set => SetValue(MaximumProperty, value); + } + + public static readonly StyledProperty MinimumProperty = AvaloniaProperty.Register, T>( + nameof(Minimum), defaultBindingMode: BindingMode.TwoWay, 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)); + + public T Step + { + get => GetValue(StepProperty); + set => SetValue(StepProperty, value); + } + + public static readonly StyledProperty EmptyInputValueProperty = + AvaloniaProperty.Register, T?>( + nameof(EmptyInputValue), defaultValue: null); + + public T? EmptyInputValue + { + get => GetValue(EmptyInputValueProperty); + set => SetValue(EmptyInputValueProperty, value); + } + + + public static readonly StyledProperty CommandProperty = AvaloniaProperty.Register, ICommand?>( + nameof(Command)); + + public ICommand? Command + { + get => GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + + public static readonly StyledProperty CommandParameterProperty = + AvaloniaProperty.Register, object?>(nameof(CommandParameter)); + + public object? CommandParameter + { + get => this.GetValue(CommandParameterProperty); + set => this.SetValue(CommandParameterProperty, value); + } + + private void InvokeCommand(object? cp) + { + if (this.Command != null && this.Command.CanExecute(cp)) + { + this.Command.Execute(cp); + } + } + + /// + /// 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); + RaiseEventCommand(e); + } + } + + private void RaiseEventCommand(ValueChangedEventArgs e) + { + InvokeCommand(this.CommandParameter ?? e.NewValue); + RaiseEvent(e); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + if (_textBox != null) + { + _textBox.Text = ConvertValueToText(Value); + } + SetValidSpinDirection(); + } + + protected virtual T? Clamp(T? value, T max, T min) + { + if (value is null) + { + return null; + } + if (value.Value.CompareTo(max) > 0) + { + return max; + } + if (value.Value.CompareTo(min) < 0) + { + return min; + } + return value; + } + + protected override void SetValidSpinDirection() + { + var validDirection = ValidSpinDirections.None; + _canIncrease = false; + _canDecrease = false; + if (!IsReadOnly) + { + if (Value is null) + { + validDirection = ValidSpinDirections.Increase | ValidSpinDirections.Decrease; + } + if (Value.HasValue && Value.Value.CompareTo(Maximum) < 0) + { + validDirection |= ValidSpinDirections.Increase; + _canIncrease = true; + } + + if (Value.HasValue && Value.Value.CompareTo(Minimum) > 0) + { + validDirection |= ValidSpinDirections.Decrease; + _canDecrease = true; + } + } + 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 + { + var newValue = ConvertTextToValue(text); + if (EmptyInputValue is not null && newValue is null) + { + newValue = EmptyInputValue; + } + if (!Equals(newValue, Value)) + { + if (Equals(Clamp(newValue, Maximum, Minimum), newValue)) + { + SetCurrentValue(ValueProperty, newValue); + } + else + { + parsedTextIsValid = false; + } + } + } + catch + { + parsedTextIsValid = false; + } + } + + if (!_updateFromTextInput) + { + if (forceTextUpdate) + { + var newText = ConvertValueToText(Value); + if (_textBox != null && !Equals(_textBox.Text, newText)) + { + _textBox.Text = newText; + _textBox.CaretIndex = newText?.Length ?? 0; + } + } + } + + 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 + { + text = TrimString(text, ParsingNumberStyle); + 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); + + public override void Clear() + { + SetCurrentValue(ValueProperty, EmptyInputValue); + SyncTextAndValue(false, forceTextUpdate: true); + } } \ No newline at end of file