UI: Tree-view scrolling and resizing support
Implements scrolling support for tree-views, as well as changing the size of the scroll-able tree-view. This is important to be able to place them in many places in the UI, where a compact layout is preferred over an every expanding one (causing following contents to be scrolled out of view). UI-lists would use scrolling and resizing to ensure this, now tree-views are on par. Enables scrolling and resizing for: - Bone collection UI (`UILayout.template_bone_collection_tree()`) - Grease Pencil layer UI (`UILayout.template_grease_pencil_layer_tree()`) - Light link collection UI (`UILayout.template_light_linking_collection()`) - UI to define a node tree interface (`UILayout.template_node_tree_interface()`) These are all cases where compact UIs make more sense than expanding ones. Internally this is enabled by calling the `set_default_rows()` method of the tree-view, although the API might change still. It shouldn't be quite simple to implement this for grid-views too if necessary, or other potential view types. Although I'd like to do some smaller code quality improvements still, this feature is important for some other modules (e.g. grease pencil module for the layers UI in grease pencil v3), so I decided to prioritize merging this. Pull Request: https://projects.blender.org/blender/blender/pulls/119668
This commit is contained in:
committed by
Julian Eisel
parent
0315eae536
commit
78e330923d
@@ -1012,6 +1012,8 @@ def km_user_interface(_params):
|
||||
("ui.list_start_filter", {"type": 'F', "value": 'PRESS', "ctrl": True}, None),
|
||||
# UI views (polls check if there's a UI view under the cursor).
|
||||
("ui.view_start_filter", {"type": 'F', "value": 'PRESS', "ctrl": True}, None),
|
||||
("ui.view_scroll", {"type": 'WHEELUPMOUSE', "value": 'ANY'}, None),
|
||||
("ui.view_scroll", {"type": 'WHEELDOWNMOUSE', "value": 'ANY'}, None),
|
||||
])
|
||||
|
||||
return keymap
|
||||
|
||||
@@ -46,6 +46,11 @@ namespace blender::ui {
|
||||
class AbstractViewItem;
|
||||
class AbstractViewItemDragController;
|
||||
|
||||
enum class ViewScrollDirection {
|
||||
UP,
|
||||
DOWN,
|
||||
};
|
||||
|
||||
class AbstractView {
|
||||
friend class AbstractViewItem;
|
||||
friend struct ::ViewLink;
|
||||
@@ -92,10 +97,13 @@ class AbstractView {
|
||||
*/
|
||||
virtual bool begin_filtering(const bContext &C) const;
|
||||
|
||||
virtual void draw_overlays(const ARegion ®ion) const;
|
||||
virtual void draw_overlays(const ARegion ®ion, const uiBlock &block) const;
|
||||
|
||||
virtual void foreach_view_item(FunctionRef<void(AbstractViewItem &)> iter_fn) const = 0;
|
||||
|
||||
virtual bool supports_scrolling() const;
|
||||
virtual void scroll(ViewScrollDirection direction);
|
||||
|
||||
/**
|
||||
* Makes \a item valid for display in this view. Behavior is undefined for items not registered
|
||||
* with this.
|
||||
|
||||
@@ -112,29 +112,36 @@ using TreeViewOrItem = TreeViewItemContainer;
|
||||
* \{ */
|
||||
|
||||
class AbstractTreeView : public AbstractView, public TreeViewItemContainer {
|
||||
int min_rows_ = 0;
|
||||
/* 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 ®ion) const override;
|
||||
void draw_overlays(const ARegion ®ion, 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 ®ion, const int2 &xy);
|
||||
|
||||
/** Visual feature: Define a number of item rows the view will always show at minimum. If there
|
||||
/** 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_min_rows(int min_rows);
|
||||
void set_default_rows(int min_rows);
|
||||
|
||||
protected:
|
||||
virtual void build_tree() = 0;
|
||||
@@ -146,14 +153,18 @@ class AbstractTreeView : public AbstractView, public TreeViewItemContainer {
|
||||
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;
|
||||
|
||||
void draw_hierarchy_lines(const ARegion ®ion) const;
|
||||
void draw_hierarchy_lines_recursive(const ARegion ®ion,
|
||||
const TreeViewOrItem &parent,
|
||||
const uint pos,
|
||||
const float aspect) const;
|
||||
bool supports_scrolling() const override;
|
||||
|
||||
AbstractTreeViewItem *find_last_visible_descendant(const AbstractTreeViewItem &parent) const;
|
||||
void draw_hierarchy_lines(const ARegion ®ion, const uiBlock &block) const;
|
||||
void get_hierarchy_lines(const ARegion ®ion,
|
||||
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;
|
||||
};
|
||||
|
||||
/** \} */
|
||||
|
||||
@@ -2521,18 +2521,23 @@ static void UI_OT_list_start_filter(wmOperatorType *ot)
|
||||
/** \name UI View Start Filter Operator
|
||||
* \{ */
|
||||
|
||||
static bool ui_view_focused_poll(bContext *C)
|
||||
static AbstractView *get_view_focused(bContext *C)
|
||||
{
|
||||
const wmWindow *win = CTX_wm_window(C);
|
||||
if (!(win && win->eventstate)) {
|
||||
return false;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ARegion *region = CTX_wm_region(C);
|
||||
if (!region) {
|
||||
return false;
|
||||
return nullptr;
|
||||
}
|
||||
const blender::ui::AbstractView *view = UI_region_view_find_at(region, win->eventstate->xy, 0);
|
||||
return UI_region_view_find_at(region, win->eventstate->xy, 0);
|
||||
}
|
||||
|
||||
static bool ui_view_focused_poll(bContext *C)
|
||||
{
|
||||
const AbstractView *view = get_view_focused(C);
|
||||
return view != nullptr;
|
||||
}
|
||||
|
||||
@@ -2613,6 +2618,72 @@ static void UI_OT_view_drop(wmOperatorType *ot)
|
||||
|
||||
/** \} */
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name UI View Drop Operator
|
||||
* \{ */
|
||||
|
||||
static bool ui_view_scroll_poll(bContext *C)
|
||||
{
|
||||
const AbstractView *view = get_view_focused(C);
|
||||
if (!view) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return view->supports_scrolling();
|
||||
}
|
||||
|
||||
static int ui_view_scroll_invoke(bContext *C, wmOperator * /*op*/, const wmEvent *event)
|
||||
{
|
||||
ARegion *region = CTX_wm_region(C);
|
||||
int type = event->type;
|
||||
bool invert_direction = false;
|
||||
|
||||
if (type == MOUSEPAN) {
|
||||
int dummy_val;
|
||||
ui_pan_to_scroll(event, &type, &dummy_val);
|
||||
|
||||
/* 'ui_pan_to_scroll' gives the absolute direction. */
|
||||
if (event->flag & WM_EVENT_SCROLL_INVERT) {
|
||||
invert_direction = true;
|
||||
}
|
||||
}
|
||||
|
||||
AbstractView *view = get_view_focused(C);
|
||||
std::optional<ViewScrollDirection> direction =
|
||||
[type, invert_direction]() -> std::optional<ViewScrollDirection> {
|
||||
switch (type) {
|
||||
case WHEELUPMOUSE:
|
||||
return invert_direction ? ViewScrollDirection::DOWN : ViewScrollDirection::UP;
|
||||
case WHEELDOWNMOUSE:
|
||||
return invert_direction ? ViewScrollDirection::UP : ViewScrollDirection::DOWN;
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}();
|
||||
if (!direction) {
|
||||
return OPERATOR_CANCELLED;
|
||||
}
|
||||
|
||||
BLI_assert(view->supports_scrolling());
|
||||
view->scroll(*direction);
|
||||
|
||||
ED_region_tag_redraw(region);
|
||||
return OPERATOR_FINISHED;
|
||||
}
|
||||
|
||||
static void UI_OT_view_scroll(wmOperatorType *ot)
|
||||
{
|
||||
ot->name = "View Scroll";
|
||||
ot->idname = "UI_OT_view_scroll";
|
||||
|
||||
ot->invoke = ui_view_scroll_invoke;
|
||||
ot->poll = ui_view_scroll_poll;
|
||||
|
||||
ot->flag = OPTYPE_INTERNAL;
|
||||
}
|
||||
|
||||
/** \} */
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name UI View Item Rename Operator
|
||||
*
|
||||
@@ -2757,6 +2828,7 @@ void ED_operatortypes_ui()
|
||||
|
||||
WM_operatortype_append(UI_OT_view_start_filter);
|
||||
WM_operatortype_append(UI_OT_view_drop);
|
||||
WM_operatortype_append(UI_OT_view_scroll);
|
||||
WM_operatortype_append(UI_OT_view_item_rename);
|
||||
|
||||
WM_operatortype_append(UI_OT_override_type_set_button);
|
||||
|
||||
+1
-1
@@ -474,7 +474,7 @@ void uiTemplateBoneCollectionTree(uiLayout *layout, bContext *C)
|
||||
"Bone Collection Tree View",
|
||||
std::make_unique<blender::ui::bonecollections::BoneCollectionTreeView>(*armature));
|
||||
tree_view->set_context_menu_title("Bone Collection");
|
||||
tree_view->set_min_rows(3);
|
||||
tree_view->set_default_rows(3);
|
||||
|
||||
ui::TreeViewBuilder::build_tree_view(*tree_view, *layout);
|
||||
}
|
||||
|
||||
+1
-1
@@ -473,7 +473,7 @@ void uiTemplateGreasePencilLayerTree(uiLayout *layout, bContext *C)
|
||||
"Grease Pencil Layer Tree View",
|
||||
std::make_unique<blender::ui::greasepencil::LayerTreeView>(grease_pencil));
|
||||
tree_view->set_context_menu_title("Grease Pencil Layer");
|
||||
tree_view->set_min_rows(6);
|
||||
tree_view->set_default_rows(6);
|
||||
|
||||
ui::TreeViewBuilder::build_tree_view(*tree_view, *layout);
|
||||
}
|
||||
|
||||
@@ -400,7 +400,7 @@ void uiTemplateLightLinkingCollection(uiLayout *layout,
|
||||
"Light Linking Collection Tree View",
|
||||
std::make_unique<blender::ui::light_linking::CollectionView>(*context_layout, *collection));
|
||||
tree_view->set_context_menu_title("Light Linking");
|
||||
tree_view->set_min_rows(3);
|
||||
tree_view->set_default_rows(3);
|
||||
|
||||
blender::ui::TreeViewBuilder::build_tree_view(*tree_view, *layout);
|
||||
}
|
||||
|
||||
+1
-1
@@ -519,7 +519,7 @@ void uiTemplateNodeTreeInterface(uiLayout *layout, PointerRNA *ptr)
|
||||
"Node Tree Declaration Tree View",
|
||||
std::make_unique<blender::ui::nodes::NodeTreeInterfaceView>(nodetree, interface));
|
||||
tree_view->set_context_menu_title("Node Tree Interface");
|
||||
tree_view->set_min_rows(3);
|
||||
tree_view->set_default_rows(3);
|
||||
|
||||
blender::ui::TreeViewBuilder::build_tree_view(*tree_view, *layout);
|
||||
}
|
||||
|
||||
@@ -121,11 +121,21 @@ bool AbstractView::begin_filtering(const bContext & /*C*/) const
|
||||
return false;
|
||||
}
|
||||
|
||||
void AbstractView::draw_overlays(const ARegion & /*region*/) const
|
||||
void AbstractView::draw_overlays(const ARegion & /*region*/, const uiBlock & /*block*/) const
|
||||
{
|
||||
/* Nothing by default. */
|
||||
}
|
||||
|
||||
bool AbstractView::supports_scrolling() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void AbstractView::scroll(ViewScrollDirection /*direction*/)
|
||||
{
|
||||
BLI_assert_msg(false, "Unsupported for this view type");
|
||||
}
|
||||
|
||||
/** \} */
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
@@ -145,7 +145,7 @@ void ui_block_views_listen(const uiBlock *block, const wmRegionListenerParams *l
|
||||
void ui_block_views_draw_overlays(const ARegion *region, const uiBlock *block)
|
||||
{
|
||||
LISTBASE_FOREACH (ViewLink *, view_link, &block->views) {
|
||||
view_link->view->draw_overlays(*region);
|
||||
view_link->view->draw_overlays(*region, *block);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "interface_intern.hh"
|
||||
|
||||
#include "UI_interface.hh"
|
||||
#include "UI_view2d.hh"
|
||||
|
||||
#include "WM_api.hh"
|
||||
#include "WM_types.hh"
|
||||
@@ -124,75 +125,117 @@ AbstractTreeViewItem *AbstractTreeView::find_hovered(const ARegion ®ion, cons
|
||||
return hovered_item;
|
||||
}
|
||||
|
||||
void AbstractTreeView::set_min_rows(int min_rows)
|
||||
void AbstractTreeView::set_default_rows(int default_rows)
|
||||
{
|
||||
min_rows_ = min_rows;
|
||||
custom_height_ = std::make_unique<int>(default_rows * padded_item_height());
|
||||
}
|
||||
|
||||
AbstractTreeViewItem *AbstractTreeView::find_last_visible_descendant(
|
||||
const AbstractTreeViewItem &parent) const
|
||||
int AbstractTreeView::count_visible_descendants(const AbstractTreeViewItem &parent) const
|
||||
{
|
||||
if (parent.is_collapsed()) {
|
||||
return nullptr;
|
||||
return 0;
|
||||
}
|
||||
int count = 0;
|
||||
for (const auto &item : parent.children_) {
|
||||
if (!item->is_filtered_visible()) {
|
||||
continue;
|
||||
}
|
||||
count++;
|
||||
count += count_visible_descendants(*item);
|
||||
}
|
||||
|
||||
AbstractTreeViewItem *last_descendant = parent.children_.last().get();
|
||||
while (!last_descendant->children_.is_empty() && !last_descendant->is_collapsed()) {
|
||||
last_descendant = last_descendant->children_.last().get();
|
||||
}
|
||||
|
||||
return last_descendant;
|
||||
return count;
|
||||
}
|
||||
|
||||
void AbstractTreeView::draw_hierarchy_lines_recursive(const ARegion ®ion,
|
||||
const TreeViewOrItem &parent,
|
||||
const uint pos,
|
||||
const float aspect) const
|
||||
void AbstractTreeView::get_hierarchy_lines(const ARegion ®ion,
|
||||
const TreeViewOrItem &parent,
|
||||
const float aspect,
|
||||
Vector<std::pair<int2, int2>> &lines,
|
||||
int &visible_item_index) const
|
||||
{
|
||||
const int scroll_ofs = scroll_value_ ? *scroll_value_ : 0;
|
||||
const int max_visible_row_count = tot_visible_row_count().value_or(
|
||||
std::numeric_limits<int>::max());
|
||||
|
||||
for (const auto &item : parent.children_) {
|
||||
if (!item->is_filtered_visible()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int item_index = visible_item_index;
|
||||
visible_item_index++;
|
||||
|
||||
if (!item->is_collapsible() || item->is_collapsed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this->draw_hierarchy_lines_recursive(region, *item, pos, aspect);
|
||||
/* Draw a hierarchy line for the descendants of this item. */
|
||||
|
||||
const AbstractTreeViewItem *first_descendant = item->children_.first().get();
|
||||
const AbstractTreeViewItem *last_descendant = find_last_visible_descendant(*item);
|
||||
if (!first_descendant->view_item_but_ || !last_descendant || !last_descendant->view_item_but_)
|
||||
const int descendant_count = count_visible_descendants(*item);
|
||||
|
||||
const int first_descendant_index = item_index + 1;
|
||||
const int last_descendant_index = first_descendant_index + descendant_count;
|
||||
|
||||
{
|
||||
return;
|
||||
const bool line_ends_above_visible = last_descendant_index < scroll_ofs;
|
||||
if (line_ends_above_visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool line_starts_below_visible = first_descendant_index >
|
||||
(scroll_ofs + long(max_visible_row_count));
|
||||
/* Can return here even, following items won't be in view anymore. */
|
||||
if (line_starts_below_visible) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const uiButViewItem &first_child_but = *first_descendant->view_item_button();
|
||||
const uiButViewItem &last_child_but = *last_descendant->view_item_button();
|
||||
|
||||
BLI_assert(first_child_but.block == last_child_but.block);
|
||||
const uiBlock *block = first_child_but.block;
|
||||
const int x = ((first_descendant->indent_width() + uiLayoutListItemPaddingWidth() -
|
||||
(0.5f * UI_ICON_SIZE) + U.pixelsize + UI_SCALE_FAC) /
|
||||
aspect);
|
||||
const int ymax = std::max(0, first_descendant_index - scroll_ofs) * padded_item_height();
|
||||
const int ymin = std::min(max_visible_row_count, last_descendant_index - scroll_ofs) *
|
||||
padded_item_height();
|
||||
lines.append(std::make_pair(int2(x, ymax), int2(x, ymin)));
|
||||
|
||||
rcti first_child_rect;
|
||||
ui_but_to_pixelrect(&first_child_rect, ®ion, block, &first_child_but);
|
||||
rcti last_child_rect;
|
||||
ui_but_to_pixelrect(&last_child_rect, ®ion, block, &last_child_but);
|
||||
|
||||
const float x = first_child_rect.xmin +
|
||||
((first_descendant->indent_width() + uiLayoutListItemPaddingWidth() -
|
||||
(0.5f * UI_ICON_SIZE) + U.pixelsize) /
|
||||
aspect);
|
||||
const int first_child_top = first_child_rect.ymax - (2.0f * UI_SCALE_FAC / aspect);
|
||||
const int last_child_bottom = last_child_rect.ymin + (4.0f * UI_SCALE_FAC / aspect);
|
||||
immBegin(GPU_PRIM_LINES, 2);
|
||||
immVertex2f(pos, x, first_child_top);
|
||||
immVertex2f(pos, x, last_child_bottom);
|
||||
immEnd();
|
||||
this->get_hierarchy_lines(region, *item, aspect, lines, visible_item_index);
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractTreeView::draw_hierarchy_lines(const ARegion ®ion) const
|
||||
static uiButViewItem *find_first_view_item_but(const uiBlock &block, const AbstractTreeView &view)
|
||||
{
|
||||
LISTBASE_FOREACH (uiBut *, but, &block.buttons) {
|
||||
if (but->type != UI_BTYPE_VIEW_ITEM) {
|
||||
continue;
|
||||
}
|
||||
uiButViewItem *view_item_but = static_cast<uiButViewItem *>(but);
|
||||
if (&view_item_but->view_item->get_view() == &view) {
|
||||
return view_item_but;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void AbstractTreeView::draw_hierarchy_lines(const ARegion ®ion, const uiBlock &block) const
|
||||
{
|
||||
const float aspect = (region.v2d.flag & V2D_IS_INIT) ?
|
||||
BLI_rctf_size_y(®ion.v2d.cur) /
|
||||
(BLI_rcti_size_y(®ion.v2d.mask) + 1) :
|
||||
1.0f;
|
||||
|
||||
uiButViewItem *first_item_but = find_first_view_item_but(block, *this);
|
||||
if (!first_item_but) {
|
||||
return;
|
||||
}
|
||||
|
||||
Vector<std::pair<int2, int2>> lines;
|
||||
int index = 0;
|
||||
get_hierarchy_lines(region, *this, aspect, lines, index);
|
||||
if (lines.is_empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
GPUVertFormat *format = immVertexFormat();
|
||||
uint pos = GPU_vertformat_attr_add(format, "pos", GPU_COMP_F32, 2, GPU_FETCH_FLOAT);
|
||||
immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR);
|
||||
@@ -200,21 +243,33 @@ void AbstractTreeView::draw_hierarchy_lines(const ARegion ®ion) const
|
||||
|
||||
GPU_line_width(1.0f / aspect);
|
||||
GPU_blend(GPU_BLEND_ALPHA);
|
||||
draw_hierarchy_lines_recursive(region, *this, pos, aspect);
|
||||
|
||||
rcti first_item_but_pixel_rect;
|
||||
ui_but_to_pixelrect(&first_item_but_pixel_rect, ®ion, &block, first_item_but);
|
||||
int2 top_left{first_item_but_pixel_rect.xmin, first_item_but_pixel_rect.ymax};
|
||||
|
||||
for (const auto &line : lines) {
|
||||
immBegin(GPU_PRIM_LINES, 2);
|
||||
immVertex2f(pos, top_left.x + line.first.x, top_left.y - line.first.y);
|
||||
immVertex2f(pos, top_left.x + line.second.x, top_left.y - line.second.y);
|
||||
immEnd();
|
||||
}
|
||||
GPU_blend(GPU_BLEND_NONE);
|
||||
|
||||
immUnbindProgram();
|
||||
}
|
||||
|
||||
void AbstractTreeView::draw_overlays(const ARegion ®ion) const
|
||||
void AbstractTreeView::draw_overlays(const ARegion ®ion, const uiBlock &block) const
|
||||
{
|
||||
this->draw_hierarchy_lines(region);
|
||||
this->draw_hierarchy_lines(region, block);
|
||||
}
|
||||
|
||||
void AbstractTreeView::update_children_from_old(const AbstractView &old_view)
|
||||
{
|
||||
const AbstractTreeView &old_tree_view = dynamic_cast<const AbstractTreeView &>(old_view);
|
||||
|
||||
custom_height_ = old_tree_view.custom_height_;
|
||||
scroll_value_ = old_tree_view.scroll_value_;
|
||||
this->update_children_from_old_recursive(*this, old_tree_view);
|
||||
}
|
||||
|
||||
@@ -257,6 +312,31 @@ AbstractTreeViewItem *AbstractTreeView::find_matching_child(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::optional<int> AbstractTreeView::tot_visible_row_count() const
|
||||
{
|
||||
if (!custom_height_) {
|
||||
return {};
|
||||
}
|
||||
if (*custom_height_ < UI_UNIT_Y) {
|
||||
return 1;
|
||||
}
|
||||
return round_fl_to_int(float(*custom_height_) / padded_item_height());
|
||||
}
|
||||
|
||||
bool AbstractTreeView::supports_scrolling() const
|
||||
{
|
||||
return custom_height_ && scroll_value_;
|
||||
}
|
||||
|
||||
void AbstractTreeView::scroll(ViewScrollDirection direction)
|
||||
{
|
||||
if (!supports_scrolling()) {
|
||||
return;
|
||||
}
|
||||
/* Scroll value will be sanitized/clamped when drawing. */
|
||||
*scroll_value_ += ((direction == ViewScrollDirection::UP) ? -1 : 1);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
TreeViewItemDropTarget::TreeViewItemDropTarget(AbstractTreeViewItem &view_item,
|
||||
@@ -645,7 +725,7 @@ class TreeViewLayoutBuilder {
|
||||
friend TreeViewBuilder;
|
||||
|
||||
public:
|
||||
void build_from_tree(const AbstractTreeView &tree_view);
|
||||
void build_from_tree(AbstractTreeView &tree_view);
|
||||
void build_row(AbstractTreeViewItem &item) const;
|
||||
|
||||
uiBlock &block() const;
|
||||
@@ -660,23 +740,97 @@ TreeViewLayoutBuilder::TreeViewLayoutBuilder(uiLayout &layout) : block_(*uiLayou
|
||||
{
|
||||
}
|
||||
|
||||
void TreeViewLayoutBuilder::build_from_tree(const AbstractTreeView &tree_view)
|
||||
static int count_visible_items(AbstractTreeView &tree_view)
|
||||
{
|
||||
uiLayout &parent_layout = this->current_layout();
|
||||
|
||||
if (add_box_) {
|
||||
uiLayout *box = uiLayoutBox(&parent_layout);
|
||||
uiLayoutColumn(box, true);
|
||||
}
|
||||
else {
|
||||
uiLayoutColumn(&parent_layout, true);
|
||||
}
|
||||
|
||||
tree_view.foreach_item([this](AbstractTreeViewItem &item) { build_row(item); },
|
||||
int item_count = 0;
|
||||
tree_view.foreach_item([&](AbstractTreeViewItem &) { item_count++; },
|
||||
AbstractTreeView::IterOptions::SkipCollapsed |
|
||||
AbstractTreeView::IterOptions::SkipFiltered);
|
||||
return item_count;
|
||||
}
|
||||
|
||||
UI_block_layout_set_current(&block(), &parent_layout);
|
||||
void TreeViewLayoutBuilder::build_from_tree(AbstractTreeView &tree_view)
|
||||
{
|
||||
uiLayout &parent_layout = this->current_layout();
|
||||
uiBlock *block = uiLayoutGetBlock(&parent_layout);
|
||||
|
||||
uiLayout *col = nullptr;
|
||||
if (add_box_) {
|
||||
uiLayout *box = uiLayoutBox(&parent_layout);
|
||||
col = uiLayoutColumn(box, true);
|
||||
}
|
||||
else {
|
||||
col = uiLayoutColumn(&parent_layout, true);
|
||||
}
|
||||
/* Row for the tree-view and the scroll bar. */
|
||||
uiLayout *row = uiLayoutRow(col, false);
|
||||
|
||||
const std::optional<int> visible_row_count = tree_view.tot_visible_row_count();
|
||||
const int tot_items = count_visible_items(tree_view);
|
||||
|
||||
/* Column for the tree view. */
|
||||
uiLayoutColumn(row, true);
|
||||
|
||||
/* Clamp scroll-value to valid range. */
|
||||
if (tree_view.scroll_value_ && visible_row_count) {
|
||||
*tree_view.scroll_value_ = std::clamp(
|
||||
*tree_view.scroll_value_, 0, tot_items - *visible_row_count);
|
||||
}
|
||||
|
||||
const int first_visible_index = tree_view.scroll_value_ ? *tree_view.scroll_value_ : 0;
|
||||
const int max_visible_index = visible_row_count ? first_visible_index + *visible_row_count - 1 :
|
||||
std::numeric_limits<int>::max();
|
||||
int index = 0;
|
||||
tree_view.foreach_item(
|
||||
[&, this](AbstractTreeViewItem &item) {
|
||||
if ((index >= first_visible_index) && (index <= max_visible_index)) {
|
||||
this->build_row(item);
|
||||
}
|
||||
index++;
|
||||
},
|
||||
AbstractTreeView::IterOptions::SkipCollapsed | AbstractTreeView::IterOptions::SkipFiltered);
|
||||
|
||||
if (tree_view.custom_height_) {
|
||||
uiLayoutColumn(row, false);
|
||||
|
||||
*tree_view.custom_height_ = visible_row_count.value_or(1) * padded_item_height();
|
||||
if (!tree_view.scroll_value_) {
|
||||
tree_view.scroll_value_ = std::make_unique<int>(0);
|
||||
}
|
||||
|
||||
if (visible_row_count && (tot_items > *visible_row_count)) {
|
||||
uiBut *but = uiDefButI(block,
|
||||
UI_BTYPE_SCROLL,
|
||||
0,
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
V2D_SCROLL_WIDTH,
|
||||
*tree_view.custom_height_,
|
||||
tree_view.scroll_value_.get(),
|
||||
0,
|
||||
tot_items - *visible_row_count,
|
||||
"");
|
||||
uiButScrollBar *but_scroll = reinterpret_cast<uiButScrollBar *>(but);
|
||||
but_scroll->visual_height = *visible_row_count;
|
||||
}
|
||||
|
||||
UI_block_layout_set_current(block, col);
|
||||
uiDefIconButI(block,
|
||||
UI_BTYPE_GRIP,
|
||||
0,
|
||||
ICON_GRIP,
|
||||
0,
|
||||
0,
|
||||
UI_UNIT_X * 10,
|
||||
UI_UNIT_Y * 0.5f,
|
||||
tree_view.custom_height_.get(),
|
||||
0,
|
||||
0,
|
||||
"");
|
||||
}
|
||||
|
||||
UI_block_layout_set_current(block, &parent_layout);
|
||||
}
|
||||
|
||||
void TreeViewLayoutBuilder::build_row(AbstractTreeViewItem &item) const
|
||||
@@ -740,16 +894,21 @@ uiLayout &TreeViewLayoutBuilder::current_layout() const
|
||||
|
||||
void TreeViewBuilder::ensure_min_rows_items(AbstractTreeView &tree_view)
|
||||
{
|
||||
const std::optional<int> visible_rows = tree_view.tot_visible_row_count();
|
||||
if (!visible_rows) {
|
||||
return;
|
||||
}
|
||||
|
||||
int tot_visible_items = 0;
|
||||
tree_view.foreach_item(
|
||||
[&tot_visible_items](AbstractTreeViewItem & /*item*/) { tot_visible_items++; },
|
||||
AbstractTreeView::IterOptions::SkipCollapsed | AbstractTreeView::IterOptions::SkipFiltered);
|
||||
|
||||
if (tot_visible_items >= tree_view.min_rows_) {
|
||||
if (tot_visible_items >= *visible_rows) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < (tree_view.min_rows_ - tot_visible_items); i++) {
|
||||
for (int i = 0; i < (*visible_rows - tot_visible_items); i++) {
|
||||
BasicTreeViewItem &new_item = tree_view.add_tree_item<BasicTreeViewItem>("");
|
||||
new_item.disable_interaction();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user