Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'source/blender/editors')
-rw-r--r--source/blender/editors/include/UI_interface.h9
-rw-r--r--source/blender/editors/include/UI_interface.hh35
-rw-r--r--source/blender/editors/include/UI_tree_view.hh238
-rw-r--r--source/blender/editors/interface/CMakeLists.txt2
-rw-r--r--source/blender/editors/interface/interface.c43
-rw-r--r--source/blender/editors/interface/interface_handlers.c20
-rw-r--r--source/blender/editors/interface/interface_intern.h18
-rw-r--r--source/blender/editors/interface/interface_query.c3
-rw-r--r--source/blender/editors/interface/interface_view.cc116
-rw-r--r--source/blender/editors/interface/interface_widgets.c37
-rw-r--r--source/blender/editors/interface/tree_view.cc316
-rw-r--r--source/blender/editors/util/CMakeLists.txt2
12 files changed, 826 insertions, 13 deletions
diff --git a/source/blender/editors/include/UI_interface.h b/source/blender/editors/include/UI_interface.h
index 916105b0f8e..f7842270746 100644
--- a/source/blender/editors/include/UI_interface.h
+++ b/source/blender/editors/include/UI_interface.h
@@ -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
diff --git a/source/blender/editors/include/UI_interface.hh b/source/blender/editors/include/UI_interface.hh
new file mode 100644
index 00000000000..4a583d0225e
--- /dev/null
+++ b/source/blender/editors/include/UI_interface.hh
@@ -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);
diff --git a/source/blender/editors/include/UI_tree_view.hh b/source/blender/editors/include/UI_tree_view.hh
new file mode 100644
index 00000000000..51b8e65521a
--- /dev/null
+++ b/source/blender/editors/include/UI_tree_view.hh
@@ -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
diff --git a/source/blender/editors/interface/CMakeLists.txt b/source/blender/editors/interface/CMakeLists.txt
index 39dd6143eb9..79e08f46292 100644
--- a/source/blender/editors/interface/CMakeLists.txt
+++ b/source/blender/editors/interface/CMakeLists.txt
@@ -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
diff --git a/source/blender/editors/interface/interface.c b/source/blender/editors/interface/interface.c
index fd75be5b847..beee622673c 100644
--- a/source/blender/editors/interface/interface.c
+++ b/source/blender/editors/interface/interface.c
@@ -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.
*/
diff --git a/source/blender/editors/interface/interface_handlers.c b/source/blender/editors/interface/interface_handlers.c
index 77ae16d7cc7..aee66ec3a93 100644
--- a/source/blender/editors/interface/interface_handlers.c
+++ b/source/blender/editors/interface/interface_handlers.c
@@ -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;
diff --git a/source/blender/editors/interface/interface_intern.h b/source/blender/editors/interface/interface_intern.h
index d61104f094e..95e6791b359 100644
--- a/source/blender/editors/interface/interface_intern.h
+++ b/source/blender/editors/interface/interface_intern.h
@@ -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
diff --git a/source/blender/editors/interface/interface_query.c b/source/blender/editors/interface/interface_query.c
index 09429bb6df5..15d1d2f2eec 100644
--- a/source/blender/editors/interface/interface_query.c
+++ b/source/blender/editors/interface/interface_query.c
@@ -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);
}
/**
diff --git a/source/blender/editors/interface/interface_view.cc b/source/blender/editors/interface/interface_view.cc
new file mode 100644
index 00000000000..7419f21cbc6
--- /dev/null
+++ b/source/blender/editors/interface/interface_view.cc
@@ -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;
+}
diff --git a/source/blender/editors/interface/interface_widgets.c b/source/blender/editors/interface/interface_widgets.c
index 0dc7c2d3f9a..375206cab44 100644
--- a/source/blender/editors/interface/interface_widgets.c
+++ b/source/blender/editors/interface/interface_widgets.c
@@ -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;
diff --git a/source/blender/editors/interface/tree_view.cc b/source/blender/editors/interface/tree_view.cc
new file mode 100644
index 00000000000..c5770e808fa
--- /dev/null
+++ b/source/blender/editors/interface/tree_view.cc
@@ -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();
+}
diff --git a/source/blender/editors/util/CMakeLists.txt b/source/blender/editors/util/CMakeLists.txt
index b396e348845..b339bfbdc47 100644
--- a/source/blender/editors/util/CMakeLists.txt
+++ b/source/blender/editors/util/CMakeLists.txt
@@ -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
)