Files
EnglewoodLAB/Assets/Scripts/UVC/UIToolkit/Property/Views/UTKFloatPropertyItemView.cs

619 lines
18 KiB
C#

#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace UVC.UIToolkit
{
/// <summary>
/// Float 속성 View 클래스입니다.
/// UTKFloatField 또는 UTKSlider를 사용하여 float 값을 표시/편집합니다.
///
/// <para><b>사용법 (단독 사용):</b></para>
/// <code>
/// // C# 코드에서 생성
/// var view = new UTKFloatPropertyItemView();
/// view.Label = "속도";
/// view.Value = 1.5f;
/// view.UseSlider = true;
/// view.MinValue = 0f;
/// view.MaxValue = 10f;
/// parent.Add(view);
///
/// // UXML에서 사용
/// <utk:UTKFloatPropertyItemView label="속도" value="1.5" use-slider="true" min-value="0" max-value="10" />
/// </code>
///
/// <para><b>사용법 (Data 바인딩):</b></para>
/// <code>
/// var data = new UTKFloatPropertyItem("speed", "속도", 1.5f);
/// var view = new UTKFloatPropertyItemView();
/// view.Bind(data);
/// </code>
/// </summary>
[UxmlElement]
public partial class UTKFloatPropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<float>
{
#region Fields
private UTKFloatField? _floatField;
private UTKSlider? _slider;
private UTKFloatStepper? _stepper;
private float _value;
private float _minValue;
private float _maxValue = 1f;
private float _step = 0.1f;
private bool _useSlider;
private bool _useStepper;
private IUTKPropertyItem<float>? _boundData;
#endregion
#region Properties
protected override string ViewTypeName => "UTKFloatPropertyItemView";
/// <summary>현재 값</summary>
[UxmlAttribute("value")]
public float Value
{
get => _value;
set
{
if (!Mathf.Approximately(_value, value))
{
var oldValue = _value;
_value = value;
UpdateValueUI();
OnValueChanged?.Invoke(value);
// 바인딩된 데이터가 있으면 동기화
if (_boundData != null && !Mathf.Approximately(_boundData.Value, value))
{
_boundData.Value = value;
}
}
}
}
/// <summary>최소값 (슬라이더 모드)</summary>
[UxmlAttribute("min-value")]
public float MinValue
{
get => _minValue;
set
{
_minValue = value;
if (_slider != null)
{
_slider.lowValue = value;
}
}
}
/// <summary>최대값 (슬라이더 모드)</summary>
[UxmlAttribute("max-value")]
public float MaxValue
{
get => _maxValue;
set
{
_maxValue = value;
if (_slider != null)
{
_slider.highValue = value;
}
}
}
/// <summary>슬라이더 사용 여부</summary>
[UxmlAttribute("use-slider")]
public bool UseSlider
{
get => _useSlider;
set
{
if (_useSlider != value)
{
_useSlider = value;
if (value) _useStepper = false;
UpdateModeClass();
}
}
}
/// <summary>스테퍼(증감 버튼) 사용 여부</summary>
[UxmlAttribute("use-stepper")]
public bool UseStepper
{
get => _useStepper;
set
{
if (_useStepper != value)
{
_useStepper = value;
if (value) _useSlider = false;
UpdateModeClass();
}
}
}
/// <summary>스테퍼 증감 단위</summary>
[UxmlAttribute("step")]
public float Step
{
get => _step;
set => _step = value > 0 ? value : 0.1f;
}
#endregion
#region Events
public event Action<float>? OnValueChanged;
#endregion
#region Constructor
public UTKFloatPropertyItemView() : base()
{
InitializeUI();
}
public UTKFloatPropertyItemView(string label, float value = 0f, bool useSlider = false) : base()
{
_value = value;
_useSlider = useSlider;
Label = label;
InitializeUI();
}
public UTKFloatPropertyItemView(UTKFloatPropertyItem data) : base()
{
_value = data.Value;
_minValue = data.MinValue;
_maxValue = data.MaxValue;
_step = data.Step;
_useSlider = data.UseSlider;
_useStepper = data.UseStepper;
Label = data.Name;
_isReadOnly = data.IsReadOnly;
InitializeUI();
Bind(data);
}
public UTKFloatPropertyItemView(string label, float value, float minValue, float maxValue, bool useSlider = true) : base()
{
_value = value;
_minValue = minValue;
_maxValue = maxValue;
_useSlider = useSlider;
Label = label;
InitializeUI();
}
#endregion
#region Initialization
private void InitializeUI()
{
AddToClassList("utk-property-item-view");
AddToClassList("utk-property-item-view--float");
if (!CreateUIFromUxml())
{
CreateUIFallback();
}
// UXML에서 요소 가져오기
QueryUIElements();
// 이벤트 등록
RegisterEvents();
// 모드 클래스 업데이트
UpdateModeClass();
UpdateValueUI();
UpdateReadOnlyState();
}
private void QueryUIElements()
{
_slider = this.Q<UTKSlider>("slider-field");
_floatField = this.Q<UTKFloatField>("value-field");
_stepper = this.Q<UTKFloatStepper>("stepper-field");
// Fallback: UXML에서 못 찾으면 생성
if (_valueContainer != null)
{
if (_slider == null)
{
_slider = new UTKSlider("", _minValue, _maxValue, _value)
{
name = "slider-field"
};
_slider.AddToClassList("utk-property-item-view__slider");
_valueContainer.Insert(0, _slider);
}
if (_floatField == null)
{
_floatField = new UTKFloatField { name = "value-field" };
_floatField.AddToClassList("utk-property-item-view__field");
_valueContainer.Add(_floatField);
}
if (_stepper == null)
{
_stepper = new UTKFloatStepper(_minValue, _maxValue, _value, _step, IsReadOnly)
{
name = "stepper-field"
};
_stepper.AddToClassList("utk-property-item-view__stepper");
_valueContainer.Add(_stepper);
}
}
// 초기 값 설정
if (_slider != null)
{
_slider.lowValue = _minValue;
_slider.highValue = _maxValue;
_slider.SetValueWithoutNotify(_value);
}
if (_floatField != null)
{
_floatField.SetValueWithoutNotify(_value);
_floatField.isReadOnly = IsReadOnly;
}
if (_stepper != null)
{
_stepper.MinValue = _minValue;
_stepper.MaxValue = _maxValue;
_stepper.Step = _step;
_stepper.SetValue(_value, false);
_stepper.IsReadOnly = IsReadOnly;
}
}
private void UpdateModeClass()
{
// ReadOnly 상태에서는 무조건 FloatField만 표시
if (IsReadOnly)
{
RemoveFromClassList("utk-property-item-view--slider");
RemoveFromClassList("utk-property-item-view--stepper");
UpdateControlVisibility();
return;
}
// 슬라이더 클래스
if (_useSlider)
{
AddToClassList("utk-property-item-view--slider");
}
else
{
RemoveFromClassList("utk-property-item-view--slider");
}
// 스테퍼 클래스
if (_useStepper)
{
AddToClassList("utk-property-item-view--stepper");
}
else
{
RemoveFromClassList("utk-property-item-view--stepper");
}
UpdateControlVisibility();
}
/// <summary>컨트롤 가시성을 업데이트합니다.</summary>
private void UpdateControlVisibility()
{
bool isReadOnlyMode = IsReadOnly;
bool showSlider = !isReadOnlyMode && _useSlider;
bool showStepper = !isReadOnlyMode && _useStepper;
bool showFloatField = !showStepper; // 스테퍼 모드가 아니면 항상 FloatField 표시
if (_slider != null)
{
_slider.style.display = showSlider ? DisplayStyle.Flex : DisplayStyle.None;
}
if (_stepper != null)
{
_stepper.style.display = showStepper ? DisplayStyle.Flex : DisplayStyle.None;
}
if (_floatField != null)
{
_floatField.style.display = showFloatField ? DisplayStyle.Flex : DisplayStyle.None;
// 슬라이더 모드일 때 필드 너비 조정
if (showSlider)
{
_floatField.AddToClassList("utk-property-item-view__field--with-slider");
}
else
{
_floatField.RemoveFromClassList("utk-property-item-view__field--with-slider");
}
}
}
#endregion
#region Override Methods
protected override void CreateValueUI(VisualElement container)
{
// UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음
}
public override void RefreshUI()
{
UpdateValueUI();
}
protected override void OnReadOnlyStateChanged(bool isReadOnly)
{
if (_floatField != null)
{
_floatField.isReadOnly = isReadOnly;
}
if (_slider != null)
{
_slider.IsEnabled = !isReadOnly;
}
if (_stepper != null)
{
_stepper.IsReadOnly = isReadOnly;
}
// ReadOnly 상태 변경 시 모드 클래스 업데이트
UpdateModeClass();
}
#endregion
#region Event Handling
private void RegisterEvents()
{
if (_floatField != null)
{
_floatField.OnValueChanged += OnFloatFieldChanged;
}
if (_slider != null)
{
_slider.OnValueChanged += OnSliderChanged;
}
if (_stepper != null)
{
_stepper.OnValueChanged += OnStepperChanged;
}
}
private void UnregisterEvents()
{
if (_floatField != null)
{
_floatField.OnValueChanged -= OnFloatFieldChanged;
}
if (_slider != null)
{
_slider.OnValueChanged -= OnSliderChanged;
}
if (_stepper != null)
{
_stepper.OnValueChanged -= OnStepperChanged;
}
}
private void OnFloatFieldChanged(float newValue)
{
float clampedValue = _useSlider ? Mathf.Clamp(newValue, _minValue, _maxValue) : newValue;
if (_slider != null && !Mathf.Approximately(_slider.Value, clampedValue))
{
_slider.SetValueWithoutNotify(clampedValue);
}
if (_floatField != null && !Mathf.Approximately(_floatField.Value, clampedValue))
{
_floatField.SetValueWithoutNotify(clampedValue);
}
// 값 설정 (이벤트 발생)
if (!Mathf.Approximately(_value, clampedValue))
{
_value = clampedValue;
OnValueChanged?.Invoke(clampedValue);
if (_boundData != null && !Mathf.Approximately(_boundData.Value, clampedValue))
{
_boundData.Value = clampedValue;
}
}
}
private void OnSliderChanged(float newValue)
{
if (_floatField != null && !Mathf.Approximately(_floatField.Value, newValue))
{
_floatField.SetValueWithoutNotify(newValue);
}
if (_stepper != null && !Mathf.Approximately(_stepper.Value, newValue))
{
_stepper.SetValue(newValue, false);
}
// 값 설정 (이벤트 발생)
if (!Mathf.Approximately(_value, newValue))
{
_value = newValue;
OnValueChanged?.Invoke(newValue);
if (_boundData != null && !Mathf.Approximately(_boundData.Value, newValue))
{
_boundData.Value = newValue;
}
}
}
private void OnStepperChanged(float newValue)
{
if (!Mathf.Approximately(_value, newValue))
{
_value = newValue;
OnValueChanged?.Invoke(newValue);
if (_boundData != null && !Mathf.Approximately(_boundData.Value, newValue))
{
_boundData.Value = newValue;
}
}
}
#endregion
#region Value Update
private void UpdateValueUI()
{
if (_floatField != null && !Mathf.Approximately(_floatField.Value, _value))
{
_floatField.SetValueWithoutNotify(_value);
}
if (_slider != null && !Mathf.Approximately(_slider.Value, _value))
{
_slider.SetValueWithoutNotify(_value);
}
if (_stepper != null && !Mathf.Approximately(_stepper.Value, _value))
{
_stepper.SetValue(_value, false);
}
}
#endregion
#region Data Binding
public void Bind(IUTKPropertyItem data)
{
if (data is IUTKPropertyItem<float> floatData)
{
Bind(floatData);
}
else
{
Debug.LogWarning($"[UTKFloatPropertyItemView] Cannot bind to non-float data: {data.GetType().Name}");
}
}
public void Bind(IUTKPropertyItem<float> data)
{
// 기존 바인딩 해제
Unbind();
_boundData = data;
BindBase(data);
// 데이터에서 속성 동기화
Label = data.Name;
_value = data.Value;
IsVisible = data.IsVisible;
TooltipText = data.Tooltip;
ShowLabel = data.ShowLabel;
// 슬라이더 및 스테퍼 관련 속성 동기화
if (data is UTKFloatPropertyItem floatItem)
{
_minValue = floatItem.MinValue;
_maxValue = floatItem.MaxValue;
_step = floatItem.Step;
// 모드 변경 확인
bool modeChanged = false;
if (_useSlider != floatItem.UseSlider)
{
_useSlider = floatItem.UseSlider;
modeChanged = true;
}
if (_useStepper != floatItem.UseStepper)
{
_useStepper = floatItem.UseStepper;
modeChanged = true;
}
if (modeChanged)
{
UpdateModeClass();
}
// 슬라이더 범위 업데이트
if (_slider != null)
{
_slider.lowValue = _minValue;
_slider.highValue = _maxValue;
}
// 스테퍼 설정 업데이트
if (_stepper != null)
{
_stepper.MinValue = _minValue;
_stepper.MaxValue = _maxValue;
_stepper.Step = _step;
}
}
// 데이터 변경 이벤트 구독
data.OnTypedValueChanged += OnDataValueChanged;
// UI 갱신
UpdateValueUI();
IsReadOnly = data.IsReadOnly;
}
public void Unbind()
{
if (_boundData != null)
{
_boundData.OnTypedValueChanged -= OnDataValueChanged;
UnbindBase();
_boundData = null;
}
}
private void OnDataValueChanged(IUTKPropertyItem<float> item, float oldValue, float newValue)
{
if (!Mathf.Approximately(_value, newValue))
{
_value = newValue;
UpdateValueUI();
}
}
#endregion
#region Dispose
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
UnregisterEvents();
Unbind();
OnValueChanged = null;
_floatField = null;
_slider = null;
_stepper?.Dispose();
_stepper = null;
}
base.Dispose(disposing);
}
#endregion
}
}