0

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???")

DrDress
  • 563
  • 4
  • 19

1 Answers1

2

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'}

batFINGER
  • 84,216
  • 10
  • 108
  • 233
  • 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? – DrDress Dec 17 '20 at 08:32
  • Like in here, under "overwrite context": https://docs.blender.org/api/current/bpy.ops.html – DrDress Dec 17 '20 at 08:45
  • 1
    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}) – batFINGER Dec 17 '20 at 10:23