1 # SPDX-FileCopyrightText: 2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 from bpy
.types
import Panel
, Menu
7 from bpy
.props
import StringProperty
8 from nodeitems_utils
import node_categories_iter
, NodeItemCustom
10 from . import operators
12 from .utils
.constants
import blend_types
, geo_combine_operations
, operations
13 from .utils
.nodes
import get_nodes_links
, nw_check
, NWBase
16 def drawlayout(context
, layout
, mode
='non-panel'):
17 tree_type
= context
.space_data
.tree_type
19 col
= layout
.column(align
=True)
20 col
.menu(NWMergeNodesMenu
.bl_idname
)
23 col
= layout
.column(align
=True)
24 col
.menu(NWSwitchNodeTypeMenu
.bl_idname
, text
="Switch Node Type")
27 if tree_type
== 'ShaderNodeTree':
28 col
= layout
.column(align
=True)
29 col
.operator(operators
.NWAddTextureSetup
.bl_idname
, text
="Add Texture Setup", icon
='NODE_SEL')
30 col
.operator(operators
.NWAddPrincipledSetup
.bl_idname
, text
="Add Principled Setup", icon
='NODE_SEL')
33 col
= layout
.column(align
=True)
34 col
.operator(operators
.NWDetachOutputs
.bl_idname
, icon
='UNLINKED')
35 col
.operator(operators
.NWSwapLinks
.bl_idname
)
36 col
.menu(NWAddReroutesMenu
.bl_idname
, text
="Add Reroutes", icon
='LAYER_USED')
39 col
= layout
.column(align
=True)
40 col
.menu(NWLinkActiveToSelectedMenu
.bl_idname
, text
="Link Active To Selected", icon
='LINKED')
41 if tree_type
!= 'GeometryNodeTree':
42 col
.operator(operators
.NWLinkToOutputNode
.bl_idname
, icon
='DRIVER')
45 col
= layout
.column(align
=True)
47 row
= col
.row(align
=True)
48 row
.operator(operators
.NWClearLabel
.bl_idname
).option
= True
49 row
.operator(operators
.NWModifyLabels
.bl_idname
)
51 col
.operator(operators
.NWClearLabel
.bl_idname
).option
= True
52 col
.operator(operators
.NWModifyLabels
.bl_idname
)
53 col
.menu(NWBatchChangeNodesMenu
.bl_idname
, text
="Batch Change")
55 col
.menu(NWCopyToSelectedMenu
.bl_idname
, text
="Copy to Selected")
58 col
= layout
.column(align
=True)
59 if tree_type
== 'CompositorNodeTree':
60 col
.operator(operators
.NWResetBG
.bl_idname
, icon
='ZOOM_PREVIOUS')
61 if tree_type
!= 'GeometryNodeTree':
62 col
.operator(operators
.NWReloadImages
.bl_idname
, icon
='FILE_REFRESH')
65 col
= layout
.column(align
=True)
66 col
.operator(operators
.NWFrameSelected
.bl_idname
, icon
='STICKY_UVS_LOC')
69 col
= layout
.column(align
=True)
70 col
.operator(operators
.NWAlignNodes
.bl_idname
, icon
='CENTER_ONLY')
73 col
= layout
.column(align
=True)
74 col
.operator(operators
.NWDeleteUnused
.bl_idname
, icon
='CANCEL')
78 class NodeWranglerPanel(Panel
, NWBase
):
79 bl_idname
= "NODE_PT_nw_node_wrangler"
80 bl_space_type
= 'NODE_EDITOR'
81 bl_label
= "Node Wrangler"
83 bl_category
= "Node Wrangler"
85 prepend
: StringProperty(
88 append
: StringProperty()
89 remove
: StringProperty()
91 def draw(self
, context
):
92 self
.layout
.label(text
="(Quick access: Shift+W)")
93 drawlayout(context
, self
.layout
, mode
='panel')
99 class NodeWranglerMenu(Menu
, NWBase
):
100 bl_idname
= "NODE_MT_nw_node_wrangler_menu"
101 bl_label
= "Node Wrangler"
103 def draw(self
, context
):
104 self
.layout
.operator_context
= 'INVOKE_DEFAULT'
105 drawlayout(context
, self
.layout
)
108 class NWMergeNodesMenu(Menu
, NWBase
):
109 bl_idname
= "NODE_MT_nw_merge_nodes_menu"
110 bl_label
= "Merge Selected Nodes"
112 def draw(self
, context
):
113 type = context
.space_data
.tree_type
115 if type == 'ShaderNodeTree':
116 layout
.menu(NWMergeShadersMenu
.bl_idname
, text
="Use Shaders")
117 if type == 'GeometryNodeTree':
118 layout
.menu(NWMergeGeometryMenu
.bl_idname
, text
="Use Geometry Nodes")
119 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
121 layout
.menu(NWMergeMixMenu
.bl_idname
, text
="Use Mix Nodes")
122 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
123 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
="Use Z-Combine Nodes")
125 props
.merge_type
= 'ZCOMBINE'
126 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
="Use Alpha Over Nodes")
128 props
.merge_type
= 'ALPHAOVER'
131 class NWMergeGeometryMenu(Menu
, NWBase
):
132 bl_idname
= "NODE_MT_nw_merge_geometry_menu"
133 bl_label
= "Merge Selected Nodes using Geometry Nodes"
135 def draw(self
, context
):
137 # The boolean node + Join Geometry node
138 for type, name
, description
in geo_combine_operations
:
139 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
=name
)
141 props
.merge_type
= 'GEOMETRY'
144 class NWMergeShadersMenu(Menu
, NWBase
):
145 bl_idname
= "NODE_MT_nw_merge_shaders_menu"
146 bl_label
= "Merge Selected Nodes using Shaders"
148 def draw(self
, context
):
150 for type in ('MIX', 'ADD'):
151 name
= f
'{type.capitalize()} Shader'
152 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
=name
)
154 props
.merge_type
= 'SHADER'
157 class NWMergeMixMenu(Menu
, NWBase
):
158 bl_idname
= "NODE_MT_nw_merge_mix_menu"
159 bl_label
= "Merge Selected Nodes using Mix"
161 def draw(self
, context
):
163 for type, name
, description
in blend_types
:
164 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
=name
)
166 props
.merge_type
= 'MIX'
169 class NWConnectionListOutputs(Menu
, NWBase
):
170 bl_idname
= "NODE_MT_nw_connection_list_out"
173 def draw(self
, context
):
175 nodes
, links
= get_nodes_links(context
)
177 n1
= nodes
[context
.scene
.NWLazySource
]
178 for index
, output
in enumerate(n1
.outputs
):
179 # Only show sockets that are exposed.
182 operators
.NWCallInputsMenu
.bl_idname
,
184 icon
="RADIOBUT_OFF").from_socket
= index
187 class NWConnectionListInputs(Menu
, NWBase
):
188 bl_idname
= "NODE_MT_nw_connection_list_in"
191 def draw(self
, context
):
193 nodes
, links
= get_nodes_links(context
)
195 n2
= nodes
[context
.scene
.NWLazyTarget
]
197 for index
, input in enumerate(n2
.inputs
):
198 # Only show sockets that are exposed.
199 # This prevents, for example, the scale value socket
200 # of the vector math node being added to the list when
201 # the mode is not 'SCALE'.
203 op
= layout
.operator(operators
.NWMakeLink
.bl_idname
, text
=input.name
, icon
="FORWARD")
204 op
.from_socket
= context
.scene
.NWSourceSocket
208 class NWMergeMathMenu(Menu
, NWBase
):
209 bl_idname
= "NODE_MT_nw_merge_math_menu"
210 bl_label
= "Merge Selected Nodes using Math"
212 def draw(self
, context
):
214 for type, name
, description
in operations
:
215 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
=name
)
217 props
.merge_type
= 'MATH'
220 class NWBatchChangeNodesMenu(Menu
, NWBase
):
221 bl_idname
= "NODE_MT_nw_batch_change_nodes_menu"
222 bl_label
= "Batch Change Selected Nodes"
224 def draw(self
, context
):
226 layout
.menu(NWBatchChangeBlendTypeMenu
.bl_idname
)
227 layout
.menu(NWBatchChangeOperationMenu
.bl_idname
)
230 class NWBatchChangeBlendTypeMenu(Menu
, NWBase
):
231 bl_idname
= "NODE_MT_nw_batch_change_blend_type_menu"
232 bl_label
= "Batch Change Blend Type"
234 def draw(self
, context
):
236 for type, name
, description
in blend_types
:
237 props
= layout
.operator(operators
.NWBatchChangeNodes
.bl_idname
, text
=name
)
238 props
.blend_type
= type
239 props
.operation
= 'CURRENT'
242 class NWBatchChangeOperationMenu(Menu
, NWBase
):
243 bl_idname
= "NODE_MT_nw_batch_change_operation_menu"
244 bl_label
= "Batch Change Math Operation"
246 def draw(self
, context
):
248 for type, name
, description
in operations
:
249 props
= layout
.operator(operators
.NWBatchChangeNodes
.bl_idname
, text
=name
)
250 props
.blend_type
= 'CURRENT'
251 props
.operation
= type
254 class NWCopyToSelectedMenu(Menu
, NWBase
):
255 bl_idname
= "NODE_MT_nw_copy_node_properties_menu"
256 bl_label
= "Copy to Selected"
258 def draw(self
, context
):
260 layout
.operator(operators
.NWCopySettings
.bl_idname
, text
="Settings from Active")
261 layout
.menu(NWCopyLabelMenu
.bl_idname
)
264 class NWCopyLabelMenu(Menu
, NWBase
):
265 bl_idname
= "NODE_MT_nw_copy_label_menu"
266 bl_label
= "Copy Label"
268 def draw(self
, context
):
270 layout
.operator(operators
.NWCopyLabel
.bl_idname
, text
="from Active Node's Label").option
= 'FROM_ACTIVE'
271 layout
.operator(operators
.NWCopyLabel
.bl_idname
, text
="from Linked Node's Label").option
= 'FROM_NODE'
272 layout
.operator(operators
.NWCopyLabel
.bl_idname
, text
="from Linked Output's Name").option
= 'FROM_SOCKET'
275 class NWAddReroutesMenu(Menu
, NWBase
):
276 bl_idname
= "NODE_MT_nw_add_reroutes_menu"
277 bl_label
= "Add Reroutes"
278 bl_description
= "Add Reroute Nodes to Selected Nodes' Outputs"
280 def draw(self
, context
):
282 layout
.operator(operators
.NWAddReroutes
.bl_idname
, text
="to All Outputs").option
= 'ALL'
283 layout
.operator(operators
.NWAddReroutes
.bl_idname
, text
="to Loose Outputs").option
= 'LOOSE'
284 layout
.operator(operators
.NWAddReroutes
.bl_idname
, text
="to Linked Outputs").option
= 'LINKED'
287 class NWLinkActiveToSelectedMenu(Menu
, NWBase
):
288 bl_idname
= "NODE_MT_nw_link_active_to_selected_menu"
289 bl_label
= "Link Active to Selected"
291 def draw(self
, context
):
293 layout
.menu(NWLinkStandardMenu
.bl_idname
)
294 layout
.menu(NWLinkUseNodeNameMenu
.bl_idname
)
295 layout
.menu(NWLinkUseOutputsNamesMenu
.bl_idname
)
298 class NWLinkStandardMenu(Menu
, NWBase
):
299 bl_idname
= "NODE_MT_nw_link_standard_menu"
300 bl_label
= "To All Selected"
302 def draw(self
, context
):
304 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
305 props
.replace
= False
306 props
.use_node_name
= False
307 props
.use_outputs_names
= False
308 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
310 props
.use_node_name
= False
311 props
.use_outputs_names
= False
314 class NWLinkUseNodeNameMenu(Menu
, NWBase
):
315 bl_idname
= "NODE_MT_nw_link_use_node_name_menu"
316 bl_label
= "Use Node Name/Label"
318 def draw(self
, context
):
320 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
321 props
.replace
= False
322 props
.use_node_name
= True
323 props
.use_outputs_names
= False
324 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
326 props
.use_node_name
= True
327 props
.use_outputs_names
= False
330 class NWLinkUseOutputsNamesMenu(Menu
, NWBase
):
331 bl_idname
= "NODE_MT_nw_link_use_outputs_names_menu"
332 bl_label
= "Use Outputs Names"
334 def draw(self
, context
):
336 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
337 props
.replace
= False
338 props
.use_node_name
= False
339 props
.use_outputs_names
= True
340 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
342 props
.use_node_name
= False
343 props
.use_outputs_names
= True
346 class NWAttributeMenu(bpy
.types
.Menu
):
347 bl_idname
= "NODE_MT_nw_node_attribute_menu"
348 bl_label
= "Attributes"
351 def poll(cls
, context
):
353 if nw_check(context
):
354 snode
= context
.space_data
355 valid
= snode
.tree_type
== 'ShaderNodeTree'
358 def draw(self
, context
):
360 nodes
, links
= get_nodes_links(context
)
361 mat
= context
.object.active_material
364 for obj
in bpy
.data
.objects
:
365 for slot
in obj
.material_slots
:
366 if slot
.material
== mat
:
370 if obj
.data
.attributes
:
371 for attr
in obj
.data
.attributes
:
372 attrs
.append(attr
.name
)
373 attrs
= list(set(attrs
)) # get a unique list
377 l
.operator(operators
.NWAddAttrNode
.bl_idname
, text
=attr
).attr_name
= attr
379 l
.label(text
="No attributes on objects with this material")
382 class NWSwitchNodeTypeMenu(Menu
, NWBase
):
383 bl_idname
= "NODE_MT_nw_switch_node_type_menu"
384 bl_label
= "Switch Type to..."
386 def draw(self
, context
):
388 categories
= [c
for c
in node_categories_iter(context
)
389 if c
.name
not in ['Group', 'Script']]
390 for cat
in categories
:
391 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
392 if hasattr(bpy
.types
, idname
):
395 layout
.label(text
="Unable to load altered node lists.")
396 layout
.label(text
="Please re-enable Node Wrangler.")
400 def draw_switch_category_submenu(self
, context
):
402 if self
.category
.name
== 'Layout':
403 for node
in self
.category
.items(context
):
404 if node
.nodetype
!= 'NodeFrame':
405 props
= layout
.operator(operators
.NWSwitchNodeType
.bl_idname
, text
=node
.label
)
406 props
.to_type
= node
.nodetype
408 for node
in self
.category
.items(context
):
409 if isinstance(node
, NodeItemCustom
):
410 node
.draw(self
, layout
, context
)
412 props
= layout
.operator(operators
.NWSwitchNodeType
.bl_idname
, text
=node
.label
)
413 props
.to_type
= node
.nodetype
416 # APPENDAGES TO EXISTING UI
420 def select_parent_children_buttons(self
, context
):
422 layout
.operator(operators
.NWSelectParentChildren
.bl_idname
,
423 text
="Select frame's members (children)").option
= 'CHILD'
424 layout
.operator(operators
.NWSelectParentChildren
.bl_idname
, text
="Select parent frame").option
= 'PARENT'
427 def attr_nodes_menu_func(self
, context
):
428 col
= self
.layout
.column(align
=True)
429 col
.menu("NODE_MT_nw_node_attribute_menu")
433 def multipleimages_menu_func(self
, context
):
434 col
= self
.layout
.column(align
=True)
435 col
.operator(operators
.NWAddMultipleImages
.bl_idname
, text
="Multiple Images")
436 col
.operator(operators
.NWAddSequence
.bl_idname
, text
="Image Sequence")
440 def bgreset_menu_func(self
, context
):
441 self
.layout
.operator(operators
.NWResetBG
.bl_idname
)
444 def save_viewer_menu_func(self
, context
):
445 if nw_check(context
):
446 if context
.space_data
.tree_type
== 'CompositorNodeTree':
447 if context
.scene
.node_tree
.nodes
.active
:
448 if context
.scene
.node_tree
.nodes
.active
.type == "VIEWER":
449 self
.layout
.operator(operators
.NWSaveViewer
.bl_idname
, icon
='FILE_IMAGE')
452 def reset_nodes_button(self
, context
):
453 node_active
= context
.active_node
454 node_selected
= context
.selected_nodes
455 node_ignore
= ["FRAME", "REROUTE", "GROUP"]
457 # Check if active node is in the selection and respective type
458 if (len(node_selected
) == 1) and node_active
and node_active
.select
and node_active
.type not in node_ignore
:
459 row
= self
.layout
.row()
460 row
.operator(operators
.NWResetNodes
.bl_idname
, text
="Reset Node", icon
="FILE_REFRESH")
461 self
.layout
.separator()
463 elif (len(node_selected
) == 1) and node_active
and node_active
.select
and node_active
.type == "FRAME":
464 row
= self
.layout
.row()
465 row
.operator(operators
.NWResetNodes
.bl_idname
, text
="Reset Nodes in Frame", icon
="FILE_REFRESH")
466 self
.layout
.separator()
476 NWConnectionListOutputs
,
477 NWConnectionListInputs
,
479 NWBatchChangeNodesMenu
,
480 NWBatchChangeBlendTypeMenu
,
481 NWBatchChangeOperationMenu
,
482 NWCopyToSelectedMenu
,
485 NWLinkActiveToSelectedMenu
,
487 NWLinkUseNodeNameMenu
,
488 NWLinkUseOutputsNamesMenu
,
490 NWSwitchNodeTypeMenu
,
495 from bpy
.utils
import register_class
500 bpy
.types
.NODE_MT_select
.append(select_parent_children_buttons
)
501 bpy
.types
.NODE_MT_category_shader_input
.prepend(attr_nodes_menu_func
)
502 bpy
.types
.NODE_PT_backdrop
.append(bgreset_menu_func
)
503 bpy
.types
.NODE_PT_active_node_generic
.append(save_viewer_menu_func
)
504 bpy
.types
.NODE_MT_category_shader_texture
.prepend(multipleimages_menu_func
)
505 bpy
.types
.NODE_MT_category_compositor_input
.prepend(multipleimages_menu_func
)
506 bpy
.types
.NODE_PT_active_node_generic
.prepend(reset_nodes_button
)
507 bpy
.types
.NODE_MT_node
.prepend(reset_nodes_button
)
512 bpy
.types
.NODE_MT_select
.remove(select_parent_children_buttons
)
513 bpy
.types
.NODE_MT_category_shader_input
.remove(attr_nodes_menu_func
)
514 bpy
.types
.NODE_PT_backdrop
.remove(bgreset_menu_func
)
515 bpy
.types
.NODE_PT_active_node_generic
.remove(save_viewer_menu_func
)
516 bpy
.types
.NODE_MT_category_shader_texture
.remove(multipleimages_menu_func
)
517 bpy
.types
.NODE_MT_category_compositor_input
.remove(multipleimages_menu_func
)
518 bpy
.types
.NODE_PT_active_node_generic
.remove(reset_nodes_button
)
519 bpy
.types
.NODE_MT_node
.remove(reset_nodes_button
)
521 from bpy
.utils
import unregister_class
523 unregister_class(cls
)