PropertyWindow 개발중

This commit is contained in:
logonkhi
2025-09-22 20:12:56 +09:00
parent 5ccb24d4dc
commit 1bc008159a
146 changed files with 50167 additions and 18 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 180a8464dbdefe7449c29c189b7f3879
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,443 @@
using Cysharp.Threading.Tasks;
using DG.Tweening;
using System;
using System.Threading.Tasks;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Log;
using UVC.Util;
namespace UVC.UI.Modal.ColorPicker
{
/// <summary>
///
/// </summary>
public class ColorPicker : MonoBehaviour
{
/// <summary>
/// Event that gets called by the ColorPicker
/// </summary>
/// <param name="c">received Color</param>
public delegate void ColorEvent(Color c);
private static string PrefabPath = "Prefabs/UI/Modal/ColorPicker/ColorPicker";
private const string blockerPrefabPath = "Prefabs/UI/Modal/ModalBlocker"; //menu blocker prefab path
private static ColorPicker instance;
/// <returns>
/// True when the ColorPicker is closed
/// </returns>
public static bool done = true;
//onColorChanged event
private static ColorEvent onCC;
//onColorSelected event
private static ColorEvent onCS;
private static Action onClose;
//Color before editing
private static Color32 originalColor;
//current Color
private static Color32 modifiedColor;
private static HSV modifiedHsv;
//useAlpha bool
private static bool useA;
private bool interact;
// these can only work with the prefab and its children
public RectTransform positionIndicator;
public Slider mainComponent;
public Slider rComponent;
public Slider gComponent;
public Slider bComponent;
public Slider aComponent;
public TMP_InputField hexaComponent;
public RawImage colorComponent;
private Canvas parentCanvas;
private static GameObject? blockerPrefabObj;
private static GameObject currentBlockerInstance;
private void Awake()
{
instance = this;
gameObject.SetActive(false);
}
/// <summary>
/// Creates a new Colorpicker
/// </summary>
/// <param name="original">Color before editing</param>
/// <param name="message">Display message</param>
/// <param name="onColorChanged">Event that gets called when the color gets modified</param>
/// <param name="onColorSelected">Event that gets called when one of the buttons done or cancel get pressed</param>
/// <param name="useAlpha">When set to false the colors used don't have an alpha channel</param>
/// <returns>
/// False if the instance is already running
/// </returns>
public static async UniTask<bool> Create(Color original, string message, ColorEvent onColorChanged, ColorEvent onColorSelected, Action onCloseAction = null, bool useAlpha = false)
{
if (instance is null)
{
var prefab = await Resources.LoadAsync<ColorPicker>(PrefabPath) as ColorPicker;
instance = Instantiate(prefab);
if (instance is null)
{
Debug.LogError("No Colorpicker prefab active on 'Start' in scene");
return false;
}
Canvas[] canvases = FindObjectsByType<Canvas>(FindObjectsSortMode.None);
//sortingOrder로 정렬 내림차순 - sortingOrder가 높은 캔버스가 먼저 오도록
Array.Sort(canvases, (x, y) => y.sortingOrder.CompareTo(x.sortingOrder));
foreach (Canvas c in canvases)
{
if (c.renderMode != RenderMode.WorldSpace)
{
instance.transform.SetParent(c.transform, false);
instance.parentCanvas = c;
break;
}
}
}
if (done)
{
done = false;
originalColor = original;
modifiedColor = original;
onCC = onColorChanged;
onCS = onColorSelected;
onClose = onCloseAction;
useA = useAlpha;
instance.gameObject.SetActive(true);
instance.transform.GetChild(0).GetChild(0).GetComponent<TextMeshProUGUI>().text = message;
instance.aComponent.gameObject.SetActive(useAlpha);
instance.RecalculateMenu(true);
instance.hexaComponent.placeholder.GetComponent<TextMeshProUGUI>().text = "RRGGBB" + (useAlpha ? "AA" : "");
//화면 센터에 위치 시키기
RectTransform rectTransform = instance.GetComponent<RectTransform>();
rectTransform.anchoredPosition = new Vector2((Screen.width - rectTransform.sizeDelta.x) / 2, -(Screen.height - rectTransform.sizeDelta.y)/2);
ShowBlock();
return true;
}
else
{
Done();
return false;
}
}
private static async void ShowBlock()
{
if (blockerPrefabObj == null)
{
blockerPrefabObj = await Resources.LoadAsync<GameObject>(blockerPrefabPath) as GameObject;
}
if (blockerPrefabObj != null)
{
// 화면에서 가장 큰 그림판(Canvas)을 찾아서 그 위에 방패를 놓을 거예요.
Canvas mainCanvasForBlocker = instance.parentCanvas;
if (mainCanvasForBlocker != null)
{
// 방패를 복제해서(Instantiate) 그림판 위에 놓고, 가장 위로 오도록 순서를 조정해요.
currentBlockerInstance = UnityEngine.Object.Instantiate(blockerPrefabObj, mainCanvasForBlocker.transform);
int siblingIndex = instance.transform.GetSiblingIndex();
currentBlockerInstance.transform.SetSiblingIndex(Math.Max(0, siblingIndex - 1));
// 방패가 부드럽게 나타나도록 CanvasGroup 컴포넌트를 사용해요. 없으면 새로 추가!
CanvasGroup blockerCanvasGroup = currentBlockerInstance.GetComponent<CanvasGroup>();
if (blockerCanvasGroup == null) blockerCanvasGroup = currentBlockerInstance.AddComponent<CanvasGroup>();
blockerCanvasGroup.alpha = 0f;// 처음엔 완전히 투명하게
blockerCanvasGroup.DOFade(0.7f, 0.3f);// 0.3초 동안 서서히 나타나게 (투명도 70%)
}
else ULog.Error("[Modal] UIBlocker를 표시할 Canvas를 찾을 수 없습니다."); // 그림판을 못 찾으면 에러!
}
else ULog.Warning($"[Modal] UIBlocker 프리팹을 다음 경로에서 찾을 수 없습니다: {blockerPrefabPath}");
}
private static void HideBlock()
{
onClose?.Invoke();
if (currentBlockerInstance != null)
{
var blockerCG = currentBlockerInstance.GetComponent<CanvasGroup>();
if (blockerCG != null)
{
blockerCG.DOFade(0f, 0.3f).OnComplete(()=>{
UnityEngine.Object.Destroy(currentBlockerInstance); // 완전히 제거
}); // 부드럽게 사라지게
}
}
}
//called when color is modified, to update other UI components
private void RecalculateMenu(bool recalculateHSV)
{
interact = false;
if (recalculateHSV)
{
modifiedHsv = new HSV(modifiedColor);
}
else
{
modifiedColor = modifiedHsv.ToColor();
}
rComponent.value = modifiedColor.r;
rComponent.transform.GetChild(3).GetComponent<TMP_InputField>().text = modifiedColor.r.ToString();
gComponent.value = modifiedColor.g;
gComponent.transform.GetChild(3).GetComponent<TMP_InputField>().text = modifiedColor.g.ToString();
bComponent.value = modifiedColor.b;
bComponent.transform.GetChild(3).GetComponent<TMP_InputField>().text = modifiedColor.b.ToString();
if (useA)
{
aComponent.value = modifiedColor.a;
aComponent.transform.GetChild(3).GetComponent<TMP_InputField>().text = modifiedColor.a.ToString();
}
mainComponent.value = (float)modifiedHsv.H;
rComponent.transform.GetChild(0).GetComponent<RawImage>().color = new Color32(255, modifiedColor.g, modifiedColor.b, 255);
rComponent.transform.GetChild(0).GetChild(0).GetComponent<RawImage>().color = new Color32(0, modifiedColor.g, modifiedColor.b, 255);
gComponent.transform.GetChild(0).GetComponent<RawImage>().color = new Color32(modifiedColor.r, 255, modifiedColor.b, 255);
gComponent.transform.GetChild(0).GetChild(0).GetComponent<RawImage>().color = new Color32(modifiedColor.r, 0, modifiedColor.b, 255);
bComponent.transform.GetChild(0).GetComponent<RawImage>().color = new Color32(modifiedColor.r, modifiedColor.g, 255, 255);
bComponent.transform.GetChild(0).GetChild(0).GetComponent<RawImage>().color = new Color32(modifiedColor.r, modifiedColor.g, 0, 255);
if (useA) aComponent.transform.GetChild(0).GetChild(0).GetComponent<RawImage>().color = new Color32(modifiedColor.r, modifiedColor.g, modifiedColor.b, 255);
positionIndicator.parent.GetChild(0).GetComponent<RawImage>().color = new HSV(modifiedHsv.H, 1d, 1d).ToColor();
positionIndicator.anchorMin = new Vector2((float)modifiedHsv.S, (float)modifiedHsv.V);
positionIndicator.anchorMax = positionIndicator.anchorMin;
hexaComponent.text = useA ? ColorUtility.ToHtmlStringRGBA(modifiedColor) : ColorUtility.ToHtmlStringRGB(modifiedColor);
colorComponent.color = modifiedColor;
onCC?.Invoke(modifiedColor);
interact = true;
}
//used by EventTrigger to calculate the chosen value in color box
public void SetChooser()
{
RectTransformUtility.ScreenPointToLocalPointInRectangle(positionIndicator.parent as RectTransform, Input.mousePosition, GetComponentInParent<Canvas>().worldCamera, out Vector2 localpoint);
localpoint = Rect.PointToNormalized((positionIndicator.parent as RectTransform).rect, localpoint);
if (positionIndicator.anchorMin != localpoint)
{
positionIndicator.anchorMin = localpoint;
positionIndicator.anchorMax = localpoint;
modifiedHsv.S = localpoint.x;
modifiedHsv.V = localpoint.y;
RecalculateMenu(false);
}
}
//gets main Slider value
public void SetMain(float value)
{
if (interact)
{
modifiedHsv.H = value;
RecalculateMenu(false);
}
}
//gets r Slider value
public void SetR(float value)
{
if (interact)
{
modifiedColor.r = (byte)value;
RecalculateMenu(true);
}
}
//gets r InputField value
public void SetR(string value)
{
if (interact)
{
modifiedColor.r = (byte)Mathf.Clamp(int.Parse(value), 0, 255);
RecalculateMenu(true);
}
}
//gets g Slider value
public void SetG(float value)
{
if (interact)
{
modifiedColor.g = (byte)value;
RecalculateMenu(true);
}
}
//gets g InputField value
public void SetG(string value)
{
if (interact)
{
modifiedColor.g = (byte)Mathf.Clamp(int.Parse(value), 0, 255);
RecalculateMenu(true);
}
}
//gets b Slider value
public void SetB(float value)
{
if (interact)
{
modifiedColor.b = (byte)value;
RecalculateMenu(true);
}
}
//gets b InputField value
public void SetB(string value)
{
if (interact)
{
modifiedColor.b = (byte)Mathf.Clamp(int.Parse(value), 0, 255);
RecalculateMenu(true);
}
}
//gets a Slider value
public void SetA(float value)
{
if (interact)
{
modifiedHsv.A = (byte)value;
RecalculateMenu(false);
}
}
//gets a InputField value
public void SetA(string value)
{
if (interact)
{
modifiedHsv.A = (byte)Mathf.Clamp(int.Parse(value), 0, 255);
RecalculateMenu(false);
}
}
//gets hexa InputField value
public void SetHexa(string value)
{
if (interact)
{
if (ColorUtility.TryParseHtmlString("#" + value, out Color c))
{
if (!useA) c.a = 1;
modifiedColor = c;
RecalculateMenu(true);
}
else
{
hexaComponent.text = useA ? ColorUtility.ToHtmlStringRGBA(modifiedColor) : ColorUtility.ToHtmlStringRGB(modifiedColor);
}
}
}
//cancel button call
public void CCancel()
{
Cancel();
}
/// <summary>
/// Manually cancel the ColorPicker and recover the default value
/// </summary>
public static void Cancel()
{
modifiedColor = originalColor;
Done();
}
//done button call
public void CDone()
{
HideBlock();
Done();
}
/// <summary>
/// Manually close the ColorPicker and apply the selected color
/// </summary>
public static void Done()
{
HideBlock();
done = true;
onCC?.Invoke(modifiedColor);
onCS?.Invoke(modifiedColor);
instance.transform.gameObject.SetActive(false);
}
//HSV helper class
private sealed class HSV
{
public double H = 0, S = 1, V = 1;
public byte A = 255;
public HSV() { }
public HSV(double h, double s, double v)
{
H = h;
S = s;
V = v;
}
public HSV(Color color)
{
float max = Mathf.Max(color.r, Mathf.Max(color.g, color.b));
float min = Mathf.Min(color.r, Mathf.Min(color.g, color.b));
float hue = (float)H;
if (min != max)
{
if (max == color.r)
{
hue = (color.g - color.b) / (max - min);
}
else if (max == color.g)
{
hue = 2f + (color.b - color.r) / (max - min);
}
else
{
hue = 4f + (color.r - color.g) / (max - min);
}
hue *= 60;
if (hue < 0) hue += 360;
}
H = hue;
S = (max == 0) ? 0 : 1d - ((double)min / max);
V = max;
A = (byte)(color.a * 255);
}
public Color32 ToColor()
{
int hi = Convert.ToInt32(Math.Floor(H / 60)) % 6;
double f = H / 60 - Math.Floor(H / 60);
double value = V * 255;
byte v = (byte)Convert.ToInt32(value);
byte p = (byte)Convert.ToInt32(value * (1 - S));
byte q = (byte)Convert.ToInt32(value * (1 - f * S));
byte t = (byte)Convert.ToInt32(value * (1 - (1 - f) * S));
switch (hi)
{
case 0:
return new Color32(v, t, p, A);
case 1:
return new Color32(q, v, p, A);
case 2:
return new Color32(p, v, t, A);
case 3:
return new Color32(p, q, v, A);
case 4:
return new Color32(t, p, v, A);
case 5:
return new Color32(v, p, q, A);
default:
return new Color32();
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 0a6d047dd7b36be40a5d4f319c3aab4e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 176831
packageName: Easy Color & Gradient Picker
packageVersion: 1.0
assetPath: Assets/ColorGradientPicker/Scripts/ColorPicker.cs
uploadId: 384684

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 069a815c572419549b7571b8e388a8ea
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,19 @@
using TMPro;
using UnityEngine;
namespace UVC.UI.Modal.DatePicker
{
public class DateItem : MonoBehaviour
{
[SerializeField]
public DatePicker picker;
[SerializeField]
public TextMeshProUGUI text;
public void OnDateItemClick()
{
picker.OnDateItemClick(text.text);
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: ea51964c1ed62bd49b030e4ab1e6b629
timeCreated: 1473653468
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,247 @@
using Cysharp.Threading.Tasks;
using DG.Tweening;
using System;
using System.Collections.Generic;
using TMPro;
using UnityEditor.Search;
using UnityEngine;
using UVC.Log;
using UVC.Util;
namespace UVC.UI.Modal.DatePicker
{
public class DatePicker : MonoBehaviour
{
private static string PrefabPath = "Prefabs/UI/Modal/DatePicker/DatePicker";
private const string blockerPrefabPath = "Prefabs/UI/Modal/ModalBlocker"; //menu blocker prefab path
private static DatePicker instance;
private static Action<DateTime> _onDateSelectedCallback;
private Canvas parentCanvas;
private static GameObject? blockerPrefabObj;
private static GameObject currentBlockerInstance;
[Header("Calendar")]
[SerializeField]
public GameObject _calendarPanel;
[SerializeField]
public TextMeshProUGUI _yearNumText;
[SerializeField]
public TextMeshProUGUI _monthNumText;
[SerializeField]
public GameObject _item;
public List<GameObject> _dateItems = new List<GameObject>();
const int _totalDateNum = 42;
private DateTime _dateTime;
public static async void Show(DateTime initialDate, Action<DateTime> onDateSelected)
{
if (instance == null)
{
var prefab = await Resources.LoadAsync<DatePicker>(PrefabPath) as DatePicker;
instance = Instantiate(prefab);
if (instance == null)
{
Debug.LogError("No DatePicker prefab active on 'Start' in scene");
return;
}
Canvas[] canvases = FindObjectsByType<Canvas>(FindObjectsSortMode.None);
//sortingOrder로 정렬 내림차순 - sortingOrder가 높은 캔버스가 먼저 오도록
Array.Sort(canvases, (x, y) => y.sortingOrder.CompareTo(x.sortingOrder));
foreach (Canvas c in canvases)
{
if (c.renderMode != RenderMode.WorldSpace)
{
instance.transform.SetParent(c.transform, false);
instance.parentCanvas = c;
break;
}
}
}
instance.gameObject.SetActive(true);
instance._dateTime = initialDate;
//화면 센터에 위치 시키기
RectTransform rectTransform = instance.GetComponent<RectTransform>();
rectTransform.anchoredPosition = new Vector2((Screen.width - rectTransform.sizeDelta.x) / 2, -(Screen.height - rectTransform.sizeDelta.y) / 2);
_onDateSelectedCallback = onDateSelected;
ShowBlock();
}
public static void Hide()
{
if (instance != null) instance.gameObject.SetActive(false);
_onDateSelectedCallback = null; // 콜백 참조 정리
HideBlock();
}
private static async void ShowBlock()
{
if (blockerPrefabObj == null)
{
blockerPrefabObj = await Resources.LoadAsync<GameObject>(blockerPrefabPath) as GameObject;
}
if (blockerPrefabObj != null)
{
// 화면에서 가장 큰 그림판(Canvas)을 찾아서 그 위에 방패를 놓을 거예요.
Canvas mainCanvasForBlocker = instance.parentCanvas;
if (mainCanvasForBlocker != null)
{
// 방패를 복제해서(Instantiate) 그림판 위에 놓고, 가장 위로 오도록 순서를 조정해요.
currentBlockerInstance = UnityEngine.Object.Instantiate(blockerPrefabObj, mainCanvasForBlocker.transform);
int siblingIndex = instance.transform.GetSiblingIndex();
currentBlockerInstance.transform.SetSiblingIndex(Math.Max(0, siblingIndex - 1));
// 방패가 부드럽게 나타나도록 CanvasGroup 컴포넌트를 사용해요. 없으면 새로 추가!
CanvasGroup blockerCanvasGroup = currentBlockerInstance.GetComponent<CanvasGroup>();
if (blockerCanvasGroup == null) blockerCanvasGroup = currentBlockerInstance.AddComponent<CanvasGroup>();
blockerCanvasGroup.alpha = 0f;// 처음엔 완전히 투명하게
blockerCanvasGroup.DOFade(0.7f, 0.3f);// 0.3초 동안 서서히 나타나게 (투명도 70%)
}
else ULog.Error("[Modal] UIBlocker를 표시할 Canvas를 찾을 수 없습니다."); // 그림판을 못 찾으면 에러!
}
else ULog.Warning($"[Modal] UIBlocker 프리팹을 다음 경로에서 찾을 수 없습니다: {blockerPrefabPath}");
}
private static void HideBlock()
{
if (currentBlockerInstance != null)
{
var blockerCG = currentBlockerInstance.GetComponent<CanvasGroup>();
if (blockerCG != null)
{
blockerCG.DOFade(0f, 0.3f).OnComplete(() => {
UnityEngine.Object.Destroy(currentBlockerInstance); // 완전히 제거
}); // 부드럽게 사라지게
}
}
}
void Start()
{
Vector3 startPos = _item.transform.localPosition;
_dateItems.Clear();
_dateItems.Add(_item);
for (int i = 1; i < _totalDateNum; i++)
{
GameObject item = GameObject.Instantiate(_item) as GameObject;
item.name = "Item" + (i + 1).ToString();
item.transform.SetParent(_item.transform.parent);
item.transform.localScale = Vector3.one;
item.transform.localRotation = Quaternion.identity;
item.transform.localPosition = new Vector3((i % 7) * 36 + startPos.x, startPos.y - (i / 7) * 30, startPos.z);
_dateItems.Add(item);
}
//_dateTime = DateTime.Now;
CreateCalendar();
}
void CreateCalendar()
{
DateTime firstDay = _dateTime.AddDays(-(_dateTime.Day - 1));
int index = GetDays(firstDay.DayOfWeek);
int date = 0;
for (int i = 0; i < _totalDateNum; i++)
{
TextMeshProUGUI label = _dateItems[i].GetComponentInChildren<TextMeshProUGUI>();
_dateItems[i].SetActive(false);
DateTime day = firstDay.AddDays(date);
Color textColor = ColorUtil.FromHex("808080");
//토요일 색 변경
if (day.DayOfWeek == DayOfWeek.Saturday) //토요일
{
textColor = ColorUtil.FromHex("4A90E2");
}
else if (day.DayOfWeek == DayOfWeek.Sunday) //일요일
{
textColor = ColorUtil.FromHex("FF5E5E");
}
//오늘 날짜인지 확인해서 text 색 변경
if (day.Date == DateTime.Now.Date) textColor = Color.white;
label.color= textColor;
if (i >= index)
{
DateTime thatDay = firstDay.AddDays(date);
if (thatDay.Month == firstDay.Month)
{
_dateItems[i].SetActive(true);
label.text = (date + 1).ToString();
date++;
}
}
}
_yearNumText.text = _dateTime.Year.ToString();
_monthNumText.text = _dateTime.Month.ToString("D2");
}
int GetDays(DayOfWeek day)
{
switch (day)
{
case DayOfWeek.Monday: return 1;
case DayOfWeek.Tuesday: return 2;
case DayOfWeek.Wednesday: return 3;
case DayOfWeek.Thursday: return 4;
case DayOfWeek.Friday: return 5;
case DayOfWeek.Saturday: return 6;
case DayOfWeek.Sunday: return 0;
}
return 0;
}
public void YearPrev()
{
_dateTime = _dateTime.AddYears(-1);
CreateCalendar();
}
public void YearNext()
{
_dateTime = _dateTime.AddYears(1);
CreateCalendar();
}
public void MonthPrev()
{
_dateTime = _dateTime.AddMonths(-1);
CreateCalendar();
}
public void MonthNext()
{
_dateTime = _dateTime.AddMonths(1);
CreateCalendar();
}
//Item 클릭했을 경우 Text에 표시.
public void OnDateItemClick(string day)
{
string date = _yearNumText.text + "-" + _monthNumText.text + "-" + int.Parse(day).ToString("D2");
_onDateSelectedCallback?.Invoke(DateTime.Parse(date));
Hide();
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: a65b983b6ab00bc4dbb0ba158aa4c3ab
timeCreated: 1473647493
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,4 +1,4 @@
using System.Collections;
using System.Collections;
using UnityEngine;
using UnityEngine.EventSystems; // IPointerEnterHandler, IPointerExitHandler 인터페이스 사용
using UnityEngine.UI; // Selectable 컴포넌트 사용 (버튼 등의 상호작용 가능 여부 확인)
@@ -74,8 +74,8 @@ namespace UVC.UI.Tooltip
public void OnPointerEnter(PointerEventData eventData)
{
// 툴팁 내용이 있고, 이 게임오브젝트에 Selectable 컴포넌트가 있거나 없거나, 있다면 interactable 상태일 때만
Selectable selectable = gameObject.GetComponent<Selectable>();
if (!string.IsNullOrEmpty(Tooltip) && (selectable == null || selectable.interactable))
//Selectable selectable = gameObject.GetComponent<Selectable>();
if (!string.IsNullOrEmpty(Tooltip))// && (selectable == null || selectable.interactable))
{
// 이전에 실행 중이던 코루틴이 있다면 중지 (빠르게 들어왔다 나갔다 반복하는 경우 대비)
if (_showTooltipCoroutine != null)
@@ -121,6 +121,12 @@ namespace UVC.UI.Tooltip
// TooltipDelay 시간 동안 마우스가 거의 움직이지 않았으면 툴팁 표시
_showTooltipCoroutine = null; // 코루틴 완료 후 참조 null 처리
OnPointerEnterAction?.Invoke(tooltip, Input.mousePosition); // 현재 마우스 위치 사용
// TooltipManager의 메서드와 연결
if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized)
{
TooltipManager.Instance.HandlePointerEnter(tooltip, Input.mousePosition);
}
}
/// <summary>
@@ -137,6 +143,10 @@ namespace UVC.UI.Tooltip
_showTooltipCoroutine = null;
}
OnPointerExitAction?.Invoke(); // 연결된 액션 호출 (TooltipManager.HandlePointerExit)
if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized)
{
TooltipManager.Instance.HandlePointerExit();
}
}
/// <summary>

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5d273c65a1a0338468d8064ae2719c7f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 50eb185ce2056f24199c64ecfca67f14
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,15 @@
namespace UVC.UI.Window.PropertyWindow
{
/// <summary>
/// 모든 속성 UI 스크립트가 구현해야 할 인터페이스입니다.
/// </summary>
public interface IPropertyUI
{
/// <summary>
/// UI를 초기화하고 데이터를 설정합니다.
/// </summary>
/// <param name="item">표시할 속성 데이터</param>
/// <param name="controller">상호작용할 컨트롤러</param>
void Setup(IPropertyItem item, PropertyWindow controller);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bb245e4f78fa6a7439629fd7c6212d7e

View File

@@ -0,0 +1,333 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace UVC.UI.Window.PropertyWindow
{
/// <summary>
/// 속성의 값 타입을 정의하는 열거형입니다.
/// View에서 이 타입을 보고 적절한 UI 컨트롤을 생성합니다.
/// </summary>
public enum PropertyType
{
String,
Int,
Float,
Bool,
Vector2,
Vector3,
Color,
Date,
DateTime,
Enum,
DropdownList,
RadioGroup,
IntRange,
FloatRange,
DateRange,
DateTimeRange,
}
/// <summary>
/// 속성 값이 변경되었을 때 전달될 이벤트 데이터 클래스입니다.
/// </summary>
public class PropertyValueChangedEventArgs : EventArgs
{
/// <summary>
/// 변경된 속성의 고유 ID
/// </summary>
public string PropertyId { get; }
/// <summary>
/// 이 인스턴스가 나타내는 속성의 유형을 가져옵니다.
/// </summary>
public PropertyType PropertyType { get; }
/// <summary>
/// 변경 전의 값
/// </summary>
public object OldValue { get; }
/// <summary>
/// 변경 후의 새로운 값
/// </summary>
public object NewValue { get; }
public PropertyValueChangedEventArgs(string propertyId, PropertyType propertyType, object oldValue, object newValue)
{
PropertyId = propertyId;
PropertyType = propertyType;
OldValue = oldValue;
NewValue = newValue;
}
}
/// <summary>
/// 모든 속성 항목이 구현해야 하는 기본 인터페이스입니다.
/// </summary>
public interface IPropertyItem
{
/// <summary>
/// 속성의 고유 식별자 (필수)
/// </summary>
string Id { get; }
/// <summary>
/// UI에 표시될 속성 이름 (필수)
/// </summary>
string Name { get; }
/// <summary>
/// 속성에 대한 간단한 설명 (선택)
/// </summary>
string Description { get; set; }
/// <summary>
/// 마우스를 올렸을 때 표시될 툴팁 (선택)
/// </summary>
string Tooltip { get; set; }
/// <summary>
/// 읽기 전용 여부. true이면 UI에서 값 수정이 비활성화됩니다.
/// </summary>
bool IsReadOnly { get; set; }
/// <summary>
/// 속성의 현재 값 (object 타입)
/// </summary>
object GetValue();
/// <summary>
/// 속성의 값을 설정합니다.
/// </summary>
/// <param name="value">새로운 값</param>
void SetValue(object value);
/// <summary>
/// 속성의 데이터 타입
/// </summary>
PropertyType PropertyType { get; }
}
/// <summary>
/// IPropertyItem 인터페이스를 구현하는 제네릭 기반의 추상 클래스입니다.
/// 공통적인 기능을 미리 구현하여 코드 중복을 줄입니다.
/// </summary>
/// <typeparam name="T">속성 값의 실제 타입</typeparam>
public abstract class PropertyItem<T> : IPropertyItem
{
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Tooltip { get; set; }
public bool IsReadOnly { get; set; } = false;
public abstract PropertyType PropertyType { get; }
/// <summary>
/// 실제 데이터가 저장되는 필드
/// </summary>
protected T _value;
/// <summary>
/// 속성의 현재 값 (제네릭 타입)
/// </summary>
public T Value
{
get => _value;
set => _value = value;
}
protected PropertyItem(string id, string name, T initialValue)
{
Id = id;
Name = name;
_value = initialValue;
}
// IPropertyItem 인터페이스 구현
public object GetValue() => _value;
public void SetValue(object value)
{
// 타입 안정성을 위해 캐스팅 시도
if (value is T typedValue)
{
_value = typedValue;
}
else
{
Debug.LogError($"[PropertyItem] ID '{Id}'에 잘못된 타입의 값({value.GetType().Name})이 할당되었습니다. 필요한 타입: {typeof(T).Name}");
}
}
}
#region Concrete Property Classes
// --- 기본 타입 속성 ---
public class StringProperty : PropertyItem<string>
{
public override PropertyType PropertyType => PropertyType.String;
public StringProperty(string id, string name, string initialValue) : base(id, name, initialValue) { }
}
public class IntProperty : PropertyItem<int>
{
public override PropertyType PropertyType => PropertyType.Int;
public bool IsSlider { get; set; } = false; // 슬라이더로 표시할지 여부
public int MinValue { get; set; } = 0; // 슬라이더 최소값
public int MaxValue { get; set; } = 100; // 슬라이더 최대값
public IntProperty(string id, string name, int initialValue, bool isSlider = false, int minValue = 0, int maxValue = 100) : base(id, name, initialValue)
{
this.IsSlider = isSlider;
this.MinValue = minValue;
this.MaxValue = maxValue;
}
}
public class FloatProperty : PropertyItem<float>
{
public override PropertyType PropertyType => PropertyType.Float;
public bool IsSlider { get; set; } = false; // 슬라이더로 표시할지 여부
public float MinValue { get; set; } = 0; // 슬라이더 최소값
public float MaxValue { get; set; } = 1; // 슬라이더 최대값
public FloatProperty(string id, string name, float initialValue, bool isSlider = false, float minValue = 0, float maxValue = 1) : base(id, name, initialValue)
{
this.IsSlider = isSlider;
this.MinValue = minValue;
this.MaxValue = maxValue;
}
}
public class BoolProperty : PropertyItem<bool>
{
public override PropertyType PropertyType => PropertyType.Bool;
public BoolProperty(string id, string name, bool initialValue) : base(id, name, initialValue) { }
}
public class ColorProperty : PropertyItem<Color>
{
public override PropertyType PropertyType => PropertyType.Color;
public ColorProperty(string id, string name, Color initialValue) : base(id, name, initialValue) { }
}
public class DateProperty : PropertyItem<DateTime>
{
public override PropertyType PropertyType => PropertyType.Date;
public DateProperty(string id, string name, DateTime initialValue) : base(id, name, initialValue) { }
}
public class DateTimeProperty : PropertyItem<DateTime>
{
public override PropertyType PropertyType => PropertyType.DateTime;
public DateTimeProperty(string id, string name, DateTime initialValue) : base(id, name, initialValue) { }
}
// --- 복합 타입 속성 ---
public class Vector2Property : PropertyItem<Vector2>
{
public override PropertyType PropertyType => PropertyType.Vector2;
/// <summary>
/// 자식 필드의 이름 목록 (예: "X", "Y")
/// </summary>
public List<string> ChildNames { get; set; } = new List<string> { "X", "Y" };
public Vector2Property(string id, string name, Vector2 initialValue) : base(id, name, initialValue) { }
}
public class Vector3Property : PropertyItem<Vector3>
{
public override PropertyType PropertyType => PropertyType.Vector3;
/// <summary>
/// 자식 필드의 이름 목록 (예: "X", "Y", "Z")
/// </summary>
public List<string> ChildNames { get; set; } = new List<string> { "X", "Y", "Z" };
public Vector3Property(string id, string name, Vector3 initialValue) : base(id, name, initialValue) { }
}
// --- 범위 타입 속성 ---
public class IntRangeProperty : PropertyItem<int>
{
public override PropertyType PropertyType => PropertyType.IntRange;
public int Value2 { get; set; }
public IntRangeProperty(string id, string name, int startValue, int endValue) : base(id, name, startValue)
{
Value2 = endValue;
}
}
public class FloatRangeProperty : PropertyItem<float>
{
public override PropertyType PropertyType => PropertyType.FloatRange;
public float Value2 { get; set; }
public FloatRangeProperty(string id, string name, float startValue, float endValue) : base(id, name, startValue)
{
Value2 = endValue;
}
}
public class DateRangeProperty : PropertyItem<DateTime>
{
public override PropertyType PropertyType => PropertyType.DateRange;
public DateTime Value2 { get; set; }
public DateRangeProperty(string id, string name, DateTime startValue, DateTime endValue) : base(id, name, startValue)
{
Value2 = endValue;
}
}
public class DateTimeRangeProperty : PropertyItem<DateTime>
{
public override PropertyType PropertyType => PropertyType.DateRange;
public DateTime Value2 { get; set; }
public DateTimeRangeProperty(string id, string name, DateTime startValue, DateTime endValue) : base(id, name, startValue)
{
Value2 = endValue;
}
}
// --- 열거형 및 목록 타입 속성 ---
public class EnumProperty : PropertyItem<Enum>
{
public override PropertyType PropertyType => PropertyType.Enum;
/// <summary>
/// UI에서 드롭다운 목록을 채우기 위한 열거형의 타입 정보
/// </summary>
public Type EnumType { get; }
public EnumProperty(string id, string name, Enum initialValue) : base(id, name, initialValue)
{
EnumType = initialValue.GetType();
}
}
public class ListProperty : PropertyItem<string> // 값은 선택된 항목의 값을 저장
{
public override PropertyType PropertyType => PropertyType.DropdownList;
/// <summary>
/// 드롭다운에 표시될 항목 목록
/// </summary>
public List<string> ItemsSource { get; }
public ListProperty(string id, string name, List<string> items, string initialValue) : base(id, name, initialValue)
{
ItemsSource = items ?? new List<string>();
}
}
public class RadioGroupProperty : PropertyItem<string> // 값은 선택된 항목의 값을 저장
{
public override PropertyType PropertyType => PropertyType.RadioGroup;
/// <summary>
/// 드롭다운에 표시될 항목 목록
/// </summary>
public List<string> ItemsSource { get; }
public RadioGroupProperty(string id, string name, List<string> items, string initialValue) : base(id, name, initialValue)
{
ItemsSource = items ?? new List<string>();
}
}
#endregion
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 679e64cd5c69c84498c8f831db40aee1

View File

@@ -0,0 +1,166 @@
using UnityEngine;
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("Property UI Prefabs")]
[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>
/// View가 상호작용할 Controller 인스턴스입니다.
/// </summary>
private PropertyWindow _controller;
/// <summary>
/// Controller를 View에 설정하고 UI를 초기화합니다.
/// </summary>
/// <param name="controller">사용할 PropertyWindow</param>
public void Initialize(PropertyWindow controller)
{
_controller = controller;
// Controller가 null이 아니면, 이벤트 핸들러를 등록하고 UI를 그립니다.
if (_controller != null)
{
_controller.PropertyValueChanged += OnPropertyValueChanged;
DrawProperties();
}
}
/// <summary>
/// Controller에 있는 속성 목록을 기반으로 UI를 생성합니다.
/// </summary>
private void DrawProperties()
{
// UI를 다시 그리기 전에 기존에 생성된 모든 자식 오브젝트를 삭제합니다.
foreach (Transform child in _container)
{
Destroy(child.gameObject);
}
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);
// 생성된 UI 인스턴스에서 IPropertyUI 컴포넌트를 찾아 Setup을 호출합니다.
var propertyUI = uiInstance.GetComponent<IPropertyUI>();
if (propertyUI != null)
{
propertyUI.Setup(propertyItem, _controller);
}
else
{
Debug.LogError($"[PropertyView] 프리팹 '{prefab.name}'에 IPropertyUI를 구현한 스크립트가 없습니다.");
}
}
else
{
Debug.LogWarning($"[PropertyView] '{propertyItem.PropertyType}' 타입에 대한 UI 프리팹이 지정되지 않았습니다.");
}
}
}
/// <summary>
/// 속성 타입에 맞는 UI 프리팹을 반환합니다.
/// 실제 구현에서는 더 많은 case가 필요합니다.
/// </summary>
private GameObject GetPrefabForProperty(PropertyType type)
{
switch (type)
{
case PropertyType.String:
return _stringPropertyPrefab;
case PropertyType.Int:
return _numberPropertyPrefab;
case PropertyType.Float:
return _numberPropertyPrefab;
case PropertyType.Bool:
return _boolPropertyPrefab;
case PropertyType.Vector2:
return _vector2PropertyPrefab;
case PropertyType.Vector3:
return _vector3PropertyPrefab;
case PropertyType.Color:
return _colorPropertyPrefab;
case PropertyType.Date:
return _datePropertyPrefab;
case PropertyType.DateTime:
return _dateTimePropertyPrefab;
case PropertyType.Enum:
return _enumPropertyPrefab;
case PropertyType.DropdownList:
return _listPropertyPrefab;
case PropertyType.RadioGroup:
return _radioGroupPropertyPrefab;
case PropertyType.IntRange:
return _numberRangePropertyPrefab;
case PropertyType.FloatRange:
return _numberRangePropertyPrefab;
case PropertyType.DateRange:
return _dateRangePropertyPrefab;
case PropertyType.DateTimeRange:
return _dateTimeRangePropertyPrefab;
default:
Debug.LogWarning($"'{type}' 타입에 대한 프리팹이 정의되지 않았습니다.");
return null;
}
}
/// <summary>
/// Controller에서 PropertyValueChanged 이벤트가 발생했을 때 호출되는 핸들러입니다.
/// </summary>
private void OnPropertyValueChanged(object sender, PropertyValueChangedEventArgs e)
{
Debug.Log($"[PropertyView] 속성 변경 감지: ID='{e.PropertyId}', 이전 값='{e.OldValue}', 새 값='{e.NewValue}'");
// 여기서 특정 속성 값의 변경에 따라 다른 UI를 업데이트하는 로직을 추가할 수 있습니다.
// 예: 특정 bool 속성이 false가 되면 다른 속성 UI를 비활성화 처리
// DrawProperties(); // 전체를 다시 그리는 가장 간단하지만 비효율적일 수 있는 방법
}
private void OnDestroy()
{
// 오브젝트가 파괴될 때 이벤트 핸들러를 안전하게 해제합니다. (메모리 누수 방지)
if (_controller != null)
{
_controller.PropertyValueChanged -= OnPropertyValueChanged;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e111cda3d9984e74d8c84723e40671af

View File

@@ -0,0 +1,108 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UVC.Core;
namespace UVC.UI.Window.PropertyWindow
{
/// <summary>
/// 속성 데이터를 관리하고, 데이터 변경 시 이벤트를 발생시키는 컨트롤러 클래스입니다.
/// Model과 View 사이의 중재자 역할을 합니다.
/// </summary>
public class PropertyWindow: SingletonScene<PropertyWindow>
{
[SerializeField]
private PropertyView _view;
/// <summary>
/// 현재 컨트롤러가 관리하는 모든 속성 항목의 목록입니다.
/// </summary>
public List<IPropertyItem> Properties { get; private set; } = new List<IPropertyItem>();
/// <summary>
/// 속성 값이 변경될 때 발생하는 이벤트입니다.
/// View는 이 이벤트를 구독하여 UI를 업데이트할 수 있습니다.
/// </summary>
public event EventHandler<PropertyValueChangedEventArgs>? PropertyValueChanged;
/// <summary>
/// 새로운 속성 목록을 로드하고 초기화합니다.
/// </summary>
/// <param name="items">표시할 속성 항목들의 목록</param>
public void LoadProperties(List<IPropertyItem> items)
{
Properties = items ?? new List<IPropertyItem>();
// 필요하다면 여기서 추가적인 초기화 로직을 수행할 수 있습니다.
if(_view != null) _view.Initialize(this);
}
/// <summary>
/// 특정 ID를 가진 속성의 값을 업데이트합니다.
/// 이 메서드는 주로 View에서 사용자 입력이 발생했을 때 호출됩니다.
/// </summary>
/// <param name="propertyId">값을 변경할 속성의 고유 ID</param>
/// <param name="propertyType">속성의 타입</param>
/// <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)
{
// 해당 ID의 속성이 없으면 오류를 기록하고 반환합니다.
UnityEngine.Debug.LogError($"[PropertyWindow] ID '{propertyId}'에 해당하는 속성을 찾을 수 없습니다.");
return;
}
// 이전 값을 저장합니다.
object oldValue = propertyItem.GetValue();
// 새 값과 이전 값이 같은지 확인합니다. (불필요한 이벤트 발생 방지)
if (Equals(oldValue, newValue))
{
return;
}
// 속성 객체의 값을 새로운 값으로 설정합니다.
propertyItem.SetValue(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));
}
public bool IsVisible()
{
return gameObject.activeSelf;
}
public void Show()
{
gameObject.SetActive(true);
}
public void Hide()
{
gameObject.SetActive(false);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1bbe888d699e08c41bd01f39bb566480

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 938420c6f0443964fb0836a2d2389af1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,98 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extention;
using UVC.UI.Tooltip;
namespace UVC.UI.Window.PropertyWindow.UI
{
/// <summary>
/// BoolProperty를 위한 UI를 제어하는 스크립트입니다.
/// Toggle 컴포넌트를 사용하여 boolean 값을 표현합니다.
/// </summary>
[RequireComponent(typeof(LayoutElement))]
public class BoolPropertyUI : MonoBehaviour, IPropertyUI
{
[Header("UI Components")]
[SerializeField]
private TextMeshProUGUI _nameLabel; // 속성 이름을 표시할 Text 컴포넌트
[SerializeField]
private TextMeshProUGUI _descriptionLabel;
[SerializeField]
private Toggle _valueToggle; // boolean 값을 표시하고 수정할 Toggle 컴포넌트
private IPropertyItem _propertyItem;
private PropertyWindow _controller;
/// <summary>
/// PropertyView에 의해 호출되어 UI를 초기화하고 데이터를 설정합니다.
/// </summary>
/// <param name="item">UI에 표시할 속성 데이터(IPropertyItem)</param>
/// <param name="controller">상호작용할 PropertyWindow</param>
public void Setup(IPropertyItem item, PropertyWindow controller)
{
_propertyItem = item;
_controller = controller;
// --- 데이터 바인딩 ---
// 1. 속성 이름 설정
_nameLabel.text = _propertyItem.Name;
// 툴팁 설정 (TooltipHandler 컴포넌트가 있다면 연동)
TooltipHandler tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
{
tooltipHandler.Tooltip = _propertyItem.Tooltip;
}
if (_propertyItem.Description.IsNullOrEmpty())
{
_descriptionLabel.gameObject.SetActive(false);
}
else
{
_descriptionLabel.gameObject.SetActive(true);
_descriptionLabel.text = _propertyItem.Description;
}
// 2. Toggle의 현재 값 설정 (object를 bool로 캐스팅)
if (_propertyItem.GetValue() is bool value)
{
_valueToggle.isOn = value;
}
// 3. 읽기 전용 상태에 따라 Toggle의 상호작용 여부 결정
_valueToggle.interactable = !_propertyItem.IsReadOnly;
// --- 이벤트 리스너 등록 ---
// 기존 리스너를 모두 제거한 후 새로 추가하여 중복 등록을 방지합니다.
_valueToggle.onValueChanged.RemoveAllListeners();
_valueToggle.onValueChanged.AddListener(OnValueChanged);
}
/// <summary>
/// 사용자가 Toggle의 값을 변경했을 때 호출되는 콜백 메서드입니다.
/// </summary>
/// <param name="newValue">Toggle의 새로운 상태 (true/false)</param>
private void OnValueChanged(bool newValue)
{
// PropertyController를 통해 모델의 값을 업데이트합니다.
// 이 호출은 PropertyValueChanged 이벤트를 발생시킵니다.
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// 메모리 누수를 방지하기 위해 등록된 리스너를 제거합니다.
/// </summary>
private void OnDestroy()
{
if (_valueToggle != null)
{
_valueToggle.onValueChanged.RemoveAllListeners();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1045178b4c433544eb2d14c4e3ee7f57

View File

@@ -0,0 +1,137 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extention;
using UVC.UI.Modal.ColorPicker;
using UVC.UI.Tooltip;
using UVC.Util;
namespace UVC.UI.Window.PropertyWindow.UI
{
/// <summary>
/// ColorProperty를 위한 UI를 제어하는 스크립트입니다.
/// Image 컴포넌트로 색상을 표시하고, Button으로 색상 선택기를 엽니다.
/// </summary>
[RequireComponent(typeof(LayoutElement))]
public class ColorPropertyUI : MonoBehaviour, IPropertyUI
{
[Header("UI Components")]
[SerializeField]
private TextMeshProUGUI _nameLabel; // 속성 이름을 표시할 Text
[SerializeField]
private TextMeshProUGUI _descriptionLabel;
[SerializeField]
private Image _colorPreviewImage; // 현재 색상을 표시할 Image
[SerializeField]
private TMP_InputField _colorLabel;
[SerializeField]
private Button _colorPickerButton; // 색상 선택기를 열기 위한 Button
private IPropertyItem _propertyItem;
private PropertyWindow _controller;
private bool openningColorPickered = false;
/// <summary>
/// PropertyView에 의해 호출되어 UI를 초기화하고 데이터를 설정합니다.
/// </summary>
/// <param name="item">UI에 표시할 속성 데이터(IPropertyItem)</param>
/// <param name="controller">상호작용할 PropertyWindow</param>
public void Setup(IPropertyItem item, PropertyWindow controller)
{
_propertyItem = item;
_controller = controller;
// --- 데이터 바인딩 ---
// 1. 속성 이름 설정
_nameLabel.text = _propertyItem.Name;
// 툴팁 설정 (TooltipHandler 컴포넌트가 있다면 연동)
TooltipHandler tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
{
tooltipHandler.Tooltip = _propertyItem.Tooltip;
}
if (_propertyItem.Description.IsNullOrEmpty())
{
_descriptionLabel.gameObject.SetActive(false);
}
else
{
_descriptionLabel.gameObject.SetActive(true);
_descriptionLabel.text = _propertyItem.Description;
}
// 2. 색상 미리보기 Image의 색상 설정
if (_propertyItem.GetValue() is Color color)
{
_colorPreviewImage.color = color;
}
_colorLabel.text = ColorUtil.ToHex(_colorPreviewImage.color, true, false);
_colorLabel.interactable = !_propertyItem.IsReadOnly;
// 3. 읽기 전용 상태에 따라 버튼 상호작용 여부 결정
_colorPickerButton.gameObject.SetActive(!_propertyItem.IsReadOnly);
_colorPickerButton.onClick.RemoveAllListeners();
if (!_propertyItem.IsReadOnly)
{
// --- 이벤트 리스너 등록 ---
_colorPickerButton.onClick.AddListener(OpenColorPicker);
}
}
/// <summary>
/// 색상 선택기 버튼을 클릭했을 때 호출됩니다.
/// </summary>
private void OpenColorPicker()
{
if(openningColorPickered == true) return;
openningColorPickered = true;
// TODO: 여기에 프로젝트에 맞는 실제 색상 선택기(Color Picker)를 여는 코드를 구현해야 합니다.
// 색상 선택기는 보통 패널 형태로 구현되며, 선택 완료 시 콜백으로 새로운 색상 값을 반환합니다.
//
// 예시:
_ = ColorPicker.Create(_colorPreviewImage.color, "Color Picker", null, OnColorSelected, OnCloseColorPicker, true);
Debug.LogWarning($"'{_propertyItem.Name}'의 색상 선택기 로직이 구현되지 않았습니다. 클릭 이벤트만 발생합니다.");
}
/// <summary>
/// 색상 선택기에서 새로운 색상이 선택되었을 때 호출되는 메서드입니다.
/// </summary>
/// <param name="newColor">선택된 새로운 색상</param>
public void OnColorSelected(Color newColor)
{
// 1. UI의 색상 미리보기를 업데이트합니다.
_colorPreviewImage.color = newColor;
_colorLabel.text = ColorUtil.ToHex(_colorPreviewImage.color, true, false);
// 2. PropertyController를 통해 데이터 모델의 값을 업데이트합니다.
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newColor);
}
private void OnCloseColorPicker()
{
openningColorPickered = false;
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>
private void OnDestroy()
{
if (_colorPickerButton != null && !_propertyItem.IsReadOnly)
{
_colorPickerButton.onClick.RemoveAllListeners();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b034c34b9d600df4f90b3376ed845c36

View File

@@ -0,0 +1,116 @@
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extention;
using UVC.UI.Modal;
using UVC.UI.Modal.DatePicker;
using UVC.UI.Tooltip; // DatePickerManager가 위치할 네임스페이스
namespace UVC.UI.Window.PropertyWindow.UI
{
/// <summary>
/// DateProperty를 위한 UI를 제어하는 스크립트입니다.
/// 날짜를 텍스트로 표시하고, 버튼을 통해 Date Picker를 엽니다.
/// </summary>
[RequireComponent(typeof(LayoutElement))]
public class DatePropertyUI : MonoBehaviour, IPropertyUI
{
[Header("UI Components")]
[SerializeField]
private TextMeshProUGUI _nameLabel; // 속성 이름을 표시할 Text
[SerializeField]
private TextMeshProUGUI _descriptionLabel;
[SerializeField]
private TextMeshProUGUI _dateText; // 선택된 날짜를 표시할 Text
[SerializeField]
private Button _datePickerButton; // Date Picker를 열기 위한 Button
private IPropertyItem _propertyItem;
private PropertyWindow _controller;
private const string DateFormat = "yyyy-MM-dd"; // 날짜 표시 형식
/// <summary>
/// PropertyView에 의해 호출되어 UI를 초기화하고 데이터를 설정합니다.
/// </summary>
/// <param name="item">UI에 표시할 속성 데이터(IPropertyItem)</param>
/// <param name="controller">상호작용할 PropertyWindow</param>
public void Setup(IPropertyItem item, PropertyWindow controller)
{
_propertyItem = item;
_controller = controller;
// --- 데이터 바인딩 ---
_nameLabel.text = _propertyItem.Name;
// 툴팁 설정 (TooltipHandler 컴포넌트가 있다면 연동)
TooltipHandler tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
{
tooltipHandler.Tooltip = _propertyItem.Tooltip;
}
if (_propertyItem.Description.IsNullOrEmpty())
{
_descriptionLabel.gameObject.SetActive(false);
}
else
{
_descriptionLabel.gameObject.SetActive(true);
_descriptionLabel.text = _propertyItem.Description;
}
if (_propertyItem.GetValue() is DateTime dateTime)
{
_dateText.text = dateTime.ToString(DateFormat);
}
_datePickerButton.interactable = !_propertyItem.IsReadOnly;
// --- 이벤트 리스너 등록 ---
_datePickerButton.onClick.RemoveAllListeners();
_datePickerButton.onClick.AddListener(OpenDatePicker);
}
/// <summary>
/// 날짜 선택기 버튼을 클릭했을 때 호출됩니다.
/// </summary>
private void OpenDatePicker()
{
if (_propertyItem.GetValue() is DateTime currentDateTime)
{
DatePicker.Show(currentDateTime, (newDate) =>
{
OnDateSelected(newDate);
});
}
}
/// <summary>
/// 날짜 선택기에서 새로운 날짜가 선택되었을 때 호출되는 메서드입니다.
/// </summary>
/// <param name="newDate">선택된 새로운 날짜</param>
public void OnDateSelected(DateTime newDate)
{
// 1. UI의 날짜 텍스트를 업데이트합니다.
_dateText.text = newDate.ToString(DateFormat);
// 2. PropertyController를 통해 데이터 모델의 값을 업데이트합니다.
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newDate);
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>
private void OnDestroy()
{
if (_datePickerButton != null)
{
_datePickerButton.onClick.RemoveAllListeners();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b2c41f469f7093b4ba439695a110610b

View File

@@ -0,0 +1,151 @@
using System;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extention;
using UVC.UI.Modal;
using UVC.UI.Modal.DatePicker;
using UVC.UI.Tooltip;
namespace UVC.UI.Window.PropertyWindow.UI
{
/// <summary>
/// DateRangeProperty를 위한 UI를 제어하는 스크립트입니다.
/// 범위의 시작 날짜와 끝 날짜를 별도의 버튼으로 관리합니다.
/// </summary>
[RequireComponent(typeof(LayoutElement))]
public class DateRangePropertyUI : MonoBehaviour, IPropertyUI
{
[Header("UI Components")]
[SerializeField]
private TextMeshProUGUI _nameLabel; // 속성 전체 이름
[SerializeField]
private TextMeshProUGUI _descriptionLabel;
[SerializeField]
private TextMeshProUGUI _startDateText; // 시작 날짜 표시
[SerializeField]
private Button _startDatePickerButton; // 시작 날짜 선택 버튼
[SerializeField]
private TextMeshProUGUI _endDateText; // 끝 날짜 표시
[SerializeField]
private Button _endDatePickerButton; // 끝 날짜 선택 버튼
private DateRangeProperty _propertyItem;
private PropertyWindow _controller;
private const string DateFormat = "yyyy-MM-dd"; // 날짜 표시 형식
/// <summary>
/// PropertyView에 의해 호출되어 UI를 초기화하고 데이터를 설정합니다.
/// </summary>
public void Setup(IPropertyItem item, PropertyWindow controller)
{
if (!(item is DateRangeProperty dateRangeItem))
{
Debug.LogError("DateRangePropertyUI에 잘못된 타입의 PropertyItem이 전달되었습니다.");
return;
}
_propertyItem = dateRangeItem;
_controller = controller;
// --- 데이터 바인딩 ---
_nameLabel.text = _propertyItem.Name;
// 툴팁 설정 (TooltipHandler 컴포넌트가 있다면 연동)
TooltipHandler tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
{
tooltipHandler.Tooltip = _propertyItem.Tooltip;
}
if (_propertyItem.Description.IsNullOrEmpty())
{
_descriptionLabel.gameObject.SetActive(false);
}
else
{
_descriptionLabel.gameObject.SetActive(true);
_descriptionLabel.text = _propertyItem.Description;
}
// 초기 값 설정
_startDateText.text = _propertyItem.Value.ToString(DateFormat);
_endDateText.text = _propertyItem.Value2.ToString(DateFormat);
// 읽기 전용 상태 설정
bool isReadOnly = _propertyItem.IsReadOnly;
_startDatePickerButton.interactable = !isReadOnly;
_endDatePickerButton.interactable = !isReadOnly;
// --- 이벤트 리스너 등록 ---
_startDatePickerButton.onClick.RemoveAllListeners();
_endDatePickerButton.onClick.RemoveAllListeners();
_startDatePickerButton.onClick.AddListener(OpenStartDatePicker);
_endDatePickerButton.onClick.AddListener(OpenEndDatePicker);
}
private void OpenStartDatePicker()
{
DatePicker.Show(_propertyItem.Value, OnStartDateSelected);
}
private void OpenEndDatePicker()
{
DatePicker.Show(_propertyItem.Value2, OnEndDateSelected);
}
private void OnStartDateSelected(DateTime newStartDate)
{
var oldValue = new Tuple<DateTime, DateTime>(_propertyItem.Value, _propertyItem.Value2);
// 시작 날짜가 끝 날짜보다 늦어지지 않도록 보정
if (newStartDate > _propertyItem.Value2)
{
_propertyItem.Value2 = newStartDate;
}
_propertyItem.Value = newStartDate;
var newValue = new Tuple<DateTime, DateTime>(_propertyItem.Value, _propertyItem.Value2);
// UI 업데이트
_startDateText.text = _propertyItem.Value.ToString(DateFormat);
_endDateText.text = _propertyItem.Value2.ToString(DateFormat);
// 변경 이벤트 알림
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
}
private void OnEndDateSelected(DateTime newEndDate)
{
var oldValue = new Tuple<DateTime, DateTime>(_propertyItem.Value, _propertyItem.Value2);
// 끝 날짜가 시작 날짜보다 빨라지지 않도록 보정
if (newEndDate < _propertyItem.Value)
{
_propertyItem.Value = newEndDate;
}
_propertyItem.Value2 = newEndDate;
var newValue = new Tuple<DateTime, DateTime>(_propertyItem.Value, _propertyItem.Value2);
// UI 업데이트
_startDateText.text = _propertyItem.Value.ToString(DateFormat);
_endDateText.text = _propertyItem.Value2.ToString(DateFormat);
// 변경 이벤트 알림
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>
private void OnDestroy()
{
if (_startDatePickerButton != null) _startDatePickerButton.onClick.RemoveAllListeners();
if (_endDatePickerButton != null) _endDatePickerButton.onClick.RemoveAllListeners();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9f3b80a3851744441acb18967290ce5d

View File

@@ -0,0 +1,169 @@
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extention;
using UVC.UI.Modal;
using UVC.UI.Modal.DatePicker;
using UVC.UI.Tooltip; // DateTimePickerManager가 위치할 네임스페이스
namespace UVC.UI.Window.PropertyWindow.UI
{
/// <summary>
/// DateTimeProperty를 위한 UI를 제어하는 스크립트입니다.
/// 날짜와 시간을 텍스트로 표시하고, 버튼을 통해 DateTime Picker를 엽니다.
/// </summary>
[RequireComponent(typeof(LayoutElement))]
public class DateTimePropertyUI : MonoBehaviour, IPropertyUI
{
[Header("UI Components")]
[SerializeField]
private TextMeshProUGUI _nameLabel; // 속성 이름을 표시할 Text
[SerializeField]
private TextMeshProUGUI _descriptionLabel;
[SerializeField]
private TextMeshProUGUI _dateText; // 선택된 날짜를 표시할 Text
[SerializeField]
private TMP_Dropdown _hourDropDown; // 선택된 시간을 표시
[SerializeField]
private TMP_Dropdown _miniteDropDown; // 선택된 분을 표시
[SerializeField]
private Button _dateTimePickerButton; // DateTime Picker를 열기 위한 Button
private IPropertyItem _propertyItem;
private PropertyWindow _controller;
private const string DateFormat = "yyyy-MM-ddmm"; // 날짜 표시 형식
/// <summary>
/// PropertyView에 의해 호출되어 UI를 초기화하고 데이터를 설정합니다.
/// </summary>
/// <param name="item">UI에 표시할 속성 데이터(IPropertyItem)</param>
/// <param name="controller">상호작용할 PropertyWindow</param>
public void Setup(IPropertyItem item, PropertyWindow controller)
{
_propertyItem = item;
_controller = controller;
List<string> hourOptions = new List<string>();
List<string> minuteOptions = new List<string>();
for (int i = 0; i < 60; i ++)
{
if (i < 24) hourOptions.Add(i.ToString("D2"));
minuteOptions.Add(i.ToString("D2"));
}
_hourDropDown.AddOptions(hourOptions);
_miniteDropDown.AddOptions(minuteOptions);
// --- 데이터 바인딩 ---
_nameLabel.text = _propertyItem.Name;
// 툴팁 설정 (TooltipHandler 컴포넌트가 있다면 연동)
TooltipHandler tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
{
tooltipHandler.Tooltip = _propertyItem.Tooltip;
}
if (_propertyItem.Description.IsNullOrEmpty())
{
_descriptionLabel.gameObject.SetActive(false);
}
else
{
_descriptionLabel.gameObject.SetActive(true);
_descriptionLabel.text = _propertyItem.Description;
}
if (_propertyItem.GetValue() is DateTime dateTime)
{
_dateText.text = dateTime.ToString(DateFormat);
_hourDropDown.value = dateTime.Hour;
_miniteDropDown.value = dateTime.Minute;
}
_dateTimePickerButton.interactable = !_propertyItem.IsReadOnly;
// --- 이벤트 리스너 등록 ---
_dateTimePickerButton.onClick.RemoveAllListeners();
_dateTimePickerButton.onClick.AddListener(OpenDateTimePicker);
_hourDropDown.onValueChanged.RemoveAllListeners();
_hourDropDown.onValueChanged.AddListener((int hour) => {
if (_propertyItem.GetValue() is DateTime currentDateTime)
{
DateTime newDateTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
hour,
currentDateTime.Minute,
currentDateTime.Second
);
OnDateTimeSelected(newDateTime);
}
});
_miniteDropDown.onValueChanged.RemoveAllListeners();
_miniteDropDown.onValueChanged.AddListener((int minute) => {
if (_propertyItem.GetValue() is DateTime currentDateTime)
{
DateTime newDateTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
currentDateTime.Hour,
minute,
currentDateTime.Second
);
OnDateTimeSelected(newDateTime);
}
});
}
/// <summary>
/// 날짜 및 시간 선택기 버튼을 클릭했을 때 호출됩니다.
/// </summary>
private void OpenDateTimePicker()
{
if (_propertyItem.GetValue() is DateTime currentDateTime)
{
DatePicker.Show(currentDateTime, (newDateTime) =>
{
OnDateTimeSelected(newDateTime);
});
}
}
/// <summary>
/// 날짜 및 시간 선택기에서 새로운 값이 선택되었을 때 호출되는 메서드입니다.
/// </summary>
/// <param name="newDateTime">선택된 새로운 날짜와 시간</param>
public void OnDateTimeSelected(DateTime newDateTime)
{
// 1. UI의 텍스트를 업데이트합니다.
_dateText.text = newDateTime.ToString(DateFormat);
// 2. PropertyController를 통해 데이터 모델의 값을 업데이트합니다.
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newDateTime);
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>
private void OnDestroy()
{
if (_dateTimePickerButton != null)
{
_dateTimePickerButton.onClick.RemoveAllListeners();
}
_hourDropDown.onValueChanged.RemoveAllListeners();
_miniteDropDown.onValueChanged.RemoveAllListeners();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8a4e5b7847b47464d92c82d64bdb5346

View File

@@ -0,0 +1,240 @@
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extention;
using UVC.UI.Modal;
using UVC.UI.Modal.DatePicker;
using UVC.UI.Tooltip;
namespace UVC.UI.Window.PropertyWindow.UI
{
/// <summary>
/// DateTimeRangeProperty를 위한 UI를 제어하는 스크립트입니다.
/// 범위의 시작 날짜/시간과 끝 날짜/시간을 별도의 버튼으로 관리합니다.
/// </summary>
[RequireComponent(typeof(LayoutElement))]
public class DateTimeRangePropertyUI : MonoBehaviour, IPropertyUI
{
[Header("UI Components")]
[SerializeField]
private TextMeshProUGUI _nameLabel; // 속성 전체 이름
[SerializeField]
private TextMeshProUGUI _descriptionLabel;
[SerializeField]
private TextMeshProUGUI _startDateText; // 시작 날짜 표시
[SerializeField]
private Button _startDatePickerButton; // 시작 날짜/시간 선택 버튼
[SerializeField]
private TMP_Dropdown _startHourDropDown; // 선택된 시간을 표시
[SerializeField]
private TMP_Dropdown _startMiniteDropDown; // 선택된 분을 표시
[SerializeField]
private TextMeshProUGUI _endDateText; // 끝 날짜/시간 표시
[SerializeField]
private Button _endDatePickerButton; // 끝 날짜/시간 선택 버튼
[SerializeField]
private TMP_Dropdown _endHourDropDown; // 선택된 시간을 표시
[SerializeField]
private TMP_Dropdown _endMiniteDropDown; // 선택된 분을 표시
private DateTimeRangeProperty _propertyItem;
private PropertyWindow _controller;
private const string DateFormat = "yyyy-MM-dd"; // 날짜 표시 형식
/// <summary>
/// PropertyView에 의해 호출되어 UI를 초기화하고 데이터를 설정합니다.
/// </summary>
public void Setup(IPropertyItem item, PropertyWindow controller)
{
if (!(item is DateTimeRangeProperty dateTimeRangeItem))
{
Debug.LogError("DateTimeRangePropertyUI에 잘못된 타입의 PropertyItem이 전달되었습니다.");
return;
}
_propertyItem = dateTimeRangeItem;
_controller = controller;
List<string> hourOptions = new List<string>();
List<string> minuteOptions = new List<string>();
for (int i = 0; i < 60; i++)
{
if (i < 24) hourOptions.Add(i.ToString("D2"));
minuteOptions.Add(i.ToString("D2"));
}
_startHourDropDown.AddOptions(hourOptions);
_startMiniteDropDown.AddOptions(minuteOptions);
_endHourDropDown.AddOptions(hourOptions);
_endMiniteDropDown.AddOptions(minuteOptions);
// --- 데이터 바인딩 ---
_nameLabel.text = _propertyItem.Name;
// 툴팁 설정 (TooltipHandler 컴포넌트가 있다면 연동)
TooltipHandler tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
{
tooltipHandler.Tooltip = _propertyItem.Tooltip;
}
if (_propertyItem.Description.IsNullOrEmpty())
{
_descriptionLabel.gameObject.SetActive(false);
}
else
{
_descriptionLabel.gameObject.SetActive(true);
_descriptionLabel.text = _propertyItem.Description;
}
// 초기 값 설정
_startDateText.text = _propertyItem.Value.ToString(DateFormat);
_endDateText.text = _propertyItem.Value2.ToString(DateFormat);
// 읽기 전용 상태 설정
bool isReadOnly = _propertyItem.IsReadOnly;
_startDatePickerButton.interactable = !isReadOnly;
_endDatePickerButton.interactable = !isReadOnly;
// --- 이벤트 리스너 등록 ---
_startDatePickerButton.onClick.RemoveAllListeners();
_endDatePickerButton.onClick.RemoveAllListeners();
_startDatePickerButton.onClick.AddListener(OpenStartDateTimePicker);
_endDatePickerButton.onClick.AddListener(OpenEndDateTimePicker);
_startHourDropDown.onValueChanged.RemoveAllListeners();
_startHourDropDown.onValueChanged.AddListener((int hour) => {
if (_propertyItem.GetValue() is DateTime currentDateTime)
{
DateTime newDateTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
hour,
currentDateTime.Minute,
currentDateTime.Second
);
OnStartDateTimeSelected(newDateTime);
}
});
_startMiniteDropDown.onValueChanged.RemoveAllListeners();
_startMiniteDropDown.onValueChanged.AddListener((int minute) => {
if (_propertyItem.GetValue() is DateTime currentDateTime)
{
DateTime newDateTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
currentDateTime.Hour,
minute,
currentDateTime.Second
);
OnStartDateTimeSelected(newDateTime);
}
});
_endHourDropDown.onValueChanged.RemoveAllListeners();
_endHourDropDown.onValueChanged.AddListener((int hour) => {
if (_propertyItem.GetValue() is DateTime currentDateTime)
{
DateTime newDateTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
hour,
currentDateTime.Minute,
currentDateTime.Second
);
OnEndDateTimeSelected(newDateTime);
}
});
_endMiniteDropDown.onValueChanged.RemoveAllListeners();
_endMiniteDropDown.onValueChanged.AddListener((int minute) => {
if (_propertyItem.GetValue() is DateTime currentDateTime)
{
DateTime newDateTime = new DateTime(
currentDateTime.Year,
currentDateTime.Month,
currentDateTime.Day,
currentDateTime.Hour,
minute,
currentDateTime.Second
);
OnEndDateTimeSelected(newDateTime);
}
});
}
private void OpenStartDateTimePicker()
{
DatePicker.Show(_propertyItem.Value, OnStartDateTimeSelected);
Debug.LogWarning($"'{_propertyItem.Name}'의 시작 날짜/시간 선택기 로직이 구현되지 않았습니다.");
}
private void OpenEndDateTimePicker()
{
DatePicker.Show(_propertyItem.Value2, OnEndDateTimeSelected);
Debug.LogWarning($"'{_propertyItem.Name}'의 끝 날짜/시간 선택기 로직이 구현되지 않았습니다.");
}
private void OnStartDateTimeSelected(DateTime newStartDateTime)
{
var oldValue = new Tuple<DateTime, DateTime>(_propertyItem.Value, _propertyItem.Value2);
// 시작 날짜/시간이 끝 날짜/시간보다 늦어지지 않도록 보정
if (newStartDateTime > _propertyItem.Value2)
{
_propertyItem.Value2 = newStartDateTime;
}
_propertyItem.Value = newStartDateTime;
var newValue = new Tuple<DateTime, DateTime>(_propertyItem.Value, _propertyItem.Value2);
// UI 업데이트
_startDateText.text = _propertyItem.Value.ToString(DateFormat);
_endDateText.text = _propertyItem.Value2.ToString(DateFormat);
// 변경 이벤트 알림
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
}
private void OnEndDateTimeSelected(DateTime newEndDateTime)
{
var oldValue = new Tuple<DateTime, DateTime>(_propertyItem.Value, _propertyItem.Value2);
// 끝 날짜/시간이 시작 날짜/시간보다 빨라지지 않도록 보정
if (newEndDateTime < _propertyItem.Value)
{
_propertyItem.Value = newEndDateTime;
}
_propertyItem.Value2 = newEndDateTime;
var newValue = new Tuple<DateTime, DateTime>(_propertyItem.Value, _propertyItem.Value2);
// UI 업데이트
_startDateText.text = _propertyItem.Value.ToString(DateFormat);
_endDateText.text = _propertyItem.Value2.ToString(DateFormat);
// 변경 이벤트 알림
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>
private void OnDestroy()
{
if (_startDatePickerButton != null) _startDatePickerButton.onClick.RemoveAllListeners();
if (_endDatePickerButton != null) _endDatePickerButton.onClick.RemoveAllListeners();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c75e03e8867b032418c0e91aa9278066

View File

@@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extention;
using UVC.UI.Tooltip;
namespace UVC.UI.Window.PropertyWindow.UI
{
/// <summary>
/// EnumProperty를 위한 UI를 제어하는 스크립트입니다.
/// TMP_Dropdown을 사용하여 열거형 값을 선택합니다.
/// </summary>
[RequireComponent(typeof(LayoutElement))]
public class EnumPropertyUI : MonoBehaviour, IPropertyUI
{
[Header("UI Components")]
[SerializeField]
private TextMeshProUGUI _nameLabel; // 속성 이름을 표시할 Text
[SerializeField]
private TextMeshProUGUI _descriptionLabel;
[SerializeField]
private TMP_Dropdown _dropdown; // 열거형 값을 선택할 Dropdown
private EnumProperty _propertyItem;
private PropertyWindow _controller;
private Array _enumValues; // Enum.GetValues() 결과를 캐싱하여 성능 향상
/// <summary>
/// PropertyView에 의해 호출되어 UI를 초기화하고 데이터를 설정합니다.
/// </summary>
public void Setup(IPropertyItem item, PropertyWindow controller)
{
if (!(item is EnumProperty enumItem))
{
Debug.LogError("EnumPropertyUI에 잘못된 타입의 PropertyItem이 전달되었습니다.");
return;
}
_propertyItem = enumItem;
_controller = controller;
// --- 데이터 바인딩 ---
_nameLabel.text = _propertyItem.Name;
// 툴팁 설정 (TooltipHandler 컴포넌트가 있다면 연동)
TooltipHandler tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
{
tooltipHandler.Tooltip = _propertyItem.Tooltip;
}
if (_propertyItem.Description.IsNullOrEmpty())
{
_descriptionLabel.gameObject.SetActive(false);
}
else
{
_descriptionLabel.gameObject.SetActive(true);
_descriptionLabel.text = _propertyItem.Description;
}
// Dropdown 옵션 초기화
_dropdown.ClearOptions();
// Enum 타입에서 이름 목록을 가져와 Dropdown 옵션으로 추가
var enumNames = Enum.GetNames(_propertyItem.EnumType).ToList();
_dropdown.AddOptions(enumNames);
// Enum 값 배열 캐싱
_enumValues = Enum.GetValues(_propertyItem.EnumType);
// 현재 값에 해당하는 인덱스를 찾아 Dropdown의 초기 값으로 설정
int currentIndex = Array.IndexOf(_enumValues, _propertyItem.Value);
if (currentIndex > -1)
{
_dropdown.SetValueWithoutNotify(currentIndex);
}
// 읽기 전용 상태 설정
_dropdown.interactable = !_propertyItem.IsReadOnly;
// --- 이벤트 리스너 등록 ---
_dropdown.onValueChanged.RemoveAllListeners();
_dropdown.onValueChanged.AddListener(OnDropdownValueChanged);
}
/// <summary>
/// 사용자가 Dropdown의 선택 항목을 변경했을 때 호출됩니다.
/// </summary>
/// <param name="index">새로 선택된 항목의 인덱스</param>
private void OnDropdownValueChanged(int index)
{
// 선택된 인덱스에 해당하는 실제 Enum 값을 가져옵니다.
if (index >= 0 && index < _enumValues.Length)
{
var newValue = _enumValues.GetValue(index);
// 컨트롤러를 통해 모델의 값을 업데이트합니다.
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>
private void OnDestroy()
{
if (_dropdown != null)
{
_dropdown.onValueChanged.RemoveAllListeners();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0f628caaf72133a4ea1b8d9e8ea77f55

View File

@@ -0,0 +1,116 @@
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extention;
using UVC.UI.Tooltip;
namespace UVC.UI.Window.PropertyWindow.UI
{
/// <summary>
/// DropdownListProperty를 위한 UI를 제어하는 스크립트입니다.
/// TMP_Dropdown을 사용하여 문자열 목록에서 항목을 선택합니다.
/// </summary>
[RequireComponent(typeof(LayoutElement))]
public class ListPropertyUI : MonoBehaviour, IPropertyUI
{
[Header("UI Components")]
[SerializeField]
private TextMeshProUGUI _nameLabel; // 속성 이름을 표시할 Text
[SerializeField]
private TextMeshProUGUI _descriptionLabel;
[SerializeField]
private TMP_Dropdown _dropdown; // 항목을 선택할 Dropdown
private ListProperty _propertyItem;
private PropertyWindow _controller;
/// <summary>
/// PropertyView에 의해 호출되어 UI를 초기화하고 데이터를 설정합니다.
/// </summary>
public void Setup(IPropertyItem item, PropertyWindow controller)
{
if (!(item is ListProperty dropdownItem))
{
Debug.LogError("DropdownListPropertyUI에 잘못된 타입의 PropertyItem이 전달되었습니다.");
return;
}
_propertyItem = dropdownItem;
_controller = controller;
// --- 데이터 바인딩 ---
_nameLabel.text = _propertyItem.Name;
// 툴팁 설정 (TooltipHandler 컴포넌트가 있다면 연동)
TooltipHandler tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
{
tooltipHandler.Tooltip = _propertyItem.Tooltip;
}
if (_propertyItem.Description.IsNullOrEmpty())
{
_descriptionLabel.gameObject.SetActive(false);
}
else
{
_descriptionLabel.gameObject.SetActive(true);
_descriptionLabel.text = _propertyItem.Description;
}
// Dropdown 옵션 초기화
_dropdown.ClearOptions();
// ItemsSource에서 목록을 가져와 Dropdown 옵션으로 추가
if (_propertyItem.ItemsSource != null)
{
_dropdown.AddOptions(_propertyItem.ItemsSource);
}
// 현재 값에 해당하는 인덱스를 찾아 Dropdown의 초기 값으로 설정
int currentIndex = _propertyItem.ItemsSource?.IndexOf(_propertyItem.Value) ?? -1;
if (currentIndex > -1)
{
_dropdown.SetValueWithoutNotify(currentIndex);
}
// 읽기 전용 상태 설정
_dropdown.interactable = !_propertyItem.IsReadOnly;
// --- 이벤트 리스너 등록 ---
_dropdown.onValueChanged.RemoveAllListeners();
_dropdown.onValueChanged.AddListener(OnDropdownValueChanged);
}
/// <summary>
/// 사용자가 Dropdown의 선택 항목을 변경했을 때 호출됩니다.
/// </summary>
/// <param name="index">새로 선택된 항목의 인덱스</param>
private void OnDropdownValueChanged(int index)
{
// 선택된 인덱스에 해당하는 실제 문자열 값을 가져옵니다.
if (_propertyItem.ItemsSource != null && index >= 0 && index < _propertyItem.ItemsSource.Count)
{
string newValue = _propertyItem.ItemsSource[index];
// 컨트롤러를 통해 모델의 값을 업데이트합니다.
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>
private void OnDestroy()
{
if (_dropdown != null)
{
_dropdown.onValueChanged.RemoveAllListeners();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e2e5cf04729c7e54b88dd90ac330c85e

View File

@@ -0,0 +1,192 @@
using TMPro;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extention;
using UVC.UI.Tooltip;
namespace UVC.UI.Window.PropertyWindow.UI
{
/// <summary>
/// int, float Property를 위한 UI를 제어하는 스크립트입니다.
/// </summary>
[RequireComponent(typeof(LayoutElement))] // UI 레이아웃 관리를 위해 추가
public class NumberPropertyUI : MonoBehaviour, IPropertyUI
{
[SerializeField] private TextMeshProUGUI _nameLabel;
[SerializeField] private TextMeshProUGUI _descriptionLabel;
[SerializeField] private TMP_InputField _valueInput;
[SerializeField] private Slider _slider;
private IPropertyItem _propertyItem;
private PropertyWindow _controller;
/// <summary>
/// PropertyView에 의해 호출되어 UI를 초기화합니다.
/// </summary>
public void Setup(IPropertyItem item, PropertyWindow controller)
{
_propertyItem = item;
_controller = controller;
// --- 데이터 바인딩 ---
// 1. 이름 설정
_nameLabel.text = _propertyItem.Name;
// 툴팁 설정 (TooltipHandler 컴포넌트가 있다면 연동)
TooltipHandler tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
{
tooltipHandler.Tooltip = _propertyItem.Tooltip;
}
if (_propertyItem.Description.IsNullOrEmpty())
{
_descriptionLabel.gameObject.SetActive(false);
}
else
{
_descriptionLabel.gameObject.SetActive(true);
_descriptionLabel.text = _propertyItem.Description;
}
if(item.PropertyType == PropertyType.Int)
{
_valueInput.contentType = TMP_InputField.ContentType.IntegerNumber;
}
else if(item.PropertyType == PropertyType.Float)
{
_valueInput.contentType = TMP_InputField.ContentType.DecimalNumber;
}
if(item is IntProperty intItem)
{
if (intItem.IsSlider && !_propertyItem.IsReadOnly) {
_slider.gameObject.SetActive(true);
_slider.minValue = intItem.MinValue;
_slider.maxValue = intItem.MaxValue;
_slider.wholeNumbers = true;
_slider.value = (int)_propertyItem.GetValue();
_slider.onValueChanged.RemoveAllListeners();
_slider.onValueChanged.AddListener(OnValueChangeSlider);
}
else
{
_slider.gameObject.SetActive(false);
}
}
else if(item is FloatProperty floatItem)
{
if (floatItem.IsSlider && !_propertyItem.IsReadOnly)
{
_slider.gameObject.SetActive(true);
_slider.minValue = floatItem.MinValue;
_slider.maxValue = floatItem.MaxValue;
_slider.wholeNumbers = false;
_slider.value = (float)_propertyItem.GetValue();
_slider.onValueChanged.RemoveAllListeners();
_slider.onValueChanged.AddListener(OnValueChangeSlider);
}
else
{
_slider.gameObject.SetActive(false);
}
}
// 2. 값 설정
string initValue = _propertyItem.GetValue().ToString();
_valueInput.text = initValue;
// 3. 읽기 전용 상태 설정
_valueInput.interactable = !_propertyItem.IsReadOnly;
// 4. 툴팁 설정 (Tooltip 컴포넌트가 있다면 연동)
// 예: var tooltip = GetComponent<TooltipTrigger>();
// if (tooltip != null) tooltip.content = _propertyItem.Tooltip;
// --- 이벤트 리스너 등록 ---
// 기존 리스너를 모두 제거한 후 새로 추가하여 중복 등록을 방지합니다.
_valueInput.onEndEdit.RemoveAllListeners();
_valueInput.onEndEdit.AddListener(OnValueSubmitted);
}
/// <summary>
/// 사용자가 InputField 수정 완료 후 Enter를 누르거나 포커스를 잃었을 때 호출됩니다.
/// </summary>
/// <param name="newValue">InputField에 입력된 새로운 문자열</param>
private void OnValueSubmitted(string newValue)
{
object value = newValue;
if (_propertyItem.PropertyType == PropertyType.Int)
{
if (int.TryParse(newValue, out int intValue))
{
value = intValue;
}
else
{
value = 0;
_valueInput.text = "0";
}
}
else if (_propertyItem.PropertyType == PropertyType.Float)
{
if (float.TryParse(newValue, out float floatValue))
{
value = floatValue;
}
else
{
value = 0f;
_valueInput.text = "0";
}
}
if (_slider.gameObject.activeSelf)
{
// 슬라이더가 활성화된 경우 슬라이더 값도 동기화
if (value is int vi)
{
if(vi > _slider.maxValue) { vi = (int)_slider.maxValue; _valueInput.text = vi.ToString(); }
else if(vi < _slider.minValue) { vi = (int)_slider.minValue; _valueInput.text = vi.ToString(); }
_slider.value = vi;
}
else if (value is float vf)
{
if (vf > _slider.maxValue) { vf = _slider.maxValue; _valueInput.text = vf.ToString(); }
else if (vf < _slider.minValue) { vf = _slider.minValue; _valueInput.text = vf.ToString(); }
_slider.value = vf;
}
}
else
{
// 컨트롤러를 통해 모델의 값을 업데이트합니다.
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, value);
}
}
private void OnValueChangeSlider(float value)
{
object v = value;
if (_propertyItem.PropertyType == PropertyType.Int)
{
v= (int)value;
}
_valueInput.text = value.ToString();
// 컨트롤러를 통해 모델의 값을 업데이트합니다.
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, v);
}
private void OnDestroy()
{
// 오브젝트가 파괴될 때 리스너를 확실히 제거합니다.
if (_valueInput != null)
{
_valueInput.onEndEdit.RemoveAllListeners();
}
if (_slider != null) _slider.onValueChanged.RemoveAllListeners();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 450872122bc4a504ca4b0946a1b50089

View File

@@ -0,0 +1,205 @@
using System;
using System.Globalization;
using TMPro;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extention;
using UVC.UI.Tooltip;
namespace UVC.UI.Window.PropertyWindow.UI
{
/// <summary>
/// Int, Float RangeProperty를 위한 UI를 제어하는 스크립트입니다.
/// 범위의 시작(Min)과 끝(Max) 값을 별도의 InputField로 관리합니다.
/// </summary>
[RequireComponent(typeof(LayoutElement))]
public class NumberRangePropertyUI : MonoBehaviour, IPropertyUI
{
[Header("UI Components")]
[SerializeField]
private TextMeshProUGUI _nameLabel; // 속성 전체 이름
[SerializeField]
private TextMeshProUGUI _descriptionLabel;
[SerializeField]
private TMP_InputField _minInputField; // 최소값 입력 필드
[SerializeField]
private TMP_InputField _maxInputField; // 최대값 입력 필드
private IPropertyItem _propertyItem;
private PropertyWindow _controller;
/// <summary>
/// PropertyView에 의해 호출되어 UI를 초기화하고 데이터를 설정합니다.
/// </summary>
public void Setup(IPropertyItem item, PropertyWindow controller)
{
if (!(item is IntRangeProperty intRangeItem) && !(item is FloatRangeProperty floatRange))
{
Debug.LogError("IntRangePropertyUI에 잘못된 타입의 PropertyItem이 전달되었습니다.");
return;
}
_propertyItem = item;
_controller = controller;
// --- 데이터 바인딩 ---
_nameLabel.text = _propertyItem.Name;
// 툴팁 설정 (TooltipHandler 컴포넌트가 있다면 연동)
TooltipHandler tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
{
tooltipHandler.Tooltip = _propertyItem.Tooltip;
}
if (_propertyItem.Description.IsNullOrEmpty())
{
_descriptionLabel.gameObject.SetActive(false);
}
else
{
_descriptionLabel.gameObject.SetActive(true);
_descriptionLabel.text = _propertyItem.Description;
}
if (item.PropertyType == PropertyType.IntRange)
{
_minInputField.contentType = TMP_InputField.ContentType.IntegerNumber;
_maxInputField.contentType = TMP_InputField.ContentType.IntegerNumber;
// 초기 값 설정
_minInputField.text = (_propertyItem as IntRangeProperty).Value.ToString(CultureInfo.InvariantCulture);
_maxInputField.text = (_propertyItem as IntRangeProperty).Value2.ToString(CultureInfo.InvariantCulture);
}
else if (item.PropertyType == PropertyType.FloatRange)
{
_minInputField.contentType = TMP_InputField.ContentType.DecimalNumber;
_maxInputField.contentType = TMP_InputField.ContentType.DecimalNumber;
// 초기 값 설정
_minInputField.text = (_propertyItem as FloatRangeProperty).Value.ToString(CultureInfo.InvariantCulture);
_maxInputField.text = (_propertyItem as FloatRangeProperty).Value2.ToString(CultureInfo.InvariantCulture);
}
// 읽기 전용 상태 설정
bool isReadOnly = _propertyItem.IsReadOnly;
_minInputField.interactable = !isReadOnly;
_maxInputField.interactable = !isReadOnly;
// --- 이벤트 리스너 등록 ---
_minInputField.onEndEdit.RemoveAllListeners();
_maxInputField.onEndEdit.RemoveAllListeners();
_minInputField.onEndEdit.AddListener(OnValueSubmitted);
_maxInputField.onEndEdit.AddListener(OnValueSubmitted);
}
/// <summary>
/// 사용자가 Min 또는 Max 입력 필드의 수정을 완료했을 때 호출됩니다.
/// </summary>
private void OnValueSubmitted(string input)
{
// 입력 필드의 값을 파싱합니다.
if (_propertyItem.PropertyType == PropertyType.IntRange)
{
OnValueSubmittedInt(input);
}
else if (_propertyItem.PropertyType == PropertyType.FloatRange)
{
OnValueSubmittedFloat(input);
}
}
private void OnValueSubmittedInt(string input)
{
IntRangeProperty propertyItem = _propertyItem as IntRangeProperty;
if (propertyItem == null) return;
int.TryParse(_minInputField.text, out int newMin);
int.TryParse(_maxInputField.text, out int newMax);
// 시작 값이 끝 값보다 크지 않도록 보정합니다.
if (newMin > newMax)
{
newMin = newMax;
}
// 이전 값을 저장합니다.
var oldValue = new Vector2Int(propertyItem.Value, propertyItem.Value2);
var newValue = new Vector2Int(newMin, newMax);
// 값이 변경되었는지 확인합니다.
if (oldValue == newValue)
{
// 값이 변경되지 않았으면 UI만 원래 값으로 복원하고 종료합니다.
_minInputField.text = propertyItem.Value.ToString(CultureInfo.InvariantCulture);
_maxInputField.text = propertyItem.Value2.ToString(CultureInfo.InvariantCulture);
return;
}
// 컨트롤러를 통해 모델의 값을 업데이트합니다.
// 범위 속성은 두 개의 값을 가지므로, 컨트롤러의 UpdatePropertyValue를 직접 사용하지 않고
// 속성 객체의 값을 직접 변경한 후, 변경 이벤트를 수동으로 발생시킵니다.
propertyItem.Value = newMin;
propertyItem.Value2 = newMax;
// 변경 이벤트를 발생시켜 다른 부분에 알립니다.
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
// 보정된 값으로 UI를 다시 업데이트합니다.
_minInputField.text = newMin.ToString(CultureInfo.InvariantCulture);
}
private void OnValueSubmittedFloat(string input)
{
FloatRangeProperty propertyItem = _propertyItem as FloatRangeProperty;
if (propertyItem == null) return;
float.TryParse(_minInputField.text, out float newMin);
float.TryParse(_maxInputField.text, out float newMax);
// 시작 값이 끝 값보다 크지 않도록 보정합니다.
if (newMin > newMax)
{
newMin = newMax;
}
// 이전 값을 저장합니다.
var oldValue = new Vector2(propertyItem.Value, propertyItem.Value2);
var newValue = new Vector2(newMin, newMax);
// 값이 변경되었는지 확인합니다.
if (oldValue == newValue)
{
// 값이 변경되지 않았으면 UI만 원래 값으로 복원하고 종료합니다.
_minInputField.text = propertyItem.Value.ToString(CultureInfo.InvariantCulture);
_maxInputField.text = propertyItem.Value2.ToString(CultureInfo.InvariantCulture);
return;
}
// 컨트롤러를 통해 모델의 값을 업데이트합니다.
// 범위 속성은 두 개의 값을 가지므로, 컨트롤러의 UpdatePropertyValue를 직접 사용하지 않고
// 속성 객체의 값을 직접 변경한 후, 변경 이벤트를 수동으로 발생시킵니다.
propertyItem.Value = newMin;
propertyItem.Value2 = newMax;
// 변경 이벤트를 발생시켜 다른 부분에 알립니다.
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
// 보정된 값으로 UI를 다시 업데이트합니다.
_minInputField.text = newMin.ToString(CultureInfo.InvariantCulture);
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>
private void OnDestroy()
{
if (_minInputField != null) _minInputField.onEndEdit.RemoveAllListeners();
if (_maxInputField != null) _maxInputField.onEndEdit.RemoveAllListeners();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 11ff195ea0a22d94e9c5dc75e1293b58

View File

@@ -0,0 +1,151 @@
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extention;
using UVC.UI.Tooltip;
namespace UVC.UI.Window.PropertyWindow.UI
{
/// <summary>
/// RadioGroupProperty를 위한 UI를 제어하는 스크립트입니다.
/// 동적으로 생성된 Toggle들을 사용하여 항목을 선택합니다.
/// </summary>
[RequireComponent(typeof(LayoutElement))]
public class RadioGroupPropertyUI : MonoBehaviour, IPropertyUI
{
[Header("UI Components")]
[SerializeField]
private TextMeshProUGUI _nameLabel; // 속성 이름을 표시할 Text
[SerializeField]
private TextMeshProUGUI _descriptionLabel;
[SerializeField]
private RectTransform _toggleContainer; // Toggle들이 생성될 부모 컨테이너
[SerializeField]
private GameObject _togglePrefab; // 개별 라디오 버튼(Toggle) 프리팹
private RadioGroupProperty _propertyItem;
private PropertyWindow _controller;
private ToggleGroup _toggleGroup;
private readonly List<Toggle> _toggles = new List<Toggle>();
/// <summary>
/// PropertyView에 의해 호출되어 UI를 초기화하고 데이터를 설정합니다.
/// </summary>
public void Setup(IPropertyItem item, PropertyWindow controller)
{
if (!(item is RadioGroupProperty radioItem))
{
Debug.LogError("RadioGroupPropertyUI에 잘못된 타입의 PropertyItem이 전달되었습니다.");
return;
}
if (_togglePrefab == null)
{
Debug.LogError("RadioGroupPropertyUI에 Toggle 프리팹이 연결되지 않았습니다.");
return;
}
_propertyItem = radioItem;
_controller = controller;
// --- 데이터 바인딩 ---
_nameLabel.text = _propertyItem.Name;
// 툴팁 설정 (TooltipHandler 컴포넌트가 있다면 연동)
TooltipHandler tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
{
tooltipHandler.Tooltip = _propertyItem.Tooltip;
}
if (_propertyItem.Description.IsNullOrEmpty())
{
_descriptionLabel.gameObject.SetActive(false);
}
else
{
_descriptionLabel.gameObject.SetActive(true);
_descriptionLabel.text = _propertyItem.Description;
}
// 기존 토글 정리
foreach (Transform child in _toggleContainer)
{
Destroy(child.gameObject);
}
_toggles.Clear();
// ToggleGroup 컴포넌트 가져오기 (없으면 추가)
_toggleGroup = _toggleContainer.GetComponent<ToggleGroup>();
if (_toggleGroup == null)
{
_toggleGroup = _toggleContainer.gameObject.AddComponent<ToggleGroup>();
}
_toggleGroup.allowSwitchOff = false; // 항상 하나는 선택되도록 설정
// ItemsSource를 기반으로 토글 동적 생성
for (int i = 0; i < _propertyItem.ItemsSource.Count; i++)
{
string optionValue = _propertyItem.ItemsSource[i];
GameObject toggleInstance = Instantiate(_togglePrefab, _toggleContainer);
toggleInstance.name = $"RadioToggle_{optionValue}";
Toggle toggle = toggleInstance.GetComponent<Toggle>();
TextMeshProUGUI label = toggleInstance.GetComponentInChildren<TextMeshProUGUI>();
if (toggle != null)
{
_toggles.Add(toggle);
toggle.group = _toggleGroup;
toggle.interactable = !_propertyItem.IsReadOnly;
// 현재 값과 일치하는 토글을 활성화
toggle.isOn = (optionValue == _propertyItem.Value);
// 리스너 등록
toggle.onValueChanged.RemoveAllListeners();
toggle.onValueChanged.AddListener((isOn) =>
{
// 토글이 켜졌을 때만 값 업데이트
if (isOn)
{
OnToggleValueChanged(optionValue);
}
});
}
if (label != null)
{
label.text = optionValue;
}
}
}
/// <summary>
/// 토글 값이 변경되었을 때 호출됩니다.
/// </summary>
/// <param name="selectedValue">선택된 토글에 해당하는 값</param>
private void OnToggleValueChanged(string selectedValue)
{
// 컨트롤러를 통해 모델의 값을 업데이트합니다.
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, selectedValue);
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>
private void OnDestroy()
{
foreach (var toggle in _toggles)
{
if (toggle != null)
{
toggle.onValueChanged.RemoveAllListeners();
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 009721941f417e64dabc3b53205102ed

View File

@@ -0,0 +1,87 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extention;
using UVC.UI.Tooltip;
namespace UVC.UI.Window.PropertyWindow.UI
{
/// <summary>
/// StringProperty를 위한 UI를 제어하는 스크립트입니다.
/// </summary>
[RequireComponent(typeof(LayoutElement))] // UI 레이아웃 관리를 위해 추가
public class StringPropertyUI : MonoBehaviour, IPropertyUI
{
[SerializeField] private TextMeshProUGUI _nameLabel;
[SerializeField] private TextMeshProUGUI _descriptionLabel;
[SerializeField] private TMP_InputField _valueInput;
private IPropertyItem _propertyItem;
private PropertyWindow _controller;
/// <summary>
/// PropertyView에 의해 호출되어 UI를 초기화합니다.
/// </summary>
public void Setup(IPropertyItem item, PropertyWindow controller)
{
_propertyItem = item;
_controller = controller;
// --- 데이터 바인딩 ---
// 1. 이름 설정
_nameLabel.text = _propertyItem.Name;
// 툴팁 설정 (TooltipHandler 컴포넌트가 있다면 연동)
TooltipHandler tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
if(tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
{
tooltipHandler.Tooltip = _propertyItem.Tooltip;
}
if (_propertyItem.Description.IsNullOrEmpty())
{
_descriptionLabel.gameObject.SetActive(false);
}
else
{
_descriptionLabel.gameObject.SetActive(true);
_descriptionLabel.text = _propertyItem.Description;
}
// 2. 값 설정
_valueInput.text = _propertyItem.GetValue() as string;
// 3. 읽기 전용 상태 설정
_valueInput.interactable = !_propertyItem.IsReadOnly;
// 4. 툴팁 설정 (Tooltip 컴포넌트가 있다면 연동)
// 예: var tooltip = GetComponent<TooltipTrigger>();
// if (tooltip != null) tooltip.content = _propertyItem.Tooltip;
// --- 이벤트 리스너 등록 ---
// 기존 리스너를 모두 제거한 후 새로 추가하여 중복 등록을 방지합니다.
_valueInput.onEndEdit.RemoveAllListeners();
_valueInput.onEndEdit.AddListener(OnValueSubmitted);
}
/// <summary>
/// 사용자가 InputField 수정 완료 후 Enter를 누르거나 포커스를 잃었을 때 호출됩니다.
/// </summary>
/// <param name="newValue">InputField에 입력된 새로운 문자열</param>
private void OnValueSubmitted(string newValue)
{
// 컨트롤러를 통해 모델의 값을 업데이트합니다.
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
}
private void OnDestroy()
{
// 오브젝트가 파괴될 때 리스너를 확실히 제거합니다.
if (_valueInput != null)
{
_valueInput.onEndEdit.RemoveAllListeners();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 02c6baf2a37cd8d42bb2ec1dce5c9b9c

View File

@@ -0,0 +1,116 @@
using System.Globalization;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extention;
using UVC.UI.Tooltip;
namespace UVC.UI.Window.PropertyWindow.UI
{
/// <summary>
/// Vector2Property를 위한 UI를 제어하는 스크립트입니다.
/// X와 Y 값을 별도의 InputField로 관리합니다.
/// </summary>
[RequireComponent(typeof(LayoutElement))]
public class Vector2PropertyUI : MonoBehaviour, IPropertyUI
{
[Header("UI Components")]
[SerializeField]
private TextMeshProUGUI _nameLabel; // 속성 전체 이름
[SerializeField]
private TextMeshProUGUI _descriptionLabel;
[SerializeField]
private TextMeshProUGUI _xLabel; // X축 자식 이름
[SerializeField]
private TMP_InputField _xInputField; // X축 값 입력 필드
[SerializeField]
private TextMeshProUGUI _yLabel; // Y축 자식 이름
[SerializeField]
private TMP_InputField _yInputField; // Y축 값 입력 필드
private Vector2Property _propertyItem;
private PropertyWindow _controller;
/// <summary>
/// PropertyView에 의해 호출되어 UI를 초기화하고 데이터를 설정합니다.
/// </summary>
public void Setup(IPropertyItem item, PropertyWindow controller)
{
if (!(item is Vector2Property vector2Item))
{
Debug.LogError("Vector2PropertyUI에 잘못된 타입의 PropertyItem이 전달되었습니다.");
return;
}
_propertyItem = vector2Item;
_controller = controller;
// --- 데이터 바인딩 ---
_nameLabel.text = _propertyItem.Name;
// 툴팁 설정 (TooltipHandler 컴포넌트가 있다면 연동)
TooltipHandler tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
{
tooltipHandler.Tooltip = _propertyItem.Tooltip;
}
if (_propertyItem.Description.IsNullOrEmpty())
{
_descriptionLabel.gameObject.SetActive(false);
}
else
{
_descriptionLabel.gameObject.SetActive(true);
_descriptionLabel.text = _propertyItem.Description;
}
// 자식 이름 설정 (예: "X", "Y")
_xLabel.text = _propertyItem.ChildNames.Count > 0 ? _propertyItem.ChildNames[0] : "X";
_yLabel.text = _propertyItem.ChildNames.Count > 1 ? _propertyItem.ChildNames[1] : "Y";
// 초기 값 설정
Vector2 currentValue = _propertyItem.Value;
_xInputField.text = currentValue.x.ToString(CultureInfo.InvariantCulture);
_yInputField.text = currentValue.y.ToString(CultureInfo.InvariantCulture);
// 읽기 전용 상태 설정
bool isReadOnly = _propertyItem.IsReadOnly;
_xInputField.interactable = !isReadOnly;
_yInputField.interactable = !isReadOnly;
// --- 이벤트 리스너 등록 ---
_xInputField.onEndEdit.RemoveAllListeners();
_yInputField.onEndEdit.RemoveAllListeners();
_xInputField.onEndEdit.AddListener(OnValueSubmitted);
_yInputField.onEndEdit.AddListener(OnValueSubmitted);
}
/// <summary>
/// 사용자가 X 또는 Y 입력 필드의 수정을 완료했을 때 호출됩니다.
/// </summary>
private void OnValueSubmitted(string input)
{
// 두 입력 필드의 값을 모두 파싱하여 새로운 Vector2 값을 만듭니다.
float.TryParse(_xInputField.text, NumberStyles.Float, CultureInfo.InvariantCulture, out float x);
float.TryParse(_yInputField.text, NumberStyles.Float, CultureInfo.InvariantCulture, out float y);
var newValue = new Vector2(x, y);
// 컨트롤러를 통해 모델의 값을 업데이트합니다.
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>
private void OnDestroy()
{
if (_xInputField != null) _xInputField.onEndEdit.RemoveAllListeners();
if (_yInputField != null) _yInputField.onEndEdit.RemoveAllListeners();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 49ebae5ac850e894898d8ce805da2e88

View File

@@ -0,0 +1,128 @@
using System.Globalization;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extention;
using UVC.UI.Tooltip;
namespace UVC.UI.Window.PropertyWindow.UI
{
/// <summary>
/// Vector3Property를 위한 UI를 제어하는 스크립트입니다.
/// X, Y, Z 값을 별도의 InputField로 관리합니다.
/// </summary>
[RequireComponent(typeof(LayoutElement))]
public class Vector3PropertyUI : MonoBehaviour, IPropertyUI
{
[Header("UI Components")]
[SerializeField]
private TextMeshProUGUI _nameLabel; // 속성 전체 이름
[SerializeField]
private TextMeshProUGUI _descriptionLabel;
[SerializeField]
private TextMeshProUGUI _xLabel; // X축 자식 이름
[SerializeField]
private TMP_InputField _xInputField; // X축 값 입력 필드
[SerializeField]
private TextMeshProUGUI _yLabel; // Y축 자식 이름
[SerializeField]
private TMP_InputField _yInputField; // Y축 값 입력 필드
[SerializeField]
private TextMeshProUGUI _zLabel; // Z축 자식 이름
[SerializeField]
private TMP_InputField _zInputField; // Z축 값 입력 필드
private Vector3Property _propertyItem;
private PropertyWindow _controller;
/// <summary>
/// PropertyView에 의해 호출되어 UI를 초기화하고 데이터를 설정합니다.
/// </summary>
public void Setup(IPropertyItem item, PropertyWindow controller)
{
if (!(item is Vector3Property vector3Item))
{
Debug.LogError("Vector3PropertyUI에 잘못된 타입의 PropertyItem이 전달되었습니다.");
return;
}
_propertyItem = vector3Item;
_controller = controller;
// --- 데이터 바인딩 ---
_nameLabel.text = _propertyItem.Name;
// 툴팁 설정 (TooltipHandler 컴포넌트가 있다면 연동)
TooltipHandler tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
{
tooltipHandler.Tooltip = _propertyItem.Tooltip;
}
if (_propertyItem.Description.IsNullOrEmpty())
{
_descriptionLabel.gameObject.SetActive(false);
}
else
{
_descriptionLabel.gameObject.SetActive(true);
_descriptionLabel.text = _propertyItem.Description;
}
// 자식 이름 설정 (예: "X", "Y", "Z")
_xLabel.text = _propertyItem.ChildNames.Count > 0 ? _propertyItem.ChildNames[0] : "X";
_yLabel.text = _propertyItem.ChildNames.Count > 1 ? _propertyItem.ChildNames[1] : "Y";
_zLabel.text = _propertyItem.ChildNames.Count > 2 ? _propertyItem.ChildNames[2] : "Z";
// 초기 값 설정
Vector3 currentValue = _propertyItem.Value;
_xInputField.text = currentValue.x.ToString(CultureInfo.InvariantCulture);
_yInputField.text = currentValue.y.ToString(CultureInfo.InvariantCulture);
_zInputField.text = currentValue.z.ToString(CultureInfo.InvariantCulture);
// 읽기 전용 상태 설정
bool isReadOnly = _propertyItem.IsReadOnly;
_xInputField.interactable = !isReadOnly;
_yInputField.interactable = !isReadOnly;
_zInputField.interactable = !isReadOnly;
// --- 이벤트 리스너 등록 ---
_xInputField.onEndEdit.RemoveAllListeners();
_yInputField.onEndEdit.RemoveAllListeners();
_zInputField.onEndEdit.RemoveAllListeners();
_xInputField.onEndEdit.AddListener(OnValueSubmitted);
_yInputField.onEndEdit.AddListener(OnValueSubmitted);
_zInputField.onEndEdit.AddListener(OnValueSubmitted);
}
/// <summary>
/// 사용자가 X, Y 또는 Z 입력 필드의 수정을 완료했을 때 호출됩니다.
/// </summary>
private void OnValueSubmitted(string input)
{
// 모든 입력 필드의 값을 파싱하여 새로운 Vector3 값을 만듭니다.
float.TryParse(_xInputField.text, NumberStyles.Float, CultureInfo.InvariantCulture, out float x);
float.TryParse(_yInputField.text, NumberStyles.Float, CultureInfo.InvariantCulture, out float y);
float.TryParse(_zInputField.text, NumberStyles.Float, CultureInfo.InvariantCulture, out float z);
var newValue = new Vector3(x, y, z);
// 컨트롤러를 통해 모델의 값을 업데이트합니다.
_controller.UpdatePropertyValue(_propertyItem.Id, _propertyItem.PropertyType, newValue);
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>
private void OnDestroy()
{
if (_xInputField != null) _xInputField.onEndEdit.RemoveAllListeners();
if (_yInputField != null) _yInputField.onEndEdit.RemoveAllListeners();
if (_zInputField != null) _zInputField.onEndEdit.RemoveAllListeners();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 086a25c3bf58a03438341f6e081a4eb1