2506 lines
117 KiB
C#
2506 lines
117 KiB
C#
#if PACKAGE_DOCS_GENERATION || UNITY_INPUT_SYSTEM_ENABLE_UI
|
|
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;
|
|
#if UNITY_EDITOR
|
|
using UnityEditor;
|
|
#endif
|
|
|
|
////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>
|
|
[HelpURL(InputSystem.kDocUrl + "/manual/UISupport.html#setting-up-ui-input")]
|
|
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>
|
|
/// A multiplier value that allows you to adjust the scroll wheel speed sent to uGUI (Unity UI) components.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This value controls the magnitude of the PointerEventData.scrollDelta value, when the scroll wheel is rotated one tick. It acts as a multiplier, so a value of 1 passes through the original value and behaves the same as the legacy Standalone Input Module.
|
|
///
|
|
/// A value larger than one increases the scrolling speed per tick, and a value less than one decreases the speed.
|
|
///
|
|
/// You can set this to a negative value to invert the scroll direction. A value of zero prevents mousewheel scrolling from working at all.
|
|
///
|
|
/// Note: this has no effect on UI Toolkit content, only uGUI components.
|
|
/// </remarks>
|
|
public float scrollDeltaPerTick
|
|
{
|
|
get => m_ScrollDeltaPerTick;
|
|
set => m_ScrollDeltaPerTick = 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 UNITY_2021_1_OR_NEWER
|
|
// 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);
|
|
}
|
|
#endif
|
|
|
|
// 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;
|
|
|
|
Transform commonRoot = FindCommonRoot(eventData.pointerEnter, currentPointerTarget)?.transform;
|
|
Transform pointerParent = ((Component)currentPointerTarget.GetComponentInParent<IPointerExitHandler>())?.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.
|
|
// ** or when !m_SendPointerEnterToParent, stop when meeting a gameobject with an exit event handler
|
|
if (eventData.pointerEnter != null)
|
|
{
|
|
var current = eventData.pointerEnter.transform;
|
|
while (current != null)
|
|
{
|
|
// if we reach the common root break out!
|
|
if (sendPointerHoverToParent && current == commonRoot)
|
|
break;
|
|
|
|
// if we reach a PointerExitEvent break out!
|
|
if (!sendPointerHoverToParent && current == pointerParent)
|
|
break;
|
|
|
|
#if UNITY_2021_3_OR_NEWER
|
|
eventData.fullyExited = current != commonRoot && eventData.pointerEnter != currentPointerTarget;
|
|
#endif
|
|
ExecuteEvents.Execute(current.gameObject, eventData, ExecuteEvents.pointerExitHandler);
|
|
eventData.hovered.Remove(current.gameObject);
|
|
|
|
if (sendPointerHoverToParent)
|
|
current = current.parent;
|
|
|
|
// if we reach the common root break out!
|
|
if (current == commonRoot)
|
|
break;
|
|
|
|
if (!sendPointerHoverToParent)
|
|
current = current.parent;
|
|
}
|
|
}
|
|
|
|
// now issue the enter call up to but not including the common root
|
|
Transform oldPointerEnter = eventData.pointerEnter ? eventData.pointerEnter.transform : null;
|
|
eventData.pointerEnter = currentPointerTarget;
|
|
if (currentPointerTarget != null)
|
|
{
|
|
Transform current = currentPointerTarget.transform;
|
|
while (current != null && !PointerShouldIgnoreTransform(current))
|
|
{
|
|
#if UNITY_2021_3_OR_NEWER
|
|
eventData.reentered = current == commonRoot && current != oldPointerEnter;
|
|
// if we are sending the event to parent, they are already in hover mode at that point. No need to bubble up the event.
|
|
if (sendPointerHoverToParent && eventData.reentered)
|
|
break;
|
|
#endif
|
|
|
|
ExecuteEvents.Execute(current.gameObject, eventData, ExecuteEvents.pointerEnterHandler);
|
|
#if UNITY_2021_1_OR_NEWER
|
|
if (wasMoved)
|
|
ExecuteEvents.Execute(current.gameObject, eventData, ExecuteEvents.pointerMoveHandler);
|
|
#endif
|
|
eventData.hovered.Add(current.gameObject);
|
|
|
|
// stop when encountering an object with the pointerEnterHandler
|
|
if (!sendPointerHoverToParent && current.GetComponent<IPointerEnterHandler>() != null)
|
|
break;
|
|
|
|
if (sendPointerHoverToParent)
|
|
current = current.parent;
|
|
|
|
// if we reach the common root break out!
|
|
if (current == commonRoot)
|
|
break;
|
|
|
|
if (!sendPointerHoverToParent)
|
|
current = current.parent;
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
var pointerClickHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
|
|
|
|
// 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 = pointerClickHandler;
|
|
|
|
// 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;
|
|
#if UNITY_2020_1_OR_NEWER // pointerClick doesn't exist before this.
|
|
eventData.pointerClick = pointerClickHandler;
|
|
#endif
|
|
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);
|
|
#if UNITY_2020_1_OR_NEWER
|
|
var isClick = eventData.pointerClick != null && eventData.pointerClick == pointerClickHandler && eventData.eligibleForClick;
|
|
#else
|
|
var isClick = eventData.pointerPress != null && eventData.pointerPress == pointerClickHandler && eventData.eligibleForClick;
|
|
#endif
|
|
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)
|
|
{
|
|
#if UNITY_2020_1_OR_NEWER
|
|
ExecuteEvents.Execute(eventData.pointerClick, eventData, ExecuteEvents.pointerClickHandler);
|
|
#else
|
|
ExecuteEvents.Execute(eventData.pointerPress, eventData, ExecuteEvents.pointerClickHandler);
|
|
#endif
|
|
}
|
|
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.WasPerformedThisFrame())
|
|
ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.cancelHandler);
|
|
if (!data.used && submitAction != null && submitAction.WasPerformedThisFrame())
|
|
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);
|
|
}
|
|
|
|
[FormerlySerializedAs("m_RepeatDelay")]
|
|
[Tooltip("The Initial delay (in seconds) between an initial move action and a repeated move action.")]
|
|
[SerializeField]
|
|
private float m_MoveRepeatDelay = 0.5f;
|
|
|
|
[FormerlySerializedAs("m_RepeatRate")]
|
|
[Tooltip("The speed (in seconds) that the move action repeats itself once repeating (max 1 per frame).")]
|
|
[SerializeField]
|
|
private float m_MoveRepeatRate = 0.1f;
|
|
|
|
[Tooltip("Scales the Eventsystem.DragThreshold, for tracked devices, to make selection easier.")]
|
|
// Hide this while we still have to figure out what to do with this.
|
|
private float m_TrackedDeviceDragThresholdMultiplier = 2.0f;
|
|
|
|
[Tooltip("Transform representing the real world origin for tracking devices. When using the XR Interaction Toolkit, this should be pointing to the XR Rig's Transform.")]
|
|
[SerializeField]
|
|
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;
|
|
}
|
|
|
|
[Obsolete("'repeatRate' has been obsoleted; use 'moveRepeatRate' instead. (UnityUpgradable) -> moveRepeatRate", false)]
|
|
public float repeatRate
|
|
{
|
|
get => moveRepeatRate;
|
|
set => moveRepeatRate = value;
|
|
}
|
|
|
|
[Obsolete("'repeatDelay' has been obsoleted; use 'moveRepeatDelay' instead. (UnityUpgradable) -> moveRepeatDelay", false)]
|
|
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;
|
|
|
|
#if DEBUG
|
|
// 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);
|
|
}
|
|
#endif
|
|
|
|
if (newValue?.action != null && actionCallback != null && actionsHooked)
|
|
{
|
|
property.action.performed += actionCallback;
|
|
property.action.canceled += actionCallback;
|
|
}
|
|
|
|
if (isActiveAndEnabled && newValue?.action != null && (oldActionEnabled || oldActionNull))
|
|
EnableInputAction(property);
|
|
}
|
|
|
|
#if DEBUG
|
|
private bool IsNavigationAction(InputActionReference reference)
|
|
{
|
|
return reference == m_SubmitAction || reference == m_CancelAction || reference == m_MoveAction;
|
|
}
|
|
|
|
#endif
|
|
|
|
/// <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"/>
|
|
|
|
private static DefaultInputActions defaultActions;
|
|
|
|
public void AssignDefaultActions()
|
|
{
|
|
if (defaultActions == null)
|
|
{
|
|
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()
|
|
{
|
|
defaultActions?.Dispose();
|
|
defaultActions = default;
|
|
actionsAsset = default;
|
|
cancel = default;
|
|
submit = default;
|
|
move = default;
|
|
leftClick = default;
|
|
rightClick = default;
|
|
middleClick = default;
|
|
point = default;
|
|
scrollWheel = default;
|
|
trackedDeviceOrientation = default;
|
|
trackedDevicePosition = default;
|
|
}
|
|
|
|
[Obsolete("'trackedDeviceSelect' has been obsoleted; use 'leftClick' instead.", true)]
|
|
public InputActionReference trackedDeviceSelect
|
|
{
|
|
get => throw new InvalidOperationException();
|
|
set => throw new InvalidOperationException();
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
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);
|
|
}
|
|
|
|
#endif
|
|
|
|
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;
|
|
|
|
////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;
|
|
#if UNITY_2022_3_OR_NEWER
|
|
eventData.displayIndex = displayIndex;
|
|
#endif
|
|
|
|
// 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;
|
|
#if UNITY_2022_3_OR_NEWER
|
|
eventData.displayIndex = displayIndex;
|
|
#endif
|
|
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>();
|
|
#if UNITY_2022_3_OR_NEWER
|
|
state.eventData.displayIndex = GetDisplayIndexFor(context.control);
|
|
#endif
|
|
}
|
|
|
|
// 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;
|
|
#if UNITY_2022_3_OR_NEWER
|
|
state.eventData.displayIndex = GetDisplayIndexFor(context.control);
|
|
#endif
|
|
}
|
|
|
|
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;
|
|
#if UNITY_2022_3_OR_NEWER
|
|
state.eventData.displayIndex = GetDisplayIndexFor(context.control);
|
|
#endif
|
|
}
|
|
|
|
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;
|
|
#if UNITY_2022_3_OR_NEWER
|
|
state.eventData.displayIndex = GetDisplayIndexFor(context.control);
|
|
#endif
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
private void OnScrollCallback(InputAction.CallbackContext context)
|
|
{
|
|
var index = GetPointerStateIndexFor(ref context);
|
|
if (index == -1)
|
|
return;
|
|
|
|
ref var state = ref GetPointerStateForIndex(index);
|
|
|
|
var scrollDelta = context.ReadValue<Vector2>();
|
|
|
|
// ISXB-704: convert input value to BaseInputModule convention.
|
|
state.scrollDelta = (scrollDelta / InputSystem.scrollWheelDeltaPerTick) * scrollDeltaPerTick;
|
|
|
|
#if UNITY_2022_3_OR_NEWER
|
|
state.eventData.displayIndex = GetDisplayIndexFor(context.control);
|
|
#endif
|
|
}
|
|
|
|
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>();
|
|
#if UNITY_2022_3_OR_NEWER
|
|
state.eventData.displayIndex = GetDisplayIndexFor(context.control);
|
|
#endif
|
|
}
|
|
|
|
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>();
|
|
#if UNITY_2022_3_OR_NEWER
|
|
state.eventData.displayIndex = GetDisplayIndexFor(context.control);
|
|
#endif
|
|
}
|
|
|
|
private void OnControlsChanged(object obj)
|
|
{
|
|
m_NeedToPurgeStalePointers = true;
|
|
}
|
|
|
|
private void FilterPointerStatesByType()
|
|
{
|
|
var pointerTypeToProcess = UIPointerType.None;
|
|
// Read all pointers device states
|
|
// Find first pointer that has changed this frame to be processed later
|
|
for (var i = 0; i < m_PointerStates.length; ++i)
|
|
{
|
|
ref var state = ref GetPointerStateForIndex(i);
|
|
state.eventData.ReadDeviceState();
|
|
state.CopyTouchOrPenStateFrom(state.eventData);
|
|
if (state.changedThisFrame && pointerTypeToProcess == UIPointerType.None)
|
|
pointerTypeToProcess = state.pointerType;
|
|
}
|
|
|
|
// 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 && pointerTypeToProcess != UIPointerType.None)
|
|
{
|
|
// var pointerTypeToProcess = m_PointerStates.firstValue.pointerType;
|
|
if (pointerTypeToProcess == 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
FilterPointerStatesByType();
|
|
|
|
// Pointer input.
|
|
for (var i = 0; i < m_PointerStates.length; i++)
|
|
{
|
|
ref var state = ref GetPointerStateForIndex(i);
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
#if UNITY_2021_1_OR_NEWER
|
|
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);
|
|
}
|
|
|
|
#endif
|
|
|
|
#if UNITY_INPUT_SYSTEM_INPUT_MODULE_SCROLL_DELTA
|
|
const float kSmallestScrollDeltaPerTick = 0.00001f;
|
|
public override Vector2 ConvertPointerEventScrollDeltaToTicks(Vector2 scrollDelta)
|
|
{
|
|
if (Mathf.Abs(scrollDeltaPerTick) < kSmallestScrollDeltaPerTick)
|
|
return Vector2.zero;
|
|
|
|
return scrollDelta / scrollDeltaPerTick;
|
|
}
|
|
|
|
#endif
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
[SerializeField, HideInInspector] private InputActionAsset m_ActionsAsset;
|
|
[SerializeField, HideInInspector] private InputActionReference m_PointAction;
|
|
[SerializeField, HideInInspector] private InputActionReference m_MoveAction;
|
|
[SerializeField, HideInInspector] private InputActionReference m_SubmitAction;
|
|
[SerializeField, HideInInspector] private InputActionReference m_CancelAction;
|
|
[SerializeField, HideInInspector] private InputActionReference m_LeftClickAction;
|
|
[SerializeField, HideInInspector] private InputActionReference m_MiddleClickAction;
|
|
[SerializeField, HideInInspector] private InputActionReference m_RightClickAction;
|
|
[SerializeField, HideInInspector] private InputActionReference m_ScrollWheelAction;
|
|
[SerializeField, HideInInspector] private InputActionReference m_TrackedDevicePositionAction;
|
|
[SerializeField, HideInInspector] private InputActionReference m_TrackedDeviceOrientationAction;
|
|
|
|
[SerializeField] private bool m_DeselectOnBackgroundClick = true;
|
|
[SerializeField] private UIPointerBehavior m_PointerBehavior = UIPointerBehavior.SingleMouseOrPenButMultiTouchAndTrack;
|
|
[SerializeField, HideInInspector] internal CursorLockBehavior m_CursorLockBehavior = CursorLockBehavior.OutsideScreen;
|
|
|
|
// See ISXB-766 for a history of where the 6.0f value comes from
|
|
// (we used to have 120 per tick on Windows and divided it by 20.)
|
|
[SerializeField] private float m_ScrollDeltaPerTick = 6.0f;
|
|
|
|
private static Dictionary<InputAction, InputActionReferenceState> s_InputActionReferenceCounts = new Dictionary<InputAction, InputActionReferenceState>();
|
|
|
|
private struct InputActionReferenceState
|
|
{
|
|
public int refCount;
|
|
public bool enabledByInputModule;
|
|
}
|
|
|
|
[NonSerialized] private bool m_ActionsHooked;
|
|
[NonSerialized] 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).
|
|
[NonSerialized] private int m_CurrentPointerId = -1; // Keeping track of the current pointer avoids searches in most cases.
|
|
[NonSerialized] private int m_CurrentPointerIndex = -1;
|
|
[NonSerialized] 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;
|
|
|
|
[NonSerialized] private GameObject m_LocalMultiPlayerRoot;
|
|
|
|
#if UNITY_INPUT_SYSTEM_SENDPOINTERHOVERTOPARENT
|
|
// Needed for testing.
|
|
internal new bool sendPointerHoverToParent
|
|
{
|
|
get => base.sendPointerHoverToParent;
|
|
set => base.sendPointerHoverToParent = value;
|
|
}
|
|
#else
|
|
private bool sendPointerHoverToParent => true;
|
|
#endif
|
|
|
|
/// <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
|
|
}
|
|
}
|
|
}
|
|
#endif
|