[go: up one dir, main page]

Skip to content

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:

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

To show how creating a view works in principle (without type specific differences), this still uses an imaginary bubble view, which would provide the ui::AbstractBubbleView and 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.

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

This uses a custom view item type MyTodoBubbleViewItem, which is 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.

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: uiLayoutGetBlock()):

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);

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

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, 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:

  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.

Preserving State

Since views are reconstructed on redraws, special handling is needed to preserve state over redraws. Otherwise, every redraw would reset state like selection, renaming, or the expanded/collapsed state of parent tree view items. There are multiple ways to preserve state for views and view items.

Note that state of individual widgets (like mouse hovering or dragging of number buttons) is preserved by Blender's widget system. Views extend this with their own state preserving.

State preserved over simple redraws

This method of preserving space only preserves state over redraws. If the UI element showing the view becomes temporarily hidden, or a new file is loaded, the state will be reset. For example if a view is placed in the Properties editor, changing to a different Properties editor tab will reset the view's state.

The advantage of this method is that it's easy to use and works for both view and view-item state. Views also preserve most state this way by default, giving basic state preservation without custom code.

To preserve custom state with this method, implement the item's AbstractViewItem::update_from_old() method to copy (or move) state from the old item to this (the new item). Be sure to call the AbstractViewItem::update_from_old() of the base class! Within this method, the old item and this can be assumed to be matching, which includes that they have the same type.

Preserving custom state for the view itself (not the view items) is not supported well yet.

Generic state preserved in files

Views can store some general state in files, making it persistent over redraws, hiding/un-hiding of UIs and even file loads. This is used for state like the scroll offset and size of the view, for views that support scrolling and resizing (like tree-views).

The main downside of this method is that it only supports state of the view, not of individual items.

How this could be supported

To store the state of individual items, we could have state storage for each item identified by its "path" (the item identifier and its ancestor identifiers, with some sound separator). Problem is that this needs syncing every time a label or the hierarchy changes. If data is identified by some UID that can be avoided, although UID conflicts may still need to be handled.

This method is quite easy to use, however, since all views need to use the uiViewState struct for this, it's better limited to general state for each view type (like scrolling offsets and custom view sizes, not filter settings that only apply to a specific animation tree-view). For this reason, it's better if use is limited to abstract view type classes like AbstractTreeView.

State is stored in the region per view identifier. So displaying multiple instances of the same view in a single region will make them share state. In separate regions the state will be separate too.

To preserve state this way, add it to uiViewState and handle it in the view's AbstractView::persistent_state() and AbstractView::persistent_state_apply() methods.

Specific state stored in data-set

The least generic way to store view state is to store it in the data-set displayed by the view, not in the UI. For example bone selection can be stored in the bones themselves, rather than in a view displaying bones. Like in this example, state is often relevant to more than a view, so the data-set is the more appropriate owner anyway.

Additionally, this is the only way to store per view item state persistently, over redraws, hiding/un-hiding of UIs and file loads. Typically this is used to preserve the active element, as well as the opened/collapsed state of tree-view items.

Compared to the other methods this takes the most implementation effort, but offers best state preservation. The view types also need to support binding to this state (see below). Note that state stored like this will be synced across multiple views displaying the same data-set (assuming they all take the state from the data-set).

Once the state is available in the data-set, it needs to be two-way bound to the view state. To do this, views provide overridable functions for specific state. For example for the active item state (see AbstractViewItem::should_be_active(), AbstractViewItem::on_activate()) and the collapsed/expanded state (tree views only; see AbstractTreeViewItem::should_be_collapsed(), AbstractTreeViewItem::on_collapse_change(), AbstractTreeViewItem::set_collapsed()).

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.