# SPDX-FileCopyrightText: 2022-2023 Blender Authors # # SPDX-License-Identifier: GPL-2.0-or-later __all__ = ( "add_closure_zone", "add_color_mix_node", "add_foreach_geometry_element_zone", "add_node_type", "add_node_type_with_outputs", "add_node_type_with_searchable_enum", "add_node_type_with_searchable_enum_socket", "add_repeat_zone", "add_simulation_zone", "draw_node_group_add_menu", ) import bpy from bpy.types import Menu from bpy.app.translations import ( pgettext_iface as iface_, contexts as i18n_contexts, ) # NOTE: This is kept for compatibility's sake, as some scripts import node_add_menu.add_node_type. def add_node_type(layout, node_type, *, label=None, poll=None, search_weight=0.0, translate=True): """Add a node type to a menu.""" return AddNodeMenu.node_operator( layout, node_type, label=label, poll=poll, search_weight=search_weight, translate=translate, ) def add_node_type_with_searchable_enum(context, layout, node_idname, property_name, search_weight=0.0): return AddNodeMenu.node_operator_with_searchable_enum(context, layout, node_idname, property_name, search_weight) def add_node_type_with_searchable_enum_socket( context, layout, node_idname, socket_identifier, enum_names, search_weight=0.0, ): return AddNodeMenu.node_operator_with_searchable_enum_socket( context, layout, node_idname, socket_identifier, enum_names, search_weight, ) def add_node_type_with_outputs(context, layout, node_type, subnames, *, label=None, search_weight=0.0): return AddNodeMenu.node_operator_with_outputs( context, layout, node_type, subnames, label=label, search_weight=search_weight, ) def add_color_mix_node(context, layout): return AddNodeMenu.color_mix_node(context, layout) def add_empty_group(layout): return AddNodeMenu.new_empty_group(layout) def draw_node_group_add_menu(context, layout): """Add items to the layout used for interacting with node groups.""" return AddNodeMenu.draw_group_menu(context, layout) def add_simulation_zone(layout, label): """Add simulation zone to a menu.""" props = layout.operator("node.add_simulation_zone", text=label, text_ctxt=i18n_contexts.default) props.use_transform = True return props def add_repeat_zone(layout, label): props = layout.operator("node.add_repeat_zone", text=label, text_ctxt=i18n_contexts.default) props.use_transform = True return props def add_foreach_geometry_element_zone(layout, label): props = layout.operator( "node.add_foreach_geometry_element_zone", text=label, text_ctxt=i18n_contexts.default, ) props.use_transform = True return props def add_closure_zone(layout, label): props = layout.operator( "node.add_closure_zone", text=label, text_ctxt=i18n_contexts.default, ) props.use_transform = True return props class NodeMenu(Menu): """A baseclass defining the shared methods for AddNodeMenu and SwapNodeMenu.""" draw_assets: bool use_transform: bool main_operator_id: str zone_operator_id: str new_empty_group_operator_id: str root_asset_menu: str pathing_dict: dict[str, str] @classmethod def poll(cls, context): return context.space_data.type == 'NODE_EDITOR' @classmethod def node_operator(cls, layout, node_type, *, label=None, poll=None, search_weight=0.0, translate=True): """The main operator defined for the node menu. \n(e.g. 'Add Node' for AddNodeMenu, or 'Swap Node' for SwapNodeMenu).""" bl_rna = bpy.types.Node.bl_rna_get_subclass(node_type) if not label: label = bl_rna.name if bl_rna else iface_("Unknown") if poll is True or poll is None: translation_context = bl_rna.translation_context if bl_rna else i18n_contexts.default props = layout.operator( cls.main_operator_id, text=label, text_ctxt=translation_context, translate=translate, search_weight=search_weight, ) props.type = node_type if hasattr(props, "use_transform"): props.use_transform = cls.use_transform return props return None @classmethod def node_operator_with_searchable_enum(cls, context, layout, node_idname, property_name, search_weight=0.0): """Similar to `node_operator`, but with extra entries based on a enum property while in search.""" operators = [] operators.append(cls.node_operator(layout, node_idname, search_weight=search_weight)) if getattr(context, "is_menu_search", False): node_type = getattr(bpy.types, node_idname) translation_context = node_type.bl_rna.properties[property_name].translation_context for item in node_type.bl_rna.properties[property_name].enum_items_static: props = cls.node_operator( layout, node_idname, label="{:s} \u25B8 {:s}".format( iface_(node_type.bl_rna.name), iface_(item.name, translation_context), ), translate=False, search_weight=search_weight, ) prop = props.settings.add() prop.name = property_name prop.value = repr(item.identifier) operators.append(props) for props in operators: if hasattr(props, "use_transform"): props.use_transform = cls.use_transform return operators @classmethod def node_operator_with_searchable_enum_socket( cls, context, layout, node_idname, socket_identifier, enum_names, search_weight=0.0, ): """Similar to `node_operator`, but with extra entries based on a enum socket while in search.""" operators = [] operators.append(cls.node_operator(layout, node_idname, search_weight=search_weight)) if getattr(context, "is_menu_search", False): node_type = getattr(bpy.types, node_idname) for enum_name in enum_names: props = cls.node_operator( layout, node_idname, label="{:s} \u25B8 {:s}".format(iface_(node_type.bl_rna.name), iface_(enum_name)), translate=False, search_weight=search_weight, ) prop = props.settings.add() prop.name = "inputs[\"{:s}\"].default_value".format(bpy.utils.escape_identifier(socket_identifier)) prop.value = repr(enum_name) operators.append(props) for props in operators: if hasattr(props, "use_transform"): props.use_transform = cls.use_transform return operators @classmethod def node_operator_with_outputs(cls, context, layout, node_type, subnames, *, label=None, search_weight=0.0): """Similar to `node_operator`, but with extra entries based on a enum socket while in search.""" bl_rna = bpy.types.Node.bl_rna_get_subclass(node_type) if not label: label = bl_rna.name if bl_rna else "Unknown" operators = [] operators.append(cls.node_operator(layout, node_type, label=label, search_weight=search_weight)) if getattr(context, "is_menu_search", False): for subname in subnames: item_props = cls.node_operator(layout, node_type, label="{:s} \u25B8 {:s}".format( iface_(label), iface_(subname)), search_weight=search_weight, translate=False) item_props.visible_output = subname operators.append(item_props) for props in operators: if hasattr(props, "use_transform"): props.use_transform = cls.use_transform return operators @classmethod def color_mix_node(cls, context, layout): """The 'Mix Color' node, with its different blend modes available while in search.""" label = iface_("Mix Color") operators = [] props = cls.node_operator(layout, "ShaderNodeMix", label=label, translate=False) ops = props.settings.add() ops.name = "data_type" ops.value = "'RGBA'" operators.append(props) if getattr(context, "is_menu_search", False): translation_context = bpy.types.ShaderNodeMix.bl_rna.properties["blend_type"].translation_context for item in bpy.types.ShaderNodeMix.bl_rna.properties["blend_type"].enum_items_static: props = cls.node_operator( layout, "ShaderNodeMix", label="{:s} \u25B8 {:s}".format( label, iface_(item.name, translation_context), ), translate=False, ) prop = props.settings.add() prop.name = "data_type" prop.value = "'RGBA'" prop = props.settings.add() prop.name = "blend_type" prop.value = repr(item.identifier) operators.append(props) for props in operators: if hasattr(props, "use_transform"): props.use_transform = cls.use_transform return operators @classmethod def new_empty_group(cls, layout): """Group Node with a newly created empty group as its assigned nodetree.""" props = layout.operator( cls.new_empty_group_operator_id, text="New Group", text_ctxt=i18n_contexts.default, icon='ADD', ) if hasattr(props, "use_transform"): props.use_transform = cls.use_transform return props @classmethod def draw_group_menu(cls, context, layout): """Show operators used for interacting with node groups.""" space_node = context.space_data node_tree = space_node.edit_tree all_node_groups = context.blend_data.node_groups operators = [] operators.append(cls.new_empty_group(layout)) if node_tree in all_node_groups.values(): layout.separator() cls.node_operator(layout, "NodeGroupInput") cls.node_operator(layout, "NodeGroupOutput") if node_tree: from nodeitems_builtins import node_tree_group_type prefs = bpy.context.preferences show_hidden = prefs.filepaths.show_hidden_files_datablocks groups = [ group for group in context.blend_data.node_groups if (group.bl_idname == node_tree.bl_idname and not group.contains_tree(node_tree) and (show_hidden or not group.name.startswith('.'))) ] if groups: layout.separator() for group in groups: search_weight = -1.0 if group.is_linked_packed else 0.0 props = cls.node_operator( layout, node_tree_group_type[group.bl_idname], label=group.name, search_weight=search_weight, ) ops = props.settings.add() ops.name = "node_tree" ops.value = "bpy.data.node_groups[{!r}]".format(group.name) ops = props.settings.add() ops.name = "width" ops.value = repr(group.default_group_node_width) ops = props.settings.add() ops.name = "name" ops.value = repr(group.name) operators.append(props) for props in operators: if hasattr(props, "use_transform"): props.use_transform = cls.use_transform return operators @classmethod def draw_menu(cls, layout, path): """ Takes the given menu path and draws the corresponding menu. Menu paths are either explicitly defined, or based on bl_label if not. """ if cls.pathing_dict is None: raise ValueError("`pathing_dict` was not set for {!s}".format(cls)) layout.menu(cls.pathing_dict[path]) @classmethod def simulation_zone(cls, layout, label): props = layout.operator(cls.zone_operator_id, text=iface_(label), translate=False) props.input_node_type = "GeometryNodeSimulationInput" props.output_node_type = "GeometryNodeSimulationOutput" props.add_default_geometry_link = True if hasattr(props, "use_transform"): props.use_transform = cls.use_transform return props @classmethod def repeat_zone(cls, layout, label): props = layout.operator(cls.zone_operator_id, text=iface_(label), translate=False) props.input_node_type = "GeometryNodeRepeatInput" props.output_node_type = "GeometryNodeRepeatOutput" props.add_default_geometry_link = True if hasattr(props, "use_transform"): props.use_transform = cls.use_transform return props @classmethod def for_each_element_zone(cls, layout, label): props = layout.operator(cls.zone_operator_id, text=iface_(label), translate=False) props.input_node_type = "GeometryNodeForeachGeometryElementInput" props.output_node_type = "GeometryNodeForeachGeometryElementOutput" props.add_default_geometry_link = False if hasattr(props, "use_transform"): props.use_transform = cls.use_transform return props @classmethod def closure_zone(cls, layout, label): props = layout.operator(cls.zone_operator_id, text=iface_(label), translate=False) props.input_node_type = "NodeClosureInput" props.output_node_type = "NodeClosureOutput" props.add_default_geometry_link = False if hasattr(props, "use_transform"): props.use_transform = cls.use_transform return props @classmethod def draw_root_assets(cls, layout): if cls.draw_assets: layout.menu_contents(cls.root_asset_menu) class AddNodeMenu(NodeMenu): draw_assets = True use_transform = True main_operator_id = "node.add_node" zone_operator_id = "node.add_zone" new_empty_group_operator_id = "node.add_empty_group" root_asset_menu = "NODE_MT_node_add_root_catalogs" @classmethod def draw_assets_for_catalog(cls, layout, catalog_path): if cls.draw_assets: layout.template_node_asset_menu_items(catalog_path=catalog_path, operator='ADD') class SwapNodeMenu(NodeMenu): draw_assets = True # NOTE: Swap operators don't have a `use_transform` property, so defining it here has no effect. main_operator_id = "node.swap_node" zone_operator_id = "node.swap_zone" new_empty_group_operator_id = "node.swap_empty_group" root_asset_menu = "NODE_MT_node_swap_root_catalogs" @classmethod def draw_assets_for_catalog(cls, layout, catalog_path): if cls.draw_assets: layout.template_node_asset_menu_items(catalog_path=catalog_path, operator='SWAP') class NODE_MT_group_base(NodeMenu): bl_label = "Group" def draw(self, context): layout = self.layout self.draw_group_menu(context, layout) self.draw_assets_for_catalog(layout, self.bl_label) class NODE_MT_layout_base(NodeMenu): bl_label = "Layout" def draw(self, _context): layout = self.layout self.node_operator(layout, "NodeFrame", search_weight=-1) self.node_operator(layout, "NodeReroute") self.draw_assets_for_catalog(layout, self.bl_label) add_base_pathing_dict = { "Group": "NODE_MT_group_add", "Layout": "NODE_MT_category_layout", } swap_base_pathing_dict = { "Group": "NODE_MT_group_swap", "Layout": "NODE_MT_layout_swap", } def generate_menu(bl_idname: str, template: Menu, layout_base: Menu, pathing_dict: dict = None): return type(bl_idname, (template, layout_base), {"bl_idname": bl_idname, "pathing_dict": pathing_dict}) def generate_menus(menus: dict, template: Menu, base_dict: dict): import copy pathing_dict = copy.copy(base_dict) menus = tuple( generate_menu(bl_idname, template, layout_base, pathing_dict) for bl_idname, layout_base in menus.items() ) generate_pathing_dict(pathing_dict, menus) return menus def generate_pathing_dict(pathing_dict, menus): for menu in menus: if hasattr(menu, "menu_path"): menu_path = menu.menu_path else: menu_path = menu.bl_label pathing_dict[menu_path] = menu.bl_idname classes = ( generate_menu("NODE_MT_group_add", template=AddNodeMenu, layout_base=NODE_MT_group_base), generate_menu("NODE_MT_group_swap", template=SwapNodeMenu, layout_base=NODE_MT_group_base), generate_menu("NODE_MT_category_layout", template=AddNodeMenu, layout_base=NODE_MT_layout_base), generate_menu("NODE_MT_layout_swap", template=SwapNodeMenu, layout_base=NODE_MT_layout_base), ) if __name__ == "__main__": # only for live edit. from bpy.utils import register_class for cls in classes: register_class(cls)