Yes, Python's coroutines can be used make the operators callbacks interact with a generator.
Here's an example of a generic re-usable operator mix-in class, and an example usage.
See modal_iter
function for an example of how (invoke
, execute
, modal
, cancel
) can be handled from within a single generator.
import bpy
from bpy.types import Operator
# -----------------------------
# Generic Modal Iterator Mix-In
class ModalIterOperator:
""" Defines Operator callbacks (invoke, execute, modal, cancel).
Sub-classes only need to define `modal_iter` generator.
"""
def invoke(self, context, event):
self._iter = iter(self.modal_iter(context))
self._iter.send(None)
result = self._iter.send(event)
if 'RUNNING_MODAL' in result:
wm = context.window_manager
wm.modal_handler_add(self)
return result
def execute(self, context):
return self.invoke(context, None)
def modal(self, context, event):
return self._iter.send(event)
def cancel(self, context):
self._iter.send(None)
# ----------------------------------
# Example of ModalIterOperator Usage
class MyIterTest(Operator, ModalIterOperator):
bl_idname = "wm.my_modal_iter_operator"
bl_label = "My Modal Iter Operator"
# -------------------------------------------
# Entire modal operator in a single function!
def modal_iter(self, context):
event = yield
if event is not None:
# -------------------------------------
# Section typically handled by invoke()
print("Operator.invoke(..., event=%r)" % event.type)
# Check we would typically do inside invoke()
# to see if we want to exit, or continue and run modal()
if event.type == 'ESC':
yield {'CANCELLED'}
return
else:
# --------------------------------------
# Section typically handled by execute()
print("Operator.execute()")
# We may want to cancel the operator here too.
if 0:
yield {'CANCELLED'}
return
# ------------------------------------
# Section typically handled by modal()
event = yield {'RUNNING_MODAL'}
while event is not None:
print("Operator.modal(..., event=%r)" % event.type)
if event.type in {'RIGHTMOUSE', 'ESC'}:
yield {'CANCELLED'}
return
# ------------------------------------
# Main modal operator logic goes here!
# ------------------------------------
event = yield {'RUNNING_MODAL'}
# cancel may be initiated externally
if event is None:
# -------------------------------------
# Section typically handled by cancel()
print("Operator.cancel()")
yield # --> None, since we're closed externally
return
yield {'FINISHED'}
def register():
bpy.utils.register_class(MyIterTest)
def unregister():
bpy.utils.unregister_class(MyIterTest)
if __name__ == "__main__":
register()
# test call
bpy.ops.wm.my_modal_iter_operator('INVOKE_DEFAULT')
Notes:
- None of the
return
's in modal_iter
are needed, since the way the generator is used these points will never be passed anyway. I've added them in so as to be clear whats happening and so any accidental iteration past yield {'CANCELLED'}
will raise a StopIteration
exception, which intentionally isn't handled.
- This code example may seem overly verbose, this is done for completeness.
In practice you may not need all of the callbacks (cancel or execute for eg).
- This article was used in writing the example above.