#nullable enable using System; using UnityEngine; using UnityEngine.UIElements; namespace UVC.UIToolkit { /// /// FloatRange 속성 View 클래스입니다. /// Min, Max 두 개의 UTKFloatField 또는 UTKFloatStepper를 사용하여 실수 범위를 표시/편집합니다. /// [UxmlElement] public partial class UTKFloatRangePropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView { #region Fields private UTKFloatField? _minField; private UTKFloatField? _maxField; private UTKFloatStepper? _minStepper; private UTKFloatStepper? _maxStepper; private UTKFloatRange _value; private bool _useStepper; private float _stepperMinValue = float.MinValue; private float _stepperMaxValue = float.MaxValue; private float _stepperStep = 1.0f; private IUTKPropertyItem? _boundData; #endregion #region Properties protected override string ViewTypeName => "UTKFloatRangePropertyItemView"; /// 스테퍼(증감 버튼) 사용 여부 public bool UseStepper { get => _useStepper; set { if (_useStepper != value) { _useStepper = value; UpdateModeClass(); } } } /// 스테퍼 최소값 public float StepperMinValue { get => _stepperMinValue; set { _stepperMinValue = value; if (_minStepper != null) _minStepper.MinValue = value; if (_maxStepper != null) _maxStepper.MinValue = value; } } /// 스테퍼 최대값 public float StepperMaxValue { get => _stepperMaxValue; set { _stepperMaxValue = value; if (_minStepper != null) _minStepper.MaxValue = value; if (_maxStepper != null) _maxStepper.MaxValue = value; } } /// 스테퍼 증감 단위 public float StepperStep { get => _stepperStep; set { _stepperStep = value > 0 ? value : 1.0f; if (_minStepper != null) _minStepper.Step = _stepperStep; if (_maxStepper != null) _maxStepper.Step = _stepperStep; } } /// 현재 값 public UTKFloatRange Value { get => _value; set { if (!_value.Equals(value)) { _value = value; UpdateValueUI(); OnValueChanged?.Invoke(value); if (_boundData != null && !_boundData.Value.Equals(value)) { _boundData.Value = value; } } } } #endregion #region Events public event Action? OnValueChanged; #endregion #region Constructor public UTKFloatRangePropertyItemView() : base() { InitializeUI(); } public UTKFloatRangePropertyItemView(string label, UTKFloatRange value = default) : base() { _value = value; Label = label; InitializeUI(); } public UTKFloatRangePropertyItemView(string label, float min, float max) : base() { _value = new UTKFloatRange(min, max); Label = label; InitializeUI(); } #endregion #region Initialization private void InitializeUI() { AddToClassList("utk-property-item-view"); AddToClassList("utk-property-item-view--float-range"); if (!CreateUIFromUxml()) { CreateUIFallback(); } // UXML에서 요소 가져오기 QueryUIElements(); // 이벤트 등록 RegisterEvents(); UpdateValueUI(); UpdateReadOnlyState(); } private void QueryUIElements() { _minField = this.Q("min-field"); _maxField = this.Q("max-field"); _minStepper = this.Q("min-stepper"); _maxStepper = this.Q("max-stepper"); // Fallback: UXML에서 못 찾으면 생성 if (_valueContainer != null) { _valueContainer.style.flexDirection = FlexDirection.Row; // FloatField 생성 if (_minField == null) { _minField = new UTKFloatField { name = "min-field" }; _minField.AddToClassList("utk-property-item-view__field"); _valueContainer.Add(_minField); } if (_maxField == null) { var separator = new UTKLabel("~", UTKLabel.LabelSize.Body2); separator.AddToClassList("utk-property-item-view__range-separator"); _valueContainer.Add(separator); _maxField = new UTKFloatField { name = "max-field" }; _maxField.AddToClassList("utk-property-item-view__field"); _valueContainer.Add(_maxField); } // FloatStepper 생성 if (_minStepper == null) { _minStepper = new UTKFloatStepper(_stepperMinValue, _stepperMaxValue, _value.Min, _stepperStep, IsReadOnly) { name = "min-stepper" }; _minStepper.AddToClassList("utk-property-item-view__stepper"); _valueContainer.Add(_minStepper); } if (_maxStepper == null) { _maxStepper = new UTKFloatStepper(_stepperMinValue, _stepperMaxValue, _value.Max, _stepperStep, IsReadOnly) { name = "max-stepper" }; _maxStepper.AddToClassList("utk-property-item-view__stepper"); _valueContainer.Add(_maxStepper); } } // 초기 값 설정 if (_minField != null) { _minField.SetValueWithoutNotify(_value.Min); _minField.isReadOnly = IsReadOnly; } if (_maxField != null) { _maxField.SetValueWithoutNotify(_value.Max); _maxField.isReadOnly = IsReadOnly; } if (_minStepper != null) { _minStepper.MinValue = _stepperMinValue; _minStepper.MaxValue = _stepperMaxValue; _minStepper.Step = _stepperStep; _minStepper.SetValue(_value.Min, false); _minStepper.IsReadOnly = IsReadOnly; } if (_maxStepper != null) { _maxStepper.MinValue = _stepperMinValue; _maxStepper.MaxValue = _stepperMaxValue; _maxStepper.Step = _stepperStep; _maxStepper.SetValue(_value.Max, false); _maxStepper.IsReadOnly = IsReadOnly; } // 컨트롤 가시성 업데이트 UpdateControlVisibility(); } private void RegisterEvents() { if (_minField != null) { _minField.OnValueChanged += OnMinChanged; } if (_maxField != null) { _maxField.OnValueChanged += OnMaxChanged; } if (_minStepper != null) { _minStepper.OnValueChanged += OnMinStepperChanged; } if (_maxStepper != null) { _maxStepper.OnValueChanged += OnMaxStepperChanged; } } private void UnregisterEvents() { if (_minField != null) { _minField.OnValueChanged -= OnMinChanged; } if (_maxField != null) { _maxField.OnValueChanged -= OnMaxChanged; } if (_minStepper != null) { _minStepper.OnValueChanged -= OnMinStepperChanged; } if (_maxStepper != null) { _maxStepper.OnValueChanged -= OnMaxStepperChanged; } } private void UpdateModeClass() { // ReadOnly 상태에서는 무조건 FloatField만 표시 if (IsReadOnly) { RemoveFromClassList("utk-property-item-view--stepper"); UpdateControlVisibility(); return; } // 스테퍼 클래스 if (_useStepper) { AddToClassList("utk-property-item-view--stepper"); } else { RemoveFromClassList("utk-property-item-view--stepper"); } UpdateControlVisibility(); } /// 컨트롤 가시성을 업데이트합니다. private void UpdateControlVisibility() { bool isReadOnlyMode = IsReadOnly; bool showStepper = !isReadOnlyMode && _useStepper; bool showFloatField = isReadOnlyMode || !_useStepper; if (_minField != null) { _minField.style.display = showFloatField ? DisplayStyle.Flex : DisplayStyle.None; } if (_maxField != null) { _maxField.style.display = showFloatField ? DisplayStyle.Flex : DisplayStyle.None; } if (_minStepper != null) { _minStepper.style.display = showStepper ? DisplayStyle.Flex : DisplayStyle.None; } if (_maxStepper != null) { _maxStepper.style.display = showStepper ? DisplayStyle.Flex : DisplayStyle.None; } } #endregion #region Override Methods protected override void CreateValueUI(VisualElement container) { // UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음 } public override void RefreshUI() { UpdateValueUI(); } protected override void OnReadOnlyStateChanged(bool isReadOnly) { if (_minField != null) _minField.isReadOnly = isReadOnly; if (_maxField != null) _maxField.isReadOnly = isReadOnly; if (_minStepper != null) _minStepper.IsReadOnly = isReadOnly; if (_maxStepper != null) _maxStepper.IsReadOnly = isReadOnly; // ReadOnly 상태 변경 시 모드 클래스 업데이트 UpdateModeClass(); } #endregion #region Event Handling private void OnMinChanged(float newMin) { var newValue = new UTKFloatRange(newMin, _value.Max); if (!_value.Equals(newValue)) { _value = newValue; OnValueChanged?.Invoke(newValue); if (_boundData != null && !_boundData.Value.Equals(newValue)) { _boundData.Value = newValue; } } } private void OnMaxChanged(float newMax) { var newValue = new UTKFloatRange(_value.Min, newMax); if (!_value.Equals(newValue)) { _value = newValue; OnValueChanged?.Invoke(newValue); if (_boundData != null && !_boundData.Value.Equals(newValue)) { _boundData.Value = newValue; } } } private void OnMinStepperChanged(float newMin) { var newValue = new UTKFloatRange(newMin, _value.Max); if (!_value.Equals(newValue)) { _value = newValue; OnValueChanged?.Invoke(newValue); if (_boundData != null && !_boundData.Value.Equals(newValue)) { _boundData.Value = newValue; } } } private void OnMaxStepperChanged(float newMax) { var newValue = new UTKFloatRange(_value.Min, newMax); if (!_value.Equals(newValue)) { _value = newValue; OnValueChanged?.Invoke(newValue); if (_boundData != null && !_boundData.Value.Equals(newValue)) { _boundData.Value = newValue; } } } #endregion #region Value Update private void UpdateValueUI() { if (_minField != null && !Mathf.Approximately(_minField.Value, _value.Min)) { _minField.SetValueWithoutNotify(_value.Min); } if (_maxField != null && !Mathf.Approximately(_maxField.Value, _value.Max)) { _maxField.SetValueWithoutNotify(_value.Max); } if (_minStepper != null && !Mathf.Approximately(_minStepper.Value, _value.Min)) { _minStepper.SetValue(_value.Min, false); } if (_maxStepper != null && !Mathf.Approximately(_maxStepper.Value, _value.Max)) { _maxStepper.SetValue(_value.Max, false); } } #endregion #region Data Binding public void Bind(IUTKPropertyItem data) { if (data is IUTKPropertyItem rangeData) { Bind(rangeData); } else { Debug.LogWarning($"[UTKFloatRangePropertyItemView] Cannot bind to non-FloatRange data: {data.GetType().Name}"); } } public void Bind(IUTKPropertyItem data) { Unbind(); _boundData = data; BindBase(data); Label = data.Name; _value = data.Value; IsVisible = data.IsVisible; TooltipText = data.Tooltip; ShowLabel = data.ShowLabel; // UTKFloatRangePropertyItem의 스테퍼 설정 적용 if (data is UTKFloatRangePropertyItem rangeItem) { _useStepper = rangeItem.UseStepper; _stepperMinValue = rangeItem.StepperMinValue; _stepperMaxValue = rangeItem.StepperMaxValue; _stepperStep = rangeItem.StepperStep; // 스테퍼에 값 적용 if (_minStepper != null) { _minStepper.MinValue = _stepperMinValue; _minStepper.MaxValue = _stepperMaxValue; _minStepper.Step = _stepperStep; } if (_maxStepper != null) { _maxStepper.MinValue = _stepperMinValue; _maxStepper.MaxValue = _stepperMaxValue; _maxStepper.Step = _stepperStep; } UpdateModeClass(); } data.OnTypedValueChanged += OnDataValueChanged; UpdateValueUI(); IsReadOnly = data.IsReadOnly; } public void Unbind() { if (_boundData != null) { _boundData.OnTypedValueChanged -= OnDataValueChanged; UnbindBase(); _boundData = null; } } private void OnDataValueChanged(IUTKPropertyItem item, UTKFloatRange oldValue, UTKFloatRange newValue) { if (!_value.Equals(newValue)) { _value = newValue; UpdateValueUI(); } } #endregion #region Dispose protected override void Dispose(bool disposing) { if (_disposed) return; if (disposing) { UnregisterEvents(); Unbind(); OnValueChanged = null; _minField = null; _maxField = null; _minStepper?.Dispose(); _minStepper = null; _maxStepper?.Dispose(); _maxStepper = null; } base.Dispose(disposing); } #endregion } }