您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
3097 行
63 KiB
3097 行
63 KiB
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>(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<DragElement> orderedList = new List<DragElement>(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<int, bool> 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<int, bool>();
|
|
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<DragElement, SerializedProperty, int> 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<int, Rect> animIDs;
|
|
|
|
public SlideGroup()
|
|
{
|
|
|
|
animIDs = new Dictionary<int, Rect>();
|
|
}
|
|
|
|
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<int>
|
|
{
|
|
|
|
private List<int> indexes;
|
|
|
|
internal int? firstSelected;
|
|
|
|
public ListSelection()
|
|
{
|
|
|
|
indexes = new List<int>();
|
|
}
|
|
|
|
public ListSelection(int[] indexes)
|
|
{
|
|
|
|
this.indexes = new List<int>(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<int> 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<int> GetEnumerator()
|
|
{
|
|
|
|
return ((IEnumerable<int>)indexes).GetEnumerator();
|
|
}
|
|
|
|
IEnumerator IEnumerable.GetEnumerator()
|
|
{
|
|
|
|
return ((IEnumerable<int>)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;
|
|
}
|
|
}
|
|
}
|