2
$\begingroup$

I want to make sure that the user acknowledges the selection of a checkbox. What am I missing?

import bpy
from bpy.types import PropertyGroup

def my_boolean_handler(self, context):
    #context.window_manager.invoke_popup(self) # error self should be an operator
    bpy.ops.my_category.custom_confirm_dialog() # prints Info: YES! because executed not invoked

class MySettings(PropertyGroup):
    my_boolean : bpy.props.BoolProperty(
        name = "My Boolean",
        description = "This checkbox triggers a Dialog Box",
        default = False,
        update = my_boolean_handler
    )

class SimpleConfirmOperator(bpy.types.Operator):
    """Really?"""
    bl_idname = "my_category.custom_confirm_dialog"
    bl_label = "Do you really want to do that?"
    bl_options = {'REGISTER', 'INTERNAL'}

    @classmethod
    def poll(cls, context):
        return True

    def execute(self, context):
        self.report({'INFO'}, "YES!")
        return {'FINISHED'}

    def invoke(self, context, event):
        return context.window_manager.invoke_confirm(self, event)
    

class OBJECT_PT_CustomPanel(bpy.types.Panel):
    bl_label = "My Panel"
    bl_idname = "OBJECT_PT_custom_panel"
    bl_space_type = "VIEW_3D"   
    bl_region_type = "UI"
    bl_category = "Tools"
    bl_context = "objectmode"

    def draw(self, context):
        settings = context.scene.world.my_settings
        layout = self.layout
        layout.prop(settings, 'my_boolean')

def register():
    bpy.utils.register_class(OBJECT_PT_CustomPanel)
    bpy.utils.register_class(SimpleConfirmOperator)
    bpy.utils.register_class(MySettings)
    bpy.types.World.my_settings = bpy.props.PointerProperty(type=MySettings)
    

def unregister():
    del bpy.types.World.my_settings
    bpy.utils.unregister_class(SimpleConfirmOperator)
    bpy.utils.unregister_class(OBJECT_PT_CustomPanel)

if __name__ == "__main__":
    register()

I saw 2 StackExchange questions pointing in the right direction, but couldn't get it working with the checkbox.

$\endgroup$

2 Answers 2

1
$\begingroup$

The simple answer is in the first Q&A you linked to. Simply modify the 6th line to

bpy.ops.my_category.custom_confirm_dialog("INVOKE_DEFAULT")

However AFAIK calling operators in property callbacks methods is unsupported. It means that it works but it shouldn't. You can see the problem here, the confirmation dialog does get displayed, but since Blender registered that you clicked the mouse on the property, when you release the mouse button it immediately validates the confirmation dialog, making it almost useless.

enter image description here

I would advise using another method, which gets rid of the callback method and uses the operator directly to modify the property value.

You need to set the layout.operator_context beforehand though, so the operator knows to display the confirmation dialog.

import bpy
from bpy.types import PropertyGroup


class MySettings(PropertyGroup):
    my_boolean : bpy.props.BoolProperty(
        name = "My Boolean",
        description = "This checkbox triggers a Dialog Box",
        default = False,
    )

class SimpleConfirmOperator(bpy.types.Operator):
    """Really?"""
    bl_idname = "my_category.custom_confirm_dialog"
    bl_label = "Do you really want to do that?"
    bl_options = {'REGISTER', 'INTERNAL'}

    @classmethod
    def poll(cls, context):
        return True

    def execute(self, context):
        context.scene.world.my_settings.my_boolean = not context.scene.world.my_settings.my_boolean
        [a.tag_redraw() for a in context.screen.areas]
        return {'FINISHED'}

    def invoke(self, context, event):
        return context.window_manager.invoke_confirm(self, event)
    

class OBJECT_PT_CustomPanel(bpy.types.Panel):
    bl_label = "My Panel"
    bl_idname = "OBJECT_PT_custom_panel"
    bl_space_type = "VIEW_3D"   
    bl_region_type = "UI"
    bl_category = "Tools"
    bl_context = "objectmode"

    def draw(self, context):
        settings = context.scene.world.my_settings
        layout = self.layout
        layout.prop(settings, 'my_boolean')
        layout.operator_context = "INVOKE_DEFAULT"
        layout.operator("my_category.custom_confirm_dialog", text="Toggle My Boolean")

def register():
    bpy.utils.register_class(OBJECT_PT_CustomPanel)
    bpy.utils.register_class(SimpleConfirmOperator)
    bpy.utils.register_class(MySettings)
    bpy.types.World.my_settings = bpy.props.PointerProperty(type=MySettings)
    

def unregister():
    del bpy.types.World.my_settings
    bpy.utils.unregister_class(SimpleConfirmOperator)
    bpy.utils.unregister_class(OBJECT_PT_CustomPanel)

if __name__ == "__main__":
    register()

enter image description here

You'll notice the interface doesn't get updated when you confirm the dialog box. You can force a redraw with [a.tag_redraw() for a in context.screen.areas].

$\endgroup$
5
  • $\begingroup$ So this approach means that I must have an operator (e.g. a submit button at the end of the form) called, because I cannot call it from the boolean directly, right? $\endgroup$
    – karlisup
    Commented May 31, 2022 at 6:23
  • $\begingroup$ I guess so, yes. But you can disguise the operator button with depress argument and change the text argument for instance. See docs.blender.org/api/current/… $\endgroup$
    – Gorgious
    Commented May 31, 2022 at 6:27
  • $\begingroup$ Based on your answer I created an edit to the question showing how would it look like if depress is set to True and emboss to False and the operator has been put in line with the checkbox. $\endgroup$
    – karlisup
    Commented May 31, 2022 at 11:14
  • 1
    $\begingroup$ @karlisup hey I'm glad you made it work ! It looks good :) However would you mind adding it as an answer rather than an edit to your question ? On this site the first post is reserved for the question, and then the other posts are the answers to this question. It will help other people with the same problem in the future. Cheers $\endgroup$
    – Gorgious
    Commented May 31, 2022 at 12:03
  • $\begingroup$ I did the same thing with modal operator, to draw some shape if checkbox on, and removing drawing if off. ("INVOKE_DEFAULT") in update function for bool property is only thing that works for me. I've noticed if you drag-release panel (where pin/unpin icon), it removes unrelease mouse state until new scene. $\endgroup$
    – APEC
    Commented Feb 12, 2023 at 19:37
1
$\begingroup$

Here's what I was able to do based on Gorgious answer: boolean as a button

It looks like a boolean but is not. It is a checkbox with an operator that is hidden under the label. For my liking solution is too hacky and it seems that it is not intended for what I want it to be.

Here is the source code:

import bpy
from bpy.types import PropertyGroup

def boop(self, context):
    context.scene.world.my_settings.my_boolean = not context.scene.world.my_settings.my_boolean

class MySettings(PropertyGroup):
    my_boolean : bpy.props.BoolProperty(
        name = "",
        description = "This checkbox triggers a Dialog Box",
        default = False,
    )


class SimpleConfirmOperator(bpy.types.Operator):
    """Really?"""
    bl_idname = "my_category.custom_confirm_dialog"
    bl_label = "Do you really want to do that?"
    bl_options = {'REGISTER', 'INTERNAL'}

    @classmethod
    def poll(cls, context):
        return True

    def execute(self, context):
        context.scene.world.my_settings.my_boolean = not context.scene.world.my_settings.my_boolean
        [a.tag_redraw() for a in context.screen.areas]
        return {'FINISHED'}

    def invoke(self, context, event):
        return context.window_manager.invoke_confirm(self, event)
    

class OBJECT_PT_CustomPanel(bpy.types.Panel):
    bl_label = "My Panel"
    bl_idname = "OBJECT_PT_custom_panel"
    bl_space_type = "VIEW_3D"   
    bl_region_type = "UI"
    bl_category = "Tools"
    bl_context = "objectmode"

    def draw(self, context):
        settings = context.scene.world.my_settings
        layout = self.layout
        row = layout.row(align=True)
        row.prop(settings, 'my_boolean')
        row.operator_context = "INVOKE_DEFAULT"
        row.operator("my_category.custom_confirm_dialog", text="Toggle My Boolean                     ", depress=True, emboss=False)

def register():
    bpy.utils.register_class(OBJECT_PT_CustomPanel)
    bpy.utils.register_class(SimpleConfirmOperator)
    bpy.utils.register_class(MySettings)
    bpy.types.World.my_settings = bpy.props.PointerProperty(type=MySettings)
    

def unregister():
    del bpy.types.World.my_settings
    bpy.utils.unregister_class(SimpleConfirmOperator)
    bpy.utils.unregister_class(OBJECT_PT_CustomPanel)

if __name__ == "__main__":
    register()
$\endgroup$
1
  • $\begingroup$ To prevent the user from being able to click on the .prop()'s checkbox graphic, you can add several leading spaces to the operator button's text, then "rewind" the layout cursor by that much space before calling .operator(). The leading spaces will both contribute to the button's click bounds and logically occlude the checkbox graphic. So, if the user clicks on the checkbox graphic, the operator button will receive the mouse click event instead (and show your popup). $\endgroup$ Commented Oct 14, 2023 at 1:30

You must log in to answer this question.

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