Wayback Machinekoobas.hobune.stream
May JUN Jul
Previous capture 12 Next capture
2021 2022 2023
1 capture
12 Jun 22 - 12 Jun 22
sparklines
Close Help
  • Products
  • Solutions
  • Made with Unity
  • Learning
  • Support & Services
  • Community
  • Asset Store
  • Get Unity

UNITY ACCOUNT

You need a Unity Account to shop in the Online and Asset Stores, participate in the Unity Community and manage your license portfolio. Login Create account
  • Blog
  • Forums
  • Answers
  • Evangelists
  • User Groups
  • Beta Program
  • Advisory Panel

Navigation

  • Home
  • Products
  • Solutions
  • Made with Unity
  • Learning
  • Support & Services
  • Community
    • Blog
    • Forums
    • Answers
    • Evangelists
    • User Groups
    • Beta Program
    • Advisory Panel

Unity account

You need a Unity Account to shop in the Online and Asset Stores, participate in the Unity Community and manage your license portfolio. Login Create account

Language

  • Chinese
  • Spanish
  • Japanese
  • Korean
  • Portuguese
  • Ask a question
  • Spaces
    • Default
    • Help Room
    • META
    • Moderators
    • Topics
    • Questions
    • Users
    • Badges
  • Home /
avatar image
1
Question by Glurth · Aug 24, 2016 at 08:09 PM · editorattributeoptionspreferences

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?

Comment
Add comment
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users

1 Reply

· Add your reply
  • Sort: 
avatar image
1
Best Answer

Answer by Bunny83 · Aug 25, 2016 at 07:02 AM

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();

 }


Comment
Add comment · Show 1 · Share
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users
avatar image Glurth · Aug 25, 2016 at 03:04 PM 0
Share

holy wow- an amazing, AND scary answer!

edit: oh boy. getting stuck on step one (not a good sign). Particularly, getting the PreferencesWindow type. The type is internal, and so I cannot simply use it. Edit:deleted wrong variants.

Ah.. of course the very first varation I try after posting works:

 Assembly assemb = typeof(EditorWindow).Assembly;  //PreferencesWindow is dervied from EditorWindow, which i CAN access
 Type PrefWindowType = assemb.GetType("UnityEditor.PreferencesWindow");

Next issue is why

 UnityEngine.Object o=UnityEngine.Object.FindObjectOfType(PrefWindowType);

returns null- even when the preference window is open! (stuck this code in one of the [PreferenceItem] functions, for initial testing- just to make sure the window was actually open)

hmm, even my sanity check variant generates 0 found objects:

 EditorWindow[] o =UnityEngine.Object.FindObjectsOfType<EditorWindow>();
  Debug.Log("found "+o.Length.ToString()+ " windows");

Got it. Using Resources.Find.., rather than Object.Find.. works: EditorWindow[] o = Resources.FindObjectsOfTypeAll(PrefWindowType) as EditorWindow[];

Your answer

Hint: You can notify a user about this post by typing @username

Up to 2 attachments (including images) can be used with a maximum of 524.3 kB each and 1.0 MB total.

Follow this Question

Answers Answers and Comments

61 People are following this question.

avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image

Related Questions

Multiple attributes 0 Answers

Way of selecting options 0 Answers

Some public attributes not shown in inspector, default references 3 Answers

Unity Restores default Preferences every launch; Fix? 0 Answers

ScriptChangesWhilePlaying option "Stop Playing and Recompile" not working as expected 1 Answer


Enterprise
Social Q&A

Social
Subscribe on YouTube social-youtube Follow on LinkedIn social-linkedin Follow on Twitter social-twitter Follow on Facebook social-facebook Follow on Instagram social-instagram

Footer

  • Purchase
    • Products
    • Subscription
    • Asset Store
    • Unity Gear
    • Resellers
  • Education
    • Students
    • Educators
    • Certification
    • Learn
    • Center of Excellence
  • Download
    • Unity
    • Beta Program
  • Unity Labs
    • Labs
    • Publications
  • Resources
    • Learn platform
    • Community
    • Documentation
    • Unity QA
    • FAQ
    • Services Status
    • Connect
  • About Unity
    • About Us
    • Blog
    • Events
    • Careers
    • Contact
    • Press
    • Partners
    • Affiliates
    • Security
Copyright © 2020 Unity Technologies
  • Legal
  • Privacy Policy
  • Cookies
  • Do Not Sell My Personal Information
  • Cookies Settings
"Unity", Unity logos, and other Unity trademarks are trademarks or registered trademarks of Unity Technologies or its affiliates in the U.S. and elsewhere (more info here). Other names or brands are trademarks of their respective owners.
  • Anonymous
  • Sign in
  • Create
  • Ask a question
  • Spaces
  • Default
  • Help Room
  • META
  • Moderators
  • Explore
  • Topics
  • Questions
  • Users
  • Badges