Search Unity

  1. If you have experience with import & exporting custom (.unitypackage) packages, please help complete a survey (open until May 15, 2024).
    Dismiss Notice
  2. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice

Drag and Drop in the editor - Explanation

Discussion in 'Scripting' started by RC-1290, Jan 18, 2014.

  1. RC-1290

    RC-1290

    Joined:
    Jul 2, 2012
    Posts:
    639
    Drag and Drop Support
    For custom editor code.
    This week I created a custom property drawer for lists in Unity, because I use quite a few lists, which all require occasional reordering. For adding objects and reordering them, I decided to take a look at Drag and Drop behavior in the editor. It turns out that most of the resources I could find were outdated, so I thought it would be a good idea to write an overview of what you need to do to make it work in Unity 4.3.

    Used Resources

    Example Code
    Let's just start with a stripped down example of the dragAndDrop code I use. ExampledDragDropGUI is a method that could be called from an OnGUI method. The example was written for a custom PropertyDrawer. It's a bit simplified, and I haven't included the code for all the implementation dependent methods, so you do still need to customize it to make it work with your specific editor code. But the basics should be very similar.

    Code (csharp):
    1.  
    2. protected void ExampleDragDropGUI(Rect dropArea, SerializedProperty property){
    3.     // Cache References:
    4.     Event currentEvent = Event.current;
    5.     EventType currentEventType = currentEvent.type;
    6.    
    7.     // The DragExited event does not have the same mouse position data as the other events,
    8.     // so it must be checked now:
    9.     if ( currentEventType == EventType.DragExited ) DragAndDrop.PrepareStartDrag();// Clear generic data when user pressed escape. (Unfortunately, DragExited is also called when the mouse leaves the drag area)
    10.  
    11.     if (!dropArea.Contains(currentEvent.mousePosition))    return;
    12.  
    13.     switch (currentEventType){
    14.     case EventType.MouseDown:
    15.         DragAndDrop.PrepareStartDrag();// reset data
    16.        
    17.         CustomDragData dragData = new CustomDragData();
    18.         dragData.originalIndex = somethingYouGotFromYourProperty;
    19.         dragData.originalList = this.targetList;
    20.        
    21.         DragAndDrop.SetGenericData(dragDropIdentifier, dragData);
    22.        
    23.         Object[] objectReferences = new Object[1]{property.objectReferenceValue};// Careful, null values cause exceptions in existing editor code.
    24.         DragAndDrop.objectReferences = objectReferences;// Note: this object won't be 'get'-able until the next GUI event.
    25.        
    26.         currentEvent.Use();
    27.        
    28.         break;
    29.     case EventType.MouseDrag:
    30.         // If drag was started here:
    31.         CustomDragData existingDragData = DragAndDrop.GetGenericData(dragDropIdentifier) as CustomDragData;
    32.        
    33.         if (existingDragData != null){
    34.             DragAndDrop.StartDrag("Dragging List ELement");
    35.             currentEvent.Use();
    36.         }
    37.        
    38.         break;
    39.     case EventType.DragUpdated:
    40.         if (IsDragTargetValid()) DragAndDrop.visualMode = DragAndDropVisualMode.Link;
    41.         else DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
    42.        
    43.         currentEvent.Use();
    44.         break;      
    45.     case EventType.Repaint:
    46.         if (
    47.         DragAndDrop.visualMode == DragAndDropVisualMode.None||
    48.         DragAndDrop.visualMode == DragAndDropVisualMode.Rejected) break;
    49.        
    50.         EditorGUI.DrawRect(dropArea, Color.grey);      
    51.         break;
    52.     case EventType.DragPerform:
    53.         DragAndDrop.AcceptDrag();
    54.        
    55.         CustomDragData receivedDragData = DragAndDrop.GetGenericData(dragDropIdentifier) as CustomDragData;
    56.        
    57.         if (receivedDragData != null  receivedDragData.originalList == this.targetList) ReorderObject();
    58.         else AddDraggedObjectsToList();
    59.        
    60.         currentEvent.Use();
    61.         break;
    62.     case EventType.MouseUp:
    63.         // Clean up, in case MouseDrag never occurred:
    64.         DragAndDrop.PrepareStartDrag();
    65.         break;
    66.     }
    67.    
    68. }
    69.  
    Added in the same file (there's no need to create a separate file for this)
    Code (csharp):
    1.  
    2. public class CustomDragData{
    3.     public int originalIndex;
    4.     public IList originalList;
    5. }
    6.  
    Details
    If you want to make your own drag and drop behavior work with existing Editor code, you might want to know a bit more about how this all works. So I'll try to break it down.

    Setup
    Code (csharp):
    1. Event currentEvent = Event.current;
    2. EventType currentEventType = currentEvent.type;
    3.  
    4. if ( currentEventType == EventType.DragExited ) DragAndDrop.PrepareStartDrag();
    5.  
    6. if (!dropArea.Contains(currentEvent.mousePosition))    return;
    First we cache references to event objects. In this example it's not really necessary, but in my production code, both values are used multiple times for certain controls. If the mouse is outside our 'drop area', we simply do nothing.
    [edit] We need to check if a previous drag operation was cancelled first, otherwise using the escape key won't cancel a drag operation that was started by this script. Unfortunately, the DragExited event uses a very different mouse position than all other drag events.

    MouseDown Event
    Code (csharp):
    1. case EventType.MouseDown:
    2.     DragAndDrop.PrepareStartDrag();// reset data
    3.  
    PrepareStartDrag removes all data from previous drag attempts. In older versions of Unity, you had to manually reset the object references and path references.


    Code (csharp):
    1.  
    2.     CustomDragData dragData = new CustomDragData();
    3.     dragData.originalIndex = somethingYouGotFromYourProperty;
    4.     dragData.originalList = this.targetList;
    5.    
    6.     DragAndDrop.SetGenericData(dragDropIdentifier, dragData);
    7.  
    I use the custom DragData class to store where the object was dragged from, for multiple reasons.
    The first reason is that you only need to start the actual drag, when the mouse is dragged, if this is the class that started the drag to begin with. In previous examples, data was setup and drag was started every frame of the drag. In my case, getting the object references could be expensive, so I decided to split the behavior.

    The second reason is that it allows you to have different behavior for objects dragged from other parts of the Unity interface, than for the ones dragged from the currently running interface. In the case of my list drawer, dragging from the current list only reorders the list, while dragging from elsewhere will add the dragged object to the list.

    To store, the extra data, you can use SetGenericData, which you can use to store any object you want. I added the CustomDragData class to the end of the file, which simply holds public references. This object is then stored in a hash table in DragAndDrop, which you can later access using GetGenericData.
    The object does not need to extend MonoBehavior or ScriptableObject, since you don't need to serialize the data. But I don't think it can be a struct. (Feel free to correct me if I'm wrong)
    The 'type' string parameter seems to simply be the 'key' used in the internal hashtable. I used a unique value, based on the full name of the class, including the namespace. That way I could be pretty certain in later steps that the drag was initiated by an instance of this class.


    Code (csharp):
    1.    
    2.     Object[] objectReferences = new Object[1]{property.objectReferenceValue};// Careful, null values cause exceptions in existing editor code.
    3.     DragAndDrop.objectReferences = objectReferences;// Note: this object won't be 'get'-able until the next GUI event.
    4.  
    The 'paths' and 'objectReferences' are used in the rest of the editor to respond to drag and drop behavior. So for example, if you add a GameObject to the objectReferences field, you can set parenting of that GameObject when you drag your mouse over the hierarchy Window.
    One thing that might be a bit confusing is that if you try to 'get' the value of the objectReferences field right after you set it, you won't get the value you just assigned. But if you try again in later events, you won't have that problem. I'm not sure why this is, but I can imagine it helps in a situation where multiple windows are trying to change and access the drag information.

    [edit] Also, this code doesn't check if property.objectReferenceValue is null, but it should. If dragAndDrop.objectReferences contains null references, some standard editor code in Unity will throw exceptions. When your references are null, it's better to use an empty objectReferences array instead.

    MouseDrag Event
    Code (csharp):
    1. CustomDragData existingDragData = DragAndDrop.GetGenericData(dragDropIdentifier) as CustomDragData;
    2.        
    3. if (existingDragData != null){
    4.     DragAndDrop.StartDrag("Dragging List ELement");
    5.     currentEvent.Use();
    6. }
    In the MouseDrag event you can check for the data set in the MouseDown event to figure out if the drag should be started from this class.
    I don know what the 'title' string parameter of StartDrag is used for. I thought it might be used for default undo behavior, but so far I haven't seen the string pop up anywhere.

    DragUpdated Event
    Code (csharp):
    1.  
    2. if (IsDragTargetValid()) DragAndDrop.visualMode = DragAndDropVisualMode.Link;
    3. else DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
    In DragUpdated you can check if any of the dragged objects (it is possible to drag multiple at the same time, although it tends to involve locking an inspector window) are valid for this drop location. The IsDragTargetValid method I used simply loops through the DragAndDrop.objectReferences list, and checks if any of them can be assigned to the type I'm looking for. This feels a bit expensive, because it uses reflection every time the drag is updated, but it didn't cause any noticeable problems on my machine. It only happens when the user is focussed on dragging anyway, and I'm pretty sure the Unity Editor itself does this too.
    If you decide that the drag operation is possible, you need to set the visualmode. Like the name suggests, this creates an icon at the cursor position that tells the user what will happen. But it is important to note that dragging will not occur if the visual mode is set to None or Rejected, so you HAVE to set it.

    In Unity 4.3 the drag icon tends to be a bit glitchy. This happens all over the editor. I assume this happens when multiple propertyDrawers try to change the visualMode.

    Repaint Event
    Code (csharp):
    1. if (
    2.     DragAndDrop.visualMode == DragAndDropVisualMode.None||
    3.     DragAndDrop.visualMode == DragAndDropVisualMode.Rejected) break;
    4. EditorGUI.DrawRect(dropArea, Color.grey);
    In the case of a list, it's useful to show the user where exactly the object will be dropped. You can use the Repaint event to draw some kind of graphic. Of course, there's no need to draw this graphic if the dragging will be rejected. Luckily, you don't have to perform this check again, you can simply base the decision to draw or not, on the value of visualMode, which was set in previous events.
    It's important to note that you probably don't want to call 'Use()' on the repaint event. Calling 'Use()' like during the other events, will prevent any of the following properties from being drawn.

    DragPerform Event
    Code (csharp):
    1. DragAndDrop.AcceptDrag();
    2.        
    3. CustomDragData receivedDragData = DragAndDrop.GetGenericData(dragDropIdentifier) as CustomDragData;
    4.        
    5. if (receivedDragData != null  receivedDragData.originalList == this.targetList) ReorderObject();
    6. else AddDraggedObjectsToList();
    The DragPerform event shows up when you release the mouse with the visualmode set to anything other than None or Rejected. You can accept the drop and start processing the objects and data related to it. In my list drawer, I reorder the list, in stead of adding to it, when the object originates from the same list.

    MouseUp Event
    Code (csharp):
    1. DragAndDrop.PrepareStartDrag();
    Lastly, in the MouseUp event, we handle the situation where the mouse button was released without dragging. In that case, we simply need to reset the drag.


    Good Luck!
    Thanks for reading this far! Hopefully that gave you all the information you needed to make your awesome custom editor. Please let me know if you have any questions or suggestions. And I'd also love to hear from you if you've made something using this information.

    - RC-1290 / Laurens Mathot
     
    Last edited: Aug 2, 2017
  2. BrUnO-XaVIeR

    BrUnO-XaVIeR

    Joined:
    Dec 6, 2010
    Posts:
    1,687
    I'm having trouble to workaround the drag operation been started every frame which causes my method to always drag the last object of the list no matter what. On a tool I'm working on I draw many GUILayout objects in a foreach loop and it was breaking drag&drop.
    Will try your guide, hope it helps, thanks!
     
  3. RC-1290

    RC-1290

    Joined:
    Jul 2, 2012
    Posts:
    639
    Is DragAndDrop data cleared when you start the drag (Using DragAndDrop.PrepareStartDrag)?
     
  4. BrUnO-XaVIeR

    BrUnO-XaVIeR

    Joined:
    Dec 6, 2010
    Posts:
    1,687
    I fixed my problem checking if the data is already there before starting a drag operation in the foreach loop.
    If data is not null than every other textFields/buttons leave the data intact until the drag is completed, now it drags the correct GUI object from the list and I can swap indexes. I did it a little bit different from this guide, but on its core it’s the same principles. Thanks
     
  5. RC-1290

    RC-1290

    Joined:
    Jul 2, 2012
    Posts:
    639
    Unfortunately, the code I posted has the significant problem that it does not correctly cancel a drag that originates from the class that starts the drag, when the user presses escape.
    I'll update the code when I find a solution.

    [Edit] It might be obvious that I didn't handle the DragExited event before. It wasn't called in my code, so I assumed that it wasn't useful. However, it turns out that it wasn't called because the mouse position is very different during that event (A jump from (31.0, 441.0) to (-1000.0, -90.0) in my listDrawer. ), and the Drag And Drop code I wrote specifically ignored any events outside the drag/drop area.

    DragExited is an event that occurs when you cancel the drag (for example when pressing the escape key on Windows). So this is another location where you want to clear the drag data.
     
    Last edited: Jan 28, 2014