OldBlueWater/BlueWater/Assets/Doozy/Runtime/UIManager/Containers/UITooltip.cs

1146 lines
53 KiB
C#

// 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.Generic;
using System.Linq;
using Doozy.Runtime.Common.Attributes;
using Doozy.Runtime.Common.Extensions;
using Doozy.Runtime.Common.Utils;
using Doozy.Runtime.Global;
using Doozy.Runtime.Reactor.Reactions;
using Doozy.Runtime.Signals;
using Doozy.Runtime.UIManager.Components;
using Doozy.Runtime.UIManager.Containers.Internal;
using Doozy.Runtime.UIManager.Input;
using Doozy.Runtime.UIManager.ScriptableObjects;
using Doozy.Runtime.UIManager.Triggers;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
#if INPUT_SYSTEM_PACKAGE
using UnityEngine.InputSystem.UI;
#endif
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedMember.Global
// ReSharper disable PartialTypeWithSinglePart
namespace Doozy.Runtime.UIManager.Containers
{
/// <summary>
/// Specialized container that behaves like a tooltip,
/// with parenting, tracking and positioning options.
/// </summary>
[AddComponentMenu("Doozy/UI/Containers/UITooltip")]
[DisallowMultipleComponent]
public partial class UITooltip : UIContainerComponent<UITooltip>
{
#if UNITY_EDITOR
[UnityEditor.MenuItem("GameObject/Doozy/UI/Containers/UITooltip", false, 8)]
private static void CreateComponent(UnityEditor.MenuCommand menuCommand)
{
GameObjectUtils.AddToScene<UITooltip>("UITooltip", false, true);
}
#endif
/// <summary> Describes where a tooltip can be instantiated under </summary>
public enum Parenting
{
/// <summary> The parent of the tooltip will be the TooltipCanvas </summary>
TooltipsCanvas = 0,
/// <summary> The parent of the tooltip will be the RectTransform of UITooltipTrigger that triggered the tooltip </summary>
TooltipTrigger = 1,
/// <summary> The parent of the tooltip will be the RectTransform of the GameObject that has the given UITagId </summary>
UITag = 2
}
/// <summary> Describes the how the tooltip behaves when it is shown </summary>
public enum Tracking
{
/// <summary> The tooltip will be shown at a predefined position and it will stay there until it is hidden </summary>
Disabled = 0,
/// <summary> The tooltip will follow the pointer until it is hidden </summary>
FollowPointer = 1,
/// <summary> The tooltip will follow the GameObject of the trigger until it is hidden </summary>
FollowTrigger = 2,
/// <summary> The tooltip will follow the GameObject of the given UITag until it is hidden </summary>
FollowTarget = 3
}
/// <summary> Describes where the tooltip should be positioned relative to the tracked target </summary>
public enum Positioning
{
TopLeft = 0,
TopCenter = 1,
TopRight = 2,
MiddleLeft = 3,
MiddleCenter = 4,
MiddleRight = 5,
BottomLeft = 6,
BottomCenter = 7,
BottomRight = 8
}
/// <summary> Maximum sorting order value for tooltips </summary>
public const int k_MaxSortingOrder = 32767;
/// <summary> Default tooltip name </summary>
public const string k_DefaultTooltipName = "None";
/// <summary> Default category used by the UITagId to identify the default Tooltip Canvas </summary>
public const string k_DefaultTooltipCanvasUITagCategory = "UITooltip";
/// <summary> Default name used by the UITagId to identify the default Tooltip Canvas </summary>
public const string k_DefaultTooltipCanvasUITagName = "Canvas";
/// <summary> Get all the visible tooltips (returns all tooltips that are either in the isVisible or isShowing state) </summary>
public static IEnumerable<UITooltip> visibleTooltips =>
database.Where(item => item.isVisible || item.isShowing);
private LayoutElement m_LayoutElement;
/// <summary> Reference to the LayoutElement attached to this GameObject </summary>
public LayoutElement layoutElement =>
m_LayoutElement
? m_LayoutElement
: m_LayoutElement = GetComponent<LayoutElement>() ?? gameObject.AddComponent<LayoutElement>();
/// <summary> Internal static reference to the default canvas used to display tooltips </summary>
[ClearOnReload]
private static Canvas s_tooltipsCanvas;
/// <summary>
/// Static reference to the default canvas used to display tooltips (tooltips get parented to this canvas).
/// <para/> You can override the default canvas by referencing another canvas (at runtime) to be used as the default canvas.
/// </summary>
public static Canvas tooltipsCanvas
{
get
{
if (s_tooltipsCanvas != null) return s_tooltipsCanvas;
//look for UITag
UITag uiTag = UITag.GetTags(k_DefaultTooltipCanvasUITagCategory, k_DefaultTooltipCanvasUITagName).FirstOrDefault();
if (uiTag != null)
{
s_tooltipsCanvas = uiTag.GetComponent<Canvas>();
if (s_tooltipsCanvas != null)
return s_tooltipsCanvas;
}
//create Tooltip Canvas
s_tooltipsCanvas = new GameObject("Tooltips Canvas").AddComponent<Canvas>();
s_tooltipsCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
s_tooltipsCanvas.overrideSorting = true;
s_tooltipsCanvas.sortingOrder = k_MaxSortingOrder;
//add the default tag
uiTag = s_tooltipsCanvas.gameObject.AddComponent<UITag>();
uiTag.Id.Category = k_DefaultTooltipCanvasUITagCategory;
uiTag.Id.Name = k_DefaultTooltipCanvasUITagName;
return s_tooltipsCanvas;
}
set => s_tooltipsCanvas = value;
}
#region Tooltip rootCanvas rootCanvasRectTransform
private Canvas m_TooltipRootCanvas;
/// <summary> Internal reference to the root canvas of this tooltip </summary>
internal Canvas tooltipRootCanvas
{
get => m_TooltipRootCanvas;
set
{
tooltipRootCanvasRectTransform = null;
m_TooltipRootCanvas = value;
if (value == null) return;
tooltipRootCanvasRectTransform = value.GetComponent<RectTransform>();
}
}
/// <summary> Internal reference to the RectTransform of the root canvas of this tooltip </summary>
internal RectTransform tooltipRootCanvasRectTransform { get; private set; }
#endregion
#region Target RectTransform rootCanvas rootCanvasRectTransform
private RectTransform m_TargetRectTransform;
/// <summary> Internal reference to the current target RectTransform </summary>
internal RectTransform targetRectTransform
{
get => m_TargetRectTransform;
set
{
targetRootCanvas = null;
targetRootCanvasRectTransform = null;
m_TargetRectTransform = value;
if (value == null) return;
targetRootCanvas = value.GetComponentInParent<Canvas>().rootCanvas;
targetRootCanvasRectTransform = targetRootCanvas.GetComponent<RectTransform>();
}
}
/// <summary> Internal reference to the current target root canvas </summary>
internal Canvas targetRootCanvas { get; private set; }
/// <summary> Internal reference to the current target root canvas RectTransform </summary>
internal RectTransform targetRootCanvasRectTransform { get; private set; }
#endregion
/// <summary> List of all the labels this UITooltip has </summary>
public List<TextMeshProUGUI> Labels = new List<TextMeshProUGUI>();
/// <summary> TRUE if the tooltip has at least one TextMeshProUGUI label reference </summary>
public bool hasLabels => Labels.RemoveNulls().Count > 0;
/// <summary> List of all the images this UITooltip has </summary>
public List<Image> Images = new List<Image>();
/// <summary> TRUE if the tooltip has at least one Image reference </summary>
public bool hasImages => Images.RemoveNulls().Count > 0;
/// <summary> List of all the buttons this UITooltip has </summary>
public List<UIButton> Buttons = new List<UIButton>();
/// <summary> TRUE if the tooltip has at least one UIButton reference </summary>
public bool hasButtons => Buttons.RemoveNulls().Count > 0;
/// <summary> Tooltip rectTransform.rect </summary>
public Rect rect => rectTransform.rect;
/// <summary> Tooltip rectTransform.rect.width </summary>
public float width => rect.width;
/// <summary> Tooltip rectTransform.rect.height </summary>
public float height => rect.height;
/// <summary> Tooltip rectTransform.pivot.x </summary>
public float pivotX => rectTransform.pivot.x;
/// <summary> Tooltip rectTransform.pivot.y </summary>
public float pivotY => rectTransform.pivot.y;
/// <summary> Set where a tooltip should be instantiated under </summary>
public Parenting ParentMode = Parenting.TooltipsCanvas;
/// <summary> Set how the tooltip behaves when it is shown </summary>
public Tracking TrackingMode = Tracking.Disabled;
/// <summary> Set where the tooltip should be positioned relative to the tracked target </summary>
public Positioning PositionMode = Positioning.MiddleCenter;
/// <summary>
/// Id used to identify the designated parent where the tooltip should be parented under,
/// after is has been instantiated
/// </summary>
public UITagId ParentTag;
/// <summary>
/// Id used to identify the follow target when the tracking mode is set to FollowTarget
/// </summary>
public UITagId FollowTag;
/// <summary>
/// Set the offset applied to the tooltip position,
/// after all the positioning has been applied
/// </summary>
public Vector3 PositionOffset = Vector3.zero;
/// <summary>
/// Set a maximum width for the tooltip.
/// If the value is 0, the tooltip will be automatically sized to fit the content")
/// </summary>
public float MaximumWidth;
/// <summary>
/// Keep the tooltip in screen at all times, while it is shown
/// </summary>
public bool KeepInScreen = true;
/// <summary>
/// Enable override sorting and set the sorting order to the maximum value,
/// for the Canvas component attached to this tooltip
/// </summary>
public bool OverrideSorting = true;
/// <summary>
/// Hide (close) the tooltip when the user clicks on any of the UIButton references.
/// <para/> At runtime, a 'hide tooltip' event will be added to all the referenced UIButtons (if any).
/// </summary>
public bool HideOnAnyButton = true;
/// <summary> Set the next 'Back' button event to hide (close) this tooltip </summary>
public bool HideOnBackButton = true;
#if INPUT_SYSTEM_PACKAGE
private static InputSystemUIInputModule s_inputSystemUIInputModule;
public static InputSystemUIInputModule inputModule
{
get
{
if (s_inputSystemUIInputModule != null) return s_inputSystemUIInputModule;
if (EventSystem.current == null)
return null;
s_inputSystemUIInputModule = EventSystem.current.GetComponent<InputSystemUIInputModule>();
return s_inputSystemUIInputModule;
}
}
#endif
/// <summary> Pointer's current position </summary>
public static Vector2 pointerPosition
{
get
{
#pragma warning disable CS0162
#if LEGACY_INPUT_MANAGER
{
return UnityEngine.Input.mousePosition;
}
#elif INPUT_SYSTEM_PACKAGE
{
return inputModule.point.action.ReadValue<Vector2>();
}
#endif
// ReSharper disable once HeuristicUnreachableCode
return Vector2.zero;
#pragma warning restore CS0162
}
}
/// <summary> Internal flag to track if show has been called </summary>
private bool showHasBeenCalled { get; set; }
/// <summary> Internal flag to track if hide has been called </summary>
private bool hideHasBeenCalled { get; set; }
/// <summary> Internal flag to check it the tooltip has a move animator for the show animation </summary>
private bool showHasMovement { get; set; }
/// <summary> Internal flag to check it the tooltip has a move animator for the hide animation </summary>
private bool hideHasMovement { get; set; }
/// <summary> Internal reference to the tooltip's show move reaction (if any) </summary>
private UIMoveReaction showMoveReaction { get; set; }
/// <summary> Internal reference to the tooltip's hide move reaction (if any) </summary>
private UIMoveReaction hideMoveReaction { get; set; }
/// <summary>
/// Internal flag used to keep track if the hide event was added to the buttons.
/// This is needed to avoid adding the event multiple times to the same button.
/// </summary>
private bool addedHideEventToButtons { get; set; }
/// <summary> Internal flag used to keep track if this tooltip added a PointerClickTrigger to it </summary>
private bool addedHideOnClick { get; set; }
#region Parent Info
public RectTransform parentRectTransform { get; internal set; }
#endregion
#region Trigger
private UITooltipTrigger m_Trigger;
public UITooltipTrigger trigger
{
get => m_Trigger;
set
{
triggerRectTransform = null;
m_Trigger = value;
if (value == null) return;
triggerRectTransform = value.GetComponent<RectTransform>();
}
}
/// <summary> RectTransform of the trigger to track when the tooltip is shown and tracking is set to FollowTrigger </summary>
public RectTransform triggerRectTransform { get; private set; }
/// <summary> TRUE if the tooltip has a trigger reference </summary>
public bool hasTrigger => trigger != null;
/// <summary> TRUE if the trigger transform has a RectTransform </summary>
public bool hasTriggerRectTransform => triggerRectTransform != null;
#endregion
#region Follow Target
private GameObject m_FollowTarget;
/// <summary> GameObject to track when the tooltip is shown and tracking is set to FollowTarget </summary>
public GameObject followTarget
{
get => m_FollowTarget;
set
{
followTargetRectTransform = null;
m_FollowTarget = value;
if (value == null) return;
followTargetRectTransform = value.GetComponent<RectTransform>();
}
}
/// <summary> RectTransform of the follow target to track when the tooltip is shown and tracking is set to FollowTarget </summary>
public RectTransform followTargetRectTransform { get; private set; }
/// <summary> TRUE if the tooltip has a follow target reference </summary>
public bool hasFollowTarget => followTarget != null;
/// <summary> TRUE if the follow target transform has a RectTransform </summary>
public bool hasFollowTargetRectTransform => followTargetRectTransform != null;
#endregion
/// <summary> 'Back' button signal receiver used to trigger the hiding of this UITooltip if HideOnBackButton is TRUE </summary>
public SignalReceiver backButtonReceiver { get; set; }
protected override void Awake()
{
base.Awake();
addedHideEventToButtons = false;
//initialize the 'Back' button signal receiver
backButtonReceiver = new SignalReceiver().SetOnSignalCallback(signal =>
{
if (!HideOnBackButton) return;
if (isHidden || isHiding) return;
Hide();
});
}
protected override void OnEnable()
{
base.OnEnable();
//connect to the 'Back' button secondary signal stream
//(this stream ignores the disabled state for the 'Back' button)
BackButton.streamIgnoreDisabled.ConnectReceiver(backButtonReceiver);
}
protected override void OnDisable()
{
base.OnDisable();
//disconnect from the 'Back' button secondary signal stream
//(this stream ignores the disabled state for the 'Back' button)
BackButton.streamIgnoreDisabled.DisconnectReceiver(backButtonReceiver);
}
private void LateUpdate()
{
CheckIfShowOrHideHaveMoveReactions();
if (isShowing && showHasMovement || isHiding && hideHasMovement)
{
ApplyPositioning();
return;
}
ApplyTracking();
ApplyPositioning();
ApplyKeepInScreen();
SetCustomStartPosition(rectTransform.anchoredPosition3D, false);
}
/// <summary> Validate the tooltip's settings </summary>
public virtual void Validate()
{
Labels.RemoveNulls();
Images.RemoveNulls();
Buttons.RemoveNulls();
}
public override void Show()
{
UpdateTarget();
ApplyMaximumWidth();
ApplyHideOnAnyButton();
base.Show();
Coroutiner.ExecuteLater
(
() =>
{
if (this == null) return;
this.ApplyOverrideSorting();
},
3 //number of frames to wait
);
}
public override void InstantShow(bool triggerCallbacks)
{
UpdateTarget();
ApplyMaximumWidth();
ApplyHideOnAnyButton();
base.InstantShow(triggerCallbacks);
this.ApplyOverrideSorting();
}
/// <summary>
/// Update the reference to the root canvas and RectTransform of the root canvas of this tooltip's current target, if any.
/// </summary>
public void UpdateTarget()
{
updateTarget = false;
targetRectTransform = null;
switch (TrackingMode)
{
case Tracking.Disabled:
case Tracking.FollowPointer:
return;
case Tracking.FollowTrigger:
targetRectTransform = triggerRectTransform;
break;
case Tracking.FollowTarget:
targetRectTransform = followTargetRectTransform;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
#region Public Methods
/// <summary>
/// Get a parent reference for the tooltip according to the tooltip's current ParentMode setting.
/// <para/> This is not the same as the tooltip's transform.parent, which is the parent of the tooltip's GameObject.
/// </summary>
public RectTransform GetParent()
{
RectTransform parent;
switch (ParentMode)
{
case Parenting.TooltipsCanvas:
parent = tooltipsCanvas.GetComponent<RectTransform>();
break;
case Parenting.TooltipTrigger:
if (trigger == null)
{
Debug.Log
(
"[Tooltip] Parenting mode set to 'Tooltip Trigger' but no tooltip trigger is set." +
"Used the TooltipCanvas as parent instead."
);
parent = tooltipsCanvas.GetComponent<RectTransform>();
break;
}
parent = trigger.GetComponent<RectTransform>();
if (parent == null)
{
Debug.Log
(
"[Tooltip] Parenting mode set to 'Tooltip Trigger' but the tooltip trigger has no RectTransform component." +
"Used the TooltipCanvas as parent instead."
);
parent = tooltipsCanvas.GetComponent<RectTransform>();
}
break;
case Parenting.UITag:
if (ParentTag == null)
{
Debug.Log
(
"[Tooltip] Parenting mode set to 'UITag' but no UITag is set." +
"Used the TooltipCanvas as parent instead."
);
parent = tooltipsCanvas.GetComponent<RectTransform>();
break;
}
var uiTag = UITag.GetFirstTag(ParentTag.Category, ParentTag.Name);
if (uiTag == null)
{
Debug.Log
(
"[Tooltip] Parenting mode set to 'UITag' but the UITag is not found." +
"Used the TooltipCanvas as parent instead."
);
parent = tooltipsCanvas.GetComponent<RectTransform>();
break;
}
parent = uiTag.GetComponent<RectTransform>();
if (parent == null)
{
Debug.Log
(
"[Tooltip] Parenting mode set to 'UITag' but the UITag has no RectTransform component." +
"Used the TooltipCanvas as parent instead."
);
parent = tooltipsCanvas.GetComponent<RectTransform>();
}
break;
default:
throw new ArgumentOutOfRangeException();
}
return parent;
}
#endregion
#region Private Methods
/// <summary> Set all the referenced buttons to hide the tooltip is HideOnAnyButton is enabled </summary>
private void ApplyHideOnAnyButton()
{
if (addedHideEventToButtons) return;
if (!hasButtons) return;
if (!HideOnAnyButton) return;
foreach (UIButton button in Buttons)
{
button.onClickBehaviour.Event.AddListener(Hide);
button.onSubmitBehaviour.Event.AddListener(Hide);
}
addedHideEventToButtons = true; //only add the event once
}
/// <summary> Check is the tooltip is moved by a show/hide animation </summary>
private void CheckIfShowOrHideHaveMoveReactions()
{
if (isShowing && !showHasBeenCalled)
{
showHasBeenCalled = true;
showHasMovement = showReactions.Any(r => r.GetType() == typeof(UIMoveReaction) && ((UIMoveReaction)r).rectTransform == rectTransform && ((UIMoveReaction)r).enabled);
if (showHasMovement)
showMoveReaction = showReactions.First(r => r.GetType() == typeof(UIMoveReaction) && ((UIMoveReaction)r).rectTransform == rectTransform) as UIMoveReaction;
return;
}
if (isHiding && !hideHasBeenCalled)
{
hideHasBeenCalled = true;
hideHasMovement = hideReactions.Any(r => r.GetType() == typeof(UIMoveReaction) && ((UIMoveReaction)r).rectTransform == rectTransform && ((UIMoveReaction)r).enabled);
if (hideHasMovement)
hideMoveReaction = hideReactions.First(r => r.GetType() == typeof(UIMoveReaction) && ((UIMoveReaction)r).rectTransform == rectTransform) as UIMoveReaction;
}
}
/// <summary> Update the tooltip position according to the tracking mode </summary>
private void ApplyTracking()
{
Vector3 targetPosition;
switch (TrackingMode)
{
case Tracking.Disabled:
return;
case Tracking.FollowPointer:
targetPosition = pointerPosition;
break;
case Tracking.FollowTrigger:
if (!hasTrigger)
{
TrackingMode = Tracking.Disabled;
return;
}
targetPosition = trigger.transform.position;
break;
case Tracking.FollowTarget:
if (!hasFollowTarget)
{
this.SetFollowTargetFromUITag(FollowTag.Category, FollowTag.Name);
if (!hasFollowTarget)
{
TrackingMode = Tracking.Disabled;
return;
}
UpdateTarget();
}
targetPosition = followTarget.transform.position;
break;
default:
throw new ArgumentOutOfRangeException();
}
transform.position = targetPosition;
}
/// <summary> Update the tooltip position according to the positioning mode </summary>
private void ApplyPositioning()
{
if (transform.parent == null) return; //cannot calculate the position if the tooltip doesn't have a parent
Vector3 calculatedTargetPosition;
switch (TrackingMode)
{
case Tracking.Disabled:
calculatedTargetPosition = CalculatePositioningWhenTrackingIsDisabled();
break;
case Tracking.FollowPointer:
calculatedTargetPosition = CalculatePositioningWhenTrackingIsFollowPointer();
break;
case Tracking.FollowTrigger:
case Tracking.FollowTarget:
calculatedTargetPosition = CalculatePositioningWhenTrackingIsEnabled();
break;
default:
throw new ArgumentOutOfRangeException();
}
// Debug.Log($"[{TrackingMode}] calculatedTargetPosition: {calculatedTargetPosition}");
//apply the position offset (set by the developer)
calculatedTargetPosition += PositionOffset;
//check for NaN values
calculatedTargetPosition.x = float.IsNaN(calculatedTargetPosition.x) ? 0 : calculatedTargetPosition.x;
calculatedTargetPosition.y = float.IsNaN(calculatedTargetPosition.y) ? 0 : calculatedTargetPosition.y;
calculatedTargetPosition.z = float.IsNaN(calculatedTargetPosition.z) ? 0 : calculatedTargetPosition.z;
//check for infinite values
calculatedTargetPosition.x = float.IsInfinity(calculatedTargetPosition.x) ? 0 : calculatedTargetPosition.x;
calculatedTargetPosition.y = float.IsInfinity(calculatedTargetPosition.y) ? 0 : calculatedTargetPosition.y;
calculatedTargetPosition.z = float.IsInfinity(calculatedTargetPosition.z) ? 0 : calculatedTargetPosition.z;
//if the tooltip is showing and the show animation has a move reaction -> update the reaction's To value to the calculated position
if (isShowing & showHasMovement)
{
showMoveReaction.SetTo(calculatedTargetPosition);
return;
}
//if the tooltip is hiding and the hide animation has a move reaction -> update the reaction's From value to the calculated position
if (isHiding & hideHasMovement)
{
hideMoveReaction.SetFrom(calculatedTargetPosition);
return;
}
//apply the calculated anchored position
rectTransform.anchoredPosition3D = calculatedTargetPosition; //update the tooltip anchored position with the calculated position
}
/// <summary> Update the tooltip position to keep it in screen, if KeepInScreen is enabled </summary>
private void ApplyKeepInScreen()
{
if (!KeepInScreen) return;
var cCorners = new Vector3[4];
tooltipRootCanvasRectTransform.GetWorldCorners(cCorners);
Vector3 cBottomLeft = cCorners[0];
Vector3 cTopRight = cCorners[2];
Vector3 cSize = cTopRight - cBottomLeft;
rectTransform.GetWorldCorners(cCorners);
Vector3 tBottomLEft = cCorners[0];
Vector3 tTopRight = cCorners[2];
Vector3 tSize = tTopRight - tBottomLEft;
Vector3 tPosition = rectTransform.position;
Vector3 deltaBottomLeft = tPosition - tBottomLEft;
Vector3 deltaTopRight = tTopRight - tPosition;
tPosition.x = tSize.x < cSize.x
? Mathf.Clamp(tPosition.x, cBottomLeft.x + deltaBottomLeft.x, cTopRight.x - deltaTopRight.x)
: Mathf.Clamp(tPosition.x, cTopRight.x - deltaTopRight.x, cBottomLeft.x + deltaBottomLeft.x);
tPosition.y = tSize.y < cSize.y
? Mathf.Clamp(tPosition.y, cBottomLeft.y + deltaBottomLeft.y, cTopRight.y - deltaTopRight.y)
: Mathf.Clamp(tPosition.y, cTopRight.y - deltaTopRight.y, cBottomLeft.y + deltaBottomLeft.y);
rectTransform.position = tPosition;
}
/// <summary> Apply a maximum width constraint to the tooltip if one is set </summary>
private void ApplyMaximumWidth()
{
if (MaximumWidth <= 0) return; //no maximum width set
layoutElement.preferredWidth = -1; //reset the preferred width to -1 so it can be recalculated
layoutElement.enabled = false; //disable the layout element so it doesn't influence the preferred width
rectTransform.ForceUpdateRectTransforms(); //force the rect transform to update
foreach (TextMeshProUGUI label in Labels) label.ForceMeshUpdate(); //force mesh updates on all labels
float maxLabelWidth = Labels.Max(label => label.preferredWidth); //get the maximum width from all labels
if (maxLabelWidth < MaximumWidth) return; //if the maximum width is smaller than the maximum width set by the user, no need to do anything
layoutElement.enabled = true; //enable the layout element
layoutElement.preferredWidth = MaximumWidth; //set the preferred width to the maximum width constraint
rectTransform.ForceUpdateRectTransforms(); //force an update of the rect transform
}
/// <summary> Internal method used if tracking is disabled to calculate the position according to the positioning mode inside its parent </summary>
/// <returns> The anchoredPosition3D to apply to the tooltip </returns>
private Vector3 CalculatePositioningWhenTrackingIsDisabled()
{
if (transform.parent == null) return rectTransform.anchoredPosition3D; //if the tooltip is not parented, just return the initial anchored position
float z = rectTransform.anchoredPosition3D.z; //save the z value of the anchored position
Rect parentRect = parentRectTransform.rect; //get the parent rect
float parentWidth = parentRect.width; //get the parent width
float parentHeight = parentRect.height; //get the parent height
return PositionMode switch
{
Positioning.TopLeft => new Vector3(-parentWidth / 2f + width * pivotX, parentHeight / 2f - height * pivotY, z),
Positioning.TopCenter => new Vector3(0f, parentHeight / 2f - height * pivotY, z),
Positioning.TopRight => new Vector3(parentWidth / 2f - width * (1 - pivotX), parentHeight / 2f - height * pivotY, z),
Positioning.MiddleLeft => new Vector3(-parentWidth / 2f + width * pivotX, 0f, z),
Positioning.MiddleCenter => new Vector3(0f, 0f, z),
Positioning.MiddleRight => new Vector3(parentWidth / 2f - width * (1 - pivotX), 0f, z),
Positioning.BottomLeft => new Vector3(-parentWidth / 2f + width * pivotX, -parentHeight / 2f + height * (1 - pivotY), z),
Positioning.BottomCenter => new Vector3(0f, -parentHeight / 2f + height * (1 - pivotY), z),
Positioning.BottomRight => new Vector3(parentWidth / 2f - width * (1 - pivotX), -parentHeight / 2f + height * (1 - pivotY), z),
_ => throw new ArgumentOutOfRangeException()
};
}
/// <summary> Internal method used to calculate the position of the tooltip according to the positioning mode and the current pointer position </summary>
/// <returns> The anchoredPosition3D to apply to the tooltip </returns>
private Vector3 CalculatePositioningWhenTrackingIsFollowPointer()
{
Vector2 point;
const float z = 0;
switch (tooltipRootCanvas.renderMode)
{
case RenderMode.ScreenSpaceOverlay:
point = parentRectTransform.InverseTransformPoint(pointerPosition);
break;
case RenderMode.ScreenSpaceCamera:
case RenderMode.WorldSpace:
RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRectTransform, pointerPosition, tooltipRootCanvas.worldCamera, out point);
break;
default:
throw new ArgumentOutOfRangeException();
}
point -= parentRectTransform.rect.center;
//adjust the point according to the positioning mode and the tooltip size and pivot
point = PositionMode switch
{
Positioning.TopLeft => new Vector3(point.x - width * pivotX, point.y + height * pivotY, z),
Positioning.TopCenter => new Vector3(point.x, point.y + height * pivotY, z),
Positioning.TopRight => new Vector3(point.x + width * (1 - pivotX), point.y + height * pivotY, z),
Positioning.MiddleLeft => new Vector3(point.x - width * pivotX, point.y, z),
Positioning.MiddleCenter => new Vector3(point.x, point.y, z),
Positioning.MiddleRight => new Vector3(point.x + width * (1 - pivotX), point.y, z),
Positioning.BottomLeft => new Vector3(point.x - width * pivotX, point.y - height * (1 - pivotY), z),
Positioning.BottomCenter => new Vector3(point.x, point.y - height * (1 - pivotY), z),
Positioning.BottomRight => new Vector3(point.x + width * (1 - pivotX), point.y - height * (1 - pivotY), z),
_ => throw new ArgumentOutOfRangeException()
};
return point;
}
internal bool updateTarget { get; set; }
/// <summary> Internal method used to calculate the position of the tooltip according to the positioning mode inside its parent </summary>
/// <returns> The anchoredPosition3D to apply to the tooltip </returns>
private Vector3 CalculatePositioningWhenTrackingIsEnabled()
{
if (updateTarget) UpdateTarget();
Vector3 point = parentRectTransform.InverseTransformPoint(targetRectTransform.position);
//calculate the target offset according to the positioning mode
Vector3 targetOffset = GetPositionOffset(targetRectTransform, PositionMode);
//fix the target offset value if the rectTransform has a scale applied
Vector3 localScale = targetRectTransform.localScale;
targetOffset.x *= localScale.x;
targetOffset.y *= localScale.y;
//calculate the scale difference between the tooltip root canvas and the target root canvas
Vector3 targetCanvasScale = targetRectTransform.lossyScale;
Vector3 tooltipCanvasScale = rectTransform.lossyScale;
var scaleDiff = new Vector3(targetCanvasScale.x / tooltipCanvasScale.x, targetCanvasScale.y / tooltipCanvasScale.y, targetCanvasScale.z / tooltipCanvasScale.z);
//apply the scale difference to the target offset for better positioning
targetOffset.x *= scaleDiff.x;
targetOffset.y *= scaleDiff.y;
targetOffset.z *= scaleDiff.z;
//apply the calculated target offset
point += targetOffset;
//calculate the tooltip offset according to the positioning mode
Vector3 tooltipOffset = GetPositionOffset(rectTransform, PositionMode);
//apply the calculated tooltip offset`
point += tooltipOffset;
//return the calculated anchored position
return point;
}
private static Vector3 GetPositionOffset(RectTransform rectTransform, Positioning positionMode)
{
Rect rect = rectTransform.rect;
return positionMode switch
{
Positioning.TopLeft => new Vector3(rect.xMin, rect.yMax, 0),
Positioning.TopCenter => new Vector3(rect.center.x, rect.yMax, 0),
Positioning.TopRight => new Vector3(rect.xMax, rect.yMax, 0),
Positioning.MiddleLeft => new Vector3(rect.xMin, rect.center.y, 0),
Positioning.MiddleCenter => new Vector3(rect.center.x, rect.center.y, 0),
Positioning.MiddleRight => new Vector3(rect.xMax, rect.center.y, 0),
Positioning.BottomLeft => new Vector3(rect.xMin, rect.yMin, 0),
Positioning.BottomCenter => new Vector3(rect.center.x, rect.yMin, 0),
Positioning.BottomRight => new Vector3(rect.xMax, rect.yMin, 0),
_ => throw new ArgumentOutOfRangeException()
};
}
#endregion
#region Static Methods
/// <summary>
/// Instantiate a new tooltip from the prefab that is registered in the database.
/// If a prefab with the given tooltip name is not found, null will be returned.
/// </summary>
/// <param name="tooltipName"> The name of the tooltip prefab in the database </param>
/// <returns> The tooltip instance. Null if the prefab is not found. </returns>
public static UITooltip Get(string tooltipName)
{
if (string.IsNullOrEmpty(tooltipName)) return null;
GameObject prefab = UITooltipDatabase.instance.GetPrefab(tooltipName);
if (prefab == null)
{
Debug.LogWarning($"UITooltip.Get({tooltipName}) - prefab not found in the database");
return null;
}
UITooltip tooltip =
Instantiate(prefab)
.GetComponent<UITooltip>()
.Reset();
//destroy the tooltip when it is hidden
tooltip.OnHiddenCallback.Event.AddListener(() =>
{
//sanity check to make sure the tooltip is not already destroyed
if (tooltip == null) return;
Destroy(tooltip.gameObject);
tooltip = null;
});
return tooltip;
}
/// <summary> Calls ShowTooltip on all UITooltipTriggers in the scene (that are active and enabled) </summary>
public static void ShowAllTooltips()
{
foreach (UITooltipTrigger tooltipTrigger in UITooltipTrigger.database)
{
if (!tooltipTrigger.isActiveAndEnabled) return;
tooltipTrigger.ShowTooltip();
}
}
/// <summary> Calls Hide on all UITooltips in the scene (that are active and enabled) </summary>
public void HideAllTooltips()
{
foreach (UITooltip tooltip in database)
{
if (!tooltip.isActiveAndEnabled) return;
tooltip.Hide();
}
}
#endregion
}
public static class UITooltipExtensions
{
/// <summary> Reset the tooltip to its initial state </summary>
/// <param name="tooltip"> Target tooltip </param>
/// <returns> The tooltip instance </returns>
public static T Reset<T>(this T tooltip) where T : UITooltip
{
tooltip.updateTarget = true;
tooltip.tooltipRootCanvas = null;
tooltip.targetRectTransform = null;
tooltip.parentRectTransform = null;
tooltip.trigger = null;
tooltip.followTarget = null;
return tooltip;
}
/// <summary> Reparent the tooltip to a new parent </summary>
/// <param name="tooltip"> Target tooltip </param>
/// <param name="parent"> The new parent </param>
/// <returns> The tooltip instance </returns>
public static T SetParent<T>(this T tooltip, RectTransform parent) where T : UITooltip
{
tooltip.tooltipRootCanvas = null;
tooltip.parentRectTransform = parent;
if (parent == null) return tooltip;
tooltip.rectTransform.SetParent(parent, true);
tooltip.rectTransform.CenterPivot();
tooltip.rectTransform.localScale = Vector3.one;
tooltip.rectTransform.anchoredPosition3D = tooltip.CustomStartPosition;
tooltip.tooltipRootCanvas = parent.GetComponentInParent<Canvas>().rootCanvas;
return tooltip;
}
/// <summary> Set the follow target for when the tooltip is visible and tracking is enabled to follow a target </summary>
/// <param name="tooltip"> Target tooltip </param>
/// <param name="target"> Target trigger </param>
/// <returns> The tooltip instance </returns>
public static T SetTrigger<T>(this T tooltip, UITooltipTrigger target) where T : UITooltip
{
tooltip.trigger = target;
return tooltip;
}
/// <summary> Set the follow target for when the tooltip is visible and tracking is enabled to follow a target </summary>
/// <param name="tooltip"> Target tooltip </param>
/// <param name="target"> Target game object </param>
/// <returns> The tooltip instance </returns>
public static T SetFollowTarget<T>(this T tooltip, GameObject target) where T : UITooltip
{
tooltip.followTarget = target;
return tooltip;
}
/// <summary> Set the follow target for when the tooltip is visible and tracking is enabled to follow a target </summary>
/// <param name="tooltip"> Target tooltip </param>
/// <param name="category"> Category of the tag </param>
/// <param name="name"> Name of the tag </param>
/// <returns> The tooltip instance </returns>
public static T SetFollowTargetFromUITag<T>(this T tooltip, string category, string name) where T : UITooltip
{
tooltip.followTarget = null;
UITag tag = UITag.GetTags(category, name).FirstOrDefault();
if (tag != null) tooltip.followTarget = tag.gameObject;
return tooltip;
}
/// <summary> Update the keep in screen setting </summary>
/// <param name="tooltip"> Target tooltip </param>
/// <param name="keepInScreen"> TRUE to keep the tooltip in screen at all times, while it is visible </param>
/// <returns> The tooltip instance </returns>
public static T SetKeepInScreen<T>(this T tooltip, bool keepInScreen) where T : UITooltip
{
tooltip.KeepInScreen = keepInScreen;
return tooltip;
}
/// <summary> Update the override sorting order setting </summary>
/// <param name="target"> Target tooltip </param>
/// <param name="overrideSortingOrder"> New override sorting order value </param>
/// <param name="apply"> TRUE to apply the new value, FALSE to only update the setting </param>
/// <returns> The tooltip instance </returns>
public static T SetOverrideSorting<T>(this T target, bool overrideSortingOrder, bool apply = false) where T : UITooltip
{
target.OverrideSorting = overrideSortingOrder;
if (apply) target.ApplyOverrideSorting();
return target;
}
/// <summary> Apply the override sorting order setting (if enabled) </summary>
/// <param name="target"> Target tooltip </param>
/// <returns> The tooltip instance </returns>
public static T ApplyOverrideSorting<T>(this T target) where T : UITooltip
{
if (!target.OverrideSorting)
return target;
target.canvas.overrideSorting = true;
target.canvas.sortingOrder = UITooltip.k_MaxSortingOrder;
if (!target.canvas.gameObject.activeInHierarchy)
Debug.Log($"Cannot apply override sorting order to tooltip {target.name} because it is not active in the scene");
if (!target.canvas.enabled)
Debug.Log($"Cannot apply override sorting order to tooltip {target.name} because its canvas is not enabled");
return target;
}
/// <summary>
/// Set the text values for all the text mesh pro labels this tooltip has references to.
/// <para/> Each string value will be set to the TextMeshProUI label with the same index in the Labels list.
/// </summary>
/// <param name="tooltip"> Target tooltip </param>
/// <param name="texts"> Text values to set </param>
/// <returns> The tooltip instance </returns>
public static T SetTexts<T>(this T tooltip, params string[] texts) where T : UITooltip
{
int textsCount = texts.Length;
if (textsCount == 0) return tooltip;
for (int i = 0; i < tooltip.Labels.Count; i++)
{
TextMeshProUGUI label = tooltip.Labels[i];
if (label == null) continue;
label.SetText(i < textsCount ? texts[i] : string.Empty);
label.ForceMeshUpdate();
}
return tooltip;
}
/// <summary>
/// Set the sprite references for all the Images this tooltip has references to.
/// <para/> Each Sprite will be referenced to the Image with the same index in the Images list.
/// </summary>
/// <param name="tooltip"> Target tooltip </param>
/// <param name="sprites"> Sprite references to set </param>
/// <returns> The tooltip instance </returns>
public static T SetSprites<T>(this T tooltip, params Sprite[] sprites) where T : UITooltip
{
int spritesCount = sprites.Length;
if (spritesCount == 0) return tooltip;
for (int i = 0; i < tooltip.Images.Count; i++)
{
Image image = tooltip.Images[i];
if (image == null) continue;
image.sprite = i < spritesCount ? sprites[i] : null;
}
return tooltip;
}
/// <summary>
/// Set the UnityEvents to invoke for all the UIButtons this tooltip has references to
/// <para/> Each UnityEvent will be assigned to the UIButton with the same index in the Buttons list.
/// <para/> The UnityEvent will be invoked when the UIButton's either on click or submit behaviour is invoked.
/// </summary>
/// <param name="tooltip"> Target tooltip </param>
/// <param name="events"> UnityEvents to invoke </param>
/// <returns> The tooltip instance </returns>
public static T SetEvents<T>(this T tooltip, params UnityEvent[] events) where T : UITooltip
{
int eventsCount = events.Length;
if (eventsCount == 0) return tooltip;
bool hasGraphicRaycaster = tooltip.GetComponentInChildren<GraphicRaycaster>();
for (int i = 0; i < tooltip.Buttons.Count; i++)
{
UIButton button = tooltip.Buttons[i]; //get the button
if (button == null) continue; //if the button is null, continue
if (!hasGraphicRaycaster) //if the tooltip doesn't have a graphic raycaster, check if the button has one
if (!button.GetComponent<GraphicRaycaster>()) //if the button doesn't have a graphic raycaster
button.gameObject.AddComponent<GraphicRaycaster>(); //add a graphic raycaster to the button
if (eventsCount <= i) continue; //if the event count is less than the index, continue
UnityEvent evt = events[i]; //get the event
if (evt == null) continue; //if the event is null, continue
button.onClickBehaviour.Event.AddListener(evt.Invoke); //add the event to the button on click behaviour
button.onSubmitBehaviour.Event.AddListener(evt.Invoke); //add the event to the button on submit behaviour
}
return tooltip;
}
/// <summary>
/// Set the UnityActions to invoke for all the UIButtons this tooltip has references to.
/// <para/> Each UnityAction will be assigned to the UIButton with the same index in the Buttons list.
/// <para/> The UnityAction will be invoked when the UIButton's either on click or submit behaviour is invoked.
/// </summary>
/// <param name="tooltip"> Target tooltip </param>
/// <param name="actions"> UnityActions to invoke </param>
/// <returns> The tooltip instance </returns>
public static T SetEvents<T>(this T tooltip, params UnityAction[] actions) where T : UITooltip
{
int actionsCount = actions.Length;
if (actionsCount == 0) return tooltip;
bool hasGraphicRaycaster = tooltip.GetComponentInChildren<GraphicRaycaster>();
for (int i = 0; i < tooltip.Buttons.Count; i++)
{
UIButton button = tooltip.Buttons[i]; //get the button
if (button == null) continue; //if the button is null, continue
if (!hasGraphicRaycaster) //if the tooltip doesn't have a graphic raycaster, check if the button has one
if (!button.GetComponent<GraphicRaycaster>()) //if the button doesn't have a graphic raycaster
button.gameObject.AddComponent<GraphicRaycaster>(); //add a graphic raycaster to the button
if (actionsCount <= i) continue; //if the event count is less than the index, continue
UnityAction action = actions[i]; //get the action
if (action == null) continue; //if the action is null, continue
button.onClickBehaviour.Event.AddListener(action.Invoke); //add the action to the button on click behaviour
button.onSubmitBehaviour.Event.AddListener(action.Invoke); //add the action to the button on submit behaviour
}
return tooltip;
}
}
}