Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
UnityCsReference/Editor/Mono/GUI/ReorderableList.cs
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
885 lines (774 sloc)
38.2 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Unity C# reference source | |
// Copyright (c) Unity Technologies. For terms of use, see | |
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License | |
using UnityEngine; | |
using UnityEditor; | |
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using Object = UnityEngine.Object; | |
namespace UnityEditorInternal | |
{ | |
//TODO: better handling for serializedObjects with mixed values | |
//TODO: make it not rely on GUILayout at all, so its safe to use under PropertyDrawers. | |
public class ReorderableList | |
{ | |
public delegate void HeaderCallbackDelegate(Rect rect); | |
public delegate void FooterCallbackDelegate(Rect rect); | |
public delegate void ElementCallbackDelegate(Rect rect, int index, bool isActive, bool isFocused); | |
public delegate float ElementHeightCallbackDelegate(int index); | |
public delegate void DrawNoneElementCallback(Rect rect); | |
public delegate void ReorderCallbackDelegateWithDetails(ReorderableList list, int oldIndex, int newIndex); | |
public delegate void ReorderCallbackDelegate(ReorderableList list); | |
public delegate void SelectCallbackDelegate(ReorderableList list); | |
public delegate void AddCallbackDelegate(ReorderableList list); | |
public delegate void AddDropdownCallbackDelegate(Rect buttonRect, ReorderableList list); | |
public delegate void RemoveCallbackDelegate(ReorderableList list); | |
public delegate void ChangedCallbackDelegate(ReorderableList list); | |
public delegate bool CanRemoveCallbackDelegate(ReorderableList list); | |
public delegate bool CanAddCallbackDelegate(ReorderableList list); | |
public delegate void DragCallbackDelegate(ReorderableList list); | |
// draw callbacks | |
public HeaderCallbackDelegate drawHeaderCallback; | |
public FooterCallbackDelegate drawFooterCallback = null; | |
public ElementCallbackDelegate drawElementCallback; | |
public ElementCallbackDelegate drawElementBackgroundCallback; | |
public DrawNoneElementCallback drawNoneElementCallback = null; | |
// layout callbacks | |
// if supplying own element heights, try to cache the results as this may be called frequently | |
public ElementHeightCallbackDelegate elementHeightCallback; | |
// interaction callbacks | |
public ReorderCallbackDelegateWithDetails onReorderCallbackWithDetails; | |
public ReorderCallbackDelegate onReorderCallback; | |
public SelectCallbackDelegate onSelectCallback; | |
public AddCallbackDelegate onAddCallback; | |
public AddDropdownCallbackDelegate onAddDropdownCallback; | |
public RemoveCallbackDelegate onRemoveCallback; | |
public DragCallbackDelegate onMouseDragCallback; | |
public SelectCallbackDelegate onMouseUpCallback; | |
public CanRemoveCallbackDelegate onCanRemoveCallback; | |
public CanAddCallbackDelegate onCanAddCallback; | |
public ChangedCallbackDelegate onChangedCallback; | |
private int m_ActiveElement = -1; | |
private float m_DragOffset = 0; | |
private GUISlideGroup m_SlideGroup; | |
private SerializedObject m_SerializedObject; | |
private SerializedProperty m_Elements; | |
private IList m_ElementList; | |
private bool m_Draggable; | |
private float m_DraggedY; | |
private bool m_Dragging; | |
private List<int> m_NonDragTargetIndices; | |
private bool m_DisplayHeader; | |
public bool displayAdd; | |
public bool displayRemove; | |
private int id = -1; | |
// class for default rendering and behavior of reorderable list - stores styles and is statically available as s_Defaults | |
public class Defaults | |
{ | |
public GUIContent iconToolbarPlus = EditorGUIUtility.TrIconContent("Toolbar Plus", "Add to list"); | |
public GUIContent iconToolbarPlusMore = EditorGUIUtility.TrIconContent("Toolbar Plus More", "Choose to add to list"); | |
public GUIContent iconToolbarMinus = EditorGUIUtility.TrIconContent("Toolbar Minus", "Remove selection from list"); | |
public readonly GUIStyle draggingHandle = "RL DragHandle"; | |
public readonly GUIStyle headerBackground = "RL Header"; | |
private readonly GUIStyle emptyHeaderBackground = "RL Empty Header"; | |
public readonly GUIStyle footerBackground = "RL Footer"; | |
public readonly GUIStyle boxBackground = "RL Background"; | |
public readonly GUIStyle preButton = "RL FooterButton"; | |
public readonly GUIStyle elementBackground = "RL Element"; | |
public const int padding = 6; | |
public const int dragHandleWidth = 20; | |
private static GUIContent s_ListIsEmpty = EditorGUIUtility.TrTextContent("List is Empty"); | |
// draw the default footer | |
public void DrawFooter(Rect rect, ReorderableList list) | |
{ | |
float rightEdge = rect.xMax - 10f; | |
float leftEdge = rightEdge - 8f; | |
if (list.displayAdd) | |
leftEdge -= 25; | |
if (list.displayRemove) | |
leftEdge -= 25; | |
rect = new Rect(leftEdge, rect.y, rightEdge - leftEdge, rect.height); | |
Rect addRect = new Rect(leftEdge + 4, rect.y, 25, 16); | |
Rect removeRect = new Rect(rightEdge - 29, rect.y, 25, 16); | |
if (Event.current.type == EventType.Repaint) | |
{ | |
footerBackground.Draw(rect, false, false, false, false); | |
} | |
if (list.displayAdd) | |
{ | |
using (new EditorGUI.DisabledScope( | |
list.onCanAddCallback != null && !list.onCanAddCallback(list))) | |
{ | |
if (GUI.Button(addRect, list.onAddDropdownCallback != null ? iconToolbarPlusMore : iconToolbarPlus, preButton)) | |
{ | |
if (list.onAddDropdownCallback != null) | |
list.onAddDropdownCallback(addRect, list); | |
else if (list.onAddCallback != null) | |
list.onAddCallback(list); | |
else | |
DoAddButton(list); | |
if (list.onChangedCallback != null) | |
list.onChangedCallback(list); | |
} | |
} | |
} | |
if (list.displayRemove) | |
{ | |
using (new EditorGUI.DisabledScope( | |
list.index < 0 || list.index >= list.count || | |
(list.onCanRemoveCallback != null && !list.onCanRemoveCallback(list)))) | |
{ | |
if (GUI.Button(removeRect, iconToolbarMinus, preButton)) | |
{ | |
if (list.onRemoveCallback == null) | |
DoRemoveButton(list); | |
else | |
list.onRemoveCallback(list); | |
if (list.onChangedCallback != null) | |
list.onChangedCallback(list); | |
} | |
} | |
} | |
} | |
// default add button behavior | |
public void DoAddButton(ReorderableList list) | |
{ | |
if (list.serializedProperty != null) | |
{ | |
list.serializedProperty.arraySize += 1; | |
list.index = list.serializedProperty.arraySize - 1; | |
} | |
else | |
{ | |
// this is ugly but there are a lot of cases like null types and default constructors | |
Type elementType = list.list.GetType().GetElementType(); | |
if (elementType == typeof(string)) | |
list.index = list.list.Add(""); | |
else if (elementType != null && elementType.GetConstructor(Type.EmptyTypes) == null) | |
Debug.LogError("Cannot add element. Type " + elementType.ToString() + " has no default constructor. Implement a default constructor or implement your own add behaviour."); | |
else if (list.list.GetType().GetGenericArguments()[0] != null) | |
list.index = list.list.Add(Activator.CreateInstance(list.list.GetType().GetGenericArguments()[0])); | |
else if (elementType != null) | |
list.index = list.list.Add(Activator.CreateInstance(elementType)); | |
else | |
Debug.LogError("Cannot add element of type Null."); | |
} | |
} | |
// default remove button behavior | |
public void DoRemoveButton(ReorderableList list) | |
{ | |
if (list.serializedProperty != null) | |
{ | |
list.serializedProperty.DeleteArrayElementAtIndex(list.index); | |
if (list.index >= list.serializedProperty.arraySize - 1) | |
list.index = list.serializedProperty.arraySize - 1; | |
} | |
else | |
{ | |
list.list.RemoveAt(list.index); | |
if (list.index >= list.list.Count - 1) | |
list.index = list.list.Count - 1; | |
} | |
} | |
// draw the default header background | |
public void DrawHeaderBackground(Rect headerRect) | |
{ | |
if (Event.current.type == EventType.Repaint) | |
{ | |
// We assume that a height smaller than 5px means a header with no content | |
if (headerRect.height < 5f) | |
{ | |
emptyHeaderBackground.Draw(headerRect, false, false, false, false); | |
} | |
else | |
{ | |
headerBackground.Draw(headerRect, false, false, false, false); | |
} | |
} | |
} | |
// draw the default header | |
public void DrawHeader(Rect headerRect, SerializedObject serializedObject, SerializedProperty element, IList elementList) | |
{ | |
EditorGUI.LabelField(headerRect, EditorGUIUtility.TempContent((element != null) ? "Serialized Property" : "IList")); | |
} | |
// draw the default element background | |
public void DrawElementBackground(Rect rect, int index, bool selected, bool focused, bool draggable) | |
{ | |
if (Event.current.type == EventType.Repaint) | |
{ | |
elementBackground.Draw(rect, false, selected, selected, focused); | |
} | |
} | |
public void DrawElementDraggingHandle(Rect rect, int index, bool selected, bool focused, bool draggable) | |
{ | |
if (Event.current.type == EventType.Repaint) | |
{ | |
if (draggable) | |
draggingHandle.Draw(new Rect(rect.x + 5, rect.y + 8, 10, 6), false, false, false, false); | |
} | |
} | |
// draw the default element | |
public void DrawElement(Rect rect, SerializedProperty element, System.Object listItem, bool selected, bool focused, bool draggable) | |
{ | |
EditorGUI.LabelField(rect, EditorGUIUtility.TempContent((element != null) ? element.displayName : listItem.ToString())); | |
} | |
// draw the default element | |
public void DrawNoneElement(Rect rect, bool draggable) | |
{ | |
EditorGUI.LabelField(rect, Defaults.s_ListIsEmpty); | |
} | |
} | |
static Defaults s_Defaults; | |
public static Defaults defaultBehaviours | |
{ | |
get | |
{ | |
return s_Defaults; | |
} | |
} | |
// constructors | |
public ReorderableList(IList elements, Type elementType) | |
{ | |
InitList(null, null, elements, true, true, true, true); | |
} | |
public ReorderableList(IList elements, Type elementType, bool draggable, bool displayHeader, bool displayAddButton, bool displayRemoveButton) | |
{ | |
InitList(null, null, elements, draggable, displayHeader, displayAddButton, displayRemoveButton); | |
} | |
public ReorderableList(SerializedObject serializedObject, SerializedProperty elements) | |
{ | |
InitList(serializedObject, elements, null, true, true, true, true); | |
} | |
public ReorderableList(SerializedObject serializedObject, SerializedProperty elements, bool draggable, bool displayHeader, bool displayAddButton, bool displayRemoveButton) | |
{ | |
InitList(serializedObject, elements, null, draggable, displayHeader, displayAddButton, displayRemoveButton); | |
} | |
private void InitList(SerializedObject serializedObject, SerializedProperty elements, IList elementList, bool draggable, bool displayHeader, bool displayAddButton, bool displayRemoveButton) | |
{ | |
id = GUIUtility.GetPermanentControlID(); | |
m_SerializedObject = serializedObject; | |
m_Elements = elements; | |
m_ElementList = elementList; | |
m_Draggable = draggable; | |
m_Dragging = false; | |
m_SlideGroup = new GUISlideGroup(); | |
displayAdd = displayAddButton; | |
m_DisplayHeader = displayHeader; | |
displayRemove = displayRemoveButton; | |
if (m_Elements != null && m_Elements.editable == false) | |
m_Draggable = false; | |
if (m_Elements != null && m_Elements.isArray == false) | |
Debug.LogError("Input elements should be an Array SerializedProperty"); | |
} | |
public SerializedProperty serializedProperty | |
{ | |
get { return m_Elements; } | |
set { m_Elements = value; } | |
} | |
public IList list | |
{ | |
get { return m_ElementList; } | |
set { m_ElementList = value; } | |
} | |
// active element index accessor | |
public int index | |
{ | |
get { return m_ActiveElement; } | |
set { m_ActiveElement = value; } | |
} | |
// individual element height accessor | |
public float elementHeight = 21; | |
// header height accessor | |
public float headerHeight = 20; | |
// footer height accessor | |
public float footerHeight = 20; | |
// show default background | |
public bool showDefaultBackground = true; | |
private float listElementTopPadding => headerHeight > 5 ? 4 : 1; // headerHeight is usually set to 3px when there is no header content. Therefore, we add a 1px top margin to match the 4px bottom margin | |
private const float kListElementBottomPadding = 4; | |
// draggable accessor | |
public bool draggable | |
{ | |
get { return m_Draggable; } | |
set { m_Draggable = value; } | |
} | |
private Rect GetContentRect(Rect rect) | |
{ | |
Rect r = rect; | |
if (draggable) | |
r.xMin += Defaults.dragHandleWidth; | |
else | |
r.xMin += Defaults.padding; | |
r.xMax -= Defaults.padding; | |
return r; | |
} | |
private float GetElementYOffset(int index) | |
{ | |
return GetElementYOffset(index, -1); | |
} | |
private float GetElementYOffset(int index, int skipIndex) | |
{ | |
if (elementHeightCallback == null) | |
return index * elementHeight; | |
float offset = 0; | |
for (int i = 0; i < index; i++) | |
{ | |
if (i != skipIndex) | |
offset += elementHeightCallback(i); | |
} | |
return offset; | |
} | |
private float GetElementHeight(int index) | |
{ | |
if (elementHeightCallback == null) | |
return elementHeight; | |
return elementHeightCallback(index); | |
} | |
private Rect GetRowRect(int index, Rect listRect) | |
{ | |
return new Rect(listRect.x, listRect.y + GetElementYOffset(index), listRect.width, GetElementHeight(index)); | |
} | |
public int count | |
{ | |
get | |
{ | |
if (m_Elements != null) | |
{ | |
if (!m_Elements.hasMultipleDifferentValues) | |
return m_Elements.arraySize; | |
int smallerArraySize = m_Elements.arraySize; | |
foreach (Object targetObject in m_Elements.serializedObject.targetObjects) | |
{ | |
SerializedObject serializedObject = new SerializedObject(targetObject); | |
SerializedProperty property = serializedObject.FindProperty(m_Elements.propertyPath); | |
smallerArraySize = Math.Min(property.arraySize, smallerArraySize); | |
} | |
return smallerArraySize; | |
} | |
return m_ElementList.Count; | |
} | |
} | |
public void DoLayoutList() //TODO: better API? | |
{ | |
if (s_Defaults == null) | |
s_Defaults = new Defaults(); | |
// do the custom or default header GUI | |
Rect headerRect = GUILayoutUtility.GetRect(0, headerHeight, GUILayout.ExpandWidth(true)); | |
//Elements area | |
Rect listRect = GUILayoutUtility.GetRect(10, GetListElementHeight(), GUILayout.ExpandWidth(true)); | |
// do the footer GUI | |
Rect footerRect = GUILayoutUtility.GetRect(4, footerHeight, GUILayout.ExpandWidth(true)); | |
// do the parts of our list | |
DoListHeader(headerRect); | |
DoListElements(listRect); | |
DoListFooter(footerRect); | |
} | |
public void DoList(Rect rect) //TODO: better API? | |
{ | |
if (s_Defaults == null) | |
s_Defaults = new Defaults(); | |
// do the custom or default header GUI | |
Rect headerRect = new Rect(rect.x, rect.y, rect.width, headerHeight); | |
//Elements area | |
Rect listRect = new Rect(rect.x, headerRect.y + headerRect.height, rect.width, GetListElementHeight()); | |
// do the footer GUI | |
Rect footerRect = new Rect(rect.x, listRect.y + listRect.height, rect.width, footerHeight); | |
// do the parts of our list | |
DoListHeader(headerRect); | |
DoListElements(listRect); | |
DoListFooter(footerRect); | |
} | |
public float GetHeight() | |
{ | |
float totalheight = 0f; | |
totalheight += GetListElementHeight(); | |
totalheight += headerHeight; | |
totalheight += footerHeight; | |
return totalheight; | |
} | |
private float GetListElementHeight() | |
{ | |
float listElementPadding = kListElementBottomPadding + listElementTopPadding; | |
int arraySize = count; | |
if (arraySize == 0) | |
{ | |
return elementHeight + listElementPadding; | |
} | |
if (elementHeightCallback != null) | |
{ | |
return GetElementYOffset(arraySize - 1) + GetElementHeight(arraySize - 1) + listElementPadding; | |
} | |
return (elementHeight * arraySize) + listElementPadding; | |
} | |
private void DoListElements(Rect listRect) | |
{ | |
// How many elements? If none, make space for showing default line that shows no elements are present | |
int arraySize = count; | |
// draw the background in repaint | |
if (showDefaultBackground && Event.current.type == EventType.Repaint) | |
s_Defaults.boxBackground.Draw(listRect, false, false, false, false); | |
// resize to the area that we want to draw our elements into | |
listRect.yMin += listElementTopPadding; listRect.yMax -= kListElementBottomPadding; | |
if (showDefaultBackground) | |
{ | |
listRect.xMin += 1; | |
listRect.xMax -= 1; | |
} | |
// create the rect for individual elements in the list | |
Rect elementRect = listRect; | |
elementRect.height = elementHeight; | |
// the content rect is what we will actually draw into -- it doesn't include the drag handle or padding | |
Rect elementContentRect = elementRect; | |
if (((m_Elements != null && m_Elements.isArray == true) || m_ElementList != null) && arraySize > 0) | |
{ | |
// If there are elements, we need to draw them -- we will do this differently depending on if we are dragging or not | |
if (IsDragging() && Event.current.type == EventType.Repaint) | |
{ | |
// we are dragging, so we need to build the new list of target indices | |
int targetIndex = CalculateRowIndex(); | |
m_NonDragTargetIndices.Clear(); | |
for (int i = 0; i < arraySize; i++) | |
{ | |
if (i != m_ActiveElement) | |
m_NonDragTargetIndices.Add(i); | |
} | |
m_NonDragTargetIndices.Insert(targetIndex, -1); | |
// now draw each element in the list (excluding the active element) | |
bool targetSeen = false; | |
for (int i = 0; i < m_NonDragTargetIndices.Count; i++) | |
{ | |
if (m_NonDragTargetIndices[i] != -1) | |
{ | |
elementRect.height = GetElementHeight(i); | |
// update the position of the rect (based on element position and accounting for sliding) | |
if (elementHeightCallback == null) | |
{ | |
elementRect.y = listRect.y + GetElementYOffset(i, m_ActiveElement); | |
} | |
else | |
{ | |
elementRect.y = listRect.y + GetElementYOffset(m_NonDragTargetIndices[i], m_ActiveElement); | |
if (targetSeen) | |
{ | |
elementRect.y += elementHeightCallback(m_ActiveElement); | |
} | |
} | |
elementRect = m_SlideGroup.GetRect(m_NonDragTargetIndices[i], elementRect); | |
// actually draw the element | |
if (drawElementBackgroundCallback == null) | |
s_Defaults.DrawElementBackground(elementRect, i, false, false, m_Draggable); | |
else | |
drawElementBackgroundCallback(elementRect, i, false, false); | |
s_Defaults.DrawElementDraggingHandle(elementRect, i, false, false, m_Draggable); | |
elementContentRect = GetContentRect(elementRect); | |
if (drawElementCallback == null) | |
{ | |
if (m_Elements != null) | |
s_Defaults.DrawElement(elementContentRect, m_Elements.GetArrayElementAtIndex(m_NonDragTargetIndices[i]), null, false, false, m_Draggable); | |
else | |
s_Defaults.DrawElement(elementContentRect, null, m_ElementList[m_NonDragTargetIndices[i]], false, false, m_Draggable); | |
} | |
else | |
{ | |
drawElementCallback(elementContentRect, m_NonDragTargetIndices[i], false, false); | |
} | |
} | |
else | |
{ | |
targetSeen = true; | |
} | |
} | |
// finally get the position of the active element | |
elementRect.y = m_DraggedY - m_DragOffset + listRect.y; | |
// actually draw the element | |
if (drawElementBackgroundCallback == null) | |
s_Defaults.DrawElementBackground(elementRect, m_ActiveElement, true, true, m_Draggable); | |
else | |
drawElementBackgroundCallback(elementRect, m_ActiveElement, true, true); | |
s_Defaults.DrawElementDraggingHandle(elementRect, m_ActiveElement, true, true, m_Draggable); | |
elementContentRect = GetContentRect(elementRect); | |
// draw the active element | |
if (drawElementCallback == null) | |
{ | |
if (m_Elements != null) | |
s_Defaults.DrawElement(elementContentRect, m_Elements.GetArrayElementAtIndex(m_ActiveElement), null, true, true, m_Draggable); | |
else | |
s_Defaults.DrawElement(elementContentRect, null, m_ElementList[m_ActiveElement], true, true, m_Draggable); | |
} | |
else | |
{ | |
drawElementCallback(elementContentRect, m_ActiveElement, true, true); | |
} | |
} | |
else | |
{ | |
// if we aren't dragging, we just draw all of the elements in order | |
for (int i = 0; i < arraySize; i++) | |
{ | |
bool activeElement = (i == m_ActiveElement); | |
bool focusedElement = (i == m_ActiveElement && HasKeyboardControl()); | |
// update the position of the element | |
elementRect.height = GetElementHeight(i); | |
elementRect.y = listRect.y + GetElementYOffset(i); | |
// draw the background | |
if (drawElementBackgroundCallback == null) | |
s_Defaults.DrawElementBackground(elementRect, i, activeElement, focusedElement, m_Draggable); | |
else | |
drawElementBackgroundCallback(elementRect, i, activeElement, focusedElement); | |
s_Defaults.DrawElementDraggingHandle(elementRect, i, activeElement, focusedElement, m_Draggable); | |
elementContentRect = GetContentRect(elementRect); | |
// do the callback for the element | |
if (drawElementCallback == null) | |
{ | |
if (m_Elements != null) | |
s_Defaults.DrawElement(elementContentRect, m_Elements.GetArrayElementAtIndex(i), null, activeElement, focusedElement, m_Draggable); | |
else | |
s_Defaults.DrawElement(elementContentRect, null, m_ElementList[i], activeElement, focusedElement, m_Draggable); | |
} | |
else | |
{ | |
drawElementCallback(elementContentRect, i, activeElement, focusedElement); | |
} | |
} | |
} | |
// handle the interaction | |
DoDraggingAndSelection(listRect); | |
} | |
else | |
{ | |
// there was no content, so we will draw an empty element | |
elementRect.y = listRect.y; | |
// draw the background | |
if (drawElementBackgroundCallback == null) | |
s_Defaults.DrawElementBackground(elementRect, -1, false, false, false); | |
else | |
drawElementBackgroundCallback(elementRect, -1, false, false); | |
s_Defaults.DrawElementDraggingHandle(elementRect, -1, false, false, false); | |
elementContentRect = elementRect; | |
elementContentRect.xMin += Defaults.padding; | |
elementContentRect.xMax -= Defaults.padding; | |
if (drawNoneElementCallback == null) | |
s_Defaults.DrawNoneElement(elementContentRect, m_Draggable); | |
else | |
drawNoneElementCallback(elementContentRect); | |
} | |
} | |
private void DoListHeader(Rect headerRect) | |
{ | |
// draw the background on repaint | |
if (showDefaultBackground && Event.current.type == EventType.Repaint) | |
s_Defaults.DrawHeaderBackground(headerRect); | |
// apply the padding to get the internal rect | |
headerRect.xMin += Defaults.padding; | |
headerRect.xMax -= Defaults.padding; | |
headerRect.height -= 2; | |
headerRect.y += 1; | |
// perform the default or overridden callback | |
if (drawHeaderCallback != null) | |
drawHeaderCallback(headerRect); | |
else if (m_DisplayHeader) | |
s_Defaults.DrawHeader(headerRect, m_SerializedObject, m_Elements, m_ElementList); | |
} | |
private void DoListFooter(Rect footerRect) | |
{ | |
// perform callback or the default footer | |
if (drawFooterCallback != null) | |
drawFooterCallback(footerRect); | |
else if (displayAdd || displayRemove) | |
s_Defaults.DrawFooter(footerRect, this); // draw the footer if the add or remove buttons are required | |
} | |
private void DoDraggingAndSelection(Rect listRect) | |
{ | |
Event evt = Event.current; | |
int oldIndex = m_ActiveElement; | |
bool clicked = false; | |
switch (evt.GetTypeForControl(id)) | |
{ | |
case EventType.KeyDown: | |
if (GUIUtility.keyboardControl != id) | |
return; | |
// if we have keyboard focus, arrow through the list | |
if (evt.keyCode == KeyCode.DownArrow) | |
{ | |
m_ActiveElement += 1; | |
evt.Use(); | |
} | |
if (evt.keyCode == KeyCode.UpArrow) | |
{ | |
m_ActiveElement -= 1; | |
evt.Use(); | |
} | |
if (evt.keyCode == KeyCode.Escape && GUIUtility.hotControl == id) | |
{ | |
GUIUtility.hotControl = 0; | |
m_Dragging = false; | |
evt.Use(); | |
} | |
// don't allow arrowing through the ends of the list | |
m_ActiveElement = Mathf.Clamp(m_ActiveElement, 0, (m_Elements != null) ? m_Elements.arraySize - 1 : m_ElementList.Count - 1); | |
break; | |
case EventType.MouseDown: | |
if (!listRect.Contains(Event.current.mousePosition) || (Event.current.button != 0 && Event.current.button != 1)) | |
break; | |
// clicking on the list should end editing any existing edits | |
EditorGUI.EndEditingActiveTextField(); | |
// pick the active element based on click position | |
m_ActiveElement = GetRowIndex(Event.current.mousePosition.y - listRect.y); | |
if (m_Draggable && Event.current.button == 0) | |
{ | |
// if we can drag, set the hot control and start dragging (storing the offset) | |
m_DragOffset = (Event.current.mousePosition.y - listRect.y) - GetElementYOffset(m_ActiveElement); | |
UpdateDraggedY(listRect); | |
GUIUtility.hotControl = id; | |
m_SlideGroup.Reset(); | |
m_NonDragTargetIndices = new List<int>(); | |
} | |
GrabKeyboardFocus(); | |
// Prevent consuming the right mouse event in order to enable the context menu | |
if (Event.current.button == 1) | |
break; | |
evt.Use(); | |
clicked = true; | |
break; | |
case EventType.MouseDrag: | |
if (!m_Draggable || GUIUtility.hotControl != id) | |
break; | |
// Set m_Dragging state on first MouseDrag event after we got hotcontrol (to prevent animating elements when deleting elements by context menu) | |
m_Dragging = true; | |
if (onMouseDragCallback != null) | |
onMouseDragCallback(this); | |
// if we are dragging, update the position | |
UpdateDraggedY(listRect); | |
evt.Use(); | |
break; | |
case EventType.MouseUp: | |
if (!m_Draggable) | |
{ | |
// if mouse up was on the same index as mouse down we fire a mouse up callback (useful if for beginning renaming on mouseup) | |
if (onMouseUpCallback != null && IsMouseInsideActiveElement(listRect)) | |
{ | |
// set the keyboard control | |
onMouseUpCallback(this); | |
} | |
break; | |
} | |
// hotcontrol is only set when list is draggable | |
if (GUIUtility.hotControl != id) | |
break; | |
evt.Use(); | |
m_Dragging = false; | |
try | |
{ | |
// What will be the index of this if we release? | |
int targetIndex = CalculateRowIndex(); | |
if (m_ActiveElement != targetIndex) | |
{ | |
// if the target index is different than the current index... | |
if (m_SerializedObject != null && m_Elements != null) | |
{ | |
// if we are working with Serialized Properties, we can handle it for you | |
m_Elements.MoveArrayElement(m_ActiveElement, targetIndex); | |
m_SerializedObject.ApplyModifiedProperties(); | |
m_SerializedObject.Update(); | |
} | |
else if (m_ElementList != null) | |
{ | |
// we are working with the IList, which is probably of a fixed length | |
System.Object tempObject = m_ElementList[m_ActiveElement]; | |
for (int i = 0; i < m_ElementList.Count - 1; i++) | |
{ | |
if (i >= m_ActiveElement) | |
m_ElementList[i] = m_ElementList[i + 1]; | |
} | |
for (int i = m_ElementList.Count - 1; i > 0; i--) | |
{ | |
if (i > targetIndex) | |
m_ElementList[i] = m_ElementList[i - 1]; | |
} | |
m_ElementList[targetIndex] = tempObject; | |
} | |
var oldActiveElement = m_ActiveElement; | |
var newActiveElement = targetIndex; | |
// update the active element, now that we've moved it | |
m_ActiveElement = targetIndex; | |
// give the user a callback | |
if (onReorderCallbackWithDetails != null) | |
onReorderCallbackWithDetails(this, oldActiveElement, newActiveElement); | |
else if (onReorderCallback != null) | |
onReorderCallback(this); | |
if (onChangedCallback != null) | |
onChangedCallback(this); | |
} | |
else | |
{ | |
// if mouse up was on the same index as mouse down we fire a mouse up callback (useful if for beginning renaming on mouseup) | |
if (onMouseUpCallback != null) | |
onMouseUpCallback(this); | |
} | |
} | |
finally | |
{ | |
// It's quite possible a call to EndGUI was made in one of our callbacks | |
// (and thus an ExitGUIException thrown). We still need to cleanup before | |
// we exitGUI proper. | |
GUIUtility.hotControl = 0; | |
m_NonDragTargetIndices = null; | |
} | |
break; | |
} | |
// if the index has changed and there is a selected callback, call it | |
if ((m_ActiveElement != oldIndex || clicked) && onSelectCallback != null) | |
onSelectCallback(this); | |
} | |
bool IsMouseInsideActiveElement(Rect listRect) | |
{ | |
int mouseRowIndex = GetRowIndex(Event.current.mousePosition.y - listRect.y); | |
return mouseRowIndex == m_ActiveElement && GetRowRect(mouseRowIndex, listRect).Contains(Event.current.mousePosition); | |
} | |
private void UpdateDraggedY(Rect listRect) | |
{ | |
m_DraggedY = Mathf.Clamp(Event.current.mousePosition.y - listRect.y, m_DragOffset, listRect.height - (GetElementHeight(m_ActiveElement) - m_DragOffset)); | |
} | |
private int CalculateRowIndex() | |
{ | |
return GetRowIndex(m_DraggedY); | |
} | |
private int GetRowIndex(float localY) | |
{ | |
if (elementHeightCallback == null) | |
return Mathf.Clamp(Mathf.FloorToInt(localY / elementHeight), 0, count - 1); | |
float rowYOffset = 0; | |
for (int i = 0; i < count; i++) | |
{ | |
float rowYHeight = elementHeightCallback(i); | |
float rowYEnd = rowYOffset + rowYHeight; | |
if (localY >= rowYOffset && localY < rowYEnd) | |
{ | |
return i; | |
} | |
rowYOffset += rowYHeight; | |
} | |
return count - 1; | |
} | |
private bool IsDragging() | |
{ | |
return m_Dragging; | |
} | |
public void GrabKeyboardFocus() | |
{ | |
GUIUtility.keyboardControl = id; | |
} | |
public void ReleaseKeyboardFocus() | |
{ | |
if (GUIUtility.keyboardControl == id) | |
{ | |
GUIUtility.keyboardControl = 0; | |
} | |
} | |
public bool HasKeyboardControl() | |
{ | |
return GUIUtility.keyboardControl == id && EditorGUIUtility.HasCurrentWindowKeyFocus(); | |
} | |
} | |
} |