#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace UVC.UI.Window.PropertyWindow
{
///
/// 속성 데이터를 관리하고, 데이터 변경 시 이벤트를 발생시키는 컨트롤러 클래스입니다.
/// Model과 View 사이의 중재자 역할을 합니다.
/// 그룹과 개별 아이템을 혼용하여 사용할 수 있습니다.
///
public class PropertyWindow : MonoBehaviour
{
[SerializeField]
private PropertyView _view;
#region Internal Data Structures
///
/// 통합 엔트리 목록 (그룹과 개별 아이템 혼합 저장)
///
private readonly List _entries = new List();
///
/// 빠른 그룹 조회를 위한 인덱스
///
private readonly Dictionary _groupIndex = new Dictionary();
///
/// 빠른 아이템 조회를 위한 인덱스
///
private readonly Dictionary _itemIndex = new Dictionary();
#endregion
#region Public Properties
///
/// 현재 컨트롤러가 관리하는 모든 속성 항목의 목록입니다.
/// 하위 호환성을 위해 유지됩니다. 그룹에 속한 아이템도 포함됩니다.
///
public List Properties
{
get
{
var allItems = new List();
foreach (var entry in _entries)
{
if (entry is IPropertyItem item && item.GroupId == null)
{
allItems.Add(item);
}
else if (entry is IPropertyGroup group)
{
allItems.AddRange(group.Items);
}
}
return allItems;
}
}
///
/// 정렬된 엔트리 목록을 반환합니다.
///
public IReadOnlyList Entries => _entries.OrderBy(e => e.Order).ToList().AsReadOnly();
///
/// 모든 그룹의 읽기 전용 목록을 반환합니다.
///
public IReadOnlyList Groups => _groupIndex.Values.ToList().AsReadOnly();
#endregion
#region Events
///
/// 속성 값이 변경될 때 발생하는 이벤트입니다.
///
public event EventHandler? PropertyValueChanged;
///
/// 그룹이 추가되었을 때 발생하는 이벤트입니다.
///
public event EventHandler? GroupAdded;
///
/// 그룹이 제거되었을 때 발생하는 이벤트입니다.
///
public event EventHandler? GroupRemoved;
///
/// 그룹의 펼침/접힘 상태가 변경되었을 때 발생하는 이벤트입니다.
///
public event EventHandler? GroupExpandedChanged;
///
/// 엔트리가 추가되었을 때 발생하는 이벤트입니다.
///
public event EventHandler? EntryAdded;
///
/// 엔트리가 제거되었을 때 발생하는 이벤트입니다.
///
public event EventHandler? EntryRemoved;
///
/// 모든 엔트리가 제거되었을 때 발생하는 이벤트입니다.
///
public event EventHandler? EntriesCleared;
#endregion
#region Load Methods (기존 호환 + 그룹 + 혼용)
///
/// [기존 방식] 그룹 없이 속성 목록을 로드합니다.
/// 모든 아이템이 flat하게 표시됩니다.
///
/// 표시할 속성 항목들의 목록
public void LoadProperties(List items)
{
Clear();
if (items != null)
{
foreach (var item in items)
{
item.GroupId = null; // 그룹 없음 명시
AddEntryInternal(item);
}
}
Refresh();
}
///
/// [그룹 방식] 그룹화된 속성 목록을 로드합니다.
///
/// 표시할 속성 그룹들의 목록
public void LoadGroupedProperties(List groups)
{
Clear();
if (groups != null)
{
foreach (var group in groups)
{
AddEntryInternal(group);
}
}
Refresh();
}
///
/// [혼용 방식] 그룹과 개별 아이템을 함께 로드합니다.
///
/// 표시할 엔트리들의 목록 (그룹 또는 아이템)
public void LoadMixedProperties(List entries)
{
Clear();
if (entries != null)
{
foreach (var entry in entries)
{
AddEntryInternal(entry);
}
}
Refresh();
}
#endregion
#region Group Management
///
/// 그룹을 추가합니다.
///
/// 추가할 그룹
public void AddGroup(IPropertyGroup group)
{
if (group == null)
throw new ArgumentNullException(nameof(group));
if (_groupIndex.ContainsKey(group.GroupId))
{
Debug.LogWarning($"[PropertyWindow] 이미 존재하는 그룹 ID입니다: {group.GroupId}");
return;
}
AddEntryInternal(group);
GroupAdded?.Invoke(this, new PropertyGroupEventArgs(group));
Refresh();
}
///
/// 그룹을 제거합니다.
///
/// 제거할 그룹의 ID
public void RemoveGroup(string groupId)
{
if (_groupIndex.TryGetValue(groupId, out var group))
{
// 그룹 내 모든 아이템의 GroupId 초기화 및 인덱스에서 제거
foreach (var item in group.Items)
{
item.GroupId = null;
_itemIndex.Remove(item.Id);
}
group.Clear();
_groupIndex.Remove(groupId);
_entries.Remove(group);
GroupRemoved?.Invoke(this, new PropertyGroupEventArgs(group));
Refresh();
}
}
///
/// 특정 ID의 그룹을 가져옵니다.
///
/// 그룹 ID
/// 그룹 또는 null
public IPropertyGroup? GetGroup(string groupId)
{
_groupIndex.TryGetValue(groupId, out var group);
return group;
}
///
/// 그룹의 펼침/접힘 상태를 변경합니다.
///
/// 그룹 ID
/// 펼침 상태
public void SetGroupExpanded(string groupId, bool isExpanded)
{
if (_groupIndex.TryGetValue(groupId, out var group))
{
bool wasExpanded = group.IsExpanded;
if (wasExpanded != isExpanded)
{
group.IsExpanded = isExpanded;
// 이벤트를 통해 PropertyView.OnGroupExpandedChanged()가 호출되어
// 부분적으로 UI를 업데이트합니다. 전체 Refresh()는 스크롤 위치를 초기화하므로 호출하지 않습니다.
GroupExpandedChanged?.Invoke(this, new PropertyGroupExpandedEventArgs(group, wasExpanded, isExpanded));
}
}
}
///
/// 그룹의 펼침/접힘 상태를 토글합니다.
///
/// 그룹 ID
public void ToggleGroupExpanded(string groupId)
{
if (_groupIndex.TryGetValue(groupId, out var group))
{
SetGroupExpanded(groupId, !group.IsExpanded);
}
}
#endregion
#region Property Management
///
/// 개별 속성 아이템을 추가합니다 (그룹 없이).
///
/// 추가할 속성 아이템
public void AddProperty(IPropertyItem item)
{
if (item == null)
throw new ArgumentNullException(nameof(item));
item.GroupId = null;
AddEntryInternal(item);
EntryAdded?.Invoke(this, new PropertyEntryEventArgs(item));
Refresh();
}
///
/// 특정 그룹에 속성 아이템을 추가합니다.
/// 그룹이 없으면 새로 생성합니다.
///
/// 그룹 ID
/// 추가할 속성 아이템
/// 그룹이 새로 생성될 경우 사용할 이름 (null이면 groupId 사용)
public void AddPropertyToGroup(string groupId, IPropertyItem item, string? groupNameIfNew = null)
{
if (string.IsNullOrEmpty(groupId))
throw new ArgumentNullException(nameof(groupId));
if (item == null)
throw new ArgumentNullException(nameof(item));
if (!_groupIndex.TryGetValue(groupId, out var group))
{
// 그룹이 없으면 새로 생성
group = new PropertyGroup(groupId, groupNameIfNew ?? groupId);
AddEntryInternal(group);
GroupAdded?.Invoke(this, new PropertyGroupEventArgs(group));
}
group.AddItem(item);
_itemIndex[item.Id] = item;
Refresh();
}
///
/// 여러 속성 아이템을 한번에 그룹에 추가합니다.
///
/// 그룹 ID
/// 추가할 속성 아이템들
/// 그룹이 새로 생성될 경우 사용할 이름
public void AddPropertiesToGroup(string groupId, IEnumerable items, string? groupNameIfNew = null)
{
if (string.IsNullOrEmpty(groupId))
throw new ArgumentNullException(nameof(groupId));
if (items == null)
throw new ArgumentNullException(nameof(items));
if (!_groupIndex.TryGetValue(groupId, out var group))
{
group = new PropertyGroup(groupId, groupNameIfNew ?? groupId);
AddEntryInternal(group);
GroupAdded?.Invoke(this, new PropertyGroupEventArgs(group));
}
foreach (var item in items)
{
group.AddItem(item);
_itemIndex[item.Id] = item;
}
Refresh();
}
///
/// 속성 아이템을 그룹에서 제거하고 독립 아이템으로 변경합니다.
///
/// 아이템 ID
public void UngroupProperty(string itemId)
{
if (_itemIndex.TryGetValue(itemId, out var item) && item.GroupId != null)
{
if (_groupIndex.TryGetValue(item.GroupId, out var group))
{
group.RemoveItem(itemId);
}
item.GroupId = null;
AddEntryInternal(item); // 독립 엔트리로 추가
Refresh();
}
}
///
/// 속성 아이템을 제거합니다.
///
/// 제거할 아이템 ID
public void RemoveProperty(string itemId)
{
if (_itemIndex.TryGetValue(itemId, out var item))
{
if (item.GroupId != null && _groupIndex.TryGetValue(item.GroupId, out var group))
{
group.RemoveItem(itemId);
}
else
{
_entries.Remove(item);
}
_itemIndex.Remove(itemId);
EntryRemoved?.Invoke(this, new PropertyEntryEventArgs(item));
Refresh();
}
}
///
/// 특정 ID의 속성 아이템을 가져옵니다.
///
/// 아이템 ID
/// 속성 아이템 또는 null
public IPropertyItem? GetProperty(string itemId)
{
_itemIndex.TryGetValue(itemId, out var item);
return item;
}
#endregion
#region Clear and Refresh
///
/// 모든 엔트리를 제거합니다.
///
public void Clear()
{
foreach (var group in _groupIndex.Values)
{
group.Clear();
}
_entries.Clear();
_groupIndex.Clear();
_itemIndex.Clear();
EntriesCleared?.Invoke(this, EventArgs.Empty);
}
///
/// View를 갱신합니다.
///
public void Refresh()
{
if (_view != null)
{
_view.Initialize(this);
}
}
#endregion
#region Value Update
///
/// 특정 ID를 가진 속성의 값을 업데이트합니다.
/// 이 메서드는 주로 View에서 사용자 입력이 발생했을 때 호출됩니다.
///
/// 값을 변경할 속성의 고유 ID
/// 속성의 타입
/// 새로운 값
public void UpdatePropertyValue(string propertyId, PropertyType propertyType, object newValue)
{
if (!_itemIndex.TryGetValue(propertyId, out var propertyItem))
{
Debug.LogError($"[PropertyWindow] ID '{propertyId}'에 해당하는 속성을 찾을 수 없습니다.");
return;
}
object? oldValue = propertyItem.GetValue();
// 값 타입일 때 새 값과 이전 값이 같은지 확인합니다.
if (oldValue != null && oldValue.GetType().IsValueType && Equals(oldValue, newValue))
{
return;
}
propertyItem.SetValue(newValue);
OnPropertyValueChanged(propertyId, propertyType, oldValue!, newValue);
}
///
/// PropertyValueChanged 이벤트를 안전하게 발생시키는 보호된 가상 메서드입니다.
///
protected virtual void OnPropertyValueChanged(string propertyId, PropertyType propertyType, object oldValue, object newValue)
{
PropertyValueChanged?.Invoke(this, new PropertyValueChangedEventArgs(propertyId, propertyType, oldValue, newValue));
}
#endregion
#region Internal Helpers
///
/// 엔트리를 내부 컬렉션에 추가합니다.
///
private void AddEntryInternal(IPropertyEntry entry)
{
if (entry is IPropertyGroup group)
{
if (!_groupIndex.ContainsKey(group.GroupId))
{
_groupIndex[group.GroupId] = group;
_entries.Add(group);
// 그룹 내 아이템들도 인덱스에 추가
foreach (var item in group.Items)
{
_itemIndex[item.Id] = item;
}
}
}
else if (entry is IPropertyItem item)
{
if (!_itemIndex.ContainsKey(item.Id))
{
_itemIndex[item.Id] = item;
if (item.GroupId == null)
{
_entries.Add(item);
}
}
}
}
#endregion
#region Visibility
public bool IsVisible => gameObject.activeSelf;
public void Show()
{
gameObject.SetActive(true);
}
public void Hide()
{
gameObject.SetActive(false);
}
public void ToggleVisibility()
{
gameObject.SetActive(!gameObject.activeSelf);
}
public void SetVisibility(bool visible)
{
gameObject.SetActive(visible);
}
#endregion
#region Property/Group Visibility
///
/// 특정 속성 아이템의 가시성을 설정합니다.
///
/// 속성 아이템의 ID
/// 가시성 여부
public void SetPropertyVisibility(string propertyId, bool visible)
{
if (_view != null)
{
_view.SetPropertyVisibility(propertyId, visible);
}
}
///
/// 여러 속성 아이템의 가시성을 한번에 설정합니다.
///
/// 속성 아이템 ID 목록
/// 가시성 여부
public void SetPropertiesVisibility(IEnumerable propertyIds, bool visible)
{
if (_view != null && propertyIds != null)
{
foreach (var propertyId in propertyIds)
{
_view.SetPropertyVisibility(propertyId, visible);
}
}
}
///
/// 특정 그룹의 가시성을 설정합니다.
///
/// 그룹 ID
/// 가시성 여부
public void SetGroupVisibility(string groupId, bool visible)
{
if (_view != null)
{
_view.SetGroupVisibility(groupId, visible);
}
}
///
/// 여러 그룹의 가시성을 한번에 설정합니다.
///
/// 그룹 ID 목록
/// 가시성 여부
public void SetGroupsVisibility(IEnumerable groupIds, bool visible)
{
if (_view != null && groupIds != null)
{
foreach (var groupId in groupIds)
{
_view.SetGroupVisibility(groupId, visible);
}
}
}
#endregion
#region Property/Group ReadOnly (Enable/Disable)
///
/// 특정 속성 아이템의 읽기 전용 상태를 설정합니다.
///
/// 속성 아이템의 ID
/// 읽기 전용 여부 (true: 비활성화, false: 활성화)
public void SetPropertyReadOnly(string propertyId, bool isReadOnly)
{
if (_itemIndex.TryGetValue(propertyId, out var item))
{
item.IsReadOnly = isReadOnly;
if (_view != null)
{
_view.SetPropertyReadOnly(propertyId, isReadOnly);
}
}
}
///
/// 특정 속성 아이템의 활성화 상태를 설정합니다.
/// SetPropertyReadOnly의 반대 동작입니다.
///
/// 속성 아이템의 ID
/// 활성화 여부 (true: 활성화, false: 비활성화)
public void SetPropertyEnabled(string propertyId, bool enabled)
{
SetPropertyReadOnly(propertyId, !enabled);
}
///
/// 여러 속성 아이템의 읽기 전용 상태를 한번에 설정합니다.
///
/// 속성 아이템 ID 목록
/// 읽기 전용 여부
public void SetPropertiesReadOnly(IEnumerable propertyIds, bool isReadOnly)
{
if (propertyIds != null)
{
foreach (var propertyId in propertyIds)
{
SetPropertyReadOnly(propertyId, isReadOnly);
}
}
}
///
/// 여러 속성 아이템의 활성화 상태를 한번에 설정합니다.
///
/// 속성 아이템 ID 목록
/// 활성화 여부
public void SetPropertiesEnabled(IEnumerable propertyIds, bool enabled)
{
SetPropertiesReadOnly(propertyIds, !enabled);
}
///
/// 특정 그룹 내 모든 속성 아이템의 읽기 전용 상태를 설정합니다.
///
/// 그룹 ID
/// 읽기 전용 여부
public void SetGroupReadOnly(string groupId, bool isReadOnly)
{
if (_groupIndex.TryGetValue(groupId, out var group))
{
foreach (var item in group.Items)
{
item.IsReadOnly = isReadOnly;
if (_view != null)
{
_view.SetPropertyReadOnly(item.Id, isReadOnly);
}
}
}
}
///
/// 특정 그룹 내 모든 속성 아이템의 활성화 상태를 설정합니다.
/// SetGroupReadOnly의 반대 동작입니다.
///
/// 그룹 ID
/// 활성화 여부
public void SetGroupEnabled(string groupId, bool enabled)
{
SetGroupReadOnly(groupId, !enabled);
}
///
/// 여러 그룹 내 모든 속성 아이템의 읽기 전용 상태를 한번에 설정합니다.
///
/// 그룹 ID 목록
/// 읽기 전용 여부
public void SetGroupsReadOnly(IEnumerable groupIds, bool isReadOnly)
{
if (groupIds != null)
{
foreach (var groupId in groupIds)
{
SetGroupReadOnly(groupId, isReadOnly);
}
}
}
///
/// 여러 그룹 내 모든 속성 아이템의 활성화 상태를 한번에 설정합니다.
///
/// 그룹 ID 목록
/// 활성화 여부
public void SetGroupsEnabled(IEnumerable groupIds, bool enabled)
{
SetGroupsReadOnly(groupIds, !enabled);
}
#endregion
}
}