개발 완료
This commit is contained in:
@@ -127,6 +127,12 @@ namespace UVC.UI.List.Tree
|
||||
/// </summary>
|
||||
public List<TreeListItem> TreeLists => root.GetComponentsInChildren<TreeListItem>().ToList();
|
||||
|
||||
// 뷰 매핑: 데이터 ↔ View O(1) 조회
|
||||
private readonly Dictionary<TreeListItemData, TreeListItem> _viewMap = new Dictionary<TreeListItemData, TreeListItem>();
|
||||
|
||||
// 플래튼 업데이트 스로틀링 플래그
|
||||
private bool _flattenScheduled;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// 드래그 & 드롭 이벤트 구독
|
||||
@@ -183,6 +189,42 @@ namespace UVC.UI.List.Tree
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View 등록(데이터↔View 맵). Init 시점에 호출됩니다.
|
||||
/// </summary>
|
||||
internal void RegisterView(TreeListItemData data, TreeListItem view)
|
||||
{
|
||||
if (data == null || view == null) return;
|
||||
_viewMap[data] = view;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View 등록 해제. Delete/OnDestroy 시점에 호출됩니다.
|
||||
/// </summary>
|
||||
internal void UnregisterView(TreeListItemData data, TreeListItem view)
|
||||
{
|
||||
if (data == null) return;
|
||||
if (_viewMap.TryGetValue(data, out var existing) && existing == view)
|
||||
{
|
||||
_viewMap.Remove(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다음 프레임에 평탄화 리스트 업데이트를 예약합니다.
|
||||
/// 과도한 재계산을 방지하기 위한 스로틀링 유틸입니다.
|
||||
/// </summary>
|
||||
public void ScheduleFlattenedUpdate()
|
||||
{
|
||||
if (_flattenScheduled) return;
|
||||
_flattenScheduled = true;
|
||||
UniTask.NextFrame().ContinueWith(() =>
|
||||
{
|
||||
_flattenScheduled = false;
|
||||
UpdateFlattenedItemDataList();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escape 키 입력 시 모든 선택 해제
|
||||
///
|
||||
@@ -535,6 +577,7 @@ namespace UVC.UI.List.Tree
|
||||
|
||||
TreeListItem item = GameObject.Instantiate<TreeListItem>(ItemPrefab, root);
|
||||
item.Init(data, this, dragDropManager);
|
||||
index = Mathf.Clamp(index, 0, items.Count);
|
||||
items.Insert(index, data);
|
||||
|
||||
UniTask.DelayFrame(1).ContinueWith(() =>
|
||||
@@ -605,20 +648,20 @@ namespace UVC.UI.List.Tree
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 아이템을 지정 인덱스로 이동
|
||||
/// 아이템을 지정 인덱스로 이동합니다(기존 위치 제거 후 삽입).
|
||||
/// </summary>
|
||||
/// <param name="data">이동할 아이템 데이터</param>
|
||||
/// <param name="index">목표 위치</param>
|
||||
public void SwapItem(TreeListItemData data, int index)
|
||||
{
|
||||
// 기존 위치 제거
|
||||
items.Remove(data);
|
||||
// 인덱스 보정 후 삽입
|
||||
index = Mathf.Clamp(index, 0, items.Count);
|
||||
items.Insert(index, data);
|
||||
|
||||
TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data);
|
||||
if (item != null) items.Insert(index, data);
|
||||
UniTask.DelayFrame(1).ContinueWith(() =>
|
||||
{
|
||||
//gameObject 순서 조절
|
||||
if (item != null) item.transform.SetSiblingIndex(index);
|
||||
|
||||
// 범위 선택에 필요한 평탄화 리스트 업데이트
|
||||
UpdateFlattenedItemDataList();
|
||||
});
|
||||
}
|
||||
@@ -674,6 +717,7 @@ namespace UVC.UI.List.Tree
|
||||
allItemDataFlattened.Clear();
|
||||
items.Clear();
|
||||
selectedItems.Clear();
|
||||
_viewMap.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -702,8 +746,7 @@ namespace UVC.UI.List.Tree
|
||||
allItemFlattened.Clear();
|
||||
foreach (var data in allItemDataFlattened)
|
||||
{
|
||||
var item = root.GetComponentsInChildren<TreeListItem>(true).FirstOrDefault(x => x.Data == data);
|
||||
if (item != null)
|
||||
if (_viewMap.TryGetValue(data, out var item) && item != null)
|
||||
{
|
||||
allItemFlattened.Add(item);
|
||||
}
|
||||
|
||||
@@ -170,6 +170,9 @@ namespace UVC.UI.List.Tree
|
||||
dragHandler.SetDragDropManager(this, control, dragDropManager);
|
||||
dragHandler.enabled = true;
|
||||
|
||||
// Register view to map
|
||||
treeList.RegisterView(data, this);
|
||||
|
||||
Debug.Log($"[TreeListItem.Init] 초기화 완료: {data.Name}");
|
||||
}
|
||||
|
||||
@@ -223,7 +226,7 @@ namespace UVC.UI.List.Tree
|
||||
{
|
||||
CreateItem(childData);
|
||||
}
|
||||
treeList.UpdateFlattenedItemDataList();
|
||||
treeList.ScheduleFlattenedUpdate();
|
||||
}
|
||||
else if (changedType == ChangedType.AddChild)
|
||||
{
|
||||
@@ -231,7 +234,7 @@ namespace UVC.UI.List.Tree
|
||||
|
||||
UniTask.DelayFrame(1).ContinueWith(() =>
|
||||
{
|
||||
treeList.UpdateFlattenedItemDataList();
|
||||
treeList.ScheduleFlattenedUpdate();
|
||||
});
|
||||
}
|
||||
else if (changedType == ChangedType.AddAtChild)
|
||||
@@ -241,7 +244,7 @@ namespace UVC.UI.List.Tree
|
||||
UniTask.DelayFrame(1).ContinueWith(() =>
|
||||
{
|
||||
item.transform.SetSiblingIndex(index);
|
||||
treeList.UpdateFlattenedItemDataList();
|
||||
treeList.ScheduleFlattenedUpdate();
|
||||
});
|
||||
}
|
||||
else if (changedType == ChangedType.AddCloneChild)
|
||||
@@ -296,7 +299,7 @@ namespace UVC.UI.List.Tree
|
||||
{
|
||||
TreeListItem? item = treeList.AllItemFlattened.FirstOrDefault(x => x.Data == changedData);
|
||||
if (item != null) item.transform.SetSiblingIndex(index);
|
||||
treeList.UpdateFlattenedItemDataList();
|
||||
treeList.ScheduleFlattenedUpdate();
|
||||
});
|
||||
}
|
||||
else if (changedType == ChangedType.RemoveChild)
|
||||
@@ -554,6 +557,12 @@ namespace UVC.UI.List.Tree
|
||||
/// <param name="deleteData">데이터도 삭제할지 여부</param>
|
||||
public void Delete(bool deleteData = false)
|
||||
{
|
||||
// 먼저 맵에서 해제(Dispose로 Data가 null 되기 전)
|
||||
if (data != null)
|
||||
{
|
||||
treeList.UnregisterView(data, this);
|
||||
}
|
||||
|
||||
// 1️. 데이터 변경 이벤트 구독 해제
|
||||
if (data != null)
|
||||
{
|
||||
@@ -585,6 +594,12 @@ namespace UVC.UI.List.Tree
|
||||
/// </summary>
|
||||
private void OnDestroy()
|
||||
{
|
||||
// 맵 해제
|
||||
if (data != null)
|
||||
{
|
||||
treeList.UnregisterView(data, this);
|
||||
}
|
||||
|
||||
// 1️. 데이터 변경 이벤트 구독 해제
|
||||
if (data != null)
|
||||
{
|
||||
|
||||
@@ -58,6 +58,9 @@ namespace UVC.UI.List.Tree
|
||||
|
||||
#region 내부 필드 (Private Fields)
|
||||
|
||||
/// <summary>고유 식별자(Id).</summary>
|
||||
private readonly Guid _id = Guid.NewGuid();
|
||||
|
||||
/// <summary>아이템 이름.</summary>
|
||||
private string _name = string.Empty;
|
||||
|
||||
@@ -173,6 +176,9 @@ namespace UVC.UI.List.Tree
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>고유 식별자(Id).</summary>
|
||||
public Guid Id => _id;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 생성자 (Constructors)
|
||||
@@ -326,8 +332,8 @@ namespace UVC.UI.List.Tree
|
||||
return false;
|
||||
}
|
||||
|
||||
// 이름으로 비교
|
||||
return left.Name == right.Name;
|
||||
// 고유 식별자(Id)로 비교
|
||||
return left.Id == right.Id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -347,7 +353,7 @@ namespace UVC.UI.List.Tree
|
||||
/// </summary>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return _name.GetHashCode();
|
||||
return Id.GetHashCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
@@ -7,24 +8,11 @@ namespace UVC.UI.List.Tree
|
||||
{
|
||||
/// <summary>
|
||||
/// TreeListItem의 드래그 & 드롭 입력을 처리하는 컴포넌트입니다.
|
||||
///
|
||||
/// 역할:
|
||||
/// 1) 마우스 입력 처리: 드래그 시작/진행/종료
|
||||
/// 2) 드래그 시각 피드백: 알파 변경, 드롭 위치 표시기 갱신
|
||||
/// 3) 마우스 위치 기반 드롭 대상 탐색 및 드롭 위치 판정
|
||||
/// 4) 드래그 매니저(TreeListDragDropManager)와 이벤트 연동
|
||||
///
|
||||
/// 이벤트 흐름(IBeginDragHandler/IDragHandler/IEndDragHandler):
|
||||
/// - OnBeginDrag: 드래그 시작 준비(안전성 검증, 오프셋 계산)
|
||||
/// - OnDrag: 최초 드래그 프레임에 StartDrag, DropIndicator 생성/갱신
|
||||
/// - OnEndDrag: 알파/레이캐스트 복구, DropIndicator 숨김, TryDrop 수행
|
||||
///
|
||||
/// 시각 피드백:
|
||||
/// - CanvasGroup.alpha를 일시적으로 낮춰 드래그 중임을 표시
|
||||
/// - DropIndicator(얇은 선 또는 블록)로 삽입 위치/자식 투입 위치를 표시
|
||||
/// </summary>
|
||||
public class TreeListItemDragHandler : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
|
||||
{
|
||||
private static readonly List<RaycastResult> s_RaycastResults = new List<RaycastResult>(32);
|
||||
|
||||
/// <summary>부모 TreeListItem.</summary>
|
||||
private TreeListItem? treeListItem;
|
||||
|
||||
@@ -65,6 +53,23 @@ namespace UVC.UI.List.Tree
|
||||
|
||||
private RectTransform? treeListRootParent;
|
||||
|
||||
/// <summary>
|
||||
/// 드롭 위치의 상/하 경계 비율.0.3이면 상하30%는 위/아래, 가운데40%는 자식으로 판단.
|
||||
/// </summary>
|
||||
[SerializeField, Range(0.05f, 0.45f)]
|
||||
private float dropEdgeRatio = 0.3f;
|
||||
|
||||
// Auto-scroll settings
|
||||
[Header("Auto Scroll While Dragging")]
|
||||
[SerializeField]
|
||||
private bool autoScrollWhileDragging = true;
|
||||
[SerializeField, Tooltip("픽셀 단위 초당 스크롤 속도")]
|
||||
private float autoScrollPixelsPerSecond = 800f;
|
||||
[SerializeField, Tooltip("뷰포트 상/하단 가장자리에서 오토 스크롤을 시작하는 패딩(픽셀)")]
|
||||
private float autoScrollEdgePadding = 24f;
|
||||
|
||||
private Vector2 _lastPointerScreenPos;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
rectTransform = GetComponent<RectTransform>();
|
||||
@@ -81,9 +86,6 @@ namespace UVC.UI.List.Tree
|
||||
/// <summary>
|
||||
/// 외부에서 드래그 매니저 및 상위 컨텍스트를 주입합니다.
|
||||
/// </summary>
|
||||
/// <param name="item">이 핸들러가 속한 아이템.</param>
|
||||
/// <param name="list">아이템 컨테이너(TreeList).</param>
|
||||
/// <param name="manager">드래그 & 드롭 매니저.</param>
|
||||
public void SetDragDropManager(TreeListItem item, TreeList list, TreeListDragDropManager manager)
|
||||
{
|
||||
treeListItem = item;
|
||||
@@ -93,42 +95,31 @@ namespace UVC.UI.List.Tree
|
||||
Debug.Log($"[TreeListItemDragHandler] 드래그 핸들러 설정: {item.Data?.Name ?? "Unknown"}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그가 시작될 때 호출됩니다.
|
||||
/// OnPointerDown 이후 마우스가 임계치 이상 이동하면 자동 호출됩니다.
|
||||
/// </summary>
|
||||
/// <param name="eventData">포인터 이벤트 데이터.</param>
|
||||
public void OnBeginDrag(PointerEventData eventData)
|
||||
{
|
||||
Debug.Log($"[OnPointerDown]");
|
||||
Debug.Log("[OnPointerDown]");
|
||||
if (!enableDragDrop || treeListItem?.Data == null || dragDropManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 마우스 버튼이 왼쪽이 아니면 무시
|
||||
if (eventData.button != PointerEventData.InputButton.Left)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 마우스 위치의 로컬 좌표 저장(필요 시 Y 오프셋 계산에 사용)
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
treeListRootParent,
|
||||
eventData.position,
|
||||
null,
|
||||
out var localPoint);
|
||||
treeListRootParent,
|
||||
eventData.position,
|
||||
null,
|
||||
out var localPoint);
|
||||
|
||||
dragOffset = localPoint;
|
||||
_lastPointerScreenPos = eventData.position;
|
||||
|
||||
Debug.Log($"[OnPointerDown] {treeListItem.Data.Name}에 포인터 다운, offset: {dragOffset}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 중 프레임마다 호출됩니다.
|
||||
/// 최초 1프레임에 드래그 상태 진입 및 표시기 생성, 이후 hover 대상/표시기 갱신을 수행합니다.
|
||||
/// </summary>
|
||||
/// <param name="eventData">포인터 이벤트 데이터.</param>
|
||||
public void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
if (!enableDragDrop || dragDropManager == null || treeListItem?.Data == null || treeList == null || rectTransform == null)
|
||||
@@ -141,65 +132,79 @@ namespace UVC.UI.List.Tree
|
||||
{
|
||||
return;
|
||||
}
|
||||
_lastPointerScreenPos = eventData.position;
|
||||
|
||||
Debug.Log($"[OnDrag] {treeListItem.Data.Name} 드래그 중");
|
||||
|
||||
// 최초 드래그 프레임: 드래그 상태 진입 및 시각 피드백/표시기 준비
|
||||
if (!dragDropManager.IsDragging)
|
||||
{
|
||||
dragDropManager.StartDrag(treeListItem.Data);
|
||||
|
||||
// 원본 부모/순서 저장
|
||||
originalParent = rectTransform.parent;
|
||||
originalSiblingIndex = rectTransform.GetSiblingIndex();
|
||||
|
||||
// 시각 피드백(투명도/레이캐스트)
|
||||
if (canvasGroup != null)
|
||||
{
|
||||
canvasGroup.alpha = dragAlpha;
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
}
|
||||
|
||||
// 드롭 위치 표시 막대 생성
|
||||
CreateDropIndicator();
|
||||
|
||||
Debug.Log($"[OnDrag] {treeListItem.Data.Name} 드래그 시작");
|
||||
}
|
||||
|
||||
// 필요 시 실제 UI를 마우스를 따라 이동시키려면 아래 호출을 활성화
|
||||
//UpdateDragPosition(eventData);
|
||||
|
||||
// 마우스 위의 드롭 대상 찾기
|
||||
var targetItem = GetItemAtMousePosition(eventData.position);
|
||||
dragDropManager.OnDragOver(targetItem?.Data);
|
||||
if (targetItem != null)
|
||||
{
|
||||
var targetRect = targetItem.GetComponent<RectTransform>();
|
||||
if (targetRect != null)
|
||||
{
|
||||
var pos = GetDropPosition(targetRect);
|
||||
dragDropManager.OnDragOver(targetItem.Data, pos);
|
||||
}
|
||||
else
|
||||
{
|
||||
dragDropManager.OnDragOver(targetItem.Data);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
dragDropManager.OnDragOver((TreeListItemData?)null);
|
||||
}
|
||||
|
||||
// 드롭 위치 표시 갱신(위/아래/자식)
|
||||
UpdateDropIndicator(targetItem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그가 종료될 때 호출됩니다(마우스 버튼 해제).
|
||||
/// 상태/시각 피드백 복구 후 TryDrop을 수행합니다.
|
||||
/// </summary>
|
||||
/// <param name="eventData">포인터 이벤트 데이터.</param>
|
||||
private void Update()
|
||||
{
|
||||
// 지속 오토 스크롤 및 표시기 갱신(마우스가 움직이지 않아도 동작)
|
||||
if (!autoScrollWhileDragging || dragDropManager == null || !dragDropManager.IsDragging) return;
|
||||
if (treeListItem?.Data == null || dragDropManager.DraggedItem != treeListItem.Data) return;
|
||||
AutoScrollIfNeeded();
|
||||
// 스크롤로 콘텐츠가 움직이면 표시기도 갱신
|
||||
var targetItem = GetItemAtMousePosition(_lastPointerScreenPos);
|
||||
UpdateDropIndicator(targetItem);
|
||||
}
|
||||
|
||||
public void OnEndDrag(PointerEventData eventData)
|
||||
{
|
||||
Debug.Log($"[OnPointerUp]");
|
||||
Debug.Log("[OnPointerUp]");
|
||||
if (!enableDragDrop || dragDropManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[OnPointerUp] 드래그 완료");
|
||||
Debug.Log("[OnPointerUp] 드래그 완료");
|
||||
|
||||
// 원본 알파값으로 복구
|
||||
if (canvasGroup != null)
|
||||
{
|
||||
canvasGroup.alpha = originalAlpha;
|
||||
canvasGroup.blocksRaycasts = true;
|
||||
}
|
||||
|
||||
// 원본 부모/순서 복구(드래그 중 시각 이동을 되돌림)
|
||||
if (originalParent != null)
|
||||
{
|
||||
rectTransform?.SetParent(originalParent);
|
||||
@@ -209,21 +214,17 @@ namespace UVC.UI.List.Tree
|
||||
}
|
||||
}
|
||||
|
||||
// 드롭 위치 표시 막대 숨김
|
||||
HideDropIndicator();
|
||||
|
||||
// 드래그가 시작되지 않았으면 무시
|
||||
// 드래그가 시작되지 않았으면 종료 처리만
|
||||
if (!dragDropManager.IsDragging)
|
||||
{
|
||||
dragDropManager.EndDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
// 마우스 위치의 대상 아이템 찾기
|
||||
var targetItem = GetItemAtMousePosition(eventData.position);
|
||||
|
||||
|
||||
// 드롭 시도 및 성공 시 데이터 동기화
|
||||
if (treeListItem?.Data != null)
|
||||
{
|
||||
var result = dragDropManager.TryDrop(targetItem?.Data);
|
||||
@@ -232,14 +233,49 @@ namespace UVC.UI.List.Tree
|
||||
if (result) HandleDropSuccess(treeListItem.Data, targetItem);
|
||||
}
|
||||
|
||||
// 계약상 TryDrop은 EndDrag를 호출하지 않음 → 여기서 종료
|
||||
dragDropManager.EndDrag();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드래그 중 아이템이 마우스를 따라다니도록 위치를 업데이트합니다.
|
||||
/// Y축만 이동(X축 고정).
|
||||
/// </summary>
|
||||
/// <param name="eventData">포인터 이벤트 데이터.</param>
|
||||
private void AutoScrollIfNeeded()
|
||||
{
|
||||
var sr = treeList != null ? treeList.GetComponent<ScrollRect>() : null;
|
||||
if (sr == null || sr.content == null) return;
|
||||
var viewport = sr.viewport != null ? sr.viewport : sr.transform as RectTransform;
|
||||
if (viewport == null) return;
|
||||
|
||||
// 콘텐츠가 스크롤 가능하지 않으면 종료
|
||||
float scrollableHeight = Mathf.Max(0f, sr.content.rect.height - viewport.rect.height);
|
||||
if (scrollableHeight <= 1f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 마우스의 Y 위치와 뷰포트 상/하단 비교(월드 좌표)
|
||||
Vector3[] corners = new Vector3[4];
|
||||
viewport.GetWorldCorners(corners);
|
||||
float bottomY = corners[0].y;
|
||||
float topY = corners[1].y;
|
||||
float mouseY = _lastPointerScreenPos.y;
|
||||
|
||||
float dt = Time.unscaledDeltaTime;
|
||||
float pixelsPerSec = autoScrollPixelsPerSecond;
|
||||
if (mouseY > topY - autoScrollEdgePadding)
|
||||
{
|
||||
// 위로 스크롤(정규화 값 증가)
|
||||
float factor = Mathf.Clamp01((mouseY - (topY - autoScrollEdgePadding)) / autoScrollEdgePadding);
|
||||
float deltaNorm = (pixelsPerSec * dt / scrollableHeight) * factor;
|
||||
sr.verticalNormalizedPosition = Mathf.Clamp01(sr.verticalNormalizedPosition + deltaNorm);
|
||||
}
|
||||
else if (mouseY < bottomY + autoScrollEdgePadding)
|
||||
{
|
||||
// 아래로 스크롤(정규화 값 감소)
|
||||
float factor = Mathf.Clamp01(((bottomY + autoScrollEdgePadding) - mouseY) / autoScrollEdgePadding);
|
||||
float deltaNorm = (pixelsPerSec * dt / scrollableHeight) * factor;
|
||||
sr.verticalNormalizedPosition = Mathf.Clamp01(sr.verticalNormalizedPosition - deltaNorm);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDragPosition(PointerEventData eventData)
|
||||
{
|
||||
if (rectTransform == null || treeList == null)
|
||||
@@ -253,26 +289,20 @@ namespace UVC.UI.List.Tree
|
||||
return;
|
||||
}
|
||||
|
||||
// 스크린 좌표를 캔버스 로컬 좌표로 변환
|
||||
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
treeListRootParent, //canvasRect,
|
||||
eventData.position,
|
||||
null,
|
||||
out var canvasLocalPoint))
|
||||
treeListRootParent,
|
||||
eventData.position,
|
||||
null,
|
||||
out var canvasLocalPoint))
|
||||
{
|
||||
// ✅ Y축만 업데이트 (X는 고정)
|
||||
// 드래그 오프셋을 고려한 Y 위치만 계산
|
||||
var currentPosition = rectTransform.anchoredPosition;
|
||||
rectTransform.anchoredPosition = new Vector2(
|
||||
currentPosition.x, // ✅ X는 그대로 유지
|
||||
canvasLocalPoint.y - dragOffset.y // ✅ Y만 마우스를 따라 이동
|
||||
currentPosition.x,
|
||||
canvasLocalPoint.y - dragOffset.y
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드롭 위치 표시기(DropIndicator)를 생성하거나 재사용합니다.
|
||||
/// </summary>
|
||||
private void CreateDropIndicator()
|
||||
{
|
||||
Debug.Log($"[CreateDropIndicator] dropIndicator != null:{dropIndicator != null}");
|
||||
@@ -281,11 +311,9 @@ namespace UVC.UI.List.Tree
|
||||
return;
|
||||
}
|
||||
|
||||
//기존에 생성된 DropIndicator가 있는지 확인
|
||||
var existingDropIndicator = FindDropIndicatorInRoot();
|
||||
if (existingDropIndicator != null)
|
||||
{
|
||||
// 기존 DropIndicator 사용
|
||||
dropIndicator = existingDropIndicator.GetComponent<Image>();
|
||||
dropIndicatorRect = existingDropIndicator.GetComponent<RectTransform>();
|
||||
dropIndicatorParent = treeListRootParent;
|
||||
@@ -294,9 +322,7 @@ namespace UVC.UI.List.Tree
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Root(root)를 부모로 설정
|
||||
// 계층 구조: TreeList > ScrollView > Viewport > Content > Root > TreeListItem
|
||||
dropIndicatorParent = treeListRootParent;// rectTransform?.parent as RectTransform;
|
||||
dropIndicatorParent = treeListRootParent;
|
||||
|
||||
if (dropIndicatorParent == null)
|
||||
{
|
||||
@@ -304,23 +330,19 @@ namespace UVC.UI.List.Tree
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 새로운 GameObject 생성
|
||||
var indicatorGo = new GameObject("DropIndicator");
|
||||
indicatorGo.transform.SetParent(dropIndicatorParent, false);
|
||||
|
||||
// Image 컴포넌트 추가
|
||||
dropIndicator = indicatorGo.AddComponent<Image>();
|
||||
dropIndicator.color = new Color(0.2f, 0.8f, 1f, 0.5f); // 반투명 파란색
|
||||
dropIndicator.color = new Color(0.2f, 0.8f, 1f, 0.5f);
|
||||
|
||||
// RectTransform 설정
|
||||
dropIndicatorRect = indicatorGo.GetComponent<RectTransform>();
|
||||
if (dropIndicatorRect != null)
|
||||
{
|
||||
dropIndicatorRect.anchorMin = new Vector2(0, 1f);
|
||||
dropIndicatorRect.anchorMax = new Vector2(1, 1f);
|
||||
dropIndicatorRect.pivot = new Vector2(0, 1f);
|
||||
dropIndicatorRect.sizeDelta = new Vector2(0, 2f); // 높이 3
|
||||
dropIndicatorRect.sizeDelta = new Vector2(0, 2f);
|
||||
}
|
||||
|
||||
dropIndicator.raycastTarget = false;
|
||||
@@ -329,12 +351,6 @@ namespace UVC.UI.List.Tree
|
||||
Debug.Log("[CreateDropIndicator] 드롭 위치 표시 막대 생성됨");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드롭 위치 표시기를 갱신합니다.
|
||||
/// 복잡도: 대상 RectTransform의 월드 코너를 부모 기준 로컬 좌표로 환산하여,
|
||||
/// 위/아래/자식(블록) 위치를 정확히 계산합니다.
|
||||
/// </summary>
|
||||
/// <param name="targetItem">현재 마우스 아래의 대상 아이템(null이면 숨김).</param>
|
||||
private void UpdateDropIndicator(TreeListItem? targetItem)
|
||||
{
|
||||
if (dropIndicator == null || dropIndicatorRect == null || dropIndicatorParent == null)
|
||||
@@ -348,7 +364,6 @@ namespace UVC.UI.List.Tree
|
||||
return;
|
||||
}
|
||||
|
||||
// 드롭 위치 판단
|
||||
var targetRect = targetItem.GetComponent<RectTransform>();
|
||||
if (targetRect == null)
|
||||
{
|
||||
@@ -360,17 +375,12 @@ namespace UVC.UI.List.Tree
|
||||
|
||||
dropIndicator.gameObject.SetActive(true);
|
||||
|
||||
// ✅ 핵심: 월드 좌표로 변환해서 부모 기준 로컬 좌표 계산
|
||||
Vector3[] targetCorners = new Vector3[4];
|
||||
targetRect.GetWorldCorners(targetCorners);
|
||||
|
||||
// 🎯 아이템의 월드 X 좌표 (왼쪽 끝)
|
||||
float targetWorldX = targetCorners[0].x;
|
||||
|
||||
// 아이템의 월드 Y 좌표
|
||||
float targetWorldY = targetCorners[0].y;
|
||||
|
||||
// 부모(Root)의 월드 좌표
|
||||
Vector3[] parentCorners = new Vector3[4];
|
||||
dropIndicatorParent.GetWorldCorners(parentCorners);
|
||||
float parentWorldY = parentCorners[0].y;
|
||||
@@ -378,59 +388,43 @@ namespace UVC.UI.List.Tree
|
||||
float parentHeight = parentCorners[1].y - parentCorners[0].y;
|
||||
float parentWidth = parentCorners[3].x - parentCorners[0].x;
|
||||
|
||||
// 🎯 월드 X를 부모 기준 로컬 X로 변환
|
||||
float relativeX = targetWorldX - parentWorldX;
|
||||
|
||||
// 월드 Y를 부모 기준 로컬 Y로 변환
|
||||
float relativeY = targetWorldY - parentWorldY;
|
||||
|
||||
// 🎯 부모의 pivot을 고려한 로컬 X 계산
|
||||
float pivotAdjustedX = relativeX - (parentWidth * dropIndicatorParent.pivot.x);
|
||||
|
||||
// 부모의 pivot을 고려한 로컬 Y 계산
|
||||
float pivotAdjustedY = relativeY - (parentHeight * dropIndicatorParent.pivot.y) + targetRect.rect.height / 2;
|
||||
|
||||
float indicatorX = 0;
|
||||
float indicatorY = 0;
|
||||
float indicatorHeight = 3f; // 기본 높이
|
||||
float indicatorHeight = 3f;
|
||||
|
||||
switch (dropPosition)
|
||||
{
|
||||
case DropPosition.Above:
|
||||
// 대상 아이템 위 (아이템 높이의 절반)
|
||||
case TreeDropPosition.Above:
|
||||
indicatorX = pivotAdjustedX;
|
||||
indicatorY = pivotAdjustedY + (targetRect.rect.height / 2);
|
||||
indicatorHeight = 2f; // 얇은 선
|
||||
indicatorHeight = 2f;
|
||||
break;
|
||||
|
||||
case DropPosition.Below:
|
||||
// 대상 아이템 아래
|
||||
case TreeDropPosition.Below:
|
||||
indicatorX = pivotAdjustedX;
|
||||
indicatorY = pivotAdjustedY - (targetRect.rect.height / 2);
|
||||
indicatorHeight = 2f; // 얇은 선
|
||||
indicatorHeight = 2f;
|
||||
break;
|
||||
|
||||
case DropPosition.InsideAsChild:
|
||||
// 대상 아이템 중앙
|
||||
case TreeDropPosition.InsideAsChild:
|
||||
indicatorX = pivotAdjustedX;
|
||||
indicatorY = pivotAdjustedY + (targetRect.rect.height / 2); //indicatorHeight 크기 때문에 반영
|
||||
// 🎯 targetRect의 높이만큼 설정
|
||||
indicatorY = pivotAdjustedY + (targetRect.rect.height / 2);
|
||||
indicatorHeight = targetRect.rect.height;
|
||||
break;
|
||||
}
|
||||
|
||||
// ✅ DropIndicator 위치 설정
|
||||
dropIndicatorRect.anchoredPosition = new Vector2(indicatorX, indicatorY);
|
||||
// 🎯 DropIndicator 높이 설정
|
||||
dropIndicatorRect.sizeDelta = new Vector2(dropIndicatorRect.sizeDelta.x, indicatorHeight);
|
||||
//dropIndicatorRect의 x 시작 위치를 targetRect.rect의 위치와 맞춤
|
||||
|
||||
Debug.Log($"[UpdateDropIndicator] {targetItem?.Data?.Name} 위치: {dropPosition}, targetY: {targetWorldY}, parentY: {parentWorldY}, indicatorY: {indicatorY}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드롭 위치 표시기를 숨깁니다.
|
||||
/// </summary>
|
||||
private void HideDropIndicator()
|
||||
{
|
||||
if (dropIndicator != null)
|
||||
@@ -439,10 +433,6 @@ namespace UVC.UI.List.Tree
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// treeListRootParent의 직접 자식 중 "DropIndicator" GameObject를 찾습니다.
|
||||
/// </summary>
|
||||
/// <returns>찾은 GameObject. 없으면 null.</returns>
|
||||
private GameObject? FindDropIndicatorInRoot()
|
||||
{
|
||||
if (treeListRootParent == null)
|
||||
@@ -451,12 +441,9 @@ namespace UVC.UI.List.Tree
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1️. treeListRootParent의 모든 직접 자식을 순회
|
||||
for (int i = 0; i < treeListRootParent.childCount; i++)
|
||||
{
|
||||
Transform child = treeListRootParent.GetChild(i);
|
||||
|
||||
// 2️. 자식의 이름이 "DropIndicator"인지 확인
|
||||
if (child.name == "DropIndicator")
|
||||
{
|
||||
Debug.Log("[FindDropIndicatorInRoot] DropIndicator를 찾았습니다");
|
||||
@@ -468,11 +455,6 @@ namespace UVC.UI.List.Tree
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스크린 좌표에 위치한 TreeListItem을 찾습니다.
|
||||
/// </summary>
|
||||
/// <param name="screenPosition">스크린 좌표.</param>
|
||||
/// <returns>찾은 TreeListItem. 없으면 null.</returns>
|
||||
private TreeListItem? GetItemAtMousePosition(Vector2 screenPosition)
|
||||
{
|
||||
if (treeList == null)
|
||||
@@ -480,21 +462,27 @@ namespace UVC.UI.List.Tree
|
||||
return null;
|
||||
}
|
||||
|
||||
// Raycast로 UI 요소 찾기
|
||||
var results = new System.Collections.Generic.List<RaycastResult>();
|
||||
var eventData = new PointerEventData(EventSystem.current)
|
||||
var es = EventSystem.current;
|
||||
if (es == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
s_RaycastResults.Clear();
|
||||
var eventData = new PointerEventData(es)
|
||||
{
|
||||
position = screenPosition
|
||||
};
|
||||
|
||||
EventSystem.current.RaycastAll(eventData, results);
|
||||
es.RaycastAll(eventData, s_RaycastResults);
|
||||
|
||||
Debug.Log($"[GetItemAtMousePosition] Raycast 결과: {results.Count}개");
|
||||
Debug.Log($"[GetItemAtMousePosition] Raycast 결과: {s_RaycastResults.Count}개");
|
||||
|
||||
foreach (var result in results)
|
||||
for (int i = 0; i < s_RaycastResults.Count; i++)
|
||||
{
|
||||
var result = s_RaycastResults[i];
|
||||
var item = result.gameObject.GetComponentInParent<TreeListItem>();
|
||||
if (item != null)// && item != treeListItem)
|
||||
if (item != null)
|
||||
{
|
||||
Debug.Log($"[GetItemAtMousePosition] 찾은 아이템: {item.Data?.Name ?? "Unknown"}");
|
||||
return item;
|
||||
@@ -504,13 +492,6 @@ namespace UVC.UI.List.Tree
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 드롭 성공 후 데이터 구조를 갱신합니다.
|
||||
/// 루트 드롭/형제 간 이동/자식으로 이동 등을 처리합니다.
|
||||
/// </summary>
|
||||
/// <param name="draggedData">드래그한 데이터.</param>
|
||||
/// <param name="targetItem">드롭 대상 UI 아이템(null: 루트로 드롭).</param>
|
||||
private void HandleDropSuccess(TreeListItemData? draggedData, TreeListItem? targetItem)
|
||||
{
|
||||
if (draggedData == null || treeList == null)
|
||||
@@ -518,12 +499,11 @@ namespace UVC.UI.List.Tree
|
||||
return;
|
||||
}
|
||||
|
||||
// 드롭 대상이 없으면 (빈 공간에 드롭) 루트로 이동
|
||||
if (targetItem == null)
|
||||
{
|
||||
Debug.Log($"[HandleDropSuccess] 루트로 이동");
|
||||
Debug.Log("[HandleDropSuccess] 루트로 이동");
|
||||
MoveToRoot(draggedData);
|
||||
treeList.UpdateFlattenedItemDataList();
|
||||
treeList.ScheduleFlattenedUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -533,80 +513,52 @@ namespace UVC.UI.List.Tree
|
||||
return;
|
||||
}
|
||||
|
||||
// 드롭 위치 판단: 대상의 위/아래 또는 자식으로
|
||||
var dropPosition = GetDropPosition(targetItem.GetComponent<RectTransform>());
|
||||
|
||||
Debug.Log($"[HandleDropSuccess] 드롭 위치: {targetItem?.Data?.Name} {dropPosition}");
|
||||
|
||||
switch (dropPosition)
|
||||
{
|
||||
case DropPosition.InsideAsChild:
|
||||
// 대상의 자식으로 이동
|
||||
case TreeDropPosition.InsideAsChild:
|
||||
MoveAsChild(draggedData, targetData);
|
||||
break;
|
||||
|
||||
case DropPosition.Above:
|
||||
// 대상 위에 위치 (같은 부모 내에서)
|
||||
case TreeDropPosition.Above:
|
||||
MoveBefore(draggedData, targetData);
|
||||
break;
|
||||
|
||||
case DropPosition.Below:
|
||||
// 대상 아래에 위치 (같은 부모 내에서)
|
||||
case TreeDropPosition.Below:
|
||||
MoveAfter(draggedData, targetData);
|
||||
break;
|
||||
}
|
||||
|
||||
treeList.UpdateFlattenedItemDataList();
|
||||
treeList.ScheduleFlattenedUpdate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 대상 RectTransform 내 마우스 Y 위치로 위/아래/자식 드롭 위치를 판정합니다.
|
||||
/// 상/하 30% → 위/아래, 중간 40% → 자식.
|
||||
/// </summary>
|
||||
/// <param name="targetRect">대상 아이템 RectTransform.</param>
|
||||
/// <returns>드롭 위치.</returns>
|
||||
private DropPosition GetDropPosition(RectTransform targetRect)
|
||||
private TreeDropPosition GetDropPosition(RectTransform targetRect)
|
||||
{
|
||||
// 목표: 마우스 Y 위치를 기반으로 위/아래/안쪽 판단
|
||||
// 상위 1/3: 위
|
||||
// 중간 1/3: 자식
|
||||
// 하위 1/3: 아래
|
||||
|
||||
var height = targetRect.rect.height;
|
||||
var thresholdUpper = height * 0.3f;
|
||||
var thresholdLower = height * 0.7f;
|
||||
var thresholdUpper = height * dropEdgeRatio;
|
||||
var thresholdLower = height * (1f - dropEdgeRatio);
|
||||
|
||||
// 월드 좌표에서 로컬 좌표로 변환
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
targetRect,
|
||||
Input.mousePosition,
|
||||
null,
|
||||
out var localMousePos);
|
||||
targetRect,
|
||||
Input.mousePosition,
|
||||
null,
|
||||
out var localMousePos);
|
||||
|
||||
// 로컬 Y 좌표로 판단 (RectTransform의 피벗이 중앙이면 -height/2 ~ height/2)
|
||||
float relativeY = localMousePos.y;
|
||||
float relativeY = localMousePos.y + height / 2f; //0..height 기준
|
||||
|
||||
//Debug.Log($"GetDropPosition height:{height}, relativeY:{relativeY}, thresholdUpper:{thresholdUpper}, thresholdLower:{thresholdLower}");
|
||||
if (relativeY <= thresholdUpper)
|
||||
{
|
||||
return TreeDropPosition.Below; // 좌표계 보정에 따라 아래/위 반전 가능 → 아래쪽 영역
|
||||
}
|
||||
if (relativeY >= thresholdLower)
|
||||
{
|
||||
return TreeDropPosition.Above; // 위쪽 영역
|
||||
}
|
||||
|
||||
if (relativeY > -thresholdUpper)
|
||||
{
|
||||
return DropPosition.Above;
|
||||
}
|
||||
else if (relativeY < -thresholdLower)
|
||||
{
|
||||
return DropPosition.Below;
|
||||
}
|
||||
else
|
||||
{
|
||||
return DropPosition.InsideAsChild;
|
||||
}
|
||||
return TreeDropPosition.InsideAsChild;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 아이템을 루트 레벨로 이동합니다.
|
||||
/// 이미 루트면 순서만 변경합니다.
|
||||
/// </summary>
|
||||
/// <param name="draggedData">드래그된 데이터.</param>
|
||||
private void MoveToRoot(TreeListItemData draggedData)
|
||||
{
|
||||
Debug.Log($"[MoveToRoot] {draggedData.Name}을(를) 루트로 이동");
|
||||
@@ -624,40 +576,24 @@ namespace UVC.UI.List.Tree
|
||||
|
||||
if (draggedData.Parent == null)
|
||||
{
|
||||
// 이미 루트 레벨인 경우 순서만 변경
|
||||
treeList.Items.Remove(draggedData);
|
||||
treeList.SwapItem(draggedData, treeList.Items.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 루트 레벨에 아이템이 없거나 유효한 위치를 찾지 못한 경우 끝에 추가
|
||||
treeList.AddCloneItem(draggedData);
|
||||
}
|
||||
Debug.Log($"[MoveToRoot] {draggedData.Name}을(를) 루트 레벨의 끝에 추가");
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 아이템을 대상의 자식으로 이동합니다.
|
||||
/// </summary>
|
||||
/// <param name="draggedData">이동할 데이터.</param>
|
||||
/// <param name="targetData">대상 데이터.</param>
|
||||
private void MoveAsChild(TreeListItemData draggedData, TreeListItemData targetData)
|
||||
{
|
||||
targetData.AddCloneChild(draggedData);
|
||||
Debug.Log($"[MoveAsChild] {draggedData.Name}을(를) {targetData.Name}의 자식으로 이동");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 아이템을 대상 앞(위)으로 이동합니다.
|
||||
/// 같은 부모면 재정렬, 다르면 부모 교체 후 삽입합니다.
|
||||
/// </summary>
|
||||
/// <param name="draggedData">이동할 데이터.</param>
|
||||
/// <param name="targetData">기준 데이터.</param>
|
||||
private void MoveBefore(TreeListItemData draggedData, TreeListItemData targetData)
|
||||
{
|
||||
|
||||
//부모가 같은 경우 순서만 변경
|
||||
if (draggedData.Parent == targetData.Parent)
|
||||
{
|
||||
if (draggedData.Parent != null)
|
||||
@@ -688,21 +624,18 @@ namespace UVC.UI.List.Tree
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
var parentData = targetData.Parent;// FindParentOfItem(targetData);
|
||||
//RemoveFromParent(draggedData);
|
||||
var parentData = targetData.Parent;
|
||||
if (parentData != null)
|
||||
{
|
||||
var targetIndex = parentData.Children.IndexOf(targetData);
|
||||
if (targetIndex >= 0)
|
||||
{
|
||||
parentData.AddCloneAtChild(draggedData, targetIndex); // 자식으로 추가
|
||||
parentData.AddCloneAtChild(draggedData, targetIndex);
|
||||
Debug.Log($"[MoveBefore] {draggedData.Name}을(를) {targetData.Name} 앞으로 {targetIndex} 이동");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 루트 레벨인 경우
|
||||
var treeListItems = treeList?.Items;
|
||||
if (treeListItems != null)
|
||||
{
|
||||
@@ -713,20 +646,12 @@ namespace UVC.UI.List.Tree
|
||||
Debug.Log($"[MoveBefore] {draggedData.Name}을(를) {targetData.Name} 앞으로 {targetIndex} 이동 (루트 레벨)");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 아이템을 대상 뒤(아래)로 이동합니다.
|
||||
/// 같은 부모면 재정렬, 다르면 부모 교체 후 삽입합니다.
|
||||
/// </summary>
|
||||
/// <param name="draggedData">이동할 데이터.</param>
|
||||
/// <param name="targetData">기준 데이터.</param>
|
||||
private void MoveAfter(TreeListItemData draggedData, TreeListItemData targetData)
|
||||
{
|
||||
//부모가 같은 경우 순서만 변경
|
||||
if (draggedData.Parent == targetData.Parent)
|
||||
{
|
||||
if (draggedData.Parent != null)
|
||||
@@ -737,7 +662,7 @@ namespace UVC.UI.List.Tree
|
||||
if (targetIndex >= 0)
|
||||
{
|
||||
draggedData.Parent.SwapChild(draggedData, targetIndex + 1);
|
||||
Debug.Log($"[MoveBefore] {draggedData.Name}을(를) {targetData.Name} 앞으로 {targetIndex} 순서 변경");
|
||||
Debug.Log($"[MoveAfter] {draggedData.Name}을(를) {targetData.Name} 뒤로 {targetIndex} 순서 변경");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -750,7 +675,7 @@ namespace UVC.UI.List.Tree
|
||||
if (targetIndex >= 0)
|
||||
{
|
||||
treeList?.SwapItem(draggedData, targetIndex + 1);
|
||||
Debug.Log($"[MoveBefore] {draggedData.Name}을(를) {targetData.Name} 앞으로 {targetIndex} 순서 변경(루트레벨)");
|
||||
Debug.Log($"[MoveAfter] {draggedData.Name}을(를) {targetData.Name} 뒤로 {targetIndex} 순서 변경(루트레벨)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -770,7 +695,6 @@ namespace UVC.UI.List.Tree
|
||||
}
|
||||
else
|
||||
{
|
||||
// 루트 레벨인 경우
|
||||
var treeListItems = treeList?.Items;
|
||||
if (treeListItems != null)
|
||||
{
|
||||
@@ -784,22 +708,5 @@ namespace UVC.UI.List.Tree
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 드롭 위치를 나타냅니다.
|
||||
/// </summary>
|
||||
private enum DropPosition
|
||||
{
|
||||
/// <summary>위 (형제 아이템으로 앞쪽)</summary>
|
||||
Above,
|
||||
|
||||
/// <summary>안쪽 (자식으로)</summary>
|
||||
InsideAsChild,
|
||||
|
||||
/// <summary>아래 (형제 아이템으로 뒤쪽)</summary>
|
||||
Below
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable enable
|
||||
using Cysharp.Threading.Tasks;
|
||||
using DG.Tweening;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using TMPro;
|
||||
@@ -10,6 +11,25 @@ using UVC.UI.List.Tree;
|
||||
|
||||
namespace UVC.UI.Window
|
||||
{
|
||||
/// <summary>
|
||||
/// 계층 데이터를 표시/검색/선택하는 창(View)입니다.
|
||||
///
|
||||
/// 책임:
|
||||
/// - 메인 트리(`treeList`)와 검색 트리(`treeListSearch`)를 관리
|
||||
/// - 입력창으로 검색을 수행하고 결과를 검색 트리에 표시(청크 처리 + 로딩 애니메이션)
|
||||
/// - `TreeList.OnItemSelectionChanged`를 구독해 외부로 선택/해제 이벤트를 전달
|
||||
/// - 외부에서 호출 가능한 간단한 항목 추가/삭제 API 제공(실제 렌더링/상태는 `TreeList`가 담당)
|
||||
///
|
||||
/// 사용 예:
|
||||
/// <example>
|
||||
/// <![CDATA[
|
||||
/// // 외부에서 창을 참조했다고 가정
|
||||
/// hierarchyWindow.OnItemSelected += item => Debug.Log($"Selected: {item.Name}");
|
||||
/// hierarchyWindow.AddItem(new TreeListItemData("Root A"));
|
||||
/// hierarchyWindow.AddItemAt(new TreeListItemData("Root B"),0);
|
||||
/// ]]>
|
||||
/// </example>
|
||||
/// </summary>
|
||||
public class HierarchyWindow : MonoBehaviour
|
||||
{
|
||||
[SerializeField]
|
||||
@@ -30,22 +50,31 @@ namespace UVC.UI.Window
|
||||
[SerializeField]
|
||||
protected Image loadingImage;
|
||||
|
||||
/// <summary>
|
||||
/// 메인/검색 리스트에서 항목이 선택될 때 발생합니다.
|
||||
/// </summary>
|
||||
public System.Action<TreeListItemData>? OnItemSelected;
|
||||
|
||||
/// <summary>
|
||||
/// 메인/검색 리스트에서 항목이 선택 해제될 때 발생합니다.
|
||||
/// </summary>
|
||||
public System.Action<TreeListItemData>? OnItemDeselected;
|
||||
|
||||
// 검색 목록에서 선택된 항목(클론된 데이터)
|
||||
private TreeListItemData? selectedSearchItem;
|
||||
|
||||
// 검색 작업 상태
|
||||
private CancellationTokenSource? searchCts;
|
||||
private bool isSearching = false;
|
||||
private float searchProgress = 0f; //unused for visual progress now but kept for future
|
||||
private float searchProgress =0f; // 내부 진행도 추적용
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("로딩 아이콘 회전 속도(도/초)")]
|
||||
private float loadingRotateSpeed = 360f;
|
||||
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 보다 높게 설정하면 이상해 보임
|
||||
private float loadingFillCycle =0.5f; // cycles per second. loadingRotateSpeed360 일때, loadingFillCycle를0.5 보다 높게 설정하면 이상해 보임
|
||||
|
||||
// DOTween tweens
|
||||
private Tween? loadingRotationTween;
|
||||
@@ -58,7 +87,13 @@ namespace UVC.UI.Window
|
||||
treeListSearch.gameObject.SetActive(false);
|
||||
inputField.onSubmit.AddListener(OnInputFieldSubmit);
|
||||
|
||||
// 검색 리스트의 선택 변경을 감지
|
||||
// 메인 리스트 선택 변경을 외부 이벤트로 전달
|
||||
if (treeList != null)
|
||||
{
|
||||
treeList.OnItemSelectionChanged += HandleMainSelectionChanged;
|
||||
}
|
||||
|
||||
// 검색 리스트의 선택 변경을 감지 (선택 결과를 원본 트리에 반영하는 용도)
|
||||
if (treeListSearch != null)
|
||||
{
|
||||
treeListSearch.OnItemSelectionChanged += OnSearchSelectionChanged;
|
||||
@@ -76,11 +111,11 @@ namespace UVC.UI.Window
|
||||
// 검색에서 선택한 항목이 있으면 원본 트리에서 동일 항목을 선택하고 펼침
|
||||
if (selectedSearchItem != null && treeList != null)
|
||||
{
|
||||
// 원본 데이터 찾기 (이 프로젝트의 Equals는 Name 기반이므로 Name으로 검색)
|
||||
var target = treeList.AllItemDataFlattened.FirstOrDefault(i => i.Name == selectedSearchItem.Name);
|
||||
// 원본 데이터 찾기 (TreeListItemData == 연산자는 Id 기반)
|
||||
var target = treeList.AllItemDataFlattened.FirstOrDefault(i => i == selectedSearchItem);
|
||||
if (target != null)
|
||||
{
|
||||
// TreeList에 새로 추가된 유틸리티를 이용해 부모 체인을 펼치고 선택 처리
|
||||
// 부모 체인을 펼치고 선택 처리
|
||||
treeList.RevealAndSelectItem(target, true);
|
||||
}
|
||||
|
||||
@@ -89,33 +124,65 @@ namespace UVC.UI.Window
|
||||
});
|
||||
}
|
||||
|
||||
private void Update()
|
||||
/// <summary>
|
||||
/// 메인 트리에 항목을 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="data">추가할 데이터.</param>
|
||||
public void AddItem(TreeListItemData data)
|
||||
{
|
||||
// DOTween handles the animations; no per-frame logic needed here
|
||||
treeList.AddItem(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 메인 트리에 항목을 특정 인덱스에 삽입합니다.
|
||||
/// </summary>
|
||||
/// <param name="data">삽입할 데이터.</param>
|
||||
/// <param name="index">삽입 인덱스(0 기반).</param>
|
||||
public void AddItemAt(TreeListItemData data, int index)
|
||||
{
|
||||
treeList.AddItemAt(data, index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 메인 트리에서 항목을 제거합니다(뷰만 제거, 데이터는 호출 측 정책에 따름).
|
||||
/// </summary>
|
||||
/// <param name="data">제거할 데이터.</param>
|
||||
public void RemoveItem(TreeListItemData data)
|
||||
{
|
||||
treeList.RemoveItem(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 메인 트리에서 항목을 완전히 삭제합니다(뷰+데이터 정리).
|
||||
/// </summary>
|
||||
/// <param name="data">삭제할 데이터.</param>
|
||||
public void DeleteItem(TreeListItemData data)
|
||||
{
|
||||
treeList.DeleteItem(data);
|
||||
}
|
||||
|
||||
private void StartLoadingAnimation()
|
||||
{
|
||||
if (loadingImage == null) return;
|
||||
|
||||
// Ensure any previous tweens are killed
|
||||
// 기존 트윈 정리
|
||||
StopLoadingAnimation();
|
||||
|
||||
loadingImage.fillAmount = 0f;
|
||||
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)
|
||||
// 회전 트윈
|
||||
float rotDuration = (loadingRotateSpeed !=0f) ? (360f / Mathf.Abs(loadingRotateSpeed)) :1f;
|
||||
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)
|
||||
// 채우기 트윈
|
||||
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);
|
||||
}
|
||||
@@ -137,9 +204,8 @@ namespace UVC.UI.Window
|
||||
if (loadingImage != null)
|
||||
{
|
||||
loadingImage.gameObject.SetActive(false);
|
||||
// reset transform / fill
|
||||
loadingImage.transform.localRotation = Quaternion.identity;
|
||||
loadingImage.fillAmount = 0f;
|
||||
loadingImage.fillAmount =0f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,9 +218,8 @@ namespace UVC.UI.Window
|
||||
searchCts = null;
|
||||
}
|
||||
isSearching = false;
|
||||
searchProgress = 0f;
|
||||
searchProgress =0f;
|
||||
|
||||
// stop DOTween animations
|
||||
StopLoadingAnimation();
|
||||
}
|
||||
|
||||
@@ -163,10 +228,24 @@ namespace UVC.UI.Window
|
||||
if (isSelected)
|
||||
{
|
||||
selectedSearchItem = data;
|
||||
OnItemSelected?.Invoke(data);
|
||||
}
|
||||
else if (selectedSearchItem == data)
|
||||
{
|
||||
selectedSearchItem = null;
|
||||
OnItemDeselected?.Invoke(data);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleMainSelectionChanged(TreeListItemData data, bool isSelected)
|
||||
{
|
||||
if (isSelected)
|
||||
{
|
||||
OnItemSelected?.Invoke(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnItemDeselected?.Invoke(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +281,7 @@ namespace UVC.UI.Window
|
||||
private async UniTaskVoid PerformSearchAsync(string text, CancellationToken token)
|
||||
{
|
||||
isSearching = true;
|
||||
searchProgress = 0f;
|
||||
searchProgress =0f;
|
||||
|
||||
var results = new System.Collections.Generic.List<TreeListItemData>();
|
||||
|
||||
@@ -215,7 +294,7 @@ namespace UVC.UI.Window
|
||||
}
|
||||
|
||||
int total = sourceList.Count;
|
||||
if (total == 0)
|
||||
if (total ==0)
|
||||
{
|
||||
isSearching = false;
|
||||
StopLoadingAnimation();
|
||||
@@ -226,8 +305,8 @@ namespace UVC.UI.Window
|
||||
string lower = text.ToLowerInvariant();
|
||||
|
||||
// 분할 처리: 일정 갯수마다 await으로 제어권을 반환
|
||||
const int chunk = 100; // 한 번에 처리할 항목 수 (조절 가능)
|
||||
for (int i = 0; i < total; i++)
|
||||
const int chunk =100;
|
||||
for (int i =0; i < total; i++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -238,7 +317,7 @@ namespace UVC.UI.Window
|
||||
}
|
||||
|
||||
// 진행도 업데이트 (내부 사용)
|
||||
if ((i % chunk) == 0)
|
||||
if ((i % chunk) ==0)
|
||||
{
|
||||
searchProgress = (float)i / (float)total;
|
||||
await UniTask.Yield(PlayerLoopTiming.Update);
|
||||
@@ -246,7 +325,7 @@ namespace UVC.UI.Window
|
||||
}
|
||||
|
||||
// 최종 진행도
|
||||
searchProgress = 1f;
|
||||
searchProgress =1f;
|
||||
|
||||
// UI 반영은 메인 스레드에서
|
||||
await UniTask.SwitchToMainThread();
|
||||
@@ -291,12 +370,12 @@ namespace UVC.UI.Window
|
||||
treeListSearch.OnItemSelectionChanged -= OnSearchSelectionChanged;
|
||||
}
|
||||
|
||||
CancelSearch();
|
||||
}
|
||||
if (treeList != null)
|
||||
{
|
||||
treeList.OnItemSelectionChanged -= HandleMainSelectionChanged;
|
||||
}
|
||||
|
||||
public void AddItem(TreeListItemData data)
|
||||
{
|
||||
treeList.AddItem(data);
|
||||
CancelSearch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user