diff --git a/Assets/Resources/UIToolkit/Menu/UTKMenuImageItemUss.uss b/Assets/Resources/UIToolkit/Menu/UTKMenuImageItemUss.uss index 1b18f213..ab44c0a8 100644 --- a/Assets/Resources/UIToolkit/Menu/UTKMenuImageItemUss.uss +++ b/Assets/Resources/UIToolkit/Menu/UTKMenuImageItemUss.uss @@ -41,13 +41,15 @@ .menu-item__arrow { width: 0; height: 0; - margin-left: 4px; - border-left-width: 4px; - border-right-width: 4px; - border-top-width: 4px; - border-bottom-width: 4px; - border-left-color: transparent; + margin-left: auto; + border-left-width: 5px; + border-right-width: 5px; + border-top-width: 5px; + border-bottom-width: 5px; + border-left-color: var(--color-text-primary); border-right-color: transparent; - border-top-color: var(--color-text-primary); + border-top-color: transparent; border-bottom-color: transparent; + flex-shrink: 0; + align-self: center; } diff --git a/Assets/Resources/UIToolkit/Menu/UTKMenuItemUss.uss b/Assets/Resources/UIToolkit/Menu/UTKMenuItemUss.uss index 4e0d3371..e23e3bef 100644 --- a/Assets/Resources/UIToolkit/Menu/UTKMenuItemUss.uss +++ b/Assets/Resources/UIToolkit/Menu/UTKMenuItemUss.uss @@ -34,13 +34,15 @@ .menu-item__arrow { width: 0; height: 0; - margin-left: 4px; - border-left-width: 4px; - border-right-width: 4px; - border-top-width: 4px; - border-bottom-width: 4px; - border-left-color: transparent; + margin-left: auto; + border-left-width: 5px; + border-right-width: 5px; + border-top-width: 5px; + border-bottom-width: 5px; + border-left-color: var(--color-text-primary); border-right-color: transparent; - border-top-color: var(--color-text-primary); + border-top-color: transparent; border-bottom-color: transparent; + flex-shrink: 0; + align-self: center; } diff --git a/Assets/Resources/UIToolkit/Menu/UTKTopMenuUss.uss b/Assets/Resources/UIToolkit/Menu/UTKTopMenuUss.uss index fe7b6884..60d802a0 100644 --- a/Assets/Resources/UIToolkit/Menu/UTKTopMenuUss.uss +++ b/Assets/Resources/UIToolkit/Menu/UTKTopMenuUss.uss @@ -14,6 +14,16 @@ border-bottom-color: var(--color-border); } +/* 세로 정렬 메뉴 */ +.top-menu__items--vertical { + flex-direction: column; + height: auto; + width: auto; + background-color: var(--color-bg-primary); + border-right-width: 1px; + border-right-color: var(--color-border); +} + /* 하위 메뉴 컨테이너 스타일 */ .submenu-container { position: absolute; diff --git a/Assets/Sample/UIToolkit/UTKMenuSample.cs b/Assets/Sample/UIToolkit/UTKMenuSample.cs index 57a226c4..244ad4b8 100644 --- a/Assets/Sample/UIToolkit/UTKMenuSample.cs +++ b/Assets/Sample/UIToolkit/UTKMenuSample.cs @@ -4,7 +4,6 @@ using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.UIElements; using UVC.UIToolkit; -using UVC.UIToolkit.Menu; using UVC.UI.Commands; using UVC.Log; @@ -24,12 +23,15 @@ namespace UVC.Sample.UIToolkit private UTKToggle? _themeToggle; private VisualElement? _root; - private UTKTopMenuView? _menuView; + private UTKTopMenu? _menuView; private UTKTopMenuModel? _menuModel; - private UTKTopMenuView? _menuView2; + private UTKTopMenu? _menuView2; private UTKTopMenuModel? _menuModel2; + private UTKTopMenu? _menuView3; + private UTKTopMenuModel? _menuModel3; + // 상태 관리용 private bool _canUndo = false; private bool _canRedo = false; @@ -72,12 +74,12 @@ namespace UVC.Sample.UIToolkit if (_root == null) return; // 1. UTKTopMenuView 생성 - _menuView = new UTKTopMenuView(); + _menuView = new UTKTopMenu(); _menuView.style.position = Position.Absolute; _menuView.style.top = 0; _menuView.style.left = 50; - _menuView2 = new UTKTopMenuView(); + _menuView2 = new UTKTopMenu(); _menuView2.style.position = Position.Absolute; _menuView2.style.top = 0; _menuView2.style.left = 0; @@ -85,10 +87,23 @@ namespace UVC.Sample.UIToolkit _root.Add(_menuView2); _root.Add(_menuView); + // 세로 정렬 메뉴 (Vertical Orientation + ItemSpacing) + _menuView3 = new UTKTopMenu(); + _menuView3.Orientation = UTKMenuOrientation.Vertical; + _menuView3.ItemSpacing = 4f; + _menuView3.SubMenuOffsetX = -10f; + _menuView3.SubMenuOffsetY = 4f; + _menuView3.style.position = Position.Absolute; + _menuView3.style.top = 50; + _menuView3.style.left = 0; + _menuView3.style.width = 120; + _root.Add(_menuView3); + // 2. UTKTopMenuModel 생성 및 메뉴 아이템 추가 _menuModel = new UTKTopMenuModel(); _menuModel2 = new UTKTopMenuModel(); - + _menuModel3 = new UTKTopMenuModel(); + CreateMenuItems(); // 3. View에 메뉴 생성 @@ -102,9 +117,15 @@ namespace UVC.Sample.UIToolkit _menuView2.CreateMenuItems(_menuModel2.MenuItems, _menuView2.MenuContainer); } + if (_menuView3.MenuContainer != null) + { + _menuView3.CreateMenuItems(_menuModel3.MenuItems, _menuView3.MenuContainer); + } + // 4. 이벤트 구독 _menuView.OnMenuItemClicked += HandleMenuItemClicked; _menuView2.OnMenuItemClicked += HandleMenuItemClicked2; + _menuView3.OnMenuItemClicked += HandleMenuItemClicked3; // 5. 상태 테스트 버튼 생성 CreateTestButtons(); @@ -117,7 +138,7 @@ namespace UVC.Sample.UIToolkit /// private void CreateMenuItems() { - if (_menuModel == null || _menuModel2 == null) return; + if (_menuModel == null || _menuModel2 == null || _menuModel3 == null) return; // 파일 메뉴 var fileMenu = new UTKMenuItemData("file", "파일"); @@ -264,7 +285,28 @@ namespace UVC.Sample.UIToolkit _menuModel2.AddMenuItem(imageMenu); - ULog.Debug($"메뉴 아이템 생성 완료: {_menuModel.MenuItems.Count}개 최상위 메뉴"); + // === 세로 메뉴 (menuView3) === + var vertFileMenu = new UTKMenuItemData("v_file", "파일"); + vertFileMenu.AddSubMenuItem(new UTKMenuItemData("v_file_new", "새 파일", new DebugLogCommand("세로 메뉴: 새 파일"), shortcut: "Ctrl+N")); + vertFileMenu.AddSubMenuItem(new UTKMenuItemData("v_file_open", "열기", new DebugLogCommand("세로 메뉴: 열기"), shortcut: "Ctrl+O")); + vertFileMenu.AddSubMenuItem(UTKMenuItemData.CreateSeparator()); + vertFileMenu.AddSubMenuItem(new UTKMenuItemData("v_file_save", "저장", new DebugLogCommand("세로 메뉴: 저장"), shortcut: "Ctrl+S")); + _menuModel3.AddMenuItem(vertFileMenu); + + var vertEditMenu = new UTKMenuItemData("v_edit", "편집"); + vertEditMenu.AddSubMenuItem(new UTKMenuItemData("v_edit_undo", "실행 취소", new DebugLogCommand("세로 메뉴: 실행 취소"), shortcut: "Ctrl+Z")); + vertEditMenu.AddSubMenuItem(new UTKMenuItemData("v_edit_redo", "다시 실행", new DebugLogCommand("세로 메뉴: 다시 실행"), shortcut: "Ctrl+Y")); + _menuModel3.AddMenuItem(vertEditMenu); + + var vertViewMenu = new UTKMenuItemData("v_view", "보기"); + vertViewMenu.AddSubMenuItem(new UTKMenuItemData("v_view_fullscreen", "전체 화면", new DebugLogCommand("세로 메뉴: 전체 화면"), shortcut: "F11")); + _menuModel3.AddMenuItem(vertViewMenu); + + var vertHelpMenu = new UTKMenuItemData("v_help", "도움말"); + vertHelpMenu.AddSubMenuItem(new UTKMenuItemData("v_help_about", "정보", new DebugLogCommand("세로 메뉴: 정보"))); + _menuModel3.AddMenuItem(vertHelpMenu); + + ULog.Debug($"메뉴 아이템 생성 완료: {_menuModel.MenuItems.Count}개 최상위 메뉴, 세로 메뉴: {_menuModel3.MenuItems.Count}개"); } /// @@ -278,7 +320,7 @@ namespace UVC.Sample.UIToolkit var buttonContainer = new VisualElement(); buttonContainer.style.position = Position.Absolute; buttonContainer.style.top = 60; - buttonContainer.style.left = 10; + buttonContainer.style.left = 300; buttonContainer.style.flexDirection = FlexDirection.Column; buttonContainer.style.width = 250; _root.Insert(0, buttonContainer); @@ -335,8 +377,25 @@ namespace UVC.Sample.UIToolkit { text = "Save 단축키 변경" }; + changeShortcutBtn.style.marginBottom = 5; buttonContainer.Add(changeShortcutBtn); + // ExecuteCommand 테스트 + var executeCommandBtn = new Button(() => TestExecuteCommand()) + { + text = "ExecuteCommand 테스트" + }; + executeCommandBtn.style.marginBottom = 5; + buttonContainer.Add(executeCommandBtn); + + // TryGetMenuItemData 테스트 + var getDataBtn = new Button(() => TestTryGetMenuItemData()) + { + text = "TryGetMenuItemData 테스트" + }; + getDataBtn.style.marginBottom = 5; + buttonContainer.Add(getDataBtn); + // 상태 표시 레이블 var statusLabel = new Label(); statusLabel.style.marginTop = 20; @@ -501,6 +560,51 @@ namespace UVC.Sample.UIToolkit label.schedule.Execute(() => UpdateStatusLabel(label)).Every(100); } + /// + /// ExecuteCommand 테스트: ItemId로 Command를 직접 실행합니다. + /// + private void TestExecuteCommand() + { + if (_menuView == null) return; + + // file_new는 항상 활성화 → true 반환 + bool result1 = _menuView.ExecuteCommand("file_new"); + ULog.Debug($"ExecuteCommand(\"file_new\"): {result1}"); + + // file_save는 비활성화 시 false 반환 + bool result2 = _menuView.ExecuteCommand("file_save"); + ULog.Debug($"ExecuteCommand(\"file_save\"): {result2} (비활성화 상태이면 false)"); + + // 존재하지 않는 ID → false 반환 + bool result3 = _menuView.ExecuteCommand("not_exist"); + ULog.Debug($"ExecuteCommand(\"not_exist\"): {result3}"); + } + + /// + /// TryGetMenuItemData 테스트: ItemId로 메뉴 데이터를 조회합니다. + /// + private void TestTryGetMenuItemData() + { + if (_menuView == null) return; + + if (_menuView.TryGetMenuItemData("file_save", out var data)) + { + ULog.Debug($"TryGetMenuItemData(\"file_save\"): " + + $"DisplayName={data?.DisplayName}, " + + $"IsEnabled={data?.IsEnabled}, " + + $"Shortcut={data?.Shortcut}, " + + $"HasCommand={data?.Command != null}"); + } + else + { + ULog.Debug("TryGetMenuItemData(\"file_save\"): 찾을 수 없음"); + } + + // 존재하지 않는 ID + bool found = _menuView.TryGetMenuItemData("not_exist", out _); + ULog.Debug($"TryGetMenuItemData(\"not_exist\"): {found}"); + } + /// /// 메뉴 아이템 클릭 핸들러입니다. /// @@ -530,6 +634,21 @@ namespace UVC.Sample.UIToolkit } } + /// + /// 세로 메뉴 클릭 핸들러입니다. + /// + private void HandleMenuItemClicked3(UTKMenuItemData itemData) + { + if (itemData == null) return; + + ULog.Debug($"세로 메뉴 클릭: {itemData.ItemId} - {itemData.DisplayName}"); + + if (itemData.Command != null) + { + itemData.Command.Execute(itemData.CommandParameter); + } + } + private void OnDestroy() { // 이벤트 구독 해제 @@ -539,8 +658,22 @@ namespace UVC.Sample.UIToolkit _menuView.Dispose(); } + if (_menuView2 != null) + { + _menuView2.OnMenuItemClicked -= HandleMenuItemClicked2; + _menuView2.Dispose(); + } + + if (_menuView3 != null) + { + _menuView3.OnMenuItemClicked -= HandleMenuItemClicked3; + _menuView3.Dispose(); + } + // 모델 정리 _menuModel?.Dispose(); + _menuModel2?.Dispose(); + _menuModel3?.Dispose(); ULog.Debug("UTKMenuSample 정리 완료"); } diff --git a/Assets/Scripts/UVC/UIToolkit/Menu/README.md b/Assets/Scripts/UVC/UIToolkit/Menu/README.md index 7d56ea1c..8ffbfe90 100644 --- a/Assets/Scripts/UVC/UIToolkit/Menu/README.md +++ b/Assets/Scripts/UVC/UIToolkit/Menu/README.md @@ -32,6 +32,15 @@ UIToolkit 기반의 Top Menu 시스템입니다. Unity 6의 최신 UIToolkit 기 - 이벤트 구독/해제 대칭 (RegisterCallback/UnregisterCallback) - Dictionary 캐싱으로 검색 최적화 (O(1)) +✅ **레이아웃 설정** +- 메뉴 아이템 간격 조절 (ItemSpacing) +- 가로/세로 정렬 전환 (Orientation: Horizontal / Vertical) +- UXML 어트리뷰트 지원 (`item-spacing`, `orientation`) + +✅ **프로그래밍 방식 제어** +- ItemId로 Command 직접 실행 (`ExecuteCommand`) +- ItemId로 메뉴 데이터 조회 (`TryGetMenuItemData`) + ✅ **고성능 최적화** - **Lazy Loading**: 서브메뉴는 첫 클릭 시에만 생성 (메모리 절약) - **리소스 캐싱**: UXML/USS 리소스 1회만 로드 (반복 로드 방지) @@ -49,7 +58,7 @@ Assets/Scripts/UVC/UIToolkit/Menu/ ├── UTKMenuItemData.cs # 메뉴 데이터 (텍스트 메뉴) ├── UTKMenuImageItemData.cs # 이미지 메뉴 데이터 (Material Icons 지원) ├── UTKTopMenuModel.cs # 데이터 모델 (Dictionary 캐싱) -├── UTKTopMenuView.cs # View (VisualElement 기반, Lazy Loading) +├── UTKTopMenu.cs # View (VisualElement 기반, Lazy Loading) ├── UTKMenuItemBase.cs # 메뉴 아이템 베이스 클래스 ├── UTKTopMenuItem.cs # 텍스트 메뉴 아이템 컴포넌트 ├── UTKTopMenuImageItem.cs # 이미지 메뉴 아이템 컴포넌트 @@ -77,8 +86,8 @@ Assets/Resources/UIToolkit/Menu/ var menuObject = new GameObject("TopMenu"); var uiDocument = menuObject.AddComponent(); -// 2. UTKTopMenuView를 UIDocument의 루트에 추가 -var menuView = new UTKTopMenuView(); +// 2. UTKTopMenu를 UIDocument의 루트에 추가 +var menuView = new UTKTopMenu(); uiDocument.rootVisualElement.Add(menuView); ``` @@ -142,7 +151,7 @@ using UnityEngine.UIElements; public class MenuSetup : MonoBehaviour { - private UTKTopMenuView? _menuView; + private UTKTopMenu? _menuView; private UTKTopMenuModel? _model; void Start() @@ -178,7 +187,7 @@ public class MenuSetup : MonoBehaviour _model.AddMenuItem(helpMenu); // View 생성 및 연결 - _menuView = new UTKTopMenuView(); + _menuView = new UTKTopMenu(); uiDocument.rootVisualElement.Add(_menuView); _menuView.CreateMenuItems(_model.GetMenuItems(), _menuView.MenuContainer, 0); @@ -285,6 +294,55 @@ if (_menuView != null) } ``` +### 메뉴 아이템 간격 조절 + +```csharp +// 메뉴 아이템 간 간격 설정 +if (_menuView != null) +{ + _menuView.ItemSpacing = 8f; // 아이템 간 8px 간격 +} +``` + +### 세로 메뉴 (Vertical Orientation) + +```csharp +// 세로 메뉴로 전환 +var sideMenu = new UTKTopMenu(); +sideMenu.Orientation = UTKMenuOrientation.Vertical; // 세로 정렬 +sideMenu.ItemSpacing = 4f; // 아이템 간 4px 간격 + +// 서브메뉴는 아이템 오른쪽에 표시됨 +sideMenu.SubMenuOffsetX = 5f; +``` + +### UXML에서 레이아웃 설정 + +```xml + + + + + +``` + +### ItemId로 Command 실행 + +```csharp +// ItemId로 메뉴 아이템의 Command를 직접 실행 +bool executed = _menuView.ExecuteCommand("file_save"); +if (!executed) +{ + Debug.Log("Command 실행 실패: 비활성화되었거나 Command가 없습니다."); +} + +// 메뉴 데이터 조회 +if (_menuView.TryGetMenuItemData("file_save", out var itemData)) +{ + Debug.Log($"Enabled: {itemData?.IsEnabled}, Shortcut: {itemData?.Shortcut}"); +} +``` + --- ## API 문서 @@ -301,15 +359,17 @@ if (_menuView != null) | `FindMenuItem(itemId)` | ID로 메뉴 아이템 검색 (O(1)) | string | UTKMenuItemData? | | `Dispose()` | 리소스 정리 | - | void | -### UTKTopMenuView +### UTKTopMenu #### Public Properties -| 속성 | 타입 | 설명 | -|------|------|------| -| `MenuContainer` | VisualElement? | 메뉴 아이템이 배치될 컨테이너 | -| `SubMenuOffsetX` | float | 최상위 메뉴의 서브메뉴 X축 offset (픽셀) | -| `SubMenuOffsetY` | float | 최상위 메뉴의 서브메뉴 Y축 offset (픽셀) | +| 속성 | 타입 | UXML 어트리뷰트 | 설명 | +|------|------|----------------|------| +| `MenuContainer` | VisualElement? | - | 메뉴 아이템이 배치될 컨테이너 | +| `SubMenuOffsetX` | float | - | 최상위 메뉴의 서브메뉴 X축 offset (픽셀) | +| `SubMenuOffsetY` | float | - | 최상위 메뉴의 서브메뉴 Y축 offset (픽셀) | +| `ItemSpacing` | float | `item-spacing` | 최상위 메뉴 아이템 간 간격 (픽셀, 기본값: 0) | +| `Orientation` | UTKMenuOrientation | `orientation` | 정렬 방향 (Horizontal / Vertical, 기본값: Horizontal) | #### Public Methods @@ -318,6 +378,8 @@ if (_menuView != null) | `CreateMenuItems(items, container, depth)` | 메뉴 아이템 생성 (Lazy Loading) | | `ClearMenuItems()` | 모든 메뉴 아이템 제거 | | `CloseAllOpenSubMenus()` | 모든 열린 서브메뉴 닫기 | +| `TryGetMenuItemData(itemId, out itemData)` | ItemId로 메뉴 아이템 데이터 조회 (O(1)) | +| `ExecuteCommand(itemId)` | ItemId로 Command 실행 (비활성화/Command 없으면 false) | | `Dispose()` | 리소스 정리 | #### Public Events @@ -472,7 +534,7 @@ void OnDisable() 1. Unity Profiler 열기 (Window > Analysis > Profiler) 2. Memory 섹션 선택 3. 씬 전환 10회 후 메모리 증가 확인 -4. Detailed View에서 UTKMenuItemData, UTKTopMenuView 검색 +4. Detailed View에서 UTKMenuItemData, UTKTopMenu 검색 --- @@ -481,7 +543,7 @@ void OnDisable() ### Lazy Loading (서브메뉴 지연 생성) ```csharp -// UTKTopMenuView는 서브메뉴를 첫 클릭 시에만 생성합니다. +// UTKTopMenu는 서브메뉴를 첫 클릭 시에만 생성합니다. // 초기 메모리 사용량을 대폭 감소시킵니다. // Before (모든 서브메뉴 사전 생성): @@ -497,10 +559,10 @@ void OnDisable() ### 리소스 캐싱 ```csharp -// UTKTopMenuView는 UXML/USS 리소스를 클래스 레벨에서 캐싱합니다. +// UTKTopMenu는 UXML/USS 리소스를 클래스 레벨에서 캐싱합니다. // 서브메뉴 아이템을 여러 개 생성해도 Resources.Load는 1회만 호출됩니다. -// 내부 구현 (UTKTopMenuView.cs): +// 내부 구현 (UTKTopMenu.cs): // private VisualTreeAsset? _cachedSubMenuItemAsset; // private StyleSheet? _cachedSubMenuItemUss; // @@ -622,7 +684,7 @@ subMenuContainer.style.display = DisplayStyle.Flex; // 표시 ```csharp if (view == null) { - Debug.LogError("UTKTopMenuView를 찾을 수 없습니다."); + Debug.LogError("UTKTopMenu를 찾을 수 없습니다."); } ``` diff --git a/Assets/Scripts/UVC/UIToolkit/Menu/UTKMenuItemBase.cs b/Assets/Scripts/UVC/UIToolkit/Menu/UTKMenuItemBase.cs index e9dfd005..c8a92102 100644 --- a/Assets/Scripts/UVC/UIToolkit/Menu/UTKMenuItemBase.cs +++ b/Assets/Scripts/UVC/UIToolkit/Menu/UTKMenuItemBase.cs @@ -162,7 +162,7 @@ namespace UVC.UIToolkit ItemId = data.ItemId; DisplayName = data.DisplayName; IsEnabled = data.IsEnabled; - Shortcut = data.Shortcut; + Shortcut = data.Shortcut ?? ""; UpdateUI(); } diff --git a/Assets/Scripts/UVC/UIToolkit/Menu/UTKTopMenuView.cs b/Assets/Scripts/UVC/UIToolkit/Menu/UTKTopMenu.cs similarity index 82% rename from Assets/Scripts/UVC/UIToolkit/Menu/UTKTopMenuView.cs rename to Assets/Scripts/UVC/UIToolkit/Menu/UTKTopMenu.cs index 77836c30..233b266b 100644 --- a/Assets/Scripts/UVC/UIToolkit/Menu/UTKTopMenuView.cs +++ b/Assets/Scripts/UVC/UIToolkit/Menu/UTKTopMenu.cs @@ -8,6 +8,17 @@ using UVC.Locale; namespace UVC.UIToolkit { + /// + /// 메뉴의 정렬 방향을 나타냅니다. + /// + public enum UTKMenuOrientation + { + /// 가로 정렬 (기본값) + Horizontal, + /// 세로 정렬 + Vertical + } + /// /// UIToolkit 기반 Top Menu의 View 레이어입니다. /// 메뉴 아이템의 시각적 표현과 사용자 상호작용을 처리합니다. @@ -20,6 +31,10 @@ namespace UVC.UIToolkit /// 테마 변경 지원 (UTKThemeManager 연동) /// 외부 클릭으로 서브메뉴 자동 닫기 /// 서브메뉴 위치 조정 (SubMenuOffsetX/Y) + /// 메뉴 아이템 간격 조절 (ItemSpacing) + /// 가로/세로 정렬 전환 (Orientation) + /// ItemId로 Command 실행 (ExecuteCommand) + /// ItemId로 메뉴 데이터 조회 (TryGetMenuItemData) /// 성능 최적화 (리소스 캐싱, Dictionary 추적) /// /// @@ -28,7 +43,7 @@ namespace UVC.UIToolkit /// // === 기본 사용법 === /// /// // 1. View 생성 및 UIDocument에 추가 - /// var menuView = new UTKTopMenuView(); + /// var menuView = new UTKTopMenu(); /// uiDocument.rootVisualElement.Add(menuView); /// /// // 2. 메뉴 데이터 생성 @@ -78,6 +93,21 @@ namespace UVC.UIToolkit /// menuView.SubMenuOffsetX = 10; // 오른쪽으로 10px 이동 /// menuView.SubMenuOffsetY = 5; // 아래쪽으로 5px 이동 /// + /// // 8. 메뉴 아이템 간격 조절 (선택적) + /// menuView.ItemSpacing = 8; // 아이템 간 8px 간격 + /// + /// // 9. 세로 정렬 (선택적) + /// menuView.Orientation = UTKMenuOrientation.Vertical; // 세로 메뉴로 전환 + /// + /// // 10. ItemId로 Command 직접 실행 (선택적) + /// bool executed = menuView.ExecuteCommand("file_save"); // true: 실행됨, false: 비활성화/Command 없음 + /// + /// // 11. ItemId로 메뉴 데이터 조회 (선택적) + /// if (menuView.TryGetMenuItemData("file_save", out var data)) + /// { + /// Debug.Log($"Enabled: {data?.IsEnabled}, Shortcut: {data?.Shortcut}"); + /// } + /// /// /// // === 이벤트 처리 === /// @@ -150,6 +180,14 @@ namespace UVC.UIToolkit /// Dispose 시 모든 리소스 및 참조 정리 /// /// + /// 레이아웃 설정: + /// + /// ItemSpacing: 최상위 메뉴 아이템 간 간격 (픽셀). 기본값 0 + /// Orientation: 가로(Horizontal, 기본값) 또는 세로(Vertical) 정렬 전환 + /// 세로 정렬 시 서브메뉴는 아이템 오른쪽에 표시됨 + /// UXML에서 item-spacing, orientation 어트리뷰트로 설정 가능 + /// + /// /// 주의사항: /// /// CreateMenuItems 호출 전에 MenuContainer가 null이 아닌지 확인 @@ -158,7 +196,7 @@ namespace UVC.UIToolkit /// /// [UxmlElement] - public partial class UTKTopMenuView : VisualElement, IDisposable + public partial class UTKTopMenu : VisualElement, IDisposable { #region Constants @@ -192,6 +230,10 @@ namespace UVC.UIToolkit private float _subMenuOffsetX = 0f; private float _subMenuOffsetY = 0f; + // 메뉴 아이템 간격 및 정렬 방향 + private float _itemSpacing = 0f; + private UTKMenuOrientation _orientation = UTKMenuOrientation.Horizontal; + // 리소스 캐싱 (성능 개선) private VisualTreeAsset? _cachedSubMenuItemAsset; private StyleSheet? _cachedSubMenuItemUss; @@ -223,6 +265,36 @@ namespace UVC.UIToolkit set => _subMenuOffsetY = value; } + /// + /// 최상위 메뉴 아이템 간 간격 (픽셀 단위). + /// 0 이상의 값을 지정하면 각 아이템 사이에 margin이 적용됩니다. + /// + [UxmlAttribute("item-spacing")] + public float ItemSpacing + { + get => _itemSpacing; + set + { + _itemSpacing = value; + ApplyItemSpacing(); + } + } + + /// + /// 최상위 메뉴의 정렬 방향입니다. + /// Horizontal(기본값)은 가로 정렬, Vertical은 세로 정렬입니다. + /// + [UxmlAttribute("orientation")] + public UTKMenuOrientation Orientation + { + get => _orientation; + set + { + _orientation = value; + ApplyOrientation(); + } + } + #endregion #region Events @@ -235,9 +307,9 @@ namespace UVC.UIToolkit #region Constructor /// - /// UTKTopMenuView의 새 인스턴스를 초기화합니다. + /// UTKTopMenu의 새 인스턴스를 초기화합니다. /// - public UTKTopMenuView() : base() + public UTKTopMenu() : base() { // 1. 테마 적용 UTKThemeManager.Instance.ApplyThemeToElement(this); @@ -540,6 +612,12 @@ namespace UVC.UIToolkit } } } + + // 최상위 메뉴인 경우 간격 적용 + if (depth == 0) + { + ApplyItemSpacing(); + } } /// @@ -628,6 +706,37 @@ namespace UVC.UIToolkit return _menuItemElements.TryGetValue(itemId, out menuItemElement); } + /// + /// ItemId로 메뉴 아이템 데이터를 가져옵니다. + /// + /// 메뉴 아이템 ID + /// 찾은 메뉴 아이템 데이터 + /// 찾았으면 true, 그렇지 않으면 false + public bool TryGetMenuItemData(string itemId, out UTKMenuItemData? itemData) + { + return _menuItemDataMap.TryGetValue(itemId, out itemData); + } + + /// + /// ItemId로 메뉴 아이템의 Command를 실행합니다. + /// Command가 없거나 비활성화된 아이템이면 false를 반환합니다. + /// + /// 실행할 메뉴 아이템 ID + /// Command가 실행되었으면 true, 그렇지 않으면 false + public bool ExecuteCommand(string itemId) + { + if (!_menuItemDataMap.TryGetValue(itemId, out var itemData)) + return false; + + if (!itemData.IsEnabled || itemData.Command == null) + return false; + + itemData.Command.Execute(itemData.CommandParameter); + OnMenuItemClicked?.Invoke(itemData); + CloseAllOpenSubMenus(); + return true; + } + /// /// 모든 열린 하위 메뉴를 닫습니다. /// @@ -658,6 +767,83 @@ namespace UVC.UIToolkit #endregion + #region Private Methods - 레이아웃 + + /// + /// 메뉴 아이템 간 간격을 적용합니다. + /// + private void ApplyItemSpacing() + { + if (_menuContainer == null) return; + + for (int i = 0; i < _menuContainer.childCount; i++) + { + var child = _menuContainer[i]; + if (_orientation == UTKMenuOrientation.Horizontal) + { + child.style.marginLeft = i > 0 ? _itemSpacing : 0; + child.style.marginTop = 0; + } + else + { + child.style.marginTop = i > 0 ? _itemSpacing : 0; + child.style.marginLeft = 0; + } + } + } + + /// + /// 메뉴 컨테이너의 정렬 방향을 적용합니다. + /// + private void ApplyOrientation() + { + if (_menuContainer == null) return; + + if (_orientation == UTKMenuOrientation.Horizontal) + { + _menuContainer.style.flexDirection = FlexDirection.Row; + _menuContainer.RemoveFromClassList("top-menu__items--vertical"); + _menuContainer.AddToClassList("top-menu__items"); + } + else + { + _menuContainer.style.flexDirection = FlexDirection.Column; + _menuContainer.RemoveFromClassList("top-menu__items"); + _menuContainer.AddToClassList("top-menu__items--vertical"); + } + + // 간격도 방향에 맞게 재적용 + ApplyItemSpacing(); + + // 최상위 메뉴 아이템의 화살표 표시 갱신 + ApplyTopMenuArrows(); + } + + /// + /// 최상위 메뉴 아이템의 화살표 표시를 정렬 방향에 맞게 갱신합니다. + /// + private void ApplyTopMenuArrows() + { + if (_menuContainer == null) return; + + foreach (var kvp in _menuItemDataMap) + { + var itemData = kvp.Value; + // 최상위 메뉴만 대상 (Depth == 0) + if (itemData.Depth != 0) continue; + + if (_menuItemElements.TryGetValue(kvp.Key, out var element) && element is UTKMenuItemBase menuItem) + { + bool showArrow = _orientation == UTKMenuOrientation.Vertical + && itemData.SubMenuItems != null + && itemData.SubMenuItems.Count > 0; + menuItem.ShowArrow(showArrow); + } + } + } + + #endregion + #region Private Methods - 메뉴 아이템 생성 /// @@ -678,7 +864,12 @@ namespace UVC.UIToolkit } menuItem.SetData(itemData); - menuItem.ShowArrow(false); // 최상위 메뉴는 화살표 표시하지 않음 + + // 세로 정렬 시 서브메뉴가 있으면 화살표 표시 + bool showArrow = _orientation == UTKMenuOrientation.Vertical + && itemData.SubMenuItems != null + && itemData.SubMenuItems.Count > 0; + menuItem.ShowArrow(showArrow); // 클릭 이벤트 등록 menuItem.OnClicked += (data) => @@ -960,9 +1151,18 @@ namespace UVC.UIToolkit if (depth == 0) { - // 1차 메뉴: 아래쪽에 표시 (offset 적용) - subMenuContainer.style.left = menuItemRect.x + _subMenuOffsetX; - subMenuContainer.style.top = menuItemRect.yMax + _subMenuOffsetY; + if (_orientation == UTKMenuOrientation.Horizontal) + { + // 가로 정렬: 아래쪽에 표시 (offset 적용) + subMenuContainer.style.left = menuItemRect.x + _subMenuOffsetX; + subMenuContainer.style.top = menuItemRect.yMax + _subMenuOffsetY; + } + else + { + // 세로 정렬: 오른쪽에 표시 (offset 적용) + subMenuContainer.style.left = menuItemRect.xMax + _subMenuOffsetX; + subMenuContainer.style.top = menuItemRect.y + _subMenuOffsetY; + } } else { diff --git a/Assets/Scripts/UVC/UIToolkit/Menu/UTKTopMenuView.cs.meta b/Assets/Scripts/UVC/UIToolkit/Menu/UTKTopMenu.cs.meta similarity index 100% rename from Assets/Scripts/UVC/UIToolkit/Menu/UTKTopMenuView.cs.meta rename to Assets/Scripts/UVC/UIToolkit/Menu/UTKTopMenu.cs.meta diff --git a/Assets/Scripts/UVC/UIToolkit/Menu/UTKTopMenuController.cs.meta b/Assets/Scripts/UVC/UIToolkit/Menu/UTKTopMenuController.cs.meta deleted file mode 100644 index 8380bc5c..00000000 --- a/Assets/Scripts/UVC/UIToolkit/Menu/UTKTopMenuController.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 2be8c935a6302b648a5c4ce5e6168a98 \ No newline at end of file