Nodes: support boolean inputs as toggles in panel headers

Adds the option to create a boolean socket that can be used as a panel toggle.
This allows creating simpler and more compact node group UIs when a panel
can be "disabled".

The toggle input is a normal input socket that is just drawn a bit differently in
the UI. Whether a boolean is a toggle input or not does not affect evaluation.

Also see #133936 for guides on how to add and remove panel toggles.

Pull Request: https://projects.blender.org/blender/blender/pulls/133936
This commit is contained in:
Falk David
2025-02-28 19:07:02 +01:00
committed by Jacques Lucke
parent 21e14163c6
commit 2822777f13
15 changed files with 464 additions and 57 deletions

View File

@@ -301,15 +301,29 @@ class NODE_OT_interface_item_new(NodeInterfaceOperator, Operator):
bl_label = "New Item"
bl_options = {'REGISTER', 'UNDO'}
item_type: EnumProperty(
name="Item Type",
description="Type of the item to create",
items=(
def get_items(_self, context):
snode = context.space_data
tree = snode.edit_tree
interface = tree.interface
items = [
('INPUT', "Input", ""),
('OUTPUT', "Output", ""),
('PANEL', "Panel", ""),
),
default='INPUT',
]
active_item = interface.active
# Panels have the extra option to add a toggle.
if active_item and active_item.item_type == 'PANEL':
items.append(('PANEL_TOGGLE', "Panel Toggle", ""))
return items
item_type: EnumProperty(
name="Item Type",
description="Type of the item to create",
items=get_items,
default=0,
)
# Returns a valid socket type for the given tree or None.
@@ -347,6 +361,18 @@ class NODE_OT_interface_item_new(NodeInterfaceOperator, Operator):
item = interface.new_socket("Socket", socket_type=self.find_valid_socket_type(tree), in_out='OUTPUT')
elif self.item_type == 'PANEL':
item = interface.new_panel("Panel")
elif self.item_type == 'PANEL_TOGGLE':
active_panel = active_item
if len(active_panel.interface_items) > 0:
first_item = active_panel.interface_items[0]
if type(first_item) is bpy.types.NodeTreeInterfaceSocketBool and first_item.is_panel_toggle:
self.report({'INFO'}, "Panel already has a toggle")
return {'CANCELLED'}
item = interface.new_socket(active_panel.name, socket_type='NodeSocketBool', in_out='INPUT')
item.is_panel_toggle = True
interface.move_to_parent(item, active_panel, 0)
# Return in this case because we don't want to move the item.
return {'FINISHED'}
else:
return {'CANCELLED'}
@@ -409,6 +435,110 @@ class NODE_OT_interface_item_remove(NodeInterfaceOperator, Operator):
return {'FINISHED'}
class NODE_OT_interface_item_make_panel_toggle(NodeInterfaceOperator, Operator):
"""Make the active boolean socket a toggle for its parent panel"""
bl_idname = "node.interface_item_make_panel_toggle"
bl_label = "Make Panel Toggle"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
if not super().poll(context):
return False
snode = context.space_data
tree = snode.edit_tree
interface = tree.interface
active_item = interface.active
if not active_item:
return False
if type(active_item) is not bpy.types.NodeTreeInterfaceSocketBool:
cls.poll_message_set("Only boolean sockets are supported")
return False
parent_panel = active_item.parent
if parent_panel.parent is None:
cls.poll_message_set("Socket must be in a panel")
return False
if len(parent_panel.interface_items) > 0:
first_item = parent_panel.interface_items[0]
if first_item.is_panel_toggle:
cls.poll_message_set("Panel already has a toggle")
return False
return True
def execute(self, context):
snode = context.space_data
tree = snode.edit_tree
interface = tree.interface
active_item = interface.active
parent_panel = active_item.parent
if not parent_panel:
return {'CANCELLED'}
if not type(active_item) is bpy.types.NodeTreeInterfaceSocketBool:
return {'CANCELLED'}
active_item.is_panel_toggle = True
# Use the same name as the panel in the UI for clarity.
active_item.name = parent_panel.name
# Move the socket to the first position.
interface.move_to_parent(active_item, parent_panel, 0)
# Make the panel active.
interface.active = parent_panel
return {'FINISHED'}
class NODE_OT_interface_item_unlink_panel_toggle(NodeInterfaceOperator, Operator):
"""Make the panel toggle a stand-alone socket"""
bl_idname = "node.interface_item_unlink_panel_toggle"
bl_label = "Unlink Panel Toggle"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
if not super().poll(context):
return False
snode = context.space_data
tree = snode.edit_tree
interface = tree.interface
active_item = interface.active
if not active_item or active_item.item_type != 'PANEL':
return False
if len(active_item.interface_items) == 0:
return False
first_item = active_item.interface_items[0]
return first_item.is_panel_toggle
def execute(self, context):
snode = context.space_data
tree = snode.edit_tree
interface = tree.interface
active_item = interface.active
if not active_item or active_item.item_type != 'PANEL':
return {'CANCELLED'}
if len(active_item.interface_items) == 0:
return {'CANCELLED'}
first_item = active_item.interface_items[0]
if not type(first_item) is bpy.types.NodeTreeInterfaceSocketBool or not first_item.is_panel_toggle:
return {'CANCELLED'}
first_item.is_panel_toggle = False
first_item.name = active_item.name
# Make the socket active.
interface.active = first_item
return {'FINISHED'}
class NODE_OT_viewer_shortcut_set(Operator):
"""Create a compositor viewer shortcut for the selected node by pressing ctrl+1,2,..9"""
bl_idname = "node.viewer_shortcut_set"
@@ -551,6 +681,8 @@ classes = (
NODE_OT_interface_item_new,
NODE_OT_interface_item_duplicate,
NODE_OT_interface_item_remove,
NODE_OT_interface_item_make_panel_toggle,
NODE_OT_interface_item_unlink_panel_toggle,
NODE_OT_tree_path_parent,
NODE_OT_viewer_shortcut_get,
NODE_OT_viewer_shortcut_set,

View File

@@ -904,10 +904,18 @@ class NODE_PT_overlay(Panel):
class NODE_MT_node_tree_interface_context_menu(Menu):
bl_label = "Node Tree Interface Specials"
def draw(self, _context):
def draw(self, context):
layout = self.layout
snode = context.space_data
tree = snode.edit_tree
active_item = tree.interface.active
layout.operator("node.interface_item_duplicate", icon='DUPLICATE')
layout.separator()
if active_item.item_type == 'SOCKET':
layout.operator("node.interface_item_make_panel_toggle")
elif active_item.item_type == 'PANEL':
layout.operator("node.interface_item_unlink_panel_toggle")
class NODE_PT_node_tree_interface(Panel):
@@ -978,6 +986,47 @@ class NODE_PT_node_tree_interface(Panel):
layout.use_property_split = False
class NODE_PT_node_tree_interface_panel_toggle(Panel):
bl_space_type = 'NODE_EDITOR'
bl_region_type = 'UI'
bl_category = "Group"
bl_parent_id = "NODE_PT_node_tree_interface"
bl_label = "Panel Toggle"
@classmethod
def poll(cls, context):
snode = context.space_data
if snode is None:
return False
tree = snode.edit_tree
if tree is None:
return False
active_item = tree.interface.active
if not active_item or active_item.item_type != 'PANEL':
return False
if not active_item.interface_items:
return False
first_item = active_item.interface_items[0]
return first_item.is_panel_toggle
def draw(self, context):
layout = self.layout
snode = context.space_data
tree = snode.edit_tree
active_item = tree.interface.active
panel_toggle_item = active_item.interface_items[0]
layout.use_property_split = True
layout.use_property_decorate = False
layout.prop(panel_toggle_item, "default_value", text="Default")
layout.prop(panel_toggle_item, "hide_in_modifier")
layout.prop(panel_toggle_item, "force_non_field")
layout.use_property_split = False
class NODE_PT_node_tree_properties(Panel):
bl_space_type = 'NODE_EDITOR'
bl_region_type = 'UI'
@@ -1081,6 +1130,7 @@ classes = (
NODE_PT_node_tree_properties,
NODE_MT_node_tree_interface_context_menu,
NODE_PT_node_tree_interface,
NODE_PT_node_tree_interface_panel_toggle,
NODE_PT_active_node_generic,
NODE_PT_active_node_color,
NODE_PT_texture_mapping,

View File

@@ -264,6 +264,8 @@ class bNodePanelRuntime : NonCopyable, NonMovable {
* #bNode::runtime::draw_bounds). */
std::optional<float> header_center_y;
std::optional<bNodePanelExtent> content_extent;
/** Optional socket that is part of the panel header. */
bNodeSocket *input_socket = nullptr;
};
/**

View File

@@ -1036,6 +1036,33 @@ void bNodeTreeInterfacePanel::foreach_item(
}
}
const bNodeTreeInterfaceSocket *bNodeTreeInterfacePanel::header_toggle_socket() const
{
if (this->items().is_empty()) {
return nullptr;
}
const bNodeTreeInterfaceItem *first_item = this->items().first();
if (first_item->item_type != NODE_INTERFACE_SOCKET) {
return nullptr;
}
const auto &socket = *reinterpret_cast<const bNodeTreeInterfaceSocket *>(first_item);
if (!(socket.flag & NODE_INTERFACE_SOCKET_INPUT) ||
!(socket.flag & NODE_INTERFACE_SOCKET_PANEL_TOGGLE))
{
return nullptr;
}
const blender::bke::bNodeSocketType *typeinfo = socket.socket_typeinfo();
if (!typeinfo || typeinfo->type != SOCK_BOOLEAN) {
return nullptr;
}
return &socket;
}
bNodeTreeInterfaceSocket *bNodeTreeInterfacePanel::header_toggle_socket()
{
return const_cast<bNodeTreeInterfaceSocket *>(
const_cast<const bNodeTreeInterfacePanel *>(this)->header_toggle_socket());
}
namespace blender::bke::node_interface {
static bNodeTreeInterfaceSocket *make_socket(const int uid,

View File

@@ -11,6 +11,7 @@
#include "BLI_rand.hh"
#include "BLI_set.hh"
#include "BLI_stack.hh"
#include "BLI_string.h"
#include "BLI_string_utf8_symbols.h"
#include "BLI_vector_set.hh"
@@ -488,6 +489,10 @@ class NodeTreeMainUpdater {
ntree.runtime->link_errors_by_target_node.clear();
if (this->update_panel_toggle_names(ntree)) {
result.interface_changed = true;
}
this->update_socket_link_and_use(ntree);
this->update_individual_nodes(ntree);
this->update_internal_links(ntree);
@@ -1711,6 +1716,29 @@ class NodeTreeMainUpdater {
ntree.tree_interface.reset_changed_flags();
}
/**
* Update the panel toggle sockets to use the same name as the panel.
*/
bool update_panel_toggle_names(bNodeTree &ntree)
{
bool changed = false;
ntree.ensure_interface_cache();
for (bNodeTreeInterfaceItem *item : ntree.interface_items()) {
if (item->item_type != NODE_INTERFACE_PANEL) {
continue;
}
bNodeTreeInterfacePanel *panel = reinterpret_cast<bNodeTreeInterfacePanel *>(item);
if (bNodeTreeInterfaceSocket *toggle_socket = panel->header_toggle_socket()) {
if (!STREQ(panel->name, toggle_socket->name)) {
MEM_SAFE_FREE(toggle_socket->name);
toggle_socket->name = BLI_strdup_null(panel->name);
changed = true;
}
}
}
return changed;
}
};
} // namespace blender::bke

View File

@@ -2409,6 +2409,13 @@ PanelLayout uiLayoutPanelProp(const bContext *C,
uiLayout *layout,
PointerRNA *open_prop_owner,
const char *open_prop_name);
PanelLayout uiLayoutPanelPropWithBoolHeader(const bContext *C,
uiLayout *layout,
PointerRNA *open_prop_owner,
const blender::StringRefNull open_prop_name,
PointerRNA *bool_prop_owner,
const blender::StringRefNull bool_prop_name,
const std::optional<blender::StringRefNull> label);
/**
* Variant of #uiLayoutPanelProp that automatically creates the header row with the

View File

@@ -5008,6 +5008,23 @@ PanelLayout uiLayoutPanelProp(const bContext *C,
return panel_layout;
}
PanelLayout uiLayoutPanelPropWithBoolHeader(const bContext *C,
uiLayout *layout,
PointerRNA *open_prop_owner,
const StringRefNull open_prop_name,
PointerRNA *bool_prop_owner,
const StringRefNull bool_prop_name,
const std::optional<StringRefNull> label)
{
PanelLayout panel = uiLayoutPanelProp(C, layout, open_prop_owner, open_prop_name.c_str());
uiLayout *panel_header = panel.header;
panel_header->flag &= ~(UI_ITEM_PROP_SEP | UI_ITEM_PROP_DECORATE | UI_ITEM_INSIDE_PROP_SEP);
uiItemR(panel_header, bool_prop_owner, bool_prop_name, UI_ITEM_NONE, label, ICON_NONE);
return panel;
}
uiLayout *uiLayoutPanelProp(const bContext *C,
uiLayout *layout,
PointerRNA *open_prop_owner,

View File

@@ -173,6 +173,7 @@ class NodePanelViewItem : public BasicTreeViewItem {
private:
bNodeTree &nodetree_;
bNodeTreeInterfacePanel &panel_;
const bNodeTreeInterfaceSocket *toggle_ = nullptr;
public:
NodePanelViewItem(bNodeTree &nodetree,
@@ -185,11 +186,24 @@ class NodePanelViewItem : public BasicTreeViewItem {
NodePanelViewItem &self = static_cast<NodePanelViewItem &>(new_active);
interface.active_item_set(&self.panel_.item);
});
toggle_ = panel.header_toggle_socket();
is_always_collapsible_ = true;
}
void build_row(uiLayout &row) override
{
uiLayout *toggle_layout = uiLayoutRow(&row, true);
/* Add boolean socket if panel has a toggle. */
if (toggle_ != nullptr) {
/* XXX Socket template only draws in embossed layouts (Julian). */
uiLayoutSetEmboss(toggle_layout, UI_EMBOSS);
/* Context is not used by the template function. */
uiTemplateNodeSocket(toggle_layout, /*C*/ nullptr, toggle_->socket_color());
}
else {
uiItemL(toggle_layout, "", ICON_BLANK1);
}
this->add_label(row);
uiLayout *sub = uiLayoutRow(&row, true);
@@ -213,11 +227,11 @@ class NodePanelViewItem : public BasicTreeViewItem {
}
bool rename(const bContext &C, StringRefNull new_name) override
{
MEM_SAFE_FREE(panel_.name);
panel_.name = BLI_strdup(new_name.c_str());
nodetree_.tree_interface.tag_items_changed();
BKE_main_ensure_invariants(*CTX_data_main(&C), nodetree_.id);
PointerRNA panel_ptr = RNA_pointer_create_discrete(
&nodetree_.id, &RNA_NodeTreeInterfacePanel, &panel_);
PropertyRNA *name_prop = RNA_struct_find_property(&panel_ptr, "name");
RNA_property_string_set(&panel_ptr, name_prop, new_name.c_str());
RNA_property_update(const_cast<bContext *>(&C), &panel_ptr, name_prop);
return true;
}
StringRef get_rename_string() const override
@@ -258,9 +272,13 @@ class NodeTreeInterfaceView : public AbstractTreeView {
protected:
void add_items_for_panel_recursive(bNodeTreeInterfacePanel &parent,
ui::TreeViewOrItem &parent_item)
ui::TreeViewOrItem &parent_item,
const bNodeTreeInterfaceItem *skip_item = nullptr)
{
for (bNodeTreeInterfaceItem *item : parent.items()) {
if (item == skip_item) {
continue;
}
switch (item->item_type) {
case NODE_INTERFACE_SOCKET: {
bNodeTreeInterfaceSocket *socket = node_interface::get_item_as<bNodeTreeInterfaceSocket>(
@@ -276,7 +294,10 @@ class NodeTreeInterfaceView : public AbstractTreeView {
NodePanelViewItem &panel_item = parent_item.add_tree_item<NodePanelViewItem>(
nodetree_, interface_, *panel);
panel_item.uncollapse_by_default();
add_items_for_panel_recursive(*panel, panel_item);
/* Skip over sockets which are a panel toggle. */
const bNodeTreeInterfaceSocket *skip_item = panel->header_toggle_socket();
add_items_for_panel_recursive(
*panel, panel_item, reinterpret_cast<const bNodeTreeInterfaceItem *>(skip_item));
break;
}
}
@@ -460,7 +481,8 @@ bool NodePanelDropTarget::on_drop(bContext *C, const DragInfo &drag_info) const
case DropLocation::Into: {
/* Insert into target */
parent = &panel_;
index = 0;
const bool has_toggle_socket = panel_.header_toggle_socket() != nullptr;
index = has_toggle_socket ? 1 : 0;
break;
}
case DropLocation::Before: {

View File

@@ -586,6 +586,8 @@ struct Separator {
struct PanelHeader {
static constexpr Type type = Type::PanelHeader;
const nodes::PanelDeclaration *decl;
/** Optional input that is drawn in the header. */
bNodeSocket *input = nullptr;
};
struct PanelContentBegin {
static constexpr Type type = Type::PanelContentBegin;
@@ -747,7 +749,14 @@ static void add_flat_items_for_panel(bNode &node,
if (!panel_visibility[panel_decl.index]) {
return;
}
r_items.append({flat_item::PanelHeader{&panel_decl}});
flat_item::PanelHeader header_item;
header_item.decl = &panel_decl;
const nodes::SocketDeclaration *panel_input_decl = panel_decl.panel_input_decl();
if (panel_input_decl) {
header_item.input = &node.socket_by_decl(*panel_input_decl);
}
r_items.append({header_item});
const bNodePanelState &panel_state = node.panel_states_array[panel_decl.index];
if (panel_state.is_collapsed()) {
return;
@@ -755,6 +764,9 @@ static void add_flat_items_for_panel(bNode &node,
r_items.append({flat_item::PanelContentBegin{&panel_decl}});
const nodes::SocketDeclaration *prev_socket_decl = nullptr;
for (const nodes::ItemDeclaration *item_decl : panel_decl.items) {
if (item_decl == panel_input_decl) {
continue;
}
if (const auto *socket_decl = dynamic_cast<const nodes::SocketDeclaration *>(item_decl)) {
add_flat_items_for_socket(node, *socket_decl, &panel_decl, prev_socket_decl, r_items);
prev_socket_decl = socket_decl;
@@ -1071,6 +1083,7 @@ static void node_update_basis_from_declaration(
for (bke::bNodePanelRuntime &panel_runtime : node.runtime->panels) {
panel_runtime.header_center_y.reset();
panel_runtime.content_extent.reset();
panel_runtime.input_socket = nullptr;
}
for (bNodeSocket *socket : node.input_sockets()) {
socket->flag &= ~SOCK_PANEL_COLLAPSED;
@@ -1155,6 +1168,11 @@ static void node_update_basis_from_declaration(
locy -= panel_header_height / 2;
panel_runtime.header_center_y = locy;
locy -= panel_header_height / 2;
bNodeSocket *input_socket = item.input;
if (input_socket) {
panel_runtime.input_socket = input_socket;
input_socket->runtime->location = float2(locx, *panel_runtime.header_center_y);
}
}
else if constexpr (std::is_same_v<ItemT, flat_item::PanelContentBegin>) {
const nodes::PanelDeclaration &node_decl = *item.decl;
@@ -2487,6 +2505,7 @@ static void node_draw_panels(bNodeTree &ntree, const bNode &node, uiBlock &block
for (const int panel_i : node_decl.panels.index_range()) {
const nodes::PanelDeclaration &panel_decl = *node_decl.panels[panel_i];
const bke::bNodePanelRuntime &panel_runtime = node.runtime->panels[panel_i];
bNodeSocket *input_socket = panel_runtime.input_socket;
const bNodePanelState &panel_state = node.panel_states_array[panel_i];
if (!panel_runtime.header_center_y.has_value()) {
continue;
@@ -2498,41 +2517,6 @@ static void node_draw_panels(bNodeTree &ntree, const bNode &node, uiBlock &block
*panel_runtime.header_center_y + NODE_DYS};
UI_block_emboss_set(&block, UI_EMBOSS_NONE);
/* Collapse/expand icon. */
const int but_size = U.widget_unit * 0.8f;
uiDefIconBut(&block,
UI_BTYPE_BUT_TOGGLE,
0,
panel_state.is_collapsed() ? ICON_RIGHTARROW : ICON_DOWNARROW_HLT,
draw_bounds.xmin + (NODE_MARGIN_X / 3),
*panel_runtime.header_center_y - but_size / 2,
but_size,
but_size,
nullptr,
0.0f,
0.0f,
"");
/* Panel label. */
uiBut *label_but = uiDefBut(
&block,
UI_BTYPE_LABEL,
0,
IFACE_(panel_decl.name),
int(draw_bounds.xmin + NODE_MARGIN_X + 0.4f),
int(*panel_runtime.header_center_y - NODE_DYS),
short(draw_bounds.xmax - draw_bounds.xmin - (30.0f * UI_SCALE_FAC)),
NODE_DY,
nullptr,
0,
0,
"");
const bool only_inactive_inputs = panel_has_only_inactive_inputs(node, panel_decl);
if (node.is_muted() || only_inactive_inputs) {
UI_but_flag_enable(label_but, UI_BUT_INACTIVE);
}
/* Invisible button covering the entire header for collapsing/expanding. */
const int header_but_margin = NODE_MARGIN_X / 3;
uiBut *toggle_action_but = uiDefIconBut(
@@ -2555,7 +2539,66 @@ static void node_draw_panels(bNodeTree &ntree, const bNode &node, uiBlock &block
const_cast<bNodePanelState *>(&panel_state),
&ntree);
/* Collapse/expand icon. */
const int but_size = U.widget_unit * 0.8f;
const int but_padding = NODE_MARGIN_X / 4;
int offsetx = draw_bounds.xmin + (NODE_MARGIN_X / 3);
uiDefIconBut(&block,
UI_BTYPE_LABEL,
0,
panel_state.is_collapsed() ? ICON_RIGHTARROW : ICON_DOWNARROW_HLT,
offsetx,
*panel_runtime.header_center_y - but_size / 2,
but_size,
but_size,
nullptr,
0.0f,
0.0f,
"");
offsetx += but_size + but_padding;
UI_block_emboss_set(&block, UI_EMBOSS);
/* Panel toggle. */
if (input_socket && !input_socket->is_logically_linked()) {
PointerRNA socket_ptr = RNA_pointer_create_discrete(
&ntree.id, &RNA_NodeSocket, input_socket);
uiDefButR(&block,
UI_BTYPE_CHECKBOX,
-1,
"",
offsetx,
int(*panel_runtime.header_center_y - NODE_DYS),
UI_UNIT_X,
NODE_DY,
&socket_ptr,
"default_value",
0,
0,
0,
"");
offsetx += UI_UNIT_X;
}
/* Panel label. */
uiBut *label_but = uiDefBut(
&block,
UI_BTYPE_LABEL,
0,
IFACE_(panel_decl.name),
offsetx,
int(*panel_runtime.header_center_y - NODE_DYS),
short(draw_bounds.xmax - draw_bounds.xmin - (30.0f * UI_SCALE_FAC)),
NODE_DY,
nullptr,
0,
0,
"");
const bool only_inactive_inputs = panel_has_only_inactive_inputs(node, panel_decl);
if (node.is_muted() || only_inactive_inputs) {
UI_but_flag_enable(label_but, UI_BUT_INACTIVE);
}
}
}

View File

@@ -65,8 +65,10 @@ typedef enum NodeTreeInterfaceSocketFlag {
NODE_INTERFACE_SOCKET_LAYER_SELECTION = 1 << 6,
/* INSPECT is used by Connect to Output operator to ensure socket that exits from node group. */
NODE_INTERFACE_SOCKET_INSPECT = 1 << 7,
/* Socket is used in the panel header as a toggle. */
NODE_INTERFACE_SOCKET_PANEL_TOGGLE = 1 << 8,
} NodeTreeInterfaceSocketFlag;
ENUM_OPERATORS(NodeTreeInterfaceSocketFlag, NODE_INTERFACE_SOCKET_INSPECT);
ENUM_OPERATORS(NodeTreeInterfaceSocketFlag, NODE_INTERFACE_SOCKET_PANEL_TOGGLE);
typedef struct bNodeTreeInterfaceSocket {
bNodeTreeInterfaceItem item;
@@ -222,6 +224,10 @@ typedef struct bNodeTreeInterfacePanel {
void foreach_item(blender::FunctionRef<bool(const bNodeTreeInterfaceItem &item)> fn,
bool include_self = false) const;
/** Get the socket that is part of the panel header if available. */
const bNodeTreeInterfaceSocket *header_toggle_socket() const;
bNodeTreeInterfaceSocket *header_toggle_socket();
private:
/** Find a valid position for inserting in the items span. */
int find_valid_insert_position_for_item(const bNodeTreeInterfaceItem &item,

View File

@@ -1034,6 +1034,14 @@ static void rna_def_node_interface_socket(BlenderRNA *brna)
"Take link out of node group to connect to root tree output node");
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_NodeTreeInterfaceItem_update");
prop = RNA_def_property(srna, "is_panel_toggle", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "flag", NODE_INTERFACE_SOCKET_PANEL_TOGGLE);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_ui_text(prop,
"Is Panel Toggle",
"This socket is meant to be used as the toggle in its panel header");
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_NodeTreeInterfaceItem_update");
prop = RNA_def_property(srna, "layer_selection_field", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "flag", NODE_INTERFACE_SOCKET_LAYER_SELECTION);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);

View File

@@ -2275,9 +2275,11 @@ static bool interface_panel_affects_output(DrawGroupInputsContext &ctx,
static void draw_interface_panel_content(DrawGroupInputsContext &ctx,
uiLayout *layout,
const bNodeTreeInterfacePanel &interface_panel)
const bNodeTreeInterfacePanel &interface_panel,
const bool skip_first = false)
{
for (const bNodeTreeInterfaceItem *item : interface_panel.items()) {
for (const bNodeTreeInterfaceItem *item : interface_panel.items().drop_front(skip_first ? 1 : 0))
{
if (item->item_type == NODE_INTERFACE_PANEL) {
const auto &sub_interface_panel = *reinterpret_cast<const bNodeTreeInterfacePanel *>(item);
if (!interface_panel_has_socket(sub_interface_panel)) {
@@ -2286,8 +2288,39 @@ static void draw_interface_panel_content(DrawGroupInputsContext &ctx,
NodesModifierPanel *panel = find_panel_by_id(ctx.nmd, sub_interface_panel.identifier);
PointerRNA panel_ptr = RNA_pointer_create_discrete(
ctx.md_ptr->owner_id, &RNA_NodesModifierPanel, panel);
PanelLayout panel_layout = uiLayoutPanelProp(&ctx.C, layout, &panel_ptr, "is_open");
uiItemL(panel_layout.header, IFACE_(sub_interface_panel.name), ICON_NONE);
PanelLayout panel_layout;
bool skip_first = false;
/* Check if the panel should have a toggle in the header. */
const bNodeTreeInterfaceSocket *toggle_socket = sub_interface_panel.header_toggle_socket();
if (toggle_socket && !(toggle_socket->flag & NODE_INTERFACE_SOCKET_HIDE_IN_MODIFIER)) {
const StringRefNull identifier = toggle_socket->identifier;
IDProperty *property = IDP_GetPropertyFromGroup(ctx.nmd.settings.properties, identifier);
/* IDProperties can be removed with python, so there could be a situation where
* there isn't a property for a socket or it doesn't have the correct type. */
if (property == nullptr ||
!nodes::id_property_type_matches_socket(*toggle_socket, *property))
{
continue;
}
char socket_id_esc[MAX_NAME * 2];
BLI_str_escape(socket_id_esc, identifier.c_str(), sizeof(socket_id_esc));
char rna_path[sizeof(socket_id_esc) + 4];
SNPRINTF(rna_path, "[\"%s\"]", socket_id_esc);
panel_layout = uiLayoutPanelPropWithBoolHeader(&ctx.C,
layout,
&panel_ptr,
"is_open",
ctx.md_ptr,
rna_path,
IFACE_(sub_interface_panel.name));
skip_first = true;
}
else {
panel_layout = uiLayoutPanelProp(&ctx.C, layout, &panel_ptr, "is_open");
uiItemL(panel_layout.header, IFACE_(sub_interface_panel.name), ICON_NONE);
}
if (!interface_panel_affects_output(ctx, sub_interface_panel)) {
uiLayoutSetActive(panel_layout.header, false);
}
@@ -2301,7 +2334,7 @@ static void draw_interface_panel_content(DrawGroupInputsContext &ctx,
nullptr,
nullptr);
if (panel_layout.body) {
draw_interface_panel_content(ctx, panel_layout.body, sub_interface_panel);
draw_interface_panel_content(ctx, panel_layout.body, sub_interface_panel, skip_first);
}
}
else {

View File

@@ -189,6 +189,8 @@ class SocketDeclaration : public ItemDeclaration {
bool is_default_link_socket = false;
/** Puts this socket on the same line as the previous one in the UI. */
bool align_with_previous_socket = false;
/** This socket is used as a toggle for the parent panel. */
bool is_panel_toggle = false;
/** Index in the list of inputs or outputs of the node. */
int index = -1;
@@ -388,6 +390,10 @@ class BaseSocketDeclarationBuilder {
const StructRNA *srna,
const void *data,
StringRef property_name);
/**
* Use the socket as a toggle in its panel.
*/
BaseSocketDeclarationBuilder &panel_toggle(bool value = true);
/** Index in the list of inputs or outputs. */
int index() const;
@@ -455,6 +461,9 @@ class PanelDeclaration : public ItemDeclaration {
void update_or_build(const bNodePanelState &old_panel, bNodePanelState &new_panel) const;
int depth() const;
/** Get the declaration for a child item that should be drawn as part of the panel header. */
const SocketDeclaration *panel_input_decl() const;
};
/**

View File

@@ -372,6 +372,7 @@ static BaseSocketDeclarationBuilder &build_interface_socket_declaration(
decl->description(io_socket.description ? io_socket.description : "");
decl->hide_value(io_socket.flag & NODE_INTERFACE_SOCKET_HIDE_VALUE);
decl->compact(io_socket.flag & NODE_INTERFACE_SOCKET_COMPACT);
decl->panel_toggle(io_socket.flag & NODE_INTERFACE_SOCKET_PANEL_TOGGLE);
return *decl;
}

View File

@@ -495,6 +495,22 @@ int PanelDeclaration::depth() const
return count;
}
const nodes::SocketDeclaration *PanelDeclaration::panel_input_decl() const
{
if (this->items.is_empty()) {
return nullptr;
}
const nodes::ItemDeclaration *item_decl = this->items.first();
if (const auto *socket_decl = dynamic_cast<const nodes::SocketDeclaration *>(item_decl)) {
if (socket_decl->is_panel_toggle && (socket_decl->in_out & SOCK_IN) &&
(socket_decl->socket_type & SOCK_BOOLEAN))
{
return socket_decl;
}
}
return nullptr;
}
BaseSocketDeclarationBuilder &BaseSocketDeclarationBuilder::supports_field()
{
BLI_assert(this->is_input());
@@ -743,6 +759,12 @@ BaseSocketDeclarationBuilder &BaseSocketDeclarationBuilder::socket_name_ptr(
property_name);
}
BaseSocketDeclarationBuilder &BaseSocketDeclarationBuilder::panel_toggle(const bool value)
{
decl_base_->is_panel_toggle = value;
return *this;
}
OutputFieldDependency OutputFieldDependency::ForFieldSource()
{
OutputFieldDependency field_dependency;