feat(sdk): add IMenuProvider interface and register_command wrapper (#355)

IMenuProvider: declarative menu placement with optional context awareness.
C++ interface with pybind11 bindings + GIL-safe holder. SDKMenuManipulator
(shared WorkbenchManipulator) injects menu items on workbench switch,
filtered by editing context when context_ids() is non-empty.

register_command(): thin Python wrapper around FreeCADGui.addCommand()
that standardizes the calling convention within the SDK contract.

Python wrappers (kindred_sdk.register_menu, kindred_sdk.register_command)
use kcsdk-first routing with FreeCADGui fallback.
This commit is contained in:
forbes
2026-03-01 13:13:44 -06:00
parent 4eb643a26f
commit 747c458e23
11 changed files with 558 additions and 1 deletions

View File

@@ -1,5 +1,6 @@
# kindred-addon-sdk — stable API for Kindred Create addon integration
from kindred_sdk.command import register_command
from kindred_sdk.compat import create_version, freecad_version
from kindred_sdk.context import (
current_context,
@@ -11,6 +12,7 @@ from kindred_sdk.context import (
unregister_overlay,
)
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.theme import get_theme_tokens, load_palette
from kindred_sdk.toolbar import register_toolbar
@@ -18,6 +20,7 @@ from kindred_sdk.version import SDK_VERSION
__all__ = [
"SDK_VERSION",
"register_command",
"register_context",
"unregister_context",
"register_overlay",
@@ -27,6 +30,7 @@ __all__ = [
"refresh_context",
"get_theme_tokens",
"load_palette",
"register_menu",
"register_toolbar",
"register_origin",
"unregister_origin",

View File

@@ -0,0 +1,58 @@
"""Command registration wrapper.
Provides a standardized SDK entry point for registering FreeCAD commands.
This is a thin wrapper around ``FreeCADGui.addCommand()`` — no C++ interface
is needed since FreeCAD's command system is already stable and well-known.
"""
import FreeCAD
def register_command(name, activated, resources, is_active=None):
"""Register a FreeCAD command through the SDK.
Parameters
----------
name : str
Command name (e.g. ``"MyAddon_DoThing"``).
activated : callable
Called when command is triggered. Receives an optional ``int``
index argument (for group commands).
resources : dict
Command resources passed to ``GetResources()``. Common keys:
``MenuText``, ``ToolTip``, ``Pixmap``, ``Accel``.
is_active : callable, optional
Zero-arg callable returning ``True`` when the command should be
enabled. Default: always active.
"""
if not callable(activated):
raise TypeError("activated must be callable")
if not isinstance(resources, dict):
raise TypeError("resources must be a dict")
if is_active is not None and not callable(is_active):
raise TypeError("is_active must be callable or None")
_resources = dict(resources)
_activated = activated
_is_active = is_active
class _SDKCommand:
def GetResources(self):
return _resources
def Activated(self, index=0):
_activated()
def IsActive(self):
if _is_active is not None:
return bool(_is_active())
return True
try:
import FreeCADGui
FreeCADGui.addCommand(name, _SDKCommand())
except Exception as e:
FreeCAD.Console.PrintWarning(
f"kindred_sdk: Failed to register command '{name}': {e}\n"
)

View File

@@ -0,0 +1,53 @@
"""Menu provider registration.
Wraps the C++ ``kcsdk.register_menu()`` API with a Python fallback
that installs a WorkbenchManipulator to inject menu items.
"""
import FreeCAD
def _kcsdk_available():
"""Return the kcsdk module if available, else None."""
try:
import kcsdk
return kcsdk
except ImportError:
return None
def register_menu(provider):
"""Register a menu provider for declarative menu placement.
When the C++ ``kcsdk`` module is available, delegates to its
``register_menu()`` which installs a shared WorkbenchManipulator
that injects items at the specified menu path.
Falls back to installing a Python WorkbenchManipulator directly.
"""
kcsdk = _kcsdk_available()
if kcsdk is not None:
kcsdk.register_menu(provider)
return
# Fallback: extract data and install a Python manipulator.
menu_path = provider.menu_path()
items = provider.items()
try:
import FreeCADGui
class _SDKMenuManipulator:
def modifyMenuBar(self, menuBar):
menu_items = [
"Separator" if item == "separator" else item for item in items
]
FreeCADGui.addCommand # Just to verify we're in GUI mode
menuBar.appendMenu(menu_path, menu_items)
FreeCADGui.addWorkbenchManipulator(_SDKMenuManipulator())
except Exception as e:
FreeCAD.Console.PrintWarning(
f"kindred_sdk: Failed to register menu '{provider.id()}': {e}\n"
)