Files
dungeons/Assets/UltimateXR/Runtime/Scripts/UI/UnityInputModule/Controls/UxrControlInput.cs
2024-08-06 21:58:35 +02:00

780 lines
27 KiB
C#

// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UxrControlInput.cs" company="VRMADA">
// Copyright (c) VRMADA, All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using UltimateXR.Extensions.System.Threading;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace UltimateXR.UI.UnityInputModule.Controls
{
public delegate void DragStartedEventHandler(UxrControlInput controlInput, PointerEventData eventData);
public delegate void DraggedEventHandler(UxrControlInput controlInput, PointerEventData eventData);
public delegate void DragEndedEventHandler(UxrControlInput controlInput, PointerEventData eventData);
public delegate void DroppedEventHandler(UxrControlInput controlInput, PointerEventData eventData);
public delegate void PressedEventHandler(UxrControlInput controlInput, PointerEventData eventData);
public delegate void ReleasedEventHandler(UxrControlInput controlInput, PointerEventData eventData);
public delegate void ClickedEventHandler(UxrControlInput controlInput, PointerEventData eventData);
public delegate void PressHeldEventHandler(UxrControlInput controlInput, PointerEventData eventData);
public delegate void CursorEnteredEventHandler(UxrControlInput controlInput, PointerEventData eventData);
public delegate void CursorExitedEventHandler(UxrControlInput controlInput, PointerEventData eventData);
public delegate void UpdateSelectedEventHandler(UxrControlInput controlInput, BaseEventData eventData);
public delegate void InputSubmittedEventHandler(UxrControlInput controlInput, BaseEventData eventData);
/// <summary>
/// A component derived from <see cref="EventTrigger" /> that simplifies the handling of events triggered by UI
/// controls. Among the key benefits are:
/// <list type="bullet">
/// <item>
/// Be able to write UI code by subscribing to events generated by UI controls. Global static events are
/// also provided to handle events coming from any control.
/// </item>
/// <item>
/// New controls with more complex behaviour can inherit from this class and add their own logic. Event
/// triggers are provided so that handling events can be done by overriding the appropriate methods, making
/// sure the base class is always called at the beginning. An example is <see cref="UxrToggleControlInput" />.
/// </item>
/// <item>
/// Each <see cref="UxrControlInput" /> can specify the audio/haptic feedback for the click/down/up events.
/// </item>
/// </list>
/// </summary>
public class UxrControlInput : EventTrigger
{
#region Inspector Properties/Serialized Fields
[SerializeField] private float _pressAndHoldDuration = 1.0f;
[SerializeField] private UxrControlFeedback _feedbackOnPress;
[SerializeField] private UxrControlFeedback _feedbackOnRelease;
[SerializeField] private UxrControlFeedback _feedbackOnClick;
#endregion
#region Public Types & Data
/// <summary>
/// Event called whenever any <see cref="UxrControlInput" /> is pressed.
/// </summary>
public static event PressedEventHandler GlobalPressed;
/// <summary>
/// Event called whenever any <see cref="UxrControlInput" /> press is released.
/// </summary>
public static event ReleasedEventHandler GlobalReleased;
/// <summary>
/// Event called whenever any <see cref="UxrControlInput" /> is clicked. A click depending on the operating mode can be
/// a press or a release after a press.
/// </summary>
public static event ClickedEventHandler GlobalClicked;
/// <summary>
/// Event called whenever any <see cref="UxrControlInput" /> started being dragged.
/// </summary>
public static event DragStartedEventHandler GlobalDragStarted;
/// <summary>
/// Event called during the frames any <see cref="UxrControlInput" /> is being dragged.
/// </summary>
public static event DraggedEventHandler GlobalDragged;
/// <summary>
/// Event called whenever any <see cref="UxrControlInput" /> stopped being dragged.
/// </summary>
public static event DragEndedEventHandler GlobalDragEnded;
/// <summary>
/// Event called whenever the control started being dragged.
/// </summary>
public event DragStartedEventHandler DragStarted;
/// <summary>
/// Event called each frame the control is being dragged.
/// </summary>
public event DraggedEventHandler Dragged;
/// <summary>
/// Event called whenever the control stopped being dragged.
/// </summary>
public event DragEndedEventHandler DragEnded;
/// <summary>
/// Event called whenever the control was dropped.
/// </summary>
public event DroppedEventHandler Dropped;
/// <summary>
/// Event called whenever the control was pressed.
/// </summary>
public event PressedEventHandler Pressed;
/// <summary>
/// Event called whenever the control was released after being pressed.
/// </summary>
public event ReleasedEventHandler Released;
/// <summary>
/// Event called whenever the control was clicked. A click depending on the operating mode can be a press or a release
/// after a press.
/// </summary>
public event ClickedEventHandler Clicked;
/// <summary>
/// Event called whenever the control was kept being pressed for <see cref="PressAndHoldDuration" /> seconds without
/// being dragged.
/// </summary>
public event PressHeldEventHandler PressHeld;
/// <summary>
/// Event called whenever the pointer entered the control.
/// </summary>
public event CursorEnteredEventHandler CursorEntered;
/// <summary>
/// Event called whenever the pointer exited the control.
/// </summary>
public event CursorExitedEventHandler CursorExited;
/// <summary>
/// Event called whenever the selected control's input field is updated/changed.
/// </summary>
public event UpdateSelectedEventHandler UpdateSelected;
/// <summary>
/// Event called whenever the control's input field was submitted (OK was pressed).
/// </summary>
public event InputSubmittedEventHandler InputSubmitted;
/// <summary>
/// Gets the control's GameObject
/// </summary>
public GameObject GameObject => gameObject;
/// <summary>
/// Gets the Unity RectTransform component.
/// </summary>
public RectTransform RectTransform { get; private set; }
/// <summary>
/// Gets the Unity Image component on the same object if it exists.
/// </summary>
public Image Image { get; private set; }
/// <summary>
/// Gets the ScrollRect reference used in drag/scroll events.
/// </summary>
public ScrollRect ScrollRect { get; private set; }
/// <summary>
/// Gets whether the control is currently being dragged.
/// </summary>
public bool IsDragging { get; private set; }
/// <summary>
/// Gets whether the component is being destroyed. This means OnDestroy() was called the same frame
/// and will effectively be destroyed at the end of it.
/// </summary>
public bool IsBeingDestroyed { get; private set; }
/// <summary>
/// Gets or sets whether the object can be interacted with and will send any events.
/// </summary>
public bool Enabled
{
get => enabled;
set
{
enabled = value;
if (_selectable != null)
{
_selectable.interactable = value && _interactable;
}
if (_graphic != null)
{
_graphic.raycastTarget = value && _raycastTarget;
}
}
}
/// <summary>
/// Gets or sets whether the widget is interactable or not. The widget should have a <see cref="Selectable" />
/// component.
/// </summary>
public bool Interactable
{
get => _interactable;
set
{
if (_selectable != null)
{
_selectable.interactable = value;
}
_interactable = value;
}
}
/// <summary>
/// Gets or sets the custom data property.
/// </summary>
public object Tag { get; set; }
/// <summary>
/// Gets whether the control was clicked since the last time it was set to false.
/// </summary>
public bool WasClicked { get; set; }
/// <summary>
/// Gets or sets how many seconds need to pass to trigger a <see cref="PressHeld" /> event
/// </summary>
public float PressAndHoldDuration
{
get => _pressAndHoldDuration;
set => _pressAndHoldDuration = value;
}
/// <summary>
/// Gets or sets the feedback when the UI element was pressed.
/// </summary>
public UxrControlFeedback FeedbackOnDown
{
get => _feedbackOnPress;
set => _feedbackOnPress = value;
}
/// <summary>
/// Gets or sets the feedback when the UI element was released.
/// </summary>
public UxrControlFeedback FeedbackOnUp
{
get => _feedbackOnRelease;
set => _feedbackOnRelease = value;
}
/// <summary>
/// Gets or sets the feedback when the UI element was clicked.
/// </summary>
public UxrControlFeedback FeedbackOnClick
{
get => _feedbackOnClick;
set => _feedbackOnClick = value;
}
#endregion
#region Public Methods
/// <summary>
/// Creates an awaitable task that blocks until a control from a given set is clicked, and returns the control that was
/// clicked.
/// </summary>
/// <param name="ct">Cancellation token, to cancel the task</param>
/// <param name="controls">Controls to listen to</param>
/// <returns>Awaitable <see cref="Task" /> returning the control that was clicked, or null if the task was cancelled</returns>
public static async Task<UxrControlInput> WaitForClickAsync(CancellationToken ct, params UxrControlInput[] controls)
{
async Task<UxrControlInput> ReadControl(UxrControlInput control, CancellationToken ct = default)
{
await control.WaitForClickAsync(ct);
return ct.IsCancellationRequested ? null : control;
}
using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
IEnumerable<Task<UxrControlInput>> tasks = controls.Select(b => ReadControl(b, ct));
Task<UxrControlInput> finishedTask = await Task.WhenAny(tasks);
if (!finishedTask.IsCanceled)
{
cts.Cancel();
}
return await finishedTask;
}
/// <summary>
/// Creates an awaitable task that blocks until the control is clicked.
/// </summary>
/// <param name="ct">Optional cancellation token, to cancel the task</param>
/// <returns>Awaitable <see cref="Task" /> returning the control that was clicked or null if the task was cancelled</returns>
public async Task WaitForClickAsync(CancellationToken ct = default)
{
bool isClicked = false;
void ControlClicked(UxrControlInput localControl, PointerEventData eventData)
{
isClicked = localControl.Interactable;
}
Clicked += ControlClicked;
await TaskExt.WaitUntil(() => isClicked, ct);
Clicked -= ControlClicked;
}
#endregion
#region Unity
/// <summary>
/// Sets up the internal references.
/// </summary>
protected virtual void Awake()
{
RectTransform = GetComponent<RectTransform>();
Image = GetComponent<Image>();
ScrollRect = GetComponentInParent<ScrollRect>();
_selectable = GetComponent<Selectable>();
_graphic = GetComponent<Graphic>();
if (_graphic)
{
_raycastTarget = _graphic.raycastTarget;
}
if (_selectable != null)
{
Interactable &= _selectable.interactable;
}
}
/// <summary>
/// Unity OnDestroy() method.
/// </summary>
protected virtual void OnDestroy()
{
IsBeingDestroyed = true;
}
/// <summary>
/// Unity OnEnable() method.
/// </summary>
protected virtual void OnEnable()
{
}
/// <summary>
/// Unity OnDisable() method.
/// </summary>
protected virtual void OnDisable()
{
}
/// <summary>
/// Resets the component.
/// </summary>
protected virtual void Reset()
{
_feedbackOnPress = UxrControlFeedback.FeedbackDown;
_feedbackOnRelease = UxrControlFeedback.FeedbackUp;
_feedbackOnClick = UxrControlFeedback.FeedbackClick;
}
/// <summary>
/// Unity Start() method.
/// </summary>
protected virtual void Start()
{
}
/// <summary>
/// Checks for the press held event.
/// </summary>
protected virtual void Update()
{
CheckPressHeldEvent();
}
#endregion
#region Event Trigger Methods
/// <summary>
/// Method called by Unity when the control started being dragged.
/// </summary>
/// <param name="eventData">Event data</param>
public override void OnBeginDrag(PointerEventData eventData)
{
base.OnBeginDrag(eventData);
OnDragStarted(eventData);
}
/// <summary>
/// Method called by Unity each frame the control is being dragged.
/// </summary>
/// <param name="eventData">Event data</param>
public override void OnDrag(PointerEventData eventData)
{
base.OnDrag(eventData);
OnDragged(eventData);
}
/// <summary>
/// Method called by Unity when a drag event ended on the control.
/// </summary>
/// <param name="eventData">Event data</param>
public override void OnEndDrag(PointerEventData eventData)
{
base.OnEndDrag(eventData);
OnDragEnded(eventData);
}
/// <summary>
/// Method called by Unity when the control was dropped.
/// </summary>
/// <param name="eventData">Event data</param>
public override void OnDrop(PointerEventData eventData)
{
base.OnDrop(eventData);
OnDropped(eventData);
}
/// <summary>
/// Method called by Unity when the control was pressed.
/// </summary>
/// <param name="eventData">Event data</param>
public override void OnPointerDown(PointerEventData eventData)
{
base.OnPointerDown(eventData);
OnPressed(eventData);
}
/// <summary>
/// Method called by Unity when the control was released after being pressed.
/// </summary>
/// <param name="eventData">Event data</param>
public override void OnPointerUp(PointerEventData eventData)
{
base.OnPointerUp(eventData);
OnReleased(eventData);
}
/// <summary>
/// Method called by Unity when the control was clicked. A click depending on the operating mode can be a press or a
/// release after a press.
/// </summary>
/// <param name="eventData">Event data</param>
public override void OnPointerClick(PointerEventData eventData)
{
base.OnPointerClick(eventData);
OnClicked(eventData);
}
/// <summary>
/// Method called by Unity when the cursor entered the control rect.
/// </summary>
/// <param name="eventData">Event data</param>
public override void OnPointerEnter(PointerEventData eventData)
{
base.OnPointerEnter(eventData);
OnCursorEntered(eventData);
}
/// <summary>
/// Method called by Unity when the cursor exited the control rect.
/// </summary>
/// <param name="eventData">Event data</param>
public override void OnPointerExit(PointerEventData eventData)
{
base.OnPointerExit(eventData);
OnCursorExited(eventData);
}
/// <summary>
/// Method called by Unity when the content of an InputField was updated on the control.
/// </summary>
/// <param name="eventData">Event data</param>
public override void OnUpdateSelected(BaseEventData eventData)
{
base.OnUpdateSelected(eventData);
if (enabled && UpdateSelected != null)
{
UpdateSelected?.Invoke(this, eventData);
}
}
/// <summary>
/// Method called by Unity when the content of an InputField was validated (OK was pressed) on the control.
/// </summary>
/// <param name="eventData">Event data</param>
public override void OnSubmit(BaseEventData eventData)
{
base.OnSubmit(eventData);
OnInputSubmitted(eventData);
}
/// <summary>
/// Method called by Unity when the content was scrolled on the control.
/// </summary>
/// <param name="eventData">Event data</param>
public override void OnScroll(PointerEventData eventData)
{
base.OnScroll(eventData);
if (ScrollRect != null)
{
ScrollRect.OnScroll(eventData);
}
}
/// <summary>
/// Method called by Unity when the control was selected.
/// </summary>
/// <param name="eventData">Event data</param>
public override void OnSelect(BaseEventData eventData)
{
base.OnSelect(eventData);
}
/// <summary>
/// Method called by Unity when a Cancel event was sent to control.
/// </summary>
/// <param name="eventData">Event data</param>
public override void OnCancel(BaseEventData eventData)
{
base.OnCancel(eventData);
}
/// <summary>
/// Method called by Unity when the control was deselected.
/// </summary>
/// <param name="eventData">Event data</param>
public override void OnDeselect(BaseEventData eventData)
{
base.OnDeselect(eventData);
}
/// <summary>
/// Method called by Unity when a potential drag could be started on the the control but the drag did not start yet.
/// </summary>
/// <param name="eventData">Event data</param>
public override void OnInitializePotentialDrag(PointerEventData eventData)
{
base.OnInitializePotentialDrag(eventData);
}
/// <summary>
/// Method called when navigating through the control.
/// </summary>
/// <param name="eventData">Event data</param>
public override void OnMove(AxisEventData eventData)
{
base.OnMove(eventData);
}
/// <summary>
/// Overridable event trigger for <see cref="DragStarted" /> and <see cref="GlobalDragStarted" />.
/// </summary>
/// <param name="eventData">Event parameters</param>
protected virtual void OnDragStarted(PointerEventData eventData)
{
if (ScrollRect != null)
{
ScrollRect.OnBeginDrag(eventData);
}
if (enabled)
{
IsDragging = true;
DragStarted?.Invoke(this, eventData);
GlobalDragStarted?.Invoke(this, eventData);
}
ResetTapAndHoldEventInfo();
}
/// <summary>
/// Overridable event trigger for <see cref="Dragged" /> and <see cref="GlobalDragged" />.
/// </summary>
/// <param name="eventData">Event parameters</param>
protected virtual void OnDragged(PointerEventData eventData)
{
if (ScrollRect != null)
{
ScrollRect.OnDrag(eventData);
}
if (enabled)
{
Dragged?.Invoke(this, eventData);
GlobalDragged?.Invoke(this, eventData);
}
}
/// <summary>
/// Overridable event trigger for <see cref="DragEnded" /> and <see cref="GlobalDragEnded" />.
/// </summary>
/// <param name="eventData">Event parameters</param>
protected virtual void OnDragEnded(PointerEventData eventData)
{
if (ScrollRect != null)
{
ScrollRect.OnEndDrag(eventData);
}
if (enabled)
{
IsDragging = false;
DragEnded?.Invoke(this, eventData);
GlobalDragEnded?.Invoke(this, eventData);
}
}
/// <summary>
/// Overridable event trigger for <see cref="Dropped" />.
/// </summary>
/// <param name="eventData">Event parameters</param>
protected virtual void OnDropped(PointerEventData eventData)
{
if (enabled && Dropped != null)
{
Dropped?.Invoke(this, eventData);
}
}
/// <summary>
/// Overridable event trigger for <see cref="Pressed" /> and <see cref="GlobalPressed" />.
/// </summary>
/// <param name="eventData">Event parameters</param>
protected virtual void OnPressed(PointerEventData eventData)
{
if (enabled)
{
_isPressAndHold = true;
_pressAndHoldEventData = eventData;
_pressAndHoldTimer = 0.0f;
GlobalPressed?.Invoke(this, eventData);
Pressed?.Invoke(this, eventData);
}
}
/// <summary>
/// Overridable event trigger for <see cref="Released" /> and <see cref="GlobalReleased" />.
/// </summary>
/// <param name="eventData">Event parameters</param>
protected virtual void OnReleased(PointerEventData eventData)
{
if (enabled)
{
GlobalReleased?.Invoke(this, eventData);
Released?.Invoke(this, eventData);
}
ResetTapAndHoldEventInfo();
}
/// <summary>
/// Overridable event trigger for <see cref="Clicked" /> and <see cref="GlobalClicked" />.
/// </summary>
/// <param name="eventData">Event parameters</param>
protected virtual void OnClicked(PointerEventData eventData)
{
if (!IsDragging && enabled && Interactable)
{
WasClicked = true;
GlobalClicked?.Invoke(this, eventData);
Clicked?.Invoke(this, eventData);
}
}
/// <summary>
/// Overridable event trigger for <see cref="CursorEntered" />.
/// </summary>
/// <param name="eventData">Event parameters</param>
protected virtual void OnCursorEntered(PointerEventData eventData)
{
if (enabled)
{
CursorEntered?.Invoke(this, eventData);
}
}
/// <summary>
/// Overridable event trigger for <see cref="CursorExited" />.
/// </summary>
/// <param name="eventData">Event parameters</param>
protected virtual void OnCursorExited(PointerEventData eventData)
{
if (enabled)
{
CursorExited?.Invoke(this, eventData);
}
}
/// <summary>
/// Overridable event trigger for <see cref="InputSubmitted" />.
/// </summary>
/// <param name="eventData">Event parameters</param>
protected virtual void OnInputSubmitted(BaseEventData eventData)
{
if (enabled && InputSubmitted != null)
{
InputSubmitted?.Invoke(this, eventData);
}
}
#endregion
#region Private Methods
/// <summary>
/// Checks if the TapAndHold timer reached its goal.
/// </summary>
private void CheckPressHeldEvent()
{
if (PressHeld != null && _isPressAndHold)
{
_pressAndHoldTimer += Time.deltaTime;
if (_pressAndHoldTimer > _pressAndHoldDuration)
{
PressHeld(this, _pressAndHoldEventData);
ResetTapAndHoldEventInfo();
}
}
}
/// <summary>
/// Resets the TapAndHold timers and state
/// </summary>
private void ResetTapAndHoldEventInfo()
{
_isPressAndHold = false;
_pressAndHoldEventData = null;
_pressAndHoldTimer = 0.0f;
}
#endregion
#region Private Types & Data
private Selectable _selectable;
private Graphic _graphic;
private bool _raycastTarget;
private float _pressAndHoldTimer;
private bool _isPressAndHold;
private PointerEventData _pressAndHoldEventData;
private bool _interactable = true;
#endregion
}
}