UI: Follow HIG for view item selecting/activating

Makes selecting/activating view items (tree and grid view items) behave
as wanted by the guidelines, consistent with many other editors:
https://developer.blender.org/docs/features/interface/human_interface_guidelines/selection/#select-tweaking

Noteworthy:
- View items activate on press again, not on release
- Pose library still only activates poses on click (releasing mouse
  before moving cursor), so dragging can be used to blend poses
- New: Clicking on empty space in a view deselects all
- Redundant activation in interface handlers code is removed

Pull Request: https://projects.blender.org/blender/blender/pulls/146569
This commit is contained in:
Julian Eisel
2025-09-26 11:26:34 +02:00
committed by Julian Eisel
parent ebb843846d
commit 90f723bdd4
11 changed files with 134 additions and 21 deletions

View File

@@ -1042,7 +1042,7 @@ def km_user_interface(_params):
("ui.view_scroll", {"type": 'WHEELUPMOUSE', "value": 'ANY'}, None),
("ui.view_scroll", {"type": 'WHEELDOWNMOUSE', "value": 'ANY'}, None),
("ui.view_scroll", {"type": 'TRACKPADPAN', "value": 'ANY'}, None),
("ui.view_item_select", {"type": 'LEFTMOUSE', "value": 'CLICK'}, 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},

View File

@@ -420,7 +420,7 @@ def km_user_interface(params):
("anim.driver_button_remove", {"type": 'D', "value": 'PRESS', "alt": True}, None),
("anim.keyingset_button_add", {"type": 'K', "value": 'PRESS'}, None),
("anim.keyingset_button_remove", {"type": 'K', "value": 'PRESS', "alt": True}, None),
("ui.view_item_select", {"type": 'LEFTMOUSE', "value": 'CLICK'}, None),
("ui.view_item_select", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None),
])
return keymap

View File

@@ -124,6 +124,12 @@ void AssetView::build_items()
if (shelf_.type->flag & ASSET_SHELF_TYPE_FLAG_NO_ASSET_DRAG) {
item.disable_asset_drag();
}
if (!shelf_.type->drag_operator.empty()) {
/* For now always select/activate items on click instead of press when there's a drag
* operator set. Important for pose library blending. Maybe we want to make this an explicit
* option of the asset shelf instead. */
item.select_on_click_set();
}
/* Make sure every click calls the #bl_activate_operator. We might want to add a flag to
* enable/disable this. Or we only call #bl_activate_operator when an item becomes active, and
* add a #bl_click_operator for repeated execution on every click. So far it seems like every

View File

@@ -216,6 +216,8 @@ class AbstractViewItem {
* children currently.
*/
bool is_always_collapsible_ = false;
/** See #select_on_click_set(). */
bool select_on_click_ = false;
/** See #always_reactivate_on_click(). */
bool reactivate_on_click_ = false;
/** See #activate_for_context_menu_set(). */
@@ -312,6 +314,13 @@ class AbstractViewItem {
bool is_interactive() const;
void disable_activatable();
/**
* Configure this view item to only select/activate on mouse-click (i.e. when the mouse is
* pressed and released without much movement in-between); the default is to select/activate on
* mouse-press.
*/
void select_on_click_set();
bool is_select_on_click() const;
/** Call #on_activate() on every click on the item, even when the item was active before. */
void always_reactivate_on_click();
/** Call #on_activate() when spawning a context menu. Otherwise the item will only be highlighted

View File

@@ -264,6 +264,13 @@ void ui_region_to_window(const ARegion *region, int *x, int *y)
*y += region->winrct.ymin;
}
void ui_region_to_window(
const ARegion *region, int region_x, int region_y, int *r_window_x, int *r_window_y)
{
*r_window_x = region_x + region->winrct.xmin;
*r_window_y = region_y + region->winrct.ymin;
}
int uiBlock::but_index(const uiBut *but) const
{
BLI_assert(!buttons.is_empty() && but);

View File

@@ -5142,7 +5142,7 @@ static int ui_do_but_VIEW_ITEM(bContext *C,
BLI_assert(view_item_but->type == ButType::ViewItem);
if (data->state == BUTTON_STATE_HIGHLIGHT) {
if ((event->type == LEFTMOUSE) && (event->modifier == 0)) {
if (event->type == LEFTMOUSE) {
switch (event->val) {
case KM_PRESS:
/* Extra icons have priority, don't mess with them. */
@@ -5155,9 +5155,6 @@ static int ui_do_but_VIEW_ITEM(bContext *C,
data->dragstartx = event->xy[0];
data->dragstarty = event->xy[1];
}
else {
force_activate_view_item_but(C, data->region, view_item_but);
}
/* Always continue for drag and drop handling. Also for cases where keymap items are
* registered to add custom activate or drag operators (the pose library does this for

View File

@@ -756,6 +756,8 @@ void ui_window_to_region(const ARegion *region, int *x, int *y);
void ui_window_to_region_rcti(const ARegion *region, rcti *rect_dst, const rcti *rct_src);
void ui_window_to_region_rctf(const ARegion *region, rctf *rect_dst, const rctf *rct_src);
void ui_region_to_window(const ARegion *region, int *x, int *y);
void ui_region_to_window(
const ARegion *region, int region_x, int region_y, int *r_window_x, int *r_window_y);
/**
* Popups will add a margin to #ARegion.winrct for shadow,
* for interactivity (point-inside tests for eg), we want the winrct without the margin added.

View File

@@ -2771,27 +2771,32 @@ static void UI_OT_view_item_rename(wmOperatorType *ot)
ot->flag = OPTYPE_INTERNAL;
}
static wmOperatorStatus ui_view_item_select_invoke(bContext *C,
wmOperator *op,
const wmEvent *event)
static wmOperatorStatus view_item_click_select(bContext &C,
AbstractViewItem *clicked_item,
const AbstractView &view,
const bool extend,
const bool range_select,
bool wait_to_deselect_others)
{
ARegion &region = *CTX_wm_region(C);
const bool already_selected = clicked_item && clicked_item->is_selected();
AbstractViewItem *clicked_item = UI_region_views_find_item_at(region, event->xy);
if (clicked_item == nullptr) {
return OPERATOR_CANCELLED;
if (extend || range_select) {
wait_to_deselect_others = false;
}
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 (clicked_item && already_selected && wait_to_deselect_others) {
return OPERATOR_RUNNING_MODAL;
}
if (!extend) {
/* Keep previous selection for extend selection, see: !138979. */
view.foreach_view_item([](AbstractViewItem &item) { item.set_selected(false); });
}
if (clicked_item == nullptr) {
/* Only clear selection (if needed). */
return OPERATOR_FINISHED;
}
if (range_select) {
bool is_inside_range = false;
view.foreach_view_item([&](AbstractViewItem &item) {
@@ -2805,26 +2810,85 @@ static wmOperatorStatus ui_view_item_select_invoke(bContext *C,
item.set_selected(true);
}
});
ED_region_tag_redraw(&region);
return OPERATOR_FINISHED;
}
clicked_item->activate(*C);
clicked_item->activate(C);
return OPERATOR_FINISHED;
}
static std::pair<AbstractView *, AbstractViewItem *> select_operator_view_and_item_find_xy(
const ARegion &region, const wmOperator &op)
{
/* Mouse coordinates in window space. */
int window_xy[2];
{
/* Mouse coordinates in region space. */
int region_xy[2];
region_xy[0] = RNA_int_get(op.ptr, "mouse_x");
region_xy[1] = RNA_int_get(op.ptr, "mouse_y");
ui_region_to_window(&region, region_xy[0], region_xy[1], &window_xy[0], &window_xy[1]);
}
AbstractView *view = UI_region_view_find_at(&region, window_xy, 0);
AbstractViewItem *item = UI_region_views_find_item_at(region, window_xy);
BLI_assert(!item || &item->get_view() == view);
return std::make_pair(view, item);
}
static wmOperatorStatus ui_view_item_select_exec(bContext *C, wmOperator *op)
{
ARegion &region = *CTX_wm_region(C);
auto [view, clicked_item] = select_operator_view_and_item_find_xy(region, *op);
if (!view) {
return OPERATOR_CANCELLED;
}
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;
const bool wait_to_deselect_others = RNA_boolean_get(op->ptr, "wait_to_deselect_others");
const wmOperatorStatus status = view_item_click_select(
*C, clicked_item, *view, extend, range_select, wait_to_deselect_others);
ED_region_tag_redraw(&region);
return status;
}
static wmOperatorStatus ui_view_item_select_invoke(bContext *C,
wmOperator *op,
const wmEvent *event)
{
const ARegion &region = *CTX_wm_region(C);
const AbstractViewItem *clicked_item = UI_region_views_find_item_at(region, event->xy);
/* Wait with selecting to see if there's a click or drag event, if requested by the view item. */
if (clicked_item && clicked_item->is_select_on_click()) {
RNA_boolean_set(op->ptr, "use_select_on_click", true);
}
return WM_generic_select_invoke(C, op, event);
}
static void UI_OT_view_item_select(wmOperatorType *ot)
{
ot->name = "Select View Item";
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->modal = WM_generic_select_modal;
ot->poll = ui_view_focused_poll;
ot->flag = OPTYPE_INTERNAL;
WM_operator_properties_generic_select(ot);
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,

View File

@@ -340,6 +340,16 @@ void AbstractViewItem::disable_activatable()
is_activatable_ = false;
}
void AbstractViewItem::select_on_click_set()
{
select_on_click_ = true;
}
bool AbstractViewItem::is_select_on_click() const
{
return select_on_click_;
}
void AbstractViewItem::always_reactivate_on_click()
{
reactivate_on_click_ = true;

View File

@@ -512,6 +512,16 @@ void WM_operator_properties_generic_select(wmOperatorType *ot)
ot->srna, "wait_to_deselect_others", false, "Wait to Deselect Others", "");
RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
/* Force the selection to act on mouse click, not press. Necessary for some cases, but isn't used
* much. */
prop = RNA_def_boolean(ot->srna,
"use_select_on_click",
false,
"Act on Click",
"Instead of selecting on mouse press, wait to see if there's drag event. "
"Otherwise select on mouse release");
RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
RNA_def_int(ot->srna, "mouse_x", 0, INT_MIN, INT_MAX, "Mouse X", "", INT_MIN, INT_MAX);
RNA_def_int(ot->srna, "mouse_y", 0, INT_MIN, INT_MAX, "Mouse Y", "", INT_MIN, INT_MAX);
}

View File

@@ -982,19 +982,27 @@ wmOperatorStatus WM_generic_select_modal(bContext *C, wmOperator *op, const wmEv
{
PropertyRNA *wait_to_deselect_prop = RNA_struct_find_property(op->ptr,
"wait_to_deselect_others");
const bool use_select_on_click = RNA_struct_property_is_set(op->ptr, "use_select_on_click");
const short init_event_type = short(POINTER_AS_INT(op->customdata));
/* Get settings from RNA properties for operator. */
const int mval[2] = {RNA_int_get(op->ptr, "mouse_x"), RNA_int_get(op->ptr, "mouse_y")};
if (init_event_type == 0) {
op->customdata = POINTER_FROM_INT(int(event->type));
if (use_select_on_click) {
/* Don't do any selection yet. Wait to see if there's a drag or click (release) event. */
WM_event_add_modal_handler(C, op);
return OPERATOR_RUNNING_MODAL | OPERATOR_PASS_THROUGH;
}
if (event->val == KM_PRESS) {
RNA_property_boolean_set(op->ptr, wait_to_deselect_prop, true);
wmOperatorStatus retval = op->type->exec(C, op);
OPERATOR_RETVAL_CHECK(retval);
op->customdata = POINTER_FROM_INT(int(event->type));
if (retval & OPERATOR_RUNNING_MODAL) {
WM_event_add_modal_handler(C, op);
}