├── .gitignore ├── Docs ├── context-menu.png ├── element-names.png ├── sortable-array.png └── sortable-drag-drop.jpg ├── EditScriptableAttribute.cs ├── Editor ├── ReorderableArrayInspector.cs └── SerializedPropExtension.cs ├── LICENSE ├── README.md └── ReorderableAttribute.cs /.gitignore: -------------------------------------------------------------------------------- 1 | /[Ll]ibrary/ 2 | /[Tt]emp/ 3 | /[Oo]bj/ 4 | /[Bb]uild/ 5 | /[Bb]uilds/ 6 | /Assets/AssetStoreTools* 7 | 8 | # Autogenerated VS/MD solution and project files 9 | ExportedObj/ 10 | *.csproj 11 | *.unityproj 12 | *.sln 13 | *.suo 14 | *.tmp 15 | *.user 16 | *.userprefs 17 | *.pidb 18 | *.booproj 19 | *.svd 20 | 21 | # Unity3D generated meta files 22 | *.pidb.meta 23 | *.meta 24 | 25 | # Unity3D Generated File On Crash Reports 26 | sysinfo.txt 27 | 28 | # Builds 29 | *.apk 30 | *.unitypackage 31 | -------------------------------------------------------------------------------- /Docs/context-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SubjectNerd-Unity/ReorderableInspector/152a050fd26acdff6b9ff3e28271027d660b2370/Docs/context-menu.png -------------------------------------------------------------------------------- /Docs/element-names.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SubjectNerd-Unity/ReorderableInspector/152a050fd26acdff6b9ff3e28271027d660b2370/Docs/element-names.png -------------------------------------------------------------------------------- /Docs/sortable-array.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SubjectNerd-Unity/ReorderableInspector/152a050fd26acdff6b9ff3e28271027d660b2370/Docs/sortable-array.png -------------------------------------------------------------------------------- /Docs/sortable-drag-drop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SubjectNerd-Unity/ReorderableInspector/152a050fd26acdff6b9ff3e28271027d660b2370/Docs/sortable-drag-drop.jpg -------------------------------------------------------------------------------- /EditScriptableAttribute.cs: -------------------------------------------------------------------------------- 1 | /*MIT License 2 | 3 | Copyright(c) 2017 Jeiel Aranal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE.*/ 22 | 23 | using UnityEngine; 24 | 25 | namespace SubjectNerd.Utilities 26 | { 27 | /// 28 | /// Display a ScriptableObject field with an inline editor 29 | /// 30 | public class EditScriptableAttribute : PropertyAttribute 31 | { 32 | 33 | } 34 | } -------------------------------------------------------------------------------- /Editor/ReorderableArrayInspector.cs: -------------------------------------------------------------------------------- 1 | /*MIT License 2 | 3 | Copyright(c) 2017 Jeiel Aranal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE.*/ 22 | 23 | // Uncomment the line below to turn all arrays into reorderable lists 24 | //#define LIST_ALL_ARRAYS 25 | 26 | // Uncomment the line below to make all ScriptableObject fields editable 27 | //#define EDIT_ALL_SCRIPTABLES 28 | 29 | using System; 30 | using System.Collections.Generic; 31 | using System.Linq; 32 | using System.Reflection; 33 | using UnityEditor; 34 | using UnityEditor.Callbacks; 35 | using UnityEditorInternal; 36 | using UnityEngine; 37 | using Object = UnityEngine.Object; 38 | 39 | namespace SubjectNerd.Utilities 40 | { 41 | [CustomEditor(typeof(UnityEngine.Object), true, isFallback = true)] 42 | [CanEditMultipleObjects] 43 | public class ReorderableArrayInspector : Editor 44 | { 45 | // Set this to true to turn every array in non custom inspectors into reorderable lists 46 | private const bool LIST_ALL_ARRAYS = false; 47 | 48 | protected static string GetGrandParentPath(SerializedProperty property) 49 | { 50 | string parent = property.propertyPath; 51 | int firstDot = property.propertyPath.IndexOf('.'); 52 | if (firstDot > 0) 53 | { 54 | parent = property.propertyPath.Substring(0, firstDot); 55 | } 56 | return parent; 57 | } 58 | 59 | protected static bool FORCE_INIT = false; 60 | [DidReloadScripts] 61 | private static void HandleScriptReload() 62 | { 63 | FORCE_INIT = true; 64 | 65 | EditorApplication.delayCall += () => { EditorApplication.delayCall += () => { FORCE_INIT = false; }; }; 66 | } 67 | 68 | private static GUIStyle styleHighlight; 69 | 70 | /// 71 | /// Internal class that manages ReorderableLists for each reorderable 72 | /// SerializedProperty in a SerializedObject's direct child 73 | /// 74 | protected class SortableListData 75 | { 76 | public string Parent { get; private set; } 77 | public Func ElementHeaderCallback = null; 78 | 79 | private readonly Dictionary propIndex = new Dictionary(); 80 | private readonly Dictionary> propDropHandlers = new Dictionary>(); 81 | private readonly Dictionary countIndex = new Dictionary(); 82 | 83 | public SortableListData(string parent) 84 | { 85 | Parent = parent; 86 | } 87 | 88 | public void AddProperty(SerializedProperty property) 89 | { 90 | // Check if this property actually belongs to the same direct child 91 | if (GetGrandParentPath(property).Equals(Parent) == false) 92 | return; 93 | 94 | ReorderableList propList = new ReorderableList( 95 | property.serializedObject, property, 96 | draggable: true, displayHeader: false, 97 | displayAddButton: true, displayRemoveButton: true) 98 | { 99 | headerHeight = 5 100 | }; 101 | 102 | propList.drawElementCallback = delegate (Rect rect, int index, bool active, bool focused) 103 | { 104 | SerializedProperty targetElement = property.GetArrayElementAtIndex(index); 105 | 106 | bool isExpanded = targetElement.isExpanded; 107 | rect.height = EditorGUI.GetPropertyHeight(targetElement, GUIContent.none, isExpanded); 108 | 109 | if (targetElement.hasVisibleChildren) 110 | rect.xMin += 10; 111 | 112 | // Get Unity to handle drawing each element 113 | GUIContent propHeader = new GUIContent(targetElement.displayName); 114 | if (ElementHeaderCallback != null) 115 | propHeader.text = ElementHeaderCallback(index); 116 | EditorGUI.PropertyField(rect, targetElement, propHeader, isExpanded); 117 | 118 | // If drawing the selected element, use it to set the element height 119 | // Element height seems to control selected background 120 | #if UNITY_5_1 || UNITY_5_2 121 | if (index == propList.index) 122 | { 123 | // Height might have changed when dealing with serialized class 124 | // Call the select callback when height changes to reset the list elementHeight 125 | float newHeight = EditorGUI.GetPropertyHeight(targetElement, GUIContent.none, targetElement.isExpanded); 126 | if (rect.height != newHeight) 127 | propList.elementHeight = Mathf.Max(propList.elementHeight, newHeight); 128 | } 129 | #endif 130 | }; 131 | 132 | // Unity 5.3 onwards allows reorderable lists to have variable element heights 133 | #if UNITY_5_3_OR_NEWER 134 | propList.elementHeightCallback = index => ElementHeightCallback(property, index); 135 | 136 | propList.drawElementBackgroundCallback = (rect, index, active, focused) => 137 | { 138 | if (styleHighlight == null) 139 | styleHighlight = GUI.skin.FindStyle("MeTransitionSelectHead"); 140 | if (focused == false) 141 | return; 142 | rect.height = ElementHeightCallback(property, index); 143 | GUI.Box(rect, GUIContent.none, styleHighlight); 144 | }; 145 | #endif 146 | propIndex.Add(property.propertyPath, propList); 147 | } 148 | 149 | private float ElementHeightCallback(SerializedProperty property, int index) 150 | { 151 | SerializedProperty arrayElement = property.GetArrayElementAtIndex(index); 152 | float calculatedHeight = EditorGUI.GetPropertyHeight(arrayElement, 153 | GUIContent.none, 154 | arrayElement.isExpanded); 155 | calculatedHeight += 3; 156 | return calculatedHeight; 157 | } 158 | 159 | public bool DoLayoutProperty(SerializedProperty property) 160 | { 161 | if (propIndex.ContainsKey(property.propertyPath) == false) 162 | return false; 163 | 164 | // Draw the header 165 | string headerText = string.Format("{0} [{1}]", property.displayName, property.arraySize); 166 | EditorGUILayout.PropertyField(property, new GUIContent(headerText), false); 167 | 168 | // Save header rect for handling drag and drop 169 | Rect dropRect = GUILayoutUtility.GetLastRect(); 170 | 171 | // Draw the reorderable list for the property 172 | if (property.isExpanded) 173 | { 174 | int newArraySize = EditorGUILayout.IntField("Size", property.arraySize); 175 | if (newArraySize != property.arraySize) 176 | property.arraySize = newArraySize; 177 | propIndex[property.propertyPath].DoLayoutList(); 178 | } 179 | 180 | // Handle drag and drop into the header 181 | Event evt = Event.current; 182 | if (evt == null) 183 | return true; 184 | 185 | #if UNITY_2018_2_OR_NEWER 186 | if (evt.type == EventType.DragUpdated || evt.type == EventType.DragPerform) 187 | #else 188 | if (evt.type == EventType.dragUpdated || evt.type == EventType.dragPerform) 189 | #endif 190 | { 191 | if (dropRect.Contains(evt.mousePosition) == false) 192 | return true; 193 | 194 | DragAndDrop.visualMode = DragAndDropVisualMode.Copy; 195 | if (evt.type == EventType.DragPerform) 196 | { 197 | DragAndDrop.AcceptDrag(); 198 | Action handler = null; 199 | if (propDropHandlers.TryGetValue(property.propertyPath, out handler)) 200 | { 201 | if (handler != null) 202 | handler(property, DragAndDrop.objectReferences); 203 | } 204 | else 205 | { 206 | foreach (Object dragged_object in DragAndDrop.objectReferences) 207 | { 208 | if (dragged_object.GetType() != property.GetType()) 209 | continue; 210 | 211 | int newIndex = property.arraySize; 212 | property.arraySize++; 213 | 214 | SerializedProperty target = property.GetArrayElementAtIndex(newIndex); 215 | target.objectReferenceInstanceIDValue = dragged_object.GetInstanceID(); 216 | } 217 | } 218 | evt.Use(); 219 | } 220 | } 221 | return true; 222 | } 223 | 224 | public int GetElementCount(SerializedProperty property) 225 | { 226 | if (property.arraySize <= 0) 227 | return 0; 228 | 229 | int count; 230 | if (countIndex.TryGetValue(property.propertyPath, out count)) 231 | return count; 232 | 233 | var element = property.GetArrayElementAtIndex(0); 234 | var countElement = element.Copy(); 235 | int childCount = 0; 236 | if (countElement.NextVisible(true)) 237 | { 238 | int depth = countElement.Copy().depth; 239 | do 240 | { 241 | if (countElement.depth != depth) 242 | break; 243 | childCount++; 244 | } while (countElement.NextVisible(false)); 245 | } 246 | 247 | countIndex.Add(property.propertyPath, childCount); 248 | return childCount; 249 | } 250 | 251 | public ReorderableList GetPropertyList(SerializedProperty property) 252 | { 253 | if (propIndex.ContainsKey(property.propertyPath)) 254 | return propIndex[property.propertyPath]; 255 | return null; 256 | } 257 | 258 | public void SetDropHandler(SerializedProperty property, Action handler) 259 | { 260 | string path = property.propertyPath; 261 | if (propDropHandlers.ContainsKey(path)) 262 | propDropHandlers[path] = handler; 263 | else 264 | propDropHandlers.Add(path, handler); 265 | } 266 | } // End SortableListData 267 | 268 | public bool isSubEditor; 269 | 270 | private readonly GUILayoutOption uiExpandWidth = GUILayout.ExpandWidth(true); 271 | private readonly GUILayoutOption uiWidth50 = GUILayout.Width(50); 272 | private readonly GUIContent labelBtnCreate = new GUIContent("Create"); 273 | private GUIStyle styleEditBox; 274 | 275 | private readonly List listIndex = new List(); 276 | private readonly Dictionary editableIndex = new Dictionary(); 277 | 278 | protected bool alwaysDrawInspector = false; 279 | protected bool isInitialized = false; 280 | protected bool hasSortableArrays = false; 281 | protected bool hasEditable = false; 282 | 283 | protected struct ContextMenuData 284 | { 285 | public string menuItem; 286 | public MethodInfo function; 287 | public MethodInfo validate; 288 | 289 | public ContextMenuData(string item) 290 | { 291 | menuItem = item; 292 | function = null; 293 | validate = null; 294 | } 295 | } 296 | 297 | protected Dictionary contextData = new Dictionary(); 298 | 299 | ~ReorderableArrayInspector() 300 | { 301 | listIndex.Clear(); 302 | //hasSortableArrays = false; 303 | editableIndex.Clear(); 304 | //hasEditable = false; 305 | isInitialized = false; 306 | } 307 | 308 | #region Initialization 309 | private void OnEnable() 310 | { 311 | InitInspector(); 312 | } 313 | 314 | protected virtual void InitInspector(bool force) 315 | { 316 | if (force) 317 | isInitialized = false; 318 | InitInspector(); 319 | } 320 | 321 | protected virtual void InitInspector() 322 | { 323 | if (isInitialized && FORCE_INIT == false) 324 | return; 325 | 326 | styleEditBox = new GUIStyle(EditorStyles.helpBox) { padding = new RectOffset(5, 5, 5, 5) }; 327 | FindTargetProperties(); 328 | FindContextMenu(); 329 | } 330 | 331 | protected void FindTargetProperties() 332 | { 333 | listIndex.Clear(); 334 | editableIndex.Clear(); 335 | Type typeScriptable = typeof(ScriptableObject); 336 | 337 | SerializedProperty iterProp = serializedObject.GetIterator(); 338 | // This iterator goes through all the child serialized properties, looking 339 | // for properties that have the SortableArray attribute 340 | if (iterProp.NextVisible(true)) 341 | { 342 | do 343 | { 344 | if (iterProp.isArray && iterProp.propertyType != SerializedPropertyType.String) 345 | { 346 | #if LIST_ALL_ARRAYS 347 | bool canTurnToList = true; 348 | #else 349 | bool canTurnToList = iterProp.HasAttribute(); 350 | #endif 351 | if (canTurnToList) 352 | { 353 | hasSortableArrays = true; 354 | CreateListData(serializedObject.FindProperty(iterProp.propertyPath)); 355 | } 356 | } 357 | 358 | if (iterProp.propertyType == SerializedPropertyType.ObjectReference) 359 | { 360 | Type propType = iterProp.GetTypeReflection(); 361 | if (propType == null) 362 | continue; 363 | 364 | bool isScriptable = propType.IsSubclassOf(typeScriptable); 365 | if (isScriptable) 366 | { 367 | #if EDIT_ALL_SCRIPTABLES 368 | bool makeEditable = true; 369 | #else 370 | bool makeEditable = iterProp.HasAttribute(); 371 | #endif 372 | 373 | if (makeEditable) 374 | { 375 | Editor scriptableEditor = null; 376 | if (iterProp.objectReferenceValue != null) 377 | { 378 | #if UNITY_5_6_OR_NEWER 379 | CreateCachedEditorWithContext(iterProp.objectReferenceValue, 380 | serializedObject.targetObject, null, 381 | ref scriptableEditor); 382 | #else 383 | CreateCachedEditor(iterProp.objectReferenceValue, null, ref scriptableEditor); 384 | #endif 385 | var reorderable = scriptableEditor as ReorderableArrayInspector; 386 | if (reorderable != null) 387 | reorderable.isSubEditor = true; 388 | } 389 | editableIndex.Add(iterProp.propertyPath, scriptableEditor); 390 | hasEditable = true; 391 | } 392 | } 393 | } 394 | } while (iterProp.NextVisible(true)); 395 | } 396 | 397 | isInitialized = true; 398 | if (hasSortableArrays == false) 399 | { 400 | listIndex.Clear(); 401 | } 402 | } 403 | 404 | private IEnumerable GetAllMethods(Type t) 405 | { 406 | if (t == null) 407 | return Enumerable.Empty(); 408 | var binding = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; 409 | return t.GetMethods(binding).Concat(GetAllMethods(t.BaseType)); 410 | } 411 | 412 | private void FindContextMenu() 413 | { 414 | contextData.Clear(); 415 | 416 | // Get context menu 417 | Type targetType = target.GetType(); 418 | Type contextMenuType = typeof(ContextMenu); 419 | MethodInfo[] methods = GetAllMethods(targetType).ToArray(); 420 | for (int index = 0; index < methods.GetLength(0); ++index) 421 | { 422 | MethodInfo methodInfo = methods[index]; 423 | foreach (ContextMenu contextMenu in methodInfo.GetCustomAttributes(contextMenuType, false)) 424 | { 425 | if (contextData.ContainsKey(contextMenu.menuItem)) 426 | { 427 | var data = contextData[contextMenu.menuItem]; 428 | if (contextMenu.validate) 429 | data.validate = methodInfo; 430 | else 431 | data.function = methodInfo; 432 | contextData[data.menuItem] = data; 433 | } 434 | else 435 | { 436 | var data = new ContextMenuData(contextMenu.menuItem); 437 | if (contextMenu.validate) 438 | data.validate = methodInfo; 439 | else 440 | data.function = methodInfo; 441 | contextData.Add(data.menuItem, data); 442 | } 443 | } 444 | } 445 | } 446 | 447 | private void CreateListData(SerializedProperty property) 448 | { 449 | string parent = GetGrandParentPath(property); 450 | 451 | // Try to find the grand parent in SortableListData 452 | SortableListData data = listIndex.Find(listData => listData.Parent.Equals(parent)); 453 | if (data == null) 454 | { 455 | data = new SortableListData(parent); 456 | listIndex.Add(data); 457 | } 458 | 459 | data.AddProperty(property); 460 | object[] attr = property.GetAttributes(); 461 | if (attr != null && attr.Length == 1) 462 | { 463 | ReorderableAttribute arrayAttr = (ReorderableAttribute)attr[0]; 464 | if (arrayAttr != null) 465 | { 466 | HandleReorderableOptions(arrayAttr, property, data); 467 | } 468 | } 469 | } 470 | 471 | private void HandleReorderableOptions(ReorderableAttribute arrayAttr, SerializedProperty property, SortableListData data) 472 | { 473 | // Custom element header 474 | if (string.IsNullOrEmpty(arrayAttr.ElementHeader) == false) 475 | { 476 | data.ElementHeaderCallback = i => string.Format("{0} {1}", arrayAttr.ElementHeader, (arrayAttr.HeaderZeroIndex ? i : i + 1)); 477 | } 478 | 479 | // Draw property as single line 480 | if (arrayAttr.ElementSingleLine) 481 | { 482 | var list = data.GetPropertyList(property); 483 | #if UNITY_5_3_OR_NEWER 484 | list.elementHeightCallback = index => EditorGUIUtility.singleLineHeight + 6; 485 | list.drawElementBackgroundCallback = (rect, index, active, focused) => 486 | { 487 | if (focused == false) 488 | return; 489 | if (styleHighlight == null) 490 | styleHighlight = GUI.skin.FindStyle("MeTransitionSelectHead"); 491 | GUI.Box(rect, GUIContent.none, styleHighlight); 492 | }; 493 | #endif 494 | 495 | list.drawElementCallback = (rect, index, active, focused) => 496 | { 497 | var element = property.GetArrayElementAtIndex(index); 498 | element.isExpanded = false; 499 | 500 | int childCount = data.GetElementCount(property); 501 | if (childCount < 1) 502 | return; 503 | 504 | rect.y += 3; 505 | rect.height -= 6; 506 | 507 | if (element.NextVisible(true)) 508 | { 509 | float restoreWidth = EditorGUIUtility.labelWidth; 510 | EditorGUIUtility.labelWidth /= childCount; 511 | 512 | float padding = 5f; 513 | float width = rect.width - padding * (childCount - 1); 514 | width /= childCount; 515 | 516 | Rect childRect = new Rect(rect) { width = width }; 517 | int depth = element.Copy().depth; 518 | do 519 | { 520 | if (element.depth != depth) 521 | break; 522 | 523 | if (childCount <= 2) 524 | EditorGUI.PropertyField(childRect, element, false); 525 | else 526 | EditorGUI.PropertyField(childRect, element, GUIContent.none, false); 527 | childRect.x += width + padding; 528 | } while (element.NextVisible(false)); 529 | 530 | EditorGUIUtility.labelWidth = restoreWidth; 531 | } 532 | }; 533 | } 534 | } 535 | 536 | /// 537 | /// Given a SerializedProperty, return the automatic ReorderableList assigned to it if any 538 | /// 539 | /// 540 | /// 541 | protected ReorderableList GetSortableList(SerializedProperty property) 542 | { 543 | if (listIndex.Count == 0) 544 | return null; 545 | 546 | string parent = GetGrandParentPath(property); 547 | 548 | SortableListData data = listIndex.Find(listData => listData.Parent.Equals(parent)); 549 | if (data == null) 550 | return null; 551 | 552 | return data.GetPropertyList(property); 553 | } 554 | 555 | /// 556 | /// Set a drag and drop handler function on a SerializedObject's ReorderableList, if any 557 | /// 558 | /// 559 | /// 560 | /// 561 | protected bool SetDragDropHandler(SerializedProperty property, Action handler) 562 | { 563 | if (listIndex.Count == 0) 564 | return false; 565 | 566 | string parent = GetGrandParentPath(property); 567 | 568 | SortableListData data = listIndex.Find(listData => listData.Parent.Equals(parent)); 569 | if (data == null) 570 | return false; 571 | 572 | data.SetDropHandler(property, handler); 573 | return true; 574 | } 575 | #endregion 576 | 577 | protected bool InspectorGUIStart(bool force = false) 578 | { 579 | // Not initialized, try initializing 580 | if (hasSortableArrays && listIndex.Count == 0) 581 | InitInspector(); 582 | if (hasEditable && editableIndex.Count == 0) 583 | InitInspector(); 584 | 585 | // No sortable arrays or list index unintialized 586 | bool cannotDrawOrderable = (hasSortableArrays == false || listIndex.Count == 0); 587 | bool cannotDrawEditable = (hasEditable == false || editableIndex.Count == 0); 588 | if (cannotDrawOrderable && cannotDrawEditable && force == false) 589 | { 590 | if (isSubEditor) 591 | DrawPropertiesExcluding(serializedObject, "m_Script"); 592 | else 593 | base.OnInspectorGUI(); 594 | 595 | DrawContextMenuButtons(); 596 | return false; 597 | } 598 | 599 | serializedObject.Update(); 600 | return true; 601 | } 602 | 603 | protected virtual void DrawInspector() 604 | { 605 | DrawPropertiesAll(); 606 | } 607 | 608 | public override void OnInspectorGUI() 609 | { 610 | if (InspectorGUIStart(alwaysDrawInspector) == false) 611 | return; 612 | 613 | EditorGUI.BeginChangeCheck(); 614 | 615 | DrawInspector(); 616 | 617 | if (EditorGUI.EndChangeCheck()) 618 | { 619 | serializedObject.ApplyModifiedProperties(); 620 | InitInspector(true); 621 | } 622 | 623 | DrawContextMenuButtons(); 624 | } 625 | 626 | protected enum IterControl 627 | { 628 | Draw, 629 | Continue, 630 | Break 631 | } 632 | 633 | protected void IterateDrawProperty(SerializedProperty property, Func filter = null) 634 | { 635 | if (property.NextVisible(true)) 636 | { 637 | // Remember depth iteration started from 638 | int depth = property.Copy().depth; 639 | do 640 | { 641 | // If goes deeper than the iteration depth, get out 642 | if (property.depth != depth) 643 | break; 644 | if (isSubEditor && property.name.Equals("m_Script")) 645 | continue; 646 | 647 | if (filter != null) 648 | { 649 | var filterResult = filter(); 650 | if (filterResult == IterControl.Break) 651 | break; 652 | if (filterResult == IterControl.Continue) 653 | continue; 654 | } 655 | 656 | DrawPropertySortableArray(property); 657 | } while (property.NextVisible(false)); 658 | } 659 | } 660 | 661 | /// 662 | /// Draw a SerializedProperty as a ReorderableList if it was found during 663 | /// initialization, otherwise use EditorGUILayout.PropertyField 664 | /// 665 | /// 666 | protected void DrawPropertySortableArray(SerializedProperty property) 667 | { 668 | // Try to get the sortable list this property belongs to 669 | SortableListData listData = null; 670 | if (listIndex.Count > 0) 671 | listData = listIndex.Find(data => property.propertyPath.StartsWith(data.Parent)); 672 | 673 | Editor scriptableEditor; 674 | bool isScriptableEditor = editableIndex.TryGetValue(property.propertyPath, out scriptableEditor); 675 | 676 | // Has ReorderableList 677 | if (listData != null) 678 | { 679 | // Try to show the list 680 | if (listData.DoLayoutProperty(property) == false) 681 | { 682 | EditorGUILayout.PropertyField(property, false); 683 | if (property.isExpanded) 684 | { 685 | EditorGUI.indentLevel++; 686 | SerializedProperty targetProp = serializedObject.FindProperty(property.propertyPath); 687 | IterateDrawProperty(targetProp); 688 | EditorGUI.indentLevel--; 689 | } 690 | } 691 | } 692 | // Else try to draw ScriptableObject editor 693 | else if (isScriptableEditor) 694 | { 695 | bool hasHeader = property.HasAttribute(); 696 | bool hasSpace = property.HasAttribute(); 697 | 698 | float foldoutSpace = hasHeader ? 24 : 7; 699 | if (hasHeader && hasSpace) 700 | foldoutSpace = 31; 701 | 702 | hasSpace |= hasHeader; 703 | 704 | // No data in property, draw property field with create button 705 | if (scriptableEditor == null) 706 | { 707 | bool doCreate; 708 | using (new EditorGUILayout.HorizontalScope()) 709 | { 710 | EditorGUILayout.PropertyField(property, uiExpandWidth); 711 | using (new EditorGUILayout.VerticalScope(uiWidth50)) 712 | { 713 | if (hasSpace) GUILayout.Space(10); 714 | doCreate = GUILayout.Button(labelBtnCreate, EditorStyles.miniButton); 715 | } 716 | } 717 | 718 | if (doCreate) 719 | { 720 | Type propType = property.GetTypeReflection(); 721 | var createdAsset = CreateAssetWithSavePrompt(propType, "Assets"); 722 | if (createdAsset != null) 723 | { 724 | property.objectReferenceValue = createdAsset; 725 | property.isExpanded = true; 726 | } 727 | } 728 | } 729 | // Has data in property, draw foldout and editor 730 | else 731 | { 732 | EditorGUILayout.PropertyField(property); 733 | 734 | Rect rectFoldout = GUILayoutUtility.GetLastRect(); 735 | rectFoldout.width = 20; 736 | if (hasSpace) rectFoldout.yMin += foldoutSpace; 737 | 738 | property.isExpanded = EditorGUI.Foldout(rectFoldout, property.isExpanded, GUIContent.none); 739 | 740 | if (property.isExpanded) 741 | { 742 | EditorGUI.indentLevel++; 743 | using (new EditorGUILayout.VerticalScope(styleEditBox)) 744 | { 745 | var restoreIndent = EditorGUI.indentLevel; 746 | EditorGUI.indentLevel = 1; 747 | scriptableEditor.serializedObject.Update(); 748 | scriptableEditor.OnInspectorGUI(); 749 | scriptableEditor.serializedObject.ApplyModifiedProperties(); 750 | EditorGUI.indentLevel = restoreIndent; 751 | } 752 | EditorGUI.indentLevel--; 753 | } 754 | } 755 | } 756 | else 757 | { 758 | SerializedProperty targetProp = serializedObject.FindProperty(property.propertyPath); 759 | 760 | bool isStartProp = targetProp.propertyPath.StartsWith("m_"); 761 | using (new EditorGUI.DisabledScope(isStartProp)) 762 | { 763 | EditorGUILayout.PropertyField(targetProp, targetProp.isExpanded); 764 | } 765 | } 766 | } 767 | 768 | // Creates a new ScriptableObject via the default Save File panel 769 | private ScriptableObject CreateAssetWithSavePrompt(Type type, string path) 770 | { 771 | path = EditorUtility.SaveFilePanelInProject("Save ScriptableObject", "New " + type.Name + ".asset", "asset", "Enter a file name for the ScriptableObject.", path); 772 | if (path == "") return null; 773 | ScriptableObject asset = ScriptableObject.CreateInstance(type); 774 | AssetDatabase.CreateAsset(asset, path); 775 | AssetDatabase.SaveAssets(); 776 | AssetDatabase.Refresh(); 777 | AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); 778 | EditorGUIUtility.PingObject(asset); 779 | return asset; 780 | } 781 | 782 | #region Helper functions 783 | /// 784 | /// Draw the default inspector, with the sortable arrays 785 | /// 786 | public void DrawPropertiesAll() 787 | { 788 | SerializedProperty iterProp = serializedObject.GetIterator(); 789 | IterateDrawProperty(iterProp); 790 | } 791 | 792 | /// 793 | /// Draw the default inspector, except for the given property names 794 | /// 795 | /// 796 | public void DrawPropertiesExcept(params string[] propertyNames) 797 | { 798 | SerializedProperty iterProp = serializedObject.GetIterator(); 799 | 800 | IterateDrawProperty(iterProp, 801 | filter: () => 802 | { 803 | if (propertyNames.Contains(iterProp.name)) 804 | return IterControl.Continue; 805 | return IterControl.Draw; 806 | }); 807 | } 808 | 809 | /// 810 | /// Draw the default inspector, starting from a given property 811 | /// 812 | /// Property name to start from 813 | public void DrawPropertiesFrom(string propertyStart) 814 | { 815 | bool canDraw = false; 816 | SerializedProperty iterProp = serializedObject.GetIterator(); 817 | IterateDrawProperty(iterProp, 818 | filter: () => 819 | { 820 | if (iterProp.name.Equals(propertyStart)) 821 | canDraw = true; 822 | if (canDraw) 823 | return IterControl.Draw; 824 | return IterControl.Continue; 825 | }); 826 | } 827 | 828 | /// 829 | /// Draw the default inspector, up to a given property 830 | /// 831 | /// Property name to stop at 832 | public void DrawPropertiesUpTo(string propertyStop) 833 | { 834 | SerializedProperty iterProp = serializedObject.GetIterator(); 835 | IterateDrawProperty(iterProp, 836 | filter: () => 837 | { 838 | if (iterProp.name.Equals(propertyStop)) 839 | return IterControl.Break; 840 | return IterControl.Draw; 841 | }); 842 | } 843 | 844 | /// 845 | /// Draw the default inspector, starting from a given property to a stopping property 846 | /// 847 | /// Property name to start from 848 | /// Property name to stop at 849 | public void DrawPropertiesFromUpTo(string propertyStart, string propertyStop) 850 | { 851 | bool canDraw = false; 852 | SerializedProperty iterProp = serializedObject.GetIterator(); 853 | IterateDrawProperty(iterProp, 854 | filter: () => 855 | { 856 | if (iterProp.name.Equals(propertyStop)) 857 | return IterControl.Break; 858 | 859 | if (iterProp.name.Equals(propertyStart)) 860 | canDraw = true; 861 | 862 | if (canDraw == false) 863 | return IterControl.Continue; 864 | 865 | return IterControl.Draw; 866 | }); 867 | } 868 | 869 | public void DrawContextMenuButtons() 870 | { 871 | if (contextData.Count == 0) return; 872 | 873 | EditorGUILayout.Space(); 874 | EditorGUILayout.LabelField("Context Menu", EditorStyles.boldLabel); 875 | foreach (KeyValuePair kv in contextData) 876 | { 877 | bool enabledState = GUI.enabled; 878 | bool isEnabled = true; 879 | if (kv.Value.validate != null) 880 | isEnabled = (bool)kv.Value.validate.Invoke(target, null); 881 | 882 | GUI.enabled = isEnabled; 883 | if (GUILayout.Button(kv.Key) && kv.Value.function != null) 884 | { 885 | kv.Value.function.Invoke(target, null); 886 | } 887 | GUI.enabled = enabledState; 888 | } 889 | } 890 | #endregion 891 | } 892 | } 893 | -------------------------------------------------------------------------------- /Editor/SerializedPropExtension.cs: -------------------------------------------------------------------------------- 1 | /*MIT License 2 | 3 | Copyright(c) 2017 Jeiel Aranal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE.*/ 22 | 23 | using System; 24 | using System.Collections; 25 | using System.Collections.Generic; 26 | using System.Linq; 27 | using System.Reflection; 28 | using UnityEditor; 29 | using UnityEngine; 30 | 31 | namespace SubjectNerd.Utilities 32 | { 33 | internal static class SerializedPropExtension 34 | { 35 | #region Simple string path based extensions 36 | /// 37 | /// Returns the path to the parent of a SerializedProperty 38 | /// 39 | /// 40 | /// 41 | public static string ParentPath(this SerializedProperty prop) 42 | { 43 | int lastDot = prop.propertyPath.LastIndexOf('.'); 44 | if (lastDot == -1) // No parent property 45 | return ""; 46 | 47 | return prop.propertyPath.Substring(0, lastDot); 48 | } 49 | 50 | /// 51 | /// Returns the parent of a SerializedProperty, as another SerializedProperty 52 | /// 53 | /// 54 | /// 55 | public static SerializedProperty GetParentProp(this SerializedProperty prop) 56 | { 57 | string parentPath = prop.ParentPath(); 58 | return prop.serializedObject.FindProperty(parentPath); 59 | } 60 | #endregion 61 | 62 | /// 63 | /// Set isExpanded of the SerializedProperty and propogate the change up the hierarchy 64 | /// 65 | /// 66 | /// isExpanded value 67 | public static void ExpandHierarchy(this SerializedProperty prop, bool expand = true) 68 | { 69 | prop.isExpanded = expand; 70 | SerializedProperty parent = GetParentProp(prop); 71 | if (parent != null) 72 | ExpandHierarchy(parent); 73 | } 74 | 75 | /*public static void CopyValues(this SerializedProperty destination, SerializedProperty source) 76 | { 77 | // Iterate through source property paths, 78 | SerializedProperty iterSource = source.Copy(); 79 | if (iterSource.NextVisible(true)) 80 | { 81 | string sourceParentPath = source.ParentPath(); 82 | int startDepth = iterSource.depth; 83 | do 84 | { 85 | if (iterSource.depth < startDepth) 86 | break; 87 | 88 | // Find the relative path from iteration 89 | string currPath = iterSource.propertyPath; 90 | if (currPath.StartsWith(sourceParentPath) == false) 91 | continue; 92 | 93 | string relPath = currPath.Substring(sourceParentPath.Length, currPath.Length - sourceParentPath.Length); 94 | SerializedProperty targetProp = destination.FindPropertyRelative(relPath); 95 | 96 | TransferValue(iterSource, targetProp); 97 | 98 | } while (iterSource.NextVisible(true)); 99 | } 100 | } 101 | 102 | public static bool TransferValue(SerializedProperty source, SerializedProperty dest) 103 | { 104 | if (source.propertyType != dest.propertyType) 105 | { 106 | return false; 107 | } 108 | 109 | switch (source.propertyType) 110 | { 111 | case SerializedPropertyType.Enum: 112 | dest.enumValueIndex = source.enumValueIndex; 113 | return true; 114 | case SerializedPropertyType.String: 115 | dest.stringValue = source.stringValue; 116 | return true; 117 | case SerializedPropertyType.Float: 118 | dest.floatValue = source.floatValue; 119 | return true; 120 | case SerializedPropertyType.Integer: 121 | dest.intValue = source.intValue; 122 | return true; 123 | case SerializedPropertyType.ObjectReference: 124 | dest.objectReferenceValue = source.objectReferenceValue; 125 | return true; 126 | } 127 | 128 | return false; 129 | }*/ 130 | 131 | #region Reflection based extensions 132 | // http://answers.unity3d.com/questions/425012/get-the-instance-the-serializedproperty-belongs-to.html 133 | 134 | /// 135 | /// Use reflection to get the actual data instance of a SerializedProperty 136 | /// 137 | /// 138 | /// 139 | /// 140 | public static object GetValue(this SerializedProperty prop) 141 | { 142 | var path = prop.propertyPath.Replace(".Array.data[", "["); 143 | object obj = prop.serializedObject.targetObject; 144 | var elements = path.Split('.'); 145 | foreach (var element in elements) 146 | { 147 | if (element.Contains("[")) 148 | { 149 | var elementName = element.Substring(0, element.IndexOf("[")); 150 | var index = Convert.ToInt32(element.Substring(element.IndexOf("[")).Replace("[", "").Replace("]", "")); 151 | obj = GetValue(obj, elementName, index); 152 | } 153 | else 154 | { 155 | obj = GetValue(obj, element); 156 | } 157 | } 158 | if (obj is T) 159 | return (T) obj; 160 | return null; 161 | } 162 | 163 | public static Type GetTypeReflection(this SerializedProperty prop) 164 | { 165 | object obj = GetParent(prop); 166 | if (obj == null) 167 | return null; 168 | 169 | Type objType = obj.GetType(); 170 | const BindingFlags bindingFlags = System.Reflection.BindingFlags.GetField 171 | | System.Reflection.BindingFlags.GetProperty 172 | | System.Reflection.BindingFlags.Instance 173 | | System.Reflection.BindingFlags.NonPublic 174 | | System.Reflection.BindingFlags.Public; 175 | FieldInfo field = objType.GetField(prop.name, bindingFlags); 176 | if (field == null) 177 | return null; 178 | return field.FieldType; 179 | } 180 | 181 | /// 182 | /// Uses reflection to get the actual data instance of the parent of a SerializedProperty 183 | /// 184 | /// 185 | /// 186 | /// 187 | public static T GetParent(this SerializedProperty prop) 188 | { 189 | var path = prop.propertyPath.Replace(".Array.data[", "["); 190 | object obj = prop.serializedObject.targetObject; 191 | var elements = path.Split('.'); 192 | foreach (var element in elements.Take(elements.Length - 1)) 193 | { 194 | if (element.Contains("[")) 195 | { 196 | var elementName = element.Substring(0, element.IndexOf("[")); 197 | var index = Convert.ToInt32(element.Substring(element.IndexOf("[")).Replace("[", "").Replace("]", "")); 198 | obj = GetValue(obj, elementName, index); 199 | } 200 | else 201 | { 202 | obj = GetValue(obj, element); 203 | } 204 | } 205 | return (T) obj; 206 | } 207 | 208 | private static object GetValue(object source, string name) 209 | { 210 | if (source == null) 211 | return null; 212 | Type type = source.GetType(); 213 | FieldInfo f = type.GetField(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); 214 | if (f == null) 215 | { 216 | PropertyInfo p = type.GetProperty(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); 217 | if (p == null) 218 | return null; 219 | return p.GetValue(source, null); 220 | } 221 | return f.GetValue(source); 222 | } 223 | 224 | private static object GetValue(object source, string name, int index) 225 | { 226 | var enumerable = GetValue(source, name) as IEnumerable; 227 | if (enumerable == null) 228 | return null; 229 | var enm = enumerable.GetEnumerator(); 230 | while (index-- >= 0) 231 | enm.MoveNext(); 232 | return enm.Current; 233 | } 234 | 235 | /// 236 | /// Use reflection to check if SerializedProperty has a given attribute 237 | /// 238 | /// 239 | /// 240 | /// 241 | public static bool HasAttribute(this SerializedProperty prop) 242 | { 243 | object[] attributes = GetAttributes(prop); 244 | if (attributes != null) 245 | { 246 | return attributes.Length > 0; 247 | } 248 | return false; 249 | } 250 | 251 | /// 252 | /// Use reflection to get the attributes of the SerializedProperty 253 | /// 254 | /// 255 | /// 256 | /// 257 | public static object[] GetAttributes(this SerializedProperty prop) 258 | { 259 | object obj = GetParent(prop); 260 | if (obj == null) 261 | return new object[0]; 262 | 263 | Type attrType = typeof (T); 264 | Type objType = obj.GetType(); 265 | const BindingFlags bindingFlags = System.Reflection.BindingFlags.GetField 266 | | System.Reflection.BindingFlags.GetProperty 267 | | System.Reflection.BindingFlags.Instance 268 | | System.Reflection.BindingFlags.NonPublic 269 | | System.Reflection.BindingFlags.Public; 270 | FieldInfo field = objType.GetField(prop.name, bindingFlags); 271 | if (field != null) 272 | return field.GetCustomAttributes(attrType, true); 273 | return new object[0]; 274 | } 275 | 276 | /// 277 | /// Find properties in the serialized object of the given type. 278 | /// 279 | /// 280 | /// 281 | /// 282 | /// 283 | public static SerializedProperty[] FindPropsOfType(this SerializedObject obj, bool enterChildren = false) 284 | { 285 | List foundProps = new List(); 286 | Type propType = typeof(T); 287 | 288 | var iterProp = obj.GetIterator(); 289 | iterProp.Next(true); 290 | 291 | if (iterProp.NextVisible(enterChildren)) 292 | { 293 | do 294 | { 295 | var propValue = iterProp.GetValue(); 296 | if (propValue == null) 297 | { 298 | if (iterProp.propertyType == SerializedPropertyType.ObjectReference) 299 | { 300 | if (iterProp.objectReferenceValue != null && iterProp.objectReferenceValue.GetType() == propType) 301 | foundProps.Add(iterProp.Copy()); 302 | } 303 | } 304 | else 305 | { 306 | foundProps.Add(iterProp.Copy()); 307 | } 308 | } while (iterProp.NextVisible(enterChildren)); 309 | } 310 | return foundProps.ToArray(); 311 | } 312 | 313 | #endregion 314 | } 315 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jeiel Aranal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reorderable Inspector 2 | 3 | Automatically turn arrays/lists into ReorderableLists in Unity inspectors. Inspired by [Alejandro Santiago's implementation](https://medium.com/developers-writing/how-about-having-nice-arrays-and-lists-in-unity3d-by-default-e4fba13d1b50). 4 | 5 | ![Sortable Array](./Docs/sortable-array.png) 6 | 7 | This is an editor enhancement that gives you nicer inspector features without having to write additional code. Easily rearrange arrays, add buttons for utility functions, and and edit linked `ScriptableObjects` right in your GameObject's inspector. 8 | 9 | ## Installation 10 | 11 | Download the UnityPackage from the [latest releases](https://github.com/ChemiKhazi/ReorderableInspector/releases) and import it into Unity. The directory can be moved after being imported. 12 | 13 | ## Usage 14 | 15 | To draw an array as a `ReorderableList`, mark the property with the `Reorderable` attribute. 16 | 17 | ```C# 18 | // Add this `using` statement to the top of your file 19 | using SubjectNerd.Utilities; 20 | 21 | public class ListReorderTest : MonoBehaviour 22 | { 23 | [Reorderable] 24 | public string[] stringArray; // This will be drawn with a ReorderableList 25 | 26 | public List stringList; // This will be drawn as a default array 27 | } 28 | ``` 29 | 30 | If you want to apply the reorderable list to all arrays, edit `ReorderableArrayInspector.cs` and uncomment the defines at the top of the file 31 | 32 | ## Additional Features 33 | 34 | ### `ContextMenu` buttons. 35 | 36 | Quickly add buttons for utility functions to the inspector by using Unity's `ContextMenu` attribute 37 | 38 | ```C# 39 | public class ContextMenuTest : MonoBehaviour 40 | { 41 | public bool isTestEnabled; 42 | 43 | [ContextMenu("Test Function")] 44 | private void MyTestFunction() 45 | { 46 | Debug.Log("Test function fired"); 47 | } 48 | 49 | [ContextMenu("Test Function", isValidateFunction:true)] 50 | private bool TestFunctionValidate() 51 | { 52 | return isTestEnabled; 53 | } 54 | 55 | [ContextMenu("Other Test")] 56 | private void NonValidatedTest() 57 | { 58 | Debug.Log("Non validated test fired"); 59 | } 60 | } 61 | ``` 62 | 63 | ![Context Menu](./Docs/context-menu.png) 64 | 65 | ### Inline `ScriptableObject` editing 66 | 67 | Edit settings stored in a `ScriptableObject` in the inspector with the `EditScriptable` attribute. A feature inspired by [Tom Kail's ExtendedScriptableObjectDrawer](https://heavens-vault-game.tumblr.com/post/162127808290/inline-scriptableobject-editing-in-unity) 68 | 69 | ```C# 70 | public class SkinData : ScriptableObject 71 | { 72 | public string id; 73 | public Sprite sprite; 74 | } 75 | 76 | public class TestEntity : MonoBehaviour 77 | { 78 | public string entityName; 79 | 80 | // Add the `EditScriptable` attribute to edit the `ScriptableObject` in the GameObject inspector 81 | [EditScriptable] 82 | public SkinData skin; 83 | } 84 | ``` 85 | 86 | ## Limitations 87 | 88 | - Only supports Unity 5 and above 89 | - ReorderableLists of class instances may be a little rough, especially below Unity version 5.3 90 | - Custom inspectors will not automatically gain the ability to turn arrays into reorderable lists. See next section. 91 | 92 | ## Custom inspectors 93 | 94 | Custom inspectors will not automatically draw arrays as ReorderableLists unless they inherit from `ReorderableArrayInspector`. 95 | 96 | This class contains helper functions that can handle default property drawing. Below is a template for a custom inspector. 97 | 98 | Additional custom inspector functionality is [discussed in the wiki](https://github.com/ChemiKhazi/ReorderableInspector/wiki/Custom-Inspectors). 99 | 100 | ## Inspector Template 101 | ```C# 102 | [CustomEditor(typeof(YourCustomClass))] 103 | public class CustomSortableInspector : ReorderableArrayInspector 104 | { 105 | // Called by OnEnable 106 | protected override void InitInspector() 107 | { 108 | base.InitInspector(); 109 | 110 | // Always call DrawInspector function 111 | alwaysDrawInspector = true; 112 | 113 | // Do other initializations here 114 | } 115 | 116 | // Override this function to draw 117 | protected override void DrawInspector() 118 | { 119 | // Call the relevant default drawer functions here 120 | // The following functions will automatically draw properties 121 | // with ReorderableList when applicable 122 | /* 123 | // Draw all properties 124 | DrawPropertiesAll(); 125 | 126 | // Like DrawPropertiesExcluding 127 | DrawPropertiesExcept("sprites"); 128 | 129 | // Draw all properties, starting from specified property 130 | DrawPropertiesFrom("propertyName"); 131 | 132 | // Draw all properties until before the specified property 133 | DrawPropertiesUpTo("endPropertyName"); 134 | 135 | // Draw properties starting from startProperty, ends before endProperty 136 | DrawPropertiesFromUpTo("startProperty", "endProperty"); 137 | */ 138 | 139 | // Write your custom inspector functions here 140 | EditorGUILayout.HelpBox("This is a custom inspector", MessageType.Info); 141 | } 142 | } 143 | ``` 144 | 145 | 146 | ## Buy me a coffee! 147 | 148 | If this Unity enhancement is useful to you, it would be great if you could [buy me a coffee](https://ko-fi.com/subjectnerd)! 149 | -------------------------------------------------------------------------------- /ReorderableAttribute.cs: -------------------------------------------------------------------------------- 1 | /*MIT License 2 | 3 | Copyright(c) 2017 Jeiel Aranal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE.*/ 22 | 23 | using UnityEngine; 24 | 25 | namespace SubjectNerd.Utilities 26 | { 27 | /// 28 | /// Display a List/Array as a sortable list in the inspector 29 | /// 30 | public class ReorderableAttribute : PropertyAttribute 31 | { 32 | public string ElementHeader { get; protected set; } 33 | public bool HeaderZeroIndex { get; protected set; } 34 | public bool ElementSingleLine { get; protected set; } 35 | 36 | /// 37 | /// Display a List/Array as a sortable list in the inspector 38 | /// 39 | public ReorderableAttribute() 40 | { 41 | ElementHeader = string.Empty; 42 | HeaderZeroIndex = false; 43 | ElementSingleLine = false; 44 | } 45 | 46 | /// 47 | /// Display a List/Array as a sortable list in the inspector 48 | /// 49 | /// Customize the element name in the inspector 50 | /// If false, start element list count from 1 51 | /// Try to fit the array elements in a single line 52 | public ReorderableAttribute(string headerString = "", bool isZeroIndex = true, bool isSingleLine = false) 53 | { 54 | ElementHeader = headerString; 55 | HeaderZeroIndex = isZeroIndex; 56 | ElementSingleLine = isSingleLine; 57 | } 58 | } 59 | } 60 | --------------------------------------------------------------------------------