diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 2f20d75813..9690d00d4c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -1,9 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; @@ -21,7 +23,7 @@ namespace osu.Game.Tests.Visual.Gameplay { public partial class TestSceneSkinEditor : PlayerTestScene { - private SkinEditor? skinEditor; + private SkinEditor skinEditor = null!; protected override bool Autoplay => true; @@ -31,26 +33,75 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] public readonly EditorClipboard Clipboard = new EditorClipboard(); + private SkinComponentsContainer targetContainer => Player.ChildrenOfType().First(); + [SetUpSteps] public override void SetUpSteps() { base.SetUpSteps(); - AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded)); + AddUntilStep("wait for hud load", () => targetContainer.ComponentsLoaded); AddStep("reload skin editor", () => { - skinEditor?.Expire(); + if (skinEditor.IsNotNull()) + skinEditor.Expire(); Player.ScaleTo(0.4f); LoadComponentAsync(skinEditor = new SkinEditor(Player), Add); }); - AddUntilStep("wait for loaded", () => skinEditor!.IsLoaded); + AddUntilStep("wait for loaded", () => skinEditor.IsLoaded); + } + + [TestCase(false)] + [TestCase(true)] + public void TestBringToFront(bool alterSelectionOrder) + { + AddAssert("Ensure over three components available", () => targetContainer.Components.Count, () => Is.GreaterThan(3)); + + IEnumerable originalOrder = null!; + + AddStep("Save order of components before operation", () => originalOrder = targetContainer.Components.Take(3).ToArray()); + + if (alterSelectionOrder) + AddStep("Select first three components in reverse order", () => skinEditor.SelectedComponents.AddRange(originalOrder.Reverse())); + else + AddStep("Select first three components", () => skinEditor.SelectedComponents.AddRange(originalOrder)); + + AddAssert("Components are not front-most", () => targetContainer.Components.TakeLast(3).ToArray(), () => Is.Not.EqualTo(skinEditor.SelectedComponents)); + + AddStep("Bring to front", () => skinEditor.BringSelectionToFront()); + AddAssert("Ensure components are now front-most in original order", () => targetContainer.Components.TakeLast(3).ToArray(), () => Is.EqualTo(originalOrder)); + AddStep("Bring to front again", () => skinEditor.BringSelectionToFront()); + AddAssert("Ensure components are still front-most in original order", () => targetContainer.Components.TakeLast(3).ToArray(), () => Is.EqualTo(originalOrder)); + } + + [TestCase(false)] + [TestCase(true)] + public void TestSendToBack(bool alterSelectionOrder) + { + AddAssert("Ensure over three components available", () => targetContainer.Components.Count, () => Is.GreaterThan(3)); + + IEnumerable originalOrder = null!; + + AddStep("Save order of components before operation", () => originalOrder = targetContainer.Components.TakeLast(3).ToArray()); + + if (alterSelectionOrder) + AddStep("Select last three components in reverse order", () => skinEditor.SelectedComponents.AddRange(originalOrder.Reverse())); + else + AddStep("Select last three components", () => skinEditor.SelectedComponents.AddRange(originalOrder)); + + AddAssert("Components are not back-most", () => targetContainer.Components.Take(3).ToArray(), () => Is.Not.EqualTo(skinEditor.SelectedComponents)); + + AddStep("Send to back", () => skinEditor.SendSelectionToBack()); + AddAssert("Ensure components are now back-most in original order", () => targetContainer.Components.Take(3).ToArray(), () => Is.EqualTo(originalOrder)); + AddStep("Send to back again", () => skinEditor.SendSelectionToBack()); + AddAssert("Ensure components are still back-most in original order", () => targetContainer.Components.Take(3).ToArray(), () => Is.EqualTo(originalOrder)); } [Test] public void TestToggleEditor() { - AddToggleStep("toggle editor visibility", _ => skinEditor!.ToggleVisibility()); + AddToggleStep("toggle editor visibility", _ => skinEditor.ToggleVisibility()); } [Test] @@ -63,7 +114,7 @@ namespace osu.Game.Tests.Visual.Gameplay var blueprint = skinEditor.ChildrenOfType().First(b => b.Item is BarHitErrorMeter); hitErrorMeter = (BarHitErrorMeter)blueprint.Item; - skinEditor!.SelectedComponents.Clear(); + skinEditor.SelectedComponents.Clear(); skinEditor.SelectedComponents.Add(blueprint.Item); }); diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs index 034ca11c5c..c090878899 100644 --- a/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs +++ b/osu.Game/Overlays/SkinEditor/SkinBlueprint.cs @@ -82,6 +82,7 @@ namespace osu.Game.Overlays.SkinEditor { Text = Item.GetType().Name, Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold), + Alpha = 0, Anchor = Anchor.BottomRight, Origin = Anchor.TopRight, }, @@ -99,7 +100,6 @@ namespace osu.Game.Overlays.SkinEditor base.LoadComplete(); updateSelectedState(); - this.FadeInFromZero(200, Easing.OutQuint); } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 9d470f58f1..02ede8c08b 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -556,7 +556,50 @@ namespace osu.Game.Overlays.SkinEditor changeHandler?.BeginChange(); foreach (var item in items) - availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item); + availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item, true); + + changeHandler?.EndChange(); + } + + public void BringSelectionToFront() + { + if (getTarget(selectedTarget.Value) is not SkinComponentsContainer target) + return; + + changeHandler?.BeginChange(); + + // Iterating by target components order ensures we maintain the same order across selected components, regardless + // of the order they were selected in. + foreach (var d in target.Components.ToArray()) + { + if (!SelectedComponents.Contains(d)) + continue; + + target.Remove(d, false); + + // Selection would be reset by the remove. + SelectedComponents.Add(d); + target.Add(d); + } + + changeHandler?.EndChange(); + } + + public void SendSelectionToBack() + { + if (getTarget(selectedTarget.Value) is not SkinComponentsContainer target) + return; + + changeHandler?.BeginChange(); + + foreach (var d in target.Components.ToArray()) + { + if (SelectedComponents.Contains(d)) + continue; + + target.Remove(d, false); + target.Add(d); + } changeHandler?.EndChange(); } diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index c628ad8480..b43f4eeb00 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -13,6 +13,7 @@ using osu.Framework.Utils; using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Skinning; using osuTK; @@ -206,6 +207,14 @@ namespace osu.Game.Overlays.SkinEditor ((Drawable)blueprint.Item).Position = Vector2.Zero; }); + yield return new EditorMenuItemSpacer(); + + yield return new OsuMenuItem("Bring to front", MenuItemType.Standard, () => skinEditor.BringSelectionToFront()); + + yield return new OsuMenuItem("Send to back", MenuItemType.Standard, () => skinEditor.SendSelectionToBack()); + + yield return new EditorMenuItemSpacer(); + foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; diff --git a/osu.Game/Skinning/ISerialisableDrawableContainer.cs b/osu.Game/Skinning/ISerialisableDrawableContainer.cs index 9f93d8a2e3..a19c8c5162 100644 --- a/osu.Game/Skinning/ISerialisableDrawableContainer.cs +++ b/osu.Game/Skinning/ISerialisableDrawableContainer.cs @@ -45,6 +45,7 @@ namespace osu.Game.Skinning /// Remove an existing skinnable component from this target. /// /// The component to remove. - void Remove(ISerialisableDrawable component); + /// Whether removed items should be immediately disposed. + void Remove(ISerialisableDrawable component, bool disposeImmediately); } } diff --git a/osu.Game/Skinning/SkinComponentsContainer.cs b/osu.Game/Skinning/SkinComponentsContainer.cs index d18e9023cd..adf0a288b4 100644 --- a/osu.Game/Skinning/SkinComponentsContainer.cs +++ b/osu.Game/Skinning/SkinComponentsContainer.cs @@ -100,7 +100,7 @@ namespace osu.Game.Skinning /// /// Thrown when attempting to add an element to a target which is not supported by the current skin. /// Thrown if the provided instance is not a . - public void Remove(ISerialisableDrawable component) + public void Remove(ISerialisableDrawable component, bool disposeImmediately) { if (content == null) throw new NotSupportedException("Attempting to remove a new component from a target container which is not supported by the current skin."); @@ -108,7 +108,7 @@ namespace osu.Game.Skinning if (!(component is Drawable drawable)) throw new ArgumentException($"Provided argument must be of type {nameof(Drawable)}.", nameof(component)); - content.Remove(drawable, true); + content.Remove(drawable, disposeImmediately); components.Remove(component); }