Files
ichni_Official/Assets/Scripts/UI/Base/Dropdown.cs
SoulliesOfficial a34461d31f 阶段性完成
2026-07-25 13:27:53 -04:00

299 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using System.Collections.Generic;
using Ichni.Localization;
using Michsky.MUIP;
using TMPro;
using UnityEngine;
using UnityEngine.Localization;
namespace Ichni.UI
{
/// <summary>
/// Settings 页面使用的下拉控件。
/// <para>
/// 本地化枚举项由 <see cref="SetUpLocalized(int,string,string[])"/> 在代码中传入 String Table 名称与
/// Entry Key。控件会创建对应的 <see cref="LocalizedString"/>,并在 Michsky 生成 Item 后为每个
/// 可见文字绑定 <see cref="LocalizedTMPText"/>;切换语言时不重建列表。
/// </para>
/// <para>
/// 分辨率、语言自称等运行时文本则使用 <see cref="SetUpRuntime"/>,不会附加本地化组件。
/// </para>
/// </summary>
public class Dropdown : SettingsUIElementBase
{
// 仅保存本次初始化创建的运行时引用,不在 Inspector 中维护本地化选项列表。
private readonly List<LocalizedString> _localizedOptions = new List<LocalizedString>();
// 保留原字段以兼容已有的外部读取。静态本地化选项使用稳定的占位名称;实际显示内容由
// 动态生成 Item 上的 LocalizedTMPText 管理。
[HideInInspector] public List<string> options = new List<string>();
public CustomDropdown dropdown;
public int selectedIndex;
public string selectedOption;
private bool _usesLocalizedOptions;
private bool _isValueChangeListenerRegistered;
private Coroutine _bindGeneratedItemsCoroutine;
/// <summary>
/// 使用代码提供的 String Table 与 Entry Key 初始化静态枚举选项。
/// <paramref name="entryKeys"/> 的顺序即设置枚举的索引顺序,必须保持稳定。
/// 标题和说明仍由其 TMP 对象上的 <see cref="LocalizedTMPText"/> 独立管理。
/// </summary>
public void SetUpLocalized(int initialValue, string tableCollection, params string[] entryKeys)
{
base.SetUp();
ClearLocalizedBindings();
ClearLocalizedOptionDefinitions();
_usesLocalizedOptions = true;
foreach (string entryKey in entryKeys)
{
_localizedOptions.Add(new LocalizedString(tableCollection, entryKey));
}
BuildLocalizedOptionPlaceholders();
EnsureValueChangeListener();
SetValue(initialValue);
ApplyOptionsToDropdown();
}
/// <summary>
/// 使用运行时生成的纯文本选项初始化下拉框。
/// 仅用于分辨率、语言自称等不应随当前 Locale 再次翻译的内容。
/// </summary>
public void SetUpRuntime(int initialValue, IReadOnlyList<string> runtimeOptions)
{
base.SetUp();
ClearLocalizedBindings();
ClearLocalizedOptionDefinitions();
_usesLocalizedOptions = false;
options = runtimeOptions == null ? new List<string>() : new List<string>(runtimeOptions);
EnsureValueChangeListener();
SetValue(initialValue);
ApplyOptionsToDropdown();
}
private void OnEnable()
{
if (_usesLocalizedOptions)
{
QueueGeneratedItemBinding();
}
}
private void OnDisable()
{
StopGeneratedItemBinding();
}
private void OnDestroy()
{
StopGeneratedItemBinding();
ClearLocalizedOptionDefinitions();
}
public int GetOptionIndex(string option)
{
return options.IndexOf(option);
}
/// <summary>
/// 只更新当前选择状态,不调用 <see cref="updateValueAction"/>。
/// 因此语言切换和 Dropdown 列表重建都不会重新应用玩家的设置。
/// </summary>
public void SetValue(int index)
{
if (options == null || options.Count == 0)
{
selectedIndex = 0;
selectedOption = string.Empty;
return;
}
selectedIndex = Mathf.Clamp(index, 0, options.Count - 1);
selectedOption = options[selectedIndex];
dropdown.selectedItemIndex = selectedIndex;
if (_usesLocalizedOptions)
{
BindSelectedLocalizedText();
}
else if (dropdown.selectedText != null)
{
dropdown.selectedText.text = selectedOption;
}
}
private void EnsureValueChangeListener()
{
if (_isValueChangeListenerRegistered)
{
return;
}
dropdown.onValueChanged.AddListener(OnDropdownValueChanged);
_isValueChangeListenerRegistered = true;
}
private void OnDropdownValueChanged(int index)
{
SetValue(index);
updateValueAction?.Invoke();
}
private void BuildLocalizedOptionPlaceholders()
{
options = new List<string>(_localizedOptions.Count);
for (int index = 0; index < _localizedOptions.Count; index++)
{
// Michsky 需要 itemName 创建 Item真正可见的内容会在创建完成后被 LocalizedTMPText 覆盖。
options.Add($"LocalizedOption_{index}");
}
}
private void ApplyOptionsToDropdown()
{
if (options == null || options.Count == 0)
{
dropdown.items = new List<CustomDropdown.Item>();
return;
}
SetValue(selectedIndex);
dropdown.items = new List<CustomDropdown.Item>(options.Count);
foreach (string option in options)
{
dropdown.items.Add(new CustomDropdown.Item { itemName = option });
}
if (dropdown.gameObject.activeInHierarchy)
{
if (dropdown.isOn)
{
dropdown.Animate();
}
dropdown.SetupDropdown();
}
else
{
dropdown.UpdateItemLayout();
}
if (_usesLocalizedOptions)
{
QueueGeneratedItemBinding();
}
}
/// <summary>
/// 等待一帧,确保 Michsky 已销毁旧 Item 并完成当前批次的实例化,再为每个 Item 绑定引用。
/// 这样不会保留已销毁 Item 的语言监听,也避免把引用绑定到上一批列表对象。
/// </summary>
private void QueueGeneratedItemBinding()
{
if (!isActiveAndEnabled || _bindGeneratedItemsCoroutine != null)
{
return;
}
_bindGeneratedItemsCoroutine = StartCoroutine(BindGeneratedItemsNextFrame());
}
private IEnumerator BindGeneratedItemsNextFrame()
{
yield return null;
_bindGeneratedItemsCoroutine = null;
BindSelectedLocalizedText();
if (dropdown.itemParent == null)
{
yield break;
}
int itemCount = Mathf.Min(dropdown.itemParent.childCount, _localizedOptions.Count);
for (int index = 0; index < itemCount; index++)
{
TMP_Text itemText = dropdown.itemParent.GetChild(index).GetComponentInChildren<TMP_Text>(true);
BindLocalizedText(itemText, _localizedOptions[index]);
}
}
private void BindSelectedLocalizedText()
{
if (!_usesLocalizedOptions || _localizedOptions.Count == 0)
{
return;
}
int optionIndex = Mathf.Clamp(selectedIndex, 0, _localizedOptions.Count - 1);
BindLocalizedText(dropdown.selectedText, _localizedOptions[optionIndex]);
}
private static void BindLocalizedText(TMP_Text text, LocalizedString reference)
{
if (text == null)
{
return;
}
LocalizedTMPText localizedText = text.GetComponent<LocalizedTMPText>();
if (localizedText == null)
{
localizedText = text.gameObject.AddComponent<LocalizedTMPText>();
}
localizedText.SetStringReference(reference);
}
/// <summary>
/// 将本 Dropdown 先前动态附加的本地化引用清空。
/// 同一控件改用运行时文本列表时必须执行此步骤,否则旧 Locale 回调会覆盖新的原始文本。
/// </summary>
private void ClearLocalizedBindings()
{
ClearLocalizedBinding(dropdown.selectedText);
if (dropdown.itemParent == null)
{
return;
}
foreach (Transform item in dropdown.itemParent)
{
ClearLocalizedBinding(item.GetComponentInChildren<TMP_Text>(true));
}
}
private static void ClearLocalizedBinding(TMP_Text text)
{
LocalizedTMPText localizedText = text == null ? null : text.GetComponent<LocalizedTMPText>();
if (localizedText != null)
{
localizedText.SetStringReference(null);
}
}
private void ClearLocalizedOptionDefinitions()
{
// LocalizedString 的订阅由每个 LocalizedTMPText 持有ClearLocalizedBindings 已经解除它们。
// 当前 Unity Localization 版本没有公开的 LocalizedString.Dispose因此清空运行时列表即可。
_localizedOptions.Clear();
}
private void StopGeneratedItemBinding()
{
if (_bindGeneratedItemsCoroutine == null)
{
return;
}
StopCoroutine(_bindGeneratedItemsCoroutine);
_bindGeneratedItemsCoroutine = null;
}
}
}