InspectableObject.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. //********************************** Banshee Engine (www.banshee3d.com) **************************************************//
  2. //**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
  3. using System.Collections.Generic;
  4. using System;
  5. using System.Reflection;
  6. using BansheeEngine;
  7. namespace BansheeEditor
  8. {
  9. /** @addtogroup Inspector
  10. * @{
  11. */
  12. /// <summary>
  13. /// Displays GUI for a serializable property containing a generic object. Inspectable object fields are displayed
  14. /// in separate rows.
  15. /// </summary>
  16. public class InspectableObject : InspectableField
  17. {
  18. private const int IndentAmount = 5;
  19. private object propertyValue;
  20. private List<InspectableField> children = new List<InspectableField>();
  21. private InspectableFieldStyleInfo style;
  22. private GUILayoutY guiLayout;
  23. private GUILayoutX guiChildLayout;
  24. private GUILayoutX guiTitleLayout;
  25. private GUILayoutX guiInternalTitleLayout;
  26. private GUIButton guiCreateBtn;
  27. private ContextMenu createContextMenu;
  28. private bool isExpanded;
  29. private bool forceUpdate = true;
  30. private State state;
  31. /// <summary>
  32. /// Creates a new inspectable array GUI for the specified property.
  33. /// </summary>
  34. /// <param name="parent">Parent Inspector this field belongs to.</param>
  35. /// <param name="title">Name of the property, or some other value to set as the title.</param>
  36. /// <param name="path">Full path to this property (includes name of this property and all parent properties).</param>
  37. /// <param name="depth">Determines how deep within the inspector nesting hierarchy is this field. Some fields may
  38. /// contain other fields, in which case you should increase this value by one.</param>
  39. /// <param name="layout">Parent layout that all the field elements will be added to.</param>
  40. /// <param name="property">Serializable property referencing the object whose contents to display.</param>
  41. /// <param name="style">Information that can be used for customizing field rendering and behaviour.</param>
  42. public InspectableObject(Inspector parent, string title, string path, int depth, InspectableFieldLayout layout,
  43. SerializableProperty property, InspectableFieldStyleInfo style)
  44. : base(parent, title, path, SerializableProperty.FieldType.Object, depth, layout, property)
  45. {
  46. this.style = style;
  47. isExpanded = parent.Persistent.GetBool(path + "_Expanded");
  48. // Builds a context menu that lets the user create objects to assign to this field.
  49. Type[] types = GetInstantiableTypes(property.InternalType);
  50. if (types.Length > 0)
  51. {
  52. createContextMenu = new ContextMenu();
  53. Array.Sort(types, (x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
  54. foreach (var type in types)
  55. {
  56. createContextMenu.AddItem(type.Namespace + "." + type.Name,
  57. () => property.SetValue(Activator.CreateInstance(type)));
  58. }
  59. }
  60. }
  61. /// <inheritdoc/>
  62. public override GUILayoutX GetTitleLayout()
  63. {
  64. return guiTitleLayout;
  65. }
  66. /// <inheritdoc/>
  67. public override InspectableState Refresh(int layoutIndex)
  68. {
  69. // Check if modified internally and rebuild if needed
  70. object newPropertyValue = property.GetValue<object>();
  71. if (forceUpdate)
  72. {
  73. propertyValue = newPropertyValue;
  74. BuildGUI(layoutIndex);
  75. forceUpdate = false;
  76. }
  77. else if (propertyValue == null && newPropertyValue != null)
  78. {
  79. propertyValue = newPropertyValue;
  80. BuildGUI(layoutIndex);
  81. }
  82. else if (newPropertyValue == null && propertyValue != null)
  83. {
  84. propertyValue = null;
  85. BuildGUI(layoutIndex);
  86. }
  87. InspectableState state = InspectableState.NotModified;
  88. int currentIndex = 0;
  89. for (int i = 0; i < children.Count; i++)
  90. {
  91. state |= children[i].Refresh(currentIndex);
  92. currentIndex += children[i].GetNumLayoutElements();
  93. }
  94. if (state.HasFlag(InspectableState.Modified))
  95. {
  96. if (style.StyleFlags.HasFlag(InspectableFieldStyleFlags.CopiedAsValue) ||
  97. style.StyleFlags.HasFlag(InspectableFieldStyleFlags.ApplyOnDirty))
  98. property.SetValue(propertyValue);
  99. }
  100. return state;
  101. }
  102. /// <summary>
  103. /// Rebuilds the GUI object header if needed.
  104. /// </summary>
  105. /// <param name="layoutIndex">Index at which to insert the GUI elements.</param>
  106. protected void BuildGUI(int layoutIndex)
  107. {
  108. Action BuildEmptyGUI = () =>
  109. {
  110. guiInternalTitleLayout = guiTitleLayout.InsertLayoutX(0);
  111. guiInternalTitleLayout.AddElement(new GUILabel(title));
  112. guiInternalTitleLayout.AddElement(new GUILabel("Empty", GUIOption.FixedWidth(100)));
  113. if (!property.IsValueType)
  114. {
  115. GUIContent createIcon = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.Create),
  116. new LocEdString("Create"));
  117. guiCreateBtn = new GUIButton(createIcon, GUIOption.FixedWidth(30));
  118. guiCreateBtn.OnClick += OnCreateButtonClicked;
  119. guiInternalTitleLayout.AddElement(guiCreateBtn);
  120. }
  121. };
  122. Action BuildFilledGUI = () =>
  123. {
  124. guiInternalTitleLayout = guiTitleLayout.InsertLayoutX(0);
  125. GUIToggle guiFoldout = new GUIToggle(title, EditorStyles.Foldout);
  126. guiFoldout.Value = isExpanded;
  127. guiFoldout.AcceptsKeyFocus = false;
  128. guiFoldout.OnToggled += OnFoldoutToggled;
  129. guiInternalTitleLayout.AddElement(guiFoldout);
  130. if (!style.StyleFlags.HasFlag(InspectableFieldStyleFlags.NotNull))
  131. {
  132. GUIContent clearIcon = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.Clear),
  133. new LocEdString("Clear"));
  134. GUIButton clearBtn = new GUIButton(clearIcon, GUIOption.FixedWidth(20));
  135. clearBtn.OnClick += OnClearButtonClicked;
  136. guiInternalTitleLayout.AddElement(clearBtn);
  137. }
  138. if (isExpanded)
  139. {
  140. SerializableObject serializableObject;
  141. // Note: Make sure to use the same object instance if it's copied as value
  142. if(style.StyleFlags.HasFlag(InspectableFieldStyleFlags.CopiedAsValue))
  143. serializableObject = new SerializableObject(propertyValue);
  144. else
  145. serializableObject = property.GetObject();
  146. SerializableField[] fields = serializableObject.Fields;
  147. if (fields.Length > 0)
  148. {
  149. guiChildLayout = guiLayout.AddLayoutX();
  150. guiChildLayout.AddSpace(IndentAmount);
  151. GUIPanel guiContentPanel = guiChildLayout.AddPanel();
  152. GUILayoutX guiIndentLayoutX = guiContentPanel.AddLayoutX();
  153. guiIndentLayoutX.AddSpace(IndentAmount);
  154. GUILayoutY guiIndentLayoutY = guiIndentLayoutX.AddLayoutY();
  155. guiIndentLayoutY.AddSpace(IndentAmount);
  156. GUILayoutY guiContentLayout = guiIndentLayoutY.AddLayoutY();
  157. guiIndentLayoutY.AddSpace(IndentAmount);
  158. guiIndentLayoutX.AddSpace(IndentAmount);
  159. guiChildLayout.AddSpace(IndentAmount);
  160. short backgroundDepth = (short) (Inspector.START_BACKGROUND_DEPTH - depth - 1);
  161. string bgPanelStyle = depth%2 == 0
  162. ? EditorStylesInternal.InspectorContentBgAlternate
  163. : EditorStylesInternal.InspectorContentBg;
  164. GUIPanel backgroundPanel = guiContentPanel.AddPanel(backgroundDepth);
  165. GUITexture inspectorContentBg = new GUITexture(null, bgPanelStyle);
  166. backgroundPanel.AddElement(inspectorContentBg);
  167. int currentIndex = 0;
  168. foreach (var field in fields)
  169. {
  170. if (!field.Flags.HasFlag(SerializableFieldAttributes.Inspectable))
  171. continue;
  172. string childPath = path + "/" + field.Name;
  173. InspectableField inspectable = CreateInspectable(parent, field.Name, childPath,
  174. currentIndex, depth + 1, new InspectableFieldLayout(guiContentLayout), field.GetProperty(), InspectableFieldStyle.Create(field));
  175. children.Add(inspectable);
  176. currentIndex += inspectable.GetNumLayoutElements();
  177. }
  178. }
  179. }
  180. else
  181. guiChildLayout = null;
  182. };
  183. if (state == State.None)
  184. {
  185. if (propertyValue != null)
  186. {
  187. BuildFilledGUI();
  188. state = State.Filled;
  189. }
  190. else
  191. {
  192. BuildEmptyGUI();
  193. state = State.Empty;
  194. }
  195. }
  196. else if (state == State.Empty)
  197. {
  198. if (propertyValue != null)
  199. {
  200. guiInternalTitleLayout.Destroy();
  201. guiCreateBtn = null;
  202. BuildFilledGUI();
  203. state = State.Filled;
  204. }
  205. }
  206. else if (state == State.Filled)
  207. {
  208. foreach (var child in children)
  209. child.Destroy();
  210. children.Clear();
  211. guiInternalTitleLayout.Destroy();
  212. guiCreateBtn = null;
  213. if (guiChildLayout != null)
  214. {
  215. guiChildLayout.Destroy();
  216. guiChildLayout = null;
  217. }
  218. if (propertyValue == null)
  219. {
  220. BuildEmptyGUI();
  221. state = State.Empty;
  222. }
  223. else
  224. {
  225. BuildFilledGUI();
  226. }
  227. }
  228. }
  229. /// <inheritdoc/>
  230. protected internal override void Initialize(int index)
  231. {
  232. guiLayout = layout.AddLayoutY(index);
  233. guiTitleLayout = guiLayout.AddLayoutX();
  234. propertyValue = property.GetValue<object>();
  235. BuildGUI(index);
  236. }
  237. /// <summary>
  238. /// Triggered when the user clicks on the expand/collapse toggle in the title bar.
  239. /// </summary>
  240. /// <param name="expanded">Determines whether the contents were expanded or collapsed.</param>
  241. private void OnFoldoutToggled(bool expanded)
  242. {
  243. parent.Persistent.SetBool(path + "_Expanded", expanded);
  244. isExpanded = expanded;
  245. forceUpdate = true;
  246. }
  247. /// <summary>
  248. /// Triggered when the user clicks on the create button on the title bar. Creates a brand new object with default
  249. /// values in the place of the current array.
  250. /// </summary>
  251. private void OnCreateButtonClicked()
  252. {
  253. if (createContextMenu == null)
  254. return;
  255. Rect2I bounds = GUIUtility.CalculateBounds(guiCreateBtn, guiInternalTitleLayout);
  256. Vector2I position = new Vector2I(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2);
  257. createContextMenu.Open(position, guiInternalTitleLayout);
  258. }
  259. /// <summary>
  260. /// Triggered when the user clicks on the clear button on the title bar. Deletes the current object and sets
  261. /// the reference to the object in the parent object to null. This is only relevant for objects of reference types.
  262. /// </summary>
  263. private void OnClearButtonClicked()
  264. {
  265. property.SetValue<object>(null);
  266. }
  267. /// <summary>
  268. /// Possible states object GUI can be in.
  269. /// </summary>
  270. private enum State
  271. {
  272. None,
  273. Empty,
  274. Filled
  275. }
  276. /// <summary>
  277. /// Returns a list of all types that can be created using the parameterless constructor and assigned to an object of
  278. /// type <paramref name="type"/>.
  279. /// </summary>
  280. /// <param name="type">Type to which the instantiable types need to be assignable to.</param>
  281. /// <returns>List of types that can be instantiated and assigned type <paramref name="type"/></returns>
  282. private static Type[] GetInstantiableTypes(Type type)
  283. {
  284. // Note: This could be cached
  285. List<Type> output = new List<Type>();
  286. Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
  287. foreach (var assembly in assemblies)
  288. {
  289. Type[] assemblyTypes = assembly.GetExportedTypes();
  290. foreach (var assemblyType in assemblyTypes)
  291. {
  292. if (assemblyType.IsAbstract)
  293. continue;
  294. if (!type.IsAssignableFrom(assemblyType))
  295. continue;
  296. var ctor = assemblyType.GetConstructor(Type.EmptyTypes);
  297. if (ctor == null)
  298. continue;
  299. output.Add(assemblyType);
  300. }
  301. }
  302. return output.ToArray();
  303. }
  304. }
  305. /** @} */
  306. }