What does the `propvalue` parameter of `bpy.types.KeyMapItems.new_modal()` need to be?

Currently I am encountering the problem that my modal operator’s invoke() method keeps getting called when I hold down the according key from the keymap which triggers it (even though it previously returned {"RUNNING_MODAL"} without concluding with a return of {"CANCELLED"} or {"FINISHED"} in modal()), so I thought maybe I need a modal keymap to go with it (instead of a normal one) - I don’t actually know whether this thinking is correct, though.

Moving on just to try, I cannot figure out what to use as the first parameter (propvalue) for bpy.types.KeyMapItems.new_modal(). The documentation only says its a string “Property Value”. I can only guess this means I need to put the value of some bpy.types.StringProperty which annotates something while having some name and some value. Why did they not simply use bl_idname here as well? Anyway, although the code does not throw, I don’t seem to be able to get my operator to be called at all anymore with a modal keymap.

?????: bpy.props.StringProperty(name="?????", default="?????") # ?????
class Foo(bpy.types.Operator):
    bl_idname = "?????"
    # ...

def register():
    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc:
        km = kc.keymaps.new(name = "3D View", space_type = 'VIEW_3D', region_type = 'WINDOW', modal=True)
        kmi = km.keymap_items.new_modal("?????", 'SPACE', "PRESS")

How does a modal keymap item know what operator to invoke? What is a modal keymap?

a modal keymap is a keymap that only works in a particular modal operation.

for example, when you’re in the middle of a bevel operation (which is a modal operator), you’re dragging the mouse around to change the radius of the bevel, and using the mouse wheel to increase/decrease the number of segments. The modal keymap for Bevel looks something like- mousewheelup - increase segments, mousewheeldown - decrease, RMB press - cancel, LMB press - confirm.

If “Foo” in your case is not a modal operator, you don’t need to make a new modal keymap item or flag your keymap as being modal. Also note that if you deviate from what Blender is already expecting, you’re going to see weird results (if you see any results at all). For example, there is already a 3D View keymap, and it is not modal- so if you flag it as being modal Blender is likely going to throw an error or (more likely) ignore it entirely.

One piece of advice would be to avoid using keymap items altogether and just monitor events in your modal function- this is what pretty much every addon I’ve ever seen (including my own) do.


def modal(self, context, event):
    if event.type in {'RIGHTMOUSE', 'ESC'} and event.value == 'PRESS':
        return {'CANCELLED'}

    if event.type in {'LEFTMOUSE', 'RETURN', 'SPACE'} and event.value == 'PRESS':
        return {'FINISHED'}

    return {'RUNNING_MODAL'}

I do entertain the thought of the user being able to change the bound key in the Preferences though. Regarding modality, I am not sure what it means in this context. Usually “modal” means that nothing else can interfere. My operator does processing of mouse events over multiple frames to give real-time feedback on what it is doing to the user ({"RUNNING_MODAL"}) which appears to be what “modal” means in Blender terminology. My operator does not need to be “modal” in the common sense, that nothing else can run besides it. It only needs to be “modal” in Blender’s sense, meaning that it processes events as they come, terminating only as soon as the user confirms or cancels it. It will consume those events it cares about and nothing else. I just want my operator’s lifecycle to work however it is that Blender intends.

If I understand you correctly, you suggest I guard excessive operator invocations with global state? It’s what I am doing already to mitigate the issue.

    def invoke(self, context, event):
        global running
        if not running and event.value == "PRESS":
            running = True
            return {"RUNNING_MODAL"}
        return {"PASS_THROUGH"}

If yes, I must admit I wasn’t aware Blender’s addon ecosystem is in such anarchistic condition.

Regarding a 3D View keymap already existing, how would I hook up to it correctly? Or even know that it exists? I’m sorry in case I am falling back into territory which I could well just look up myself.

Yes, you’ve got it correct. as long as you return PASS_THROUGH, blender will let your operator pass events through it without consuming them. returning RUNNING_MODAL consumes the event (and thus prevents things like viewport navigation, etc). Addons like BoxCutter use this feature to great effect, and it’s the intended use of the feature.

More or less yeah

This one is a little harder to explain because it’s not really documented and it’s one of those things you just ‘learn in the trenches’, usually after being shot multiple times. a keyconfig contains a dictionary of keymap types, it doesn’t really care how many you throw at it and so long as it’s not already identical to one that’s already in the collection it’ll continue to add them. Therefore, it’s possible to add a keymap called 3D View that is not the one that shows up in the preferences->input menu! for example- if you create a keymap called 3D View that is not in space_type VIEW_3D, or you create one with modal=True. These keymaps all have the right name but blender has no idea wtf they are or what they’re for, so it just ignores them and assumes you know what you’re doing. Working with keymaps is, unfortunately, one of the more arcane dark arts of blender addon development (if you see my post history you’ll find more than one rant on the subject).

Alrighty then. I did some testing:

        km = kc.keymaps.find(name = "3D View", space_type = "VIEW_3D", region_type = "WINDOW")
        if km is None:
            print("Adding missign 3D View keymap")
            km = kc.keymaps.new(name = "3D View", space_type = 'VIEW_3D', region_type = 'WINDOW', modal=False)
            print("Found existing keymap")
            addedAnywayKm = kc.keymaps.new(name = "3D View", space_type = 'VIEW_3D', region_type = 'WINDOW', modal=False)
            print(km is addedAnywayKm)

km is indeed not None when I load my addon for the second time. The last print() then prints False, meaning (if I am not making false assumptions about how it works under the hood) that it is possible to add new keymaps with the same name, space_type and region_name multiple times. (Unless it replaces existing ones) So…

(1) Should I try to use find() before new() and use the returned keymap if it is not None?
(2) Should I call wm.keyconfigs.addon.remove(km) when my addon is unregistered?
(3) If I do (2), will I screw any addon which does (1)?

Thanks for sticking with me.

1- There’s more than one way to do it, and that would work, yes.
2- Definitely not. Unfortunately, Blender dumps all addon keymaps into the addon keyconfig, so if you remove the keymap you can actually break other addons. It truly is a mess.
3- See above

Alright, I’ll use find() and see if it ever causes problems. Do we know the answer to the original question though? What is the propvalue-parameter of new_modal()? I see people put in strings which look like event types or other enum values, but I can’t seem to find what they correspond to.

propvalue is the property value that shows up in the modal map enum dropdown in the input editor. think of it like an alias to a particular modal action you want to perform (honestly, this is how the entire keymap system should work IMO, but I digress)

For example, the transform modal keymap has tons of propvalues: confirm, cancel, etc: