VR4Medical/ICI/Library/PackageCache/com.unity.xr.interaction.toolkit@42ef3600567b/Runtime/UI/UIInputModule.cs
2025-07-29 13:45:50 +03:00

881 lines
38 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem.UI;
using UnityEngine.Serialization;
using UnityEngine.XR.Interaction.Toolkit.Interactors;
using UnityEngine.UI;
namespace UnityEngine.XR.Interaction.Toolkit.UI
{
/// <summary>
/// Base class for input modules that send UI input.
/// </summary>
/// <remarks>
/// Multiple input modules may be placed on the same event system. In such a setup,
/// the modules will synchronize with each other.
/// </remarks>
[DefaultExecutionOrder(XRInteractionUpdateOrder.k_UIInputModule)]
public abstract partial class UIInputModule : BaseInputModule
{
[Header("Configuration")]
[SerializeField, FormerlySerializedAs("clickSpeed")]
[Tooltip("The maximum time (in seconds) between two mouse presses for it to be consecutive click.")]
float m_ClickSpeed = 0.3f;
/// <summary>
/// The maximum time (in seconds) between two mouse presses for it to be consecutive click.
/// </summary>
public float clickSpeed
{
get => m_ClickSpeed;
set => m_ClickSpeed = value;
}
[SerializeField, FormerlySerializedAs("moveDeadzone")]
[Tooltip("The absolute value required by a move action on either axis required to trigger a move event.")]
float m_MoveDeadzone = 0.6f;
/// <summary>
/// The absolute value required by a move action on either axis required to trigger a move event.
/// </summary>
public float moveDeadzone
{
get => m_MoveDeadzone;
set => m_MoveDeadzone = value;
}
[SerializeField, FormerlySerializedAs("repeatDelay")]
[Tooltip("The Initial delay (in seconds) between an initial move action and a repeated move action.")]
float m_RepeatDelay = 0.5f;
/// <summary>
/// The Initial delay (in seconds) between an initial move action and a repeated move action.
/// </summary>
public float repeatDelay
{
get => m_RepeatDelay;
set => m_RepeatDelay = value;
}
[FormerlySerializedAs("repeatRate")]
[SerializeField, Tooltip("The speed (in seconds) that the move action repeats itself once repeating.")]
float m_RepeatRate = 0.1f;
/// <summary>
/// The speed (in seconds) that the move action repeats itself once repeating.
/// </summary>
public float repeatRate
{
get => m_RepeatRate;
set => m_RepeatRate = value;
}
[FormerlySerializedAs("trackedDeviceDragThresholdMultiplier")]
[SerializeField, Tooltip("Scales the EventSystem.pixelDragThreshold, for tracked devices, to make selection easier.")]
float m_TrackedDeviceDragThresholdMultiplier = 2f;
/// <summary>
/// Scales the <see cref="EventSystem.pixelDragThreshold"/>, for tracked devices, to make selection easier.
/// </summary>
public float trackedDeviceDragThresholdMultiplier
{
get => m_TrackedDeviceDragThresholdMultiplier;
set => m_TrackedDeviceDragThresholdMultiplier = value;
}
[SerializeField, Tooltip("Scales the scrollDelta in event data, for tracked devices, to scroll at an expected speed.")]
float m_TrackedScrollDeltaMultiplier = 5f;
/// <summary>
/// Scales the scrollDelta in event data, for tracked devices, to scroll at an expected speed.
/// </summary>
public float trackedScrollDeltaMultiplier
{
get => m_TrackedScrollDeltaMultiplier;
set => m_TrackedScrollDeltaMultiplier = value;
}
[SerializeField, Tooltip("Disables sending events from Event System to UI Toolkit on behalf of this Input Module.")]
bool m_BypassUIToolkitEvents;
/// <summary>
/// Disables sending events from Event System to UI Toolkit on behalf of this Input Module.
/// </summary>
public bool bypassUIToolkitEvents
{
get => m_BypassUIToolkitEvents;
set => m_BypassUIToolkitEvents = value;
}
Camera m_UICamera;
/// <summary>
/// The <see cref="Camera"/> that Unity uses to perform ray casts when determining the screen space location of a tracked device cursor.
/// </summary>
public Camera uiCamera
{
get
{
// If set by the user, always use this Camera.
if (m_UICamera != null)
return m_UICamera;
// Camera.main returns the first active and enabled Main Camera, so if the cached one
// is no longer enabled, find the new Main Camera. This is to support, for example,
// toggling between different XROrigin rigs each with their own Main Camera.
if (m_MainCameraCache == null || !m_MainCameraCache.isActiveAndEnabled)
m_MainCameraCache = Camera.main;
return m_MainCameraCache;
}
set => m_UICamera = value;
}
Camera m_MainCameraCache;
AxisEventData m_CachedAxisEvent;
readonly Dictionary<int, PointerEventData> m_PointerEventByPointerId = new Dictionary<int, PointerEventData>();
readonly Dictionary<int, TrackedDeviceEventData> m_TrackedDeviceEventByPointerId = new Dictionary<int, TrackedDeviceEventData>();
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
/// <remarks>
/// Processing is postponed from earlier in the frame (<see cref="EventSystem"/> has a
/// script execution order of <c>-1000</c>) until this Update to allow other systems to
/// update the poses that will be used to generate the ray casts used by this input module.
/// <br />
/// For Ray Interactor, it must wait until after the Controller pose updates and Locomotion
/// moves the Rig in order to generate the current sample points used to create the rays used
/// for this frame. Those positions will be determined during <see cref="DoProcess"/>.
/// Ray Interactor needs the UI ray casts to be completed by the time <see cref="XRInteractionManager"/>
/// calls into <see cref="IXRInteractor.GetValidTargets"/> since that is dependent on
/// whether a UI hit was closer than a 3D hit. This processing must therefore be done
/// between Locomotion and <see cref="IXRInteractor.PreprocessInteractor"/> to minimize latency.
/// </remarks>
protected virtual void Update()
{
// Check to make sure that Process should still be called.
// It would likely cause unexpected results if processing was done
// when this module is no longer the current one.
if (eventSystem.IsActive() && eventSystem.currentInputModule == this && eventSystem == EventSystem.current)
{
DoProcess();
}
}
/// <summary>
/// Process the current tick for the module.
/// </summary>
/// <remarks>
/// Executed once per Update call. Override for custom processing.
/// </remarks>
/// <seealso cref="Process"/>
protected virtual void DoProcess()
{
SendUpdateEventToSelectedObject();
}
/// <summary>
/// See <a href="https://docs.unity3d.com/Packages/com.unity.ugui@1.0/api/UnityEngine.EventSystems.BaseInputModule.html#UnityEngine_EventSystems_BaseInputModule_Process">BaseInputModule.Process()</a>.
/// </summary>
public override void Process()
{
// Postpone processing until later in the frame
}
/// <summary>
/// Sends an update event to the currently selected object.
/// </summary>
/// <returns>Returns whether the update event was used by the selected object.</returns>
protected bool SendUpdateEventToSelectedObject()
{
var selectedGameObject = eventSystem.currentSelectedGameObject;
if (selectedGameObject == null)
return false;
var data = GetBaseEventData();
updateSelected?.Invoke(selectedGameObject, data);
ExecuteEvents.Execute(selectedGameObject, data, ExecuteEvents.updateSelectedHandler);
return data.used;
}
/// <summary>
/// Called by <c>EventSystem</c> when the input module is made current.
/// </summary>
public override void ActivateModule()
{
base.ActivateModule();
// This is required for mouse/pointer events to work with UI Toolkit
if (bypassUIToolkitEvents)
EventSystem.SetUITookitEventSystemOverride(eventSystem, false, false);
// Select firstSelectedGameObject if nothing is selected ATM.
var toSelect = eventSystem.currentSelectedGameObject;
if (toSelect == null)
toSelect = eventSystem.firstSelectedGameObject;
eventSystem.SetSelectedGameObject(toSelect, GetBaseEventData());
}
/// <summary>
/// This will check the existing lists of pointer events and hand back the most current
/// GameObject entered by the current pointer.
/// </summary>
/// <param name="pointerId">ID of the XR device pointer, mouse pointer or touch registered with the UIInputModule.
/// Meaning this should correspond to either <see cref="PointerEventData"/>.<c>pointerId</c> or <see cref="TrackedDeviceEventData"/>.<c>pointerId</c>.
/// </param>
/// <returns>The GameObject that triggered the <see cref="PointerEventData.pointerEnter"/> event.</returns>
/// <remarks>
/// Any negative value used for <paramref name="pointerId"/> will be treated as <c>any</c>. The first event
/// from a tracked device will be used first, then to standard pointer devices such as mice and touchscreens.
/// </remarks>
/// <seealso cref="IsPointerOverGameObject" />
public GameObject GetCurrentGameObject(int pointerId)
{
// For negative pointer IDs, find any cached pointer events that have a registered pointerEnter object
if (pointerId < 0)
{
foreach (var trackedEvent in m_TrackedDeviceEventByPointerId.Values)
{
if (trackedEvent != null && trackedEvent.pointerEnter != null)
return trackedEvent.pointerEnter;
}
foreach (var trackedEvent in m_PointerEventByPointerId.Values)
{
if (trackedEvent != null && trackedEvent.pointerEnter != null)
return trackedEvent.pointerEnter;
}
}
else
{
if (m_TrackedDeviceEventByPointerId.TryGetValue(pointerId, out var trackedDeviceEvent))
return trackedDeviceEvent?.pointerEnter;
if (m_PointerEventByPointerId.TryGetValue(pointerId, out var pointerEvent))
return pointerEvent?.pointerEnter;
}
return null;
}
/// <summary>
/// Is the pointer with the given ID over an EventSystem object?
/// </summary>
/// <param name="pointerId">ID of the XR device pointer, mouse pointer or touch registered with the UIInputModule.
/// Meaning this should correspond to either <see cref="PointerEventData"/>.<c>pointerId</c> or <see cref="TrackedDeviceEventData"/>.<c>pointerId</c>.
/// </param>
/// <returns>Returns <see langword="true"/> if the given pointer is currently hovering over a <c>GameObject</c>. Otherwise, returns <see langword="false"/>.</returns>
/// <remarks>
/// The pointer IDs are generated at runtime by the UIInputModule as devices are registered. Calling this method
/// without any parameters will attempt to use the Left Mouse Button and will likely result in unexpected behavior.
/// A negative pointerId value will be interpreted as "any pointer" and will return true if any XR pointer is
/// currently over a GameObject.
/// Note: The IDs used to check for interaction are not the same as standard InputDevice device IDs.
/// </remarks>
/// <seealso cref="GetCurrentGameObject" />
/// <example>
/// <code>
/// using UnityEngine;
/// using UnityEngine.EventSystems;
/// using UnityEngine.XR.Interaction.Toolkit.UI;
///
/// public class ClickExample : MonoBehaviour
/// {
/// [SerializeField]
/// UIInputModule inputModule;
///
/// private void OnEnable()
/// {
/// if (inputModule != null)
/// {
/// inputModule.pointerClick += OnDeviceButtonClick;
/// }
/// }
///
/// private void OnDisable()
/// {
/// if (inputModule != null)
/// {
/// inputModule.pointerClick -= OnDeviceButtonClick;
/// }
/// }
///
/// // This method will fire after registering with the UIInputModule callbacks. The UIInputModule will
/// // pass the PointerEventData for the device responsible for triggering the callback and can be used to
/// // find the pointerId registered with the EventSystem for that device-specific event.
/// private void OnDeviceButtonClick(GameObject selected, PointerEventData pointerData)
/// {
/// if (EventSystem.current.IsPointerOverGameObject(pointerData.pointerId))
/// {
/// Debug.Log($"Clicked on {EventSystem.current.currentSelectedGameObject}", this);
/// }
/// }
/// }
/// </code>
/// </example>
public override bool IsPointerOverGameObject(int pointerId)
{
return GetCurrentGameObject(pointerId) != null;
}
RaycastResult PerformRaycast(PointerEventData eventData)
{
if (eventData == null)
throw new ArgumentNullException(nameof(eventData));
eventSystem.RaycastAll(eventData, m_RaycastResultCache);
finalizeRaycastResults?.Invoke(eventData, m_RaycastResultCache);
var result = FindFirstRaycast(m_RaycastResultCache);
m_RaycastResultCache.Clear();
return result;
}
/// <summary>
/// Takes an existing <see cref="PointerModel"/> and dispatches all relevant changes through the event system.
/// It also updates the internal data of the <see cref="PointerModel"/>.
/// </summary>
/// <param name="pointerState">The pointer state you want to forward into the UI Event System.</param>
private protected void ProcessPointerState(ref PointerModel pointerState)
{
if (!pointerState.changedThisFrame)
return;
var eventData = GetOrCreateCachedPointerEvent(pointerState.pointerId);
eventData.Reset();
pointerState.CopyTo(eventData);
eventData.pointerCurrentRaycast = PerformRaycast(eventData);
// Left Mouse Button
// The left mouse button is 'dominant' and we want to also process hover and scroll events as if the occurred during the left click.
var buttonState = pointerState.leftButton;
eventData.button = PointerEventData.InputButton.Left;
buttonState.CopyTo(eventData);
ProcessPointerButton(buttonState.lastFrameDelta, eventData);
ProcessPointerMovement(eventData);
ProcessScrollWheel(eventData);
pointerState.CopyFrom(eventData);
ProcessPointerButtonDrag(eventData, UIPointerType.MouseOrPen);
buttonState.CopyFrom(eventData);
pointerState.leftButton = buttonState;
// Right Mouse Button
buttonState = pointerState.rightButton;
eventData.button = PointerEventData.InputButton.Right;
buttonState.CopyTo(eventData);
ProcessPointerButton(buttonState.lastFrameDelta, eventData);
ProcessPointerButtonDrag(eventData, UIPointerType.MouseOrPen);
buttonState.CopyFrom(eventData);
pointerState.rightButton = buttonState;
// Middle Mouse Button
buttonState = pointerState.middleButton;
eventData.button = PointerEventData.InputButton.Middle;
buttonState.CopyTo(eventData);
ProcessPointerButton(buttonState.lastFrameDelta, eventData);
ProcessPointerButtonDrag(eventData, UIPointerType.MouseOrPen);
buttonState.CopyFrom(eventData);
pointerState.middleButton = buttonState;
pointerState.OnFrameFinished();
}
void ProcessPointerMovement(PointerEventData eventData)
{
var currentPointerTarget = eventData.pointerCurrentRaycast.gameObject;
#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)
{
pointerMove?.Invoke(eventData.hovered[i], eventData);
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)
{
foreach (var hovered in eventData.hovered)
{
pointerExit?.Invoke(hovered, eventData);
ExecuteEvents.Execute(hovered, eventData, ExecuteEvents.pointerExitHandler);
}
eventData.hovered.Clear();
if (currentPointerTarget == null)
{
eventData.pointerEnter = null;
return;
}
}
if (eventData.pointerEnter == currentPointerTarget)
return;
var commonRoot = FindCommonRoot(eventData.pointerEnter, currentPointerTarget);
// We walk up the tree until a common root and the last entered and current entered object is found.
// Then send exit and enter events up to, but not including, the common root.
if (eventData.pointerEnter != null)
{
var target = eventData.pointerEnter.transform;
while (target != null)
{
if (commonRoot != null && commonRoot.transform == target)
break;
var targetGameObject = target.gameObject;
pointerExit?.Invoke(targetGameObject, eventData);
ExecuteEvents.Execute(targetGameObject, eventData, ExecuteEvents.pointerExitHandler);
eventData.hovered.Remove(targetGameObject);
target = target.parent;
}
}
eventData.pointerEnter = currentPointerTarget;
// ReSharper disable once ConditionIsAlwaysTrueOrFalse -- Could be null if it was destroyed immediately after executing above
if (currentPointerTarget != null)
{
var target = currentPointerTarget.transform;
while (target != null && target.gameObject != commonRoot)
{
var targetGameObject = target.gameObject;
pointerEnter?.Invoke(targetGameObject, eventData);
ExecuteEvents.Execute(targetGameObject, eventData, ExecuteEvents.pointerEnterHandler);
#if UNITY_2021_1_OR_NEWER
if (wasMoved)
{
pointerMove?.Invoke(targetGameObject, eventData);
ExecuteEvents.Execute(targetGameObject, eventData, ExecuteEvents.pointerMoveHandler);
}
#endif
eventData.hovered.Add(targetGameObject);
target = target.parent;
}
}
}
void ProcessPointerButton(ButtonDeltaState mouseButtonChanges, PointerEventData eventData)
{
var hoverTarget = eventData.pointerCurrentRaycast.gameObject;
if ((mouseButtonChanges & ButtonDeltaState.Pressed) != 0)
{
eventData.eligibleForClick = true;
eventData.delta = Vector2.zero;
eventData.dragging = false;
eventData.pressPosition = eventData.position;
eventData.pointerPressRaycast = eventData.pointerCurrentRaycast;
eventData.useDragThreshold = true;
var selectHandler = ExecuteEvents.GetEventHandler<ISelectHandler>(hoverTarget);
// If we have clicked something new, deselect the old thing
// and leave 'selection handling' up to the press event.
if (eventSystem.currentSelectedGameObject != null && selectHandler != eventSystem.currentSelectedGameObject)
eventSystem.SetSelectedGameObject(null, eventData);
// search for the control that will receive the press.
// if we can't find a press handler set the press
// handler to be what would receive a click.
pointerDown?.Invoke(hoverTarget, eventData);
var newPressed = ExecuteEvents.ExecuteHierarchy(hoverTarget, eventData, ExecuteEvents.pointerDownHandler);
// We didn't find a press handler, so we search for a click handler.
if (newPressed == null)
newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(hoverTarget);
var time = Time.unscaledTime;
if (newPressed == eventData.lastPress && ((time - eventData.clickTime) < m_ClickSpeed))
++eventData.clickCount;
else
eventData.clickCount = 1;
eventData.clickTime = time;
eventData.pointerPress = newPressed;
eventData.rawPointerPress = hoverTarget;
// Save the drag handler for drag events during this mouse down.
var dragObject = ExecuteEvents.GetEventHandler<IDragHandler>(hoverTarget);
eventData.pointerDrag = dragObject;
if (dragObject != null)
{
initializePotentialDrag?.Invoke(dragObject, eventData);
ExecuteEvents.Execute(dragObject, eventData, ExecuteEvents.initializePotentialDrag);
}
}
if ((mouseButtonChanges & ButtonDeltaState.Released) != 0)
{
var target = eventData.pointerPress;
pointerUp?.Invoke(target, eventData);
ExecuteEvents.Execute(target, eventData, ExecuteEvents.pointerUpHandler);
var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(hoverTarget);
var pointerDrag = eventData.pointerDrag;
if (target == pointerUpHandler && eventData.eligibleForClick)
{
pointerClick?.Invoke(target, eventData);
ExecuteEvents.Execute(target, eventData, ExecuteEvents.pointerClickHandler);
}
else if (eventData.dragging && pointerDrag != null)
{
drop?.Invoke(hoverTarget, eventData);
ExecuteEvents.ExecuteHierarchy(hoverTarget, eventData, ExecuteEvents.dropHandler);
}
eventData.eligibleForClick = false;
eventData.pointerPress = null;
eventData.rawPointerPress = null;
if (eventData.dragging && pointerDrag != null)
{
endDrag?.Invoke(pointerDrag, eventData);
ExecuteEvents.Execute(pointerDrag, eventData, ExecuteEvents.endDragHandler);
}
eventData.dragging = false;
eventData.pointerDrag = null;
}
}
void ProcessPointerButtonDrag(PointerEventData eventData, UIPointerType pointerType, float pixelDragThresholdMultiplier = 1.0f)
{
if (!eventData.IsPointerMoving() ||
(pointerType == UIPointerType.MouseOrPen && Cursor.lockState == CursorLockMode.Locked) ||
eventData.pointerDrag == null)
{
return;
}
if (!eventData.dragging)
{
var threshold = eventSystem.pixelDragThreshold * pixelDragThresholdMultiplier;
if (!eventData.useDragThreshold || (eventData.pressPosition - eventData.position).sqrMagnitude >= (threshold * threshold))
{
var target = eventData.pointerDrag;
beginDrag?.Invoke(target, eventData);
ExecuteEvents.Execute(target, eventData, ExecuteEvents.beginDragHandler);
eventData.dragging = true;
}
}
if (eventData.dragging)
{
// If we moved from our initial press object, process an up for that object.
var target = eventData.pointerPress;
if (target != eventData.pointerDrag)
{
pointerUp?.Invoke(target, eventData);
ExecuteEvents.Execute(target, eventData, ExecuteEvents.pointerUpHandler);
eventData.eligibleForClick = false;
eventData.pointerPress = null;
eventData.rawPointerPress = null;
}
drag?.Invoke(eventData.pointerDrag, eventData);
ExecuteEvents.Execute(eventData.pointerDrag, eventData, ExecuteEvents.dragHandler);
}
}
void ProcessScrollWheel(PointerEventData eventData)
{
var scrollDelta = eventData.scrollDelta;
if (!Mathf.Approximately(scrollDelta.sqrMagnitude, 0f))
{
var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(eventData.pointerEnter);
scroll?.Invoke(scrollHandler, eventData);
ExecuteEvents.ExecuteHierarchy(scrollHandler, eventData, ExecuteEvents.scrollHandler);
}
}
#if ENABLE_LEGACY_INPUT_MANAGER && !ENABLE_INPUT_SYSTEM
private protected void ProcessTouch(ref TouchModel touchState)
{
if (!touchState.changedThisFrame)
return;
var eventData = GetOrCreateCachedPointerEvent(touchState.pointerId);
eventData.Reset();
touchState.CopyTo(eventData);
eventData.pointerCurrentRaycast = (touchState.selectPhase == TouchPhase.Canceled) ? new RaycastResult() : PerformRaycast(eventData);
eventData.button = PointerEventData.InputButton.Left;
ProcessPointerButton(touchState.selectDelta, eventData);
ProcessPointerMovement(eventData);
ProcessPointerButtonDrag(eventData, UIPointerType.Touch);
touchState.CopyFrom(eventData);
touchState.OnFrameFinished();
}
#endif
private protected void ProcessTrackedDevice(ref TrackedDeviceModel deviceState, bool force = false)
{
if (!deviceState.changedThisFrame && !force)
return;
var eventData = GetOrCreateCachedTrackedDeviceEvent(deviceState.pointerId);
eventData.Reset();
deviceState.CopyTo(eventData);
eventData.scrollDelta *= m_TrackedScrollDeltaMultiplier;
eventData.button = PointerEventData.InputButton.Left;
// Demolish the screen position so we don't trigger any hits from a GraphicRaycaster component on a Canvas.
// The position value is not used by the TrackedDeviceGraphicRaycaster.
// Restore the original value after the Raycast is complete.
var savedPosition = eventData.position;
eventData.position = new Vector2(float.MinValue, float.MinValue);
eventData.pointerCurrentRaycast = PerformRaycast(eventData);
eventData.position = savedPosition;
if (TryGetCamera(eventData, out var screenPointCamera))
{
Vector2 screenPosition;
if (eventData.pointerCurrentRaycast.isValid)
{
screenPosition = screenPointCamera.WorldToScreenPoint(eventData.pointerCurrentRaycast.worldPosition);
if ((deviceState.selectDelta & ButtonDeltaState.Pressed) != 0)
{
eventData.pressWorldPosition = eventData.pointerCurrentRaycast.worldPosition;
}
}
else
{
var endPosition = eventData.rayPoints.Count > 0 ? eventData.rayPoints[eventData.rayPoints.Count - 1] : Vector3.zero;
screenPosition = screenPointCamera.WorldToScreenPoint(endPosition);
eventData.position = screenPosition;
}
var thisFrameDelta = screenPosition - eventData.position;
eventData.position = screenPosition;
eventData.delta = thisFrameDelta;
ProcessPointerButton(deviceState.selectDelta, eventData);
ProcessPointerMovement(eventData);
ProcessScrollWheel(eventData);
// In a VR headset context, the camera can move while the pointer/controller stays put, but this
// breaks the standard 2D screen space model. This will ensure that the initial press position used
// for drag detection is updated as head-movement updates each frame.
if (eventData.pressPosition != Vector2.zero)
{
eventData.pressPosition = screenPointCamera.WorldToScreenPoint(eventData.pressWorldPosition);
}
ProcessPointerButtonDrag(eventData, UIPointerType.Tracked, m_TrackedDeviceDragThresholdMultiplier);
var oldTarget = deviceState.implementationData.pointerTarget;
deviceState.CopyFrom(eventData);
var newTarget = deviceState.implementationData.pointerTarget;
if (oldTarget != newTarget)
{
if (newTarget != null)
{
var selectable = newTarget.GetComponentInParent<ISelectHandler>();
var scrollable = newTarget.GetComponentInParent<IScrollHandler>();
deviceState.selectableObject = (selectable as Component)?.gameObject;
deviceState.isScrollable = scrollable != null;
}
else
{
deviceState.selectableObject = null;
deviceState.isScrollable = false;
}
}
}
deviceState.OnFrameFinished();
}
bool TryGetCamera(PointerEventData eventData, out Camera screenPointCamera)
{
// Get associated Camera, or Main Camera, or Camera from ray cast, and if *nothing* exists, then abort processing this frame.
screenPointCamera = uiCamera;
if (screenPointCamera != null)
return true;
var module = eventData.pointerCurrentRaycast.module;
if (module != null)
{
screenPointCamera = module.eventCamera;
return screenPointCamera != null;
}
return false;
}
/// <summary>
/// Takes an existing NavigationModel and dispatches all relevant changes through the event system.
/// It also updates the internal data of the NavigationModel.
/// </summary>
/// <param name="navigationState">The navigation state you want to forward into the UI Event System</param>
private protected void ProcessNavigationState(ref NavigationModel navigationState)
{
var usedSelectionChange = SendUpdateEventToSelectedObject();
// Don't send move events if disabled in the EventSystem.
if (!eventSystem.sendNavigationEvents)
return;
var implementationData = navigationState.implementationData;
var selectedGameObject = eventSystem.currentSelectedGameObject;
var movement = navigationState.move;
if (!usedSelectionChange && (!Mathf.Approximately(movement.x, 0f) || !Mathf.Approximately(movement.y, 0f)))
{
var time = Time.unscaledTime;
var moveDirection = MoveDirection.None;
if (movement.sqrMagnitude > m_MoveDeadzone * m_MoveDeadzone)
{
if (Mathf.Abs(movement.x) > Mathf.Abs(movement.y))
moveDirection = (movement.x > 0f) ? MoveDirection.Right : MoveDirection.Left;
else
moveDirection = (movement.y > 0f) ? MoveDirection.Up : MoveDirection.Down;
}
if (moveDirection != implementationData.lastMoveDirection)
{
implementationData.consecutiveMoveCount = 0;
}
if (moveDirection != MoveDirection.None)
{
var allow = true;
if (implementationData.consecutiveMoveCount != 0)
{
if (implementationData.consecutiveMoveCount > 1)
allow = (time > (implementationData.lastMoveTime + m_RepeatRate));
else
allow = (time > (implementationData.lastMoveTime + m_RepeatDelay));
}
if (allow)
{
var eventData = GetOrCreateCachedAxisEvent();
eventData.Reset();
eventData.moveVector = movement;
eventData.moveDir = moveDirection;
move?.Invoke(selectedGameObject, eventData);
ExecuteEvents.Execute(selectedGameObject, eventData, ExecuteEvents.moveHandler);
usedSelectionChange = eventData.used;
implementationData.consecutiveMoveCount++;
implementationData.lastMoveTime = time;
implementationData.lastMoveDirection = moveDirection;
}
}
else
{
implementationData.consecutiveMoveCount = 0;
}
}
else
{
implementationData.consecutiveMoveCount = 0;
}
if (!usedSelectionChange)
{
if (selectedGameObject != null)
{
var data = GetBaseEventData();
if ((navigationState.submitButtonDelta & ButtonDeltaState.Pressed) != 0)
{
submit?.Invoke(selectedGameObject, data);
ExecuteEvents.Execute(selectedGameObject, data, ExecuteEvents.submitHandler);
}
if (!data.used && (navigationState.cancelButtonDelta & ButtonDeltaState.Pressed) != 0)
{
cancel?.Invoke(selectedGameObject, data);
ExecuteEvents.Execute(selectedGameObject, data, ExecuteEvents.cancelHandler);
}
}
}
navigationState.implementationData = implementationData;
navigationState.OnFrameFinished();
}
/// <summary>
/// Remove the <see cref="PointerEventData"/> associated with the given ID.
/// </summary>
/// <param name="pointerId">ID of the XR device pointer, mouse pointer or touch registered with the UIInputModule.
/// Meaning this should correspond to either <see cref="PointerEventData"/>.<c>pointerId</c> or <see cref="TrackedDeviceEventData"/>.<c>pointerId</c>.
/// </param>
private protected void RemovePointerEventData(int pointerId)
{
if (!m_TrackedDeviceEventByPointerId.Remove(pointerId))
m_PointerEventByPointerId.Remove(pointerId);
}
PointerEventData GetOrCreateCachedPointerEvent(int pointerId)
{
if (!m_PointerEventByPointerId.TryGetValue(pointerId, out var result))
{
result = new PointerEventData(eventSystem);
m_PointerEventByPointerId.Add(pointerId, result);
}
return result;
}
TrackedDeviceEventData GetOrCreateCachedTrackedDeviceEvent(int pointerId)
{
if (!m_TrackedDeviceEventByPointerId.TryGetValue(pointerId, out var result))
{
result = new TrackedDeviceEventData(eventSystem);
m_TrackedDeviceEventByPointerId.Add(pointerId, result);
}
return result;
}
AxisEventData GetOrCreateCachedAxisEvent()
{
var result = m_CachedAxisEvent;
if (result == null)
{
result = new AxisEventData(eventSystem);
m_CachedAxisEvent = result;
}
return result;
}
}
}