단축키, UndoRedo 개발중

This commit is contained in:
logonkhi
2025-12-22 19:49:36 +09:00
parent ac071813f4
commit 94dd7782ae
61 changed files with 5433 additions and 1628 deletions

View File

@@ -13,13 +13,13 @@ namespace UVC.UI.Window
{
/// <summary>
/// 계층 데이터를 표시/검색/선택하는 창(View)입니다.
///
///
/// 책임:
/// - 메인 트리(`treeList`)와 검색 트리(`treeListSearch`)를 관리
/// - 입력창으로 검색을 수행하고 결과를 검색 트리에 표시(청크 처리 + 로딩 애니메이션)
/// - `TreeList.OnItemSelectionChanged`를 구독해 외부로 선택/해제 이벤트를 전달
/// - 외부에서 호출 가능한 간단한 항목 추가/삭제 API 제공(실제 렌더링/상태는 `TreeList`가 담당)
///
///
/// 사용 예:
/// <example>
/// <![CDATA[
@@ -65,6 +65,16 @@ namespace UVC.UI.Window
/// </summary>
public System.Action<TreeListItemData, bool>? OnItemVisibilityChanged;
/// <summary>
/// 메인/검색 리스트에서 항목이 삭제될 때 발생합니다 (Delete 키).
/// </summary>
public System.Action<TreeListItemData>? OnItemDeleted;
/// <summary>
/// 메인/검색 리스트에서 항목이 더블클릭될 때 발생합니다.
/// </summary>
public System.Action<TreeListItemData>? OnItemDoubleClicked;
// 검색 목록에서 선택된 항목(클론된 데이터)
protected TreeListItemData? selectedSearchItem;
@@ -97,6 +107,8 @@ namespace UVC.UI.Window
{
treeList.OnItemSelectionChanged += HandleMainSelectionChanged;
treeList.OnItemVisibilityChanged += HandleMainVisibilityChanged;
treeList.OnItemDataChanged += HandleMainDataChanged;
treeList.OnItemDoubleClicked += HandleMainDoubleClicked;
}
// 검색 리스트의 선택 변경을 감지 (선택 결과를 원본 트리에 반영하는 용도)
@@ -104,6 +116,8 @@ namespace UVC.UI.Window
{
treeListSearch.OnItemSelectionChanged += OnSearchSelectionChanged;
treeListSearch.OnItemVisibilityChanged += HandleMainVisibilityChanged;
treeListSearch.OnItemDataChanged += HandleMainDataChanged;
treeListSearch.OnItemDoubleClicked += HandleMainDoubleClicked;
}
clearTextButton.onClick.AddListener(() =>
@@ -362,6 +376,20 @@ namespace UVC.UI.Window
OnItemVisibilityChanged?.Invoke(data, isVisible);
}
protected void HandleMainDataChanged(ChangedType changedType, TreeListItemData data, int index)
{
// Delete 키로 삭제된 경우 외부 이벤트 발생
if (changedType == ChangedType.Delete)
{
OnItemDeleted?.Invoke(data);
}
}
protected void HandleMainDoubleClicked(TreeListItemData data)
{
OnItemDoubleClicked?.Invoke(data);
}
protected void OnInputFieldSubmit(string text)
{
// 검색어가 있으면 검색 결과 목록 표시
@@ -502,17 +530,23 @@ namespace UVC.UI.Window
{
treeListSearch.OnItemSelectionChanged -= OnSearchSelectionChanged;
treeListSearch.OnItemVisibilityChanged -= HandleMainVisibilityChanged;
treeListSearch.OnItemDataChanged -= HandleMainDataChanged;
treeListSearch.OnItemDoubleClicked -= HandleMainDoubleClicked;
}
if (treeList != null)
{
treeList.OnItemSelectionChanged -= HandleMainSelectionChanged;
treeList.OnItemVisibilityChanged -= HandleMainVisibilityChanged;
treeList.OnItemDataChanged -= HandleMainDataChanged;
treeList.OnItemDoubleClicked -= HandleMainDoubleClicked;
}
// 4. 외부 이벤트 핸들러 정리
OnItemSelected = null;
OnItemDeselected = null;
OnItemDeleted = null;
OnItemDoubleClicked = null;
// 5. 참조 정리
selectedSearchItem = null;

View File

@@ -17,5 +17,12 @@ namespace UVC.UI.Window.PropertyWindow
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부 (true: 비활성화, false: 활성화)</param>
void SetReadOnly(bool isReadOnly);
/// <summary>
/// UI에 표시되는 값을 업데이트합니다.
/// Undo/Redo 시 PropertyValueChanged 이벤트 없이 값을 반영할 때 사용됩니다.
/// </summary>
/// <param name="value">새로운 값</param>
void UpdateValue(object value);
}
}

View File

@@ -376,5 +376,27 @@ namespace UVC.UI.Window.PropertyWindow
}
#endregion
#region Property Value Update
/// <summary>
/// 특정 속성 아이템의 값을 UI에 반영합니다.
/// Undo/Redo 시 PropertyValueChanged 이벤트 없이 값을 반영할 때 사용됩니다.
/// </summary>
/// <param name="propertyId">속성 아이템의 ID</param>
/// <param name="value">새로운 값</param>
public void UpdatePropertyValue(string propertyId, object value)
{
if (_itemViews.TryGetValue(propertyId, out var itemView) && itemView != null)
{
var propertyUI = itemView.GetComponent<IPropertyUI>();
if (propertyUI != null)
{
propertyUI.UpdateValue(value);
}
}
}
#endregion
}
}

View File

@@ -457,6 +457,36 @@ namespace UVC.UI.Window.PropertyWindow
PropertyValueChanged?.Invoke(this, new PropertyValueChangedEventArgs(propertyId, propertyType, oldValue, newValue));
}
/// <summary>
/// 특정 ID를 가진 속성의 값을 설정합니다.
/// 이 메서드는 Undo/Redo 시 값을 복원할 때 사용됩니다.
/// PropertyValueChanged 이벤트를 발생시키지 않습니다.
/// </summary>
/// <param name="propertyId">값을 변경할 속성의 고유 ID</param>
/// <param name="value">새로운 값</param>
/// <returns>값이 성공적으로 설정되었는지 여부</returns>
public bool SetPropertyValue(string propertyId, object? value)
{
if (!_itemIndex.TryGetValue(propertyId, out var propertyItem))
{
Debug.LogWarning($"[PropertyWindow] ID '{propertyId}'에 해당하는 속성을 찾을 수 없습니다.");
return false;
}
if (value != null)
{
propertyItem.SetValue(value);
// View에 값 반영 (UI 업데이트)
if (_view != null)
{
_view.UpdatePropertyValue(propertyId, value);
}
}
return true;
}
#endregion
#region Internal Helpers

View File

@@ -111,6 +111,18 @@ namespace UVC.UI.Window.PropertyWindow.UI
}
}
/// <summary>
/// UI에 표시되는 값을 업데이트합니다.
/// </summary>
/// <param name="value">새로운 값</param>
public void UpdateValue(object value)
{
if (_valueToggle != null && value is bool boolValue)
{
_valueToggle.SetIsOnWithoutNotify(boolValue);
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// 메모리 누수를 방지하기 위해 등록된 리스너를 제거합니다.

View File

@@ -144,6 +144,19 @@ namespace UVC.UI.Window.PropertyWindow.UI
if (_colorPickerButton != null) _colorPickerButton.gameObject.SetActive(!isReadOnly);
}
/// <summary>
/// UI에 표시되는 값을 업데이트합니다.
/// </summary>
/// <param name="value">새로운 값</param>
public void UpdateValue(object value)
{
if (value is Color colorValue)
{
if (_colorPreviewImage != null) _colorPreviewImage.color = colorValue;
if (_colorLabel != null) _colorLabel.SetTextWithoutNotify(ColorUtil.ToHex(colorValue, true, false));
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -204,6 +204,23 @@ namespace UVC.UI.Window.PropertyWindow.UI
if (_colorPickerButton != null) _colorPickerButton.gameObject.SetActive(!isReadOnly);
}
/// <summary>
/// UI에 표시되는 값을 업데이트합니다.
/// </summary>
/// <param name="value">새로운 값</param>
public void UpdateValue(object value)
{
if (value is Tuple<string, string, Color?> colorStateValue)
{
if (_stateLabel != null) _stateLabel.SetTextWithoutNotify(colorStateValue.Item2);
if (colorStateValue.Item3.HasValue)
{
if (_colorPreviewImage != null) _colorPreviewImage.color = colorStateValue.Item3.Value;
if (_colorLabel != null) _colorLabel.SetTextWithoutNotify(ColorUtil.ToHex(colorStateValue.Item3.Value, true, false));
}
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -128,6 +128,18 @@ namespace UVC.UI.Window.PropertyWindow.UI
}
}
/// <summary>
/// UI에 표시되는 값을 업데이트합니다.
/// </summary>
/// <param name="value">새로운 값</param>
public void UpdateValue(object value)
{
if (value is DateTime dateValue && _dateText != null)
{
_dateText.SetTextWithoutNotify(dateValue.ToString(DateFormat));
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -171,6 +171,19 @@ namespace UVC.UI.Window.PropertyWindow.UI
}
}
/// <summary>
/// UI에 표시되는 값을 업데이트합니다.
/// </summary>
/// <param name="value">새로운 값</param>
public void UpdateValue(object value)
{
if (value is Tuple<DateTime, DateTime> dateRange)
{
if (_startDateText != null) _startDateText.SetTextWithoutNotify(dateRange.Item1.ToString(DateFormat));
if (_endDateText != null) _endDateText.SetTextWithoutNotify(dateRange.Item2.ToString(DateFormat));
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -192,6 +192,20 @@ namespace UVC.UI.Window.PropertyWindow.UI
if (_miniteDropDown != null) _miniteDropDown.interactable = !isReadOnly;
}
/// <summary>
/// UI에 표시되는 값을 업데이트합니다.
/// </summary>
/// <param name="value">새로운 값</param>
public void UpdateValue(object value)
{
if (value is DateTime dateTimeValue)
{
if (_dateText != null) _dateText.SetTextWithoutNotify(dateTimeValue.ToString(DateFormat));
if (_hourDropDown != null) _hourDropDown.SetValueWithoutNotify(dateTimeValue.Hour);
if (_miniteDropDown != null) _miniteDropDown.SetValueWithoutNotify(dateTimeValue.Minute);
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -303,6 +303,23 @@ namespace UVC.UI.Window.PropertyWindow.UI
if (_endMiniteDropDown != null) _endMiniteDropDown.interactable = !isReadOnly;
}
/// <summary>
/// UI에 표시되는 값을 업데이트합니다.
/// </summary>
/// <param name="value">새로운 값</param>
public void UpdateValue(object value)
{
if (value is Tuple<DateTime, DateTime> dateTimeRange)
{
if (_startDateText != null) _startDateText.SetTextWithoutNotify(dateTimeRange.Item1.ToString(DateFormat));
if (_endDateText != null) _endDateText.SetTextWithoutNotify(dateTimeRange.Item2.ToString(DateFormat));
if (_startHourDropDown != null) _startHourDropDown.SetValueWithoutNotify(dateTimeRange.Item1.Hour);
if (_startMiniteDropDown != null) _startMiniteDropDown.SetValueWithoutNotify(dateTimeRange.Item1.Minute);
if (_endHourDropDown != null) _endHourDropDown.SetValueWithoutNotify(dateTimeRange.Item2.Hour);
if (_endMiniteDropDown != null) _endMiniteDropDown.SetValueWithoutNotify(dateTimeRange.Item2.Minute);
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -123,6 +123,22 @@ namespace UVC.UI.Window.PropertyWindow.UI
if (_dropdown != null) _dropdown.interactable = !isReadOnly;
}
/// <summary>
/// UI에 표시되는 값을 업데이트합니다.
/// </summary>
/// <param name="value">새로운 값</param>
public void UpdateValue(object value)
{
if (_dropdown != null && _enumValues != null && value != null)
{
int index = Array.IndexOf(_enumValues, value);
if (index >= 0)
{
_dropdown.SetValueWithoutNotify(index);
}
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -120,6 +120,22 @@ namespace UVC.UI.Window.PropertyWindow.UI
if (_dropdown != null) _dropdown.interactable = !isReadOnly;
}
/// <summary>
/// UI에 표시되는 값을 업데이트합니다.
/// </summary>
/// <param name="value">새로운 값</param>
public void UpdateValue(object value)
{
if (_dropdown != null && _propertyItem?.ItemsSource != null && value is string stringValue)
{
int index = _propertyItem.ItemsSource.IndexOf(stringValue);
if (index >= 0)
{
_dropdown.SetValueWithoutNotify(index);
}
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -215,6 +215,32 @@ namespace UVC.UI.Window.PropertyWindow.UI
}
}
/// <summary>
/// UI에 표시되는 값을 업데이트합니다.
/// </summary>
/// <param name="value">새로운 값</param>
public void UpdateValue(object value)
{
if (value == null) return;
string textValue = value.ToString();
if (_valueInput != null)
{
_valueInput.SetTextWithoutNotify(textValue);
}
if (_slider != null && _slider.gameObject.activeSelf)
{
if (value is int intValue)
{
_slider.SetValueWithoutNotify(intValue);
}
else if (value is float floatValue)
{
_slider.SetValueWithoutNotify(floatValue);
}
}
}
private void OnDestroy()
{
// 오브젝트가 파괴될 때 리스너를 확실히 제거합니다.

View File

@@ -195,6 +195,24 @@ namespace UVC.UI.Window.PropertyWindow.UI
if (_maxInputField != null) _maxInputField.interactable = !isReadOnly;
}
/// <summary>
/// UI에 표시되는 값을 업데이트합니다.
/// </summary>
/// <param name="value">새로운 값</param>
public void UpdateValue(object value)
{
if (value is Tuple<int, int> intRange)
{
if (_minInputField != null) _minInputField.SetTextWithoutNotify(intRange.Item1.ToString(CultureInfo.InvariantCulture));
if (_maxInputField != null) _maxInputField.SetTextWithoutNotify(intRange.Item2.ToString(CultureInfo.InvariantCulture));
}
else if (value is Tuple<float, float> floatRange)
{
if (_minInputField != null) _minInputField.SetTextWithoutNotify(floatRange.Item1.ToString(CultureInfo.InvariantCulture));
if (_maxInputField != null) _maxInputField.SetTextWithoutNotify(floatRange.Item2.ToString(CultureInfo.InvariantCulture));
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -159,6 +159,21 @@ namespace UVC.UI.Window.PropertyWindow.UI
}
}
/// <summary>
/// UI에 표시되는 값을 업데이트합니다.
/// </summary>
/// <param name="value">새로운 값</param>
public void UpdateValue(object value)
{
if (_propertyItem?.ItemsSource == null || value is not string stringValue) return;
int index = _propertyItem.ItemsSource.IndexOf(stringValue);
if (index >= 0 && index < _toggles.Count)
{
_toggles[index].SetIsOnWithoutNotify(true);
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -104,6 +104,18 @@ namespace UVC.UI.Window.PropertyWindow.UI
}
}
/// <summary>
/// UI에 표시되는 값을 업데이트합니다.
/// </summary>
/// <param name="value">새로운 값</param>
public void UpdateValue(object value)
{
if (_valueInput != null && value != null)
{
_valueInput.SetTextWithoutNotify(value.ToString());
}
}
private void OnDestroy()
{
// 오브젝트가 파괴될 때 리스너를 확실히 제거합니다.

View File

@@ -183,6 +183,19 @@ namespace UVC.UI.Window.PropertyWindow.UI
if (_yInputField != null) _yInputField.interactable = !isReadOnly;
}
/// <summary>
/// UI에 표시되는 값을 업데이트합니다.
/// </summary>
/// <param name="value">새로운 값</param>
public void UpdateValue(object value)
{
if (value is Vector2 vector2Value)
{
if (_xInputField != null) _xInputField.SetTextWithoutNotify(vector2Value.x.ToString(CultureInfo.InvariantCulture));
if (_yInputField != null) _yInputField.SetTextWithoutNotify(vector2Value.y.ToString(CultureInfo.InvariantCulture));
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -216,6 +216,20 @@ namespace UVC.UI.Window.PropertyWindow.UI
if (_zInputField != null) _zInputField.interactable = !isReadOnly;
}
/// <summary>
/// UI에 표시되는 값을 업데이트합니다.
/// </summary>
/// <param name="value">새로운 값</param>
public void UpdateValue(object value)
{
if (value is Vector3 vector3Value)
{
if (_xInputField != null) _xInputField.SetTextWithoutNotify(vector3Value.x.ToString(CultureInfo.InvariantCulture));
if (_yInputField != null) _yInputField.SetTextWithoutNotify(vector3Value.y.ToString(CultureInfo.InvariantCulture));
if (_zInputField != null) _zInputField.SetTextWithoutNotify(vector3Value.z.ToString(CultureInfo.InvariantCulture));
}
}
/// <summary>
/// 이 UI 오브젝트가 파괴될 때 Unity에 의해 호출됩니다.
/// </summary>

View File

@@ -89,6 +89,26 @@ namespace UVC.UI.Window.PropertyWindow
// 기본 구현 없음 - 파생 클래스에서 필요시 구현
}
/// <summary>
/// UI에 표시되는 값을 업데이트합니다.
/// Undo/Redo 시 PropertyValueChanged 이벤트 없이 값을 반영할 때 사용됩니다.
/// </summary>
/// <param name="value">새로운 값</param>
public virtual void UpdateValue(object value)
{
// 파생 클래스에서 구체적인 구현을 제공해야 합니다.
ApplyValueToUI(value);
}
/// <summary>
/// 파생 클래스에서 UI에 값을 적용합니다.
/// </summary>
/// <param name="value">적용할 값</param>
protected virtual void ApplyValueToUI(object value)
{
// 기본 구현 없음 - 파생 클래스에서 필요시 구현
}
#endregion
#region Common UI Setup