#nullable enable using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; namespace UVC.UIToolkit { /// /// 클릭 시 서브 메뉴를 펼치는 확장 버튼 컴포넌트입니다. /// 서브 버튼 목록을 지연 생성(Lazy Loading)합니다. /// [UxmlElement] public partial class UTKToolBarExpandableButton : UTKToolBarButtonBase { #region Fields private VisualElement? _arrowIcon; private VisualElement? _subMenuContainer; private List? _subMenuItems; private VisualTreeAsset? _cachedSubMenuAsset; private StyleSheet? _cachedSubMenuUss; private bool _subMenuCreated; #endregion #region Properties /// 서브 메뉴 열림 상태 public bool IsSubMenuOpen { get; private set; } /// 현재 툴바 배치 방향 (서브 메뉴 위치 계산용) public UTKToolBarOrientation CurrentOrientation { get; set; } = UTKToolBarOrientation.Horizontal; #endregion #region Events /// 서브 메뉴 열림/닫힘 이벤트 public event Action? OnSubMenuToggled; #endregion #region Constructor /// /// UTKToolBarExpandableButton의 새 인스턴스를 초기화합니다. /// public UTKToolBarExpandableButton() : base() { _uxmlPath = "UIToolkit/ToolBar/UTKToolBarExpandableButton"; _ussPath = "UIToolkit/ToolBar/UTKToolBarExpandableButtonUss"; // 버튼 기본 USS도 로드 var buttonUss = Resources.Load("UIToolkit/ToolBar/UTKToolBarButtonUss"); if (buttonUss != null) { styleSheets.Add(buttonUss); } CreateUI(); } #endregion #region Setup /// /// UXML에서 UI 생성 후 화살표 아이콘 참조를 가져옵니다. /// /// UXML 에셋 protected override void CreateUIFromUxml(VisualTreeAsset asset) { base.CreateUIFromUxml(asset); _arrowIcon = this.Q("arrow"); } /// /// 코드 Fallback으로 UI 생성 시 화살표 아이콘을 추가합니다. /// 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 /// /// 클릭 시 서브 메뉴를 토글합니다. /// 서브 메뉴 영역 내 클릭은 무시합니다 (버블링 방지). /// /// 클릭 이벤트 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 /// /// 서브 메뉴를 토글합니다. /// public void ToggleSubMenu() { if (IsSubMenuOpen) { CloseSubMenu(); } else { OpenSubMenu(); } } /// /// 서브 메뉴를 엽니다. 처음 열 때 지연 생성합니다. /// panel.visualTree에 서브 메뉴를 추가하여 overflow 제약 없이 표시합니다. /// 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); } /// /// 서브 메뉴를 닫습니다. /// 서브 메뉴를 원래 위치(this)로 되돌립니다. /// 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); } /// /// 서브 메뉴를 생성합니다 (지연 로드). /// 서브 메뉴는 초기에는 this에 추가되며, Open 시 panel.visualTree로 이동합니다. /// /// 확장 버튼 데이터 private void CreateSubMenu(UTKToolBarExpandableButtonData expandableData) { _subMenuCreated = true; // 서브 메뉴 리소스 캐싱 if (_cachedSubMenuAsset == null) { _cachedSubMenuAsset = Resources.Load("UIToolkit/ToolBar/UTKToolBarSubMenu"); _cachedSubMenuUss = Resources.Load("UIToolkit/ToolBar/UTKToolBarSubMenuUss"); } VisualElement? container; if (_cachedSubMenuAsset != null) { var subMenuRoot = _cachedSubMenuAsset.Instantiate(); _subMenuContainer = subMenuRoot.Q("submenu-root"); container = subMenuRoot.Q("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(); foreach (var subButtonData in expandableData.SubButtons) { var subButton = new UTKToolBarStandardButton(); subButton.BindData(subButtonData); subButton.OnButtonClicked += OnSubButtonClicked; container.Add(subButton); _subMenuItems.Add(subButton); } } /// /// 서브 버튼 클릭 핸들러. 서브 메뉴를 닫고 선택을 반영합니다. /// /// 클릭된 서브 버튼 데이터 private void OnSubButtonClicked(UTKToolBarButtonData clickedData) { if (_data is UTKToolBarExpandableButtonData expandableData) { expandableData.SelectSubButton(clickedData); } CloseSubMenu(); } /// /// 지정된 요소가 서브 메뉴 내부에 있는지 확인합니다. /// panel.visualTree에 추가된 서브 메뉴의 외부 클릭 감지에 사용됩니다. /// /// 확인할 요소 /// 서브 메뉴 내부이면 true 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; } /// /// 서브 메뉴 위치를 계산합니다 (가로/세로 배치 대응). /// panel.visualTree에 추가된 상태이므로 worldBound 기준으로 절대 위치를 설정합니다. /// 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 /// /// 리소스를 정리합니다. /// 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 } }