Danro/Assets/Scripts/Settings/UI/SettingsUIGenerator.cs
Danil Iarosh 24aaf18d4b
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
Added new settings in settings system
- Добавил больше настроек в систему настроек
- Пока у большинства настроек нет функционала
- Немного изменил внешний вид настроек
2026-02-06 08:43:51 +03:00

288 lines
11 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;
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();
}
}
}