Detect if modal operator is running

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.

Any help is appreciated!

1 Like

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 :stuck_out_tongue:
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")
12 Likes

Very clever! I wasn’t expecting it to be simple, I’m just glad there’s a way. Thanks!

This is a tricky one to understand. Any chance you could offer some help on how to only detect modal grab?

Cheers!

I dont think there’s a easy way to detect if an operator is active without accessing blenders process memory.

But maybe its enough to detect if an object has been moved?

That’s a really good thought. I’m already detecting movement. Not sure why it didn’t occur to me.

Thanks!

For future add-on devs who stumble on this old thread, I just submitted a patch that addresses this issue without needing to use ctypes.

https://developer.blender.org/D15546

6 Likes

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!

According to git blame a new field was added wmWindow back in july.
I’ve updated the script - it should support up to Blender 3.3.

3 Likes

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
1 Like

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).

1 Like

Do you know where the status of this “is_operator_modal” patch can be viewed now that the linked site has been archived?

My patch would need to be resubmitted to the new system. It’s not a large change, so it shouldn’t take too long. I’ll add it to my to-do list.

3 Likes

Usually when there’s a breakage I look for the version that broke and download the source from https://download.blender.org/source/. It’s about 50mb.

Then I just check what’s different in the struct definitions.

The changes in 3.5.0 added new event gesture data and moved some old fields around, so no wonder it broke.

Anyway, the original script is updated to support 3.5.1.

3 Likes

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!

1 Like

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.