using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace RR.Common.UI
{
///
/// Центральный менеджер модальных окон.
/// Управляет стеком открытых модальных окон и фокусом между ними.
///
public class ModalManager : MonoBehaviour
{
private static ModalManager _instance;
private static bool _applicationQuitting;
///
/// Singleton-инстанс менеджера модальных окон.
/// Создаётся автоматически при первом обращении.
///
public static ModalManager Instance
{
get
{
if (_applicationQuitting) return null;
if (_instance) return _instance;
// Ищем существующий в сцене
_instance = FindAnyObjectByType();
if (_instance) return _instance;
// Создаём новый
GameObject go = new GameObject("[ModalManager]");
_instance = go.AddComponent();
DontDestroyOnLoad(go);
return _instance;
}
}
///
/// Информация о состоянии UI до открытия модального окна.
///
private class ModalStackEntry
{
public Modal Modal;
public GameObject PreviousSelected;
public List DisabledGroups = new();
}
private readonly Stack _stack = new();
///
/// Событие, вызываемое при открытии любого модального окна.
///
public event Action OnModalOpened;
///
/// Событие, вызываемое при закрытии любого модального окна.
///
public event Action OnModalClosed;
///
/// Текущее активное модальное окно (на вершине стека).
///
public Modal CurrentModal => _stack.Count > 0 ? _stack.Peek().Modal : null;
///
/// Есть ли открытые модальные окна.
///
public bool HasOpenModals => _stack.Count > 0;
///
/// Количество открытых модальных окон.
///
public int OpenModalCount => _stack.Count;
private void Awake()
{
if (_instance && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
}
private void OnDestroy()
{
if (_instance == this)
{
_instance = null;
}
}
private void OnApplicationQuit()
{
_applicationQuitting = true;
}
#if UNITY_EDITOR
///
/// Сбрасываем статический флаг при выходе из Play Mode в редакторе,
/// иначе при повторном запуске Instance всегда будет null.
///
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetStatics()
{
_applicationQuitting = false;
_instance = null;
}
#endif
private void Update()
{
// Следим за тем, чтобы фокус оставался внутри текущего модального окна
if (_stack.Count == 0) return;
ModalStackEntry top = _stack.Peek();
if (!top.Modal || !top.Modal.gameObject.activeInHierarchy) return;
if (!top.Modal.EnforceFocus) return;
EnsureFocusInModal(top.Modal);
}
///
/// Регистрирует модальное окно в стеке (вызывается из Modal.OnEnable).
///
internal void Push(Modal modal)
{
if (modal == null) return;
// Проверяем, не зарегистрировано ли уже это окно
foreach (ModalStackEntry entry in _stack)
{
if (entry.Modal == modal) return;
}
ModalStackEntry newEntry = new ModalStackEntry
{
Modal = modal,
PreviousSelected = EventSystem.current ? EventSystem.current.currentSelectedGameObject : null
};
// Блокируем интерактивность всего, что не принадлежит этому модальному окну
if (modal.BlockBackground)
{
BlockBackgroundInteraction(modal, newEntry);
}
_stack.Push(newEntry);
// Устанавливаем фокус на первый элемент модального окна
SetInitialFocus(modal);
OnModalOpened?.Invoke(modal);
}
///
/// Удаляет модальное окно из стека (вызывается из Modal.OnDisable).
///
internal void Pop(Modal modal)
{
if (modal == null) return;
if (_stack.Count == 0) return;
// Ищем и удаляем модальное окно из стека
Stack temp = new Stack();
ModalStackEntry foundEntry = null;
while (_stack.Count > 0)
{
ModalStackEntry entry = _stack.Pop();
if (entry.Modal == modal)
{
foundEntry = entry;
break;
}
temp.Push(entry);
}
// Возвращаем остальные в стек
while (temp.Count > 0)
{
_stack.Push(temp.Pop());
}
if (foundEntry == null) return;
// Восстанавливаем интерактивность заблокированных групп
RestoreBackgroundInteraction(foundEntry);
// Восстанавливаем фокус
RestoreFocus(foundEntry);
OnModalClosed?.Invoke(modal);
}
///
/// Закрывает все открытые модальные окна.
///
public void CloseAll()
{
while (_stack.Count > 0)
{
ModalStackEntry entry = _stack.Peek();
if (entry.Modal != null)
{
entry.Modal.Close();
}
else
{
_stack.Pop();
}
}
}
///
/// Закрывает текущее (верхнее) модальное окно.
///
public void CloseCurrent()
{
if (_stack.Count == 0) return;
Modal current = _stack.Peek().Modal;
if (current != null)
{
current.Close();
}
}
private void BlockBackgroundInteraction(Modal modal, ModalStackEntry entry)
{
// Находим все CanvasGroup, которые не принадлежат модальному окну
CanvasGroup[] allGroups = FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
foreach (CanvasGroup group in allGroups)
{
if (!group || !group.interactable) continue;
if (IsUnderModal(group.transform, modal)) continue;
// Не блокируем родительские CanvasGroup модального окна
if (IsParentOfModal(group.transform, modal)) continue;
// Не блокируем CanvasGroup, если модальное окно является его потомком
if (modal.transform.IsChildOf(group.transform)) continue;
// Проверяем, не принадлежит ли эта группа другому модальному окну в стеке
bool belongsToOtherModal = false;
foreach (ModalStackEntry stackEntry in _stack)
{
if (stackEntry.Modal && IsUnderModal(group.transform, stackEntry.Modal))
{
belongsToOtherModal = true;
break;
}
}
if (belongsToOtherModal) continue;
group.interactable = false;
entry.DisabledGroups.Add(group);
}
// Если у модального окна нет CanvasGroup-родителя, блокируем все Selectable напрямую
if (entry.DisabledGroups.Count == 0)
{
BlockSelectablesOutsideModal(modal, entry);
}
}
private void BlockSelectablesOutsideModal(Modal modal, ModalStackEntry entry)
{
Selectable[] all = FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
foreach (Selectable s in all)
{
if (!s || !s.interactable) continue;
if (IsUnderModal(s.transform, modal)) continue;
// Добавляем временную CanvasGroup для блокировки
CanvasGroup cg = s.GetComponent();
if (!cg)
{
cg = s.gameObject.AddComponent();
}
if (cg.interactable)
{
cg.interactable = false;
entry.DisabledGroups.Add(cg);
}
}
}
private void RestoreBackgroundInteraction(ModalStackEntry entry)
{
foreach (CanvasGroup group in entry.DisabledGroups)
{
if (group)
{
group.interactable = true;
}
}
entry.DisabledGroups.Clear();
}
private void SetInitialFocus(Modal modal)
{
if (!EventSystem.current) return;
GameObject toSelect = null;
if (modal.FirstSelected && modal.FirstSelected.gameObject.activeInHierarchy)
{
toSelect = modal.FirstSelected.gameObject;
}
else
{
Selectable any = modal.GetComponentInChildren(includeInactive: false);
if (any) toSelect = any.gameObject;
}
if (!toSelect) return;
// Сбрасываем и устанавливаем фокус
EventSystem.current.SetSelectedGameObject(null);
EventSystem.current.SetSelectedGameObject(toSelect);
}
private void RestoreFocus(ModalStackEntry entry)
{
if (!EventSystem.current) return;
// Если есть другие модальные окна в стеке, фокусируемся на верхнем
if (_stack.Count > 0)
{
Modal topModal = _stack.Peek().Modal;
if (topModal != null)
{
SetInitialFocus(topModal);
return;
}
}
// Иначе восстанавливаем предыдущий фокус
GameObject toSelect = null;
if (entry.PreviousSelected && entry.PreviousSelected.activeInHierarchy)
{
Selectable selectable = entry.PreviousSelected.GetComponent();
if (selectable && selectable.interactable)
{
toSelect = entry.PreviousSelected;
}
}
// Если предыдущий элемент недоступен, ищем любой доступный
if (!toSelect)
{
Selectable anySelectable = FindAnyObjectByType(FindObjectsInactive.Exclude);
if (anySelectable && anySelectable.interactable)
{
toSelect = anySelectable.gameObject;
}
}
EventSystem.current.SetSelectedGameObject(toSelect);
}
private void EnsureFocusInModal(Modal modal)
{
if (!EventSystem.current) return;
GameObject current = EventSystem.current.currentSelectedGameObject;
// Если текущий выделенный элемент находится внутри модального окна, всё ок
if (current && IsUnderModal(current.transform, modal))
{
return;
}
// Иначе возвращаем фокус внутрь модального окна
SetInitialFocus(modal);
}
private bool IsUnderModal(Transform t, Modal modal)
{
if (!modal) return false;
Transform root = modal.transform;
for (Transform cur = t; cur; cur = cur.parent)
{
if (cur == root) return true;
}
return false;
}
///
/// Проверяет, является ли указанный Transform родителем модального окна.
///
private bool IsParentOfModal(Transform t, Modal modal)
{
if (!modal || !t) return false;
Transform modalTransform = modal.transform;
for (Transform cur = modalTransform.parent; cur; cur = cur.parent)
{
if (cur == t) return true;
}
return false;
}
}
}