Addon keymap changes are lost after restarting Blender

Hello,
I found some open posts with the same issue and tried the different solutions. I can’t get the hotkeys to work. I have been trying for days to fix this issue and try out different solutions, but nothing works. What am I doing wrong? This is the keymap code:

import bpy

keys = []

def add_hotkey():
    wm = bpy.context.window_manager
    kc = wm.keyconfigs.user
 
    if kc:
        # register to 3d view mode tab
        km = kc.keymaps.new(name="3D View Generic", space_type="VIEW_3D")

        kmi = km.keymap_items.new(idname='wm.call_menu_pie', type='C', value='PRESS', ctrl=True, shift=True)
        kmi.properties.name = "COLLISION_MT_pie_menu"
        kmi.active = True
        
        keys.append((km, kmi))

        kmi = km.keymap_items.new(idname='wm.call_panel', type='P', value='PRESS', shift=True)
        kmi.properties.name = 'VIEW3D_PT_collission_visibility_panel'
        kmi.active = True
        
        keys.append((km, kmi))
        
        kmi = km.keymap_items.new(idname='wm.call_panel', type='P', value='PRESS', shift=True, ctrl=True)
        kmi.properties.name = 'VIEW3D_PT_collission_material_panel'
        kmi.active = True
        
        keys.append((km, kmi))



def remove_hotkey():
    ''' Clears custom hotkeys stored in addon_keymaps '''

    # only works for menues and pie menus
    for km, kmi in keys:
        if hasattr(kmi.properties, 'name') and kmi.properties.name in ['COLLISION_MT_pie_menu', 'VIEW3D_PT_collission_visibility_panel', 'VIEW3D_PT_collission_material_panel']:
            km.keymap_items.remove(kmi)
    keys.clear()


def get_hotkey_entry_item(km, kmi_name, kmi_value, properties):
    for i, km_item in enumerate(km.keymap_items):
        if km.keymap_items.keys()[i] == kmi_name:
            if properties == 'name':
                if km.keymap_items[i].properties.name == kmi_value:
                    return km_item
            elif properties == 'tab':
                if km.keymap_items[i].properties.tab == kmi_value:
                    return km_item
            elif properties == 'none':
                return km_item
    return None
    
class COLLISION_OT_add_hotkey_renaming(bpy.types.Operator):
    ''' Add hotkey entry '''
    bl_idname = "collision_tool.add_hotkey"
    bl_label = "Addon preferences Example"
    bl_options = {'REGISTER', 'INTERNAL'}

    def execute(self, context):
        add_hotkey()
        return {'FINISHED'}

# keymap needs to be registered before the preferences UI
classes = (
    COLLISION_OT_add_hotkey_renaming,
)


def register():
    from bpy.utils import register_class

    for cls in classes:
        register_class(cls) 
    
    add_hotkey()


def unregister():
    from bpy.utils import unregister_class

    remove_hotkey()

    for cls in reversed(classes):
        unregister_class(cls)
        

This is the code for the Preferences UI

def draw_key_item(kc, layout, title, kmi_name, kmi_value):
        
    row = layout.row(align=True)
    row.label(text=title)
    km = kc.keymaps['3D View Generic']
    kmi = get_hotkey_entry_item(km, kmi_name, kmi_value, 'name')
    if kmi:
        layout.context_pointer_set("keymap", km)
        rna_keymap_ui.draw_kmi([], kc, km, kmi, layout, 0)

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

      wm = context.window_manager
      kc = wm.keyconfigs.user
      
      # Main Pie
      sub_box = layout.box()
      draw_key_item(kc, sub_box, 'Main Pie', 'wm.call_menu_pie', 'COLLISION_MT_pie_menu')
      draw_key_item(kc, sub_box, 'Visibilitty Menu', 'wm.call_panel', 'VIEW3D_PT_collission_visibility_panel')
      draw_key_item(kc, sub_box, 'Material Menu', 'wm.call_panel', 'VIEW3D_PT_collission_material_panel')
      row = sub_box.row(align = True)
      row.operator('collision_tool.add_hotkey', text = "Reset Hotkeys")

I understood that the issue might be related to the keyconfigs. I have tried the different addon, user, and active but nothing worked.

I am thankful for any help!

1 Like

I’d just like to say that I’m also currently struggling with this problem, it seems like such a key thing and yet there seem to be almost no resources out there on how to fix it…

If someone knows how it can be done, I’d also be very grateful to hear it!

If you- incredible as you are with Blender Python- don’t know how to fix it, there’s a 150% chance this is a bug :sweat_smile: report it and see what happens?

1 Like

Maybe you’re right :sweat_smile:, from what I’ve found it seems like this isn’t so much of a bug as it is an oversight; it just hasn’t been implemented yet.

Explanation

It seems that addons are supposed to register their keymap additions using the addon keyconfig. However, this is re-added each time the addon is registered, so just directly drawing the keymap items from the addon config means that any edits made in the UI will be reset each time the addon is reloaded.
Instead, we need to understand the steps of how keyconfigs are handled in Blender (as far as I can tell). First, all the default key maps are added to the active keyconfig.
Then the addon keymaps are registered to the addon keyconfig, and are added to the end of the active keyconfig.
Finally, all the user modified keymap settings are loaded from the userprefs.blend file and are applied to the active keyconfig, overwriting the defaults supplied by Blender and the addons registration.

All of that means that instead of drawing the keymap items in the UI directly from the addon keyconfig, we instead need to find the corresponding keymap items in the active keyconfig, and draw them instead, which will mean that any changes made in the UI will be saved in the userprefs file, and will be automatically applied back when we next load the keymap from the addon.

However, as far as I can tell, there is no officially supported way to get the corresponding active keymap item if you only have the addon version. There are definitely workarounds involving just iterating through all active keymaps items and comparing the operator and it’s properties to the required addon keymap item’s, but they aren’t perfect, and don’t account for things such as duplicate entries with different keybindings.
This is where I think the API falls short, and what it needs is a way to automatically get the active version of an addon keymap item so that it can be drawn in the UI and saved properly so that it won’t be reset when the addon is next registered.

This is just what I’ve managed to gather from about a few weeks of on and off googling, so be warned that it may be completely inaccurate.

Either way, I’ll make a bug report about it tomorrow and see whether there’s some obvious solution to this that I’ve managed to miss xD.

2 Likes

That’s great that you are making a bug report. Thanks

I did analyse some other commercial addons (Speedflow, HardOps, MeshMashine, Fluent) and some of them have issues as well but they managed to somehow get it working.
They seem to use both user in the drawing of the preferences and addon when adding the hotkeys. This doesn’t work for me.

Also using addon doesn’t work for me at all.

1 Like

Hmm, yeah, I may have got the user and active keyconfigs mixed up… I’ll definitely try and have a look at some other addons that do it, and see if I can get it working…

1 Like

Let me know if you figure something out or make a bug report.

I couldn’t get keyconfigs to work to save the hotkey assignment. Whatever I tried - user, active… - nothing worked. I switched to storing the hotkey assignments manually in the preferences and reassigning them every time you start Blender. I am not using the pre-existing keymap UI anymore. I tried to keep it as independent from the blender keyconfigs as possible.

The code provided won’t work at its own, you need to incorporate it into your script/addon. The pie menu I am assigning the hotkey to is also missing from the script.

A simplified version of the keymap python:

# keymap.py
import bpy

keys = []


def add_keymap():
    km = bpy.context.window_manager.keyconfigs.addon.keymaps.new(name='3D View', space_type='VIEW_3D')
    prefs = bpy.context.preferences.addons[__package__.split('.')[
        0]].preferences

    # type, ctrl, shift, alt parameters are stored and retrived from the preferences.
    kmi = km.keymap_items.new(idname='wm.call_menu_pie', type=prefs.collision_pie_type, value='PRESS',
                              ctrl=prefs.collision_pie_ctrl, shift=prefs.collision_pie_shift,
                              alt=prefs.collision_pie_alt)
    add_key_to_keymap("COLLISION_MT_pie_menu", kmi, km, active=prefs.collision_pie_active)

def add_key_to_keymap(idname, kmi, km, active=True):
    ''' Add ta key to the appropriate keymap '''
    kmi.properties.name = idname
    kmi.active = active
    keys.append((km, kmi))


def remove_key(context, idname, properties_name):
    '''Removes addon hotkeys from the keymap'''
    wm = bpy.context.window_manager
    km = wm.keyconfigs.addon.keymaps["Window"]

    for kmi in km.keymap_items:
        if kmi.idname == idname and kmi.properties.name == properties_name:
            km.keymap_items.remove(kmi)


def remove_keymap():
    '''Removes keys from the keymap. Currently this is only called when unregistering the addon. '''
    # only works for menues and pie menus
    for km, kmi in keys:
        # Not sure if this check is even needed
        # properties.name contains the Panel or Menu name
        if hasattr(kmi.properties, 'name') and kmi.properties.name in ['COLLISION_MT_pie_menu']:
            km.keymap_items.remove(kmi)
    keys.clear()



def register():
    # Register classes and initialize properties

    # It's important to register preferences before the add_keymap() function is called
    # Add the keymap
    add_keymap()


def unregister():
    # Unregister classes and remove properties
    remove_keymap()

The basic preferences code related to keymaps and hotkey UI:

#preferences.py
import bpy
from .keymap import remove_key

def add_key(self, km, idname, properties_name, collision_pie_type, collision_pie_ctrl, collision_pie_shift, collision_pie_alt, collision_pie_active):
    # Adds key to keymap
    kmi = km.keymap_items.new(idname=idname, type=collision_pie_type, value='PRESS',
                              ctrl=collision_pie_ctrl, shift=collision_pie_shift, alt=collision_pie_alt)
    kmi.properties.name = properties_name
    kmi.active = collision_pie_active

def update_pie_key(self, context):
    # This functions gets called when the hotkey assignment is updated in the preferences
    wm = bpy.context.window_manager
    km = context.window_manager.keyconfigs.addon.keymaps["3D View"]
    collision_pie_type = self.collision_pie_type.upper()

    # Remove previous key assignment
    remove_key(context, 'wm.call_menu_pie', "COLLISION_MT_pie_menu")
    # Add updated key
    add_key(self, km, 'wm.call_menu_pie', "COLLISION_MT_pie_menu", collision_pie_type,
            self.collision_pie_ctrl, self.collision_pie_shift, self.collision_pie_alt, self.collision_pie_active)
    self.collision_pie_type = collision_pie_type

class BUTTON_OT_change_key(bpy.types.Operator):
    """UI button to assign a new key to a addon hotkey"""
    bl_idname = "collider.key_selection_button"
    bl_label = "Press the button you want to assign to this operation."
    bl_options = {'REGISTER','INTERNAL'}

    # String Properties for assigning the correct hotkey
    menu_id: bpy.props.StringProperty()
    my_event: bpy.props.StringProperty()
    my_type: bpy.props.StringProperty()

    def __init__(self):
        self.my_event = ''

    def invoke(self, context, event):
        self.prefs = context.preferences.addons[__package__.split('.')[0]].preferences
        setattr(self.prefs, f'{self.menu_id}_type', 'NONE')

        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}

    def modal(self, context, event):
        print('modal')
        self.my_event = 'NONE'

        if event.type and event.value=='RELEASE':  # Apply
            self.my_event = event.type
            setattr(self.prefs, f'{self.menu_id}_type', self.my_event)
            self.execute(context)
            return {'FINISHED'}
        return {'RUNNING_MODAL'}

    def execute(self, context):
        self.report({'INFO'}, "Key change: " + bpy.types.Event.bl_rna.properties['type'].enum_items[self.my_event].name)
        return {'FINISHED'}


class REMOVE_OT_hotkey(bpy.types.Operator):
    """Operator to remove the key assignment of a hotkey"""
    bl_idname = "collision.remove_hotkey"
    bl_label = "Remove hotkey"
    bl_description = "Remove hotkey"
    bl_options = {'REGISTER', 'INTERNAL'}

    # String Properties to take the parameters
    idname: bpy.props.StringProperty()
    properties_name: bpy.props.StringProperty()
    property_prefix: bpy.props.StringProperty()

    def execute(self, context):
        remove_key(context, self.idname, self.properties_name)

        prefs = context.preferences.addons[__package__.split('.')[
            0]].preferences
        setattr(prefs, f'{self.property_prefix}_type', "NONE")
        setattr(prefs, f'{self.property_prefix}_ctrl', False)
        setattr(prefs, f'{self.property_prefix}_shift', False)
        setattr(prefs, f'{self.property_prefix}_alt', False)

        return {'FINISHED'}

class CollisionAddonPrefs(bpy.types.AddonPreferences):
    """Addon preferences for Collider Tools"""
    bl_idname = __package__.split('.')[0]
    bl_options = {'REGISTER'}


    collision_pie_type: bpy.props.StringProperty(
        name="Collider Pie Menu",
        default="C",
        update=update_pie_key
    )

    collision_pie_ctrl: bpy.props.BoolProperty(
        name="Ctrl",
        default=True,
        update=update_pie_key
    )

    collision_pie_shift: bpy.props.BoolProperty(
        name="Shift",
        default=True,
        update=update_pie_key
    )
    collision_pie_alt: bpy.props.BoolProperty(
        name="Alt",
        default=False,
        update=update_pie_key
    )

    collision_pie_active: bpy.props.BoolProperty(
        name="Active",
        default=True,
        update=update_pie_key
    )
    def keymap_ui(self, layout, title, property_prefix, id_name, properties_name):
        # basic layout
        box = layout.box()
        split = box.split(align=True, factor=0.5)
        col = split.column()

        # Is hotkey active checkbox
        row = col.row(align=True)
        row.prop(self, f'{property_prefix}_active', text="")
        row.label(text=title)

        # Button to assign the key assignments
        col = split.column()
        row = col.row(align=True)
        key_type = getattr(self, f'{property_prefix}_type')
        text = (
            bpy.types.Event.bl_rna.properties['type'].enum_items[key_type].name
            if key_type != 'NONE'
            else 'Press a key'
        )
        op = row.operator("collider.key_selection_button", text= text)
        op.menu_id = property_prefix
        op = row.operator("collision.remove_hotkey", text="", icon="X")
        op.idname = id_name
        op.properties_name = properties_name
        op.property_prefix = property_prefix

        # Shift, Ctrl, Alt buttons for the hotkey assignment.
        row = col.row(align=True)
        row.prop(self, f'{property_prefix}_ctrl')
        row.prop(self, f'{property_prefix}_shift')
        row.prop(self, f'{property_prefix}_alt')

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

        row = layout.row(align=True)
        row.prop(self, "prefs_tabs", expand=True)

        wm = context.window_manager

        self.keymap_ui(layout, 'Collider Pie Menu', 'collision_pie', 'wm.call_menu_pie', "COLLISION_MT_pie_menu")



I made a video of how the current version looks and works: