개발중

This commit is contained in:
logonkhi
2025-11-12 12:28:17 +09:00
parent 0443abbfab
commit 8850f51193
54 changed files with 6131 additions and 2919 deletions

View File

@@ -429,7 +429,7 @@ namespace Gpm.Ui
}
/// <summary>
/// 내부 ScrollRect의 onValueChanged 이벤트에 리스너를 추가합니다.
/// 내부 ScrollRect의 OnValueChanged 이벤트에 리스너를 추가합니다.
/// 사용자가 스크롤을 움직일 때마다 콜백을 받고 싶을 때 사용합니다.
/// </summary>
/// <param name="listener">추가할 리스너(콜백 함수)</param>
@@ -544,7 +544,7 @@ namespace Gpm.Ui
return ItemPostionFromOffset(move);
}
// ScrollRect의 onValueChanged 이벤트에 연결되어 호출되는 메서드
// ScrollRect의 OnValueChanged 이벤트에 연결되어 호출되는 메서드
private void OnValueChanged(Vector2 value)
{
// 스크롤이 시작/끝 지점에 도달했는지 확인하고 이벤트를 발생시킵니다.

8
Assets/Scripts/SHI.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1081c3de3d7409d4e8254544beb39649
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1e58e2024a2f4ec448732b865cc98821
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,39 @@
using UnityEngine;
using UnityEngine.UI;
using UVC.UI.Buttons;
using UVC.UI.Window;
namespace SHI.modal
{
public class BlockDetailModal: MonoBehaviour
{
[SerializeField]
private Button closeButton;
[SerializeField]
private HierarchyWindow hierarchyWindow;
[SerializeField]
private ModelDetailView modelView;
[SerializeField]
private Transform chartView;
[SerializeField]
private Button modelViewExpandButton;
[SerializeField]
private Button chartViewExpandButton;
[SerializeField]
private Button dragButton;
[SerializeField]
private ImageToggle showHierarchyButton;
public void Start()
{
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: daf98703dca342f458a0cf5df71adddb

View File

@@ -0,0 +1,43 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UVC.UI.Buttons;
using UVC.UI.List.Tree;
namespace SHI.modal
{
public class ModelDetailListItem: TreeListItem
{
/// <summary>
/// 가시성 상태를 표시하는 배경 이미지.
/// </summary>
[SerializeField]
protected ImageToggle visibleToggle;
public virtual void Init(TreeListItemData data, TreeList control, TreeListDragDropManager dragDropManager)
{
base.Init(data, control, dragDropManager);
if (visibleToggle != null)
{
visibleToggle.OnValueChanged.AddListener(isOn =>
{
if(data is ModelDetailListItemData modelData)
{
modelData.IsVisible = isOn;
modelData.OnClickVisibleAction?.Invoke(modelData, isOn);
}
});
}
}
protected override void OnDestroy()
{
if (visibleToggle != null) visibleToggle.OnValueChanged.RemoveAllListeners();
base.OnDestroy();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1e59b82317d5c194ab2ab4e87730e631

View File

@@ -0,0 +1,16 @@
#nullable enable
using System;
using UVC.UI.List.Tree;
namespace SHI.modal
{
public class ModelDetailListItemData : TreeListItemData
{
/// <summary>
/// 아이템의 가시성 아이콘 클릭 시 실행할 사용자 정의 동작.
/// </summary>
public Action<TreeListItemData, bool>? OnClickVisibleAction;
public bool IsVisible { get; set; } = true;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 21fbea5ce75b98f489f259fc7cf88fe3

View File

@@ -0,0 +1,395 @@
#nullable enable
using Cysharp.Threading.Tasks;
using DG.Tweening;
using System.Linq;
using System.Threading;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.UI.List.Tree;
namespace SHI.modal
{
/// <summary>
/// 계층 데이터를 표시/검색/선택하는 창(View)입니다.
///
/// 책임:
/// - 메인 트리(`treeList`)와 검색 트리(`treeListSearch`)를 관리
/// - 입력창으로 검색을 수행하고 결과를 검색 트리에 표시(청크 처리 + 로딩 애니메이션)
/// - `TreeList.OnItemSelectionChanged`를 구독해 외부로 선택/해제 이벤트를 전달
/// - 외부에서 호출 가능한 간단한 항목 추가/삭제 API 제공(실제 렌더링/상태는 `TreeList`가 담당)
///
/// 사용 예:
/// <example>
/// <![CDATA[
/// // 외부에서 창을 참조했다고 가정
/// accordionWindow.OnItemSelected += item => Debug.Log($"Selected: {item.Name}");
/// accordionWindow.AddItem(new TreeListItemData("Root A"));
/// accordionWindow.AddItemAt(new TreeListItemData("Root B"),0);
/// ]]>
/// </example>
/// </summary>
public class ModelDetailListView : MonoBehaviour
{
[SerializeField]
protected TreeList treeList;
/// <summary>
/// 검색 결과 용 목록
/// </summary>
[SerializeField]
protected TreeList treeListSearch;
[SerializeField]
protected TMP_InputField inputField;
[SerializeField]
protected Button clearTextButton;
[SerializeField]
protected Image loadingImage;
/// <summary>
/// 메인/검색 리스트에서 항목이 선택될 때 발생합니다.
/// </summary>
public System.Action<TreeListItemData>? OnItemSelected;
/// <summary>
/// 메인/검색 리스트에서 항목이 선택 해제될 때 발생합니다.
/// </summary>
public System.Action<TreeListItemData>? OnItemDeselected;
// 검색 목록에서 선택된 항목(클론된 데이터)
protected TreeListItemData? selectedSearchItem;
// 검색 작업 상태
protected CancellationTokenSource? searchCts;
protected bool isSearching = false;
protected float searchProgress = 0f; // 내부 진행도 추적용
[SerializeField]
[Tooltip("로딩 아이콘 회전 속도(도/초)")]
protected float loadingRotateSpeed = 360f;
[SerializeField]
[Tooltip("로딩 이미지의 채우기 애니메이션 속도(사이클/초)")]
protected float loadingFillCycle = 0.5f; // cycles per second. loadingRotateSpeed360 일때, loadingFillCycle를0.5 보다 높게 설정하면 이상해 보임
// DOTween tweens
protected Tween? loadingRotationTween;
protected Tween? loadingFillTween;
protected void Awake()
{
loadingImage.gameObject.SetActive(false);
treeListSearch.gameObject.SetActive(false);
inputField.onSubmit.AddListener(OnInputFieldSubmit);
// 메인 리스트 선택 변경을 외부 이벤트로 전달
if (treeList != null)
{
treeList.OnItemSelectionChanged += HandleMainSelectionChanged;
}
// 검색 리스트의 선택 변경을 감지 (선택 결과를 원본 트리에 반영하는 용도)
if (treeListSearch != null)
{
treeListSearch.OnItemSelectionChanged += OnSearchSelectionChanged;
}
clearTextButton.onClick.AddListener(() =>
{
inputField.text = string.Empty;
// 취소
CancelSearch();
treeListSearch.gameObject.SetActive(false);
treeList.gameObject.SetActive(true);
// 검색에서 선택한 항목이 있으면 원본 트리에서 동일 항목을 선택하고 펼침
if (selectedSearchItem != null && treeList != null)
{
// 원본 데이터 찾기 (TreeListItemData == 연산자는 Id 기반)
var target = treeList.AllItemDataFlattened.FirstOrDefault(i => i == selectedSearchItem);
if (target != null)
{
// 부모 체인을 펼치고 선택 처리
treeList.RevealAndSelectItem(target, true);
}
selectedSearchItem = null;
}
});
}
/// <summary>
/// 메인 트리에 항목을 추가합니다.
/// </summary>
/// <param name="data">추가할 데이터.</param>
public void AddItem(TreeListItemData data)
{
treeList.AddItem(data);
}
/// <summary>
/// 메인 트리에 항목을 특정 인덱스에 삽입합니다.
/// </summary>
/// <param name="data">삽입할 데이터.</param>
/// <param name="index">삽입 인덱스(0 기반).</param>
public void AddItemAt(TreeListItemData data, int index)
{
treeList.AddItemAt(data, index);
}
/// <summary>
/// 메인 트리에서 항목을 제거합니다(뷰만 제거, 데이터는 호출 측 정책에 따름).
/// </summary>
/// <param name="data">제거할 데이터.</param>
public void RemoveItem(TreeListItemData data)
{
treeList.RemoveItem(data);
}
/// <summary>
/// 메인 트리에서 항목을 완전히 삭제합니다(뷰+데이터 정리).
/// </summary>
/// <param name="data">삭제할 데이터.</param>
public void DeleteItem(TreeListItemData data)
{
treeList.DeleteItem(data);
}
/// <summary>
/// 이름으로 아이템 선택
/// </summary>
/// <param name="name"></param>
public void SelectItem(string name)
{
//검색 중이면 취소
CancelSearch();
treeListSearch.gameObject.SetActive(false);
treeList.gameObject.SetActive(true);
treeList.SelectItem(name);
}
protected void StartLoadingAnimation()
{
if (loadingImage == null) return;
// 기존 트윈 정리
StopLoadingAnimation();
loadingImage.fillAmount = 0f;
loadingImage.transform.localRotation = Quaternion.identity;
loadingImage.gameObject.SetActive(true);
// 회전 트윈
float rotDuration = (loadingRotateSpeed != 0f) ? (360f / Mathf.Abs(loadingRotateSpeed)) : 1f;
loadingRotationTween = loadingImage.transform
.DOLocalRotate(new Vector3(0f, 0f, -360f), rotDuration, RotateMode.LocalAxisAdd)
.SetEase(Ease.Linear)
.SetLoops(-1, LoopType.Restart);
// 채우기 트윈
float fullDuration = (loadingFillCycle > 0f) ? (1f / loadingFillCycle) : 1f;
loadingFillTween = DOTween
.To(() => loadingImage.fillAmount, x => loadingImage.fillAmount = x, 1f, fullDuration)
.SetEase(Ease.InOutSine)
.SetLoops(-1, LoopType.Yoyo);
}
protected void StopLoadingAnimation()
{
if (loadingRotationTween != null)
{
loadingRotationTween.Kill();
loadingRotationTween = null;
}
if (loadingFillTween != null)
{
loadingFillTween.Kill();
loadingFillTween = null;
}
if (loadingImage != null)
{
loadingImage.gameObject.SetActive(false);
loadingImage.transform.localRotation = Quaternion.identity;
loadingImage.fillAmount = 0f;
}
}
protected void CancelSearch()
{
if (searchCts != null)
{
try { searchCts.Cancel(); } catch { }
searchCts.Dispose();
searchCts = null;
}
isSearching = false;
searchProgress = 0f;
StopLoadingAnimation();
}
protected async void OnSearchSelectionChanged(TreeListItemData data, bool isSelected)
{
if (isSelected)
{
selectedSearchItem = data;
OnItemSelected?.Invoke(data);
}
else if (selectedSearchItem == data)
{
selectedSearchItem = null;
OnItemDeselected?.Invoke(data);
}
}
protected void HandleMainSelectionChanged(TreeListItemData data, bool isSelected)
{
if (isSelected)
{
OnItemSelected?.Invoke(data);
}
else
{
OnItemDeselected?.Invoke(data);
}
}
protected void OnInputFieldSubmit(string text)
{
// 기존 검색 취소
CancelSearch();
// 검색어가 있으면 검색 결과 목록 표시
if (!string.IsNullOrEmpty(text))
{
treeListSearch.gameObject.SetActive(true);
treeList.gameObject.SetActive(false);
// 시작 애니메이션
StartLoadingAnimation();
searchCts = new CancellationTokenSource();
// 비동기 검색 실행(UITask 스타일: 메인스레드에서 작업을 분할하여 UI가 멈추지 않게 함)
_ = PerformSearchAsync(text, searchCts.Token);
}
else
{
treeListSearch.gameObject.SetActive(false);
treeList.gameObject.SetActive(true);
}
}
/// <summary>
/// 검색을 메인 스레드에서 분할 처리하여 UI 업데이트(로딩 애니메이션)가 가능하도록 구현합니다.
/// </summary>
protected async UniTaskVoid PerformSearchAsync(string text, CancellationToken token)
{
isSearching = true;
searchProgress = 0f;
var results = new System.Collections.Generic.List<TreeListItemData>();
var sourceList = treeList?.AllItemDataFlattened;
if (sourceList == null)
{
isSearching = false;
StopLoadingAnimation();
return;
}
int total = sourceList.Count;
if (total == 0)
{
isSearching = false;
StopLoadingAnimation();
return;
}
// 소문자 비교 준비
string lower = text.ToLowerInvariant();
// 분할 처리: 일정 갯수마다 await으로 제어권을 반환
const int chunk = 100;
for (int i = 0; i < total; i++)
{
token.ThrowIfCancellationRequested();
var item = sourceList[i];
if (!string.IsNullOrEmpty(item.Name) && item.Name.ToLowerInvariant().Contains(lower))
{
results.Add(item);
}
// 진행도 업데이트 (내부 사용)
if ((i % chunk) == 0)
{
searchProgress = (float)i / (float)total;
await UniTask.Yield(PlayerLoopTiming.Update);
}
}
// 최종 진행도
searchProgress = 1f;
// UI 반영은 메인 스레드에서
if (!PlayerLoopHelper.IsMainThread) await UniTask.SwitchToMainThread();
try
{
if (token.IsCancellationRequested) return;
treeListSearch.ClearItems();
foreach (var r in results)
{
treeListSearch.AddItem(r.Clone());
}
// 로딩 종료
isSearching = false;
searchProgress = 0f;
StopLoadingAnimation();
}
catch (System.Exception ex)
{
Debug.LogError($"PerformSearchAsync error: {ex}");
}
finally
{
if (searchCts != null)
{
searchCts.Dispose();
searchCts = null;
}
}
}
protected void OnDestroy()
{
inputField.onSubmit.RemoveListener(OnInputFieldSubmit);
clearTextButton.onClick.RemoveAllListeners();
if (treeListSearch != null)
{
treeListSearch.OnItemSelectionChanged -= OnSearchSelectionChanged;
}
if (treeList != null)
{
treeList.OnItemSelectionChanged -= HandleMainSelectionChanged;
}
CancelSearch();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 59c744534c4342f4b9d032ed55beb194

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
namespace SHI.modal
{
public class ModelDetailView: MonoBehaviour
{
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6f867baed0364ea4bb3ce7a26ce84bdf

View File

@@ -0,0 +1,187 @@
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace UVC.UI.Buttons
{
/// <summary>
/// Unity 기본 Toggle을 상속하지 않고 직접 토글 기능을 구현한 이미지 토글.
/// isOn 상태에 따라 targetImage의 Sprite를 교체합니다.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// using UnityEngine;
/// using UVC.UI.Buttons; // ImageToggle 네임스페이스
/// using UnityEngine.UI;
///
/// public class ImageToggleSample : MonoBehaviour
/// {
/// [SerializeField] private ImageToggle imageToggle; // Hierarchy에 있는 ImageToggle 할당
/// [SerializeField] private Image targetImage; // Sprite를 교체할 Image
/// [SerializeField] private Sprite onSprite;
/// [SerializeField] private Sprite offSprite;
/// [SerializeField] private Text stateText; // 상태 출력용 (선택)
///
/// void Awake()
/// {
/// // 런타임 동적 생성도 가능
/// // var go = new GameObject("DynamicImageToggle", typeof(RectTransform));
/// // imageToggle = go.AddComponent<ImageToggle>();
/// }
///
/// void Start()
/// {
/// // 필드 연결
/// imageToggle.SetIsOnWithoutNotify(false); // 초기 상태 강제 설정(이벤트 없이)
///
/// // targetImage 및 Sprite 지정
/// var so = new SerializedObject(imageToggle); // (직접 참조 가능하지만 예시로 표시)
/// imageToggle.GetType(); // 의미 없는 호출(예시 컴파일 방지 목적) -> 실제 코드는 제거 가능
/// imageToggle.gameObject.name = "SampleImageToggle";
/// // 에디터에서는 인스펙터에서 설정하는 것이 일반적
///
/// // 리스너 등록
/// imageToggle.OnValueChanged.AddListener(OnToggleChanged);
///
/// // 초기 텍스트 표시
/// if (stateText != null)
/// stateText.text = imageToggle.isOn ? "ON" : "OFF";
/// }
/// }
///
/// private void OnToggleChanged(bool value)
/// {
/// if (stateText != null)
/// stateText.text = value ? "ON" : "OFF";
/// }
///
/// // 외부에서 호출하여 상태를 강제로 변경하는 메서드 예시
/// public void ForceOn()
/// {
/// imageToggle.isOn = true;
/// }
/// }
/// ]]>
/// </code>
/// </example>
public class ImageToggle : Selectable, IPointerClickHandler, ISubmitHandler
{
#region
/// <summary>
/// 토글 값 변경 시(bool) 호출되는 UnityEvent.
/// </summary>
[System.Serializable]
public class ImageToggleEvent : UnityEvent<bool> { }
#endregion
[Header("Sprite 교체 대상")]
[Tooltip("Sprite를 교체할 대상 Image 컴포넌트")]
[SerializeField] private Image targetImage;
[Header("상태별 Sprite")]
[Tooltip("토글이 켜짐(isOn=true) 상태일 때 사용할 Sprite")]
[SerializeField] private Sprite onSprite;
[Tooltip("토글이 꺼짐(isOn=false) 상태일 때 사용할 Sprite")]
[SerializeField] private Sprite offSprite;
[Header("이벤트")]
[Tooltip("토글 값이 변경될 때 호출되는 이벤트")]
public ImageToggleEvent OnValueChanged = new ImageToggleEvent();
[Header("초기 상태")]
[Tooltip("시작 시 토글이 켜짐 상태인지 여부")]
[SerializeField] private bool m_IsOn = false;
/// <summary>
/// 현재 토글 상태(읽기/쓰기). 값을 설정하면 시각과 이벤트를 갱신합니다.
/// </summary>
public bool isOn
{
get => m_IsOn;
set => Set(value);
}
/// <summary>
/// isOn 값을 이벤트 없이 변경합니다.
/// </summary>
public void SetIsOnWithoutNotify(bool value)
{
Set(value, false);
}
/// <summary>
/// 내부 설정 로직. 상태가 변경되면 비주얼과 콜백을 처리합니다.
/// </summary>
private void Set(bool value, bool sendCallback = true)
{
if (m_IsOn == value) // 동일 값이면 무시
return;
m_IsOn = value;
UpdateVisual();
if (sendCallback)
{
OnValueChanged.Invoke(m_IsOn);
}
}
protected override void Start()
{
base.Start();
UpdateVisual();
}
protected override void OnEnable()
{
base.OnEnable();
UpdateVisual();
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
UpdateVisual();
}
#endif
/// <summary>
/// 현재 isOn 상태에 맞게 targetImage Sprite를 교체.
/// </summary>
private void UpdateVisual()
{
if (targetImage == null)
return;
targetImage.sprite = m_IsOn ? onSprite : offSprite;
}
/// <summary>
/// 토글 상태를 반전시킵니다.
/// </summary>
private void Toggle()
{
if (!IsActive() || !IsInteractable())
return;
isOn = !isOn; // Set 통해 처리
}
/// <summary>
/// 마우스 클릭 처리(왼쪽 버튼).
/// </summary>
public void OnPointerClick(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
Toggle();
}
/// <summary>
/// 키보드/게임패드 Submit 처리.
/// </summary>
public void OnSubmit(BaseEventData eventData)
{
Toggle();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d5b3b44478924734bb216de085616f87

View File

@@ -128,12 +128,12 @@ namespace UVC.UI.List.Tree
public List<TreeListItem> TreeLists => root.GetComponentsInChildren<TreeListItem>().ToList();
// 뷰 매핑: 데이터 ↔ View O(1) 조회
private readonly Dictionary<TreeListItemData, TreeListItem> _viewMap = new Dictionary<TreeListItemData, TreeListItem>();
protected readonly Dictionary<TreeListItemData, TreeListItem> _viewMap = new Dictionary<TreeListItemData, TreeListItem>();
// 플래튼 업데이트 스로틀링 플래그
private bool _flattenScheduled;
protected bool _flattenScheduled;
private void Awake()
protected void Awake()
{
// 드래그 & 드롭 이벤트 구독
if (enableDragDrop)
@@ -142,7 +142,7 @@ namespace UVC.UI.List.Tree
}
}
private void OnDestroy()
protected void OnDestroy()
{
if (enableDragDrop)
{
@@ -152,7 +152,7 @@ namespace UVC.UI.List.Tree
if (OnItemSelectionChanged != null) OnItemSelectionChanged = null;
}
private void Update()
protected void Update()
{
// Escape 키 입력 감지 - 선택 해제
if (Input.GetKeyDown(KeyCode.Escape))
@@ -235,7 +235,7 @@ namespace UVC.UI.List.Tree
///
/// 용도: 사용자가 선택을 취소하고 싶을 때 빠른 해제
/// </summary>
private void HandleEscapeKeyPressed()
protected void HandleEscapeKeyPressed()
{
// 선택된 아이템이 없으면 아무것도 하지 않음
if (selectedItems.Count == 0)
@@ -264,7 +264,7 @@ namespace UVC.UI.List.Tree
/// 주의: 반복 중 리스트 수정으로 인한 문제 방지를 위해
/// ToList()로 복사 후 순회합니다.
/// </summary>
private void HandleDeleteKeyPressed()
protected void HandleDeleteKeyPressed()
{
// 선택된 아이템이 없으면 아무것도 하지 않음
if (selectedItems.Count == 0)
@@ -297,7 +297,7 @@ namespace UVC.UI.List.Tree
///
/// 용도: 화살표 키 네비게이션 시 보이는 아이템만 선택 가능
/// </summary>
private List<TreeListItemData> GetVisibleFlattenedItems()
protected List<TreeListItemData> GetVisibleFlattenedItems()
{
List<TreeListItemData> visibleItems = new List<TreeListItemData>();
@@ -320,7 +320,7 @@ namespace UVC.UI.List.Tree
/// </summary>
/// <param name="item">현재 처리할 아이템</param>
/// <param name="visibleItems">수집 결과 리스트</param>
private void AddVisibleItemsRecursive(TreeListItemData item, List<TreeListItemData> visibleItems)
protected void AddVisibleItemsRecursive(TreeListItemData item, List<TreeListItemData> visibleItems)
{
// 현재 아이템을 리스트에 추가
visibleItems.Add(item);
@@ -345,7 +345,7 @@ namespace UVC.UI.List.Tree
///
/// 제약: 단일 선택 상태에서만 작동
/// </summary>
private void HandleUpArrowKeyPressed()
protected void HandleUpArrowKeyPressed()
{
// 정확히 하나의 아이템만 선택되어 있어야 함
if (selectedItems.Count != 1)
@@ -396,7 +396,7 @@ namespace UVC.UI.List.Tree
///
/// 제약: 단일 선택 상태에서만 작동
/// </summary>
private void HandleDownArrowKeyPressed()
protected void HandleDownArrowKeyPressed()
{
// 펼쳐진 아이템들만 포함하는 임시 리스트 생성
List<TreeListItemData> visibleItems = GetVisibleFlattenedItems();
@@ -462,7 +462,7 @@ namespace UVC.UI.List.Tree
///
/// 제약: 이미 펼쳐진 아이템은 동작 없음
/// </summary>
private void HandleRightArrowKeyPressed()
protected void HandleRightArrowKeyPressed()
{
// 선택된 아이템이 없으면 아무것도 하지 않음
if (selectedItems.Count == 0)
@@ -501,7 +501,7 @@ namespace UVC.UI.List.Tree
///
/// 제약: 이미 접혀진 아이템은 동작 없음
/// </summary>
private void HandleLeftArrowKeyPressed()
protected void HandleLeftArrowKeyPressed()
{
// 선택된 아이템이 없으면 아무것도 하지 않음
if (selectedItems.Count == 0)
@@ -764,7 +764,7 @@ namespace UVC.UI.List.Tree
/// 빠른 인덱스 검색 및 범위 선택을 가능하게 합니다.
/// </summary>
/// <param name="data">현재 처리할 아이템</param>
private void AddItemDataToFlattened(TreeListItemData data)
protected void AddItemDataToFlattened(TreeListItemData data)
{
// 현재 아이템을 평탄화 리스트에 추가
allItemDataFlattened.Add(data);
@@ -866,7 +866,7 @@ namespace UVC.UI.List.Tree
/// </summary>
/// <param name="startItem">범위 시작 아이템</param>
/// <param name="endItem">범위 끝 아이템</param>
private void SelectRange(TreeListItemData startItem, TreeListItemData endItem)
protected void SelectRange(TreeListItemData startItem, TreeListItemData endItem)
{
// 평탄화 리스트에서 시작 아이템의 위치(인덱스) 찾기
int startIndex = allItemDataFlattened.IndexOf(startItem);
@@ -916,6 +916,19 @@ namespace UVC.UI.List.Tree
}
}
/// <summary>
/// 이름으로 아이템 선택
/// </summary>
/// <param name="name"></param>
public void SelectItem(string name)
{
var item = allItemDataFlattened.FirstOrDefault(x => x.Name == name);
if (item != null)
{
SelectItem(item);
}
}
/// <summary>
/// 아이템 선택
///
@@ -1036,7 +1049,7 @@ namespace UVC.UI.List.Tree
/// </summary>
/// <param name="draggedItem">드래그된 아이템</param>
/// <param name="targetItem">드롭 대상 아이템 (null: 루트)</param>
private void HandleItemDropped(TreeListItemData draggedItem, TreeListItemData? targetItem)
protected void HandleItemDropped(TreeListItemData draggedItem, TreeListItemData? targetItem)
{
// 평탄화 리스트 업데이트
UpdateFlattenedItemDataList();

View File

@@ -111,7 +111,7 @@ namespace UVC.UI.List.Tree
/// - control: 상위 TreeList
/// - dragDropManager: 드래그 & 드롭 매니저
/// </summary>
public void Init(TreeListItemData data, TreeList control, TreeListDragDropManager dragDropManager)
public virtual void Init(TreeListItemData data, TreeList control, TreeListDragDropManager dragDropManager)
{
// 1. 기본 정보 할당
this.treeList = control;
@@ -188,7 +188,7 @@ namespace UVC.UI.List.Tree
/// - changedData: 변경 대상 데이터(자식 추가 등일 때 유효)
/// - index: 삽입/이동 시 기준 인덱스(해당되는 경우)
/// </summary>
private void OnDataChanged(ChangedType changedType, TreeListItemData changedData, int index)
protected void OnDataChanged(ChangedType changedType, TreeListItemData changedData, int index)
{
if (data == null) return;
@@ -326,7 +326,7 @@ namespace UVC.UI.List.Tree
/// - changedData: 변경된 데이터(자기 자신)
/// - isSelected: 선택 여부
/// </summary>
private void OnSelectionChanged(TreeListItemData changedData, bool isSelected)
protected void OnSelectionChanged(TreeListItemData changedData, bool isSelected)
{
// 선택 상태 UI 업데이트 (배경 표시/숨김)
UpdateSelectionUI();
@@ -335,7 +335,7 @@ namespace UVC.UI.List.Tree
/// <summary>
/// 선택 상태에 따라 배경 표시/숨김을 갱신합니다.
/// </summary>
private void UpdateSelectionUI()
protected void UpdateSelectionUI()
{
if (data == null) return;
// IsSelected 상태에 따라 배경 표시/숨김
@@ -349,7 +349,7 @@ namespace UVC.UI.List.Tree
/// <summary>
/// 모든 레벨에서 선택 배경의 왼쪽 정렬을 루트와 맞춥니다.
/// </summary>
private void AlignSelectedBgToRoot()
protected void AlignSelectedBgToRoot()
{
if (selectedBg == null) return;
@@ -393,7 +393,7 @@ namespace UVC.UI.List.Tree
/// 반환:
/// - 루트 TreeListItem, 없으면 null
/// </summary>
private TreeListItem? GetRootTreeListItem()
protected TreeListItem? GetRootTreeListItem()
{
// 현재 객체의 부모부터 시작
Transform current = transform.parent;
@@ -437,7 +437,7 @@ namespace UVC.UI.List.Tree
/// - Ctrl/Shift 상태를 읽어 TreeList에 전달
/// (일반/토글/범위 선택)
/// </summary>
private void OnItemClicked()
protected void OnItemClicked()
{
if (data == null) return;
@@ -592,7 +592,7 @@ namespace UVC.UI.List.Tree
/// <summary>
/// Unity 파괴 시점에 정리합니다. (중복 정리 방지)
/// </summary>
private void OnDestroy()
protected virtual void OnDestroy()
{
// 맵 해제
if (data != null)

View File

@@ -30,7 +30,7 @@ namespace UVC.UI.Modal
/// {
/// myInputField.text = ""; // 입력창 비우기
/// // 입력창에 변화가 있을 때마다 _inputValue를 업데이트하도록 설정할 수 있어요.
/// myInputField.onValueChanged.AddListener(OnInputChanged);
/// myInputField.OnValueChanged.AddListener(OnInputChanged);
/// }
/// }
///
@@ -51,7 +51,7 @@ namespace UVC.UI.Modal
/// ULog.Debug("입력 모달이 닫힙니다.");
/// if (myInputField != null)
/// {
/// myInputField.onValueChanged.RemoveListener(OnInputChanged); // 리스너 정리
/// myInputField.OnValueChanged.RemoveListener(OnInputChanged); // 리스너 정리
/// }
/// await base.OnClose(content);
/// }

View File

@@ -130,7 +130,7 @@ namespace UVC.UI.Toolbar.Model
if (parameter is bool newSelectedStateFromUI)
{
// UI로부터 직접 상태가 전달된 경우 (View의 onValueChanged 리스너)
// UI로부터 직접 상태가 전달된 경우 (View의 OnValueChanged 리스너)
// IsSelected 프로퍼티 setter가 OnToggle 및 NotifyStateChanged를 호출
IsSelected = newSelectedStateFromUI;
}

View File

@@ -66,7 +66,7 @@ namespace UVC.UI.Toolbar.View
/// <summary>
/// 생성된 라디오 버튼 UI GameObject에 필요한 상호작용을 설정하고 초기 시각적 상태를 업데이트합니다.
/// - UI Toggle 컴포넌트를 가져와 모델의 GroupName에 해당하는 ToggleGroup에 할당합니다.
/// - UI Toggle의 상태 변경(onValueChanged) 시, 선택된 경우에만 모델의 ExecuteClick 메서드 호출 설정.
/// - UI Toggle의 상태 변경(OnValueChanged) 시, 선택된 경우에만 모델의 ExecuteClick 메서드 호출 설정.
/// - 모델의 초기 IsSelected 상태를 UI Toggle의 isOn 상태에 반영.
/// - 모델의 초기 텍스트, 아이콘, 활성화 상태를 UI에 반영.
/// </summary>

View File

@@ -68,7 +68,7 @@ namespace UVC.UI.Toolbar.View
/// 1. <paramref name="buttonModel"/>을 <see cref="ToolbarToggleButton"/>으로 캐스팅합니다.
/// 2. <paramref name="buttonUIObject"/>에서 <see cref="Toggle"/> 컴포넌트를 찾습니다.
/// 3. <see cref="Toggle"/> 컴포넌트의 초기 `isOn` 상태를 모델의 <see cref="ToolbarToggleButton.IsSelected"/> 값으로 설정합니다. (UI 이벤트 발생 없이)
/// 4. <see cref="Toggle"/> 컴포넌트의 `onValueChanged` 이벤트에 리스너를 추가하여, UI에서 토글 상태가 변경되면 모델의 <see cref="ToolbarToggleButton.ExecuteClick"/> 메서드를 호출합니다.
/// 4. <see cref="Toggle"/> 컴포넌트의 `OnValueChanged` 이벤트에 리스너를 추가하여, UI에서 토글 상태가 변경되면 모델의 <see cref="ToolbarToggleButton.ExecuteClick"/> 메서드를 호출합니다.
/// 이를 통해 모델의 상태가 업데이트되고, 연결된 커맨드가 실행될 수 있습니다.
/// 5. <see cref="UpdateCommonButtonVisuals"/>를 호출하여 텍스트, 아이콘 등 공통 시각 요소를 초기화합니다.
/// 6. <see cref="UpdateToggleStateVisuals"/>를 호출하여 토글 상태에 따른 시각적 표현(예: 아이콘 변경)을 초기화합니다.
@@ -82,7 +82,7 @@ namespace UVC.UI.Toolbar.View
if (toggleComponent != null)
{
// 1. 모델의 현재 선택 상태로 UI의 Toggle 컴포넌트 상태를 초기화합니다.
// SetIsOnWithoutNotify를 사용하여 이 변경으로 인해 onValueChanged 이벤트가 발생하지 않도록 합니다.
// SetIsOnWithoutNotify를 사용하여 이 변경으로 인해 OnValueChanged 이벤트가 발생하지 않도록 합니다.
toggleComponent.SetIsOnWithoutNotify(toggleModel.IsSelected);
// 2. UI의 Toggle 컴포넌트의 값이 변경될 때(사용자가 클릭 등) 모델의 ExecuteClick 메서드를 호출합니다.

View File

@@ -1,7 +1,6 @@
#nullable enable
using Cysharp.Threading.Tasks;
using DG.Tweening;
using System;
using System.Linq;
using System.Threading;
using TMPro;
@@ -66,15 +65,15 @@ namespace UVC.UI.Window
// 검색 작업 상태
protected CancellationTokenSource? searchCts;
protected bool isSearching = false;
protected float searchProgress =0f; // 내부 진행도 추적용
protected float searchProgress = 0f; // 내부 진행도 추적용
[SerializeField]
[Tooltip("로딩 아이콘 회전 속도(도/초)")]
protected float loadingRotateSpeed =360f;
protected float loadingRotateSpeed = 360f;
[SerializeField]
[Tooltip("로딩 이미지의 채우기 애니메이션 속도(사이클/초)")]
protected float loadingFillCycle =0.5f; // cycles per second. loadingRotateSpeed360 일때, loadingFillCycle를0.5 보다 높게 설정하면 이상해 보임
protected float loadingFillCycle = 0.5f; // cycles per second. loadingRotateSpeed360 일때, loadingFillCycle를0.5 보다 높게 설정하면 이상해 보임
// DOTween tweens
protected Tween? loadingRotationTween;
@@ -161,6 +160,21 @@ namespace UVC.UI.Window
treeList.DeleteItem(data);
}
/// <summary>
/// 이름으로 아이템 선택
/// </summary>
/// <param name="name"></param>
public void SelectItem(string name)
{
//검색 중이면 취소
CancelSearch();
treeListSearch.gameObject.SetActive(false);
treeList.gameObject.SetActive(true);
treeList.SelectItem(name);
}
protected void StartLoadingAnimation()
{
if (loadingImage == null) return;
@@ -168,21 +182,21 @@ namespace UVC.UI.Window
// 기존 트윈 정리
StopLoadingAnimation();
loadingImage.fillAmount =0f;
loadingImage.fillAmount = 0f;
loadingImage.transform.localRotation = Quaternion.identity;
loadingImage.gameObject.SetActive(true);
// 회전 트윈
float rotDuration = (loadingRotateSpeed !=0f) ? (360f / Mathf.Abs(loadingRotateSpeed)) :1f;
float rotDuration = (loadingRotateSpeed != 0f) ? (360f / Mathf.Abs(loadingRotateSpeed)) : 1f;
loadingRotationTween = loadingImage.transform
.DOLocalRotate(new Vector3(0f,0f, -360f), rotDuration, RotateMode.LocalAxisAdd)
.DOLocalRotate(new Vector3(0f, 0f, -360f), rotDuration, RotateMode.LocalAxisAdd)
.SetEase(Ease.Linear)
.SetLoops(-1, LoopType.Restart);
// 채우기 트윈
float fullDuration = (loadingFillCycle >0f) ? (1f / loadingFillCycle) :1f;
float fullDuration = (loadingFillCycle > 0f) ? (1f / loadingFillCycle) : 1f;
loadingFillTween = DOTween
.To(() => loadingImage.fillAmount, x => loadingImage.fillAmount = x,1f, fullDuration)
.To(() => loadingImage.fillAmount, x => loadingImage.fillAmount = x, 1f, fullDuration)
.SetEase(Ease.InOutSine)
.SetLoops(-1, LoopType.Yoyo);
}
@@ -205,7 +219,7 @@ namespace UVC.UI.Window
{
loadingImage.gameObject.SetActive(false);
loadingImage.transform.localRotation = Quaternion.identity;
loadingImage.fillAmount =0f;
loadingImage.fillAmount = 0f;
}
}
@@ -218,7 +232,7 @@ namespace UVC.UI.Window
searchCts = null;
}
isSearching = false;
searchProgress =0f;
searchProgress = 0f;
StopLoadingAnimation();
}
@@ -281,7 +295,7 @@ namespace UVC.UI.Window
protected async UniTaskVoid PerformSearchAsync(string text, CancellationToken token)
{
isSearching = true;
searchProgress =0f;
searchProgress = 0f;
var results = new System.Collections.Generic.List<TreeListItemData>();
@@ -294,7 +308,7 @@ namespace UVC.UI.Window
}
int total = sourceList.Count;
if (total ==0)
if (total == 0)
{
isSearching = false;
StopLoadingAnimation();
@@ -305,8 +319,8 @@ namespace UVC.UI.Window
string lower = text.ToLowerInvariant();
// 분할 처리: 일정 갯수마다 await으로 제어권을 반환
const int chunk =100;
for (int i =0; i < total; i++)
const int chunk = 100;
for (int i = 0; i < total; i++)
{
token.ThrowIfCancellationRequested();
@@ -317,7 +331,7 @@ namespace UVC.UI.Window
}
// 진행도 업데이트 (내부 사용)
if ((i % chunk) ==0)
if ((i % chunk) == 0)
{
searchProgress = (float)i / (float)total;
await UniTask.Yield(PlayerLoopTiming.Update);
@@ -325,7 +339,7 @@ namespace UVC.UI.Window
}
// 최종 진행도
searchProgress =1f;
searchProgress = 1f;
// UI 반영은 메인 스레드에서
if (!PlayerLoopHelper.IsMainThread) await UniTask.SwitchToMainThread();