1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 16:02:55 +08:00

Merge pull request #29532 from bdach/redesign-timing-table-tracking

Redesign timing table tracking
This commit is contained in:
Bartłomiej Dach 2024-08-22 08:21:39 +02:00 committed by GitHub
commit d33f4837fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 131 additions and 126 deletions

View File

@ -114,40 +114,6 @@ namespace osu.Game.Tests.Visual.Editing
}); });
} }
[Test]
public void TestTrackingCurrentTimeWhileRunning()
{
AddStep("Select first effect point", () =>
{
InputManager.MoveMouseTo(Child.ChildrenOfType<EffectRowAttribute>().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<EffectRowAttribute>().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] [Test]
public void TestScrollControlGroupIntoView() public void TestScrollControlGroupIntoView()
{ {

View File

@ -9,9 +9,9 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osuTK; using osuTK;
namespace osu.Game.Screens.Edit.Timing namespace osu.Game.Screens.Edit.Timing
@ -31,7 +31,7 @@ namespace osu.Game.Screens.Edit.Timing
private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!; private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colours) private void load(OsuColour colours)
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -44,6 +44,26 @@ namespace osu.Game.Screens.Edit.Timing
Groups = { BindTarget = Beatmap.ControlPointInfo.Groups, }, Groups = { BindTarget = Beatmap.ControlPointInfo.Groups, },
}, },
new FillFlowContainer 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, AutoSizeAxes = Axes.Both,
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
@ -60,6 +80,7 @@ namespace osu.Game.Screens.Edit.Timing
Action = delete, Action = delete,
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
BackgroundColour = colours.Red3,
}, },
addButton = new RoundedButton addButton = new RoundedButton
{ {
@ -97,78 +118,18 @@ namespace osu.Game.Screens.Edit.Timing
{ {
base.Update(); base.Update();
trackActivePoint();
addButton.Enabled.Value = clock.CurrentTimeAccurate != selectedGroup.Value?.Time; addButton.Enabled.Value = clock.CurrentTimeAccurate != selectedGroup.Value?.Time;
} }
private Type? trackedType; private void goToCurrentGroup()
/// <summary>
/// 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.
/// </summary>
private void trackActivePoint()
{ {
// For simplicity only match on the first type of the active control point. double accurateTime = clock.CurrentTimeAccurate;
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;
// If the selected group only has one control point, update the tracking type. var activeTimingPoint = Beatmap.ControlPointInfo.TimingPointAt(accurateTime);
case 1: var activeEffectPoint = Beatmap.ControlPointInfo.EffectPointAt(accurateTime);
trackedType = selectedGroup.Value?.ControlPoints[0].GetType();
break;
// If the selected group has more than one control point, choose the first as the tracking type double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time);
// if we don't already have a singular tracked type. selectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(latestActiveTime);
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;
}
} }
private void delete() private void delete()

View File

@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -27,10 +28,27 @@ namespace osu.Game.Screens.Edit.Timing
{ {
public BindableList<ControlPointGroup> Groups { get; } = new BindableList<ControlPointGroup>(); public BindableList<ControlPointGroup> Groups { get; } = new BindableList<ControlPointGroup>();
[Cached]
private Bindable<TimingControlPoint?> activeTimingPoint { get; } = new Bindable<TimingControlPoint?>();
[Cached]
private Bindable<EffectControlPoint?> activeEffectPoint { get; } = new Bindable<EffectControlPoint?>();
[Resolved]
private EditorBeatmap beatmap { get; set; } = null!;
[Resolved]
private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!;
[Resolved]
private EditorClock editorClock { get; set; } = null!;
private const float timing_column_width = 300; private const float timing_column_width = 300;
private const float row_height = 25; private const float row_height = 25;
private const float row_horizontal_padding = 20; private const float row_horizontal_padding = 20;
private ControlPointRowList list = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colours) private void load(OverlayColourProvider colours)
{ {
@ -65,7 +83,7 @@ namespace osu.Game.Screens.Edit.Timing
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = 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, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = row_height }, Padding = new MarginPadding { Top = row_height },
Child = new ControlPointRowList Child = list = new ControlPointRowList
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
RowData = { BindTarget = Groups, }, 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<ControlPointGroup, DrawableControlGroup> private partial class ControlPointRowList : VirtualisedListContainer<ControlPointGroup, DrawableControlGroup>
{ {
[Resolved]
private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!;
public ControlPointRowList() public ControlPointRowList()
: base(row_height, 50) : base(row_height, 50)
{ {
} }
protected override ScrollContainer<Drawable> CreateScrollContainer() => new OsuScrollContainer(); protected override ScrollContainer<Drawable> 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
// can't use `.ScrollIntoView()` here because of the list virtualisation not giving // using estimated row height.
// child items valid coordinates from the start, so ballpark something similar var row = Items.FlowingChildren.SingleOrDefault(item => item.Row.Equals(group));
// using estimated row height.
var row = Items.FlowingChildren.SingleOrDefault(item => item.Row.Equals(val.NewValue));
if (row == null) if (row == null)
return; return;
float minPos = row.Y; float minPos = row.Y;
float maxPos = minPos + row_height; float maxPos = minPos + row_height;
if (minPos < Scroll.Current) if (minPos < Scroll.Current)
Scroll.ScrollTo(minPos); Scroll.ScrollTo(minPos);
else if (maxPos > Scroll.Current + Scroll.DisplayableContent) else if (maxPos > Scroll.Current + Scroll.DisplayableContent)
Scroll.ScrollTo(maxPos - Scroll.DisplayableContent); Scroll.ScrollTo(maxPos - Scroll.DisplayableContent);
});
} }
} }
@ -130,13 +171,23 @@ namespace osu.Game.Screens.Edit.Timing
private readonly BindableWithCurrent<ControlPointGroup> current = new BindableWithCurrent<ControlPointGroup>(); private readonly BindableWithCurrent<ControlPointGroup> current = new BindableWithCurrent<ControlPointGroup>();
private Box background = null!; private Box background = null!;
private Box currentIndicator = null!;
[Resolved] [Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!; private OverlayColourProvider colourProvider { get; set; } = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved] [Resolved]
private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!; private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!;
[Resolved]
private Bindable<TimingControlPoint?> activeTimingPoint { get; set; } = null!;
[Resolved]
private Bindable<EffectControlPoint?> activeEffectPoint { get; set; } = null!;
[Resolved] [Resolved]
private EditorClock editorClock { get; set; } = null!; private EditorClock editorClock { get; set; } = null!;
@ -153,6 +204,12 @@ namespace osu.Game.Screens.Edit.Timing
Colour = colourProvider.Background1, Colour = colourProvider.Background1,
Alpha = 0, Alpha = 0,
}, },
currentIndicator = new Box
{
RelativeSizeAxes = Axes.Y,
Width = 5,
Alpha = 0,
},
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -174,7 +231,9 @@ namespace osu.Game.Screens.Edit.Timing
{ {
base.LoadComplete(); base.LoadComplete();
selectedGroup.BindValueChanged(_ => updateState(), true); selectedGroup.BindValueChanged(_ => updateState());
activeEffectPoint.BindValueChanged(_ => updateState());
activeTimingPoint.BindValueChanged(_ => updateState(), true);
FinishTransforms(true); FinishTransforms(true);
} }
@ -213,12 +272,31 @@ namespace osu.Game.Screens.Edit.Timing
{ {
bool isSelected = selectedGroup.Value?.Equals(current.Value) == true; 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) if (IsHovered || isSelected)
background.FadeIn(100, Easing.OutQuint); background.FadeIn(100, Easing.OutQuint);
else if (hasCurrentTimingPoint || hasCurrentEffectPoint)
background.FadeTo(0.2f, 100, Easing.OutQuint);
else else
background.FadeOut(100, Easing.OutQuint); background.FadeOut(100, Easing.OutQuint);
background.Colour = isSelected ? colourProvider.Colour3 : colourProvider.Background1; 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);
} }
} }