Maze / Library /PackageCache /com.unity.inputsystem@1.6.1 /InputSystem /Plugins /UI /InputSystemUIInputModule.cs
| using System; | |
| using System.Collections.Generic; | |
| using UnityEngine.EventSystems; | |
| using UnityEngine.InputSystem.Controls; | |
| using UnityEngine.InputSystem.LowLevel; | |
| using UnityEngine.InputSystem.Utilities; | |
| using UnityEngine.Serialization; | |
| using UnityEngine.UI; | |
| using UnityEditor; | |
| ////FIXME: The UI is currently not reacting to pointers until they are moved after the UI module has been enabled. What needs to | |
| //// happen is that point, trackedDevicePosition, and trackedDeviceOrientation have initial state checks. However, for touch, | |
| //// we do *not* want to react to the initial value as then we also get presses (unlike with other pointers). Argh. | |
| ////REVIEW: I think this would be much better served by having a composite type input for each of the three basic types of input (pointer, navigation, tracked) | |
| //// I.e. there'd be a PointerInput, a NavigationInput, and a TrackedInput composite. This would solve several problems in one go and make | |
| //// it much more obvious which inputs go together. | |
| //// NOTE: This does not actually solve the problem. Even if, for example, we have a PointerInput value struct and a PointerInputComposite | |
| //// that binds the individual inputs to controls, and then we use it to bind touch0 as a pointer input source, there may still be multiple | |
| //// touchscreens and thus multiple touches coming in through the same composite. This leads back to the same situation. | |
| ////REVIEW: The current input model has too much complexity for pointer input; find a way to simplify this. | |
| ////REVIEW: how does this/uGUI support drag-scrolls on touch? [GESTURES] | |
| ////REVIEW: how does this/uGUI support two-finger right-clicks with touch? [GESTURES] | |
| ////TODO: add ability to query which device was last used with any of the actions | |
| ////REVIEW: also give access to the last/current UI event? | |
| ////TODO: ToString() method a la PointerInputModule | |
| namespace UnityEngine.InputSystem.UI | |
| { | |
| /// <summary> | |
| /// Input module that takes its input from <see cref="InputAction">input actions</see>. | |
| /// </summary> | |
| /// <remarks> | |
| /// This UI input module has the advantage over other such modules that it doesn't have to know | |
| /// what devices and types of devices input is coming from. Instead, the actions hide the actual | |
| /// sources of input from the module. | |
| /// | |
| /// When adding this component from code (such as through <c>GameObject.AddComponent</c>), the | |
| /// resulting module will automatically have a set of default input actions assigned to it | |
| /// (see <see cref="AssignDefaultActions"/>). | |
| /// </remarks> | |
| [] | |
| public class InputSystemUIInputModule : BaseInputModule | |
| { | |
| /// <summary> | |
| /// Whether to clear the current selection when a click happens that does not hit any <c>GameObject</c>. | |
| /// </summary> | |
| /// <value>If true (default), clicking outside of any GameObject will reset the current selection.</value> | |
| /// <remarks> | |
| /// By toggling this behavior off, background clicks will keep the current selection. I.e. | |
| /// <c>EventSystem.currentSelectedGameObject</c> will not be changed. | |
| /// </remarks> | |
| public bool deselectOnBackgroundClick | |
| { | |
| get => m_DeselectOnBackgroundClick; | |
| set => m_DeselectOnBackgroundClick = value; | |
| } | |
| /// <summary> | |
| /// How to deal with the presence of pointer-type input from multiple devices. | |
| /// </summary> | |
| /// <remarks> | |
| /// By default, this is set to <see cref="UIPointerBehavior.SingleMouseOrPenButMultiTouchAndTrack"/> which will | |
| /// treat input from <see cref="Mouse"/> and <see cref="Pen"/> devices as coming from a single on-screen pointer | |
| /// but will treat input from devices such as <see cref="XR.XRController"/> and <see cref="Touchscreen"/> as | |
| /// their own discrete pointers. | |
| /// | |
| /// The primary effect of this setting is to determine whether the user can concurrently point at more than | |
| /// a single UI element or not. Whenever multiple pointers are allowed, more than one element may have a pointer | |
| /// over it at any one point and thus several elements can be interacted with concurrently. | |
| /// </remarks> | |
| public UIPointerBehavior pointerBehavior | |
| { | |
| get => m_PointerBehavior; | |
| set => m_PointerBehavior = value; | |
| } | |
| /// <summary> | |
| /// Where to position the pointer when the cursor is locked. | |
| /// </summary> | |
| /// <remarks> | |
| /// By default, the pointer is positioned at -1, -1 in screen space when the cursor is locked. This has implications | |
| /// for using ray casters like <see cref="PhysicsRaycaster"/> because the raycasts will be sent from the pointer | |
| /// position. By setting the value of <see cref="cursorLockBehavior"/> to <see cref="CursorLockBehavior.ScreenCenter"/>, | |
| /// the raycasts will be sent from the center of the screen. This is useful when trying to interact with world space UI | |
| /// using the <see cref="IPointerEnterHandler"/> and <see cref="IPointerExitHandler"/> interfaces when the cursor | |
| /// is locked. | |
| /// </remarks> | |
| /// <see cref="Cursor.lockState"/> | |
| public CursorLockBehavior cursorLockBehavior | |
| { | |
| get => m_CursorLockBehavior; | |
| set => m_CursorLockBehavior = value; | |
| } | |
| /// <summary> | |
| /// A root game object to support correct navigation in local multi-player UIs. | |
| /// <remarks> | |
| /// In local multi-player games where each player has their own UI, players should not be able to navigate into | |
| /// another player's UI. Each player should have their own instance of an InputSystemUIInputModule, and this property | |
| /// should be set to the root game object containing all UI objects for that player. If set, navigation using the | |
| /// <see cref="InputSystemUIInputModule.move"/> action will be constrained to UI objects under that root. | |
| /// </remarks> | |
| /// </summary> | |
| internal GameObject localMultiPlayerRoot | |
| { | |
| get => m_LocalMultiPlayerRoot; | |
| set => m_LocalMultiPlayerRoot = value; | |
| } | |
| /// <summary> | |
| /// Called by <c>EventSystem</c> when the input module is made current. | |
| /// </summary> | |
| public override void ActivateModule() | |
| { | |
| base.ActivateModule(); | |
| // Select firstSelectedGameObject if nothing is selected ATM. | |
| var toSelect = eventSystem.currentSelectedGameObject; | |
| if (toSelect == null) | |
| toSelect = eventSystem.firstSelectedGameObject; | |
| eventSystem.SetSelectedGameObject(toSelect, GetBaseEventData()); | |
| } | |
| /// <summary> | |
| /// Check whether the given pointer or touch is currently hovering over a <c>GameObject</c>. | |
| /// </summary> | |
| /// <param name="pointerOrTouchId">ID of the pointer or touch. Meaning this should correspond to either | |
| /// <c>PointerEventData.pointerId</c> or <see cref="ExtendedPointerEventData.touchId"/>. The pointer ID | |
| /// generally corresponds to the <see cref="InputDevice.deviceId"/> of the pointer device. An exception | |
| /// to this are touches as a <see cref="Touchscreen"/> may have multiple pointers (one for each active | |
| /// finger). For touch, you can use the <see cref="TouchControl.touchId"/> of the touch. | |
| /// | |
| /// Note that for touch, a pointer will stay valid for one frame before being removed. In other words, | |
| /// when <see cref="TouchPhase.Ended"/> or <see cref="TouchPhase.Canceled"/> is received for a touch | |
| /// and the touch was over a <c>GameObject</c>, the associated pointer is still considered over that | |
| /// object for the frame in which the touch ended. | |
| /// | |
| /// To check whether any pointer is over a <c>GameObject</c>, simply pass a negative value such as -1.</param> | |
| /// <returns>True if the given pointer is currently hovering over a <c>GameObject</c>.</returns> | |
| /// <remarks> | |
| /// The result is true if the given pointer has caused an <c>IPointerEnter</c> event to be sent to a | |
| /// <c>GameObject</c>. | |
| /// | |
| /// This method can be invoked via <c>EventSystem.current.IsPointerOverGameObject</c>. | |
| /// | |
| /// Be aware that this method relies on state set up during UI event processing that happens in <c>EventSystem.Update</c>, | |
| /// that is, as part of <c>MonoBehaviour</c> updates. This step happens <em>after</em> input processing. | |
| /// Thus, calling this method earlier than that in the frame will make it poll state from <em>last</em> frame. | |
| /// | |
| /// Calling this method from within an <see cref="InputAction"/> callback (such as <see cref="InputAction.performed"/>) | |
| /// will result in a warning. See the "UI vs Game Input" sample shipped with the Input System package for | |
| /// how to deal with this fact. | |
| /// | |
| /// <example> | |
| /// <code> | |
| /// // In general, the pointer ID corresponds to the device ID: | |
| /// EventSystem.current.IsPointerOverGameObject(XRController.leftHand.deviceId); | |
| /// EventSystem.current.IsPointerOverGameObject(Mouse.current.deviceId); | |
| /// | |
| /// // For touch input, pass the ID of a touch: | |
| /// EventSystem.current.IsPointerOverGameObject(Touchscreen.primaryTouch.touchId.ReadValue()); | |
| /// | |
| /// // But can also pass the ID of the entire Touchscreen in which case the result | |
| /// // is true if any touch is over a GameObject: | |
| /// EventSystem.current.IsPointerOverGameObject(Touchscreen.current.deviceId); | |
| /// | |
| /// // Finally, any negative value will be interpreted as "any pointer" and will | |
| /// // return true if any one pointer is currently over a GameObject: | |
| /// EventSystem.current.IsPointerOverGameObject(-1); | |
| /// EventSystem.current.IsPointerOverGameObject(); // Equivalent. | |
| /// </code> | |
| /// </example> | |
| /// </remarks> | |
| /// <seealso cref="ExtendedPointerEventData.touchId"/> | |
| /// <seealso cref="InputDevice.deviceId"/> | |
| public override bool IsPointerOverGameObject(int pointerOrTouchId) | |
| { | |
| if (InputSystem.isProcessingEvents) | |
| Debug.LogWarning( | |
| "Calling IsPointerOverGameObject() from within event processing (such as from InputAction callbacks) will not work as expected; it will query UI state from the last frame"); | |
| var stateIndex = -1; | |
| if (pointerOrTouchId < 0) | |
| { | |
| if (m_CurrentPointerId != -1) | |
| { | |
| stateIndex = m_CurrentPointerIndex; | |
| } | |
| else | |
| { | |
| // No current pointer. Can happen, for example, when a touch just ended and its pointer record | |
| // was removed as a result. If we still have some active pointer, use it. | |
| if (m_PointerStates.length > 0) | |
| stateIndex = 0; | |
| } | |
| } | |
| else | |
| { | |
| stateIndex = GetPointerStateIndexFor(pointerOrTouchId); | |
| } | |
| if (stateIndex == -1) | |
| return false; | |
| return m_PointerStates[stateIndex].eventData.pointerEnter != null; | |
| } | |
| /// <summary> | |
| /// Returns the most recent raycast information for a given pointer or touch. | |
| /// </summary> | |
| /// <param name="pointerOrTouchId">ID of the pointer or touch. Meaning this should correspond to either | |
| /// <c>PointerEventData.pointerId</c> or <see cref="ExtendedPointerEventData.touchId"/>. The pointer ID | |
| /// generally corresponds to the <see cref="InputDevice.deviceId"/> of the pointer device. An exception | |
| /// to this are touches as a <see cref="Touchscreen"/> may have multiple pointers (one for each active | |
| /// finger). For touch, you can use the <see cref="TouchControl.touchId"/> of the touch. | |
| /// | |
| /// Negative values will return an invalid <see cref="RaycastResult"/>.</param> | |
| /// <returns>The most recent raycast information.</returns> | |
| /// <remarks> | |
| /// This method is for the most recent raycast, but depending on when it's called is not guaranteed to be for the current frame. | |
| /// This method can be used to determine raycast distances and hit information for visualization. | |
| /// <br /> | |
| /// Use <see cref="RaycastResult.isValid"/> to determine if pointer hit anything. | |
| /// </remarks> | |
| /// <seealso cref="ExtendedPointerEventData.touchId"/> | |
| /// <seealso cref="InputDevice.deviceId"/> | |
| public RaycastResult GetLastRaycastResult(int pointerOrTouchId) | |
| { | |
| var stateIndex = GetPointerStateIndexFor(pointerOrTouchId); | |
| if (stateIndex == -1) | |
| return default; | |
| return m_PointerStates[stateIndex].eventData.pointerCurrentRaycast; | |
| } | |
| private RaycastResult PerformRaycast(ExtendedPointerEventData eventData) | |
| { | |
| if (eventData == null) | |
| throw new ArgumentNullException(nameof(eventData)); | |
| // If it's an event from a tracked device, see if we have a TrackedDeviceRaycaster and give it | |
| // the first shot. | |
| if (eventData.pointerType == UIPointerType.Tracked && TrackedDeviceRaycaster.s_Instances.length > 0) | |
| { | |
| for (var i = 0; i < TrackedDeviceRaycaster.s_Instances.length; ++i) | |
| { | |
| var trackedDeviceRaycaster = TrackedDeviceRaycaster.s_Instances[i]; | |
| m_RaycastResultCache.Clear(); | |
| trackedDeviceRaycaster.PerformRaycast(eventData, m_RaycastResultCache); | |
| if (m_RaycastResultCache.Count > 0) | |
| { | |
| var raycastResult = m_RaycastResultCache[0]; | |
| m_RaycastResultCache.Clear(); | |
| return raycastResult; | |
| } | |
| } | |
| return default; | |
| } | |
| // Otherwise pass it along to the normal raycasting logic. | |
| eventSystem.RaycastAll(eventData, m_RaycastResultCache); | |
| var result = FindFirstRaycast(m_RaycastResultCache); | |
| m_RaycastResultCache.Clear(); | |
| return result; | |
| } | |
| // Mouse, pen, touch, and tracked device pointer input all go through here. | |
| private void ProcessPointer(ref PointerModel state) | |
| { | |
| var eventData = state.eventData; | |
| // Sync position. | |
| var pointerType = eventData.pointerType; | |
| if (pointerType == UIPointerType.MouseOrPen && Cursor.lockState == CursorLockMode.Locked) | |
| { | |
| eventData.position = m_CursorLockBehavior == CursorLockBehavior.OutsideScreen ? | |
| new Vector2(-1, -1) : | |
| new Vector2(Screen.width / 2f, Screen.height / 2f); | |
| ////REVIEW: This is consistent with StandaloneInputModule but having no deltas in locked mode seems wrong | |
| eventData.delta = default; | |
| } | |
| else if (pointerType == UIPointerType.Tracked) | |
| { | |
| var position = state.worldPosition; | |
| var rotation = state.worldOrientation; | |
| if (m_XRTrackingOrigin != null) | |
| { | |
| position = m_XRTrackingOrigin.TransformPoint(position); | |
| rotation = m_XRTrackingOrigin.rotation * rotation; | |
| } | |
| eventData.trackedDeviceOrientation = rotation; | |
| eventData.trackedDevicePosition = position; | |
| } | |
| else | |
| { | |
| eventData.delta = state.screenPosition - eventData.position; | |
| eventData.position = state.screenPosition; | |
| } | |
| // Clear the 'used' flag. | |
| eventData.Reset(); | |
| // Raycast from current position. | |
| eventData.pointerCurrentRaycast = PerformRaycast(eventData); | |
| // Sync position for tracking devices. For those, we can only do this | |
| // after the raycast as the screen-space position is a byproduct of the raycast. | |
| if (pointerType == UIPointerType.Tracked && eventData.pointerCurrentRaycast.isValid) | |
| { | |
| var screenPos = eventData.pointerCurrentRaycast.screenPosition; | |
| eventData.delta = screenPos - eventData.position; | |
| eventData.position = eventData.pointerCurrentRaycast.screenPosition; | |
| } | |
| ////REVIEW: for touch, we only need the left button; should we skip right and middle button processing? then we also don't need to copy to/from the event | |
| // Left mouse button. Movement and scrolling is processed with event set left button. | |
| eventData.button = PointerEventData.InputButton.Left; | |
| state.leftButton.CopyPressStateTo(eventData); | |
| // Unlike StandaloneInputModule, we process moves before processing buttons. This way | |
| // UI elements get pointer enters/exits before they get button ups/downs and clicks. | |
| ProcessPointerMovement(ref state, eventData); | |
| // We always need to process move-related events in order to get PointerEnter and Exit events | |
| // when we change UI state (e.g. show/hide objects) without moving the pointer. This unfortunately | |
| // also means that we will invariably raycast on every update. | |
| // However, after that, early out at this point when there's no changes to the pointer state (except | |
| // for tracked pointers as the tracking origin may have moved). | |
| if (!state.changedThisFrame && (xrTrackingOrigin == null || state.pointerType != UIPointerType.Tracked)) | |
| return; | |
| ProcessPointerButton(ref state.leftButton, eventData); | |
| ProcessPointerButtonDrag(ref state.leftButton, eventData); | |
| ProcessPointerScroll(ref state, eventData); | |
| // Right mouse button. | |
| eventData.button = PointerEventData.InputButton.Right; | |
| state.rightButton.CopyPressStateTo(eventData); | |
| ProcessPointerButton(ref state.rightButton, eventData); | |
| ProcessPointerButtonDrag(ref state.rightButton, eventData); | |
| // Middle mouse button. | |
| eventData.button = PointerEventData.InputButton.Middle; | |
| state.middleButton.CopyPressStateTo(eventData); | |
| ProcessPointerButton(ref state.middleButton, eventData); | |
| ProcessPointerButtonDrag(ref state.middleButton, eventData); | |
| } | |
| // if we are using a MultiplayerEventSystem, ignore any transforms | |
| // not under the current MultiplayerEventSystem's root. | |
| private bool PointerShouldIgnoreTransform(Transform t) | |
| { | |
| if (eventSystem is MultiplayerEventSystem multiplayerEventSystem && multiplayerEventSystem.playerRoot != null) | |
| { | |
| if (!t.IsChildOf(multiplayerEventSystem.playerRoot.transform)) | |
| return true; | |
| } | |
| return false; | |
| } | |
| private void ProcessPointerMovement(ref PointerModel pointer, ExtendedPointerEventData eventData) | |
| { | |
| var currentPointerTarget = | |
| // If the pointer is a touch that was released the *previous* frame, we generate pointer-exit events | |
| // and then later remove the pointer. | |
| eventData.pointerType == UIPointerType.Touch && !pointer.leftButton.isPressed && !pointer.leftButton.wasReleasedThisFrame | |
| ? null | |
| : eventData.pointerCurrentRaycast.gameObject; | |
| ProcessPointerMovement(eventData, currentPointerTarget); | |
| } | |
| private void ProcessPointerMovement(ExtendedPointerEventData eventData, GameObject currentPointerTarget) | |
| { | |
| // If the pointer moved, send move events to all UI elements the pointer is | |
| // currently over. | |
| var wasMoved = eventData.IsPointerMoving(); | |
| if (wasMoved) | |
| { | |
| for (var i = 0; i < eventData.hovered.Count; ++i) | |
| ExecuteEvents.Execute(eventData.hovered[i], eventData, ExecuteEvents.pointerMoveHandler); | |
| } | |
| // If we have no target or pointerEnter has been deleted, | |
| // we just send exit events to anything we are tracking | |
| // and then exit. | |
| if (currentPointerTarget == null || eventData.pointerEnter == null) | |
| { | |
| for (var i = 0; i < eventData.hovered.Count; ++i) | |
| ExecuteEvents.Execute(eventData.hovered[i], eventData, ExecuteEvents.pointerExitHandler); | |
| eventData.hovered.Clear(); | |
| if (currentPointerTarget == null) | |
| { | |
| eventData.pointerEnter = null; | |
| return; | |
| } | |
| } | |
| if (eventData.pointerEnter == currentPointerTarget && currentPointerTarget) | |
| return; | |
| var commonRoot = FindCommonRoot(eventData.pointerEnter, currentPointerTarget)?.transform; | |
| // We walk up the tree until a common root and the last entered and current entered object is found. | |
| // Then send exit and enter events up to, but not including, the common root. | |
| if (eventData.pointerEnter != null) | |
| { | |
| for (var current = eventData.pointerEnter.transform; current != null && current != commonRoot; current = current.parent) | |
| { | |
| ExecuteEvents.Execute(current.gameObject, eventData, ExecuteEvents.pointerExitHandler); | |
| eventData.hovered.Remove(current.gameObject); | |
| } | |
| } | |
| eventData.pointerEnter = currentPointerTarget; | |
| if (currentPointerTarget != null) | |
| { | |
| for (var current = currentPointerTarget.transform; | |
| current != null && current != commonRoot && !PointerShouldIgnoreTransform(current); | |
| current = current.parent) | |
| { | |
| ExecuteEvents.Execute(current.gameObject, eventData, ExecuteEvents.pointerEnterHandler); | |
| if (wasMoved) | |
| ExecuteEvents.Execute(current.gameObject, eventData, ExecuteEvents.pointerMoveHandler); | |
| eventData.hovered.Add(current.gameObject); | |
| } | |
| } | |
| } | |
| private const float kClickSpeed = 0.3f; | |
| private void ProcessPointerButton(ref PointerModel.ButtonState button, PointerEventData eventData) | |
| { | |
| var currentOverGo = eventData.pointerCurrentRaycast.gameObject; | |
| if (currentOverGo != null && PointerShouldIgnoreTransform(currentOverGo.transform)) | |
| return; | |
| // Button press. | |
| if (button.wasPressedThisFrame) | |
| { | |
| button.pressTime = InputRuntime.s_Instance.unscaledGameTime; | |
| eventData.delta = Vector2.zero; | |
| eventData.dragging = false; | |
| eventData.pressPosition = eventData.position; | |
| eventData.pointerPressRaycast = eventData.pointerCurrentRaycast; | |
| eventData.eligibleForClick = true; | |
| eventData.useDragThreshold = true; | |
| var selectHandler = ExecuteEvents.GetEventHandler<ISelectHandler>(currentOverGo); | |
| // If we have clicked something new, deselect the old thing and leave 'selection handling' up | |
| // to the press event (except if there's none and we're told to not deselect in that case). | |
| if (selectHandler != eventSystem.currentSelectedGameObject && (selectHandler != null || m_DeselectOnBackgroundClick)) | |
| eventSystem.SetSelectedGameObject(null, eventData); | |
| // Invoke OnPointerDown, if present. | |
| var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, eventData, ExecuteEvents.pointerDownHandler); | |
| // If no GO responded to OnPointerDown, look for one that responds to OnPointerClick. | |
| // NOTE: This only looks up the handler. We don't invoke OnPointerClick here. | |
| if (newPressed == null) | |
| newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo); | |
| // Reset click state if delay to last release was too long or if we didn't | |
| // press on the same object as last time. The latter part we don't know until | |
| // we've actually run the press handler. | |
| button.clickedOnSameGameObject = newPressed == eventData.lastPress && button.pressTime - eventData.clickTime <= kClickSpeed; | |
| if (eventData.clickCount > 0 && !button.clickedOnSameGameObject) | |
| { | |
| eventData.clickCount = default; | |
| eventData.clickTime = default; | |
| } | |
| // Set pointerPress. This nukes lastPress. Meaning that after OnPointerDown, lastPress will | |
| // become null. | |
| eventData.pointerPress = newPressed; | |
| eventData.rawPointerPress = currentOverGo; | |
| // Save the drag handler for drag events during this mouse down. | |
| eventData.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo); | |
| if (eventData.pointerDrag != null) | |
| ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.initializePotentialDrag); | |
| } | |
| // Button release. | |
| if (button.wasReleasedThisFrame) | |
| { | |
| // Check for click. Release must be on same GO that we pressed on and we must not | |
| // have moved beyond our move tolerance (doing so will set eligibleForClick to false). | |
| // NOTE: There's two difference to click handling here compared to StandaloneInputModule. | |
| // 1) StandaloneInputModule counts clicks entirely on press meaning that clickCount is increased | |
| // before a click has actually happened. | |
| // 2) StandaloneInputModule increases click counts even if something is eventually not deemed a | |
| // click and OnPointerClick is thus never invoked. | |
| var pointerClickHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo); | |
| var isClick = eventData.pointerPress == pointerClickHandler && eventData.eligibleForClick; | |
| if (isClick) | |
| { | |
| // Count clicks. | |
| if (button.clickedOnSameGameObject) | |
| { | |
| // We re-clicked on the same UI element within 0.3 seconds so count | |
| // it as a repeat click. | |
| ++eventData.clickCount; | |
| } | |
| else | |
| { | |
| // First click on this object. | |
| eventData.clickCount = 1; | |
| } | |
| eventData.clickTime = InputRuntime.s_Instance.unscaledGameTime; | |
| } | |
| // Invoke OnPointerUp. | |
| ExecuteEvents.Execute(eventData.pointerPress, eventData, ExecuteEvents.pointerUpHandler); | |
| // Invoke OnPointerClick or OnDrop. | |
| if (isClick) | |
| ExecuteEvents.Execute(eventData.pointerPress, eventData, ExecuteEvents.pointerClickHandler); | |
| else if (eventData.dragging && eventData.pointerDrag != null) | |
| ExecuteEvents.ExecuteHierarchy(currentOverGo, eventData, ExecuteEvents.dropHandler); | |
| eventData.eligibleForClick = false; | |
| eventData.pointerPress = null; | |
| eventData.rawPointerPress = null; | |
| if (eventData.dragging && eventData.pointerDrag != null) | |
| ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.endDragHandler); | |
| eventData.dragging = false; | |
| eventData.pointerDrag = null; | |
| button.ignoreNextClick = false; | |
| } | |
| button.CopyPressStateFrom(eventData); | |
| } | |
| private void ProcessPointerButtonDrag(ref PointerModel.ButtonState button, ExtendedPointerEventData eventData) | |
| { | |
| if (!eventData.IsPointerMoving() || | |
| (eventData.pointerType == UIPointerType.MouseOrPen && Cursor.lockState == CursorLockMode.Locked) || | |
| eventData.pointerDrag == null) | |
| return; | |
| // Detect drags. | |
| if (!eventData.dragging) | |
| { | |
| if (!eventData.useDragThreshold || (eventData.pressPosition - eventData.position).sqrMagnitude >= | |
| (double)eventSystem.pixelDragThreshold * eventSystem.pixelDragThreshold * (eventData.pointerType == UIPointerType.Tracked | |
| ? m_TrackedDeviceDragThresholdMultiplier | |
| : 1)) | |
| { | |
| // Started dragging. Invoke OnBeginDrag. | |
| ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.beginDragHandler); | |
| eventData.dragging = true; | |
| } | |
| } | |
| if (eventData.dragging) | |
| { | |
| // If we moved from our initial press object, process an up for that object. | |
| if (eventData.pointerPress != eventData.pointerDrag) | |
| { | |
| ExecuteEvents.Execute(eventData.pointerPress, eventData, ExecuteEvents.pointerUpHandler); | |
| eventData.eligibleForClick = false; | |
| eventData.pointerPress = null; | |
| eventData.rawPointerPress = null; | |
| } | |
| ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.dragHandler); | |
| button.CopyPressStateFrom(eventData); | |
| } | |
| } | |
| private static void ProcessPointerScroll(ref PointerModel pointer, PointerEventData eventData) | |
| { | |
| var scrollDelta = pointer.scrollDelta; | |
| if (!Mathf.Approximately(scrollDelta.sqrMagnitude, 0.0f)) | |
| { | |
| eventData.scrollDelta = scrollDelta; | |
| var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(eventData.pointerEnter); | |
| ExecuteEvents.ExecuteHierarchy(scrollHandler, eventData, ExecuteEvents.scrollHandler); | |
| } | |
| } | |
| internal void ProcessNavigation(ref NavigationModel navigationState) | |
| { | |
| var usedSelectionChange = false; | |
| if (eventSystem.currentSelectedGameObject != null) | |
| { | |
| var data = GetBaseEventData(); | |
| ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.updateSelectedHandler); | |
| usedSelectionChange = data.used; | |
| } | |
| // Don't send move events if disabled in the EventSystem. | |
| if (!eventSystem.sendNavigationEvents) | |
| return; | |
| // Process move. | |
| var movement = navigationState.move; | |
| if (!usedSelectionChange && (!Mathf.Approximately(movement.x, 0f) || !Mathf.Approximately(movement.y, 0f))) | |
| { | |
| var time = InputRuntime.s_Instance.unscaledGameTime; | |
| var moveVector = navigationState.move; | |
| var moveDirection = MoveDirection.None; | |
| if (moveVector.sqrMagnitude > 0) | |
| { | |
| if (Mathf.Abs(moveVector.x) > Mathf.Abs(moveVector.y)) | |
| moveDirection = moveVector.x > 0 ? MoveDirection.Right : MoveDirection.Left; | |
| else | |
| moveDirection = moveVector.y > 0 ? MoveDirection.Up : MoveDirection.Down; | |
| } | |
| ////REVIEW: is resetting move repeats when direction changes really useful behavior? | |
| if (moveDirection != m_NavigationState.lastMoveDirection) | |
| m_NavigationState.consecutiveMoveCount = 0; | |
| if (moveDirection != MoveDirection.None) | |
| { | |
| var allow = true; | |
| if (m_NavigationState.consecutiveMoveCount != 0) | |
| { | |
| if (m_NavigationState.consecutiveMoveCount > 1) | |
| allow = time > m_NavigationState.lastMoveTime + moveRepeatRate; | |
| else | |
| allow = time > m_NavigationState.lastMoveTime + moveRepeatDelay; | |
| } | |
| if (allow) | |
| { | |
| var eventData = m_NavigationState.eventData; | |
| if (eventData == null) | |
| { | |
| eventData = new ExtendedAxisEventData(eventSystem); | |
| m_NavigationState.eventData = eventData; | |
| } | |
| eventData.Reset(); | |
| eventData.moveVector = moveVector; | |
| eventData.moveDir = moveDirection; | |
| if (IsMoveAllowed(eventData)) | |
| { | |
| ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, eventData, ExecuteEvents.moveHandler); | |
| usedSelectionChange = eventData.used; | |
| m_NavigationState.consecutiveMoveCount = m_NavigationState.consecutiveMoveCount + 1; | |
| m_NavigationState.lastMoveTime = time; | |
| m_NavigationState.lastMoveDirection = moveDirection; | |
| } | |
| } | |
| } | |
| else | |
| m_NavigationState.consecutiveMoveCount = 0; | |
| } | |
| else | |
| { | |
| m_NavigationState.consecutiveMoveCount = 0; | |
| } | |
| // Process submit and cancel events. | |
| if (!usedSelectionChange && eventSystem.currentSelectedGameObject != null) | |
| { | |
| // NOTE: Whereas we use callbacks for the other actions, we rely on WasPressedThisFrame() for | |
| // submit and cancel. This makes their behavior inconsistent with pointer click behavior where | |
| // a click will register on button *up*, but consistent with how other UI systems work where | |
| // click occurs on key press. This nuance in behavior becomes important in combination with | |
| // action enable/disable changes in response to submit or cancel. We react to button *down* | |
| // instead of *up*, so button *up* will come in *after* we have applied the state change. | |
| var submitAction = m_SubmitAction?.action; | |
| var cancelAction = m_CancelAction?.action; | |
| var data = GetBaseEventData(); | |
| if (cancelAction != null && cancelAction.WasPressedThisFrame()) | |
| ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.cancelHandler); | |
| if (!data.used && submitAction != null && submitAction.WasPressedThisFrame()) | |
| ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.submitHandler); | |
| } | |
| } | |
| private bool IsMoveAllowed(AxisEventData eventData) | |
| { | |
| if (m_LocalMultiPlayerRoot == null) | |
| return true; | |
| if (eventSystem.currentSelectedGameObject == null) | |
| return true; | |
| var selectable = eventSystem.currentSelectedGameObject.GetComponent<Selectable>(); | |
| if (selectable == null) | |
| return true; | |
| Selectable navigationTarget = null; | |
| switch (eventData.moveDir) | |
| { | |
| case MoveDirection.Right: | |
| navigationTarget = selectable.FindSelectableOnRight(); | |
| break; | |
| case MoveDirection.Up: | |
| navigationTarget = selectable.FindSelectableOnUp(); | |
| break; | |
| case MoveDirection.Left: | |
| navigationTarget = selectable.FindSelectableOnLeft(); | |
| break; | |
| case MoveDirection.Down: | |
| navigationTarget = selectable.FindSelectableOnDown(); | |
| break; | |
| } | |
| if (navigationTarget == null) | |
| return true; | |
| return navigationTarget.transform.IsChildOf(m_LocalMultiPlayerRoot.transform); | |
| } | |
| [] | |
| [] | |
| [] | |
| private float m_MoveRepeatDelay = 0.5f; | |
| [] | |
| [] | |
| [] | |
| private float m_MoveRepeatRate = 0.1f; | |
| [] | |
| // Hide this while we still have to figure out what to do with this. | |
| private float m_TrackedDeviceDragThresholdMultiplier = 2.0f; | |
| [] | |
| [] | |
| private Transform m_XRTrackingOrigin; | |
| /// <summary> | |
| /// Delay in seconds between an initial move action and a repeated move action while <see cref="move"/> is actuated. | |
| /// </summary> | |
| /// <remarks> | |
| /// While <see cref="move"/> is being held down, the input module will first wait for <see cref="moveRepeatDelay"/> seconds | |
| /// after the first actuation of <see cref="move"/> and then trigger a move event every <see cref="moveRepeatRate"/> seconds. | |
| /// </remarks> | |
| /// <seealso cref="moveRepeatRate"/> | |
| /// <seealso cref="AxisEventData"/> | |
| /// <see cref="move"/> | |
| public float moveRepeatDelay | |
| { | |
| get => m_MoveRepeatDelay; | |
| set => m_MoveRepeatDelay = value; | |
| } | |
| /// <summary> | |
| /// Delay in seconds between repeated move actions while <see cref="move"/> is actuated. | |
| /// </summary> | |
| /// <remarks> | |
| /// While <see cref="move"/> is being held down, the input module will first wait for <see cref="moveRepeatDelay"/> seconds | |
| /// after the first actuation of <see cref="move"/> and then trigger a move event every <see cref="moveRepeatRate"/> seconds. | |
| /// | |
| /// Note that a maximum of one <see cref="AxisEventData"/> will be sent per frame. This means that even if multiple time | |
| /// increments of the repeat delay have passed since the last update, only one move repeat event will be generated. | |
| /// </remarks> | |
| /// <seealso cref="moveRepeatDelay"/> | |
| /// <seealso cref="AxisEventData"/> | |
| /// <see cref="move"/> | |
| public float moveRepeatRate | |
| { | |
| get => m_MoveRepeatRate; | |
| set => m_MoveRepeatRate = value; | |
| } | |
| private bool explictlyIgnoreFocus => InputSystem.settings.backgroundBehavior == InputSettings.BackgroundBehavior.IgnoreFocus; | |
| private bool shouldIgnoreFocus | |
| { | |
| // By default, key this on whether running the background is enabled or not. Rationale is that | |
| // if running in the background is enabled, we already have rules in place what kind of input | |
| // is allowed through and what isn't. And for the input that *IS* allowed through, the UI should | |
| // react. | |
| get => explictlyIgnoreFocus || InputRuntime.s_Instance.runInBackground; | |
| } | |
| [] | |
| public float repeatRate | |
| { | |
| get => moveRepeatRate; | |
| set => moveRepeatRate = value; | |
| } | |
| [] | |
| public float repeatDelay | |
| { | |
| get => moveRepeatDelay; | |
| set => moveRepeatDelay = value; | |
| } | |
| /// <summary> | |
| /// A <see cref="Transform"/> representing the real world origin for tracking devices. | |
| /// This is used to convert real world positions and rotations for <see cref="UIPointerType.Tracked"/> pointers into Unity's global space. | |
| /// When using the XR Interaction Toolkit, this should be pointing to the XR Rig's Transform. | |
| /// </summary> | |
| /// <remarks>This will transform all tracked pointers. If unset, or set to null, the Unity world origin will be used as the basis for all tracked positions and rotations.</remarks> | |
| public Transform xrTrackingOrigin | |
| { | |
| get => m_XRTrackingOrigin; | |
| set => m_XRTrackingOrigin = value; | |
| } | |
| /// <summary> | |
| /// Scales the drag threshold of <c>EventSystem</c> for tracked devices to make selection easier. | |
| /// </summary> | |
| public float trackedDeviceDragThresholdMultiplier | |
| { | |
| get => m_TrackedDeviceDragThresholdMultiplier; | |
| set => m_TrackedDeviceDragThresholdMultiplier = value; | |
| } | |
| private void SwapAction(ref InputActionReference property, InputActionReference newValue, bool actionsHooked, Action<InputAction.CallbackContext> actionCallback) | |
| { | |
| if (property == newValue || (property != null && newValue != null && property.action == newValue.action)) | |
| return; | |
| if (property != null && actionCallback != null && actionsHooked) | |
| { | |
| property.action.performed -= actionCallback; | |
| property.action.canceled -= actionCallback; | |
| } | |
| var oldActionNull = property?.action == null; | |
| var oldActionEnabled = property?.action != null && property.action.enabled; | |
| TryDisableInputAction(property); | |
| property = newValue; | |
| // We source inputs from arbitrary pointers through a set of pointer-related actions (point, click, etc). This means that in any frame, | |
| // multiple pointers may pipe input through to the same action and we do not want the disambiguation code in InputActionState.ShouldIgnoreControlStateChange() | |
| // to prevent input from getting to us. Thus, these actions should generally be set to InputActionType.PassThrough. | |
| // | |
| // We treat navigation actions differently as there is only a single NavigationModel for the UI that all navigation input feeds into. | |
| // Thus, those actions should be configured with disambiguation active (i.e. Move should be a Value action and Submit and Cancel should | |
| // be Button actions). This is especially important for Submit and Cancel as we get proper press and release action this way. | |
| if (newValue != null && newValue.action != null && newValue.action.type != InputActionType.PassThrough && !IsNavigationAction(newValue)) | |
| { | |
| Debug.LogWarning("Pointer-related actions used with the UI input module should generally be set to Pass-Through type so that the module can properly distinguish between " | |
| + $"input from multiple pointers (action {newValue.action} is set to {newValue.action.type})", this); | |
| } | |
| if (newValue?.action != null && actionCallback != null && actionsHooked) | |
| { | |
| property.action.performed += actionCallback; | |
| property.action.canceled += actionCallback; | |
| } | |
| if (isActiveAndEnabled && newValue?.action != null && (oldActionEnabled || oldActionNull)) | |
| EnableInputAction(property); | |
| } | |
| private bool IsNavigationAction(InputActionReference reference) | |
| { | |
| return reference == m_SubmitAction || reference == m_CancelAction || reference == m_MoveAction; | |
| } | |
| /// <summary> | |
| /// An <see cref="InputAction"/> delivering a <see cref="Vector2"/> 2D screen position | |
| /// used as a cursor for pointing at UI elements. | |
| /// </summary> | |
| /// <remarks> | |
| /// The values read from this action determine <see cref="PointerEventData.position"/> and <see cref="PointerEventData.delta"/>. | |
| /// | |
| /// Together with <see cref="leftClick"/>, <see cref="rightClick"/>, <see cref="middleClick"/>, and | |
| /// <see cref="scrollWheel"/>, this forms the basis for pointer-type UI input. | |
| /// | |
| /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its | |
| /// <see cref="InputAction.expectedControlType"/> set to <c>"Vector2"</c>. | |
| /// | |
| /// <example> | |
| /// <code> | |
| /// var asset = ScriptableObject.Create<InputActionAsset>(); | |
| /// var map = asset.AddActionMap("UI"); | |
| /// var pointAction = map.AddAction("Point"); | |
| /// | |
| /// pointAction.AddBinding("<Mouse>/position"); | |
| /// pointAction.AddBinding("<Touchscreen>/touch*/position"); | |
| /// | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).point = | |
| /// InputActionReference.Create(pointAction); | |
| /// </code> | |
| /// </example> | |
| /// </remarks> | |
| /// <seealso cref="leftClick"/> | |
| /// <seealso cref="rightClick"/> | |
| /// <seealso cref="middleClick"/> | |
| /// <seealso cref="scrollWheel"/> | |
| public InputActionReference point | |
| { | |
| get => m_PointAction; | |
| set => SwapAction(ref m_PointAction, value, m_ActionsHooked, m_OnPointDelegate); | |
| } | |
| /// <summary> | |
| /// An <see cref="InputAction"/> delivering a <c>Vector2</c> scroll wheel value | |
| /// used for sending <see cref="PointerEventData"/> events. | |
| /// </summary> | |
| /// <remarks> | |
| /// The values read from this action determine <see cref="PointerEventData.scrollDelta"/>. | |
| /// | |
| /// Together with <see cref="leftClick"/>, <see cref="rightClick"/>, <see cref="middleClick"/>, and | |
| /// <see cref="point"/>, this forms the basis for pointer-type UI input. | |
| /// | |
| /// Note that the action is optional. A pointer is fully functional with just <see cref="point"/> | |
| /// and <see cref="leftClick"/> alone. | |
| /// | |
| /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its | |
| /// <see cref="InputAction.expectedControlType"/> set to <c>"Vector2"</c>. | |
| /// | |
| /// <example> | |
| /// <code> | |
| /// var asset = ScriptableObject.Create<InputActionAsset>(); | |
| /// var map = asset.AddActionMap("UI"); | |
| /// var pointAction = map.AddAction("scroll"); | |
| /// var scrollAction = map.AddAction("scroll"); | |
| /// | |
| /// pointAction.AddBinding("<Mouse>/position"); | |
| /// pointAction.AddBinding("<Touchscreen>/touch*/position"); | |
| /// | |
| /// scrollAction.AddBinding("<Mouse>/scroll"); | |
| /// | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).point = | |
| /// InputActionReference.Create(pointAction); | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).scrollWheel = | |
| /// InputActionReference.Create(scrollAction); | |
| /// </code> | |
| /// </example> | |
| /// </remarks> | |
| /// <seealso cref="leftClick"/> | |
| /// <seealso cref="rightClick"/> | |
| /// <seealso cref="middleClick"/> | |
| /// <seealso cref="point"/> | |
| public InputActionReference scrollWheel | |
| { | |
| get => m_ScrollWheelAction; | |
| set => SwapAction(ref m_ScrollWheelAction, value, m_ActionsHooked, m_OnScrollWheelDelegate); | |
| } | |
| /// <summary> | |
| /// An <see cref="InputAction"/> delivering a <c>float</c> button value that determines | |
| /// whether the left button of a pointer is pressed. | |
| /// </summary> | |
| /// <remarks> | |
| /// Clicks on this button will use <see cref="PointerEventData.InputButton.Left"/> for <see cref="PointerEventData.button"/>. | |
| /// | |
| /// Together with <see cref="point"/>, <see cref="rightClick"/>, <see cref="middleClick"/>, and | |
| /// <see cref="scrollWheel"/>, this forms the basis for pointer-type UI input. | |
| /// | |
| /// Note that together with <see cref="point"/>, this action is necessary for a pointer to be functional. The other clicks | |
| /// and <see cref="scrollWheel"/> are optional, however. | |
| /// | |
| /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its | |
| /// <see cref="InputAction.expectedControlType"/> set to <c>"Button"</c>. | |
| /// | |
| /// <example> | |
| /// <code> | |
| /// var asset = ScriptableObject.Create<InputActionAsset>(); | |
| /// var map = asset.AddActionMap("UI"); | |
| /// var pointAction = map.AddAction("scroll"); | |
| /// var clickAction = map.AddAction("click"); | |
| /// | |
| /// pointAction.AddBinding("<Mouse>/position"); | |
| /// pointAction.AddBinding("<Touchscreen>/touch*/position"); | |
| /// | |
| /// clickAction.AddBinding("<Mouse>/leftButton"); | |
| /// clickAction.AddBinding("<Touchscreen>/touch*/press"); | |
| /// | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).point = | |
| /// InputActionReference.Create(pointAction); | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).leftClick = | |
| /// InputActionReference.Create(clickAction); | |
| /// </code> | |
| /// </example> | |
| /// </remarks> | |
| /// <seealso cref="rightClick"/> | |
| /// <seealso cref="middleClick"/> | |
| /// <seealso cref="scrollWheel"/> | |
| /// <seealso cref="point"/> | |
| public InputActionReference leftClick | |
| { | |
| get => m_LeftClickAction; | |
| set => SwapAction(ref m_LeftClickAction, value, m_ActionsHooked, m_OnLeftClickDelegate); | |
| } | |
| /// <summary> | |
| /// An <see cref="InputAction"/> delivering a <c>float</c> button value that determines | |
| /// whether the middle button of a pointer is pressed. | |
| /// </summary> | |
| /// <remarks> | |
| /// Clicks on this button will use <see cref="PointerEventData.InputButton.Middle"/> for <see cref="PointerEventData.button"/>. | |
| /// | |
| /// Together with <see cref="leftClick"/>, <see cref="rightClick"/>, <see cref="scrollWheel"/>, and | |
| /// <see cref="point"/>, this forms the basis for pointer-type UI input. | |
| /// | |
| /// Note that the action is optional. A pointer is fully functional with just <see cref="point"/> | |
| /// and <see cref="leftClick"/> alone. | |
| /// | |
| /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its | |
| /// <see cref="InputAction.expectedControlType"/> set to <c>"Button"</c>. | |
| /// | |
| /// <example> | |
| /// <code> | |
| /// var asset = ScriptableObject.Create<InputActionAsset>(); | |
| /// var map = asset.AddActionMap("UI"); | |
| /// var pointAction = map.AddAction("scroll"); | |
| /// var leftClickAction = map.AddAction("leftClick"); | |
| /// var middleClickAction = map.AddAction("middleClick"); | |
| /// | |
| /// pointAction.AddBinding("<Mouse>/position"); | |
| /// pointAction.AddBinding("<Touchscreen>/touch*/position"); | |
| /// | |
| /// leftClickAction.AddBinding("<Mouse>/leftButton"); | |
| /// leftClickAction.AddBinding("<Touchscreen>/touch*/press"); | |
| /// | |
| /// middleClickAction.AddBinding("<Mouse>/middleButton"); | |
| /// | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).point = | |
| /// InputActionReference.Create(pointAction); | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).leftClick = | |
| /// InputActionReference.Create(leftClickAction); | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).middleClick = | |
| /// InputActionReference.Create(middleClickAction); | |
| /// </code> | |
| /// </example> | |
| /// </remarks> | |
| /// <seealso cref="leftClick"/> | |
| /// <seealso cref="rightClick"/> | |
| /// <seealso cref="scrollWheel"/> | |
| /// <seealso cref="point"/> | |
| public InputActionReference middleClick | |
| { | |
| get => m_MiddleClickAction; | |
| set => SwapAction(ref m_MiddleClickAction, value, m_ActionsHooked, m_OnMiddleClickDelegate); | |
| } | |
| /// <summary> | |
| /// An <see cref="InputAction"/> delivering a <c>float"</c> button value that determines | |
| /// whether the right button of a pointer is pressed. | |
| /// </summary> | |
| /// <remarks> | |
| /// Clicks on this button will use <see cref="PointerEventData.InputButton.Right"/> for <see cref="PointerEventData.button"/>. | |
| /// | |
| /// Together with <see cref="leftClick"/>, <see cref="middleClick"/>, <see cref="scrollWheel"/>, and | |
| /// <see cref="point"/>, this forms the basis for pointer-type UI input. | |
| /// | |
| /// Note that the action is optional. A pointer is fully functional with just <see cref="point"/> | |
| /// and <see cref="leftClick"/> alone. | |
| /// | |
| /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its | |
| /// <see cref="InputAction.expectedControlType"/> set to <c>"Button"</c>. | |
| /// | |
| /// <example> | |
| /// <code> | |
| /// var asset = ScriptableObject.Create<InputActionAsset>(); | |
| /// var map = asset.AddActionMap("UI"); | |
| /// var pointAction = map.AddAction("scroll"); | |
| /// var leftClickAction = map.AddAction("leftClick"); | |
| /// var rightClickAction = map.AddAction("rightClick"); | |
| /// | |
| /// pointAction.AddBinding("<Mouse>/position"); | |
| /// pointAction.AddBinding("<Touchscreen>/touch*/position"); | |
| /// | |
| /// leftClickAction.AddBinding("<Mouse>/leftButton"); | |
| /// leftClickAction.AddBinding("<Touchscreen>/touch*/press"); | |
| /// | |
| /// rightClickAction.AddBinding("<Mouse>/rightButton"); | |
| /// | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).point = | |
| /// InputActionReference.Create(pointAction); | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).leftClick = | |
| /// InputActionReference.Create(leftClickAction); | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).rightClick = | |
| /// InputActionReference.Create(rightClickAction); | |
| /// </code> | |
| /// </example> | |
| /// </remarks> | |
| /// <seealso cref="leftClick"/> | |
| /// <seealso cref="middleClick"/> | |
| /// <seealso cref="scrollWheel"/> | |
| /// <seealso cref="point"/> | |
| public InputActionReference rightClick | |
| { | |
| get => m_RightClickAction; | |
| set => SwapAction(ref m_RightClickAction, value, m_ActionsHooked, m_OnRightClickDelegate); | |
| } | |
| /// <summary> | |
| /// An <see cref="InputAction"/> delivering a <c>Vector2</c> 2D motion vector | |
| /// used for sending <see cref="AxisEventData"/> navigation events. | |
| /// </summary> | |
| /// <remarks> | |
| /// The events generated from this input will be received by <see cref="IMoveHandler.OnMove"/>. | |
| /// | |
| /// This action together with <see cref="submit"/> and <see cref="cancel"/> form the sources for navigation-style | |
| /// UI input. | |
| /// | |
| /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its | |
| /// <see cref="InputAction.expectedControlType"/> set to <c>"Vector2"</c>. | |
| /// | |
| /// <example> | |
| /// <code> | |
| /// var asset = ScriptableObject.Create<InputActionAsset>(); | |
| /// var map = asset.AddActionMap("UI"); | |
| /// var pointAction = map.AddAction("move"); | |
| /// var submitAction = map.AddAction("submit"); | |
| /// var cancelAction = map.AddAction("cancel"); | |
| /// | |
| /// moveAction.AddBinding("<Gamepad>/*stick"); | |
| /// moveAction.AddBinding("<Gamepad>/dpad"); | |
| /// submitAction.AddBinding("<Gamepad>/buttonSouth"); | |
| /// cancelAction.AddBinding("<Gamepad>/buttonEast"); | |
| /// | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).move = | |
| /// InputActionReference.Create(moveAction); | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).submit = | |
| /// InputActionReference.Create(submitAction); | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).cancelAction = | |
| /// InputActionReference.Create(cancelAction); | |
| /// </code> | |
| /// </example> | |
| /// </remarks> | |
| /// <seealso cref="submit"/> | |
| /// <seealso cref="cancel"/> | |
| public InputActionReference move | |
| { | |
| get => m_MoveAction; | |
| set => SwapAction(ref m_MoveAction, value, m_ActionsHooked, m_OnMoveDelegate); | |
| } | |
| /// <summary> | |
| /// An <see cref="InputAction"/> delivering a <c>float</c> button value that determines when <c>ISubmitHandler</c> | |
| /// is triggered. | |
| /// </summary> | |
| /// <remarks> | |
| /// The events generated from this input will be received by <see cref="ISubmitHandler"/>. | |
| /// | |
| /// This action together with <see cref="move"/> and <see cref="cancel"/> form the sources for navigation-style | |
| /// UI input. | |
| /// | |
| /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.Button"/>. | |
| /// | |
| /// <example> | |
| /// <code> | |
| /// var asset = ScriptableObject.Create<InputActionAsset>(); | |
| /// var map = asset.AddActionMap("UI"); | |
| /// var pointAction = map.AddAction("move"); | |
| /// var submitAction = map.AddAction("submit"); | |
| /// var cancelAction = map.AddAction("cancel"); | |
| /// | |
| /// moveAction.AddBinding("<Gamepad>/*stick"); | |
| /// moveAction.AddBinding("<Gamepad>/dpad"); | |
| /// submitAction.AddBinding("<Gamepad>/buttonSouth"); | |
| /// cancelAction.AddBinding("<Gamepad>/buttonEast"); | |
| /// | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).move = | |
| /// InputActionReference.Create(moveAction); | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).submit = | |
| /// InputActionReference.Create(submitAction); | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).cancelAction = | |
| /// InputActionReference.Create(cancelAction); | |
| /// </code> | |
| /// </example> | |
| /// </remarks> | |
| /// <seealso cref="move"/> | |
| /// <seealso cref="cancel"/> | |
| public InputActionReference submit | |
| { | |
| get => m_SubmitAction; | |
| set => SwapAction(ref m_SubmitAction, value, m_ActionsHooked, null); | |
| } | |
| /// <summary> | |
| /// An <see cref="InputAction"/> delivering a <c>float</c> button value that determines when <c>ICancelHandler</c> | |
| /// is triggered. | |
| /// </summary> | |
| /// <remarks> | |
| /// The events generated from this input will be received by <see cref="ICancelHandler"/>. | |
| /// | |
| /// This action together with <see cref="move"/> and <see cref="submit"/> form the sources for navigation-style | |
| /// UI input. | |
| /// | |
| /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.Button"/>. | |
| /// | |
| /// <example> | |
| /// <code> | |
| /// var asset = ScriptableObject.Create<InputActionAsset>(); | |
| /// var map = asset.AddActionMap("UI"); | |
| /// var pointAction = map.AddAction("move"); | |
| /// var submitAction = map.AddAction("submit"); | |
| /// var cancelAction = map.AddAction("cancel"); | |
| /// | |
| /// moveAction.AddBinding("<Gamepad>/*stick"); | |
| /// moveAction.AddBinding("<Gamepad>/dpad"); | |
| /// submitAction.AddBinding("<Gamepad>/buttonSouth"); | |
| /// cancelAction.AddBinding("<Gamepad>/buttonEast"); | |
| /// | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).move = | |
| /// InputActionReference.Create(moveAction); | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).submit = | |
| /// InputActionReference.Create(submitAction); | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).cancelAction = | |
| /// InputActionReference.Create(cancelAction); | |
| /// </code> | |
| /// </example> | |
| /// </remarks> | |
| /// <seealso cref="move"/> | |
| /// <seealso cref="submit"/> | |
| public InputActionReference cancel | |
| { | |
| get => m_CancelAction; | |
| set => SwapAction(ref m_CancelAction, value, m_ActionsHooked, null); | |
| } | |
| /// <summary> | |
| /// An <see cref="InputAction"/> delivering a <c>Quaternion</c> value reflecting the orientation of <see cref="TrackedDevice"/>s. | |
| /// In combination with <see cref="trackedDevicePosition"/>, this is used to determine the transform of tracked devices from which | |
| /// to raycast into the UI scene. | |
| /// </summary> | |
| /// <remarks> | |
| /// <see cref="trackedDeviceOrientation"/> and <see cref="trackedDevicePosition"/> together replace <see cref="point"/> for | |
| /// UI input from <see cref="TrackedDevice"/>. Other than that, UI input for tracked devices is no different from "normal" | |
| /// pointer-type input. This means that <see cref="leftClick"/>, <see cref="rightClick"/>, <see cref="middleClick"/>, and | |
| /// <see cref="scrollWheel"/> can all be used for tracked device input like for regular pointer input. | |
| /// | |
| /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its | |
| /// <see cref="InputAction.expectedControlType"/> set to <c>"Quaternion"</c>. | |
| /// | |
| /// <example> | |
| /// <code> | |
| /// var asset = ScriptableObject.Create<InputActionAsset>(); | |
| /// var map = asset.AddActionMap("UI"); | |
| /// var positionAction = map.AddAction("position"); | |
| /// var orientationAction = map.AddAction("orientation"); | |
| /// var clickAction = map.AddAction("click"); | |
| /// | |
| /// positionAction.AddBinding("<TrackedDevice>/devicePosition"); | |
| /// orientationAction.AddBinding("<TrackedDevice>/deviceRotation"); | |
| /// clickAction.AddBinding("<TrackedDevice>/trigger"); | |
| /// | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).trackedDevicePosition = | |
| /// InputActionReference.Create(positionAction); | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).trackedDeviceOrientation = | |
| /// InputActionReference.Create(orientationAction); | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).leftClick = | |
| /// InputActionReference.Create(clickAction); | |
| /// </code> | |
| /// </example> | |
| /// </remarks> | |
| /// <seealso cref="trackedDevicePosition"/> | |
| public InputActionReference trackedDeviceOrientation | |
| { | |
| get => m_TrackedDeviceOrientationAction; | |
| set => SwapAction(ref m_TrackedDeviceOrientationAction, value, m_ActionsHooked, m_OnTrackedDeviceOrientationDelegate); | |
| } | |
| /// <summary> | |
| /// An <see cref="InputAction"/> delivering a <c>Vector3</c> value reflecting the position of <see cref="TrackedDevice"/>s. | |
| /// In combination with <see cref="trackedDeviceOrientation"/>, this is used to determine the transform of tracked devices from which | |
| /// to raycast into the UI scene. | |
| /// </summary> | |
| /// <remarks> | |
| /// <see cref="trackedDeviceOrientation"/> and <see cref="trackedDevicePosition"/> together replace <see cref="point"/> for | |
| /// UI input from <see cref="TrackedDevice"/>. Other than that, UI input for tracked devices is no different from "normal" | |
| /// pointer-type input. This means that <see cref="leftClick"/>, <see cref="rightClick"/>, <see cref="middleClick"/>, and | |
| /// <see cref="scrollWheel"/> can all be used for tracked device input like for regular pointer input. | |
| /// | |
| /// This action should have its <see cref="InputAction.type"/> set to <see cref="InputActionType.PassThrough"/> and its | |
| /// <see cref="InputAction.expectedControlType"/> set to <c>"Vector3"</c>. | |
| /// | |
| /// <example> | |
| /// <code> | |
| /// var asset = ScriptableObject.Create<InputActionAsset>(); | |
| /// var map = asset.AddActionMap("UI"); | |
| /// var positionAction = map.AddAction("position"); | |
| /// var orientationAction = map.AddAction("orientation"); | |
| /// var clickAction = map.AddAction("click"); | |
| /// | |
| /// positionAction.AddBinding("<TrackedDevice>/devicePosition"); | |
| /// orientationAction.AddBinding("<TrackedDevice>/deviceRotation"); | |
| /// clickAction.AddBinding("<TrackedDevice>/trigger"); | |
| /// | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).trackedDevicePosition = | |
| /// InputActionReference.Create(positionAction); | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).trackedDeviceOrientation = | |
| /// InputActionReference.Create(orientationAction); | |
| /// ((InputSystemUIInputModule)EventSystem.current.currentInputModule).leftClick = | |
| /// InputActionReference.Create(clickAction); | |
| /// </code> | |
| /// </example> | |
| /// </remarks> | |
| /// <seealso cref="trackedDeviceOrientation"/> | |
| public InputActionReference trackedDevicePosition | |
| { | |
| get => m_TrackedDevicePositionAction; | |
| set => SwapAction(ref m_TrackedDevicePositionAction, value, m_ActionsHooked, m_OnTrackedDevicePositionDelegate); | |
| } | |
| /// <summary> | |
| /// Assigns default input actions asset and input actions, similar to how defaults are assigned when creating UI module in editor. | |
| /// Useful for creating <see cref="InputSystemUIInputModule"/> at runtime. | |
| /// </summary> | |
| /// <remarks> | |
| /// This instantiates <see cref="DefaultInputActions"/> and assigns it to <see cref="actionsAsset"/>. It also | |
| /// assigns all the various individual actions such as <see cref="point"/> and <see cref="leftClick"/>. | |
| /// | |
| /// Note that if an <c>InputSystemUIInputModule</c> component is programmatically added to a <c>GameObject</c>, | |
| /// it will automatically receive the default actions as part of its <c>OnEnable</c> method. Use <see cref="UnassignActions"/> | |
| /// to remove these assignments. | |
| /// | |
| /// <example> | |
| /// <code> | |
| /// var go = new GameObject(); | |
| /// go.AddComponent<EventSystem>(); | |
| /// | |
| /// // Adding the UI module like this will implicitly enable it and thus lead to | |
| /// // automatic assignment of the default input actions. | |
| /// var uiModule = go.AddComponent<InputSystemUIInputModule>(); | |
| /// | |
| /// // Manually remove the default input actions. | |
| /// uiModule.UnassignActions(); | |
| /// </code> | |
| /// </example> | |
| /// </remarks> | |
| /// <seealso cref="actionsAsset"/> | |
| /// <seealso cref="DefaultInputActions"/> | |
| public void AssignDefaultActions() | |
| { | |
| var defaultActions = new DefaultInputActions(); | |
| actionsAsset = defaultActions.asset; | |
| cancel = InputActionReference.Create(defaultActions.UI.Cancel); | |
| submit = InputActionReference.Create(defaultActions.UI.Submit); | |
| move = InputActionReference.Create(defaultActions.UI.Navigate); | |
| leftClick = InputActionReference.Create(defaultActions.UI.Click); | |
| rightClick = InputActionReference.Create(defaultActions.UI.RightClick); | |
| middleClick = InputActionReference.Create(defaultActions.UI.MiddleClick); | |
| point = InputActionReference.Create(defaultActions.UI.Point); | |
| scrollWheel = InputActionReference.Create(defaultActions.UI.ScrollWheel); | |
| trackedDeviceOrientation = InputActionReference.Create(defaultActions.UI.TrackedDeviceOrientation); | |
| trackedDevicePosition = InputActionReference.Create(defaultActions.UI.TrackedDevicePosition); | |
| } | |
| /// <summary> | |
| /// Remove all action assignments, that is <see cref="actionsAsset"/> as well as all individual | |
| /// actions such as <see cref="leftClick"/>. | |
| /// </summary> | |
| /// <remarks> | |
| /// If the current actions were enabled by the UI input module, they will be disabled in the process. | |
| /// </remarks> | |
| /// <seealso cref="AssignDefaultActions"/> | |
| public void UnassignActions() | |
| { | |
| actionsAsset = default; | |
| cancel = default; | |
| submit = default; | |
| move = default; | |
| leftClick = default; | |
| rightClick = default; | |
| middleClick = default; | |
| point = default; | |
| scrollWheel = default; | |
| trackedDeviceOrientation = default; | |
| trackedDevicePosition = default; | |
| } | |
| [] | |
| public InputActionReference trackedDeviceSelect | |
| { | |
| get => throw new InvalidOperationException(); | |
| set => throw new InvalidOperationException(); | |
| } | |
| protected override void Reset() | |
| { | |
| base.Reset(); | |
| var asset = (InputActionAsset)AssetDatabase.LoadAssetAtPath( | |
| UnityEngine.InputSystem.Editor.PlayerInputEditor.kDefaultInputActionsAssetPath, | |
| typeof(InputActionAsset)); | |
| // Setting default asset and actions when creating via inspector | |
| Editor.InputSystemUIInputModuleEditor.ReassignActions(this, asset); | |
| } | |
| protected override void Awake() | |
| { | |
| base.Awake(); | |
| m_NavigationState.Reset(); | |
| } | |
| protected override void OnDestroy() | |
| { | |
| base.OnDestroy(); | |
| UnhookActions(); | |
| } | |
| protected override void OnEnable() | |
| { | |
| base.OnEnable(); | |
| if (m_OnControlsChangedDelegate == null) | |
| m_OnControlsChangedDelegate = OnControlsChanged; | |
| InputActionState.s_GlobalState.onActionControlsChanged.AddCallback(m_OnControlsChangedDelegate); | |
| if (HasNoActions()) | |
| AssignDefaultActions(); | |
| ResetPointers(); | |
| HookActions(); | |
| EnableAllActions(); | |
| } | |
| protected override void OnDisable() | |
| { | |
| ResetPointers(); | |
| InputActionState.s_GlobalState.onActionControlsChanged.RemoveCallback(m_OnControlsChangedDelegate); | |
| DisableAllActions(); | |
| UnhookActions(); | |
| base.OnDisable(); | |
| } | |
| private void ResetPointers() | |
| { | |
| var numPointers = m_PointerStates.length; | |
| for (var i = 0; i < numPointers; ++i) | |
| SendPointerExitEventsAndRemovePointer(0); | |
| m_CurrentPointerId = -1; | |
| m_CurrentPointerIndex = -1; | |
| m_CurrentPointerType = UIPointerType.None; | |
| } | |
| private bool HasNoActions() | |
| { | |
| if (m_ActionsAsset != null) | |
| return false; | |
| return m_PointAction?.action == null | |
| && m_LeftClickAction?.action == null | |
| && m_RightClickAction?.action == null | |
| && m_MiddleClickAction?.action == null | |
| && m_SubmitAction?.action == null | |
| && m_CancelAction?.action == null | |
| && m_ScrollWheelAction?.action == null | |
| && m_TrackedDeviceOrientationAction?.action == null | |
| && m_TrackedDevicePositionAction?.action == null; | |
| } | |
| private void EnableAllActions() | |
| { | |
| EnableInputAction(m_PointAction); | |
| EnableInputAction(m_LeftClickAction); | |
| EnableInputAction(m_RightClickAction); | |
| EnableInputAction(m_MiddleClickAction); | |
| EnableInputAction(m_MoveAction); | |
| EnableInputAction(m_SubmitAction); | |
| EnableInputAction(m_CancelAction); | |
| EnableInputAction(m_ScrollWheelAction); | |
| EnableInputAction(m_TrackedDeviceOrientationAction); | |
| EnableInputAction(m_TrackedDevicePositionAction); | |
| } | |
| private void DisableAllActions() | |
| { | |
| TryDisableInputAction(m_PointAction, true); | |
| TryDisableInputAction(m_LeftClickAction, true); | |
| TryDisableInputAction(m_RightClickAction, true); | |
| TryDisableInputAction(m_MiddleClickAction, true); | |
| TryDisableInputAction(m_MoveAction, true); | |
| TryDisableInputAction(m_SubmitAction, true); | |
| TryDisableInputAction(m_CancelAction, true); | |
| TryDisableInputAction(m_ScrollWheelAction, true); | |
| TryDisableInputAction(m_TrackedDeviceOrientationAction, true); | |
| TryDisableInputAction(m_TrackedDevicePositionAction, true); | |
| } | |
| private void EnableInputAction(InputActionReference inputActionReference) | |
| { | |
| var action = inputActionReference?.action; | |
| if (action == null) | |
| return; | |
| if (s_InputActionReferenceCounts.TryGetValue(action, out var referenceState)) | |
| { | |
| referenceState.refCount++; | |
| s_InputActionReferenceCounts[action] = referenceState; | |
| } | |
| else | |
| { | |
| // if the action is already enabled but its reference count is zero then it was enabled by | |
| // something outside the input module and the input module should never disable it. | |
| referenceState = new InputActionReferenceState {refCount = 1, enabledByInputModule = !action.enabled}; | |
| s_InputActionReferenceCounts.Add(action, referenceState); | |
| } | |
| action.Enable(); | |
| } | |
| private void TryDisableInputAction(InputActionReference inputActionReference, bool isComponentDisabling = false) | |
| { | |
| var action = inputActionReference?.action; | |
| if (action == null) | |
| return; | |
| // Don't decrement refCount when we were not responsible for incrementing it. | |
| // I.e. when we were not enabled yet. When OnDisabled is called, isActiveAndEnabled will | |
| // already have been set to false. In that case we pass isComponentDisabling to check if we | |
| // came from OnDisabled and therefore need to allow disabling. | |
| if (!isActiveAndEnabled && !isComponentDisabling) | |
| return; | |
| if (!s_InputActionReferenceCounts.TryGetValue(action, out var referenceState)) | |
| return; | |
| if (referenceState.refCount - 1 == 0 && referenceState.enabledByInputModule) | |
| { | |
| action.Disable(); | |
| s_InputActionReferenceCounts.Remove(action); | |
| return; | |
| } | |
| referenceState.refCount--; | |
| s_InputActionReferenceCounts[action] = referenceState; | |
| } | |
| private int GetPointerStateIndexFor(int pointerOrTouchId) | |
| { | |
| if (pointerOrTouchId == m_CurrentPointerId) | |
| return m_CurrentPointerIndex; | |
| for (var i = 0; i < m_PointerIds.length; ++i) | |
| if (m_PointerIds[i] == pointerOrTouchId) | |
| return i; | |
| // Search for Device or Touch Ids as a fallback | |
| for (var i = 0; i < m_PointerStates.length; ++i) | |
| { | |
| var eventData = m_PointerStates[i].eventData; | |
| if (eventData.touchId == pointerOrTouchId || (eventData.touchId != 0 && eventData.device.deviceId == pointerOrTouchId)) | |
| return i; | |
| } | |
| return -1; | |
| } | |
| private ref PointerModel GetPointerStateForIndex(int index) | |
| { | |
| if (index == 0) | |
| return ref m_PointerStates.firstValue; | |
| return ref m_PointerStates.additionalValues[index - 1]; | |
| } | |
| private int GetDisplayIndexFor(InputControl control) | |
| { | |
| int displayIndex = 0; | |
| if (control.device is Pointer pointerCast) | |
| { | |
| displayIndex = pointerCast.displayIndex.ReadValue(); | |
| Debug.Assert(displayIndex <= byte.MaxValue, "Display index was larger than expected"); | |
| } | |
| return displayIndex; | |
| } | |
| private int GetPointerStateIndexFor(ref InputAction.CallbackContext context) | |
| { | |
| if (CheckForRemovedDevice(ref context)) | |
| return -1; | |
| var phase = context.phase; | |
| return GetPointerStateIndexFor(context.control, createIfNotExists: phase != InputActionPhase.Canceled); | |
| } | |
| // This is the key method for determining which pointer a particular input is associated with. | |
| // The principal determinant is the device that is sending the input which, in general, is expected | |
| // to be a Pointer (Mouse, Pen, Touchscreen) or TrackedDevice. | |
| // | |
| // Note, however, that the input is not guaranteed to even come from a pointer-like device. One can | |
| // bind the space key to a left click, for example. As long as we have an active pointer that can | |
| // deliver position input, we accept that setup and treat pressing the space key the same as pressing | |
| // the left button input on the respective pointer. | |
| // | |
| // Quite a lot going on in this method but we're dealing with three different UI interaction paradigms | |
| // here which we all support from a single input path and allow seamless switching between. | |
| private int GetPointerStateIndexFor(InputControl control, bool createIfNotExists = true) | |
| { | |
| Debug.Assert(control != null, "Control must not be null"); | |
| ////REVIEW: Any way we can cut down on the hops all over memory that we're doing here? | |
| var device = control.device; | |
| ////TODO: We're repeatedly inspecting the control setup here. Do this once and only redo it if the control setup changes. | |
| ////REVIEW: It seems wrong that we are picking up an input here that is *NOT* reflected in our actions. We just end | |
| //// up reading a touchId control implicitly instead of allowing actions to deliver IDs to us. On the other hand, | |
| //// making that setup explicit in actions may be quite awkward and not nearly as robust. | |
| // Determine the pointer (and touch) ID. We default the pointer ID to the device | |
| // ID of the InputDevice. | |
| var controlParent = control.parent; | |
| var touchControlIndex = m_PointerTouchControls.IndexOfReference(controlParent); | |
| if (touchControlIndex != -1) | |
| { | |
| // For touches, we cache a reference to the control of a pointer so that we don't | |
| // have to continuously do ReadValue() on the touch ID control. | |
| m_CurrentPointerId = m_PointerIds[touchControlIndex]; | |
| m_CurrentPointerIndex = touchControlIndex; | |
| m_CurrentPointerType = UIPointerType.Touch; | |
| return touchControlIndex; | |
| } | |
| var pointerId = device.deviceId; | |
| var touchId = 0; | |
| var touchPosition = Vector2.zero; | |
| // Need to check if it's a touch so that we get a correct pointerId. | |
| if (controlParent is TouchControl touchControl) | |
| { | |
| touchId = touchControl.touchId.value; | |
| touchPosition = touchControl.position.value; | |
| } | |
| // Could be it's a toplevel control on Touchscreen (like "<Touchscreen>/position"). In that case, | |
| // read the touch ID from primaryTouch. | |
| else if (controlParent is Touchscreen touchscreen) | |
| { | |
| touchId = touchscreen.primaryTouch.touchId.value; | |
| touchPosition = touchscreen.primaryTouch.position.value; | |
| } | |
| int displayIndex = GetDisplayIndexFor(control); | |
| if (touchId != 0) | |
| pointerId = ExtendedPointerEventData.MakePointerIdForTouch(pointerId, touchId); | |
| // Early out if it's the last used pointer. | |
| // NOTE: Can't just compare by device here because of touchscreens potentially having multiple associated pointers. | |
| if (m_CurrentPointerId == pointerId) | |
| return m_CurrentPointerIndex; | |
| // Search m_PointerIds for an existing entry. | |
| // NOTE: This is a linear search but m_PointerIds is only IDs and the number of concurrent pointers | |
| // should be very low at any one point (in fact, we don't generally expect to have more than one | |
| // which is why we are using InlinedArrays). | |
| if (touchId == 0) // Not necessary for touches; see above. | |
| { | |
| for (var i = 0; i < m_PointerIds.length; i++) | |
| { | |
| if (m_PointerIds[i] == pointerId) | |
| { | |
| // Existing entry found. Make it the current pointer. | |
| m_CurrentPointerId = pointerId; | |
| m_CurrentPointerIndex = i; | |
| m_CurrentPointerType = m_PointerStates[i].pointerType; | |
| return i; | |
| } | |
| } | |
| } | |
| if (!createIfNotExists) | |
| return -1; | |
| // Determine pointer type. | |
| var pointerType = UIPointerType.None; | |
| if (touchId != 0) | |
| pointerType = UIPointerType.Touch; | |
| else if (HaveControlForDevice(device, point)) | |
| pointerType = UIPointerType.MouseOrPen; | |
| else if (HaveControlForDevice(device, trackedDevicePosition)) | |
| pointerType = UIPointerType.Tracked; | |
| // For SingleMouseOrPenButMultiTouchAndTrack, we keep a single pointer for mouse and pen but only for as | |
| // long as there is no touch or tracked input. If we get that kind, we remove the mouse/pen pointer. | |
| if (m_PointerBehavior == UIPointerBehavior.SingleMouseOrPenButMultiTouchAndTrack && pointerType != UIPointerType.None) | |
| { | |
| if (pointerType == UIPointerType.MouseOrPen) | |
| { | |
| // We have input on a mouse or pen. Kill all touch and tracked pointers we may have. | |
| for (var i = 0; i < m_PointerStates.length; ++i) | |
| { | |
| if (m_PointerStates[i].pointerType != UIPointerType.MouseOrPen) | |
| { | |
| SendPointerExitEventsAndRemovePointer(i); | |
| --i; | |
| } | |
| } | |
| } | |
| else | |
| { | |
| // We have touch or tracked input. Kill mouse/pen pointer, if we have it. | |
| for (var i = 0; i < m_PointerStates.length; ++i) | |
| { | |
| if (m_PointerStates[i].pointerType == UIPointerType.MouseOrPen) | |
| { | |
| SendPointerExitEventsAndRemovePointer(i); | |
| --i; | |
| } | |
| } | |
| } | |
| } | |
| ////REVIEW: For touch, probably makes sense to force-ignore any input other than from primaryTouch. | |
| // If the behavior is SingleUnifiedPointer, we only ever create a single pointer state | |
| // and use that for all pointer input that is coming in. | |
| if ((m_PointerBehavior == UIPointerBehavior.SingleUnifiedPointer && pointerType != UIPointerType.None) || | |
| (m_PointerBehavior == UIPointerBehavior.SingleMouseOrPenButMultiTouchAndTrack && pointerType == UIPointerType.MouseOrPen)) | |
| { | |
| if (m_CurrentPointerIndex == -1) | |
| { | |
| m_CurrentPointerIndex = AllocatePointer(pointerId, displayIndex, touchId, pointerType, control, device, touchId != 0 ? controlParent : null); | |
| } | |
| else | |
| { | |
| // Update pointer record to reflect current device. We know they're different because we checked | |
| // m_CurrentPointerId earlier in the method. | |
| // NOTE: This path may repeatedly switch the pointer type and ID on the same single event instance. | |
| ref var pointer = ref GetPointerStateForIndex(m_CurrentPointerIndex); | |
| var eventData = pointer.eventData; | |
| eventData.control = control; | |
| eventData.device = device; | |
| eventData.pointerType = pointerType; | |
| eventData.pointerId = pointerId; | |
| eventData.touchId = touchId; | |
| eventData.displayIndex = displayIndex; | |
| // Make sure these don't linger around when we switch to a different kind of pointer. | |
| eventData.trackedDeviceOrientation = default; | |
| eventData.trackedDevicePosition = default; | |
| } | |
| if (pointerType == UIPointerType.Touch) | |
| GetPointerStateForIndex(m_CurrentPointerIndex).screenPosition = touchPosition; | |
| m_CurrentPointerId = pointerId; | |
| m_CurrentPointerType = pointerType; | |
| return m_CurrentPointerIndex; | |
| } | |
| // No existing record for the device. Find out if the device has the ability to point at all. | |
| // If not, we need to use a pointer state from a different device (if present). | |
| var index = -1; | |
| if (pointerType != UIPointerType.None) | |
| { | |
| // Device has an associated position input. Create a new pointer record. | |
| index = AllocatePointer(pointerId, displayIndex, touchId, pointerType, control, device, touchId != 0 ? controlParent : null); | |
| } | |
| else | |
| { | |
| // Device has no associated position input. Find a pointer device to route the change into. | |
| // As a last resort, create a pointer without a position input. | |
| // If we have a current pointer, route the input into that. The majority of times we end | |
| // up in this branch, this should settle things. | |
| if (m_CurrentPointerId != -1) | |
| return m_CurrentPointerIndex; | |
| // NOTE: In most cases, we end up here when there is input on a non-pointer device bound to one of the pointer-related | |
| // actions before there is input from a pointer device. In this scenario, we don't have a pointer state allocated | |
| // for the device yet. | |
| // If we have anything bound to the `point` action, create a pointer for it. | |
| var pointControls = point?.action?.controls; | |
| var pointerDevice = pointControls.HasValue && pointControls.Value.Count > 0 ? pointControls.Value[0].device : null; | |
| if (pointerDevice != null && !(pointerDevice is Touchscreen)) // Touchscreen only temporarily allocate pointer states. | |
| { | |
| // Create MouseOrPen style pointer. | |
| index = AllocatePointer(pointerDevice.deviceId, displayIndex, 0, UIPointerType.MouseOrPen, pointControls.Value[0], pointerDevice); | |
| } | |
| else | |
| { | |
| // Do the same but look at the `position` action. | |
| var positionControls = trackedDevicePosition?.action?.controls; | |
| var trackedDevice = positionControls.HasValue && positionControls.Value.Count > 0 | |
| ? positionControls.Value[0].device | |
| : null; | |
| if (trackedDevice != null) | |
| { | |
| // Create a Tracked style pointer. | |
| index = AllocatePointer(trackedDevice.deviceId, displayIndex, 0, UIPointerType.Tracked, positionControls.Value[0], trackedDevice); | |
| } | |
| else | |
| { | |
| // We got input from a non-pointer device and apparently there's no pointer we can route the | |
| // input into. Just create a pointer state for the device and leave it at that. | |
| index = AllocatePointer(pointerId, displayIndex, 0, UIPointerType.None, control, device); | |
| } | |
| } | |
| } | |
| if (pointerType == UIPointerType.Touch) | |
| GetPointerStateForIndex(index).screenPosition = touchPosition; | |
| m_CurrentPointerId = pointerId; | |
| m_CurrentPointerIndex = index; | |
| m_CurrentPointerType = pointerType; | |
| return index; | |
| } | |
| private int AllocatePointer(int pointerId, int displayIndex, int touchId, UIPointerType pointerType, InputControl control, InputDevice device, InputControl touchControl = null) | |
| { | |
| // Recover event instance from previous record. | |
| var eventData = default(ExtendedPointerEventData); | |
| if (m_PointerStates.Capacity > m_PointerStates.length) | |
| { | |
| if (m_PointerStates.length == 0) | |
| eventData = m_PointerStates.firstValue.eventData; | |
| else | |
| eventData = m_PointerStates.additionalValues[m_PointerStates.length - 1].eventData; | |
| } | |
| // Or allocate event. | |
| if (eventData == null) | |
| eventData = new ExtendedPointerEventData(eventSystem); | |
| eventData.pointerId = pointerId; | |
| eventData.displayIndex = displayIndex; | |
| eventData.touchId = touchId; | |
| eventData.pointerType = pointerType; | |
| eventData.control = control; | |
| eventData.device = device; | |
| // Allocate state. | |
| m_PointerIds.AppendWithCapacity(pointerId); | |
| m_PointerTouchControls.AppendWithCapacity(touchControl); | |
| return m_PointerStates.AppendWithCapacity(new PointerModel(eventData)); | |
| } | |
| private void SendPointerExitEventsAndRemovePointer(int index) | |
| { | |
| var eventData = m_PointerStates[index].eventData; | |
| if (eventData.pointerEnter != null) | |
| ProcessPointerMovement(eventData, null); | |
| RemovePointerAtIndex(index); | |
| } | |
| private void RemovePointerAtIndex(int index) | |
| { | |
| Debug.Assert(m_PointerStates[index].eventData.pointerEnter == null, "Pointer should have exited all objects before being removed"); | |
| // Retain event data so that we can reuse the event the next time we allocate a PointerModel record. | |
| var eventData = m_PointerStates[index].eventData; | |
| Debug.Assert(eventData != null, "Pointer state should have an event instance!"); | |
| // Update current pointer, if necessary. | |
| if (index == m_CurrentPointerIndex) | |
| { | |
| m_CurrentPointerId = -1; | |
| m_CurrentPointerIndex = -1; | |
| m_CurrentPointerType = default; | |
| } | |
| else if (m_CurrentPointerIndex == m_PointerIds.length - 1) | |
| { | |
| // We're about to move the last entry so update the index it will | |
| // be at. | |
| m_CurrentPointerIndex = index; | |
| } | |
| // Remove. Note that we may change the order of pointers here. This can save us needless copying | |
| // and m_CurrentPointerIndex should be the only index we get around for longer. | |
| m_PointerIds.RemoveAtByMovingTailWithCapacity(index); | |
| m_PointerTouchControls.RemoveAtByMovingTailWithCapacity(index); | |
| m_PointerStates.RemoveAtByMovingTailWithCapacity(index); | |
| Debug.Assert(m_PointerIds.length == m_PointerStates.length, "Pointer ID array should match state array in length"); | |
| // Put event instance back in place at one past last entry of array (which we know we have | |
| // as we just erased one entry). This entry will be the next one that will be used when we | |
| // allocate a new entry. | |
| // Wipe the event. | |
| // NOTE: We only wipe properties here that contain reference data. The rest we rely on | |
| // the event handling code to initialize when using the event. | |
| eventData.hovered.Clear(); | |
| eventData.device = null; | |
| eventData.pointerCurrentRaycast = default; | |
| eventData.pointerPressRaycast = default; | |
| eventData.pointerPress = default; // Twice to wipe lastPress, too. | |
| eventData.pointerPress = default; | |
| eventData.pointerDrag = default; | |
| eventData.pointerEnter = default; | |
| eventData.rawPointerPress = default; | |
| if (m_PointerStates.length == 0) | |
| m_PointerStates.firstValue.eventData = eventData; | |
| else | |
| m_PointerStates.additionalValues[m_PointerStates.length - 1].eventData = eventData; | |
| } | |
| // Remove any pointer that no longer has the ability to point. | |
| private void PurgeStalePointers() | |
| { | |
| for (var i = 0; i < m_PointerStates.length; ++i) | |
| { | |
| ref var state = ref GetPointerStateForIndex(i); | |
| var device = state.eventData.device; | |
| if (!device.added || // Check if device was removed altogether. | |
| (!HaveControlForDevice(device, point) && | |
| !HaveControlForDevice(device, trackedDevicePosition) && | |
| !HaveControlForDevice(device, trackedDeviceOrientation))) | |
| { | |
| SendPointerExitEventsAndRemovePointer(i); | |
| --i; | |
| } | |
| } | |
| m_NeedToPurgeStalePointers = false; | |
| } | |
| private static bool HaveControlForDevice(InputDevice device, InputActionReference actionReference) | |
| { | |
| var action = actionReference?.action; | |
| if (action == null) | |
| return false; | |
| var controls = action.controls; | |
| for (var i = 0; i < controls.Count; ++i) | |
| if (controls[i].device == device) | |
| return true; | |
| return false; | |
| } | |
| // The pointer actions we unfortunately cannot poll as we may be sourcing input from multiple pointers. | |
| private void OnPointCallback(InputAction.CallbackContext context) | |
| { | |
| // When a pointer is removed, there's like a non-zero coordinate on the position control and thus | |
| // we will see cancellations on the "Point" action. Ignore these as they provide no useful values | |
| // and we want to avoid doing a read of touch IDs in GetPointerStateFor() on an already removed | |
| // touchscreen. | |
| if (CheckForRemovedDevice(ref context) || context.canceled) | |
| return; | |
| var index = GetPointerStateIndexFor(context.control); | |
| if (index == -1) | |
| return; | |
| ref var state = ref GetPointerStateForIndex(index); | |
| state.screenPosition = context.ReadValue<Vector2>(); | |
| state.eventData.displayIndex = GetDisplayIndexFor(context.control); | |
| } | |
| // NOTE: In the click events, we specifically react to the Canceled phase to make sure we do NOT perform | |
| // button *clicks* when an action resets. However, we still need to send pointer ups. | |
| private bool IgnoreNextClick(ref InputAction.CallbackContext context, bool wasPressed) | |
| { | |
| // If explicitly ignoring focus due to setting, never ignore clicks | |
| if (explictlyIgnoreFocus) | |
| return false; | |
| // If a currently active click is cancelled (by focus change), ignore next click if device cannot run in background. | |
| // This prevents the cancelled click event being registered when focus is returned i.e. if | |
| // the button was released while another window was focused. | |
| return context.canceled && !InputRuntime.s_Instance.isPlayerFocused && !context.control.device.canRunInBackground && wasPressed; | |
| } | |
| private void OnLeftClickCallback(InputAction.CallbackContext context) | |
| { | |
| var index = GetPointerStateIndexFor(ref context); | |
| if (index == -1) | |
| return; | |
| ref var state = ref GetPointerStateForIndex(index); | |
| bool wasPressed = state.leftButton.isPressed; | |
| state.leftButton.isPressed = context.ReadValueAsButton(); | |
| state.changedThisFrame = true; | |
| if (IgnoreNextClick(ref context, wasPressed)) | |
| state.leftButton.ignoreNextClick = true; | |
| state.eventData.displayIndex = GetDisplayIndexFor(context.control); | |
| } | |
| private void OnRightClickCallback(InputAction.CallbackContext context) | |
| { | |
| var index = GetPointerStateIndexFor(ref context); | |
| if (index == -1) | |
| return; | |
| ref var state = ref GetPointerStateForIndex(index); | |
| bool wasPressed = state.rightButton.isPressed; | |
| state.rightButton.isPressed = context.ReadValueAsButton(); | |
| state.changedThisFrame = true; | |
| if (IgnoreNextClick(ref context, wasPressed)) | |
| state.rightButton.ignoreNextClick = true; | |
| state.eventData.displayIndex = GetDisplayIndexFor(context.control); | |
| } | |
| private void OnMiddleClickCallback(InputAction.CallbackContext context) | |
| { | |
| var index = GetPointerStateIndexFor(ref context); | |
| if (index == -1) | |
| return; | |
| ref var state = ref GetPointerStateForIndex(index); | |
| bool wasPressed = state.middleButton.isPressed; | |
| state.middleButton.isPressed = context.ReadValueAsButton(); | |
| state.changedThisFrame = true; | |
| if (IgnoreNextClick(ref context, wasPressed)) | |
| state.middleButton.ignoreNextClick = true; | |
| state.eventData.displayIndex = GetDisplayIndexFor(context.control); | |
| } | |
| private bool CheckForRemovedDevice(ref InputAction.CallbackContext context) | |
| { | |
| // When a device is removed, we want to simply cancel ongoing pointer | |
| // operations. Most importantly, we want to prevent GetPointerStateFor() | |
| // doing ReadValue() on touch ID controls when a touchscreen has already | |
| // been removed. | |
| if (context.canceled && !context.control.device.added) | |
| { | |
| m_NeedToPurgeStalePointers = true; | |
| return true; | |
| } | |
| return false; | |
| } | |
| internal const float kPixelPerLine = 20; | |
| private void OnScrollCallback(InputAction.CallbackContext context) | |
| { | |
| var index = GetPointerStateIndexFor(ref context); | |
| if (index == -1) | |
| return; | |
| ref var state = ref GetPointerStateForIndex(index); | |
| // The old input system reported scroll deltas in lines, we report pixels. | |
| // Need to scale as the UI system expects lines. | |
| state.scrollDelta = context.ReadValue<Vector2>() * (1 / kPixelPerLine); | |
| state.eventData.displayIndex = GetDisplayIndexFor(context.control); | |
| } | |
| private void OnMoveCallback(InputAction.CallbackContext context) | |
| { | |
| ////REVIEW: should we poll this? or set the action to not be pass-through? (ps4 controller is spamming this action) | |
| m_NavigationState.move = context.ReadValue<Vector2>(); | |
| } | |
| private void OnTrackedDeviceOrientationCallback(InputAction.CallbackContext context) | |
| { | |
| var index = GetPointerStateIndexFor(ref context); | |
| if (index == -1) | |
| return; | |
| ref var state = ref GetPointerStateForIndex(index); | |
| state.worldOrientation = context.ReadValue<Quaternion>(); | |
| state.eventData.displayIndex = GetDisplayIndexFor(context.control); | |
| } | |
| private void OnTrackedDevicePositionCallback(InputAction.CallbackContext context) | |
| { | |
| var index = GetPointerStateIndexFor(ref context); | |
| if (index == -1) | |
| return; | |
| ref var state = ref GetPointerStateForIndex(index); | |
| state.worldPosition = context.ReadValue<Vector3>(); | |
| state.eventData.displayIndex = GetDisplayIndexFor(context.control); | |
| } | |
| private void OnControlsChanged(object obj) | |
| { | |
| m_NeedToPurgeStalePointers = true; | |
| } | |
| public override void Process() | |
| { | |
| if (m_NeedToPurgeStalePointers) | |
| PurgeStalePointers(); | |
| // Reset devices of changes since we don't want to spool up changes once we gain focus. | |
| if (!eventSystem.isFocused && !shouldIgnoreFocus) | |
| { | |
| for (var i = 0; i < m_PointerStates.length; ++i) | |
| m_PointerStates[i].OnFrameFinished(); | |
| } | |
| else | |
| { | |
| // Navigation input. | |
| ProcessNavigation(ref m_NavigationState); | |
| // Pointer input. | |
| for (var i = 0; i < m_PointerStates.length; i++) | |
| { | |
| ref var state = ref GetPointerStateForIndex(i); | |
| state.eventData.ReadDeviceState(); | |
| state.CopyTouchOrPenStateFrom(state.eventData); | |
| ProcessPointer(ref state); | |
| // If it's a touch and the touch has ended, release the pointer state. | |
| // NOTE: We defer this by one frame such that OnPointerUp happens in the frame of release | |
| // and OnPointerExit happens one frame later. This is so that IsPointerOverGameObject() | |
| // stays true for the touch in the frame of release (see UI_TouchPointersAreKeptForOneFrameAfterRelease). | |
| if (state.pointerType == UIPointerType.Touch && !state.leftButton.isPressed && !state.leftButton.wasReleasedThisFrame) | |
| { | |
| RemovePointerAtIndex(i); | |
| --i; | |
| continue; | |
| } | |
| state.OnFrameFinished(); | |
| } | |
| } | |
| } | |
| public override int ConvertUIToolkitPointerId(PointerEventData sourcePointerData) | |
| { | |
| // Case 1369081: when using SingleUnifiedPointer, the same (default) pointerId should be sent to UIToolkit | |
| // regardless of pointer type or finger id. | |
| if (m_PointerBehavior == UIPointerBehavior.SingleUnifiedPointer) | |
| return UIElements.PointerId.mousePointerId; | |
| return sourcePointerData is ExtendedPointerEventData ep | |
| ? ep.uiToolkitPointerId | |
| : base.ConvertUIToolkitPointerId(sourcePointerData); | |
| } | |
| private void HookActions() | |
| { | |
| if (m_ActionsHooked) | |
| return; | |
| if (m_OnPointDelegate == null) | |
| m_OnPointDelegate = OnPointCallback; | |
| if (m_OnLeftClickDelegate == null) | |
| m_OnLeftClickDelegate = OnLeftClickCallback; | |
| if (m_OnRightClickDelegate == null) | |
| m_OnRightClickDelegate = OnRightClickCallback; | |
| if (m_OnMiddleClickDelegate == null) | |
| m_OnMiddleClickDelegate = OnMiddleClickCallback; | |
| if (m_OnScrollWheelDelegate == null) | |
| m_OnScrollWheelDelegate = OnScrollCallback; | |
| if (m_OnMoveDelegate == null) | |
| m_OnMoveDelegate = OnMoveCallback; | |
| if (m_OnTrackedDeviceOrientationDelegate == null) | |
| m_OnTrackedDeviceOrientationDelegate = OnTrackedDeviceOrientationCallback; | |
| if (m_OnTrackedDevicePositionDelegate == null) | |
| m_OnTrackedDevicePositionDelegate = OnTrackedDevicePositionCallback; | |
| SetActionCallbacks(true); | |
| } | |
| private void UnhookActions() | |
| { | |
| if (!m_ActionsHooked) | |
| return; | |
| SetActionCallbacks(false); | |
| } | |
| private void SetActionCallbacks(bool install) | |
| { | |
| m_ActionsHooked = install; | |
| SetActionCallback(m_PointAction, m_OnPointDelegate, install); | |
| SetActionCallback(m_MoveAction, m_OnMoveDelegate, 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); | |
| } | |
| private static void SetActionCallback(InputActionReference actionReference, Action<InputAction.CallbackContext> callback, bool install) | |
| { | |
| if (!install && callback == null) | |
| return; | |
| if (actionReference == null) | |
| return; | |
| var action = actionReference.action; | |
| if (action == null) | |
| return; | |
| if (install) | |
| { | |
| action.performed += callback; | |
| action.canceled += callback; | |
| } | |
| else | |
| { | |
| action.performed -= callback; | |
| action.canceled -= callback; | |
| } | |
| } | |
| private InputActionReference UpdateReferenceForNewAsset(InputActionReference actionReference) | |
| { | |
| var oldAction = actionReference?.action; | |
| if (oldAction == null) | |
| return null; | |
| var oldActionMap = oldAction.actionMap; | |
| Debug.Assert(oldActionMap != null, "Not expected to end up with a singleton action here"); | |
| var newActionMap = m_ActionsAsset?.FindActionMap(oldActionMap.name); | |
| if (newActionMap == null) | |
| return null; | |
| var newAction = newActionMap.FindAction(oldAction.name); | |
| if (newAction == null) | |
| return null; | |
| return InputActionReference.Create(newAction); | |
| } | |
| public InputActionAsset actionsAsset | |
| { | |
| get => m_ActionsAsset; | |
| set | |
| { | |
| if (value != m_ActionsAsset) | |
| { | |
| UnhookActions(); | |
| m_ActionsAsset = value; | |
| point = UpdateReferenceForNewAsset(point); | |
| move = UpdateReferenceForNewAsset(move); | |
| leftClick = UpdateReferenceForNewAsset(leftClick); | |
| rightClick = UpdateReferenceForNewAsset(rightClick); | |
| middleClick = UpdateReferenceForNewAsset(middleClick); | |
| scrollWheel = UpdateReferenceForNewAsset(scrollWheel); | |
| submit = UpdateReferenceForNewAsset(submit); | |
| cancel = UpdateReferenceForNewAsset(cancel); | |
| trackedDeviceOrientation = UpdateReferenceForNewAsset(trackedDeviceOrientation); | |
| trackedDevicePosition = UpdateReferenceForNewAsset(trackedDevicePosition); | |
| HookActions(); | |
| } | |
| } | |
| } | |
| [] private InputActionAsset m_ActionsAsset; | |
| [] private InputActionReference m_PointAction; | |
| [] private InputActionReference m_MoveAction; | |
| [] private InputActionReference m_SubmitAction; | |
| [] private InputActionReference m_CancelAction; | |
| [] private InputActionReference m_LeftClickAction; | |
| [] private InputActionReference m_MiddleClickAction; | |
| [] private InputActionReference m_RightClickAction; | |
| [] private InputActionReference m_ScrollWheelAction; | |
| [] private InputActionReference m_TrackedDevicePositionAction; | |
| [] private InputActionReference m_TrackedDeviceOrientationAction; | |
| [] private bool m_DeselectOnBackgroundClick = true; | |
| [] private UIPointerBehavior m_PointerBehavior = UIPointerBehavior.SingleMouseOrPenButMultiTouchAndTrack; | |
| [] internal CursorLockBehavior m_CursorLockBehavior = CursorLockBehavior.OutsideScreen; | |
| private static Dictionary<InputAction, InputActionReferenceState> s_InputActionReferenceCounts = new Dictionary<InputAction, InputActionReferenceState>(); | |
| private struct InputActionReferenceState | |
| { | |
| public int refCount; | |
| public bool enabledByInputModule; | |
| } | |
| [] private bool m_ActionsHooked; | |
| [] private bool m_NeedToPurgeStalePointers; | |
| private Action<InputAction.CallbackContext> m_OnPointDelegate; | |
| private Action<InputAction.CallbackContext> m_OnMoveDelegate; | |
| private Action<InputAction.CallbackContext> m_OnLeftClickDelegate; | |
| private Action<InputAction.CallbackContext> m_OnRightClickDelegate; | |
| private Action<InputAction.CallbackContext> m_OnMiddleClickDelegate; | |
| private Action<InputAction.CallbackContext> m_OnScrollWheelDelegate; | |
| private Action<InputAction.CallbackContext> m_OnTrackedDevicePositionDelegate; | |
| private Action<InputAction.CallbackContext> m_OnTrackedDeviceOrientationDelegate; | |
| private Action<object> m_OnControlsChangedDelegate; | |
| // Pointer-type input (also tracking-type). | |
| [] private int m_CurrentPointerId = -1; // Keeping track of the current pointer avoids searches in most cases. | |
| [] private int m_CurrentPointerIndex = -1; | |
| [] internal UIPointerType m_CurrentPointerType = UIPointerType.None; | |
| internal InlinedArray<int> m_PointerIds; // Index in this array maps to index in m_PointerStates. Separated out to make searching more efficient (we do a linear search). | |
| internal InlinedArray<InputControl> m_PointerTouchControls; | |
| internal InlinedArray<PointerModel> m_PointerStates; | |
| // Navigation-type input. | |
| private NavigationModel m_NavigationState; | |
| [] private GameObject m_LocalMultiPlayerRoot; | |
| /// <summary> | |
| /// Controls the origin point of raycasts when the cursor is locked. | |
| /// </summary> | |
| public enum CursorLockBehavior | |
| { | |
| /// <summary> | |
| /// The internal pointer position will be set to -1, -1. This short-circuits the raycasting | |
| /// logic so no objects will be intersected. This is the default setting. | |
| /// </summary> | |
| OutsideScreen, | |
| /// <summary> | |
| /// Raycasts will originate from the center of the screen. This mode can be useful for | |
| /// example to check in pointer-driven FPS games if the player is looking at some world-space | |
| /// object that implements the <see cref="IPointerEnterHandler"/> and <see cref="IPointerExitHandler"/> | |
| /// interfaces. | |
| /// </summary> | |
| ScreenCenter | |
| } | |
| } | |
| } | |