From 85c5cf769d3312b09d3ac425bf8a7d1fd89e2fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akan=20Sidenvall?= Date: Mon, 5 Feb 2024 23:54:50 +0100 Subject: [PATCH 01/48] Replacing use of Inputmanager.asset with promoted InputActionsAsset --- .../Editor/ActiveAssetEditorHelper.cs | 27 ++++++++ .../Editor/ActiveAssetEditorHelper.cs.meta | 3 + .../InputActionImporterEditor.cs | 66 ++++++++++++++++--- .../ProjectWideActionsAsset.cs | 2 +- .../Settings/InputSettingsBuildProvider.cs | 52 ++++++++++----- .../Editor/Settings/InputSettingsProvider.cs | 26 ++++---- .../InputActionsEditorSettingsProvider.cs | 29 +++++--- .../InputSystem/InputManager.cs | 27 ++++++++ .../InputSystem/InputSystem.cs | 62 +++++++++-------- 9 files changed, 215 insertions(+), 79 deletions(-) create mode 100644 Packages/com.unity.inputsystem/InputSystem/Editor/ActiveAssetEditorHelper.cs create mode 100644 Packages/com.unity.inputsystem/InputSystem/Editor/ActiveAssetEditorHelper.cs.meta diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/ActiveAssetEditorHelper.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/ActiveAssetEditorHelper.cs new file mode 100644 index 0000000000..1e33f110b3 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/ActiveAssetEditorHelper.cs @@ -0,0 +1,27 @@ +using System; +using UnityEditor; + +namespace UnityEngine.InputSystem.Editor +{ + internal static class ActiveAssetEditorHelper + { + public static void DrawMakeActiveGui(T current, T target, string targetName, string entity, Action apply) + where T : ScriptableObject + { + if (current == target) + { + EditorGUILayout.HelpBox($"This asset contains the currently active {entity} for the Input System.", MessageType.Info); + return; + } + + string currentlyActiveAssetsPath = null; + if (current != null) + currentlyActiveAssetsPath = AssetDatabase.GetAssetPath(current); + if (!string.IsNullOrEmpty(currentlyActiveAssetsPath)) + currentlyActiveAssetsPath = $"The currently active {entity} are stored in {currentlyActiveAssetsPath}. "; + EditorGUILayout.HelpBox($"Note that this asset does not contain the currently active {entity} for the Input System. {currentlyActiveAssetsPath??""}Click \"Make Active\" below to make \"{targetName}\" the active one.", MessageType.Warning); + if (GUILayout.Button($"Make active", EditorStyles.miniButton)) + apply(target); + } + } +} diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/ActiveAssetEditorHelper.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/ActiveAssetEditorHelper.cs.meta new file mode 100644 index 0000000000..bc35c9efc6 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/ActiveAssetEditorHelper.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6260a62900d74d10953648863a1e9c5a +timeCreated: 1707137172 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporterEditor.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporterEditor.cs index 32fb619895..98ca49e182 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporterEditor.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporterEditor.cs @@ -27,26 +27,27 @@ public override void OnInspectorGUI() // like in other types of editors. serializedObject.Update(); - if (inputActionAsset == null) + EditorGUILayout.Space(); + + if (inputActionAsset == null) // TODO Why would this ever happen? EditorGUILayout.HelpBox("The currently selected object is not an editable input action asset.", MessageType.Info); // Button to pop up window to edit the asset. using (new EditorGUI.DisabledScope(inputActionAsset == null)) { - if (GUILayout.Button("Edit asset")) - { -#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS - if (!InputSystem.settings.IsFeatureEnabled(InputFeatureNames.kUseIMGUIEditorForAssets)) - InputActionsEditorWindow.OpenEditor(inputActionAsset); - else -#endif - InputActionEditorWindow.OpenEditor(inputActionAsset); - } + if (GUILayout.Button(GetOpenEditorButtonText(inputActionAsset), GUILayout.Height(30))) + OpenEditor(inputActionAsset); } EditorGUILayout.Space(); + // Project-wide Input Actions Asset UI. + ActiveAssetEditorHelper.DrawMakeActiveGui(InputSystem.actions, inputActionAsset, + inputActionAsset.name, "Project-wide Input Actions", (value) => InputSystem.actions = value); + + EditorGUILayout.Space(); + // Importer settings UI. var generateWrapperCodeProperty = serializedObject.FindProperty("m_GenerateWrapperCode"); EditorGUILayout.PropertyField(generateWrapperCodeProperty, m_GenerateWrapperCodeLabel); @@ -108,6 +109,51 @@ private InputActionAsset GetAsset() return assetTarget as InputActionAsset; } + protected override bool ShouldHideOpenButton() + { + return IsProjectWideActionsAsset(); + } + + private bool IsProjectWideActionsAsset() + { + return IsProjectWideActionsAsset(GetAsset()); + } + + private static bool IsProjectWideActionsAsset(InputActionAsset asset) + { + return !ReferenceEquals(asset, null) && InputSystem.actions == asset; + } + + private string GetOpenEditorButtonText(InputActionAsset asset) + { +#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + if (IsProjectWideActionsAsset(asset)) + return "Edit in Project Settings Window"; +#endif + return "Edit Asset"; + } + + private static void OpenEditor(InputActionAsset asset) + { +#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + // Redirect to Project-settings Input Actions editor if this is the project-wide actions asset + if (IsProjectWideActionsAsset(asset)) + { + SettingsService.OpenProjectSettings(InputSettingsPath.kSettingsRootPath); + return; + } + + // Redirect to UI-Toolkit window editor if not configured to use IMGUI explicitly + if (!InputSystem.settings.IsFeatureEnabled(InputFeatureNames.kUseIMGUIEditorForAssets)) + InputActionsEditorWindow.OpenEditor(asset); + else + InputActionEditorWindow.OpenEditor(asset); +#else + // Redirect to IMGUI editor + InputActionEditorWindow.OpenEditor(asset); +#endif + } + private readonly GUIContent m_GenerateWrapperCodeLabel = EditorGUIUtility.TrTextContent("Generate C# Class"); private readonly GUIContent m_WrapperCodePathLabel = EditorGUIUtility.TrTextContent("C# Class File"); private readonly GUIContent m_WrapperClassNameLabel = EditorGUIUtility.TrTextContent("C# Class Name"); diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs index f56024fa49..861f69ac1d 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs @@ -173,7 +173,7 @@ internal static void DeleteActionAssetAndActionReferences() if (obj is InputActionReference) { var actionReference = obj as InputActionReference; - actionReference.Set(null); + actionReference.Set(null); // TODO This is wrong it should be destroyed?! AssetDatabase.RemoveObjectFromAsset(obj); } else if (obj is InputActionAsset) diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsBuildProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsBuildProvider.cs index 539f70f28a..c0622d5934 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsBuildProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsBuildProvider.cs @@ -1,4 +1,5 @@ #if UNITY_EDITOR +using System; using System.Linq; using UnityEditor; using UnityEditor.Build; @@ -7,9 +8,9 @@ namespace UnityEngine.InputSystem.Editor { + // TODO This class is incorrectly named if not single-purpose for settings, either create a separate one for project-wide actions or rename this and relocate it internal class InputSettingsBuildProvider : IPreprocessBuildWithReport, IPostprocessBuildWithReport { - InputActionAsset m_ProjectWideActions; Object[] m_OriginalPreloadedAssets; public int callbackOrder => 0; @@ -17,32 +18,51 @@ public void OnPreprocessBuild(BuildReport report) { m_OriginalPreloadedAssets = PlayerSettings.GetPreloadedAssets(); var preloadedAssets = PlayerSettings.GetPreloadedAssets(); + Debug.Assert(!ReferenceEquals(m_OriginalPreloadedAssets, preloadedAssets)); + + var oldSize = preloadedAssets.Length; + var newSize = oldSize; #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS - m_ProjectWideActions = Editor.ProjectWideActionsAsset.GetOrCreate(); - if (m_ProjectWideActions != null) - { - if (!preloadedAssets.Contains(m_ProjectWideActions)) - { - ArrayHelpers.Append(ref preloadedAssets, m_ProjectWideActions); - PlayerSettings.SetPreloadedAssets(preloadedAssets); - } - } + // Determine if we need to preload project-wide InputActionsAsset. + var actions = InputSystem.actions; + var actionsMissing = NeedsToBeAdded(preloadedAssets, actions, ref newSize); #endif - if (InputSystem.settings == null) + + // Determine if we need to preload InputSettings asset. + var settings = InputSystem.settings; + var settingsMissing = NeedsToBeAdded(preloadedAssets, settings, ref newSize); + + // Return immediately if all assets are already present + if (newSize == oldSize) return; - if (!preloadedAssets.Contains(InputSystem.settings)) - { - ArrayHelpers.Append(ref preloadedAssets, InputSystem.settings); - PlayerSettings.SetPreloadedAssets(preloadedAssets); - } + // Modify array so allocation only happens once + Array.Resize(ref preloadedAssets, newSize); +#if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS + if (actionsMissing) + ArrayHelpers.Append(ref preloadedAssets, actions); +#endif + if (settingsMissing) + ArrayHelpers.Append(ref preloadedAssets, settings); + + // Update preloaded assets (once) + PlayerSettings.SetPreloadedAssets(preloadedAssets); } public void OnPostprocessBuild(BuildReport report) { // Revert back to original state PlayerSettings.SetPreloadedAssets(m_OriginalPreloadedAssets); + m_OriginalPreloadedAssets = null; + } + + private static bool NeedsToBeAdded(Object[] preloadedAssets, Object asset, ref int extraCapacity) + { + var isMissing = (asset != null) && !preloadedAssets.Contains(asset); + if (isMissing) + ++extraCapacity; + return isMissing; } } } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs index 3270693fed..5375434212 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs @@ -487,24 +487,20 @@ internal class InputSettingsEditor : UnityEditor.Editor { public override void OnInspectorGUI() { - GUILayout.Space(10); + EditorGUILayout.Space(); + if (GUILayout.Button("Open Input Settings Window", GUILayout.Height(30))) InputSettingsProvider.Open(); - GUILayout.Space(10); - if (InputSystem.settings == target) - EditorGUILayout.HelpBox("This asset contains the currently active settings for the Input System.", MessageType.Info); - else - { - string currentlyActiveAssetsPath = null; - if (InputSystem.settings != null) - currentlyActiveAssetsPath = AssetDatabase.GetAssetPath(InputSystem.settings); - if (!string.IsNullOrEmpty(currentlyActiveAssetsPath)) - currentlyActiveAssetsPath = $"The currently active settings are stored in {currentlyActiveAssetsPath}. "; - EditorGUILayout.HelpBox($"Note that this asset does not contain the currently active settings for the Input System. {currentlyActiveAssetsPath??""}Click \"Make Active\" below to make {target.name} the active one.", MessageType.Warning); - if (GUILayout.Button($"Make active", EditorStyles.miniButton)) - InputSystem.settings = (InputSettings)target; - } + EditorGUILayout.Space(); + + ActiveAssetEditorHelper.DrawMakeActiveGui(InputSystem.settings, target as InputSettings, + target.name, "settings", (value) => InputSystem.settings = value); + } + + protected override bool ShouldHideOpenButton() + { + return true; } } } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs index 5d30c3d2a0..fb6debbda9 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs @@ -5,6 +5,8 @@ namespace UnityEngine.InputSystem.Editor { + // TODO This editor should react to InputSystem.actions being reassigned from outside + internal class InputActionsEditorSettingsProvider : SettingsProvider { public const string kSettingsPath = InputSettingsPath.kSettingsRootPath; @@ -22,9 +24,10 @@ public InputActionsEditorSettingsProvider(string path, SettingsScope scopes, IEn public override void OnActivate(string searchContext, VisualElement rootElement) { m_RootVisualElement = rootElement; - var asset = ProjectWideActionsAsset.GetOrCreate(); - var serializedAsset = new SerializedObject(asset); - m_State = new InputActionsEditorState(serializedAsset); + var asset = InputSystem.actions; + if (asset != null) + m_State = new InputActionsEditorState(new SerializedObject(asset)); + BuildUI(); // Monitor focus state of root element @@ -76,7 +79,8 @@ private void OnEditFocusLost(FocusOutEvent @event) m_HasEditFocus = false; #if UNITY_INPUT_SYSTEM_INPUT_ACTIONS_EDITOR_AUTO_SAVE_ON_FOCUS_LOST - InputActionsEditorWindowUtils.SaveAsset(m_State.serializedObject); + if (hasAsset) + InputActionsEditorWindowUtils.SaveAsset(m_State.serializedObject); #endif } } @@ -93,12 +97,15 @@ private void OnStateChanged(InputActionsEditorState newState) private void BuildUI() { - m_StateContainer = new StateContainer(m_RootVisualElement, m_State); - m_StateContainer.StateChanged += OnStateChanged; - m_RootVisualElement.styleSheets.Add(InputActionsEditorWindowUtils.theme); - var view = new InputActionsEditorView(m_RootVisualElement, m_StateContainer); - view.postResetAction += OnResetAsset; - m_StateContainer.Initialize(); + if (hasAsset) + { + m_StateContainer = new StateContainer(m_RootVisualElement, m_State); + m_StateContainer.StateChanged += OnStateChanged; + m_RootVisualElement.styleSheets.Add(InputActionsEditorWindowUtils.theme); + var view = new InputActionsEditorView(m_RootVisualElement, m_StateContainer); + view.postResetAction += OnResetAsset; + m_StateContainer.Initialize(); + } // Hide the save / auto save buttons in the project wide input actions // Project wide input actions always auto save @@ -116,6 +123,8 @@ private void OnResetAsset(InputActionAsset newAsset) m_State = new InputActionsEditorState(serializedAsset); } + private bool hasAsset => m_State.serializedObject != null; + [SettingsProvider] public static SettingsProvider CreateGlobalInputActionsEditorProvider() { diff --git a/Packages/com.unity.inputsystem/InputSystem/InputManager.cs b/Packages/com.unity.inputsystem/InputSystem/InputManager.cs index 47543eb675..f0682c3378 100644 --- a/Packages/com.unity.inputsystem/InputSystem/InputManager.cs +++ b/Packages/com.unity.inputsystem/InputSystem/InputManager.cs @@ -106,6 +106,26 @@ public InputSettings settings } } + public InputActionAsset actions + { + get + { + return m_Actions; + } + + set + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + if (m_Actions == value) + return; + + m_Actions = value; + // ApplyActions(); + } + } + public InputUpdateType updateMask { get => m_UpdateMask; @@ -2056,6 +2076,7 @@ internal struct AvailableDevice internal IInputRuntime m_Runtime; internal InputMetrics m_Metrics; internal InputSettings m_Settings; + internal InputActionAsset m_Actions; #if UNITY_EDITOR internal IInputDiagnostics m_Diagnostics; @@ -3714,6 +3735,7 @@ internal struct SerializedState public InputUpdateType updateMask; public InputMetrics metrics; public InputSettings settings; + public InputActionAsset actions; #if UNITY_ANALYTICS || UNITY_EDITOR public bool haveSentStartupAnalytics; @@ -3757,6 +3779,7 @@ internal SerializedState SaveState() updateMask = m_UpdateMask, metrics = m_Metrics, settings = m_Settings, + actions = m_Actions, #if UNITY_ANALYTICS || UNITY_EDITOR haveSentStartupAnalytics = m_HaveSentStartupAnalytics, @@ -3776,6 +3799,10 @@ internal void RestoreStateWithoutDevices(SerializedState state) Object.DestroyImmediate(m_Settings); m_Settings = state.settings; + if (m_Actions != null) + Object.DestroyImmediate(m_Actions); + m_Actions = state.actions; + #if UNITY_ANALYTICS || UNITY_EDITOR m_HaveSentStartupAnalytics = state.haveSentStartupAnalytics; #endif diff --git a/Packages/com.unity.inputsystem/InputSystem/InputSystem.cs b/Packages/com.unity.inputsystem/InputSystem/InputSystem.cs index 250e74f2c6..698f9785c2 100644 --- a/Packages/com.unity.inputsystem/InputSystem/InputSystem.cs +++ b/Packages/com.unity.inputsystem/InputSystem/InputSystem.cs @@ -3011,7 +3011,6 @@ internal static bool ShouldDrawWarningIconForBinding(string bindingPath) #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS - private static InputActionAsset s_ProjectWideActions; internal const string kProjectWideActionsAssetName = "ProjectWideInputActions"; /// @@ -3026,36 +3025,33 @@ internal static bool ShouldDrawWarningIconForBinding(string bindingPath) /// public static InputActionAsset actions { - get - { - if (s_ProjectWideActions != null) - return s_ProjectWideActions; - - #if UNITY_EDITOR - s_ProjectWideActions = ProjectWideActionsAsset.GetOrCreate(); - #else - s_ProjectWideActions = Resources.FindObjectsOfTypeAll().FirstOrDefault(o => o != null && o.name == kProjectWideActionsAssetName); - #endif - - if (s_ProjectWideActions == null) - Debug.LogError($"Couldn't initialize project-wide input actions"); - return s_ProjectWideActions; - } - + get => s_Manager.actions; set { if (value == null) throw new ArgumentNullException(nameof(value)); - if (s_ProjectWideActions == value) + if (s_Manager.m_Actions == value) return; - s_ProjectWideActions?.Disable(); - s_ProjectWideActions = value; - s_ProjectWideActions.Enable(); + // In the editor, we keep track of the appointed project-wide action asset through EditorBuildSettings. +#if UNITY_EDITOR + if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(value))) + { + EditorBuildSettings.AddConfigObject(InputSettingsProvider.kEditorBuildSettingsActionsConfigKey, + value, true); + } +#endif // UNITY_EDITOR + + var current = s_Manager.actions; + if (current != null) + current.Disable(); + s_Manager.actions = value; + if (value != null) + value.Enable(); } } -#endif +#endif // UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS /// /// Event that is signalled when the state of enabled actions in the system changes or @@ -3494,6 +3490,17 @@ internal static void InitializeInEditor(IInputRuntime runtime = null) s_Manager.ApplySettings(); } + // See if we have a saved actions object + if (EditorBuildSettings.TryGetConfigObject(InputSettingsProvider.kEditorBuildSettingsActionsConfigKey, + out InputActionAsset inputActionAsset)) + { + if (s_Manager.m_Actions != null && s_Manager.m_Actions.hideFlags == HideFlags.HideAndDontSave) + ScriptableObject.DestroyImmediate(s_Manager.m_Actions); + s_Manager.m_Actions = inputActionAsset; + + // TODO Let listeners know about the change similar to settings + } + InputEditorUserSettings.Load(); SetUpRemoting(); @@ -3746,13 +3753,14 @@ private static void Reset(bool enableRemoting = false, IInputRuntime runtime = n #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS // Avoid touching the `actions` property directly here, to prevent unwanted initialisation. - if (s_ProjectWideActions) + var projectWideActions = s_Manager?.actions; + if (projectWideActions != null) { - s_ProjectWideActions.Disable(); - s_ProjectWideActions?.OnSetupChanged(); // Cleanup ActionState (remove unused controls after disabling) - s_ProjectWideActions = null; + projectWideActions.Disable(); + projectWideActions.OnSetupChanged(); // Cleanup ActionState (remove unused controls after disabling) + s_Manager.actions = null; } -#endif +#endif // UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS // Some devices keep globals. Get rid of them by pretending the devices // are removed. From 66a8f3a6b18e4b0a89fb375e97510d7dd0b5e405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akan=20Sidenvall?= Date: Wed, 7 Feb 2024 16:07:49 +0100 Subject: [PATCH 02/48] WIP: More changes towards asset based workflow --- Assets/QA/Input_Test/Editor/Utils.cs | 34 +++ Assets/QA/Input_Test/Editor/Utils.cs.meta | 3 + .../CoreTests_ProjectWideActions.cs | 10 +- .../Editor/ActiveAssetEditorHelper.cs | 27 -- .../Editor/ActiveAssetEditorHelper.cs.meta | 3 - .../Editor/AssetImporter/AssetContext.cs | 106 ++++++++ .../Editor/AssetImporter/AssetContext.cs.meta | 3 + .../InputActionAssetIconLoader.cs | 3 + .../AssetImporter/InputActionImporter.cs | 242 +++++++++++------- .../InputActionImporterEditor.cs | 4 +- .../Editor/InputAssetEditorUtils.cs | 92 +++++++ .../Editor/InputAssetEditorUtils.cs.meta | 3 + .../ProjectWideActionsAsset.cs | 206 +++------------ .../PropertyDrawers/InputActionAssetDrawer.cs | 75 +++--- .../InputActionReferenceSearchProviders.cs | 8 +- .../Editor/Settings/InputSettingsProvider.cs | 40 +-- .../UITKAssetEditor/Commands/Commands.cs | 4 +- .../InputActionsEditorSettingsProvider.cs | 39 +++ .../InputActionsEditorWindowUtils.cs | 15 +- .../InputSystem/InputManager.cs | 19 +- .../InputSystem/InputSystem.cs | 29 ++- .../Plugins/PlayerInput/PlayerInput.cs | 3 +- 22 files changed, 579 insertions(+), 389 deletions(-) create mode 100644 Assets/QA/Input_Test/Editor/Utils.cs create mode 100644 Assets/QA/Input_Test/Editor/Utils.cs.meta delete mode 100644 Packages/com.unity.inputsystem/InputSystem/Editor/ActiveAssetEditorHelper.cs delete mode 100644 Packages/com.unity.inputsystem/InputSystem/Editor/ActiveAssetEditorHelper.cs.meta create mode 100644 Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/AssetContext.cs create mode 100644 Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/AssetContext.cs.meta create mode 100644 Packages/com.unity.inputsystem/InputSystem/Editor/InputAssetEditorUtils.cs create mode 100644 Packages/com.unity.inputsystem/InputSystem/Editor/InputAssetEditorUtils.cs.meta diff --git a/Assets/QA/Input_Test/Editor/Utils.cs b/Assets/QA/Input_Test/Editor/Utils.cs new file mode 100644 index 0000000000..063ba3b9f8 --- /dev/null +++ b/Assets/QA/Input_Test/Editor/Utils.cs @@ -0,0 +1,34 @@ +#if UNITY_EDITOR + +using UnityEditor; +using UnityEngine; +using UnityEngine.InputSystem; + +namespace QA.Input_Test.Editor +{ + public static class Utils + { + // Sets Input System project-wide actions to null which is equivalent to user assigning null + [MenuItem("QA Tools/Show Active Input System Project-wide Actions")] + static void ShowInputSystemProjectWideActions() + { + var actions = InputSystem.actions; + if (actions == null) + { + Debug.Log("InputSystem.actions is currently NOT set"); + return; + } + Debug.Log($"InputSystem.actions is currently name: {actions.name}, assetPath: {AssetDatabase.GetAssetPath(actions)}, instanceID: {actions.GetInstanceID()}"); + } + + // Sets Input System project-wide actions to null which is equivalent to user assigning null + [MenuItem("QA Tools/Reset Input System Project-wide Actions")] + static void ResetInputSystemProjectWideActions() + { + InputSystem.actions = null; + Debug.Log("InputSystem.actions successfully reset to null"); + } + } +} + +#endif // UNITY_EDITOR diff --git a/Assets/QA/Input_Test/Editor/Utils.cs.meta b/Assets/QA/Input_Test/Editor/Utils.cs.meta new file mode 100644 index 0000000000..35dff839c8 --- /dev/null +++ b/Assets/QA/Input_Test/Editor/Utils.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0961c85ae676402ea664d9adafe8682e +timeCreated: 1707306889 \ No newline at end of file diff --git a/Assets/Tests/InputSystem/CoreTests_ProjectWideActions.cs b/Assets/Tests/InputSystem/CoreTests_ProjectWideActions.cs index 47a2795dc3..768ee28313 100644 --- a/Assets/Tests/InputSystem/CoreTests_ProjectWideActions.cs +++ b/Assets/Tests/InputSystem/CoreTests_ProjectWideActions.cs @@ -1,3 +1,4 @@ +/* TODO Temporarily disabled #if UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS using System; @@ -162,13 +163,13 @@ public void ProjectWideActionsAsset_CanModifySaveAndLoadAsset() LogAssert.Expect(LogType.Warning, new Regex($"The UI action '{defaultActionName1}' name has been modified")); } - #endif + #endif // UNITY_2023_2_OR_NEWER -#endif +#endif // UNITY_EDITOR [Test] [Category(TestCategory)] - public void ProjectWideActions_AreEnabledByDefault() + public void ProjectWideActions_AreEnabledByDefault() // TODO Expected to fail at the moment { Assert.That(InputSystem.actions, Is.Not.Null); Assert.That(InputSystem.actions.enabled, Is.True); @@ -187,7 +188,7 @@ public void ProjectWideActions_ContainsTemplateActions() #else Assert.That(InputSystem.actions.actionMaps[0].actions.Count, Is.EqualTo(9)); Assert.That(InputSystem.actions.actionMaps[0].actions[0].name, Is.EqualTo("Move")); -#endif +#endif // UNITY_EDITOR } [Test] @@ -256,3 +257,4 @@ public void ProjectWideActions_CanReplaceExistingActions() } #endif +*/ diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/ActiveAssetEditorHelper.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/ActiveAssetEditorHelper.cs deleted file mode 100644 index 1e33f110b3..0000000000 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/ActiveAssetEditorHelper.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using UnityEditor; - -namespace UnityEngine.InputSystem.Editor -{ - internal static class ActiveAssetEditorHelper - { - public static void DrawMakeActiveGui(T current, T target, string targetName, string entity, Action apply) - where T : ScriptableObject - { - if (current == target) - { - EditorGUILayout.HelpBox($"This asset contains the currently active {entity} for the Input System.", MessageType.Info); - return; - } - - string currentlyActiveAssetsPath = null; - if (current != null) - currentlyActiveAssetsPath = AssetDatabase.GetAssetPath(current); - if (!string.IsNullOrEmpty(currentlyActiveAssetsPath)) - currentlyActiveAssetsPath = $"The currently active {entity} are stored in {currentlyActiveAssetsPath}. "; - EditorGUILayout.HelpBox($"Note that this asset does not contain the currently active {entity} for the Input System. {currentlyActiveAssetsPath??""}Click \"Make Active\" below to make \"{targetName}\" the active one.", MessageType.Warning); - if (GUILayout.Button($"Make active", EditorStyles.miniButton)) - apply(target); - } - } -} diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/ActiveAssetEditorHelper.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/ActiveAssetEditorHelper.cs.meta deleted file mode 100644 index bc35c9efc6..0000000000 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/ActiveAssetEditorHelper.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 6260a62900d74d10953648863a1e9c5a -timeCreated: 1707137172 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/AssetContext.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/AssetContext.cs new file mode 100644 index 0000000000..d811eb1ddf --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/AssetContext.cs @@ -0,0 +1,106 @@ +#if UNITY_EDITOR + +using System; +using UnityEditor; +using UnityEditor.AssetImporters; + +namespace UnityEngine.InputSystem.Editor +{ + /// + /// Represents an asset import or creation context. + /// + internal interface IAssetContext + { + /// + /// The associated asset destination path of the main object. + /// + string assetPath { get; } + + /// + /// The associated source path. + /// + string sourcePath { get; } + + /// + /// Adds an object to the asset. + /// + /// Object identifier + /// The object to be added. + /// Optional icon + void AddObjectToAsset(string identifier, Object obj, Texture2D icon = null); + + /// + /// Sets the main object of the asset. + /// + /// The main object of the asset + void SetMainObject(Object obj); + + /// + /// Logs an error associated with the importing or creation of the asset. + /// + /// An error message. Never null. + void LogError(string message); + } + + struct AssetImporterAssetContext : IAssetContext + { + private readonly AssetImportContext m_Context; + + public AssetImporterAssetContext(AssetImportContext context) + { + m_Context = context; + } + + public string assetPath => m_Context.assetPath; + public string sourcePath => m_Context.assetPath; + + public void AddObjectToAsset(string identifier, Object subAsset, Texture2D icon) + { + m_Context.AddObjectToAsset(identifier, subAsset, icon); + } + + public void SetMainObject(Object obj) + { + // Note that importer doesn't need to do any storage + m_Context.SetMainObject(obj); + } + + public void LogError(string message) + { + m_Context.LogImportError(message); + } + } + + struct AssetDatabaseAssetContext : IAssetContext + { + private readonly string m_AssetPath; + private readonly string m_SourcePath; + + public AssetDatabaseAssetContext(string assetPath, string sourcePath) + { + m_AssetPath = assetPath ?? throw new ArgumentNullException(nameof(assetPath)); + m_SourcePath = sourcePath ?? throw new ArgumentNullException(nameof(sourcePath)); + } + + public string assetPath => m_AssetPath; + public string sourcePath => m_SourcePath; + + public void AddObjectToAsset(string identifier, Object subAsset, Texture2D icon) + { + AssetDatabase.AddObjectToAsset(subAsset, m_AssetPath); + } + + public void SetMainObject(Object obj) + { + AssetDatabase.AddObjectToAsset(obj, assetPath); // Note that adding is necessary here compared to importer + AssetDatabase.SetMainObject(obj, m_AssetPath); + } + + public void LogError(string message) + { + Debug.LogError(message); + } + } +} + +#endif // UNITY_EDITOR diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/AssetContext.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/AssetContext.cs.meta new file mode 100644 index 0000000000..c086cca4ed --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/AssetContext.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 625169be36af48e5885dde442e53df85 +timeCreated: 1707298117 \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionAssetIconLoader.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionAssetIconLoader.cs index 6e6ad8a9e3..cca5439155 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionAssetIconLoader.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionAssetIconLoader.cs @@ -3,6 +3,9 @@ namespace UnityEngine.InputSystem.Editor { + // Note that non-existing caching here is intentional since icon selected might be theme dependent. + // There is no reason to cache icons unless there is a significant performance impact on the editor. + /// /// Provides access to icons associated with and . /// diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporter.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporter.cs index 24d528e87d..aec7c96aa3 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporter.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporter.cs @@ -43,59 +43,122 @@ public static event Action onImport remove => s_OnImportCallbacks.Remove(value); } - public override void OnImportAsset(AssetImportContext ctx) + internal static InputActionAsset CreateFromJson(IAssetContext context, string assetName = null, bool addRoot = true) { - if (ctx == null) - throw new ArgumentNullException(nameof(ctx)); - - foreach (var callback in s_OnImportCallbacks) - callback(); + if (context == null) throw new ArgumentNullException(nameof(context)); ////REVIEW: need to check with version control here? - // Read file. - string text; + // Read JSON file. + string content; try { - text = File.ReadAllText(ctx.assetPath); + content = File.ReadAllText(FileUtil.GetPhysicalPath(context.sourcePath)); } catch (Exception exception) { - ctx.LogImportError($"Could not read file '{ctx.assetPath}' ({exception})"); - return; + context.LogError($"Could not read file '{context.sourcePath}' ({exception})"); + return null; } // Create asset. var asset = ScriptableObject.CreateInstance(); - // Parse JSON. + // Parse JSON and configure asset. try { - ////TODO: make sure action names are unique - asset.LoadFromJson(text); + // Attempt to parse JSON + asset.LoadFromJson(content); + + // Make sure action map names are unique within JSON file + var names = new HashSet(); + foreach (var map in asset.actionMaps) + { + if (!names.Add(map.name)) + { + throw new Exception( + "Unable to parse {context.assetPath} due to duplicate Action Map name: '{map.name}'. Make sure Action Map names are unique within the asset and reattempt import."); + } + } + + // Make sure action names are unique within each action map in JSON file + names.Clear(); + foreach (var map in asset.actionMaps) + { + foreach (var action in map.actions) + { + if (!names.Add(action.name)) + { + throw new Exception( + $"Unable to parse {{context.assetPath}} due to duplicate Action name: '{action.name}' within Action Map '{map.name}'. Make sure Action Map names are unique within the asset and reattempt import."); + } + } + + names.Clear(); + } + + // Force name of asset to be that on the file on disk instead of what may be serialized + // as the 'name' property in JSON. (Unless explicitly given) + if (string.IsNullOrEmpty(assetName)) + asset.name = Path.GetFileNameWithoutExtension(context.assetPath); + else + asset.name = assetName; + + // Add asset. + ////REVIEW: the icons won't change if the user changes skin; not sure it makes sense to differentiate here + if (addRoot) + context.AddObjectToAsset("", asset, InputActionAssetIconLoader.LoadAssetIcon()); + context.SetMainObject(asset); + + // Make sure all the elements in the asset have GUIDs and that they are indeed unique. + // Create sub-assets for each action to allow search and editor referencing/picking. + SetupAsset(asset, context.AddObjectToAsset); } catch (Exception exception) { - ctx.LogImportError($"Could not parse input actions in JSON format from '{ctx.assetPath}' ({exception})"); + context.LogError($"Could not parse input actions in JSON format from '{context.sourcePath}' ({exception})"); DestroyImmediate(asset); - return; + return null; } - // Force name of asset to be that on the file on disk instead of what may be serialized - // as the 'name' property in JSON. - asset.name = Path.GetFileNameWithoutExtension(assetPath); + return null; + } - // Load icons. - ////REVIEW: the icons won't change if the user changes skin; not sure it makes sense to differentiate here - var assetIcon = InputActionAssetIconLoader.LoadAssetIcon(); - var actionIcon = InputActionAssetIconLoader.LoadActionIcon(); + public override void OnImportAsset(AssetImportContext ctx) + { + if (ctx == null) + throw new ArgumentNullException(nameof(ctx)); + + foreach (var callback in s_OnImportCallbacks) + callback(); + + var asset = CreateFromJson(new AssetImporterAssetContext(ctx)); + + if (m_GenerateWrapperCode) + GenerateWrapperCode(ctx, asset, m_WrapperCodeNamespace, m_WrapperClassName, m_WrapperCodePath); + + // Refresh editors. + InputActionEditorWindow.RefreshAllOnAssetReimport(); + // TODO UITK editor window is missing + } + + internal static void SetupAsset(InputActionAsset asset) + { + SetupAsset(asset, (identifier, subAsset, icon) => + AssetDatabase.AddObjectToAsset(subAsset, asset)); + } + + private delegate void AddObjectToAsset(string identifier, Object subAsset, Texture2D icon); - // Add asset. - ctx.AddObjectToAsset("", asset, assetIcon); - ctx.SetMainObject(asset); + private static void SetupAsset(InputActionAsset asset, AddObjectToAsset addObjectToAsset) + { + FixMissingGuids(asset); + CreateInputActionReferences(asset, addObjectToAsset); + } + private static void FixMissingGuids(InputActionAsset asset) + { // Make sure all the elements in the asset have GUIDs and that they are indeed unique. - var maps = asset.actionMaps; - foreach (var map in maps) + foreach (var map in asset.actionMaps) { // Make sure action map has GUID. if (string.IsNullOrEmpty(map.m_Id) || asset.actionMaps.Count(x => x.m_Id == map.m_Id) > 1) @@ -117,15 +180,18 @@ public override void OnImportAsset(AssetImportContext ctx) map.m_Bindings[i].GenerateId(); } } + } - // Create subasset for each action. - foreach (var map in maps) + private static void CreateInputActionReferences(InputActionAsset asset, AddObjectToAsset addObjectToAsset) + { + var actionIcon = InputActionAssetIconLoader.LoadActionIcon(); + foreach (var map in asset.actionMaps) { foreach (var action in map.actions) { var actionReference = ScriptableObject.CreateInstance(); actionReference.Set(action); - ctx.AddObjectToAsset(action.m_Id, actionReference, actionIcon); + addObjectToAsset(action.m_Id, actionReference, actionIcon); // Backwards-compatibility (added for 1.0.0-preview.7). // We used to call AddObjectToAsset using objectName instead of action.m_Id as the object name. This fed @@ -141,73 +207,69 @@ public override void OnImportAsset(AssetImportContext ctx) var backcompatActionReference = Instantiate(actionReference); backcompatActionReference.name = actionReference.name; // Get rid of the (Clone) suffix. backcompatActionReference.hideFlags = HideFlags.HideInHierarchy; - ctx.AddObjectToAsset(actionReference.name, backcompatActionReference, actionIcon); + addObjectToAsset(actionReference.name, backcompatActionReference, actionIcon); } } + } - // Generate wrapper code, if enabled. - if (m_GenerateWrapperCode) + private static void GenerateWrapperCode(AssetImportContext ctx, InputActionAsset asset, string codeNamespace, string codeClassName, string codePath) + { + var maps = asset.actionMaps; + // When using code generation, it is an error for any action map to be named the same as the asset itself. + // https://fogbugz.unity3d.com/f/cases/1212052/ + var className = !string.IsNullOrEmpty(codeClassName) ? codeClassName : CSharpCodeHelpers.MakeTypeName(asset.name); + if (maps.Any(x => + CSharpCodeHelpers.MakeTypeName(x.name) == className || CSharpCodeHelpers.MakeIdentifier(x.name) == className)) { - // When using code generation, it is an error for any action map to be named the same as the asset itself. - // https://fogbugz.unity3d.com/f/cases/1212052/ - var className = !string.IsNullOrEmpty(m_WrapperClassName) ? m_WrapperClassName : CSharpCodeHelpers.MakeTypeName(asset.name); - if (maps.Any(x => - CSharpCodeHelpers.MakeTypeName(x.name) == className || CSharpCodeHelpers.MakeIdentifier(x.name) == className)) - { - ctx.LogImportError( - $"{asset.name}: An action map in an .inputactions asset cannot be named the same as the asset itself if 'Generate C# Class' is used. " - + "You can rename the action map in the asset, rename the asset itself or assign a different C# class name in the import settings."); - } - else - { - var wrapperFilePath = m_WrapperCodePath; - if (string.IsNullOrEmpty(wrapperFilePath)) - { - // Placed next to .inputactions file. - var assetPath = ctx.assetPath; - var directory = Path.GetDirectoryName(assetPath); - var fileName = Path.GetFileNameWithoutExtension(assetPath); - wrapperFilePath = Path.Combine(directory, fileName) + ".cs"; - } - else if (wrapperFilePath.StartsWith("./") || wrapperFilePath.StartsWith(".\\") || - wrapperFilePath.StartsWith("../") || wrapperFilePath.StartsWith("..\\")) - { - // User-specified file relative to location of .inputactions file. - var assetPath = ctx.assetPath; - var directory = Path.GetDirectoryName(assetPath); - wrapperFilePath = Path.Combine(directory, wrapperFilePath); - } - else if (!wrapperFilePath.ToLower().StartsWith("assets/") && - !wrapperFilePath.ToLower().StartsWith("assets\\")) - { - // User-specified file in Assets/ folder. - wrapperFilePath = Path.Combine("Assets", wrapperFilePath); - } + ctx.LogImportError( + $"{asset.name}: An action map in an .inputactions asset cannot be named the same as the asset itself if 'Generate C# Class' is used. " + + "You can rename the action map in the asset, rename the asset itself or assign a different C# class name in the import settings."); + return; + } + + var wrapperFilePath = codePath; + if (string.IsNullOrEmpty(wrapperFilePath)) + { + // Placed next to .inputactions file. + var assetPath = ctx.assetPath; + var directory = Path.GetDirectoryName(assetPath); + var fileName = Path.GetFileNameWithoutExtension(assetPath); + wrapperFilePath = Path.Combine(directory, fileName) + ".cs"; + } + else if (wrapperFilePath.StartsWith("./") || wrapperFilePath.StartsWith(".\\") || + wrapperFilePath.StartsWith("../") || wrapperFilePath.StartsWith("..\\")) + { + // User-specified file relative to location of .inputactions file. + var assetPath = ctx.assetPath; + var directory = Path.GetDirectoryName(assetPath); + wrapperFilePath = Path.Combine(directory, wrapperFilePath); + } + else if (!wrapperFilePath.ToLower().StartsWith("assets/") && + !wrapperFilePath.ToLower().StartsWith("assets\\")) + { + // User-specified file in Assets/ folder. + wrapperFilePath = Path.Combine("Assets", wrapperFilePath); + } - var dir = Path.GetDirectoryName(wrapperFilePath); - if (!Directory.Exists(dir)) - Directory.CreateDirectory(dir); + var dir = Path.GetDirectoryName(wrapperFilePath); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); - var options = new InputActionCodeGenerator.Options - { - sourceAssetPath = ctx.assetPath, - namespaceName = m_WrapperCodeNamespace, - className = m_WrapperClassName, - }; + var options = new InputActionCodeGenerator.Options + { + sourceAssetPath = ctx.assetPath, + namespaceName = codeNamespace, + className = codeClassName, + }; - if (InputActionCodeGenerator.GenerateWrapperCode(wrapperFilePath, asset, options)) - { - // When we generate the wrapper code cs file during asset import, we cannot call ImportAsset on that directly because - // script assets have to be imported before all other assets, and are not allowed to be added to the import queue during - // asset import. So instead we register a callback to trigger a delayed asset refresh which should then pick up the - // changed/added script, and trigger a new import. - EditorApplication.delayCall += AssetDatabase.Refresh; - } - } + if (InputActionCodeGenerator.GenerateWrapperCode(wrapperFilePath, asset, options)) + { + // When we generate the wrapper code cs file during asset import, we cannot call ImportAsset on that directly because + // script assets have to be imported before all other assets, and are not allowed to be added to the import queue during + // asset import. So instead we register a callback to trigger a delayed asset refresh which should then pick up the + // changed/added script, and trigger a new import. + EditorApplication.delayCall += AssetDatabase.Refresh; } - - // Refresh editors. - InputActionEditorWindow.RefreshAllOnAssetReimport(); } internal static IEnumerable LoadInputActionReferencesFromAsset(InputActionAsset asset) diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporterEditor.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporterEditor.cs index 98ca49e182..8bef40dcf5 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporterEditor.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/AssetImporter/InputActionImporterEditor.cs @@ -29,7 +29,7 @@ public override void OnInspectorGUI() EditorGUILayout.Space(); - if (inputActionAsset == null) // TODO Why would this ever happen? + if (inputActionAsset == null) EditorGUILayout.HelpBox("The currently selected object is not an editable input action asset.", MessageType.Info); @@ -43,7 +43,7 @@ public override void OnInspectorGUI() EditorGUILayout.Space(); // Project-wide Input Actions Asset UI. - ActiveAssetEditorHelper.DrawMakeActiveGui(InputSystem.actions, inputActionAsset, + InputAssetEditorUtils.DrawMakeActiveGui(InputSystem.actions, inputActionAsset, inputActionAsset.name, "Project-wide Input Actions", (value) => InputSystem.actions = value); EditorGUILayout.Space(); diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/InputAssetEditorUtils.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/InputAssetEditorUtils.cs new file mode 100644 index 0000000000..41edf88421 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/InputAssetEditorUtils.cs @@ -0,0 +1,92 @@ +#if UNITY_EDITOR + +using System; +using System.IO; +using UnityEditor; + +namespace UnityEngine.InputSystem.Editor +{ + internal static class InputAssetEditorUtils + { + internal enum DialogResult + { + InvalidPath, + Cancelled, + Valid + } + + internal struct PromptResult + { + public PromptResult(DialogResult result, string path) + { + this.result = result; + this.relativePath = path; + } + + public readonly DialogResult result; + public readonly string relativePath; + } + + internal static string MakeProjectFileName(string projectNameSuffixNoExtension) + { + return PlayerSettings.productName + "." + projectNameSuffixNoExtension; + } + + internal static PromptResult PromptUserForAsset(string friendlyName, string suggestedAssetFilePathWithoutExtension, string assetFileExtension) + { + // Prompt user for a file name. + var fullAssetFileExtension = "." + assetFileExtension; + var path = EditorUtility.SaveFilePanel( + title: $"Create {friendlyName} File", + directory: "Assets", + defaultName: suggestedAssetFilePathWithoutExtension + "." + assetFileExtension, + extension: assetFileExtension); + if (string.IsNullOrEmpty(path)) + return new PromptResult(DialogResult.Cancelled, null); + + // Make sure the path is in the Assets/ folder. + path = path.Replace("\\", "/"); // Make sure we only get '/' separators. + var dataPath = Application.dataPath + "/"; + if (!path.StartsWith(dataPath, StringComparison.CurrentCultureIgnoreCase)) + { + Debug.LogError($"{friendlyName} must be stored in Assets folder of the project (got: '{path}')"); + return new PromptResult(DialogResult.InvalidPath, null); + } + + // Make sure path ends with expected extension + var extension = Path.GetExtension(path); + if (string.Compare(extension, fullAssetFileExtension, StringComparison.InvariantCultureIgnoreCase) != 0) + path += fullAssetFileExtension; + + return new PromptResult(DialogResult.Valid, "Assets/" + path.Substring(dataPath.Length)); + } + + internal static T CreateAsset(T asset, string relativePath) where T : ScriptableObject + { + AssetDatabase.CreateAsset(asset, relativePath); + EditorGUIUtility.PingObject(asset); + return asset; + } + + public static void DrawMakeActiveGui(T current, T target, string targetName, string entity, Action apply) + where T : ScriptableObject + { + if (current == target) + { + EditorGUILayout.HelpBox($"This asset contains the currently active {entity} for the Input System.", MessageType.Info); + return; + } + + string currentlyActiveAssetsPath = null; + if (current != null) + currentlyActiveAssetsPath = AssetDatabase.GetAssetPath(current); + if (!string.IsNullOrEmpty(currentlyActiveAssetsPath)) + currentlyActiveAssetsPath = $"The currently active {entity} are stored in {currentlyActiveAssetsPath}. "; + EditorGUILayout.HelpBox($"Note that this asset does not contain the currently active {entity} for the Input System. {currentlyActiveAssetsPath??""}Click \"Make Active\" below to make \"{targetName}\" the active one.", MessageType.Warning); + if (GUILayout.Button($"Make active", EditorStyles.miniButton)) + apply(target); + } + } +} + +#endif // UNITY_EDITOR diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/InputAssetEditorUtils.cs.meta b/Packages/com.unity.inputsystem/InputSystem/Editor/InputAssetEditorUtils.cs.meta new file mode 100644 index 0000000000..96efb86512 --- /dev/null +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/InputAssetEditorUtils.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 64d9d71f43124cea89b28b356e84a412 +timeCreated: 1707258043 \ 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 861f69ac1d..f78fee5585 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsAsset.cs @@ -8,113 +8,61 @@ namespace UnityEngine.InputSystem.Editor { - internal static class ProjectWideActionsAsset + // Placeholder for converting InputManager.asset actions into regular asset to support conversion from 1.8.0-pre1 and 1.8.0-pre2 to asset based Project-wide actions + // TODO Currently not used + internal static class ProjectSettingsProjectWideActionsAssetConverter { - 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; - -#if UNITY_INCLUDE_TESTS - internal static void SetAssetPaths(string defaultAssetPath, string assetPath) - { - s_DefaultAssetPath = defaultAssetPath; - s_AssetPath = assetPath; - } - internal static void Reset() - { - s_DefaultAssetPath = kDefaultAssetPath; - s_AssetPath = kAssetPath; - } - -#endif - - [InitializeOnLoadMethod] - internal static void InstallProjectWideActions() - { - GetOrCreate(); - } - - 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(); - } + // TODO 1. Implement reading the kAssetPath into InputActionAsset. + // TODO 2. Serialize as JSON and write as an .inputactions file into Asset directory. + // TODO Consider preserving GUIDs to potentially enable references to stay intact. + // TODO 3. Let InputActionImporter do its job on importing and configuring the asset. + // TODO 4. Assign to InputSystem.actions - internal static InputActionAsset CreateNewActionAsset() + internal static void DeleteActionAssetAndActionReferences() { - // Always clean out old actions asset and action references first before we add new - DeleteActionAssetAndActionReferences(); - - // Create new asset data - var json = File.ReadAllText(FileUtil.GetPhysicalPath(s_DefaultAssetPath)); - - var asset = ScriptableObject.CreateInstance(); - asset.LoadFromJson(json); - asset.name = kAssetName; + var objects = AssetDatabase.LoadAllAssetsAtPath(kAssetPath); + if (objects == null) + return; - AssetDatabase.AddObjectToAsset(asset, s_AssetPath); - - // Make sure all the elements in the asset have GUIDs and that they are indeed unique. - var maps = asset.actionMaps; - foreach (var map in maps) + // Handle deleting all InputActionAssets as older 1.8.0 pre release could create more than one project wide input asset in the file + foreach (var obj in objects) { - // Make sure action map has GUID. - if (string.IsNullOrEmpty(map.m_Id) || asset.actionMaps.Count(x => x.m_Id == map.m_Id) > 1) - map.GenerateId(); - - // Make sure all actions have GUIDs. - foreach (var action in map.actions) + if (obj is InputActionReference) { - var actionId = action.m_Id; - if (string.IsNullOrEmpty(actionId) || asset.actionMaps.Sum(m => m.actions.Count(a => a.m_Id == actionId)) > 1) - action.GenerateId(); + var actionReference = obj as InputActionReference; + AssetDatabase.RemoveObjectFromAsset(obj); + Object.DestroyImmediate(actionReference); } - - // Make sure all bindings have GUIDs. - for (var i = 0; i < map.m_Bindings.LengthSafe(); ++i) + else if (obj is InputActionAsset) { - var bindingId = map.m_Bindings[i].m_Id; - if (string.IsNullOrEmpty(bindingId) || asset.actionMaps.Sum(m => m.bindings.Count(b => b.m_Id == bindingId)) > 1) - map.m_Bindings[i].GenerateId(); + AssetDatabase.RemoveObjectFromAsset(obj); } } + } + } - CreateInputActionReferences(asset); - AssetDatabase.SaveAssets(); + internal static class ProjectWideActionsAsset + { + internal const string kDefaultAssetPath = "Packages/com.unity.inputsystem/InputSystem/Editor/ProjectWideActions/ProjectWideActionsTemplate.json"; - return asset; + internal static void CreateNewAsset(string relativePath, string sourcePath) + { + // Note that we only copy file here and let the InputActionImporter handle the asset management + File.Copy(FileUtil.GetPhysicalPath(sourcePath), FileUtil.GetPhysicalPath(relativePath), overwrite: true); } - internal static InputActionMap GetDefaultUIActionMap() + internal static void CreateNewAsset(string relativePath) { - var json = File.ReadAllText(FileUtil.GetPhysicalPath(s_DefaultAssetPath)); - var actionMaps = InputActionMap.FromJson(json); - return actionMaps[actionMaps.IndexOf(x => x.name == "UI")]; + CreateNewAsset(relativePath, kDefaultAssetPath); } - private static void CreateInputActionReferences(InputActionAsset asset) + internal static InputActionMap GetDefaultUIActionMap() { - var maps = asset.actionMaps; - foreach (var map in maps) - { - foreach (var action in map.actions) - { - var actionReference = ScriptableObject.CreateInstance(); - actionReference.Set(action); - AssetDatabase.AddObjectToAsset(actionReference, asset); - } - } + var json = File.ReadAllText(FileUtil.GetPhysicalPath(kDefaultAssetPath)); + var actionMaps = InputActionMap.FromJson(json); + return actionMaps[actionMaps.IndexOf(x => x.name == "UI")]; } #if UNITY_2023_2_OR_NEWER @@ -122,9 +70,8 @@ private static void CreateInputActionReferences(InputActionAsset asset) /// Checks if the default UI action map has been modified or removed, to let the user know if their changes will /// break the UI input at runtime, when using the UI Toolkit. /// - internal static void CheckForDefaultUIActionMapChanges() + internal static void CheckForDefaultUIActionMapChanges(InputActionAsset asset) { - var asset = GetOrCreate(); if (asset != null) { var defaultUIActionMap = GetDefaultUIActionMap(); @@ -152,85 +99,12 @@ internal static void CheckForDefaultUIActionMapChanges() #endif /// - /// Reset project wide input actions asset - /// - internal static void ResetActionAsset() - { - CreateNewActionAsset(); - } - - /// - /// Delete project wide input actions - /// - internal static void DeleteActionAssetAndActionReferences() - { - var objects = AssetDatabase.LoadAllAssetsAtPath(s_AssetPath); - if (objects != null) - { - // Handle deleting all InputActionAssets as older 1.8.0 pre release could create more than one project wide input asset in the file - foreach (var obj in objects) - { - if (obj is InputActionReference) - { - var actionReference = obj as InputActionReference; - actionReference.Set(null); // TODO This is wrong it should be destroyed?! - AssetDatabase.RemoveObjectFromAsset(obj); - } - else if (obj is InputActionAsset) - { - AssetDatabase.RemoveObjectFromAsset(obj); - } - } - } - } - - /// - /// Updates the input action references in the asset by updating names, removing dangling references - /// and adding new ones. + /// Reset the given asset to Project-wide Input Action asset defaults /// - internal static void UpdateInputActionReferences() + internal static void ResetActionAsset(InputActionAsset asset) { - var asset = GetOrCreate(); - var existingReferences = InputActionImporter.LoadInputActionReferencesFromAsset(asset).ToList(); - - // Check if referenced input action exists in the asset and remove the reference if it doesn't. - foreach (var actionReference in existingReferences) - { - if (actionReference.action != null && asset.FindAction(actionReference.action.id) == null) - { - actionReference.Set(null); - AssetDatabase.RemoveObjectFromAsset(actionReference); - } - } - - // Check if all actions have a reference - foreach (var action in asset) - { - // Catch error that's possible to appear in previous versions of the package. - if (action.actionMap.m_Asset == null) - action.actionMap.m_Asset = 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); - } - } - } - - AssetDatabase.SaveAssets(); + var path = AssetDatabase.GetAssetPath(asset); + // TODO Overwrite and let importer handle it? } } } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionAssetDrawer.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionAssetDrawer.cs index a31d736f9a..c79dc0ede9 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionAssetDrawer.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionAssetDrawer.cs @@ -22,36 +22,51 @@ enum AssetOptions [CustomPropertyDrawer(typeof(InputActionAsset))] internal class InputActionAssetDrawer : PropertyDrawer { - static readonly string[] k_ActionsTypeOptions = new[] { "Project-Wide Actions", "Actions Asset" }; + //static readonly string[] k_ActionsTypeOptions = new[] { "Project-Wide Actions", "Actions Asset" }; public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, label, property); - var isAssetProjectWideActions = IsAssetProjectWideActions(property); - var selectedAssetOptionIndex = isAssetProjectWideActions ? AssetOptions.ProjectWideActions : AssetOptions.ActionsAsset; + var actions = InputSystem.actions; + var hasProjectWideActions = !ReferenceEquals(actions, null); + var current = property.objectReferenceValue as InputActionAsset; + var currentIsProjectWideActions = (hasProjectWideActions) && current == InputSystem.actions; + var currentSelectedAssetOptionIndex = (currentIsProjectWideActions) ? AssetOptions.ProjectWideActions : AssetOptions.ActionsAsset; - EditorGUILayout.BeginHorizontal(); - // Draw dropdown menu to select between using project-wide actions or an action asset - var selected = (AssetOptions)EditorGUILayout.EnumPopup(label, selectedAssetOptionIndex); - // Draw button to edit the asset - DoOpenAssetButtonUI(property, selected); - EditorGUILayout.EndHorizontal(); + var selected = AssetOptions.ActionsAsset; + if (hasProjectWideActions) + { + EditorGUILayout.BeginHorizontal(); + + // Draw dropdown menu to select between using project-wide actions or an action asset + selected = (AssetOptions)EditorGUILayout.EnumPopup(label, currentSelectedAssetOptionIndex); + + // Draw button to edit the asset + DoOpenAssetButtonUI(property, selected); + + EditorGUILayout.EndHorizontal(); + } // Update property in case there's a change in the dropdown popup - if (selectedAssetOptionIndex != selected) + if (currentSelectedAssetOptionIndex != selected) { - UpdatePropertyWithSelectedOption(property, selected); - selectedAssetOptionIndex = selected; + if (selected == AssetOptions.ProjectWideActions) + property.objectReferenceValue = actions; + else + property.objectReferenceValue = null; + property.serializedObject.ApplyModifiedProperties(); } // Show relevant UI elements depending on the option selected // In case project-wide actions are selected, the object picker is not shown. - if (selectedAssetOptionIndex == AssetOptions.ActionsAsset) + if (selected == AssetOptions.ActionsAsset) { - ++EditorGUI.indentLevel; + if (hasProjectWideActions) + ++EditorGUI.indentLevel; EditorGUILayout.PropertyField(property, new GUIContent("Actions Asset") , true); - --EditorGUI.indentLevel; + if (hasProjectWideActions) + --EditorGUI.indentLevel; } EditorGUI.EndProperty(); @@ -69,36 +84,6 @@ static void DoOpenAssetButtonUI(SerializedProperty property, AssetOptions select SettingsService.OpenProjectSettings(InputActionsEditorSettingsProvider.kSettingsPath); } } - - static void UpdatePropertyWithSelectedOption(SerializedProperty assetProperty, AssetOptions selected) - { - if (selected == AssetOptions.ProjectWideActions) - { - assetProperty.objectReferenceValue = ProjectWideActionsAsset.GetOrCreate(); - } - else - { - // Reset the actions asset to null if the first time user selects the "Actions Asset" option - assetProperty.objectReferenceValue = null; - } - - 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/InputActionReferenceSearchProviders.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferenceSearchProviders.cs index 8830de0a41..49b8a051ea 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferenceSearchProviders.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferenceSearchProviders.cs @@ -42,7 +42,13 @@ internal static SearchProvider CreateInputActionReferenceSearchProviderForProjec return CreateInputActionReferenceSearchProvider(k_ProjectWideActionsSearchProviderId, "Project-Wide Input Actions", (obj) => "(Project-Wide Input Actions)", - () => InputActionImporter.LoadInputActionReferencesFromAsset(ProjectWideActionsAsset.GetOrCreate())); + () => + { + var asset = InputSystem.actions; + if (asset == null) + return Array.Empty(); + return InputActionImporter.LoadInputActionReferencesFromAsset(asset); + }); } private static SearchProvider CreateInputActionReferenceSearchProvider(string id, string displayName, diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs index 5375434212..87166d469f 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Settings/InputSettingsProvider.cs @@ -191,41 +191,19 @@ private static void ShowPlatformSettings() private static void CreateNewSettingsAsset(string relativePath) { - // Create settings file. - var settings = ScriptableObject.CreateInstance(); - AssetDatabase.CreateAsset(settings, relativePath); - EditorGUIUtility.PingObject(settings); - // Install the settings. This will lead to an InputSystem.onSettingsChange event which in turn + // Create and install the settings. This will lead to an InputSystem.onSettingsChange event which in turn // will cause us to re-initialize. - InputSystem.settings = settings; + InputSystem.settings = InputAssetEditorUtils.CreateAsset(ScriptableObject.CreateInstance(), relativePath); } private static void CreateNewSettingsAsset() { - // Query for file name. - var projectName = PlayerSettings.productName; - var path = EditorUtility.SaveFilePanel("Create Input Settings File", "Assets", - projectName + ".inputsettings", "asset"); - if (string.IsNullOrEmpty(path)) - return; - - // Make sure the path is in the Assets/ folder. - path = path.Replace("\\", "/"); // Make sure we only get '/' separators. - var dataPath = Application.dataPath + "/"; - if (!path.StartsWith(dataPath, StringComparison.CurrentCultureIgnoreCase)) - { - Debug.LogError($"Input settings must be stored in Assets folder of the project (got: '{path}')"); - return; - } - - // Make sure it ends with .asset. - var extension = Path.GetExtension(path); - if (string.Compare(extension, ".asset", StringComparison.InvariantCultureIgnoreCase) != 0) - path += ".asset"; - - // Create settings file. - var relativePath = "Assets/" + path.Substring(dataPath.Length); - CreateNewSettingsAsset(relativePath); + var result = InputAssetEditorUtils.PromptUserForAsset( + friendlyName: "Input Settings", + suggestedAssetFilePathWithoutExtension: InputAssetEditorUtils.MakeProjectFileName("inputsettings"), + assetFileExtension: "asset"); + if (result.result == InputAssetEditorUtils.DialogResult.Valid) + CreateNewSettingsAsset(result.relativePath); } private void InitializeWithCurrentSettingsIfNecessary() @@ -494,7 +472,7 @@ public override void OnInspectorGUI() EditorGUILayout.Space(); - ActiveAssetEditorHelper.DrawMakeActiveGui(InputSystem.settings, target as InputSettings, + InputAssetEditorUtils.DrawMakeActiveGui(InputSystem.settings, target as InputSettings, target.name, "settings", (value) => InputSystem.settings = value); } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Commands/Commands.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Commands/Commands.cs index d0eed0dce2..95cde7b8a1 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Commands/Commands.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Commands/Commands.cs @@ -425,9 +425,11 @@ public static Command ResetGlobalInputAsset(Action postResetAc { return (in InputActionsEditorState state) => { + // TODO Here we would instead modify the asset based on an instance loaded from defaults. Note that we cannot use importer here since we modify an in-memory asset as serialized object.... needs some thought to not break Undo. + /*ProjectWideActionsAsset.ResetActionAsset(); ProjectWideActionsAsset.ResetActionAsset(); var asset = ProjectWideActionsAsset.GetOrCreate(); - postResetAction?.Invoke(asset); + postResetAction?.Invoke(asset);*/ return state; }; } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs index fb6debbda9..29ad012717 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorSettingsProvider.cs @@ -1,6 +1,7 @@ #if UNITY_EDITOR && UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS using System.Collections.Generic; using UnityEditor; +using UnityEditor.AssetImporters; using UnityEngine.UIElements; namespace UnityEngine.InputSystem.Editor @@ -9,6 +10,10 @@ namespace UnityEngine.InputSystem.Editor internal class InputActionsEditorSettingsProvider : SettingsProvider { + private class ImportDetector : AssetPostprocessor + { + } + public const string kSettingsPath = InputSettingsPath.kSettingsRootPath; [SerializeField] InputActionsEditorState m_State; @@ -106,6 +111,40 @@ private void BuildUI() view.postResetAction += OnResetAsset; m_StateContainer.Initialize(); } + else + { + // TODO This is a very temporary solution, it will be reworked before this lands in any shape or form + Button button = new Button(); + button.name = "createProjectWideInputActionsAssetButton"; + button.text = "Create a new Project-wide Input Actions Asset"; + button.RegisterCallback(evt => + { + var result = InputAssetEditorUtils.PromptUserForAsset( + friendlyName: "Input Actions", + suggestedAssetFilePathWithoutExtension: InputAssetEditorUtils.MakeProjectFileName("Actions"), + assetFileExtension: "inputactions"); + if (result.result != InputAssetEditorUtils.DialogResult.Valid) + return; // Either invalid path selected or cancelled by user + + // Create a new asset + ProjectWideActionsAsset.CreateNewAsset(result.relativePath); + + // Refresh asset database to allow for importer to recognize the asset + AssetDatabase.Refresh(); + + // Load the asset we just created and assign it as the Project-wide actions + var asset = AssetDatabase.LoadAssetAtPath(result.relativePath); + if (asset != null) + InputSystem.actions = asset; + + // TODO This is not how this should be done, it should instead be triggered by InputSystem.actions being assigned since this might also happen from user code + //m_RootVisualElement.Remove(button); // TODO Why a problem?! + m_State = new InputActionsEditorState(new SerializedObject(asset)); + BuildUI(); + }); + + m_RootVisualElement.Add(button); + } // Hide the save / auto save buttons in the project wide input actions // Project wide input actions always auto save diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindowUtils.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindowUtils.cs index c22c899afc..282e71ee6a 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindowUtils.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindowUtils.cs @@ -15,16 +15,11 @@ internal class InputActionsEditorWindowUtils public static void SaveAsset(SerializedObject serializedAsset) { var asset = (InputActionAsset)serializedAsset.targetObject; - // For project-wide actions asset save works differently. The asset is in YAML format, not JSON. - if (asset.name == ProjectWideActionsAsset.kAssetName) - { -#if UNITY_2023_2_OR_NEWER - ProjectWideActionsAsset.CheckForDefaultUIActionMapChanges(); -#endif - ProjectWideActionsAsset.UpdateInputActionReferences(); - AssetDatabase.SaveAssets(); - return; - } + SaveAsset(asset); + } + + public static void SaveAsset(InputActionAsset asset) + { var assetPath = AssetDatabase.GetAssetPath(asset); var assetJson = asset.ToJson(); var existingJson = File.Exists(assetPath) ? File.ReadAllText(assetPath) : ""; diff --git a/Packages/com.unity.inputsystem/InputSystem/InputManager.cs b/Packages/com.unity.inputsystem/InputSystem/InputManager.cs index f0682c3378..084ffb9862 100644 --- a/Packages/com.unity.inputsystem/InputSystem/InputManager.cs +++ b/Packages/com.unity.inputsystem/InputSystem/InputManager.cs @@ -115,14 +115,11 @@ public InputActionAsset actions set { - if (value == null) - throw new ArgumentNullException(nameof(value)); - if (m_Actions == value) return; m_Actions = value; - // ApplyActions(); + ApplyActions(); } } @@ -252,6 +249,12 @@ public event Action onSettingsChange remove => m_SettingsChangedListeners.RemoveCallback(value); } + public event Action onActionsChange + { + add => m_ActionsChangedListeners.AddCallback(value); + remove => m_ActionsChangedListeners.RemoveCallback(value); + } + public bool isProcessingEvents => m_InputEventStream.isOpen; #if UNITY_EDITOR @@ -1775,6 +1778,7 @@ internal void Initialize(IInputRuntime runtime, InputSettings settings) InstallGlobals(); ApplySettings(); + ApplyActions(); } internal void Destroy() @@ -2048,6 +2052,7 @@ internal struct AvailableDevice private CallbackArray m_BeforeUpdateListeners; private CallbackArray m_AfterUpdateListeners; private CallbackArray m_SettingsChangedListeners; + private CallbackArray m_ActionsChangedListeners; private bool m_NativeBeforeUpdateHooked; private bool m_HaveDevicesWithStateCallbackReceivers; private bool m_HasFocus; @@ -2632,6 +2637,12 @@ internal void ApplySettings() "InputSystem.onSettingsChange"); } + internal void ApplyActions() + { + // Let listeners know. + DelegateHelpers.InvokeCallbacksSafe(ref m_ActionsChangedListeners, "InputSystem.onActionsChange"); + } + internal unsafe long ExecuteGlobalCommand(ref TCommand command) where TCommand : struct, IInputDeviceCommandInfo { diff --git a/Packages/com.unity.inputsystem/InputSystem/InputSystem.cs b/Packages/com.unity.inputsystem/InputSystem/InputSystem.cs index 698f9785c2..096832bc35 100644 --- a/Packages/com.unity.inputsystem/InputSystem/InputSystem.cs +++ b/Packages/com.unity.inputsystem/InputSystem/InputSystem.cs @@ -3013,6 +3013,8 @@ internal static bool ShouldDrawWarningIconForBinding(string bindingPath) internal const string kProjectWideActionsAssetName = "ProjectWideInputActions"; + internal static bool hasActions => s_Manager.actions != null; + /// /// An input action asset (see ) which is always available by default. /// @@ -3028,21 +3030,27 @@ public static InputActionAsset actions get => s_Manager.actions; set { - if (value == null) - throw new ArgumentNullException(nameof(value)); + //if (value == null) + // throw new ArgumentNullException(nameof(value)); if (s_Manager.m_Actions == value) return; - // In the editor, we keep track of the appointed project-wide action asset through EditorBuildSettings. #if UNITY_EDITOR - if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(value))) + // In the editor, we keep track of the appointed project-wide action asset through EditorBuildSettings. + // Note that if set to null we need to remove the config object to not act as a broken reference. + if (value != null && !string.IsNullOrEmpty(AssetDatabase.GetAssetPath(value))) { EditorBuildSettings.AddConfigObject(InputSettingsProvider.kEditorBuildSettingsActionsConfigKey, value, true); } + else + { + EditorBuildSettings.RemoveConfigObject(InputSettingsProvider.kEditorBuildSettingsActionsConfigKey); + } #endif // UNITY_EDITOR + // Update underlying value var current = s_Manager.actions; if (current != null) current.Disable(); @@ -3051,6 +3059,19 @@ public static InputActionAsset actions value.Enable(); } } + + /// + /// Event that is triggered if any of the maps, actions or bindings in changes or if + /// is replaced entirely with a new object. + /// + /// + /// + public static event Action onActionsChange + { + add => s_Manager.onActionsChange += value; + remove => s_Manager.onActionsChange -= value; + } + #endif // UNITY_INPUT_SYSTEM_PROJECT_WIDE_ACTIONS /// diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInput.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInput.cs index e541036e9e..0dbfc82e16 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInput.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/PlayerInput/PlayerInput.cs @@ -1615,7 +1615,8 @@ private void AssignPlayerIndex() void Reset() { // Set default actions to project wide actions. - m_Actions = ProjectWideActionsAsset.GetOrCreate(); + m_Actions = InputSystem.actions; + // TODO Need to monitor changes? } #endif From 879857d4f6d1dd8151ea8a480197a5cec51eec56 Mon Sep 17 00:00:00 2001 From: Lyndon Homewood Date: Thu, 8 Feb 2024 15:23:10 +0000 Subject: [PATCH 03/48] Fixes to allow to compile --- .../PropertyDrawers/InputActionReferencePropertyDrawer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferencePropertyDrawer.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferencePropertyDrawer.cs index 3186315b9f..d880ccca2a 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferencePropertyDrawer.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/PropertyDrawers/InputActionReferencePropertyDrawer.cs @@ -1,7 +1,6 @@ // 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; @@ -33,6 +32,7 @@ static void ValidatePropertyWithDanglingInputActionReferences(SerializedProperty { if (property?.objectReferenceValue is InputActionReference reference) { +/* // Check only if the reference is a project-wide action. if (reference?.asset?.name == ProjectWideActionsAsset.kAssetName) { @@ -43,6 +43,7 @@ static void ValidatePropertyWithDanglingInputActionReferences(SerializedProperty property.serializedObject.ApplyModifiedProperties(); } } +*/ } } } From 5aba932b9cd79432bfef5aad90adde0900a69a6d Mon Sep 17 00:00:00 2001 From: Lyndon Homewood Date: Fri, 9 Feb 2024 10:06:21 +0000 Subject: [PATCH 04/48] Improved UX for the project wide input actions in project settings --- .../InputActionsEditorConstants.cs | 1 + .../InputActionsEditorWindow.cs | 2 +- .../Resources/InputActionsEditor.uxml | 8 +++--- .../InputActionsProjectSettings.uxml | 10 +++++++ .../InputActionsProjectSettings.uxml.meta | 10 +++++++ .../Views/InputActionsEditorView.cs | 26 +++++++++++++------ 6 files changed, 43 insertions(+), 14 deletions(-) create mode 100644 Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsProjectSettings.uxml create mode 100644 Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsProjectSettings.uxml.meta diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorConstants.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorConstants.cs index 7876ec2a08..c9e922d2fe 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorConstants.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorConstants.cs @@ -7,6 +7,7 @@ internal class InputActionsEditorConstants public const string ResourcesPath = "/InputSystem/Editor/UITKAssetEditor/Resources"; /// Template names + public const string ProjectSettingsUxml = "/InputActionsProjectSettings.uxml"; public const string MainEditorViewNameUxml = "/InputActionsEditor.uxml"; public const string BindingsPanelRowTemplateUxml = "/BindingPanelRowTemplate.uxml"; public const string NameAndParametersListViewItemUxml = "/NameAndParameterListViewItemTemplate.uxml"; diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindow.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindow.cs index 5c3df016b4..c4e90bb10b 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindow.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/InputActionsEditorWindow.cs @@ -171,7 +171,7 @@ private void BuildUI() stateContainer.StateChanged += OnStateChanged; rootVisualElement.styleSheets.Add(InputActionsEditorWindowUtils.theme); - var view = new InputActionsEditorView(rootVisualElement, stateContainer); + var view = new InputActionsEditorView(rootVisualElement, stateContainer, false); view.postSaveAction += PostSaveAction; stateContainer.Initialize(); } diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsEditor.uxml b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsEditor.uxml index 3e7f5d36c6..a1145b11d2 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsEditor.uxml +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/UITKAssetEditor/Resources/InputActionsEditor.uxml @@ -1,16 +1,14 @@