Files
EnglewoodLAB/Assets/Scripts/Studio/Manager/ShortcutManager.cs

483 lines
16 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using RTGLite;
using UnityEngine;
using UnityEngine.InputSystem;
using UVC.Core;
using UVC.Studio.Config;
using UVC.UI.Commands;
namespace UVC.Studio.Manager
{
/// <summary>
/// Studio 단축키 관리자
/// RTGLite의 RTInput과 UVC Setting 시스템을 연결하는 브릿지 역할을 합니다.
/// </summary>
/// <remarks>
/// <para><b>[ 개요 ]</b></para>
/// <para>RTInput의 카메라/그리드/기즈모 단축키는 그대로 유지하면서,</para>
/// <para>UVC의 메뉴와 도구 단축키를 Setting에서 관리합니다.</para>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// // 1. 단축키 등록
/// shortcutManager.RegisterMenuShortcut("newProject", () => new FileNewCommand().Execute());
///
/// // 2. 단축키 활성화/비활성화
/// shortcutManager.SetShortcutEnabled("newProject", false);
///
/// // 3. 설정 변경 후 갱신
/// shortcutManager.RefreshShortcuts();
/// </code>
/// </remarks>
[DefaultExecutionOrder(100)] // RTInput보다 늦게 실행
public class ShortcutManager : MonoBehaviour
{
#region Singleton
private static ShortcutManager? _instance;
public static ShortcutManager? Instance => _instance;
#endregion
#region Private Fields
private Setting? _setting;
// 메뉴 단축키 액션 매핑
private readonly Dictionary<string, ShortcutEntry> _menuShortcuts = new();
// 도구 단축키 액션 매핑
private readonly Dictionary<string, ShortcutEntry> _toolShortcuts = new();
// 단축키 활성화 상태
private readonly Dictionary<string, bool> _shortcutEnabled = new();
// 전역 단축키 비활성화 플래그 (모달 등에서 사용)
private bool _globalEnabled = true;
// 입력 필드 포커스 상태 캐싱
private bool _isInputFieldFocused = false;
#endregion
#region Public Properties
/// <summary>
/// 단축키 전역 활성화 상태
/// </summary>
public bool GlobalEnabled
{
get => _globalEnabled;
set => _globalEnabled = value;
}
#endregion
#region Unity Lifecycle
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
}
private void Start()
{
// Setting 가져오기
if (InjectorAppContext.Instance != null)
{
_setting = InjectorAppContext.Instance.Get<Setting>();
}
}
private void Update()
{
// 전역 비활성화 상태면 스킵
if (!_globalEnabled) return;
// 입력 필드에 포커스가 있으면 스킵
if (IsInputFieldFocused()) return;
// RTInput의 마우스 조작 중에는 스킵 (카메라 이동 등)
if (RTInput.get != null && RTInput.get.anyMBPressed) return;
// 단축키 처리
ProcessShortcuts();
}
private void OnDestroy()
{
if (_instance == this)
{
_instance = null;
}
}
#endregion
#region Public Methods
/// <summary>
/// Setting을 주입합니다.
/// </summary>
public void SetSetting(Setting setting)
{
_setting = setting;
}
/// <summary>
/// 메뉴 단축키를 등록합니다.
/// </summary>
/// <param name="shortcutId">단축키 ID (Setting의 필드명과 매칭)</param>
/// <param name="action">실행할 액션</param>
public void RegisterMenuShortcut(string shortcutId, Action action)
{
_menuShortcuts[shortcutId] = new ShortcutEntry(action);
_shortcutEnabled[shortcutId] = true;
}
/// <summary>
/// 메뉴 단축키를 ICommand와 함께 등록합니다.
/// IUndoableCommand인 경우 UndoRedoManager를 통해 실행됩니다.
/// </summary>
public void RegisterMenuShortcut(string shortcutId, ICommand command)
{
RegisterMenuShortcut(shortcutId, () => ExecuteCommand(command));
}
/// <summary>
/// 도구 단축키를 등록합니다.
/// </summary>
public void RegisterToolShortcut(string shortcutId, Action action)
{
_toolShortcuts[shortcutId] = new ShortcutEntry(action);
_shortcutEnabled[shortcutId] = true;
}
/// <summary>
/// 도구 단축키를 ICommand와 함께 등록합니다.
/// </summary>
public void RegisterToolShortcut(string shortcutId, ICommand command)
{
RegisterToolShortcut(shortcutId, () => command.Execute());
}
/// <summary>
/// 단축키 활성화 상태를 설정합니다.
/// </summary>
public void SetShortcutEnabled(string shortcutId, bool enabled)
{
_shortcutEnabled[shortcutId] = enabled;
}
/// <summary>
/// 모든 단축키의 활성화 상태를 조회합니다.
/// </summary>
public bool IsShortcutEnabled(string shortcutId)
{
return _shortcutEnabled.TryGetValue(shortcutId, out var enabled) && enabled;
}
/// <summary>
/// Setting에서 단축키 정보를 다시 읽어옵니다.
/// </summary>
public void RefreshShortcuts()
{
if (_setting == null && InjectorAppContext.Instance != null)
{
_setting = InjectorAppContext.Instance.Get<Setting>();
}
Debug.Log("[StudioShortcutManager] 단축키가 갱신되었습니다.");
}
/// <summary>
/// 등록된 단축키를 제거합니다.
/// </summary>
public void UnregisterShortcut(string shortcutId)
{
_menuShortcuts.Remove(shortcutId);
_toolShortcuts.Remove(shortcutId);
_shortcutEnabled.Remove(shortcutId);
}
/// <summary>
/// 모든 등록된 단축키를 제거합니다.
/// </summary>
public void ClearAllShortcuts()
{
_menuShortcuts.Clear();
_toolShortcuts.Clear();
_shortcutEnabled.Clear();
}
#endregion
#region Private Methods
/// <summary>
/// Command를 실행합니다.
/// IUndoableCommand인 경우 UndoRedoManager를 통해 실행합니다.
/// </summary>
private void ExecuteCommand(ICommand command)
{
if (command == null) return;
if (command is IUndoableCommand undoableCommand)
{
var undoRedoManager = UndoRedoManager.Instance;
if (undoRedoManager != null)
{
undoRedoManager.ExecuteCommand(undoableCommand);
return;
}
}
command.Execute();
}
/// <summary>
/// 단축키를 처리합니다.
/// </summary>
private void ProcessShortcuts()
{
if (_setting == null) return;
var shortcuts = _setting.Data?.shortcuts;
if (shortcuts == null) return;
// 메뉴 단축키 처리
ProcessMenuShortcuts(shortcuts.menu);
// 도구 단축키 처리 (카메라 Fly 모드가 아닐 때만)
if (RTCamera.get == null || RTCamera.get.navigationMode == ECameraNavigationMode.None)
{
ProcessToolShortcuts(shortcuts.tools);
}
}
/// <summary>
/// 메뉴 단축키를 처리합니다.
/// </summary>
private void ProcessMenuShortcuts(MenuShortcuts? menu)
{
if (menu == null) return;
TryExecuteShortcut("newProject", menu.newProject, _menuShortcuts);
TryExecuteShortcut("openProject", menu.openProject, _menuShortcuts);
TryExecuteShortcut("saveProject", menu.saveProject, _menuShortcuts);
TryExecuteShortcut("saveAsProject", menu.saveAsProject, _menuShortcuts);
TryExecuteShortcut("insertDb", menu.insertDb, _menuShortcuts);
TryExecuteShortcut("exportLayout", menu.exportLayout, _menuShortcuts);
TryExecuteShortcut("exportMeta", menu.exportMeta, _menuShortcuts);
TryExecuteShortcut("exportGltf", menu.exportGltf, _menuShortcuts);
TryExecuteShortcut("undo", menu.undo, _menuShortcuts);
TryExecuteShortcut("redo", menu.redo, _menuShortcuts);
TryExecuteShortcut("duplicate", menu.duplicate, _menuShortcuts);
TryExecuteShortcut("delete", menu.delete, _menuShortcuts);
TryExecuteShortcut("createPlane", menu.createPlane, _menuShortcuts);
}
/// <summary>
/// 도구 단축키를 처리합니다.
/// </summary>
private void ProcessToolShortcuts(ToolShortcuts? tools)
{
if (tools == null) return;
TryExecuteShortcut("select", tools.select, _toolShortcuts);
TryExecuteShortcut("move", tools.move, _toolShortcuts);
TryExecuteShortcut("rotate", tools.rotate, _toolShortcuts);
TryExecuteShortcut("scale", tools.scale, _toolShortcuts);
TryExecuteShortcut("snap", tools.snap, _toolShortcuts);
TryExecuteShortcut("guide", tools.guide, _toolShortcuts);
TryExecuteShortcut("node", tools.node, _toolShortcuts);
TryExecuteShortcut("link", tools.link, _toolShortcuts);
TryExecuteShortcut("arc", tools.arc, _toolShortcuts);
}
/// <summary>
/// 단축키가 눌렸는지 확인하고 액션을 실행합니다.
/// </summary>
private void TryExecuteShortcut(string shortcutId, ShortcutItem? shortcutItem, Dictionary<string, ShortcutEntry> shortcuts)
{
if (shortcutItem == null || string.IsNullOrEmpty(shortcutItem.key)) return;
if (!shortcuts.TryGetValue(shortcutId, out var entry)) return;
if (!IsShortcutEnabled(shortcutId)) return;
if (IsShortcutPressed(shortcutItem))
{
entry.Action?.Invoke();
}
}
/// <summary>
/// ShortcutItem의 단축키가 눌렸는지 확인합니다.
/// </summary>
private bool IsShortcutPressed(ShortcutItem shortcutItem)
{
var binding = CreateBindingFromShortcutItem(shortcutItem);
return IsBindingActive(binding);
}
/// <summary>
/// ShortcutItem에서 ShortcutBinding을 생성합니다.
/// </summary>
private ShortcutBinding CreateBindingFromShortcutItem(ShortcutItem shortcutItem)
{
var binding = new ShortcutBinding
{
NeedCtrl = shortcutItem.ctrl,
NeedShift = shortcutItem.shift,
NeedAlt = shortcutItem.alt,
NeedCmd = false,
MainKey = Key.None
};
// 메인 키 파싱
var keyText = shortcutItem.key.ToLower();
// 먼저 ParseSpecialKey로 시도 (숫자 키 및 특수 키)
var specialKey = ParseSpecialKey(keyText);
if (specialKey != Key.None)
{
binding.MainKey = specialKey;
}
else if (KeyEx.KeyFromText(keyText, out Key key))
{
binding.MainKey = key;
}
else
{
Debug.LogWarning($"[ShortcutManager] Failed to parse key: '{keyText}'");
}
return binding;
}
/// <summary>
/// 특수 키를 파싱합니다.
/// </summary>
private Key ParseSpecialKey(string keyText)
{
return keyText.ToLower() switch
{
// 숫자 키 (KeyEx가 실패할 경우 대비)
"0" => Key.Digit0,
"1" => Key.Digit1,
"2" => Key.Digit2,
"3" => Key.Digit3,
"4" => Key.Digit4,
"5" => Key.Digit5,
"6" => Key.Digit6,
"7" => Key.Digit7,
"8" => Key.Digit8,
"9" => Key.Digit9,
// 특수 키
"delete" => Key.Delete,
"backspace" => Key.Backspace,
"enter" => Key.Enter,
"return" => Key.Enter,
"escape" => Key.Escape,
"esc" => Key.Escape,
"space" => Key.Space,
"tab" => Key.Tab,
"up" => Key.UpArrow,
"down" => Key.DownArrow,
"left" => Key.LeftArrow,
"right" => Key.RightArrow,
"f1" => Key.F1,
"f2" => Key.F2,
"f3" => Key.F3,
"f4" => Key.F4,
"f5" => Key.F5,
"f6" => Key.F6,
"f7" => Key.F7,
"f8" => Key.F8,
"f9" => Key.F9,
"f10" => Key.F10,
"f11" => Key.F11,
"f12" => Key.F12,
_ => Key.None
};
}
/// <summary>
/// 바인딩이 활성화되었는지 확인합니다.
/// </summary>
private bool IsBindingActive(ShortcutBinding binding)
{
var rtInput = RTInput.get;
if (rtInput == null) return false;
// 수정자 키 확인
if (binding.NeedCtrl != rtInput.ctrlPressed) return false;
if (binding.NeedShift != rtInput.shiftPressed) return false;
if (binding.NeedAlt != rtInput.altPresed) return false;
if (binding.NeedCmd != rtInput.cmdPressed) return false;
// 메인 키 확인 (KeyWentDown으로 단발 입력만 감지)
if (binding.MainKey != Key.None && !rtInput.KeyWentDown(binding.MainKey))
{
return false;
}
return true;
}
/// <summary>
/// 입력 필드에 포커스가 있는지 확인합니다.
/// </summary>
private bool IsInputFieldFocused()
{
// Unity EventSystem에서 현재 선택된 객체 확인
var eventSystem = UnityEngine.EventSystems.EventSystem.current;
if (eventSystem == null) return false;
var selected = eventSystem.currentSelectedGameObject;
if (selected == null) return false;
// TMP_InputField 확인 (자기 자신 및 부모 계층에서 검색)
var tmpInput = selected.GetComponent<TMPro.TMP_InputField>();
if (tmpInput == null)
{
tmpInput = selected.GetComponentInParent<TMPro.TMP_InputField>();
}
if (tmpInput != null && tmpInput.isFocused) return true;
// Legacy InputField 확인 (자기 자신 및 부모 계층에서 검색)
var legacyInput = selected.GetComponent<UnityEngine.UI.InputField>();
if (legacyInput == null)
{
legacyInput = selected.GetComponentInParent<UnityEngine.UI.InputField>();
}
if (legacyInput != null && legacyInput.isFocused) return true;
return false;
}
#endregion
#region Nested Types
/// <summary>
/// 단축키 바인딩 정보
/// </summary>
private struct ShortcutBinding
{
public bool NeedCtrl;
public bool NeedShift;
public bool NeedAlt;
public bool NeedCmd;
public Key MainKey;
}
/// <summary>
/// 단축키 엔트리
/// </summary>
private class ShortcutEntry
{
public Action? Action { get; }
public ShortcutEntry(Action action)
{
Action = action;
}
}
#endregion
}
}