From 0b6defd30b00bdec5dd01fd8bf496bfe53eb7992 Mon Sep 17 00:00:00 2001 From: Anthony Yakovlev Date: Thu, 28 Aug 2025 10:32:32 +0300 Subject: [PATCH 1/5] track the submit and cancel events using callbacks since otherwise it was impossible to do automatic testing with mocks --- .../Plugins/UI/InputSystemUIInputModule.cs | 50 +++++++++++++------ .../InputSystem/Plugins/UI/NavigationModel.cs | 4 ++ 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs index 667a833494..0244395296 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/InputSystemUIInputModule.cs @@ -914,11 +914,15 @@ internal void ProcessNavigation(ref NavigationModel navigationState) data.device = m_SubmitCancelState.device; - if (cancelAction != null && cancelAction.WasPerformedThisDynamicUpdate()) + if (cancelAction != null && m_NavigationState.wasCancelButtonReleased) ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.cancelHandler); - if (!data.used && submitAction != null && submitAction.WasPerformedThisDynamicUpdate()) + if (!data.used && submitAction != null && m_NavigationState.wasSubmitButtonReleased) ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.submitHandler); } + + // We clear the following flags here to mark that we've processed them already + m_NavigationState.wasSubmitButtonReleased = false; + m_NavigationState.wasCancelButtonReleased = false; } private bool IsMoveAllowed(AxisEventData eventData) @@ -1403,7 +1407,7 @@ public InputActionReference move public InputActionReference submit { get => m_SubmitAction; - set => SwapAction(ref m_SubmitAction, value, m_ActionsHooked, m_OnSubmitCancelDelegate); + set => SwapAction(ref m_SubmitAction, value, m_ActionsHooked, m_OnSubmitDelegate); } /// @@ -1443,7 +1447,7 @@ public InputActionReference submit public InputActionReference cancel { get => m_CancelAction; - set => SwapAction(ref m_CancelAction, value, m_ActionsHooked, m_OnSubmitCancelDelegate); + set => SwapAction(ref m_CancelAction, value, m_ActionsHooked, m_OnCancelDelegate); } /// @@ -2287,11 +2291,6 @@ private void OnMoveCallback(InputAction.CallbackContext context) m_NavigationState.device = context.control.device; } - private void OnSubmitCancelCallback(InputAction.CallbackContext context) - { - m_SubmitCancelState.device = context.control.device; - } - private void OnTrackedDeviceOrientationCallback(InputAction.CallbackContext context) { var index = GetPointerStateIndexFor(ref context); @@ -2318,6 +2317,26 @@ private void OnTrackedDevicePositionCallback(InputAction.CallbackContext context #endif } + private void OnSubmit(InputAction.CallbackContext context) + { + m_SubmitCancelState.device = context.control.device; + + if (!context.ReadValueAsButton()) + { + m_NavigationState.wasSubmitButtonReleased = true; + } + } + + private void OnCancel(InputAction.CallbackContext context) + { + m_SubmitCancelState.device = context.control.device; + + if (!context.ReadValueAsButton()) + { + m_NavigationState.wasCancelButtonReleased = true; + } + } + private void OnControlsChanged(object obj) { m_NeedToPurgeStalePointers = true; @@ -2509,12 +2528,14 @@ private void HookActions() m_OnScrollWheelDelegate = OnScrollCallback; if (m_OnMoveDelegate == null) m_OnMoveDelegate = OnMoveCallback; - if (m_OnSubmitCancelDelegate == null) - m_OnSubmitCancelDelegate = OnSubmitCancelCallback; if (m_OnTrackedDeviceOrientationDelegate == null) m_OnTrackedDeviceOrientationDelegate = OnTrackedDeviceOrientationCallback; if (m_OnTrackedDevicePositionDelegate == null) m_OnTrackedDevicePositionDelegate = OnTrackedDevicePositionCallback; + if (m_OnSubmitDelegate == null) + m_OnSubmitDelegate = OnSubmit; + if (m_OnCancelDelegate == null) + m_OnCancelDelegate = OnCancel; SetActionCallbacks(true); } @@ -2532,14 +2553,14 @@ private void SetActionCallbacks(bool install) m_ActionsHooked = install; SetActionCallback(m_PointAction, m_OnPointDelegate, install); SetActionCallback(m_MoveAction, m_OnMoveDelegate, install); - SetActionCallback(m_SubmitAction, m_OnSubmitCancelDelegate, install); - SetActionCallback(m_CancelAction, m_OnSubmitCancelDelegate, install); SetActionCallback(m_LeftClickAction, m_OnLeftClickDelegate, install); SetActionCallback(m_RightClickAction, m_OnRightClickDelegate, install); SetActionCallback(m_MiddleClickAction, m_OnMiddleClickDelegate, install); SetActionCallback(m_ScrollWheelAction, m_OnScrollWheelDelegate, install); SetActionCallback(m_TrackedDeviceOrientationAction, m_OnTrackedDeviceOrientationDelegate, install); SetActionCallback(m_TrackedDevicePositionAction, m_OnTrackedDevicePositionDelegate, install); + SetActionCallback(m_SubmitAction, m_OnSubmitDelegate, install); + SetActionCallback(m_CancelAction, m_OnCancelDelegate, install); } private static void SetActionCallback(InputActionReference actionReference, Action callback, bool install) @@ -2649,13 +2670,14 @@ private struct InputActionReferenceState private Action m_OnPointDelegate; private Action m_OnMoveDelegate; - private Action m_OnSubmitCancelDelegate; private Action m_OnLeftClickDelegate; private Action m_OnRightClickDelegate; private Action m_OnMiddleClickDelegate; private Action m_OnScrollWheelDelegate; private Action m_OnTrackedDevicePositionDelegate; private Action m_OnTrackedDeviceOrientationDelegate; + private Action m_OnSubmitDelegate; + private Action m_OnCancelDelegate; private Action m_OnControlsChangedDelegate; // Pointer-type input (also tracking-type). diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/NavigationModel.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/NavigationModel.cs index 5fb6d80c79..7e0d33f313 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/NavigationModel.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/UI/NavigationModel.cs @@ -11,10 +11,14 @@ internal struct NavigationModel public float lastMoveTime; public AxisEventData eventData; public InputDevice device; + public bool wasSubmitButtonReleased; + public bool wasCancelButtonReleased; public void Reset() { move = Vector2.zero; + wasCancelButtonReleased = false; + wasSubmitButtonReleased = false; } } From 3d328961990b2aa791d2f33fa1f4753b896a110e Mon Sep 17 00:00:00 2001 From: Anthony Yakovlev Date: Thu, 28 Aug 2025 11:00:28 +0300 Subject: [PATCH 2/5] add a set of tests to cover for the input system UI module regression --- .../Plugins/MockKeyboardWithUITests.cs | 105 ++++++++++++++++++ .../Plugins/MockKeyboardWithUITests.cs.meta | 2 + 2 files changed, 107 insertions(+) create mode 100644 Assets/Tests/InputSystem/Plugins/MockKeyboardWithUITests.cs create mode 100644 Assets/Tests/InputSystem/Plugins/MockKeyboardWithUITests.cs.meta diff --git a/Assets/Tests/InputSystem/Plugins/MockKeyboardWithUITests.cs b/Assets/Tests/InputSystem/Plugins/MockKeyboardWithUITests.cs new file mode 100644 index 0000000000..eb1703e594 --- /dev/null +++ b/Assets/Tests/InputSystem/Plugins/MockKeyboardWithUITests.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.UI; +using UnityEngine.TestTools; +using UnityEngine.UI; + +[Description("Check https://jira.unity3d.com/browse/ISXB-1561 for more details.")] +public class MockKeyboardWithUITests : InputTestFixture +{ + private Keyboard m_Keyboard; + private GameObject m_CanvasGo; + private GameObject m_EventSystemGo; + private GameObject m_ButtonGo; + private InputActionAsset m_InputActionAsset; + + public class CancellationHandler : MonoBehaviour, ICancelHandler + { + public Action trigger; + public void OnCancel(BaseEventData eventData) => trigger(); + } + + public override void Setup() + { + base.Setup(); + + // Add a mock keyboard + m_Keyboard = InputSystem.AddDevice(); + + // first creating the UI stuff + m_CanvasGo = new GameObject("canvas"); + m_ButtonGo = new GameObject("button", typeof(Button), typeof(CancellationHandler)); + m_ButtonGo.transform.SetParent(m_CanvasGo.transform, false); + + // Now creating the Input layer + m_EventSystemGo = new GameObject("eventSystem", typeof(EventSystem)); + EventSystem.current.SetSelectedGameObject(m_ButtonGo); + + var inputModule = m_EventSystemGo.AddComponent(); + + m_InputActionAsset = ScriptableObject.CreateInstance(); + var actionMap = new InputActionMap("UI"); + var submitAction = actionMap.AddAction("Submit", binding: "Keyboard/enter", type: InputActionType.Button); + var cancelAction = actionMap.AddAction("Cancel", binding: "Keyboard/escape", type: InputActionType.Button); + m_InputActionAsset.AddActionMap(actionMap); + actionMap.Enable(); + inputModule.actionsAsset = m_InputActionAsset; + inputModule.submit = InputActionReference.Create(submitAction); + inputModule.cancel = InputActionReference.Create(cancelAction); + } + + public override void TearDown() + { + GameObject.Destroy(m_EventSystemGo); + GameObject.Destroy(m_CanvasGo); + + InputSystem.RemoveDevice(m_Keyboard); + + base.TearDown(); + } + + private static bool[] FrameWaitingOptions = { true, false }; + + [UnityTest] + public IEnumerator MockKeyboard_PressEnter_TriggersButtonOnClick([ValueSource(nameof(FrameWaitingOptions))] bool waitFrame) + { + bool invokedListener = false; + + m_ButtonGo.GetComponent