UTKTopMenu 기능 추가

This commit is contained in:
logonkhi
2026-02-19 10:43:52 +09:00
parent 200a7faa6b
commit 0333b83b57
9 changed files with 457 additions and 50 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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
/// </summary>
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}개");
}
/// <summary>
@@ -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);
}
/// <summary>
/// ExecuteCommand 테스트: ItemId로 Command를 직접 실행합니다.
/// </summary>
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}");
}
/// <summary>
/// TryGetMenuItemData 테스트: ItemId로 메뉴 데이터를 조회합니다.
/// </summary>
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}");
}
/// <summary>
/// 메뉴 아이템 클릭 핸들러입니다.
/// </summary>
@@ -530,6 +634,21 @@ namespace UVC.Sample.UIToolkit
}
}
/// <summary>
/// 세로 메뉴 클릭 핸들러입니다.
/// </summary>
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 정리 완료");
}

View File

@@ -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<UIDocument>();
// 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
<!-- 가로 메뉴 (기본값) -->
<utk:UTKTopMenu item-spacing="8" orientation="Horizontal" />
<!-- 세로 메뉴 -->
<utk:UTKTopMenu item-spacing="4" orientation="Vertical" />
```
### 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를 찾을 수 없습니다.");
}
```

View File

@@ -162,7 +162,7 @@ namespace UVC.UIToolkit
ItemId = data.ItemId;
DisplayName = data.DisplayName;
IsEnabled = data.IsEnabled;
Shortcut = data.Shortcut;
Shortcut = data.Shortcut ?? "";
UpdateUI();
}

View File

@@ -8,6 +8,17 @@ using UVC.Locale;
namespace UVC.UIToolkit
{
/// <summary>
/// 메뉴의 정렬 방향을 나타냅니다.
/// </summary>
public enum UTKMenuOrientation
{
/// <summary>가로 정렬 (기본값)</summary>
Horizontal,
/// <summary>세로 정렬</summary>
Vertical
}
/// <summary>
/// UIToolkit 기반 Top Menu의 View 레이어입니다.
/// 메뉴 아이템의 시각적 표현과 사용자 상호작용을 처리합니다.
@@ -20,6 +31,10 @@ namespace UVC.UIToolkit
/// <item>테마 변경 지원 (UTKThemeManager 연동)</item>
/// <item>외부 클릭으로 서브메뉴 자동 닫기</item>
/// <item>서브메뉴 위치 조정 (SubMenuOffsetX/Y)</item>
/// <item>메뉴 아이템 간격 조절 (ItemSpacing)</item>
/// <item>가로/세로 정렬 전환 (Orientation)</item>
/// <item>ItemId로 Command 실행 (ExecuteCommand)</item>
/// <item>ItemId로 메뉴 데이터 조회 (TryGetMenuItemData)</item>
/// <item>성능 최적화 (리소스 캐싱, Dictionary 추적)</item>
/// </list>
/// </summary>
@@ -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
/// <item>Dispose 시 모든 리소스 및 참조 정리</item>
/// </list>
///
/// <para><strong>레이아웃 설정:</strong></para>
/// <list type="bullet">
/// <item><strong>ItemSpacing:</strong> 최상위 메뉴 아이템 간 간격 (픽셀). 기본값 0</item>
/// <item><strong>Orientation:</strong> 가로(Horizontal, 기본값) 또는 세로(Vertical) 정렬 전환</item>
/// <item>세로 정렬 시 서브메뉴는 아이템 오른쪽에 표시됨</item>
/// <item>UXML에서 item-spacing, orientation 어트리뷰트로 설정 가능</item>
/// </list>
///
/// <para><strong>주의사항:</strong></para>
/// <list type="bullet">
/// <item>CreateMenuItems 호출 전에 MenuContainer가 null이 아닌지 확인</item>
@@ -158,7 +196,7 @@ namespace UVC.UIToolkit
/// </list>
/// </remarks>
[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;
}
/// <summary>
/// 최상위 메뉴 아이템 간 간격 (픽셀 단위).
/// 0 이상의 값을 지정하면 각 아이템 사이에 margin이 적용됩니다.
/// </summary>
[UxmlAttribute("item-spacing")]
public float ItemSpacing
{
get => _itemSpacing;
set
{
_itemSpacing = value;
ApplyItemSpacing();
}
}
/// <summary>
/// 최상위 메뉴의 정렬 방향입니다.
/// Horizontal(기본값)은 가로 정렬, Vertical은 세로 정렬입니다.
/// </summary>
[UxmlAttribute("orientation")]
public UTKMenuOrientation Orientation
{
get => _orientation;
set
{
_orientation = value;
ApplyOrientation();
}
}
#endregion
#region Events
@@ -235,9 +307,9 @@ namespace UVC.UIToolkit
#region Constructor
/// <summary>
/// UTKTopMenuView의 새 인스턴스를 초기화합니다.
/// UTKTopMenu의 새 인스턴스를 초기화합니다.
/// </summary>
public UTKTopMenuView() : base()
public UTKTopMenu() : base()
{
// 1. 테마 적용
UTKThemeManager.Instance.ApplyThemeToElement(this);
@@ -540,6 +612,12 @@ namespace UVC.UIToolkit
}
}
}
// 최상위 메뉴인 경우 간격 적용
if (depth == 0)
{
ApplyItemSpacing();
}
}
/// <summary>
@@ -628,6 +706,37 @@ namespace UVC.UIToolkit
return _menuItemElements.TryGetValue(itemId, out menuItemElement);
}
/// <summary>
/// ItemId로 메뉴 아이템 데이터를 가져옵니다.
/// </summary>
/// <param name="itemId">메뉴 아이템 ID</param>
/// <param name="itemData">찾은 메뉴 아이템 데이터</param>
/// <returns>찾았으면 true, 그렇지 않으면 false</returns>
public bool TryGetMenuItemData(string itemId, out UTKMenuItemData? itemData)
{
return _menuItemDataMap.TryGetValue(itemId, out itemData);
}
/// <summary>
/// ItemId로 메뉴 아이템의 Command를 실행합니다.
/// Command가 없거나 비활성화된 아이템이면 false를 반환합니다.
/// </summary>
/// <param name="itemId">실행할 메뉴 아이템 ID</param>
/// <returns>Command가 실행되었으면 true, 그렇지 않으면 false</returns>
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;
}
/// <summary>
/// 모든 열린 하위 메뉴를 닫습니다.
/// </summary>
@@ -658,6 +767,83 @@ namespace UVC.UIToolkit
#endregion
#region Private Methods -
/// <summary>
/// 메뉴 아이템 간 간격을 적용합니다.
/// </summary>
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;
}
}
}
/// <summary>
/// 메뉴 컨테이너의 정렬 방향을 적용합니다.
/// </summary>
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();
}
/// <summary>
/// 최상위 메뉴 아이템의 화살표 표시를 정렬 방향에 맞게 갱신합니다.
/// </summary>
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 -
/// <summary>
@@ -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
{

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 2be8c935a6302b648a5c4ce5e6168a98