using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; using UVC.Extension; using UVC.Locale; using UVC.UI.Tooltip; namespace UVC.UI.Toolbar { /// /// ToolbarModel에 정의된 데이터를 기반으로 실제 툴바 UI를 생성하고 관리하는 MonoBehaviour 클래스입니다. /// 사용자의 UI 상호작용을 감지하여 모델의 상태를 변경하거나 커맨드를 실행하고, /// 모델의 상태 변경에 따라 UI를 업데이트합니다. /// /// /// 주요 역할: /// - UI 렌더링: ToolbarModel의 Items 리스트를 순회하며 각 항목에 맞는 UI GameObject(버튼, 구분선 등)를 프리팹으로부터 생성하여 화면에 표시합니다. /// - 이벤트 바인딩: 생성된 UI 요소(예: Unity Button의 onClick)와 ToolbarButtonBase 모델의 ExecuteClick 메서드를 연결합니다. /// 또한, 모델의 OnStateChanged, OnToggleStateChanged 등의 이벤트를 구독하여 모델 상태 변경 시 UI를 업데이트합니다. /// - UI 업데이트: 모델의 상태(텍스트, 아이콘, 활성화 상태, 선택 상태 등)가 변경되면 해당 UI 요소의 시각적 표현을 업데이트합니다. /// - 리소스 관리: 생성된 UI GameObject들과 이벤트 구독을 관리하며, 툴바가 파괴되거나 다시 렌더링될 때 정리합니다(ClearToolbar). /// - 하위 메뉴 관리: ToolbarExpandableButton 클릭 시 하위 메뉴 UI를 생성하고 토글합니다. /// - 툴팁 관리: 각 버튼에 TooltipHandler를 추가하여 툴팁 기능을 제공합니다. /// /// 이 클래스의 인스턴스는 Unity 씬 내의 GameObject에 컴포넌트로 추가되어야 하며, /// Inspector를 통해 필요한 프리팹들(standardButtonPrefab, toggleButtonPrefab 등)과 /// UI 요소들이 배치될 부모 Transform(toolbarContainer), LayoutGroup 등을 할당받아야 합니다. /// /// /// /// // 1. Unity 에디터에서 빈 GameObject를 만들고 ToolbarView 스크립트를 추가합니다. /// // 2. Inspector에서 ToolbarView 컴포넌트의 다음 필드들을 설정합니다: /// // - Standard Button Prefab: 표준 버튼 UI 프리팹 /// // - Toggle Button Prefab: 토글 버튼 UI 프리팹 /// // - Radio Button Prefab: 라디오 버튼 UI 프리팹 /// // - Expandable Button Prefab: 확장 버튼 UI 프리팹 /// // - Separator Prefab: 구분선 UI 프리팹 /// // - Sub Menu Panel Prefab: 확장 버튼의 하위 메뉴 패널 UI 프리팹 /// // - Toolbar Container: 생성된 툴바 항목 UI들이 자식으로 추가될 Transform (보통 이 GameObject 자체 또는 자식 Panel) /// // - Layout Group: Toolbar Container에 연결된 LayoutGroup (예: VerticalLayoutGroup, HorizontalLayoutGroup) /// // - Root Canvas: (선택 사항, TooltipManager 초기화에 필요하며 보통 자동으로 찾음) /// // /// // 3. ToolbarController (또는 유사 클래스)에서 ToolbarModel을 설정한 후, /// // ToolbarView의 Initialize 메서드를 호출하여 툴바 UI를 생성합니다. /// // ToolbarController controller = GetComponent(); // 또는 다른 방식으로 참조 /// // ToolbarModel model = controller.GetConfiguredModel(); // 가정: 컨트롤러가 모델을 반환 /// // Initialize(model); /// /// public class ToolbarView : MonoBehaviour { /// /// 현재 툴바 UI의 기반이 되는 데이터 모델입니다. /// Initialize 메서드를 통해 외부(주로 ToolbarController)에서 주입받습니다. /// protected ToolbarModel ToolbarModel { get; private set; } // --- Inspector에서 할당할 프리팹 및 UI 요소들 --- [Header("UI Prefabs")] [Tooltip("표준 버튼 UI에 사용될 프리팹입니다.")] public GameObject standardButtonPrefab; [Tooltip("토글 버튼 UI에 사용될 프리팹입니다.")] public GameObject toggleButtonPrefab; [Tooltip("라디오 버튼 UI에 사용될 프리팹입니다.")] public GameObject radioButtonPrefab; [Tooltip("확장 가능한 버튼 UI에 사용될 프리팹입니다.")] public GameObject expandableButtonPrefab; [Tooltip("구분선 UI에 사용될 프리팹입니다.")] public GameObject separatorPrefab; [Tooltip("확장 버튼의 하위 메뉴 패널 UI에 사용될 프리팹입니다.")] public GameObject subMenuPanelPrefab; [Header("UI Layout")] [Tooltip("생성된 툴바 항목 UI들이 자식으로 추가될 부모 Transform입니다.")] public Transform toolbarContainer; [Tooltip("툴바 항목들의 자동 배치를 담당하는 LayoutGroup 컴포넌트입니다 (예: VerticalLayoutGroup).")] public LayoutGroup layoutGroup; /// /// 툴바 버튼 모델(ToolbarButtonBase)과 해당 모델을 나타내는 실제 UI GameObject를 매핑하는 딕셔너리입니다. /// 모델 상태 변경 시 해당 GameObject를 찾아 UI를 업데이트하거나, UI 정리 시 사용됩니다. /// protected Dictionary _modelToGameObjectMap = new Dictionary(); /// /// 툴팁 표시에 사용될 루트 Canvas입니다. /// Inspector에서 할당하거나, Awake에서 자동으로 찾으려고 시도합니다. /// protected Canvas rootCanvas; /// /// MonoBehaviour의 Awake 메서드입니다. /// toolbarContainer, layoutGroup, rootCanvas 등의 필수 참조를 초기화하고, /// TooltipManager를 초기화합니다. /// protected virtual void Awake() { // toolbarContainer 자동 할당 (Inspector에서 할당되지 않은 경우) if (toolbarContainer == null) toolbarContainer = GetComponent(); if (toolbarContainer == null) toolbarContainer = GetComponentInChildren(true); // 비활성 자식도 포함 if (toolbarContainer == null) Debug.LogError("ToolbarView: toolbarContainer가 할당되지 않았습니다. Inspector에서 설정하거나 현재 GameObject에 Transform이 있어야 합니다.", this); // layoutGroup 자동 할당 및 추가 (Inspector에서 할당되지 않은 경우) if (layoutGroup == null && toolbarContainer != null) { layoutGroup = toolbarContainer.gameObject.GetComponent(); if (layoutGroup == null) { // 기본으로 VerticalLayoutGroup 추가. 필요시 프로젝트에 맞게 수정. layoutGroup = toolbarContainer.gameObject.AddComponent(); Debug.LogWarning("ToolbarView: LayoutGroup이 toolbarContainer에 없어 새로 추가합니다. (기본 VerticalLayoutGroup)", this); } } if (layoutGroup == null) Debug.LogError("ToolbarView: layoutGroup이 할당되지 않았습니다. toolbarContainer에 LayoutGroup 컴포넌트를 추가해주세요.", this); // rootCanvas 자동 찾기 (TooltipManager 초기화에 필요) if (rootCanvas == null) rootCanvas = GetComponentInParent(); if (rootCanvas == null) { Canvas[] canvases = FindObjectsByType(FindObjectsSortMode.None); // Unity 2023부터 FindObjectsByType 사용 권장 foreach (Canvas c in canvases) { if (c.isRootCanvas) { rootCanvas = c; break; } } if (rootCanvas == null && canvases.Length > 0) rootCanvas = canvases[0]; // 최후의 수단 } // TooltipManager 초기화 if (rootCanvas != null) { if(!TooltipManager.Instance.IsInitialized) TooltipManager.Instance.Initialize(rootCanvas.transform, rootCanvas); } else { Debug.LogError("ToolbarView: rootCanvas를 찾을 수 없어 TooltipVisualManager를 초기화할 수 없습니다."); } } /// /// 지정된 ToolbarModel을 사용하여 툴바 UI를 초기화하고 렌더링합니다. /// 기존 UI가 있다면 정리(ClearToolbar)한 후, 새 모델에 따라 UI를 다시 생성(RenderToolbar)합니다. /// /// 화면에 표시할 툴바의 데이터 모델입니다. public virtual void Initialize(ToolbarModel toolbarModel) { this.ToolbarModel = toolbarModel; if (toolbarContainer == null) { Debug.LogError("ToolbarView: Initialize 실패. toolbarContainer가 할당되지 않았습니다.", this); return; } if (layoutGroup == null) // layoutGroup도 필수적이므로 확인 { Debug.LogError("ToolbarView: Initialize 실패. layoutGroup이 할당되지 않았습니다.", this); return; } if (this.ToolbarModel == null) { Debug.LogError("ToolbarView: Initialize 실패. 제공된 toolbarModel이 null입니다.", this); ClearToolbar(); // 모델이 없으면 기존 UI라도 정리 return; } RenderToolbar(); } /// /// 현재 툴바에 표시된 모든 UI 요소들을 제거하고, 관련된 이벤트 구독을 해제합니다. /// _modelToGameObjectMap, _toggleGroups를 비우고, 열려있는 하위 메뉴(currentSubMenu)도 파괴합니다. /// 툴팁도 숨깁니다. /// protected virtual void ClearToolbar() { if (_modelToGameObjectMap != null) { foreach (var pair in _modelToGameObjectMap) { if (pair.Key != null) // 모델(버튼)에 연결된 이벤트 구독 해제 { pair.Key.OnStateChanged -= () => UpdateItemVisuals(pair.Key); if (pair.Key is ToolbarToggleButton toggleButton) { toggleButton.OnToggleStateChanged -= (isSelected) => UpdateToggleVisuals(toggleButton, isSelected); } } if (pair.Value != null) // UI GameObject 파괴 { TooltipHandler handler = pair.Value.GetComponent(); if (handler != null) // 툴팁 핸들러 이벤트도 정리 (필요시) { handler.OnPointerEnterAction = null; handler.OnPointerExitAction = null; } Destroy(pair.Value); } } _modelToGameObjectMap.Clear(); } if (_toggleGroups != null) // 토글 그룹 GameObject들도 정리 { foreach (var groupObj in _toggleGroups.Values) { if (groupObj != null && groupObj.gameObject != null) Destroy(groupObj.gameObject); } _toggleGroups.Clear(); } if (currentSubMenu != null) // 열려있는 하위 메뉴 파괴 { Destroy(currentSubMenu); currentSubMenu = null; } if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized) // 툴팁 숨기기 { TooltipManager.Instance.HideTooltip(); } } /// /// ToolbarModel에 정의된 항목들을 기반으로 실제 UI 요소들을 생성하여 툴바를 구성합니다. /// 먼저 ClearToolbar를 호출하여 기존 UI를 정리한 후, 모델의 각 항목에 대해 적절한 프리팹을 사용하여 /// UI GameObject를 생성하고, 필요한 설정(이벤트 바인딩, 툴팁 등)을 수행합니다. /// protected void RenderToolbar() { ClearToolbar(); // 기존 UI 및 이벤트 구독 정리 if (ToolbarModel == null || ToolbarModel.Items == null) return; if (toolbarContainer == null) { Debug.LogError("ToolbarView: RenderToolbar 중단. toolbarContainer가 null입니다.", this); return; } foreach (var itemModel in ToolbarModel.Items) { GameObject itemUIObject = null;// 생성될 UI GameObject // 1. 모델 타입에 따라 적절한 프리팹 선택 및 인스턴스화 if (itemModel is ToolbarSeparator) { if (separatorPrefab != null) itemUIObject = Instantiate(separatorPrefab, toolbarContainer); else Debug.LogError("ToolbarView: separatorPrefab이 할당되지 않았습니다.", this); } else if (itemModel is ToolbarButtonBase buttonModel) // 모든 버튼 타입의 기본 처리 { GameObject prefabToUse = null; if (buttonModel is ToolbarRadioButton) prefabToUse = radioButtonPrefab; else if (buttonModel is ToolbarToggleButton) prefabToUse = toggleButtonPrefab; // 라디오 버튼보다 먼저 와야 함 (상속 관계) else if (buttonModel is ToolbarExpandableButton) prefabToUse = expandableButtonPrefab; else if (buttonModel is ToolbarStandardButton) prefabToUse = standardButtonPrefab; // else: 다른 커스텀 버튼 타입이 있다면 여기에 추가 if (prefabToUse != null) { itemUIObject = Instantiate(prefabToUse, toolbarContainer); } else { Debug.LogError($"ToolbarView: {buttonModel.GetType().Name}에 대한 프리팹이 할당되지 않았거나 알 수 없는 버튼 타입입니다.", this); continue; // 이 버튼은 건너뜀 } // 2. 모델-UI 매핑 및 모델 상태 변경 구독 _modelToGameObjectMap[buttonModel] = itemUIObject; // 모델의 OnStateChanged 이벤트가 발생하면 UpdateItemVisuals 메서드를 호출하여 UI를 업데이트합니다. // 람다 표현식에서 buttonModel을 직접 클로저로 캡처합니다. buttonModel.OnStateChanged += () => UpdateItemVisuals(buttonModel); // 3. 버튼 UI 초기 설정 및 사용자 상호작용(이벤트) 바인딩 SetupButtonVisualsAndInteractions(buttonModel, itemUIObject); // 4. 툴팁 핸들러 추가 및 설정 if (!string.IsNullOrEmpty(buttonModel.TooltipKey)) { TooltipHandler tooltipHandler = itemUIObject.GetComponent(); if (tooltipHandler == null) tooltipHandler = itemUIObject.AddComponent(); tooltipHandler.Tooltip = buttonModel.TooltipKey; // 툴팁 내용 또는 키 전달 // TooltipManager의 이벤트 핸들러 연결 if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized) { tooltipHandler.OnPointerEnterAction = TooltipManager.Instance.HandlePointerEnter; tooltipHandler.OnPointerExitAction = TooltipManager.Instance.HandlePointerExit; } } } else { Debug.LogWarning($"ToolbarView: 알 수 없는 IToolbarItem 타입 ({itemModel.GetType().Name})이거나, UI 생성이 지원되지 않습니다.", this); } if (itemUIObject != null) { itemUIObject.name = $"ToolbarItem_{itemModel.GetType().Name}_{(itemModel is ToolbarButtonBase b ? b.Text : "")}"; } } // 모든 UI 항목이 추가된 후 레이아웃 그룹 업데이트 if (layoutGroup != null) { // LayoutRebuilder.ForceRebuildLayoutImmediate(layoutGroup.GetComponent()); // 즉시 업데이트 // 또는 FitToChildren 확장 메서드 사용 (UVC.Extension에 정의되어 있다고 가정) layoutGroup.FitToChildren(width: layoutGroup is VerticalLayoutGroup, height: true); } } /// /// 버튼 모델(ToolbarButtonBase)과 해당 UI GameObject를 받아, /// 버튼의 초기 시각적 요소(텍스트, 아이콘, 활성화 상태)를 설정하고, /// UI 상호작용(예: 클릭) 시 모델의 메서드가 호출되도록 이벤트를 연결합니다. /// /// 설정할 버튼의 데이터 모델입니다. /// 모델에 해당하는, 화면에 표시될 UI GameObject입니다. protected void SetupButtonVisualsAndInteractions(ToolbarButtonBase model, GameObject itemObj) { // 1. 공통 UI 요소(텍스트, 아이콘, 활성화 상태) 초기 업데이트 UpdateCommonButtonVisuals(model, itemObj); // 2. 버튼 타입별 특화된 UI 요소 설정 및 이벤트 바인딩// 2. 버튼 타입별 특화된 UI 요소 설정 및 이벤트 바인딩 if (model is ToolbarRadioButton radioModel) { Toggle toggleComponent = itemObj.GetComponent(); // 라디오 버튼 프리팹에는 Toggle 컴포넌트가 있어야 함 if (toggleComponent != null) { // UnityEngine.UI.ToggleGroup 가져오거나 생성하여 할당 ToggleGroup uiToggleGroup = GetOrCreateToggleGroup(radioModel.GroupName); toggleComponent.group = uiToggleGroup; // 초기 선택 상태 설정 (UI 이벤트 발생 없이) toggleComponent.SetIsOnWithoutNotify(radioModel.IsSelected); // UI 토글 값 변경(사용자 클릭) 시 모델 업데이트 toggleComponent.onValueChanged.AddListener((isSelected) => { // 사용자가 UI를 직접 클릭하여 상태를 변경한 경우 if (isSelected) // 라디오 버튼은 선택될 때만 모델 업데이트 요청 { // 모델의 ExecuteClick을 호출하여 그룹 내 선택 로직 및 커맨드 실행을 트리거합니다. // 파라미터로 true (선택됨) 또는 모델 자체를 전달할 수 있습니다. radioModel.ExecuteClick(true); } }); // 모델의 IsSelected 상태 변경(OnToggleStateChanged 이벤트) 시 UI 업데이트 // 이 이벤트는 모델의 IsSelected 속성이 코드에 의해 변경될 때 발생합니다. radioModel.OnToggleStateChanged += (isSelected) => UpdateToggleVisuals(radioModel, isSelected); } else Debug.LogError($"ToolbarView: RadioButton '{model.Text}'의 GameObject에 Toggle 컴포넌트가 없습니다.", itemObj); } else if (model is ToolbarToggleButton toggleModel) { Toggle toggleComponent = itemObj.GetComponent(); // 토글 버튼 프리팹에도 Toggle 컴포넌트 필요 if (toggleComponent != null) { toggleComponent.SetIsOnWithoutNotify(toggleModel.IsSelected); toggleComponent.onValueChanged.AddListener((isSelected) => { // 사용자가 UI 토글을 클릭하면 모델의 ExecuteClick을 호출합니다. // ExecuteClick 내부에서 IsSelected가 변경되고 관련 이벤트/콜백이 호출됩니다. toggleModel.ExecuteClick(); }); toggleModel.OnToggleStateChanged += (isSelected) => UpdateToggleVisuals(toggleModel, isSelected); } else Debug.LogError($"ToolbarView: ToggleButton '{model.Text}'의 GameObject에 Toggle 컴포넌트가 없습니다.", itemObj); } else if (model is ToolbarExpandableButton expandableModel) { Button uiButton = itemObj.GetComponent