단축키, 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

@@ -0,0 +1,17 @@
#nullable enable
using UnityEngine;
using UVC.UI.Commands;
namespace UVC.Studio.Command
{
/// <summary>
/// 평면 생성 커맨드
/// </summary>
public class EditCreatePlaneCommand : ICommand
{
public void Execute(object? parameter = null)
{
Debug.Log("[EditCreatePlaneCommand] 평면 생성 실행");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 523b8d20bbc8bb24c8e0ef50a813fa51

View File

@@ -0,0 +1,17 @@
#nullable enable
using UnityEngine;
using UVC.UI.Commands;
namespace UVC.Studio.Command
{
/// <summary>
/// 삭제 커맨드
/// </summary>
public class EditDeleteCommand : ICommand
{
public void Execute(object? parameter = null)
{
Debug.Log("[EditDeleteCommand] 삭제 실행");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 020434f1949a50b42bc2e42d539f79d0

View File

@@ -0,0 +1,17 @@
#nullable enable
using UnityEngine;
using UVC.UI.Commands;
namespace UVC.Studio.Command
{
/// <summary>
/// 복제 커맨드
/// </summary>
public class EditDuplicateCommand : ICommand
{
public void Execute(object? parameter = null)
{
Debug.Log("[EditDuplicateCommand] 복제 실행");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5a444000df366b245b0f8de533a3d65d

View File

@@ -0,0 +1,17 @@
#nullable enable
using UnityEngine;
using UVC.UI.Commands;
namespace UVC.Studio.Command
{
/// <summary>
/// 다시 실행(Redo) 커맨드
/// </summary>
public class EditRedoCommand : ICommand
{
public void Execute(object? parameter = null)
{
Debug.Log("[EditRedoCommand] 다시 실행 실행");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: db560e448649b964c871f812fb1d13f2

View File

@@ -0,0 +1,17 @@
#nullable enable
using UnityEngine;
using UVC.UI.Commands;
namespace UVC.Studio.Command
{
/// <summary>
/// 되돌리기(Undo) 커맨드
/// </summary>
public class EditUndoCommand : ICommand
{
public void Execute(object? parameter = null)
{
Debug.Log("[EditUndoCommand] 되돌리기 실행");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cba471b0f84f6614bbcab9d29d7b835b

View File

@@ -0,0 +1,17 @@
#nullable enable
using UnityEngine;
using UVC.UI.Commands;
namespace UVC.Studio.Command
{
/// <summary>
/// GLTF 내보내기 커맨드
/// </summary>
public class FileExportGltfCommand : ICommand
{
public void Execute(object? parameter = null)
{
Debug.Log("[FileExportGltfCommand] GLTF 내보내기 실행");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ddc95f7402023d14eba701165d846d05

View File

@@ -0,0 +1,17 @@
#nullable enable
using UnityEngine;
using UVC.UI.Commands;
namespace UVC.Studio.Command
{
/// <summary>
/// Layout Json 내보내기 커맨드
/// </summary>
public class FileExportLayoutCommand : ICommand
{
public void Execute(object? parameter = null)
{
Debug.Log("[FileExportLayoutCommand] Layout Json 내보내기 실행");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: af9b092ea7f932545aec538a78f23c4c

View File

@@ -0,0 +1,17 @@
#nullable enable
using UnityEngine;
using UVC.UI.Commands;
namespace UVC.Studio.Command
{
/// <summary>
/// Meta Data Json 내보내기 커맨드
/// </summary>
public class FileExportMetaCommand : ICommand
{
public void Execute(object? parameter = null)
{
Debug.Log("[FileExportMetaCommand] Meta Data Json 내보내기 실행");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cc4809e03e9bd6343996f1b12cb68b4a

View File

@@ -0,0 +1,17 @@
#nullable enable
using UnityEngine;
using UVC.UI.Commands;
namespace UVC.Studio.Command
{
/// <summary>
/// 데이터베이스 삽입 커맨드
/// </summary>
public class FileInsertDbCommand : ICommand
{
public void Execute(object? parameter = null)
{
Debug.Log("[FileInsertDbCommand] 데이터베이스 삽입 실행");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 210fe2c58c6bba44eb2fb812b8bc59dc

View File

@@ -0,0 +1,17 @@
#nullable enable
using UnityEngine;
using UVC.UI.Commands;
namespace UVC.Studio.Command
{
/// <summary>
/// 새 파일 생성 커맨드
/// </summary>
public class FileNewCommand : ICommand
{
public void Execute(object? parameter = null)
{
Debug.Log("[FileNewCommand] 새 파일 생성 실행");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 86262663cb50b634689a511589245f0a

View File

@@ -0,0 +1,17 @@
#nullable enable
using UnityEngine;
using UVC.UI.Commands;
namespace UVC.Studio.Command
{
/// <summary>
/// 파일 열기 커맨드
/// </summary>
public class FileOpenCommand : ICommand
{
public void Execute(object? parameter = null)
{
Debug.Log("[FileOpenCommand] 파일 열기 실행");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 70708e355b03ef14a8473c8615e57cb2

View File

@@ -0,0 +1,17 @@
#nullable enable
using UnityEngine;
using UVC.UI.Commands;
namespace UVC.Studio.Command
{
/// <summary>
/// 다른 이름으로 저장 커맨드
/// </summary>
public class FileSaveAsCommand : ICommand
{
public void Execute(object? parameter = null)
{
Debug.Log("[FileSaveAsCommand] 다른 이름으로 저장 실행");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 897b4bc4deb9e6648ba03de4bc5fec8b

View File

@@ -0,0 +1,17 @@
#nullable enable
using UnityEngine;
using UVC.UI.Commands;
namespace UVC.Studio.Command
{
/// <summary>
/// 파일 저장 커맨드
/// </summary>
public class FileSaveCommand : ICommand
{
public void Execute(object? parameter = null)
{
Debug.Log("[FileSaveCommand] 파일 저장 실행");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7edb402279b68d648b565362457cdbf8

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 26cbb88db439178438f4bc3bd8180d1f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,8 +1,8 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using EPOOutline;
using RTGLite;
using UnityEngine;
using UVC.Core;
using UVC.Studio.Config;
@@ -11,11 +11,28 @@ using UVC.UI.Window.PropertyWindow;
namespace UVC.Studio.Manager
{
/// <summary>
/// 변환 도구 타입
/// </summary>
public enum TransformToolType
{
/// <summary>선택 도구 (기즈모 없음)</summary>
Select,
/// <summary>이동 도구</summary>
Move,
/// <summary>회전 도구</summary>
Rotate,
/// <summary>크기 조절 도구</summary>
Scale
}
/// <summary>
/// 스테이지 객체의 선택 상태를 관리하는 매니저 클래스
/// </summary>
public class SelectionManager
{
#region Private Fields - Selection
/// <summary>
/// 현재 선택된 StageObject 목록
/// </summary>
@@ -36,12 +53,60 @@ namespace UVC.Studio.Manager
/// </summary>
private StageObjectManager.StageObject? _currentDisplayedObject;
/// <summary>
/// 현재 PropertyWindow에 표시 중인 자식 Transform (모델의 자식 선택 시)
/// </summary>
private Transform? _currentDisplayedChildTransform;
/// <summary>
/// 미리보기 색상 적용 전 원본 Material 색상을 저장하는 딕셔너리
/// Key: Renderer, Value: (프로퍼티 이름, 원본 색상)
/// </summary>
private readonly Dictionary<Renderer, (string propertyName, Color color)> _originalColors = new();
/// <summary>
/// 캐싱된 Outliner 참조 (매번 FindFirstObjectByType 호출 방지)
/// </summary>
private Outliner? _cachedOutliner;
/// <summary>
/// Outliner 캐시 초기화 여부
/// </summary>
private bool _outlinerCacheInitialized = false;
#endregion
#region Private Fields - Gizmos
/// <summary>
/// 이동 기즈모
/// </summary>
private MoveGizmo? _moveGizmo;
/// <summary>
/// 회전 기즈모
/// </summary>
private RotateGizmo? _rotateGizmo;
/// <summary>
/// 크기 조절 기즈모
/// </summary>
private ScaleGizmo? _scaleGizmo;
/// <summary>
/// 현재 활성화된 변환 도구 타입
/// </summary>
private TransformToolType _currentToolType = TransformToolType.Select;
/// <summary>
/// 기즈모 초기화 여부
/// </summary>
private bool _gizmosInitialized = false;
#endregion
#region Events
/// <summary>
/// 선택 변경 시 발생하는 이벤트
/// (선택된 객체, 선택 여부)
@@ -54,6 +119,15 @@ namespace UVC.Studio.Manager
/// </summary>
public event Action<string, string>? OnObjectNameChanged;
/// <summary>
/// 변환 도구가 변경되었을 때 발생하는 이벤트
/// </summary>
public event Action<TransformToolType>? OnToolChanged;
#endregion
#region Public Properties
/// <summary>
/// 현재 선택된 객체 목록 (읽기 전용)
/// </summary>
@@ -64,6 +138,13 @@ namespace UVC.Studio.Manager
/// </summary>
public int SelectedCount => _selectedObjects.Count;
/// <summary>
/// 현재 활성화된 변환 도구 타입
/// </summary>
public TransformToolType CurrentToolType => _currentToolType;
#endregion
/// <summary>
/// 생성자
/// </summary>
@@ -234,22 +315,18 @@ namespace UVC.Studio.Manager
EnsureOutlinableSettings(outlinable);
outlinable.enabled = enabled;
Debug.Log($"[SelectionManager] SetOutlinableEnabled: {stageObject.GameObject.name} -> {enabled}, Targets: {outlinable.OutlineTargets.Count}, RenderStyle: {outlinable.RenderStyle}, FrontEnabled: {outlinable.FrontParameters.Enabled}, FrontColor: {outlinable.FrontParameters.Color}");
// Outliner 존재 여부 확인
var outliner = UnityEngine.Object.FindFirstObjectByType<Outliner>();
if (outliner != null)
// Outliner 존재 여부 확인 (캐싱)
if (!_outlinerCacheInitialized)
{
Debug.Log($"[SelectionManager] Outliner found on: {outliner.gameObject.name}, enabled: {outliner.enabled}");
_cachedOutliner = UnityEngine.Object.FindFirstObjectByType<Outliner>();
_outlinerCacheInitialized = true;
if (_cachedOutliner == null)
{
Debug.LogWarning("[SelectionManager] Outliner component not found in scene! EPOOutline requires Outliner on camera.");
}
}
else
{
Debug.LogWarning("[SelectionManager] Outliner component not found in scene! EPOOutline requires Outliner on camera.");
}
}
else
{
Debug.LogWarning($"[SelectionManager] SetOutlinableEnabled: Outlinable not found on {stageObject.GameObject.name}");
}
}
@@ -424,6 +501,53 @@ namespace UVC.Studio.Manager
#region PropertyWindow Integration
/// <summary>
/// 자식 Transform의 속성을 PropertyWindow에 표시합니다.
/// </summary>
/// <param name="childTransform">표시할 자식 Transform</param>
/// <param name="childName">자식 이름</param>
public void DisplayChildTransformProperties(Transform childTransform, string childName)
{
if (_propertyWindow == null || childTransform == null) return;
// 기존 StageObject 표시 상태 초기화
_currentDisplayedObject = null;
_currentDisplayedChildTransform = childTransform;
var entries = new List<IPropertyEntry>();
int orderIndex = 0;
// 1. 자식 이름 표시 (읽기 전용)
var nameProperty = new StringProperty("child_name", "Name", childName)
{
IsReadOnly = true,
Order = orderIndex++
};
entries.Add(nameProperty);
// 2. Transform 그룹 추가
var transformGroup = new PropertyGroup("transform", "Transform", order: orderIndex++);
transformGroup.AddItems(new IPropertyItem[]
{
new Vector3Property("transform_position", "Position", childTransform.localPosition),
new Vector3Property("transform_rotation", "Rotation", childTransform.localEulerAngles),
new Vector3Property("transform_scale", "Scale", childTransform.localScale)
});
entries.Add(transformGroup);
_propertyWindow.LoadMixedProperties(entries);
Debug.Log($"[SelectionManager] DisplayChildTransformProperties: {childName}");
}
/// <summary>
/// 자식 Transform 표시 상태를 초기화합니다.
/// </summary>
public void ClearChildTransformDisplay()
{
_currentDisplayedChildTransform = null;
}
/// <summary>
/// StageObject의 Equipment PropertiesInfo를 PropertyWindow에 표시합니다.
/// </summary>
@@ -432,6 +556,8 @@ namespace UVC.Studio.Manager
{
if (_propertyWindow == null) return;
// 자식 Transform 표시 상태 초기화
_currentDisplayedChildTransform = null;
_currentDisplayedObject = stageObject;
var equipment = stageObject.Equipment;
@@ -544,6 +670,33 @@ namespace UVC.Studio.Manager
/// </summary>
private void OnPropertyValueChanged(object? sender, PropertyValueChangedEventArgs e)
{
// 자식 Transform이 표시 중인 경우
if (_currentDisplayedChildTransform != null)
{
switch (e.PropertyId)
{
case "transform_position":
if (e.NewValue is Vector3 childPos)
{
_currentDisplayedChildTransform.localPosition = childPos;
}
break;
case "transform_rotation":
if (e.NewValue is Vector3 childRot)
{
_currentDisplayedChildTransform.localEulerAngles = childRot;
}
break;
case "transform_scale":
if (e.NewValue is Vector3 childScale)
{
_currentDisplayedChildTransform.localScale = childScale;
}
break;
}
return;
}
if (_currentDisplayedObject == null) return;
switch (e.PropertyId)
@@ -706,5 +859,314 @@ namespace UVC.Studio.Manager
}
#endregion
#region Gizmo Management
/// <summary>
/// 기즈모를 초기화합니다. RTGizmos가 준비된 후 호출해야 합니다.
/// </summary>
public void InitializeGizmos()
{
if (_gizmosInitialized) return;
if (RTGizmos.get == null)
{
Debug.LogWarning("[SelectionManager] RTGizmos가 아직 초기화되지 않았습니다.");
return;
}
// 기즈모 생성
_moveGizmo = RTGizmos.get.CreateObjectMoveGizmo();
_rotateGizmo = RTGizmos.get.CreateObjectRotateGizmo();
_scaleGizmo = RTGizmos.get.CreateObjectScaleGizmo();
// 기본적으로 모든 기즈모 비활성화 (Select 모드)
_moveGizmo.enabled = false;
_rotateGizmo.enabled = false;
_scaleGizmo.enabled = false;
_gizmosInitialized = true;
_currentToolType = TransformToolType.Select;
Debug.Log("[SelectionManager] 기즈모가 초기화되었습니다.");
}
/// <summary>
/// 마우스 위치에서 객체를 픽킹합니다. RTScene.Raycast를 사용합니다.
/// </summary>
/// <returns>픽킹된 GameObject, 없으면 null</returns>
public GameObject? PickObject()
{
if (RTScene.get == null) return null;
// 마우스 위치에서 Ray 생성
Ray ray = Camera.main.ScreenPointToRay(UnityEngine.Input.mousePosition);
// RTScene의 Raycast 사용 (Collider 없이도 MeshRenderer로 감지)
if (RTScene.get.Raycast(ray, null, false, out SceneRayHit rayHit))
{
return rayHit.objectHit.gameObject;
}
return null;
}
/// <summary>
/// 마우스 클릭으로 객체를 선택합니다. Update에서 호출됩니다.
/// </summary>
/// <param name="addToSelection">다중 선택 여부 (Ctrl 키 누름)</param>
public void HandleMouseSelection(bool addToSelection = false)
{
var pickedObject = PickObject();
if (pickedObject == null)
{
// 빈 공간 클릭 시 선택 해제 (Ctrl 키가 눌려있지 않을 때만)
if (!addToSelection)
{
DeselectAll();
UpdateGizmoTargets();
}
return;
}
// 픽킹된 객체가 StageObject인지 확인
var stageObject = FindStageObjectForGameObject(pickedObject);
if (stageObject != null)
{
if (addToSelection)
{
// Ctrl+클릭: 토글 선택
ToggleSelection(stageObject, true);
}
else
{
// 일반 클릭: 단일 선택
Select(stageObject, false);
}
// 기즈모 타겟 업데이트
UpdateGizmoTargets();
}
}
/// <summary>
/// GameObject로부터 해당하는 StageObject를 찾습니다.
/// </summary>
/// <param name="gameObject">검색할 GameObject</param>
/// <returns>찾은 StageObject, 없으면 null</returns>
private StageObjectManager.StageObject? FindStageObjectForGameObject(GameObject gameObject)
{
if (gameObject == null) return null;
// StageObjectManager의 GetByGameObject 사용
var stageObject = _stageObjectManager.GetByGameObject(gameObject);
if (stageObject != null)
{
return stageObject;
}
// 부모 계층에서 StageObject 검색
Transform? current = gameObject.transform.parent;
while (current != null)
{
stageObject = _stageObjectManager.GetByGameObject(current.gameObject);
if (stageObject != null)
{
return stageObject;
}
current = current.parent;
}
return null;
}
/// <summary>
/// 선택된 모든 객체를 기즈모 타겟으로 설정합니다.
/// </summary>
public void UpdateGizmoTargets()
{
if (!_gizmosInitialized) return;
// 선택된 객체들의 GameObject 리스트 생성
var targetObjects = new List<GameObject>();
foreach (var stageObject in _selectedObjects)
{
if (stageObject.GameObject != null)
{
targetObjects.Add(stageObject.GameObject);
}
}
// 모든 기즈모에 타겟 설정
if (_moveGizmo != null)
{
_moveGizmo.objectTransformGizmo.SetTargets(targetObjects);
}
if (_rotateGizmo != null)
{
_rotateGizmo.objectTransformGizmo.SetTargets(targetObjects);
}
if (_scaleGizmo != null)
{
_scaleGizmo.objectTransformGizmo.SetTargets(targetObjects);
}
// 현재 활성 기즈모 새로고침
RefreshActiveGizmo();
Debug.Log($"[SelectionManager] 기즈모 타겟 업데이트: {targetObjects.Count}개 객체");
}
/// <summary>
/// 활성 변환 도구를 설정합니다.
/// </summary>
/// <param name="toolType">설정할 도구 타입</param>
public void SetActiveTool(TransformToolType toolType)
{
if (!_gizmosInitialized)
{
Debug.LogWarning("[SelectionManager] 기즈모가 초기화되지 않았습니다. InitializeGizmos()를 먼저 호출하세요.");
return;
}
_currentToolType = toolType;
// 모든 기즈모 비활성화
if (_moveGizmo != null) _moveGizmo.enabled = false;
if (_rotateGizmo != null) _rotateGizmo.enabled = false;
if (_scaleGizmo != null) _scaleGizmo.enabled = false;
// 선택된 도구에 해당하는 기즈모 활성화
switch (toolType)
{
case TransformToolType.Move:
if (_moveGizmo != null)
{
_moveGizmo.enabled = true;
_moveGizmo.objectTransformGizmo.Refresh();
}
break;
case TransformToolType.Rotate:
if (_rotateGizmo != null)
{
_rotateGizmo.enabled = true;
_rotateGizmo.objectTransformGizmo.Refresh();
}
break;
case TransformToolType.Scale:
if (_scaleGizmo != null)
{
_scaleGizmo.enabled = true;
_scaleGizmo.objectTransformGizmo.Refresh();
}
break;
case TransformToolType.Select:
default:
// 모든 기즈모 비활성화 상태 유지
break;
}
Debug.Log($"[SelectionManager] 활성 도구 변경: {toolType}");
OnToolChanged?.Invoke(toolType);
}
/// <summary>
/// 현재 활성화된 기즈모를 새로고침합니다.
/// </summary>
public void RefreshActiveGizmo()
{
if (!_gizmosInitialized) return;
switch (_currentToolType)
{
case TransformToolType.Move:
_moveGizmo?.objectTransformGizmo.Refresh();
break;
case TransformToolType.Rotate:
_rotateGizmo?.objectTransformGizmo.Refresh();
break;
case TransformToolType.Scale:
_scaleGizmo?.objectTransformGizmo.Refresh();
break;
}
}
/// <summary>
/// 기즈모가 초기화되었는지 여부를 반환합니다.
/// </summary>
public bool IsGizmosInitialized => _gizmosInitialized;
#endregion
#region Memory Management
/// <summary>
/// 리소스를 정리합니다. 씬 전환 시 호출해야 합니다.
/// </summary>
public void Dispose()
{
// 이벤트 구독 해제
if (_propertyWindow != null)
{
_propertyWindow.PropertyValueChanged -= OnPropertyValueChanged;
}
// 선택 해제
DeselectAll();
// 원본 색상 딕셔너리 정리
_originalColors.Clear();
// 캐시 초기화
_cachedOutliner = null;
_outlinerCacheInitialized = false;
// 기즈모 참조 해제 (RTGizmos가 실제 파괴 담당)
_moveGizmo = null;
_rotateGizmo = null;
_scaleGizmo = null;
_gizmosInitialized = false;
Debug.Log("[SelectionManager] Disposed");
}
/// <summary>
/// 파괴된 Renderer 참조를 정리합니다.
/// 주기적으로 또는 객체 삭제 시 호출하면 좋습니다.
/// </summary>
public void CleanupDestroyedRenderers()
{
// Unity에서 파괴된 객체는 == null로 체크 가능하지만 실제로는 null이 아님
// 따라서 null 체크된 키들을 별도 리스트에 저장 후 삭제
var keysToRemove = new List<Renderer>();
foreach (var kvp in _originalColors)
{
// Unity 객체의 파괴 여부 확인
if (kvp.Key == null || !kvp.Key)
{
keysToRemove.Add(kvp.Key!);
}
}
int removedCount = 0;
foreach (var key in keysToRemove)
{
if (_originalColors.Remove(key))
{
removedCount++;
}
}
if (removedCount > 0)
{
Debug.Log($"[SelectionManager] Cleaned up {removedCount} destroyed renderer references");
}
}
#endregion
}
}

View File

@@ -0,0 +1,455 @@
#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
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9bd93a337d873344bbb0fe708dd4df34

View File

@@ -242,6 +242,12 @@ namespace UVC.Studio.Modal.Settings
{
await setting.SaveAsync();
Debug.Log("Shortcut settings saved.");
// TopMenu 단축키 갱신
if (StudioSceneMain.Instance != null)
{
StudioSceneMain.Instance.RefreshMenuShortcuts();
}
}
}
}

View File

@@ -1,18 +1,17 @@
using Cysharp.Threading.Tasks;
using RTGLite;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using UVC.Core;
using UVC.Data;
using UVC.Data.Core;
using UVC.Locale;
using UVC.Studio.Command;
using UVC.Studio.Config;
using UVC.Studio.Manager;
using UVC.Studio.Modal.Settings;
using UVC.UI.Commands;
using UVC.UI.Loading;
using UVC.UI.Menu;
using UVC.UI.Modal;
using UVC.UI.Toolbar;
using UVC.UI.Toolbar.Model;
using UVC.UI.ToolBar;
@@ -39,11 +38,14 @@ namespace UVC.Studio
[Inject]
private PropertyWindow propertyWindow;
//test code
MoveGizmo mMoveGizmo; // 이동 기즈모
RotateGizmo mRotateGizmo; // 회전 기즈모
ScaleGizmo mScaleGizmo; // 크기 조절 기즈모
[Inject]
private Setting setting;
[Inject]
private SelectionManager selectionManager;
// 단축키 관리자
private StudioShortcutManager? shortcutManager;
public Action Initialized;
@@ -73,65 +75,42 @@ namespace UVC.Studio
private void Initialize()
{
SetupTopMenu();
SetupToolBox();
SetupShortcutManager();
sideTabBar.InitTab();
// SelectionManager 기즈모 초기화
if (selectionManager != null)
{
selectionManager.InitializeGizmos();
}
Initialized?.Invoke();
UILoading.Hide();
// 오브젝트를 이동, 회전, 크기 조절할 수 있는 변환 기즈모를 생성합니다
mMoveGizmo = RTGizmos.get.CreateObjectMoveGizmo();
mRotateGizmo = RTGizmos.get.CreateObjectRotateGizmo();
mScaleGizmo = RTGizmos.get.CreateObjectScaleGizmo();
// 이 간단한 데모에서는 단일 타겟 오브젝트가 있으며 모든 기즈모가 이 오브젝트를 제어합니다
GameObject targetObject = GameObject.Find("Cube");
mMoveGizmo.objectTransformGizmo.SetTarget(targetObject);
mRotateGizmo.objectTransformGizmo.SetTarget(targetObject);
mScaleGizmo.objectTransformGizmo.SetTarget(targetObject);
// 기본적으로 이동 기즈모를 제외한 모든 기즈모를 비활성화합니다. 그런 다음 단축키로 기즈모를 켜고 끌 수 있습니다.
mRotateGizmo.enabled = false;
mScaleGizmo.enabled = false;
}
void Update()
private void Update()
{
// 단축키를 사용하여 활성 기즈모를 변경합니다.
// 참고: 이러한 키 중 일부는 카메라 탐색 키와 충돌할 수 있으므로,
// 카메라가 현재 탐색 중이 아닌 경우에만 진행합니다.
if (RTCamera.get.navigationMode == ECameraNavigationMode.None)
// 마우스 왼쪽 버튼 클릭 시 객체 선택 처리
if (UnityEngine.Input.GetMouseButtonDown(0))
{
if (RTInput.get.KeyWentDown(Key.W)) // 이동 기즈모 활성화
// UI 위에서 클릭한 경우 무시
if (UnityEngine.EventSystems.EventSystem.current != null &&
UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject())
{
mMoveGizmo.enabled = true;
mRotateGizmo.enabled = false;
mScaleGizmo.enabled = false;
mMoveGizmo.objectTransformGizmo.Refresh();
}
else
if (RTInput.get.KeyWentDown(Key.E)) // 회전 기즈모 활성화
{
mMoveGizmo.enabled = false;
mRotateGizmo.enabled = true;
mScaleGizmo.enabled = false;
mRotateGizmo.objectTransformGizmo.Refresh();
}
else
if (RTInput.get.KeyWentDown(Key.R)) // 크기 조절 기즈모 활성화
{
mMoveGizmo.enabled = false;
mRotateGizmo.enabled = false;
mScaleGizmo.enabled = true;
mScaleGizmo.objectTransformGizmo.Refresh();
return;
}
// Ctrl 키가 눌려있으면 다중 선택
bool addToSelection = UnityEngine.Input.GetKey(KeyCode.LeftControl) ||
UnityEngine.Input.GetKey(KeyCode.RightControl);
selectionManager?.HandleMouseSelection(addToSelection);
}
}
private void SetupTopMenu()
{
if (topMenu == null)
@@ -140,30 +119,47 @@ namespace UVC.Studio
return;
}
// Setting에서 단축키 정보 가져오기
// Scene Injection이 실패할 경우 App 레벨에서 직접 가져오기
Setting currentSetting = setting;
if (currentSetting == null && InjectorAppContext.Instance != null)
{
currentSetting = InjectorAppContext.Instance.Get<Setting>();
}
MenuShortcuts menuShortcuts = currentSetting?.Data?.shortcuts?.menu;
if (menuShortcuts == null)
{
Debug.LogWarning("[SetupTopMenu] Setting 또는 shortcuts.menu가 null입니다. 단축키가 표시되지 않습니다.");
}
// File 메뉴
topMenu.AddMenuItem(new MenuItemData("file", "menu_file", subMenuItems: new List<MenuItemData>
{
new MenuItemData("file_new", "menu_file_new", new DebugLogCommand("새 파일 선택됨 (Command 실행)")),
new MenuItemData("file_open", "menu_file_open", new DebugLogCommand("파일 열기 선택됨 (Command 실행)")),
new MenuItemData("file_save", "menu_file_save", new DebugLogCommand("파일 저장 선택됨 (Command 실행)")),
new MenuItemData("file_saveas", "menu_file_save_as", new DebugLogCommand("다른 이름으로 저장 선택됨 (Command 실행)")),
new MenuItemData("file_insert_db", "menu_file_insert_db", new DebugLogCommand("데이터베이스 삽입 선택됨 (Command 실행)")),
new MenuItemData("file_new", "menu_file_new", new FileNewCommand(), shortcut: menuShortcuts?.newProject?.key),
new MenuItemData("file_open", "menu_file_open", new FileOpenCommand(), shortcut: menuShortcuts?.openProject?.key),
new MenuItemData("file_save", "menu_file_save", new FileSaveCommand(), shortcut: menuShortcuts?.saveProject?.key),
new MenuItemData("file_saveas", "menu_file_save_as", new FileSaveAsCommand(), shortcut: menuShortcuts?.saveAsProject?.key),
new MenuItemData("file_insert_db", "menu_file_insert_db", new FileInsertDbCommand(), shortcut: menuShortcuts?.insertDb?.key),
new MenuItemData("file_export", "menu_file_export", subMenuItems: new List<MenuItemData>
{
new MenuItemData("file_export_layout", "menu_file_export_layout", new DebugLogCommand("Layout Json 내보내기 선택됨 (Command 실행)")),
new MenuItemData("file_export_meta", "menu_file_export_meta", new DebugLogCommand("Meta Data Json 내보내기 선택됨 (Command 실행)")),
new MenuItemData("file_export_gltf", "menu_file_export_gltf", new DebugLogCommand("GLTF 내보내기 선택됨 (Command 실행)")),
new MenuItemData("file_export_layout", "menu_file_export_layout", new FileExportLayoutCommand(), shortcut: menuShortcuts?.exportLayout?.key),
new MenuItemData("file_export_meta", "menu_file_export_meta", new FileExportMetaCommand(), shortcut: menuShortcuts?.exportMeta?.key),
new MenuItemData("file_export_gltf", "menu_file_export_gltf", new FileExportGltfCommand(), shortcut: menuShortcuts?.exportGltf?.key),
}),
}));
// pool 로그
// Edit 메뉴
topMenu.AddMenuItem(new MenuItemData("edit", "menu_edit", subMenuItems: new List<MenuItemData>
{
new MenuItemData("edit_undo", "menu_edit_undo", new DebugLogCommand("되돌리기 선택됨 (Command 실행)")),
new MenuItemData("edit_redo", "menu_edit_redo", new DebugLogCommand("다시 실행 선택됨 (Command 실행)")),
new MenuItemData("edit_duplicate", "menu_edit_duplicate", new DebugLogCommand("복제 선택됨 (Command 실행)")),
new MenuItemData("edit_delete", "menu_edit_delete", new DebugLogCommand("삭제 선택됨 (Command 실행)")),
new MenuItemData("edit_create_plane", "menu_edit_create_plane", new DebugLogCommand("평면 생성 선택됨 (Command 실행)")),
new MenuItemData("edit_undo", "menu_edit_undo", new EditUndoCommand(), shortcut: menuShortcuts?.undo?.key),
new MenuItemData("edit_redo", "menu_edit_redo", new EditRedoCommand(), shortcut: menuShortcuts?.redo?.key),
new MenuItemData("edit_duplicate", "menu_edit_duplicate", new EditDuplicateCommand(), shortcut: menuShortcuts?.duplicate?.key),
new MenuItemData("edit_delete", "menu_edit_delete", new EditDeleteCommand(), shortcut: menuShortcuts?.delete?.key),
new MenuItemData("edit_create_plane", "menu_edit_create_plane", new EditCreatePlaneCommand(), shortcut: menuShortcuts?.createPlane?.key),
}));
// Setting 메뉴
topMenu.AddMenuItem(new MenuItemData("setting", "menu_setting", subMenuItems: new List<MenuItemData>
{
new MenuItemData("setting_db", "menu_setting_db", new SettingOpenCommand("shortcut:Database")),
@@ -171,10 +167,173 @@ namespace UVC.Studio
new MenuItemData("setting_library", "menu_setting_library", new SettingOpenCommand("shortcut:Library")),
new MenuItemData("setting_shortcut", "menu_setting_shortcut", new SettingOpenCommand("shortcut:Shortcut")),
}));
topMenu.Initialize();
}
/// <summary>
/// Setting에서 단축키가 변경된 후 TopMenu의 단축키를 갱신합니다.
/// </summary>
public void RefreshMenuShortcuts()
{
if (topMenu == null) return;
Setting currentSetting = setting;
if (currentSetting == null && InjectorAppContext.Instance != null)
{
currentSetting = InjectorAppContext.Instance.Get<Setting>();
}
MenuShortcuts menuShortcuts = currentSetting?.Data?.shortcuts?.menu;
if (menuShortcuts == null)
{
Debug.LogWarning("[RefreshMenuShortcuts] Setting 또는 shortcuts.menu가 null입니다.");
return;
}
// File 메뉴 단축키 업데이트
topMenu.SetMenuItemShortcut("file_new", menuShortcuts.newProject?.key);
topMenu.SetMenuItemShortcut("file_open", menuShortcuts.openProject?.key);
topMenu.SetMenuItemShortcut("file_save", menuShortcuts.saveProject?.key);
topMenu.SetMenuItemShortcut("file_saveas", menuShortcuts.saveAsProject?.key);
topMenu.SetMenuItemShortcut("file_insert_db", menuShortcuts.insertDb?.key);
topMenu.SetMenuItemShortcut("file_export_layout", menuShortcuts.exportLayout?.key);
topMenu.SetMenuItemShortcut("file_export_meta", menuShortcuts.exportMeta?.key);
topMenu.SetMenuItemShortcut("file_export_gltf", menuShortcuts.exportGltf?.key);
// Edit 메뉴 단축키 업데이트
topMenu.SetMenuItemShortcut("edit_undo", menuShortcuts.undo?.key);
topMenu.SetMenuItemShortcut("edit_redo", menuShortcuts.redo?.key);
topMenu.SetMenuItemShortcut("edit_duplicate", menuShortcuts.duplicate?.key);
topMenu.SetMenuItemShortcut("edit_delete", menuShortcuts.delete?.key);
topMenu.SetMenuItemShortcut("edit_create_plane", menuShortcuts.createPlane?.key);
// ShortcutManager 갱신
shortcutManager?.RefreshShortcuts();
Debug.Log("[RefreshMenuShortcuts] TopMenu 단축키가 갱신되었습니다.");
}
/// <summary>
/// 단축키 관리자를 설정합니다.
/// </summary>
private void SetupShortcutManager()
{
// StudioShortcutManager 생성 또는 가져오기
shortcutManager = StudioShortcutManager.Instance;
if (shortcutManager == null)
{
var go = new GameObject("StudioShortcutManager");
shortcutManager = go.AddComponent<StudioShortcutManager>();
DontDestroyOnLoad(go);
}
// Setting 주입
Setting currentSetting = setting;
if (currentSetting == null && InjectorAppContext.Instance != null)
{
currentSetting = InjectorAppContext.Instance.Get<Setting>();
}
if (currentSetting != null)
{
shortcutManager.SetSetting(currentSetting);
}
// 메뉴 단축키 등록
RegisterMenuShortcuts();
// 도구 단축키 등록
RegisterToolShortcuts();
Debug.Log("[SetupShortcutManager] 단축키 관리자가 설정되었습니다.");
}
/// <summary>
/// 메뉴 단축키를 등록합니다.
/// </summary>
private void RegisterMenuShortcuts()
{
if (shortcutManager == null) return;
// File 메뉴
shortcutManager.RegisterMenuShortcut("newProject", new FileNewCommand());
shortcutManager.RegisterMenuShortcut("openProject", new FileOpenCommand());
shortcutManager.RegisterMenuShortcut("saveProject", new FileSaveCommand());
shortcutManager.RegisterMenuShortcut("saveAsProject", new FileSaveAsCommand());
shortcutManager.RegisterMenuShortcut("insertDb", new FileInsertDbCommand());
shortcutManager.RegisterMenuShortcut("exportLayout", new FileExportLayoutCommand());
shortcutManager.RegisterMenuShortcut("exportMeta", new FileExportMetaCommand());
shortcutManager.RegisterMenuShortcut("exportGltf", new FileExportGltfCommand());
// Edit 메뉴
shortcutManager.RegisterMenuShortcut("undo", new EditUndoCommand());
shortcutManager.RegisterMenuShortcut("redo", new EditRedoCommand());
shortcutManager.RegisterMenuShortcut("duplicate", new EditDuplicateCommand());
shortcutManager.RegisterMenuShortcut("delete", new EditDeleteCommand());
shortcutManager.RegisterMenuShortcut("createPlane", new EditCreatePlaneCommand());
}
/// <summary>
/// 도구 단축키를 등록합니다.
/// </summary>
private void RegisterToolShortcuts()
{
if (shortcutManager == null) return;
// 도구 단축키 - SelectionManager를 통해 기즈모 제어
shortcutManager.RegisterToolShortcut("select", () =>
{
if (selectionManager != null) selectionManager.SetActiveTool(TransformToolType.Select);
if (toolBox != null) toolBox.SetRadioButtonSelection("SizeControlGroup", "Selection Tool");
});
shortcutManager.RegisterToolShortcut("move", () =>
{
if (selectionManager != null) selectionManager.SetActiveTool(TransformToolType.Move);
if (toolBox != null) toolBox.SetRadioButtonSelection("SizeControlGroup", "Movement Tool");
});
shortcutManager.RegisterToolShortcut("rotate", () =>
{
if (selectionManager != null) selectionManager.SetActiveTool(TransformToolType.Rotate);
if (toolBox != null) toolBox.SetRadioButtonSelection("SizeControlGroup", "Rotation Tool");
});
shortcutManager.RegisterToolShortcut("scale", () =>
{
if (selectionManager != null) selectionManager.SetActiveTool(TransformToolType.Scale);
if (toolBox != null) toolBox.SetRadioButtonSelection("SizeControlGroup", "Scale Tool");
});
shortcutManager.RegisterToolShortcut("snap", () =>
{
Debug.Log("[Shortcut] Snap Tool 토글");
});
shortcutManager.RegisterToolShortcut("guide", () =>
{
Debug.Log("[Shortcut] Guide Tool 토글");
});
shortcutManager.RegisterToolShortcut("node", () =>
{
if (toolBox != null) toolBox.SetRadioButtonSelection("NodeControlGroup", "Node Tool");
CursorManager.Instance.SetCursor(CursorType.Node);
});
shortcutManager.RegisterToolShortcut("link", () =>
{
if (toolBox != null) toolBox.SetRadioButtonSelection("NodeControlGroup", "Link Tool");
CursorManager.Instance.SetCursor(CursorType.Link);
});
shortcutManager.RegisterToolShortcut("arc", () =>
{
if (toolBox != null) toolBox.SetRadioButtonSelection("NodeControlGroup", "Arc Tool");
CursorManager.Instance.SetCursor(CursorType.Arc);
});
}
private void SetupToolBox()
{
// ToolbarModel 인스턴스 생성

View File

@@ -63,19 +63,29 @@ namespace UVC.Studio.Tab
/// <summary>
/// HierarchyWindow에서 아이템이 선택되었을 때 호출
/// SelectionManager를 통해 객체를 선택하고 Outlinable을 활성화
/// 자식 항목 선택 시 해당 Transform의 속성을 PropertyWindow에 표시
/// </summary>
private void OnItemSelectedHandler(TreeListItemData item)
{
Debug.Log($"[StudioSideTabBarHierarchy] Item selected: {item.Name} (ExternalKey: {item.ExternalKey})");
if (_isProcessingSelection) return;
if (_selectionManager == null || string.IsNullOrEmpty(item.ExternalKey)) return;
if (_selectionManager == null) return;
_isProcessingSelection = true;
try
{
// SelectionManager를 통해 객체 선택 (Outlinable 활성화됨)
_selectionManager.SelectById(item.ExternalKey);
// ExternalKey가 있으면 루트 StageObject 선택
if (!string.IsNullOrEmpty(item.ExternalKey))
{
// SelectionManager를 통해 객체 선택 (Outlinable 활성화됨)
_selectionManager.SelectById(item.ExternalKey);
}
// ExternalKey가 없으면 자식 항목 - Tag에서 Transform 가져와서 PropertyWindow에 표시
else if (item.Tag is Transform childTransform)
{
_selectionManager.DisplayChildTransformProperties(childTransform, item.Name);
}
}
finally
{
@@ -92,13 +102,22 @@ namespace UVC.Studio.Tab
Debug.Log($"[StudioSideTabBarHierarchy] Item deselected: {item.Name} (ExternalKey: {item.ExternalKey})");
if (_isProcessingSelection) return;
if (_selectionManager == null || string.IsNullOrEmpty(item.ExternalKey)) return;
if (_selectionManager == null) return;
_isProcessingSelection = true;
try
{
// SelectionManager를 통해 객체 선택 해제 (Outlinable 비활성화됨)
_selectionManager.DeselectById(item.ExternalKey);
// ExternalKey가 있으면 루트 StageObject 선택 해제
if (!string.IsNullOrEmpty(item.ExternalKey))
{
// SelectionManager를 통해 객체 선택 해제 (Outlinable 비활성화됨)
_selectionManager.DeselectById(item.ExternalKey);
}
// ExternalKey가 없으면 자식 항목 - 자식 Transform 표시 상태 초기화
else if (item.Tag is Transform)
{
_selectionManager.ClearChildTransformDisplay();
}
}
finally
{
@@ -254,6 +273,9 @@ namespace UVC.Studio.Tab
treeItem.ExternalKey = externalKey;
}
// Transform을 Tag에 저장 (자식 Transform 접근용)
treeItem.Tag = transform;
// Transform이 있으면 자식들을 재귀적으로 추가
if (transform != null)
{

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;
}
}
}