#nullable enable using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; using UVC.UIToolkit; /// /// UTKAccordionListWindow의 기능을 테스트하기 위한 샘플 MonoBehaviour입니다. /// 아코디언 리스트 데이터를 생성하고 다양한 이벤트 핸들러를 등록하여 동작을 확인합니다. /// /// 테스트 기능: /// /// 섹션 펼침/접힘 /// 수평 레이아웃 (Head-Content-Tail) /// 그리드 레이아웃 (Image + Caption) /// 검색 필터링 /// 드래그 앤 드롭 이벤트 /// /// public class UTKAccordionListWindowSample : MonoBehaviour { #region 필드 (Fields) [SerializeField] [Tooltip("UI를 표시할 UIDocument 컴포넌트")] public UIDocument? uiDocument; [SerializeField] [Tooltip("드래그 시 고스트 이미지 표시 여부")] private bool showDragGhost = true; [SerializeField] [Tooltip("시작 시 적용할 테마")] private UTKTheme initialTheme = UTKTheme.Dark; private UTKToggle _themeToggle; /// UTKAccordionListWindow 인스턴스 private UTKAccordionListWindow _accordionWindow; /// 드롭 위치 (스크린 좌표) private Vector2 _lastDropScreenPosition; /// 메인 카메라 참조 private Camera? _mainCamera; /// 드래그 중인 3D 프리뷰 오브젝트 private GameObject? _dragPreview3D; /// 드래그 중 리스트 영역 밖에 있는지 여부 private bool _isOutsideListArea; #endregion #region Unity 라이프사이클 private void Start() { // UIDocument 참조 확인 var doc = GetComponent(); if (doc == null) { Debug.LogError("UIDocument가 할당되지 않았습니다."); return; } uiDocument = doc; var toggle = uiDocument.rootVisualElement.Q("toggle"); if (toggle == null) { Debug.LogError("UXML에서 UTKToggle을 찾을 수 없습니다."); return; } _themeToggle = toggle; var window = uiDocument.rootVisualElement.Q("window"); if (window == null) { Debug.LogError("UXML에서 UTKAccordionListWindow를 찾을 수 없습니다."); return; } _accordionWindow = window; UTKThemeManager.Instance.RegisterRoot(uiDocument.rootVisualElement); UTKThemeManager.Instance.SetTheme(initialTheme); _themeToggle.OnValueChanged += (isOn) => { UTKThemeManager.Instance.SetTheme(!isOn ? UTKTheme.Dark : UTKTheme.Light); }; // UTKAccordionListWindow 인스턴스 생성 및 추가 _accordionWindow.ShowDragGhost = showDragGhost; _accordionWindow.Title = "ACCORDION"; _accordionWindow.ShowCloseButton = true; // 테스트 데이터 생성 CreateTestData(); // 이벤트 핸들러 등록 RegisterEventHandlers(); Debug.Log("[UTKAccordionListWindowSample] 초기화 완료"); } private void OnDestroy() { // 3D 프리뷰 정리 Destroy3DPreview(); // 리소스 정리 _accordionWindow?.Dispose(); _accordionWindow = null; } #endregion #region 테스트 데이터 생성 /// /// 테스트용 아코디언 데이터를 생성합니다. /// AccordionSample과 동일한 구조로 데이터를 생성합니다. /// private void CreateTestData() { if (_accordionWindow == null) return; var data = new UTKAccordionData(); // ======================================== // 수평 레이아웃 섹션 1: Settings // ======================================== var settingsSection = new UTKAccordionSectionData { Title = "Settings", IsExpanded = true, LayoutType = UTKAccordionLayoutType.Horizontal, HorizontalItems = new List { new UTKAccordionHorizontalItemData { Head = UTKAccordionContentSpec.FromImage("Prefabs/UI/images/icon_side_tab_library_24"), Content = UTKAccordionContentSpec.FromText("Graphics", "open_graphics"), Tail = new List { UTKAccordionContentSpec.FromIconButton(UTKMaterialIcons.Refresh, 12, "refresh_graphics", "새로고침"), UTKAccordionContentSpec.FromIconButton(UTKMaterialIcons.Settings, 12, "setting_graphics", "설정"), } }, new UTKAccordionHorizontalItemData { Head = UTKAccordionContentSpec.FromImage("Prefabs/UI/images/icon_side_tab_fleet_128"), Content = UTKAccordionContentSpec.FromText("Audio", "open_audio"), Tail = new List { UTKAccordionContentSpec.FromIconButton(UTKMaterialIcons.Refresh, 12, "refresh_audio", "새로고침"), UTKAccordionContentSpec.FromIconButton(UTKMaterialIcons.Settings, 12, "setting_audio", "설정"), } }, new UTKAccordionHorizontalItemData { Head = UTKAccordionContentSpec.FromImage("Prefabs/UI/images/icon_side_tab_explorer_24"), Content = UTKAccordionContentSpec.FromText("Network", "open_network"), Tail = new List { UTKAccordionContentSpec.FromIconButton(UTKMaterialIcons.Refresh, 12, "refresh_network", "새로고침"), } } } }; data.Sections.Add(settingsSection); // ======================================== // 수평 레이아웃 섹션 2: Components // ======================================== var componentsSection = new UTKAccordionSectionData { Title = "Components", IsExpanded = false, LayoutType = UTKAccordionLayoutType.Horizontal, HorizontalItems = new List { new UTKAccordionHorizontalItemData { Content = UTKAccordionContentSpec.FromText("Transform", "open_transform"), }, new UTKAccordionHorizontalItemData { Content = UTKAccordionContentSpec.FromText("Rigidbody", "open_rigidbody"), }, new UTKAccordionHorizontalItemData { Content = UTKAccordionContentSpec.FromText("Collider", "open_collider"), } } }; data.Sections.Add(componentsSection); // ======================================== // 그리드 레이아웃 섹션 1: Vehicles // ======================================== var vehiclesSection = new UTKAccordionSectionData { Title = "Vehicles", IsExpanded = true, LayoutType = UTKAccordionLayoutType.Grid, GridItems = new List { new UTKAccordionGridItemData { Caption = "Forklift", ImagePath = "Simulator/Images/lib_forklift_400x300", PrefabPath = "Simulator/FreeForkLift/Prefabs/Forklift", Tag = "vehicle" }, new UTKAccordionGridItemData { Caption = "Truck", ImagePath = "Simulator/Images/lib_forklift_400x300", PrefabPath = "Simulator/FreeForkLift/Prefabs/Forklift", Tag = "vehicle" } } }; data.Sections.Add(vehiclesSection); // ======================================== // 그리드 레이아웃 섹션 2: Objects // ======================================== var objectsSection = new UTKAccordionSectionData { Title = "Objects", IsExpanded = true, LayoutType = UTKAccordionLayoutType.Grid, GridItems = new List { new UTKAccordionGridItemData { Caption = "Pallet", ImagePath = "Simulator/Images/lib_pallet_400x300", PrefabPath = "Simulator/FreeForkLift/Prefabs/PalletEmpty", Tag = "object" }, new UTKAccordionGridItemData { Caption = "Pallet (Full)", ImagePath = "Simulator/Images/lib_pallet_400x300", PrefabPath = "Simulator/FreeForkLift/Prefabs/PalletEmpty", Tag = "object" }, new UTKAccordionGridItemData { Caption = "Box", ImagePath = "Simulator/Images/lib_pallet_400x300", PrefabPath = "Simulator/FreeForkLift/Prefabs/PalletEmpty", Tag = "object" } } }; data.Sections.Add(objectsSection); // ======================================== // 그리드 레이아웃 섹션 3: Characters // ======================================== var charactersSection = new UTKAccordionSectionData { Title = "Characters", IsExpanded = true, LayoutType = UTKAccordionLayoutType.Grid, GridItems = new List { new UTKAccordionGridItemData { Caption = "Worker", ImagePath = "Simulator/Images/lib_worker_400x300", PrefabPath = "Simulator/CharCrafter – Free Preset Characters Pack (Vol. 1)/Prefabs/Male Young Guy", Tag = "character" }, new UTKAccordionGridItemData { Caption = "Manager", ImagePath = "Simulator/Images/lib_worker_400x300", PrefabPath = "Simulator/CharCrafter – Free Preset Characters Pack (Vol. 1)/Prefabs/Male Young Guy", Tag = "character" } } }; data.Sections.Add(charactersSection); // 데이터 설정 _accordionWindow.SetData(data); Debug.Log($"[UTKAccordionListWindowSample] 테스트 데이터 생성 완료: {data.Sections.Count}개 섹션"); } #endregion #region 이벤트 핸들러 등록 /// /// UTKAccordionListWindow의 이벤트 핸들러들을 등록합니다. /// private void RegisterEventHandlers() { if (_accordionWindow == null) return; // ======================================== // 수평 아이템 이벤트 // ======================================== // 수평 아이템 클릭 이벤트 _accordionWindow.OnHorizontalItemClick += (item, contentSpec) => { var text = contentSpec?.Text ?? item.Content?.Text ?? "Unknown"; var actionId = contentSpec?.ActionId ?? "none"; Debug.Log($"[수평 아이템 클릭] {text} (ActionId: {actionId})"); // ActionId에 따라 분기 처리 HandleHorizontalItemAction(actionId); }; // 수평 아이템 아이콘 버튼 클릭 이벤트 _accordionWindow.OnHorizontalItemIconClick += (item, iconSpec) => { var actionId = iconSpec.ActionId ?? "none"; var itemText = item.Content?.Text ?? "Unknown"; Debug.Log($"[아이콘 버튼 클릭] {itemText} - {actionId}"); HandleHorizontalItemAction(actionId); }; // ======================================== // 그리드 아이템 이벤트 // ======================================== // 그리드 아이템 클릭 이벤트 _accordionWindow.OnGridItemClick += (item) => { Debug.Log($"[그리드 아이템 클릭] {item.Caption} (Tag: {item.Tag})"); }; // 드래그 시작 이벤트 _accordionWindow.OnGridItemBeginDrag += (item, position) => { Debug.Log($"[드래그 시작] {item.Caption} at {position}"); }; // 드래그 중 이벤트 _accordionWindow.OnGridItemDrag += (item, position) => { // 3D 프리뷰가 있으면 위치 업데이트 if (_dragPreview3D != null) { Update3DPreviewPosition(position); } }; // 드래그 종료 이벤트 _accordionWindow.OnGridItemEndDrag += (item, position) => { Debug.Log($"[드래그 종료] {item.Caption} at {position}"); _lastDropScreenPosition = position; // 드롭 이벤트에서 처리하므로 여기서는 프리뷰를 제거하지 않음 }; // 리스트 영역 이탈 이벤트 (3D 프리팹 미리보기 표시) _accordionWindow.OnDragExitList += (item, position) => { Debug.Log($"[리스트 영역 이탈] {item.Caption} - 3D 프리뷰 생성"); _isOutsideListArea = true; Create3DPreview(item, position); }; // 리스트 영역 진입 이벤트 (3D 프리팹 미리보기 숨김) _accordionWindow.OnDragEnterList += (item, position) => { Debug.Log($"[리스트 영역 진입] {item.Caption} - 3D 프리뷰 제거"); _isOutsideListArea = false; Destroy3DPreview(); }; // 그리드 아이템 드롭 이벤트 _accordionWindow.OnGridItemDrop += (item) => { Debug.Log($"[드롭] {item.Caption} - PrefabPath: {item.PrefabPath}, Outside: {_isOutsideListArea}"); // 리스트 영역 밖에서 드롭한 경우에만 프리팹 생성 if (!_isOutsideListArea) { Debug.Log("[UTKAccordionListWindowSample] 리스트 영역 내부에서 드롭 - 프리팹 생성 안함"); _isOutsideListArea = false; return; } if (string.IsNullOrEmpty(item.PrefabPath)) { Debug.LogWarning("[UTKAccordionListWindowSample] PrefabPath가 설정되지 않았습니다."); Destroy3DPreview(); _isOutsideListArea = false; return; } // 프리팹 로드 var prefab = Resources.Load(item.PrefabPath); if (prefab == null) { Debug.LogWarning($"[UTKAccordionListWindowSample] 프리팹을 찾을 수 없습니다: {item.PrefabPath}"); Destroy3DPreview(); _isOutsideListArea = false; return; } // 월드 좌표 계산 Vector3 worldPosition = ScreenToWorldPosition(_lastDropScreenPosition); // 프리팹 인스턴스화 var instance = Instantiate(prefab, worldPosition, Quaternion.identity); Debug.Log($"[UTKAccordionListWindowSample] 프리팹 생성됨: {instance.name} at {worldPosition}"); // 3D 프리뷰 정리 및 상태 초기화 Destroy3DPreview(); _isOutsideListArea = false; }; // ======================================== // 섹션 이벤트 // ======================================== // 섹션 펼침/접힘 이벤트 _accordionWindow.OnSectionToggled += (section, isExpanded) => { Debug.Log($"[섹션 토글] {section.Title} - {(isExpanded ? "펼침" : "접힘")}"); }; // ======================================== // 윈도우 이벤트 // ======================================== // 윈도우 닫기 이벤트 _accordionWindow.OnClosed += () => { Debug.Log("[윈도우 닫힘]"); }; Debug.Log("[UTKAccordionListWindowSample] 이벤트 핸들러 등록 완료"); } /// /// 수평 아이템의 ActionId에 따라 동작을 처리합니다. /// private void HandleHorizontalItemAction(string? actionId) { switch (actionId) { case "open_graphics": Debug.Log(" → Graphics 설정 열기"); break; case "refresh_graphics": Debug.Log(" → Graphics 새로고침"); break; case "setting_graphics": Debug.Log(" → Graphics 상세 설정"); break; case "open_audio": Debug.Log(" → Audio 설정 열기"); break; case "refresh_audio": Debug.Log(" → Audio 새로고침"); break; case "setting_audio": Debug.Log(" → Audio 상세 설정"); break; case "open_network": Debug.Log(" → Network 설정 열기"); break; case "refresh_network": Debug.Log(" → Network 새로고침"); break; case "open_transform": Debug.Log(" → Transform 컴포넌트 열기"); break; case "open_rigidbody": Debug.Log(" → Rigidbody 컴포넌트 열기"); break; case "open_collider": Debug.Log(" → Collider 컴포넌트 열기"); break; default: Debug.Log($" → 알 수 없는 액션: {actionId}"); break; } } #endregion #region 좌표 변환 (Coordinate Conversion) /// /// UI Toolkit 좌표를 Unity Screen 좌표로 변환합니다. /// private Vector2 UIToolkitToScreenPosition(Vector2 uiToolkitPosition) { return new Vector2(uiToolkitPosition.x, Screen.height - uiToolkitPosition.y); } /// /// 스크린 좌표를 월드 좌표로 변환합니다. /// private Vector3 ScreenToWorldPosition(Vector2 uiToolkitPosition) { _mainCamera ??= Camera.main; if (_mainCamera == null) { Debug.LogWarning("[UTKAccordionListWindowSample] 메인 카메라를 찾을 수 없습니다."); return Vector3.zero; } Vector2 screenPosition = UIToolkitToScreenPosition(uiToolkitPosition); Ray ray = _mainCamera.ScreenPointToRay(screenPosition); // 바닥면(Y=0)과의 교차점 계산 Plane groundPlane = new Plane(Vector3.up, Vector3.zero); if (groundPlane.Raycast(ray, out float distance)) { return ray.GetPoint(distance); } // 물리 레이캐스트 if (Physics.Raycast(ray, out RaycastHit hit, 100f)) { return hit.point; } return ray.GetPoint(10f); } #endregion #region 3D 프리뷰 (3D Preview) /// /// 드래그 중 3D 프리뷰 오브젝트를 생성합니다. /// private void Create3DPreview(UTKAccordionGridItemData item, Vector2 screenPosition) { Destroy3DPreview(); if (string.IsNullOrEmpty(item.PrefabPath)) return; var prefab = Resources.Load(item.PrefabPath); if (prefab == null) { Debug.LogWarning($"[UTKAccordionListWindowSample] 프리팹을 찾을 수 없습니다: {item.PrefabPath}"); return; } Vector3 worldPosition = ScreenToWorldPosition(screenPosition); _dragPreview3D = Instantiate(prefab, worldPosition, Quaternion.identity); _dragPreview3D.name = $"DragPreview_{item.Caption}"; SetPreviewTransparency(_dragPreview3D, 0.6f); DisableColliders(_dragPreview3D); Debug.Log($"[UTKAccordionListWindowSample] 3D 프리뷰 생성됨: {_dragPreview3D.name} at {worldPosition}"); } /// /// 3D 프리뷰 오브젝트를 제거합니다. /// private void Destroy3DPreview() { if (_dragPreview3D != null) { Destroy(_dragPreview3D); _dragPreview3D = null; } } /// /// 3D 프리뷰 오브젝트의 위치를 업데이트합니다. /// private void Update3DPreviewPosition(Vector2 screenPosition) { if (_dragPreview3D == null) return; _dragPreview3D.transform.position = ScreenToWorldPosition(screenPosition); } /// /// 오브젝트의 모든 렌더러를 반투명하게 설정합니다. /// private void SetPreviewTransparency(GameObject obj, float alpha) { var renderers = obj.GetComponentsInChildren(); foreach (var renderer in renderers) { foreach (var material in renderer.materials) { if (material.HasProperty("_Color")) { var color = material.color; color.a = alpha; material.color = color; material.SetFloat("_Mode", 3); material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); material.SetInt("_ZWrite", 0); material.DisableKeyword("_ALPHATEST_ON"); material.EnableKeyword("_ALPHABLEND_ON"); material.DisableKeyword("_ALPHAPREMULTIPLY_ON"); material.renderQueue = 3000; } } } } /// /// 오브젝트의 모든 콜라이더를 비활성화합니다. /// private void DisableColliders(GameObject obj) { var colliders = obj.GetComponentsInChildren(); foreach (var collider in colliders) { collider.enabled = false; } } #endregion #region 에디터 테스트용 메서드 /// /// 모든 섹션을 펼칩니다. /// [ContextMenu("Expand All Sections")] public void ExpandAllSections() { _accordionWindow?.ExpandAll(); Debug.Log("[UTKAccordionListWindowSample] 모든 섹션 펼침"); } /// /// 모든 섹션을 접습니다. /// [ContextMenu("Collapse All Sections")] public void CollapseAllSections() { _accordionWindow?.CollapseAll(); Debug.Log("[UTKAccordionListWindowSample] 모든 섹션 접힘"); } /// /// 검색을 테스트합니다. /// [ContextMenu("Test Search (Pallet)")] public void TestSearch() { _accordionWindow?.Search("Pallet"); Debug.Log("[UTKAccordionListWindowSample] 검색 테스트: 'Pallet'"); } /// /// 검색을 초기화합니다. /// [ContextMenu("Clear Search")] public void ClearSearchTest() { _accordionWindow?.ClearSearch(); Debug.Log("[UTKAccordionListWindowSample] 검색 초기화"); } /// /// 모든 데이터를 제거합니다. /// [ContextMenu("Clear All Data")] public void ClearAllData() { _accordionWindow?.Clear(); Debug.Log("[UTKAccordionListWindowSample] 모든 데이터 제거됨"); } /// /// 윈도우를 다시 표시합니다. /// [ContextMenu("Show Window")] public void ShowWindow() { _accordionWindow?.Show(); } /// /// 데이터를 다시 로드합니다. /// [ContextMenu("Reload Data")] public void ReloadData() { CreateTestData(); Debug.Log("[UTKAccordionListWindowSample] 데이터 다시 로드됨"); } /// /// 새로운 통합 API(UTKAccordionItemData)를 사용하여 데이터를 생성합니다. /// TreeView 기반 구현에 최적화된 데이터 구조입니다. /// [ContextMenu("Load Data (New API)")] public void LoadDataWithNewAPI() { if (_accordionWindow == null) return; var roots = new List(); // ======================================== // 섹션 1: Settings (수평 레이아웃) // ======================================== var settingsSection = UTKAccordionItemData.CreateSection("Settings", isExpanded: true); settingsSection .AddHorizontalItem( head: UTKAccordionContentSpec.FromImage("Prefabs/UI/images/icon_side_tab_library_128"), content: UTKAccordionContentSpec.FromText("Graphics", "open_graphics"), tail: new List { UTKAccordionContentSpec.FromIconButton(UTKMaterialIcons.Refresh, 12, "refresh_graphics", "새로고침"), UTKAccordionContentSpec.FromIconButton(UTKMaterialIcons.Settings, 12, "setting_graphics", "설정"), }) .AddHorizontalItem( head: UTKAccordionContentSpec.FromImage("Prefabs/UI/images/icon_side_tab_fleet_128"), content: UTKAccordionContentSpec.FromText("Audio", "open_audio"), tail: new List { UTKAccordionContentSpec.FromIconButton(UTKMaterialIcons.Refresh, 12, "refresh_audio", "새로고침"), }); roots.Add(settingsSection); // ======================================== // 섹션 2: Vehicles (그리드 레이아웃) // ======================================== var vehiclesSection = UTKAccordionItemData.CreateSection("Vehicles", isExpanded: true); vehiclesSection .AddGridItem("Forklift", "Simulator/Images/lib_forklift_400x300", "Simulator/FreeForkLift/Prefabs/Forklift", "vehicle") .AddGridItem("Truck", "Simulator/Images/lib_forklift_400x300", "Simulator/FreeForkLift/Prefabs/Forklift", "vehicle"); roots.Add(vehiclesSection); // ======================================== // 섹션 3: Objects (그리드 레이아웃) // ======================================== var objectsSection = UTKAccordionItemData.CreateSection("Objects", isExpanded: true); objectsSection .AddGridItem("Pallet", "Simulator/Images/lib_pallet_400x300", "Simulator/FreeForkLift/Prefabs/PalletEmpty", "object") .AddGridItem("Box", "Simulator/Images/lib_pallet_400x300", "Simulator/FreeForkLift/Prefabs/PalletEmpty", "object"); roots.Add(objectsSection); // 새로운 통합 API로 데이터 설정 _accordionWindow.SetData(roots); Debug.Log($"[UTKAccordionListWindowSample] 새 API로 데이터 생성 완료: {roots.Count}개 섹션"); } #endregion }