Files
EnglewoodLAB/Assets/Sample/UIToolkit/UTKMenuSample.cs

683 lines
26 KiB
C#

#nullable enable
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit;
using UVC.UI.Commands;
using UVC.Log;
namespace UVC.Sample.UIToolkit
{
/// <summary>
/// UTKMenu 샘플 코드
/// UIToolkit 기반 TopMenu 시스템을 테스트합니다.
/// </summary>
public class UTKMenuSample : MonoBehaviour
{
[SerializeField] private UIDocument? _uiDocument;
[SerializeField]
[Tooltip("시작 시 적용할 테마")]
private UTKTheme initialTheme = UTKTheme.Dark;
private UTKToggle? _themeToggle;
private VisualElement? _root;
private UTKTopMenu? _menuView;
private UTKTopMenuModel? _menuModel;
private UTKTopMenu? _menuView2;
private UTKTopMenuModel? _menuModel2;
private UTKTopMenu? _menuView3;
private UTKTopMenuModel? _menuModel3;
// 상태 관리용
private bool _canUndo = false;
private bool _canRedo = false;
private bool _isFileOpen = false;
private void Start()
{
// UIDocument 참조 확인
var doc = GetComponent<UIDocument>();
if (doc == null)
{
Debug.LogError("UIDocument가 할당되지 않았습니다.");
return;
}
_uiDocument = doc;
var toggle = _uiDocument.rootVisualElement.Q<UTKToggle>("toggle");
if (toggle == null)
{
Debug.LogError("UXML에서 UTKToggle을 찾을 수 없습니다.");
return;
}
_themeToggle = toggle;
UTKThemeManager.Instance.RegisterRoot(_uiDocument.rootVisualElement);
UTKThemeManager.Instance.SetTheme(initialTheme);
_themeToggle.OnValueChanged += (isOn) =>
{
UTKThemeManager.Instance.SetTheme(!isOn ? UTKTheme.Dark : UTKTheme.Light);
};
_root = _uiDocument.rootVisualElement;
CreateSampleUI();
}
private void CreateSampleUI()
{
if (_root == null) return;
// 1. UTKTopMenuView 생성
_menuView = new UTKTopMenu();
_menuView.style.position = Position.Absolute;
_menuView.style.top = 0;
_menuView.style.left = 50;
_menuView2 = new UTKTopMenu();
_menuView2.style.position = Position.Absolute;
_menuView2.style.top = 0;
_menuView2.style.left = 0;
_menuView2.SubMenuOffsetX = 30;
_menuView2.SubMenuOffsetY = -20;
_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에 메뉴 생성
if (_menuView.MenuContainer != null)
{
_menuView.CreateMenuItems(_menuModel.MenuItems, _menuView.MenuContainer);
}
if (_menuView2.MenuContainer != null)
{
_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();
ULog.Debug("UTKTopMenu 샘플 UI 생성 완료");
}
/// <summary>
/// 메뉴 아이템들을 생성합니다.
/// </summary>
private void CreateMenuItems()
{
if (_menuModel == null || _menuModel2 == null || _menuModel3 == null) return;
// 파일 메뉴
var fileMenu = new UTKMenuItemData("file", "파일");
fileMenu.AddSubMenuItem(new UTKMenuItemData(
"file_new",
"새 파일",
new DebugLogCommand("새 파일 생성"),
shortcut: "Ctrl+N"
));
fileMenu.AddSubMenuItem(new UTKMenuItemData(
"file_open",
"열기",
new DebugLogCommand("파일 열기"),
shortcut: "Ctrl+O"
));
fileMenu.AddSubMenuItem(UTKMenuItemData.CreateSeparator());
fileMenu.AddSubMenuItem(new UTKMenuItemData(
"file_save",
"저장",
new DebugLogCommand("파일 저장"),
isEnabled: false, // 초기에는 비활성화
shortcut: "Ctrl+S"
));
fileMenu.AddSubMenuItem(new UTKMenuItemData(
"file_save_as",
"다른 이름으로 저장",
new DebugLogCommand("다른 이름으로 저장"),
isEnabled: false,
shortcut: "Ctrl+Shift+S"
));
fileMenu.AddSubMenuItem(UTKMenuItemData.CreateSeparator());
fileMenu.AddSubMenuItem(new UTKMenuItemData(
"file_exit",
"종료",
new DebugLogCommand("애플리케이션 종료"),
shortcut: "Alt+F4"
));
_menuModel.AddMenuItem(fileMenu);
// 편집 메뉴
var editMenu = new UTKMenuItemData("edit", "편집");
editMenu.AddSubMenuItem(new UTKMenuItemData(
"edit_undo",
"실행 취소",
new DebugLogCommand("실행 취소"),
isEnabled: false,
shortcut: "Ctrl+Z"
));
editMenu.AddSubMenuItem(new UTKMenuItemData(
"edit_redo",
"다시 실행",
new DebugLogCommand("다시 실행"),
isEnabled: false,
shortcut: "Ctrl+Y"
));
editMenu.AddSubMenuItem(UTKMenuItemData.CreateSeparator());
editMenu.AddSubMenuItem(new UTKMenuItemData(
"edit_cut",
"잘라내기",
new DebugLogCommand("잘라내기"),
shortcut: "Ctrl+X"
));
editMenu.AddSubMenuItem(new UTKMenuItemData(
"edit_copy",
"복사",
new DebugLogCommand("복사"),
shortcut: "Ctrl+C"
));
editMenu.AddSubMenuItem(new UTKMenuItemData(
"edit_paste",
"붙여넣기",
new DebugLogCommand("붙여넣기"),
shortcut: "Ctrl+V"
));
_menuModel.AddMenuItem(editMenu);
// 보기 메뉴 (하위 메뉴 테스트)
var viewMenu = new UTKMenuItemData("view", "보기");
var layoutMenu = new UTKMenuItemData("view_layout", "레이아웃");
layoutMenu.AddSubMenuItem(new UTKMenuItemData("view_layout_default", "기본", new DebugLogCommand("기본 레이아웃")));
layoutMenu.AddSubMenuItem(new UTKMenuItemData("view_layout_wide", "와이드", new DebugLogCommand("와이드 레이아웃")));
layoutMenu.AddSubMenuItem(new UTKMenuItemData("view_layout_compact", "컴팩트", new DebugLogCommand("컴팩트 레이아웃")));
viewMenu.AddSubMenuItem(layoutMenu);
viewMenu.AddSubMenuItem(UTKMenuItemData.CreateSeparator());
viewMenu.AddSubMenuItem(new UTKMenuItemData("view_fullscreen", "전체 화면", new DebugLogCommand("전체 화면 전환"), shortcut: "F11"));
_menuModel.AddMenuItem(viewMenu);
// 도구 메뉴
var toolsMenu = new UTKMenuItemData("tools", "도구");
toolsMenu.AddSubMenuItem(new UTKMenuItemData("tools_options", "옵션", new DebugLogCommand("옵션 열기")));
toolsMenu.AddSubMenuItem(new UTKMenuItemData("tools_settings", "설정", new DebugLogCommand("설정 열기")));
_menuModel.AddMenuItem(toolsMenu);
// 도움말 메뉴
var helpMenu = new UTKMenuItemData("help", "도움말");
helpMenu.AddSubMenuItem(new UTKMenuItemData("help_documentation", "문서", new DebugLogCommand("문서 열기"), shortcut: "F1"));
helpMenu.AddSubMenuItem(UTKMenuItemData.CreateSeparator());
helpMenu.AddSubMenuItem(new UTKMenuItemData("help_about", "정보", new DebugLogCommand("정보 표시")));
_menuModel.AddMenuItem(helpMenu);
// 이미지 메뉴 (4 depth 테스트)
var imageMenu = new UTKMenuImageItemData(
"image_menu",
UTKMaterialIcons.Settings, // Material Icon 사용
useMaterialIcon: true,
imageSize: 24f
);
// Depth 1 아이템
var depth1Item = new UTKMenuItemData("depth1", "레벨 1 메뉴");
var depth1Item1 = new UTKMenuItemData("depth11", "레벨 1 1메뉴");
var depth1Item2 = new UTKMenuItemData("depth12", "레벨 1 2메뉴");
var depth1Item3 = new UTKMenuItemData("depth13", "레벨 1 3메뉴");
// Depth 2 아이템
var depth2Item = new UTKMenuItemData("depth2", "레벨 2 메뉴");
var depth2Item1 = new UTKMenuItemData("depth21", "레벨 2 1 메뉴");
var depth2Item2 = new UTKMenuItemData("depth22", "레벨 2 2 메뉴");
var depth2Item3 = new UTKMenuItemData("depth23", "레벨 2 3 메뉴");
// Depth 3 아이템
var depth3Item = new UTKMenuItemData("depth3", "레벨 3 메뉴");
// Depth 4 아이템들 (실제 액션)
var depth4Action1 = new UTKMenuItemData("depth4_action1", "액션 1", new DebugLogCommand("4 Depth 액션 1 실행"));
var depth4Action2 = new UTKMenuItemData("depth4_action2", "액션 2", new DebugLogCommand("4 Depth 액션 2 실행"));
var depth4Action3 = new UTKMenuItemData("depth4_action3", "액션 3", new DebugLogCommand("4 Depth 액션 3 실행"));
// 계층 구조 구성 (역순으로)
depth3Item.AddSubMenuItem(depth4Action1);
depth3Item.AddSubMenuItem(depth4Action2);
depth3Item.AddSubMenuItem(depth4Action3);
depth2Item.AddSubMenuItem(depth3Item);
depth1Item.AddSubMenuItem(depth2Item);
depth1Item.AddSubMenuItem(depth2Item1);
depth1Item.AddSubMenuItem(depth2Item2);
depth1Item.AddSubMenuItem(depth2Item3);
imageMenu.AddSubMenuItem(depth1Item1);
imageMenu.AddSubMenuItem(depth1Item2);
imageMenu.AddSubMenuItem(depth1Item3);
imageMenu.AddSubMenuItem(depth1Item);
_menuModel2.AddMenuItem(imageMenu);
// === 세로 메뉴 (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>
/// 테스트 버튼들을 생성합니다.
/// </summary>
private void CreateTestButtons()
{
if (_root == null) return;
// 테스트 버튼 컨테이너
var buttonContainer = new VisualElement();
buttonContainer.style.position = Position.Absolute;
buttonContainer.style.top = 60;
buttonContainer.style.left = 300;
buttonContainer.style.flexDirection = FlexDirection.Column;
buttonContainer.style.width = 250;
_root.Insert(0, buttonContainer);
// 타이틀
var title = new Label("메뉴 상태 테스트");
title.style.fontSize = 16;
title.style.unityFontStyleAndWeight = FontStyle.Bold;
title.style.marginBottom = 10;
buttonContainer.Add(title);
// 파일 열기/닫기 버튼
var fileToggleBtn = new Button(() => ToggleFileOpen())
{
text = "파일 열기 (Save 활성화)"
};
fileToggleBtn.style.marginBottom = 5;
buttonContainer.Add(fileToggleBtn);
// Undo/Redo 토글 버튼
var undoToggleBtn = new Button(() => ToggleUndoRedo())
{
text = "Undo/Redo 활성화"
};
undoToggleBtn.style.marginBottom = 5;
buttonContainer.Add(undoToggleBtn);
// 모든 하위 메뉴 닫기
var closeAllBtn = new Button(() => _menuView?.CloseAllOpenSubMenus())
{
text = "모든 하위 메뉴 닫기"
};
closeAllBtn.style.marginBottom = 5;
buttonContainer.Add(closeAllBtn);
// 메뉴 아이템 추가
var addMenuBtn = new Button(() => AddDynamicMenu())
{
text = "동적 메뉴 추가"
};
addMenuBtn.style.marginBottom = 5;
buttonContainer.Add(addMenuBtn);
// 메뉴 아이템 제거
var removeMenuBtn = new Button(() => RemoveDynamicMenu())
{
text = "동적 메뉴 제거"
};
removeMenuBtn.style.marginBottom = 5;
buttonContainer.Add(removeMenuBtn);
// 단축키 변경
var changeShortcutBtn = new Button(() => ChangeShortcut())
{
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;
statusLabel.style.fontSize = 12;
statusLabel.style.color = new Color(0.7f, 0.7f, 0.7f);
buttonContainer.Add(statusLabel);
UpdateStatusLabel(statusLabel);
}
/// <summary>
/// 파일 열기/닫기 상태를 토글합니다.
/// </summary>
private void ToggleFileOpen()
{
_isFileOpen = !_isFileOpen;
var saveItem = _menuModel?.FindMenuItem("file_save");
var saveAsItem = _menuModel?.FindMenuItem("file_save_as");
if (saveItem != null)
{
saveItem.IsEnabled = _isFileOpen;
if (_menuView != null && _menuView.TryGetMenuItemElement("file_save", out var element))
{
var button = element.Q<Button>("submenu-button");
button?.SetEnabled(_isFileOpen);
}
}
if (saveAsItem != null)
{
saveAsItem.IsEnabled = _isFileOpen;
if (_menuView != null && _menuView.TryGetMenuItemElement("file_save_as", out var element))
{
var button = element.Q<Button>("submenu-button");
button?.SetEnabled(_isFileOpen);
}
}
ULog.Debug($"파일 상태: {(_isFileOpen ? "" : "")} - Save 메뉴 {(_isFileOpen ? "" : "")}");
}
/// <summary>
/// Undo/Redo 상태를 토글합니다.
/// </summary>
private void ToggleUndoRedo()
{
_canUndo = !_canUndo;
_canRedo = !_canRedo;
var undoItem = _menuModel?.FindMenuItem("edit_undo");
var redoItem = _menuModel?.FindMenuItem("edit_redo");
if (undoItem != null)
{
undoItem.IsEnabled = _canUndo;
if (_menuView != null && _menuView.TryGetMenuItemElement("edit_undo", out var element))
{
var button = element.Q<Button>("submenu-button");
button?.SetEnabled(_canUndo);
}
}
if (redoItem != null)
{
redoItem.IsEnabled = _canRedo;
if (_menuView != null && _menuView.TryGetMenuItemElement("edit_redo", out var element))
{
var button = element.Q<Button>("submenu-button");
button?.SetEnabled(_canRedo);
}
}
ULog.Debug($"Undo/Redo 상태: {(_canUndo ? "" : "")}");
}
/// <summary>
/// 동적으로 메뉴를 추가합니다.
/// </summary>
private void AddDynamicMenu()
{
if (_menuModel == null || _menuView == null) return;
// 이미 존재하는지 확인
if (_menuModel.FindMenuItem("dynamic") != null)
{
ULog.Warning("동적 메뉴가 이미 존재합니다.");
return;
}
var dynamicMenu = new UTKMenuItemData("dynamic", "동적 메뉴");
dynamicMenu.AddSubMenuItem(new UTKMenuItemData("dynamic_action1", "액션 1", new DebugLogCommand("동적 액션 1")));
dynamicMenu.AddSubMenuItem(new UTKMenuItemData("dynamic_action2", "액션 2", new DebugLogCommand("동적 액션 2")));
_menuModel.AddMenuItem(dynamicMenu);
// View 갱신 (기존 메뉴 제거 후 재생성)
_menuView.ClearMenuItems();
if (_menuView.MenuContainer != null)
{
_menuView.CreateMenuItems(_menuModel.MenuItems, _menuView.MenuContainer);
}
ULog.Debug("동적 메뉴가 추가되었습니다.");
}
/// <summary>
/// 동적 메뉴를 제거합니다.
/// </summary>
private void RemoveDynamicMenu()
{
if (_menuModel == null || _menuView == null) return;
if (_menuModel.RemoveMenuItem("dynamic"))
{
// View 갱신
_menuView.ClearMenuItems();
if (_menuView.MenuContainer != null)
{
_menuView.CreateMenuItems(_menuModel.MenuItems, _menuView.MenuContainer);
}
ULog.Debug("동적 메뉴가 제거되었습니다.");
}
else
{
ULog.Warning("제거할 동적 메뉴를 찾을 수 없습니다.");
}
}
/// <summary>
/// Save 메뉴의 단축키를 변경합니다.
/// </summary>
private void ChangeShortcut()
{
if (_menuModel == null || _menuView == null) return;
var saveItem = _menuModel.FindMenuItem("file_save");
if (saveItem != null)
{
// 단축키 변경
string newShortcut = saveItem.Shortcut == "Ctrl+S" ? "Ctrl+Shift+S" : "Ctrl+S";
saveItem.Shortcut = newShortcut;
// View 업데이트
_menuView.UpdateShortcutText("file_save", newShortcut);
ULog.Debug($"Save 단축키 변경: {newShortcut}");
}
}
/// <summary>
/// 상태 레이블을 업데이트합니다.
/// </summary>
private void UpdateStatusLabel(Label label)
{
label.text = $"파일: {(_isFileOpen ? "" : "")}\n" +
$"Undo: {(_canUndo ? "" : "")}\n" +
$"Redo: {(_canRedo ? "" : "")}";
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>
private void HandleMenuItemClicked(UTKMenuItemData itemData)
{
if (itemData == null) return;
ULog.Debug($"메뉴 클릭: {itemData.ItemId} - {itemData.DisplayName}");
// Command 실행
if (itemData.Command != null)
{
itemData.Command.Execute(itemData.CommandParameter);
}
}
private void HandleMenuItemClicked2(UTKMenuItemData itemData)
{
if (itemData == null) return;
ULog.Debug($"메뉴2 클릭: {itemData.ItemId} - {itemData.DisplayName}");
// Command 실행
if (itemData.Command != null)
{
itemData.Command.Execute(itemData.CommandParameter);
}
}
/// <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()
{
// 이벤트 구독 해제
if (_menuView != null)
{
_menuView.OnMenuItemClicked -= HandleMenuItemClicked;
_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 정리 완료");
}
}
}