diff --git a/Assets/Tests/InputSystem/Plugins/InputForUITests.cs b/Assets/Tests/InputSystem/Plugins/InputForUITests.cs index 0cb49e7171..98720c0ee4 100644 --- a/Assets/Tests/InputSystem/Plugins/InputForUITests.cs +++ b/Assets/Tests/InputSystem/Plugins/InputForUITests.cs @@ -37,6 +37,7 @@ public class InputForUITests : InputTestFixture readonly List m_InputForUIEvents = new List(); private int m_CurrentInputEventToCheck; InputSystemProvider m_InputSystemProvider; + private bool m_ClearedMockProvider; private InputActionAsset storedActions; @@ -45,6 +46,7 @@ public override void Setup() { base.Setup(); m_CurrentInputEventToCheck = 0; + m_ClearedMockProvider = false; storedActions = InputSystem.actions; @@ -59,9 +61,11 @@ public override void Setup() public override void TearDown() { EventProvider.Unsubscribe(InputForUIOnEvent); - EventProvider.ClearMockProvider(); + if (!m_ClearedMockProvider) + EventProvider.ClearMockProvider(); m_InputForUIEvents.Clear(); + // InputSystem.actions setter throws in play mode, so we use the internal manager property here. InputSystem.manager.actions = storedActions; #if UNITY_EDITOR @@ -92,6 +96,98 @@ public void InputSystemActionAssetIsNotNull() "Test is invalid since InputSystemProvider actions are not available"); } + // Creates a minimal project-wide asset recognised by SelectInputActionAsset(). + // At least one action is required: InputActionMap.enabled is m_EnabledActionsCount > 0, + // so an empty map can never report as enabled. + static InputActionAsset CreateProjectWideAssetWithUIMap(out InputActionMap uiMap) + { + var asset = ScriptableObject.CreateInstance(); + uiMap = new InputActionMap("UI"); + uiMap.AddAction("Point", InputActionType.PassThrough, "/position"); + asset.AddActionMap(uiMap); + return asset; + } + + [Test] + [Category(kTestCategory)] + public void Shutdown_DoesNotDisableProjectWideActionsAsset() + { + var asset = CreateProjectWideAssetWithUIMap(out var uiMap); + + // A non-UI map the user has enabled — provider must never touch it. + var gameplayMap = new InputActionMap("Gameplay"); + gameplayMap.AddAction("Jump", InputActionType.Button, "/space"); + asset.AddActionMap(gameplayMap); + gameplayMap.Enable(); // Enable after all maps are added; modifying the asset while any map is enabled is not allowed. + + // InputSystem.actions setter throws in play mode, so we use the internal manager property here. + InputSystem.manager.actions = asset; + try + { + m_InputSystemProvider.OnActionsChange(); + Assert.That(uiMap.enabled, Is.True, "UI action map should be enabled by provider initialization."); + Assert.That(gameplayMap.enabled, Is.True, "Provider must not change enabled state of non-UI maps."); + + EventProvider.ClearMockProvider(); + m_ClearedMockProvider = true; + + Assert.That(uiMap.enabled, Is.True, "Provider must not disable the UI map in a project-wide asset on shutdown."); + Assert.That(gameplayMap.enabled, Is.True, "Provider must not disable non-UI maps on shutdown."); + } + finally + { + Object.DestroyImmediate(asset); + } + } + + [Test] + [Category(kTestCategory)] + public void Shutdown_DoesNotDisableProjectWideUIMap_WhenAlreadyEnabledBeforeInit() + { + var asset = CreateProjectWideAssetWithUIMap(out var uiMap); + uiMap.Enable(); // User had the UI map enabled before the provider started. + + // InputSystem.actions setter throws in play mode, so we use the internal manager property here. + InputSystem.manager.actions = asset; + try + { + m_InputSystemProvider.OnActionsChange(); + Assert.That(uiMap.enabled, Is.True, "UI action map should remain enabled after provider initialization."); + + EventProvider.ClearMockProvider(); + m_ClearedMockProvider = true; + + // The provider did not enable the map, so it must not disable it on shutdown. + Assert.That(uiMap.enabled, Is.True, "UI action map must remain enabled after provider shutdown when the user had it enabled before initialization."); + } + finally + { + Object.DestroyImmediate(asset); + } + } + + [Test] + [Category(kTestCategory)] + public void Shutdown_DisablesUIActionMap_ForProviderOwnedAsset() + { + InputActionMap capturedUIMap = null; + InputSystemProvider.SetOnRegisterActions(asset => + capturedUIMap = asset?.FindActionMap("UI", false)); + + // Remove project-wide actions so the provider falls back to its own internal default asset. + // InputSystem.actions setter throws in play mode, so we use the internal manager property here. + InputSystem.manager.actions = null; + m_InputSystemProvider.OnActionsChange(); + InputSystemProvider.SetOnRegisterActions(null); + + Assert.That(capturedUIMap, Is.Not.Null, "Provider should have a UI action map in its internal default asset."); + Assert.That(capturedUIMap.enabled, Is.True, "UI action map should be enabled by provider initialization."); + + EventProvider.ClearMockProvider(); + m_ClearedMockProvider = true; + Assert.That(capturedUIMap.enabled, Is.False, "UI action map should be disabled after provider shutdown for provider-owned assets."); + } + [Test] [Category(kTestCategory)] // Checks that mouse events are ignored when a touch is active. diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index 02a6867477..14e27636f1 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed +- Fixed `InputSystemProvider` disabling project-wide actions on shutdown when UI Toolkit destroys its objects mid-play. The provider now scopes its lifecycle to the UI action map only and does not disable project-wide actions [UUM-134130](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-134130) - Fixed `InputActionRebindingExtensions.GetBindingDisplayString(InputAction, InputBinding, ...)` returning an empty string for composite bindings when the binding mask filters by group [UUM-141423](https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-141423) - Fixed `InputEventPtr.handled` not preventing actions from triggering when switching devices. The default event handled policy has been changed from `SuppressStateUpdates` (now deprecated) to `SuppressActionEventNotifications`, which keeps device state synchronized while suppressing action callbacks for handled events. [ISXB-1097](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1097) - Fixed all `InputAction.WasXxxThisFrame()` and `WasXxxThisDynamicUpdate()` polling APIs to use per-action suppression tracking instead of a map-wide flag. Previously, when multiple events arrived in the same frame with mixed handled/unhandled states, the last event's suppression state could incorrectly affect all actions in the map. [ISXB-1097](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1097) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs index 1b056f7e76..cbc8f53901 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/InputForUI/InputSystemProvider.cs @@ -17,6 +17,8 @@ internal class InputSystemProvider : IEventProviderImpl DefaultInputActions m_DefaultInputActions; InputActionAsset m_InputActionAsset; + InputActionMap m_UIActionMap; + bool m_ShouldDisableUIActionMapOnUnregister; // Note that these are plain action references instead since InputActionReference do // not provide any value when this integration doesn't have any UI. If this integration @@ -636,14 +638,18 @@ void RegisterActions() m_RightClickAction = FindActionAndRegisterCallback(Actions.RightClickAction, OnRightClickPerformed); m_ScrollWheelAction = FindActionAndRegisterCallback(Actions.ScrollWheelAction, OnScrollWheelPerformed); - // When adding new actions, don't forget to add them to UnregisterActions - if (InputSystem.actions == null) + // Only touch the UI map so we don't change the enabled state of unrelated maps. + m_UIActionMap = m_InputActionAsset?.FindActionMap("UI", false); + if (m_UIActionMap != null && !m_UIActionMap.enabled) { - // If we've not loaded a user-created set of actions, just enable the UI actions from our defaults. - m_InputActionAsset.FindActionMap("UI", true).Enable(); + m_UIActionMap.Enable(); + + // For provider-owned assets we are responsible for cleanup on shutdown. + // For project-wide actions the play-mode lifecycle manages the asset, so + // leave it as-is when the provider goes away. + if (m_InputActionAsset != InputSystem.actions) + m_ShouldDisableUIActionMapOnUnregister = true; } - else - m_InputActionAsset.Enable(); } void UnregisterAction(ref InputAction action, Action callback = null) @@ -664,8 +670,11 @@ void UnregisterActions() UnregisterAction(ref m_RightClickAction, OnRightClickPerformed); UnregisterAction(ref m_ScrollWheelAction, OnScrollWheelPerformed); - if (m_InputActionAsset != null) - m_InputActionAsset.Disable(); + if (m_ShouldDisableUIActionMapOnUnregister && m_UIActionMap != null) + m_UIActionMap.Disable(); + + m_UIActionMap = null; + m_ShouldDisableUIActionMapOnUnregister = false; } void SelectInputActionAsset()