PageRenderTime 53ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/Scenes/UserInterfaces/Controls/Slider.cs

#
C# | 746 lines | 401 code | 81 blank | 264 comment | 25 complexity | 96dbac347d1063a12b71217ec8b3bc47 MD5 | raw file
Possible License(s): Apache-2.0
  1. using System.IO;
  2. using Delta.Engine;
  3. using Delta.Engine.SettingsNodes;
  4. using Delta.InputSystem;
  5. using Delta.InputSystem.Devices;
  6. using Delta.Rendering.Basics.Fonts;
  7. using Delta.Scenes.Enums;
  8. using Delta.Utilities;
  9. using Delta.Utilities.Datatypes;
  10. using Delta.Utilities.Datatypes.Advanced;
  11. using Delta.Utilities.Helpers;
  12. using NUnit.Framework;
  13. namespace Delta.Scenes.UserInterfaces.Controls
  14. {
  15. /// <summary>
  16. /// This is a slider control which allows changing the value (limited by
  17. /// 'MinValue' and 'MaxValue' range) by dragging the "knob" symbol.
  18. /// Note: This control has no Rotation because we never use rotated
  19. /// sliders (would be very strange)! If you set a rotation value then it
  20. /// will be reset to zero and you get a warning.
  21. /// </summary>
  22. public class Slider : Button
  23. {
  24. #region SliderMarker Class
  25. /// <summary>
  26. /// Slider marker
  27. /// </summary>
  28. private class SliderMarker : AlignableElement
  29. {
  30. #region SliderValueInPercent (Public)
  31. /// <summary>
  32. /// The current value of the slider in the normalized range [0,1] which is
  33. /// used for representing and updating the "knob position".
  34. /// </summary>
  35. /// <remarks>
  36. /// This property will don't do any value checking because it's just used
  37. /// and thought as internal helper.
  38. /// </remarks>
  39. public float SliderValueInPercent
  40. {
  41. get;
  42. set;
  43. }
  44. #endregion
  45. #region Private
  46. #region lastSliderValueInPercent (Private)
  47. /// <summary>
  48. /// The last set value which is used to "detect" the value changes.
  49. /// </summary>
  50. private float lastSliderValueInPercent;
  51. #endregion
  52. #region owner (Private)
  53. /// <summary>
  54. /// The slider where this marker is attached to.
  55. /// </summary>
  56. private readonly Slider owner;
  57. #endregion
  58. #endregion
  59. #region Constructors
  60. /// <summary>
  61. /// Create slider knob
  62. /// </summary>
  63. public SliderMarker(Slider setOwner)
  64. {
  65. owner = setOwner;
  66. HorizontalAlignment = HorizontalAlignment.Left;
  67. VerticalAlignment = VerticalAlignment.Centered;
  68. // Forbid that this element would be saved because it's helper element
  69. // which gets created automatically every time the parent will be
  70. // created
  71. IsSavingAllowed = false;
  72. }
  73. #endregion
  74. #region Methods (Private)
  75. #region DetectChanges
  76. /// <summary>
  77. /// This method implements the checks of the changes which are should be
  78. /// detected in this element. It also cares about triggering the events
  79. /// and the event handler methods.
  80. /// </summary>
  81. protected override void DetectChanges()
  82. {
  83. base.DetectChanges();
  84. // Every time the value will change
  85. if (SliderValueInPercent != lastSliderValueInPercent)
  86. {
  87. // we have to recompute the position of the marker on the trackbar
  88. // to correctly represent the current slider value visually
  89. Margin = new Margin
  90. {
  91. Left = SliderValueInPercent * owner.TrackbarDrawArea.Width,
  92. Top = Margin.Top,
  93. Right = Margin.Right,
  94. Bottom = Margin.Bottom
  95. };
  96. lastSliderValueInPercent = SliderValueInPercent;
  97. } // if
  98. }
  99. #endregion
  100. #endregion
  101. }
  102. #endregion
  103. #region Constants
  104. /// <summary>
  105. /// The current version of the implementation of this class.
  106. /// </summary>
  107. private const int VersionNumber = 1;
  108. #endregion
  109. #region Delegates
  110. /// <summary>
  111. /// The delegate declaration for the "Slider.ValueChanged" event.
  112. /// </summary>
  113. public delegate void SliderValueChangedDelegate(Slider sender);
  114. #endregion
  115. #region MinValue (Public)
  116. /// <summary>
  117. /// The minimum value of the slider.
  118. /// </summary>
  119. public float MinValue
  120. {
  121. get;
  122. set;
  123. }
  124. #endregion
  125. #region MaxValue (Public)
  126. /// <summary>
  127. /// The maximum value of the slider.
  128. /// </summary>
  129. public float MaxValue
  130. {
  131. get;
  132. set;
  133. }
  134. #endregion
  135. #region Value (Public)
  136. /// <summary>
  137. /// The current value of the slider.
  138. /// </summary>
  139. public float Value
  140. {
  141. get;
  142. set;
  143. }
  144. #endregion
  145. #region Protected
  146. #region FallbackDesign (Protected)
  147. /// <summary>
  148. /// Defines the design which will be used if no "Design" was set
  149. /// explicitely.
  150. /// </summary>
  151. protected override ControlDesign FallbackDesign
  152. {
  153. get
  154. {
  155. return Theme.Current.SliderDesign;
  156. } // get
  157. }
  158. #endregion
  159. #region TrackbarDrawArea (Protected)
  160. /// <summary>
  161. /// The drawing area of the slider track bar.
  162. /// </summary>
  163. /// <remarks>
  164. /// This value will be computed automatically by the 'DrawData()' method.
  165. /// </remarks>
  166. protected internal Rectangle TrackbarDrawArea
  167. {
  168. get;
  169. set;
  170. }
  171. #endregion
  172. #region MarkerDrawArea (Protected)
  173. /// <summary>
  174. /// The drawing area of the slider marker.
  175. /// </summary>
  176. /// <remarks>
  177. /// There is no need to cache that value because this property is only
  178. /// called once, in the 'SliderDesign' to draw the slider marker.
  179. /// </remarks>
  180. protected internal Rectangle MarkerDrawArea
  181. {
  182. get
  183. {
  184. return marker.DrawArea;
  185. } // get
  186. }
  187. #endregion
  188. #endregion
  189. #region Private
  190. #region lastValue (Private)
  191. /// <summary>
  192. /// The last set slider value which is used to "detect" value changes of
  193. /// this slider by the user.
  194. /// </summary>
  195. private float lastValue;
  196. #endregion
  197. #region marker (Private)
  198. /// <summary>
  199. /// The marker which indicates visually the current set value.
  200. /// </summary>
  201. private readonly SliderMarker marker;
  202. #endregion
  203. #region isMarkerValueUpdateRequired (Private)
  204. /// <summary>
  205. /// Indicates if the marker value needs to be re-computed by because of the
  206. /// new slider value or is it already done by a visual click on the slider.
  207. /// </summary>
  208. private bool isMarkerValueUpdateRequired;
  209. #endregion
  210. #endregion
  211. #region Constructors
  212. /// <summary>
  213. /// Creates a slider
  214. /// </summary>
  215. public Slider()
  216. {
  217. // Init the slider marker element
  218. marker = new SliderMarker(this);
  219. Add(marker);
  220. // and the slider values
  221. MinValue = 0;
  222. MaxValue = 1;
  223. Value = 0.5f;
  224. // incl. forcing first visual update for the marker position
  225. lastValue = float.NaN;
  226. isMarkerValueUpdateRequired = true;
  227. // Last we still have to clear text which we have derived by the "Label"
  228. // control
  229. Text = null;
  230. TextContentElement.HorizontalAlignment = HorizontalAlignment.Centered;
  231. TextContentElement.VerticalAlignment = VerticalAlignment.Bottom;
  232. }
  233. #endregion
  234. #region ValueChanged (Event)
  235. /// <summary>
  236. /// Occurs every time the value of the slider is changed.
  237. /// </summary>
  238. public event SliderValueChangedDelegate ValueChanged;
  239. #endregion
  240. #region Save (Public)
  241. /// <summary>
  242. /// Saves all data which are necessary to restore the object again.
  243. /// </summary>
  244. /// <param name="dataWriter">
  245. /// The writer which contains the stream where the data should be saved
  246. /// into now.
  247. /// </param>
  248. public override void Save(BinaryWriter dataWriter)
  249. {
  250. // At first we write the data of the base class
  251. base.Save(dataWriter);
  252. // and then save the version of the current data format
  253. dataWriter.Write(VersionNumber);
  254. // before we can finally save the properties
  255. dataWriter.Write(MinValue);
  256. dataWriter.Write(MaxValue);
  257. dataWriter.Write(Value);
  258. }
  259. #endregion
  260. #region Load (Public)
  261. /// <summary>
  262. /// Loads and restores all previously saved values that belongs to this
  263. /// class only from the given data reader.
  264. /// </summary>
  265. /// <param name="dataReader">
  266. /// The reader which contains the stream with the saved data which needs to
  267. /// be loaded now.
  268. /// </param>
  269. public override void Load(BinaryReader dataReader)
  270. {
  271. // At first we need to load all data of the base class
  272. base.Load(dataReader);
  273. // and then check which version of the data need to load now
  274. int version = dataReader.ReadInt32();
  275. switch (version)
  276. {
  277. // Version 1
  278. case VersionNumber:
  279. MinValue = dataReader.ReadSingle();
  280. MaxValue = dataReader.ReadSingle();
  281. Value = dataReader.ReadSingle();
  282. break;
  283. default:
  284. Log.InvalidVersionWarning(GetType().Name, version, VersionNumber);
  285. break;
  286. } // switch
  287. }
  288. #endregion
  289. #region Methods (Private)
  290. #region OnInputEvent
  291. /// <summary>
  292. /// On input event
  293. /// </summary>
  294. /// <param name="input">Input event</param>
  295. protected override void OnInputEvent(CommandTrigger input)
  296. {
  297. switch (input.CommandName)
  298. {
  299. case Command.UIClickBegin:
  300. State = ElementState.Pressed;
  301. ComputeNewCurrentValue(input.Position);
  302. input.IsHandled = true;
  303. return;
  304. case Command.UIPositionChange:
  305. if (State == ElementState.Pressed)
  306. {
  307. ComputeNewCurrentValue(input.Position);
  308. input.IsHandled = true;
  309. } // if
  310. return;
  311. case Command.UIClick:
  312. ComputeNewCurrentValue(input.Position);
  313. if (State == ElementState.Pressed)
  314. {
  315. State = IsInControl(input.Position)
  316. ? ElementState.Hovered
  317. : ElementState.Enabled;
  318. } // if
  319. input.IsHandled = true;
  320. return;
  321. default:
  322. // Unknown input event for the UI control
  323. base.OnInputEvent(input);
  324. return;
  325. } // switch
  326. }
  327. #endregion
  328. #region OnSizeChanging
  329. /// <summary>
  330. /// On size changing
  331. /// </summary>
  332. /// <param name="oldSize">Old size</param>
  333. /// <returns>
  334. /// 'True' if the new value can be used or 'false' if the change should be
  335. /// aborted.
  336. /// </returns>
  337. protected override bool OnSizeChanging(Size oldSize)
  338. {
  339. // If the size of the Slider will change
  340. if (base.OnSizeChanging(oldSize))
  341. {
  342. // compute the new marker size which is based on the Slider height
  343. float markerHeight = Size.Height * 0.8f;
  344. marker.Size = new Size(markerHeight * 0.5f, markerHeight);
  345. // and also update the width of the text area (because the height
  346. // depends on the height of the font which gets updated by the
  347. // 'OnDesignChanging' method)
  348. TextContentElement.Size = new Size(Size.Width,
  349. TextContentElement.Size.Height);
  350. return true;
  351. } // if
  352. return false;
  353. }
  354. #endregion
  355. #region OnDesignChanging
  356. /// <summary>
  357. /// On design changed
  358. /// </summary>
  359. /// <param name="oldDesign">Old design</param>
  360. /// <returns>
  361. /// 'True' if the new value can be used or 'false' if the change should be
  362. /// aborted.
  363. /// </returns>
  364. protected override bool OnDesignChanging(ControlDesign oldDesign)
  365. {
  366. // Every time the design of the control will change
  367. if (base.OnDesignChanging(oldDesign))
  368. {
  369. // we have to check if there is now a new font with an other height
  370. // because the height on the text area of the Slider is only based on
  371. // it
  372. float fontHeight = TextFont.LineHeight;
  373. if (TextContentElement.Margin.Bottom != -fontHeight)
  374. {
  375. // If the font size has changed we need to update the margin of our
  376. // text area which is "docked below" the control by using a negative
  377. // margin value
  378. TextContentElement.Margin = new Margin
  379. {
  380. Left = TextContentElement.Margin.Left,
  381. Top = TextContentElement.Margin.Top,
  382. Right = TextContentElement.Margin.Right,
  383. Bottom = -fontHeight
  384. };
  385. TextContentElement.Size = new Size(TextContentElement.Size.Width,
  386. fontHeight);
  387. } // if
  388. return true;
  389. } // if
  390. return false;
  391. }
  392. #endregion
  393. #region OnValueChanging
  394. /// <summary>
  395. /// On value changing
  396. /// </summary>
  397. /// <param name="oldValue">Old value</param>
  398. /// <returns>
  399. /// 'True' if the new value can be used or 'false' if the change should be
  400. /// aborted.
  401. /// </returns>
  402. protected virtual bool OnValueChanging(float oldValue)
  403. {
  404. return true;
  405. }
  406. #endregion
  407. #region ComputeNewCurrentValue
  408. /// <summary>
  409. /// Compute new current value of the slider based on the (clicked) position
  410. /// on the track bar.
  411. /// Note: This is the "visual version" of setting the "Value" property.
  412. /// </summary>
  413. /// <param name="clickedPosition">Clicked screen position</param>
  414. private void ComputeNewCurrentValue(Point clickedPosition)
  415. {
  416. // Compute the normalized value of the normalized value range based on
  417. // the given click position (in Quadratic Space)
  418. // To get in the range [0,1] we just have subtract the (relative)
  419. // position on the track bar and divide it by the track bar length.
  420. // That works because the given position is in a normalized space too.
  421. // -> e.g. Slider [(0.1, 0.3), (0.6, 0.1)] and cursor position (0.4, 0.3)
  422. // => (0.4 - 0.1) = 0.3 => 0.3 / 0.6 = 0.5
  423. // => clicked at 50 % of the slider (without the offsets)
  424. float clickedPercentage =
  425. (clickedPosition.X - TrackbarDrawArea.Left) / TrackbarDrawArea.Width;
  426. // Now tell the marker that there is a new changed value and the position
  427. // of it can be re-computed
  428. // Note:
  429. // We have to clamp the range to [0,1] because the user can while
  430. // changing the value (per marker dragging) move the marker outside the
  431. // control area of the slider
  432. float newPercentValue = MathHelper.Clamp(clickedPercentage);
  433. // In the case that the value will not change we can abort here to avoid
  434. // senseless computation
  435. // Note:
  436. // We even have to cancel here because if not we would set the ignore the
  437. // 'SliderValueInPercent' computation on the next value change and in the
  438. // case that the change is not set by this method we would miss the
  439. // re-computation of this value and the visual marker wouldn't be at the
  440. // correct position only on the next time
  441. if (newPercentValue != marker.SliderValueInPercent)
  442. {
  443. marker.SliderValueInPercent = newPercentValue;
  444. // Also avoid the percentage re-computation inside the
  445. // 'DetectChanges()' method again because we have already set it here
  446. isMarkerValueUpdateRequired = false;
  447. // Finally we still need to compute the "real" new slider value so that
  448. // the 'DetectChanges()' method will notice the "value change by click"
  449. Value = MathHelper.ComputeValue(clickedPercentage, MinValue, MaxValue);
  450. } // if
  451. }
  452. #endregion
  453. #region DetectChanges
  454. /// <summary>
  455. /// This method implements the checks of the changes which are should be
  456. /// detected in this element. It also cares about triggering the events and
  457. /// the event handler methods.
  458. /// </summary>
  459. protected override void DetectChanges()
  460. {
  461. base.DetectChanges();
  462. // If some "event" has happen (like mouse clicking on the marker or just
  463. // directly assigning a new value at the property) we have to update the
  464. // current value
  465. if (Value != lastValue)
  466. {
  467. // then make sure that new value is in the correct range
  468. Value = MathHelper.Clamp(Value, MinValue, MaxValue);
  469. // If the change of new value is ok
  470. if (OnValueChanging(lastValue))
  471. {
  472. lastValue = Value;
  473. // If we have to tell the slider marker the new value in percent by
  474. // ourself because the value change wasn't triggered by a click
  475. if (isMarkerValueUpdateRequired)
  476. {
  477. // then do it right now
  478. marker.SliderValueInPercent =
  479. (Value - MinValue) / (MaxValue - MinValue);
  480. } // if
  481. else
  482. {
  483. // otherwise we don't need to compute the percent value this time
  484. // but next time we maybe have to do it, if not the
  485. // "ComputeNewCurrentValue()" method will disable this "flag" again
  486. isMarkerValueUpdateRequired = true;
  487. } // else
  488. // After setting the new value also inform all external listeners
  489. // about the change
  490. if (ValueChanged != null &&
  491. // but only if it isn't just the value initialization
  492. isRuntimeValueChange)
  493. {
  494. ValueChanged(this);
  495. }
  496. } // if
  497. else
  498. {
  499. // if the change is not ok, then just reset to old value again
  500. Value = lastValue;
  501. } // else
  502. } // if
  503. }
  504. #endregion
  505. #region UpdateDrawData
  506. /// <summary>
  507. /// Updates all data of this element which are required for the following
  508. /// draw call.
  509. /// </summary>
  510. /// <remarks>
  511. /// This method will only be called if the element is in an enabled state.
  512. /// </remarks>
  513. protected override void UpdateDrawData()
  514. {
  515. base.UpdateDrawData();
  516. // Make sure that the draw area of the trackbar is always up-to-date
  517. TrackbarDrawArea = Rectangle.FromCenter(
  518. DrawArea.Center.X, DrawArea.Center.Y,
  519. DrawArea.Width - marker.Size.Width, DrawArea.Height * 0.25f);
  520. }
  521. #endregion
  522. #region DrawDebugInfo
  523. /// <summary>
  524. /// Draw debug info
  525. /// </summary>
  526. protected override void DrawDebugInfo()
  527. {
  528. base.DrawDebugInfo();
  529. Font.Default.Draw("Value=" + Value, DrawArea.Move(0.0f, 0.031f),
  530. 0.0f,
  531. Point.Zero);
  532. }
  533. #endregion
  534. #endregion
  535. /// <summary>
  536. /// Tests for Slider controls
  537. /// </summary>
  538. [Category("Visual")]
  539. internal class SliderTests
  540. {
  541. #region DisplaySlider (Static)
  542. /// <summary>
  543. /// Display slider
  544. /// </summary>
  545. [Test]
  546. public static void DisplaySlider()
  547. {
  548. Slider testSlider = new Slider
  549. {
  550. LocalArea = Rectangle.FromCenter(0.5f, 0.5f, 0.4f, 0.05f),
  551. //Size = new Size(0.4f, 0.025f),
  552. };
  553. testSlider.Text = "Value = '" + testSlider.Value + "'";
  554. float timeSinceValueHasChanged = 0.0f;
  555. testSlider.ValueChanged += delegate
  556. {
  557. timeSinceValueHasChanged = 3.5f;
  558. testSlider.Text = "Value = '" + testSlider.Value + "'";
  559. };
  560. Assert.Equal("Slider", testSlider.GetType().Name);
  561. Screen testScene = new Screen();
  562. testScene.Add(testSlider);
  563. // Open now the scene to "activate" for the test
  564. testScene.Open();
  565. Application.Start(delegate
  566. {
  567. if (timeSinceValueHasChanged > 0.0f)
  568. {
  569. Font.DrawTopLeftInformation(
  570. "\nSlider value has changed to '" + testSlider.Value + "'");
  571. timeSinceValueHasChanged -= Time.Delta;
  572. } // if
  573. BaseKeyboard keyboard = Input.Keyboard;
  574. if (keyboard.CursorLeftReleased)
  575. {
  576. testSlider.Value -= 0.1f;
  577. } // if
  578. if (keyboard.CursorRightReleased)
  579. {
  580. testSlider.Value += 0.1f;
  581. } // if
  582. //const float lineGap = 0.02f;
  583. //pos.Y += lineGap;
  584. //infoFont.Write(pos, "TextArea='" + testLabel.TextArea + "'",
  585. // TextAlignmentType.TopLeft);
  586. //pos.Y += lineGap;
  587. });
  588. }
  589. #endregion
  590. #region DisabledSlider (Static)
  591. /// <summary>
  592. /// Disabled slider
  593. /// </summary>
  594. [Test]
  595. public static void DisabledSlider()
  596. {
  597. Slider testSlider = new Slider
  598. {
  599. LocalArea = Rectangle.FromCenter(0.5f, 0.5f, 0.4f, 0.05f),
  600. //Size = new Size(0.4f, 0.025f),
  601. Text = "Enabled",
  602. };
  603. Screen testScene = new Screen();
  604. testScene.Add(testSlider);
  605. // Open now the scene to "activate" for the test
  606. testScene.Open();
  607. Settings.Debug.SetDrawDebugInfoMode(ProfilingMode.UI, true);
  608. Application.Start(delegate
  609. {
  610. // Start the visual test to see the Slider
  611. // Disable/enable the Slider by pressing the "Space" key
  612. if (Input.Keyboard.SpaceReleased)
  613. {
  614. if (testSlider.State >= ElementState.Enabled)
  615. {
  616. testSlider.State = ElementState.Disabled;
  617. }
  618. else
  619. {
  620. testSlider.State = testSlider.IsInControl(Input.Mouse.Position)
  621. ? ElementState.Hovered
  622. : ElementState.Enabled;
  623. } // else
  624. testSlider.Text = testSlider.State.ToString();
  625. //Log.Test("The slider is now " + testSlider.State);
  626. }
  627. });
  628. Settings.Debug.SetDrawDebugInfoMode(ProfilingMode.UI, false);
  629. }
  630. #endregion
  631. #region MaterialAndSlider (Static)
  632. /// <summary>
  633. /// Display slider
  634. /// Occasionally there were rendering errors when an additive texture
  635. /// overlapped with the slider.
  636. /// </summary>
  637. [Test]
  638. public static void MaterialAndSlider()
  639. {
  640. // Slider testSlider = new Slider
  641. // {
  642. // //LocalArea = Rectangle.FromCenter(0.5f, 0.5f, 0.4f, 0.05f),
  643. // Size = new Size(0.4f, 0.025f),
  644. // };
  645. // UserScreen testScene = new UserScreen();
  646. // testScene.Add(testSlider);
  647. // // Open now the scene to "activate" for the test
  648. // testScene.Open();
  649. // Material2DColored testMaterial = new Material2DColored("blobAdditive")
  650. // {
  651. // DrawLayer = RenderLayer.Normal,
  652. // };
  653. // Application.Start(delegate
  654. // {
  655. // // Start the visual test to see the slider
  656. // testMaterial.Draw(Rectangle.FromCenter(Point.Half, 0.2f));
  657. // //testMaterial.Draw( Point.Half, 1.0f, 0.0f, Color.White,
  658. // // RenderLayer.Normal);
  659. // testMaterial.Draw(new Rectangle(0.5f, 0.5f, 0.5f, 0.5f));
  660. // });
  661. }
  662. #endregion
  663. }
  664. }
  665. }