toolbar 코드 개선

This commit is contained in:
김형인
2025-06-18 00:16:49 +09:00
parent 078fc6df4c
commit e897c911bb
60 changed files with 816 additions and 547 deletions

View File

@@ -0,0 +1,51 @@
using UnityEngine;
using UVC.UI.Toolbar.Model;
using UVC.UI.Toolbar.View;
namespace UVC.UI.ToolBar.View
{
/// <summary>
/// 툴바 버튼의 UI 생성, 설정 및 업데이트 로직을 처리하는 인터페이스입니다.
/// 각 버튼 타입(표준, 토글, 라디오, 확장 등)에 대한 구체적인 UI 처리 클래스가 이 인터페이스를 구현합니다.
/// </summary>
public interface IButtonViewProcessor
{
/// <summary>
/// 지정된 버튼 모델에 대한 UI GameObject를 생성하고 초기화합니다.
/// </summary>
/// <param name="buttonModel">UI를 생성할 버튼의 데이터 모델입니다.</param>
/// <param name="parentContainer">생성된 UI GameObject가 자식으로 추가될 부모 Transform입니다.</param>
/// <param name="viewContext">현재 ToolbarView의 컨텍스트입니다. 프리팹 참조, 헬퍼 메서드 접근 등에 사용됩니다.</param>
/// <returns>생성된 UI GameObject입니다. 생성 실패 시 null을 반환할 수 있습니다.</returns>
GameObject CreateButtonUI(ToolbarButtonBase buttonModel, Transform parentContainer, ToolbarView viewContext);
/// <summary>
/// 생성된 버튼 UI GameObject에 대해 초기 시각적 요소(텍스트, 아이콘 등)를 설정하고,
/// 사용자 상호작용(예: 클릭 이벤트) 시 모델의 메서드가 호출되도록 이벤트를 바인딩합니다.
/// 또한, 모델의 상태 변경 이벤트(OnStateChanged, OnToggleStateChanged 등)를 구독하여 UI가 업데이트되도록 설정합니다.
/// </summary>
/// <param name="buttonModel">설정할 버튼의 데이터 모델입니다.</param>
/// <param name="buttonUIObject">모델에 해당하는, 화면에 표시될 UI GameObject입니다.</param>
/// <param name="viewContext">현재 ToolbarView의 컨텍스트입니다.</param>
void SetupButtonInteractions(ToolbarButtonBase buttonModel, GameObject buttonUIObject, ToolbarView viewContext);
/// <summary>
/// 버튼 모델의 공통적인 상태(텍스트, 아이콘, 활성화 상태 등)가 변경되었을 때,
/// 해당 버튼 UI의 시각적 요소를 업데이트합니다.
/// </summary>
/// <param name="buttonModel">상태가 변경된 버튼의 모델입니다.</param>
/// <param name="buttonUIObject">업데이트할 UI GameObject입니다.</param>
/// <param name="viewContext">현재 ToolbarView의 컨텍스트입니다.</param>
void UpdateCommonButtonVisuals(ToolbarButtonBase buttonModel, GameObject buttonUIObject, ToolbarView viewContext);
/// <summary>
/// 토글 가능한 버튼(ToolbarToggleButton, ToolbarRadioButton)의 선택 상태(IsSelected)가 모델에서 변경되었을 때 호출됩니다.
/// 해당 UI Toggle 컴포넌트의 isOn 상태를 모델의 IsSelected 값과 동기화합니다.
/// </summary>
/// <param name="toggleButtonModel">IsSelected 상태가 변경된 토글/라디오 버튼의 모델입니다.</param>
/// <param name="buttonUIObject">업데이트할 UI GameObject입니다.</param>
/// <param name="isSelected">모델의 새로운 IsSelected 값입니다.</param>
/// <param name="viewContext">현재 ToolbarView의 컨텍스트입니다.</param>
void UpdateToggleStateVisuals(ToolbarToggleButton toggleButtonModel, GameObject buttonUIObject, bool isSelected, ToolbarView viewContext);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 813fbe357e2233d49beaf93864c6bfd8

View File

@@ -0,0 +1,51 @@
using UnityEngine;
using UnityEngine.UI;
using UVC.UI.Toolbar.Model;
using UVC.UI.ToolBar.View;
namespace UVC.UI.Toolbar.View
{
public class ToolbarExpandableButtonViewProcessor : IButtonViewProcessor
{
public GameObject CreateButtonUI(ToolbarButtonBase buttonModel, Transform parentContainer, ToolbarView viewContext)
{
if (viewContext.expandableButtonPrefab == null)
{
Debug.LogError("ExpandableButtonViewProcessor: expandableButtonPrefab이 ToolbarView에 할당되지 않았습니다.", viewContext);
return null;
}
return Object.Instantiate(viewContext.expandableButtonPrefab, parentContainer);
}
public void SetupButtonInteractions(ToolbarButtonBase buttonModel, GameObject buttonUIObject, ToolbarView viewContext)
{
ToolbarExpandableButton expandableModel = buttonModel as ToolbarExpandableButton;
if (expandableModel == null) return;
Button uiButton = buttonUIObject.GetComponent<Button>();
if (uiButton != null)
{
uiButton.onClick.AddListener(() =>
{
expandableModel.ExecuteClick();
viewContext.ToggleSubMenu(expandableModel, buttonUIObject);
});
}
else
{
Debug.LogError($"ExpandableButtonViewProcessor: ExpandableButton '{expandableModel.Text}'의 GameObject에 Button 컴포넌트가 없습니다.", buttonUIObject);
}
UpdateCommonButtonVisuals(buttonModel, buttonUIObject, viewContext);
}
public void UpdateCommonButtonVisuals(ToolbarButtonBase buttonModel, GameObject buttonUIObject, ToolbarView viewContext)
{
viewContext.InternalUpdateCommonButtonVisuals(buttonModel, buttonUIObject);
}
public void UpdateToggleStateVisuals(ToolbarToggleButton toggleButtonModel, GameObject buttonUIObject, bool isSelected, ToolbarView viewContext)
{
// 확장 버튼은 기본 토글 상태가 없음 (하위 메뉴 표시 여부는 다른 메커니즘으로 관리)
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 91d4340b73cb5a84d91986ba644d398f

View File

@@ -0,0 +1,70 @@
using UnityEngine;
using UnityEngine.UI;
using UVC.UI.Toolbar.Model;
using UVC.UI.ToolBar.View;
namespace UVC.UI.Toolbar.View
{
public class ToolbarRadioButtonViewProcessor : IButtonViewProcessor
{
public GameObject CreateButtonUI(ToolbarButtonBase buttonModel, Transform parentContainer, ToolbarView viewContext)
{
if (viewContext.radioButtonPrefab == null)
{
Debug.LogError("RadioButtonViewProcessor: radioButtonPrefab이 ToolbarView에 할당되지 않았습니다.", viewContext);
return null;
}
return Object.Instantiate(viewContext.radioButtonPrefab, parentContainer);
}
public void SetupButtonInteractions(ToolbarButtonBase buttonModel, GameObject buttonUIObject, ToolbarView viewContext)
{
ToolbarRadioButton radioModel = buttonModel as ToolbarRadioButton;
if (radioModel == null) return;
Toggle toggleComponent = buttonUIObject.GetComponent<Toggle>();
if (toggleComponent != null)
{
ToggleGroup uiToggleGroup = viewContext.GetOrCreateToggleGroup(radioModel.GroupName);
toggleComponent.group = uiToggleGroup;
toggleComponent.SetIsOnWithoutNotify(radioModel.IsSelected);
toggleComponent.onValueChanged.AddListener((isSelected) =>
{
if (isSelected)
{
radioModel.ExecuteClick(true);
}
});
}
else
{
Debug.LogError($"RadioButtonViewProcessor: RadioButton '{radioModel.Text}'의 GameObject에 Toggle 컴포넌트가 없습니다.", buttonUIObject);
}
UpdateCommonButtonVisuals(buttonModel, buttonUIObject, viewContext);
UpdateToggleStateVisuals(radioModel, buttonUIObject, radioModel.IsSelected, viewContext);
}
public void UpdateCommonButtonVisuals(ToolbarButtonBase buttonModel, GameObject buttonUIObject, ToolbarView viewContext)
{
viewContext.InternalUpdateCommonButtonVisuals(buttonModel, buttonUIObject);
}
public void UpdateToggleStateVisuals(ToolbarToggleButton toggleButtonModel, GameObject buttonUIObject, bool isSelected, ToolbarView viewContext)
{
// ToolbarToggleButton 타입으로 캐스팅 시도
ToolbarRadioButton radioModel = toggleButtonModel as ToolbarRadioButton;
if (radioModel == null) return;
Toggle toggleComponent = buttonUIObject.GetComponent<Toggle>();
if (toggleComponent != null)
{
if (toggleComponent.isOn != isSelected)
{
toggleComponent.SetIsOnWithoutNotify(isSelected);
}
}
viewContext.InternalUpdateCommonButtonVisuals(radioModel, buttonUIObject); // 아이콘 업데이트를 위해 호출
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1e8beeab960cbd549ad03e44b366896e

View File

@@ -0,0 +1,54 @@
using UnityEngine;
using UnityEngine.UI;
using UVC.UI.Toolbar.Model;
using UVC.UI.ToolBar.View;
namespace UVC.UI.Toolbar.View
{
public class ToolbarStandardButtonViewProcessor : IButtonViewProcessor
{
public GameObject CreateButtonUI(ToolbarButtonBase buttonModel, Transform parentContainer, ToolbarView viewContext)
{
if (viewContext.standardButtonPrefab == null)
{
Debug.LogError("StandardButtonViewProcessor: standardButtonPrefab이 ToolbarView에 할당되지 않았습니다.", viewContext);
return null;
}
return Object.Instantiate(viewContext.standardButtonPrefab, parentContainer);
}
public void SetupButtonInteractions(ToolbarButtonBase buttonModel, GameObject buttonUIObject, ToolbarView viewContext)
{
ToolbarStandardButton standardModel = buttonModel as ToolbarStandardButton;
if (standardModel == null) return;
Button uiButton = buttonUIObject.GetComponent<Button>();
if (uiButton != null)
{
uiButton.onClick.AddListener(() =>
{
standardModel.ExecuteClick(standardModel.Text);
});
}
else
{
Debug.LogError($"StandardButtonViewProcessor: StandardButton '{standardModel.Text}'의 GameObject에 Button 컴포넌트가 없습니다.", buttonUIObject);
}
// 공통적인 모델 상태 변경 구독은 ToolbarView의 RenderToolbar에서 처리하거나,
// 각 Processor가 viewContext를 통해 UpdateCommonButtonVisuals 등을 직접 호출하도록 할 수 있습니다.
// 여기서는 ToolbarView에서 공통 구독을 처리한다고 가정합니다.
UpdateCommonButtonVisuals(buttonModel, buttonUIObject, viewContext);
}
public void UpdateCommonButtonVisuals(ToolbarButtonBase buttonModel, GameObject buttonUIObject, ToolbarView viewContext)
{
viewContext.InternalUpdateCommonButtonVisuals(buttonModel, buttonUIObject);
}
public void UpdateToggleStateVisuals(ToolbarToggleButton toggleButtonModel, GameObject buttonUIObject, bool isSelected, ToolbarView viewContext)
{
// 표준 버튼은 토글 상태가 없음
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3a22df99b306444429b5c67ec0e8d46b

View File

@@ -0,0 +1,60 @@
using UnityEngine;
using UnityEngine.UI;
using UVC.UI.Toolbar.Model;
using UVC.UI.ToolBar.View;
namespace UVC.UI.Toolbar.View
{
public class ToolbarToggleButtonViewProcessor : IButtonViewProcessor
{
public GameObject CreateButtonUI(ToolbarButtonBase buttonModel, Transform parentContainer, ToolbarView viewContext)
{
if (viewContext.toggleButtonPrefab == null)
{
Debug.LogError("ToggleButtonViewProcessor: toggleButtonPrefab이 ToolbarView에 할당되지 않았습니다.", viewContext);
return null;
}
return Object.Instantiate(viewContext.toggleButtonPrefab, parentContainer);
}
public void SetupButtonInteractions(ToolbarButtonBase buttonModel, GameObject buttonUIObject, ToolbarView viewContext)
{
ToolbarToggleButton toggleModel = buttonModel as ToolbarToggleButton;
if (toggleModel == null) return;
Toggle toggleComponent = buttonUIObject.GetComponent<Toggle>();
if (toggleComponent != null)
{
toggleComponent.SetIsOnWithoutNotify(toggleModel.IsSelected);
toggleComponent.onValueChanged.AddListener((isSelected) =>
{
toggleModel.ExecuteClick();
});
}
else
{
Debug.LogError($"ToggleButtonViewProcessor: ToggleButton '{toggleModel.Text}'의 GameObject에 Toggle 컴포넌트가 없습니다.", buttonUIObject);
}
UpdateCommonButtonVisuals(buttonModel, buttonUIObject, viewContext);
UpdateToggleStateVisuals(toggleModel, buttonUIObject, toggleModel.IsSelected, viewContext);
}
public void UpdateCommonButtonVisuals(ToolbarButtonBase buttonModel, GameObject buttonUIObject, ToolbarView viewContext)
{
viewContext.InternalUpdateCommonButtonVisuals(buttonModel, buttonUIObject);
}
public void UpdateToggleStateVisuals(ToolbarToggleButton toggleButtonModel, GameObject buttonUIObject, bool isSelected, ToolbarView viewContext)
{
Toggle toggleComponent = buttonUIObject.GetComponent<Toggle>();
if (toggleComponent != null)
{
if (toggleComponent.isOn != isSelected)
{
toggleComponent.SetIsOnWithoutNotify(isSelected);
}
}
viewContext.InternalUpdateCommonButtonVisuals(toggleButtonModel, buttonUIObject); // 아이콘 업데이트를 위해 호출
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d7a0ee99f42c57745b899b45fd7dad30

View 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 정도가 적당할 수 있음.
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2eb6cd37a2c94ca48b031d1a9c160e46