oscillode/src/blender_maxwell/registration.py

197 lines
6.2 KiB
Python

"""Manages the registration of Blender classes, including delayed registrations that require access to Python dependencies.
Attributes:
BL_KEYMAP: Addon-specific keymap used to register operator hotkeys. REG__CLASSES: Currently registered Blender classes.
REG__KEYMAP_ITEMS: Currently registered Blender keymap items.
DELAYED_REGISTRATIONS: Currently pending registration operations, which can be realized with `run_delayed_registration()`.
EVENT__DEPS_SATISFIED: A constant representing a semantic choice of key for `DELAYED_REGISTRATIONS`.
"""
import typing as typ
from pathlib import Path
import bpy
from .nodeps.utils import simple_logger
log = simple_logger.get(__name__)
# TODO: More types for these things!
DelayedRegKey: typ.TypeAlias = str
BLClass: typ.TypeAlias = (
bpy.types.Panel
| bpy.types.UIList
| bpy.types.Menu
| bpy.types.Header
| bpy.types.Operator
| bpy.types.KeyingSetInfo
| bpy.types.RenderEngine
| bpy.types.AssetShelf
| bpy.types.FileHandler
)
BLKeymapItem: typ.TypeAlias = typ.Any ## TODO: Better Type
KeymapItemDef: typ.TypeAlias = typ.Any ## TODO: Better Type
####################
# - Globals
####################
BL_KEYMAP: bpy.types.KeyMap | None = None
REG__CLASSES: list[BLClass] = []
REG__KEYMAP_ITEMS: list[BLKeymapItem] = []
DELAYED_REGISTRATIONS: dict[DelayedRegKey, typ.Callable[[Path], None]] = {}
####################
# - Delayed Registration Keys
####################
EVENT__DEPS_SATISFIED: DelayedRegKey = 'on_deps_satisfied'
####################
# - Class Registration
####################
def register_classes(bl_register: list[BLClass]) -> None:
"""Registers a Blender class, allowing it to hook into relevant Blender features.
Caches registered classes in the module global `REG__CLASSES`.
Parameters:
bl_register: List of Blender classes to register.
"""
log.info('Registering %s Classes', len(bl_register))
for cls in bl_register:
if cls.bl_idname in REG__CLASSES:
msg = f'Skipping register of {cls.bl_idname}'
log.info(msg)
continue
log.debug(
'Registering Class %s',
repr(cls),
)
bpy.utils.register_class(cls)
REG__CLASSES.append(cls)
def unregister_classes() -> None:
"""Unregisters all previously registered Blender classes.
All previously registered Blender classes can be found in the module global variable `REG__CLASSES`.
"""
log.info('Unregistering %s Classes', len(REG__CLASSES))
for cls in reversed(REG__CLASSES):
log.debug(
'Unregistering Class %s',
repr(cls),
)
bpy.utils.unregister_class(cls)
REG__CLASSES.clear()
####################
# - Keymap Registration
####################
def register_keymap_items(keymap_item_defs: list[dict]):
# Lazy-Load BL_NODE_KEYMAP
global BL_KEYMAP # noqa: PLW0603
if BL_KEYMAP is None:
BL_KEYMAP = bpy.context.window_manager.keyconfigs.addon.keymaps.new(
name='Node Editor',
space_type='NODE_EDITOR',
)
log.info(
'Registered Keymap %s',
str(BL_KEYMAP),
)
# Register Keymaps
log.info('Registering %s Keymap Items', len(keymap_item_defs))
for keymap_item_def in keymap_item_defs:
keymap_item = BL_KEYMAP.keymap_items.new(
*keymap_item_def['_'],
ctrl=keymap_item_def['ctrl'],
shift=keymap_item_def['shift'],
alt=keymap_item_def['alt'],
)
log.debug(
'Registered Keymap Item %s with spec %s',
repr(keymap_item),
keymap_item_def,
)
REG__KEYMAP_ITEMS.append(keymap_item)
def unregister_keymap_items():
global BL_KEYMAP # noqa: PLW0603
# Unregister Keymaps
log.info('Unregistering %s Keymap Items', len(REG__KEYMAP_ITEMS))
for keymap_item in reversed(REG__KEYMAP_ITEMS):
log.debug(
'Unregistered Keymap Item %s',
repr(keymap_item),
)
BL_KEYMAP.keymap_items.remove(keymap_item)
# Lazy-Unload BL_NODE_KEYMAP
if BL_KEYMAP is not None:
log.info(
'Unregistered Keymap %s',
repr(BL_KEYMAP),
)
REG__KEYMAP_ITEMS.clear()
BL_KEYMAP = None
####################
# - Delayed Registration Semantics
####################
def delay_registration(
delayed_reg_key: DelayedRegKey,
classes_cb: typ.Callable[[Path], list[BLClass]],
keymap_item_defs_cb: typ.Callable[[Path], list[KeymapItemDef]],
) -> None:
"""Delays the registration of Blender classes that depend on certain Python dependencies, for which neither the location nor validity is yet known.
The function that registers is stored in the module global `DELAYED_REGISTRATIONS`, indexed by `delayed_reg_key`.
Once the PyDeps location and validity is determined, `run_delayed_registration()` can be used as a shorthand for accessing `DELAYED_REGISTRATIONS[delayed_reg_key]`.
Parameters:
delayed_reg_key: The identifier with which to index the registration callback.
Module-level constants like `EVENT__DEPS_SATISFIED` are a good choice.
classes_cb: A function that takes a `sys.path`-compatible path to Python dependencies needed by the Blender classes in question, and returns a list of Blender classes to import.
`register_classes()` will be used to actually register the returned Blender classes.
keymap_item_defs_cb: Similar, except for addon keymap items.
Returns:
A function that takes a `sys.path`-compatible path to the Python dependencies needed to import the given Blender classes.
"""
if delayed_reg_key in DELAYED_REGISTRATIONS:
msg = f'Already delayed a registration with key {delayed_reg_key}'
raise ValueError(msg)
def register_cb(path_pydeps: Path):
log.info(
'Running Delayed Registration (key %s) with PyDeps: %s',
delayed_reg_key,
path_pydeps,
)
register_classes(classes_cb(path_pydeps))
register_keymap_items(keymap_item_defs_cb(path_pydeps))
DELAYED_REGISTRATIONS[delayed_reg_key] = register_cb
def run_delayed_registration(delayed_reg_key: DelayedRegKey, path_pydeps: Path) -> None:
"""Run a delayed registration, by using `delayed_reg_key` to lookup the correct path, passing `path_pydeps` to the registration.
Parameters:
delayed_reg_key: The identifier with which to index the registration callback.
Must match the parameter with which the delayed registration was first declared.
path_pydeps: The `sys.path`-compatible path to the Python dependencies that the classes need to have available in order to register.
"""
register_cb = DELAYED_REGISTRATIONS.pop(delayed_reg_key)
register_cb(path_pydeps)