Files
blender-portable-repo/extensions/user_default/blenderkit/keymap_utils.py
T
Raincloud 692e200ffe work
save startup blend for animation tab & whatnot
2026-04-08 12:10:18 -06:00

266 lines
8.1 KiB
Python

# ##### 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)