InspectableField.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. //********************************** Banshee Engine (www.banshee3d.com) **************************************************//
  2. //**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Text;
  6. using bs;
  7. namespace bs.Editor
  8. {
  9. /** @addtogroup Inspector
  10. * @{
  11. */
  12. /// <summary>
  13. /// Inspectable field displays GUI elements for a single <see cref="SerializableProperty"/>. This is a base class that
  14. /// should be specialized for all supported types contained by <see cref="SerializableProperty"/>. Inspectable fields
  15. /// can and should be created recursively - normally complex types like objects and arrays will contain fields of their
  16. /// own, while primitive types like integer or boolean will consist of only a GUI element.
  17. /// </summary>
  18. public abstract class InspectableField
  19. {
  20. private const int IndentAmount = 5;
  21. protected InspectableContext context;
  22. protected InspectableFieldLayout layout;
  23. protected SerializableProperty property;
  24. protected string title;
  25. protected string path;
  26. protected int depth;
  27. protected SerializableProperty.FieldType type;
  28. /// <summary>
  29. /// Property this field is displaying contents of.
  30. /// </summary>
  31. public SerializableProperty Property
  32. {
  33. get { return property; }
  34. set
  35. {
  36. if (value != null && value.Type != type)
  37. {
  38. throw new ArgumentException(
  39. "Attempting to initialize an inspectable field with a property of invalid type.");
  40. }
  41. property = value;
  42. }
  43. }
  44. /// <summary>
  45. /// Creates a new inspectable field GUI for the specified property.
  46. /// </summary>
  47. /// <param name="context">Context shared by all inspectable fields created by the same parent.</param>
  48. /// <param name="title">Name of the property, or some other value to set as the title.</param>
  49. /// <param name="path">Full path to this property (includes name of this property and all parent properties).</param>
  50. /// <param name="type">Type of property this field will be used for displaying.</param>
  51. /// <param name="depth">Determines how deep within the inspector nesting hierarchy is this field. Some fields may
  52. /// contain other fields, in which case you should increase this value by one.</param>
  53. /// <param name="layout">Parent layout that all the field elements will be added to.</param>
  54. /// <param name="property">Serializable property referencing the array whose contents to display.</param>
  55. public InspectableField(InspectableContext context, string title, string path, SerializableProperty.FieldType type,
  56. int depth, InspectableFieldLayout layout, SerializableProperty property)
  57. {
  58. this.context = context;
  59. this.layout = layout;
  60. this.title = title;
  61. this.path = path;
  62. this.type = type;
  63. this.depth = depth;
  64. Property = property;
  65. }
  66. /// <summary>
  67. /// Checks if contents of the field have been modified, and updates them if needed.
  68. /// </summary>
  69. /// <param name="layoutIndex">Index in the parent's layout at which to insert the GUI elements for this field.
  70. /// </param>
  71. /// <returns>State representing was anything modified between two last calls to <see cref="Refresh"/>.</returns>
  72. public virtual InspectableState Refresh(int layoutIndex)
  73. {
  74. return InspectableState.NotModified;
  75. }
  76. /// <summary>
  77. /// Returns the total number of GUI elements in the field's layout.
  78. /// </summary>
  79. /// <returns>Number of GUI elements in the field's layout.</returns>
  80. public int GetNumLayoutElements()
  81. {
  82. return layout.NumElements;
  83. }
  84. /// <summary>
  85. /// Returns an optional title layout. Certain fields may contain separate title and content layouts. Parent fields
  86. /// may use the separate title layout instead of the content layout to append elements. Having a separate title
  87. /// layout is purely cosmetical.
  88. /// </summary>
  89. /// <returns>Title layout if the field has one, null otherwise.</returns>
  90. public virtual GUILayoutX GetTitleLayout()
  91. {
  92. return null;
  93. }
  94. /// <summary>
  95. /// Initializes the GUI elements for the field.
  96. /// </summary>
  97. /// <param name="layoutIndex">Index at which to insert the GUI elements.</param>
  98. protected internal abstract void Initialize(int layoutIndex);
  99. /// <summary>
  100. /// Destroys all GUI elements in the inspectable field.
  101. /// </summary>
  102. public virtual void Destroy()
  103. {
  104. layout.DestroyElements();
  105. }
  106. /// <summary>
  107. /// Allows the user to override the default inspector GUI for a specific field in an object. If this method
  108. /// returns null the default field will be used instead.
  109. /// </summary>
  110. /// <param name="field">Field to generate inspector GUI for.</param>
  111. /// <param name="context">Context shared by all inspectable fields created by the same parent.</param>
  112. /// <param name="path">Full path to the provided field (includes name of this field and all parent fields).</param>
  113. /// <param name="layout">Parent layout that all the field elements will be added to.</param>
  114. /// <param name="layoutIndex">Index into the parent layout at which to insert the GUI elements for the field .</param>
  115. /// <param name="depth">
  116. /// Determines how deep within the inspector nesting hierarchy is this field. Some fields may contain other fields,
  117. /// in which case you should increase this value by one.
  118. /// </param>
  119. /// <returns>
  120. /// Inspectable field implementation that can be used for displaying the GUI for the provided field. Or null if
  121. /// default field GUI should be used instead.
  122. /// </returns>
  123. public delegate InspectableField FieldOverrideCallback(SerializableField field, InspectableContext context, string path,
  124. InspectableFieldLayout layout, int layoutIndex, int depth);
  125. /// <summary>
  126. /// Creates inspectable fields all the fields/properties of the specified object.
  127. /// </summary>
  128. /// <param name="obj">Object whose fields the GUI will be drawn for.</param>
  129. /// <param name="context">Context shared by all inspectable fields created by the same parent.</param>
  130. /// <param name="path">Full path to the field this provided object was retrieved from.</param>
  131. /// <param name="depth">
  132. /// Determines how deep within the inspector nesting hierarchy is this objects. Some fields may contain other
  133. /// fields, in which case you should increase this value by one.
  134. /// </param>
  135. /// <param name="layout">Parent layout that all the field GUI elements will be added to.</param>
  136. /// <param name="overrideCallback">
  137. /// Optional callback that allows you to override the look of individual fields in the object. If non-null the
  138. /// callback will be called with information about every field in the provided object. If the callback returns
  139. /// non-null that inspectable field will be used for drawing the GUI, otherwise the default inspector field type
  140. /// will be used.
  141. /// </param>
  142. public static List<InspectableField> CreateFields(SerializableObject obj, InspectableContext context, string path,
  143. int depth, GUILayoutY layout, FieldOverrideCallback overrideCallback = null)
  144. {
  145. // Retrieve fields and sort by order
  146. SerializableField[] fields = obj.Fields;
  147. Array.Sort(fields,
  148. (x, y) =>
  149. {
  150. int orderX = x.Flags.HasFlag(SerializableFieldAttributes.Order) ? x.Style.Order : 0;
  151. int orderY = y.Flags.HasFlag(SerializableFieldAttributes.Order) ? y.Style.Order : 0;
  152. return orderX.CompareTo(orderY);
  153. });
  154. // Generate per-field GUI while grouping by category
  155. int rootIndex = 0;
  156. int categoryIndex = 0;
  157. string categoryName = null;
  158. InspectableCategory category = null;
  159. List<InspectableField> inspectableFields = new List<InspectableField>();
  160. foreach (var field in fields)
  161. {
  162. if (!field.Flags.HasFlag(SerializableFieldAttributes.Inspectable))
  163. continue;
  164. if (field.Flags.HasFlag(SerializableFieldAttributes.Category))
  165. {
  166. string newCategory = field.Style.CategoryName;
  167. if (!string.IsNullOrEmpty(newCategory) && categoryName != newCategory)
  168. {
  169. string categoryPath = path + "/[" + newCategory + "]";
  170. category = new InspectableCategory(context, newCategory, categoryPath, depth,
  171. new InspectableFieldLayout(layout));
  172. category.Initialize(rootIndex);
  173. category.Refresh(rootIndex);
  174. rootIndex += category.GetNumLayoutElements();
  175. inspectableFields.Add(category);
  176. categoryName = newCategory;
  177. categoryIndex = 0;
  178. }
  179. else
  180. {
  181. categoryName = null;
  182. category = null;
  183. }
  184. }
  185. int currentIndex;
  186. GUILayoutY parentLayout;
  187. if (category != null)
  188. {
  189. currentIndex = categoryIndex;
  190. parentLayout = category.ChildLayout;
  191. }
  192. else
  193. {
  194. currentIndex = rootIndex;
  195. parentLayout = layout;
  196. }
  197. string fieldName = GetReadableIdentifierName(field.Name);
  198. string childPath = string.IsNullOrEmpty(path) ? fieldName : path + "/" + fieldName;
  199. InspectableField inspectableField = null;
  200. if(overrideCallback != null)
  201. inspectableField = overrideCallback(field, context, path, new InspectableFieldLayout(parentLayout),
  202. currentIndex, depth);
  203. if (inspectableField == null)
  204. {
  205. inspectableField = CreateField(context, fieldName, childPath,
  206. currentIndex, depth, new InspectableFieldLayout(parentLayout), field.GetProperty(),
  207. InspectableFieldStyle.Create(field));
  208. }
  209. if (category != null)
  210. category.AddChild(inspectableField);
  211. else
  212. inspectableFields.Add(inspectableField);
  213. currentIndex += inspectableField.GetNumLayoutElements();
  214. if (category != null)
  215. categoryIndex = currentIndex;
  216. else
  217. rootIndex = currentIndex;
  218. }
  219. return inspectableFields;
  220. }
  221. /// <summary>
  222. /// Creates a new inspectable field, automatically detecting the most appropriate implementation for the type
  223. /// contained in the provided serializable property. This may be one of the built-in inspectable field implemetations
  224. /// (like ones for primitives like int or bool), or a user defined implementation defined with a
  225. /// <see cref="CustomInspector"/> attribute.
  226. /// </summary>
  227. /// <param name="context">Context shared by all inspectable fields created by the same parent.</param>
  228. /// <param name="title">Name of the property, or some other value to set as the title.</param>
  229. /// <param name="path">Full path to this property (includes name of this property and all parent properties).</param>
  230. /// <param name="layoutIndex">Index into the parent layout at which to insert the GUI elements for the field .</param>
  231. /// <param name="depth">Determines how deep within the inspector nesting hierarchy is this field. Some fields may
  232. /// contain other fields, in which case you should increase this value by one.</param>
  233. /// <param name="layout">Parent layout that all the field elements will be added to.</param>
  234. /// <param name="property">Serializable property referencing the array whose contents to display.</param>
  235. /// <param name="style">Information that can be used for customizing field rendering and behaviour.</param>
  236. /// <returns>Inspectable field implementation that can be used for displaying the GUI for a serializable property
  237. /// of the provided type.</returns>
  238. public static InspectableField CreateField(InspectableContext context, string title, string path, int layoutIndex,
  239. int depth, InspectableFieldLayout layout, SerializableProperty property, InspectableFieldStyleInfo style = null)
  240. {
  241. InspectableField field = null;
  242. Type type = property.InternalType;
  243. if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(RRef<>))
  244. type = type.GenericTypeArguments[0];
  245. Type customInspectable = InspectorUtility.GetCustomInspectable(type);
  246. if (customInspectable != null)
  247. {
  248. field = (InspectableField) Activator.CreateInstance(customInspectable, context, title, path, depth, layout,
  249. property, style);
  250. }
  251. else
  252. {
  253. switch (property.Type)
  254. {
  255. case SerializableProperty.FieldType.Int:
  256. if (style != null && style.StyleFlags.HasFlag(InspectableFieldStyleFlags.AsLayerMask))
  257. field = new InspectableLayerMask(context, title, path, depth, layout, property);
  258. else
  259. {
  260. if (style?.RangeStyle == null || !style.RangeStyle.Slider)
  261. field = new InspectableInt(context, title, path, depth, layout, property, style);
  262. else
  263. field = new InspectableRangedInt(context, title, path, depth, layout, property, style);
  264. }
  265. break;
  266. case SerializableProperty.FieldType.Float:
  267. if (style?.RangeStyle == null || !style.RangeStyle.Slider)
  268. field = new InspectableFloat(context, title, path, depth, layout, property, style);
  269. else
  270. field = new InspectableRangedFloat(context, title, path, depth, layout, property, style);
  271. break;
  272. case SerializableProperty.FieldType.Bool:
  273. field = new InspectableBool(context, title, path, depth, layout, property);
  274. break;
  275. case SerializableProperty.FieldType.Color:
  276. field = new InspectableColor(context, title, path, depth, layout, property);
  277. break;
  278. case SerializableProperty.FieldType.ColorGradient:
  279. field = new InspectableColorGradient(context, title, path, depth, layout, property);
  280. break;
  281. case SerializableProperty.FieldType.Curve:
  282. field = new InspectableCurve(context, title, path, depth, layout, property);
  283. break;
  284. case SerializableProperty.FieldType.FloatDistribution:
  285. field = new InspectableFloatDistribution(context, title, path, depth, layout, property);
  286. break;
  287. case SerializableProperty.FieldType.Vector2Distribution:
  288. field = new InspectableVector2Distribution(context, title, path, depth, layout, property);
  289. break;
  290. case SerializableProperty.FieldType.Vector3Distribution:
  291. field = new InspectableVector3Distribution(context, title, path, depth, layout, property);
  292. break;
  293. case SerializableProperty.FieldType.ColorDistribution:
  294. field = new InspectableColorDistribution(context, title, path, depth, layout, property);
  295. break;
  296. case SerializableProperty.FieldType.String:
  297. field = new InspectableString(context, title, path, depth, layout, property);
  298. break;
  299. case SerializableProperty.FieldType.Vector2:
  300. field = new InspectableVector2(context, title, path, depth, layout, property);
  301. break;
  302. case SerializableProperty.FieldType.Vector3:
  303. field = new InspectableVector3(context, title, path, depth, layout, property);
  304. break;
  305. case SerializableProperty.FieldType.Vector4:
  306. field = new InspectableVector4(context, title, path, depth, layout, property);
  307. break;
  308. case SerializableProperty.FieldType.Quaternion:
  309. if (style != null && style.StyleFlags.HasFlag(InspectableFieldStyleFlags.AsQuaternion))
  310. field = new InspectableQuaternion(context, title, path, depth, layout, property);
  311. else
  312. field = new InspectableEuler(context, title, path, depth, layout, property);
  313. break;
  314. case SerializableProperty.FieldType.Resource:
  315. field = new InspectableResource(context, title, path, depth, layout, property);
  316. break;
  317. case SerializableProperty.FieldType.RRef:
  318. field = new InspectableRRef(context, title, path, depth, layout, property);
  319. break;
  320. case SerializableProperty.FieldType.GameObjectRef:
  321. field = new InspectableGameObjectRef(context, title, path, depth, layout, property);
  322. break;
  323. case SerializableProperty.FieldType.Object:
  324. field = new InspectableObject(context, title, path, depth, layout, property, style);
  325. break;
  326. case SerializableProperty.FieldType.Array:
  327. field = new InspectableArray(context, title, path, depth, layout, property, style);
  328. break;
  329. case SerializableProperty.FieldType.List:
  330. field = new InspectableList(context, title, path, depth, layout, property);
  331. break;
  332. case SerializableProperty.FieldType.Dictionary:
  333. field = new InspectableDictionary(context, title, path, depth, layout, property);
  334. break;
  335. case SerializableProperty.FieldType.Enum:
  336. field = new InspectableEnum(context, title, path, depth, layout, property);
  337. break;
  338. }
  339. }
  340. if (field == null)
  341. throw new Exception("No inspector exists for the provided field type.");
  342. field.Initialize(layoutIndex);
  343. field.Refresh(layoutIndex);
  344. return field;
  345. }
  346. /// <summary>
  347. /// Converts a name of an identifier (such as a field or a property) into a human readable name.
  348. /// </summary>
  349. /// <param name="input">Identifier to convert.</param>
  350. /// <returns>Human readable name with spaces.</returns>
  351. public static string GetReadableIdentifierName(string input)
  352. {
  353. if (string.IsNullOrEmpty(input))
  354. return "";
  355. StringBuilder sb = new StringBuilder();
  356. bool nextUpperIsSpace = true;
  357. if (input[0] == '_')
  358. {
  359. // Skip
  360. nextUpperIsSpace = false;
  361. }
  362. else if (input[0] == 'm' && input.Length > 1 && char.IsUpper(input[1]))
  363. {
  364. // Skip
  365. nextUpperIsSpace = false;
  366. }
  367. else if (char.IsLower(input[0]))
  368. sb.Append(char.ToUpper(input[0]));
  369. else
  370. {
  371. sb.Append(input[0]);
  372. nextUpperIsSpace = false;
  373. }
  374. for (int i = 1; i < input.Length; i++)
  375. {
  376. if (input[i] == '_')
  377. {
  378. sb.Append(' ');
  379. nextUpperIsSpace = false;
  380. }
  381. else if (char.IsUpper(input[i]))
  382. {
  383. if (nextUpperIsSpace)
  384. {
  385. sb.Append(' ');
  386. sb.Append(input[i]);
  387. }
  388. else
  389. sb.Append(char.ToLower(input[i]));
  390. nextUpperIsSpace = false;
  391. }
  392. else
  393. {
  394. sb.Append(input[i]);
  395. nextUpperIsSpace = true;
  396. }
  397. }
  398. return sb.ToString();
  399. }
  400. }
  401. /// <summary>
  402. /// Contains information shared between multiple inspector fields.
  403. /// </summary>
  404. public class InspectableContext
  405. {
  406. /// <summary>
  407. /// Creates a new context.
  408. /// </summary>
  409. public InspectableContext()
  410. {
  411. Persistent = new SerializableProperties();
  412. }
  413. /// <summary>
  414. /// Creates a new context with user-provided peristent property storage.
  415. /// </summary>
  416. /// <param name="persistent">Existing object into which to inspectable fields can store persistent data.</param>
  417. public InspectableContext(SerializableProperties persistent)
  418. {
  419. Persistent = persistent;
  420. }
  421. /// <summary>
  422. /// A set of properties that the inspector can read/write. They will be persisted even after the inspector is closed
  423. /// and restored when it is re-opened.
  424. /// </summary>
  425. public SerializableProperties Persistent { get; }
  426. }
  427. /** @} */
  428. }