Files
create/mods/sdk/kindred_sdk/context.py
forbes f44aba7388
All checks were successful
Build and Test / build (pull_request) Successful in 29m14s
feat(sdk): migrate editing context API to kcsdk (#351)
Add context/overlay registration, injection, query, and refresh to the
KCSDK C++ library and kcsdk pybind11 module.

New files:
- src/Gui/SDK/Types.h — ContextDef, OverlayDef, ContextSnapshot structs
  (plain C++, no Qt in public API)

Modified:
- src/Gui/SDK/SDKRegistry.h/.cpp — register_context/overlay, unregister,
  inject_commands, current_context, refresh (delegates to
  EditingContextResolver with std↔Qt conversion)
- src/Gui/SDK/CMakeLists.txt — add Types.h, link FreeCADGui
- src/Gui/SDK/bindings/kcsdk_py.cpp — bind all context functions with
  GIL-safe match callable wrapping and dict-based snapshot return
- mods/sdk/kindred_sdk/context.py — try kcsdk first, fall back to
  FreeCADGui for backwards compatibility
2026-02-27 14:05:07 -06:00

182 lines
5.8 KiB
Python

"""Editing context and overlay registration wrappers.
Routes through the ``kcsdk`` C++ module when available, falling back to
the legacy ``FreeCADGui`` Python bindings 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
def _gui():
"""Lazy import of FreeCADGui (not available in console mode)."""
import FreeCADGui
return FreeCADGui
def register_context(context_id, label, color, toolbars, match, priority=50):
"""Register an editing context.
Parameters
----------
context_id : str
Unique identifier (e.g. ``"myaddon.edit"``).
label : str
Display label template. Supports ``{name}`` placeholder.
color : str
Hex color for the breadcrumb (e.g. ``"#f38ba8"``).
toolbars : list[str]
Toolbar names to show when this context is active.
match : callable
Zero-argument callable returning *True* when this context is active.
priority : int, optional
Higher values are checked first. Default 50.
"""
if not isinstance(context_id, str):
raise TypeError(f"context_id must be str, got {type(context_id).__name__}")
if not isinstance(toolbars, list):
raise TypeError(f"toolbars must be list, got {type(toolbars).__name__}")
if not callable(match):
raise TypeError("match must be callable")
try:
if _kcsdk is not None:
_kcsdk.register_context(context_id, label, color, toolbars, match, priority)
else:
_gui().registerEditingContext(
context_id, label, color, toolbars, match, priority
)
except Exception as e:
FreeCAD.Console.PrintWarning(
f"kindred_sdk: Failed to register context '{context_id}': {e}\n"
)
def unregister_context(context_id):
"""Remove a previously registered editing context."""
if not isinstance(context_id, str):
raise TypeError(f"context_id must be str, got {type(context_id).__name__}")
try:
if _kcsdk is not None:
_kcsdk.unregister_context(context_id)
else:
_gui().unregisterEditingContext(context_id)
except Exception as e:
FreeCAD.Console.PrintWarning(
f"kindred_sdk: Failed to unregister context '{context_id}': {e}\n"
)
def register_overlay(overlay_id, toolbars, match):
"""Register an editing overlay.
Overlays add toolbars to whatever context is currently active when
*match* returns True.
Parameters
----------
overlay_id : str
Unique overlay identifier.
toolbars : list[str]
Toolbar names to append.
match : callable
Zero-argument callable returning *True* when the overlay applies.
"""
if not isinstance(overlay_id, str):
raise TypeError(f"overlay_id must be str, got {type(overlay_id).__name__}")
if not isinstance(toolbars, list):
raise TypeError(f"toolbars must be list, got {type(toolbars).__name__}")
if not callable(match):
raise TypeError("match must be callable")
try:
if _kcsdk is not None:
_kcsdk.register_overlay(overlay_id, toolbars, match)
else:
_gui().registerEditingOverlay(overlay_id, toolbars, match)
except Exception as e:
FreeCAD.Console.PrintWarning(
f"kindred_sdk: Failed to register overlay '{overlay_id}': {e}\n"
)
def unregister_overlay(overlay_id):
"""Remove a previously registered editing overlay."""
if not isinstance(overlay_id, str):
raise TypeError(f"overlay_id must be str, got {type(overlay_id).__name__}")
try:
if _kcsdk is not None:
_kcsdk.unregister_overlay(overlay_id)
else:
_gui().unregisterEditingOverlay(overlay_id)
except Exception as e:
FreeCAD.Console.PrintWarning(
f"kindred_sdk: Failed to unregister overlay '{overlay_id}': {e}\n"
)
def inject_commands(context_id, toolbar_name, commands):
"""Inject commands into a context's toolbar.
Parameters
----------
context_id : str
Target context identifier.
toolbar_name : str
Toolbar within that context.
commands : list[str]
Command names to add.
"""
if not isinstance(context_id, str):
raise TypeError(f"context_id must be str, got {type(context_id).__name__}")
if not isinstance(toolbar_name, str):
raise TypeError(f"toolbar_name must be str, got {type(toolbar_name).__name__}")
if not isinstance(commands, list):
raise TypeError(f"commands must be list, got {type(commands).__name__}")
try:
if _kcsdk is not None:
_kcsdk.inject_commands(context_id, toolbar_name, commands)
else:
_gui().injectEditingCommands(context_id, toolbar_name, commands)
except Exception as e:
FreeCAD.Console.PrintWarning(
f"kindred_sdk: Failed to inject commands into '{context_id}': {e}\n"
)
def current_context():
"""Return the current editing context as a dict.
Keys: ``id``, ``label``, ``color``, ``toolbars``, ``breadcrumb``,
``breadcrumbColors``. Returns ``None`` if no context is active.
"""
try:
if _kcsdk is not None:
return _kcsdk.current_context()
return _gui().currentEditingContext()
except Exception as e:
FreeCAD.Console.PrintWarning(
f"kindred_sdk: Failed to get current context: {e}\n"
)
return None
def refresh_context():
"""Force re-resolution and update of the editing context."""
try:
if _kcsdk is not None:
_kcsdk.refresh()
else:
_gui().refreshEditingContext()
except Exception as e:
FreeCAD.Console.PrintWarning(f"kindred_sdk: Failed to refresh context: {e}\n")