// 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 System.Reflection; using WallstopStudios.UnityHelpers.Core.Attributes; /// /// Provides shared condition evaluation logic for WShowIf attribute drawers. /// /// /// This utility class consolidates common code used by both the standard PropertyDrawer /// and the Odin Inspector drawer implementations of WShowIf. By centralizing these /// elements, we ensure consistent behavior and eliminate code duplication. /// public static class ShowIfConditionEvaluator { /// /// Binding flags for resolving members on types. /// public const BindingFlags MemberBindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy; private static readonly Dictionary CompareToMethodCache = new(); /// /// Tries to evaluate the condition and determine whether the property should be shown. /// /// The current value of the condition field. /// The WShowIf attribute containing comparison settings. /// /// When this method returns true, contains whether the property should be shown. /// /// /// True if the condition was successfully evaluated; false if evaluation failed. /// public static bool TryEvaluateCondition( object conditionValue, WShowIfAttribute showIf, out bool shouldShow ) { bool? evaluation = EvaluateCondition(conditionValue, showIf); if (!evaluation.HasValue) { shouldShow = true; return false; } bool matched = evaluation.Value; shouldShow = showIf.inverse ? !matched : matched; return true; } /// /// Evaluates the condition based on the condition value and attribute settings. /// /// The current value of the condition field. /// The WShowIf attribute containing comparison settings. /// /// True if condition matches, false if it doesn't match, null if evaluation failed. /// public static bool? EvaluateCondition(object conditionValue, WShowIfAttribute attribute) { WShowIfComparison comparison = attribute.comparison; #pragma warning disable CS0618 // Type or member is obsolete if (comparison == WShowIfComparison.Unknown) #pragma warning restore CS0618 // Type or member is obsolete { comparison = WShowIfComparison.Equal; } switch (comparison) { case WShowIfComparison.IsNull: return IsNull(conditionValue); case WShowIfComparison.IsNotNull: return !IsNull(conditionValue); case WShowIfComparison.IsNullOrEmpty: return IsNullOrEmpty(conditionValue); case WShowIfComparison.IsNotNullOrEmpty: return !IsNullOrEmpty(conditionValue); default: break; } object[] expectedValues = attribute.expectedValues; if (conditionValue is bool boolean) { return EvaluateBooleanCondition(boolean, comparison, expectedValues); } if (expectedValues == null || expectedValues.Length == 0) { return null; } switch (comparison) { case WShowIfComparison.Equal: return MatchesAny(conditionValue, expectedValues); case WShowIfComparison.NotEqual: return !MatchesAny(conditionValue, expectedValues); case WShowIfComparison.GreaterThan: case WShowIfComparison.GreaterThanOrEqual: case WShowIfComparison.LessThan: case WShowIfComparison.LessThanOrEqual: object referenceValue = expectedValues[0]; return EvaluateRelationalComparison(conditionValue, referenceValue, comparison); default: return MatchesAny(conditionValue, expectedValues); } } /// /// Evaluates a boolean condition against expected values. /// /// The boolean value to evaluate. /// The comparison type. /// The expected values to compare against. /// True if condition matches, false otherwise. public static bool? EvaluateBooleanCondition( bool value, WShowIfComparison comparison, object[] expectedValues ) { if (expectedValues is { Length: > 0 }) { bool matches = MatchesAny(value, expectedValues); if (comparison == WShowIfComparison.NotEqual) { return !matches; } return matches; } if (comparison == WShowIfComparison.NotEqual) { return !value; } return value; } /// /// Checks if the condition value matches any of the expected values. /// /// The current value to check. /// The expected values to compare against. /// True if condition value matches any expected value. public static bool MatchesAny(object conditionValue, object[] expectedValues) { for (int index = 0; index < expectedValues.Length; index++) { if (ValuesEqual(conditionValue, expectedValues[index])) { return true; } } return false; } /// /// Compares two values for equality, handling enums, flags, and numeric conversions. /// /// The actual value. /// The expected value. /// True if values are considered equal. public static bool ValuesEqual(object actual, object expected) { if (ReferenceEquals(actual, expected)) { return true; } if (actual == null || expected == null) { return false; } if (actual.Equals(expected)) { return true; } Type actualType = actual.GetType(); Type expectedType = expected.GetType(); try { if (actualType.IsEnum || expectedType.IsEnum) { long actualValue = Convert.ToInt64(actual); long expectedValue = Convert.ToInt64(expected); Type enumType = actualType.IsEnum ? actualType : expectedType; if (enumType.IsDefined(typeof(FlagsAttribute), false)) { return (actualValue & expectedValue) == expectedValue; } return actualValue == expectedValue; } } catch { return false; } if (actual is not IConvertible || expected is not IConvertible) { return false; } try { double actualValue = Convert.ToDouble(actual); double expectedValue = Convert.ToDouble(expected); return Math.Abs(actualValue - expectedValue) < double.Epsilon; } catch { return false; } } /// /// Evaluates a relational comparison between two values. /// /// The actual value. /// The expected value to compare against. /// The comparison type. /// /// True if comparison succeeds, false if it fails, null if comparison is not possible. /// public static bool? EvaluateRelationalComparison( object actual, object expected, WShowIfComparison comparison ) { if (!TryCompare(actual, expected, out int compareResult)) { return null; } switch (comparison) { case WShowIfComparison.GreaterThan: return compareResult > 0; case WShowIfComparison.GreaterThanOrEqual: return compareResult >= 0; case WShowIfComparison.LessThan: return compareResult < 0; case WShowIfComparison.LessThanOrEqual: return compareResult <= 0; default: return null; } } /// /// Tries to compare two values and returns the comparison result. /// /// The actual value. /// The expected value. /// /// When this method returns true, contains the comparison result /// (negative if actual less than expected, zero if equal, positive if greater than). /// /// True if comparison was successful; false otherwise. public static bool TryCompare(object actual, object expected, out int comparisonResult) { comparisonResult = 0; if (actual == null || expected == null) { return false; } IComparable comparable = actual as IComparable; if (comparable != null) { object converted = ConvertValue( actual.GetType(), expected, out bool conversionSucceeded ); if (conversionSucceeded) { try { comparisonResult = comparable.CompareTo(converted); return true; } catch { // Fall through to other comparison methods } } } if (TryGenericComparableCompare(actual, expected, out comparisonResult, false)) { return true; } IComparable expectedComparable = expected as IComparable; if (expectedComparable != null) { object converted = ConvertValue( expected.GetType(), actual, out bool conversionSucceeded ); if (conversionSucceeded) { try { comparisonResult = -expectedComparable.CompareTo(converted); return true; } catch { // Fall through to other comparison methods } } } if (TryGenericComparableCompare(expected, actual, out comparisonResult, true)) { return true; } if ( TryConvertToDouble(actual, out double actualDouble) && TryConvertToDouble(expected, out double expectedDouble) ) { comparisonResult = actualDouble.CompareTo(expectedDouble); return true; } return false; } /// /// Converts a value to the target type. /// /// The type to convert to. /// The value to convert. /// True if conversion succeeded; false otherwise. /// The converted value, or null if conversion failed. public static object ConvertValue(Type targetType, object value, out bool success) { success = true; if (value == null) { if (targetType.IsValueType && Nullable.GetUnderlyingType(targetType) == null) { success = false; } return null; } if (targetType.IsInstanceOfType(value)) { return value; } try { if (targetType.IsEnum) { Type underlyingType = Enum.GetUnderlyingType(targetType); object numericValue = Convert.ChangeType(value, underlyingType); return Enum.ToObject(targetType, numericValue); } return Convert.ChangeType(value, targetType); } catch { success = false; return null; } } /// /// Tries to convert a value to a double. /// /// The value to convert. /// When this method returns true, contains the converted double. /// True if conversion succeeded; false otherwise. public static bool TryConvertToDouble(object value, out double result) { result = 0d; if (value == null) { return false; } try { result = Convert.ToDouble(value); return true; } catch { return false; } } /// /// Tries to compare values using IComparable<T> interface. /// /// The left-hand side value. /// The right-hand side value. /// /// When this method returns true, contains the comparison result. /// /// /// If true, the comparison result is inverted (for when comparing rhs to lhs). /// /// True if comparison was successful; false otherwise. public static bool TryGenericComparableCompare( object lhs, object rhs, out int comparisonResult, bool invert ) { comparisonResult = 0; if (lhs == null) { return false; } Type lhsType = lhs.GetType(); if (!CompareToMethodCache.TryGetValue(lhsType, out MethodInfo compareTo)) { compareTo = FindCompareToMethod(lhsType); CompareToMethodCache[lhsType] = compareTo; } if (compareTo == null) { return false; } Type genericArgument = compareTo.GetParameters()[0].ParameterType; object converted = ConvertValue(genericArgument, rhs, out bool success); if (!success) { return false; } try { object[] args = new object[1]; args[0] = converted; object compareResult = compareTo.Invoke(lhs, args); comparisonResult = Convert.ToInt32(compareResult); if (invert) { comparisonResult = -comparisonResult; } return true; } catch { return false; } } /// /// Finds the CompareTo method for IComparable<T> on a type. /// /// The type to search for CompareTo method. /// The CompareTo method, or null if not found. public static MethodInfo FindCompareToMethod(Type type) { Type[] interfaces = type.GetInterfaces(); for (int index = 0; index < interfaces.Length; index++) { Type iface = interfaces[index]; if ( !iface.IsGenericType || iface.GetGenericTypeDefinition() != typeof(IComparable<>) ) { continue; } Type genericArgument = iface.GetGenericArguments()[0]; Type[] paramTypes = new Type[1]; paramTypes[0] = genericArgument; MethodInfo method = iface.GetMethod("CompareTo", paramTypes); if (method != null) { return method; } } return null; } /// /// Checks if a value is null, with special handling for Unity objects. /// /// The value to check. /// True if value is null or a destroyed Unity object. public static bool IsNull(object value) { if (value == null) { return true; } // Use ReferenceEquals to check if the cast succeeded, avoiding Unity's // overloaded == operator which returns true for destroyed objects. // We want to detect destroyed objects here, not skip them. UnityEngine.Object unityObject = value as UnityEngine.Object; if (!ReferenceEquals(unityObject, null)) { // Unity's == operator returns true for destroyed objects return unityObject == null; } return false; } /// /// Checks if a value is null or empty (for strings, collections, and enumerables). /// /// The value to check. /// True if value is null, empty string, or empty collection. public static bool IsNullOrEmpty(object value) { if (IsNull(value)) { return true; } string stringValue = value as string; if (stringValue != null) { return stringValue.Length == 0; } ICollection collection = value as ICollection; if (collection != null) { return collection.Count == 0; } IEnumerable enumerable = value as IEnumerable; if (enumerable != null) { IEnumerator enumerator = enumerable.GetEnumerator(); try { return !enumerator.MoveNext(); } finally { IDisposable disposable = enumerator as IDisposable; if (disposable != null) { disposable.Dispose(); } } } return false; } } #endif }