2
$\begingroup$

I would like to create a dialog box with a slider in Blender that changes orthographic scale of the camera when the slider is changed. My first problem is to call a popup and the modal operator at the same time. Currently Blender crashes every time I run the code. The second problem is that LEFTMOUSE is not quite right, rather let go of the leftmouse. Is there a command here too?

import bpy

class my_text_Class(bpy.types.Operator):
    bl_idname = "dialog.box"
    bl_label = "To start marker detection, click: OK (no input) Start = 10"
    user_iput: bpy.props.FloatProperty(name = "Change Camera Size to:")
    # bl_options = {'REGISTER', 'INTERNAL'}

    def __init__(self):
        print("Start")

    def __del__(self):
        print("End")

    def execute(self, context):
        if self.user_iput == "0":
            marker_detection()
        else:
            try:
                bpy.context.object.data.ortho_scale = self.user_iput
            except ValueError:
                print("Wrong input! Only floats are allowed as input Parameter!!")
                bpy.ops.dialog.box("INVOKE_DEFAULT")
        return {'FINISHED'}

    def modal(self, context, event):
        if event.type == 'LEFTMOUSE':  # Apply
            bpy.context.object.data.ortho_scale = self.user_input
        elif event.type == 'RIGHTMOUSE':  # Confirm
            return {'FINISHED'}
        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        context.window_manager.modal_handler_add(self)
        {'RUNNING_MODAL'}
        return context.window_manager.invoke_props_dialog(self)


if __name__ == "__main__":
    bpy.utils.register_class(my_text_Class)
    bpy.ops.dialog.box('INVOKE_DEFAULT')
$\endgroup$
0

1 Answer 1

3
$\begingroup$

Full examples at the bottom! (There's one for the "restarting" workaround and one for the "suspending" workaround)

Edit-Note: Blender nowadays requires the properties to be declared as annotations. In earlier versions they were declared as variables. The code is now updated to work with 2.9x and higher. (Basically, instead of "myBool = bpy.props..." we now write "myBool : bpy.props...")

Lets start with the second part:

'LEFTMOUSE' is correct, as that is your key. To check wether or not it is pressed, test for event.value.

if event.type == 'LEFTMOUSE':    # the KEY is left mouse
    if event.value == 'PRESS':   # the ACTION is pressed
        pass
    if event.value == 'RELEASE': # the ACTION is released
        bpy.context.object.data.ortho_scale = self.user_input

Now to the real meat: your first problem!

About the Problem:

This is a known bug. Sadly it's not dokumented.

The popup does a modal execution inside itself. The problem arises from the popup and the normal modal execution fighting for the modal execution (on the same running instance). After one of them finishes, it closes the operator on the other one. The second one therefore will take a nosedive.

This problem was pointed out by domlysz on blenderartists. They fixed it in 2016 (see task T48196) but it somehow reappeared later on.

Solution:

The correct solution would be calling window_manager.modal_handler_remove and temporarily remove the operator. OR to repatch blender. However modal_handler_remove doesn't exist, and patching ... someday.

So, lets get to the work-arounds: First consider if you need a set number of Interface-Popups or an undefined number of them. If e.g. you want one thing, have a popup, then do another thing and be done, you might want to use a bpy.types.Macro. Macros are an undocumented feature of blender. They are basically operators, that allow you to chain other operators in a certain order of execution. If you do extrude-and-move and press escape, the (instantanious) extrusion is still there, just the movement was cancelled. You can read more at this question. Your workflow would be:

  1. Create a (modal) startup operator
  2. Create a popup operator
  3. ...
  4. Create a operator for final stuff
  5. Create a macro to run the operators above

A different workaround would be, to stop the modal operator and use a second operator for the popup: Finish the modal operator and start the popup-operator. The popups execute in turn restarts the modal operator.

Step-by-step:

  1. Do some modal interaction.
  2. Event "show some popup" happens:
  3. Start popup, push props to popup and close modal.
  4. Do some interface stuff, changing props.
  5. Start modal, push props to modal, close popup.
  6. Continue with your modal actions.

Notes:

  1. While domlysz called the popup-operator inside the __del__ function, it seems to work fine from within modal. Just be sure to return {'FINISHED'} or {'CANCELLED'} straight after calling the popup.
  2. If they exist, a modal operator will run __init__ before invoke and __del__ after finishing/cancelling.
  3. Cancelling either the popup, or the modal operator will both stop your interaction.

Here's an enhanced example. The mixin simplifys transfering the properties (usefull for larger sets of properties). The double-asterisk unrolls a dict into variable assignements.:

import bpy

class ModalPopup_Props: # mixin to use in the modal and the popup
    myBool   : bpy.props.BoolProperty()
    myInt    : bpy.props.IntProperty()
    myString : bpy.props.StringProperty()
    __ModalPopupProps = ["myBool", "myInt", "myString"]
    def _props_as_dict(self):
        return {prop: getattr(self, prop) for prop in self.__ModalPopupProps}

class ModalPopup_Popup(bpy.types.Operator, ModalPopup_Props):
    """This will be callable from the search"""
    bl_idname = "test.popup"
    bl_label = "ModalPopup Popup"
    
    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self)
    
    def execute(self, context):
        bpy.ops.test.modal('INVOKE_DEFAULT', **self._props_as_dict())
        return {'FINISHED'}

class ModalPopup_Modal(bpy.types.Operator, ModalPopup_Props):
    bl_idname = "test.modal"
    bl_label = "ModalPopup Modal"
    bl_options = {'INTERNAL'} # Internal removes this from the searchbar
    
    def execute(self, context):
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}

    def modal(self, context, event):
        # some dummy event. let's print the props.
        if event.type == 'LEFTMOUSE':
            if event.value == 'RELEASE':
                print(self._props_as_dict())

        # open the popup
        if event.type == 'SPACE':
            bpy.ops.test.popup('INVOKE_DEFAULT', **self._props_as_dict())
            return {'CANCELLED'} # or return {'FINISHED'}

        # cancel modal execution
        if event.type in {'ESC'}:
            return {'CANCELLED'}
        
        return {'PASS_THROUGH'} # (or use {'RUNNING_MODAL'} to block stuff)

def register():
    bpy.utils.register_class(ModalPopup_Popup)
    bpy.utils.register_class(ModalPopup_Modal)

def unregister():
    bpy.utils.unregister_class(ModalPopup_Modal)
    bpy.utils.unregister_class(ModalPopup_Popup)

if __name__ == "__main__":
    register()

Finally, one might think of suspending and updating the original modal operator. Indeed this is possible using WinMan (the window-manager) as storage. However not by storing the operator in WinMan (blender would crash). Instead we use WinMan as a dead-drop for our passed values. The modal operator listens in, clears the dead-drop and resumes work. Note that if we fail to provide data, we'll be stuck in suspension, and depending on our type of suspension, blender will freeze. So in the popup we need cancel to provide an empty dict. The modal operator will update (nothing) from the empty dict, clear out the dead-drop and resume its work. Note that in this case, cancelling the popup will not cancel the modal operator (you might have the same buttons for it though, doing it in series). Try that by escaping the popup with rightklick vs escape.

Full example:

import bpy

class ModalPopup_Props: # mixin to use in the modal and the popup
    myBool   : bpy.props.BoolProperty()
    myInt    : bpy.props.IntProperty()
    myString : bpy.props.StringProperty()
    __ModalPopupProps = ["myBool", "myInt", "myString"]
    def _props_as_dict(self):
        return {prop: getattr(self, prop) for prop in self.__ModalPopupProps}

class ModalPopup_Popup(bpy.types.Operator, ModalPopup_Props):
    """This will be callable from the search"""
    bl_idname = "test.popup"
    bl_label = "ModalPopup Popup"
    
    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self)

    # this is run on cancellation.
    def cancel(self, context):
        if 'ModalPopup' in context.window_manager: # there's a running instance
            # we MUST(!!) provide data to the dead-drop or we'll be stuck forever
            #    so we just give it an empty dict. The modal operator resumes
            context.window_manager['ModalPopup'] = {}

    def execute(self, context): # start the modal op
        if 'ModalPopup' in context.window_manager: # lets see if there's a running instance
            # we cannot get the running instance directly, so we'll dead-drop the data
            context.window_manager['ModalPopup'] = self._props_as_dict()
        else: # no running instance? start one.
            bpy.ops.test.modal('INVOKE_DEFAULT', **self._props_as_dict())
        return {'FINISHED'}

class ModalPopup_Modal(bpy.types.Operator, ModalPopup_Props):
    bl_idname = "test.modal"
    bl_label = "ModalPopup Modal"
    bl_options = {'INTERNAL'} # Internal removes this from the searchbar
    
    def execute(self, context):
        print("Modal operator has been started")
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}

    def modal(self, context, event):
        # Listening to the dead-drop. This MUST stay on top AND return
        # {'RUNNING_MODAL'} or {'PASS_THROUGH'} to "suspend" the operator.
        if 'ModalPopup' in context.window_manager:
            # get the dead-drop from our popup
            if context.window_manager['ModalPopup'] != "SUSPENDED":
                print("resuming Modal")
                for k,v in context.window_manager['ModalPopup'].items():
                    setattr(self, k, v) # read in our data
                del context.window_manager['ModalPopup'] # clear off the dead-drop
            else:
                print("Passing by, saying hi.")
            return {'RUNNING_MODAL'}
        
        # some dummy event. let's print the props.
        if event.type == 'LEFTMOUSE':
            if event.value == 'RELEASE':
                print(self._props_as_dict())

        # open the popup
        if event.type == 'SPACE':
            print("suspending Modal")
            context.window_manager['ModalPopup'] = "SUSPENDED" # setup deaddrop
            bpy.ops.test.popup('INVOKE_DEFAULT', **self._props_as_dict())
            return {'RUNNING_MODAL'}# otherwise space would call the searchmenue too.

        # cancel modal execution
        if event.type in {'ESC'}:
            return {'CANCELLED'}
        
        return {'PASS_THROUGH'} # (or use {'RUNNING_MODAL'} to block stuff)

def register():
    bpy.utils.register_class(ModalPopup_Popup)
    bpy.utils.register_class(ModalPopup_Modal)

def unregister():
    bpy.utils.unregister_class(ModalPopup_Modal)
    bpy.utils.unregister_class(ModalPopup_Popup)

if __name__ == "__main__":
    register()
$\endgroup$
2
  • $\begingroup$ 12/6/2022 getting an error in Blender 2.9.3 and 3.3.1 -> TypeError: Converting py args to operator properties: : keyword "myBool" unrecognized. This was calling the dialog at the end of the python app with: bpy.ops.test.popup('INVOKE_DEFAULT'). $\endgroup$
    – PPL
    Dec 6, 2022 at 21:19
  • $\begingroup$ @PPL This was still written for the old style of creating Blender-properties. Nowadays Blender requires properties not to be assigned, but to be declared as annotations. (simply replacing the = with : would be enough). Editing my answer to reflect that. Thanks for the notice. $\endgroup$
    – Teck-freak
    Dec 8, 2022 at 9:39

You must log in to answer this question.

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