Documentation:Creating a Custom Editor Window for Scriptable Object Generation

From UBC Wiki
Emerging Media Lab
UBC Emerging Media Lab Signature.png
About EML
A collaborative space for UBC faculty, students and staff for exploration of emerging technologies and development of innovative tools and solutions. This wiki is for public-facing documentation for our projects and procedures.
Subpages



Introduction

This page is a high-level guide and reference for developers aiming to create a custom editor window in Unity using UI Toolkit, giving an overview of all important topics including UXML, USS, C#. Though the official Unity documentation, tutorials, and non-official tutorials often have more specific and detailed information, this guide serves as a tour through the issues and knowledge encountered by a developer when trying to create a custom editor window for the first time, using examples from the AnimationDescription generation window. Therefore, it may set developers on the right track as an intuitive and non-fragmented guide.

Initial Setup for a Custom Window

Inside the Assets folder, create a folder called Editor. This is a special folder which contains scripts for the Unity Editor, as opposed to runtime.

To develop a custom window, one must create a C# script containing a class which extends UnityEditor.EditorWindow. Create this file and place it inside the Editor folder. Alternatively, going to Assets->Create->UI Toolkit->Editor Window will allow you to create a C# script (with boilerplate code), UXML document, and USS document at the same time.

public class CustomWindowClass : EditorWindow
{
	// specify the menu item and hotkey combination to open your window
	[MenuItem(“Window/UI Toolkit/CustomWindowClass”)]
	public static void ShowWindow()
	{
            CustomWindowClass wnd = GetWindow<CustomWindowClass>();
            wnd.titleContent = new GUIContent("Create a new Window");
	}
	
	public void OnEnable()
	{
		// each editor window contains a root VisualElement field which is the root of the visual tree
		VisualElement root = rootVisualElement;
		Label lab = new Label(“Example”);
		root.Add(lab);

	}

	public void CreateGUI()
	{

	}

}

There are many ways to get your custom window to display when you click on the menu option or press the hotkey combination. Refer to the EditorWindow documentation for the various methods. In particular, the MenuItem constructor seen in the code above can include special characters in the string to represent a key combination. One can also use ShortcutManagement.

After setting up the window in ShowWindow, either the CreateGUI or OnEnable methods would be where you load the initial GUI layout. Think of it as a constructor for the GUI. All editor windows contain a visual tree, and the inherited field rootVisualElement from EditorWindow is the root of the visual tree. In the code sample above, the VisualElement.Add method called on root adds a Label to the tree. Opening the window at this point should show the Label. While a static GUI layout can be created using C# like this, there is an easier way: specifying the layout in a UXML document.

Working with UXML for GUI layout

UXML is a markup language with essentially the same syntax as XML. It specifies the visual tree using a tree structure made of XML elements. Each of these elements has a tag indicating the type of the corresponding VisualElement and various attributes depending on the type. These attributes are typically a subset of the fields of the class. A reference can be found here. An element can enclose only itself or it may enclose an arbitrary number of child elements.

To create a UXML document, go to Assets->Create->UI Toolkit->UI Document.

The visual tree is a hierarchy in which each VisualElement contains an indexed list of VisualElements (children). Children are contained within parents, and parents act as a flexbox. This means that children are arranged horizontally or vertically in order of their index and the parent will stretch to fit them. Children added to a parent are added at the end of the list.

A UXML document contains one optional metadata element followed by an element with the tag UXML. This element has metadata attributes, notably the XML namespace(s). Within this element, the top-level children are contained.

XML Namespaces

There are two main namespaces for Unity UI classes, and you may need to use UI elements from both. The namespaces used in a UXML document must be specified in the attributes of the top-level element. The easiest way to do so is to set the default namespace xmlns as either UnityEngine.UIElements or UnityEditor.UIElements and create an arbitrary alias for the other one. An example using the alias editor can be seen in the next code sample. Also, note that elements from the default namespace do not need to be declared with a namespace specifier, but elements from other namespaces do. This may be simpler than the default code that appears when a UXML document is created.

UXML Templates

UXML Templates are UXML documents stored in separate files and can be instantiated in other UXML documents and in C#. This is useful for keeping UXML DRY.

<?xml version="1.0" encoding="UTF-8" ?>
<UXML xmlns="UnityEngine.UIElements"
      xmlns:editor="UnityEditor.UIElements"
>
  <VisualElement class="obj-state-pair">
        <!-- note specifying editor namespace -->
        <editor:ObjectField name="objField" type="UnityEngine.GameObject, UnityEngine.CoreModule" text="" class="object-field"/>   
        <editor:ToolbarMenu name="dropdown" text="Select State" class="trigger-menu"/>
        <Button name="remove-pair" class="add-remove" text="-"/>
  </VisualElement>
</UXML>

Instantiation of the template within another UXML file requires the path to the template from Assets. Then, the template can be used like any other element.


<Template path="Assets/Editor/ObjectStatePairTemplate.uxml" name="obj-state-pair-template"/>
UXML context...
<Instance template="obj-state-pair-template" name="bla"/>
<Instance template="obj-state-pair-template" name="bla"/>
<Instance template="obj-state-pair-template" name="bla"/>

Working with USS for element styling

Unity Editor custom windows use USS to style elements. USS stylesheets are contained in separate files, or can be set in C#. For the purposes of maintainability and standardization, it is recommended to keep stylesheets separate. To create a USS file, go to Assets->Create->UI Toolkit->Style Sheet.

USS is a strict subset of CSS with a relatively small number of supported properties. A CSS or USS property is simply an aspect of style (e.g. width, height, font size) applied to a VisualElement. USS documents consist of styles made of properties, and each style can be assigned selectors. Selectors specify which visual elements get which styles. Note that selectors have different precedence and generally the most specific selector determines which styles get applied. A VisualElement's assigned stylesheet also applies to all of its children. Therefore, a stylesheet added to the root applies to everything.

Commonly Used Properties

There are a few commonly used properties and some common pitfalls. width and height are essential, but for reactive interfaces always use %, which is relative, instead of px, which is an absolute number of pixels. The percentage data type defines the size relative to the size of the parent element. Margins such as margin-left will need to use an absolute number of pixels if they are to be fixed. One problem with applying a style with a margin property to all elements is that child elements will have the margin applied more than once (one for each parent layer which has the margin applied), so they are more heavily indented than their parents, even if all elements are meant to be flush. This can be solved by setting the child's margin to zero. The default flex direction is column, so flex-direction: row; should be applied to containers whose children should be arranged in a row.

USS Best Practices

USS Best Practices are similar to CSS best practices and have the goal of keeping the code DRY to reduce bugs and increase maintainability. Rather than thinking of a stylesheet as a place to put a class for every VisualElement, think of it as a collection of styles, each style being applicable to multiple VisualElements. This may involve using less-specific USS selectors, or assigning more than one USS class to one style, and more than one style to each USS class. This should result in minimal code duplication.

A simple example is shown here:

Button {
    margin-left: 10px;
    margin-right: 10px;
    margin-bottom: 10px;
}
Label {
    margin-left: 10px;
    margin-right: 10px;
    margin-bottom: 10px;
}
Button, Label {
    margin-left: 10px;
    margin-right: 10px;
    margin-bottom: 10px;
}

In the example below, there are multiple references to .label-tooltip-pair because it shares a style with other classes, but also has a unique property by itself. This shows multiple styles per class (or selector). Also, the bottom style is shared by multiple classes.

.label-tooltip-pair {
    justify-content: space-between;
}
#FLEX-ROW,
.obj-state-pair,
.label-tooltip-pair,
.flex-row
{
    display: flex;
    flex-direction: row;
    align-items: center;
    margin-left: 10px;
    margin-right: 10px;
    height: 100%;
    width: 100%;
}

Working with USS/CSS for the first time typically takes a lot of trial and error to build intuition for how the system works and what best practices are. The recommended workflow is to only change one thing at a time and constantly re-render the custom window to see the effects.

UI Builder for graphical editing of UXML, USS

UI Builder is a GUI package for editing static GUI using a drag-and-drop approach. It automatically generates UXML and USS documents according to the layout the user creates. This can speed up the process of window creation and trial-and-error during such. Use of UI Builder is recommended after developers learn to manually write UXML, USS, and is usually compatible with manual editing of such.

Loading UXML and USS

To load UXML, USS and add it to the root visual element, use the AssetDatabase interface to load from the file path containing those files. It is recommended that the entire visual tree uses the same stylesheet.

 
var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/CreateAnimDescription.uss");
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/CreateAnimDescription.uxml");
layout = visualTree.Instantiate(); // turns the asset into an actual VisualElement
root.styleSheets.Add(styleSheet);
root.Add(layout);

Loading a UXML template is similar:

        VisualElement pair = AssetDatabase
             .LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/ObjectStatePairTemplate.uxml")
             .Instantiate();


Working with C# for business logic and dynamic GUI layout

Accessing the Visual Tree

After a UXML document has been loaded into the root visual element, it is active and ready for display. However, the UI layout is still accessible and editable. To find a VisualElement in the hierarchy, use the VisualElement.Query<Type> method. This queries the subtree of the visual tree rooted at the specified element and tries to find element(s) matching the given type and parameters. Once you have a reference to an element or its parent, you can dynamically remove it using VisualElement.RemoveFromHierarchy. You can also dynamically insert new elements, including UXML template instantiations at a specific location using an index using VisualElement.Insert. One example would be a TextField that has a warning appear below whenever the contents are invalid.

// layout must be the parent visual element of the TextField for IndexOf to work.
duplicateWarningLabel = new Label("invalid");
var idx = layout.IndexOf(root.Query<TextField>("name-of-text-field"));
layout.Insert(idx + 1, duplicateWarningLabel);

Callback Functions

Unity’s UI Toolkit classes follow an event-based asynchronous paradigm, where the user interacting with UI elements triggers various event types which can have callback functions registered. For example, opening a link in the browser when a button is clicked.

Callback functions in C# have a closure, a capturing of local variables, which means that they can access local variables in the greater method scope. These local variables will be the specific instances that were in scope when the closure was created. This is very useful; don't think that the only information a callback can access is globals/fields and arguments as that is very restrictive.

While creating a custom window, you may have to nest callbacks several levels deep (callbacks within callbacks). However, very deep nesting is unreadable and should be refactored.

A useful method for setting callbacks on elements is VisualElement.RegisterCallback<EventType> or VisualElement.RegisterValueChangedCallback.

Here is an example of accessing the visual tree and attaching a callback:

        root.Query<Button>("tooltip").ForEach((Button b)
            => b.clickable.clicked
            += () => Application.OpenURL(WIKI_LINK));

Interacting with the File System (Saving/Reading)

Interacting with the file system to save and read assets (AnimationDescriptions in the case of the generator window) is similar to loading USS and UXML into the GUI. This is enabled by a few interfaces such as System.IO, EditorUtility, AssetDatabase. To save a scriptable object, create an instance and then save to the AssetDatabase.

        AnimationDescription ad = ScriptableObject.CreateInstance<AnimationDescription>();
        AssetDatabase.CreateAsset(ad, "Entire path including filename and extension");
        AssetDatabase.SaveAssets();

To open a folder panel that lets you select a folder, which can then be passed to an element, use EditorUtility.OpenFolderPanel. This returns an absolute path, and you can use string manipulation to get a relative path.

        string absPath = EditorUtility.OpenFolderPanel("Select a Folder", ROOT, "");
        if (absPath != "")
        {
            savePath = absPath.Substring(absPath.IndexOf(ROOT));
        }

To examine the contents of a directory, such as to determine if a file exists, use System.IO.Directory

    private static bool IsDuplicateFileName(string savePath, string fileName)
    {
        if (savePath == "" || fileName == "") return false;
        return System.IO.Directory.GetFiles(savePath, fileName + EXTENSION).Any();
    }

Design Patterns and Practices

Reasoning about parts of GUI functionality as a state machine, or in other words using a simple version of State Pattern, can simplify development and eliminate bugs. For example, saveability for the generation window depends on the name being set, the path being set, and the name not being a duplicate. The unsaveable state also has a slightly different GUI layout with a warning and different text. A good way to approach this is to have a single function responsible for deciding what state the window is in, if it should transition, and also handle the transition.

As always, it is also good to follow general best practices such as DRY or YAGNI. DRY in this case can be not duplicating UXML and USS, but also being aware of duplicating string constants between UXML and C#. This should be kept to a minimum. For YAGNI, try to write the simplest logic first and don't use any superfluous data structures.

Elements Used in Generation Window

Here are some notes about elements used in the generation window and which are generally useful. Not all elements used are included here, only those that have non-obvious existence or usage notes.

VisualElement

The most basic kind of element like a div in HTML. Very flexible and useful as a container for other elements, while not being visible itself. Other uses include developing a mini-window system within the custom window.

ToolbarMenu

ToolbarMenu is a typical drop-down menu where the user clicks on the menu to expand it, and then clicks on an item in the menu to select it. This is used for selecting animator controller state machine behaviours in the generation window. It uses an internal field which actually contains the data called menu. menu.AppendAction lets you add items to the dropdown. Beware that ToolbarMenu's interface does not have any way to get the last item clicked on. The developer must instead save the item themselves using the callback that is triggered when the user clicks on an item. One good way to save is to set the text field of the ToolbarMenu. This way, the value is saved and also displays on the collapsed menu.

Button

A typical button which can display text and handle various events. A shortcut method for assigning click event callbacks to buttons is button.clickable.clicked += callbackfn .

ScrollView

A scrollview is a container element for an arbitrarily long list of children which can scroll to display all of the children. In the generation window this is used to hold object-state pairs.

External Resources

Generator Window Github Repository

UXML

UXML Elements Reference

Introduction to UXML format

Loading UXML in C#

Writing UXML templates

Block comment in HTML, XML

USS

Supported Properties

UI Toolkit

Overview

Guided introductory tutorial custom window

Another tutorial

UI Builder

EditorWindow

Unity Editor

How to use Visual Studio with Unity

Creating and Using Scripts

Related Pages

Documentation:AnimationDescription Generating Window: User Manual

Related Projects

Documentation:Metabolism

Interactive Animation Toolkit for Unity

License

Some rights reserved
Permission is granted to copy, distribute and/or modify this document according to the terms in Creative Commons License, Attribution-ShareAlike 4.0. The full text of this license may be found here: CC by-sa 4.0
Attribution-Share-a-like