From 098be390ca375cc320b3465b536923a027c9a499 Mon Sep 17 00:00:00 2001 From: Ramon Klauck Date: Wed, 3 Sep 2025 17:57:00 +0200 Subject: [PATCH] VSE: Implement Select Circle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This feature works like the select circle in any other modes. The user can press "C" in preview or timeline and then select or deselect strips by pressing the left or middle mouse button. It’s an enhancement for the VSE preview because: 1. It makes it more similar to other editors in Blender 2. This behavior makes it easier to select specific overlapping strips in preview, that is because the select circle only checks for the origin of the strip. Pull Request: https://projects.blender.org/blender/blender/pulls/141422 --- .../keyconfig/keymap_data/blender_default.py | 34 +++- .../startup/bl_ui/space_toolsystem_common.py | 11 +- .../startup/bl_ui/space_toolsystem_toolbar.py | 54 +++++- .../space_sequencer/sequencer_intern.hh | 1 + .../editors/space_sequencer/sequencer_ops.cc | 1 + .../space_sequencer/sequencer_select.cc | 162 ++++++++++++++++++ .../windowmanager/intern/wm_operators.cc | 1 + 7 files changed, 259 insertions(+), 5 deletions(-) diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index 2db0707a23f..8991ea4d220 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -3031,6 +3031,7 @@ def km_sequencer(params): ("sequencer.select_box", {"type": 'B', "value": 'PRESS'}, None), ("sequencer.select_box", {"type": 'B', "value": 'PRESS', "ctrl": True}, {"properties": [("include_handles", True)]}), + ("sequencer.select_circle", {"type": 'C', "value": 'PRESS'}, None), ("sequencer.select_grouped", {"type": 'G', "value": 'PRESS', "shift": True}, None), *_template_items_select_actions(params, "sequencer.select_all"), ("sequencer.split", {"type": 'K', "value": 'PRESS'}, @@ -3099,7 +3100,7 @@ def km_sequencer(params): ) ), op_menu("SEQUENCER_MT_add", {"type": 'A', "value": 'PRESS', "shift": True}), - op_menu("SEQUENCER_MT_change", {"type": 'C', "value": 'PRESS'}), + op_menu("SEQUENCER_MT_change", {"type": 'C', "value": 'PRESS', "shift": True}), op_menu_pie("SEQUENCER_MT_view_pie", {"type": 'ACCENT_GRAVE', "value": 'PRESS'}), ("sequencer.slip", {"type": 'S', "value": 'PRESS'}, {"properties": [("use_cursor_position", False)]}), ("wm.context_set_int", {"type": 'O', "value": 'PRESS'}, @@ -3199,6 +3200,7 @@ def km_sequencer_preview(params): ), *_template_items_select_actions(params, "sequencer.select_all"), ("sequencer.select_box", {"type": 'B', "value": 'PRESS'}, None), + ("sequencer.select_circle", {"type": 'C', "value": 'PRESS'}, None), # View. ("sequencer.view_selected", {"type": 'NUMPAD_PERIOD', "value": 'PRESS'}, None), @@ -8448,6 +8450,18 @@ def km_sequencer_tool_generic_select_box(params, *, fallback): ]}, ) +def km_sequencer_tool_generic_select_circle(params, *, fallback): + return ( + _fallback_id("Sequencer Tool: Select Circle", fallback), + {"space_type": 'SEQUENCE_EDITOR', "region_type": 'WINDOW'}, + {"items": [ + *([] if (fallback and not params.use_fallback_tool) else _template_items_tool_select_actions_simple( + "sequencer.select_circle", + **(params.select_tweak_event if (fallback and params.use_fallback_tool_select_mouse) else + {"type": params.tool_mouse, "value": 'PRESS'}), + properties=[("wait_for_input", False)])), + ]}, + ) def km_sequencer_preview_tool_generic_select(params, *, fallback): return ( @@ -8482,6 +8496,20 @@ def km_sequencer_preview_tool_generic_select_box(params, *, fallback): ) +def km_sequencer_preview_tool_generic_select_circle(params, *, fallback): + return ( + _fallback_id("Preview Tool: Select Circle", fallback), + {"space_type": 'SEQUENCE_EDITOR', "region_type": 'WINDOW'}, + {"items": [ + *([] if (fallback and not params.use_fallback_tool) else _template_items_tool_select_actions_simple( + "sequencer.select_circle", + **(params.select_tweak_event if (fallback and params.use_fallback_tool_select_mouse) else + {"type": params.tool_mouse, "value": 'PRESS'}), + properties=[("wait_for_input", False)])), + ]}, + ) + + def km_sequencer_preview_tool_generic_cursor(params): return ( "Preview Tool: Cursor", @@ -8867,6 +8895,10 @@ def generate_keymaps(params=None): for fallback in (False, True)), *(km_sequencer_preview_tool_generic_select_box(params, fallback=fallback) for fallback in (False, True)), + *(km_sequencer_preview_tool_generic_select_circle(params, fallback=fallback) + for fallback in (False, True)), + *(km_sequencer_tool_generic_select_circle(params, fallback=fallback) + for fallback in (False, True)), km_3d_view_tool_paint_grease_pencil_trim(params), km_3d_view_tool_edit_grease_pencil_texture_gradient(params), km_sequencer_tool_blade(params), diff --git a/scripts/startup/bl_ui/space_toolsystem_common.py b/scripts/startup/bl_ui/space_toolsystem_common.py index 24d672bc2eb..32cab6b2d27 100644 --- a/scripts/startup/bl_ui/space_toolsystem_common.py +++ b/scripts/startup/bl_ui/space_toolsystem_common.py @@ -1073,14 +1073,19 @@ def _activate_by_item(context, space_type, item, index, *, as_fallback=False): WindowManager = bpy.types.WindowManager handle_map = _activate_by_item._cursor_draw_handle - handle = handle_map.pop(space_type, None) + # view_type used when in VSE, check if view_type exists because not every space_data has it. + view_type = getattr(context.space_data, "view_type", None) + handle = handle_map.pop((space_type, view_type), None) if handle is not None: WindowManager.draw_cursor_remove(handle) if item.draw_cursor is not None: def handle_fn(context, item, tool, xy): item.draw_cursor(context, tool, xy) - handle = WindowManager.draw_cursor_add(handle_fn, (context, item, tool), space_type, 'WINDOW') - handle_map[space_type] = handle + if view_type == 'PREVIEW': + handle = WindowManager.draw_cursor_add(handle_fn, (context, item, tool), space_type, 'PREVIEW') + else: + handle = WindowManager.draw_cursor_add(handle_fn, (context, item, tool), space_type, 'WINDOW') + handle_map[(space_type, view_type)] = handle _activate_by_item._cursor_draw_handle = {} diff --git a/scripts/startup/bl_ui/space_toolsystem_toolbar.py b/scripts/startup/bl_ui/space_toolsystem_toolbar.py index 8e4ce562343..96d2fbe0328 100644 --- a/scripts/startup/bl_ui/space_toolsystem_toolbar.py +++ b/scripts/startup/bl_ui/space_toolsystem_toolbar.py @@ -3272,6 +3272,54 @@ class _defs_sequencer_select: draw_settings=draw_settings, ) + @ToolDef.from_fn + def circle_timeline(): + def draw_settings(_context, layout, tool): + props = tool.operator_properties("sequencer.select_circle") + row = layout.row() + row.use_property_split = False + row.prop(props, "mode", text="", expand=True, icon_only=True) + layout.prop(props, "radius") + + def draw_cursor(_context, tool, xy): + from gpu_extras.presets import draw_circle_2d + props = tool.operator_properties("sequencer.select_circle") + radius = props.radius + draw_circle_2d(xy, (1.0,) * 4, radius, segments=32) + return dict( + idname="sequencer.select_circle", + label="Select Circle", + icon="ops.generic.select_circle", + widget=None, + keymap="Sequencer Tool: Select Circle", + draw_settings=draw_settings, + draw_cursor=draw_cursor, + ) + + @ToolDef.from_fn + def circle_preview(): + def draw_settings(_context, layout, tool): + props = tool.operator_properties("sequencer.select_circle") + row = layout.row() + row.use_property_split = False + row.prop(props, "mode", text="", expand=True, icon_only=True) + layout.prop(props, "radius") + + def draw_cursor(_context, tool, xy): + from gpu_extras.presets import draw_circle_2d + props = tool.operator_properties("sequencer.select_circle") + radius = props.radius + draw_circle_2d(xy, (1.0,) * 4, radius, segments=32) + return dict( + idname="sequencer.select_circle", + label="Select Circle", + icon="ops.generic.select_circle", + widget=None, + keymap="Preview Tool: Select Circle", + draw_settings=draw_settings, + draw_cursor=draw_cursor, + ) + class IMAGE_PT_tools_active(ToolSelectPanelHelper, Panel): bl_space_type = 'IMAGE_EDITOR' @@ -3958,6 +4006,7 @@ class SEQUENCER_PT_tools_active(ToolSelectPanelHelper, Panel): ( _defs_sequencer_select.select_preview, _defs_sequencer_select.box_preview, + _defs_sequencer_select.circle_preview, ), _defs_sequencer_generic.cursor, None, @@ -3970,7 +4019,10 @@ class SEQUENCER_PT_tools_active(ToolSelectPanelHelper, Panel): *_tools_annotate, ], 'SEQUENCER': [ - _defs_sequencer_select.box_timeline, + ( + _defs_sequencer_select.box_timeline, + _defs_sequencer_select.circle_timeline, + ), _defs_sequencer_generic.blade, _defs_sequencer_generic.slip ], diff --git a/source/blender/editors/space_sequencer/sequencer_intern.hh b/source/blender/editors/space_sequencer/sequencer_intern.hh index 58f1e4bf0c2..a2898ee109a 100644 --- a/source/blender/editors/space_sequencer/sequencer_intern.hh +++ b/source/blender/editors/space_sequencer/sequencer_intern.hh @@ -274,6 +274,7 @@ void SEQUENCER_OT_select_linked_pick(wmOperatorType *ot); void SEQUENCER_OT_select_handles(wmOperatorType *ot); void SEQUENCER_OT_select_side(wmOperatorType *ot); void SEQUENCER_OT_select_box(wmOperatorType *ot); +void SEQUENCER_OT_select_circle(wmOperatorType *ot); void SEQUENCER_OT_select_inverse(wmOperatorType *ot); void SEQUENCER_OT_select_grouped(wmOperatorType *ot); diff --git a/source/blender/editors/space_sequencer/sequencer_ops.cc b/source/blender/editors/space_sequencer/sequencer_ops.cc index 105629e78c4..0a16538f9ef 100644 --- a/source/blender/editors/space_sequencer/sequencer_ops.cc +++ b/source/blender/editors/space_sequencer/sequencer_ops.cc @@ -105,6 +105,7 @@ void sequencer_operatortypes() WM_operatortype_append(SEQUENCER_OT_select_side); WM_operatortype_append(SEQUENCER_OT_select_side_of_frame); WM_operatortype_append(SEQUENCER_OT_select_box); + WM_operatortype_append(SEQUENCER_OT_select_circle); WM_operatortype_append(SEQUENCER_OT_select_grouped); /* `sequencer_add.cc` */ diff --git a/source/blender/editors/space_sequencer/sequencer_select.cc b/source/blender/editors/space_sequencer/sequencer_select.cc index 6c92d4f0636..709523ae145 100644 --- a/source/blender/editors/space_sequencer/sequencer_select.cc +++ b/source/blender/editors/space_sequencer/sequencer_select.cc @@ -10,6 +10,7 @@ #include #include +#include "BLI_rect.h" #include "MEM_guardedalloc.h" #include "BLI_ghash.h" @@ -2325,6 +2326,167 @@ void SEQUENCER_OT_select_box(wmOperatorType *ot) RNA_def_property_flag(prop, PROP_SKIP_SAVE); } +static bool strip_circle_select_radius_image_isect(const Scene *scene, + const Strip *strip, + const int *radius, + const float2 mval) +{ + float2 origin = seq::image_transform_origin_offset_pixelspace_get(scene, strip); + + float dx = origin.x - float(mval[0]); + float dy = origin.y - float(mval[1]); + float dist_sq = sqrt(dx * dx + dy * dy); + + return dist_sq <= *radius; +} + +static void seq_circle_select_strip_from_preview(bContext *C, + int radius, + const float2 mval, + const eSelectOp mode) +{ + Scene *scene = CTX_data_scene(C); + Editing *ed = seq::editing_get(scene); + ListBase *seqbase = seq::active_seqbase_get(ed); + ListBase *channels = seq::channels_displayed_get(ed); + SpaceSeq *sseq = CTX_wm_space_seq(C); + + VectorSet strips = seq::query_rendered_strips( + scene, channels, seqbase, scene->r.cfra, sseq->chanshown); + for (Strip *strip : strips) { + if (!strip_circle_select_radius_image_isect(scene, strip, &radius, mval)) { + continue; + } + + if (ELEM(mode, SEL_OP_ADD, SEL_OP_SET)) { + strip->flag |= SELECT; + } + else { + BLI_assert(mode == SEL_OP_SUB); + strip->flag &= ~SELECT; + } + } +} + +static bool check_circle_intersection_in_timeline(const rctf *rect, + const float xy[2], + const float x_radius, + const float y_radius) +{ + float dx, dy; + + if (xy[0] >= rect->xmin && xy[0] <= rect->xmax) { + dx = 0; + } + else { + dx = (xy[0] < rect->xmin) ? (rect->xmin - xy[0]) : (xy[0] - rect->xmax); + } + + if (xy[1] >= rect->ymin && xy[1] <= rect->ymax) { + dy = 0; + } + else { + dy = (xy[1] < rect->ymin) ? (rect->ymin - xy[1]) : (xy[1] - rect->ymax); + } + + return ((dx * dx) / (x_radius * x_radius) + (dy * dy) / (y_radius * y_radius) <= 1.0f); +} +static wmOperatorStatus vse_circle_select_exec(bContext *C, wmOperator *op) +{ + const int radius = RNA_int_get(op->ptr, "radius"); + const int mval[2] = {RNA_int_get(op->ptr, "x"), RNA_int_get(op->ptr, "y")}; + wmGesture *gesture = static_cast(op->customdata); + const eSelectOp sel_op = eSelectOp(RNA_enum_get(op->ptr, "mode")); + + Scene *scene = CTX_data_scene(C); + View2D *v2d = UI_view2d_fromcontext(C); + Editing *ed = seq::editing_get(scene); + ARegion *region = CTX_wm_region(C); + + const bool use_pre_deselect = SEL_OP_USE_PRE_DESELECT(sel_op); + + if (use_pre_deselect && WM_gesture_is_modal_first(gesture)) { + deselect_all_strips(scene); + sequencer_select_do_updates(C, scene); + } + + if (ed == nullptr) { + return OPERATOR_CANCELLED; + } + + float2 view_mval; + UI_view2d_region_to_view(v2d, mval[0], mval[1], &view_mval[0], &view_mval[1]); + float pixel_radius = radius / UI_view2d_scale_get_x(v2d); + + if (region->regiontype == RGN_TYPE_PREVIEW) { + seq_circle_select_strip_from_preview(C, pixel_radius, view_mval, sel_op); + sequencer_select_do_updates(C, scene); + return OPERATOR_FINISHED; + } + + float x_radius = radius / UI_view2d_scale_get_x(v2d); + float y_radius = radius / UI_view2d_scale_get_y(v2d); + bool changed = false; + LISTBASE_FOREACH (Strip *, strip, ed->seqbasep) { + rctf rq; + strip_rectf(scene, strip, &rq); + /* Use custom function to check the distance because in timeline the circle is a ellipse. */ + if (check_circle_intersection_in_timeline(&rq, view_mval, x_radius, y_radius)) { + if (ELEM(sel_op, SEL_OP_ADD, SEL_OP_SET)) { + strip->flag |= SELECT; + } + else { + BLI_assert(sel_op == SEL_OP_SUB); + strip->flag &= ~SELECT; + } + changed = true; + + const bool ignore_connections = RNA_boolean_get(op->ptr, "ignore_connections"); + if (!ignore_connections) { + /* Propagate selection to connected strips. */ + StripSelection selection; + selection.strip1 = strip; + sequencer_select_connected_strips(selection); + } + } + } + if (changed) { + sequencer_select_do_updates(C, scene); + } + return OPERATOR_FINISHED; +} + +void SEQUENCER_OT_select_circle(wmOperatorType *ot) +{ + PropertyRNA *prop; + + ot->name = "Circle Select"; + ot->description = "Select strips using circle selection"; + ot->idname = "SEQUENCER_OT_select_circle"; + + ot->invoke = WM_gesture_circle_invoke; + ot->modal = WM_gesture_circle_modal; + ot->exec = vse_circle_select_exec; + + ot->poll = ED_operator_sequencer_active; + + ot->get_name = ED_select_circle_get_name; + + /* flags */ + ot->flag = OPTYPE_UNDO; + + /* properties */ + WM_operator_properties_gesture_circle(ot); + WM_operator_properties_select_operation_simple(ot); + + prop = RNA_def_boolean(ot->srna, + "ignore_connections", + false, + "Ignore Connections", + "Select strips individually whether or not they are connected"); + RNA_def_property_flag(prop, PROP_SKIP_SAVE); +} + /** \} */ /* -------------------------------------------------------------------- */ diff --git a/source/blender/windowmanager/intern/wm_operators.cc b/source/blender/windowmanager/intern/wm_operators.cc index 69b491b3c82..71878863484 100644 --- a/source/blender/windowmanager/intern/wm_operators.cc +++ b/source/blender/windowmanager/intern/wm_operators.cc @@ -4276,6 +4276,7 @@ static void gesture_circle_modal_keymap(wmKeyConfig *keyconf) /* Assign map to operators. */ WM_modalkeymap_assign(keymap, "VIEW3D_OT_select_circle"); WM_modalkeymap_assign(keymap, "UV_OT_select_circle"); + WM_modalkeymap_assign(keymap, "SEQUENCER_OT_select_circle"); WM_modalkeymap_assign(keymap, "CLIP_OT_select_circle"); WM_modalkeymap_assign(keymap, "MASK_OT_select_circle"); WM_modalkeymap_assign(keymap, "NODE_OT_select_circle");