using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; using UVC.Locale; namespace UVC.UI.ToolBar { public class ToolbarView : MonoBehaviour { public Toolbar ToolbarModel { get; private set; } public GameObject standardButtonPrefab; public GameObject toggleButtonPrefab; public GameObject radioButtonPrefab; public GameObject expandableButtonPrefab; public GameObject separatorPrefab; public GameObject subMenuPanelPrefab; public GameObject tooltipPrefab; // 툴팁 UI 프리팹 private Transform toolbarContainer; // UI 요소와 모델을 매핑하여 상태 업데이트 시 사용 private Dictionary _modelToGameObjectMap = new Dictionary(); private GameObject _activeTooltipInstance; private TextMeshProUGUI _tooltipTextElement; private RectTransform _tooltipRectTransform; public void Initialize(Toolbar toolbarModel, Transform container) { ToolbarModel = toolbarModel; toolbarContainer = container; // UI 레이아웃: toolbarContainer에 VerticalLayoutGroup 또는 HorizontalLayoutGroup 컴포넌트를 추가하고 // 자식 크기 제어 (Child Force Expand 등) 옵션을 조정하면 UI 요소들이 자동으로 정렬됩니다. // 예: var layoutGroup = toolbarContainer.GetComponent(); // if (layoutGroup == null) layoutGroup = toolbarContainer.gameObject.AddComponent(); // layoutGroup.childControlHeight = true; layoutGroup.childControlWidth = false; // 예시 설정 RenderToolbar(); if (tooltipPrefab != null) { _activeTooltipInstance = Instantiate(tooltipPrefab, transform); // ToolbarView의 자식으로 생성 (Canvas 내 다른 곳이어도 됨) _tooltipTextElement = _activeTooltipInstance.GetComponentInChildren(); _tooltipRectTransform = _activeTooltipInstance.GetComponent(); _activeTooltipInstance.SetActive(false); // 처음에는 숨김 } } private 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); HideTooltip(); // 툴바가 클리어될 때 툴팁도 숨김 } private 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.TooltipKey = buttonModel.TooltipKey; tooltipHandler.OnPointerEnterAction = HandlePointerEnter; tooltipHandler.OnPointerExitAction = HandlePointerExit; } } } } } private void HandlePointerEnter(string tooltipKey, Vector3 mousePosition) { if (LocalizationManager.Instance != null && _tooltipTextElement != null) { string tooltipText = LocalizationManager.Instance.GetString(tooltipKey); if (string.IsNullOrEmpty(tooltipText) || tooltipText == $"[{tooltipKey}]") // 번역 실패 또는 키 그대로 반환 시 { // 번역이 없거나 실패한 경우 툴팁을 표시하지 않거나, 기본 메시지를 표시할 수 있습니다. // 여기서는 표시하지 않도록 합니다. HideTooltip(); return; } ShowTooltip(tooltipText, mousePosition); } } private void HandlePointerExit() { HideTooltip(); } private void ShowTooltip(string text, Vector3 mousePosition) { if (_activeTooltipInstance == null || _tooltipTextElement == null) return; _tooltipTextElement.text = text; _activeTooltipInstance.SetActive(true); // 툴팁 위치 설정 (마우스 커서 기준, 화면 가장자리 넘어가지 않도록 조정 필요) // Canvas Render Mode에 따라 위치 계산 방식이 달라질 수 있습니다. // Screen Space - Overlay 예시: if (_tooltipRectTransform != null) { // TextMeshPro의 preferredWidth/Height를 사용하여 크기 조절 _tooltipTextElement.ForceMeshUpdate(); // 텍스트 변경 후 메시 업데이트 강제 Vector2 textSize = _tooltipTextElement.GetRenderedValues(false); Vector2 padding = new Vector2(10, 5); // 툴팁 내부 여백 _tooltipRectTransform.sizeDelta = textSize + padding * 2; // 화면 가장자리 처리 (간단한 예시) Vector2 localPoint; RectTransformUtility.ScreenPointToLocalPointInRectangle( transform.root as RectTransform, // Canvas의 최상위 RectTransform mousePosition, transform.root.GetComponent().worldCamera, // Screen Space - Camera 경우 필요 out localPoint ); // 툴팁을 마우스 오른쪽 아래에 표시 (오프셋 조정 가능) _tooltipRectTransform.localPosition = localPoint + new Vector2(_tooltipRectTransform.sizeDelta.x * 0.5f + 10f, -_tooltipRectTransform.sizeDelta.y * 0.5f - 5f); // 화면 경계 체크 및 위치 조정 (더 정교한 로직 필요) Vector3[] corners = new Vector3[4]; _tooltipRectTransform.GetWorldCorners(corners); float screenWidth = Screen.width; float screenHeight = Screen.height; // 오른쪽 경계 넘어감 if (corners[2].x > screenWidth) { Vector3 currentPos = _tooltipRectTransform.position; currentPos.x -= (corners[2].x - screenWidth); _tooltipRectTransform.position = currentPos; } // 왼쪽 경계 넘어감 if (corners[0].x < 0) { Vector3 currentPos = _tooltipRectTransform.position; currentPos.x -= corners[0].x; _tooltipRectTransform.position = currentPos; } // 아래쪽 경계 넘어감 (툴팁을 위로 표시하도록 변경 가능) if (corners[0].y < 0) { Vector3 currentPos = _tooltipRectTransform.position; currentPos.y -= corners[0].y; // 위로 올림 // 또는 마우스 위쪽으로 위치 변경 // _tooltipRectTransform.localPosition = localPoint + new Vector2(_tooltipRectTransform.sizeDelta.x * 0.5f + 10f, _tooltipRectTransform.sizeDelta.y * 0.5f + 5f); _tooltipRectTransform.position = currentPos; } // 위쪽 경계 넘어감 if (corners[1].y > screenHeight) { Vector3 currentPos = _tooltipRectTransform.position; currentPos.y -= (corners[1].y - screenHeight); _tooltipRectTransform.position = currentPos; } } } private void HideTooltip() { if (_activeTooltipInstance != null) { _activeTooltipInstance.SetActive(false); } } // 버튼 모델과 게임 오브젝트를 받아 초기 시각적 요소 설정 및 UI 상호작용을 연결합니다. private 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(); // 모델의 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