using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; public class ReorderableList { private const float ELEMENT_EDGE_TOP = 1; private const float ELEMENT_EDGE_BOT = 3; private const float ELEMENT_HEIGHT_OFFSET = ELEMENT_EDGE_TOP + ELEMENT_EDGE_BOT; private static int selectionHash = "ReorderableListSelection".GetHashCode(); private static int dragAndDropHash = "ReorderableListDragAndDrop".GetHashCode(); private const string EMPTY_LABEL = "List is Empty"; private const string ARRAY_ERROR = "{0} is not an Array!"; public enum ElementDisplayType { Auto, Expandable, SingleLine } public delegate void DrawHeaderDelegate(Rect rect, GUIContent label); public delegate void DrawFooterDelegate(Rect rect); public delegate void DrawElementDelegate(Rect rect, SerializedProperty element, GUIContent label, bool selected, bool focused); public delegate void ActionDelegate(ReorderableList list); public delegate bool ActionBoolDelegate(ReorderableList list); public delegate void AddDropdownDelegate(Rect buttonRect, ReorderableList list); public delegate Object DragDropReferenceDelegate(Object[] references, ReorderableList list); public delegate void DragDropAppendDelegate(Object reference, ReorderableList list); public delegate float GetElementHeightDelegate(SerializedProperty element); public delegate float GetElementsHeightDelegate(ReorderableList list); public delegate string GetElementNameDelegate(SerializedProperty element); public delegate GUIContent GetElementLabelDelegate(SerializedProperty element); public delegate void SurrogateCallback(SerializedProperty element, Object objectReference, ReorderableList list); public event DrawHeaderDelegate drawHeaderCallback; public event DrawFooterDelegate drawFooterCallback; public event DrawElementDelegate drawElementCallback; public event DrawElementDelegate drawElementBackgroundCallback; public event GetElementHeightDelegate getElementHeightCallback; public event GetElementsHeightDelegate getElementsHeightCallback; public event GetElementNameDelegate getElementNameCallback; public event GetElementLabelDelegate getElementLabelCallback; public event DragDropReferenceDelegate onValidateDragAndDropCallback; public event DragDropAppendDelegate onAppendDragDropCallback; public event ActionDelegate onReorderCallback; public event ActionDelegate onSelectCallback; public event ActionDelegate onAddCallback; public event AddDropdownDelegate onAddDropdownCallback; public event ActionDelegate onRemoveCallback; public event ActionDelegate onMouseUpCallback; public event ActionBoolDelegate onCanRemoveCallback; public event ActionDelegate onChangedCallback; public bool canAdd; public bool canRemove; public bool draggable; public bool sortable; public bool expandable; public bool multipleSelection; public GUIContent label; public float headerHeight; public float footerHeight; public float slideEasing; public float verticalSpacing; public bool showDefaultBackground; public ElementDisplayType elementDisplayType; public string elementNameProperty; public string elementNameOverride; public bool elementLabels; public Texture elementIcon; public Surrogate surrogate; public bool paginate { get { return pagination.enabled; } set { pagination.enabled = value; } } public int pageSize { get { return pagination.fixedPageSize; } set { pagination.fixedPageSize = value; } } internal readonly int id; private SerializedProperty list; private int controlID = -1; private Rect[] elementRects; private GUIContent elementLabel; private GUIContent pageInfoContent; private GUIContent pageSizeContent; private ListSelection selection; private SlideGroup slideGroup; private int pressIndex; private bool doPagination { get { return pagination.enabled && !list.serializedObject.isEditingMultipleObjects; } } private float elementSpacing { get { return Mathf.Max(0, verticalSpacing - 2); } } private bool dragging; private float pressPosition; private float dragPosition; private int dragDirection; private DragList dragList; private ListSelection beforeDragSelection; private Pagination pagination; private int dragDropControlID = -1; public ReorderableList(SerializedProperty list) : this(list, true, true, true) { } public ReorderableList(SerializedProperty list, bool canAdd, bool canRemove, bool draggable) : this(list, canAdd, canRemove, draggable, ElementDisplayType.Auto, null, null, null) { } public ReorderableList(SerializedProperty list, bool canAdd, bool canRemove, bool draggable, ElementDisplayType elementDisplayType, string elementNameProperty, Texture elementIcon) : this(list, canAdd, canRemove, draggable, elementDisplayType, elementNameProperty, null, elementIcon) { } public ReorderableList(SerializedProperty list, bool canAdd, bool canRemove, bool draggable, ElementDisplayType elementDisplayType, string elementNameProperty, string elementNameOverride, Texture elementIcon) { if (list == null) { throw new MissingListExeption(); } else if (!list.isArray) { //check if user passed in a ReorderableArray, if so, that becomes the list object SerializedProperty array = list.FindPropertyRelative("array"); if (array == null || !array.isArray) { throw new InvalidListException(); } this.list = array; } else { this.list = list; } this.canAdd = canAdd; this.canRemove = canRemove; this.draggable = draggable; this.elementDisplayType = elementDisplayType; this.elementNameProperty = elementNameProperty; this.elementNameOverride = elementNameOverride; this.elementIcon = elementIcon; id = GetHashCode(); list.isExpanded = true; label = new GUIContent(list.displayName); pageInfoContent = new GUIContent(); pageSizeContent = new GUIContent(); #if UNITY_5_6_OR_NEWER verticalSpacing = EditorGUIUtility.standardVerticalSpacing; #else verticalSpacing = 2f; #endif headerHeight = 18f; footerHeight = 13f; slideEasing = 0.15f; expandable = true; elementLabels = true; showDefaultBackground = true; multipleSelection = true; pagination = new Pagination(); elementLabel = new GUIContent(); dragList = new DragList(0); selection = new ListSelection(); slideGroup = new SlideGroup(); elementRects = new Rect[0]; } // // -- PROPERTIES -- // public SerializedProperty List { get { return list; } internal set { list = value; } } public bool HasList { get { return list != null && list.isArray; } } public int Length { get { if (!HasList) { return 0; } else if (!list.hasMultipleDifferentValues) { return list.arraySize; } //When multiple objects are selected, because of a Unity bug, list.arraySize is never guranteed to actually be the smallest //array size. So we have to find it. Not that great since we're creating SerializedObjects here. There has to be a better way! int smallerArraySize = list.arraySize; foreach (Object targetObject in list.serializedObject.targetObjects) { SerializedObject serializedObject = new SerializedObject(targetObject); SerializedProperty property = serializedObject.FindProperty(list.propertyPath); smallerArraySize = Mathf.Min(property.arraySize, smallerArraySize); } return smallerArraySize; } } public int VisibleLength { get { return pagination.GetVisibleLength(Length); } } public int[] Selected { get { return selection.ToArray(); } set { selection = new ListSelection(value); } } public int Index { get { return selection.First; } set { selection.Select(value); } } public bool IsDragging { get { return dragging; } } // // -- PUBLIC -- // public float GetHeight() { if (HasList) { float topHeight = doPagination ? headerHeight * 2 : headerHeight; return list.isExpanded ? topHeight + GetElementsHeight() + footerHeight : headerHeight; } else { return EditorGUIUtility.singleLineHeight; } } public void DoLayoutList() { Rect position = EditorGUILayout.GetControlRect(false, GetHeight(), EditorStyles.largeLabel); DoList(EditorGUI.IndentedRect(position), label); } public void DoList(Rect rect, GUIContent label) { int indent = EditorGUI.indentLevel; EditorGUI.indentLevel = 0; Rect headerRect = rect; headerRect.height = headerHeight; if (!HasList) { DrawEmpty(headerRect, string.Format(ARRAY_ERROR, label.text), GUIStyle.none, EditorStyles.helpBox); } else { controlID = GUIUtility.GetControlID(selectionHash, FocusType.Keyboard, rect); dragDropControlID = GUIUtility.GetControlID(dragAndDropHash, FocusType.Passive, rect); DrawHeader(headerRect, label); if (list.isExpanded) { if (doPagination) { Rect paginateHeaderRect = headerRect; paginateHeaderRect.y += headerRect.height; DrawPaginationHeader(paginateHeaderRect); headerRect.yMax = paginateHeaderRect.yMax - 1; } Rect elementBackgroundRect = rect; elementBackgroundRect.yMin = headerRect.yMax; elementBackgroundRect.yMax = rect.yMax - footerHeight; Event evt = Event.current; if (selection.Length > 1) { if (evt.type == EventType.ContextClick && CanSelect(evt.mousePosition)) { HandleMultipleContextClick(evt); } } if (Length > 0) { //update element rects if not dragging. Dragging caches draw rects so no need to update if (!dragging) { UpdateElementRects(elementBackgroundRect, evt); } if (elementRects.Length > 0) { int start, end; pagination.GetVisibleRange(elementRects.Length, out start, out end); Rect selectableRect = elementBackgroundRect; selectableRect.yMin = elementRects[start].yMin; selectableRect.yMax = elementRects[end - 1].yMax; HandlePreSelection(selectableRect, evt); DrawElements(elementBackgroundRect, evt); HandlePostSelection(selectableRect, evt); } } else { DrawEmpty(elementBackgroundRect, EMPTY_LABEL, Style.boxBackground, Style.verticalLabel); } Rect footerRect = rect; footerRect.yMin = elementBackgroundRect.yMax; footerRect.xMin = rect.xMax - 58; DrawFooter(footerRect); } } EditorGUI.indentLevel = indent; } public SerializedProperty AddItem(T item) where T : Object { SerializedProperty property = AddItem(); if (property != null) { property.objectReferenceValue = item; } return property; } public SerializedProperty AddItem() { if (HasList) { //TODO Validate add on multiple selected objects list.arraySize++; selection.Select(list.arraySize - 1); SetPageByIndex(list.arraySize - 1); DispatchChange(); return list.GetArrayElementAtIndex(selection.Last); } else { throw new InvalidListException(); } } public void Remove(int[] selection) { System.Array.Sort(selection); int i = selection.Length; while (--i > -1) { RemoveItem(selection[i]); } } public void RemoveItem(int index) { if (index >= 0 && index < Length) { SerializedProperty property = list.GetArrayElementAtIndex(index); if (property.propertyType == SerializedPropertyType.ObjectReference && property.objectReferenceValue) { property.objectReferenceValue = null; } list.DeleteArrayElementAtIndex(index); selection.Remove(index); //TODO Validate removal on multiple selected objects if (Length > 0) { selection.Select(Mathf.Max(0, index - 1)); } DispatchChange(); } } public SerializedProperty GetItem(int index) { if (index >= 0 && index < Length) { return list.GetArrayElementAtIndex(index); } else { return null; } } public int IndexOf(SerializedProperty element) { if (element != null) { int i = Length; while (--i > -1) { if (SerializedProperty.EqualContents(element, list.GetArrayElementAtIndex(i))) { return i; } } } return -1; } public void GrabKeyboardFocus() { GUIUtility.keyboardControl = id; } public bool HasKeyboardControl() { return GUIUtility.keyboardControl == id; } public void ReleaseKeyboardFocus() { if (GUIUtility.keyboardControl == id) { GUIUtility.keyboardControl = 0; } } public void SetPage(int page) { if (doPagination) { pagination.page = page; } } public void SetPageByIndex(int index) { if (doPagination) { pagination.page = pagination.GetPageForIndex(index); } } public int GetPage(int index) { return doPagination ? pagination.page : 0; } public int GetPageByIndex(int index) { return doPagination ? pagination.GetPageForIndex(index) : 0; } // // -- PRIVATE -- // private float GetElementsHeight() { if (getElementsHeightCallback != null) { return getElementsHeightCallback(this); } int i, len = Length; if (len == 0) { return 28; } float totalHeight = 0; float spacing = elementSpacing; int start, end; pagination.GetVisibleRange(len, out start, out end); for (i = start; i < end; i++) { totalHeight += GetElementHeight(list.GetArrayElementAtIndex(i)) + spacing; } return totalHeight + 7 - spacing; } private float GetElementHeight(SerializedProperty element) { if (getElementHeightCallback != null) { return getElementHeightCallback(element) + ELEMENT_HEIGHT_OFFSET; } else { return EditorGUI.GetPropertyHeight(element, GetElementLabel(element, elementLabels), IsElementExpandable(element)) + ELEMENT_HEIGHT_OFFSET; } } private Rect GetElementDrawRect(int index, Rect desiredRect) { if (slideEasing <= 0) { return desiredRect; } else { //lerp the drag easing toward slide easing, this creates a stronger easing at the start then slower at the end //when dealing with large lists, we can return dragging ? slideGroup.GetRect(dragList[index].startIndex, desiredRect, slideEasing) : slideGroup.SetRect(index, desiredRect); } } /* private Rect GetElementHeaderRect(SerializedProperty element, Rect elementRect) { Rect rect = elementRect; rect.height = EditorGUIUtility.singleLineHeight + verticalSpacing; return rect; } */ private Rect GetElementRenderRect(SerializedProperty element, Rect elementRect) { float offset = draggable ? 20 : 5; Rect rect = elementRect; rect.xMin += IsElementExpandable(element) ? offset + 10 : offset; rect.xMax -= 5; rect.yMin += ELEMENT_EDGE_TOP; rect.yMax -= ELEMENT_EDGE_BOT; return rect; } private void DrawHeader(Rect rect, GUIContent label) { if (showDefaultBackground && Event.current.type == EventType.Repaint) { Style.headerBackground.Draw(rect, false, false, false, false); } HandleDragAndDrop(rect, Event.current); bool multiline = elementDisplayType != ElementDisplayType.SingleLine; Rect titleRect = rect; titleRect.xMin += 6f; titleRect.xMax -= multiline ? 95f : 55f; titleRect.height -= 2f; titleRect.y++; label = EditorGUI.BeginProperty(titleRect, label, list); if (drawHeaderCallback != null) { drawHeaderCallback(titleRect, label); } else if (expandable) { titleRect.xMin += 10; EditorGUI.BeginChangeCheck(); bool isExpanded = EditorGUI.Foldout(titleRect, list.isExpanded, label, true); if (EditorGUI.EndChangeCheck()) { list.isExpanded = isExpanded; } } else { GUI.Label(titleRect, label, EditorStyles.label); } EditorGUI.EndProperty(); if (multiline) { Rect bRect1 = rect; bRect1.xMin = rect.xMax - 25; bRect1.xMax = rect.xMax - 5; if (GUI.Button(bRect1, Style.expandButton, Style.preButton)) { ExpandElements(true); } Rect bRect2 = rect; bRect2.xMin = bRect1.xMin - 20; bRect2.xMax = bRect1.xMin; if (GUI.Button(bRect2, Style.collapseButton, Style.preButton)) { ExpandElements(false); } rect.xMax = bRect2.xMin + 5; } //draw sorting options if (sortable) { Rect sortRect1 = rect; sortRect1.xMin = rect.xMax - 25; sortRect1.xMax = rect.xMax; Rect sortRect2 = rect; sortRect2.xMin = sortRect1.xMin - 20; sortRect2.xMax = sortRect1.xMin; if (EditorGUI.DropdownButton(sortRect1, Style.sortAscending, FocusType.Passive, Style.preButton)) { SortElements(sortRect1, false); } if (EditorGUI.DropdownButton(sortRect2, Style.sortDescending, FocusType.Passive, Style.preButton)) { SortElements(sortRect2, true); } } } private void ExpandElements(bool expand) { if (!list.isExpanded && expand) { list.isExpanded = true; } int i, len = Length; for (i = 0; i < len; i++) { list.GetArrayElementAtIndex(i).isExpanded = expand; } } private void SortElements(Rect rect, bool descending) { int total = Length; //no point in sorting a list with 1 element! if (total <= 1) { return; } //the first property tells us what type of items are in the list //if generic, then we give the user a list of properties to sort on SerializedProperty prop = list.GetArrayElementAtIndex(0); if (prop.propertyType == SerializedPropertyType.Generic) { GenericMenu menu = new GenericMenu(); SerializedProperty property = prop.Copy(); SerializedProperty end = property.GetEndProperty(); bool enterChildren = true; while (property.NextVisible(enterChildren) && !SerializedProperty.EqualContents(property, end)) { menu.AddItem(new GUIContent(property.name), false, userData => { //sort based on the property selected then apply the changes ListSort.SortOnProperty(list, total, descending, (string)userData); ApplyReorder(); HandleUtility.Repaint(); }, property.name); enterChildren = false; } menu.DropDown(rect); } else { //list is not generic, so we just sort directly on the type then apply the changes ListSort.SortOnType(list, total, descending, prop.propertyType); ApplyReorder(); } } private void DrawEmpty(Rect rect, string label, GUIStyle backgroundStyle, GUIStyle labelStyle) { if (showDefaultBackground && Event.current.type == EventType.Repaint) { backgroundStyle.Draw(rect, false, false, false, false); } EditorGUI.LabelField(rect, label, labelStyle); } private void UpdateElementRects(Rect rect, Event evt) { //resize array if elements changed int i, len = Length; if (len != elementRects.Length) { System.Array.Resize(ref elementRects, len); } if (evt.type == EventType.Repaint) { //start rect Rect elementRect = rect; elementRect.yMin = elementRect.yMax = rect.yMin + 2; float spacing = elementSpacing; int start, end; pagination.GetVisibleRange(len, out start, out end); for (i = start; i < end; i++) { SerializedProperty element = list.GetArrayElementAtIndex(i); //update the elementRects value for this object. Grab the last elementRect for startPosition elementRect.y = elementRect.yMax; elementRect.height = GetElementHeight(element); elementRects[i] = elementRect; elementRect.yMax += spacing; } } } private void DrawElements(Rect rect, Event evt) { //draw list background if (showDefaultBackground && evt.type == EventType.Repaint) { Style.boxBackground.Draw(rect, false, false, false, false); } //if not dragging, draw elements as usual if (!dragging) { int start, end; pagination.GetVisibleRange(Length, out start, out end); for (int i = start; i < end; i++) { bool selected = selection.Contains(i); DrawElement(list.GetArrayElementAtIndex(i), GetElementDrawRect(i, elementRects[i]), selected, selected && GUIUtility.keyboardControl == controlID); } } else if (evt.type == EventType.Repaint) { //draw dragging elements only when repainting int i, s, len = dragList.Length; int sLen = selection.Length; //first, find the rects of the selected elements, we need to use them for overlap queries for (i = 0; i < sLen; i++) { DragElement element = dragList[i]; //update the element desiredRect if selected. Selected elements appear first in the dragList, so other elements later in iteration will have rects to compare element.desiredRect.y = dragPosition - element.dragOffset; dragList[i] = element; } //draw elements, start from the bottom of the list as first elements are the ones selected, so should be drawn last i = len; while (--i > -1) { DragElement element = dragList[i]; //draw dragging elements last as the loop is backwards if (element.selected) { DrawElement(element.property, element.desiredRect, true, true); continue; } //loop over selection and see what overlaps //if dragging down we start from the bottom of the selection //otherwise we start from the top. This helps to cover multiple selected objects Rect elementRect = element.rect; int elementIndex = element.startIndex; int start = dragDirection > 0 ? sLen - 1 : 0; int end = dragDirection > 0 ? -1 : sLen; for (s = start; s != end; s -= dragDirection) { DragElement selected = dragList[s]; if (selected.Overlaps(elementRect, elementIndex, dragDirection)) { elementRect.y -= selected.rect.height * dragDirection; elementIndex += dragDirection; } } //draw the element with the new rect DrawElement(element.property, GetElementDrawRect(i, elementRect), false, false); //reassign the element back into the dragList element.desiredRect = elementRect; dragList[i] = element; } } } private void DrawElement(SerializedProperty element, Rect rect, bool selected, bool focused) { Event evt = Event.current; if (drawElementBackgroundCallback != null) { drawElementBackgroundCallback(rect, element, null, selected, focused); } else if (evt.type == EventType.Repaint) { Style.elementBackground.Draw(rect, false, selected, selected, focused); } if (evt.type == EventType.Repaint && draggable) { Style.draggingHandle.Draw(new Rect(rect.x + 5, rect.y + 6, 10, rect.height - (rect.height - 6)), false, false, false, false); } GUIContent label = GetElementLabel(element, elementLabels); Rect renderRect = GetElementRenderRect(element, rect); if (drawElementCallback != null) { drawElementCallback(renderRect, element, label, selected, focused); } else { EditorGUI.PropertyField(renderRect, element, label, true); } //handle context click int controlId = GUIUtility.GetControlID(label, FocusType.Passive, rect); switch (evt.GetTypeForControl(controlId)) { case EventType.ContextClick: if (rect.Contains(evt.mousePosition)) { HandleSingleContextClick(evt, element); } break; } } private GUIContent GetElementLabel(SerializedProperty element, bool allowElementLabel) { if (!allowElementLabel) { return GUIContent.none; } else if (getElementLabelCallback != null) { return getElementLabelCallback(element); } string name; if (getElementNameCallback != null) { name = getElementNameCallback(element); } else { name = GetElementName(element, elementNameProperty, elementNameOverride); } elementLabel.text = !string.IsNullOrEmpty(name) ? name : element.displayName; elementLabel.tooltip = element.tooltip; elementLabel.image = elementIcon; return elementLabel; } private static string GetElementName(SerializedProperty element, string nameProperty, string nameOverride) { if (!string.IsNullOrEmpty(nameOverride)) { string path = element.propertyPath; const string arrayEndDelimeter = "]"; const char arrayStartDelimeter = '['; if (path.EndsWith(arrayEndDelimeter)) { int startIndex = path.LastIndexOf(arrayStartDelimeter) + 1; return string.Format("{0} {1}", nameOverride, path.Substring(startIndex, path.Length - startIndex - 1)); } return nameOverride; } else if (string.IsNullOrEmpty(nameProperty)) { return null; } else if (element.propertyType == SerializedPropertyType.ObjectReference && nameProperty == "name") { return element.objectReferenceValue ? element.objectReferenceValue.name : null; } SerializedProperty prop = element.FindPropertyRelative(nameProperty); if (prop != null) { switch (prop.propertyType) { case SerializedPropertyType.ObjectReference: return prop.objectReferenceValue ? prop.objectReferenceValue.name : null; case SerializedPropertyType.Enum: return prop.enumDisplayNames[prop.enumValueIndex]; case SerializedPropertyType.Integer: case SerializedPropertyType.Character: return prop.intValue.ToString(); case SerializedPropertyType.LayerMask: return GetLayerMaskName(prop.intValue); case SerializedPropertyType.String: return prop.stringValue; case SerializedPropertyType.Float: return prop.floatValue.ToString(); } return prop.displayName; } return null; } private static string GetLayerMaskName(int mask) { if (mask == 0) { return "Nothing"; } else if (mask < 0) { return "Everything"; } string name = string.Empty; int n = 0; for (int i = 0; i < 32; i++) { if (((1 << i) & mask) != 0) { if (n == 4) { return "Mixed ..."; } name += (n > 0 ? ", " : string.Empty) + LayerMask.LayerToName(i); n++; } } return name; } private void DrawFooter(Rect rect) { if (drawFooterCallback != null) { drawFooterCallback(rect); return; } if (Event.current.type == EventType.Repaint) { Style.footerBackground.Draw(rect, false, false, false, false); } Rect addRect = new Rect(rect.xMin + 4f, rect.y - 3f, 25f, 13f); Rect subRect = new Rect(rect.xMax - 29f, rect.y - 3f, 25f, 13f); EditorGUI.BeginDisabledGroup(!canAdd); if (GUI.Button(addRect, onAddDropdownCallback != null ? Style.iconToolbarPlusMore : Style.iconToolbarPlus, Style.preButton)) { if (onAddDropdownCallback != null) { onAddDropdownCallback(addRect, this); } else if (onAddCallback != null) { onAddCallback(this); } else { AddItem(); } } EditorGUI.EndDisabledGroup(); EditorGUI.BeginDisabledGroup(!CanSelect(selection) || !canRemove || (onCanRemoveCallback != null && !onCanRemoveCallback(this))); if (GUI.Button(subRect, Style.iconToolbarMinus, Style.preButton)) { if (onRemoveCallback != null) { onRemoveCallback(this); } else { Remove(selection.ToArray()); } } EditorGUI.EndDisabledGroup(); } private void DrawPaginationHeader(Rect rect) { int total = Length; int pages = pagination.GetPageCount(total); int page = Mathf.Clamp(pagination.page, 0, pages - 1); //some actions may have reduced the page count, so we need to check the current page against the clamped one //if different, we need to change and repaint if (page != pagination.page) { pagination.page = page; HandleUtility.Repaint(); } Rect prevRect = new Rect(rect.xMin + 4f, rect.y - 1f, 17f, 14f); Rect popupRect = new Rect(prevRect.xMax, rect.y - 1f, 14f, 14f); Rect nextRect = new Rect(popupRect.xMax, rect.y - 1f, 17f, 14f); if (Event.current.type == EventType.Repaint) { Style.paginationHeader.Draw(rect, false, true, true, false); } pageInfoContent.text = string.Format(Style.PAGE_INFO_FORMAT, pagination.page + 1, pages); Rect pageInfoRect = rect; pageInfoRect.width = Style.paginationText.CalcSize(pageInfoContent).x; pageInfoRect.x = rect.xMax - pageInfoRect.width - 7; pageInfoRect.y += 2; //draw page info GUI.Label(pageInfoRect, pageInfoContent, Style.paginationText); //draw page buttons and page popup if (GUI.Button(prevRect, Style.iconPagePrev, Style.preButton)) { pagination.page = Mathf.Max(0, pagination.page - 1); } if (EditorGUI.DropdownButton(popupRect, Style.iconPagePopup, FocusType.Passive, Style.preButton)) { GenericMenu menu = new GenericMenu(); for (int i = 0; i < pages; i++) { int pageIndex = i; menu.AddItem(new GUIContent(string.Format("Page {0}", i + 1)), i == pagination.page, OnPageDropDownSelect, pageIndex); } menu.DropDown(popupRect); } if (GUI.Button(nextRect, Style.iconPageNext, Style.preButton)) { pagination.page = Mathf.Min(pages - 1, pagination.page + 1); } //if we're allowed to control the page size manually, show an editor bool useFixedPageSize = pagination.fixedPageSize > 0; EditorGUI.BeginDisabledGroup(useFixedPageSize); pageSizeContent.text = total.ToString(); GUIStyle style = Style.pageSizeTextField; Texture icon = Style.listIcon.image; float min = nextRect.xMax + 5; float max = pageInfoRect.xMin - 5; float space = max - min; float labelWidth = icon.width + 2; float width = style.CalcSize(pageSizeContent).x + 50 + labelWidth; Rect pageSizeRect = rect; pageSizeRect.y--; pageSizeRect.x = min + (space - width) / 2; pageSizeRect.width = width - labelWidth; EditorGUI.BeginChangeCheck(); EditorGUIUtility.labelWidth = labelWidth; EditorGUIUtility.SetIconSize(new Vector2(icon.width, icon.height)); int newPageSize = EditorGUI.DelayedIntField(pageSizeRect, Style.listIcon, useFixedPageSize ? pagination.fixedPageSize : pagination.customPageSize, style); EditorGUIUtility.labelWidth = 0; EditorGUIUtility.SetIconSize(Vector2.zero); if (EditorGUI.EndChangeCheck()) { pagination.customPageSize = Mathf.Clamp(newPageSize, 0, total); pagination.page = Mathf.Min(pagination.GetPageCount(total) - 1, pagination.page); } EditorGUI.EndDisabledGroup(); } private void OnPageDropDownSelect(object userData) { pagination.page = (int)userData; } private void DispatchChange() { if (onChangedCallback != null) { onChangedCallback(this); } } private void HandleSingleContextClick(Event evt, SerializedProperty element) { selection.Select(IndexOf(element)); GenericMenu menu = new GenericMenu(); if (element.isInstantiatedPrefab) { menu.AddItem(new GUIContent("Revert " + GetElementLabel(element, true).text + " to Prefab"), false, selection.RevertValues, list); menu.AddSeparator(string.Empty); } HandleSharedContextClick(evt, menu, "Duplicate Array Element", "Delete Array Element", "Move Array Element"); } private void HandleMultipleContextClick(Event evt) { GenericMenu menu = new GenericMenu(); if (selection.CanRevert(list)) { menu.AddItem(new GUIContent("Revert Values to Prefab"), false, selection.RevertValues, list); menu.AddSeparator(string.Empty); } HandleSharedContextClick(evt, menu, "Duplicate Array Elements", "Delete Array Elements", "Move Array Elements"); } private void HandleSharedContextClick(Event evt, GenericMenu menu, string duplicateLabel, string deleteLabel, string moveLabel) { menu.AddItem(new GUIContent(duplicateLabel), false, HandleDuplicate, list); menu.AddItem(new GUIContent(deleteLabel), false, HandleDelete, list); if (doPagination) { int pages = pagination.GetPageCount(Length); if (pages > 1) { for (int i = 0; i < pages; i++) { string label = string.Format("{0}/Page {1}", moveLabel, i + 1); menu.AddItem(new GUIContent(label), i == pagination.page, HandleMoveElement, i); } } } menu.ShowAsContext(); evt.Use(); } private void HandleMoveElement(object userData) { int toPage = (int)userData; int fromPage = pagination.page; int size = pagination.pageSize; int offset = (toPage * size) - (fromPage * size); int direction = offset > 0 ? 1 : -1; int total = Length; //We need to find the actually positions things will move to and not clamp the index //because sometimes something wants to move to a negative index, or beyond the length //we need to find this overlow and adjust the move offsets based on that int overflow = 0; for (int i = 0; i < selection.Length; i++) { int desiredIndex = selection[i] + offset; overflow = direction < 0 ? Mathf.Min(overflow, desiredIndex) : Mathf.Max(overflow, desiredIndex - total); } offset -= overflow; //copy the current list to prepare for moving UpdateDragList(0, 0, total); //create a list that will act as our new order List orderedList = new List(dragList.Elements.Where(t => !selection.Contains(t.startIndex))); //go through the selection and insert them into the new order based on the page offset selection.Sort(); for (int i = 0; i < selection.Length; i++) { int selIndex = selection[i]; int oldIndex = dragList.GetIndexFromSelection(selIndex); int newIndex = Mathf.Clamp(selIndex + offset, 0, orderedList.Count); orderedList.Insert(newIndex, dragList[oldIndex]); } //finally, perform the re-order dragList.Elements = orderedList.ToArray(); ReorderDraggedElements(direction, 0, null); //assume we still want to view these items pagination.page = toPage; HandleUtility.Repaint(); } private void HandleDelete(object userData) { selection.Delete(userData as SerializedProperty); DispatchChange(); } private void HandleDuplicate(object userData) { selection.Duplicate(userData as SerializedProperty); DispatchChange(); } private void HandleDragAndDrop(Rect rect, Event evt) { switch (evt.GetTypeForControl(dragDropControlID)) { case EventType.DragUpdated: case EventType.DragPerform: if (GUI.enabled && rect.Contains(evt.mousePosition)) { Object[] objectReferences = DragAndDrop.objectReferences; Object[] references = new Object[1]; bool acceptDrag = false; foreach (Object object1 in objectReferences) { references[0] = object1; Object object2 = ValidateObjectDragAndDrop(references); if (object2 != null) { DragAndDrop.visualMode = DragAndDropVisualMode.Copy; if (evt.type == EventType.DragPerform) { AppendDragAndDropValue(object2); acceptDrag = true; DragAndDrop.activeControlID = 0; } else { DragAndDrop.activeControlID = dragDropControlID; } } } if (acceptDrag) { GUI.changed = true; DragAndDrop.AcceptDrag(); } } break; case EventType.DragExited: if (GUI.enabled) { HandleUtility.Repaint(); } break; } } private Object ValidateObjectDragAndDrop(Object[] references) { if (onValidateDragAndDropCallback != null) { return onValidateDragAndDropCallback(references, this); } else if (surrogate.HasType) { //if we have a surrogate type, then validate using the surrogate type rather than the list return Internals.ValidateObjectDragAndDrop(references, null, surrogate.type, surrogate.exactType); } return Internals.ValidateObjectDragAndDrop(references, list, null, false); } private void AppendDragAndDropValue(Object obj) { if (onAppendDragDropCallback != null) { onAppendDragDropCallback(obj, this); } else { //check if we have a surrogate type. If so use that for appending if (surrogate.HasType) { surrogate.Invoke(AddItem(), obj, this); } else { Internals.AppendDragAndDropValue(obj, list); } } DispatchChange(); } private void HandlePreSelection(Rect rect, Event evt) { if (evt.type == EventType.MouseDrag && draggable && GUIUtility.hotControl == controlID) { if (selection.Length > 0 && UpdateDragPosition(evt.mousePosition, rect, dragList)) { GUIUtility.keyboardControl = controlID; dragging = true; } evt.Use(); } /* TODO This is buggy. The reason for this is to allow selection and dragging of an element using the header, or top row (if any) * The main issue here is determining whether the element has an "expandable" drop down arrow, which if it does, will capture the mouse event *without* the code below * Because of property drawers and certain property types, it's impossible to know this automatically (without dirty reflection) * So if the below code is active and we determine that the property is expandable but isn't actually. Then we'll accidently capture the mouse focus and prevent anything else from receiving it :( * So for now, in order to drag or select a row, the user must select empty space on the row. Not a huge deal, and doesn't break functionality. * What needs to happen is the drag event needs to occur independent of the event type. But that's messy too, as some controls have horizontal drag sliders :( if (evt.type == EventType.MouseDown) { //check if we contain the mouse press //we also need to check what has current focus. If nothing we can assume control //if there's something, check if the header has been pressed if the element is expandable //if we did press the header, then override the control if (rect.Contains(evt.mousePosition) && IsSelectionButton(evt)) { int index = GetSelectionIndex(evt.mousePosition); if (CanSelect(index)) { SerializedProperty element = list.GetArrayElementAtIndex(index); if (IsElementExpandable(element)) { Rect elementHeaderRect = GetElementHeaderRect(element, elementRects[index]); Rect elementRenderRect = GetElementRenderRect(element, elementRects[index]); Rect elementExpandRect = elementHeaderRect; elementExpandRect.xMin = elementRenderRect.xMin - 10; elementExpandRect.xMax = elementRenderRect.xMin; if (elementHeaderRect.Contains(evt.mousePosition) && !elementExpandRect.Contains(evt.mousePosition)) { DoSelection(index, true, evt); HandleUtility.Repaint(); } } } } } */ } private void HandlePostSelection(Rect rect, Event evt) { switch (evt.GetTypeForControl(controlID)) { case EventType.MouseDown: if (rect.Contains(evt.mousePosition) && IsSelectionButton(evt)) { int index = GetSelectionIndex(evt.mousePosition); if (CanSelect(index)) { DoSelection(index, GUIUtility.keyboardControl == 0 || GUIUtility.keyboardControl == controlID || evt.button == 2, evt); } else { selection.Clear(); } HandleUtility.Repaint(); } break; case EventType.MouseUp: if (!draggable) { //select the single object if no selection modifier is being performed selection.SelectWhenNoAction(pressIndex, evt); if (onMouseUpCallback != null && IsPositionWithinElement(evt.mousePosition, selection.Last)) { onMouseUpCallback(this); } } else if (GUIUtility.hotControl == controlID) { evt.Use(); if (dragging) { dragging = false; //move elements in list ReorderDraggedElements(dragDirection, dragList.StartIndex, () => dragList.SortByPosition()); } else { //if we didn't drag, then select the original pressed object selection.SelectWhenNoAction(pressIndex, evt); if (onMouseUpCallback != null) { onMouseUpCallback(this); } } GUIUtility.hotControl = 0; } HandleUtility.Repaint(); break; case EventType.KeyDown: if (GUIUtility.keyboardControl == controlID) { if (evt.keyCode == KeyCode.DownArrow && !dragging) { selection.Select(Mathf.Min(selection.Last + 1, Length - 1)); evt.Use(); } else if (evt.keyCode == KeyCode.UpArrow && !dragging) { selection.Select(Mathf.Max(selection.Last - 1, 0)); evt.Use(); } else if (evt.keyCode == KeyCode.Escape && GUIUtility.hotControl == controlID) { GUIUtility.hotControl = 0; if (dragging) { dragging = false; selection = beforeDragSelection; } evt.Use(); } } break; } } private bool IsSelectionButton(Event evt) { return evt.button == 0 || evt.button == 2; } private void DoSelection(int index, bool setKeyboardControl, Event evt) { //append selections based on action, this may be a additive (ctrl) or range (shift) selection if (multipleSelection) { selection.AppendWithAction(pressIndex = index, evt); } else { selection.Select(pressIndex = index); } if (onSelectCallback != null) { onSelectCallback(this); } if (draggable) { dragging = false; dragPosition = pressPosition = evt.mousePosition.y; int start, end; pagination.GetVisibleRange(Length, out start, out end); UpdateDragList(dragPosition, start, end); selection.Trim(start, end); beforeDragSelection = selection.Clone(); GUIUtility.hotControl = controlID; } if (setKeyboardControl) { GUIUtility.keyboardControl = controlID; } evt.Use(); } private void UpdateDragList(float dragPosition, int start, int end) { dragList.Resize(start, end - start); for (int i = start; i < end; i++) { SerializedProperty property = list.GetArrayElementAtIndex(i); Rect elementRect = elementRects[i]; DragElement dragElement = new DragElement() { property = property, dragOffset = dragPosition - elementRect.y, rect = elementRect, desiredRect = elementRect, selected = selection.Contains(i), startIndex = i }; dragList[i - start] = dragElement; } //finally, sort the dragList by selection, selected objects appear first in the list //selection order is preserved as well dragList.SortByIndex(); } private bool UpdateDragPosition(Vector2 position, Rect bounds, DragList dragList) { //find new drag position int startIndex = 0; int endIndex = selection.Length - 1; float minOffset = dragList[startIndex].dragOffset; float maxOffset = dragList[endIndex].rect.height - dragList[endIndex].dragOffset; dragPosition = Mathf.Clamp(position.y, bounds.yMin + minOffset, bounds.yMax - maxOffset); if (Mathf.Abs(dragPosition - pressPosition) > 1) { dragDirection = (int)Mathf.Sign(dragPosition - pressPosition); return true; } return false; } private void ReorderDraggedElements(int direction, int offset, System.Action sortList) { //save the current expanded states on all elements. I don't see any other way to do this //MoveArrayElement does not move the foldout states, so... fun. dragList.RecordState(); if (sortList != null) { sortList(); } selection.Sort((a, b) => { int d1 = dragList.GetIndexFromSelection(a); int d2 = dragList.GetIndexFromSelection(b); return direction > 0 ? d1.CompareTo(d2) : d2.CompareTo(d1); }); //swap the selected elements in the List int s = selection.Length; while (--s > -1) { int newIndex = dragList.GetIndexFromSelection(selection[s]); int listIndex = newIndex + offset; selection[s] = listIndex; list.MoveArrayElement(dragList[newIndex].startIndex, listIndex); } //restore expanded states on items dragList.RestoreState(list); //apply and update ApplyReorder(); } private void ApplyReorder() { list.serializedObject.ApplyModifiedProperties(); list.serializedObject.Update(); if (onReorderCallback != null) { onReorderCallback(this); } DispatchChange(); } private int GetSelectionIndex(Vector2 position) { int start, end; pagination.GetVisibleRange(elementRects.Length, out start, out end); for (int i = start; i < end; i++) { Rect rect = elementRects[i]; if (rect.Contains(position) || (i == 0 && position.y <= rect.yMin) || (i == end - 1 && position.y >= rect.yMax)) { return i; } } return -1; } private bool CanSelect(ListSelection selection) { return selection.Length > 0 ? selection.All(s => CanSelect(s)) : false; } private bool CanSelect(int index) { return index >= 0 && index < Length; } private bool CanSelect(Vector2 position) { return selection.Length > 0 ? selection.Any(s => IsPositionWithinElement(position, s)) : false; } private bool IsPositionWithinElement(Vector2 position, int index) { return CanSelect(index) ? elementRects[index].Contains(position) : false; } private bool IsElementExpandable(SerializedProperty element) { switch (elementDisplayType) { case ElementDisplayType.Auto: return element.hasVisibleChildren && IsTypeExpandable(element.propertyType); case ElementDisplayType.Expandable: return true; case ElementDisplayType.SingleLine: return false; } return false; } private bool IsTypeExpandable(SerializedPropertyType type) { switch (type) { case SerializedPropertyType.Generic: case SerializedPropertyType.Vector4: case SerializedPropertyType.Quaternion: case SerializedPropertyType.ArraySize: return true; default: return false; } } // // -- LIST STYLE -- // static class Style { internal const string PAGE_INFO_FORMAT = "{0} / {1}"; internal static GUIContent iconToolbarPlus; internal static GUIContent iconToolbarPlusMore; internal static GUIContent iconToolbarMinus; internal static GUIContent iconPagePrev; internal static GUIContent iconPageNext; internal static GUIContent iconPagePopup; internal static GUIStyle paginationText; internal static GUIStyle pageSizeTextField; internal static GUIStyle draggingHandle; internal static GUIStyle headerBackground; internal static GUIStyle footerBackground; internal static GUIStyle paginationHeader; internal static GUIStyle boxBackground; internal static GUIStyle preButton; internal static GUIStyle elementBackground; internal static GUIStyle verticalLabel; internal static GUIContent expandButton; internal static GUIContent collapseButton; internal static GUIContent sortAscending; internal static GUIContent sortDescending; internal static GUIContent listIcon; static Style() { iconToolbarPlus = EditorGUIUtility.IconContent("Toolbar Plus", "Add to list"); iconToolbarPlusMore = EditorGUIUtility.IconContent("Toolbar Plus More", "Choose to add to list"); iconToolbarMinus = EditorGUIUtility.IconContent("Toolbar Minus", "Remove selection from list"); iconPagePrev = EditorGUIUtility.IconContent("Animation.PrevKey", "Previous page"); iconPageNext = EditorGUIUtility.IconContent("Animation.NextKey", "Next page"); #if UNITY_2018_3_OR_NEWER iconPagePopup = EditorGUIUtility.IconContent("ShurikenPopup", "Select page"); #else iconPagePopup = EditorGUIUtility.IconContent("MiniPopupNoBg", "Select page"); #endif paginationText = new GUIStyle(); paginationText.margin = new RectOffset(2, 2, 0, 0); paginationText.fontSize = EditorStyles.miniTextField.fontSize; paginationText.font = EditorStyles.miniFont; paginationText.normal.textColor = EditorStyles.miniTextField.normal.textColor; paginationText.alignment = TextAnchor.UpperLeft; paginationText.clipping = TextClipping.Clip; pageSizeTextField = new GUIStyle("RL Footer"); pageSizeTextField.alignment = TextAnchor.MiddleLeft; pageSizeTextField.clipping = TextClipping.Clip; pageSizeTextField.fixedHeight = 0; pageSizeTextField.padding = new RectOffset(3, 0, 0, 0); pageSizeTextField.overflow = new RectOffset(0, 0, -2, -3); pageSizeTextField.contentOffset = new Vector2(0, -1); pageSizeTextField.font = EditorStyles.miniFont; pageSizeTextField.fontSize = EditorStyles.miniTextField.fontSize; pageSizeTextField.fontStyle = FontStyle.Normal; pageSizeTextField.wordWrap = false; draggingHandle = new GUIStyle("RL DragHandle"); headerBackground = new GUIStyle("RL Header"); footerBackground = new GUIStyle("RL Footer"); //paginationHeader = new GUIStyle("RectangleToolHBar"); paginationHeader = new GUIStyle("RL Element"); paginationHeader.border = new RectOffset(2, 3, 2, 3); elementBackground = new GUIStyle("RL Element"); elementBackground.border = new RectOffset(2, 3, 2, 3); verticalLabel = new GUIStyle(EditorStyles.label); verticalLabel.alignment = TextAnchor.UpperLeft; verticalLabel.contentOffset = new Vector2(10, 3); boxBackground = new GUIStyle("RL Background"); boxBackground.border = new RectOffset(6, 3, 3, 6); preButton = new GUIStyle("RL FooterButton"); expandButton = EditorGUIUtility.IconContent("winbtn_win_max"); expandButton.tooltip = "Expand All Elements"; collapseButton = EditorGUIUtility.IconContent("winbtn_win_min"); collapseButton.tooltip = "Collapse All Elements"; sortAscending = EditorGUIUtility.IconContent("align_vertically_bottom"); sortAscending.tooltip = "Sort Ascending"; sortDescending = EditorGUIUtility.IconContent("align_vertically_top"); sortDescending.tooltip = "Sort Descending"; listIcon = EditorGUIUtility.IconContent("align_horizontally_right"); } } // // -- DRAG LIST -- // struct DragList { private int startIndex; private DragElement[] elements; private int length; internal DragList(int length) { this.length = length; startIndex = 0; elements = new DragElement[length]; } internal int StartIndex { get { return startIndex; } } internal int Length { get { return length; } } internal DragElement[] Elements { get { return elements; } set { elements = value; } } internal DragElement this[int index] { get { return elements[index]; } set { elements[index] = value; } } internal void Resize(int start, int length) { startIndex = start; this.length = length; if (elements.Length != length) { System.Array.Resize(ref elements, length); } } internal void SortByIndex() { System.Array.Sort(elements, (a, b) => { if (b.selected) { return a.selected ? a.startIndex.CompareTo(b.startIndex) : 1; } else if (a.selected) { return b.selected ? b.startIndex.CompareTo(a.startIndex) : -1; } return a.startIndex.CompareTo(b.startIndex); }); } internal void RecordState() { for (int i = 0; i < length; i++) { elements[i].RecordState(); } } internal void RestoreState(SerializedProperty list) { for (int i = 0; i < length; i++) { elements[i].RestoreState(list.GetArrayElementAtIndex(i + startIndex)); } } internal void SortByPosition() { System.Array.Sort(elements, (a, b) => a.desiredRect.center.y.CompareTo(b.desiredRect.center.y)); } internal int GetIndexFromSelection(int index) { return System.Array.FindIndex(elements, t => t.startIndex == index); } } // // -- DRAG ELEMENT -- // struct DragElement { internal SerializedProperty property; internal int startIndex; internal float dragOffset; internal bool selected; internal Rect rect; internal Rect desiredRect; private bool isExpanded; private Dictionary states; internal bool Overlaps(Rect value, int index, int direction) { if (direction < 0 && index < startIndex) { return desiredRect.yMin < value.center.y; } else if (direction > 0 && index > startIndex) { return desiredRect.yMax > value.center.y; } return false; } internal void RecordState() { states = new Dictionary(); isExpanded = property.isExpanded; Iterate(this, property, (DragElement e, SerializedProperty p, int index) => { e.states[index] = p.isExpanded; }); } internal void RestoreState(SerializedProperty property) { property.isExpanded = isExpanded; Iterate(this, property, (DragElement e, SerializedProperty p, int index) => { p.isExpanded = e.states[index]; }); } private static void Iterate(DragElement element, SerializedProperty property, System.Action action) { SerializedProperty copy = property.Copy(); SerializedProperty end = copy.GetEndProperty(); int index = 0; while (copy.NextVisible(true) && !SerializedProperty.EqualContents(copy, end)) { if (copy.hasVisibleChildren) { action(element, copy, index); index++; } } } } // // -- SLIDE GROUP -- // class SlideGroup { private Dictionary animIDs; public SlideGroup() { animIDs = new Dictionary(); } public Rect GetRect(int id, Rect r, float easing) { if (Event.current.type != EventType.Repaint) { return r; } if (!animIDs.ContainsKey(id)) { animIDs.Add(id, r); return r; } else { Rect rect = animIDs[id]; if (rect.y != r.y) { float delta = r.y - rect.y; float absDelta = Mathf.Abs(delta); //if the distance between current rect and target is too large, then move the element towards the target rect so it reaches the destination faster if (absDelta > (rect.height * 2)) { r.y = delta > 0 ? r.y - rect.height : r.y + rect.height; } else if (absDelta > 0.5) { r.y = Mathf.Lerp(rect.y, r.y, easing); } animIDs[id] = r; HandleUtility.Repaint(); } return r; } } public Rect SetRect(int id, Rect rect) { if (animIDs.ContainsKey(id)) { animIDs[id] = rect; } else { animIDs.Add(id, rect); } return rect; } } // // -- PAGINATION -- // struct Pagination { internal bool enabled; internal int fixedPageSize; internal int customPageSize; internal int page; internal bool usePagination { get { return enabled && pageSize > 0; } } internal int pageSize { get { return fixedPageSize > 0 ? fixedPageSize : customPageSize; } } internal int GetVisibleLength(int total) { int start, end; if (GetVisibleRange(total, out start, out end)) { return end - start; } return total; } internal int GetPageForIndex(int index) { return usePagination ? Mathf.FloorToInt(index / (float)pageSize) : 0; } internal int GetPageCount(int total) { return usePagination ? Mathf.CeilToInt(total / (float)pageSize) : 1; } internal bool GetVisibleRange(int total, out int start, out int end) { if (usePagination) { int size = pageSize; start = Mathf.Clamp(page * size, 0, total - 1); end = Mathf.Min(start + size, total); return true; } start = 0; end = total; return false; } } // // -- SELECTION -- // class ListSelection : IEnumerable { private List indexes; internal int? firstSelected; public ListSelection() { indexes = new List(); } public ListSelection(int[] indexes) { this.indexes = new List(indexes); } public int First { get { return indexes.Count > 0 ? indexes[0] : -1; } } public int Last { get { return indexes.Count > 0 ? indexes[indexes.Count - 1] : -1; } } public int Length { get { return indexes.Count; } } public int this[int index] { get { return indexes[index]; } set { int oldIndex = indexes[index]; indexes[index] = value; if (oldIndex == firstSelected) { firstSelected = value; } } } public bool Contains(int index) { return indexes.Contains(index); } public void Clear() { indexes.Clear(); firstSelected = null; } public void SelectWhenNoAction(int index, Event evt) { if (!EditorGUI.actionKey && !evt.shift) { Select(index); } } public void Select(int index) { indexes.Clear(); indexes.Add(index); firstSelected = index; } public void Remove(int index) { if (indexes.Contains(index)) { indexes.Remove(index); } } public void AppendWithAction(int index, Event evt) { if (EditorGUI.actionKey) { if (Contains(index)) { Remove(index); } else { Append(index); firstSelected = index; } } else if (evt.shift && indexes.Count > 0 && firstSelected.HasValue) { indexes.Clear(); AppendRange(firstSelected.Value, index); } else if (!Contains(index)) { Select(index); } } public void Sort() { if (indexes.Count > 0) { indexes.Sort(); } } public void Sort(System.Comparison comparison) { if (indexes.Count > 0) { indexes.Sort(comparison); } } public int[] ToArray() { return indexes.ToArray(); } public ListSelection Clone() { ListSelection clone = new ListSelection(ToArray()); clone.firstSelected = firstSelected; return clone; } internal void Trim(int min, int max) { int i = indexes.Count; while (--i > -1) { int index = indexes[i]; if (index < min || index >= max) { if (index == firstSelected && i > 0) { firstSelected = indexes[i - 1]; } indexes.RemoveAt(i); } } } internal bool CanRevert(SerializedProperty list) { if (list.serializedObject.targetObjects.Length == 1) { for (int i = 0; i < Length; i++) { if (list.GetArrayElementAtIndex(this[i]).isInstantiatedPrefab) { return true; } } } return false; } internal void RevertValues(object userData) { SerializedProperty list = userData as SerializedProperty; for (int i = 0; i < Length; i++) { SerializedProperty property = list.GetArrayElementAtIndex(this[i]); if (property.isInstantiatedPrefab) { property.prefabOverride = false; } } list.serializedObject.ApplyModifiedProperties(); list.serializedObject.Update(); HandleUtility.Repaint(); } internal void Duplicate(SerializedProperty list) { int offset = 0; for (int i = 0; i < Length; i++) { this[i] += offset; list.GetArrayElementAtIndex(this[i]).DuplicateCommand(); list.serializedObject.ApplyModifiedProperties(); list.serializedObject.Update(); offset++; } HandleUtility.Repaint(); } internal void Delete(SerializedProperty list) { Sort(); int i = Length; while (--i > -1) { list.GetArrayElementAtIndex(this[i]).DeleteCommand(); } Clear(); list.serializedObject.ApplyModifiedProperties(); list.serializedObject.Update(); HandleUtility.Repaint(); } private void Append(int index) { if (index >= 0 && !indexes.Contains(index)) { indexes.Add(index); } } private void AppendRange(int from, int to) { int dir = (int)Mathf.Sign(to - from); if (dir != 0) { for (int i = from; i != to; i += dir) { Append(i); } } Append(to); } public IEnumerator GetEnumerator() { return ((IEnumerable)indexes).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)indexes).GetEnumerator(); } } // // -- SORTING -- // static class ListSort { private delegate int SortComparision(SerializedProperty p1, SerializedProperty p2); internal static void SortOnProperty(SerializedProperty list, int length, bool descending, string propertyName) { BubbleSort(list, length, (p1, p2) => { SerializedProperty a = p1.FindPropertyRelative(propertyName); SerializedProperty b = p2.FindPropertyRelative(propertyName); if (a != null && b != null && a.propertyType == b.propertyType) { int comparison = Compare(a, b, descending, a.propertyType); return descending ? -comparison : comparison; } return 0; }); } internal static void SortOnType(SerializedProperty list, int length, bool descending, SerializedPropertyType type) { BubbleSort(list, length, (p1, p2) => { int comparision = Compare(p1, p2, descending, type); return descending ? -comparision : comparision; }); } // // -- PRIVATE -- // private static void BubbleSort(SerializedProperty list, int length, SortComparision comparision) { for (int i = 0; i < length; i++) { SerializedProperty p1 = list.GetArrayElementAtIndex(i); for (int j = i + 1; j < length; j++) { SerializedProperty p2 = list.GetArrayElementAtIndex(j); if (comparision(p1, p2) > 0) { list.MoveArrayElement(j, i); } } } } private static int Compare(SerializedProperty p1, SerializedProperty p2, bool descending, SerializedPropertyType type) { if (p1 == null || p2 == null) { return 0; } switch (type) { case SerializedPropertyType.Boolean: return p1.boolValue.CompareTo(p2.boolValue); case SerializedPropertyType.Character: case SerializedPropertyType.Enum: case SerializedPropertyType.Integer: case SerializedPropertyType.LayerMask: return p1.longValue.CompareTo(p2.longValue); case SerializedPropertyType.Color: return p1.colorValue.grayscale.CompareTo(p2.colorValue.grayscale); case SerializedPropertyType.ExposedReference: return CompareObjects(p1.exposedReferenceValue, p2.exposedReferenceValue, descending); case SerializedPropertyType.Float: return p1.doubleValue.CompareTo(p2.doubleValue); case SerializedPropertyType.ObjectReference: return CompareObjects(p1.objectReferenceValue, p2.objectReferenceValue, descending); case SerializedPropertyType.String: return p1.stringValue.CompareTo(p2.stringValue); default: return 0; } } private static int CompareObjects(Object obj1, Object obj2, bool descending) { if (obj1 && obj2) { return obj1.name.CompareTo(obj2.name); } else if (obj1) { return descending ? 1 : -1; } return descending ? -1 : 1; } } // // -- SURROGATE -- // public struct Surrogate { public System.Type type; public bool exactType; public SurrogateCallback callback; internal bool enabled; public bool HasType { get { return enabled && type != null; } } public Surrogate(System.Type type) : this(type, null) { } public Surrogate(System.Type type, SurrogateCallback callback) { this.type = type; this.callback = callback; enabled = true; exactType = false; } public void Invoke(SerializedProperty element, Object objectReference, ReorderableList list) { if (element != null && callback != null) { callback.Invoke(element, objectReference, list); } } } // // -- EXCEPTIONS -- // class InvalidListException : System.InvalidOperationException { public InvalidListException() : base("ReorderableList serializedProperty must be an array") { } } class MissingListExeption : System.ArgumentNullException { public MissingListExeption() : base("ReorderableList serializedProperty is null") { } } // // -- INTERNAL -- // static class Internals { private static MethodInfo dragDropValidation; private static object[] dragDropValidationParams; private static MethodInfo appendDragDrop; private static object[] appendDragDropParams; static Internals() { dragDropValidation = System.Type.GetType("UnityEditor.EditorGUI, UnityEditor").GetMethod("ValidateObjectFieldAssignment", BindingFlags.NonPublic | BindingFlags.Static); appendDragDrop = typeof(SerializedProperty).GetMethod("AppendFoldoutPPtrValue", BindingFlags.NonPublic | BindingFlags.Instance); } internal static Object ValidateObjectDragAndDrop(Object[] references, SerializedProperty property, System.Type type, bool exactType) { #if UNITY_2017_1_OR_NEWER dragDropValidationParams = GetParams(ref dragDropValidationParams, 4); dragDropValidationParams[0] = references; dragDropValidationParams[1] = type; dragDropValidationParams[2] = property; dragDropValidationParams[3] = exactType ? 1 : 0; #else dragDropValidationParams = GetParams(ref dragDropValidationParams, 3); dragDropValidationParams[0] = references; dragDropValidationParams[1] = type; dragDropValidationParams[2] = property; #endif return dragDropValidation.Invoke(null, dragDropValidationParams) as Object; } internal static void AppendDragAndDropValue(Object obj, SerializedProperty list) { appendDragDropParams = GetParams(ref appendDragDropParams, 1); appendDragDropParams[0] = obj; appendDragDrop.Invoke(list, appendDragDropParams); } private static object[] GetParams(ref object[] parameters, int count) { if (parameters == null) { parameters = new object[count]; } return parameters; } } }