UnityMemberDrawer.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Reflection;
  5. using Chronos.Controls.Editor;
  6. using Chronos.Reflection.Internal;
  7. using UnityEditor;
  8. using UnityEngine;
  9. using UnityObject = UnityEngine.Object;
  10. namespace Chronos.Reflection.Editor
  11. {
  12. [CustomPropertyDrawer(typeof(UnityMember))]
  13. public abstract class UnityMemberDrawer<TMember> : TargetedDrawer where TMember : UnityMember
  14. {
  15. internal static FilterAttribute filterOverride;
  16. #region Fields
  17. /// <summary>
  18. /// The filter attribute on the inspected field.
  19. /// </summary>
  20. protected FilterAttribute filter;
  21. /// <summary>
  22. /// The inspected property, of type UnityMember.
  23. /// </summary>
  24. protected SerializedProperty property;
  25. /// <summary>
  26. /// The UnityMember.component of the inspected property, of type string.
  27. /// </summary>
  28. protected SerializedProperty componentProperty;
  29. /// <summary>
  30. /// The UnityMember.name of the inspected property, of type string.
  31. /// </summary>
  32. protected SerializedProperty nameProperty;
  33. /// <summary>
  34. /// The targeted Unity Objects.
  35. /// </summary>
  36. protected UnityObject[] targets;
  37. /// <summary>
  38. /// The type of targeted objects.
  39. /// </summary>
  40. protected UnityObjectType targetType;
  41. #endregion
  42. /// <inheritdoc />
  43. protected override void Update(SerializedProperty property)
  44. {
  45. // Update the targeted drawer
  46. base.Update(property);
  47. // Assign the property and sub-properties
  48. this.property = property;
  49. componentProperty = property.FindPropertyRelative("_component");
  50. nameProperty = property.FindPropertyRelative("_name");
  51. // Fetch the filter
  52. filter = filterOverride ?? (FilterAttribute)fieldInfo.GetCustomAttributes(typeof(FilterAttribute), true).FirstOrDefault() ?? DefaultFilter();
  53. // Find the targets
  54. targets = FindTargets();
  55. targetType = DetermineTargetType();
  56. }
  57. /// <inheritdoc />
  58. protected override void RenderMemberControl(Rect position)
  59. {
  60. // Other Targets
  61. // Some Unity Objects, like Assets, are not supported by the drawer.
  62. // Just display an error message to let the user change their target.
  63. if (targetType == UnityObjectType.Other)
  64. {
  65. EditorGUI.HelpBox(position, "Unsupported Unity Object type.", MessageType.None);
  66. return;
  67. }
  68. // Display a list of all available reflected members in a popup.
  69. var options = new List<DropdownOption<TMember>>();
  70. TMember value = GetValue();
  71. DropdownOption<TMember> selectedOption = null;
  72. DropdownOption<TMember> noneOption = new DropdownOption<TMember>(null, string.Format("No {0}", memberLabel));
  73. if (targetType == UnityObjectType.GameObject)
  74. {
  75. // Check if all targets have a GameObject (none are empty).
  76. // If they do, display all members of the GameObject type.
  77. if (HasSharedGameObject())
  78. {
  79. var gameObjectOptions = GetSortedMemberOptions(typeof(GameObject));
  80. foreach (var gameObjectOption in gameObjectOptions)
  81. {
  82. // Prefix label by GameObject for popup clarity.
  83. gameObjectOption.label = string.Format("GameObject/{0}", gameObjectOption.label);
  84. options.Add(gameObjectOption);
  85. }
  86. }
  87. // Find all shared component types across targets.
  88. // Display all members of each one found.
  89. foreach (Type componentType in GetSharedComponentTypes())
  90. {
  91. var componentOptions = GetSortedMemberOptions(componentType, componentType.Name);
  92. foreach (var componentOption in componentOptions)
  93. {
  94. // Prefix label and option by component type for clear distinction.
  95. componentOption.label = string.Format("{0}/{1}", componentType.Name, componentOption.label);
  96. options.Add(componentOption);
  97. }
  98. }
  99. // Determine which option is currently selected.
  100. if (value != null)
  101. {
  102. string label;
  103. if (value.component == null)
  104. {
  105. label = string.Format("GameObject.{0}", value.name);
  106. }
  107. else
  108. {
  109. label = string.Format("{0}.{1}", value.component, value.name);
  110. }
  111. UnityMethod method = value as UnityMethod;
  112. if (method != null)
  113. {
  114. string parameterString = string.Join(", ", method.parameterTypes.Select(t => t.PrettyName()).ToArray());
  115. label += string.Format(" ({0})", parameterString);
  116. }
  117. TMember valueInOptions = options.Select(option => option.value).FirstOrDefault(member => member.Corresponds(value));
  118. if (valueInOptions != null)
  119. {
  120. selectedOption = new DropdownOption<TMember>(valueInOptions, label);
  121. }
  122. else
  123. {
  124. selectedOption = new DropdownOption<TMember>(value, label);
  125. }
  126. }
  127. }
  128. else if (targetType == UnityObjectType.ScriptableObject)
  129. {
  130. // ScriptableObject Target
  131. // Make sure all targets share the same ScriptableObject Type.
  132. // If they do, display all members of that type.
  133. Type scriptableObjectType = GetSharedScriptableObjectType();
  134. if (scriptableObjectType != null)
  135. {
  136. options.AddRange(GetSortedMemberOptions(scriptableObjectType));
  137. // Determine which option is currently selected.
  138. if (value != null)
  139. {
  140. selectedOption = options.Find(o => o.value.Corresponds(value));
  141. if (selectedOption == null)
  142. {
  143. selectedOption = new DropdownOption<TMember>(value, value.name);
  144. }
  145. }
  146. }
  147. }
  148. // Make sure the callback uses the property of this drawer, not at its later value.
  149. var propertyNow = property;
  150. bool enabled = targetType != UnityObjectType.None;
  151. if (!enabled) EditorGUI.BeginDisabledGroup(true);
  152. DropdownGUI<TMember>.PopupSingle
  153. (
  154. position,
  155. newValue =>
  156. {
  157. Update(propertyNow);
  158. SetValue(newValue);
  159. propertyNow.serializedObject.ApplyModifiedProperties();
  160. },
  161. options,
  162. selectedOption,
  163. noneOption,
  164. hasMultipleDifferentValues
  165. );
  166. if (!enabled) EditorGUI.EndDisabledGroup();
  167. }
  168. #region Value
  169. /// <summary>
  170. /// Constructs a new instance of the member from the specified component and name.
  171. /// </summary>
  172. protected abstract TMember BuildValue(string component, string name);
  173. /// <summary>
  174. /// Returns a member constructed from the current parameter values.
  175. /// </summary>
  176. /// <returns></returns>
  177. protected TMember GetValue()
  178. {
  179. if (hasMultipleDifferentValues ||
  180. string.IsNullOrEmpty(nameProperty.stringValue))
  181. {
  182. return null;
  183. }
  184. string component = componentProperty.stringValue;
  185. string name = nameProperty.stringValue;
  186. if (component == string.Empty) component = null;
  187. if (name == string.Empty) name = null;
  188. return BuildValue(component, name);
  189. }
  190. /// <summary>
  191. /// Assigns the property values from a specified member.
  192. /// </summary>
  193. protected virtual void SetValue(TMember value)
  194. {
  195. if (value != null)
  196. {
  197. componentProperty.stringValue = value.component;
  198. nameProperty.stringValue = value.name;
  199. }
  200. else
  201. {
  202. componentProperty.stringValue = null;
  203. nameProperty.stringValue = null;
  204. }
  205. }
  206. /// <summary>
  207. /// Indicated whether the property has multiple different values.
  208. /// </summary>
  209. protected virtual bool hasMultipleDifferentValues
  210. {
  211. get
  212. {
  213. return componentProperty.hasMultipleDifferentValues || nameProperty.hasMultipleDifferentValues;
  214. }
  215. }
  216. #endregion
  217. #region Targeting
  218. /// <summary>
  219. /// Get the list of targets on the inspected objects.
  220. /// </summary>
  221. protected UnityObject[] FindTargets()
  222. {
  223. if (isSelfTargeted)
  224. {
  225. // In self targeting mode, the targets are the inspected objects themselves.
  226. return property.serializedObject.targetObjects;
  227. }
  228. else
  229. {
  230. // In manual targeting mode, the targets the values of each target property.
  231. return targetProperty.Multiple().Select(p => p.objectReferenceValue).ToArray();
  232. }
  233. }
  234. /// <summary>
  235. /// Determine the Unity type of the targets.
  236. /// </summary>
  237. protected UnityObjectType DetermineTargetType()
  238. {
  239. UnityObjectType unityObjectType = UnityObjectType.None;
  240. foreach (UnityObject targetObject in targets)
  241. {
  242. // Null (non-specified) targets don't affect the type
  243. // If no non-null target is specified, the type will be None
  244. // as the loop will simply exit.
  245. if (targetObject == null)
  246. {
  247. continue;
  248. }
  249. if (targetObject is GameObject || targetObject is Component)
  250. {
  251. // For GameObjects and Components, the target is either the
  252. // GameObject itself, or the one to which the Component belongs.
  253. // If a ScriptableObject target was previously found,
  254. // return that the targets are of mixed types.
  255. if (unityObjectType == UnityObjectType.ScriptableObject)
  256. {
  257. return UnityObjectType.Mixed;
  258. }
  259. unityObjectType = UnityObjectType.GameObject;
  260. }
  261. else if (targetObject is ScriptableObject)
  262. {
  263. // For ScriptableObjects, the target is simply the
  264. // ScriptableObject itself.
  265. // If a GameObject target was previously found,
  266. // return that the targets are of mixed types.
  267. if (unityObjectType == UnityObjectType.GameObject)
  268. {
  269. return UnityObjectType.Mixed;
  270. }
  271. unityObjectType = UnityObjectType.ScriptableObject;
  272. }
  273. else
  274. {
  275. // Other target types
  276. return UnityObjectType.Other;
  277. }
  278. }
  279. return unityObjectType;
  280. }
  281. /// <summary>
  282. /// Determines if the targets all share a GameObject.
  283. /// </summary>
  284. public bool HasSharedGameObject()
  285. {
  286. return !targets.Contains(null);
  287. }
  288. /// <summary>
  289. /// Determines which types of Components are shared on all GameObject targets.
  290. /// </summary>
  291. protected IEnumerable<Type> GetSharedComponentTypes()
  292. {
  293. if (targets.Contains(null))
  294. {
  295. return Enumerable.Empty<Type>();
  296. }
  297. var childrenComponents = targets.OfType<GameObject>().Select(gameObject => gameObject.GetComponents<Component>().Where(c => c != null));
  298. var siblingComponents = targets.OfType<Component>().Select(component => component.GetComponents<Component>().Where(c => c != null));
  299. return childrenComponents.Concat(siblingComponents)
  300. .Select(components => components.Select(component => component.GetType()))
  301. .IntersectAll()
  302. .Distinct();
  303. }
  304. /// <summary>
  305. /// Determines which type of ScriptableObject is shared across targets.
  306. /// Returns null if none are shared.
  307. /// </summary>
  308. protected Type GetSharedScriptableObjectType()
  309. {
  310. if (targets.Contains(null))
  311. {
  312. return null;
  313. }
  314. return targets
  315. .OfType<ScriptableObject>()
  316. .Select(scriptableObject => scriptableObject.GetType())
  317. .Distinct()
  318. .SingleOrDefault(); // Null (default) if multiple or zero
  319. }
  320. #endregion
  321. #region Reflection
  322. /// <summary>
  323. /// Gets the list of members available on a type as popup options.
  324. /// </summary>
  325. protected virtual List<DropdownOption<TMember>> GetMemberOptions(Type type, string component = null)
  326. {
  327. return type
  328. .GetMembers(validBindingFlags)
  329. .Where(member => validMemberTypes.HasFlag(member.MemberType))
  330. .Where(ValidateMember)
  331. .Select(member => GetMemberOption(member, component, member.DeclaringType != type))
  332. .ToList();
  333. }
  334. /// <summary>
  335. /// Gets the sorted list of members available on a type as popup options.
  336. /// </summary>
  337. protected virtual List<DropdownOption<TMember>> GetSortedMemberOptions(Type type, string component = null)
  338. {
  339. var options = GetMemberOptions(type, component);
  340. var withoutSlashes = options.Where(o => !o.label.Contains("/")).ToList();
  341. var withSlashes = options.Where(o => o.label.Contains("/")).ToList();
  342. options.Clear();
  343. options.AddRange(withSlashes);
  344. options.AddRange(withoutSlashes);
  345. return options;
  346. }
  347. protected abstract DropdownOption<TMember> GetMemberOption(MemberInfo member, string component, bool inherited);
  348. #endregion
  349. #region Filtering
  350. /// <summary>
  351. /// The label of a member, displayed in options.
  352. /// </summary>
  353. protected virtual string memberLabel
  354. {
  355. get
  356. {
  357. return "Member";
  358. }
  359. }
  360. /// <summary>
  361. /// The default applied filter attribute if none is specified.
  362. /// </summary>
  363. protected virtual FilterAttribute DefaultFilter()
  364. {
  365. return new FilterAttribute();
  366. }
  367. /// <summary>
  368. /// The valid BindingFlags when looking for reflected members.
  369. /// </summary>
  370. protected virtual BindingFlags validBindingFlags
  371. {
  372. get
  373. {
  374. // Build the flags from the filter attribute
  375. BindingFlags flags = (BindingFlags)0;
  376. if (filter.Public) flags |= BindingFlags.Public;
  377. if (filter.NonPublic) flags |= BindingFlags.NonPublic;
  378. if (filter.Instance) flags |= BindingFlags.Instance;
  379. if (filter.Static) flags |= BindingFlags.Static;
  380. if (!filter.Inherited) flags |= BindingFlags.DeclaredOnly;
  381. if (filter.Static && filter.Inherited) flags |= BindingFlags.FlattenHierarchy;
  382. return flags;
  383. }
  384. }
  385. /// <summary>
  386. /// The valid MemberTypes when looking for reflected members.
  387. /// </summary>
  388. protected virtual MemberTypes validMemberTypes
  389. {
  390. get
  391. {
  392. return MemberTypes.All;
  393. }
  394. }
  395. /// <summary>
  396. /// Determines whether a given MemberInfo should be included in the options.
  397. /// This check follows the BindingFlags and MemberTypes filtering.
  398. /// </summary>
  399. protected virtual bool ValidateMember(MemberInfo member)
  400. {
  401. return true;
  402. }
  403. /// <summary>
  404. /// Determines whether a MemberInfo of the given type should be included in the options.
  405. /// </summary>
  406. protected virtual bool ValidateMemberType(Type type)
  407. {
  408. bool validFamily = false;
  409. bool validType;
  410. // Allow type families based on the filter attribute
  411. TypeFamily families = filter.TypeFamilies;
  412. if (families.HasFlag(TypeFamily.Array)) validFamily |= type.IsArray;
  413. if (families.HasFlag(TypeFamily.Class)) validFamily |= type.IsClass;
  414. if (families.HasFlag(TypeFamily.Enum)) validFamily |= type.IsEnum;
  415. if (families.HasFlag(TypeFamily.Interface)) validFamily |= type.IsInterface;
  416. if (families.HasFlag(TypeFamily.Primitive)) validFamily |= type.IsPrimitive;
  417. if (families.HasFlag(TypeFamily.Reference)) validFamily |= !type.IsValueType;
  418. if (families.HasFlag(TypeFamily.Value)) validFamily |= (type.IsValueType && type != typeof(void));
  419. if (families.HasFlag(TypeFamily.Void)) validFamily |= type == typeof(void);
  420. // Allow types based on the filter attribute
  421. // If no filter types are specified, all types are allowed.
  422. if (filter.Types.Count > 0)
  423. {
  424. validType = false;
  425. foreach (Type allowedType in filter.Types)
  426. {
  427. if (allowedType.IsAssignableFrom(type))
  428. {
  429. validType = true;
  430. break;
  431. }
  432. }
  433. }
  434. else
  435. {
  436. validType = true;
  437. }
  438. return validFamily && validType;
  439. }
  440. #endregion
  441. }
  442. }