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