Anyone know of a way to detect if a modal operator is running? Blender definitely has some sort of internal mechanism for it, because if there is a pending autosave it will be postponed until the modal operator is finished or cancelled. I have similar needs, where I have a process running on depsgraph_update_post that needs to be postponed until there are no modal operators running.
Note that I am not talking about context.active_operator. I’m looking to know if a modal operator is currently running, not the last one that was completed.
The internal handler code is fragile. Exposing this to rna could be a potential nightmare.
This uses ctypes to see if there are any modal operators running. Handles are stored per window, so to check everywhere you’d need to loop over context.window_manager.windows. Tested on 2.83, might be wrong offsets for earlier versions. To find the actual operator is probably possible, but would add to the boilerplate. This, even though just for finding a handle is stretching it.
Edit: Fixed
Edit3: Updated for Blender 3.5.1 (April 2023)
import bpy
from ctypes import *
# Handler type enum. Operator is 3
WM_HANDLER_TYPE_GIZMO = 1
WM_HANDLER_TYPE_UI = 2
WM_HANDLER_TYPE_OP = 3
WM_HANDLER_TYPE_DROPBOX = 4
WM_HANDLER_TYPE_KEYMAP = 5
version = bpy.app.version
functype = type(lambda: None)
as_ptr = bpy.types.bpy_struct.as_pointer
class ListBase(Structure):
_cache = {}
_fields_ = (("first", c_void_p), ("last", c_void_p))
def __new__(cls, c_type=None):
if c_type in cls._cache: return cls._cache[c_type]
elif c_type is None: ListBase_ = cls
else:
class ListBase_(Structure):
__name__ = __qualname__ = f"ListBase{cls.__qualname__}"
_fields_ = (("first", POINTER(c_type)),
("last", POINTER(c_type)))
__iter__ = cls.__iter__
__bool__ = cls.__bool__
return cls._cache.setdefault(c_type, ListBase_)
def __iter__(self):
links_p = []
elem_n = self.first or self.last
elem_p = elem_n and elem_n.contents.prev
if elem_p:
while elem_p:
links_p.append(elem_p.contents)
elem_p = elem_p.contents.prev
yield from reversed(links_p)
while elem_n:
yield elem_n.contents
elem_n = elem_n.contents.next
def __bool__(self):
return bool(self.first or self.last)
class StructBase(Structure):
_structs = []
__annotations__ = {}
def __init_subclass__(cls):
cls._structs.append(cls)
def __new__(cls, srna=None):
if srna is None:
return super().__new__(cls)
try:
return cls.from_address(as_ptr(srna))
except AttributeError:
raise Exception("Not a StructRNA instance")
# Required
def __init__(self, *_):
pass
# source\blender\windowmanager\wm_event_system.h
class wmEventHandler(StructBase):
next: lambda: POINTER(wmEventHandler)
prev: lambda: POINTER(wmEventHandler)
type: c_int
flag: c_char
poll: c_void_p
# source\blender\makesdna\DNA_windowmanager_types.h
class wmWindow(StructBase):
next: lambda: POINTER(wmWindow)
prev: lambda: POINTER(wmWindow)
ghostwin: c_void_p
gpuctx: c_void_p
parent: lambda: POINTER(wmWindow)
scene: c_void_p
new_scene: c_void_p
view_layer_name: c_char * 64
if version >= (3, 3):
unpinned_scene: c_void_p # Scene
workspace_hook: c_void_p
global_areas: ListBase * 3 # ScrAreaMap
screen: c_void_p # bScreen # (deprecated)
if version > (2, 92):
winid: c_int
pos: c_short * 2
size: c_short * 2
windowstate: c_char
active: c_char
if version < (3, 0):
_pad0: c_char * 4
cursor: c_short
lastcursor: c_short
modalcursor: c_short
grabcursor: c_short
if version >= (3, 5, 0):
pie_event_type_lock: c_short
pie_event_type_last: c_short
addmousemove: c_char
tag_cursor_refresh: c_char
if version <= (2, 93):
winid: c_int
if version > (2, 93):
event_queue_check_click: c_char
event_queue_check_drag: c_char
event_queue_check_drag_handled: c_char
if version < (3, 5, 0):
_pad0: c_char * 1
else:
event_queue_consecutive_gesture_type: c_char
event_queue_consecutive_gesture_xy: c_int * 2
event_queue_consecutive_gesture_data: c_void_p # wmEvent_ConsecutiveData
if version < (3, 5, 0):
pie_event_type_lock: c_short
pie_event_type_last: c_short
eventstate: c_void_p
if version > (3, 1):
event_last_handled: c_void_p
else:
tweak: c_void_p
ime_data: c_void_p # wmIMEData
event_queue: ListBase
handlers: ListBase(wmEventHandler)
modalhandlers: ListBase(wmEventHandler)
gesture: ListBase
stereo3d_format: c_void_p
drawcalls: ListBase
cursor_keymap_status: c_void_p
# source\blender\windowmanager\wm_event_system.h
class wmEventHandler_Op(StructBase):
class context(StructBase): # Anonymous
win: lambda: POINTER(wmWindow)
area: c_void_p # ScrArea ptr
region: c_void_p # ARegion ptr
region_type: c_short
head: wmEventHandler
op: c_void_p # wmOperator
is_file_select: c_bool
context: context
del context
def init_structs():
for struct in StructBase._structs:
fields = []
anons = []
for key, value in struct.__annotations__.items():
if isinstance(value, functype):
value = value()
elif isinstance(value, Union):
anons.append(key)
fields.append((key, value))
if anons:
struct._anonynous_ = anons
# Base classes might not have _fields_. Don't set anything.
if fields:
struct._fields_ = fields
struct.__annotations__.clear()
StructBase._structs.clear()
ListBase._cache.clear()
if __name__ == "__main__":
# Important. This sets up the struct fields.
init_structs()
win = wmWindow(bpy.context.window)
for handle in win.modalhandlers:
if handle.type == WM_HANDLER_TYPE_OP:
print("Modal running")
break
else:
print("No running modals")
Using this ctypes solution from iceythe has worked really well for me up through 3.2.2, then something must’ve changed in 3.3 that broke it (maybe the offsets have changed in the newer versions? I don’t understand this method well enough to give any real insight.) This patch gfxcoder has put forward will fix the problem, but I’d like to maintain compatibility with versions that don’t have “is_operator_modal” included in the python api.
Does anyone know of a fix to get this working again in 3.3? Any help is much appreciated!
This is spectacular! I’ve run into an issue when trying to pack this solution into my addon, though. Maybe you’ll understand better than I what’s going on.
All code for the Ctype, and struct initialization is in its original place (RunningModal.py)
I’ve moved the checkModal() fcn to another file that imports RunningModal.py
This move was done because I need to perform an operation when there are no running modals with locking behavior. Here’s a look at the modified fcn:
def checkModal(scene, post):
win = RunningModal.wmWindow(bpy.context.window)
global startTime, clear
for handle in win.modalhandlers:
print(str(handle.type)+" | "+str(RunningModal.WM_HANDLER_TYPE_OP))
if handle.type == RunningModal.WM_HANDLER_TYPE_OP:
print("Modal running")
if time.time() - startTime >.1:
clear = 1
break
else:
print(" "+str(handle.type)+" | "+str(RunningModal.WM_HANDLER_TYPE_OP))
#print("No running modals")
if clear == 1: #Run Modals here, and make sure to set clear=0 immediately before running them to avoid infinte recursion
AutoInverse_Prefs.AutoInverse()
else:
pass
The print statements above are always returning handle.type = 5 and RunningModal.WM_HANDLER_TYPE_OP = 3. Any idea why that is? This works fine when I run everything in a single contained script.
UPDATE: I’ve solved this problem. This has the behavior I want:
def checkModal(scene, post):
for window in bpy.context.window_manager.windows:
win = RunningModal.wmWindow(window)
global startTime, clear
for handle in win.modalhandlers:
if handle.type == RunningModal.WM_HANDLER_TYPE_OP:
print("Modal running")
if time.time() - startTime >.1:
clear = 1
break
else:
print("No running modals")
if clear == 1: #Run Modals here, and make sure to set clear=0 immediately before running them to avoid infinte recursion
AutoInverse_Prefs.AutoInverse()
else:
pass
break
if clear == 1:
break
else:
clear = 0
Sorry to bump this again. It seems that in 3.5+ this ctypes solution breaks. I’m guessing a new field was added to DNA_windowmanager_types.h that changed the offsets, but I can’t seem to find what’s relevant in git blame (though I honestly don’t really know what to look for).
You’re a life saver, thank you very much! I appreciate the explanation as well. I think I know what to look for now when this likely happens in the future. thanks again!
Just to clarify on how it works - you rebuilt blender wmWindow structure from source\blender\makesdna\DNA_windowmanager_types.h in python using ctypes and because of that you can access more of it’s properties, such as modalhandlers?
PS Amazing work, I think this can be legitimately considered as a pure black magic.