No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SerializedPropertyHelpers.cs 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. #if UNITY_EDITOR
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Reflection;
  5. using System.Text;
  6. using UnityEditor;
  7. using UnityEngine.InputSystem.Utilities;
  8. namespace UnityEngine.InputSystem.Editor
  9. {
  10. /// <summary>
  11. /// Helpers for working with <see cref="SerializedProperty"/> in the editor.
  12. /// </summary>
  13. internal static class SerializedPropertyHelpers
  14. {
  15. // Show a PropertyField with a greyed-out default text if the field is empty and not being edited.
  16. // This is meant to communicate the fact that filling these properties is optional and that Unity will
  17. // use reasonable defaults if left empty.
  18. public static void PropertyFieldWithDefaultText(this SerializedProperty prop, GUIContent label, string defaultText)
  19. {
  20. GUI.SetNextControlName(label.text);
  21. var rt = GUILayoutUtility.GetRect(label, GUI.skin.textField);
  22. EditorGUI.PropertyField(rt, prop, label);
  23. if (string.IsNullOrEmpty(prop.stringValue) && GUI.GetNameOfFocusedControl() != label.text && Event.current.type == EventType.Repaint)
  24. {
  25. using (new EditorGUI.DisabledScope(true))
  26. {
  27. rt.xMin += EditorGUIUtility.labelWidth;
  28. GUI.skin.textField.Draw(rt, new GUIContent(defaultText), false, false, false, false);
  29. }
  30. }
  31. }
  32. public static SerializedProperty GetParentProperty(this SerializedProperty property)
  33. {
  34. var path = property.propertyPath;
  35. var lastDot = path.LastIndexOf('.');
  36. if (lastDot == -1)
  37. return null;
  38. var parentPath = path.Substring(0, lastDot);
  39. return property.serializedObject.FindProperty(parentPath);
  40. }
  41. public static SerializedProperty GetArrayPropertyFromElement(this SerializedProperty property)
  42. {
  43. // Arrays have a structure of 'arrayName.Array.data[index]'.
  44. // Given property should be element and thus 'data[index]'.
  45. var arrayProperty = property.GetParentProperty();
  46. Debug.Assert(arrayProperty.name == "Array", "Expecting 'Array' property");
  47. return arrayProperty.GetParentProperty();
  48. }
  49. public static int GetIndexOfArrayElement(this SerializedProperty property)
  50. {
  51. if (property == null)
  52. return -1;
  53. var propertyPath = property.propertyPath;
  54. if (propertyPath[propertyPath.Length - 1] != ']')
  55. return -1;
  56. var lastIndexOfLeftBracket = propertyPath.LastIndexOf('[');
  57. if (int.TryParse(
  58. propertyPath.Substring(lastIndexOfLeftBracket + 1, propertyPath.Length - lastIndexOfLeftBracket - 2),
  59. out var index))
  60. return index;
  61. return -1;
  62. }
  63. public static Type GetArrayElementType(this SerializedProperty property)
  64. {
  65. Debug.Assert(property.isArray, $"Property {property.propertyPath} is not an array");
  66. var fieldType = property.GetFieldType();
  67. if (fieldType == null)
  68. throw new ArgumentException($"Cannot determine managed field type of {property.propertyPath}",
  69. nameof(property));
  70. return fieldType.GetElementType();
  71. }
  72. public static void ResetValuesToDefault(this SerializedProperty property)
  73. {
  74. var isString = property.propertyType == SerializedPropertyType.String;
  75. if (property.isArray && !isString)
  76. {
  77. property.ClearArray();
  78. }
  79. else if (property.hasChildren && !isString)
  80. {
  81. foreach (var child in property.GetChildren())
  82. ResetValuesToDefault(child);
  83. }
  84. else
  85. {
  86. switch (property.propertyType)
  87. {
  88. case SerializedPropertyType.Float:
  89. property.floatValue = default(float);
  90. break;
  91. case SerializedPropertyType.Boolean:
  92. property.boolValue = default(bool);
  93. break;
  94. case SerializedPropertyType.Enum:
  95. case SerializedPropertyType.Integer:
  96. property.intValue = default(int);
  97. break;
  98. case SerializedPropertyType.String:
  99. property.stringValue = string.Empty;
  100. break;
  101. case SerializedPropertyType.ObjectReference:
  102. property.objectReferenceValue = null;
  103. break;
  104. }
  105. }
  106. }
  107. public static string ToJson(this SerializedObject serializedObject)
  108. {
  109. return JsonUtility.ToJson(serializedObject, prettyPrint: true);
  110. }
  111. // The following is functionality that allows turning Unity data into text and text
  112. // back into Unity data. Given that this is essential functionality for any kind of
  113. // copypaste support, I'm not sure why the Unity editor API isn't providing this out
  114. // of the box. Internally, we do have support for this on a whole-object kind of level
  115. // but not for parts of serialized objects.
  116. /// <summary>
  117. ///
  118. /// </summary>
  119. /// <param name="property"></param>
  120. /// <returns></returns>
  121. /// <remarks>
  122. /// Converting entire objects to JSON is easy using Unity's serialization system but we cannot
  123. /// easily convert just a part of the serialized graph to JSON (or any text format for that matter)
  124. /// and then recreate the same data from text through SerializedProperties. This method helps by manually
  125. /// turning an arbitrary part of a graph into JSON which can then be used with <see cref="RestoreFromJson"/>
  126. /// to write the data back into an existing property.
  127. ///
  128. /// The primary use for this is copy-paste where serialized data needs to be stored in
  129. /// <see cref="EditorGUIUtility.systemCopyBuffer"/>.
  130. /// </remarks>
  131. public static string CopyToJson(this SerializedProperty property, bool ignoreObjectReferences = false)
  132. {
  133. var buffer = new StringBuilder();
  134. CopyToJson(property, buffer, ignoreObjectReferences);
  135. return buffer.ToString();
  136. }
  137. public static void CopyToJson(this SerializedProperty property, StringBuilder buffer, bool ignoreObjectReferences = false)
  138. {
  139. CopyToJson(property, buffer, noPropertyName: true, ignoreObjectReferences: ignoreObjectReferences);
  140. }
  141. private static void CopyToJson(this SerializedProperty property, StringBuilder buffer, bool noPropertyName, bool ignoreObjectReferences)
  142. {
  143. var propertyType = property.propertyType;
  144. if (ignoreObjectReferences && propertyType == SerializedPropertyType.ObjectReference)
  145. return;
  146. // Property name.
  147. if (!noPropertyName)
  148. {
  149. buffer.Append('"');
  150. buffer.Append(property.name);
  151. buffer.Append('"');
  152. buffer.Append(':');
  153. }
  154. // Strings are classified as arrays and have children.
  155. var isString = propertyType == SerializedPropertyType.String;
  156. // Property value.
  157. if (property.isArray && !isString)
  158. {
  159. buffer.Append('[');
  160. var arraySize = property.arraySize;
  161. var isFirst = true;
  162. for (var i = 0; i < arraySize; ++i)
  163. {
  164. var element = property.GetArrayElementAtIndex(i);
  165. if (ignoreObjectReferences && element.propertyType == SerializedPropertyType.ObjectReference)
  166. continue;
  167. if (!isFirst)
  168. buffer.Append(',');
  169. CopyToJson(element, buffer, true, ignoreObjectReferences);
  170. isFirst = false;
  171. }
  172. buffer.Append(']');
  173. }
  174. else if (property.hasChildren && !isString)
  175. {
  176. // Any structured data we represent as a JSON object.
  177. buffer.Append('{');
  178. var isFirst = true;
  179. foreach (var child in property.GetChildren())
  180. {
  181. if (ignoreObjectReferences && child.propertyType == SerializedPropertyType.ObjectReference)
  182. continue;
  183. if (!isFirst)
  184. buffer.Append(',');
  185. CopyToJson(child, buffer, false, ignoreObjectReferences);
  186. isFirst = false;
  187. }
  188. buffer.Append('}');
  189. }
  190. else
  191. {
  192. switch (propertyType)
  193. {
  194. case SerializedPropertyType.Enum:
  195. case SerializedPropertyType.Integer:
  196. buffer.Append(property.intValue);
  197. break;
  198. case SerializedPropertyType.Float:
  199. buffer.Append(property.floatValue);
  200. break;
  201. case SerializedPropertyType.String:
  202. buffer.Append('"');
  203. buffer.Append(property.stringValue.Escape());
  204. buffer.Append('"');
  205. break;
  206. case SerializedPropertyType.Boolean:
  207. if (property.boolValue)
  208. buffer.Append("true");
  209. else
  210. buffer.Append("false");
  211. break;
  212. ////TODO: other property types
  213. default:
  214. throw new NotImplementedException($"Support for {property.propertyType} property type");
  215. }
  216. }
  217. }
  218. public static void RestoreFromJson(this SerializedProperty property, string json)
  219. {
  220. var parser = new JsonParser(json);
  221. RestoreFromJson(property, ref parser);
  222. }
  223. public static void RestoreFromJson(this SerializedProperty property, ref JsonParser parser)
  224. {
  225. var isString = property.propertyType == SerializedPropertyType.String;
  226. if (property.isArray && !isString)
  227. {
  228. property.ClearArray();
  229. parser.ParseToken('[');
  230. while (!parser.ParseToken(']') && !parser.isAtEnd)
  231. {
  232. var index = property.arraySize;
  233. property.InsertArrayElementAtIndex(index);
  234. var elementProperty = property.GetArrayElementAtIndex(index);
  235. RestoreFromJson(elementProperty, ref parser);
  236. parser.ParseToken(',');
  237. }
  238. }
  239. else if (property.hasChildren && !isString)
  240. {
  241. parser.ParseToken('{');
  242. while (!parser.ParseToken('}') && !parser.isAtEnd)
  243. {
  244. parser.ParseStringValue(out var propertyName);
  245. parser.ParseToken(':');
  246. var childProperty = property.FindPropertyRelative(propertyName.ToString());
  247. if (childProperty == null)
  248. throw new ArgumentException($"Cannot find property '{propertyName}' in {property}", nameof(property));
  249. RestoreFromJson(childProperty, ref parser);
  250. parser.ParseToken(',');
  251. }
  252. }
  253. else
  254. {
  255. switch (property.propertyType)
  256. {
  257. case SerializedPropertyType.Float:
  258. {
  259. parser.ParseNumber(out var num);
  260. property.floatValue = (float)num.ToDouble();
  261. break;
  262. }
  263. case SerializedPropertyType.String:
  264. {
  265. parser.ParseStringValue(out var str);
  266. property.stringValue = str.ToString();
  267. break;
  268. }
  269. case SerializedPropertyType.Boolean:
  270. {
  271. parser.ParseBooleanValue(out var b);
  272. property.boolValue = b.ToBoolean();
  273. break;
  274. }
  275. case SerializedPropertyType.Enum:
  276. case SerializedPropertyType.Integer:
  277. {
  278. parser.ParseNumber(out var num);
  279. property.intValue = (int)num.ToInteger();
  280. break;
  281. }
  282. default:
  283. throw new NotImplementedException(
  284. $"Restoring property value of type {property.propertyType} (property: {property})");
  285. }
  286. }
  287. }
  288. public static IEnumerable<SerializedProperty> GetChildren(this SerializedProperty property)
  289. {
  290. if (!property.hasChildren)
  291. yield break;
  292. using (var iter = property.Copy())
  293. {
  294. var end = iter.GetEndProperty(true);
  295. // Go to first child.
  296. if (!iter.Next(true))
  297. yield break; // Shouldn't happen; we've already established we have children.
  298. // Iterate over children.
  299. while (!SerializedProperty.EqualContents(iter, end))
  300. {
  301. yield return iter;
  302. if (!iter.Next(false))
  303. break;
  304. }
  305. }
  306. }
  307. public static FieldInfo GetField(this SerializedProperty property)
  308. {
  309. var objectType = property.serializedObject.targetObject.GetType();
  310. var currentSerializableType = objectType;
  311. var pathComponents = property.propertyPath.Split('.');
  312. FieldInfo result = null;
  313. foreach (var component in pathComponents)
  314. {
  315. // Handle arrays. They are followed by "Array" and "data[N]" elements.
  316. if (result != null && currentSerializableType.IsArray)
  317. {
  318. if (component == "Array")
  319. continue;
  320. if (component.StartsWith("data["))
  321. {
  322. currentSerializableType = currentSerializableType.GetElementType();
  323. continue;
  324. }
  325. }
  326. result = currentSerializableType.GetField(component,
  327. BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy);
  328. if (result == null)
  329. return null;
  330. currentSerializableType = result.FieldType;
  331. }
  332. return result;
  333. }
  334. public static Type GetFieldType(this SerializedProperty property)
  335. {
  336. return GetField(property)?.FieldType;
  337. }
  338. public static void SetStringValue(this SerializedProperty property, string propertyName, string value)
  339. {
  340. var propertyRelative = property?.FindPropertyRelative(propertyName);
  341. if (propertyRelative != null)
  342. propertyRelative.stringValue = value;
  343. }
  344. }
  345. }
  346. #endif // UNITY_EDITOR