├── .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