From f8cac4acf95ae91f26edb1ce32dbd36558964710 Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Mon, 29 Jan 2024 22:04:52 +0100 Subject: [PATCH] Node Wrangler: Add more specific poll methods Since it is useful to set a poll message to tell the user why an operator cannot be called, decompose poll conditions into basic functions and make every operator use them. - Make the basic nw_check report issues. - Add functions to check that: - the node tree is not empty, - a node is active, - a specified number of nodes are selected, - the active node is of a specific type, - it has visible outputs, - there is a viewer image. - These functions report the issue to the poll message otherwise. - Go through operators and add or update poll methods using those various functions. - In a few operators, remove obsolete error reports that are now caught at the poll stage. Fixes a few issues: - PreviewNode would not work immediately when the active node was a material or world output. - ReloadImages needed an active node. --- node_wrangler/__init__.py | 2 +- node_wrangler/interface.py | 77 +++++++++++-------- node_wrangler/operators.py | 177 ++++++++++++++++++++++++++----------------- node_wrangler/utils/nodes.py | 89 +++++++++++++++++++--- 4 files changed, 231 insertions(+), 114 deletions(-) diff --git a/node_wrangler/__init__.py b/node_wrangler/__init__.py index 13a3178b..b1baa24c 100644 --- a/node_wrangler/__init__.py +++ b/node_wrangler/__init__.py @@ -5,7 +5,7 @@ bl_info = { "name": "Node Wrangler", "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer", - "version": (3, 51), + "version": (3, 52), "blender": (4, 0, 0), "location": "Node Editor Toolbar or Shift-W", "description": "Various tools to enhance and speed up node-based workflow", diff --git a/node_wrangler/interface.py b/node_wrangler/interface.py index e28e947a..d0131d28 100644 --- a/node_wrangler/interface.py +++ b/node_wrangler/interface.py @@ -10,7 +10,7 @@ from nodeitems_utils import node_categories_iter, NodeItemCustom from . import operators from .utils.constants import blend_types, geo_combine_operations, operations -from .utils.nodes import get_nodes_links, nw_check, NWBase +from .utils.nodes import get_nodes_links, NWBaseMenu def drawlayout(context, layout, mode='non-panel'): @@ -71,7 +71,7 @@ def drawlayout(context, layout, mode='non-panel'): col.separator() -class NodeWranglerPanel(Panel, NWBase): +class NodeWranglerPanel(Panel, NWBaseMenu): bl_idname = "NODE_PT_nw_node_wrangler" bl_space_type = 'NODE_EDITOR' bl_label = "Node Wrangler" @@ -92,7 +92,7 @@ class NodeWranglerPanel(Panel, NWBase): # # M E N U S # -class NodeWranglerMenu(Menu, NWBase): +class NodeWranglerMenu(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_node_wrangler_menu" bl_label = "Node Wrangler" @@ -101,7 +101,7 @@ class NodeWranglerMenu(Menu, NWBase): drawlayout(context, self.layout) -class NWMergeNodesMenu(Menu, NWBase): +class NWMergeNodesMenu(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_merge_nodes_menu" bl_label = "Merge Selected Nodes" @@ -124,7 +124,7 @@ class NWMergeNodesMenu(Menu, NWBase): props.merge_type = 'ALPHAOVER' -class NWMergeGeometryMenu(Menu, NWBase): +class NWMergeGeometryMenu(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_merge_geometry_menu" bl_label = "Merge Selected Nodes using Geometry Nodes" @@ -137,7 +137,7 @@ class NWMergeGeometryMenu(Menu, NWBase): props.merge_type = 'GEOMETRY' -class NWMergeShadersMenu(Menu, NWBase): +class NWMergeShadersMenu(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_merge_shaders_menu" bl_label = "Merge Selected Nodes using Shaders" @@ -150,7 +150,7 @@ class NWMergeShadersMenu(Menu, NWBase): props.merge_type = 'SHADER' -class NWMergeMixMenu(Menu, NWBase): +class NWMergeMixMenu(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_merge_mix_menu" bl_label = "Merge Selected Nodes using Mix" @@ -162,7 +162,7 @@ class NWMergeMixMenu(Menu, NWBase): props.merge_type = 'MIX' -class NWConnectionListOutputs(Menu, NWBase): +class NWConnectionListOutputs(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_connection_list_out" bl_label = "From:" @@ -180,7 +180,7 @@ class NWConnectionListOutputs(Menu, NWBase): icon="RADIOBUT_OFF").from_socket = index -class NWConnectionListInputs(Menu, NWBase): +class NWConnectionListInputs(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_connection_list_in" bl_label = "To:" @@ -201,7 +201,7 @@ class NWConnectionListInputs(Menu, NWBase): op.to_socket = index -class NWMergeMathMenu(Menu, NWBase): +class NWMergeMathMenu(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_merge_math_menu" bl_label = "Merge Selected Nodes using Math" @@ -213,7 +213,7 @@ class NWMergeMathMenu(Menu, NWBase): props.merge_type = 'MATH' -class NWBatchChangeNodesMenu(Menu, NWBase): +class NWBatchChangeNodesMenu(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_batch_change_nodes_menu" bl_label = "Batch Change Selected Nodes" @@ -223,7 +223,7 @@ class NWBatchChangeNodesMenu(Menu, NWBase): layout.menu(NWBatchChangeOperationMenu.bl_idname) -class NWBatchChangeBlendTypeMenu(Menu, NWBase): +class NWBatchChangeBlendTypeMenu(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_batch_change_blend_type_menu" bl_label = "Batch Change Blend Type" @@ -235,7 +235,7 @@ class NWBatchChangeBlendTypeMenu(Menu, NWBase): props.operation = 'CURRENT' -class NWBatchChangeOperationMenu(Menu, NWBase): +class NWBatchChangeOperationMenu(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_batch_change_operation_menu" bl_label = "Batch Change Math Operation" @@ -247,7 +247,7 @@ class NWBatchChangeOperationMenu(Menu, NWBase): props.operation = type -class NWCopyToSelectedMenu(Menu, NWBase): +class NWCopyToSelectedMenu(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_copy_node_properties_menu" bl_label = "Copy to Selected" @@ -257,7 +257,7 @@ class NWCopyToSelectedMenu(Menu, NWBase): layout.menu(NWCopyLabelMenu.bl_idname) -class NWCopyLabelMenu(Menu, NWBase): +class NWCopyLabelMenu(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_copy_label_menu" bl_label = "Copy Label" @@ -268,7 +268,7 @@ class NWCopyLabelMenu(Menu, NWBase): layout.operator(operators.NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET' -class NWAddReroutesMenu(Menu, NWBase): +class NWAddReroutesMenu(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_add_reroutes_menu" bl_label = "Add Reroutes" bl_description = "Add Reroute Nodes to Selected Nodes' Outputs" @@ -280,7 +280,7 @@ class NWAddReroutesMenu(Menu, NWBase): layout.operator(operators.NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED' -class NWLinkActiveToSelectedMenu(Menu, NWBase): +class NWLinkActiveToSelectedMenu(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_link_active_to_selected_menu" bl_label = "Link Active to Selected" @@ -291,7 +291,7 @@ class NWLinkActiveToSelectedMenu(Menu, NWBase): layout.menu(NWLinkUseOutputsNamesMenu.bl_idname) -class NWLinkStandardMenu(Menu, NWBase): +class NWLinkStandardMenu(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_link_standard_menu" bl_label = "To All Selected" @@ -307,7 +307,7 @@ class NWLinkStandardMenu(Menu, NWBase): props.use_outputs_names = False -class NWLinkUseNodeNameMenu(Menu, NWBase): +class NWLinkUseNodeNameMenu(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_link_use_node_name_menu" bl_label = "Use Node Name/Label" @@ -323,7 +323,7 @@ class NWLinkUseNodeNameMenu(Menu, NWBase): props.use_outputs_names = False -class NWLinkUseOutputsNamesMenu(Menu, NWBase): +class NWLinkUseOutputsNamesMenu(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_link_use_outputs_names_menu" bl_label = "Use Outputs Names" @@ -345,7 +345,11 @@ class NWAttributeMenu(bpy.types.Menu): @classmethod def poll(cls, context): - return nw_check(context) and context.space_data.tree_type == 'ShaderNodeTree' + space = context.space_data + return (space.type == 'NODE_EDITOR' + and space.node_tree is not None + and space.node_tree.library is None + and space.tree_type == 'ShaderNodeTree') def draw(self, context): l = self.layout @@ -372,7 +376,7 @@ class NWAttributeMenu(bpy.types.Menu): l.label(text="No attributes on objects with this material") -class NWSwitchNodeTypeMenu(Menu, NWBase): +class NWSwitchNodeTypeMenu(Menu, NWBaseMenu): bl_idname = "NODE_MT_nw_switch_node_type_menu" bl_label = "Switch Type to..." @@ -411,8 +415,11 @@ def bgreset_menu_func(self, context): def save_viewer_menu_func(self, context): - if (nw_check(context) - and context.space_data.tree_type == 'CompositorNodeTree' + space = context.space_data + if (space.type == 'NODE_EDITOR' + and space.node_tree is not None + and space.node_tree.library is None + and space.tree_type == 'CompositorNodeTree' and context.scene.node_tree.nodes.active and context.scene.node_tree.nodes.active.type == "VIEWER"): self.layout.operator(operators.NWSaveViewer.bl_idname, icon='FILE_IMAGE') @@ -421,18 +428,22 @@ def save_viewer_menu_func(self, context): def reset_nodes_button(self, context): node_active = context.active_node node_selected = context.selected_nodes - node_ignore = ["FRAME", "REROUTE", "GROUP"] - # Check if active node is in the selection and respective type - if (len(node_selected) == 1) and node_active and node_active.select and node_active.type not in node_ignore: - row = self.layout.row() - row.operator(operators.NWResetNodes.bl_idname, text="Reset Node", icon="FILE_REFRESH") - self.layout.separator() + # Check if active node is in the selection, ignore some node types + if (len(node_selected) != 1 + or node_active is None + or not node_active.select + or node_active.type in {"REROUTE", "GROUP"}): + return + + row = self.layout.row() - elif (len(node_selected) == 1) and node_active and node_active.select and node_active.type == "FRAME": - row = self.layout.row() + if node_active.type == "FRAME": row.operator(operators.NWResetNodes.bl_idname, text="Reset Nodes in Frame", icon="FILE_REFRESH") - self.layout.separator() + else: + row.operator(operators.NWResetNodes.bl_idname, text="Reset Node", icon="FILE_REFRESH") + + self.layout.separator() classes = ( diff --git a/node_wrangler/operators.py b/node_wrangler/operators.py index 9de22a07..d719925a 100644 --- a/node_wrangler/operators.py +++ b/node_wrangler/operators.py @@ -29,7 +29,9 @@ from .utils.draw import draw_callback_nodeoutline from .utils.paths import match_files_to_socket_names, split_into_components from .utils.nodes import (node_mid_pt, autolink, node_at_pos, get_nodes_links, is_viewer_socket, is_viewer_link, get_group_output_node, get_output_location, force_update, get_internal_socket, nw_check, - nw_check_space_type, NWBase, get_first_enabled_output, is_visible_socket, viewer_socket_name) + nw_check_not_empty, nw_check_selected, nw_check_active, nw_check_space_type, + nw_check_node_type, nw_check_visible_outputs, nw_check_viewer_node, NWBase, + get_first_enabled_output, is_visible_socket, viewer_socket_name) class NWLazyMix(Operator, NWBase): """Add a Mix RGB/Shader node by interactively drawing lines between nodes""" @@ -37,6 +39,10 @@ class NWLazyMix(Operator, NWBase): bl_label = "Mix Nodes" bl_options = {'REGISTER', 'UNDO'} + @classmethod + def poll(cls, context): + return nw_check(cls, context) and nw_check_not_empty(cls, context) + def modal(self, context, event): context.area.tag_redraw() nodes, links = get_nodes_links(context) @@ -115,6 +121,10 @@ class NWLazyConnect(Operator, NWBase): bl_options = {'REGISTER', 'UNDO'} with_menu: BoolProperty() + @classmethod + def poll(cls, context): + return nw_check(cls, context) and nw_check_not_empty(cls, context) + def modal(self, context, event): context.area.tag_redraw() nodes, links = get_nodes_links(context) @@ -244,10 +254,10 @@ class NWDeleteUnused(Operator, NWBase): @classmethod def poll(cls, context): """Disabled for custom nodes as we do not know which nodes are supported.""" - return (nw_check(context) - and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree', - 'TextureNodeTree', 'GeometryNodeTree') - and context.space_data.node_tree.nodes) + return (nw_check(cls, context) + and nw_check_not_empty(cls, context) + and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree', + 'TextureNodeTree', 'GeometryNodeTree'})) def execute(self, context): nodes, links = get_nodes_links(context) @@ -334,7 +344,7 @@ class NWSwapLinks(Operator, NWBase): @classmethod def poll(cls, context): - return nw_check(context) and context.selected_nodes and len(context.selected_nodes) <= 2 + return nw_check(cls, context) and nw_check_selected(cls, context, max=2) def execute(self, context): nodes, links = get_nodes_links(context) @@ -448,8 +458,7 @@ class NWResetBG(Operator, NWBase): @classmethod def poll(cls, context): - return (nw_check(context) - and nw_check_space_type(cls, context, 'CompositorNodeTree')) + return nw_check(cls, context) and nw_check_space_type(cls, context, {'CompositorNodeTree'}) def execute(self, context): context.space_data.backdrop_zoom = 1 @@ -468,8 +477,7 @@ class NWAddAttrNode(Operator, NWBase): @classmethod def poll(cls, context): - return (nw_check(context) - and nw_check_space_type(cls, context, 'ShaderNodeTree')) + return nw_check(cls, context) and nw_check_space_type(cls, context, {'ShaderNodeTree'}) def execute(self, context): bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute") @@ -495,10 +503,8 @@ class NWPreviewNode(Operator, NWBase): @classmethod def poll(cls, context): """Already implemented natively for compositing nodes.""" - return (nw_check(context) - and nw_check_space_type(cls, context, 'ShaderNodeTree', 'GeometryNodeTree') - and (not context.active_node - or context.active_node.type not in {"OUTPUT_MATERIAL", "OUTPUT_WORLD"})) + return (nw_check(cls, context) + and nw_check_space_type(cls, context, {'ShaderNodeTree', 'GeometryNodeTree'})) @staticmethod def get_output_sockets(node_tree): @@ -845,11 +851,10 @@ class NWReloadImages(Operator): @classmethod def poll(cls, context): - return (nw_check(context) - and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree', - 'TextureNodeTree', 'GeometryNodeTree') - and context.active_node is not None - and any(is_visible_socket(out) for out in context.active_node.outputs)) + """Disabled for custom nodes.""" + return (nw_check(cls, context) + and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree', + 'TextureNodeTree', 'GeometryNodeTree'})) def execute(self, context): nodes, links = get_nodes_links(context) @@ -975,9 +980,10 @@ class NWMergeNodes(Operator, NWBase): @classmethod def poll(cls, context): - return (nw_check(context) - and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree', - 'TextureNodeTree', 'GeometryNodeTree')) + return (nw_check(cls, context) + and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree', + 'TextureNodeTree', 'GeometryNodeTree'}) + and nw_check_selected(cls, context)) def execute(self, context): settings = context.preferences.addons[__package__].preferences @@ -1298,9 +1304,10 @@ class NWBatchChangeNodes(Operator, NWBase): @classmethod def poll(cls, context): - return (nw_check(context) - and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree', - 'TextureNodeTree', 'GeometryNodeTree')) + return (nw_check(cls, context) + and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree', + 'TextureNodeTree', 'GeometryNodeTree'}) + and nw_check_selected(cls, context)) def execute(self, context): blend_type = self.blend_type @@ -1354,6 +1361,10 @@ class NWChangeMixFactor(Operator, NWBase): bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes" bl_options = {'REGISTER', 'UNDO'} + @classmethod + def poll(cls, context): + return nw_check(cls, context) and nw_check_selected(cls, context) + # option: Change factor. # If option is 1.0 or 0.0 - set to 1.0 or 0.0 # Else - change factor by option value. @@ -1387,24 +1398,15 @@ class NWCopySettings(Operator, NWBase): @classmethod def poll(cls, context): - return (nw_check(context) - and context.active_node is not None - and context.active_node.type != 'FRAME') + return (nw_check(cls, context) + and nw_check_active(cls, context) + and nw_check_selected(cls, context, min=2) + and nw_check_node_type(cls, context, 'FRAME', invert=True)) def execute(self, context): node_active = context.active_node node_selected = context.selected_nodes - - # Error handling - if not (len(node_selected) > 1): - self.report({'ERROR'}, "2 nodes must be selected at least") - return {'CANCELLED'} - - # Check if active node is in the selection selected_node_names = [n.name for n in node_selected] - if node_active.name not in selected_node_names: - self.report({'ERROR'}, "No active node") - return {'CANCELLED'} # Get nodes in selection by type valid_nodes = [n for n in node_selected if n.type == node_active.type] @@ -1498,6 +1500,7 @@ class NWCopyLabel(Operator, NWBase): bl_idname = "node.nw_copy_label" bl_label = "Copy Label" bl_options = {'REGISTER', 'UNDO'} + bl_description = "Copy label from active to selected nodes" option: EnumProperty( name="option", @@ -1509,6 +1512,10 @@ class NWCopyLabel(Operator, NWBase): ) ) + @classmethod + def poll(cls, context): + return nw_check(cls, context) and nw_check_selected(cls, context, min=2) + def execute(self, context): nodes, links = get_nodes_links(context) option = self.option @@ -1542,9 +1549,14 @@ class NWClearLabel(Operator, NWBase): bl_idname = "node.nw_clear_label" bl_label = "Clear Label" bl_options = {'REGISTER', 'UNDO'} + bl_description = "Clear labels on selected nodes" option: BoolProperty() + @classmethod + def poll(cls, context): + return nw_check(cls, context) and nw_check_selected(cls, context) + def execute(self, context): nodes, links = get_nodes_links(context) for node in [n for n in nodes if n.select]: @@ -1560,7 +1572,7 @@ class NWClearLabel(Operator, NWBase): class NWModifyLabels(Operator, NWBase): - """Modify Labels of all selected nodes""" + """Modify labels of all selected nodes""" bl_idname = "node.nw_modify_labels" bl_label = "Modify Labels" bl_options = {'REGISTER', 'UNDO'} @@ -1578,6 +1590,10 @@ class NWModifyLabels(Operator, NWBase): name="Replace with" ) + @classmethod + def poll(cls, context): + return nw_check(cls, context) and nw_check_selected(cls, context) + def execute(self, context): nodes, links = get_nodes_links(context) for node in [n for n in nodes if n.select]: @@ -1605,8 +1621,9 @@ class NWAddTextureSetup(Operator, NWBase): @classmethod def poll(cls, context): - return (nw_check(context) - and nw_check_space_type(cls, context, 'ShaderNodeTree')) + return (nw_check(cls, context) + and nw_check_space_type(cls, context, {'ShaderNodeTree'}) + and nw_check_selected(cls, context)) def execute(self, context): nodes, links = get_nodes_links(context) @@ -1708,23 +1725,22 @@ class NWAddPrincipledSetup(Operator, NWBase, ImportHelper): @classmethod def poll(cls, context): - return (nw_check(context) - and nw_check_space_type(cls, context, 'ShaderNodeTree')) + return (nw_check(cls, context) + and nw_check_active(cls, context) + and nw_check_space_type(cls, context, {'ShaderNodeTree'}) + and nw_check_node_type(cls, context, 'BSDF_PRINCIPLED')) def execute(self, context): # Check if everything is ok if not self.directory: - self.report({'INFO'}, 'No Folder Selected') + self.report({'INFO'}, 'No folder selected') return {'CANCELLED'} if not self.files[:]: - self.report({'INFO'}, 'No Files Selected') + self.report({'INFO'}, 'No files selected') return {'CANCELLED'} nodes, links = get_nodes_links(context) active_node = nodes.active - if not (active_node and active_node.bl_idname == 'ShaderNodeBsdfPrincipled'): - self.report({'INFO'}, 'Select Principled BSDF') - return {'CANCELLED'} # Filter textures names for texturetypes in filenames # [Socket Name, [abbreviations and keyword list], Filename placeholder] @@ -1982,6 +1998,10 @@ class NWAddReroutes(Operator, NWBase): ] ) + @classmethod + def poll(cls, context): + return nw_check(cls, context) and nw_check_selected(cls, context) + def execute(self, context): nodes, _links = get_nodes_links(context) post_select = [] # Nodes to be selected after execution. @@ -2063,9 +2083,9 @@ class NWLinkActiveToSelected(Operator, NWBase): @classmethod def poll(cls, context): - return (nw_check(context) - and context.active_node is not None - and context.active_node.select) + return (nw_check(cls, context) + and nw_check_active(cls, context) + and nw_check_selected(cls, context, min=2)) def execute(self, context): nodes, links = get_nodes_links(context) @@ -2136,6 +2156,10 @@ class NWAlignNodes(Operator, NWBase): bl_options = {'REGISTER', 'UNDO'} margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes') + @classmethod + def poll(cls, context): + return nw_check(cls, context) and nw_check_not_empty(cls, context) + def execute(self, context): nodes, links = get_nodes_links(context) margin = self.margin @@ -2214,6 +2238,10 @@ class NWSelectParentChildren(Operator, NWBase): ) ) + @classmethod + def poll(cls, context): + return nw_check(cls, context) and nw_check_selected(cls, context) + def execute(self, context): nodes, links = get_nodes_links(context) option = self.option @@ -2238,6 +2266,10 @@ class NWDetachOutputs(Operator, NWBase): bl_label = "Detach Outputs" bl_options = {'REGISTER', 'UNDO'} + @classmethod + def poll(cls, context): + return nw_check(cls, context) and nw_check_selected(cls, context) + def execute(self, context): nodes, links = get_nodes_links(context) selected = context.selected_nodes @@ -2263,11 +2295,11 @@ class NWLinkToOutputNode(Operator): @classmethod def poll(cls, context): """Disabled for custom nodes as we do not know which nodes are outputs.""" - return (nw_check(context) - and nw_check_space_type(cls, context, 'ShaderNodeTree', 'CompositorNodeTree', - 'TextureNodeTree', 'GeometryNodeTree') - and context.active_node is not None - and any(is_visible_socket(out) for out in context.active_node.outputs)) + return (nw_check(cls, context) + and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree', + 'TextureNodeTree', 'GeometryNodeTree'}) + and nw_check_active(cls, context) + and nw_check_visible_outputs(cls, context)) def execute(self, context): nodes, links = get_nodes_links(context) @@ -2387,6 +2419,11 @@ class NWAddSequence(Operator, NWBase, ImportHelper): default=True ) + @classmethod + def poll(cls, context): + return (nw_check(cls, context) + and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree'})) + def draw(self, context): layout = self.layout layout.alignment = 'LEFT' @@ -2499,6 +2536,11 @@ class NWAddMultipleImages(Operator, NWBase, ImportHelper): options={'HIDDEN', 'SKIP_SAVE'} ) + @classmethod + def poll(cls, context): + return (nw_check(cls, context) + and nw_check_space_type(cls, context, {'ShaderNodeTree', 'CompositorNodeTree'})) + def execute(self, context): nodes, links = get_nodes_links(context) @@ -2548,8 +2590,8 @@ class NWViewerFocus(bpy.types.Operator): @classmethod def poll(cls, context): - return (nw_check(context) - and nw_check_space_type(cls, context, 'CompositorNodeTree')) + return (nw_check(cls, context) + and nw_check_space_type(cls, context, {'CompositorNodeTree'})) def execute(self, context): return {'FINISHED'} @@ -2619,12 +2661,9 @@ class NWSaveViewer(bpy.types.Operator, ExportHelper): @classmethod def poll(cls, context): - return (nw_check(context) - and nw_check_space_type(cls, context, 'CompositorNodeTree') - and any(img.source == 'VIEWER' - and img.render_slots == 0 - for img in bpy.data.images) - and sum(bpy.data.images["Viewer Node"].size) > 0) # False if not connected or connected but no image + return (nw_check(cls, context) + and nw_check_space_type(cls, context, {'CompositorNodeTree'}) + and nw_check_viewer_node(cls)) def execute(self, context): fp = self.filepath @@ -2662,19 +2701,15 @@ class NWResetNodes(bpy.types.Operator): @classmethod def poll(cls, context): - space = context.space_data - return space.type == 'NODE_EDITOR' + return (nw_check(cls, context) + and nw_check_selected(cls, context) + and nw_check_active(cls, context)) def execute(self, context): node_active = context.active_node node_selected = context.selected_nodes node_ignore = ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"] - # Check if one node is selected at least - if not (len(node_selected) > 0): - self.report({'ERROR'}, "1 node must be selected at least") - return {'CANCELLED'} - active_node_name = node_active.name if node_active.select else None valid_nodes = [n for n in node_selected if n.type not in node_ignore] diff --git a/node_wrangler/utils/nodes.py b/node_wrangler/utils/nodes.py index fd2782af..29c0046f 100644 --- a/node_wrangler/utils/nodes.py +++ b/node_wrangler/utils/nodes.py @@ -4,7 +4,7 @@ import bpy from bpy_extras.node_utils import connect_sockets -from math import hypot +from math import hypot, inf def force_update(context): @@ -200,23 +200,85 @@ def get_output_location(tree): return loc_x, loc_y -def nw_check(context): +def nw_check(cls, context): space = context.space_data + if space.type != 'NODE_EDITOR': + cls.poll_message_set("Current editor is not a node editor.") + return False + if space.node_tree is None: + cls.poll_message_set("No node tree was found in the current node editor.") + return False + if space.node_tree.library is not None: + cls.poll_message_set("Current node tree is linked from another .blend file.") + return False + return True + + +def nw_check_not_empty(cls, context): + if not context.space_data.node_tree.nodes: + cls.poll_message_set("Current node tree does not contain any nodes.") + return False + return True + - return (space.type == 'NODE_EDITOR' - and space.node_tree is not None - and space.node_tree.library is None) +def nw_check_active(cls, context): + if context.active_node is None or not context.active_node.select: + cls.poll_message_set("No active node.") + return False + return True + + +def nw_check_selected(cls, context, min=1, max=inf): + num_selected = len(context.selected_nodes) + if num_selected < min: + if min > 1: + cls.poll_message_set(f"At least {min} nodes must be selected.") + else: + cls.poll_message_set(f"At least {min} node must be selected.") + return False + if num_selected > max: + cls.poll_message_set(f"{num_selected} nodes are selected, but this operator can only work on {max}.") + return False + return True -def nw_check_space_type(cls, context, *args): - if context.space_data.tree_type not in args: - tree_types_str = ", ".join(t.split('NodeTree')[0].lower() for t in sorted(args)) +def nw_check_space_type(cls, context, types): + if context.space_data.tree_type not in types: + tree_types_str = ", ".join(t.split('NodeTree')[0].lower() for t in sorted(types)) cls.poll_message_set("Current node tree type not supported.\n" "Should be one of " + tree_types_str + ".") return False return True +def nw_check_node_type(cls, context, type, invert=False): + if invert and context.active_node.type == type: + cls.poll_message_set(f"Active node should be not of type {type}.") + return False + elif not invert and context.active_node.type != type: + cls.poll_message_set(f"Active node should be of type {type}.") + return False + return True + + +def nw_check_visible_outputs(cls, context): + if not any(is_visible_socket(out) for out in context.active_node.outputs): + cls.poll_message_set("Current node has no visible outputs.") + return False + return True + + +def nw_check_viewer_node(cls): + for img in bpy.data.images: + # False if not connected or connected but no image + if (img.source == 'VIEWER' + and len(img.render_slots) == 0 + and sum(img.size) > 0): + return True + cls.poll_message_set("Viewer image not found.") + return False + + def get_first_enabled_output(node): for output in node.outputs: if output.enabled: @@ -232,4 +294,13 @@ def is_visible_socket(socket): class NWBase: @classmethod def poll(cls, context): - return nw_check(context) + return nw_check(cls, context) + + +class NWBaseMenu: + @classmethod + def poll(cls, context): + space = context.space_data + return (space.type == 'NODE_EDITOR' + and space.node_tree is not None + and space.node_tree.library is None) -- 2.11.4.GIT