Files
EnglewoodLAB/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarExpandableButton.cs

361 lines
12 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// 클릭 시 서브 메뉴를 펼치는 확장 버튼 컴포넌트입니다.
/// 서브 버튼 목록을 지연 생성(Lazy Loading)합니다.
/// </summary>
[UxmlElement]
public partial class UTKToolBarExpandableButton : UTKToolBarButtonBase
{
#region Fields
private VisualElement? _arrowIcon;
private VisualElement? _subMenuContainer;
private List<UTKToolBarButtonBase>? _subMenuItems;
private VisualTreeAsset? _cachedSubMenuAsset;
private StyleSheet? _cachedSubMenuUss;
private bool _subMenuCreated;
#endregion
#region Properties
/// <summary>서브 메뉴 열림 상태</summary>
public bool IsSubMenuOpen { get; private set; }
/// <summary>현재 툴바 배치 방향 (서브 메뉴 위치 계산용)</summary>
public UTKToolBarOrientation CurrentOrientation { get; set; } = UTKToolBarOrientation.Horizontal;
#endregion
#region Events
/// <summary>서브 메뉴 열림/닫힘 이벤트</summary>
public event Action<bool>? OnSubMenuToggled;
#endregion
#region Constructor
/// <summary>
/// UTKToolBarExpandableButton의 새 인스턴스를 초기화합니다.
/// </summary>
public UTKToolBarExpandableButton() : base()
{
_uxmlPath = "UIToolkit/ToolBar/UTKToolBarExpandableButton";
_ussPath = "UIToolkit/ToolBar/UTKToolBarExpandableButtonUss";
// 버튼 기본 USS도 로드
var buttonUss = Resources.Load<StyleSheet>("UIToolkit/ToolBar/UTKToolBarButtonUss");
if (buttonUss != null)
{
styleSheets.Add(buttonUss);
}
CreateUI();
}
#endregion
#region Setup
/// <summary>
/// UXML에서 UI 생성 후 화살표 아이콘 참조를 가져옵니다.
/// </summary>
/// <param name="asset">UXML 에셋</param>
protected override void CreateUIFromUxml(VisualTreeAsset asset)
{
base.CreateUIFromUxml(asset);
_arrowIcon = this.Q<VisualElement>("arrow");
}
/// <summary>
/// 코드 Fallback으로 UI 생성 시 화살표 아이콘을 추가합니다.
/// </summary>
protected override void CreateUIFallback()
{
base.CreateUIFallback();
_arrowIcon = new VisualElement();
_arrowIcon.AddToClassList("utk-toolbar-expandable__arrow");
_rootButton?.Add(_arrowIcon);
_rootButton?.AddToClassList("utk-toolbar-expandable");
}
#endregion
#region Click Handler
/// <summary>
/// 클릭 시 서브 메뉴를 토글합니다.
/// 서브 메뉴 영역 내 클릭은 무시합니다 (버블링 방지).
/// </summary>
/// <param name="evt">클릭 이벤트</param>
protected override void OnClick(ClickEvent evt)
{
if (_data == null || !_data.IsEnabled) return;
// 서브 메뉴 내부 클릭이면 무시 (서브 버튼이 자체 처리)
if (_subMenuContainer != null && evt.target is VisualElement target)
{
var ancestor = target;
while (ancestor != null)
{
if (ancestor == _subMenuContainer) return;
ancestor = ancestor.parent;
}
}
ToggleSubMenu();
RaiseOnButtonClicked(_data);
}
#endregion
#region Sub Menu
/// <summary>
/// 서브 메뉴를 토글합니다.
/// </summary>
public void ToggleSubMenu()
{
if (IsSubMenuOpen)
{
CloseSubMenu();
}
else
{
OpenSubMenu();
}
}
/// <summary>
/// 서브 메뉴를 엽니다. 처음 열 때 지연 생성합니다.
/// panel.visualTree에 서브 메뉴를 추가하여 overflow 제약 없이 표시합니다.
/// </summary>
private void OpenSubMenu()
{
if (_data is not UTKToolBarExpandableButtonData expandableData) return;
if (panel == null) return;
if (!_subMenuCreated)
{
CreateSubMenu(expandableData);
}
if (_subMenuContainer != null)
{
// panel.visualTree로 이동 (UTKDropdown 패턴)
if (_subMenuContainer.parent != panel.visualTree)
{
_subMenuContainer.RemoveFromHierarchy();
panel.visualTree.Add(_subMenuContainer);
UTKThemeManager.Instance.ApplyThemeToElement(_subMenuContainer);
if (_cachedSubMenuUss != null)
{
_subMenuContainer.styleSheets.Add(_cachedSubMenuUss);
}
}
_subMenuContainer.style.display = DisplayStyle.Flex;
PositionSubMenu();
}
IsSubMenuOpen = true;
OnSubMenuToggled?.Invoke(true);
}
/// <summary>
/// 서브 메뉴를 닫습니다.
/// 서브 메뉴를 원래 위치(this)로 되돌립니다.
/// </summary>
public void CloseSubMenu()
{
if (_subMenuContainer != null)
{
_subMenuContainer.style.display = DisplayStyle.None;
// panel.visualTree에서 제거하여 원래 위치로 되돌림
if (_subMenuContainer.parent != this)
{
_subMenuContainer.RemoveFromHierarchy();
Add(_subMenuContainer);
}
}
IsSubMenuOpen = false;
OnSubMenuToggled?.Invoke(false);
}
/// <summary>
/// 서브 메뉴를 생성합니다 (지연 로드).
/// 서브 메뉴는 초기에는 this에 추가되며, Open 시 panel.visualTree로 이동합니다.
/// </summary>
/// <param name="expandableData">확장 버튼 데이터</param>
private void CreateSubMenu(UTKToolBarExpandableButtonData expandableData)
{
_subMenuCreated = true;
// 서브 메뉴 리소스 캐싱
if (_cachedSubMenuAsset == null)
{
_cachedSubMenuAsset = Resources.Load<VisualTreeAsset>("UIToolkit/ToolBar/UTKToolBarSubMenu");
_cachedSubMenuUss = Resources.Load<StyleSheet>("UIToolkit/ToolBar/UTKToolBarSubMenuUss");
}
VisualElement? container;
if (_cachedSubMenuAsset != null)
{
var subMenuRoot = _cachedSubMenuAsset.Instantiate();
_subMenuContainer = subMenuRoot.Q<VisualElement>("submenu-root");
container = subMenuRoot.Q<VisualElement>("submenu-container");
if (_subMenuContainer != null)
{
// TemplateContainer에서 분리하여 직접 관리
_subMenuContainer.RemoveFromHierarchy();
}
else
{
_subMenuContainer = subMenuRoot;
}
}
else
{
// Fallback: 코드로 서브 메뉴 컨테이너 생성
_subMenuContainer = new VisualElement();
_subMenuContainer.AddToClassList("utk-toolbar-submenu");
container = new VisualElement();
container.AddToClassList("utk-toolbar-submenu__container");
_subMenuContainer.Add(container);
}
// 서브 메뉴를 닫힌 상태로 this에 추가 (Open 시 panel.visualTree로 이동)
_subMenuContainer.style.display = DisplayStyle.None;
Add(_subMenuContainer);
// 서브 버튼 생성
container ??= _subMenuContainer;
_subMenuItems = new List<UTKToolBarButtonBase>();
foreach (var subButtonData in expandableData.SubButtons)
{
var subButton = new UTKToolBarStandardButton();
subButton.BindData(subButtonData);
subButton.OnButtonClicked += OnSubButtonClicked;
container.Add(subButton);
_subMenuItems.Add(subButton);
}
}
/// <summary>
/// 서브 버튼 클릭 핸들러. 서브 메뉴를 닫고 선택을 반영합니다.
/// </summary>
/// <param name="clickedData">클릭된 서브 버튼 데이터</param>
private void OnSubButtonClicked(UTKToolBarButtonData clickedData)
{
if (_data is UTKToolBarExpandableButtonData expandableData)
{
expandableData.SelectSubButton(clickedData);
}
CloseSubMenu();
}
/// <summary>
/// 지정된 요소가 서브 메뉴 내부에 있는지 확인합니다.
/// panel.visualTree에 추가된 서브 메뉴의 외부 클릭 감지에 사용됩니다.
/// </summary>
/// <param name="target">확인할 요소</param>
/// <returns>서브 메뉴 내부이면 true</returns>
public bool IsInsideSubMenu(VisualElement target)
{
if (_subMenuContainer == null) return false;
var ancestor = target;
while (ancestor != null)
{
if (ancestor == _subMenuContainer) return true;
ancestor = ancestor.parent;
}
return false;
}
/// <summary>
/// 서브 메뉴 위치를 계산합니다 (가로/세로 배치 대응).
/// panel.visualTree에 추가된 상태이므로 worldBound 기준으로 절대 위치를 설정합니다.
/// </summary>
private void PositionSubMenu()
{
if (_subMenuContainer == null || _rootButton == null) return;
// schedule로 다음 프레임에 위치 계산 (레이아웃 완료 후)
schedule.Execute(() =>
{
if (_rootButton == null || _subMenuContainer == null) return;
var buttonBounds = _rootButton.worldBound;
_subMenuContainer.style.position = Position.Absolute;
if (CurrentOrientation == UTKToolBarOrientation.Horizontal)
{
// 가로 배치: 버튼 아래로 펼침
_subMenuContainer.style.left = buttonBounds.x;
_subMenuContainer.style.top = buttonBounds.yMax + 2;
}
else
{
// 세로 배치: 버튼 오른쪽으로 펼침
_subMenuContainer.style.left = buttonBounds.xMax + 2;
_subMenuContainer.style.top = buttonBounds.y;
}
});
}
#endregion
#region IDisposable
/// <summary>
/// 리소스를 정리합니다.
/// </summary>
public override void Dispose()
{
if (_disposed) return;
// 서브 메뉴 아이템 정리
if (_subMenuItems != null)
{
foreach (var item in _subMenuItems)
{
item.OnButtonClicked -= OnSubButtonClicked;
item.Dispose();
}
_subMenuItems.Clear();
_subMenuItems = null;
}
// panel.visualTree에 남아 있는 서브 메뉴 제거
_subMenuContainer?.RemoveFromHierarchy();
OnSubMenuToggled = null;
_subMenuContainer = null;
_arrowIcon = null;
base.Dispose();
}
#endregion
}
}