diff --git a/MiraAPI.Example/ExampleLocalSetings.cs b/MiraAPI.Example/ExampleLocalSetings.cs index a258967..21b68f0 100644 --- a/MiraAPI.Example/ExampleLocalSetings.cs +++ b/MiraAPI.Example/ExampleLocalSetings.cs @@ -2,6 +2,7 @@ using MiraAPI.LocalSettings; using MiraAPI.LocalSettings.Attributes; using MiraAPI.Utilities; +using Reactor.Utilities; using UnityEngine; namespace MiraAPI.Example; @@ -13,11 +14,13 @@ public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config) public override LocalSettingTabAppearance TabAppearance => new() { - TabColor = Color.cyan, ToggleActiveColor = Color.blue, ToggleInactiveColor = Color.red, }; + [LocalSettingsButton] + public LocalSettingsButton ExampleButton { get; private set; } = new("Example Button", OnExampleButtonClick); + [LocalToggleSetting] public ConfigEntry ExampleToggle { get; private set; } = config.Bind("General", "Example Bool", true); @@ -32,6 +35,11 @@ public class ExampleLocalSettings(ConfigFile config) : LocalSettingsTab(config) [LocalEnumSetting] public ConfigEntry ExampleEnum { get; private set; } = config.Bind("General", "Example Enum", ExampleEnumSetting.Fries); + + private static void OnExampleButtonClick(LocalSettingsTab instance) + { + Logger.Warning("Button clicked"); + } } public enum ExampleEnumSetting @@ -40,4 +48,4 @@ public enum ExampleEnumSetting Sandwich, Fries, ChickenNuggets, -} \ No newline at end of file +} diff --git a/MiraAPI/LocalSettings/Attributes/LocalSettingsButtonAttribute.cs b/MiraAPI/LocalSettings/Attributes/LocalSettingsButtonAttribute.cs new file mode 100644 index 0000000..043a81d --- /dev/null +++ b/MiraAPI/LocalSettings/Attributes/LocalSettingsButtonAttribute.cs @@ -0,0 +1,12 @@ +using System; +using BepInEx.Configuration; + +namespace MiraAPI.LocalSettings.Attributes; + +/// +/// Attribute for local setting buttons. +/// +[AttributeUsage(AttributeTargets.Property)] +public class LocalSettingsButtonAttribute : Attribute +{ +} diff --git a/MiraAPI/LocalSettings/LocalSettingsButton.cs b/MiraAPI/LocalSettings/LocalSettingsButton.cs new file mode 100644 index 0000000..2876dac --- /dev/null +++ b/MiraAPI/LocalSettings/LocalSettingsButton.cs @@ -0,0 +1,80 @@ +using System; +using MiraAPI.Utilities; +using Reactor.Localization.Utilities; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using Object = UnityEngine.Object; + +namespace MiraAPI.LocalSettings; + +/// +/// Represents a local settings button. +/// +public class LocalSettingsButton(string text, Action onClick) +{ + /// + /// Gets or sets the button text. + /// + public string Text { get; set; } = text; + + /// + /// Gets or sets the button on-click action. + /// + public Action OnClick { get; set; } = onClick; + + /// + /// Gets the tab instance this button belongs to. + /// + public LocalSettingsTab Tab { get; internal set; } + + internal GameObject CreateButton(ToggleButtonBehaviour toggle, Transform parent, ref float offset, ref int order, bool last) + { + var button = Object.Instantiate(toggle, parent).GetComponent(); + var tmp = button.transform.FindChild("Text_TMP").GetComponent(); + var rollover = button.GetComponent(); + tmp.GetComponent().Destroy(); + button.gameObject.SetActive(true); + + var toggleComp = button.GetComponent(); + var background = toggleComp.Background; + button.transform.FindChild("ButtonHighlight")?.gameObject.DestroyImmediate(); + toggleComp.Destroy(); + + if (last && order == 1) + { + // Button in the middle + button.transform.localPosition = new Vector3(0, 1.85f - offset, -7); + } + else + { + button.transform.localPosition = new Vector3(order == 1 ? -1.185f : 1.185f, 1.85f - offset, -7); + } + + tmp.text = Text; + button.name = Text; + button.OnClick = new UnityEngine.UI.Button.ButtonClickedEvent(); + rollover.OutColor = Tab!.TabAppearance.ButtonColor; + rollover.OverColor = Tab!.TabAppearance.ButtonHoverColor; + rollover.Target = background; + background.color = Tab!.TabAppearance.ButtonColor; + + var tab = Tab; + var click = OnClick; + button.OnClick.AddListener((UnityAction)(() => click(tab))); + + Helpers.DivideSize(button.gameObject, 1.1f); + + order++; + if (order > 2 && !last) + { + offset += 0.5f; + order = 1; + } + if (last) + offset += 0.6f; + + return button.gameObject; + } +} diff --git a/MiraAPI/LocalSettings/LocalSettingsTab.cs b/MiraAPI/LocalSettings/LocalSettingsTab.cs index 5516d05..7f30306 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTab.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTab.cs @@ -42,11 +42,32 @@ public abstract class LocalSettingsTab(ConfigFile config) /// public ConfigFile Config { get; } = config ?? throw new ArgumentNullException(nameof(config)); - internal TabGroup? TabButton { get; set; } - internal List Settings { get; } = []; + /// + /// Gets the tab button instance. + /// + public TabGroup? TabButton { get; internal set; } + + /// + /// Gets the list of settings. + /// + public List Settings { get; } = []; + + /// + /// Gets the list of buttons. + /// + public List Buttons { get; } = []; + private Scroller? Scroller { get; set; } - internal GameObject CreateTab(OptionsMenuBehaviour instance) + /// + /// Invoked when an option value is changed. + /// + /// The config entry which was changed. + public virtual void OnOptionChanged(ConfigEntryBase configEntry) + { + } + + public virtual GameObject CreateTab(OptionsMenuBehaviour instance) { var tab = Object.Instantiate(instance.transform.FindChild("GeneralTab").gameObject, instance.transform); tab.name = $"{TabName}Tab"; @@ -120,11 +141,40 @@ public abstract class LocalSettingsTab(ConfigFile config) contentOrder = 1; } - Scroller.SetBounds(new FloatRange(0, Settings.Count * 0.5f - 5f), new FloatRange(0, 0)); + contentIndex = 0; + contentOrder = 1; + foreach (var button in Buttons) + { + var obj = button.CreateButton( + toggle, + Scroller.Inner, + ref contentOffset, + ref contentOrder, + contentIndex == Buttons.Count); + + obj!.GetComponentsInChildren(true).Do(x => + { + x.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; + x.sortingOrder = 150; + x.sortingLayerName = "Default"; + }); + + obj.GetComponentsInChildren(true).Do(x => + { + x.sortingOrder = 151; + x.sortingLayerName = "Default"; + }); + + obj.GetComponentsInChildren().Do(x => { x.ClickMask = OptionsMenuPatches.MaskCollider; }); + + contentIndex++; + } + + Scroller.SetBounds(new FloatRange(0, (Settings.Count + Buttons.Count) * 0.5f - 5f), new FloatRange(0, 0)); return tab; } - internal GameObject CreateTabButton(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset) + public virtual GameObject CreateTabButton(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset) { var tabButtonObject = Object.Instantiate(instance.Tabs[0], instance.transform); tabButtonObject.name = $"{TabName} Button"; diff --git a/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs b/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs index 2cad514..26fd726 100644 --- a/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs +++ b/MiraAPI/LocalSettings/LocalSettingsTabAppearance.cs @@ -78,4 +78,14 @@ public record struct LocalSettingTabAppearance() /// Gets or sets hover color of enum buttons. /// public Color EnumHoverColor { get; set; } = Palette.AcceptedGreen; + + /// + /// Gets or sets color of buttons. + /// + public Color ButtonColor { get; set; } = Color.white; + + /// + /// Gets or sets hover color of buttons. + /// + public Color ButtonHoverColor { get; set; } = Palette.AcceptedGreen; } diff --git a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs index 7d2bc5b..f991376 100644 --- a/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs +++ b/MiraAPI/LocalSettings/SettingTypes/LocalSettingBase.cs @@ -68,5 +68,6 @@ public abstract class LocalSettingBase : ILocalSetting public void SetValue(T value) { ConfigEntry.BoxedValue = value; + Tab?.OnOptionChanged(ConfigEntry); } } diff --git a/MiraAPI/PluginLoading/MiraPluginManager.cs b/MiraAPI/PluginLoading/MiraPluginManager.cs index 27d8e92..0c987cd 100644 --- a/MiraAPI/PluginLoading/MiraPluginManager.cs +++ b/MiraAPI/PluginLoading/MiraPluginManager.cs @@ -344,6 +344,20 @@ public sealed class MiraPluginManager foreach (var property in type.GetProperties()) { + if (LocalSettingsManager.TypeToTab[type] is not { } tabInstance) + { + continue; + } + + if (property.GetCustomAttribute() != null && + property.PropertyType.IsAssignableTo(typeof(LocalSettingsButton))) + { + var button = property.GetValue(tabInstance) as LocalSettingsButton; + button!.Tab = tabInstance; + tabInstance.Buttons.Add(button); + continue; + } + if (!typeof(ConfigEntryBase).IsAssignableFrom(property.PropertyType)) { continue; @@ -355,11 +369,6 @@ public sealed class MiraPluginManager continue; } - if (LocalSettingsManager.TypeToTab[type] is not { } tabInstance) - { - continue; - } - if (property.GetValue(tabInstance) is not ConfigEntryBase configEntry) { Logger.Error($"Option property {property.Name} in {type.Name} has to be a config entry.");