InspectableField.cs 29 KB

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