diff --git a/Assets/Tests/InputSystem/CoreTests_Actions.cs b/Assets/Tests/InputSystem/CoreTests_Actions.cs index aa84d64f0b..b33cae8f10 100644 --- a/Assets/Tests/InputSystem/CoreTests_Actions.cs +++ b/Assets/Tests/InputSystem/CoreTests_Actions.cs @@ -9307,7 +9307,6 @@ public void Actions_CanApplyOverrideToActionWithEmptyBinding() action.Disable(); action.ApplyBindingOverride(0, "/gamepad/leftTrigger"); action.Enable(); - Press(gamepad.leftTrigger); Assert.That(performed); @@ -9550,6 +9549,36 @@ public void Actions_CanResolveActionReference() Assert.That(referencedAction, Is.SameAs(action2)); } + + [Test] + [Category("Actions")] + public void Actions_CanResolveActionReference_WhenUsingToInputActionToConstructANewReference() + { + var map = new InputActionMap("map"); + map.AddAction("action1"); + var action2 = map.AddAction("action2"); + var asset = ScriptableObject.CreateInstance(); + asset.AddActionMap(map); + + var reference = ScriptableObject.CreateInstance(); + reference.Set(asset, "map", "action2"); + + var copy1 = InputActionReference.Create(reference.action); + var copy2 = InputActionReference.Create(reference.ToInputAction()); + + // Expecting action to be the same + Assert.That(reference.action, Is.SameAs(copy1.action)); + Assert.That(reference.action, Is.SameAs(copy2.action)); + } + + [Test] + [Category("Actions")] + public void Actions_CanImplicitlyConvertReferenceToAction_WhenAssigningActionFromReference() + { + var reference = ScriptableObject.CreateInstance(); + InputAction action = reference; // implicit conversion + Assert.That(reference.action, Is.Null); + } [Test] [Category("Actions")] diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionAsset.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionAsset.cs index 5c54b13bcd..ab32a889ed 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionAsset.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionAsset.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using UnityEngine.InputSystem.Utilities; ////TODO: make the FindAction logic available on any IEnumerable and IInputActionCollection via extension methods @@ -924,11 +923,41 @@ private void OnDestroy() } } + #if UNITY_EDITOR + + private void OnValidate() + { + // Only currently validates references if serialized property has been set to true + if (m_ValidateReferencesInEditMode) + Editor.InputActionReferenceValidator.ValidateReferences(this); + } + + #endif + + internal InputAction FindActionById(string actionId) + { + if (m_ActionMaps == null) + return null; + + foreach (var t in m_ActionMaps) + { + var action = t.FindAction(actionId); + if (action != null) + return action; + } + + return null; + } + ////TODO: ApplyBindingOverrides, RemoveBindingOverrides, RemoveAllBindingOverrides [SerializeField] internal InputActionMap[] m_ActionMaps; [SerializeField] internal InputControlScheme[] m_ControlSchemes; + // Note: not serialized to JSON only as asset objects + [NonSerialized] internal InputActionReference[] m_References; // TODO Tentative + [SerializeField] internal bool m_ValidateReferencesInEditMode;// TODO Tentative + ////TODO: make this persistent across domain reloads /// /// Shared state for all action maps in the asset. diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionReference.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionReference.cs index 0090f46bb8..2bc2fc1b9b 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionReference.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionReference.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using UnityEditor; ////REVIEW: Can we somehow make this a simple struct? The one problem we have is that we can't put struct instances as sub-assets into //// the import (i.e. InputActionImporter can't do AddObjectToAsset with them). However, maybe there's a way around that. The thing @@ -160,6 +161,29 @@ public override string ToString() return base.ToString(); } +#if UNITY_EDITOR + + private void OnEnable() + { + // This is invoked after InputActionReference deserialization + if (m_Action == null && m_Asset != null) + { + m_Action = m_Asset.FindActionById(m_ActionId); + Invalidate(); + } + } + + internal void Invalidate() + { + // TODO Check if it makes a difference to do full re-evaluation here (only to see if reference invalidation for asset is broken) + + // Reflect action name as the name of this SerializableObject + if (m_Action != null) + name = GetDisplayName(m_Action); + } + +#endif // #if UNITY_EDITOR + private static string GetDisplayName(InputAction action) { return !string.IsNullOrEmpty(action?.actionMap?.name) ? $"{action.actionMap?.name}/{action.name}" : action?.name; diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionAssetIconProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionAssetIconProvider.cs new file mode 100644 index 0000000000..d953dc7817 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionAssetIconProvider.cs @@ -0,0 +1,35 @@ +#if UNITY_EDITOR +using UnityEditor; + +namespace UnityEngine.InputSystem.Editor +{ + /// + /// Provides access to icons associated with InputActionAsset. + /// + internal static class InputActionAssetIconProvider + { + private const string kActionIcon = "Packages/com.unity.inputsystem/InputSystem/Editor/Icons/InputAction.png"; + private const string kAssetIcon = "Packages/com.unity.inputsystem/InputSystem/Editor/Icons/InputActionAsset.png"; + + /// + /// Attempts to load the icon associated with an InputActionAsset (.inputactions) asset. + /// + /// Icon resource reference or null if the resource could not be loaded. + public static Texture2D LoadAssetIcon() + { + return (Texture2D)EditorGUIUtility.Load(kAssetIcon); + } + + /// + /// Attempts to load the icon associated with an InputActionReference sub-asset of an + /// InputActionAsset (.inputactions) asset. + /// + /// Icon resource reference or null if the resource could not be loaded. + public static Texture2D LoadActionIcon() + { + return (Texture2D)EditorGUIUtility.Load(kActionIcon); + } + } +} + +#endif // #if UNITY_EDITOR diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionAssetIconProvider.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionAssetIconProvider.cs.meta new file mode 100644 index 0000000000..3f68a790e1 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionAssetIconProvider.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 310c27cc89b74e5291ce1fcb0a9d793d +timeCreated: 1696829469 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporter.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporter.cs index 5f45b6f02b..7863b1edf4 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporter.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporter.cs @@ -1,5 +1,6 @@ #if UNITY_EDITOR using System; +using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; @@ -29,9 +30,6 @@ internal class InputActionImporter : ScriptedImporter { private const int kVersion = 13; - private const string kActionIcon = "Packages/com.unity.inputsystem/InputSystem/Editor/Icons/InputAction.png"; - private const string kAssetIcon = "Packages/com.unity.inputsystem/InputSystem/Editor/Icons/InputActionAsset.png"; - [SerializeField] private bool m_GenerateWrapperCode; [SerializeField] private string m_WrapperCodePath; [SerializeField] private string m_WrapperClassName; @@ -88,8 +86,8 @@ public override void OnImportAsset(AssetImportContext ctx) // Load icons. ////REVIEW: the icons won't change if the user changes skin; not sure it makes sense to differentiate here - var assetIcon = (Texture2D)EditorGUIUtility.Load(kAssetIcon); - var actionIcon = (Texture2D)EditorGUIUtility.Load(kActionIcon); + var assetIcon = InputActionAssetIconProvider.LoadAssetIcon(); + var actionIcon = InputActionAssetIconProvider.LoadActionIcon(); // Add asset. ctx.AddObjectToAsset("", asset, assetIcon); @@ -212,12 +210,50 @@ public override void OnImportAsset(AssetImportContext ctx) InputActionEditorWindow.RefreshAllOnAssetReimport(); } + internal static IEnumerable LoadInputActionReferencesFromAsset(InputActionAsset asset) + { + //Get all InputActionReferences are stored at the same asset path as InputActionAsset + return AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(asset)).Where( + o => o is InputActionReference).Cast(); + } + + internal static IEnumerable LoadInputActionReferencesFromAssetDatabase(string[] folderPath = null) + { + string[] searchInFolderPath = null; + // If folderPath is null, search in "Assets" folder. + if (folderPath == null) + { + searchInFolderPath = new string[] { "Assets" }; + } + + // Get all InputActionReference from assets in "Asset" folder. It does not search inside "Packages" folder. + var inputActionReferenceGUIDs = AssetDatabase.FindAssets($"t:{typeof(InputActionReference).Name}", searchInFolderPath); + + // To find all the InputActionReferences, the GUID of the asset containing at least one action reference is + // used to find the asset path. This is because InputActionReferences are stored in the asset database as sub-assets of InputActionAsset. + // Then the whole asset is loaded and all the InputActionReferences are extracted from it. + // Also, the action references are duplicated to have backwards compatibility with the 1.0.0-preview.7. That + // is why we look for references withouth the `HideFlags.HideInHierarchy` flag. + var inputActionReferencesList = new List(); + foreach (var guid in inputActionReferenceGUIDs) + { + var assetName = AssetDatabase.GUIDToAssetPath(guid); + var assetInputActionReferenceList = AssetDatabase.LoadAllAssetsAtPath(assetName).Where( + o => o is InputActionReference && + !((InputActionReference)o).hideFlags.HasFlag(HideFlags.HideInHierarchy)) + .Cast().ToList(); + + inputActionReferencesList.AddRange(assetInputActionReferenceList); + } + return inputActionReferencesList; + } + // Add item to plop an .inputactions asset into the project. [MenuItem("Assets/Create/Input Actions")] public static void CreateInputAsset() { ProjectWindowUtil.CreateAssetWithContent("New Controls." + InputActionAsset.Extension, - InputActionAsset.kDefaultAssetLayoutJson, (Texture2D)EditorGUIUtility.Load(kAssetIcon)); + InputActionAsset.kDefaultAssetLayoutJson, InputActionAssetIconProvider.LoadAssetIcon()); } } } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/InputActionReferenceValidator.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/InputActionReferenceValidator.cs new file mode 100644 index 0000000000..2b49d909a6 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/InputActionReferenceValidator.cs @@ -0,0 +1,185 @@ +#if UNITY_EDITOR + +using System.Linq; +using UnityEditor; +using UnityEngine.InputSystem.Utilities; + +namespace UnityEngine.InputSystem.Editor +{ + interface IInputActionAssetReferenceValidator + { + void OnInvalidate(InputActionAsset asset, InputActionReference reference); + void OnInvalidReference(InputActionAsset asset, InputActionReference reference); + void OnDuplicateReference(InputActionAsset asset, InputActionReference reference); + void OnMissingReference(InputActionAsset asset, InputAction action); + } + + internal class DefaultInputActionAssetReferenceValidator : IInputActionAssetReferenceValidator + { + public void OnInvalidate(InputActionAsset asset, InputActionReference reference) + { + reference.Invalidate(); + } + + public void OnInvalidReference(InputActionAsset asset, InputActionReference reference) + { + AssetDatabase.RemoveObjectFromAsset(reference); + Undo.DestroyObjectImmediate(reference); // Enable undo + } + + public void OnDuplicateReference(InputActionAsset asset, InputActionReference reference) + { + OnInvalidReference(asset, reference); // Same action, delete it + } + + public void OnMissingReference(InputActionAsset asset, InputAction action) + { + var reference = InputActionReference.Create(action); + AssetDatabase.AddObjectToAsset(reference, asset); + } + } + + internal class LoggingInputActionAssetReferenceValidator : IInputActionAssetReferenceValidator + { + public void OnInvalidate(InputActionAsset asset, InputActionReference reference) + { + Debug.Log($"OnInvalidate(asset: {asset}, reference:{reference})"); + } + + public void OnInvalidReference(InputActionAsset asset, InputActionReference reference) + { + Debug.Log($"OnInvalidReference(asset: {asset}, reference:{reference})"); + } + + public void OnDuplicateReference(InputActionAsset asset, InputActionReference reference) + { + Debug.Log($"OnDuplicateReference(asset: {asset}, reference:{reference})"); + } + + public void OnMissingReference(InputActionAsset asset, InputAction action) + { + Debug.Log($"OnMissingReference(asset: {asset}, action:{action})"); + } + } + + // TODO Known issues: + // This doesn't work correctly, doing bulk updates like this works well only up to the point the user + // engages with the undo system. Scenario: Reference an action in the Inspector. Delete action in editor, + // notice that inspector changes to "Missing (Input Action Reference)" as object is destroyed. + // Then if user undo the operation there are two issues: + // - Editor is restored to an object not referencing an existing editor object. + // - The deletion in editor and inspector re-resolve is considered two different undo steps, they need + // to be an atomic step. + // + // TODO Instead we likely need to integrate all reference management into each step of the editor. + internal class InputActionReferenceValidator + { + public static void ValidateReferences(InputActionAsset asset) + { + if (asset == null) + return; + + //Debug.Log("ValidateReferences " + asset); + + void RemoveReferenceAtIndex(InputActionReference[] references, ref int count, int index) + { + var reference = references[index]; + ArrayHelpers.EraseAtByMovingTail(references, ref count, index); + AssetDatabase.RemoveObjectFromAsset(reference); + Undo.DestroyObjectImmediate(reference); // Enable undo + } + + // Fetch input action references (Note that this will allocate an array) + var references = InputActionImporter.LoadInputActionReferencesFromAsset(asset).ToArray(); + + // Remove dangling references + var initialCount = references.Length; + var count = initialCount; + for (var i = count - 1; i >= 0; --i) + { + // TODO Below comparison want work,. maybe ReferenceEquals works or find another way + // If invalid (no asset, no action ID or not found within this asset) - remove the reference + /*if (!references[i].m_Asset != asset) + { + Debug.Log("Removing reference with invalid asset reference"); + RemoveReferenceAtIndex(references, ref count, i); + continue; + }*/ + + var referencedAction = asset.FindActionById(references[i].m_ActionId); + if (referencedAction == null) + { + Debug.Log("Removing invalid or dangling InputActionReference: " + references[i]); + RemoveReferenceAtIndex(references, ref count, i); + } + else // Reference is associated with an action of this asset as expected + { + // Look for first duplicate reference referencing the same action and eliminate this reference + // if duplicates exist (Should not happen, basically corrupt asset). Additional duplicates are + // covered since this is evaluated for each element. + for (var j = i - 1; j >= 0; --j) + { + if (ReferenceEquals(references[j].m_Asset, references[i].m_Asset) && + references[j].m_ActionId == references[i].m_ActionId) + { + Debug.Log("Removing duplicate InputActionReference: " + references[i]); + RemoveReferenceAtIndex(references, ref count, i); + break; + } + } + } + } + + // Handle added or removed actions + foreach (var action in asset) + { + var referenceIndex = references.IndexOf(r => r.m_ActionId == action.m_Id, 0, count); + if (referenceIndex >= 0) + { + // Action has exactly one reference as expected, invalidate name if action has changed name + var reference = references[referenceIndex]; + //SerializedObject obj = new SerializedObject(reference); + //var assetProperty = obj.FindProperty(nameof(InputActionReference.m_Asset)); + //var actionIdProperty = obj.FindProperty(nameof(InputActionReference.m_ActionId)); + //var action = obj.FindProperty(nameof(InputActionReference.m_Ac)) + reference.Set(action); // Invalidate() + } + else + { + // Action is missing a reference so we add it + var reference = InputActionReference.Create(action); + ArrayHelpers.Append(ref references, reference); + AssetDatabase.AddObjectToAsset(reference, asset); + Debug.Log("Added missing action reference: " + reference); + } + } + + asset.m_References = references; + } + + // TODO Code below this point is targeted at debugging and should be removed before merge + + [MenuItem("Test/Log AssetPath References")] + public static void Dump() + { + var assets = AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(ProjectWideActionsAsset.GetOrCreate())); + foreach (var asset in assets) + { + var reference = asset as InputActionReference; + if (reference != null) + Debug.Log(reference); + } + } + + [MenuItem("Test/Log InputActionAsset References")] + public static void Dump2() + { + foreach (var reference in ProjectWideActionsAsset.GetOrCreate().m_References) + { + Debug.Log(reference); + } + } + } +} + +#endif // #if UNITY_EDITOR diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/InputActionReferenceValidator.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/InputActionReferenceValidator.cs.meta new file mode 100644 index 0000000000..95c97e3b57 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Internal/InputActionReferenceValidator.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ddac3ccb045745ed9e89e0772832e82f +timeCreated: 1697620704 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs index 68146646f1..54ef227e34 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs @@ -10,18 +10,18 @@ namespace UnityEngine.InputSystem.Editor { internal static class ProjectWideActionsAsset { - internal const string kDefaultAssetPath = "Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsTemplate.json"; - internal const string kAssetPath = "ProjectSettings/InputManager.asset"; - internal const string kAssetName = InputSystem.kProjectWideActionsAssetName; - - static string s_DefaultAssetPath = kDefaultAssetPath; - static string s_AssetPath = kAssetPath; + private const string kDefaultAssetPath = "Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsTemplate.json"; + private const string kAssetPath = "ProjectSettings/InputManager.asset"; + private const string kAssetName = InputSystem.kProjectWideActionsAssetName; #if UNITY_INCLUDE_TESTS - internal static void SetAssetPaths(string defaultAssetPath, string assetPath) + private static string s_DefaultAssetPath = kDefaultAssetPath; + private static string s_AssetPath = kAssetPath; + + internal static void SetAssetPaths(string defaultAssetPathOverride, string assetPathOverride) { - s_DefaultAssetPath = defaultAssetPath; - s_AssetPath = assetPath; + s_DefaultAssetPath = defaultAssetPathOverride; + s_AssetPath = assetPathOverride; } internal static void Reset() @@ -30,6 +30,11 @@ internal static void Reset() s_AssetPath = kAssetPath; } + internal static string defaultAssetPath => s_DefaultAssetPath; + internal static string assetPath => s_AssetPath; +#else + internal static string defaultAssetPath => kDefaultAssetPath; + internal static string assetPath => kAssetPath; #endif [InitializeOnLoadMethod] @@ -38,26 +43,36 @@ internal static void InstallProjectWideActions() GetOrCreate(); } + internal static bool IsProjectWideActionsAsset(Object obj) + { + return IsProjectWideActionsAsset(obj as InputActionAsset); + } + + internal static bool IsProjectWideActionsAsset(InputActionAsset asset) + { + if (ReferenceEquals(asset, null)) + return false; + return kAssetName.Equals(asset.name); + } + internal static InputActionAsset GetOrCreate() { var objects = AssetDatabase.LoadAllAssetsAtPath(s_AssetPath); - if (objects != null) - { - var inputActionsAsset = objects.FirstOrDefault(o => o != null && o.name == kAssetName) as InputActionAsset; - if (inputActionsAsset != null) - return inputActionsAsset; - } - - return CreateNewActionAsset(); + var inputActionsAsset = (InputActionAsset)objects?.FirstOrDefault(IsProjectWideActionsAsset); + if (ReferenceEquals(inputActionsAsset, null)) + return CreateNewActionAsset(); + return inputActionsAsset; } private static InputActionAsset CreateNewActionAsset() { + // Read JSON file content representing a serialized version of the InputActionAsset var json = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, s_DefaultAssetPath)); var asset = ScriptableObject.CreateInstance(); asset.LoadFromJson(json); asset.name = kAssetName; + asset.m_ValidateReferencesInEditMode = true; AssetDatabase.AddObjectToAsset(asset, s_AssetPath); @@ -88,16 +103,26 @@ private static InputActionAsset CreateNewActionAsset() // Create sub-asset for each action. This is so that users can select individual input actions from the asset when they're // trying to assign to a field that accepts only one action. + var index = 0; + var inputActionReferences = new InputActionReference[asset.Count()]; foreach (var map in maps) { foreach (var action in map.actions) { - var actionReference = ScriptableObject.CreateInstance(); - actionReference.Set(action); + var actionReference = InputActionReference.Create(action); AssetDatabase.AddObjectToAsset(actionReference, asset); + + // Keep track of associated references to avoid creating new ones when queried by e.g. pickers + // (not a big deal) but also to provide object persistence. If we would not create these we could + // instead let all references serialize at user-side, but that would also require us to anyway + // keep a registry of them in InputActionAsset in order to validate them. + //asset.m_References.Add(actionReference); + inputActionReferences[index++] = actionReference; } } + asset.m_References = inputActionReferences; + AssetDatabase.SaveAssets(); return asset; diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/AssetSearchProviders.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/AssetSearchProviders.cs new file mode 100644 index 0000000000..18e3c84594 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/AssetSearchProviders.cs @@ -0,0 +1,95 @@ +#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor.Search; + +namespace UnityEngine.InputSystem.Editor +{ + internal static class SearchConstants + { + // SearchFlags : these flags are used to customize how search is performed and how search + // results are displayed. + // Note that SearchFlags.Packages is not currently used and hides all results from packages. + internal static readonly SearchFlags PickerSearchFlags = SearchFlags.Sorted | SearchFlags.OpenPicker; + + // Search.SearchViewFlags : these flags are used to customize the appearance of the PickerWindow. + internal static readonly Search.SearchViewFlags PickerViewFlags = Search.SearchViewFlags.OpenInBuilderMode + | Search.SearchViewFlags.DisableBuilderModeToggle + | Search.SearchViewFlags.DisableInspectorPreview + | Search.SearchViewFlags.DisableSavedSearchQuery; + } + + internal static class AssetSearchProviders + { + const string k_AssetFolderSearchProviderId = "AssetsInputActionReferenceSearchProvider"; + const string k_ProjectWideActionsSearchProviderId = "ProjectWideInputActionReferenceSearchProvider"; + + internal static SearchProvider CreateInputActionReferenceSearchProviderForAssets() + { + return CreateInputActionReferenceSearchProvider(k_AssetFolderSearchProviderId, + "Asset Input Actions", + (obj) => "(Input Actions)", + () => InputActionImporter.LoadInputActionReferencesFromAssetDatabase()); + } + + private static SearchProvider CreateInputActionReferenceSearchProvider(string id, string displayName, + Func createItemFetchDescription, Func> fetchAssets) + { + // Match icon used for sub-assets from importer for InputActionReferences. + // Note that we assign description+label in FilteredSearch but also provide a fetchDescription+fetchLabel below. + // This is needed to support all zoom-modes for unknown reason. + // Also note that fetchLabel/fetchDescription and what is provided to CreateItem is playing different + // roles at different zoom levels. + var inputActionReferenceIcon = InputActionAssetIconProvider.LoadActionIcon(); + + return new SearchProvider(id, displayName) + { + priority = 25, + fetchDescription = FetchLabel, + fetchItems = (context, items, provider) => FilteredSearch(context, provider, FetchLabel, createItemFetchDescription, + fetchAssets, "(Project-Wide Input Actions)"), + fetchLabel = FetchLabel, + fetchPreview = (item, context, size, options) => inputActionReferenceIcon, + fetchThumbnail = (item, context) => inputActionReferenceIcon, + toObject = (item, type) => item.data as Object, + }; + } + + internal static SearchProvider CreateInputActionReferenceSearchProviderForProjectWideActions() + { + return CreateInputActionReferenceSearchProvider(k_ProjectWideActionsSearchProviderId, + "Project-Wide Input Actions", + (obj) => "(Project-Wide Input Actions)", + () => InputActionImporter.LoadInputActionReferencesFromAsset(ProjectWideActionsAsset.GetOrCreate())); + } + + // Custom search function with label matching filtering. + private static IEnumerable FilteredSearch(SearchContext context, SearchProvider provider, + Func fetchObjectLabel, Func createItemFetchDescription, Func> fetchAssets, string description) + { + foreach (var asset in fetchAssets()) + { + var label = fetchObjectLabel(asset); + if (!label.Contains(context.searchText, System.StringComparison.InvariantCultureIgnoreCase)) + continue; // Ignore due to filtering + yield return provider.CreateItem(context, asset.GetInstanceID().ToString(), label, createItemFetchDescription(asset), + null, asset); + } + } + + // Note that this is overloaded to allow utilizing FetchLabel inside fetchItems to keep label formatting + // consistent between CreateItem and additional fetchLabel calls. + + private static string FetchLabel(Object obj) + { + return obj.name; + } + + private static string FetchLabel(SearchItem item, SearchContext context) + { + return FetchLabel((item.data as Object) !); + } + } +} +#endif diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/AssetSearchProviders.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/AssetSearchProviders.cs.meta new file mode 100644 index 0000000000..188b26ae2d --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/AssetSearchProviders.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2305180900cc45989b0cbfd4a0dd1c35 +timeCreated: 1696573295 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionAssetDrawer.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionAssetDrawer.cs index a31d736f9a..458d3c0dda 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionAssetDrawer.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionAssetDrawer.cs @@ -28,7 +28,7 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten { EditorGUI.BeginProperty(position, label, property); - var isAssetProjectWideActions = IsAssetProjectWideActions(property); + var isAssetProjectWideActions = ProjectWideActionsAsset.IsProjectWideActionsAsset(property.objectReferenceValue); var selectedAssetOptionIndex = isAssetProjectWideActions ? AssetOptions.ProjectWideActions : AssetOptions.ActionsAsset; EditorGUILayout.BeginHorizontal(); @@ -84,21 +84,6 @@ static void UpdatePropertyWithSelectedOption(SerializedProperty assetProperty, A assetProperty.serializedObject.ApplyModifiedProperties(); } - - static bool IsAssetProjectWideActions(SerializedProperty property) - { - var isAssetProjectWideActions = false; - - // Check if the property InputActionAsset name is the same as project-wide actions to determine if - // project-wide actions are set - if (property.objectReferenceValue != null) - { - var asset = (InputActionAsset)property.objectReferenceValue; - isAssetProjectWideActions = asset?.name == ProjectWideActionsAsset.kAssetName; - } - - return isAssetProjectWideActions; - } } } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferencePropertyDrawer.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferencePropertyDrawer.cs new file mode 100644 index 0000000000..13508d968c --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferencePropertyDrawer.cs @@ -0,0 +1,35 @@ +// Note: If not UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS we do not use a custom property drawer and +// picker for InputActionReferences but rather rely on default (classic) object picker. +#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + +using UnityEditor; +using UnityEditor.Search; + +namespace UnityEngine.InputSystem.Editor +{ + /// + /// Custom property drawer in order to use the "Advanced Picker" from UnityEditor.Search. + /// + [CustomPropertyDrawer(typeof(InputActionReference))] + internal sealed class InputActionReferencePropertyDrawer : PropertyDrawer + { + private readonly SearchContext m_Context = UnityEditor.Search.SearchService.CreateContext(new[] + { + AssetSearchProviders.CreateInputActionReferenceSearchProviderForAssets(), + AssetSearchProviders.CreateInputActionReferenceSearchProviderForProjectWideActions(), + }, string.Empty, SearchConstants.PickerSearchFlags); + + private void OnValidate() + { + Debug.Log("OnValidate editor"); + } + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + ObjectField.DoObjectField(position, property, typeof(InputActionReference), label, + m_Context, SearchConstants.PickerViewFlags); + } + } +} + +#endif diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferencePropertyDrawer.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferencePropertyDrawer.cs.meta new file mode 100644 index 0000000000..343a83f26a --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferencePropertyDrawer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3a321ed133764ac2b2ca53fde879482d +timeCreated: 1696488621 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs index 3b186ad404..9e8a97e5db 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs @@ -35,7 +35,7 @@ public override void OnActivate(string searchContext, VisualElement rootElement) // Note that focused element will be set if we are navigating back to // an existing instance when switching setting in the left project settings panel since // this doesn't recreate the editor. - if (m_RootVisualElement.focusController.focusedElement != null) + if (m_RootVisualElement?.focusController?.focusedElement != null) OnEditFocus(null); } @@ -76,6 +76,7 @@ private void OnEditFocusLost(FocusOutEvent @event) m_HasEditFocus = false; #if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST + Debug.Log("SaveAsset"); InputActionsEditorWindowUtils.SaveAsset(m_State.serializedObject); #endif } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindowUtils.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindowUtils.cs index 1ace6c81bd..ee9f5b4bde 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindowUtils.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindowUtils.cs @@ -15,8 +15,9 @@ public static void SaveAsset(SerializedObject serializedAsset) { var asset = (InputActionAsset)serializedAsset.targetObject; // for the global actions asset: save differently (as it is a yaml file and not a json) - if (asset.name == ProjectWideActionsAsset.kAssetName) + if (ProjectWideActionsAsset.IsProjectWideActionsAsset(asset)) { + InputActionReferenceValidator.ValidateReferences(asset); AssetDatabase.SaveAssets(); return; } diff --git a/Packages/com.unity.inputsystem/InputSystem/Utilities/ArrayHelpers.cs b/Packages/com.unity.inputsystem/InputSystem/Utilities/ArrayHelpers.cs index 54a4972495..3312a19f95 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Utilities/ArrayHelpers.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Utilities/ArrayHelpers.cs @@ -628,6 +628,27 @@ public static bool Erase(ref TValue[] array, TValue value) return false; } + public static void EraseAll(ref TValue[] array, TValue value, IEqualityComparer comparer) + { + Debug.Assert(array != null); + + // Move tail element into position of element to be deleted (if necessary) and finally + // resize the array to to match capacity of elements contained. + // Note that Array.Resize is a no-op if size is unchanged. + var count = array.Length; + for (var i = count - 1; i >= 0; --i) + { + if (comparer.Equals(array[i], value)) + { + if (i != count - 1) + array[i] = array[count - 1]; + --count; + } + } + + Array.Resize(ref array, count); + } + /// /// Erase an element from the array by moving the tail element into its place. ///