diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs index 92d1cba2c2..bcd5579f03 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs @@ -1,12 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; 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.Containers; +using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -23,6 +26,12 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + [SetUpSteps] + public void SetUpSteps() + { + AddStep("reset selected mods", () => SelectedMods.SetDefault()); + } + [Test] public void TestVariousModPresets() { @@ -37,6 +46,78 @@ namespace osu.Game.Tests.Visual.UserInterface }); } + [Test] + public void TestPresetSelectionStateAfterExternalModChanges() + { + ModPresetPanel? panel = null; + + AddStep("create panel", () => Child = panel = new ModPresetPanel(createTestPresets().First().ToLiveUnmanaged()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f + }); + AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value); + + AddStep("set mods to HR", () => SelectedMods.Value = new[] { new OsuModHardRock() }); + AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value); + + AddStep("set mods to DT", () => SelectedMods.Value = new[] { new OsuModDoubleTime() }); + AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value); + + AddStep("set mods to HR+DT", () => SelectedMods.Value = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }); + AddAssert("panel is active", () => panel.AsNonNull().Active.Value); + + AddStep("set mods to HR+customised DT", () => SelectedMods.Value = new Mod[] + { + new OsuModHardRock(), + new OsuModDoubleTime + { + SpeedChange = { Value = 1.25 } + } + }); + AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value); + + AddStep("set mods to HR+DT", () => SelectedMods.Value = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }); + AddAssert("panel is active", () => panel.AsNonNull().Active.Value); + + AddStep("customise mod in place", () => SelectedMods.Value.OfType().Single().SpeedChange.Value = 1.33); + AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value); + + AddStep("set mods to HD+HR+DT", () => SelectedMods.Value = new Mod[] { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() }); + AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value); + } + + [Test] + public void TestActivatingPresetTogglesIncludedMods() + { + ModPresetPanel? panel = null; + + AddStep("create panel", () => Child = panel = new ModPresetPanel(createTestPresets().First().ToLiveUnmanaged()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f + }); + + AddStep("activate panel", () => panel.AsNonNull().TriggerClick()); + assertSelectedModsEquivalentTo(new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }); + + AddStep("deactivate panel", () => panel.AsNonNull().TriggerClick()); + assertSelectedModsEquivalentTo(Array.Empty()); + + AddStep("set different mod", () => SelectedMods.Value = new[] { new OsuModHidden() }); + AddStep("activate panel", () => panel.AsNonNull().TriggerClick()); + assertSelectedModsEquivalentTo(new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }); + + AddStep("set customised mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } }); + AddStep("activate panel", () => panel.AsNonNull().TriggerClick()); + assertSelectedModsEquivalentTo(new Mod[] { new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } }); + } + + private void assertSelectedModsEquivalentTo(IEnumerable mods) + => AddAssert("selected mods changed correctly", () => new HashSet(SelectedMods.Value).SetEquals(mods)); + private static IEnumerable createTestPresets() => new[] { new ModPreset diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index 49e7d55bca..a259645479 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -1,10 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; +using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; @@ -22,12 +26,19 @@ namespace osu.Game.Overlays.Mods [Resolved] private IDialogOverlay? dialogOverlay { get; set; } + [Resolved] + private Bindable> selectedMods { get; set; } = null!; + + private ModSettingChangeTracker? settingChangeTracker; + public ModPresetPanel(Live preset) { Preset = preset; Title = preset.Value.Name; Description = preset.Value.Description; + + Action = toggleRequestedByUser; } [BackgroundDependencyLoader] @@ -36,6 +47,37 @@ namespace osu.Game.Overlays.Mods AccentColour = colours.Orange1; } + protected override void LoadComplete() + { + base.LoadComplete(); + + selectedMods.BindValueChanged(_ => selectedModsChanged(), true); + } + + private void toggleRequestedByUser() + { + // if the preset is not active at the point of the user click, then set the mods using the preset directly, discarding any previous selections. + // if the preset is active when the user has clicked it, then it means that the set of active mods is exactly equal to the set of mods in the preset + // (there are no other active mods than what the preset specifies, and the mod settings match exactly). + // therefore it's safe to just clear selected mods, since it will have the effect of toggling the preset off. + selectedMods.Value = !Active.Value + ? Preset.Value.Mods.ToArray() + : Array.Empty(); + } + + private void selectedModsChanged() + { + settingChangeTracker?.Dispose(); + settingChangeTracker = new ModSettingChangeTracker(selectedMods.Value); + settingChangeTracker.SettingChanged = _ => updateActiveState(); + updateActiveState(); + } + + private void updateActiveState() + { + Active.Value = new HashSet(Preset.Value.Mods).SetEquals(selectedMods.Value); + } + #region IHasCustomTooltip public ModPreset TooltipContent => Preset.Value; @@ -51,5 +93,12 @@ namespace osu.Game.Overlays.Mods }; #endregion + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + settingChangeTracker?.Dispose(); + } } }