// Copyright (c) 2015 - 2023 Doozy Entertainment. All Rights Reserved.
// This code can only be used under the standard Unity Asset Store End User License Agreement
// A Copy of the EULA APPENDIX 1 is available at http://unity3d.com/company/legal/as_terms
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Doozy.Runtime.Common.Utils;
using Doozy.Runtime.UIManager.Events;
using Doozy.Runtime.UIManager.Input;
using Doozy.Runtime.UIManager.ScriptableObjects;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
// ReSharper disable MemberCanBeProtected.Global
// ReSharper disable MemberCanBePrivate.Global
namespace Doozy.Runtime.UIManager.Components
{
/// UI object used to create a selectable control. This class is derived from Unity's Selectable class
[DisallowMultipleComponent]
[AddComponentMenu("Doozy/UI/Components/UISelectable")]
[SelectionBase]
public partial class UISelectable : Selectable, ICanvasElement, IUseMultiplayerInfo
{
#if UNITY_EDITOR
[UnityEditor.MenuItem("GameObject/Doozy/UI/Components/UISelectable", false, 8)]
private static void CreateComponent(UnityEditor.MenuCommand menuCommand)
{
GameObjectUtils.AddToScene("UISelectable", false, true);
}
#endif
public const string k_StreamCategory = nameof(UISelectable);
public const float k_DefaultAnimationDuration = 0.2f;
#region MultiplayerInfo
[SerializeField] private MultiplayerInfo MultiplayerInfo;
public MultiplayerInfo multiplayerInfo => MultiplayerInfo;
public bool hasMultiplayerInfo => multiplayerInfo != null;
public int playerIndex => multiplayerMode & hasMultiplayerInfo ? multiplayerInfo.playerIndex : inputSettings.defaultPlayerIndex;
public void SetMultiplayerInfo(MultiplayerInfo reference) => MultiplayerInfo = reference;
#endregion
/// Reference to the UIManager Input Settings
public static UIManagerInputSettings inputSettings => UIManagerInputSettings.instance;
/// True Multiplayer Mode is enabled
public static bool multiplayerMode => inputSettings.multiplayerMode;
public enum SelectableType
{
Button,
Toggle
}
/// Selectable type
public virtual SelectableType selectableType => SelectableType.Button;
/// Returns TRUE if the selectable type is Button
public bool isButton => selectableType == SelectableType.Button;
/// Returns TRUE if the selectable type is Toggle
public bool isToggle => selectableType == SelectableType.Toggle;
[SerializeField] internal bool IsOn;
/// TRUE or FALSE for Toggle selectable type
public virtual bool isOn
{
get => true;
// ReSharper disable once ValueParameterNotUsed
set => IsOn = true;
}
private static IEnumerable s_uiSelectionStates;
/// Enumeration of all the UISelectionState enum values
public static IEnumerable uiSelectionStates => s_uiSelectionStates ?? (s_uiSelectionStates = Enum.GetValues(typeof(UISelectionState)).Cast());
/// Copy of the array of all the UISelectable objects currently active in the scene.
public static UISelectable[] allUISelectablesArray =>
allSelectablesArray.Where(selectable => selectable is UISelectable).Cast().ToArray();
private RectTransform m_RectTransform;
/// Reference to the RectTransform component
public RectTransform rectTransform => m_RectTransform ? m_RectTransform : m_RectTransform = GetComponent();
[SerializeField] private UISelectionState CurrentUISelectionState;
public UISelectionState currentUISelectionState => CurrentUISelectionState;
[SerializeField] private bool DeselectAfterPress;
public bool deselectAfterPress
{
get => DeselectAfterPress;
set => DeselectAfterPress = value;
}
/// UISelectionState changed - callback invoked when selection state changed
public UISelectionStateEvent OnSelectionStateChangedCallback = new UISelectionStateEvent();
[SerializeField] private string CurrentStateName;
/// Name of the current selection state
public string currentStateName => CurrentStateName;
[SerializeField] private UISelectableState NormalState = new UISelectableState(UISelectionState.Normal);
/// Callbacks for the Normal selection state
public UISelectableState normalState => NormalState;
[SerializeField] private UISelectableState HighlightedState = new UISelectableState(UISelectionState.Highlighted);
/// Callbacks for the Highlighted selection state
public UISelectableState highlightedState => HighlightedState;
[SerializeField] private UISelectableState PressedState = new UISelectableState(UISelectionState.Pressed);
/// Callbacks for the Pressed selection state
public UISelectableState pressedState => PressedState;
[SerializeField] private UISelectableState SelectedState = new UISelectableState(UISelectionState.Selected);
/// Callbacks for the Selected selection state
public UISelectableState selectedState => SelectedState;
[SerializeField] private UISelectableState DisabledState = new UISelectableState(UISelectionState.Disabled);
/// Callbacks for the disabled selection state
public UISelectableState disabledState => DisabledState;
[SerializeField] private UIBehaviours Behaviours;
/// Manages UIBehaviour components
public UIBehaviours behaviours => Behaviours;
#region UIBehaviour shortcuts
/// PointerEnter UIBehaviour
public UIBehaviour onPointerEnterBehaviour => AddBehaviour(UIBehaviour.Name.PointerEnter);
/// PointerExit UIBehaviour
public UIBehaviour onPointerExitBehaviour => AddBehaviour(UIBehaviour.Name.PointerExit);
/// PointerDown UIBehaviour
public UIBehaviour onPointerDownBehaviour => AddBehaviour(UIBehaviour.Name.PointerDown);
/// PointerUp UIBehaviour
public UIBehaviour onPointerUpBehaviour => AddBehaviour(UIBehaviour.Name.PointerUp);
/// PointerClick UIBehaviour
public UIBehaviour onClickBehaviour => AddBehaviour(UIBehaviour.Name.PointerClick);
/// PointerDoubleClick UIBehaviour
public UIBehaviour onDoubleClickBehaviour => AddBehaviour(UIBehaviour.Name.PointerDoubleClick);
/// PointerLongClick UIBehaviour
public UIBehaviour onLongClickBehaviour => AddBehaviour(UIBehaviour.Name.PointerLongClick);
/// PointerLeftClick UIBehaviour
public UIBehaviour onLeftClickBehaviour => AddBehaviour(UIBehaviour.Name.PointerLeftClick);
/// PointerMiddleClick UIBehaviour
public UIBehaviour onMiddleClickBehaviour => AddBehaviour(UIBehaviour.Name.PointerMiddleClick);
/// PointerRightClick UIBehaviour
public UIBehaviour onRightClickBehaviour => AddBehaviour(UIBehaviour.Name.PointerRightClick);
/// Selected UIBehaviour
public UIBehaviour onSelectedBehaviour => AddBehaviour(UIBehaviour.Name.Selected);
/// Deselected UIBehaviour
public UIBehaviour onDeselectedBehaviour => AddBehaviour(UIBehaviour.Name.Deselected);
/// Submit UIBehaviour
public UIBehaviour onSubmitBehaviour => AddBehaviour(UIBehaviour.Name.Submit);
#endregion
#region UnityEvent shortcuts from UIBehaviour
/// PointerEnter UnityEvent
public UnityEvent onPointerEnterEvent => onPointerEnterBehaviour.Event;
/// PointerExit UnityEvent
public UnityEvent onPointerExitEvent => onPointerExitBehaviour.Event;
/// PointerDown UnityEvent
public UnityEvent onPointerDownEvent => onPointerDownBehaviour.Event;
/// PointerUp UnityEvent
public UnityEvent onPointerUpEvent => onPointerUpBehaviour.Event;
/// PointerClick UnityEvent
public UnityEvent onClickEvent => onClickBehaviour.Event;
/// PointerDoubleClick UnityEvent
public UnityEvent onDoubleClickEvent => onDoubleClickBehaviour.Event;
/// PointerLongClick UnityEvent
public UnityEvent onLongClickEvent => onLongClickBehaviour.Event;
/// PointerLeftClick UnityEvent
public UnityEvent onLeftClickEvent => onLeftClickBehaviour.Event;
/// PointerMiddleClick UnityEvent
public UnityEvent onMiddleClickEvent => onMiddleClickBehaviour.Event;
/// PointerRightClick UnityEvent
public UnityEvent onRightClickEvent => onRightClickBehaviour.Event;
/// Selected UnityEvent
public UnityEvent onSelectedEvent => onSelectedBehaviour.Event;
/// Deselected UnityEvent
public UnityEvent onDeselectedEvent => onDeselectedBehaviour.Event;
/// Submit UnityEvent
public UnityEvent onSubmitEvent => onSubmitBehaviour.Event;
#endregion
///
/// Cooldown time in seconds before the selectable can be interacted with again.
/// This is useful when you want to prevent the selectable from being clicked multiple times in a short period of time.
///
public float Cooldown;
///
/// Set the interactable state to false during the cooldown time (and set selectable state to disabled).
///
public bool DisableWhenInCooldown;
/// Internal coroutine to handle the cooldown.
private Coroutine cooldownRoutine { get; set; }
/// Flag to indicate if the selectable is in cooldown.
public bool inCooldown { get; protected set; }
private bool selectableInitialized { get; set; }
#if UNITY_EDITOR
protected override void OnValidate()
{
if (!UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this) && !Application.isPlaying)
CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this);
base.OnValidate();
}
protected override void Reset()
{
base.Reset();
targetGraphic = null;
transition = Transition.None;
inCooldown = false;
}
#endif // if UNITY_EDITOR
#region ICanvasElement
public virtual void Rebuild(CanvasUpdate executing) {}
public virtual void LayoutComplete() {}
public virtual void GraphicUpdateComplete() {}
#endregion
public UISelectable()
{
Behaviours =
new UIBehaviours()
.SetSelectable(this);
}
protected override void Awake()
{
if (Application.isPlaying)
BackButton.Initialize();
targetGraphic = null;
transition = Transition.None;
m_RectTransform = GetComponent();
selectableInitialized = false;
Behaviours
.SetSelectable(this)
.SetSignalSource(gameObject);
inCooldown = false;
}
protected override void Start()
{
base.Start();
RefreshState();
}
protected override void OnEnable()
{
if (Application.isPlaying)
{
BackButton.Initialize();
}
base.OnEnable();
if (!Application.isPlaying) return;
if (selectableInitialized) RefreshState();
StartCoroutine(ConnectBehaviours());
inCooldown = false;
}
protected override void OnDisable()
{
base.OnDisable();
behaviours.Disconnect();
inCooldown = false;
}
private IEnumerator ConnectBehaviours()
{
yield return null;
if(behaviours?.behaviours == null) yield break;
if(behaviours.behaviours.Count == 0) yield break;
behaviours
.SetSelectable(this)
.SetSignalSource(gameObject)
.Connect();
}
///
/// Add the given behaviour and get a reference to it (automatically connects)
/// If the behaviour already exists, the reference to it will get automatically returned.
///
/// UIBehaviour.Name
public UIBehaviour AddBehaviour(UIBehaviour.Name behaviourName) =>
behaviours.AddBehaviour(behaviourName);
/// Remove the given behaviour (automatically disconnects)
/// UIBehaviour.Name
public void RemoveBehaviour(UIBehaviour.Name behaviourName) =>
behaviours.RemoveBehaviour(behaviourName);
/// Check if the given behaviour has been added (exists)
/// UIBehaviour.Name
public bool HasBehaviour(UIBehaviour.Name behaviourName) =>
behaviours.HasBehaviour(behaviourName);
///
/// Get the behaviour with the given name.
/// Returns null if the behaviour has not been added (does not exist)
///
/// UIBehaviour.Name
public UIBehaviour GetBehaviour(UIBehaviour.Name behaviourName) =>
behaviours.GetBehaviour(behaviourName);
protected override void InstantClearState()
{
base.InstantClearState();
if (currentUISelectionState != UISelectionState.Normal)
SetState(UISelectionState.Normal);
}
protected override void DoStateTransition(SelectionState state, bool instant)
{
if (!gameObject.activeInHierarchy)
return;
if (selectableInitialized & currentUISelectionState == GetUISelectionState(state))
return;
SetState(GetUISelectionState(state));
}
/// Set (by force) the UISelectable to the given selection state
/// Target selection state
public UISelectable SetState(UISelectionState state)
{
selectableInitialized = true;
if (deselectAfterPress && CurrentUISelectionState == UISelectionState.Pressed && state == UISelectionState.Selected)
{
EventSystem.current.SetSelectedGameObject(null);
// state = UISelectionState.Normal;
}
OnSelectionStateChangedCallback?.Invoke(state);
CurrentUISelectionState = state;
CurrentStateName = state.ToString();
GetUISelectableState(state).stateEvent.Execute();
return this;
}
public UISelectable RefreshState() =>
SetState(currentUISelectionState);
/// Get a reference to the UISelectableState for the given selection state
/// Target selection state
public UISelectableState GetUISelectableState(UISelectionState state) =>
state switch
{
UISelectionState.Normal => normalState,
UISelectionState.Highlighted => highlightedState,
UISelectionState.Pressed => pressedState,
UISelectionState.Selected => selectedState,
UISelectionState.Disabled => disabledState,
_ => throw new ArgumentOutOfRangeException(nameof(state), state, null)
};
/// Get a reference to the current UISelectableState
public UISelectableState GetCurrentUISelectableState() =>
GetUISelectableState(currentUISelectionState);
///
/// Convert a SelectionState to a UISelectionState
/// This is needed because the SelectedState enum (in the Selectable class) is set to protected instead of public (THANKS UNITY)
///
/// Selection state to convert
private static UISelectionState GetUISelectionState(SelectionState selectionState) =>
selectionState switch
{
SelectionState.Normal => UISelectionState.Normal,
SelectionState.Highlighted => UISelectionState.Highlighted,
SelectionState.Pressed => UISelectionState.Pressed,
SelectionState.Selected => UISelectionState.Selected,
SelectionState.Disabled => UISelectionState.Disabled,
_ => throw new ArgumentOutOfRangeException(nameof(selectionState), selectionState, null)
};
#region Private Methods
/// Start the cooldown
protected void StartCooldown()
{
StopCooldown();
if (Cooldown <= 0) return;
cooldownRoutine = StartCoroutine(CooldownRoutine());
}
/// Stop the cooldown
protected void StopCooldown()
{
inCooldown = false;
if (DisableWhenInCooldown) interactable = true;
if (cooldownRoutine == null) return;
StopCoroutine(cooldownRoutine);
cooldownRoutine = null;
}
/// Internal coroutine to handle the cooldown.
protected IEnumerator CooldownRoutine()
{
if (DisableWhenInCooldown) interactable = false;
yield return new WaitForEndOfFrame(); //mark as in cooldown after the first frame to avoid race conditions with UIBehaviours
inCooldown = true;
yield return new WaitForSecondsRealtime(Cooldown);
if (DisableWhenInCooldown) interactable = true;
inCooldown = false;
cooldownRoutine = null;
}
#endregion
}
}