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 Is it possible to create a dynamic property drawer using UIElements?

Discussion in 'UI Toolkit' started by TheNullReference, Aug 18, 2023.

  1. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    222
    What I mean by dynamic is that it can be changed with methods like a button click. Here's the full context of my code so far:

    Code (csharp):
    1.  
    2. #if UNITY_EDITOR
    3. using System;
    4. using System.Linq;
    5. using System.Reflection;
    6. using UnityEditor;
    7. using UnityEditor.Overlays;
    8. using UnityEditor.UIElements;
    9. using UnityEngine.UIElements;
    10. using UnityEngine;
    11.  
    12. namespace Anvil
    13. {
    14.     [CustomPropertyDrawer(typeof(FunctionSystem<,>))]
    15.     public class FunctionSystemDrawer : PropertyDrawer
    16.     {
    17.  
    18.         private SerializedProperty _property;
    19.         private SerializedProperty _functionsProperty;
    20.         private VisualElement _container;
    21.         private VisualElement _parentContainer;
    22.        
    23.         private VisualElement DrawHeader()
    24.         {
    25.             var headerGroup = new VisualElement();
    26.             headerGroup.style.flexDirection = FlexDirection.Column;
    27.             headerGroup.style.backgroundColor = new Color(0.0f, 0.0f, 0.0f, 0.15f); // Change to your desired color
    28.             headerGroup.Add(new Label("Function System"));
    29.             return headerGroup;
    30.         }
    31.         private VisualElement DrawBody()
    32.         {
    33.             var bodyGroup = new VisualElement();
    34.  
    35.             // Access the _buffer and _functions fields
    36.             var functionsProperty = _property.FindPropertyRelative("_functions");
    37.  
    38.             // for each no null function in the list, draw a custom field for it.
    39.             for (var i = 0; i < functionsProperty.arraySize; i++)
    40.             {
    41.                 var element = functionsProperty.GetArrayElementAtIndex(i);
    42.                 if (element.managedReferenceValue == null) continue;
    43.                
    44.                 // Create a horizontal group
    45.                 var horizontalGroup = new VisualElement
    46.                 {
    47.                     style =
    48.                     {
    49.                         flexDirection = FlexDirection.Row,
    50.                         justifyContent = Justify.SpaceBetween,
    51.                         alignContent = Align.Center
    52.                     }
    53.                 };
    54.  
    55.  
    56.                 // Make the name label a button itself
    57.                 var label = new Label(element.managedReferenceValue.GetType().Name)
    58.                 {
    59.                     style =
    60.                     {
    61.                         unityFontStyleAndWeight = FontStyle.Normal,
    62.                         unityTextAlign = TextAnchor.MiddleCenter
    63.                     }
    64.                 };
    65.                 horizontalGroup.Add(label);
    66.  
    67.                 var buttonGroup = new VisualElement();
    68.                    
    69.                 // make button group horizontal
    70.                 buttonGroup.style.flexDirection = FlexDirection.Row;
    71.                    
    72.                 // Create an up, down, remove and inspect button.
    73.                 var i1 = i;
    74.                 var upButton = new Button(() =>
    75.                 {
    76.                     // Move the function up in the list
    77.                     var index = functionsProperty.GetArrayElementAtIndex(i1);
    78.                     functionsProperty.MoveArrayElement(i1, i1 - 1);
    79.                     _property.serializedObject.ApplyModifiedProperties();
    80.                     _container.Clear();
    81.                     _container.Add(CreatePropertyGUI(_property));
    82.                 });
    83.  
    84.                 var i2 = i;
    85.                 var downButton = new Button(() =>
    86.                 {
    87.                     // Move the function down in the list
    88.                     var index = functionsProperty.GetArrayElementAtIndex(i2);
    89.                     functionsProperty.MoveArrayElement(i2, i2 + 1);
    90.                     _property.serializedObject.ApplyModifiedProperties();
    91.                     _container.Clear();
    92.                     _container.Add(CreatePropertyGUI(_property));
    93.                 });
    94.  
    95.                 var i3 = i;
    96.                 var removeButton = new Button(() =>
    97.                 {
    98.                     // Remove the function from the list
    99.                     var index = functionsProperty.GetArrayElementAtIndex(i3);
    100.                     functionsProperty.DeleteArrayElementAtIndex(i3);
    101.                     _property.serializedObject.ApplyModifiedProperties();
    102.                     _container.Clear();
    103.                     _container.Add(CreatePropertyGUI(_property));
    104.                 });
    105.  
    106.                 upButton.text = "↑";
    107.                 downButton.text = "↓";
    108.                 removeButton.text = "╳";
    109.                    
    110.                 buttonGroup.Add(upButton);
    111.                 buttonGroup.Add(downButton);
    112.                 buttonGroup.Add(removeButton);
    113.                 horizontalGroup.Add(buttonGroup);
    114.                 bodyGroup.Add(horizontalGroup);
    115.  
    116.                 // get the serialized element at index
    117.                 var serializedElement = functionsProperty.GetArrayElementAtIndex(i);
    118.                
    119.                 var endProperty = serializedElement.GetEndProperty();
    120.                 Debug.Log($"End Property: {endProperty.serializedObject.targetObject.GetType().Name}");
    121.  
    122.                 SerializedProperty iterator = serializedElement.Copy();
    123.                 bool enterChildren = true;
    124.                 while (iterator.NextVisible(enterChildren))
    125.                 {
    126.                     Debug.Log($"Iterator: {iterator.propertyType} {iterator.propertyPath} {iterator.name}");
    127.                     enterChildren = false;
    128.  
    129.                     if(iterator.name == "data") continue;
    130.                     // Draw the property using the appropriate field without label
    131.                     bodyGroup.Add(new PropertyField(iterator));
    132.                 }
    133.                
    134.                 serializedElement.serializedObject.ApplyModifiedProperties();
    135.          
    136.             }
    137.  
    138.             // Add a button to body group that adds a new type of function
    139.             var button = new Button(() =>
    140.             {
    141.                 var menu = new GenericMenu();
    142.  
    143.                 // Get the type of the generic type parameter
    144.                 var genericType = fieldInfo.FieldType.GenericTypeArguments[1];
    145.  
    146.                 // Find all classes in the assembly that implement the generic type (for example IWishDirectionHandler)
    147.                 // Find all classes in the assembly that implement the generic type
    148.                 var types = AppDomain.CurrentDomain.GetAssemblies()
    149.                     .SelectMany(s => s.GetTypes())
    150.                     .Where(p => genericType.IsAssignableFrom(p) && !p.IsInterface && !p.IsAbstract);
    151.  
    152.                 // Add a menu item for each type
    153.                 foreach (var type in types)
    154.                 {
    155.                     menu.AddItem(new GUIContent(type.Name), false, () =>
    156.                     {
    157.                         // Add the function to the list
    158.                         var index = functionsProperty.arraySize++;
    159.                         var element = functionsProperty.GetArrayElementAtIndex(index);
    160.                         element.managedReferenceValue = Activator.CreateInstance(type);
    161.                        
    162.                         Refresh();
    163.            
    164.                     });
    165.                 }
    166.                
    167.                 menu.ShowAsContext();
    168.                
    169.             });
    170.             button.text = "Add Function";
    171.             bodyGroup.Add(button);
    172.             return bodyGroup;
    173.         }
    174.  
    175.         private void Refresh()
    176.         {
    177.             // Apply any serialized property changes
    178.             _property.serializedObject.ApplyModifiedProperties();
    179.            
    180.             // Clear the container  
    181.             _container.MarkDirtyRepaint();
    182.             _parentContainer.Clear();
    183.             _parentContainer.Add(CreatePropertyGUI(_property).Children().First());
    184.         }
    185.        
    186.         // Override the CreatePropertyGUI method to provide your custom UI
    187.         public override VisualElement CreatePropertyGUI(SerializedProperty property)
    188.         {
    189.             _property = property;
    190.             _functionsProperty = property.FindPropertyRelative("_functions");
    191.             _parentContainer = new VisualElement();
    192.             _container = new VisualElement();
    193.            
    194.             // Add the header group to the container
    195.             _parentContainer.Add(_container);
    196.             _container.Add(DrawHeader());
    197.             _container.Add(DrawBody());
    198.            
    199.             // apply all properties
    200.             property.serializedObject.ApplyModifiedProperties();
    201.  
    202.             return _parentContainer;
    203.         }
    204.     }
    205. }
    206. #endif
    207.  
    The main focus is the Refresh() function where I try to update the inspector UI as I'm using it. It seems it's only possible to mark the inspector for repaint on the next frame, and as far as I can tell, the only way to trigger the next frame is to click off the gameobject, and click back onto it. This has the effect of the UI essentially being unresponsive.

    Screenshot 2023-08-18 110016.png

    Here is how the UI looks at the moment. Hitting the "X" will remove the element on the serialized object, but the UI will not update until the inspector window is re-visited.
     
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    UI Toolkit doesn't really have the same concept of repainting as IMGUI has, as it has actual state. If you want elements to change, you change them via code.

    This can simply involve binding a UI element to a serialised object/property so that changes in one reflect upon the other.
     
  3. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    222
    Seem like changing the state is the only way to get it to redraw, this seems to work.

    Code (csharp):
    1.  
    2.         private void UpdateUI(SerializedProperty property)
    3.         {
    4.             // Clear the existing UI and redraw it
    5.             _container.Clear();
    6.             _container.Add(DrawHeader());
    7.             _container.Add(DrawBody());
    8.         }
    9.  
    unfortunately it doesn't seem to be updating elements created in the while loop

    Code (csharp):
    1.  
    2.                 SerializedProperty iterator = serializedElement.Copy();
    3.                 bool enterChildren = true;
    4.                 while (iterator.NextVisible(enterChildren))
    5.                 {
    6.                     Debug.Log($"Iterator: {iterator.propertyType} {iterator.propertyPath} {iterator.name}");
    7.                     enterChildren = false;
    8.  
    9.                     if(iterator.name == "data") continue;
    10.                     // Draw the property using the appropriate field without label
    11.                     bodyGroup.Add(new PropertyField(iterator));
    12.                 }
    13.                
    14.                 serializedElement.serializedObject.ApplyModifiedProperties();
    15.  
    It does draw correctly by clicking on and off the behaviour, just not by redrawing the UI, ill keep trying a bit before going back to IMGUI
     
  4. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    TheNullReference likes this.
  5. TheNullReference

    TheNullReference

    Joined:
    Nov 30, 2018
    Posts:
    222
    That did seem to be the problem.

    Code (csharp):
    1.  
    2.                 SerializedProperty iterator = serializedElement.Copy();
    3.                
    4.                 bool enterChildren = true;
    5.                 while (iterator.NextVisible(enterChildren))
    6.                 {
    7.                     // Make sure to set enterChildren to false for subsequent iterations
    8.                     enterChildren = false;
    9.  
    10.                     if (iterator.name == "data") continue;
    11.  
    12.                     Debug.Log("Adding Property to Body!");
    13.                     var propertyField = new PropertyField(iterator.Copy());
    14.                     // Just had to add this line.
    15.                     propertyField.BindProperty(iterator.Copy());
    16.                     bodyGroup.Add(propertyField);
    17.                 }
    18.                 serializedElement.serializedObject.ApplyModifiedProperties();
    19.  
    Binding the field fixed the issue. Thanks!
     
    martinpa_unity and spiney199 like this.