Merge pull request 'feat(sdk): context introspection — available_contexts and context_history (#383)' (#400) from feat/sdk-context-introspection into main
Some checks failed
Build and Test / build (push) Has been cancelled

Reviewed-on: #400
This commit was merged in pull request #400.
This commit is contained in:
2026-03-04 19:44:02 +00:00
8 changed files with 125 additions and 1 deletions

View File

@@ -3,6 +3,7 @@
from kindred_sdk.command import register_command
from kindred_sdk.compat import create_version, freecad_version
from kindred_sdk.context import (
available_contexts,
current_context,
inject_commands,
refresh_context,
@@ -13,7 +14,7 @@ from kindred_sdk.context import (
)
from kindred_sdk.dock import register_dock_panel
from kindred_sdk.events import emit, off, on
from kindred_sdk.lifecycle import on_context_enter, on_context_exit
from kindred_sdk.lifecycle import context_history, on_context_enter, on_context_exit
from kindred_sdk.menu import register_menu
from kindred_sdk.origin import (
active_origin,
@@ -33,6 +34,8 @@ __all__ = [
"SDK_VERSION",
"active_origin",
"addon_version",
"available_contexts",
"context_history",
"create_version",
"current_context",
"emit",

View File

@@ -157,6 +157,23 @@ def current_context():
return None
def available_contexts():
"""Return metadata for all registered editing contexts.
Returns a list of dicts with keys: ``id``, ``label_template``,
``color``, ``priority``. Sorted by descending priority (highest first).
Returns an empty list on failure.
"""
_require_kcsdk()
try:
return _kcsdk.available_contexts()
except Exception as e:
FreeCAD.Console.PrintWarning(
f"kindred_sdk: Failed to get available contexts: {e}\n"
)
return []
def refresh_context():
"""Force re-resolution and update of the editing context."""
_require_kcsdk()

View File

@@ -7,6 +7,9 @@
#
# 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
@@ -15,6 +18,7 @@ _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):
@@ -55,6 +59,27 @@ def on_context_exit(context_id, callback):
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, "*"):
@@ -92,6 +117,15 @@ def _on_context_changed(new_ctx):
_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."""

View File

@@ -599,6 +599,16 @@ EditingContext EditingContextResolver::currentContext() const
return d->current;
}
QList<EditingContextResolver::ContextInfo> EditingContextResolver::registeredContexts() const
{
QList<ContextInfo> result;
result.reserve(d->contexts.size());
for (const auto& def : d->contexts) {
result.append({def.id, def.labelTemplate, def.color, def.priority});
}
return result;
}
// ---------------------------------------------------------------------------
// Breadcrumb building

View File

@@ -103,6 +103,19 @@ public:
EditingContext currentContext() const;
/// Static metadata for a registered context definition.
struct ContextInfo
{
QString id;
QString labelTemplate;
QString color;
int priority;
};
/// Return static metadata for all registered context definitions.
/// Sorted by descending priority (highest first).
QList<ContextInfo> registeredContexts() const;
Q_SIGNALS:
void contextChanged(const EditingContext& ctx);

View File

@@ -167,6 +167,22 @@ ContextSnapshot SDKRegistry::currentContext() const
return snap;
}
std::vector<SDKRegistry::ContextInfo> SDKRegistry::registeredContexts() const
{
auto guiContexts =
Gui::EditingContextResolver::instance()->registeredContexts();
std::vector<ContextInfo> result;
result.reserve(guiContexts.size());
for (const auto& c : guiContexts) {
result.push_back({fromQString(c.id),
fromQString(c.labelTemplate),
fromQString(c.color),
c.priority});
}
return result;
}
void SDKRegistry::refresh()
{
Gui::EditingContextResolver::instance()->refresh();

View File

@@ -89,6 +89,19 @@ public:
/// Return a snapshot of the current editing context.
ContextSnapshot currentContext() const;
/// Static metadata for a registered context definition.
struct ContextInfo
{
std::string id;
std::string labelTemplate;
std::string color;
int priority;
};
/// Return static metadata for all registered editing contexts.
/// Sorted by descending priority (highest first).
std::vector<ContextInfo> registeredContexts() const;
/// Force re-resolution of the editing context.
void refresh();

View File

@@ -209,6 +209,24 @@ PYBIND11_MODULE(kcsdk, m)
},
"Force re-resolution of the editing context.");
m.def("available_contexts",
[]() {
auto contexts = SDKRegistry::instance().registeredContexts();
py::list result;
for (const auto& c : contexts) {
py::dict d;
d["id"] = c.id;
d["label_template"] = c.labelTemplate;
d["color"] = c.color;
d["priority"] = c.priority;
result.append(d);
}
return result;
},
"Return metadata for all registered editing contexts.\n\n"
"Each entry is a dict with keys: id, label_template, color, priority.\n"
"Sorted by descending priority (highest first).");
m.def("on_context_changed",
[](py::function callback) {
auto held = std::make_shared<py::object>(std::move(callback));