InspectableField.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  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. if (foundSections == 0)
  214. sb.Append(path);
  215. return sb.ToString();
  216. }
  217. /// <summary>
  218. /// Zero parameter wrapper for <see cref="StartUndo(string)"/>
  219. /// </summary>
  220. protected void StartUndo()
  221. {
  222. StartUndo(null);
  223. }
  224. /// <summary>
  225. /// Notifies the system to start recording a new undo command. Any changes to the field after this is called
  226. /// will be recorded in the command. User must call <see cref="EndUndo"/> after field is done being changed.
  227. /// </summary>
  228. /// <param name="subPath">Additional path to append to the end of the current field path.</param>
  229. protected void StartUndo(string subPath)
  230. {
  231. if (context.Component != null)
  232. {
  233. string fullPath = path;
  234. if (!string.IsNullOrEmpty(subPath))
  235. fullPath = path.TrimEnd('/') + '/' + subPath.TrimStart('/');
  236. GameObjectUndo.RecordComponent(context.Component, fullPath);
  237. }
  238. }
  239. /// <summary>
  240. /// Finishes recording an undo command started via <see cref="StartUndo(string)"/>. If any changes are detected on
  241. /// the field an undo command is recorded onto the undo-redo stack, otherwise nothing is done.
  242. /// </summary>
  243. protected void EndUndo()
  244. {
  245. GameObjectUndo.ResolveDiffs();
  246. }
  247. /// <summary>
  248. /// Activates or deactivates the underlying GUI elements.
  249. /// </summary>
  250. protected virtual void SetActive(bool active)
  251. {
  252. if (this.active != active)
  253. {
  254. layout.SetActive(active);
  255. this.active = active;
  256. }
  257. }
  258. /// <summary>
  259. /// Creates a new inspectable field, automatically detecting the most appropriate implementation for the type
  260. /// contained in the provided serializable property. This may be one of the built-in inspectable field implemetations
  261. /// (like ones for primitives like int or bool), or a user defined implementation defined with a
  262. /// <see cref="CustomInspector"/> attribute.
  263. /// </summary>
  264. /// <param name="context">Context shared by all inspectable fields created by the same parent.</param>
  265. /// <param name="title">Name of the property, or some other value to set as the title.</param>
  266. /// <param name="path">Full path to this property (includes name of this property and all parent properties).</param>
  267. /// <param name="layoutIndex">Index into the parent layout at which to insert the GUI elements for the field .</param>
  268. /// <param name="depth">Determines how deep within the inspector nesting hierarchy is this field. Some fields may
  269. /// contain other fields, in which case you should increase this value by one.</param>
  270. /// <param name="layout">Parent layout that all the field elements will be added to.</param>
  271. /// <param name="property">Serializable property referencing the array whose contents to display.</param>
  272. /// <param name="style">Information that can be used for customizing field rendering and behaviour.</param>
  273. /// <returns>Inspectable field implementation that can be used for displaying the GUI for a serializable property
  274. /// of the provided type.</returns>
  275. public static InspectableField CreateField(InspectableContext context, string title, string path, int layoutIndex,
  276. int depth, InspectableFieldLayout layout, SerializableProperty property, InspectableFieldStyleInfo style = null)
  277. {
  278. InspectableField field = null;
  279. Type type = property.InternalType;
  280. if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(RRef<>))
  281. type = type.GenericTypeArguments[0];
  282. Type customInspectable = InspectorUtility.GetCustomInspectable(type);
  283. if (customInspectable != null)
  284. {
  285. field = (InspectableField) Activator.CreateInstance(customInspectable, context, title, path, depth, layout,
  286. property, style);
  287. }
  288. else
  289. {
  290. switch (property.Type)
  291. {
  292. case SerializableProperty.FieldType.Int:
  293. if (style != null && style.StyleFlags.HasFlag(InspectableFieldStyleFlags.AsLayerMask))
  294. field = new InspectableLayerMask(context, title, path, depth, layout, property);
  295. else
  296. {
  297. if (style?.RangeStyle == null || !style.RangeStyle.Slider)
  298. field = new InspectableInt(context, title, path, depth, layout, property, style);
  299. else
  300. field = new InspectableRangedInt(context, title, path, depth, layout, property, style);
  301. }
  302. break;
  303. case SerializableProperty.FieldType.Float:
  304. if (style?.RangeStyle == null || !style.RangeStyle.Slider)
  305. field = new InspectableFloat(context, title, path, depth, layout, property, style);
  306. else
  307. field = new InspectableRangedFloat(context, title, path, depth, layout, property, style);
  308. break;
  309. case SerializableProperty.FieldType.Bool:
  310. field = new InspectableBool(context, title, path, depth, layout, property);
  311. break;
  312. case SerializableProperty.FieldType.Color:
  313. field = new InspectableColor(context, title, path, depth, layout, property);
  314. break;
  315. case SerializableProperty.FieldType.ColorGradient:
  316. field = new InspectableColorGradient(context, title, path, depth, layout, property);
  317. break;
  318. case SerializableProperty.FieldType.Curve:
  319. field = new InspectableCurve(context, title, path, depth, layout, property);
  320. break;
  321. case SerializableProperty.FieldType.FloatDistribution:
  322. field = new InspectableFloatDistribution(context, title, path, depth, layout, property);
  323. break;
  324. case SerializableProperty.FieldType.Vector2Distribution:
  325. field = new InspectableVector2Distribution(context, title, path, depth, layout, property);
  326. break;
  327. case SerializableProperty.FieldType.Vector3Distribution:
  328. field = new InspectableVector3Distribution(context, title, path, depth, layout, property);
  329. break;
  330. case SerializableProperty.FieldType.ColorDistribution:
  331. field = new InspectableColorDistribution(context, title, path, depth, layout, property);
  332. break;
  333. case SerializableProperty.FieldType.String:
  334. field = new InspectableString(context, title, path, depth, layout, property);
  335. break;
  336. case SerializableProperty.FieldType.Vector2:
  337. field = new InspectableVector2(context, title, path, depth, layout, property);
  338. break;
  339. case SerializableProperty.FieldType.Vector3:
  340. field = new InspectableVector3(context, title, path, depth, layout, property);
  341. break;
  342. case SerializableProperty.FieldType.Vector4:
  343. field = new InspectableVector4(context, title, path, depth, layout, property);
  344. break;
  345. case SerializableProperty.FieldType.Quaternion:
  346. if (style != null && style.StyleFlags.HasFlag(InspectableFieldStyleFlags.AsQuaternion))
  347. field = new InspectableQuaternion(context, title, path, depth, layout, property);
  348. else
  349. field = new InspectableEuler(context, title, path, depth, layout, property);
  350. break;
  351. case SerializableProperty.FieldType.Resource:
  352. field = new InspectableResource(context, title, path, depth, layout, property);
  353. break;
  354. case SerializableProperty.FieldType.RRef:
  355. field = new InspectableRRef(context, title, path, depth, layout, property, style);
  356. break;
  357. case SerializableProperty.FieldType.GameObjectRef:
  358. field = new InspectableGameObjectRef(context, title, path, depth, layout, property);
  359. break;
  360. case SerializableProperty.FieldType.Object:
  361. field = new InspectableObject(context, title, path, depth, layout, property, style);
  362. break;
  363. case SerializableProperty.FieldType.Array:
  364. field = new InspectableArray(context, title, path, depth, layout, property, style);
  365. break;
  366. case SerializableProperty.FieldType.List:
  367. field = new InspectableList(context, title, path, depth, layout, property);
  368. break;
  369. case SerializableProperty.FieldType.Dictionary:
  370. field = new InspectableDictionary(context, title, path, depth, layout, property);
  371. break;
  372. case SerializableProperty.FieldType.Enum:
  373. field = new InspectableEnum(context, title, path, depth, layout, property);
  374. break;
  375. }
  376. }
  377. if (field == null)
  378. throw new Exception("No inspector exists for the provided field type.");
  379. field.Initialize(layoutIndex);
  380. field.Refresh(layoutIndex);
  381. return field;
  382. }
  383. /// <summary>
  384. /// Converts a name of an identifier (such as a field or a property) into a human readable name.
  385. /// </summary>
  386. /// <param name="input">Identifier to convert.</param>
  387. /// <returns>Human readable name with spaces.</returns>
  388. public static string GetReadableIdentifierName(string input)
  389. {
  390. if (string.IsNullOrEmpty(input))
  391. return "";
  392. StringBuilder sb = new StringBuilder();
  393. bool nextUpperIsSpace = true;
  394. if (input[0] == '_')
  395. {
  396. // Skip
  397. nextUpperIsSpace = false;
  398. }
  399. else if (input[0] == 'm' && input.Length > 1 && char.IsUpper(input[1]))
  400. {
  401. // Skip
  402. nextUpperIsSpace = false;
  403. }
  404. else if (char.IsLower(input[0]))
  405. sb.Append(char.ToUpper(input[0]));
  406. else
  407. {
  408. sb.Append(input[0]);
  409. nextUpperIsSpace = false;
  410. }
  411. for (int i = 1; i < input.Length; i++)
  412. {
  413. if (input[i] == '_')
  414. {
  415. sb.Append(' ');
  416. nextUpperIsSpace = false;
  417. }
  418. else if (char.IsUpper(input[i]))
  419. {
  420. if (nextUpperIsSpace)
  421. {
  422. sb.Append(' ');
  423. sb.Append(input[i]);
  424. }
  425. else
  426. sb.Append(char.ToLower(input[i]));
  427. nextUpperIsSpace = false;
  428. }
  429. else
  430. {
  431. sb.Append(input[i]);
  432. nextUpperIsSpace = true;
  433. }
  434. }
  435. return sb.ToString();
  436. }
  437. }
  438. /// <summary>
  439. /// Contains information shared between multiple inspector fields.
  440. /// </summary>
  441. public class InspectableContext
  442. {
  443. /// <summary>
  444. /// Creates a new context.
  445. /// </summary>
  446. /// <param name="component">
  447. /// Component object that inspector fields are editing. Can be null if the object being edited is not a component.
  448. /// </param>
  449. public InspectableContext(Component component = null)
  450. {
  451. Persistent = new SerializableProperties();
  452. Component = component;
  453. }
  454. /// <summary>
  455. /// Creates a new context with user-provided persistent property storage.
  456. /// </summary>
  457. /// <param name="persistent">Existing object into which to inspectable fields can store persistent data.</param>
  458. /// <param name="component">
  459. /// Component object that inspector fields are editing. Can be null if the object being edited is not a component.
  460. /// </param>
  461. public InspectableContext(SerializableProperties persistent, Component component = null)
  462. {
  463. Persistent = persistent;
  464. Component = component;
  465. }
  466. /// <summary>
  467. /// A set of properties that the inspector can read/write. They will be persisted even after the inspector is closed
  468. /// and restored when it is re-opened.
  469. /// </summary>
  470. public SerializableProperties Persistent { get; }
  471. /// <summary>
  472. /// Component object that inspector fields are editing. Can be null if the object being edited is not a component.
  473. /// </summary>
  474. public Component Component { get; }
  475. }
  476. /** @} */
  477. }