2
$\begingroup$

I'm trying to make a configuration dialog, which prompts the user for some options and can then be committed or cancelled entirely. The dialog is opened from a node. I would like to copy the existing configuration to be edited in the dialog Operator, and then copy it back when the Operator is done, or not in case it is cancelled.

Something like this:

class Configuration(bpy.types.PropertyGroup):
    configuration_option = bpy.props.StringProperty()
    ... more items

class ConfigurationOperator(bpy.types.Operator):
    ...
    bl_idname = "test.configure"
    conf_to_edit = bpy.props.PointerProperty(type=Configuration)
    def invoke(self, context, event):
        # Copy context.node.conf -> conf_to_edit
        # Store node reference somehow
        return context.window_manager.invoke_props_dialog(self)
    def execute(self, context):
        # Copy conf_to_edit -> node_ref.conf
        return {'FINISHED'}

class ExampleNode(bpy.types.Node):
    ...
    conf = bpy.props.PointerProperty(type=Configuration)
    def draw_buttons(self, context, layout):
        layout.operator("test.configure", "Configure Node")

Is there any cleaner way to do this than copying the contents of the PropertyGroup manually (or with some kind of hacky iteration)? I don't intend to nest PropertyGroups.

$\endgroup$

1 Answer 1

3
$\begingroup$

Example using custom node template.

enter image description here

Test code, put together with the custom nodes template for 2.8. Recommend that scripting now in 2.8 will save time going forward

Using Set up methods in the property group have a method to draw, and set and get from another object that shares the same pointer.

The draw method simply iterates and draws a basic prop layout for each property in way of example. I've added a list of chosen props. Begs for a nicely formatted method.

The operator requires context.node this has been added to the context at where the operator is displayed in custom node UI, the context member is set to the node, which is self in the node code. For the N panel draw, the context active node is used instead.

    def draw_buttons(self, context, layout):
        box = layout.box()
        box.context_pointer_set("node", self)
        op = box.operator("node.configure")  # XXX

The config properties are drawn using. box is any layout element defined, could be self.layout or self.layout.column() etc.

        self.config.draw(box, context)

Note could flag to use self.node instead of self above would display the node values directly. Contemplating using this but inverting the operator to revert if cancelled, and make props live so one can dynamically dick around and only save when happy.

The node values are loaded on invoke using CustomNodeConfig.getfrom and set on execute with CustomNodeConfig.setfrom

Note It appears the config pointer will not register on the operator if it has an ID type pointer as a member.

from nodeitems_utils import NodeCategory, NodeItem
import nodeitems_utils
import bpy
from bpy.types import NodeTree, Node, NodeSocket


class CustomNodeConfig(bpy.types.PropertyGroup):
    __props = ["x", "my_enum_prop"]
    x: bpy.props.FloatProperty(name="X factor")
    my_items = (
        ('DOWN', "Down", "Where your feet are"),
        ('UP', "Up", "Where your head should be"),
        ('LEFT', "Left", "Not right"),
        ('RIGHT', "Right", "Not left"),
    )

    my_enum_prop: bpy.props.EnumProperty(
        name="Direction",
        description="Just an example",
        items=my_items,
        default='UP',
    )

    def getfrom(self, node):
        for p in node.__props:
            setattr(self, p, getattr(node, p))

    def setfrom(self, node):
        for p in self.__props:
            setattr(node, p, getattr(self, p))

    def draw(self, layout, context):
        layout.label(text="Settings...")
        for p in self.__props:
            layout.prop(self, p)


class NODE_OT_configure(bpy.types.Operator):
    bl_label = "Configure"
    bl_idname = "node.configure"
    node = None
    config: bpy.props.PointerProperty(type=CustomNodeConfig)

    def invoke(self, context, event):
        self.node = context.node
        if self.node:
            self.config.getfrom(self.node.config)
        return context.window_manager.invoke_props_dialog(self)

    def execute(self, context):

        if self.node:

            self.config.setfrom(self.node.config)
        # Copy config -> node_ref.conf
        return {'FINISHED'}

    def draw(self, context):
        if self.node:
            self.layout.label(text="%s" % (repr(self.node.id_data)))
            self.layout.label(text="%s" % (self.node.name))
        self.config.draw(self.layout, context)


class MyCustomTree(NodeTree):
    '''A custom node tree type that will show up in the node editor header'''
    bl_idname = 'CustomTreeType'
    bl_label = "Custom Node Tree"
    bl_icon = 'NODETREE'


class MyCustomTreeNode:
    @classmethod
    def poll(cls, ntree):
        return ntree.bl_idname == 'CustomTreeType'


class MyCustomNode(Node, MyCustomTreeNode):
    '''A custom node'''
    bl_idname = 'CustomNodeType'
    bl_label = "Custom Node"
    bl_icon = 'SOUND'

    my_string_prop: bpy.props.StringProperty()
    my_float_prop: bpy.props.FloatProperty(default=3.1415926)
    config: bpy.props.PointerProperty(type=CustomNodeConfig)

    def init(self, context):
        self.inputs.new('NodeSocketFloat', "World")
        self.outputs.new('NodeSocketFloat', "How")

    def copy(self, node):
        print("Copying from node ", node)

    def free(self):
        print("Removing node ", self, ", Goodbye!")

    def draw_buttons(self, context, layout):
        box = layout.box()
        box.context_pointer_set("node", self)
        op = box.operator("node.configure")  # XXX
        self.config.draw(box, context)
        layout.label(text="Node settings")
        layout.prop(self, "my_float_prop")

    def draw_buttons_ext(self, context, layout):
        box = layout.box()
        box.context_pointer_set("node", context.active_node)
        op = box.operator("node.configure")
        self.config.draw(box, context)
        layout.prop(self, "my_float_prop")
        layout.prop(self, "my_string_prop")

    def draw_label(self):
        return "I am a custom node"


class MyNodeCategory(NodeCategory):
    @classmethod
    def poll(cls, context):
        return context.space_data.tree_type == 'CustomTreeType'


# all categories in a list
node_categories = [
    # identifier, label, items list
    MyNodeCategory(
        'SOMENODES',
        "Some Nodes",
        items=[
            # our basic node
            NodeItem("CustomNodeType"),
        ]),
]

classes = (
    CustomNodeConfig,
    NODE_OT_configure,
    MyCustomTree,
    MyCustomNode,
)


def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)

    nodeitems_utils.register_node_categories('CUSTOM_NODES', node_categories)


def unregister():
    nodeitems_utils.unregister_node_categories('CUSTOM_NODES')

    from bpy.utils import unregister_class
    for cls in reversed(classes):
        unregister_class(cls)


if __name__ == "__main__":
    register()
$\endgroup$
4
  • $\begingroup$ Uhhh, yes. The custom node tree even appears in the Editor Type dropdown. Great example. $\endgroup$
    – p2or
    Mar 30, 2019 at 18:31
  • $\begingroup$ Cheers. Coincidentally after writing blender.stackexchange.com/a/135614/15543 wanted to list the possible values of Area,ui_type only way I found is to deliberately get it wrong and read error. Maybe a question I subbose $\endgroup$
    – batFINGER
    Mar 30, 2019 at 18:50
  • $\begingroup$ Also can't find any better way. You can get all areas via rna: print ([e.identifier for e in bpy.context.area.bl_rna.properties["type"].enum_items]). However, I'd expect getting a complete list... $\endgroup$
    – p2or
    Mar 30, 2019 at 20:24
  • $\begingroup$ Thanks. I was hoping there'd be a cleaner solution (this falls under "hacky iteration") but I guess this is the way to go. context_pointer_set could be very useful. $\endgroup$
    – ollpu
    Mar 30, 2019 at 20:43

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .