I did a presentation on Extending the Editor With UI Toolkit at Helsinki Unity Meetup in April, this post has the slides and some additional code examples.
GraphView Tutorial
I used these tutorial videos to learn about the GraphView:
Toolkit Wrapper
Unity (at least 2021 LTS) doesn’t support UTK-based Property Drawers on lists out of the box, so here’s a little wrapper that fixes that:
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
[CustomEditor(typeof(ScriptableObject), true)]
public class ImguiToToolkitWrapper: UnityEditor.Editor
{
public override VisualElement CreateInspectorGUI()
{
var root = new VisualElement();
var prop = serializedObject.GetIterator();
if (prop.NextVisible(true))
{
do
{
var field = new PropertyField(prop);
if (prop.name == "m_Script")
{
field.SetEnabled(false);
}
root.Add(field);
}
while (prop.NextVisible(false));
}
return root;
}
}
Quick Navigation
One of the editor extensions I presented was a quick navigation window that lets you quickly jump between often-used scenes and folders. It also serves as an example of a simple dynamic UI Tookit widget.
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace EditorClasses
{
[CreateAssetMenu]
public class EditorDebugSettings : ScriptableObject
{
public List<string> CommonScenes = new List<string>()
{
"Assets/Scenes/SampleScene.unity",
"Assets/Scenes/MainMenu.unity"
};
public List<string> LastUsedScenes;
public List<string> Links;
public void OnSceneOpened(Scene scene)
{
if(CommonScenes.Contains(scene.path))
return;
if (LastUsedScenes.Contains(scene.path))
{
LastUsedScenes.Remove(scene.path);
}
LastUsedScenes.Insert(0, scene.path);
EditorUtility.SetDirty(this);
}
}
}
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;
namespace EditorClasses
{
[InitializeOnLoad]
public class EditorGlobal
{
private static EditorDebugSettings _debugSettings;
public static List<string> CommonScenes => _debugSettings.CommonScenes;
public static List<string> LastUsedScenes => _debugSettings.LastUsedScenes;
public static List<string> Links => _debugSettings.Links;
[MenuItem("Tools/Reload Editor Global")]
private static void LoadEditorGlobal()
{
EditorSceneManager.sceneOpened += EditorSceneManagerOnsceneOpened;
_debugSettings = AssetDatabase.LoadAssetAtPath<EditorDebugSettings>("Assets/Editor/EditorDebugSettings.asset");
if (_debugSettings.LastUsedScenes == null)
{
_debugSettings.LastUsedScenes = new List<string>();
EditorUtility.SetDirty(_debugSettings);
}
}
static EditorGlobal()
{
LoadEditorGlobal();
}
private static void EditorSceneManagerOnsceneOpened(Scene scene, OpenSceneMode mode)
{
_debugSettings.OnSceneOpened(scene);
}
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.UIElements;
using Object = UnityEngine.Object;
namespace EditorClasses
{
public class QuickNavigation : EditorWindow
{
private int GetInstanceID( string link)
{
string path = Path.Combine("Assets", link);
return AssetDatabase.LoadAssetAtPath<Object>(path).GetInstanceID();
}
private void AddLink( string id)
{
var button = new Button(() =>
{
ShowFolderContents(GetInstanceID(id));
Close();
});
button.text = id;
rootVisualElement.Add(button);
}
[MenuItem("Window/Quick Navigation %g")]
public static void Open() => GetWindow< QuickNavigation >( true, "Quick Navigation", true);
private void AddSceneButton(string scene)
{
var shortName = Path.GetFileNameWithoutExtension(scene);
var button = new Button(() =>
{
EditorSceneManager.OpenScene(scene);
Close();
});
button.text = shortName;
rootVisualElement.Add(button);
}
private void AddLabel(string text)
{
var label = new Label(text);
label.style.unityFontStyleAndWeight = FontStyle.Bold;
label.style.marginBottom = 10;
label.style.marginTop = 10;
label.style.alignSelf = Align.Center;
rootVisualElement.Add(label);
}
/// <summary>
/// Create GUI is called by Unity when the window is initialized or needs to be redrawn
/// </summary>
///
private void CreateGUI()
{
AddLabel("Common Scenes");
foreach (var scene in EditorGlobal.CommonScenes)
{
AddSceneButton(scene);
}
AddLabel("Last Used Scenes");
var lastUsedScenes = EditorGlobal.LastUsedScenes;
int count = lastUsedScenes.Count > 5 ? 5 : lastUsedScenes.Count;
for (int i = 0; i < count; i++)
{
AddSceneButton(lastUsedScenes[i]);
}
AddLabel("Folders");
foreach ( var key in EditorGlobal.Links )
{
AddLink( key );
}
}
/// <summary>
/// Selects a folder in the project window and shows its content.
/// Opens a new project window, if none is open yet.
/// </summary>
/// <param name="folderInstanceID">The instance of the folder asset to open.</param>
private static void ShowFolderContents(int folderInstanceID)
{
// Find the internal ProjectBrowser class in the editor assembly.
Assembly editorAssembly = typeof(UnityEditor.Editor).Assembly;
System.Type projectBrowserType = editorAssembly.GetType("UnityEditor.ProjectBrowser");
// This is the internal method, which performs the desired action.
// Should only be called if the project window is in two column mode.
MethodInfo showFolderContents = projectBrowserType.GetMethod(
"ShowFolderContents", BindingFlags.Instance | BindingFlags.NonPublic);
if (showFolderContents == null)
throw new Exception("Can't find method ShowFolderContents");
// Find any open project browser windows.
Object[] projectBrowserInstances = Resources.FindObjectsOfTypeAll(projectBrowserType);
if (projectBrowserInstances.Length > 0)
{
for (int i = 0; i < projectBrowserInstances.Length; i++)
{
if (projectBrowserInstances[i] == null)
{
Debug.LogWarning($"Null Project Browser instance found!");
continue;
}
ShowFolderContentsInternal(projectBrowserInstances[i], showFolderContents, folderInstanceID);
}
}
else
{
EditorWindow projectBrowser = OpenNewProjectBrowser(projectBrowserType);
ShowFolderContentsInternal(projectBrowser, showFolderContents, folderInstanceID);
}
}
private static void ShowFolderContentsInternal(Object projectBrowser, MethodInfo showFolderContents, int folderInstanceID)
{
// Sadly, there is no method to check for the view mode.
// We can use the serialized object to find the private property.
SerializedObject serializedObject = new SerializedObject(projectBrowser);
bool inTwoColumnMode = serializedObject.FindProperty("m_ViewMode").enumValueIndex == 1;
if (!inTwoColumnMode)
{
// If the browser is not in two column mode, we must set it to show the folder contents.
MethodInfo setTwoColumns = projectBrowser.GetType().GetMethod(
"SetTwoColumns", BindingFlags.Instance | BindingFlags.NonPublic);
setTwoColumns.Invoke(projectBrowser, null);
}
bool revealAndFrameInFolderTree = true;
showFolderContents.Invoke(projectBrowser, new object[] { folderInstanceID, revealAndFrameInFolderTree });
}
private static EditorWindow OpenNewProjectBrowser(System.Type projectBrowserType)
{
EditorWindow projectBrowser = EditorWindow.GetWindow(projectBrowserType);
projectBrowser.Show();
// Unity does some special initialization logic, which we must call,
// before we can use the ShowFolderContents method (else we get a NullReferenceException).
MethodInfo init = projectBrowserType.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public);
init.Invoke(projectBrowser, null);
return projectBrowser;
}
}
}