// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Editor.CustomDrawers.Utils { #if UNITY_EDITOR using System; using System.Collections; using System.Collections.Generic; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; using WallstopStudios.UnityHelpers.Core.Attributes; /// /// Provides shared constants, validation logic, and helper methods for validation attribute drawers. /// /// /// This utility class consolidates common code used by ValidateAssignment and WNotNull drawers /// (both standard PropertyDrawer and Odin Inspector implementations). By centralizing these /// elements, we ensure consistent behavior and eliminate code duplication. /// public static class ValidationShared { /// /// Padding between the help box and the property field in pixels. /// public const float HelpBoxPadding = 2f; /// /// Default message format for ValidateAssignment validation failures. /// Use with string.Format where {0} is the field name. /// public const string ValidateAssignmentMessageFormat = "{0} is not assigned or is empty"; /// /// Fallback message when field name cannot be determined for ValidateAssignment. /// public const string ValidateAssignmentFallbackMessage = "Field is not assigned or is empty"; /// /// Default message format for WNotNull validation failures. /// Use with string.Format where {0} is the field name. /// public const string NotNullMessageFormat = "{0} must not be null"; /// /// Fallback message when field name cannot be determined for WNotNull. /// public const string NotNullFallbackMessage = "Field is null or unassigned"; private static readonly Dictionary HelpBoxHeightCache = new( StringComparer.Ordinal ); private static readonly GUIContent ReusableContent = new(); /// /// Clears the help box height cache. Useful for tests or when font settings change. /// public static void ClearHeightCache() { HelpBoxHeightCache.Clear(); } /// /// Calculates the height of a help box for the given message text. /// Results are cached for performance. /// /// The message to display in the help box. /// The calculated height in pixels. public static float GetHelpBoxHeight(string message) { if (string.IsNullOrEmpty(message)) { return EditorGUIUtility.singleLineHeight * 2f; } if (HelpBoxHeightCache.TryGetValue(message, out float cachedHeight)) { return cachedHeight; } ReusableContent.text = message; GUIStyle helpBoxStyle = EditorStyles.helpBox; float minHeight = EditorGUIUtility.singleLineHeight * 2f; float viewWidth = 600f; try { viewWidth = Mathf.Max(0f, EditorGUIUtility.currentViewWidth); } catch { // Called outside OnGUI context; use fallback viewWidth = 600f; } float calculatedHeight = helpBoxStyle.CalcHeight(ReusableContent, viewWidth - 40f); float height = Mathf.Max(minHeight, calculatedHeight); HelpBoxHeightCache[message] = height; return height; } /// /// Converts a to an IMGUI . /// /// The validation message type. /// The corresponding IMGUI message type. public static MessageType ToMessageType(ValidateAssignmentMessageType messageType) { return messageType switch { ValidateAssignmentMessageType.Error => MessageType.Error, _ => MessageType.Warning, }; } /// /// Converts a to a UI Toolkit . /// /// The validation message type. /// The corresponding UI Toolkit help box message type. public static HelpBoxMessageType ToHelpBoxMessageType( ValidateAssignmentMessageType messageType ) { return messageType switch { ValidateAssignmentMessageType.Error => HelpBoxMessageType.Error, _ => HelpBoxMessageType.Warning, }; } /// /// Gets the IMGUI for a . /// /// The attribute, or null for default behavior. /// The corresponding IMGUI message type (Warning if attribute is null). public static MessageType GetMessageType(ValidateAssignmentAttribute validateAttribute) { if (validateAttribute == null) { return MessageType.Warning; } return ToMessageType(validateAttribute.MessageType); } /// /// Gets the UI Toolkit for a . /// /// The attribute, or null for default behavior. /// The corresponding UI Toolkit help box message type (Warning if attribute is null). public static HelpBoxMessageType GetHelpBoxMessageType( ValidateAssignmentAttribute validateAttribute ) { if (validateAttribute == null) { return HelpBoxMessageType.Warning; } return ToHelpBoxMessageType(validateAttribute.MessageType); } /// /// Converts a to an IMGUI . /// /// The not-null message type. /// The corresponding IMGUI message type. public static MessageType ToMessageType(WNotNullMessageType messageType) { return messageType switch { WNotNullMessageType.Error => MessageType.Error, _ => MessageType.Warning, }; } /// /// Converts a to a UI Toolkit . /// /// The not-null message type. /// The corresponding UI Toolkit help box message type. public static HelpBoxMessageType ToHelpBoxMessageType(WNotNullMessageType messageType) { return messageType switch { WNotNullMessageType.Error => HelpBoxMessageType.Error, _ => HelpBoxMessageType.Warning, }; } /// /// Gets the IMGUI for a . /// /// The attribute, or null for default behavior. /// The corresponding IMGUI message type (Warning if attribute is null). public static MessageType GetMessageType(WNotNullAttribute notNullAttribute) { if (notNullAttribute == null) { return MessageType.Warning; } return ToMessageType(notNullAttribute.MessageType); } /// /// Gets the UI Toolkit for a . /// /// The attribute, or null for default behavior. /// The corresponding UI Toolkit help box message type (Warning if attribute is null). public static HelpBoxMessageType GetHelpBoxMessageType(WNotNullAttribute notNullAttribute) { if (notNullAttribute == null) { return HelpBoxMessageType.Warning; } return ToHelpBoxMessageType(notNullAttribute.MessageType); } /// /// Gets the validation message for a using the property's display name. /// /// The serialized property being validated. /// The attribute, or null for default behavior. /// The message to display in the help box. public static string GetValidateAssignmentMessage( SerializedProperty property, ValidateAssignmentAttribute validateAttribute ) { if (validateAttribute != null && !string.IsNullOrEmpty(validateAttribute.CustomMessage)) { return validateAttribute.CustomMessage; } string fieldName = property?.displayName ?? ValidateAssignmentFallbackMessage; return string.Format(ValidateAssignmentMessageFormat, fieldName); } /// /// Gets the validation message for a using a custom field name. /// Primarily used by Odin Inspector drawers which use Property.NiceName. /// /// The display name of the field. /// The attribute, or null for default behavior. /// The message to display in the help box. public static string GetValidateAssignmentMessage( string fieldName, ValidateAssignmentAttribute validateAttribute ) { if (validateAttribute != null && !string.IsNullOrEmpty(validateAttribute.CustomMessage)) { return validateAttribute.CustomMessage; } string displayName = fieldName ?? ValidateAssignmentFallbackMessage; return string.Format(ValidateAssignmentMessageFormat, displayName); } /// /// Gets the validation message for a using the property's display name. /// /// The serialized property being validated. /// The attribute, or null for default behavior. /// The message to display in the help box. public static string GetNotNullMessage( SerializedProperty property, WNotNullAttribute notNullAttribute ) { if (notNullAttribute != null && !string.IsNullOrEmpty(notNullAttribute.CustomMessage)) { return notNullAttribute.CustomMessage; } string fieldName = property?.displayName ?? NotNullFallbackMessage; return string.Format(NotNullMessageFormat, fieldName); } /// /// Gets the validation message for a using a custom field name. /// Primarily used by Odin Inspector drawers which use Property.NiceName. /// /// The display name of the field. /// The attribute, or null for default behavior. /// The message to display in the help box. public static string GetNotNullMessage(string fieldName, WNotNullAttribute notNullAttribute) { if (notNullAttribute != null && !string.IsNullOrEmpty(notNullAttribute.CustomMessage)) { return notNullAttribute.CustomMessage; } string displayName = fieldName ?? NotNullFallbackMessage; return string.Format(NotNullMessageFormat, displayName); } /// /// Checks if a value is null for WNotNull validation purposes. /// Handles both standard CLR null and Unity Object fake null. /// /// The value to check. /// True if the value is null or a destroyed Unity Object. public static bool IsValueNull(object value) { if (value == null) { return true; } if (value is UnityEngine.Object unityObject) { return unityObject == null; } return false; } /// /// Checks if a value is invalid for ValidateAssignment validation purposes. /// Handles null, empty strings, empty collections, and empty enumerables. /// /// The value to check. /// True if the value is invalid (null, empty, or has no elements). public static bool IsValueInvalid(object value) { if (value == null) { return true; } if (value is UnityEngine.Object unityObject) { return unityObject == null; } if (value is string stringValue) { return string.IsNullOrWhiteSpace(stringValue); } if (value is ICollection collection) { return collection.Count <= 0; } if (value is IEnumerable enumerable) { IEnumerator enumerator = enumerable.GetEnumerator(); try { return !enumerator.MoveNext(); } finally { if (enumerator is IDisposable disposable) { disposable.Dispose(); } } } return false; } /// /// Checks if a serialized property value is null. /// /// The property to check. /// True if the property value is null. public static bool IsPropertyNull(SerializedProperty property) { if (property == null) { return true; } switch (property.propertyType) { case SerializedPropertyType.ObjectReference: return property.objectReferenceValue == null; case SerializedPropertyType.ExposedReference: return property.exposedReferenceValue == null; case SerializedPropertyType.ManagedReference: return property.managedReferenceValue == null; case SerializedPropertyType.String: return string.IsNullOrEmpty(property.stringValue); default: return false; } } /// /// Checks if a serialized property value is invalid (null, empty string, or empty collection). /// /// The property to check. /// True if the property value is invalid. public static bool IsPropertyInvalid(SerializedProperty property) { if (property == null) { return true; } // Check arrays/lists first - they have isArray = true regardless of propertyType // String has isArray = true but should be checked separately if (property.isArray && property.propertyType != SerializedPropertyType.String) { return property.arraySize <= 0; } switch (property.propertyType) { case SerializedPropertyType.ObjectReference: return property.objectReferenceValue == null; case SerializedPropertyType.ExposedReference: return property.exposedReferenceValue == null; case SerializedPropertyType.ManagedReference: return property.managedReferenceValue == null; case SerializedPropertyType.String: return string.IsNullOrWhiteSpace(property.stringValue); case SerializedPropertyType.Generic: return IsGenericPropertyInvalid(property); default: return false; } } /// /// Checks if a generic serialized property (typically a collection) is invalid. /// /// The property to check. /// True if the property is an empty collection. public static bool IsGenericPropertyInvalid(SerializedProperty property) { // Arrays are handled in IsPropertyInvalid before we get here if (property.isArray) { return property.arraySize <= 0; } SerializedProperty arraySizeProperty = property.FindPropertyRelative("Array.size"); if (arraySizeProperty != null) { return arraySizeProperty.intValue <= 0; } SerializedProperty countProperty = property.FindPropertyRelative("_size"); if (countProperty != null) { return countProperty.intValue <= 0; } countProperty = property.FindPropertyRelative("m_Size"); if (countProperty != null) { return countProperty.intValue <= 0; } return false; } /// /// Draws a validation help box for a ValidateAssignment property if it is invalid. /// Call this from custom editors for array/list properties that won't have /// their PropertyDrawer invoked at the array level. /// /// The property to validate. /// The attribute containing validation settings. /// True if a help box was drawn, false otherwise. public static bool DrawValidateAssignmentHelpBoxIfNeeded( SerializedProperty property, ValidateAssignmentAttribute validateAttribute ) { if (!IsPropertyInvalid(property)) { return false; } string message = GetValidateAssignmentMessage(property, validateAttribute); MessageType messageType = GetMessageType(validateAttribute); EditorGUILayout.HelpBox(message, messageType); return true; } /// /// Draws a validation help box for a WNotNull property if it is null. /// Call this from custom editors for array/list properties that won't have /// their PropertyDrawer invoked at the array level. /// /// The property to validate. /// The attribute containing validation settings. /// True if a help box was drawn, false otherwise. public static bool DrawNotNullHelpBoxIfNeeded( SerializedProperty property, WNotNullAttribute notNullAttribute ) { if (!IsPropertyNull(property)) { return false; } string message = GetNotNullMessage(property, notNullAttribute); MessageType messageType = GetMessageType(notNullAttribute); EditorGUILayout.HelpBox(message, messageType); return true; } } #endif }