feat(sdk): add status bar widget wrapper and origin query bindings (#356)

register_status_widget(): pure Python wrapper that adds a live widget
to the main window status bar with context menu discoverability.

Origin query bindings (kcsdk.list_origins, active_origin, get_origin,
set_active_origin): thin C++ forwarding to OriginManager with Python
wrappers using kcsdk-first routing.

IOriginProvider and IStatusBarProvider C++ interfaces dropped — existing
FileOrigin stack is already complete, and status bar widgets don't need
C++ lifecycle management.
This commit is contained in:
forbes
2026-03-01 14:13:31 -06:00
parent 747c458e23
commit a6a6cefc16
5 changed files with 276 additions and 16 deletions

View File

@@ -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",
]

View File

@@ -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

View File

@@ -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"
)

View File

@@ -24,6 +24,7 @@ target_link_libraries(kcsdk_py
pybind11::module
Python3::Python
KCSDK
FreeCADGui
)
if(FREECAD_WARN_ERROR)

View File

@@ -32,6 +32,9 @@
#include <Gui/SDK/ThemeEngine.h>
#include <Gui/SDK/Types.h>
#include <Gui/FileOrigin.h>
#include <Gui/OriginManager.h>
#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<int>(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<int>(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<std::string>{};
},
"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.");
}