Help needed: Assign Hotkey to a series of commands

Hi all. I know that these kind of questions have been asked a lot here, but I’m a bit lost and what I want is fairly easy (I guess).
In 2.79 I used to have Sensei Format keymaps enabled (from the time Sensei Format was free to use). I was really accustomed with a snapping option, with which when I pressed a specific hotkey the cursor was automatically snapping to the closest vertex or the median point of an edge or surface, whatever was closer. I’d like to customize such an operation inside 2.80, but I’m a bit lost with all these tutorials etc. and I don’t have the time to learn all these new things about python, even though I’m really interested in it.
The series of commands executed when I use this hotkey in 2.79 is this (I copy from the info window):

bpy.ops.object.select_all(action=‘DESELECT’)
bpy.ops.view3d.snap_cursor_to_selected()
bpy.ops.object.select_all(action=‘DESELECT’)
bpy.ops.object.editmode_toggle()
bpy.ops.view3d.snap_cursor_to_selected()
bpy.ops.object.editmode_toggle()
bpy.ops.object.select_all(action=‘DESELECT’)

If I understand correctly, this little scripts enables edit mode and snaps cursor to selected edge or face, which means that the cursor would be placed at the median point of the closest element, and then toggles back to object mode and clears all selections. The question is how should an executable script, that automates this process, look like and how could I assign it to a hotkey of my choice.
I’m trying to bring the new 2.80 UI closer to my previous workflow, and hotkey operations like that are really important, because they speed up my modeling by a great deal.

Thanks in advance!

PS I’ve already found workarounds for this operation online (besides the snap to selected method), but I’d like to have it enabled with a single hotkey if possible.

Anyone? :worried:

You have to define a class which the window manager can understand, it looks like this:

class CustomSetCursor(bpy.types.Operator):
    """Move the 3d cursor to the active vertex."""
# this is the basic info the window manager demands
    bl_idname = "object.custom_set_cursor"
    bl_label = "Snap to vert"
    bl_options = {'MACRO','INTERNAL'} # this means it won't show up in the search bar
# if you want it to show up in the search bar, please RTFM here: https://docs.blender.org/manual/en/latest/advanced/scripting/addon_tutorial.html
# execute is what happens when the key is pressed
    def execute(self, context):
        bpy.ops.object.select_all(action=‘DESELECT’)
        bpy.ops.view3d.snap_cursor_to_selected()
        bpy.ops.object.select_all(action=‘DESELECT’)
        bpy.ops.object.editmode_toggle()
        bpy.ops.view3d.snap_cursor_to_selected()
        bpy.ops.object.editmode_toggle()
        bpy.ops.object.select_all(action=‘DESELECT’)
        return {'FINISHED'}

Then you need to define 2 more functions, register and unregister.
These are run when the plugin is loaded.

In register, set your hotkey up.

def register():
# register_class is what informs the window manager we made a custom function just for it
    bpy.utils.register_class(CustomSetCursor)
    # handle the keymap
    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc: # name='Window' is not just for fun, it changes where this keymap is registered in prefs
        km = wm.keyconfigs.addon.keymaps.new(name='Window', space_type='EMPTY')
        addon_keymaps.append((km, km.keymap_items.new(CustomSetCursor.bl_idname, 'ONE', 'PRESS')))

This is line actually defines the hotkey
addon_keymaps.append((km, km.keymap_items.new(CustomSetCursor.bl_idname, 'ONE', 'PRESS')))
the ‘ONE’ is the key, you could use ‘K’ for example to map the letter K, the list of usable keys is in the blender docs. If you want to use alt, ctrl etc, then after ‘PRESS’ put:
'PRESS', ctrl=True, shift=True, alt=True)

Edit: here is a link to the list of usable keys.
https://docs.blender.org/api/2.80/bpy.types.KeyMapItem.html#bpy.types.KeyMapItem

When the plugin is unloaded, if your unregister function doesn’t work right, the program will probably crash or act up.

def unregister():
    for km, kmi in addon_keymaps:
        km.keymap_items.remove(kmi)
    addon_keymaps.clear()
    bpy.utils.unregister_class(CustomSetCursor)

Notice all we did was the reverse of register.

One last thing, in order to make this code installable as an add-on, you have to add meta-data to the file. Plus, you need to import bpy…

So at the very top of the file, add these lines:

bl_info = {
    "name": "Custom Vert Snap",
    "category": "Object",
    "blender": (2,80,0)
}

import bpy

Then put the class, register, and unregister function after it

finally, at the very end, simply put:

if __name__ == "__main__":
    register()

which will run the script when it’s loaded

Save the file as a .py file and then you can install it from preferences.

1 Like

Sorry for answering so late. I was away from home for a couple of days, and just came back.

Wow! I simply don’t have words to thank you for all this help! Much obliged!!
You explained every step thoroughly to a hopeless like me…

In the top section, when we write “category”: “Object”, we mean that this command will be executed only in object mode? Can I use it in Edit mode too? Again, sorry if this question is too basic. I have no former experience in coding whatsoever.

One more question, if you don’t mind. If I want to assign a SHIFT+Right Mouse Button to this custom cursor snap, how would the script should be written in the hotkey section? Like this?
addon_keymaps.append((km, km.keymap_items.new(CustomSetCursor.bl_idname, 'RIGHTMOUSE', 'PRESS', shift=True)))

I’ll post the full script here according to your guidelines. Again, if it’s not too much of a burden, please take a look at it and tell me if it’s OK. I’ve made some small changes to “name” and bl_label only.

bl_info = {
    "name": "Custom Cursor Snap",
    "category": "Object",
    "blender": (2,80,0)
}

import bpy
class CustomSetCursor(bpy.types.Operator):
    """Move the 3d cursor to the active element."""
# this is the basic info the window manager demands
    bl_idname = "object.custom_set_cursor"
    bl_label = "Snap to active"
    bl_options = {'MACRO','INTERNAL'} # this means it won't show up in the search bar
# if you want it to show up in the search bar, please RTFM here: https://docs.blender.org/manual/en/latest/advanced/scripting/addon_tutorial.html
# execute is what happens when the key is pressed
    def execute(self, context):
        bpy.ops.object.select_all(action=‘DESELECT’)
        bpy.ops.view3d.snap_cursor_to_selected()
        bpy.ops.object.select_all(action=‘DESELECT’)
        bpy.ops.object.editmode_toggle()
        bpy.ops.view3d.snap_cursor_to_selected()
        bpy.ops.object.editmode_toggle()
        bpy.ops.object.select_all(action=‘DESELECT’)
        return {'FINISHED'}
    
    def register():
# register_class is what informs the window manager we made a custom function just for it
    bpy.utils.register_class(CustomSetCursor)
    # handle the keymap
    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc: # name='Window' is not just for fun, it changes where this keymap is registered in prefs
        km = wm.keyconfigs.addon.keymaps.new(name='Window', space_type='EMPTY')
        addon_keymaps.append((km, km.keymap_items.new(CustomSetCursor.bl_idname, 'RIGHTMOUSE', 'PRESS', shift=True)))
        
        def unregister():
    for km, kmi in addon_keymaps:
        km.keymap_items.remove(kmi)
    addon_keymaps.clear()
    bpy.utils.unregister_class(CustomSetCursor)
    
    if __name__ == "__main__":
    register()

I run this script (corrected according to @horusscope 's first post

bl_info = {
    "name": "Custom Vert Snap",
    "category": "Object",
    "blender": (2,80,0)
}

import bpy

class CustomSetCursor(bpy.types.Operator):
    """Move the 3d cursor to the active vertex."""
# this is the basic info the window manager demands
    bl_idname = "object.Custom Set Cursor"
    bl_label = "Snap to vert"
    bl_options = {'MACRO','INTERNAL'} # this means it won't show up in the search bar
# if you want it to show up in the search bar, please RTFM here: https://docs.blender.org/manual/en/latest/advanced/scripting/addon_tutorial.html
# execute is what happens when the key is pressed
    def execute(self, context):
        bpy.ops.object.select_all(action='DESELECT')
        bpy.ops.view3d.snap_cursor_to_selected()
        bpy.ops.object.select_all(action='DESELECT')
        bpy.ops.object.editmode_toggle()
        bpy.ops.view3d.snap_cursor_to_selected()
        bpy.ops.object.editmode_toggle()
        bpy.ops.object.select_all(action='DESELECT')
        return {'FINISHED'}
    
    def register():
# register_class is what informs the window manager we made a custom function just for it
    bpy.utils.register_class(CustomSetCursor)
    # handle the keymap
    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc: # name='Window' is not just for fun, it changes where this keymap is registered in prefs
        km = wm.keyconfigs.addon.keymaps.new(name='Window', space_type='EMPTY')
        addon_keymaps.append((km, km.keymap_items.new(CustomSetCursor.bl_idname, 'RIGHTMOUSE', 'PRESS', shift=True)))
        
        def unregister():
    for km, kmi in addon_keymaps:
        km.keymap_items.remove(kmi)
    addon_keymaps.clear()
    bpy.utils.unregister_class(CustomSetCursor)
    
    if __name__ == "__main__":
    register()

and I get this message

  File "\Custom_Cursor_Snap.py", line 29
    bpy.utils.register_class(CustomSetCursor)
      ^
IndentationError: expected an indented block

location: <unknown location>:-1

I don’t know what an indentation error is exactly. I found online that it has to do with spaces and tabs and dashes, but I can’t figure out what’s the problem here. It’s a bit like rocket science to me…

In your code you have this:

        def unregister():
    for km, kmi in addon_keymaps:
        km.keymap_items.remove(kmi)

It has to be this:

def unregister():
    for km, kmi in addon_keymaps:
        km.keymap_items.remove(kmi)

Same with the register function, you can’t put a tab before def there either.
Python has very strict indentation and whitespace formatting rules.

bl_info category is where the add-on will be found in the preferences -> add-ons view, not where the hotkey will work.

You want it to work on release instead, so you’d put this to bind Shift+RMB
addon_keymaps.append((km, km.keymap_items.new(TestTest1.bl_idname, 'RIGHTMOUSE', 'RELEASE', shift=True)))

The if name also can’t be indented, those should be all the way to the left:

class TestTest1(bpy.types.Operator):
    """Swap the active draw brush to the first hot slot."""

def register():
    bpy.utils.register_class(TestTest1)

def unregister():
    for km, kmi in addon_keymaps:

if __name__ == "__main__":
    register()
1 Like

Thank you very much. I fixed the Tab issue. I’m starting to understand it a bit.

The final script is as follows (I changed the keymap to ALT+SHIFT+RMB, because SHIFT+RMB is assigned to cursor movement by default and I think it’s better to leave it that way):

bl_info = {
    "name": "Custom Vert Snap",
    "category": "Object",
    "blender": (2,80,0)
}

import bpy

class CustomSetCursor(bpy.types.Operator):
    """Move the 3d cursor to the active vertex."""
# this is the basic info the window manager demands
    bl_idname = "object.custom_set_cursor"
    bl_label = "Snap to vert"
    bl_options = {'MACRO','INTERNAL'} # this means it won't show up in the search bar
# if you want it to show up in the search bar, please RTFM here: https://docs.blender.org/manual/en/latest/advanced/scripting/addon_tutorial.html
# execute is what happens when the key is pressed
def execute(self, context):
        bpy.ops.object.select_all(action='DESELECT')
        bpy.ops.view3d.snap_cursor_to_selected()
        bpy.ops.object.select_all(action='DESELECT')
        bpy.ops.object.editmode_toggle()
        bpy.ops.view3d.snap_cursor_to_selected()
        bpy.ops.object.editmode_toggle()
        bpy.ops.object.select_all(action='DESELECT')
        return {'FINISHED'}
    
def register():
# register_class is what informs the window manager we made a custom function just for it
    bpy.utils.register_class(CustomSetCursor)
    # handle the keymap
    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc: # name='Window' is not just for fun, it changes where this keymap is registered in prefs
        km = wm.keyconfigs.addon.keymaps.new(name='Window', space_type='EMPTY')
        addon_keymaps.append((km, km.keymap_items.new(CustomSetCursor.bl_idname, 'RIGHTMOUSE', 'PRESS', shift=True, alt=True)))
        
def unregister():
    for km, kmi in addon_keymaps:
        km.keymap_items.remove(kmi)
    addon_keymaps.clear()
    bpy.utils.unregister_class(CustomSetCursor)
    
if __name__ == "__main__":
    register()

Now, when I try to install it as an addon, I have an error:

Traceback (most recent call last):
  File "C:\Program Files\Blender Foundation\Blender\2.80\scripts\modules\addon_utils.py", line 384, in enable
    mod.register()
  File "C:\Users\*********\AppData\Roaming\Blender Foundation\Blender\2.80\scripts\addons\Custom_Cursor_Snap.py", line 35, in register
    addon_keymaps.append((km, km.keymap_items.new(CustomSetCursor.bl_idname, 'RIGHTMOUSE', 'PRESS', shift=True, alt=True)))
NameError: name 'addon_keymaps' is not defined

I don’t know exactly what’s wrong here. When it says “name” does it mean “CustomSetCursor”, or something else?

no, it means the var addon_keymaps isn’t defined, you use it for example here:
addon_keymaps.append((km, km.keymap_items.new(CustomSetCursor.bl_idname, 'RIGHTMOUSE', 'PRESS', shift=True, alt=True)))

add it above register in the script scope

addon_keymaps = []

def register():

By the way, keymap_items.new would work without that variable addon_keymaps, but without addon_keymaps, the unregister function would become unworkable. That list is used so that the script can later reference what it must unregister.

1 Like

The script is OK now. It runs without errors. But there are more to it in order to work properly. The snapping function isn’t working of course. More lines are needed. I thought it was easy… :grinning:
How foolish of me. I’ll try to study a bit more in order to make this work.

@horusscope I really thank you for all this help. It was hard for me to understand basic things about scripting. I now realize how many things have to be taken into account in order to make something work. I guess, I’ll have to use the snapping workarounds for now, and leave to be solved in the future.

1 Like