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 { public class ToolbarView : MonoBehaviour { protected ToolbarModel ToolbarModel { get; private set; } // 이제 이 프리팹들은 ToolbarView GameObject의 Inspector에서 직접 할당해야 합니다. public GameObject standardButtonPrefab; public GameObject toggleButtonPrefab; public GameObject radioButtonPrefab; public GameObject expandableButtonPrefab; public GameObject separatorPrefab; public GameObject subMenuPanelPrefab; public Transform toolbarContainer; public LayoutGroup layoutGroup; // UI 요소와 모델을 매핑하여 상태 업데이트 시 사용 protected Dictionary _modelToGameObjectMap = new Dictionary(); // Canvas 참조 (Inspector에서 할당하거나 GetComponentInParent 등으로 찾기) protected Canvas rootCanvas; protected virtual void Awake() { if (toolbarContainer == null) { toolbarContainer = GetComponent(); } if (toolbarContainer == null) { toolbarContainer = GetComponentInChildren(); } if (layoutGroup == null) { layoutGroup = toolbarContainer.gameObject.GetComponent(); } if (layoutGroup == null) { layoutGroup = toolbarContainer.gameObject.AddComponent(); } // TooltipVisualManager 초기화 // rootCanvas가 Inspector에서 할당되지 않았다면 여기서 찾아야 합니다. if (rootCanvas == null) { rootCanvas = GetComponentInParent(); if (rootCanvas == null) { // 최상위 Canvas를 찾기 위한 더 강력한 방법 (예: FindObjectOfType() 후 필터링) // 또는 씬에 하나의 메인 Canvas만 있다는 가정 하에 동작 Canvas[] canvases = FindObjectsByType(FindObjectsSortMode.InstanceID); foreach (Canvas c in canvases) { if (c.isRootCanvas) { rootCanvas = c; break; } } if (rootCanvas == null && canvases.Length > 0) rootCanvas = canvases[0]; // 최후의 수단 } } if (rootCanvas != null) { // defaultParent는 툴팁이 생성될 위치입니다. // rootCanvas.transform을 사용하면 Canvas 직속 자식으로 생성됩니다. // 또는 툴팁 전용 레이어를 두고 싶다면 해당 Transform을 사용합니다. TooltipManager.Instance.Initialize(rootCanvas.transform, rootCanvas); } else { if (rootCanvas == null) Debug.LogError("ToolbarView: rootCanvas를 찾을 수 없어 TooltipVisualManager를 초기화할 수 없습니다."); } } public virtual void Initialize(ToolbarModel toolbarModel) { ToolbarModel = toolbarModel; if (toolbarContainer == null) { Debug.LogError("toolbarContainer가 할당되지 않았습니다."); return; } if (layoutGroup == null) { Debug.LogError("layoutGroup이 할당되지 않았습니다."); return; } RenderToolbar(); } protected virtual void ClearToolbar() { 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) { // TooltipHandler 이벤트 구독 해제 (필요 시) TooltipHandler handler = pair.Value.GetComponent(); if (handler != null) { handler.OnPointerEnterAction = null; handler.OnPointerExitAction = null; } Destroy(pair.Value); } } _modelToGameObjectMap.Clear(); _toggleGroups.Clear(); // 토글 그룹도 정리 if (currentSubMenu != null) Destroy(currentSubMenu); TooltipManager.Instance.HideTooltip(); // 툴바가 클리어될 때 툴팁도 숨김 } protected void RenderToolbar() { ClearToolbar(); // 기존 UI 및 이벤트 구독 정리 if (ToolbarModel == null || ToolbarModel.Items == null) return; foreach (var item in ToolbarModel.Items) { GameObject itemObj = null; if (item is ToolbarSeparator) { itemObj = Instantiate(separatorPrefab, toolbarContainer); } else if (item is ToolbarButtonBase buttonModel) // 모든 버튼 타입의 기본 처리 { // 적절한 프리팹 선택 if (buttonModel is ToolbarRadioButton) itemObj = Instantiate(radioButtonPrefab, toolbarContainer); else if (buttonModel is ToolbarToggleButton) itemObj = Instantiate(toggleButtonPrefab, toolbarContainer); else if (buttonModel is ToolbarExpandableButton) itemObj = Instantiate(expandableButtonPrefab, toolbarContainer); else if (buttonModel is ToolbarStandardButton) itemObj = Instantiate(standardButtonPrefab, toolbarContainer); // else // 다른 커스텀 버튼 타입이 있다면 추가 if (itemObj != null) { _modelToGameObjectMap[buttonModel] = itemObj; buttonModel.OnStateChanged += () => UpdateItemVisuals(buttonModel); // 모델 상태 변경 시 UI 업데이트 구독 // 초기 UI 설정 및 이벤트 바인딩 SetupButtonVisualsAndInteractions(buttonModel, itemObj); // 툴팁 핸들러 추가 및 설정 if (!string.IsNullOrEmpty(buttonModel.TooltipKey)) { TooltipHandler tooltipHandler = itemObj.GetComponent(); if (tooltipHandler == null) tooltipHandler = itemObj.AddComponent(); tooltipHandler.Tooltip = buttonModel.TooltipKey; tooltipHandler.OnPointerEnterAction = TooltipManager.Instance.HandlePointerEnter; tooltipHandler.OnPointerExitAction = TooltipManager.Instance.HandlePointerExit; } } } } // 모든 항목이 추가된 후 toolbarContainer의 높이를 layoutGroup의 내용에 맞게 조절합니다. if (layoutGroup != null) { // layoutGroup 컴포넌트가 있는 RectTransform을 가져옵니다. // 일반적으로 toolbarContainer의 RectTransform과 동일합니다. RectTransform layoutRectTransform = layoutGroup.GetComponent(); if (layoutRectTransform != null) { // 레이아웃을 즉시 재계산하도록 강제합니다. // 이렇게 하면 preferredHeight가 현재 자식들을 기준으로 정확하게 계산됩니다. LayoutRebuilder.ForceRebuildLayoutImmediate(layoutRectTransform); // LayoutGroup에 의해 계산된 선호 높이를 가져옵니다. float preferredHeight = LayoutUtility.GetPreferredHeight(layoutRectTransform); // toolbarContainer (즉, layoutRectTransform)의 높이를 설정합니다. // 이 코드는 toolbarContainer의 높이가 sizeDelta.y에 의해 제어된다고 가정합니다. // 앵커가 수직으로 늘어나도록 설정된 경우, 이 방식이 원하는 대로 작동하지 않거나 // anchoredPosition 및 오프셋을 대신 조정해야 할 수 있습니다. layoutRectTransform.sizeDelta = new Vector2(layoutRectTransform.sizeDelta.x, preferredHeight); } } } // 버튼 모델과 게임 오브젝트를 받아 초기 시각적 요소 설정 및 UI 상호작용을 연결합니다. protected void SetupButtonVisualsAndInteractions(ToolbarButtonBase model, GameObject itemObj) { // 공통 UI 요소 업데이트 (Text, Icon, Enabled) UpdateCommonButtonVisuals(model, itemObj); // 타입별 UI 요소 및 이벤트 설정 if (model is ToolbarRadioButton radioModel) { Toggle toggle = itemObj.GetComponent(); if (toggle != null) { ToggleGroup toggleGroup = GetOrCreateToggleGroup(radioModel.GroupName); toggle.group = toggleGroup; toggle.SetIsOnWithoutNotify(radioModel.IsSelected); // 초기 상태 설정 (이벤트 발생 방지) toggle.onValueChanged.AddListener((isSelected) => { // UI에서 사용자가 직접 토글한 경우 모델 업데이트 // 중요: 라디오 버튼은 그룹에 의해 선택이 관리되므로, isSelected가 true일 때만 모델 업데이트 요청 if (isSelected) radioModel.ExecuteClick(true); // 모델의 ExecuteClick -> RadioGroup.SetSelected 호출 }); // IsSelected 변경은 OnStateChanged를 통해 UpdateItemVisuals에서 처리되거나, // 좀 더 명시적인 OnToggleStateChanged 이벤트를 사용할 수 있습니다. radioModel.OnToggleStateChanged += (isSelected) => UpdateToggleVisuals(radioModel, isSelected); } } else if (model is ToolbarToggleButton toggleModel) { Toggle toggle = itemObj.GetComponent(); if (toggle != null) { toggle.SetIsOnWithoutNotify(toggleModel.IsSelected); toggle.onValueChanged.AddListener((isSelected) => { toggleModel.ExecuteClick(); // 모델의 ExecuteClick이 IsSelected를 변경하고 OnStateChanged 호출 }); toggleModel.OnToggleStateChanged += (isSelected) => UpdateToggleVisuals(toggleModel, isSelected); } } else if (model is ToolbarExpandableButton expandableModel) { Button uiButton = itemObj.GetComponent