#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 { /// /// Studio 단축키 관리자 /// RTGLite의 RTInput과 UVC Setting 시스템을 연결하는 브릿지 역할을 합니다. /// /// /// [ 개요 ] /// RTInput의 카메라/그리드/기즈모 단축키는 그대로 유지하면서, /// UVC의 메뉴와 도구 단축키를 Setting에서 관리합니다. /// /// [ 사용 예시 ] /// /// // 1. 단축키 등록 /// shortcutManager.RegisterMenuShortcut("newProject", () => new FileNewCommand().Execute()); /// /// // 2. 단축키 활성화/비활성화 /// shortcutManager.SetShortcutEnabled("newProject", false); /// /// // 3. 설정 변경 후 갱신 /// shortcutManager.RefreshShortcuts(); /// /// [DefaultExecutionOrder(100)] // RTInput보다 늦게 실행 public class StudioShortcutManager : MonoBehaviour { #region Singleton private static StudioShortcutManager? _instance; public static StudioShortcutManager? Instance => _instance; #endregion #region Private Fields private Setting? _setting; // 메뉴 단축키 액션 매핑 private readonly Dictionary _menuShortcuts = new(); // 도구 단축키 액션 매핑 private readonly Dictionary _toolShortcuts = new(); // 단축키 활성화 상태 private readonly Dictionary _shortcutEnabled = new(); // 전역 단축키 비활성화 플래그 (모달 등에서 사용) private bool _globalEnabled = true; // 입력 필드 포커스 상태 캐싱 private bool _isInputFieldFocused = false; #endregion #region Public Properties /// /// 단축키 전역 활성화 상태 /// 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(); } } 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 /// /// Setting을 주입합니다. /// public void SetSetting(Setting setting) { _setting = setting; } /// /// 메뉴 단축키를 등록합니다. /// /// 단축키 ID (Setting의 필드명과 매칭) /// 실행할 액션 public void RegisterMenuShortcut(string shortcutId, Action action) { _menuShortcuts[shortcutId] = new ShortcutEntry(action); _shortcutEnabled[shortcutId] = true; } /// /// 메뉴 단축키를 ICommand와 함께 등록합니다. /// public void RegisterMenuShortcut(string shortcutId, ICommand command) { RegisterMenuShortcut(shortcutId, () => command.Execute()); } /// /// 도구 단축키를 등록합니다. /// public void RegisterToolShortcut(string shortcutId, Action action) { _toolShortcuts[shortcutId] = new ShortcutEntry(action); _shortcutEnabled[shortcutId] = true; } /// /// 도구 단축키를 ICommand와 함께 등록합니다. /// public void RegisterToolShortcut(string shortcutId, ICommand command) { RegisterToolShortcut(shortcutId, () => command.Execute()); } /// /// 단축키 활성화 상태를 설정합니다. /// public void SetShortcutEnabled(string shortcutId, bool enabled) { _shortcutEnabled[shortcutId] = enabled; } /// /// 모든 단축키의 활성화 상태를 조회합니다. /// public bool IsShortcutEnabled(string shortcutId) { return _shortcutEnabled.TryGetValue(shortcutId, out var enabled) && enabled; } /// /// Setting에서 단축키 정보를 다시 읽어옵니다. /// public void RefreshShortcuts() { if (_setting == null && InjectorAppContext.Instance != null) { _setting = InjectorAppContext.Instance.Get(); } Debug.Log("[StudioShortcutManager] 단축키가 갱신되었습니다."); } /// /// 등록된 단축키를 제거합니다. /// public void UnregisterShortcut(string shortcutId) { _menuShortcuts.Remove(shortcutId); _toolShortcuts.Remove(shortcutId); _shortcutEnabled.Remove(shortcutId); } /// /// 모든 등록된 단축키를 제거합니다. /// public void ClearAllShortcuts() { _menuShortcuts.Clear(); _toolShortcuts.Clear(); _shortcutEnabled.Clear(); } #endregion #region Private Methods /// /// 단축키를 처리합니다. /// 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); } } /// /// 메뉴 단축키를 처리합니다. /// private void ProcessMenuShortcuts(MenuShortcuts? menu) { if (menu == null) return; TryExecuteShortcut("newProject", menu.newProject?.key, _menuShortcuts); TryExecuteShortcut("openProject", menu.openProject?.key, _menuShortcuts); TryExecuteShortcut("saveProject", menu.saveProject?.key, _menuShortcuts); TryExecuteShortcut("saveAsProject", menu.saveAsProject?.key, _menuShortcuts); TryExecuteShortcut("insertDb", menu.insertDb?.key, _menuShortcuts); TryExecuteShortcut("exportLayout", menu.exportLayout?.key, _menuShortcuts); TryExecuteShortcut("exportMeta", menu.exportMeta?.key, _menuShortcuts); TryExecuteShortcut("exportGltf", menu.exportGltf?.key, _menuShortcuts); TryExecuteShortcut("undo", menu.undo?.key, _menuShortcuts); TryExecuteShortcut("redo", menu.redo?.key, _menuShortcuts); TryExecuteShortcut("duplicate", menu.duplicate?.key, _menuShortcuts); TryExecuteShortcut("delete", menu.delete?.key, _menuShortcuts); TryExecuteShortcut("createPlane", menu.createPlane?.key, _menuShortcuts); } /// /// 도구 단축키를 처리합니다. /// private void ProcessToolShortcuts(ToolShortcuts? tools) { if (tools == null) return; TryExecuteShortcut("select", tools.select?.key, _toolShortcuts); TryExecuteShortcut("move", tools.move?.key, _toolShortcuts); TryExecuteShortcut("rotate", tools.rotate?.key, _toolShortcuts); TryExecuteShortcut("scale", tools.scale?.key, _toolShortcuts); TryExecuteShortcut("snap", tools.snap?.key, _toolShortcuts); TryExecuteShortcut("guide", tools.guide?.key, _toolShortcuts); TryExecuteShortcut("node", tools.node?.key, _toolShortcuts); TryExecuteShortcut("link", tools.link?.key, _toolShortcuts); TryExecuteShortcut("arc", tools.arc?.key, _toolShortcuts); } /// /// 단축키가 눌렸는지 확인하고 액션을 실행합니다. /// private void TryExecuteShortcut(string shortcutId, string? keyCombo, Dictionary shortcuts) { if (string.IsNullOrEmpty(keyCombo)) return; if (!shortcuts.TryGetValue(shortcutId, out var entry)) return; if (!IsShortcutEnabled(shortcutId)) return; if (IsKeyComboPressed(keyCombo)) { entry.Action?.Invoke(); } } /// /// 키 조합이 눌렸는지 확인합니다. /// /// "Ctrl+S", "Ctrl+Shift+Z", "W" 형식의 키 조합 private bool IsKeyComboPressed(string keyCombo) { var binding = ParseKeyCombo(keyCombo); return IsBindingActive(binding); } /// /// 키 조합 문자열을 파싱합니다. /// private ShortcutBinding ParseKeyCombo(string keyCombo) { var binding = new ShortcutBinding(); if (string.IsNullOrEmpty(keyCombo)) return binding; // 공백 제거 및 '+' 로 분리 var parts = keyCombo.Replace(" ", "").Split('+'); foreach (var part in parts) { var p = part.ToLower(); switch (p) { case "ctrl": case "control": binding.NeedCtrl = true; break; case "shift": binding.NeedShift = true; break; case "alt": binding.NeedAlt = true; break; case "cmd": case "command": binding.NeedCmd = true; break; default: // 메인 키 파싱 if (KeyEx.KeyFromText(p, out Key key)) { binding.MainKey = key; } else { // 특수 키 처리 binding.MainKey = ParseSpecialKey(p); } break; } } return binding; } /// /// 특수 키를 파싱합니다. /// private Key ParseSpecialKey(string keyText) { return keyText.ToLower() switch { "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 }; } /// /// 바인딩이 활성화되었는지 확인합니다. /// 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; } /// /// 입력 필드에 포커스가 있는지 확인합니다. /// 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 또는 InputField 컴포넌트 확인 var tmpInput = selected.GetComponent(); if (tmpInput != null && tmpInput.isFocused) return true; var legacyInput = selected.GetComponent(); if (legacyInput != null && legacyInput.isFocused) return true; return false; } #endregion #region Nested Types /// /// 단축키 바인딩 정보 /// private struct ShortcutBinding { public bool NeedCtrl; public bool NeedShift; public bool NeedAlt; public bool NeedCmd; public Key MainKey; } /// /// 단축키 엔트리 /// private class ShortcutEntry { public Action? Action { get; } public ShortcutEntry(Action action) { Action = action; } } #endregion } }