csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
458 lines
17 KiB
458 lines
17 KiB
using System;
|
|
using Avalonia.Collections;
|
|
using Avalonia.Automation.Peers;
|
|
using Avalonia.Controls.Metadata;
|
|
using Avalonia.Controls.Mixins;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Data;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Layout;
|
|
using Avalonia.Utilities;
|
|
using Avalonia.Automation;
|
|
|
|
namespace Avalonia.Controls
|
|
{
|
|
/// <summary>
|
|
/// Enum which describes how to position the ticks in a <see cref="Slider"/>.
|
|
/// </summary>
|
|
public enum TickPlacement
|
|
{
|
|
/// <summary>
|
|
/// No tick marks will appear.
|
|
/// </summary>
|
|
None,
|
|
|
|
/// <summary>
|
|
/// Tick marks will appear above the track for a horizontal <see cref="Slider"/>, or to the left of the track for a vertical <see cref="Slider"/>.
|
|
/// </summary>
|
|
TopLeft,
|
|
|
|
/// <summary>
|
|
/// Tick marks will appear below the track for a horizontal <see cref="Slider"/>, or to the right of the track for a vertical <see cref="Slider"/>.
|
|
/// </summary>
|
|
BottomRight,
|
|
|
|
/// <summary>
|
|
/// Tick marks appear on both sides of either a horizontal or vertical <see cref="Slider"/>.
|
|
/// </summary>
|
|
Outside
|
|
}
|
|
|
|
/// <summary>
|
|
/// A control that lets the user select from a range of values by moving a Thumb control along a Track.
|
|
/// </summary>
|
|
[TemplatePart("PART_DecreaseButton", typeof(Button))]
|
|
[TemplatePart("PART_IncreaseButton", typeof(Button))]
|
|
[TemplatePart("PART_Track", typeof(Track))]
|
|
[PseudoClasses(":vertical", ":horizontal", ":pressed")]
|
|
public class Slider : RangeBase
|
|
{
|
|
/// <summary>
|
|
/// Defines the <see cref="Orientation"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<Orientation> OrientationProperty =
|
|
ScrollBar.OrientationProperty.AddOwner<Slider>();
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="IsDirectionReversed"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<bool> IsDirectionReversedProperty =
|
|
Track.IsDirectionReversedProperty.AddOwner<Slider>();
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="IsSnapToTickEnabled"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<bool> IsSnapToTickEnabledProperty =
|
|
AvaloniaProperty.Register<Slider, bool>(nameof(IsSnapToTickEnabled), false);
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="TickFrequency"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<double> TickFrequencyProperty =
|
|
AvaloniaProperty.Register<Slider, double>(nameof(TickFrequency), 0.0);
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="TickPlacement"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<TickPlacement> TickPlacementProperty =
|
|
AvaloniaProperty.Register<TickBar, TickPlacement>(nameof(TickPlacement), 0d);
|
|
|
|
/// <summary>
|
|
/// Defines the <see cref="TicksProperty"/> property.
|
|
/// </summary>
|
|
public static readonly StyledProperty<AvaloniaList<double>> TicksProperty =
|
|
TickBar.TicksProperty.AddOwner<Slider>();
|
|
|
|
// Slider required parts
|
|
private bool _isDragging = false;
|
|
private Track? _track;
|
|
private Button? _decreaseButton;
|
|
private Button? _increaseButton;
|
|
private IDisposable? _decreaseButtonPressDispose;
|
|
private IDisposable? _decreaseButtonReleaseDispose;
|
|
private IDisposable? _increaseButtonSubscription;
|
|
private IDisposable? _increaseButtonReleaseDispose;
|
|
private IDisposable? _pointerMovedDispose;
|
|
|
|
private const double Tolerance = 0.0001;
|
|
|
|
/// <summary>
|
|
/// Initializes static members of the <see cref="Slider"/> class.
|
|
/// </summary>
|
|
static Slider()
|
|
{
|
|
PressedMixin.Attach<Slider>();
|
|
FocusableProperty.OverrideDefaultValue<Slider>(true);
|
|
OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal);
|
|
Thumb.DragStartedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble);
|
|
Thumb.DragCompletedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragCompleted(e),
|
|
RoutingStrategies.Bubble);
|
|
|
|
ValueProperty.OverrideMetadata<Slider>(new DirectPropertyMetadata<double>(enableDataValidation: true));
|
|
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<Slider>(AutomationControlType.Slider);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Instantiates a new instance of the <see cref="Slider"/> class.
|
|
/// </summary>
|
|
public Slider()
|
|
{
|
|
UpdatePseudoClasses(Orientation);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Defines the ticks to be drawn on the tick bar.
|
|
/// </summary>
|
|
public AvaloniaList<double> Ticks
|
|
{
|
|
get => GetValue(TicksProperty);
|
|
set => SetValue(TicksProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the orientation of a <see cref="Slider"/>.
|
|
/// </summary>
|
|
public Orientation Orientation
|
|
{
|
|
get { return GetValue(OrientationProperty); }
|
|
set { SetValue(OrientationProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the direction of increasing value.
|
|
/// </summary>
|
|
/// <value>
|
|
/// true if the direction of increasing value is to the left for a horizontal slider or
|
|
/// down for a vertical slider; otherwise, false. The default is false.
|
|
/// </value>
|
|
public bool IsDirectionReversed
|
|
{
|
|
get { return GetValue(IsDirectionReversedProperty); }
|
|
set { SetValue(IsDirectionReversedProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value that indicates whether the <see cref="Slider"/> automatically moves the <see cref="Thumb"/> to the closest tick mark.
|
|
/// </summary>
|
|
public bool IsSnapToTickEnabled
|
|
{
|
|
get { return GetValue(IsSnapToTickEnabledProperty); }
|
|
set { SetValue(IsSnapToTickEnabledProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the interval between tick marks.
|
|
/// </summary>
|
|
public double TickFrequency
|
|
{
|
|
get { return GetValue(TickFrequencyProperty); }
|
|
set { SetValue(TickFrequencyProperty, value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value that indicates where to draw
|
|
/// tick marks in relation to the track.
|
|
/// </summary>
|
|
public TickPlacement TickPlacement
|
|
{
|
|
get { return GetValue(TickPlacementProperty); }
|
|
set { SetValue(TickPlacementProperty, value); }
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
|
{
|
|
base.OnApplyTemplate(e);
|
|
|
|
_decreaseButtonPressDispose?.Dispose();
|
|
_decreaseButtonReleaseDispose?.Dispose();
|
|
_increaseButtonSubscription?.Dispose();
|
|
_increaseButtonReleaseDispose?.Dispose();
|
|
_pointerMovedDispose?.Dispose();
|
|
|
|
_decreaseButton = e.NameScope.Find<Button>("PART_DecreaseButton");
|
|
_track = e.NameScope.Find<Track>("PART_Track");
|
|
_increaseButton = e.NameScope.Find<Button>("PART_IncreaseButton");
|
|
|
|
if (_track != null)
|
|
{
|
|
_track.IsThumbDragHandled = true;
|
|
}
|
|
|
|
if (_decreaseButton != null)
|
|
{
|
|
_decreaseButtonPressDispose = _decreaseButton.AddDisposableHandler(PointerPressedEvent, TrackPressed, RoutingStrategies.Tunnel);
|
|
_decreaseButtonReleaseDispose = _decreaseButton.AddDisposableHandler(PointerReleasedEvent, TrackReleased, RoutingStrategies.Tunnel);
|
|
}
|
|
|
|
if (_increaseButton != null)
|
|
{
|
|
_increaseButtonSubscription = _increaseButton.AddDisposableHandler(PointerPressedEvent, TrackPressed, RoutingStrategies.Tunnel);
|
|
_increaseButtonReleaseDispose = _increaseButton.AddDisposableHandler(PointerReleasedEvent, TrackReleased, RoutingStrategies.Tunnel);
|
|
}
|
|
|
|
_pointerMovedDispose = this.AddDisposableHandler(PointerMovedEvent, TrackMoved, RoutingStrategies.Tunnel);
|
|
}
|
|
|
|
protected override void OnKeyDown(KeyEventArgs e)
|
|
{
|
|
base.OnKeyDown(e);
|
|
|
|
if (e.Handled || e.KeyModifiers != KeyModifiers.None) return;
|
|
|
|
var handled = true;
|
|
|
|
switch (e.Key)
|
|
{
|
|
case Key.Down:
|
|
case Key.Left:
|
|
MoveToNextTick(IsDirectionReversed ? SmallChange : -SmallChange);
|
|
break;
|
|
|
|
case Key.Up:
|
|
case Key.Right:
|
|
MoveToNextTick(IsDirectionReversed ? -SmallChange : SmallChange);
|
|
break;
|
|
|
|
case Key.PageUp:
|
|
MoveToNextTick(IsDirectionReversed ? -LargeChange : LargeChange);
|
|
break;
|
|
|
|
case Key.PageDown:
|
|
MoveToNextTick(IsDirectionReversed ? LargeChange : -LargeChange);
|
|
break;
|
|
|
|
case Key.Home:
|
|
Value = Minimum;
|
|
break;
|
|
|
|
case Key.End:
|
|
Value = Maximum;
|
|
break;
|
|
|
|
default:
|
|
handled = false;
|
|
break;
|
|
}
|
|
|
|
e.Handled = handled;
|
|
}
|
|
|
|
private void MoveToNextTick(double direction)
|
|
{
|
|
if (direction == 0.0) return;
|
|
|
|
var value = Value;
|
|
|
|
// Find the next value by snapping
|
|
var next = SnapToTick(Math.Max(Minimum, Math.Min(Maximum, value + direction)));
|
|
|
|
var greaterThan = direction > 0; //search for the next tick greater than value?
|
|
|
|
// If the snapping brought us back to value, find the next tick point
|
|
if (Math.Abs(next - value) < Tolerance
|
|
&& !(greaterThan && Math.Abs(value - Maximum) < Tolerance) // Stop if searching up if already at Max
|
|
&& !(!greaterThan && Math.Abs(value - Minimum) < Tolerance)) // Stop if searching down if already at Min
|
|
{
|
|
var ticks = Ticks;
|
|
|
|
// If ticks collection is available, use it.
|
|
// Note that ticks may be unsorted.
|
|
if (ticks != null && ticks.Count > 0)
|
|
{
|
|
foreach (var tick in ticks)
|
|
{
|
|
// Find the smallest tick greater than value or the largest tick less than value
|
|
if (greaterThan && MathUtilities.GreaterThan(tick, value) &&
|
|
(MathUtilities.LessThan(tick, next) || Math.Abs(next - value) < Tolerance)
|
|
|| !greaterThan && MathUtilities.LessThan(tick, value) &&
|
|
(MathUtilities.GreaterThan(tick, next) || Math.Abs(next - value) < Tolerance))
|
|
{
|
|
next = tick;
|
|
}
|
|
}
|
|
}
|
|
else if (MathUtilities.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 (Math.Abs(next - value) > Tolerance)
|
|
{
|
|
Value = next;
|
|
}
|
|
}
|
|
|
|
private void TrackMoved(object? sender, PointerEventArgs e)
|
|
{
|
|
if (_isDragging)
|
|
{
|
|
MoveToPoint(e.GetCurrentPoint(_track));
|
|
}
|
|
}
|
|
|
|
private void TrackReleased(object? sender, PointerReleasedEventArgs e)
|
|
{
|
|
_isDragging = false;
|
|
}
|
|
|
|
private void TrackPressed(object? sender, PointerPressedEventArgs e)
|
|
{
|
|
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
|
{
|
|
MoveToPoint(e.GetCurrentPoint(_track));
|
|
_isDragging = true;
|
|
}
|
|
}
|
|
|
|
private void MoveToPoint(PointerPoint posOnTrack)
|
|
{
|
|
if (_track is null)
|
|
return;
|
|
|
|
var orient = Orientation == Orientation.Horizontal;
|
|
var thumbLength = (orient
|
|
? _track.Thumb.Bounds.Width
|
|
: _track.Thumb.Bounds.Height) + double.Epsilon;
|
|
var trackLength = (orient
|
|
? _track.Bounds.Width
|
|
: _track.Bounds.Height) - thumbLength;
|
|
var trackPos = orient ? posOnTrack.Position.X : posOnTrack.Position.Y;
|
|
var logicalPos = MathUtilities.Clamp((trackPos - thumbLength * 0.5) / trackLength, 0.0d, 1.0d);
|
|
var invert = orient ?
|
|
IsDirectionReversed ? 1 : 0 :
|
|
IsDirectionReversed ? 0 : 1;
|
|
var calcVal = Math.Abs(invert - logicalPos);
|
|
var range = Maximum - Minimum;
|
|
var finalValue = calcVal * range + Minimum;
|
|
|
|
Value = IsSnapToTickEnabled ? SnapToTick(finalValue) : finalValue;
|
|
}
|
|
|
|
protected override void UpdateDataValidation(
|
|
AvaloniaProperty property,
|
|
BindingValueType state,
|
|
Exception? error)
|
|
{
|
|
if (property == ValueProperty)
|
|
{
|
|
DataValidationErrors.SetError(this, error);
|
|
}
|
|
}
|
|
|
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
|
{
|
|
base.OnPropertyChanged(change);
|
|
|
|
if (change.Property == OrientationProperty)
|
|
{
|
|
UpdatePseudoClasses(change.GetNewValue<Orientation>());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when user start dragging the <see cref="Thumb"/>.
|
|
/// </summary>
|
|
/// <param name="e"></param>
|
|
protected virtual void OnThumbDragStarted(VectorEventArgs e)
|
|
{
|
|
_isDragging = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when user stop dragging the <see cref="Thumb"/>.
|
|
/// </summary>
|
|
/// <param name="e"></param>
|
|
protected virtual void OnThumbDragCompleted(VectorEventArgs e)
|
|
{
|
|
_isDragging = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Snap the input 'value' to the closest tick.
|
|
/// </summary>
|
|
/// <param name="value">Value that want to snap to closest Tick.</param>
|
|
private double SnapToTick(double value)
|
|
{
|
|
if (IsSnapToTickEnabled)
|
|
{
|
|
var previous = Minimum;
|
|
var next = Maximum;
|
|
|
|
// This property is rarely set so let's try to avoid the GetValue
|
|
var ticks = Ticks;
|
|
|
|
// If ticks collection is available, use it.
|
|
// Note that ticks may be unsorted.
|
|
if (ticks != null && ticks.Count > 0)
|
|
{
|
|
foreach (var tick in ticks)
|
|
{
|
|
if (MathUtilities.AreClose(tick, value))
|
|
{
|
|
return value;
|
|
}
|
|
|
|
if (MathUtilities.LessThan(tick, value) && MathUtilities.GreaterThan(tick, previous))
|
|
{
|
|
previous = tick;
|
|
}
|
|
else if (MathUtilities.GreaterThan(tick, value) && MathUtilities.LessThan(tick, next))
|
|
{
|
|
next = tick;
|
|
}
|
|
}
|
|
}
|
|
else if (MathUtilities.GreaterThan(TickFrequency, 0.0))
|
|
{
|
|
previous = Minimum + Math.Round((value - Minimum) / TickFrequency) * TickFrequency;
|
|
next = Math.Min(Maximum, previous + TickFrequency);
|
|
}
|
|
|
|
// Choose the closest value between previous and next. If tie, snap to 'next'.
|
|
value = MathUtilities.GreaterThanOrClose(value, (previous + next) * 0.5) ? next : previous;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
|
|
private void UpdatePseudoClasses(Orientation o)
|
|
{
|
|
PseudoClasses.Set(":vertical", o == Orientation.Vertical);
|
|
PseudoClasses.Set(":horizontal", o == Orientation.Horizontal);
|
|
}
|
|
}
|
|
}
|
|
|