UI: Tree-View API for easy creation of tree UIs

This follows three main targets:

* Make creation of new tree UIs easy.
* Groundwork to generalize tree UIs (so e.g. Outliner, animation
  channels, asset catalogs and spreadsheet data-sets don't have to
  re-implement basic tree UI code) or even other data-view UIs.
* Better separate data and UI state. E.g. with this, tree-item selection
  or the open/collapsed state can be stored on the UI level, rather than
  in data. (Asset Catalogs need this, storing UI state info in them is
  not an option.)

In addition, the design should be well testable and could even be
exposed to Python.

Note that things will likely change in master still. E.g. the actually
resulting UI isn't very nice visually yet.

The design is documented here:
https://wiki.blender.org/wiki/Source/Interface/Views

Differential Revision: https://developer.blender.org/D12573
This commit is contained in:
Julian Eisel 2021-09-23 18:56:29 +02:00
parent eb0eb54d96
commit 323fd80aad
12 changed files with 826 additions and 13 deletions

View File

@ -84,6 +84,10 @@ typedef struct uiBlock uiBlock;
typedef struct uiBut uiBut;
typedef struct uiLayout uiLayout;
typedef struct uiPopupBlockHandle uiPopupBlockHandle;
/* C handle for C++ #ui::AbstractTreeView type. */
typedef struct uiTreeViewHandle uiTreeViewHandle;
/* C handle for C++ #ui::AbstractTreeViewItem type. */
typedef struct uiTreeViewItemHandle uiTreeViewItemHandle;
/* Defines */
@ -389,6 +393,8 @@ typedef enum {
UI_BTYPE_GRIP = 57 << 9,
UI_BTYPE_DECORATOR = 58 << 9,
UI_BTYPE_DATASETROW = 59 << 9,
/* An item in a tree view. Parent items may be collapsible. */
UI_BTYPE_TREEROW = 60 << 9,
} eButType;
#define BUTTYPE (63 << 9)
@ -1672,6 +1678,7 @@ void UI_but_datasetrow_component_set(uiBut *but, uint8_t geometry_component_type
void UI_but_datasetrow_domain_set(uiBut *but, uint8_t attribute_domain);
uint8_t UI_but_datasetrow_component_get(uiBut *but);
uint8_t UI_but_datasetrow_domain_get(uiBut *but);
void UI_but_treerow_indentation_set(uiBut *but, int indentation);
void UI_but_node_link_set(uiBut *but, struct bNodeSocket *socket, const float draw_color[4]);
@ -2754,6 +2761,8 @@ void UI_interface_tag_script_reload(void);
/* Support click-drag motion which presses the button and closes a popover (like a menu). */
#define USE_UI_POPOVER_ONCE
bool UI_tree_view_item_is_active(uiTreeViewItemHandle *item_);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,35 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup editorui
*/
#pragma once
#include <memory>
#include "BLI_string_ref.hh"
struct uiBlock;
namespace blender::ui {
class AbstractTreeView;
}
blender::ui::AbstractTreeView *UI_block_add_view(
uiBlock &block,
blender::StringRef idname,
std::unique_ptr<blender::ui::AbstractTreeView> tree_view);

View File

@ -0,0 +1,238 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup editorui
*/
#pragma once
#include <memory>
#include <string>
#include "BLI_function_ref.hh"
#include "BLI_vector.hh"
#include "UI_resources.h"
struct PointerRNA;
struct uiBlock;
struct uiBut;
struct uiButTreeRow;
struct uiLayout;
namespace blender::ui {
class AbstractTreeView;
class AbstractTreeViewItem;
/* ---------------------------------------------------------------------- */
/** \name Tree-View Item Container
* \{ */
/**
* Helper base class to expose common child-item data and functionality to both #AbstractTreeView
* and #AbstractTreeViewItem.
*
* That means this type can be used whenever either a #AbstractTreeView or a
* #AbstractTreeViewItem is needed.
*/
class TreeViewItemContainer {
friend class AbstractTreeView;
friend class AbstractTreeViewItem;
/* Private constructor, so only the friends above can create this! */
TreeViewItemContainer() = default;
protected:
Vector<std::unique_ptr<AbstractTreeViewItem>> children_;
/** Adding the first item to the root will set this, then it's passed on to all children. */
TreeViewItemContainer *root_ = nullptr;
/** Pointer back to the owning item. */
AbstractTreeViewItem *parent_ = nullptr;
public:
enum class IterOptions {
None = 0,
SkipCollapsed = 1 << 0,
/* Keep ENUM_OPERATORS() below updated! */
};
using ItemIterFn = FunctionRef<void(AbstractTreeViewItem &)>;
/**
* Convenience wrapper taking the arguments needed to construct an item of type \a ItemT. Calls
* the version just below.
*/
template<class ItemT, typename... Args> ItemT &add_tree_item(Args &&...args)
{
static_assert(std::is_base_of<AbstractTreeViewItem, ItemT>::value,
"Type must derive from and implement the AbstractTreeViewItem interface");
return dynamic_cast<ItemT &>(
add_tree_item(std::make_unique<ItemT>(std::forward<Args>(args)...)));
}
AbstractTreeViewItem &add_tree_item(std::unique_ptr<AbstractTreeViewItem> item);
protected:
void foreach_item_recursive(ItemIterFn iter_fn, IterOptions options = IterOptions::None) const;
};
ENUM_OPERATORS(TreeViewItemContainer::IterOptions,
TreeViewItemContainer::IterOptions::SkipCollapsed);
/** \} */
/* ---------------------------------------------------------------------- */
/** \name Tree-View Builders
* \{ */
class TreeViewBuilder {
uiBlock &block_;
public:
TreeViewBuilder(uiBlock &block);
void build_tree_view(AbstractTreeView &tree_view);
};
class TreeViewLayoutBuilder {
uiBlock &block_;
friend TreeViewBuilder;
public:
void build_row(AbstractTreeViewItem &item) const;
uiBlock &block() const;
uiLayout *current_layout() const;
private:
/* Created through #TreeViewBuilder. */
TreeViewLayoutBuilder(uiBlock &block);
};
/** \} */
/* ---------------------------------------------------------------------- */
/** \name Tree-View Base Class
* \{ */
class AbstractTreeView : public TreeViewItemContainer {
friend TreeViewBuilder;
friend TreeViewLayoutBuilder;
public:
virtual ~AbstractTreeView() = default;
void foreach_item(ItemIterFn iter_fn, IterOptions options = IterOptions::None) const;
protected:
virtual void build_tree() = 0;
private:
/** Match the tree-view against an earlier version of itself (if any) and copy the old UI state
* (e.g. collapsed, active, selected) to the new one. See
* #AbstractTreeViewItem.update_from_old(). */
void update_from_old(uiBlock &new_block);
static void update_children_from_old_recursive(const TreeViewItemContainer &new_items,
const TreeViewItemContainer &old_items);
static AbstractTreeViewItem *find_matching_child(const AbstractTreeViewItem &lookup_item,
const TreeViewItemContainer &items);
void build_layout_from_tree(const TreeViewLayoutBuilder &builder);
};
/** \} */
/* ---------------------------------------------------------------------- */
/** \name Tree-View Item Type
* \{ */
/** \brief Abstract base class for defining a customizable tree-view item.
*
* The tree-view item defines how to build its data into a tree-row. There are implementations for
* common layouts, e.g. #BasicTreeViewItem.
* It also stores state information that needs to be persistent over redraws, like the collapsed
* state.
*/
class AbstractTreeViewItem : public TreeViewItemContainer {
friend class AbstractTreeView;
bool is_open_ = false;
bool is_active_ = false;
protected:
/** This label is used for identifying an item (together with its parent's labels). */
std::string label_{};
public:
virtual ~AbstractTreeViewItem() = default;
virtual void build_row(uiLayout &row) = 0;
virtual void on_activate();
/** Copy persistent state (e.g. is-collapsed flag, selection, etc.) from a matching item of the
* last redraw to this item. If sub-classes introduce more advanced state they should override
* this and make it update their state accordingly. */
virtual void update_from_old(AbstractTreeViewItem &old);
const AbstractTreeView &get_tree_view() const;
int count_parents() const;
void set_active(bool value = true);
bool is_active() const;
void toggle_collapsed();
bool is_collapsed() const;
void set_collapsed(bool collapsed);
bool is_collapsible() const;
};
/** \} */
/* ---------------------------------------------------------------------- */
/** \name Predefined Tree-View Item Types
*
* Common, Basic Tree-View Item Types.
* \{ */
/**
* The most basic type, just a label with an icon.
*/
class BasicTreeViewItem : public AbstractTreeViewItem {
public:
using ActivateFn = std::function<void(BasicTreeViewItem &new_active)>;
BIFIconID icon;
BasicTreeViewItem(StringRef label, BIFIconID icon = ICON_NONE, ActivateFn activate_fn = nullptr);
void build_row(uiLayout &row) override;
void on_activate() override;
protected:
/** Created in the #build() function. */
uiButTreeRow *tree_row_but_ = nullptr;
/** Optionally passed to the #BasicTreeViewItem constructor. Called when activating this tree
* view item. This way users don't have to sub-class #BasicTreeViewItem, just to implement
* custom activation behavior (a common thing to do). */
ActivateFn activate_fn_;
uiBut *button();
BIFIconID get_draw_icon() const;
};
/** \} */
} // namespace blender::ui

View File

@ -73,8 +73,10 @@ set(SRC
interface_templates.c
interface_undo.c
interface_utils.c
interface_view.cc
interface_widgets.c
resources.c
tree_view.cc
view2d.c
view2d_draw.c
view2d_edge_pan.c

View File

@ -856,10 +856,21 @@ static void ui_but_update_old_active_from_new(uiBut *oldbut, uiBut *but)
oldbut->hardmax = but->hardmax;
}
if (oldbut->type == UI_BTYPE_PROGRESS_BAR) {
uiButProgressbar *progress_oldbut = (uiButProgressbar *)oldbut;
uiButProgressbar *progress_but = (uiButProgressbar *)but;
progress_oldbut->progress = progress_but->progress;
switch (oldbut->type) {
case UI_BTYPE_PROGRESS_BAR: {
uiButProgressbar *progress_oldbut = (uiButProgressbar *)oldbut;
uiButProgressbar *progress_but = (uiButProgressbar *)but;
progress_oldbut->progress = progress_but->progress;
break;
}
case UI_BTYPE_TREEROW: {
uiButTreeRow *treerow_oldbut = (uiButTreeRow *)oldbut;
uiButTreeRow *treerow_newbut = (uiButTreeRow *)but;
SWAP(uiTreeViewItemHandle *, treerow_newbut->tree_item, treerow_oldbut->tree_item);
break;
}
default:
break;
}
/* move/copy string from the new button to the old */
@ -2203,6 +2214,15 @@ int ui_but_is_pushed_ex(uiBut *but, double *value)
}
}
break;
case UI_BTYPE_TREEROW: {
uiButTreeRow *tree_row_but = (uiButTreeRow *)but;
is_push = -1;
if (tree_row_but->tree_item) {
is_push = UI_tree_view_item_is_active(tree_row_but->tree_item);
}
break;
}
default:
is_push = -1;
break;
@ -3447,6 +3467,7 @@ void UI_block_free(const bContext *C, uiBlock *block)
BLI_freelistN(&block->color_pickers.list);
ui_block_free_button_groups(block);
ui_block_free_views(block);
MEM_freeN(block);
}
@ -3942,6 +3963,10 @@ static void ui_but_alloc_info(const eButType type,
alloc_size = sizeof(uiButDatasetRow);
alloc_str = "uiButDatasetRow";
break;
case UI_BTYPE_TREEROW:
alloc_size = sizeof(uiButTreeRow);
alloc_str = "uiButTreeRow";
break;
default:
alloc_size = sizeof(uiBut);
alloc_str = "uiBut";
@ -4141,6 +4166,7 @@ static uiBut *ui_def_but(uiBlock *block,
UI_BTYPE_BUT_MENU,
UI_BTYPE_SEARCH_MENU,
UI_BTYPE_DATASETROW,
UI_BTYPE_TREEROW,
UI_BTYPE_POPOVER)) {
but->drawflag |= (UI_BUT_TEXT_LEFT | UI_BUT_ICON_LEFT);
}
@ -6878,6 +6904,15 @@ void UI_but_datasetrow_indentation_set(uiBut *but, int indentation)
BLI_assert(indentation >= 0);
}
void UI_but_treerow_indentation_set(uiBut *but, int indentation)
{
uiButTreeRow *but_row = (uiButTreeRow *)but;
BLI_assert(but->type == UI_BTYPE_TREEROW);
but_row->indentation = indentation;
BLI_assert(indentation >= 0);
}
/**
* Adds a hint to the button which draws right aligned, grayed out and never clipped.
*/

View File

@ -384,6 +384,8 @@ typedef struct uiHandleButtonData {
/* booleans (could be made into flags) */
bool cancel, escapecancel;
bool applied, applied_interactive;
/* Button is being applied through an extra icon. */
bool apply_through_extra_icon;
bool changed_cursor;
wmTimer *flashtimer;
@ -1164,6 +1166,16 @@ static void ui_apply_but_ROW(bContext *C, uiBlock *block, uiBut *but, uiHandleBu
data->applied = true;
}
static void ui_apply_but_TREEROW(bContext *C, uiBlock *block, uiBut *but, uiHandleButtonData *data)
{
if (data->apply_through_extra_icon) {
/* Don't apply this, it would cause unintended tree-row toggling when clicking on extra icons.
*/
return;
}
ui_apply_but_ROW(C, block, but, data);
}
/**
* \note Ownership of \a properties is moved here. The #uiAfterFunc owns it now.
*
@ -2307,6 +2319,9 @@ static void ui_apply_but(
case UI_BTYPE_ROW:
ui_apply_but_ROW(C, block, but, data);
break;
case UI_BTYPE_TREEROW:
ui_apply_but_TREEROW(C, block, but, data);
break;
case UI_BTYPE_LISTROW:
ui_apply_but_LISTROW(C, block, but, data);
break;
@ -4194,6 +4209,8 @@ static void ui_numedit_apply(bContext *C, uiBlock *block, uiBut *but, uiHandleBu
static void ui_but_extra_operator_icon_apply(bContext *C, uiBut *but, uiButExtraOpIcon *op_icon)
{
but->active->apply_through_extra_icon = true;
if (but->active->interactive) {
ui_apply_but(C, but->block, but, but->active, true);
}
@ -4737,7 +4754,7 @@ static int ui_do_but_TOG(bContext *C, uiBut *but, uiHandleButtonData *data, cons
/* Behave like other menu items. */
do_activate = (event->val == KM_RELEASE);
}
else {
else if (!ui_do_but_extra_operator_icon(C, but, data, event)) {
/* Also use double-clicks to prevent fast clicks to leak to other handlers (T76481). */
do_activate = ELEM(event->val, KM_PRESS, KM_DBL_CLICK);
}
@ -7966,6 +7983,7 @@ static int ui_do_button(bContext *C, uiBlock *block, uiBut *but, const wmEvent *
case UI_BTYPE_CHECKBOX:
case UI_BTYPE_CHECKBOX_N:
case UI_BTYPE_ROW:
case UI_BTYPE_TREEROW:
case UI_BTYPE_DATASETROW:
retval = ui_do_but_TOG(C, but, data, event);
break;

View File

@ -360,6 +360,14 @@ typedef struct uiButDatasetRow {
int indentation;
} uiButDatasetRow;
/** Derived struct for #UI_BTYPE_TREEROW. */
typedef struct uiButTreeRow {
uiBut but;
uiTreeViewItemHandle *tree_item;
int indentation;
} uiButTreeRow;
/** Derived struct for #UI_BTYPE_HSVCUBE. */
typedef struct uiButHSVCube {
uiBut but;
@ -488,6 +496,11 @@ struct uiBlock {
ListBase contexts;
/** A block can store "views" on data-sets. Currently tree-views (#AbstractTreeView) only.
* Others are imaginable, e.g. table-views, grid-views, etc. These are stored here to support
* state that is persistent over redraws (e.g. collapsed tree-view items). */
ListBase views;
char name[UI_MAX_NAME_STR];
float winmat[4][4];
@ -1274,6 +1287,11 @@ bool ui_jump_to_target_button_poll(struct bContext *C);
/* interface_queries.c */
void ui_interface_tag_script_reload_queries(void);
/* interface_view.cc */
void ui_block_free_views(struct uiBlock *block);
uiTreeViewHandle *ui_block_view_find_matching_in_old_block(const uiBlock *new_block,
const uiTreeViewHandle *new_view);
#ifdef __cplusplus
}
#endif

View File

@ -69,7 +69,8 @@ bool ui_but_is_toggle(const uiBut *but)
UI_BTYPE_CHECKBOX,
UI_BTYPE_CHECKBOX_N,
UI_BTYPE_ROW,
UI_BTYPE_DATASETROW);
UI_BTYPE_DATASETROW,
UI_BTYPE_TREEROW);
}
/**

View File

@ -0,0 +1,116 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup edinterface
*
* This part of the UI-View API is mostly needed to support persistent state of items within the
* view. Views are stored in #uiBlock's, and kept alive with it until after the next redraw. So we
* can compare the old view items with the new view items and keep state persistent for matching
* ones.
*/
#include <memory>
#include <variant>
#include "BLI_listbase.h"
#include "interface_intern.h"
#include "UI_interface.hh"
#include "UI_tree_view.hh"
using namespace blender;
using namespace blender::ui;
/**
* Wrapper to store views in a #ListBase. There's no `uiView` base class, we just store views as a
* #std::variant.
*/
struct ViewLink : public Link {
using TreeViewPtr = std::unique_ptr<AbstractTreeView>;
std::string idname;
/* Note: Can't use std::get() on this until minimum macOS deployment target is 10.14. */
std::variant<TreeViewPtr> view;
};
template<class T> T *get_view_from_link(ViewLink &link)
{
auto *t_uptr = std::get_if<std::unique_ptr<T>>(&link.view);
return t_uptr ? t_uptr->get() : nullptr;
}
/**
* Override this for all available tree types.
*/
AbstractTreeView *UI_block_add_view(uiBlock &block,
StringRef idname,
std::unique_ptr<AbstractTreeView> tree_view)
{
ViewLink *view_link = OBJECT_GUARDED_NEW(ViewLink);
BLI_addtail(&block.views, view_link);
view_link->view = std::move(tree_view);
view_link->idname = idname;
return get_view_from_link<AbstractTreeView>(*view_link);
}
void ui_block_free_views(uiBlock *block)
{
LISTBASE_FOREACH_MUTABLE (ViewLink *, link, &block->views) {
OBJECT_GUARDED_DELETE(link, ViewLink);
}
}
static StringRef ui_block_view_find_idname(const uiBlock &block, const AbstractTreeView &view)
{
/* First get the idname the of the view we're looking for. */
LISTBASE_FOREACH (ViewLink *, view_link, &block.views) {
if (get_view_from_link<AbstractTreeView>(*view_link) == &view) {
return view_link->idname;
}
}
return {};
}
uiTreeViewHandle *ui_block_view_find_matching_in_old_block(const uiBlock *new_block,
const uiTreeViewHandle *new_view_handle)
{
const AbstractTreeView &needle_view = reinterpret_cast<const AbstractTreeView &>(
*new_view_handle);
uiBlock *old_block = new_block->oldblock;
if (!old_block) {
return nullptr;
}
StringRef idname = ui_block_view_find_idname(*new_block, needle_view);
if (idname.is_empty()) {
return nullptr;
}
LISTBASE_FOREACH (ViewLink *, old_view_link, &old_block->views) {
if (old_view_link->idname == idname) {
return reinterpret_cast<uiTreeViewHandle *>(
get_view_from_link<AbstractTreeView>(*old_view_link));
}
}
return nullptr;
}

View File

@ -115,6 +115,7 @@ typedef enum {
UI_WTYPE_PROGRESSBAR,
UI_WTYPE_NODESOCKET,
UI_WTYPE_DATASETROW,
UI_WTYPE_TREEROW,
} uiWidgetTypeEnum;
/* Button state argument shares bits with 'uiBut.flag'.
@ -3679,10 +3680,9 @@ static void widget_progressbar(
widgetbase_draw(&wtb_bar, wcol);
}
static void widget_datasetrow(
uiBut *but, uiWidgetColors *wcol, rcti *rect, int state, int UNUSED(roundboxalign))
static void widget_treerow_exec(
uiWidgetColors *wcol, rcti *rect, int state, int UNUSED(roundboxalign), int indentation)
{
uiButDatasetRow *but_componentrow = (uiButDatasetRow *)but;
uiWidgetBase wtb;
widget_init(&wtb);
@ -3695,10 +3695,24 @@ static void widget_datasetrow(
widgetbase_draw(&wtb, wcol);
}
BLI_rcti_resize(rect,
BLI_rcti_size_x(rect) - UI_UNIT_X * but_componentrow->indentation,
BLI_rcti_size_y(rect));
BLI_rcti_translate(rect, 0.5f * UI_UNIT_X * but_componentrow->indentation, 0);
BLI_rcti_resize(rect, BLI_rcti_size_x(rect) - UI_UNIT_X * indentation, BLI_rcti_size_y(rect));
BLI_rcti_translate(rect, 0.5f * UI_UNIT_X * indentation, 0);
}
static void widget_treerow(
uiBut *but, uiWidgetColors *wcol, rcti *rect, int state, int roundboxalign)
{
uiButTreeRow *tree_row = (uiButTreeRow *)but;
BLI_assert(but->type == UI_BTYPE_TREEROW);
widget_treerow_exec(wcol, rect, state, roundboxalign, tree_row->indentation);
}
static void widget_datasetrow(
uiBut *but, uiWidgetColors *wcol, rcti *rect, int state, int roundboxalign)
{
uiButDatasetRow *dataset_row = (uiButDatasetRow *)but;
BLI_assert(but->type == UI_BTYPE_DATASETROW);
widget_treerow_exec(wcol, rect, state, roundboxalign, dataset_row->indentation);
}
static void widget_nodesocket(
@ -4492,6 +4506,10 @@ static uiWidgetType *widget_type(uiWidgetTypeEnum type)
wt.custom = widget_datasetrow;
break;
case UI_WTYPE_TREEROW:
wt.custom = widget_treerow;
break;
case UI_WTYPE_NODESOCKET:
wt.custom = widget_nodesocket;
break;
@ -4824,6 +4842,11 @@ void ui_draw_but(const bContext *C, struct ARegion *region, uiStyle *style, uiBu
fstyle = &style->widgetlabel;
break;
case UI_BTYPE_TREEROW:
wt = widget_type(UI_WTYPE_TREEROW);
fstyle = &style->widgetlabel;
break;
case UI_BTYPE_SCROLL:
wt = widget_type(UI_WTYPE_SCROLL);
break;

View File

@ -0,0 +1,316 @@
/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup edinterface
*/
#include "DNA_userdef_types.h"
#include "interface_intern.h"
#include "UI_interface.h"
#include "UI_tree_view.hh"
namespace blender::ui {
/* ---------------------------------------------------------------------- */
/**
* Add a tree-item to the container. This is the only place where items should be added, it handles
* important invariants!
*/
AbstractTreeViewItem &TreeViewItemContainer::add_tree_item(
std::unique_ptr<AbstractTreeViewItem> item)
{
children_.append(std::move(item));
/* The first item that will be added to the root sets this. */
if (root_ == nullptr) {
root_ = this;
}
AbstractTreeViewItem &added_item = *children_.last();
added_item.root_ = root_;
if (root_ != this) {
/* Any item that isn't the root can be assumed to the a #AbstractTreeViewItem. Not entirely
* nice to static_cast this, but well... */
added_item.parent_ = static_cast<AbstractTreeViewItem *>(this);
}
return added_item;
}
void TreeViewItemContainer::foreach_item_recursive(ItemIterFn iter_fn, IterOptions options) const
{
for (auto &child : children_) {
iter_fn(*child);
if (bool(options & IterOptions::SkipCollapsed) && child->is_collapsed()) {
continue;
}
child->foreach_item_recursive(iter_fn, options);
}
}
/* ---------------------------------------------------------------------- */
void AbstractTreeView::foreach_item(ItemIterFn iter_fn, IterOptions options) const
{
foreach_item_recursive(iter_fn, options);
}
void AbstractTreeView::build_layout_from_tree(const TreeViewLayoutBuilder &builder)
{
uiLayout *prev_layout = builder.current_layout();
uiLayoutColumn(prev_layout, true);
foreach_item([&builder](AbstractTreeViewItem &item) { builder.build_row(item); },
IterOptions::SkipCollapsed);
UI_block_layout_set_current(&builder.block(), prev_layout);
}
void AbstractTreeView::update_from_old(uiBlock &new_block)
{
uiBlock *old_block = new_block.oldblock;
if (!old_block) {
return;
}
uiTreeViewHandle *old_view_handle = ui_block_view_find_matching_in_old_block(
&new_block, reinterpret_cast<uiTreeViewHandle *>(this));
if (!old_view_handle) {
return;
}
AbstractTreeView &old_view = reinterpret_cast<AbstractTreeView &>(*old_view_handle);
update_children_from_old_recursive(*this, old_view);
}
void AbstractTreeView::update_children_from_old_recursive(const TreeViewItemContainer &new_items,
const TreeViewItemContainer &old_items)
{
for (const auto &new_item : new_items.children_) {
AbstractTreeViewItem *matching_old_item = find_matching_child(*new_item, old_items);
if (!matching_old_item) {
continue;
}
new_item->update_from_old(*matching_old_item);
/* Recurse into children of the matched item. */
update_children_from_old_recursive(*new_item, *matching_old_item);
}
}
AbstractTreeViewItem *AbstractTreeView::find_matching_child(
const AbstractTreeViewItem &lookup_item, const TreeViewItemContainer &items)
{
for (const auto &iter_item : items.children_) {
if (lookup_item.label_ == iter_item->label_) {
/* We have a matching item! */
return iter_item.get();
}
}
return nullptr;
}
/* ---------------------------------------------------------------------- */
void AbstractTreeViewItem::on_activate()
{
/* Do nothing by default. */
}
void AbstractTreeViewItem::update_from_old(AbstractTreeViewItem &old)
{
is_open_ = old.is_open_;
is_active_ = old.is_active_;
}
const AbstractTreeView &AbstractTreeViewItem::get_tree_view() const
{
return static_cast<AbstractTreeView &>(*root_);
}
int AbstractTreeViewItem::count_parents() const
{
int i = 0;
for (TreeViewItemContainer *parent = parent_; parent; parent = parent->parent_) {
i++;
}
return i;
}
void AbstractTreeViewItem::set_active(bool value)
{
if (value && !is_active()) {
/* Deactivate other items in the tree. */
get_tree_view().foreach_item([](auto &item) { item.set_active(false); });
on_activate();
}
is_active_ = value;
}
bool AbstractTreeViewItem::is_active() const
{
return is_active_;
}
bool AbstractTreeViewItem::is_collapsed() const
{
return is_collapsible() && !is_open_;
}
void AbstractTreeViewItem::toggle_collapsed()
{
is_open_ = !is_open_;
}
void AbstractTreeViewItem::set_collapsed(bool collapsed)
{
is_open_ = !collapsed;
}
bool AbstractTreeViewItem::is_collapsible() const
{
return !children_.is_empty();
}
/* ---------------------------------------------------------------------- */
TreeViewBuilder::TreeViewBuilder(uiBlock &block) : block_(block)
{
}
void TreeViewBuilder::build_tree_view(AbstractTreeView &tree_view)
{
tree_view.build_tree();
tree_view.update_from_old(block_);
tree_view.build_layout_from_tree(TreeViewLayoutBuilder(block_));
}
/* ---------------------------------------------------------------------- */
TreeViewLayoutBuilder::TreeViewLayoutBuilder(uiBlock &block) : block_(block)
{
}
void TreeViewLayoutBuilder::build_row(AbstractTreeViewItem &item) const
{
uiLayout *prev_layout = current_layout();
uiLayout *row = uiLayoutRow(prev_layout, false);
item.build_row(*row);
UI_block_layout_set_current(&block(), prev_layout);
}
uiBlock &TreeViewLayoutBuilder::block() const
{
return block_;
}
uiLayout *TreeViewLayoutBuilder::current_layout() const
{
return block().curlayout;
}
/* ---------------------------------------------------------------------- */
BasicTreeViewItem::BasicTreeViewItem(StringRef label, BIFIconID icon_, ActivateFn activate_fn)
: icon(icon_), activate_fn_(activate_fn)
{
label_ = label;
}
static void tree_row_click_fn(struct bContext *UNUSED(C), void *but_arg1, void *UNUSED(arg2))
{
uiButTreeRow *tree_row_but = (uiButTreeRow *)but_arg1;
AbstractTreeViewItem &tree_item = reinterpret_cast<AbstractTreeViewItem &>(
*tree_row_but->tree_item);
/* Let a click on an opened item activate it, a second click will close it then.
* TODO Should this be for asset catalogs only? */
if (tree_item.is_collapsed() || tree_item.is_active()) {
tree_item.toggle_collapsed();
}
tree_item.set_active();
}
void BasicTreeViewItem::build_row(uiLayout &row)
{
uiBlock *block = uiLayoutGetBlock(&row);
tree_row_but_ = (uiButTreeRow *)uiDefIconTextBut(block,
UI_BTYPE_TREEROW,
0,
/* TODO allow icon besides the chevron icon? */
get_draw_icon(),
label_.data(),
0,
0,
UI_UNIT_X,
UI_UNIT_Y,
nullptr,
0,
0,
0,
0,
nullptr);
tree_row_but_->tree_item = reinterpret_cast<uiTreeViewItemHandle *>(this);
UI_but_func_set(&tree_row_but_->but, tree_row_click_fn, tree_row_but_, nullptr);
UI_but_treerow_indentation_set(&tree_row_but_->but, count_parents());
}
void BasicTreeViewItem::on_activate()
{
if (activate_fn_) {
activate_fn_(*this);
}
}
BIFIconID BasicTreeViewItem::get_draw_icon() const
{
if (icon) {
return icon;
}
if (is_collapsible()) {
return is_collapsed() ? ICON_TRIA_RIGHT : ICON_TRIA_DOWN;
}
return ICON_NONE;
}
uiBut *BasicTreeViewItem::button()
{
return &tree_row_but_->but;
}
} // namespace blender::ui
using namespace blender::ui;
bool UI_tree_view_item_is_active(uiTreeViewItemHandle *item_)
{
AbstractTreeViewItem &item = reinterpret_cast<AbstractTreeViewItem &>(*item_);
return item.is_active();
}

View File

@ -103,8 +103,10 @@ set(SRC
../include/ED_view3d_offscreen.h
../include/UI_icons.h
../include/UI_interface.h
../include/UI_interface.hh
../include/UI_interface_icons.h
../include/UI_resources.h
../include/UI_tree_view.hh
../include/UI_view2d.h
)