From 59b2f028289521f6eef68456662ca9255913287d Mon Sep 17 00:00:00 2001 From: LeNitrous Date: Tue, 8 Oct 2019 18:34:09 +0800 Subject: [PATCH 01/13] initial implementation of customizable mods --- osu.Game/Overlays/Mods/ModControlSection.cs | 56 ++++++++++++++++++++ osu.Game/Overlays/Mods/ModSelectOverlay.cs | 58 +++++++++++++++++++++ osu.Game/Rulesets/Mods/IModHasSettings.cs | 15 ++++++ osu.Game/Rulesets/Mods/Mod.cs | 2 +- 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Overlays/Mods/ModControlSection.cs create mode 100644 osu.Game/Rulesets/Mods/IModHasSettings.cs diff --git a/osu.Game/Overlays/Mods/ModControlSection.cs b/osu.Game/Overlays/Mods/ModControlSection.cs new file mode 100644 index 0000000000..df6f4d15b0 --- /dev/null +++ b/osu.Game/Overlays/Mods/ModControlSection.cs @@ -0,0 +1,56 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Mods; +using osuTK; + +namespace osu.Game.Overlays.Mods +{ + public class ModControlSection : Container + { + protected FillFlowContainer FlowContent; + protected override Container Content => FlowContent; + + public readonly Mod Mod; + + public ModControlSection(Mod mod) + { + Mod = mod; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + FlowContent = new FillFlowContainer + { + Margin = new MarginPadding { Top = 30 }, + Spacing = new Vector2(0, 5), + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }; + + if (Mod is IModHasSettings modHasSettings) + AddRange(modHasSettings.CreateControls()); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AddRangeInternal(new Drawable[] + { + new OsuSpriteText + { + Text = Mod.Name, + Font = OsuFont.GetFont(weight: FontWeight.Bold), + Colour = colours.Yellow, + }, + FlowContent + }); + } + } +} \ No newline at end of file diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 9ff320841a..5493080964 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -31,6 +31,7 @@ namespace osu.Game.Overlays.Mods public class ModSelectOverlay : WaveOverlayContainer { protected readonly TriangleButton DeselectAllButton; + protected readonly TriangleButton CustomizeButton; protected readonly TriangleButton CloseButton; protected readonly OsuSpriteText MultiplierLabel; @@ -42,6 +43,10 @@ namespace osu.Game.Overlays.Mods protected readonly FillFlowContainer ModSectionsContainer; + protected readonly FillFlowContainer ModSettingsContent; + + protected readonly Container ModSettingsContainer; + protected readonly Bindable> SelectedMods = new Bindable>(Array.Empty()); protected readonly IBindable Ruleset = new Bindable(); @@ -226,6 +231,16 @@ namespace osu.Game.Overlays.Mods Right = 20 } }, + CustomizeButton = new TriangleButton + { + Width = 180, + Text = "Customization", + Action = () => ModSettingsContainer.Alpha = ModSettingsContainer.Alpha == 1 ? 0 : 1, + Margin = new MarginPadding + { + Right = 20 + } + }, CloseButton = new TriangleButton { Width = 180, @@ -271,6 +286,36 @@ namespace osu.Game.Overlays.Mods }, }, }, + ModSettingsContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Width = 0.25f, + Alpha = 0, + X = -100, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = new Color4(0, 0, 0, 192) + }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = ModSettingsContent = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 10f), + Padding = new MarginPadding(20), + } + } + } + } }; } @@ -284,10 +329,23 @@ namespace osu.Game.Overlays.Mods Ruleset.BindTo(ruleset); if (mods != null) SelectedMods.BindTo(mods); + SelectedMods.ValueChanged += updateModSettings; + sampleOn = audio.Samples.Get(@"UI/check-on"); sampleOff = audio.Samples.Get(@"UI/check-off"); } + private void updateModSettings(ValueChangedEvent> selectedMods) + { + var added = selectedMods.NewValue.Except(selectedMods.OldValue).FirstOrDefault(); + var removed = selectedMods.OldValue.Except(selectedMods.NewValue).FirstOrDefault(); + + if (added is IModHasSettings) + ModSettingsContent.Add(new ModControlSection(added)); + else if (removed is IModHasSettings) + ModSettingsContent.Remove(ModSettingsContent.Children.Where(section => section.Mod == removed).FirstOrDefault()); + } + public void DeselectAll() { foreach (var section in ModSectionsContainer.Children) diff --git a/osu.Game/Rulesets/Mods/IModHasSettings.cs b/osu.Game/Rulesets/Mods/IModHasSettings.cs new file mode 100644 index 0000000000..004279f71d --- /dev/null +++ b/osu.Game/Rulesets/Mods/IModHasSettings.cs @@ -0,0 +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 osu.Framework.Graphics; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// An interface for mods that allows user control over it's properties. + /// + public interface IModHasSettings + { + Drawable[] CreateControls(); + } +} diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 023d37497a..1c280c820d 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Mods /// /// Creates a copy of this initialised to a default state. /// - public virtual Mod CreateCopy() => (Mod)Activator.CreateInstance(GetType()); + public virtual Mod CreateCopy() => (Mod)MemberwiseClone(); public bool Equals(IMod other) => GetType() == other?.GetType(); } From 9375ef5eeae16c551b9e950becbb148a1a782dfd Mon Sep 17 00:00:00 2001 From: LeNitrous Date: Tue, 8 Oct 2019 19:42:15 +0800 Subject: [PATCH 02/13] clear settings controls when changing rulesets --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 5493080964..a344bcc254 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -330,6 +330,7 @@ namespace osu.Game.Overlays.Mods if (mods != null) SelectedMods.BindTo(mods); SelectedMods.ValueChanged += updateModSettings; + Ruleset.ValueChanged += _ => ModSettingsContent.Clear(); sampleOn = audio.Samples.Get(@"UI/check-on"); sampleOff = audio.Samples.Get(@"UI/check-off"); From 2bc6932567548187d6696f4900ae98cfd1eaddc9 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 5 Nov 2019 00:55:55 +0800 Subject: [PATCH 03/13] make interface mod applicable --- osu.Game/Rulesets/Mods/IModHasSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/IModHasSettings.cs b/osu.Game/Rulesets/Mods/IModHasSettings.cs index 004279f71d..b5058de82b 100644 --- a/osu.Game/Rulesets/Mods/IModHasSettings.cs +++ b/osu.Game/Rulesets/Mods/IModHasSettings.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Mods /// /// An interface for mods that allows user control over it's properties. /// - public interface IModHasSettings + public interface IModHasSettings : IApplicableMod { Drawable[] CreateControls(); } From a92b32f6dc94a164e5a2502e5f070240673f555e Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 5 Nov 2019 00:56:09 +0800 Subject: [PATCH 04/13] add basic tests --- .../TestSceneModCustomizationSettings.cs | 108 ++++++++++++++++++ osu.Game/Overlays/Mods/ModSelectOverlay.cs | 8 +- 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs new file mode 100644 index 0000000000..ec5b3c1e16 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs @@ -0,0 +1,108 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Mods; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneModCustomizationSettings : OsuTestScene + { + private TestModSelectOverlay modSelect; + + [BackgroundDependencyLoader] + private void load() + { + Add(modSelect = new TestModSelectOverlay + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + }); + + var testMod = new TestModCustomizable(); + + AddStep("open", modSelect.Show); + AddAssert("button disabled", () => !modSelect.CustomizeButton.Enabled.Value); + AddStep("select mod", () => modSelect.SelectMod(testMod)); + AddAssert("button enabled", () => modSelect.CustomizeButton.Enabled.Value); + AddStep("open customization", () => modSelect.CustomizeButton.Click()); + AddAssert("controls exist", () => modSelect.GetControlSection(testMod) != null); + AddStep("deselect mod", () => modSelect.SelectMod(testMod)); + AddAssert("controls hidden", () => modSelect.ModSettingsContainer.Alpha == 0); + } + + private class TestModSelectOverlay : ModSelectOverlay + { + public new Container ModSettingsContainer => base.ModSettingsContainer; + public new TriangleButton CustomizeButton => base.CustomizeButton; + + public void SelectMod(Mod mod) => + ModSectionsContainer.Children.Single((s) => s.ModType == mod.Type) + .ButtonsContainer.OfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())).SelectNext(1); + + public ModControlSection GetControlSection(Mod mod) => + ModSettingsContent.Children.FirstOrDefault((s) => s.Mod == mod); + + protected override void LoadComplete() + { + base.LoadComplete(); + + foreach (var section in ModSectionsContainer) + if (section.ModType == ModType.Conversion) + section.Mods = new Mod[] { new TestModCustomizable() }; + else + section.Mods = new Mod[] { }; + } + } + + private class TestModCustomizable : Mod, IModHasSettings + { + public override string Name => "Customizable Mod"; + + public override string Acronym => "CM"; + + public override double ScoreMultiplier => 1.0; + + public override ModType Type => ModType.Conversion; + + public readonly BindableFloat sliderBindable = new BindableFloat + { + MinValue = 0, + MaxValue = 10, + }; + + public readonly BindableBool tickBindable = new BindableBool(); + + public Drawable[] CreateControls() + { + BindableFloat sliderControl = new BindableFloat(); + BindableBool tickControl = new BindableBool(); + + sliderControl.BindTo(sliderBindable); + tickControl.BindTo(tickBindable); + + return new Drawable[] + { + new SettingsSlider + { + LabelText = "Slider", + Bindable = sliderControl + }, + new SettingsCheckbox + { + LabelText = "Checkbox", + Bindable = tickControl + } + }; + } + } + } +} \ No newline at end of file diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index a344bcc254..fc5ed1b3f3 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -331,6 +331,7 @@ namespace osu.Game.Overlays.Mods SelectedMods.ValueChanged += updateModSettings; Ruleset.ValueChanged += _ => ModSettingsContent.Clear(); + CustomizeButton.Enabled.Value = false; sampleOn = audio.Samples.Get(@"UI/check-on"); sampleOff = audio.Samples.Get(@"UI/check-off"); @@ -344,7 +345,12 @@ namespace osu.Game.Overlays.Mods if (added is IModHasSettings) ModSettingsContent.Add(new ModControlSection(added)); else if (removed is IModHasSettings) - ModSettingsContent.Remove(ModSettingsContent.Children.Where(section => section.Mod == removed).FirstOrDefault()); + ModSettingsContent.Remove(ModSettingsContent.Children.Where(section => section.Mod == removed).Single()); + + CustomizeButton.Enabled.Value = ModSettingsContent.Children.Count > 0; + + if (ModSettingsContainer.Alpha == 1 && ModSettingsContent.Children.Count == 0) + ModSettingsContainer.Alpha = 0; } public void DeselectAll() From 15e85234e4d4b80df421bb1c9d8383b35c5aab10 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 15 Nov 2019 11:12:51 +0800 Subject: [PATCH 05/13] remove unecessary step --- .../Visual/UserInterface/TestSceneModCustomizationSettings.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs index ec5b3c1e16..83d77cccdb 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs @@ -34,7 +34,6 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("select mod", () => modSelect.SelectMod(testMod)); AddAssert("button enabled", () => modSelect.CustomizeButton.Enabled.Value); AddStep("open customization", () => modSelect.CustomizeButton.Click()); - AddAssert("controls exist", () => modSelect.GetControlSection(testMod) != null); AddStep("deselect mod", () => modSelect.SelectMod(testMod)); AddAssert("controls hidden", () => modSelect.ModSettingsContainer.Alpha == 0); } From af35df4077015c5b45ebb5b8f304e9bdc58702f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Dec 2019 15:42:11 +0900 Subject: [PATCH 06/13] Add multiple mod testing and update test code style --- .../TestSceneModCustomizationSettings.cs | 56 +++++++++++-------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs index 83d77cccdb..f789e60252 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.BottomCentre, }); - var testMod = new TestModCustomizable(); + var testMod = new TestModCustomizable1(); AddStep("open", modSelect.Show); AddAssert("button disabled", () => !modSelect.CustomizeButton.Enabled.Value); @@ -44,64 +44,74 @@ namespace osu.Game.Tests.Visual.UserInterface public new TriangleButton CustomizeButton => base.CustomizeButton; public void SelectMod(Mod mod) => - ModSectionsContainer.Children.Single((s) => s.ModType == mod.Type) - .ButtonsContainer.OfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())).SelectNext(1); + ModSectionsContainer.Children.Single(s => s.ModType == mod.Type) + .ButtonsContainer.OfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())).SelectNext(1); public ModControlSection GetControlSection(Mod mod) => - ModSettingsContent.Children.FirstOrDefault((s) => s.Mod == mod); + ModSettingsContent.Children.FirstOrDefault(s => s.Mod == mod); protected override void LoadComplete() { base.LoadComplete(); foreach (var section in ModSectionsContainer) + { if (section.ModType == ModType.Conversion) - section.Mods = new Mod[] { new TestModCustomizable() }; + { + section.Mods = new Mod[] + { + new TestModCustomizable1(), + new TestModCustomizable2() + }; + } else section.Mods = new Mod[] { }; + } } } - private class TestModCustomizable : Mod, IModHasSettings + private class TestModCustomizable1 : TestModCustomizable { - public override string Name => "Customizable Mod"; + public override string Name => "Customizable Mod 1"; - public override string Acronym => "CM"; + public override string Acronym => "CM1"; + } + private class TestModCustomizable2 : TestModCustomizable + { + public override string Name => "Customizable Mod 2"; + + public override string Acronym => "CM2"; + } + + private abstract class TestModCustomizable : Mod, IModHasSettings + { public override double ScoreMultiplier => 1.0; public override ModType Type => ModType.Conversion; - public readonly BindableFloat sliderBindable = new BindableFloat + public readonly BindableFloat SliderBindable = new BindableFloat { MinValue = 0, MaxValue = 10, }; - public readonly BindableBool tickBindable = new BindableBool(); + public readonly BindableBool TickBindable = new BindableBool(); - public Drawable[] CreateControls() - { - BindableFloat sliderControl = new BindableFloat(); - BindableBool tickControl = new BindableBool(); - - sliderControl.BindTo(sliderBindable); - tickControl.BindTo(tickBindable); - - return new Drawable[] + public Drawable[] CreateControls() => + new Drawable[] { new SettingsSlider { LabelText = "Slider", - Bindable = sliderControl + Bindable = SliderBindable }, new SettingsCheckbox { LabelText = "Checkbox", - Bindable = tickControl + Bindable = TickBindable } }; - } } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index fc5ed1b3f3..d7d1135e81 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -345,7 +345,7 @@ namespace osu.Game.Overlays.Mods if (added is IModHasSettings) ModSettingsContent.Add(new ModControlSection(added)); else if (removed is IModHasSettings) - ModSettingsContent.Remove(ModSettingsContent.Children.Where(section => section.Mod == removed).Single()); + ModSettingsContent.Remove(ModSettingsContent.Children.Single(section => section.Mod == removed)); CustomizeButton.Enabled.Value = ModSettingsContent.Children.Count > 0; From 23e47530c50a38bd42874f85ec4c698b967f9e86 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Dec 2019 17:09:24 +0900 Subject: [PATCH 07/13] Add a method for getting settings UI components automatically from a target class --- .../Configuration/SettingSourceAttribute.cs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 osu.Game/Configuration/SettingSourceAttribute.cs diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs new file mode 100644 index 0000000000..fe582b1461 --- /dev/null +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -0,0 +1,85 @@ +// 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 System.Reflection; +using JetBrains.Annotations; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Configuration +{ + /// + /// An attribute to mark a bindable as being exposed to the user via settings controls. + /// Can be used in conjunction with to automatically create UI controls. + /// + [MeansImplicitUse] + [AttributeUsage(AttributeTargets.Property)] + public class SettingSourceAttribute : Attribute + { + public string Label { get; } + + public string Description { get; } + + public SettingSourceAttribute(string label, string description = null) + { + Label = label ?? string.Empty; + Description = description ?? string.Empty; + } + } + + public static class SettingSourceExtensions + { + public static IEnumerable CreateSettingsControls(this object obj) + { + var configProperties = obj.GetType().GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetCustomAttribute(true) != null); + + foreach (var property in configProperties) + { + var attr = property.GetCustomAttribute(true); + + switch (property.GetValue(obj)) + { + case BindableNumber bNumber: + yield return new SettingsSlider + { + LabelText = attr.Label, + Bindable = bNumber + }; + + break; + + case BindableNumber bNumber: + yield return new SettingsSlider + { + LabelText = attr.Label, + Bindable = bNumber + }; + + break; + + case BindableNumber bNumber: + yield return new SettingsSlider + { + LabelText = attr.Label, + Bindable = bNumber + }; + + break; + + case Bindable bBool: + yield return new SettingsCheckbox + { + LabelText = attr.Label, + Bindable = bBool + }; + + break; + } + } + } + } +} From a5d5099868f2c7d1a8672dc266030065366592c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Dec 2019 17:09:48 +0900 Subject: [PATCH 08/13] Use SettingsSource for mod cusomisation --- .../TestSceneModCustomizationSettings.cs | 30 +++++-------------- osu.Game/Overlays/Mods/ModControlSection.cs | 6 ++-- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 24 ++++++++------- osu.Game/Rulesets/Mods/IModHasSettings.cs | 15 ---------- 4 files changed, 25 insertions(+), 50 deletions(-) delete mode 100644 osu.Game/Rulesets/Mods/IModHasSettings.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs index f789e60252..8227fe8f7a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs @@ -6,9 +6,9 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods; -using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Mods; namespace osu.Game.Tests.Visual.UserInterface @@ -47,9 +47,6 @@ namespace osu.Game.Tests.Visual.UserInterface ModSectionsContainer.Children.Single(s => s.ModType == mod.Type) .ButtonsContainer.OfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())).SelectNext(1); - public ModControlSection GetControlSection(Mod mod) => - ModSettingsContent.Children.FirstOrDefault(s => s.Mod == mod); - protected override void LoadComplete() { base.LoadComplete(); @@ -84,34 +81,23 @@ namespace osu.Game.Tests.Visual.UserInterface public override string Acronym => "CM2"; } - private abstract class TestModCustomizable : Mod, IModHasSettings + private abstract class TestModCustomizable : Mod, IApplicableMod { public override double ScoreMultiplier => 1.0; public override ModType Type => ModType.Conversion; - public readonly BindableFloat SliderBindable = new BindableFloat + [SettingSource("Sample float", "Change something for a mod")] + public BindableFloat SliderBindable { get; } = new BindableFloat { MinValue = 0, MaxValue = 10, + Default = 5, + Value = 7 }; - public readonly BindableBool TickBindable = new BindableBool(); - - public Drawable[] CreateControls() => - new Drawable[] - { - new SettingsSlider - { - LabelText = "Slider", - Bindable = SliderBindable - }, - new SettingsCheckbox - { - LabelText = "Checkbox", - Bindable = TickBindable - } - }; + [SettingSource("Sample bool", "Clicking this changes a setting")] + public BindableBool TickBindable { get; } = new BindableBool(); } } } diff --git a/osu.Game/Overlays/Mods/ModControlSection.cs b/osu.Game/Overlays/Mods/ModControlSection.cs index df6f4d15b0..f4b588ddb3 100644 --- a/osu.Game/Overlays/Mods/ModControlSection.cs +++ b/osu.Game/Overlays/Mods/ModControlSection.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; @@ -34,8 +35,7 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.X, }; - if (Mod is IModHasSettings modHasSettings) - AddRange(modHasSettings.CreateControls()); + AddRange(Mod.CreateSettingsControls()); } [BackgroundDependencyLoader] @@ -53,4 +53,4 @@ namespace osu.Game.Overlays.Mods }); } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index d7d1135e81..cfad0126eb 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; @@ -236,6 +237,7 @@ namespace osu.Game.Overlays.Mods Width = 180, Text = "Customization", Action = () => ModSettingsContainer.Alpha = ModSettingsContainer.Alpha == 1 ? 0 : 1, + Enabled = { Value = false }, Margin = new MarginPadding { Right = 20 @@ -331,7 +333,6 @@ namespace osu.Game.Overlays.Mods SelectedMods.ValueChanged += updateModSettings; Ruleset.ValueChanged += _ => ModSettingsContent.Clear(); - CustomizeButton.Enabled.Value = false; sampleOn = audio.Samples.Get(@"UI/check-on"); sampleOff = audio.Samples.Get(@"UI/check-off"); @@ -339,18 +340,21 @@ namespace osu.Game.Overlays.Mods private void updateModSettings(ValueChangedEvent> selectedMods) { - var added = selectedMods.NewValue.Except(selectedMods.OldValue).FirstOrDefault(); - var removed = selectedMods.OldValue.Except(selectedMods.NewValue).FirstOrDefault(); + foreach (var added in selectedMods.NewValue.Except(selectedMods.OldValue)) + { + var controls = added.CreateSettingsControls().ToList(); + if (controls.Count > 0) + ModSettingsContent.Add(new ModControlSection(added) { Children = controls }); + } - if (added is IModHasSettings) - ModSettingsContent.Add(new ModControlSection(added)); - else if (removed is IModHasSettings) - ModSettingsContent.Remove(ModSettingsContent.Children.Single(section => section.Mod == removed)); + foreach (var removed in selectedMods.OldValue.Except(selectedMods.NewValue)) + ModSettingsContent.RemoveAll(section => section.Mod == removed); - CustomizeButton.Enabled.Value = ModSettingsContent.Children.Count > 0; + bool hasSettings = ModSettingsContent.Children.Count > 0; + CustomizeButton.Enabled.Value = hasSettings; - if (ModSettingsContainer.Alpha == 1 && ModSettingsContent.Children.Count == 0) - ModSettingsContainer.Alpha = 0; + if (!hasSettings) + ModSettingsContainer.Hide(); } public void DeselectAll() diff --git a/osu.Game/Rulesets/Mods/IModHasSettings.cs b/osu.Game/Rulesets/Mods/IModHasSettings.cs deleted file mode 100644 index b5058de82b..0000000000 --- a/osu.Game/Rulesets/Mods/IModHasSettings.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; - -namespace osu.Game.Rulesets.Mods -{ - /// - /// An interface for mods that allows user control over it's properties. - /// - public interface IModHasSettings : IApplicableMod - { - Drawable[] CreateControls(); - } -} From 46d055604a6f049735a6d2b0ed12d33ef5287dab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Dec 2019 18:57:11 +0900 Subject: [PATCH 09/13] Customize -> Customise --- ...ionSettings.cs => TestSceneModSettings.cs} | 26 +++++++++---------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 6 ++--- 2 files changed, 16 insertions(+), 16 deletions(-) rename osu.Game.Tests/Visual/UserInterface/{TestSceneModCustomizationSettings.cs => TestSceneModSettings.cs} (78%) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs similarity index 78% rename from osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs index 8227fe8f7a..7a11ba5294 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomizationSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneModCustomizationSettings : OsuTestScene + public class TestSceneModSettings : OsuTestScene { private TestModSelectOverlay modSelect; @@ -27,13 +27,13 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.BottomCentre, }); - var testMod = new TestModCustomizable1(); + var testMod = new TestModCustomisable1(); AddStep("open", modSelect.Show); - AddAssert("button disabled", () => !modSelect.CustomizeButton.Enabled.Value); + AddAssert("button disabled", () => !modSelect.CustomiseButton.Enabled.Value); AddStep("select mod", () => modSelect.SelectMod(testMod)); - AddAssert("button enabled", () => modSelect.CustomizeButton.Enabled.Value); - AddStep("open customization", () => modSelect.CustomizeButton.Click()); + AddAssert("button enabled", () => modSelect.CustomiseButton.Enabled.Value); + AddStep("open Customisation", () => modSelect.CustomiseButton.Click()); AddStep("deselect mod", () => modSelect.SelectMod(testMod)); AddAssert("controls hidden", () => modSelect.ModSettingsContainer.Alpha == 0); } @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.UserInterface private class TestModSelectOverlay : ModSelectOverlay { public new Container ModSettingsContainer => base.ModSettingsContainer; - public new TriangleButton CustomizeButton => base.CustomizeButton; + public new TriangleButton CustomiseButton => base.CustomiseButton; public void SelectMod(Mod mod) => ModSectionsContainer.Children.Single(s => s.ModType == mod.Type) @@ -57,8 +57,8 @@ namespace osu.Game.Tests.Visual.UserInterface { section.Mods = new Mod[] { - new TestModCustomizable1(), - new TestModCustomizable2() + new TestModCustomisable1(), + new TestModCustomisable2() }; } else @@ -67,21 +67,21 @@ namespace osu.Game.Tests.Visual.UserInterface } } - private class TestModCustomizable1 : TestModCustomizable + private class TestModCustomisable1 : TestModCustomisable { - public override string Name => "Customizable Mod 1"; + public override string Name => "Customisable Mod 1"; public override string Acronym => "CM1"; } - private class TestModCustomizable2 : TestModCustomizable + private class TestModCustomisable2 : TestModCustomisable { - public override string Name => "Customizable Mod 2"; + public override string Name => "Customisable Mod 2"; public override string Acronym => "CM2"; } - private abstract class TestModCustomizable : Mod, IApplicableMod + private abstract class TestModCustomisable : Mod, IApplicableMod { public override double ScoreMultiplier => 1.0; diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index cfad0126eb..e860463b23 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Mods public class ModSelectOverlay : WaveOverlayContainer { protected readonly TriangleButton DeselectAllButton; - protected readonly TriangleButton CustomizeButton; + protected readonly TriangleButton CustomiseButton; protected readonly TriangleButton CloseButton; protected readonly OsuSpriteText MultiplierLabel; @@ -232,7 +232,7 @@ namespace osu.Game.Overlays.Mods Right = 20 } }, - CustomizeButton = new TriangleButton + CustomiseButton = new TriangleButton { Width = 180, Text = "Customization", @@ -351,7 +351,7 @@ namespace osu.Game.Overlays.Mods ModSettingsContent.RemoveAll(section => section.Mod == removed); bool hasSettings = ModSettingsContent.Children.Count > 0; - CustomizeButton.Enabled.Value = hasSettings; + CustomiseButton.Enabled.Value = hasSettings; if (!hasSettings) ModSettingsContainer.Hide(); From 347373a3ba3ef852dabd35306721ff689b47b0af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Dec 2019 19:04:55 +0900 Subject: [PATCH 10/13] Fix test failures --- .../UserInterface/TestSceneModSettings.cs | 3 +++ osu.Game/Overlays/Mods/ModSection.cs | 22 +++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs index 7a11ba5294..435dfd92be 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs @@ -31,6 +31,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("open", modSelect.Show); AddAssert("button disabled", () => !modSelect.CustomiseButton.Enabled.Value); + AddUntilStep("wait for button load", () => modSelect.ButtonsLoaded); AddStep("select mod", () => modSelect.SelectMod(testMod)); AddAssert("button enabled", () => modSelect.CustomiseButton.Enabled.Value); AddStep("open Customisation", () => modSelect.CustomiseButton.Click()); @@ -43,6 +44,8 @@ namespace osu.Game.Tests.Visual.UserInterface public new Container ModSettingsContainer => base.ModSettingsContainer; public new TriangleButton CustomiseButton => base.CustomiseButton; + public bool ButtonsLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded); + public void SelectMod(Mod mod) => ModSectionsContainer.Children.Single(s => s.ModType == mod.Type) .ButtonsContainer.OfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())).SelectNext(1); diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 9c0a164ad6..c55d1d8f70 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -57,6 +57,15 @@ namespace osu.Game.Overlays.Mods }).ToArray(); modsLoadCts?.Cancel(); + + if (modContainers.Length == 0) + { + ModIconsLoaded = true; + headerLabel.Hide(); + Hide(); + return; + } + ModIconsLoaded = false; LoadComponentsAsync(modContainers, c => @@ -67,17 +76,8 @@ namespace osu.Game.Overlays.Mods buttons = modContainers.OfType().ToArray(); - if (value.Any()) - { - headerLabel.FadeIn(200); - this.FadeIn(200); - } - else - { - // transition here looks weird as mods instantly disappear. - headerLabel.Hide(); - Hide(); - } + headerLabel.FadeIn(200); + this.FadeIn(200); } } From f65866648e3e914a358ec4a1639d24d4862c01c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Dec 2019 20:14:56 +0900 Subject: [PATCH 11/13] Use Array.Empty --- osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs index 435dfd92be..fc44c5f595 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs @@ -1,6 +1,7 @@ // 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.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -65,7 +66,7 @@ namespace osu.Game.Tests.Visual.UserInterface }; } else - section.Mods = new Mod[] { }; + section.Mods = Array.Empty(); } } } From a37af311d010a1122d64ff5b8ca1c025a2ec3931 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Dec 2019 13:19:02 +0900 Subject: [PATCH 12/13] Simplify settings update logic --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 47 +++++++++++----------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index e860463b23..ec5f99dc31 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -331,32 +331,10 @@ namespace osu.Game.Overlays.Mods Ruleset.BindTo(ruleset); if (mods != null) SelectedMods.BindTo(mods); - SelectedMods.ValueChanged += updateModSettings; - Ruleset.ValueChanged += _ => ModSettingsContent.Clear(); - sampleOn = audio.Samples.Get(@"UI/check-on"); sampleOff = audio.Samples.Get(@"UI/check-off"); } - private void updateModSettings(ValueChangedEvent> selectedMods) - { - foreach (var added in selectedMods.NewValue.Except(selectedMods.OldValue)) - { - var controls = added.CreateSettingsControls().ToList(); - if (controls.Count > 0) - ModSettingsContent.Add(new ModControlSection(added) { Children = controls }); - } - - foreach (var removed in selectedMods.OldValue.Except(selectedMods.NewValue)) - ModSettingsContent.RemoveAll(section => section.Mod == removed); - - bool hasSettings = ModSettingsContent.Children.Count > 0; - CustomiseButton.Enabled.Value = hasSettings; - - if (!hasSettings) - ModSettingsContainer.Hide(); - } - public void DeselectAll() { foreach (var section in ModSectionsContainer.Children) @@ -450,12 +428,14 @@ namespace osu.Game.Overlays.Mods refreshSelectedMods(); } - private void selectedModsChanged(ValueChangedEvent> e) + private void selectedModsChanged(ValueChangedEvent> mods) { foreach (var section in ModSectionsContainer.Children) - section.SelectTypes(e.NewValue.Select(m => m.GetType()).ToList()); + section.SelectTypes(mods.NewValue.Select(m => m.GetType()).ToList()); updateMods(); + + updateModSettings(mods); } private void updateMods() @@ -480,6 +460,25 @@ namespace osu.Game.Overlays.Mods UnrankedLabel.FadeTo(ranked ? 0 : 1, 200); } + private void updateModSettings(ValueChangedEvent> selectedMods) + { + foreach (var added in selectedMods.NewValue.Except(selectedMods.OldValue)) + { + var controls = added.CreateSettingsControls().ToList(); + if (controls.Count > 0) + ModSettingsContent.Add(new ModControlSection(added) { Children = controls }); + } + + foreach (var removed in selectedMods.OldValue.Except(selectedMods.NewValue)) + ModSettingsContent.RemoveAll(section => section.Mod == removed); + + bool hasSettings = ModSettingsContent.Children.Count > 0; + CustomiseButton.Enabled.Value = hasSettings; + + if (!hasSettings) + ModSettingsContainer.Hide(); + } + private void modButtonPressed(Mod selectedMod) { if (selectedMod != null) From 5624b9fd3f981451db3c52a7da0c94bc56971a20 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Dec 2019 13:19:13 +0900 Subject: [PATCH 13/13] Fix US english --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index ec5f99dc31..e8ea43e3f2 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -235,7 +235,7 @@ namespace osu.Game.Overlays.Mods CustomiseButton = new TriangleButton { Width = 180, - Text = "Customization", + Text = "Customisation", Action = () => ModSettingsContainer.Alpha = ModSettingsContainer.Alpha == 1 ? 0 : 1, Enabled = { Value = false }, Margin = new MarginPadding