250 lines
8.6 KiB
C#
250 lines
8.6 KiB
C#
|
#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
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// Keeps a reference to the asset being edited and maintains a copy of the asset object
|
||
|
/// around for editing.
|
||
|
/// </summary>
|
||
|
[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;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Returns the Asset GUID uniquely identifying the associated imported asset.
|
||
|
/// </summary>
|
||
|
public string guid => m_AssetGUID;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Returns the current Asset Path for the associated imported asset.
|
||
|
/// If the asset have been deleted this will be <c>null</c>.
|
||
|
/// </summary>
|
||
|
public string path
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
Debug.Assert(!string.IsNullOrEmpty(m_AssetGUID), "Asset GUID is empty");
|
||
|
return AssetDatabase.GUIDToAssetPath(m_AssetGUID);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Returns the name of the associated imported asset.
|
||
|
/// </summary>
|
||
|
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<bool> 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<InputActionAsset>(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);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Saves an asset to the given <c>assetPath</c> with file content corresponding to <c>assetJson</c>
|
||
|
/// if the current content of the asset given by <c>assetPath</c> is different or the asset do not exist.
|
||
|
/// </summary>
|
||
|
/// <param name="assetPath">Destination asset path.</param>
|
||
|
/// <param name="assetJson">The JSON file content to be written to the asset.</param>
|
||
|
/// <returns><c>true</c> if the asset was successfully modified or created, else <c>false</c>.</returns>
|
||
|
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
|