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:
committed by
Pratik Borhade
parent
0e8110fa94
commit
a559fb833c
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 ®ion = *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);
|
||||
}
|
||||
/** \} */
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user