1
$\begingroup$

I've created a modal operator that I currently start by pressing a button in a panel, but want to adapt it so that I can use it from a workspace tool button instead. I've written a test script that creates my button and can send commands to an operator - but the trouble is that the operator acts like a one-off operator and not a modal one. Only the invoke method seems to be called. The modal method never is even though the operator is returning RUNNING_MODAL. How can I adapt this to create a modal operator that maintains state for as long as I'm using the tool?

import bpy
import bpy.utils.previews

class TerrainSculptWorkspaceTool(bpy.types.WorkSpaceTool):  
    bl_space_type = 'VIEW_3D'
    bl_context_mode = 'OBJECT'

    bl_idname = "kitfox_terrain.terrain_brush_draw"
    bl_label = "Terrain Brush Draw"
    bl_description = ("Raise or lower terrain under cursor to the current brush height.")

    bl_icon =  "ops.gpencil.draw.poly"          
    bl_widget = None 
    bl_keymap = (
        ("kitfox.echo_tool", {"type": 'LEFTMOUSE', "value": 'PRESS'},
         {"properties": [("tool_mode", "DEFAULT" )]}),
        ("kitfox.echo_tool", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True},
         {"properties": [("tool_mode", "CONTROL")]}),
        ("kitfox.echo_tool", {"type": 'LEFTMOUSE', "value": 'PRESS', "alt": True},
         {"properties": [("tool_mode", "ALT")]}),
        ("kitfox.echo_tool", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
         {"properties": [("tool_mode", "SHIFT")]}),
        ("kitfox.echo_tool", {"type": 'LEFTMOUSE', "value": 'PRESS', "oskey": True},
         {"properties": [("tool_mode", "OSKEY")]}),
        ("kitfox.echo_tool", {"type": 'LEFTMOUSE', "value": 'PRESS', "oskey" : True , "alt": True},
         {"properties": [("tool_mode", "OS+ALT")]}),    
        ("kitfox.echo_tool", {"type": 'MOUSEMOVE', "value": 'ANY' }, {"properties": []}),
    )



class EchoToolOperator(bpy.types.Operator):
    """Echo tool"""
    bl_idname = "kitfox.echo_tool"
    bl_label = "Echo tool"
    bl_options = {"REGISTER", "UNDO"}
    
    is_running = False

    tool_mode : bpy.props.StringProperty(
        name="Tool Mode",
        description="Tool Mode",
    )

    # @classmethod
    # def poll( cls , context ) :
    #     return not EchoToolOperator.is_running

    def __init__(self):
        self.picking = False

    def modal(self, context, event):
        print("modal evTyp:%s evVal:%s mode:%s" % (str(event.type), str(event.value), self.tool_mode))

        if context.mode != 'OBJECT':
            EchoToolOperator.is_running = False
            return {'CANCELED'}

        return {'PASS_THROUGH'}

    def invoke(self, context, event):
        print("invoke evTyp:%s evVal:%s mode:%s" % (str(event.type), str(event.value), self.tool_mode))

        EchoToolOperator.is_running = True

        return {'RUNNING_MODAL'}
$\endgroup$

1 Answer 1

2
$\begingroup$

You have to put this into your invoke method.

def invoke(self, context, event):
        
        print("Starting my Modal")
        # this line
        context.window_manager.modal_handler_add(self)
        return {"RUNNING_MODAL"}

    
def modal(self, context, event):
    if event.type == "MOUSEMOVE":
        print(f"{event.mouse_x}, {event.mouse_y}")
        return {"RUNNING_MODAL"}
    elif event.type in {"RIGHTMOUSE", "ESC"}:
        print("Done!")
        return {"FINISHED"}
        


$\endgroup$
4
  • $\begingroup$ I tried adding that but now have a new problem - the operator never exits. I need to be able to finish or cancel the operator if the user clicks on one of the other workspace tools. $\endgroup$
    – kitfox
    Commented Jun 28, 2022 at 17:20
  • $\begingroup$ You have to add a modal method to your operator and handle the events. A modal is like a mini-program that runs repeatedly on every frame of the viewport rendering, and depending on the what you return from each event.type it will either continue or finished etc. I put an example in the answer. $\endgroup$
    – Jakemoyo
    Commented Jun 29, 2022 at 8:06
  • $\begingroup$ I think you're missing the point that this all needs to be driven from a WorkspaceTool. This imposes additional constrains regular modal operators do not have. I cannot return 'FINISHED' because I never receive any signal I can interpret as finishing the operation. By capturing and processing mouse input, I effectively disable the ability for the user to click on other workspace tools which is how one would normally expect to stop using a tool. $\endgroup$
    – kitfox
    Commented Jun 29, 2022 at 17:38
  • $\begingroup$ You can put any condition in the modal event loop to trigger a return {"FINISHED"} outcome. It doesn't just have to be an event like keypresses. Your question was specifically oriented around "How to get a modal to work" so that's what I'm answering. $\endgroup$
    – Jakemoyo
    Commented Jun 30, 2022 at 10:22

You must log in to answer this question.

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