// 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 } }