Skip to content

Commit 4aa72ef

Browse files
NEW: Allow selection of InputActionReferences from project-wide actions (#1779)
* Add InputActionReference advanced picker This new picker allows us to search InputActionReferences from the project wide actions asset. Since this is asset is in ProjectSettings, a normal object picker doesn't work. * Update InputActionReferences of project-wide actions on save * Check for dangling action references in Inspector and set them to "None" A dangling action reference means a actin reference that point to a non-existent InputAction. This needs to be checked here in case an input action is removed from the project-wide actions asset. Otherwise the UI will still hold on to a dangling action reference. * Remove unused code * Fix comments references * Correct variables naming * Test updating references when asset actions are changed * Only check dangling project-wide action references * Update CHANGELOG * Make InputActionAssetIconLoader methods internal * Make advanced picker open by default with list view and simple search query OpenInBuilder mode allows to search by "tags". But no implementation was done so they did not work. * Run added tests only on the Editor
1 parent 0adad49 commit 4aa72ef

13 files changed

+298
-13
lines changed

Assets/Tests/InputSystem/CoreTests_ProjectWideActions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
22

33
using System;
4+
using System.Collections.Generic;
45
using System.IO;
6+
using System.Linq;
57
using NUnit.Framework;
68
using UnityEditor;
79
using UnityEngine;

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ however, it has to be formatted properly to pass verification tests.
2323
### Added
2424
- Support for [Game rotation vector](https://developer.android.com/reference/android/hardware/Sensor#TYPE_GAME_ROTATION_VECTOR) sensor on Android
2525
- Duplicate Input Action Items in the new Input Action Asset Editor with Ctrl+D (Windows) or Cmd+D (Mac)
26+
- 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.
2627

2728
### Fixed
2829
- 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.
3839
- 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).
3940
- Fixed [ISX-1661](https://jira.unity3d.com/browse/ISX-1661) where undoing duplications of action maps caused console errors
4041
- Fix for BindingSyntax `WithInteraction()` which was incorrectly using processors.
42+
- Fixed issue of visual elements being null during editing project-wide actions in project settings which prompted console errors.
4143

4244

4345
## [1.8.0-pre.1] - 2023-09-04

Packages/com.unity.inputsystem/InputSystem/Actions/InputActionReference.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public override string ToString()
160160
return base.ToString();
161161
}
162162

163-
private static string GetDisplayName(InputAction action)
163+
internal static string GetDisplayName(InputAction action)
164164
{
165165
return !string.IsNullOrEmpty(action?.actionMap?.name) ? $"{action.actionMap?.name}/{action.name}" : action?.name;
166166
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#if UNITY_EDITOR
2+
using UnityEditor;
3+
4+
namespace UnityEngine.InputSystem.Editor
5+
{
6+
/// <summary>
7+
/// Provides access to icons associated with <see cref="InputActionAsset"/> and <see cref="InputActionReference"/>.
8+
/// </summary>
9+
internal static class InputActionAssetIconLoader
10+
{
11+
private const string kActionIcon = "Packages/com.unity.inputsystem/InputSystem/Editor/Icons/InputAction.png";
12+
private const string kAssetIcon = "Packages/com.unity.inputsystem/InputSystem/Editor/Icons/InputActionAsset.png";
13+
14+
/// <summary>
15+
/// Attempts to load the icon associated with an <see cref="InputActionAsset"/>.
16+
/// </summary>
17+
/// <returns>Icon resource reference or <code>null</code> if the resource could not be loaded.</returns>
18+
internal static Texture2D LoadAssetIcon()
19+
{
20+
return (Texture2D)EditorGUIUtility.Load(kAssetIcon);
21+
}
22+
23+
/// <summary>
24+
/// Attempts to load the icon associated with an <see cref="InputActionReference"/> sub-asset of an
25+
/// <see cref="InputActionAsset"/>.
26+
/// </summary>
27+
/// <returns>Icon resource reference or <code>null</code> if the resource could not be loaded.</returns>
28+
internal static Texture2D LoadActionIcon()
29+
{
30+
return (Texture2D)EditorGUIUtility.Load(kActionIcon);
31+
}
32+
}
33+
}
34+
35+
#endif // #if UNITY_EDITOR

Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionAssetIconLoader.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporter.cs

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#if UNITY_EDITOR
22
using System;
3+
using System.Collections.Generic;
34
using System.IO;
45
using System.Linq;
56
using UnityEditor;
@@ -29,9 +30,6 @@ internal class InputActionImporter : ScriptedImporter
2930
{
3031
private const int kVersion = 13;
3132

32-
private const string kActionIcon = "Packages/com.unity.inputsystem/InputSystem/Editor/Icons/InputAction.png";
33-
private const string kAssetIcon = "Packages/com.unity.inputsystem/InputSystem/Editor/Icons/InputActionAsset.png";
34-
3533
[SerializeField] private bool m_GenerateWrapperCode;
3634
[SerializeField] private string m_WrapperCodePath;
3735
[SerializeField] private string m_WrapperClassName;
@@ -88,8 +86,8 @@ public override void OnImportAsset(AssetImportContext ctx)
8886

8987
// Load icons.
9088
////REVIEW: the icons won't change if the user changes skin; not sure it makes sense to differentiate here
91-
var assetIcon = (Texture2D)EditorGUIUtility.Load(kAssetIcon);
92-
var actionIcon = (Texture2D)EditorGUIUtility.Load(kActionIcon);
89+
var assetIcon = InputActionAssetIconLoader.LoadAssetIcon();
90+
var actionIcon = InputActionAssetIconLoader.LoadActionIcon();
9391

9492
// Add asset.
9593
ctx.AddObjectToAsset("<root>", asset, assetIcon);
@@ -212,12 +210,51 @@ public override void OnImportAsset(AssetImportContext ctx)
212210
InputActionEditorWindow.RefreshAllOnAssetReimport();
213211
}
214212

213+
internal static IEnumerable<InputActionReference> LoadInputActionReferencesFromAsset(InputActionAsset asset)
214+
{
215+
//Get all InputActionReferences are stored at the same asset path as InputActionAsset
216+
return AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(asset)).Where(
217+
o => o is InputActionReference && o.name != "InputManager").Cast<InputActionReference>();
218+
}
219+
220+
// Get all InputActionReferences from assets in the project. By default it only gets the assets in the "Assets" folder.
221+
internal static IEnumerable<InputActionReference> LoadInputActionReferencesFromAssetDatabase(string[] foldersPath = null)
222+
{
223+
string[] searchFolders = null;
224+
// If folderPath is null, search in "Assets" folder.
225+
if (foldersPath == null)
226+
{
227+
searchFolders = new string[] { "Assets" };
228+
}
229+
230+
// Get all InputActionReference from assets in "Asset" folder. It does not search inside "Packages" folder.
231+
var inputActionReferenceGUIDs = AssetDatabase.FindAssets($"t:{typeof(InputActionReference).Name}", searchFolders);
232+
233+
// To find all the InputActionReferences, the GUID of the asset containing at least one action reference is
234+
// used to find the asset path. This is because InputActionReferences are stored in the asset database as sub-assets of InputActionAsset.
235+
// Then the whole asset is loaded and all the InputActionReferences are extracted from it.
236+
// Also, the action references are duplicated to have backwards compatibility with the 1.0.0-preview.7. That
237+
// is why we look for references withouth the `HideFlags.HideInHierarchy` flag.
238+
var inputActionReferencesList = new List<InputActionReference>();
239+
foreach (var guid in inputActionReferenceGUIDs)
240+
{
241+
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
242+
var assetInputActionReferenceList = AssetDatabase.LoadAllAssetsAtPath(assetPath).Where(
243+
o => o is InputActionReference &&
244+
!((InputActionReference)o).hideFlags.HasFlag(HideFlags.HideInHierarchy))
245+
.Cast<InputActionReference>().ToList();
246+
247+
inputActionReferencesList.AddRange(assetInputActionReferenceList);
248+
}
249+
return inputActionReferencesList;
250+
}
251+
215252
// Add item to plop an .inputactions asset into the project.
216253
[MenuItem("Assets/Create/Input Actions")]
217254
public static void CreateInputAsset()
218255
{
219256
ProjectWindowUtil.CreateAssetWithContent("New Controls." + InputActionAsset.Extension,
220-
InputActionAsset.kDefaultAssetLayoutJson, (Texture2D)EditorGUIUtility.Load(kAssetIcon));
257+
InputActionAsset.kDefaultAssetLayoutJson, InputActionAssetIconLoader.LoadAssetIcon());
221258
}
222259
}
223260
}

Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,16 @@ private static InputActionAsset CreateNewActionAsset()
8686
}
8787
}
8888

89-
// Create sub-asset for each action. This is so that users can select individual input actions from the asset when they're
90-
// trying to assign to a field that accepts only one action.
89+
CreateInputActionReferences(asset);
90+
91+
AssetDatabase.SaveAssets();
92+
93+
return asset;
94+
}
95+
96+
private static void CreateInputActionReferences(InputActionAsset asset)
97+
{
98+
var maps = asset.actionMaps;
9199
foreach (var map in maps)
92100
{
93101
foreach (var action in map.actions)
@@ -97,10 +105,51 @@ private static InputActionAsset CreateNewActionAsset()
97105
AssetDatabase.AddObjectToAsset(actionReference, asset);
98106
}
99107
}
108+
}
100109

101-
AssetDatabase.SaveAssets();
110+
/// <summary>
111+
/// Updates the input action references in the asset by updating names, removing dangling references
112+
/// and adding new ones.
113+
/// </summary>
114+
/// <param name="asset"></param>
115+
internal static void UpdateInputActionReferences()
116+
{
117+
var asset = GetOrCreate();
118+
var existingReferences = InputActionImporter.LoadInputActionReferencesFromAsset(asset).ToList();
102119

103-
return asset;
120+
// Check if referenced input action exists in the asset and remove the reference if it doesn't.
121+
foreach (var actionReference in existingReferences)
122+
{
123+
var action = asset.FindAction(actionReference.action.id);
124+
if (action == null)
125+
{
126+
actionReference.Set(null);
127+
AssetDatabase.RemoveObjectFromAsset(actionReference);
128+
}
129+
}
130+
131+
// Check if all actions have a reference
132+
foreach (var action in asset)
133+
{
134+
var actionReference = existingReferences.FirstOrDefault(r => r.m_ActionId == action.id.ToString());
135+
// The input action doesn't have a reference, create a new one.
136+
if (actionReference == null)
137+
{
138+
var actionReferenceNew = ScriptableObject.CreateInstance<InputActionReference>();
139+
actionReferenceNew.Set(action);
140+
AssetDatabase.AddObjectToAsset(actionReferenceNew, asset);
141+
}
142+
else
143+
{
144+
// Update the name of the reference if it doesn't match the action name.
145+
if (actionReference.name != InputActionReference.GetDisplayName(action))
146+
{
147+
AssetDatabase.RemoveObjectFromAsset(actionReference);
148+
actionReference.name = InputActionReference.GetDisplayName(action);
149+
AssetDatabase.AddObjectToAsset(actionReference, asset);
150+
}
151+
}
152+
}
104153
}
105154
}
106155
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Note: If not UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS we do not use a custom property drawer and
2+
// picker for InputActionReferences but rather rely on default (classic) object picker.
3+
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
4+
5+
using UnityEditor;
6+
using UnityEditor.Search;
7+
8+
namespace UnityEngine.InputSystem.Editor
9+
{
10+
/// <summary>
11+
/// Custom property drawer in order to use the "Advanced Picker" from UnityEditor.Search.
12+
/// </summary>
13+
[CustomPropertyDrawer(typeof(InputActionReference))]
14+
internal sealed class InputActionReferencePropertyDrawer : PropertyDrawer
15+
{
16+
private readonly SearchContext m_Context = UnityEditor.Search.SearchService.CreateContext(new[]
17+
{
18+
InputActionReferenceSearchProviders.CreateInputActionReferenceSearchProviderForAssets(),
19+
InputActionReferenceSearchProviders.CreateInputActionReferenceSearchProviderForProjectWideActions(),
20+
}, string.Empty, SearchConstants.PickerSearchFlags);
21+
22+
23+
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
24+
{
25+
// Sets the property to null if the action is not found in the asset.
26+
ValidatePropertyWithDanglingInputActionReferences(property);
27+
28+
ObjectField.DoObjectField(position, property, typeof(InputActionReference), label,
29+
m_Context, SearchConstants.PickerViewFlags);
30+
}
31+
32+
static void ValidatePropertyWithDanglingInputActionReferences(SerializedProperty property)
33+
{
34+
if (property?.objectReferenceValue is InputActionReference reference)
35+
{
36+
// Check only if the reference is a project-wide action.
37+
if (reference?.asset?.name == ProjectWideActionsAsset.kAssetName)
38+
{
39+
var action = reference?.asset?.FindAction(reference.action.id);
40+
if (action is null)
41+
{
42+
property.objectReferenceValue = null;
43+
property.serializedObject.ApplyModifiedProperties();
44+
}
45+
}
46+
}
47+
}
48+
}
49+
}
50+
51+
#endif

Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferencePropertyDrawer.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS
2+
using System;
3+
using System.Collections.Generic;
4+
using UnityEditor;
5+
using UnityEditor.Search;
6+
using UnityEngine.Search;
7+
8+
namespace UnityEngine.InputSystem.Editor
9+
{
10+
internal static class SearchConstants
11+
{
12+
// SearchFlags: these flags are used to customize how search is performed and how search
13+
// results are displayed in the advanced object picker.
14+
// Note: SearchFlags.Packages is not currently used and hides all results from packages.
15+
internal static readonly SearchFlags PickerSearchFlags = SearchFlags.Sorted | SearchFlags.OpenPicker;
16+
17+
// Search.SearchViewFlags : these flags are used to customize the appearance of the PickerWindow.
18+
internal static readonly Search.SearchViewFlags PickerViewFlags = SearchViewFlags.DisableBuilderModeToggle
19+
| SearchViewFlags.DisableInspectorPreview
20+
| SearchViewFlags.ListView
21+
| SearchViewFlags.DisableSavedSearchQuery;
22+
}
23+
24+
internal static class InputActionReferenceSearchProviders
25+
{
26+
const string k_AssetFolderSearchProviderId = "AssetsInputActionReferenceSearchProvider";
27+
const string k_ProjectWideActionsSearchProviderId = "ProjectWideInputActionReferenceSearchProvider";
28+
29+
// Search provider for InputActionReferences for all assets in the project, without project-wide actions.
30+
internal static SearchProvider CreateInputActionReferenceSearchProviderForAssets()
31+
{
32+
return CreateInputActionReferenceSearchProvider(k_AssetFolderSearchProviderId,
33+
"Asset Input Actions",
34+
// Show the asset path in the description.
35+
(obj) => AssetDatabase.GetAssetPath((obj as InputActionReference).asset),
36+
() => InputActionImporter.LoadInputActionReferencesFromAssetDatabase());
37+
}
38+
39+
// Search provider for InputActionReferences for project-wide actions
40+
internal static SearchProvider CreateInputActionReferenceSearchProviderForProjectWideActions()
41+
{
42+
return CreateInputActionReferenceSearchProvider(k_ProjectWideActionsSearchProviderId,
43+
"Project-Wide Input Actions",
44+
(obj) => "(Project-Wide Input Actions)",
45+
() => InputActionImporter.LoadInputActionReferencesFromAsset(ProjectWideActionsAsset.GetOrCreate()));
46+
}
47+
48+
private static SearchProvider CreateInputActionReferenceSearchProvider(string id, string displayName,
49+
Func<Object, string> createItemFetchDescription, Func<IEnumerable<Object>> fetchAssets)
50+
{
51+
// Match icon used for sub-assets from importer for InputActionReferences.
52+
// We assign description+label in FilteredSearch but also provide a fetchDescription+fetchLabel below.
53+
// This is needed to support all zoom-modes for an unknown reason.
54+
// Also, fetchLabel/fetchDescription and what is provided to CreateItem is playing different
55+
// roles at different zoom levels.
56+
var inputActionReferenceIcon = InputActionAssetIconLoader.LoadActionIcon();
57+
58+
return new SearchProvider(id, displayName)
59+
{
60+
priority = 25,
61+
fetchDescription = FetchLabel,
62+
fetchItems = (context, items, provider) => FilteredSearch(context, provider, FetchLabel, createItemFetchDescription,
63+
fetchAssets, "(Project-Wide Input Actions)"),
64+
fetchLabel = FetchLabel,
65+
fetchPreview = (item, context, size, options) => inputActionReferenceIcon,
66+
fetchThumbnail = (item, context) => inputActionReferenceIcon,
67+
toObject = (item, type) => item.data as Object,
68+
};
69+
}
70+
71+
// Custom search function with label matching filtering.
72+
private static IEnumerable<SearchItem> FilteredSearch(SearchContext context, SearchProvider provider,
73+
Func<Object, string> fetchObjectLabel, Func<Object, string> createItemFetchDescription, Func<IEnumerable<Object>> fetchAssets, string description)
74+
{
75+
foreach (var asset in fetchAssets())
76+
{
77+
var label = fetchObjectLabel(asset);
78+
if (!label.Contains(context.searchText, System.StringComparison.InvariantCultureIgnoreCase))
79+
continue; // Ignore due to filtering
80+
yield return provider.CreateItem(context, asset.GetInstanceID().ToString(), label, createItemFetchDescription(asset),
81+
null, asset);
82+
}
83+
}
84+
85+
// Note that this is overloaded to allow utilizing FetchLabel inside fetchItems to keep label formatting
86+
// consistent between CreateItem and additional fetchLabel calls.
87+
private static string FetchLabel(Object obj)
88+
{
89+
return obj.name;
90+
}
91+
92+
private static string FetchLabel(SearchItem item, SearchContext context)
93+
{
94+
return FetchLabel((item.data as Object) !);
95+
}
96+
}
97+
}
98+
#endif

0 commit comments

Comments
 (0)