#nullable enable using System; using UnityEngine; using UnityEngine.UIElements; namespace UVC.UIToolkit { /// /// Int 속성 View 클래스입니다. /// UTKIntegerField, UTKSliderInt, 또는 UTKNumberStepper를 사용하여 int 값을 표시/편집합니다. /// /// 사용법 (단독 사용): /// /// // C# 코드에서 생성 /// var view = new UTKIntPropertyItemView(); /// view.Label = "수량"; /// view.Value = 10; /// view.UseSlider = true; /// view.MinValue = 0; /// view.MaxValue = 100; /// parent.Add(view); /// /// // 스테퍼 모드 /// var stepperView = new UTKIntPropertyItemView(); /// stepperView.UseStepper = true; /// stepperView.Step = 5; /// /// // UXML에서 사용 /// <utk:UTKIntPropertyItemView label="수량" value="10" use-slider="true" min-value="0" max-value="100" /> /// <utk:UTKIntPropertyItemView label="개수" value="1" use-stepper="true" step="1" /> /// /// [UxmlElement] public partial class UTKIntPropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView { #region Fields private UTKIntegerField? _intField; private UTKSliderInt? _slider; private UTKNumberStepper? _stepper; private int _value; private int _minValue; private int _maxValue = 100; private int _step = 1; private bool _useSlider; private bool _useStepper; private IUTKPropertyItem? _boundData; #endregion #region Properties protected override string ViewTypeName => "UTKIntPropertyItemView"; /// 현재 값 [UxmlAttribute("value")] public int Value { get => _value; set { if (_value != value) { _value = value; UpdateValueUI(); OnValueChanged?.Invoke(value); if (_boundData != null && _boundData.Value != value) { _boundData.Value = value; } } } } /// 최소값 (슬라이더 모드) [UxmlAttribute("min-value")] public int MinValue { get => _minValue; set { _minValue = value; if (_slider != null) { _slider.lowValue = value; } } } /// 최대값 (슬라이더 모드) [UxmlAttribute("max-value")] public int MaxValue { get => _maxValue; set { _maxValue = value; if (_slider != null) { _slider.highValue = value; } } } /// 슬라이더 사용 여부 [UxmlAttribute("use-slider")] public bool UseSlider { get => _useSlider; set { if (_useSlider != value) { _useSlider = value; if (value) _useStepper = false; UpdateModeClass(); } } } /// 스테퍼(증감 버튼) 사용 여부 [UxmlAttribute("use-stepper")] public bool UseStepper { get => _useStepper; set { if (_useStepper != value) { _useStepper = value; if (value) _useSlider = false; UpdateModeClass(); } } } /// 스테퍼 증감 단위 [UxmlAttribute("step")] public int Step { get => _step; set { _step = Math.Max(1, value); if (_stepper != null) { _stepper.Step = _step; } } } #endregion #region Events public event Action? OnValueChanged; #endregion #region Constructor public UTKIntPropertyItemView() : base() { InitializeUI(); } public UTKIntPropertyItemView(string label, int value = 0, bool useSlider = false) : base() { _value = value; _useSlider = useSlider; Label = label; InitializeUI(); } public UTKIntPropertyItemView(UTKIntPropertyItem item) : base() { _value = item.Value; _minValue = item.MinValue; _maxValue = item.MaxValue; _useSlider = item.UseSlider; _isReadOnly = item.IsReadOnly; InitializeUI(); Bind(item); } public UTKIntPropertyItemView(string label, int value, int minValue, int 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--int"); if (!CreateUIFromUxml()) { CreateUIFallback(); } // UXML에서 요소 가져오기 QueryUIElements(); // 이벤트 등록 RegisterEvents(); // 슬라이더 클래스 업데이트 UpdateModeClass(); UpdateValueUI(); UpdateReadOnlyState(); } private void QueryUIElements() { _slider = this.Q("slider-field"); _intField = this.Q("value-field"); _stepper = this.Q("stepper-field"); // Fallback: UXML에서 못 찾으면 생성 if (_valueContainer != null) { if (_slider == null) { _slider = new UTKSliderInt("", _minValue, _maxValue, _value) { name = "slider-field" }; _slider.AddToClassList("utk-property-item-view__slider"); _valueContainer.Insert(0, _slider); } if (_intField == null) { _intField = new UTKIntegerField { name = "value-field" }; _intField.AddToClassList("utk-property-item-view__field"); _valueContainer.Add(_intField); } if (_stepper == null) { _stepper = new UTKNumberStepper(_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 (_intField != null) { _intField.SetValueWithoutNotify(_value); _intField.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 상태에서는 무조건 IntegerField만 표시 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(); } #endregion #region Override Methods protected override void CreateValueUI(VisualElement container) { // UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음 } public override void RefreshUI() { UpdateValueUI(); } protected override void OnReadOnlyStateChanged(bool isReadOnly) { if (_intField != null) { _intField.isReadOnly = isReadOnly; } if (_slider != null) { _slider.IsEnabled = !isReadOnly; } if (_stepper != null) { _stepper.IsReadOnly = isReadOnly; } // ReadOnly 상태 변경 시 모드 클래스 업데이트 UpdateModeClass(); } /// 컨트롤 가시성을 업데이트합니다. private void UpdateControlVisibility() { bool isReadOnlyMode = IsReadOnly; bool showSlider = !isReadOnlyMode && _useSlider; bool showStepper = !isReadOnlyMode && _useStepper; bool showIntField = isReadOnlyMode || (!_useSlider && !_useStepper); if (_slider != null) { _slider.style.display = showSlider ? DisplayStyle.Flex : DisplayStyle.None; } if (_stepper != null) { _stepper.style.display = showStepper ? DisplayStyle.Flex : DisplayStyle.None; } if (_intField != null) { _intField.style.display = showIntField ? DisplayStyle.Flex : DisplayStyle.None; } } #endregion #region Event Handling private void RegisterEvents() { if (_intField != null) { _intField.OnValueChanged += OnIntFieldChanged; } if (_slider != null) { _slider.OnValueChanged += OnSliderChanged; } if (_stepper != null) { _stepper.OnValueChanged += OnStepperChanged; } } private void UnregisterEvents() { if (_intField != null) { _intField.OnValueChanged -= OnIntFieldChanged; } if (_slider != null) { _slider.OnValueChanged -= OnSliderChanged; } if (_stepper != null) { _stepper.OnValueChanged -= OnStepperChanged; } } private void OnIntFieldChanged(int newValue) { int clampedValue = _useSlider ? Mathf.Clamp(newValue, _minValue, _maxValue) : newValue; if (_slider != null && _slider.Value != clampedValue) { _slider.SetValueWithoutNotify(clampedValue); } if (_intField != null && _intField.Value != clampedValue) { _intField.SetValueWithoutNotify(clampedValue); } if (_value != clampedValue) { _value = clampedValue; OnValueChanged?.Invoke(clampedValue); if (_boundData != null && _boundData.Value != clampedValue) { _boundData.Value = clampedValue; } } } private void OnSliderChanged(int newValue) { if (_intField != null && _intField.Value != newValue) { _intField.SetValueWithoutNotify(newValue); } if (_value != newValue) { _value = newValue; OnValueChanged?.Invoke(newValue); if (_boundData != null && _boundData.Value != newValue) { _boundData.Value = newValue; } } } private void OnStepperChanged(int newValue) { if (_value != newValue) { _value = newValue; OnValueChanged?.Invoke(newValue); if (_boundData != null && _boundData.Value != newValue) { _boundData.Value = newValue; } } } #endregion #region Value Update private void UpdateValueUI() { if (_intField != null && _intField.Value != _value) { _intField.SetValueWithoutNotify(_value); } if (_slider != null && _slider.Value != _value) { _slider.SetValueWithoutNotify(_value); } if (_stepper != null && _stepper.Value != _value) { _stepper.SetValue(_value, false); } } #endregion #region Data Binding public void Bind(IUTKPropertyItem data) { if (data is IUTKPropertyItem intData) { Bind(intData); } else { Debug.LogWarning($"[UTKIntPropertyItemView] Cannot bind to non-int data: {data.GetType().Name}"); } } public void Bind(IUTKPropertyItem data) { Unbind(); _boundData = data; Label = data.Name; _value = data.Value; IsReadOnly = data.IsReadOnly; IsVisible = data.IsVisible; TooltipText = data.Tooltip; if (data is UTKIntPropertyItem intItem) { _minValue = intItem.MinValue; _maxValue = intItem.MaxValue; _step = intItem.Step; // 모드 변경 확인 bool modeChanged = false; if (_useSlider != intItem.UseSlider) { _useSlider = intItem.UseSlider; modeChanged = true; } if (_useStepper != intItem.UseStepper) { _useStepper = intItem.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; UpdateValueUI(); UpdateReadOnlyState(); } public void Unbind() { if (_boundData != null) { _boundData.OnTypedValueChanged -= OnDataValueChanged; _boundData = null; } } private void OnDataValueChanged(IUTKPropertyItem item, int oldValue, int newValue) { if (_value != newValue) { _value = newValue; UpdateValueUI(); } } #endregion #region Dispose protected override void Dispose(bool disposing) { if (_disposed) return; if (disposing) { UnregisterEvents(); Unbind(); OnValueChanged = null; _intField = null; _slider = null; _stepper?.Dispose(); _stepper = null; } base.Dispose(disposing); } #endregion } }