using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Unity.XR.CoreUtils;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit.Utilities.Internal;
using Object = UnityEngine.Object;
namespace UnityEditor.XR.Interaction.Toolkit.Utilities.Internal
{
///
/// Draws the fields marked with the and validates if the referenced value
/// implements the interface supplied in the attribute.
/// If the referenced object is not valid, a warning message will be displayed below the gui control in the Inspector.
/// This class does not validate the interface implementation when dragging objects into the foldout array (or list)
/// in the Inspector.
///
///
/// This is accomplished by listening to some ImGUI events and changing the Object Field behavior:
///
///
/// The Object Field's button click check is overriden to call the Object Selector window with a search
/// filter containing the minimum set of valid field types that implement the given interface ().
///
///
/// The Object Field's Repaint event has a different object type to correctly display the interface type
/// name in the Inspector ().
///
///
/// The Object Field's Drag Update and Perform events have a different object type to correctly discard
/// dragged references that are not valid ().
///
///
///
[CustomPropertyDrawer(typeof(RequireInterfaceAttribute))]
class RequireInterfaceDrawer : PropertyDrawer
{
static class Contents
{
public const float objectFieldMiniThumbnailHeight = 18f;
public const float objectFieldMiniThumbnailWidth = 32f;
public const float mismatchImplementationMessageHeight = 20f;
public static GUIContent invalidTypeMessage { get; } = EditorGUIUtility.TrTextContent($"Use {nameof(RequireInterfaceAttribute)} with Object reference fields.");
public static GUIContent invalidAttributeMessage { get; } = EditorGUIUtility.TrTextContent($"The attribute is not a {nameof(RequireInterfaceAttribute)}.");
public static GUIContent invalidInterfaceMessage { get; } = EditorGUIUtility.TrTextContent("The required type is not an interface.");
public static GUIContent mismatchImplementationMessage { get; } = EditorGUIUtility.TrTextContent("The referenced object does not implement {0}.");
}
const string k_ObjectSelectorUpdateCommand = "ObjectSelectorUpdated";
///
/// Map that caches the search filter of a field and interface type pair.
///
static readonly Dictionary> s_FilterMapByFieldType = new Dictionary>();
///
/// Reusable string builder to create search filters.
///
static readonly StringBuilder s_SearchFilterBuilder = new StringBuilder();
///
/// Reusable list used to store the minimum assignable field types that implement the given interface.
///
static readonly List s_MinimumAssignableImplementations = new List();
#region Object Field
///
/// Copied from the internal EditorGUI.ObjectFieldVisualType
///
enum ObjectFieldVisualType
{
IconAndText,
LargePreview,
MiniPreview,
}
///
/// Copied from the internal EditorGUI.GetButtonRect and modified to receive the object type as parameter instead
/// of the ObjectFieldVisualType.
///
/// The type to get the button rect.
/// The property rect position.
/// The Object Field button picker rect position.
static Rect GetObjectFieldButtonRect(Type objectType, Rect position)
{
var hasThumbnail = EditorGUIUtility.HasObjectThumbnail(objectType);
var visualType = ObjectFieldVisualType.IconAndText;
if (hasThumbnail && position.height <= Contents.objectFieldMiniThumbnailHeight && position.width <= Contents.objectFieldMiniThumbnailWidth)
visualType = ObjectFieldVisualType.MiniPreview;
else if (hasThumbnail && position.height > EditorGUIUtility.singleLineHeight)
visualType = ObjectFieldVisualType.LargePreview;
switch (visualType)
{
case ObjectFieldVisualType.IconAndText:
return new Rect(position.xMax - 19, position.y, 19, position.height);
case ObjectFieldVisualType.MiniPreview:
return new Rect(position.xMax - 14, position.y, 14, position.height);
case ObjectFieldVisualType.LargePreview:
return new Rect(position.xMax - 36, position.yMax - 14, 36, 14);
default:
throw new ArgumentOutOfRangeException();
}
}
static Type GetObjectFieldType(Rect position, Type fieldType, Type interfaceType, out bool? dragAndDropAssignable)
{
dragAndDropAssignable = null;
// Used to correctly display the interface type name
if (Event.current.type == EventType.Repaint)
return interfaceType;
// Used to correctly update the DragAndDrop.visualMode when dragging references that are not assignable
if (GUI.enabled &&
(Event.current.type == EventType.DragUpdated || Event.current.type == EventType.DragPerform) &&
DragAndDrop.objectReferences.Length > 0 &&
position.Contains(Event.current.mousePosition))
{
var referencedValue = DragAndDrop.objectReferences[0];
if (referencedValue != null)
{
dragAndDropAssignable = TryGetAssignableObject(referencedValue, fieldType, interfaceType, out _);
if (!dragAndDropAssignable.Value)
return interfaceType;
}
}
return fieldType;
}
#endregion
#region Search Filter
static bool IsDirectImplementation(Type type, Type interfaceType)
{
var directImplementedInterfaces = type.BaseType == null ? type.GetInterfaces() : type.GetInterfaces().Except(type.BaseType.GetInterfaces());
return directImplementedInterfaces.Contains(interfaceType);
}
static void GetDirectImplementations(Type fieldType, Type interfaceType, List resultList)
{
if (!interfaceType.IsInterface)
return;
ReflectionUtils.ForEachType(t =>
{
if (!t.IsInterface && fieldType.IsAssignableFrom(t) && interfaceType.IsAssignableFrom(t) && IsDirectImplementation(t, interfaceType))
resultList.Add(t);
});
}
static string GetSearchFilter(Type fieldType, Type interfaceType)
{
if (!s_FilterMapByFieldType.TryGetValue(fieldType, out var filterByInterfaceType))
{
filterByInterfaceType = new Dictionary();
s_FilterMapByFieldType.Add(fieldType, filterByInterfaceType);
}
else if (filterByInterfaceType.TryGetValue(interfaceType, out var cachedSearchFilter))
{
return cachedSearchFilter;
}
s_MinimumAssignableImplementations.Clear();
GetDirectImplementations(fieldType, interfaceType, s_MinimumAssignableImplementations);
s_SearchFilterBuilder.Clear();
foreach (var type in s_MinimumAssignableImplementations)
{
s_SearchFilterBuilder.Append("t:");
s_SearchFilterBuilder.Append(type.Name);
s_SearchFilterBuilder.Append(" ");
}
var searchFilter = s_SearchFilterBuilder.ToString();
filterByInterfaceType.Add(interfaceType, searchFilter);
return searchFilter;
}
#endregion
#region Helper Methods
static bool TryGetAssignableObject(Object objectToValidate, Type fieldType, Type interfaceType, out Object assignableObject)
{
if (objectToValidate == null)
{
assignableObject = null;
return true;
}
var valueType = objectToValidate.GetType();
if (fieldType.IsAssignableFrom(valueType) && interfaceType.IsAssignableFrom(valueType))
{
assignableObject = objectToValidate;
return true;
}
// If the given objectToValidate is a GameObject, search its components as well
if (objectToValidate is GameObject gameObject)
{
assignableObject = gameObject.GetComponent(interfaceType);
if (assignableObject != null && fieldType.IsInstanceOfType(assignableObject) && interfaceType.IsInstanceOfType(assignableObject))
return true;
}
assignableObject = null;
return false;
}
static Type GetFieldOrElementType(Type fieldType)
{
if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(List<>))
{
var types = fieldType.GetGenericArguments();
return types.Length <= 0 ? null : types[0];
}
if (fieldType.IsArray)
return fieldType.GetElementType();
return fieldType;
}
#endregion
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
var propertyHeight = base.GetPropertyHeight(property, label);
if (property.propertyType == SerializedPropertyType.ObjectReference && property.objectReferenceValue != null &&
attribute is RequireInterfaceAttribute requireInterfaceAttr &&
requireInterfaceAttr.interfaceType.IsInterface &&
!requireInterfaceAttr.interfaceType.IsInstanceOfType(property.objectReferenceValue))
{
propertyHeight += Contents.mismatchImplementationMessageHeight + 4f;
}
return propertyHeight;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var objectPickerID = GUIUtility.GetControlID(FocusType.Passive);
if (property.propertyType != SerializedPropertyType.ObjectReference)
{
EditorGUI.LabelField(position, label, Contents.invalidTypeMessage);
return;
}
if (!(attribute is RequireInterfaceAttribute requireInterfaceAttr))
{
EditorGUI.LabelField(position, label, Contents.invalidAttributeMessage);
return;
}
if (requireInterfaceAttr.interfaceType == null || !requireInterfaceAttr.interfaceType.IsInterface)
{
EditorGUI.LabelField(position, label, Contents.invalidInterfaceMessage);
return;
}
if (property.objectReferenceValue != null && !requireInterfaceAttr.interfaceType.IsInstanceOfType(property.objectReferenceValue))
{
var messagePosition = position;
position.height -= Contents.mismatchImplementationMessageHeight + 4f;
messagePosition.y = position.yMax + 2f;
messagePosition.height = Contents.mismatchImplementationMessageHeight;
EditorGUI.HelpBox(messagePosition, string.Format(Contents.mismatchImplementationMessage.text, requireInterfaceAttr.interfaceType.Name), MessageType.Warning);
}
var fieldType = GetFieldOrElementType(fieldInfo.FieldType);
using (var scope = new EditorGUI.PropertyScope(position, label, property))
using (var check = new EditorGUI.ChangeCheckScope())
{
var allowSceneObjs = !EditorUtility.IsPersistent(property.serializedObject.targetObject);
var objectFieldType = GetObjectFieldType(position, fieldType, requireInterfaceAttr.interfaceType, out var dragAndDropAssignable);
// Override the Object Field button to call the Object Selector window with a filter containing the minimum set of assignable field types that implement the required interface
if (GUI.enabled && Event.current.type == EventType.MouseDown && Event.current.button == 0 && position.Contains(Event.current.mousePosition))
{
var buttonRect = GetObjectFieldButtonRect(objectFieldType, position);
if (buttonRect.Contains(Event.current.mousePosition))
{
EditorGUIUtility.editingTextField = false;
var searchFilter = GetSearchFilter(fieldType, requireInterfaceAttr.interfaceType);
EditorGUIUtility.ShowObjectPicker