using System; using System.Collections; using System.Collections.Generic; using System.Linq; using RR.Common.Settings; using RR.Common.Settings.UI; using TMPro; using UnityEngine; namespace RR.Common.UI { public class SettingsUIGenerator : MonoBehaviour { [Header("Source")] [SerializeField] private SettingsDatabase database; [Header("Layout")] [SerializeField] private Transform contentRoot; /// /// Корневой Transform для контента настроек /// 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 groupOrder = new(); [Header("Sorting")] [SerializeField] private bool sortByNameInsideGroup = true; [Header("Prefabs (rows)")] [SerializeField] private SettingUIItem toggleRowPrefab; [SerializeField] private SettingUIItem sliderRowPrefab; [SerializeField] private SettingUIItem valueSelectorRowPrefab; [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 spawnedItems = new(); private readonly List spawnedGroups = new(); private Coroutine _buildCoroutine; /// /// Событие вызывается когда построение UI завершено /// public event Action OnBuildComplete; /// /// Событие вызывается при сворачивании/разворачивании любой группы настроек /// public event Action OnGroupExpandedChanged; /// /// Первый созданный Selectable элемент /// 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 для корректной навигации if (contentRoot != null && contentRoot is RectTransform rectTransform) { UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(rectTransform); } // Ещё кадр для применения layout yield return null; // Находим первый Selectable элемент if (contentRoot != null) { FirstSelectable = contentRoot.GetComponentInChildren(); } // Оповещаем о завершении 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 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> groups = entries .GroupBy(e => string.IsNullOrWhiteSpace(e.group) ? "General" : e.group) .ToDictionary(g => g.Key, g => g.ToList()); // Порядок групп: сначала заданные, потом остальные по алфавиту List orderedGroupNames = new(); if (groupOrder != null && groupOrder.Count > 0) { orderedGroupNames.AddRange( groupOrder.Where(g => !string.IsNullOrWhiteSpace(g) && groups.ContainsKey(g))); } List rest = groups.Keys .Except(orderedGroupNames) .OrderBy(x => x) .ToList(); orderedGroupNames.AddRange(rest); foreach (string groupName in orderedGroupNames) { List 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(); 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) { if (entry == null || parent == null) { Debug.LogWarning("[SettingsUIGenerator] Cannot spawn setting row: entry or parent is null"); return; } 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); // Если это Int ValueSelector, настраиваем компонент ValueSelectorRow if (entry.valueType == SettingValueType.Int && entry.intDisplayFormat == IntDisplayFormat.ValueSelector) { ValueSelectorRow valueSelector = item.GetComponent(); if (valueSelector != null) { int currentValue = SettingsManager.Instance.GetInt(entry.key); valueSelector.Setup(entry, currentValue); } else { Debug.LogWarning($"[SettingsUIGenerator] ValueSelectorRow component not found on prefab for '{entry.key}'"); } } // Если это Float ValueSelector, настраиваем компонент ValueSelectorRow if (entry.valueType == SettingValueType.Float && entry.floatDisplayFormat == FloatDisplayFormat.ValueSelector) { ValueSelectorRow valueSelector = item.GetComponent(); if (valueSelector != null) { float currentValue = SettingsManager.Instance.GetFloat(entry.key); valueSelector.Setup(entry, currentValue); } else { Debug.LogWarning($"[SettingsUIGenerator] ValueSelectorRow component not found on prefab for '{entry.key}'"); } } if (entry.valueType == SettingValueType.OptionPicker && entry.optionPickerDisplayFormat == OptionPickerDisplayFormat.ValueSelector) { ValueSelectorRow valueSelector = item.GetComponent(); if (valueSelector != null) { int currentValue = SettingsManager.Instance.GetInt(entry.key); valueSelector.SetupOptionPicker(entry, currentValue); } else { Debug.LogWarning($"[SettingsUIGenerator] ValueSelectorRow component not found on prefab for '{entry.key}'"); } } spawnedItems.Add(item); } private SettingUIItem PickPrefab(SettingEntry e) { return e.valueType switch { SettingValueType.Bool => toggleRowPrefab, SettingValueType.Float => e.floatDisplayFormat == FloatDisplayFormat.ValueSelector ? valueSelectorRowPrefab : sliderRowPrefab, SettingValueType.Int => e.intDisplayFormat == IntDisplayFormat.ValueSelector ? valueSelectorRowPrefab : sliderRowPrefab ? sliderRowPrefab : dropdownRowPrefab, SettingValueType.String => inputRowPrefab, SettingValueType.Button => buttonRowPrefab, SettingValueType.OptionPicker => e.optionPickerDisplayFormat == OptionPickerDisplayFormat.ValueSelector ? valueSelectorRowPrefab : optionPickerRowPrefab ? optionPickerRowPrefab : dropdownRowPrefab, _ => null }; } public void RefreshValues() { for (int i = 0; i < spawnedItems.Count; i++) { if (spawnedItems[i]) { spawnedItems[i].RefreshFromSettings(); // Если есть компонент ValueSelectorRow, обновляем и его ValueSelectorRow valueSelector = spawnedItems[i].GetComponent(); if (valueSelector != null && !string.IsNullOrEmpty(valueSelector.Key)) { if (valueSelector.IsFloat) { float fValue = SettingsManager.Instance.GetFloat(valueSelector.Key); valueSelector.SetValue(fValue); } else { int iValue = SettingsManager.Instance.GetInt(valueSelector.Key); valueSelector.SetValue(iValue); } } } } } private void OnGroupPanelExpandedChanged(SettingsGroupPanel panel, bool expanded) { // Уведомляем подписчиков (например SettingsUI) об изменении состояния группы OnGroupExpandedChanged?.Invoke(); } public void Clear() { // Отписываемся от событий групп foreach (var group in spawnedGroups) { if (group != null) group.OnExpandedChanged -= OnGroupPanelExpandedChanged; } spawnedGroups.Clear(); if (contentRoot != null) { for (int i = contentRoot.childCount - 1; i >= 0; i--) { Transform child = contentRoot.GetChild(i); if (child != null) Destroy(child.gameObject); } } spawnedItems.Clear(); } } }