UTKAccordionList 개발 완료
This commit is contained in:
1602
Assets/Scripts/UVC/UIToolkit/List/UTKAccordionList.cs
Normal file
1602
Assets/Scripts/UVC/UIToolkit/List/UTKAccordionList.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 11db88c46df8e6242bad6972aa1db2f7
|
||||
520
Assets/Scripts/UVC/UIToolkit/List/UTKAccordionListItemData.cs
Normal file
520
Assets/Scripts/UVC/UIToolkit/List/UTKAccordionListItemData.cs
Normal file
@@ -0,0 +1,520 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UVC.UIToolkit.List
|
||||
{
|
||||
/// <summary>
|
||||
/// 콘텐츠 종류를 정의합니다.
|
||||
/// </summary>
|
||||
public enum UTKAccordionContentKind
|
||||
{
|
||||
None,
|
||||
Text,
|
||||
Image,
|
||||
IconButton
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 아이템 노드 타입을 정의합니다.
|
||||
/// </summary>
|
||||
public enum UTKAccordionNodeType
|
||||
{
|
||||
/// <summary>섹션 (펼침/접힘 가능한 부모 노드)</summary>
|
||||
Section,
|
||||
/// <summary>수평 아이템 (Head-Content-Tail)</summary>
|
||||
HorizontalItem,
|
||||
/// <summary>그리드 아이템 (Image + Caption)</summary>
|
||||
GridItem,
|
||||
/// <summary>그리드 행 (2개의 GridItem을 포함하는 컨테이너)</summary>
|
||||
GridRow
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 콘텐츠 명세 (Head/Content/Tail 각 요소 정의)
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class UTKAccordionContentSpec
|
||||
{
|
||||
/// <summary>콘텐츠 종류</summary>
|
||||
public UTKAccordionContentKind Kind { get; set; } = UTKAccordionContentKind.None;
|
||||
|
||||
/// <summary>표시 텍스트</summary>
|
||||
public string? Text { get; set; }
|
||||
|
||||
/// <summary>이미지 경로 (Resources 기준)</summary>
|
||||
public string? ImagePath { get; set; }
|
||||
|
||||
/// <summary>이미지 색상</summary>
|
||||
public Color ImageColor { get; set; } = Color.white;
|
||||
|
||||
/// <summary>아이콘 이름 또는 경로</summary>
|
||||
public string? IconName { get; set; }
|
||||
|
||||
/// <summary>툴팁 텍스트</summary>
|
||||
public string? Tooltip { get; set; }
|
||||
|
||||
/// <summary>클릭 이벤트 식별용 액션 ID</summary>
|
||||
public string? ActionId { get; set; }
|
||||
|
||||
/// <summary>사용자 정의 데이터</summary>
|
||||
public object? UserData { get; set; }
|
||||
|
||||
/// <summary>텍스트 콘텐츠 스펙을 생성합니다.</summary>
|
||||
public static UTKAccordionContentSpec FromText(string text, string? actionId = null, string? tooltip = null)
|
||||
=> new UTKAccordionContentSpec
|
||||
{
|
||||
Kind = UTKAccordionContentKind.Text,
|
||||
Text = text,
|
||||
ActionId = actionId,
|
||||
Tooltip = tooltip
|
||||
};
|
||||
|
||||
/// <summary>이미지 콘텐츠 스펙을 생성합니다.</summary>
|
||||
public static UTKAccordionContentSpec FromImage(string path, Color? color = null, string? tooltip = null)
|
||||
=> new UTKAccordionContentSpec
|
||||
{
|
||||
Kind = UTKAccordionContentKind.Image,
|
||||
ImagePath = path,
|
||||
ImageColor = color ?? Color.white,
|
||||
Tooltip = tooltip
|
||||
};
|
||||
|
||||
/// <summary>아이콘 버튼 스펙을 생성합니다.</summary>
|
||||
public static UTKAccordionContentSpec FromIconButton(string iconName, string? actionId = null, string? tooltip = null, object? userData = null)
|
||||
=> new UTKAccordionContentSpec
|
||||
{
|
||||
Kind = UTKAccordionContentKind.IconButton,
|
||||
IconName = iconName,
|
||||
ActionId = actionId,
|
||||
Tooltip = tooltip,
|
||||
UserData = userData
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 통합 아코디언 아이템 데이터 (TreeView용)
|
||||
/// 섹션, 수평 아이템, 그리드 아이템을 하나의 타입으로 통합합니다.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class UTKAccordionItemData : IDisposable
|
||||
{
|
||||
#region 공통 필드
|
||||
|
||||
/// <summary>내부 고유 ID (TreeView용, 자동 할당)</summary>
|
||||
public int id;
|
||||
|
||||
/// <summary>외부 시스템 연동용 식별자</summary>
|
||||
public string? externalId;
|
||||
|
||||
/// <summary>노드 타입 (Section, HorizontalItem, GridItem)</summary>
|
||||
public UTKAccordionNodeType nodeType;
|
||||
|
||||
/// <summary>표시 이름 (섹션 제목 또는 아이템 이름)</summary>
|
||||
public string name = string.Empty;
|
||||
|
||||
/// <summary>자식 아이템 리스트 (섹션인 경우)</summary>
|
||||
public List<UTKAccordionItemData>? children;
|
||||
|
||||
/// <summary>펼침 상태 (섹션인 경우)</summary>
|
||||
public bool isExpanded = true;
|
||||
|
||||
/// <summary>사용자 정의 태그</summary>
|
||||
public string? tag;
|
||||
|
||||
/// <summary>사용자 정의 데이터</summary>
|
||||
public object? userData;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 수평 아이템 전용 필드
|
||||
|
||||
/// <summary>헤드 영역 콘텐츠 (수평 아이템)</summary>
|
||||
public UTKAccordionContentSpec? head;
|
||||
|
||||
/// <summary>콘텐츠 영역 (수평 아이템)</summary>
|
||||
public UTKAccordionContentSpec? content;
|
||||
|
||||
/// <summary>테일 영역 아이콘 버튼 리스트 (수평 아이템)</summary>
|
||||
public List<UTKAccordionContentSpec>? tail;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 그리드 아이템 전용 필드
|
||||
|
||||
/// <summary>이미지 경로 (그리드 아이템)</summary>
|
||||
public string? imagePath;
|
||||
|
||||
/// <summary>캡션 텍스트 (그리드 아이템)</summary>
|
||||
public string? caption;
|
||||
|
||||
/// <summary>캡션 색상 (그리드 아이템)</summary>
|
||||
public Color? captionColor;
|
||||
|
||||
/// <summary>캡션 폰트 크기 (그리드 아이템)</summary>
|
||||
public int? captionSize;
|
||||
|
||||
/// <summary>프리팹 경로 (드래그 앤 드롭용)</summary>
|
||||
public string? prefabPath;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 그리드 행 전용 필드
|
||||
|
||||
/// <summary>왼쪽 그리드 아이템 (GridRow 전용)</summary>
|
||||
public UTKAccordionItemData? leftGridItem;
|
||||
|
||||
/// <summary>오른쪽 그리드 아이템 (GridRow 전용)</summary>
|
||||
public UTKAccordionItemData? rightGridItem;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 팩토리 메서드
|
||||
|
||||
/// <summary>섹션을 생성합니다.</summary>
|
||||
public static UTKAccordionItemData CreateSection(string title, bool isExpanded = true)
|
||||
=> new UTKAccordionItemData
|
||||
{
|
||||
nodeType = UTKAccordionNodeType.Section,
|
||||
name = title,
|
||||
isExpanded = isExpanded,
|
||||
children = new List<UTKAccordionItemData>()
|
||||
};
|
||||
|
||||
/// <summary>수평 아이템을 생성합니다.</summary>
|
||||
public static UTKAccordionItemData CreateHorizontalItem(
|
||||
UTKAccordionContentSpec? head = null,
|
||||
UTKAccordionContentSpec? content = null,
|
||||
List<UTKAccordionContentSpec>? tail = null,
|
||||
string? tag = null,
|
||||
object? userData = null)
|
||||
=> new UTKAccordionItemData
|
||||
{
|
||||
nodeType = UTKAccordionNodeType.HorizontalItem,
|
||||
name = content?.Text ?? string.Empty,
|
||||
head = head,
|
||||
content = content,
|
||||
tail = tail,
|
||||
tag = tag,
|
||||
userData = userData
|
||||
};
|
||||
|
||||
/// <summary>그리드 아이템을 생성합니다.</summary>
|
||||
public static UTKAccordionItemData CreateGridItem(
|
||||
string caption,
|
||||
string? imagePath = null,
|
||||
string? prefabPath = null,
|
||||
string? tag = null,
|
||||
object? userData = null)
|
||||
=> new UTKAccordionItemData
|
||||
{
|
||||
nodeType = UTKAccordionNodeType.GridItem,
|
||||
name = caption,
|
||||
caption = caption,
|
||||
imagePath = imagePath,
|
||||
prefabPath = prefabPath,
|
||||
tag = tag,
|
||||
userData = userData
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
#region 헬퍼 메서드
|
||||
|
||||
/// <summary>섹션에 자식 아이템을 추가합니다.</summary>
|
||||
public UTKAccordionItemData AddChild(UTKAccordionItemData child)
|
||||
{
|
||||
children ??= new List<UTKAccordionItemData>();
|
||||
children.Add(child);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>섹션에 수평 아이템을 추가합니다.</summary>
|
||||
public UTKAccordionItemData AddHorizontalItem(
|
||||
UTKAccordionContentSpec? head = null,
|
||||
UTKAccordionContentSpec? content = null,
|
||||
List<UTKAccordionContentSpec>? tail = null,
|
||||
string? tag = null)
|
||||
{
|
||||
return AddChild(CreateHorizontalItem(head, content, tail, tag));
|
||||
}
|
||||
|
||||
/// <summary>섹션에 그리드 아이템을 추가합니다.</summary>
|
||||
public UTKAccordionItemData AddGridItem(
|
||||
string caption,
|
||||
string? imagePath = null,
|
||||
string? prefabPath = null,
|
||||
string? tag = null)
|
||||
{
|
||||
return AddChild(CreateGridItem(caption, imagePath, prefabPath, tag));
|
||||
}
|
||||
|
||||
/// <summary>자식이 있는지 확인합니다.</summary>
|
||||
public bool HasChildren => children != null && children.Count > 0;
|
||||
|
||||
/// <summary>섹션인지 확인합니다.</summary>
|
||||
public bool IsSection => nodeType == UTKAccordionNodeType.Section;
|
||||
|
||||
/// <summary>수평 아이템인지 확인합니다.</summary>
|
||||
public bool IsHorizontalItem => nodeType == UTKAccordionNodeType.HorizontalItem;
|
||||
|
||||
/// <summary>그리드 아이템인지 확인합니다.</summary>
|
||||
public bool IsGridItem => nodeType == UTKAccordionNodeType.GridItem;
|
||||
|
||||
/// <summary>그리드 행인지 확인합니다.</summary>
|
||||
public bool IsGridRow => nodeType == UTKAccordionNodeType.GridRow;
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
head = null;
|
||||
content = null;
|
||||
tail?.Clear();
|
||||
tail = null;
|
||||
|
||||
if (children != null)
|
||||
{
|
||||
foreach (var child in children)
|
||||
{
|
||||
child.Dispose();
|
||||
}
|
||||
children.Clear();
|
||||
children = null;
|
||||
}
|
||||
|
||||
externalId = null;
|
||||
name = string.Empty;
|
||||
tag = null;
|
||||
userData = null;
|
||||
imagePath = null;
|
||||
caption = null;
|
||||
prefabPath = null;
|
||||
|
||||
// GridRow 정리
|
||||
leftGridItem?.Dispose();
|
||||
leftGridItem = null;
|
||||
rightGridItem?.Dispose();
|
||||
rightGridItem = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#region 하위 호환성을 위한 타입 별칭 (Backward Compatibility)
|
||||
|
||||
// 기존 API 호환성을 위해 레거시 타입 유지
|
||||
// 새 코드에서는 UTKAccordionItemData 사용 권장
|
||||
|
||||
/// <summary>
|
||||
/// [레거시] 수평 아이템 데이터 - UTKAccordionItemData.CreateHorizontalItem() 사용 권장
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class UTKAccordionHorizontalItemData : IDisposable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string? ExternalId { get; set; }
|
||||
public UTKAccordionContentSpec? Head { get; set; }
|
||||
public UTKAccordionContentSpec? Content { get; set; }
|
||||
public List<UTKAccordionContentSpec>? Tail { get; set; }
|
||||
public string? Tag { get; set; }
|
||||
public object? UserData { get; set; }
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
Head = null;
|
||||
Content = null;
|
||||
Tail?.Clear();
|
||||
Tail = null;
|
||||
Tag = null;
|
||||
ExternalId = null;
|
||||
UserData = null;
|
||||
}
|
||||
|
||||
/// <summary>UTKAccordionItemData로 변환합니다.</summary>
|
||||
public UTKAccordionItemData ToItemData()
|
||||
{
|
||||
return new UTKAccordionItemData
|
||||
{
|
||||
id = Id,
|
||||
externalId = ExternalId,
|
||||
nodeType = UTKAccordionNodeType.HorizontalItem,
|
||||
name = Content?.Text ?? string.Empty,
|
||||
head = Head,
|
||||
content = Content,
|
||||
tail = Tail,
|
||||
tag = Tag,
|
||||
userData = UserData
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [레거시] 그리드 아이템 데이터 - UTKAccordionItemData.CreateGridItem() 사용 권장
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class UTKAccordionGridItemData : IDisposable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string? ExternalId { get; set; }
|
||||
public string? ImagePath { get; set; }
|
||||
public string Caption { get; set; } = string.Empty;
|
||||
public Color? CaptionColor { get; set; }
|
||||
public int? CaptionSize { get; set; }
|
||||
public string? Tag { get; set; }
|
||||
public object? UserData { get; set; }
|
||||
public string? PrefabPath { get; set; }
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
ExternalId = null;
|
||||
ImagePath = null;
|
||||
Caption = string.Empty;
|
||||
Tag = null;
|
||||
UserData = null;
|
||||
PrefabPath = null;
|
||||
}
|
||||
|
||||
/// <summary>UTKAccordionItemData로 변환합니다.</summary>
|
||||
public UTKAccordionItemData ToItemData()
|
||||
{
|
||||
return new UTKAccordionItemData
|
||||
{
|
||||
id = Id,
|
||||
externalId = ExternalId,
|
||||
nodeType = UTKAccordionNodeType.GridItem,
|
||||
name = Caption,
|
||||
caption = Caption,
|
||||
imagePath = ImagePath,
|
||||
captionColor = CaptionColor,
|
||||
captionSize = CaptionSize,
|
||||
prefabPath = PrefabPath,
|
||||
tag = Tag,
|
||||
userData = UserData
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [레거시] 섹션 데이터 - UTKAccordionItemData.CreateSection() 사용 권장
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class UTKAccordionSectionData : IDisposable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public bool IsExpanded { get; set; } = true;
|
||||
public UTKAccordionLayoutType LayoutType { get; set; } = UTKAccordionLayoutType.Horizontal;
|
||||
public List<UTKAccordionHorizontalItemData>? HorizontalItems { get; set; }
|
||||
public List<UTKAccordionGridItemData>? GridItems { get; set; }
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
if (HorizontalItems != null)
|
||||
{
|
||||
foreach (var item in HorizontalItems)
|
||||
item.Dispose();
|
||||
HorizontalItems.Clear();
|
||||
HorizontalItems = null;
|
||||
}
|
||||
|
||||
if (GridItems != null)
|
||||
{
|
||||
foreach (var item in GridItems)
|
||||
item.Dispose();
|
||||
GridItems.Clear();
|
||||
GridItems = null;
|
||||
}
|
||||
|
||||
Title = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>UTKAccordionItemData로 변환합니다.</summary>
|
||||
public UTKAccordionItemData ToItemData()
|
||||
{
|
||||
var section = UTKAccordionItemData.CreateSection(Title, IsExpanded);
|
||||
section.id = Id;
|
||||
|
||||
if (LayoutType == UTKAccordionLayoutType.Horizontal && HorizontalItems != null)
|
||||
{
|
||||
foreach (var item in HorizontalItems)
|
||||
{
|
||||
section.AddChild(item.ToItemData());
|
||||
}
|
||||
}
|
||||
else if (LayoutType == UTKAccordionLayoutType.Grid && GridItems != null)
|
||||
{
|
||||
foreach (var item in GridItems)
|
||||
{
|
||||
section.AddChild(item.ToItemData());
|
||||
}
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [레거시] 아코디언 전체 데이터 - List<UTKAccordionItemData> 사용 권장
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class UTKAccordionData : IDisposable
|
||||
{
|
||||
public List<UTKAccordionSectionData> Sections { get; set; } = new();
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
foreach (var section in Sections)
|
||||
section.Dispose();
|
||||
Sections.Clear();
|
||||
}
|
||||
|
||||
/// <summary>UTKAccordionItemData 리스트로 변환합니다.</summary>
|
||||
public List<UTKAccordionItemData> ToItemDataList()
|
||||
{
|
||||
var result = new List<UTKAccordionItemData>();
|
||||
foreach (var section in Sections)
|
||||
{
|
||||
result.Add(section.ToItemData());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [레거시] 아이템 레이아웃 타입 - UTKAccordionNodeType 사용 권장
|
||||
/// </summary>
|
||||
public enum UTKAccordionLayoutType
|
||||
{
|
||||
Horizontal,
|
||||
Grid
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 793db34bde0a1a145ac4e4ae31b7c452
|
||||
@@ -249,6 +249,7 @@ namespace UVC.UIToolkit.List
|
||||
// 검색어 지우기 버튼
|
||||
if(_clearButton != null)
|
||||
{
|
||||
_clearButton.style.display = DisplayStyle.None; // 초기에는 숨김
|
||||
_clearButton.clicked += () =>
|
||||
{
|
||||
if (_searchField.value.Length > 0)
|
||||
@@ -256,6 +257,7 @@ namespace UVC.UIToolkit.List
|
||||
_searchField.value = string.Empty;
|
||||
OnSearch(string.Empty);
|
||||
}
|
||||
_clearButton.style.display = DisplayStyle.None; // 클리어 후 숨김
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1255,6 +1257,12 @@ namespace UVC.UIToolkit.List
|
||||
/// <param name="query">검색어</param>
|
||||
private void OnSearch(string query)
|
||||
{
|
||||
// Clear 버튼 가시성 토글
|
||||
if (_clearButton != null)
|
||||
{
|
||||
_clearButton.style.display = string.IsNullOrEmpty(query) ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
}
|
||||
|
||||
// 검색어가 없으면 원본 데이터 복원
|
||||
if (string.IsNullOrEmpty(query))
|
||||
{
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace UVC.UIToolkit.List
|
||||
private const string ITEM_UXML_PATH = "UIToolkit/List/UTKImageListItem";
|
||||
|
||||
/// <summary>최소 검색어 길이</summary>
|
||||
private const int MIN_SEARCH_LENGTH = 3;
|
||||
private const int MIN_SEARCH_LENGTH = 2;
|
||||
|
||||
/// <summary>이미지 로딩 디바운스 시간 (ms)</summary>
|
||||
private const int IMAGE_LOAD_DEBOUNCE_MS = 50;
|
||||
@@ -118,6 +118,9 @@ namespace UVC.UIToolkit.List
|
||||
/// <summary>현재 로딩 중인 이미지 경로 추적 (중복 로딩 방지)</summary>
|
||||
private readonly HashSet<string> _loadingImages = new();
|
||||
|
||||
/// <summary>RowData 객체 풀 (GC 부담 감소)</summary>
|
||||
private readonly Queue<RowData> _rowDataPool = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 내부 클래스 (Internal Classes)
|
||||
@@ -148,6 +151,13 @@ namespace UVC.UIToolkit.List
|
||||
/// </summary>
|
||||
public bool DragImageFollowCursor { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 영역 체크에 사용할 요소를 설정합니다.
|
||||
/// null이면 자신(UTKImageList)의 worldBound를 사용합니다.
|
||||
/// 부모 윈도우 등 다른 요소의 영역을 체크하려면 해당 요소를 설정하세요.
|
||||
/// </summary>
|
||||
public VisualElement? DragBoundsElement { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 현재 검색어를 가져오거나 설정합니다.
|
||||
/// 설정 시 검색 필드의 값만 변경하고 검색은 실행하지 않습니다.
|
||||
@@ -198,6 +208,20 @@ namespace UVC.UIToolkit.List
|
||||
/// </summary>
|
||||
public Action<UTKImageListItemData, Vector2>? OnItemEndDrag;
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 중 리스트 영역을 벗어났을 때 발생하는 이벤트입니다.
|
||||
/// (아이템 데이터, 화면 좌표)
|
||||
/// 3D 프리팹 미리보기를 표시하는데 사용합니다.
|
||||
/// </summary>
|
||||
public Action<UTKImageListItemData, Vector2>? OnDragExitList;
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 중 리스트 영역에 다시 진입했을 때 발생하는 이벤트입니다.
|
||||
/// (아이템 데이터, 화면 좌표)
|
||||
/// 3D 프리팹 미리보기를 숨기는데 사용합니다.
|
||||
/// </summary>
|
||||
public Action<UTKImageListItemData, Vector2>? OnDragEnterList;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 생성자 (Constructor)
|
||||
@@ -243,20 +267,135 @@ namespace UVC.UIToolkit.List
|
||||
/// </summary>
|
||||
private void InitializeLogic()
|
||||
{
|
||||
// 검색 필드 이벤트 등록
|
||||
// 검색 필드 이벤트 등록 (Enter 키 또는 포커스 잃을 때 검색)
|
||||
if (_searchField != null)
|
||||
{
|
||||
_searchField.RegisterCallback<KeyDownEvent>(OnSearchFieldKeyDown);
|
||||
_searchField.RegisterCallback<FocusOutEvent>(OnSearchFieldFocusOut);
|
||||
}
|
||||
|
||||
// 검색어 지우기 버튼 이벤트 등록
|
||||
if (_clearButton != null)
|
||||
{
|
||||
_clearButton.clicked += OnClearButtonClicked;
|
||||
_clearButton.style.display = DisplayStyle.None; // 초기에는 숨김
|
||||
}
|
||||
|
||||
// ListView 설정
|
||||
SetupListView();
|
||||
|
||||
// 루트 레벨 포인터 이벤트 등록 (드래그 처리용)
|
||||
RegisterRootPointerEvents();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 루트 레벨 포인터 이벤트를 등록합니다.
|
||||
/// 포인터 캡처 후에도 이벤트를 받을 수 있도록 합니다.
|
||||
/// </summary>
|
||||
private void RegisterRootPointerEvents()
|
||||
{
|
||||
// UTKImageList 자체에서 포인터 이벤트 수신 (캡처 후에도 받을 수 있음)
|
||||
RegisterCallback<PointerMoveEvent>(OnRootPointerMove);
|
||||
RegisterCallback<PointerUpEvent>(OnRootPointerUp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 루트 레벨 포인터 이동 이벤트 핸들러
|
||||
/// </summary>
|
||||
private void OnRootPointerMove(PointerMoveEvent evt)
|
||||
{
|
||||
if (_dragData == null) return;
|
||||
|
||||
// 드래그 시작 임계값 체크 (5픽셀)
|
||||
if (!_isDragging)
|
||||
{
|
||||
var distance = Vector2.Distance(_dragStartPosition, evt.position);
|
||||
if (distance < 5f) return;
|
||||
|
||||
// 드래그 시작
|
||||
_isDragging = true;
|
||||
_isInsideListArea = true; // 시작 시 항상 내부
|
||||
OnItemBeginDrag?.Invoke(_dragData, evt.position);
|
||||
|
||||
// 드래그 고스트 생성
|
||||
if (DragImageFollowCursor)
|
||||
{
|
||||
CreateDragGhost(_dragData, evt.position);
|
||||
}
|
||||
}
|
||||
|
||||
// 드래그 중 이벤트
|
||||
OnItemDrag?.Invoke(_dragData, evt.position);
|
||||
|
||||
// 리스트 영역 체크 (진입/이탈 이벤트 발생)
|
||||
CheckListAreaBounds(evt.position);
|
||||
|
||||
// 드래그 고스트 이동 (리스트 영역 내부일 때만 표시)
|
||||
if (_dragGhost != null)
|
||||
{
|
||||
UpdateDragGhostPosition(evt.position);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 포인터가 리스트 영역 내부인지 체크하고, 진입/이탈 이벤트를 발생시킵니다.
|
||||
/// </summary>
|
||||
private void CheckListAreaBounds(Vector2 position)
|
||||
{
|
||||
if (_dragData == null || !_isDragging) return;
|
||||
|
||||
// 현재 포인터 위치가 리스트 영역 내부인지 체크
|
||||
// DragBoundsElement가 설정되어 있으면 해당 요소의 영역을, 아니면 자신의 영역을 사용
|
||||
var boundsElement = DragBoundsElement ?? this;
|
||||
bool isInside = boundsElement.worldBound.Contains(position);
|
||||
|
||||
// 상태 변경 시 이벤트 발생
|
||||
if (_isInsideListArea && !isInside)
|
||||
{
|
||||
// 내부 → 외부로 이동 (3D 프리팹 표시)
|
||||
_isInsideListArea = false;
|
||||
if (_dragGhost != null)
|
||||
{
|
||||
_dragGhost.style.visibility = Visibility.Hidden;
|
||||
}
|
||||
OnDragExitList?.Invoke(_dragData, position);
|
||||
}
|
||||
else if (!_isInsideListArea && isInside)
|
||||
{
|
||||
// 외부 → 내부로 이동 (고스트 이미지 표시)
|
||||
_isInsideListArea = true;
|
||||
if (_dragGhost != null)
|
||||
{
|
||||
_dragGhost.style.visibility = Visibility.Visible;
|
||||
}
|
||||
OnDragEnterList?.Invoke(_dragData, position);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 루트 레벨 포인터 업 이벤트 핸들러
|
||||
/// </summary>
|
||||
private void OnRootPointerUp(PointerUpEvent evt)
|
||||
{
|
||||
if (_dragData == null) return;
|
||||
|
||||
// 포인터 캡처 해제
|
||||
if (this.HasPointerCapture(_dragPointerId))
|
||||
{
|
||||
this.ReleasePointer(_dragPointerId);
|
||||
}
|
||||
|
||||
if (_isDragging)
|
||||
{
|
||||
OnItemEndDrag?.Invoke(_dragData, evt.position);
|
||||
OnItemDrop?.Invoke(_dragData);
|
||||
}
|
||||
|
||||
// 드래그 상태 초기화
|
||||
DestroyDragGhost();
|
||||
_isDragging = false;
|
||||
_isInsideListArea = false;
|
||||
_dragData = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -432,23 +571,46 @@ namespace UVC.UIToolkit.List
|
||||
/// <summary>
|
||||
/// 아이템 데이터를 행 단위로 묶습니다.
|
||||
/// 2개씩 묶어서 RowData 리스트를 생성합니다.
|
||||
/// ObjectPool 패턴으로 GC 부담을 줄입니다.
|
||||
/// </summary>
|
||||
/// <param name="items">원본 아이템 리스트</param>
|
||||
private void BuildRowData(List<UTKImageListItemData> items)
|
||||
{
|
||||
_rowData.Clear();
|
||||
// 기존 RowData를 풀에 반환
|
||||
ReturnRowDataToPool();
|
||||
|
||||
for (int i = 0; i < items.Count; i += ITEMS_PER_ROW)
|
||||
{
|
||||
var row = new RowData
|
||||
{
|
||||
LeftItem = items[i],
|
||||
RightItem = (i + 1 < items.Count) ? items[i + 1] : null
|
||||
};
|
||||
var row = GetRowDataFromPool();
|
||||
row.LeftItem = items[i];
|
||||
row.RightItem = (i + 1 < items.Count) ? items[i + 1] : null;
|
||||
_rowData.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ObjectPool에서 RowData를 가져옵니다.
|
||||
/// 풀이 비어있으면 새로 생성합니다.
|
||||
/// </summary>
|
||||
private RowData GetRowDataFromPool()
|
||||
{
|
||||
return _rowDataPool.Count > 0 ? _rowDataPool.Dequeue() : new RowData();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 사용 중인 RowData를 모두 풀에 반환합니다.
|
||||
/// </summary>
|
||||
private void ReturnRowDataToPool()
|
||||
{
|
||||
foreach (var row in _rowData)
|
||||
{
|
||||
row.LeftItem = null;
|
||||
row.RightItem = null;
|
||||
_rowDataPool.Enqueue(row);
|
||||
}
|
||||
_rowData.Clear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ListView 바인딩 (ListView Binding)
|
||||
@@ -578,9 +740,7 @@ namespace UVC.UIToolkit.List
|
||||
if (itemRoot.userData is ItemEventHandlers handlers)
|
||||
{
|
||||
itemRoot.UnregisterCallback<ClickEvent>(handlers.ClickHandler);
|
||||
itemRoot.UnregisterCallback<PointerDownEvent>(handlers.PointerDownHandler);
|
||||
itemRoot.UnregisterCallback<PointerMoveEvent>(handlers.PointerMoveHandler);
|
||||
itemRoot.UnregisterCallback<PointerUpEvent>(handlers.PointerUpHandler);
|
||||
itemRoot.UnregisterCallback<PointerDownEvent>(handlers.PointerDownHandler, TrickleDown.TrickleDown);
|
||||
itemRoot.userData = null;
|
||||
}
|
||||
}
|
||||
@@ -596,25 +756,20 @@ namespace UVC.UIToolkit.List
|
||||
if (itemRoot.userData is ItemEventHandlers oldHandlers)
|
||||
{
|
||||
itemRoot.UnregisterCallback<ClickEvent>(oldHandlers.ClickHandler);
|
||||
itemRoot.UnregisterCallback<PointerDownEvent>(oldHandlers.PointerDownHandler);
|
||||
itemRoot.UnregisterCallback<PointerMoveEvent>(oldHandlers.PointerMoveHandler);
|
||||
itemRoot.UnregisterCallback<PointerUpEvent>(oldHandlers.PointerUpHandler);
|
||||
itemRoot.UnregisterCallback<PointerDownEvent>(oldHandlers.PointerDownHandler, TrickleDown.TrickleDown);
|
||||
}
|
||||
|
||||
// 핸들러 생성 및 등록
|
||||
// 핸들러 생성 및 등록 (PointerMove/Up은 루트 레벨에서 처리)
|
||||
var handlers = new ItemEventHandlers
|
||||
{
|
||||
Data = data,
|
||||
ClickHandler = evt => HandleItemClick(data),
|
||||
PointerDownHandler = evt => HandlePointerDown(evt, data),
|
||||
PointerMoveHandler = evt => HandlePointerMove(evt, data),
|
||||
PointerUpHandler = evt => HandlePointerUp(evt, data)
|
||||
PointerDownHandler = evt => HandlePointerDown(evt, data)
|
||||
};
|
||||
|
||||
itemRoot.RegisterCallback(handlers.ClickHandler);
|
||||
itemRoot.RegisterCallback(handlers.PointerDownHandler);
|
||||
itemRoot.RegisterCallback(handlers.PointerMoveHandler);
|
||||
itemRoot.RegisterCallback(handlers.PointerUpHandler);
|
||||
// TrickleDown으로 등록하여 ListView 선택 전에 이벤트를 받음
|
||||
itemRoot.RegisterCallback(handlers.PointerDownHandler, TrickleDown.TrickleDown);
|
||||
|
||||
// 나중에 해제할 수 있도록 저장
|
||||
itemRoot.userData = handlers;
|
||||
@@ -629,8 +784,6 @@ namespace UVC.UIToolkit.List
|
||||
public UTKImageListItemData? Data;
|
||||
public EventCallback<ClickEvent>? ClickHandler;
|
||||
public EventCallback<PointerDownEvent>? PointerDownHandler;
|
||||
public EventCallback<PointerMoveEvent>? PointerMoveHandler;
|
||||
public EventCallback<PointerUpEvent>? PointerUpHandler;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -762,7 +915,8 @@ namespace UVC.UIToolkit.List
|
||||
private UTKImageListItemData? _dragData;
|
||||
private Vector2 _dragStartPosition;
|
||||
private VisualElement? _dragGhost;
|
||||
private VisualElement? _capturedElement;
|
||||
private int _dragPointerId;
|
||||
private bool _isInsideListArea; // 드래그 중 리스트 영역 내부 여부
|
||||
|
||||
/// <summary>
|
||||
/// 포인터 다운 이벤트를 처리합니다 (드래그 시작 준비).
|
||||
@@ -772,68 +926,10 @@ namespace UVC.UIToolkit.List
|
||||
_dragData = data;
|
||||
_dragStartPosition = evt.position;
|
||||
_isDragging = false;
|
||||
_dragPointerId = evt.pointerId;
|
||||
|
||||
// 포인터 캡처하여 요소 외부에서도 이벤트 수신
|
||||
_capturedElement = evt.target as VisualElement;
|
||||
_capturedElement?.CapturePointer(evt.pointerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 포인터 이동 이벤트를 처리합니다 (드래그 중).
|
||||
/// </summary>
|
||||
private void HandlePointerMove(PointerMoveEvent evt, UTKImageListItemData data)
|
||||
{
|
||||
if (_dragData == null) return;
|
||||
|
||||
// 드래그 시작 임계값 체크 (5픽셀)
|
||||
if (!_isDragging)
|
||||
{
|
||||
var distance = Vector2.Distance(_dragStartPosition, evt.position);
|
||||
if (distance < 5f) return;
|
||||
|
||||
// 드래그 시작
|
||||
_isDragging = true;
|
||||
OnItemBeginDrag?.Invoke(_dragData, evt.position);
|
||||
|
||||
// 드래그 고스트 생성
|
||||
if (DragImageFollowCursor)
|
||||
{
|
||||
CreateDragGhost(_dragData, evt.position);
|
||||
}
|
||||
}
|
||||
|
||||
// 드래그 중 이벤트
|
||||
OnItemDrag?.Invoke(_dragData, evt.position);
|
||||
|
||||
// 드래그 고스트 이동
|
||||
if (_dragGhost != null)
|
||||
{
|
||||
UpdateDragGhostPosition(evt.position);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 포인터 업 이벤트를 처리합니다 (드래그 종료).
|
||||
/// </summary>
|
||||
private void HandlePointerUp(PointerUpEvent evt, UTKImageListItemData data)
|
||||
{
|
||||
// 포인터 캡처 해제
|
||||
if (_capturedElement != null && _capturedElement.HasPointerCapture(evt.pointerId))
|
||||
{
|
||||
_capturedElement.ReleasePointer(evt.pointerId);
|
||||
}
|
||||
_capturedElement = null;
|
||||
|
||||
if (_dragData != null && _isDragging)
|
||||
{
|
||||
OnItemEndDrag?.Invoke(_dragData, evt.position);
|
||||
OnItemDrop?.Invoke(_dragData);
|
||||
}
|
||||
|
||||
// 드래그 상태 초기화
|
||||
DestroyDragGhost();
|
||||
_isDragging = false;
|
||||
_dragData = null;
|
||||
// UTKImageList 자체에서 포인터 캡처하여 영역 외부에서도 이벤트 수신
|
||||
this.CapturePointer(evt.pointerId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -847,13 +943,27 @@ namespace UVC.UIToolkit.List
|
||||
{
|
||||
// 이미지만 표시하는 고스트 생성
|
||||
_dragGhost = new VisualElement();
|
||||
_dragGhost.AddToClassList("drag-ghost");
|
||||
_dragGhost.name = "drag-ghost";
|
||||
_dragGhost.pickingMode = PickingMode.Ignore;
|
||||
|
||||
// 고스트 컨테이너 스타일 (인라인) - UTKAccordionList와 동일한 크기
|
||||
_dragGhost.style.width = 116;
|
||||
_dragGhost.style.height = 87;
|
||||
_dragGhost.style.position = Position.Absolute;
|
||||
_dragGhost.style.opacity = 0.8f;
|
||||
|
||||
// 이미지 요소
|
||||
var image = new VisualElement();
|
||||
image.AddToClassList("item-image");
|
||||
image.name = "drag-ghost-image";
|
||||
image.pickingMode = PickingMode.Ignore;
|
||||
image.style.width = Length.Percent(100);
|
||||
image.style.height = Length.Percent(100);
|
||||
image.style.backgroundPositionX = new BackgroundPosition(BackgroundPositionKeyword.Center);
|
||||
image.style.backgroundPositionY = new BackgroundPosition(BackgroundPositionKeyword.Center);
|
||||
image.style.backgroundRepeat = new BackgroundRepeat(Repeat.NoRepeat, Repeat.NoRepeat);
|
||||
image.style.backgroundSize = new BackgroundSize(BackgroundSizeType.Contain);
|
||||
|
||||
// 스프라이트 캐시에서 이미지 로드
|
||||
if (!string.IsNullOrEmpty(data.imagePath) && _spriteCache.TryGetValue(data.imagePath, out var sprite) && sprite != null)
|
||||
{
|
||||
image.style.backgroundImage = new StyleBackground(sprite);
|
||||
@@ -861,12 +971,23 @@ namespace UVC.UIToolkit.List
|
||||
_dragGhost.Add(image);
|
||||
|
||||
// 위치 설정
|
||||
_dragGhost.style.position = Position.Absolute;
|
||||
UpdateDragGhostPosition(position);
|
||||
|
||||
// 루트에 추가
|
||||
var root = this.panel?.visualTree;
|
||||
root?.Add(_dragGhost);
|
||||
if (root != null)
|
||||
{
|
||||
root.Add(_dragGhost);
|
||||
#if UNITY_EDITOR && UTK_DEBUG
|
||||
UnityEngine.Debug.Log($"[UTKImageList] 드래그 고스트 생성됨, 루트에 추가: {root.name}");
|
||||
#endif
|
||||
}
|
||||
#if UNITY_EDITOR && UTK_DEBUG
|
||||
else
|
||||
{
|
||||
UnityEngine.Debug.LogWarning("[UTKImageList] 드래그 고스트 생성 실패: panel.visualTree가 null입니다.");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -876,13 +997,10 @@ namespace UVC.UIToolkit.List
|
||||
{
|
||||
if (_dragGhost == null) return;
|
||||
|
||||
// 리스트 영역 외부에서만 고스트 표시 (AccordionGridItemView와 동일한 동작)
|
||||
var listBounds = this.worldBound;
|
||||
bool isInsideList = listBounds.Contains(position);
|
||||
_dragGhost.style.visibility = isInsideList ? Visibility.Hidden : Visibility.Visible;
|
||||
|
||||
_dragGhost.style.left = position.x - 40;
|
||||
_dragGhost.style.top = position.y - 40;
|
||||
// 위치만 업데이트 (visibility는 CheckListAreaBounds에서 관리)
|
||||
// 고스트 크기: 116x87, 커서를 이미지 중앙에 배치
|
||||
_dragGhost.style.left = position.x - 58;
|
||||
_dragGhost.style.top = position.y - 43;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -913,6 +1031,14 @@ namespace UVC.UIToolkit.List
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검색 필드가 포커스를 잃었을 때 검색을 실행합니다.
|
||||
/// </summary>
|
||||
private void OnSearchFieldFocusOut(FocusOutEvent evt)
|
||||
{
|
||||
OnSearch(_searchField?.value ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검색어 지우기 버튼 클릭 이벤트를 처리합니다.
|
||||
/// </summary>
|
||||
@@ -923,15 +1049,27 @@ namespace UVC.UIToolkit.List
|
||||
_searchField.value = string.Empty;
|
||||
OnSearch(string.Empty);
|
||||
}
|
||||
|
||||
// 클리어 후 버튼 숨김
|
||||
if (_clearButton != null)
|
||||
{
|
||||
_clearButton.style.display = DisplayStyle.None;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검색을 실행합니다.
|
||||
/// 성능 최적화: LINQ 대신 직접 반복, 소문자 변환 최소화
|
||||
/// 성능 최적화: LINQ 대신 직접 반복, StringComparison 사용
|
||||
/// </summary>
|
||||
/// <param name="query">검색어</param>
|
||||
private void OnSearch(string query)
|
||||
{
|
||||
// Clear 버튼 가시성 토글
|
||||
if (_clearButton != null)
|
||||
{
|
||||
_clearButton.style.display = string.IsNullOrEmpty(query) ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
}
|
||||
|
||||
// 빈 검색어 → 원본 데이터 복원
|
||||
if (string.IsNullOrEmpty(query))
|
||||
{
|
||||
@@ -942,18 +1080,20 @@ namespace UVC.UIToolkit.List
|
||||
// 최소 검색어 길이 체크
|
||||
if (query.Length < MIN_SEARCH_LENGTH)
|
||||
{
|
||||
#if UNITY_EDITOR && UTK_DEBUG
|
||||
Debug.Log($"[UTKImageList] 검색어는 {MIN_SEARCH_LENGTH}글자 이상 입력해주세요.");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// 검색 실행 (대소문자 무시)
|
||||
var queryLower = query.Trim().ToLowerInvariant();
|
||||
// 검색 실행 (대소문자 무시, 문화권 독립적)
|
||||
var trimmedQuery = query.Trim();
|
||||
_filteredData.Clear();
|
||||
|
||||
// 성능: foreach 루프 사용 (LINQ 오버헤드 방지)
|
||||
// 성능: foreach 루프 + StringComparison.OrdinalIgnoreCase (ToLower 할당 방지)
|
||||
foreach (var item in _originalData)
|
||||
{
|
||||
if (item.itemName.ToLowerInvariant().Contains(queryLower))
|
||||
if (item.itemName.Contains(trimmedQuery, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_filteredData.Add(item);
|
||||
}
|
||||
@@ -1017,10 +1157,21 @@ namespace UVC.UIToolkit.List
|
||||
// 이미지 로딩 취소
|
||||
CancelImageLoading();
|
||||
|
||||
// 루트 레벨 포인터 이벤트 해제 (메모리 누수 방지)
|
||||
UnregisterCallback<PointerMoveEvent>(OnRootPointerMove);
|
||||
UnregisterCallback<PointerUpEvent>(OnRootPointerUp);
|
||||
|
||||
// 포인터 캡처 해제
|
||||
if (_dragPointerId != 0 && this.HasPointerCapture(_dragPointerId))
|
||||
{
|
||||
this.ReleasePointer(_dragPointerId);
|
||||
}
|
||||
|
||||
// 이벤트 핸들러 해제
|
||||
if (_searchField != null)
|
||||
{
|
||||
_searchField.UnregisterCallback<KeyDownEvent>(OnSearchFieldKeyDown);
|
||||
_searchField.UnregisterCallback<FocusOutEvent>(OnSearchFieldFocusOut);
|
||||
}
|
||||
|
||||
if (_clearButton != null)
|
||||
@@ -1040,6 +1191,9 @@ namespace UVC.UIToolkit.List
|
||||
_filteredData.Clear();
|
||||
_rowData.Clear();
|
||||
|
||||
// ObjectPool 정리 (메모리 해제)
|
||||
_rowDataPool.Clear();
|
||||
|
||||
// 캐시 정리
|
||||
ClearSpriteCache();
|
||||
|
||||
@@ -1049,6 +1203,8 @@ namespace UVC.UIToolkit.List
|
||||
OnItemBeginDrag = null;
|
||||
OnItemDrag = null;
|
||||
OnItemEndDrag = null;
|
||||
OnDragExitList = null;
|
||||
OnDragEnterList = null;
|
||||
|
||||
// UI 참조 정리
|
||||
_searchField = null;
|
||||
@@ -1056,6 +1212,7 @@ namespace UVC.UIToolkit.List
|
||||
_clearButton = null;
|
||||
_searchResultLabel = null;
|
||||
_itemTemplate = null;
|
||||
DragBoundsElement = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
358
Assets/Scripts/UVC/UIToolkit/Window/UTKAccordionListWindow.cs
Normal file
358
Assets/Scripts/UVC/UIToolkit/Window/UTKAccordionListWindow.cs
Normal file
@@ -0,0 +1,358 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using UVC.UIToolkit.List;
|
||||
|
||||
namespace UVC.UIToolkit.Window
|
||||
{
|
||||
/// <summary>
|
||||
/// UTKAccordionList를 래핑하여 윈도우 형태로 제공하는 컴포넌트입니다.
|
||||
/// 기존 AccordionWindow의 UIToolkit 버전입니다.
|
||||
///
|
||||
/// <para><b>개요:</b></para>
|
||||
/// <para>
|
||||
/// UTKAccordionListWindow는 UTKAccordionList를 내부에 포함하고 헤더(타이틀, 닫기 버튼)를 추가한
|
||||
/// 윈도우 형태의 컴포넌트입니다. 모든 리스트 관련 기능은 내부 UTKAccordionList에 위임됩니다.
|
||||
/// </para>
|
||||
///
|
||||
/// <para><b>주요 기능:</b></para>
|
||||
/// <list type="bullet">
|
||||
/// <item>윈도우 형태의 UI 제공 (헤더 + 닫기 버튼)</item>
|
||||
/// <item>내부 UTKAccordionList의 모든 기능 위임</item>
|
||||
/// <item>드래그 앤 드롭 이벤트 전달</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para><b>관련 리소스:</b></para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Resources/UIToolkit/Window/UTKAccordionListWindow.uxml - 윈도우 레이아웃</item>
|
||||
/// <item>Resources/UIToolkit/Window/UTKAccordionListWindow.uss - 윈도우 스타일</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
[UxmlElement]
|
||||
public partial class UTKAccordionListWindow : VisualElement, IDisposable
|
||||
{
|
||||
#region 상수 (Constants)
|
||||
|
||||
private const string UXML_PATH = "UIToolkit/Window/UTKAccordionListWindow";
|
||||
|
||||
#endregion
|
||||
|
||||
#region UI 컴포넌트 참조 (UI Component References)
|
||||
|
||||
private UTKAccordionList? _accordionList;
|
||||
private Button? _closeButton;
|
||||
private Label? _titleLabel;
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 공개 속성 (Public Properties)
|
||||
|
||||
/// <summary>드래그 시 고스트 이미지 표시 여부</summary>
|
||||
public bool ShowDragGhost
|
||||
{
|
||||
get => _accordionList?.ShowDragGhost ?? true;
|
||||
set { if (_accordionList != null) _accordionList.ShowDragGhost = value; }
|
||||
}
|
||||
|
||||
/// <summary>드래그 영역 체크에 사용할 요소 (기본: 윈도우 자체)</summary>
|
||||
public VisualElement? DragBoundsElement
|
||||
{
|
||||
get => _accordionList?.DragBoundsElement;
|
||||
set { if (_accordionList != null) _accordionList.DragBoundsElement = value; }
|
||||
}
|
||||
|
||||
/// <summary>검색 기능 활성화 여부</summary>
|
||||
public bool EnableSearch
|
||||
{
|
||||
get => _accordionList?.EnableSearch ?? true;
|
||||
set { if (_accordionList != null) _accordionList.EnableSearch = value; }
|
||||
}
|
||||
|
||||
/// <summary>윈도우 제목</summary>
|
||||
public string Title
|
||||
{
|
||||
get => _titleLabel?.text ?? string.Empty;
|
||||
set { if (_titleLabel != null) _titleLabel.text = value; }
|
||||
}
|
||||
|
||||
/// <summary>닫기 버튼 표시 여부</summary>
|
||||
public bool ShowCloseButton
|
||||
{
|
||||
get => _closeButton?.style.display == DisplayStyle.Flex;
|
||||
set { if (_closeButton != null) _closeButton.style.display = value ? DisplayStyle.Flex : DisplayStyle.None; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 이벤트 위임 (Event Delegation)
|
||||
|
||||
/// <summary>수평 아이템 클릭 이벤트</summary>
|
||||
public event Action<UTKAccordionHorizontalItemData, UTKAccordionContentSpec?>? OnHorizontalItemClick
|
||||
{
|
||||
add { if (_accordionList != null) _accordionList.OnHorizontalItemClick += value; }
|
||||
remove { if (_accordionList != null) _accordionList.OnHorizontalItemClick -= value; }
|
||||
}
|
||||
|
||||
/// <summary>수평 아이템 아이콘 버튼 클릭 이벤트</summary>
|
||||
public event Action<UTKAccordionHorizontalItemData, UTKAccordionContentSpec>? OnHorizontalItemIconClick
|
||||
{
|
||||
add { if (_accordionList != null) _accordionList.OnHorizontalItemIconClick += value; }
|
||||
remove { if (_accordionList != null) _accordionList.OnHorizontalItemIconClick -= value; }
|
||||
}
|
||||
|
||||
/// <summary>그리드 아이템 클릭 이벤트</summary>
|
||||
public event Action<UTKAccordionGridItemData>? OnGridItemClick
|
||||
{
|
||||
add { if (_accordionList != null) _accordionList.OnGridItemClick += value; }
|
||||
remove { if (_accordionList != null) _accordionList.OnGridItemClick -= value; }
|
||||
}
|
||||
|
||||
/// <summary>그리드 아이템 드롭 이벤트</summary>
|
||||
public event Action<UTKAccordionGridItemData>? OnGridItemDrop
|
||||
{
|
||||
add { if (_accordionList != null) _accordionList.OnGridItemDrop += value; }
|
||||
remove { if (_accordionList != null) _accordionList.OnGridItemDrop -= value; }
|
||||
}
|
||||
|
||||
/// <summary>드래그 시작 이벤트</summary>
|
||||
public event Action<UTKAccordionGridItemData, Vector2>? OnGridItemBeginDrag
|
||||
{
|
||||
add { if (_accordionList != null) _accordionList.OnGridItemBeginDrag += value; }
|
||||
remove { if (_accordionList != null) _accordionList.OnGridItemBeginDrag -= value; }
|
||||
}
|
||||
|
||||
/// <summary>드래그 중 이벤트</summary>
|
||||
public event Action<UTKAccordionGridItemData, Vector2>? OnGridItemDrag
|
||||
{
|
||||
add { if (_accordionList != null) _accordionList.OnGridItemDrag += value; }
|
||||
remove { if (_accordionList != null) _accordionList.OnGridItemDrag -= value; }
|
||||
}
|
||||
|
||||
/// <summary>드래그 종료 이벤트</summary>
|
||||
public event Action<UTKAccordionGridItemData, Vector2>? OnGridItemEndDrag
|
||||
{
|
||||
add { if (_accordionList != null) _accordionList.OnGridItemEndDrag += value; }
|
||||
remove { if (_accordionList != null) _accordionList.OnGridItemEndDrag -= value; }
|
||||
}
|
||||
|
||||
/// <summary>드래그 중 리스트 영역 이탈 이벤트 (3D 미리보기용)</summary>
|
||||
public event Action<UTKAccordionGridItemData, Vector2>? OnDragExitList
|
||||
{
|
||||
add { if (_accordionList != null) _accordionList.OnDragExitList += value; }
|
||||
remove { if (_accordionList != null) _accordionList.OnDragExitList -= value; }
|
||||
}
|
||||
|
||||
/// <summary>드래그 중 리스트 영역 진입 이벤트</summary>
|
||||
public event Action<UTKAccordionGridItemData, Vector2>? OnDragEnterList
|
||||
{
|
||||
add { if (_accordionList != null) _accordionList.OnDragEnterList += value; }
|
||||
remove { if (_accordionList != null) _accordionList.OnDragEnterList -= value; }
|
||||
}
|
||||
|
||||
/// <summary>섹션 펼침/접힘 이벤트</summary>
|
||||
public event Action<UTKAccordionSectionData, bool>? OnSectionToggled
|
||||
{
|
||||
add { if (_accordionList != null) _accordionList.OnSectionToggled += value; }
|
||||
remove { if (_accordionList != null) _accordionList.OnSectionToggled -= value; }
|
||||
}
|
||||
|
||||
/// <summary>윈도우가 닫힐 때 발생</summary>
|
||||
public event Action? OnClosed;
|
||||
|
||||
// ========================================
|
||||
// 통합 API 이벤트 (New Unified Events)
|
||||
// ========================================
|
||||
|
||||
/// <summary>통합 아이템 클릭 이벤트</summary>
|
||||
public event Action<UTKAccordionItemData>? OnItemClick
|
||||
{
|
||||
add { if (_accordionList != null) _accordionList.OnItemClick += value; }
|
||||
remove { if (_accordionList != null) _accordionList.OnItemClick -= value; }
|
||||
}
|
||||
|
||||
/// <summary>통합 아이템 아이콘 클릭 이벤트</summary>
|
||||
public event Action<UTKAccordionItemData, UTKAccordionContentSpec>? OnItemIconClick
|
||||
{
|
||||
add { if (_accordionList != null) _accordionList.OnItemIconClick += value; }
|
||||
remove { if (_accordionList != null) _accordionList.OnItemIconClick -= value; }
|
||||
}
|
||||
|
||||
/// <summary>통합 아이템 드래그 시작 이벤트</summary>
|
||||
public event Action<UTKAccordionItemData, Vector2>? OnItemBeginDrag
|
||||
{
|
||||
add { if (_accordionList != null) _accordionList.OnItemBeginDrag += value; }
|
||||
remove { if (_accordionList != null) _accordionList.OnItemBeginDrag -= value; }
|
||||
}
|
||||
|
||||
/// <summary>통합 아이템 드래그 중 이벤트</summary>
|
||||
public event Action<UTKAccordionItemData, Vector2>? OnItemDrag
|
||||
{
|
||||
add { if (_accordionList != null) _accordionList.OnItemDrag += value; }
|
||||
remove { if (_accordionList != null) _accordionList.OnItemDrag -= value; }
|
||||
}
|
||||
|
||||
/// <summary>통합 아이템 드래그 종료 이벤트</summary>
|
||||
public event Action<UTKAccordionItemData, Vector2>? OnItemEndDrag
|
||||
{
|
||||
add { if (_accordionList != null) _accordionList.OnItemEndDrag += value; }
|
||||
remove { if (_accordionList != null) _accordionList.OnItemEndDrag -= value; }
|
||||
}
|
||||
|
||||
/// <summary>통합 아이템 드롭 이벤트</summary>
|
||||
public event Action<UTKAccordionItemData>? OnItemDrop
|
||||
{
|
||||
add { if (_accordionList != null) _accordionList.OnItemDrop += value; }
|
||||
remove { if (_accordionList != null) _accordionList.OnItemDrop -= value; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 생성자 (Constructor)
|
||||
|
||||
public UTKAccordionListWindow()
|
||||
{
|
||||
// UXML 로드
|
||||
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
|
||||
if (visualTree == null)
|
||||
{
|
||||
Debug.LogError($"[UTKAccordionListWindow] UXML not found at: {UXML_PATH}");
|
||||
return;
|
||||
}
|
||||
visualTree.CloneTree(this);
|
||||
|
||||
// 내부 UTKAccordionList 찾기
|
||||
_accordionList = this.Q<UTKAccordionList>();
|
||||
if (_accordionList == null)
|
||||
{
|
||||
Debug.LogError("[UTKAccordionListWindow] UTKAccordionList not found in UXML");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 드래그 영역 체크에 윈도우 전체 영역을 사용
|
||||
_accordionList.DragBoundsElement = this;
|
||||
}
|
||||
|
||||
// 헤더 요소 참조
|
||||
_titleLabel = this.Q<Label>("title");
|
||||
_closeButton = this.Q<Button>("close-btn");
|
||||
|
||||
// 닫기 버튼 이벤트
|
||||
if (_closeButton != null)
|
||||
{
|
||||
_closeButton.clicked += OnCloseButtonClicked;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 공개 메서드 (Public Methods)
|
||||
|
||||
/// <summary>윈도우를 화면에 표시합니다.</summary>
|
||||
public void Show()
|
||||
{
|
||||
style.display = DisplayStyle.Flex;
|
||||
_accordionList?.Show();
|
||||
}
|
||||
|
||||
/// <summary>윈도우를 화면에서 숨깁니다.</summary>
|
||||
public void Close()
|
||||
{
|
||||
style.display = DisplayStyle.None;
|
||||
OnClosed?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>레거시 API: 아코디언 데이터를 설정합니다.</summary>
|
||||
public void SetData(UTKAccordionData data)
|
||||
{
|
||||
_accordionList?.SetData(data);
|
||||
}
|
||||
|
||||
/// <summary>통합 API: UTKAccordionItemData 리스트를 설정합니다.</summary>
|
||||
public void SetData(List<UTKAccordionItemData> roots)
|
||||
{
|
||||
_accordionList?.SetData(roots);
|
||||
}
|
||||
|
||||
/// <summary>모든 데이터를 제거합니다.</summary>
|
||||
public void Clear()
|
||||
{
|
||||
_accordionList?.Clear();
|
||||
}
|
||||
|
||||
/// <summary>특정 섹션을 펼치거나 접습니다.</summary>
|
||||
public void SetSectionExpanded(int sectionId, bool expanded)
|
||||
{
|
||||
_accordionList?.SetSectionExpanded(sectionId, expanded);
|
||||
}
|
||||
|
||||
/// <summary>모든 섹션을 펼칩니다.</summary>
|
||||
public void ExpandAll()
|
||||
{
|
||||
_accordionList?.ExpandAll();
|
||||
}
|
||||
|
||||
/// <summary>모든 섹션을 접습니다.</summary>
|
||||
public void CollapseAll()
|
||||
{
|
||||
_accordionList?.CollapseAll();
|
||||
}
|
||||
|
||||
/// <summary>검색어로 필터링합니다.</summary>
|
||||
public void Search(string query)
|
||||
{
|
||||
_accordionList?.Search(query);
|
||||
}
|
||||
|
||||
/// <summary>검색을 초기화합니다.</summary>
|
||||
public void ClearSearch()
|
||||
{
|
||||
_accordionList?.ClearSearch();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 이벤트 핸들러 (Event Handlers)
|
||||
|
||||
private void OnCloseButtonClicked()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
// 내부 UTKAccordionList 정리
|
||||
_accordionList?.Dispose();
|
||||
_accordionList = null;
|
||||
|
||||
// 닫기 버튼 이벤트 해제
|
||||
if (_closeButton != null)
|
||||
{
|
||||
_closeButton.clicked -= OnCloseButtonClicked;
|
||||
}
|
||||
|
||||
// 외부 이벤트 정리
|
||||
OnClosed = null;
|
||||
|
||||
// UI 참조 정리
|
||||
_closeButton = null;
|
||||
_titleLabel = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4a8ae56f0677484f87647337ff3fc03
|
||||
@@ -83,6 +83,16 @@ namespace UVC.UIToolkit.Window
|
||||
set { if (_imageList != null) _imageList.DragImageFollowCursor = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 영역 체크에 사용할 요소를 설정합니다.
|
||||
/// 기본값은 윈도우 자체입니다.
|
||||
/// </summary>
|
||||
public VisualElement? DragBoundsElement
|
||||
{
|
||||
get => _imageList?.DragBoundsElement;
|
||||
set { if (_imageList != null) _imageList.DragBoundsElement = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 윈도우 제목을 가져오거나 설정합니다.
|
||||
/// </summary>
|
||||
@@ -159,6 +169,28 @@ namespace UVC.UIToolkit.Window
|
||||
set { if (_imageList != null) _imageList.OnItemEndDrag = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 중 리스트 영역을 벗어났을 때 발생하는 이벤트입니다.
|
||||
/// (아이템 데이터, 화면 좌표)
|
||||
/// 3D 프리팹 미리보기를 표시하는데 사용합니다.
|
||||
/// </summary>
|
||||
public Action<UTKImageListItemData, Vector2>? OnDragExitList
|
||||
{
|
||||
get => _imageList?.OnDragExitList;
|
||||
set { if (_imageList != null) _imageList.OnDragExitList = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 중 리스트 영역에 다시 진입했을 때 발생하는 이벤트입니다.
|
||||
/// (아이템 데이터, 화면 좌표)
|
||||
/// 3D 프리팹 미리보기를 숨기는데 사용합니다.
|
||||
/// </summary>
|
||||
public Action<UTKImageListItemData, Vector2>? OnDragEnterList
|
||||
{
|
||||
get => _imageList?.OnDragEnterList;
|
||||
set { if (_imageList != null) _imageList.OnDragEnterList = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 윈도우가 닫힐 때(숨겨질 때) 발생합니다.
|
||||
/// 닫기 버튼 클릭 또는 Close() 메서드 호출 시 트리거됩니다.
|
||||
@@ -190,6 +222,11 @@ namespace UVC.UIToolkit.Window
|
||||
{
|
||||
Debug.LogError("[UTKImageListWindow] UTKImageList not found in UXML");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 드래그 영역 체크에 윈도우 전체 영역을 사용하도록 설정
|
||||
_imageList.DragBoundsElement = this;
|
||||
}
|
||||
|
||||
// 3. 헤더 요소 참조
|
||||
_titleLabel = this.Q<Label>("title");
|
||||
|
||||
@@ -228,6 +228,7 @@ namespace UVC.UIToolkit.Window
|
||||
// 검색어 지우기 버튼
|
||||
if(_clearButton != null)
|
||||
{
|
||||
_clearButton.style.display = DisplayStyle.None; // 초기에는 숨김
|
||||
_clearButton.clicked += () =>
|
||||
{
|
||||
if (_searchField.value.Length > 0)
|
||||
@@ -235,6 +236,7 @@ namespace UVC.UIToolkit.Window
|
||||
_searchField.value = string.Empty;
|
||||
OnSearch(string.Empty);
|
||||
}
|
||||
_clearButton.style.display = DisplayStyle.None; // 클리어 후 숨김
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1123,6 +1125,12 @@ namespace UVC.UIToolkit.Window
|
||||
/// <param name="query">검색어</param>
|
||||
private void OnSearch(string query)
|
||||
{
|
||||
// Clear 버튼 가시성 토글
|
||||
if (_clearButton != null)
|
||||
{
|
||||
_clearButton.style.display = string.IsNullOrEmpty(query) ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
}
|
||||
|
||||
// 검색어가 없으면 원본 데이터 복원
|
||||
if (string.IsNullOrEmpty(query))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user