A game about forced loneliness, made by TACStudios
at master 16 kB view raw
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