From 2f89f8cbb0ffe30dcbc4c17a9544d63197f8ede3 Mon Sep 17 00:00:00 2001 From: forbes-0023 Date: Thu, 5 Mar 2026 10:32:12 -0600 Subject: [PATCH] docs: add example addon template (#395) - 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 --- docs/examples/example-addon/Init.py | 12 +++ docs/examples/example-addon/InitGui.py | 93 +++++++++++++++++++ docs/examples/example-addon/README.md | 75 +++++++++++++++ .../example-addon/example_addon/__init__.py | 1 + .../example-addon/example_addon/commands.py | 29 ++++++ .../example-addon/example_addon/panel.py | 21 +++++ docs/examples/example-addon/package.xml | 27 ++++++ docs/src/SUMMARY.md | 4 + docs/src/development/writing-an-addon.md | 2 + 9 files changed, 264 insertions(+) create mode 100644 docs/examples/example-addon/Init.py create mode 100644 docs/examples/example-addon/InitGui.py create mode 100644 docs/examples/example-addon/README.md create mode 100644 docs/examples/example-addon/example_addon/__init__.py create mode 100644 docs/examples/example-addon/example_addon/commands.py create mode 100644 docs/examples/example-addon/example_addon/panel.py create mode 100644 docs/examples/example-addon/package.xml diff --git a/docs/examples/example-addon/Init.py b/docs/examples/example-addon/Init.py new file mode 100644 index 0000000000..bc52dd0b4f --- /dev/null +++ b/docs/examples/example-addon/Init.py @@ -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") diff --git a/docs/examples/example-addon/InitGui.py b/docs/examples/example-addon/InitGui.py new file mode 100644 index 0000000000..3c1b6ec423 --- /dev/null +++ b/docs/examples/example-addon/InitGui.py @@ -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) diff --git a/docs/examples/example-addon/README.md b/docs/examples/example-addon/README.md new file mode 100644 index 0000000000..3bed07a473 --- /dev/null +++ b/docs/examples/example-addon/README.md @@ -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 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 `` 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 `sdk`** — 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 diff --git a/docs/examples/example-addon/example_addon/__init__.py b/docs/examples/example-addon/example_addon/__init__.py new file mode 100644 index 0000000000..c8c05c4264 --- /dev/null +++ b/docs/examples/example-addon/example_addon/__init__.py @@ -0,0 +1 @@ +"""Example addon for Kindred Create.""" diff --git a/docs/examples/example-addon/example_addon/commands.py b/docs/examples/example-addon/example_addon/commands.py new file mode 100644 index 0000000000..80b39706b9 --- /dev/null +++ b/docs/examples/example-addon/example_addon/commands.py @@ -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") diff --git a/docs/examples/example-addon/example_addon/panel.py b/docs/examples/example-addon/example_addon/panel.py new file mode 100644 index 0000000000..207b0c0bb6 --- /dev/null +++ b/docs/examples/example-addon/example_addon/panel.py @@ -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 diff --git a/docs/examples/example-addon/package.xml b/docs/examples/example-addon/package.xml new file mode 100644 index 0000000000..6f7a28e8de --- /dev/null +++ b/docs/examples/example-addon/package.xml @@ -0,0 +1,27 @@ + + + example-addon + Example addon template for Kindred Create + 0.1.0 + Your Name + LGPL-2.1-or-later + https://example.com/your-addon + + + + + ExampleAddonProvider + example_addon + + + + + + 0.1.0 + 80 + + sdk + + + diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 22c0cab703..6fda4f806a 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -30,6 +30,10 @@ - [Writing an Addon](./development/writing-an-addon.md) - [Testing](./development/testing.md) +# Examples + +- [Example Addon Template](../examples/example-addon/README.md) + # Silo Server - [Specification](./silo-server/SPECIFICATION.md) diff --git a/docs/src/development/writing-an-addon.md b/docs/src/development/writing-an-addon.md index 3c1b32f4f5..497fbccdfa 100644 --- a/docs/src/development/writing-an-addon.md +++ b/docs/src/development/writing-an-addon.md @@ -2,6 +2,8 @@ This guide walks through creating a Kindred Create addon from scratch. Addons are Python packages in the `mods/` directory that extend Create with commands, panels, and UI modifications through the SDK. +> **Quick start:** A complete working example is available at [`docs/examples/example-addon/`](../../examples/example-addon/README.md). Copy the directory into `mods/` and rename it to start building. + ## Addon structure A minimal addon has this layout: