Tree View: Multi-select support

Add support to select multiple tree view elements (similar to outliner/anim channels)
`Ctrl + LMB` to select+activate element under the mouse
`Shift + LMB` to select all items between active and clicked item.

As of now, only Shape key has support for multi-select. (straightforward to include
other views). `KEYBLOCK_SEL` flag is used for storing selection state.

Pull Request: https://projects.blender.org/blender/blender/pulls/138979
This commit is contained in:
Pratik Borhade
2025-07-04 15:45:18 +02:00
committed by Pratik Borhade
parent 0e8110fa94
commit a559fb833c
7 changed files with 111 additions and 6 deletions

View File

@@ -1043,6 +1043,10 @@ def km_user_interface(_params):
("ui.view_scroll", {"type": 'WHEELDOWNMOUSE', "value": 'ANY'}, None),
("ui.view_scroll", {"type": 'TRACKPADPAN', "value": 'ANY'}, None),
("ui.view_item_select", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None),
("ui.view_item_select", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
{"properties": [("extend", True)]}),
("ui.view_item_select", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
{"properties": [("range_select", True)]}),
])
return keymap

View File

@@ -76,6 +76,7 @@ class AbstractView {
std::string context_menu_title;
/** See #set_popup_keep_open(). */
bool popup_keep_open_ = false;
bool is_multiselect_supported_ = false;
public:
virtual ~AbstractView() = default;
@@ -152,6 +153,8 @@ class AbstractView {
void set_popup_keep_open();
void clear_search_highlight();
void allow_multiselect_items();
bool is_multiselect_supported() const;
protected:
AbstractView() = default;
@@ -199,6 +202,7 @@ class AbstractViewItem {
bool is_activatable_ = true;
bool is_interactive_ = true;
bool is_active_ = false;
bool is_selected_ = false;
bool is_renaming_ = false;
/** See #is_search_highlight(). */
bool is_highlighted_search_ = false;
@@ -232,6 +236,8 @@ class AbstractViewItem {
*/
virtual std::optional<bool> should_be_active() const;
virtual std::optional<bool> should_be_selected() const;
virtual void set_selected(const bool select);
/**
* Queries if the view item supports renaming in principle. Renaming may still fail, e.g. if
* another item is already being renamed.
@@ -306,6 +312,7 @@ class AbstractViewItem {
* can't be sure about the item state.
*/
bool is_active() const;
bool is_selected() const;
/**
* Should this item be highlighted as matching search result? Only one item should be highlighted
* this way at a time. Pressing enter will activate it.

View File

@@ -2792,16 +2792,47 @@ static void UI_OT_view_item_rename(wmOperatorType *ot)
ot->flag = OPTYPE_INTERNAL;
}
static wmOperatorStatus ui_view_item_select_exec(bContext *C, wmOperator * /*op*/)
static wmOperatorStatus ui_view_item_select_invoke(bContext *C,
wmOperator *op,
const wmEvent * /*event*/)
{
const wmWindow &win = *CTX_wm_window(C);
const ARegion &region = *CTX_wm_region(C);
if (AbstractViewItem *active_item = UI_region_views_find_item_at(region, win.eventstate->xy)) {
active_item->activate(*C);
AbstractViewItem *clicked_item = UI_region_views_find_item_at(region, win.eventstate->xy);
if (clicked_item == nullptr) {
return OPERATOR_CANCELLED;
}
AbstractView &view = clicked_item->get_view();
const bool is_multiselect = view.is_multiselect_supported();
const bool extend = RNA_boolean_get(op->ptr, "extend") && is_multiselect;
const bool range_select = RNA_boolean_get(op->ptr, "range_select") && is_multiselect;
if (!extend) {
/* Keep previous selection for extend selection, see: !138979. */
view.foreach_view_item([](AbstractViewItem &item) { item.set_selected(false); });
}
if (range_select) {
bool is_inside_range = false;
view.foreach_view_item([&](AbstractViewItem &item) {
if ((item.is_active()) ^ (&item == clicked_item)) {
is_inside_range = !is_inside_range;
/* Select end items from the range. */
item.set_selected(true);
}
if (is_inside_range) {
/* Select items within the range. */
item.set_selected(true);
}
});
return OPERATOR_FINISHED;
}
return OPERATOR_CANCELLED;
clicked_item->activate(*C);
return OPERATOR_FINISHED;
}
static void UI_OT_view_item_select(wmOperatorType *ot)
@@ -2810,10 +2841,19 @@ static void UI_OT_view_item_select(wmOperatorType *ot)
ot->idname = "UI_OT_view_item_select";
ot->description = "Activate selected view item";
ot->exec = ui_view_item_select_exec;
ot->invoke = ui_view_item_select_invoke;
ot->poll = ui_view_focused_poll;
ot->flag = OPTYPE_INTERNAL;
PropertyRNA *prop = RNA_def_boolean(ot->srna, "extend", false, "extend", "Extend Selection");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
prop = RNA_def_boolean(ot->srna,
"range_select",
false,
"Range Select",
"Select all between clicked and active items");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
}
/** \} */

View File

@@ -44,6 +44,8 @@
#include "GPU_matrix.hh"
#include "GPU_state.hh"
#include "UI_abstract_view.hh"
#ifdef WITH_INPUT_IME
# include "WM_types.hh"
#endif
@@ -4369,9 +4371,17 @@ static void widget_list_itembut(uiBut *but,
const float zoom)
{
rcti draw_rect = *rect;
bool is_selected = state->but_flag & UI_SELECT;
if (but->type == UI_BTYPE_VIEW_ITEM) {
uiButViewItem *item_but = static_cast<uiButViewItem *>(but);
blender::ui::AbstractViewItem &view_item = *item_but->view_item;
if (!view_item.is_active() && view_item.is_selected()) {
copy_v4_v4_uchar(wcol->inner, wcol->inner_sel);
color_blend_v3_v3(wcol->inner, wcol->outline, 0.5);
is_selected = true;
}
if (item_but->draw_width > 0) {
BLI_rcti_resize_x(&draw_rect, zoom * item_but->draw_width);
}
@@ -4388,7 +4398,7 @@ static void widget_list_itembut(uiBut *but,
if (state->but_flag & UI_HOVER) {
color_blend_v3_v3(wcol->inner, wcol->text, 0.2);
wcol->inner[3] = (state->but_flag & UI_SELECT) ? 255 : 20;
wcol->inner[3] = is_selected ? 255 : 20;
}
widgetbase_draw(&wtb, wcol);

View File

@@ -246,6 +246,16 @@ void AbstractView::clear_search_highlight()
{
this->foreach_view_item([](AbstractViewItem &item) { item.is_highlighted_search_ = false; });
}
void AbstractView::allow_multiselect_items()
{
is_multiselect_supported_ = true;
}
bool AbstractView::is_multiselect_supported() const
{
return is_multiselect_supported_;
}
/** \} */
} // namespace blender::ui

View File

@@ -30,6 +30,7 @@ void AbstractViewItem::update_from_old(const AbstractViewItem &old)
is_active_ = old.is_active_;
is_renaming_ = old.is_renaming_;
is_highlighted_search_ = old.is_highlighted_search_;
is_selected_ = old.is_selected_;
}
/** \} */
@@ -71,6 +72,7 @@ void AbstractViewItem::activate(bContext &C)
{
if (set_state_active()) {
on_activate(C);
set_selected(true);
}
}
@@ -79,6 +81,16 @@ void AbstractViewItem::deactivate()
is_active_ = false;
}
std::optional<bool> AbstractViewItem::should_be_selected() const
{
return std::nullopt;
}
void AbstractViewItem::set_selected(const bool select)
{
is_selected_ = select;
}
/** \} */
/* ---------------------------------------------------------------------- */
@@ -97,6 +109,9 @@ void AbstractViewItem::change_state_delayed()
is_active_ = false;
}
}
if (std::optional<bool> is_selected = should_be_selected()) {
set_selected(is_selected.value_or(false));
}
}
/** \} */
@@ -320,6 +335,13 @@ bool AbstractViewItem::is_active() const
return is_active_;
}
bool AbstractViewItem::is_selected() const
{
BLI_assert_msg(this->get_view().is_reconstructed(),
"State can't be queried until reconstruction is completed");
return is_selected_;
}
bool AbstractViewItem::is_search_highlight() const
{
return is_highlighted_search_;

View File

@@ -197,6 +197,17 @@ class ShapeKeyItem : public ui::AbstractTreeViewItem {
ED_undo_push(&C, "Set Active Shape Key");
}
std::optional<bool> should_be_selected() const override
{
return shape_key_.kb->flag & KEYBLOCK_SEL;
}
void set_selected(const bool select) override
{
AbstractViewItem::set_selected(select);
SET_FLAG_FROM_TEST(shape_key_.kb->flag, select, KEYBLOCK_SEL);
}
bool supports_renaming() const override
{
return true;
@@ -256,6 +267,7 @@ void template_tree(uiLayout *layout, bContext *C)
std::make_unique<ed::object::shapekey::ShapeKeyTreeView>(*ob));
tree_view->set_context_menu_title("Shape Key");
tree_view->set_default_rows(4);
tree_view->allow_multiselect_items();
ui::TreeViewBuilder::build_tree_view(*C, *tree_view, *layout);
}