From a3cdb6c1edd7927dc933148ecc481f60bd0e505a Mon Sep 17 00:00:00 2001 From: dzhelnin Date: Tue, 20 Mar 2018 12:36:28 +0300 Subject: [PATCH 01/11] Update buttons of ButtonSpinner on ValidSpinDirections changes --- src/Avalonia.Controls/ButtonSpinner.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Controls/ButtonSpinner.cs b/src/Avalonia.Controls/ButtonSpinner.cs index 3ce81e0b12..866237ecce 100644 --- a/src/Avalonia.Controls/ButtonSpinner.cs +++ b/src/Avalonia.Controls/ButtonSpinner.cs @@ -201,6 +201,11 @@ namespace Avalonia.Controls } } + protected override void OnValidSpinDirectionChanged(ValidSpinDirections oldValue, ValidSpinDirections newValue) + { + SetButtonUsage(); + } + /// /// Called when the property value changed. /// From 0953673b763e4d479ddf864b17f09207f32fe2d3 Mon Sep 17 00:00:00 2001 From: dzhelnin Date: Tue, 20 Mar 2018 13:57:55 +0300 Subject: [PATCH 02/11] Added UpDownBase class --- .../NumericUpDown/UpDownBase.cs | 824 ++++++++++++++++++ 1 file changed, 824 insertions(+) create mode 100644 src/Avalonia.Controls/NumericUpDown/UpDownBase.cs diff --git a/src/Avalonia.Controls/NumericUpDown/UpDownBase.cs b/src/Avalonia.Controls/NumericUpDown/UpDownBase.cs new file mode 100644 index 0000000000..fa95eb01a7 --- /dev/null +++ b/src/Avalonia.Controls/NumericUpDown/UpDownBase.cs @@ -0,0 +1,824 @@ +using System; +using System.Globalization; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Threading; + +namespace Avalonia.Controls +{ + public abstract class UpDownBase : TemplatedControl + { + } + + /// + /// Base class for controls that represents a TextBox with button spinners that allow incrementing and decrementing values. + /// + public abstract class UpDownBase : UpDownBase + { + /// + /// Defines the property. + /// + public static readonly StyledProperty AllowSpinProperty = + ButtonSpinner.AllowSpinProperty.AddOwner>(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ButtonSpinnerLocationProperty = + ButtonSpinner.ButtonSpinnerLocationProperty.AddOwner>(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowButtonSpinnerProperty = + ButtonSpinner.ShowButtonSpinnerProperty.AddOwner>(); + + /// + /// Defines the property. + /// + public static readonly DirectProperty, bool> ClipValueToMinMaxProperty = + AvaloniaProperty.RegisterDirect, bool>(nameof(ClipValueToMinMax), + updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b); + + /// + /// Defines the property. + /// + public static readonly DirectProperty, CultureInfo> CultureInfoProperty = + AvaloniaProperty.RegisterDirect, CultureInfo>(nameof(CultureInfo), o => o.CultureInfo, + (o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture); + + /// + /// Defines the property. + /// + public static readonly StyledProperty DefaultValueProperty = + AvaloniaProperty.Register, T>(nameof(DefaultValue)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty DisplayDefaultValueOnEmptyTextProperty = + AvaloniaProperty.Register, bool>(nameof(DisplayDefaultValueOnEmptyText)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsReadOnlyProperty = + AvaloniaProperty.Register, bool>(nameof(IsReadOnly)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaximumProperty = + AvaloniaProperty.Register, T>(nameof(Maximum), validate: OnCoerceMaximum); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinimumProperty = + AvaloniaProperty.Register, T>(nameof(Minimum), validate: OnCoerceMinimum); + + /// + /// Defines the property. + /// + public static readonly DirectProperty, string> TextProperty = + AvaloniaProperty.RegisterDirect, string>(nameof(Text), o => o.Text, (o, v) => o.Text = v, + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly DirectProperty, T> ValueProperty = + AvaloniaProperty.RegisterDirect, T>(nameof(Value), updown => updown.Value, + (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly StyledProperty WatermarkProperty = + AvaloniaProperty.Register, string>(nameof(Watermark)); + + private IDisposable _textBoxTextChangedSubscription; + private T _value; + private string _text; + private bool _internalValueSet; + private bool _clipValueToMinMax; + private bool _isSyncingTextAndValueProperties; + private bool _isTextChangedFromUI; + private CultureInfo _cultureInfo; + + /// + /// Gets the Spinner template part. + /// + protected Spinner Spinner { get; private set; } + + /// + /// Gets the TextBox template part. + /// + protected TextBox TextBox { get; private set; } + + /// + /// Gets or sets the ability to perform increment/decrement operations via the keyboard, button spinners, or mouse wheel. + /// + public bool AllowSpin + { + get { return GetValue(AllowSpinProperty); } + set { SetValue(AllowSpinProperty, value); } + } + + /// + /// Gets or sets current location of the . + /// + public Location ButtonSpinnerLocation + { + get { return GetValue(ButtonSpinnerLocationProperty); } + set { SetValue(ButtonSpinnerLocationProperty, value); } + } + + /// + /// Gets or sets a value indicating whether the spin buttons should be shown. + /// + public bool ShowButtonSpinner + { + get { return GetValue(ShowButtonSpinnerProperty); } + set { SetValue(ShowButtonSpinnerProperty, value); } + } + + /// + /// Gets or sets if the value should be clipped when minimum/maximum is reached. + /// + public bool ClipValueToMinMax + { + get { return _clipValueToMinMax; } + set { SetAndRaise(ClipValueToMinMaxProperty, ref _clipValueToMinMax, value); } + } + + /// + /// Gets or sets the current CultureInfo. + /// + public CultureInfo CultureInfo + { + get { return _cultureInfo; } + set { SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); } + } + + /// + /// Gets or sets the value to use when the is null and an increment/decrement operation is performed. + /// + public T DefaultValue + { + get { return GetValue(DefaultValueProperty); } + set { SetValue(DefaultValueProperty, value); } + } + + /// + /// Gets or sets if the defaultValue should be displayed when the Text is empty. + /// + public bool DisplayDefaultValueOnEmptyText + { + get { return GetValue(DisplayDefaultValueOnEmptyTextProperty); } + set { SetValue(DisplayDefaultValueOnEmptyTextProperty, value); } + } + + /// + /// Gets or sets if the control is read only. + /// + public bool IsReadOnly + { + get { return GetValue(IsReadOnlyProperty); } + set { SetValue(IsReadOnlyProperty, value); } + } + + /// + /// Gets or sets the maximum allowed value. + /// + public T Maximum + { + get { return GetValue(MaximumProperty); } + set { SetValue(MaximumProperty, value); } + } + + /// + /// Gets or sets the minimum allowed value. + /// + public T Minimum + { + get { return GetValue(MinimumProperty); } + set { SetValue(MinimumProperty, value); } + } + + /// + /// Gets or sets the formatted string representation of the value. + /// + public string Text + { + get { return _text; } + set { SetAndRaise(TextProperty, ref _text, value); } + } + + /// + /// Gets or sets the value. + /// + public T Value + { + get { return _value; } + set + { + value = OnCoerceValue(value); + SetAndRaise(ValueProperty, ref _value, value); + } + } + + /// + /// Gets or sets the object to use as a watermark if the is null. + /// + public string Watermark + { + get { return GetValue(WatermarkProperty); } + set { SetValue(WatermarkProperty, value); } + } + + /// + /// Initializes new instance of class. + /// + protected UpDownBase() + { + Initialized += (sender, e) => + { + if (!_internalValueSet && IsInitialized) + { + SyncTextAndValueProperties(false, null, true); + } + + SetValidSpinDirection(); + }; + } + + /// + /// Initializes static members of the class. + /// + static UpDownBase() + { + CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged); + DefaultValueProperty.Changed.Subscribe(OnDefaultValueChanged); + DisplayDefaultValueOnEmptyTextProperty.Changed.Subscribe(OnDisplayDefaultValueOnEmptyTextChanged); + IsReadOnlyProperty.Changed.Subscribe(OnIsReadOnlyChanged); + MaximumProperty.Changed.Subscribe(OnMaximumChanged); + MinimumProperty.Changed.Subscribe(OnMinimumChanged); + TextProperty.Changed.Subscribe(OnTextChanged); + ValueProperty.Changed.Subscribe(OnValueChanged); + } + + /// + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + if (TextBox != null) + { + TextBox.PointerPressed -= TextBoxOnPointerPressed; + _textBoxTextChangedSubscription?.Dispose(); + } + TextBox = e.NameScope.Find("PART_TextBox"); + if (TextBox != null) + { + TextBox.Text = Text; + TextBox.PointerPressed += TextBoxOnPointerPressed; + _textBoxTextChangedSubscription = TextBox.GetObservable(TextBox.TextProperty).Subscribe(txt => TextBoxOnTextChanged()); + } + + if (Spinner != null) + { + Spinner.Spin -= OnSpinnerSpin; + } + + Spinner = e.NameScope.Find("PART_Spinner"); + + if (Spinner != null) + { + Spinner.Spin += OnSpinnerSpin; + } + + SetValidSpinDirection(); + } + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + switch (e.Key) + { + case Key.Enter: + var commitSuccess = CommitInput(); + e.Handled = !commitSuccess; + break; + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnCultureInfoChanged(CultureInfo oldValue, CultureInfo newValue) + { + if (IsInitialized) + { + SyncTextAndValueProperties(false, null); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnDefaultValueChanged(T oldValue, T newValue) + { + if (IsInitialized && string.IsNullOrEmpty(Text)) + { + SyncTextAndValueProperties(true, Text); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnDisplayDefaultValueOnEmptyTextChanged(bool oldValue, bool newValue) + { + if (IsInitialized && string.IsNullOrEmpty(Text)) + { + SyncTextAndValueProperties(false, Text); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnIsReadOnlyChanged(bool oldValue, bool newValue) + { + SetValidSpinDirection(); + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnMaximumChanged(T oldValue, T newValue) + { + if (IsInitialized) + { + SetValidSpinDirection(); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnMinimumChanged(T oldValue, T newValue) + { + if (IsInitialized) + { + SetValidSpinDirection(); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnTextChanged(string oldValue, string newValue) + { + if (IsInitialized) + { + SyncTextAndValueProperties(true, Text); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnValueChanged(T oldValue, T newValue) + { + if (!_internalValueSet && IsInitialized) + { + SyncTextAndValueProperties(false, null, true); + } + + SetValidSpinDirection(); + + RaiseValueChangedEvent(oldValue, newValue); + } + + /// + /// Called when the property has to be coerced. + /// + /// The value. + protected virtual T OnCoerceMaximum(T baseValue) + { + return baseValue; + } + + /// + /// Called when the property has to be coerced. + /// + /// The value. + protected virtual T OnCoerceMinimum(T baseValue) + { + return baseValue; + } + + /// + /// Called when the property has to be coerced. + /// + /// The value. + protected virtual T OnCoerceValue(T baseValue) + { + return baseValue; + } + + /// + /// Raises the OnSpin event when spinning is initiated by the end-user. + /// + /// The event args. + protected virtual void OnSpin(SpinEventArgs e) + { + if (e == null) + { + throw new ArgumentNullException("e"); + } + + var handler = Spinned; + handler?.Invoke(this, e); + + if (e.Direction == SpinDirection.Increase) + { + DoIncrement(); + } + else + { + DoDecrement(); + } + } + + /// + /// Raises the event. + /// + /// The old value. + /// The new value. + protected virtual void RaiseValueChangedEvent(T oldValue, T newValue) + { + var e = new UpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue); + RaiseEvent(e); + } + + /// + /// Converts the formatted text to a value. + /// + protected abstract T ConvertTextToValue(string text); + + /// + /// Converts the value to formatted text. + /// + /// + protected abstract string ConvertValueToText(); + + /// + /// Called by OnSpin when the spin direction is SpinDirection.Increase. + /// + protected abstract void OnIncrement(); + + /// + /// Called by OnSpin when the spin direction is SpinDirection.Descrease. + /// + protected abstract void OnDecrement(); + + /// + /// Sets the valid spin directions. + /// + protected abstract void SetValidSpinDirection(); + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnCultureInfoChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is UpDownBase upDown) + { + var oldValue = (CultureInfo)e.OldValue; + var newValue = (CultureInfo)e.NewValue; + upDown.OnCultureInfoChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnDefaultValueChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is UpDownBase upDown) + { + var oldValue = (T)e.OldValue; + var newValue = (T)e.NewValue; + upDown.OnDefaultValueChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnDisplayDefaultValueOnEmptyTextChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is UpDownBase upDown) + { + var oldValue = (bool) e.OldValue; + var newValue = (bool) e.NewValue; + upDown.OnDisplayDefaultValueOnEmptyTextChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is UpDownBase upDown) + { + var oldValue = (bool)e.OldValue; + var newValue = (bool)e.NewValue; + upDown.OnIsReadOnlyChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnMaximumChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is UpDownBase upDown) + { + var oldValue = (T)e.OldValue; + var newValue = (T)e.NewValue; + upDown.OnMaximumChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is UpDownBase upDown) + { + var oldValue = (T)e.OldValue; + var newValue = (T)e.NewValue; + upDown.OnMinimumChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnTextChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is UpDownBase upDown) + { + var oldValue = (string)e.OldValue; + var newValue = (string)e.NewValue; + upDown.OnTextChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnValueChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is UpDownBase upDown) + { + var oldValue = (T)e.OldValue; + var newValue = (T)e.NewValue; + upDown.OnValueChanged(oldValue, newValue); + } + } + + private void SetValueInternal(T value) + { + _internalValueSet = true; + try + { + Value = value; + } + finally + { + _internalValueSet = false; + } + } + + private static T OnCoerceMaximum(UpDownBase upDown, T value) + { + return upDown.OnCoerceMaximum(value); + } + + private static T OnCoerceMinimum(UpDownBase upDown, T value) + { + return upDown.OnCoerceMinimum(value); + } + + private void TextBoxOnTextChanged() + { + try + { + _isTextChangedFromUI = true; + if (TextBox != null) + { + Text = TextBox.Text; + } + } + finally + { + _isTextChangedFromUI = false; + } + } + + private void OnSpinnerSpin(object sender, SpinEventArgs e) + { + if (AllowSpin && !IsReadOnly) + { + var spin = !e.UsingMouseWheel; + spin |= ((TextBox != null) && TextBox.IsFocused); + + if (spin) + { + e.Handled = true; + OnSpin(e); + } + } + } + + internal void DoDecrement() + { + if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Decrease) == ValidSpinDirections.Decrease) + { + OnDecrement(); + } + } + + internal void DoIncrement() + { + if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Increase) == ValidSpinDirections.Increase) + { + OnIncrement(); + } + } + + public event EventHandler Spinned; + + private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e) + { + if (e.Device.Captured != Spinner) + { + Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input); + } + } + + /// + /// Defines the event. + /// + public static readonly RoutedEvent> ValueChangedEvent = + RoutedEvent.Register, UpDownValueChangedEventArgs>(nameof(ValueChanged), RoutingStrategies.Bubble); + + /// + /// Raised when the changes. + /// + public event EventHandler ValueChanged + { + add { AddHandler(ValueChangedEvent, value); } + remove { RemoveHandler(ValueChangedEvent, value); } + } + + private bool CommitInput() + { + return SyncTextAndValueProperties(true, Text); + } + + /// + /// Synchronize and properties. + /// + /// If value should be updated from text. + /// The text. + protected bool SyncTextAndValueProperties(bool updateValueFromText, string text) + { + return SyncTextAndValueProperties(updateValueFromText, text, false); + } + + /// + /// Synchronize and properties. + /// + /// If value should be updated from text. + /// The text. + /// Force text update. + private bool SyncTextAndValueProperties(bool updateValueFromText, string text, bool forceTextUpdate) + { + if (_isSyncingTextAndValueProperties) + return true; + + _isSyncingTextAndValueProperties = true; + var parsedTextIsValid = true; + try + { + if (updateValueFromText) + { + if (string.IsNullOrEmpty(text)) + { + // An empty input sets the value to the default value. + SetValueInternal(DefaultValue); + } + else + { + try + { + var newValue = ConvertTextToValue(text); + if (!Equals(newValue, Value)) + { + SetValueInternal(newValue); + } + } + catch + { + parsedTextIsValid = false; + } + } + } + + // Do not touch the ongoing text input from user. + if (!_isTextChangedFromUI) + { + // Don't replace the empty Text with the non-empty representation of DefaultValue. + var shouldKeepEmpty = !forceTextUpdate && string.IsNullOrEmpty(Text) && Equals(Value, DefaultValue) && !DisplayDefaultValueOnEmptyText; + if (!shouldKeepEmpty) + { + var newText = ConvertValueToText(); + if (!Equals(Text, newText)) + { + Text = newText; + } + } + + // Sync Text and textBox + if (TextBox != null) + { + TextBox.Text = Text; + } + } + + if (_isTextChangedFromUI && !parsedTextIsValid) + { + // Text input was made from the user and the text + // repesents an invalid value. Disable the spinner in this case. + if (Spinner != null) + { + Spinner.ValidSpinDirection = ValidSpinDirections.None; + } + } + else + { + SetValidSpinDirection(); + } + } + finally + { + _isSyncingTextAndValueProperties = false; + } + return parsedTextIsValid; + } + } + + public class UpDownValueChangedEventArgs : RoutedEventArgs + { + public UpDownValueChangedEventArgs(RoutedEvent routedEvent, T oldValue, T newValue) : base(routedEvent) + { + OldValue = oldValue; + NewValue = newValue; + } + + public T OldValue { get; } + public T NewValue { get; } + } +} From d07ceec674d1a555233cc417fe35394794326942 Mon Sep 17 00:00:00 2001 From: dzhelnin Date: Tue, 20 Mar 2018 14:11:24 +0300 Subject: [PATCH 03/11] Added NumericUpDown class. --- .../NumericUpDown/NumericUpDown.cs | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs new file mode 100644 index 0000000000..402dec7284 --- /dev/null +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -0,0 +1,132 @@ +using System; +using System.Globalization; + +namespace Avalonia.Controls +{ + /// + /// Base class for controls that represents a TextBox with button spinners that allow incrementing and decrementing numeric values. + /// + public abstract class NumericUpDown : UpDownBase + { + /// + /// Defines the property. + /// + public static readonly StyledProperty FormatStringProperty = + AvaloniaProperty.Register, string>(nameof(FormatString), string.Empty); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IncrementProperty = + AvaloniaProperty.Register, T>(nameof(Increment), default(T), validate: OnCoerceIncrement); + + /// + /// Initializes static members of the class. + /// + static NumericUpDown() + { + FormatStringProperty.Changed.Subscribe(FormatStringChanged); + IncrementProperty.Changed.Subscribe(IncrementChanged); + } + + /// + /// Gets or sets the display format of the . + /// + public string FormatString + { + get { return GetValue(FormatStringProperty); } + set { SetValue(FormatStringProperty, value); } + } + + /// + /// Gets or sets the amount in which to increment the . + /// + public T Increment + { + get { return GetValue(IncrementProperty); } + set { SetValue(IncrementProperty, value); } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnFormatStringChanged(string oldValue, string newValue) + { + if (IsInitialized) + { + SyncTextAndValueProperties(false, null); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnIncrementChanged(T oldValue, T newValue) + { + if (IsInitialized) + { + SetValidSpinDirection(); + } + } + + /// + /// Called when the property has to be coerced. + /// + /// The value. + protected virtual T OnCoerceIncrement(T baseValue) + { + return baseValue; + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void IncrementChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (T)e.OldValue; + var newValue = (T)e.NewValue; + upDown.OnIncrementChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void FormatStringChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (string) e.OldValue; + var newValue = (string) e.NewValue; + upDown.OnFormatStringChanged(oldValue, newValue); + } + } + + private static T OnCoerceIncrement(NumericUpDown numericUpDown, T value) + { + return numericUpDown.OnCoerceIncrement(value); + } + + /// + /// Parse percent format text + /// + /// Text to parse. + /// The culture info. + protected static decimal ParsePercent(string text, IFormatProvider cultureInfo) + { + var info = NumberFormatInfo.GetInstance(cultureInfo); + text = text.Replace(info.PercentSymbol, null); + var result = decimal.Parse(text, NumberStyles.Any, info); + result = result / 100; + return result; + } + } +} \ No newline at end of file From 272c0c882fa6c53b3b98fc9fb2af6db10bfd2ca2 Mon Sep 17 00:00:00 2001 From: dzhelnin Date: Tue, 20 Mar 2018 14:54:42 +0300 Subject: [PATCH 04/11] Added CommonNumericUpDown class. --- .../NumericUpDown/CommonNumericUpDown.cs | 346 ++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 src/Avalonia.Controls/NumericUpDown/CommonNumericUpDown.cs diff --git a/src/Avalonia.Controls/NumericUpDown/CommonNumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/CommonNumericUpDown.cs new file mode 100644 index 0000000000..1ce2508b4a --- /dev/null +++ b/src/Avalonia.Controls/NumericUpDown/CommonNumericUpDown.cs @@ -0,0 +1,346 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; + +namespace Avalonia.Controls +{ + /// + /// Base class for controls that represents a TextBox with button spinners that allow incrementing and decrementing numeric values. + /// + public abstract class CommonNumericUpDown : NumericUpDown where T : struct, IFormattable, IComparable + { + protected delegate bool FromText(string s, NumberStyles style, IFormatProvider provider, out T result); + protected delegate T FromDecimal(decimal d); + + private readonly FromText _fromText; + private readonly FromDecimal _fromDecimal; + private readonly Func _fromLowerThan; + private readonly Func _fromGreaterThan; + + private NumberStyles _parsingNumberStyle = NumberStyles.Any; + + /// + /// Defines the property. + /// + public static readonly DirectProperty, NumberStyles> ParsingNumberStyleProperty = + AvaloniaProperty.RegisterDirect, NumberStyles>(nameof(ParsingNumberStyle), + updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style); + + + /// + /// Initializes new instance of the class. + /// + /// Delegate to parse value from text. + /// Delegate to parse value from decimal. + /// Delegate to compare if one value is lower than another. + /// Delegate to compare if one value is greater than another. + protected CommonNumericUpDown(FromText fromText, FromDecimal fromDecimal, Func fromLowerThan, Func fromGreaterThan) + { + _fromText = fromText ?? throw new ArgumentNullException(nameof(fromText)); + _fromDecimal = fromDecimal ?? throw new ArgumentNullException(nameof(fromDecimal)); + _fromLowerThan = fromLowerThan ?? throw new ArgumentNullException(nameof(fromLowerThan)); + _fromGreaterThan = fromGreaterThan ?? throw new ArgumentNullException(nameof(fromGreaterThan)); + } + + /// + /// Gets or sets the parsing style (AllowLeadingWhite, Float, AllowHexSpecifier, ...). By default, Any. + /// + public NumberStyles ParsingNumberStyle + { + get { return _parsingNumberStyle; } + set { SetAndRaise(ParsingNumberStyleProperty, ref _parsingNumberStyle, value); } + } + + /// + protected override void OnIncrement() + { + if (!HandleNullSpin()) + { + var result = IncrementValue(Value.Value, Increment.Value); + Value = CoerceValueMinMax(result); + } + } + + /// + protected override void OnDecrement() + { + if (!HandleNullSpin()) + { + var result = DecrementValue(Value.Value, Increment.Value); + Value = CoerceValueMinMax(result); + } + } + + /// + protected override void OnMinimumChanged(T? oldValue, T? newValue) + { + base.OnMinimumChanged(oldValue, newValue); + + if (Value.HasValue && ClipValueToMinMax) + { + Value = CoerceValueMinMax(Value.Value); + } + } + + /// + protected override void OnMaximumChanged(T? oldValue, T? newValue) + { + base.OnMaximumChanged(oldValue, newValue); + + if (Value.HasValue && ClipValueToMinMax) + { + Value = CoerceValueMinMax(Value.Value); + } + } + + /// + protected override T? ConvertTextToValue(string text) + { + T? result = null; + + if (string.IsNullOrEmpty(text)) + { + return result; + } + + // Since the conversion from Value to text using a FormartString may not be parsable, + // we verify that the already existing text is not the exact same value. + var currentValueText = ConvertValueToText(); + if (Equals(currentValueText, text)) + { + return Value; + } + + result = ConvertTextToValueCore(currentValueText, text); + + if (ClipValueToMinMax) + { + return GetClippedMinMaxValue(result); + } + + ValidateDefaultMinMax(result); + + return result; + } + + /// + protected override string ConvertValueToText() + { + if (Value == null) + { + return string.Empty; + } + + //Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind. + if (FormatString.Contains("{0")) + { + return string.Format(CultureInfo, FormatString, Value.Value); + } + + return Value.Value.ToString(FormatString, CultureInfo); + } + + /// + protected override void SetValidSpinDirection() + { + var validDirections = ValidSpinDirections.None; + + // Null increment always prevents spin. + if (Increment != null && !IsReadOnly) + { + if (IsLowerThan(Value, Maximum) || !Value.HasValue || !Maximum.HasValue) + { + validDirections = validDirections | ValidSpinDirections.Increase; + } + + if (IsGreaterThan(Value, Minimum) || !Value.HasValue || !Minimum.HasValue) + { + validDirections = validDirections | ValidSpinDirections.Decrease; + } + } + + if (Spinner != null) + { + Spinner.ValidSpinDirection = validDirections; + } + } + + /// + /// Checks if provided value is within allowed values. + /// + /// The alowed values. + /// The value to check. + protected void TestInputSpecialValue(AllowedSpecialValues allowedValues, AllowedSpecialValues valueToCompare) + { + if ((allowedValues & valueToCompare) != valueToCompare) + { + switch (valueToCompare) + { + case AllowedSpecialValues.NaN: + throw new InvalidDataException("Value to parse shouldn't be NaN."); + case AllowedSpecialValues.PositiveInfinity: + throw new InvalidDataException("Value to parse shouldn't be Positive Infinity."); + case AllowedSpecialValues.NegativeInfinity: + throw new InvalidDataException("Value to parse shouldn't be Negative Infinity."); + } + } + } + + protected static void UpdateMetadata(Type type, T? increment, T? minimun, T? maximum) + { + IncrementProperty.OverrideDefaultValue(type, increment); + MinimumProperty.OverrideDefaultValue(type, minimun); + MaximumProperty.OverrideDefaultValue(type, maximum); + } + + protected abstract T IncrementValue(T value, T increment); + + protected abstract T DecrementValue(T value, T increment); + + private bool IsLowerThan(T? value1, T? value2) + { + if (value1 == null || value2 == null) + { + return false; + } + return _fromLowerThan(value1.Value, value2.Value); + } + + private bool IsGreaterThan(T? value1, T? value2) + { + if (value1 == null || value2 == null) + { + return false; + } + return _fromGreaterThan(value1.Value, value2.Value); + } + + private bool HandleNullSpin() + { + if (!Value.HasValue) + { + var forcedValue = DefaultValue ?? default(T); + Value = CoerceValueMinMax(forcedValue); + return true; + } + else if (!Increment.HasValue) + { + return true; + } + return false; + } + + internal bool IsValid(T? value) + { + return !IsLowerThan(value, Minimum) && !IsGreaterThan(value, Maximum); + } + + private T? CoerceValueMinMax(T value) + { + if (IsLowerThan(value, Minimum)) + { + return Minimum; + } + else if (IsGreaterThan(value, Maximum)) + { + return Maximum; + } + else + { + return value; + } + } + + private bool IsPercent(string stringToTest) + { + var PIndex = stringToTest.IndexOf("P", StringComparison.Ordinal); + if (PIndex >= 0) + { + //stringToTest contains a "P" between 2 "'", it's considered as text, not percent + var isText = stringToTest.Substring(0, PIndex).Contains("'") + && stringToTest.Substring(PIndex, FormatString.Length - PIndex).Contains("'"); + + return !isText; + } + return false; + } + + private T? ConvertTextToValueCore(string currentValueText, string text) + { + T? result; + + if (IsPercent(FormatString)) + { + result = _fromDecimal(ParsePercent(text, CultureInfo)); + } + else + { + // Problem while converting new text + if (!_fromText(text, ParsingNumberStyle, CultureInfo, out T outputValue)) + { + var shouldThrow = true; + + // Check if CurrentValueText is also failing => it also contains special characters. ex : 90° + if (!_fromText(currentValueText, ParsingNumberStyle, CultureInfo, out T _)) + { + // extract non-digit characters + var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c)); + var textSpecialCharacters = text.Where(c => !char.IsDigit(c)); + // same non-digit characters on currentValueText and new text => remove them on new Text to parse it again. + if (currentValueTextSpecialCharacters.Except(textSpecialCharacters).ToList().Count == 0) + { + foreach (var character in textSpecialCharacters) + { + text = text.Replace(character.ToString(), string.Empty); + } + // if without the special characters, parsing is good, do not throw + if (_fromText(text, ParsingNumberStyle, CultureInfo, out outputValue)) + { + shouldThrow = false; + } + } + } + + if (shouldThrow) + { + throw new InvalidDataException("Input string was not in a correct format."); + } + } + result = outputValue; + } + return result; + } + + private T? GetClippedMinMaxValue(T? result) + { + if (IsGreaterThan(result, Maximum)) + { + return Maximum; + } + else if (IsLowerThan(result, Minimum)) + { + return Minimum; + } + return result; + } + + private void ValidateDefaultMinMax(T? value) + { + // DefaultValue is always accepted. + if (Equals(value, DefaultValue)) + { + return; + } + + if (IsLowerThan(value, Minimum)) + { + throw new ArgumentOutOfRangeException(nameof(Minimum), string.Format("Value must be greater than Minimum value of {0}", Minimum)); + } + else if (IsGreaterThan(value, Maximum)) + { + throw new ArgumentOutOfRangeException(nameof(Maximum), string.Format("Value must be less than Maximum value of {0}", Maximum)); + } + } + } +} \ No newline at end of file From c1ed2b3b280b4341139796b2c7ec558f888293fa Mon Sep 17 00:00:00 2001 From: dzhelnin Date: Tue, 20 Mar 2018 15:14:53 +0300 Subject: [PATCH 05/11] Added concrete implementations for various numeric types --- .../NumericUpDown/AllowedSpecialValues.cs | 15 +++ .../NumericUpDown/ByteUpDown.cs | 24 ++++ .../NumericUpDown/CommonNumericUpDown.cs | 2 +- .../NumericUpDown/DecimalUpDown.cs | 24 ++++ .../NumericUpDown/DoubleUpDown.cs | 109 ++++++++++++++++++ .../NumericUpDown/IntegerUpDown.cs | 24 ++++ .../NumericUpDown/LongUpDown.cs | 24 ++++ .../NumericUpDown/ShortUpDown.cs | 24 ++++ .../NumericUpDown/SingleUpDown.cs | 106 +++++++++++++++++ 9 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Controls/NumericUpDown/AllowedSpecialValues.cs create mode 100644 src/Avalonia.Controls/NumericUpDown/ByteUpDown.cs create mode 100644 src/Avalonia.Controls/NumericUpDown/DecimalUpDown.cs create mode 100644 src/Avalonia.Controls/NumericUpDown/DoubleUpDown.cs create mode 100644 src/Avalonia.Controls/NumericUpDown/IntegerUpDown.cs create mode 100644 src/Avalonia.Controls/NumericUpDown/LongUpDown.cs create mode 100644 src/Avalonia.Controls/NumericUpDown/ShortUpDown.cs create mode 100644 src/Avalonia.Controls/NumericUpDown/SingleUpDown.cs diff --git a/src/Avalonia.Controls/NumericUpDown/AllowedSpecialValues.cs b/src/Avalonia.Controls/NumericUpDown/AllowedSpecialValues.cs new file mode 100644 index 0000000000..a4ec5ecdaf --- /dev/null +++ b/src/Avalonia.Controls/NumericUpDown/AllowedSpecialValues.cs @@ -0,0 +1,15 @@ +using System; + +namespace Avalonia.Controls +{ + [Flags] + public enum AllowedSpecialValues + { + None = 0, + NaN = 1, + PositiveInfinity = 2, + NegativeInfinity = 4, + AnyInfinity = PositiveInfinity | NegativeInfinity, + Any = NaN | AnyInfinity + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/ByteUpDown.cs b/src/Avalonia.Controls/NumericUpDown/ByteUpDown.cs new file mode 100644 index 0000000000..835abae773 --- /dev/null +++ b/src/Avalonia.Controls/NumericUpDown/ByteUpDown.cs @@ -0,0 +1,24 @@ +namespace Avalonia.Controls +{ + /// + public class ByteUpDown : CommonNumericUpDown + { + /// + /// Initializes static members of the class. + /// + static ByteUpDown() => UpdateMetadata(typeof(ByteUpDown), 1, byte.MinValue, byte.MaxValue); + + /// + /// Initializes new instance of the class. + /// + public ByteUpDown() : base(byte.TryParse, decimal.ToByte, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2) + { + } + + /// + protected override byte IncrementValue(byte value, byte increment) => (byte)(value + increment); + + /// + protected override byte DecrementValue(byte value, byte increment) => (byte)(value - increment); + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/CommonNumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/CommonNumericUpDown.cs index 1ce2508b4a..eed792d9dd 100644 --- a/src/Avalonia.Controls/NumericUpDown/CommonNumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/CommonNumericUpDown.cs @@ -6,7 +6,7 @@ using System.Linq; namespace Avalonia.Controls { /// - /// Base class for controls that represents a TextBox with button spinners that allow incrementing and decrementing numeric values. + /// Base class for controls that represents a TextBox with button spinners that allow incrementing and decrementing nullable numeric values. /// public abstract class CommonNumericUpDown : NumericUpDown where T : struct, IFormattable, IComparable { diff --git a/src/Avalonia.Controls/NumericUpDown/DecimalUpDown.cs b/src/Avalonia.Controls/NumericUpDown/DecimalUpDown.cs new file mode 100644 index 0000000000..10cfa537d7 --- /dev/null +++ b/src/Avalonia.Controls/NumericUpDown/DecimalUpDown.cs @@ -0,0 +1,24 @@ +namespace Avalonia.Controls +{ + /// + public class DecimalUpDown : CommonNumericUpDown + { + /// + /// Initializes static members of the class. + /// + static DecimalUpDown() => UpdateMetadata(typeof(DecimalUpDown), 1m, decimal.MinValue, decimal.MaxValue); + + /// + /// Initializes new instance of the class. + /// + public DecimalUpDown() : base(decimal.TryParse, d => d, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2) + { + } + + /// + protected override decimal IncrementValue(decimal value, decimal increment) => value + increment; + + /// + protected override decimal DecrementValue(decimal value, decimal increment) => value - increment; + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/DoubleUpDown.cs b/src/Avalonia.Controls/NumericUpDown/DoubleUpDown.cs new file mode 100644 index 0000000000..edee1247fb --- /dev/null +++ b/src/Avalonia.Controls/NumericUpDown/DoubleUpDown.cs @@ -0,0 +1,109 @@ +using System; + +namespace Avalonia.Controls +{ + /// + public class DoubleUpDown : CommonNumericUpDown + { + /// + /// Defines the property. + /// + public static readonly DirectProperty AllowInputSpecialValuesProperty = + AvaloniaProperty.RegisterDirect(nameof(AllowInputSpecialValues), + updown => updown.AllowInputSpecialValues, (updown, v) => updown.AllowInputSpecialValues = v); + + private AllowedSpecialValues _allowInputSpecialValues; + + /// + /// Initializes static members of the class. + /// + static DoubleUpDown() => UpdateMetadata(typeof(DoubleUpDown), 1d, double.NegativeInfinity, double.PositiveInfinity); + + /// + /// Initializes new instance of the class. + /// + public DoubleUpDown() : base(double.TryParse, decimal.ToDouble, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2) + { + } + + /// + /// Gets or sets a value representing the special values the user is allowed to input, such as "Infinity", "-Infinity" and "NaN" values. + /// + public AllowedSpecialValues AllowInputSpecialValues + { + get { return _allowInputSpecialValues; } + set { SetAndRaise(AllowInputSpecialValuesProperty, ref _allowInputSpecialValues, value); } + } + + /// + protected override double IncrementValue(double value, double increment) => value + increment; + + /// + protected override double DecrementValue(double value, double increment) => value - increment; + + /// + protected override double? OnCoerceIncrement(double? baseValue) + { + if (baseValue.HasValue && double.IsNaN(baseValue.Value)) + { + throw new ArgumentException("NaN is invalid for Increment."); + } + return base.OnCoerceIncrement(baseValue); + } + + /// + protected override double? OnCoerceMaximum(double? baseValue) + { + if (baseValue.HasValue && double.IsNaN(baseValue.Value)) + { + throw new ArgumentException("NaN is invalid for Maximum."); + } + return base.OnCoerceMaximum(baseValue); + } + + /// + protected override double? OnCoerceMinimum(double? baseValue) + { + if (baseValue.HasValue && double.IsNaN(baseValue.Value)) + { + throw new ArgumentException("NaN is invalid for Minimum."); + } + return base.OnCoerceMinimum(baseValue); + } + + /// + protected override void SetValidSpinDirection() + { + if (Value.HasValue && double.IsInfinity(Value.Value) && (Spinner != null)) + { + Spinner.ValidSpinDirection = ValidSpinDirections.None; + } + else + { + base.SetValidSpinDirection(); + } + } + + /// + protected override double? ConvertTextToValue(string text) + { + var result = base.ConvertTextToValue(text); + if (result != null) + { + if (double.IsNaN(result.Value)) + { + TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.NaN); + } + else if (double.IsPositiveInfinity(result.Value)) + { + TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.PositiveInfinity); + } + else if (double.IsNegativeInfinity(result.Value)) + { + TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.NegativeInfinity); + } + } + return result; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/IntegerUpDown.cs b/src/Avalonia.Controls/NumericUpDown/IntegerUpDown.cs new file mode 100644 index 0000000000..7f9e05762c --- /dev/null +++ b/src/Avalonia.Controls/NumericUpDown/IntegerUpDown.cs @@ -0,0 +1,24 @@ +namespace Avalonia.Controls +{ + /// + public class IntegerUpDown : CommonNumericUpDown + { + /// + /// Initializes static members of the class. + /// + static IntegerUpDown() => UpdateMetadata(typeof(IntegerUpDown), 1, int.MinValue, int.MaxValue); + + /// + /// Initializes new instance of the class. + /// + public IntegerUpDown() : base(int.TryParse, decimal.ToInt32, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2) + { + } + + /// + protected override int IncrementValue(int value, int increment) => value + increment; + + /// + protected override int DecrementValue(int value, int increment) => value - increment; + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/LongUpDown.cs b/src/Avalonia.Controls/NumericUpDown/LongUpDown.cs new file mode 100644 index 0000000000..d116df0ad2 --- /dev/null +++ b/src/Avalonia.Controls/NumericUpDown/LongUpDown.cs @@ -0,0 +1,24 @@ +namespace Avalonia.Controls +{ + /// + public class LongUpDown : CommonNumericUpDown + { + /// + /// Initializes static members of the class. + /// + static LongUpDown() => UpdateMetadata(typeof(LongUpDown), 1L, long.MinValue, long.MaxValue); + + /// + /// Initializes new instance of the class. + /// + public LongUpDown() : base(long.TryParse, decimal.ToInt64, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2) + { + } + + /// + protected override long IncrementValue(long value, long increment) => value + increment; + + /// + protected override long DecrementValue(long value, long increment) => value - increment; + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/ShortUpDown.cs b/src/Avalonia.Controls/NumericUpDown/ShortUpDown.cs new file mode 100644 index 0000000000..cdba5b9b64 --- /dev/null +++ b/src/Avalonia.Controls/NumericUpDown/ShortUpDown.cs @@ -0,0 +1,24 @@ +namespace Avalonia.Controls +{ + /// + public class ShortUpDown : CommonNumericUpDown + { + /// + /// Initializes static members of the class. + /// + static ShortUpDown() => UpdateMetadata(typeof(ShortUpDown), 1, short.MinValue, short.MaxValue); + + /// + /// Initializes new instance of the class. + /// + public ShortUpDown() : base(short.TryParse, decimal.ToInt16, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2) + { + } + + /// + protected override short IncrementValue(short value, short increment) => (short)(value + increment); + + /// + protected override short DecrementValue(short value, short increment) => (short)(value - increment); + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/SingleUpDown.cs b/src/Avalonia.Controls/NumericUpDown/SingleUpDown.cs new file mode 100644 index 0000000000..cc3da078bf --- /dev/null +++ b/src/Avalonia.Controls/NumericUpDown/SingleUpDown.cs @@ -0,0 +1,106 @@ +using System; + +namespace Avalonia.Controls +{ + /// + public class SingleUpDown : CommonNumericUpDown + { + /// + /// Defines the property. + /// + public static readonly DirectProperty AllowInputSpecialValuesProperty = + AvaloniaProperty.RegisterDirect(nameof(AllowInputSpecialValues), + updown => updown.AllowInputSpecialValues, (updown, v) => updown.AllowInputSpecialValues = v); + + private AllowedSpecialValues _allowInputSpecialValues; + + /// + /// Initializes static members of the class. + /// + static SingleUpDown() => UpdateMetadata(typeof(SingleUpDown), 1f, float.NegativeInfinity, float.PositiveInfinity); + + /// + /// Initializes new instance of the class. + /// + public SingleUpDown() : base(float.TryParse, decimal.ToSingle, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2) + { + } + + /// + /// Gets or sets a value representing the special values the user is allowed to input, such as "Infinity", "-Infinity" and "NaN" values. + /// + public AllowedSpecialValues AllowInputSpecialValues + { + get { return _allowInputSpecialValues; } + set { SetAndRaise(AllowInputSpecialValuesProperty, ref _allowInputSpecialValues, value); } + } + + /// + protected override float? OnCoerceIncrement(float? baseValue) + { + if (baseValue.HasValue && float.IsNaN(baseValue.Value)) + throw new ArgumentException("NaN is invalid for Increment."); + + return base.OnCoerceIncrement(baseValue); + } + + /// + protected override float? OnCoerceMaximum(float? baseValue) + { + if (baseValue.HasValue && float.IsNaN(baseValue.Value)) + throw new ArgumentException("NaN is invalid for Maximum."); + + return base.OnCoerceMaximum(baseValue); + } + + /// + protected override float? OnCoerceMinimum(float? baseValue) + { + if (baseValue.HasValue && float.IsNaN(baseValue.Value)) + throw new ArgumentException("NaN is invalid for Minimum."); + + return base.OnCoerceMinimum(baseValue); + } + + /// + protected override float IncrementValue(float value, float increment) => value + increment; + + /// + protected override float DecrementValue(float value, float increment) => value - increment; + + /// + protected override void SetValidSpinDirection() + { + if (Value.HasValue && float.IsInfinity(Value.Value) && (Spinner != null)) + { + Spinner.ValidSpinDirection = ValidSpinDirections.None; + } + else + { + base.SetValidSpinDirection(); + } + } + + /// + protected override float? ConvertTextToValue(string text) + { + var result = base.ConvertTextToValue(text); + if (result != null) + { + if (float.IsNaN(result.Value)) + { + TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.NaN); + } + else if (float.IsPositiveInfinity(result.Value)) + { + TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.PositiveInfinity); + } + else if (float.IsNegativeInfinity(result.Value)) + { + TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.NegativeInfinity); + } + } + return result; + } + } +} \ No newline at end of file From 1aa4d8749f5c1bc9b6c64b748835f6f6e18de54f Mon Sep 17 00:00:00 2001 From: dzhelnin Date: Tue, 20 Mar 2018 15:15:43 +0300 Subject: [PATCH 06/11] Added default template for NumericUpDown classes. --- src/Avalonia.Themes.Default/DefaultTheme.xaml | 1 + .../NumericUpDown.xaml | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/Avalonia.Themes.Default/NumericUpDown.xaml diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 7c567c1835..aa1a3c6385 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -43,4 +43,5 @@ + diff --git a/src/Avalonia.Themes.Default/NumericUpDown.xaml b/src/Avalonia.Themes.Default/NumericUpDown.xaml new file mode 100644 index 0000000000..e2ab6bf149 --- /dev/null +++ b/src/Avalonia.Themes.Default/NumericUpDown.xaml @@ -0,0 +1,41 @@ + + + \ No newline at end of file From 89cfa644ae2ff3fac454a9c27e1148aaef9dd68d Mon Sep 17 00:00:00 2001 From: dzhelnin Date: Tue, 20 Mar 2018 15:17:42 +0300 Subject: [PATCH 07/11] Added NumbersPage to ControlCatalog. --- samples/ControlCatalog/ControlCatalog.csproj | 6 + samples/ControlCatalog/MainView.xaml | 1 + samples/ControlCatalog/Pages/NumbersPage.xaml | 96 +++++++++++++++ .../ControlCatalog/Pages/NumbersPage.xaml.cs | 111 ++++++++++++++++++ 4 files changed, 214 insertions(+) create mode 100644 samples/ControlCatalog/Pages/NumbersPage.xaml create mode 100644 samples/ControlCatalog/Pages/NumbersPage.xaml.cs diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index a3d7a0cdce..c3f14b045d 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -78,6 +78,9 @@ Designer + + Designer + Designer @@ -169,6 +172,9 @@ ButtonSpinnerPage.xaml + + + NumbersPage.xaml diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 142d0d42b1..f8976401e7 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -19,6 +19,7 @@ + diff --git a/samples/ControlCatalog/Pages/NumbersPage.xaml b/samples/ControlCatalog/Pages/NumbersPage.xaml new file mode 100644 index 0000000000..fa1990b472 --- /dev/null +++ b/samples/ControlCatalog/Pages/NumbersPage.xaml @@ -0,0 +1,96 @@ + + + Numeric up-down controls + Numeric up-down controls provide a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel. + The following controls are available to support various native numeric types: + ByteUpDown, ShortUpDown, IntegerUpDown, LongUpDown, SingleUpDown, DoubleUpDown, DecimalUpDown. + + Features: + + + ShowButtonSpinner: + + + IsReadOnly: + + + AllowSpin: + + + ClipValueToMinMax: + + + DisplayDefaultValueOnEmptyText: + + + + FormatString: + + + + + + + + + + + + + ButtonSpinnerLocation: + + + CultureInfo: + + + Watermark: + + + Text: + + + + Minimum: + + + Maximum: + + + Increment: + + + Value: + + + DefaultValue: + + + + + + DoubleUpDown and SingleUpDown support the AllowInputSpecialValues property + + AllowInputSpecialValues: + + + + + Usage of DoubleUpDown: + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/NumbersPage.xaml.cs b/samples/ControlCatalog/Pages/NumbersPage.xaml.cs new file mode 100644 index 0000000000..a68bdc3bcf --- /dev/null +++ b/samples/ControlCatalog/Pages/NumbersPage.xaml.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Markup.Xaml; +using ReactiveUI; + +namespace ControlCatalog.Pages +{ + public class NumbersPage : UserControl + { + public NumbersPage() + { + this.InitializeComponent(); + var viewModel = new NumbersPageViewModel(); + DataContext = viewModel; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + } + + public class NumbersPageViewModel : ReactiveObject + { + private IList _formats; + private FormatObject _selectedFormat; + private IList _spinnerLocations; + private IList _allowedSpecialValues; + + public NumbersPageViewModel() + { + SelectedFormat = Formats.FirstOrDefault(); + } + + public IList Formats + { + get + { + return _formats ?? (_formats = new List() + { + new FormatObject() {Name = "Currency", Value = "C2"}, + new FormatObject() {Name = "Fixed point", Value = "F2"}, + new FormatObject() {Name = "General", Value = "G"}, + new FormatObject() {Name = "Number", Value = "N"}, + new FormatObject() {Name = "Percent", Value = "P"}, + new FormatObject() {Name = "Degrees", Value = "{0:N2} °"}, + }); + } + } + + public IList SpinnerLocations + { + get + { + if (_spinnerLocations == null) + { + _spinnerLocations = new List(); + foreach (Location value in Enum.GetValues(typeof(Location))) + { + _spinnerLocations.Add(value); + } + } + return _spinnerLocations ; + } + } + + public IList Cultures { get; } = new List() + { + new CultureInfo("en-US"), + new CultureInfo("en-GB"), + new CultureInfo("fr-FR"), + new CultureInfo("ar-DZ"), + new CultureInfo("zh-CN"), + new CultureInfo("cs-CZ") + }; + + public IList AllowedSpecialValues + { + get + { + if (_allowedSpecialValues == null) + { + _allowedSpecialValues = new List(); + foreach (AllowedSpecialValues value in Enum.GetValues(typeof(AllowedSpecialValues))) + { + _allowedSpecialValues.Add(value); + } + } + return _allowedSpecialValues; + } + } + + public FormatObject SelectedFormat + { + get { return _selectedFormat; } + set { this.RaiseAndSetIfChanged(ref _selectedFormat, value); } + } + } + + public class FormatObject + { + public string Value { get; set; } + public string Name { get; set; } + } +} From d6b5e04f0c73c29d6f6b69c5ed4366d262432e58 Mon Sep 17 00:00:00 2001 From: dzhelnin Date: Sat, 24 Mar 2018 18:43:51 +0300 Subject: [PATCH 08/11] Reworked to provide common NumericUpDown control which values are doubles. --- samples/ControlCatalog/ControlCatalog.csproj | 6 +- samples/ControlCatalog/MainView.xaml | 2 +- samples/ControlCatalog/Pages/NumbersPage.xaml | 96 -- .../Pages/NumericUpDownPage.xaml | 86 ++ ...Page.xaml.cs => NumericUpDownPage.xaml.cs} | 21 +- .../NumericUpDown/AllowedSpecialValues.cs | 15 - .../NumericUpDown/ByteUpDown.cs | 24 - .../NumericUpDown/CommonNumericUpDown.cs | 346 ----- .../NumericUpDown/DecimalUpDown.cs | 24 - .../NumericUpDown/DoubleUpDown.cs | 109 -- .../NumericUpDown/IntegerUpDown.cs | 24 - .../NumericUpDown/LongUpDown.cs | 24 - .../NumericUpDown/NumericUpDown.cs | 1117 ++++++++++++++++- .../NumericUpDownValueChangedEventArgs.cs | 16 + .../NumericUpDown/ShortUpDown.cs | 24 - .../NumericUpDown/SingleUpDown.cs | 106 -- .../NumericUpDown/UpDownBase.cs | 824 ------------ .../NumericUpDown.xaml | 2 +- 18 files changed, 1184 insertions(+), 1682 deletions(-) delete mode 100644 samples/ControlCatalog/Pages/NumbersPage.xaml create mode 100644 samples/ControlCatalog/Pages/NumericUpDownPage.xaml rename samples/ControlCatalog/Pages/{NumbersPage.xaml.cs => NumericUpDownPage.xaml.cs} (79%) delete mode 100644 src/Avalonia.Controls/NumericUpDown/AllowedSpecialValues.cs delete mode 100644 src/Avalonia.Controls/NumericUpDown/ByteUpDown.cs delete mode 100644 src/Avalonia.Controls/NumericUpDown/CommonNumericUpDown.cs delete mode 100644 src/Avalonia.Controls/NumericUpDown/DecimalUpDown.cs delete mode 100644 src/Avalonia.Controls/NumericUpDown/DoubleUpDown.cs delete mode 100644 src/Avalonia.Controls/NumericUpDown/IntegerUpDown.cs delete mode 100644 src/Avalonia.Controls/NumericUpDown/LongUpDown.cs create mode 100644 src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs delete mode 100644 src/Avalonia.Controls/NumericUpDown/ShortUpDown.cs delete mode 100644 src/Avalonia.Controls/NumericUpDown/SingleUpDown.cs delete mode 100644 src/Avalonia.Controls/NumericUpDown/UpDownBase.cs diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index c3f14b045d..5d8f661990 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -78,7 +78,7 @@ Designer - + Designer @@ -173,8 +173,8 @@ ButtonSpinnerPage.xaml - - NumbersPage.xaml + + NumericUpDownPage.xaml diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index f8976401e7..a2e0980d6a 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -19,7 +19,7 @@ - + diff --git a/samples/ControlCatalog/Pages/NumbersPage.xaml b/samples/ControlCatalog/Pages/NumbersPage.xaml deleted file mode 100644 index fa1990b472..0000000000 --- a/samples/ControlCatalog/Pages/NumbersPage.xaml +++ /dev/null @@ -1,96 +0,0 @@ - - - Numeric up-down controls - Numeric up-down controls provide a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel. - The following controls are available to support various native numeric types: - ByteUpDown, ShortUpDown, IntegerUpDown, LongUpDown, SingleUpDown, DoubleUpDown, DecimalUpDown. - - Features: - - - ShowButtonSpinner: - - - IsReadOnly: - - - AllowSpin: - - - ClipValueToMinMax: - - - DisplayDefaultValueOnEmptyText: - - - - FormatString: - - - - - - - - - - - - - ButtonSpinnerLocation: - - - CultureInfo: - - - Watermark: - - - Text: - - - - Minimum: - - - Maximum: - - - Increment: - - - Value: - - - DefaultValue: - - - - - - DoubleUpDown and SingleUpDown support the AllowInputSpecialValues property - - AllowInputSpecialValues: - - - - - Usage of DoubleUpDown: - - - - - \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml new file mode 100644 index 0000000000..d90f2dffb2 --- /dev/null +++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml @@ -0,0 +1,86 @@ + + + Numeric up-down control + Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel. + + Features: + + + ShowButtonSpinner: + + + IsReadOnly: + + + AllowSpin: + + + ClipValueToMinMax: + + + DisplayDefaultValueOnEmptyText: + + + + FormatString: + + + + + + + + + + + + + ButtonSpinnerLocation: + + + CultureInfo: + + + Watermark: + + + Text: + + + + Minimum: + + + Maximum: + + + Increment: + + + Value: + + + DefaultValue: + + + + + + + Usage of NumericUpDown: + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/NumbersPage.xaml.cs b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs similarity index 79% rename from samples/ControlCatalog/Pages/NumbersPage.xaml.cs rename to samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs index a68bdc3bcf..4e3da69ede 100644 --- a/samples/ControlCatalog/Pages/NumbersPage.xaml.cs +++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml.cs @@ -10,9 +10,9 @@ using ReactiveUI; namespace ControlCatalog.Pages { - public class NumbersPage : UserControl + public class NumericUpDownPage : UserControl { - public NumbersPage() + public NumericUpDownPage() { this.InitializeComponent(); var viewModel = new NumbersPageViewModel(); @@ -31,7 +31,6 @@ namespace ControlCatalog.Pages private IList _formats; private FormatObject _selectedFormat; private IList _spinnerLocations; - private IList _allowedSpecialValues; public NumbersPageViewModel() { @@ -80,22 +79,6 @@ namespace ControlCatalog.Pages new CultureInfo("cs-CZ") }; - public IList AllowedSpecialValues - { - get - { - if (_allowedSpecialValues == null) - { - _allowedSpecialValues = new List(); - foreach (AllowedSpecialValues value in Enum.GetValues(typeof(AllowedSpecialValues))) - { - _allowedSpecialValues.Add(value); - } - } - return _allowedSpecialValues; - } - } - public FormatObject SelectedFormat { get { return _selectedFormat; } diff --git a/src/Avalonia.Controls/NumericUpDown/AllowedSpecialValues.cs b/src/Avalonia.Controls/NumericUpDown/AllowedSpecialValues.cs deleted file mode 100644 index a4ec5ecdaf..0000000000 --- a/src/Avalonia.Controls/NumericUpDown/AllowedSpecialValues.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Avalonia.Controls -{ - [Flags] - public enum AllowedSpecialValues - { - None = 0, - NaN = 1, - PositiveInfinity = 2, - NegativeInfinity = 4, - AnyInfinity = PositiveInfinity | NegativeInfinity, - Any = NaN | AnyInfinity - } -} \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/ByteUpDown.cs b/src/Avalonia.Controls/NumericUpDown/ByteUpDown.cs deleted file mode 100644 index 835abae773..0000000000 --- a/src/Avalonia.Controls/NumericUpDown/ByteUpDown.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Avalonia.Controls -{ - /// - public class ByteUpDown : CommonNumericUpDown - { - /// - /// Initializes static members of the class. - /// - static ByteUpDown() => UpdateMetadata(typeof(ByteUpDown), 1, byte.MinValue, byte.MaxValue); - - /// - /// Initializes new instance of the class. - /// - public ByteUpDown() : base(byte.TryParse, decimal.ToByte, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2) - { - } - - /// - protected override byte IncrementValue(byte value, byte increment) => (byte)(value + increment); - - /// - protected override byte DecrementValue(byte value, byte increment) => (byte)(value - increment); - } -} \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/CommonNumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/CommonNumericUpDown.cs deleted file mode 100644 index eed792d9dd..0000000000 --- a/src/Avalonia.Controls/NumericUpDown/CommonNumericUpDown.cs +++ /dev/null @@ -1,346 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Linq; - -namespace Avalonia.Controls -{ - /// - /// Base class for controls that represents a TextBox with button spinners that allow incrementing and decrementing nullable numeric values. - /// - public abstract class CommonNumericUpDown : NumericUpDown where T : struct, IFormattable, IComparable - { - protected delegate bool FromText(string s, NumberStyles style, IFormatProvider provider, out T result); - protected delegate T FromDecimal(decimal d); - - private readonly FromText _fromText; - private readonly FromDecimal _fromDecimal; - private readonly Func _fromLowerThan; - private readonly Func _fromGreaterThan; - - private NumberStyles _parsingNumberStyle = NumberStyles.Any; - - /// - /// Defines the property. - /// - public static readonly DirectProperty, NumberStyles> ParsingNumberStyleProperty = - AvaloniaProperty.RegisterDirect, NumberStyles>(nameof(ParsingNumberStyle), - updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style); - - - /// - /// Initializes new instance of the class. - /// - /// Delegate to parse value from text. - /// Delegate to parse value from decimal. - /// Delegate to compare if one value is lower than another. - /// Delegate to compare if one value is greater than another. - protected CommonNumericUpDown(FromText fromText, FromDecimal fromDecimal, Func fromLowerThan, Func fromGreaterThan) - { - _fromText = fromText ?? throw new ArgumentNullException(nameof(fromText)); - _fromDecimal = fromDecimal ?? throw new ArgumentNullException(nameof(fromDecimal)); - _fromLowerThan = fromLowerThan ?? throw new ArgumentNullException(nameof(fromLowerThan)); - _fromGreaterThan = fromGreaterThan ?? throw new ArgumentNullException(nameof(fromGreaterThan)); - } - - /// - /// Gets or sets the parsing style (AllowLeadingWhite, Float, AllowHexSpecifier, ...). By default, Any. - /// - public NumberStyles ParsingNumberStyle - { - get { return _parsingNumberStyle; } - set { SetAndRaise(ParsingNumberStyleProperty, ref _parsingNumberStyle, value); } - } - - /// - protected override void OnIncrement() - { - if (!HandleNullSpin()) - { - var result = IncrementValue(Value.Value, Increment.Value); - Value = CoerceValueMinMax(result); - } - } - - /// - protected override void OnDecrement() - { - if (!HandleNullSpin()) - { - var result = DecrementValue(Value.Value, Increment.Value); - Value = CoerceValueMinMax(result); - } - } - - /// - protected override void OnMinimumChanged(T? oldValue, T? newValue) - { - base.OnMinimumChanged(oldValue, newValue); - - if (Value.HasValue && ClipValueToMinMax) - { - Value = CoerceValueMinMax(Value.Value); - } - } - - /// - protected override void OnMaximumChanged(T? oldValue, T? newValue) - { - base.OnMaximumChanged(oldValue, newValue); - - if (Value.HasValue && ClipValueToMinMax) - { - Value = CoerceValueMinMax(Value.Value); - } - } - - /// - protected override T? ConvertTextToValue(string text) - { - T? result = null; - - if (string.IsNullOrEmpty(text)) - { - return result; - } - - // Since the conversion from Value to text using a FormartString may not be parsable, - // we verify that the already existing text is not the exact same value. - var currentValueText = ConvertValueToText(); - if (Equals(currentValueText, text)) - { - return Value; - } - - result = ConvertTextToValueCore(currentValueText, text); - - if (ClipValueToMinMax) - { - return GetClippedMinMaxValue(result); - } - - ValidateDefaultMinMax(result); - - return result; - } - - /// - protected override string ConvertValueToText() - { - if (Value == null) - { - return string.Empty; - } - - //Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind. - if (FormatString.Contains("{0")) - { - return string.Format(CultureInfo, FormatString, Value.Value); - } - - return Value.Value.ToString(FormatString, CultureInfo); - } - - /// - protected override void SetValidSpinDirection() - { - var validDirections = ValidSpinDirections.None; - - // Null increment always prevents spin. - if (Increment != null && !IsReadOnly) - { - if (IsLowerThan(Value, Maximum) || !Value.HasValue || !Maximum.HasValue) - { - validDirections = validDirections | ValidSpinDirections.Increase; - } - - if (IsGreaterThan(Value, Minimum) || !Value.HasValue || !Minimum.HasValue) - { - validDirections = validDirections | ValidSpinDirections.Decrease; - } - } - - if (Spinner != null) - { - Spinner.ValidSpinDirection = validDirections; - } - } - - /// - /// Checks if provided value is within allowed values. - /// - /// The alowed values. - /// The value to check. - protected void TestInputSpecialValue(AllowedSpecialValues allowedValues, AllowedSpecialValues valueToCompare) - { - if ((allowedValues & valueToCompare) != valueToCompare) - { - switch (valueToCompare) - { - case AllowedSpecialValues.NaN: - throw new InvalidDataException("Value to parse shouldn't be NaN."); - case AllowedSpecialValues.PositiveInfinity: - throw new InvalidDataException("Value to parse shouldn't be Positive Infinity."); - case AllowedSpecialValues.NegativeInfinity: - throw new InvalidDataException("Value to parse shouldn't be Negative Infinity."); - } - } - } - - protected static void UpdateMetadata(Type type, T? increment, T? minimun, T? maximum) - { - IncrementProperty.OverrideDefaultValue(type, increment); - MinimumProperty.OverrideDefaultValue(type, minimun); - MaximumProperty.OverrideDefaultValue(type, maximum); - } - - protected abstract T IncrementValue(T value, T increment); - - protected abstract T DecrementValue(T value, T increment); - - private bool IsLowerThan(T? value1, T? value2) - { - if (value1 == null || value2 == null) - { - return false; - } - return _fromLowerThan(value1.Value, value2.Value); - } - - private bool IsGreaterThan(T? value1, T? value2) - { - if (value1 == null || value2 == null) - { - return false; - } - return _fromGreaterThan(value1.Value, value2.Value); - } - - private bool HandleNullSpin() - { - if (!Value.HasValue) - { - var forcedValue = DefaultValue ?? default(T); - Value = CoerceValueMinMax(forcedValue); - return true; - } - else if (!Increment.HasValue) - { - return true; - } - return false; - } - - internal bool IsValid(T? value) - { - return !IsLowerThan(value, Minimum) && !IsGreaterThan(value, Maximum); - } - - private T? CoerceValueMinMax(T value) - { - if (IsLowerThan(value, Minimum)) - { - return Minimum; - } - else if (IsGreaterThan(value, Maximum)) - { - return Maximum; - } - else - { - return value; - } - } - - private bool IsPercent(string stringToTest) - { - var PIndex = stringToTest.IndexOf("P", StringComparison.Ordinal); - if (PIndex >= 0) - { - //stringToTest contains a "P" between 2 "'", it's considered as text, not percent - var isText = stringToTest.Substring(0, PIndex).Contains("'") - && stringToTest.Substring(PIndex, FormatString.Length - PIndex).Contains("'"); - - return !isText; - } - return false; - } - - private T? ConvertTextToValueCore(string currentValueText, string text) - { - T? result; - - if (IsPercent(FormatString)) - { - result = _fromDecimal(ParsePercent(text, CultureInfo)); - } - else - { - // Problem while converting new text - if (!_fromText(text, ParsingNumberStyle, CultureInfo, out T outputValue)) - { - var shouldThrow = true; - - // Check if CurrentValueText is also failing => it also contains special characters. ex : 90° - if (!_fromText(currentValueText, ParsingNumberStyle, CultureInfo, out T _)) - { - // extract non-digit characters - var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c)); - var textSpecialCharacters = text.Where(c => !char.IsDigit(c)); - // same non-digit characters on currentValueText and new text => remove them on new Text to parse it again. - if (currentValueTextSpecialCharacters.Except(textSpecialCharacters).ToList().Count == 0) - { - foreach (var character in textSpecialCharacters) - { - text = text.Replace(character.ToString(), string.Empty); - } - // if without the special characters, parsing is good, do not throw - if (_fromText(text, ParsingNumberStyle, CultureInfo, out outputValue)) - { - shouldThrow = false; - } - } - } - - if (shouldThrow) - { - throw new InvalidDataException("Input string was not in a correct format."); - } - } - result = outputValue; - } - return result; - } - - private T? GetClippedMinMaxValue(T? result) - { - if (IsGreaterThan(result, Maximum)) - { - return Maximum; - } - else if (IsLowerThan(result, Minimum)) - { - return Minimum; - } - return result; - } - - private void ValidateDefaultMinMax(T? value) - { - // DefaultValue is always accepted. - if (Equals(value, DefaultValue)) - { - return; - } - - if (IsLowerThan(value, Minimum)) - { - throw new ArgumentOutOfRangeException(nameof(Minimum), string.Format("Value must be greater than Minimum value of {0}", Minimum)); - } - else if (IsGreaterThan(value, Maximum)) - { - throw new ArgumentOutOfRangeException(nameof(Maximum), string.Format("Value must be less than Maximum value of {0}", Maximum)); - } - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/DecimalUpDown.cs b/src/Avalonia.Controls/NumericUpDown/DecimalUpDown.cs deleted file mode 100644 index 10cfa537d7..0000000000 --- a/src/Avalonia.Controls/NumericUpDown/DecimalUpDown.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Avalonia.Controls -{ - /// - public class DecimalUpDown : CommonNumericUpDown - { - /// - /// Initializes static members of the class. - /// - static DecimalUpDown() => UpdateMetadata(typeof(DecimalUpDown), 1m, decimal.MinValue, decimal.MaxValue); - - /// - /// Initializes new instance of the class. - /// - public DecimalUpDown() : base(decimal.TryParse, d => d, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2) - { - } - - /// - protected override decimal IncrementValue(decimal value, decimal increment) => value + increment; - - /// - protected override decimal DecrementValue(decimal value, decimal increment) => value - increment; - } -} \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/DoubleUpDown.cs b/src/Avalonia.Controls/NumericUpDown/DoubleUpDown.cs deleted file mode 100644 index edee1247fb..0000000000 --- a/src/Avalonia.Controls/NumericUpDown/DoubleUpDown.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; - -namespace Avalonia.Controls -{ - /// - public class DoubleUpDown : CommonNumericUpDown - { - /// - /// Defines the property. - /// - public static readonly DirectProperty AllowInputSpecialValuesProperty = - AvaloniaProperty.RegisterDirect(nameof(AllowInputSpecialValues), - updown => updown.AllowInputSpecialValues, (updown, v) => updown.AllowInputSpecialValues = v); - - private AllowedSpecialValues _allowInputSpecialValues; - - /// - /// Initializes static members of the class. - /// - static DoubleUpDown() => UpdateMetadata(typeof(DoubleUpDown), 1d, double.NegativeInfinity, double.PositiveInfinity); - - /// - /// Initializes new instance of the class. - /// - public DoubleUpDown() : base(double.TryParse, decimal.ToDouble, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2) - { - } - - /// - /// Gets or sets a value representing the special values the user is allowed to input, such as "Infinity", "-Infinity" and "NaN" values. - /// - public AllowedSpecialValues AllowInputSpecialValues - { - get { return _allowInputSpecialValues; } - set { SetAndRaise(AllowInputSpecialValuesProperty, ref _allowInputSpecialValues, value); } - } - - /// - protected override double IncrementValue(double value, double increment) => value + increment; - - /// - protected override double DecrementValue(double value, double increment) => value - increment; - - /// - protected override double? OnCoerceIncrement(double? baseValue) - { - if (baseValue.HasValue && double.IsNaN(baseValue.Value)) - { - throw new ArgumentException("NaN is invalid for Increment."); - } - return base.OnCoerceIncrement(baseValue); - } - - /// - protected override double? OnCoerceMaximum(double? baseValue) - { - if (baseValue.HasValue && double.IsNaN(baseValue.Value)) - { - throw new ArgumentException("NaN is invalid for Maximum."); - } - return base.OnCoerceMaximum(baseValue); - } - - /// - protected override double? OnCoerceMinimum(double? baseValue) - { - if (baseValue.HasValue && double.IsNaN(baseValue.Value)) - { - throw new ArgumentException("NaN is invalid for Minimum."); - } - return base.OnCoerceMinimum(baseValue); - } - - /// - protected override void SetValidSpinDirection() - { - if (Value.HasValue && double.IsInfinity(Value.Value) && (Spinner != null)) - { - Spinner.ValidSpinDirection = ValidSpinDirections.None; - } - else - { - base.SetValidSpinDirection(); - } - } - - /// - protected override double? ConvertTextToValue(string text) - { - var result = base.ConvertTextToValue(text); - if (result != null) - { - if (double.IsNaN(result.Value)) - { - TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.NaN); - } - else if (double.IsPositiveInfinity(result.Value)) - { - TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.PositiveInfinity); - } - else if (double.IsNegativeInfinity(result.Value)) - { - TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.NegativeInfinity); - } - } - return result; - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/IntegerUpDown.cs b/src/Avalonia.Controls/NumericUpDown/IntegerUpDown.cs deleted file mode 100644 index 7f9e05762c..0000000000 --- a/src/Avalonia.Controls/NumericUpDown/IntegerUpDown.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Avalonia.Controls -{ - /// - public class IntegerUpDown : CommonNumericUpDown - { - /// - /// Initializes static members of the class. - /// - static IntegerUpDown() => UpdateMetadata(typeof(IntegerUpDown), 1, int.MinValue, int.MaxValue); - - /// - /// Initializes new instance of the class. - /// - public IntegerUpDown() : base(int.TryParse, decimal.ToInt32, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2) - { - } - - /// - protected override int IncrementValue(int value, int increment) => value + increment; - - /// - protected override int DecrementValue(int value, int increment) => value - increment; - } -} \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/LongUpDown.cs b/src/Avalonia.Controls/NumericUpDown/LongUpDown.cs deleted file mode 100644 index d116df0ad2..0000000000 --- a/src/Avalonia.Controls/NumericUpDown/LongUpDown.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Avalonia.Controls -{ - /// - public class LongUpDown : CommonNumericUpDown - { - /// - /// Initializes static members of the class. - /// - static LongUpDown() => UpdateMetadata(typeof(LongUpDown), 1L, long.MinValue, long.MaxValue); - - /// - /// Initializes new instance of the class. - /// - public LongUpDown() : base(long.TryParse, decimal.ToInt64, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2) - { - } - - /// - protected override long IncrementValue(long value, long increment) => value + increment; - - /// - protected override long DecrementValue(long value, long increment) => value - increment; - } -} \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 402dec7284..67c0827155 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -1,36 +1,207 @@ using System; using System.Globalization; +using System.IO; +using System.Linq; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Threading; namespace Avalonia.Controls { /// - /// Base class for controls that represents a TextBox with button spinners that allow incrementing and decrementing numeric values. + /// Control that represents a TextBox with button spinners that allow incrementing and decrementing numeric values. /// - public abstract class NumericUpDown : UpDownBase + public class NumericUpDown : TemplatedControl { + /// + /// Defines the property. + /// + public static readonly StyledProperty AllowSpinProperty = + ButtonSpinner.AllowSpinProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ButtonSpinnerLocationProperty = + ButtonSpinner.ButtonSpinnerLocationProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowButtonSpinnerProperty = + ButtonSpinner.ShowButtonSpinnerProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly DirectProperty ClipValueToMinMaxProperty = + AvaloniaProperty.RegisterDirect(nameof(ClipValueToMinMax), + updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b); + + /// + /// Defines the property. + /// + public static readonly DirectProperty CultureInfoProperty = + AvaloniaProperty.RegisterDirect(nameof(CultureInfo), o => o.CultureInfo, + (o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture); + + /// + /// Defines the property. + /// + public static readonly StyledProperty DefaultValueProperty = + AvaloniaProperty.Register(nameof(DefaultValue)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty DisplayDefaultValueOnEmptyTextProperty = + AvaloniaProperty.Register(nameof(DisplayDefaultValueOnEmptyText)); + /// /// Defines the property. /// public static readonly StyledProperty FormatStringProperty = - AvaloniaProperty.Register, string>(nameof(FormatString), string.Empty); + AvaloniaProperty.Register(nameof(FormatString), string.Empty); /// /// Defines the property. /// - public static readonly StyledProperty IncrementProperty = - AvaloniaProperty.Register, T>(nameof(Increment), default(T), validate: OnCoerceIncrement); + public static readonly StyledProperty IncrementProperty = + AvaloniaProperty.Register(nameof(Increment), 1.0d, validate: OnCoerceIncrement); /// - /// Initializes static members of the class. + /// Defines the property. /// - static NumericUpDown() + public static readonly StyledProperty IsReadOnlyProperty = + AvaloniaProperty.Register(nameof(IsReadOnly)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaximumProperty = + AvaloniaProperty.Register(nameof(Maximum), double.MaxValue, validate: OnCoerceMaximum); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinimumProperty = + AvaloniaProperty.Register(nameof(Minimum), double.MinValue, validate: OnCoerceMinimum); + + /// + /// Defines the property. + /// + public static readonly DirectProperty ParsingNumberStyleProperty = + AvaloniaProperty.RegisterDirect(nameof(ParsingNumberStyle), + updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style); + + /// + /// Defines the property. + /// + public static readonly DirectProperty TextProperty = + AvaloniaProperty.RegisterDirect(nameof(Text), o => o.Text, (o, v) => o.Text = v, + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect(nameof(Value), updown => updown.Value, + (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly StyledProperty WatermarkProperty = + AvaloniaProperty.Register(nameof(Watermark)); + + private IDisposable _textBoxTextChangedSubscription; + + private double? _value; + private string _text; + private bool _internalValueSet; + private bool _clipValueToMinMax; + private bool _isSyncingTextAndValueProperties; + private bool _isTextChangedFromUI; + private CultureInfo _cultureInfo; + private NumberStyles _parsingNumberStyle = NumberStyles.Any; + + /// + /// Gets the Spinner template part. + /// + private Spinner Spinner { get; set; } + + /// + /// Gets the TextBox template part. + /// + private TextBox TextBox { get; set; } + + /// + /// Gets or sets the ability to perform increment/decrement operations via the keyboard, button spinners, or mouse wheel. + /// + public bool AllowSpin { - FormatStringProperty.Changed.Subscribe(FormatStringChanged); - IncrementProperty.Changed.Subscribe(IncrementChanged); + get { return GetValue(AllowSpinProperty); } + set { SetValue(AllowSpinProperty, value); } + } + + /// + /// Gets or sets current location of the . + /// + public Location ButtonSpinnerLocation + { + get { return GetValue(ButtonSpinnerLocationProperty); } + set { SetValue(ButtonSpinnerLocationProperty, value); } + } + + /// + /// Gets or sets a value indicating whether the spin buttons should be shown. + /// + public bool ShowButtonSpinner + { + get { return GetValue(ShowButtonSpinnerProperty); } + set { SetValue(ShowButtonSpinnerProperty, value); } + } + + /// + /// Gets or sets if the value should be clipped when minimum/maximum is reached. + /// + public bool ClipValueToMinMax + { + get { return _clipValueToMinMax; } + set { SetAndRaise(ClipValueToMinMaxProperty, ref _clipValueToMinMax, value); } + } + + /// + /// Gets or sets the current CultureInfo. + /// + public CultureInfo CultureInfo + { + get { return _cultureInfo; } + set { SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); } + } + + /// + /// Gets or sets the value to use when the is null and an increment/decrement operation is performed. + /// + public double? DefaultValue + { + get { return GetValue(DefaultValueProperty); } + set { SetValue(DefaultValueProperty, value); } + } + + /// + /// Gets or sets if the defaultValue should be displayed when the Text is empty. + /// + public bool DisplayDefaultValueOnEmptyText + { + get { return GetValue(DisplayDefaultValueOnEmptyTextProperty); } + set { SetValue(DisplayDefaultValueOnEmptyTextProperty, value); } } /// - /// Gets or sets the display format of the . + /// Gets or sets the display format of the . /// public string FormatString { @@ -39,14 +210,196 @@ namespace Avalonia.Controls } /// - /// Gets or sets the amount in which to increment the . + /// Gets or sets the amount in which to increment the . /// - public T Increment + public double Increment { get { return GetValue(IncrementProperty); } set { SetValue(IncrementProperty, value); } } + /// + /// Gets or sets if the control is read only. + /// + public bool IsReadOnly + { + get { return GetValue(IsReadOnlyProperty); } + set { SetValue(IsReadOnlyProperty, value); } + } + + /// + /// Gets or sets the maximum allowed value. + /// + public double Maximum + { + get { return GetValue(MaximumProperty); } + set { SetValue(MaximumProperty, value); } + } + + /// + /// Gets or sets the minimum allowed value. + /// + public double Minimum + { + get { return GetValue(MinimumProperty); } + set { SetValue(MinimumProperty, value); } + } + + /// + /// Gets or sets the parsing style (AllowLeadingWhite, Float, AllowHexSpecifier, ...). By default, Any. + /// + public NumberStyles ParsingNumberStyle + { + get { return _parsingNumberStyle; } + set { SetAndRaise(ParsingNumberStyleProperty, ref _parsingNumberStyle, value); } + } + + /// + /// Gets or sets the formatted string representation of the value. + /// + public string Text + { + get { return _text; } + set { SetAndRaise(TextProperty, ref _text, value); } + } + + /// + /// Gets or sets the value. + /// + public double? Value + { + get { return _value; } + set + { + value = OnCoerceValue(value); + SetAndRaise(ValueProperty, ref _value, value); + } + } + + /// + /// Gets or sets the object to use as a watermark if the is null. + /// + public string Watermark + { + get { return GetValue(WatermarkProperty); } + set { SetValue(WatermarkProperty, value); } + } + + /// + /// Initializes new instance of class. + /// + public NumericUpDown() + { + Initialized += (sender, e) => + { + if (!_internalValueSet && IsInitialized) + { + SyncTextAndValueProperties(false, null, true); + } + + SetValidSpinDirection(); + }; + } + + /// + /// Initializes static members of the class. + /// + static NumericUpDown() + { + CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged); + DefaultValueProperty.Changed.Subscribe(OnDefaultValueChanged); + DisplayDefaultValueOnEmptyTextProperty.Changed.Subscribe(OnDisplayDefaultValueOnEmptyTextChanged); + FormatStringProperty.Changed.Subscribe(FormatStringChanged); + IncrementProperty.Changed.Subscribe(IncrementChanged); + IsReadOnlyProperty.Changed.Subscribe(OnIsReadOnlyChanged); + MaximumProperty.Changed.Subscribe(OnMaximumChanged); + MinimumProperty.Changed.Subscribe(OnMinimumChanged); + TextProperty.Changed.Subscribe(OnTextChanged); + ValueProperty.Changed.Subscribe(OnValueChanged); + } + + /// + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + if (TextBox != null) + { + TextBox.PointerPressed -= TextBoxOnPointerPressed; + _textBoxTextChangedSubscription?.Dispose(); + } + TextBox = e.NameScope.Find("PART_TextBox"); + if (TextBox != null) + { + TextBox.Text = Text; + TextBox.PointerPressed += TextBoxOnPointerPressed; + _textBoxTextChangedSubscription = TextBox.GetObservable(TextBox.TextProperty).Subscribe(txt => TextBoxOnTextChanged()); + } + + if (Spinner != null) + { + Spinner.Spin -= OnSpinnerSpin; + } + + Spinner = e.NameScope.Find("PART_Spinner"); + + if (Spinner != null) + { + Spinner.Spin += OnSpinnerSpin; + } + + SetValidSpinDirection(); + } + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + switch (e.Key) + { + case Key.Enter: + var commitSuccess = CommitInput(); + e.Handled = !commitSuccess; + break; + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnCultureInfoChanged(CultureInfo oldValue, CultureInfo newValue) + { + if (IsInitialized) + { + SyncTextAndValueProperties(false, null); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnDefaultValueChanged(double oldValue, double newValue) + { + if (IsInitialized && string.IsNullOrEmpty(Text)) + { + SyncTextAndValueProperties(true, Text); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnDisplayDefaultValueOnEmptyTextChanged(bool oldValue, bool newValue) + { + if (IsInitialized && string.IsNullOrEmpty(Text)) + { + SyncTextAndValueProperties(false, Text); + } + } + /// /// Called when the property value changed. /// @@ -65,7 +418,7 @@ namespace Avalonia.Controls /// /// The old value. /// The new value. - protected virtual void OnIncrementChanged(T oldValue, T newValue) + protected virtual void OnIncrementChanged(double oldValue, double newValue) { if (IsInitialized) { @@ -74,59 +427,739 @@ namespace Avalonia.Controls } /// - /// Called when the property has to be coerced. + /// Called when the property value changed. /// - /// The value. - protected virtual T OnCoerceIncrement(T baseValue) + /// The old value. + /// The new value. + protected virtual void OnIsReadOnlyChanged(bool oldValue, bool newValue) { - return baseValue; + SetValidSpinDirection(); } /// - /// Called when the property value changed. + /// Called when the property value changed. /// - /// The event args. - private static void IncrementChanged(AvaloniaPropertyChangedEventArgs e) + /// The old value. + /// The new value. + protected virtual void OnMaximumChanged(double oldValue, double newValue) { - if (e.Sender is NumericUpDown upDown) + if (IsInitialized) { - var oldValue = (T)e.OldValue; - var newValue = (T)e.NewValue; - upDown.OnIncrementChanged(oldValue, newValue); + SetValidSpinDirection(); + } + if (Value.HasValue && ClipValueToMinMax) + { + Value = CoerceValueMinMax(Value.Value); } } /// - /// Called when the property value changed. + /// Called when the property value changed. /// - /// The event args. - private static void FormatStringChanged(AvaloniaPropertyChangedEventArgs e) + /// The old value. + /// The new value. + protected virtual void OnMinimumChanged(double oldValue, double newValue) { - if (e.Sender is NumericUpDown upDown) + if (IsInitialized) { - var oldValue = (string) e.OldValue; - var newValue = (string) e.NewValue; - upDown.OnFormatStringChanged(oldValue, newValue); + SetValidSpinDirection(); + } + if (Value.HasValue && ClipValueToMinMax) + { + Value = CoerceValueMinMax(Value.Value); } } - private static T OnCoerceIncrement(NumericUpDown numericUpDown, T value) + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnTextChanged(string oldValue, string newValue) { - return numericUpDown.OnCoerceIncrement(value); + if (IsInitialized) + { + SyncTextAndValueProperties(true, Text); + } } /// - /// Parse percent format text + /// Called when the property value changed. /// - /// Text to parse. - /// The culture info. - protected static decimal ParsePercent(string text, IFormatProvider cultureInfo) + /// The old value. + /// The new value. + protected virtual void OnValueChanged(double? oldValue, double? newValue) { - var info = NumberFormatInfo.GetInstance(cultureInfo); - text = text.Replace(info.PercentSymbol, null); - var result = decimal.Parse(text, NumberStyles.Any, info); - result = result / 100; - return result; + if (!_internalValueSet && IsInitialized) + { + SyncTextAndValueProperties(false, null, true); + } + + SetValidSpinDirection(); + + RaiseValueChangedEvent(oldValue, newValue); + } + + /// + /// Called when the property has to be coerced. + /// + /// The value. + protected virtual double OnCoerceIncrement(double baseValue) + { + return baseValue; + } + + /// + /// Called when the property has to be coerced. + /// + /// The value. + protected virtual double OnCoerceMaximum(double baseValue) + { + return baseValue; + } + + /// + /// Called when the property has to be coerced. + /// + /// The value. + protected virtual double OnCoerceMinimum(double baseValue) + { + return baseValue; + } + + /// + /// Called when the property has to be coerced. + /// + /// The value. + protected virtual double? OnCoerceValue(double? baseValue) + { + return baseValue; + } + + /// + /// Raises the OnSpin event when spinning is initiated by the end-user. + /// + /// The event args. + protected virtual void OnSpin(SpinEventArgs e) + { + if (e == null) + { + throw new ArgumentNullException("e"); + } + + var handler = Spinned; + handler?.Invoke(this, e); + + if (e.Direction == SpinDirection.Increase) + { + DoIncrement(); + } + else + { + DoDecrement(); + } + } + + /// + /// Raises the event. + /// + /// The old value. + /// The new value. + protected virtual void RaiseValueChangedEvent(double? oldValue, double? newValue) + { + var e = new NumericUpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue); + RaiseEvent(e); + } + + /// + /// Converts the formatted text to a value. + /// + private double? ConvertTextToValue(string text) + { + double? result = null; + + if (string.IsNullOrEmpty(text)) + { + return result; + } + + // Since the conversion from Value to text using a FormartString may not be parsable, + // we verify that the already existing text is not the exact same value. + var currentValueText = ConvertValueToText(); + if (Equals(currentValueText, text)) + { + return Value; + } + + result = ConvertTextToValueCore(currentValueText, text); + + if (ClipValueToMinMax) + { + return GetClippedMinMaxValue(result); + } + + ValidateDefaultMinMax(result); + + return result; + } + + /// + /// Converts the value to formatted text. + /// + /// + private string ConvertValueToText() + { + if (Value == null) + { + return string.Empty; + } + + //Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind. + if (FormatString.Contains("{0")) + { + return string.Format(CultureInfo, FormatString, Value.Value); + } + + return Value.Value.ToString(FormatString, CultureInfo); + + } + + /// + /// Called by OnSpin when the spin direction is SpinDirection.Increase. + /// + private void OnIncrement() + { + if (!HandleNullSpin()) + { + var result = Value.Value + Increment; + Value = CoerceValueMinMax(result); + } + } + + /// + /// Called by OnSpin when the spin direction is SpinDirection.Descrease. + /// + private void OnDecrement() + { + if (!HandleNullSpin()) + { + var result = Value.Value - Increment; + Value = CoerceValueMinMax(result); + } + } + + /// + /// Sets the valid spin directions. + /// + private void SetValidSpinDirection() + { + var validDirections = ValidSpinDirections.None; + + // Zero increment always prevents spin. + if (Increment != 0 && !IsReadOnly) + { + if (IsLowerThan(Value, Maximum) || !Value.HasValue) + { + validDirections = validDirections | ValidSpinDirections.Increase; + } + + if (IsGreaterThan(Value, Minimum) || !Value.HasValue) + { + validDirections = validDirections | ValidSpinDirections.Decrease; + } + } + + if (Spinner != null) + { + Spinner.ValidSpinDirection = validDirections; + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnCultureInfoChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (CultureInfo)e.OldValue; + var newValue = (CultureInfo)e.NewValue; + upDown.OnCultureInfoChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnDefaultValueChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (double)e.OldValue; + var newValue = (double)e.NewValue; + upDown.OnDefaultValueChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnDisplayDefaultValueOnEmptyTextChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (bool) e.OldValue; + var newValue = (bool) e.NewValue; + upDown.OnDisplayDefaultValueOnEmptyTextChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void IncrementChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (double)e.OldValue; + var newValue = (double)e.NewValue; + upDown.OnIncrementChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void FormatStringChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (string)e.OldValue; + var newValue = (string)e.NewValue; + upDown.OnFormatStringChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (bool)e.OldValue; + var newValue = (bool)e.NewValue; + upDown.OnIsReadOnlyChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnMaximumChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (double)e.OldValue; + var newValue = (double)e.NewValue; + upDown.OnMaximumChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (double)e.OldValue; + var newValue = (double)e.NewValue; + upDown.OnMinimumChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnTextChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (string)e.OldValue; + var newValue = (string)e.NewValue; + upDown.OnTextChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnValueChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (double?)e.OldValue; + var newValue = (double?)e.NewValue; + upDown.OnValueChanged(oldValue, newValue); + } + } + + private void SetValueInternal(double? value) + { + _internalValueSet = true; + try + { + Value = value; + } + finally + { + _internalValueSet = false; + } + } + + private static double OnCoerceMaximum(NumericUpDown upDown, double value) + { + return upDown.OnCoerceMaximum(value); + } + + private static double OnCoerceMinimum(NumericUpDown upDown, double value) + { + return upDown.OnCoerceMinimum(value); + } + + private static double OnCoerceIncrement(NumericUpDown upDown, double value) + { + return upDown.OnCoerceIncrement(value); + } + + private void TextBoxOnTextChanged() + { + try + { + _isTextChangedFromUI = true; + if (TextBox != null) + { + Text = TextBox.Text; + } + } + finally + { + _isTextChangedFromUI = false; + } + } + + private void OnSpinnerSpin(object sender, SpinEventArgs e) + { + if (AllowSpin && !IsReadOnly) + { + var spin = !e.UsingMouseWheel; + spin |= ((TextBox != null) && TextBox.IsFocused); + + if (spin) + { + e.Handled = true; + OnSpin(e); + } + } + } + + private void DoDecrement() + { + if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Decrease) == ValidSpinDirections.Decrease) + { + OnDecrement(); + } + } + + private void DoIncrement() + { + if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Increase) == ValidSpinDirections.Increase) + { + OnIncrement(); + } + } + + public event EventHandler Spinned; + + private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e) + { + if (e.Device.Captured != Spinner) + { + Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input); + } + } + + /// + /// 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); } + } + + private bool CommitInput() + { + return SyncTextAndValueProperties(true, Text); + } + + /// + /// Synchronize and properties. + /// + /// If value should be updated from text. + /// The text. + private bool SyncTextAndValueProperties(bool updateValueFromText, string text) + { + return SyncTextAndValueProperties(updateValueFromText, text, false); + } + + /// + /// Synchronize and properties. + /// + /// If value should be updated from text. + /// The text. + /// Force text update. + private bool SyncTextAndValueProperties(bool updateValueFromText, string text, bool forceTextUpdate) + { + if (_isSyncingTextAndValueProperties) + return true; + + _isSyncingTextAndValueProperties = true; + var parsedTextIsValid = true; + try + { + if (updateValueFromText) + { + if (string.IsNullOrEmpty(text)) + { + // An empty input sets the value to the default value. + SetValueInternal(DefaultValue); + } + else + { + try + { + var newValue = ConvertTextToValue(text); + if (!Equals(newValue, Value)) + { + SetValueInternal(newValue); + } + } + catch + { + parsedTextIsValid = false; + } + } + } + + // Do not touch the ongoing text input from user. + if (!_isTextChangedFromUI) + { + // Don't replace the empty Text with the non-empty representation of DefaultValue. + var shouldKeepEmpty = !forceTextUpdate && string.IsNullOrEmpty(Text) && Equals(Value, DefaultValue) && !DisplayDefaultValueOnEmptyText; + if (!shouldKeepEmpty) + { + var newText = ConvertValueToText(); + if (!Equals(Text, newText)) + { + Text = newText; + } + } + + // Sync Text and textBox + if (TextBox != null) + { + TextBox.Text = Text; + } + } + + if (_isTextChangedFromUI && !parsedTextIsValid) + { + // Text input was made from the user and the text + // repesents an invalid value. Disable the spinner in this case. + if (Spinner != null) + { + Spinner.ValidSpinDirection = ValidSpinDirections.None; + } + } + else + { + SetValidSpinDirection(); + } + } + finally + { + _isSyncingTextAndValueProperties = false; + } + return parsedTextIsValid; + } + + private double? CoerceValueMinMax(double? value) + { + if (IsLowerThan(value, Minimum)) + { + return Minimum; + } + else if (IsGreaterThan(value, Maximum)) + { + return Maximum; + } + else + { + return value; + } + } + + private static bool IsLowerThan(double? value1, double? value2) + { + if (value1 == null || value2 == null) + { + return false; + } + return value1.Value < value2.Value; + } + + private static bool IsGreaterThan(double? value1, double? value2) + { + if (value1 == null || value2 == null) + { + return false; + } + return value1.Value > value2.Value; + } + + private bool HandleNullSpin() + { + if (!Value.HasValue) + { + var forcedValue = DefaultValue ?? default(double); + Value = CoerceValueMinMax(forcedValue); + return true; + } + return false; + } + + private double? ConvertTextToValueCore(string currentValueText, string text) + { + double? result; + + if (IsPercent(FormatString)) + { + result = decimal.ToDouble(ParsePercent(text, CultureInfo)); + } + else + { + // Problem while converting new text + if (!double.TryParse(text, ParsingNumberStyle, CultureInfo, out var outputValue)) + { + var shouldThrow = true; + + // Check if CurrentValueText is also failing => it also contains special characters. ex : 90° + if (!double.TryParse(currentValueText, ParsingNumberStyle, CultureInfo, out var _)) + { + // extract non-digit characters + var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c)); + var textSpecialCharacters = text.Where(c => !char.IsDigit(c)); + // same non-digit characters on currentValueText and new text => remove them on new Text to parse it again. + if (currentValueTextSpecialCharacters.Except(textSpecialCharacters).ToList().Count == 0) + { + foreach (var character in textSpecialCharacters) + { + text = text.Replace(character.ToString(), string.Empty); + } + // if without the special characters, parsing is good, do not throw + if (double.TryParse(text, ParsingNumberStyle, CultureInfo, out outputValue)) + { + shouldThrow = false; + } + } + } + + if (shouldThrow) + { + throw new InvalidDataException("Input string was not in a correct format."); + } + } + result = outputValue; + } + return result; + } + + private double? GetClippedMinMaxValue(double? result) + { + if (IsGreaterThan(result, Maximum)) + { + return Maximum; + } + else if (IsLowerThan(result, Minimum)) + { + return Minimum; + } + return result; + } + + private void ValidateDefaultMinMax(double? value) + { + // DefaultValue is always accepted. + if (Equals(value, DefaultValue)) + { + return; + } + + if (IsLowerThan(value, Minimum)) + { + throw new ArgumentOutOfRangeException(nameof(Minimum), string.Format("Value must be greater than Minimum value of {0}", Minimum)); + } + else if (IsGreaterThan(value, Maximum)) + { + throw new ArgumentOutOfRangeException(nameof(Maximum), string.Format("Value must be less than Maximum value of {0}", Maximum)); + } + } + + /// + /// Parse percent format text + /// + /// Text to parse. + /// The culture info. + private static decimal ParsePercent(string text, IFormatProvider cultureInfo) + { + var info = NumberFormatInfo.GetInstance(cultureInfo); + text = text.Replace(info.PercentSymbol, null); + var result = decimal.Parse(text, NumberStyles.Any, info); + result = result / 100; + return result; + } + + + private bool IsPercent(string stringToTest) + { + var PIndex = stringToTest.IndexOf("P", StringComparison.Ordinal); + if (PIndex >= 0) + { + //stringToTest contains a "P" between 2 "'", it's considered as text, not percent + var isText = stringToTest.Substring(0, PIndex).Contains("'") + && stringToTest.Substring(PIndex, FormatString.Length - PIndex).Contains("'"); + + return !isText; + } + return false; } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs new file mode 100644 index 0000000000..4fd8873c53 --- /dev/null +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDownValueChangedEventArgs.cs @@ -0,0 +1,16 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + public class NumericUpDownValueChangedEventArgs : RoutedEventArgs + { + public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, double? oldValue, double? newValue) : base(routedEvent) + { + OldValue = oldValue; + NewValue = newValue; + } + + public double? OldValue { get; } + public double? NewValue { get; } + } +} diff --git a/src/Avalonia.Controls/NumericUpDown/ShortUpDown.cs b/src/Avalonia.Controls/NumericUpDown/ShortUpDown.cs deleted file mode 100644 index cdba5b9b64..0000000000 --- a/src/Avalonia.Controls/NumericUpDown/ShortUpDown.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Avalonia.Controls -{ - /// - public class ShortUpDown : CommonNumericUpDown - { - /// - /// Initializes static members of the class. - /// - static ShortUpDown() => UpdateMetadata(typeof(ShortUpDown), 1, short.MinValue, short.MaxValue); - - /// - /// Initializes new instance of the class. - /// - public ShortUpDown() : base(short.TryParse, decimal.ToInt16, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2) - { - } - - /// - protected override short IncrementValue(short value, short increment) => (short)(value + increment); - - /// - protected override short DecrementValue(short value, short increment) => (short)(value - increment); - } -} \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/SingleUpDown.cs b/src/Avalonia.Controls/NumericUpDown/SingleUpDown.cs deleted file mode 100644 index cc3da078bf..0000000000 --- a/src/Avalonia.Controls/NumericUpDown/SingleUpDown.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; - -namespace Avalonia.Controls -{ - /// - public class SingleUpDown : CommonNumericUpDown - { - /// - /// Defines the property. - /// - public static readonly DirectProperty AllowInputSpecialValuesProperty = - AvaloniaProperty.RegisterDirect(nameof(AllowInputSpecialValues), - updown => updown.AllowInputSpecialValues, (updown, v) => updown.AllowInputSpecialValues = v); - - private AllowedSpecialValues _allowInputSpecialValues; - - /// - /// Initializes static members of the class. - /// - static SingleUpDown() => UpdateMetadata(typeof(SingleUpDown), 1f, float.NegativeInfinity, float.PositiveInfinity); - - /// - /// Initializes new instance of the class. - /// - public SingleUpDown() : base(float.TryParse, decimal.ToSingle, (v1, v2) => v1 < v2, (v1, v2) => v1 > v2) - { - } - - /// - /// Gets or sets a value representing the special values the user is allowed to input, such as "Infinity", "-Infinity" and "NaN" values. - /// - public AllowedSpecialValues AllowInputSpecialValues - { - get { return _allowInputSpecialValues; } - set { SetAndRaise(AllowInputSpecialValuesProperty, ref _allowInputSpecialValues, value); } - } - - /// - protected override float? OnCoerceIncrement(float? baseValue) - { - if (baseValue.HasValue && float.IsNaN(baseValue.Value)) - throw new ArgumentException("NaN is invalid for Increment."); - - return base.OnCoerceIncrement(baseValue); - } - - /// - protected override float? OnCoerceMaximum(float? baseValue) - { - if (baseValue.HasValue && float.IsNaN(baseValue.Value)) - throw new ArgumentException("NaN is invalid for Maximum."); - - return base.OnCoerceMaximum(baseValue); - } - - /// - protected override float? OnCoerceMinimum(float? baseValue) - { - if (baseValue.HasValue && float.IsNaN(baseValue.Value)) - throw new ArgumentException("NaN is invalid for Minimum."); - - return base.OnCoerceMinimum(baseValue); - } - - /// - protected override float IncrementValue(float value, float increment) => value + increment; - - /// - protected override float DecrementValue(float value, float increment) => value - increment; - - /// - protected override void SetValidSpinDirection() - { - if (Value.HasValue && float.IsInfinity(Value.Value) && (Spinner != null)) - { - Spinner.ValidSpinDirection = ValidSpinDirections.None; - } - else - { - base.SetValidSpinDirection(); - } - } - - /// - protected override float? ConvertTextToValue(string text) - { - var result = base.ConvertTextToValue(text); - if (result != null) - { - if (float.IsNaN(result.Value)) - { - TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.NaN); - } - else if (float.IsPositiveInfinity(result.Value)) - { - TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.PositiveInfinity); - } - else if (float.IsNegativeInfinity(result.Value)) - { - TestInputSpecialValue(AllowInputSpecialValues, AllowedSpecialValues.NegativeInfinity); - } - } - return result; - } - } -} \ No newline at end of file diff --git a/src/Avalonia.Controls/NumericUpDown/UpDownBase.cs b/src/Avalonia.Controls/NumericUpDown/UpDownBase.cs deleted file mode 100644 index fa95eb01a7..0000000000 --- a/src/Avalonia.Controls/NumericUpDown/UpDownBase.cs +++ /dev/null @@ -1,824 +0,0 @@ -using System; -using System.Globalization; -using Avalonia.Controls.Primitives; -using Avalonia.Data; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.Threading; - -namespace Avalonia.Controls -{ - public abstract class UpDownBase : TemplatedControl - { - } - - /// - /// Base class for controls that represents a TextBox with button spinners that allow incrementing and decrementing values. - /// - public abstract class UpDownBase : UpDownBase - { - /// - /// Defines the property. - /// - public static readonly StyledProperty AllowSpinProperty = - ButtonSpinner.AllowSpinProperty.AddOwner>(); - - /// - /// Defines the property. - /// - public static readonly StyledProperty ButtonSpinnerLocationProperty = - ButtonSpinner.ButtonSpinnerLocationProperty.AddOwner>(); - - /// - /// Defines the property. - /// - public static readonly StyledProperty ShowButtonSpinnerProperty = - ButtonSpinner.ShowButtonSpinnerProperty.AddOwner>(); - - /// - /// Defines the property. - /// - public static readonly DirectProperty, bool> ClipValueToMinMaxProperty = - AvaloniaProperty.RegisterDirect, bool>(nameof(ClipValueToMinMax), - updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b); - - /// - /// Defines the property. - /// - public static readonly DirectProperty, CultureInfo> CultureInfoProperty = - AvaloniaProperty.RegisterDirect, CultureInfo>(nameof(CultureInfo), o => o.CultureInfo, - (o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture); - - /// - /// Defines the property. - /// - public static readonly StyledProperty DefaultValueProperty = - AvaloniaProperty.Register, T>(nameof(DefaultValue)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty DisplayDefaultValueOnEmptyTextProperty = - AvaloniaProperty.Register, bool>(nameof(DisplayDefaultValueOnEmptyText)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty IsReadOnlyProperty = - AvaloniaProperty.Register, bool>(nameof(IsReadOnly)); - - /// - /// Defines the property. - /// - public static readonly StyledProperty MaximumProperty = - AvaloniaProperty.Register, T>(nameof(Maximum), validate: OnCoerceMaximum); - - /// - /// Defines the property. - /// - public static readonly StyledProperty MinimumProperty = - AvaloniaProperty.Register, T>(nameof(Minimum), validate: OnCoerceMinimum); - - /// - /// Defines the property. - /// - public static readonly DirectProperty, string> TextProperty = - AvaloniaProperty.RegisterDirect, string>(nameof(Text), o => o.Text, (o, v) => o.Text = v, - defaultBindingMode: BindingMode.TwoWay); - - /// - /// Defines the property. - /// - public static readonly DirectProperty, T> ValueProperty = - AvaloniaProperty.RegisterDirect, T>(nameof(Value), updown => updown.Value, - (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay); - - /// - /// Defines the property. - /// - public static readonly StyledProperty WatermarkProperty = - AvaloniaProperty.Register, string>(nameof(Watermark)); - - private IDisposable _textBoxTextChangedSubscription; - private T _value; - private string _text; - private bool _internalValueSet; - private bool _clipValueToMinMax; - private bool _isSyncingTextAndValueProperties; - private bool _isTextChangedFromUI; - private CultureInfo _cultureInfo; - - /// - /// Gets the Spinner template part. - /// - protected Spinner Spinner { get; private set; } - - /// - /// Gets the TextBox template part. - /// - protected TextBox TextBox { get; private set; } - - /// - /// Gets or sets the ability to perform increment/decrement operations via the keyboard, button spinners, or mouse wheel. - /// - public bool AllowSpin - { - get { return GetValue(AllowSpinProperty); } - set { SetValue(AllowSpinProperty, value); } - } - - /// - /// Gets or sets current location of the . - /// - public Location ButtonSpinnerLocation - { - get { return GetValue(ButtonSpinnerLocationProperty); } - set { SetValue(ButtonSpinnerLocationProperty, value); } - } - - /// - /// Gets or sets a value indicating whether the spin buttons should be shown. - /// - public bool ShowButtonSpinner - { - get { return GetValue(ShowButtonSpinnerProperty); } - set { SetValue(ShowButtonSpinnerProperty, value); } - } - - /// - /// Gets or sets if the value should be clipped when minimum/maximum is reached. - /// - public bool ClipValueToMinMax - { - get { return _clipValueToMinMax; } - set { SetAndRaise(ClipValueToMinMaxProperty, ref _clipValueToMinMax, value); } - } - - /// - /// Gets or sets the current CultureInfo. - /// - public CultureInfo CultureInfo - { - get { return _cultureInfo; } - set { SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); } - } - - /// - /// Gets or sets the value to use when the is null and an increment/decrement operation is performed. - /// - public T DefaultValue - { - get { return GetValue(DefaultValueProperty); } - set { SetValue(DefaultValueProperty, value); } - } - - /// - /// Gets or sets if the defaultValue should be displayed when the Text is empty. - /// - public bool DisplayDefaultValueOnEmptyText - { - get { return GetValue(DisplayDefaultValueOnEmptyTextProperty); } - set { SetValue(DisplayDefaultValueOnEmptyTextProperty, value); } - } - - /// - /// Gets or sets if the control is read only. - /// - public bool IsReadOnly - { - get { return GetValue(IsReadOnlyProperty); } - set { SetValue(IsReadOnlyProperty, value); } - } - - /// - /// Gets or sets the maximum allowed value. - /// - public T Maximum - { - get { return GetValue(MaximumProperty); } - set { SetValue(MaximumProperty, value); } - } - - /// - /// Gets or sets the minimum allowed value. - /// - public T Minimum - { - get { return GetValue(MinimumProperty); } - set { SetValue(MinimumProperty, value); } - } - - /// - /// Gets or sets the formatted string representation of the value. - /// - public string Text - { - get { return _text; } - set { SetAndRaise(TextProperty, ref _text, value); } - } - - /// - /// Gets or sets the value. - /// - public T Value - { - get { return _value; } - set - { - value = OnCoerceValue(value); - SetAndRaise(ValueProperty, ref _value, value); - } - } - - /// - /// Gets or sets the object to use as a watermark if the is null. - /// - public string Watermark - { - get { return GetValue(WatermarkProperty); } - set { SetValue(WatermarkProperty, value); } - } - - /// - /// Initializes new instance of class. - /// - protected UpDownBase() - { - Initialized += (sender, e) => - { - if (!_internalValueSet && IsInitialized) - { - SyncTextAndValueProperties(false, null, true); - } - - SetValidSpinDirection(); - }; - } - - /// - /// Initializes static members of the class. - /// - static UpDownBase() - { - CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged); - DefaultValueProperty.Changed.Subscribe(OnDefaultValueChanged); - DisplayDefaultValueOnEmptyTextProperty.Changed.Subscribe(OnDisplayDefaultValueOnEmptyTextChanged); - IsReadOnlyProperty.Changed.Subscribe(OnIsReadOnlyChanged); - MaximumProperty.Changed.Subscribe(OnMaximumChanged); - MinimumProperty.Changed.Subscribe(OnMinimumChanged); - TextProperty.Changed.Subscribe(OnTextChanged); - ValueProperty.Changed.Subscribe(OnValueChanged); - } - - /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) - { - if (TextBox != null) - { - TextBox.PointerPressed -= TextBoxOnPointerPressed; - _textBoxTextChangedSubscription?.Dispose(); - } - TextBox = e.NameScope.Find("PART_TextBox"); - if (TextBox != null) - { - TextBox.Text = Text; - TextBox.PointerPressed += TextBoxOnPointerPressed; - _textBoxTextChangedSubscription = TextBox.GetObservable(TextBox.TextProperty).Subscribe(txt => TextBoxOnTextChanged()); - } - - if (Spinner != null) - { - Spinner.Spin -= OnSpinnerSpin; - } - - Spinner = e.NameScope.Find("PART_Spinner"); - - if (Spinner != null) - { - Spinner.Spin += OnSpinnerSpin; - } - - SetValidSpinDirection(); - } - - /// - protected override void OnKeyDown(KeyEventArgs e) - { - switch (e.Key) - { - case Key.Enter: - var commitSuccess = CommitInput(); - e.Handled = !commitSuccess; - break; - } - } - - /// - /// Called when the property value changed. - /// - /// The old value. - /// The new value. - protected virtual void OnCultureInfoChanged(CultureInfo oldValue, CultureInfo newValue) - { - if (IsInitialized) - { - SyncTextAndValueProperties(false, null); - } - } - - /// - /// Called when the property value changed. - /// - /// The old value. - /// The new value. - protected virtual void OnDefaultValueChanged(T oldValue, T newValue) - { - if (IsInitialized && string.IsNullOrEmpty(Text)) - { - SyncTextAndValueProperties(true, Text); - } - } - - /// - /// Called when the property value changed. - /// - /// The old value. - /// The new value. - protected virtual void OnDisplayDefaultValueOnEmptyTextChanged(bool oldValue, bool newValue) - { - if (IsInitialized && string.IsNullOrEmpty(Text)) - { - SyncTextAndValueProperties(false, Text); - } - } - - /// - /// Called when the property value changed. - /// - /// The old value. - /// The new value. - protected virtual void OnIsReadOnlyChanged(bool oldValue, bool newValue) - { - SetValidSpinDirection(); - } - - /// - /// Called when the property value changed. - /// - /// The old value. - /// The new value. - protected virtual void OnMaximumChanged(T oldValue, T newValue) - { - if (IsInitialized) - { - SetValidSpinDirection(); - } - } - - /// - /// Called when the property value changed. - /// - /// The old value. - /// The new value. - protected virtual void OnMinimumChanged(T oldValue, T newValue) - { - if (IsInitialized) - { - SetValidSpinDirection(); - } - } - - /// - /// Called when the property value changed. - /// - /// The old value. - /// The new value. - protected virtual void OnTextChanged(string oldValue, string newValue) - { - if (IsInitialized) - { - SyncTextAndValueProperties(true, Text); - } - } - - /// - /// Called when the property value changed. - /// - /// The old value. - /// The new value. - protected virtual void OnValueChanged(T oldValue, T newValue) - { - if (!_internalValueSet && IsInitialized) - { - SyncTextAndValueProperties(false, null, true); - } - - SetValidSpinDirection(); - - RaiseValueChangedEvent(oldValue, newValue); - } - - /// - /// Called when the property has to be coerced. - /// - /// The value. - protected virtual T OnCoerceMaximum(T baseValue) - { - return baseValue; - } - - /// - /// Called when the property has to be coerced. - /// - /// The value. - protected virtual T OnCoerceMinimum(T baseValue) - { - return baseValue; - } - - /// - /// Called when the property has to be coerced. - /// - /// The value. - protected virtual T OnCoerceValue(T baseValue) - { - return baseValue; - } - - /// - /// Raises the OnSpin event when spinning is initiated by the end-user. - /// - /// The event args. - protected virtual void OnSpin(SpinEventArgs e) - { - if (e == null) - { - throw new ArgumentNullException("e"); - } - - var handler = Spinned; - handler?.Invoke(this, e); - - if (e.Direction == SpinDirection.Increase) - { - DoIncrement(); - } - else - { - DoDecrement(); - } - } - - /// - /// Raises the event. - /// - /// The old value. - /// The new value. - protected virtual void RaiseValueChangedEvent(T oldValue, T newValue) - { - var e = new UpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue); - RaiseEvent(e); - } - - /// - /// Converts the formatted text to a value. - /// - protected abstract T ConvertTextToValue(string text); - - /// - /// Converts the value to formatted text. - /// - /// - protected abstract string ConvertValueToText(); - - /// - /// Called by OnSpin when the spin direction is SpinDirection.Increase. - /// - protected abstract void OnIncrement(); - - /// - /// Called by OnSpin when the spin direction is SpinDirection.Descrease. - /// - protected abstract void OnDecrement(); - - /// - /// Sets the valid spin directions. - /// - protected abstract void SetValidSpinDirection(); - - /// - /// Called when the property value changed. - /// - /// The event args. - private static void OnCultureInfoChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is UpDownBase upDown) - { - var oldValue = (CultureInfo)e.OldValue; - var newValue = (CultureInfo)e.NewValue; - upDown.OnCultureInfoChanged(oldValue, newValue); - } - } - - /// - /// Called when the property value changed. - /// - /// The event args. - private static void OnDefaultValueChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is UpDownBase upDown) - { - var oldValue = (T)e.OldValue; - var newValue = (T)e.NewValue; - upDown.OnDefaultValueChanged(oldValue, newValue); - } - } - - /// - /// Called when the property value changed. - /// - /// The event args. - private static void OnDisplayDefaultValueOnEmptyTextChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is UpDownBase upDown) - { - var oldValue = (bool) e.OldValue; - var newValue = (bool) e.NewValue; - upDown.OnDisplayDefaultValueOnEmptyTextChanged(oldValue, newValue); - } - } - - /// - /// Called when the property value changed. - /// - /// The event args. - private static void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is UpDownBase upDown) - { - var oldValue = (bool)e.OldValue; - var newValue = (bool)e.NewValue; - upDown.OnIsReadOnlyChanged(oldValue, newValue); - } - } - - /// - /// Called when the property value changed. - /// - /// The event args. - private static void OnMaximumChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is UpDownBase upDown) - { - var oldValue = (T)e.OldValue; - var newValue = (T)e.NewValue; - upDown.OnMaximumChanged(oldValue, newValue); - } - } - - /// - /// Called when the property value changed. - /// - /// The event args. - private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is UpDownBase upDown) - { - var oldValue = (T)e.OldValue; - var newValue = (T)e.NewValue; - upDown.OnMinimumChanged(oldValue, newValue); - } - } - - /// - /// Called when the property value changed. - /// - /// The event args. - private static void OnTextChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is UpDownBase upDown) - { - var oldValue = (string)e.OldValue; - var newValue = (string)e.NewValue; - upDown.OnTextChanged(oldValue, newValue); - } - } - - /// - /// Called when the property value changed. - /// - /// The event args. - private static void OnValueChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Sender is UpDownBase upDown) - { - var oldValue = (T)e.OldValue; - var newValue = (T)e.NewValue; - upDown.OnValueChanged(oldValue, newValue); - } - } - - private void SetValueInternal(T value) - { - _internalValueSet = true; - try - { - Value = value; - } - finally - { - _internalValueSet = false; - } - } - - private static T OnCoerceMaximum(UpDownBase upDown, T value) - { - return upDown.OnCoerceMaximum(value); - } - - private static T OnCoerceMinimum(UpDownBase upDown, T value) - { - return upDown.OnCoerceMinimum(value); - } - - private void TextBoxOnTextChanged() - { - try - { - _isTextChangedFromUI = true; - if (TextBox != null) - { - Text = TextBox.Text; - } - } - finally - { - _isTextChangedFromUI = false; - } - } - - private void OnSpinnerSpin(object sender, SpinEventArgs e) - { - if (AllowSpin && !IsReadOnly) - { - var spin = !e.UsingMouseWheel; - spin |= ((TextBox != null) && TextBox.IsFocused); - - if (spin) - { - e.Handled = true; - OnSpin(e); - } - } - } - - internal void DoDecrement() - { - if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Decrease) == ValidSpinDirections.Decrease) - { - OnDecrement(); - } - } - - internal void DoIncrement() - { - if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Increase) == ValidSpinDirections.Increase) - { - OnIncrement(); - } - } - - public event EventHandler Spinned; - - private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e) - { - if (e.Device.Captured != Spinner) - { - Dispatcher.UIThread.InvokeAsync(() => { e.Device.Capture(Spinner); }, DispatcherPriority.Input); - } - } - - /// - /// Defines the event. - /// - public static readonly RoutedEvent> ValueChangedEvent = - RoutedEvent.Register, UpDownValueChangedEventArgs>(nameof(ValueChanged), RoutingStrategies.Bubble); - - /// - /// Raised when the changes. - /// - public event EventHandler ValueChanged - { - add { AddHandler(ValueChangedEvent, value); } - remove { RemoveHandler(ValueChangedEvent, value); } - } - - private bool CommitInput() - { - return SyncTextAndValueProperties(true, Text); - } - - /// - /// Synchronize and properties. - /// - /// If value should be updated from text. - /// The text. - protected bool SyncTextAndValueProperties(bool updateValueFromText, string text) - { - return SyncTextAndValueProperties(updateValueFromText, text, false); - } - - /// - /// Synchronize and properties. - /// - /// If value should be updated from text. - /// The text. - /// Force text update. - private bool SyncTextAndValueProperties(bool updateValueFromText, string text, bool forceTextUpdate) - { - if (_isSyncingTextAndValueProperties) - return true; - - _isSyncingTextAndValueProperties = true; - var parsedTextIsValid = true; - try - { - if (updateValueFromText) - { - if (string.IsNullOrEmpty(text)) - { - // An empty input sets the value to the default value. - SetValueInternal(DefaultValue); - } - else - { - try - { - var newValue = ConvertTextToValue(text); - if (!Equals(newValue, Value)) - { - SetValueInternal(newValue); - } - } - catch - { - parsedTextIsValid = false; - } - } - } - - // Do not touch the ongoing text input from user. - if (!_isTextChangedFromUI) - { - // Don't replace the empty Text with the non-empty representation of DefaultValue. - var shouldKeepEmpty = !forceTextUpdate && string.IsNullOrEmpty(Text) && Equals(Value, DefaultValue) && !DisplayDefaultValueOnEmptyText; - if (!shouldKeepEmpty) - { - var newText = ConvertValueToText(); - if (!Equals(Text, newText)) - { - Text = newText; - } - } - - // Sync Text and textBox - if (TextBox != null) - { - TextBox.Text = Text; - } - } - - if (_isTextChangedFromUI && !parsedTextIsValid) - { - // Text input was made from the user and the text - // repesents an invalid value. Disable the spinner in this case. - if (Spinner != null) - { - Spinner.ValidSpinDirection = ValidSpinDirections.None; - } - } - else - { - SetValidSpinDirection(); - } - } - finally - { - _isSyncingTextAndValueProperties = false; - } - return parsedTextIsValid; - } - } - - public class UpDownValueChangedEventArgs : RoutedEventArgs - { - public UpDownValueChangedEventArgs(RoutedEvent routedEvent, T oldValue, T newValue) : base(routedEvent) - { - OldValue = oldValue; - NewValue = newValue; - } - - public T OldValue { get; } - public T NewValue { get; } - } -} diff --git a/src/Avalonia.Themes.Default/NumericUpDown.xaml b/src/Avalonia.Themes.Default/NumericUpDown.xaml index e2ab6bf149..e6325d07dc 100644 --- a/src/Avalonia.Themes.Default/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Default/NumericUpDown.xaml @@ -1,5 +1,5 @@ -