#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 } }