using System.Linq;
using UnityEditor;
using UnityEngine;
namespace UnityAtoms.Editor
{
///
/// A custom property drawer for References (Events and regular). Makes it possible to reference a resources (Variable or Event) through multiple options.
///
public abstract class AtomBaseReferenceDrawer : PropertyDrawer
{
protected abstract class UsageData
{
public abstract int Value { get; }
public abstract string PropertyName { get; }
public abstract string DisplayName { get; }
}
private const string USAGE_PROPERTY_NAME = "_usage";
protected abstract UsageData[] GetUsages(SerializedProperty prop = null);
private string[] GetPopupOptions(SerializedProperty prop = null) => GetUsages(prop).Select(u => u.DisplayName).ToArray();
private static GUIStyle _popupStyle;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
Rect initialPosition = position;
if (_popupStyle == null)
{
_popupStyle = new GUIStyle(GUI.skin.GetStyle("PaneOptions"))
{
imagePosition = ImagePosition.ImageOnly
};
}
using (var scope = new EditorGUI.PropertyScope(position, label, property))
{
label = scope.content;
position = EditorGUI.PrefixLabel(position, label);
// Store old indent level and set it to 0, the PrefixLabel takes care of it
int indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
{
EditorGUI.BeginChangeCheck();
{
DetermineDragAndDropFieldReferenceType(position, property);
DrawConfigurationButton(ref position, property);
DrawField(position, property, label, initialPosition);
}
if (EditorGUI.EndChangeCheck())
{
property.serializedObject.ApplyModifiedProperties();
}
}
EditorGUI.indentLevel = indent;
}
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
var usageIntVal = GetUsageIndex(property);
var usageData = GetUsages(property)[0];
for (int i = 0; i < GetUsages(property).Length; ++i)
{
if (GetUsages(property)[i].Value == usageIntVal)
{
usageData = GetUsages(property)[i];
break;
}
}
var innerProperty = property.FindPropertyRelative(usageData.PropertyName);
bool forceSingleLine = false;
#if UNITY_2021_2_OR_NEWER
// This is needed for similar reasons as described in the comment in the DrawField method below.
// This is basically a hack to fix a bug on Unity's side, which we need to revert when / if Unity fix it on their side.
forceSingleLine = innerProperty != null && innerProperty.propertyType == SerializedPropertyType.Quaternion;
#endif
return innerProperty == null || forceSingleLine ?
EditorGUIUtility.singleLineHeight :
EditorGUI.GetPropertyHeight(innerProperty, label);
}
private void DrawConfigurationButton(ref Rect position, SerializedProperty property)
{
Rect button = new Rect(position);
button.yMin += _popupStyle.margin.top;
button.yMax = button.yMin + EditorGUIUtility.singleLineHeight;
button.width = _popupStyle.fixedWidth + _popupStyle.margin.right;
position.xMin = button.xMax;
var currentUsageIndex = GetUsageIndex(property);
var newUsageValue = EditorGUI.Popup(button, currentUsageIndex, GetPopupOptions(property), _popupStyle);
SetUsageIndex(property, newUsageValue);
}
private void DrawField(in Rect position, SerializedProperty property, GUIContent label, in Rect originalPosition)
{
string usageTypePropertyName = GetUsages(property)[GetUsageIndex(property)].PropertyName;
var usageTypeProperty = property.FindPropertyRelative(usageTypePropertyName);
if (usageTypeProperty == null)
{
EditorGUI.LabelField(position, "[Non serialized value]");
}
else
{
var expanded = usageTypeProperty.isExpanded;
usageTypeProperty.isExpanded = true;
#if UNITY_2021_2_OR_NEWER
var valueFieldHeight = usageTypeProperty.propertyType == SerializedPropertyType.Quaternion ?
// In versions prior to 2022.3 GetPropertyHeight returns the wrong value for "SerializedPropertyType.Quaternion"
// In later versions, the fix is introduced _but only_ when using the SerializedPropertyType parameter, not when using the SerializedProperty parameter version.
// ALSO the SerializedPropertyType parameter version does not work with the isExpanded flag which we set to true exactly for this reason a (few) lines above.
EditorGUI.GetPropertyHeight(SerializedPropertyType.Vector3, label) :
EditorGUI.GetPropertyHeight(usageTypeProperty, label);
#else
var valueFieldHeight = EditorGUI.GetPropertyHeight(usageTypeProperty, label);
#endif
usageTypeProperty.isExpanded = expanded;
if (usageTypePropertyName == "_value" && (valueFieldHeight > EditorGUIUtility.singleLineHeight + 2))
{
EditorGUI.PropertyField(originalPosition, usageTypeProperty, GUIContent.none, true);
}
else
{
EditorGUI.PropertyField(position, usageTypeProperty, GUIContent.none);
}
}
}
private static void SetUsageIndex(SerializedProperty property, int index)
{
property.FindPropertyRelative(USAGE_PROPERTY_NAME).intValue = index;
}
private static int GetUsageIndex(SerializedProperty property)
{
return property.FindPropertyRelative(USAGE_PROPERTY_NAME).intValue;
}
#region Auto Drag And Drop Usage Type Detection
private void DetermineDragAndDropFieldReferenceType(in Rect position, SerializedProperty property)
{
EventType mouseEventType = Event.current.type;
if (mouseEventType != EventType.DragUpdated && mouseEventType != EventType.DragPerform)
{
return;
}
if (!IsMouseHoveringOverProperty(position))
{
return;
}
var draggedObjects = DragAndDrop.objectReferences;
if (draggedObjects.Length < 1)
{
return;
}
Object draggedObject = draggedObjects[0];
if (draggedObject is GameObject gameObject)
{
object[] instancers = gameObject.GetComponents();
UpdateUsageConfigurationOption(property, instancers);
}
else
{
UpdateUsageConfigurationOption(property, draggedObject);
}
}
private void UpdateUsageConfigurationOption(SerializedProperty property, params object[] draggedObjects)
{
if (draggedObjects == null || draggedObjects.Length < 1)
{
return;
}
var usages = GetUsages(property);
int currentUsageIndex = GetUsageIndex(property);
int newUsageIndex = -1;
foreach (object draggedObject in draggedObjects)
{
for (int index = 0; index < usages.Length; index++)
{
var usage = usages[index];
var usageProperty = property.FindPropertyRelative(usage.PropertyName);
bool isDraggedTypeSameAsUsageType = AreTypesEqual(usageProperty, draggedObject);
if (isDraggedTypeSameAsUsageType)
{
bool isUsageSetByUser = currentUsageIndex == index;
bool isNewUsageIndexSet = newUsageIndex > -1;
if (isUsageSetByUser)
{
return;
}
if (!isNewUsageIndexSet)
{
newUsageIndex = index;
}
break;
}
}
}
if (newUsageIndex > -1)
{
SetUsageIndex(property, newUsageIndex);
}
}
private static bool AreTypesEqual(SerializedProperty property, object otherObject)
{
string otherObjectTypeName = otherObject.GetType().Name;
string propertyObjectTypeName = GetPropertyTypeName(property);
return otherObjectTypeName == propertyObjectTypeName;
}
private static readonly string PPTR_GENERIC_PREFIX = "PPtr<$";
private static string GetPropertyTypeName(SerializedProperty property)
{
if (!property.type.StartsWith(PPTR_GENERIC_PREFIX))
{
return property.type;
}
string fieldPropertyType = property.type.Replace(PPTR_GENERIC_PREFIX, "");
return fieldPropertyType.Remove(fieldPropertyType.Length - 1);
}
private static bool IsMouseHoveringOverProperty(in Rect rectPosition)
{
const int HEIGHT_OFFSET_TO_AVOID_OVERLAP = 1;
Rect controlRect = rectPosition;
controlRect.height -= HEIGHT_OFFSET_TO_AVOID_OVERLAP;
return controlRect.Contains(Event.current.mousePosition);
}
#endregion
}
}