Initial commit
This commit is contained in:
72
mod/PLibOptions/ButtonOptionsEntry.cs
Normal file
72
mod/PLibOptions/ButtonOptionsEntry.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.UI;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An options entry that displays a button. Not intended to be serializable to the
|
||||
/// options file, instead declare a read-only property that returns a handler method as
|
||||
/// an Action in the settings class, e.g:
|
||||
///
|
||||
/// [Option("Click Here!", "Button tool tip")]
|
||||
/// public System.Action<object> MyButton => Handler;
|
||||
///
|
||||
/// public void Handler() {
|
||||
/// // ...
|
||||
/// }
|
||||
/// </summary>
|
||||
public class ButtonOptionsEntry : OptionsEntry {
|
||||
public override object Value {
|
||||
get {
|
||||
return value;
|
||||
}
|
||||
set {
|
||||
if (value is Action<object> newValue)
|
||||
this.value = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The action to invoke when the button is pushed.
|
||||
/// </summary>
|
||||
private Action<object> value;
|
||||
|
||||
public ButtonOptionsEntry(string field, IOptionSpec spec) : base(field, spec) { }
|
||||
|
||||
public override void CreateUIEntry(PGridPanel parent, ref int row) {
|
||||
parent.AddChild(new PButton(Field) {
|
||||
Text = LookInStrings(Title), ToolTip = LookInStrings(Tooltip),
|
||||
OnClick = OnButtonClicked
|
||||
}.SetKleiPinkStyle(), new GridComponentSpec(row, 0) {
|
||||
Margin = CONTROL_MARGIN, Alignment = TextAnchor.MiddleCenter, ColumnSpan = 2
|
||||
});
|
||||
}
|
||||
|
||||
private void OnButtonClicked(GameObject _) {
|
||||
value?.Invoke(null);
|
||||
}
|
||||
|
||||
public override GameObject GetUIComponent() {
|
||||
// Will not be invoked
|
||||
return new GameObject("Empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
102
mod/PLibOptions/CategoryExpandHandler.cs
Normal file
102
mod/PLibOptions/CategoryExpandHandler.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// Handles events for expanding and contracting options categories.
|
||||
/// </summary>
|
||||
internal sealed class CategoryExpandHandler {
|
||||
/// <summary>
|
||||
/// The realized panel containing the options.
|
||||
/// </summary>
|
||||
private GameObject contents;
|
||||
|
||||
/// <summary>
|
||||
/// The initial state of the button.
|
||||
/// </summary>
|
||||
private readonly bool initialState;
|
||||
|
||||
/// <summary>
|
||||
/// The realized toggle button.
|
||||
/// </summary>
|
||||
private GameObject toggle;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new options category.
|
||||
/// </summary>
|
||||
/// <param name="initialState">true to start expanded, or false to start collapsed.</param>
|
||||
public CategoryExpandHandler(bool initialState = true) {
|
||||
this.initialState = initialState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the options category is expanded or contracted.
|
||||
/// </summary>
|
||||
/// <param name="on">true if the button is on, or false if it is off.</param>
|
||||
public void OnExpandContract(GameObject _, bool on) {
|
||||
var scale = on ? Vector3.one : Vector3.zero;
|
||||
if (contents != null) {
|
||||
var rt = contents.rectTransform();
|
||||
rt.localScale = scale;
|
||||
if (rt != null)
|
||||
UnityEngine.UI.LayoutRebuilder.MarkLayoutForRebuild(rt);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the header is clicked.
|
||||
/// </summary>
|
||||
private void OnHeaderClicked() {
|
||||
if (toggle != null) {
|
||||
bool state = PToggle.GetToggleState(toggle);
|
||||
PToggle.SetToggleState(toggle, !state);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the category label is realized.
|
||||
/// </summary>
|
||||
/// <param name="header">The realized header label of the category.</param>
|
||||
public void OnRealizeHeader(GameObject header) {
|
||||
var button = header.AddComponent<UnityEngine.UI.Button>();
|
||||
button.onClick.AddListener(new UnityAction(OnHeaderClicked));
|
||||
button.interactable = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the body is realized.
|
||||
/// </summary>
|
||||
/// <param name="panel">The realized body of the category.</param>
|
||||
public void OnRealizePanel(GameObject panel) {
|
||||
contents = panel;
|
||||
OnExpandContract(null, initialState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the toggle button is realized.
|
||||
/// </summary>
|
||||
/// <param name="toggle">The realized expand/contract button.</param>
|
||||
public void OnRealizeToggle(GameObject toggle) {
|
||||
this.toggle = toggle;
|
||||
}
|
||||
}
|
||||
}
|
||||
71
mod/PLibOptions/CheckboxOptionsEntry.cs
Normal file
71
mod/PLibOptions/CheckboxOptionsEntry.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An options entry which represents bool and displays a check box.
|
||||
/// </summary>
|
||||
public class CheckboxOptionsEntry : OptionsEntry {
|
||||
public override object Value {
|
||||
get {
|
||||
return check;
|
||||
}
|
||||
set {
|
||||
if (value is bool newCheck) {
|
||||
check = newCheck;
|
||||
Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// true if it is checked, or false otherwise
|
||||
/// </summary>
|
||||
private bool check;
|
||||
|
||||
/// <summary>
|
||||
/// The realized item checkbox.
|
||||
/// </summary>
|
||||
private GameObject checkbox;
|
||||
|
||||
public CheckboxOptionsEntry(string field, IOptionSpec spec) : base(field, spec) {
|
||||
check = false;
|
||||
checkbox = null;
|
||||
}
|
||||
|
||||
public override GameObject GetUIComponent() {
|
||||
checkbox = new PCheckBox() {
|
||||
OnChecked = (source, state) => {
|
||||
// Swap the check: checked and partial -> unchecked
|
||||
check = state == PCheckBox.STATE_UNCHECKED;
|
||||
Update();
|
||||
}, ToolTip = LookInStrings(Tooltip)
|
||||
}.SetKleiBlueStyle().Build();
|
||||
Update();
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
private void Update() {
|
||||
PCheckBox.SetCheckState(checkbox, check ? PCheckBox.STATE_CHECKED : PCheckBox.
|
||||
STATE_UNCHECKED);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
mod/PLibOptions/Color32OptionsEntry.cs
Normal file
38
mod/PLibOptions/Color32OptionsEntry.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An options entry which represents Color32 and displays a color picker with sliders.
|
||||
/// </summary>
|
||||
internal class Color32OptionsEntry : ColorBaseOptionsEntry {
|
||||
public override object Value {
|
||||
get => (Color32)value;
|
||||
set {
|
||||
if (value is Color32 newValue) {
|
||||
this.value = newValue;
|
||||
UpdateAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Color32OptionsEntry(string field, IOptionSpec spec) : base(field, spec) { }
|
||||
}
|
||||
}
|
||||
328
mod/PLibOptions/ColorBaseOptionsEntry.cs
Normal file
328
mod/PLibOptions/ColorBaseOptionsEntry.cs
Normal file
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using PeterHan.PLib.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// The abstract base of options entries that display a color picker with sliders.
|
||||
/// </summary>
|
||||
internal abstract class ColorBaseOptionsEntry : OptionsEntry {
|
||||
/// <summary>
|
||||
/// The margin between the color sliders and the rest of the dialog.
|
||||
/// </summary>
|
||||
protected static readonly RectOffset ENTRY_MARGIN = new RectOffset(10, 10, 2, 5);
|
||||
|
||||
/// <summary>
|
||||
/// The margin around each slider.
|
||||
/// </summary>
|
||||
protected static readonly RectOffset SLIDER_MARGIN = new RectOffset(10, 0, 2, 2);
|
||||
|
||||
/// <summary>
|
||||
/// The size of the sample swatch.
|
||||
/// </summary>
|
||||
protected const float SWATCH_SIZE = 32.0f;
|
||||
|
||||
/// <summary>
|
||||
/// The hue displayed gradient.
|
||||
/// </summary>
|
||||
protected ColorGradient hueGradient;
|
||||
|
||||
/// <summary>
|
||||
/// The hue slider.
|
||||
/// </summary>
|
||||
protected KSlider hueSlider;
|
||||
|
||||
/// <summary>
|
||||
/// The realized text field for BLUE.
|
||||
/// </summary>
|
||||
protected TMP_InputField blue;
|
||||
|
||||
/// <summary>
|
||||
/// The realized text field for GREEN.
|
||||
/// </summary>
|
||||
protected TMP_InputField green;
|
||||
|
||||
/// <summary>
|
||||
/// The realized text field for RED.
|
||||
/// </summary>
|
||||
protected TMP_InputField red;
|
||||
|
||||
/// <summary>
|
||||
/// The saturation displayed gradient.
|
||||
/// </summary>
|
||||
protected ColorGradient satGradient;
|
||||
|
||||
/// <summary>
|
||||
/// The saturation slider.
|
||||
/// </summary>
|
||||
protected KSlider satSlider;
|
||||
|
||||
/// <summary>
|
||||
/// The color sample swatch.
|
||||
/// </summary>
|
||||
protected Image swatch;
|
||||
|
||||
/// <summary>
|
||||
/// The value displayed gradient.
|
||||
/// </summary>
|
||||
protected ColorGradient valGradient;
|
||||
|
||||
/// <summary>
|
||||
/// The value slider.
|
||||
/// </summary>
|
||||
protected KSlider valSlider;
|
||||
|
||||
/// <summary>
|
||||
/// The value as a Color.
|
||||
/// </summary>
|
||||
protected Color value;
|
||||
|
||||
protected ColorBaseOptionsEntry(string field, IOptionSpec spec) : base(field, spec) {
|
||||
value = Color.white;
|
||||
blue = null;
|
||||
green = null;
|
||||
red = null;
|
||||
hueGradient = null; hueSlider = null;
|
||||
satGradient = null; satSlider = null;
|
||||
valGradient = null; valSlider = null;
|
||||
swatch = null;
|
||||
}
|
||||
|
||||
public override void CreateUIEntry(PGridPanel parent, ref int row) {
|
||||
base.CreateUIEntry(parent, ref row);
|
||||
// Add 3 rows for the H, S, and V
|
||||
parent.AddRow(new GridRowSpec());
|
||||
var h = new PSliderSingle("Hue") {
|
||||
ToolTip = PLibStrings.TOOLTIP_HUE, MinValue = 0.0f, MaxValue = 1.0f,
|
||||
CustomTrack = true, FlexSize = Vector2.right, OnValueChanged = OnHueChanged
|
||||
}.AddOnRealize(OnHueRealized);
|
||||
var s = new PSliderSingle("Saturation") {
|
||||
ToolTip = PLibStrings.TOOLTIP_SATURATION, MinValue = 0.0f, MaxValue = 1.0f,
|
||||
CustomTrack = true, FlexSize = Vector2.right, OnValueChanged = OnSatChanged
|
||||
}.AddOnRealize(OnSatRealized);
|
||||
var v = new PSliderSingle("Value") {
|
||||
ToolTip = PLibStrings.TOOLTIP_VALUE, MinValue = 0.0f, MaxValue = 1.0f,
|
||||
CustomTrack = true, FlexSize = Vector2.right, OnValueChanged = OnValChanged
|
||||
}.AddOnRealize(OnValRealized);
|
||||
var sw = new PLabel("Swatch") {
|
||||
ToolTip = LookInStrings(Tooltip), DynamicSize = false,
|
||||
Sprite = PUITuning.Images.BoxBorder, SpriteMode = Image.Type.Sliced,
|
||||
SpriteSize = new Vector2(SWATCH_SIZE, SWATCH_SIZE)
|
||||
}.AddOnRealize(OnSwatchRealized);
|
||||
var panel = new PRelativePanel("ColorPicker") {
|
||||
FlexSize = Vector2.right, DynamicSize = false
|
||||
}.AddChild(h).AddChild(s).AddChild(v).AddChild(sw).SetRightEdge(h, fraction: 1.0f).
|
||||
SetRightEdge(s, fraction: 1.0f).SetRightEdge(v, fraction: 1.0f).
|
||||
SetLeftEdge(sw, fraction: 0.0f).SetMargin(h, SLIDER_MARGIN).
|
||||
SetMargin(s, SLIDER_MARGIN).SetMargin(v, SLIDER_MARGIN).AnchorYAxis(sw).
|
||||
SetLeftEdge(h, toRight: sw).SetLeftEdge(s, toRight: sw).
|
||||
SetLeftEdge(v, toRight: sw).SetTopEdge(h, fraction: 1.0f).
|
||||
SetBottomEdge(v, fraction: 0.0f).SetTopEdge(s, below: h).
|
||||
SetTopEdge(v, below: s);
|
||||
parent.AddChild(panel, new GridComponentSpec(++row, 0) {
|
||||
ColumnSpan = 2, Margin = ENTRY_MARGIN
|
||||
});
|
||||
}
|
||||
|
||||
public override GameObject GetUIComponent() {
|
||||
Color32 rgb = value;
|
||||
var go = new PPanel("RGB") {
|
||||
DynamicSize = false, Alignment = TextAnchor.MiddleRight, Spacing = 5,
|
||||
Direction = PanelDirection.Horizontal
|
||||
}.AddChild(new PLabel("Red") {
|
||||
TextStyle = PUITuning.Fonts.TextLightStyle, Text = PLibStrings.LABEL_R
|
||||
}).AddChild(new PTextField("RedValue") {
|
||||
OnTextChanged = OnRGBChanged, ToolTip = PLibStrings.TOOLTIP_RED,
|
||||
Text = rgb.r.ToString(), MinWidth = 32, MaxLength = 3,
|
||||
Type = PTextField.FieldType.Integer
|
||||
}.AddOnRealize(OnRedRealized)).AddChild(new PLabel("Green") {
|
||||
TextStyle = PUITuning.Fonts.TextLightStyle, Text = PLibStrings.LABEL_G
|
||||
}).AddChild(new PTextField("GreenValue") {
|
||||
OnTextChanged = OnRGBChanged, ToolTip = PLibStrings.TOOLTIP_GREEN,
|
||||
Text = rgb.g.ToString(), MinWidth = 32, MaxLength = 3,
|
||||
Type = PTextField.FieldType.Integer
|
||||
}.AddOnRealize(OnGreenRealized)).AddChild(new PLabel("Blue") {
|
||||
TextStyle = PUITuning.Fonts.TextLightStyle, Text = PLibStrings.LABEL_B
|
||||
}).AddChild(new PTextField("BlueValue") {
|
||||
OnTextChanged = OnRGBChanged, ToolTip = PLibStrings.TOOLTIP_BLUE,
|
||||
Text = rgb.b.ToString(), MinWidth = 32, MaxLength = 3,
|
||||
Type = PTextField.FieldType.Integer
|
||||
}.AddOnRealize(OnBlueRealized)).Build();
|
||||
UpdateAll();
|
||||
return go;
|
||||
}
|
||||
|
||||
private void OnBlueRealized(GameObject realized) {
|
||||
blue = realized.GetComponentInChildren<TMP_InputField>();
|
||||
}
|
||||
|
||||
private void OnGreenRealized(GameObject realized) {
|
||||
green = realized.GetComponentInChildren<TMP_InputField>();
|
||||
}
|
||||
|
||||
private void OnHueChanged(GameObject _, float newHue) {
|
||||
if (hueGradient != null && hueSlider != null) {
|
||||
float oldAlpha = value.a;
|
||||
hueGradient.Position = hueSlider.value;
|
||||
value = hueGradient.SelectedColor;
|
||||
value.a = oldAlpha;
|
||||
UpdateRGB();
|
||||
UpdateSat(false);
|
||||
UpdateVal(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHueRealized(GameObject realized) {
|
||||
hueGradient = realized.AddOrGet<ColorGradient>();
|
||||
realized.TryGetComponent(out hueSlider);
|
||||
}
|
||||
|
||||
private void OnRedRealized(GameObject realized) {
|
||||
red = realized.GetComponentInChildren<TMP_InputField>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the red, green, or blue field's text is changed.
|
||||
/// </summary>
|
||||
/// <param name="text">The new color value.</param>
|
||||
protected void OnRGBChanged(GameObject _, string text) {
|
||||
Color32 rgb = value;
|
||||
if (byte.TryParse(red.text, out byte r))
|
||||
rgb.r = r;
|
||||
if (byte.TryParse(green.text, out byte g))
|
||||
rgb.g = g;
|
||||
if (byte.TryParse(blue.text, out byte b))
|
||||
rgb.b = b;
|
||||
value = rgb;
|
||||
UpdateAll();
|
||||
}
|
||||
|
||||
private void OnSatChanged(GameObject _, float newSat) {
|
||||
if (satGradient != null && satSlider != null) {
|
||||
float oldAlpha = value.a;
|
||||
satGradient.Position = satSlider.value;
|
||||
value = satGradient.SelectedColor;
|
||||
value.a = oldAlpha;
|
||||
UpdateRGB();
|
||||
UpdateHue(false);
|
||||
UpdateVal(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSatRealized(GameObject realized) {
|
||||
satGradient = realized.AddOrGet<ColorGradient>();
|
||||
realized.TryGetComponent(out satSlider);
|
||||
}
|
||||
|
||||
private void OnSwatchRealized(GameObject realized) {
|
||||
swatch = realized.GetComponentInChildren<Image>();
|
||||
}
|
||||
|
||||
private void OnValChanged(GameObject _, float newValue) {
|
||||
if (valGradient != null && valSlider != null) {
|
||||
float oldAlpha = value.a;
|
||||
valGradient.Position = valSlider.value;
|
||||
value = valGradient.SelectedColor;
|
||||
value.a = oldAlpha;
|
||||
UpdateRGB();
|
||||
UpdateHue(false);
|
||||
UpdateSat(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnValRealized(GameObject realized) {
|
||||
valGradient = realized.AddOrGet<ColorGradient>();
|
||||
realized.TryGetComponent(out valSlider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the color is changed externally, updates all sliders.
|
||||
/// </summary>
|
||||
protected void UpdateAll() {
|
||||
UpdateRGB();
|
||||
UpdateHue(true);
|
||||
UpdateSat(true);
|
||||
UpdateVal(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the position of the hue slider with the currently selected color.
|
||||
/// </summary>
|
||||
/// <param name="moveSlider">true to move the slider handle if necessary, or false to
|
||||
/// leave it where it is.</param>
|
||||
protected void UpdateHue(bool moveSlider) {
|
||||
if (hueGradient != null && hueSlider != null) {
|
||||
Color.RGBToHSV(value, out _, out float s, out float v);
|
||||
hueGradient.SetRange(0.0f, 1.0f, s, s, v, v);
|
||||
hueGradient.SelectedColor = value;
|
||||
if (moveSlider)
|
||||
hueSlider.value = hueGradient.Position;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the displayed value.
|
||||
/// </summary>
|
||||
protected void UpdateRGB() {
|
||||
Color32 rgb = value;
|
||||
if (red != null)
|
||||
red.text = rgb.r.ToString();
|
||||
if (green != null)
|
||||
green.text = rgb.g.ToString();
|
||||
if (blue != null)
|
||||
blue.text = rgb.b.ToString();
|
||||
if (swatch != null)
|
||||
swatch.color = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the position of the saturation slider with the currently selected color.
|
||||
/// </summary>
|
||||
/// <param name="moveSlider">true to move the slider handle if necessary, or false to
|
||||
/// leave it where it is.</param>
|
||||
protected void UpdateSat(bool moveSlider) {
|
||||
if (satGradient != null && satSlider != null) {
|
||||
Color.RGBToHSV(value, out float h, out _, out float v);
|
||||
satGradient.SetRange(h, h, 0.0f, 1.0f, v, v);
|
||||
satGradient.SelectedColor = value;
|
||||
if (moveSlider)
|
||||
satSlider.value = satGradient.Position;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the position of the value slider with the currently selected color.
|
||||
/// </summary>
|
||||
/// <param name="moveSlider">true to move the slider handle if necessary, or false to
|
||||
/// leave it where it is.</param>
|
||||
protected void UpdateVal(bool moveSlider) {
|
||||
if (valGradient != null && valSlider != null) {
|
||||
Color.RGBToHSV(value, out float h, out float s, out _);
|
||||
valGradient.SetRange(h, h, s, s, 0.0f, 1.0f);
|
||||
valGradient.SelectedColor = value;
|
||||
if (moveSlider)
|
||||
valSlider.value = valGradient.Position;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
227
mod/PLibOptions/ColorGradient.cs
Normal file
227
mod/PLibOptions/ColorGradient.cs
Normal file
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// A background image which displays a gradient between two different colors using HSV
|
||||
/// interpolation.
|
||||
/// </summary>
|
||||
internal sealed class ColorGradient : Image {
|
||||
/// <summary>
|
||||
/// The position to use on the track.
|
||||
/// </summary>
|
||||
public float Position {
|
||||
get => position;
|
||||
set {
|
||||
position = Mathf.Clamp01(value);
|
||||
SetPosition();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The currently selected color.
|
||||
/// </summary>
|
||||
public Color SelectedColor {
|
||||
get => current;
|
||||
set {
|
||||
current = value;
|
||||
EstimatePosition();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The currently selected color.
|
||||
/// </summary>
|
||||
private Color current;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the image texture needs to be regenerated.
|
||||
/// </summary>
|
||||
private bool dirty;
|
||||
|
||||
/// <summary>
|
||||
/// Gradient unfortunately always uses RGB. Run curves manually using HSV.
|
||||
/// </summary>
|
||||
private Vector2 hue;
|
||||
|
||||
/// <summary>
|
||||
/// The position to use on the track.
|
||||
/// </summary>
|
||||
private float position;
|
||||
|
||||
/// <summary>
|
||||
/// The texture used when drawing the background.
|
||||
/// </summary>
|
||||
private Texture2D preview;
|
||||
|
||||
private Vector2 sat;
|
||||
|
||||
private Vector2 val;
|
||||
|
||||
public ColorGradient() {
|
||||
current = Color.black;
|
||||
position = 0.0f;
|
||||
preview = null;
|
||||
dirty = true;
|
||||
hue = Vector2.right;
|
||||
sat = Vector2.right;
|
||||
val = Vector2.right;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estimates a position based on the selected color.
|
||||
/// </summary>
|
||||
internal void EstimatePosition() {
|
||||
Color.RGBToHSV(current, out float h, out float s, out float v);
|
||||
float newPos = 0.0f, divider = 0.0f;
|
||||
if (!Mathf.Approximately(hue.x, hue.y)) {
|
||||
newPos += Mathf.Clamp01(Mathf.InverseLerp(hue.x, hue.y, h));
|
||||
divider += 1.0f;
|
||||
}
|
||||
if (!Mathf.Approximately(sat.x, sat.y)) {
|
||||
newPos += Mathf.Clamp01(Mathf.InverseLerp(sat.x, sat.y, s));
|
||||
divider += 1.0f;
|
||||
}
|
||||
if (!Mathf.Approximately(val.x, val.y)) {
|
||||
newPos += Mathf.Clamp01(Mathf.InverseLerp(val.x, val.y, v));
|
||||
divider += 1.0f;
|
||||
}
|
||||
if (divider > 0.0f) {
|
||||
position = newPos / divider;
|
||||
SetPosition();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up the preview when the component is disposed.
|
||||
/// </summary>
|
||||
protected override void OnDestroy() {
|
||||
if (preview != null) {
|
||||
Destroy(preview);
|
||||
preview = null;
|
||||
}
|
||||
if (sprite != null) {
|
||||
Destroy(sprite);
|
||||
sprite = null;
|
||||
}
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the image needs to be resized.
|
||||
/// </summary>
|
||||
protected override void OnRectTransformDimensionsChange() {
|
||||
base.OnRectTransformDimensionsChange();
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the currently selected color with the interpolated value based on the
|
||||
/// current position.
|
||||
/// </summary>
|
||||
internal void SetPosition() {
|
||||
float h = Mathf.Clamp01(Mathf.Lerp(hue.x, hue.y, position));
|
||||
float s = Mathf.Clamp01(Mathf.Lerp(sat.x, sat.y, position));
|
||||
float v = Mathf.Clamp01(Mathf.Lerp(val.x, val.y, position));
|
||||
current = Color.HSVToRGB(h, s, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the range of colors to be displayed.
|
||||
/// </summary>
|
||||
/// <param name="hMin">The minimum hue.</param>
|
||||
/// <param name="hMax">The maximum hue.</param>
|
||||
/// <param name="sMin">The minimum saturation.</param>
|
||||
/// <param name="sMax">The maximum saturation.</param>
|
||||
/// <param name="vMin">The minimum value.</param>
|
||||
/// <param name="vMax">The maximum value.</param>
|
||||
public void SetRange(float hMin, float hMax, float sMin, float sMax, float vMin,
|
||||
float vMax) {
|
||||
// Ensure correct order
|
||||
if (hMin > hMax) {
|
||||
hue.x = hMax; hue.y = hMin;
|
||||
} else {
|
||||
hue.x = hMin; hue.y = hMax;
|
||||
}
|
||||
if (sMin > sMax) {
|
||||
sat.x = sMax; sat.y = sMin;
|
||||
} else {
|
||||
sat.x = sMin; sat.y = sMax;
|
||||
}
|
||||
if (vMin > vMax) {
|
||||
val.x = vMax; val.y = vMin;
|
||||
} else {
|
||||
val.x = vMin; val.y = vMax;
|
||||
}
|
||||
EstimatePosition();
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by Unity when the component is created.
|
||||
/// </summary>
|
||||
protected override void Start() {
|
||||
base.Start();
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regenerates the color gradient image if necessary.
|
||||
/// </summary>
|
||||
internal void Update() {
|
||||
if (dirty || preview == null || sprite == null) {
|
||||
var rt = rectTransform.rect;
|
||||
int w = Mathf.RoundToInt(rt.width), h = Mathf.RoundToInt(rt.height);
|
||||
if (w > 0 && h > 0) {
|
||||
var newData = new Color[w * h];
|
||||
float hMin = hue.x, hMax = hue.y, sMin = sat.x, sMax = sat.y, vMin = val.x,
|
||||
vMax = val.y;
|
||||
var oldSprite = sprite;
|
||||
bool rebuild = preview == null || oldSprite == null || preview.width !=
|
||||
w || preview.height != h;
|
||||
if (rebuild) {
|
||||
if (preview != null)
|
||||
Destroy(preview);
|
||||
if (oldSprite != null)
|
||||
Destroy(oldSprite);
|
||||
preview = new Texture2D(w, h);
|
||||
}
|
||||
// Generate one row of colors
|
||||
for (int i = 0; i < w; i++) {
|
||||
float t = (float)i / w;
|
||||
newData[i] = Color.HSVToRGB(Mathf.Lerp(hMin, hMax, t), Mathf.Lerp(sMin,
|
||||
sMax, t), Mathf.Lerp(vMin, vMax, t));
|
||||
}
|
||||
// Native copy row to all others
|
||||
for (int i = 1; i < h; i++)
|
||||
Array.Copy(newData, 0, newData, i * w, w);
|
||||
preview.SetPixels(newData);
|
||||
preview.Apply();
|
||||
if (rebuild)
|
||||
sprite = Sprite.Create(preview, new Rect(0.0f, 0.0f, w, h),
|
||||
new Vector2(0.5f, 0.5f));
|
||||
}
|
||||
dirty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
mod/PLibOptions/ColorOptionsEntry.cs
Normal file
38
mod/PLibOptions/ColorOptionsEntry.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An options entry which represents Color and displays a color picker with sliders.
|
||||
/// </summary>
|
||||
internal class ColorOptionsEntry : ColorBaseOptionsEntry {
|
||||
public override object Value {
|
||||
get => value;
|
||||
set {
|
||||
if (value is Color newValue) {
|
||||
this.value = newValue;
|
||||
UpdateAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ColorOptionsEntry(string field, IOptionSpec spec) : base(field, spec) { }
|
||||
}
|
||||
}
|
||||
161
mod/PLibOptions/CompositeOptionsEntry.cs
Normal file
161
mod/PLibOptions/CompositeOptionsEntry.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using PeterHan.PLib.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An options entry that encapsulates other options. The category annotation on those
|
||||
/// objects will be ignored, and the category of the Option attribute on the property
|
||||
/// that declared those options (to avoid infinite loops) will be used instead.
|
||||
///
|
||||
/// <b>This object is not in the scene graph.</b> Any events in OnRealize will never be
|
||||
/// invoked, and it is never "built".
|
||||
/// </summary>
|
||||
internal class CompositeOptionsEntry : OptionsEntry {
|
||||
/// <summary>
|
||||
/// Creates an options entry wrapper for the specified property, iterating its internal
|
||||
/// fields to create sub-options if needed (recursively).
|
||||
/// </summary>
|
||||
/// <param name="info">The property to wrap.</param>
|
||||
/// <param name="spec">The option title and tool tip.</param>
|
||||
/// <param name="depth">The current depth of iteration to avoid infinite loops.</param>
|
||||
/// <returns>An options wrapper, or null if no inner properties are themselves options.</returns>
|
||||
internal static CompositeOptionsEntry Create(IOptionSpec spec, PropertyInfo info,
|
||||
int depth) {
|
||||
var type = info.PropertyType;
|
||||
var composite = new CompositeOptionsEntry(info.Name, spec, type);
|
||||
// Skip static properties if they exist
|
||||
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.
|
||||
Instance)) {
|
||||
var entry = TryCreateEntry(prop, depth + 1);
|
||||
if (entry != null)
|
||||
composite.AddField(prop, entry);
|
||||
}
|
||||
return composite.ChildCount > 0 ? composite : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reports the number of options contained inside this one.
|
||||
/// </summary>
|
||||
public int ChildCount {
|
||||
get {
|
||||
return subOptions.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public override object Value {
|
||||
get {
|
||||
return value;
|
||||
}
|
||||
set {
|
||||
if (value != null && targetType.IsAssignableFrom(value.GetType()))
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The options encapsulated in this object.
|
||||
/// </summary>
|
||||
protected readonly IDictionary<PropertyInfo, IOptionsEntry> subOptions;
|
||||
|
||||
/// <summary>
|
||||
/// The type of the encapsulated object.
|
||||
/// </summary>
|
||||
protected readonly Type targetType;
|
||||
|
||||
/// <summary>
|
||||
/// The object thus wrapped.
|
||||
/// </summary>
|
||||
protected object value;
|
||||
|
||||
public CompositeOptionsEntry(string field, IOptionSpec spec, Type fieldType) :
|
||||
base(field, spec) {
|
||||
subOptions = new Dictionary<PropertyInfo, IOptionsEntry>(16);
|
||||
targetType = fieldType ?? throw new ArgumentNullException(nameof(fieldType));
|
||||
value = OptionsDialog.CreateOptions(fieldType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an options entry object that operates on Option fields of the encapsulated
|
||||
/// object.
|
||||
/// </summary>
|
||||
/// <param name="info">The property that is wrapped.</param>
|
||||
/// <param name="entry">The entry to add.</param>
|
||||
public void AddField(PropertyInfo info, IOptionsEntry entry) {
|
||||
if (entry == null)
|
||||
throw new ArgumentNullException(nameof(entry));
|
||||
if (info == null)
|
||||
throw new ArgumentNullException(nameof(info));
|
||||
subOptions.Add(info, entry);
|
||||
}
|
||||
|
||||
public override void CreateUIEntry(PGridPanel parent, ref int row) {
|
||||
int i = row;
|
||||
bool first = true;
|
||||
parent.AddOnRealize(WhenRealized);
|
||||
// Render each sub-entry - order is always Add Spec, Create Entry, Increment Row
|
||||
foreach (var pair in subOptions) {
|
||||
if (!first) {
|
||||
i++;
|
||||
parent.AddRow(new GridRowSpec());
|
||||
}
|
||||
pair.Value.CreateUIEntry(parent, ref i);
|
||||
first = false;
|
||||
}
|
||||
row = i;
|
||||
}
|
||||
|
||||
public override GameObject GetUIComponent() {
|
||||
// Will not be invoked
|
||||
return new GameObject("Empty");
|
||||
}
|
||||
|
||||
public override void ReadFrom(object settings) {
|
||||
base.ReadFrom(settings);
|
||||
foreach (var pair in subOptions)
|
||||
pair.Value.ReadFrom(value);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return "{1}[field={0},title={2},children=[{3}]]".F(Field, GetType().Name, Title,
|
||||
subOptions.Join());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the child objects for the first time when the panel is realized.
|
||||
/// </summary>
|
||||
private void WhenRealized(GameObject _) {
|
||||
foreach (var pair in subOptions)
|
||||
pair.Value.ReadFrom(value);
|
||||
}
|
||||
|
||||
public override void WriteTo(object settings) {
|
||||
foreach (var pair in subOptions)
|
||||
// Cannot detour as the types are not known at compile time, and delegates
|
||||
// bake in the target object which changes upon each update
|
||||
pair.Value.WriteTo(value);
|
||||
base.WriteTo(settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
59
mod/PLibOptions/ConfigFileAttribute.cs
Normal file
59
mod/PLibOptions/ConfigFileAttribute.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An attribute placed on an options class only (will not function on a member property)
|
||||
/// which denotes the config file name to use for that mod, and allows save/load options
|
||||
/// to be set.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||
public sealed class ConfigFileAttribute : Attribute {
|
||||
/// <summary>
|
||||
/// The configuration file name. If null, the default file name will be used.
|
||||
/// </summary>
|
||||
public string ConfigFileName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the output should be indented nicely. Defaults to false for smaller
|
||||
/// config files.
|
||||
/// </summary>
|
||||
public bool IndentOutput { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the config file will be moved from the mod folder to a folder in the
|
||||
/// config directory shared across mods. This change preserves the mod configuration
|
||||
/// across updates, but may not be cleared when the mod is uninstalled. Use with
|
||||
/// caution.
|
||||
/// </summary>
|
||||
public bool UseSharedConfigLocation { get; }
|
||||
|
||||
public ConfigFileAttribute(string FileName = POptions.CONFIG_FILE_NAME,
|
||||
bool IndentOutput = false, bool SharedConfigLocation = false) {
|
||||
ConfigFileName = FileName;
|
||||
this.IndentOutput = IndentOutput;
|
||||
UseSharedConfigLocation = SharedConfigLocation;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return ConfigFileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
mod/PLibOptions/DynamicOptionAttribute.cs
Normal file
54
mod/PLibOptions/DynamicOptionAttribute.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using System;
|
||||
|
||||
namespace PeterHan.PLib {
|
||||
/// <summary>
|
||||
/// An attribute placed on an option property for a class used as mod options in order to
|
||||
/// make PLib use a custom options handler. The type used for the handler must inherit
|
||||
/// from IOptionsEntry.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||
public sealed class DynamicOptionAttribute : Attribute {
|
||||
/// <summary>
|
||||
/// The option category.
|
||||
/// </summary>
|
||||
public string Category { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The option handler.
|
||||
/// </summary>
|
||||
public Type Handler { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Denotes a mod option field.
|
||||
/// </summary>
|
||||
/// <param name="type">The type that will handle this dynamic option.</param>
|
||||
/// <param name="category">The category to use, or null for the default category.</param>
|
||||
public DynamicOptionAttribute(Type type, string category = null) {
|
||||
Category = category;
|
||||
Handler = type ?? throw new ArgumentNullException(nameof(type));
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return "DynamicOption[handler={0},category={1}]".F(Handler.FullName, Category);
|
||||
}
|
||||
}
|
||||
}
|
||||
119
mod/PLibOptions/FloatOptionsEntry.cs
Normal file
119
mod/PLibOptions/FloatOptionsEntry.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An options entry which represents float and displays a text field and slider.
|
||||
/// </summary>
|
||||
public class FloatOptionsEntry : SlidingBaseOptionsEntry {
|
||||
/// <summary>
|
||||
/// The format to use if none is provided.
|
||||
/// </summary>
|
||||
public const string DEFAULT_FORMAT = "F2";
|
||||
|
||||
public override object Value {
|
||||
get {
|
||||
return value;
|
||||
}
|
||||
set {
|
||||
if (value is float newValue) {
|
||||
this.value = newValue;
|
||||
Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The realized text field.
|
||||
/// </summary>
|
||||
private GameObject textField;
|
||||
|
||||
/// <summary>
|
||||
/// The value in the text field.
|
||||
/// </summary>
|
||||
private float value;
|
||||
|
||||
public FloatOptionsEntry(string field, IOptionSpec spec, LimitAttribute limit = null) :
|
||||
base(field, spec, limit) {
|
||||
textField = null;
|
||||
value = 0.0f;
|
||||
}
|
||||
|
||||
protected override PSliderSingle GetSlider() {
|
||||
return new PSliderSingle() {
|
||||
OnValueChanged = OnSliderChanged, ToolTip = LookInStrings(Tooltip),
|
||||
MinValue = (float)limits.Minimum, MaxValue = (float)limits.Maximum,
|
||||
InitialValue = value
|
||||
};
|
||||
}
|
||||
|
||||
public override GameObject GetUIComponent() {
|
||||
textField = new PTextField() {
|
||||
OnTextChanged = OnTextChanged, ToolTip = LookInStrings(Tooltip),
|
||||
Text = value.ToString(Format ?? DEFAULT_FORMAT), MinWidth = 64, MaxLength = 16,
|
||||
Type = PTextField.FieldType.Float
|
||||
}.Build();
|
||||
Update();
|
||||
return textField;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the slider's value is changed.
|
||||
/// </summary>
|
||||
/// <param name="newValue">The new slider value.</param>
|
||||
private void OnSliderChanged(GameObject _, float newValue) {
|
||||
// Record the value
|
||||
if (limits != null)
|
||||
value = limits.ClampToRange(newValue);
|
||||
else
|
||||
value = newValue;
|
||||
Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the input field's text is changed.
|
||||
/// </summary>
|
||||
/// <param name="text">The new text.</param>
|
||||
private void OnTextChanged(GameObject _, string text) {
|
||||
if (float.TryParse(text, out float newValue)) {
|
||||
if (Format != null && Format.ToUpperInvariant().IndexOf('P') >= 0)
|
||||
newValue *= 0.01f;
|
||||
if (limits != null)
|
||||
newValue = limits.ClampToRange(newValue);
|
||||
// Record the valid value
|
||||
value = newValue;
|
||||
}
|
||||
Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the displayed value.
|
||||
/// </summary>
|
||||
protected override void Update() {
|
||||
var field = textField?.GetComponentInChildren<TMP_InputField>();
|
||||
if (field != null)
|
||||
field.text = value.ToString(Format ?? DEFAULT_FORMAT);
|
||||
if (slider != null)
|
||||
PSliderSingle.SetCurrentValue(slider, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
mod/PLibOptions/IOptionSpec.cs
Normal file
53
mod/PLibOptions/IOptionSpec.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// The common parent of all classes that can specify the user visible attributes of an
|
||||
/// option.
|
||||
/// </summary>
|
||||
public interface IOptionSpec {
|
||||
/// <summary>
|
||||
/// The option category. Ignored and replaced with the parent option's category if
|
||||
/// this option is part of a custom grouped type.
|
||||
/// </summary>
|
||||
string Category { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The format string to use when displaying this option value. Only applicable for
|
||||
/// some types of options.
|
||||
///
|
||||
/// <b>Warning</b>: Attribute may have issues on nested classes that are used as custom
|
||||
/// grouped options. To mitigate, try declaring the custom class in a non-nested
|
||||
/// context (i.e. not declared inside another class).
|
||||
/// </summary>
|
||||
string Format { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The option title. Ignored for fields which are displayed as custom grouped types
|
||||
/// types of other options.
|
||||
/// </summary>
|
||||
string Title { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The option description tooltip. Ignored for fields which are displayed as custom
|
||||
/// grouped types of other options.
|
||||
/// </summary>
|
||||
string Tooltip { get; }
|
||||
}
|
||||
}
|
||||
54
mod/PLibOptions/IOptions.cs
Normal file
54
mod/PLibOptions/IOptions.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An optional interface which can be implemented to give mods the ability to dynamically
|
||||
/// add new options at runtime, or to get a notification when options are updated to the
|
||||
/// options file.
|
||||
///
|
||||
/// This interface is <b>optional</b>. There is no need to implement it to use PLib
|
||||
/// Options. But if one method is implemented, the other must also be. If not used,
|
||||
/// OnOptionsChanged should be empty, and CreateOptions should return an empty collection.
|
||||
/// </summary>
|
||||
public interface IOptions {
|
||||
/// <summary>
|
||||
/// Called to create additional options. After the options in this class have been
|
||||
/// read from the data file, but before the dialog is shown, this method will be
|
||||
/// invoked. Each return value must be of a type that implements IOptionsEntry.
|
||||
///
|
||||
/// The options will be sorted and categorized normally as if they were present at the
|
||||
/// end of the property list in a regular options class.
|
||||
///
|
||||
/// This method can be an enumerator using code like
|
||||
/// yield return new MyOptionsHandler();
|
||||
/// </summary>
|
||||
/// <returns>The custom options to implement.</returns>
|
||||
IEnumerable<IOptionsEntry> CreateOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Called when options are written to the file. The current object will have the same
|
||||
/// values as the data that was just written to the file. This call happens after the
|
||||
/// options have been stored to disk, but before any restart required dialog is shown
|
||||
/// (if [RestartRequired] is also on this class).
|
||||
/// </summary>
|
||||
void OnOptionsChanged();
|
||||
}
|
||||
}
|
||||
46
mod/PLibOptions/IOptionsEntry.cs
Normal file
46
mod/PLibOptions/IOptionsEntry.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.UI;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// All options handlers, including user dynamic option handlers, implement this type.
|
||||
/// </summary>
|
||||
public interface IOptionsEntry : IOptionSpec {
|
||||
/// <summary>
|
||||
/// Creates UI components that will present this option.
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent panel where the components should be added.</param>
|
||||
/// <param name="row">The row index where the component should be placed. If multiple
|
||||
/// rows of components are added, increment this value for each additional row.</param>
|
||||
void CreateUIEntry(PGridPanel parent, ref int row);
|
||||
|
||||
/// <summary>
|
||||
/// Reads the option value into the UI from the provided settings object.
|
||||
/// </summary>
|
||||
/// <param name="settings">The settings object.</param>
|
||||
void ReadFrom(object settings);
|
||||
|
||||
/// <summary>
|
||||
/// Writes the option value from the UI into the provided settings object.
|
||||
/// </summary>
|
||||
/// <param name="settings">The settings object.</param>
|
||||
void WriteTo(object settings);
|
||||
}
|
||||
}
|
||||
112
mod/PLibOptions/IntOptionsEntry.cs
Normal file
112
mod/PLibOptions/IntOptionsEntry.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An options entry which represents int and displays a text field and slider.
|
||||
/// </summary>
|
||||
public class IntOptionsEntry : SlidingBaseOptionsEntry {
|
||||
public override object Value {
|
||||
get {
|
||||
return value;
|
||||
}
|
||||
set {
|
||||
if (value is int newValue) {
|
||||
this.value = newValue;
|
||||
Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The realized text field.
|
||||
/// </summary>
|
||||
private GameObject textField;
|
||||
|
||||
/// <summary>
|
||||
/// The value in the text field.
|
||||
/// </summary>
|
||||
private int value;
|
||||
|
||||
public IntOptionsEntry(string field, IOptionSpec spec, LimitAttribute limit = null) :
|
||||
base(field, spec, limit) {
|
||||
textField = null;
|
||||
value = 0;
|
||||
}
|
||||
|
||||
protected override PSliderSingle GetSlider() {
|
||||
return new PSliderSingle() {
|
||||
OnValueChanged = OnSliderChanged, ToolTip = LookInStrings(Tooltip),
|
||||
MinValue = (float)limits.Minimum, MaxValue = (float)limits.Maximum,
|
||||
InitialValue = value, IntegersOnly = true
|
||||
};
|
||||
}
|
||||
|
||||
public override GameObject GetUIComponent() {
|
||||
textField = new PTextField() {
|
||||
OnTextChanged = OnTextChanged, ToolTip = LookInStrings(Tooltip),
|
||||
Text = value.ToString(Format ?? "D"), MinWidth = 64, MaxLength = 10,
|
||||
Type = PTextField.FieldType.Integer
|
||||
}.Build();
|
||||
Update();
|
||||
return textField;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the slider's value is changed.
|
||||
/// </summary>
|
||||
/// <param name="newValue">The new slider value.</param>
|
||||
private void OnSliderChanged(GameObject _, float newValue) {
|
||||
int newIntValue = Mathf.RoundToInt(newValue);
|
||||
if (limits != null)
|
||||
newIntValue = limits.ClampToRange(newIntValue);
|
||||
// Record the value
|
||||
value = newIntValue;
|
||||
Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the input field's text is changed.
|
||||
/// </summary>
|
||||
/// <param name="text">The new text.</param>
|
||||
private void OnTextChanged(GameObject _, string text) {
|
||||
if (int.TryParse(text, out int newValue)) {
|
||||
if (limits != null)
|
||||
newValue = limits.ClampToRange(newValue);
|
||||
// Record the valid value
|
||||
value = newValue;
|
||||
}
|
||||
Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the displayed value.
|
||||
/// </summary>
|
||||
protected override void Update() {
|
||||
var field = textField?.GetComponentInChildren<TMP_InputField>();
|
||||
if (field != null)
|
||||
field.text = value.ToString(Format ?? "D");
|
||||
if (slider != null)
|
||||
PSliderSingle.SetCurrentValue(slider, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
75
mod/PLibOptions/LimitAttribute.cs
Normal file
75
mod/PLibOptions/LimitAttribute.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using System;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An attribute placed on an option field for a property used as mod options to define
|
||||
/// minimum and maximum acceptable values.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
|
||||
public sealed class LimitAttribute : Attribute {
|
||||
/// <summary>
|
||||
/// The maximum value (inclusive).
|
||||
/// </summary>
|
||||
public double Maximum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The minimum value (inclusive).
|
||||
/// </summary>
|
||||
public double Minimum { get; }
|
||||
|
||||
public LimitAttribute(double min, double max) {
|
||||
Minimum = min.IsNaNOrInfinity() ? 0.0 : min;
|
||||
Maximum = (max.IsNaNOrInfinity() || max < min) ? min : max;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps the specified value to the range of this Limits object.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to coerce.</param>
|
||||
/// <returns>The nearest value included by these limits to the specified value.</returns>
|
||||
public float ClampToRange(float value) {
|
||||
return value.InRange((float)Minimum, (float)Maximum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps the specified value to the range of this Limits object.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to coerce.</param>
|
||||
/// <returns>The nearest value included by these limits to the specified value.</returns>
|
||||
public int ClampToRange(int value) {
|
||||
return value.InRange((int)Minimum, (int)Maximum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reports whether a value is in the range included in these limits.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to check.</param>
|
||||
/// <returns>true if it is included in the limits, or false otherwise.</returns>
|
||||
public bool InRange(double value) {
|
||||
return value >= Minimum && value <= Maximum;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return "{0:F2} to {1:F2}".F(Minimum, Maximum);
|
||||
}
|
||||
}
|
||||
}
|
||||
120
mod/PLibOptions/LogFloatOptionsEntry.cs
Normal file
120
mod/PLibOptions/LogFloatOptionsEntry.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.UI;
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An options entry which represents float and displays a text field and slider.
|
||||
/// This entry uses a logarithmic scale.
|
||||
/// </summary>
|
||||
public class LogFloatOptionsEntry : SlidingBaseOptionsEntry {
|
||||
/// <summary>
|
||||
/// The format to use if none is provided.
|
||||
/// </summary>
|
||||
public const string DEFAULT_FORMAT = "F2";
|
||||
|
||||
public override object Value {
|
||||
get {
|
||||
return value;
|
||||
}
|
||||
set {
|
||||
if (value is float newValue) {
|
||||
this.value = limits.ClampToRange(newValue);
|
||||
Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The realized text field.
|
||||
/// </summary>
|
||||
private GameObject textField;
|
||||
|
||||
/// <summary>
|
||||
/// The value in the text field.
|
||||
/// </summary>
|
||||
private float value;
|
||||
|
||||
public LogFloatOptionsEntry(string field, IOptionSpec spec, LimitAttribute limit) :
|
||||
base(field, spec, limit) {
|
||||
if (limit == null)
|
||||
throw new ArgumentNullException(nameof(limit));
|
||||
if (limit.Minimum <= 0.0f || limit.Maximum <= 0.0f)
|
||||
throw new ArgumentOutOfRangeException(nameof(limit),
|
||||
"Logarithmic values must be positive");
|
||||
textField = null;
|
||||
value = limit.ClampToRange(1.0f);
|
||||
}
|
||||
|
||||
protected override PSliderSingle GetSlider() {
|
||||
return new PSliderSingle() {
|
||||
OnValueChanged = OnSliderChanged, ToolTip = LookInStrings(Tooltip),
|
||||
MinValue = Mathf.Log((float)limits.Minimum), MaxValue = Mathf.Log(
|
||||
(float)limits.Maximum), InitialValue = Mathf.Log(value)
|
||||
};
|
||||
}
|
||||
|
||||
public override GameObject GetUIComponent() {
|
||||
textField = new PTextField() {
|
||||
OnTextChanged = OnTextChanged, ToolTip = LookInStrings(Tooltip),
|
||||
Text = value.ToString(Format ?? DEFAULT_FORMAT), MinWidth = 64, MaxLength = 16,
|
||||
Type = PTextField.FieldType.Float
|
||||
}.Build();
|
||||
Update();
|
||||
return textField;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the slider's value is changed.
|
||||
/// </summary>
|
||||
/// <param name="newValue">The new slider value.</param>
|
||||
private void OnSliderChanged(GameObject _, float newValue) {
|
||||
value = limits.ClampToRange(Mathf.Exp(newValue));
|
||||
Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the input field's text is changed.
|
||||
/// </summary>
|
||||
/// <param name="text">The new text.</param>
|
||||
private void OnTextChanged(GameObject _, string text) {
|
||||
if (float.TryParse(text, out float newValue)) {
|
||||
if (Format != null && Format.ToUpperInvariant().IndexOf('P') >= 0)
|
||||
newValue *= 0.01f;
|
||||
value = limits.ClampToRange(newValue);
|
||||
}
|
||||
Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the displayed value.
|
||||
/// </summary>
|
||||
protected override void Update() {
|
||||
var field = textField?.GetComponentInChildren<TMP_InputField>();
|
||||
if (field != null)
|
||||
field.text = value.ToString(Format ?? DEFAULT_FORMAT);
|
||||
if (slider != null)
|
||||
PSliderSingle.SetCurrentValue(slider, Mathf.Log(Mathf.Max(float.Epsilon,
|
||||
value)));
|
||||
}
|
||||
}
|
||||
}
|
||||
93
mod/PLibOptions/ModDialogInfo.cs
Normal file
93
mod/PLibOptions/ModDialogInfo.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using System;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// Stores the information displayed about a mod in its options dialog.
|
||||
/// </summary>
|
||||
internal sealed class ModDialogInfo {
|
||||
/// <summary>
|
||||
/// Gets the text shown for a mod's version.
|
||||
/// </summary>
|
||||
/// <param name="optionsType">The type used for the mod settings.</param>
|
||||
/// <returns>The mod version description.</returns>
|
||||
private static string GetModVersionText(Type optionsType) {
|
||||
var asm = optionsType.Assembly;
|
||||
string version = asm.GetFileVersion();
|
||||
// Use FileVersion if available, else assembly version
|
||||
if (string.IsNullOrEmpty(version))
|
||||
version = string.Format(PLibStrings.MOD_ASSEMBLY_VERSION, asm.GetName().
|
||||
Version);
|
||||
else
|
||||
version = string.Format(PLibStrings.MOD_VERSION, version);
|
||||
return version;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The path to the image displayed (on the file system) for this mod.
|
||||
/// </summary>
|
||||
public string Image { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The mod title. The title is taken directly from the mod version information.
|
||||
/// </summary>
|
||||
public string Title { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The URL which will be displayed. If none was provided, the Steam workshop page URL
|
||||
/// will be reported for Steam mods, and an empty string for local/dev mods.
|
||||
/// </summary>
|
||||
public string URL { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The mod version.
|
||||
/// </summary>
|
||||
public string Version { get; }
|
||||
|
||||
internal ModDialogInfo(Type type, string url, string image) {
|
||||
var mod = POptions.GetModFromType(type);
|
||||
string title, version;
|
||||
Image = image ?? "";
|
||||
if (mod != null) {
|
||||
string modInfoVersion = mod.packagedModInfo?.version;
|
||||
title = mod.title;
|
||||
if (string.IsNullOrEmpty(url) && mod.label.distribution_platform == KMod.Label.
|
||||
DistributionPlatform.Steam)
|
||||
url = "https://steamcommunity.com/sharedfiles/filedetails/?id=" + mod.
|
||||
label.id;
|
||||
if (!string.IsNullOrEmpty(modInfoVersion))
|
||||
version = string.Format(PLibStrings.MOD_VERSION, modInfoVersion);
|
||||
else
|
||||
version = GetModVersionText(type);
|
||||
} else {
|
||||
title = type.Assembly.GetNameSafe();
|
||||
version = GetModVersionText(type);
|
||||
}
|
||||
Title = title ?? "";
|
||||
URL = url ?? "";
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return base.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
57
mod/PLibOptions/ModInfoAttribute.cs
Normal file
57
mod/PLibOptions/ModInfoAttribute.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// Allows mod authors to specify attributes for their mods to be shown in the Options
|
||||
/// dialog.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||
public sealed class ModInfoAttribute : Attribute {
|
||||
/// <summary>
|
||||
/// If true, forces all categories in the options screen to begin collapsed (except
|
||||
/// the default category).
|
||||
/// </summary>
|
||||
public bool ForceCollapseCategories { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the image file (in the mod's root directory) to display in the options
|
||||
/// dialog. If null or empty (or it cannot be loaded), no image is displayed.
|
||||
/// </summary>
|
||||
public string Image { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The URL to use for the mod. If null or empty, the Steam workshop link will be used
|
||||
/// if possible, or otherwise the button will not be shown.
|
||||
/// </summary>
|
||||
public string URL { get; }
|
||||
|
||||
public ModInfoAttribute(string url, string image = null, bool collapse = false)
|
||||
{
|
||||
ForceCollapseCategories = collapse;
|
||||
Image = image;
|
||||
URL = url;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("ModInfoAttribute[URL={1},Image={2}]", URL, Image);
|
||||
}
|
||||
}
|
||||
}
|
||||
132
mod/PLibOptions/NullableFloatOptionsEntry.cs
Normal file
132
mod/PLibOptions/NullableFloatOptionsEntry.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An options entry which represents float? and displays a text field and slider.
|
||||
/// </summary>
|
||||
public class NullableFloatOptionsEntry : SlidingBaseOptionsEntry {
|
||||
/// <summary>
|
||||
/// The text that is rendered for the current value of the entry.
|
||||
/// </summary>
|
||||
protected virtual string FieldText {
|
||||
get {
|
||||
return value?.ToString(Format ?? FloatOptionsEntry.DEFAULT_FORMAT) ?? string.
|
||||
Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public override object Value {
|
||||
get {
|
||||
return value;
|
||||
}
|
||||
set {
|
||||
if (value == null) {
|
||||
this.value = null;
|
||||
Update();
|
||||
} else if (value is float newValue) {
|
||||
this.value = newValue;
|
||||
Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The realized text field.
|
||||
/// </summary>
|
||||
private GameObject textField;
|
||||
|
||||
/// <summary>
|
||||
/// The value in the text field.
|
||||
/// </summary>
|
||||
private float? value;
|
||||
|
||||
public NullableFloatOptionsEntry(string field, IOptionSpec spec,
|
||||
LimitAttribute limit = null) : base(field, spec, limit) {
|
||||
textField = null;
|
||||
value = null;
|
||||
}
|
||||
|
||||
protected override PSliderSingle GetSlider() {
|
||||
float minLimit = (float)limits.Minimum, maxLimit = (float)limits.Maximum;
|
||||
return new PSliderSingle() {
|
||||
OnValueChanged = OnSliderChanged, ToolTip = LookInStrings(Tooltip),
|
||||
MinValue = minLimit, MaxValue = maxLimit, InitialValue = 0.5f * (minLimit +
|
||||
maxLimit)
|
||||
};
|
||||
}
|
||||
|
||||
public override GameObject GetUIComponent() {
|
||||
textField = new PTextField() {
|
||||
OnTextChanged = OnTextChanged, ToolTip = LookInStrings(Tooltip),
|
||||
Text = FieldText, MinWidth = 64, MaxLength = 16,
|
||||
Type = PTextField.FieldType.Float
|
||||
}.Build();
|
||||
Update();
|
||||
return textField;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the slider's value is changed.
|
||||
/// </summary>
|
||||
/// <param name="newValue">The new slider value.</param>
|
||||
private void OnSliderChanged(GameObject _, float newValue) {
|
||||
// Record the value
|
||||
if (limits != null)
|
||||
value = limits.ClampToRange(newValue);
|
||||
else
|
||||
value = newValue;
|
||||
Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the input field's text is changed.
|
||||
/// </summary>
|
||||
/// <param name="text">The new text.</param>
|
||||
private void OnTextChanged(GameObject _, string text) {
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
// Limits are assumed to allow null, because why not use non-nullable
|
||||
// otherwise?
|
||||
value = null;
|
||||
else if (float.TryParse(text, out float newValue)) {
|
||||
if (Format != null && Format.ToUpperInvariant().IndexOf('P') >= 0)
|
||||
newValue *= 0.01f;
|
||||
if (limits != null)
|
||||
newValue = limits.ClampToRange(newValue);
|
||||
// Record the valid value
|
||||
value = newValue;
|
||||
}
|
||||
Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the displayed value.
|
||||
/// </summary>
|
||||
protected override void Update() {
|
||||
var field = textField?.GetComponentInChildren<TMP_InputField>();
|
||||
if (field != null)
|
||||
field.text = FieldText;
|
||||
if (slider != null && value != null)
|
||||
PSliderSingle.SetCurrentValue(slider, (float)value);
|
||||
}
|
||||
}
|
||||
}
|
||||
130
mod/PLibOptions/NullableIntOptionsEntry.cs
Normal file
130
mod/PLibOptions/NullableIntOptionsEntry.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.UI;
|
||||
using System.Reflection;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An options entry which represents int? and displays a text field and slider.
|
||||
/// </summary>
|
||||
public class NullableIntOptionsEntry : SlidingBaseOptionsEntry {
|
||||
/// <summary>
|
||||
/// The text that is rendered for the current value of the entry.
|
||||
/// </summary>
|
||||
protected virtual string FieldText {
|
||||
get {
|
||||
return value?.ToString(Format ?? "D") ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public override object Value {
|
||||
get {
|
||||
return value;
|
||||
}
|
||||
set {
|
||||
if (value == null) {
|
||||
this.value = null;
|
||||
Update();
|
||||
} else if (value is int newValue) {
|
||||
this.value = newValue;
|
||||
Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The realized text field.
|
||||
/// </summary>
|
||||
private GameObject textField;
|
||||
|
||||
/// <summary>
|
||||
/// The value in the text field.
|
||||
/// </summary>
|
||||
private int? value;
|
||||
|
||||
public NullableIntOptionsEntry(string field, IOptionSpec spec,
|
||||
LimitAttribute limit = null) : base(field, spec, limit) {
|
||||
textField = null;
|
||||
value = null;
|
||||
}
|
||||
|
||||
protected override PSliderSingle GetSlider() {
|
||||
float minLimit = (float)limits.Minimum, maxLimit = (float)limits.Maximum;
|
||||
return new PSliderSingle() {
|
||||
OnValueChanged = OnSliderChanged, ToolTip = LookInStrings(Tooltip),
|
||||
MinValue = minLimit, MaxValue = maxLimit, InitialValue = minLimit,
|
||||
IntegersOnly = true
|
||||
};
|
||||
}
|
||||
|
||||
public override GameObject GetUIComponent() {
|
||||
textField = new PTextField() {
|
||||
OnTextChanged = OnTextChanged, ToolTip = LookInStrings(Tooltip),
|
||||
Text = FieldText, MinWidth = 64, MaxLength = 10,
|
||||
Type = PTextField.FieldType.Integer
|
||||
}.Build();
|
||||
Update();
|
||||
return textField;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the slider's value is changed.
|
||||
/// </summary>
|
||||
/// <param name="newValue">The new slider value.</param>
|
||||
private void OnSliderChanged(GameObject _, float newValue) {
|
||||
int newIntValue = Mathf.RoundToInt(newValue);
|
||||
if (limits != null)
|
||||
newIntValue = limits.ClampToRange(newIntValue);
|
||||
// Record the value
|
||||
value = newIntValue;
|
||||
Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the input field's text is changed.
|
||||
/// </summary>
|
||||
/// <param name="text">The new text.</param>
|
||||
private void OnTextChanged(GameObject _, string text) {
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
// Limits are assumed to allow null, because why not use non-nullable
|
||||
// otherwise?
|
||||
value = null;
|
||||
else if (int.TryParse(text, out int newValue)) {
|
||||
if (limits != null)
|
||||
newValue = limits.ClampToRange(newValue);
|
||||
// Record the valid value
|
||||
value = newValue;
|
||||
}
|
||||
Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the displayed value.
|
||||
/// </summary>
|
||||
protected override void Update() {
|
||||
var field = textField?.GetComponentInChildren<TMP_InputField>();
|
||||
if (field != null)
|
||||
field.text = FieldText;
|
||||
if (slider != null && value != null)
|
||||
PSliderSingle.SetCurrentValue(slider, (int)value);
|
||||
}
|
||||
}
|
||||
}
|
||||
80
mod/PLibOptions/OptionAttribute.cs
Normal file
80
mod/PLibOptions/OptionAttribute.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An attribute placed on an option property or enum value for a class used as mod options
|
||||
/// in order to denote the display title and other options.
|
||||
///
|
||||
/// Options attributes will be recursively searched if a custom type is used for a property
|
||||
/// with this attribute. If fields in that type have Option attributes, they will be
|
||||
/// displayed under the category of their parent option (ignoring their own category
|
||||
/// declaration).
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false,
|
||||
Inherited = true)]
|
||||
public sealed class OptionAttribute : Attribute, IOptionSpec {
|
||||
public string Category { get; }
|
||||
|
||||
public string Format { get; set; }
|
||||
|
||||
public string Title { get; }
|
||||
|
||||
public string Tooltip { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Denotes a mod option field. Can also be used on members of an Enum type to give
|
||||
/// them a friendly display name.
|
||||
///
|
||||
/// This overload will take the option strings from STRINGS, using the namespace of the
|
||||
/// declaring type and the name of the property. A type declared in the MyName.
|
||||
/// MyNamespace namespace with a property named TestProperty will get the title
|
||||
/// STRINGS.MYNAME.MYNAMESPACE.OPTIONS.TESTPROPERTY.NAME, the tooltip
|
||||
/// STRINGS.MYNAME.MYNAMESPACE.OPTIONS.TESTPROPERTY.TOOLTIP, and the category
|
||||
/// STRINGS.MYNAME.MYNAMESPACE.OPTIONS.TESTPROPERTY.CATEGORY.
|
||||
/// </summary>
|
||||
public OptionAttribute() {
|
||||
Format = null;
|
||||
Title = null;
|
||||
Tooltip = null;
|
||||
Category = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Denotes a mod option field. Can also be used on members of an Enum type to give
|
||||
/// them a friendly display name.
|
||||
/// </summary>
|
||||
/// <param name="title">The field title to display.</param>
|
||||
/// <param name="tooltip">The tool tip for the field.</param>
|
||||
/// <param name="category">The category to use, or null for the default category.</param>
|
||||
public OptionAttribute(string title, string tooltip = null, string category = null) {
|
||||
if (string.IsNullOrEmpty(title))
|
||||
throw new ArgumentNullException(nameof(title));
|
||||
Category = category;
|
||||
Format = null;
|
||||
Title = title;
|
||||
Tooltip = tooltip;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return Title;
|
||||
}
|
||||
}
|
||||
}
|
||||
473
mod/PLibOptions/OptionsDialog.cs
Normal file
473
mod/PLibOptions/OptionsDialog.cs
Normal file
@@ -0,0 +1,473 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using PeterHan.PLib.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
using OptionsList = System.Collections.Generic.ICollection<PeterHan.PLib.Options.IOptionsEntry>;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// A dialog for handling mod options events.
|
||||
/// </summary>
|
||||
internal sealed class OptionsDialog {
|
||||
/// <summary>
|
||||
/// The color of option category titles.
|
||||
/// </summary>
|
||||
private static readonly Color CATEGORY_TITLE_COLOR = new Color32(143, 150, 175, 255);
|
||||
|
||||
/// <summary>
|
||||
/// The text style applied to option category titles.
|
||||
/// </summary>
|
||||
private static readonly TextStyleSetting CATEGORY_TITLE_STYLE;
|
||||
|
||||
/// <summary>
|
||||
/// The margins inside the colored boxes in each config section.
|
||||
/// </summary>
|
||||
private static readonly int CATEGORY_MARGIN = 8;
|
||||
|
||||
/// <summary>
|
||||
/// The size of the mod preview image displayed.
|
||||
/// </summary>
|
||||
private static readonly Vector2 MOD_IMAGE_SIZE = new Vector2(192.0f, 192.0f);
|
||||
|
||||
/// <summary>
|
||||
/// The margins between the dialog edge and the colored boxes in each config section.
|
||||
/// </summary>
|
||||
private static readonly int OUTER_MARGIN = 10;
|
||||
|
||||
/// <summary>
|
||||
/// The default size of the Mod Settings dialog.
|
||||
/// </summary>
|
||||
private static readonly Vector2 SETTINGS_DIALOG_SIZE = new Vector2(320.0f, 200.0f);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum size of the Mod Settings dialog before it gets scroll bars.
|
||||
/// </summary>
|
||||
private static readonly Vector2 SETTINGS_DIALOG_MAX_SIZE = new Vector2(800.0f, 600.0f);
|
||||
|
||||
/// <summary>
|
||||
/// The size of the toggle button on each (non-default) config section.
|
||||
/// </summary>
|
||||
private static readonly Vector2 TOGGLE_SIZE = new Vector2(12.0f, 12.0f);
|
||||
|
||||
static OptionsDialog() {
|
||||
CATEGORY_TITLE_STYLE = PUITuning.Fonts.UILightStyle.DeriveStyle(newColor:
|
||||
CATEGORY_TITLE_COLOR, style: TMPro.FontStyles.Bold);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an options object using the default constructor if possible.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the object to create.</param>
|
||||
internal static object CreateOptions(Type type) {
|
||||
object result = null;
|
||||
try {
|
||||
var cons = type.GetConstructor(Type.EmptyTypes);
|
||||
if (cons != null)
|
||||
result = cons.Invoke(null);
|
||||
} catch (TargetInvocationException e) {
|
||||
// Other mod's error
|
||||
PUtil.LogExcWarn(e.GetBaseException());
|
||||
} catch (AmbiguousMatchException e) {
|
||||
// Other mod's error
|
||||
PUtil.LogException(e);
|
||||
} catch (MemberAccessException e) {
|
||||
// Other mod's error
|
||||
PUtil.LogException(e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the mod enabled settings and restarts the game.
|
||||
/// </summary>
|
||||
private static void SaveAndRestart() {
|
||||
#if OPTIONS_ONLY
|
||||
POptionsPatches.SaveMods();
|
||||
#else
|
||||
PGameUtils.SaveMods();
|
||||
#endif
|
||||
App.instance.Restart();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If true, all categories begin collapsed.
|
||||
/// </summary>
|
||||
private readonly bool collapseCategories;
|
||||
|
||||
/// <summary>
|
||||
/// The config file attribute for the options type, if present.
|
||||
/// </summary>
|
||||
private readonly ConfigFileAttribute configAttr;
|
||||
|
||||
/// <summary>
|
||||
/// The currently active dialog.
|
||||
/// </summary>
|
||||
private KScreen dialog;
|
||||
|
||||
/// <summary>
|
||||
/// The sprite to display for this mod.
|
||||
/// </summary>
|
||||
private Sprite modImage;
|
||||
|
||||
/// <summary>
|
||||
/// Collects information from the ModInfoAttribute and KMod.Mod objects for display.
|
||||
/// </summary>
|
||||
private readonly ModDialogInfo displayInfo;
|
||||
|
||||
/// <summary>
|
||||
/// The event to invoke when the dialog is closed.
|
||||
/// </summary>
|
||||
public Action<object> OnClose { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The option entries in the dialog.
|
||||
/// </summary>
|
||||
private readonly IDictionary<string, OptionsList> optionCategories;
|
||||
|
||||
/// <summary>
|
||||
/// The options read from the config. It might contain hidden options so preserve its
|
||||
/// contents here.
|
||||
/// </summary>
|
||||
private object options;
|
||||
|
||||
/// <summary>
|
||||
/// The type used to determine which options are visible.
|
||||
/// </summary>
|
||||
private readonly Type optionsType;
|
||||
|
||||
internal OptionsDialog(Type optionsType) {
|
||||
OnClose = null;
|
||||
dialog = null;
|
||||
modImage = null;
|
||||
this.optionsType = optionsType ?? throw new ArgumentNullException(nameof(
|
||||
optionsType));
|
||||
optionCategories = OptionsEntry.BuildOptions(optionsType);
|
||||
options = null;
|
||||
// Determine config location
|
||||
var infoAttr = optionsType.GetCustomAttribute<ModInfoAttribute>();
|
||||
collapseCategories = infoAttr != null && infoAttr.ForceCollapseCategories;
|
||||
configAttr = optionsType.GetCustomAttribute<ConfigFileAttribute>();
|
||||
displayInfo = new ModDialogInfo(optionsType, infoAttr?.URL, infoAttr?.Image);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a category header to the dialog.
|
||||
/// </summary>
|
||||
/// <param name="container">The parent of the header.</param>
|
||||
/// <param name="category">The header title.</param>
|
||||
/// <param name="contents">The panel containing the options in this category.</param>
|
||||
private void AddCategoryHeader(PGridPanel container, string category,
|
||||
PGridPanel contents) {
|
||||
contents.AddColumn(new GridColumnSpec(flex: 1.0f)).AddColumn(new GridColumnSpec());
|
||||
if (!string.IsNullOrEmpty(category)) {
|
||||
bool state = !collapseCategories;
|
||||
var handler = new CategoryExpandHandler(state);
|
||||
container.AddColumn(new GridColumnSpec()).AddColumn(new GridColumnSpec(
|
||||
flex: 1.0f)).AddRow(new GridRowSpec()).AddRow(new GridRowSpec(flex: 1.0f));
|
||||
// Toggle is upper left, header is upper right
|
||||
container.AddChild(new PLabel("CategoryHeader") {
|
||||
Text = OptionsEntry.LookInStrings(category), TextStyle =
|
||||
CATEGORY_TITLE_STYLE, TextAlignment = TextAnchor.LowerCenter
|
||||
}.AddOnRealize(handler.OnRealizeHeader), new GridComponentSpec(0, 1) {
|
||||
Margin = new RectOffset(OUTER_MARGIN, OUTER_MARGIN, 0, 0)
|
||||
}).AddChild(new PToggle("CategoryToggle") {
|
||||
Color = PUITuning.Colors.ComponentDarkStyle, InitialState = state,
|
||||
ToolTip = PLibStrings.TOOLTIP_TOGGLE, Size = TOGGLE_SIZE,
|
||||
OnStateChanged = handler.OnExpandContract
|
||||
}.AddOnRealize(handler.OnRealizeToggle), new GridComponentSpec(0, 0));
|
||||
contents.OnRealize += handler.OnRealizePanel;
|
||||
container.AddChild(contents, new GridComponentSpec(1, 0) { ColumnSpan = 2 });
|
||||
} else
|
||||
// Default of unconstrained fills the whole panel
|
||||
container.AddColumn(new GridColumnSpec(flex: 1.0f)).AddRow(new GridRowSpec(
|
||||
flex: 1.0f)).AddChild(contents, new GridComponentSpec(0, 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills in the mod info screen, assuming that infoAttr is non-null.
|
||||
/// </summary>
|
||||
/// <param name="optionsDialog">The dialog to populate.</param>
|
||||
private void AddModInfoScreen(PDialog optionsDialog) {
|
||||
string image = displayInfo.Image;
|
||||
var body = optionsDialog.Body;
|
||||
// Try to load the mod image sprite if possible
|
||||
if (modImage == null && !string.IsNullOrEmpty(image)) {
|
||||
string rootDir = PUtil.GetModPath(optionsType.Assembly);
|
||||
modImage = PUIUtils.LoadSpriteFile(rootDir == null ? image : Path.Combine(
|
||||
rootDir, image));
|
||||
}
|
||||
var websiteButton = new PButton("ModSite") {
|
||||
Text = PLibStrings.MOD_HOMEPAGE, ToolTip = PLibStrings.TOOLTIP_HOMEPAGE,
|
||||
OnClick = VisitModHomepage, Margin = PDialog.BUTTON_MARGIN
|
||||
}.SetKleiBlueStyle();
|
||||
var versionLabel = new PLabel("ModVersion") {
|
||||
Text = displayInfo.Version, ToolTip = PLibStrings.TOOLTIP_VERSION,
|
||||
TextStyle = PUITuning.Fonts.UILightStyle, Margin = new RectOffset(0, 0,
|
||||
OUTER_MARGIN, 0)
|
||||
};
|
||||
// Find mod URL
|
||||
string modURL = displayInfo.URL;
|
||||
if (modImage != null) {
|
||||
// 2 rows and 1 column
|
||||
if (optionCategories.Count > 0)
|
||||
body.Direction = PanelDirection.Horizontal;
|
||||
var infoPanel = new PPanel("ModInfo") {
|
||||
FlexSize = Vector2.up, Direction = PanelDirection.Vertical,
|
||||
Alignment = TextAnchor.UpperCenter
|
||||
}.AddChild(new PLabel("ModImage") {
|
||||
SpriteSize = MOD_IMAGE_SIZE, TextAlignment = TextAnchor.UpperLeft,
|
||||
Margin = new RectOffset(0, OUTER_MARGIN, 0, OUTER_MARGIN),
|
||||
Sprite = modImage
|
||||
});
|
||||
if (!string.IsNullOrEmpty(modURL))
|
||||
infoPanel.AddChild(websiteButton);
|
||||
body.AddChild(infoPanel.AddChild(versionLabel));
|
||||
} else {
|
||||
if (!string.IsNullOrEmpty(modURL))
|
||||
body.AddChild(websiteButton);
|
||||
body.AddChild(versionLabel);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks the mod config class for the [RestartRequired] attribute, and brings up a
|
||||
/// restart dialog if necessary.
|
||||
/// </summary>
|
||||
private void CheckForRestart() {
|
||||
if (options != null && options.GetType().GetCustomAttribute(typeof(
|
||||
RestartRequiredAttribute)) != null)
|
||||
// Prompt user to restart
|
||||
PUIElements.ShowConfirmDialog(null, PLibStrings.RESTART_REQUIRED,
|
||||
SaveAndRestart, null, PLibStrings.RESTART_OK, PLibStrings.RESTART_CANCEL);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the current dialog.
|
||||
/// </summary>
|
||||
private void CloseDialog() {
|
||||
if (dialog != null) {
|
||||
dialog.Deactivate();
|
||||
// dialog's game object is destroyed by Deactivate()
|
||||
dialog = null;
|
||||
}
|
||||
if (modImage != null) {
|
||||
UnityEngine.Object.Destroy(modImage);
|
||||
modImage = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills in the actual mod option fields.
|
||||
/// </summary>
|
||||
/// <param name="optionsDialog">The dialog to populate.</param>
|
||||
private void FillModOptions(PDialog optionsDialog) {
|
||||
IEnumerable<IOptionsEntry> dynamicOptions;
|
||||
var body = optionsDialog.Body;
|
||||
var margin = new RectOffset(CATEGORY_MARGIN, CATEGORY_MARGIN, CATEGORY_MARGIN,
|
||||
CATEGORY_MARGIN);
|
||||
// For each option, add its UI component to panel
|
||||
body.Margin = new RectOffset();
|
||||
var scrollBody = new PPanel("ScrollContent") {
|
||||
Spacing = OUTER_MARGIN, Direction = PanelDirection.Vertical, Alignment =
|
||||
TextAnchor.UpperCenter, FlexSize = Vector2.right
|
||||
};
|
||||
var allOptions = optionCategories;
|
||||
// Add options from the user's class
|
||||
if (options is IOptions dynOptions && (dynamicOptions = dynOptions.
|
||||
CreateOptions()) != null) {
|
||||
allOptions = new Dictionary<string, OptionsList>(optionCategories);
|
||||
foreach (var dynamicOption in dynamicOptions)
|
||||
OptionsEntry.AddToCategory(allOptions, dynamicOption);
|
||||
}
|
||||
// Display all categories
|
||||
foreach (var catEntries in allOptions) {
|
||||
string category = catEntries.Key;
|
||||
var optionsList = catEntries.Value;
|
||||
if (optionsList.Count > 0) {
|
||||
string name = string.IsNullOrEmpty(category) ? "Default" : category;
|
||||
int i = 0;
|
||||
// Not optimal for layout performance, but the panel is needed to have a
|
||||
// different background color for each category "box"
|
||||
var container = new PGridPanel("Category_" + name) {
|
||||
Margin = margin, BackColor = PUITuning.Colors.DialogDarkBackground,
|
||||
FlexSize = Vector2.right
|
||||
};
|
||||
// Needs to be a separate panel so that it can be collapsed
|
||||
var contents = new PGridPanel("Entries") { FlexSize = Vector2.right };
|
||||
AddCategoryHeader(container, catEntries.Key, contents);
|
||||
foreach (var entry in optionsList) {
|
||||
contents.AddRow(new GridRowSpec());
|
||||
entry.CreateUIEntry(contents, ref i);
|
||||
i++;
|
||||
}
|
||||
scrollBody.AddChild(container);
|
||||
}
|
||||
}
|
||||
// Manual config and reset button
|
||||
scrollBody.AddChild(new PPanel("ConfigButtons") {
|
||||
Spacing = 10, Direction = PanelDirection.Horizontal, Alignment =
|
||||
TextAnchor.MiddleCenter, FlexSize = Vector2.right
|
||||
}.AddChild(new PButton("ManualConfig") {
|
||||
Text = PLibStrings.BUTTON_MANUAL, ToolTip = PLibStrings.TOOLTIP_MANUAL,
|
||||
OnClick = OnManualConfig, TextAlignment = TextAnchor.MiddleCenter, Margin =
|
||||
PDialog.BUTTON_MARGIN
|
||||
}.SetKleiBlueStyle()).AddChild(new PButton("ResetConfig") {
|
||||
Text = PLibStrings.BUTTON_RESET, ToolTip = PLibStrings.TOOLTIP_RESET,
|
||||
OnClick = OnResetConfig, TextAlignment = TextAnchor.MiddleCenter, Margin =
|
||||
PDialog.BUTTON_MARGIN
|
||||
}.SetKleiBlueStyle()));
|
||||
body.AddChild(new PScrollPane() {
|
||||
ScrollHorizontal = false, ScrollVertical = allOptions.Count > 0,
|
||||
Child = scrollBody, FlexSize = Vector2.right, TrackSize = 8,
|
||||
AlwaysShowHorizontal = false, AlwaysShowVertical = false
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the manual config button is pressed.
|
||||
/// </summary>
|
||||
private void OnManualConfig(GameObject _) {
|
||||
string uri = null, path = POptions.GetConfigFilePath(optionsType);
|
||||
try {
|
||||
uri = new Uri(Path.GetDirectoryName(path) ?? path).AbsoluteUri;
|
||||
} catch (UriFormatException e) {
|
||||
PUtil.LogWarning("Unable to convert parent of " + path + " to a URI:");
|
||||
PUtil.LogExcWarn(e);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(uri)) {
|
||||
// Open the config folder, opening the file itself might start an unknown
|
||||
// editor which could execute the json somehow...
|
||||
WriteOptions();
|
||||
CloseDialog();
|
||||
PUtil.LogDebug("Opening config folder: " + uri);
|
||||
Application.OpenURL(uri);
|
||||
CheckForRestart();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the dialog is closed.
|
||||
/// </summary>
|
||||
/// <param name="action">The action key taken.</param>
|
||||
private void OnOptionsSelected(string action) {
|
||||
switch (action) {
|
||||
case "ok":
|
||||
// Save changes to mod options
|
||||
WriteOptions();
|
||||
CheckForRestart();
|
||||
break;
|
||||
case PDialog.DIALOG_KEY_CLOSE:
|
||||
OnClose?.Invoke(options);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the reset to default button is pressed.
|
||||
/// </summary>
|
||||
private void OnResetConfig(GameObject _) {
|
||||
options = CreateOptions(optionsType);
|
||||
UpdateOptions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when the Mod Options button is clicked.
|
||||
/// </summary>
|
||||
public void ShowDialog() {
|
||||
string title;
|
||||
if (string.IsNullOrEmpty(displayInfo.Title))
|
||||
title = PLibStrings.BUTTON_OPTIONS;
|
||||
else
|
||||
title = string.Format(PLibStrings.DIALOG_TITLE, OptionsEntry.LookInStrings(
|
||||
displayInfo.Title));
|
||||
// Close current dialog if open
|
||||
CloseDialog();
|
||||
// Ensure that it is on top of other screens (which may be +100 modal)
|
||||
var pDialog = new PDialog("ModOptions") {
|
||||
Title = title, Size = SETTINGS_DIALOG_SIZE, SortKey = 150.0f,
|
||||
DialogBackColor = PUITuning.Colors.OptionsBackground,
|
||||
DialogClosed = OnOptionsSelected, MaxSize = SETTINGS_DIALOG_MAX_SIZE,
|
||||
RoundToNearestEven = true
|
||||
}.AddButton("ok", STRINGS.UI.CONFIRMDIALOG.OK, PLibStrings.TOOLTIP_OK,
|
||||
PUITuning.Colors.ButtonPinkStyle).AddButton(PDialog.DIALOG_KEY_CLOSE,
|
||||
STRINGS.UI.CONFIRMDIALOG.CANCEL, PLibStrings.TOOLTIP_CANCEL,
|
||||
PUITuning.Colors.ButtonBlueStyle);
|
||||
options = POptions.ReadSettings(POptions.GetConfigFilePath(optionsType),
|
||||
optionsType) ?? CreateOptions(optionsType);
|
||||
AddModInfoScreen(pDialog);
|
||||
FillModOptions(pDialog);
|
||||
// Manually build the dialog so the options can be updated after realization
|
||||
var obj = pDialog.Build();
|
||||
UpdateOptions();
|
||||
if (obj.TryGetComponent(out dialog))
|
||||
dialog.Activate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls the user OnOptionsChanged handler if present.
|
||||
/// </summary>
|
||||
/// <param name="newOptions">The updated options object.</param>
|
||||
private void TriggerUpdateOptions(object newOptions) {
|
||||
// Call the user handler
|
||||
if (newOptions is IOptions onChanged)
|
||||
onChanged.OnOptionsChanged();
|
||||
OnClose?.Invoke(newOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the dialog with the latest options from the file.
|
||||
/// </summary>
|
||||
private void UpdateOptions() {
|
||||
// Read into local options
|
||||
if (options != null)
|
||||
foreach (var catEntries in optionCategories)
|
||||
foreach (var option in catEntries.Value)
|
||||
option.ReadFrom(options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If configured, opens the mod's home page in the default browser.
|
||||
/// </summary>
|
||||
private void VisitModHomepage(GameObject _) {
|
||||
if (!string.IsNullOrWhiteSpace(displayInfo.URL))
|
||||
Application.OpenURL(displayInfo.URL);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the mod options to its config file.
|
||||
/// </summary>
|
||||
private void WriteOptions() {
|
||||
if (options != null) {
|
||||
// Update from local options
|
||||
foreach (var catEntries in optionCategories)
|
||||
foreach (var option in catEntries.Value)
|
||||
option.WriteTo(options);
|
||||
POptions.WriteSettings(options, POptions.GetConfigFilePath(optionsType),
|
||||
configAttr?.IndentOutput ?? false);
|
||||
TriggerUpdateOptions(options);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
383
mod/PLibOptions/OptionsEntry.cs
Normal file
383
mod/PLibOptions/OptionsEntry.cs
Normal file
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
using OptionsList = System.Collections.Generic.ICollection<PeterHan.PLib.Options.IOptionsEntry>;
|
||||
using AllOptions = System.Collections.Generic.IDictionary<string, System.Collections.Generic.
|
||||
ICollection<PeterHan.PLib.Options.IOptionsEntry>>;
|
||||
using PeterHan.PLib.Core;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An abstract parent class containing methods shared by all built-in options handlers.
|
||||
/// </summary>
|
||||
public abstract class OptionsEntry : IOptionsEntry, IComparable<OptionsEntry>,
|
||||
IUIComponent {
|
||||
private const BindingFlags INSTANCE_PUBLIC = BindingFlags.Public | BindingFlags.
|
||||
Instance;
|
||||
|
||||
/// <summary>
|
||||
/// The margins around the control used in each entry.
|
||||
/// </summary>
|
||||
protected static readonly RectOffset CONTROL_MARGIN = new RectOffset(0, 0, 2, 2);
|
||||
|
||||
/// <summary>
|
||||
/// The margins around the label for each entry.
|
||||
/// </summary>
|
||||
protected static readonly RectOffset LABEL_MARGIN = new RectOffset(0, 5, 2, 2);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an options entry to the category list, creating a new category if necessary.
|
||||
/// </summary>
|
||||
/// <param name="entries">The existing categories.</param>
|
||||
/// <param name="entry">The option entry to add.</param>
|
||||
internal static void AddToCategory(AllOptions entries, IOptionsEntry entry) {
|
||||
string category = entry.Category ?? "";
|
||||
if (!entries.TryGetValue(category, out OptionsList inCat)) {
|
||||
inCat = new List<IOptionsEntry>(16);
|
||||
entries.Add(category, inCat);
|
||||
}
|
||||
inCat.Add(entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the options entries from the type.
|
||||
/// </summary>
|
||||
/// <param name="forType">The type of the options class.</param>
|
||||
/// <returns>A list of all public properties annotated for options dialogs.</returns>
|
||||
internal static AllOptions BuildOptions(Type forType) {
|
||||
var entries = new SortedList<string, OptionsList>(8);
|
||||
OptionsHandlers.InitPredefinedOptions();
|
||||
foreach (var prop in forType.GetProperties(INSTANCE_PUBLIC)) {
|
||||
// Must have the annotation
|
||||
var entry = TryCreateEntry(prop, 0);
|
||||
if (entry != null)
|
||||
AddToCategory(entries, entry);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a default UI entry. This entry will have the title and tool tip in the
|
||||
/// first column, and the provided UI component in the second column. Only one row is
|
||||
/// added by this method.
|
||||
/// </summary>
|
||||
/// <param name="entry">The options entry to be presented.</param>
|
||||
/// <param name="parent">The parent where the components will be added.</param>
|
||||
/// <param name="row">The row index where the components will be added.</param>
|
||||
/// <param name="presenter">The presenter that can display this option's value.</param>
|
||||
public static void CreateDefaultUIEntry(IOptionsEntry entry, PGridPanel parent,
|
||||
int row, IUIComponent presenter) {
|
||||
parent.AddChild(new PLabel("Label") {
|
||||
Text = LookInStrings(entry.Title), ToolTip = LookInStrings(entry.Tooltip),
|
||||
TextStyle = PUITuning.Fonts.TextLightStyle
|
||||
}, new GridComponentSpec(row, 0) {
|
||||
Margin = LABEL_MARGIN, Alignment = TextAnchor.MiddleLeft
|
||||
});
|
||||
parent.AddChild(presenter, new GridComponentSpec(row, 1) {
|
||||
Alignment = TextAnchor.MiddleRight, Margin = CONTROL_MARGIN
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a dynamic options entry.
|
||||
/// </summary>
|
||||
/// <param name="prop">The property to be created.</param>
|
||||
/// <param name="handler">The type which can handle the property.</param>
|
||||
/// <returns>The created entry, or null if no entry could be created.</returns>
|
||||
private static IOptionsEntry CreateDynamicOption(PropertyInfo prop, Type handler) {
|
||||
IOptionsEntry result = null;
|
||||
var constructors = handler.GetConstructors(BindingFlags.Public | BindingFlags.
|
||||
Instance);
|
||||
string name = prop.Name;
|
||||
int n = constructors.Length;
|
||||
for (int i = 0; i < n && result == null; i++)
|
||||
try {
|
||||
if (ExecuteConstructor(prop, constructors[i]) is IOptionsEntry entry)
|
||||
result = entry;
|
||||
} catch (TargetInvocationException e) {
|
||||
PUtil.LogError("Unable to create option handler for property " +
|
||||
name + ":");
|
||||
PUtil.LogException(e.GetBaseException());
|
||||
} catch (MemberAccessException) {
|
||||
// Should never happen, filtered to public and instance
|
||||
} catch (AmbiguousMatchException) {
|
||||
} catch (TypeLoadException e) {
|
||||
PUtil.LogError("Unable to instantiate option handler for property " +
|
||||
name + ":");
|
||||
PUtil.LogException(e.GetBaseException());
|
||||
}
|
||||
if (result == null)
|
||||
PUtil.LogWarning("Unable to create option handler for property " +
|
||||
name + ", it must have a public constructor");
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a dynamic option constructor.
|
||||
/// </summary>
|
||||
/// <param name="prop">The property to be created.</param>
|
||||
/// <param name="cons">The constructor to run.</param>
|
||||
/// <returns>The constructed dynamic option.</returns>
|
||||
private static object ExecuteConstructor(PropertyInfo prop, ConstructorInfo cons) {
|
||||
object result;
|
||||
var parameters = cons.GetParameters();
|
||||
int p = parameters.Length;
|
||||
if (p == 0)
|
||||
// Public default
|
||||
result = cons.Invoke(null);
|
||||
else {
|
||||
var values = new object[p];
|
||||
for (int j = 0; j < p; j++) {
|
||||
var targetType = parameters[j].ParameterType;
|
||||
// Custom attribute
|
||||
if (typeof(Attribute).IsAssignableFrom(targetType))
|
||||
values[j] = prop.GetCustomAttribute(targetType);
|
||||
else if (targetType == typeof(IOptionSpec))
|
||||
values[j] = prop.GetCustomAttribute<OptionAttribute>();
|
||||
else if (targetType == typeof(string))
|
||||
values[j] = prop.Name;
|
||||
else
|
||||
PUtil.LogWarning("DynamicOption cannot handle constructor parameter of type " +
|
||||
targetType.FullName);
|
||||
}
|
||||
result = cons.Invoke(values);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Substitutes default strings for an options entry with an empty title.
|
||||
/// </summary>
|
||||
/// <param name="spec">The option attribute supplied (Format is still accepted!)</param>
|
||||
/// <param name="member">The item declaring the attribute.</param>
|
||||
/// <returns>A substitute attribute with default values from STRINGS.</returns>
|
||||
internal static IOptionSpec HandleDefaults(IOptionSpec spec, MemberInfo member) {
|
||||
// Replace with entries takem from the strings
|
||||
string prefix = "STRINGS.{0}.OPTIONS.{1}.".F(member.DeclaringType?.
|
||||
Namespace?.ToUpperInvariant(), member.Name.ToUpperInvariant());
|
||||
string category = "";
|
||||
if (Strings.TryGet(prefix + "CATEGORY", out var entry))
|
||||
category = entry.String;
|
||||
return new OptionAttribute(prefix + "NAME", prefix + "TOOLTIP", category) {
|
||||
Format = spec.Format
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// First looks to see if the string exists in the string database; if it does, returns
|
||||
/// the localized value, otherwise returns the string unmodified.
|
||||
///
|
||||
/// This method is somewhat slow. Cache the result if possible.
|
||||
/// </summary>
|
||||
/// <param name="keyOrValue">The string key to check.</param>
|
||||
/// <returns>The string value with that key, or the key if there is no such localized
|
||||
/// string value.</returns>
|
||||
public static string LookInStrings(string keyOrValue) {
|
||||
string result = keyOrValue;
|
||||
if (!string.IsNullOrEmpty(keyOrValue) && Strings.TryGet(keyOrValue, out var
|
||||
entry))
|
||||
result = entry.String;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared code to create an options entry if an [Option] attribute is found on a
|
||||
/// property.
|
||||
/// </summary>
|
||||
/// <param name="prop">The property to inspect.</param>
|
||||
/// <param name="depth">The current depth of iteration to avoid infinite loops.</param>
|
||||
/// <returns>The OptionsEntry created, or null if none was.</returns>
|
||||
internal static IOptionsEntry TryCreateEntry(PropertyInfo prop, int depth) {
|
||||
IOptionsEntry result = null;
|
||||
// Must have the annotation, cannot be indexed
|
||||
var indexes = prop.GetIndexParameters();
|
||||
if (indexes.Length < 1) {
|
||||
var attributes = ListPool<Attribute, OptionsEntry>.Allocate();
|
||||
bool dlcMatch = true;
|
||||
attributes.AddRange(prop.GetCustomAttributes());
|
||||
int n = attributes.Count;
|
||||
for (int i = 0; i < n; i++)
|
||||
// Do not create an entry if the DLC does not match
|
||||
if (attributes[i] is RequireDLCAttribute requireDLC && DlcManager.
|
||||
IsContentActive(requireDLC.DlcID) != requireDLC.Required) {
|
||||
dlcMatch = false;
|
||||
break;
|
||||
}
|
||||
if (dlcMatch)
|
||||
for (int i = 0; i < n; i++) {
|
||||
result = TryCreateEntry(attributes[i], prop, depth);
|
||||
if (result != null)
|
||||
break;
|
||||
}
|
||||
attributes.Recycle();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an options entry if an attribute is a valid IOptionSpec or
|
||||
/// DynamicOptionAttribute.
|
||||
/// </summary>
|
||||
/// <param name="attribute">The attribute to parse.</param>
|
||||
/// <param name="prop">The property to inspect.</param>
|
||||
/// <param name="depth">The current depth of iteration to avoid infinite loops.</param>
|
||||
/// <returns>The OptionsEntry created from the attribute, or null if none was.</returns>
|
||||
private static IOptionsEntry TryCreateEntry(Attribute attribute, PropertyInfo prop,
|
||||
int depth) {
|
||||
IOptionsEntry result = null;
|
||||
if (prop == null)
|
||||
throw new ArgumentNullException(nameof(prop));
|
||||
if (attribute is IOptionSpec spec) {
|
||||
if (string.IsNullOrEmpty(spec.Title))
|
||||
spec = HandleDefaults(spec, prop);
|
||||
// Attempt to find a class that will represent it
|
||||
var type = prop.PropertyType;
|
||||
result = OptionsHandlers.FindOptionClass(spec, prop);
|
||||
// See if it has entries that can themselves be added, ignore
|
||||
// value types and avoid infinite recursion
|
||||
if (result == null && !type.IsValueType && depth < 16 && type !=
|
||||
prop.DeclaringType)
|
||||
result = CompositeOptionsEntry.Create(spec, prop, depth);
|
||||
} else if (attribute is DynamicOptionAttribute doa &&
|
||||
typeof(IOptionsEntry).IsAssignableFrom(doa.Handler))
|
||||
result = CreateDynamicOption(prop, doa.Handler);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The category for this entry.
|
||||
/// </summary>
|
||||
public string Category { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The option field name.
|
||||
/// </summary>
|
||||
public string Field { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The format string to use when rendering this option, or null if none was supplied.
|
||||
/// </summary>
|
||||
public string Format { get; }
|
||||
|
||||
public virtual string Name => nameof(OptionsEntry);
|
||||
|
||||
public event PUIDelegates.OnRealize OnRealize;
|
||||
|
||||
/// <summary>
|
||||
/// The option title on screen.
|
||||
/// </summary>
|
||||
public string Title { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tool tip to display.
|
||||
/// </summary>
|
||||
public string Tooltip { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current value selected by the user.
|
||||
/// </summary>
|
||||
public abstract object Value { get; set; }
|
||||
|
||||
protected OptionsEntry(string field, IOptionSpec attr) {
|
||||
if (attr == null)
|
||||
throw new ArgumentNullException(nameof(attr));
|
||||
Field = field;
|
||||
Format = attr.Format;
|
||||
Title = attr.Title ?? throw new ArgumentException("attr.Title is null");
|
||||
Tooltip = attr.Tooltip;
|
||||
Category = attr.Category;
|
||||
}
|
||||
|
||||
public GameObject Build() {
|
||||
var comp = GetUIComponent();
|
||||
OnRealize?.Invoke(comp);
|
||||
return comp;
|
||||
}
|
||||
|
||||
public int CompareTo(OptionsEntry other) {
|
||||
if (other == null)
|
||||
throw new ArgumentNullException(nameof(other));
|
||||
return string.Compare(Category, other.Category, StringComparison.
|
||||
CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the line item entry for this options entry.
|
||||
/// </summary>
|
||||
/// <param name="parent">The location to add this entry.</param>
|
||||
/// <param name="row">The layout row index to use. If updated, the row index will
|
||||
/// continue to count up from the new value.</param>
|
||||
public virtual void CreateUIEntry(PGridPanel parent, ref int row) {
|
||||
CreateDefaultUIEntry(this, parent, row, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the UI component which can alter this setting. It should be sized
|
||||
/// properly to display any of the valid settings. The actual value will be set after
|
||||
/// the component is realized.
|
||||
/// </summary>
|
||||
/// <returns>The UI component to display.</returns>
|
||||
public abstract GameObject GetUIComponent();
|
||||
|
||||
public virtual void ReadFrom(object settings) {
|
||||
if (Field != null && settings != null)
|
||||
try {
|
||||
var prop = settings.GetType().GetProperty(Field, INSTANCE_PUBLIC);
|
||||
if (prop != null && prop.CanRead)
|
||||
Value = prop.GetValue(settings, null);
|
||||
} catch (TargetInvocationException e) {
|
||||
// Other mod's error
|
||||
PUtil.LogException(e);
|
||||
} catch (AmbiguousMatchException e) {
|
||||
// Other mod's error
|
||||
PUtil.LogException(e);
|
||||
} catch (InvalidCastException e) {
|
||||
// Our error!
|
||||
PUtil.LogException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return "{1}[field={0},title={2}]".F(Field, GetType().Name, Title);
|
||||
}
|
||||
|
||||
public virtual void WriteTo(object settings) {
|
||||
if (Field != null && settings != null)
|
||||
try {
|
||||
var prop = settings.GetType().GetProperty(Field, INSTANCE_PUBLIC);
|
||||
if (prop != null && prop.CanWrite)
|
||||
prop.SetValue(settings, Value, null);
|
||||
} catch (TargetInvocationException e) {
|
||||
// Other mod's error
|
||||
PUtil.LogException(e);
|
||||
} catch (AmbiguousMatchException e) {
|
||||
// Other mod's error
|
||||
PUtil.LogException(e);
|
||||
} catch (InvalidCastException e) {
|
||||
// Our error!
|
||||
PUtil.LogException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
151
mod/PLibOptions/OptionsHandlers.cs
Normal file
151
mod/PLibOptions/OptionsHandlers.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using PeterHan.PLib.Core;
|
||||
using PeterHan.PLib.Detours;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// Registers types to options entry classes that can handle them.
|
||||
/// </summary>
|
||||
public static class OptionsHandlers {
|
||||
private delegate IOptionsEntry CreateOption(string field, IOptionSpec spec);
|
||||
|
||||
private delegate IOptionsEntry CreateOptionType(string field, IOptionSpec spec,
|
||||
Type fieldType);
|
||||
|
||||
private delegate IOptionsEntry CreateOptionLimit(string field, IOptionSpec spec,
|
||||
LimitAttribute limit);
|
||||
|
||||
/// <summary>
|
||||
/// Maps types to the constructor delegate that can create an options entry for them.
|
||||
/// </summary>
|
||||
private static readonly IDictionary<Type, Delegate> OPTIONS_HANDLERS =
|
||||
new Dictionary<Type, Delegate>(64);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom type to handle all options entries of a specific type. The change
|
||||
/// will only affect this mod's options.
|
||||
/// </summary>
|
||||
/// <param name="optionType">The property type to be handled.</param>
|
||||
/// <param name="handlerType">The type which will handle all option attributes of
|
||||
/// this type. It must subclass from IOptionsEntry and have a constructor of the
|
||||
/// signature HandlerType(string, IOptionSpec).</param>
|
||||
public static void AddOptionClass(Type optionType, Type handlerType) {
|
||||
if (optionType != null && handlerType != null && !OPTIONS_HANDLERS.
|
||||
ContainsKey(optionType) && typeof(IOptionsEntry).IsAssignableFrom(
|
||||
handlerType)) {
|
||||
var constructors = handlerType.GetConstructors();
|
||||
int n = constructors.Length;
|
||||
for (int i = 0; i < n; i++) {
|
||||
var del = CreateDelegate(handlerType, constructors[i]);
|
||||
if (del != null) {
|
||||
OPTIONS_HANDLERS[optionType] = del;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If a candidate options entry constructor is valid, creates a delegate which can
|
||||
/// call the constructor.
|
||||
/// </summary>
|
||||
/// <param name="constructor">The constructor to wrap.</param>
|
||||
/// <param name="handlerType">The type which will handle all option attributes of this type.</param>
|
||||
/// <returns>If the constructor can be used to create an options entry, a delegate
|
||||
/// which calls the constructor using one of the delegate types declared in this class;
|
||||
/// otherwise, null.</returns>
|
||||
private static Delegate CreateDelegate(Type handlerType, ConstructorInfo constructor) {
|
||||
var param = constructor.GetParameters();
|
||||
int n = param.Length;
|
||||
Delegate result = null;
|
||||
// Must begin with string, IOptionsSpec
|
||||
if (n > 1 && param[0].ParameterType.IsAssignableFrom(typeof(string)) &&
|
||||
param[1].ParameterType.IsAssignableFrom(typeof(IOptionSpec))) {
|
||||
switch (n) {
|
||||
case 2:
|
||||
result = constructor.Detour<CreateOption>();
|
||||
break;
|
||||
case 3:
|
||||
var extraType = param[2].ParameterType;
|
||||
if (extraType.IsAssignableFrom(typeof(LimitAttribute)))
|
||||
result = constructor.Detour<CreateOptionLimit>();
|
||||
else if (extraType.IsAssignableFrom(typeof(Type)))
|
||||
result = constructor.Detour<CreateOptionType>();
|
||||
break;
|
||||
default:
|
||||
PUtil.LogWarning("Constructor on options handler type " + handlerType +
|
||||
" cannot be constructed by OptionsHandlers");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an options entry wrapper for the specified property.
|
||||
/// </summary>
|
||||
/// <param name="info">The property to wrap.</param>
|
||||
/// <param name="spec">The option title and tool tip.</param>
|
||||
/// <returns>An options wrapper, or null if none can handle this type.</returns>
|
||||
public static IOptionsEntry FindOptionClass(IOptionSpec spec, PropertyInfo info) {
|
||||
IOptionsEntry entry = null;
|
||||
if (spec != null && info != null) {
|
||||
var type = info.PropertyType;
|
||||
string field = info.Name;
|
||||
if (type.IsEnum)
|
||||
// Enumeration type
|
||||
entry = new SelectOneOptionsEntry(field, spec, type);
|
||||
else if (OPTIONS_HANDLERS.TryGetValue(type, out var handler)) {
|
||||
if (handler is CreateOption createOption)
|
||||
entry = createOption.Invoke(field, spec);
|
||||
else if (handler is CreateOptionLimit createOptionLimit)
|
||||
entry = createOptionLimit.Invoke(field, spec, info.
|
||||
GetCustomAttribute<LimitAttribute>());
|
||||
else if (handler is CreateOptionType createOptionType)
|
||||
entry = createOptionType.Invoke(field, spec, type);
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the predefined options classes.
|
||||
/// </summary>
|
||||
internal static void InitPredefinedOptions() {
|
||||
if (OPTIONS_HANDLERS.Count < 1) {
|
||||
AddOptionClass(typeof(bool), typeof(CheckboxOptionsEntry));
|
||||
AddOptionClass(typeof(int), typeof(IntOptionsEntry));
|
||||
AddOptionClass(typeof(int?), typeof(NullableIntOptionsEntry));
|
||||
AddOptionClass(typeof(float), typeof(FloatOptionsEntry));
|
||||
AddOptionClass(typeof(float?), typeof(NullableFloatOptionsEntry));
|
||||
AddOptionClass(typeof(Color32), typeof(Color32OptionsEntry));
|
||||
AddOptionClass(typeof(Color), typeof(ColorOptionsEntry));
|
||||
AddOptionClass(typeof(int), typeof(IntOptionsEntry));
|
||||
AddOptionClass(typeof(Action<object>), typeof(ButtonOptionsEntry));
|
||||
AddOptionClass(typeof(int), typeof(IntOptionsEntry));
|
||||
AddOptionClass(typeof(LocText), typeof(TextBlockOptionsEntry));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
mod/PLibOptions/PLibOptions.csproj
Normal file
21
mod/PLibOptions/PLibOptions.csproj
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Title>PLib Options</Title>
|
||||
<AssemblyTitle>PLib.Options</AssemblyTitle>
|
||||
<Version>4.11.0.0</Version>
|
||||
<UsesPLib>false</UsesPLib>
|
||||
<RootNamespace>PeterHan.PLib.Options</RootNamespace>
|
||||
<AssemblyVersion>4.11.0.0</AssemblyVersion>
|
||||
<DistributeMod>false</DistributeMod>
|
||||
<PLibCore>true</PLibCore>
|
||||
<Platforms>Vanilla;Mergedown</Platforms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DocumentationFile>bin\$(Platform)\Release\PLibOptions.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../PLibCore/PLibCore.csproj" />
|
||||
<ProjectReference Include="../PLibUI/PLibUI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
405
mod/PLibOptions/POptions.cs
Normal file
405
mod/PLibOptions/POptions.cs
Normal file
@@ -0,0 +1,405 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using HarmonyLib;
|
||||
using Newtonsoft.Json;
|
||||
using PeterHan.PLib.Core;
|
||||
using PeterHan.PLib.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// Adds an "Options" screen to a mod in the Mods menu.
|
||||
/// </summary>
|
||||
public sealed class POptions : PForwardedComponent {
|
||||
/// <summary>
|
||||
/// The configuration file name used by default for classes that do not specify
|
||||
/// otherwise. This file name is case sensitive.
|
||||
/// </summary>
|
||||
public const string CONFIG_FILE_NAME = "config.json";
|
||||
|
||||
/// <summary>
|
||||
/// The maximum nested class depth which will be serialized in mod options to avoid
|
||||
/// infinite loops.
|
||||
/// </summary>
|
||||
public const int MAX_SERIALIZATION_DEPTH = 8;
|
||||
|
||||
/// <summary>
|
||||
/// The margins around the Options button.
|
||||
/// </summary>
|
||||
internal static readonly RectOffset OPTION_BUTTON_MARGIN = new RectOffset(11, 11, 5, 5);
|
||||
|
||||
/// <summary>
|
||||
/// The shared mod configuration folder, which works between archived versions and
|
||||
/// local/dev/Steam.
|
||||
/// </summary>
|
||||
public const string SHARED_CONFIG_FOLDER = "config";
|
||||
|
||||
/// <summary>
|
||||
/// The version of this component. Uses the running PLib version.
|
||||
/// </summary>
|
||||
internal static readonly Version VERSION = new Version(PVersion.VERSION);
|
||||
|
||||
/// <summary>
|
||||
/// The instantiated copy of this class.
|
||||
/// </summary>
|
||||
internal static POptions Instance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Applied to ModsScreen if mod options are registered, after BuildDisplay runs.
|
||||
/// </summary>
|
||||
private static void BuildDisplay_Postfix(GameObject ___entryPrefab,
|
||||
System.Collections.IEnumerable ___displayedMods) {
|
||||
if (Instance != null) {
|
||||
int index = 0;
|
||||
// Harmony does not check the type at all on accessing private fields with ___
|
||||
foreach (var displayedMod in ___displayedMods)
|
||||
Instance.AddModOptions(displayedMod, index++, ___entryPrefab);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the configuration file path used by PLib Options for a specified type.
|
||||
/// </summary>
|
||||
/// <param name="optionsType">The options type stored in the config file.</param>
|
||||
/// <returns>The path to the configuration file that will be used by PLib for that
|
||||
/// mod's config.</returns>
|
||||
public static string GetConfigFilePath(Type optionsType) {
|
||||
return GetConfigPath(optionsType.GetCustomAttribute<ConfigFileAttribute>(),
|
||||
optionsType.Assembly);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to find the mod which owns the specified type.
|
||||
/// </summary>
|
||||
/// <param name="optionsType">The type to look up.</param>
|
||||
/// <returns>The Mod that owns it, or null if no owning mod could be found, such as for
|
||||
/// types in System or Assembly-CSharp.</returns>
|
||||
internal static KMod.Mod GetModFromType(Type optionsType) {
|
||||
if (optionsType == null)
|
||||
throw new ArgumentNullException(nameof(optionsType));
|
||||
var sd = PRegistry.Instance.GetSharedData(typeof(POptions).FullName);
|
||||
// Look up mod in the shared data
|
||||
if (!(sd is IDictionary<Assembly, KMod.Mod> lookup) || !lookup.TryGetValue(
|
||||
optionsType.Assembly, out KMod.Mod result))
|
||||
result = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the configuration file path used by PLib Options for a specified type.
|
||||
/// </summary>
|
||||
/// <param name="attr">The config file attribute for that type.</param>
|
||||
/// <param name="modAssembly">The assembly to use for determining the path.</param>
|
||||
/// <returns>The path to the configuration file that will be used by PLib for that
|
||||
/// mod's config.</returns>
|
||||
private static string GetConfigPath(ConfigFileAttribute attr, Assembly modAssembly) {
|
||||
string path, name = modAssembly.GetNameSafe();
|
||||
path = (name != null && (attr?.UseSharedConfigLocation == true)) ?
|
||||
Path.Combine(KMod.Manager.GetDirectory(), SHARED_CONFIG_FOLDER, name) :
|
||||
PUtil.GetModPath(modAssembly);
|
||||
return Path.Combine(path, attr?.ConfigFileName ?? CONFIG_FILE_NAME);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a mod's settings from its configuration file. The assembly defining T is used
|
||||
/// to resolve the proper settings folder.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the settings object.</typeparam>
|
||||
/// <returns>The settings read, or null if they could not be read (e.g. newly installed).</returns>
|
||||
public static T ReadSettings<T>() where T : class {
|
||||
var type = typeof(T);
|
||||
return ReadSettings(GetConfigPath(type.GetCustomAttribute<ConfigFileAttribute>(),
|
||||
type.Assembly), type) as T;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a mod's settings from its configuration file.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the settings file.</param>
|
||||
/// <param name="optionsType">The options type.</param>
|
||||
/// <returns>The settings read, or null if they could not be read (e.g. newly installed)</returns>
|
||||
internal static object ReadSettings(string path, Type optionsType) {
|
||||
object options = null;
|
||||
try {
|
||||
using (var jr = new JsonTextReader(File.OpenText(path))) {
|
||||
var serializer = new JsonSerializer { MaxDepth = MAX_SERIALIZATION_DEPTH };
|
||||
// Deserialize from stream avoids reading file text into memory
|
||||
options = serializer.Deserialize(jr, optionsType);
|
||||
}
|
||||
} catch (FileNotFoundException) {
|
||||
#if DEBUG
|
||||
PUtil.LogDebug("{0} was not found; using default settings".F(Path.GetFileName(
|
||||
path)));
|
||||
#endif
|
||||
} catch (UnauthorizedAccessException e) {
|
||||
// Options will be set to defaults
|
||||
PUtil.LogExcWarn(e);
|
||||
} catch (IOException e) {
|
||||
// Again set defaults
|
||||
PUtil.LogExcWarn(e);
|
||||
} catch (JsonException e) {
|
||||
// Again set defaults
|
||||
PUtil.LogExcWarn(e);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows a mod options dialog now, as if Options was used inside the Mods menu.
|
||||
/// </summary>
|
||||
/// <param name="optionsType">The type of the options to show. The mod to configure,
|
||||
/// configuration directory, and so forth will be retrieved from the provided type.
|
||||
/// This type must be the same type configured in RegisterOptions for the mod.</param>
|
||||
/// <param name="onClose">The method to call when the dialog is closed.</param>
|
||||
public static void ShowDialog(Type optionsType, Action<object> onClose = null) {
|
||||
var args = new OpenDialogArgs(optionsType, onClose);
|
||||
var allOptions = PRegistry.Instance.GetAllComponents(typeof(POptions).FullName);
|
||||
if (allOptions != null)
|
||||
foreach (var mod in allOptions)
|
||||
mod?.Process(0, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a mod's settings to its configuration file. The assembly defining T is used
|
||||
/// to resolve the proper settings folder.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the settings object.</typeparam>
|
||||
/// <param name="settings">The settings to write.</param>
|
||||
public static void WriteSettings<T>(T settings) where T : class {
|
||||
var attr = typeof(T).GetCustomAttribute<ConfigFileAttribute>();
|
||||
WriteSettings(settings, GetConfigPath(attr, typeof(T).Assembly), attr?.
|
||||
IndentOutput ?? false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a mod's settings to its configuration file.
|
||||
/// </summary>
|
||||
/// <param name="settings">The settings to write.</param>
|
||||
/// <param name="path">The path to the settings file.</param>
|
||||
/// <param name="indent">true to indent the output, or false to leave it in one line.</param>
|
||||
internal static void WriteSettings(object settings, string path, bool indent = false) {
|
||||
if (settings != null)
|
||||
try {
|
||||
// SharedConfigLocation
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
using (var jw = new JsonTextWriter(File.CreateText(path))) {
|
||||
var serializer = new JsonSerializer {
|
||||
MaxDepth = MAX_SERIALIZATION_DEPTH
|
||||
};
|
||||
serializer.Formatting = indent ? Formatting.Indented : Formatting.None;
|
||||
// Serialize from stream avoids creating file text in memory
|
||||
serializer.Serialize(jw, settings);
|
||||
}
|
||||
} catch (UnauthorizedAccessException e) {
|
||||
// Options cannot be set
|
||||
PUtil.LogExcWarn(e);
|
||||
} catch (IOException e) {
|
||||
// Options cannot be set
|
||||
PUtil.LogExcWarn(e);
|
||||
} catch (JsonException e) {
|
||||
// Options cannot be set
|
||||
PUtil.LogExcWarn(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps mod static IDs to their options.
|
||||
/// </summary>
|
||||
private readonly IDictionary<string, Type> modOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Maps mod assemblies to handlers that can fire their options. Only populated in
|
||||
/// the instantiated copy of POptions.
|
||||
/// </summary>
|
||||
private readonly IDictionary<string, ModOptionsHandler> registered;
|
||||
|
||||
public override Version Version => VERSION;
|
||||
|
||||
public POptions() {
|
||||
modOptions = new Dictionary<string, Type>(8);
|
||||
registered = new Dictionary<string, ModOptionsHandler>(32);
|
||||
InstanceData = modOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the Options button to the Mods screen.
|
||||
/// </summary>
|
||||
/// <param name="modEntry">The mod entry where the button should be added.</param>
|
||||
/// <param name="fallbackIndex">The index to use if it cannot be determined from the entry.</param>
|
||||
/// <param name="parent">The parent where the entries were added, used only if the
|
||||
/// fallback index is required.</param>
|
||||
private void AddModOptions(object modEntry, int fallbackIndex, GameObject parent) {
|
||||
var mods = Global.Instance.modManager?.mods;
|
||||
if (!PPatchTools.TryGetFieldValue(modEntry, "mod_index", out int index))
|
||||
index = fallbackIndex;
|
||||
if (!PPatchTools.TryGetFieldValue(modEntry, "rect_transform", out Transform
|
||||
transform))
|
||||
transform = parent.transform.GetChild(index);
|
||||
if (mods != null && index >= 0 && index < mods.Count && transform != null) {
|
||||
var modSpec = mods[index];
|
||||
string label = modSpec.staticID;
|
||||
if (modSpec.IsEnabledForActiveDlc() && registered.TryGetValue(label,
|
||||
out ModOptionsHandler handler)) {
|
||||
#if DEBUG
|
||||
PUtil.LogDebug("Adding options for mod: {0}".F(modSpec.staticID));
|
||||
#endif
|
||||
// Create delegate to open settings dialog
|
||||
new PButton("ModSettingsButton") {
|
||||
FlexSize = Vector2.up, OnClick = handler.ShowDialog,
|
||||
ToolTip = PLibStrings.DIALOG_TITLE.text.F(modSpec.title), Text =
|
||||
CultureInfo.CurrentCulture.TextInfo.ToTitleCase(PLibStrings.
|
||||
BUTTON_OPTIONS.text.ToLower()), Margin = OPTION_BUTTON_MARGIN
|
||||
// Move before the subscription and enable button
|
||||
}.SetKleiPinkStyle().AddTo(transform.gameObject, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes and stores the options table for quicker lookups later.
|
||||
/// </summary>
|
||||
public override void Initialize(Harmony plibInstance) {
|
||||
Instance = this;
|
||||
|
||||
registered.Clear();
|
||||
SetSharedData(PUtil.CreateAssemblyToModTable());
|
||||
foreach (var optionsProvider in PRegistry.Instance.GetAllComponents(ID)) {
|
||||
var options = optionsProvider.GetInstanceData<IDictionary<string, Type>>();
|
||||
if (options != null)
|
||||
// Map the static ID to the mod's option type and fire the correct handler
|
||||
foreach (var pair in options) {
|
||||
string label = pair.Key;
|
||||
if (registered.ContainsKey(label))
|
||||
PUtil.LogWarning("Mod {0} already has options registered - only one option type per mod".
|
||||
F(label ?? "?"));
|
||||
else
|
||||
registered.Add(label, new ModOptionsHandler(optionsProvider,
|
||||
pair.Value));
|
||||
}
|
||||
}
|
||||
|
||||
plibInstance.Patch(typeof(ModsScreen), "BuildDisplay", postfix: PatchMethod(nameof(
|
||||
BuildDisplay_Postfix)));
|
||||
}
|
||||
|
||||
public override void Process(uint operation, object args) {
|
||||
// POptions is no longer forwarded, show the dialog from the assembly that has the
|
||||
// options type - ignore calls that are not for our mod
|
||||
if (operation == 0 && PPatchTools.TryGetPropertyValue(args, nameof(OpenDialogArgs.
|
||||
OptionsType), out Type forType)) {
|
||||
foreach (var pair in modOptions)
|
||||
// Linear search is not phenomenal, but there is usually only one options
|
||||
// type per instance
|
||||
if (pair.Value == forType) {
|
||||
var dialog = new OptionsDialog(forType);
|
||||
if (PPatchTools.TryGetPropertyValue(args, nameof(OpenDialogArgs.
|
||||
OnClose), out Action<object> handler))
|
||||
dialog.OnClose = handler;
|
||||
dialog.ShowDialog();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a class as a mod options class. The type is registered for the mod
|
||||
/// instance specified, which is easily available in OnLoad.
|
||||
/// </summary>
|
||||
/// <param name="mod">The mod for which the type will be registered.</param>
|
||||
/// <param name="optionsType">The class which will represent the options for this mod.</param>
|
||||
public void RegisterOptions(KMod.UserMod2 mod, Type optionsType) {
|
||||
var kmod = mod?.mod;
|
||||
if (optionsType == null)
|
||||
throw new ArgumentNullException(nameof(optionsType));
|
||||
if (kmod == null)
|
||||
throw new ArgumentNullException(nameof(mod));
|
||||
RegisterForForwarding();
|
||||
string id = kmod.staticID;
|
||||
if (modOptions.TryGetValue(id, out Type curType))
|
||||
PUtil.LogWarning("Mod {0} already has options type {1}".F(id, curType.
|
||||
FullName));
|
||||
else {
|
||||
modOptions.Add(id, optionsType);
|
||||
PUtil.LogDebug("Registered mod options class {0} for {1}".F(optionsType.
|
||||
FullName, id));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the mod options dialog for a specific mod assembly.
|
||||
/// </summary>
|
||||
private sealed class ModOptionsHandler {
|
||||
/// <summary>
|
||||
/// The type whose options will be shown.
|
||||
/// </summary>
|
||||
private readonly Type forType;
|
||||
|
||||
/// <summary>
|
||||
/// The options instance that will handle the dialog.
|
||||
/// </summary>
|
||||
private readonly PForwardedComponent options;
|
||||
|
||||
internal ModOptionsHandler(PForwardedComponent options, Type forType) {
|
||||
this.forType = forType ?? throw new ArgumentNullException(nameof(forType));
|
||||
this.options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the options dialog.
|
||||
/// </summary>
|
||||
internal void ShowDialog(GameObject _) {
|
||||
options.Process(0, new OpenDialogArgs(forType, null));
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return "ModOptionsHandler[Type={0}]".F(forType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The arguments to be passed with message SHOW_DIALOG_MOD.
|
||||
/// </summary>
|
||||
private sealed class OpenDialogArgs {
|
||||
/// <summary>
|
||||
/// The handler (if not null) to be called when the dialog is closed.
|
||||
/// </summary>
|
||||
public Action<object> OnClose { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The mod options type to show.
|
||||
/// </summary>
|
||||
public Type OptionsType { get; }
|
||||
|
||||
public OpenDialogArgs(Type optionsType, Action<object> onClose) {
|
||||
OnClose = onClose;
|
||||
OptionsType = optionsType ?? throw new ArgumentNullException(
|
||||
nameof(optionsType));
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return "OpenDialogArgs[Type={0}]".F(OptionsType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
mod/PLibOptions/RequireDLCAttribute.cs
Normal file
68
mod/PLibOptions/RequireDLCAttribute.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.Core;
|
||||
using System;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An attribute placed on an option property for a class used as mod options in order to
|
||||
/// show or hide it for particular DLCs. If the option is hidden, the value currently
|
||||
/// in the options file is preserved unchanged when reading or writing.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
|
||||
public sealed class RequireDLCAttribute : Attribute {
|
||||
/// <summary>
|
||||
/// The DLC ID to check.
|
||||
/// </summary>
|
||||
public string DlcID { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the DLC is required, and the option is hidden if the DLC is inactive.
|
||||
/// If false, the DLC is forbidden, and the option is hidden if the DLC is active.
|
||||
/// </summary>
|
||||
public bool Required { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Annotates an option field as requiring the specified DLC. The [Option] attribute
|
||||
/// must also be present to be displayed at all.
|
||||
/// </summary>
|
||||
/// <param name="dlcID">The DLC ID to require. Must be one of:
|
||||
/// DlcManager.EXPANSION1_ID, DlcManager.VANILLA_ID</param>
|
||||
public RequireDLCAttribute(string dlcID) {
|
||||
DlcID = dlcID;
|
||||
Required = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Annotates an option field as requiring or forbidding the specified DLC. The
|
||||
/// [Option] attribute must also be present to be displayed at all.
|
||||
/// </summary>
|
||||
/// <param name="dlcID">The DLC ID to require or forbid. Must be one of:
|
||||
/// DlcManager.EXPANSION1_ID, DlcManager.VANILLA_ID</param>
|
||||
/// <param name="required">true to require the DLC, or false to forbid it.</param>
|
||||
public RequireDLCAttribute(string dlcID, bool required) {
|
||||
DlcID = dlcID ?? "";
|
||||
Required = required;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return "RequireDLC[DLC={0},require={1}]".F(DlcID, Required);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
mod/PLibOptions/RestartRequiredAttribute.cs
Normal file
30
mod/PLibOptions/RestartRequiredAttribute.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An empty marker attribute. If applied to an options class, PLib will notify the user
|
||||
/// that the game must be restarted to apply the options. This attribute will not work if
|
||||
/// it is applied to an individual option, only if applied to the class as a whole.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||
public sealed class RestartRequiredAttribute : Attribute {
|
||||
}
|
||||
}
|
||||
179
mod/PLibOptions/SelectOneOptionsEntry.cs
Normal file
179
mod/PLibOptions/SelectOneOptionsEntry.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using PeterHan.PLib.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An options entry which represents Enum and displays a spinner with text options.
|
||||
/// </summary>
|
||||
public class SelectOneOptionsEntry : OptionsEntry {
|
||||
/// <summary>
|
||||
/// Obtains the title and tool tip for an enumeration value.
|
||||
/// </summary>
|
||||
/// <param name="enumValue">The value in the enumeration.</param>
|
||||
/// <param name="fieldType">The type of the Enum field.</param>
|
||||
/// <returns>The matching Option</returns>
|
||||
private static EnumOption GetAttribute(object enumValue, Type fieldType) {
|
||||
if (enumValue == null)
|
||||
throw new ArgumentNullException(nameof(enumValue));
|
||||
string valueName = enumValue.ToString(), title = valueName, tooltip = "";
|
||||
foreach (var enumField in fieldType.GetMember(valueName, BindingFlags.Public |
|
||||
BindingFlags.Static))
|
||||
if (enumField.DeclaringType == fieldType) {
|
||||
// Search for OptionsAttribute
|
||||
foreach (var attrib in enumField.GetCustomAttributes(false))
|
||||
if (attrib is IOptionSpec spec) {
|
||||
if (string.IsNullOrEmpty(spec.Title))
|
||||
spec = HandleDefaults(spec, enumField);
|
||||
title = LookInStrings(spec.Title);
|
||||
tooltip = LookInStrings(spec.Tooltip);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return new EnumOption(title, tooltip, enumValue);
|
||||
}
|
||||
|
||||
public override object Value {
|
||||
get {
|
||||
return chosen?.Value;
|
||||
}
|
||||
set {
|
||||
// Find a matching value, if possible
|
||||
string valueStr = value?.ToString() ?? "";
|
||||
foreach (var option in options)
|
||||
if (valueStr == option.Value.ToString()) {
|
||||
chosen = option;
|
||||
Update();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The chosen item in the array.
|
||||
/// </summary>
|
||||
protected EnumOption chosen;
|
||||
|
||||
/// <summary>
|
||||
/// The realized item label.
|
||||
/// </summary>
|
||||
private GameObject comboBox;
|
||||
|
||||
/// <summary>
|
||||
/// The available options to cycle through.
|
||||
/// </summary>
|
||||
protected readonly IList<EnumOption> options;
|
||||
|
||||
public SelectOneOptionsEntry(string field, IOptionSpec spec, Type fieldType) :
|
||||
base(field, spec) {
|
||||
var eval = Enum.GetValues(fieldType);
|
||||
if (eval == null)
|
||||
throw new ArgumentException("No values, or invalid values, for enum");
|
||||
int n = eval.Length;
|
||||
if (n == 0)
|
||||
throw new ArgumentException("Enum has no declared members");
|
||||
chosen = null;
|
||||
comboBox = null;
|
||||
options = new List<EnumOption>(n);
|
||||
for (int i = 0; i < n; i++)
|
||||
options.Add(GetAttribute(eval.GetValue(i), fieldType));
|
||||
}
|
||||
|
||||
public override GameObject GetUIComponent() {
|
||||
// Find largest option to size the label appropriately, using em width this time!
|
||||
EnumOption firstOption = null;
|
||||
int maxLen = 0;
|
||||
foreach (var option in options) {
|
||||
int len = option.Title?.Trim()?.Length ?? 0;
|
||||
// Kerning each string is slow, so estimate based on em width instead
|
||||
if (firstOption == null && len > 0)
|
||||
firstOption = option;
|
||||
if (len > maxLen)
|
||||
maxLen = len;
|
||||
}
|
||||
comboBox = new PComboBox<EnumOption>("Select") {
|
||||
BackColor = PUITuning.Colors.ButtonPinkStyle, InitialItem = firstOption,
|
||||
Content = options, EntryColor = PUITuning.Colors.ButtonBlueStyle,
|
||||
TextStyle = PUITuning.Fonts.TextLightStyle, OnOptionSelected = UpdateValue,
|
||||
}.SetMinWidthInCharacters(maxLen).Build();
|
||||
Update();
|
||||
return comboBox;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the displayed text to match the current item.
|
||||
/// </summary>
|
||||
private void Update() {
|
||||
if (comboBox != null && chosen != null)
|
||||
PComboBox<EnumOption>.SetSelectedItem(comboBox, chosen, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when the value chosen from the combo box has been changed.
|
||||
/// </summary>
|
||||
/// <param name="selected">The value selected by the user.</param>
|
||||
private void UpdateValue(GameObject _, EnumOption selected) {
|
||||
if (selected != null)
|
||||
chosen = selected;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a selectable option.
|
||||
/// </summary>
|
||||
protected sealed class EnumOption : ITooltipListableOption {
|
||||
/// <summary>
|
||||
/// The option title.
|
||||
/// </summary>
|
||||
public string Title { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The option tool tip.
|
||||
/// </summary>
|
||||
public string ToolTip { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The value to assign if this option is chosen.
|
||||
/// </summary>
|
||||
public object Value { get; }
|
||||
|
||||
public EnumOption(string title, string toolTip, object value) {
|
||||
Title = title ?? throw new ArgumentNullException(nameof(title));
|
||||
ToolTip = toolTip;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public string GetProperName() {
|
||||
return Title;
|
||||
}
|
||||
|
||||
public string GetToolTipText() {
|
||||
return ToolTip;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return string.Format("Option[Title={0},Value={1}]", Title, Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
mod/PLibOptions/SingletonOptions.cs
Normal file
52
mod/PLibOptions/SingletonOptions.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// A class which can be used by mods to maintain a singleton of their options. This
|
||||
/// class should be the superclass of the mod options class, and <T> should be
|
||||
/// the type of the options class to store.
|
||||
///
|
||||
/// This class only initializes the mod options once by default. If the settings can
|
||||
/// be updated without restarting the game, update the Instance manually using
|
||||
/// IOptions.OnOptionsChanged. If the game has to be restarted anyways, add
|
||||
/// [RestartRequired].
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The mod options class to wrap.</typeparam>
|
||||
public abstract class SingletonOptions<T> where T : class, new() {
|
||||
/// <summary>
|
||||
/// The only instance of the singleton options.
|
||||
/// </summary>
|
||||
protected static T instance;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the program options, or lazily initializes them if not yet loaded.
|
||||
/// </summary>
|
||||
public static T Instance {
|
||||
get {
|
||||
if (instance == null)
|
||||
instance = POptions.ReadSettings<T>() ?? new T();
|
||||
return instance;
|
||||
}
|
||||
protected set {
|
||||
if (value != null)
|
||||
instance = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
105
mod/PLibOptions/SlidingBaseOptionsEntry.cs
Normal file
105
mod/PLibOptions/SlidingBaseOptionsEntry.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An options entry which displays a slider below it.
|
||||
/// </summary>
|
||||
public abstract class SlidingBaseOptionsEntry : OptionsEntry {
|
||||
/// <summary>
|
||||
/// The margin between the slider extra row and the rest of the dialog.
|
||||
/// </summary>
|
||||
internal static readonly RectOffset ENTRY_MARGIN = new RectOffset(15, 0, 0, 5);
|
||||
|
||||
/// <summary>
|
||||
/// The margin between the slider and its labels.
|
||||
/// </summary>
|
||||
internal static readonly RectOffset SLIDER_MARGIN = new RectOffset(10, 10, 0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// The limits allowed for the entry.
|
||||
/// </summary>
|
||||
protected readonly LimitAttribute limits;
|
||||
|
||||
/// <summary>
|
||||
/// The realized slider.
|
||||
/// </summary>
|
||||
protected GameObject slider;
|
||||
|
||||
protected SlidingBaseOptionsEntry(string field, IOptionSpec spec,
|
||||
LimitAttribute limit = null) : base(field, spec) {
|
||||
limits = limit;
|
||||
slider = null;
|
||||
}
|
||||
|
||||
public override void CreateUIEntry(PGridPanel parent, ref int row) {
|
||||
double minLimit, maxLimit;
|
||||
base.CreateUIEntry(parent, ref row);
|
||||
if (limits != null && (minLimit = limits.Minimum) > float.MinValue && (maxLimit =
|
||||
limits.Maximum) < float.MaxValue && maxLimit > minLimit) {
|
||||
// NaN will be false on either comparison
|
||||
var slider = GetSlider().AddOnRealize(OnRealizeSlider);
|
||||
// Min and max labels
|
||||
var minLabel = new PLabel("MinValue") {
|
||||
TextStyle = PUITuning.Fonts.TextLightStyle, Text = minLimit.
|
||||
ToString(Format ?? "G3"), TextAlignment = TextAnchor.MiddleRight
|
||||
};
|
||||
var maxLabel = new PLabel("MaxValue") {
|
||||
TextStyle = PUITuning.Fonts.TextLightStyle, Text = maxLimit.
|
||||
ToString(Format ?? "G3"), TextAlignment = TextAnchor.MiddleLeft
|
||||
};
|
||||
// Lay out left to right
|
||||
var panel = new PRelativePanel("Slider Grid") {
|
||||
FlexSize = Vector2.right, DynamicSize = false
|
||||
}.AddChild(slider).AddChild(minLabel).AddChild(maxLabel).AnchorYAxis(slider).
|
||||
AnchorYAxis(minLabel, 0.5f).AnchorYAxis(maxLabel, 0.5f).SetLeftEdge(
|
||||
minLabel, fraction: 0.0f).SetRightEdge(maxLabel, fraction: 1.0f).
|
||||
SetLeftEdge(slider, toRight: minLabel).SetRightEdge(slider, toLeft:
|
||||
maxLabel).SetMargin(slider, SLIDER_MARGIN);
|
||||
// Add another row for the slider
|
||||
parent.AddRow(new GridRowSpec());
|
||||
parent.AddChild(panel, new GridComponentSpec(++row, 0) {
|
||||
ColumnSpan = 2, Margin = ENTRY_MARGIN
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the initialized PLib slider to be used for value display.
|
||||
/// </summary>
|
||||
/// <returns>The slider to be used.</returns>
|
||||
protected abstract PSliderSingle GetSlider();
|
||||
|
||||
/// <summary>
|
||||
/// Called when the slider is realized.
|
||||
/// </summary>
|
||||
/// <param name="realized">The actual slider.</param>
|
||||
protected void OnRealizeSlider(GameObject realized) {
|
||||
slider = realized;
|
||||
Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the displayed value.
|
||||
/// </summary>
|
||||
protected abstract void Update();
|
||||
}
|
||||
}
|
||||
94
mod/PLibOptions/StringOptionsEntry.cs
Normal file
94
mod/PLibOptions/StringOptionsEntry.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.UI;
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An options entry which represents a string and displays a text field.
|
||||
/// </summary>
|
||||
public class StringOptionsEntry : OptionsEntry {
|
||||
public override object Value {
|
||||
get {
|
||||
return value;
|
||||
}
|
||||
set {
|
||||
this.value = (value == null) ? "" : value.ToString();
|
||||
Update();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The maximum entry length.
|
||||
/// </summary>
|
||||
private readonly int maxLength;
|
||||
|
||||
/// <summary>
|
||||
/// The realized text field.
|
||||
/// </summary>
|
||||
private GameObject textField;
|
||||
|
||||
/// <summary>
|
||||
/// The value in the text field.
|
||||
/// </summary>
|
||||
private string value;
|
||||
|
||||
public StringOptionsEntry(string field, IOptionSpec spec,
|
||||
LimitAttribute limit = null) : base(field, spec) {
|
||||
// Use the maximum limit value for the max length, if present
|
||||
if (limit != null)
|
||||
maxLength = Math.Max(2, (int)Math.Round(limit.Maximum));
|
||||
else
|
||||
maxLength = 256;
|
||||
textField = null;
|
||||
value = "";
|
||||
}
|
||||
|
||||
public override GameObject GetUIComponent() {
|
||||
textField = new PTextField() {
|
||||
OnTextChanged = OnTextChanged, ToolTip = LookInStrings(Tooltip),
|
||||
Text = value.ToString(), MinWidth = 128, Type = PTextField.FieldType.Text,
|
||||
MaxLength = maxLength
|
||||
}.Build();
|
||||
Update();
|
||||
return textField;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the input field's text is changed.
|
||||
/// </summary>
|
||||
/// <param name="text">The new text.</param>
|
||||
private void OnTextChanged(GameObject _, string text) {
|
||||
value = text;
|
||||
Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the displayed value.
|
||||
/// </summary>
|
||||
private void Update() {
|
||||
TMP_InputField field;
|
||||
if (textField != null && (field = textField.
|
||||
GetComponentInChildren<TMP_InputField>()) != null)
|
||||
field.text = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
76
mod/PLibOptions/TextBlockOptionsEntry.cs
Normal file
76
mod/PLibOptions/TextBlockOptionsEntry.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2022 Peter Han
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without
|
||||
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
||||
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using PeterHan.PLib.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace PeterHan.PLib.Options {
|
||||
/// <summary>
|
||||
/// An options entry that displays static text. Not intended to be serializable to the
|
||||
/// options file, instead declare a read-only property that returns null with a type of
|
||||
/// LocText, e.g:
|
||||
///
|
||||
/// [Option("Your text goes here", "Tool tip for the text")]
|
||||
/// public LocText MyLabel => null;
|
||||
///
|
||||
/// Unity font formatting can be used in the text. The name of a strings table entry can
|
||||
/// also be used to allow localization.
|
||||
/// </summary>
|
||||
public class TextBlockOptionsEntry : OptionsEntry {
|
||||
/// <summary>
|
||||
/// A font style that looks like TextLightStyle but allows word wrapping.
|
||||
/// </summary>
|
||||
private static readonly TextStyleSetting WRAP_TEXT_STYLE;
|
||||
|
||||
static TextBlockOptionsEntry() {
|
||||
WRAP_TEXT_STYLE = PUITuning.Fonts.TextLightStyle.DeriveStyle();
|
||||
WRAP_TEXT_STYLE.enableWordWrapping = true;
|
||||
}
|
||||
|
||||
public override object Value {
|
||||
get {
|
||||
return ignore;
|
||||
}
|
||||
set {
|
||||
if (value is LocText newValue)
|
||||
ignore = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This value is not used, it only exists to satisfy the contract.
|
||||
/// </summary>
|
||||
private LocText ignore;
|
||||
|
||||
public TextBlockOptionsEntry(string field, IOptionSpec spec) : base(field, spec) { }
|
||||
|
||||
public override void CreateUIEntry(PGridPanel parent, ref int row) {
|
||||
parent.AddChild(new PLabel(Field) {
|
||||
Text = LookInStrings(Title), ToolTip = LookInStrings(Tooltip),
|
||||
TextStyle = WRAP_TEXT_STYLE
|
||||
}, new GridComponentSpec(row, 0) {
|
||||
Margin = CONTROL_MARGIN, Alignment = TextAnchor.MiddleCenter, ColumnSpan = 2
|
||||
});
|
||||
}
|
||||
|
||||
public override GameObject GetUIComponent() {
|
||||
// Will not be invoked
|
||||
return new GameObject("Empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user