diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index 970c5e0e32e..953c02554d1 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -1246,6 +1246,14 @@ def km_outliner(params): {"properties": [("extend_range", True), ("deselect_all", not params.legacy)]}), ("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True, "shift": True}, {"properties": [("extend", True), ("extend_range", True), ("deselect_all", not params.legacy)]}), + ("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK'}, + {"properties": [("recurse", True), ("deselect_all", True)]}), + ("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "ctrl": True}, + {"properties": [("recurse", True), ("extend", True), ("deselect_all", True)]}), + ("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "shift": True}, + {"properties": [("recurse", True), ("extend_range", True), ("deselect_all", True)]}), + ("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "ctrl": True, "shift": True}, + {"properties": [("recurse", True), ("extend", True), ("extend_range", True), ("deselect_all", True)]}), ("outliner.select_box", {"type": 'B', "value": 'PRESS'}, None), ("outliner.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": [("tweak", True)]}), ("outliner.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, diff --git a/scripts/presets/keyconfig/keymap_data/industry_compatible_data.py b/scripts/presets/keyconfig/keymap_data/industry_compatible_data.py index bd6a971b934..9ccfd81d2fb 100644 --- a/scripts/presets/keyconfig/keymap_data/industry_compatible_data.py +++ b/scripts/presets/keyconfig/keymap_data/industry_compatible_data.py @@ -486,6 +486,14 @@ def km_outliner(params): {"properties": [("extend", False), ("extend_range", True), ("deselect_all", True)]}), ("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True, "shift": True}, {"properties": [("extend", True), ("extend_range", True), ("deselect_all", True)]}), + ("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK'}, + {"properties": [("recurse", True), ("deselect_all", True)]}), + ("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "ctrl": True}, + {"properties": [("recurse", True), ("extend", True), ("deselect_all", True)]}), + ("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "shift": True}, + {"properties": [("recurse", True), ("extend_range", True), ("deselect_all", True)]}), + ("outliner.item_activate", {"type": 'LEFTMOUSE', "value": 'DOUBLE_CLICK', "ctrl": True, "shift": True}, + {"properties": [("recurse", True), ("extend", True), ("extend_range", True), ("deselect_all", True)]}), ("outliner.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": [("tweak", True)]}), ("outliner.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": [("tweak", True), ("mode", 'ADD')]}), diff --git a/source/blender/editors/space_outliner/outliner_edit.cc b/source/blender/editors/space_outliner/outliner_edit.cc index aa4088347f0..7734984970a 100644 --- a/source/blender/editors/space_outliner/outliner_edit.cc +++ b/source/blender/editors/space_outliner/outliner_edit.cc @@ -413,7 +413,7 @@ static int outliner_item_rename_invoke(bContext *C, wmOperator *op, const wmEven TreeElement *te = use_active ? outliner_item_rename_find_active(space_outliner, op->reports) : outliner_item_rename_find_hovered(space_outliner, region, event); if (!te) { - return OPERATOR_CANCELLED; + return OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH; } /* Force element into view. */ diff --git a/source/blender/editors/space_outliner/outliner_select.cc b/source/blender/editors/space_outliner/outliner_select.cc index 1e6ec29b4ab..6768d766210 100644 --- a/source/blender/editors/space_outliner/outliner_select.cc +++ b/source/blender/editors/space_outliner/outliner_select.cc @@ -1569,30 +1569,84 @@ void outliner_item_select(bContext *C, } } +static bool can_select_recursive(TreeElement *te, Collection *in_collection) +{ + if (te->store_elem->type == TSE_LAYER_COLLECTION) { + return true; + } + + if (te->store_elem->type == TSE_SOME_ID && te->idcode == ID_OB) { + /* Only actually select the object if + * 1. We are not restricted to any collection, or + * 2. The object is in fact in the given collection. */ + if (!in_collection || BKE_collection_has_object_recursive( + in_collection, reinterpret_cast(te->store_elem->id))) + { + return true; + } + } + + return false; +} + +static void do_outliner_select_recursive(ListBase *lb, bool selecting, Collection *in_collection) +{ + LISTBASE_FOREACH (TreeElement *, te, lb) { + TreeStoreElem *tselem = TREESTORE(te); + /* Recursive selection only on collections or objects. */ + if (can_select_recursive(te, in_collection)) { + tselem->flag = selecting ? (tselem->flag | TSE_SELECTED) : (tselem->flag & ~TSE_SELECTED); + if (tselem->type == TSE_LAYER_COLLECTION) { + /* Restrict sub-tree selections to this collection. This prevents undesirable behavior in + * the edge-case where there is an object which is part of this collection, but which has + * children that are part of another collection. */ + do_outliner_select_recursive( + &te->subtree, selecting, static_cast(te->directdata)->collection); + } + else { + do_outliner_select_recursive(&te->subtree, selecting, in_collection); + } + } + else { + tselem->flag &= ~TSE_SELECTED; + } + } +} + static bool do_outliner_range_select_recursive(ListBase *lb, TreeElement *active, TreeElement *cursor, - bool selecting) + bool selecting, + const bool recurse, + Collection *in_collection) { LISTBASE_FOREACH (TreeElement *, te, lb) { TreeStoreElem *tselem = TREESTORE(te); - if (selecting) { - tselem->flag |= TSE_SELECTED; - } + bool can_select = !recurse || can_select_recursive(te, in_collection); + + /* Remember if we are selecting before we potentially change the selecting state. */ + bool selecting_before = selecting; /* Set state for selection */ if (ELEM(te, active, cursor)) { selecting = !selecting; } - if (selecting) { + if (can_select && (selecting_before || selecting)) { tselem->flag |= TSE_SELECTED; } - /* Don't look inside closed elements */ - if (!(tselem->flag & TSE_CLOSED)) { - selecting = do_outliner_range_select_recursive(&te->subtree, active, cursor, selecting); + /* Don't look inside closed elements, unless we're forcing the recursion all the way down. */ + if (!(tselem->flag & TSE_CLOSED) || recurse) { + /* If this tree element is a collection, then it sets + * the precedent for inclusion of its subobjects. */ + Collection *child_collection = in_collection; + if (tselem->type == TSE_LAYER_COLLECTION) { + child_collection = static_cast(te->directdata)->collection; + } + selecting = do_outliner_range_select_recursive( + &te->subtree, active, cursor, selecting, recurse, child_collection); } } @@ -1603,7 +1657,9 @@ static bool do_outliner_range_select_recursive(ListBase *lb, static void do_outliner_range_select(bContext *C, SpaceOutliner *space_outliner, TreeElement *cursor, - const bool extend) + const bool extend, + const bool recurse, + Collection *in_collection) { TreeElement *active = outliner_find_element_with_flag(&space_outliner->tree, TSE_ACTIVE); @@ -1632,7 +1688,8 @@ static void do_outliner_range_select(bContext *C, return; } - do_outliner_range_select_recursive(&space_outliner->tree, active, cursor, false); + do_outliner_range_select_recursive( + &space_outliner->tree, active, cursor, false, recurse, in_collection); } static bool outliner_is_co_within_restrict_columns(const SpaceOutliner *space_outliner, @@ -1673,7 +1730,8 @@ static int outliner_item_do_activate_from_cursor(bContext *C, const int mval[2], const bool extend, const bool use_range, - const bool deselect_all) + const bool deselect_all, + const bool recurse) { ARegion *region = CTX_wm_region(C); SpaceOutliner *space_outliner = CTX_wm_space_outliner(C); @@ -1716,21 +1774,73 @@ static int outliner_item_do_activate_from_cursor(bContext *C, TreeStoreElem *activate_tselem = TREESTORE(activate_te); + /* If we're recursing, we need to know the collection of the selected item in order + * to prevent selecting across collection boundaries. (Object hierarchies might cross + * collection boundaries, i.e., children may be in different collections from their + * parents.) */ + Collection *parent_collection = nullptr; + if (recurse) { + if (activate_tselem->type == TSE_LAYER_COLLECTION) { + parent_collection = static_cast(activate_te->directdata)->collection; + } + else if (activate_tselem->type == TSE_SOME_ID && activate_te->idcode == ID_OB) { + parent_collection = BKE_collection_object_find( + CTX_data_main(C), + CTX_data_scene(C), + nullptr, + reinterpret_cast(activate_tselem->id)); + } + } + + /* If we're not recursing (not double clicking), and we are extending or range selecting by + * holding CTRL or SHIFT, ignore events when the cursor is over the icon. This disambiguates + * the case where we are recursing *and* holding CTRL or SHIFT in order to extend or range + * select recursively. */ + if (!recurse && (extend || use_range) && + outliner_item_is_co_over_icon(activate_te, view_mval[0])) + { + return OPERATOR_CANCELLED; + } + if (use_range) { - do_outliner_range_select(C, space_outliner, activate_te, extend); + do_outliner_range_select(C, space_outliner, activate_te, extend, recurse, parent_collection); + if (recurse) { + do_outliner_select_recursive(&activate_te->subtree, true, parent_collection); + } } else { const bool is_over_name_icons = outliner_item_is_co_over_name_icons(activate_te, view_mval[0]); - /* Always select unless already active and selected */ - const bool select = !extend || !(activate_tselem->flag & TSE_ACTIVE && - activate_tselem->flag & TSE_SELECTED); + /* Always select unless already active and selected. */ + bool select = !extend || !(activate_tselem->flag & TSE_ACTIVE) || + !(activate_tselem->flag & TSE_SELECTED); + + /* If we're CTRL+double-clicking and the element is aleady + * selected, skip the activation and go straight to deselection. */ + if (extend && recurse && activate_tselem->flag & TSE_SELECTED) { + select = false; + } const short select_flag = OL_ITEM_ACTIVATE | (select ? OL_ITEM_SELECT : OL_ITEM_DESELECT) | (is_over_name_icons ? OL_ITEM_SELECT_DATA : 0) | (extend ? OL_ITEM_EXTEND : 0); - outliner_item_select(C, space_outliner, activate_te, select_flag); + /* The recurse flag is set when the user double-clicks + * to select everything in a collection or hierarchy. */ + if (recurse) { + if (outliner_item_is_co_over_icon(activate_te, view_mval[0])) { + /* Select or deselect object hierarchy recursively. */ + outliner_item_select(C, space_outliner, activate_te, select_flag); + do_outliner_select_recursive(&activate_te->subtree, select, parent_collection); + } + else { + /* Double-clicked, but it wasn't on the icon. */ + return OPERATOR_CANCELLED; + } + } + else { + outliner_item_select(C, space_outliner, activate_te, select_flag); + } /* Only switch properties editor tabs when icons are selected. */ if (is_over_icon) { @@ -1765,10 +1875,11 @@ static int outliner_item_activate_invoke(bContext *C, wmOperator *op, const wmEv const bool extend = RNA_boolean_get(op->ptr, "extend"); const bool use_range = RNA_boolean_get(op->ptr, "extend_range"); const bool deselect_all = RNA_boolean_get(op->ptr, "deselect_all"); + const bool recurse = RNA_boolean_get(op->ptr, "recurse"); int mval[2]; WM_event_drag_start_mval(event, region, mval); - return outliner_item_do_activate_from_cursor(C, mval, extend, use_range, deselect_all); + return outliner_item_do_activate_from_cursor(C, mval, extend, use_range, deselect_all, recurse); } void OUTLINER_OT_item_activate(wmOperatorType *ot) @@ -1796,6 +1907,10 @@ void OUTLINER_OT_item_activate(wmOperatorType *ot) "Deselect On Nothing", "Deselect all when nothing under the cursor"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); + + prop = RNA_def_boolean( + ot->srna, "recurse", false, "Recurse", "Select objects recursively from active element"); + RNA_def_property_flag(prop, PROP_SKIP_SAVE); } /** \} */