Skip to content

Commit

Permalink
Merge pull request #28737 from OliBomby/doubleclick
Browse files Browse the repository at this point in the history
Add more ways to seek to sample points
  • Loading branch information
peppy authored Aug 30, 2024
2 parents 60c1e0f + 3a1afda commit a71bc3a
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
Expand Down Expand Up @@ -307,6 +308,46 @@ public void TestNodeSamplePopover()
hitObjectNodeHasSampleVolume(0, 1, 10);
}

[Test]
public void TestSamplePointSeek()
{
AddStep("add slider", () =>
{
EditorBeatmap.Clear();
EditorBeatmap.Add(new Slider
{
Position = new Vector2(256, 256),
StartTime = 0,
Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }),
Samples =
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
},
NodeSamples =
{
new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) },
new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) },
},
RepeatCount = 1
});
});

seekSamplePiece(-1);
editorTimeIs(0);
samplePopoverIsOpen();
seekSamplePiece(-1);
editorTimeIs(0);
samplePopoverIsOpen();
seekSamplePiece(1);
editorTimeIs(406);
seekSamplePiece(1);
editorTimeIs(813);
seekSamplePiece(1);
editorTimeIs(1627);
seekSamplePiece(1);
editorTimeIs(1627);
}

[Test]
public void TestHotkeysMultipleSelectionWithSameSampleBank()
{
Expand Down Expand Up @@ -626,7 +667,7 @@ public void TestSelectingObjectDoesNotMutateSamples()

private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () =>
{
var samplePiece = this.ChildrenOfType<SamplePointPiece>().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
var samplePiece = this.ChildrenOfType<SamplePointPiece>().Single(piece => piece is not NodeSamplePointPiece && piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
InputManager.MoveMouseTo(samplePiece);
InputManager.Click(MouseButton.Left);
Expand All @@ -640,6 +681,21 @@ private void clickNodeSamplePiece(int objectIndex, int nodeIndex) => AddStep($"c
InputManager.Click(MouseButton.Left);
});

private void seekSamplePiece(int direction) => AddStep($"seek sample piece {direction}", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(direction < 1 ? Key.Left : Key.Right);
InputManager.ReleaseKey(Key.ShiftLeft);
InputManager.ReleaseKey(Key.ControlLeft);
});

private void samplePopoverIsOpen() => AddUntilStep("sample popover is open", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault(o => o.IsPresent);
return popover != null;
});

private void samplePopoverHasNoFocus() => AddUntilStep("sample popover textbox not focused", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
Expand Down Expand Up @@ -784,5 +840,7 @@ private void hitObjectNodeHasSampleAdditionBank(int objectIndex, int nodeIndex,
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
});

private void editorTimeIs(double time) => AddAssert($"editor time is {time}", () => Precision.AlmostEquals(EditorClock.CurrentTimeAccurate, time, 1));
}
}
16 changes: 16 additions & 0 deletions osu.Game/Input/Bindings/GlobalActionContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ public static IEnumerable<GlobalAction> GetGlobalActionsFor(GlobalActionCategory
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor),
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl),
new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject),
new KeyBinding(new[] { InputKey.Control, InputKey.Right }, GlobalAction.EditorSeekToNextHitObject),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.Left }, GlobalAction.EditorSeekToPreviousSamplePoint),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.Right }, GlobalAction.EditorSeekToNextSamplePoint),
};

private static IEnumerable<KeyBinding> editorTestPlayKeyBindings => new[]
Expand Down Expand Up @@ -456,6 +460,18 @@ public enum GlobalAction

[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayQuickExitToCurrentTime))]
EditorTestPlayQuickExitToCurrentTime,

[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToPreviousHitObject))]
EditorSeekToPreviousHitObject,

[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToNextHitObject))]
EditorSeekToNextHitObject,

[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToPreviousSamplePoint))]
EditorSeekToPreviousSamplePoint,

[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToNextSamplePoint))]
EditorSeekToNextSamplePoint,
}

public enum GlobalActionCategory
Expand Down
20 changes: 20 additions & 0 deletions osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,26 @@ public static class GlobalActionKeyBindingStrings
/// </summary>
public static LocalisableString DecreaseModSpeed => new TranslatableString(getKey(@"decrease_mod_speed"), @"Decrease mod speed");

/// <summary>
/// "Seek to previous hit object"
/// </summary>
public static LocalisableString EditorSeekToPreviousHitObject => new TranslatableString(getKey(@"editor_seek_to_previous_hit_object"), @"Seek to previous hit object");

/// <summary>
/// "Seek to next hit object"
/// </summary>
public static LocalisableString EditorSeekToNextHitObject => new TranslatableString(getKey(@"editor_seek_to_next_hit_object"), @"Seek to next hit object");

/// <summary>
/// "Seek to previous sample point"
/// </summary>
public static LocalisableString EditorSeekToPreviousSamplePoint => new TranslatableString(getKey(@"editor_seek_to_previous_sample_point"), @"Seek to previous sample point");

/// <summary>
/// "Seek to next sample point"
/// </summary>
public static LocalisableString EditorSeekToNextSamplePoint => new TranslatableString(getKey(@"editor_seek_to_next_sample_point"), @"Seek to next sample point");

private static string getKey(string key) => $@"{prefix}:{key}";
}
}
8 changes: 4 additions & 4 deletions osu.Game/Overlays/Volume/VolumeControlReceptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
case GlobalAction.DecreaseVolume:
case GlobalAction.IncreaseVolume:
ActionRequested?.Invoke(e.Action);
return true;
return ActionRequested?.Invoke(e.Action) == true;

case GlobalAction.ToggleMute:
case GlobalAction.NextVolumeMeter:
case GlobalAction.PreviousVolumeMeter:
if (!e.Repeat)
ActionRequested?.Invoke(e.Action);
return true;
return ActionRequested?.Invoke(e.Action) == true;

return false;
}

return false;
Expand Down
12 changes: 8 additions & 4 deletions osu.Game/Overlays/VolumeOverlay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,18 @@ public bool Adjust(GlobalAction action, float amount = 1, bool isPrecise = false
return true;

case GlobalAction.NextVolumeMeter:
if (State.Value == Visibility.Visible)
volumeMeters.SelectNext();
if (State.Value != Visibility.Visible)
return false;

volumeMeters.SelectNext();
Show();
return true;

case GlobalAction.PreviousVolumeMeter:
if (State.Value == Visibility.Visible)
volumeMeters.SelectPrevious();
if (State.Value != Visibility.Visible)
return false;

volumeMeters.SelectPrevious();
Show();
return true;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ public NodeSamplePointPiece(HitObject hitObject, int nodeIndex)
NodeIndex = nodeIndex;
}

protected override double GetTime()
{
var hasRepeats = (IHasRepeats)HitObject;
return HitObject.StartTime + hasRepeats.Duration * NodeIndex / hasRepeats.SpanCount();
}

protected override IList<HitSampleInfo> GetSamples()
{
var hasRepeats = (IHasRepeats)HitObject;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
Expand All @@ -33,6 +34,12 @@ public partial class SamplePointPiece : HitObjectPointPiece, IHasPopover
{
public readonly HitObject HitObject;

[Resolved]
private EditorClock? editorClock { get; set; }

[Resolved]
private Editor? editor { get; set; }

public SamplePointPiece(HitObject hitObject)
{
HitObject = hitObject;
Expand All @@ -43,11 +50,32 @@ public SamplePointPiece(HitObject hitObject)

protected override Color4 GetRepresentingColour(OsuColour colours) => AlternativeColor ? colours.Pink2 : colours.Pink1;

protected virtual double GetTime() => HitObject is IHasRepeats r ? HitObject.StartTime + r.Duration / r.SpanCount() / 2 : HitObject.StartTime;

[BackgroundDependencyLoader]
private void load()
{
HitObject.DefaultsApplied += _ => updateText();
updateText();

if (editor != null)
editor.ShowSampleEditPopoverRequested += onShowSampleEditPopoverRequested;
}

protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);

if (editor != null)
editor.ShowSampleEditPopoverRequested -= onShowSampleEditPopoverRequested;
}

private void onShowSampleEditPopoverRequested(double time)
{
if (!Precision.AlmostEquals(time, GetTime())) return;

editorClock?.SeekSmoothlyTo(GetTime());
this.ShowPopover();
}

protected override bool OnClick(ClickEvent e)
Expand Down
89 changes: 86 additions & 3 deletions osu.Game/Screens/Edit/Editor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
Expand Down Expand Up @@ -224,6 +225,9 @@ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnl
/// </remarks>
public Bindable<bool> ComposerFocusMode { get; } = new Bindable<bool>();

[CanBeNull]
public event Action<double> ShowSampleEditPopoverRequested;

public Editor(EditorLoader loader = null)
{
this.loader = loader;
Expand Down Expand Up @@ -713,6 +717,26 @@ protected override bool OnScroll(ScrollEvent e)

public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
// Repeatable actions
switch (e.Action)
{
case GlobalAction.EditorSeekToPreviousHitObject:
seekHitObject(-1);
return true;

case GlobalAction.EditorSeekToNextHitObject:
seekHitObject(1);
return true;

case GlobalAction.EditorSeekToPreviousSamplePoint:
seekSamplePoint(-1);
return true;

case GlobalAction.EditorSeekToNextSamplePoint:
seekSamplePoint(1);
return true;
}

if (e.Repeat)
return false;

Expand Down Expand Up @@ -750,10 +774,9 @@ public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
case GlobalAction.EditorTestGameplay:
bottomBar.TestGameplayButton.TriggerClick();
return true;

default:
return false;
}

return false;
}

public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
Expand Down Expand Up @@ -1077,6 +1100,66 @@ private void seekControlPoint(int direction)
clock.Seek(found.Time);
}

private void seekHitObject(int direction)
{
var found = direction < 1
? editorBeatmap.HitObjects.LastOrDefault(p => p.StartTime < clock.CurrentTimeAccurate)
: editorBeatmap.HitObjects.FirstOrDefault(p => p.StartTime > clock.CurrentTimeAccurate);

if (found != null)
clock.SeekSmoothlyTo(found.StartTime);
}

private void seekSamplePoint(int direction)
{
double currentTime = clock.CurrentTimeAccurate;

// Check if we are currently inside a hit object with node samples, if so seek to the next node sample point
var current = direction < 1
? editorBeatmap.HitObjects.LastOrDefault(p => p is IHasRepeats r && p.StartTime < currentTime && r.EndTime >= currentTime)
: editorBeatmap.HitObjects.LastOrDefault(p => p is IHasRepeats r && p.StartTime <= currentTime && r.EndTime > currentTime);

if (current != null)
{
// Find the next node sample point
var r = (IHasRepeats)current;
double[] nodeSamplePointTimes = new double[r.RepeatCount + 3];

nodeSamplePointTimes[0] = current.StartTime;
// The sample point for the main samples is sandwiched between the head and the first repeat
nodeSamplePointTimes[1] = current.StartTime + r.Duration / r.SpanCount() / 2;

for (int i = 0; i < r.SpanCount(); i++)
{
nodeSamplePointTimes[i + 2] = current.StartTime + r.Duration * (i + 1) / r.SpanCount();
}

double found = direction < 1
? nodeSamplePointTimes.Last(p => p < currentTime)
: nodeSamplePointTimes.First(p => p > currentTime);

clock.SeekSmoothlyTo(found);
}
else
{
if (direction < 1)
{
current = editorBeatmap.HitObjects.LastOrDefault(p => p.StartTime < currentTime);
if (current != null)
clock.SeekSmoothlyTo(current is IHasRepeats r ? r.EndTime : current.StartTime);
}
else
{
current = editorBeatmap.HitObjects.FirstOrDefault(p => p.StartTime > currentTime);
if (current != null)
clock.SeekSmoothlyTo(current.StartTime);
}
}

// Show the sample edit popover at the current time
ShowSampleEditPopoverRequested?.Invoke(clock.CurrentTimeAccurate);
}

private void seek(UIEvent e, int direction)
{
double amount = e.ShiftPressed ? 4 : 1;
Expand Down

0 comments on commit a71bc3a

Please sign in to comment.