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
, NWBaseMenu
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 if tree_type
== 'ShaderNodeTree':
24 col
= layout
.column(align
=True)
25 col
.operator(operators
.NWAddTextureSetup
.bl_idname
, text
="Add Texture Setup", icon
='NODE_SEL')
26 col
.operator(operators
.NWAddPrincipledSetup
.bl_idname
, text
="Add Principled Setup", icon
='NODE_SEL')
29 col
= layout
.column(align
=True)
30 col
.operator(operators
.NWDetachOutputs
.bl_idname
, icon
='UNLINKED')
31 col
.operator(operators
.NWSwapLinks
.bl_idname
)
32 col
.menu(NWAddReroutesMenu
.bl_idname
, text
="Add Reroutes", icon
='LAYER_USED')
35 col
= layout
.column(align
=True)
36 col
.menu(NWLinkActiveToSelectedMenu
.bl_idname
, text
="Link Active To Selected", icon
='LINKED')
37 if tree_type
!= 'GeometryNodeTree':
38 col
.operator(operators
.NWLinkToOutputNode
.bl_idname
, icon
='DRIVER')
41 col
= layout
.column(align
=True)
43 row
= col
.row(align
=True)
44 row
.operator(operators
.NWClearLabel
.bl_idname
).option
= True
45 row
.operator(operators
.NWModifyLabels
.bl_idname
)
47 col
.operator(operators
.NWClearLabel
.bl_idname
).option
= True
48 col
.operator(operators
.NWModifyLabels
.bl_idname
)
49 col
.menu(NWBatchChangeNodesMenu
.bl_idname
, text
="Batch Change")
51 col
.menu(NWCopyToSelectedMenu
.bl_idname
, text
="Copy to Selected")
54 col
= layout
.column(align
=True)
55 if tree_type
== 'CompositorNodeTree':
56 col
.operator(operators
.NWResetBG
.bl_idname
, icon
='ZOOM_PREVIOUS')
57 if tree_type
!= 'GeometryNodeTree':
58 col
.operator(operators
.NWReloadImages
.bl_idname
, icon
='FILE_REFRESH')
61 col
= layout
.column(align
=True)
62 col
.operator(operators
.NWFrameSelected
.bl_idname
, icon
='STICKY_UVS_LOC')
65 col
= layout
.column(align
=True)
66 col
.operator(operators
.NWAlignNodes
.bl_idname
, icon
='CENTER_ONLY')
69 col
= layout
.column(align
=True)
70 col
.operator(operators
.NWDeleteUnused
.bl_idname
, icon
='CANCEL')
74 class NodeWranglerPanel(Panel
, NWBaseMenu
):
75 bl_idname
= "NODE_PT_nw_node_wrangler"
76 bl_space_type
= 'NODE_EDITOR'
77 bl_label
= "Node Wrangler"
79 bl_category
= "Node Wrangler"
81 prepend
: StringProperty(
84 append
: StringProperty()
85 remove
: StringProperty()
87 def draw(self
, context
):
88 self
.layout
.label(text
="(Quick access: Shift+W)")
89 drawlayout(context
, self
.layout
, mode
='panel')
95 class NodeWranglerMenu(Menu
, NWBaseMenu
):
96 bl_idname
= "NODE_MT_nw_node_wrangler_menu"
97 bl_label
= "Node Wrangler"
99 def draw(self
, context
):
100 self
.layout
.operator_context
= 'INVOKE_DEFAULT'
101 drawlayout(context
, self
.layout
)
104 class NWMergeNodesMenu(Menu
, NWBaseMenu
):
105 bl_idname
= "NODE_MT_nw_merge_nodes_menu"
106 bl_label
= "Merge Selected Nodes"
108 def draw(self
, context
):
109 type = context
.space_data
.tree_type
111 if type == 'ShaderNodeTree':
112 layout
.menu(NWMergeShadersMenu
.bl_idname
, text
="Use Shaders")
113 if type == 'GeometryNodeTree':
114 layout
.menu(NWMergeGeometryMenu
.bl_idname
, text
="Use Geometry Nodes")
115 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
117 layout
.menu(NWMergeMixMenu
.bl_idname
, text
="Use Mix Nodes")
118 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
119 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
="Use Z-Combine Nodes")
121 props
.merge_type
= 'ZCOMBINE'
122 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
="Use Alpha Over Nodes")
124 props
.merge_type
= 'ALPHAOVER'
127 class NWMergeGeometryMenu(Menu
, NWBaseMenu
):
128 bl_idname
= "NODE_MT_nw_merge_geometry_menu"
129 bl_label
= "Merge Selected Nodes using Geometry Nodes"
131 def draw(self
, context
):
133 # The boolean node + Join Geometry node
134 for type, name
, description
in geo_combine_operations
:
135 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
=name
)
137 props
.merge_type
= 'GEOMETRY'
140 class NWMergeShadersMenu(Menu
, NWBaseMenu
):
141 bl_idname
= "NODE_MT_nw_merge_shaders_menu"
142 bl_label
= "Merge Selected Nodes using Shaders"
144 def draw(self
, context
):
146 for type in ('MIX', 'ADD'):
147 name
= f
'{type.capitalize()} Shader'
148 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
=name
)
150 props
.merge_type
= 'SHADER'
153 class NWMergeMixMenu(Menu
, NWBaseMenu
):
154 bl_idname
= "NODE_MT_nw_merge_mix_menu"
155 bl_label
= "Merge Selected Nodes using Mix"
157 def draw(self
, context
):
159 for type, name
, description
in blend_types
:
160 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
=name
)
162 props
.merge_type
= 'MIX'
165 class NWConnectionListOutputs(Menu
, NWBaseMenu
):
166 bl_idname
= "NODE_MT_nw_connection_list_out"
169 def draw(self
, context
):
171 nodes
, links
= get_nodes_links(context
)
173 n1
= nodes
[context
.scene
.NWLazySource
]
174 for index
, output
in enumerate(n1
.outputs
):
175 # Only show sockets that are exposed.
178 operators
.NWCallInputsMenu
.bl_idname
,
180 icon
="RADIOBUT_OFF").from_socket
= index
183 class NWConnectionListInputs(Menu
, NWBaseMenu
):
184 bl_idname
= "NODE_MT_nw_connection_list_in"
187 def draw(self
, context
):
189 nodes
, links
= get_nodes_links(context
)
191 n2
= nodes
[context
.scene
.NWLazyTarget
]
193 for index
, input in enumerate(n2
.inputs
):
194 # Only show sockets that are exposed.
195 # This prevents, for example, the scale value socket
196 # of the vector math node being added to the list when
197 # the mode is not 'SCALE'.
199 op
= layout
.operator(operators
.NWMakeLink
.bl_idname
, text
=input.name
, icon
="FORWARD")
200 op
.from_socket
= context
.scene
.NWSourceSocket
204 class NWMergeMathMenu(Menu
, NWBaseMenu
):
205 bl_idname
= "NODE_MT_nw_merge_math_menu"
206 bl_label
= "Merge Selected Nodes using Math"
208 def draw(self
, context
):
210 for type, name
, description
in operations
:
211 props
= layout
.operator(operators
.NWMergeNodes
.bl_idname
, text
=name
)
213 props
.merge_type
= 'MATH'
216 class NWBatchChangeNodesMenu(Menu
, NWBaseMenu
):
217 bl_idname
= "NODE_MT_nw_batch_change_nodes_menu"
218 bl_label
= "Batch Change Selected Nodes"
220 def draw(self
, context
):
222 layout
.menu(NWBatchChangeBlendTypeMenu
.bl_idname
)
223 layout
.menu(NWBatchChangeOperationMenu
.bl_idname
)
226 class NWBatchChangeBlendTypeMenu(Menu
, NWBaseMenu
):
227 bl_idname
= "NODE_MT_nw_batch_change_blend_type_menu"
228 bl_label
= "Batch Change Blend Type"
230 def draw(self
, context
):
232 for type, name
, description
in blend_types
:
233 props
= layout
.operator(operators
.NWBatchChangeNodes
.bl_idname
, text
=name
)
234 props
.blend_type
= type
235 props
.operation
= 'CURRENT'
238 class NWBatchChangeOperationMenu(Menu
, NWBaseMenu
):
239 bl_idname
= "NODE_MT_nw_batch_change_operation_menu"
240 bl_label
= "Batch Change Math Operation"
242 def draw(self
, context
):
244 for type, name
, description
in operations
:
245 props
= layout
.operator(operators
.NWBatchChangeNodes
.bl_idname
, text
=name
)
246 props
.blend_type
= 'CURRENT'
247 props
.operation
= type
250 class NWCopyToSelectedMenu(Menu
, NWBaseMenu
):
251 bl_idname
= "NODE_MT_nw_copy_node_properties_menu"
252 bl_label
= "Copy to Selected"
254 def draw(self
, context
):
256 layout
.operator(operators
.NWCopySettings
.bl_idname
, text
="Settings from Active")
257 layout
.menu(NWCopyLabelMenu
.bl_idname
)
260 class NWCopyLabelMenu(Menu
, NWBaseMenu
):
261 bl_idname
= "NODE_MT_nw_copy_label_menu"
262 bl_label
= "Copy Label"
264 def draw(self
, context
):
266 layout
.operator(operators
.NWCopyLabel
.bl_idname
, text
="from Active Node's Label").option
= 'FROM_ACTIVE'
267 layout
.operator(operators
.NWCopyLabel
.bl_idname
, text
="from Linked Node's Label").option
= 'FROM_NODE'
268 layout
.operator(operators
.NWCopyLabel
.bl_idname
, text
="from Linked Output's Name").option
= 'FROM_SOCKET'
271 class NWAddReroutesMenu(Menu
, NWBaseMenu
):
272 bl_idname
= "NODE_MT_nw_add_reroutes_menu"
273 bl_label
= "Add Reroutes"
274 bl_description
= "Add Reroute Nodes to Selected Nodes' Outputs"
276 def draw(self
, context
):
278 layout
.operator(operators
.NWAddReroutes
.bl_idname
, text
="to All Outputs").option
= 'ALL'
279 layout
.operator(operators
.NWAddReroutes
.bl_idname
, text
="to Loose Outputs").option
= 'LOOSE'
280 layout
.operator(operators
.NWAddReroutes
.bl_idname
, text
="to Linked Outputs").option
= 'LINKED'
283 class NWLinkActiveToSelectedMenu(Menu
, NWBaseMenu
):
284 bl_idname
= "NODE_MT_nw_link_active_to_selected_menu"
285 bl_label
= "Link Active to Selected"
287 def draw(self
, context
):
289 layout
.menu(NWLinkStandardMenu
.bl_idname
)
290 layout
.menu(NWLinkUseNodeNameMenu
.bl_idname
)
291 layout
.menu(NWLinkUseOutputsNamesMenu
.bl_idname
)
294 class NWLinkStandardMenu(Menu
, NWBaseMenu
):
295 bl_idname
= "NODE_MT_nw_link_standard_menu"
296 bl_label
= "To All Selected"
298 def draw(self
, context
):
300 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
301 props
.replace
= False
302 props
.use_node_name
= False
303 props
.use_outputs_names
= False
304 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
306 props
.use_node_name
= False
307 props
.use_outputs_names
= False
310 class NWLinkUseNodeNameMenu(Menu
, NWBaseMenu
):
311 bl_idname
= "NODE_MT_nw_link_use_node_name_menu"
312 bl_label
= "Use Node Name/Label"
314 def draw(self
, context
):
316 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
317 props
.replace
= False
318 props
.use_node_name
= True
319 props
.use_outputs_names
= False
320 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
322 props
.use_node_name
= True
323 props
.use_outputs_names
= False
326 class NWLinkUseOutputsNamesMenu(Menu
, NWBaseMenu
):
327 bl_idname
= "NODE_MT_nw_link_use_outputs_names_menu"
328 bl_label
= "Use Outputs Names"
330 def draw(self
, context
):
332 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
333 props
.replace
= False
334 props
.use_node_name
= False
335 props
.use_outputs_names
= True
336 props
= layout
.operator(operators
.NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
338 props
.use_node_name
= False
339 props
.use_outputs_names
= True
342 class NWAttributeMenu(bpy
.types
.Menu
):
343 bl_idname
= "NODE_MT_nw_node_attribute_menu"
344 bl_label
= "Attributes"
347 def poll(cls
, context
):
348 space
= context
.space_data
349 return (space
.type == 'NODE_EDITOR'
350 and space
.node_tree
is not None
351 and space
.node_tree
.library
is None
352 and space
.tree_type
== 'ShaderNodeTree')
354 def draw(self
, context
):
356 nodes
, links
= get_nodes_links(context
)
357 mat
= context
.object.active_material
360 for obj
in bpy
.data
.objects
:
361 for slot
in obj
.material_slots
:
362 if slot
.material
== mat
:
366 if obj
.data
.attributes
:
367 for attr
in obj
.data
.attributes
:
368 if not attr
.is_internal
:
369 attrs
.append(attr
.name
)
370 attrs
= list(set(attrs
)) # get a unique list
374 l
.operator(operators
.NWAddAttrNode
.bl_idname
, text
=attr
).attr_name
= attr
376 l
.label(text
="No attributes on objects with this material")
379 class NWSwitchNodeTypeMenu(Menu
, NWBaseMenu
):
380 bl_idname
= "NODE_MT_nw_switch_node_type_menu"
381 bl_label
= "Switch Type to..."
383 def draw(self
, context
):
385 layout
.label(text
="This operator is removed due to the changes of node menus.", icon
='ERROR')
386 layout
.label(text
="A native implementation of the function is expected in the future.")
389 # APPENDAGES TO EXISTING UI
393 def select_parent_children_buttons(self
, context
):
395 layout
.operator(operators
.NWSelectParentChildren
.bl_idname
,
396 text
="Select frame's members (children)").option
= 'CHILD'
397 layout
.operator(operators
.NWSelectParentChildren
.bl_idname
, text
="Select parent frame").option
= 'PARENT'
400 def attr_nodes_menu_func(self
, context
):
401 col
= self
.layout
.column(align
=True)
402 col
.menu("NODE_MT_nw_node_attribute_menu")
406 def multipleimages_menu_func(self
, context
):
407 col
= self
.layout
.column(align
=True)
408 col
.operator(operators
.NWAddMultipleImages
.bl_idname
, text
="Multiple Images")
409 col
.operator(operators
.NWAddSequence
.bl_idname
, text
="Image Sequence")
413 def bgreset_menu_func(self
, context
):
414 self
.layout
.operator(operators
.NWResetBG
.bl_idname
)
417 def save_viewer_menu_func(self
, context
):
418 space
= context
.space_data
419 if (space
.type == 'NODE_EDITOR'
420 and space
.node_tree
is not None
421 and space
.node_tree
.library
is None
422 and space
.tree_type
== 'CompositorNodeTree'
423 and context
.scene
.node_tree
.nodes
.active
424 and context
.scene
.node_tree
.nodes
.active
.type == "VIEWER"):
425 self
.layout
.operator(operators
.NWSaveViewer
.bl_idname
, icon
='FILE_IMAGE')
428 def reset_nodes_button(self
, context
):
429 node_active
= context
.active_node
430 node_selected
= context
.selected_nodes
432 # Check if active node is in the selection, ignore some node types
433 if (len(node_selected
) != 1
434 or node_active
is None
435 or not node_active
.select
436 or node_active
.type in {"REROUTE", "GROUP"}):
439 row
= self
.layout
.row()
441 if node_active
.type == "FRAME":
442 row
.operator(operators
.NWResetNodes
.bl_idname
, text
="Reset Nodes in Frame", icon
="FILE_REFRESH")
444 row
.operator(operators
.NWResetNodes
.bl_idname
, text
="Reset Node", icon
="FILE_REFRESH")
446 self
.layout
.separator()
456 NWConnectionListOutputs
,
457 NWConnectionListInputs
,
459 NWBatchChangeNodesMenu
,
460 NWBatchChangeBlendTypeMenu
,
461 NWBatchChangeOperationMenu
,
462 NWCopyToSelectedMenu
,
465 NWLinkActiveToSelectedMenu
,
467 NWLinkUseNodeNameMenu
,
468 NWLinkUseOutputsNamesMenu
,
470 NWSwitchNodeTypeMenu
,
475 from bpy
.utils
import register_class
480 bpy
.types
.NODE_MT_select
.append(select_parent_children_buttons
)
481 bpy
.types
.NODE_MT_category_shader_input
.prepend(attr_nodes_menu_func
)
482 bpy
.types
.NODE_PT_backdrop
.append(bgreset_menu_func
)
483 bpy
.types
.NODE_PT_active_node_generic
.append(save_viewer_menu_func
)
484 bpy
.types
.NODE_MT_category_shader_texture
.prepend(multipleimages_menu_func
)
485 bpy
.types
.NODE_MT_category_compositor_input
.prepend(multipleimages_menu_func
)
486 bpy
.types
.NODE_PT_active_node_generic
.prepend(reset_nodes_button
)
487 bpy
.types
.NODE_MT_node
.prepend(reset_nodes_button
)
492 bpy
.types
.NODE_MT_select
.remove(select_parent_children_buttons
)
493 bpy
.types
.NODE_MT_category_shader_input
.remove(attr_nodes_menu_func
)
494 bpy
.types
.NODE_PT_backdrop
.remove(bgreset_menu_func
)
495 bpy
.types
.NODE_PT_active_node_generic
.remove(save_viewer_menu_func
)
496 bpy
.types
.NODE_MT_category_shader_texture
.remove(multipleimages_menu_func
)
497 bpy
.types
.NODE_MT_category_compositor_input
.remove(multipleimages_menu_func
)
498 bpy
.types
.NODE_PT_active_node_generic
.remove(reset_nodes_button
)
499 bpy
.types
.NODE_MT_node
.remove(reset_nodes_button
)
501 from bpy
.utils
import unregister_class
503 unregister_class(cls
)