Files
HandyControl/src/Shared/HandyControl_Shared/Controls/Slider/RangeSlider/RangeSlider.cs
T
2021-06-08 23:11:14 +08:00

652 lines
26 KiB
C#

// Adapted from https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/Slider.cs
using System;
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using HandyControl.Data;
using HandyControl.Expression.Drawing;
using HandyControl.Tools;
namespace HandyControl.Controls
{
[DefaultEvent("ValueChanged"), DefaultProperty("Value")]
[TemplatePart(Name = ElementTrack, Type = typeof(Track))]
public class RangeSlider : TwoWayRangeBase
{
private const string ElementTrack = "PART_Track";
private RangeTrack _track;
private readonly ToolTip _autoToolTipStart = null;
private readonly ToolTip _autoToolTipEnd = null;
private RangeThumb _thumbCurrent;
private object _thumbOriginalToolTip;
private Point _originThumbPoint;
private Point _previousScreenCoordPosition;
static RangeSlider()
{
InitializeCommands();
MinimumProperty.OverrideMetadata(typeof(RangeSlider), new FrameworkPropertyMetadata(ValueBoxes.Double0Box, FrameworkPropertyMetadataOptions.AffectsMeasure));
MaximumProperty.OverrideMetadata(typeof(RangeSlider), new FrameworkPropertyMetadata(ValueBoxes.Double10Box, FrameworkPropertyMetadataOptions.AffectsMeasure));
ValueStartProperty.OverrideMetadata(typeof(RangeSlider), new FrameworkPropertyMetadata(ValueBoxes.Double0Box, FrameworkPropertyMetadataOptions.AffectsMeasure));
ValueEndProperty.OverrideMetadata(typeof(RangeSlider), new FrameworkPropertyMetadata(ValueBoxes.Double0Box, FrameworkPropertyMetadataOptions.AffectsMeasure));
// Register Event Handler for the Thumb
EventManager.RegisterClassHandler(typeof(RangeSlider), Thumb.DragStartedEvent, new DragStartedEventHandler(OnThumbDragStarted));
EventManager.RegisterClassHandler(typeof(RangeSlider), Thumb.DragDeltaEvent, new DragDeltaEventHandler(OnThumbDragDelta));
EventManager.RegisterClassHandler(typeof(RangeSlider), Thumb.DragCompletedEvent, new DragCompletedEventHandler(OnThumbDragCompleted));
// Listen to MouseLeftButtonDown event to determine if slide should move focus to itself
EventManager.RegisterClassHandler(typeof(RangeSlider), Mouse.MouseDownEvent, new MouseButtonEventHandler(OnMouseLeftButtonDown), true);
}
public RangeSlider()
{
CommandBindings.Add(new CommandBinding(IncreaseLarge, OnIncreaseLarge));
CommandBindings.Add(new CommandBinding(IncreaseSmall, OnIncreaseSmall));
CommandBindings.Add(new CommandBinding(DecreaseLarge, OnDecreaseLarge));
CommandBindings.Add(new CommandBinding(DecreaseSmall, OnDecreaseSmall));
CommandBindings.Add(new CommandBinding(CenterLarge, OnCenterLarge));
CommandBindings.Add(new CommandBinding(CenterSmall, OnCenterSmall));
}
private void OnIncreaseLarge(object sender, ExecutedRoutedEventArgs e) => (sender as RangeSlider)?.OnIncreaseLarge();
private void OnIncreaseSmall(object sender, ExecutedRoutedEventArgs e) => (sender as RangeSlider)?.OnIncreaseSmall();
private void OnDecreaseLarge(object sender, ExecutedRoutedEventArgs e) => (sender as RangeSlider)?.OnDecreaseLarge();
private void OnDecreaseSmall(object sender, ExecutedRoutedEventArgs e) => (sender as RangeSlider)?.OnDecreaseSmall();
private void OnCenterLarge(object sender, ExecutedRoutedEventArgs e) => (sender as RangeSlider)?.OnCenterLarge(e.Parameter);
private void OnCenterSmall(object sender, ExecutedRoutedEventArgs e) => (sender as RangeSlider)?.OnCenterSmall(e.Parameter);
protected virtual void OnIncreaseLarge() => MoveToNextTick(LargeChange, false);
protected virtual void OnIncreaseSmall() => MoveToNextTick(SmallChange, false);
protected virtual void OnDecreaseLarge() => MoveToNextTick(-LargeChange, true);
protected virtual void OnDecreaseSmall() => MoveToNextTick(-SmallChange, true);
protected virtual void OnCenterLarge(object parameter) => MoveToNextTick(LargeChange, false, true);
protected virtual void OnCenterSmall(object parameter) => MoveToNextTick(SmallChange, false, true);
public static RoutedCommand IncreaseLarge { get; private set; }
public static RoutedCommand IncreaseSmall { get; private set; }
public static RoutedCommand DecreaseLarge { get; private set; }
public static RoutedCommand DecreaseSmall { get; private set; }
public static RoutedCommand CenterLarge { get; private set; }
public static RoutedCommand CenterSmall { get; private set; }
private static void InitializeCommands()
{
IncreaseLarge = new RoutedCommand(nameof(IncreaseLarge), typeof(RangeSlider));
IncreaseSmall = new RoutedCommand(nameof(IncreaseSmall), typeof(RangeSlider));
DecreaseLarge = new RoutedCommand(nameof(DecreaseLarge), typeof(RangeSlider));
DecreaseSmall = new RoutedCommand(nameof(DecreaseSmall), typeof(RangeSlider));
CenterLarge = new RoutedCommand(nameof(CenterLarge), typeof(RangeSlider));
CenterSmall = new RoutedCommand(nameof(CenterSmall), typeof(RangeSlider));
}
public override void OnApplyTemplate()
{
_thumbCurrent = null;
base.OnApplyTemplate();
_track = GetTemplateChild(ElementTrack) as RangeTrack;
if (_autoToolTipStart != null)
{
_autoToolTipStart.PlacementTarget = _track?.ThumbStart;
}
if (_autoToolTipEnd != null)
{
_autoToolTipEnd.PlacementTarget = _track?.ThumbEnd;
}
}
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
"Orientation", typeof(Orientation), typeof(RangeSlider), new PropertyMetadata(default(Orientation)));
public Orientation Orientation
{
get => (Orientation) GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
public static readonly DependencyProperty IsDirectionReversedProperty = DependencyProperty.Register(
"IsDirectionReversed", typeof(bool), typeof(RangeSlider), new PropertyMetadata(ValueBoxes.FalseBox));
public bool IsDirectionReversed
{
get => (bool) GetValue(IsDirectionReversedProperty);
set => SetValue(IsDirectionReversedProperty, ValueBoxes.BooleanBox(value));
}
public static readonly DependencyProperty DelayProperty = RepeatButton.DelayProperty.AddOwner(typeof(RangeSlider), new FrameworkPropertyMetadata(GetKeyboardDelay()));
public int Delay
{
get => (int) GetValue(DelayProperty);
set => SetValue(DelayProperty, value);
}
internal static int GetKeyboardDelay()
{
var delay = SystemParameters.KeyboardDelay;
if (delay < 0 || delay > 3)
delay = 0;
return (delay + 1) * 250;
}
public static readonly DependencyProperty IntervalProperty = RepeatButton.IntervalProperty.AddOwner(typeof(RangeSlider), new FrameworkPropertyMetadata(GetKeyboardSpeed()));
public int Interval
{
get => (int) GetValue(IntervalProperty);
set => SetValue(IntervalProperty, value);
}
internal static int GetKeyboardSpeed()
{
var speed = SystemParameters.KeyboardSpeed;
if (speed < 0 || speed > 31)
speed = 31;
return (31 - speed) * (400 - 1000 / 30) / 31 + 1000 / 30;
}
public static readonly DependencyProperty AutoToolTipPlacementProperty = DependencyProperty.Register(
"AutoToolTipPlacement", typeof(AutoToolTipPlacement), typeof(RangeSlider), new PropertyMetadata(default(AutoToolTipPlacement)));
public AutoToolTipPlacement AutoToolTipPlacement
{
get => (AutoToolTipPlacement) GetValue(AutoToolTipPlacementProperty);
set => SetValue(AutoToolTipPlacementProperty, value);
}
public static readonly DependencyProperty AutoToolTipPrecisionProperty = DependencyProperty.Register(
"AutoToolTipPrecision", typeof(int), typeof(RangeSlider), new PropertyMetadata(ValueBoxes.Int0Box),
ValidateHelper.IsInRangeOfPosIntIncludeZero);
public int AutoToolTipPrecision
{
get => (int) GetValue(AutoToolTipPrecisionProperty);
set => SetValue(AutoToolTipPrecisionProperty, value);
}
public static readonly DependencyProperty IsSnapToTickEnabledProperty = DependencyProperty.Register(
"IsSnapToTickEnabled", typeof(bool), typeof(RangeSlider), new PropertyMetadata(ValueBoxes.FalseBox));
public bool IsSnapToTickEnabled
{
get => (bool) GetValue(IsSnapToTickEnabledProperty);
set => SetValue(IsSnapToTickEnabledProperty, ValueBoxes.BooleanBox(value));
}
public static readonly DependencyProperty TickPlacementProperty = DependencyProperty.Register(
"TickPlacement", typeof(TickPlacement), typeof(RangeSlider), new PropertyMetadata(default(TickPlacement)));
public TickPlacement TickPlacement
{
get => (TickPlacement) GetValue(TickPlacementProperty);
set => SetValue(TickPlacementProperty, value);
}
public static readonly DependencyProperty TickFrequencyProperty = DependencyProperty.Register(
"TickFrequency", typeof(double), typeof(RangeSlider), new PropertyMetadata(ValueBoxes.Double1Box),
ValidateHelper.IsInRangeOfPosDoubleIncludeZero);
public double TickFrequency
{
get => (double) GetValue(TickFrequencyProperty);
set => SetValue(TickFrequencyProperty, value);
}
public static readonly DependencyProperty TicksProperty = DependencyProperty.Register(
"Ticks", typeof(DoubleCollection), typeof(RangeSlider), new PropertyMetadata(new DoubleCollection()));
public DoubleCollection Ticks
{
get => (DoubleCollection) GetValue(TicksProperty);
set => SetValue(TicksProperty, value);
}
public static readonly DependencyProperty IsMoveToPointEnabledProperty = DependencyProperty.Register(
"IsMoveToPointEnabled", typeof(bool), typeof(RangeSlider), new PropertyMetadata(ValueBoxes.FalseBox));
public bool IsMoveToPointEnabled
{
get => (bool) GetValue(IsMoveToPointEnabledProperty);
set => SetValue(IsMoveToPointEnabledProperty, ValueBoxes.BooleanBox(value));
}
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (IsMoveToPointEnabled && _track.ThumbStart is {IsMouseOver: false} && _track.ThumbEnd is {IsMouseOver: false})
{
// Here we need to determine whether it's closer to the starting point or the end point.
var pt = e.MouseDevice.GetPosition(_track);
UpdateValue(pt);
e.Handled = true;
}
base.OnPreviewMouseLeftButtonDown(e);
}
private void MoveToNextTick(double direction, bool isStart, bool isCenter = false, object parameter = null)
{
if (MathHelper.AreClose(direction, 0)) return;
if (isCenter)
{
if (parameter == null)
{
var pt = Mouse.GetPosition(_track);
var newValue = _track.ValueFromPoint(pt);
if (ValidateHelper.IsInRangeOfDouble(newValue))
{
var dir = (ValueStart + ValueEnd) / 2 > newValue;
if (!dir)
{
isStart = false;
direction = -Math.Abs(direction);
}
else
{
isStart = true;
}
}
}
if (parameter is bool parameterValue)
{
isStart = parameterValue;
}
}
var value = isStart ? ValueStart : ValueEnd;
var next = SnapToTick(Math.Max(Minimum, Math.Min(Maximum, value + direction)));
var greaterThan = direction > 0;
// If the snapping brought us back to value, find the next tick point
if (MathHelper.AreClose(next, value) &&
!(greaterThan && MathHelper.AreClose(value, Maximum)) &&
!(!greaterThan && MathHelper.AreClose(value, Minimum)))
{
// If ticks collection is available, use it.
// Note that ticks may be unsorted.
if (Ticks is {Count: > 0})
{
foreach (var tick in Ticks)
{
// Find the smallest tick greater than value or the largest tick less than value
if (greaterThan && MathHelper.GreaterThan(tick, value) && (MathHelper.LessThan(tick, next) || MathHelper.AreClose(next, value))
|| !greaterThan && MathHelper.LessThan(tick, value) && (MathHelper.GreaterThan(tick, next) || MathHelper.AreClose(next, value)))
{
next = tick;
}
}
}
else if (MathHelper.GreaterThan(TickFrequency, 0.0))
{
// Find the current tick we are at
var tickNumber = Math.Round((value - Minimum) / TickFrequency);
if (greaterThan)
tickNumber += 1.0;
else
tickNumber -= 1.0;
next = Minimum + tickNumber * TickFrequency;
}
}
// Update if we've found a better value
if (!MathHelper.AreClose(next, value))
{
SetCurrentValue(isStart ? ValueStartProperty : ValueEndProperty, next);
}
}
private double SnapToTick(double value)
{
if (!IsSnapToTickEnabled) return value;
var previous = Minimum;
var next = Maximum;
if (Ticks is {Count: > 0})
{
foreach (var tick in Ticks)
{
if (MathHelper.AreClose(tick, value))
{
return value;
}
if (MathHelper.LessThan(tick, value) && MathHelper.GreaterThan(tick, previous))
{
previous = tick;
}
else if (MathHelper.GreaterThan(tick, value) && MathHelper.LessThan(tick, next))
{
next = tick;
}
}
}
else if (MathHelper.GreaterThan(TickFrequency, 0.0))
{
previous = Minimum + Math.Round((value - Minimum) / TickFrequency) * TickFrequency;
next = Math.Min(Maximum, previous + TickFrequency);
}
return MathHelper.GreaterThanOrClose(value, (previous + next) * 0.5) ? next : previous;
}
private void UpdateValue(Point point)
{
var newValue = _track.ValueFromPoint(point);
if (ValidateHelper.IsInRangeOfDouble(newValue))
{
var isStart = (ValueStart + ValueEnd) / 2 > newValue;
UpdateValue(newValue, isStart);
}
}
private void UpdateValue(double value, bool isStart)
{
var snappedValue = SnapToTick(value);
if (isStart)
{
if (!MathHelper.AreClose(snappedValue, ValueStart))
{
var start = Math.Max(Minimum, Math.Min(Maximum, snappedValue));
if (start > ValueEnd)
{
SetCurrentValue(ValueStartProperty, ValueEnd);
SetCurrentValue(ValueEndProperty, start);
_track.ThumbStart.CancelDrag();
_track.ThumbEnd.StartDrag();
_thumbCurrent = _track.ThumbEnd;
}
else
{
SetCurrentValue(ValueStartProperty, start);
}
}
}
else
{
if (!MathHelper.AreClose(snappedValue, ValueEnd))
{
var end = Math.Max(Minimum, Math.Min(Maximum, snappedValue));
if (end < ValueStart)
{
SetCurrentValue(ValueEndProperty, ValueStart);
SetCurrentValue(ValueStartProperty, end);
_track.ThumbEnd.CancelDrag();
_track.ThumbStart.StartDrag();
_thumbCurrent = _track.ThumbStart;
}
else
{
SetCurrentValue(ValueEndProperty, end);
}
}
}
}
private static void OnThumbDragStarted(object sender, DragStartedEventArgs e) => (sender as RangeSlider)?.OnThumbDragStarted(e);
protected virtual void OnThumbDragStarted(DragStartedEventArgs e)
{
// Show AutoToolTip if needed.
if (e.OriginalSource is not RangeThumb thumb) return;
_thumbCurrent = thumb;
_originThumbPoint = Mouse.GetPosition(_thumbCurrent);
_thumbCurrent.StartDrag();
if (AutoToolTipPlacement == AutoToolTipPlacement.None)
{
return;
}
var isStart = thumb.Equals(_track.ThumbStart);
if (!isStart)
{
if (!thumb.Equals(_track.ThumbEnd)) return;
}
// Save original tooltip
_thumbOriginalToolTip = thumb.ToolTip;
OnThumbDragStarted(isStart ? _autoToolTipStart : _autoToolTipEnd, isStart);
}
private void OnThumbDragStarted(ToolTip toolTip, bool isStart)
{
toolTip ??= new ToolTip
{
Placement = PlacementMode.Custom,
PlacementTarget = isStart ? _track.ThumbStart : _track.ThumbEnd,
CustomPopupPlacementCallback = AutoToolTipCustomPlacementCallback
};
if (isStart)
{
_track.ThumbStart.ToolTip = toolTip;
}
else
{
_track.ThumbEnd.ToolTip = toolTip;
}
toolTip.Content = GetAutoToolTipNumber(isStart);
toolTip.IsOpen = true;
}
private CustomPopupPlacement[] AutoToolTipCustomPlacementCallback(Size popupSize, Size targetSize, Point offset)
{
switch (AutoToolTipPlacement)
{
case AutoToolTipPlacement.TopLeft:
if (Orientation == Orientation.Horizontal)
{
// Place popup at top of thumb
return new[]{new CustomPopupPlacement(
new Point((targetSize.Width - popupSize.Width) * 0.5, -popupSize.Height),
PopupPrimaryAxis.Horizontal)
};
}
else
{
// Place popup at left of thumb
return new[] {
new CustomPopupPlacement(
new Point(-popupSize.Width, (targetSize.Height - popupSize.Height) * 0.5),
PopupPrimaryAxis.Vertical)
};
}
case AutoToolTipPlacement.BottomRight:
if (Orientation == Orientation.Horizontal)
{
// Place popup at bottom of thumb
return new[] {
new CustomPopupPlacement(
new Point((targetSize.Width - popupSize.Width) * 0.5, targetSize.Height) ,
PopupPrimaryAxis.Horizontal)
};
}
else
{
// Place popup at right of thumb
return new[] {
new CustomPopupPlacement(
new Point(targetSize.Width, (targetSize.Height - popupSize.Height) * 0.5),
PopupPrimaryAxis.Vertical)
};
}
default:
return new CustomPopupPlacement[] { };
}
}
private string GetAutoToolTipNumber(bool isStart)
{
var format = (NumberFormatInfo) NumberFormatInfo.CurrentInfo.Clone();
format.NumberDecimalDigits = AutoToolTipPrecision;
return isStart ? ValueStart.ToString("N", format) : ValueEnd.ToString("N", format);
}
private static void OnThumbDragDelta(object sender, DragDeltaEventArgs e) => (sender as RangeSlider)?.OnThumbDragDelta(e);
protected virtual void OnThumbDragDelta(DragDeltaEventArgs e)
{
if (e.OriginalSource is not Thumb thumb) return;
var isStart = thumb.Equals(_track.ThumbStart);
if (!isStart)
{
if (!thumb.Equals(_track.ThumbEnd)) return;
}
// Convert to Track's co-ordinate
OnThumbDragDelta(_track, isStart, e);
}
private void OnThumbDragDelta(RangeTrack track, bool isStart, DragDeltaEventArgs e)
{
if (track == null || track.ThumbStart == null | _track.ThumbEnd == null) return;
var newValue = (isStart ? ValueStart : ValueEnd) + track.ValueFromDistance(e.HorizontalChange, e.VerticalChange);
if (ValidateHelper.IsInRangeOfDouble(newValue))
{
UpdateValue(newValue, isStart);
}
// Show AutoToolTip if needed
if (AutoToolTipPlacement != AutoToolTipPlacement.None)
{
var toolTip = (isStart ? _autoToolTipStart : _autoToolTipEnd) ?? new ToolTip();
toolTip.Content = GetAutoToolTipNumber(isStart);
var thumb = isStart ? _track.ThumbStart : _track.ThumbEnd;
if (!Equals(thumb.ToolTip, toolTip))
{
thumb.ToolTip = toolTip;
}
if (!toolTip.IsOpen)
{
toolTip.IsOpen = true;
}
}
}
private static void OnThumbDragCompleted(object sender, DragCompletedEventArgs e) => (sender as RangeSlider)?.OnThumbDragCompleted(e);
protected virtual void OnThumbDragCompleted(DragCompletedEventArgs e)
{
// Show AutoToolTip if needed.
if (e.OriginalSource is not Thumb thumb || AutoToolTipPlacement == AutoToolTipPlacement.None)
{
return;
}
var isStart = thumb.Equals(_track.ThumbStart);
if (!isStart)
{
if (!thumb.Equals(_track.ThumbEnd)) return;
}
var toolTip = isStart ? _autoToolTipStart : _autoToolTipEnd;
if (toolTip != null)
{
toolTip.IsOpen = false;
}
thumb.ToolTip = _thumbOriginalToolTip;
}
private static void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton != MouseButton.Left) return;
var slider = (RangeSlider) sender;
// When someone click on the Slider's part, and it's not focusable
// Slider need to take the focus in order to process keyboard correctly
if (!slider.IsKeyboardFocusWithin)
{
e.Handled = slider.Focus() || e.Handled;
}
if (slider._track.ThumbStart.IsMouseOver)
{
slider._track.ThumbStart.StartDrag();
slider._thumbCurrent = slider._track.ThumbStart;
}
if (slider._track.ThumbEnd.IsMouseOver)
{
slider._track.ThumbEnd.StartDrag();
slider._thumbCurrent = slider._track.ThumbEnd;
}
}
protected override void OnMouseMove(MouseEventArgs e)
{
if (_thumbCurrent == null) return;
if (e.MouseDevice.LeftButton != MouseButtonState.Pressed) return;
if (!_thumbCurrent.IsDragging) return;
var thumbCoordPosition = e.GetPosition(_thumbCurrent);
var screenCoordPosition = PointFromScreen(thumbCoordPosition);
if (screenCoordPosition != _previousScreenCoordPosition)
{
_previousScreenCoordPosition = screenCoordPosition;
_thumbCurrent.RaiseEvent(new DragDeltaEventArgs(thumbCoordPosition.X - _originThumbPoint.X,
thumbCoordPosition.Y - _originThumbPoint.Y));
}
}
protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnPreviewMouseLeftButtonUp(e);
_thumbCurrent = null;
}
}
}