diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 5db502804d..4111a67b24 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using JetBrains.Annotations; @@ -170,6 +171,39 @@ namespace osu.Game.Configuration private static readonly ConcurrentDictionary property_info_cache = new ConcurrentDictionary(); + /// + /// Returns the underlying value of the given mod setting object. + /// Can be used for serialization and equality comparison purposes. + /// + /// A bindable. + public static object GetUnderlyingSettingValue(this object setting) + { + switch (setting) + { + case Bindable d: + return d.Value; + + case Bindable i: + return i.Value; + + case Bindable f: + return f.Value; + + case Bindable b: + return b.Value; + + case IBindable u: + // An unknown (e.g. enum) generic type. + var valueMethod = u.GetType().GetProperty(nameof(IBindable.Value)); + Debug.Assert(valueMethod != null); + return valueMethod.GetValue(u); + + default: + // fall back for non-bindable cases. + return setting; + } + } + public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetSettingsSourceProperties(this object obj) { var type = obj.GetType(); diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs index 005804789e..d1aba2bfe3 100644 --- a/osu.Game/Extensions/DrawableExtensions.cs +++ b/osu.Game/Extensions/DrawableExtensions.cs @@ -1,8 +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 Humanizer; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osuTK; @@ -59,8 +62,18 @@ namespace osu.Game.Extensions component.Origin = info.Origin; if (component is ISkinnableDrawable skinnable) + { skinnable.UsesFixedAnchor = info.UsesFixedAnchor; + foreach (var (_, property) in component.GetSettingsSourceProperties()) + { + if (!info.Settings.TryGetValue(property.Name.Underscore(), out object settingValue)) + continue; + + skinnable.CopyAdjustedSetting((IBindable)property.GetValue(component), settingValue); + } + } + if (component is Container container) { foreach (var child in info.Children) diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index 44b1c460d5..524f7b7108 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -12,7 +12,6 @@ using osu.Framework.Logging; using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Utils; namespace osu.Game.Online.API { @@ -43,7 +42,7 @@ namespace osu.Game.Online.API var bindable = (IBindable)property.GetValue(mod); if (!bindable.IsDefault) - Settings.Add(property.Name.Underscore(), ModUtils.GetSettingUnderlyingValue(bindable)); + Settings.Add(property.Name.Underscore(), bindable.GetUnderlyingSettingValue()); } } @@ -93,13 +92,13 @@ namespace osu.Game.Online.API public bool Equals(KeyValuePair x, KeyValuePair y) { - object xValue = ModUtils.GetSettingUnderlyingValue(x.Value); - object yValue = ModUtils.GetSettingUnderlyingValue(y.Value); + object xValue = x.Value.GetUnderlyingSettingValue(); + object yValue = y.Value.GetUnderlyingSettingValue(); return x.Key == y.Key && EqualityComparer.Default.Equals(xValue, yValue); } - public int GetHashCode(KeyValuePair obj) => HashCode.Combine(obj.Key, ModUtils.GetSettingUnderlyingValue(obj.Value)); + public int GetHashCode(KeyValuePair obj) => HashCode.Combine(obj.Key, obj.Value.GetUnderlyingSettingValue()); } } } diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs index 81ecc74ddb..a7c63c17f9 100644 --- a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs +++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Text; using MessagePack; using MessagePack.Formatters; -using osu.Game.Utils; +using osu.Game.Configuration; namespace osu.Game.Online.API { @@ -23,7 +23,7 @@ namespace osu.Game.Online.API var stringBytes = new ReadOnlySequence(Encoding.UTF8.GetBytes(kvp.Key)); writer.WriteString(in stringBytes); - primitiveFormatter.Serialize(ref writer, ModUtils.GetSettingUnderlyingValue(kvp.Value), options); + primitiveFormatter.Serialize(ref writer, kvp.Value.GetUnderlyingSettingValue(), options); } } diff --git a/osu.Game/Rulesets/Edit/ScrollingToolboxGroup.cs b/osu.Game/Rulesets/Edit/ScrollingToolboxGroup.cs index 9998a997b3..98e026c49a 100644 --- a/osu.Game/Rulesets/Edit/ScrollingToolboxGroup.cs +++ b/osu.Game/Rulesets/Edit/ScrollingToolboxGroup.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Edit { protected readonly OsuScrollContainer Scroll; + protected readonly FillFlowContainer FillFlow; + protected override Container Content { get; } public ScrollingToolboxGroup(string title, float scrollAreaHeight) @@ -20,7 +22,7 @@ namespace osu.Game.Rulesets.Edit { RelativeSizeAxes = Axes.X, Height = scrollAreaHeight, - Child = Content = new FillFlowContainer + Child = Content = FillFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 7136795461..b2d4be54ce 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Mods hashCode.Add(GetType()); foreach (var setting in Settings) - hashCode.Add(ModUtils.GetSettingUnderlyingValue(setting)); + hashCode.Add(setting.GetUnderlyingSettingValue()); return hashCode.ToHashCode(); } @@ -208,13 +208,13 @@ namespace osu.Game.Rulesets.Mods public bool Equals(IBindable x, IBindable y) { - object xValue = x == null ? null : ModUtils.GetSettingUnderlyingValue(x); - object yValue = y == null ? null : ModUtils.GetSettingUnderlyingValue(y); + object xValue = x?.GetUnderlyingSettingValue(); + object yValue = y?.GetUnderlyingSettingValue(); return EqualityComparer.Default.Equals(xValue, yValue); } - public int GetHashCode(IBindable obj) => ModUtils.GetSettingUnderlyingValue(obj).GetHashCode(); + public int GetHashCode(IBindable obj) => obj.GetUnderlyingSettingValue().GetHashCode(); } } } diff --git a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs index a2b84c79af..95395f8181 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableInfo.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableInfo.cs @@ -4,9 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; +using Humanizer; using Newtonsoft.Json; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Skinning; using osuTK; @@ -34,6 +37,8 @@ namespace osu.Game.Screens.Play.HUD /// public bool UsesFixedAnchor { get; set; } + public Dictionary Settings { get; set; } = new Dictionary(); + public List Children { get; } = new List(); [JsonConstructor] @@ -58,6 +63,14 @@ namespace osu.Game.Screens.Play.HUD if (component is ISkinnableDrawable skinnable) UsesFixedAnchor = skinnable.UsesFixedAnchor; + foreach (var (_, property) in component.GetSettingsSourceProperties()) + { + var bindable = (IBindable)property.GetValue(component); + + if (!bindable.IsDefault) + Settings.Add(property.Name.Underscore(), bindable.GetUnderlyingSettingValue()); + } + if (component is Container container) { foreach (var child in container.OfType().OfType()) diff --git a/osu.Game/Skinning/Components/BigBlackBox.cs b/osu.Game/Skinning/Components/BigBlackBox.cs new file mode 100644 index 0000000000..373e6467e8 --- /dev/null +++ b/osu.Game/Skinning/Components/BigBlackBox.cs @@ -0,0 +1,92 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Skinning.Components +{ + /// + /// Intended to be a test bed for skinning. May be removed at some point in the future. + /// + [UsedImplicitly] + public class BigBlackBox : CompositeDrawable, ISkinnableDrawable + { + public bool UsesFixedAnchor { get; set; } + + [SettingSource("Spinning text", "Whether the big text should spin")] + public Bindable TextSpin { get; } = new BindableBool(); + + [SettingSource("Alpha", "The alpha value of this box")] + public BindableNumber BoxAlpha { get; } = new BindableNumber(1) + { + MinValue = 0, + MaxValue = 1, + Precision = 0.01f, + }; + + private readonly Box box; + private readonly OsuSpriteText text; + private readonly OsuTextFlowContainer disclaimer; + + public BigBlackBox() + { + Size = new Vector2(250); + + Masking = true; + CornerRadius = 20; + CornerExponent = 5; + + InternalChildren = new Drawable[] + { + box = new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Text = "Big Black Box", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Default.With(size: 40) + }, + disclaimer = new OsuTextFlowContainer(st => st.Font = OsuFont.Default.With(size: 10)) + { + Text = "This is intended to be a test component and may disappear in the future!", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding(10), + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + TextAnchor = Anchor.TopCentre, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + BoxAlpha.BindValueChanged(alpha => box.Alpha = alpha.NewValue, true); + TextSpin.BindValueChanged(spin => + { + if (spin.NewValue) + text.Spin(1000, RotationDirection.Clockwise); + else + text.ClearTransforms(); + }, true); + + disclaimer.FadeOutFromOne(5000, Easing.InQuint); + } + } +} diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 459fbf3be5..ef26682c03 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -11,10 +12,12 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Testing; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Components.Menus; namespace osu.Game.Skinning.Editor @@ -44,6 +47,8 @@ namespace osu.Game.Skinning.Editor private Container content; + private EditorToolboxGroup settingsToolbox; + public SkinEditor(Drawable targetScreen) { RelativeSizeAxes = Axes.Both; @@ -103,7 +108,8 @@ namespace osu.Game.Skinning.Editor ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize), - new Dimension() + new Dimension(), + new Dimension(GridSizeMode.AutoSize), }, Content = new[] { @@ -119,6 +125,11 @@ namespace osu.Game.Skinning.Editor { RelativeSizeAxes = Axes.Both, }, + settingsToolbox = new SkinSettingsToolbox + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + } } } } @@ -143,12 +154,15 @@ namespace osu.Game.Skinning.Editor hasBegunMutating = false; Scheduler.AddOnce(skinChanged); }, true); + + SelectedComponents.BindCollectionChanged(selectionChanged); } public void UpdateTargetScreen(Drawable targetScreen) { this.targetScreen = targetScreen; + SelectedComponents.Clear(); Scheduler.AddOnce(loadBlueprintContainer); void loadBlueprintContainer() @@ -210,6 +224,18 @@ namespace osu.Game.Skinning.Editor SelectedComponents.Add(component); } + private void selectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + settingsToolbox.Clear(); + + var first = SelectedComponents.OfType().FirstOrDefault(); + + if (first != null) + { + settingsToolbox.Children = first.CreateSettingsControls().ToArray(); + } + } + private IEnumerable availableTargets => targetScreen.ChildrenOfType(); private ISkinnableTarget getTarget(SkinnableTarget target) diff --git a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs index abd8272633..08cdbf0aa9 100644 --- a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs +++ b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs @@ -101,9 +101,11 @@ namespace osu.Game.Skinning.Editor private void editorVisibilityChanged(ValueChangedEvent visibility) { + const float toolbar_padding_requirement = 0.18f; + if (visibility.NewValue == Visibility.Visible) { - target.SetCustomRect(new RectangleF(0.18f, 0.1f, VISIBLE_TARGET_SCALE, VISIBLE_TARGET_SCALE), true); + target.SetCustomRect(new RectangleF(toolbar_padding_requirement, 0.1f, 0.8f - toolbar_padding_requirement, 0.7f), true); } else { diff --git a/osu.Game/Skinning/Editor/SkinSettingsToolbox.cs b/osu.Game/Skinning/Editor/SkinSettingsToolbox.cs new file mode 100644 index 0000000000..c0ef8e7316 --- /dev/null +++ b/osu.Game/Skinning/Editor/SkinSettingsToolbox.cs @@ -0,0 +1,23 @@ +// 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; +using osu.Game.Rulesets.Edit; +using osuTK; + +namespace osu.Game.Skinning.Editor +{ + internal class SkinSettingsToolbox : ScrollingToolboxGroup + { + public const float WIDTH = 200; + + public SkinSettingsToolbox() + : base("Settings", 600) + { + RelativeSizeAxes = Axes.None; + Width = WIDTH; + + FillFlow.Spacing = new Vector2(10); + } + } +} diff --git a/osu.Game/Skinning/ISkinnableDrawable.cs b/osu.Game/Skinning/ISkinnableDrawable.cs index 60b40982e5..3fc6a2fdd8 100644 --- a/osu.Game/Skinning/ISkinnableDrawable.cs +++ b/osu.Game/Skinning/ISkinnableDrawable.cs @@ -1,6 +1,9 @@ // 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 osu.Framework.Bindables; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; namespace osu.Game.Skinning @@ -21,5 +24,22 @@ namespace osu.Game.Skinning /// If , a fixed anchor point has been defined. /// bool UsesFixedAnchor { get; set; } + + void CopyAdjustedSetting(IBindable target, object source) + { + if (source is IBindable sourceBindable) + { + // copy including transfer of default values. + target.BindTo(sourceBindable); + target.UnbindFrom(sourceBindable); + } + else + { + if (!(target is IParseable parseable)) + throw new InvalidOperationException($"Bindable type {target.GetType().ReadableName()} is not {nameof(IParseable)}."); + + parseable.Parse(source); + } + } } } diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index cdca277dd8..d5ea74c404 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -1,18 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -#nullable enable - namespace osu.Game.Utils { /// @@ -154,39 +152,6 @@ namespace osu.Game.Utils yield return mod; } - /// - /// Returns the underlying value of the given mod setting object. - /// Used in for serialization and equality comparison purposes. - /// - /// The mod setting. - public static object GetSettingUnderlyingValue(object setting) - { - switch (setting) - { - case Bindable d: - return d.Value; - - case Bindable i: - return i.Value; - - case Bindable f: - return f.Value; - - case Bindable b: - return b.Value; - - case IBindable u: - // A mod with unknown (e.g. enum) generic type. - var valueMethod = u.GetType().GetProperty(nameof(IBindable.Value)); - Debug.Assert(valueMethod != null); - return valueMethod.GetValue(u); - - default: - // fall back for non-bindable cases. - return setting; - } - } - /// /// Verifies all proposed mods are valid for a given ruleset and returns instantiated s for further processing. ///