Community Made Tools

Have you made any useful utilities with Odin?

Login and submit your creations here

Tooltips as compact help boxes

Authored by Maciej
Shared 03-12-2022

Set of scripts that change how tooltips are drawn when using Odin Inspector and add new attribute for drawing compact help boxes.

When folded, the tooltip is displayed by hovering over a toggle button.

If you don't want to change how tooltips are drawn, remove TooltipDetailsAttribute.cs and HelpBoxTooltipDrawer.cs.

If you don't want HelpBox attribute, remove HelpBoxAttribute.cs and HelpBoxAttributeDrawer.cs.

To add details to tooltip, add TooltipDetails attribute.

https://github.com/My-Key/OdinHelpBox

Github is easier and faster to update, so any changes will be there first

Usage

[Tooltip("Tooltip example")]
[TooltipDetails("@" + nameof(m_text))]
[SerializeField] private float m_test;

[PropertyTooltip("@" + nameof(m_text))]
[ShowInInspector]
private bool TestValue { get; set; }

[HelpBox("@" + nameof(m_text), "Help box details example")]
[SerializeField] private Vector2 m_vector;

[SerializeField] private string m_text = "Example text";

Attributes

HelpBoxAttribute.cs

using System;

namespace OdinExtra.HelpBox
{
	[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
	public class HelpBoxAttribute : Attribute
	{
		public string Text { get; private set; }

		public string Details { get; private set; }
		
		/// <summary>
		/// Draws compact help box
		/// </summary>
		/// <param name="text">Main text text. Can be a regular string or resolved to method that returns string to display</param>
		/// <param name="details">Details text. Can be a regular string or resolved to method that returns string to display</param>
		public HelpBoxAttribute(string text, string details = null)
		{
			Text = text;
			Details = details;
		}
	}
}

TooltipDetailsAttribute .cs

using System;

namespace OdinExtra.HelpBox
{
	public class TooltipDetailsAttribute : Attribute
	{
		public string Details { get; }

		/// <summary>
		/// Adds details to tooltip help box. 
		/// </summary>
		/// <param name="details">Details text. Can be a regular string or resolved to method that returns string to display</param>
		public TooltipDetailsAttribute(string details)
		{
			Details = details;
		}
	}
}

Editor scripts

HelpBoxCommon.cs

using Sirenix.OdinInspector;
using Sirenix.OdinInspector.Editor;
using Sirenix.Utilities;
using Sirenix.Utilities.Editor;
using UnityEditor;
using UnityEngine;

namespace OdinExtra.Editor.HelpBox
{
	public static class HelpBoxCommon
	{
		private static readonly Color TOGGLE_OFF_COLOR = new Color(0.4f, 0.9f, 1.0f, 0.5f);
		private static readonly Color TOGGLE_ON_COLOR = new Color(0.25f, 1.0f, 1.0f);

		private static readonly Color BG_COLOR = new Color(0.5f, 1.0f, 1.0f);

		public static bool ToggleButton(bool value, EditorIcon icon, Color toggleOnColor, Color toggleOffColor,
			int width = 18, int height = 18, string tooltip = null)
		{
			var rect = EditorGUILayout.GetControlRect(false,
				GUILayoutOptions.ExpandWidth(false).Width(width).Height(height));

			var current = GUI.Toggle(rect, value, GUIHelper.TempContent(null, null, tooltip), GUIStyle.none);

			if (current != value)
				GUIHelper.RemoveFocusControl();

			if (Event.current.type == EventType.Repaint)
			{
				float drawSize = Mathf.Min(rect.height, rect.width);
				var color = value ? toggleOnColor : toggleOffColor;
				GUIHelper.PushColor(color);
				icon.Draw(rect, drawSize);
				GUIHelper.PopColor();
			}

			return current;
		}

		public static bool ToggleButton(bool value, SdfIconType icon, Color toggleOnColor, Color toggleOffColor,
			int width = 18, int height = 18, string tooltip = null)
		{
			var rect = EditorGUILayout.GetControlRect(false,
				GUILayoutOptions.ExpandWidth(false).Width(width).Height(height));

			var current = GUI.Toggle(rect, value, GUIHelper.TempContent(null, null, tooltip), GUIStyle.none);

			if (current != value)
				GUIHelper.RemoveFocusControl();

			if (Event.current.type == EventType.Repaint)
			{
				var color = value ? toggleOnColor : toggleOffColor;
				GUIHelper.PushColor(color);
				SdfIcons.DrawIcon(rect, icon);
				GUIHelper.PopColor();
			}

			return current;
		}

		public static bool BeginHelpBox(string tooltip, LocalPersistentContext<bool> isVisible, object fadeKey,
			string detailed, LocalPersistentContext<bool> isVisibleDetailed, object fadeDetailedKey)
		{
			var buttonTooltip = isVisible.Value ? null : tooltip;

			var visible = isVisible.Value;

			GUIHelper.PushHierarchyMode(false);

			SirenixEditorGUI.BeginIndentedHorizontal();

			GUIHelper.PushGUIEnabled(true);

			isVisible.Value = ToggleButton(isVisible.Value,
				isVisible.Value ? SdfIconType.InfoCircleFill : SdfIconType.InfoCircle, TOGGLE_ON_COLOR, TOGGLE_ON_COLOR,
				14, tooltip: buttonTooltip);
			GUIHelper.PopGUIEnabled();

			// if (visible)
			// 	SirenixEditorGUI.BeginBox();

			SirenixEditorGUI.BeginIndentedVertical();

			if (SirenixEditorGUI.BeginFadeGroup(fadeKey, isVisible.Value))
				MessageBox(tooltip, detailed, isVisibleDetailed, fadeDetailedKey);

			SirenixEditorGUI.EndFadeGroup();

			return visible;
		}

		public static void EndHelpBox(bool visible)
		{
			SirenixEditorGUI.EndIndentedVertical();

			SirenixEditorGUI.EndIndentedHorizontal();

			GUIHelper.PopHierarchyMode();

			// if (visible)
			// 	SirenixEditorGUI.EndBox();
		}

		private static GUIStyle MESSSAGE_BOX;

		public static GUIStyle MessageStyle =>
			MESSSAGE_BOX ??= new GUIStyle("Label")
			{
				margin = new RectOffset(4, 4, 2, 2),
				richText = true,
				wordWrap = true
			};

		private static void MessageBox(string message, string detailMessage, LocalPersistentContext<bool> isVisible,
			object key)
		{
			bool detailMessageIsValid = !string.IsNullOrWhiteSpace(detailMessage);

			GUIHelper.PushColor(BG_COLOR);
			SirenixEditorGUI.BeginBox();
			GUIHelper.PopColor();

			EditorGUILayout.BeginHorizontal();

			if (detailMessageIsValid)
			{
				isVisible.Value = ToggleButton(isVisible.Value,
					isVisible.Value ? EditorIcons.TriangleDown : EditorIcons.TriangleRight, Color.white, Color.white,
					tooltip: isVisible.Value ? "Hide details" : "Show details");
			}

			EditorGUILayout.BeginVertical();

			GUILayout.Label(message, MessageStyle);

			if (SirenixEditorGUI.BeginFadeGroup(key, isVisible.Value))
			{
				SirenixEditorGUI.HorizontalLineSeparator(Color.black * 0.25f);
				GUILayout.Label(detailMessage, MessageStyle);
			}

			SirenixEditorGUI.EndFadeGroup();

			EditorGUILayout.EndVertical();

			EditorGUILayout.EndHorizontal();
			SirenixEditorGUI.EndBox();
		}
	}
}

HelpBoxAttributeDrawer .cs

using OdinExtra.HelpBox;
using Sirenix.OdinInspector.Editor;
using Sirenix.OdinInspector.Editor.ValueResolvers;
using UnityEngine;

namespace OdinExtra.Editor.HelpBox
{
	[DrawerPriority(0, 100, 0)]
	public class HelpBoxAttributeDrawer : OdinAttributeDrawer<HelpBoxAttribute>
	{
		private LocalPersistentContext<bool> m_isVisible;
		private LocalPersistentContext<bool> m_isVisibleDetails;

		private ValueResolver<string> m_textResolver;
		private ValueResolver<string> m_detailsTextResolver;

		protected override void Initialize()
		{
			base.Initialize();

			m_isVisible = this.GetPersistentValue<bool>("IsVisible");
			m_isVisibleDetails = this.GetPersistentValue<bool>("IsVisibleDetails");

			m_textResolver = ValueResolver.GetForString(Property, Attribute.Text);
			m_detailsTextResolver = ValueResolver.GetForString(Property, Attribute.Details);
		}

		protected override void DrawPropertyLayout(GUIContent label)
		{
			if (m_textResolver.HasError)
			{
				m_textResolver.DrawError();
				CallNextDrawer(label);
				return;
			}

			var tooltip = m_textResolver.GetValue();

			string details = null;

			if (m_detailsTextResolver != null)
			{
				m_detailsTextResolver.DrawError();
				details = m_detailsTextResolver.GetValue();
			}

			var visible = HelpBoxCommon.BeginHelpBox(tooltip, m_isVisible, this, details, m_isVisibleDetails,
				m_isVisibleDetails);

			CallNextDrawer(label);

			HelpBoxCommon.EndHelpBox(visible);
		}
	}
}

HelpBoxTooltipDrawer.cs

using OdinExtra.HelpBox;
using Sirenix.OdinInspector;
using Sirenix.OdinInspector.Editor;
using Sirenix.OdinInspector.Editor.ValueResolvers;
using UnityEngine;

namespace OdinExtra.Editor.HelpBox
{
	[DontApplyToListElements]
	[DrawerPriority(0, 100, 0)]
	public sealed class HelpBoxTooltipDrawer<T> : OdinValueDrawer<T>
	{
		private LocalPersistentContext<bool> m_isVisible;
		private LocalPersistentContext<bool> m_isVisibleDetails;

		private ValueResolver<string> m_detailsTextResolver;

		protected override void Initialize()
		{
			base.Initialize();

			m_isVisible = this.GetPersistentValue<bool>("IsVisible");
			m_isVisibleDetails = this.GetPersistentValue<bool>("IsVisibleDetails");

			var detailsAttribute = Property.GetAttribute<TooltipDetailsAttribute>();

			if (detailsAttribute != null)
				m_detailsTextResolver = ValueResolver.GetForString(Property, detailsAttribute.Details);
		}

		protected override void DrawPropertyLayout(GUIContent label)
		{
			var hasTooltip = label != null && !string.IsNullOrWhiteSpace(label.tooltip);

			if (!hasTooltip)
			{
				CallNextDrawer(label);
				return;
			}

			var tooltip = label.tooltip;
			label.tooltip = null;

			string details = null;

			if (m_detailsTextResolver != null)
			{
				m_detailsTextResolver.DrawError();
				details = m_detailsTextResolver.GetValue();
			}

			var visible = HelpBoxCommon.BeginHelpBox(tooltip, m_isVisible, this, details, m_isVisibleDetails,
				m_isVisibleDetails);

			CallNextDrawer(label);

			HelpBoxCommon.EndHelpBox(visible);
		}
	}
}