Source/Interface/Views

Views

Views make it easy to build user interfaces for (possibly large) sets of data. Each unit of data can be displayed as an item in the view, and the items provide features like selection, activation, renaming, custom context menus or drag & drop controllers. Views are designed to be efficient and customizable.

There are multiple kinds of views available:

Sections below cover general aspects of views, the links above only contain the bits for that are specific to a view type.

Overview

Say we have a list of To-Do items, that we want to display as a field of "bubbles". For this, we use an imaginary bubble-view:

Ui-view-architecture.png

The graphic displays some basic aspects of the view system:

  • Data set -> View -> Layout
    A data set "drives" a view, which in return will output a layout of UI elements.
  • Inherit from a view base class
    A new view can be implemented by creating a class that inherits from a view base class of the wanted type (here: MyTodoBubbleView inherits from ui::AbstractBubbleView).
  • ui::AbstractView
    The root base class for all views to use.
  • ui::AbstractViewItem
    The root base class for all view items to use.

The white boxes of the view represent the the public parts of the view system. They provide a relatively simple interface behind which a lot of heavy lifting is done. A single view may actually hold items of different types, they just need to share the same view type specific base class. For example, say you want to display To-Do tasks that are done as well, but they should display a button to reopen the To-Do, show a different context menu, disable renaming, ... Rather than adding a bunch of ifs in the item's code, there could be a separate MyDoneTodoBubbleViewItem, dedicated to just that.


The following sections will cover three main topics:

  • Creating views
  • View Reconstruction: Views are reconstructed on every redraw. State of items (selection, renaming, ect.) is preserved by comparing the reconstructed view to its earlier version from the last redraw, and copying the state of items from the previous to the new version.
  • Additional Features: Renaming, context menu building, selection/active binding, etc.

Creating a View

Views are built by first defining a number of items (for tree views a hierarchy even), then each visible item extends the layout.

How exactly the view is created depends a bit on the specific type, but it looks something like this:

class MyTodoBubbleView : ui::AbstractBubbleView {
  void build_items() override
  {
    add_item<MyTodoBubbleViewItem>(IFACE_("Become a millionaire"), ICON_MONEY);
    add_item<MyTodoBubbleViewItem>(IFACE_("Mow the lawn"));
  }
};

Item types can be defined in a similar fashion:

class MyTodoBubbleViewItem : ui::AbstractBubbleViewItem {
  void build_layout(uiLayout& layout) override
  {
    /* ... Regular UI layout code ... */
  }
};

There are some ready-to-use implementations for common/basic view item types. For example, for tree-views there is ui::BasicTreeViewItem, which just displays a label and an icon for each item in the tree, at the expected level of indentation. Similarly, for grid-views there is ui::PreviewGridItem, which to display a nice large preview image with a label below, like in the thumbnail mode of the File Browser.

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

ui::AbstractBubbleView *view = UI_block_add_view(
    block,
    "My todo bubble view", /* Internal identifier */
    std::make_unique<MyTodoBubbleView>());

ui::BubbleViewBuilder builder(*block);
builder.build_bubble_view(*view_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

View Reconstruction

Like most UI components in Blender, views are reconstructed on every redraw. This makes it easy to always represent the latest state of data. So the UI doesn't have to be updated carefully as data changes, the UI is just recreated with the latest input data. An important task of the view API is reliable reconstruction of the views including their state (like which items are collapsed or selected) over redraws.

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

The reconstruction is a two part process:

  1. Build the view
    Calls the view items build function (e.g. ui::AbstractTreeView::build_tree() or ui::AbstractGridView::build_items) to create the individual view items for the current state of the data to represent.
  2. Reconstruct state
    First the view system attempts to recognize the view and all of its items from a previous redraw. This is done by looking up the view by name in uiBlock.oldblock, and then comparing each new item with the previous items. Items are compared using the ui::AbstractViewItem::matches function, which can be overridden if the default of the view type isn't enough to identify items reliably. If two items were identified as matching (meaning the view system thinks an item represents the same data as the matched item from the previous frame, i.e. it recognizes it), the state of the old item is copied to the new one using the ui::AbstractViewItem::update_from_old(). If you want to implement a view with some custom state (say a show_details boolean to display more information in the item), this function has to be overridden so that your custom state is also copied to the new item. The base function should always be called.

Once both steps are completed ui::AbstractView::is_reconstructed() will return true. Only then the final state of the view 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 item) is not considered part of the reconstruction.

Further Features

The following features are supported typically. Not all view types may support all of them (yet).

Custom Activation Behavior

An item type's ::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 the details of a To-Do for display in a sidebar, when activating a bubble item.

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 item's layout:

class MyTodoBubbleViewItem : ui::AbstractBubbleViewItem {
  void build_layout(uiLayout& row) override
  {
    /* ... Regular UI layout code for the bubble ... */
  }
  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, the ::update_from_old() of each recognized item is called to copy state from the tem of the last redraw, to the matching one in this redraw. This allows persistent state, e.g. a tree-view 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. The same goes for other view types.

Drag Support

Some view items may support being dragged.

TODO: Document ui::AbstractTreeViewItemDragController design.

Drop Actions

Needs Updating
This section needs to be updated to reflect the new ui::AbstractTreeViewItemDropController design.

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 view item. It can also be implemented assuming can_drop() returned true already.