#nullable enable using System; using System.Collections.Generic; using System.Linq; using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.UIElements; using UVC.Util; namespace UVC.UIToolkit { /// /// 이미지가 포함된 그리드/리스트를 표시하는 UIToolkit 컴포넌트입니다. /// PrefabGrid의 UIToolkit 버전입니다. /// /// 개요: /// /// UTKImageList는 Unity UI Toolkit의 ListView를 래핑하여 이미지+텍스트 형태의 /// 아이템 목록을 2열 그리드로 표시합니다. 검색 필터링, 드래그 앤 드롭 기능을 제공합니다. /// /// /// 주요 기능: /// /// 이미지+텍스트 형태의 아이템을 2열 그리드로 표시 /// 실시간 검색 필터링 (2글자 이상) /// 드래그 앤 드롭 지원 /// 가상화를 통한 대량 데이터 성능 최적화 /// /// /// UXML 사용 예시: /// /// /// /// /// ]]> /// /// C# 사용 예시: /// ("image-list"); /// /// // 2. 데이터 구성 - 이미지 경로와 이름을 가진 아이템들 /// var data = new List /// { /// new UTKImageListItemData /// { /// itemName = "의자", /// imagePath = "Prefabs/Thumbnails/chair", /// prefabPath = "Prefabs/Furniture/Chair" /// }, /// new UTKImageListItemData /// { /// itemName = "책상", /// imagePath = "Prefabs/Thumbnails/desk", /// prefabPath = "Prefabs/Furniture/Desk" /// }, /// new UTKImageListItemData /// { /// itemName = "소파", /// imagePath = "Prefabs/Thumbnails/sofa", /// prefabPath = "Prefabs/Furniture/Sofa" /// } /// }; /// /// // 3. 데이터 설정 /// imageList.SetData(data); /// /// // 4. 아이템 클릭 이벤트 구독 /// imageList.OnItemClick += (item) => /// { /// Debug.Log($"클릭된 아이템: {item.itemName}"); /// }; /// /// // 5. 드래그 앤 드롭 이벤트 (씬에 프리팹 배치용) /// imageList.OnItemBeginDrag += (item, screenPos) => /// { /// Debug.Log($"드래그 시작: {item.itemName}"); /// }; /// /// imageList.OnItemDrag += (item, screenPos) => /// { /// // 드래그 중 - 3D 미리보기 위치 업데이트 등 /// }; /// /// imageList.OnItemEndDrag += (item, screenPos) => /// { /// Debug.Log($"드래그 종료: {item.itemName}"); /// }; /// /// imageList.OnItemDrop += (item) => /// { /// // 드롭 완료 - 프리팹 인스턴스화 /// if (!string.IsNullOrEmpty(item.prefabPath)) /// { /// var prefab = Resources.Load(item.prefabPath); /// Instantiate(prefab, dropPosition, Quaternion.identity); /// } /// }; /// /// // 6. 리스트 영역 밖으로 드래그 시 3D 미리보기 표시 /// imageList.OnDragExitList += (item, screenPos) => /// { /// // 리스트 밖 = 3D 씬 영역 /// Show3DPreview(item.prefabPath, screenPos); /// }; /// /// // 7. 리스트 영역 안으로 다시 들어왔을 때 미리보기 숨김 /// imageList.OnDragEnterList += (item, screenPos) => /// { /// Hide3DPreview(); /// }; /// /// // 8. 드래그 영역 설정 (부모 윈도우 영역 기준으로 체크) /// imageList.DragBoundsElement = parentWindow; /// /// // 9. 드래그 고스트 이미지 따라다니기 설정 /// imageList.DragImageFollowCursor = true; /// /// // 10. 검색 실행 (2글자 이상) /// imageList.ApplySearch("의자"); /// /// // 11. 현재 검색어 확인 /// string currentQuery = imageList.SearchQuery; /// /// // 12. 아이템 추가/제거 /// var newItem = new UTKImageListItemData { itemName = "새 아이템" }; /// imageList.AddItem(newItem); /// imageList.RemoveItem(newItem); /// /// // 13. 전체 삭제 /// imageList.Clear(); /// /// // 14. 아이템 개수 확인 /// int count = imageList.ItemCount; /// /// // 15. 리스트 표시/숨김 /// imageList.Show(); /// imageList.Hide(); /// /// // 16. 리소스 해제 (OnDestroy에서 호출) /// imageList.Dispose(); /// ]]> /// /// 관련 리소스: /// /// Resources/UIToolkit/List/UTKImageList.uxml - 메인 레이아웃 /// Resources/UIToolkit/List/UTKImageListItem.uxml - 행 템플릿 (2열) /// Resources/UIToolkit/List/UTKImageListUss.uss - 스타일 /// /// [UxmlElement] public partial class UTKImageList : VisualElement, IDisposable { #region 상수 (Constants) /// 메인 UXML 파일 경로 (Resources 폴더 기준) private const string UXML_PATH = "UIToolkit/List/UTKImageList"; private const string USS_PATH = "UIToolkit/List/UTKImageListUss"; /// 아이템 UXML 파일 경로 (Resources 폴더 기준) private const string ITEM_UXML_PATH = "UIToolkit/List/UTKImageListItem"; /// 최소 검색어 길이 private const int MIN_SEARCH_LENGTH = 2; /// 이미지 로딩 디바운스 시간 (ms) private const int IMAGE_LOAD_DEBOUNCE_MS = 50; /// 한 행에 표시할 아이템 수 private const int ITEMS_PER_ROW = 2; #endregion #region 캐싱된 리소스 (Cached Resources) /// 아이템 UXML 템플릿 (재사용을 위해 캐싱) private VisualTreeAsset? _itemTemplate; #endregion #region UI 컴포넌트 참조 (UI Component References) /// 검색어 입력 필드 private UTKInputField? _searchField; /// Unity UI Toolkit의 ListView 컴포넌트 private ListView? _listView; /// 검색어 지우기 버튼 (UTKButton) private UTKButton? _clearButton; /// 검색 결과 건수 라벨 private Label? _searchResultLabel; #endregion #region 내부 데이터 (Internal Data) /// 원본 데이터 (검색 필터 해제 시 복원용) private List _originalData = new(); /// 필터링된 데이터 (검색 결과) private List _filteredData = new(); /// 행 단위로 묶인 데이터 (ListView용) private List _rowData = new(); /// 항목 ID 자동 생성을 위한 시드 값 private int _idSeed = 1; /// 현재 검색 모드 여부 private bool _isSearchMode; /// 이미지 로딩 취소 토큰 소스 (메모리 누수 방지) private CancellationTokenSource? _imageLoadCts; /// 로드된 이미지 스프라이트 캐시 (경로 → 스프라이트) private readonly Dictionary _spriteCache = new(); /// 현재 로딩 중인 이미지 경로 추적 (중복 로딩 방지) private readonly HashSet _loadingImages = new(); /// RowData 객체 풀 (GC 부담 감소) private readonly Queue _rowDataPool = new(); #endregion #region 내부 클래스 (Internal Classes) /// /// 행 데이터를 저장하는 클래스입니다. /// 한 행에 최대 2개의 아이템을 포함합니다. /// private class RowData { public UTKImageListItemData? LeftItem; public UTKImageListItemData? RightItem; } #endregion #region IDisposable private bool _disposed; #endregion #region 공개 속성 (Public Properties) /// /// 드래그 시 이미지가 커서를 따라다니도록 설정합니다. /// 기본값은 true입니다. /// public bool DragImageFollowCursor { get; set; } = true; /// /// 드래그 영역 체크에 사용할 요소를 설정합니다. /// null이면 자신(UTKImageList)의 worldBound를 사용합니다. /// 부모 윈도우 등 다른 요소의 영역을 체크하려면 해당 요소를 설정하세요. /// public VisualElement? DragBoundsElement { get; set; } /// /// 현재 검색어를 가져오거나 설정합니다. /// 설정 시 검색 필드의 값만 변경하고 검색은 실행하지 않습니다. /// public string SearchQuery { get => _searchField?.value ?? string.Empty; set { if (_searchField != null) _searchField.value = value; } } /// /// 현재 표시 중인 아이템 수를 반환합니다. /// 검색 모드에서는 필터링된 결과 수를, 아니면 전체 수를 반환합니다. /// public int ItemCount => _isSearchMode ? _filteredData.Count : _originalData.Count; #endregion #region 외부 이벤트 (Public Events) /// /// 아이템 클릭 이벤트입니다. /// 아이템을 클릭하면 해당 데이터와 함께 발생합니다. /// public Action? OnItemClick; /// /// 아이템 드롭 이벤트입니다. /// 드래그 앤 드롭이 완료되면 발생합니다. /// public Action? OnItemDrop; /// /// 드래그 시작 이벤트입니다. /// (아이템 데이터, 화면 좌표) /// public Action? OnItemBeginDrag; /// /// 드래그 중 이벤트입니다. /// (아이템 데이터, 화면 좌표) /// public Action? OnItemDrag; /// /// 드래그 종료 이벤트입니다. /// (아이템 데이터, 화면 좌표) /// public Action? OnItemEndDrag; /// /// 드래그 중 리스트 영역을 벗어났을 때 발생하는 이벤트입니다. /// (아이템 데이터, 화면 좌표) /// 3D 프리팹 미리보기를 표시하는데 사용합니다. /// public Action? OnDragExitList; /// /// 드래그 중 리스트 영역에 다시 진입했을 때 발생하는 이벤트입니다. /// (아이템 데이터, 화면 좌표) /// 3D 프리팹 미리보기를 숨기는데 사용합니다. /// public Action? OnDragEnterList; #endregion #region 생성자 (Constructor) /// /// UTKImageList 컴포넌트를 초기화합니다. /// UXML 템플릿을 로드하고 내부 컴포넌트를 설정합니다. /// public UTKImageList() { // 1. 메인 UXML 로드 및 복제 var visualTree = Resources.Load(UXML_PATH); if (visualTree == null) { Debug.LogError($"[UTKImageList] UXML not found at: {UXML_PATH}"); return; } visualTree.CloneTree(this); // 2. 아이템 템플릿 로드 (성능: 한 번만 로드하여 재사용) _itemTemplate = Resources.Load(ITEM_UXML_PATH); if (_itemTemplate == null) { Debug.LogError($"[UTKImageList] Item UXML not found at: {ITEM_UXML_PATH}"); } // 3. 테마 적용 및 변경 구독 UTKThemeManager.Instance.ApplyThemeToElement(this); SubscribeToThemeChanges(); // USS 로드 (테마 변수 스타일시트 이후에 로드되어야 변수가 해석됨) var uss = Resources.Load(USS_PATH); if (uss != null) { styleSheets.Add(uss); } // 4. UI 요소 참조 획득 _searchField = this.Q("search-field"); _listView = this.Q("main-list-view"); _clearButton = this.Q("clear-btn"); _searchResultLabel = this.Q