diff --git a/mods/sdk/kindred_sdk/__init__.py b/mods/sdk/kindred_sdk/__init__.py index 0026ff1705..0100622b9d 100644 --- a/mods/sdk/kindred_sdk/__init__.py +++ b/mods/sdk/kindred_sdk/__init__.py @@ -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", diff --git a/mods/sdk/kindred_sdk/context.py b/mods/sdk/kindred_sdk/context.py index fd61884fa9..5c79fcb516 100644 --- a/mods/sdk/kindred_sdk/context.py +++ b/mods/sdk/kindred_sdk/context.py @@ -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() diff --git a/mods/sdk/kindred_sdk/lifecycle.py b/mods/sdk/kindred_sdk/lifecycle.py index 2d2383a808..bdb83def1f 100644 --- a/mods/sdk/kindred_sdk/lifecycle.py +++ b/mods/sdk/kindred_sdk/lifecycle.py @@ -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.""" diff --git a/src/Gui/EditingContext.cpp b/src/Gui/EditingContext.cpp index a64726c9e3..c1220467a1 100644 --- a/src/Gui/EditingContext.cpp +++ b/src/Gui/EditingContext.cpp @@ -599,6 +599,16 @@ EditingContext EditingContextResolver::currentContext() const return d->current; } +QList EditingContextResolver::registeredContexts() const +{ + QList 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 diff --git a/src/Gui/EditingContext.h b/src/Gui/EditingContext.h index 2e415f051b..4dda0c1d9b 100644 --- a/src/Gui/EditingContext.h +++ b/src/Gui/EditingContext.h @@ -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 registeredContexts() const; + Q_SIGNALS: void contextChanged(const EditingContext& ctx); diff --git a/src/Gui/SDK/SDKRegistry.cpp b/src/Gui/SDK/SDKRegistry.cpp index a898034def..2a533c0ada 100644 --- a/src/Gui/SDK/SDKRegistry.cpp +++ b/src/Gui/SDK/SDKRegistry.cpp @@ -167,6 +167,22 @@ ContextSnapshot SDKRegistry::currentContext() const return snap; } +std::vector SDKRegistry::registeredContexts() const +{ + auto guiContexts = + Gui::EditingContextResolver::instance()->registeredContexts(); + + std::vector 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(); diff --git a/src/Gui/SDK/SDKRegistry.h b/src/Gui/SDK/SDKRegistry.h index 2a9a1cde4a..59dba86ca8 100644 --- a/src/Gui/SDK/SDKRegistry.h +++ b/src/Gui/SDK/SDKRegistry.h @@ -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 registeredContexts() const; + /// Force re-resolution of the editing context. void refresh(); diff --git a/src/Gui/SDK/bindings/kcsdk_py.cpp b/src/Gui/SDK/bindings/kcsdk_py.cpp index f36da35562..0e288a3d84 100644 --- a/src/Gui/SDK/bindings/kcsdk_py.cpp +++ b/src/Gui/SDK/bindings/kcsdk_py.cpp @@ -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(std::move(callback));