Files
XRLib/Assets/Scripts/Studio/Modal/Settings/SettingShortcutTabContent.cs
2025-12-19 18:29:22 +09:00

254 lines
9.6 KiB
C#

#nullable enable
using Cysharp.Threading.Tasks;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Core;
using UVC.Studio.Config;
using UVC.UI.Tab;
namespace UVC.Studio.Modal.Settings
{
public class SettingShortcutTabContent : MonoBehaviour, ITabContent
{
[SerializeField]
private ScrollRect? scrollRect;
[SerializeField]
private LayoutGroup? labelGroup;
[SerializeField]
private LayoutGroup? valueGroup;
[SerializeField]
private TextMeshProUGUI? firstLabelTxt;
[SerializeField]
private TMP_InputField? firstValueTxt;
private Setting? setting;
private List<TextMeshProUGUI> labelTxts = new List<TextMeshProUGUI>();
private List<TMP_InputField> valueTxts = new List<TMP_InputField>();
private bool changedValue = false;
/// <summary>
/// 탭 콘텐츠에 데이터를 전달합니다.
/// </summary>
/// <param name="data">전달할 데이터 객체</param>
public async void SetContentData(object? data)
{
if (setting == null)
{
// 동적으로 로드되는 Prefab이므로 [Inject]가 자동으로 동작하지 않음
await InjectorAppContext.Instance.WaitForInitializationAsync();
setting = InjectorAppContext.Instance.Get<Setting>();
}
labelTxts.Clear();
valueTxts.Clear();
if (setting != null && labelGroup != null && valueGroup != null)
{
changedValue = false;
ShortcutsSetting shortcuts = setting.Data.shortcuts;
// menu와 tools의 모든 ShortcutItem 필드를 순회
var shortcutGroups = new object[] { shortcuts.menu, shortcuts.tools };
foreach (var group in shortcutGroups)
{
var fields = group.GetType().GetFields();
for (int i = 0; i < fields.Length; i++)
{
var field = fields[i];
if (field.FieldType == typeof(ShortcutItem))
{
var shortcut = (ShortcutItem?)field.GetValue(group);
if (shortcut == null) continue;
if (i == 0 && group == shortcuts.menu)
{
firstLabelTxt!.text = shortcut.label;
firstValueTxt!.text = shortcut.key;
labelTxts.Add(firstLabelTxt);
valueTxts.Add(firstValueTxt);
// 영문 대문자만 입력되도록 필터링
firstValueTxt.onValueChanged.AddListener((value) =>
{
firstValueTxt.text = value.ToUpper();
});
// 값 변경 리스너 추가
firstValueTxt.onEndEdit.AddListener((value) =>
{
shortcut.key = value;
changedValue = true;
});
}
else
{
// 라벨 복제
if (firstLabelTxt != null)
{
TextMeshProUGUI labelInstance = Instantiate(firstLabelTxt, labelGroup.transform);
labelInstance.text = shortcut.label;
labelTxts.Add(labelInstance);
}
// 값 복제
if (firstValueTxt != null)
{
TMP_InputField valueInstance = Instantiate(firstValueTxt, valueGroup.transform);
valueInstance.text = shortcut.key;
valueTxts.Add(valueInstance);
// 영문 대문자만 입력되도록 필터링
valueInstance.onValueChanged.AddListener((value) =>
{
valueInstance.text = value.ToUpper();
});
valueInstance.onEndEdit.AddListener((value) =>
{
shortcut.key = value;
changedValue = true;
});
}
}
}
}
}
}
}
private void Update()
{
// Tab 키 입력 처리
if (Input.GetKeyDown(KeyCode.Tab) && valueTxts.Count > 0)
{
int currentIndex = GetCurrentFocusedIndex();
if (currentIndex >= 0)
{
int nextIndex = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift)
? (currentIndex - 1 + valueTxts.Count) % valueTxts.Count // Shift+Tab: 이전
: (currentIndex + 1) % valueTxts.Count; // Tab: 다음
valueTxts[nextIndex].Select();
ScrollToItem(nextIndex);
}
}
}
private int GetCurrentFocusedIndex()
{
for (int i = 0; i < valueTxts.Count; i++)
{
if (valueTxts[i] != null && valueTxts[i].isFocused)
{
return i;
}
}
return -1;
}
/// <summary>
/// 해당 인덱스의 항목이 보이도록 스크롤합니다.
/// </summary>
private void ScrollToItem(int index)
{
if (scrollRect == null || scrollRect.content == null || index < 0 || index >= valueTxts.Count)
return;
RectTransform targetRect = valueTxts[index].GetComponent<RectTransform>();
RectTransform contentRect = scrollRect.content;
RectTransform viewportRect = scrollRect.viewport != null ? scrollRect.viewport : scrollRect.GetComponent<RectTransform>();
// 타겟 항목의 위치 계산
float contentHeight = contentRect.rect.height;
float viewportHeight = viewportRect.rect.height;
if (contentHeight <= viewportHeight) return; // 스크롤 필요 없음
// 타겟의 로컬 위치 (content 기준)
float targetY = -targetRect.anchoredPosition.y;
float itemHeight = targetRect.rect.height;
// 현재 스크롤 위치에서 보이는 영역
float scrollY = contentRect.anchoredPosition.y;
float visibleTop = scrollY;
float visibleBottom = scrollY + viewportHeight;
// 항목이 보이지 않으면 스크롤
if (targetY < visibleTop)
{
// 위로 스크롤
float normalizedPos = targetY / (contentHeight - viewportHeight);
scrollRect.verticalNormalizedPosition = 1f - Mathf.Clamp01(normalizedPos);
}
else if (targetY + itemHeight > visibleBottom)
{
// 아래로 스크롤
float normalizedPos = (targetY + itemHeight - viewportHeight) / (contentHeight - viewportHeight);
scrollRect.verticalNormalizedPosition = 1f - Mathf.Clamp01(normalizedPos);
}
}
/// <summary>
/// 탭 전환 시 데이터가 있는 경우 전달 되는 데이터. SetContentData 이후 호출 됨
/// </summary>
/// <param name="data">전달할 데이터 객체</param>
public void UpdateContentData(object? data)
{
if (data != null && data is string content)
{
Debug.Log($"UpdateContentData: {content}");
}
}
/// <summary>
/// 닫힐 때 실행되는 로직을 처리합니다.
/// </summary>
/// <returns>비동기 닫기 작업을 나타내는 <see cref="UniTask"/>입니다.</returns>
public async UniTask OnCloseAsync()
{
foreach (var labelTxt in labelTxts)
{
if(labelTxt != firstLabelTxt)
{
Destroy(labelTxt.gameObject);
}
}
foreach (var valueTxt in valueTxts)
{
valueTxt.onValueChanged.RemoveAllListeners();
valueTxt.onEndEdit.RemoveAllListeners();
if(valueTxt != firstValueTxt)
{
Destroy(valueTxt.gameObject);
}
}
Debug.Log($"SettingShortcutTabContent OnCloseAsync: changedValue={changedValue} setting == null:{setting == null}");
if (changedValue && setting != null)
{
await setting.SaveAsync();
Debug.Log("Shortcut settings saved.");
// TopMenu 단축키 갱신
if (StudioSceneMain.Instance != null)
{
StudioSceneMain.Instance.RefreshMenuShortcuts();
}
}
}
}
}