Files
XRLib/Assets/Scripts/UVC/UI/List/Tree/TreeListItemData.cs
2025-11-03 19:07:04 +09:00

446 lines
15 KiB
C#

#nullable enable
using Gpm.Ui;
using System;
using System.Collections.Generic;
namespace UVC.UI.List.Tree
{
/// <summary>
/// 트리 리스트 아이템의 데이터 모델.
///
/// 목적:
/// - 이름/옵션/선택/펼침 상태 및 계층(부모-자식) 보유
/// - 상태 변경 시 UI에 알릴 이벤트 제공(데이터 변경/선택 변경)
/// - 자식 추가/삽입/복제/재배치/초기화 등 계층 편집 기능
///
/// 이벤트 설계:
/// - OnDataChanged(ChangedType type, TreeListItemData changed, int index):
/// 데이터/구조 변경 통지. changed는 변동 주체(보통 this 또는 대상 자식),
/// index는 삽입/이동 시 사용(해당 없으면 -1).
/// - OnSelectionChanged(TreeListItemData data, bool isSelected):
/// 선택 상태 변경 통지(선택 UI/로직 처리용).
///
/// 동등성/해시:
/// - 이름(Name) 기반 비교/해시 코드. 이름이 같으면 같은 개체로 간주(대소문자 구분).
/// - 해시/Equals가 이름에 의존하므로 Name 변경 시 컬렉션 키로 사용 중이면 주의.
///
/// 주의사항:
/// - ClearChildren은 각 자식에 Dispose를 호출하여 이벤트/내부 상태를 정리한 뒤
/// 리스트를 비웁니다(연결만 끊는 수준을 넘어 자식도 정리됨).
/// - AddClone계열은 전달된 원본 child를 Dispose한 뒤 복제본을 추가합니다(파괴적).
/// </summary>
public class TreeListItemData : IDisposable
{
#region (Events)
/// <summary>
/// 데이터/구조 변경 통지 이벤트.
/// 시그니처: (ChangedType type, TreeListItemData changed, int index)
/// - type: 변경 종류
/// - changed: 변경 대상(보통 this 또는 특정 자식)
/// - index: 위치 관련 변경 시 사용(해당 없으면 -1)
/// </summary>
public Action<ChangedType, TreeListItemData, int>? OnDataChanged;
/// <summary>
/// 선택 상태 변경 통지 이벤트.
/// 시그니처: (TreeListItemData data, bool isSelected)
/// </summary>
public Action<TreeListItemData, bool>? OnSelectionChanged;
/// <summary>
/// 아이템 클릭 시 실행할 사용자 정의 동작.
/// (예: 속성 패널 열기, 포커스 이동 등. 확장/축소와는 별개)
/// </summary>
public Action<TreeListItemData>? OnClickAction;
#endregion
#region (Private Fields)
/// <summary>고유 식별자(Id).</summary>
private readonly Guid _id = Guid.NewGuid();
/// <summary>아이템 이름.</summary>
private string _name = string.Empty;
/// <summary>추가 옵션 문자열.</summary>
private string _option = string.Empty;
/// <summary>자식 펼침 여부.</summary>
private bool _isExpanded = false;
/// <summary>선택 여부.</summary>
private bool _isSelected = false;
/// <summary>부모</summary>
private TreeListItemData? _parent;
/// <summary>자식 리스트.</summary>
private List<TreeListItemData> _children = new List<TreeListItemData>();
#endregion
#region (Public Properties)
/// <summary>
/// 아이템 이름.
/// 변경 시 OnDataChanged(Name) 발생.
/// </summary>
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
NotifyDataChanged(ChangedType.Name); // UI에 변경을 알림
}
}
}
/// <summary>
/// 옵션 문자열.
/// 변경 시 OnDataChanged(Option) 발생.
/// </summary>
public string Option
{
get => _option;
set
{
if (_option != value)
{
_option = value;
NotifyDataChanged(ChangedType.Option);
}
}
}
/// <summary>
/// 펼침 상태(같은 어셈블리 내 전용).
/// 변경 시 OnDataChanged(Expanded) 발생.
/// </summary>
internal bool IsExpanded
{
get => _isExpanded;
set
{
if (_isExpanded != value)
{
_isExpanded = value;
NotifyDataChanged(ChangedType.Expanded); // 트리 구조 UI 갱신
}
}
}
/// <summary>
/// 선택 상태.
/// 변경 시 OnSelectionChanged 발생.
/// </summary>
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected != value)
{
_isSelected = value;
// OnSelectionChanged 이벤트 발생
// 예: "폴더가 선택되었습니다" 같은 처리를 수행
OnSelectionChanged?.Invoke(this, value);
}
}
}
/// <summary>부모 데이터(내부 전용).</summary>
internal TreeListItemData? Parent
{
get => _parent;
set => _parent = value;
}
/// <summary>
/// 자식 컬렉션(내부 전용).
/// set 시 null을 빈 리스트로 대체하고 OnDataChanged(ResetChildren) 발생.
/// </summary>
internal List<TreeListItemData> Children
{
get => _children;
set
{
_children = value ?? new List<TreeListItemData>();
NotifyDataChanged(ChangedType.ResetChildren);
}
}
/// <summary>고유 식별자(Id).</summary>
public Guid Id => _id;
#endregion
#region (Constructors)
/// <summary>
/// 기본 생성자.
/// 초기값: Name="", Option="", IsExpanded=false, IsSelected=false, Children=[]
/// </summary>
public TreeListItemData()
{
_name = string.Empty;
_option = string.Empty;
_isExpanded = false;
_isSelected = false;
_children = new List<TreeListItemData>();
}
/// <summary>
/// 이름과 초기 자식으로 생성합니다.
/// </summary>
/// <param name="generalName">아이템 이름(필수).</param>
/// <param name="childrenItemData">초기 자식 목록(null 허용).</param>
public TreeListItemData(string generalName, List<TreeListItemData>? childrenItemData = null)
{
_name = generalName;
_option = string.Empty;
_isExpanded = false;
_children = childrenItemData ?? new List<TreeListItemData>();
}
#endregion
#region (Child Management Methods)
/// <summary>
/// 자식을 끝에 추가합니다.
/// </summary>
/// <param name="child">추가할 자식.</param>
public void AddChild(TreeListItemData child)
{
child.Parent = this;
_children.Add(child);
NotifyDataChanged(ChangedType.AddChild, child); // UI에 트리 구조 변경 알림
}
/// <summary>
/// 자식을 지정 인덱스에 삽입합니다.
/// </summary>
/// <param name="child">삽입할 자식.</param>
/// <param name="index">삽입 인덱스(0 기반).</param>
public void AddChildAt(TreeListItemData child, int index)
{
child._parent = this;
_children.Insert(index, child);
NotifyDataChanged(ChangedType.AddAtChild, child, index); // UI에 트리 구조 변경 알림
}
/// <summary>
/// 전달된 자식을 복제하여 추가합니다.
/// 주의: 원본 child는 Dispose됩니다(파괴적).
/// </summary>
/// <param name="child">복제 및 대체할 원본 자식.</param>
public void AddCloneChild(TreeListItemData child)
{
NotifyDataChanged(ChangedType.AddCloneChild, child); // UI에 트리 구조 변경 알림
}
/// <summary>
/// 전달된 자식을 복제하여 지정 인덱스에 삽입합니다.
/// 주의: 원본 child는 Dispose됩니다(파괴적).
/// </summary>
/// <param name="child">복제 및 대체할 원본 자식.</param>
/// <param name="index">삽입 인덱스(0 기반).</param>
public void AddCloneAtChild(TreeListItemData child, int index)
{
NotifyDataChanged(ChangedType.AddCloneAtChild, child, index); // UI에 트리 구조 변경 알림
}
/// <summary>
/// 지정 자식을 지정 인덱스 위치에 삽입합니다.
/// 주의: 기존 위치에서 제거하지 않으므로 "이동"이 아닌 "삽입" 동작입니다.
/// (호출 측에서 기존 위치 정리는 별도로 해야 합니다)
/// </summary>
/// <param name="child">삽입할 기존 자식 참조.</param>
/// <param name="index">삽입 인덱스(0 기반).</param>
public void SwapChild(TreeListItemData child, int index)
{
_children.Insert(index, child);
NotifyDataChanged(ChangedType.SwapChild, child, index); // UI에 트리 구조 변경 알림
}
public void RemoveChild(TreeListItemData child)
{
if (_children.Remove(child))
{
child._parent = null;
NotifyDataChanged(ChangedType.RemoveChild, child); // UI에 트리 구조 변경 알림
}
}
/// <summary>
/// 모든 자식을 제거하고 정리합니다.
/// 각 자식에 대해 Parent=null 및 Dispose()를 호출한 뒤 리스트를 비웁니다.
/// </summary>
public void ClearChildren()
{
foreach (var child in _children)
{
child._parent = null;
child.Dispose();
}
_children.Clear();
NotifyDataChanged(ChangedType.ResetChildren); // UI에 트리 구조 변경 알림
}
#endregion
#region (Internal Methods)
/// <summary>
/// 변경을 구독자에게 알립니다.
/// </summary>
/// <param name="changedType">변경 종류.</param>
/// <param name="target">변경 대상(null이면 this).</param>
/// <param name="index">위치 관련 인덱스(없으면 -1).</param>
internal void NotifyDataChanged(ChangedType changedType, TreeListItemData? target = null, int index = -1)
{
// OnDataChanged가 등록되어 있으면 실행
// ?. 연산자: null이면 실행하지 않음 (null reference exception 방지)
OnDataChanged?.Invoke(changedType, (target == null ? this : target), index);
}
#endregion
#region (Comparison Operators)
/// <summary>
/// 이름(대소문자 구분, Ordinal) 기반 동등 비교.
/// </summary>
public static bool operator ==(TreeListItemData? left, TreeListItemData? right)
{
// 같은 객체인지 확인 (메모리 주소 비교)
if (ReferenceEquals(left, right))
{
return true;
}
// 하나 이상이 null이면 false (ReferenceEquals에서 둘 다 null인 경우는 true 반환)
if (left is null || right is null)
{
return false;
}
// 고유 식별자(Id)로 비교
return left.Id == right.Id;
}
/// <summary>
/// 이름 기반 비동등 비교.
/// </summary>
public static bool operator !=(TreeListItemData? left, TreeListItemData? right)
{
return !(left == right);
}
#endregion
#region (Object Methods)
/// <summary>
/// 이름 기반 해시 코드 반환.
/// </summary>
public override int GetHashCode()
{
return Id.GetHashCode();
}
/// <summary>
/// 이름 기반 동등성 비교(== 사용).
/// </summary>
/// <param name="obj">비교 대상.</param>
public override bool Equals(object? obj)
{
// obj가 TreeListItemData 타입인지 확인
if (obj is TreeListItemData other)
{
// TreeListItemData면 == 연산자로 비교
return this == other;
}
// 다른 타입이면 false
return false;
}
/// <summary>
/// 이벤트/참조/자식 컬렉션을 정리합니다.
/// - Parent=null, 이벤트 핸들러 해제, Children.Clear()
/// - 하위 항목을 재귀적으로 Dispose하지는 않습니다
/// (재귀 정리는 ClearChildren에서 수행).
/// </summary>
public void Dispose()
{
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)
{
Children.Clear();
}
}
/// <summary>
/// 깊은 복제본을 생성합니다(자식까지 재귀 복제).
/// </summary>
/// <returns>복제된 새 인스턴스.</returns>
public TreeListItemData CloneWithChild()
{
TreeListItemData clone = new TreeListItemData();
clone.Name = this.Name;
clone.Option = this.Option;
clone.IsExpanded = this.IsExpanded;
clone.IsSelected = this.IsSelected;
foreach (var child in this.Children)
{
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
}
/// <summary>
/// 데이터 변경 종류 열거형.
/// </summary>
public enum ChangedType
{
Name,
Option,
Expanded,
ResetChildren,
AddChild,
RemoveChild,
AddAtChild,
AddCloneChild,
AddCloneAtChild,
SwapChild,
}
}