단축키, Command 설정 개발 중

This commit is contained in:
logonkhi
2025-12-19 18:29:22 +09:00
parent 158a42ab9b
commit ddec52df13
52 changed files with 3260 additions and 222 deletions

View File

@@ -49,12 +49,6 @@ namespace UVC.UI.List.Tree
[SerializeField]
protected Button childExpand;
/// <summary>
/// 자식 아이템들을 포함하는 컨테이너 GameObject.
/// </summary>
[SerializeField]
protected GameObject childContainer;
/// <summary>
/// 자식 TreeListItem이 배치될 부모 RectTransform.
/// </summary>
@@ -145,7 +139,6 @@ namespace UVC.UI.List.Tree
childRootLayoutGroup = childRoot.GetComponent<VerticalLayoutGroup>();
}
if (data.Children.Count > 0)
{
// 각 자식 데이터에 대해 UI 생성
@@ -214,7 +207,7 @@ namespace UVC.UI.List.Tree
if (changedType == ChangedType.Expanded)
{
childContainer.SetActive(data.IsExpanded);
childRoot.gameObject.SetActive(data.IsExpanded);
// 펼침/접힘 상태 변경 처리
SetExpand();
return;
@@ -521,7 +514,7 @@ namespace UVC.UI.List.Tree
// 펼침 버튼
childExpand.gameObject.SetActive(data!.Children.Count > 0);
childContainer.SetActive(data!.Children.Count > 0 && data!.IsExpanded);
childRoot.gameObject.SetActive(data!.Children.Count > 0 && data!.IsExpanded);
// DORotate(목표 각도, 지속 시간)
// IsExpanded가 true면 0도 (▼), false면 90도 (▶)
@@ -550,7 +543,7 @@ namespace UVC.UI.List.Tree
CreateItem(data);
// 2. 자식 컨테이너 활성화 (표시)
childContainer.SetActive(true);
childRoot.gameObject.SetActive(true);
// 3. 0.3초에 걸쳐 펼침 애니메이션 실행
// 사용자가 새 자식이 추가되었음을 자연스럽게 인식

View File

@@ -121,6 +121,12 @@ namespace UVC.UI.List.Tree
/// </summary>
public string ExternalKey { get; set; } = string.Empty;
/// <summary>
/// 외부에서 사용하기 위한 사용자 정의 데이터 태그
/// Transform, GameObject 등 임의 객체 참조에 사용
/// </summary>
public object? Tag { get; set; }
#endregion
#region (Constructors)

View File

@@ -322,6 +322,52 @@ namespace UVC.UI.Menu
}
}
/// <summary>
/// 특정 메뉴 아이템의 단축키를 동적으로 변경합니다.
/// 이 메서드는 모델(<see cref="TopMenuModel"/>)의 데이터를 변경하고,
/// 변경 사항을 뷰(<see cref="TopMenuView"/>)에 반영합니다.
/// </summary>
/// <param name="itemId">단축키를 변경할 메뉴 아이템의 고유 ID입니다.</param>
/// <param name="shortcut">새로운 단축키 문자열입니다.</param>
public virtual void SetMenuItemShortcut(string itemId, string shortcut)
{
if (model == null || model.MenuItems == null)
{
ULog.Warning("모델이 초기화되지 않아 메뉴 아이템 단축키를 변경할 수 없습니다.");
return;
}
MenuItemData targetItem = FindMenuItemRecursive(model.MenuItems, itemId);
if (targetItem != null)
{
targetItem.Shortcut = shortcut;
if (view != null && view.isActiveAndEnabled)
{
view.UpdateShortcutText(itemId, shortcut);
}
}
else
{
ULog.Warning($"ID가 '{itemId}'인 메뉴 아이템을 모델에서 찾을 수 없어 단축키를 변경할 수 없습니다.");
}
}
/// <summary>
/// 모든 메뉴 아이템의 단축키를 최신 데이터로 업데이트합니다.
/// Setting에서 단축키가 변경된 후 호출하여 UI를 갱신합니다.
/// </summary>
public virtual void RefreshAllShortcuts()
{
if (model == null || model.MenuItems == null || view == null)
{
ULog.Warning("모델 또는 뷰가 초기화되지 않아 단축키를 갱신할 수 없습니다.");
return;
}
view.UpdateAllShortcuts(model.MenuItems);
ULog.Debug("모든 메뉴 아이템의 단축키가 갱신되었습니다.");
}
/// <summary>
/// 제공된 메뉴 아이템 리스트(<paramref name="items"/>)와 그 하위 메뉴들을 재귀적으로 탐색하여
/// 지정된 ID(<paramref name="itemId"/>)를 가진 <see cref="MenuItemData"/>를 찾습니다.

View File

@@ -55,6 +55,13 @@ namespace UVC.UI.Menu
/// </summary>
public bool IsEnabled { get; set; }
/// <summary>
/// 메뉴 아이템의 단축키 문자열입니다.
/// 예: "Ctrl+S", "Ctrl+Shift+N" 등
/// 단축키가 없는 경우 null 또는 빈 문자열입니다.
/// </summary>
public string? Shortcut { get; set; }
private int depth = 0; // 메뉴 아이템의 깊이 (하위 메뉴의 레벨)
/// <summary>
/// 계층 구조에서 현재 개체의 깊이를 가져옵니다.
@@ -85,7 +92,8 @@ namespace UVC.UI.Menu
/// <param name="subMenuItems">하위 메뉴 아이템 목록 (선택 사항).</param>
/// <param name="isSeparator">구분선 여부 (선택 사항, 기본값: false).</param>
/// <param name="isEnabled">활성화 상태 (선택 사항, 기본값: true).</param>
public MenuItemData(string itemId, string displayName, ICommand? command = null, object? commandParameter = null, List<MenuItemData>? subMenuItems = null, bool isSeparator = false, bool isEnabled = true, int depth = 0)
/// <param name="shortcut">단축키 문자열 (선택 사항). 예: "Ctrl+S"</param>
public MenuItemData(string itemId, string displayName, ICommand? command = null, object? commandParameter = null, List<MenuItemData>? subMenuItems = null, bool isSeparator = false, bool isEnabled = true, int depth = 0, string? shortcut = null)
{
ItemId = itemId;
DisplayName = displayName;
@@ -95,6 +103,7 @@ namespace UVC.UI.Menu
IsSeparator = isSeparator;
IsEnabled = isEnabled;
this.depth = depth; // 메뉴 아이템의 깊이 설정
Shortcut = shortcut; // 단축키 문자열 설정
SetupDepthAndParent();
}

View File

@@ -79,6 +79,9 @@ namespace UVC.UI.Menu
[Tooltip("하위 메뉴가 있음을 나타내는 화살표 UI GameObject의 이름입니다. (메뉴 아이템 프리팹 내부에 존재)")]
[SerializeField] public string subMenuArrowName = "SubMenuArrow";
[Tooltip("단축키를 표시하는 TextMeshProUGUI의 이름입니다. (SubMenuItemPrefab 내부에 존재)")]
[SerializeField] public string shortcutTextName = "ShorcutText";
[Header("Layout Settings")]
[Tooltip("1차 메뉴 아이템과 그 하위 메뉴 컨테이너 간의 간격 (수평, 수직)")]
[SerializeField] public Vector2 menuDepthSpace = new Vector2(0, -5);
@@ -289,11 +292,23 @@ namespace UVC.UI.Menu
menuItemInstance.name = $"MenuItem_{itemData.ItemId}_Depth{depth}";
_menuItemObjects[itemData.ItemId] = menuItemInstance; // 관리 목록에 추가
// 메뉴 아이템 텍스트 설정
TextMeshProUGUI buttonText = menuItemInstance.GetComponentInChildren<TextMeshProUGUI>(true); // 비활성화된 자식도 검색
// 메뉴 아이템 버튼 기능 설정
Button button = menuItemInstance.GetComponent<Button>();
// 메뉴 아이템 텍스트 설정
// 먼저 "Text"라는 이름의 자식을 찾고, 없으면 GetComponentInChildren으로 찾기
Transform textTransform = menuItemInstance.transform.Find("Text");
TextMeshProUGUI? buttonText = null;
if (textTransform != null)
{
buttonText = textTransform.GetComponent<TextMeshProUGUI>();
}
else
{
// "Text" 이름의 자식이 없으면 자식 중에서 TextMeshProUGUI를 찾음
buttonText = menuItemInstance.GetComponentInChildren<TextMeshProUGUI>(true);
}
if (buttonText != null && !string.IsNullOrEmpty(itemData.DisplayName))
{
buttonText.alpha = itemData.IsEnabled ? 1 : 0.25f;
@@ -309,6 +324,29 @@ namespace UVC.UI.Menu
}
}
// 2차 메뉴 이상(depth > 0)일 때 단축키 텍스트 설정
if (depth > 0 && !string.IsNullOrEmpty(shortcutTextName))
{
Debug.Log($"Setting shortcut for menu item '{itemData.ItemId}': '{itemData.Shortcut}'");
Transform shortcutTransform = menuItemInstance.transform.Find(shortcutTextName);
if (shortcutTransform != null)
{
TextMeshProUGUI shortcutText = shortcutTransform.GetComponent<TextMeshProUGUI>();
if (shortcutText != null)
{
if (!string.IsNullOrEmpty(itemData.Shortcut))
{
shortcutText.text = itemData.Shortcut;
shortcutText.alpha = itemData.IsEnabled ? 1 : 0.25f;
}
else
{
shortcutText.text = string.Empty;
}
}
}
}
// isShrinkMenuItemWidth가 true일 때 1차 메뉴 아이템의 너비를 텍스트에 맞게 조절합니다.
// 텍스트가 설정된 후에 이 로직을 실행해야 정확한 너비를 계산할 수 있습니다.
if (depth == 0 && isShrinkMenuItemWidth && buttonText != null)
@@ -727,5 +765,56 @@ namespace UVC.UI.Menu
{
return _menuItemObjects.TryGetValue(itemId, out menuItemGO);
}
/// <summary>
/// 특정 메뉴 아이템의 단축키 텍스트를 업데이트합니다.
/// </summary>
/// <param name="itemId">업데이트할 메뉴 아이템의 고유 ID입니다.</param>
/// <param name="shortcut">새로운 단축키 문자열입니다.</param>
public virtual void UpdateShortcutText(string itemId, string shortcut)
{
if (_menuItemObjects.TryGetValue(itemId, out GameObject menuItemInstance))
{
Transform shortcutTransform = menuItemInstance.transform.Find(shortcutTextName);
if (shortcutTransform != null)
{
TextMeshProUGUI shortcutText = shortcutTransform.GetComponent<TextMeshProUGUI>();
if (shortcutText != null)
{
shortcutText.text = shortcut ?? string.Empty;
}
}
}
}
/// <summary>
/// 모든 메뉴 아이템의 단축키 텍스트를 업데이트합니다.
/// </summary>
/// <param name="items">최상위 메뉴 아이템들의 데이터 리스트입니다.</param>
public virtual void UpdateAllShortcuts(List<MenuItemData> items)
{
UpdateShortcutsRecursive(items);
}
/// <summary>
/// 재귀적으로 메뉴 아이템들의 단축키 텍스트를 업데이트합니다.
/// </summary>
/// <param name="items">단축키를 업데이트할 메뉴 아이템들의 데이터 리스트입니다.</param>
protected virtual void UpdateShortcutsRecursive(List<MenuItemData> items)
{
if (items == null) return;
foreach (var itemData in items)
{
if (itemData.IsSeparator) continue;
UpdateShortcutText(itemData.ItemId, itemData.Shortcut);
if (itemData.SubMenuItems != null && itemData.SubMenuItems.Count > 0)
{
UpdateShortcutsRecursive(itemData.SubMenuItems);
}
}
}
}
}

View File

@@ -87,6 +87,65 @@ namespace UVC.UI.Window.PropertyWindow.UI
_yInputField.onEndEdit.RemoveAllListeners();
_xInputField.onEndEdit.AddListener(OnValueSubmitted);
_yInputField.onEndEdit.AddListener(OnValueSubmitted);
// Tab 키 네비게이션 설정: X → Y 순환
SetupTabNavigation();
}
/// <summary>
/// Tab 키로 X → Y 순서로 포커스가 이동하도록 네비게이션을 설정합니다.
/// </summary>
private void SetupTabNavigation()
{
// X InputField 네비게이션 설정
var xNav = _xInputField.navigation;
xNav.mode = Navigation.Mode.Explicit;
xNav.selectOnRight = _yInputField;
xNav.selectOnDown = _yInputField;
_xInputField.navigation = xNav;
// Y InputField 네비게이션 설정
var yNav = _yInputField.navigation;
yNav.mode = Navigation.Mode.Explicit;
yNav.selectOnLeft = _xInputField;
yNav.selectOnUp = _xInputField;
_yInputField.navigation = yNav;
// Tab 키 입력 처리를 위한 onSelect 이벤트 등록
_xInputField.onSelect.RemoveAllListeners();
_yInputField.onSelect.RemoveAllListeners();
_xInputField.onSelect.AddListener(_ => _currentFocusedField = _xInputField);
_yInputField.onSelect.AddListener(_ => _currentFocusedField = _yInputField);
}
private TMP_InputField _currentFocusedField;
private void Update()
{
// Tab 키 입력 처리
if (Input.GetKeyDown(KeyCode.Tab) && _currentFocusedField != null)
{
bool shiftHeld = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
if (shiftHeld)
{
// Shift+Tab: 역방향 이동
if (_currentFocusedField == _yInputField)
{
_xInputField.Select();
_xInputField.ActivateInputField();
}
}
else
{
// Tab: 정방향 이동
if (_currentFocusedField == _xInputField)
{
_yInputField.Select();
_yInputField.ActivateInputField();
}
}
}
}
/// <summary>
@@ -129,8 +188,17 @@ namespace UVC.UI.Window.PropertyWindow.UI
/// </summary>
private void OnDestroy()
{
if (_xInputField != null) _xInputField.onEndEdit.RemoveAllListeners();
if (_yInputField != null) _yInputField.onEndEdit.RemoveAllListeners();
if (_xInputField != null)
{
_xInputField.onEndEdit.RemoveAllListeners();
_xInputField.onSelect.RemoveAllListeners();
}
if (_yInputField != null)
{
_yInputField.onEndEdit.RemoveAllListeners();
_yInputField.onSelect.RemoveAllListeners();
}
_currentFocusedField = null;
}
}
}

View File

@@ -97,6 +97,86 @@ namespace UVC.UI.Window.PropertyWindow.UI
_xInputField.onEndEdit.AddListener(OnValueSubmitted);
_yInputField.onEndEdit.AddListener(OnValueSubmitted);
_zInputField.onEndEdit.AddListener(OnValueSubmitted);
// Tab 키 네비게이션 설정: X → Y → Z → X 순환
SetupTabNavigation();
}
/// <summary>
/// Tab 키로 X → Y → Z 순서로 포커스가 이동하도록 네비게이션을 설정합니다.
/// </summary>
private void SetupTabNavigation()
{
// X InputField 네비게이션 설정
var xNav = _xInputField.navigation;
xNav.mode = Navigation.Mode.Explicit;
xNav.selectOnRight = _yInputField;
xNav.selectOnDown = _yInputField;
_xInputField.navigation = xNav;
// Y InputField 네비게이션 설정
var yNav = _yInputField.navigation;
yNav.mode = Navigation.Mode.Explicit;
yNav.selectOnLeft = _xInputField;
yNav.selectOnUp = _xInputField;
yNav.selectOnRight = _zInputField;
yNav.selectOnDown = _zInputField;
_yInputField.navigation = yNav;
// Z InputField 네비게이션 설정
var zNav = _zInputField.navigation;
zNav.mode = Navigation.Mode.Explicit;
zNav.selectOnLeft = _yInputField;
zNav.selectOnUp = _yInputField;
_zInputField.navigation = zNav;
// Tab 키 입력 처리를 위한 onSelect 이벤트 등록
_xInputField.onSelect.RemoveAllListeners();
_yInputField.onSelect.RemoveAllListeners();
_zInputField.onSelect.RemoveAllListeners();
_xInputField.onSelect.AddListener(_ => _currentFocusedField = _xInputField);
_yInputField.onSelect.AddListener(_ => _currentFocusedField = _yInputField);
_zInputField.onSelect.AddListener(_ => _currentFocusedField = _zInputField);
}
private TMP_InputField _currentFocusedField;
private void Update()
{
// Tab 키 입력 처리
if (Input.GetKeyDown(KeyCode.Tab) && _currentFocusedField != null)
{
bool shiftHeld = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
if (shiftHeld)
{
// Shift+Tab: 역방향 이동
if (_currentFocusedField == _zInputField)
{
_yInputField.Select();
_yInputField.ActivateInputField();
}
else if (_currentFocusedField == _yInputField)
{
_xInputField.Select();
_xInputField.ActivateInputField();
}
}
else
{
// Tab: 정방향 이동
if (_currentFocusedField == _xInputField)
{
_yInputField.Select();
_yInputField.ActivateInputField();
}
else if (_currentFocusedField == _yInputField)
{
_zInputField.Select();
_zInputField.ActivateInputField();
}
}
}
}
/// <summary>
@@ -141,9 +221,22 @@ namespace UVC.UI.Window.PropertyWindow.UI
/// </summary>
private void OnDestroy()
{
if (_xInputField != null) _xInputField.onEndEdit.RemoveAllListeners();
if (_yInputField != null) _yInputField.onEndEdit.RemoveAllListeners();
if (_zInputField != null) _zInputField.onEndEdit.RemoveAllListeners();
if (_xInputField != null)
{
_xInputField.onEndEdit.RemoveAllListeners();
_xInputField.onSelect.RemoveAllListeners();
}
if (_yInputField != null)
{
_yInputField.onEndEdit.RemoveAllListeners();
_yInputField.onSelect.RemoveAllListeners();
}
if (_zInputField != null)
{
_zInputField.onEndEdit.RemoveAllListeners();
_zInputField.onSelect.RemoveAllListeners();
}
_currentFocusedField = null;
}
}
}