using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UVC.Locale;
using UVC.Log;
using UVC.UI.Commands;
using UVC.UI.Modal;
namespace UVC.UI.Menu
{
///
/// 상단 메뉴의 로직을 관리하는 컨트롤러 클래스입니다.
/// Model(데이터: )과 View(표시: ) 사이의 중재자 역할을 하며,
/// 메뉴 아이템 초기화, 사용자 입력 처리, 언어 변경 감지 등의 기능을 수행합니다.
/// 이 클래스는 MonoBehaviour를 상속받아 Unity 게임 오브젝트에 컴포넌트로 추가될 수 있습니다.
/// Inspector에서 와 추가해서 사용합니다.
///
///
/// 다음은 TopMenuController를 사용해 메뉴를 구성하는 예제 입니다.
///
/// private TopMenuController topMenu;
///
/// // 예: '도움말' 메뉴 추가
/// topMenu.AddMenuItem(new MenuItemData("help", "menu_help", subMenuItems: new List;
/// {
/// new MenuItemData("help_about", "menu_help_about", new DebugLogCommand("도움말 > 정보 선택됨"))
/// }));
///
/// topMenu.Initialize(); // 메뉴 초기화 및 생성 요청
///
///
public class TopMenuController : MonoBehaviour
{
///
/// 메뉴의 시각적 표현을 담당하는 컴포넌트에 대한 참조입니다.
/// Inspector에서 할당하거나 메서드에서 자동으로 찾습니다.
///
protected TopMenuView view;
///
/// 메뉴 아이템들의 데이터 구조를 관리하는 인스턴스입니다.
/// 메뉴의 내용(예: 아이템 이름, 명령, 하위 메뉴)을 저장합니다.
///
protected TopMenuModel model;
///
/// 다국어 처리를 위한 의 인스턴스입니다.
/// 메뉴 아이템의 텍스트를 현재 설정된 언어에 맞게 표시하는 데 사용됩니다.
///
protected LocalizationManager _locManager;
protected bool isInitialized = false;
///
/// MonoBehaviour의 Awake 메시지입니다. 스크립트 인스턴스가 로드될 때 호출됩니다.
/// 주로 컴포넌트를 찾는 데 사용됩니다.
/// 상속 클래스에서 오버라이드하여 추가적인 초기화 로직을 구현할 수 있습니다.
///
protected virtual void Awake()
{
// 1. 이 GameObject에 연결된 TopMenuView 컴포넌트를 찾습니다.
view = GetComponent();
// 2. 만약 현재 GameObject에 없다면, 자식 GameObject들 중에서 TopMenuView 컴포넌트를 찾습니다.
if (view == null)
{
view = GetComponentInChildren();
}
// 메뉴 데이터 모델 인스턴스 생성
model = new TopMenuModel();
// 다국어 관리자 인스턴스 가져오기
_locManager = LocalizationManager.Instance;
}
///
/// MonoBehaviour의 Start 메시지입니다. 첫 번째 프레임 업데이트 전에 호출됩니다.
/// 모델 초기화, 인스턴스 가져오기, 메뉴 아이템 데이터 설정,
/// View에 메뉴 생성 요청, 이벤트 핸들러 등록 등의 주요 초기화 작업을 수행합니다.
///
protected virtual void Start()
{
// View 컴포넌트가 할당되었는지 확인
if (view == null)
{
ULog.Error("TopMenuView가 Inspector에서 할당되지 않았거나 찾을 수 없습니다. TopMenuController가 정상적으로 작동하지 않습니다.");
return; // View가 없으면 더 이상 진행할 수 없음
}
// LocalizationManager 인스턴스 확인
if (_locManager == null)
{
ULog.Error("LocalizationManager 인스턴스를 찾을 수 없습니다. 메뉴 텍스트가 올바르게 표시되지 않거나, 언어 변경 기능이 작동하지 않을 수 있습니다.");
// _locManager가 null이어도 메뉴 구조 자체는 생성될 수 있도록 계속 진행합니다.
// TopMenuView와 이 클래스의 다른 부분에서 _locManager null 체크를 통해 안전하게 처리합니다.
}
}
///
/// MonoBehaviour의 OnDestroy 메시지입니다. GameObject가 파괴될 때 호출됩니다.
/// 등록된 이벤트 핸들러들을 해제하여 메모리 누수 및 잠재적 오류를 방지합니다.
///
protected virtual void OnDestroy()
{
// View의 메뉴 아이템 클릭 이벤트 핸들러 해제
if (view != null)
{
view.OnMenuItemClicked -= HandleMenuItemClicked;
}
// LocalizationManager의 언어 변경 이벤트 핸들러 해제
if (_locManager != null)
{
_locManager.OnLanguageChanged -= HandleLanguageChanged;
}
}
///
/// 모델 초기화, 인스턴스 가져오기, 메뉴 아이템 데이터 설정,
/// View에 메뉴 생성 요청, 이벤트 핸들러 등록 등의 주요 초기화 작업을 수행합니다.
///
public virtual void Initialize()
{
if (isInitialized) {
Debug.LogWarning("TopMenuController가 이미 초기화되었습니다. 중복 초기화를 방지합니다.");
return;
}
// View에 기존 메뉴 아이템들을 지우도록 요청
view.ClearMenuItems();
// View에 현재 모델 데이터를 기반으로 메뉴 UI를 생성하도록 요청
// view.MenuContainer는 TopMenuView에서 메뉴 UI 요소들이 배치될 부모 Transform을 가리킵니다.
view.CreateMenuItems(model.MenuItems, view.menuContainer);
// View에서 발생하는 메뉴 아이템 클릭 이벤트에 대한 핸들러 등록
view.OnMenuItemClicked += HandleMenuItemClicked;
// LocalizationManager가 존재하고, 언어 변경 이벤트를 지원한다면 핸들러 등록
if (_locManager != null)
{
_locManager.OnLanguageChanged += HandleLanguageChanged;
}
isInitialized = true;
}
///
/// 메뉴 아이템을 추가합니다.
///
/// 추가할 아이템
public void AddMenuItem(MenuItemData newItem)
{
if (model == null || model.MenuItems == null)
{
ULog.Warning("모델이 초기화되지 않아 메뉴 아이템을 추가할 수 없습니다.");
return;
}
// 모델에 새 메뉴 아이템 추가
model.MenuItems.Add(newItem);
//ULog.Debug($"새로운 메뉴 아이템 '{newItem.ItemId}'가 모델에 추가되었습니다.");
}
///
/// 추가 된 아이템을 제거 합니다.
///
/// 삭제 할 아이템의 ID
public void RemoveMenuItem(string itemId)
{
if (model == null || model.MenuItems == null)
{
ULog.Warning("모델이 초기화되지 않아 메뉴 아이템을 제거할 수 없습니다.");
return;
}
// 모델에서 해당 ID를 가진 메뉴 아이템을 재귀적으로 검색
MenuItemData targetItem = FindMenuItemRecursive(model.MenuItems, itemId);
if (targetItem != null)
{
// 구분선은 제거할 수 없음
if (targetItem.IsSeparator)
{
ULog.Warning($"구분선('{itemId}')은 제거할 수 없습니다. 작업이 무시됩니다.");
return;
}
// 모델에서 아이템 제거
model.MenuItems.Remove(targetItem);
ULog.Debug($"ID가 '{itemId}'인 메뉴 아이템이 모델에서 제거되었습니다.");
}
else
{
ULog.Warning($"ID가 '{itemId}'인 메뉴 아이템을 모델에서 찾을 수 없어 제거할 수 없습니다.");
}
}
///
/// 이벤트가 발생했을 때 호출되는 핸들러입니다.
/// 클릭된 메뉴 아이템()의 유효성을 검사하고,
/// 연결된 를 실행합니다.
/// IUndoableCommand인 경우 Undo/Redo 히스토리에 기록됩니다.
///
/// 사용자가 클릭한 메뉴 아이템의 데이터입니다.
protected virtual void HandleMenuItemClicked(MenuItemData clickedItemData)
{
// 클릭된 아이템이 구분선이거나 비활성화 상태인지 확인
if (clickedItemData.IsSeparator)
{
if (!clickedItemData.IsEnabled)
{
Debug.Log($"비활성화된 메뉴 아이템 클릭 시도: {clickedItemData.ItemId} (표시 키: {clickedItemData.DisplayName})");
}
return;
}
if (!clickedItemData.IsEnabled)
{
// 비활성화된 아이템 클릭 시 로그 (디버깅 목적)
ULog.Debug($"비활성화된 메뉴 아이템 클릭 시도: {clickedItemData.ItemId} (표시 키: {clickedItemData.DisplayName})");
return; // 비활성화된 아이템은 명령을 실행하지 않음
}
// 클릭된 메뉴 아이템 정보 로그 (디버깅 목적)
ULog.Debug($"메뉴 아이템 클릭됨: {clickedItemData.ItemId} (표시 키: {clickedItemData.DisplayName})");
// 메뉴 아이템에 연결된 Command 실행
ExecuteCommand(clickedItemData.Command, clickedItemData.CommandParameter);
}
///
/// Command를 실행합니다.
/// IUndoableCommand인 경우 UndoRedoManager를 통해 실행하여 히스토리에 기록합니다.
/// 일반 ICommand인 경우 직접 실행합니다.
///
/// 실행할 Command
/// Command 파라미터
protected virtual void ExecuteCommand(ICommand command, object parameter = null)
{
if (command == null) return;
// IUndoableCommand인 경우 UndoRedoManager를 통해 실행
if (command is IUndoableCommand undoableCommand)
{
// UndoRedoManager가 존재하는지 확인 (Studio 씬에서만 사용 가능)
var undoRedoManager = FindAnyObjectByType();
if (undoRedoManager != null)
{
undoRedoManager.ExecuteCommand(undoableCommand, parameter);
return;
}
}
// 일반 ICommand이거나 UndoRedoManager가 없는 경우 직접 실행
command.Execute(parameter);
}
///
/// 이벤트가 발생했을 때 호출되는 핸들러입니다.
/// 새로운 언어 코드()가 적용되었음을 감지하고,
/// 에 모든 메뉴 텍스트를 업데이트하도록 요청합니다.
///
/// 새롭게 설정된 언어 코드입니다 (예: "ko", "en").
protected virtual void HandleLanguageChanged(string newLanguageCode)
{
ULog.Debug($"언어 변경 감지됨: {newLanguageCode}. 메뉴 텍스트 업데이트를 시도합니다...");
// View와 Model이 모두 유효한 경우에만 텍스트 업데이트 진행
if (view != null && model != null)
{
view.UpdateAllMenuTexts(model.MenuItems);
ULog.Debug("메뉴 텍스트 업데이트 완료.");
}
else
{
ULog.Warning("View 또는 Model이 null이므로 메뉴 텍스트를 업데이트할 수 없습니다.");
}
}
///
/// 특정 ID를 가진 메뉴 아이템의 활성화 상태를 동적으로 변경합니다.
/// 이 메서드는 모델()의 데이터를 변경하고,
/// 변경 사항을 뷰()에 반영하여 UI의 버튼 상호작용 상태를 업데이트합니다.
///
/// 상태를 변경할 메뉴 아이템의 고유 ID입니다.
/// 새로운 활성화 상태입니다 (true: 활성, false: 비활성).
public virtual void SetMenuItemEnabled(string itemId, bool isEnabled)
{
if (model == null || model.MenuItems == null)
{
ULog.Warning("모델이 초기화되지 않아 메뉴 아이템 활성화 상태를 변경할 수 없습니다.");
return;
}
// 모델에서 해당 ID를 가진 메뉴 아이템을 재귀적으로 검색
MenuItemData targetItem = FindMenuItemRecursive(model.MenuItems, itemId);
if (targetItem != null)
{
// 구분선의 활성화 상태는 변경할 수 없음
if (targetItem.IsSeparator)
{
ULog.Warning($"구분선('{itemId}')의 활성화 상태는 변경할 수 없습니다. 작업이 무시됩니다.");
return;
}
// 이미 요청된 상태와 동일하다면 변경하지 않음 (불필요한 업데이트 방지)
if (targetItem.IsEnabled == isEnabled)
{
ULog.Debug($"메뉴 아이템 '{itemId}'은(는) 이미 요청된 활성화 상태({isEnabled})입니다.");
return;
}
// 1. 모델 데이터의 활성화 상태 변경
targetItem.IsEnabled = isEnabled;
ULog.Debug($"모델에서 메뉴 아이템 '{itemId}'의 활성화 상태가 '{isEnabled}'(으)로 변경되었습니다.");
// 2. View 업데이트: 해당 메뉴 아이템 GameObject를 찾아 Button 컴포넌트의 interactable 속성 갱신
if (view != null && view.isActiveAndEnabled) // View가 유효하고 활성화된 상태일 때만 시도
{
if (view.TryGetMenuItemGameObject(itemId, out GameObject menuItemGO))
{
Button button = menuItemGO.GetComponent