Inspector Tooltips

Lately I’ve been working on a project with several artists on a game, and they don’t always know what I meant by some of my variable names–even if I name it very specifically. I saw the new Property Attributes in Unity 4 (we just switched about a month ago) and I instantly thought about tooltips in inspector windows. So yesterday evening, I threw together two scripts that did just that. The first was a simple Tooltip script that inherited from PropertyAttribute, and then I got to work on the drawer class. After writing about 10 or so if statements, I got most of the inspector tooltips working.

There are a few, however, that I couldn’t get working. Namely enumerations, generics/arrays, and more advanced inspectors like gradient and Vector types. I know there is an EditorGUI.PropertyField or something like that, but everytime I try to use this with say, a generic, Unity throws a Stack Overflow exception and the like and the entire inspector disappears. Could someone help me get these working? I’ll post the drawer class below, as well as the attribute class if anyone wants to use the existing properties in their project.

// Tooltip.cs - Does NOT go in Editor folder
using UnityEngine;
using System.Collections;

public class Tooltip : PropertyAttribute
{
    public string EditorTooltip;

    public Tooltip(string EditorTooltip)
    {
        this.EditorTooltip = EditorTooltip;
    }
}
// This DOES go in the Editor folder (UnityEditor) 
using System;
using System.ComponentModel;
using System.Diagnostics;
using UnityEditor;
using UnityEngine;
using Debug = System.Diagnostics.Debug;

[CustomPropertyDrawer(typeof(Tooltip))]
public class TooltipDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        Tooltip tooltipAttribute = attribute as Tooltip;

        if (property.propertyType == SerializedPropertyType.AnimationCurve)
        {
            property.animationCurveValue = EditorGUI.CurveField(position, new GUIContent(label.text, tooltipAttribute.EditorTooltip), property.animationCurveValue);
        }

        if (property.propertyType == SerializedPropertyType.Boolean)
        {
            property.boolValue = EditorGUI.Toggle(position, new GUIContent(label.text, tooltipAttribute.EditorTooltip), property.boolValue);
        }

        if (property.propertyType == SerializedPropertyType.Bounds)
        {
            property.boundsValue = EditorGUI.BoundsField(position, new GUIContent(label.text, tooltipAttribute.EditorTooltip), property.boundsValue);
        }

        if (property.propertyType == SerializedPropertyType.Color)
        {
            property.colorValue = EditorGUI.ColorField(position, new GUIContent(label.text, tooltipAttribute.EditorTooltip),
                property.colorValue);
        }

        if (property.propertyType == SerializedPropertyType.Float)
        {
            property.floatValue = EditorGUI.FloatField(position,
                new GUIContent(label.text, tooltipAttribute.EditorTooltip), property.floatValue);
        }

        if (property.propertyType == SerializedPropertyType.Integer)
        {
            property.intValue = EditorGUI.IntField(position, new GUIContent(label.text, tooltipAttribute.EditorTooltip), property.intValue);
        }
        
        if (property.propertyType == SerializedPropertyType.Rect)
        {
            property.rectValue = EditorGUI.RectField(position, new GUIContent(label.text, tooltipAttribute.EditorTooltip),
                property.rectValue);
        }

        if (property.propertyType == SerializedPropertyType.String)
        {
            property.stringValue = EditorGUI.TextField(position,
                new GUIContent(label.text, tooltipAttribute.EditorTooltip), property.stringValue);
        }
    }
}

To get the tooltips, you can create a “Test” script or something like that and add the attribute like this:

    [Tooltip("Test string value")]
    public bool TestValue = true;

Hi QuantumCD,

I made a simple plugin that uses some internal members from UnityEditor.dll to mimic the default behavior: GitHub - luizgpa/unity-tooltips: Simple plugin to automatically create tooltips in Unity Editor

It was tested only on Unity 4.1.2 in Windows.

Hi Quantum CD,

I wanted to add some property drawer for specific enums and I’ve got Stack Overflow exception also, did you manage to get it working ?

Unfortunately not. I think it might be a limitation of the serialization. Perhaps I will try again in a future release to see if something is added. The API has a lot of potential.

I ended up creating a specific attribute in Unity4 for my enum type (not cleaned up as I ran some test on it) so I’m able to select multiple values :

	// Draw the property inside the given rect
    public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) {
		// Default rect is too wide
		position.xMin += 4;
		position.xMax -= 4;
        // Using BeginProperty / EndProperty on the parent property means that
        // prefab override logic works on the entire property.
        EditorGUI.BeginProperty (position, label, property);
		
		// Draw label
		EditorGUIUtility.LookLikeControls(150);
		Rect labelRect = new Rect(position.x, position.y, 150, EditorGUI.GetPropertyHeight(property));
		//Rect labelRect = GUILayoutUtility.GetRect(label, EditorStyles.label, GUILayout.Width(150));
		// GUIUtility.GetControlID (FocusType.Passive)
        if (GUI.Button (labelRect, label,EditorStyles.label)) {
			property.isExpanded = false;
			//property.intValue = 0;
		}
		
		position.x += labelRect.width;
		position.width -= labelRect.width;
		// PropertyField does not work (stack overflow exception) for Enum
		if (property.isExpanded) {
			string[] names = property.enumNames;
			for (int i=0;i<names.Length;i++) {
				int enumValue = (int)Enum.Parse(maskAttribute.enumType, names[i]);
				string prefix = "  ";
				if ((property.intValue  enumValue) != 0)
					prefix = "x ";
				// TODO : create a window with EditorWindow.ShowAsDropDown
				if (GUI.Button(position, prefix + names[i])) {
					if ((property.intValue  enumValue) != 0)
						property.intValue ^= enumValue;
					else
						property.intValue |= enumValue;
					property.isExpanded = false;
				}
				position.y += EditorGUI.GetPropertyHeight(property) / names.Length;
			}
		} else {
			if (GUI.Button(position, "" + property.intValue, EditorStyles.popup)) {
				property.isExpanded = true;
			}
		}
        
        EditorGUI.EndProperty ();
	}
	
	public override float GetPropertyHeight (SerializedProperty property, GUIContent label)

    {

        //Get the base height when not expanded

        var height = base.GetPropertyHeight(property, label);

 

        // if the property is expanded go thru all its children and get their height -> just multiply its height for now as it's always rendered exactly the same

        if(property.isExpanded)

        {
			height *= property.enumNames.Length;
			/*
            var propEnum = property.GetEnumerator ();

            while (propEnum.MoveNext())

                height += EditorGUI.GetPropertyHeight((SerializedProperty)propEnum.Current, GUIContent.none, true);
			 // */
        }

        return height;

    }

Hello QuantumCD,

I really like your PropertyDrawer for the most part and have expanded it some to include some of the property types you have not had. I also optimized it some and felt I should offer my code in return.

using UnityEngine;


public class LabelAttribute : PropertyAttribute
{
    #region Members


    public readonly string text;
    public readonly string tooltip;


    #endregion


    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


    #region Exposed


    public LabelAttribute(string text) : this(text, null)
    {
    }




    public LabelAttribute(string text, string tooltip)
    {
        this.text = text;
        this.tooltip = tooltip;
    }


    #endregion
}
using System;
using System.Reflection;
using UnityEditor;
using UnityEngine;


[CustomPropertyDrawer(typeof(LabelAttribute))]
public class LabelDrawer : PropertyDrawer
{
    #region Members


    private static Type _editorType = null;
    private static MethodInfo _layerMaskFieldMethod = null;
    private Type _fieldType = null;
    private GUIContent _label = null;


    #endregion


    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


    #region Exposed


    public override void OnGUI(Rect position, SerializedProperty property, GUIContent oldLabel)
    {
        switch(property.propertyType)
        {
            case SerializedPropertyType.AnimationCurve:
            {
                property.animationCurveValue = EditorGUI.CurveField(position,
                    label, property.animationCurveValue);


                break;
            }
//            case SerializedPropertyType.ArraySize:
//            {
//                break;
//            }
            case SerializedPropertyType.Boolean:
            {
                property.boolValue = EditorGUI.Toggle(position,
                    label, property.boolValue);


                break;
            }
            case SerializedPropertyType.Bounds:
            {
                property.boundsValue = EditorGUI.BoundsField(position,
                    label, property.boundsValue);


                break;
            }
//            case SerializedPropertyType.Character:
//            {
//                break;
//            }
            case SerializedPropertyType.Color:
            {
                property.colorValue = EditorGUI.ColorField(position,
                    label, property.colorValue);


                break;
            }
            case SerializedPropertyType.Enum:
            {
                property.enumValueIndex = (int)(object)EditorGUI.EnumPopup(position, label,
                    Enum.Parse(GetFieldType(property), property.enumNames[property.enumValueIndex]) as Enum);


                break;
            }
            case SerializedPropertyType.Float:
            {
                property.floatValue = EditorGUI.FloatField(position,
                    label, property.floatValue);


                break;
            }
//            case SerializedPropertyType.Generic:
//            {
//                break;
//            }
//            case SerializedPropertyType.Gradient:
//            {
//                break;
//            }
            case SerializedPropertyType.Integer:
            {
                property.intValue = EditorGUI.IntField(position,
                    label, property.intValue);


                break;
            }
            case SerializedPropertyType.LayerMask:
            {
                layerMaskFieldMethod.Invoke(property.intValue, new object[] { position, property, label });


                break;
            }
            case SerializedPropertyType.ObjectReference:
            {
                property.objectReferenceValue = EditorGUI.ObjectField(position,
                    label, property.objectReferenceValue,
                    GetFieldType(property),


                    true);
                break;
            }
            case SerializedPropertyType.Rect:
            {
                property.rectValue = EditorGUI.RectField(position,
                    label, property.rectValue);


                break;
            }
            case SerializedPropertyType.String:
            {
                property.stringValue = EditorGUI.TextField(position,
                    label, property.stringValue);


                break;
            }
//            case SerializedPropertyType.Vector2:
//            {
//                break;
//            }
//            case SerializedPropertyType.Vector3:
//            {
//                Type[] typeDecleration = new Type[] {typeof(Rect), typeof(SerializedProperty), typeof(GUIContent)};
//
//                MethodInfo vector3FieldMethod = editorType.GetMethod("Vector3Field", editorBindingFlags,
//                    Type.DefaultBinder, typeDecleration, null);
//
//                vector3FieldMethod.Invoke(property, new object[] { position, property, label });
//                break;
//            }
            default:
            {
                Debug.LogWarning("LabelDrawer: found an un-handled type: " + property.propertyType);
                break;
            }
        }
    }


    #endregion


    // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


    #region Internal


    private static Type editorType
    {
        get
        {
            if(_editorType == null)
            {
                Assembly assembly = Assembly.GetAssembly(typeof(UnityEditor.EditorGUI));
                _editorType = assembly.GetType("UnityEditor.EditorGUI");
                if(_editorType == null)
                {
                    Debug.LogWarning("LabelDrawer: Failed to open source file of EditorGUI");
                }
            }
            return _editorType;
        }
    }




    private static MethodInfo layerMaskFieldMethod
    {
        get
        {
            if(_layerMaskFieldMethod == null)
            {
                Type[] typeDecleration = new Type[] {typeof(Rect), typeof(SerializedProperty), typeof(GUIContent)};


                _layerMaskFieldMethod = editorType.GetMethod("LayerMaskField", BindingFlags.NonPublic | BindingFlags.Static,
                        Type.DefaultBinder, typeDecleration, null);


                if(_layerMaskFieldMethod == null)
                {
                    Debug.LogError("LabelDrawer: Failed to locate the internal LayerMaskField method.");
                }
            }
            return _layerMaskFieldMethod;
        }
    }




    private GUIContent label
    {
        get
        {
            if(_label == null)
            {
                LabelAttribute labelAttribute = attribute as LabelAttribute;
                _label = new GUIContent(labelAttribute.text, labelAttribute.tooltip);
            }


            return _label;
        }
    }


    
    private Type GetFieldType(SerializedProperty property)
    {
        if(_fieldType == null)
        {
            Type parentClassType = property.serializedObject.targetObject.GetType();
            FieldInfo fieldInfo = parentClassType.GetField(property.name, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);


            if(fieldInfo == null)
            {
                Debug.LogError("LabelDrawer: Could not locate the object in the parent class");
                return null;
            }


            _fieldType = fieldInfo.FieldType;
        }
        return _fieldType;
    }


    #endregion
}

My version from ianjosephfischer’s version.

  1. Automatic property text
  2. Prefab override bold
  3. Check Change Method

Tooltip.cs (Asset/Plugins)

public class ToolTip : PropertyAttribute
{
	#region Members

	public readonly string tooltip;

	#endregion

	#region Exposed
	public ToolTip(string tooltip)
	{
		this.tooltip = tooltip;
	}
	#endregion
}

TooltipDrawer.cs (Asset/Plugins/Editor)

using System;
using System.Reflection;
using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(ToolTip))]
public class ToolTipDrawer : PropertyDrawer
{
	#region Members

	private static Type _editorType = null;
	private static MethodInfo _layerMaskFieldMethod = null;
	private Type _fieldType = null;
	private GUIContent _label = null;
	private GUIContent _oldlabel = null;
	//private SerializedProperty currentProperty = null;


	#endregion

	#region Exposed

	public override void OnGUI(Rect position, SerializedProperty property, GUIContent oldLabel)
	{
		_oldlabel = oldLabel;

		EditorGUI.BeginProperty (position, label, property);
		EditorGUI.BeginChangeCheck ();

		switch(property.propertyType)
		{
		case SerializedPropertyType.AnimationCurve:
			AnimationCurve newAnimationCurveValue = EditorGUI.CurveField(position, label, property.animationCurveValue);
			if(EditorGUI.EndChangeCheck()) property.animationCurveValue = newAnimationCurveValue;
			break;
		case SerializedPropertyType.Boolean:
			bool newBoolValue = EditorGUI.Toggle(position, label, property.boolValue);
			if(EditorGUI.EndChangeCheck()) property.boolValue = newBoolValue;
			break;
		case SerializedPropertyType.Bounds:
			Bounds newBoundsValue = EditorGUI.BoundsField(position, label, property.boundsValue);
			if(EditorGUI.EndChangeCheck()) property.boundsValue = newBoundsValue;
			break;
		case SerializedPropertyType.Color:
			Color newColorValue = EditorGUI.ColorField(position, label, property.colorValue);
			if(EditorGUI.EndChangeCheck()) property.colorValue = newColorValue;
			break;
		case SerializedPropertyType.Enum:
			int newEnumValueIndex = (int)(object)EditorGUI.EnumPopup(position, label, Enum.Parse(GetFieldType(property), property.enumNames[property.enumValueIndex]) as Enum);
			if(EditorGUI.EndChangeCheck()) property.enumValueIndex = newEnumValueIndex;
			break;
		case SerializedPropertyType.Float:
			float newFloatValue = EditorGUI.FloatField(position, label, property.floatValue);
			if(EditorGUI.EndChangeCheck()) property.floatValue = newFloatValue;
			break;
		case SerializedPropertyType.Integer:
			int newIntValue = EditorGUI.IntField(position, label, property.intValue);
			if(EditorGUI.EndChangeCheck()) property.intValue = newIntValue;
			break;
		case SerializedPropertyType.LayerMask:
			layerMaskFieldMethod.Invoke(property.intValue, new object[] { position, property, label });
			break;
		case SerializedPropertyType.ObjectReference:
			UnityEngine.Object newObjectReferenceValue = EditorGUI.ObjectField(position, label, property.objectReferenceValue, GetFieldType(property), true);
			if(EditorGUI.EndChangeCheck()) property.objectReferenceValue = newObjectReferenceValue;
			break;
		case SerializedPropertyType.Rect:
			Rect newRectValue = EditorGUI.RectField(position, label, property.rectValue);
			if(EditorGUI.EndChangeCheck()) property.rectValue = newRectValue;
			break;
		case SerializedPropertyType.String:
			string newStringValue = EditorGUI.TextField(position, label, property.stringValue);
			if(EditorGUI.EndChangeCheck()) property.stringValue = newStringValue;
			break;
		default:
			Debug.LogWarning("ToolTipDrawer: found an un-handled type: " + property.propertyType);
			break;
		}

		EditorGUI.EndProperty ();
	}

	#endregion

	#region Internal
	private static Type editorType
	{
		get
		{
			if(_editorType == null)
			{
				Assembly assembly = Assembly.GetAssembly(typeof(UnityEditor.EditorGUI));
				_editorType = assembly.GetType("UnityEditor.EditorGUI");
				if(_editorType == null)
				{
					Debug.LogWarning("ToolTipDrawer: Failed to open source file of EditorGUI");
				}
			}
			return _editorType;
		}
	}

	private static MethodInfo layerMaskFieldMethod
	{
		get
		{
			if(_layerMaskFieldMethod == null)
			{
				Type[] typeDecleration = new Type[] {typeof(Rect), typeof(SerializedProperty), typeof(GUIContent)};
				_layerMaskFieldMethod = editorType.GetMethod("LayerMaskField", BindingFlags.NonPublic | BindingFlags.Static,
				                                             Type.DefaultBinder, typeDecleration, null);
				if(_layerMaskFieldMethod == null)
				{
					Debug.LogError("ToolTipDrawer: Failed to locate the internal LayerMaskField method.");
				}
			}
			return _layerMaskFieldMethod;
		}
	}

	private GUIContent label
	{
		get
		{
			if(_label == null)
			{
				ToolTip labelAttribute = attribute as ToolTip;
				_label = new GUIContent(_oldlabel.text, labelAttribute.tooltip);
			}


			return _label;
		}
	}

	private Type GetFieldType(SerializedProperty property)
	{
		if(_fieldType == null)
		{
			Type parentClassType = property.serializedObject.targetObject.GetType();
			FieldInfo fieldInfo = parentClassType.GetField(property.name, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);

			if(fieldInfo == null)
			{
				Debug.LogError("ToolTipDrawer: Could not locate the object in the parent class");
				return null;
			}
			_fieldType = fieldInfo.FieldType;
		}
		return _fieldType;
	}

	#endregion
	
}

ToolTipTest.cs

using UnityEngine;
using System.Collections;

public class ToolTipTest : MonoBehaviour {

	[ToolTip("This is a tooltip of String Field")]
	[SerializeField] string stringField;

}

Thanks a lot! :smile:

1 Like

For anyone stumbling upon this thread from their searches: Note that since Unity 4.5, The Tooltip property attribute is now built-in to Unity and uses the same syntax as above without requiring any of the plugin code.

That is, the example above in ToolTipTest.cs now works without having to include Tooltip.cs or TooltipDrawer.cs.

3 Likes

Hi!
I’m still using Unity 4.3 and I found this ToolTip property drawer to be rather useful. However, it doesn’t seem to work with custom inspectors. My custom inspector is only adding a few buttons. Besides that it I’m using DrawDefaultInspector(). Any ideas as to what the problem might be?

Cheers!

This is useful in most case, but can not handle customized class array field like this:

public MyCustomField[ ] test;

Can I use it with another PropertyAttribute?
Like:

[ToolTip("This is a great tooltip")]  
[Range(0f,500f)]
public int MyInt=50;

or:

[ToolTip("This is a great tooltip"), Range(0f,500f)]
public int MyInt=50;

Is possible?