InspectableField.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  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. protected InspectableContext context;
  21. protected InspectableFieldLayout layout;
  22. protected SerializableProperty property;
  23. protected string title;
  24. protected string path;
  25. protected string name;
  26. protected int depth;
  27. protected bool active = true;
  28. protected SerializableProperty.FieldType type;
  29. /// <summary>
  30. /// Property this field is displaying contents of.
  31. /// </summary>
  32. public SerializableProperty Property
  33. {
  34. get { return property; }
  35. set
  36. {
  37. if (value != null && value.Type != type)
  38. {
  39. throw new ArgumentException(
  40. "Attempting to initialize an inspectable field with a property of invalid type.");
  41. }
  42. property = value;
  43. }
  44. }
  45. /// <summary>
  46. /// Returns the path to the field.
  47. /// </summary>
  48. public string Path => path;
  49. /// <summary>
  50. /// Name portion of the field path.
  51. /// </summary>
  52. public string Name => name;
  53. /// <summary>
  54. /// Activates or deactivates the underlying GUI elements.
  55. /// </summary>
  56. public bool Active
  57. {
  58. get => active;
  59. set => SetActive(value);
  60. }
  61. /// <summary>
  62. /// Creates a new inspectable field GUI for the specified property.
  63. /// </summary>
  64. /// <param name="context">Context shared by all inspectable fields created by the same parent.</param>
  65. /// <param name="title">Name of the property, or some other value to set as the title.</param>
  66. /// <param name="path">Full path to this property (includes name of this property and all parent properties).</param>
  67. /// <param name="type">Type of property this field will be used for displaying.</param>
  68. /// <param name="depth">Determines how deep within the inspector nesting hierarchy is this field. Some fields may
  69. /// contain other fields, in which case you should increase this value by one.</param>
  70. /// <param name="layout">Parent layout that all the field elements will be added to.</param>
  71. /// <param name="property">Serializable property referencing the array whose contents to display.</param>
  72. public InspectableField(InspectableContext context, string title, string path, SerializableProperty.FieldType type,
  73. int depth, InspectableFieldLayout layout, SerializableProperty property)
  74. {
  75. this.context = context;
  76. this.layout = layout;
  77. this.title = title;
  78. this.path = path;
  79. this.type = type;
  80. this.depth = depth;
  81. if (path != null)
  82. {
  83. int lastSlash = path.LastIndexOf('/');
  84. if (lastSlash == -1)
  85. name = path;
  86. else
  87. name = path.Substring(lastSlash);
  88. }
  89. Property = property;
  90. }
  91. /// <summary>
  92. /// Checks if contents of the field have been modified, and updates them if needed.
  93. /// </summary>
  94. /// <param name="layoutIndex">Index in the parent's layout at which to insert the GUI elements for this field.
  95. /// </param>
  96. /// <returns>State representing was anything modified between two last calls to <see cref="Refresh"/>.</returns>
  97. public virtual InspectableState Refresh(int layoutIndex)
  98. {
  99. return InspectableState.NotModified;
  100. }
  101. /// <summary>
  102. /// Returns the total number of GUI elements in the field's layout.
  103. /// </summary>
  104. /// <returns>Number of GUI elements in the field's layout.</returns>
  105. public int GetNumLayoutElements()
  106. {
  107. return layout.NumElements;
  108. }
  109. /// <summary>
  110. /// Returns an optional title layout. Certain fields may contain separate title and content layouts. Parent fields
  111. /// may use the separate title layout instead of the content layout to append elements. Having a separate title
  112. /// layout is purely cosmetical.
  113. /// </summary>
  114. /// <returns>Title layout if the field has one, null otherwise.</returns>
  115. public virtual GUILayoutX GetTitleLayout()
  116. {
  117. return null;
  118. }
  119. /// <summary>
  120. /// Initializes the GUI elements for the field.
  121. /// </summary>
  122. /// <param name="layoutIndex">Index at which to insert the GUI elements.</param>
  123. protected internal abstract void Initialize(int layoutIndex);
  124. /// <summary>
  125. /// Destroys all GUI elements in the inspectable field.
  126. /// </summary>
  127. public virtual void Destroy()
  128. {
  129. layout.DestroyElements();
  130. }
  131. /// <summary>
  132. /// Moves keyboard focus to this field.
  133. /// </summary>
  134. /// <param name="subFieldName">
  135. /// Name of the sub-field to focus on. Only relevant if the inspectable field represents multiple GUI
  136. /// input elements.
  137. /// </param>
  138. public virtual void SetHasFocus(string subFieldName = null) { }
  139. /// <summary>
  140. /// Searches for a child field with the specified path.
  141. /// </summary>
  142. /// <param name="path">
  143. /// Path relative to the current field. Path entries are field names separated with "/". Fields within
  144. /// categories are placed within a special category group, surrounded by "[]". Some examples:
  145. /// - myField
  146. /// - myObject/myField
  147. /// - myObject/[myCategory]/myField
  148. /// </param>
  149. /// <returns>Matching field if one is found, null otherwise.</returns>
  150. public virtual InspectableField FindPath(string path)
  151. {
  152. return null;
  153. }
  154. /// <summary>
  155. /// Searches for a field with the specified path.
  156. /// </summary>
  157. /// <param name="path">
  158. /// Path to search for. Path entries are readable field names separated with "/". Fields within categories are
  159. /// placed within a special category group, surrounded by "[]". Some examples:
  160. /// - myField
  161. /// - myObject/myField
  162. /// - myObject/[myCategory]/myField
  163. /// </param>
  164. /// <param name="depth">Path depth at which the provided set of fields is at.</param>
  165. /// <param name="fields">List of fields to search. Children will be searched recursively.</param>
  166. /// <returns>Matching field if one is found, null otherwise.</returns>
  167. public static InspectableField FindPath(string path, int depth, IEnumerable<InspectableField> fields)
  168. {
  169. string subPath = GetSubPath(path, depth + 1);
  170. foreach (var field in fields)
  171. {
  172. InspectableField foundField = null;
  173. if (field.path == path)
  174. foundField = field;
  175. else if (field.path == subPath)
  176. foundField = field.FindPath(path);
  177. if (foundField != null)
  178. return foundField;
  179. }
  180. return null;
  181. }
  182. /// <summary>
  183. /// Returns the top-most part of the provided field path.
  184. /// See <see cref="FindPath(string, int, IEnumerable{InspectableField})"/> for more information about paths.
  185. /// </summary>
  186. /// <param name="path">Path to return the sub-path of.</param>
  187. /// <param name="count">Number of path elements to retrieve.</param>
  188. /// <returns>First <paramref name="count"/> elements of the path.</returns>
  189. public static string GetSubPath(string path, int count)
  190. {
  191. if (count <= 0)
  192. return null;
  193. StringBuilder sb = new StringBuilder();
  194. int foundSections = 0;
  195. bool gotFirstChar = false;
  196. for (int i = 0; i < path.Length; i++)
  197. {
  198. if (path[i] == '/')
  199. {
  200. if (!gotFirstChar)
  201. {
  202. gotFirstChar = true;
  203. continue;
  204. }
  205. foundSections++;
  206. if (foundSections == count)
  207. break;
  208. }
  209. sb.Append(path[i]);
  210. gotFirstChar = true;
  211. }
  212. return sb.ToString();
  213. }
  214. /// <summary>
  215. /// Zero parameter wrapper for <see cref="StartUndo(string)"/>
  216. /// </summary>
  217. protected void StartUndo()
  218. {
  219. StartUndo(null);
  220. }
  221. /// <summary>
  222. /// Notifies the system to start recording a new undo command. Any changes to the field after this is called
  223. /// will be recorded in the command. User must call <see cref="EndUndo"/> after field is done being changed.
  224. /// </summary>
  225. /// <param name="subPath">Additional path to append to the end of the current field path.</param>
  226. protected void StartUndo(string subPath)
  227. {
  228. if (context.Component != null)
  229. {
  230. string fullPath = path;
  231. if (!string.IsNullOrEmpty(subPath))
  232. fullPath = path.TrimEnd('/') + '/' + subPath.TrimStart('/');
  233. GameObjectUndo.RecordComponent(context.Component, fullPath);
  234. }
  235. }
  236. /// <summary>
  237. /// Finishes recording an undo command started via <see cref="StartUndo(string)"/>. If any changes are detected on
  238. /// the field an undo command is recorded onto the undo-redo stack, otherwise nothing is done.
  239. /// </summary>
  240. protected void EndUndo()
  241. {
  242. GameObjectUndo.ResolveDiffs();
  243. }
  244. /// <summary>
  245. /// Activates or deactivates the underlying GUI elements.
  246. /// </summary>
  247. protected virtual void SetActive(bool active)
  248. {
  249. if (this.active != active)
  250. {
  251. layout.SetActive(active);
  252. this.active = active;
  253. }
  254. }
  255. /// <summary>
  256. /// Creates a new inspectable field, automatically detecting the most appropriate implementation for the type
  257. /// contained in the provided serializable property. This may be one of the built-in inspectable field implemetations
  258. /// (like ones for primitives like int or bool), or a user defined implementation defined with a
  259. /// <see cref="CustomInspector"/> attribute.
  260. /// </summary>
  261. /// <param name="context">Context shared by all inspectable fields created by the same parent.</param>
  262. /// <param name="title">Name of the property, or some other value to set as the title.</param>
  263. /// <param name="path">Full path to this property (includes name of this property and all parent properties).</param>
  264. /// <param name="layoutIndex">Index into the parent layout at which to insert the GUI elements for the field .</param>
  265. /// <param name="depth">Determines how deep within the inspector nesting hierarchy is this field. Some fields may
  266. /// contain other fields, in which case you should increase this value by one.</param>
  267. /// <param name="layout">Parent layout that all the field elements will be added to.</param>
  268. /// <param name="property">Serializable property referencing the array whose contents to display.</param>
  269. /// <param name="style">Information that can be used for customizing field rendering and behaviour.</param>
  270. /// <returns>Inspectable field implementation that can be used for displaying the GUI for a serializable property
  271. /// of the provided type.</returns>
  272. public static InspectableField CreateField(InspectableContext context, string title, string path, int layoutIndex,
  273. int depth, InspectableFieldLayout layout, SerializableProperty property, InspectableFieldStyleInfo style = null)
  274. {
  275. InspectableField field = null;
  276. Type type = property.InternalType;
  277. if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(RRef<>))
  278. type = type.GenericTypeArguments[0];
  279. Type customInspectable = InspectorUtility.GetCustomInspectable(type);
  280. if (customInspectable != null)
  281. {
  282. field = (InspectableField) Activator.CreateInstance(customInspectable, context, title, path, depth, layout,
  283. property, style);
  284. }
  285. else
  286. {
  287. switch (property.Type)
  288. {
  289. case SerializableProperty.FieldType.Int:
  290. if (style != null && style.StyleFlags.HasFlag(InspectableFieldStyleFlags.AsLayerMask))
  291. field = new InspectableLayerMask(context, title, path, depth, layout, property);
  292. else
  293. {
  294. if (style?.RangeStyle == null || !style.RangeStyle.Slider)
  295. field = new InspectableInt(context, title, path, depth, layout, property, style);
  296. else
  297. field = new InspectableRangedInt(context, title, path, depth, layout, property, style);
  298. }
  299. break;
  300. case SerializableProperty.FieldType.Float:
  301. if (style?.RangeStyle == null || !style.RangeStyle.Slider)
  302. field = new InspectableFloat(context, title, path, depth, layout, property, style);
  303. else
  304. field = new InspectableRangedFloat(context, title, path, depth, layout, property, style);
  305. break;
  306. case SerializableProperty.FieldType.Bool:
  307. field = new InspectableBool(context, title, path, depth, layout, property);
  308. break;
  309. case SerializableProperty.FieldType.Color:
  310. field = new InspectableColor(context, title, path, depth, layout, property);
  311. break;
  312. case SerializableProperty.FieldType.ColorGradient:
  313. field = new InspectableColorGradient(context, title, path, depth, layout, property);
  314. break;
  315. case SerializableProperty.FieldType.Curve:
  316. field = new InspectableCurve(context, title, path, depth, layout, property);
  317. break;
  318. case SerializableProperty.FieldType.FloatDistribution:
  319. field = new InspectableFloatDistribution(context, title, path, depth, layout, property);
  320. break;
  321. case SerializableProperty.FieldType.Vector2Distribution:
  322. field = new InspectableVector2Distribution(context, title, path, depth, layout, property);
  323. break;
  324. case SerializableProperty.FieldType.Vector3Distribution:
  325. field = new InspectableVector3Distribution(context, title, path, depth, layout, property);
  326. break;
  327. case SerializableProperty.FieldType.ColorDistribution:
  328. field = new InspectableColorDistribution(context, title, path, depth, layout, property);
  329. break;
  330. case SerializableProperty.FieldType.String:
  331. field = new InspectableString(context, title, path, depth, layout, property);
  332. break;
  333. case SerializableProperty.FieldType.Vector2:
  334. field = new InspectableVector2(context, title, path, depth, layout, property);
  335. break;
  336. case SerializableProperty.FieldType.Vector3:
  337. field = new InspectableVector3(context, title, path, depth, layout, property);
  338. break;
  339. case SerializableProperty.FieldType.Vector4:
  340. field = new InspectableVector4(context, title, path, depth, layout, property);
  341. break;
  342. case SerializableProperty.FieldType.Quaternion:
  343. if (style != null && style.StyleFlags.HasFlag(InspectableFieldStyleFlags.AsQuaternion))
  344. field = new InspectableQuaternion(context, title, path, depth, layout, property);
  345. else
  346. field = new InspectableEuler(context, title, path, depth, layout, property);
  347. break;
  348. case SerializableProperty.FieldType.Resource:
  349. field = new InspectableResource(context, title, path, depth, layout, property);
  350. break;
  351. case SerializableProperty.FieldType.RRef:
  352. field = new InspectableRRef(context, title, path, depth, layout, property, style);
  353. break;
  354. case SerializableProperty.FieldType.GameObjectRef:
  355. field = new InspectableGameObjectRef(context, title, path, depth, layout, property);
  356. break;
  357. case SerializableProperty.FieldType.Object:
  358. field = new InspectableObject(context, title, path, depth, layout, property, style);
  359. break;
  360. case SerializableProperty.FieldType.Array:
  361. field = new InspectableArray(context, title, path, depth, layout, property, style);
  362. break;
  363. case SerializableProperty.FieldType.List:
  364. field = new InspectableList(context, title, path, depth, layout, property);
  365. break;
  366. case SerializableProperty.FieldType.Dictionary:
  367. field = new InspectableDictionary(context, title, path, depth, layout, property);
  368. break;
  369. case SerializableProperty.FieldType.Enum:
  370. field = new InspectableEnum(context, title, path, depth, layout, property);
  371. break;
  372. }
  373. }
  374. if (field == null)
  375. throw new Exception("No inspector exists for the provided field type.");
  376. field.Initialize(layoutIndex);
  377. field.Refresh(layoutIndex);
  378. return field;
  379. }
  380. /// <summary>
  381. /// Converts a name of an identifier (such as a field or a property) into a human readable name.
  382. /// </summary>
  383. /// <param name="input">Identifier to convert.</param>
  384. /// <returns>Human readable name with spaces.</returns>
  385. public static string GetReadableIdentifierName(string input)
  386. {
  387. if (string.IsNullOrEmpty(input))
  388. return "";
  389. StringBuilder sb = new StringBuilder();
  390. bool nextUpperIsSpace = true;
  391. if (input[0] == '_')
  392. {
  393. // Skip
  394. nextUpperIsSpace = false;
  395. }
  396. else if (input[0] == 'm' && input.Length > 1 && char.IsUpper(input[1]))
  397. {
  398. // Skip
  399. nextUpperIsSpace = false;
  400. }
  401. else if (char.IsLower(input[0]))
  402. sb.Append(char.ToUpper(input[0]));
  403. else
  404. {
  405. sb.Append(input[0]);
  406. nextUpperIsSpace = false;
  407. }
  408. for (int i = 1; i < input.Length; i++)
  409. {
  410. if (input[i] == '_')
  411. {
  412. sb.Append(' ');
  413. nextUpperIsSpace = false;
  414. }
  415. else if (char.IsUpper(input[i]))
  416. {
  417. if (nextUpperIsSpace)
  418. {
  419. sb.Append(' ');
  420. sb.Append(input[i]);
  421. }
  422. else
  423. sb.Append(char.ToLower(input[i]));
  424. nextUpperIsSpace = false;
  425. }
  426. else
  427. {
  428. sb.Append(input[i]);
  429. nextUpperIsSpace = true;
  430. }
  431. }
  432. return sb.ToString();
  433. }
  434. }
  435. /// <summary>
  436. /// Contains information shared between multiple inspector fields.
  437. /// </summary>
  438. public class InspectableContext
  439. {
  440. /// <summary>
  441. /// Creates a new context.
  442. /// </summary>
  443. /// <param name="component">
  444. /// Component object that inspector fields are editing. Can be null if the object being edited is not a component.
  445. /// </param>
  446. public InspectableContext(Component component = null)
  447. {
  448. Persistent = new SerializableProperties();
  449. Component = component;
  450. }
  451. /// <summary>
  452. /// Creates a new context with user-provided persistent property storage.
  453. /// </summary>
  454. /// <param name="persistent">Existing object into which to inspectable fields can store persistent data.</param>
  455. /// <param name="component">
  456. /// Component object that inspector fields are editing. Can be null if the object being edited is not a component.
  457. /// </param>
  458. public InspectableContext(SerializableProperties persistent, Component component = null)
  459. {
  460. Persistent = persistent;
  461. Component = component;
  462. }
  463. /// <summary>
  464. /// A set of properties that the inspector can read/write. They will be persisted even after the inspector is closed
  465. /// and restored when it is re-opened.
  466. /// </summary>
  467. public SerializableProperties Persistent { get; }
  468. /// <summary>
  469. /// Component object that inspector fields are editing. Can be null if the object being edited is not a component.
  470. /// </summary>
  471. public Component Component { get; }
  472. }
  473. /** @} */
  474. }