Skip to main content
3 of 3
added 103 characters in body
kitfox
  • 1.7k
  • 6
  • 26
  • 43

How to make a Panel accessible while modal operator running?

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(
        name="Normal", 
        description="Direction of normal in Fixed mode", 
        default = (1, 0, 0), 
        subtype="DIRECTION"
    )
    
    
#-------------------------------

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

        context.area.tag_redraw()

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

            context.area.tag_redraw()

            context.window_manager.modal_handler_add(self)
            return {'RUNNING_MODAL'}
        else:
            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.utils.register_class(NormalToolSettings)
    bpy.utils.register_class(ModalDrawOperator)
    bpy.utils.register_class(NormalToolPropsPanel)

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



def unregister():
    bpy.utils.unregister_class(NormalToolSettings)
    bpy.utils.unregister_class(ModalDrawOperator)
    bpy.utils.unregister_class(NormalToolPropsPanel)

    del bpy.types.Scene.my_tool
    
    

if __name__ == "__main__":
    register()
```
kitfox
  • 1.7k
  • 6
  • 26
  • 43