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:
- Create a (modal) startup operator
- Create a popup operator
- ...
- Create a operator for final stuff
- 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:
- Do some modal interaction.
- Event "show some popup" happens:
- Start popup, push props to popup and close modal.
- Do some interface stuff, changing props.
- Start modal, push props to modal, close popup.
- Continue with your modal actions.
Notes:
- 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.
- If they exist, a modal operator will run
__init__
before invoke
and __del__
after finishing/cancelling.
- 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()