PropertyWindow 개발중
This commit is contained in:
8
Assets/Scripts/UVC/UI/Modal/ColorPicker.meta
Normal file
8
Assets/Scripts/UVC/UI/Modal/ColorPicker.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 180a8464dbdefe7449c29c189b7f3879
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
443
Assets/Scripts/UVC/UI/Modal/ColorPicker/ColorPicker.cs
Normal file
443
Assets/Scripts/UVC/UI/Modal/ColorPicker/ColorPicker.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Assets/Scripts/UVC/UI/Modal/ColorPicker/ColorPicker.cs.meta
Normal file
18
Assets/Scripts/UVC/UI/Modal/ColorPicker/ColorPicker.cs.meta
Normal 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
|
||||
8
Assets/Scripts/UVC/UI/Modal/DatePicker.meta
Normal file
8
Assets/Scripts/UVC/UI/Modal/DatePicker.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 069a815c572419549b7571b8e388a8ea
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
19
Assets/Scripts/UVC/UI/Modal/DatePicker/DateItem.cs
Normal file
19
Assets/Scripts/UVC/UI/Modal/DatePicker/DateItem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Assets/Scripts/UVC/UI/Modal/DatePicker/DateItem.cs.meta
Normal file
12
Assets/Scripts/UVC/UI/Modal/DatePicker/DateItem.cs.meta
Normal 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:
|
||||
247
Assets/Scripts/UVC/UI/Modal/DatePicker/DatePicker.cs
Normal file
247
Assets/Scripts/UVC/UI/Modal/DatePicker/DatePicker.cs
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
12
Assets/Scripts/UVC/UI/Modal/DatePicker/DatePicker.cs.meta
Normal file
12
Assets/Scripts/UVC/UI/Modal/DatePicker/DatePicker.cs.meta
Normal 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:
|
||||
@@ -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>
|
||||
|
||||
8
Assets/Scripts/UVC/UI/Window.meta
Normal file
8
Assets/Scripts/UVC/UI/Window.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d273c65a1a0338468d8064ae2719c7f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/UVC/UI/Window/PropertyWindow.meta
Normal file
8
Assets/Scripts/UVC/UI/Window/PropertyWindow.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 50eb185ce2056f24199c64ecfca67f14
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
15
Assets/Scripts/UVC/UI/Window/PropertyWindow/IPropertyUI.cs
Normal file
15
Assets/Scripts/UVC/UI/Window/PropertyWindow/IPropertyUI.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bb245e4f78fa6a7439629fd7c6212d7e
|
||||
333
Assets/Scripts/UVC/UI/Window/PropertyWindow/PropertyItem.cs
Normal file
333
Assets/Scripts/UVC/UI/Window/PropertyWindow/PropertyItem.cs
Normal 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
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 679e64cd5c69c84498c8f831db40aee1
|
||||
166
Assets/Scripts/UVC/UI/Window/PropertyWindow/PropertyView.cs
Normal file
166
Assets/Scripts/UVC/UI/Window/PropertyWindow/PropertyView.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e111cda3d9984e74d8c84723e40671af
|
||||
108
Assets/Scripts/UVC/UI/Window/PropertyWindow/PropertyWindow.cs
Normal file
108
Assets/Scripts/UVC/UI/Window/PropertyWindow/PropertyWindow.cs
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1bbe888d699e08c41bd01f39bb566480
|
||||
8
Assets/Scripts/UVC/UI/Window/PropertyWindow/UI.meta
Normal file
8
Assets/Scripts/UVC/UI/Window/PropertyWindow/UI.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 938420c6f0443964fb0836a2d2389af1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1045178b4c433544eb2d14c4e3ee7f57
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b034c34b9d600df4f90b3376ed845c36
|
||||
116
Assets/Scripts/UVC/UI/Window/PropertyWindow/UI/DatePropertyUI.cs
Normal file
116
Assets/Scripts/UVC/UI/Window/PropertyWindow/UI/DatePropertyUI.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2c41f469f7093b4ba439695a110610b
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f3b80a3851744441acb18967290ce5d
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a4e5b7847b47464d92c82d64bdb5346
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c75e03e8867b032418c0e91aa9278066
|
||||
119
Assets/Scripts/UVC/UI/Window/PropertyWindow/UI/EnumPropertyUI.cs
Normal file
119
Assets/Scripts/UVC/UI/Window/PropertyWindow/UI/EnumPropertyUI.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0f628caaf72133a4ea1b8d9e8ea77f55
|
||||
116
Assets/Scripts/UVC/UI/Window/PropertyWindow/UI/ListPropertyUI.cs
Normal file
116
Assets/Scripts/UVC/UI/Window/PropertyWindow/UI/ListPropertyUI.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2e5cf04729c7e54b88dd90ac330c85e
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 450872122bc4a504ca4b0946a1b50089
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 11ff195ea0a22d94e9c5dc75e1293b58
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 009721941f417e64dabc3b53205102ed
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 02c6baf2a37cd8d42bb2ec1dce5c9b9c
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 49ebae5ac850e894898d8ce805da2e88
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 086a25c3bf58a03438341f6e081a4eb1
|
||||
@@ -17,6 +17,7 @@ namespace UVC.Util
|
||||
/// </example>
|
||||
public static Color FromHex(string hexCode)
|
||||
{
|
||||
if(!hexCode.StartsWith("#")) hexCode = "#" + hexCode;
|
||||
Color color;
|
||||
if (ColorUtility.TryParseHtmlString(hexCode, out color))
|
||||
{
|
||||
@@ -40,6 +41,7 @@ namespace UVC.Util
|
||||
/// </example>
|
||||
public static Color FromHex(string hexCode, float alpha)
|
||||
{
|
||||
if (!hexCode.StartsWith("#")) hexCode = "#" + hexCode;
|
||||
Color color;
|
||||
if (ColorUtility.TryParseHtmlString(hexCode, out color))
|
||||
{
|
||||
@@ -52,5 +54,17 @@ namespace UVC.Util
|
||||
color.a = alpha;
|
||||
return color;
|
||||
}
|
||||
|
||||
public static string ToHex(this Color color, bool includeAlpha = false, bool hasSharp = true)
|
||||
{
|
||||
if (includeAlpha)
|
||||
{
|
||||
return $"{(hasSharp?"#":"")}{ColorUtility.ToHtmlStringRGBA(color)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{(hasSharp ? "#" : "")}{ColorUtility.ToHtmlStringRGB(color)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user