361 lines
12 KiB
C#
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
|
|
}
|
|
}
|