diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs index ed7bb9e301..131bfde86e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs @@ -20,8 +20,8 @@ namespace osu.Game.Tests.Visual.Editing private BeatDivisorControl beatDivisorControl; private BindableBeatDivisor bindableBeatDivisor; - private SliderBar tickSliderBar; - private EquilateralTriangle tickMarkerHead; + private SliderBar tickSliderBar => beatDivisorControl.ChildrenOfType>().Single(); + private EquilateralTriangle tickMarkerHead => tickSliderBar.ChildrenOfType().Single(); [SetUp] public void SetUp() => Schedule(() => @@ -32,9 +32,6 @@ namespace osu.Game.Tests.Visual.Editing Origin = Anchor.Centre, Size = new Vector2(90, 90) }; - - tickSliderBar = beatDivisorControl.ChildrenOfType>().Single(); - tickMarkerHead = tickSliderBar.ChildrenOfType().Single(); }); [Test] diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index 246d1f8af5..af03d639be 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -164,7 +164,7 @@ namespace osu.Game.Beatmaps.ControlPoints int closestDivisor = 0; double closestTime = double.MaxValue; - foreach (int divisor in BindableBeatDivisor.VALID_DIVISORS) + foreach (int divisor in BindableBeatDivisor.PREDEFINED_DIVISORS) { double distanceFromSnap = Math.Abs(time - getClosestSnappedTime(timingPoint, time, divisor)); diff --git a/osu.Game/Screens/Edit/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/BindableBeatDivisor.cs index 1a350d7261..9077898ec8 100644 --- a/osu.Game/Screens/Edit/BindableBeatDivisor.cs +++ b/osu.Game/Screens/Edit/BindableBeatDivisor.cs @@ -1,46 +1,63 @@ // 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.Linq; using osu.Framework.Bindables; using osu.Game.Graphics; +using osu.Game.Screens.Edit.Compose.Components; using osuTK.Graphics; namespace osu.Game.Screens.Edit { public class BindableBeatDivisor : BindableInt { - public static readonly int[] VALID_DIVISORS = { 1, 2, 3, 4, 6, 8, 12, 16 }; + public static readonly int[] PREDEFINED_DIVISORS = { 1, 2, 3, 4, 6, 8, 12, 16 }; + + public Bindable ValidDivisors { get; } = new Bindable(BeatDivisorPresetCollection.COMMON); public BindableBeatDivisor(int value = 1) : base(value) { + ValidDivisors.BindValueChanged(_ => updateBindableProperties(), true); + BindValueChanged(_ => ensureValidDivisor()); } - public void Next() => Value = VALID_DIVISORS[Math.Min(VALID_DIVISORS.Length - 1, Array.IndexOf(VALID_DIVISORS, Value) + 1)]; - - public void Previous() => Value = VALID_DIVISORS[Math.Max(0, Array.IndexOf(VALID_DIVISORS, Value) - 1)]; - - public override int Value + private void updateBindableProperties() { - get => base.Value; - set - { - if (!VALID_DIVISORS.Contains(value)) - { - // If it doesn't match, value will be 0, but will be clamped to the valid range via DefaultMinValue - value = Array.FindLast(VALID_DIVISORS, d => d < value); - } + ensureValidDivisor(); - base.Value = value; - } + MinValue = ValidDivisors.Value.Presets.Min(); + MaxValue = ValidDivisors.Value.Presets.Max(); + } + + private void ensureValidDivisor() + { + if (!ValidDivisors.Value.Presets.Contains(Value)) + Value = 1; + } + + public void Next() + { + var presets = ValidDivisors.Value.Presets; + Value = presets.Cast().SkipWhile(preset => preset != Value).ElementAtOrDefault(1) ?? presets[0]; + } + + public void Previous() + { + var presets = ValidDivisors.Value.Presets; + Value = presets.Cast().TakeWhile(preset => preset != Value).LastOrDefault() ?? presets[^1]; } - protected override int DefaultMinValue => VALID_DIVISORS.First(); - protected override int DefaultMaxValue => VALID_DIVISORS.Last(); protected override int DefaultPrecision => 1; + public override void BindTo(Bindable them) + { + base.BindTo(them); + + if (them is BindableBeatDivisor otherBeatDivisor) + ValidDivisors.BindTo(otherBeatDivisor.ValidDivisors); + } + protected override Bindable CreateInstance() => new BindableBeatDivisor(); /// @@ -92,7 +109,7 @@ namespace osu.Game.Screens.Edit { int beat = index % beatDivisor; - foreach (int divisor in BindableBeatDivisor.VALID_DIVISORS) + foreach (int divisor in PREDEFINED_DIVISORS) { if ((beat * divisor) % beatDivisor == 0) return divisor; diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index f1779c0d18..a9b1213738 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -27,7 +27,6 @@ namespace osu.Game.Screens.Edit.Compose.Components public class BeatDivisorControl : CompositeDrawable { private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); - private readonly Bindable divisorType = new Bindable(); public BeatDivisorControl(BindableBeatDivisor beatDivisor) { @@ -66,7 +65,7 @@ namespace osu.Game.Screens.Edit.Compose.Components RelativeSizeAxes = Axes.Both, Colour = Color4.Black }, - new TickSliderBar(beatDivisor, BindableBeatDivisor.VALID_DIVISORS) + new TickSliderBar(beatDivisor) { RelativeSizeAxes = Axes.Both, } @@ -156,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components Icon = FontAwesome.Solid.ChevronLeft, Action = () => cycleDivisorType(-1) }, - new DivisorTypeText { BeatDivisorType = { BindTarget = divisorType } }, + new DivisorTypeText { BeatDivisor = { BindTarget = beatDivisor } }, new ChevronButton { Icon = FontAwesome.Solid.ChevronRight, @@ -189,7 +188,26 @@ namespace osu.Game.Screens.Edit.Compose.Components private void cycleDivisorType(int direction) { Debug.Assert(Math.Abs(direction) == 1); - divisorType.Value = (BeatDivisorType)(((int)divisorType.Value + direction) % (int)(BeatDivisorType.Last + 1)); + int nextDivisorType = (int)beatDivisor.ValidDivisors.Value.Type + direction; + if (nextDivisorType > (int)BeatDivisorType.Last) + nextDivisorType = (int)BeatDivisorType.First; + else if (nextDivisorType < (int)BeatDivisorType.First) + nextDivisorType = (int)BeatDivisorType.Last; + + switch ((BeatDivisorType)nextDivisorType) + { + case BeatDivisorType.Common: + beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.COMMON; + break; + + case BeatDivisorType.Triplets: + beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.TRIPLETS; + break; + + case BeatDivisorType.Custom: + beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.Custom(18); // todo + break; + } } private class DivisorText : SpriteText @@ -217,7 +235,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private class DivisorTypeText : OsuSpriteText { - public Bindable BeatDivisorType { get; } = new Bindable(); + public BindableBeatDivisor BeatDivisor { get; } = new BindableBeatDivisor(); public DivisorTypeText() { @@ -230,7 +248,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void LoadComplete() { base.LoadComplete(); - BeatDivisorType.BindValueChanged(val => Text = val.NewValue.Humanize(LetterCasing.LowerCase), true); + BeatDivisor.ValidDivisors.BindValueChanged(val => Text = val.NewValue.Type.Humanize(LetterCasing.LowerCase), true); } } @@ -265,20 +283,27 @@ namespace osu.Game.Screens.Edit.Compose.Components private OsuColour colours { get; set; } private readonly BindableBeatDivisor beatDivisor; - private readonly int[] availableDivisors; - public TickSliderBar(BindableBeatDivisor beatDivisor, params int[] divisors) + public TickSliderBar(BindableBeatDivisor beatDivisor) { CurrentNumber.BindTo(this.beatDivisor = beatDivisor); - availableDivisors = divisors; Padding = new MarginPadding { Horizontal = 5 }; } - [BackgroundDependencyLoader] - private void load() + protected override void LoadComplete() { - foreach (int t in availableDivisors) + base.LoadComplete(); + + beatDivisor.ValidDivisors.BindValueChanged(_ => updateDivisors(), true); + } + + private void updateDivisors() + { + ClearInternal(); + CurrentNumber.ValueChanged -= moveMarker; + + foreach (int t in beatDivisor.ValidDivisors.Value.Presets) { AddInternal(new Tick { @@ -291,17 +316,14 @@ namespace osu.Game.Screens.Edit.Compose.Components } AddInternal(marker = new Marker()); + CurrentNumber.ValueChanged += moveMarker; + CurrentNumber.TriggerChange(); } - protected override void LoadComplete() + private void moveMarker(ValueChangedEvent divisor) { - base.LoadComplete(); - - CurrentNumber.BindValueChanged(div => - { - marker.MoveToX(getMappedPosition(div.NewValue), 100, Easing.OutQuint); - marker.Flash(); - }, true); + marker.MoveToX(getMappedPosition(divisor.NewValue), 100, Easing.OutQuint); + marker.Flash(); } protected override void UpdateValue(float value) @@ -362,11 +384,11 @@ namespace osu.Game.Screens.Edit.Compose.Components // copied from SliderBar so we can do custom spacing logic. float xPosition = (ToLocalSpace(screenSpaceMousePosition).X - RangePadding) / UsableWidth; - CurrentNumber.Value = availableDivisors.OrderBy(d => Math.Abs(getMappedPosition(d) - xPosition)).First(); + CurrentNumber.Value = beatDivisor.ValidDivisors.Value.Presets.OrderBy(d => Math.Abs(getMappedPosition(d) - xPosition)).First(); OnUserChange(Current.Value); } - private float getMappedPosition(float divisor) => MathF.Pow((divisor - 1) / (availableDivisors.Last() - 1), 0.90f); + private float getMappedPosition(float divisor) => MathF.Pow((divisor - 1) / (beatDivisor.ValidDivisors.Value.Presets.Last() - 1), 0.90f); private class Tick : CompositeDrawable { diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorPresetCollection.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorPresetCollection.cs new file mode 100644 index 0000000000..4616669c6d --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorPresetCollection.cs @@ -0,0 +1,41 @@ +// 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.Collections.Generic; +using System.Linq; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public class BeatDivisorPresetCollection + { + public BeatDivisorType Type { get; } + public IReadOnlyList Presets { get; } + + private BeatDivisorPresetCollection(BeatDivisorType type, IEnumerable presets) + { + Type = type; + Presets = presets.ToArray(); + } + + public static readonly BeatDivisorPresetCollection COMMON = new BeatDivisorPresetCollection(BeatDivisorType.Common, new[] { 1, 2, 4, 8, 16 }); + + public static readonly BeatDivisorPresetCollection TRIPLETS = new BeatDivisorPresetCollection(BeatDivisorType.Triplets, new[] { 1, 3, 6, 12 }); + + public static BeatDivisorPresetCollection Custom(int maxDivisor) + { + var presets = new List(); + + for (int candidate = 1; candidate <= Math.Sqrt(maxDivisor); ++candidate) + { + if (maxDivisor % candidate != 0) + continue; + + presets.Add(candidate); + presets.Add(maxDivisor / candidate); + } + + return new BeatDivisorPresetCollection(BeatDivisorType.Custom, presets.Distinct().OrderBy(d => d)); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorType.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorType.cs index 2a7774118e..15a8c504c5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorType.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorType.cs @@ -20,6 +20,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Custom, + First = Common, Last = Custom } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index cc4041394d..3a32dc18e5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private OsuColour colours { get; set; } - private static readonly int highest_divisor = BindableBeatDivisor.VALID_DIVISORS.Last(); + private static readonly int highest_divisor = BindableBeatDivisor.PREDEFINED_DIVISORS.Last(); public TimelineTickDisplay() {