mirror of
https://github.com/ppy/osu.git
synced 2025-02-13 21:12:55 +08:00
Merge pull request #28737 from OliBomby/doubleclick
Add more ways to seek to sample points
This commit is contained in:
commit
a71bc3a24a
@ -7,6 +7,7 @@ using Humanizer;
|
||||
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;
|
||||
@ -307,6 +308,46 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
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()
|
||||
{
|
||||
@ -626,7 +667,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
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);
|
||||
@ -640,6 +681,21 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
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();
|
||||
@ -784,5 +840,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -147,6 +147,10 @@ namespace osu.Game.Input.Bindings
|
||||
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[]
|
||||
@ -456,6 +460,18 @@ namespace osu.Game.Input.Bindings
|
||||
|
||||
[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
|
||||
|
@ -404,6 +404,26 @@ namespace osu.Game.Localisation
|
||||
/// </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}";
|
||||
}
|
||||
}
|
||||
|
@ -23,15 +23,15 @@ namespace osu.Game.Overlays.Volume
|
||||
{
|
||||
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;
|
||||
|
@ -110,14 +110,18 @@ namespace osu.Game.Overlays
|
||||
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;
|
||||
|
||||
|
@ -22,6 +22,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
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;
|
||||
|
@ -14,6 +14,7 @@ using osu.Framework.Graphics.Containers;
|
||||
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;
|
||||
@ -33,6 +34,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public readonly HitObject HitObject;
|
||||
|
||||
[Resolved]
|
||||
private EditorClock? editorClock { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private Editor? editor { get; set; }
|
||||
|
||||
public SamplePointPiece(HitObject hitObject)
|
||||
{
|
||||
HitObject = hitObject;
|
||||
@ -43,11 +50,32 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
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)
|
||||
|
@ -44,6 +44,7 @@ using osu.Game.Overlays.OSD;
|
||||
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;
|
||||
@ -224,6 +225,9 @@ namespace osu.Game.Screens.Edit
|
||||
/// </remarks>
|
||||
public Bindable<bool> ComposerFocusMode { get; } = new Bindable<bool>();
|
||||
|
||||
[CanBeNull]
|
||||
public event Action<double> ShowSampleEditPopoverRequested;
|
||||
|
||||
public Editor(EditorLoader loader = null)
|
||||
{
|
||||
this.loader = loader;
|
||||
@ -713,6 +717,26 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
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;
|
||||
|
||||
@ -750,10 +774,9 @@ namespace osu.Game.Screens.Edit
|
||||
case GlobalAction.EditorTestGameplay:
|
||||
bottomBar.TestGameplayButton.TriggerClick();
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
@ -1077,6 +1100,66 @@ namespace osu.Game.Screens.Edit
|
||||
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;
|
||||
|
Loading…
Reference in New Issue
Block a user