Files
XRLib/Assets/Scripts/UVC/UI/ToolBar/ToolbarView.cs
2025-06-13 17:10:58 +09:00

454 lines
22 KiB
C#

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<ToolbarButtonBase, GameObject> _modelToGameObjectMap = new Dictionary<ToolbarButtonBase, GameObject>();
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<HorizontalLayoutGroup>();
// if (layoutGroup == null) layoutGroup = toolbarContainer.gameObject.AddComponent<HorizontalLayoutGroup>();
// layoutGroup.childControlHeight = true; layoutGroup.childControlWidth = false; // 예시 설정
RenderToolbar();
if (tooltipPrefab != null)
{
_activeTooltipInstance = Instantiate(tooltipPrefab, transform); // ToolbarView의 자식으로 생성 (Canvas 내 다른 곳이어도 됨)
_tooltipTextElement = _activeTooltipInstance.GetComponentInChildren<TextMeshProUGUI>();
_tooltipRectTransform = _activeTooltipInstance.GetComponent<RectTransform>();
_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<TooltipHandler>();
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<TooltipHandler>();
if (tooltipHandler == null) tooltipHandler = itemObj.AddComponent<TooltipHandler>();
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<Canvas>().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<Toggle>();
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<Toggle>();
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<Button>();
if (uiButton != null)
{
uiButton.onClick.AddListener(() =>
{
expandableModel.ExecuteClick();
ToggleSubMenu(expandableModel, itemObj); // itemObj 전달하여 위치 기준으로 삼기
});
}
}
else if (model is ToolbarStandardButton standardModel)
{
Button uiButton = itemObj.GetComponent<Button>();
if (uiButton != null)
{
uiButton.onClick.AddListener(() =>
{
standardModel.ExecuteClick();
});
}
}
}
// 모델의 상태가 변경되었을 때 호출되어 모든 관련 UI를 업데이트합니다.
private void UpdateItemVisuals(ToolbarButtonBase model)
{
if (_modelToGameObjectMap.TryGetValue(model, out GameObject itemObj))
{
UpdateCommonButtonVisuals(model, itemObj); // 공통 부분 업데이트
// 타입별 특화된 부분 업데이트 (예: Toggle의 isOn 상태)
if (model is ToolbarToggleButton tb) // ToolbarRadioButton도 여기에 해당
{
Toggle toggle = itemObj.GetComponent<Toggle>();
if (toggle != null && toggle.isOn != tb.IsSelected) // UI와 모델 상태가 다를 때만 업데이트
{
toggle.SetIsOnWithoutNotify(tb.IsSelected);
}
}
// 다른 버튼 타입에 대한 추가적인 시각적 업데이트 로직
}
}
// 특정 토글 버튼/라디오 버튼의 IsSelected 상태가 모델에서 변경되었을 때 호출됩니다.
private void UpdateToggleVisuals(ToolbarToggleButton model, bool isSelected)
{
if (_modelToGameObjectMap.TryGetValue(model, out GameObject itemObj))
{
Toggle toggle = itemObj.GetComponent<Toggle>();
if (toggle != null && toggle.isOn != isSelected)
{
toggle.SetIsOnWithoutNotify(isSelected);
}
}
}
// 공통 버튼 시각적 요소(텍스트, 아이콘, 활성화 상태)를 업데이트합니다.
private void UpdateCommonButtonVisuals(ToolbarButtonBase model, GameObject itemObj)
{
// 프리팹 구조에 대한 가정:
// - 텍스트는 TextMeshProUGUI 컴포넌트를 가진 자식 오브젝트에 표시됩니다.
// - 아이콘은 "Icon"이라는 이름의 자식 오브젝트에 Image 컴포넌트로 표시됩니다.
// 이러한 구조는 프로젝트의 프리팹 표준에 맞게 조정해야 합니다.
TextMeshProUGUI buttonTextComponent = itemObj.GetComponentInChildren<TextMeshProUGUI>(true);
if (buttonTextComponent != null)
{
if (LocalizationManager.Instance != null && !string.IsNullOrEmpty(model.Text))
{
// model.Text에는 이제 다국어 키가 저장되어 있습니다.
buttonTextComponent.text = LocalizationManager.Instance.GetString(model.Text);
}
else
{
// LocalizationManager가 없거나 Text(키)가 비어있는 경우, 키를 그대로 표시하거나 기본값 처리
buttonTextComponent.text = model.Text;
}
}
Transform iconTransform = itemObj.transform.Find("Icon"); // 프리팹에 "Icon" 자식 오브젝트가 있다고 가정
if (iconTransform != null)
{
Image buttonIcon = iconTransform.GetComponent<Image>();
if (buttonIcon != null)
{
buttonIcon.sprite = model.Icon;
buttonIcon.gameObject.SetActive(model.Icon != null);
}
}
// 상호작용 가능 상태 업데이트
Selectable selectable = itemObj.GetComponent<Selectable>(); // Button, Toggle 등
if (selectable != null)
{
selectable.interactable = model.IsEnabled;
}
}
private Dictionary<string, ToggleGroup> _toggleGroups = new Dictionary<string, ToggleGroup>();
private ToggleGroup GetOrCreateToggleGroup(string groupName)
{
if (!_toggleGroups.TryGetValue(groupName, out ToggleGroup group))
{
GameObject groupObj = new GameObject($"ToggleGroup_{groupName}");
groupObj.transform.SetParent(toolbarContainer);
group = groupObj.AddComponent<ToggleGroup>();
group.allowSwitchOff = false; // 라디오 버튼 그룹은 일반적으로 하나는 선택되어 있도록 함
_toggleGroups.Add(groupName, group);
}
return group;
}
private GameObject currentSubMenu = null;
// expandableButtonObj는 확장 메뉴의 위치를 잡기 위해 사용될 수 있습니다.
private void ToggleSubMenu(ToolbarExpandableButton expandableButton, GameObject expandableButtonObj)
{
if (currentSubMenu != null)
{
Destroy(currentSubMenu);
currentSubMenu = null;
return;
}
if (subMenuPanelPrefab == null || expandableButton.SubButtons.Count == 0) return;
currentSubMenu = Instantiate(subMenuPanelPrefab, transform); // ToolbarView의 자식으로 생성 후 위치 조정
// 위치 조정 로직: expandableButtonObj의 위치를 기준으로 currentSubMenu의 RectTransform을 조정합니다.
// 예: currentSubMenu.transform.position = expandableButtonObj.transform.position + offset;
RectTransform panelRect = currentSubMenu.GetComponent<RectTransform>();
// 하위 메뉴 패널에 LayoutGroup이 있다면 자식 버튼들이 자동으로 정렬됩니다.
foreach (var subItemBase in expandableButton.SubButtons)
{
if (subItemBase is ToolbarButtonBase subItem) // 모든 하위 아이템은 ToolbarButtonBase라고 가정
{
// 하위 버튼도 적절한 프리팹을 사용해야 합니다. 여기서는 standardButtonPrefab을 예시로 사용합니다.
// 실제로는 subItem의 타입에 따라 다른 프리팹을 선택할 수 있습니다.
GameObject subButtonObj = Instantiate(standardButtonPrefab, panelRect); // 패널의 자식으로 생성
// 하위 버튼의 시각적 요소 설정 및 상호작용 연결
UpdateCommonButtonVisuals(subItem, subButtonObj); // 공통 시각 요소 업데이트
Button subUiButton = subButtonObj.GetComponent<Button>();
if (subUiButton != null)
{
subUiButton.interactable = subItem.IsEnabled; // 상호작용 상태 설정
subUiButton.onClick.AddListener(() =>
{
expandableButton.SelectSubButton(subItem); // 모델 업데이트 및 주 버튼 외형 변경 요청
// 주 버튼 UI는 expandableButton의 OnStateChanged 이벤트에 의해 자동으로 업데이트됩니다.
Destroy(currentSubMenu);
currentSubMenu = null;
});
}
// 하위 버튼 툴팁 처리
if (!string.IsNullOrEmpty(subItem.TooltipKey))
{
TooltipHandler tooltipHandler = subButtonObj.GetComponent<TooltipHandler>();
if (tooltipHandler == null) tooltipHandler = subButtonObj.AddComponent<TooltipHandler>();
tooltipHandler.TooltipKey = subItem.TooltipKey;
tooltipHandler.OnPointerEnterAction = HandlePointerEnter;
tooltipHandler.OnPointerExitAction = HandlePointerExit;
}
// 하위 버튼 모델의 OnStateChanged도 구독하여 하위 버튼 자체의 상태 변경(예: 텍스트)도 반영할 수 있습니다.
// subItem.OnStateChanged += () => UpdateCommonButtonVisuals(subItem, subButtonObj);
// _modelToGameObjectMap에 하위 버튼도 추가하여 ClearToolbar에서 정리되도록 해야 합니다. (선택적 확장)
}
}
}
void OnDestroy()
{
// 씬 전환 등으로 ToolbarView가 파괴될 때 모든 이벤트 구독 해제
ClearToolbar();
if (_activeTooltipInstance != null)
{
Destroy(_activeTooltipInstance); // 툴팁 인스턴스도 파괴
}
}
}
}