123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547 |
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Reflection;
- using Chronos.Controls.Editor;
- using Chronos.Reflection.Internal;
- using UnityEditor;
- using UnityEngine;
- using UnityObject = UnityEngine.Object;
- namespace Chronos.Reflection.Editor
- {
- [CustomPropertyDrawer(typeof(UnityMember))]
- public abstract class UnityMemberDrawer<TMember> : TargetedDrawer where TMember : UnityMember
- {
- internal static FilterAttribute filterOverride;
- #region Fields
- /// <summary>
- /// The filter attribute on the inspected field.
- /// </summary>
- protected FilterAttribute filter;
- /// <summary>
- /// The inspected property, of type UnityMember.
- /// </summary>
- protected SerializedProperty property;
- /// <summary>
- /// The UnityMember.component of the inspected property, of type string.
- /// </summary>
- protected SerializedProperty componentProperty;
- /// <summary>
- /// The UnityMember.name of the inspected property, of type string.
- /// </summary>
- protected SerializedProperty nameProperty;
- /// <summary>
- /// The targeted Unity Objects.
- /// </summary>
- protected UnityObject[] targets;
- /// <summary>
- /// The type of targeted objects.
- /// </summary>
- protected UnityObjectType targetType;
- #endregion
- /// <inheritdoc />
- protected override void Update(SerializedProperty property)
- {
- // Update the targeted drawer
- base.Update(property);
- // Assign the property and sub-properties
- this.property = property;
- componentProperty = property.FindPropertyRelative("_component");
- nameProperty = property.FindPropertyRelative("_name");
- // Fetch the filter
- filter = filterOverride ?? (FilterAttribute)fieldInfo.GetCustomAttributes(typeof(FilterAttribute), true).FirstOrDefault() ?? DefaultFilter();
- // Find the targets
- targets = FindTargets();
- targetType = DetermineTargetType();
- }
- /// <inheritdoc />
- protected override void RenderMemberControl(Rect position)
- {
- // Other Targets
- // Some Unity Objects, like Assets, are not supported by the drawer.
- // Just display an error message to let the user change their target.
- if (targetType == UnityObjectType.Other)
- {
- EditorGUI.HelpBox(position, "Unsupported Unity Object type.", MessageType.None);
- return;
- }
- // Display a list of all available reflected members in a popup.
- var options = new List<DropdownOption<TMember>>();
- TMember value = GetValue();
- DropdownOption<TMember> selectedOption = null;
- DropdownOption<TMember> noneOption = new DropdownOption<TMember>(null, string.Format("No {0}", memberLabel));
- if (targetType == UnityObjectType.GameObject)
- {
- // Check if all targets have a GameObject (none are empty).
- // If they do, display all members of the GameObject type.
- if (HasSharedGameObject())
- {
- var gameObjectOptions = GetSortedMemberOptions(typeof(GameObject));
- foreach (var gameObjectOption in gameObjectOptions)
- {
- // Prefix label by GameObject for popup clarity.
- gameObjectOption.label = string.Format("GameObject/{0}", gameObjectOption.label);
- options.Add(gameObjectOption);
- }
- }
- // Find all shared component types across targets.
- // Display all members of each one found.
- foreach (Type componentType in GetSharedComponentTypes())
- {
- var componentOptions = GetSortedMemberOptions(componentType, componentType.Name);
- foreach (var componentOption in componentOptions)
- {
- // Prefix label and option by component type for clear distinction.
- componentOption.label = string.Format("{0}/{1}", componentType.Name, componentOption.label);
- options.Add(componentOption);
- }
- }
- // Determine which option is currently selected.
- if (value != null)
- {
- string label;
- if (value.component == null)
- {
- label = string.Format("GameObject.{0}", value.name);
- }
- else
- {
- label = string.Format("{0}.{1}", value.component, value.name);
- }
- UnityMethod method = value as UnityMethod;
- if (method != null)
- {
- string parameterString = string.Join(", ", method.parameterTypes.Select(t => t.PrettyName()).ToArray());
- label += string.Format(" ({0})", parameterString);
- }
- TMember valueInOptions = options.Select(option => option.value).FirstOrDefault(member => member.Corresponds(value));
- if (valueInOptions != null)
- {
- selectedOption = new DropdownOption<TMember>(valueInOptions, label);
- }
- else
- {
- selectedOption = new DropdownOption<TMember>(value, label);
- }
- }
- }
- else if (targetType == UnityObjectType.ScriptableObject)
- {
- // ScriptableObject Target
- // Make sure all targets share the same ScriptableObject Type.
- // If they do, display all members of that type.
- Type scriptableObjectType = GetSharedScriptableObjectType();
- if (scriptableObjectType != null)
- {
- options.AddRange(GetSortedMemberOptions(scriptableObjectType));
- // Determine which option is currently selected.
- if (value != null)
- {
- selectedOption = options.Find(o => o.value.Corresponds(value));
- if (selectedOption == null)
- {
- selectedOption = new DropdownOption<TMember>(value, value.name);
- }
- }
- }
- }
- // Make sure the callback uses the property of this drawer, not at its later value.
- var propertyNow = property;
- bool enabled = targetType != UnityObjectType.None;
- if (!enabled) EditorGUI.BeginDisabledGroup(true);
- DropdownGUI<TMember>.PopupSingle
- (
- position,
- newValue =>
- {
- Update(propertyNow);
- SetValue(newValue);
- propertyNow.serializedObject.ApplyModifiedProperties();
- },
- options,
- selectedOption,
- noneOption,
- hasMultipleDifferentValues
- );
- if (!enabled) EditorGUI.EndDisabledGroup();
- }
- #region Value
- /// <summary>
- /// Constructs a new instance of the member from the specified component and name.
- /// </summary>
- protected abstract TMember BuildValue(string component, string name);
- /// <summary>
- /// Returns a member constructed from the current parameter values.
- /// </summary>
- /// <returns></returns>
- protected TMember GetValue()
- {
- if (hasMultipleDifferentValues ||
- string.IsNullOrEmpty(nameProperty.stringValue))
- {
- return null;
- }
- string component = componentProperty.stringValue;
- string name = nameProperty.stringValue;
- if (component == string.Empty) component = null;
- if (name == string.Empty) name = null;
- return BuildValue(component, name);
- }
- /// <summary>
- /// Assigns the property values from a specified member.
- /// </summary>
- protected virtual void SetValue(TMember value)
- {
- if (value != null)
- {
- componentProperty.stringValue = value.component;
- nameProperty.stringValue = value.name;
- }
- else
- {
- componentProperty.stringValue = null;
- nameProperty.stringValue = null;
- }
- }
- /// <summary>
- /// Indicated whether the property has multiple different values.
- /// </summary>
- protected virtual bool hasMultipleDifferentValues
- {
- get
- {
- return componentProperty.hasMultipleDifferentValues || nameProperty.hasMultipleDifferentValues;
- }
- }
- #endregion
- #region Targeting
- /// <summary>
- /// Get the list of targets on the inspected objects.
- /// </summary>
- protected UnityObject[] FindTargets()
- {
- if (isSelfTargeted)
- {
- // In self targeting mode, the targets are the inspected objects themselves.
- return property.serializedObject.targetObjects;
- }
- else
- {
- // In manual targeting mode, the targets the values of each target property.
- return targetProperty.Multiple().Select(p => p.objectReferenceValue).ToArray();
- }
- }
- /// <summary>
- /// Determine the Unity type of the targets.
- /// </summary>
- protected UnityObjectType DetermineTargetType()
- {
- UnityObjectType unityObjectType = UnityObjectType.None;
- foreach (UnityObject targetObject in targets)
- {
- // Null (non-specified) targets don't affect the type
- // If no non-null target is specified, the type will be None
- // as the loop will simply exit.
- if (targetObject == null)
- {
- continue;
- }
- if (targetObject is GameObject || targetObject is Component)
- {
- // For GameObjects and Components, the target is either the
- // GameObject itself, or the one to which the Component belongs.
- // If a ScriptableObject target was previously found,
- // return that the targets are of mixed types.
- if (unityObjectType == UnityObjectType.ScriptableObject)
- {
- return UnityObjectType.Mixed;
- }
- unityObjectType = UnityObjectType.GameObject;
- }
- else if (targetObject is ScriptableObject)
- {
- // For ScriptableObjects, the target is simply the
- // ScriptableObject itself.
- // If a GameObject target was previously found,
- // return that the targets are of mixed types.
- if (unityObjectType == UnityObjectType.GameObject)
- {
- return UnityObjectType.Mixed;
- }
- unityObjectType = UnityObjectType.ScriptableObject;
- }
- else
- {
- // Other target types
- return UnityObjectType.Other;
- }
- }
- return unityObjectType;
- }
- /// <summary>
- /// Determines if the targets all share a GameObject.
- /// </summary>
- public bool HasSharedGameObject()
- {
- return !targets.Contains(null);
- }
- /// <summary>
- /// Determines which types of Components are shared on all GameObject targets.
- /// </summary>
- protected IEnumerable<Type> GetSharedComponentTypes()
- {
- if (targets.Contains(null))
- {
- return Enumerable.Empty<Type>();
- }
- var childrenComponents = targets.OfType<GameObject>().Select(gameObject => gameObject.GetComponents<Component>().Where(c => c != null));
- var siblingComponents = targets.OfType<Component>().Select(component => component.GetComponents<Component>().Where(c => c != null));
- return childrenComponents.Concat(siblingComponents)
- .Select(components => components.Select(component => component.GetType()))
- .IntersectAll()
- .Distinct();
- }
- /// <summary>
- /// Determines which type of ScriptableObject is shared across targets.
- /// Returns null if none are shared.
- /// </summary>
- protected Type GetSharedScriptableObjectType()
- {
- if (targets.Contains(null))
- {
- return null;
- }
- return targets
- .OfType<ScriptableObject>()
- .Select(scriptableObject => scriptableObject.GetType())
- .Distinct()
- .SingleOrDefault(); // Null (default) if multiple or zero
- }
- #endregion
- #region Reflection
- /// <summary>
- /// Gets the list of members available on a type as popup options.
- /// </summary>
- protected virtual List<DropdownOption<TMember>> GetMemberOptions(Type type, string component = null)
- {
- return type
- .GetMembers(validBindingFlags)
- .Where(member => validMemberTypes.HasFlag(member.MemberType))
- .Where(ValidateMember)
- .Select(member => GetMemberOption(member, component, member.DeclaringType != type))
- .ToList();
- }
- /// <summary>
- /// Gets the sorted list of members available on a type as popup options.
- /// </summary>
- protected virtual List<DropdownOption<TMember>> GetSortedMemberOptions(Type type, string component = null)
- {
- var options = GetMemberOptions(type, component);
- var withoutSlashes = options.Where(o => !o.label.Contains("/")).ToList();
- var withSlashes = options.Where(o => o.label.Contains("/")).ToList();
- options.Clear();
- options.AddRange(withSlashes);
- options.AddRange(withoutSlashes);
- return options;
- }
- protected abstract DropdownOption<TMember> GetMemberOption(MemberInfo member, string component, bool inherited);
- #endregion
- #region Filtering
- /// <summary>
- /// The label of a member, displayed in options.
- /// </summary>
- protected virtual string memberLabel
- {
- get
- {
- return "Member";
- }
- }
- /// <summary>
- /// The default applied filter attribute if none is specified.
- /// </summary>
- protected virtual FilterAttribute DefaultFilter()
- {
- return new FilterAttribute();
- }
- /// <summary>
- /// The valid BindingFlags when looking for reflected members.
- /// </summary>
- protected virtual BindingFlags validBindingFlags
- {
- get
- {
- // Build the flags from the filter attribute
- BindingFlags flags = (BindingFlags)0;
- if (filter.Public) flags |= BindingFlags.Public;
- if (filter.NonPublic) flags |= BindingFlags.NonPublic;
- if (filter.Instance) flags |= BindingFlags.Instance;
- if (filter.Static) flags |= BindingFlags.Static;
- if (!filter.Inherited) flags |= BindingFlags.DeclaredOnly;
- if (filter.Static && filter.Inherited) flags |= BindingFlags.FlattenHierarchy;
- return flags;
- }
- }
- /// <summary>
- /// The valid MemberTypes when looking for reflected members.
- /// </summary>
- protected virtual MemberTypes validMemberTypes
- {
- get
- {
- return MemberTypes.All;
- }
- }
- /// <summary>
- /// Determines whether a given MemberInfo should be included in the options.
- /// This check follows the BindingFlags and MemberTypes filtering.
- /// </summary>
- protected virtual bool ValidateMember(MemberInfo member)
- {
- return true;
- }
- /// <summary>
- /// Determines whether a MemberInfo of the given type should be included in the options.
- /// </summary>
- protected virtual bool ValidateMemberType(Type type)
- {
- bool validFamily = false;
- bool validType;
- // Allow type families based on the filter attribute
- TypeFamily families = filter.TypeFamilies;
- if (families.HasFlag(TypeFamily.Array)) validFamily |= type.IsArray;
- if (families.HasFlag(TypeFamily.Class)) validFamily |= type.IsClass;
- if (families.HasFlag(TypeFamily.Enum)) validFamily |= type.IsEnum;
- if (families.HasFlag(TypeFamily.Interface)) validFamily |= type.IsInterface;
- if (families.HasFlag(TypeFamily.Primitive)) validFamily |= type.IsPrimitive;
- if (families.HasFlag(TypeFamily.Reference)) validFamily |= !type.IsValueType;
- if (families.HasFlag(TypeFamily.Value)) validFamily |= (type.IsValueType && type != typeof(void));
- if (families.HasFlag(TypeFamily.Void)) validFamily |= type == typeof(void);
- // Allow types based on the filter attribute
- // If no filter types are specified, all types are allowed.
- if (filter.Types.Count > 0)
- {
- validType = false;
- foreach (Type allowedType in filter.Types)
- {
- if (allowedType.IsAssignableFrom(type))
- {
- validType = true;
- break;
- }
- }
- }
- else
- {
- validType = true;
- }
- return validFamily && validType;
- }
- #endregion
- }
- }
|