diff --git a/Assets/Tests/InputSystem/CoreTests_ProjectWideActions.cs b/Assets/Tests/InputSystem/CoreTests_ProjectWideActions.cs index 89539950ff..5ead638bbc 100644 --- a/Assets/Tests/InputSystem/CoreTests_ProjectWideActions.cs +++ b/Assets/Tests/InputSystem/CoreTests_ProjectWideActions.cs @@ -1,7 +1,9 @@ #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using NUnit.Framework; using UnityEditor; using UnityEngine; diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 0cb8f32068..2f0a7c5b14 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -23,6 +23,7 @@ however, it has to be formatted properly to pass verification tests. ### Added - Support for [Game rotation vector](https://developer.android.com/reference/android/hardware/Sensor#TYPE_GAME_ROTATION_VECTOR) sensor on Android - Duplicate Input Action Items in the new Input Action Asset Editor with Ctrl+D (Windows) or Cmd+D (Mac) +- Selection of InputActionReferences from project-wide actions on fields that are of type InputActionReference. Uses a new advanced object picker that allows better searching and filtering of actions. ### Fixed - Partially fixed case ISX-1357 (Investigate performance regressing over time). A sample showed that leaving an InputActionMap enabled could lead to an internal list of listeners growing. This leads to slow-down, so we now warn if we think this is happening. @@ -38,6 +39,7 @@ however, it has to be formatted properly to pass verification tests. - Fixed case [ISX-1668] (The Profiler shows incorrect data and spams the console with "Missing Profiler.EndSample" errors when there is an Input System Component in Scene). - Fixed [ISX-1661](https://jira.unity3d.com/browse/ISX-1661) where undoing duplications of action maps caused console errors - Fix for BindingSyntax `WithInteraction()` which was incorrectly using processors. +- Fixed issue of visual elements being null during editing project-wide actions in project settings which prompted console errors. ## [1.8.0-pre.1] - 2023-09-04 diff --git a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionReference.cs b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionReference.cs index 0090f46bb8..73a73cd68e 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionReference.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Actions/InputActionReference.cs @@ -160,7 +160,7 @@ public override string ToString() return base.ToString(); } - private static string GetDisplayName(InputAction action) + internal 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/InputActionAssetIconLoader.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionAssetIconLoader.cs new file mode 100644 index 0000000000..6e6ad8a9e3 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionAssetIconLoader.cs @@ -0,0 +1,35 @@ +#if UNITY_EDITOR +using UnityEditor; + +namespace UnityEngine.InputSystem.Editor +{ + /// + /// Provides access to icons associated with and . + /// + internal static class InputActionAssetIconLoader + { + 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 . + /// + /// Icon resource reference or null if the resource could not be loaded. + internal static Texture2D LoadAssetIcon() + { + return (Texture2D)EditorGUIUtility.Load(kAssetIcon); + } + + /// + /// Attempts to load the icon associated with an sub-asset of an + /// . + /// + /// Icon resource reference or null if the resource could not be loaded. + internal static Texture2D LoadActionIcon() + { + return (Texture2D)EditorGUIUtility.Load(kActionIcon); + } + } +} + +#endif // #if UNITY_EDITOR diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionAssetIconLoader.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionAssetIconLoader.cs.meta new file mode 100644 index 0000000000..2e1d7a85f4 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionAssetIconLoader.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 015a929bdae646a99b91b756b998233f +timeCreated: 1698142059 \ 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..24d528e87d 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 = InputActionAssetIconLoader.LoadAssetIcon(); + var actionIcon = InputActionAssetIconLoader.LoadActionIcon(); // Add asset. ctx.AddObjectToAsset("", asset, assetIcon); @@ -212,12 +210,51 @@ 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 && o.name != "InputManager").Cast(); + } + + // Get all InputActionReferences from assets in the project. By default it only gets the assets in the "Assets" folder. + internal static IEnumerable LoadInputActionReferencesFromAssetDatabase(string[] foldersPath = null) + { + string[] searchFolders = null; + // If folderPath is null, search in "Assets" folder. + if (foldersPath == null) + { + searchFolders = 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}", searchFolders); + + // 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 assetPath = AssetDatabase.GUIDToAssetPath(guid); + var assetInputActionReferenceList = AssetDatabase.LoadAllAssetsAtPath(assetPath).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, InputActionAssetIconLoader.LoadAssetIcon()); } } } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs index 68146646f1..b8646565c9 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs @@ -86,8 +86,16 @@ 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. + CreateInputActionReferences(asset); + + AssetDatabase.SaveAssets(); + + return asset; + } + + private static void CreateInputActionReferences(InputActionAsset asset) + { + var maps = asset.actionMaps; foreach (var map in maps) { foreach (var action in map.actions) @@ -97,10 +105,51 @@ private static InputActionAsset CreateNewActionAsset() AssetDatabase.AddObjectToAsset(actionReference, asset); } } + } - AssetDatabase.SaveAssets(); + /// + /// Updates the input action references in the asset by updating names, removing dangling references + /// and adding new ones. + /// + /// + internal static void UpdateInputActionReferences() + { + var asset = GetOrCreate(); + var existingReferences = InputActionImporter.LoadInputActionReferencesFromAsset(asset).ToList(); - return asset; + // Check if referenced input action exists in the asset and remove the reference if it doesn't. + foreach (var actionReference in existingReferences) + { + var action = asset.FindAction(actionReference.action.id); + if (action == null) + { + actionReference.Set(null); + AssetDatabase.RemoveObjectFromAsset(actionReference); + } + } + + // Check if all actions have a reference + foreach (var action in asset) + { + var actionReference = existingReferences.FirstOrDefault(r => r.m_ActionId == action.id.ToString()); + // The input action doesn't have a reference, create a new one. + if (actionReference == null) + { + var actionReferenceNew = ScriptableObject.CreateInstance(); + actionReferenceNew.Set(action); + AssetDatabase.AddObjectToAsset(actionReferenceNew, asset); + } + else + { + // Update the name of the reference if it doesn't match the action name. + if (actionReference.name != InputActionReference.GetDisplayName(action)) + { + AssetDatabase.RemoveObjectFromAsset(actionReference); + actionReference.name = InputActionReference.GetDisplayName(action); + AssetDatabase.AddObjectToAsset(actionReference, asset); + } + } + } } } } 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..3186315b9f --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferencePropertyDrawer.cs @@ -0,0 +1,51 @@ +// 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[] + { + InputActionReferenceSearchProviders.CreateInputActionReferenceSearchProviderForAssets(), + InputActionReferenceSearchProviders.CreateInputActionReferenceSearchProviderForProjectWideActions(), + }, string.Empty, SearchConstants.PickerSearchFlags); + + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + // Sets the property to null if the action is not found in the asset. + ValidatePropertyWithDanglingInputActionReferences(property); + + ObjectField.DoObjectField(position, property, typeof(InputActionReference), label, + m_Context, SearchConstants.PickerViewFlags); + } + + static void ValidatePropertyWithDanglingInputActionReferences(SerializedProperty property) + { + if (property?.objectReferenceValue is InputActionReference reference) + { + // Check only if the reference is a project-wide action. + if (reference?.asset?.name == ProjectWideActionsAsset.kAssetName) + { + var action = reference?.asset?.FindAction(reference.action.id); + if (action is null) + { + property.objectReferenceValue = null; + property.serializedObject.ApplyModifiedProperties(); + } + } + } + } + } +} + +#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..6a705fdc38 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferencePropertyDrawer.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 30bd04fe045c46918b5fe7bc6efc4ce2 +timeCreated: 1698143402 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferenceSearchProviders.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferenceSearchProviders.cs new file mode 100644 index 0000000000..8830de0a41 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferenceSearchProviders.cs @@ -0,0 +1,98 @@ +#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEditor.Search; +using UnityEngine.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 in the advanced object picker. + // Note: 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 = SearchViewFlags.DisableBuilderModeToggle + | SearchViewFlags.DisableInspectorPreview + | SearchViewFlags.ListView + | SearchViewFlags.DisableSavedSearchQuery; + } + + internal static class InputActionReferenceSearchProviders + { + const string k_AssetFolderSearchProviderId = "AssetsInputActionReferenceSearchProvider"; + const string k_ProjectWideActionsSearchProviderId = "ProjectWideInputActionReferenceSearchProvider"; + + // Search provider for InputActionReferences for all assets in the project, without project-wide actions. + internal static SearchProvider CreateInputActionReferenceSearchProviderForAssets() + { + return CreateInputActionReferenceSearchProvider(k_AssetFolderSearchProviderId, + "Asset Input Actions", + // Show the asset path in the description. + (obj) => AssetDatabase.GetAssetPath((obj as InputActionReference).asset), + () => InputActionImporter.LoadInputActionReferencesFromAssetDatabase()); + } + + // Search provider for InputActionReferences for project-wide actions + internal static SearchProvider CreateInputActionReferenceSearchProviderForProjectWideActions() + { + return CreateInputActionReferenceSearchProvider(k_ProjectWideActionsSearchProviderId, + "Project-Wide Input Actions", + (obj) => "(Project-Wide Input Actions)", + () => InputActionImporter.LoadInputActionReferencesFromAsset(ProjectWideActionsAsset.GetOrCreate())); + } + + private static SearchProvider CreateInputActionReferenceSearchProvider(string id, string displayName, + Func createItemFetchDescription, Func> fetchAssets) + { + // Match icon used for sub-assets from importer for InputActionReferences. + // We assign description+label in FilteredSearch but also provide a fetchDescription+fetchLabel below. + // This is needed to support all zoom-modes for an unknown reason. + // Also, fetchLabel/fetchDescription and what is provided to CreateItem is playing different + // roles at different zoom levels. + var inputActionReferenceIcon = InputActionAssetIconLoader.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, + }; + } + + // 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/InputActionReferenceSearchProviders.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferenceSearchProviders.cs.meta new file mode 100644 index 0000000000..e04b11ddf9 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferenceSearchProviders.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: baaf1bd005134bc6825c06f3510c6643 +timeCreated: 1698141997 \ 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..0eac5ce70e 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); } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindowUtils.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindowUtils.cs index 1ace6c81bd..937e0f1d7e 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindowUtils.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindowUtils.cs @@ -1,5 +1,6 @@ #if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS using System.IO; +using System.Linq; using UnityEditor; using UnityEngine.UIElements; @@ -14,9 +15,10 @@ internal class InputActionsEditorWindowUtils 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) + // For project-wide actions asset save works differently. The asset is in YAML format, not JSON. if (asset.name == ProjectWideActionsAsset.kAssetName) { + ProjectWideActionsAsset.UpdateInputActionReferences(); AssetDatabase.SaveAssets(); return; }