2025-08-22 14:54:40 -04:00
|
|
|
|
using System.Collections;
|
|
|
|
|
|
using System.Collections.Generic;
|
2026-07-25 13:27:53 -04:00
|
|
|
|
using Ichni.Localization;
|
2025-08-22 14:54:40 -04:00
|
|
|
|
using Michsky.MUIP;
|
2026-07-25 13:27:53 -04:00
|
|
|
|
using TMPro;
|
2025-08-22 14:54:40 -04:00
|
|
|
|
using UnityEngine;
|
2026-07-25 13:27:53 -04:00
|
|
|
|
using UnityEngine.Localization;
|
2025-08-22 14:54:40 -04:00
|
|
|
|
|
|
|
|
|
|
namespace Ichni.UI
|
|
|
|
|
|
{
|
2026-07-25 13:27:53 -04:00
|
|
|
|
/// <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>
|
2025-08-22 14:54:40 -04:00
|
|
|
|
public class Dropdown : SettingsUIElementBase
|
|
|
|
|
|
{
|
2026-07-25 13:27:53 -04:00
|
|
|
|
// 仅保存本次初始化创建的运行时引用,不在 Inspector 中维护本地化选项列表。
|
|
|
|
|
|
private readonly List<LocalizedString> _localizedOptions = new List<LocalizedString>();
|
|
|
|
|
|
|
|
|
|
|
|
// 保留原字段以兼容已有的外部读取。静态本地化选项使用稳定的占位名称;实际显示内容由
|
|
|
|
|
|
// 动态生成 Item 上的 LocalizedTMPText 管理。
|
|
|
|
|
|
[HideInInspector] public List<string> options = new List<string>();
|
2025-08-22 14:54:40 -04:00
|
|
|
|
public CustomDropdown dropdown;
|
|
|
|
|
|
public int selectedIndex;
|
|
|
|
|
|
public string selectedOption;
|
2026-07-25 13:27:53 -04:00
|
|
|
|
|
|
|
|
|
|
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)
|
2025-08-22 14:54:40 -04:00
|
|
|
|
{
|
2026-07-25 13:27:53 -04:00
|
|
|
|
base.SetUp();
|
|
|
|
|
|
|
|
|
|
|
|
ClearLocalizedBindings();
|
|
|
|
|
|
ClearLocalizedOptionDefinitions();
|
|
|
|
|
|
_usesLocalizedOptions = true;
|
|
|
|
|
|
|
|
|
|
|
|
foreach (string entryKey in entryKeys)
|
2025-08-22 14:54:40 -04:00
|
|
|
|
{
|
2026-07-25 13:27:53 -04:00
|
|
|
|
_localizedOptions.Add(new LocalizedString(tableCollection, entryKey));
|
2025-08-22 14:54:40 -04:00
|
|
|
|
}
|
2026-07-25 13:27:53 -04:00
|
|
|
|
|
|
|
|
|
|
BuildLocalizedOptionPlaceholders();
|
|
|
|
|
|
EnsureValueChangeListener();
|
2025-08-22 14:54:40 -04:00
|
|
|
|
SetValue(initialValue);
|
2026-07-25 13:27:53 -04:00
|
|
|
|
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();
|
2025-08-22 14:54:40 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public int GetOptionIndex(string option)
|
|
|
|
|
|
{
|
|
|
|
|
|
return options.IndexOf(option);
|
|
|
|
|
|
}
|
2026-07-25 13:27:53 -04:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 只更新当前选择状态,不调用 <see cref="updateValueAction"/>。
|
|
|
|
|
|
/// 因此语言切换和 Dropdown 列表重建都不会重新应用玩家的设置。
|
|
|
|
|
|
/// </summary>
|
2025-08-22 14:54:40 -04:00
|
|
|
|
public void SetValue(int index)
|
|
|
|
|
|
{
|
2026-07-25 13:27:53 -04:00
|
|
|
|
if (options == null || options.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
selectedIndex = 0;
|
|
|
|
|
|
selectedOption = string.Empty;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
selectedIndex = Mathf.Clamp(index, 0, options.Count - 1);
|
2025-08-22 14:54:40 -04:00
|
|
|
|
selectedOption = options[selectedIndex];
|
|
|
|
|
|
dropdown.selectedItemIndex = selectedIndex;
|
2026-07-25 13:27:53 -04:00
|
|
|
|
|
|
|
|
|
|
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;
|
2025-08-22 14:54:40 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-19 16:44:58 -04:00
|
|
|
|
}
|