// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osuTK.Input; using osuTK; using osu.Framework.Input.Events; namespace osu.Framework.Graphics.UserInterface { public abstract class SliderBar : Container, IHasCurrentValue where T : struct, IComparable, IConvertible, IEquatable { /// /// Range padding reduces the range of movement a slider bar is allowed to have /// while still receiving input in the padded region. This behavior is necessary /// for finite-sized nubs and can not be achieved (currently) by existing /// scene graph padding / margin functionality. /// public float RangePadding; /// /// Whether keyboard control should be allowed even when the bar is not hovered. /// [Obsolete("Implement this kind of behaviour separately instead.")] // Can be removed 20220107 protected virtual bool AllowKeyboardInputWhenNotHovered => false; public float UsableWidth => DrawWidth - 2 * RangePadding; /// /// A custom step value for each key press which actuates a change on this control. /// public float KeyboardStep; private readonly BindableNumber currentNumberInstantaneous; /// /// When set, value changes based on user input are only transferred to any bound on commit. /// This is useful if the UI interaction could be adversely affected by the value changing, such as the position of the on the screen. /// public bool TransferValueOnCommit; private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(); protected BindableNumber CurrentNumber => current; public Bindable Current { get => current; set { if (value == null) throw new ArgumentNullException(nameof(value)); current.Current = value; currentNumberInstantaneous.Default = current.Default; } } protected SliderBar() { currentNumberInstantaneous = new BindableNumber(); current.ValueChanged += e => currentNumberInstantaneous.Value = e.NewValue; current.MinValueChanged += v => currentNumberInstantaneous.MinValue = v; current.MaxValueChanged += v => currentNumberInstantaneous.MaxValue = v; current.PrecisionChanged += v => currentNumberInstantaneous.Precision = v; current.DisabledChanged += v => currentNumberInstantaneous.Disabled = v; currentNumberInstantaneous.ValueChanged += e => { if (!TransferValueOnCommit) current.Value = e.NewValue; }; } protected float NormalizedValue { get { if (!currentNumberInstantaneous.HasDefinedRange) { throw new InvalidOperationException($"A {nameof(SliderBar)}'s {nameof(Current)} must have user-defined {nameof(BindableNumber.MinValue)}" + $" and {nameof(BindableNumber.MaxValue)} to produce a valid {nameof(NormalizedValue)}."); } var min = Convert.ToSingle(currentNumberInstantaneous.MinValue); var max = Convert.ToSingle(currentNumberInstantaneous.MaxValue); if (max - min == 0) return 1; var val = Convert.ToSingle(currentNumberInstantaneous.Value); return (val - min) / (max - min); } } /// /// Triggered when the value has changed. Used to update the displayed value. /// /// The normalized value. protected abstract void UpdateValue(float value); protected override void LoadComplete() { base.LoadComplete(); currentNumberInstantaneous.ValueChanged += _ => Scheduler.AddOnce(updateValue); currentNumberInstantaneous.MinValueChanged += _ => Scheduler.AddOnce(updateValue); currentNumberInstantaneous.MaxValueChanged += _ => Scheduler.AddOnce(updateValue); Scheduler.AddOnce(updateValue); } private void updateValue() => UpdateValue(NormalizedValue); private bool handleClick; protected override bool OnMouseDown(MouseDownEvent e) { handleClick = true; return base.OnMouseDown(e); } protected override bool OnClick(ClickEvent e) { if (handleClick) { handleMouseInput(e); commit(); } return true; } protected override void OnDrag(DragEvent e) { handleMouseInput(e); } protected override bool OnDragStart(DragStartEvent e) { Vector2 posDiff = e.MouseDownPosition - e.MousePosition; if (Math.Abs(posDiff.X) < Math.Abs(posDiff.Y)) { handleClick = false; return false; } handleMouseInput(e); return true; } protected override void OnDragEnd(DragEndEvent e) { handleMouseInput(e); commit(); } protected override bool OnKeyDown(KeyDownEvent e) { if (currentNumberInstantaneous.Disabled) return false; #pragma warning disable 618 bool shouldHandle = IsHovered || AllowKeyboardInputWhenNotHovered; #pragma warning restore 618 if (!shouldHandle) return false; var step = KeyboardStep != 0 ? KeyboardStep : (Convert.ToSingle(currentNumberInstantaneous.MaxValue) - Convert.ToSingle(currentNumberInstantaneous.MinValue)) / 20; if (currentNumberInstantaneous.IsInteger) step = MathF.Ceiling(step); switch (e.Key) { case Key.Right: currentNumberInstantaneous.Add(step); onUserChange(currentNumberInstantaneous.Value); return true; case Key.Left: currentNumberInstantaneous.Add(-step); onUserChange(currentNumberInstantaneous.Value); return true; default: return false; } } protected override void OnKeyUp(KeyUpEvent e) { if (e.Key == Key.Left || e.Key == Key.Right) commit(); } private bool uncommittedChanges; private bool commit() { if (!uncommittedChanges) return false; current.Value = currentNumberInstantaneous.Value; uncommittedChanges = false; return true; } private void handleMouseInput(UIEvent e) { var xPosition = ToLocalSpace(e.ScreenSpaceMousePosition).X - RangePadding; if (currentNumberInstantaneous.Disabled) return; currentNumberInstantaneous.SetProportional(xPosition / UsableWidth, e.ShiftPressed ? KeyboardStep : 0); onUserChange(currentNumberInstantaneous.Value); } private void onUserChange(T value) { uncommittedChanges = true; OnUserChange(value); } /// /// Triggered when the value is changed based on end-user input to this control. /// protected virtual void OnUserChange(T value) { } } }