diff --git a/mods/sdk/kindred_sdk/__init__.py b/mods/sdk/kindred_sdk/__init__.py index 43b678b371..410e18b31d 100644 --- a/mods/sdk/kindred_sdk/__init__.py +++ b/mods/sdk/kindred_sdk/__init__.py @@ -13,28 +13,41 @@ from kindred_sdk.context import ( ) from kindred_sdk.dock import register_dock_panel from kindred_sdk.menu import register_menu -from kindred_sdk.origin import register_origin, unregister_origin +from kindred_sdk.origin import ( + active_origin, + get_origin, + list_origins, + register_origin, + set_active_origin, + unregister_origin, +) +from kindred_sdk.statusbar import register_status_widget from kindred_sdk.theme import get_theme_tokens, load_palette from kindred_sdk.toolbar import register_toolbar from kindred_sdk.version import SDK_VERSION __all__ = [ "SDK_VERSION", + "active_origin", + "create_version", + "current_context", + "freecad_version", + "get_origin", + "get_theme_tokens", + "inject_commands", + "list_origins", + "load_palette", + "refresh_context", "register_command", "register_context", - "unregister_context", - "register_overlay", - "unregister_overlay", - "inject_commands", - "current_context", - "refresh_context", - "get_theme_tokens", - "load_palette", - "register_menu", - "register_toolbar", - "register_origin", - "unregister_origin", "register_dock_panel", - "create_version", - "freecad_version", + "register_menu", + "register_origin", + "register_status_widget", + "register_toolbar", + "set_active_origin", + "unregister_context", + "unregister_origin", + "unregister_overlay", + "register_overlay", ] diff --git a/mods/sdk/kindred_sdk/origin.py b/mods/sdk/kindred_sdk/origin.py index 02bb953c3b..8d19eda1d4 100644 --- a/mods/sdk/kindred_sdk/origin.py +++ b/mods/sdk/kindred_sdk/origin.py @@ -1,12 +1,22 @@ -"""FileOrigin registration wrappers. +"""FileOrigin registration and query wrappers. Wraps ``FreeCADGui.addOrigin()`` / ``removeOrigin()`` with validation and error handling. Addons implement the FileOrigin duck-typed interface directly (see Silo's ``SiloOrigin`` for the full contract). + +Query functions (``list_origins``, ``active_origin``, etc.) route +through the ``kcsdk`` C++ module when available, falling back to +``FreeCADGui.*`` for backwards compatibility. """ import FreeCAD +# Try to import the C++ SDK module; None if not yet built/installed. +try: + import kcsdk as _kcsdk +except ImportError: + _kcsdk = None + _REQUIRED_METHODS = ("id", "name", "type", "ownsDocument") @@ -40,3 +50,96 @@ def unregister_origin(origin): FreeCAD.Console.PrintLog(f"kindred_sdk: Unregistered origin '{origin.id()}'\n") except Exception as e: FreeCAD.Console.PrintWarning(f"kindred_sdk: Failed to unregister origin: {e}\n") + + +def list_origins(): + """Return IDs of all registered origins. + + Returns + ------- + list[str] + Origin IDs (always includes ``"local"``). + """ + if _kcsdk is not None: + try: + return _kcsdk.list_origins() + except Exception: + pass + try: + return _gui().listOrigins() + except Exception as e: + FreeCAD.Console.PrintWarning(f"kindred_sdk: list_origins failed: {e}\n") + return [] + + +def active_origin(): + """Return info about the currently active origin. + + Returns + ------- + dict or None + Origin info dict with keys: ``id``, ``name``, ``nickname``, + ``type``, ``tracksExternally``, ``requiresAuthentication``, + ``supportsRevisions``, ``supportsBOM``, ``supportsPartNumbers``, + ``connectionState``. + """ + if _kcsdk is not None: + try: + return _kcsdk.active_origin() + except Exception: + pass + try: + return _gui().activeOrigin() + except Exception as e: + FreeCAD.Console.PrintWarning(f"kindred_sdk: active_origin failed: {e}\n") + return None + + +def set_active_origin(origin_id): + """Set the active origin by ID. + + Parameters + ---------- + origin_id : str + The origin ID to activate. + + Returns + ------- + bool + True if the origin was found and activated. + """ + if _kcsdk is not None: + try: + return _kcsdk.set_active_origin(origin_id) + except Exception: + pass + try: + return _gui().setActiveOrigin(origin_id) + except Exception as e: + FreeCAD.Console.PrintWarning(f"kindred_sdk: set_active_origin failed: {e}\n") + return False + + +def get_origin(origin_id): + """Get info about a specific origin by ID. + + Parameters + ---------- + origin_id : str + The origin ID to look up. + + Returns + ------- + dict or None + Origin info dict, or None if not found. + """ + if _kcsdk is not None: + try: + return _kcsdk.get_origin(origin_id) + except Exception: + pass + try: + return _gui().getOrigin(origin_id) + except Exception as e: + FreeCAD.Console.PrintWarning(f"kindred_sdk: get_origin failed: {e}\n") + return None diff --git a/mods/sdk/kindred_sdk/statusbar.py b/mods/sdk/kindred_sdk/statusbar.py new file mode 100644 index 0000000000..6ce153e8f6 --- /dev/null +++ b/mods/sdk/kindred_sdk/statusbar.py @@ -0,0 +1,74 @@ +"""Status bar widget registration wrapper. + +Provides a standardized SDK entry point for adding widgets to the main +window's status bar. This is a pure Python wrapper — no C++ interface +is needed since the widget is created once and Qt manages its lifecycle. +""" + +import FreeCAD + + +def register_status_widget(object_name, label, widget, position="right"): + """Add a widget to the main window status bar. + + Parameters + ---------- + object_name : str + Qt object name for duplicate prevention. + label : str + Display name shown in the status bar's right-click context menu + (becomes the widget's ``windowTitle``). + widget : QWidget + The widget instance to add. The caller keeps its own reference + for live updates (e.g. connecting signals, updating text). + position : str, optional + ``"left"`` (stretches) or ``"right"`` (permanent, fixed width). + Default ``"right"``. + """ + if not isinstance(object_name, str) or not object_name: + raise TypeError("object_name must be a non-empty string") + if not isinstance(label, str) or not label: + raise TypeError("label must be a non-empty string") + if position not in ("left", "right"): + raise ValueError(f"position must be 'left' or 'right', got {position!r}") + + try: + import FreeCADGui + from PySide import QtWidgets + + mw = FreeCADGui.getMainWindow() + if mw is None: + FreeCAD.Console.PrintWarning( + "kindred_sdk: Main window not available, " + f"cannot register status widget '{object_name}'\n" + ) + return + + sb = mw.statusBar() + + # Duplicate check + for child in sb.children(): + if ( + isinstance(child, QtWidgets.QWidget) + and child.objectName() == object_name + ): + FreeCAD.Console.PrintLog( + f"kindred_sdk: Status widget '{object_name}' already exists, skipping\n" + ) + return + + widget.setObjectName(object_name) + widget.setWindowTitle(label) + + if position == "left": + sb.addWidget(widget) + else: + sb.addPermanentWidget(widget) + + FreeCAD.Console.PrintLog( + f"kindred_sdk: Registered status widget '{object_name}'\n" + ) + except Exception as e: + FreeCAD.Console.PrintWarning( + f"kindred_sdk: Failed to register status widget '{object_name}': {e}\n" + ) diff --git a/src/Gui/SDK/bindings/CMakeLists.txt b/src/Gui/SDK/bindings/CMakeLists.txt index 7916a76025..98c917eaeb 100644 --- a/src/Gui/SDK/bindings/CMakeLists.txt +++ b/src/Gui/SDK/bindings/CMakeLists.txt @@ -24,6 +24,7 @@ target_link_libraries(kcsdk_py pybind11::module Python3::Python KCSDK + FreeCADGui ) if(FREECAD_WARN_ERROR) diff --git a/src/Gui/SDK/bindings/kcsdk_py.cpp b/src/Gui/SDK/bindings/kcsdk_py.cpp index 8bd556abc7..c4de50f23c 100644 --- a/src/Gui/SDK/bindings/kcsdk_py.cpp +++ b/src/Gui/SDK/bindings/kcsdk_py.cpp @@ -32,6 +32,9 @@ #include #include +#include +#include + #include "PyIMenuProvider.h" #include "PyIPanelProvider.h" #include "PyIToolbarProvider.h" @@ -47,6 +50,23 @@ using namespace KCSDK; namespace { +/// Build a Python dict from a FileOrigin* (same keys as ApplicationPy). +py::dict originToDict(Gui::FileOrigin* origin) +{ + py::dict d; + d["id"] = origin->id(); + d["name"] = origin->name(); + d["nickname"] = origin->nickname(); + d["type"] = static_cast(origin->type()); + d["tracksExternally"] = origin->tracksExternally(); + d["requiresAuthentication"] = origin->requiresAuthentication(); + d["supportsRevisions"] = origin->supportsRevisions(); + d["supportsBOM"] = origin->supportsBOM(); + d["supportsPartNumbers"] = origin->supportsPartNumbers(); + d["connectionState"] = static_cast(origin->connectionState()); + return d; +} + /// Convert a ContextSnapshot to a Python dict (same keys as ApplicationPy). py::dict contextSnapshotToDict(const ContextSnapshot& snap) { @@ -369,4 +389,53 @@ PYBIND11_MODULE(kcsdk, m) }, py::arg("name") = "catppuccin-mocha", "Load a named palette. Returns True on success."); + + // -- Origin query API --------------------------------------------------- + + m.def("list_origins", + []() { + auto* mgr = Gui::OriginManager::instance(); + return mgr ? mgr->originIds() : std::vector{}; + }, + "Return IDs of all registered origins."); + + m.def("active_origin", + []() -> py::object { + auto* mgr = Gui::OriginManager::instance(); + if (!mgr) { + return py::none(); + } + Gui::FileOrigin* origin = mgr->currentOrigin(); + if (!origin) { + return py::none(); + } + return originToDict(origin); + }, + "Return the active origin as a dict, or None."); + + m.def("set_active_origin", + [](const std::string& id) { + auto* mgr = Gui::OriginManager::instance(); + if (!mgr) { + return false; + } + return mgr->setCurrentOrigin(id); + }, + py::arg("id"), + "Set the active origin by ID. Returns True on success."); + + m.def("get_origin", + [](const std::string& id) -> py::object { + auto* mgr = Gui::OriginManager::instance(); + if (!mgr) { + return py::none(); + } + Gui::FileOrigin* origin = mgr->getOrigin(id); + if (!origin) { + return py::none(); + } + return originToDict(origin); + }, + py::arg("id"), + "Get origin info by ID as a dict, or None if not found."); }