Files
test/source/blender/editors/include/UI_tree_view.hh

438 lines
15 KiB
C++

/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup editorui
*
* API for simple creation of tree UIs supporting typically needed features.
* https://developer.blender.org/docs/features/interface/views/tree_views/
*/
#pragma once
#include <functional>
#include <memory>
#include <string>
#include "BLI_function_ref.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_vector.hh"
#include "UI_abstract_view.hh"
#include "UI_resources.hh"
struct bContext;
struct uiBlock;
struct uiLayout;
namespace blender::ui {
class AbstractTreeView;
class AbstractTreeViewItem;
class TreeViewItemDropTarget;
/* ---------------------------------------------------------------------- */
/** \name Tree-View Item Container
*
* Base class for tree-view and tree-view items, so both can contain children.
* \{ */
/**
* Both the tree-view (as the root of the tree) and the items can have children. This is the base
* class for both, to store and manage child items. Children are owned by their parent container
* (tree-view or item).
*
* That means this type can be used whenever either an #AbstractTreeView or an
* #AbstractTreeViewItem is needed, but the #TreeViewOrItem alias is a better name to use then.
*/
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,
SkipFiltered = 1 << 1,
/* Keep ENUM_OPERATORS() below updated! */
};
using ItemIterFn = FunctionRef<void(AbstractTreeViewItem &)>;
/**
* Convenience wrapper constructing the item by forwarding given arguments to the constructor of
* the type (\a ItemT).
*
* E.g. if your tree-item type has the following constructor:
* \code{.cpp}
* MyTreeItem(std::string str, int i);
* \endcode
* You can add an item like this:
* \code
* add_tree_item<MyTreeItem>("blabla", 42);
* \endcode
*/
template<class ItemT, typename... Args> inline ItemT &add_tree_item(Args &&...args);
/**
* Add an already constructed tree item to this parent. Ownership is moved to it.
* All tree items must be added through this, it handles important invariants!
*/
AbstractTreeViewItem &add_tree_item(std::unique_ptr<AbstractTreeViewItem> item);
protected:
void foreach_item_recursive(ItemIterFn iter_fn, IterOptions options = IterOptions::None) const;
void foreach_parent(ItemIterFn iter_fn) const;
};
ENUM_OPERATORS(TreeViewItemContainer::IterOptions,
TreeViewItemContainer::IterOptions::SkipCollapsed);
/**
* The container class is the base for both the tree-view and the items. This alias gives it a
* clearer name for handles that accept both. Use whenever something wants to act on child-items,
* irrespective of if they are stored at root level or as children of some other item.
*/
using TreeViewOrItem = TreeViewItemContainer;
/** \} */
/* ---------------------------------------------------------------------- */
/** \name Tree-View Base Class
* \{ */
class AbstractTreeView : public AbstractView, public TreeViewItemContainer {
/* Shared pointer so the pointer can be kept persistent over redraws. The grip button gets a
* pointer to modify the value on resizing, and it uses it to identify the button over redraws.*/
/* TODO support region zoom. */
std::shared_ptr<int> custom_height_ = nullptr;
std::shared_ptr<int> scroll_value_ = nullptr;
friend class AbstractTreeViewItem;
friend class TreeViewBuilder;
friend class TreeViewLayoutBuilder;
friend class TreeViewItemDropTarget;
public:
/* virtual */ ~AbstractTreeView() override = default;
void draw_overlays(const ARegion &region, const uiBlock &block) const override;
void foreach_item(ItemIterFn iter_fn, IterOptions options = IterOptions::None) const;
void scroll(ViewScrollDirection direction) override;
/**
* \param xy: The mouse coordinates in window space.
*/
AbstractTreeViewItem *find_hovered(const ARegion &region, const int2 &xy);
/** Visual feature: Define a number of item rows the view will show by default. If there
* are fewer items, empty dummy items will be added. These contribute to the view bounds, so the
* drop target of the view includes them, but they are not interactive (e.g. no mouse-hover
* highlight). */
void set_default_rows(int default_rows);
protected:
virtual void build_tree() = 0;
private:
void foreach_view_item(FunctionRef<void(AbstractViewItem &)> iter_fn) const final;
void update_children_from_old(const AbstractView &old_view) override;
static void update_children_from_old_recursive(const TreeViewOrItem &new_items,
const TreeViewOrItem &old_items);
static AbstractTreeViewItem *find_matching_child(
const AbstractTreeViewItem &lookup_item, const Span<AbstractTreeViewItem *> possible_items);
std::optional<int> tot_visible_row_count() const;
bool supports_scrolling() const override;
void draw_hierarchy_lines(const ARegion &region, const uiBlock &block) const;
void get_hierarchy_lines(const ARegion &region,
const TreeViewOrItem &parent,
const float aspect,
Vector<std::pair<int2, int2>> &lines,
int &visible_item_index) const;
int count_visible_descendants(const AbstractTreeViewItem &parent) const;
};
/** \} */
/* ---------------------------------------------------------------------- */
/** \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 AbstractViewItem, public TreeViewItemContainer {
friend class AbstractTreeView;
friend class TreeViewLayoutBuilder;
/* Higher-level API. */
friend class TreeViewItemAPIWrapper;
private:
bool is_open_ = false;
protected:
/** This label is used as the default way to identifying an item within its parent. */
std::string label_;
public:
/* virtual */ ~AbstractTreeViewItem() override = default;
virtual void build_row(uiLayout &row) = 0;
/* virtual */ std::optional<std::string> debug_name() const override;
std::unique_ptr<DropTargetInterface> create_item_drop_target() final;
virtual std::unique_ptr<TreeViewItemDropTarget> create_drop_target();
AbstractTreeView &get_tree_view() const;
/**
* Calculate the view item rectangle from its view-item button, converted to window space.
* Returns an unset optional if there is no view item button for this item.
*/
std::optional<rctf> get_win_rect(const ARegion &region) const;
void begin_renaming();
/**
* Toggle the expanded/collapsed state.
*
* \note this does not call #on_collapse_change().
* \returns true when the collapsed state was changed, false otherwise.
*/
bool toggle_collapsed();
/**
* Expand or collapse this tree view item.
*
* \note this does not call #on_collapse_change().
* \returns true when the collapsed state was changed, false otherwise.
*/
virtual bool set_collapsed(bool collapsed);
/**
* Make this item be uncollapsed on first draw (may later be overridden by
* #should_be_collapsed()). Must only be done during tree building.
*
* \note this does not call #on_collapse_change() or #set_collapsed() overrides.
*/
void uncollapse_by_default();
/**
* Requires the tree to have completed reconstruction, see #is_reconstructed(). Otherwise we
* can't be sure about the item state.
*/
bool is_collapsed() const;
bool is_collapsible() const;
int count_parents() const;
/**
* Called when the view changes an item's state from expanded to collapsed, or vice versa. Will
* only be called if the state change is triggered through the view, not through external
* changes. E.g. a click on an item calls it, a change in the value returned by
* #should_be_collapsed() to reflect an external state change does not.
*/
virtual void on_collapse_change(bContext &C, bool is_collapsed);
/**
* If the result is not empty, it controls whether the item should be collapsed or not, usually
* depending on the data that the view represents.
*/
virtual std::optional<bool> should_be_collapsed() const;
protected:
/** See AbstractViewItem::get_rename_string(). */
/* virtual */ StringRef get_rename_string() const override;
/** See AbstractViewItem::rename(). */
/* virtual */ bool rename(const bContext &C, StringRefNull new_name) override;
/**
* Return whether the item can be collapsed. Used to disable collapsing for items with children.
* The default implementation returns true.
*/
virtual bool supports_collapsing() const;
/**
* Toggle the collapsed/expanded state, and call on_collapse_change() if it changed.
*/
void toggle_collapsed_from_view(bContext &C);
void change_state_delayed() override;
/** See #AbstractViewItem::matches(). */
/* virtual */ bool matches(const AbstractViewItem &other) const override;
/** See #AbstractViewItem::update_from_old(). */
/* virtual */ void update_from_old(const AbstractViewItem &old) override;
/**
* Compare this item to \a other to check if they represent the same data.
* Used to recognize an item from a previous redraw, to be able to keep its state (e.g.
* open/closed, active, etc.). Items are only matched if their parents also match.
* By default this just matches the item's label (if the parents match!). If that isn't
* good enough for a sub-class, that can override it.
*
* TODO #matches_single() is a rather temporary name, used to indicate that this only compares
* the item itself, not the parents. Item matching is expected to change quite a bit anyway.
*/
virtual bool matches_single(const AbstractTreeViewItem &other) const;
/**
* Can be called from the #AbstractTreeViewItem::build_row() implementation, but not earlier. The
* hovered state can't be queried reliably otherwise.
* Note that this does a linear lookup in the old block, so isn't too great performance-wise.
*/
bool is_hovered() const;
void ensure_parents_uncollapsed();
private:
static void tree_row_click_fn(bContext *, void *, void *);
static void collapse_chevron_click_fn(bContext *, void *but_arg1, void *);
/**
* Override of #AbstractViewItem::set_state_active() that also ensures the parents of this
* element are uncollapsed so that the item is visible.
*/
bool set_state_active() final;
void add_treerow_button(uiBlock &block);
int indent_width() const;
void add_indent(uiLayout &row) const;
void add_collapse_chevron(uiBlock &block) const;
void add_rename_button(uiLayout &row);
bool has_active_child() 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 IsActiveFn = std::function<bool()>;
using ActivateFn = std::function<void(bContext &C, BasicTreeViewItem &new_active)>;
BIFIconID icon;
explicit BasicTreeViewItem(StringRef label, BIFIconID icon = ICON_NONE);
void build_row(uiLayout &row) override;
void add_label(uiLayout &layout, StringRefNull label_override = "");
void set_on_activate_fn(ActivateFn fn);
/**
* Set a custom callback to check if this item should be active.
*/
void set_is_active_fn(IsActiveFn is_active_fn);
protected:
/**
* 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_;
IsActiveFn is_active_fn_;
private:
static void tree_row_click_fn(bContext *C, void *arg1, void *arg2);
std::optional<bool> should_be_active() const override;
void on_activate(bContext &C) override;
};
/** \} */
/* ---------------------------------------------------------------------- */
/** \name Drag & Drop
* \{ */
/**
* Class to define the behavior when dropping something onto/into a view item, plus the behavior
* when dragging over this item. An item can return a drop target for itself via a custom
* implementation of #AbstractTreeViewItem::create_drop_target().
*
* By default the drop target only supports dropping into/onto itself. To support
* inserting/reordering behavior, where dropping before or after the drop-target is supported, pass
* a different #DropBehavior to the constructor.
*/
class TreeViewItemDropTarget : public DropTargetInterface {
protected:
AbstractTreeViewItem &view_item_;
const DropBehavior behavior_;
public:
TreeViewItemDropTarget(AbstractTreeViewItem &view_item,
DropBehavior behavior = DropBehavior::Insert);
std::optional<DropLocation> choose_drop_location(const ARegion &region,
const wmEvent &event) const override;
/** Request the view the item is registered for as type #ViewType. Throws a `std::bad_cast`
* exception if the view is not of the requested type. */
template<class ViewType> inline ViewType &get_view() const;
};
/** \} */
/* ---------------------------------------------------------------------- */
/** \name Tree-View Builder
* \{ */
class TreeViewBuilder {
public:
static void build_tree_view(AbstractTreeView &tree_view,
uiLayout &layout,
std::optional<StringRef> search_string = {},
bool add_box = true);
private:
static void ensure_min_rows_items(AbstractTreeView &tree_view);
};
/** \} */
/* ---------------------------------------------------------------------- */
template<class ItemT, typename... Args>
inline ItemT &TreeViewItemContainer::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)...)));
}
template<class ViewType> ViewType &TreeViewItemDropTarget::get_view() const
{
static_assert(std::is_base_of<AbstractTreeView, ViewType>::value,
"Type must derive from and implement the ui::AbstractTreeView interface");
return dynamic_cast<ViewType &>(view_item_.get_tree_view());
}
} // namespace blender::ui