라이브러리 정리
This commit is contained in:
8
Assets/Scripts/UVC/UI/Window/PropertyWindow/Core.meta
Normal file
8
Assets/Scripts/UVC/UI/Window/PropertyWindow/Core.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ec534859551f9c429fd3b50b8a1cf47
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace UVC.UI.Window.PropertyWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// PropertyWindow에 추가할 수 있는 항목의 통합 인터페이스입니다.
|
||||
/// 그룹(IPropertyGroup) 또는 개별 아이템(IPropertyItem) 모두 이 타입으로 처리됩니다.
|
||||
/// </summary>
|
||||
public interface IPropertyEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 렌더링 순서를 결정하는 값입니다.
|
||||
/// 낮은 값이 먼저 표시됩니다.
|
||||
/// </summary>
|
||||
int Order { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86e2cde5cc1d5cb4d88c1937d95bae42
|
||||
@@ -0,0 +1,61 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UVC.UI.Window.PropertyWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// 속성 아이템들을 그룹으로 묶어서 관리하는 인터페이스입니다.
|
||||
/// Unity Inspector 스타일의 접이식 그룹(Foldout Group)을 지원합니다.
|
||||
/// </summary>
|
||||
public interface IPropertyGroup : IPropertyEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 그룹의 고유 식별자
|
||||
/// </summary>
|
||||
string GroupId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// UI에 표시될 그룹 이름
|
||||
/// </summary>
|
||||
string GroupName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 그룹의 접힘/펼침 상태
|
||||
/// true: 펼쳐진 상태, false: 접힌 상태
|
||||
/// </summary>
|
||||
bool IsExpanded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 그룹에 포함된 속성 아이템들의 읽기 전용 목록
|
||||
/// </summary>
|
||||
IReadOnlyList<IPropertyItem> Items { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 그룹에 속성 아이템을 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="item">추가할 속성 아이템</param>
|
||||
void AddItem(IPropertyItem item);
|
||||
|
||||
/// <summary>
|
||||
/// 그룹에 여러 속성 아이템을 한번에 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="items">추가할 속성 아이템들</param>
|
||||
void AddItems(IEnumerable<IPropertyItem> items);
|
||||
|
||||
/// <summary>
|
||||
/// 그룹에서 특정 ID의 속성 아이템을 제거합니다.
|
||||
/// </summary>
|
||||
/// <param name="itemId">제거할 아이템의 ID</param>
|
||||
/// <returns>제거 성공 여부</returns>
|
||||
bool RemoveItem(string itemId);
|
||||
|
||||
/// <summary>
|
||||
/// 그룹의 모든 속성 아이템을 제거합니다.
|
||||
/// </summary>
|
||||
void Clear();
|
||||
|
||||
/// <summary>
|
||||
/// 그룹에 포함된 아이템 수를 반환합니다.
|
||||
/// </summary>
|
||||
int Count { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d407d231edf6e3f4cbe8630fc9651bcc
|
||||
@@ -0,0 +1,169 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace UVC.UI.Window.PropertyWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// IPropertyGroup 인터페이스의 기본 구현 클래스입니다.
|
||||
/// 속성 아이템들을 그룹으로 묶어서 관리합니다.
|
||||
/// </summary>
|
||||
public class PropertyGroup : IPropertyGroup
|
||||
{
|
||||
private readonly List<IPropertyItem> _items = new List<IPropertyItem>();
|
||||
|
||||
/// <summary>
|
||||
/// 그룹의 고유 식별자
|
||||
/// </summary>
|
||||
public string GroupId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// UI에 표시될 그룹 이름
|
||||
/// </summary>
|
||||
public string GroupName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 그룹의 접힘/펼침 상태
|
||||
/// </summary>
|
||||
public bool IsExpanded { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 렌더링 순서
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 그룹에 포함된 속성 아이템들의 읽기 전용 목록
|
||||
/// </summary>
|
||||
public IReadOnlyList<IPropertyItem> Items => _items.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// 그룹에 포함된 아이템 수
|
||||
/// </summary>
|
||||
public int Count => _items.Count;
|
||||
|
||||
/// <summary>
|
||||
/// 아이템이 추가되었을 때 발생하는 이벤트
|
||||
/// </summary>
|
||||
public event EventHandler<PropertyGroupItemEventArgs> ItemAdded;
|
||||
|
||||
/// <summary>
|
||||
/// 아이템이 제거되었을 때 발생하는 이벤트
|
||||
/// </summary>
|
||||
public event EventHandler<PropertyGroupItemEventArgs> ItemRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// 그룹이 비워졌을 때 발생하는 이벤트
|
||||
/// </summary>
|
||||
public event EventHandler Cleared;
|
||||
|
||||
/// <summary>
|
||||
/// PropertyGroup을 생성합니다.
|
||||
/// </summary>
|
||||
/// <param name="groupId">그룹 고유 ID</param>
|
||||
/// <param name="groupName">그룹 표시명</param>
|
||||
/// <param name="order">렌더링 순서 (기본값: 0)</param>
|
||||
/// <param name="isExpanded">초기 펼침 상태 (기본값: true)</param>
|
||||
public PropertyGroup(string groupId, string groupName, int order = 0, bool isExpanded = true)
|
||||
{
|
||||
GroupId = groupId ?? throw new ArgumentNullException(nameof(groupId));
|
||||
GroupName = groupName ?? throw new ArgumentNullException(nameof(groupName));
|
||||
Order = order;
|
||||
IsExpanded = isExpanded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 그룹에 속성 아이템을 추가합니다.
|
||||
/// </summary>
|
||||
public void AddItem(IPropertyItem item)
|
||||
{
|
||||
if (item == null)
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
|
||||
// 중복 ID 체크
|
||||
if (_items.Any(i => i.Id == item.Id))
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"[PropertyGroup] 이미 존재하는 아이템 ID입니다: {item.Id}");
|
||||
return;
|
||||
}
|
||||
|
||||
item.GroupId = GroupId;
|
||||
_items.Add(item);
|
||||
ItemAdded?.Invoke(this, new PropertyGroupItemEventArgs(GroupId, item));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 그룹에 여러 속성 아이템을 한번에 추가합니다.
|
||||
/// </summary>
|
||||
public void AddItems(IEnumerable<IPropertyItem> items)
|
||||
{
|
||||
if (items == null)
|
||||
throw new ArgumentNullException(nameof(items));
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
AddItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 그룹에서 특정 ID의 속성 아이템을 제거합니다.
|
||||
/// </summary>
|
||||
public bool RemoveItem(string itemId)
|
||||
{
|
||||
var item = _items.FirstOrDefault(i => i.Id == itemId);
|
||||
if (item != null)
|
||||
{
|
||||
item.GroupId = null;
|
||||
_items.Remove(item);
|
||||
ItemRemoved?.Invoke(this, new PropertyGroupItemEventArgs(GroupId, item));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 그룹의 모든 속성 아이템을 제거합니다.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var item in _items)
|
||||
{
|
||||
item.GroupId = null;
|
||||
}
|
||||
_items.Clear();
|
||||
Cleared?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 ID의 아이템을 가져옵니다.
|
||||
/// </summary>
|
||||
public IPropertyItem GetItem(string itemId)
|
||||
{
|
||||
return _items.FirstOrDefault(i => i.Id == itemId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 ID의 아이템이 존재하는지 확인합니다.
|
||||
/// </summary>
|
||||
public bool ContainsItem(string itemId)
|
||||
{
|
||||
return _items.Any(i => i.Id == itemId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PropertyGroup 아이템 관련 이벤트 인자
|
||||
/// </summary>
|
||||
public class PropertyGroupItemEventArgs : EventArgs
|
||||
{
|
||||
public string GroupId { get; }
|
||||
public IPropertyItem Item { get; }
|
||||
|
||||
public PropertyGroupItemEventArgs(string groupId, IPropertyItem item)
|
||||
{
|
||||
GroupId = groupId;
|
||||
Item = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a8f10eca2b388e4bbfa893f415783da
|
||||
8
Assets/Scripts/UVC/UI/Window/PropertyWindow/Events.meta
Normal file
8
Assets/Scripts/UVC/UI/Window/PropertyWindow/Events.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6732ae9ae5e1c7d4cb457b92ab4efdb6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
|
||||
namespace UVC.UI.Window.PropertyWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// 그룹 추가/제거 이벤트 인자
|
||||
/// </summary>
|
||||
public class PropertyGroupEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 이벤트 대상 그룹
|
||||
/// </summary>
|
||||
public IPropertyGroup Group { get; }
|
||||
|
||||
public PropertyGroupEventArgs(IPropertyGroup group)
|
||||
{
|
||||
Group = group;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 그룹 펼침/접힘 상태 변경 이벤트 인자
|
||||
/// </summary>
|
||||
public class PropertyGroupExpandedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 이벤트 대상 그룹
|
||||
/// </summary>
|
||||
public IPropertyGroup Group { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 변경 전 펼침 상태
|
||||
/// </summary>
|
||||
public bool WasExpanded { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 변경 후 펼침 상태
|
||||
/// </summary>
|
||||
public bool IsExpanded { get; }
|
||||
|
||||
public PropertyGroupExpandedEventArgs(IPropertyGroup group, bool wasExpanded, bool isExpanded)
|
||||
{
|
||||
Group = group;
|
||||
WasExpanded = wasExpanded;
|
||||
IsExpanded = isExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 엔트리(그룹 또는 아이템) 추가/제거 이벤트 인자
|
||||
/// </summary>
|
||||
public class PropertyEntryEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 이벤트 대상 엔트리
|
||||
/// </summary>
|
||||
public IPropertyEntry Entry { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 엔트리가 그룹인지 여부
|
||||
/// </summary>
|
||||
public bool IsGroup => Entry is IPropertyGroup;
|
||||
|
||||
/// <summary>
|
||||
/// 엔트리가 아이템인지 여부
|
||||
/// </summary>
|
||||
public bool IsItem => Entry is IPropertyItem;
|
||||
|
||||
public PropertyEntryEventArgs(IPropertyEntry entry)
|
||||
{
|
||||
Entry = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1e0750845e63f443b44c64062b7015b
|
||||
8
Assets/Scripts/UVC/UI/Window/PropertyWindow/Pool.meta
Normal file
8
Assets/Scripts/UVC/UI/Window/PropertyWindow/Pool.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9f56f3d625d0d543b0ff282a1a9f35a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,353 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UVC.UI.Window.PropertyWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// PropertyUI 오브젝트들을 풀링하여 재사용하는 시스템입니다.
|
||||
/// 매번 Instantiate/Destroy 대신 풀에서 가져오고 반환하여 성능을 향상시킵니다.
|
||||
/// </summary>
|
||||
public class PropertyUIPool : MonoBehaviour
|
||||
{
|
||||
[Header("Pool Settings")]
|
||||
[SerializeField] private int _defaultPoolSize = 5;
|
||||
[SerializeField] private Transform _poolContainer;
|
||||
|
||||
[Header("Prefabs")]
|
||||
[SerializeField] private GameObject _groupPrefab;
|
||||
[SerializeField] private GameObject _stringPropertyPrefab;
|
||||
[SerializeField] private GameObject _numberPropertyPrefab;
|
||||
[SerializeField] private GameObject _boolPropertyPrefab;
|
||||
[SerializeField] private GameObject _vector2PropertyPrefab;
|
||||
[SerializeField] private GameObject _vector3PropertyPrefab;
|
||||
[SerializeField] private GameObject _colorPropertyPrefab;
|
||||
[SerializeField] private GameObject _datePropertyPrefab;
|
||||
[SerializeField] private GameObject _dateTimePropertyPrefab;
|
||||
[SerializeField] private GameObject _enumPropertyPrefab;
|
||||
[SerializeField] private GameObject _listPropertyPrefab;
|
||||
[SerializeField] private GameObject _radioGroupPropertyPrefab;
|
||||
[SerializeField] private GameObject _numberRangePropertyPrefab;
|
||||
[SerializeField] private GameObject _dateRangePropertyPrefab;
|
||||
[SerializeField] private GameObject _dateTimeRangePropertyPrefab;
|
||||
|
||||
/// <summary>
|
||||
/// PropertyType별 UI 오브젝트 풀
|
||||
/// </summary>
|
||||
private readonly Dictionary<PropertyType, Queue<GameObject>> _itemPools = new Dictionary<PropertyType, Queue<GameObject>>();
|
||||
|
||||
/// <summary>
|
||||
/// 그룹 UI 오브젝트 풀
|
||||
/// </summary>
|
||||
private readonly Queue<PropertyGroupView> _groupPool = new Queue<PropertyGroupView>();
|
||||
|
||||
/// <summary>
|
||||
/// PropertyType별 프리팹 매핑
|
||||
/// </summary>
|
||||
private Dictionary<PropertyType, GameObject> _prefabMap;
|
||||
|
||||
private bool _isInitialized = false;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 풀을 초기화합니다.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
if (_isInitialized) return;
|
||||
|
||||
// 풀 컨테이너 생성 (없으면)
|
||||
if (_poolContainer == null)
|
||||
{
|
||||
var containerObj = new GameObject("PropertyUIPool_Container");
|
||||
containerObj.transform.SetParent(transform);
|
||||
containerObj.SetActive(false);
|
||||
_poolContainer = containerObj.transform;
|
||||
}
|
||||
|
||||
// 프리팹 맵 초기화
|
||||
InitializePrefabMap();
|
||||
|
||||
// 각 타입별 풀 초기화
|
||||
foreach (PropertyType type in System.Enum.GetValues(typeof(PropertyType)))
|
||||
{
|
||||
if (!_itemPools.ContainsKey(type))
|
||||
{
|
||||
_itemPools[type] = new Queue<GameObject>();
|
||||
}
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PropertyType별 프리팹 매핑을 초기화합니다.
|
||||
/// </summary>
|
||||
private void InitializePrefabMap()
|
||||
{
|
||||
_prefabMap = new Dictionary<PropertyType, GameObject>
|
||||
{
|
||||
{ PropertyType.String, _stringPropertyPrefab },
|
||||
{ PropertyType.Int, _numberPropertyPrefab },
|
||||
{ PropertyType.Float, _numberPropertyPrefab },
|
||||
{ PropertyType.Bool, _boolPropertyPrefab },
|
||||
{ PropertyType.Vector2, _vector2PropertyPrefab },
|
||||
{ PropertyType.Vector3, _vector3PropertyPrefab },
|
||||
{ PropertyType.Color, _colorPropertyPrefab },
|
||||
{ PropertyType.Date, _datePropertyPrefab },
|
||||
{ PropertyType.DateTime, _dateTimePropertyPrefab },
|
||||
{ PropertyType.Enum, _enumPropertyPrefab },
|
||||
{ PropertyType.DropdownList, _listPropertyPrefab },
|
||||
{ PropertyType.RadioGroup, _radioGroupPropertyPrefab },
|
||||
{ PropertyType.IntRange, _numberRangePropertyPrefab },
|
||||
{ PropertyType.FloatRange, _numberRangePropertyPrefab },
|
||||
{ PropertyType.DateRange, _dateRangePropertyPrefab },
|
||||
{ PropertyType.DateTimeRange, _dateTimeRangePropertyPrefab }
|
||||
};
|
||||
}
|
||||
|
||||
#region Item Pool Methods
|
||||
|
||||
/// <summary>
|
||||
/// 풀에서 PropertyUI 오브젝트를 가져옵니다.
|
||||
/// 풀이 비어있으면 새로 생성합니다.
|
||||
/// </summary>
|
||||
/// <param name="type">속성 타입</param>
|
||||
/// <returns>UI GameObject 또는 프리팹이 없으면 null</returns>
|
||||
public GameObject GetItemUI(PropertyType type)
|
||||
{
|
||||
if (!_isInitialized) Initialize();
|
||||
|
||||
// 풀에서 가져오기 시도
|
||||
if (_itemPools.TryGetValue(type, out var pool) && pool.Count > 0)
|
||||
{
|
||||
var obj = pool.Dequeue();
|
||||
obj.SetActive(true);
|
||||
return obj;
|
||||
}
|
||||
|
||||
// 풀이 비어있으면 새로 생성
|
||||
return CreateItemUI(type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PropertyUI 오브젝트를 풀에 반환합니다.
|
||||
/// </summary>
|
||||
/// <param name="type">속성 타입</param>
|
||||
/// <param name="obj">반환할 오브젝트</param>
|
||||
public void ReturnItemUI(PropertyType type, GameObject obj)
|
||||
{
|
||||
if (obj == null) return;
|
||||
|
||||
// UI 컴포넌트 정리
|
||||
var propertyUI = obj.GetComponent<IPropertyUI>();
|
||||
if (propertyUI is IPoolable poolable)
|
||||
{
|
||||
poolable.OnReturnToPool();
|
||||
}
|
||||
|
||||
obj.SetActive(false);
|
||||
obj.transform.SetParent(_poolContainer, false);
|
||||
|
||||
if (_itemPools.TryGetValue(type, out var pool))
|
||||
{
|
||||
pool.Enqueue(obj);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 새로운 PropertyUI 오브젝트를 생성합니다.
|
||||
/// </summary>
|
||||
private GameObject CreateItemUI(PropertyType type)
|
||||
{
|
||||
if (_prefabMap == null || !_prefabMap.TryGetValue(type, out var prefab) || prefab == null)
|
||||
{
|
||||
Debug.LogWarning($"[PropertyUIPool] '{type}' 타입에 대한 프리팹이 설정되지 않았습니다.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var obj = Instantiate(prefab);
|
||||
return obj;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Group Pool Methods
|
||||
|
||||
/// <summary>
|
||||
/// 풀에서 PropertyGroupView를 가져옵니다.
|
||||
/// </summary>
|
||||
/// <returns>PropertyGroupView 또는 프리팹이 없으면 null</returns>
|
||||
public PropertyGroupView GetGroupUI()
|
||||
{
|
||||
if (!_isInitialized) Initialize();
|
||||
|
||||
// 풀에서 가져오기 시도
|
||||
if (_groupPool.Count > 0)
|
||||
{
|
||||
var groupView = _groupPool.Dequeue();
|
||||
groupView.gameObject.SetActive(true);
|
||||
return groupView;
|
||||
}
|
||||
|
||||
// 풀이 비어있으면 새로 생성
|
||||
return CreateGroupUI();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PropertyGroupView를 풀에 반환합니다.
|
||||
/// </summary>
|
||||
/// <param name="groupView">반환할 그룹 뷰</param>
|
||||
public void ReturnGroupUI(PropertyGroupView groupView)
|
||||
{
|
||||
if (groupView == null) return;
|
||||
|
||||
groupView.Reset();
|
||||
groupView.gameObject.SetActive(false);
|
||||
groupView.transform.SetParent(_poolContainer, false);
|
||||
_groupPool.Enqueue(groupView);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 새로운 PropertyGroupView를 생성합니다.
|
||||
/// </summary>
|
||||
private PropertyGroupView CreateGroupUI()
|
||||
{
|
||||
if (_groupPrefab == null)
|
||||
{
|
||||
Debug.LogWarning("[PropertyUIPool] 그룹 프리팹이 설정되지 않았습니다.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var obj = Instantiate(_groupPrefab);
|
||||
return obj.GetComponent<PropertyGroupView>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Pool Management
|
||||
|
||||
/// <summary>
|
||||
/// 특정 타입의 UI를 미리 생성하여 풀에 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="type">속성 타입</param>
|
||||
/// <param name="count">미리 생성할 개수</param>
|
||||
public void Prewarm(PropertyType type, int count)
|
||||
{
|
||||
if (!_isInitialized) Initialize();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var obj = CreateItemUI(type);
|
||||
if (obj != null)
|
||||
{
|
||||
ReturnItemUI(type, obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 그룹 UI를 미리 생성하여 풀에 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="count">미리 생성할 개수</param>
|
||||
public void PrewarmGroups(int count)
|
||||
{
|
||||
if (!_isInitialized) Initialize();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var groupView = CreateGroupUI();
|
||||
if (groupView != null)
|
||||
{
|
||||
ReturnGroupUI(groupView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 풀을 비우고 오브젝트를 파괴합니다.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
// 아이템 풀 정리
|
||||
foreach (var pool in _itemPools.Values)
|
||||
{
|
||||
while (pool.Count > 0)
|
||||
{
|
||||
var obj = pool.Dequeue();
|
||||
if (obj != null)
|
||||
{
|
||||
Destroy(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
_itemPools.Clear();
|
||||
|
||||
// 그룹 풀 정리
|
||||
while (_groupPool.Count > 0)
|
||||
{
|
||||
var groupView = _groupPool.Dequeue();
|
||||
if (groupView != null)
|
||||
{
|
||||
Destroy(groupView.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
// 풀 컨테이너의 모든 자식 삭제
|
||||
if (_poolContainer != null)
|
||||
{
|
||||
foreach (Transform child in _poolContainer)
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 풀 상태를 반환합니다 (디버그용).
|
||||
/// </summary>
|
||||
public string GetPoolStatus()
|
||||
{
|
||||
var status = new System.Text.StringBuilder();
|
||||
status.AppendLine("[PropertyUIPool Status]");
|
||||
status.AppendLine($"Groups in pool: {_groupPool.Count}");
|
||||
|
||||
foreach (var kvp in _itemPools)
|
||||
{
|
||||
if (kvp.Value.Count > 0)
|
||||
{
|
||||
status.AppendLine($"{kvp.Key}: {kvp.Value.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
return status.ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 풀링 가능한 UI 컴포넌트가 구현해야 하는 인터페이스입니다.
|
||||
/// </summary>
|
||||
public interface IPoolable
|
||||
{
|
||||
/// <summary>
|
||||
/// 풀에서 가져올 때 호출됩니다.
|
||||
/// </summary>
|
||||
void OnGetFromPool();
|
||||
|
||||
/// <summary>
|
||||
/// 풀에 반환될 때 호출됩니다.
|
||||
/// 이벤트 해제 및 상태 초기화를 수행해야 합니다.
|
||||
/// </summary>
|
||||
void OnReturnToPool();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a9acf4264efbf44e957f7c7870280dc
|
||||
@@ -65,8 +65,9 @@ namespace UVC.UI.Window.PropertyWindow
|
||||
|
||||
/// <summary>
|
||||
/// 모든 속성 항목이 구현해야 하는 기본 인터페이스입니다.
|
||||
/// IPropertyEntry를 상속하여 그룹과 혼용하여 사용할 수 있습니다.
|
||||
/// </summary>
|
||||
public interface IPropertyItem
|
||||
public interface IPropertyItem : IPropertyEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 속성의 고유 식별자 (필수)
|
||||
@@ -108,6 +109,12 @@ namespace UVC.UI.Window.PropertyWindow
|
||||
/// 속성의 데이터 타입
|
||||
/// </summary>
|
||||
PropertyType PropertyType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 아이템이 속한 그룹의 ID입니다.
|
||||
/// null이면 그룹에 속하지 않은 독립 아이템입니다.
|
||||
/// </summary>
|
||||
string GroupId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -124,6 +131,16 @@ namespace UVC.UI.Window.PropertyWindow
|
||||
public bool IsReadOnly { get; set; } = false;
|
||||
public abstract PropertyType PropertyType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 렌더링 순서를 결정하는 값입니다. (IPropertyEntry 구현)
|
||||
/// </summary>
|
||||
public int Order { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 아이템이 속한 그룹의 ID입니다. null이면 그룹에 속하지 않습니다.
|
||||
/// </summary>
|
||||
public string GroupId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 실제 데이터가 저장되는 필드
|
||||
/// </summary>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UVC.UI.Window.PropertyWindow
|
||||
@@ -5,19 +7,19 @@ namespace UVC.UI.Window.PropertyWindow
|
||||
/// <summary>
|
||||
/// 속성창의 UI를 담당하는 View 클래스입니다.
|
||||
/// Controller로부터 데이터를 받아와 동적으로 UI 요소들을 생성하고 관리합니다.
|
||||
/// 이 클래스는 MonoBehaviour를 상속받아 Unity 씬에 배치될 수 있습니다.
|
||||
/// 그룹과 개별 아이템을 혼용하여 렌더링할 수 있습니다.
|
||||
/// </summary>
|
||||
public class PropertyView : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// UI 요소들이 생성될 부모 컨테이너입니다.
|
||||
/// Unity 에디터에서 Vertical Layout Group 컴포넌트가 추가된 Panel 등을 연결합니다.
|
||||
/// </summary>
|
||||
[Tooltip("속성 UI들이 생성될 부모 Transform (예: Vertical Layout Group이 있는 Panel)")]
|
||||
[SerializeField] private Transform _container;
|
||||
|
||||
// 각 속성 타입에 맞는 UI 프리팹들입니다.
|
||||
// 실제 프로젝트에서는 이 프리팹들을 만들고 여기에 연결해야 합니다.
|
||||
[Header("Group UI Prefab")]
|
||||
[SerializeField] private GameObject _groupPrefab;
|
||||
|
||||
[Header("Property UI Prefabs")]
|
||||
[SerializeField] private GameObject _stringPropertyPrefab;
|
||||
[SerializeField] private GameObject _numberPropertyPrefab;
|
||||
@@ -39,63 +41,216 @@ namespace UVC.UI.Window.PropertyWindow
|
||||
/// </summary>
|
||||
private PropertyWindow _controller;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 표시 중인 그룹 뷰들의 캐시
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, PropertyGroupView> _groupViews = new Dictionary<string, PropertyGroupView>();
|
||||
|
||||
/// <summary>
|
||||
/// 현재 표시 중인 아이템 UI들의 캐시 (아이템 ID -> GameObject)
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, GameObject> _itemViews = new Dictionary<string, GameObject>();
|
||||
|
||||
/// <summary>
|
||||
/// Controller를 View에 설정하고 UI를 초기화합니다.
|
||||
/// </summary>
|
||||
/// <param name="controller">사용할 PropertyWindow</param>
|
||||
public void Initialize(PropertyWindow controller)
|
||||
{
|
||||
// 기존 컨트롤러 이벤트 해제
|
||||
if (_controller != null)
|
||||
{
|
||||
_controller.PropertyValueChanged -= OnPropertyValueChanged;
|
||||
_controller.GroupExpandedChanged -= OnGroupExpandedChanged;
|
||||
}
|
||||
|
||||
_controller = controller;
|
||||
|
||||
// Controller가 null이 아니면, 이벤트 핸들러를 등록하고 UI를 그립니다.
|
||||
if (_controller != null)
|
||||
{
|
||||
_controller.PropertyValueChanged += OnPropertyValueChanged;
|
||||
DrawProperties();
|
||||
_controller.GroupExpandedChanged += OnGroupExpandedChanged;
|
||||
DrawEntries();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Controller에 있는 속성 목록을 기반으로 UI를 생성합니다.
|
||||
/// Controller에 있는 엔트리 목록을 기반으로 UI를 생성합니다.
|
||||
/// 그룹과 개별 아이템을 혼용하여 렌더링합니다.
|
||||
/// </summary>
|
||||
private void DrawProperties()
|
||||
private void DrawEntries()
|
||||
{
|
||||
// UI를 다시 그리기 전에 기존에 생성된 모든 자식 오브젝트를 삭제합니다.
|
||||
foreach (Transform child in _container)
|
||||
{
|
||||
DestroyImmediate(child.gameObject);
|
||||
}
|
||||
// 기존 UI 정리
|
||||
ClearAllViews();
|
||||
|
||||
if (_controller == null) return;
|
||||
|
||||
// 각 속성 항목에 대해 적절한 UI를 생성합니다.
|
||||
foreach (var propertyItem in _controller.Properties)
|
||||
{
|
||||
// 속성 타입에 맞는 UI 프리팹을 찾습니다.
|
||||
GameObject prefab = GetPrefabForProperty(propertyItem.PropertyType);
|
||||
if (prefab != null)
|
||||
{
|
||||
// 프리팹을 인스턴스화하여 컨테이너의 자식으로 추가합니다.
|
||||
GameObject uiInstance = Instantiate(prefab, _container);
|
||||
// Order 순으로 정렬된 엔트리들을 렌더링
|
||||
var sortedEntries = _controller.Entries;
|
||||
|
||||
// 생성된 UI 인스턴스에서 IPropertyUI 컴포넌트를 찾아 Setup을 호출합니다.
|
||||
var propertyUI = uiInstance.GetComponent<IPropertyUI>();
|
||||
if (propertyUI != null)
|
||||
foreach (var entry in sortedEntries)
|
||||
{
|
||||
if (entry is IPropertyGroup group)
|
||||
{
|
||||
DrawGroup(group);
|
||||
}
|
||||
else if (entry is IPropertyItem item && item.GroupId == null)
|
||||
{
|
||||
// 그룹에 속하지 않은 개별 아이템만 직접 렌더링
|
||||
DrawPropertyItem(item, _container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 그룹 UI를 생성합니다.
|
||||
/// </summary>
|
||||
private void DrawGroup(IPropertyGroup group)
|
||||
{
|
||||
if (_groupPrefab == null)
|
||||
{
|
||||
Debug.LogWarning("[PropertyView] 그룹 프리팹이 설정되지 않았습니다. 그룹 내 아이템만 표시합니다.");
|
||||
// 그룹 프리팹이 없으면 아이템들만 직접 렌더링
|
||||
foreach (var item in group.Items)
|
||||
{
|
||||
DrawPropertyItem(item, _container);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 그룹 UI 생성
|
||||
GameObject groupInstance = Instantiate(_groupPrefab, _container);
|
||||
var groupView = groupInstance.GetComponent<PropertyGroupView>();
|
||||
|
||||
if (groupView != null)
|
||||
{
|
||||
groupView.Setup(group, _controller);
|
||||
_groupViews[group.GroupId] = groupView;
|
||||
|
||||
// 그룹이 펼쳐진 상태면 자식 아이템들 렌더링
|
||||
if (group.IsExpanded)
|
||||
{
|
||||
foreach (var item in group.Items)
|
||||
{
|
||||
propertyUI.Setup(propertyItem, _controller);
|
||||
DrawPropertyItem(item, groupView.ItemContainer);
|
||||
}
|
||||
else
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[PropertyView] 그룹 프리팹 '{_groupPrefab.name}'에 PropertyGroupView 컴포넌트가 없습니다.");
|
||||
Destroy(groupInstance);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 개별 속성 아이템 UI를 생성합니다.
|
||||
/// </summary>
|
||||
private void DrawPropertyItem(IPropertyItem item, Transform container)
|
||||
{
|
||||
GameObject prefab = GetPrefabForProperty(item.PropertyType);
|
||||
if (prefab != null)
|
||||
{
|
||||
GameObject uiInstance = Instantiate(prefab, container);
|
||||
|
||||
var propertyUI = uiInstance.GetComponent<IPropertyUI>();
|
||||
if (propertyUI != null)
|
||||
{
|
||||
propertyUI.Setup(item, _controller);
|
||||
_itemViews[item.Id] = uiInstance;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[PropertyView] 프리팹 '{prefab.name}'에 IPropertyUI를 구현한 스크립트가 없습니다.");
|
||||
Destroy(uiInstance);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[PropertyView] '{item.PropertyType}' 타입에 대한 UI 프리팹이 지정되지 않았습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 View를 정리합니다.
|
||||
/// </summary>
|
||||
private void ClearAllViews()
|
||||
{
|
||||
// 그룹 뷰 정리
|
||||
foreach (var groupView in _groupViews.Values)
|
||||
{
|
||||
if (groupView != null)
|
||||
{
|
||||
groupView.Reset();
|
||||
Destroy(groupView.gameObject);
|
||||
}
|
||||
}
|
||||
_groupViews.Clear();
|
||||
|
||||
// 아이템 뷰 정리
|
||||
foreach (var itemView in _itemViews.Values)
|
||||
{
|
||||
if (itemView != null)
|
||||
{
|
||||
Destroy(itemView);
|
||||
}
|
||||
}
|
||||
_itemViews.Clear();
|
||||
|
||||
// 컨테이너의 모든 자식 삭제 (혹시 누락된 것이 있을 경우)
|
||||
foreach (Transform child in _container)
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 그룹 펼침/접힘 상태가 변경되었을 때 호출됩니다.
|
||||
/// </summary>
|
||||
private void OnGroupExpandedChanged(object sender, PropertyGroupExpandedEventArgs e)
|
||||
{
|
||||
if (_groupViews.TryGetValue(e.Group.GroupId, out var groupView))
|
||||
{
|
||||
groupView.UpdateExpandedState();
|
||||
|
||||
// 펼쳐진 경우 자식 아이템들 렌더링
|
||||
if (e.IsExpanded)
|
||||
{
|
||||
foreach (var item in e.Group.Items)
|
||||
{
|
||||
Debug.LogError($"[PropertyView] 프리팹 '{prefab.name}'에 IPropertyUI를 구현한 스크립트가 없습니다.");
|
||||
// 이미 존재하는 아이템은 건너뜀
|
||||
if (!_itemViews.ContainsKey(item.Id))
|
||||
{
|
||||
DrawPropertyItem(item, groupView.ItemContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[PropertyView] '{propertyItem.PropertyType}' 타입에 대한 UI 프리팹이 지정되지 않았습니다.");
|
||||
// 접힌 경우 자식 아이템들 제거
|
||||
groupView.ClearItems();
|
||||
foreach (var item in e.Group.Items)
|
||||
{
|
||||
if (_itemViews.TryGetValue(item.Id, out var itemView))
|
||||
{
|
||||
Destroy(itemView);
|
||||
_itemViews.Remove(item.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [하위 호환성] 기존 방식으로 속성 목록을 그립니다.
|
||||
/// </summary>
|
||||
[System.Obsolete("Use DrawEntries() instead. This method is kept for backward compatibility.")]
|
||||
private void DrawProperties()
|
||||
{
|
||||
DrawEntries();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 속성 타입에 맞는 UI 프리팹을 반환합니다.
|
||||
/// 실제 구현에서는 더 많은 case가 필요합니다.
|
||||
@@ -160,7 +315,12 @@ namespace UVC.UI.Window.PropertyWindow
|
||||
if (_controller != null)
|
||||
{
|
||||
_controller.PropertyValueChanged -= OnPropertyValueChanged;
|
||||
_controller.GroupExpandedChanged -= OnGroupExpandedChanged;
|
||||
}
|
||||
|
||||
// 캐시 정리
|
||||
_groupViews.Clear();
|
||||
_itemViews.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,35 +13,414 @@ namespace UVC.UI.Window.PropertyWindow
|
||||
/// <summary>
|
||||
/// 속성 데이터를 관리하고, 데이터 변경 시 이벤트를 발생시키는 컨트롤러 클래스입니다.
|
||||
/// Model과 View 사이의 중재자 역할을 합니다.
|
||||
/// 그룹과 개별 아이템을 혼용하여 사용할 수 있습니다.
|
||||
/// </summary>
|
||||
public class PropertyWindow: SingletonScene<PropertyWindow>, IPointerEnterHandler, IPointerExitHandler
|
||||
public class PropertyWindow : SingletonScene<PropertyWindow>, IPointerEnterHandler, IPointerExitHandler
|
||||
{
|
||||
|
||||
[SerializeField]
|
||||
private PropertyView _view;
|
||||
|
||||
#region Internal Data Structures
|
||||
|
||||
/// <summary>
|
||||
/// 통합 엔트리 목록 (그룹과 개별 아이템 혼합 저장)
|
||||
/// </summary>
|
||||
private readonly List<IPropertyEntry> _entries = new List<IPropertyEntry>();
|
||||
|
||||
/// <summary>
|
||||
/// 빠른 그룹 조회를 위한 인덱스
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, IPropertyGroup> _groupIndex = new Dictionary<string, IPropertyGroup>();
|
||||
|
||||
/// <summary>
|
||||
/// 빠른 아이템 조회를 위한 인덱스
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, IPropertyItem> _itemIndex = new Dictionary<string, IPropertyItem>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Properties
|
||||
|
||||
/// <summary>
|
||||
/// 현재 컨트롤러가 관리하는 모든 속성 항목의 목록입니다.
|
||||
/// 하위 호환성을 위해 유지됩니다. 그룹에 속한 아이템도 포함됩니다.
|
||||
/// </summary>
|
||||
public List<IPropertyItem> Properties { get; private set; } = new List<IPropertyItem>();
|
||||
public List<IPropertyItem> Properties
|
||||
{
|
||||
get
|
||||
{
|
||||
var allItems = new List<IPropertyItem>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 정렬된 엔트리 목록을 반환합니다.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IPropertyEntry> Entries => _entries.OrderBy(e => e.Order).ToList().AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// 모든 그룹의 읽기 전용 목록을 반환합니다.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IPropertyGroup> Groups => _groupIndex.Values.ToList().AsReadOnly();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// 속성 값이 변경될 때 발생하는 이벤트입니다.
|
||||
/// View는 이 이벤트를 구독하여 UI를 업데이트할 수 있습니다.
|
||||
/// </summary>
|
||||
public event EventHandler<PropertyValueChangedEventArgs>? PropertyValueChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 새로운 속성 목록을 로드하고 초기화합니다.
|
||||
/// 그룹이 추가되었을 때 발생하는 이벤트입니다.
|
||||
/// </summary>
|
||||
public event EventHandler<PropertyGroupEventArgs>? GroupAdded;
|
||||
|
||||
/// <summary>
|
||||
/// 그룹이 제거되었을 때 발생하는 이벤트입니다.
|
||||
/// </summary>
|
||||
public event EventHandler<PropertyGroupEventArgs>? GroupRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// 그룹의 펼침/접힘 상태가 변경되었을 때 발생하는 이벤트입니다.
|
||||
/// </summary>
|
||||
public event EventHandler<PropertyGroupExpandedEventArgs>? GroupExpandedChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 엔트리가 추가되었을 때 발생하는 이벤트입니다.
|
||||
/// </summary>
|
||||
public event EventHandler<PropertyEntryEventArgs>? EntryAdded;
|
||||
|
||||
/// <summary>
|
||||
/// 엔트리가 제거되었을 때 발생하는 이벤트입니다.
|
||||
/// </summary>
|
||||
public event EventHandler<PropertyEntryEventArgs>? EntryRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// 모든 엔트리가 제거되었을 때 발생하는 이벤트입니다.
|
||||
/// </summary>
|
||||
public event EventHandler? EntriesCleared;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Load Methods (기존 호환 + 그룹 + 혼용)
|
||||
|
||||
/// <summary>
|
||||
/// [기존 방식] 그룹 없이 속성 목록을 로드합니다.
|
||||
/// 모든 아이템이 flat하게 표시됩니다.
|
||||
/// </summary>
|
||||
/// <param name="items">표시할 속성 항목들의 목록</param>
|
||||
public void LoadProperties(List<IPropertyItem> items)
|
||||
{
|
||||
Properties = items ?? new List<IPropertyItem>();
|
||||
// 필요하다면 여기서 추가적인 초기화 로직을 수행할 수 있습니다.
|
||||
if(_view != null) _view.Initialize(this);
|
||||
Clear();
|
||||
if (items != null)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
item.GroupId = null; // 그룹 없음 명시
|
||||
AddEntryInternal(item);
|
||||
}
|
||||
}
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [그룹 방식] 그룹화된 속성 목록을 로드합니다.
|
||||
/// </summary>
|
||||
/// <param name="groups">표시할 속성 그룹들의 목록</param>
|
||||
public void LoadGroupedProperties(List<IPropertyGroup> groups)
|
||||
{
|
||||
Clear();
|
||||
if (groups != null)
|
||||
{
|
||||
foreach (var group in groups)
|
||||
{
|
||||
AddEntryInternal(group);
|
||||
}
|
||||
}
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [혼용 방식] 그룹과 개별 아이템을 함께 로드합니다.
|
||||
/// </summary>
|
||||
/// <param name="entries">표시할 엔트리들의 목록 (그룹 또는 아이템)</param>
|
||||
public void LoadMixedProperties(List<IPropertyEntry> entries)
|
||||
{
|
||||
Clear();
|
||||
if (entries != null)
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
AddEntryInternal(entry);
|
||||
}
|
||||
}
|
||||
Refresh();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Group Management
|
||||
|
||||
/// <summary>
|
||||
/// 그룹을 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="group">추가할 그룹</param>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 그룹을 제거합니다.
|
||||
/// </summary>
|
||||
/// <param name="groupId">제거할 그룹의 ID</param>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 ID의 그룹을 가져옵니다.
|
||||
/// </summary>
|
||||
/// <param name="groupId">그룹 ID</param>
|
||||
/// <returns>그룹 또는 null</returns>
|
||||
public IPropertyGroup? GetGroup(string groupId)
|
||||
{
|
||||
_groupIndex.TryGetValue(groupId, out var group);
|
||||
return group;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 그룹의 펼침/접힘 상태를 변경합니다.
|
||||
/// </summary>
|
||||
/// <param name="groupId">그룹 ID</param>
|
||||
/// <param name="isExpanded">펼침 상태</param>
|
||||
public void SetGroupExpanded(string groupId, bool isExpanded)
|
||||
{
|
||||
if (_groupIndex.TryGetValue(groupId, out var group))
|
||||
{
|
||||
bool wasExpanded = group.IsExpanded;
|
||||
if (wasExpanded != isExpanded)
|
||||
{
|
||||
group.IsExpanded = isExpanded;
|
||||
GroupExpandedChanged?.Invoke(this, new PropertyGroupExpandedEventArgs(group, wasExpanded, isExpanded));
|
||||
Refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 그룹의 펼침/접힘 상태를 토글합니다.
|
||||
/// </summary>
|
||||
/// <param name="groupId">그룹 ID</param>
|
||||
public void ToggleGroupExpanded(string groupId)
|
||||
{
|
||||
if (_groupIndex.TryGetValue(groupId, out var group))
|
||||
{
|
||||
SetGroupExpanded(groupId, !group.IsExpanded);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Management
|
||||
|
||||
/// <summary>
|
||||
/// 개별 속성 아이템을 추가합니다 (그룹 없이).
|
||||
/// </summary>
|
||||
/// <param name="item">추가할 속성 아이템</param>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 그룹에 속성 아이템을 추가합니다.
|
||||
/// 그룹이 없으면 새로 생성합니다.
|
||||
/// </summary>
|
||||
/// <param name="groupId">그룹 ID</param>
|
||||
/// <param name="item">추가할 속성 아이템</param>
|
||||
/// <param name="groupNameIfNew">그룹이 새로 생성될 경우 사용할 이름 (null이면 groupId 사용)</param>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 여러 속성 아이템을 한번에 그룹에 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="groupId">그룹 ID</param>
|
||||
/// <param name="items">추가할 속성 아이템들</param>
|
||||
/// <param name="groupNameIfNew">그룹이 새로 생성될 경우 사용할 이름</param>
|
||||
public void AddPropertiesToGroup(string groupId, IEnumerable<IPropertyItem> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 속성 아이템을 그룹에서 제거하고 독립 아이템으로 변경합니다.
|
||||
/// </summary>
|
||||
/// <param name="itemId">아이템 ID</param>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 속성 아이템을 제거합니다.
|
||||
/// </summary>
|
||||
/// <param name="itemId">제거할 아이템 ID</param>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 ID의 속성 아이템을 가져옵니다.
|
||||
/// </summary>
|
||||
/// <param name="itemId">아이템 ID</param>
|
||||
/// <returns>속성 아이템 또는 null</returns>
|
||||
public IPropertyItem? GetProperty(string itemId)
|
||||
{
|
||||
_itemIndex.TryGetValue(itemId, out var item);
|
||||
return item;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Clear and Refresh
|
||||
|
||||
/// <summary>
|
||||
/// 모든 엔트리를 제거합니다.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var group in _groupIndex.Values)
|
||||
{
|
||||
group.Clear();
|
||||
}
|
||||
_entries.Clear();
|
||||
_groupIndex.Clear();
|
||||
_itemIndex.Clear();
|
||||
EntriesCleared?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View를 갱신합니다.
|
||||
/// </summary>
|
||||
public void Refresh()
|
||||
{
|
||||
if (_view != null)
|
||||
{
|
||||
_view.Initialize(this);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Value Update
|
||||
|
||||
/// <summary>
|
||||
/// 특정 ID를 가진 속성의 값을 업데이트합니다.
|
||||
/// 이 메서드는 주로 View에서 사용자 입력이 발생했을 때 호출됩니다.
|
||||
@@ -51,45 +430,71 @@ namespace UVC.UI.Window.PropertyWindow
|
||||
/// <param name="newValue">새로운 값</param>
|
||||
public void UpdatePropertyValue(string propertyId, PropertyType propertyType, object newValue)
|
||||
{
|
||||
// ID에 해당하는 속성을 찾습니다.
|
||||
var propertyItem = Properties.FirstOrDefault(p => p.Id == propertyId);
|
||||
|
||||
if (propertyItem == null)
|
||||
if (!_itemIndex.TryGetValue(propertyId, out var propertyItem))
|
||||
{
|
||||
// 해당 ID의 속성이 없으면 오류를 기록하고 반환합니다.
|
||||
UnityEngine.Debug.LogError($"[PropertyWindow] ID '{propertyId}'에 해당하는 속성을 찾을 수 없습니다.");
|
||||
Debug.LogError($"[PropertyWindow] ID '{propertyId}'에 해당하는 속성을 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 이전 값을 저장합니다.
|
||||
object oldValue = propertyItem.GetValue();
|
||||
object? oldValue = propertyItem.GetValue();
|
||||
|
||||
// 값 타입일 때 새 값과 이전 값이 같은지 확인합니다. 참조 타입은 PropertyUI에서 필터링 (불필요한 이벤트 발생 방지)
|
||||
if (oldValue.GetType().IsValueType && Equals(oldValue, newValue))
|
||||
// 값 타입일 때 새 값과 이전 값이 같은지 확인합니다.
|
||||
if (oldValue != null && oldValue.GetType().IsValueType && Equals(oldValue, newValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 속성 객체의 값을 새로운 값으로 설정합니다.
|
||||
propertyItem.SetValue(newValue);
|
||||
|
||||
// 값이 변경되었음을 알리는 이벤트를 발생시킵니다.
|
||||
OnPropertyValueChanged(propertyId, propertyType, oldValue, newValue);
|
||||
OnPropertyValueChanged(propertyId, propertyType, oldValue!, newValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PropertyValueChanged 이벤트를 안전하게 발생시키는 보호된 가상 메서드입니다.
|
||||
/// </summary>
|
||||
/// <param name="propertyId">변경된 속성 ID</param>
|
||||
/// <param name="oldValue">이전 값</param>
|
||||
/// <param name="newValue">새로운 값</param>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// 엔트리를 내부 컬렉션에 추가합니다.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
@@ -101,26 +506,29 @@ namespace UVC.UI.Window.PropertyWindow
|
||||
public void Hide()
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
FactoryCameraController.Instance.Enable = true; // 카메라 컨트롤러 활성화
|
||||
FactoryCameraController.Instance.Enable = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Pointer Events
|
||||
|
||||
/// <summary>
|
||||
/// 마우스 포인터가 이 UI 요소의 영역 안으로 들어왔을 때 호출됩니다.
|
||||
/// UI와 상호작용하는 동안 3D 뷰의 카메라가 움직이지 않도록 컨트롤러를 비활성화합니다.
|
||||
/// </summary>
|
||||
public virtual void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
FactoryCameraController.Instance.Enable = false; // 카메라 컨트롤러 비활성화
|
||||
FactoryCameraController.Instance.Enable = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마우스 포인터가 이 UI 요소의 영역 밖으로 나갔을 때 호출됩니다.
|
||||
/// 카메라 컨트롤을 다시 활성화하여 3D 뷰를 조작할 수 있도록 합니다.
|
||||
/// </summary>
|
||||
public virtual void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
FactoryCameraController.Instance.Enable = true; // 카메라 컨트롤러 활성화
|
||||
FactoryCameraController.Instance.Enable = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
8
Assets/Scripts/UVC/UI/Window/PropertyWindow/View.meta
Normal file
8
Assets/Scripts/UVC/UI/Window/PropertyWindow/View.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 290d4b3761c68414b8afb4350b257340
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,151 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace UVC.UI.Window.PropertyWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// 속성 그룹의 UI를 담당하는 View 클래스입니다.
|
||||
/// 접이식 헤더와 자식 아이템들의 컨테이너를 관리합니다.
|
||||
/// </summary>
|
||||
public class PropertyGroupView : MonoBehaviour
|
||||
{
|
||||
[Header("Header")]
|
||||
[SerializeField] private Button _headerButton;
|
||||
[SerializeField] private TextMeshProUGUI _groupNameLabel;
|
||||
[SerializeField] private Image _expandIcon;
|
||||
|
||||
[Header("Icons")]
|
||||
[SerializeField] private Sprite _expandedIcon;
|
||||
[SerializeField] private Sprite _collapsedIcon;
|
||||
|
||||
[Header("Content")]
|
||||
[SerializeField] private Transform _itemContainer;
|
||||
[SerializeField] private GameObject _contentPanel;
|
||||
|
||||
private IPropertyGroup _group;
|
||||
private PropertyWindow _controller;
|
||||
|
||||
/// <summary>
|
||||
/// 자식 PropertyItem UI들이 생성될 컨테이너입니다.
|
||||
/// </summary>
|
||||
public Transform ItemContainer => _itemContainer;
|
||||
|
||||
/// <summary>
|
||||
/// 그룹 데이터
|
||||
/// </summary>
|
||||
public IPropertyGroup Group => _group;
|
||||
|
||||
/// <summary>
|
||||
/// 그룹 ID
|
||||
/// </summary>
|
||||
public string GroupId => _group?.GroupId;
|
||||
|
||||
/// <summary>
|
||||
/// PropertyGroupView를 초기화합니다.
|
||||
/// </summary>
|
||||
/// <param name="group">표시할 그룹 데이터</param>
|
||||
/// <param name="controller">상호작용할 컨트롤러</param>
|
||||
public void Setup(IPropertyGroup group, PropertyWindow controller)
|
||||
{
|
||||
_group = group;
|
||||
_controller = controller;
|
||||
|
||||
// 그룹명 설정
|
||||
if (_groupNameLabel != null)
|
||||
{
|
||||
_groupNameLabel.text = group.GroupName;
|
||||
}
|
||||
|
||||
// 헤더 버튼 이벤트 등록
|
||||
if (_headerButton != null)
|
||||
{
|
||||
_headerButton.onClick.RemoveAllListeners();
|
||||
_headerButton.onClick.AddListener(OnHeaderClicked);
|
||||
}
|
||||
|
||||
// 펼침/접힘 상태 반영
|
||||
UpdateExpandedState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 헤더 클릭 시 호출됩니다.
|
||||
/// </summary>
|
||||
private void OnHeaderClicked()
|
||||
{
|
||||
if (_controller != null && _group != null)
|
||||
{
|
||||
_controller.ToggleGroupExpanded(_group.GroupId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 펼침/접힘 상태를 UI에 반영합니다.
|
||||
/// </summary>
|
||||
public void UpdateExpandedState()
|
||||
{
|
||||
if (_group == null) return;
|
||||
|
||||
bool isExpanded = _group.IsExpanded;
|
||||
|
||||
// 컨텐츠 패널 표시/숨김
|
||||
if (_contentPanel != null)
|
||||
{
|
||||
_contentPanel.SetActive(isExpanded);
|
||||
}
|
||||
|
||||
// 아이콘 변경
|
||||
if (_expandIcon != null)
|
||||
{
|
||||
if (isExpanded && _expandedIcon != null)
|
||||
{
|
||||
_expandIcon.sprite = _expandedIcon;
|
||||
}
|
||||
else if (!isExpanded && _collapsedIcon != null)
|
||||
{
|
||||
_expandIcon.sprite = _collapsedIcon;
|
||||
}
|
||||
|
||||
// 아이콘 회전으로 표현할 경우
|
||||
_expandIcon.transform.rotation = Quaternion.Euler(0, 0, isExpanded ? 0 : -90);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 그룹 내 모든 아이템 UI를 제거합니다.
|
||||
/// </summary>
|
||||
public void ClearItems()
|
||||
{
|
||||
if (_itemContainer == null) return;
|
||||
|
||||
foreach (Transform child in _itemContainer)
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 풀에 반환하기 전에 정리합니다.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_group = null;
|
||||
_controller = null;
|
||||
|
||||
if (_headerButton != null)
|
||||
{
|
||||
_headerButton.onClick.RemoveAllListeners();
|
||||
}
|
||||
|
||||
ClearItems();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_headerButton != null)
|
||||
{
|
||||
_headerButton.onClick.RemoveAllListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1292fe635565fff43a6bd494f3f597b8
|
||||
@@ -0,0 +1,231 @@
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UVC.Extention;
|
||||
using UVC.UI.Tooltip;
|
||||
|
||||
namespace UVC.UI.Window.PropertyWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// 모든 PropertyUI의 공통 베이스 클래스입니다.
|
||||
/// 공통 기능을 제공하고 IPropertyUI, IPoolable, IDisposable을 구현합니다.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(LayoutElement))]
|
||||
public abstract class PropertyUIBase<T> : MonoBehaviour, IPropertyUI, IPoolable, IDisposable
|
||||
where T : class, IPropertyItem
|
||||
{
|
||||
[Header("Common UI Elements")]
|
||||
[SerializeField] protected TextMeshProUGUI _nameLabel;
|
||||
[SerializeField] protected TextMeshProUGUI _descriptionLabel;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 표시 중인 속성 아이템
|
||||
/// </summary>
|
||||
protected T _propertyItem;
|
||||
|
||||
/// <summary>
|
||||
/// 상호작용할 컨트롤러
|
||||
/// </summary>
|
||||
protected PropertyWindow _controller;
|
||||
|
||||
/// <summary>
|
||||
/// 이미 해제되었는지 여부
|
||||
/// </summary>
|
||||
protected bool _isDisposed = false;
|
||||
|
||||
#region IPropertyUI Implementation
|
||||
|
||||
/// <summary>
|
||||
/// PropertyView에 의해 호출되어 UI를 초기화합니다.
|
||||
/// </summary>
|
||||
public virtual void Setup(IPropertyItem item, PropertyWindow controller)
|
||||
{
|
||||
// 기존 상태 정리
|
||||
Cleanup();
|
||||
|
||||
// 타입 체크
|
||||
if (!(item is T typedItem))
|
||||
{
|
||||
Debug.LogError($"[{GetType().Name}] 잘못된 타입의 PropertyItem이 전달되었습니다. 예상: {typeof(T).Name}, 실제: {item.GetType().Name}");
|
||||
return;
|
||||
}
|
||||
|
||||
_propertyItem = typedItem;
|
||||
_controller = controller;
|
||||
_isDisposed = false;
|
||||
|
||||
// 공통 UI 설정
|
||||
SetupCommonUI();
|
||||
|
||||
// 파생 클래스의 추가 설정
|
||||
SetupValueUI();
|
||||
|
||||
// 이벤트 구독
|
||||
SubscribeEvents();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Common UI Setup
|
||||
|
||||
/// <summary>
|
||||
/// 공통 UI 요소들을 설정합니다.
|
||||
/// </summary>
|
||||
protected virtual void SetupCommonUI()
|
||||
{
|
||||
if (_propertyItem == null) return;
|
||||
|
||||
// 이름 설정
|
||||
if (_nameLabel != null)
|
||||
{
|
||||
_nameLabel.text = _propertyItem.Name;
|
||||
|
||||
// 툴팁 설정
|
||||
var tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
|
||||
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
|
||||
{
|
||||
tooltipHandler.Tooltip = _propertyItem.Tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
// 설명 설정
|
||||
if (_descriptionLabel != null)
|
||||
{
|
||||
if (_propertyItem.Description.IsNullOrEmpty())
|
||||
{
|
||||
_descriptionLabel.gameObject.SetActive(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_descriptionLabel.gameObject.SetActive(true);
|
||||
_descriptionLabel.text = _propertyItem.Description;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 파생 클래스에서 값 관련 UI를 설정합니다.
|
||||
/// </summary>
|
||||
protected abstract void SetupValueUI();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Management
|
||||
|
||||
/// <summary>
|
||||
/// 이벤트를 구독합니다. 파생 클래스에서 재정의할 수 있습니다.
|
||||
/// </summary>
|
||||
protected virtual void SubscribeEvents()
|
||||
{
|
||||
// 기본 구현 없음 - 파생 클래스에서 필요시 구현
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이벤트 구독을 해제합니다. 파생 클래스에서 재정의할 수 있습니다.
|
||||
/// </summary>
|
||||
protected virtual void UnsubscribeEvents()
|
||||
{
|
||||
// 기본 구현 없음 - 파생 클래스에서 필요시 구현
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Value Update
|
||||
|
||||
/// <summary>
|
||||
/// 컨트롤러를 통해 값 변경을 통지합니다.
|
||||
/// </summary>
|
||||
/// <param name="newValue">새로운 값</param>
|
||||
protected void NotifyValueChanged(object newValue)
|
||||
{
|
||||
if (_controller == null || _propertyItem == null) return;
|
||||
|
||||
// 값이 변경되지 않았으면 무시
|
||||
var oldValue = _propertyItem.GetValue();
|
||||
if (Equals(oldValue, newValue)) return;
|
||||
|
||||
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 읽기 전용 상태를 설정합니다.
|
||||
/// </summary>
|
||||
/// <param name="interactable">UI 요소</param>
|
||||
protected void ApplyReadOnlyState(Selectable interactable)
|
||||
{
|
||||
if (interactable != null && _propertyItem != null)
|
||||
{
|
||||
interactable.interactable = !_propertyItem.IsReadOnly;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 읽기 전용 상태를 설정합니다 (TMP_InputField용).
|
||||
/// </summary>
|
||||
/// <param name="inputField">입력 필드</param>
|
||||
protected void ApplyReadOnlyState(TMP_InputField inputField)
|
||||
{
|
||||
if (inputField != null && _propertyItem != null)
|
||||
{
|
||||
inputField.interactable = !_propertyItem.IsReadOnly;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cleanup and Disposal
|
||||
|
||||
/// <summary>
|
||||
/// 상태를 정리합니다.
|
||||
/// </summary>
|
||||
protected virtual void Cleanup()
|
||||
{
|
||||
UnsubscribeEvents();
|
||||
_propertyItem = null;
|
||||
_controller = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IDisposable 구현
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
Cleanup();
|
||||
_isDisposed = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IPoolable Implementation
|
||||
|
||||
/// <summary>
|
||||
/// 풀에서 가져올 때 호출됩니다.
|
||||
/// </summary>
|
||||
public virtual void OnGetFromPool()
|
||||
{
|
||||
_isDisposed = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 풀에 반환될 때 호출됩니다.
|
||||
/// </summary>
|
||||
public virtual void OnReturnToPool()
|
||||
{
|
||||
Cleanup();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97fb1aaa6e72f9742b99c8d8ddf43ebc
|
||||
8
Assets/Scripts/UVC/UI/Window/PropertyWindow/View/UI.meta
Normal file
8
Assets/Scripts/UVC/UI/Window/PropertyWindow/View/UI.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba2f83247bcb33a45b3e8e656b4d3ed0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,87 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UVC.UI.Window.PropertyWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// StringProperty를 위한 PropertyUIBase 기반 UI 스크립트입니다.
|
||||
/// 기존 StringPropertyUI의 리팩토링 버전입니다.
|
||||
/// </summary>
|
||||
public class StringPropertyUIBase : PropertyUIBase<StringProperty>
|
||||
{
|
||||
[Header("String Property UI")]
|
||||
[SerializeField] private TMP_InputField _valueInput;
|
||||
|
||||
#region PropertyUIBase Implementation
|
||||
|
||||
/// <summary>
|
||||
/// 값 관련 UI를 설정합니다.
|
||||
/// </summary>
|
||||
protected override void SetupValueUI()
|
||||
{
|
||||
if (_valueInput == null || _propertyItem == null) return;
|
||||
|
||||
// 값 설정
|
||||
_valueInput.text = _propertyItem.Value;
|
||||
|
||||
// 읽기 전용 상태 설정
|
||||
ApplyReadOnlyState(_valueInput);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이벤트를 구독합니다.
|
||||
/// </summary>
|
||||
protected override void SubscribeEvents()
|
||||
{
|
||||
base.SubscribeEvents();
|
||||
|
||||
if (_valueInput != null)
|
||||
{
|
||||
_valueInput.onEndEdit.AddListener(OnValueSubmitted);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이벤트 구독을 해제합니다.
|
||||
/// </summary>
|
||||
protected override void UnsubscribeEvents()
|
||||
{
|
||||
base.UnsubscribeEvents();
|
||||
|
||||
if (_valueInput != null)
|
||||
{
|
||||
_valueInput.onEndEdit.RemoveListener(OnValueSubmitted);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
/// <summary>
|
||||
/// 사용자가 InputField 수정 완료 후 Enter를 누르거나 포커스를 잃었을 때 호출됩니다.
|
||||
/// </summary>
|
||||
/// <param name="newValue">InputField에 입력된 새로운 문자열</param>
|
||||
private void OnValueSubmitted(string newValue)
|
||||
{
|
||||
NotifyValueChanged(newValue);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IPoolable Implementation
|
||||
|
||||
public override void OnReturnToPool()
|
||||
{
|
||||
base.OnReturnToPool();
|
||||
|
||||
// 입력 필드 초기화
|
||||
if (_valueInput != null)
|
||||
{
|
||||
_valueInput.text = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cab478cef001a8c43afe2fe8389124ae
|
||||
Reference in New Issue
Block a user