From 7a01f736a59e0fcdea08c2f97fae52359229ae47 Mon Sep 17 00:00:00 2001 From: Maxime-Cots Date: Wed, 20 Aug 2025 17:53:34 +0200 Subject: [PATCH] Nodes: Make context path breadcrumbs interactive Make it possible to navigate in/out node groups through the breacrumbs context path overlay. See PR for details and screenshots. Pull Request: https://projects.blender.org/blender/blender/pulls/141292 --- scripts/startup/bl_operators/node.py | 10 +++++++- scripts/startup/bl_ui/space_node.py | 3 ++- .../blender/editors/include/UI_interface.hh | 5 +++- .../interface/interface_context_path.cc | 19 +++++++++++---- .../editors/space_node/node_context_path.cc | 24 +++++++++++++++++-- 5 files changed, 51 insertions(+), 10 deletions(-) diff --git a/scripts/startup/bl_operators/node.py b/scripts/startup/bl_operators/node.py index d3c5fa130f7..428c1bd0d4d 100644 --- a/scripts/startup/bl_operators/node.py +++ b/scripts/startup/bl_operators/node.py @@ -328,6 +328,12 @@ class NODE_OT_tree_path_parent(Operator): bl_label = "Parent Node Tree" bl_options = {'REGISTER', 'UNDO'} + parent_tree_index: IntProperty( + name="Parent Index", + description="Parent index in context path", + default=0, + ) + @classmethod def poll(cls, context): space = context.space_data @@ -337,7 +343,9 @@ class NODE_OT_tree_path_parent(Operator): def execute(self, context): space = context.space_data - space.path.pop() + parent_number_to_pop = len(space.path) - 1 - self.parent_tree_index + for _ in range(parent_number_to_pop): + space.path.pop() return {'FINISHED'} diff --git a/scripts/startup/bl_ui/space_node.py b/scripts/startup/bl_ui/space_node.py index b9aa50f6dc3..8c1af6cabce 100644 --- a/scripts/startup/bl_ui/space_node.py +++ b/scripts/startup/bl_ui/space_node.py @@ -193,7 +193,8 @@ class NODE_HT_header(Header): layout.separator_spacer() if len(snode.path) > 1: - layout.operator("node.tree_path_parent", text="", icon='FILE_PARENT') + op = layout.operator("node.tree_path_parent", text="", icon='FILE_PARENT') + op.parent_tree_index = len(snode.path) - 2 # Backdrop if is_compositor: diff --git a/source/blender/editors/include/UI_interface.hh b/source/blender/editors/include/UI_interface.hh index 5bd604feec0..39fc00af37a 100644 --- a/source/blender/editors/include/UI_interface.hh +++ b/source/blender/editors/include/UI_interface.hh @@ -76,12 +76,15 @@ struct ContextPathItem { /* #BIFIconID */ int icon; int icon_indicator_number; + + std::function handle_func; }; void context_path_add_generic(Vector &path, StructRNA &rna_type, void *ptr, - const BIFIconID icon_override = ICON_NONE); + const BIFIconID icon_override = ICON_NONE, + std::function handle_func = nullptr); void template_breadcrumbs(uiLayout &layout, Span context_path); diff --git a/source/blender/editors/interface/interface_context_path.cc b/source/blender/editors/interface/interface_context_path.cc index 1716f2c5ea0..622d0bf20a7 100644 --- a/source/blender/editors/interface/interface_context_path.cc +++ b/source/blender/editors/interface/interface_context_path.cc @@ -6,6 +6,7 @@ * \ingroup edinterface */ +#include "BLF_api.hh" #include "BLI_vector.hh" #include "RNA_access.hh" @@ -21,7 +22,8 @@ namespace blender::ui { void context_path_add_generic(Vector &path, StructRNA &rna_type, void *ptr, - const BIFIconID icon_override) + const BIFIconID icon_override, + std::function handle_func) { /* Add the null check here to make calling functions less verbose. */ if (!ptr) { @@ -38,10 +40,10 @@ void context_path_add_generic(Vector &path, if (&rna_type == &RNA_NodeTree) { ID *id = (ID *)ptr; - path.append({name, icon, ID_REAL_USERS(id)}); + path.append({name, icon, ID_REAL_USERS(id), handle_func}); } else { - path.append({name, icon, 1}); + path.append({name, icon, 1, handle_func}); } if (name != name_buf) { MEM_freeN(name); @@ -64,8 +66,15 @@ void template_breadcrumbs(uiLayout &layout, Span context_path) if (i > 0) { sub_row->label("", ICON_RIGHTARROW_THIN); } - uiBut *but = uiItemL_ex( - sub_row, context_path[i].name.c_str(), context_path[i].icon, false, false); + uiBut *but; + int icon = context_path[i].icon; + std::string name = context_path[i].name; + if (context_path[i].handle_func) { + but = sub_row->button(name.c_str(), icon, context_path[i].handle_func); + } + else { + but = uiItemL_ex(sub_row, name.c_str(), icon, false, false); + } UI_but_icon_indicator_number_set(but, context_path[i].icon_indicator_number); } } diff --git a/source/blender/editors/space_node/node_context_path.cc b/source/blender/editors/space_node/node_context_path.cc index 2f1104056ba..ab76cf9a877 100644 --- a/source/blender/editors/space_node/node_context_path.cc +++ b/source/blender/editors/space_node/node_context_path.cc @@ -16,10 +16,13 @@ #include "BKE_material.hh" #include "BKE_object.hh" +#include "RNA_access.hh" #include "RNA_prototypes.hh" #include "ED_screen.hh" +#include "WM_api.hh" + #include "UI_interface.hh" #include "UI_resources.hh" @@ -52,8 +55,25 @@ static void context_path_add_node_tree_and_node_groups(const SpaceNode &snode, Vector &path, const bool skip_base = false) { - LISTBASE_FOREACH (const bNodeTreePath *, path_item, &snode.treepath) { - if (!(skip_base && path_item == snode.treepath.first)) { + int i = 0; + LISTBASE_FOREACH_INDEX (const bNodeTreePath *, path_item, &snode.treepath, i) { + if (skip_base && path_item == snode.treepath.first) { + continue; + } + if (path_item != snode.treepath.last) { + // We don't need to add handle function to last nodetree + ui::context_path_add_generic( + path, RNA_NodeTree, path_item->nodetree, ICON_NODETREE, [i](bContext &C) { + PointerRNA op_props; + wmOperatorType *ot = WM_operatortype_find("NODE_OT_tree_path_parent", false); + WM_operator_properties_create_ptr(&op_props, ot); + RNA_int_set(&op_props, "parent_tree_index", i); + WM_operator_name_call_ptr( + &C, ot, blender::wm::OpCallContext::InvokeDefault, &op_props, nullptr); + WM_operator_properties_free(&op_props); + }); + } + else { ui::context_path_add_generic(path, RNA_NodeTree, path_item->nodetree, ICON_NODETREE); } }