diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index 17b8189fc7..3a709cb66d 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -1,14 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Timing; +using osu.Game.Screens.Edit.Timing.RowAttributes; +using osuTK.Input; namespace osu.Game.Tests.Visual.Editing { @@ -22,6 +26,8 @@ namespace osu.Game.Tests.Visual.Editing [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + private TimingScreen timingScreen; + protected override bool ScrollUsingMouseWheel => false; public TestSceneTimingScreen() @@ -36,12 +42,52 @@ namespace osu.Game.Tests.Visual.Editing Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); Beatmap.Disabled = true; - Child = new TimingScreen + Child = timingScreen = new TimingScreen { State = { Value = Visibility.Visible }, }; } + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Stop clock", () => Clock.Stop()); + } + + [Test] + public void TestTrackingCurrentTimeWhileRunning() + { + AddStep("Select first effect point", () => + { + InputManager.MoveMouseTo(Child.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670); + AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670); + + AddStep("Seek to just before next point", () => Clock.Seek(69000)); + AddStep("Start clock", () => Clock.Start()); + + AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670); + } + + [Test] + public void TestTrackingCurrentTimeWhilePaused() + { + AddStep("Select first effect point", () => + { + InputManager.MoveMouseTo(Child.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670); + AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670); + + AddStep("Seek to later", () => Clock.Seek(80000)); + AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670); + } + protected override void Dispose(bool isDisposing) { Beatmap.Disabled = false; diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index f71a8d7d22..0cf4fca58a 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.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.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -19,7 +20,7 @@ namespace osu.Game.Screens.Edit.Timing public class TimingScreen : EditorScreenWithTimeline { [Cached] - private Bindable selectedGroup = new Bindable(); + public readonly Bindable SelectedGroup = new Bindable(); public TimingScreen() : base(EditorScreenMode.Timing) @@ -132,6 +133,61 @@ namespace osu.Game.Screens.Edit.Timing }, true); } + protected override void Update() + { + base.Update(); + + trackActivePoint(); + } + + /// + /// Given the user has selected a control point group, we want to track any group which is + /// active at the current point in time which matches the type the user has selected. + /// + /// So if the user is currently looking at a timing point and seeks into the future, a + /// future timing point would be automatically selected if it is now the new "current" point. + /// + private void trackActivePoint() + { + // For simplicity only match on the first type of the active control point. + var selectedPointType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType(); + + if (selectedPointType != null) + { + // We don't have an efficient way of looking up groups currently, only individual point types. + // To improve the efficiency of this in the future, we should reconsider the overall structure of ControlPointInfo. + IEnumerable groups = Beatmap.ControlPointInfo.Groups; + + bool currentTimeBeforeSelectedGroup = clock.CurrentTimeAccurate < selectedGroup.Value.Time; + + // Decide whether we are searching backwards or forwards. + if (currentTimeBeforeSelectedGroup) + groups = groups.Reverse(); + + // Find the next group which has the same type as the selected one. + groups = groups.SkipWhile(g => g != selectedGroup.Value) + .Skip(1) + .Where(g => g.ControlPoints.Any(cp => cp.GetType() == selectedPointType)); + + ControlPointGroup newGroup = groups.FirstOrDefault(); + + if (newGroup != null) + { + if (currentTimeBeforeSelectedGroup) + { + // When seeking backwards, the first match from the LINQ query is always what we want. + selectedGroup.Value = newGroup; + } + else + { + // When seeking forwards, we also need to check that the next match is before the current time. + if (newGroup.Time <= clock.CurrentTimeAccurate) + selectedGroup.Value = newGroup; + } + } + } + } + private void delete() { if (selectedGroup.Value == null)