1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 12:33:01 +08:00

Merge pull request #24289 from peppy/editor-prefer-closest

Change beatmap editor to always select the closest object in time
This commit is contained in:
Bartłomiej Dach 2023-07-20 19:54:17 +02:00 committed by GitHub
commit 2f772abea6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 122 additions and 25 deletions

View File

@ -6,21 +6,21 @@
using System; using System;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Tests.Beatmaps;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Beatmaps;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -217,6 +217,75 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
} }
[Test]
public void TestNearestSelection()
{
var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 };
var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 };
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject }));
moveMouseToObject(() => firstObject);
AddStep("seek near first", () => EditorClock.Seek(100));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear());
AddStep("seek near second", () => EditorClock.Seek(500));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject));
AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear());
AddStep("seek halfway", () => EditorClock.Seek(300));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
}
[Test]
public void TestNearestSelectionWithEndTime()
{
var firstObject = new Slider
{
Position = new Vector2(256, 192),
StartTime = 0,
Path = new SliderPath(new[]
{
new PathControlPoint(),
new PathControlPoint(new Vector2(50, 0)),
})
};
var secondObject = new HitCircle
{
Position = new Vector2(256, 192),
StartTime = 600
};
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new HitObject[] { firstObject, secondObject }));
moveMouseToObject(() => firstObject);
AddStep("seek near first", () => EditorClock.Seek(100));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear());
AddStep("seek near second", () => EditorClock.Seek(500));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject));
AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear());
AddStep("seek roughly halfway", () => EditorClock.Seek(350));
AddStep("left click", () => InputManager.Click(MouseButton.Left));
// Slider gets priority due to end time.
AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject));
}
[TestCase(false)] [TestCase(false)]
[TestCase(true)] [TestCase(true)]
public void TestMultiSelectFromDrag(bool alreadySelectedBeforeDrag) public void TestMultiSelectFromDrag(bool alreadySelectedBeforeDrag)

View File

@ -138,24 +138,28 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestCyclicSelection() public void TestCyclicSelection()
{ {
SkinBlueprint[] blueprints = null!; List<SkinBlueprint> blueprints = new List<SkinBlueprint>();
AddStep("Add big black boxes", () => AddStep("clear list", () => blueprints.Clear());
for (int i = 0; i < 3; i++)
{
AddStep("Add big black box", () =>
{ {
InputManager.MoveMouseTo(skinEditor.ChildrenOfType<BigBlackBox>().First()); InputManager.MoveMouseTo(skinEditor.ChildrenOfType<BigBlackBox>().First());
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Left);
}); });
AddStep("store box", () =>
{
// Add blueprints one-by-one so we have a stable order for testing reverse cyclic selection against.
blueprints.Add(skinEditor.ChildrenOfType<SkinBlueprint>().Single(s => s.IsSelected));
});
}
AddAssert("Three black boxes added", () => targetContainer.Components.OfType<BigBlackBox>().Count(), () => Is.EqualTo(3)); AddAssert("Three black boxes added", () => targetContainer.Components.OfType<BigBlackBox>().Count(), () => Is.EqualTo(3));
AddStep("Store black box blueprints", () => AddAssert("Selection is last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item));
{
blueprints = skinEditor.ChildrenOfType<SkinBlueprint>().Where(b => b.Item is BigBlackBox).ToArray();
});
AddAssert("Selection is black box 1", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item));
AddStep("move cursor to black box", () => AddStep("move cursor to black box", () =>
{ {
@ -164,13 +168,13 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left)); AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left));
AddAssert("Selection is black box 2", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[1].Item)); AddAssert("Selection is second last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[1].Item));
AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left)); AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left));
AddAssert("Selection is black box 3", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item)); AddAssert("Selection is last", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item));
AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left)); AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left));
AddAssert("Selection is black box 1", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item)); AddAssert("Selection is first", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item));
AddStep("select all boxes", () => AddStep("select all boxes", () =>
{ {

View File

@ -381,6 +381,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary> /// </summary>
private bool selectedBlueprintAlreadySelectedOnMouseDown; private bool selectedBlueprintAlreadySelectedOnMouseDown;
/// <summary>
/// Sorts the supplied <paramref name="blueprints"/> by the order of preference when making a selection.
/// Blueprints at the start of the list will be prioritised over later items if the selection requested is ambiguous due to spatial overlap.
/// </summary>
protected virtual IEnumerable<SelectionBlueprint<T>> ApplySelectionOrder(IEnumerable<SelectionBlueprint<T>> blueprints) => blueprints.Reverse();
/// <summary> /// <summary>
/// Attempts to select any hovered blueprints. /// Attempts to select any hovered blueprints.
/// </summary> /// </summary>
@ -390,15 +396,28 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
// Iterate from the top of the input stack (blueprints closest to the front of the screen first). // Iterate from the top of the input stack (blueprints closest to the front of the screen first).
// Priority is given to already-selected blueprints. // Priority is given to already-selected blueprints.
foreach (SelectionBlueprint<T> blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected)) foreach (SelectionBlueprint<T> blueprint in SelectionBlueprints.AliveChildren.Where(b => b.IsSelected))
{ {
if (!blueprint.IsHovered) continue; if (runForBlueprint(blueprint))
return true;
}
selectedBlueprintAlreadySelectedOnMouseDown = blueprint.State == SelectionState.Selected; foreach (SelectionBlueprint<T> blueprint in ApplySelectionOrder(SelectionBlueprints.AliveChildren))
return clickSelectionHandled = SelectionHandler.MouseDownSelectionRequested(blueprint, e); {
if (runForBlueprint(blueprint))
return true;
} }
return false; return false;
bool runForBlueprint(SelectionBlueprint<T> blueprint)
{
if (!blueprint.IsHovered) return false;
selectedBlueprintAlreadySelectedOnMouseDown = blueprint.State == SelectionState.Selected;
clickSelectionHandled = SelectionHandler.MouseDownSelectionRequested(blueprint, e);
return true;
}
} }
/// <summary> /// <summary>
@ -432,13 +451,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
// The depth of blueprints is constantly changing (see above where selected blueprints are brought to the front). // The depth of blueprints is constantly changing (see above where selected blueprints are brought to the front).
// For this logic, we want a stable sort order so we can correctly cycle, thus using the blueprintMap instead. // For this logic, we want a stable sort order so we can correctly cycle, thus using the blueprintMap instead.
IEnumerable<SelectionBlueprint<T>> cyclingSelectionBlueprints = blueprintMap.Values; IEnumerable<SelectionBlueprint<T>> cyclingSelectionBlueprints = ApplySelectionOrder(blueprintMap.Values);
// If there's already a selection, let's start from the blueprint after the selection. // If there's already a selection, let's start from the blueprint after the selection.
cyclingSelectionBlueprints = cyclingSelectionBlueprints.SkipWhile(b => !b.IsSelected).Skip(1); cyclingSelectionBlueprints = cyclingSelectionBlueprints.SkipWhile(b => !b.IsSelected).Skip(1);
// Add the blueprints from before the selection to the end of the enumerable to allow for cyclic selection. // Add the blueprints from before the selection to the end of the enumerable to allow for cyclic selection.
cyclingSelectionBlueprints = cyclingSelectionBlueprints.Concat(blueprintMap.Values.TakeWhile(b => !b.IsSelected)); cyclingSelectionBlueprints = cyclingSelectionBlueprints.Concat(ApplySelectionOrder(blueprintMap.Values).TakeWhile(b => !b.IsSelected));
foreach (SelectionBlueprint<T> blueprint in cyclingSelectionBlueprints) foreach (SelectionBlueprint<T> blueprint in cyclingSelectionBlueprints)
{ {

View File

@ -3,6 +3,7 @@
#nullable disable #nullable disable
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -129,6 +130,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
return true; return true;
} }
protected override IEnumerable<SelectionBlueprint<HitObject>> ApplySelectionOrder(IEnumerable<SelectionBlueprint<HitObject>> blueprints) =>
base.ApplySelectionOrder(blueprints)
.OrderBy(b => Math.Min(Math.Abs(EditorClock.CurrentTime - b.Item.GetEndTime()), Math.Abs(EditorClock.CurrentTime - b.Item.StartTime)));
protected override Container<SelectionBlueprint<HitObject>> CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }; protected override Container<SelectionBlueprint<HitObject>> CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both };
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new EditorSelectionHandler(); protected override SelectionHandler<HitObject> CreateSelectionHandler() => new EditorSelectionHandler();