#nullable enable using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; namespace UVC.UIToolkit { /// /// Float + Dropdown 복합 속성 View 클래스입니다. /// UTKFloatField(또는 UTKFloatStepper)와 UTKDropdown을 사용하여 /// float 값과 문자열 선택을 하나의 행에 표시/편집합니다. /// /// 사용법 (Data 바인딩): /// /// var data = new UTKFloatDropdownPropertyItem("id", "Label", 1.5f, /// new List { "A", "B", "C" }, "A"); /// var view = new UTKFloatDropdownPropertyItemView(); /// view.Bind(data); /// /// [UxmlElement] public partial class UTKFloatDropdownPropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView { #region Fields private UTKFloatField? _floatField; private UTKFloatStepper? _stepper; private UTKDropdown? _dropdown; private UTKInputField? _readOnlyField; private UTKFloatDropdownValue _value; private List _choices = new(); private bool _useStepper; private float _floatMinValue = float.MinValue; private float _floatMaxValue = float.MaxValue; private float _stepperStep = 1.0f; private IUTKPropertyItem? _boundData; #endregion #region Properties protected override string ViewTypeName => "UTKFloatDropdownPropertyItemView"; /// 현재 값 public UTKFloatDropdownValue Value { get => _value; set { if (!_value.Equals(value)) { _value = value; UpdateValueUI(); OnValueChanged?.Invoke(value); if (_boundData != null && !_boundData.Value.Equals(value)) { _boundData.Value = value; } } } } /// Dropdown 선택 목록 public List Choices { get => _choices; set { _choices = value ?? new List(); _dropdown?.SetOptions(_choices); } } /// 스테퍼(증감 버튼) 사용 여부 public bool UseStepper { get => _useStepper; set { if (_useStepper != value) { _useStepper = value; UpdateControlVisibility(); } } } #endregion #region Events public event Action? OnValueChanged; #endregion #region Constructor public UTKFloatDropdownPropertyItemView() : base() { InitializeUI(); } public UTKFloatDropdownPropertyItemView(string label, List choices) : base() { _choices = new List(choices); Label = label; InitializeUI(); } public UTKFloatDropdownPropertyItemView(UTKFloatDropdownPropertyItem data) : base() { _value = data.Value; _choices = new List(data.Choices); _useStepper = data.UseStepper; _floatMinValue = data.FloatMinValue; _floatMaxValue = data.FloatMaxValue; _stepperStep = data.StepperStep; Label = data.Name; _isReadOnly = data.IsReadOnly; InitializeUI(); Bind(data); } #endregion #region Initialization private void InitializeUI() { AddToClassList("utk-property-item-view"); AddToClassList("utk-property-item-view--float-dropdown"); if (!CreateUIFromUxml()) { CreateUIFallback(); } QueryUIElements(); RegisterEvents(); UpdateControlVisibility(); UpdateValueUI(); UpdateReadOnlyState(); } private void QueryUIElements() { _floatField = this.Q("float-field"); _dropdown = this.Q("dropdown-field"); _stepper = this.Q("stepper-field"); if (_valueContainer != null) { _valueContainer.style.flexDirection = FlexDirection.Row; // FloatField 생성 if (_floatField == null) { _floatField = new UTKFloatField { name = "float-field" }; _floatField.AddToClassList("utk-float-dropdown-view__float-field"); _valueContainer.Add(_floatField); } // FloatStepper 생성 if (_stepper == null) { _stepper = new UTKFloatStepper(_floatMinValue, _floatMaxValue, _value.FloatValue, _stepperStep, IsReadOnly) { name = "stepper-field" }; _stepper.AddToClassList("utk-float-dropdown-view__stepper"); _valueContainer.Add(_stepper); } // Dropdown 생성 if (_dropdown == null) { _dropdown = new UTKDropdown { name = "dropdown-field" }; _dropdown.AddToClassList("utk-float-dropdown-view__dropdown"); _valueContainer.Add(_dropdown); } } // 초기 값 설정 if (_floatField != null) { _floatField.SetValueWithoutNotify(_value.FloatValue); _floatField.isReadOnly = IsReadOnly; } if (_stepper != null) { _stepper.MinValue = _floatMinValue; _stepper.MaxValue = _floatMaxValue; _stepper.Step = _stepperStep; _stepper.SetValue(_value.FloatValue, false); _stepper.IsReadOnly = IsReadOnly; } if (_dropdown != null) { _dropdown.SetOptions(_choices); _dropdown.SetSelectedValue(_value.DropdownValue, notify: false); _dropdown.IsEnabled = !IsReadOnly; } } private void RegisterEvents() { if (_floatField != null) { _floatField.OnValueChanged += OnFloatFieldChanged; } if (_stepper != null) { _stepper.OnValueChanged += OnStepperChanged; } if (_dropdown != null) { _dropdown.OnSelectionChanged += OnDropdownChanged; } } private void UnregisterEvents() { if (_floatField != null) { _floatField.OnValueChanged -= OnFloatFieldChanged; } if (_stepper != null) { _stepper.OnValueChanged -= OnStepperChanged; } if (_dropdown != null) { _dropdown.OnSelectionChanged -= OnDropdownChanged; } } /// 컨트롤 가시성을 업데이트합니다. private void UpdateControlVisibility() { bool isReadOnlyMode = IsReadOnly; bool showStepper = !isReadOnlyMode && _useStepper; bool showFloatField = !isReadOnlyMode && !_useStepper; bool showDropdown = !isReadOnlyMode; if (_floatField != null) { _floatField.style.display = showFloatField ? DisplayStyle.Flex : DisplayStyle.None; } if (_stepper != null) { _stepper.style.display = showStepper ? DisplayStyle.Flex : DisplayStyle.None; } if (_dropdown != null) { _dropdown.style.display = showDropdown ? DisplayStyle.Flex : DisplayStyle.None; } // ReadOnly 모드: 단일 텍스트 필드로 표시 if (isReadOnlyMode) { EnsureReadOnlyField(); if (_readOnlyField != null) { _readOnlyField.style.display = DisplayStyle.Flex; _readOnlyField.Value = _value.ToString(); } } else { if (_readOnlyField != null) { _readOnlyField.style.display = DisplayStyle.None; } } } /// ReadOnly 전용 필드를 필요 시 생성합니다. private void EnsureReadOnlyField() { if (_readOnlyField != null || _valueContainer == null) return; _readOnlyField = new UTKInputField { name = "readonly-field" }; _readOnlyField.AddToClassList("utk-float-dropdown-view__readonly-field"); _readOnlyField.isReadOnly = true; _readOnlyField.Value = _value.ToString(); _valueContainer.Add(_readOnlyField); } #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 (_stepper != null) { _stepper.IsReadOnly = isReadOnly; } if (_dropdown != null) { _dropdown.IsEnabled = !isReadOnly; } UpdateControlVisibility(); } #endregion #region Event Handling private void OnFloatFieldChanged(float newFloat) { var newValue = new UTKFloatDropdownValue(newFloat, _value.DropdownValue); if (!_value.Equals(newValue)) { _value = newValue; // 스테퍼가 있으면 동기화 if (_stepper != null && !Mathf.Approximately(_stepper.Value, newFloat)) { _stepper.SetValue(newFloat, false); } OnValueChanged?.Invoke(newValue); if (_boundData != null && !_boundData.Value.Equals(newValue)) { _boundData.Value = newValue; } } } private void OnStepperChanged(float newFloat) { var newValue = new UTKFloatDropdownValue(newFloat, _value.DropdownValue); if (!_value.Equals(newValue)) { _value = newValue; // FloatField가 있으면 동기화 if (_floatField != null && !Mathf.Approximately(_floatField.Value, newFloat)) { _floatField.SetValueWithoutNotify(newFloat); } OnValueChanged?.Invoke(newValue); if (_boundData != null && !_boundData.Value.Equals(newValue)) { _boundData.Value = newValue; } } } private void OnDropdownChanged(int index, string newDropdownValue) { var newValue = new UTKFloatDropdownValue(_value.FloatValue, newDropdownValue); 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 (_floatField != null && !Mathf.Approximately(_floatField.Value, _value.FloatValue)) { _floatField.SetValueWithoutNotify(_value.FloatValue); } if (_stepper != null && !Mathf.Approximately(_stepper.Value, _value.FloatValue)) { _stepper.SetValue(_value.FloatValue, false); } if (_dropdown != null) { string? selectedValue = _dropdown.SelectedValue; if (selectedValue != _value.DropdownValue) { _dropdown.SetSelectedValue(_value.DropdownValue, notify: false); } } if (_readOnlyField != null && IsReadOnly) { _readOnlyField.Value = _value.ToString(); } } #endregion #region Data Binding public void Bind(IUTKPropertyItem data) { if (data is IUTKPropertyItem typedData) { Bind(typedData); } else { Debug.LogWarning($"[UTKFloatDropdownPropertyItemView] Cannot bind to non-FloatDropdown 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; // UTKFloatDropdownPropertyItem의 설정 적용 if (data is UTKFloatDropdownPropertyItem floatDropdownItem) { _choices = new List(floatDropdownItem.Choices); _useStepper = floatDropdownItem.UseStepper; _floatMinValue = floatDropdownItem.FloatMinValue; _floatMaxValue = floatDropdownItem.FloatMaxValue; _stepperStep = floatDropdownItem.StepperStep; // Dropdown 옵션 업데이트 _dropdown?.SetOptions(_choices); // 스테퍼 설정 업데이트 if (_stepper != null) { _stepper.MinValue = _floatMinValue; _stepper.MaxValue = _floatMaxValue; _stepper.Step = _stepperStep; } UpdateControlVisibility(); } data.OnTypedValueChanged += OnDataValueChanged; UpdateValueUI(); IsReadOnly = data.IsReadOnly; } public void Unbind() { if (_boundData != null) { _boundData.OnTypedValueChanged -= OnDataValueChanged; UnbindBase(); _boundData = null; } } private void OnDataValueChanged(IUTKPropertyItem item, UTKFloatDropdownValue oldValue, UTKFloatDropdownValue 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; _floatField = null; _stepper?.Dispose(); _stepper = null; _dropdown?.Dispose(); _dropdown = null; _readOnlyField = null; } base.Dispose(disposing); } #endregion } }