From 3116f06e17ab95003ff1b97a12fbdb5bd08116f7 Mon Sep 17 00:00:00 2001 From: Tim <47110241+timunie@users.noreply.github.com> Date: Mon, 27 May 2024 14:47:40 +0200 Subject: [PATCH 1/4] Add sample to FASandbox for debugging purposes --- samples/FASandbox/MainWindow.axaml | 22 ++++++++-------------- samples/FASandbox/MainWindowViewModel.cs | 13 +++++++++++++ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/samples/FASandbox/MainWindow.axaml b/samples/FASandbox/MainWindow.axaml index c83da31e..953f0eae 100644 --- a/samples/FASandbox/MainWindow.axaml +++ b/samples/FASandbox/MainWindow.axaml @@ -1,16 +1,10 @@ - + + + + + + + + - - diff --git a/samples/FASandbox/MainWindowViewModel.cs b/samples/FASandbox/MainWindowViewModel.cs index e46f67b2..e7012295 100644 --- a/samples/FASandbox/MainWindowViewModel.cs +++ b/samples/FASandbox/MainWindowViewModel.cs @@ -7,6 +7,9 @@ namespace FASandbox; public class MainWindowViewModel : INotifyPropertyChanged { + private double _min; + private double _max; + public MainWindowViewModel() { @@ -33,6 +36,16 @@ private bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName]str return false; } + + public double Min + { + get => _min; + set => RaiseAndSetIfChanged(ref _min, value); + }public double Max + { + get => _max; + set => RaiseAndSetIfChanged(ref _max, value); + } } public class Command : ICommand From 437025fedc6ea659c1dffed60af647c07da18ee3 Mon Sep 17 00:00:00 2001 From: Tim <47110241+timunie@users.noreply.github.com> Date: Mon, 27 May 2024 14:50:28 +0200 Subject: [PATCH 2/4] Improvements to RangeSlider - Use CoerceValue where possible (same as Slider in Avalonia uses) - Add property to control SnapBehavior (not everyone may like it, same as Slider in Avalonia) - SnapToFrequency should be handled in input events, not in PropertyChanged (don't touch Values from the ViewModel sent) --- .../UI/Controls/RangeSlider/RangeSlider.cs | 201 +++++------------- .../Controls/RangeSlider/RangeSlider.props.cs | 62 +++++- 2 files changed, 113 insertions(+), 150 deletions(-) diff --git a/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.cs b/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.cs index 7cbd0beb..fb8cbb43 100644 --- a/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.cs +++ b/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.cs @@ -1,16 +1,11 @@ using System.Collections.Concurrent; -using System.Diagnostics; using Avalonia; -using Avalonia.Automation; -using Avalonia.Automation.Peers; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Input; using Avalonia.Threading; using Avalonia.Utilities; -using Avalonia.VisualTree; -using FluentAvalonia.Core; namespace FluentAvalonia.UI.Controls; @@ -19,103 +14,51 @@ public partial class RangeSlider : TemplatedControl protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); - + if (change.Property == RangeStartProperty) { - _minSet = true; - - if (!_valuesAssigned) - return; - - var newV = change.GetNewValue(); - RangeMinToStepFrequency(); - - if (_valuesAssigned) - { - if (newV < Minimum) - RangeStart = Minimum; - else if (newV > Maximum) - RangeStart = Maximum; - - SyncActiveRectangle(); - - if (newV > RangeEnd) - RangeEnd = newV; - } + var direction = change.GetNewValue() - change.GetOldValue(); + CoerceValue(RangeStartProperty); SyncThumbs(); if (!_isDraggingEnd && !_isDraggingStart) { - OnValueChanged(new RangeChangedEventArgs(change.GetOldValue(), newV, RangeSelectorProperty.RangeStartValue)); + OnValueChanged(new RangeChangedEventArgs(change.GetOldValue(), RangeStart, RangeSelectorProperty.RangeStartValue)); } } else if (change.Property == RangeEndProperty) { - _maxSet = true; - - if (!_valuesAssigned) - return; - - var newV = change.GetNewValue(); - RangeMaxToStepFrequency(); - - if (_valuesAssigned) - { - if (newV < Minimum) - RangeEnd = Minimum; - else if (newV > Maximum) - RangeEnd = Maximum; - - SyncActiveRectangle(); - - if (newV < RangeStart) - RangeStart = newV; - } + var direction = change.GetNewValue() - change.GetOldValue(); + CoerceValue(RangeEndProperty); SyncThumbs(); if (!_isDraggingEnd && !_isDraggingStart) { - OnValueChanged(new RangeChangedEventArgs(change.GetOldValue(), newV, RangeSelectorProperty.RangeEndValue)); + OnValueChanged(new RangeChangedEventArgs(change.GetOldValue(), RangeEnd, RangeSelectorProperty.RangeEndValue)); } } else if (change.Property == MinimumProperty) { - if (!_valuesAssigned) - return; - var (oldV, newV) = change.GetOldAndNewValue(); - if (Maximum < newV) - Maximum = newV + Epsilon; - - if (RangeStart < newV) - RangeStart = newV; + CoerceValue(MaximumProperty); + CoerceValue(RangeStartProperty); + CoerceValue(RangeEndProperty); - if (RangeEnd < newV) - RangeEnd = newV; - - if (newV != oldV) + if (Math.Abs(newV - oldV) > Epsilon) SyncThumbs(); } else if (change.Property == MaximumProperty) { - if (!_valuesAssigned) - return; - var (oldV, newV) = change.GetOldAndNewValue(); + + CoerceValue(MinimumProperty); + CoerceValue(RangeEndProperty); + CoerceValue(RangeStartProperty); - if (Minimum > newV) - Maximum = newV + Epsilon; - - if (RangeEnd > newV) - RangeEnd = newV; - - if (RangeStart > newV) - RangeStart = newV; - - if (newV != oldV) + if (Math.Abs(newV - oldV) > Epsilon) SyncThumbs(); } } @@ -145,9 +88,6 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) base.OnApplyTemplate(e); - VerifyValues(); - _valuesAssigned = true; - _activeRectangle = e.NameScope.Get(s_tpActiveRectangle); _minThumb = e.NameScope.Get(s_tpMinThumb); _maxThumb = e.NameScope.Get(s_tpMaxThumb); @@ -205,13 +145,12 @@ private void MinThumbDragDelta(object sender, VectorEventArgs e) var limit = RangeEnd - MinimumRange; if (newStart > limit) { - RangeEnd += newStart - oldStart; - newStart -= newStart - limit; - RangeStart = newStart; + RangeMaxToStepFrequency(newStart - oldStart); + RangeMinToStepFrequency(limit - newStart); } else { - RangeStart = newStart; + RangeMinToStepFrequency(newStart - oldStart); } if (_toolTipText != null) @@ -230,13 +169,12 @@ private void MaxThumbDragDelta(object sender, VectorEventArgs e) var limit = RangeStart + MinimumRange; if (newEnd < limit) { - RangeStart -= oldEnd - newEnd; - newEnd -= newEnd - limit; - RangeEnd = newEnd; + RangeMinToStepFrequency(newEnd - oldEnd); + RangeMaxToStepFrequency(limit - newEnd); } else { - RangeEnd = newEnd; + RangeMaxToStepFrequency(newEnd - oldEnd); } if (_toolTipText != null) @@ -307,7 +245,7 @@ private void MinThumbKeyDown(object sender, KeyEventArgs e) switch (e.Key) { case Key.Left: - RangeStart -= StepFrequency; + RangeMinToStepFrequency(-StepFrequency); SyncThumbs(fromMinKeyDown: true); SetToolTipAt(_minThumb, true); @@ -316,7 +254,7 @@ private void MinThumbKeyDown(object sender, KeyEventArgs e) break; case Key.Right: - RangeStart += StepFrequency; + RangeMinToStepFrequency(+StepFrequency); SyncThumbs(fromMinKeyDown: true); SetToolTipAt(_minThumb, true); @@ -331,7 +269,7 @@ private void MaxThumbKeyDown(object sender, KeyEventArgs e) switch (e.Key) { case Key.Left: - RangeEnd -= StepFrequency; + RangeMaxToStepFrequency(-StepFrequency); SyncThumbs(fromMaxKeyDown: true); if (!ToolTip.GetIsOpen(_maxThumb)) @@ -346,7 +284,7 @@ private void MaxThumbKeyDown(object sender, KeyEventArgs e) e.Handled = true; break; case Key.Right: - RangeEnd += StepFrequency; + RangeMaxToStepFrequency(+StepFrequency); SyncThumbs(fromMaxKeyDown: true); if (!ToolTip.GetIsOpen(_maxThumb)) @@ -467,10 +405,9 @@ private void ContainerCanvasPointerMoved(object sender, PointerEventArgs e) if (rs + delta < min) delta = min - rs; } - - RangeStart += delta; - RangeEnd += delta; + RangeMinToStepFrequency(delta); + RangeMaxToStepFrequency(delta); _absolutePosition = position; return; } @@ -532,69 +469,43 @@ private void UpdateToolTipText(double newValue) _toolTipText.Text = newValue.ToString(format); } } - - private void VerifyValues() + + private void RangeMinToStepFrequency(double direction) { - if (Minimum > Maximum) + if (_isProgramaticChange) { - Minimum = Maximum; - Maximum = Maximum; - } - - if (Minimum == Maximum) - { - Maximum += Epsilon; - } - - if (!_maxSet) - { - RangeEnd = Maximum; - } - - if (!_minSet) - { - RangeStart = Minimum; - } - - if (RangeStart < Minimum) - { - RangeStart = Minimum; - } - - if (RangeEnd < Minimum) - { - RangeEnd = Minimum; - } - - if (RangeStart > Maximum) - { - RangeStart = Maximum; - } - - if (RangeEnd > Maximum) - { - RangeEnd = Maximum; + return; } - if (RangeEnd < RangeStart) - { - RangeStart = RangeEnd; - } + _isProgramaticChange = true; + SetCurrentValue(RangeStartProperty, MoveToStepFrequency(RangeStart, direction)); + _isProgramaticChange = false; } - private void RangeMinToStepFrequency() + private void RangeMaxToStepFrequency(double direction) { - RangeStart = MoveToStepFrequency(RangeStart); - } + var newValue = MoveToStepFrequency(RangeEnd, direction); + + if (_isProgramaticChange || Math.Abs(RangeEnd - newValue) < Epsilon) + { + return; + } - private void RangeMaxToStepFrequency() - { - RangeEnd = MoveToStepFrequency(RangeEnd); + _isProgramaticChange = true; + SetCurrentValue(RangeEndProperty, newValue); + _isProgramaticChange = false; } - private double MoveToStepFrequency(double rangeValue) + private double MoveToStepFrequency(double rangeValue, double direction) { - double newValue = Minimum + (((int)Math.Round((rangeValue - Minimum) / StepFrequency)) * StepFrequency); + if (!IsSnapToStepFrequencyEnabled) + { + return rangeValue + direction; + } + + double newValue = direction > 0 + ? Minimum + (((int)Math.Floor((rangeValue + direction - Minimum) / StepFrequency)) * StepFrequency) + : Minimum + (((int)Math.Ceiling((rangeValue +direction - Minimum) / StepFrequency)) * StepFrequency); if (newValue < Minimum) { @@ -712,9 +623,6 @@ private static void UnParentToolTip(Control c) private Thumb _maxThumb; private Canvas _containerCanvas; private double _oldValue; - private bool _valuesAssigned; - private bool _minSet; - private bool _maxSet; private bool _pointerManipulatingMin; private bool _pointerManipulatingMax; private bool _pointerManipulatingBoth; @@ -724,6 +632,7 @@ private static void UnParentToolTip(Control c) private const double Epsilon = 0.01; private bool _isDraggingStart; private bool _isDraggingEnd; + private bool _isProgramaticChange; private readonly DispatcherTimer _keyTimer = new DispatcherTimer(); } diff --git a/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.props.cs b/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.props.cs index 6f61ad4e..56f75b49 100644 --- a/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.props.cs +++ b/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.props.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Shapes; using Avalonia.Data; using Avalonia.Input; +using Avalonia.Utilities; namespace FluentAvalonia.UI.Controls; @@ -23,28 +24,28 @@ public partial class RangeSlider /// public static readonly StyledProperty MinimumProperty = RangeBase.MinimumProperty.AddOwner( - new StyledPropertyMetadata(0d)); + new StyledPropertyMetadata(0d, coerce: CoerceMinimum)); /// /// Defines the property /// public static readonly StyledProperty MaximumProperty = RangeBase.MaximumProperty.AddOwner( - new StyledPropertyMetadata(100d)); + new StyledPropertyMetadata(100d, coerce: CoerceMaximum)); /// /// Defines the property /// public static readonly StyledProperty RangeStartProperty = AvaloniaProperty.Register(nameof(RangeStart), - defaultValue: 0, defaultBindingMode: BindingMode.TwoWay); + defaultValue: 0, defaultBindingMode: BindingMode.TwoWay, coerce: CoerceRangeStart); /// /// Defines the property /// public static readonly StyledProperty RangeEndProperty = AvaloniaProperty.Register(nameof(RangeEnd), - defaultValue: 100, defaultBindingMode: BindingMode.TwoWay); + defaultValue: 100, defaultBindingMode: BindingMode.TwoWay, coerce: CoerceRangeEnd); /// /// Defines the property @@ -52,6 +53,13 @@ public partial class RangeSlider public static readonly StyledProperty StepFrequencyProperty = AvaloniaProperty.Register(nameof(StepFrequency), defaultValue: 1); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsSnapToStepFrequencyEnabledProperty = + AvaloniaProperty.Register(nameof(IsSnapToStepFrequencyEnabled), true); + /// /// Defines the property @@ -80,7 +88,13 @@ public double Minimum get => GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); } + + private static double CoerceMinimum(AvaloniaObject sender, double value) + { + return ValidateDouble(value) ? value : sender.GetValue(MinimumProperty); + } + /// /// Gets or sets the maximum allowed value for the RangeSlider /// @@ -89,6 +103,14 @@ public double Maximum get => GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); } + + private static double CoerceMaximum(AvaloniaObject sender, double value) + { + return ValidateDouble(value) + ? Math.Max(value, sender.GetValue(MinimumProperty)) + : sender.GetValue(MaximumProperty); + } + /// /// Gets or sets the start of the selected range @@ -98,6 +120,13 @@ public double RangeStart get => GetValue(RangeStartProperty); set => SetValue(RangeStartProperty, value); } + + private static double CoerceRangeStart(AvaloniaObject sender, double value) + { + return ValidateDouble(value) + ? MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), Math.Min(sender.GetValue(RangeEndProperty), sender.GetValue(MaximumProperty))) // TODO: How to deal with MinRange here? + : sender.GetValue(RangeStartProperty); + } /// /// Gets or sets the end of the selected range @@ -108,6 +137,13 @@ public double RangeEnd set => SetValue(RangeEndProperty, value); } + private static double CoerceRangeEnd(AvaloniaObject sender, double value) + { + return ValidateDouble(value) + ? MathUtilities.Clamp(value, Math.Max(sender.GetValue(MinimumProperty), sender.GetValue(RangeStartProperty)), sender.GetValue(MaximumProperty)) // TODO: How to deal with MinRange here? + : sender.GetValue(RangeEndProperty); + } + /// /// Gets or sets the frequency of ticks when dragging the slider /// @@ -116,6 +152,15 @@ public double StepFrequency get => GetValue(StepFrequencyProperty); set => SetValue(StepFrequencyProperty, value); } + + /// + /// Gets or sets a value that indicates whether the automatically moves the to the closest step frequency. + /// + public bool IsSnapToStepFrequencyEnabled + { + get => GetValue(IsSnapToStepFrequencyEnabledProperty); + set => SetValue(IsSnapToStepFrequencyEnabledProperty, value); + } /// /// Gets or sets the string format used in the value ToolTip when dragging @@ -175,5 +220,14 @@ public bool ShowValueToolTip private const string s_tpMaxThumb = "MaxThumb"; private const string s_tpContainerCanvas = "ContainerCanvas"; private const string s_tpToolTipText = "ToolTipText"; + + /// + /// Checks if the double value is not infinity nor NaN. + /// + /// The value. + private static bool ValidateDouble(double value) + { + return !double.IsInfinity(value) && !double.IsNaN(value); + } } From be1cf16f443ddb900f381a37827a776dd220174a Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 27 May 2024 14:56:31 +0200 Subject: [PATCH 3/4] Fix Copy & Pasta error --- .../UI/Controls/RangeSlider/RangeSlider.props.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.props.cs b/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.props.cs index 56f75b49..d45ca382 100644 --- a/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.props.cs +++ b/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.props.cs @@ -58,7 +58,7 @@ public partial class RangeSlider /// Defines the property. /// public static readonly StyledProperty IsSnapToStepFrequencyEnabledProperty = - AvaloniaProperty.Register(nameof(IsSnapToStepFrequencyEnabled), true); + AvaloniaProperty.Register(nameof(IsSnapToStepFrequencyEnabled), true); /// @@ -154,7 +154,7 @@ public double StepFrequency } /// - /// Gets or sets a value that indicates whether the automatically moves the to the closest step frequency. + /// Gets or sets a value that indicates whether the automatically moves the to the closest step frequency. /// public bool IsSnapToStepFrequencyEnabled { From 10d669f532d847610100fed40a5594b938885abb Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 28 May 2024 10:45:03 +0200 Subject: [PATCH 4/4] More RangeSlider fixes and improvements - Do not crash when closing the App due to reset DataContext in CoerceValue - Make sure to follow Snap when pointer down outside the handles --- .../UI/Controls/RangeSlider/RangeSlider.cs | 34 +++++-------------- .../Controls/RangeSlider/RangeSlider.props.cs | 4 ++- 2 files changed, 11 insertions(+), 27 deletions(-) diff --git a/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.cs b/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.cs index fb8cbb43..7149afda 100644 --- a/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.cs +++ b/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.cs @@ -17,9 +17,9 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (change.Property == RangeStartProperty) { - var direction = change.GetNewValue() - change.GetOldValue(); CoerceValue(RangeStartProperty); - + CoerceValue(RangeEndProperty); + SyncThumbs(); if (!_isDraggingEnd && !_isDraggingStart) @@ -29,8 +29,8 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang } else if (change.Property == RangeEndProperty) { - var direction = change.GetNewValue() - change.GetOldValue(); CoerceValue(RangeEndProperty); + CoerceValue(RangeStartProperty); SyncThumbs(); @@ -416,12 +416,12 @@ private void ContainerCanvasPointerMoved(object sender, PointerEventArgs e) if (_pointerManipulatingMin && normalizedPosition < RangeEnd) { - RangeStart = DragThumb(_minThumb, 0, Canvas.GetLeft(_maxThumb), position); + RangeMinToStepFrequency(DragThumb(_minThumb, 0, Canvas.GetLeft(_maxThumb), position) - RangeStart); UpdateToolTipText(RangeStart); } else if (_pointerManipulatingMax && normalizedPosition > RangeStart) { - RangeEnd = DragThumb(_maxThumb, Canvas.GetLeft(_minThumb), DragWidth, position); + RangeMaxToStepFrequency(DragThumb(_maxThumb, Canvas.GetLeft(_minThumb), DragWidth, position)- RangeEnd); UpdateToolTipText(RangeEnd); } } @@ -447,13 +447,13 @@ private void ContainerCanvasPointerPressed(object sender, PointerPressedEventArg if (upperValueDiff < lowerValueDiff) { - RangeEnd = normalizedPosition; + RangeMaxToStepFrequency(normalizedPosition - RangeEnd); _pointerManipulatingMax = true; HandleThumbDragStarted(_maxThumb); } else { - RangeStart = normalizedPosition; + RangeMinToStepFrequency(normalizedPosition - RangeStart); _pointerManipulatingMin = true; HandleThumbDragStarted(_minThumb); } @@ -472,28 +472,12 @@ private void UpdateToolTipText(double newValue) private void RangeMinToStepFrequency(double direction) { - if (_isProgramaticChange) - { - return; - } - - _isProgramaticChange = true; SetCurrentValue(RangeStartProperty, MoveToStepFrequency(RangeStart, direction)); - _isProgramaticChange = false; } private void RangeMaxToStepFrequency(double direction) { - var newValue = MoveToStepFrequency(RangeEnd, direction); - - if (_isProgramaticChange || Math.Abs(RangeEnd - newValue) < Epsilon) - { - return; - } - - _isProgramaticChange = true; - SetCurrentValue(RangeEndProperty, newValue); - _isProgramaticChange = false; + SetCurrentValue(RangeEndProperty, MoveToStepFrequency(RangeEnd, direction)); } private double MoveToStepFrequency(double rangeValue, double direction) @@ -617,7 +601,6 @@ private static void UnParentToolTip(Control c) } } - private Rectangle _activeRectangle; private Thumb _minThumb; private Thumb _maxThumb; @@ -632,7 +615,6 @@ private static void UnParentToolTip(Control c) private const double Epsilon = 0.01; private bool _isDraggingStart; private bool _isDraggingEnd; - private bool _isProgramaticChange; private readonly DispatcherTimer _keyTimer = new DispatcherTimer(); } diff --git a/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.props.cs b/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.props.cs index d45ca382..db788fc4 100644 --- a/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.props.cs +++ b/src/FluentAvalonia/UI/Controls/RangeSlider/RangeSlider.props.cs @@ -140,7 +140,9 @@ public double RangeEnd private static double CoerceRangeEnd(AvaloniaObject sender, double value) { return ValidateDouble(value) - ? MathUtilities.Clamp(value, Math.Max(sender.GetValue(MinimumProperty), sender.GetValue(RangeStartProperty)), sender.GetValue(MaximumProperty)) // TODO: How to deal with MinRange here? + ? MathUtilities.Clamp(value, + Math.Min(Math.Max(sender.GetValue(MinimumProperty), sender.GetValue(RangeStartProperty)), sender.GetValue(MaximumProperty)), + sender.GetValue(MaximumProperty)) // TODO: How to deal with MinRange here? : sender.GetValue(RangeEndProperty); }