# ##### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### from __future__ import annotations import logging from dataclasses import dataclass, field import bpy import rna_keymap_ui bk_logger = logging.getLogger(__name__) @dataclass class KeyMapItemDef: """Description of a keymap item we want to register.""" idname: str type: str value: str shift: bool = False ctrl: bool = False alt: bool = False oskey: bool = False key_modifier: str = "NONE" properties: dict[str, object] = field(default_factory=dict) @dataclass class KeyMapDef: """Description of a keymap with its items.""" name: str space_type: str region_type: str = "WINDOW" items: list[KeyMapItemDef] = field(default_factory=list) # Default bindings we ship. Users can edit these in Preferences. DEFAULT_KEYMAP_ITEMS: list[KeyMapItemDef] = [ KeyMapItemDef( idname="view3d.run_assetbar_fix_context", type="SEMI_COLON", value="PRESS", properties={"keep_running": False, "do_search": False}, ), KeyMapItemDef( idname="wm.blenderkit_menu_rating_upload", type="R", value="PRESS", ), ] DEFAULT_KEYMAPS: list[KeyMapDef] = [ # Register into the standard "Window" keymap so Blender shows it in the main tree. KeyMapDef( name="Window", # must be windows otherwise blender will not show it in the default keymap space_type="EMPTY", region_type="WINDOW", items=DEFAULT_KEYMAP_ITEMS, ), ] # Store only the keymap items we create so we can clean them up without touching user overrides. _registered_keymaps: list[ tuple[bpy.types.KeyConfig, bpy.types.KeyMap, bpy.types.KeyMapItem] ] = [] def _keymap_has_item(km: bpy.types.KeyMap, idname: str) -> bpy.types.KeyMapItem | None: for item in km.keymap_items: if item.idname == idname: return item return None def _find_in_keyconfig( keyconfig: bpy.types.KeyConfig, idname: str ) -> bpy.types.KeyMapItem | None: for km in keyconfig.keymaps: kmi = _keymap_has_item(km, idname) if kmi: return kmi return None def register_keymaps(custom_keymaps: list[KeyMapDef] | None = None) -> None: """Register keymaps for the add-on. Args: custom_keymaps: Optional iterable of KeyMapDef if callers want to override defaults. """ wm = bpy.context.window_manager if not wm: bk_logger.warning("Unable to register keymaps: no window manager available") return kc_addon = wm.keyconfigs.addon kc_user = wm.keyconfigs.user if not kc_addon: bk_logger.warning("Unable to register keymaps: no add-on keyconfig available") return bk_logger.debug("Registering keymaps for BlenderKit add-on") keymaps = list(custom_keymaps) if custom_keymaps is not None else DEFAULT_KEYMAPS for km_def in keymaps: # If the user already has a custom binding in their keyconfig, don't recreate it. if kc_user and _find_in_keyconfig(kc_user, km_def.items[0].idname): bk_logger.debug( f"User keyconfig already has binding for {km_def.items[0].idname}; leaving user override intact" ) continue km = kc_addon.keymaps.find( km_def.name, space_type=km_def.space_type, region_type=km_def.region_type ) if km is None: bk_logger.debug( f"Keymap {km_def.name} not found in {kc_addon.name}, creating new one" ) km = kc_addon.keymaps.new( name=km_def.name, space_type=km_def.space_type, region_type=km_def.region_type, ) for item_def in km_def.items: if _keymap_has_item(km, item_def.idname): bk_logger.debug( f"Keymap {km_def.name} in {kc_addon.name} already has item {item_def.idname}, skipping" ) continue bk_logger.debug( f"Adding keymap item {item_def.idname} to keymap {km_def.name} in {kc_addon.name}" ) kmi = km.keymap_items.new( idname=item_def.idname, type=item_def.type, value=item_def.value, shift=item_def.shift, ctrl=item_def.ctrl, alt=item_def.alt, oskey=item_def.oskey, key_modifier=item_def.key_modifier, ) for prop_name, prop_value in item_def.properties.items(): bk_logger.debug( f"Setting property {prop_name}={prop_value} on keymap item {item_def.idname} in {kc_addon.name}" ) setattr(kmi.properties, prop_name, prop_value) _registered_keymaps.append((kc_addon, km, kmi)) def unregister_keymaps() -> None: if not _registered_keymaps: return for kc, km, kmi in _registered_keymaps: try: km.keymap_items.remove(kmi) except RuntimeError: # Already removed by user; ignore. pass _registered_keymaps.clear() def get_keymap_item(idname: str) -> bpy.types.KeyMapItem | None: """Return the current keymap item for the given operator. Prefers the user's key configuration (where edits are stored) and falls back to the add-on keyconfig defaults. """ wm = bpy.context.window_manager if not wm: return None for cfg in (wm.keyconfigs.user, wm.keyconfigs.addon): if cfg: kmi = _find_in_keyconfig(cfg, idname) if kmi: return kmi return None def format_keymap_item(kmi: bpy.types.KeyMapItem) -> str: """Return a human readable shortcut label for a KeyMapItem.""" parts = [] if kmi.ctrl: parts.append("Ctrl") if kmi.alt: parts.append("Alt") if kmi.shift: parts.append("Shift") if kmi.oskey: parts.append("Cmd") if kmi.key_modifier and kmi.key_modifier != "NONE": parts.append(kmi.key_modifier.replace("_", " ").title()) parts.append(kmi.type.replace("_", " ").title()) return "+".join(parts) def get_shortcut_label(idname: str, fallback: str = "") -> str: """Return a formatted shortcut string for the operator if available.""" kmi = get_keymap_item(idname) if not kmi: return fallback return format_keymap_item(kmi) def _find_km_and_kmi( keyconfig: bpy.types.KeyConfig, idname: str ) -> tuple[bpy.types.KeyMap, bpy.types.KeyMapItem] | None: if not keyconfig: return None for km in keyconfig.keymaps: kmi = _keymap_has_item(km, idname) if kmi: return km, kmi return None def draw_keymap(self, context): layout = self.layout wm = context.window_manager kc_addon = wm.keyconfigs.addon kc_user = wm.keyconfigs.user if not kc_addon: return box = layout.box() box.label(text="BlenderKit Keymaps") for item_def in DEFAULT_KEYMAP_ITEMS: # Prefer user override if available, otherwise show addon default. entry = _find_km_and_kmi(kc_user, item_def.idname) if kc_user else None source_kc = kc_user if entry else kc_addon km, kmi = ( entry if entry else _find_km_and_kmi(kc_addon, item_def.idname) or (None, None) ) if not km or not kmi: continue rna_keymap_ui.draw_kmi([], source_kc, km, kmi, box, 0)