573 lines
28 KiB
C#
573 lines
28 KiB
C#
#nullable enable
|
|
using Cysharp.Threading.Tasks;
|
|
using System.Collections.Generic;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using UVC.Core;
|
|
using UVC.Locale;
|
|
using UVC.Studio.Config;
|
|
using UVC.UI.Modal;
|
|
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 Toggle? firstCtrlToggle;
|
|
|
|
[SerializeField]
|
|
private Toggle? firstShiftToggle;
|
|
|
|
[SerializeField]
|
|
private Toggle? firstAltToggle;
|
|
|
|
[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 List<Toggle> ctrlToggles = new List<Toggle>();
|
|
private List<Toggle> shiftToggles = new List<Toggle>();
|
|
private List<Toggle> altToggles = new List<Toggle>();
|
|
|
|
private bool changedValue = false;
|
|
|
|
// 단축키로 사용할 수 없는 키 목록 (카메라 조작 등에 사용됨)
|
|
private static readonly HashSet<string> reservedKeys = new HashSet<string> { "A", "D", "Q", "E", "S", "W", "V" };
|
|
|
|
/// <summary>
|
|
/// 해당 인덱스의 수정자 키(Ctrl, Shift, Alt) 중 하나라도 체크되어 있는지 확인합니다.
|
|
/// </summary>
|
|
private bool HasAnyModifier(int index)
|
|
{
|
|
if (index < 0 || index >= ctrlToggles.Count) return false;
|
|
return ctrlToggles[index].isOn || shiftToggles[index].isOn || altToggles[index].isOn;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 해당 인덱스의 키가 예약된 키인지 확인합니다.
|
|
/// </summary>
|
|
private bool IsReservedKey(int index)
|
|
{
|
|
if (index < 0 || index >= valueTxts.Count) return false;
|
|
return reservedKeys.Contains(valueTxts[index].text.ToUpper());
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 단축키 조합이 다른 항목과 중복되는지 확인합니다.
|
|
/// </summary>
|
|
/// <param name="excludeIndex">중복 체크에서 제외할 인덱스 (자기 자신)</param>
|
|
/// <param name="key">확인할 키</param>
|
|
/// <param name="ctrl">Ctrl 체크 여부</param>
|
|
/// <param name="shift">Shift 체크 여부</param>
|
|
/// <param name="alt">Alt 체크 여부</param>
|
|
/// <returns>중복이 있으면 true</returns>
|
|
private bool IsDuplicateShortcut(int excludeIndex, string key, bool ctrl, bool shift, bool alt)
|
|
{
|
|
if (string.IsNullOrEmpty(key)) return false;
|
|
|
|
string upperKey = key.ToUpper();
|
|
for (int i = 0; i < valueTxts.Count; i++)
|
|
{
|
|
if (i == excludeIndex) continue;
|
|
if (i >= ctrlToggles.Count || i >= shiftToggles.Count || i >= altToggles.Count) continue;
|
|
|
|
string otherKey = valueTxts[i].text.ToUpper();
|
|
if (string.IsNullOrEmpty(otherKey)) continue;
|
|
|
|
bool otherCtrl = ctrlToggles[i].isOn;
|
|
bool otherShift = shiftToggles[i].isOn;
|
|
bool otherAlt = altToggles[i].isOn;
|
|
|
|
// 키와 모든 수정자가 동일하면 중복
|
|
if (upperKey == otherKey && ctrl == otherCtrl && shift == otherShift && alt == otherAlt)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return 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();
|
|
ctrlToggles.Clear();
|
|
shiftToggles.Clear();
|
|
altToggles.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);
|
|
|
|
// Toggle 초기값 설정
|
|
int currentIndex = 0; // 첫 번째 항목의 인덱스
|
|
if (firstCtrlToggle != null)
|
|
{
|
|
firstCtrlToggle.isOn = shortcut.ctrl;
|
|
ctrlToggles.Add(firstCtrlToggle);
|
|
firstCtrlToggle.onValueChanged.AddListener((value) =>
|
|
{
|
|
// 체크 해제 시 예약된 키가 있으면 차단
|
|
if (!value && IsReservedKey(currentIndex) && !HasAnyModifier(currentIndex))
|
|
{
|
|
firstCtrlToggle.isOn = true;
|
|
Toast.Show(LocalizationManager.Instance.GetString("msg_not_shortcut"));
|
|
return;
|
|
}
|
|
// 중복 단축키 체크
|
|
string key = valueTxts[currentIndex].text;
|
|
bool newCtrl = value;
|
|
bool newShift = shiftToggles[currentIndex].isOn;
|
|
bool newAlt = altToggles[currentIndex].isOn;
|
|
if (IsDuplicateShortcut(currentIndex, key, newCtrl, newShift, newAlt))
|
|
{
|
|
firstCtrlToggle.isOn = !value;
|
|
Toast.Show(LocalizationManager.Instance.GetString("msg_duplicate_shortcut"));
|
|
return;
|
|
}
|
|
shortcut.ctrl = value;
|
|
changedValue = true;
|
|
});
|
|
}
|
|
if (firstShiftToggle != null)
|
|
{
|
|
firstShiftToggle.isOn = shortcut.shift;
|
|
shiftToggles.Add(firstShiftToggle);
|
|
firstShiftToggle.onValueChanged.AddListener((value) =>
|
|
{
|
|
// 체크 해제 시 예약된 키가 있으면 차단
|
|
if (!value && IsReservedKey(currentIndex) && !HasAnyModifier(currentIndex))
|
|
{
|
|
firstShiftToggle.isOn = true;
|
|
Toast.Show(LocalizationManager.Instance.GetString("msg_not_shortcut"));
|
|
return;
|
|
}
|
|
// 중복 단축키 체크
|
|
string key = valueTxts[currentIndex].text;
|
|
bool newCtrl = ctrlToggles[currentIndex].isOn;
|
|
bool newShift = value;
|
|
bool newAlt = altToggles[currentIndex].isOn;
|
|
if (IsDuplicateShortcut(currentIndex, key, newCtrl, newShift, newAlt))
|
|
{
|
|
firstShiftToggle.isOn = !value;
|
|
Toast.Show(LocalizationManager.Instance.GetString("msg_duplicate_shortcut"));
|
|
return;
|
|
}
|
|
shortcut.shift = value;
|
|
changedValue = true;
|
|
});
|
|
}
|
|
if (firstAltToggle != null)
|
|
{
|
|
firstAltToggle.isOn = shortcut.alt;
|
|
altToggles.Add(firstAltToggle);
|
|
firstAltToggle.onValueChanged.AddListener((value) =>
|
|
{
|
|
// 체크 해제 시 예약된 키가 있으면 차단
|
|
if (!value && IsReservedKey(currentIndex) && !HasAnyModifier(currentIndex))
|
|
{
|
|
firstAltToggle.isOn = true;
|
|
Toast.Show(LocalizationManager.Instance.GetString("msg_not_shortcut"));
|
|
return;
|
|
}
|
|
// 중복 단축키 체크
|
|
string key = valueTxts[currentIndex].text;
|
|
bool newCtrl = ctrlToggles[currentIndex].isOn;
|
|
bool newShift = shiftToggles[currentIndex].isOn;
|
|
bool newAlt = value;
|
|
if (IsDuplicateShortcut(currentIndex, key, newCtrl, newShift, newAlt))
|
|
{
|
|
firstAltToggle.isOn = !value;
|
|
Toast.Show(LocalizationManager.Instance.GetString("msg_duplicate_shortcut"));
|
|
return;
|
|
}
|
|
shortcut.alt = value;
|
|
changedValue = true;
|
|
});
|
|
}
|
|
|
|
// 영문 대문자만 입력되도록 필터링 및 예약된 키 체크
|
|
string previousKey = shortcut.key;
|
|
firstValueTxt.onValueChanged.AddListener((value) =>
|
|
{
|
|
string upperValue = value.ToUpper();
|
|
// 수정자 키가 하나도 없을 때만 예약된 키 체크
|
|
if (reservedKeys.Contains(upperValue) && !HasAnyModifier(currentIndex))
|
|
{
|
|
firstValueTxt.text = previousKey;
|
|
Toast.Show(LocalizationManager.Instance.GetString("msg_not_shortcut"));
|
|
return;
|
|
}
|
|
// 중복 단축키 체크
|
|
bool ctrl = ctrlToggles[currentIndex].isOn;
|
|
bool shift = shiftToggles[currentIndex].isOn;
|
|
bool alt = altToggles[currentIndex].isOn;
|
|
if (IsDuplicateShortcut(currentIndex, upperValue, ctrl, shift, alt))
|
|
{
|
|
firstValueTxt.text = previousKey;
|
|
Toast.Show(LocalizationManager.Instance.GetString("msg_duplicate_shortcut"));
|
|
return;
|
|
}
|
|
previousKey = upperValue;
|
|
firstValueTxt.text = upperValue;
|
|
});
|
|
|
|
// 값 변경 리스너 추가
|
|
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);
|
|
}
|
|
|
|
// Toggle 복제
|
|
int itemIndex = ctrlToggles.Count; // 현재 항목의 인덱스 (추가 전)
|
|
if (firstCtrlToggle != null)
|
|
{
|
|
Toggle ctrlInstance = Instantiate(firstCtrlToggle, firstCtrlToggle.transform.parent);
|
|
ctrlInstance.isOn = shortcut.ctrl;
|
|
ctrlToggles.Add(ctrlInstance);
|
|
int capturedIndex = itemIndex;
|
|
ctrlInstance.onValueChanged.AddListener((value) =>
|
|
{
|
|
// 체크 해제 시 예약된 키가 있으면 차단
|
|
if (!value && IsReservedKey(capturedIndex) && !HasAnyModifier(capturedIndex))
|
|
{
|
|
ctrlInstance.isOn = true;
|
|
Toast.Show(LocalizationManager.Instance.GetString("msg_not_shortcut"));
|
|
return;
|
|
}
|
|
// 중복 단축키 체크
|
|
string key = valueTxts[capturedIndex].text;
|
|
bool newCtrl = value;
|
|
bool newShift = shiftToggles[capturedIndex].isOn;
|
|
bool newAlt = altToggles[capturedIndex].isOn;
|
|
if (IsDuplicateShortcut(capturedIndex, key, newCtrl, newShift, newAlt))
|
|
{
|
|
ctrlInstance.isOn = !value;
|
|
Toast.Show(LocalizationManager.Instance.GetString("msg_duplicate_shortcut"));
|
|
return;
|
|
}
|
|
shortcut.ctrl = value;
|
|
changedValue = true;
|
|
});
|
|
}
|
|
if (firstShiftToggle != null)
|
|
{
|
|
Toggle shiftInstance = Instantiate(firstShiftToggle, firstShiftToggle.transform.parent);
|
|
shiftInstance.isOn = shortcut.shift;
|
|
shiftToggles.Add(shiftInstance);
|
|
int capturedIndex = itemIndex;
|
|
shiftInstance.onValueChanged.AddListener((value) =>
|
|
{
|
|
// 체크 해제 시 예약된 키가 있으면 차단
|
|
if (!value && IsReservedKey(capturedIndex) && !HasAnyModifier(capturedIndex))
|
|
{
|
|
shiftInstance.isOn = true;
|
|
Toast.Show(LocalizationManager.Instance.GetString("msg_not_shortcut"));
|
|
return;
|
|
}
|
|
// 중복 단축키 체크
|
|
string key = valueTxts[capturedIndex].text;
|
|
bool newCtrl = ctrlToggles[capturedIndex].isOn;
|
|
bool newShift = value;
|
|
bool newAlt = altToggles[capturedIndex].isOn;
|
|
if (IsDuplicateShortcut(capturedIndex, key, newCtrl, newShift, newAlt))
|
|
{
|
|
shiftInstance.isOn = !value;
|
|
Toast.Show(LocalizationManager.Instance.GetString("msg_duplicate_shortcut"));
|
|
return;
|
|
}
|
|
shortcut.shift = value;
|
|
changedValue = true;
|
|
});
|
|
}
|
|
if (firstAltToggle != null)
|
|
{
|
|
Toggle altInstance = Instantiate(firstAltToggle, firstAltToggle.transform.parent);
|
|
altInstance.isOn = shortcut.alt;
|
|
altToggles.Add(altInstance);
|
|
int capturedIndex = itemIndex;
|
|
altInstance.onValueChanged.AddListener((value) =>
|
|
{
|
|
// 체크 해제 시 예약된 키가 있으면 차단
|
|
if (!value && IsReservedKey(capturedIndex) && !HasAnyModifier(capturedIndex))
|
|
{
|
|
altInstance.isOn = true;
|
|
Toast.Show(LocalizationManager.Instance.GetString("msg_not_shortcut"));
|
|
return;
|
|
}
|
|
// 중복 단축키 체크
|
|
string key = valueTxts[capturedIndex].text;
|
|
bool newCtrl = ctrlToggles[capturedIndex].isOn;
|
|
bool newShift = shiftToggles[capturedIndex].isOn;
|
|
bool newAlt = value;
|
|
if (IsDuplicateShortcut(capturedIndex, key, newCtrl, newShift, newAlt))
|
|
{
|
|
altInstance.isOn = !value;
|
|
Toast.Show(LocalizationManager.Instance.GetString("msg_duplicate_shortcut"));
|
|
return;
|
|
}
|
|
shortcut.alt = value;
|
|
changedValue = true;
|
|
});
|
|
}
|
|
|
|
// 값 복제
|
|
if (firstValueTxt != null)
|
|
{
|
|
TMP_InputField valueInstance = Instantiate(firstValueTxt, valueGroup.transform);
|
|
valueInstance.text = shortcut.key;
|
|
valueTxts.Add(valueInstance);
|
|
|
|
int capturedIndex = itemIndex;
|
|
string prevKey = shortcut.key;
|
|
// 영문 대문자만 입력되도록 필터링 및 예약된 키 체크
|
|
valueInstance.onValueChanged.AddListener((value) =>
|
|
{
|
|
string upperValue = value.ToUpper();
|
|
// 수정자 키가 하나도 없을 때만 예약된 키 체크
|
|
if (reservedKeys.Contains(upperValue) && !HasAnyModifier(capturedIndex))
|
|
{
|
|
valueInstance.text = prevKey;
|
|
Toast.Show(LocalizationManager.Instance.GetString("msg_not_shortcut"));
|
|
return;
|
|
}
|
|
// 중복 단축키 체크
|
|
bool ctrl = ctrlToggles[capturedIndex].isOn;
|
|
bool shift = shiftToggles[capturedIndex].isOn;
|
|
bool alt = altToggles[capturedIndex].isOn;
|
|
if (IsDuplicateShortcut(capturedIndex, upperValue, ctrl, shift, alt))
|
|
{
|
|
valueInstance.text = prevKey;
|
|
Toast.Show(LocalizationManager.Instance.GetString("msg_duplicate_shortcut"));
|
|
return;
|
|
}
|
|
prevKey = upperValue;
|
|
valueInstance.text = upperValue;
|
|
});
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Toggle 리스너 정리 및 복제된 Toggle 삭제
|
|
foreach (var toggle in ctrlToggles)
|
|
{
|
|
toggle.onValueChanged.RemoveAllListeners();
|
|
if (toggle != firstCtrlToggle)
|
|
{
|
|
Destroy(toggle.gameObject);
|
|
}
|
|
}
|
|
foreach (var toggle in shiftToggles)
|
|
{
|
|
toggle.onValueChanged.RemoveAllListeners();
|
|
if (toggle != firstShiftToggle)
|
|
{
|
|
Destroy(toggle.gameObject);
|
|
}
|
|
}
|
|
foreach (var toggle in altToggles)
|
|
{
|
|
toggle.onValueChanged.RemoveAllListeners();
|
|
if (toggle != firstAltToggle)
|
|
{
|
|
Destroy(toggle.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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |