3using System.Collections;
4using System.Collections.Generic;
6using System.Reflection;
7using System.Runtime.CompilerServices;
11#pragma warning disable IDE0130
13#pragma warning restore IDE0130
19 internal static class ConditionalMemberEvaluator
23 private static readonly FieldInfo customPropertyDrawerTypeField =
24 typeof(CustomPropertyDrawer).GetField(
"m_Type", BindingFlags.Instance | BindingFlags.NonPublic);
26 private static readonly FieldInfo customPropertyDrawerUseForChildrenField =
27 typeof(CustomPropertyDrawer).GetField(
"m_UseForChildren", BindingFlags.Instance | BindingFlags.NonPublic);
29 private static readonly FieldInfo propertyDrawerFieldInfoField =
30 typeof(PropertyDrawer).GetField(
"m_FieldInfo", BindingFlags.Instance | BindingFlags.NonPublic);
32 private static readonly BuiltInDrawerRegistration[] builtInDrawerRegistrations = BuildBuiltInDrawerRegistrations();
34 private static readonly Type[] builtInDrawerConflictTypes = builtInDrawerRegistrations
35 .Select(registration => registration.TargetType)
39 private static readonly Dictionary<string, PropertyDrawer> builtInDrawerCache =
new();
40 private static readonly Dictionary<Type, MethodInfo> builtInDrawerOnGuiMethodCache =
new();
41 private static readonly Dictionary<Type, MethodInfo> builtInDrawerGetPropertyHeightMethodCache =
new();
43 private static readonly Type[] onGuiParameterTypes =
46 typeof(SerializedProperty),
50 private static readonly Type[] getPropertyHeightParameterTypes =
52 typeof(SerializedProperty),
65 internal static bool ShouldShow(SerializedProperty property, ShowIfAttribute attribute) =>
66 attribute ==
null ||
property ==
null ||
67 !TryEvaluate(property, attribute.MemberName, Array.Empty<
object>(), out var conditionMet) ||
79 internal static bool ShouldEnable(SerializedProperty property, EnableIfAttribute attribute) =>
80 attribute ==
null ||
property ==
null ||
81 !TryEvaluate(property, attribute.MemberName, attribute.ExpectedValues, out var conditionMet) ||
92 internal static bool TryGetBuiltInDrawer(SerializedProperty property, FieldInfo fieldInfo, out PropertyDrawer drawer)
95 if (property ==
null || fieldInfo ==
null || !RequiresBuiltInDrawerDelegation(fieldInfo.FieldType) ||
96 !TryGetBuiltInDrawerRegistration(fieldInfo.FieldType, out var registration))
99 var cacheKey = GetBuiltInDrawerCacheKey(property, registration.DrawerType);
100 if (!
string.IsNullOrEmpty(cacheKey) && builtInDrawerCache.TryGetValue(cacheKey, out drawer))
101 return drawer !=
null;
103 drawer = CreateBuiltInDrawer(fieldInfo, registration);
107 if (!
string.IsNullOrEmpty(cacheKey))
108 builtInDrawerCache[cacheKey] = drawer;
121 internal static void DrawBuiltInDrawer(PropertyDrawer drawer, Rect position, SerializedProperty property, GUIContent label)
126 var method = GetDrawerMethod(drawer.GetType(), nameof(PropertyDrawer.OnGUI), builtInDrawerOnGuiMethodCache, onGuiParameterTypes);
127 method?.Invoke(drawer,
new object[] { position, property, label });
138 internal static float GetBuiltInDrawerHeight(PropertyDrawer drawer, SerializedProperty property, GUIContent label)
141 return EditorGUI.GetPropertyHeight(property, label,
true);
143 var method = GetDrawerMethod(
145 nameof(PropertyDrawer.GetPropertyHeight),
146 builtInDrawerGetPropertyHeightMethodCache,
147 getPropertyHeightParameterTypes);
148 return method?.Invoke(drawer,
new object[] { property, label }) is
float height
150 : EditorGUI.GetPropertyHeight(property, label, true);
155 private static bool TryEvaluate(
156 SerializedProperty property,
158 object[] expectedValues,
159 out
bool conditionMet)
161 conditionMet =
false;
162 if (property ==
null ||
string.IsNullOrEmpty(memberName))
165 if (!TryGetConditionValue(property, memberName, out var conditionValue))
168 conditionMet = MatchesExpectedValues(conditionValue, expectedValues);
172 private static bool TryGetConditionValue(SerializedProperty property,
string memberName, out
object value) =>
173 TryGetConditionFromSerializedProperty(property, memberName, out value) ||
174 TryGetConditionFromMember(GetTargetObjectOfProperty(property,
false), memberName, out value);
176 private static bool MatchesExpectedValues(
object conditionValue,
object[] expectedValues) =>
177 expectedValues ==
null || expectedValues.Length == 0
178 ? conditionValue is true
179 : expectedValues.Any(expectedValue => AreEqual(conditionValue, expectedValue));
181 private static bool AreEqual(
object left,
object right)
183 if (ReferenceEquals(left, right))
186 if (left ==
null || right ==
null)
189 if (left.Equals(right))
192 var leftText = left.ToString();
193 var rightText = right.ToString();
194 if (
string.Equals(leftText, rightText, StringComparison.Ordinal))
199 var convertedRight = Convert.ChangeType(right, left.GetType());
200 return left.Equals(convertedRight);
210 #region Built-In Drawers
214 private sealed class BuiltInDrawerRegistration
216 public Type TargetType {
get; }
217 public Type DrawerType {
get; }
218 public bool UseForChildren {
get; }
220 public BuiltInDrawerRegistration(Type targetType, Type drawerType,
bool useForChildren)
222 TargetType = targetType;
223 DrawerType = drawerType;
224 UseForChildren = useForChildren;
230 private static BuiltInDrawerRegistration[] BuildBuiltInDrawerRegistrations() => AppDomain.CurrentDomain
232 .Where(IsUnityAssembly)
233 .SelectMany(GetTypesSafely)
234 .Where(type => typeof(PropertyDrawer).IsAssignableFrom(type) && !type.IsAbstract)
235 .SelectMany(GetBuiltInDrawerRegistrations)
236 .GroupBy(registration => $
"{registration.TargetType.AssemblyQualifiedName}|{registration.DrawerType.AssemblyQualifiedName}|{registration.UseForChildren}")
237 .Select(group => group.First())
240 private static IEnumerable<Type> GetTypesSafely(Assembly assembly)
242 if (assembly ==
null)
243 return Array.Empty<Type>();
247 return assembly.GetTypes();
249 catch (ReflectionTypeLoadException exception)
251 return exception.Types.Where(type => type !=
null);
255 return Array.Empty<Type>();
259 private static IEnumerable<BuiltInDrawerRegistration> GetBuiltInDrawerRegistrations(Type drawerType)
261 if (drawerType ==
null || customPropertyDrawerTypeField ==
null || customPropertyDrawerUseForChildrenField ==
null)
264 var attributes = drawerType.GetCustomAttributes(typeof(CustomPropertyDrawer),
false);
265 foreach (var attribute
in attributes)
267 var targetType = customPropertyDrawerTypeField.GetValue(attribute) as Type;
268 if (targetType ==
null || typeof(PropertyAttribute).IsAssignableFrom(targetType))
271 yield
return new BuiltInDrawerRegistration(
274 customPropertyDrawerUseForChildrenField.GetValue(attribute) is
true);
278 private static bool TryGetBuiltInDrawerRegistration(Type fieldType, out BuiltInDrawerRegistration registration)
281 if (fieldType ==
null)
284 registration = builtInDrawerRegistrations.FirstOrDefault(entry => IsExactDrawerTarget(entry.TargetType, fieldType));
285 if (registration !=
null)
288 registration = builtInDrawerRegistrations
289 .Where(entry => entry.UseForChildren && MatchesDrawerTarget(entry.TargetType, fieldType))
290 .OrderByDescending(entry => GetSpecificity(entry.TargetType))
292 return registration !=
null;
295 private static bool RequiresBuiltInDrawerDelegation(Type fieldType) =>
297 builtInDrawerConflictTypes.Any(conflictType => IsExactDrawerTarget(conflictType, fieldType) || conflictType.IsAssignableFrom(fieldType));
299 private static bool IsExactDrawerTarget(Type targetType, Type fieldType) =>
300 targetType == fieldType ||
301 (targetType.IsGenericTypeDefinition &&
302 fieldType.IsGenericType &&
303 fieldType.GetGenericTypeDefinition() == targetType);
305 private static bool MatchesDrawerTarget(Type targetType, Type fieldType) =>
306 !targetType.IsGenericTypeDefinition &&
307 targetType.IsAssignableFrom(fieldType);
309 private static int GetSpecificity(Type type)
315 type = type.BaseType;
321 private static bool IsUnityAssembly(Assembly assembly)
323 var assemblyName = assembly?.GetName().Name;
324 return !
string.IsNullOrEmpty(assemblyName) &&
325 assemblyName.StartsWith(nameof(Unity), StringComparison.Ordinal);
328 private static PropertyDrawer CreateBuiltInDrawer(FieldInfo fieldInfo, BuiltInDrawerRegistration registration)
330 if (fieldInfo ==
null || registration?.DrawerType ==
null)
335 if (Activator.CreateInstance(registration.DrawerType,
true) is not PropertyDrawer drawer)
338 propertyDrawerFieldInfoField?.SetValue(drawer, fieldInfo);
347 private static MethodInfo GetDrawerMethod(
350 Dictionary<Type, MethodInfo> cache,
351 Type[] parameterTypes)
353 if (drawerType ==
null ||
string.IsNullOrEmpty(methodName) || cache ==
null || parameterTypes ==
null)
356 if (cache.TryGetValue(drawerType, out var method))
359 method = drawerType.GetMethod(
361 BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
365 cache[drawerType] = method;
369 private static string GetBuiltInDrawerCacheKey(SerializedProperty property, Type drawerType)
371 if (property ==
null || drawerType ==
null)
374 var targetObject =
property.serializedObject?.targetObject;
375 var targetId = targetObject !=
null
376 ? RuntimeHelpers.GetHashCode(targetObject)
378 return $
"{drawerType.AssemblyQualifiedName}|{targetId}|{property.propertyPath}";
383 #region SerializedProperty
385 private static bool TryGetConditionFromSerializedProperty(SerializedProperty property,
string memberName, out
object value)
388 if (property ==
null)
391 var conditionProperty = FindSiblingProperty(property, memberName);
392 return conditionProperty !=
null && TryGetSerializedPropertyValue(conditionProperty, out value);
395 private static bool TryGetSerializedPropertyValue(SerializedProperty property, out
object value)
398 if (property ==
null)
401 switch (property.propertyType)
403 case SerializedPropertyType.Boolean:
404 value =
property.boolValue;
406 case SerializedPropertyType.Integer:
407 value =
property.intValue;
409 case SerializedPropertyType.Float:
410 value =
property.floatValue;
412 case SerializedPropertyType.String:
413 value =
property.stringValue;
415 case SerializedPropertyType.Enum:
416 value = property.enumValueIndex >= 0 && property.enumValueIndex < property.enumNames.Length
417 ?
property.enumNames[
property.enumValueIndex]
418 :
property.enumValueIndex;
420 case SerializedPropertyType.Generic:
421 case SerializedPropertyType.Color:
422 case SerializedPropertyType.ObjectReference:
423 case SerializedPropertyType.LayerMask:
424 case SerializedPropertyType.Vector2:
425 case SerializedPropertyType.Vector3:
426 case SerializedPropertyType.Vector4:
427 case SerializedPropertyType.Rect:
428 case SerializedPropertyType.ArraySize:
429 case SerializedPropertyType.Character:
430 case SerializedPropertyType.AnimationCurve:
431 case SerializedPropertyType.Bounds:
432 case SerializedPropertyType.Gradient:
433 case SerializedPropertyType.Quaternion:
434 case SerializedPropertyType.ExposedReference:
435 case SerializedPropertyType.FixedBufferSize:
436 case SerializedPropertyType.Vector2Int:
437 case SerializedPropertyType.Vector3Int:
438 case SerializedPropertyType.RectInt:
439 case SerializedPropertyType.BoundsInt:
440 case SerializedPropertyType.ManagedReference:
441 case SerializedPropertyType.Hash128:
447 private static SerializedProperty FindSiblingProperty(SerializedProperty property,
string name)
449 var path =
property.propertyPath;
450 if (
string.IsNullOrEmpty(path))
453 var lastDot = path.LastIndexOf(
'.');
454 var parentPath = lastDot >= 0
455 ? path[..(lastDot + 1)]
457 return property.serializedObject.FindProperty(parentPath + name);
464 private static bool TryGetConditionFromMember(
object target,
string memberName, out
object value)
467 if (target ==
null ||
string.IsNullOrEmpty(memberName))
470 const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
471 var type = target.GetType();
474 var field = type.GetField(memberName, flags);
477 value = field.GetValue(target);
481 var
property = type.GetProperty(memberName, flags);
482 if (property !=
null && property.GetIndexParameters().Length == 0)
484 value =
property.GetValue(target,
null);
488 var method = type.GetMethod(memberName, flags,
null, Type.EmptyTypes,
null);
491 value = method.Invoke(target,
null);
495 type = type.BaseType;
501 private static object GetTargetObjectOfProperty(SerializedProperty property,
bool includeSelf)
503 if (property?.serializedObject?.targetObject is not
object target)
506 var path =
property.propertyPath.Replace(
".Array.data[",
"[");
507 var elements = path.Split(
'.');
508 var endIndex = includeSelf
510 : Math.Max(0, elements.Length - 1);
512 for (var i = 0; i < endIndex; i++)
514 var element = elements[i];
515 if (element.EndsWith(
"]"))
517 var bracketIndex = element.IndexOf(
'[');
518 var fieldName = element[..bracketIndex];
519 var indexText = element.Substring(bracketIndex + 1, element.Length - bracketIndex - 2);
520 if (!
int.TryParse(indexText, out var index))
523 target = GetIndexedValue(target, fieldName, index);
527 target = GetMemberValue(target, element);
537 private static object GetMemberValue(
object source,
string name)
539 if (source ==
null ||
string.IsNullOrEmpty(name))
542 const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
543 var type = source.GetType();
544 var field = type.GetField(name, flags);
546 return field.GetValue(source);
548 var
property = type.GetProperty(name, flags);
549 return property !=
null &&
property.GetIndexParameters().Length == 0
550 ?
property.GetValue(source,
null)
554 private static object GetIndexedValue(
object source,
string name,
int index)
556 if (GetMemberValue(source, name) is not IEnumerable enumerable)
559 var enumerator = enumerable.GetEnumerator();
560 using var disposableEnumerator = enumerator as IDisposable;
561 for (var i = 0; i <= index; i++)
563 if (!enumerator.MoveNext())
567 return enumerator.Current;