diff --git a/oscillode/__init__.py b/oscillode/__init__.py
index caa112f..98cbea7 100644
--- a/oscillode/__init__.py
+++ b/oscillode/__init__.py
@@ -16,7 +16,10 @@
"""A visual DSL for electromagnetic simulation design and analysis implemented as a Blender node editor."""
-# from . import assets, node_trees, operators, preferences, registration
+from functools import reduce
+
+# from . import node_trees, operators, preferences, registration
+from . import assets, preferences, registration
from . import contracts as ct
from .utils import logger
@@ -32,6 +35,17 @@ BL_REGISTER: list[ct.BLClass] = [
# *node_trees.BL_REGISTER,
]
+BL_HANDLERS: ct.BLHandlers = reduce(
+ lambda a, b: a + b,
+ [
+ assets.BL_HANDLERS,
+ # *operators.BL_HANDLERS,
+ # *assets.BL_HANDLERS,
+ # *node_trees.BL_HANDLERS,
+ ],
+ ct.BLHandlers(),
+)
+
BL_HOTKEYS: list[ct.KeymapItemDef] = [
# *operators.BL_HOTKEYS,
# *assets.BL_HOTKEYS,
@@ -64,6 +78,7 @@ def register() -> None:
log.info('Registering Addon: %s', ct.addon.NAME)
registration.register_classes(BL_REGISTER)
+ registration.register_handlers(BL_HANDLERS)
registration.register_hotkeys(BL_HOTKEYS)
log.info('Finished Registration of Addon: %s', ct.addon.NAME)
@@ -83,7 +98,8 @@ def unregister() -> None:
"""
log.info('Starting %s Unregister', ct.addon.NAME)
- registration.unregister_classes()
registration.unregister_hotkeys()
+ registration.unregister_handlers()
+ registration.unregister_classes()
log.info('Finished %s Unregister', ct.addon.NAME)
diff --git a/oscillode/assets/__init__.py b/oscillode/assets/__init__.py
index 2ba4717..d4c2f1d 100644
--- a/oscillode/assets/__init__.py
+++ b/oscillode/assets/__init__.py
@@ -14,17 +14,30 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from functools import reduce
+
+import oscillode.contracts as ct
+
from . import geonodes
-BL_REGISTER = [
+BL_REGISTER: list[ct.BLClass] = [
*geonodes.BL_REGISTER,
]
-BL_HOTKEYS = [
+BL_HANDLERS: ct.BLHandlers = reduce(
+ lambda a, b: a + b,
+ [
+ geonodes.BL_HANDLERS,
+ ],
+ ct.BLHandlers(),
+)
+
+BL_HOTKEYS: list[ct.KeymapItemDef] = [
*geonodes.BL_HOTKEYS,
]
__all__ = [
'BL_REGISTER',
+ 'BL_HANDLERS',
'BL_HOTKEYS',
]
diff --git a/oscillode/assets/geonodes.py b/oscillode/assets/geonodes.py
index fb7ac37..e6fbce7 100644
--- a/oscillode/assets/geonodes.py
+++ b/oscillode/assets/geonodes.py
@@ -579,11 +579,11 @@ def initialize_asset_libraries(_: bpy.types.Scene):
)
-bpy.app.handlers.load_pre.append(initialize_asset_libraries)
-
BL_REGISTER = [
NodeAssetPanel,
GeoNodesToStructureNode,
]
-BL_HOTKEYS = []
+BL_HANDLERS: ct.BLHandlers = ct.BLHandlers(load_pre=(initialize_asset_libraries,))
+
+BL_HOTKEYS: list[ct.KeymapItemDef] = []
diff --git a/oscillode/contracts/__init__.py b/oscillode/contracts/__init__.py
index 620cd55..58f65d4 100644
--- a/oscillode/contracts/__init__.py
+++ b/oscillode/contracts/__init__.py
@@ -39,6 +39,7 @@ from .bl import (
PropName,
SocketName,
)
+from .bl_handlers import BLHandlers
from .icons import Icon
from .mobj_types import ManagedObjType
from .node_tree_types import (
@@ -74,6 +75,7 @@ __all__ = [
'PresetName',
'PropName',
'SocketName',
+ 'BLHandlers',
'Icon',
'BLInstance',
'InstanceID',
diff --git a/oscillode/contracts/bl_handlers.py b/oscillode/contracts/bl_handlers.py
new file mode 100644
index 0000000..914a580
--- /dev/null
+++ b/oscillode/contracts/bl_handlers.py
@@ -0,0 +1,122 @@
+# oscillode
+# Copyright (C) 2024 oscillode Project Contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+"""Declares types for working with `bpy.app.handlers` callbacks."""
+
+import typing as typ
+
+import bpy
+import pydantic as pyd
+
+from oscillode.utils.staticproperty import staticproperty
+
+BLHandler = typ.Callable[[], None]
+BLHandlerWithFile = typ.Callable[[str], None]
+BLHandlerWithRenderStats = typ.Callable[[typ.Any], None]
+
+
+class BLHandlers(pyd.BaseModel):
+ """Contains lists of handlers associated with this addon."""
+
+ animation_playback_post: tuple[BLHandler, ...] = ()
+ animation_playback_pre: tuple[BLHandler, ...] = ()
+ annotation_post: tuple[BLHandler, ...] = ()
+ annotation_pre: tuple[BLHandler, ...] = ()
+ composite_cancel: tuple[BLHandler, ...] = ()
+ composite_post: tuple[BLHandler, ...] = ()
+ composite_pre: tuple[BLHandler, ...] = ()
+ depsgraph_update_post: tuple[BLHandler, ...] = ()
+ depsgraph_update_pre: tuple[BLHandler, ...] = ()
+ frame_change_post: tuple[BLHandler, ...] = ()
+ frame_change_pre: tuple[BLHandler, ...] = ()
+ load_factory_preferences_post: tuple[BLHandler, ...] = ()
+ load_factory_startup_post: tuple[BLHandler, ...] = ()
+ load_post: tuple[BLHandlerWithFile, ...] = ()
+ load_post_fail: tuple[BLHandlerWithFile, ...] = ()
+ load_pre: tuple[BLHandlerWithFile, ...] = ()
+ object_bake_cancel: tuple[BLHandler, ...] = ()
+ object_bake_complete: tuple[BLHandler, ...] = ()
+ object_bake_pre: tuple[BLHandler, ...] = ()
+ redo_post: tuple[BLHandler, ...] = ()
+ redo_pre: tuple[BLHandler, ...] = ()
+ render_cancel: tuple[BLHandler, ...] = ()
+ render_complete: tuple[BLHandler, ...] = ()
+ render_init: tuple[BLHandler, ...] = ()
+ render_post: tuple[BLHandler, ...] = ()
+ render_pre: tuple[BLHandler, ...] = ()
+ render_stats: tuple[BLHandler, ...] = ()
+
+ ####################
+ # - Properties
+ ####################
+ @staticproperty # type: ignore[arg-type]
+ def hander_categories() -> tuple[str, ...]: # type: ignore[misc]
+ """Returns an immutable string sequence of handler categories."""
+ return (
+ 'animation_playback_post',
+ 'animation_playback_pre',
+ 'annotation_post',
+ 'annotation_pre',
+ 'composite_cancel',
+ 'composite_post',
+ 'composite_pre',
+ 'depsgraph_update_post',
+ 'depsgraph_update_pre',
+ 'frame_change_post',
+ 'frame_change_pre',
+ 'load_factory_preferences_post',
+ 'load_factory_startup_post',
+ 'load_post',
+ 'load_post_fail',
+ 'load_pre',
+ 'object_bake_cancel',
+ 'object_bake_complete',
+ 'object_bake_pre',
+ 'redo_post',
+ 'redo_pre',
+ 'render_cancel',
+ 'render_complete',
+ 'render_init',
+ 'render_post',
+ 'render_pre',
+ 'render_stats',
+ )
+
+ ####################
+ # - Merging
+ ####################
+ def __add__(self, other: typ.Self) -> typ.Self:
+ """Concatenate the handlers of two `BLHandlers` objects."""
+ return BLHandlers(
+ **{
+ hndl_cat: getattr(self, hndl_cat) + getattr(self, hndl_cat)
+ for hndl_cat in self.handler_categories
+ }
+ )
+
+ def register(self) -> None:
+ """Registers all handlers declared by-category."""
+ for handler_category in BLHandlers.handler_categories:
+ for handler in getattr(self, handler_category):
+ getattr(bpy.app.handlers, handler_category).append(handler)
+
+ def unregister(self) -> None:
+ """Unregisters only this addon's handlers from bpy.app.handlers."""
+ for handler_category in BLHandlers.handler_categories:
+ for handler in getattr(self, handler_category):
+ bpy_handlers = getattr(bpy.app.handlers, handler_category)
+ if handler in bpy_handlers:
+ bpy_handlers.remove(handler)
diff --git a/oscillode/registration.py b/oscillode/registration.py
index f1fd812..b12e4f5 100644
--- a/oscillode/registration.py
+++ b/oscillode/registration.py
@@ -33,9 +33,12 @@ log = logger.get(__name__)
# - Globals
####################
_REGISTERED_CLASSES: list[ct.BLClass] = []
+
_ADDON_KEYMAP: bpy.types.KeyMap | None = None
_REGISTERED_HOTKEYS: list[ct.BLKeymapItem] = []
+_REGISTERED_HANDLERS: ct.BLHandlers | None = None
+
####################
# - Class Registration
@@ -74,6 +77,35 @@ def unregister_classes() -> None:
_REGISTERED_CLASSES.clear()
+####################
+# - Handler Registration
+####################
+def register_handlers(bl_handlers: ct.BLHandlers) -> None:
+ """Register the given Blender handlers."""
+ global _REGISTERED_HANDLERS # noqa: PLW0603
+
+ log.info('Registering BLHandlers') ## TODO: More information
+ if _REGISTERED_HANDLERS is None:
+ bl_handlers.register()
+ _REGISTERED_HANDLERS = bl_handlers
+
+ msg = 'There are already BLHandlers registered; they must be unregistered before a new set can be registered.'
+ raise ValueError(msg)
+
+
+def unregister_handlers() -> None:
+ """Unregister this addon's registered Blender handlers."""
+ global _REGISTERED_HANDLERS # noqa: PLW0603
+
+ log.info('Unregistering BLHandlers') ## TODO: More information
+ if _REGISTERED_HANDLERS is not None:
+ _REGISTERED_HANDLERS.register()
+ _REGISTERED_HANDLERS = None
+
+ msg = 'There are no BLHandlers registered; therefore, there is nothing to register.'
+ raise ValueError(msg)
+
+
####################
# - Keymap Registration
####################
diff --git a/pyproject.toml b/pyproject.toml
index 69f2446..be42db3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -193,6 +193,12 @@ ignore = [
"E501", # Let Formatter Worry about Line Length
]
+[tool.ruff.lint.per-file-ignores]
+"tests/*" = [
+ "D100", # It's okay to not have module-level docstrings in test modules.
+ "D104", # Same for packages.
+]
+
####################
# - Tooling: Ruff Sublinters
####################