namespace jeanf.validationTools
{
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
///
/// Hierarchy feedback for ill-setup GameObjects: the name turns ORANGE and a
/// small orange dot appears left of it whenever any component on the object
/// has a validation issue ([Validation] field unset, or IValidatable false).
/// ANCESTORS of an ill-setup object get a dimmed dot (name untouched), so a
/// collapsed parent can never hide a problem in its children.
/// Results are cached per object and invalidated on any object change, so
/// scanning cost stays negligible even in large scenes.
///
[InitializeOnLoad]
public static class ValidationHighlighter
{
private static readonly Color BackgroundColor = new Color(0.7843f, 0.7843f, 0.7843f);
private static readonly Color BackgroundProColor = new Color(0.2196f, 0.2196f, 0.2196f);
private static readonly Color BackgroundSelectedColor = new Color(0.22745f, 0.447f, 0.6902f);
private static readonly Color BackgroundSelectedProColor = new Color(0.1725f, 0.3647f, 0.5294f);
private enum IssueScope { None, Self, InChildren }
private static readonly Dictionary IssuesById = new Dictionary();
static ValidationHighlighter()
{
EditorApplication.hierarchyWindowItemByEntityIdOnGUI += OnHierarchyWindowItemOnGUI;
EditorApplication.hierarchyChanged += ClearCache;
Undo.undoRedoPerformed += ClearCache;
// Field edits in the inspector don't fire hierarchyChanged — this does.
ObjectChangeEvents.changesPublished += (ref ObjectChangeEventStream stream) => ClearCache();
}
private static void ClearCache()
{
IssuesById.Clear();
EditorApplication.RepaintHierarchyWindow();
}
private static void OnHierarchyWindowItemOnGUI(EntityId entityId, Rect selectionRect)
{
if (Event.current.type != EventType.Repaint) return;
if (!IssuesById.TryGetValue(entityId, out var scope))
{
scope = ComputeScope(EditorUtility.EntityIdToObject(entityId) as GameObject);
IssuesById[entityId] = scope;
}
if (scope == IssueScope.None) return;
var obj = EditorUtility.EntityIdToObject(entityId) as GameObject;
if (obj == null) return;
// Little orange dot left of the name — far enough left to clear the
// foldout arrow of expandable objects. Ancestors of an ill-setup
// child get a DIMMED dot, so collapsing them never hides a problem.
var dotRect = new Rect(selectionRect.x - 24f, selectionRect.y + selectionRect.height * 0.5f - 6f, 12f, 12f);
if (scope == IssueScope.InChildren)
{
var previousColor = GUI.color;
GUI.color = new Color(1f, 1f, 1f, 0.45f);
GUI.Label(dotRect, "●", ValidationUi.OrangeLabel);
GUI.color = previousColor;
return; // the object itself is fine: dot only, name untouched
}
GUI.Label(dotRect, "●", ValidationUi.OrangeLabel);
// Repaint the name in orange (over a row-colored patch so the default
// white label underneath doesn't bleed through).
var labelRect = selectionRect;
labelRect.x += 16f; // skip the object icon
var backgroundColor = EditorGUIUtility.isProSkin ? BackgroundProColor : BackgroundColor;
if (Selection.Contains(entityId))
backgroundColor = EditorGUIUtility.isProSkin ? BackgroundSelectedProColor : BackgroundSelectedColor;
var backgroundRect = labelRect;
backgroundRect.width = ValidationUi.OrangeLabel.CalcSize(new GUIContent(obj.name)).x;
EditorGUI.DrawRect(backgroundRect, backgroundColor);
EditorGUI.LabelField(labelRect, obj.name, ValidationUi.OrangeLabel);
}
private static IssueScope ComputeScope(GameObject gameObject)
{
if (gameObject == null) return IssueScope.None;
if (ValidationScanner.HasIssues(gameObject)) return IssueScope.Self;
foreach (var component in gameObject.GetComponentsInChildren(true))
{
if (component == null || component.gameObject == gameObject) continue;
if (ValidationScanner.HasIssues(component)) return IssueScope.InChildren;
}
return IssueScope.None;
}
}
#endif
}