#nullable enable using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; namespace UVC.UIToolkit { /// /// UIToolkit 기반 툴바 메인 컴포넌트입니다. /// 가로/세로 배치를 지원하며, 4가지 버튼 타입과 구분선을 렌더링합니다. /// /// 주요 기능: /// /// 가로/세로 배치 전환 (Orientation) /// Standard, Toggle, Radio, Expandable 버튼 지원 /// 구분선 지원 /// 서브 메뉴 외부 클릭 감지 자동 닫기 /// 테마 변경 지원 (UTKThemeManager 연동) /// 성능 최적화 (리소스 캐싱, Dictionary 추적) /// /// /// /// /// // 1. 모델 생성 /// var model = new UTKToolBarModel(); /// model.AddStandardButton("Save", UTKMaterialIcons.Save); /// model.AddSeparator(); /// model.AddToggleButton("Grid", false, UTKMaterialIcons.GridOn, UTKMaterialIcons.GridOff); /// /// // 2. View 생성 및 추가 /// var toolbar = new UTKToolBar(); /// toolbar.Orientation = UTKToolBarOrientation.Horizontal; /// uiDocument.rootVisualElement.Add(toolbar); /// /// // 3. 툴바 빌드 /// toolbar.BuildToolBar(model); /// /// // 4. 이벤트 구독 /// toolbar.OnAction += args => Debug.Log($"{args.Text}: {args.ActionType}"); /// /// // 5. 배치 방향 변경 /// toolbar.SetOrientation(UTKToolBarOrientation.Vertical); /// /// [UxmlElement] public partial class UTKToolBar : VisualElement, IDisposable { #region Constants private const string UXML_PATH = "UIToolkit/ToolBar/UTKToolBar"; private const string USS_PATH = "UIToolkit/ToolBar/UTKToolBarUss"; #endregion #region Fields private UTKToolBarOrientation _orientation = UTKToolBarOrientation.Horizontal; private VisualElement? _itemContainer; private readonly List _itemElements = new(); private readonly Dictionary _buttonMap = new(); private readonly List _expandableButtons = new(); private bool _disposed; #endregion #region UXML Attributes /// /// 툴바 배치 방향. 변경 시 즉시 레이아웃이 전환됩니다. /// [UxmlAttribute("orientation")] public UTKToolBarOrientation Orientation { get => _orientation; set { if (_orientation != value) { _orientation = value; ApplyOrientation(); } } } /// 아이템 간 간격 (px) [UxmlAttribute("item-spacing")] public float ItemSpacing { get; set; } = 2f; /// 툴바 크기 (가로 시 높이, 세로 시 너비) [UxmlAttribute("toolbar-size")] public float ToolBarSize { get; set; } = 40f; #endregion #region Events /// 버튼 액션 이벤트 public event Action? OnAction; #endregion #region Constructor /// /// UTKToolBar의 새 인스턴스를 초기화합니다. /// public UTKToolBar() { // 1. 테마 적용 UTKThemeManager.Instance.ApplyThemeToElement(this); // 2. USS 로드 var uss = Resources.Load(USS_PATH); if (uss != null) { styleSheets.Add(uss); } // 3. UI 생성 CreateUI(); // 4. 배치 방향 적용 ApplyOrientation(); // 5. 테마 구독 SubscribeToThemeChanges(); } #endregion #region Setup /// /// UXML/USS 로드 및 UI 구성. /// private void CreateUI() { var asset = Resources.Load(UXML_PATH); if (asset != null) { var root = asset.Instantiate(); root.style.flexGrow = 1; _itemContainer = root.Q("toolbar-container"); Add(root); } else { // Fallback AddToClassList("utk-toolbar"); _itemContainer = new VisualElement(); _itemContainer.AddToClassList("utk-toolbar__container"); Add(_itemContainer); } } #endregion #region Public Methods /// /// 모델 데이터로 툴바를 생성합니다. /// /// 툴바 데이터 모델 public void BuildToolBar(UTKToolBarModel model) { ClearToolBar(); if (_itemContainer == null || model == null) return; foreach (var item in model.Items) { var element = CreateItemElement(item); if (element != null) { _itemContainer.Add(element); _itemElements.Add(element); } } // 아이템 간격 적용 ApplyItemSpacing(); } /// /// 툴바의 모든 아이템을 제거합니다. /// public void ClearToolBar() { // 서브 메뉴 닫기 CloseAllSubMenus(); // 버튼 정리 foreach (var kvp in _buttonMap) { kvp.Value.OnButtonClicked -= OnItemClicked; kvp.Value.Dispose(); } _buttonMap.Clear(); // 확장 버튼 리스트 정리 _expandableButtons.Clear(); // 구분선 정리 foreach (var element in _itemElements) { if (element is UTKToolBarSeparator separator) { separator.Dispose(); } } _itemElements.Clear(); _itemContainer?.Clear(); } /// /// 특정 버튼의 활성화 상태를 변경합니다. /// /// 아이템 ID /// 활성화 여부 public void SetButtonEnabled(string itemId, bool isEnabled) { if (_buttonMap.TryGetValue(itemId, out var button)) { button.SetDataEnabled(isEnabled); } } /// /// 배치 방향을 변경합니다. /// /// 새 배치 방향 public void SetOrientation(UTKToolBarOrientation orientation) { Orientation = orientation; } /// /// 특정 버튼 요소를 가져옵니다. /// /// 아이템 ID /// 찾은 버튼 (out) /// 존재 여부 public bool TryGetButtonElement(string itemId, out UTKToolBarButtonBase? button) { return _buttonMap.TryGetValue(itemId, out button); } #endregion #region Private Methods /// /// 배치 방향을 적용합니다. CSS 클래스를 전환합니다. /// private void ApplyOrientation() { RemoveFromClassList("utk-toolbar--horizontal"); RemoveFromClassList("utk-toolbar--vertical"); // 루트 요소에도 클래스 적용 (자손 선택자 동작을 위해) var root = this.Q("toolbar-root"); root?.RemoveFromClassList("utk-toolbar--horizontal"); root?.RemoveFromClassList("utk-toolbar--vertical"); if (_orientation == UTKToolBarOrientation.Horizontal) { AddToClassList("utk-toolbar--horizontal"); root?.AddToClassList("utk-toolbar--horizontal"); } else { AddToClassList("utk-toolbar--vertical"); root?.AddToClassList("utk-toolbar--vertical"); } // 확장 버튼의 방향 업데이트 foreach (var expandable in _expandableButtons) { expandable.CurrentOrientation = _orientation; } // 아이템 간격 재적용 (방향에 따라 margin 축이 변경됨) ApplyItemSpacing(); // 열린 서브 메뉴 닫기 (위치 재계산 필요) CloseAllSubMenus(); } /// /// 아이템 간격을 적용합니다. 첫 번째 아이템을 제외하고 방향에 맞는 margin을 설정합니다. /// private void ApplyItemSpacing() { bool isHorizontal = _orientation == UTKToolBarOrientation.Horizontal; for (int i = 0; i < _itemElements.Count; i++) { var element = _itemElements[i]; if (i == 0) { element.style.marginLeft = 0; element.style.marginTop = 0; } else { if (isHorizontal) { element.style.marginLeft = ItemSpacing; element.style.marginTop = 0; } else { element.style.marginTop = ItemSpacing; element.style.marginLeft = 0; } } } } /// /// 모델 아이템으로 개별 버튼/구분선을 생성합니다. /// /// 아이템 데이터 /// 생성된 VisualElement private VisualElement? CreateItemElement(IUTKToolBarItem item) { switch (item) { case UTKToolBarRadioButtonData radioData: { var button = new UTKToolBarRadioButton(); button.BindData(radioData); button.OnButtonClicked += OnItemClicked; _buttonMap[radioData.ItemId] = button; // 라디오 버튼 선택 변경 → OnAction 이벤트 radioData.OnToggleStateChanged += (isSelected) => { if (isSelected) { RaiseOnAction(radioData.GroupName, UTKToolBarActionType.Radio, radioData.Text); } }; return button; } case UTKToolBarToggleButtonData toggleData: { var button = new UTKToolBarToggleButton(); button.BindData(toggleData); button.OnButtonClicked += OnItemClicked; _buttonMap[toggleData.ItemId] = button; // 토글 상태 변경 → OnAction 이벤트 toggleData.OnToggleStateChanged += (isSelected) => { RaiseOnAction(toggleData.Text, UTKToolBarActionType.Toggle, isSelected); }; return button; } case UTKToolBarExpandableButtonData expandableData: { var button = new UTKToolBarExpandableButton(); button.CurrentOrientation = _orientation; button.BindData(expandableData); button.OnButtonClicked += OnItemClicked; _buttonMap[expandableData.ItemId] = button; _expandableButtons.Add(button); // 서브 버튼 선택 변경 → OnAction 이벤트 expandableData.OnSubButtonSelectionChanged += (mainText, subText) => { RaiseOnAction(mainText, UTKToolBarActionType.Expandable, subText); }; return button; } case UTKToolBarStandardButtonData standardData: { var button = new UTKToolBarStandardButton(); button.BindData(standardData); button.OnButtonClicked += OnItemClicked; _buttonMap[standardData.ItemId] = button; // 클릭 → OnAction 이벤트 standardData.OnClicked += () => { RaiseOnAction(standardData.Text, UTKToolBarActionType.Standard, null); }; return button; } case UTKToolBarSeparatorData: { var separator = new UTKToolBarSeparator(); return separator; } default: return null; } } /// /// 아이템 클릭 핸들러 (공통). /// /// 클릭된 버튼 데이터 private void OnItemClicked(UTKToolBarButtonData data) { if (data is UTKToolBarExpandableButtonData) { // 다른 Expandable 버튼의 서브 메뉴 닫기 (클릭한 것만 남김) foreach (var expandable in _expandableButtons) { if (expandable.IsSubMenuOpen && expandable.BoundData != data) { expandable.CloseSubMenu(); } } } else { // 일반 버튼 클릭 시, 열린 서브 메뉴 모두 닫기 CloseAllSubMenus(); } } /// /// 모든 열린 서브 메뉴를 닫습니다. /// private void CloseAllSubMenus() { foreach (var expandable in _expandableButtons) { if (expandable.IsSubMenuOpen) { expandable.CloseSubMenu(); } } } /// /// OnAction 이벤트를 발생시킵니다. /// /// 버튼 텍스트 /// 액션 타입 /// 액션 값 private void RaiseOnAction(string text, UTKToolBarActionType actionType, object? value = null) { OnAction?.Invoke(new UTKToolBarActionEventArgs { Text = text, ActionType = actionType, Value = value }); } /// /// 외부 클릭 감지 (서브 메뉴 닫기용). /// Panel의 PointerDownEvent를 캡처 단계에서 감지합니다. /// /// 포인터 다운 이벤트 private void OnPanelPointerDown(PointerDownEvent evt) { // 열린 서브 메뉴가 없으면 무시 bool hasOpenSubMenu = false; foreach (var expandable in _expandableButtons) { if (expandable.IsSubMenuOpen) { hasOpenSubMenu = true; break; } } if (!hasOpenSubMenu) return; var target = evt.target as VisualElement; if (target != null) { // 이 UTKToolBar 내부 클릭이면 무시 (개별 버튼이 처리) var ancestor = target; while (ancestor != null) { if (ancestor == this) return; ancestor = ancestor.parent; } // panel.visualTree에 추가된 서브 메뉴 내부 클릭이면 무시 foreach (var expandable in _expandableButtons) { if (expandable.IsSubMenuOpen && expandable.IsInsideSubMenu(target)) { return; } } } // 외부 클릭 → 모든 서브 메뉴 닫기 CloseAllSubMenus(); } #endregion #region Theme private void SubscribeToThemeChanges() { UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged; RegisterCallback(OnAttachToPanelForTheme); RegisterCallback(OnDetachFromPanelForTheme); } private void OnAttachToPanelForTheme(AttachToPanelEvent evt) { UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged; UTKThemeManager.Instance.ApplyThemeToElement(this); // 패널 레벨 외부 클릭 감지 등록 if (panel != null) { panel.visualTree.RegisterCallback(OnPanelPointerDown, TrickleDown.TrickleDown); } } private void OnDetachFromPanelForTheme(DetachFromPanelEvent evt) { UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; // 패널 레벨 외부 클릭 감지 해제 if (evt.originPanel?.visualTree != null) { evt.originPanel.visualTree.UnregisterCallback(OnPanelPointerDown, TrickleDown.TrickleDown); } } private void OnThemeChanged(UTKTheme theme) { UTKThemeManager.Instance.ApplyThemeToElement(this); } #endregion #region IDisposable /// /// 리소스를 정리합니다. /// public void Dispose() { if (_disposed) return; _disposed = true; ClearToolBar(); // 테마 구독 해제 UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged; UnregisterCallback(OnAttachToPanelForTheme); UnregisterCallback(OnDetachFromPanelForTheme); OnAction = null; } #endregion } }