개발중
This commit is contained in:
@@ -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
8
Assets/Scripts/SHI.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1081c3de3d7409d4e8254544beb39649
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/SHI/modal.meta
Normal file
8
Assets/Scripts/SHI/modal.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1e58e2024a2f4ec448732b865cc98821
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
39
Assets/Scripts/SHI/modal/BlockDetailModal.cs
Normal file
39
Assets/Scripts/SHI/modal/BlockDetailModal.cs
Normal 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()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/SHI/modal/BlockDetailModal.cs.meta
Normal file
2
Assets/Scripts/SHI/modal/BlockDetailModal.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: daf98703dca342f458a0cf5df71adddb
|
||||
43
Assets/Scripts/SHI/modal/ModelDetailListItem.cs
Normal file
43
Assets/Scripts/SHI/modal/ModelDetailListItem.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/SHI/modal/ModelDetailListItem.cs.meta
Normal file
2
Assets/Scripts/SHI/modal/ModelDetailListItem.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1e59b82317d5c194ab2ab4e87730e631
|
||||
16
Assets/Scripts/SHI/modal/ModelDetailListItemData.cs
Normal file
16
Assets/Scripts/SHI/modal/ModelDetailListItemData.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/SHI/modal/ModelDetailListItemData.cs.meta
Normal file
2
Assets/Scripts/SHI/modal/ModelDetailListItemData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21fbea5ce75b98f489f259fc7cf88fe3
|
||||
395
Assets/Scripts/SHI/modal/ModelDetailListView.cs
Normal file
395
Assets/Scripts/SHI/modal/ModelDetailListView.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/SHI/modal/ModelDetailListView.cs.meta
Normal file
2
Assets/Scripts/SHI/modal/ModelDetailListView.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59c744534c4342f4b9d032ed55beb194
|
||||
13
Assets/Scripts/SHI/modal/ModelDetailView.cs
Normal file
13
Assets/Scripts/SHI/modal/ModelDetailView.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/SHI/modal/ModelDetailView.cs.meta
Normal file
2
Assets/Scripts/SHI/modal/ModelDetailView.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f867baed0364ea4bb3ce7a26ce84bdf
|
||||
187
Assets/Scripts/UVC/UI/Buttons/ImageToggle.cs
Normal file
187
Assets/Scripts/UVC/UI/Buttons/ImageToggle.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/UI/Buttons/ImageToggle.cs.meta
Normal file
2
Assets/Scripts/UVC/UI/Buttons/ImageToggle.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5b3b44478924734bb216de085616f87
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
/// }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 메서드를 호출합니다.
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user