#if UNITY_EDITOR using System; using System.Diagnostics.CodeAnalysis; using System.IO; using UnityEditor; ////TODO: ensure that GUIDs in the asset are unique namespace UnityEngine.InputSystem.Editor { /// /// Keeps a reference to the asset being edited and maintains a copy of the asset object /// around for editing. /// [Serializable] internal class InputActionAssetManager : IDisposable { [SerializeField] private InputActionAsset m_AssetObjectForEditing; [SerializeField] private InputActionAsset m_ImportedAssetObject; [SerializeField] private string m_AssetGUID; [SerializeField] private string m_ImportedAssetJson; [SerializeField] private bool m_IsDirty; private SerializedObject m_SerializedObject; /// /// Returns the Asset GUID uniquely identifying the associated imported asset. /// public string guid => m_AssetGUID; /// /// Returns the current Asset Path for the associated imported asset. /// If the asset have been deleted this will be null. /// public string path { get { Debug.Assert(!string.IsNullOrEmpty(m_AssetGUID), "Asset GUID is empty"); return AssetDatabase.GUIDToAssetPath(m_AssetGUID); } } /// /// Returns the name of the associated imported asset. /// public string name { get { var asset = importedAsset; if (asset != null) return asset.name; if (!string.IsNullOrEmpty(path)) return Path.GetFileNameWithoutExtension(path); return string.Empty; } } private InputActionAsset importedAsset { get { // Note that this may be null after deserialization from domain reload if (m_ImportedAssetObject == null) LoadImportedObjectFromGuid(); return m_ImportedAssetObject; } } public InputActionAsset editedAsset => m_AssetObjectForEditing; // TODO Remove if redundant public Action onDirtyChanged { get; set; } public InputActionAssetManager(InputActionAsset inputActionAsset) { if (inputActionAsset == null) throw new NullReferenceException(nameof(inputActionAsset)); m_AssetGUID = EditorHelpers.GetAssetGUID(inputActionAsset); if (m_AssetGUID == null) throw new Exception($"Failed to get asset {inputActionAsset.name} GUID"); m_ImportedAssetObject = inputActionAsset; Initialize(); } public SerializedObject serializedObject => m_SerializedObject; public bool dirty => m_IsDirty; public bool Initialize() { if (m_AssetObjectForEditing == null) { if (importedAsset == null) { // The asset we want to edit no longer exists. return false; } CreateWorkingCopyAsset(); } else { m_SerializedObject = new SerializedObject(m_AssetObjectForEditing); } return true; } public void Dispose() { if (m_SerializedObject == null) return; m_SerializedObject?.Dispose(); m_SerializedObject = null; } public bool ReInitializeIfAssetHasChanged() { var json = importedAsset.ToJson(); if (m_ImportedAssetJson == json) return false; CreateWorkingCopyAsset(); return true; } public static InputActionAsset CreateWorkingCopy(InputActionAsset source) { var copy = Object.Instantiate(source); copy.hideFlags = HideFlags.HideAndDontSave; copy.name = source.name; return copy; } public static void CreateWorkingCopyAsset(ref InputActionAsset copy, InputActionAsset source) { if (copy != null) Cleanup(ref copy); copy = CreateWorkingCopy(source); } private void CreateWorkingCopyAsset() // TODO Can likely be removed if combined with Initialize { if (m_AssetObjectForEditing != null) Cleanup(); // Duplicate the asset along 1:1. Unlike calling Clone(), this will also preserve GUIDs. var asset = importedAsset; m_AssetObjectForEditing = CreateWorkingCopy(asset); m_ImportedAssetJson = asset.ToJson(); m_SerializedObject = new SerializedObject(m_AssetObjectForEditing); } public void Cleanup() { Cleanup(ref m_AssetObjectForEditing); } public static void Cleanup(ref InputActionAsset asset) { if (asset == null) return; Object.DestroyImmediate(asset); asset = null; } private void LoadImportedObjectFromGuid() { // https://fogbugz.unity3d.com/f/cases/1313185/ // InputActionEditorWindow being an EditorWindow, it will be saved as part of the editor's // window layout. When a project is opened that has no Library/ folder, the layout from the // most recently opened project is used. Which means that when opening an .inputactions // asset in project A, then closing it, and then opening project B, restoring the window layout // also tries to restore the InputActionEditorWindow having that very same asset open -- which // will lead nowhere except there happens to be an InputActionAsset with the very same GUID in // the project. var assetPath = path; if (!string.IsNullOrEmpty(assetPath)) m_ImportedAssetObject = AssetDatabase.LoadAssetAtPath(assetPath); } public void ApplyChanges() { m_SerializedObject.ApplyModifiedProperties(); m_SerializedObject.Update(); } internal void SaveChangesToAsset() { // If this is invoked after a domain reload, importAsset will resolve itself. // However, if the asset do not exist importedAsset will be null and we cannot complete the operation. if (importedAsset == null) throw new Exception("Unable to save changes. Associated asset does not exist."); SaveAsset(path, m_AssetObjectForEditing.ToJson()); SetDirty(false); } /// /// Saves an asset to the given assetPath with file content corresponding to assetJson /// if the current content of the asset given by assetPath is different or the asset do not exist. /// /// Destination asset path. /// The JSON file content to be written to the asset. /// true if the asset was successfully modified or created, else false. internal static bool SaveAsset(string assetPath, string assetJson) { var existingJson = File.Exists(assetPath) ? File.ReadAllText(assetPath) : string.Empty; // Return immediately if file content has not changed, i.e. touching the file would not yield a difference. if (assetJson == existingJson) return false; // Attempt to write asset to disc (including checkout the file) and inform the user if this fails. if (EditorHelpers.WriteAsset(assetPath, assetJson)) return true; Debug.LogError($"Unable save asset to \"{assetPath}\" since the asset-path could not be checked-out as editable in the underlying version-control system."); return false; } public void MarkDirty() { SetDirty(true); } public void UpdateAssetDirtyState() { m_SerializedObject.Update(); SetDirty(m_AssetObjectForEditing.ToJson() != importedAsset.ToJson()); // TODO Why not using cached version? } private void SetDirty(bool newValue) { m_IsDirty = newValue; if (onDirtyChanged != null) onDirtyChanged(newValue); } } } #endif // UNITY_EDITOR