InspectableField.cs 23 KB

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