#nullable enable using System; using UnityEngine; using UnityEngine.UIElements; namespace UVC.UIToolkit { /// /// DateRange 속성 View 클래스입니다. /// 시작일, 종료일 두 개의 DatePicker를 사용하여 날짜 범위를 표시/편집합니다. /// [UxmlElement] public partial class UTKDateRangePropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView { #region Fields private UTKInputField? _startField; private UTKInputField? _endField; private UTKButton? _startPickerBtn; private UTKButton? _endPickerBtn; private UTKDatePicker? _currentPicker; private bool _isEditingStart; private UTKDateRange _value; private string _dateFormat = "yyyy-MM-dd"; private IUTKPropertyItem? _boundData; #endregion #region Properties protected override string ViewTypeName => "UTKDateRangePropertyItemView"; /// 현재 값 public UTKDateRange Value { get => _value; set { if (!_value.Equals(value)) { _value = value; UpdateValueUI(); OnValueChanged?.Invoke(value); if (_boundData != null && !_boundData.Value.Equals(value)) { _boundData.Value = value; } } } } /// 날짜 표시 형식 [UxmlAttribute("date-format")] public string DateFormat { get => _dateFormat; set { _dateFormat = value ?? "yyyy-MM-dd"; UpdateValueUI(); } } #endregion #region Events public event Action? OnValueChanged; #endregion #region Constructor public UTKDateRangePropertyItemView() : base() { _value = new UTKDateRange(DateTime.Today, DateTime.Today); InitializeUI(); } public UTKDateRangePropertyItemView(string label, UTKDateRange value = default) : base() { _value = value.Start == default ? new UTKDateRange(DateTime.Today, DateTime.Today) : value; Label = label; InitializeUI(); } public UTKDateRangePropertyItemView(string label, DateTime start, DateTime end) : base() { _value = new UTKDateRange(start, end); Label = label; InitializeUI(); } #endregion #region Initialization private void InitializeUI() { AddToClassList("utk-property-item-view"); AddToClassList("utk-property-item-view--date-range"); if (!CreateUIFromUxml()) { CreateUIFallback(); } // UXML에서 요소 가져오기 QueryUIElements(); // 이벤트 등록 RegisterEvents(); UpdateValueUI(); UpdateReadOnlyState(); } private void QueryUIElements() { _startField = this.Q("start-field"); _startPickerBtn = this.Q("start-picker-btn"); _endField = this.Q("end-field"); _endPickerBtn = this.Q("end-picker-btn"); // Fallback: UXML에서 못 찾으면 생성 if (_valueContainer != null) { if (_startField == null) { _startField = new UTKInputField { name = "start-field" }; _startField.style.flexGrow = 1; _startField.AddToClassList("utk-property-item-view__field"); _valueContainer.Add(_startField); } if (_startPickerBtn == null) { _startPickerBtn = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary) { name = "start-picker-btn" }; _startPickerBtn.AddToClassList("utk-property-item-view__picker-btn"); _valueContainer.Add(_startPickerBtn); } // Separator 확인 및 생성 if (this.Q(className: "utk-property-item-view__range-separator") == null) { var separator = new UTKLabel("~", UTKLabel.LabelSize.Body2); separator.AddToClassList("utk-property-item-view__range-separator"); _valueContainer.Add(separator); } if (_endField == null) { _endField = new UTKInputField { name = "end-field" }; _endField.style.flexGrow = 1; _endField.AddToClassList("utk-property-item-view__field"); _valueContainer.Add(_endField); } if (_endPickerBtn == null) { _endPickerBtn = new UTKButton("...", "", UTKButton.ButtonVariant.Secondary) { name = "end-picker-btn" }; _endPickerBtn.AddToClassList("utk-property-item-view__picker-btn"); _valueContainer.Add(_endPickerBtn); } } // 초기 값 설정 if (_startField != null) { _startField.SetValue(_value.Start.ToString(_dateFormat), false); _startField.isReadOnly = true;//IsReadOnly와 상관 없이 편집 불가 } if (_endField != null) { _endField.SetValue(_value.End.ToString(_dateFormat), false); _endField.isReadOnly = true;//IsReadOnly와 상관 없이 편집 불가 } if (_startPickerBtn != null) { _startPickerBtn.IsEnabled = !IsReadOnly; } if (_endPickerBtn != null) { _endPickerBtn.IsEnabled = !IsReadOnly; } } private void RegisterEvents() { if (_startField != null) { _startField.OnValueChanged += OnStartTextChanged; } if (_endField != null) { _endField.OnValueChanged += OnEndTextChanged; } if (_startPickerBtn != null) { _startPickerBtn.OnClicked += OnStartPickerClicked; } if (_endPickerBtn != null) { _endPickerBtn.OnClicked += OnEndPickerClicked; } } private void UnregisterEvents() { if (_startField != null) { _startField.OnValueChanged -= OnStartTextChanged; } if (_endField != null) { _endField.OnValueChanged -= OnEndTextChanged; } if (_startPickerBtn != null) { _startPickerBtn.OnClicked -= OnStartPickerClicked; } if (_endPickerBtn != null) { _endPickerBtn.OnClicked -= OnEndPickerClicked; } } #endregion #region Override Methods protected override void CreateValueUI(VisualElement container) { // UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음 } public override void RefreshUI() { UpdateValueUI(); } protected override void OnReadOnlyStateChanged(bool isReadOnly) { if (_startField != null) _startField.isReadOnly = isReadOnly; if (_endField != null) _endField.isReadOnly = isReadOnly; if (_startPickerBtn != null) _startPickerBtn.style.display = isReadOnly ? DisplayStyle.None : DisplayStyle.Flex; if (_endPickerBtn != null) _endPickerBtn.style.display = isReadOnly ? DisplayStyle.None : DisplayStyle.Flex; } #endregion #region Event Handling private void OnStartPickerClicked() { _isEditingStart = true; OpenPicker(_value.Start); } private void OnEndPickerClicked() { _isEditingStart = false; OpenPicker(_value.End); } private void OnStartTextChanged(string newValue) { if (DateTime.TryParse(newValue, out DateTime date)) { var range = new UTKDateRange(date.Date, _value.End); if (!_value.Equals(range)) { _value = range; OnValueChanged?.Invoke(range); if (_boundData != null && !_boundData.Value.Equals(range)) { _boundData.Value = range; } } } } private void OnEndTextChanged(string newValue) { if (DateTime.TryParse(newValue, out DateTime date)) { var range = new UTKDateRange(_value.Start, date.Date); if (!_value.Equals(range)) { _value = range; OnValueChanged?.Invoke(range); if (_boundData != null && !_boundData.Value.Equals(range)) { _boundData.Value = range; } } } } private void OpenPicker(DateTime initialDate) { if (_currentPicker != null) return; var root = this as VisualElement; while (root.parent != null) root = root.parent; string title = _isEditingStart ? $"{Label} - 시작일" : $"{Label} - 종료일"; _currentPicker = UTKDatePicker.Show(root, initialDate, UTKDatePicker.PickerMode.DateOnly, title); _currentPicker.OnDateSelected += OnPickerDateSelected; _currentPicker.OnClosed += OnPickerClosed; } private void ClosePicker() { if (_currentPicker != null) { _currentPicker.OnDateSelected -= OnPickerDateSelected; _currentPicker.OnClosed -= OnPickerClosed; _currentPicker.Close(); _currentPicker = null; } } private void OnPickerDateSelected(DateTime date) { if (_isEditingStart) { Value = new UTKDateRange(date.Date, _value.End); } else { Value = new UTKDateRange(_value.Start, date.Date); } ClosePicker(); } private void OnPickerClosed() => _currentPicker = null; #endregion #region Value Update private void UpdateValueUI() { if (_startField != null) { var formatted = _value.Start.ToString(_dateFormat); if (_startField.Value != formatted) { _startField.SetValue(formatted, false); } } if (_endField != null) { var formatted = _value.End.ToString(_dateFormat); if (_endField.Value != formatted) { _endField.SetValue(formatted, false); } } } #endregion #region Data Binding public void Bind(IUTKPropertyItem data) { if (data is IUTKPropertyItem rangeData) { Bind(rangeData); } else { Debug.LogWarning($"[UTKDateRangePropertyItemView] Cannot bind to non-DateRange 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 UTKDateRangePropertyItem dateRangeItem) { _dateFormat = dateRangeItem.DateFormat; } data.OnTypedValueChanged += OnDataValueChanged; UpdateValueUI(); UpdateReadOnlyState(); } public void Unbind() { if (_boundData != null) { _boundData.OnTypedValueChanged -= OnDataValueChanged; _boundData = null; } } private void OnDataValueChanged(IUTKPropertyItem item, UTKDateRange oldValue, UTKDateRange newValue) { if (!_value.Equals(newValue)) { _value = newValue; UpdateValueUI(); } } #endregion #region Dispose protected override void Dispose(bool disposing) { if (_disposed) return; if (disposing) { ClosePicker(); UnregisterEvents(); Unbind(); OnValueChanged = null; _startField = null; _endField = null; _startPickerBtn = null; _endPickerBtn = null; } base.Dispose(disposing); } #endregion } }