161 lines
5.9 KiB
C#
161 lines
5.9 KiB
C#
using System;
|
||
using System.Reflection;
|
||
using System.Text;
|
||
using SLSUtilities.General;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.EventSystems;
|
||
|
||
namespace Cielonos.Settings.UI
|
||
{
|
||
/// <summary>
|
||
/// 设置条目 UI 的抽象基类。
|
||
/// <para>
|
||
/// 持有对设置实例和字段的反射引用,子类负责具体的 UI 交互逻辑。
|
||
/// 通过 <see cref="Initialize"/> 绑定到指定设置实例的字段后,
|
||
/// 子类在 <see cref="SetupUI"/> 中配置 UI 组件,
|
||
/// 在 <see cref="RefreshValue"/> 中同步字段值到 UI 显示。
|
||
/// </para>
|
||
/// <para>
|
||
/// 支持鼠标悬停时通过 <see cref="OnHoverEnter"/> / <see cref="OnHoverExit"/>
|
||
/// 通知父级显示/隐藏右侧说明面板。
|
||
/// </para>
|
||
/// </summary>
|
||
public abstract class SettingsEntryBase : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
|
||
{
|
||
[SerializeField] protected TMP_Text labelText;
|
||
|
||
protected object settingsInstance;
|
||
protected FieldInfo fieldInfo;
|
||
protected Action onValueChanged;
|
||
|
||
/// <summary>条目的显示标题。</summary>
|
||
public string DisplayLabel { get; protected set; }
|
||
|
||
/// <summary>条目的说明文本(可为空,为空时悬停不触发说明面板)。</summary>
|
||
public string Description { get; protected set; }
|
||
|
||
/// <summary>鼠标悬停进入时触发,参数为 (标题, 说明文本)。由 SettingsUIPage 设置。</summary>
|
||
public Action<string, string> OnHoverEnter { get; set; }
|
||
|
||
/// <summary>鼠标悬停离开时触发。由 SettingsUIPage 设置。</summary>
|
||
public Action OnHoverExit { get; set; }
|
||
|
||
/// <summary>
|
||
/// 初始化设置条目,绑定到指定设置实例的字段。
|
||
/// </summary>
|
||
/// <param name="instance">设置类实例(如 GameplaySettings)。</param>
|
||
/// <param name="field">要绑定的字段反射信息。</param>
|
||
/// <param name="onChanged">字段值变更时的回调。</param>
|
||
public virtual void Initialize(object instance, FieldInfo field, Action onChanged)
|
||
{
|
||
settingsInstance = instance;
|
||
fieldInfo = field;
|
||
onValueChanged = onChanged;
|
||
|
||
// 优先使用 SettingsDisplayAttribute 提供的本地化键
|
||
var displayAttr = field.GetCustomAttribute<SettingsDisplayAttribute>();
|
||
if (displayAttr != null)
|
||
{
|
||
string localized = displayAttr.LabelKey.Localize(displayAttr.TableName);
|
||
DisplayLabel = !string.IsNullOrEmpty(localized) ? localized : displayAttr.LabelKey;
|
||
|
||
if (!string.IsNullOrEmpty(displayAttr.DescriptionKey))
|
||
{
|
||
string localizedDesc = displayAttr.DescriptionKey.Localize(displayAttr.TableName);
|
||
Description = !string.IsNullOrEmpty(localizedDesc)
|
||
? localizedDesc
|
||
: displayAttr.DescriptionKey;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
DisplayLabel = FormatFieldName(field.Name);
|
||
}
|
||
|
||
SetLabel(DisplayLabel);
|
||
SetupUI();
|
||
RefreshValue();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 子类实现:配置 UI 组件(如 Slider 的范围、Dropdown 的选项)。
|
||
/// 在 <see cref="Initialize"/> 中于 <see cref="RefreshValue"/> 之前调用。
|
||
/// </summary>
|
||
protected abstract void SetupUI();
|
||
|
||
/// <summary>
|
||
/// 子类实现:从字段读取当前值并更新 UI 显示。
|
||
/// </summary>
|
||
public abstract void RefreshValue();
|
||
|
||
/// <summary>
|
||
/// 将值写回设置字段,并通知外部值已变更。
|
||
/// </summary>
|
||
protected void SetFieldValue(object value)
|
||
{
|
||
fieldInfo.SetValue(settingsInstance, value);
|
||
onValueChanged?.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从设置字段读取当前值。
|
||
/// </summary>
|
||
protected object GetFieldValue()
|
||
{
|
||
return fieldInfo.GetValue(settingsInstance);
|
||
}
|
||
|
||
protected void SetLabel(string text)
|
||
{
|
||
if (labelText != null)
|
||
labelText.text = text;
|
||
}
|
||
|
||
// ──────────────────── 悬停事件 ────────────────────
|
||
|
||
public void OnPointerEnter(PointerEventData eventData)
|
||
{
|
||
OnHoverEnter?.Invoke(DisplayLabel, Description);
|
||
}
|
||
|
||
public void OnPointerExit(PointerEventData eventData)
|
||
{
|
||
OnHoverExit?.Invoke();
|
||
}
|
||
|
||
// ──────────────────── 工具方法 ────────────────────
|
||
|
||
/// <summary>
|
||
/// 将 camelCase 字段名转换为可读格式。
|
||
/// <para>
|
||
/// <c>cameraSensitivityX</c> → "Camera Sensitivity X"<br/>
|
||
/// <c>showFPS</c> → "Show FPS"<br/>
|
||
/// <c>invertYAxis</c> → "Invert Y Axis"
|
||
/// </para>
|
||
/// </summary>
|
||
protected static string FormatFieldName(string fieldName)
|
||
{
|
||
if (string.IsNullOrEmpty(fieldName)) return fieldName;
|
||
|
||
var sb = new StringBuilder(fieldName.Length + 4);
|
||
sb.Append(char.ToUpper(fieldName[0]));
|
||
|
||
for (int i = 1; i < fieldName.Length; i++)
|
||
{
|
||
char c = fieldName[i];
|
||
if (char.IsUpper(c))
|
||
{
|
||
bool prevIsLower = char.IsLower(fieldName[i - 1]);
|
||
bool nextIsLower = i + 1 < fieldName.Length && char.IsLower(fieldName[i + 1]);
|
||
if (prevIsLower || nextIsLower)
|
||
sb.Append(' ');
|
||
}
|
||
sb.Append(c);
|
||
}
|
||
|
||
return sb.ToString();
|
||
}
|
||
}
|
||
}
|