Some checks failed
Build and Test / build (pull_request) Has been cancelled
Replaces the flat context model with a tree-structured hierarchy: - ContextDefinition gains parentId field for declaring parent-child relationships between contexts - Resolver builds a context stack by walking parentId links from leaf to root, verifying each ancestor matches current state - Breadcrumb is now auto-built from the stack — each level contributes its expanded label and color, replacing all hardcoded special cases - EditingContext gains stack field (QStringList, root to leaf) Transition guards (#386): - addTransitionGuard() / removeTransitionGuard() on resolver - Guards run synchronously before applyContext(); first rejection cancels the transition and emits contextTransitionBlocked signal - Full SDK/pybind11/Python bindings Breadcrumb injection (#387): - injectBreadcrumb() / removeBreadcrumbInjection() on resolver - Addons can append segments to any context's breadcrumb display - Active only when the target context is in the current stack - Full SDK/pybind11/Python bindings Built-in parent assignments: - partdesign.body → partdesign.workbench - partdesign.feature → partdesign.body - partdesign.in_assembly → assembly.edit - sketcher.edit → partdesign.body - assembly.idle → assembly.workbench - assembly.edit → assembly.idle - Workbench-level and root contexts have no parent SDK surface: - Types.h: parentId on ContextDef, stack on ContextSnapshot - SDKRegistry: guard/injection delegation, snapshotFromGui helper - kcsdk_py: parent_id param, context_stack(), guard/injection bindings - kindred_sdk: context_stack(), add/remove_transition_guard(), inject/remove_breadcrumb_injection(), parent_id on register_context() Closes #385, closes #386, closes #387
261 lines
8.0 KiB
Python
261 lines
8.0 KiB
Python
"""Editing context and overlay registration wrappers.
|
|
|
|
Routes through the ``kcsdk`` C++ module. The legacy ``FreeCADGui``
|
|
Python bindings are deprecated — kcsdk is required.
|
|
"""
|
|
|
|
import FreeCAD
|
|
|
|
try:
|
|
import kcsdk as _kcsdk
|
|
except ImportError:
|
|
_kcsdk = None
|
|
|
|
|
|
def _require_kcsdk():
|
|
if _kcsdk is None:
|
|
raise RuntimeError(
|
|
"kcsdk module not available. "
|
|
"The kindred_sdk requires the kcsdk C++ module (libKCSDK)."
|
|
)
|
|
|
|
|
|
def register_context(
|
|
context_id, label, color, toolbars, match, priority=50, parent_id=""
|
|
):
|
|
"""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.
|
|
parent_id : str, optional
|
|
Parent context ID for hierarchy. Empty string for root-level.
|
|
"""
|
|
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")
|
|
|
|
_require_kcsdk()
|
|
try:
|
|
_kcsdk.register_context(
|
|
context_id, label, color, toolbars, match, priority, parent_id=parent_id
|
|
)
|
|
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__}")
|
|
|
|
_require_kcsdk()
|
|
try:
|
|
_kcsdk.unregister_context(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")
|
|
|
|
_require_kcsdk()
|
|
try:
|
|
_kcsdk.register_overlay(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__}")
|
|
|
|
_require_kcsdk()
|
|
try:
|
|
_kcsdk.unregister_overlay(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__}")
|
|
|
|
_require_kcsdk()
|
|
try:
|
|
_kcsdk.inject_commands(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``, ``stack``. Returns ``None`` if no context is active.
|
|
"""
|
|
_require_kcsdk()
|
|
try:
|
|
return _kcsdk.current_context()
|
|
except Exception as e:
|
|
FreeCAD.Console.PrintWarning(
|
|
f"kindred_sdk: Failed to get current context: {e}\n"
|
|
)
|
|
return None
|
|
|
|
|
|
def available_contexts():
|
|
"""Return metadata for all registered editing contexts.
|
|
|
|
Returns a list of dicts with keys: ``id``, ``parent_id``,
|
|
``label_template``, ``color``, ``priority``. Sorted by descending
|
|
priority (highest first).
|
|
Returns an empty list on failure.
|
|
"""
|
|
_require_kcsdk()
|
|
try:
|
|
return _kcsdk.available_contexts()
|
|
except Exception as e:
|
|
FreeCAD.Console.PrintWarning(
|
|
f"kindred_sdk: Failed to get available contexts: {e}\n"
|
|
)
|
|
return []
|
|
|
|
|
|
def context_stack():
|
|
"""Return the current context stack as a list of context IDs.
|
|
|
|
The list is ordered root-to-leaf (e.g.
|
|
``["partdesign.workbench", "partdesign.body", "sketcher.edit"]``).
|
|
Returns an empty list if no context is active.
|
|
"""
|
|
_require_kcsdk()
|
|
try:
|
|
return _kcsdk.context_stack()
|
|
except Exception as e:
|
|
FreeCAD.Console.PrintWarning(f"kindred_sdk: Failed to get context stack: {e}\n")
|
|
return []
|
|
|
|
|
|
def add_transition_guard(callback):
|
|
"""Register a transition guard.
|
|
|
|
The *callback* receives ``(from_ctx, to_ctx)`` dicts and must return
|
|
either a ``bool`` or a ``(bool, reason_str)`` tuple.
|
|
|
|
Returns an integer guard ID for later removal via
|
|
:func:`remove_transition_guard`.
|
|
"""
|
|
if not callable(callback):
|
|
raise TypeError("callback must be callable")
|
|
|
|
_require_kcsdk()
|
|
return _kcsdk.add_transition_guard(callback)
|
|
|
|
|
|
def remove_transition_guard(guard_id):
|
|
"""Remove a previously registered transition guard by ID."""
|
|
_require_kcsdk()
|
|
_kcsdk.remove_transition_guard(guard_id)
|
|
|
|
|
|
def inject_breadcrumb(context_id, segments, colors=None):
|
|
"""Inject additional breadcrumb segments into a context.
|
|
|
|
Segments are appended after the context's own label when it appears
|
|
in the current stack.
|
|
|
|
Parameters
|
|
----------
|
|
context_id : str
|
|
Target context identifier.
|
|
segments : list[str]
|
|
Additional breadcrumb segment labels.
|
|
colors : list[str], optional
|
|
Per-segment hex colors. Defaults to the context's own color.
|
|
"""
|
|
if not isinstance(context_id, str):
|
|
raise TypeError(f"context_id must be str, got {type(context_id).__name__}")
|
|
if not isinstance(segments, list):
|
|
raise TypeError(f"segments must be list, got {type(segments).__name__}")
|
|
|
|
_require_kcsdk()
|
|
_kcsdk.inject_breadcrumb(context_id, segments, colors or [])
|
|
|
|
|
|
def remove_breadcrumb_injection(context_id):
|
|
"""Remove a previously injected breadcrumb for a context."""
|
|
if not isinstance(context_id, str):
|
|
raise TypeError(f"context_id must be str, got {type(context_id).__name__}")
|
|
|
|
_require_kcsdk()
|
|
_kcsdk.remove_breadcrumb_injection(context_id)
|
|
|
|
|
|
def refresh_context():
|
|
"""Force re-resolution and update of the editing context."""
|
|
_require_kcsdk()
|
|
try:
|
|
_kcsdk.refresh()
|
|
except Exception as e:
|
|
FreeCAD.Console.PrintWarning(f"kindred_sdk: Failed to refresh context: {e}\n")
|