1 # SPDX-License-Identifier: GPL-2.0-or-later
4 from bpy
.types
import Panel
, Menu
5 from bpy
.props
import StringProperty
6 from nodeitems_utils
import node_categories_iter
, NodeItemCustom
8 from . import operators
10 from .utils
.constants
import blend_types
, geo_combine_operations
, operations
11 from .utils
.nodes
import get_nodes_links
, nw_check
, NWBase
14 def drawlayout(context
, layout
, mode
='non-panel'):
15 tree_type
= context
.space_data
.tree_type
17 col
= layout
.column(align
=True)
18 col
.menu(NWMergeNodesMenu
.bl_idname
)
21 col
= layout
.column(align
=True)
22 col
.menu(NWSwitchNodeTypeMenu
.bl_idname
, text
="Switch Node Type")
25 if tree_type
== 'ShaderNodeTree':
26 col
= layout
.column(align
=True)
27 col
.operator(operators
.NWAddTextureSetup
.bl_idname
, text
="Add Texture Setup", icon
='NODE_SEL')
28 col
.operator(operators
.NWAddPrincipledSetup
.bl_idname
, text
="Add Principled Setup", icon
='NODE_SEL')
31 col
= layout
.column(align
=True)
32 col
.operator(operators
.NWDetachOutputs
.bl_idname
, icon
='UNLINKED')
33 col
.operator(operators
.NWSwapLinks
.bl_idname
)
34 col
.menu(NWAddReroutesMenu
.bl_idname
, text
="Add Reroutes", icon
='LAYER_USED')
37 col
= layout
.column(align
=True)
38 col
.menu(NWLinkActiveToSelectedMenu
.bl_idname
, text
="Link Active To Selected", icon
='LINKED')
39 if tree_type
!= 'GeometryNodeTree':
40 col
.operator(operators
.NWLinkToOutputNode
.bl_idname
, icon
='DRIVER')
43 col
= layout
.column(align
=True)
45 row
= col
.row(align
=True)
46 row
.operator(operators
.NWClearLabel
.bl_idname
).option
= True
47 row
.operator(operators
.NWModifyLabels
.bl_idname
)
49 col
.operator(operators
.NWClearLabel
.bl_idname
).option
= True
50 col
.operator(operators
.NWModifyLabels
.bl_idname
)
51 col
.menu(NWBatchChangeNodesMenu
.bl_idname
, text
="Batch Change")
53 col
.menu(NWCopyToSelectedMenu
.bl_idname
, text
="Copy to Selected")
56 col
= layout
.column(align
=True)
57 if tree_type
== 'CompositorNodeTree':
58 col
.operator(operators
.NWResetBG
.bl_idname
, icon
='ZOOM_PREVIOUS')
59 if tree_type
!= 'GeometryNodeTree':
60 col
.operator(operators
.NWReloadImages
.bl_idname
, icon
='FILE_REFRESH')
63 col
= layout
.column(align
=True)
64 col
.operator(operators
.NWFrameSelected
.bl_idname
, icon
='STICKY_UVS_LOC')
67 col
= layout
.column(align
=True)
68 col
.operator(operators
.NWAlignNodes
.bl_idname
, icon
='CENTER_ONLY')
71 col
= layout
.column(align
=True)
72 col
.operator(operators
.NWDeleteUnused
.bl_idname
, icon
='CANCEL')
76 class NodeWranglerPanel(Panel
, NWBase
):
77 bl_idname
= "NODE_PT_nw_node_wrangler"
78 bl_space_type
= 'NODE_EDITOR'
79 bl_label
= "Node Wrangler"
81 bl_category
= "Node Wrangler"
83 prepend
: StringProperty(
86 append
: StringProperty()
87 remove
: StringProperty()
89 def draw(self
, context
):
90 self
.layout
.label(text
="(Quick access: Shift+W)")
91 drawlayout(context
, self
.layout
, mode
='panel')
97 class NodeWranglerMenu(Menu
, NWBase
):
98 bl_idname
= "NODE_MT_nw_node_wrangler_menu"
99 bl_label
= "Node Wrangler"
101 def draw(self
, context
):
102 self
.layout
.operator_context
= 'INVOKE_DEFAULT'
103 drawlayout(context
, self
.layout
)
106 class NWMergeNodesMenu(Menu
, NWBase
):
107 bl_idname
= "NODE_MT_nw_merge_nodes_menu"
108 bl_label
= "Merge Selected Nodes"
110 def draw(self
, context
):
111 type = context
.space_data
.tree_type
113 if type == 'ShaderNodeTree':
114 layout
.menu(NWMergeShadersMenu
.bl_idname
, text
="Use Shaders")
115 if type == 'GeometryNodeTree':
116 layout
.menu(NWMergeGeometryMenu
.bl_idname
, text
="Use Geometry Nodes")
117 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
119 layout
.menu(NWMergeMixMenu
.bl_idname
, text
="Use Mix Nodes")
120 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
121 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
="Use Z-Combine Nodes")
123 props
.merge_type
= 'ZCOMBINE'
124 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
="Use Alpha Over Nodes")
126 props
.merge_type
= 'ALPHAOVER'
129 class NWMergeGeometryMenu(Menu
, NWBase
):
130 bl_idname
= "NODE_MT_nw_merge_geometry_menu"
131 bl_label
= "Merge Selected Nodes using Geometry Nodes"
133 def draw(self
, context
):
135 # The boolean node + Join Geometry node
136 for type, name
, description
in geo_combine_operations
:
137 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
=name
)
139 props
.merge_type
= 'GEOMETRY'
142 class NWMergeShadersMenu(Menu
, NWBase
):
143 bl_idname
= "NODE_MT_nw_merge_shaders_menu"
144 bl_label
= "Merge Selected Nodes using Shaders"
146 def draw(self
, context
):
148 for type in ('MIX', 'ADD'):
149 name
= f
'{type.capitalize()} Shader'
150 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
=name
)
152 props
.merge_type
= 'SHADER'
155 class NWMergeMixMenu(Menu
, NWBase
):
156 bl_idname
= "NODE_MT_nw_merge_mix_menu"
157 bl_label
= "Merge Selected Nodes using Mix"
159 def draw(self
, context
):
161 for type, name
, description
in blend_types
:
162 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
=name
)
164 props
.merge_type
= 'MIX'
167 class NWConnectionListOutputs(Menu
, NWBase
):
168 bl_idname
= "NODE_MT_nw_connection_list_out"
171 def draw(self
, context
):
173 nodes
, links
= get_nodes_links(context
)
175 n1
= nodes
[context
.scene
.NWLazySource
]
176 for index
, output
in enumerate(n1
.outputs
):
177 # Only show sockets that are exposed.
180 operators
.NWCallInputsMenu
.bl_idname
,
182 icon
="RADIOBUT_OFF").from_socket
= index
185 class NWConnectionListInputs(Menu
, NWBase
):
186 bl_idname
= "NODE_MT_nw_connection_list_in"
189 def draw(self
, context
):
191 nodes
, links
= get_nodes_links(context
)
193 n2
= nodes
[context
.scene
.NWLazyTarget
]
195 for index
, input in enumerate(n2
.inputs
):
196 # Only show sockets that are exposed.
197 # This prevents, for example, the scale value socket
198 # of the vector math node being added to the list when
199 # the mode is not 'SCALE'.
201 op
= layout
.operator(operators
.NWMakeLink
.bl_idname
, text
=input.name
, icon
="FORWARD")
202 op
.from_socket
= context
.scene
.NWSourceSocket
206 class NWMergeMathMenu(Menu
, NWBase
):
207 bl_idname
= "NODE_MT_nw_merge_math_menu"
208 bl_label
= "Merge Selected Nodes using Math"
210 def draw(self
, context
):
212 for type, name
, description
in operations
:
213 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
=name
)
215 props
.merge_type
= 'MATH'
218 class NWBatchChangeNodesMenu(Menu
, NWBase
):
219 bl_idname
= "NODE_MT_nw_batch_change_nodes_menu"
220 bl_label
= "Batch Change Selected Nodes"
222 def draw(self
, context
):
224 layout
.menu(NWBatchChangeBlendTypeMenu
.bl_idname
)
225 layout
.menu(NWBatchChangeOperationMenu
.bl_idname
)
228 class NWBatchChangeBlendTypeMenu(Menu
, NWBase
):
229 bl_idname
= "NODE_MT_nw_batch_change_blend_type_menu"
230 bl_label
= "Batch Change Blend Type"
232 def draw(self
, context
):
234 for type, name
, description
in blend_types
:
235 props
= layout
.operator(operators
.NWBatchChangeNodes
.bl_idname
, text
=name
)
236 props
.blend_type
= type
237 props
.operation
= 'CURRENT'
240 class NWBatchChangeOperationMenu(Menu
, NWBase
):
241 bl_idname
= "NODE_MT_nw_batch_change_operation_menu"
242 bl_label
= "Batch Change Math Operation"
244 def draw(self
, context
):
246 for type, name
, description
in operations
:
247 props
= layout
.operator(operators
.NWBatchChangeNodes
.bl_idname
, text
=name
)
248 props
.blend_type
= 'CURRENT'
249 props
.operation
= type
252 class NWCopyToSelectedMenu(Menu
, NWBase
):
253 bl_idname
= "NODE_MT_nw_copy_node_properties_menu"
254 bl_label
= "Copy to Selected"
256 def draw(self
, context
):
258 layout
.operator(operators
.NWCopySettings
.bl_idname
, text
="Settings from Active")
259 layout
.menu(NWCopyLabelMenu
.bl_idname
)
262 class NWCopyLabelMenu(Menu
, NWBase
):
263 bl_idname
= "NODE_MT_nw_copy_label_menu"
264 bl_label
= "Copy Label"
266 def draw(self
, context
):
268 layout
.operator(operators
.NWCopyLabel
.bl_idname
, text
="from Active Node's Label").option
= 'FROM_ACTIVE'
269 layout
.operator(operators
.NWCopyLabel
.bl_idname
, text
="from Linked Node's Label").option
= 'FROM_NODE'
270 layout
.operator(operators
.NWCopyLabel
.bl_idname
, text
="from Linked Output's Name").option
= 'FROM_SOCKET'
273 class NWAddReroutesMenu(Menu
, NWBase
):
274 bl_idname
= "NODE_MT_nw_add_reroutes_menu"
275 bl_label
= "Add Reroutes"
276 bl_description
= "Add Reroute Nodes to Selected Nodes' Outputs"
278 def draw(self
, context
):
280 layout
.operator(operators
.NWAddReroutes
.bl_idname
, text
="to All Outputs").option
= 'ALL'
281 layout
.operator(operators
.NWAddReroutes
.bl_idname
, text
="to Loose Outputs").option
= 'LOOSE'
282 layout
.operator(operators
.NWAddReroutes
.bl_idname
, text
="to Linked Outputs").option
= 'LINKED'
285 class NWLinkActiveToSelectedMenu(Menu
, NWBase
):
286 bl_idname
= "NODE_MT_nw_link_active_to_selected_menu"
287 bl_label
= "Link Active to Selected"
289 def draw(self
, context
):
291 layout
.menu(NWLinkStandardMenu
.bl_idname
)
292 layout
.menu(NWLinkUseNodeNameMenu
.bl_idname
)
293 layout
.menu(NWLinkUseOutputsNamesMenu
.bl_idname
)
296 class NWLinkStandardMenu(Menu
, NWBase
):
297 bl_idname
= "NODE_MT_nw_link_standard_menu"
298 bl_label
= "To All Selected"
300 def draw(self
, context
):
302 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
303 props
.replace
= False
304 props
.use_node_name
= False
305 props
.use_outputs_names
= False
306 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
308 props
.use_node_name
= False
309 props
.use_outputs_names
= False
312 class NWLinkUseNodeNameMenu(Menu
, NWBase
):
313 bl_idname
= "NODE_MT_nw_link_use_node_name_menu"
314 bl_label
= "Use Node Name/Label"
316 def draw(self
, context
):
318 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
319 props
.replace
= False
320 props
.use_node_name
= True
321 props
.use_outputs_names
= False
322 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
324 props
.use_node_name
= True
325 props
.use_outputs_names
= False
328 class NWLinkUseOutputsNamesMenu(Menu
, NWBase
):
329 bl_idname
= "NODE_MT_nw_link_use_outputs_names_menu"
330 bl_label
= "Use Outputs Names"
332 def draw(self
, context
):
334 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
335 props
.replace
= False
336 props
.use_node_name
= False
337 props
.use_outputs_names
= True
338 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
340 props
.use_node_name
= False
341 props
.use_outputs_names
= True
344 class NWAttributeMenu(bpy
.types
.Menu
):
345 bl_idname
= "NODE_MT_nw_node_attribute_menu"
346 bl_label
= "Attributes"
349 def poll(cls
, context
):
351 if nw_check(context
):
352 snode
= context
.space_data
353 valid
= snode
.tree_type
== 'ShaderNodeTree'
356 def draw(self
, context
):
358 nodes
, links
= get_nodes_links(context
)
359 mat
= context
.object.active_material
362 for obj
in bpy
.data
.objects
:
363 for slot
in obj
.material_slots
:
364 if slot
.material
== mat
:
368 if obj
.data
.attributes
:
369 for attr
in obj
.data
.attributes
:
370 attrs
.append(attr
.name
)
371 attrs
= list(set(attrs
)) # get a unique list
375 l
.operator(operators
.NWAddAttrNode
.bl_idname
, text
=attr
).attr_name
= attr
377 l
.label(text
="No attributes on objects with this material")
380 class NWSwitchNodeTypeMenu(Menu
, NWBase
):
381 bl_idname
= "NODE_MT_nw_switch_node_type_menu"
382 bl_label
= "Switch Type to..."
384 def draw(self
, context
):
386 categories
= [c
for c
in node_categories_iter(context
)
387 if c
.name
not in ['Group', 'Script']]
388 for cat
in categories
:
389 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
390 if hasattr(bpy
.types
, idname
):
393 layout
.label(text
="Unable to load altered node lists.")
394 layout
.label(text
="Please re-enable Node Wrangler.")
398 def draw_switch_category_submenu(self
, context
):
400 if self
.category
.name
== 'Layout':
401 for node
in self
.category
.items(context
):
402 if node
.nodetype
!= 'NodeFrame':
403 props
= layout
.operator(operators
.NWSwitchNodeType
.bl_idname
, text
=node
.label
)
404 props
.to_type
= node
.nodetype
406 for node
in self
.category
.items(context
):
407 if isinstance(node
, NodeItemCustom
):
408 node
.draw(self
, layout
, context
)
410 props
= layout
.operator(operators
.NWSwitchNodeType
.bl_idname
, text
=node
.label
)
411 props
.to_type
= node
.nodetype
414 # APPENDAGES TO EXISTING UI
418 def select_parent_children_buttons(self
, context
):
420 layout
.operator(operators
.NWSelectParentChildren
.bl_idname
,
421 text
="Select frame's members (children)").option
= 'CHILD'
422 layout
.operator(operators
.NWSelectParentChildren
.bl_idname
, text
="Select parent frame").option
= 'PARENT'
425 def attr_nodes_menu_func(self
, context
):
426 col
= self
.layout
.column(align
=True)
427 col
.menu("NODE_MT_nw_node_attribute_menu")
431 def multipleimages_menu_func(self
, context
):
432 col
= self
.layout
.column(align
=True)
433 col
.operator(operators
.NWAddMultipleImages
.bl_idname
, text
="Multiple Images")
434 col
.operator(operators
.NWAddSequence
.bl_idname
, text
="Image Sequence")
438 def bgreset_menu_func(self
, context
):
439 self
.layout
.operator(operators
.NWResetBG
.bl_idname
)
442 def save_viewer_menu_func(self
, context
):
443 if nw_check(context
):
444 if context
.space_data
.tree_type
== 'CompositorNodeTree':
445 if context
.scene
.node_tree
.nodes
.active
:
446 if context
.scene
.node_tree
.nodes
.active
.type == "VIEWER":
447 self
.layout
.operator(operators
.NWSaveViewer
.bl_idname
, icon
='FILE_IMAGE')
450 def reset_nodes_button(self
, context
):
451 node_active
= context
.active_node
452 node_selected
= context
.selected_nodes
453 node_ignore
= ["FRAME", "REROUTE", "GROUP"]
455 # Check if active node is in the selection and respective type
456 if (len(node_selected
) == 1) and node_active
and node_active
.select
and node_active
.type not in node_ignore
:
457 row
= self
.layout
.row()
458 row
.operator(operators
.NWResetNodes
.bl_idname
, text
="Reset Node", icon
="FILE_REFRESH")
459 self
.layout
.separator()
461 elif (len(node_selected
) == 1) and node_active
and node_active
.select
and node_active
.type == "FRAME":
462 row
= self
.layout
.row()
463 row
.operator(operators
.NWResetNodes
.bl_idname
, text
="Reset Nodes in Frame", icon
="FILE_REFRESH")
464 self
.layout
.separator()
474 NWConnectionListOutputs
,
475 NWConnectionListInputs
,
477 NWBatchChangeNodesMenu
,
478 NWBatchChangeBlendTypeMenu
,
479 NWBatchChangeOperationMenu
,
480 NWCopyToSelectedMenu
,
483 NWLinkActiveToSelectedMenu
,
485 NWLinkUseNodeNameMenu
,
486 NWLinkUseOutputsNamesMenu
,
488 NWSwitchNodeTypeMenu
,
493 from bpy
.utils
import register_class
498 bpy
.types
.NODE_MT_select
.append(select_parent_children_buttons
)
499 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.prepend(attr_nodes_menu_func
)
500 bpy
.types
.NODE_PT_backdrop
.append(bgreset_menu_func
)
501 bpy
.types
.NODE_PT_active_node_generic
.append(save_viewer_menu_func
)
502 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.prepend(multipleimages_menu_func
)
503 bpy
.types
.NODE_MT_category_CMP_INPUT
.prepend(multipleimages_menu_func
)
504 bpy
.types
.NODE_PT_active_node_generic
.prepend(reset_nodes_button
)
505 bpy
.types
.NODE_MT_node
.prepend(reset_nodes_button
)
510 bpy
.types
.NODE_MT_select
.remove(select_parent_children_buttons
)
511 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.remove(attr_nodes_menu_func
)
512 bpy
.types
.NODE_PT_backdrop
.remove(bgreset_menu_func
)
513 bpy
.types
.NODE_PT_active_node_generic
.remove(save_viewer_menu_func
)
514 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.remove(multipleimages_menu_func
)
515 bpy
.types
.NODE_MT_category_CMP_INPUT
.remove(multipleimages_menu_func
)
516 bpy
.types
.NODE_PT_active_node_generic
.remove(reset_nodes_button
)
517 bpy
.types
.NODE_MT_node
.remove(reset_nodes_button
)
519 from bpy
.utils
import unregister_class
521 unregister_class(cls
)