3
$\begingroup$

How do you add a listener to a button? I need to handle something in the panel once a button is clicked but I don't know how to add this click event listener.

import bpy

class SimplePanel(bpy.types.Panel):

    def on_button_click(self, context):
        print("button clicked")

    def draw(self, context):
        layout = self.layout
        row = layout.row()
        op = row.operator("object.simple_operator")
        op.add_click_listener(on_button_click) #FIXME

If I add a callback setter function in my operator like

def set_exec_callback(self, callback):
   self.exec_callback = callback

and then do:

op.set_exec_callback(on_click)

It mistakenly tells me that

AttributeError: 'OBJECT_OT_my_operator' object has no attribute 'set_exec_callback'

even though it is clearly defined in my operator

Here's a script that reproduces the bug:

import bpy

class SimpleOperator(bpy.types.Operator):
    bl_idname = "object.simple_operator"
    bl_label = "Simple Operator"

    def __init__(self):
        self.callback = None

    def set_callback(self, callback):
        self.callback = callback

    def execute(self, context):
        self.callback()
        return {'FINISHED'}

def my_callback():
    print("Callback function called")


class TestPanel(bpy.types.Panel):
    bl_label = "Test Panel"
    bl_idname = "PT_TestPanel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Test'

    def draw(self, context):
        layout = self.layout
        op = layout.operator(SimpleOperator.bl_idname, text="Execute Simple Operator")
        op.set_callback(my_callback) # AttributeError: 'OBJECT_OT_simple_operator' object has no attribute 'set_callback'

def register():
    bpy.utils.register_class(SimpleOperator)
    bpy.utils.register_class(TestPanel)

def unregister():
    bpy.utils.unregister_class(SimpleOperator)
    bpy.utils.unregister_class(TestPanel)

# Test the panel
if __name__ == "__main__":
    register()
$\endgroup$

2 Answers 2

3
$\begingroup$

The API abstracts some stuff from what you actually code in python, so the actual operator construct that is executed is not the class you define in python. You need to serialize your properties using Blender's serialization system if you want it to work. Unfortunately I think in your case since you can't serialize a function, that means storing your callback as a string and exec'ing it.

That opens up a whole can of worms of vulnerabilities though, so make sure you know what you're doing if you use this solution. I'm sure there is other solutions to your problem, like storing a variable on a data block like you did in your answer.

import bpy

class SimpleOperator(bpy.types.Operator):
    bl_idname = "object.simple_operator"
    bl_label = "Simple Operator"
    # We set the option so it resets to default on each operator call
    callback: bpy.props.StringProperty(default="", options={"SKIP_SAVE"})

    def execute(self, context):
        if self.callback:
            exec(self.callback)        
        return {'FINISHED'}

def my_callback():
    print("Callback function called")


class TestPanel(bpy.types.Panel):
    bl_label = "Test Panel"
    bl_idname = "PT_TestPanel"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Test'

    def draw(self, context):
        layout = self.layout
        op = layout.operator(SimpleOperator.bl_idname, text="Execute Simple Operator")
        op.callback = "my_callback()"

def register():
    bpy.utils.register_class(SimpleOperator)
    bpy.utils.register_class(TestPanel)

def unregister():
    bpy.utils.unregister_class(SimpleOperator)
    bpy.utils.unregister_class(TestPanel)

# Test the panel
if __name__ == "__main__":
    register()
$\endgroup$
3
  • 1
    $\begingroup$ Hi @Gorgious thank you for your response! I shall try this one. But you mentioned it opens a can of worms? haha. So what do you think about my solution with bpy.types.Scene.on_button_click = on_button_click? $\endgroup$
    – Megan Love
    Commented Mar 22 at 14:47
  • 1
    $\begingroup$ exec is an infamous bad code smell, it can be used to inject malicious code and it makes debugging a nightmare. If used properly there really isn't any risk but it should be avoided when possible. I think your solution is the most sane and blender-y. You can prevent namespace pollution by providing a custom name eg my_addon_on_button_click or by using a PropertyGroup to hold your custom props $\endgroup$
    – Gorgious
    Commented Mar 22 at 16:16
  • $\begingroup$ thank you for this info! :) $\endgroup$
    – Megan Love
    Commented Mar 22 at 22:57
2
$\begingroup$

I found a hacky way to solve my problem but I'm sure that's not the right answer as it causes namespace pollution and is sort of spaghetti coding. I just define the following in my panel file:

bpy.types.Scene.on_button_click = on_button_click

and then call context.scene.on_button_click(context) in the operator class file's execute function.

Crappy solution but it does what I need it to do.

$\endgroup$

You must log in to answer this question.

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