Search Unity

UI Builder merging multiple tools into a single tool

Discussion in 'UI Toolkit' started by MaskedMouse, Mar 12, 2020.

  1. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    I'm kind of wondering if you've made a few small tools already put the code behind it and then go for a bigger tool which combines multiple tools in one single window.
    How do you clone the tree but still let it use the code behind it that you've already written?

    Example:

    Tool A uxml has a button which outputs a debug log "Hello I am tool A"
    Tool B uxml has a button which outputs a debug log "I'm tool B"

    Tool C uxml I add Tool A uxml and Tool B uxml. But how to bind the functionality again which reside in their own tool? Will I have to write duplicate code or maybe bind via a static method?
     
  2. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    If you use UIElements' query system (ie. myRoot.Q<Button>("my-button")), even if your Button is now part of an instance of a UXML inside another parent UXML, that query will still work, so the code/bindings should still work. You just have to be more specific with your element names.

    You also have the option to bundle a UXML layout (with USS styling) _and_ functionality into a stand-alone custom control - a C# element that inherits and extends the VisualElement class. You can then clone the UXML in your constructor and make sure all the bindings are done there. You can then expose this custom element, by C# type name, to UXML so you can use it on other UXML documents like any other tag.

    If you find yourself duplicating things there's likely a way to avoid it. Feel free to post specific examples here.
     
  3. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    My example would be an ADB tool

    I have a few buttons which run an ADB command.
    Uninstall APK, Install APK and Run APK
    I have this in its own ADBTools uxml, its cs is an editor window.

    Now I want to make a build tool which shows all relevant project information that is set and add the ADBTools.uxml to it so you don't have to open the ADB tool window separately. But drag dropping in other uxml's doesn't make the UI functional as all the initialization / bindings happens in its own ADBTools.cs. Not in the BuildTools.cs

    So yes I could query for all ADB tool elements again in the BuildTools but I cannot bind the functionality that has already been defined in ADBTools.cs.

    From what I can interpret of what you said, ADBTools should extend the
    VisualElement
    class rather than a
    EditorWindow
    class. Then the uxml should refer to the C# type name?
    Then I should be able to instantiate the ADB toolset (fully functional) in any UI Elements window?
     
  4. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    Yes, exactly. This sounds like a scenario where a custom C# element would make more sense.

    Yep!
     
  5. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    Hmm, I've tried setting it up and still can't get it to work.

    So what I have is:
    Code (CSharp):
    1. public class ADBTool : VisualElement
    2. {
    3.     public new class UxmlFactory : UxmlFactory<ADBTool>{}
    4.     public ADBTool()
    5.     {
    6.         Debug.Log("ADB Tool initialize");
    7.      
    8.         var doButton = this.Q<Button>("DoButton");
    9.         if (doButton != null)
    10.             doButton.clickable.clicked += () => Debug.Log("Clicked a button");
    11.     }
    12. }
    An ADBTool.uxml
    Code (CSharp):
    1. <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
    2.     <ui:VisualElement class="ADBTool">
    3.         <Style path="Assets/EyeQ/Editor/UIElements/ADB/ADBTool.uss" />
    4.         <ui:Label text="Label" />
    5.         <ui:Button text="Button" name="DoButton" />
    6.     </ui:VisualElement>
    7. </ui:UXML>
    And a BuildTools.uxml where I want to use the ADBTool
    Code (CSharp):
    1. <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements">
    2.     <ui:Template name="ADBTool" path="Assets/EyeQ/Editor/UIElements/ADB/ADBTool.uxml" />
    3.     <ui:Instance template="ADBTool" />
    4. </ui:UXML>
    5.  
    The template is instantiated but the constructor isn't executed so it doesn't bind the button.
    What am I doing wrong?
     
  6. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    The "class" attribute of VisualElement is not used for this purpose. It's used to assign it USS classes that are then used for styling (in USS selectors).

    Here's an example of a custom C# element:

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEditor;
    3. using UnityEditor.UIElements;
    4. using UnityEngine;
    5. using UnityEngine.UIElements;
    6.  
    7. namespace MyCustomNamespace
    8. {
    9.     internal class ABDTool : VisualElement
    10.     {
    11.         public enum Existance
    12.         {
    13.             None,
    14.             Good,
    15.             Bad
    16.         }
    17.  
    18.         public new class UxmlFactory : UxmlFactory<BuilderAttributesTestElement, UxmlTraits> { }
    19.  
    20.         public new class UxmlTraits : VisualElement.UxmlTraits
    21.         {
    22.             UxmlStringAttributeDescription m_String = new UxmlStringAttributeDescription { name = "string-attr", defaultValue = "default_value" };
    23.             UxmlFloatAttributeDescription m_Float = new UxmlFloatAttributeDescription { name = "float-attr", defaultValue = 0.1f };
    24.             UxmlDoubleAttributeDescription m_Double = new UxmlDoubleAttributeDescription { name = "double-attr", defaultValue = 0.1 };
    25.             UxmlIntAttributeDescription m_Int = new UxmlIntAttributeDescription { name = "int-attr", defaultValue = 2 };
    26.             UxmlLongAttributeDescription m_Long = new UxmlLongAttributeDescription { name = "long-attr", defaultValue = 3 };
    27.             UxmlBoolAttributeDescription m_Bool = new UxmlBoolAttributeDescription { name = "bool-attr", defaultValue = false };
    28.             UxmlColorAttributeDescription m_Color = new UxmlColorAttributeDescription { name = "color-attr", defaultValue = Color.red };
    29.             UxmlEnumAttributeDescription<Existance> m_Enum = new UxmlEnumAttributeDescription<Existance> { name = "enum-attr", defaultValue = Existance.Bad };
    30.  
    31.             public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
    32.             {
    33.                 get { yield break; }
    34.             }
    35.  
    36.             public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
    37.             {
    38.                 base.Init(ve, bag, cc);
    39.                 var ate = ve as BuilderAttributesTestElement;
    40.  
    41.                 ate.Clear();
    42.  
    43.                 ate.stringAttr = m_String.GetValueFromBag(bag, cc);
    44.                 ate.Add(new TextField("String") { value = ate.stringAttr });
    45.  
    46.                 ate.floatAttr = m_Float.GetValueFromBag(bag, cc);
    47.                 ate.Add(new FloatField("Float") { value = ate.floatAttr });
    48.  
    49.                 ate.doubleAttr = m_Double.GetValueFromBag(bag, cc);
    50.                 ate.Add(new DoubleField("Double") { value = ate.doubleAttr });
    51.  
    52.                 ate.intAttr = m_Int.GetValueFromBag(bag, cc);
    53.                 ate.Add(new IntegerField("Integer") { value = ate.intAttr });
    54.  
    55.                 ate.longAttr = m_Long.GetValueFromBag(bag, cc);
    56.                 ate.Add(new LongField("Long") { value = ate.longAttr });
    57.  
    58.                 ate.boolAttr = m_Bool.GetValueFromBag(bag, cc);
    59.                 ate.Add(new Toggle("Toggle") { value = ate.boolAttr });
    60.  
    61.                 ate.colorAttr = m_Color.GetValueFromBag(bag, cc);
    62.                 ate.Add(new ColorField("Color") { value = ate.colorAttr });
    63.  
    64.                 ate.enumAttr = m_Enum.GetValueFromBag(bag, cc);
    65.                 var en = new EnumField("Enum");
    66.                 en.Init(m_Enum.defaultValue);
    67.                 en.value = ate.enumAttr;
    68.                 ate.Add(en);
    69.             }
    70.         }
    71.  
    72.         public string stringAttr { get; set; }
    73.         public float floatAttr { get; set; }
    74.         public double doubleAttr { get; set; }
    75.         public int intAttr { get; set; }
    76.         public long longAttr { get; set; }
    77.         public bool boolAttr { get; set; }
    78.         public Color colorAttr { get; set; }
    79.         public Existance enumAttr { get; set; }
    80.     }
    81. }
    82.  
    If you have no need for custom attributes, the main thing you need in your class is:
    Code (CSharp):
    1. public new class UxmlFactory : UxmlFactory<ABDTool, UxmlTraits> { }
    Then, in UXML, you would just do:

    Code (CSharp):
    1. <UXML>
    2.     <MyCustomNamespace.ABDTool />
    3. </UXML>
    4.  
    5. or..
    6.  
    7. <UXML xmlns:mcn="MyCustomNamespace">
    8.     <mcn:ABDTool />
    9. </UXML>
     
    MaskedMouse likes this.
  7. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    Right. So after reading the documentation & your input very carefully multiple times .

    I now have a
    ADBTool : VisualElement
    which has a
    new class UxmlFactory : UxmlFactory<ADBTool>


    As it doesn't have attributes... and functionality needs to be bound to the buttons / input fields etc.
    I used the constructor of the
    ADBTool
    class to initialize it's functional bindings.

    Now I was wondering, why does my UI not appear. Well this is because code & UI are separated. In the UI Builder there was an empty name with my ADBTool, I dragged that in.

    The uxml became:
    Code (CSharp):
    1. <ADBTool>
    2.         <Style path="Assets/Editor/UIElements/BuilderTool.uss" />
    3.     </ADBTool>
    The class is constructed (instantiated), but not the UI itself. So what I did was in the constructor of the ADBTool, load the uxml of the ADBTool, clone it and add it onto itself.

    So if for any reason someone else is struggling here's an example:

    Code (CSharp):
    1. public class CustomVisualElementClassName : VisualElement
    2. {
    3.    // If you have no uxml attributes
    4.    public new class UxmlFactory : UxmlFactory<CustomVisualElementClassName> {}
    5.  
    6.    /// Constructor to initialize this visual element instance
    7.    public CustomVisualElementClassName()
    8.    {
    9.        // Root UXML of your custom visual element, which can be at any given path inside an Editor folder
    10.        var rootUxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/UIElements/CustomUI.uxml");
    11.        var root = rootUxml.CloneTree();
    12.      
    13.        // Add the Style Sheet
    14.         var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/UIElements/CustomUI.uss");
    15.         root.styleSheets.Add(styleSheet);
    16.      
    17.        // Add the root instance to itself because this is a visual element container.
    18.        Add(root);
    19.      
    20.        // Initialize your visual element bindings
    21.        // This requires a button with the Name property set to: CustomButton
    22.        var button = root.Q<Button>("CustomButton");
    23.        // Bind the button clicked functionality by subscribing
    24.        if(button != null) button.clickable.clicked += () => Debug.Log("Button binding works!");
    25.      
    26.        // Sadly we cannot use button?.clickable.clicked because it is an assignment and not an accessor
    27.        // You could create a class which extends Button, which binds the clickable.clicked the same way as above.
    28.        // and then be able to do something like this: button?.BindOnClick(() => Debug.Log("Bind!"));
    29.    }
    30. }
    31.  
     
    Last edited: Mar 27, 2020
    Foxaphantsum likes this.
  8. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    Yep, this is exactly the pattern for custom C# elements with their own internal UI tree.

    Small tip, you can simply use:
    Code (CSharp):
    1. rootUxml.CloneTree(this);
    to clone the contents of the UXML inside your custom element without creating another level (for the TemplateContainer)
     
  9. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    Ok, so with 2019.3 the UI Builder has a
    Binding Path
    . Reading the documentation carefully it requires a
    SerializedObject
    . However now that my
    ADBTool
    inherits from
    VisualElement
    I cannot pass the
    ADBTool
    as
    SerializedObject
    . With the
    EditorWindow
    it could've been passed as
    SerializedObject
    .
    So how should I bind the ADBTool properties?

    I got an idea but it requires a separate inner data class which could serve as
    SerializedObject
    .
    I know I can use the Value Changed events and hook in to those but just wondering, can I somehow still pass the ADBTool to the binding?

    Because
    new SerializedObject(this)
    does not seem to work for VisualElement based classes.