diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 3a0d3c476316..3ce3ad345f46 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -45,12 +45,9 @@ private void load() { angleInput = new SliderWithTextBoxInput("Angle (degrees):") { - Current = new BindableNumber - { - MinValue = -360, - MaxValue = 360, - Precision = 1 - }, + SliderPrecision = 1, + SliderMinValue = -360, + SliderMaxValue = 360, Instantaneous = true }, rotationOrigin = new EditorRadioButtonCollection diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs index 06b9623508f5..36381d177c5a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSliderWithTextBoxInput.cs @@ -127,5 +127,38 @@ public void TestInstantaneousMode() AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); } + + [Test] + public void TestMultiPrecision() + { + AddStep("set slider precision to 1", () => sliderWithTextBoxInput.SliderPrecision = 1f); + + AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox)); + AddStep("change text", () => textBox.Text = "3.4"); + AddAssert("slider not moved", () => slider.Current.Value, () => Is.Zero); + AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.Zero); + + AddStep("commit text", () => InputManager.Key(Key.Enter)); + AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3)); + AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3.4).Within(0.01)); + + AddStep("move mouse to nub", () => InputManager.MoveMouseTo(nub)); + AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left)); + AddStep("move mouse to minimum", () => InputManager.MoveMouseTo(sliderWithTextBoxInput.ScreenSpaceDrawQuad.BottomLeft)); + AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("3.4")); + AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3.4).Within(0.01)); + + AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left)); + AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5")); + AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); + + AddStep("move mouse to nub", () => InputManager.MoveMouseTo(nub)); + AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left)); + AddStep("move mouse to 55%", () => InputManager.MoveMouseTo(sliderWithTextBoxInput.ScreenSpaceDrawQuad.BottomLeft * 0.45f + sliderWithTextBoxInput.ScreenSpaceDrawQuad.TopRight * 0.55f)); + + AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left)); + AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("1")); + AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(1)); + } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs index 50d8d763e1ce..ca553f32d946 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SliderWithTextBoxInput.cs @@ -1,6 +1,7 @@ // 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 System.Numerics; using System.Globalization; using osu.Framework.Bindables; @@ -26,10 +27,83 @@ public float KeyboardStep set => slider.KeyboardStep = value; } + private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(); + public Bindable Current { - get => slider.Current; - set => slider.Current = value; + get => current; + set + { + current.Current = value; + slider.Current = value; + } + } + + private T? sliderPrecision; + + public T? SliderPrecision + { + get => sliderPrecision; + set + { + if (value.HasValue) + { + T multiple = value.Value / current.Precision; + if (!T.IsNaN(multiple) && !T.IsInfinity(multiple) && !T.IsZero(multiple % T.One)) + throw new ArgumentException(@"Precision override must be a multiple of the current precision."); + } + + sliderPrecision = value; + slider.Current = new BindableNumber + { + MinValue = sliderMinValue ?? current.MinValue, + MaxValue = sliderMaxValue ?? current.MaxValue, + Default = current.Default, + Precision = value ?? current.Precision, + }; + } + } + + private T? sliderMinValue; + + public T? SliderMinValue + { + get => sliderMinValue; + set + { + if (value.HasValue && value.Value < current.MinValue) + throw new ArgumentException(@"Minimum value override must be greater than or equal to the current minimum value."); + + sliderMinValue = value; + slider.Current = new BindableNumber + { + MinValue = value ?? current.MinValue, + MaxValue = sliderMaxValue ?? current.MaxValue, + Default = current.Default, + Precision = sliderPrecision ?? current.Precision, + }; + } + } + + private T? sliderMaxValue; + + public T? SliderMaxValue + { + get => sliderMaxValue; + set + { + if (value.HasValue && value.Value > current.MaxValue) + throw new ArgumentException(@"Maximum value override must be less than or equal to the current maximum value."); + + sliderMaxValue = value; + slider.Current = new BindableNumber + { + MinValue = sliderMinValue ?? current.MinValue, + MaxValue = value ?? current.MaxValue, + Default = current.Default, + Precision = sliderPrecision ?? current.Precision, + }; + } } private bool instantaneous; @@ -82,37 +156,38 @@ public SliderWithTextBoxInput(LocalisableString labelText) textBox.OnCommit += textCommitted; textBox.Current.BindValueChanged(textChanged); - Current.BindValueChanged(updateTextBoxFromSlider, true); + slider.Current.BindValueChanged(updateCurrentFromSlider); + Current.BindValueChanged(updateTextBoxAndSliderFromCurrent, true); } public bool TakeFocus() => GetContainingFocusManager()?.ChangeFocus(textBox) == true; public bool SelectAll() => textBox.SelectAll(); - private bool updatingFromTextBox; + private bool updatingFromCurrent; private void textChanged(ValueChangedEvent change) { if (!instantaneous) return; - tryUpdateSliderFromTextBox(); + tryUpdateCurrentFromTextBox(); } private void textCommitted(TextBox t, bool isNew) { - tryUpdateSliderFromTextBox(); + tryUpdateCurrentFromTextBox(); // If the attempted update above failed, restore text box to match the slider. Current.TriggerChange(); } - private void tryUpdateSliderFromTextBox() + private void tryUpdateCurrentFromTextBox() { - updatingFromTextBox = true; + if (updatingFromCurrent) return; try { - switch (slider.Current) + switch (Current) { case Bindable bindableInt: bindableInt.Value = int.Parse(textBox.Current.Value); @@ -123,7 +198,7 @@ private void tryUpdateSliderFromTextBox() break; default: - slider.Current.Parse(textBox.Current.Value, CultureInfo.CurrentCulture); + Current.Parse(textBox.Current.Value, CultureInfo.CurrentCulture); break; } } @@ -132,16 +207,25 @@ private void tryUpdateSliderFromTextBox() // ignore parsing failures. // sane state will eventually be restored by a commit (either explicit, or implicit via focus loss). } + } - updatingFromTextBox = false; + private void updateCurrentFromSlider(ValueChangedEvent _) + { + if (updatingFromCurrent) return; + + Current.Value = slider.Current.Value; } - private void updateTextBoxFromSlider(ValueChangedEvent _) + private void updateTextBoxAndSliderFromCurrent(ValueChangedEvent _) { - if (updatingFromTextBox) return; + updatingFromCurrent = true; - decimal decimalValue = decimal.CreateTruncating(slider.Current.Value); + slider.Current.Value = Current.Value; + + decimal decimalValue = decimal.CreateTruncating(Current.Value); textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}"); + + updatingFromCurrent = false; } } }