Skip to content
Random Potion
Menu
  • Home
  • Games
  • Services
  • About
  • Contact
Menu

Extending the Editor With UI Toolkit

Posted on April 4, 2023 by admin

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.

Extending the Editor with UI ToolkitDownload

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;
		}

	}
}

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

  • Mastodon
  • Twitter
  • YouTube
  • Facebook
  • LinkedIn
  • Mail
  
  
Loading Mastodon feed...
© 2025 Random Potion | Powered by Minimalist Blog WordPress Theme