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.
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:
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:
The root base class for all views to use.
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
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¶
To show how creating a view works in principle (without type specific
differences), this still uses an imaginary bubble view, which would
ui::AbstractBubbleViewItem base classes.
Views are built by first defining a number of items (for tree views a hierarchy even), which will then be used to build the layout.
This uses a custom view item type
MyTodoBubbleViewItem, which is
defined in a similar fashion:
There are some ready-to-use implementations for common/basic view item
types. For example, for tree-views there is
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.
Now that the view is defined, an actual instance of it has to be
created, and built into the layout. UI definition code (like a panel
draw() callback) can add it to a
uiBlock (e.g. a new block or
the block of the layout:
For a more complex real-life example, check the asset catalog tree-view code: https://projects.blender.org/blender/blender/src/branch/main/source/blender/editors/space_file/asset_catalog_tree_view.cc
Like most UI components in Blender, views are reconstructed on every redraw. This makes it easy to always represent the latest state of data, rather than having to manipulate the view in complex ways on data change events. 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:
- Build the view
Calls the view items build function (e.g.
ui::AbstractGridView::build_items) to create the individual view items for the current state of the data to represent.
- 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::matchesfunction, 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_detailsboolean 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
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.
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
An item can build a context menu similar to how it builds its item's layout:
It's recommended to use
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.
Some view items may support being dragged.
This section needs to be updated to reflect the new
View items may support responding to drop events. The important
functions to override for this are
bool can_drop(const wmDrag &drag)
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
can_drop() returned true already.