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:
@@ -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",
|
||||
|
||||
58
mods/sdk/kindred_sdk/command.py
Normal file
58
mods/sdk/kindred_sdk/command.py
Normal 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"
|
||||
)
|
||||
53
mods/sdk/kindred_sdk/menu.py
Normal file
53
mods/sdk/kindred_sdk/menu.py
Normal 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"
|
||||
)
|
||||
Reference in New Issue
Block a user