456 lines
15 KiB
C#
456 lines
15 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 StudioShortcutManager : MonoBehaviour
|
|
{
|
|
#region Singleton
|
|
private static StudioShortcutManager? _instance;
|
|
public static StudioShortcutManager? 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와 함께 등록합니다.
|
|
/// </summary>
|
|
public void RegisterMenuShortcut(string shortcutId, ICommand command)
|
|
{
|
|
RegisterMenuShortcut(shortcutId, () => command.Execute());
|
|
}
|
|
|
|
/// <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>
|
|
/// 단축키를 처리합니다.
|
|
/// </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?.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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 도구 단축키를 처리합니다.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 단축키가 눌렸는지 확인하고 액션을 실행합니다.
|
|
/// </summary>
|
|
private void TryExecuteShortcut(string shortcutId, string? keyCombo, Dictionary<string, ShortcutEntry> shortcuts)
|
|
{
|
|
if (string.IsNullOrEmpty(keyCombo)) return;
|
|
if (!shortcuts.TryGetValue(shortcutId, out var entry)) return;
|
|
if (!IsShortcutEnabled(shortcutId)) return;
|
|
|
|
if (IsKeyComboPressed(keyCombo))
|
|
{
|
|
entry.Action?.Invoke();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 키 조합이 눌렸는지 확인합니다.
|
|
/// </summary>
|
|
/// <param name="keyCombo">"Ctrl+S", "Ctrl+Shift+Z", "W" 형식의 키 조합</param>
|
|
private bool IsKeyComboPressed(string keyCombo)
|
|
{
|
|
var binding = ParseKeyCombo(keyCombo);
|
|
return IsBindingActive(binding);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 키 조합 문자열을 파싱합니다.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 특수 키를 파싱합니다.
|
|
/// </summary>
|
|
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
|
|
};
|
|
}
|
|
|
|
/// <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 또는 InputField 컴포넌트 확인
|
|
var tmpInput = selected.GetComponent<TMPro.TMP_InputField>();
|
|
if (tmpInput != null && tmpInput.isFocused) return true;
|
|
|
|
var legacyInput = selected.GetComponent<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
|
|
}
|
|
}
|