Unity doesn't support generic editor windows? (GetWindow < Window < T > >)

So I made a cool GameObject selection window that picks up all GOs in the scene, you could search, filter, etc.

It looks something like this:

Now I’m trying to parametrize it, make it generic and inject what’s needed - Having “Re-usability” and good coding practice in mind.

The idea is for example, let’s say I wanted a selection window for GameObjects, is to call SelectionWindow < GameObject > .Show(this, () => GameObject.FindObjectsOfType);

Or maybe I want to view all scenes in my project, I would pass SelectionWindow < string > .Show(this, () => Utils.GetAssets<object>("Assets", "*.unity", SearchOption.AllDirectories)); etc…

‘this’ is the caller, which should be implementing an ISelectionWindowUser interface.

The delegate, is the item assignment delegate - basically what to show up in the window.

What I’m failing at however, is showing up the window:

public static void Show(ISelectionWindowUser<T> user, Func<T[]> refreshAction)
{
	var window = GetWindow<SelectionWindow<T>>(); // BREAKS! NullReferenceException :((
	//var window = GetWindow(typeof(SelectTargetGoWindow<T>)) as SelectTargetGoWindow<T>; // also tried this...
	window.Init(user, refreshAction);
	window.ShowUtility();
}

Full code:

SelectionWindow.cs

using UnityEngine;
using System.Linq;
using System.Collections.Generic;
using UnityEditor;
using System.Text.RegularExpressions;
using System;
using Object = UnityEngine.Object;

public class SelectionWindow<T> : EditorWindow where T : Object
{
	private ISelectionWindowUser<T> user;
	private Func<T[]> refreshAction;
	private bool hasInitStyles;
	private Vector2 scrollPosition;
	private string search = "";
	private const float INDENT_WIDTH = 20f;
	private T[] items;
	private T[] filteredItems;

	private StyleDue styleDue;

	// GUI Styles...

	private void InitStyles()
	{
		// Initializing styles...
	}

	public static void Show(ISelectionWindowUser<T> user, Func<T[]> refreshAction)
	{
		var window = GetWindow(typeof(SelectionWindow<T>)) as SelectionWindow<T>;

		//var window = GetWindow<SelectTargetGoWindow<T>>();
		window.Init(user, refreshAction);
		window.ShowUtility();
	}
	private void Init(ISelectionWindowUser<T> user, Func<T[]> refreshAction)
	{
		this.user = user;
		this.refreshAction = refreshAction;
		styleDue = new StyleDue(Utils.HexToColor("CCCCCC"), Utils.HexToColor("BABABA"));
	}

	void OnGUI()
	{
		if (!hasInitStyles) {
			hasInitStyles = true;
			InitStyles();
		}

		GUIHelper.HorizontalBlock(() =>
		{
			GUILayout.Label("GameObjects", GameObjectsLabel);
			search = EditorGUILayout.TextField("", search);
			filteredItems = items.Where(go => Regex.IsMatch(go.name, search, RegexOptions.IgnoreCase)).ToArray();
			if (GUILayout.Button(new GUIContent("↶", "Refresh"), RefreshButton, GUILayout.Width(20)))
				Refresh();
		});

		scrollPosition = GUIHelper.ScrollViewBlock(scrollPosition, false, false, () =>
		{
			foreach (var item in filteredItems) {
				bool selected = item == user.target;
				var nextStyle = styleDue.NextStyle;
				GUIHelper.HorizontalBlock(selected ? SelectedStyle : nextStyle, () =>
				{
					GUILayout.Space(INDENT_WIDTH);
					var spaceRect = GUILayoutUtility.GetLastRect();
					GUILayout.Label(item.name, selected ? SelectedLabel : UnselectedLabel);
					var labelRect = GUILayoutUtility.GetLastRect();
					var buttonRect = GUIHelper.CombineRects(spaceRect, labelRect);
					if (!selected) {
						EditorGUIUtility.AddCursorRect(buttonRect, MouseCursor.Link);
						if (GUI.Button(buttonRect, "", GUIStyle.none)) {
							user.target = item;
						}
					}
				});
			}
		});
	}

	void Refresh()
	{
		items = refreshAction();
	}
	void OnFocus()
	{
		Refresh();
	}
}

ISelectionWindowUser.cs

public interface ISelectionWindowUser<T> where T : Object
{
	T target { get; set; }
}

User code:

	if (GUILayout.Button(new GUIContent("Select target", "Select a target game object to inspect"), SelectButton, GUILayout.Height(20))) {
		SelectionWindow<GameObject>.Show(this, () => GameObject.FindObjectsOfType<GameObject>());
	}

As mentioned before the user should implement ISelectionWindowUser < T > where T is what he’s interested in (GameObject, etc)

I’ll be really sad if I hear that Unity doesn’t support this, I mean come on…

Again, my problem is this GetWindow < SelectionWindow < GameObject > >();

Any help would be very appreciated!

Thanks.

EDIT:

Full error:

NullReferenceException: Object reference not set to an instance of an object
UnityEditor.EditorWindow.GetWindow (System.Type t, Boolean utility, System.String title, Boolean focus) (at C:/BuildAgent/work/d3d49558e4d408f4/artifacts/EditorGenerated/EditorWindow.cs:423)
UnityEditor.EditorWindow.GetWindow[SelectionWindow`1] (Boolean utility, System.String title, Boolean focus) (at C:/BuildAgent/work/d3d49558e4d408f4/artifacts/EditorGenerated/EditorWindow.cs:461)
UnityEditor.EditorWindow.GetWindow[SelectionWindow`1] () (at C:/BuildAgent/work/d3d49558e4d408f4/artifacts/EditorGenerated/EditorWindow.cs:434)
SelectionWindow`1[UnityEngine.GameObject].Show (ISelectionWindowUser`1 user, System.Func`1 refreshAction) (at Assets/Editor/SelectionWindow.cs:89)
ShowInspectorWindow.<OnGUI>m__22 () (at Assets/Editor/ShowInspectorWindow.cs:125)
GUIHelper.HorizontalBlock (System.Action block) (at Assets/Editor/GUIHelper.cs:54)
ShowInspectorWindow.OnGUI () (at Assets/Editor/ShowInspectorWindow.cs:111)
System.Reflection.MonoMethod.Invoke (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) (at /Users/builduser/buildslave/monoAndRuntimeClassLibs/build/mcs/class/corlib/System.Reflection/MonoMethod.cs:222)
Rethrow as TargetInvocationException: Exception has been thrown by the target of an invocation.
System.Reflection.MonoMethod.Invoke (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) (at /Users/builduser/buildslave/monoAndRuntimeClassLibs/build/mcs/class/corlib/System.Reflection/MonoMethod.cs:232)
System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) (at /Users/builduser/buildslave/monoAndRuntimeClassLibs/build/mcs/class/corlib/System.Reflection/MethodBase.cs:115)
UnityEditor.HostView.Invoke (System.String methodName, System.Object obj) (at C:/BuildAgent/work/d3d49558e4d408f4/Editor/Mono/GUI/DockArea.cs:231)
UnityEditor.HostView.Invoke (System.String methodName) (at C:/BuildAgent/work/d3d49558e4d408f4/Editor/Mono/GUI/DockArea.cs:224)
UnityEditor.HostView.OnGUI () (at C:/BuildAgent/work/d3d49558e4d408f4/Editor/Mono/GUI/DockArea.cs:120)

While you can’t create a generic window, you could have a generic class with all your functionality in it wrapped by a non-generic window that accepts a type parameter to create a strongly-typed version of your generic class.

I’ve been using Reflection and System.Type shenanigans a lot lately gosh it’s fun. I made a very basic example, you should be able to plug’n’play without too much work:

using UnityEngine;
using UnityEditor;

// this just lets you use "myImplementation as ISelectionWindow" instead of going through reflection to convert to a real selectionwindow
// i value the code legibility over the extra interface
public interface ISelectionWindow {
	void OnGUI();
}

public class SelectionWindow<T> : ISelectionWindow {
	// your code here
	public void OnGUI() {
		GUILayout.Label("I'm of type " + typeof(T));
	}
}

public class SelectionWindowWrapper : EditorWindow {
	System.Type _type=null;
	// I wasn't able to quickly find a solution to "have a variable that could represent a specific generic class of any type"
	// but object works fine - note the little o, this is C#'s object not Unity's Object
	object myImplementation=null;
  
	[MenuItem("Window/Custom Tools/Selection Window")] // for testing purposes
	public static void Init() {
		SelectionWindowWrapper sww = EditorWindow.CreateInstance<SelectionWindowWrapper>();
		sww.Show();
	}
  
	public System.Type Type {
		get { return _type.GetGenericArguments()[0]; }
		set {
			// These two lines are how you get your specific implementation
			_type = typeof(SelectionWindow<>).MakeGenericType(value);
			myImplementation = System.Activator.CreateInstance(_type);
		}
	}
	
	void OnGUI() {
		if ( null == myImplementation ) { // provide selection options or prevent this condition
			if ( GUILayout.Button("GameObject") ) Type = typeof(GameObject); // notice capital T, we're using the setter
			return;
		}
		(myImplementation as ISelectionWindow).OnGUI();
	}
}

I guess at worse you could try:

public class SomeEditorWindow : SelectionWindow<GameObject>
{

}

And then GetWindow on SomeEditorWindow

public class SelectionWindow : EditorWindow
{
    private IGenericWindow genericWindow;
    public static void Show<T>(T t) 
    {
        SelectionWindow window = EditorWindow.GetWindow<SelectionWindow>(false, "ADD");
        window.minSize = new Vector2(300.0f, 500.0f);
        GenericWindow<T> genericWindow = new GenericWindow<T>();
        genericWindow.t = t;
        window.genericWindow = genericWindow;
    }
    public void OnGUI()
    {
        genericWindow.OnGUI();
    }
}
  
public class GenericWindow<T> : IGenericWindow
{
    public T t;
  
    public void OnGUI()
    {
    }
}
  
public interface IGenericWindow
{
    void OnGUI();
}

Sorry to reawaken the dead.

I ran into this issue as well, but was looking for a cleaner method.

I’ve found a solution that omits wrapper classes + interfaces in favour of variable casting.

The window class defines a generic static method that takes a custom “input” type. It then stores the provided OnGUI and Input Handler actions/funcs by leveraging lambda functions and casting it to the provided input type:

public class InputWindow : EditorWindow
{
   private Func<object> onGUI;
   private Action<object> onInputReceived;
   private string submitText;

   public static void ShowWindow<T>(string title,
                                    Func<T> onGUI,
                                    Action<T> onInputReceived,
                                    string submitText = "Submit")
   {
       InputWindow window = GetWindow<InputWindow>(title);
       window.onGUI = () => onGUI.Invoke();
       window.onInputReceived = (input) => onInputReceived.Invoke((T)input);
       window.submitText = submitText;
   }

   private void OnGUI()
   {
       if (onGUI == null)
       {
           Close();
           return();
       }  
       object input = onGUI();

       if (GUILayout.Button(submitText))
       {
           if (onInputReceived != null)
           {
               onInputReceived(input);
           }
           Close();
       }
   }
}

The usage then is as simple as defining your custom OnGUI method and your input handler:

public static string input;
  
[MenuItem("GameObject/Core/Log String Input")]
public static void NarrativeSceneTimeline()
{
    InputWindow.ShowWindow<string>(
        "Try it out!",
        LogStringInputOnGUI,
        HandleLogStringInput,
        "Log"
    );
}

public void LogStringInputOnGUI()
{
    input = EditorGUILayout.TextField("Type Input Here:", input);
    return input;
}

public void HandleLogStringInput(string input)
{
    Debug.Log(input);
}