diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index be5dd59206..b1a7ef976a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Linq; using NUnit.Framework; @@ -82,7 +80,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestNudgeSelection() { - HitCircle[] addedObjects = null; + HitCircle[] addedObjects = null!; AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] { @@ -104,7 +102,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestRotateHotkeys() { - HitCircle[] addedObjects = null; + HitCircle[] addedObjects = null!; AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] { @@ -136,7 +134,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestGlobalFlipHotkeys() { - HitCircle addedObject = null; + HitCircle addedObject = null!; AddStep("add hitobjects", () => EditorBeatmap.Add(addedObject = new HitCircle { StartTime = 100 })); @@ -286,11 +284,104 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); } + [Test] + public void TestCyclicSelection() + { + var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 }; + var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 300 }; + var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject })); + + moveMouseToObject(() => firstObject); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject)); + + // cycle around + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + } + + [Test] + public void TestCyclicSelectionOutwards() + { + var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 }; + var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 300 }; + var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject })); + + moveMouseToObject(() => firstObject); + + AddStep("seek near second", () => EditorClock.Seek(320)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + // cycle around + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + } + + [Test] + public void TestCyclicSelectionBackwards() + { + var firstObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 0 }; + var secondObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 300 }; + var thirdObject = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { firstObject, secondObject, thirdObject })); + + moveMouseToObject(() => firstObject); + + AddStep("seek to third", () => EditorClock.Seek(600)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("second selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondObject)); + + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("first selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(firstObject)); + + // cycle around + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("third selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(thirdObject)); + } + + [Test] + public void TestDoubleClickToSeek() + { + var hitCircle = new HitCircle { Position = new Vector2(256, 192), StartTime = 600 }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] { hitCircle })); + + moveMouseToObject(() => hitCircle); + + AddRepeatStep("double click", () => InputManager.Click(MouseButton.Left), 2); + + AddUntilStep("seeked to circle", () => EditorClock.CurrentTime, () => Is.EqualTo(600)); + } + [TestCase(false)] [TestCase(true)] public void TestMultiSelectFromDrag(bool alreadySelectedBeforeDrag) { - HitCircle[] addedObjects = null; + HitCircle[] addedObjects = null!; AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] { @@ -389,7 +480,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestQuickDeleteRemovesSliderControlPoint() { - Slider slider = null; + Slider slider = null!; PathControlPoint[] points = { diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs index db27e20010..3f8d9f80d4 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs @@ -25,8 +25,6 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private SkinEditor editor { get; set; } = null!; - protected override bool AllowCyclicSelection => true; - public SkinBlueprintContainer(ISerialisableDrawableContainer targetContainer) { this.targetContainer = targetContainer; diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 4a2d36b677..2ee162bf3b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -46,15 +46,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected readonly BindableList SelectedItems = new BindableList(); - /// - /// Whether to allow cyclic selection on clicking multiple times. - /// - /// - /// Disabled by default as it does not work well with editors that support double-clicking or other advanced interactions. - /// Can probably be made to work with more thought. - /// - protected virtual bool AllowCyclicSelection => false; - protected BlueprintContainer() { RelativeSizeAxes = Axes.Both; @@ -167,6 +158,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (ClickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != ClickedBlueprint) return false; + doubleClickHandled = true; return true; } @@ -177,6 +169,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { endClickSelection(e); clickSelectionHandled = false; + doubleClickHandled = false; isDraggingBlueprint = false; wasDragStarted = false; }); @@ -376,6 +369,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// private bool clickSelectionHandled; + /// + /// Whether a blueprint was double-clicked since last mouse down. + /// + private bool doubleClickHandled; + /// /// Whether the selected blueprint(s) were already selected on mouse down. Generally used to perform selection cycling on mouse up in such a case. /// @@ -427,8 +425,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a click selection was active. private bool endClickSelection(MouseButtonEvent e) { - // If already handled a selection or drag, we don't want to perform a mouse up / click action. - if (clickSelectionHandled || isDraggingBlueprint) return true; + // If already handled a selection, double-click, or drag, we don't want to perform a mouse up / click action. + if (clickSelectionHandled || doubleClickHandled || isDraggingBlueprint) return true; if (e.Button != MouseButton.Left) return false; @@ -444,7 +442,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; } - if (!wasDragStarted && selectedBlueprintAlreadySelectedOnMouseDown && SelectedItems.Count == 1 && AllowCyclicSelection) + if (!wasDragStarted && selectedBlueprintAlreadySelectedOnMouseDown && SelectedItems.Count == 1) { // If a click occurred and was handled by the currently selected blueprint but didn't result in a drag, // cycle between other blueprints which are also under the cursor.