Skip to content
Open
4 changes: 2 additions & 2 deletions osu.Game.Tests/Skins/SkinDeserialisationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ public void TestSkinnableComponentsCoveredByDeserialisationTests()
}
}

var editableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables().Where(t => (Activator.CreateInstance(t) as ISerialisableDrawable)?.IsEditable == true);
var availableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables();

Assert.That(instantiatedTypes, Is.EquivalentTo(editableTypes));
Assert.That(instantiatedTypes, Is.EquivalentTo(availableTypes));
}

[Test]
Expand Down
79 changes: 79 additions & 0 deletions osu.Game.Tests/Skins/SkinnableContainerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
using osuTK;

namespace osu.Game.Tests.Skins
{
[HeadlessTest]
[TestFixture]
public partial class SkinnableContainerTest : OsuTestScene
{
[Test]
public void TestSkinnableConstraintsAreRespectedAtDeserialisationTime()
{
SkinnableContainer container = null!;

AddStep("create container", () =>
{
Child = container = new SkinnableContainer(new GlobalSkinnableContainerLookup((GlobalSkinnableContainers)(-1)));
});
AddUntilStep("wait for loaded", () => container.ComponentsLoaded);

AddStep("create components", () =>
{
container.Add(new SingletonSkinnable());
container.Add(new SingletonSkinnable());
container.Add(new ImmovableSkinnable { Position = new Vector2(300) });
container.Add(new UnscaleableSkinnable { Scale = new Vector2(3) });
container.Add(new NonrotatableSkinnable { Rotation = 42 });
});

AddStep("serialise and deserialise", () =>
{
var serialised = ((ISerialisableDrawableContainer)container).CreateSerialisedInfo();
container.Reload(new Container
{
ChildrenEnumerable = serialised.Select(s => s.CreateInstance())
});
});

AddAssert("singleton constraint respected", () => container.Components.OfType<SingletonSkinnable>().Count(), () => Is.EqualTo(1));
AddAssert("immovable constraint respected", () => container.Components.OfType<ImmovableSkinnable>().Single().Position, () => Is.EqualTo(Vector2.Zero));
AddAssert("unscaleable constraint respected", () => container.Components.OfType<UnscaleableSkinnable>().Single().Scale, () => Is.EqualTo(Vector2.One));
AddAssert("nonrotatable constraint respected", () => container.Components.OfType<NonrotatableSkinnable>().Single().Rotation, () => Is.Zero);
}

private partial class SingletonSkinnable : Drawable, ISerialisableDrawable
{
public bool UsesFixedAnchor { get; set; }
public bool CanBePlaced(IEnumerable<ISerialisableDrawable> components) => !components.OfType<SingletonSkinnable>().Any();
}

private partial class ImmovableSkinnable : Drawable, ISerialisableDrawable
{
public bool UsesFixedAnchor { get; set; }
public bool CanBeMoved => false;
}

private partial class UnscaleableSkinnable : Drawable, ISerialisableDrawable
{
public bool UsesFixedAnchor { get; set; }
public bool CanBeScaled(Axes _) => false;
}

private partial class NonrotatableSkinnable : Drawable, ISerialisableDrawable
{
public bool UsesFixedAnchor { get; set; }
public bool CanBeRotated => false;
}
}
}
140 changes: 140 additions & 0 deletions osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
Expand Down Expand Up @@ -590,6 +595,103 @@ public void TestFlip()
AddAssert("big black box position is 0,0", () => Precision.AlmostEquals(box1.Position, new Vector2()));
}

[Test]
public void TestConstraints()
{
TestConstrainedBox unselectable = null!, immovable = null!, unscaleable = null!, nonrotatable = null!;

AddStep("add constrained boxes", () =>
{
var target = Player.ChildrenOfType<SkinnableContainer>().First();
target.Add(unselectable = new TestConstrainedBox
{
Text = nameof(unselectable),
CanBeSelected = false,
X = 400,
Y = 200,
});
target.Add(immovable = new TestConstrainedBox
{
Text = nameof(immovable),
CanBeMoved = false,
X = 800,
Y = 200,
});
target.Add(unscaleable = new TestConstrainedBox
{
Text = nameof(unscaleable),
CanBeScaled = false,
X = 400,
Y = 600,
});
target.Add(nonrotatable = new TestConstrainedBox
{
Text = nameof(nonrotatable),
CanBeRotated = false,
X = 800,
Y = 600,
});
});

AddStep("click unselectable", () =>
{
InputManager.MoveMouseTo(unselectable);
InputManager.Click(MouseButton.Left);
});
AddAssert("nothing selected", () => skinEditor.SelectedComponents, () => Is.Empty);

AddStep("start dragging immovable", () =>
{
InputManager.MoveMouseTo(immovable);
InputManager.PressButton(MouseButton.Left);
});
AddStep("finish drag", () =>
{
InputManager.MoveMouseTo(immovable.ScreenSpaceDrawQuad.Centre + new Vector2(300));
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("immovable didn't move", () => immovable.Position, () => Is.EqualTo(new Vector2(800, 200)));

AddStep("click unscaleable", () =>
{
InputManager.MoveMouseTo(unscaleable);
InputManager.Click(MouseButton.Left);
});
AddStep("start dragging on bottom right of selection box", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<SelectionBox>().Single().ScreenSpaceDrawQuad.BottomRight);
InputManager.PressButton(MouseButton.Left);
});
AddStep("attempt resize", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<SelectionBox>().Single().ScreenSpaceDrawQuad.BottomRight + new Vector2(200));
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("unscaleable didn't resize", () => unscaleable.Size, () => Is.EqualTo(new Vector2(200)));
AddAssert("unscaleable didn't scale", () => unscaleable.Scale, () => Is.EqualTo(Vector2.One));
AddStep("attempt flip", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.H);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddAssert("unscaleable didn't scale", () => unscaleable.Scale, () => Is.EqualTo(Vector2.One));

AddStep("click nonrotatable", () =>
{
InputManager.MoveMouseTo(nonrotatable);
InputManager.Click(MouseButton.Left);
});
AddAssert("no rotation handles present", this.ChildrenOfType<SelectionBoxRotationHandle>, () => Is.Empty);
AddStep("attempt rotate", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Comma);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddAssert("nonrotatable didn't rotate", () => nonrotatable.Rotation, () => Is.Zero);
}

private Skin importSkinFromArchives(string filename)
{
var imported = skins.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();
Expand All @@ -615,5 +717,43 @@ public byte[] GetCurrentState()
return newState;
}
}

private partial class TestConstrainedBox : CompositeDrawable, ISerialisableDrawable
{
public bool UsesFixedAnchor { get; set; }

public required LocalisableString Text { get; init; }

public bool CanBeSelected { get; init; } = true;
public bool CanBeMoved { get; init; } = true;
public bool CanBeScaled { get; init; } = true;
bool ISerialisableDrawable.CanBeScaled(Axes axes) => CanBeScaled;
public bool CanBeRotated { get; init; } = true;

[BackgroundDependencyLoader]
private void load()
{
var backgroundColour = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1);
var foregroundColour = OsuColour.ForegroundTextColourFor(backgroundColour);

Size = new Vector2(200);
InternalChildren =
[
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour,
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = Text,
Colour = foregroundColour,
Font = OsuFont.Style.Title,
}
];
}
}
}
}
2 changes: 1 addition & 1 deletion osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ private void componentsChanged(object? sender, NotifyCollectionChangedEventArgs

protected override void AddBlueprintFor(ISerialisableDrawable item)
{
if (!item.IsEditable)
if (!item.CanBeSelected)
return;

base.AddBlueprintFor(item);
Expand Down
6 changes: 3 additions & 3 deletions osu.Game/Overlays/SkinEditor/SkinComponentToolbox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ private void load()
Spacing = new Vector2(EditorSidebar.PADDING)
};

reloadComponents();
ReloadComponents();
}

private void reloadComponents()
public void ReloadComponents()
{
fill.Clear();

Expand All @@ -71,7 +71,7 @@ private void attemptAddComponent(Type type)
{
Drawable instance = (Drawable)Activator.CreateInstance(type)!;

if (!((ISerialisableDrawable)instance).IsEditable) return;
if (!((ISerialisableDrawable)instance).CanBePlaced(target.Components)) return;

fill.Add(new ToolboxComponentButton(instance, target)
{
Expand Down
19 changes: 16 additions & 3 deletions osu.Game/Overlays/SkinEditor/SkinEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public partial class SkinEditor : VisibilityContainer, ICanAcceptFiles, IKeyBind
private EditorSidebar settingsSidebar = null!;

private SkinEditorChangeHandler? changeHandler;
private readonly IBindableList<ISerialisableDrawable> components = new BindableList<ISerialisableDrawable>();

private EditorMenuItem undoMenuItem = null!;
private EditorMenuItem redoMenuItem = null!;
Expand Down Expand Up @@ -280,6 +281,7 @@ protected override void LoadComplete()
Scheduler.AddOnce(skinChanged);
}, true);

components.BindCollectionChanged((_, _) => Scheduler.AddOnce(reloadToolboxes), true);
SelectedComponents.BindCollectionChanged((_, _) => Scheduler.AddOnce(populateSettings), true);

selectedTarget.BindValueChanged(targetChanged, true);
Expand Down Expand Up @@ -393,7 +395,7 @@ private void targetChanged(ValueChangedEvent<GlobalSkinnableContainerLookup?> ta
}

if (skinComponentsContainer.ComponentsLoaded)
bindChangeHandler(skinComponentsContainer);
bindSkinnableContainer(skinComponentsContainer);
else
skinComponentsContainer.OnComponentsLoaded += onComponentsLoaded;

Expand Down Expand Up @@ -433,7 +435,7 @@ void onComponentsLoaded(Drawable d)
{
SkinnableContainer container = (SkinnableContainer)d;
container.OnComponentsLoaded -= onComponentsLoaded;
Schedule(() => bindChangeHandler(container));
Schedule(() => bindSkinnableContainer(container));
}

void requestPlacement(Type type)
Expand All @@ -445,14 +447,23 @@ void requestPlacement(Type type)
placeComponent(component);
}

void bindChangeHandler(SkinnableContainer skinnableContainer)
void bindSkinnableContainer(SkinnableContainer skinnableContainer)
{
changeHandler = new SkinEditorChangeHandler(skinnableContainer);
changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);

components.UnbindBindings();
components.BindTo(skinnableContainer.Components);
}
}

private void reloadToolboxes()
{
foreach (var toolbox in componentsSidebar.OfType<SkinComponentToolbox>())
toolbox.ReloadComponents();
}

private void skinChanged()
{
if (skins.EnsureMutableSkin())
Expand All @@ -478,6 +489,8 @@ private void skinChanged()
changeHandler?.Dispose();
changeHandler = null;

components.UnbindBindings();

// Schedule is required to ensure that all layout in `LoadComplete` methods has been completed
// before storing an undo state.
//
Expand Down
10 changes: 8 additions & 2 deletions osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ private void updateTernaryStates()

public override bool HandleFlip(Direction direction, bool flipOverOrigin)
{
if (SelectedBlueprints.Any(b => !b.Item.CanBeScaled(direction == Direction.Horizontal ? Axes.X : Axes.Y)))
return false;

var selectionQuad = getSelectionQuad();
Vector2 scaleFactor = direction == Direction.Horizontal ? new Vector2(-1, 1) : new Vector2(1, -1);

Expand All @@ -100,6 +103,9 @@ public override bool HandleFlip(Direction direction, bool flipOverOrigin)

public override bool HandleMovement(MoveSelectionEvent<ISerialisableDrawable> moveEvent)
{
if (SelectedBlueprints.Any(b => !b.Item.CanBeMoved))
return false;

foreach (var c in SelectedBlueprints)
{
var item = c.Item;
Expand All @@ -126,8 +132,8 @@ protected override void OnSelectionChanged()
{
base.OnSelectionChanged();

SelectionBox.CanFlipX = true;
SelectionBox.CanFlipY = true;
SelectionBox.CanFlipX = SelectedItems.All(i => i.CanBeScaled(Axes.X));
SelectionBox.CanFlipY = SelectedItems.All(i => i.CanBeScaled(Axes.Y));
SelectionBox.CanReverse = false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ protected override void LoadComplete()

private void updateState()
{
CanRotateAroundSelectionOrigin.Value = selectedItems.Count > 0;
CanRotateAroundSelectionOrigin.Value = selectedItems.Count > 0 && selectedItems.All(i => i.CanBeRotated);
}

private Drawable[]? objectsInRotation;
Expand Down
Loading
Loading