Files
XRLib/Assets/Scripts/UVC/UI/List/Tree/TreeListDragDropManager.cs
2025-11-03 18:27:04 +09:00

205 lines
7.5 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
namespace UVC.UI.List.Tree
{
/// <summary>
/// 드래그 위치 표현. 위/안쪽/아래.
/// </summary>
public enum TreeDropPosition
{
Above,
InsideAsChild,
Below
}
/// <summary>
/// 트리 리스트의 드래그 & 드롭 상호작용을 상태와 이벤트로 중재하는 관리자입니다.
///
/// 역할:
/// - 드래그 시작/진행/종료 상태 관리(상태 머신)
/// - 드롭 유효성 검사(자기 자신/조상에게 드롭 금지로 순환 참조 방지)
/// - 구독자에게 이벤트로 진행 상황 전달
///
/// 책임 경계:
/// - 이 매니저는 데이터를 직접 수정(이동/부모 변경)하지 않습니다.
/// - 실제 아이템 재배치/재귀 로직은 이벤트 구독자(예: 리스트/뷰 모델)에서 수행해야 합니다.
///
/// 이벤트 흐름:
/// - StartDrag → OnDragStarted(once)
/// - OnDragOver/OnDragHovered → OnDragEntered(repeat)
/// - TryDrop 유효 시 → OnDropped → (caller) EndDrag → OnDragEnded
/// - TryDrop 무효/취소 시 → (caller) EndDrag → OnDragEnded
/// </summary>
public class TreeListDragDropManager
{
/// <summary>
/// 현재 드래그 중인 아이템입니다(드래그 중이 아니면 null).
/// </summary>
public TreeListItemData? DraggedItem { get; private set; }
/// <summary>
/// 현재 드래그 중인 상태인지 여부입니다.
/// </summary>
public bool IsDragging { get; private set; }
/// <summary>
/// 드래그 시작 시1회 발생하는 이벤트입니다.
/// 핸들러 시그니처: (TreeListItemData dragged)
/// </summary>
public Action<TreeListItemData>? OnDragStarted;
/// <summary>
/// 드래그 진행 중 마우스가 특정 아이템 위에 있을 때 반복적으로 발생하는 이벤트입니다.
/// 빈 공간 위라면 targetItem은 null이 될 수 있습니다.
/// 핸들러 시그니처: (TreeListItemData dragged, TreeListItemData? targetItem)
/// </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>
public Action<TreeListItemData>? OnDragEnded;
/// <summary>
/// 유효성 검사를 통과한 드롭이 확정될 때 발생하는 이벤트입니다.
/// 핸들러 시그니처: (TreeListItemData dragged, TreeListItemData? target)
/// 이 이벤트에서 실제 데이터 구조 변경(이동/부모 변경/정렬)을 수행하세요.
/// </summary>
public Action<TreeListItemData, TreeListItemData?>? OnDropped;
/// <summary>
/// 드래그를 시작합니다. 이미 드래그 중이면 무시됩니다.
/// </summary>
/// <param name="draggedItem">드래그할 아이템.</param>
public void StartDrag(TreeListItemData draggedItem)
{
if (IsDragging)
{
return;
}
DraggedItem = draggedItem;
IsDragging = true;
OnDragStarted?.Invoke(draggedItem);
}
/// <summary>
/// 드래그 중 마우스가 다른 아이템(또는 빈 영역) 위에 있을 때 호출됩니다.
/// 상태를 변경하지 않고, 단순히 현재 hover 대상을 이벤트로 통지합니다.
/// </summary>
/// <param name="targetItem">현재 마우스 아래의 아이템. 빈 공간이면 null.</param>
public void OnDragOver(TreeListItemData? targetItem)
{
if (!IsDragging || DraggedItem == null)
{
return;
}
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>
public void EndDrag()
{
if (!IsDragging || DraggedItem == null)
{
return;
}
OnDragEnded?.Invoke(DraggedItem);
IsDragging = false;
DraggedItem = null;
}
/// <summary>
/// 드래그된 아이템을 대상 아이템에 드롭 시도합니다.
/// 유효성 검사(자기 자신/조상에게 드롭 금지)를 통과한 경우에만 OnDropped를 발생시킵니다.
/// EndDrag는 호출 측(뷰/핸들러)에서 호출해야 합니다.
/// </summary>
/// <param name="targetItem">드롭 대상 아이템. 루트 레벨로 드롭하려면 null.</param>
/// <param name="insertIndex">미사용 예약 필드. -1이면 끝에 추가 의도.</param>
/// <returns>OnDropped가 발생하면 true, 아니면 false.</returns>
public bool TryDrop(TreeListItemData? targetItem, int insertIndex = -1)
{
if (!IsDragging || DraggedItem == null)
{
return false;
}
// 자기 자신에게 드롭하는 경우 무시
if (targetItem != null && targetItem == DraggedItem)
{
return false;
}
// 순환 참조 검사 (드래그 아이템이 드롭 대상의 부모인 경우)
if (targetItem != null && IsAncestorOf(DraggedItem, targetItem))
{
return false;
}
OnDropped?.Invoke(DraggedItem, targetItem);
return true;
}
/// <summary>
/// 첫 번째 아이템이 두 번째 아이템의 조상인지 확인합니다.
/// </summary>
public static bool IsAncestorOf(TreeListItemData potentialAncestor, TreeListItemData potentialDescendant)
{
var current = potentialDescendant.Parent;
while (current != null)
{
if (current == potentialAncestor)
{
return true;
}
current = current.Parent;
}
return false;
}
/// <summary>
/// 드래그 & 드롭 상태를 초기화합니다.
/// </summary>
public void Reset()
{
IsDragging = false;
DraggedItem = null;
}
}
}