Source/Interface/Views

Views

The Blender interface code includes APIs/systems for easy creation of "views", that is, layouts giving a view on data sets. Currently there are only tree-views.

Tree-View

The Asset Browser's catalog UI is created using the tree-view API.

This kind of view is made for showing data that is hierarchical. Parent items can be collapsed, other features are activating items, renaming an item and custom drop actions. The tree is built by first defining a number of items and child items, then each item builds a row in the tree (if no parent item is collapsed). The layout of the rows is customizable.


Creating a Tree-View

Here are the basics on how to create tree-views.

Defining a new tree can be done as simple as this:

class MyTreeView : ui::AbstractTreeView {
  void build_tree() override
  {
    ui::BasicTreeViewItem& item = add_tree_item<ui::BasicTreeViewItem>(IFACE_("Some Item"), ICON_HOME);
    /* Child item. */
    item.add_tree_item<ui::BasicTreeViewItem>(IFACE_("Child Item"), ICON_CON_CHILDOF);

    add_tree_item<ui::BasicTreeViewItem>(IFACE_("Item without icon"));
  }
};

Each item added to the tree will be displayed as a row (if it is visible), nested under its parent. Parent items are collapsible. In the example above, the items are of type ui::BasicTreeViewItem, which is a predefined item type for simple tree rows. Each row only contains an icon and a label.
For more control over what goes into a row, custom item types can be defined in a similar fashion:

class MyTreeViewItem : ui::AbstractTreeViewItem {
  void build_row(uiLayout& row) override
  {
    /* ... Regular UI layout code ... */
  }
};

Different item types can be mixed in a tree.

Lastly, the actual instance of the tree-view can be created for a uiBlock:

ui::AbstractTreeView *tree_view = UI_block_add_view(
    block,
    "My tree View",
    std::make_unique<MyTreeView>());

ui::TreeViewBuilder builder(*block);
builder.build_tree_view(*tree_view);

For a more complex real-life example, check the asset catalog tree-view code: https://developer.blender.org/diffusion/B/browse/master/source/blender/editors/space_file/asset_catalog_tree_view.cc

Tree Reconstruction

Like most UI components in Blender, tree-views are reconstructed on every redraw. This makes it easy to always represent the latest state of data. An important task of the tree-view API is reliable reconstruction of the views including their tree state (like which items are collapsed or selected) over redraws.

Most complexity is handled by the tree-view internals. But it's important to have an understanding of what's going on there.

The reconstruction is a two part process:

  1. Build the tree
    Calls the ui::AbstractTreeView::build_tree() function to create the individual tree items for the current state of the represented data.
  2. Reconstruct state
    First the tree-view code attempts to recognize the tree and all of its items from a previous redraw. This is done by looking up the tree-view by name in uiBlock.oldblock, and then comparing each new item with the previous items. The comparing is done via the overridable ui::AbstractTreeViewItem::matches() function. The default implementation just compares the labels. However, the matching process respects the hierarchy: Parent items are matched before children, and only children of a matching parent will be matched with each other.
    For each match, the state of the old item is copied to the new item using the ui::AbstractTreeViewItem::update_from_old() function. This can be overridden by sub-classes that implement custom state. The base function should always be called.

Once both steps are completed ui::AbstractTreeView::is_reconstructed() will return true. Only then the final state of the tree and the items is known. So only then can state be queried reliably and state changes be detected.

Note: Actually building the layout (e.g. placing the widgets for each row) is not considered part of the reconstruction.

Design Notes

  • Tree-view (re-)construction and layout definition are separate phases:
    • Firstly, we (re-)construct the tree as described above. Further processing, like filtering or lazy caching could be done after this.
    • Secondly, the layouts for the visible items (skipping collapsed sub-trees) are created.
  • Idea is to support other views than tree-views (e.g. list-views to replace/rewrite UIList, table-views, grid-views for File/Asset Browser like display with big thumbnails, etc). These would probably share plenty of design ideas.
  • Every row has a tree-row button (UI_BTYPE_TREEROW). An overlapping layout is used to place it underneath the customizable row layout of each item.

Further Customizations

The ui::AbstractTreeViewItem base class allows customizing behavior by overriding certain functions of it.


Custom Activation Behavior

ui::AbstractTreeViewItem::on_activate() can be overriden and is executed whenever the item gets activated (note: activated, not selected). E.g. this could be used to load a file into an editor that was selected in the tree-view.

To not have to create a sub-class of ui::BasicTreeViewItem just to customize its activation behavior, it offers a different way to set the custom behavior:

ui::BasicTreeViewItem& item = add_tree_item<ui::BasicTreeViewItem>(IFACE_("All"), ICON_HOME);

/* Pass activation behavior as lambda, function object or plain old function pointer. */
item.on_activate([](ui::BasicTreeViewItem &item) {
  std::cout << "I've been activated!" << std::endl;
});


Context Menus

An item can build a context menu similar to how it builds its row layout:

class MyTreeViewItem : ui::AbstractTreeViewItem {
  void build_row(uiLayout& row) override
  {
    /* ... Regular UI layout code for the row ... */
  }
  void build_context_menu(bContext &C, uiLayout& column) override
  {
    /* ... Regular UI layout code for the context menu ... */
  }
};

It's recommended to use WM_menutype_find() and UI_menutype_draw() to draw a context menu defined in Python. This makes it easy to edit the menu and allows add-ons to extend it.


Advanced Persistent State

When the UI is redrawn, void update_from_old(const ui::AbstractTreeViewItem &old) is called to copy state from the tree-view item of the last redraw, to the matching one in this redraw. This allows persistent state, e.g. an item can stay collapsed or expanded over redraws that way. If a custom tree item contains own state it wants to keep persistent, it should override the function, call ui::AbstractTreeViewItem::update_from_old() and then copy (or move) its custom state variables from old to itself.


Drop Actions

Tree-view items may support responding to drop events. The important functions to override for this are bool can_drop(const wmDrag &drag) and bool on_drop(). The latter can assume the former returns true when executed. In addition, std::string drop_tooltip(...) provides a way to construct a string that will be shown to the user, whenever something is dragged over this specific tree-view item. It can also be implemented assuming can_drop() returned true already.