Been on this for 2 days now without any success.
Just want to know that it’s absolutely not possible to use the DragAndDrop class for dragging something from and EditorWindow into the SceneView for custom instantiation?
I think not, because every time the mouse leaves my window I get “DragExited” event. Which just kills any DragAndDrop events from that point on until the mouse comes back into the Window.
I’ve tried looking at Unity source, but can’t see any hidden magic going on.
How have Unity managed to use DragAndDrop from the ProjectView to the SceneView without hitting a DragExit event?
I’m completely baffled.
Adding a SceneView.onSceneGUIDelegate callback is no help either, as that never receives any event other than “repaint” and “layout”
If I recall correctly, this should technically be possible using some reflection magic. I investigated this for my own asset awhile back. I don’t remember exactly what would be required. It’s definitely not possible using the existing public APIs, however.
Yeah it doesn’t work as one might expect. The issue is “DragExit” event occurs on both MouseUp and when Mouse leaves a window that has control.
I was wrong about SceneView.onSceneGUIDelegate not receiving mouse events.
I was setting hotControl to the control that initiated the drag, thus preventing any other drag events from occurring on other windows (i.e SceneView)
The solution I came to was:
Listen to “MouseDown” event in custom control
Set hotControl to its controlId
Listen for “MouseDrag” when hotControl == controlId
Set hotControl to 0, Start the drag and add SceneView.onSceneGUIDelegate for drop handling
Listen for “DragUpdated”, “DragPerform” and “DragExit” in SceneView delegate handler
Don’t cancel and remove delegates from “DragExit” - Impossible to know if DragExit was a MouseUp or just lost Focus
Handle Drop functionality in DragUpdated and DragPerform
Only remove SceneView.onSceneGUIDelegate when “DragPerform” is accepted
The issue I was trying to solve was how to clean up the SceneView.onSceneGUIDelegate on DragExit. Because you can never know if a DragExit was an actual cancellation of the Drag. So if you remove that delegate before a Drag was actually complete, Drag will break.
So the solution is to check the DragAndDrop.GetGenericData and compare it with expected values, if you don’t get the value you’re looking for, then do nothing. If you do, then capture that event and handle
So, yes, it’s possible that the SceneView.onSceneGUIDelegate will just linger around if a Drop was never performed.
Here’s part of the Solution I came up with:
using UnityEngine;
namespace UnityEditor {
public static class SceneDragAndDrop {
private static readonly int sceneDragHint = "SceneDragAndDrop".GetHashCode();
private const string DRAG_ID = "SceneDragAndDrop";
private static readonly Object[] emptyObjects = new Object[0];
private static readonly string[] emptyPaths = new string[0];
public static void StartDrag(ISceneDragReceiver receiver, string title) {
//stop any drag before starting a new one
StopDrag();
if (receiver != null) {
//make sure we release any control from something that has it
//this is done because SceneView delegate needs DragEvents!
GUIUtility.hotControl = 0;
//do the necessary steps to start a drag
//we set the GenericData to our receiver so it can handle
DragAndDrop.PrepareStartDrag();
DragAndDrop.objectReferences = emptyObjects;
DragAndDrop.paths = emptyPaths;
DragAndDrop.SetGenericData(DRAG_ID, receiver);
receiver.StartDrag();
//start drag and listen for Scene drop
DragAndDrop.StartDrag(title);
SceneView.onSceneGUIDelegate += OnSceneGUI;
}
}
public static void StopDrag() {
//cleanup delegate
SceneView.onSceneGUIDelegate -= OnSceneGUI;
}
private static void OnSceneGUI(SceneView sceneView) {
//get a controlId so we can grab events
int controlId = GUIUtility.GetControlID(sceneDragHint, FocusType.Passive);
Event evt = Event.current;
EventType eventType = evt.GetTypeForControl(controlId);
ISceneDragReceiver receiver;
switch (eventType) {
case EventType.DragPerform:
case EventType.DragUpdated:
//check that GenericData is the expected type
//if not, we do nothing
//it would seem that whenever a Drag is started, GenericData is cleared, so we don't have to explicitly clear it ourself
receiver = DragAndDrop.GetGenericData(DRAG_ID) as ISceneDragReceiver;
if (receiver != null) {
//let receiver handle the drag functionality
DragAndDrop.visualMode = receiver.UpdateDrag(evt, eventType);
//perform drag if accepted
if (eventType == EventType.DragPerform && DragAndDrop.visualMode != DragAndDropVisualMode.None) {
receiver.PerformDrag(evt);
DragAndDrop.AcceptDrag();
DragAndDrop.SetGenericData(DRAG_ID, default(ISceneDragReceiver));
//we can safely stop listening to scene gui now
StopDrag();
}
evt.Use();
}
break;
case EventType.DragExited:
//Drag exited, This can happen when:
// - focus left the SceneView
// - user cancelled manually (Escape Key)
// - user released mouse
//So we want to inform the receiver (if any) that is was cancelled, and it can handle appropriatley
receiver = DragAndDrop.GetGenericData(DRAG_ID) as ISceneDragReceiver;
if (receiver != null) {
receiver.StopDrag();
evt.Use();
}
break;
}
}
}
public interface ISceneDragReceiver {
void StartDrag();
void StopDrag();
DragAndDropVisualMode UpdateDrag(Event evt, EventType eventType);
void PerformDrag(Event evt);
}
}
The other part is with a ISceneDragReceiver.
That object should handle the creation/destruction of a Draggable object, whatever that might be
New
Hello, CDF. I am implementing my own custom Project Window and I am confusing on drag and drop operation. What is the other part “ISceneDragReceiver” like?
It’s whatever you want it to be. You implement custom logic inside it.
So for instantiating an object in the scene, you could do something like this:
class MyObjectDrag : ISceneDragReceiver {
private string assetPath;
private GameObject instance;
public MyObjectDrag(string assetPath) {
this.assetPath = assetPath;
}
public void StartDrag() {
}
public DragAndDropVisualMode UpdateDrag(Event evt, EventType eventType) {
//don't have an asset path, so reject the drag
if (string.IsNullOrEmpty(assetPath)) {
return DragAndDropVisualMode.None;
}
//see if we have an existing instance, if not, create one
if (instance == null) {
//load the asset and instantiate. Set the HideFlags to HideInHierarchy as user may not actually complete the drag
//only on perform do we reveal the instance in the hierarchy
var asset = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
instance = Instantiate(asset);
instance.hideFlags = HideFlags.HideInHierarchy;
instance.name = asset.name;
}
//move the instance to the ground plane of the scene
MoveInstanceToMouse(instance, evt);
//return that we're going to "Copy"
return DragAndDropVisualMode.Copy;
}
public void PerformDrag(Event evt) {
//drag was performed, so if we have an instance, reset the hideFlags and nullify our reference so it doesn't get destroyed
if (instance != null) {
instance.hideFlags = HideFlags.None;
instance = null;
}
}
public void StopDrag() {
//drag was stopped (either by a cancel or mouse release), so check if we have an instance and if so, destroy it
if (instance != null) {
Object.DestroyImmediate(instance, false);
}
}
private void MoveInstanceToMouse(GameObject instance, Event evt) {
//get the ground mouse position and place the instance there
Ray ray = HandleUtility.GUIPointToWorldRay(evt.mousePosition);
Plane plane = new Plane(Vector3.up, 0);
if (plane.Raycast(ray, out float hit)) {
Vector3 point = ray.GetPoint(hit);
instance.transform.position = point;
}
}
}
I experienced this problem but with dragging into the inspector (instead of the scene view) from my custom window. I just wanted to share that the solution to set the hotControl to 0 in MouseDrag worked for me too. Thanks for posting your solution, CDF!
public void OnSceneDrag(SceneView sceneView, int index)
{
Event evt = Event.current;
OnSceneDragInternal(sceneView, index, evt.type, evt.mousePosition, evt.alt);
}
By declaring public void OnSceneDrag(SceneView sceneView, int index) in the customEditor of your asset, you can handle when your asset is dropped in SceneView. It will be sent each time there is a drag-and-drop-related event.
By default, the cursor will be set to “Reject” so don’t forget to use this line in your code to have the correct cursor.