Unity editor PreferenceItem creation at runtime possible?

I had been using the PreferenceItem , attribute to define the name of my Tab in the window, and a static function that will draw the various options.

But I wanted a way to specify “categories”, for the options I display in that window.
So, I created a VendorPrefAttribute class. This attribute takes TWO parameters, the vendor and, additionally, the category string. I tried to make this class generalized enough that anyone could use it.

While it works fine, it has one issue that complicates it’s use, that I would like to work around.

In order for a new tab to actually be created for the vendor, the unity PreferenceItem attribute, MUST be defined somewhere. E.g.

\\class exists only to define the [PreferenceItem("My Vendor Name")] attribute
public class MyVendorNamePrefsAttribute : VendorPrefAttribute  
{
    public MyVendorNamePrefsAttribute(string _category) : base("My Vendor Name",_category) { }

    [PreferenceItem("My Vendor Name")]
    public static void OnGui() { VendorPrefAttribute.DrawVendorPreferencesGUI("My Vendor Name"); }
}

Only now can the programmer finally apply the attribute the drawing function: e.g

[MyVendorNamePrefsAttribute("Favorites")]
        public static void OnGUI()
        {
            favoriteColor.Value = EditorGUILayout.ColorField(favoriteColor.label, favoriteColor.Value);
        }
[MyVendorNamePrefsAttribute("Secondary Options")]
        public static void OnGUI()
        {
            capitalOfAssyria.Value = EditorGUILayout.TextField(capitalOfAssyria.label, capitalOfAssyria.Value);
        }

Ideally, I would prefer the usage to be simpler, and do without the extra derived class, (since every version of this class will be identical in form, just different in the vendor string):

[VendorPrefAttribute ("My Vendor Name","Favorites")]
        public static void OnGUI()
        {
            favoriteColor.Value = EditorGUILayout.ColorField(favoriteColor.label, favoriteColor.Value);
        }
[VendorPrefAttribute ("My Vendor Name","Secondary Options")]
        public static void OnGUI()
        {
            capitalOfAssyria.Value = EditorGUILayout.TextField(capitalOfAssyria.label, capitalOfAssyria.Value);
        }

Alas, I’m not sure how to tell unity to create that vendors tab without the programmer having to specify the derived class using the [PreferenceItem] attribute (MyVendorNamePrefsAttribute).

Generally, is there any way to work around this?
Particularly, can a tab, and it’s drawing callback function, be added to the Unity editor preference window, at run-time?

Well, this is going to be tricky. The first simple answer is: No, it’s not possible.

The long answer is: Yes, it should be possible with a quite a bit of reflection ^^. There are several problems that need to be solved:

  • First of all the PreferencesWindow (an internal EditorWindow) will create the sections for the built in stuff in OnEnable and will add the custom stuff (PreferenceItem) inside OnGUI the first time OnGUI runs. So any modifications of the internal list can only be done after the first OnGUI iteration of the PreferencesWindow. So it’s difficult how you apply your changes.
  • The preferences itself are inside an internal List of an internal type called “Section”. A Section just has a GUIContent and a void delegate. So once you find a way to determine the right point in time when to change / modify the sections the real fun begins ^^.

You would need the following types:

  • UnityEngine.PreferencesWindow (internal class)
  • UnityEngine.PreferencesWindow.Section (private nested class)
  • UnityEngine.PreferencesWindow.OnGUIDelegate (private nested delegate type)
  • Construct the generic type List<Section> using MakeGenericType

And the following fields:

  • UnityEngine.PreferencesWindow.m_Sections (private List of Section)
  • UnityEngine.PreferencesWindow.m_RefreshCustomPreferences (private bool)
  • List<Section>.Add

There’s probably no way around “polling” for the preferences window to appear using FindObjectsOfType. Keep in mind that the EditorApplication.update delegate run about 100 times (recently it was 200 times) per second. So you might want to cut down the time with a counter. Once every second is probably enough. When the window is opened it might take a second until your custom stuff appears.

Once you found a PreferencesWindow instance you want to check if m_Sections is not null. If it has a list it means OnEnable has already fired and the default items have been added to the sections list. OnEnable sets “m_RefreshCustomPreferences” to true and it gets set back to false after the first OnGUI call. At this point the custom sections have been added.

Now you can start your “patching”. You would need to create a delegate using Delegate.CreateDelegate with a methodinfo and the “OnGUIDelegate” delegate type. Keep in mind if you want to use an instance method of one of your classes you also have to provide the object reference for the method.

Use the System.Activator class to create an instance of the Section class. The Section has 3 constructor overloads which might cause some problems. The overloads are:

  • string, OnGUIDelegate
  • string, Texture, OnGUIDelegate
  • GUIContent, OnGUIDelegate

The one that would make the least problems would be the second one as it’s the only one with 3 parameters.

Once you have your sections created, use the list that you got from your “m_Sections” field. Use the Add method of the constructed generic List type to add your sections to the end of the list.

Finally cast the object instance of the PreferencesWindow to “EditorWindow” and call Repaint to refresh the window.

Make sure you only execute this “adding” once for the lifetime of the preferences window.

edit
Just to add to my first simple answer:
Attributes are added to a member or class at compile time. So no, you can’t add or remove attributes at runtime. The actual data of the attribute instance is baked into the code. Here’s the PreferenceItem attribute in IL code. Notice the “.custom instance” at the top. That’s actually the “AttributeUsageAttribute” that is attached to the PreferenceItem itself.

class public auto ansi sealed beforefieldinit UnityEditor.PreferenceItem
	extends [mscorlib]System.Attribute
{
	.custom instance void [mscorlib]System.AttributeUsageAttribute::.ctor(valuetype [mscorlib]System.AttributeTargets) = (
		01 00 40 00 00 00 00 00
	)
	// Fields
	.field public string name

	// Methods
	.method public hidebysig specialname rtspecialname 
		instance void .ctor (
			string name
		) cil managed 
	{
		// Method begins at RVA 0x268f0
		// Code size 14 (0xe)
		.maxstack 8

		IL_0000: ldarg.0
		IL_0001: call instance void [mscorlib]System.Attribute::.ctor()
		IL_0006: ldarg.0
		IL_0007: ldarg.1
		IL_0008: stfld string UnityEditor.PreferenceItem::name
		IL_000d: ret
	} // end of method PreferenceItem::.ctor

} // end of class UnityEditor.PreferenceItem

The bytes you see there is the actual data of the instance in memory. The attribute usage attribute has 4 booleans and one enum value

    01    00    40 00 00 00    00    00
   bool  bool      enum       bool  bool
    |                |
   \|/              \|/
"inherited"      "valid_on"
                     =
           AttributeTargets.Method ( == 0x40 == 64 )

Unfortunately Unity sealed the PreferenceItem class so you can’t derive your own attribute from that. However it wouldn’t help much. Unity would still treat each attribute as seperate section. Also to get the actual attached attribute you would need to go a similar route as explained above. You could extract the attributes from the delegate reference by reading it’s MethodInfo.

In the end it’s questionable if it’s worth all that fiddling.

EDIT- test code added to accepted answer by Glurth:

Amazing! Never would have figured this out without your assistance @Bunny83, many thanks:

These injected tabs show up listed below the normal list of preference tabs, but above any attribute-defined tabs (including “cache Server”!).

This is the init function:

[InitializeOnLoad]
    public class SetupPrefWindowPolling
    { static SetupPrefWindowPolling() { EditorApplication.update += VendorPrefAttribute.CheckForEditorPrefInit; } }

These functions are (arbitrarily) in the attribute class VendorPrefAttribute

public static void DrawTest()
{
    EditorGUILayout.LabelField("test12345");

}
static bool firstPassFlag = true;
static Assembly assemb;
static Type prefWindowType;
static Type sectionType;
static Type onGUIDelegateType;
static Type sectionListType;
static FieldInfo sectionListFieldInfo;
static EditorWindow prefWindow;

static float nextCheckTime;

public static void CheckForEditorPrefInit()
{
    if(firstPassFlag)
    {
        firstPassFlag = false;

        assemb = typeof(EditorWindow).Assembly;
        prefWindowType = assemb.GetType("UnityEditor.PreferencesWindow");
        sectionType = prefWindowType.GetNestedType("Section", BindingFlags.NonPublic);
        onGUIDelegateType = prefWindowType.GetNestedType("OnGUIDelegate", BindingFlags.NonPublic);
        sectionListType = typeof(List<>).MakeGenericType(new Type[1] { sectionType });

        sectionListFieldInfo = prefWindowType.GetField("m_Sections", BindingFlags.Instance | BindingFlags.NonPublic);
    }
    if (nextCheckTime < Time.realtimeSinceStartup)
    {
        nextCheckTime = Time.realtimeSinceStartup + 2.0f;
        EditorWindow[] prefWindowArray = Resources.FindObjectsOfTypeAll(prefWindowType) as EditorWindow[];

        if (prefWindowArray != null && prefWindowArray.Length > 0)
        {
            prefWindow = prefWindowArray[0];
            object sectionListMemberRef = sectionListFieldInfo.GetValue(prefWindow);
            if (sectionListMemberRef != null)
            {
                Debug.Log("EditorPrefs Open and initialized. Injecting Sections");
                InjectVendorTab(DrawTest, "TestVendor"); //for now, test one section injection

                EditorApplication.update -= CheckForEditorPrefInit;
            }
        }
    }
}
static void InjectVendorTab(PreferencesGUI drawing_callback,string VendorName)
{

    object[] SectionConstructorParams = new object[3];
    SectionConstructorParams[0] = VendorName;
    SectionConstructorParams[1] = new Texture2D(0,0);
    SectionConstructorParams[2] = Delegate.CreateDelegate(onGUIDelegateType, drawing_callback.Method, false);

    object InjectedSection = System.Activator.CreateInstance(sectionType, SectionConstructorParams);
    
    object sectionListMemberRef= sectionListFieldInfo.GetValue(prefWindow);
    MethodInfo addMethod = sectionListType.GetMethod("Add");
    addMethod.Invoke(sectionListMemberRef, new object[1] { InjectedSection });

    prefWindow.Repaint();

}