Slider.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. using System;
  2. using Avalonia.Collections;
  3. using Avalonia.Controls.Metadata;
  4. using Avalonia.Controls.Mixins;
  5. using Avalonia.Controls.Primitives;
  6. using Avalonia.Data;
  7. using Avalonia.Input;
  8. using Avalonia.Interactivity;
  9. using Avalonia.Layout;
  10. using Avalonia.Utilities;
  11. namespace Avalonia.Controls
  12. {
  13. /// <summary>
  14. /// Enum which describes how to position the ticks in a <see cref="Slider"/>.
  15. /// </summary>
  16. public enum TickPlacement
  17. {
  18. /// <summary>
  19. /// No tick marks will appear.
  20. /// </summary>
  21. None,
  22. /// <summary>
  23. /// 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"/>.
  24. /// </summary>
  25. TopLeft,
  26. /// <summary>
  27. /// 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"/>.
  28. /// </summary>
  29. BottomRight,
  30. /// <summary>
  31. /// Tick marks appear on both sides of either a horizontal or vertical <see cref="Slider"/>.
  32. /// </summary>
  33. Outside
  34. }
  35. /// <summary>
  36. /// A control that lets the user select from a range of values by moving a Thumb control along a Track.
  37. /// </summary>
  38. [PseudoClasses(":vertical", ":horizontal", ":pressed")]
  39. public class Slider : RangeBase
  40. {
  41. /// <summary>
  42. /// Defines the <see cref="Orientation"/> property.
  43. /// </summary>
  44. public static readonly StyledProperty<Orientation> OrientationProperty =
  45. ScrollBar.OrientationProperty.AddOwner<Slider>();
  46. /// <summary>
  47. /// Defines the <see cref="IsDirectionReversed"/> property.
  48. /// </summary>
  49. public static readonly StyledProperty<bool> IsDirectionReversedProperty =
  50. Track.IsDirectionReversedProperty.AddOwner<Slider>();
  51. /// <summary>
  52. /// Defines the <see cref="IsSnapToTickEnabled"/> property.
  53. /// </summary>
  54. public static readonly StyledProperty<bool> IsSnapToTickEnabledProperty =
  55. AvaloniaProperty.Register<Slider, bool>(nameof(IsSnapToTickEnabled), false);
  56. /// <summary>
  57. /// Defines the <see cref="TickFrequency"/> property.
  58. /// </summary>
  59. public static readonly StyledProperty<double> TickFrequencyProperty =
  60. AvaloniaProperty.Register<Slider, double>(nameof(TickFrequency), 0.0);
  61. /// <summary>
  62. /// Defines the <see cref="TickPlacement"/> property.
  63. /// </summary>
  64. public static readonly StyledProperty<TickPlacement> TickPlacementProperty =
  65. AvaloniaProperty.Register<TickBar, TickPlacement>(nameof(TickPlacement), 0d);
  66. /// <summary>
  67. /// Defines the <see cref="TicksProperty"/> property.
  68. /// </summary>
  69. public static readonly StyledProperty<AvaloniaList<double>> TicksProperty =
  70. TickBar.TicksProperty.AddOwner<Slider>();
  71. // Slider required parts
  72. private bool _isDragging = false;
  73. private Track _track;
  74. private Button _decreaseButton;
  75. private Button _increaseButton;
  76. private IDisposable _decreaseButtonPressDispose;
  77. private IDisposable _decreaseButtonReleaseDispose;
  78. private IDisposable _increaseButtonSubscription;
  79. private IDisposable _increaseButtonReleaseDispose;
  80. private IDisposable _pointerMovedDispose;
  81. private const double Tolerance = 0.0001;
  82. /// <summary>
  83. /// Initializes static members of the <see cref="Slider"/> class.
  84. /// </summary>
  85. static Slider()
  86. {
  87. PressedMixin.Attach<Slider>();
  88. FocusableProperty.OverrideDefaultValue<Slider>(true);
  89. OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal);
  90. Thumb.DragStartedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble);
  91. Thumb.DragCompletedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragCompleted(e),
  92. RoutingStrategies.Bubble);
  93. ValueProperty.OverrideMetadata<Slider>(new DirectPropertyMetadata<double>(enableDataValidation: true));
  94. }
  95. /// <summary>
  96. /// Instantiates a new instance of the <see cref="Slider"/> class.
  97. /// </summary>
  98. public Slider()
  99. {
  100. UpdatePseudoClasses(Orientation);
  101. }
  102. /// <summary>
  103. /// Defines the ticks to be drawn on the tick bar.
  104. /// </summary>
  105. public AvaloniaList<double> Ticks
  106. {
  107. get => GetValue(TicksProperty);
  108. set => SetValue(TicksProperty, value);
  109. }
  110. /// <summary>
  111. /// Gets or sets the orientation of a <see cref="Slider"/>.
  112. /// </summary>
  113. public Orientation Orientation
  114. {
  115. get { return GetValue(OrientationProperty); }
  116. set { SetValue(OrientationProperty, value); }
  117. }
  118. /// <summary>
  119. /// Gets or sets the direction of increasing value.
  120. /// </summary>
  121. /// <value>
  122. /// true if the direction of increasing value is to the left for a horizontal slider or
  123. /// down for a vertical slider; otherwise, false. The default is false.
  124. /// </value>
  125. public bool IsDirectionReversed
  126. {
  127. get { return GetValue(IsDirectionReversedProperty); }
  128. set { SetValue(IsDirectionReversedProperty, value); }
  129. }
  130. /// <summary>
  131. /// Gets or sets a value that indicates whether the <see cref="Slider"/> automatically moves the <see cref="Thumb"/> to the closest tick mark.
  132. /// </summary>
  133. public bool IsSnapToTickEnabled
  134. {
  135. get { return GetValue(IsSnapToTickEnabledProperty); }
  136. set { SetValue(IsSnapToTickEnabledProperty, value); }
  137. }
  138. /// <summary>
  139. /// Gets or sets the interval between tick marks.
  140. /// </summary>
  141. public double TickFrequency
  142. {
  143. get { return GetValue(TickFrequencyProperty); }
  144. set { SetValue(TickFrequencyProperty, value); }
  145. }
  146. /// <summary>
  147. /// Gets or sets a value that indicates where to draw
  148. /// tick marks in relation to the track.
  149. /// </summary>
  150. public TickPlacement TickPlacement
  151. {
  152. get { return GetValue(TickPlacementProperty); }
  153. set { SetValue(TickPlacementProperty, value); }
  154. }
  155. /// <inheritdoc/>
  156. protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
  157. {
  158. base.OnApplyTemplate(e);
  159. _decreaseButtonPressDispose?.Dispose();
  160. _decreaseButtonReleaseDispose?.Dispose();
  161. _increaseButtonSubscription?.Dispose();
  162. _increaseButtonReleaseDispose?.Dispose();
  163. _pointerMovedDispose?.Dispose();
  164. _decreaseButton = e.NameScope.Find<Button>("PART_DecreaseButton");
  165. _track = e.NameScope.Find<Track>("PART_Track");
  166. _increaseButton = e.NameScope.Find<Button>("PART_IncreaseButton");
  167. if (_track != null)
  168. {
  169. _track.IsThumbDragHandled = true;
  170. }
  171. if (_decreaseButton != null)
  172. {
  173. _decreaseButtonPressDispose = _decreaseButton.AddDisposableHandler(PointerPressedEvent, TrackPressed, RoutingStrategies.Tunnel);
  174. _decreaseButtonReleaseDispose = _decreaseButton.AddDisposableHandler(PointerReleasedEvent, TrackReleased, RoutingStrategies.Tunnel);
  175. }
  176. if (_increaseButton != null)
  177. {
  178. _increaseButtonSubscription = _increaseButton.AddDisposableHandler(PointerPressedEvent, TrackPressed, RoutingStrategies.Tunnel);
  179. _increaseButtonReleaseDispose = _increaseButton.AddDisposableHandler(PointerReleasedEvent, TrackReleased, RoutingStrategies.Tunnel);
  180. }
  181. _pointerMovedDispose = this.AddDisposableHandler(PointerMovedEvent, TrackMoved, RoutingStrategies.Tunnel);
  182. }
  183. protected override void OnKeyDown(KeyEventArgs e)
  184. {
  185. base.OnKeyDown(e);
  186. if (e.Handled || e.KeyModifiers != KeyModifiers.None) return;
  187. var handled = true;
  188. switch (e.Key)
  189. {
  190. case Key.Down:
  191. case Key.Left:
  192. MoveToNextTick(IsDirectionReversed ? SmallChange : -SmallChange);
  193. break;
  194. case Key.Up:
  195. case Key.Right:
  196. MoveToNextTick(IsDirectionReversed ? -SmallChange : SmallChange);
  197. break;
  198. case Key.PageUp:
  199. MoveToNextTick(IsDirectionReversed ? -LargeChange : LargeChange);
  200. break;
  201. case Key.PageDown:
  202. MoveToNextTick(IsDirectionReversed ? LargeChange : -LargeChange);
  203. break;
  204. case Key.Home:
  205. Value = Minimum;
  206. break;
  207. case Key.End:
  208. Value = Maximum;
  209. break;
  210. default:
  211. handled = false;
  212. break;
  213. }
  214. e.Handled = handled;
  215. }
  216. private void MoveToNextTick(double direction)
  217. {
  218. if (direction == 0.0) return;
  219. var value = Value;
  220. // Find the next value by snapping
  221. var next = SnapToTick(Math.Max(Minimum, Math.Min(Maximum, value + direction)));
  222. var greaterThan = direction > 0; //search for the next tick greater than value?
  223. // If the snapping brought us back to value, find the next tick point
  224. if (Math.Abs(next - value) < Tolerance
  225. && !(greaterThan && Math.Abs(value - Maximum) < Tolerance) // Stop if searching up if already at Max
  226. && !(!greaterThan && Math.Abs(value - Minimum) < Tolerance)) // Stop if searching down if already at Min
  227. {
  228. var ticks = Ticks;
  229. // If ticks collection is available, use it.
  230. // Note that ticks may be unsorted.
  231. if (ticks != null && ticks.Count > 0)
  232. {
  233. foreach (var tick in ticks)
  234. {
  235. // Find the smallest tick greater than value or the largest tick less than value
  236. if (greaterThan && MathUtilities.GreaterThan(tick, value) &&
  237. (MathUtilities.LessThan(tick, next) || Math.Abs(next - value) < Tolerance)
  238. || !greaterThan && MathUtilities.LessThan(tick, value) &&
  239. (MathUtilities.GreaterThan(tick, next) || Math.Abs(next - value) < Tolerance))
  240. {
  241. next = tick;
  242. }
  243. }
  244. }
  245. else if (MathUtilities.GreaterThan(TickFrequency, 0.0))
  246. {
  247. // Find the current tick we are at
  248. var tickNumber = Math.Round((value - Minimum) / TickFrequency);
  249. if (greaterThan)
  250. tickNumber += 1.0;
  251. else
  252. tickNumber -= 1.0;
  253. next = Minimum + tickNumber * TickFrequency;
  254. }
  255. }
  256. // Update if we've found a better value
  257. if (Math.Abs(next - value) > Tolerance)
  258. {
  259. Value = next;
  260. }
  261. }
  262. private void TrackMoved(object sender, PointerEventArgs e)
  263. {
  264. if (_isDragging)
  265. {
  266. MoveToPoint(e.GetCurrentPoint(_track));
  267. }
  268. }
  269. private void TrackReleased(object sender, PointerReleasedEventArgs e)
  270. {
  271. _isDragging = false;
  272. }
  273. private void TrackPressed(object sender, PointerPressedEventArgs e)
  274. {
  275. if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
  276. {
  277. MoveToPoint(e.GetCurrentPoint(_track));
  278. _isDragging = true;
  279. }
  280. }
  281. private void MoveToPoint(PointerPoint x)
  282. {
  283. var orient = Orientation == Orientation.Horizontal;
  284. var pointDen = orient ? _track.Bounds.Width : _track.Bounds.Height;
  285. // Just add epsilon to avoid NaN in case 0/0
  286. pointDen += double.Epsilon;
  287. var pointNum = orient ? x.Position.X : x.Position.Y;
  288. var logicalPos = MathUtilities.Clamp(pointNum / pointDen, 0.0d, 1.0d);
  289. var invert = orient ?
  290. IsDirectionReversed ? 1 : 0 :
  291. IsDirectionReversed ? 0 : 1;
  292. var calcVal = Math.Abs(invert - logicalPos);
  293. var range = Maximum - Minimum;
  294. var finalValue = calcVal * range + Minimum;
  295. Value = IsSnapToTickEnabled ? SnapToTick(finalValue) : finalValue;
  296. }
  297. protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
  298. {
  299. if (property == ValueProperty)
  300. {
  301. DataValidationErrors.SetError(this, value.Error);
  302. }
  303. }
  304. protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
  305. {
  306. base.OnPropertyChanged(change);
  307. if (change.Property == OrientationProperty)
  308. {
  309. UpdatePseudoClasses(change.NewValue.GetValueOrDefault<Orientation>());
  310. }
  311. }
  312. /// <summary>
  313. /// Called when user start dragging the <see cref="Thumb"/>.
  314. /// </summary>
  315. /// <param name="e"></param>
  316. protected virtual void OnThumbDragStarted(VectorEventArgs e)
  317. {
  318. _isDragging = true;
  319. }
  320. /// <summary>
  321. /// Called when user stop dragging the <see cref="Thumb"/>.
  322. /// </summary>
  323. /// <param name="e"></param>
  324. protected virtual void OnThumbDragCompleted(VectorEventArgs e)
  325. {
  326. _isDragging = false;
  327. }
  328. /// <summary>
  329. /// Snap the input 'value' to the closest tick.
  330. /// </summary>
  331. /// <param name="value">Value that want to snap to closest Tick.</param>
  332. private double SnapToTick(double value)
  333. {
  334. if (IsSnapToTickEnabled)
  335. {
  336. var previous = Minimum;
  337. var next = Maximum;
  338. // This property is rarely set so let's try to avoid the GetValue
  339. var ticks = Ticks;
  340. // If ticks collection is available, use it.
  341. // Note that ticks may be unsorted.
  342. if (ticks != null && ticks.Count > 0)
  343. {
  344. foreach (var tick in ticks)
  345. {
  346. if (MathUtilities.AreClose(tick, value))
  347. {
  348. return value;
  349. }
  350. if (MathUtilities.LessThan(tick, value) && MathUtilities.GreaterThan(tick, previous))
  351. {
  352. previous = tick;
  353. }
  354. else if (MathUtilities.GreaterThan(tick, value) && MathUtilities.LessThan(tick, next))
  355. {
  356. next = tick;
  357. }
  358. }
  359. }
  360. else if (MathUtilities.GreaterThan(TickFrequency, 0.0))
  361. {
  362. previous = Minimum + Math.Round((value - Minimum) / TickFrequency) * TickFrequency;
  363. next = Math.Min(Maximum, previous + TickFrequency);
  364. }
  365. // Choose the closest value between previous and next. If tie, snap to 'next'.
  366. value = MathUtilities.GreaterThanOrClose(value, (previous + next) * 0.5) ? next : previous;
  367. }
  368. return value;
  369. }
  370. private void UpdatePseudoClasses(Orientation o)
  371. {
  372. PseudoClasses.Set(":vertical", o == Orientation.Vertical);
  373. PseudoClasses.Set(":horizontal", o == Orientation.Horizontal);
  374. }
  375. }
  376. }