656 lines
22 KiB
C#
656 lines
22 KiB
C#
//
|
|
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
//
|
|
using Microsoft.MixedReality.Toolkit.Utilities;
|
|
using System.Collections.Generic;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
|
|
namespace Microsoft.MixedReality.Toolkit.UI
|
|
{
|
|
/// <summary>
|
|
/// Class for Tooltip object
|
|
/// Creates a floating tooltip that is attached to an object and moves to stay in view as object rotates with respect to the view.
|
|
/// </summary>
|
|
[ExecuteAlways]
|
|
[HelpURL("https://docs.microsoft.com/windows/mixed-reality/mrtk-unity/features/ux-building-blocks/tooltip")]
|
|
[AddComponentMenu("Scripts/MRTK/SDK/ToolTip")]
|
|
public class ToolTip : MonoBehaviour
|
|
{
|
|
[SerializeField]
|
|
[Tooltip("Show the opaque background of tooltip.")]
|
|
private bool showBackground = true;
|
|
|
|
/// <summary>
|
|
/// Show the opaque background of tooltip.
|
|
/// </summary>
|
|
public bool ShowBackground
|
|
{
|
|
get { return showBackground; }
|
|
set { showBackground = value; }
|
|
}
|
|
|
|
[SerializeField]
|
|
private bool showHighlight = false;
|
|
|
|
/// <summary>
|
|
/// Shows white trim around edge of tooltip.
|
|
/// </summary>
|
|
public bool ShowHighlight
|
|
{
|
|
get
|
|
{
|
|
return showHighlight;
|
|
}
|
|
set
|
|
{
|
|
showHighlight = value;
|
|
}
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("Show the connecting stem between the tooltip and its parent GameObject.")]
|
|
private bool showConnector = true;
|
|
|
|
/// <summary>
|
|
/// Show the connecting stem between the tooltip and its parent GameObject.
|
|
/// </summary>
|
|
public bool ShowConnector
|
|
{
|
|
get { return showConnector; }
|
|
set { showConnector = value; }
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("Display the state of the tooltip.")]
|
|
private DisplayMode tipState = DisplayMode.On;
|
|
|
|
/// <summary>
|
|
/// The display the state of the tooltip.
|
|
/// </summary>
|
|
public DisplayMode TipState
|
|
{
|
|
get { return tipState; }
|
|
set { tipState = value; }
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("Display the state of a group of tooltips.")]
|
|
private DisplayMode groupTipState;
|
|
|
|
/// <summary>
|
|
/// Display the state of a group of tooltips.
|
|
/// </summary>
|
|
public DisplayMode GroupTipState
|
|
{
|
|
set { groupTipState = value; }
|
|
get { return groupTipState; }
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("Display the state of the master tooltip.")]
|
|
private DisplayMode masterTipState;
|
|
|
|
/// <summary>
|
|
/// Display the state of the master tooltip.
|
|
/// </summary>
|
|
public DisplayMode MasterTipState
|
|
{
|
|
set { masterTipState = value; }
|
|
get { return masterTipState; }
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("GameObject that the line and text are attached to")]
|
|
private GameObject anchor;
|
|
/// <summary>
|
|
/// getter/setter for ameObject that the line and text are attached to
|
|
/// </summary>
|
|
public GameObject Anchor
|
|
{
|
|
get { return anchor; }
|
|
set { anchor = value; }
|
|
}
|
|
|
|
[Tooltip("Pivot point that text will rotate around as well as the point where the Line will be rendered to.")]
|
|
[SerializeField]
|
|
private GameObject pivot;
|
|
|
|
/// <summary>
|
|
/// Pivot point that text will rotate around as well as the point where the Line will be rendered to.
|
|
/// </summary>
|
|
public GameObject Pivot => pivot;
|
|
|
|
[SerializeField]
|
|
[Tooltip("GameObject text that is displayed on the tooltip.")]
|
|
private GameObject label;
|
|
|
|
[SerializeField]
|
|
[Tooltip("Parent of the Text and Background")]
|
|
private GameObject contentParent;
|
|
|
|
[TextArea]
|
|
[SerializeField]
|
|
[Tooltip("Text for the ToolTip to display")]
|
|
private string toolTipText;
|
|
|
|
/// <summary>
|
|
/// Text for the ToolTip to display
|
|
/// </summary>
|
|
public string ToolTipText
|
|
{
|
|
set
|
|
{
|
|
toolTipText = value;
|
|
if (!Application.isPlaying)
|
|
{ // Only force refresh in edit mode
|
|
RefreshLocalContent();
|
|
}
|
|
}
|
|
get { return toolTipText; }
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("The padding around the content (height / width)")]
|
|
private Vector2 backgroundPadding = Vector2.zero;
|
|
|
|
[SerializeField]
|
|
[Tooltip("The offset of the background (x / y / z)")]
|
|
private Vector3 backgroundOffset = Vector3.zero;
|
|
|
|
/// <summary>
|
|
/// The offset of the background (x / y / z)
|
|
/// </summary>
|
|
public Vector3 LocalContentOffset => backgroundOffset;
|
|
|
|
[SerializeField]
|
|
[Range(0.01f, 3f)]
|
|
[Tooltip("The scale of all the content (label, backgrounds, etc.)")]
|
|
private float contentScale = 1f;
|
|
|
|
/// <summary>
|
|
/// The scale of all the content (label, backgrounds, etc.)
|
|
/// </summary>
|
|
public float ContentScale
|
|
{
|
|
get { return contentScale; }
|
|
set
|
|
{
|
|
contentScale = value;
|
|
if (!Application.isPlaying)
|
|
{ // Only force refresh in edit mode
|
|
RefreshLocalContent();
|
|
}
|
|
}
|
|
}
|
|
|
|
[SerializeField]
|
|
[Range(10, 60)]
|
|
[Tooltip("The font size of the tooltip.")]
|
|
private int fontSize = 30;
|
|
|
|
/// <summary>
|
|
/// The font size of the tooltip.
|
|
/// </summary>
|
|
public int FontSize
|
|
{
|
|
get { return fontSize; }
|
|
set
|
|
{
|
|
fontSize = value;
|
|
if (!Application.isPlaying)
|
|
{ // Only force refresh in edit mode
|
|
RefreshLocalContent();
|
|
}
|
|
}
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("Determines where the line will attach to the tooltip content.")]
|
|
private ToolTipAttachPoint attachPointType = ToolTipAttachPoint.Closest;
|
|
|
|
public ToolTipAttachPoint PivotType
|
|
{
|
|
get
|
|
{
|
|
return attachPointType;
|
|
}
|
|
set
|
|
{
|
|
attachPointType = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// point where ToolTip is attached
|
|
/// </summary>
|
|
public Vector3 AttachPointPosition
|
|
{
|
|
get { return attachPointPosition; }
|
|
set
|
|
{
|
|
// apply the difference to the offset
|
|
attachPointOffset = value - contentParent.transform.TransformPoint(localAttachPoint);
|
|
}
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("Added as an offset to the pivot position. Modifying AttachPointPosition directly changes this value.")]
|
|
private Vector3 attachPointOffset;
|
|
|
|
[SerializeField]
|
|
[Tooltip("The line connecting the anchor to the pivot. If present, this component will be updated automatically.\n\nRecommended: SimpleLine, Spline, and ParabolaConstrainted")]
|
|
private BaseMixedRealityLineDataProvider toolTipLine;
|
|
|
|
private Vector2 localContentSize;
|
|
|
|
/// <summary>
|
|
/// getter/setter for size of tooltip.
|
|
/// </summary>
|
|
public Vector2 LocalContentSize => localContentSize;
|
|
|
|
private Vector3 pivotPosition;
|
|
private Vector3 attachPointPosition;
|
|
private Vector3 anchorPosition;
|
|
private Vector3 localAttachPoint;
|
|
private Vector3[] localAttachPointPositions;
|
|
private List<IToolTipBackground> backgrounds = new List<IToolTipBackground>();
|
|
private List<IToolTipHighlight> highlights = new List<IToolTipHighlight>();
|
|
private TextMeshPro cachedLabelText;
|
|
private int prevTextLength = -1;
|
|
private int prevTextHash = -1;
|
|
private int prevFontSize = -1;
|
|
|
|
/// <summary>
|
|
/// point about which ToolTip pivots to face camera
|
|
/// </summary>
|
|
public Vector3 PivotPosition
|
|
{
|
|
get { return pivotPosition; }
|
|
set
|
|
{
|
|
pivotPosition = value;
|
|
pivot.transform.position = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// point where ToolTip connector is attached
|
|
/// </summary>
|
|
public Vector3 AnchorPosition
|
|
{
|
|
get { return anchorPosition; }
|
|
set { anchor.transform.position = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Transform of object to which ToolTip is attached
|
|
/// </summary>
|
|
public Transform ContentParentTransform => contentParent.transform;
|
|
|
|
/// <summary>
|
|
/// is ToolTip active and displaying
|
|
/// </summary>
|
|
public bool IsOn
|
|
{
|
|
get
|
|
{
|
|
return ResolveTipState(masterTipState, groupTipState, tipState, HasFocus);
|
|
}
|
|
}
|
|
|
|
public static bool ResolveTipState(DisplayMode masterTipState, DisplayMode groupTipState, DisplayMode tipState, bool hasFocus)
|
|
{
|
|
switch (masterTipState)
|
|
{
|
|
case DisplayMode.None:
|
|
default:
|
|
// Use our group state
|
|
switch (groupTipState)
|
|
{
|
|
case DisplayMode.None:
|
|
default:
|
|
// Use our local State
|
|
switch (tipState)
|
|
{
|
|
case DisplayMode.None:
|
|
case DisplayMode.Off:
|
|
default:
|
|
return false;
|
|
|
|
case DisplayMode.On:
|
|
return true;
|
|
|
|
case DisplayMode.OnFocus:
|
|
return hasFocus;
|
|
}
|
|
|
|
case DisplayMode.On:
|
|
return true;
|
|
|
|
case DisplayMode.Off:
|
|
return false;
|
|
|
|
case DisplayMode.OnFocus:
|
|
return hasFocus;
|
|
}
|
|
|
|
case DisplayMode.On:
|
|
return true;
|
|
|
|
case DisplayMode.Off:
|
|
return false;
|
|
|
|
case DisplayMode.OnFocus:
|
|
return hasFocus;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// does the ToolTip have focus.
|
|
/// </summary>
|
|
public virtual bool HasFocus
|
|
{
|
|
get
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// virtual functions
|
|
/// </summary>
|
|
protected virtual void OnEnable()
|
|
{
|
|
ValidateHeirarchy();
|
|
|
|
label.EnsureComponent<TextMeshPro>();
|
|
gameObject.EnsureComponent<ToolTipConnector>();
|
|
|
|
// Get our line if it exists
|
|
if (toolTipLine == null)
|
|
toolTipLine = gameObject.GetComponent<BaseMixedRealityLineDataProvider>();
|
|
|
|
// Make sure the tool tip text isn't empty
|
|
if (string.IsNullOrEmpty(toolTipText))
|
|
toolTipText = " ";
|
|
|
|
backgrounds.Clear();
|
|
foreach (IToolTipBackground background in GetComponents<IToolTipBackground>())
|
|
{
|
|
backgrounds.Add(background);
|
|
}
|
|
|
|
highlights.Clear();
|
|
foreach (IToolTipHighlight highlight in GetComponents<IToolTipHighlight>())
|
|
{
|
|
highlights.Add(highlight);
|
|
}
|
|
|
|
contentParent.SetActive(false);
|
|
ShowBackground = showBackground;
|
|
ShowHighlight = showHighlight;
|
|
ShowConnector = showConnector;
|
|
}
|
|
|
|
protected virtual void Update()
|
|
{
|
|
// Cache our pivot / anchor / attach point positions
|
|
pivotPosition = pivot.transform.position;
|
|
anchorPosition = anchor.transform.position;
|
|
attachPointPosition = contentParent.transform.TransformPoint(localAttachPoint) + attachPointOffset;
|
|
|
|
// Enable / disable our line if it exists
|
|
if (toolTipLine != null)
|
|
{
|
|
toolTipLine.enabled = showConnector;
|
|
|
|
if (!(toolTipLine is ParabolaConstrainedLineDataProvider))
|
|
{
|
|
toolTipLine.FirstPoint = AnchorPosition;
|
|
}
|
|
|
|
toolTipLine.LastPoint = AttachPointPosition;
|
|
}
|
|
|
|
if (IsOn)
|
|
{
|
|
contentParent.SetActive(true);
|
|
localAttachPoint = ToolTipUtility.FindClosestAttachPointToAnchor(anchor.transform, contentParent.transform, localAttachPointPositions, PivotType);
|
|
}
|
|
else
|
|
{
|
|
contentParent.SetActive(false);
|
|
}
|
|
|
|
RefreshLocalContent();
|
|
}
|
|
|
|
protected virtual void RefreshLocalContent()
|
|
{
|
|
// Set the scale of the pivot
|
|
contentParent.transform.localScale = Vector3.one * contentScale;
|
|
label.transform.localScale = Vector3.one * 0.005f;
|
|
// Set the content using a text mesh by default
|
|
// This function can be overridden for tooltips that use Unity UI
|
|
|
|
// Has content or fontSize changed?
|
|
int currentTextLength = toolTipText.Length;
|
|
int currentTextHash = toolTipText.GetHashCode();
|
|
int currentFontSize = fontSize;
|
|
|
|
// If it has, update the content
|
|
if (currentTextLength != prevTextLength || currentTextHash != prevTextHash || currentFontSize != prevFontSize)
|
|
{
|
|
prevTextHash = currentTextHash;
|
|
prevTextLength = currentTextLength;
|
|
prevFontSize = currentFontSize;
|
|
|
|
if (cachedLabelText == null)
|
|
cachedLabelText = label.GetComponent<TextMeshPro>();
|
|
|
|
if (cachedLabelText != null && !string.IsNullOrEmpty(toolTipText))
|
|
{
|
|
cachedLabelText.fontSize = fontSize;
|
|
cachedLabelText.text = toolTipText.Trim();
|
|
// Force text mesh to use center alignment
|
|
cachedLabelText.alignment = TextAlignmentOptions.CenterGeoAligned;
|
|
// Update text so we get an accurate scale
|
|
cachedLabelText.ForceMeshUpdate();
|
|
// Get the world scale of the text
|
|
// Convert that to local scale using the content parent
|
|
Vector3 localScale = Vector3.Scale(cachedLabelText.transform.lossyScale / contentScale, cachedLabelText.textBounds.size);
|
|
localContentSize.x = localScale.x + backgroundPadding.x;
|
|
localContentSize.y = localScale.y + backgroundPadding.y;
|
|
}
|
|
|
|
// Now that we have the size of our content, get our pivots
|
|
ToolTipUtility.GetAttachPointPositions(ref localAttachPointPositions, localContentSize);
|
|
localAttachPoint = ToolTipUtility.FindClosestAttachPointToAnchor(anchor.transform, contentParent.transform, localAttachPointPositions, PivotType);
|
|
|
|
foreach (IToolTipBackground background in backgrounds)
|
|
{
|
|
background.OnContentChange(localContentSize, LocalContentOffset, contentParent.transform);
|
|
}
|
|
}
|
|
|
|
foreach (IToolTipBackground background in backgrounds)
|
|
{
|
|
background.IsVisible = showBackground;
|
|
}
|
|
|
|
foreach (IToolTipHighlight highlight in highlights)
|
|
{
|
|
highlight.ShowHighlight = ShowHighlight;
|
|
}
|
|
}
|
|
|
|
protected virtual bool EnforceHierarchy()
|
|
{
|
|
Transform pivotTransform = transform.Find("Pivot");
|
|
Transform anchorTransform = transform.Find("Anchor");
|
|
|
|
if (pivotTransform == null || anchorTransform == null)
|
|
{
|
|
if (Application.isPlaying)
|
|
{
|
|
Debug.LogError("Found error in hierarchy, disabling.");
|
|
enabled = false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
Transform contentParentTransform = pivotTransform.Find("ContentParent");
|
|
|
|
if (contentParentTransform == null)
|
|
{
|
|
if (Application.isPlaying)
|
|
{
|
|
Debug.LogError("Found error in hierarchy, disabling.");
|
|
enabled = false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
Transform labelTransform = contentParentTransform.Find("Label");
|
|
if (labelTransform == null)
|
|
{
|
|
if (Application.isPlaying)
|
|
{
|
|
Debug.LogError("Found error in hierarchy, disabling.");
|
|
enabled = false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
contentParentTransform.localPosition = Vector3.zero;
|
|
contentParentTransform.localRotation = Quaternion.identity;
|
|
contentParentTransform.localScale = Vector3.one * contentScale;
|
|
labelTransform.localPosition = Vector3.zero;
|
|
labelTransform.localScale = Vector3.one * 0.025f;
|
|
labelTransform.localRotation = Quaternion.identity;
|
|
pivotTransform.localScale = Vector3.one;
|
|
|
|
pivot = pivotTransform.gameObject;
|
|
anchor = anchorTransform.gameObject;
|
|
contentParent = contentParentTransform.gameObject;
|
|
label = labelTransform.gameObject;
|
|
|
|
return true;
|
|
}
|
|
|
|
public static Vector3 GetTextMeshLocalScale(TextMesh textMesh)
|
|
{
|
|
Vector3 localScale = Vector3.zero;
|
|
|
|
if (string.IsNullOrEmpty(textMesh.text))
|
|
return localScale;
|
|
|
|
string[] splitStrings = textMesh.text.Split(new string[] { System.Environment.NewLine, "\n" }, System.StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
// Calculate the width of the text using character info
|
|
float widestLine = 0f;
|
|
foreach (string splitString in splitStrings)
|
|
{
|
|
float lineWidth = 0f;
|
|
foreach (char symbol in splitString)
|
|
{
|
|
CharacterInfo info;
|
|
if (textMesh.font.GetCharacterInfo(symbol, out info, textMesh.fontSize, textMesh.fontStyle))
|
|
{
|
|
lineWidth += info.advance;
|
|
}
|
|
}
|
|
if (lineWidth > widestLine)
|
|
widestLine = lineWidth;
|
|
}
|
|
localScale.x = widestLine;
|
|
|
|
// Use this to multiply the character size
|
|
Vector3 transformScale = textMesh.transform.localScale;
|
|
localScale.x = (localScale.x * textMesh.characterSize * 0.1f) * transformScale.x;
|
|
localScale.z = transformScale.z;
|
|
|
|
// We could calculate the height based on line height and character size
|
|
// But I've found that method can be flaky and has a lot of magic numbers
|
|
// that may break in future Unity versions
|
|
Vector3 eulerAngles = textMesh.transform.eulerAngles;
|
|
Vector3 rendererScale = Vector3.zero;
|
|
textMesh.transform.rotation = Quaternion.identity;
|
|
rendererScale = textMesh.GetComponent<MeshRenderer>().bounds.size;
|
|
textMesh.transform.eulerAngles = eulerAngles;
|
|
localScale.y = textMesh.transform.worldToLocalMatrix.MultiplyVector(rendererScale).y * transformScale.y;
|
|
|
|
return localScale;
|
|
}
|
|
|
|
private void ValidateHeirarchy()
|
|
{
|
|
// Generate default objects if we haven't set up our tooltip yet
|
|
if (anchor == null)
|
|
{
|
|
Transform anchorTransform = transform.Find("Anchor");
|
|
if (anchorTransform == null)
|
|
{
|
|
anchorTransform = new GameObject("Anchor").transform;
|
|
anchorTransform.SetParent(transform);
|
|
anchorTransform.localPosition = Vector3.zero;
|
|
}
|
|
anchor = anchorTransform.gameObject;
|
|
}
|
|
|
|
if (anchor.transform.parent != transform)
|
|
anchor.transform.SetParent(transform);
|
|
|
|
if (pivot == null)
|
|
{
|
|
Transform pivotTransform = transform.Find("Pivot");
|
|
if (pivotTransform == null)
|
|
{
|
|
pivotTransform = new GameObject("Pivot").transform;
|
|
pivotTransform.SetParent(transform);
|
|
pivotTransform.localPosition = Vector3.up;
|
|
}
|
|
pivot = pivotTransform.gameObject;
|
|
}
|
|
|
|
if (pivot.transform.parent != transform)
|
|
pivot.transform.SetParent(transform, true);
|
|
|
|
if (contentParent == null)
|
|
{
|
|
Transform contentParentTransform = pivot.transform.Find("ContentParent");
|
|
if (contentParentTransform == null)
|
|
{
|
|
contentParentTransform = new GameObject("ContentParent").transform;
|
|
contentParentTransform.SetParent(pivot.transform);
|
|
contentParentTransform.localPosition = Vector3.zero;
|
|
}
|
|
contentParent = contentParentTransform.gameObject;
|
|
}
|
|
|
|
if (contentParent.transform.parent != pivot.transform)
|
|
contentParent.transform.SetParent(pivot.transform, true);
|
|
|
|
if (label == null)
|
|
{
|
|
Transform labelTransform = contentParent.transform.Find("Label");
|
|
if (labelTransform == null)
|
|
{
|
|
labelTransform = new GameObject("Label").transform;
|
|
labelTransform.SetParent(contentParent.transform);
|
|
labelTransform.localScale = Vector3.one * 0.005f;
|
|
labelTransform.localPosition = Vector3.zero;
|
|
}
|
|
label = labelTransform.gameObject;
|
|
}
|
|
|
|
if (label.transform.parent != contentParent.transform)
|
|
label.transform.SetParent(contentParent.transform.parent, true);
|
|
}
|
|
}
|
|
}
|