Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Custom binding not showing "Apply to Prefab" context menu

Discussion in 'UI Toolkit' started by fipsy, Nov 25, 2022.

  1. fipsy

    fipsy

    Joined:
    Sep 3, 2016
    Posts:
    13
    Hey,
    I have created a custom bindable ui element. Inside the inspector, the visuals work correctly when overriding the property on a prefab (i.e. the ui is displayed as bold and blue) BUT the "Apply to Prefab" context menu is missing e.g. when right clicking on the label.

    I have created the following ui element which allows me to bind an enum to a simple int field:
    Code (CSharp):
    1. public class EnumAsIntField : BindableElement, INotifyValueChanged<int>
    2.     {
    3.         private EnumField _enumField;
    4.         private int _value;
    5.         private Type _enumType;
    6.         public EnumAsIntField(string label, Type enumType)
    7.         {
    8.             _enumType = enumType;
    9.             _enumField = new EnumField(label, IntToEnum(_enumType, 0));
    10.             SetValueWithoutNotify(0);
    11.             _enumField.RegisterValueChangedCallback(evt =>
    12.             {
    13.                 value = Convert.ToInt32(evt.newValue);
    14.             });
    15.             Add(_enumField);
    16.         }
    17.  
    18.         public void SetValueWithoutNotify(int newValue)
    19.         {
    20.             _value = newValue;
    21.             _enumField.SetValueWithoutNotify(IntToEnum(_enumType, _value));
    22.         }
    23.  
    24.         private Enum IntToEnum(Type enumType, int intValue)
    25.         {
    26.             return (Enum)Enum.ToObject(enumType, intValue);
    27.         }
    28.  
    29.         public int value
    30.         {
    31.             get => _value;
    32.             set
    33.             {
    34.                 if(value.Equals(this.value))
    35.                     return;
    36.                 var previous = this.value;
    37.                 SetValueWithoutNotify(value);
    38.  
    39.                 using (var evt = ChangeEvent<int>.GetPooled(previous, value))
    40.                 {
    41.                     evt.target = this;
    42.                     SendEvent(evt);
    43.                 }
    44.             }
    45.         }
    46.     }
    I use the following component and inspector to make the field appear:
    Code (CSharp):
    1. public class EnumTest : MonoBehaviour
    2.     {
    3.         public int MyEnumAsInt;
    4.     }
    5.  
    6.     [Serializable]
    7.     public enum MyTestEnum
    8.     {
    9.         Val1,
    10.         Val2,
    11.         Val3,
    12.         Val4,
    13.     }
    Code (CSharp):
    1. [CustomEditor(typeof(EnumTest))]
    2. public class EnumTest_Inspector : Editor
    3. {
    4.     public override VisualElement CreateInspectorGUI()
    5.     {
    6.         var enumField = new EnumAsIntField("label", typeof(MyTestEnum));
    7.         enumField.bindingPath = "MyEnumAsInt";
    8.         return enumField;
    9.     }
    10. }
    Can anyone tell me what I'm missing?
    Cheers
     
    oscarAbraham likes this.
  2. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    UITK is currently lacking a lot of functionality related to prefab overrides. A lot of basic prefab stuff that could be done with IMGUI can't be done with UITK.

    Currently, only Foldouts and fields that inherit from BaseField get the property context menu that you are missing. So you could solve your specific issue by inheriting from BaseField<int>. Another current limitation is that the menu is only added to the label (element's without a label can't access this menu), so make sure you use the BaseField's label functionality, or that you have a Label with the labelUssClassName.

    Another workaround that I've used is to use the header of a heavily modified Foldout as a Label to get that menu. This is very dirty, but it's the only practical solution in some cases (i.e. when one needs to access the menu of a composite property).

    Finally, one could use reflection to make an element that mimics BeginProperty/EndProperty blocks from IMGUI (except for the mixed value thing). It seems everything needed is inside this class. I'll probably do it if no other solution comes up. If I end up doing it, I'll post it here.
     
    Last edited: Nov 28, 2022
    fipsy likes this.
  3. fipsy

    fipsy

    Joined:
    Sep 3, 2016
    Posts:
    13
    Thank you very much for your detailed answer, I really appreciate it!
    Just yesterday I tried to reuse the context menu of a lable by sending it a fake right click event whenever I right click on the custom component. Eventough I seem to synthesize the fake event correctly it does not work. Maybe still another approach you want to investigate...
     
    oscarAbraham likes this.
  4. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    I really like that idea; it sounds much easier than reflection. One could use a Foldout instead of a Label for better support. I'll try it and report back.
     
  5. fipsy

    fipsy

    Joined:
    Sep 3, 2016
    Posts:
    13
    Damn... now I feel stupid. Spent half a day figuring out why my idea with the fake event was not working, only to find out that I was listening/sending the wrong event the whole time.... it's the UP event, not DOWN :confused:

    Anyway, here is my working solution to show the "Apply to Prefab" context menu on any element you want.

    We basically register a callback for a right click on our target element. Inside this callback we create a copy of the element and send it to a label/foldout element to trigger its context prefab menu functionality. Here is an example of an ObjectField that hides its label but still has the right click functionality:

    Code (CSharp):
    1.         var objField = new ObjectField("My Label");
    2.         objField.objectType = typeof(Object);
    3.         objField.bindingPath = "MyObject";
    4.      
    5.         objField.labelElement.style.display = new StyleEnum<DisplayStyle>(DisplayStyle.None); // hide the label
    6.      
    7.         // register event callback for "right click up" on the sub element of the object field
    8.         objField.Q(null, "unity-object-field-display").RegisterCallback<PointerUpEvent>(evt =>
    9.         {
    10.             // is it a right click?
    11.             if (evt.button == 1)
    12.             {
    13.                 // copy the event and send it to the hidden label
    14.                 using (PointerUpEvent labelEvent = PointerUpEvent.GetPooled(evt))
    15.                 {
    16.                     labelEvent.target = objField.labelElement;
    17.                     objField.panel.visualTree.SendEvent(labelEvent);
    18.                 }
    19.             }
    20.         });
    So I hope this helps some of you (and somewhat justifies the time I have wasted on this problem).

    I think the solution for my original problem will be to also bind a standard IntegerField or a foldout to the property, hide it and use its label to show the menu.

    It goes without saying that this is a very hacky workaround for a problem that should not exist in the first place. So please Unity devs, if you see this, give us more options for prefab functionality and binding in general.
     
    Last edited: Nov 28, 2022
    oscarAbraham likes this.
  6. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    I've just finished implementing a more generalized solution, using your idea of replicating the mouse event, so you could use that. Here it is. I made it without any dependencies on the rest of the package, so you can just copy that class. I have only tested it using it as is, but I think it should work for your EnumAsIntField if you inherit from it instead of inheriting from BindableElement.

    Right. Please, if you see this, here are some other notes:

    While making the PropertyContainer, I've found bugs with the implementation in BindingStyleHelpers. It doesn't work correctly in macOS. It never unregisters right click from Foldouts because the relevant overload of UnregisterRightClickMenu is missing. This line (250) should use 0 instead of xMin; 0 is left in local space, xMin is left in the local space of the parent. Also, it'd be nice if it used ContextualMenuPopulateEvent, like I reported here.

    More generally, my two cents is that UITK could really use some validation by usage in the Editor. 2022.2 makes all default inspectors use UITK, but Unity's own custom inspectors are still done with IMGUI. I think that this case would be different if Unity's custom editors were made with UITK, and people found out sooner that they can't really use UnityEvents with prefab overrides.

    There's lots of little issues that maybe could've been avoided with some validation by usage. For example, yesterday I found out that deleting from a ListView is buggy, and it's been reported in the forum for a year; the console prints errors when an element is removed and then the list is reordered.

    I believe some missing features would be very noticeable if Unity used UITK more in their own Editors. For example, we are really missing an overload of CreatePropertyGUI that receives the label.

    Don't get me wrong, I absolutely love UITK, and I think the work the UITK team has done is marvelous. This is just just something I think could help from my experience of using UIToolkit a lot in the Editor.
     
    Last edited: Nov 29, 2022