TopMenu 개발 완료
This commit is contained in:
8
Assets/Scripts/UVC/Editor.meta
Normal file
8
Assets/Scripts/UVC/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5d9214dc67e05d429fe8fbdad351b1a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -417,7 +417,6 @@ namespace UVC.Extension
|
||||
float preferredHeight = LayoutUtility.GetPreferredHeight(rectTransform);
|
||||
vector.y = preferredHeight;
|
||||
}
|
||||
|
||||
rectTransform.sizeDelta = vector;
|
||||
}
|
||||
else
|
||||
|
||||
@@ -152,7 +152,7 @@ namespace UVC.UI.Menu
|
||||
view.ClearMenuItems();
|
||||
// View에 현재 모델 데이터를 기반으로 메뉴 UI를 생성하도록 요청
|
||||
// view.MenuContainer는 TopMenuView에서 메뉴 UI 요소들이 배치될 부모 Transform을 가리킵니다.
|
||||
view.CreateMenuItems(model.MenuItems, view.MenuContainer);
|
||||
view.CreateMenuItems(model.MenuItems, view.menuContainer);
|
||||
|
||||
// View에서 발생하는 메뉴 아이템 클릭 이벤트에 대한 핸들러 등록
|
||||
view.OnMenuItemClicked += HandleMenuItemClicked;
|
||||
@@ -220,6 +220,10 @@ namespace UVC.UI.Menu
|
||||
new MenuItemData("edit_undo", "menu_edit_undo", new DebugLogCommand("실행 취소 선택됨 (Command 실행)")),
|
||||
new MenuItemData("edit_redo", "menu_edit_redo", new DebugLogCommand("다시 실행 선택됨 (Command 실행)")),
|
||||
MenuItemData.CreateSeparator("edit_sep1"), // 구분선
|
||||
new MenuItemData("file_save", "menu_file_save", command: new DebugLogCommand("저장 선택됨 (Command 실행)") , subMenuItems: new List<MenuItemData>
|
||||
{
|
||||
new MenuItemData("file_save_as", "menu_file_save_as", new DebugLogCommand("다른 이름으로 저장 선택됨 (Command 실행)"))
|
||||
}),
|
||||
new MenuItemData("preferences", "menu_preferences", new DebugLogCommand("환경설정 선택됨 (Command 실행)"))
|
||||
}));
|
||||
|
||||
@@ -227,7 +231,11 @@ namespace UVC.UI.Menu
|
||||
{
|
||||
// 각 언어 메뉴 아이템에 ChangeLanguageCommand를 연결하여 언어 변경 기능 수행
|
||||
new MenuItemData("lang_ko", "menu_lang_korean", new ChangeLanguageCommand("ko-KR"), commandParameter: "ko-KR"),
|
||||
new MenuItemData("lang_en", "menu_lang_english", new ChangeLanguageCommand("en-US"))
|
||||
new MenuItemData("lang_en", "menu_lang_english", new ChangeLanguageCommand("en-US")),
|
||||
new MenuItemData("file_save", "menu_file_save", command: new DebugLogCommand("저장 선택됨 (Command 실행)") , subMenuItems: new List<MenuItemData>
|
||||
{
|
||||
new MenuItemData("file_save_as", "menu_file_save_as", new DebugLogCommand("다른 이름으로 저장 선택됨 (Command 실행)"))
|
||||
}),
|
||||
// 필요에 따라 다른 언어들도 추가 가능
|
||||
}));
|
||||
}
|
||||
@@ -245,7 +253,7 @@ namespace UVC.UI.Menu
|
||||
{
|
||||
if (!clickedItemData.IsEnabled)
|
||||
{
|
||||
Debug.Log($"비활성화된 메뉴 아이템 클릭 시도: {clickedItemData.ItemId} (표시 키: {clickedItemData.DisplayNameKey})");
|
||||
Debug.Log($"비활성화된 메뉴 아이템 클릭 시도: {clickedItemData.ItemId} (표시 키: {clickedItemData.DisplayName})");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -253,12 +261,12 @@ namespace UVC.UI.Menu
|
||||
if (!clickedItemData.IsEnabled)
|
||||
{
|
||||
// 비활성화된 아이템 클릭 시 로그 (디버깅 목적)
|
||||
ULog.Debug($"비활성화된 메뉴 아이템 클릭 시도: {clickedItemData.ItemId} (표시 키: {clickedItemData.DisplayNameKey})");
|
||||
ULog.Debug($"비활성화된 메뉴 아이템 클릭 시도: {clickedItemData.ItemId} (표시 키: {clickedItemData.DisplayName})");
|
||||
return; // 비활성화된 아이템은 명령을 실행하지 않음
|
||||
}
|
||||
|
||||
// 클릭된 메뉴 아이템 정보 로그 (디버깅 목적)
|
||||
ULog.Debug($"메뉴 아이템 클릭됨: {clickedItemData.ItemId} (표시 키: {clickedItemData.DisplayNameKey})");
|
||||
ULog.Debug($"메뉴 아이템 클릭됨: {clickedItemData.ItemId} (표시 키: {clickedItemData.DisplayName})");
|
||||
|
||||
// 메뉴 아이템에 연결된 Command가 있다면 실행
|
||||
// Command가 null일 수 있으므로 null 조건부 연산자(?.) 사용
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UVC.UI.Commands;
|
||||
|
||||
@@ -19,7 +21,7 @@ namespace UVC.UI.Menu
|
||||
/// UI에 표시될 메뉴 아이템의 이름입니다. 다국어 키도 가능합니다.
|
||||
/// 이 키를 사용하여 실제 표시될 텍스트를 가져옵니다.
|
||||
/// </summary>
|
||||
public string DisplayNameKey { get; private set; }
|
||||
public string DisplayName { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 메뉴 아이템이 선택되었을 때 실행될 명령입니다.
|
||||
@@ -53,6 +55,26 @@ namespace UVC.UI.Menu
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; }
|
||||
|
||||
private int depth = 0; // 메뉴 아이템의 깊이 (하위 메뉴의 레벨)
|
||||
/// <summary>
|
||||
/// 계층 구조에서 현재 개체의 깊이를 가져옵니다.
|
||||
/// </summary>
|
||||
public int Depth
|
||||
{
|
||||
get { return depth; } // 외부에서 접근할 수 있도록 프로퍼티로 노출
|
||||
internal set { depth = value; }
|
||||
}
|
||||
|
||||
private MenuItemData? parent = null;
|
||||
/// <summary>
|
||||
/// 현재 메뉴 항목의 부모 메뉴 항목을 가져옵니다.
|
||||
/// </summary>
|
||||
public MenuItemData? Parent
|
||||
{
|
||||
get { return parent; } // 외부에서 접근할 수 있도록 프로퍼티로 노출
|
||||
internal set { parent = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// `MenuItemData` 클래스의 새 인스턴스를 초기화합니다.
|
||||
/// </summary>
|
||||
@@ -63,15 +85,38 @@ namespace UVC.UI.Menu
|
||||
/// <param name="subMenuItems">하위 메뉴 아이템 목록 (선택 사항).</param>
|
||||
/// <param name="isSeparator">구분선 여부 (선택 사항, 기본값: false).</param>
|
||||
/// <param name="isEnabled">활성화 상태 (선택 사항, 기본값: true).</param>
|
||||
public MenuItemData(string itemId, string displayName, ICommand command = null, object commandParameter = null, List<MenuItemData> subMenuItems = null, bool isSeparator = false, bool isEnabled = true)
|
||||
public MenuItemData(string itemId, string displayName, ICommand command = null, object commandParameter = null, List<MenuItemData> subMenuItems = null, bool isSeparator = false, bool isEnabled = true, int depth = 0)
|
||||
{
|
||||
ItemId = itemId;
|
||||
DisplayNameKey = displayName;
|
||||
DisplayName = displayName;
|
||||
Command = command;
|
||||
CommandParameter = commandParameter; // 파라미터 저장
|
||||
SubMenuItems = subMenuItems ?? new List<MenuItemData>(); // null인 경우 빈 리스트로 초기화
|
||||
IsSeparator = isSeparator;
|
||||
IsEnabled = isEnabled;
|
||||
this.depth = depth; // 메뉴 아이템의 깊이 설정
|
||||
|
||||
SetupDepthAndParent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 하위 메뉴 항목의 깊이와 부모 관계를 구성합니다.
|
||||
/// </summary>
|
||||
/// <remarks>이 메서드는 하위 메뉴 항목 컬렉션을 반복하며 깊이와
|
||||
/// 부모 속성을 설정합니다. 깊이는 현재 항목의 깊이에 따라 증가하고, 부모는
|
||||
/// 현재 항목으로 설정됩니다. 하위 메뉴 항목에 자체 하위 메뉴 항목이 포함된 경우, 이 메서드는 재귀적으로 호출되어
|
||||
/// 깊이와 부모 관계를 구성합니다.</remarks>
|
||||
protected void SetupDepthAndParent()
|
||||
{
|
||||
for (int i = 0; i < SubMenuItems.Count; i++)
|
||||
{
|
||||
SubMenuItems[i].Depth = this.depth + 1; // 하위 메뉴 아이템의 깊이를 현재 아이템의 깊이 + 1로 설정
|
||||
SubMenuItems[i].Parent = this; // 하위 메뉴 아이템의 부모를 현재 아이템으로 설정
|
||||
if (SubMenuItems[i].SubMenuItems.Count > 0)
|
||||
{
|
||||
SubMenuItems[i].SetupDepthAndParent(); // 재귀적으로 하위 메뉴 아이템의 깊이 설정
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -82,6 +127,8 @@ namespace UVC.UI.Menu
|
||||
public void AddSubMenuItem(MenuItemData subItem)
|
||||
{
|
||||
if (IsSeparator) return; // 구분선에는 하위 메뉴를 추가할 수 없음
|
||||
subItem.Depth = this.depth + 1; // 하위 메뉴 아이템의 깊이를 현재 아이템의 깊이 + 1로 설정
|
||||
subItem.Parent = this; // 하위 메뉴 아이템의 부모를 현재 아이템으로 설정
|
||||
SubMenuItems.Add(subItem);
|
||||
}
|
||||
|
||||
@@ -92,9 +139,20 @@ namespace UVC.UI.Menu
|
||||
/// <returns>구분선 역할을 하는 새로운 `MenuItemData` 객체입니다.</returns>
|
||||
public static MenuItemData CreateSeparator(string itemId = null)
|
||||
{
|
||||
// 구분선은 특정 동작이나 표시 이름이 필요 없으므로, displayNameKey는 비워두고 command는 null로 설정합니다.
|
||||
// 구분선은 특정 동작이나 표시 이름이 필요 없으므로, displayName는 비워두고 command는 null로 설정합니다.
|
||||
return new MenuItemData(itemId ?? $"separator_{Guid.NewGuid()}", string.Empty, null, null, null, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 메뉴 항목에 연관된 하위 메뉴 항목이 있는지 여부를 확인합니다.
|
||||
/// </summary>
|
||||
/// <param name="ItemId">확인할 메뉴 항목의 고유 식별자입니다.</param>
|
||||
/// 지정된 메뉴 항목에 대한 하위 메뉴 항목이 있으면 <see langword="true"/>를 반환하고, 그렇지 않으면 <see langword="false"/>를 반환합니다. </returns>
|
||||
public bool HasSubMenuItems(string ItemId)
|
||||
{
|
||||
return SubMenuItems.Exists(item => item.ItemId == ItemId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class TopMenuModel
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
using System;
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UVC.Extension;
|
||||
using UVC.Locale; // 다국어 처리 네임스페이스
|
||||
using UVC.Log;
|
||||
|
||||
namespace UVC.UI.Menu
|
||||
{
|
||||
@@ -22,16 +25,6 @@ namespace UVC.UI.Menu
|
||||
/// public class CustomTopMenuView : TopMenuView
|
||||
/// {
|
||||
/// /// <summary>
|
||||
/// /// 커스텀 메뉴 아이템 프리팹의 Resources 폴더 내 경로입니다.
|
||||
/// /// </summary>
|
||||
/// protected override string MenuItemPrefabPath => "Prefabs/UI/Menu/CustomMenuItem"; // 다른 프리팹 경로로 변경
|
||||
///
|
||||
/// /// <summary>
|
||||
/// /// 커스텀 하위 메뉴 아이템 프리팹의 Resources 폴더 내 경로입니다.
|
||||
/// /// </summary>
|
||||
/// protected override string SubMenuItemPrefabPath => "Prefabs/UI/Menu/CustomSubMenuItem"; // 다른 프리팹 경로로 변경
|
||||
///
|
||||
/// /// <summary>
|
||||
/// /// MonoBehaviour의 Awake 메시지입니다.
|
||||
/// /// 부모 클래스의 Awake를 호출한 후, 추가적인 커스텀 초기화 로직을 수행합니다.
|
||||
/// /// </summary>
|
||||
@@ -43,34 +36,6 @@ namespace UVC.UI.Menu
|
||||
///
|
||||
/// // 여기에 CustomTopMenuView만의 추가적인 초기화 코드를 작성할 수 있습니다.
|
||||
/// ULog.Debug("CustomTopMenuView Awake 실행됨. 커스텀 프리팹 경로가 사용됩니다.");
|
||||
///
|
||||
/// // 예: 특정 조건에 따라 메뉴 아이템 간 간격을 변경
|
||||
/// // this.menuItemSpace = new Vector2(40, 5);
|
||||
/// }
|
||||
///
|
||||
/// /// <summary>
|
||||
/// /// UI 요소(메뉴 아이템 또는 구분선)의 위치와 크기를 설정합니다.
|
||||
/// /// 이 예제에서는 기본 레이아웃 로직을 그대로 사용하지만,
|
||||
/// /// 필요에 따라 완전히 새로운 레이아웃 로직을 구현할 수 있습니다.
|
||||
/// /// 예를 들어, 1차 메뉴 아이템들을 수직으로 정렬하도록 변경할 수 있습니다.
|
||||
/// /// </summary>
|
||||
/// protected override void LayoutMenuItem(GameObject uiElement, MenuItemData itemData, int depth, int itemIndex, List<MenuItemData> siblingItems)
|
||||
/// {
|
||||
/// // 기본 레이아웃 로직을 그대로 사용하려면 base.LayoutMenuItem을 호출합니다.
|
||||
/// base.LayoutMenuItem(uiElement, itemData, depth, itemIndex, siblingItems);
|
||||
///
|
||||
/// // 여기에 추가적인 레이아웃 조정 로직을 넣을 수 있습니다.
|
||||
/// // 예: 특정 아이템 ID를 가진 메뉴만 특별한 스타일을 적용 (색상 변경 등 - 여기서는 RectTransform만 다룸)
|
||||
/// if (itemData.ItemId == "special_menu_item")
|
||||
/// {
|
||||
/// RectTransform rt = uiElement.GetComponent<RectTransform>();
|
||||
/// if (rt != null)
|
||||
/// {
|
||||
/// // 예시: 특정 아이템의 높이를 약간 늘림 (실제로는 크기 변경 시 다른 아이템 위치도 재조정 필요)
|
||||
/// // rt.sizeDelta = new Vector2(rt.sizeDelta.x, rt.sizeDelta.y + 10);
|
||||
/// ULog.Debug($"Special item '{itemData.ItemId}' layout customized.");
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// /// <summary>
|
||||
@@ -82,7 +47,7 @@ namespace UVC.UI.Menu
|
||||
/// /// </summary>
|
||||
/// protected override void ToggleSubMenuDisplay(MenuItemData itemData, GameObject menuItemInstance)
|
||||
/// {
|
||||
/// ULog.Debug($"Custom ToggleSubMenuDisplay: {itemData.DisplayNameKey} 클릭됨.");
|
||||
/// ULog.Debug($"Custom ToggleSubMenuDisplay: {itemData.DisplayName} 클릭됨.");
|
||||
/// base.ToggleSubMenuDisplay(itemData, menuItemInstance); // 부모의 원래 로직 실행
|
||||
/// }
|
||||
///
|
||||
@@ -93,70 +58,39 @@ namespace UVC.UI.Menu
|
||||
/// </example>
|
||||
public class TopMenuView : MonoBehaviour
|
||||
{
|
||||
// --- 프리팹 경로 및 UI 요소 이름 ---
|
||||
// 메뉴 아이템, 하위 메뉴 아이템, 구분선 프리팹의 Resources 폴더 내 경로입니다.
|
||||
// 상속 클래스에서 이 경로들을 변경하여 다른 프리팹을 사용할 수 있습니다.
|
||||
protected string menuItemPrefabPath = "Prefabs/UI/Menu/MenuItem";
|
||||
protected string subMenuItemPrefabPath = "Prefabs/UI/Menu/SubMenuItem";
|
||||
protected string menuSeparatorPrefabPath = "Prefabs/UI/Menu/MenuSeparator";
|
||||
[Header("UI References")]
|
||||
[Tooltip("메인 메뉴 아이템을 위한 프리팹입니다.")]
|
||||
[SerializeField] public GameObject menuItemPrefab;
|
||||
|
||||
// 메뉴 UI 요소들을 찾기 위한 이름들입니다.
|
||||
// 프리팹의 계층 구조나 이름이 다를 경우, 상속 클래스에서 이 값들을 변경해야 합니다.
|
||||
protected string menuContainerName = "TopMenu"; // 최상위 메뉴 아이템들이 배치될 부모 GameObject의 이름
|
||||
protected string subMenuContainerName = "SubMenuContainer"; // 하위 메뉴 아이템들이 배치될 부모 GameObject의 이름 (각 메뉴 아이템 내부에 존재)
|
||||
protected string subMenuArrowName = "SubMenuArrow"; // 하위 메뉴가 있음을 나타내는 화살표 UI GameObject의 이름 (각 메뉴 아이템 내부에 존재)
|
||||
[Tooltip("하위 메뉴 아이템을 위한 프리팹입니다.")]
|
||||
[SerializeField] public GameObject subMenuItemPrefab;
|
||||
|
||||
/// <summary>
|
||||
/// 기본 메뉴 아이템 프리팹의 Resources 폴더 내 경로입니다.
|
||||
/// 상속 클래스에서 오버라이드하여 다른 경로를 지정할 수 있습니다.
|
||||
/// </summary>
|
||||
protected virtual string MenuItemPrefabPath => menuItemPrefabPath;
|
||||
/// <summary>
|
||||
/// 하위 메뉴 아이템 프리팹의 Resources 폴더 내 경로입니다.
|
||||
/// 상속 클래스에서 오버라이드하여 다른 경로를 지정할 수 있습니다.
|
||||
/// </summary>
|
||||
protected virtual string SubMenuItemPrefabPath => subMenuItemPrefabPath;
|
||||
/// <summary>
|
||||
/// 메뉴 구분선 프리팹의 Resources 폴더 내 경로입니다.
|
||||
/// 상속 클래스에서 오버라이드하여 다른 경로를 지정할 수 있습니다.
|
||||
/// </summary>
|
||||
protected virtual string MenuSeparatorPrefabPath => menuSeparatorPrefabPath;
|
||||
/// <summary>
|
||||
/// 최상위 메뉴 아이템들이 배치될 컨테이너 GameObject의 이름입니다.
|
||||
/// </summary>
|
||||
protected virtual string MenuContainerName => menuContainerName;
|
||||
/// <summary>
|
||||
/// 하위 메뉴 아이템들이 배치될 컨테이너 GameObject의 이름입니다. (각 메뉴 아이템 프리팹 내부에 있어야 함)
|
||||
/// </summary>
|
||||
protected virtual string SubMenuContainerName => subMenuContainerName;
|
||||
/// <summary>
|
||||
/// 하위 메뉴 존재를 나타내는 화살표 UI GameObject의 이름입니다. (각 메뉴 아이템 프리팹 내부에 있어야 함)
|
||||
/// </summary>
|
||||
protected virtual string SubMenuArrowName => subMenuArrowName;
|
||||
[Tooltip("메뉴 구분선을 위한 프리팹입니다. (선택 사항)")]
|
||||
[SerializeField] public GameObject menuSeparatorPrefab;
|
||||
|
||||
// --- 로드된 프리팹 및 UI 참조 ---
|
||||
protected GameObject menuItemPrefab; // 로드된 기본 메뉴 아이템 프리팹
|
||||
protected GameObject subMenuItemPrefab; // 로드된 하위 메뉴 아이템 프리팹
|
||||
protected GameObject menuSeparatorPrefab; // 로드된 메뉴 구분선 프리팹
|
||||
protected Transform menuContainer; // 메뉴 아이템들이 실제로 배치될 부모 Transform
|
||||
[Tooltip("최상위 메뉴 아이템들이 배치될 부모 Transform입니다.")]
|
||||
[SerializeField] public Transform menuContainer;
|
||||
|
||||
// --- UI 레이아웃 관련 설정 값 ---
|
||||
// 이 값들은 프리팹에서 자동으로 읽어오려고 시도하지만, 실패 시 또는 커스텀 로직에서 사용될 수 있습니다.
|
||||
// 상속 클래스에서 이 값들을 직접 변경하거나, Awake()에서 프리팹으로부터 다시 읽어오도록 수정할 수 있습니다.
|
||||
protected Vector2 menuItemInitPosition = new Vector2(0, 0); // 1depth 메뉴 아이템의 초기 위치 (부모 컨테이너 기준)
|
||||
protected Vector2 menuItemSize = new Vector2(100, 30); // 기본 메뉴 아이템의 크기
|
||||
protected Vector2 subMenuItemSize = new Vector2(100, 30); // 하위 메뉴 아이템의 크기
|
||||
protected Vector2 menuItemSeparatorSize = new Vector2(100, 1); // 메뉴 구분선의 크기
|
||||
protected Vector4 menuItemSeparatorMargin = new Vector4(0, 4, 0, 4); // 구분선의 좌, 상, 우, 하 마진 (주로 상하 마진 사용)
|
||||
protected Vector2 menuItemSpace = new Vector2(10, 2); // 메뉴 아이템 간의 간격 (수평, 수직)
|
||||
protected Vector2 menuDepthSpace = new Vector2(5, 10); // 1차 메뉴 아이템과 그 하위 메뉴 컨테이너 간의 간격 (수평, 수직)
|
||||
protected Vector2 subMenuDepthSpace = new Vector2(5, 10); // 하위 메뉴 아이템과 그 하위 메뉴 컨테이너 간의 간격 (수평, 수직)
|
||||
protected Vector4 subContainerPadding = new Vector4(4, 10, 4, 10); // 하위 메뉴 컨테이너 내부의 좌, 상, 우, 하 패딩
|
||||
[Tooltip("하위 메뉴 아이템들이 배치될 부모 Transform입니다.")]
|
||||
[SerializeField] public Transform subMenuContainer;
|
||||
|
||||
/// <summary>
|
||||
/// 메뉴 아이템들이 배치되는 부모 Transform 입니다.
|
||||
/// </summary>
|
||||
public Transform MenuContainer { get => menuContainer; }
|
||||
[Header("UI Element Names")]
|
||||
|
||||
[Tooltip("하위 메뉴가 있음을 나타내는 화살표 UI GameObject의 이름입니다. (메뉴 아이템 프리팹 내부에 존재)")]
|
||||
[SerializeField] public string subMenuArrowName = "SubMenuArrow";
|
||||
|
||||
[Header("Layout Settings")]
|
||||
[Tooltip("1차 메뉴 아이템과 그 하위 메뉴 컨테이너 간의 간격 (수평, 수직)")]
|
||||
[SerializeField] public Vector2 menuDepthSpace = new Vector2(0, -5);
|
||||
|
||||
[Tooltip("하위 메뉴 아이템과 그 하위 메뉴 컨테이너 간의 간격 (수평, 수직)")]
|
||||
[SerializeField] public Vector2 subMenuDepthSpace = new Vector2(-5, 0);
|
||||
|
||||
[Tooltip("1 depth 메뉴를 넓이를 글자 크기로 줄일지 여부")]
|
||||
[SerializeField] public bool isShrinkMenuItemWidth = true;
|
||||
|
||||
[Tooltip("isShrinkMenuItemWidth가 활성화됐을 때, 텍스트 좌우에 추가될 여백입니다.")]
|
||||
[SerializeField] public float menuItemWidthPadding = 20f;
|
||||
|
||||
/// <summary>
|
||||
/// 메뉴 아이템이 클릭되었을 때 발생하는 이벤트입니다.
|
||||
@@ -167,6 +101,8 @@ namespace UVC.UI.Menu
|
||||
// --- 내부 상태 변수 ---
|
||||
// 생성된 메뉴 아이템 GameObject들을 관리하는 딕셔너리 (키: ItemId, 값: GameObject)
|
||||
protected Dictionary<string, GameObject> _menuItemObjects = new Dictionary<string, GameObject>();
|
||||
protected Dictionary<string, GameObject> _subMenuContainerObjects = new Dictionary<string, GameObject>();
|
||||
protected Dictionary<string, MenuItemData> _menuItemDataMap = new Dictionary<string, MenuItemData>();
|
||||
protected LocalizationManager _locManager; // 다국어 처리를 위한 LocalizationManager 인스턴스
|
||||
protected GameObject uiBlockerInstance; // 하위 메뉴가 열렸을 때 다른 UI 상호작용을 막기 위한 투명한 UI 요소
|
||||
protected bool isAnySubMenuOpen = false; // 하나 이상의 하위 메뉴가 열려있는지 여부
|
||||
@@ -184,67 +120,37 @@ namespace UVC.UI.Menu
|
||||
{
|
||||
// LocalizationManager가 필수적인 경우, 여기서 에러를 발생시키거나 게임을 중단시킬 수 있습니다.
|
||||
// 여기서는 경고만 기록하고 진행합니다.
|
||||
ULog.Error("LocalizationManager 인스턴스를 찾을 수 없습니다. 메뉴 텍스트가 제대로 표시되지 않을 수 있습니다.", new InvalidOperationException("LocalizationManager 인스턴스를 찾을 수 없습니다."));
|
||||
Debug.LogError("LocalizationManager 인스턴스를 찾을 수 없습니다. 메뉴 텍스트가 제대로 표시되지 않을 수 있습니다.");
|
||||
}
|
||||
|
||||
// 메뉴 아이템 프리팹 로드 및 크기 정보 추출
|
||||
menuItemPrefab = Resources.Load<GameObject>(MenuItemPrefabPath);
|
||||
if (menuItemPrefab != null)
|
||||
// Inspector에서 할당된 참조 확인
|
||||
if (menuItemPrefab == null)
|
||||
{
|
||||
RectTransform rt = menuItemPrefab.GetComponent<RectTransform>();
|
||||
if (rt != null) menuItemSize = rt.sizeDelta;
|
||||
else ULog.Error($"메뉴 아이템 프리팹 '{MenuItemPrefabPath}'에 RectTransform 컴포넌트가 없습니다. 기본 크기 값을 사용합니다.");
|
||||
Debug.LogError("menuItemPrefab이 Inspector에서 할당되지 않았습니다.", this);
|
||||
}
|
||||
else
|
||||
if (subMenuItemPrefab == null)
|
||||
{
|
||||
ULog.Error($"메뉴 아이템 프리팹을 Resources 폴더에서 로드할 수 없습니다. 경로: {MenuItemPrefabPath}", new System.IO.FileNotFoundException($"Prefab not found at Resources/{MenuItemPrefabPath}"));
|
||||
Debug.LogError("subMenuItemPrefab이 Inspector에서 할당되지 않았습니다.", this);
|
||||
}
|
||||
|
||||
// 하위 메뉴 아이템 프리팹 로드 및 크기 정보 추출
|
||||
subMenuItemPrefab = Resources.Load<GameObject>(SubMenuItemPrefabPath);
|
||||
if (subMenuItemPrefab != null)
|
||||
{
|
||||
RectTransform rt = subMenuItemPrefab.GetComponent<RectTransform>();
|
||||
if (rt != null) subMenuItemSize = rt.sizeDelta;
|
||||
else ULog.Error($"서브 메뉴 아이템 프리팹 '{SubMenuItemPrefabPath}'에 RectTransform 컴포넌트가 없습니다. 기본 크기 값을 사용합니다.");
|
||||
}
|
||||
else
|
||||
{
|
||||
ULog.Error($"서브 메뉴 아이템 프리팹을 Resources 폴더에서 로드할 수 없습니다. 경로: {SubMenuItemPrefabPath}", new System.IO.FileNotFoundException($"Prefab not found at Resources/{SubMenuItemPrefabPath}"));
|
||||
}
|
||||
|
||||
// 메뉴 구분선 프리팹 로드 및 크기 정보 추출
|
||||
menuSeparatorPrefab = Resources.Load<GameObject>(MenuSeparatorPrefabPath);
|
||||
if (menuSeparatorPrefab != null)
|
||||
{
|
||||
RectTransform rt = menuSeparatorPrefab.GetComponent<RectTransform>();
|
||||
if (rt != null) menuItemSeparatorSize = rt.sizeDelta;
|
||||
else ULog.Error($"메뉴 구분선 프리팹 '{MenuSeparatorPrefabPath}'에 RectTransform 컴포넌트가 없습니다. 기본 크기 값을 사용합니다.");
|
||||
}
|
||||
else
|
||||
if (menuSeparatorPrefab == null)
|
||||
{
|
||||
// 구분선은 선택 사항일 수 있으므로, 경고 수준으로 처리합니다.
|
||||
ULog.Warning($"메뉴 구분선 프리팹을 Resources 폴더에서 로드할 수 없습니다. 경로: {MenuSeparatorPrefabPath}. 구분선 기능이 작동하지 않습니다.");
|
||||
Debug.LogWarning("menuSeparatorPrefab이 Inspector에서 할당되지 않았습니다. 구분선 기능이 작동하지 않습니다.", this);
|
||||
}
|
||||
|
||||
// 메뉴 컨테이너 Transform 찾기
|
||||
Transform containerTransform = transform.Find(MenuContainerName);
|
||||
if (containerTransform != null)
|
||||
// 메뉴 컨테이너 확인
|
||||
if (menuContainer == null)
|
||||
{
|
||||
menuContainer = containerTransform;
|
||||
Debug.LogError("menuContainer가 Inspector에서 할당되지 않았습니다. Inspector에서 참조를 설정해주세요.", this);
|
||||
}
|
||||
else
|
||||
{
|
||||
// MenuContainerName으로 지정된 자식 GameObject를 찾지 못한 경우, 현재 GameObject의 Transform을 사용합니다.
|
||||
// 이는 TopMenuView GameObject 자체가 메뉴 아이템들의 직접적인 부모가 되는 경우를 의미합니다.
|
||||
ULog.Warning($"'{MenuContainerName}'이라는 이름의 자식 GameObject를 찾을 수 없어 현재 Transform을 menuContainer로 사용합니다. 의도된 동작이 아니라면 확인해주세요.");
|
||||
menuContainer = transform;
|
||||
}
|
||||
if (menuContainer == null)
|
||||
{
|
||||
// menuContainer를 어떤 방식으로도 설정할 수 없는 경우, 에러를 기록합니다.
|
||||
// 이는 메뉴 시스템이 정상적으로 작동할 수 없음을 의미합니다.
|
||||
ULog.Error("menuContainer를 설정할 수 없습니다. 메뉴 아이템이 표시되지 않습니다.", new InvalidOperationException("menuContainer could not be set."));
|
||||
// 메뉴 컨테이너에 LayoutGroup이 있는지 확인하고, 없다면 경고를 표시합니다.
|
||||
// 자동 배치를 위해 HorizontalLayoutGroup 또는 VerticalLayoutGroup이 필요합니다.
|
||||
if (menuContainer.GetComponent<LayoutGroup>() == null)
|
||||
{
|
||||
Debug.LogWarning($"menuContainer '{menuContainer.name}'에 LayoutGroup 컴포넌트가 없습니다. 메뉴 아이템이 자동으로 배치되지 않을 수 있습니다. Inspector에서 HorizontalLayoutGroup 또는 VerticalLayoutGroup을 추가해주세요.");
|
||||
}
|
||||
}
|
||||
|
||||
// UI 블로커 생성
|
||||
@@ -267,7 +173,7 @@ namespace UVC.UI.Menu
|
||||
if (blockerParent == null) // 부모를 찾지 못한 극단적인 경우, TopMenuView 자신을 부모로 설정 (권장되지 않음)
|
||||
{
|
||||
blockerParent = transform;
|
||||
ULog.Warning("TopMenuUIBlocker의 부모를 TopMenuView 자신으로 설정합니다. Canvas 하위에 배치하는 것을 권장합니다.");
|
||||
Debug.LogWarning("TopMenuUIBlocker의 부모를 TopMenuView 자신으로 설정합니다. Canvas 하위에 배치하는 것을 권장합니다.");
|
||||
}
|
||||
uiBlockerInstance.transform.SetParent(blockerParent, false);
|
||||
|
||||
@@ -299,9 +205,7 @@ namespace UVC.UI.Menu
|
||||
// menuContainer 바로 뒤에 오도록 (SiblingIndex가 작을수록 먼저 그려짐 - 뒤에 위치)
|
||||
// 블로커가 메뉴보다 먼저 그려져야 메뉴를 가리지 않으면서 메뉴 외 영역 클릭을 막을 수 있음.
|
||||
// 하지만 UI 이벤트는 앞쪽에 있는 것부터 받으므로, 블로커가 메뉴보다 뒤에 있어야 함.
|
||||
// 따라서, 블로커는 메뉴보다 낮은 SiblingIndex - 1를 가져야 함.
|
||||
// 여기서는 메뉴 컨테이너 바로 뒤에 오도록 설정 (블로커가 렌더링된 후 메뉴가 렌더링되어 메뉴를 덮지 않도록)
|
||||
// 혼동을 피하기 위해, 블로커는 메뉴 시스템 전체보다 뒤에 위치해야 클릭을 가로챌 수 있습니다.
|
||||
// 따라서, 블로커는 메뉴 시스템 전체보다 뒤에 위치해야 클릭을 가로챌 수 있습니다.
|
||||
// CreateUIBlocker 호출 시점에는 menuContainer의 sibling index가 확정되지 않았을 수 있으므로,
|
||||
// UpdateBlockerVisibility에서 최종적으로 조정하는 것이 더 안전할 수 있습니다.
|
||||
// 여기서는 일단 menuContainer의 뒤에 두도록 시도합니다.
|
||||
@@ -327,42 +231,35 @@ namespace UVC.UI.Menu
|
||||
/// <param name="items">생성할 메뉴 아이템들의 데이터 리스트입니다.</param>
|
||||
/// <param name="parentContainer">생성된 메뉴 아이템들이 자식으로 추가될 부모 Transform입니다.</param>
|
||||
/// <param name="depth">현재 메뉴의 깊이입니다. 최상위 메뉴는 0입니다.</param>
|
||||
/// <returns>생성된 메뉴 아이템들이 차지하는 전체 영역의 크기 (너비, 높이)를 반환합니다.</returns>
|
||||
public virtual Vector2 CreateMenuItems(List<MenuItemData> items, Transform parentContainer, int depth = 0)
|
||||
public virtual void CreateMenuItems(List<MenuItemData> items, Transform parentContainer, int depth = 0)
|
||||
{
|
||||
if (items == null || parentContainer == null)
|
||||
{
|
||||
ULog.Error("메뉴 아이템 생성에 필요한 'items' 또는 'parentContainer'가 null입니다.", new ArgumentNullException(items == null ? "items" : "parentContainer"));
|
||||
return Vector2.zero; // 오류 발생 시 크기 0 반환
|
||||
Debug.LogError("메뉴 아이템 생성에 필요한 'items' 또는 'parentContainer'가 null입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 현재 레벨(깊이)에서 메뉴 아이템들이 차지할 총 너비와 높이를 계산하기 위한 변수
|
||||
float currentLevelCalculatedWidth = 0;
|
||||
float currentLevelCalculatedHeight = 0;
|
||||
|
||||
// 아이템이 없고, 하위 메뉴를 그리는 경우가 아니라면 (depth > 0), 크기는 0
|
||||
if (items.Count == 0 && depth > 0)
|
||||
{
|
||||
return Vector2.zero;
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
MenuItemData itemData = items[i];
|
||||
GameObject instanceToLayout = null; // 레이아웃을 적용할 GameObject (메뉴 아이템 또는 구분선)
|
||||
GameObject prefabToUse = null; // 인스턴스화할 프리팹
|
||||
_menuItemDataMap[itemData.ItemId] = itemData;
|
||||
GameObject prefabToUse = null;
|
||||
|
||||
if (itemData.IsSeparator) // 구분선 아이템인 경우
|
||||
{
|
||||
if (menuSeparatorPrefab == null)
|
||||
{
|
||||
ULog.Error("menuSeparatorPrefab이 할당되지 않았습니다. 구분선을 생성할 수 없습니다.", new InvalidOperationException("menuSeparatorPrefab is not loaded."));
|
||||
Debug.LogError("menuSeparatorPrefab이 할당되지 않았습니다. 구분선을 생성할 수 없습니다.");
|
||||
continue; // 다음 아이템으로 넘어감
|
||||
}
|
||||
GameObject separatorInstance = Instantiate(menuSeparatorPrefab, parentContainer);
|
||||
separatorInstance.name = $"Separator_{itemData.ItemId}_Depth{depth}";
|
||||
_menuItemObjects[itemData.ItemId] = separatorInstance; // 관리 목록에 추가
|
||||
instanceToLayout = separatorInstance;
|
||||
}
|
||||
else // 일반 메뉴 아이템인 경우
|
||||
{
|
||||
@@ -372,7 +269,7 @@ namespace UVC.UI.Menu
|
||||
prefabToUse = menuItemPrefab;
|
||||
if (prefabToUse == null)
|
||||
{
|
||||
ULog.Error("menuItemPrefab이 할당되지 않았습니다. 1차 메뉴 아이템을 생성할 수 없습니다.", new InvalidOperationException("menuItemPrefab is not loaded."));
|
||||
Debug.LogError("menuItemPrefab이 할당되지 않았습니다. 1차 메뉴 아이템을 생성할 수 없습니다.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -381,7 +278,7 @@ namespace UVC.UI.Menu
|
||||
prefabToUse = subMenuItemPrefab;
|
||||
if (prefabToUse == null)
|
||||
{
|
||||
ULog.Error("subMenuItemPrefab이 할당되지 않았습니다. 하위 메뉴 아이템을 생성할 수 없습니다.", new InvalidOperationException("subMenuItemPrefab is not loaded."));
|
||||
Debug.LogError("subMenuItemPrefab이 할당되지 않았습니다. 하위 메뉴 아이템을 생성할 수 없습니다.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -389,27 +286,42 @@ namespace UVC.UI.Menu
|
||||
GameObject menuItemInstance = Instantiate(prefabToUse, parentContainer);
|
||||
menuItemInstance.name = $"MenuItem_{itemData.ItemId}_Depth{depth}";
|
||||
_menuItemObjects[itemData.ItemId] = menuItemInstance; // 관리 목록에 추가
|
||||
instanceToLayout = menuItemInstance;
|
||||
|
||||
// 메뉴 아이템 텍스트 설정
|
||||
TextMeshProUGUI buttonText = menuItemInstance.GetComponentInChildren<TextMeshProUGUI>(true); // 비활성화된 자식도 검색
|
||||
// 메뉴 아이템 버튼 기능 설정
|
||||
Button button = menuItemInstance.GetComponent<Button>();
|
||||
|
||||
if (buttonText != null && !string.IsNullOrEmpty(itemData.DisplayNameKey))
|
||||
if (buttonText != null && !string.IsNullOrEmpty(itemData.DisplayName))
|
||||
{
|
||||
if (_locManager != null)
|
||||
{
|
||||
buttonText.text = _locManager.GetString(itemData.DisplayNameKey);
|
||||
buttonText.text = _locManager.GetString(itemData.DisplayName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// LocalizationManager가 없는 경우, 키 값을 그대로 표시 (개발 중 확인 용도)
|
||||
ULog.Warning($"LocalizationManager 인스턴스가 없어 메뉴 아이템 텍스트를 키 값으로 설정합니다: {itemData.DisplayNameKey}");
|
||||
buttonText.text = itemData.DisplayNameKey;
|
||||
Debug.LogWarning($"LocalizationManager 인스턴스가 없어 메뉴 아이템 텍스트를 키 값으로 설정합니다: {itemData.DisplayName}");
|
||||
buttonText.text = itemData.DisplayName;
|
||||
}
|
||||
}
|
||||
|
||||
// isShrinkMenuItemWidth가 true일 때 1차 메뉴 아이템의 너비를 텍스트에 맞게 조절합니다.
|
||||
// 텍스트가 설정된 후에 이 로직을 실행해야 정확한 너비를 계산할 수 있습니다.
|
||||
if (depth == 0 && isShrinkMenuItemWidth && buttonText != null)
|
||||
{
|
||||
buttonText.ForceMeshUpdate(); // 텍스트 변경 후 메쉬 강제 업데이트 (정확한 크기 계산 위함)
|
||||
Vector2 textSize = buttonText.GetPreferredValues(buttonText.text); // 텍스트 내용에 따른 선호 크기 계산
|
||||
RectTransform rect = menuItemInstance.GetComponent<RectTransform>();
|
||||
textSize.x += menuItemWidthPadding; // 좌우 여백 추가
|
||||
if (rect != null) rect.sizeDelta = new Vector2(textSize.x, rect.rect.height);
|
||||
|
||||
// LayoutElement가 없으면 추가
|
||||
LayoutElement layoutElement = menuItemInstance.GetComponent<LayoutElement>() ?? menuItemInstance.AddComponent<LayoutElement>();
|
||||
|
||||
layoutElement.preferredWidth = buttonText.preferredWidth;
|
||||
}
|
||||
|
||||
if (button != null)
|
||||
{
|
||||
button.onClick.RemoveAllListeners(); // 기존 리스너 제거 (프리팹에 설정된 것이 있을 수 있으므로)
|
||||
@@ -421,7 +333,7 @@ namespace UVC.UI.Menu
|
||||
if (itemData.SubMenuItems != null && itemData.SubMenuItems.Count > 0)
|
||||
{
|
||||
// 하위 메뉴가 있으면 해당 하위 메뉴의 표시 상태를 토글
|
||||
ToggleSubMenuDisplay(itemData, menuItemInstance);
|
||||
ToggleSubMenuDisplay(itemData, menuItemInstance, depth);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -437,21 +349,27 @@ namespace UVC.UI.Menu
|
||||
if (itemData.SubMenuItems != null && itemData.SubMenuItems.Count > 0)
|
||||
{
|
||||
// 하위 메뉴 화살표 아이콘 표시
|
||||
Transform subMenuArrowTransform = menuItemInstance.transform.Find(SubMenuArrowName);
|
||||
Transform subMenuArrowTransform = menuItemInstance.transform.Find(subMenuArrowName);
|
||||
if (subMenuArrowTransform != null)
|
||||
{
|
||||
subMenuArrowTransform.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
// 하위 메뉴 컨테이너 찾기 및 설정
|
||||
Transform subMenuContainerTransform = menuItemInstance.transform.Find(SubMenuContainerName);
|
||||
if (subMenuContainerTransform != null)
|
||||
// 하위 메뉴 컨테이너 생성 및 설정
|
||||
if (subMenuContainer != null)
|
||||
{
|
||||
RectTransform subMenuRect = subMenuContainerTransform.GetComponent<RectTransform>();
|
||||
GameObject subMenuContainerInstance = Instantiate(subMenuContainer.gameObject);
|
||||
subMenuContainerInstance.name = $"SubMenuContainer_{itemData.ItemId}";
|
||||
_subMenuContainerObjects[itemData.ItemId] = subMenuContainerInstance;
|
||||
|
||||
// 부모를 메뉴 아이템으로 설정하여 위치를 쉽게 계산
|
||||
subMenuContainerInstance.transform.SetParent(menuItemInstance.transform, false);
|
||||
|
||||
RectTransform subMenuRect = subMenuContainerInstance.GetComponent<RectTransform>();
|
||||
if (subMenuRect == null)
|
||||
{
|
||||
ULog.Warning($"{SubMenuContainerName} for '{menuItemInstance.name}' is missing RectTransform. Adding one.");
|
||||
subMenuRect = subMenuContainerTransform.gameObject.AddComponent<RectTransform>();
|
||||
Debug.LogWarning($"{subMenuContainer.name} for '{menuItemInstance.name}' is missing RectTransform. Adding one.");
|
||||
subMenuRect = subMenuContainerInstance.gameObject.AddComponent<RectTransform>();
|
||||
}
|
||||
|
||||
// 하위 메뉴 컨테이너의 RectTransform 기본 설정 (앵커, 피벗)
|
||||
@@ -459,219 +377,105 @@ namespace UVC.UI.Menu
|
||||
subMenuRect.anchorMax = new Vector2(0, 1); // 좌상단 기준
|
||||
subMenuRect.pivot = new Vector2(0, 1); // 좌상단 기준
|
||||
|
||||
// 하위 메뉴 위치 조정
|
||||
if (depth == 0) // 1차 메뉴의 하위 메뉴는 부모 아이템의 오른쪽 아래에 펼쳐짐
|
||||
RectTransform menuItemRect = menuItemInstance.GetComponent<RectTransform>();
|
||||
|
||||
|
||||
// 최종 부모 설정
|
||||
if (menuContainer != null && menuContainer.parent != null)
|
||||
{
|
||||
// 부모 메뉴 아이템의 너비만큼 오른쪽으로, 설정된 깊이 간격만큼 아래로 이동
|
||||
subMenuRect.anchoredPosition = new Vector2(menuItemSize.x - menuDepthSpace.x, -menuDepthSpace.y);
|
||||
subMenuContainerInstance.transform.SetParent(menuContainer.parent, true);
|
||||
}
|
||||
else // 2차 이상 메뉴의 하위 메뉴는 부모 아이템의 오른쪽 아래에 펼쳐짐
|
||||
else
|
||||
{
|
||||
// 부모 하위 메뉴 아이템의 너비만큼 오른쪽으로, 설정된 깊이 간격만큼 아래로 이동
|
||||
subMenuRect.anchoredPosition = new Vector2(subMenuItemSize.x - subMenuDepthSpace.x, -subMenuDepthSpace.y);
|
||||
Debug.LogWarning("menuContainer 또는 menuContainer.parent가 null이므로 하위 메뉴 컨테이너를 최상위로 설정합니다.");
|
||||
}
|
||||
|
||||
// 재귀 호출을 통해 하위 메뉴 아이템들 생성 및 배치
|
||||
Vector2 subContentSize = CreateMenuItems(itemData.SubMenuItems, subMenuContainerTransform, depth + 1);
|
||||
|
||||
// 하위 메뉴 컨테이너의 크기를 내용물(하위 메뉴 아이템들)과 패딩에 맞게 조절
|
||||
float containerWidth = subContentSize.x + subContainerPadding.x + subContainerPadding.z; // 좌우 패딩 추가
|
||||
float containerHeight = subContentSize.y + subContainerPadding.y + subContainerPadding.w; // 상하 패딩 추가
|
||||
subMenuRect.sizeDelta = new Vector2(containerWidth, containerHeight);
|
||||
CreateMenuItems(itemData.SubMenuItems, subMenuContainerInstance.transform, depth + 1);
|
||||
|
||||
// 하위 메뉴는 초기에 숨겨진 상태로 설정
|
||||
subMenuContainerTransform.gameObject.SetActive(false);
|
||||
subMenuContainerInstance.gameObject.SetActive(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
ULog.Warning($"'{menuItemInstance.name}' 내부에 '{SubMenuContainerName}'를 찾을 수 없습니다. 하위 메뉴가 정상적으로 표시되지 않을 수 있습니다.");
|
||||
Debug.LogWarning($"'subMenuContainer' 프리팹이 Inspector에서 할당되지 않았습니다. 하위 메뉴가 정상적으로 표시되지 않을 수 있습니다.");
|
||||
}
|
||||
}
|
||||
else // 하위 메뉴가 없는 경우
|
||||
{
|
||||
// 하위 메뉴 화살표 아이콘 숨김 (프리팹에 기본적으로 활성화되어 있을 수 있으므로)
|
||||
Transform existingArrow = menuItemInstance.transform.Find(SubMenuArrowName);
|
||||
Transform existingArrow = menuItemInstance.transform.Find(subMenuArrowName);
|
||||
if (existingArrow != null)
|
||||
{
|
||||
existingArrow.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 생성된 메뉴 아이템(또는 구분선)의 레이아웃(위치, 크기) 설정
|
||||
if (instanceToLayout != null)
|
||||
{
|
||||
LayoutMenuItem(instanceToLayout, itemData, depth, i, items);
|
||||
}
|
||||
|
||||
// 현재 레벨의 전체 너비와 높이 누적 계산
|
||||
if (depth == 0) // 최상위 메뉴 (수평으로 아이템 나열)
|
||||
{
|
||||
if (i > 0) currentLevelCalculatedWidth += menuItemSpace.x; // 아이템 간 수평 간격 추가
|
||||
// 아이템 너비 추가 (구분선은 구분선 크기 + 좌우 마진, 일반 아이템은 menuItemSize.x)
|
||||
currentLevelCalculatedWidth += itemData.IsSeparator ? (menuItemSeparatorSize.x + menuItemSeparatorMargin.x + menuItemSeparatorMargin.z) : menuItemSize.x;
|
||||
// 현재 레벨의 최대 높이 갱신 (구분선은 구분선 높이 + 상하 마진, 일반 아이템은 menuItemSize.y)
|
||||
currentLevelCalculatedHeight = Mathf.Max(currentLevelCalculatedHeight, itemData.IsSeparator ? (menuItemSeparatorSize.y + menuItemSeparatorMargin.y + menuItemSeparatorMargin.w) : menuItemSize.y);
|
||||
}
|
||||
else // 하위 메뉴 (수직으로 아이템 나열)
|
||||
{
|
||||
if (i > 0) currentLevelCalculatedHeight += menuItemSpace.y; // 아이템 간 수직 간격 추가
|
||||
|
||||
if (itemData.IsSeparator) // 구분선인 경우
|
||||
{
|
||||
// 구분선 높이 + 상하 마진 추가
|
||||
currentLevelCalculatedHeight += menuItemSeparatorSize.y + menuItemSeparatorMargin.y + menuItemSeparatorMargin.w;
|
||||
}
|
||||
else // 일반 하위 메뉴 아이템인 경우
|
||||
{
|
||||
currentLevelCalculatedHeight += subMenuItemSize.y; // 하위 메뉴 아이템 높이 추가
|
||||
}
|
||||
// 현재 레벨의 최대 너비 갱신 (모든 하위 메뉴 아이템은 동일한 너비를 가진다고 가정)
|
||||
currentLevelCalculatedWidth = Mathf.Max(currentLevelCalculatedWidth, subMenuItemSize.x);
|
||||
}
|
||||
}
|
||||
return new Vector2(currentLevelCalculatedWidth, currentLevelCalculatedHeight);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// UI 요소(메뉴 아이템 또는 구분선)의 위치와 크기를 설정합니다.
|
||||
/// 메뉴의 깊이(depth)와 아이템의 인덱스(itemIndex)에 따라 수평 또는 수직으로 배치합니다.
|
||||
/// </summary>
|
||||
/// <param name="uiElement">레이아웃을 적용할 메뉴 아이템 또는 구분선 GameObject입니다.</param>
|
||||
/// <param name="itemData">해당 UI 요소의 MenuItemData입니다.</param>
|
||||
/// <param name="depth">현재 메뉴의 깊이입니다.</param>
|
||||
/// <param name="itemIndex">현재 아이템의 부모 컨테이너 내에서의 인덱스입니다.</param>
|
||||
/// <param name="siblingItems">현재 아이템과 같은 레벨에 있는 모든 형제 아이템들의 리스트입니다.</param>
|
||||
protected virtual void LayoutMenuItem(GameObject uiElement, MenuItemData itemData, int depth, int itemIndex, List<MenuItemData> siblingItems)
|
||||
{
|
||||
RectTransform rectTransform = uiElement.GetComponent<RectTransform>();
|
||||
if (rectTransform == null)
|
||||
{
|
||||
ULog.Error($"GameObject '{uiElement.name}'에 RectTransform 컴포넌트가 없습니다. 레이아웃을 적용할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 모든 메뉴 아이템과 구분선의 앵커 및 피벗을 좌상단으로 통일 (계산 편의성)
|
||||
rectTransform.anchorMin = new Vector2(0, 1); // 좌상단
|
||||
rectTransform.anchorMax = new Vector2(0, 1); // 좌상단
|
||||
rectTransform.pivot = new Vector2(0, 1); // 좌상단
|
||||
|
||||
float currentX = 0; // 계산될 X 위치
|
||||
float currentY = 0; // 계산될 Y 위치
|
||||
|
||||
if (depth == 0) // 1차 깊이 메뉴 (최상위 메뉴, 수평 레이아웃)
|
||||
{
|
||||
// 초기 위치 설정 (menuContainer의 좌상단 기준)
|
||||
currentX = menuItemInitPosition.x;
|
||||
currentY = -menuItemInitPosition.y; // Y는 아래로 내려가므로 음수
|
||||
|
||||
// 현재 아이템 이전의 형제 아이템들의 너비와 간격을 누적하여 X 위치 계산
|
||||
for (int k = 0; k < itemIndex; k++)
|
||||
{
|
||||
MenuItemData sibling = siblingItems[k];
|
||||
if (sibling.IsSeparator)
|
||||
{
|
||||
// 구분선 너비 + 좌우 마진 + 아이템 간 간격
|
||||
currentX += menuItemSeparatorSize.x + menuItemSeparatorMargin.x + menuItemSeparatorMargin.z;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentX += menuItemSize.x; // 일반 메뉴 아이템 너비
|
||||
}
|
||||
currentX += menuItemSpace.x; // 아이템 간 수평 간격
|
||||
}
|
||||
|
||||
if (itemData.IsSeparator) // 현재 아이템이 구분선인 경우
|
||||
{
|
||||
rectTransform.sizeDelta = menuItemSeparatorSize; // 구분선 크기 설정
|
||||
// 구분선 위치에 좌측 마진 적용, Y 위치에 상단 마진 적용
|
||||
rectTransform.anchoredPosition = new Vector2(currentX + menuItemSeparatorMargin.x, currentY - menuItemSeparatorMargin.y);
|
||||
}
|
||||
else // 현재 아이템이 일반 메뉴 아이템인 경우
|
||||
{
|
||||
rectTransform.sizeDelta = menuItemSize; // 메뉴 아이템 크기 설정
|
||||
rectTransform.anchoredPosition = new Vector2(currentX, currentY); // 계산된 위치 설정
|
||||
}
|
||||
}
|
||||
else // 2차 깊이 이상 하위 메뉴 (수직 레이아웃)
|
||||
{
|
||||
// 초기 위치 설정 (부모 SubMenuContainer의 좌상단 기준, 패딩 적용)
|
||||
currentX = subContainerPadding.x; // 좌측 패딩
|
||||
currentY = -subContainerPadding.y; // 상단 패딩 (Y는 아래로 내려가므로 음수)
|
||||
|
||||
// 현재 아이템 이전의 형제 아이템들의 높이와 간격을 누적하여 Y 위치 계산
|
||||
for (int k = 0; k < itemIndex; k++)
|
||||
{
|
||||
MenuItemData sibling = siblingItems[k];
|
||||
if (sibling.IsSeparator)
|
||||
{
|
||||
// 구분선 상단 마진 + 구분선 높이 + 구분선 하단 마진 + 아이템 간 간격
|
||||
currentY -= (menuItemSeparatorMargin.y + menuItemSeparatorSize.y + menuItemSeparatorMargin.w);
|
||||
}
|
||||
else
|
||||
{
|
||||
currentY -= subMenuItemSize.y; // 하위 메뉴 아이템 높이
|
||||
}
|
||||
currentY -= menuItemSpace.y; // 아이템 간 수직 간격
|
||||
}
|
||||
|
||||
if (itemData.IsSeparator) // 현재 아이템이 구분선인 경우
|
||||
{
|
||||
// 구분선 Y 위치에 추가로 자신의 상단 마진 적용
|
||||
currentY -= menuItemSeparatorMargin.y;
|
||||
rectTransform.sizeDelta = menuItemSeparatorSize; // 구분선 크기 설정
|
||||
// 구분선 X 위치에 좌측 마진 적용
|
||||
rectTransform.anchoredPosition = new Vector2(currentX + menuItemSeparatorMargin.x, currentY);
|
||||
}
|
||||
else // 현재 아이템이 일반 하위 메뉴 아이템인 경우
|
||||
{
|
||||
rectTransform.sizeDelta = subMenuItemSize; // 하위 메뉴 아이템 크기 설정
|
||||
rectTransform.anchoredPosition = new Vector2(currentX, currentY); // 계산된 위치 설정
|
||||
}
|
||||
}
|
||||
// 최종 계산된 anchoredPosition을 다시 한번 설정 (depth 0 일반 아이템의 경우 중복될 수 있으나 일관성을 위해)
|
||||
// rectTransform.anchoredPosition = new Vector2(currentX, currentY); // 이 줄은 depth 0 일반 아이템의 경우 이미 위에서 설정됨. 조건부로 하거나 위의 로직을 재검토.
|
||||
// 현재 로직에서는 각 분기 내에서 anchoredPosition을 설정하므로 이 줄은 불필요하거나,
|
||||
// depth 0 일반 아이템의 X 위치 계산 후 여기서 Y와 함께 최종 설정하는 방식으로 변경 가능.
|
||||
// 현재는 각 분기에서 설정하는 것이 명확해 보임.
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 특정 메뉴 아이템에 연결된 하위 메뉴의 표시 상태를 토글(열기/닫기)합니다.
|
||||
/// 하위 메뉴를 열 때, 같은 레벨의 다른 열려있는 하위 메뉴들은 닫습니다.
|
||||
/// </summary>
|
||||
/// <param name="itemData">토글할 하위 메뉴를 가진 부모 메뉴의 MenuItemData입니다.</param>
|
||||
/// <param name="menuItemInstance">부모 메뉴 아이템의 GameObject입니다.</param>
|
||||
protected virtual void ToggleSubMenuDisplay(MenuItemData itemData, GameObject menuItemInstance)
|
||||
/// <param name="depth">클릭된 메뉴 아이템의 깊이입니다.</param>
|
||||
protected virtual void ToggleSubMenuDisplay(MenuItemData itemData, GameObject menuItemInstance, int depth)
|
||||
{
|
||||
if (itemData.IsSeparator) return; // 구분선은 하위 메뉴를 가질 수 없음
|
||||
|
||||
// 메뉴 아이템 내부에서 SubMenuContainer 이름의 GameObject를 찾음
|
||||
Transform subMenuContainer = menuItemInstance.transform.Find(SubMenuContainerName);
|
||||
if (subMenuContainer != null)
|
||||
if (_subMenuContainerObjects.TryGetValue(itemData.ItemId, out GameObject subMenuContainerObject))
|
||||
{
|
||||
bool isActive = subMenuContainer.gameObject.activeSelf; // 현재 하위 메뉴의 활성화 상태
|
||||
bool isActive = subMenuContainerObject.activeSelf; // 현재 하위 메뉴의 활성화 상태
|
||||
bool becomingActive = !isActive; // 토글 후 활성화될 상태
|
||||
|
||||
if (becomingActive) // 하위 메뉴가 열리려고 하는 경우
|
||||
// 새로 메뉴를 여는 경우, 현재 메뉴와 관련된 다른 메뉴들을 먼저 닫습니다.
|
||||
if (becomingActive)
|
||||
{
|
||||
// 현재 클릭된 메뉴 아이템의 부모 컨테이너에서, 이 하위 메뉴를 제외한 다른 모든 하위 메뉴를 닫음
|
||||
CloseOtherSubMenusInParent(menuItemInstance.transform.parent, subMenuContainer);
|
||||
// 클릭된 아이템의 부모 컨테이너(같은 레벨의 메뉴 아이템들이 있는 곳)를 기준으로
|
||||
// 현재 열려는 하위 메뉴(subMenuContainerObject)를 제외한 다른 모든 하위 메뉴를 닫습니다.
|
||||
CloseOtherSubMenusInParent(itemData, menuItemInstance.transform.parent, subMenuContainerObject);
|
||||
|
||||
// 하위 메뉴를 포함하는 menuItemInstance를 그 부모 컨테이너 내에서 가장 앞으로 가져와서
|
||||
// 다른 형제 메뉴 아이템에 가려지지 않도록 합니다. (렌더링 순서 조정)
|
||||
menuItemInstance.transform.SetAsLastSibling();
|
||||
// 만약 1차 메뉴(depth 0)를 클릭해서 2차 메뉴를 여는 경우라면,
|
||||
// 현재 열려있는 모든 하위 메뉴를 닫아줍니다.
|
||||
// 이렇게 하면 다른 메뉴 가지(branch)에 열려있던 하위 메뉴들이 모두 닫힙니다.
|
||||
if (depth == 0)
|
||||
{
|
||||
CloseAllOpenSubMenus();
|
||||
}
|
||||
}
|
||||
else // 하위 메뉴가 닫히려고 하는 경우
|
||||
{
|
||||
// 닫으려는 메뉴의 모든 자식 메뉴들도 함께 닫습니다.
|
||||
CloseSubMenuAndDescendants(itemData);
|
||||
}
|
||||
|
||||
// 하위 메뉴 컨테이너의 활성화 상태를 토글
|
||||
subMenuContainer.gameObject.SetActive(becomingActive);
|
||||
subMenuContainerObject.SetActive(becomingActive);
|
||||
|
||||
if (becomingActive) subMenuContainerObject.GetComponent<LayoutGroup>()?.FitToChildren(); // 하위 메뉴 컨테이너의 크기를 자식 아이템에 맞게 조정
|
||||
|
||||
// isAnySubMenuOpen 상태 및 UI 블로커 업데이트
|
||||
if (becomingActive)
|
||||
{
|
||||
isAnySubMenuOpen = true; // 하나라도 열리면 true
|
||||
|
||||
RectTransform menuItemRect = menuItemInstance.GetComponent<RectTransform>();
|
||||
RectTransform menuItemRectparent = menuItemInstance.transform.parent?.GetComponent<RectTransform>();
|
||||
RectTransform subMenuRect = subMenuContainerObject.transform?.GetComponent<RectTransform>();
|
||||
if (menuItemRect != null && subMenuRect != null && menuItemRectparent != null)
|
||||
{
|
||||
// 하위 메뉴 위치 조정
|
||||
if (depth == 0) // 1차 메뉴의 하위 메뉴는 부모 아이템의 아래쪽에 펼쳐짐
|
||||
{
|
||||
subMenuRect.anchoredPosition = new Vector2(menuItemRect.anchoredPosition.x + menuDepthSpace.x, -menuItemRect.rect.height + menuDepthSpace.y);
|
||||
}
|
||||
else // 2차 이상 메뉴의 하위 메뉴는 부모 아이템의 오른쪽에 펼쳐짐
|
||||
{
|
||||
subMenuRect.anchoredPosition = new Vector2(menuItemRectparent.anchoredPosition.x + menuItemRectparent.rect.width + subMenuDepthSpace.x, menuItemRectparent.anchoredPosition.y + menuItemRect.anchoredPosition.y + subMenuDepthSpace.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -682,35 +486,71 @@ namespace UVC.UI.Menu
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 메뉴 아이템의 모든 하위 메뉴들을 재귀적으로 닫습니다.
|
||||
/// </summary>
|
||||
/// <param name="parentItemData">하위 메뉴들을 닫을 부모 메뉴의 MenuItemData입니다.</param>
|
||||
protected virtual void CloseSubMenuAndDescendants(MenuItemData parentItemData)
|
||||
{
|
||||
if (parentItemData?.SubMenuItems == null) return;
|
||||
|
||||
foreach (var subItem in parentItemData.SubMenuItems)
|
||||
{
|
||||
if (subItem.IsSeparator) continue;
|
||||
|
||||
if (_subMenuContainerObjects.TryGetValue(subItem.ItemId, out GameObject subContainer))
|
||||
{
|
||||
if (subContainer != null && subContainer.activeSelf)
|
||||
{
|
||||
subContainer.SetActive(false);
|
||||
// 이 하위 메뉴의 자식들도 재귀적으로 닫습니다.
|
||||
CloseSubMenuAndDescendants(subItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 부모 컨테이너 내에서, 지정된 하위 메뉴(subMenuToExclude)를 제외한 모든 다른 열려있는 하위 메뉴들을 닫습니다.
|
||||
/// 이는 한 번에 하나의 하위 메뉴만 열려 있도록 보장하는 데 사용됩니다.
|
||||
/// </summary>
|
||||
/// <param name="itemData">토글할 하위 메뉴를 가진 메뉴의 MenuItemData입니다.</param>
|
||||
/// <param name="currentMenuItemsParent">하위 메뉴들을 검사할 부모 Transform입니다. (예: TopMenu 또는 다른 SubMenuContainer)</param>
|
||||
/// <param name="subMenuToExclude">닫지 않고 유지할 특정 하위 메뉴의 Transform입니다. (보통 새로 열리려는 하위 메뉴)</param>
|
||||
protected virtual void CloseOtherSubMenusInParent(Transform currentMenuItemsParent, Transform subMenuToExclude)
|
||||
/// <param name="subMenuToExclude">닫지 않고 유지할 특정 하위 메뉴의 GameObject입니다. (보통 새로 열리려는 하위 메뉴)</param>
|
||||
protected virtual void CloseOtherSubMenusInParent(MenuItemData itemData, Transform currentMenuItemsParent, GameObject subMenuToExclude)
|
||||
{
|
||||
if (currentMenuItemsParent == null)
|
||||
{
|
||||
ULog.Warning("CloseOtherSubMenusInParent 호출 시 currentMenuItemsParent가 null입니다. 다른 하위 메뉴를 닫을 수 없습니다.");
|
||||
Debug.LogWarning("CloseOtherSubMenusInParent 호출 시 currentMenuItemsParent가 null입니다. 다른 하위 메뉴를 닫을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 부모 컨테이너의 모든 자식 메뉴 아이템들을 순회
|
||||
for (int i = 0; i < currentMenuItemsParent.childCount; i++)
|
||||
Debug.Log($"CloseOtherSubMenusInParent 호출: 현재 부모 - {currentMenuItemsParent.name}, 제외할 하위 메뉴 - {subMenuToExclude?.name}");
|
||||
|
||||
string parentName = currentMenuItemsParent.name + "_";
|
||||
|
||||
// 현재 부모 컨테이너 내에서 열려있는 하위 메뉴들을 찾습니다.
|
||||
MenuItemData? parentItemData = itemData.Parent;
|
||||
|
||||
if (parentItemData != null)
|
||||
{
|
||||
Transform siblingMenuItemTransform = currentMenuItemsParent.GetChild(i);
|
||||
// ToList()를 사용하여 반복 중 컬렉션 수정 문제를 방지합니다.
|
||||
var openSubMenus = _subMenuContainerObjects.Where(pair => pair.Value != null &&
|
||||
pair.Value.activeSelf && pair.Value != subMenuToExclude && parentItemData.HasSubMenuItems(pair.Key)).ToList();
|
||||
|
||||
// 자기 자신이거나, 제외할 하위 메뉴의 부모 메뉴 아이템인 경우는 건너뜀
|
||||
if (siblingMenuItemTransform == null || siblingMenuItemTransform == subMenuToExclude?.parent) continue;
|
||||
|
||||
// 형제 메뉴 아이템 내부에서 SubMenuContainer를 찾음
|
||||
Transform potentialSubMenu = siblingMenuItemTransform.Find(SubMenuContainerName);
|
||||
|
||||
// 찾은 SubMenuContainer가 존재하고, 제외할 하위 메뉴가 아니며, 현재 활성화 상태라면 비활성화시킴
|
||||
if (potentialSubMenu != null && potentialSubMenu != subMenuToExclude && potentialSubMenu.gameObject.activeSelf)
|
||||
foreach (var pair in openSubMenus)
|
||||
{
|
||||
potentialSubMenu.gameObject.SetActive(false);
|
||||
string itemId = pair.Key;
|
||||
GameObject containerToClose = pair.Value;
|
||||
|
||||
// 닫아야 할 형제 하위 메뉴를 찾았습니다.
|
||||
containerToClose.SetActive(false);
|
||||
|
||||
// 이 하위 메뉴의 자손 메뉴들도 모두 재귀적으로 닫습니다.
|
||||
if (_menuItemDataMap.TryGetValue(itemId, out MenuItemData itemDataToClose))
|
||||
{
|
||||
CloseSubMenuAndDescendants(itemDataToClose);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -723,18 +563,12 @@ namespace UVC.UI.Menu
|
||||
{
|
||||
bool anyActuallyClosed = false; // 실제로 닫힌 하위 메뉴가 있었는지 추적
|
||||
|
||||
// _menuItemObjects에 저장된 모든 메뉴 아이템 GameObject들을 순회
|
||||
foreach (GameObject menuItemGO in _menuItemObjects.Values)
|
||||
foreach (GameObject subMenuContainer in _subMenuContainerObjects.Values)
|
||||
{
|
||||
if (menuItemGO == null) continue; // null 체크
|
||||
|
||||
// 각 메뉴 아이템에서 SubMenuContainer를 찾아 비활성화
|
||||
// 구분선 아이템은 SubMenuContainer가 없으므로 Find 결과가 null이 되어 자동으로 처리됨.
|
||||
Transform subMenuContainer = menuItemGO.transform.Find(SubMenuContainerName);
|
||||
if (subMenuContainer != null && subMenuContainer.gameObject.activeSelf)
|
||||
if (subMenuContainer != null && subMenuContainer.activeSelf)
|
||||
{
|
||||
subMenuContainer.gameObject.SetActive(false);
|
||||
anyActuallyClosed = true; // 하나라도 닫았으면 true
|
||||
subMenuContainer.SetActive(false);
|
||||
anyActuallyClosed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -752,12 +586,9 @@ namespace UVC.UI.Menu
|
||||
protected virtual void CheckIfAnySubMenuRemainsOpen()
|
||||
{
|
||||
isAnySubMenuOpen = false; // 일단 false로 가정
|
||||
foreach (GameObject menuItemGO in _menuItemObjects.Values)
|
||||
foreach (GameObject subMenuGO in _subMenuContainerObjects.Values)
|
||||
{
|
||||
if (menuItemGO == null) continue;
|
||||
|
||||
Transform subMenuContainer = menuItemGO.transform.Find(SubMenuContainerName);
|
||||
if (subMenuContainer != null && subMenuContainer.gameObject.activeSelf)
|
||||
if (subMenuGO != null && subMenuGO.activeSelf)
|
||||
{
|
||||
isAnySubMenuOpen = true; // 하나라도 열려있으면 true로 설정하고 반복 종료
|
||||
break;
|
||||
@@ -784,33 +615,20 @@ namespace UVC.UI.Menu
|
||||
Transform blockerParent = uiBlockerInstance.transform.parent;
|
||||
if (menuContainer != null && menuContainer.parent == blockerParent)
|
||||
{
|
||||
// menuContainer 바로 뒤에 오도록 (SiblingIndex가 작을수록 먼저 그려짐 - 뒤에 위치)
|
||||
// 블로커가 메뉴보다 먼저 그려져야 메뉴를 가리지 않으면서 메뉴 외 영역 클릭을 막을 수 있음.
|
||||
// 하지만 UI 이벤트는 앞쪽에 있는 것부터 받으므로, 블로커가 메뉴보다 뒤에 있어야 함.
|
||||
// 따라서, 블로커는 메뉴보다 낮은 SiblingIndex - 1를 가져야 함.
|
||||
// 여기서는 메뉴 컨테이너 바로 뒤에 오도록 설정 (블로커가 렌더링된 후 메뉴가 렌더링되어 메뉴를 덮지 않도록)
|
||||
// 혼동을 피하기 위해, 블로커는 메뉴 시스템 전체보다 뒤에 위치해야 클릭을 가로챌 수 있습니다.
|
||||
// CreateUIBlocker 호출 시점에는 menuContainer의 sibling index가 확정되지 않았을 수 있으므로,
|
||||
// UpdateBlockerVisibility에서 최종적으로 조정하는 것이 더 안전할 수 있습니다.
|
||||
// 여기서는 일단 menuContainer의 뒤에 두도록 시도합니다.
|
||||
uiBlockerInstance.transform.SetSiblingIndex(menuContainer.GetSiblingIndex() - 1);
|
||||
}
|
||||
else if (transform.parent == blockerParent)
|
||||
{
|
||||
// TopMenuView 자체가 blockerParent의 자식인 경우, TopMenuView 뒤에 블로커를 둡니다.
|
||||
uiBlockerInstance.transform.SetSiblingIndex(transform.GetSiblingIndex() - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 그 외의 경우, 블로커를 부모의 마지막 자식으로 보내 UI 요소들 중 가장 앞에 오도록 합니다.
|
||||
// 이렇게 하면 다른 UI 요소들의 클릭을 막을 수 있습니다.
|
||||
uiBlockerInstance.transform.SetAsLastSibling();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 화면에 표시된 모든 메뉴 아이템들을 제거하고 관련 내부 데이터도 초기화합니다.
|
||||
/// </summary>
|
||||
@@ -825,21 +643,23 @@ namespace UVC.UI.Menu
|
||||
}
|
||||
_menuItemObjects.Clear();
|
||||
|
||||
foreach (var pair in _subMenuContainerObjects)
|
||||
{
|
||||
if (pair.Value != null) Destroy(pair.Value);
|
||||
}
|
||||
_subMenuContainerObjects.Clear();
|
||||
_menuItemDataMap.Clear();
|
||||
|
||||
// menuContainer의 모든 자식 GameObject들을 직접 파괴합니다.
|
||||
// 이는 _menuItemObjects에 등록되지 않은 자식 요소(예: 동적으로 추가된 다른 요소)도 제거합니다.
|
||||
if (menuContainer != null)
|
||||
{
|
||||
foreach (Transform child in menuContainer)
|
||||
{
|
||||
// 자식 GameObject를 직접 파괴합니다.
|
||||
// Instantiate로 생성된 객체들이므로 Destroy로 제거하는 것이 맞습니다.
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
}
|
||||
// menuContainer 자체를 파괴하는 것이 아니라 그 내용물만 비우는 것입니다.
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 제공된 MenuItemData 리스트를 기반으로 화면에 표시된 모든 메뉴 아이템들의 텍스트를 업데이트합니다.
|
||||
/// 주로 언어 변경 시 호출됩니다.
|
||||
@@ -849,18 +669,18 @@ namespace UVC.UI.Menu
|
||||
{
|
||||
if (_locManager == null)
|
||||
{
|
||||
ULog.Warning("LocalizationManager가 없어 메뉴 텍스트를 업데이트할 수 없습니다.");
|
||||
Debug.LogWarning("LocalizationManager가 없어 메뉴 텍스트를 업데이트할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
// 재귀 함수를 호출하여 모든 메뉴 아이템(하위 메뉴 포함)의 텍스트를 업데이트합니다.
|
||||
UpdateMenuTextsRecursive(items);
|
||||
UpdateMenuTextsRecursive(items, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 재귀적으로 메뉴 아이템들의 텍스트를 업데이트하는 내부 도우미 함수입니다.
|
||||
/// 재귀적으로 메뉴 아이템들의 텍스트를 업데이트하고, 필요에 따라 너비를 조절하는 내부 도우미 함수입니다.
|
||||
/// </summary>
|
||||
/// <param name="items">텍스트를 업데이트할 메뉴 아이템들의 데이터 리스트입니다.</param>
|
||||
protected virtual void UpdateMenuTextsRecursive(List<MenuItemData> items)
|
||||
/// <param name="depth">현재 메뉴의 깊이입니다.</param>
|
||||
protected virtual void UpdateMenuTextsRecursive(List<MenuItemData> items, int depth = 0)
|
||||
{
|
||||
if (items == null) return;
|
||||
|
||||
@@ -868,21 +688,26 @@ namespace UVC.UI.Menu
|
||||
{
|
||||
if (itemData.IsSeparator) continue; // 구분선은 텍스트가 없음
|
||||
|
||||
// _menuItemObjects에서 해당 아이템의 GameObject를 찾음
|
||||
if (_menuItemObjects.TryGetValue(itemData.ItemId, out GameObject menuItemInstance))
|
||||
{
|
||||
// GameObject에서 TextMeshProUGUI 컴포넌트를 찾아 텍스트 업데이트
|
||||
TextMeshProUGUI buttonText = menuItemInstance.GetComponentInChildren<TextMeshProUGUI>(true);
|
||||
if (buttonText != null && !string.IsNullOrEmpty(itemData.DisplayNameKey))
|
||||
if (buttonText != null && !string.IsNullOrEmpty(itemData.DisplayName))
|
||||
{
|
||||
buttonText.text = _locManager.GetString(itemData.DisplayNameKey);
|
||||
buttonText.text = _locManager.GetString(itemData.DisplayName);
|
||||
|
||||
// isShrinkMenuItemWidth가 true일 때 1차 메뉴 아이템의 너비를 텍스트에 맞게 조절
|
||||
if (depth == 0 && isShrinkMenuItemWidth)
|
||||
{
|
||||
LayoutElement layoutElement = menuItemInstance.GetComponent<LayoutElement>() ?? menuItemInstance.AddComponent<LayoutElement>();
|
||||
// 텍스트의 preferredWidth를 기반으로 선호 너비 설정 (좌우 여백 포함)
|
||||
layoutElement.preferredWidth = buttonText.preferredWidth + menuItemWidthPadding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 하위 메뉴가 있으면 재귀적으로 호출하여 하위 메뉴 아이템들의 텍스트도 업데이트
|
||||
if (itemData.SubMenuItems != null && itemData.SubMenuItems.Count > 0)
|
||||
{
|
||||
UpdateMenuTextsRecursive(itemData.SubMenuItems);
|
||||
UpdateMenuTextsRecursive(itemData.SubMenuItems, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using UVC.UI.Toolbar.Model;
|
||||
using UVC.UI.Toolbar.View;
|
||||
|
||||
namespace UVC.UI.ToolBar.View
|
||||
namespace UVC.UI.Toolbar.View
|
||||
{
|
||||
/// <summary>
|
||||
/// 툴바 버튼의 UI 생성, 설정 및 업데이트 로직을 처리하는 인터페이스입니다.
|
||||
|
||||
330
Assets/Scripts/UVC/UI/ToolBar/View/SubMenuHandler.cs
Normal file
330
Assets/Scripts/UVC/UI/ToolBar/View/SubMenuHandler.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UVC.Extension;
|
||||
using UVC.UI.Toolbar.Model;
|
||||
using UVC.UI.Tooltip;
|
||||
|
||||
namespace UVC.UI.Toolbar.View
|
||||
{
|
||||
/// <summary>
|
||||
/// 확장 가능한 툴바 버튼(ToolbarExpandableButton)의 하위 메뉴 UI를 생성, 관리 및 상호작용을 처리하는 헬퍼 클래스입니다.
|
||||
/// ToolbarView로부터 하위 메뉴 관련 로직을 위임받아 처리하여 ToolbarView의 복잡도를 낮춥니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 주요 역할:
|
||||
/// - 하위 메뉴 토글: 특정 확장 버튼에 대한 하위 메뉴를 열거나 닫습니다.
|
||||
/// - 하위 메뉴 UI 생성: `subMenuPanelPrefab`을 사용하여 하위 메뉴의 패널과 그 안의 버튼들을 동적으로 생성합니다.
|
||||
/// - 위치 계산: 하위 메뉴가 나타날 위치를 주 확장 버튼의 위치를 기준으로 계산하여 설정합니다.
|
||||
/// - 이벤트 처리: 생성된 하위 메뉴의 버튼들에 대한 클릭 이벤트를 설정하고, 클릭 시 메뉴를 닫는 등의 동작을 처리합니다.
|
||||
/// - 외부 클릭 감지: 하위 메뉴가 열려 있을 때 메뉴 영역 바깥을 클릭하면 메뉴가 닫히도록 처리합니다.
|
||||
/// - 리소스 정리: 하위 메뉴가 닫힐 때 생성되었던 모든 UI 요소와 이벤트 리스너를 깨끗하게 제거합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 이 클래스는 ToolbarView 내부에서 다음과 같이 사용됩니다.
|
||||
///
|
||||
/// // 1. ToolbarView의 Awake 메서드에서 SubMenuHandler 인스턴스 생성
|
||||
/// // public class ToolbarView : MonoBehaviour
|
||||
/// // {
|
||||
/// // private SubMenuHandler _subMenuHandler;
|
||||
/// //
|
||||
/// // void Awake()
|
||||
/// // {
|
||||
/// // _subMenuHandler = new SubMenuHandler(this);
|
||||
/// // }
|
||||
/// //
|
||||
/// // // ...
|
||||
/// // }
|
||||
///
|
||||
/// // 2. 확장 버튼(ExpandableButton)의 UI 프로세서에서 `ToggleSubMenu` 호출
|
||||
/// // public class ToolbarExpandableButtonViewProcessor : IButtonViewProcessor
|
||||
/// // {
|
||||
/// // public void SetupButtonInteractions(ToolbarButtonBase buttonModel, GameObject buttonUIObject, ToolbarView viewContext)
|
||||
/// // {
|
||||
/// // ToolbarExpandableButton expandableModel = buttonModel as ToolbarExpandableButton;
|
||||
/// // Button uiButton = buttonUIObject.GetComponent<Button>();
|
||||
/// // if (uiButton != null)
|
||||
/// // {
|
||||
/// // uiButton.onClick.AddListener(() =>
|
||||
/// // {
|
||||
/// // // viewContext의 ToggleSubMenu를 호출하면 내부적으로 SubMenuHandler의 ToggleSubMenu가 실행됩니다.
|
||||
/// // viewContext.ToggleSubMenu(expandableModel, buttonUIObject);
|
||||
/// // });
|
||||
/// // }
|
||||
/// // }
|
||||
/// // }
|
||||
///
|
||||
/// // 3. ToolbarView의 Update 메서드에서 매 프레임 외부 클릭 감지 로직 실행
|
||||
/// // public class ToolbarView : MonoBehaviour
|
||||
/// // {
|
||||
/// // void Update()
|
||||
/// // {
|
||||
/// // _subMenuHandler.Update(); // 매 프레임 호출하여 외부 클릭 감지
|
||||
/// // }
|
||||
/// // }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class SubMenuHandler
|
||||
{
|
||||
private readonly ToolbarView _view;
|
||||
private GameObject _currentSubMenu;
|
||||
private ToolbarExpandableButton _ownerOfCurrentSubMenu;
|
||||
|
||||
/// <summary>
|
||||
/// SubMenuHandler의 새 인스턴스를 초기화합니다.
|
||||
/// </summary>
|
||||
/// <param name="view">이 핸들러를 소유하고 관리하는 ToolbarView의 인스턴스입니다. 프리팹, 캔버스 등의 참조에 사용됩니다.</param>
|
||||
public SubMenuHandler(ToolbarView view)
|
||||
{
|
||||
_view = view;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 확장 버튼에 대한 하위 메뉴 UI를 토글합니다.
|
||||
/// 이미 해당 버튼의 하위 메뉴가 열려있으면 닫고, 닫혀있으면 엽니다.
|
||||
/// 다른 버튼의 하위 메뉴가 열려있었다면, 그 메뉴는 닫고 새로운 메뉴를 엽니다.
|
||||
/// </summary>
|
||||
/// <param name="expandableButtonModel">하위 버튼 목록을 가진 확장 버튼의 데이터 모델입니다.</param>
|
||||
/// <param name="expandableButtonObj">하위 메뉴의 위치를 결정하는 데 사용될 주 확장 버튼의 UI GameObject입니다.</param>
|
||||
public void ToggleSubMenu(ToolbarExpandableButton expandableButtonModel, GameObject expandableButtonObj)
|
||||
{
|
||||
bool closeOnly = false;
|
||||
// 이미 하위 메뉴가 열려있는 경우
|
||||
if (_currentSubMenu != null)
|
||||
{
|
||||
// 현재 열린 메뉴가 지금 클릭한 버튼에 의해 열린 것인지 확인
|
||||
if (_ownerOfCurrentSubMenu == expandableButtonModel)
|
||||
{
|
||||
// 같은 버튼을 다시 클릭했으므로, 메뉴를 닫기만 하고 다시 열지 않습니다.
|
||||
closeOnly = true;
|
||||
}
|
||||
// 기존에 열려있던 하위 메뉴를 정리합니다.
|
||||
DestroyCurrentSubMenuAndClearListeners();
|
||||
}
|
||||
|
||||
// 닫기만 해야 하는 경우, 여기서 함수를 종료합니다.
|
||||
if (closeOnly)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// --- 새 하위 메뉴를 여는 로직 ---
|
||||
|
||||
// 필요한 프리팹이 할당되었는지 확인합니다.
|
||||
if (_view.subMenuPanelPrefab == null)
|
||||
{
|
||||
Debug.LogError("ToolbarView: subMenuPanelPrefab이 할당되지 않아 하위 메뉴를 열 수 없습니다.", _view);
|
||||
return;
|
||||
}
|
||||
// 하위 버튼이 하나라도 있는지 확인합니다.
|
||||
if (expandableButtonModel.SubButtons == null || expandableButtonModel.SubButtons.Count == 0)
|
||||
{
|
||||
return; // 하위 버튼이 없으면 메뉴를 열지 않습니다.
|
||||
}
|
||||
|
||||
// 하위 메뉴 패널 UI를 생성합니다. 생성 위치는 ToolbarView의 자식으로 합니다.
|
||||
_currentSubMenu = Object.Instantiate(_view.subMenuPanelPrefab, _view.transform);
|
||||
_currentSubMenu.name = $"SubMenu_{expandableButtonModel.Text}";
|
||||
_ownerOfCurrentSubMenu = expandableButtonModel; // 이 메뉴를 연 버튼 모델을 기록합니다.
|
||||
|
||||
RectTransform panelRect = _currentSubMenu.GetComponent<RectTransform>();
|
||||
if (panelRect == null)
|
||||
{
|
||||
Debug.LogError("ToolbarView: subMenuPanelPrefab에 RectTransform이 없습니다.", _currentSubMenu);
|
||||
Object.Destroy(_currentSubMenu);
|
||||
_currentSubMenu = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 하위 메뉴 패널의 위치와 피벗을 설정합니다.
|
||||
RectTransform expandableButtonRect = expandableButtonObj.GetComponent<RectTransform>();
|
||||
panelRect.anchorMin = new Vector2(0, 1);
|
||||
panelRect.anchorMax = new Vector2(0, 1);
|
||||
panelRect.pivot = new Vector2(0, 1); // 좌상단 기준
|
||||
_currentSubMenu.SetActive(true);
|
||||
|
||||
// UI 계층에서 가장 마지막 자식으로 만들어 다른 UI 요소들 위에 표시되도록 합니다.
|
||||
if (panelRect.parent != null) panelRect.SetAsLastSibling();
|
||||
|
||||
// 하위 메뉴 패널에 LayoutGroup이 있는지 확인하고, 없다면 기본값으로 추가합니다.
|
||||
LayoutGroup subMenuLayoutGroup = _currentSubMenu.GetComponent<LayoutGroup>();
|
||||
if (subMenuLayoutGroup == null)
|
||||
{
|
||||
subMenuLayoutGroup = _currentSubMenu.AddComponent<VerticalLayoutGroup>();
|
||||
}
|
||||
|
||||
// 툴바의 레이아웃 방향(수직/수평)에 따라 하위 메뉴의 위치를 조정합니다.
|
||||
Vector2 offset = new Vector2(expandableButtonRect.anchoredPosition.x, expandableButtonRect.anchoredPosition.y);
|
||||
if (_view.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;
|
||||
|
||||
// 확장 버튼 모델에 포함된 모든 하위 버튼들을 UI로 생성합니다.
|
||||
foreach (var subItemBase in expandableButtonModel.SubButtons)
|
||||
{
|
||||
if (subItemBase is ToolbarButtonBase subItemModel)
|
||||
{
|
||||
// 하위 버튼 타입에 맞는 UI 처리기를 가져옵니다.
|
||||
IButtonViewProcessor subProcessor = _view.GetButtonViewProcessor(subItemModel.GetType());
|
||||
if (subProcessor == null)
|
||||
{
|
||||
Debug.LogWarning($"하위 버튼 '{subItemModel.Text}'에 대한 ViewProcessor를 찾을 수 없습니다.", _view);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 하위 버튼 UI를 생성합니다.
|
||||
GameObject subButtonObj = subProcessor.CreateButtonUI(subItemModel, panelRect, _view);
|
||||
if (subButtonObj == null)
|
||||
{
|
||||
Debug.LogError($"하위 버튼 '{subItemModel.Text}'의 UI를 생성하지 못했습니다.", _view);
|
||||
continue;
|
||||
}
|
||||
|
||||
subButtonObj.name = $"SubItem_{subItemModel.Text}";
|
||||
|
||||
// 하위 버튼의 상호작용을 설정합니다. (클릭 이벤트 등)
|
||||
subProcessor.SetupButtonInteractions(subItemModel, subButtonObj, _view);
|
||||
|
||||
// 하위 버튼의 공통 시각적 요소(텍스트, 아이콘 등)를 업데이트합니다.
|
||||
_view.InternalUpdateCommonButtonVisuals(subItemModel, subButtonObj);
|
||||
|
||||
// 하위 버튼의 클릭 이벤트에 추가적인 로직을 연결합니다.
|
||||
// (하위 메뉴를 닫고, 주 확장 버튼의 상태를 업데이트하는 등)
|
||||
var buttonComponent = subButtonObj.GetComponent<Button>();
|
||||
if (buttonComponent != null)
|
||||
{
|
||||
// 기존 리스너를 모두 지우고 새로 설정하여 중복을 방지합니다.
|
||||
buttonComponent.onClick.RemoveAllListeners();
|
||||
buttonComponent.onClick.AddListener(() =>
|
||||
{
|
||||
// 1. 주 확장 버튼 모델에 하위 버튼이 선택되었음을 알립니다.
|
||||
// 이를 통해 주 버튼의 아이콘이나 텍스트가 선택된 하위 버튼의 것으로 변경될 수 있습니다.
|
||||
expandableButtonModel.SelectSubButton(subItemModel);
|
||||
|
||||
// 2. 하위 버튼 자체의 커맨드를 실행합니다.
|
||||
subItemModel.ExecuteClick();
|
||||
|
||||
// 3. 하위 메뉴를 닫습니다.
|
||||
CloseSubMenu();
|
||||
|
||||
// 4. 툴팁이 남아있을 경우를 대비해 숨깁니다.
|
||||
if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized)
|
||||
{
|
||||
TooltipManager.Instance.HideTooltip();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var toggleComponent = subButtonObj.GetComponent<Toggle>();
|
||||
if (toggleComponent != null)
|
||||
{
|
||||
toggleComponent.onValueChanged.RemoveAllListeners();
|
||||
toggleComponent.onValueChanged.AddListener((isSelected) =>
|
||||
{
|
||||
if (isSelected) // 토글은 선택될 때만 동작하도록 처리
|
||||
{
|
||||
expandableButtonModel.SelectSubButton(subItemModel);
|
||||
subItemModel.ExecuteClick();
|
||||
CloseSubMenu();
|
||||
if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized)
|
||||
{
|
||||
TooltipManager.Instance.HideTooltip();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 모든 하위 버튼이 추가된 후, 레이아웃 그룹의 크기를 자식 요소에 맞게 조절합니다.
|
||||
if (subMenuLayoutGroup != null)
|
||||
{
|
||||
subMenuLayoutGroup.FitToChildren();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 열려있는 하위 메뉴를 닫습니다.
|
||||
/// </summary>
|
||||
public void CloseSubMenu()
|
||||
{
|
||||
DestroyCurrentSubMenuAndClearListeners();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 열려있는 하위 메뉴의 모든 UI 요소와 이벤트 리스너를 정리하고 GameObject를 파괴합니다.
|
||||
/// </summary>
|
||||
private void DestroyCurrentSubMenuAndClearListeners()
|
||||
{
|
||||
if (_currentSubMenu == null) return;
|
||||
|
||||
// 하위 메뉴에 있는 모든 버튼의 클릭 리스너를 제거합니다.
|
||||
Button[] subButtons = _currentSubMenu.GetComponentsInChildren<Button>(true);
|
||||
foreach (Button btn in subButtons)
|
||||
{
|
||||
btn.onClick.RemoveAllListeners();
|
||||
}
|
||||
|
||||
// 하위 메뉴에 있는 모든 토글의 값 변경 리스너를 제거합니다.
|
||||
Toggle[] subToggles = _currentSubMenu.GetComponentsInChildren<Toggle>(true);
|
||||
foreach (Toggle tgl in subToggles)
|
||||
{
|
||||
tgl.onValueChanged.RemoveAllListeners();
|
||||
}
|
||||
|
||||
// 하위 메뉴에 있는 모든 툴팁 핸들러의 액션을 정리합니다.
|
||||
TooltipHandler[] subTooltipHandlers = _currentSubMenu.GetComponentsInChildren<TooltipHandler>(true);
|
||||
foreach (TooltipHandler handler in subTooltipHandlers)
|
||||
{
|
||||
handler.OnPointerEnterAction = null;
|
||||
handler.OnPointerExitAction = null;
|
||||
}
|
||||
|
||||
// 하위 메뉴 GameObject를 파괴합니다.
|
||||
Object.Destroy(_currentSubMenu);
|
||||
_currentSubMenu = null;
|
||||
_ownerOfCurrentSubMenu = null; // 메뉴 소유자 참조도 초기화합니다.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매 프레임 호출되어야 하는 로직을 처리합니다.
|
||||
/// 현재는 하위 메뉴 외부를 클릭했을 때 메뉴를 닫는 기능을 수행합니다.
|
||||
/// </summary>
|
||||
public void Update()
|
||||
{
|
||||
// 마우스 왼쪽 버튼이 클릭되었고, 하위 메뉴가 열려있는 상태일 때
|
||||
if (Input.GetMouseButtonDown(0) && _currentSubMenu != null && _currentSubMenu.activeSelf && _view.rootCanvas != null)
|
||||
{
|
||||
RectTransform subMenuRect = _currentSubMenu.GetComponent<RectTransform>();
|
||||
if (subMenuRect == null) return;
|
||||
|
||||
// 캔버스의 렌더 모드에 따라 이벤트 카메라를 가져옵니다.
|
||||
Camera eventCamera = null;
|
||||
if (_view.rootCanvas.renderMode == RenderMode.ScreenSpaceCamera || _view.rootCanvas.renderMode == RenderMode.WorldSpace)
|
||||
{
|
||||
eventCamera = _view.rootCanvas.worldCamera;
|
||||
}
|
||||
|
||||
// 마우스 포인터가 하위 메뉴 영역 바깥에 있는지 확인합니다.
|
||||
if (!RectTransformUtility.RectangleContainsScreenPoint(subMenuRect, Input.mousePosition, eventCamera))
|
||||
{
|
||||
// 바깥을 클릭했다면 하위 메뉴를 닫습니다.
|
||||
CloseSubMenu();
|
||||
if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized)
|
||||
{
|
||||
TooltipManager.Instance.HideTooltip();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3d02215318a5a34f942ce2fd67dd75e
|
||||
136
Assets/Scripts/UVC/UI/ToolBar/View/ToggleGroupManager.cs
Normal file
136
Assets/Scripts/UVC/UI/ToolBar/View/ToggleGroupManager.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace UVC.UI.Toolbar.View
|
||||
{
|
||||
/// <summary>
|
||||
/// 툴바 내의 라디오 버튼 그룹(UnityEngine.UI.ToggleGroup)을 생성하고 관리하는 헬퍼 클래스입니다.
|
||||
/// ToolbarView로부터 ToggleGroup 관련 로직을 위임받아 처리함으로써 ToolbarView의 복잡도를 낮추고 역할을 분리합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 주요 역할:
|
||||
/// - ToggleGroup 동적 생성: 라디오 버튼 모델의 GroupName을 기반으로 해당하는 ToggleGroup을 찾거나 새로 생성합니다.
|
||||
/// - 리소스 관리: 생성된 ToggleGroup GameObject들을 내부적으로 관리하며, 툴바가 정리될 때 함께 파괴하여 메모리 누수를 방지합니다.
|
||||
/// - 레이아웃 독립성: 생성된 ToggleGroup GameObject가 툴바의 UI 레이아웃에 영향을 주지 않도록 LayoutElement 컴포넌트를 추가하고 `ignoreLayout`을 true로 설정합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 이 클래스는 ToolbarView 내부에서 다음과 같이 사용됩니다.
|
||||
///
|
||||
/// // 1. ToolbarView의 Awake 메서드에서 ToggleGroupManager 인스턴스 생성
|
||||
/// // public class ToolbarView : MonoBehaviour
|
||||
/// // {
|
||||
/// // public Transform toolbarContainer;
|
||||
/// // private ToggleGroupManager _toggleGroupManager;
|
||||
/// //
|
||||
/// // void Awake()
|
||||
/// // {
|
||||
/// // _toggleGroupManager = new ToggleGroupManager(toolbarContainer);
|
||||
/// // }
|
||||
/// // // ...
|
||||
/// // }
|
||||
///
|
||||
/// // 2. 라디오 버튼(RadioButton)의 UI 프로세서에서 `GetOrCreateToggleGroup` 호출
|
||||
/// // public class ToolbarRadioButtonViewProcessor : IButtonViewProcessor
|
||||
/// // {
|
||||
/// // public void SetupButtonInteractions(ToolbarButtonBase buttonModel, GameObject buttonUIObject, ToolbarView viewContext)
|
||||
/// // {
|
||||
/// // ToolbarRadioButton radioModel = buttonModel as ToolbarRadioButton;
|
||||
/// // Toggle uiToggle = buttonUIObject.GetComponent<Toggle>();
|
||||
/// // if (uiToggle != null)
|
||||
/// // {
|
||||
/// // // viewContext의 GetOrCreateToggleGroup을 호출하면 내부적으로 ToggleGroupManager의 메서드가 실행됩니다.
|
||||
/// // uiToggle.group = viewContext.GetOrCreateToggleGroup(radioModel.GroupName);
|
||||
/// // }
|
||||
/// // }
|
||||
/// // }
|
||||
///
|
||||
/// // 3. 툴바를 정리(Clear)할 때 Manager의 Clear 메서드 호출
|
||||
/// // public class ToolbarView : MonoBehaviour
|
||||
/// // {
|
||||
/// // protected virtual void ClearToolbar()
|
||||
/// // {
|
||||
/// // // ... 다른 UI 요소 정리 ...
|
||||
/// // _toggleGroupManager.Clear(); // 생성된 모든 토글 그룹 GameObject들을 파괴
|
||||
/// // }
|
||||
/// // }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class ToggleGroupManager
|
||||
{
|
||||
private readonly Transform _container;
|
||||
private readonly Dictionary<string, ToggleGroup> _toggleGroups = new Dictionary<string, ToggleGroup>();
|
||||
|
||||
/// <summary>
|
||||
/// ToggleGroupManager의 새 인스턴스를 초기화합니다.
|
||||
/// </summary>
|
||||
/// <param name="container">생성될 ToggleGroup GameObject들이 자식으로 추가될 부모 Transform입니다. 보통 툴바의 컨테이너를 지정합니다.</param>
|
||||
public ToggleGroupManager(Transform container)
|
||||
{
|
||||
_container = container;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 그룹 이름에 해당하는 UnityEngine.UI.ToggleGroup을 가져오거나, 없으면 새로 생성합니다.
|
||||
/// </summary>
|
||||
/// <param name="groupName">찾거나 생성할 토글 그룹의 이름입니다.</param>
|
||||
/// <returns>요청된 이름에 해당하는 ToggleGroup 컴포넌트입니다.</returns>
|
||||
public ToggleGroup GetOrCreateToggleGroup(string groupName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(groupName))
|
||||
{
|
||||
Debug.LogError("ToggleGroupManager: GetOrCreateToggleGroup - groupName이 null이거나 비어있습니다.");
|
||||
// 그룹 이름이 없는 비정상적인 경우를 위해 임시 그룹을 생성하여 반환합니다.
|
||||
GameObject tempGroupObj = new GameObject($"ToggleGroup_UnnamedDynamic");
|
||||
tempGroupObj.transform.SetParent(_container, false);
|
||||
return tempGroupObj.AddComponent<ToggleGroup>();
|
||||
}
|
||||
|
||||
// 캐시된 딕셔너리에서 그룹 이름으로 ToggleGroup을 찾아봅니다.
|
||||
if (!_toggleGroups.TryGetValue(groupName, out ToggleGroup group))
|
||||
{
|
||||
// 해당하는 ToggleGroup이 없으면 새로 생성합니다.
|
||||
GameObject groupObj = new GameObject($"ToggleGroup_{groupName}");
|
||||
groupObj.transform.SetParent(_container);
|
||||
|
||||
// RectTransform을 추가하고 크기를 0으로 설정하여 레이아웃에 영향을 주지 않도록 합니다.
|
||||
RectTransform groupRect = groupObj.AddComponent<RectTransform>();
|
||||
groupRect.sizeDelta = Vector2.zero;
|
||||
groupRect.anchoredPosition = Vector2.zero;
|
||||
groupRect.anchorMin = Vector2.zero;
|
||||
groupRect.anchorMax = Vector2.one;
|
||||
|
||||
// ToggleGroup 컴포넌트를 추가합니다.
|
||||
group = groupObj.AddComponent<ToggleGroup>();
|
||||
group.allowSwitchOff = false; // 라디오 버튼 그룹은 일반적으로 항상 하나가 선택된 상태를 유지해야 하므로, 선택 해제를 허용하지 않습니다.
|
||||
|
||||
// LayoutElement를 추가하고 ignoreLayout을 true로 설정하여 부모의 LayoutGroup 계산에서 이 GameObject를 무시하도록 합니다.
|
||||
LayoutElement element = groupObj.AddComponent<LayoutElement>();
|
||||
element.ignoreLayout = true;
|
||||
|
||||
// 새로 생성된 그룹을 딕셔너리에 추가하여 다음 요청 시 재사용할 수 있도록 합니다.
|
||||
_toggleGroups.Add(groupName, group);
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이 매니저를 통해 생성된 모든 ToggleGroup GameObject들을 파괴하고 내부 캐시를 비웁니다.
|
||||
/// 툴바가 다시 렌더링되거나 파괴될 때 호출하여 메모리 누수를 방지합니다.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
if (_toggleGroups == null) return;
|
||||
foreach (var groupObj in _toggleGroups.Values)
|
||||
{
|
||||
if (groupObj != null && groupObj.gameObject != null)
|
||||
{
|
||||
// 생성했던 GameObject를 파괴합니다.
|
||||
Object.Destroy(groupObj.gameObject);
|
||||
}
|
||||
}
|
||||
_toggleGroups.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f9b063a5e328b734fa10fbf1d06bde5c
|
||||
@@ -1,7 +1,6 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UVC.UI.Toolbar.Model;
|
||||
using UVC.UI.ToolBar.View;
|
||||
|
||||
namespace UVC.UI.Toolbar.View
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UVC.UI.Toolbar.Model;
|
||||
using UVC.UI.ToolBar.View;
|
||||
|
||||
namespace UVC.UI.Toolbar.View
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UVC.UI.Toolbar.Model;
|
||||
using UVC.UI.ToolBar.View;
|
||||
|
||||
namespace UVC.UI.Toolbar.View
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UVC.UI.Toolbar.Model;
|
||||
using UVC.UI.ToolBar.View;
|
||||
|
||||
namespace UVC.UI.Toolbar.View
|
||||
{
|
||||
|
||||
@@ -5,8 +5,6 @@ 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
|
||||
@@ -23,8 +21,7 @@ namespace UVC.UI.Toolbar.View
|
||||
/// 또한, 모델의 OnStateChanged, OnToggleStateChanged 등의 이벤트를 구독하여 모델 상태 변경 시 UI를 업데이트합니다.
|
||||
/// - UI 업데이트: 모델의 상태(텍스트, 아이콘, 활성화 상태, 선택 상태 등)가 변경되면 해당 UI 요소의 시각적 표현을 업데이트합니다.
|
||||
/// - 리소스 관리: 생성된 UI GameObject들과 이벤트 구독을 관리하며, 툴바가 파괴되거나 다시 렌더링될 때 정리합니다(ClearToolbar).
|
||||
/// - 하위 메뉴 관리: ToolbarExpandableButton 클릭 시 하위 메뉴 UI를 생성하고 토글합니다.
|
||||
/// - 툴팁 관리: 각 버튼에 TooltipHandler를 추가하여 툴팁 기능을 제공합니다.
|
||||
/// - 하위 로직 위임: 라디오 버튼 그룹 관리는 `ToggleGroupManager`에, 확장 버튼의 하위 메뉴 관리는 `SubMenuHandler`에 위임하여 클래스의 복잡도를 낮춥니다.
|
||||
///
|
||||
/// 이 클래스의 인스턴스는 Unity 씬 내의 GameObject에 컴포넌트로 추가되어야 하며,
|
||||
/// Inspector를 통해 필요한 프리팹들(standardButtonPrefab, toggleButtonPrefab 등)과
|
||||
@@ -40,67 +37,66 @@ namespace UVC.UI.Toolbar.View
|
||||
/// // - 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);
|
||||
/// // - Toolbar Container: 생성된 툴바 항목 UI들이 자식으로 추가될 Transform
|
||||
/// // - Layout Group: Toolbar Container에 연결된 LayoutGroup (예: VerticalLayoutGroup)
|
||||
///
|
||||
/// // 3. 툴바를 제어하는 다른 스크립트(예: UIManager, ToolbarController)에서 아래와 같이 사용합니다.
|
||||
/// public class MyToolbarController : MonoBehaviour
|
||||
/// {
|
||||
/// public ToolbarView toolbarView; // Inspector에서 ToolbarView 할당
|
||||
///
|
||||
/// void Start()
|
||||
/// {
|
||||
/// // 3-1. 툴바에 표시할 데이터 모델(ToolbarModel)을 생성하고 설정합니다.
|
||||
/// ToolbarModel myModel = new ToolbarModel();
|
||||
///
|
||||
/// // 표준 버튼 추가
|
||||
/// myModel.AddStandardButton("새 파일", "icons/new_file", null, "새 파일을 생성합니다.");
|
||||
///
|
||||
/// // 구분선 추가
|
||||
/// myModel.AddSeparator();
|
||||
///
|
||||
/// // 토글 버튼 추가
|
||||
/// myModel.AddToggleButton("그리드 보기", false, "icons/grid_on", "icons/grid_off",
|
||||
/// (isSelected) => Debug.Log($"그리드 표시: {isSelected}"));
|
||||
///
|
||||
/// // 라디오 버튼 그룹 추가
|
||||
/// string viewModeGroup = "ViewMode";
|
||||
/// myModel.AddRadioButton(viewModeGroup, "2D 보기", true, "icons/view_2d");
|
||||
/// myModel.AddRadioButton(viewModeGroup, "3D 보기", false, "icons/view_3d");
|
||||
///
|
||||
/// // 3-2. 설정된 모델을 ToolbarView에 전달하여 UI를 생성하도록 합니다.
|
||||
/// toolbarView.Initialize(myModel);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class ToolbarView : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// 현재 툴바 UI의 기반이 되는 데이터 모델입니다.
|
||||
/// Initialize 메서드를 통해 외부(주로 Toolbar)에서 주입받습니다.
|
||||
/// Initialize 메서드를 통해 외부에서 주입받습니다.
|
||||
/// </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;
|
||||
|
||||
@@ -112,73 +108,77 @@ namespace UVC.UI.Toolbar.View
|
||||
|
||||
/// <summary>
|
||||
/// 버튼 타입별 UI 처리기를 저장하는 딕셔너리입니다.
|
||||
/// 각 버튼 타입(표준, 토글 등)에 맞는 IButtonViewProcessor 구현체를 등록하여 사용합니다.
|
||||
/// </summary>
|
||||
private Dictionary<System.Type, IButtonViewProcessor> _buttonViewProcessors = new Dictionary<System.Type, IButtonViewProcessor>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 툴팁 표시에 사용될 루트 Canvas입니다.
|
||||
/// Inspector에서 할당하거나, Awake에서 자동으로 찾으려고 시도합니다.
|
||||
/// </summary>
|
||||
protected Canvas rootCanvas;
|
||||
internal Canvas rootCanvas;
|
||||
|
||||
// --- 헬퍼 클래스 ---
|
||||
private ToggleGroupManager _toggleGroupManager;
|
||||
private SubMenuHandler _subMenuHandler;
|
||||
|
||||
/// <summary>
|
||||
/// MonoBehaviour의 Awake 메서드입니다.
|
||||
/// toolbarContainer, layoutGroup, rootCanvas 등의 필수 참조를 초기화하고,
|
||||
/// TooltipManager를 초기화합니다.
|
||||
/// 필수 참조를 확인 및 초기화하고, 버튼 뷰 프로세서와 헬퍼 클래스들을 준비합니다.
|
||||
/// </summary>
|
||||
protected virtual void Awake()
|
||||
{
|
||||
|
||||
// 필수 프리팹들이 Inspector에서 할당되었는지 확인합니다.
|
||||
if (standardButtonPrefab == null || toggleButtonPrefab == null || radioButtonPrefab == null ||
|
||||
expandableButtonPrefab == null || separatorPrefab == null || subMenuPanelPrefab == null)
|
||||
{
|
||||
Debug.LogError("ToolbarView: 필수 프리팹이 할당되지 않았습니다. Inspector에서 모든 프리팹을 설정해주세요.", this);
|
||||
}
|
||||
|
||||
// toolbarContainer 자동 할당 (Inspector에서 할당되지 않은 경우)
|
||||
// UI 컨테이너와 레이아웃 그룹을 자동으로 찾거나 설정합니다.
|
||||
if (toolbarContainer == null) toolbarContainer = GetComponent<Transform>();
|
||||
if (toolbarContainer == null) toolbarContainer = GetComponentInChildren<Transform>(true); // 비활성 자식도 포함
|
||||
if (toolbarContainer == null) Debug.LogError("ToolbarView: toolbarContainer가 할당되지 않았습니다. Inspector에서 설정하거나 현재 GameObject에 Transform이 있어야 합니다.", this);
|
||||
if (toolbarContainer == null) toolbarContainer = GetComponentInChildren<Transform>(true);
|
||||
if (toolbarContainer == null) Debug.LogError("ToolbarView: toolbarContainer가 할당되지 않았습니다.", 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);
|
||||
if (layoutGroup == null) Debug.LogError("ToolbarView: layoutGroup이 할당되지 않았습니다.", this);
|
||||
|
||||
|
||||
// rootCanvas 자동 찾기 (TooltipManager 초기화에 필요)
|
||||
// 툴팁 표시에 필요한 루트 캔버스를 찾습니다.
|
||||
if (rootCanvas == null) rootCanvas = GetComponentInParent<Canvas>();
|
||||
if (rootCanvas == null)
|
||||
{
|
||||
Canvas[] canvases = FindObjectsByType<Canvas>(FindObjectsSortMode.None); // Unity 2023부터 FindObjectsByType 사용 권장
|
||||
Canvas[] canvases = FindObjectsByType<Canvas>(FindObjectsSortMode.None);
|
||||
foreach (Canvas c in canvases) { if (c.isRootCanvas) { rootCanvas = c; break; } }
|
||||
if (rootCanvas == null && canvases.Length > 0) rootCanvas = canvases[0]; // 최후의 수단
|
||||
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를 초기화할 수 없습니다.");
|
||||
Debug.LogError("ToolbarView: rootCanvas를 찾을 수 없어 TooltipManager를 초기화할 수 없습니다.");
|
||||
}
|
||||
|
||||
// 버튼 뷰 처리기 등록
|
||||
// 각 버튼 타입에 대한 뷰 프로세서를 등록합니다.
|
||||
_buttonViewProcessors[typeof(ToolbarStandardButton)] = new ToolbarStandardButtonViewProcessor();
|
||||
_buttonViewProcessors[typeof(ToolbarToggleButton)] = new ToolbarToggleButtonViewProcessor();
|
||||
_buttonViewProcessors[typeof(ToolbarRadioButton)] = new ToolbarRadioButtonViewProcessor();
|
||||
_buttonViewProcessors[typeof(ToolbarExpandableButton)] = new ToolbarExpandableButtonViewProcessor();
|
||||
// 새로운 버튼 타입이 추가되면 여기에 처리기를 등록합니다.
|
||||
|
||||
// 헬퍼 클래스들을 초기화합니다.
|
||||
_toggleGroupManager = new ToggleGroupManager(toolbarContainer);
|
||||
_subMenuHandler = new SubMenuHandler(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -190,19 +190,19 @@ namespace UVC.UI.Toolbar.View
|
||||
{
|
||||
this.ToolbarModel = toolbarModel;
|
||||
|
||||
// 필수 조건들을 다시 한 번 확인합니다.
|
||||
if (standardButtonPrefab == null || toggleButtonPrefab == null || radioButtonPrefab == null ||
|
||||
expandableButtonPrefab == null || separatorPrefab == null || subMenuPanelPrefab == null)
|
||||
{
|
||||
Debug.LogError("ToolbarView: 필수 프리팹이 할당되지 않았습니다. Inspector에서 모든 프리팹을 설정해주세요.", this);
|
||||
Debug.LogError("ToolbarView: 필수 프리팹이 할당되지 않았습니다.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (toolbarContainer == null)
|
||||
{
|
||||
Debug.LogError("ToolbarView: Initialize 실패. toolbarContainer가 할당되지 않았습니다.", this);
|
||||
return;
|
||||
}
|
||||
if (layoutGroup == null) // layoutGroup도 필수적이므로 확인
|
||||
if (layoutGroup == null)
|
||||
{
|
||||
Debug.LogError("ToolbarView: Initialize 실패. layoutGroup이 할당되지 않았습니다.", this);
|
||||
return;
|
||||
@@ -210,20 +210,17 @@ namespace UVC.UI.Toolbar.View
|
||||
if (this.ToolbarModel == null)
|
||||
{
|
||||
Debug.LogError("ToolbarView: Initialize 실패. 제공된 toolbarModel이 null입니다.", this);
|
||||
ClearToolbar(); // 모델이 없으면 기존 UI라도 정리
|
||||
ClearToolbar(); // 모델이 없으면 기존 UI라도 정리합니다.
|
||||
return;
|
||||
}
|
||||
|
||||
// 모든 조건이 충족되면 툴바 렌더링을 시작합니다.
|
||||
RenderToolbar();
|
||||
}
|
||||
|
||||
// 현재 열린 하위 메뉴의 원인 제공자(확장 버튼 모델) 추적용
|
||||
protected ToolbarExpandableButton _ownerOfCurrentSubMenu;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 툴바에 표시된 모든 UI 요소들을 제거하고, 관련된 이벤트 구독을 해제합니다.
|
||||
/// _modelToGameObjectMap, _toggleGroups를 비우고, 열려있는 하위 메뉴(currentSubMenu)도 파괴합니다.
|
||||
/// 툴팁도 숨깁니다.
|
||||
/// 툴바를 새로 그리거나 뷰가 파괴될 때 호출됩니다.
|
||||
/// </summary>
|
||||
protected virtual void ClearToolbar()
|
||||
{
|
||||
@@ -231,57 +228,39 @@ namespace UVC.UI.Toolbar.View
|
||||
{
|
||||
foreach (var pair in _modelToGameObjectMap)
|
||||
{
|
||||
if (pair.Key != null) // 모델(버튼)에 연결된 이벤트 구독 해제
|
||||
if (pair.Key != null)
|
||||
{
|
||||
// ToolbarButtonBase 및 파생 클래스에 정의된 ClearEventHandlers를 호출하여
|
||||
// 모델에 연결된 모든 이벤트 구독을 명시적으로 해제합니다.
|
||||
pair.Key.ClearEventHandlers();
|
||||
}
|
||||
if (pair.Value != null) // UI GameObject 파괴
|
||||
{
|
||||
// UI 컴포넌트의 이벤트 리스너 명시적 해제
|
||||
// ToggleButton 또는 RadioButton의 경우 Toggle 컴포넌트 리스너 해제
|
||||
if (pair.Value != null)
|
||||
{
|
||||
// UI 컴포넌트의 이벤트 리스너를 명시적으로 해제합니다.
|
||||
Toggle toggleComponent = pair.Value.GetComponent<Toggle>();
|
||||
if (toggleComponent != null)
|
||||
{
|
||||
toggleComponent.onValueChanged.RemoveAllListeners();
|
||||
}
|
||||
if (toggleComponent != null) toggleComponent.onValueChanged.RemoveAllListeners();
|
||||
|
||||
// StandardButton 또는 ExpandableButton의 경우 Button 컴포넌트 리스너 해제
|
||||
// (필요에 따라 다른 버튼 타입의 리스너도 유사하게 처리)
|
||||
Button buttonComponent = pair.Value.GetComponent<Button>();
|
||||
if (buttonComponent != null)
|
||||
{
|
||||
buttonComponent.onClick.RemoveAllListeners();
|
||||
}
|
||||
if (buttonComponent != null) buttonComponent.onClick.RemoveAllListeners();
|
||||
|
||||
TooltipHandler handler = pair.Value.GetComponent<TooltipHandler>();
|
||||
if (handler != null) // 툴팁 핸들러 이벤트도 정리 (필요시)
|
||||
if (handler != null)
|
||||
{
|
||||
handler.OnPointerEnterAction = null;
|
||||
handler.OnPointerExitAction = null;
|
||||
}
|
||||
// UI GameObject를 파괴합니다.
|
||||
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();
|
||||
}
|
||||
// 헬퍼 클래스들이 관리하던 리소스도 정리합니다.
|
||||
_toggleGroupManager?.Clear();
|
||||
_subMenuHandler?.CloseSubMenu();
|
||||
|
||||
if (currentSubMenu != null) // 열려있는 하위 메뉴 파괴
|
||||
{
|
||||
DestroyCurrentSubMenuAndClearListeners(); // 헬퍼 메서드 사용
|
||||
}
|
||||
|
||||
if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized) // 툴팁 숨기기
|
||||
// 툴팁이 화면에 남아있을 경우를 대비해 숨깁니다.
|
||||
if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized)
|
||||
{
|
||||
TooltipManager.Instance.HideTooltip();
|
||||
}
|
||||
@@ -292,24 +271,17 @@ namespace UVC.UI.Toolbar.View
|
||||
/// </summary>
|
||||
/// <param name="buttonModelType">버튼 모델의 System.Type 객체입니다.</param>
|
||||
/// <returns>해당 타입의 처리기. 없으면 null을 반환합니다.</returns>
|
||||
protected IButtonViewProcessor GetButtonViewProcessor(System.Type buttonModelType)
|
||||
internal 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()
|
||||
{
|
||||
@@ -322,41 +294,42 @@ namespace UVC.UI.Toolbar.View
|
||||
return;
|
||||
}
|
||||
|
||||
// 모델의 각 아이템을 순회하며 UI를 생성합니다.
|
||||
foreach (var itemModel in ToolbarModel.Items)
|
||||
{
|
||||
GameObject itemUIObject = null;// 생성될 UI GameObject
|
||||
GameObject itemUIObject = null;
|
||||
|
||||
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)
|
||||
{
|
||||
// 해당 버튼 타입의 프로세서를 사용하여 UI를 생성하고 설정합니다.
|
||||
itemUIObject = processor.CreateButtonUI(buttonModel, toolbarContainer, this);
|
||||
if (itemUIObject != null)
|
||||
{
|
||||
_modelToGameObjectMap[buttonModel] = itemUIObject;
|
||||
processor.SetupButtonInteractions(buttonModel, itemUIObject, this);
|
||||
|
||||
// 모델 상태 변경 이벤트 구독 (공통)
|
||||
// 모델의 상태 변경 이벤트를 구독하여 UI를 자동으로 업데이트하도록 합니다.
|
||||
buttonModel.OnStateChanged += () => processor.UpdateCommonButtonVisuals(buttonModel, itemUIObject, this);
|
||||
|
||||
// 토글 버튼/라디오 버튼의 경우 추가적인 이벤트 구독
|
||||
if (buttonModel is ToolbarToggleButton toggleModel) // ToolbarRadioButton도 ToolbarToggleButton을 상속
|
||||
if (buttonModel is ToolbarToggleButton toggleModel)
|
||||
{
|
||||
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 tooltipHandler = itemUIObject.GetComponent<TooltipHandler>() ?? itemUIObject.AddComponent<TooltipHandler>();
|
||||
tooltipHandler.Tooltip = buttonModel.Tooltip;
|
||||
if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized)
|
||||
{
|
||||
@@ -368,18 +341,16 @@ namespace UVC.UI.Toolbar.View
|
||||
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);
|
||||
Debug.LogWarning($"ToolbarView: 알 수 없는 IToolbarItem 타입 ({itemModel.GetType().Name})입니다.", this);
|
||||
}
|
||||
|
||||
if (itemUIObject != null)
|
||||
@@ -388,11 +359,9 @@ namespace UVC.UI.Toolbar.View
|
||||
}
|
||||
}
|
||||
|
||||
// 모든 UI 항목이 추가된 후 레이아웃 그룹 업데이트
|
||||
// 모든 UI 항목이 추가된 후 레이아웃을 강제로 업데이트합니다.
|
||||
if (layoutGroup != null)
|
||||
{
|
||||
// LayoutRebuilder.ForceRebuildLayoutImmediate(layoutGroup.GetComponent<RectTransform>()); // 즉시 업데이트
|
||||
// 또는 FitToChildren 확장 메서드 사용 (UVC.Extension에 정의되어 있다고 가정)
|
||||
layoutGroup.FitToChildren(width: layoutGroup is VerticalLayoutGroup, height: true);
|
||||
}
|
||||
}
|
||||
@@ -407,7 +376,7 @@ namespace UVC.UI.Toolbar.View
|
||||
{
|
||||
if (model == null || itemObj == null) return;
|
||||
|
||||
// 1. 텍스트 업데이트
|
||||
// 1. 텍스트 업데이트 (다국어 지원 포함)
|
||||
TextMeshProUGUI buttonTextComponent = itemObj.GetComponentInChildren<TextMeshProUGUI>(true);
|
||||
if (buttonTextComponent != null)
|
||||
{
|
||||
@@ -420,18 +389,12 @@ namespace UVC.UI.Toolbar.View
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
Image iconImageComponent = itemObj.transform.Find("Icon")?.GetComponent<Image>() ?? itemObj.GetComponentInChildren<Image>(true);
|
||||
if (iconImageComponent != null)
|
||||
{
|
||||
string iconPathToLoad = model.IconSpritePath;
|
||||
if (model is ToolbarToggleButton toggleButton)
|
||||
{
|
||||
iconPathToLoad = toggleButton.IsSelected ? toggleButton.IconSpritePath : toggleButton.OffIconSpritePath;
|
||||
}
|
||||
string iconPathToLoad = (model is ToolbarToggleButton toggleButton && !toggleButton.IsSelected)
|
||||
? toggleButton.OffIconSpritePath
|
||||
: model.IconSpritePath;
|
||||
|
||||
if (!string.IsNullOrEmpty(iconPathToLoad))
|
||||
{
|
||||
@@ -454,279 +417,20 @@ namespace UVC.UI.Toolbar.View
|
||||
}
|
||||
}
|
||||
|
||||
protected Dictionary<string, ToggleGroup> _toggleGroups = new Dictionary<string, ToggleGroup>();
|
||||
/// <summary>
|
||||
/// 라디오 버튼 그룹 이름에 해당하는 UnityEngine.UI.ToggleGroup을 가져오거나, 없으면 새로 생성합니다.
|
||||
/// 생성된 ToggleGroup GameObject는 toolbarContainer의 자식으로 추가되며, 레이아웃에 영향을 주지 않도록 설정됩니다.
|
||||
/// 라디오 버튼 그룹 이름에 해당하는 ToggleGroup을 가져오거나 생성합니다. (ToggleGroupManager에 위임)
|
||||
/// </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;
|
||||
return _toggleGroupManager.GetOrCreateToggleGroup(groupName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 열려있는 하위 메뉴(currentSubMenu)의 자식 UI 요소들의 이벤트 리스너를 제거하고,
|
||||
/// 하위 메뉴 GameObject를 파괴한 후 currentSubMenu 참조를 null로 설정합니다.
|
||||
/// 확장 버튼의 하위 메뉴를 토글합니다. (SubMenuHandler에 위임)
|
||||
/// </summary>
|
||||
protected void DestroyCurrentSubMenuAndClearListeners()
|
||||
{
|
||||
if (currentSubMenu == null) return;
|
||||
|
||||
// currentSubMenu의 자식들을 순회하며 Button 리스너 제거
|
||||
Button[] subButtons = currentSubMenu.GetComponentsInChildren<Button>(true);
|
||||
foreach (Button btn in subButtons)
|
||||
{
|
||||
btn.onClick.RemoveAllListeners();
|
||||
}
|
||||
|
||||
// currentSubMenu의 자식들을 순회하며 Toggle 리스너 제거
|
||||
Toggle[] subToggles = currentSubMenu.GetComponentsInChildren<Toggle>(true);
|
||||
foreach (Toggle tgl in subToggles)
|
||||
{
|
||||
tgl.onValueChanged.RemoveAllListeners();
|
||||
}
|
||||
|
||||
// currentSubMenu의 자식들에 있는 TooltipHandler 정리 (필요시)
|
||||
TooltipHandler[] subTooltipHandlers = currentSubMenu.GetComponentsInChildren<TooltipHandler>(true);
|
||||
foreach (TooltipHandler handler in subTooltipHandlers)
|
||||
{
|
||||
handler.OnPointerEnterAction = null;
|
||||
handler.OnPointerExitAction = null;
|
||||
}
|
||||
|
||||
Destroy(currentSubMenu);
|
||||
currentSubMenu = null;
|
||||
_ownerOfCurrentSubMenu = null; // 하위 메뉴 소유자 참조도 초기화
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
bool closeOnly = false;
|
||||
// 이미 다른 하위 메뉴가 열려있거나, 현재 클릭한 버튼의 하위 메뉴가 열려있다면 닫습니다.
|
||||
if (currentSubMenu != null)
|
||||
{
|
||||
// 현재 열린 하위 메뉴가 지금 클릭한 확장 버튼에 의해 열린 것인지 확인
|
||||
if (_ownerOfCurrentSubMenu == expandableButtonModel)
|
||||
{
|
||||
closeOnly = true; // 같은 버튼을 다시 클릭했으므로 닫기만 함
|
||||
}
|
||||
DestroyCurrentSubMenuAndClearListeners(); // 기존 하위 메뉴 정리 (리스너 포함)
|
||||
// 만약 현재 클릭한 버튼의 하위 메뉴가 이미 열려있어서 닫는 경우라면, 여기서 함수 종료
|
||||
// (다시 열리지 않도록). 이 로직은 currentSubMenu가 어떤 버튼에 의해 열렸는지 알아야 함.
|
||||
// 간단하게는, 어떤 확장 버튼이든 클릭하면 기존 서브메뉴는 닫고, 필요하면 새로 연다.
|
||||
// 지금은 그냥 닫고, 아래 로직에서 새로 열 조건이 되면 열도록 함.
|
||||
// 만약 클릭한 버튼이 이미 열린 메뉴의 주인이면, 그냥 닫기만 하고 리턴하는게 자연스러울 수 있음.
|
||||
// 이 부분은 UI/UX 정책에 따라 상세 조정 필요. 현재는 "토글"이므로, 다시 누르면 닫히고, 없으면 연다.
|
||||
// 즉, 아래 로직에서 currentSubMenu가 null이 되었으므로, 다시 열릴 수 있음.
|
||||
// 만약 "닫기만" 하고 싶다면, 여기서 return 해야 함.
|
||||
// 현재 로직: 이미 열려있으면 무조건 닫고, 아래에서 다시 열지 결정.
|
||||
// 수정 제안: 만약 currentSubMenu가 이 expandableButtonModel에 의해 열린 것이라면, 닫고 return.
|
||||
// (이를 위해서는 currentSubMenu가 어떤 모델에 연결되었는지 정보가 필요)
|
||||
// 현재는 간단히 토글로 동작: 열려있으면 닫고, 닫혀있으면 연다.
|
||||
// 즉, if (currentSubMenu != null) { Destroy; currentSubMenu = null; return; } 이면 닫기만 하고 끝.
|
||||
// 지금은 닫고, 아래에서 다시 열 수 있게 함.
|
||||
}
|
||||
|
||||
if (closeOnly)
|
||||
{
|
||||
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}";
|
||||
_ownerOfCurrentSubMenu = expandableButtonModel; // 새 하위 메뉴의 소유자 설정
|
||||
|
||||
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 필요 가정
|
||||
}
|
||||
_subMenuHandler.ToggleSubMenu(expandableButtonModel, expandableButtonObj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -736,68 +440,36 @@ namespace UVC.UI.Toolbar.View
|
||||
/// <returns>로드된 Sprite 객체. 실패 시 null을 반환합니다.</returns>
|
||||
protected Sprite LoadSpriteFromResources(string spritePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(spritePath))
|
||||
{
|
||||
Debug.LogWarning("LoadSpriteFromResources: spritePath가 null이거나 비어있습니다.");
|
||||
return null;
|
||||
}
|
||||
if (string.IsNullOrEmpty(spritePath)) return null;
|
||||
|
||||
Sprite loadedSprite = Resources.Load<Sprite>(spritePath);
|
||||
if (loadedSprite == null)
|
||||
{
|
||||
// 경로에 파일이 없을 경우 오류 로깅. 게임 빌드 시 Resources 폴더 내용이 포함되는지 확인 필요.
|
||||
Debug.LogError($"ToolbarView: Resources 폴더에서 '{spritePath}' 경로의 Sprite를 로드할 수 없습니다. 경로 및 파일 존재 여부를 확인하세요.", this);
|
||||
Debug.LogError($"ToolbarView: Resources 폴더에서 '{spritePath}' 경로의 Sprite를 로드할 수 없습니다.", this);
|
||||
}
|
||||
return loadedSprite;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MonoBehaviour의 Update 메서드입니다. 매 프레임 호출됩니다.
|
||||
/// 여기서는 열려있는 하위 메뉴(currentSubMenu) 외부를 클릭했을 때 메뉴를 닫는 로직을 처리합니다.
|
||||
/// SubMenuHandler의 Update를 호출하여 하위 메뉴 외부 클릭 감지 등을 처리합니다.
|
||||
/// </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))
|
||||
{
|
||||
// 하위 메뉴 영역 바깥을 클릭한 경우, 하위 메뉴를 닫습니다.
|
||||
DestroyCurrentSubMenuAndClearListeners();
|
||||
if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized) TooltipManager.Instance.HideTooltip();
|
||||
}
|
||||
}
|
||||
_subMenuHandler?.Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MonoBehaviour의 OnDestroy 메서드입니다. 이 컴포넌트 또는 GameObject가 파괴될 때 호출됩니다.
|
||||
/// 툴바 UI를 정리(ClearToolbar)하고, TooltipManager 등의 리소스를 해제(Dispose)합니다.
|
||||
/// 툴바 UI를 정리(ClearToolbar)하여 메모리 누수를 방지합니다.
|
||||
/// </summary>
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
ClearToolbar(); // 모든 UI 요소 및 이벤트 구독 해제
|
||||
{
|
||||
ClearToolbar();
|
||||
|
||||
// TooltipManager가 싱글톤이고 다른 곳에서도 사용된다면, 여기서 Dispose 호출은 신중해야 합니다.
|
||||
// 만약 이 ToolbarView가 TooltipManager의 유일한 사용자이거나,
|
||||
// 씬 전환 등으로 모든 관련 UI가 사라지는 상황이라면 적절할 수 있습니다.
|
||||
// 현재 TooltipManager.Instance.Dispose()는 TooltipVisualManager의 내용을 정리하는 것으로 보입니다.
|
||||
if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized)
|
||||
{
|
||||
// TooltipManager.Instance.Dispose(); // 주석 처리된 기존 코드. 필요시 활성화.
|
||||
// Dispose의 구체적인 내용에 따라 호출 여부 결정.
|
||||
// 만약 TooltipManager가 전역적이고 계속 사용되어야 한다면, 여기서 Dispose하면 안됨.
|
||||
// HideTooltip 정도가 적당할 수 있음.
|
||||
TooltipManager.Instance.HideTooltip();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user