// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) VRMADA, All rights reserved. // // -------------------------------------------------------------------------------------------------------------------- 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); /// /// A component derived from that simplifies the handling of events triggered by UI /// controls. Among the key benefits are: /// /// /// 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. /// /// /// 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 . /// /// /// Each can specify the audio/haptic feedback for the click/down/up events. /// /// /// 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 /// /// Event called whenever any is pressed. /// public static event PressedEventHandler GlobalPressed; /// /// Event called whenever any press is released. /// public static event ReleasedEventHandler GlobalReleased; /// /// Event called whenever any is clicked. A click depending on the operating mode can be /// a press or a release after a press. /// public static event ClickedEventHandler GlobalClicked; /// /// Event called whenever any started being dragged. /// public static event DragStartedEventHandler GlobalDragStarted; /// /// Event called during the frames any is being dragged. /// public static event DraggedEventHandler GlobalDragged; /// /// Event called whenever any stopped being dragged. /// public static event DragEndedEventHandler GlobalDragEnded; /// /// Event called whenever the control started being dragged. /// public event DragStartedEventHandler DragStarted; /// /// Event called each frame the control is being dragged. /// public event DraggedEventHandler Dragged; /// /// Event called whenever the control stopped being dragged. /// public event DragEndedEventHandler DragEnded; /// /// Event called whenever the control was dropped. /// public event DroppedEventHandler Dropped; /// /// Event called whenever the control was pressed. /// public event PressedEventHandler Pressed; /// /// Event called whenever the control was released after being pressed. /// public event ReleasedEventHandler Released; /// /// Event called whenever the control was clicked. A click depending on the operating mode can be a press or a release /// after a press. /// public event ClickedEventHandler Clicked; /// /// Event called whenever the control was kept being pressed for seconds without /// being dragged. /// public event PressHeldEventHandler PressHeld; /// /// Event called whenever the pointer entered the control. /// public event CursorEnteredEventHandler CursorEntered; /// /// Event called whenever the pointer exited the control. /// public event CursorExitedEventHandler CursorExited; /// /// Event called whenever the selected control's input field is updated/changed. /// public event UpdateSelectedEventHandler UpdateSelected; /// /// Event called whenever the control's input field was submitted (OK was pressed). /// public event InputSubmittedEventHandler InputSubmitted; /// /// Gets the control's GameObject /// public GameObject GameObject => gameObject; /// /// Gets the Unity RectTransform component. /// public RectTransform RectTransform { get; private set; } /// /// Gets the Unity Image component on the same object if it exists. /// public Image Image { get; private set; } /// /// Gets the ScrollRect reference used in drag/scroll events. /// public ScrollRect ScrollRect { get; private set; } /// /// Gets whether the control is currently being dragged. /// public bool IsDragging { get; private set; } /// /// 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. /// public bool IsBeingDestroyed { get; private set; } /// /// Gets or sets whether the object can be interacted with and will send any events. /// public bool Enabled { get => enabled; set { enabled = value; if (_selectable != null) { _selectable.interactable = value && _interactable; } if (_graphic != null) { _graphic.raycastTarget = value && _raycastTarget; } } } /// /// Gets or sets whether the widget is interactable or not. The widget should have a /// component. /// public bool Interactable { get => _interactable; set { if (_selectable != null) { _selectable.interactable = value; } _interactable = value; } } /// /// Gets or sets the custom data property. /// public object Tag { get; set; } /// /// Gets whether the control was clicked since the last time it was set to false. /// public bool WasClicked { get; set; } /// /// Gets or sets how many seconds need to pass to trigger a event /// public float PressAndHoldDuration { get => _pressAndHoldDuration; set => _pressAndHoldDuration = value; } /// /// Gets or sets the feedback when the UI element was pressed. /// public UxrControlFeedback FeedbackOnDown { get => _feedbackOnPress; set => _feedbackOnPress = value; } /// /// Gets or sets the feedback when the UI element was released. /// public UxrControlFeedback FeedbackOnUp { get => _feedbackOnRelease; set => _feedbackOnRelease = value; } /// /// Gets or sets the feedback when the UI element was clicked. /// public UxrControlFeedback FeedbackOnClick { get => _feedbackOnClick; set => _feedbackOnClick = value; } #endregion #region Public Methods /// /// Creates an awaitable task that blocks until a control from a given set is clicked, and returns the control that was /// clicked. /// /// Cancellation token, to cancel the task /// Controls to listen to /// Awaitable returning the control that was clicked, or null if the task was cancelled public static async Task WaitForClickAsync(CancellationToken ct, params UxrControlInput[] controls) { async Task ReadControl(UxrControlInput control, CancellationToken ct = default) { await control.WaitForClickAsync(ct); return ct.IsCancellationRequested ? null : control; } using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(ct); IEnumerable> tasks = controls.Select(b => ReadControl(b, ct)); Task finishedTask = await Task.WhenAny(tasks); if (!finishedTask.IsCanceled) { cts.Cancel(); } return await finishedTask; } /// /// Creates an awaitable task that blocks until the control is clicked. /// /// Optional cancellation token, to cancel the task /// Awaitable returning the control that was clicked or null if the task was cancelled 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 /// /// Sets up the internal references. /// protected virtual void Awake() { RectTransform = GetComponent(); Image = GetComponent(); ScrollRect = GetComponentInParent(); _selectable = GetComponent(); _graphic = GetComponent(); if (_graphic) { _raycastTarget = _graphic.raycastTarget; } if (_selectable != null) { Interactable &= _selectable.interactable; } } /// /// Unity OnDestroy() method. /// protected virtual void OnDestroy() { IsBeingDestroyed = true; } /// /// Unity OnEnable() method. /// protected virtual void OnEnable() { } /// /// Unity OnDisable() method. /// protected virtual void OnDisable() { } /// /// Resets the component. /// protected virtual void Reset() { _feedbackOnPress = UxrControlFeedback.FeedbackDown; _feedbackOnRelease = UxrControlFeedback.FeedbackUp; _feedbackOnClick = UxrControlFeedback.FeedbackClick; } /// /// Unity Start() method. /// protected virtual void Start() { } /// /// Checks for the press held event. /// protected virtual void Update() { CheckPressHeldEvent(); } #endregion #region Event Trigger Methods /// /// Method called by Unity when the control started being dragged. /// /// Event data public override void OnBeginDrag(PointerEventData eventData) { base.OnBeginDrag(eventData); OnDragStarted(eventData); } /// /// Method called by Unity each frame the control is being dragged. /// /// Event data public override void OnDrag(PointerEventData eventData) { base.OnDrag(eventData); OnDragged(eventData); } /// /// Method called by Unity when a drag event ended on the control. /// /// Event data public override void OnEndDrag(PointerEventData eventData) { base.OnEndDrag(eventData); OnDragEnded(eventData); } /// /// Method called by Unity when the control was dropped. /// /// Event data public override void OnDrop(PointerEventData eventData) { base.OnDrop(eventData); OnDropped(eventData); } /// /// Method called by Unity when the control was pressed. /// /// Event data public override void OnPointerDown(PointerEventData eventData) { base.OnPointerDown(eventData); OnPressed(eventData); } /// /// Method called by Unity when the control was released after being pressed. /// /// Event data public override void OnPointerUp(PointerEventData eventData) { base.OnPointerUp(eventData); OnReleased(eventData); } /// /// 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. /// /// Event data public override void OnPointerClick(PointerEventData eventData) { base.OnPointerClick(eventData); OnClicked(eventData); } /// /// Method called by Unity when the cursor entered the control rect. /// /// Event data public override void OnPointerEnter(PointerEventData eventData) { base.OnPointerEnter(eventData); OnCursorEntered(eventData); } /// /// Method called by Unity when the cursor exited the control rect. /// /// Event data public override void OnPointerExit(PointerEventData eventData) { base.OnPointerExit(eventData); OnCursorExited(eventData); } /// /// Method called by Unity when the content of an InputField was updated on the control. /// /// Event data public override void OnUpdateSelected(BaseEventData eventData) { base.OnUpdateSelected(eventData); if (enabled && UpdateSelected != null) { UpdateSelected?.Invoke(this, eventData); } } /// /// Method called by Unity when the content of an InputField was validated (OK was pressed) on the control. /// /// Event data public override void OnSubmit(BaseEventData eventData) { base.OnSubmit(eventData); OnInputSubmitted(eventData); } /// /// Method called by Unity when the content was scrolled on the control. /// /// Event data public override void OnScroll(PointerEventData eventData) { base.OnScroll(eventData); if (ScrollRect != null) { ScrollRect.OnScroll(eventData); } } /// /// Method called by Unity when the control was selected. /// /// Event data public override void OnSelect(BaseEventData eventData) { base.OnSelect(eventData); } /// /// Method called by Unity when a Cancel event was sent to control. /// /// Event data public override void OnCancel(BaseEventData eventData) { base.OnCancel(eventData); } /// /// Method called by Unity when the control was deselected. /// /// Event data public override void OnDeselect(BaseEventData eventData) { base.OnDeselect(eventData); } /// /// Method called by Unity when a potential drag could be started on the the control but the drag did not start yet. /// /// Event data public override void OnInitializePotentialDrag(PointerEventData eventData) { base.OnInitializePotentialDrag(eventData); } /// /// Method called when navigating through the control. /// /// Event data public override void OnMove(AxisEventData eventData) { base.OnMove(eventData); } /// /// Overridable event trigger for and . /// /// Event parameters 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(); } /// /// Overridable event trigger for and . /// /// Event parameters protected virtual void OnDragged(PointerEventData eventData) { if (ScrollRect != null) { ScrollRect.OnDrag(eventData); } if (enabled) { Dragged?.Invoke(this, eventData); GlobalDragged?.Invoke(this, eventData); } } /// /// Overridable event trigger for and . /// /// Event parameters protected virtual void OnDragEnded(PointerEventData eventData) { if (ScrollRect != null) { ScrollRect.OnEndDrag(eventData); } if (enabled) { IsDragging = false; DragEnded?.Invoke(this, eventData); GlobalDragEnded?.Invoke(this, eventData); } } /// /// Overridable event trigger for . /// /// Event parameters protected virtual void OnDropped(PointerEventData eventData) { if (enabled && Dropped != null) { Dropped?.Invoke(this, eventData); } } /// /// Overridable event trigger for and . /// /// Event parameters protected virtual void OnPressed(PointerEventData eventData) { if (enabled) { _isPressAndHold = true; _pressAndHoldEventData = eventData; _pressAndHoldTimer = 0.0f; GlobalPressed?.Invoke(this, eventData); Pressed?.Invoke(this, eventData); } } /// /// Overridable event trigger for and . /// /// Event parameters protected virtual void OnReleased(PointerEventData eventData) { if (enabled) { GlobalReleased?.Invoke(this, eventData); Released?.Invoke(this, eventData); } ResetTapAndHoldEventInfo(); } /// /// Overridable event trigger for and . /// /// Event parameters protected virtual void OnClicked(PointerEventData eventData) { if (!IsDragging && enabled && Interactable) { WasClicked = true; GlobalClicked?.Invoke(this, eventData); Clicked?.Invoke(this, eventData); } } /// /// Overridable event trigger for . /// /// Event parameters protected virtual void OnCursorEntered(PointerEventData eventData) { if (enabled) { CursorEntered?.Invoke(this, eventData); } } /// /// Overridable event trigger for . /// /// Event parameters protected virtual void OnCursorExited(PointerEventData eventData) { if (enabled) { CursorExited?.Invoke(this, eventData); } } /// /// Overridable event trigger for . /// /// Event parameters protected virtual void OnInputSubmitted(BaseEventData eventData) { if (enabled && InputSubmitted != null) { InputSubmitted?.Invoke(this, eventData); } } #endregion #region Private Methods /// /// Checks if the TapAndHold timer reached its goal. /// private void CheckPressHeldEvent() { if (PressHeld != null && _isPressAndHold) { _pressAndHoldTimer += Time.deltaTime; if (_pressAndHoldTimer > _pressAndHoldDuration) { PressHeld(this, _pressAndHoldEventData); ResetTapAndHoldEventInfo(); } } } /// /// Resets the TapAndHold timers and state /// 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 } }