Cleanup: Node Wrangler: preview_node operator
[blender-addons.git] / node_wrangler / interface.py
blobf2ac3e8daeaa516be3c365c48af4f374c31b983a
1 # SPDX-FileCopyrightText: 2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 import bpy
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)
21 col.separator()
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')
27 col.separator()
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')
33 col.separator()
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')
39 col.separator()
41 col = layout.column(align=True)
42 if mode == 'panel':
43 row = col.row(align=True)
44 row.operator(operators.NWClearLabel.bl_idname).option = True
45 row.operator(operators.NWModifyLabels.bl_idname)
46 else:
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")
50 col.separator()
51 col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected")
52 col.separator()
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')
59 col.separator()
61 col = layout.column(align=True)
62 col.operator(operators.NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC')
63 col.separator()
65 col = layout.column(align=True)
66 col.operator(operators.NWAlignNodes.bl_idname, icon='CENTER_ONLY')
67 col.separator()
69 col = layout.column(align=True)
70 col.operator(operators.NWDeleteUnused.bl_idname, icon='CANCEL')
71 col.separator()
74 class NodeWranglerPanel(Panel, NWBase):
75 bl_idname = "NODE_PT_nw_node_wrangler"
76 bl_space_type = 'NODE_EDITOR'
77 bl_label = "Node Wrangler"
78 bl_region_type = "UI"
79 bl_category = "Node Wrangler"
81 prepend: StringProperty(
82 name='prepend',
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')
93 # M E N U S
95 class NodeWranglerMenu(Menu, NWBase):
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, NWBase):
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
110 layout = self.layout
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")
116 else:
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")
120 props.mode = 'MIX'
121 props.merge_type = 'ZCOMBINE'
122 props = layout.operator(operators.NWMergeNodes.bl_idname, text="Use Alpha Over Nodes")
123 props.mode = 'MIX'
124 props.merge_type = 'ALPHAOVER'
127 class NWMergeGeometryMenu(Menu, NWBase):
128 bl_idname = "NODE_MT_nw_merge_geometry_menu"
129 bl_label = "Merge Selected Nodes using Geometry Nodes"
131 def draw(self, context):
132 layout = self.layout
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)
136 props.mode = type
137 props.merge_type = 'GEOMETRY'
140 class NWMergeShadersMenu(Menu, NWBase):
141 bl_idname = "NODE_MT_nw_merge_shaders_menu"
142 bl_label = "Merge Selected Nodes using Shaders"
144 def draw(self, context):
145 layout = self.layout
146 for type in ('MIX', 'ADD'):
147 name = f'{type.capitalize()} Shader'
148 props = layout.operator(operators.NWMergeNodes.bl_idname, text=name)
149 props.mode = type
150 props.merge_type = 'SHADER'
153 class NWMergeMixMenu(Menu, NWBase):
154 bl_idname = "NODE_MT_nw_merge_mix_menu"
155 bl_label = "Merge Selected Nodes using Mix"
157 def draw(self, context):
158 layout = self.layout
159 for type, name, description in blend_types:
160 props = layout.operator(operators.NWMergeNodes.bl_idname, text=name)
161 props.mode = type
162 props.merge_type = 'MIX'
165 class NWConnectionListOutputs(Menu, NWBase):
166 bl_idname = "NODE_MT_nw_connection_list_out"
167 bl_label = "From:"
169 def draw(self, context):
170 layout = self.layout
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.
176 if output.enabled:
177 layout.operator(
178 operators.NWCallInputsMenu.bl_idname,
179 text=output.name,
180 icon="RADIOBUT_OFF").from_socket = index
183 class NWConnectionListInputs(Menu, NWBase):
184 bl_idname = "NODE_MT_nw_connection_list_in"
185 bl_label = "To:"
187 def draw(self, context):
188 layout = self.layout
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'.
198 if input.enabled:
199 op = layout.operator(operators.NWMakeLink.bl_idname, text=input.name, icon="FORWARD")
200 op.from_socket = context.scene.NWSourceSocket
201 op.to_socket = index
204 class NWMergeMathMenu(Menu, NWBase):
205 bl_idname = "NODE_MT_nw_merge_math_menu"
206 bl_label = "Merge Selected Nodes using Math"
208 def draw(self, context):
209 layout = self.layout
210 for type, name, description in operations:
211 props = layout.operator(operators.NWMergeNodes.bl_idname, text=name)
212 props.mode = type
213 props.merge_type = 'MATH'
216 class NWBatchChangeNodesMenu(Menu, NWBase):
217 bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
218 bl_label = "Batch Change Selected Nodes"
220 def draw(self, context):
221 layout = self.layout
222 layout.menu(NWBatchChangeBlendTypeMenu.bl_idname)
223 layout.menu(NWBatchChangeOperationMenu.bl_idname)
226 class NWBatchChangeBlendTypeMenu(Menu, NWBase):
227 bl_idname = "NODE_MT_nw_batch_change_blend_type_menu"
228 bl_label = "Batch Change Blend Type"
230 def draw(self, context):
231 layout = self.layout
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, NWBase):
239 bl_idname = "NODE_MT_nw_batch_change_operation_menu"
240 bl_label = "Batch Change Math Operation"
242 def draw(self, context):
243 layout = self.layout
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, NWBase):
251 bl_idname = "NODE_MT_nw_copy_node_properties_menu"
252 bl_label = "Copy to Selected"
254 def draw(self, context):
255 layout = self.layout
256 layout.operator(operators.NWCopySettings.bl_idname, text="Settings from Active")
257 layout.menu(NWCopyLabelMenu.bl_idname)
260 class NWCopyLabelMenu(Menu, NWBase):
261 bl_idname = "NODE_MT_nw_copy_label_menu"
262 bl_label = "Copy Label"
264 def draw(self, context):
265 layout = self.layout
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, NWBase):
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):
277 layout = self.layout
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, NWBase):
284 bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
285 bl_label = "Link Active to Selected"
287 def draw(self, context):
288 layout = self.layout
289 layout.menu(NWLinkStandardMenu.bl_idname)
290 layout.menu(NWLinkUseNodeNameMenu.bl_idname)
291 layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
294 class NWLinkStandardMenu(Menu, NWBase):
295 bl_idname = "NODE_MT_nw_link_standard_menu"
296 bl_label = "To All Selected"
298 def draw(self, context):
299 layout = self.layout
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")
305 props.replace = True
306 props.use_node_name = False
307 props.use_outputs_names = False
310 class NWLinkUseNodeNameMenu(Menu, NWBase):
311 bl_idname = "NODE_MT_nw_link_use_node_name_menu"
312 bl_label = "Use Node Name/Label"
314 def draw(self, context):
315 layout = self.layout
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")
321 props.replace = True
322 props.use_node_name = True
323 props.use_outputs_names = False
326 class NWLinkUseOutputsNamesMenu(Menu, NWBase):
327 bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
328 bl_label = "Use Outputs Names"
330 def draw(self, context):
331 layout = self.layout
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")
337 props.replace = True
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"
346 @classmethod
347 def poll(cls, context):
348 return nw_check(context) and context.space_data.tree_type == 'ShaderNodeTree'
350 def draw(self, context):
351 l = self.layout
352 nodes, links = get_nodes_links(context)
353 mat = context.object.active_material
355 objs = []
356 for obj in bpy.data.objects:
357 for slot in obj.material_slots:
358 if slot.material == mat:
359 objs.append(obj)
360 attrs = []
361 for obj in objs:
362 if obj.data.attributes:
363 for attr in obj.data.attributes:
364 attrs.append(attr.name)
365 attrs = list(set(attrs)) # get a unique list
367 if attrs:
368 for attr in attrs:
369 l.operator(operators.NWAddAttrNode.bl_idname, text=attr).attr_name = attr
370 else:
371 l.label(text="No attributes on objects with this material")
374 class NWSwitchNodeTypeMenu(Menu, NWBase):
375 bl_idname = "NODE_MT_nw_switch_node_type_menu"
376 bl_label = "Switch Type to..."
378 def draw(self, context):
379 layout = self.layout
380 layout.label(text="This operator is removed due to the changes of node menus.", icon='ERROR')
381 layout.label(text="A native implementation of the function is expected in the future.")
384 # APPENDAGES TO EXISTING UI
388 def select_parent_children_buttons(self, context):
389 layout = self.layout
390 layout.operator(operators.NWSelectParentChildren.bl_idname,
391 text="Select frame's members (children)").option = 'CHILD'
392 layout.operator(operators.NWSelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT'
395 def attr_nodes_menu_func(self, context):
396 col = self.layout.column(align=True)
397 col.menu("NODE_MT_nw_node_attribute_menu")
398 col.separator()
401 def multipleimages_menu_func(self, context):
402 col = self.layout.column(align=True)
403 col.operator(operators.NWAddMultipleImages.bl_idname, text="Multiple Images")
404 col.operator(operators.NWAddSequence.bl_idname, text="Image Sequence")
405 col.separator()
408 def bgreset_menu_func(self, context):
409 self.layout.operator(operators.NWResetBG.bl_idname)
412 def save_viewer_menu_func(self, context):
413 if (nw_check(context)
414 and context.space_data.tree_type == 'CompositorNodeTree'
415 and context.scene.node_tree.nodes.active
416 and context.scene.node_tree.nodes.active.type == "VIEWER"):
417 self.layout.operator(operators.NWSaveViewer.bl_idname, icon='FILE_IMAGE')
420 def reset_nodes_button(self, context):
421 node_active = context.active_node
422 node_selected = context.selected_nodes
423 node_ignore = ["FRAME", "REROUTE", "GROUP"]
425 # Check if active node is in the selection and respective type
426 if (len(node_selected) == 1) and node_active and node_active.select and node_active.type not in node_ignore:
427 row = self.layout.row()
428 row.operator(operators.NWResetNodes.bl_idname, text="Reset Node", icon="FILE_REFRESH")
429 self.layout.separator()
431 elif (len(node_selected) == 1) and node_active and node_active.select and node_active.type == "FRAME":
432 row = self.layout.row()
433 row.operator(operators.NWResetNodes.bl_idname, text="Reset Nodes in Frame", icon="FILE_REFRESH")
434 self.layout.separator()
437 classes = (
438 NodeWranglerPanel,
439 NodeWranglerMenu,
440 NWMergeNodesMenu,
441 NWMergeGeometryMenu,
442 NWMergeShadersMenu,
443 NWMergeMixMenu,
444 NWConnectionListOutputs,
445 NWConnectionListInputs,
446 NWMergeMathMenu,
447 NWBatchChangeNodesMenu,
448 NWBatchChangeBlendTypeMenu,
449 NWBatchChangeOperationMenu,
450 NWCopyToSelectedMenu,
451 NWCopyLabelMenu,
452 NWAddReroutesMenu,
453 NWLinkActiveToSelectedMenu,
454 NWLinkStandardMenu,
455 NWLinkUseNodeNameMenu,
456 NWLinkUseOutputsNamesMenu,
457 NWAttributeMenu,
458 NWSwitchNodeTypeMenu,
462 def register():
463 from bpy.utils import register_class
464 for cls in classes:
465 register_class(cls)
467 # menu items
468 bpy.types.NODE_MT_select.append(select_parent_children_buttons)
469 bpy.types.NODE_MT_category_shader_input.prepend(attr_nodes_menu_func)
470 bpy.types.NODE_PT_backdrop.append(bgreset_menu_func)
471 bpy.types.NODE_PT_active_node_generic.append(save_viewer_menu_func)
472 bpy.types.NODE_MT_category_shader_texture.prepend(multipleimages_menu_func)
473 bpy.types.NODE_MT_category_compositor_input.prepend(multipleimages_menu_func)
474 bpy.types.NODE_PT_active_node_generic.prepend(reset_nodes_button)
475 bpy.types.NODE_MT_node.prepend(reset_nodes_button)
478 def unregister():
479 # menu items
480 bpy.types.NODE_MT_select.remove(select_parent_children_buttons)
481 bpy.types.NODE_MT_category_shader_input.remove(attr_nodes_menu_func)
482 bpy.types.NODE_PT_backdrop.remove(bgreset_menu_func)
483 bpy.types.NODE_PT_active_node_generic.remove(save_viewer_menu_func)
484 bpy.types.NODE_MT_category_shader_texture.remove(multipleimages_menu_func)
485 bpy.types.NODE_MT_category_compositor_input.remove(multipleimages_menu_func)
486 bpy.types.NODE_PT_active_node_generic.remove(reset_nodes_button)
487 bpy.types.NODE_MT_node.remove(reset_nodes_button)
489 from bpy.utils import unregister_class
490 for cls in classes:
491 unregister_class(cls)