597 lines
17 KiB
C#
597 lines
17 KiB
C#
#nullable enable
|
|
using System;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace UVC.UIToolkit
|
|
{
|
|
/// <summary>
|
|
/// Int 속성 View 클래스입니다.
|
|
/// UTKIntegerField, UTKSliderInt, 또는 UTKNumberStepper를 사용하여 int 값을 표시/편집합니다.
|
|
///
|
|
/// <para><b>사용법 (단독 사용):</b></para>
|
|
/// <code>
|
|
/// // 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" />
|
|
/// </code>
|
|
/// </summary>
|
|
[UxmlElement]
|
|
public partial class UTKIntPropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView<int>
|
|
{
|
|
#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<int>? _boundData;
|
|
#endregion
|
|
|
|
#region Properties
|
|
protected override string ViewTypeName => "UTKIntPropertyItemView";
|
|
|
|
/// <summary>현재 값</summary>
|
|
[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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>최소값 (슬라이더 모드)</summary>
|
|
[UxmlAttribute("min-value")]
|
|
public int MinValue
|
|
{
|
|
get => _minValue;
|
|
set
|
|
{
|
|
_minValue = value;
|
|
if (_slider != null)
|
|
{
|
|
_slider.lowValue = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>최대값 (슬라이더 모드)</summary>
|
|
[UxmlAttribute("max-value")]
|
|
public int 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 int Step
|
|
{
|
|
get => _step;
|
|
set
|
|
{
|
|
_step = Math.Max(1, value);
|
|
if (_stepper != null)
|
|
{
|
|
_stepper.Step = _step;
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Events
|
|
public event Action<int>? 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<UTKSliderInt>("slider-field");
|
|
_intField = this.Q<UTKIntegerField>("value-field");
|
|
_stepper = this.Q<UTKNumberStepper>("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();
|
|
}
|
|
|
|
/// <summary>컨트롤 가시성을 업데이트합니다.</summary>
|
|
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<int> intData)
|
|
{
|
|
Bind(intData);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"[UTKIntPropertyItemView] Cannot bind to non-int data: {data.GetType().Name}");
|
|
}
|
|
}
|
|
|
|
public void Bind(IUTKPropertyItem<int> 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<int> 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
|
|
}
|
|
}
|