Parent match() functions are designed for flat priority resolution and
may not return true when a more specific child is active (e.g.
assembly.idle checks getActivePartObject() which may not return the
assembly during edit mode). The parentId declaration is the author's
assertion of structural containment — the leaf already matched, so
we walk the full chain unconditionally.
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
Add kindred_sdk.addon_diagnostics() returning per-addon load state,
timing, and error info as a list of dicts. Reads name, state,
load_time_ms, and error from AddonManifest via AddonRegistry.
Add _print_load_summary() to addon_loader.py that prints a formatted
summary table to the console after each load phase (Init.py and
InitGui.py), replacing interleaved individual log lines with a
consolidated view.
Closes#390
Add kindred_sdk.addon_resource(name, relative_path) that resolves
bundled asset paths relative to an addon's root directory. Looks up
the addon manifest from AddonRegistry for the install root, joins
the relative path, and validates existence on disk.
Raises LookupError if the addon is not registered, FileNotFoundError
if the resolved path does not exist.
Closes#389
C++ layer:
- EditingContextResolver::registeredContexts() returns static metadata
(id, labelTemplate, color, priority) for all registered contexts
- SDKRegistry::registeredContexts() wraps with Qt-to-std conversion
- kcsdk.available_contexts() pybind11 binding returns list of dicts
Python layer:
- kindred_sdk.available_contexts() wraps kcsdk binding
- kindred_sdk.context_history(limit=10) returns recent transitions
from a 50-entry ring buffer tracked in lifecycle.py, with timestamps
Closes#383
Expose EditingContextResolver::contextChanged to Python addons via
two-layer design:
C++ layer:
- SDKRegistry::onContextChanged() stores callbacks and lazily connects
to the Qt signal on first registration
- pybind11 binding kcsdk.on_context_changed() wraps Python callables
with GIL-safe invocation
Python layer:
- kindred_sdk.on_context_enter(context_id, callback) subscribes to
context activation ("*" wildcard supported)
- kindred_sdk.on_context_exit(context_id, callback) subscribes to
context deactivation
- Internal tracking of previous context derives enter/exit transitions
- Emits context.enter / context.exit on the SDK event bus
Closes#381
New module kindred_sdk/events.py provides lightweight publish-subscribe
so addons can signal each other without direct imports.
New public API:
- sdk.on(event, handler) — subscribe to a named event
- sdk.off(event, handler) — unsubscribe
- sdk.emit(event, data) — publish event with dict payload
Pure Python, synchronous dispatch, snapshot-safe iteration.
Handlers that raise are logged and skipped without breaking the chain.
New module kindred_sdk/registry.py wraps FreeCAD.KindredAddons so
addons use a stable SDK import instead of reaching into FreeCAD
internals directly.
New public API:
- sdk.is_addon_loaded(name) — boolean check
- sdk.addon_version(name) — version string or None
- sdk.loaded_addons() — list of loaded addon names in load order
Add .mailmap to parent repo and all submodules (silo, gears, datums,
solver) to normalize historical author/committer display.
Maps stale identities (forbes-0023, josephforbes23, Zoe Forbes, admin)
and old emails to: forbes <contact@kindred-systems.com>
rattler-build's source.path copy does not include submodule contents —
directories like mods/silo/, mods/solver/, mods/gears/, and mods/datums/
end up empty in the work directory, causing cmake --install to fail
when it tries to install files from those paths.
Add a clone_if_empty helper to build.sh that detects empty submodule
directories and shallow-clones them from their remotes before CMake
runs. Add git to build requirements in recipe.yaml.
This fixes the release build (AppImage) failure where cmake --install
failed at the silo addon install step after successfully installing
the silo tree-node icons from src/Mod/Create/resources/.
Convert mods/datums/ from tracked files to a git submodule pointing
to https://git.kindred-systems.com/kindred/datums.git (branch: main).
Follows the same submodule pattern as silo, solver, and gears.
The datums remote repo was initialized from the existing tracked files
with the package.xml repository URL updated to point to the new repo.
CMake install targets in src/Mod/Create/CMakeLists.txt continue to
work unchanged since the files remain at the same paths.
Update silo submodule to include root-level package.xml, Init.py, and
InitGui.py following the standard addon layout used by sdk, gears,
datums, and solver.
Move five Silo-specific deferred setup functions from
src/Mod/Create/InitGui.py into the silo addon's own InitGui.py:
- Document observer registration (600ms)
- Auth dock panel via kindred_sdk.register_dock_panel() (2000ms)
- First-start settings check (3000ms)
- Activity dock panel via kindred_sdk.register_dock_panel() (4000ms)
- Remove duplicate origin registration (was 1500ms, already done in
SiloWorkbench.Initialize())
Update CMake install targets to include root-level silo files.
Create core InitGui.py now only handles kc_format (500ms) and
update_checker (10000ms).
- Bump SDK_VERSION to 1.0.0 in version.py and package.xml
- Remove <pure_python> tag from package.xml (kcsdk is C++)
- Remove all FreeCADGui.* fallback paths in context.py, dock.py,
toolbar.py, menu.py; require kcsdk module
- Remove query fallbacks in origin.py (keep register/unregister
which still need FreeCADGui.addOrigin/removeOrigin)
- Add deprecation warnings to 11 superseded FreeCADGui methods
in ApplicationPy.cpp (not addOrigin/removeOrigin)
- All 39 Tier 1 tests pass
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.
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.
EditingContextResolver constructor did not call refresh(), leaving
d->current as a default empty EditingContext. When BreadcrumbToolBar
queried currentContext() on creation, it received an empty context
with no breadcrumb segments, causing the navbar to appear blank.
Add refresh() at end of constructor so the initial state is resolved
before any View3DInventor queries it.
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
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.