This repository has been archived on 2023-10-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
blender-archive/release/scripts/startup/bl_operators/node.py
Philipp Oeser 4aac251b42 Fix adding certain nodes can cause python errors
Adding nodes via `NodeAddOperator` could fail if the node's poll fails
in `rna_NodeTree_node_new`. Examples are trying to add a RenderLayers
node or a Cryptomatte node to a nodegroup.

Now except the python error and use blender error reporting only instead
of throwing full python stacktraces at the user.

Differential Revision: https://developer.blender.org/D14584
2022-04-11 18:49:16 +02:00

306 lines
8.9 KiB
Python

# SPDX-License-Identifier: GPL-2.0-or-later
# <pep8-80 compliant>
from __future__ import annotations
import bpy
from bpy.types import (
Operator,
PropertyGroup,
)
from bpy.props import (
BoolProperty,
CollectionProperty,
EnumProperty,
IntProperty,
StringProperty,
)
class NodeSetting(PropertyGroup):
value: StringProperty(
name="Value",
description="Python expression to be evaluated "
"as the initial node setting",
default="",
)
# Base class for node 'Add' operators
class NodeAddOperator:
type: StringProperty(
name="Node Type",
description="Node type",
)
use_transform: BoolProperty(
name="Use Transform",
description="Start transform operator after inserting the node",
default=False,
)
settings: CollectionProperty(
name="Settings",
description="Settings to be applied on the newly created node",
type=NodeSetting,
options={'SKIP_SAVE'},
)
@staticmethod
def store_mouse_cursor(context, event):
space = context.space_data
tree = space.edit_tree
# convert mouse position to the View2D for later node placement
if context.region.type == 'WINDOW':
# convert mouse position to the View2D for later node placement
space.cursor_location_from_region(
event.mouse_region_x, event.mouse_region_y)
else:
space.cursor_location = tree.view_center
# XXX explicit node_type argument is usually not necessary,
# but required to make search operator work:
# add_search has to override the 'type' property
# since it's hardcoded in bpy_operator_wrap.c ...
def create_node(self, context, node_type=None):
space = context.space_data
tree = space.edit_tree
if node_type is None:
node_type = self.type
# select only the new node
for n in tree.nodes:
n.select = False
try:
node = tree.nodes.new(type=node_type)
except RuntimeError as e:
self.report({'ERROR'}, str(e))
return None
for setting in self.settings:
# XXX catch exceptions here?
value = eval(setting.value)
node_data = node
node_attr_name = setting.name
# Support path to nested data.
if '.' in node_attr_name:
node_data_path, node_attr_name = node_attr_name.rsplit(".", 1)
node_data = node.path_resolve(node_data_path)
try:
setattr(node_data, node_attr_name, value)
except AttributeError as e:
self.report(
{'ERROR_INVALID_INPUT'},
"Node has no attribute " + setting.name)
print(str(e))
# Continue despite invalid attribute
node.select = True
tree.nodes.active = node
node.location = space.cursor_location
return node
@classmethod
def poll(cls, context):
space = context.space_data
# needs active node editor and a tree to add nodes to
return (space and (space.type == 'NODE_EDITOR') and
space.edit_tree and not space.edit_tree.library)
# Default execute simply adds a node
def execute(self, context):
if self.properties.is_property_set("type"):
self.create_node(context)
return {'FINISHED'}
else:
return {'CANCELLED'}
# Default invoke stores the mouse position to place the node correctly
# and optionally invokes the transform operator
def invoke(self, context, event):
self.store_mouse_cursor(context, event)
result = self.execute(context)
if self.use_transform and ('FINISHED' in result):
# removes the node again if transform is canceled
bpy.ops.node.translate_attach_remove_on_cancel('INVOKE_DEFAULT')
return result
# Simple basic operator for adding a node
class NODE_OT_add_node(NodeAddOperator, Operator):
'''Add a node to the active tree'''
bl_idname = "node.add_node"
bl_label = "Add Node"
bl_options = {'REGISTER', 'UNDO'}
# Add a node and link it to an existing socket
class NODE_OT_add_and_link_node(NodeAddOperator, Operator):
'''Add a node to the active tree and link to an existing socket'''
bl_idname = "node.add_and_link_node"
bl_label = "Add and Link Node"
bl_options = {'REGISTER', 'UNDO'}
link_socket_index: IntProperty(
name="Link Socket Index",
description="Index of the socket to link",
)
def execute(self, context):
space = context.space_data
ntree = space.edit_tree
node = self.create_node(context)
if not node:
return {'CANCELLED'}
to_socket = getattr(context, "link_to_socket", None)
if to_socket:
ntree.links.new(node.outputs[self.link_socket_index], to_socket)
from_socket = getattr(context, "link_from_socket", None)
if from_socket:
ntree.links.new(from_socket, node.inputs[self.link_socket_index])
return {'FINISHED'}
class NODE_OT_add_search(NodeAddOperator, Operator):
'''Add a node to the active tree'''
bl_idname = "node.add_search"
bl_label = "Search and Add Node"
bl_options = {'REGISTER', 'UNDO'}
bl_property = "node_item"
_enum_item_hack = []
# Create an enum list from node items
def node_enum_items(self, context):
import nodeitems_utils
enum_items = NODE_OT_add_search._enum_item_hack
enum_items.clear()
for index, item in enumerate(nodeitems_utils.node_items_iter(context)):
if isinstance(item, nodeitems_utils.NodeItem):
enum_items.append(
(str(index),
item.label,
"",
index,
))
return enum_items
# Look up the item based on index
def find_node_item(self, context):
import nodeitems_utils
node_item = int(self.node_item)
for index, item in enumerate(nodeitems_utils.node_items_iter(context)):
if index == node_item:
return item
return None
node_item: EnumProperty(
name="Node Type",
description="Node type",
items=NODE_OT_add_search.node_enum_items,
)
def execute(self, context):
item = self.find_node_item(context)
# no need to keep
self._enum_item_hack.clear()
if item:
# apply settings from the node item
for setting in item.settings.items():
ops = self.settings.add()
ops.name = setting[0]
ops.value = setting[1]
self.create_node(context, item.nodetype)
if self.use_transform:
bpy.ops.node.translate_attach_remove_on_cancel(
'INVOKE_DEFAULT')
return {'FINISHED'}
else:
return {'CANCELLED'}
def invoke(self, context, event):
self.store_mouse_cursor(context, event)
# Delayed execution in the search popup
context.window_manager.invoke_search_popup(self)
return {'CANCELLED'}
class NODE_OT_collapse_hide_unused_toggle(Operator):
'''Toggle collapsed nodes and hide unused sockets'''
bl_idname = "node.collapse_hide_unused_toggle"
bl_label = "Collapse and Hide Unused Sockets"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
space = context.space_data
# needs active node editor and a tree
return (space and (space.type == 'NODE_EDITOR') and
(space.edit_tree and not space.edit_tree.library))
def execute(self, context):
space = context.space_data
tree = space.edit_tree
for node in tree.nodes:
if node.select:
hide = (not node.hide)
node.hide = hide
# Note: connected sockets are ignored internally
for socket in node.inputs:
socket.hide = hide
for socket in node.outputs:
socket.hide = hide
return {'FINISHED'}
class NODE_OT_tree_path_parent(Operator):
'''Go to parent node tree'''
bl_idname = "node.tree_path_parent"
bl_label = "Parent Node Tree"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
space = context.space_data
# needs active node editor and a tree
return (space and (space.type == 'NODE_EDITOR') and len(space.path) > 1)
def execute(self, context):
space = context.space_data
space.path.pop()
return {'FINISHED'}
classes = (
NodeSetting,
NODE_OT_add_and_link_node,
NODE_OT_add_node,
NODE_OT_add_search,
NODE_OT_collapse_hide_unused_toggle,
NODE_OT_tree_path_parent,
)