using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; namespace RR.Common.UI { /// /// Модальное окно для ввода текста через выбор символов. /// Использует ячейки TextInputCell для ввода каждого символа. /// public class TextInputModal : Modal { [Header("Text Input Settings")] [SerializeField] private int cellCount = 10; [SerializeField] private string[] alphabet = new string[] { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", " ", "-", "_" }; [Header("UI References")] [SerializeField] private Transform cellsContainer; [SerializeField] private GameObject cellPrefab; [SerializeField] private TextMeshProUGUI titleText; [SerializeField] private TextMeshProUGUI previewText; [SerializeField] private Button confirmButton; [SerializeField] private Button cancelButton; private List cells = new List(); private int currentCellIndex; // Параметры для отложенной генерации (когда модалка станет активной) private bool needsRegeneration = false; private string pendingText = ""; // События public event Action OnTextConfirmed; public event Action OnTextCancelled; protected override void Awake() { base.Awake(); // НЕ генерируем ячейки здесь - они будут созданы через SetCellCount() или SetAlphabet() // Настраиваем кнопки if (confirmButton) { confirmButton.onClick.AddListener(ConfirmInput); } if (cancelButton) { cancelButton.onClick.AddListener(CancelInput); } } private void OnDestroy() { // Отписываемся от всех событий ячеек foreach (var cell in cells) { if (cell != null) { cell.OnSymbolChanged -= OnCellSymbolChanged; } } // Отписываемся от кнопок if (confirmButton) { confirmButton.onClick.RemoveListener(ConfirmInput); } if (cancelButton) { cancelButton.onClick.RemoveListener(CancelInput); } } private void Update() { if (!gameObject.activeInHierarchy) return; // Синхронизируем currentCellIndex с текущим выбранным элементом EventSystem SyncCurrentCellWithEventSystem(); } /// /// Синхронизирует currentCellIndex с текущим выбранным элементом EventSystem /// private void SyncCurrentCellWithEventSystem() { if (UnityEngine.EventSystems.EventSystem.current == null) return; var selected = UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject; if (selected == null) return; // Проверяем, является ли выбранный объект одной из наших ячеек var selectedCell = selected.GetComponent(); if (selectedCell != null) { int index = cells.IndexOf(selectedCell); if (index >= 0 && index != currentCellIndex) { currentCellIndex = index; // Обновляем визуальное выделение без установки фокуса (чтобы избежать рекурсии) for (int i = 0; i < cells.Count; i++) { cells[i].SetCellSelected(i == currentCellIndex); } UpdatePreview(); } } } /// /// Генерирует ячейки для ввода текста /// /// Начальный текст для заполнения ячеек private void GenerateCells(string initialText = "") { Debug.Log($"[TextInputModal] GenerateCells: cellCount={cellCount}, initialText='{initialText}', activeInHierarchy={gameObject.activeInHierarchy}"); if (cellPrefab == null || cellsContainer == null) { Debug.LogError("[TextInputModal] Cell prefab or container is not assigned!"); return; } // Отписываемся от событий старых ячеек и очищаем их foreach (var cell in cells) { if (cell != null) { cell.OnSymbolChanged -= OnCellSymbolChanged; } } // Очищаем существующие ячейки foreach (Transform child in cellsContainer) { Destroy(child.gameObject); } cells.Clear(); // Создаем новые ячейки с начальными символами for (int i = 0; i < cellCount; i++) { GameObject cellObj = Instantiate(cellPrefab, cellsContainer); TextInputCell cell = cellObj.GetComponent(); if (cell != null) { // Определяем начальный символ для этой ячейки string symbol = (i < initialText.Length) ? initialText[i].ToString() : null; // Инициализируем ячейку с алфавитом и символом cell.Initialize(alphabet, 0, symbol); // Подписываемся на изменение символа cell.OnSymbolChanged += OnCellSymbolChanged; cells.Add(cell); } else { Debug.LogError($"[TextInputModal] Cell prefab doesn't have TextInputCell component!"); } } Debug.Log($"[TextInputModal] Created {cells.Count} cells. Setting up navigation..."); // Настраиваем циклическую горизонтальную навигацию SetupCyclicNavigation(); // Выделяем первую ячейку и устанавливаем фокус if (cells.Count > 0) { currentCellIndex = 0; SetFirstSelected(cells[0]); UpdateCellSelection(); } // Обновляем preview с актуальными данными из ячеек UpdatePreview(); Debug.Log($"[TextInputModal] GenerateCells completed successfully"); } /// /// Настраивает циклическую горизонтальную навигацию между всеми элементами /// private void SetupCyclicNavigation() { if (cells.Count == 0) return; // Создаем список всех элементов для навигации: ячейки + кнопки List allElements = new List(); // Добавляем все ячейки allElements.AddRange(cells); // Добавляем кнопки в конец if (confirmButton != null) allElements.Add(confirmButton); if (cancelButton != null) allElements.Add(cancelButton); // Настраиваем циклическую горизонтальную навигацию для всех элементов for (int i = 0; i < allElements.Count; i++) { var element = allElements[i]; var nav = new Navigation(); nav.mode = Navigation.Mode.Explicit; // Предыдущий элемент (циклично) int prevIndex = (i - 1 + allElements.Count) % allElements.Count; nav.selectOnLeft = allElements[prevIndex]; // Следующий элемент (циклично) int nextIndex = (i + 1) % allElements.Count; nav.selectOnRight = allElements[nextIndex]; // Вверх/вниз: только для ячеек (не для кнопок) if (element is TextInputCell) { // Для ячеек вверх/вниз обрабатываются в OnMove (изменение символа) nav.selectOnUp = null; nav.selectOnDown = null; } else { // Для кнопок вверх/вниз не используются nav.selectOnUp = null; nav.selectOnDown = null; } element.navigation = nav; } } /// /// Открывает модальное окно с заданным заголовком и начальным текстом /// public void OpenForInput(string title, string initialText = "") { Debug.Log($"[TextInputModal] OpenForInput called. Title: '{title}', InitialText: '{initialText}'"); if (titleText) { titleText.text = title; } // Сохраняем текст для генерации после активации pendingText = initialText ?? ""; // Помечаем что нужна регенерация (если еще не помечено) if (cells.Count != cellCount || cells.Count == 0) { needsRegeneration = true; } // Проверяем что ячейки не null if (!needsRegeneration && cells.Count > 0) { for (int i = 0; i < cells.Count; i++) { if (cells[i] == null) { Debug.LogWarning($"[TextInputModal] Cell {i} is null, will regenerate"); needsRegeneration = true; break; } } } Debug.Log($"[TextInputModal] needsRegeneration: {needsRegeneration}, calling Open()"); // Open() активирует модалку и вызовет генерацию ячеек Open(); } /// /// Переопределяем Open для генерации ячеек ПОСЛЕ активации /// public override void Open() { Debug.Log($"[TextInputModal] Open() called. needsRegeneration: {needsRegeneration}, pendingText: '{pendingText}'"); Debug.Log($"[TextInputModal] BEFORE base.Open() - gameObject.activeSelf: {gameObject.activeSelf}, activeInHierarchy: {gameObject.activeInHierarchy}"); // Сначала активируем модалку base.Open(); Debug.Log($"[TextInputModal] AFTER base.Open() - gameObject.activeSelf: {gameObject.activeSelf}, activeInHierarchy: {gameObject.activeInHierarchy}"); // Теперь, когда модалка активна, генерируем ячейки если нужно if (needsRegeneration) { Debug.Log($"[TextInputModal] Generating cells NOW (after activation) with text: '{pendingText}'"); GenerateCells(pendingText); needsRegeneration = false; } else if (!string.IsNullOrEmpty(pendingText)) { Debug.Log($"[TextInputModal] Setting text to existing cells: '{pendingText}'"); SetText(pendingText); } // Очищаем pending text pendingText = ""; Debug.Log($"[TextInputModal] Open() completed. cells.Count: {cells.Count}"); } /// /// Устанавливает текст в ячейки (переинициализирует ячейки с новыми символами) /// public void SetText(string text) { if (text == null) text = ""; Debug.Log($"[TextInputModal] SetText called with: '{text}'"); // Проверяем, что ячейки созданы if (cells.Count == 0) { Debug.LogWarning("[TextInputModal] No cells available, cannot set text!"); return; } Debug.Log($"[TextInputModal] Setting text to {cells.Count} cells"); // Переинициализируем каждую ячейку с новым символом for (int i = 0; i < cells.Count; i++) { if (cells[i] == null) { Debug.LogWarning($"[TextInputModal] Cell {i} is null!"); continue; } // Определяем символ для этой ячейки string symbol = (i < text.Length) ? text[i].ToString() : null; Debug.Log($"[TextInputModal] Cell {i}: initializing with symbol '{symbol}', alphabet has {(cells[i] != null ? alphabet.Length : 0)} symbols"); // Переинициализируем ячейку с новым символом cells[i].Initialize(alphabet, 0, symbol); } Debug.Log($"[TextInputModal] SetText completed, updating preview"); UpdatePreview(); } /// /// Получает текущий введенный текст /// public string GetText() { string result = ""; foreach (var cell in cells) { string symbol = cell.CurrentSymbol; // Добавляем символ, но первый символ алфавита (обычно пробел или 0) считается "пустым" if (cell.CurrentIndex == 0 && string.IsNullOrEmpty(symbol.Trim())) { result += " "; // Добавляем пробел для пустых ячеек } else { result += symbol; } } return result.TrimEnd(); // Убираем пробелы в конце } /// /// Вызывается когда символ в ячейке изменяется /// private void OnCellSymbolChanged(TextInputCell cell) { UpdatePreview(); } /// /// Обновляет выделение ячеек /// private void UpdateCellSelection() { for (int i = 0; i < cells.Count; i++) { cells[i].SetCellSelected(i == currentCellIndex); } // Устанавливаем фокус EventSystem на текущую ячейку if (currentCellIndex >= 0 && currentCellIndex < cells.Count && cells[currentCellIndex] != null) { if (UnityEngine.EventSystems.EventSystem.current != null) { UnityEngine.EventSystems.EventSystem.current.SetSelectedGameObject(cells[currentCellIndex].gameObject); } } } /// /// Обновляет предпросмотр текста /// private void UpdatePreview() { if (previewText) { previewText.text = GetText(); } } /// /// Подтверждает ввод текста /// private void ConfirmInput() { string text = GetText(); OnTextConfirmed?.Invoke(text); Close(); } /// /// Отменяет ввод текста /// private void CancelInput() { OnTextCancelled?.Invoke(); Close(); } /// /// Устанавливает алфавит для всех ячеек /// public void SetAlphabet(string[] newAlphabet) { Debug.Log($"[TextInputModal] SetAlphabet called with {(newAlphabet != null ? newAlphabet.Length : 0)} symbols"); if (newAlphabet == null || newAlphabet.Length == 0) { Debug.LogWarning("[TextInputModal] Cannot set empty alphabet!"); return; } alphabet = newAlphabet; // Помечаем что нужна регенерация (она произойдет при открытии модалки) needsRegeneration = true; Debug.Log($"[TextInputModal] Alphabet set, marked for regeneration"); } /// /// Устанавливает количество ячеек и перегенерирует их /// public void SetCellCount(int count) { if (count <= 0) { return; } cellCount = count; // Помечаем что нужна регенерация (она произойдет при открытии модалки) needsRegeneration = true; } /// /// Сбрасывает все ячейки к первому символу /// public void ResetAllCells() { foreach (var cell in cells) { if (cell != null) { cell.Initialize(alphabet); } } currentCellIndex = 0; UpdateCellSelection(); UpdatePreview(); } } }