483 lines
16 KiB
C#
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
|
|
}
|
|
}
|