Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- Добавил больше настроек в систему настроек - Пока у большинства настроек нет функционала - Немного изменил внешний вид настроек
288 lines
11 KiB
C#
288 lines
11 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using RR.Common.Settings;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
|
||
namespace RR.Common.UI
|
||
{
|
||
public class SettingsUIGenerator : MonoBehaviour
|
||
{
|
||
[Header("Source")] [SerializeField] private SettingsDatabase database;
|
||
|
||
[Header("Layout")] [SerializeField] private Transform contentRoot;
|
||
|
||
/// <summary>
|
||
/// Корневой Transform для контента настроек
|
||
/// </summary>
|
||
public Transform ContentRoot => contentRoot;
|
||
|
||
[Header("Grouping")] [SerializeField] private bool groupByGroup = true;
|
||
[SerializeField] private bool collapsibleGroups = true;
|
||
[SerializeField] private SettingsGroupPanel groupPanelPrefab; // префаб группы (foldout)
|
||
[SerializeField] private bool defaultGroupsExpanded = true;
|
||
[SerializeField] private bool rememberGroupFoldout = true;
|
||
|
||
[Tooltip("Если заполнить — группы будут идти в этом порядке, остальные добавятся в конец по алфавиту.")]
|
||
[SerializeField]
|
||
private List<string> groupOrder = new();
|
||
|
||
[Header("Sorting")] [SerializeField] private bool sortByNameInsideGroup = true;
|
||
|
||
[Header("Prefabs (rows)")] [SerializeField]
|
||
private SettingUIItem toggleRowPrefab;
|
||
|
||
[SerializeField] private SettingUIItem sliderRowPrefab;
|
||
[SerializeField] private SettingUIItem dropdownRowPrefab;
|
||
[SerializeField] private SettingUIItem inputRowPrefab;
|
||
[SerializeField] private SettingUIItem buttonRowPrefab;
|
||
[SerializeField] private SettingUIItem optionPickerRowPrefab;
|
||
|
||
[Header("Behaviour")] [SerializeField] private bool buildOnEnable = true;
|
||
[SerializeField] private bool clearBeforeBuild = true;
|
||
[SerializeField] private bool showDescription = true;
|
||
|
||
private readonly List<SettingUIItem> spawnedItems = new();
|
||
private readonly List<SettingsGroupPanel> spawnedGroups = new();
|
||
private Coroutine _buildCoroutine;
|
||
|
||
/// <summary>
|
||
/// Событие вызывается когда построение UI завершено
|
||
/// </summary>
|
||
public event Action OnBuildComplete;
|
||
|
||
/// <summary>
|
||
/// Событие вызывается при сворачивании/разворачивании любой группы настроек
|
||
/// </summary>
|
||
public event Action OnGroupExpandedChanged;
|
||
|
||
/// <summary>
|
||
/// Первый созданный Selectable элемент
|
||
/// </summary>
|
||
public UnityEngine.UI.Selectable FirstSelectable { get; private set; }
|
||
|
||
private void OnEnable()
|
||
{
|
||
if (buildOnEnable)
|
||
{
|
||
// Запускаем корутину, которая ждёт SettingsManager
|
||
if (_buildCoroutine != null) StopCoroutine(_buildCoroutine);
|
||
_buildCoroutine = StartCoroutine(BuildWhenReady());
|
||
}
|
||
else
|
||
{
|
||
RefreshValues();
|
||
}
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
if (_buildCoroutine != null)
|
||
{
|
||
StopCoroutine(_buildCoroutine);
|
||
_buildCoroutine = null;
|
||
}
|
||
}
|
||
|
||
private IEnumerator BuildWhenReady()
|
||
{
|
||
// Ждём пока SettingsManager инициализируется (максимум 2 секунды)
|
||
float timeout = 2f;
|
||
float elapsed = 0f;
|
||
|
||
while (!SettingsManager.Instance && elapsed < timeout)
|
||
{
|
||
elapsed += Time.unscaledDeltaTime;
|
||
yield return null;
|
||
}
|
||
|
||
if (!SettingsManager.Instance)
|
||
{
|
||
Debug.LogError("[SettingsUIGenerator] SettingsManager.Instance not found after waiting. Make sure SettingsManager is in scene.");
|
||
yield break;
|
||
}
|
||
|
||
Build();
|
||
|
||
// Ждём кадр чтобы layout обновился
|
||
yield return null;
|
||
|
||
// Принудительно обновляем layout для корректной навигации
|
||
UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(contentRoot as RectTransform);
|
||
|
||
// Ещё кадр для применения layout
|
||
yield return null;
|
||
|
||
// Находим первый Selectable элемент
|
||
FirstSelectable = contentRoot.GetComponentInChildren<UnityEngine.UI.Selectable>();
|
||
|
||
// Оповещаем о завершении
|
||
OnBuildComplete?.Invoke();
|
||
|
||
_buildCoroutine = null;
|
||
}
|
||
|
||
public void Build()
|
||
{
|
||
if (!database || !contentRoot)
|
||
{
|
||
Debug.LogWarning("[SettingsUIGenerator] database/contentRoot not set.");
|
||
return;
|
||
}
|
||
|
||
if (!SettingsManager.Instance)
|
||
{
|
||
Debug.LogWarning("[SettingsUIGenerator] SettingsManager.Instance not found in scene.");
|
||
return;
|
||
}
|
||
|
||
if (clearBeforeBuild) Clear();
|
||
spawnedItems.Clear();
|
||
|
||
List<SettingEntry> entries = database.entries
|
||
.Where(e => e != null && !string.IsNullOrWhiteSpace(e.key) && e.shownInSettings)
|
||
.ToList();
|
||
|
||
if (!groupByGroup)
|
||
{
|
||
// Без групп — просто одним списком
|
||
if (sortByNameInsideGroup)
|
||
entries = entries.OrderBy(e => e.displayName ?? e.key).ToList();
|
||
|
||
foreach (SettingEntry entry in entries)
|
||
SpawnSettingRow(entry, contentRoot);
|
||
|
||
return;
|
||
}
|
||
|
||
// Группировка по e.group
|
||
Dictionary<string, List<SettingEntry>> groups = entries
|
||
.GroupBy(e => string.IsNullOrWhiteSpace(e.group) ? "General" : e.group)
|
||
.ToDictionary(g => g.Key, g => g.ToList());
|
||
|
||
// Порядок групп: сначала заданные, потом остальные по алфавиту
|
||
List<string> orderedGroupNames = new();
|
||
|
||
if (groupOrder != null && groupOrder.Count > 0)
|
||
{
|
||
orderedGroupNames.AddRange(
|
||
groupOrder.Where(g => !string.IsNullOrWhiteSpace(g) && groups.ContainsKey(g)));
|
||
}
|
||
|
||
List<string> rest = groups.Keys
|
||
.Except(orderedGroupNames)
|
||
.OrderBy(x => x)
|
||
.ToList();
|
||
|
||
orderedGroupNames.AddRange(rest);
|
||
|
||
foreach (string groupName in orderedGroupNames)
|
||
{
|
||
List<SettingEntry> list = groups[groupName];
|
||
|
||
if (sortByNameInsideGroup)
|
||
list = list.OrderBy(e => e.displayName ?? e.key).ToList();
|
||
|
||
Transform parentForRows = contentRoot;
|
||
|
||
// Создаём UI группы (foldout) если включено
|
||
if (collapsibleGroups && groupPanelPrefab)
|
||
{
|
||
SettingsGroupPanel panel = Instantiate(groupPanelPrefab, contentRoot);
|
||
panel.name = $"Group_{groupName}";
|
||
|
||
string prefsKey = $"settings.ui.group.{groupName}";
|
||
panel.Init(groupName, prefsKey, defaultGroupsExpanded, rememberGroupFoldout);
|
||
|
||
// Подписываемся на изменение состояния группы
|
||
panel.OnExpandedChanged += OnGroupPanelExpandedChanged;
|
||
spawnedGroups.Add(panel);
|
||
|
||
parentForRows = panel.ContentRoot;
|
||
}
|
||
else
|
||
{
|
||
// Если не используем foldout — можно хотя бы заголовок простым текстом
|
||
// (не обязательно; если не надо — удали блок)
|
||
GameObject headerGO = new GameObject($"Header_{groupName}", typeof(RectTransform),
|
||
typeof(TextMeshProUGUI));
|
||
headerGO.transform.SetParent(contentRoot, false);
|
||
TextMeshProUGUI text = headerGO.GetComponent<TextMeshProUGUI>();
|
||
text.text = groupName;
|
||
text.fontSize = 28;
|
||
text.fontStyle = FontStyles.Bold;
|
||
}
|
||
|
||
foreach (var entry in list)
|
||
SpawnSettingRow(entry, parentForRows);
|
||
}
|
||
}
|
||
|
||
private void SpawnSettingRow(SettingEntry entry, Transform parent)
|
||
{
|
||
SettingUIItem prefab = PickPrefab(entry);
|
||
if (!prefab)
|
||
{
|
||
Debug.LogWarning(
|
||
$"[SettingsUIGenerator] No prefab for '{entry.key}' ({entry.valueType}/{entry.applyTarget})");
|
||
return;
|
||
}
|
||
|
||
SettingUIItem item = Instantiate(prefab, parent);
|
||
item.name = $"Setting_{entry.key}";
|
||
item.Bind(entry, showDescription);
|
||
|
||
spawnedItems.Add(item);
|
||
}
|
||
|
||
private SettingUIItem PickPrefab(SettingEntry e)
|
||
{
|
||
|
||
return e.valueType switch
|
||
{
|
||
// По типу
|
||
SettingValueType.Bool => toggleRowPrefab,
|
||
SettingValueType.Float => sliderRowPrefab,
|
||
SettingValueType.Int => sliderRowPrefab ? sliderRowPrefab : dropdownRowPrefab,
|
||
SettingValueType.String => inputRowPrefab,
|
||
SettingValueType.Button => buttonRowPrefab,
|
||
SettingValueType.OptionPicker => optionPickerRowPrefab ? optionPickerRowPrefab : dropdownRowPrefab,
|
||
_ => null
|
||
};
|
||
|
||
}
|
||
|
||
public void RefreshValues()
|
||
{
|
||
for (int i = 0; i < spawnedItems.Count; i++)
|
||
if (spawnedItems[i])
|
||
spawnedItems[i].RefreshFromSettings();
|
||
}
|
||
|
||
private void OnGroupPanelExpandedChanged(SettingsGroupPanel panel, bool expanded)
|
||
{
|
||
Debug.Log($"[SettingsUIGenerator] Group '{panel.name}' expanded: {expanded}");
|
||
// Уведомляем подписчиков (например SettingsUI) об изменении состояния группы
|
||
OnGroupExpandedChanged?.Invoke();
|
||
}
|
||
|
||
public void Clear()
|
||
{
|
||
// Отписываемся от событий групп
|
||
foreach (var group in spawnedGroups)
|
||
{
|
||
if (group != null)
|
||
group.OnExpandedChanged -= OnGroupPanelExpandedChanged;
|
||
}
|
||
spawnedGroups.Clear();
|
||
|
||
for (int i = contentRoot.childCount - 1; i >= 0; i--)
|
||
Destroy(contentRoot.GetChild(i).gameObject);
|
||
|
||
spawnedItems.Clear();
|
||
}
|
||
}
|
||
}
|