All checks were successful
Build and Test / build (pull_request) Successful in 25m9s
C++ layer: - EditingContextResolver::registeredContexts() returns static metadata (id, labelTemplate, color, priority) for all registered contexts - SDKRegistry::registeredContexts() wraps with Qt-to-std conversion - kcsdk.available_contexts() pybind11 binding returns list of dicts Python layer: - kindred_sdk.available_contexts() wraps kcsdk binding - kindred_sdk.context_history(limit=10) returns recent transitions from a 50-entry ring buffer tracked in lifecycle.py, with timestamps Closes #383
153 lines
4.7 KiB
Python
153 lines
4.7 KiB
Python
# kindred_sdk.lifecycle — context enter/exit callbacks
|
|
#
|
|
# Provides on_context_enter() and on_context_exit() so addons can react
|
|
# to context transitions declaratively. Internally wires up a single
|
|
# C++ callback via kcsdk.on_context_changed() and derives enter/exit
|
|
# events by tracking the previous context.
|
|
#
|
|
# Also emits "context.enter" / "context.exit" on the SDK event bus.
|
|
|
|
import time
|
|
from collections import deque
|
|
|
|
import FreeCAD
|
|
|
|
from kindred_sdk.events import emit as _emit_event
|
|
|
|
_enter_listeners: dict[str, list] = {}
|
|
_exit_listeners: dict[str, list] = {}
|
|
_previous_context_id: str = ""
|
|
_initialized: bool = False
|
|
_history: deque = deque(maxlen=50)
|
|
|
|
|
|
def on_context_enter(context_id, callback):
|
|
"""Subscribe *callback* to fire when *context_id* becomes active.
|
|
|
|
Use ``"*"`` as a wildcard to receive all enter events.
|
|
The callback receives the new context dict (id, label, color, ...).
|
|
|
|
>>> import kindred_sdk as sdk
|
|
>>> sdk.on_context_enter("sketcher.edit", lambda ctx: print("editing sketch"))
|
|
"""
|
|
if not isinstance(context_id, str):
|
|
raise TypeError(f"context_id must be str, got {type(context_id).__name__}")
|
|
if not callable(callback):
|
|
raise TypeError(f"callback must be callable, got {type(callback).__name__}")
|
|
|
|
subs = _enter_listeners.setdefault(context_id, [])
|
|
if callback not in subs:
|
|
subs.append(callback)
|
|
|
|
|
|
def on_context_exit(context_id, callback):
|
|
"""Subscribe *callback* to fire when *context_id* is deactivated.
|
|
|
|
Use ``"*"`` as a wildcard to receive all exit events.
|
|
The callback receives the old context dict (id, label, color, ...).
|
|
|
|
>>> import kindred_sdk as sdk
|
|
>>> sdk.on_context_exit("sketcher.edit", lambda ctx: print("left sketch"))
|
|
"""
|
|
if not isinstance(context_id, str):
|
|
raise TypeError(f"context_id must be str, got {type(context_id).__name__}")
|
|
if not callable(callback):
|
|
raise TypeError(f"callback must be callable, got {type(callback).__name__}")
|
|
|
|
subs = _exit_listeners.setdefault(context_id, [])
|
|
if callback not in subs:
|
|
subs.append(callback)
|
|
|
|
|
|
def context_history(limit=10):
|
|
"""Return recent context transitions, newest first.
|
|
|
|
Each entry is a dict with keys:
|
|
|
|
- ``id`` — the context that was entered
|
|
- ``timestamp`` — ``time.time()`` when the transition occurred
|
|
- ``previous_id`` — the context that was exited (empty string if none)
|
|
|
|
Parameters
|
|
----------
|
|
limit : int, optional
|
|
Maximum entries to return. Default 10. Pass 0 for all (up to 50).
|
|
"""
|
|
entries = list(_history)
|
|
entries.reverse()
|
|
if limit:
|
|
entries = entries[:limit]
|
|
return entries
|
|
|
|
|
|
def _fire_callbacks(listeners, context_id, ctx_dict):
|
|
"""Dispatch to listeners for a specific id and the wildcard."""
|
|
for key in (context_id, "*"):
|
|
subs = listeners.get(key)
|
|
if not subs:
|
|
continue
|
|
for cb in list(subs): # snapshot for re-entrancy safety
|
|
try:
|
|
cb(ctx_dict)
|
|
except Exception as e:
|
|
FreeCAD.Console.PrintWarning(
|
|
f"kindred_sdk: lifecycle callback for '{key}' raised "
|
|
f"{type(e).__name__}: {e}\n"
|
|
)
|
|
|
|
|
|
def _on_context_changed(new_ctx):
|
|
"""Internal callback wired to kcsdk.on_context_changed()."""
|
|
global _previous_context_id
|
|
|
|
new_id = new_ctx.get("id", "")
|
|
if new_id == _previous_context_id:
|
|
return
|
|
|
|
old_id = _previous_context_id
|
|
_previous_context_id = new_id
|
|
|
|
# Build a minimal dict for the exited context (we only have the id).
|
|
if old_id:
|
|
old_ctx = {"id": old_id}
|
|
_fire_callbacks(_exit_listeners, old_id, old_ctx)
|
|
_emit_event("context.exit", old_ctx)
|
|
|
|
if new_id:
|
|
_fire_callbacks(_enter_listeners, new_id, new_ctx)
|
|
_emit_event("context.enter", new_ctx)
|
|
|
|
# Record transition in history ring buffer.
|
|
_history.append(
|
|
{
|
|
"id": new_id,
|
|
"timestamp": time.time(),
|
|
"previous_id": old_id,
|
|
}
|
|
)
|
|
|
|
|
|
def _init_lifecycle():
|
|
"""Wire up the C++ context change signal. Called once from InitGui.py."""
|
|
global _initialized, _previous_context_id
|
|
|
|
if _initialized:
|
|
return
|
|
_initialized = True
|
|
|
|
try:
|
|
import kcsdk
|
|
except ImportError:
|
|
FreeCAD.Console.PrintWarning(
|
|
"kindred_sdk: kcsdk not available, lifecycle callbacks disabled\n"
|
|
)
|
|
return
|
|
|
|
# Seed previous context so the first real change fires correctly.
|
|
current = kcsdk.current_context()
|
|
if current is not None:
|
|
_previous_context_id = current.get("id", "")
|
|
|
|
kcsdk.on_context_changed(_on_context_changed)
|
|
FreeCAD.Console.PrintLog("kindred_sdk: lifecycle callbacks initialized\n")
|