개선 전 저장
This commit is contained in:
@@ -75,12 +75,12 @@ namespace UVC.UI.List.Tree
|
||||
/// <summary>
|
||||
/// 모든 아이템을 1차원으로 평탄화한 리스트 (범위 선택용)
|
||||
/// </summary>
|
||||
protected List<TreeListItemData> allItemDatasFlattened = new List<TreeListItemData>();
|
||||
protected List<TreeListItemData> allItemDataFlattened = new List<TreeListItemData>();
|
||||
|
||||
/// <summary>
|
||||
/// 평탄화된 아이템 데이터 리스트 (읽기 전용)
|
||||
/// </summary>
|
||||
public List<TreeListItemData> AllItemsFlattened => allItemDatasFlattened;
|
||||
public List<TreeListItemData> AllItemDataFlattened => allItemDataFlattened;
|
||||
|
||||
/// <summary>
|
||||
/// 루트 레벨 아이템 리스트
|
||||
@@ -148,6 +148,12 @@ namespace UVC.UI.List.Tree
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Escape 키 입력 감지 - 선택 해제
|
||||
if (Input.GetKeyDown(KeyCode.Escape))
|
||||
{
|
||||
HandleEscapeKeyPressed();
|
||||
}
|
||||
|
||||
// Delete 키 입력 감지
|
||||
if (Input.GetKeyDown(KeyCode.Delete))
|
||||
{
|
||||
@@ -177,6 +183,34 @@ namespace UVC.UI.List.Tree
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escape 키 입력 시 모든 선택 해제
|
||||
///
|
||||
/// 동작:
|
||||
/// 1. 선택된 아이템이 있는지 확인
|
||||
/// 2. ClearSelection() 호출하여 모든 선택 해제
|
||||
/// 3. lastSelectedItem 초기화
|
||||
///
|
||||
/// 용도: 사용자가 선택을 취소하고 싶을 때 빠른 해제
|
||||
/// </summary>
|
||||
private void HandleEscapeKeyPressed()
|
||||
{
|
||||
// 선택된 아이템이 없으면 아무것도 하지 않음
|
||||
if (selectedItems.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 디버그 로그
|
||||
Debug.Log($"Escape key pressed. Clearing {selectedItems.Count} selected item(s).");
|
||||
|
||||
// 모든 선택 해제
|
||||
ClearSelection();
|
||||
|
||||
// 마지막 선택 아이템 초기화
|
||||
lastSelectedItem = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete 키 입력 시 선택된 모든 아이템 삭제
|
||||
///
|
||||
@@ -519,7 +553,11 @@ namespace UVC.UI.List.Tree
|
||||
/// <param name="data">복제할 원본 아이템 데이터</param>
|
||||
public void AddCloneItem(TreeListItemData data)
|
||||
{
|
||||
TreeListItemData clone = data.Clone();
|
||||
TreeListItemData clone = data.CloneWithChild();
|
||||
|
||||
//호출 순서 중요
|
||||
//data에 해당하는 TreeListItem 찾기
|
||||
TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data);
|
||||
|
||||
//changedData 부모에게 알림
|
||||
if (data.Parent != null)
|
||||
@@ -531,10 +569,9 @@ namespace UVC.UI.List.Tree
|
||||
RemoveItem(data);
|
||||
}
|
||||
|
||||
//data에 해당하는 TreeListItem 찾기
|
||||
TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data);
|
||||
if (item != null) item.Delete(true);
|
||||
|
||||
|
||||
AddItem(clone);
|
||||
|
||||
}
|
||||
@@ -546,7 +583,11 @@ namespace UVC.UI.List.Tree
|
||||
/// <param name="index">삽입 위치</param>
|
||||
public void AddCloneItemAt(TreeListItemData data, int index)
|
||||
{
|
||||
TreeListItemData clone = data.Clone();
|
||||
TreeListItemData clone = data.CloneWithChild();
|
||||
|
||||
//호출 순서 중요
|
||||
//data에 해당하는 TreeListItem 찾기
|
||||
TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data);
|
||||
|
||||
//changedData 부모에게 알림
|
||||
if (data.Parent != null)
|
||||
@@ -558,8 +599,6 @@ namespace UVC.UI.List.Tree
|
||||
RemoveItem(data);
|
||||
}
|
||||
|
||||
//data에 해당하는 TreeListItem 찾기
|
||||
TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data);
|
||||
if (item != null) item.Delete(true);
|
||||
|
||||
AddItemAt(clone, index);
|
||||
@@ -618,6 +657,25 @@ namespace UVC.UI.List.Tree
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 컬렉션에서 모든 항목을 지우고 관련 리소스를 해제합니다.
|
||||
/// </summary>
|
||||
/// <remarks>이 메서드는 컬렉션에서 모든 항목을 제거하고, 내부 데이터 구조를 지우고,
|
||||
/// <see cref="Delete"/> 메서드를 <see
|
||||
/// langword="true"/> 매개변수로 호출하여 각 항목을 삭제합니다.</remarks>
|
||||
public void ClearItems()
|
||||
{
|
||||
foreach (var item in allItemFlattened)
|
||||
{
|
||||
item.Delete(true);
|
||||
}
|
||||
|
||||
allItemFlattened.Clear();
|
||||
allItemDataFlattened.Clear();
|
||||
items.Clear();
|
||||
selectedItems.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 아이템을 1차원 리스트로 평탄화
|
||||
///
|
||||
@@ -631,7 +689,7 @@ namespace UVC.UI.List.Tree
|
||||
internal void UpdateFlattenedItemDataList()
|
||||
{
|
||||
// 기존 평탄화 리스트 비우기
|
||||
allItemDatasFlattened.Clear();
|
||||
allItemDataFlattened.Clear();
|
||||
|
||||
// root의 모든 직접 자식을 순회
|
||||
// (손자, 증손자는 재귀로 처리됨)
|
||||
@@ -642,9 +700,9 @@ namespace UVC.UI.List.Tree
|
||||
}
|
||||
|
||||
allItemFlattened.Clear();
|
||||
foreach (var data in allItemDatasFlattened)
|
||||
foreach (var data in allItemDataFlattened)
|
||||
{
|
||||
var item = root.GetComponentsInChildren<TreeListItem>().FirstOrDefault(x => x.Data == data);
|
||||
var item = root.GetComponentsInChildren<TreeListItem>(true).FirstOrDefault(x => x.Data == data);
|
||||
if (item != null)
|
||||
{
|
||||
allItemFlattened.Add(item);
|
||||
@@ -666,7 +724,7 @@ namespace UVC.UI.List.Tree
|
||||
private void AddItemDataToFlattened(TreeListItemData data)
|
||||
{
|
||||
// 현재 아이템을 평탄화 리스트에 추가
|
||||
allItemDatasFlattened.Add(data);
|
||||
allItemDataFlattened.Add(data);
|
||||
|
||||
// 현재 아이템의 모든 자식을 순회
|
||||
foreach (var child in data.Children)
|
||||
@@ -768,10 +826,10 @@ namespace UVC.UI.List.Tree
|
||||
private void SelectRange(TreeListItemData startItem, TreeListItemData endItem)
|
||||
{
|
||||
// 평탄화 리스트에서 시작 아이템의 위치(인덱스) 찾기
|
||||
int startIndex = allItemDatasFlattened.IndexOf(startItem);
|
||||
int startIndex = allItemDataFlattened.IndexOf(startItem);
|
||||
|
||||
// 평탄화 리스트에서 끝 아이템의 위치(인덱스) 찾기
|
||||
int endIndex = allItemDatasFlattened.IndexOf(endItem);
|
||||
int endIndex = allItemDataFlattened.IndexOf(endItem);
|
||||
|
||||
// 두 아이템 모두 리스트에 없으면 종료
|
||||
if (startIndex == -1 || endIndex == -1)
|
||||
@@ -789,7 +847,7 @@ namespace UVC.UI.List.Tree
|
||||
// i = minIndex부터 i = maxIndex까지 (포함)
|
||||
for (int i = minIndex; i <= maxIndex; i++)
|
||||
{
|
||||
SelectItem(allItemDatasFlattened[i]);
|
||||
SelectItem(allItemDataFlattened[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -944,5 +1002,33 @@ namespace UVC.UI.List.Tree
|
||||
Debug.Log($"Item '{draggedItem.Name}' dropped on '{(targetItem?.Name ?? "Root")}'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정한 데이터 항목을 트리에서 찾아 부모 체인을 펼치고 선택합니다.
|
||||
/// 기존 선택이 있으면 해제합니다.
|
||||
/// </summary>
|
||||
/// <param name="data">선택 및 표시할 데이터 (트리의 실제 데이터 참조)</param>
|
||||
/// <param name="clearExisting">true면 기존 선택을 모두 해제</param>
|
||||
public void RevealAndSelectItem(TreeListItemData? data, bool clearExisting = true)
|
||||
{
|
||||
if (data == null) return;
|
||||
|
||||
if (clearExisting)
|
||||
{
|
||||
ClearSelection();
|
||||
}
|
||||
|
||||
// 부모 체인 펼치기
|
||||
var parent = data.Parent;
|
||||
while (parent != null)
|
||||
{
|
||||
parent.IsExpanded = true;
|
||||
parent = parent.Parent;
|
||||
}
|
||||
|
||||
// 평탄화 리스트 갱신 및 대상 선택
|
||||
UpdateFlattenedItemDataList();
|
||||
SelectItem(data);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,16 @@ using UnityEngine;
|
||||
|
||||
namespace UVC.UI.List.Tree
|
||||
{
|
||||
/// <summary>
|
||||
/// 드래그 위치 표현. 위/안쪽/아래.
|
||||
/// </summary>
|
||||
public enum TreeDropPosition
|
||||
{
|
||||
Above,
|
||||
InsideAsChild,
|
||||
Below
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 트리 리스트의 드래그 & 드롭 상호작용을 상태와 이벤트로 중재하는 관리자입니다.
|
||||
///
|
||||
@@ -19,9 +29,9 @@ namespace UVC.UI.List.Tree
|
||||
///
|
||||
/// 이벤트 흐름:
|
||||
/// - StartDrag → OnDragStarted(once)
|
||||
/// - OnDragOver → OnDragEntered(repeat, hover 대상에 따라 여러 번)
|
||||
/// - TryDrop 유효 시 → OnDropped → EndDrag → OnDragEnded
|
||||
/// - TryDrop 무효/취소 시 → EndDrag → OnDragEnded
|
||||
/// - OnDragOver/OnDragHovered → OnDragEntered(repeat)
|
||||
/// - TryDrop 유효 시 → OnDropped → (caller) EndDrag → OnDragEnded
|
||||
/// - TryDrop 무효/취소 시 → (caller) EndDrag → OnDragEnded
|
||||
/// </summary>
|
||||
public class TreeListDragDropManager
|
||||
{
|
||||
@@ -36,39 +46,35 @@ namespace UVC.UI.List.Tree
|
||||
public bool IsDragging { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 시작 시 1회 발생하는 이벤트입니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 드래그 시작 시1회 발생하는 이벤트입니다.
|
||||
/// 핸들러 시그니처: (TreeListItemData dragged)
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
public Action<TreeListItemData>? OnDragStarted;
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 진행 중 마우스가 특정 아이템 위에 있을 때 반복적으로 발생하는 이벤트입니다.
|
||||
/// 빈 공간 위라면 <c>targetItem</c>은 null이 될 수 있습니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 빈 공간 위라면 targetItem은 null이 될 수 있습니다.
|
||||
/// 핸들러 시그니처: (TreeListItemData dragged, TreeListItemData? targetItem)
|
||||
/// 이벤트 발생 빈도가 높으므로, 처리 로직은 가볍게 유지하세요.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
public Action<TreeListItemData, TreeListItemData?>? OnDragEntered;
|
||||
|
||||
/// <summary>
|
||||
/// 드래그가 종료될 때 발생하는 이벤트입니다(드롭 성공/실패/취소 포함).
|
||||
/// 드래그 진행 중 hover 대상과 의도된 드롭 위치를 함께 통지합니다.
|
||||
/// 핸들러 시그니처: (TreeListItemData dragged, TreeListItemData? targetItem, TreeDropPosition pos)
|
||||
/// </summary>
|
||||
public Action<TreeListItemData, TreeListItemData?, TreeDropPosition>? OnDragHovered;
|
||||
|
||||
/// <summary>
|
||||
/// 드래그가 종료될 때 발생하는 이벤트입니다(드롭 성공/실패/취소 포함).
|
||||
/// 핸들러 시그니처: (TreeListItemData dragged)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 핸들러 시그니처: (TreeListItemData dragged)<br/>
|
||||
/// 드롭 성공 시에는 OnDropped 이후에 호출됩니다.
|
||||
/// </remarks>
|
||||
public Action<TreeListItemData>? OnDragEnded;
|
||||
|
||||
/// <summary>
|
||||
/// 유효성 검사를 통과한 드롭이 확정될 때 발생하는 이벤트입니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 핸들러 시그니처: (TreeListItemData dragged, TreeListItemData? target)<br/>
|
||||
/// 핸들러 시그니처: (TreeListItemData dragged, TreeListItemData? target)
|
||||
/// 이 이벤트에서 실제 데이터 구조 변경(이동/부모 변경/정렬)을 수행하세요.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
public Action<TreeListItemData, TreeListItemData?>? OnDropped;
|
||||
|
||||
/// <summary>
|
||||
@@ -103,6 +109,23 @@ namespace UVC.UI.List.Tree
|
||||
OnDragEntered?.Invoke(DraggedItem, targetItem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 중 hover 대상과 드롭 위치를 함께 통지합니다.
|
||||
/// </summary>
|
||||
/// <param name="targetItem">현재 마우스 아래의 아이템(null 가능)</param>
|
||||
/// <param name="position">의도된 드롭 위치</param>
|
||||
public void OnDragOver(TreeListItemData? targetItem, TreeDropPosition position)
|
||||
{
|
||||
if (!IsDragging || DraggedItem == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 하위 호환 이벤트도 호출
|
||||
OnDragEntered?.Invoke(DraggedItem, targetItem);
|
||||
OnDragHovered?.Invoke(DraggedItem, targetItem, position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그를 종료합니다. 드래그 중이 아니면 아무 동작도 하지 않습니다.
|
||||
/// </summary>
|
||||
@@ -122,17 +145,11 @@ namespace UVC.UI.List.Tree
|
||||
/// <summary>
|
||||
/// 드래그된 아이템을 대상 아이템에 드롭 시도합니다.
|
||||
/// 유효성 검사(자기 자신/조상에게 드롭 금지)를 통과한 경우에만 OnDropped를 발생시킵니다.
|
||||
/// EndDrag는 호출 측(뷰/핸들러)에서 호출해야 합니다.
|
||||
/// </summary>
|
||||
/// <param name="targetItem">드롭 대상 아이템. 루트 레벨로 드롭하려면 null.</param>
|
||||
/// <param name="insertIndex">
|
||||
/// 대상 부모 내 삽입 위치. -1이면 끝에 추가 의도.
|
||||
/// 현재 구현에서는 이 값이 내부에서 사용되지 않으며, 필요 시 이벤트 모델 확장이 필요합니다.
|
||||
/// </param>
|
||||
/// <returns>드롭을 성공적으로 수락하여 이벤트를 발생시켰으면 true, 그 외는 false.</returns>
|
||||
/// <remarks>
|
||||
/// 성공 시 순서: OnDropped(once) → EndDrag → OnDragEnded.<br/>
|
||||
/// 실패/무효 시: EndDrag → OnDragEnded.
|
||||
/// </remarks>
|
||||
/// <param name="insertIndex">미사용 예약 필드. -1이면 끝에 추가 의도.</param>
|
||||
/// <returns>OnDropped가 발생하면 true, 아니면 false.</returns>
|
||||
public bool TryDrop(TreeListItemData? targetItem, int insertIndex = -1)
|
||||
{
|
||||
if (!IsDragging || DraggedItem == null)
|
||||
@@ -143,33 +160,22 @@ namespace UVC.UI.List.Tree
|
||||
// 자기 자신에게 드롭하는 경우 무시
|
||||
if (targetItem != null && targetItem == DraggedItem)
|
||||
{
|
||||
EndDrag();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 순환 참조 검사 (드래그 아이템이 드롭 대상의 부모인 경우)
|
||||
if (targetItem != null && IsAncestorOf(DraggedItem, targetItem))
|
||||
{
|
||||
EndDrag();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 주의: insertIndex는 현재 이벤트로 전달되지 않습니다(모델 확장 필요).
|
||||
OnDropped?.Invoke(DraggedItem, targetItem);
|
||||
|
||||
EndDrag();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 첫 번째 아이템이 두 번째 아이템의 조상인지 확인합니다.
|
||||
/// </summary>
|
||||
/// <param name="potentialAncestor">조상일 가능성이 있는 아이템.</param>
|
||||
/// <param name="potentialDescendant">후손일 가능성이 있는 아이템.</param>
|
||||
/// <returns>조상-후손 관계이면 true, 아니면 false.</returns>
|
||||
/// <remarks>
|
||||
/// 상향 탐색으로 O(h) 시간 복잡도입니다(h: 트리 높이).
|
||||
/// </remarks>
|
||||
public static bool IsAncestorOf(TreeListItemData potentialAncestor, TreeListItemData potentialDescendant)
|
||||
{
|
||||
var current = potentialDescendant.Parent;
|
||||
@@ -187,7 +193,6 @@ namespace UVC.UI.List.Tree
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 & 드롭 상태를 초기화합니다.
|
||||
/// </summary>
|
||||
|
||||
@@ -247,7 +247,11 @@ namespace UVC.UI.List.Tree
|
||||
else if (changedType == ChangedType.AddCloneChild)
|
||||
{
|
||||
//데이터 복사
|
||||
TreeListItemData clone = changedData.Clone();
|
||||
TreeListItemData clone = changedData.CloneWithChild();
|
||||
|
||||
//호출 순서 중요
|
||||
// TreeListItem 제거
|
||||
TreeListItem? item = treeList.AllItemFlattened.FirstOrDefault(x => x.Data == changedData);
|
||||
|
||||
//changedData 부모에게 알림 - UI 갱신 용
|
||||
if (changedData.Parent != null)
|
||||
@@ -259,8 +263,6 @@ namespace UVC.UI.List.Tree
|
||||
treeList.RemoveItem(changedData);
|
||||
}
|
||||
|
||||
// TreeListItem 제거
|
||||
TreeListItem? item = treeList.AllItemFlattened.FirstOrDefault(x => x.Data == changedData);
|
||||
if (item != null) item.Delete(true);
|
||||
|
||||
data.AddChild(clone);
|
||||
@@ -268,7 +270,11 @@ namespace UVC.UI.List.Tree
|
||||
else if (changedType == ChangedType.AddCloneAtChild)
|
||||
{
|
||||
//데이터 복사
|
||||
TreeListItemData clone = changedData.Clone();
|
||||
TreeListItemData clone = changedData.CloneWithChild();
|
||||
|
||||
//호출 순서 중요
|
||||
// TreeListItem 제거
|
||||
TreeListItem? item = treeList.AllItemFlattened.FirstOrDefault(x => x.Data == changedData);
|
||||
|
||||
//changedData 부모에게 알림
|
||||
if (changedData.Parent != null)
|
||||
@@ -280,8 +286,6 @@ namespace UVC.UI.List.Tree
|
||||
treeList.RemoveItem(changedData);
|
||||
}
|
||||
|
||||
// TreeListItem 제거
|
||||
TreeListItem? item = treeList.AllItemFlattened.FirstOrDefault(x => x.Data == changedData);
|
||||
if (item != null) item.Delete(true);
|
||||
|
||||
data.AddChildAt(clone, index);
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace UVC.UI.List.Tree
|
||||
/// 리스트를 비웁니다(연결만 끊는 수준을 넘어 자식도 정리됨).
|
||||
/// - AddClone계열은 전달된 원본 child를 Dispose한 뒤 복제본을 추가합니다(파괴적).
|
||||
/// </summary>
|
||||
public class TreeListItemData: IDisposable
|
||||
public class TreeListItemData : IDisposable
|
||||
{
|
||||
#region 이벤트 (Events)
|
||||
|
||||
@@ -150,7 +150,7 @@ namespace UVC.UI.List.Tree
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>부모 데이터(내부 전용).</summary>
|
||||
internal TreeListItemData? Parent
|
||||
@@ -236,7 +236,7 @@ namespace UVC.UI.List.Tree
|
||||
/// </summary>
|
||||
/// <param name="child">복제 및 대체할 원본 자식.</param>
|
||||
public void AddCloneChild(TreeListItemData child)
|
||||
{
|
||||
{
|
||||
NotifyDataChanged(ChangedType.AddCloneChild, child); // UI에 트리 구조 변경 알림
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ namespace UVC.UI.List.Tree
|
||||
/// <param name="child">복제 및 대체할 원본 자식.</param>
|
||||
/// <param name="index">삽입 인덱스(0 기반).</param>
|
||||
public void AddCloneAtChild(TreeListItemData child, int index)
|
||||
{
|
||||
{
|
||||
NotifyDataChanged(ChangedType.AddCloneAtChild, child, index); // UI에 트리 구조 변경 알림
|
||||
}
|
||||
|
||||
@@ -378,9 +378,9 @@ namespace UVC.UI.List.Tree
|
||||
if (_parent != null && _parent.Children.Contains(this)) _parent.Children.Remove(this);
|
||||
_parent = null;
|
||||
if (OnDataChanged != null) OnDataChanged = null;
|
||||
if(OnSelectionChanged != null) OnSelectionChanged = null;
|
||||
if(OnClickAction != null) OnClickAction = null;
|
||||
if(Children != null)
|
||||
if (OnSelectionChanged != null) OnSelectionChanged = null;
|
||||
if (OnClickAction != null) OnClickAction = null;
|
||||
if (Children != null)
|
||||
{
|
||||
Children.Clear();
|
||||
}
|
||||
@@ -390,7 +390,7 @@ namespace UVC.UI.List.Tree
|
||||
/// 깊은 복제본을 생성합니다(자식까지 재귀 복제).
|
||||
/// </summary>
|
||||
/// <returns>복제된 새 인스턴스.</returns>
|
||||
public TreeListItemData Clone()
|
||||
public TreeListItemData CloneWithChild()
|
||||
{
|
||||
TreeListItemData clone = new TreeListItemData();
|
||||
clone.Name = this.Name;
|
||||
@@ -399,11 +399,25 @@ namespace UVC.UI.List.Tree
|
||||
clone.IsSelected = this.IsSelected;
|
||||
foreach (var child in this.Children)
|
||||
{
|
||||
clone.AddChild(child.Clone());
|
||||
clone.AddChild(child.CloneWithChild());
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 인스턴스의 복사본인 <see cref="TreeListItemData"/>의 새 인스턴스를 생성합니다.
|
||||
/// </summary>
|
||||
/// <returns>현재 인스턴스와 동일한 속성 값을 가진 새 <see cref="TreeListItemData"/> 객체를 생성합니다.</returns>
|
||||
public TreeListItemData Clone()
|
||||
{
|
||||
TreeListItemData clone = new TreeListItemData();
|
||||
clone.Name = this.Name;
|
||||
clone.Option = this.Option;
|
||||
clone.IsExpanded = this.IsExpanded;
|
||||
clone.IsSelected = this.IsSelected;
|
||||
return clone;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,299 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
#nullable enable
|
||||
using Cysharp.Threading.Tasks;
|
||||
using DG.Tweening;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UVC.UI.List.Tree;
|
||||
|
||||
namespace UVC.UI.Window
|
||||
{
|
||||
public class HierarchyWindow: MonoBehaviour
|
||||
public class HierarchyWindow : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
protected TreeList treeList;
|
||||
|
||||
/// <summary>
|
||||
/// 검색 결과 용 목록
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
protected TreeList treeListSearch;
|
||||
|
||||
[SerializeField]
|
||||
protected TMP_InputField inputField;
|
||||
|
||||
[SerializeField]
|
||||
protected Button clearTextButton;
|
||||
|
||||
[SerializeField]
|
||||
protected Image loadingImage;
|
||||
|
||||
// 검색 목록에서 선택된 항목(클론된 데이터)
|
||||
private TreeListItemData? selectedSearchItem;
|
||||
|
||||
// 검색 작업 상태
|
||||
private CancellationTokenSource? searchCts;
|
||||
private bool isSearching = false;
|
||||
private float searchProgress = 0f; //unused for visual progress now but kept for future
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("로딩 아이콘 회전 속도(도/초)")]
|
||||
private float loadingRotateSpeed = 360f;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("로딩 이미지의 채우기 애니메이션 속도(사이클/초)")]
|
||||
private float loadingFillCycle = 0.5f; // cycles per second (full0->1->0 cycle per second)
|
||||
//loadingRotateSpeed 360 일때, loadingFillCycle를 0.5 보다 높게 설정하면 이상해 보임
|
||||
|
||||
// DOTween tweens
|
||||
private Tween? loadingRotationTween;
|
||||
private Tween? loadingFillTween;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
loadingImage.gameObject.SetActive(false);
|
||||
|
||||
treeListSearch.gameObject.SetActive(false);
|
||||
inputField.onSubmit.AddListener(OnInputFieldSubmit);
|
||||
|
||||
// 검색 리스트의 선택 변경을 감지
|
||||
if (treeListSearch != null)
|
||||
{
|
||||
treeListSearch.OnItemSelectionChanged += OnSearchSelectionChanged;
|
||||
}
|
||||
|
||||
clearTextButton.onClick.AddListener(() =>
|
||||
{
|
||||
inputField.text = string.Empty;
|
||||
// 취소
|
||||
CancelSearch();
|
||||
|
||||
treeListSearch.gameObject.SetActive(false);
|
||||
treeList.gameObject.SetActive(true);
|
||||
|
||||
// 검색에서 선택한 항목이 있으면 원본 트리에서 동일 항목을 선택하고 펼침
|
||||
if (selectedSearchItem != null && treeList != null)
|
||||
{
|
||||
// 원본 데이터 찾기 (이 프로젝트의 Equals는 Name 기반이므로 Name으로 검색)
|
||||
var target = treeList.AllItemDataFlattened.FirstOrDefault(i => i.Name == selectedSearchItem.Name);
|
||||
if (target != null)
|
||||
{
|
||||
// TreeList에 새로 추가된 유틸리티를 이용해 부모 체인을 펼치고 선택 처리
|
||||
treeList.RevealAndSelectItem(target, true);
|
||||
}
|
||||
|
||||
selectedSearchItem = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// DOTween handles the animations; no per-frame logic needed here
|
||||
}
|
||||
|
||||
private void StartLoadingAnimation()
|
||||
{
|
||||
if (loadingImage == null) return;
|
||||
|
||||
// Ensure any previous tweens are killed
|
||||
StopLoadingAnimation();
|
||||
|
||||
loadingImage.fillAmount = 0f;
|
||||
loadingImage.transform.localRotation = Quaternion.identity;
|
||||
loadingImage.gameObject.SetActive(true);
|
||||
|
||||
// Rotation: rotate360 degrees repeatedly. Duration for one360 rotation (seconds)
|
||||
float rotDuration = (loadingRotateSpeed != 0f) ? (360f / Mathf.Abs(loadingRotateSpeed)) : 1f;
|
||||
// Use LocalAxisAdd to rotate continuously
|
||||
loadingRotationTween = loadingImage.transform.DOLocalRotate(new Vector3(0f, 0f, -360f), rotDuration, RotateMode.LocalAxisAdd)
|
||||
.SetEase(Ease.Linear)
|
||||
.SetLoops(-1, LoopType.Restart);
|
||||
|
||||
// Fill animation:0 ->1 ->0 in one cycle. Forward duration = half cycle
|
||||
// For a0->1 then jump-to-0 repeat, use Restart loop and full cycle duration
|
||||
float fullDuration = (loadingFillCycle > 0f) ? (1f / loadingFillCycle) :1f;
|
||||
loadingFillTween = DOTween.To(() => loadingImage.fillAmount, x => loadingImage.fillAmount = x, 1f, fullDuration)
|
||||
.SetEase(Ease.InOutSine)
|
||||
.SetLoops(-1, LoopType.Yoyo);
|
||||
}
|
||||
|
||||
private void StopLoadingAnimation()
|
||||
{
|
||||
if (loadingRotationTween != null)
|
||||
{
|
||||
loadingRotationTween.Kill();
|
||||
loadingRotationTween = null;
|
||||
}
|
||||
|
||||
if (loadingFillTween != null)
|
||||
{
|
||||
loadingFillTween.Kill();
|
||||
loadingFillTween = null;
|
||||
}
|
||||
|
||||
if (loadingImage != null)
|
||||
{
|
||||
loadingImage.gameObject.SetActive(false);
|
||||
// reset transform / fill
|
||||
loadingImage.transform.localRotation = Quaternion.identity;
|
||||
loadingImage.fillAmount = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelSearch()
|
||||
{
|
||||
if (searchCts != null)
|
||||
{
|
||||
try { searchCts.Cancel(); } catch { }
|
||||
searchCts.Dispose();
|
||||
searchCts = null;
|
||||
}
|
||||
isSearching = false;
|
||||
searchProgress = 0f;
|
||||
|
||||
// stop DOTween animations
|
||||
StopLoadingAnimation();
|
||||
}
|
||||
|
||||
private async void OnSearchSelectionChanged(TreeListItemData data, bool isSelected)
|
||||
{
|
||||
if (isSelected)
|
||||
{
|
||||
selectedSearchItem = data;
|
||||
}
|
||||
else if (selectedSearchItem == data)
|
||||
{
|
||||
selectedSearchItem = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInputFieldSubmit(string text)
|
||||
{
|
||||
// 기존 검색 취소
|
||||
CancelSearch();
|
||||
|
||||
// 검색어가 있으면 검색 결과 목록 표시
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
treeListSearch.gameObject.SetActive(true);
|
||||
treeList.gameObject.SetActive(false);
|
||||
|
||||
// 시작 애니메이션
|
||||
StartLoadingAnimation();
|
||||
|
||||
searchCts = new CancellationTokenSource();
|
||||
// 비동기 검색 실행(UITask 스타일: 메인스레드에서 작업을 분할하여 UI가 멈추지 않게 함)
|
||||
_ = PerformSearchAsync(text, searchCts.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
treeListSearch.gameObject.SetActive(false);
|
||||
treeList.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검색을 메인 스레드에서 분할 처리하여 UI 업데이트(로딩 애니메이션)가 가능하도록 구현합니다.
|
||||
/// </summary>
|
||||
private async UniTaskVoid PerformSearchAsync(string text, CancellationToken token)
|
||||
{
|
||||
isSearching = true;
|
||||
searchProgress = 0f;
|
||||
|
||||
var results = new System.Collections.Generic.List<TreeListItemData>();
|
||||
|
||||
var sourceList = treeList?.AllItemDataFlattened;
|
||||
if (sourceList == null)
|
||||
{
|
||||
isSearching = false;
|
||||
StopLoadingAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
int total = sourceList.Count;
|
||||
if (total == 0)
|
||||
{
|
||||
isSearching = false;
|
||||
StopLoadingAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
// 소문자 비교 준비
|
||||
string lower = text.ToLowerInvariant();
|
||||
|
||||
// 분할 처리: 일정 갯수마다 await으로 제어권을 반환
|
||||
const int chunk = 100; // 한 번에 처리할 항목 수 (조절 가능)
|
||||
for (int i = 0; i < total; i++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var item = sourceList[i];
|
||||
if (!string.IsNullOrEmpty(item.Name) && item.Name.ToLowerInvariant().Contains(lower))
|
||||
{
|
||||
results.Add(item);
|
||||
}
|
||||
|
||||
// 진행도 업데이트 (내부 사용)
|
||||
if ((i % chunk) == 0)
|
||||
{
|
||||
searchProgress = (float)i / (float)total;
|
||||
await UniTask.Yield(PlayerLoopTiming.Update);
|
||||
}
|
||||
}
|
||||
|
||||
// 최종 진행도
|
||||
searchProgress = 1f;
|
||||
|
||||
// UI 반영은 메인 스레드에서
|
||||
await UniTask.SwitchToMainThread();
|
||||
|
||||
try
|
||||
{
|
||||
if (token.IsCancellationRequested) return;
|
||||
|
||||
treeListSearch.ClearItems();
|
||||
foreach (var r in results)
|
||||
{
|
||||
treeListSearch.AddItem(r.Clone());
|
||||
}
|
||||
|
||||
// 로딩 종료
|
||||
isSearching = false;
|
||||
searchProgress = 0f;
|
||||
StopLoadingAnimation();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"PerformSearchAsync error: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (searchCts != null)
|
||||
{
|
||||
searchCts.Dispose();
|
||||
searchCts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
inputField.onSubmit.RemoveListener(OnInputFieldSubmit);
|
||||
clearTextButton.onClick.RemoveAllListeners();
|
||||
|
||||
if (treeListSearch != null)
|
||||
{
|
||||
treeListSearch.OnItemSelectionChanged -= OnSearchSelectionChanged;
|
||||
}
|
||||
|
||||
CancelSearch();
|
||||
}
|
||||
|
||||
public void AddItem(TreeListItemData data)
|
||||
{
|
||||
treeList.AddItem(data);
|
||||
|
||||
Reference in New Issue
Block a user