UI 자동화를 위해 바인딩 기능 구현
- 유니티 에셋 인증 오류로 meta 재생성
This commit is contained in:
@@ -0,0 +1,521 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Callbacks;
|
||||
using UnityEditor.ShortcutManagement;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEngine.InputSystem.Editor
|
||||
{
|
||||
internal class InputActionsEditorWindow : EditorWindow, IInputActionAssetEditor
|
||||
{
|
||||
// Register editor type via static constructor to enable asset monitoring
|
||||
static InputActionsEditorWindow()
|
||||
{
|
||||
InputActionAssetEditor.RegisterType<InputActionsEditorWindow>();
|
||||
}
|
||||
|
||||
static readonly Vector2 k_MinWindowSize = new Vector2(740, 450);
|
||||
// For UI testing purpose
|
||||
internal InputActionAsset currentAssetInEditor => m_AssetObjectForEditing;
|
||||
[SerializeField] private InputActionAsset m_AssetObjectForEditing;
|
||||
[SerializeField] private InputActionsEditorState m_State;
|
||||
[SerializeField] private string m_AssetGUID;
|
||||
|
||||
private string m_AssetJson;
|
||||
private bool m_IsDirty;
|
||||
|
||||
private StateContainer m_StateContainer;
|
||||
private InputActionsEditorView m_View;
|
||||
|
||||
private InputActionsEditorSessionAnalytic m_Analytics;
|
||||
|
||||
private InputActionsEditorSessionAnalytic analytics =>
|
||||
m_Analytics ??= new InputActionsEditorSessionAnalytic(
|
||||
InputActionsEditorSessionAnalytic.Data.Kind.EditorWindow);
|
||||
|
||||
// Unity 6.3 changed signature of OpenAsset, and now it accepts entity id instead of instance id.
|
||||
[OnOpenAsset]
|
||||
#if UNITY_6000_3_OR_NEWER
|
||||
public static bool OpenAsset(EntityId entityId, int line)
|
||||
{
|
||||
if (!InputActionImporter.IsInputActionAssetPath(AssetDatabase.GetAssetPath(entityId)))
|
||||
return false;
|
||||
|
||||
return OpenAsset(EditorUtility.EntityIdToObject(entityId));
|
||||
}
|
||||
|
||||
#else
|
||||
public static bool OpenAsset(int instanceId, int line)
|
||||
{
|
||||
if (!InputActionImporter.IsInputActionAssetPath(AssetDatabase.GetAssetPath(instanceId)))
|
||||
return false;
|
||||
|
||||
return OpenAsset(EditorUtility.InstanceIDToObject(instanceId));
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
private static bool OpenAsset(Object obj)
|
||||
{
|
||||
// Grab InputActionAsset.
|
||||
// NOTE: We defer checking out an asset until we save it. This allows a user to open an .inputactions asset and look at it
|
||||
// without forcing a checkout.
|
||||
var asset = obj as InputActionAsset;
|
||||
|
||||
string actionMapToSelect = null;
|
||||
string actionToSelect = null;
|
||||
|
||||
// Means we're dealing with an InputActionReference, e.g. when expanding the an .input action asset
|
||||
// on the Asset window and selecting an Action.
|
||||
if (asset == null)
|
||||
{
|
||||
var actionReference = obj as InputActionReference;
|
||||
if (actionReference != null && actionReference.asset != null)
|
||||
{
|
||||
asset = actionReference.asset;
|
||||
actionMapToSelect = actionReference.action.actionMap?.name;
|
||||
actionToSelect = actionReference.action?.name;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
OpenWindow(asset, actionMapToSelect, actionToSelect);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static InputActionsEditorWindow OpenWindow(InputActionAsset asset, string actionMapToSelect = null, string actionToSelect = null)
|
||||
{
|
||||
////REVIEW: It'd be great if the window got docked by default but the public EditorWindow API doesn't allow that
|
||||
//// to be done for windows that aren't singletons (GetWindow<T>() will only create one window and it's the
|
||||
//// only way to get programmatic docking with the current API).
|
||||
// See if we have an existing editor window that has the asset open.
|
||||
var existingWindow = InputActionAssetEditor.FindOpenEditor<InputActionsEditorWindow>(AssetDatabase.GetAssetPath(asset));
|
||||
if (existingWindow != null)
|
||||
{
|
||||
existingWindow.Focus();
|
||||
return existingWindow;
|
||||
}
|
||||
|
||||
var window = GetWindow<InputActionsEditorWindow>();
|
||||
if (window.m_IsDirty)
|
||||
{
|
||||
var assetPath = AssetDatabase.GUIDToAssetPath(window.m_AssetGUID);
|
||||
if (!string.IsNullOrEmpty(assetPath))
|
||||
{
|
||||
// Prompt user with a dialog
|
||||
var result = Dialog.InputActionAsset.ShowSaveChanges(assetPath);
|
||||
switch (result)
|
||||
{
|
||||
case Dialog.Result.Save:
|
||||
window.Save(isAutoSave: false);
|
||||
break;
|
||||
case Dialog.Result.Cancel:
|
||||
return window;
|
||||
case Dialog.Result.Discard:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(result));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.m_IsDirty = false;
|
||||
window.minSize = k_MinWindowSize;
|
||||
window.SetAsset(asset, actionToSelect, actionMapToSelect);
|
||||
window.Show();
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open the specified <paramref name="asset"/> in an editor window. Used when someone hits the "Edit Asset" button in the
|
||||
/// importer inspector.
|
||||
/// </summary>
|
||||
/// <param name="asset">The InputActionAsset to open.</param>
|
||||
/// <returns>The editor window.</returns>
|
||||
public static InputActionsEditorWindow OpenEditor(InputActionAsset asset)
|
||||
{
|
||||
return OpenWindow(asset, null, null);
|
||||
}
|
||||
|
||||
private static GUIContent GetEditorTitle(InputActionAsset asset, bool isDirty)
|
||||
{
|
||||
var text = asset.name + " (Input Actions Editor)";
|
||||
if (isDirty)
|
||||
text = "(*) " + text;
|
||||
return new GUIContent(text);
|
||||
}
|
||||
|
||||
private void SetAsset(InputActionAsset asset, string actionToSelect = null, string actionMapToSelect = null)
|
||||
{
|
||||
var existingWorkingCopy = m_AssetObjectForEditing;
|
||||
|
||||
try
|
||||
{
|
||||
// Obtain and persist GUID for the associated asset
|
||||
Debug.Assert(AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out m_AssetGUID, out long _),
|
||||
$"Failed to get asset {asset.name} GUID");
|
||||
|
||||
// Attempt to update editor and internals based on associated asset
|
||||
if (!TryUpdateFromAsset())
|
||||
return;
|
||||
|
||||
// Select the action that was selected on the Asset window.
|
||||
if (actionMapToSelect != null && actionToSelect != null)
|
||||
{
|
||||
m_State = m_State.SelectActionMap(actionMapToSelect);
|
||||
m_State = m_State.SelectAction(actionToSelect);
|
||||
}
|
||||
|
||||
BuildUI();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogException(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (existingWorkingCopy != null)
|
||||
DestroyImmediate(existingWorkingCopy);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateGUI() // Only domain reload
|
||||
{
|
||||
// When opening the window for the first time there will be no state or asset yet.
|
||||
// In that case, we don't do anything as SetAsset() will be called later and at that point the UI can be created.
|
||||
// Here we only recreate the UI e.g. after a domain reload.
|
||||
if (string.IsNullOrEmpty(m_AssetGUID))
|
||||
return;
|
||||
|
||||
// After domain reloads the state will be in a invalid state as some of the fields
|
||||
// cannot be serialized and will become null.
|
||||
// Therefore we recreate the state here using the fields which were saved.
|
||||
if (m_State.serializedObject == null)
|
||||
{
|
||||
InputActionAsset workingCopy = null;
|
||||
try
|
||||
{
|
||||
var assetPath = AssetDatabase.GUIDToAssetPath(m_AssetGUID);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<InputActionAsset>(assetPath);
|
||||
|
||||
if (asset == null)
|
||||
throw new Exception($"Failed to load asset \"{assetPath}\". The file may have been deleted or moved.");
|
||||
|
||||
m_AssetJson = InputActionsEditorWindowUtils.ToJsonWithoutName(asset);
|
||||
|
||||
if (m_AssetObjectForEditing == null)
|
||||
{
|
||||
workingCopy = InputActionAssetManager.CreateWorkingCopy(asset);
|
||||
if (m_State.m_Analytics == null)
|
||||
m_State.m_Analytics = analytics;
|
||||
m_State = new InputActionsEditorState(m_State, new SerializedObject(workingCopy));
|
||||
m_AssetObjectForEditing = workingCopy;
|
||||
}
|
||||
else
|
||||
m_State = new InputActionsEditorState(m_State, new SerializedObject(m_AssetObjectForEditing));
|
||||
m_IsDirty = HasContentChanged();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogException(e);
|
||||
if (workingCopy != null)
|
||||
DestroyImmediate(workingCopy);
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
BuildUI();
|
||||
}
|
||||
|
||||
private void CleanupStateContainer()
|
||||
{
|
||||
if (m_StateContainer != null)
|
||||
{
|
||||
m_StateContainer.StateChanged -= OnStateChanged;
|
||||
m_StateContainer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildUI()
|
||||
{
|
||||
CleanupStateContainer();
|
||||
|
||||
if (m_State.m_Analytics == null)
|
||||
m_State.m_Analytics = m_Analytics;
|
||||
|
||||
m_StateContainer = new StateContainer(m_State, m_AssetGUID);
|
||||
m_StateContainer.StateChanged += OnStateChanged;
|
||||
|
||||
rootVisualElement.Clear();
|
||||
if (!rootVisualElement.styleSheets.Contains(InputActionsEditorWindowUtils.theme))
|
||||
rootVisualElement.styleSheets.Add(InputActionsEditorWindowUtils.theme);
|
||||
m_View = new InputActionsEditorView(rootVisualElement, m_StateContainer, false, () => Save(isAutoSave: false));
|
||||
|
||||
m_StateContainer.Initialize(rootVisualElement.Q("action-editor"));
|
||||
}
|
||||
|
||||
private void OnStateChanged(InputActionsEditorState newState, UIRebuildMode editorRebuildMode)
|
||||
{
|
||||
DirtyInputActionsEditorWindow(newState);
|
||||
m_State = newState;
|
||||
}
|
||||
|
||||
private void UpdateWindowTitle()
|
||||
{
|
||||
titleContent = GetEditorTitle(GetEditedAsset(), m_IsDirty);
|
||||
}
|
||||
|
||||
private InputActionAsset GetEditedAsset()
|
||||
{
|
||||
return m_State.serializedObject.targetObject as InputActionAsset;
|
||||
}
|
||||
|
||||
private void Save(bool isAutoSave)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(m_AssetGUID);
|
||||
|
||||
var projectWideActions = InputSystem.actions;
|
||||
if (projectWideActions != null && path == AssetDatabase.GetAssetPath(projectWideActions))
|
||||
ProjectWideActionsAsset.Verify(GetEditedAsset());
|
||||
|
||||
if (InputActionAssetManager.SaveAsset(path, GetEditedAsset().ToJson()))
|
||||
TryUpdateFromAsset();
|
||||
|
||||
if (isAutoSave)
|
||||
analytics.RegisterAutoSave();
|
||||
else
|
||||
analytics.RegisterExplicitSave();
|
||||
}
|
||||
|
||||
private bool HasContentChanged()
|
||||
{
|
||||
var editedAsset = GetEditedAsset();
|
||||
var editedAssetJson = InputActionsEditorWindowUtils.ToJsonWithoutName(editedAsset);
|
||||
return editedAssetJson != m_AssetJson;
|
||||
}
|
||||
|
||||
private void DirtyInputActionsEditorWindow(InputActionsEditorState newState)
|
||||
{
|
||||
var isWindowDirty = HasContentChanged();
|
||||
|
||||
if (m_IsDirty == isWindowDirty)
|
||||
return;
|
||||
|
||||
m_IsDirty = isWindowDirty;
|
||||
UpdateWindowTitle();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
analytics.Begin();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
analytics.End();
|
||||
}
|
||||
|
||||
private void OnFocus()
|
||||
{
|
||||
analytics.RegisterEditorFocusIn();
|
||||
}
|
||||
|
||||
private void OnLostFocus()
|
||||
{
|
||||
if (InputEditorUserSettings.autoSaveInputActionAssets && m_IsDirty)
|
||||
{
|
||||
// We'd like to avoid saving in case the focus was lost due to the drop-down window being spawned.
|
||||
// This code should be cleaned up once we migrate the InputControl stuff from ImGUI completely.
|
||||
// Since at that point it stops being a separate window that steals focus.
|
||||
// (See case ISXB-1221)
|
||||
if (!InputControlPathEditor.IsShowingDropdown)
|
||||
Save(isAutoSave: true);
|
||||
}
|
||||
|
||||
analytics.RegisterEditorFocusOut();
|
||||
}
|
||||
|
||||
private void HandleOnDestroy()
|
||||
{
|
||||
// Do we have unsaved changes that we need to ask the user to save or discard?
|
||||
if (!m_IsDirty)
|
||||
return;
|
||||
|
||||
// Get target asset path from GUID, if this fails file no longer exists and we need to abort.
|
||||
var assetPath = AssetDatabase.GUIDToAssetPath(m_AssetGUID);
|
||||
if (string.IsNullOrEmpty(assetPath))
|
||||
return;
|
||||
|
||||
// Prompt user with a dialog
|
||||
var result = Dialog.InputActionAsset.ShowSaveChanges(assetPath);
|
||||
switch (result)
|
||||
{
|
||||
case Dialog.Result.Save:
|
||||
Save(isAutoSave: false);
|
||||
break;
|
||||
case Dialog.Result.Cancel:
|
||||
// Cancel editor quit. (open new editor window with the edited asset)
|
||||
ReshowEditorWindowWithUnsavedChanges();
|
||||
break;
|
||||
case Dialog.Result.Discard:
|
||||
// Don't save, quit - reload the old asset from the json to prevent the asset from being dirtied
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(result));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
HandleOnDestroy();
|
||||
|
||||
// Clean-up
|
||||
CleanupStateContainer();
|
||||
if (m_AssetObjectForEditing != null)
|
||||
DestroyImmediate(m_AssetObjectForEditing);
|
||||
|
||||
m_View?.DestroyView();
|
||||
}
|
||||
|
||||
private void ReshowEditorWindowWithUnsavedChanges()
|
||||
{
|
||||
var window = CreateWindow<InputActionsEditorWindow>();
|
||||
|
||||
// Move/transfer ownership of m_AssetObjectForEditing to new window
|
||||
window.m_AssetObjectForEditing = m_AssetObjectForEditing;
|
||||
m_AssetObjectForEditing = null;
|
||||
|
||||
// Move/transfer ownership of m_State to new window (struct)
|
||||
window.m_State = m_State;
|
||||
m_State = new InputActionsEditorState();
|
||||
|
||||
// Just copy trivial arguments
|
||||
window.m_AssetGUID = m_AssetGUID;
|
||||
window.m_AssetJson = m_AssetJson;
|
||||
window.m_IsDirty = m_IsDirty;
|
||||
|
||||
// Note that view and state container will get destroyed with this window instance
|
||||
// and recreated for this window below
|
||||
window.BuildUI();
|
||||
window.Show();
|
||||
|
||||
// Make sure window title is up to date
|
||||
window.UpdateWindowTitle();
|
||||
}
|
||||
|
||||
private bool TryUpdateFromAsset()
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(m_AssetGUID), "Asset GUID is empty");
|
||||
var assetPath = AssetDatabase.GUIDToAssetPath(m_AssetGUID);
|
||||
if (assetPath == null)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"Failed to open InputActionAsset with GUID {m_AssetGUID}. The asset might have been deleted.");
|
||||
return false;
|
||||
}
|
||||
|
||||
InputActionAsset workingCopy = null;
|
||||
try
|
||||
{
|
||||
var asset = AssetDatabase.LoadAssetAtPath<InputActionAsset>(assetPath);
|
||||
workingCopy = InputActionAssetManager.CreateWorkingCopy(asset);
|
||||
m_AssetJson = InputActionsEditorWindowUtils.ToJsonWithoutName(asset);
|
||||
m_State = new InputActionsEditorState(m_State, new SerializedObject(workingCopy));
|
||||
m_IsDirty = false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (workingCopy != null)
|
||||
DestroyImmediate(workingCopy);
|
||||
Debug.LogException(e);
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
m_AssetObjectForEditing = workingCopy;
|
||||
UpdateWindowTitle();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#region IInputActionEditorWindow
|
||||
|
||||
public string assetGUID => m_AssetGUID;
|
||||
public bool isDirty => m_IsDirty;
|
||||
|
||||
public void OnAssetMoved()
|
||||
{
|
||||
// When an asset is moved, we only need to update window title since content is unchanged
|
||||
UpdateWindowTitle();
|
||||
}
|
||||
|
||||
public void OnAssetDeleted()
|
||||
{
|
||||
// When associated asset is deleted on disk, just close the editor, but also mark the editor
|
||||
// as not being dirty to avoid prompting the user to save changes.
|
||||
m_IsDirty = false;
|
||||
Close();
|
||||
}
|
||||
|
||||
public void OnAssetImported()
|
||||
{
|
||||
// If the editor has pending changes done by the user and the contents changes on disc, there
|
||||
// is not much we can do about it but to ignore loading the changes. If the editors asset is
|
||||
// unmodified, we can refresh the editor with the latest content from disc.
|
||||
if (m_IsDirty)
|
||||
return;
|
||||
|
||||
// If our asset has disappeared from disk, just close the window.
|
||||
var assetPath = AssetDatabase.GUIDToAssetPath(assetGUID);
|
||||
if (string.IsNullOrEmpty(assetPath))
|
||||
{
|
||||
m_IsDirty = false; // Avoid checks
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
SetAsset(AssetDatabase.LoadAssetAtPath<InputActionAsset>(assetPath));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Shortcuts
|
||||
[Shortcut("Input Action Editor/Save", typeof(InputActionsEditorWindow), KeyCode.S, ShortcutModifiers.Action)]
|
||||
private static void SaveShortcut(ShortcutArguments arguments)
|
||||
{
|
||||
var window = (InputActionsEditorWindow)arguments.context;
|
||||
window.Save(isAutoSave: false);
|
||||
}
|
||||
|
||||
[Shortcut("Input Action Editor/Add Action Map", typeof(InputActionsEditorWindow), KeyCode.M, ShortcutModifiers.Alt)]
|
||||
private static void AddActionMapShortcut(ShortcutArguments arguments)
|
||||
{
|
||||
var window = (InputActionsEditorWindow)arguments.context;
|
||||
window.m_StateContainer.Dispatch(Commands.AddActionMap());
|
||||
}
|
||||
|
||||
[Shortcut("Input Action Editor/Add Action", typeof(InputActionsEditorWindow), KeyCode.A, ShortcutModifiers.Alt)]
|
||||
private static void AddActionShortcut(ShortcutArguments arguments)
|
||||
{
|
||||
var window = (InputActionsEditorWindow)arguments.context;
|
||||
window.m_StateContainer.Dispatch(Commands.AddAction());
|
||||
}
|
||||
|
||||
[Shortcut("Input Action Editor/Add Binding", typeof(InputActionsEditorWindow), KeyCode.B, ShortcutModifiers.Alt)]
|
||||
private static void AddBindingShortcut(ShortcutArguments arguments)
|
||||
{
|
||||
var window = (InputActionsEditorWindow)arguments.context;
|
||||
window.m_StateContainer.Dispatch(Commands.AddBinding());
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user