toolbar 코드 개선
This commit is contained in:
741
Assets/Scripts/UVC/UI/ToolBar/View/ToolbarView.cs
Normal file
741
Assets/Scripts/UVC/UI/ToolBar/View/ToolbarView.cs
Normal file
@@ -0,0 +1,741 @@
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UVC.Extension;
|
||||
using UVC.Locale;
|
||||
using UVC.UI.Toolbar.Model;
|
||||
using UVC.UI.ToolBar;
|
||||
using UVC.UI.ToolBar.View;
|
||||
using UVC.UI.Tooltip;
|
||||
|
||||
namespace UVC.UI.Toolbar.View
|
||||
{
|
||||
/// <summary>
|
||||
/// ToolbarModel에 정의된 데이터를 기반으로 실제 툴바 UI를 생성하고 관리하는 MonoBehaviour 클래스입니다.
|
||||
/// 사용자의 UI 상호작용을 감지하여 모델의 상태를 변경하거나 커맨드를 실행하고,
|
||||
/// 모델의 상태 변경에 따라 UI를 업데이트합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 주요 역할:
|
||||
/// - 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 등을 할당받아야 합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 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. Toolbar (또는 유사 클래스)에서 ToolbarModel을 설정한 후,
|
||||
/// // ToolbarView의 Initialize 메서드를 호출하여 툴바 UI를 생성합니다.
|
||||
/// // Toolbar controller = GetComponent<Toolbar>(); // 또는 다른 방식으로 참조
|
||||
/// // ToolbarModel model = controller.GetConfiguredModel(); // 가정: 컨트롤러가 모델을 반환
|
||||
/// // Initialize(model);
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class ToolbarView : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// 현재 툴바 UI의 기반이 되는 데이터 모델입니다.
|
||||
/// Initialize 메서드를 통해 외부(주로 Toolbar)에서 주입받습니다.
|
||||
/// </summary>
|
||||
protected ToolbarModel ToolbarModel { get; private set; }
|
||||
|
||||
// --- Inspector에서 할당할 프리팹 및 UI 요소들 ---
|
||||
[Header("UI Prefabs")]
|
||||
/// <summary>
|
||||
/// 표준 버튼 UI에 사용될 프리팹입니다. Inspector를 통해 할당해야 합니다.
|
||||
/// </summary>
|
||||
[Tooltip("표준 버튼 UI에 사용될 프리팹입니다.")]
|
||||
public GameObject standardButtonPrefab;
|
||||
/// <summary>
|
||||
/// 토글 버튼 UI에 사용될 프리팹입니다. Inspector를 통해 할당해야 합니다.
|
||||
/// </summary>
|
||||
[Tooltip("토글 버튼 UI에 사용될 프리팹입니다.")]
|
||||
public GameObject toggleButtonPrefab;
|
||||
/// <summary>
|
||||
/// 라디오 버튼 UI에 사용될 프리팹입니다. Inspector를 통해 할당해야 합니다.
|
||||
/// </summary>
|
||||
[Tooltip("라디오 버튼 UI에 사용될 프리팹입니다.")]
|
||||
public GameObject radioButtonPrefab;
|
||||
/// <summary>
|
||||
/// 확장 가능한 버튼 UI에 사용될 프리팹입니다. Inspector를 통해 할당해야 합니다.
|
||||
/// </summary>
|
||||
[Tooltip("확장 가능한 버튼 UI에 사용될 프리팹입니다.")]
|
||||
public GameObject expandableButtonPrefab;
|
||||
/// <summary>
|
||||
/// 구분선 UI에 사용될 프리팹입니다. Inspector를 통해 할당해야 합니다.
|
||||
/// </summary>
|
||||
[Tooltip("구분선 UI에 사용될 프리팹입니다.")]
|
||||
public GameObject separatorPrefab;
|
||||
/// <summary>
|
||||
/// 확장 버튼의 하위 메뉴 패널 UI에 사용될 프리팹입니다. Inspector를 통해 할당해야 합니다.
|
||||
/// </summary>
|
||||
[Tooltip("확장 버튼의 하위 메뉴 패널 UI에 사용될 프리팹입니다.")]
|
||||
public GameObject subMenuPanelPrefab;
|
||||
|
||||
[Header("UI Layout")]
|
||||
/// <summary>
|
||||
/// 생성된 툴바 항목 UI들이 자식으로 추가될 부모 Transform입니다. Inspector를 통해 할당해야 합니다.
|
||||
/// </summary>
|
||||
[Tooltip("생성된 툴바 항목 UI들이 자식으로 추가될 부모 Transform입니다.")]
|
||||
public Transform toolbarContainer;
|
||||
/// <summary>
|
||||
/// 툴바 항목들의 자동 배치를 담당하는 LayoutGroup 컴포넌트입니다. Inspector를 통해 할당해야 합니다.
|
||||
/// </summary>
|
||||
[Tooltip("툴바 항목들의 자동 배치를 담당하는 LayoutGroup 컴포넌트입니다 (예: VerticalLayoutGroup).")]
|
||||
public LayoutGroup layoutGroup;
|
||||
|
||||
/// <summary>
|
||||
/// 툴바 버튼 모델(ToolbarButtonBase)과 해당 모델을 나타내는 실제 UI GameObject를 매핑하는 딕셔너리입니다.
|
||||
/// 모델 상태 변경 시 해당 GameObject를 찾아 UI를 업데이트하거나, UI 정리 시 사용됩니다.
|
||||
/// </summary>
|
||||
protected Dictionary<ToolbarButtonBase, GameObject> _modelToGameObjectMap = new Dictionary<ToolbarButtonBase, GameObject>();
|
||||
|
||||
/// <summary>
|
||||
/// 버튼 타입별 UI 처리기를 저장하는 딕셔너리입니다.
|
||||
/// </summary>
|
||||
private Dictionary<System.Type, IButtonViewProcessor> _buttonViewProcessors = new Dictionary<System.Type, IButtonViewProcessor>();
|
||||
|
||||
/// <summary>
|
||||
/// 툴팁 표시에 사용될 루트 Canvas입니다.
|
||||
/// Inspector에서 할당하거나, Awake에서 자동으로 찾으려고 시도합니다.
|
||||
/// </summary>
|
||||
protected Canvas rootCanvas;
|
||||
|
||||
/// <summary>
|
||||
/// MonoBehaviour의 Awake 메서드입니다.
|
||||
/// toolbarContainer, layoutGroup, rootCanvas 등의 필수 참조를 초기화하고,
|
||||
/// TooltipManager를 초기화합니다.
|
||||
/// </summary>
|
||||
protected virtual void Awake()
|
||||
{
|
||||
|
||||
if (standardButtonPrefab == null || toggleButtonPrefab == null || radioButtonPrefab == null ||
|
||||
expandableButtonPrefab == null || separatorPrefab == null || subMenuPanelPrefab == null)
|
||||
{
|
||||
Debug.LogError("ToolbarView: 필수 프리팹이 할당되지 않았습니다. Inspector에서 모든 프리팹을 설정해주세요.", this);
|
||||
}
|
||||
|
||||
// toolbarContainer 자동 할당 (Inspector에서 할당되지 않은 경우)
|
||||
if (toolbarContainer == null) toolbarContainer = GetComponent<Transform>();
|
||||
if (toolbarContainer == null) toolbarContainer = GetComponentInChildren<Transform>(true); // 비활성 자식도 포함
|
||||
if (toolbarContainer == null) Debug.LogError("ToolbarView: toolbarContainer가 할당되지 않았습니다. Inspector에서 설정하거나 현재 GameObject에 Transform이 있어야 합니다.", this);
|
||||
|
||||
// layoutGroup 자동 할당 및 추가 (Inspector에서 할당되지 않은 경우)
|
||||
if (layoutGroup == null && toolbarContainer != null)
|
||||
{
|
||||
layoutGroup = toolbarContainer.gameObject.GetComponent<LayoutGroup>();
|
||||
if (layoutGroup == null)
|
||||
{
|
||||
// 기본으로 VerticalLayoutGroup 추가. 필요시 프로젝트에 맞게 수정.
|
||||
layoutGroup = toolbarContainer.gameObject.AddComponent<VerticalLayoutGroup>();
|
||||
Debug.LogWarning("ToolbarView: LayoutGroup이 toolbarContainer에 없어 새로 추가합니다. (기본 VerticalLayoutGroup)", this);
|
||||
}
|
||||
}
|
||||
if (layoutGroup == null) Debug.LogError("ToolbarView: layoutGroup이 할당되지 않았습니다. toolbarContainer에 LayoutGroup 컴포넌트를 추가해주세요.", this);
|
||||
|
||||
|
||||
// rootCanvas 자동 찾기 (TooltipManager 초기화에 필요)
|
||||
if (rootCanvas == null) rootCanvas = GetComponentInParent<Canvas>();
|
||||
if (rootCanvas == null)
|
||||
{
|
||||
Canvas[] canvases = FindObjectsByType<Canvas>(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를 초기화할 수 없습니다.");
|
||||
}
|
||||
|
||||
// 버튼 뷰 처리기 등록
|
||||
_buttonViewProcessors[typeof(ToolbarStandardButton)] = new ToolbarStandardButtonViewProcessor();
|
||||
_buttonViewProcessors[typeof(ToolbarToggleButton)] = new ToolbarToggleButtonViewProcessor();
|
||||
_buttonViewProcessors[typeof(ToolbarRadioButton)] = new ToolbarRadioButtonViewProcessor();
|
||||
_buttonViewProcessors[typeof(ToolbarExpandableButton)] = new ToolbarExpandableButtonViewProcessor();
|
||||
// 새로운 버튼 타입이 추가되면 여기에 처리기를 등록합니다.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 ToolbarModel을 사용하여 툴바 UI를 초기화하고 렌더링합니다.
|
||||
/// 기존 UI가 있다면 정리(ClearToolbar)한 후, 새 모델에 따라 UI를 다시 생성(RenderToolbar)합니다.
|
||||
/// </summary>
|
||||
/// <param name="toolbarModel">화면에 표시할 툴바의 데이터 모델입니다.</param>
|
||||
public virtual void Initialize(ToolbarModel toolbarModel)
|
||||
{
|
||||
this.ToolbarModel = toolbarModel;
|
||||
|
||||
if (standardButtonPrefab == null || toggleButtonPrefab == null || radioButtonPrefab == null ||
|
||||
expandableButtonPrefab == null || separatorPrefab == null || subMenuPanelPrefab == null)
|
||||
{
|
||||
Debug.LogError("ToolbarView: 필수 프리팹이 할당되지 않았습니다. Inspector에서 모든 프리팹을 설정해주세요.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 툴바에 표시된 모든 UI 요소들을 제거하고, 관련된 이벤트 구독을 해제합니다.
|
||||
/// _modelToGameObjectMap, _toggleGroups를 비우고, 열려있는 하위 메뉴(currentSubMenu)도 파괴합니다.
|
||||
/// 툴팁도 숨깁니다.
|
||||
/// </summary>
|
||||
protected virtual void ClearToolbar()
|
||||
{
|
||||
if (_modelToGameObjectMap != null)
|
||||
{
|
||||
foreach (var pair in _modelToGameObjectMap)
|
||||
{
|
||||
if (pair.Key != null) // 모델(버튼)에 연결된 이벤트 구독 해제
|
||||
{
|
||||
// ToolbarButtonBase 및 파생 클래스에 정의된 ClearEventHandlers를 호출하여
|
||||
// 모델에 연결된 모든 이벤트 구독을 명시적으로 해제합니다.
|
||||
pair.Key.ClearEventHandlers();
|
||||
}
|
||||
if (pair.Value != null) // UI GameObject 파괴
|
||||
{
|
||||
TooltipHandler handler = pair.Value.GetComponent<TooltipHandler>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 버튼 모델 타입에 맞는 IButtonViewProcessor를 반환합니다.
|
||||
/// </summary>
|
||||
/// <param name="buttonModelType">버튼 모델의 System.Type 객체입니다.</param>
|
||||
/// <returns>해당 타입의 처리기. 없으면 null을 반환합니다.</returns>
|
||||
protected IButtonViewProcessor GetButtonViewProcessor(System.Type buttonModelType)
|
||||
{
|
||||
if (_buttonViewProcessors.TryGetValue(buttonModelType, out var processor))
|
||||
{
|
||||
return processor;
|
||||
}
|
||||
// 상속 관계를 고려하여 부모 타입의 처리기를 찾을 수도 있습니다.
|
||||
// 예를 들어, MyCustomButton : ToolbarStandardButton 인 경우, ToolbarStandardButton 처리기를 반환할 수 있습니다.
|
||||
// 이 로직은 필요에 따라 확장할 수 있습니다.
|
||||
// 현재는 정확한 타입 매칭만 지원합니다.
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ToolbarModel에 정의된 항목들을 기반으로 실제 UI 요소들을 생성하여 툴바를 구성합니다.
|
||||
/// 먼저 ClearToolbar를 호출하여 기존 UI를 정리한 후, 모델의 각 항목에 대해 적절한 프리팹을 사용하여
|
||||
/// UI GameObject를 생성하고, 필요한 설정(이벤트 바인딩, 툴팁 등)을 수행합니다.
|
||||
/// 버튼 항목의 경우 IButtonViewProcessor 구현체에 UI 생성을 위임합니다.
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
if (itemModel is ToolbarSeparator)
|
||||
{
|
||||
if (separatorPrefab != null) itemUIObject = Instantiate(separatorPrefab, toolbarContainer);
|
||||
else Debug.LogError("ToolbarView: separatorPrefab이 할당되지 않았습니다.", this);
|
||||
}
|
||||
else if (itemModel is ToolbarButtonBase buttonModel)
|
||||
{
|
||||
IButtonViewProcessor processor = GetButtonViewProcessor(buttonModel.GetType());
|
||||
if (processor != null)
|
||||
{
|
||||
itemUIObject = processor.CreateButtonUI(buttonModel, toolbarContainer, this);
|
||||
if (itemUIObject != null)
|
||||
{
|
||||
_modelToGameObjectMap[buttonModel] = itemUIObject;
|
||||
processor.SetupButtonInteractions(buttonModel, itemUIObject, this);
|
||||
|
||||
// 모델 상태 변경 이벤트 구독 (공통)
|
||||
buttonModel.OnStateChanged += () => processor.UpdateCommonButtonVisuals(buttonModel, itemUIObject, this);
|
||||
|
||||
// 토글 버튼/라디오 버튼의 경우 추가적인 이벤트 구독
|
||||
if (buttonModel is ToolbarToggleButton toggleModel) // ToolbarRadioButton도 ToolbarToggleButton을 상속
|
||||
{
|
||||
toggleModel.OnToggleStateChanged += (isSelected) => processor.UpdateToggleStateVisuals(toggleModel, itemUIObject, isSelected, this);
|
||||
}
|
||||
|
||||
// 툴팁 핸들러 추가 및 설정
|
||||
if (!string.IsNullOrEmpty(buttonModel.Tooltip))
|
||||
{
|
||||
TooltipHandler tooltipHandler = itemUIObject.GetComponent<TooltipHandler>();
|
||||
if (tooltipHandler == null) tooltipHandler = itemUIObject.AddComponent<TooltipHandler>();
|
||||
|
||||
tooltipHandler.Tooltip = buttonModel.Tooltip;
|
||||
if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized)
|
||||
{
|
||||
tooltipHandler.OnPointerEnterAction = TooltipManager.Instance.HandlePointerEnter;
|
||||
tooltipHandler.OnPointerExitAction = TooltipManager.Instance.HandlePointerExit;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"ToolbarView: {buttonModel.GetType().Name}의 UI를 생성하지 못했습니다 (Processor 반환 null).", this);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"ToolbarView: {buttonModel.GetType().Name}에 대한 프리팹이 할당되지 않았거나 알 수 없는 버튼 타입입니다.", this);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
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<RectTransform>()); // 즉시 업데이트
|
||||
// 또는 FitToChildren 확장 메서드 사용 (UVC.Extension에 정의되어 있다고 가정)
|
||||
layoutGroup.FitToChildren(width: layoutGroup is VerticalLayoutGroup, height: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 버튼 모델과 UI GameObject를 기반으로 공통적인 시각적 요소(텍스트, 아이콘, 활성화 상태)를 업데이트합니다.
|
||||
/// 이 메서드는 각 IButtonViewProcessor 구현체에서 호출됩니다.
|
||||
/// </summary>
|
||||
/// <param name="model">업데이트할 버튼의 데이터 모델입니다.</param>
|
||||
/// <param name="itemObj">업데이트할 UI GameObject입니다.</param>
|
||||
internal void InternalUpdateCommonButtonVisuals(ToolbarButtonBase model, GameObject itemObj)
|
||||
{
|
||||
if (model == null || itemObj == null) return;
|
||||
|
||||
// 1. 텍스트 업데이트
|
||||
TextMeshProUGUI buttonTextComponent = itemObj.GetComponentInChildren<TextMeshProUGUI>(true);
|
||||
if (buttonTextComponent != null)
|
||||
{
|
||||
string displayText = model.Text;
|
||||
if (LocalizationManager.Instance != null && !string.IsNullOrEmpty(model.Text))
|
||||
{
|
||||
displayText = LocalizationManager.Instance.GetString(model.Text, model.Text);
|
||||
}
|
||||
buttonTextComponent.text = displayText;
|
||||
}
|
||||
|
||||
// 2. 아이콘 업데이트
|
||||
Image iconImageComponent = null;
|
||||
Transform iconTransform = itemObj.transform.Find("Icon");
|
||||
if (iconTransform != null) iconImageComponent = iconTransform.GetComponent<Image>();
|
||||
if (iconImageComponent == null) iconImageComponent = itemObj.GetComponentInChildren<Image>(true);
|
||||
|
||||
if (iconImageComponent != null)
|
||||
{
|
||||
string iconPathToLoad = model.IconSpritePath;
|
||||
if (model is ToolbarToggleButton toggleButton)
|
||||
{
|
||||
iconPathToLoad = toggleButton.IsSelected ? toggleButton.IconSpritePath : toggleButton.OffIconSpritePath;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(iconPathToLoad))
|
||||
{
|
||||
Sprite iconSprite = LoadSpriteFromResources(iconPathToLoad);
|
||||
iconImageComponent.sprite = iconSprite;
|
||||
iconImageComponent.enabled = (iconSprite != null);
|
||||
}
|
||||
else
|
||||
{
|
||||
iconImageComponent.sprite = null;
|
||||
iconImageComponent.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 활성화 상태(interactable) 업데이트
|
||||
Selectable selectableComponent = itemObj.GetComponent<Selectable>();
|
||||
if (selectableComponent != null)
|
||||
{
|
||||
selectableComponent.interactable = model.IsEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
protected Dictionary<string, ToggleGroup> _toggleGroups = new Dictionary<string, ToggleGroup>();
|
||||
/// <summary>
|
||||
/// 라디오 버튼 그룹 이름에 해당하는 UnityEngine.UI.ToggleGroup을 가져오거나, 없으면 새로 생성합니다.
|
||||
/// 생성된 ToggleGroup GameObject는 toolbarContainer의 자식으로 추가되며, 레이아웃에 영향을 주지 않도록 설정됩니다.
|
||||
/// </summary>
|
||||
/// <param name="groupName">찾거나 생성할 토글 그룹의 이름입니다.</param>
|
||||
/// <returns>해당 이름의 ToggleGroup 컴포넌트입니다.</returns>
|
||||
internal ToggleGroup GetOrCreateToggleGroup(string groupName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(groupName))
|
||||
{
|
||||
Debug.LogError("ToolbarView: GetOrCreateToggleGroup - groupName이 null이거나 비어있습니다.", this);
|
||||
// 임시 그룹 또는 null 반환 등의 예외 처리 필요
|
||||
GameObject tempGroupObj = new GameObject($"ToggleGroup_UnnamedDynamic");
|
||||
tempGroupObj.transform.SetParent(toolbarContainer, false);
|
||||
return tempGroupObj.AddComponent<ToggleGroup>();
|
||||
}
|
||||
|
||||
if (!_toggleGroups.TryGetValue(groupName, out ToggleGroup group))
|
||||
{
|
||||
// 새 ToggleGroup GameObject 생성
|
||||
GameObject groupObj = new GameObject($"ToggleGroup_{groupName}");
|
||||
groupObj.transform.SetParent(toolbarContainer);
|
||||
|
||||
// RectTransform 설정 (크기가 0이 되도록 하여 레이아웃에 영향 최소화)
|
||||
RectTransform groupRect = groupObj.AddComponent<RectTransform>();
|
||||
groupRect.sizeDelta = new Vector2(0, 0); // 크기는 필요에 따라 조정
|
||||
groupRect.anchoredPosition = Vector2.zero;
|
||||
groupRect.sizeDelta = Vector2.zero;
|
||||
groupRect.anchorMin = Vector2.zero;
|
||||
groupRect.anchorMax = Vector2.one;
|
||||
|
||||
group = groupObj.AddComponent<ToggleGroup>();
|
||||
group.allowSwitchOff = false; // 라디오 버튼 그룹은 일반적으로 하나는 선택되어 있도록 함
|
||||
|
||||
LayoutElement element = groupObj.AddComponent<LayoutElement>();
|
||||
element.ignoreLayout = true; // 레이아웃 그룹에서 무시하도록 설정
|
||||
|
||||
_toggleGroups.Add(groupName, group);
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 열려있는 하위 메뉴 UI GameObject입니다. 없으면 null입니다.
|
||||
/// </summary>
|
||||
protected GameObject currentSubMenu = null;
|
||||
|
||||
/// <summary>
|
||||
/// 확장 가능한 버튼(ToolbarExpandableButton)에 대한 하위 메뉴 UI를 토글합니다.
|
||||
/// 이미 열려있으면 닫고, 닫혀있으면 엽니다.
|
||||
/// 하위 메뉴는 subMenuPanelPrefab을 사용하여 생성되며, expandableButtonObj의 위치를 기준으로 표시됩니다.
|
||||
/// </summary>
|
||||
/// <param name="expandableButtonModel">하위 버튼 목록을 가진 확장 버튼 모델입니다.</param>
|
||||
/// <param name="expandableButtonObj">주 확장 버튼의 UI GameObject로, 하위 메뉴 위치 결정에 사용됩니다.</param>
|
||||
internal void ToggleSubMenu(ToolbarExpandableButton expandableButtonModel, GameObject expandableButtonObj)
|
||||
{
|
||||
// 이미 다른 하위 메뉴가 열려있거나, 현재 클릭한 버튼의 하위 메뉴가 열려있다면 닫습니다.
|
||||
if (currentSubMenu != null)
|
||||
{
|
||||
Destroy(currentSubMenu);
|
||||
currentSubMenu = null;
|
||||
// 만약 현재 클릭한 버튼의 하위 메뉴가 이미 열려있어서 닫는 경우라면, 여기서 함수 종료
|
||||
// (다시 열리지 않도록). 이 로직은 currentSubMenu가 어떤 버튼에 의해 열렸는지 알아야 함.
|
||||
// 간단하게는, 어떤 확장 버튼이든 클릭하면 기존 서브메뉴는 닫고, 필요하면 새로 연다.
|
||||
// 지금은 그냥 닫고, 아래 로직에서 새로 열 조건이 되면 열도록 함.
|
||||
// 만약 클릭한 버튼이 이미 열린 메뉴의 주인이면, 그냥 닫기만 하고 리턴하는게 자연스러울 수 있음.
|
||||
// 이 부분은 UI/UX 정책에 따라 상세 조정 필요. 현재는 "토글"이므로, 다시 누르면 닫히고, 없으면 연다.
|
||||
// 즉, 아래 로직에서 currentSubMenu가 null이 되었으므로, 다시 열릴 수 있음.
|
||||
// 만약 "닫기만" 하고 싶다면, 여기서 return 해야 함.
|
||||
// 현재 로직: 이미 열려있으면 무조건 닫고, 아래에서 다시 열지 결정.
|
||||
// 수정 제안: 만약 currentSubMenu가 이 expandableButtonModel에 의해 열린 것이라면, 닫고 return.
|
||||
// (이를 위해서는 currentSubMenu가 어떤 모델에 연결되었는지 정보가 필요)
|
||||
// 현재는 간단히 토글로 동작: 열려있으면 닫고, 닫혀있으면 연다.
|
||||
// 즉, if (currentSubMenu != null) { Destroy; currentSubMenu = null; return; } 이면 닫기만 하고 끝.
|
||||
// 지금은 닫고, 아래에서 다시 열 수 있게 함.
|
||||
}
|
||||
|
||||
// 새 하위 메뉴를 열 조건 확인 (닫혀 있었고, 프리팹과 하위 버튼이 있는 경우)
|
||||
if (subMenuPanelPrefab == null)
|
||||
{
|
||||
Debug.LogError("ToolbarView: subMenuPanelPrefab이 할당되지 않아 하위 메뉴를 열 수 없습니다.", this);
|
||||
return;
|
||||
}
|
||||
if (expandableButtonModel.SubButtons == null || expandableButtonModel.SubButtons.Count == 0)
|
||||
{
|
||||
// Debug.LogWarning($"ToolbarView: 확장 버튼 '{expandableButtonModel.Text}'에 하위 버튼이 없어 메뉴를 열지 않습니다.", this);
|
||||
return; // 하위 버튼 없으면 메뉴 열지 않음
|
||||
}
|
||||
|
||||
// 하위 메뉴 패널 UI 생성
|
||||
// 생성 위치는 일단 ToolbarView의 자식으로 하고, 이후 위치 조정
|
||||
currentSubMenu = Instantiate(subMenuPanelPrefab, transform);
|
||||
currentSubMenu.name = $"SubMenu_{expandableButtonModel.Text}";
|
||||
|
||||
RectTransform panelRect = currentSubMenu.GetComponent<RectTransform>();
|
||||
if (panelRect == null)
|
||||
{
|
||||
Debug.LogError("ToolbarView: subMenuPanelPrefab에 RectTransform이 없습니다.", currentSubMenu);
|
||||
Destroy(currentSubMenu); currentSubMenu = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 하위 메뉴 패널 위치 설정 (확장 버튼 UI 오른쪽 또는 아래 등)
|
||||
// 이 로직은 툴바의 방향(수평/수직)과 UI 디자인에 따라 매우 달라질 수 있습니다.
|
||||
// 여기서는 간단한 예시로, 확장 버튼의 오른쪽에 표시한다고 가정합니다.
|
||||
RectTransform expandableButtonRect = expandableButtonObj.GetComponent<RectTransform>();
|
||||
|
||||
// panelRect의 앵커 및 피벗 설정 (예: 좌상단 기준)
|
||||
panelRect.anchorMin = new Vector2(0, 1);
|
||||
panelRect.anchorMax = new Vector2(0, 1);
|
||||
panelRect.pivot = new Vector2(0, 1); // 좌상단 피벗
|
||||
currentSubMenu.SetActive(true);
|
||||
|
||||
// 부모 내에서 가장 마지막 자식으로 만들어 최상단에 표시되도록 합니다.
|
||||
if (panelRect.parent != null) panelRect.SetAsLastSibling();
|
||||
|
||||
LayoutGroup subMenuLayoutGroup = currentSubMenu.GetComponent<LayoutGroup>();
|
||||
if (subMenuLayoutGroup == null)
|
||||
{
|
||||
// 하위 메뉴 패널에 LayoutGroup이 없다면 추가합니다.
|
||||
subMenuLayoutGroup = currentSubMenu.AddComponent<VerticalLayoutGroup>();
|
||||
}
|
||||
|
||||
// 패널 위치를 확장 버튼의 위치에 맞춥니다.
|
||||
Vector2 offset = new Vector2(expandableButtonRect.anchoredPosition.x, expandableButtonRect.anchoredPosition.y);
|
||||
if (layoutGroup is VerticalLayoutGroup)
|
||||
{
|
||||
offset.x += expandableButtonRect.rect.width; // 수직 레이아웃인 경우 버튼 오른쪽에 표시 되도록
|
||||
}
|
||||
else
|
||||
{
|
||||
if (subMenuLayoutGroup != null) offset.x -= subMenuLayoutGroup.padding.left; // 수평 레이아웃인 경우 패딩을 고려
|
||||
offset.y -= expandableButtonRect.rect.height; // 수평 레이아웃인 경우 버튼 아래에 표시 되도록
|
||||
}
|
||||
panelRect.anchoredPosition = offset; // 위치 조정
|
||||
|
||||
// 하위 버튼들 생성. 하위 메뉴 패널에 LayoutGroup이 있다면 자식 버튼들이 자동으로 정렬됩니다.
|
||||
foreach (var subItemBase in expandableButtonModel.SubButtons)
|
||||
{
|
||||
if (subItemBase is ToolbarButtonBase subItemModel) // 모든 하위 아이템은 ToolbarButtonBase라고 가정
|
||||
{
|
||||
IButtonViewProcessor subProcessor = GetButtonViewProcessor(subItemModel.GetType());
|
||||
// 하위 버튼도 적절한 프리팹을 사용해야 합니다.
|
||||
// 여기서는 모든 하위 버튼이 standardButtonPrefab을 사용한다고 가정합니다.
|
||||
// 실제로는 subItemModel의 타입에 따라 다른 프리팹을 선택할 수 있습니다.
|
||||
GameObject subButtonPrefabToUse = standardButtonPrefab; // 기본값
|
||||
|
||||
if (subProcessor != null) // 하위 버튼도 Processor를 통해 생성 시도
|
||||
{
|
||||
if (subItemModel is ToolbarToggleButton) subButtonPrefabToUse = toggleButtonPrefab;
|
||||
else subButtonPrefabToUse = standardButtonPrefab; // 기본은 표준 버튼
|
||||
|
||||
// ... 다른 타입에 대한 처리 ...
|
||||
if (subButtonPrefabToUse == null)
|
||||
{
|
||||
Debug.LogError($"ToolbarView: 하위 버튼 '{subItemModel.Text}'에 대한 프리팹을 결정할 수 없습니다 (standardButtonPrefab 사용 시도).", this);
|
||||
continue;
|
||||
}
|
||||
|
||||
GameObject subButtonObj = Instantiate(subButtonPrefabToUse, panelRect); // 하위 메뉴 패널의 자식으로 생성
|
||||
subButtonObj.name = $"SubItem_{subItemModel.Text}";
|
||||
|
||||
// 하위 버튼의 시각적 요소 설정 및 상호작용 연결
|
||||
InternalUpdateCommonButtonVisuals(subItemModel, subButtonObj); // 공통 UI 업데이트
|
||||
|
||||
// 하위 버튼 클릭 이벤트 연결
|
||||
Button subUiButton = subButtonObj.GetComponent<Button>(); // 또는 Toggle 등 타입에 맞는 컴포넌트
|
||||
Toggle subUiToggle = subButtonObj.GetComponent<Toggle>();
|
||||
if (subUiButton != null)
|
||||
{
|
||||
subUiButton.interactable = subItemModel.IsEnabled; // 상호작용 상태 설정
|
||||
subUiButton.onClick.AddListener(() =>
|
||||
{
|
||||
// 1. 주 확장 버튼 모델에 하위 버튼 선택 알림 (주 버튼 모양 업데이트 등)
|
||||
// SelectSubButton의 두 번째 파라미터(GameObject)는 제거되었으므로, 모델만 전달
|
||||
expandableButtonModel.SelectSubButton(subItemModel); // 모델 업데이트 및 주 버튼 외형 변경 요청
|
||||
|
||||
// 2. 선택된 하위 버튼 자체의 커맨드 실행
|
||||
subItemModel.ExecuteClick(subItemModel.Text); // 하위 버튼의 Command 실행
|
||||
|
||||
// 3. 하위 메뉴 닫기
|
||||
Destroy(currentSubMenu);
|
||||
currentSubMenu = null;
|
||||
|
||||
// 4. 툴팁 숨기기 (하위 메뉴가 닫힐 때 관련 툴팁도 숨김)
|
||||
if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized) TooltipManager.Instance.HandlePointerExit();
|
||||
});
|
||||
}
|
||||
else if (subUiToggle != null && subItemModel is ToolbarToggleButton subToggleModel) // 하위 아이템이 토글 버튼인 경우
|
||||
{
|
||||
subUiToggle.interactable = subToggleModel.IsEnabled;
|
||||
subUiToggle.SetIsOnWithoutNotify(subToggleModel.IsSelected);
|
||||
subUiToggle.onValueChanged.AddListener((isSelected) =>
|
||||
{
|
||||
// 하위 토글 버튼은 보통 클릭 시 바로 액션 실행 및 메뉴 닫힘
|
||||
expandableButtonModel.SelectSubButton(subToggleModel); // 주 버튼 모양 업데이트
|
||||
subToggleModel.ExecuteClick(); // 하위 버튼 커맨드 실행 (내부에서 IsSelected 변경)
|
||||
Destroy(currentSubMenu);
|
||||
currentSubMenu = null;
|
||||
if (TooltipManager.Instance != null) TooltipManager.Instance.HideTooltip();
|
||||
});
|
||||
}
|
||||
|
||||
// 하위 버튼 툴팁 처리
|
||||
if (!string.IsNullOrEmpty(subItemModel.Tooltip))
|
||||
{
|
||||
TooltipHandler tooltipHandler = subButtonObj.GetComponent<TooltipHandler>();
|
||||
if (tooltipHandler == null) tooltipHandler = subButtonObj.AddComponent<TooltipHandler>();
|
||||
|
||||
tooltipHandler.Tooltip = subItemModel.Tooltip;
|
||||
tooltipHandler.OnPointerEnterAction = TooltipManager.Instance.HandlePointerEnter;
|
||||
tooltipHandler.OnPointerExitAction = TooltipManager.Instance.HandlePointerExit;
|
||||
}
|
||||
|
||||
// 하위 버튼 모델의 상태 변경도 구독하여 UI 업데이트 (선택적 확장)
|
||||
// subItemModel.OnStateChanged += () => UpdateCommonButtonVisuals(subItemModel, subButtonObj);
|
||||
// 이 경우, _modelToGameObjectMap에 하위 버튼도 추가하고 ClearToolbar에서 정리해야 함.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 하위 메뉴 레이아웃 그룹 크기 맞춤
|
||||
if (subMenuLayoutGroup != null)
|
||||
{
|
||||
subMenuLayoutGroup.FitToChildren(); // UVC.Extension 필요 가정
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 경로에서 Sprite를 로드합니다. Sprite 파일은 Resources 폴더 또는 그 하위 폴더에 있어야 합니다.
|
||||
/// </summary>
|
||||
/// <param name="spritePath">Resources 폴더 기준의 Sprite 경로입니다 (확장자 제외).</param>
|
||||
/// <returns>로드된 Sprite 객체. 실패 시 null을 반환합니다.</returns>
|
||||
protected Sprite LoadSpriteFromResources(string spritePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(spritePath))
|
||||
{
|
||||
Debug.LogWarning("LoadSpriteFromResources: spritePath가 null이거나 비어있습니다.");
|
||||
return null;
|
||||
}
|
||||
Sprite loadedSprite = Resources.Load<Sprite>(spritePath);
|
||||
if (loadedSprite == null)
|
||||
{
|
||||
// 경로에 파일이 없을 경우 오류 로깅. 게임 빌드 시 Resources 폴더 내용이 포함되는지 확인 필요.
|
||||
Debug.LogError($"ToolbarView: Resources 폴더에서 '{spritePath}' 경로의 Sprite를 로드할 수 없습니다. 경로 및 파일 존재 여부를 확인하세요.", this);
|
||||
}
|
||||
return loadedSprite;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MonoBehaviour의 Update 메서드입니다. 매 프레임 호출됩니다.
|
||||
/// 여기서는 열려있는 하위 메뉴(currentSubMenu) 외부를 클릭했을 때 메뉴를 닫는 로직을 처리합니다.
|
||||
/// </summary>
|
||||
protected virtual void Update()
|
||||
{
|
||||
// 마우스 왼쪽 버튼이 클릭되었고, 현재 열려있는 하위 메뉴가 있으며, 루트 캔버스가 존재하는 경우
|
||||
if (Input.GetMouseButtonDown(0) && currentSubMenu != null && currentSubMenu.activeSelf && rootCanvas != null)
|
||||
{
|
||||
RectTransform subMenuRect = currentSubMenu.GetComponent<RectTransform>();
|
||||
if (subMenuRect == null) return; // 하위 메뉴에 RectTransform이 없는 경우 (비정상)
|
||||
|
||||
// 마우스 포인터가 하위 메뉴 영역(RectTransform) 내에 있는지 확인
|
||||
// 이를 위해 RectTransformUtility.RectangleContainsScreenPoint 사용
|
||||
Camera eventCamera = null; // 스크린 스페이스 오버레이 캔버스인 경우 null
|
||||
if (rootCanvas.renderMode == RenderMode.ScreenSpaceCamera || rootCanvas.renderMode == RenderMode.WorldSpace)
|
||||
{
|
||||
eventCamera = rootCanvas.worldCamera; // 캔버스에 할당된 카메라 사용
|
||||
}
|
||||
|
||||
// 마우스 포인터가 하위 메뉴 영역 밖에 있는지 확인
|
||||
if (!RectTransformUtility.RectangleContainsScreenPoint(subMenuRect, Input.mousePosition, eventCamera))
|
||||
{
|
||||
// 하위 메뉴 영역 바깥을 클릭한 경우, 하위 메뉴를 닫습니다.
|
||||
Destroy(currentSubMenu);
|
||||
currentSubMenu = null;
|
||||
if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized) TooltipManager.Instance.HideTooltip();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MonoBehaviour의 OnDestroy 메서드입니다. 이 컴포넌트 또는 GameObject가 파괴될 때 호출됩니다.
|
||||
/// 툴바 UI를 정리(ClearToolbar)하고, TooltipManager 등의 리소스를 해제(Dispose)합니다.
|
||||
/// </summary>
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
ClearToolbar(); // 모든 UI 요소 및 이벤트 구독 해제
|
||||
|
||||
// TooltipManager가 싱글톤이고 다른 곳에서도 사용된다면, 여기서 Dispose 호출은 신중해야 합니다.
|
||||
// 만약 이 ToolbarView가 TooltipManager의 유일한 사용자이거나,
|
||||
// 씬 전환 등으로 모든 관련 UI가 사라지는 상황이라면 적절할 수 있습니다.
|
||||
// 현재 TooltipManager.Instance.Dispose()는 TooltipVisualManager의 내용을 정리하는 것으로 보입니다.
|
||||
if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized)
|
||||
{
|
||||
// TooltipManager.Instance.Dispose(); // 주석 처리된 기존 코드. 필요시 활성화.
|
||||
// Dispose의 구체적인 내용에 따라 호출 여부 결정.
|
||||
// 만약 TooltipManager가 전역적이고 계속 사용되어야 한다면, 여기서 Dispose하면 안됨.
|
||||
// HideTooltip 정도가 적당할 수 있음.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user