/****************************************************************************** * Copyright (C) Ultraleap, Inc. 2011-2021. * * * * Use subject to the terms of the Apache License 2.0 available at * * http://www.apache.org/licenses/LICENSE-2.0, or another agreement * * between Ultraleap and you, your company or other organization. * ******************************************************************************/ using UnityEngine; using System; using System.Collections.Generic; using Leap.Unity.Query; using System.Text.RegularExpressions; #if UNITY_EDITOR using UnityEditor; #endif using UnityObject = UnityEngine.Object; namespace Leap.Unity.Attributes { public class ImplementsInterfaceAttribute : CombinablePropertyAttribute, IPropertyConstrainer, IFullPropertyDrawer, ISupportDragAndDrop { #pragma warning disable 0414 private Type type; #pragma warning restore 0414 public ImplementsInterfaceAttribute(Type type) { if (!type.IsInterface) { throw new System.Exception(type.Name + " is not an interface."); } this.type = type; } #if UNITY_EDITOR public void ConstrainValue(SerializedProperty property) { if (property.objectReferenceValue != null) { UnityObject implementingObject = FindImplementer(property, property.objectReferenceValue); if (implementingObject == null) { Debug.LogError(property.objectReferenceValue.GetType().Name + " does not implement " + type.Name); } property.objectReferenceValue = implementingObject; } } /// /// Checks if the object or one of its associated GameObject components implements /// the interface that this attribute constrains objects to, and returns the object /// that implements that interface, or null if none was found. /// public UnityObject FindImplementer(SerializedProperty property, UnityObject fromObj) { // Don't use fieldInfo here because it is very convenient for it to be left null // on the CombinablePropertyAttribute when dealing with generic-type situations // where the ImplementsInterface class has to be constructed manually in a custom // editor. (Specific case: StreamConnectorEditor.cs) bool isTypeDefinitelyIncompatible; { isTypeDefinitelyIncompatible = !this.type.IsAssignableFrom(fromObj.GetType()); // We have to make an exception when a GameObject is dragged into a field whose // type is a Component, because it's expected that we can use GetComponent in // that case. (We might still fail later if the component isn't found.) var objIsGameObject = fromObj.GetType() == typeof(GameObject); if (objIsGameObject) { isTypeDefinitelyIncompatible = false; } // We also make an exception when a Component is dragged into a field that // doesn't directly satisfy the interface because it might have other Components // on its GameObject that _do_ satisfy the field, again via GetComponent. var objIsComponent = typeof(Component).IsAssignableFrom(fromObj.GetType()); if (objIsComponent) { isTypeDefinitelyIncompatible = false; } // However, you can't assign a ScriptableObject to a field expecting a Component, // or vice-versa, no matter what. var fieldIsScriptableObject = fieldInfo.FieldType.IsAssignableFrom(typeof(ScriptableObject)); if (fieldIsScriptableObject && (objIsComponent || objIsGameObject)) { isTypeDefinitelyIncompatible = true; } var fieldTakesComponent = typeof(Component).IsAssignableFrom(fieldInfo.FieldType); if (fieldTakesComponent && (!objIsComponent && !objIsGameObject)) { isTypeDefinitelyIncompatible = true; } } if (isTypeDefinitelyIncompatible) { return null; } if (fromObj.GetType().ImplementsInterface(type)) { // All good! This object reference implements the interface. return fromObj; } else { UnityObject implementingObject; if (fromObj is GameObject) { fromObj = (fromObj as GameObject).transform; } if (fromObj is Component) { // If the object is a Component, first search the rest of the GameObject // for a component that implements the interface. If found, assign it instead, // otherwise null out the property. implementingObject = (fromObj as Component) .GetComponents() .Query() .Where(c => c.GetType().ImplementsInterface(type)) .FirstOrDefault(); } else { // If the object is not a Component, just null out the property. implementingObject = null; } return implementingObject; } } public void DrawProperty(Rect rect, SerializedProperty property, GUIContent label) { if (property.objectReferenceValue != null) { EditorGUI.ObjectField(rect, property, type, label); } else { EditorGUI.ObjectField(rect, label, null, type, false); } } public Rect GetDropArea(Rect rect, SerializedProperty property) { return rect; } public bool IsDropValid(UnityObject[] draggedObjects, SerializedProperty property) { return draggedObjects.Query().Any(o => FindImplementer(property, o) != null); } public void ProcessDroppedObjects(UnityObject[] droppedObjects, SerializedProperty property) { var implementer = droppedObjects.Query() .FirstOrDefault(o => FindImplementer(property, o)); if (implementer == null) { Debug.LogError(property.objectReferenceValue.GetType().Name + " does not implement " + type.Name); } else { property.objectReferenceValue = implementer; } } public override IEnumerable SupportedTypes { get { yield return SerializedPropertyType.ObjectReference; } } #endif } }