0
$\begingroup$

I want to start a modal operator by clicking a button in a panel in the properties region. It should then stop when I click somewhere in the 3D viewport. I want to execute some code when I start and finish the operator (see the code below).

I tried modifiyng the modal operator template with an execute method to start the modal operator. But the now the context.space_data.type is always "PROPERTIES" because that's where the panel is located, I guess. Is this a bad way of doing what I want?

class MY_OPERATOR_OT_click_on_3dview(bpy.types.Operator):
    
    bl_idname = "mesh.click_on_3dview"
    bl_label = "Click on 3D"

    def modal(self, context, event):
        
        if context.space_data.type != 'VIEW_3D':
            return {'PASS_THROUGH'}
        
        if event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
            # allow navigation
            return {'PASS_THROUGH'}
        elif event.type == 'LEFTMOUSE':
            # DO ALL THE POST-SELECT STUFF
            return {'FINISHED'}
        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            return {'CANCELLED'}

        return {'RUNNING_MODAL'}

    def execute(self, context):
        # DO ALL THE SETUP STUFF  
        context.window_manager.modal_handler_add(self)     
        return {'RUNNING_MODAL'}  
    
    def cancel(self, context):
        print("CANCEL???")
$\endgroup$

1 Answer 1

2
$\begingroup$

Use the screen.

Edited Code from https://blender.stackexchange.com/a/101894/15543

Creates a modal timer operator which is invoked from the script. (Invoke is equiv of pressing button.)

While the modal timer below is running removes right click support from all areas that are not text editors nor 3d views.

Instead of using context.space_data find the space from its coordinates in the screens areas collection.

import bpy

class ModalTimerOperator(bpy.types.Operator):
    """Operator which runs its self from a timer"""
    bl_idname = "wm.modal_timer_operator"
    bl_label = "Modal Timer Operator"

    _timer = None

    def modal(self, context, event):
        if event.type in {'RIGHTMOUSE'}:
            screen = context.screen
            x, y = event.mouse_x, event.mouse_y

            areas = [a for a in screen.areas if a.x < x < a.x + a.width
                    and a.y < y < a.y + a.height]
            
            if areas and areas[0].type not in {'VIEW_3D', 'TEXT_EDITOR'}:
                print("Right Click Taken Out")
                return {'RUNNING_MODAL'}

        if event.type in {'ESC'}:
            self.cancel(context)
            return {'CANCELLED'}

        if event.type == 'TIMER':
            # change theme color, silly!
            pass
        return {'PASS_THROUGH'}

    def execute(self, context):
        wm = context.window_manager
        self._timer = wm.event_timer_add(0.1, window=context.window)
        wm.modal_handler_add(self)
        return {'RUNNING_MODAL'}

    def cancel(self, context):
        wm = context.window_manager
        wm.event_timer_remove(self._timer)

def register():
    bpy.utils.register_class(ModalTimerOperator)

def unregister():
    bpy.utils.unregister_class(ModalTimerOperator)

if __name__ == "__main__":
    register()
    # test call
    bpy.ops.wm.modal_timer_operator('INVOKE_DEFAULT')

The above is from modal timer template, will work same as a modal.

def execute(self, context):
    wm = context.window_manager
    #self._timer = wm.event_timer_add(0.1, window=context.window)
    #wm.modal_handler_add(self)
    wm.modal_handler_add(self)
    return {'RUNNING_MODAL'}

def cancel(self, context):
    wm = context.window_manager
    #wm.event_timer_remove(self._timer)

Override the context.

To run an operator with different context members, override the context. For example modify above to give the usual suspect override dictionary of area, space_data, and region

As an example, edit for above to clear the console if right clicked. Notice the override has passed the poll test of the bpy.ops.console.clear() operator.

def override(self, context, event):
    screen = context.screen
    x, y = event.mouse_x, event.mouse_y

    areas = [a for a in screen.areas if a.x < x < a.x + a.width
            and a.y < y < a.y + a.height]        
    if areas:                    
        return {
                "area" : areas[0],
                "space_data" : areas[0].spaces.active,
                "region": areas[0].regions[-1],
                }
    return {}
            
def modal(self, context, event):
    if event.type in {'RIGHTMOUSE'}:
        override = self.override(context,  event)
        if bpy.ops.console.clear.poll(override):
            bpy.ops.console.clear(override)
            return {'RUNNING_MODAL'}
$\endgroup$
3
  • $\begingroup$ This is really good. Thanks. But I'm struggling with your overwrite object. It seems that it only contains 3 entries. Shouldn't it have all of the context with the 3 entries changed? $\endgroup$
    – DrDress
    Commented Dec 17, 2020 at 8:32
  • $\begingroup$ Like in here, under "overwrite context": docs.blender.org/api/current/bpy.ops.html $\endgroup$
    – DrDress
    Commented Dec 17, 2020 at 8:45
  • 1
    $\begingroup$ AFAIK the result is the same. To confirm this for yourself insert print(context.object) in simple operator template main method and test call it with bpy.ops.object.simple_operator({"foo" : 2}) $\endgroup$
    – batFINGER
    Commented Dec 17, 2020 at 10:23

You must log in to answer this question.

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