In my script, I've created an Operator, a PropertiesGroup which holds the properties of my operator and a Panel which lets the user change the properties and start the operator running.

My operator uses mouse input, so it returns RUNNING_MODAL for the LEFTMOUSE event (and many other events too). Unfortunately, this prevents the user from using the Panel while the operator is running. If I return PASS_THROUGH from the left mouse event instead, I can use the panel but it has the side effect that my operator never gets the RELEASE event should the user click within the panel. This has the effect of leaving my operator in a state where it thinks the mouse is permanently held down.

What would be great was if there were some way to make sure that the Panel got to handle the events first and only passed them onto my operator if they weren't used. Maybe there's some way to make the Panel a 'child' of my Operator?

In the below program, if you start the operator, you cannot change the panel controls until you press enter to exit the operator:

import bpy

class NormalToolSettings(bpy.types.PropertyGroup):
    normal : bpy.props.FloatVectorProperty(
        description="Direction of normal in Fixed mode", 
        default = (1, 0, 0), 

class ModalDrawOperator(bpy.types.Operator):
    """Adjust normals"""
    bl_idname = "kitfox.normal_tool"
    bl_label = "Normal Tool Kitfox"

    dragging = False

    def mouse_move(self, context, event):
        mouse_pos = (event.mouse_region_x, event.mouse_region_y)

        ctx = bpy.context
        if self.dragging:
            print("mouse drag to " + str(mouse_pos))

    def mouse_down(self, context, event):
        if event.value == "PRESS":
            self.dragging = True
        elif event.value == "RELEASE":
            self.dragging = False

    def modal(self, context, event):


        if event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
            # allow navigation
            return {'PASS_THROUGH'}

        elif event.type == 'MOUSEMOVE':
            self.mouse_move(context, event)
            if self.dragging:
                return {'RUNNING_MODAL'}
                return {'PASS_THROUGH'}
        elif event.type == 'LEFTMOUSE':
            self.mouse_down(context, event)
#            return {'PASS_THROUGH'}
            return {'RUNNING_MODAL'}

        elif event.type in {'RET'}:
            return {'FINISHED'}

        elif event.type in {'PAGE_UP', 'RIGHT_BRACKET'}:
            if event.value == "PRESS":
                brush_radius = context.scene.my_tool.radius
                brush_radius = brush_radius + .1
                context.scene.my_tool.radius = brush_radius
            return {'RUNNING_MODAL'}

        elif event.type in {'PAGE_DOWN', 'LEFT_BRACKET'}:
            if event.value == "PRESS":
                brush_radius = context.scene.my_tool.radius
                brush_radius = max(brush_radius - .1, .1)
                context.scene.my_tool.radius = brush_radius
            return {'RUNNING_MODAL'}
        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            return {'CANCELLED'}

        return {'PASS_THROUGH'}

    def invoke(self, context, event):
        if context.area.type == 'VIEW_3D':
            # the arguments we pass the the callback
            args = (self, context)
            # Add the region OpenGL drawing callback
            # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
            self._context = context


            return {'RUNNING_MODAL'}
            self.report({'WARNING'}, "View3D not found, cannot run operator")
            return {'CANCELLED'}

class NormalToolPropsPanel(bpy.types.Panel):

    """Properties Panel for the Normal Tool on tool shelf"""
    bl_label = "Normal Tool"
    bl_idname = "OBJECT_PT_normal_tool_props"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Kitfox2"

    def draw(self, context):
        layout = self.layout

        scene = context.scene
        settings = scene.my_tool

        col = layout.column();
        col.operator("kitfox.normal_tool", text="Start Normal Tool")
        col.prop(settings, "normal")

def register():


    bpy.types.Scene.my_tool = bpy.props.PointerProperty(type=NormalToolSettings)

def unregister():

    del bpy.types.Scene.my_tool

if __name__ == "__main__":
  • $\begingroup$ Could you please expand on "Panel got to handle the events first and only passed them onto my operator if they weren't used. " $\endgroup$
    – batFINGER
    Commented Feb 1, 2021 at 9:45
  • $\begingroup$ I'm thinking of windowing systems used by other APIs. If you were to think of the viewport as a modeless panel and the Panel I'm creating here as another modeless panel sitting on top of it (and a child of it), the priority would be for the child panel to get the events first and then mark them as 'consumed' if they should not propagate further or leave them alone if the window the next level up should get them. (Blender uses the term 'modal' quite differently that other APIs I've used). $\endgroup$
    – kitfox
    Commented Feb 1, 2021 at 10:47

1 Answer 1


Use a modal timer operator.

The idea of a modal is to lock the UI to it, often reporting to area title bar until it is finished or cancelled.

Instead could use a modal timer operator https://blender.stackexchange.com/a/69599/15543

Store session variables in the window manager.

here is an example of using a property to start / stop operator.


which can also be used to poll the panel.

The property is added to the scene, but in hindsight would be far better as a prop defined on the window manager bpy.types.WindowManager. Use the window manager to store session variables to share between UI and operators.

  • $\begingroup$ I've read through the example you linked, but I don't think calling my operator on a timer will help. The problem is that if I return PASS_THROUGH when the mouse button is pressed and the Panel captures it instead, I never receive the coresponding mouse released event. This makes it impossible to tell what state the mouse is in. $\endgroup$
    – kitfox
    Commented Feb 1, 2021 at 10:53

You must log in to answer this question.

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