Skip to content

Instantly share code, notes, and snippets.

@Thundernerd
Last active January 24, 2024 09:32
Show Gist options
  • Star 32 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save Thundernerd/5085ec29819b2960f5ff2ee32ad57cbb to your computer and use it in GitHub Desktop.
Save Thundernerd/5085ec29819b2960f5ff2ee32ad57cbb to your computer and use it in GitHub Desktop.
Helper to dock EditorWindows
#if UNITY_EDITOR
using System;
using System.Reflection;
using UnityEditor;
using UnityEngine;
public static class Docker
{
#region Reflection Types
private class _EditorWindow
{
private EditorWindow instance;
private Type type;
public _EditorWindow( EditorWindow instance ) {
this.instance = instance;
type = instance.GetType();
}
public object m_Parent {
get {
var field = type.GetField( "m_Parent", BindingFlags.Instance | BindingFlags.NonPublic );
return field.GetValue( instance );
}
}
}
private class _DockArea
{
private object instance;
private Type type;
public _DockArea( object instance ) {
this.instance = instance;
type = instance.GetType();
}
public object window {
get {
var property = type.GetProperty( "window", BindingFlags.Instance | BindingFlags.Public );
return property.GetValue( instance, null );
}
}
public object s_OriginalDragSource {
set {
var field = type.GetField( "s_OriginalDragSource", BindingFlags.Static | BindingFlags.NonPublic );
field.SetValue( null, value );
}
}
}
private class _ContainerWindow
{
private object instance;
private Type type;
public _ContainerWindow( object instance ) {
this.instance = instance;
type = instance.GetType();
}
public object rootSplitView {
get {
var property = type.GetProperty( "rootSplitView", BindingFlags.Instance | BindingFlags.Public );
return property.GetValue( instance, null );
}
}
}
private class _SplitView
{
private object instance;
private Type type;
public _SplitView( object instance ) {
this.instance = instance;
type = instance.GetType();
}
public object DragOver( EditorWindow child, Vector2 screenPoint ) {
var method = type.GetMethod( "DragOver", BindingFlags.Instance | BindingFlags.Public );
return method.Invoke( instance, new object[] { child, screenPoint } );
}
public void PerformDrop( EditorWindow child, object dropInfo, Vector2 screenPoint ) {
var method = type.GetMethod( "PerformDrop", BindingFlags.Instance | BindingFlags.Public );
method.Invoke( instance, new object[] { child, dropInfo, screenPoint } );
}
}
#endregion
public enum DockPosition
{
Left,
Top,
Right,
Bottom
}
/// <summary>
/// Docks the second window to the first window at the given position
/// </summary>
public static void Dock( this EditorWindow wnd, EditorWindow other, DockPosition position ) {
var mousePosition = GetFakeMousePosition( wnd, position );
var parent = new _EditorWindow( wnd );
var child = new _EditorWindow( other );
var dockArea = new _DockArea( parent.m_Parent );
var containerWindow = new _ContainerWindow( dockArea.window );
var splitView = new _SplitView( containerWindow.rootSplitView );
var dropInfo = splitView.DragOver( other, mousePosition );
dockArea.s_OriginalDragSource = child.m_Parent;
splitView.PerformDrop( other, dropInfo, mousePosition );
}
private static Vector2 GetFakeMousePosition( EditorWindow wnd, DockPosition position ) {
Vector2 mousePosition = Vector2.zero;
// The 20 is required to make the docking work.
// Smaller values might not work when faking the mouse position.
switch ( position ) {
case DockPosition.Left:
mousePosition = new Vector2( 20, wnd.position.size.y / 2 );
break;
case DockPosition.Top:
mousePosition = new Vector2( wnd.position.size.x / 2, 20 );
break;
case DockPosition.Right:
mousePosition = new Vector2( wnd.position.size.x - 20, wnd.position.size.y / 2 );
break;
case DockPosition.Bottom:
mousePosition = new Vector2( wnd.position.size.x / 2, wnd.position.size.y - 20 );
break;
}
return GUIUtility.GUIToScreenPoint( mousePosition );
}
}
#endif
@dizzy2003
Copy link

dizzy2003 commented Sep 16, 2017

Not working for me, dropInfo is null at line 114
I thought maybe it was my dual monitor setup, but disabling one display it still happens.

I am calling the Dock function as soon as the window is created, not sure if maybe that causes issues with GUIToScreenPoint..

EDIT: Dock seems to work fine if you perform the dock from within OnGui of the parent window.

@kbornema
Copy link

kbornema commented Mar 2, 2018

This is awesome! Thank you a lot :)

@moonlitocean
Copy link

moonlitocean commented Jun 23, 2018

Recommand to fix the problem where the code may not work outside parent window OnGUI:

Change last line of GetFakeMousePosition() from:
return GUIUtility.GUIToScreenPoint( mousePosition );
to
return new Vector2(wnd.position.x + mousePosition.x, wnd.position.y + mousePosition.y);

Reasoning: GUIUtility.GUIToScreenPoint relies on the current gui rect, but the Dock() function may call outside current of the parent window OnGUI.

@jeason1997
Copy link

Recommand to fix the problem where the code may not work outside parent window OnGUI:

Change last line of GetFakeMousePosition() from:
return GUIUtility.GUIToScreenPoint( mousePosition );
to
return new Vector2(wnd.position.x + mousePosition.x, wnd.position.y + mousePosition.y);

Reasoning: GUIUtility.GUIToScreenPoint relies on the current gui rect, but the Dock() function may call outside current of the parent window OnGUI.

You're right. it won't work until it's fix.

@pawwilon
Copy link

pawwilon commented Feb 12, 2019

Compressed version:

public enum DockPosition
{
	Left,
	Top,
	Right,
	Bottom
}

private static Vector2 GetFakeMousePosition(EditorWindow wnd, DockPosition position)
{
	Vector2 mousePosition = Vector2.zero;

	// The 20 is required to make the docking work.
	// Smaller values might not work when faking the mouse position.
	switch(position)
	{
	case DockPosition.Left: mousePosition = new Vector2(20, wnd.position.size.y / 2); break;
	case DockPosition.Top: mousePosition = new Vector2(wnd.position.size.x / 2, 20); break;
	case DockPosition.Right: mousePosition = new Vector2(wnd.position.size.x - 20, wnd.position.size.y / 2); break;
	case DockPosition.Bottom: mousePosition = new Vector2(wnd.position.size.x / 2, wnd.position.size.y - 20); break;
	}

	return new Vector2(wnd.position.x + mousePosition.x, wnd.position.y + mousePosition.y);
}

/// <summary>
/// Docks the "docked" window to the "anchor" window at the given position
/// </summary>
public static void DockWindow(this EditorWindow anchor, EditorWindow docked, DockPosition position)
{
	var anchorParent = GetParentOf(anchor);

	SetDragSource(anchorParent, GetParentOf(docked));
	PerformDrop(GetWindowOf(anchorParent), docked, GetFakeMousePosition(anchor, position));
}

static object GetParentOf(object target)
{
	var field = target.GetType().GetField("m_Parent", BindingFlags.Instance | BindingFlags.NonPublic);
	return field.GetValue(target);			
}

static object GetWindowOf(object target)
{
	var property = target.GetType().GetProperty("window", BindingFlags.Instance | BindingFlags.Public);
	return property.GetValue(target, null);			
}

static void SetDragSource(object target, object source)
{
	var field = target.GetType().GetField("s_OriginalDragSource", BindingFlags.Static | BindingFlags.NonPublic);
	field.SetValue(null, source);
}

static void PerformDrop(object window, EditorWindow child, Vector2 screenPoint)
{
	var rootSplitViewProperty = window.GetType().GetProperty("rootSplitView", BindingFlags.Instance | BindingFlags.Public);
	object rootSplitView = rootSplitViewProperty.GetValue(window, null);

	var dragMethod = rootSplitView.GetType().GetMethod("DragOver", BindingFlags.Instance | BindingFlags.Public);
	var dropMethod = rootSplitView.GetType().GetMethod("PerformDrop", BindingFlags.Instance | BindingFlags.Public);

	var dropInfo = dragMethod.Invoke(rootSplitView, new object[] { child, screenPoint });
	dropMethod.Invoke(rootSplitView, new object[] { child, dropInfo, screenPoint });
}

@thecrazy
Copy link

thecrazy commented Apr 1, 2019

Works very well, thank you for writing this.

I would like to make a case for an additional feature; the ability to dock a window as a tab in line with another tab (in the same dockarea, not beside it) same behavior as opening a new tab by typing "ctrl-t" in a web browser.

WHY:
GetWindow allows to do this by setting desiredDockNextTo, it does NOT create a new window if one of the same type is allready open and afaik it is the ONLY way to specify the desiredDockNextTo type. Also, since it assumes there are no two open windows of the same type it docks the window in line with the first occurence found which kinda lacks in control.

In the end, we are forced to use ScriptableObject.CreateInstance to open multiple windows of the same type and no solution to handle adding the window to an existing dockarea.

HOW:
If you look at EditorWindow.cs you can see how this is handled:

foreach (var desired in desiredDockNextTo)
{
    var windows = ContainerWindow.windows;
    foreach (var w in windows)
    {
        foreach (var view in w.rootView.allChildren)
        {
            var dockArea = view as DockArea;
            if (dockArea == null) continue;
            if (dockArea.m_Panes.Any(pane => pane.GetType() == desired))
            {
                dockArea.AddTab(win);
                return win;
            }
        }
    }
}

Since we want to specify which window to dock in line with we could probably get rid of the foreach loops.

I tried to make it work but I dont quite understand how to use reflection yet. I supose we have to gain access to ContainerWindow to access the windows array OR even better maybe we can get the DockArea of a window directly and use:

dockArea.AddTab(win);

@jimtang
Copy link

jimtang commented May 21, 2019

@pawwilon Add a if checking whether the target window is already docked:

var dropInfo = dragMethod.Invoke(rootSplitView, new object[] { child, screenPoint });
if (dropInfo == null) return;
FieldInfo fi = dropInfo.GetType().GetField("dropArea");
if (fi != null && fi.GetValue(dropInfo) == null) return;

@usernameHed
Copy link

usernameHed commented Mar 19, 2020

Yes, may be add in the DockPosition enum "ON_FRONT" and "ON_BACKGROUND". Too bad it's not there, I was searching a way to do specificly that.

@Jayatubi
Copy link

I tried this code on Unity 5.6 and the DragOver always returns null thus the Dock function fails to me.
Any suggestion?

@Jayatubi
Copy link

Recommand to fix the problem where the code may not work outside parent window OnGUI:

Change last line of GetFakeMousePosition() from:
return GUIUtility.GUIToScreenPoint( mousePosition );
to
return new Vector2(wnd.position.x + mousePosition.x, wnd.position.y + mousePosition.y);

Reasoning: GUIUtility.GUIToScreenPoint relies on the current gui rect, but the Dock() function may call outside current of the parent window OnGUI.

Wow that did the trick!

@Jayatubi
Copy link

I just made some modifications to make this Dock function could work for those already docked windows. https://gist.github.com/Jayatubi/f6cafb4d5a5fcb54b537e79be77aa714

@s7887177
Copy link

This is what I need! Thank you so much!
It's so incredible I search for all day and found this only thread.

@haikushisui
Copy link

haikushisui commented Dec 23, 2021

@thecrazy

I needed to use AddTab for my project too here is how I did it.
Hope it helps someone.

I made a function just like Dock to do it but you also can add a tab position in the enum wand do it in the Dock function


        /// <summary>
        /// Docks the second window to the first window as a tab
        /// </summary>
        public static void AddTab(this EditorWindow wnd, EditorWindow other)
        {
            var parent = new _EditorWindow(wnd);
            var child = new _EditorWindow(other);
            var dockArea = new _DockArea(parent.m_Parent);
            var childDockArea = new _DockArea(child.m_Parent);
            childDockArea.RemoveTab(other);
            dockArea.AddTab(other);
        }

If you don't remove the tab from the window you're about to dock it will just create a duplicate and you'll have two windows.
So to do that we have to add two functions in the _DockArea class : AddTab and RemoveTab.

            public void AddTab(EditorWindow child)
            {
                var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public);
                var method = methods.FirstOrDefault(c =>
                {
                    return
                        c.Name == "AddTab" &&
                        c.GetParameters()[0].ParameterType == typeof(EditorWindow);
                });
                method.Invoke(instance, new object[] { child, true });
            }

            public void RemoveTab(EditorWindow pane)
            {
                var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public);
                var method = methods.FirstOrDefault(c =>
                {
                    return
                        c.Name == "RemoveTab" &&
                        c.GetParameters().Count() == 1;
                });
                method.Invoke(instance, new object[] { pane });
            }

and that's it 😄

@tengiplex
Copy link

Thank you guys for this... what a life saver! Spent so much time trying to figure out Unity's "documentation". New to reflection.. so it was difficult using MS's docs, but this really helps put it all together. <3

My small contribution: Added shortcuts to some static methods using this attribute. In this example, Control + M calls the method that follows. Here's the documentation on the attribute!

[Shortcut("AddNewInspectorWindow", KeyCode.M, ShortcutModifiers.Action)]
public static void MyAddInspectorFunction()
{
//code
};

@somedeveloperhappy
Copy link

hey. the code seems to not work properly in Unity 2020.3.25f1

@VitaliyPanov
Copy link

VitaliyPanov commented Feb 27, 2023

Some advice for astronauts: you can use internal bridges of unity: https://github.com/Unity-Technologies/UnityCsReference/blob/a6cadb936f3855ab7e5bd8e19d85af403d6802c6/Editor/Mono/AssemblyInfo/AssemblyInfo.cs
Just create an assembly with the one of the names (Unity.InternalAPIEditorBridge.001 for example) and then you can use internals of unity inside this assebly. You don't need reflection afrer that

       public static void DockTo(this EditorWindow first, EditorWindow second, DockPosition position)
        {
            Vector2 mousePosition = GetFakeMousePosition(second, position);
            SplitView targetView = null;
            DropInfo dropInfo = null;
            var windows = ContainerWindow.windows;
            for (int i = 0; i < windows.Length; i++)
            {
                SplitView rootSplitView = windows[i].rootSplitView;
                if (rootSplitView != null)
                {
                    dropInfo = rootSplitView.DragOverRootView(mousePosition);
                    targetView = rootSplitView;
                }

                if (dropInfo == null)
                {
                    View rootView = windows[i].rootView;
                    for (int j = 0; j < rootView.allChildren.Length; j++)
                    {
                        if (rootView.allChildren[j] is IDropArea dropArea)
                        {
                            dropInfo = dropArea.DragOver(second, mousePosition);
                            if (dropInfo != null)
                            {
                                targetView = rootView.allChildren[j] as SplitView;
                                break;
                            }
                        }
                    }
                }
                else
                {
                    break;
                }
            }


            if (targetView != null && dropInfo != null)
            {
                DockArea.s_OriginalDragSource = (DockArea) first.m_Parent;
                targetView.PerformDrop(first, dropInfo, mousePosition);
            }
        }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment