diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index 6181024230..cf07ce2431 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -114,40 +114,6 @@ namespace osu.Game.Tests.Visual.Editing }); } - [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", () => EditorClock.CurrentTimeAccurate == 54670); - - AddStep("Seek to just before next point", () => EditorClock.Seek(69000)); - AddStep("Start clock", () => EditorClock.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", () => EditorClock.CurrentTimeAccurate == 54670); - - AddStep("Seek to later", () => EditorClock.Seek(80000)); - AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670); - } - [Test] public void TestScrollControlGroupIntoView() { diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index b7367dddda..8699c388b3 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -9,9 +9,9 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Overlays; using osuTK; namespace osu.Game.Screens.Edit.Timing @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Edit.Timing private Bindable selectedGroup { get; set; } = null!; [BackgroundDependencyLoader] - private void load(OverlayColourProvider colours) + private void load(OsuColour colours) { RelativeSizeAxes = Axes.Both; @@ -44,6 +44,26 @@ namespace osu.Game.Screens.Edit.Timing Groups = { BindTarget = Beatmap.ControlPointInfo.Groups, }, }, new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding(margins), + Spacing = new Vector2(5), + Children = new Drawable[] + { + new RoundedButton + { + Text = "Select closest to current time", + Action = goToCurrentGroup, + Size = new Vector2(220, 30), + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, + } + }, + new FillFlowContainer { AutoSizeAxes = Axes.Both, Anchor = Anchor.BottomRight, @@ -60,6 +80,7 @@ namespace osu.Game.Screens.Edit.Timing Action = delete, Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, + BackgroundColour = colours.Red3, }, addButton = new RoundedButton { @@ -97,78 +118,18 @@ namespace osu.Game.Screens.Edit.Timing { base.Update(); - trackActivePoint(); - addButton.Enabled.Value = clock.CurrentTimeAccurate != selectedGroup.Value?.Time; } - private Type? trackedType; - - /// - /// 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() + private void goToCurrentGroup() { - // For simplicity only match on the first type of the active control point. - if (selectedGroup.Value == null) - trackedType = null; - else - { - switch (selectedGroup.Value.ControlPoints.Count) - { - // If the selected group has no control points, clear the tracked type. - // Otherwise the user will be unable to select a group with no control points. - case 0: - trackedType = null; - break; + double accurateTime = clock.CurrentTimeAccurate; - // If the selected group only has one control point, update the tracking type. - case 1: - trackedType = selectedGroup.Value?.ControlPoints[0].GetType(); - break; + var activeTimingPoint = Beatmap.ControlPointInfo.TimingPointAt(accurateTime); + var activeEffectPoint = Beatmap.ControlPointInfo.EffectPointAt(accurateTime); - // If the selected group has more than one control point, choose the first as the tracking type - // if we don't already have a singular tracked type. - default: - trackedType ??= selectedGroup.Value?.ControlPoints[0].GetType(); - break; - } - } - - if (trackedType != null) - { - double accurateTime = clock.CurrentTimeAccurate; - - // 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. - - // Find the next group which has the same type as the selected one. - ControlPointGroup? found = null; - - for (int i = 0; i < Beatmap.ControlPointInfo.Groups.Count; i++) - { - var g = Beatmap.ControlPointInfo.Groups[i]; - - if (g.Time > accurateTime) - continue; - - for (int j = 0; j < g.ControlPoints.Count; j++) - { - if (g.ControlPoints[j].GetType() == trackedType) - { - found = g; - break; - } - } - } - - if (found != null) - selectedGroup.Value = found; - } + double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time); + selectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(latestActiveTime); } private void delete() diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 8dc0ced30e..501d8c0e41 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; @@ -27,10 +28,27 @@ namespace osu.Game.Screens.Edit.Timing { public BindableList Groups { get; } = new BindableList(); + [Cached] + private Bindable activeTimingPoint { get; } = new Bindable(); + + [Cached] + private Bindable activeEffectPoint { get; } = new Bindable(); + + [Resolved] + private EditorBeatmap beatmap { get; set; } = null!; + + [Resolved] + private Bindable selectedGroup { get; set; } = null!; + + [Resolved] + private EditorClock editorClock { get; set; } = null!; + private const float timing_column_width = 300; private const float row_height = 25; private const float row_horizontal_padding = 20; + private ControlPointRowList list = null!; + [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) { @@ -65,7 +83,7 @@ namespace osu.Game.Screens.Edit.Timing { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Left = ControlPointTable.timing_column_width } + Margin = new MarginPadding { Left = timing_column_width } }, } }, @@ -73,7 +91,7 @@ namespace osu.Game.Screens.Edit.Timing { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Top = row_height }, - Child = new ControlPointRowList + Child = list = new ControlPointRowList { RelativeSizeAxes = Axes.Both, RowData = { BindTarget = Groups, }, @@ -82,40 +100,63 @@ namespace osu.Game.Screens.Edit.Timing }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedGroup.BindValueChanged(_ => scrollToMostRelevantRow(force: true), true); + } + + protected override void Update() + { + base.Update(); + + scrollToMostRelevantRow(force: false); + } + + private void scrollToMostRelevantRow(bool force) + { + double accurateTime = editorClock.CurrentTimeAccurate; + + activeTimingPoint.Value = beatmap.ControlPointInfo.TimingPointAt(accurateTime); + activeEffectPoint.Value = beatmap.ControlPointInfo.EffectPointAt(accurateTime); + + double latestActiveTime = Math.Max(activeTimingPoint.Value?.Time ?? double.NegativeInfinity, activeEffectPoint.Value?.Time ?? double.NegativeInfinity); + var groupToShow = selectedGroup.Value ?? beatmap.ControlPointInfo.GroupAt(latestActiveTime); + list.ScrollTo(groupToShow, force); + } + private partial class ControlPointRowList : VirtualisedListContainer { - [Resolved] - private Bindable selectedGroup { get; set; } = null!; - public ControlPointRowList() : base(row_height, 50) { } - protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer(); + protected override ScrollContainer CreateScrollContainer() => new UserTrackingScrollContainer(); - protected override void LoadComplete() + protected new UserTrackingScrollContainer Scroll => (UserTrackingScrollContainer)base.Scroll; + + public void ScrollTo(ControlPointGroup group, bool force) { - base.LoadComplete(); + if (Scroll.UserScrolling && !force) + return; - selectedGroup.BindValueChanged(val => - { - // can't use `.ScrollIntoView()` here because of the list virtualisation not giving - // child items valid coordinates from the start, so ballpark something similar - // using estimated row height. - var row = Items.FlowingChildren.SingleOrDefault(item => item.Row.Equals(val.NewValue)); + // can't use `.ScrollIntoView()` here because of the list virtualisation not giving + // child items valid coordinates from the start, so ballpark something similar + // using estimated row height. + var row = Items.FlowingChildren.SingleOrDefault(item => item.Row.Equals(group)); - if (row == null) - return; + if (row == null) + return; - float minPos = row.Y; - float maxPos = minPos + row_height; + float minPos = row.Y; + float maxPos = minPos + row_height; - if (minPos < Scroll.Current) - Scroll.ScrollTo(minPos); - else if (maxPos > Scroll.Current + Scroll.DisplayableContent) - Scroll.ScrollTo(maxPos - Scroll.DisplayableContent); - }); + if (minPos < Scroll.Current) + Scroll.ScrollTo(minPos); + else if (maxPos > Scroll.Current + Scroll.DisplayableContent) + Scroll.ScrollTo(maxPos - Scroll.DisplayableContent); } } @@ -130,13 +171,23 @@ namespace osu.Game.Screens.Edit.Timing private readonly BindableWithCurrent current = new BindableWithCurrent(); private Box background = null!; + private Box currentIndicator = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + [Resolved] + private OsuColour colours { get; set; } = null!; + [Resolved] private Bindable selectedGroup { get; set; } = null!; + [Resolved] + private Bindable activeTimingPoint { get; set; } = null!; + + [Resolved] + private Bindable activeEffectPoint { get; set; } = null!; + [Resolved] private EditorClock editorClock { get; set; } = null!; @@ -153,6 +204,12 @@ namespace osu.Game.Screens.Edit.Timing Colour = colourProvider.Background1, Alpha = 0, }, + currentIndicator = new Box + { + RelativeSizeAxes = Axes.Y, + Width = 5, + Alpha = 0, + }, new Container { RelativeSizeAxes = Axes.Both, @@ -174,7 +231,9 @@ namespace osu.Game.Screens.Edit.Timing { base.LoadComplete(); - selectedGroup.BindValueChanged(_ => updateState(), true); + selectedGroup.BindValueChanged(_ => updateState()); + activeEffectPoint.BindValueChanged(_ => updateState()); + activeTimingPoint.BindValueChanged(_ => updateState(), true); FinishTransforms(true); } @@ -213,12 +272,31 @@ namespace osu.Game.Screens.Edit.Timing { bool isSelected = selectedGroup.Value?.Equals(current.Value) == true; + bool hasCurrentTimingPoint = activeTimingPoint.Value != null && current.Value.ControlPoints.Contains(activeTimingPoint.Value); + bool hasCurrentEffectPoint = activeEffectPoint.Value != null && current.Value.ControlPoints.Contains(activeEffectPoint.Value); + if (IsHovered || isSelected) background.FadeIn(100, Easing.OutQuint); + else if (hasCurrentTimingPoint || hasCurrentEffectPoint) + background.FadeTo(0.2f, 100, Easing.OutQuint); else background.FadeOut(100, Easing.OutQuint); background.Colour = isSelected ? colourProvider.Colour3 : colourProvider.Background1; + + if (hasCurrentTimingPoint || hasCurrentEffectPoint) + { + currentIndicator.FadeIn(100, Easing.OutQuint); + + if (hasCurrentTimingPoint && hasCurrentEffectPoint) + currentIndicator.Colour = ColourInfo.GradientVertical(activeTimingPoint.Value!.GetRepresentingColour(colours), activeEffectPoint.Value!.GetRepresentingColour(colours)); + else if (hasCurrentTimingPoint) + currentIndicator.Colour = activeTimingPoint.Value!.GetRepresentingColour(colours); + else + currentIndicator.Colour = activeEffectPoint.Value!.GetRepresentingColour(colours); + } + else + currentIndicator.FadeOut(100, Easing.OutQuint); } }