docs: add example addon template (#395)
All checks were successful
Build and Test / build (pull_request) Successful in 30m3s

- Create docs/examples/example-addon/ with a complete, copy-paste-ready
  addon skeleton demonstrating command registration, context injection,
  dock panels, lifecycle hooks, and event bus subscription
- Add Examples section to docs/src/SUMMARY.md
- Add quick-start cross-reference in writing-an-addon.md
This commit is contained in:
2026-03-05 10:32:12 -06:00
parent c5881147d0
commit 2f89f8cbb0
9 changed files with 264 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
"""Example addon — console initialization.
This file runs in both console and GUI mode. Use it for non-GUI setup
such as registering custom property types or document observers that
should work without a display.
Most addons only need a log line here. Heavy setup belongs in InitGui.py.
"""
import FreeCAD
FreeCAD.Console.PrintLog("example-addon: console init\n")

View File

@@ -0,0 +1,93 @@
"""Example addon — GUI initialization.
Demonstrates the standard addon bootstrap pattern using the Kindred SDK.
Each registration step is wrapped in try/except and deferred with
QTimer.singleShot to avoid blocking the startup sequence.
"""
def _register_commands():
"""Register commands and inject into existing contexts."""
try:
from example_addon.commands import register_commands
register_commands()
except Exception as e:
import FreeCAD
FreeCAD.Console.PrintWarning(f"example-addon: command registration failed: {e}\n")
# Inject our command into an existing context's toolbar so it appears
# automatically when the user enters that context.
try:
from kindred_sdk import inject_commands
inject_commands("partdesign.body", "Part Design", ["ExampleAddon_Hello"])
except Exception as e:
import FreeCAD
FreeCAD.Console.PrintWarning(f"example-addon: context injection failed: {e}\n")
def _register_panel():
"""Register a dock panel with deferred creation."""
try:
from kindred_sdk import register_dock_panel
from example_addon.panel import create_panel
register_dock_panel(
"ExampleAddonPanel",
"Example Panel",
create_panel,
area="right",
delay_ms=2000,
)
except Exception as e:
import FreeCAD
FreeCAD.Console.PrintWarning(f"example-addon: panel registration failed: {e}\n")
def _register_lifecycle():
"""Subscribe to context lifecycle events.
Use ``on_context_enter`` / ``on_context_exit`` to react when the user
enters or leaves a specific editing context. Pass ``"*"`` to match
all contexts.
"""
try:
from kindred_sdk import on_context_enter, on_context_exit
on_context_enter("*", lambda ctx: print(f"[example-addon] entered: {ctx['id']}"))
on_context_exit("*", lambda ctx: print(f"[example-addon] exited: {ctx['id']}"))
except Exception as e:
import FreeCAD
FreeCAD.Console.PrintWarning(f"example-addon: lifecycle hooks failed: {e}\n")
def _register_events():
"""Subscribe to event bus events.
The event bus lets addons communicate without direct imports.
Use ``kindred_sdk.on(event, handler)`` to listen and
``kindred_sdk.emit(event, data)`` to publish.
"""
try:
from kindred_sdk import on
on("document.saved", lambda data: print(f"[example-addon] document saved: {data}"))
except Exception as e:
import FreeCAD
FreeCAD.Console.PrintWarning(f"example-addon: event subscription failed: {e}\n")
# Deferred initialization — stagger delays to avoid blocking startup.
# Lower delays run first; keep each step fast.
from PySide6.QtCore import QTimer
QTimer.singleShot(500, _register_commands)
QTimer.singleShot(600, _register_panel)
QTimer.singleShot(700, _register_lifecycle)
QTimer.singleShot(800, _register_events)

View File

@@ -0,0 +1,75 @@
# Example Addon Template
A minimal but complete Kindred Create addon that demonstrates all major SDK integration points. Copy this directory into `mods/`, rename it, and start building.
## File Structure
```
example-addon/
├── package.xml Addon manifest with <kindred> extensions
├── Init.py Console-mode bootstrap (runs in all modes)
├── InitGui.py GUI bootstrap — deferred registration
└── example_addon/
├── __init__.py Python package marker
├── commands.py FreeCAD command via kindred_sdk.register_command()
└── panel.py Dock panel widget factory
```
## What This Template Demonstrates
| Feature | File | SDK function |
|---------|------|-------------|
| Addon manifest with version bounds and dependencies | `package.xml` | — |
| Console bootstrap | `Init.py` | — |
| Deferred GUI initialization with `QTimer.singleShot` | `InitGui.py` | — |
| Command registration | `commands.py` | `kindred_sdk.register_command()` |
| Injecting commands into existing contexts | `InitGui.py` | `kindred_sdk.inject_commands()` |
| Dock panel registration | `InitGui.py`, `panel.py` | `kindred_sdk.register_dock_panel()` |
| Context lifecycle hooks | `InitGui.py` | `kindred_sdk.on_context_enter()` |
| Event bus subscription | `InitGui.py` | `kindred_sdk.on()` |
## Installation (for testing)
1. Copy or symlink this directory into the `mods/` folder at the repository root:
```bash
cp -r docs/examples/example-addon mods/example-addon
```
2. Launch Kindred Create:
```bash
pixi run freecad
```
3. Open the Python console (View > Panels > Python console) and verify:
```
example-addon: console init
```
4. The "Example Panel" dock widget should appear on the right after ~2 seconds.
5. To test the command, create a Part Design Body and look for "Hello World" in the Part Design toolbar.
## Customizing
1. **Rename** — change `example-addon` and `example_addon` to your addon's name in all files
2. **Update `package.xml`** — set your name, description, repository URL, and load priority
3. **Add commands** — create more functions in `commands.py` and register them
4. **Add contexts** — use `kindred_sdk.register_context()` for custom editing modes
5. **Add overlays** — use `kindred_sdk.register_overlay()` for conditional toolbars
6. **Theme colors** — use `kindred_sdk.get_theme_tokens()` for Catppuccin Mocha palette access
## Common Pitfalls
- **Missing `<workbench>` tag** — `InitGui.py` will not be loaded without it, even if you don't register a workbench
- **Import at module level** — avoid importing `kindred_sdk` or `PySide6` at the top of `InitGui.py`; use deferred imports inside functions to avoid load-order issues
- **Blocking startup** — keep each `QTimer.singleShot` callback fast; do heavy work in background threads
- **Forgetting `<dependency>sdk</dependency>`** — your addon may load before the SDK if you omit this
## Further Reading
- [Writing an Addon](../../src/development/writing-an-addon.md) — full tutorial
- [Package.xml Schema](../../src/development/package-xml-schema.md) — manifest reference
- [KCSDK Python API](../../src/reference/kcsdk-python.md) — complete API reference

View File

@@ -0,0 +1 @@
"""Example addon for Kindred Create."""

View File

@@ -0,0 +1,29 @@
"""Example addon commands.
Each command is registered via ``kindred_sdk.register_command()`` which
wraps FreeCAD's ``Gui.addCommand()`` with input validation.
"""
from kindred_sdk import register_command
def register_commands():
"""Register all commands for this addon."""
register_command(
name="ExampleAddon_Hello",
activated=_on_hello,
resources={
"MenuText": "Hello World",
"ToolTip": "Show a greeting in the console",
# "Pixmap": "path/to/icon.svg", # optional icon
# "Accel": "Ctrl+Shift+H", # optional shortcut
},
is_active=lambda: True,
)
def _on_hello():
"""Command handler — prints a greeting to the FreeCAD console."""
import FreeCAD
FreeCAD.Console.PrintMessage("Hello from example-addon!\n")

View File

@@ -0,0 +1,21 @@
"""Example addon dock panel.
The factory function is passed to ``kindred_sdk.register_dock_panel()``
and called once after the configured delay to create the widget.
"""
def create_panel():
"""Create the example dock panel widget.
Must return a QWidget. The widget is embedded in a QDockWidget and
managed by FreeCAD's DockWindowManager.
"""
from PySide6 import QtWidgets
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout(widget)
layout.addWidget(QtWidgets.QLabel("Example Addon Panel"))
layout.addWidget(QtWidgets.QLabel("Replace this with your content."))
layout.addStretch()
return widget

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
<name>example-addon</name>
<description>Example addon template for Kindred Create</description>
<version>0.1.0</version>
<maintainer email="you@example.com">Your Name</maintainer>
<license file="LICENSE">LGPL-2.1-or-later</license>
<url type="repository">https://example.com/your-addon</url>
<content>
<!-- A <workbench> tag is required for InitGui.py to be loaded,
even if the addon does not register an actual workbench. -->
<workbench>
<classname>ExampleAddonProvider</classname>
<subdirectory>example_addon</subdirectory>
</workbench>
</content>
<!-- Kindred-specific extensions — ignored by stock FreeCAD. -->
<kindred>
<min_create_version>0.1.0</min_create_version>
<load_priority>80</load_priority>
<dependencies>
<dependency>sdk</dependency>
</dependencies>
</kindred>
</package>