Skip to content

Commit 596b703

Browse files
authored
Merge pull request #32641 from bdach/skin-menu-checkboxes
Fix skin editor anchor/origin context menu ternary states not updating properly
2 parents 143593b + 005b204 commit 596b703

File tree

3 files changed

+131
-40
lines changed

3 files changed

+131
-40
lines changed

osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using osu.Framework.Input;
1616
using osu.Framework.Testing;
1717
using osu.Game.Database;
18+
using osu.Game.Graphics.UserInterface;
1819
using osu.Game.Overlays;
1920
using osu.Game.Overlays.Settings;
2021
using osu.Game.Overlays.SkinEditor;
@@ -458,6 +459,62 @@ public void TestMigrationLegacy()
458459
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyDefaultComboCounter>().Count() == 1);
459460
}
460461

462+
[Test]
463+
public void TestAnchorRadioButtonBehavior()
464+
{
465+
ISerialisableDrawable? selectedComponent = null;
466+
467+
AddStep("Select first component", () =>
468+
{
469+
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().First();
470+
skinEditor.SelectedComponents.Clear();
471+
skinEditor.SelectedComponents.Add(blueprint.Item);
472+
selectedComponent = blueprint.Item;
473+
});
474+
475+
AddStep("Right-click to open context menu", () =>
476+
{
477+
if (selectedComponent != null)
478+
InputManager.MoveMouseTo(((Drawable)selectedComponent).ScreenSpaceDrawQuad.Centre);
479+
InputManager.Click(MouseButton.Right);
480+
});
481+
482+
AddStep("Click on Anchor menu", () =>
483+
{
484+
InputManager.MoveMouseTo(getMenuItemByText("Anchor"));
485+
InputManager.Click(MouseButton.Left);
486+
});
487+
488+
AddStep("Right-click TopLeft anchor", () =>
489+
{
490+
InputManager.MoveMouseTo(getMenuItemByText("TopLeft"));
491+
InputManager.Click(MouseButton.Right);
492+
});
493+
494+
AddAssert("TopLeft item checked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
495+
496+
AddStep("Right-click Centre anchor", () =>
497+
{
498+
InputManager.MoveMouseTo(getMenuItemByText("Centre"));
499+
InputManager.Click(MouseButton.Right);
500+
});
501+
502+
AddAssert("Centre item checked", () => (getMenuItemByText("Centre").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
503+
AddAssert("TopLeft item unchecked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.False);
504+
505+
AddStep("Right-click Closest anchor", () =>
506+
{
507+
InputManager.MoveMouseTo(getMenuItemByText("Closest"));
508+
InputManager.Click(MouseButton.Right);
509+
});
510+
511+
AddAssert("Closest item checked", () => (getMenuItemByText("Closest").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
512+
AddAssert("Centre item unchecked", () => (getMenuItemByText("Centre").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.False);
513+
514+
Menu.DrawableMenuItem getMenuItemByText(string text)
515+
=> this.ChildrenOfType<Menu.DrawableMenuItem>().First(m => m.Item.Text.ToString() == text);
516+
}
517+
461518
private Skin importSkinFromArchives(string filename)
462519
{
463520
var imported = skins.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();

osu.Game/Overlays/SkinEditor/SkinEditor.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -760,11 +760,7 @@ public RevertConfirmDialog(Action revert)
760760

761761
#region Delegation of IEditorChangeHandler
762762

763-
public event Action? OnStateChange
764-
{
765-
add => throw new NotImplementedException();
766-
remove => throw new NotImplementedException();
767-
}
763+
public event Action? OnStateChange;
768764

769765
private IEditorChangeHandler? beginChangeHandler;
770766

@@ -773,6 +769,9 @@ public void BeginChange()
773769
// Change handler may change between begin and end, which can cause unbalanced operations.
774770
// Let's track the one that was used when beginning the change so we can call EndChange on it specifically.
775771
(beginChangeHandler = changeHandler)?.BeginChange();
772+
773+
if (beginChangeHandler != null)
774+
beginChangeHandler.OnStateChange += OnStateChange;
776775
}
777776

778777
public void EndChange() => beginChangeHandler?.EndChange();

osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ namespace osu.Game.Overlays.SkinEditor
2222
{
2323
public partial class SkinSelectionHandler : SelectionHandler<ISerialisableDrawable>
2424
{
25-
private OsuMenuItem originMenu = null!;
25+
private OsuMenuItem? originMenu;
26+
27+
private TernaryStateRadioMenuItem? closestAnchor;
28+
private AnchorMenuItem[]? fixedAnchors;
2629

2730
[Resolved]
2831
private SkinEditor skinEditor { get; set; } = null!;
@@ -44,6 +47,38 @@ public override SelectionScaleHandler CreateScaleHandler()
4447
return scaleHandler;
4548
}
4649

50+
protected override void LoadComplete()
51+
{
52+
base.LoadComplete();
53+
54+
if (ChangeHandler != null)
55+
ChangeHandler.OnStateChange += updateTernaryStates;
56+
SelectedItems.BindCollectionChanged((_, _) => updateTernaryStates());
57+
}
58+
59+
private void updateTernaryStates()
60+
{
61+
var usingClosestAnchor = GetStateFromSelection(SelectedBlueprints, c => !c.Item.UsesFixedAnchor);
62+
63+
if (closestAnchor != null)
64+
closestAnchor.State.Value = usingClosestAnchor;
65+
66+
if (fixedAnchors != null)
67+
{
68+
foreach (var fixedAnchor in fixedAnchors)
69+
fixedAnchor.State.Value = GetStateFromSelection(SelectedBlueprints, c => c.Item.UsesFixedAnchor && ((Drawable)c.Item).Anchor == fixedAnchor.Anchor);
70+
}
71+
72+
if (originMenu != null)
73+
{
74+
foreach (var origin in originMenu.Items.OfType<AnchorMenuItem>())
75+
{
76+
origin.State.Value = GetStateFromSelection(SelectedBlueprints, c => ((Drawable)c.Item).Origin == origin.Anchor);
77+
origin.Action.Disabled = usingClosestAnchor == TernaryState.True;
78+
}
79+
}
80+
}
81+
4782
public override bool HandleFlip(Direction direction, bool flipOverOrigin)
4883
{
4984
var selectionQuad = getSelectionQuad();
@@ -102,27 +137,17 @@ protected override void DeleteItems(IEnumerable<ISerialisableDrawable> items) =>
102137

103138
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<ISerialisableDrawable>> selection)
104139
{
105-
var closestItem = new TernaryStateRadioMenuItem(SkinEditorStrings.Closest, MenuItemType.Standard, _ => applyClosestAnchors())
106-
{
107-
State = { Value = GetStateFromSelection(selection, c => !c.Item.UsesFixedAnchor) }
108-
};
140+
closestAnchor = new TernaryStateRadioMenuItem(SkinEditorStrings.Closest, MenuItemType.Standard, _ => applyClosestAnchors());
141+
fixedAnchors = createAnchorItems(applyFixedAnchors).ToArray();
109142

110143
yield return new OsuMenuItem(SkinEditorStrings.Anchor)
111144
{
112-
Items = createAnchorItems((d, a) => d.UsesFixedAnchor && ((Drawable)d).Anchor == a, applyFixedAnchors)
113-
.Prepend(closestItem)
114-
.ToArray()
145+
Items = fixedAnchors.Prepend(closestAnchor).ToArray()
115146
};
116147

117148
yield return originMenu = new OsuMenuItem(SkinEditorStrings.Origin);
118149

119-
closestItem.State.BindValueChanged(s =>
120-
{
121-
// For UX simplicity, origin should only be user-editable when "closest" anchor mode is disabled.
122-
originMenu.Items = s.NewValue == TernaryState.True
123-
? Array.Empty<MenuItem>()
124-
: createAnchorItems((d, o) => ((Drawable)d).Origin == o, applyOrigins).ToArray();
125-
}, true);
150+
originMenu.Items = createAnchorItems(applyOrigins).ToArray();
126151

127152
yield return new OsuMenuItemSpacer();
128153

@@ -163,27 +188,37 @@ protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumer
163188
foreach (var item in base.GetContextMenuItemsForSelection(selection))
164189
yield return item;
165190

166-
IEnumerable<TernaryStateMenuItem> createAnchorItems(Func<ISerialisableDrawable, Anchor, bool> checkFunction, Action<Anchor> applyFunction)
191+
updateTernaryStates();
192+
}
193+
194+
private IEnumerable<AnchorMenuItem> createAnchorItems(Action<Anchor> applyFunction)
195+
{
196+
var displayableAnchors = new[]
167197
{
168-
var displayableAnchors = new[]
169-
{
170-
Anchor.TopLeft,
171-
Anchor.TopCentre,
172-
Anchor.TopRight,
173-
Anchor.CentreLeft,
174-
Anchor.Centre,
175-
Anchor.CentreRight,
176-
Anchor.BottomLeft,
177-
Anchor.BottomCentre,
178-
Anchor.BottomRight,
179-
};
180-
return displayableAnchors.Select(a =>
181-
{
182-
return new TernaryStateRadioMenuItem(a.ToString(), MenuItemType.Standard, _ => applyFunction(a))
183-
{
184-
State = { Value = GetStateFromSelection(selection, c => checkFunction(c.Item, a)) }
185-
};
186-
});
198+
Anchor.TopLeft,
199+
Anchor.TopCentre,
200+
Anchor.TopRight,
201+
Anchor.CentreLeft,
202+
Anchor.Centre,
203+
Anchor.CentreRight,
204+
Anchor.BottomLeft,
205+
Anchor.BottomCentre,
206+
Anchor.BottomRight,
207+
};
208+
return displayableAnchors.Select(a =>
209+
{
210+
return new AnchorMenuItem(a, _ => applyFunction(a));
211+
});
212+
}
213+
214+
private partial class AnchorMenuItem : TernaryStateRadioMenuItem
215+
{
216+
public readonly Anchor Anchor;
217+
218+
public AnchorMenuItem(Anchor anchor, Action<Anchor> applyFunction)
219+
: base(anchor.ToString(), MenuItemType.Standard, _ => applyFunction(anchor))
220+
{
221+
Anchor = anchor;
187222
}
188223
}
189224

0 commit comments

Comments
 (0)