PageRenderTime 128ms CodeModel.GetById 40ms app.highlight 42ms RepoModel.GetById 39ms app.codeStats 0ms

/Scenes/UserInterfaces/Controls/Slider.cs

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