Streamingle_URP/Assets/NiloToonURP/Editor/Attribute/RevertiblePropertyDrawer.cs

510 lines
20 KiB
C#

// For how to use [Revertible], see RevertibleAttribute.cs
using UnityEngine;
using UnityEditor;
using System.Reflection;
using System;
using System.Linq;
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(RevertibleAttribute))]
public class RevertiblePropertyDrawer : PropertyDrawer
{
private const float BUTTON_WIDTH = 20f;
private const float SPACING = 2f;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
// Get the default value
object defaultValue = GetDefaultValue(property);
// Check if current value differs from default
bool isDifferentFromDefault = !IsEqualToDefault(property, defaultValue);
// Calculate rects
Rect propertyRect = position;
if (isDifferentFromDefault)
{
propertyRect.width -= BUTTON_WIDTH + SPACING;
}
// Draw the property field with other attributes (like Range)
DrawPropertyWithOtherAttributes(propertyRect, property, label);
// Draw revert button if value is different from default
if (isDifferentFromDefault)
{
Rect buttonRect = new Rect(
position.x + position.width - BUTTON_WIDTH,
position.y,
BUTTON_WIDTH,
EditorGUIUtility.singleLineHeight
);
// Create tooltip
GUIContent buttonContent = new GUIContent("↺", "Revert to default value");
// Alternative symbols you can use:
// "⟲" - circle arrow
// "↶" - anticlockwise arrow
// "⎌" - undo symbol
// "✕" - X mark
// "⌫" - delete/backspace
// "◀" - back arrow
// Style the button
GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
buttonStyle.fontSize = 14;
buttonStyle.fontStyle = FontStyle.Bold;
buttonStyle.padding = new RectOffset(0, 0, 0, 0);
// Use orange tint for visibility
Color oldColor = GUI.color;
GUI.color = new Color(1f, 0.8f, 0.5f, 1f); // Orange tint
if (GUI.Button(buttonRect, buttonContent, buttonStyle))
{
RevertToDefault(property, defaultValue);
}
GUI.color = oldColor;
}
}
private void DrawPropertyWithOtherAttributes(Rect position, SerializedProperty property, GUIContent label)
{
// Get the field info
FieldInfo fieldInfo = GetFieldInfo(property);
if (fieldInfo == null)
{
EditorGUI.PropertyField(position, property, label, true);
return;
}
// Check for Range attribute
RangeAttribute rangeAttribute = fieldInfo.GetCustomAttribute<RangeAttribute>();
if (rangeAttribute != null)
{
if (property.propertyType == SerializedPropertyType.Float)
{
EditorGUI.Slider(position, property, rangeAttribute.min, rangeAttribute.max, label);
}
else if (property.propertyType == SerializedPropertyType.Integer)
{
EditorGUI.IntSlider(position, property, (int)rangeAttribute.min, (int)rangeAttribute.max, label);
}
else
{
EditorGUI.PropertyField(position, property, label, true);
}
return;
}
// Check for Min attribute (Unity 2021.2+)
#if UNITY_2021_2_OR_NEWER
MinAttribute minAttribute = fieldInfo.GetCustomAttribute<MinAttribute>();
if (minAttribute != null)
{
EditorGUI.BeginChangeCheck();
EditorGUI.PropertyField(position, property, label, true);
if (EditorGUI.EndChangeCheck())
{
if (property.propertyType == SerializedPropertyType.Float)
{
property.floatValue = Mathf.Max(property.floatValue, minAttribute.min);
}
else if (property.propertyType == SerializedPropertyType.Integer)
{
property.intValue = Mathf.Max(property.intValue, (int)minAttribute.min);
}
}
return;
}
#endif
// Check for TextArea attribute
TextAreaAttribute textAreaAttribute = fieldInfo.GetCustomAttribute<TextAreaAttribute>();
if (textAreaAttribute != null && property.propertyType == SerializedPropertyType.String)
{
position.height = EditorGUIUtility.singleLineHeight * (textAreaAttribute.maxLines + 1);
property.stringValue = EditorGUI.TextArea(position, label.text, property.stringValue);
return;
}
// Check for Multiline attribute
MultilineAttribute multilineAttribute = fieldInfo.GetCustomAttribute<MultilineAttribute>();
if (multilineAttribute != null && property.propertyType == SerializedPropertyType.String)
{
position.height = EditorGUIUtility.singleLineHeight * multilineAttribute.lines;
property.stringValue = EditorGUI.TextArea(position, label.text, property.stringValue);
return;
}
// Default property field
EditorGUI.PropertyField(position, property, label, true);
}
private FieldInfo GetFieldInfo(SerializedProperty property)
{
Type targetType = property.serializedObject.targetObject.GetType();
string[] path = property.propertyPath.Split('.');
FieldInfo field = null;
Type currentType = targetType;
for (int i = 0; i < path.Length; i++)
{
// Skip array element paths
if (path[i].Contains("["))
continue;
field = currentType.GetField(path[i],
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (field != null && i < path.Length - 1)
{
currentType = field.FieldType;
// Handle arrays and lists
if (currentType.IsArray)
{
currentType = currentType.GetElementType();
}
else if (currentType.IsGenericType && currentType.GetGenericTypeDefinition() == typeof(System.Collections.Generic.List<>))
{
currentType = currentType.GetGenericArguments()[0];
}
}
}
return field;
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
FieldInfo fieldInfo = GetFieldInfo(property);
if (fieldInfo != null)
{
// Check for TextArea attribute
TextAreaAttribute textAreaAttribute = fieldInfo.GetCustomAttribute<TextAreaAttribute>();
if (textAreaAttribute != null && property.propertyType == SerializedPropertyType.String)
{
return EditorGUIUtility.singleLineHeight * (textAreaAttribute.maxLines + 1);
}
// Check for Multiline attribute
MultilineAttribute multilineAttribute = fieldInfo.GetCustomAttribute<MultilineAttribute>();
if (multilineAttribute != null && property.propertyType == SerializedPropertyType.String)
{
return EditorGUIUtility.singleLineHeight * multilineAttribute.lines;
}
}
return EditorGUI.GetPropertyHeight(property, label, true);
}
private object GetDefaultValue(SerializedProperty property)
{
// Get the target object type
Type targetType = property.serializedObject.targetObject.GetType();
// Check if it's a MonoBehaviour or Component - if so, create temp GameObject
if (typeof(MonoBehaviour).IsAssignableFrom(targetType) ||
typeof(Component).IsAssignableFrom(targetType))
{
GameObject tempGO = new GameObject("TempForDefaults");
tempGO.hideFlags = HideFlags.HideAndDontSave;
try
{
Component tempComponent = SafeAddComponentWithRequirements(tempGO,targetType);
// Navigate through the property path to get the field
string[] path = property.propertyPath.Split('.');
object currentObject = tempComponent;
FieldInfo field = null;
for (int i = 0; i < path.Length; i++)
{
// Handle array elements
if (path[i].Contains("["))
{
return GetDefaultValueForType(property.propertyType);
}
field = currentObject.GetType().GetField(path[i],
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (field != null && i < path.Length - 1)
{
currentObject = field.GetValue(currentObject);
if (currentObject == null)
{
return GetDefaultValueForType(property.propertyType);
}
}
}
if (field != null)
{
return field.GetValue(currentObject);
}
}
finally
{
UnityEngine.Object.DestroyImmediate(tempGO);
}
return GetDefaultValueForType(property.propertyType);
}
// Check if it's a ScriptableObject (including ScriptableRendererFeature)
if (typeof(ScriptableObject).IsAssignableFrom(targetType))
{
ScriptableObject tempInstance = ScriptableObject.CreateInstance(targetType);
try
{
// Navigate through the property path to get the field
string[] path = property.propertyPath.Split('.');
object currentObject = tempInstance;
FieldInfo field = null;
for (int i = 0; i < path.Length; i++)
{
// Handle array elements
if (path[i].Contains("["))
{
return GetDefaultValueForType(property.propertyType);
}
field = currentObject.GetType().GetField(path[i],
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (field != null && i < path.Length - 1)
{
currentObject = field.GetValue(currentObject);
if (currentObject == null)
{
return GetDefaultValueForType(property.propertyType);
}
}
}
if (field != null)
{
return field.GetValue(currentObject);
}
}
finally
{
UnityEngine.Object.DestroyImmediate(tempInstance);
}
return GetDefaultValueForType(property.propertyType);
}
// For other types, use Activator.CreateInstance
object tempInstance2 = Activator.CreateInstance(targetType);
// Navigate through the property path to get the field
string[] path2 = property.propertyPath.Split('.');
object currentObject2 = tempInstance2;
FieldInfo field2 = null;
for (int i = 0; i < path2.Length; i++)
{
// Handle array elements
if (path2[i].Contains("["))
{
return GetDefaultValueForType(property.propertyType);
}
field2 = currentObject2.GetType().GetField(path2[i],
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (field2 != null && i < path2.Length - 1)
{
currentObject2 = field2.GetValue(currentObject2);
if (currentObject2 == null)
{
return GetDefaultValueForType(property.propertyType);
}
}
}
if (field2 != null)
{
return field2.GetValue(currentObject2);
}
return GetDefaultValueForType(property.propertyType);
}
private object GetDefaultValueForType(SerializedPropertyType propertyType)
{
switch (propertyType)
{
case SerializedPropertyType.Integer:
return 0;
case SerializedPropertyType.Float:
return 0f;
case SerializedPropertyType.Boolean:
return false;
case SerializedPropertyType.String:
return "";
case SerializedPropertyType.Vector2:
return Vector2.zero;
case SerializedPropertyType.Vector3:
return Vector3.zero;
case SerializedPropertyType.Vector4:
return Vector4.zero;
case SerializedPropertyType.Color:
return Color.white;
default:
return null;
}
}
private bool IsEqualToDefault(SerializedProperty property, object defaultValue)
{
if (defaultValue == null)
return false;
switch (property.propertyType)
{
case SerializedPropertyType.Integer:
return property.intValue == (int)defaultValue;
case SerializedPropertyType.Float:
return Mathf.Approximately(property.floatValue, (float)defaultValue);
case SerializedPropertyType.Boolean:
return property.boolValue == (bool)defaultValue;
case SerializedPropertyType.String:
return property.stringValue == (string)defaultValue;
case SerializedPropertyType.Vector2:
return property.vector2Value == (Vector2)defaultValue;
case SerializedPropertyType.Vector3:
return property.vector3Value == (Vector3)defaultValue;
case SerializedPropertyType.Vector4:
return property.vector4Value == (Vector4)defaultValue;
case SerializedPropertyType.Color:
return property.colorValue == (Color)defaultValue;
case SerializedPropertyType.ObjectReference:
return property.objectReferenceValue == (UnityEngine.Object)defaultValue;
case SerializedPropertyType.Enum:
// Handle enum properly - convert enum value to name then compare
if (defaultValue is Enum enumValue)
{
string defaultEnumName = enumValue.ToString();
string currentEnumName = property.enumNames.Length > property.enumValueIndex && property.enumValueIndex >= 0
? property.enumNames[property.enumValueIndex]
: "";
return defaultEnumName == currentEnumName;
}
return false;
default:
return false;
}
}
private void RevertToDefault(SerializedProperty property, object defaultValue)
{
if (defaultValue == null)
return;
Undo.RecordObject(property.serializedObject.targetObject, "Revert to Default");
switch (property.propertyType)
{
case SerializedPropertyType.Integer:
property.intValue = (int)defaultValue;
break;
case SerializedPropertyType.Float:
property.floatValue = (float)defaultValue;
break;
case SerializedPropertyType.Boolean:
property.boolValue = (bool)defaultValue;
break;
case SerializedPropertyType.String:
property.stringValue = (string)defaultValue;
break;
case SerializedPropertyType.Vector2:
property.vector2Value = (Vector2)defaultValue;
break;
case SerializedPropertyType.Vector3:
property.vector3Value = (Vector3)defaultValue;
break;
case SerializedPropertyType.Vector4:
property.vector4Value = (Vector4)defaultValue;
break;
case SerializedPropertyType.Color:
property.colorValue = (Color)defaultValue;
break;
case SerializedPropertyType.ObjectReference:
property.objectReferenceValue = (UnityEngine.Object)defaultValue;
break;
case SerializedPropertyType.Enum:
// Handle enum properly - find the index by name
if (defaultValue is Enum enumValue)
{
string enumName = enumValue.ToString();
int index = Array.IndexOf(property.enumNames, enumName);
if (index >= 0)
{
property.enumValueIndex = index;
}
else
{
// If exact name not found, try to use the integer value if it's valid
int intValue = Convert.ToInt32(defaultValue);
if (intValue >= 0 && intValue < property.enumNames.Length)
{
property.enumValueIndex = intValue;
}
else
{
// Fall back to first enum value
property.enumValueIndex = 0;
}
}
}
break;
}
property.serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(property.serializedObject.targetObject);
}
private Component SafeAddComponentWithRequirements(GameObject go, Type targetType)
{
// Add any required dependencies first
var requireAttrs = (RequireComponent[])targetType.GetCustomAttributes(typeof(RequireComponent), true);
foreach (var attr in requireAttrs)
{
if (attr.m_Type0 != null) SafeAddComponentWithRequirements(go, attr.m_Type0);
if (attr.m_Type1 != null) SafeAddComponentWithRequirements(go, attr.m_Type1);
if (attr.m_Type2 != null) SafeAddComponentWithRequirements(go, attr.m_Type2);
}
// Skip if already added
Component existing = go.GetComponent(targetType);
if (existing != null)
return existing;
// Handle abstract or problematic base types (like Renderer)
if (targetType == typeof(Renderer))
{
return go.AddComponent<MeshRenderer>(); // safe default
}
// Skip unsupported abstract classes and interfaces
if (targetType.IsAbstract || targetType.IsInterface)
{
Debug.LogWarning($"[Revertible] Skipped adding abstract/interface component type '{targetType}'.");
return null;
}
// Finally, add the actual component
return go.AddComponent(targetType);
}
}
#endif