using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
namespace OM.Editor
{
///
/// A comprehensive utility class for extending Unity's SerializedProperty.
/// Adds support for setting/getting values, copying, reflection, traversal, and attribute access.
///
public static class OM_SerializedPropertyExtensions
{
private static readonly Regex PropertyPathRegex = new Regex(@"(?<=\.|^)(?:Array\.data\[\d+\]|[^.]+)");
/// Resets the property's value to its default.
public static void ResetPropertyValue(this SerializedProperty property)
{
switch (property.propertyType)
{
case SerializedPropertyType.Integer:
property.intValue = 0;
break;
case SerializedPropertyType.Vector3:
property.vector3Value = Vector3.zero;
break;
case SerializedPropertyType.Vector2:
property.vector2Value = Vector2.zero;
break;
case SerializedPropertyType.Float:
property.floatValue = 0f;
break;
case SerializedPropertyType.Boolean:
property.boolValue = false;
break;
case SerializedPropertyType.String:
property.stringValue = string.Empty;
break;
case SerializedPropertyType.Color:
property.colorValue = Color.white;
break;
case SerializedPropertyType.ObjectReference:
property.objectReferenceValue = null;
break;
case SerializedPropertyType.Enum:
property.enumValueIndex = 0;
break;
// Add more cases as needed
}
property.serializedObject.ApplyModifiedPropertiesWithoutUndo();
}
/// Sets the property's value using boxed object input.
public static void SetValue(this SerializedProperty property, object value)
{
switch (property.propertyType)
{
case SerializedPropertyType.Integer:
property.intValue = (int)value;
break;
case SerializedPropertyType.Boolean:
property.boolValue = (bool)value;
break;
case SerializedPropertyType.Float:
property.floatValue = (float)value;
break;
case SerializedPropertyType.String:
property.stringValue = (string)value;
break;
case SerializedPropertyType.Color:
property.colorValue = (Color)value;
break;
case SerializedPropertyType.ObjectReference:
property.objectReferenceValue = value as UnityEngine.Object;
break;
case SerializedPropertyType.LayerMask:
property.intValue = (int)value;
break;
case SerializedPropertyType.Enum:
property.enumValueIndex = (int)value;
break;
case SerializedPropertyType.Vector2:
property.vector2Value = (Vector2)value;
break;
case SerializedPropertyType.Vector3:
property.vector3Value = (Vector3)value;
break;
case SerializedPropertyType.Vector4:
property.vector4Value = (Vector4)value;
break;
case SerializedPropertyType.Rect:
property.rectValue = (Rect)value;
break;
case SerializedPropertyType.ArraySize:
property.arraySize = (int)value;
break;
case SerializedPropertyType.Character:
property.stringValue = (string)value;
break;
case SerializedPropertyType.AnimationCurve:
property.animationCurveValue = (AnimationCurve)value;
break;
case SerializedPropertyType.Bounds:
property.boundsValue = (Bounds)value;
break;
case SerializedPropertyType.Gradient:
property.gradientValue = (Gradient)value;
break;
case SerializedPropertyType.Quaternion:
property.quaternionValue = (Quaternion)value;
break;
case SerializedPropertyType.ExposedReference:
property.exposedReferenceValue = (UnityEngine.Object)value;
break;
case SerializedPropertyType.Vector2Int:
property.vector2IntValue = (Vector2Int)value;
break;
case SerializedPropertyType.Vector3Int:
property.vector3IntValue = (Vector3Int)value;
break;
case SerializedPropertyType.RectInt:
property.rectIntValue = (RectInt)value;
break;
case SerializedPropertyType.BoundsInt:
property.boundsIntValue = (BoundsInt)value;
break;
default:
Debug.LogWarning($"Unsupported property type: {property.propertyType}");
break;
}
property.serializedObject.ApplyModifiedProperties();
}
/// Gets the current value of the property as a boxed object.
public static object GetValue(this SerializedProperty property)
{
if (property?.serializedObject == null || property.serializedObject.targetObject == null)
{
return null;
}
switch (property.propertyType)
{
case SerializedPropertyType.Integer:
return property.intValue;
case SerializedPropertyType.Boolean:
return property.boolValue;
case SerializedPropertyType.Float:
return property.floatValue;
case SerializedPropertyType.String:
return property.stringValue;
case SerializedPropertyType.Color:
return property.colorValue;
case SerializedPropertyType.ObjectReference:
return property.objectReferenceValue;
case SerializedPropertyType.LayerMask:
return property.intValue;
case SerializedPropertyType.Enum:
return property.enumValueIndex;
case SerializedPropertyType.Vector2:
return property.vector2Value;
case SerializedPropertyType.Vector3:
return property.vector3Value;
case SerializedPropertyType.Vector4:
return property.vector4Value;
case SerializedPropertyType.Rect:
return property.rectValue;
case SerializedPropertyType.ArraySize:
return property.arraySize;
case SerializedPropertyType.Character:
return property.stringValue;
case SerializedPropertyType.AnimationCurve:
return property.animationCurveValue;
case SerializedPropertyType.Bounds:
return property.boundsValue;
case SerializedPropertyType.Gradient:
return property.gradientValue;
case SerializedPropertyType.Quaternion:
return property.quaternionValue;
case SerializedPropertyType.ExposedReference:
return property.exposedReferenceValue;
case SerializedPropertyType.Vector2Int:
return property.vector2IntValue;
case SerializedPropertyType.Vector3Int:
return property.vector3IntValue;
case SerializedPropertyType.RectInt:
return property.rectIntValue;
case SerializedPropertyType.BoundsInt:
return property.boundsIntValue;
default:
return property.GetValueCustom();
}
}
/// Copies the value from one SerializedProperty to another.
public static void CopyFromSerializedProperty(this SerializedProperty to, SerializedProperty from)
{
switch (to.propertyType)
{
case SerializedPropertyType.Integer:
to.intValue = from.intValue;
break;
case SerializedPropertyType.Boolean:
to.boolValue = from.boolValue;
break;
case SerializedPropertyType.Float:
to.floatValue = from.floatValue;
break;
case SerializedPropertyType.String:
to.stringValue = from.stringValue;
break;
case SerializedPropertyType.Color:
to.colorValue = from.colorValue;
break;
case SerializedPropertyType.ObjectReference:
to.objectReferenceValue = from.objectReferenceValue;
break;
case SerializedPropertyType.LayerMask:
to.intValue = from.intValue;
break;
case SerializedPropertyType.Enum:
to.enumValueIndex = from.enumValueIndex;
break;
case SerializedPropertyType.Vector2:
to.vector2Value = from.vector2Value;
break;
case SerializedPropertyType.Vector3:
to.vector3Value = from.vector3Value;
break;
case SerializedPropertyType.Vector4:
to.vector4Value = from.vector4Value;
break;
case SerializedPropertyType.Rect:
to.rectValue = from.rectValue;
break;
case SerializedPropertyType.ArraySize:
to.arraySize = from.arraySize;
break;
case SerializedPropertyType.Character:
to.stringValue = from.stringValue;
break;
case SerializedPropertyType.AnimationCurve:
to.animationCurveValue = from.animationCurveValue;
break;
case SerializedPropertyType.Bounds:
to.boundsValue = from.boundsValue;
break;
case SerializedPropertyType.Gradient:
to.gradientValue = from.gradientValue;
break;
case SerializedPropertyType.Quaternion:
to.quaternionValue = from.quaternionValue;
break;
case SerializedPropertyType.ExposedReference:
to.exposedReferenceValue = from.exposedReferenceValue;
break;
case SerializedPropertyType.Vector2Int:
to.vector2IntValue = from.vector2IntValue;
break;
case SerializedPropertyType.Vector3Int:
to.vector3IntValue = from.vector3IntValue;
break;
case SerializedPropertyType.RectInt:
to.rectIntValue = from.rectIntValue;
break;
case SerializedPropertyType.BoundsInt:
to.boundsIntValue = from.boundsIntValue;
break;
default:
Debug.LogWarning($"Unsupported property type: {to.propertyType}");
break;
}
}
/// Enumerates all visible children of a property.
public static IEnumerable GetAllProperties(this SerializedProperty property,
bool enterChildren = true)
{
if (property == null)
{
yield break;
}
var currentProperty = property.Copy();
var startDepth = currentProperty.depth;
while (currentProperty.NextVisible(enterChildren) && currentProperty.depth > startDepth)
{
enterChildren = false;
yield return currentProperty;
}
}
/// Gets properties by name from a SerializedObject.
public static IEnumerable GetPropertiesByName(this SerializedObject targetSerializedObject,params string[] propertyNames)
{
foreach (var propertyName in propertyNames)
{
var serializedProperty = targetSerializedObject.FindProperty(propertyName);
if (serializedProperty != null) yield return serializedProperty;
}
}
/// Gets properties by name from a SerializedProperty.
public static IEnumerable GetPropertiesByName(this SerializedProperty targetProperty,params string[] propertyNames)
{
foreach (var propertyName in propertyNames)
{
var serializedProperty = targetProperty.FindPropertyRelative(propertyName);
if (serializedProperty != null) yield return serializedProperty;
}
}
/// Enumerates all visible top-level properties of a SerializedObject.
public static IEnumerable GetAllProperties(this SerializedObject serializedObject,
bool enterChildren = true)
{
if (serializedObject == null)
{
yield break;
}
var currentProperty = serializedObject.GetIterator();
var startDepth = currentProperty.depth;
while (currentProperty.NextVisible(enterChildren) && currentProperty.depth > startDepth)
{
enterChildren = false;
yield return currentProperty;
}
}
/// Splits a property path into its individual segments.
public static string[] SplitPropertyPath(string propertyPath)
{
if (string.IsNullOrEmpty(propertyPath)) return new string[0];
var matches = PropertyPathRegex.Matches(propertyPath);
var parts = new List();
foreach (Match match in matches)
{
parts.Add(match.Value);
}
return parts.ToArray();
}
/// Gets the parent SerializedProperty of a nested property.
public static SerializedProperty GetParentProperty(this SerializedProperty property)
{
if (property == null) return null;
string[] parts = SplitPropertyPath(property.propertyPath);
if (parts.Length <= 1) return null;
SerializedProperty parent = property.serializedObject.FindProperty(parts[0]);
for (var i = 1; i < parts.Length - 1; i++)
{
parent = parent.FindPropertyRelative(parts[i]);
}
return parent;
}
/// Gets the FieldInfo of the parent of the property.
public static FieldInfo GetFieldInfoOfTheParent(this SerializedProperty property,bool checkAllParents)
{
if (property == null) return null;
object obj = property.serializedObject.targetObject;
if (property.propertyPath.Contains(".") == false)
{
var type = obj.GetType();
FieldInfo field = null;
if (checkAllParents)
{
field = type.GetFieldInfoFromAllParents(property.name);
}
else
{
field = type.GetField(property.name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
}
return field;
}
var parts = SplitPropertyPath(property.propertyPath);
FieldInfo lastField = null;
for (var index = 0; index < parts.Length - 1; index++)
{
var part = parts[index];
Debug.Log(part);
if (obj == null) return null;
if (part.StartsWith("Array.data["))
{
// Handle arrays and lists
obj = HandleArrayOrList(part, obj);
}
else
{
// Handle regular fields
var type = obj.GetType();
FieldInfo field = null;
if (checkAllParents)
{
field = type.GetFieldInfoFromAllParents(part);
}
else
{
field = type.GetField(part, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
}
if (field == null) return null;
lastField = field; // Update the last valid FieldInfo
obj = field.GetValue(obj); // Traverse deeper
}
}
return lastField;
}
/// Gets the FieldInfo for a property based on a specific object.
public static FieldInfo GetFieldInfo(this SerializedProperty property,object target,bool checkAllParents)
{
if (property == null) return null;
var type1 = target.GetType();
if (checkAllParents)
{
return type1.GetFieldInfoFromAllParents(property.name);
}
var fieldInfo = type1.GetField(property.name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
return fieldInfo;
}
/// Gets FieldInfo from all parent types in hierarchy.
public static FieldInfo GetFieldInfoFromAllParents(this Type type, string fieldName)
{
if (type == null || string.IsNullOrEmpty(fieldName)) return null;
// Traverse up the class hierarchy
while (type != null)
{
// Look for the field in the current type (including private, public, and protected fields)
var field = type.GetField(fieldName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
if (field != null)
{
return field; // Return the found field
}
// Move up to the parent class
type = type.BaseType;
}
return null; // Return null if not found in the class hierarchy
}
/// Gets the FieldInfo of the final property segment using reflection.
public static FieldInfo GetFieldInfo(this SerializedProperty property,bool checkAllParents)
{
if (property == null) return null;
object obj = property.serializedObject.targetObject;
string[] parts = SplitPropertyPath(property.propertyPath);
FieldInfo lastField = null;
foreach (var part in parts)
{
if (obj == null) return null;
if (part.StartsWith("Array.data["))
{
// Handle arrays and lists
obj = HandleArrayOrList(part, obj);
}
else
{
// Handle regular fields
var type = obj.GetType();
FieldInfo field = null;
if (checkAllParents)
{
field = type.GetFieldInfoFromAllParents(part);
}
else
{
field = type.GetField(part, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
}
if (field == null) return null;
lastField = field; // Update the last valid FieldInfo
obj = field.GetValue(obj); // Traverse deeper
}
}
return lastField;
}
/// Returns the parent value object of a serialized property.
public static object GetParentValue(this SerializedProperty property)
{
if (property == null) return null;
var parent = property.GetParentProperty();
if (parent != null)
{
return parent != null ? parent.GetValue() : null;
}
return property.serializedObject.targetObject;
}
public static bool IsSerializedPropertyValid(this SerializedProperty property)
{
if (property == null) return false;
if (property.serializedObject == null) return false;
if (property.serializedObject.targetObject == null) return false;
try
{
var _ = property.propertyPath;
}
catch
{
return false;
}
return true;
}
/// Uses custom reflection logic to retrieve the actual value behind a property path.
public static object GetValueCustom(this SerializedProperty property,bool checkAllParents = true,bool debug = false)
{
if (property == null) return null;
object obj = property.serializedObject.targetObject;
string[] parts = SplitPropertyPath(property.propertyPath);
if (debug)
{
Debug.Log(parts.Length);
var stringBuilder = new StringBuilder();
foreach (var part in parts)
{
stringBuilder.Append(part);
stringBuilder.Append(" - ");
}
Debug.Log(stringBuilder.ToString());
}
foreach (var part in parts)
{
if(debug) Debug.Log(part);
if (obj == null)
{
if(debug) Debug.Log("Object is null");
return null;
}
if (part.Contains("Array"))
{
// Handle arrays and lists
obj = HandleArrayOrList(part, obj);
}
else
{
// Handle regular fields
var type = obj.GetType();
FieldInfo field = null;
if (checkAllParents)
{
field = type.GetFieldInfoFromAllParents(part);
}
else
{
field = type.GetField(part, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
}
//var field = type.GetField(part, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (field == null)
{
if(debug) Debug.Log("Field is null Part: " + part + " Type: " + type + " Object: " + obj);
return null;
}
obj = field.GetValue(obj);
}
}
if(debug) Debug.Log("Returning: " + obj);
return obj;
}
/// Handles list/array element access for reflection.
private static object HandleArrayOrList(string part, object obj)
{
if (!part.StartsWith("Array.data["))
{
return obj;
}
// Extract the index from the "Array.data[index]" format
int startIdx = part.IndexOf('[') + 1;
int endIdx = part.IndexOf(']');
if (startIdx < 0 || endIdx < 0) return null;
if (int.TryParse(part.Substring(startIdx, endIdx - startIdx), out int index))
{
if (obj is IList list && index >= 0 && index < list.Count)
{
return list[index];
}
}
return null;
}
/// Gets a single attribute of the specified type from the property’s field.
public static T GetAttribute(this SerializedProperty property,bool checkAllParents) where T : Attribute
{
var fieldInfo = GetFieldInfo(property,checkAllParents);
return fieldInfo != null ? fieldInfo.GetCustomAttribute() : null;
}
/// Gets all attributes applied to the property's field.
public static Attribute[] GetAttributes(this SerializedProperty property,bool checkAllParents)
{
var fieldInfo = GetFieldInfo(property,checkAllParents);
if (fieldInfo != null)
{
return (Attribute[])fieldInfo.GetCustomAttributes(false);
}
return Array.Empty();
}
/// Finds a nested property by string path, supporting array access.
public static SerializedProperty GetSerializedPropertyByPath(this SerializedObject target, string path)
{
if (target == null || string.IsNullOrEmpty(path))
return null;
string[] parts = path.Split('.');
SerializedProperty property = target.FindProperty(parts[0]);
for (int i = 1; i < parts.Length; i++)
{
if (property == null)
return null;
// Handle array indexing
if (parts[i].StartsWith("Array.data["))
{
// Extract the index from "Array.data[index]"
int startIndex = parts[i].IndexOf('[') + 1;
int endIndex = parts[i].IndexOf(']');
if (startIndex >= 0 && endIndex > startIndex)
{
string indexString = parts[i].Substring(startIndex, endIndex - startIndex);
if (int.TryParse(indexString, out int index))
{
property = property.GetArrayElementAtIndex(index);
}
else
{
Debug.LogError($"Invalid array index in path: {parts[i]}");
return null;
}
}
else
{
Debug.LogError($"Malformed array path segment: {parts[i]}");
return null;
}
}
else
{
// Handle regular property access
property = property.FindPropertyRelative(parts[i]);
}
}
return property;
}
}
}