InspectableArray.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. //********************************** Banshee Engine (www.banshee3d.com) **************************************************//
  2. //**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
  3. using System;
  4. using System.Text;
  5. using bs;
  6. namespace bs.Editor
  7. {
  8. /** @addtogroup Inspector
  9. * @{
  10. */
  11. /// <summary>
  12. /// Displays GUI for a serializable property containing an array. Array contents are displayed as rows of entries
  13. /// that can be shown, hidden or manipulated.
  14. /// </summary>
  15. public class InspectableArray : InspectableField
  16. {
  17. private InspectableArrayGUI arrayGUIField;
  18. private InspectableFieldStyleInfo style;
  19. /// <summary>
  20. /// Style applied to the elements of the array and the array itself.
  21. /// </summary>
  22. internal InspectableFieldStyleInfo Style
  23. {
  24. get { return style; }
  25. }
  26. /// <summary>
  27. /// Creates a new inspectable array GUI for the specified property.
  28. /// </summary>
  29. /// <param name="context">Context shared by all inspectable fields created by the same parent.</param>
  30. /// <param name="title">Name of the property, or some other value to set as the title.</param>
  31. /// <param name="path">Full path to this property (includes name of this property and all parent properties).</param>
  32. /// <param name="depth">Determines how deep within the inspector nesting hierarchy is this field. Some fields may
  33. /// contain other fields, in which case you should increase this value by one.</param>
  34. /// <param name="layout">Parent layout that all the field elements will be added to.</param>
  35. /// <param name="property">Serializable property referencing the array whose contents to display.</param>
  36. /// <param name="style">Information that can be used for customizing field rendering and behaviour.</param>
  37. public InspectableArray(InspectableContext context, string title, string path, int depth, InspectableFieldLayout layout,
  38. SerializableProperty property, InspectableFieldStyleInfo style)
  39. : base(context, title, path, SerializableProperty.FieldType.Array, depth, layout, property)
  40. {
  41. this.style = style;
  42. }
  43. /// <inheritdoc/>
  44. public override GUILayoutX GetTitleLayout()
  45. {
  46. return arrayGUIField.GetTitleLayout();
  47. }
  48. /// <inheritdoc/>
  49. public override InspectableState Refresh(int layoutIndex, bool force = false)
  50. {
  51. return arrayGUIField.Refresh(force);
  52. }
  53. /// <inheritdoc/>
  54. protected internal override void Initialize(int layoutIndex)
  55. {
  56. GUILayout arrayLayout = layout.AddLayoutY(layoutIndex);
  57. arrayGUIField = InspectableArrayGUI.Create(context, title, path, property, arrayLayout, depth, style);
  58. arrayGUIField.IsExpanded = context.Persistent.GetBool(path + "_Expanded");
  59. arrayGUIField.OnExpand += x => context.Persistent.SetBool(path + "_Expanded", x);
  60. }
  61. /// <inheritdoc />
  62. public override InspectableField FindPath(string path)
  63. {
  64. string subPath = GetSubPath(path, depth + 1);
  65. if (string.IsNullOrEmpty(subPath))
  66. return null;
  67. int lastLeftIdx = subPath.LastIndexOf('[');
  68. int lastRightIdx = subPath.LastIndexOf(']', lastLeftIdx);
  69. if (lastLeftIdx == -1 || lastRightIdx == -1)
  70. return null;
  71. int count = lastRightIdx - 1 - lastLeftIdx;
  72. if (count <= 0)
  73. return null;
  74. string arrayIdxStr = subPath.Substring(lastLeftIdx, count);
  75. if (!int.TryParse(arrayIdxStr, out int idx))
  76. return null;
  77. if (idx >= arrayGUIField.NumRows)
  78. return null;
  79. InspectableArrayGUIRow row = arrayGUIField.GetRow(idx);
  80. InspectableField field = row?.Field;
  81. if (field != null)
  82. {
  83. if (field.Path == path)
  84. return field;
  85. return field.FindPath(path);
  86. }
  87. return null;
  88. }
  89. /// <summary>
  90. /// Handles creation of GUI elements for a GUI list field that displays a <see cref="SerializableArray"/> object.
  91. /// </summary>
  92. private class InspectableArrayGUI : GUIListFieldBase
  93. {
  94. private Array array;
  95. private int numElements;
  96. private InspectableContext context;
  97. private SerializableProperty property;
  98. private string path;
  99. private InspectableFieldStyleInfo style;
  100. /// <summary>
  101. /// Context shared by all inspectable fields created by the same parent.
  102. /// </summary>
  103. public InspectableContext Context
  104. {
  105. get { return context; }
  106. }
  107. /// <summary>
  108. /// Returns a property path to the array field (name of the array field and all parent object fields).
  109. /// </summary>
  110. public string Path
  111. {
  112. get { return path; }
  113. }
  114. /// <summary>
  115. /// Style applied to the elements of the array and the array itself.
  116. /// </summary>
  117. public InspectableFieldStyleInfo Style
  118. {
  119. get { return style; }
  120. }
  121. /// <summary>
  122. /// Returns the number of rows in the array.
  123. /// </summary>
  124. public int NumRows
  125. {
  126. get { return GetNumRows(); }
  127. }
  128. /// <summary>
  129. /// Constructs a new inspectable array GUI.
  130. /// </summary>
  131. /// <param name="context">Context shared by all inspectable fields created by the same parent.</param>
  132. /// <param name="title">Label to display on the list GUI title.</param>
  133. /// <param name="path">Full path to this property (includes name of this property and all parent properties).
  134. /// </param>
  135. /// <param name="property">Serializable property referencing a single-dimensional array.</param>
  136. /// <param name="layout">Layout to which to append the list GUI elements to.</param>
  137. /// <param name="depth">Determines at which depth to render the background. Useful when you have multiple
  138. /// nested containers whose backgrounds are overlaping. Also determines background style,
  139. /// depths divisible by two will use an alternate style.</param>
  140. /// <param name="style">Information that can be used for customizing field rendering and behaviour.</param>
  141. public InspectableArrayGUI(InspectableContext context, LocString title, string path, SerializableProperty property,
  142. GUILayout layout, int depth, InspectableFieldStyleInfo style)
  143. : base(title, layout, depth)
  144. {
  145. this.property = property;
  146. this.context = context;
  147. this.path = path;
  148. this.style = style;
  149. array = property.GetValue<Array>();
  150. if (array != null)
  151. numElements = array.Length;
  152. }
  153. /// <summary>
  154. /// Creates a new inspectable array GUI object that displays the contents of the provided serializable property.
  155. /// </summary>
  156. /// <param name="context">Context shared by all inspectable fields created by the same parent.</param>
  157. /// <param name="title">Label to display on the list GUI title.</param>
  158. /// <param name="path">Full path to this property (includes name of this property and all parent properties).
  159. /// </param>
  160. /// <param name="property">Serializable property referencing a single-dimensional array.</param>
  161. /// <param name="layout">Layout to which to append the list GUI elements to.</param>
  162. /// <param name="depth">Determines at which depth to render the background. Useful when you have multiple
  163. /// nested containers whose backgrounds are overlaping. Also determines background style,
  164. /// depths divisible by two will use an alternate style.</param>
  165. /// <param name="style">Information that can be used for customizing field rendering and behaviour.</param>
  166. public static InspectableArrayGUI Create(InspectableContext context, LocString title, string path,
  167. SerializableProperty property, GUILayout layout, int depth, InspectableFieldStyleInfo style)
  168. {
  169. InspectableArrayGUI guiArray = new InspectableArrayGUI(context, title, path, property, layout, depth, style);
  170. guiArray.BuildGUI();
  171. return guiArray;
  172. }
  173. /// <summary>
  174. /// Returns an array row at the specified index.
  175. /// </summary>
  176. /// <param name="idx">Index of the row.</param>
  177. /// <returns>Array row representation or null if index is out of range.</returns>
  178. public InspectableArrayGUIRow GetRow(int idx)
  179. {
  180. if (idx < GetNumRows())
  181. return (InspectableArrayGUIRow)rows[idx];
  182. return null;
  183. }
  184. /// <inheritdoc/>
  185. public override InspectableState Refresh(bool force)
  186. {
  187. // Check if any modifications to the array were made outside the inspector
  188. Array newArray = property.GetValue<Array>();
  189. if (array == null && newArray != null)
  190. {
  191. array = newArray;
  192. numElements = array.Length;
  193. BuildGUI(true);
  194. }
  195. else if (newArray == null && array != null)
  196. {
  197. array = null;
  198. numElements = 0;
  199. BuildGUI(true);
  200. }
  201. else
  202. {
  203. if (newArray != null)
  204. {
  205. if (numElements != newArray.Length || force)
  206. {
  207. array = newArray;
  208. numElements = array.Length;
  209. BuildGUI(true);
  210. }
  211. }
  212. }
  213. return base.Refresh(force);
  214. }
  215. /// <inheritdoc/>
  216. protected override GUIListFieldRow CreateRow()
  217. {
  218. return new InspectableArrayGUIRow();
  219. }
  220. /// <inheritdoc/>
  221. protected override bool IsNull()
  222. {
  223. return array == null;
  224. }
  225. /// <inheritdoc/>
  226. protected override int GetNumRows()
  227. {
  228. if (array != null)
  229. return array.Length;
  230. return 0;
  231. }
  232. /// <inheritdoc/>
  233. protected internal override object GetValue(int seqIndex)
  234. {
  235. SerializableArray serzArray = property.GetArray();
  236. // Create a property wrapper for native arrays so we get notified when the array values change
  237. if (style.StyleFlags.HasFlag(InspectableFieldStyleFlags.NativeWrapper))
  238. {
  239. SerializableProperty.Getter getter = () =>
  240. {
  241. Array array = property.GetValue<Array>();
  242. if (array != null)
  243. return array.GetValue(seqIndex);
  244. else
  245. return null;
  246. };
  247. SerializableProperty.Setter setter = (object value) =>
  248. {
  249. Array array = property.GetValue<Array>();
  250. if (array != null)
  251. {
  252. array.SetValue(value, seqIndex);
  253. property.SetValue(array);
  254. }
  255. };
  256. return new SerializableProperty(serzArray.ElementPropertyType, serzArray.ElementType, getter, setter);
  257. }
  258. else
  259. return serzArray.GetProperty(seqIndex);
  260. }
  261. /// <inheritdoc/>
  262. protected internal override void SetValue(int seqIndex, object value)
  263. {
  264. // Setting the value should be done through the property
  265. throw new InvalidOperationException();
  266. }
  267. /// <inheritdoc/>
  268. protected override void CreateList()
  269. {
  270. StartUndo();
  271. array = property.CreateArrayInstance(new int[1] { 0 });
  272. property.SetValue(array);
  273. numElements = 0;
  274. EndUndo();
  275. }
  276. /// <inheritdoc/>
  277. protected override void ResizeList()
  278. {
  279. StartUndo();
  280. int size = guiSizeField.Value; // TODO - Support multi-rank arrays
  281. Array newArray = property.CreateArrayInstance(new int[] { size });
  282. int maxSize = MathEx.Min(size, array.Length);
  283. for (int i = 0; i < maxSize; i++)
  284. newArray.SetValue(array.GetValue(i), i);
  285. property.SetValue(newArray);
  286. array = newArray;
  287. numElements = size;
  288. EndUndo();
  289. }
  290. /// <inheritdoc/>
  291. protected override void ClearList()
  292. {
  293. // Native arrays cannot be set to null, so just clear their size to zero instead
  294. if (style.StyleFlags.HasFlag(InspectableFieldStyleFlags.NativeWrapper))
  295. CreateList();
  296. else
  297. {
  298. StartUndo();
  299. property.SetValue<object>(null);
  300. array = null;
  301. numElements = 0;
  302. EndUndo();
  303. }
  304. }
  305. /// <inheritdoc/>
  306. protected internal override void DeleteElement(int index)
  307. {
  308. StartUndo();
  309. int size = MathEx.Max(0, array.Length - 1);
  310. Array newArray = property.CreateArrayInstance(new int[] { size });
  311. int destIdx = 0;
  312. for (int i = 0; i < array.Length; i++)
  313. {
  314. if (i == index)
  315. continue;
  316. newArray.SetValue(array.GetValue(i), destIdx);
  317. destIdx++;
  318. }
  319. property.SetValue(newArray);
  320. array = newArray;
  321. numElements = array.Length;
  322. EndUndo();
  323. }
  324. /// <inheritdoc/>
  325. protected internal override void CloneElement(int index)
  326. {
  327. StartUndo();
  328. SerializableArray array = property.GetArray();
  329. int size = array.GetLength() + 1;
  330. Array newArray = property.CreateArrayInstance(new int[] { size });
  331. object clonedEntry = null;
  332. for (int i = 0; i < array.GetLength(); i++)
  333. {
  334. object value = array.GetProperty(i).GetValue<object>();
  335. newArray.SetValue(value, i);
  336. if (i == index)
  337. {
  338. clonedEntry = SerializableUtility.Clone(array.GetProperty(i).GetValue<object>());
  339. }
  340. }
  341. newArray.SetValue(clonedEntry, size - 1);
  342. property.SetValue(newArray);
  343. this.array = newArray;
  344. numElements = newArray.Length;
  345. EndUndo();
  346. }
  347. /// <inheritdoc/>
  348. protected internal override void MoveUpElement(int index)
  349. {
  350. if ((index - 1) >= 0)
  351. {
  352. StartUndo();
  353. object previousEntry = array.GetValue(index - 1);
  354. array.SetValue(array.GetValue(index), index - 1);
  355. array.SetValue(previousEntry, index);
  356. // Natively wrapped arrays are passed by copy
  357. if(style.StyleFlags.HasFlag(InspectableFieldStyleFlags.NativeWrapper))
  358. property.SetValue(array);
  359. }
  360. }
  361. /// <inheritdoc/>
  362. protected internal override void MoveDownElement(int index)
  363. {
  364. if ((index + 1) < array.Length)
  365. {
  366. StartUndo();
  367. object nextEntry = array.GetValue(index + 1);
  368. array.SetValue(array.GetValue(index), index + 1);
  369. array.SetValue(nextEntry, index);
  370. // Natively wrapped arrays are passed by copy
  371. if(style.StyleFlags.HasFlag(InspectableFieldStyleFlags.NativeWrapper))
  372. property.SetValue(array);
  373. EndUndo();
  374. }
  375. }
  376. /// <summary>
  377. /// Notifies the system to start recording a new undo command. Any changes to the field after this is called
  378. /// will be recorded in the command. User must call <see cref="EndUndo"/> after field is done being changed.
  379. /// </summary>
  380. protected void StartUndo()
  381. {
  382. if (context.Component != null)
  383. GameObjectUndo.RecordComponent(context.Component, path);
  384. }
  385. /// <summary>
  386. /// Finishes recording an undo command started via <see cref="StartUndo"/>. If any changes are detected on the
  387. /// field an undo command is recorded onto the undo-redo stack, otherwise nothing is done.
  388. /// </summary>
  389. protected void EndUndo()
  390. {
  391. GameObjectUndo.ResolveDiffs();
  392. }
  393. }
  394. /// <summary>
  395. /// Contains GUI elements for a single entry in the array.
  396. /// </summary>
  397. private class InspectableArrayGUIRow : GUIListFieldRow
  398. {
  399. /// <summary>
  400. /// Inspectable field displayed on the array row.
  401. /// </summary>
  402. public InspectableField Field { get; private set; }
  403. /// <inheritdoc/>
  404. protected override GUILayoutX CreateGUI(GUILayoutY layout)
  405. {
  406. InspectableArrayGUI arrayParent = (InspectableArrayGUI)parent;
  407. SerializableProperty property = GetValue<SerializableProperty>();
  408. InspectableFieldStyleInfo styleInfo = arrayParent.Style.Clone();
  409. styleInfo.StyleFlags &= ~InspectableFieldStyleFlags.NativeWrapper;
  410. string entryPath = arrayParent.Path + "[" + SeqIndex + "]";
  411. Field = CreateField(arrayParent.Context, SeqIndex + ".", entryPath, 0, Depth + 1,
  412. new InspectableFieldLayout(layout), property, styleInfo);
  413. return Field.GetTitleLayout();
  414. }
  415. /// <inheritdoc/>
  416. protected internal override InspectableState Refresh(bool force)
  417. {
  418. Field.Property = GetValue<SerializableProperty>();
  419. return Field.Refresh(0, force);
  420. }
  421. }
  422. }
  423. /** @} */
  424. }