/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
- using System.IO;
- using Delta.Engine;
- using Delta.Engine.SettingsNodes;
- using Delta.InputSystem;
- using Delta.InputSystem.Devices;
- using Delta.Rendering.Basics.Fonts;
- using Delta.Scenes.Enums;
- using Delta.Utilities;
- using Delta.Utilities.Datatypes;
- using Delta.Utilities.Datatypes.Advanced;
- using Delta.Utilities.Helpers;
- using NUnit.Framework;
-
- namespace Delta.Scenes.UserInterfaces.Controls
- {
- /// <summary>
- /// This is a slider control which allows changing the value (limited by
- /// 'MinValue' and 'MaxValue' range) by dragging the "knob" symbol.
- /// Note: This control has no Rotation because we never use rotated
- /// sliders (would be very strange)! If you set a rotation value then it
- /// will be reset to zero and you get a warning.
- /// </summary>
- public class Slider : Button
- {
- #region SliderMarker Class
- /// <summary>
- /// Slider marker
- /// </summary>
- private class SliderMarker : AlignableElement
- {
- #region SliderValueInPercent (Public)
- /// <summary>
- /// The current value of the slider in the normalized range [0,1] which is
- /// used for representing and updating the "knob position".
- /// </summary>
- /// <remarks>
- /// This property will don't do any value checking because it's just used
- /// and thought as internal helper.
- /// </remarks>
- public float SliderValueInPercent
- {
- get;
- set;
- }
- #endregion
-
- #region Private
-
- #region lastSliderValueInPercent (Private)
- /// <summary>
- /// The last set value which is used to "detect" the value changes.
- /// </summary>
- private float lastSliderValueInPercent;
- #endregion
-
- #region owner (Private)
- /// <summary>
- /// The slider where this marker is attached to.
- /// </summary>
- private readonly Slider owner;
- #endregion
-
- #endregion
-
- #region Constructors
- /// <summary>
- /// Create slider knob
- /// </summary>
- public SliderMarker(Slider setOwner)
- {
- owner = setOwner;
- HorizontalAlignment = HorizontalAlignment.Left;
- VerticalAlignment = VerticalAlignment.Centered;
- // Forbid that this element would be saved because it's helper element
- // which gets created automatically every time the parent will be
- // created
- IsSavingAllowed = false;
- }
- #endregion
-
- #region Methods (Private)
-
- #region DetectChanges
- /// <summary>
- /// This method implements the checks of the changes which are should be
- /// detected in this element. It also cares about triggering the events
- /// and the event handler methods.
- /// </summary>
- protected override void DetectChanges()
- {
- base.DetectChanges();
-
- // Every time the value will change
- if (SliderValueInPercent != lastSliderValueInPercent)
- {
- // we have to recompute the position of the marker on the trackbar
- // to correctly represent the current slider value visually
- Margin = new Margin
- {
- Left = SliderValueInPercent * owner.TrackbarDrawArea.Width,
- Top = Margin.Top,
- Right = Margin.Right,
- Bottom = Margin.Bottom
- };
- lastSliderValueInPercent = SliderValueInPercent;
- } // if
- }
- #endregion
-
- #endregion
- }
- #endregion
-
- #region Constants
- /// <summary>
- /// The current version of the implementation of this class.
- /// </summary>
- private const int VersionNumber = 1;
- #endregion
-
- #region Delegates
- /// <summary>
- /// The delegate declaration for the "Slider.ValueChanged" event.
- /// </summary>
- public delegate void SliderValueChangedDelegate(Slider sender);
- #endregion
-
- #region MinValue (Public)
- /// <summary>
- /// The minimum value of the slider.
- /// </summary>
- public float MinValue
- {
- get;
- set;
- }
- #endregion
-
- #region MaxValue (Public)
- /// <summary>
- /// The maximum value of the slider.
- /// </summary>
- public float MaxValue
- {
- get;
- set;
- }
- #endregion
-
- #region Value (Public)
- /// <summary>
- /// The current value of the slider.
- /// </summary>
- public float Value
- {
- get;
- set;
- }
- #endregion
-
- #region Protected
-
- #region FallbackDesign (Protected)
- /// <summary>
- /// Defines the design which will be used if no "Design" was set
- /// explicitely.
- /// </summary>
- protected override ControlDesign FallbackDesign
- {
- get
- {
- return Theme.Current.SliderDesign;
- } // get
- }
- #endregion
-
- #region TrackbarDrawArea (Protected)
- /// <summary>
- /// The drawing area of the slider track bar.
- /// </summary>
- /// <remarks>
- /// This value will be computed automatically by the 'DrawData()' method.
- /// </remarks>
- protected internal Rectangle TrackbarDrawArea
- {
- get;
- set;
- }
- #endregion
-
- #region MarkerDrawArea (Protected)
- /// <summary>
- /// The drawing area of the slider marker.
- /// </summary>
- /// <remarks>
- /// There is no need to cache that value because this property is only
- /// called once, in the 'SliderDesign' to draw the slider marker.
- /// </remarks>
- protected internal Rectangle MarkerDrawArea
- {
- get
- {
- return marker.DrawArea;
- } // get
- }
- #endregion
-
- #endregion
-
- #region Private
-
- #region lastValue (Private)
- /// <summary>
- /// The last set slider value which is used to "detect" value changes of
- /// this slider by the user.
- /// </summary>
- private float lastValue;
- #endregion
-
- #region marker (Private)
- /// <summary>
- /// The marker which indicates visually the current set value.
- /// </summary>
- private readonly SliderMarker marker;
- #endregion
-
- #region isMarkerValueUpdateRequired (Private)
- /// <summary>
- /// Indicates if the marker value needs to be re-computed by because of the
- /// new slider value or is it already done by a visual click on the slider.
- /// </summary>
- private bool isMarkerValueUpdateRequired;
- #endregion
-
- #endregion
-
- #region Constructors
- /// <summary>
- /// Creates a slider
- /// </summary>
- public Slider()
- {
- // Init the slider marker element
- marker = new SliderMarker(this);
- Add(marker);
-
- // and the slider values
- MinValue = 0;
- MaxValue = 1;
- Value = 0.5f;
-
- // incl. forcing first visual update for the marker position
- lastValue = float.NaN;
- isMarkerValueUpdateRequired = true;
-
- // Last we still have to clear text which we have derived by the "Label"
- // control
- Text = null;
- TextContentElement.HorizontalAlignment = HorizontalAlignment.Centered;
- TextContentElement.VerticalAlignment = VerticalAlignment.Bottom;
- }
- #endregion
-
- #region ValueChanged (Event)
- /// <summary>
- /// Occurs every time the value of the slider is changed.
- /// </summary>
- public event SliderValueChangedDelegate ValueChanged;
- #endregion
-
- #region Save (Public)
- /// <summary>
- /// Saves all data which are necessary to restore the object again.
- /// </summary>
- /// <param name="dataWriter">
- /// The writer which contains the stream where the data should be saved
- /// into now.
- /// </param>
- public override void Save(BinaryWriter dataWriter)
- {
- // At first we write the data of the base class
- base.Save(dataWriter);
-
- // and then save the version of the current data format
- dataWriter.Write(VersionNumber);
-
- // before we can finally save the properties
- dataWriter.Write(MinValue);
- dataWriter.Write(MaxValue);
- dataWriter.Write(Value);
- }
- #endregion
-
- #region Load (Public)
- /// <summary>
- /// Loads and restores all previously saved values that belongs to this
- /// class only from the given data reader.
- /// </summary>
- /// <param name="dataReader">
- /// The reader which contains the stream with the saved data which needs to
- /// be loaded now.
- /// </param>
- public override void Load(BinaryReader dataReader)
- {
- // At first we need to load all data of the base class
- base.Load(dataReader);
-
- // and then check which version of the data need to load now
- int version = dataReader.ReadInt32();
- switch (version)
- {
- // Version 1
- case VersionNumber:
- MinValue = dataReader.ReadSingle();
- MaxValue = dataReader.ReadSingle();
- Value = dataReader.ReadSingle();
- break;
-
- default:
- Log.InvalidVersionWarning(GetType().Name, version, VersionNumber);
- break;
- } // switch
- }
- #endregion
-
- #region Methods (Private)
-
- #region OnInputEvent
- /// <summary>
- /// On input event
- /// </summary>
- /// <param name="input">Input event</param>
- protected override void OnInputEvent(CommandTrigger input)
- {
- switch (input.CommandName)
- {
- case Command.UIClickBegin:
- State = ElementState.Pressed;
- ComputeNewCurrentValue(input.Position);
- input.IsHandled = true;
- return;
-
- case Command.UIPositionChange:
- if (State == ElementState.Pressed)
- {
- ComputeNewCurrentValue(input.Position);
- input.IsHandled = true;
- } // if
- return;
-
- case Command.UIClick:
- ComputeNewCurrentValue(input.Position);
- if (State == ElementState.Pressed)
- {
- State = IsInControl(input.Position)
- ? ElementState.Hovered
- : ElementState.Enabled;
- } // if
- input.IsHandled = true;
- return;
-
- default:
- // Unknown input event for the UI control
- base.OnInputEvent(input);
- return;
- } // switch
- }
- #endregion
-
- #region OnSizeChanging
- /// <summary>
- /// On size changing
- /// </summary>
- /// <param name="oldSize">Old size</param>
- /// <returns>
- /// 'True' if the new value can be used or 'false' if the change should be
- /// aborted.
- /// </returns>
- protected override bool OnSizeChanging(Size oldSize)
- {
- // If the size of the Slider will change
- if (base.OnSizeChanging(oldSize))
- {
- // compute the new marker size which is based on the Slider height
- float markerHeight = Size.Height * 0.8f;
- marker.Size = new Size(markerHeight * 0.5f, markerHeight);
-
- // and also update the width of the text area (because the height
- // depends on the height of the font which gets updated by the
- // 'OnDesignChanging' method)
- TextContentElement.Size = new Size(Size.Width,
- TextContentElement.Size.Height);
-
- return true;
- } // if
-
- return false;
- }
- #endregion
-
- #region OnDesignChanging
- /// <summary>
- /// On design changed
- /// </summary>
- /// <param name="oldDesign">Old design</param>
- /// <returns>
- /// 'True' if the new value can be used or 'false' if the change should be
- /// aborted.
- /// </returns>
- protected override bool OnDesignChanging(ControlDesign oldDesign)
- {
- // Every time the design of the control will change
- if (base.OnDesignChanging(oldDesign))
- {
- // we have to check if there is now a new font with an other height
- // because the height on the text area of the Slider is only based on
- // it
- float fontHeight = TextFont.LineHeight;
- if (TextContentElement.Margin.Bottom != -fontHeight)
- {
- // If the font size has changed we need to update the margin of our
- // text area which is "docked below" the control by using a negative
- // margin value
- TextContentElement.Margin = new Margin
- {
- Left = TextContentElement.Margin.Left,
- Top = TextContentElement.Margin.Top,
- Right = TextContentElement.Margin.Right,
- Bottom = -fontHeight
- };
-
- TextContentElement.Size = new Size(TextContentElement.Size.Width,
- fontHeight);
- } // if
-
- return true;
- } // if
-
- return false;
- }
- #endregion
-
- #region OnValueChanging
- /// <summary>
- /// On value changing
- /// </summary>
- /// <param name="oldValue">Old value</param>
- /// <returns>
- /// 'True' if the new value can be used or 'false' if the change should be
- /// aborted.
- /// </returns>
- protected virtual bool OnValueChanging(float oldValue)
- {
- return true;
- }
- #endregion
-
- #region ComputeNewCurrentValue
- /// <summary>
- /// Compute new current value of the slider based on the (clicked) position
- /// on the track bar.
- /// Note: This is the "visual version" of setting the "Value" property.
- /// </summary>
- /// <param name="clickedPosition">Clicked screen position</param>
- private void ComputeNewCurrentValue(Point clickedPosition)
- {
- // Compute the normalized value of the normalized value range based on
- // the given click position (in Quadratic Space)
-
- // To get in the range [0,1] we just have subtract the (relative)
- // position on the track bar and divide it by the track bar length.
- // That works because the given position is in a normalized space too.
- // -> e.g. Slider [(0.1, 0.3), (0.6, 0.1)] and cursor position (0.4, 0.3)
- // => (0.4 - 0.1) = 0.3 => 0.3 / 0.6 = 0.5
- // => clicked at 50 % of the slider (without the offsets)
- float clickedPercentage =
- (clickedPosition.X - TrackbarDrawArea.Left) / TrackbarDrawArea.Width;
-
- // Now tell the marker that there is a new changed value and the position
- // of it can be re-computed
- // Note:
- // We have to clamp the range to [0,1] because the user can while
- // changing the value (per marker dragging) move the marker outside the
- // control area of the slider
- float newPercentValue = MathHelper.Clamp(clickedPercentage);
- // In the case that the value will not change we can abort here to avoid
- // senseless computation
- // Note:
- // We even have to cancel here because if not we would set the ignore the
- // 'SliderValueInPercent' computation on the next value change and in the
- // case that the change is not set by this method we would miss the
- // re-computation of this value and the visual marker wouldn't be at the
- // correct position only on the next time
- if (newPercentValue != marker.SliderValueInPercent)
- {
- marker.SliderValueInPercent = newPercentValue;
-
- // Also avoid the percentage re-computation inside the
- // 'DetectChanges()' method again because we have already set it here
- isMarkerValueUpdateRequired = false;
-
- // Finally we still need to compute the "real" new slider value so that
- // the 'DetectChanges()' method will notice the "value change by click"
- Value = MathHelper.ComputeValue(clickedPercentage, MinValue, MaxValue);
- } // if
- }
- #endregion
-
- #region DetectChanges
- /// <summary>
- /// This method implements the checks of the changes which are should be
- /// detected in this element. It also cares about triggering the events and
- /// the event handler methods.
- /// </summary>
- protected override void DetectChanges()
- {
- base.DetectChanges();
-
- // If some "event" has happen (like mouse clicking on the marker or just
- // directly assigning a new value at the property) we have to update the
- // current value
- if (Value != lastValue)
- {
- // then make sure that new value is in the correct range
- Value = MathHelper.Clamp(Value, MinValue, MaxValue);
-
-
- // If the change of new value is ok
- if (OnValueChanging(lastValue))
- {
- lastValue = Value;
-
- // If we have to tell the slider marker the new value in percent by
- // ourself because the value change wasn't triggered by a click
- if (isMarkerValueUpdateRequired)
- {
- // then do it right now
- marker.SliderValueInPercent =
- (Value - MinValue) / (MaxValue - MinValue);
- } // if
- else
- {
- // otherwise we don't need to compute the percent value this time
- // but next time we maybe have to do it, if not the
- // "ComputeNewCurrentValue()" method will disable this "flag" again
- isMarkerValueUpdateRequired = true;
- } // else
-
- // After setting the new value also inform all external listeners
- // about the change
- if (ValueChanged != null &&
- // but only if it isn't just the value initialization
- isRuntimeValueChange)
- {
- ValueChanged(this);
- }
- } // if
- else
- {
- // if the change is not ok, then just reset to old value again
- Value = lastValue;
- } // else
- } // if
- }
- #endregion
-
- #region UpdateDrawData
- /// <summary>
- /// Updates all data of this element which are required for the following
- /// draw call.
- /// </summary>
- /// <remarks>
- /// This method will only be called if the element is in an enabled state.
- /// </remarks>
- protected override void UpdateDrawData()
- {
- base.UpdateDrawData();
-
- // Make sure that the draw area of the trackbar is always up-to-date
- TrackbarDrawArea = Rectangle.FromCenter(
- DrawArea.Center.X, DrawArea.Center.Y,
- DrawArea.Width - marker.Size.Width, DrawArea.Height * 0.25f);
- }
- #endregion
-
- #region DrawDebugInfo
- /// <summary>
- /// Draw debug info
- /// </summary>
- protected override void DrawDebugInfo()
- {
- base.DrawDebugInfo();
-
- Font.Default.Draw("Value=" + Value, DrawArea.Move(0.0f, 0.031f),
- 0.0f,
- Point.Zero);
- }
- #endregion
-
- #endregion
-
- /// <summary>
- /// Tests for Slider controls
- /// </summary>
- [Category("Visual")]
- internal class SliderTests
- {
- #region DisplaySlider (Static)
- /// <summary>
- /// Display slider
- /// </summary>
- [Test]
- public static void DisplaySlider()
- {
- Slider testSlider = new Slider
- {
- LocalArea = Rectangle.FromCenter(0.5f, 0.5f, 0.4f, 0.05f),
- //Size = new Size(0.4f, 0.025f),
- };
- testSlider.Text = "Value = '" + testSlider.Value + "'";
-
- float timeSinceValueHasChanged = 0.0f;
- testSlider.ValueChanged += delegate
- {
- timeSinceValueHasChanged = 3.5f;
-
- testSlider.Text = "Value = '" + testSlider.Value + "'";
- };
- Assert.Equal("Slider", testSlider.GetType().Name);
-
- Screen testScene = new Screen();
- testScene.Add(testSlider);
- // Open now the scene to "activate" for the test
- testScene.Open();
-
- Application.Start(delegate
- {
- if (timeSinceValueHasChanged > 0.0f)
- {
- Font.DrawTopLeftInformation(
- "\nSlider value has changed to '" + testSlider.Value + "'");
- timeSinceValueHasChanged -= Time.Delta;
- } // if
-
- BaseKeyboard keyboard = Input.Keyboard;
- if (keyboard.CursorLeftReleased)
- {
- testSlider.Value -= 0.1f;
- } // if
-
- if (keyboard.CursorRightReleased)
- {
- testSlider.Value += 0.1f;
- } // if
-
- //const float lineGap = 0.02f;
- //pos.Y += lineGap;
- //infoFont.Write(pos, "TextArea='" + testLabel.TextArea + "'",
- // TextAlignmentType.TopLeft);
- //pos.Y += lineGap;
- });
- }
- #endregion
-
- #region DisabledSlider (Static)
- /// <summary>
- /// Disabled slider
- /// </summary>
- [Test]
- public static void DisabledSlider()
- {
- Slider testSlider = new Slider
- {
- LocalArea = Rectangle.FromCenter(0.5f, 0.5f, 0.4f, 0.05f),
- //Size = new Size(0.4f, 0.025f),
- Text = "Enabled",
- };
-
- Screen testScene = new Screen();
- testScene.Add(testSlider);
- // Open now the scene to "activate" for the test
- testScene.Open();
-
- Settings.Debug.SetDrawDebugInfoMode(ProfilingMode.UI, true);
- Application.Start(delegate
- {
- // Start the visual test to see the Slider
- // Disable/enable the Slider by pressing the "Space" key
- if (Input.Keyboard.SpaceReleased)
- {
- if (testSlider.State >= ElementState.Enabled)
- {
- testSlider.State = ElementState.Disabled;
- }
- else
- {
- testSlider.State = testSlider.IsInControl(Input.Mouse.Position)
- ? ElementState.Hovered
- : ElementState.Enabled;
- } // else
- testSlider.Text = testSlider.State.ToString();
- //Log.Test("The slider is now " + testSlider.State);
- }
- });
- Settings.Debug.SetDrawDebugInfoMode(ProfilingMode.UI, false);
- }
- #endregion
-
- #region MaterialAndSlider (Static)
- /// <summary>
- /// Display slider
- /// Occasionally there were rendering errors when an additive texture
- /// overlapped with the slider.
- /// </summary>
- [Test]
- public static void MaterialAndSlider()
- {
- // Slider testSlider = new Slider
- // {
- // //LocalArea = Rectangle.FromCenter(0.5f, 0.5f, 0.4f, 0.05f),
- // Size = new Size(0.4f, 0.025f),
- // };
-
- // UserScreen testScene = new UserScreen();
- // testScene.Add(testSlider);
- // // Open now the scene to "activate" for the test
- // testScene.Open();
-
- // Material2DColored testMaterial = new Material2DColored("blobAdditive")
- // {
- // DrawLayer = RenderLayer.Normal,
- // };
-
- // Application.Start(delegate
- // {
- // // Start the visual test to see the slider
- // testMaterial.Draw(Rectangle.FromCenter(Point.Half, 0.2f));
- // //testMaterial.Draw( Point.Half, 1.0f, 0.0f, Color.White,
- // // RenderLayer.Normal);
- // testMaterial.Draw(new Rectangle(0.5f, 0.5f, 0.5f, 0.5f));
- // });
- }
- #endregion
- }
- }
- }