@@ -116,6 +116,17 @@ mods/silo/ [submodule → silo-mod.git] FreeCAD workbench
|
|||||||
├── silo_commands.py Commands + FreeCADSiloSettings adapter
|
├── silo_commands.py Commands + FreeCADSiloSettings adapter
|
||||||
└── silo_origin.py FileOrigin backend for Silo (via SDK)
|
└── silo_origin.py FileOrigin backend for Silo (via SDK)
|
||||||
|
|
||||||
|
src/Gui/SDK/ KCSDK C++ shared library (libKCSDK.so)
|
||||||
|
├── KCSDKGlobal.h DLL export macros
|
||||||
|
├── Types.h Plain C++ types (ContextDef, DockArea, PanelPersistence)
|
||||||
|
├── IPanelProvider.h Abstract dock panel interface
|
||||||
|
├── WidgetBridge.h/.cpp PySide QWidget <-> C++ QWidget* (via Gui::PythonWrapper)
|
||||||
|
├── SDKRegistry.h/.cpp Singleton registry — contexts, panels, providers
|
||||||
|
└── bindings/ pybind11 module (kcsdk.so)
|
||||||
|
├── kcsdk_py.cpp Module definition — enums, functions, classes
|
||||||
|
├── PyIPanelProvider.h Trampoline for Python subclassing
|
||||||
|
└── PyProviderHolder.h GIL-safe forwarding wrapper
|
||||||
|
|
||||||
src/Gui/EditingContext.h/.cpp EditingContextResolver singleton + context registry
|
src/Gui/EditingContext.h/.cpp EditingContextResolver singleton + context registry
|
||||||
src/Gui/BreadcrumbToolBar.h/.cpp Color-coded breadcrumb toolbar (Catppuccin Mocha)
|
src/Gui/BreadcrumbToolBar.h/.cpp Color-coded breadcrumb toolbar (Catppuccin Mocha)
|
||||||
src/Gui/FileOrigin.h/.cpp FileOrigin base class + LocalFileOrigin
|
src/Gui/FileOrigin.h/.cpp FileOrigin base class + LocalFileOrigin
|
||||||
|
|||||||
@@ -167,6 +167,52 @@ Theme colors are now centralized in the SDK's YAML palette (`mods/sdk/kindred_sd
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Phase 7: KCSDK — C++-backed SDK module -- IN PROGRESS
|
||||||
|
|
||||||
|
**Goal:** Replace the pure-Python SDK wrappers with a C++ shared library (`libKCSDK.so`) and pybind11 bindings (`kcsdk.so`). This gives addons a stable, typed API with proper GIL safety and enables future C++ addon development without Python.
|
||||||
|
|
||||||
|
**Architecture:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Python Addons (silo, future addons, ...)
|
||||||
|
|
|
||||||
|
kindred_sdk (mods/sdk/) <- convenience layer (try kcsdk, fallback FreeCADGui)
|
||||||
|
|
|
||||||
|
kcsdk.so (pybind11 module) <- C++ API bindings
|
||||||
|
|
|
||||||
|
KCSDK (C++ shared library) <- SDKRegistry + provider interfaces
|
||||||
|
|
|
||||||
|
FreeCADGui (EditingContextResolver, DockWindowManager, OriginManager, ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sub-phases:**
|
||||||
|
|
||||||
|
| # | Issue | Status | Description |
|
||||||
|
|---|-------|--------|-------------|
|
||||||
|
| 1 | #350 | DONE | Scaffold KCSDK library + kcsdk pybind11 module |
|
||||||
|
| 2 | #351 | DONE | Migrate editing context API to kcsdk |
|
||||||
|
| 3 | #352 | DONE | Panel provider system (IPanelProvider) |
|
||||||
|
| 4 | #353 | — | C++ theme engine |
|
||||||
|
| 5 | #354 | — | Toolbar provider system (IToolbarProvider) |
|
||||||
|
| 6 | #355 | — | Menu and action system |
|
||||||
|
| 7 | #356 | — | Status bar provider + origin migration |
|
||||||
|
| 8 | #357 | — | Deprecation cleanup + SDK v1.0.0 |
|
||||||
|
|
||||||
|
**Key files:**
|
||||||
|
|
||||||
|
- `src/Gui/SDK/` — C++ library (KCSDKGlobal.h, Types.h, SDKRegistry, IPanelProvider, WidgetBridge)
|
||||||
|
- `src/Gui/SDK/bindings/` — pybind11 module (kcsdk_py.cpp, PyIPanelProvider, PyProviderHolder)
|
||||||
|
- `mods/sdk/kindred_sdk/` — Python wrappers with kcsdk/legacy fallback
|
||||||
|
|
||||||
|
**Design decisions:**
|
||||||
|
|
||||||
|
- **No Qt in public C++ API** — `Types.h` uses `std::string`, `std::vector`, `std::function`. Qt conversion happens internally in `SDKRegistry.cpp`.
|
||||||
|
- **GIL-safe Python callables** — Python callbacks stored via `std::make_shared<py::object>` with `py::gil_scoped_acquire` before every invocation.
|
||||||
|
- **PySide widget bridging** — `WidgetBridge::toQWidget()` converts PySide QWidget objects to C++ `QWidget*` via `Gui::PythonWrapper` (Shiboken).
|
||||||
|
- **Provider pattern** — Interfaces like `IPanelProvider` enable addons to register factories. The registry calls `create_widget()` once and manages the lifecycle through `DockWindowManager`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Design decisions
|
## Design decisions
|
||||||
|
|
||||||
1. **`Create::` namespace prefix.** All Kindred Create C++ features use this prefix to distinguish them from FreeCAD core.
|
1. **`Create::` namespace prefix.** All Kindred Create C++ features use this prefix to distinguish them from FreeCAD core.
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
- [Build System](./development/build-system.md)
|
- [Build System](./development/build-system.md)
|
||||||
- [Gui Module Build](./development/gui-build-integration.md)
|
- [Gui Module Build](./development/gui-build-integration.md)
|
||||||
- [Package.xml Schema Extensions](./development/package-xml-schema.md)
|
- [Package.xml Schema Extensions](./development/package-xml-schema.md)
|
||||||
|
- [Writing an Addon](./development/writing-an-addon.md)
|
||||||
|
|
||||||
# Silo Server
|
# Silo Server
|
||||||
|
|
||||||
@@ -76,4 +77,5 @@
|
|||||||
- [OriginSelectorWidget](./reference/cpp-origin-selector-widget.md)
|
- [OriginSelectorWidget](./reference/cpp-origin-selector-widget.md)
|
||||||
- [FileOriginPython Bridge](./reference/cpp-file-origin-python.md)
|
- [FileOriginPython Bridge](./reference/cpp-file-origin-python.md)
|
||||||
- [Creating a Custom Origin (C++)](./reference/cpp-custom-origin-guide.md)
|
- [Creating a Custom Origin (C++)](./reference/cpp-custom-origin-guide.md)
|
||||||
|
- [KCSDK Python API](./reference/kcsdk-python.md)
|
||||||
- [KCSolve Python API](./reference/kcsolve-python.md)
|
- [KCSolve Python API](./reference/kcsolve-python.md)
|
||||||
|
|||||||
283
docs/src/development/writing-an-addon.md
Normal file
283
docs/src/development/writing-an-addon.md
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
# Writing an Addon
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Addon structure
|
||||||
|
|
||||||
|
A minimal addon has this layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
mods/my-addon/
|
||||||
|
├── package.xml # Manifest (required)
|
||||||
|
├── Init.py # Console-phase bootstrap
|
||||||
|
├── InitGui.py # GUI-phase bootstrap
|
||||||
|
└── my_addon/
|
||||||
|
├── __init__.py
|
||||||
|
└── commands.py # Your commands
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 1: Create the manifest
|
||||||
|
|
||||||
|
Every addon needs a `package.xml` with a `<kindred>` extension block. The `<workbench>` tag is required for `InitGui.py` to be loaded, even if your addon doesn't register a workbench.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<package format="1">
|
||||||
|
<name>my-addon</name>
|
||||||
|
<description>My custom addon for Kindred Create.</description>
|
||||||
|
<version>0.1.0</version>
|
||||||
|
<maintainer email="you@example.com">Your Name</maintainer>
|
||||||
|
<license>LGPL-2.1-or-later</license>
|
||||||
|
|
||||||
|
<!-- Required for InitGui.py loading -->
|
||||||
|
<workbench>
|
||||||
|
<classname>MyAddonWorkbench</classname>
|
||||||
|
</workbench>
|
||||||
|
|
||||||
|
<kindred>
|
||||||
|
<min_create_version>0.1.5</min_create_version>
|
||||||
|
<load_priority>70</load_priority>
|
||||||
|
<pure_python>true</pure_python>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>sdk</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</kindred>
|
||||||
|
</package>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Priority ranges
|
||||||
|
|
||||||
|
| Range | Use |
|
||||||
|
|-------|-----|
|
||||||
|
| 0-9 | SDK and core infrastructure |
|
||||||
|
| 10-49 | Foundation addons |
|
||||||
|
| 50-99 | Standard addons (ztools, silo) |
|
||||||
|
| 100+ | Optional/user addons |
|
||||||
|
|
||||||
|
See [Package.xml Schema Extensions](./package-xml-schema.md) for the full schema.
|
||||||
|
|
||||||
|
## Step 2: Console bootstrap (Init.py)
|
||||||
|
|
||||||
|
`Init.py` runs during FreeCAD's console initialization, before the GUI exists. Use it for non-GUI setup.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import FreeCAD
|
||||||
|
|
||||||
|
FreeCAD.Console.PrintLog("my-addon: loaded (console)\n")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: GUI bootstrap (InitGui.py)
|
||||||
|
|
||||||
|
`InitGui.py` runs when the GUI is ready. This is where you register commands, contexts, panels, and overlays.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import FreeCAD
|
||||||
|
import FreeCADGui
|
||||||
|
|
||||||
|
FreeCAD.Console.PrintLog("my-addon: loaded (GUI)\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _deferred_setup():
|
||||||
|
"""Register commands and UI after the main window is ready."""
|
||||||
|
from my_addon import commands
|
||||||
|
commands.register()
|
||||||
|
|
||||||
|
|
||||||
|
from PySide.QtCore import QTimer
|
||||||
|
QTimer.singleShot(2000, _deferred_setup)
|
||||||
|
```
|
||||||
|
|
||||||
|
Deferred setup via `QTimer.singleShot()` avoids timing issues during startup. See [Create Module Bootstrap](../reference/create-module-bootstrap.md) for the full timer cascade.
|
||||||
|
|
||||||
|
## Step 4: Register commands
|
||||||
|
|
||||||
|
FreeCAD commands use `Gui.addCommand()`. This is a stable FreeCAD API and does not need SDK wrappers.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# my_addon/commands.py
|
||||||
|
|
||||||
|
import FreeCAD
|
||||||
|
import FreeCADGui
|
||||||
|
|
||||||
|
|
||||||
|
class MyCommand:
|
||||||
|
def GetResources(self):
|
||||||
|
return {
|
||||||
|
"MenuText": "My Command",
|
||||||
|
"ToolTip": "Does something useful",
|
||||||
|
}
|
||||||
|
|
||||||
|
def Activated(self):
|
||||||
|
FreeCAD.Console.PrintMessage("My command activated\n")
|
||||||
|
|
||||||
|
def IsActive(self):
|
||||||
|
return FreeCAD.ActiveDocument is not None
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
FreeCADGui.addCommand("MyAddon_MyCommand", MyCommand())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5: Inject into editing contexts
|
||||||
|
|
||||||
|
Use the SDK to add your commands to existing toolbar contexts, rather than creating a standalone workbench.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from kindred_sdk import inject_commands
|
||||||
|
|
||||||
|
# Add your command to the PartDesign body context toolbar
|
||||||
|
inject_commands("partdesign.body", "PartDesign", ["MyAddon_MyCommand"])
|
||||||
|
```
|
||||||
|
|
||||||
|
Built-in contexts you can inject into: `sketcher.edit`, `assembly.edit`, `partdesign.feature`, `partdesign.body`, `assembly.idle`, `spreadsheet`, `empty_document`, `no_document`.
|
||||||
|
|
||||||
|
## Step 6: Register a custom context
|
||||||
|
|
||||||
|
If your addon has its own editing mode, register a context to control which toolbars are visible.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from kindred_sdk import register_context
|
||||||
|
|
||||||
|
def _is_my_object_in_edit():
|
||||||
|
import FreeCADGui
|
||||||
|
doc = FreeCADGui.activeDocument()
|
||||||
|
if doc and doc.getInEdit():
|
||||||
|
obj = doc.getInEdit().Object
|
||||||
|
return obj.isDerivedFrom("App::FeaturePython") and hasattr(obj, "MyAddonType")
|
||||||
|
return False
|
||||||
|
|
||||||
|
register_context(
|
||||||
|
"myaddon.edit",
|
||||||
|
"Editing {name}",
|
||||||
|
"#f9e2af", # Catppuccin yellow
|
||||||
|
["MyAddonToolbar", "StandardViews"],
|
||||||
|
_is_my_object_in_edit,
|
||||||
|
priority=55,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 7: Register a dock panel
|
||||||
|
|
||||||
|
For panels that live in the dock area (like Silo's database panels), use the SDK panel registration.
|
||||||
|
|
||||||
|
### Simple approach (recommended for most addons)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from kindred_sdk import register_dock_panel
|
||||||
|
|
||||||
|
def _create_my_panel():
|
||||||
|
from PySide import QtWidgets
|
||||||
|
widget = QtWidgets.QTreeWidget()
|
||||||
|
widget.setHeaderLabels(["Name", "Value"])
|
||||||
|
return widget
|
||||||
|
|
||||||
|
register_dock_panel(
|
||||||
|
"MyAddonPanel", # unique object name
|
||||||
|
"My Addon", # title bar text
|
||||||
|
_create_my_panel,
|
||||||
|
area="right",
|
||||||
|
delay_ms=3000, # create 3 seconds after startup
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced approach (IPanelProvider)
|
||||||
|
|
||||||
|
For full control over panel behavior, implement the `IPanelProvider` interface directly:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import kcsdk
|
||||||
|
|
||||||
|
class MyPanelProvider(kcsdk.IPanelProvider):
|
||||||
|
def id(self):
|
||||||
|
return "myaddon.inspector"
|
||||||
|
|
||||||
|
def title(self):
|
||||||
|
return "Inspector"
|
||||||
|
|
||||||
|
def create_widget(self):
|
||||||
|
from PySide import QtWidgets
|
||||||
|
tree = QtWidgets.QTreeWidget()
|
||||||
|
tree.setHeaderLabels(["Property", "Value"])
|
||||||
|
return tree
|
||||||
|
|
||||||
|
def preferred_area(self):
|
||||||
|
return kcsdk.DockArea.Left
|
||||||
|
|
||||||
|
def context_affinity(self):
|
||||||
|
return "myaddon.edit" # only visible in your custom context
|
||||||
|
|
||||||
|
# Register and create
|
||||||
|
kcsdk.register_panel(MyPanelProvider())
|
||||||
|
kcsdk.create_panel("myaddon.inspector")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 8: Use theme colors
|
||||||
|
|
||||||
|
The SDK provides the Catppuccin Mocha palette for consistent theming.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from kindred_sdk import get_theme_tokens, load_palette
|
||||||
|
|
||||||
|
# Quick lookup
|
||||||
|
tokens = get_theme_tokens()
|
||||||
|
blue = tokens["blue"] # "#89b4fa"
|
||||||
|
error = tokens["error"] # mapped from semantic role
|
||||||
|
|
||||||
|
# Full palette object
|
||||||
|
palette = load_palette()
|
||||||
|
palette.get("accent.primary") # semantic role lookup
|
||||||
|
palette.get("mauve") # direct color lookup
|
||||||
|
|
||||||
|
# Format QSS templates
|
||||||
|
qss = palette.format_qss("background: {base}; color: {text};")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete example
|
||||||
|
|
||||||
|
Putting it all together, here's a minimal addon that adds a command and a dock panel:
|
||||||
|
|
||||||
|
```
|
||||||
|
mods/my-addon/
|
||||||
|
├── package.xml
|
||||||
|
├── Init.py
|
||||||
|
├── InitGui.py
|
||||||
|
└── my_addon/
|
||||||
|
├── __init__.py
|
||||||
|
└── commands.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**InitGui.py:**
|
||||||
|
```python
|
||||||
|
import FreeCAD
|
||||||
|
|
||||||
|
def _setup():
|
||||||
|
from my_addon.commands import register
|
||||||
|
from kindred_sdk import inject_commands, register_dock_panel
|
||||||
|
|
||||||
|
register()
|
||||||
|
inject_commands("partdesign.body", "PartDesign", ["MyAddon_MyCommand"])
|
||||||
|
|
||||||
|
from PySide import QtWidgets
|
||||||
|
register_dock_panel(
|
||||||
|
"MyAddonPanel", "My Addon",
|
||||||
|
lambda: QtWidgets.QLabel("Hello from my addon"),
|
||||||
|
area="right", delay_ms=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
from PySide.QtCore import QTimer
|
||||||
|
QTimer.singleShot(2500, _setup)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key patterns
|
||||||
|
|
||||||
|
- **Use `kindred_sdk` wrappers** instead of `FreeCADGui.*` internals. The SDK handles fallback and error logging.
|
||||||
|
- **Defer initialization** with `QTimer.singleShot()` to avoid startup timing issues.
|
||||||
|
- **Declare `<dependency>sdk</dependency>`** in your manifest to ensure the SDK loads before your addon.
|
||||||
|
- **Inject commands into existing contexts** rather than creating standalone workbenches. This gives users a unified toolbar experience.
|
||||||
|
- **Use theme tokens** from the palette for colors. Don't hardcode hex values.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [KCSDK Python API Reference](../reference/kcsdk-python.md)
|
||||||
|
- [Package.xml Schema Extensions](./package-xml-schema.md)
|
||||||
|
- [Create Module Bootstrap](../reference/create-module-bootstrap.md)
|
||||||
252
docs/src/reference/kcsdk-python.md
Normal file
252
docs/src/reference/kcsdk-python.md
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
# KCSDK Python API Reference
|
||||||
|
|
||||||
|
The `kcsdk` module provides Python access to the Kindred Create addon SDK. It is built with pybind11 and installed alongside the Create module.
|
||||||
|
|
||||||
|
The `kindred_sdk` package (`mods/sdk/kindred_sdk/`) provides convenience wrappers that route through `kcsdk` when available, falling back to legacy `FreeCADGui.*` bindings. Addons should prefer `kindred_sdk` over importing `kcsdk` directly.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import kcsdk # C++ bindings (low-level)
|
||||||
|
import kindred_sdk # Python wrappers (recommended)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module constants
|
||||||
|
|
||||||
|
| Name | Value | Description |
|
||||||
|
|------|-------|-------------|
|
||||||
|
| `API_VERSION_MAJOR` | `1` | KCSDK API major version |
|
||||||
|
|
||||||
|
## Enums
|
||||||
|
|
||||||
|
### DockArea
|
||||||
|
|
||||||
|
Dock widget placement area. Values match `Qt::DockWidgetArea`.
|
||||||
|
|
||||||
|
| Value | Integer | Description |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| `DockArea.Left` | 1 | Left dock area |
|
||||||
|
| `DockArea.Right` | 2 | Right dock area |
|
||||||
|
| `DockArea.Top` | 4 | Top dock area |
|
||||||
|
| `DockArea.Bottom` | 8 | Bottom dock area |
|
||||||
|
|
||||||
|
### PanelPersistence
|
||||||
|
|
||||||
|
Whether a dock panel's visibility survives application restarts.
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `PanelPersistence.Session` | Visible until application close |
|
||||||
|
| `PanelPersistence.Persistent` | Saved to preferences and restored on next launch |
|
||||||
|
|
||||||
|
## Editing Context API
|
||||||
|
|
||||||
|
These functions manage the context-aware UI system. Contexts control which toolbars are visible based on the current editing state.
|
||||||
|
|
||||||
|
### register_context(id, label, color, toolbars, match, priority=50)
|
||||||
|
|
||||||
|
Register an editing context.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `id` | `str` | Unique identifier (e.g. `"myaddon.edit"`) |
|
||||||
|
| `label` | `str` | Display label template. Supports `{name}` placeholder |
|
||||||
|
| `color` | `str` | Hex color for breadcrumb (e.g. `"#f38ba8"`) |
|
||||||
|
| `toolbars` | `list[str]` | Toolbar names to show when active |
|
||||||
|
| `match` | `callable` | Zero-arg callable returning `True` when active |
|
||||||
|
| `priority` | `int` | Higher values checked first. Default 50 |
|
||||||
|
|
||||||
|
```python
|
||||||
|
kcsdk.register_context(
|
||||||
|
"myworkbench.edit",
|
||||||
|
"Editing {name}",
|
||||||
|
"#89b4fa",
|
||||||
|
["MyToolbar", "StandardViews"],
|
||||||
|
lambda: is_my_object_in_edit(),
|
||||||
|
priority=60,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### unregister_context(id)
|
||||||
|
|
||||||
|
Remove a previously registered editing context.
|
||||||
|
|
||||||
|
### register_overlay(id, toolbars, match)
|
||||||
|
|
||||||
|
Register an editing overlay. Overlays add toolbars to whatever context is currently active when `match()` returns `True`.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `id` | `str` | Unique overlay identifier |
|
||||||
|
| `toolbars` | `list[str]` | Toolbar names to append |
|
||||||
|
| `match` | `callable` | Zero-arg callable returning `True` when the overlay applies |
|
||||||
|
|
||||||
|
### unregister_overlay(id)
|
||||||
|
|
||||||
|
Remove a previously registered overlay.
|
||||||
|
|
||||||
|
### inject_commands(context_id, toolbar_name, commands)
|
||||||
|
|
||||||
|
Inject additional commands into an existing context's toolbar.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `context_id` | `str` | Target context identifier |
|
||||||
|
| `toolbar_name` | `str` | Toolbar within that context |
|
||||||
|
| `commands` | `list[str]` | Command names to add |
|
||||||
|
|
||||||
|
```python
|
||||||
|
kcsdk.inject_commands("partdesign.body", "PartDesign", ["MyAddon_CustomFeature"])
|
||||||
|
```
|
||||||
|
|
||||||
|
### current_context()
|
||||||
|
|
||||||
|
Return the current editing context as a dict, or `None`.
|
||||||
|
|
||||||
|
Keys: `id`, `label`, `color`, `toolbars`, `breadcrumb`, `breadcrumbColors`.
|
||||||
|
|
||||||
|
### refresh()
|
||||||
|
|
||||||
|
Force re-resolution of the editing context.
|
||||||
|
|
||||||
|
## Panel Provider API
|
||||||
|
|
||||||
|
These functions manage dock panel registration. Panels are created through the `IPanelProvider` interface and managed by `DockWindowManager`.
|
||||||
|
|
||||||
|
### IPanelProvider
|
||||||
|
|
||||||
|
Abstract base class for dock panel providers. Subclass in Python to create custom panels.
|
||||||
|
|
||||||
|
Three methods must be implemented:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyPanel(kcsdk.IPanelProvider):
|
||||||
|
def id(self):
|
||||||
|
return "myaddon.panel"
|
||||||
|
|
||||||
|
def title(self):
|
||||||
|
return "My Panel"
|
||||||
|
|
||||||
|
def create_widget(self):
|
||||||
|
from PySide import QtWidgets
|
||||||
|
label = QtWidgets.QLabel("Hello from my addon")
|
||||||
|
return label
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional methods with defaults:
|
||||||
|
|
||||||
|
| Method | Return type | Default | Description |
|
||||||
|
|--------|-------------|---------|-------------|
|
||||||
|
| `preferred_area()` | `DockArea` | `DockArea.Right` | Dock placement area |
|
||||||
|
| `persistence()` | `PanelPersistence` | `PanelPersistence.Session` | Visibility persistence |
|
||||||
|
| `context_affinity()` | `str` | `""` (always visible) | Only show in named context |
|
||||||
|
|
||||||
|
```python
|
||||||
|
class SidePanel(kcsdk.IPanelProvider):
|
||||||
|
def id(self): return "myaddon.side"
|
||||||
|
def title(self): return "Side Panel"
|
||||||
|
def create_widget(self):
|
||||||
|
from PySide import QtWidgets
|
||||||
|
return QtWidgets.QTreeWidget()
|
||||||
|
def preferred_area(self):
|
||||||
|
return kcsdk.DockArea.Left
|
||||||
|
def context_affinity(self):
|
||||||
|
return "partdesign.body" # only visible in PartDesign body context
|
||||||
|
```
|
||||||
|
|
||||||
|
### register_panel(provider)
|
||||||
|
|
||||||
|
Register a dock panel provider. The provider is stored in the registry until `create_panel()` is called to instantiate the actual dock widget.
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `provider` | `IPanelProvider` | Panel provider instance |
|
||||||
|
|
||||||
|
### unregister_panel(id)
|
||||||
|
|
||||||
|
Remove a registered panel provider and destroy its dock widget if created.
|
||||||
|
|
||||||
|
### create_panel(id)
|
||||||
|
|
||||||
|
Instantiate the dock widget for a registered panel. Calls the provider's `create_widget()` once and embeds the result in a `QDockWidget` via `DockWindowManager`. Skips silently if the panel already exists.
|
||||||
|
|
||||||
|
### create_all_panels()
|
||||||
|
|
||||||
|
Instantiate dock widgets for all registered panels.
|
||||||
|
|
||||||
|
### registered_panels()
|
||||||
|
|
||||||
|
Return IDs of all registered panel providers as `list[str]`.
|
||||||
|
|
||||||
|
### available()
|
||||||
|
|
||||||
|
Return names of all registered providers (across all provider types) as `list[str]`.
|
||||||
|
|
||||||
|
## `kindred_sdk` Convenience Wrappers
|
||||||
|
|
||||||
|
The `kindred_sdk` Python package wraps the `kcsdk` C++ module with input validation, error handling, and fallback to legacy APIs.
|
||||||
|
|
||||||
|
### kindred_sdk.register_dock_panel(object_name, title, widget_factory, area="right", delay_ms=0)
|
||||||
|
|
||||||
|
High-level dock panel registration. Creates an anonymous `IPanelProvider` internally and schedules creation via `QTimer`.
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `object_name` | `str` | | Qt object name (used as panel ID) |
|
||||||
|
| `title` | `str` | | Dock widget title |
|
||||||
|
| `widget_factory` | `callable` | | Zero-arg callable returning a `QWidget` |
|
||||||
|
| `area` | `str` | `"right"` | `"left"`, `"right"`, `"top"`, or `"bottom"` |
|
||||||
|
| `delay_ms` | `int` | `0` | Defer creation by this many milliseconds |
|
||||||
|
|
||||||
|
```python
|
||||||
|
from kindred_sdk import register_dock_panel
|
||||||
|
from PySide import QtWidgets
|
||||||
|
|
||||||
|
register_dock_panel(
|
||||||
|
"MyAddonPanel",
|
||||||
|
"My Addon",
|
||||||
|
lambda: QtWidgets.QLabel("Hello"),
|
||||||
|
area="left",
|
||||||
|
delay_ms=2000,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other `kindred_sdk` Wrappers
|
||||||
|
|
||||||
|
These mirror the `kcsdk` functions with added type validation and try/except error handling:
|
||||||
|
|
||||||
|
| Function | Maps to |
|
||||||
|
|----------|---------|
|
||||||
|
| `kindred_sdk.register_context()` | `kcsdk.register_context()` |
|
||||||
|
| `kindred_sdk.unregister_context()` | `kcsdk.unregister_context()` |
|
||||||
|
| `kindred_sdk.register_overlay()` | `kcsdk.register_overlay()` |
|
||||||
|
| `kindred_sdk.unregister_overlay()` | `kcsdk.unregister_overlay()` |
|
||||||
|
| `kindred_sdk.inject_commands()` | `kcsdk.inject_commands()` |
|
||||||
|
| `kindred_sdk.current_context()` | `kcsdk.current_context()` |
|
||||||
|
| `kindred_sdk.refresh_context()` | `kcsdk.refresh()` |
|
||||||
|
| `kindred_sdk.register_origin()` | `FreeCADGui.addOrigin()` |
|
||||||
|
| `kindred_sdk.unregister_origin()` | `FreeCADGui.removeOrigin()` |
|
||||||
|
| `kindred_sdk.get_theme_tokens()` | YAML palette lookup |
|
||||||
|
| `kindred_sdk.load_palette()` | `Palette` object from YAML |
|
||||||
|
| `kindred_sdk.create_version()` | Kindred Create version string |
|
||||||
|
| `kindred_sdk.freecad_version()` | FreeCAD version tuple |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Python Addon Code
|
||||||
|
|
|
||||||
|
kindred_sdk (mods/sdk/) <- convenience wrappers + validation
|
||||||
|
|
|
||||||
|
kcsdk.so (pybind11 module) <- C++ API bindings
|
||||||
|
|
|
||||||
|
libKCSDK.so (C++ shared library) <- SDKRegistry + provider interfaces
|
||||||
|
|
|
||||||
|
FreeCADGui (EditingContextResolver, DockWindowManager, OriginManager, ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
When `kcsdk` is not available (console mode, build not installed), `kindred_sdk` falls back to legacy `FreeCADGui.*` Python bindings.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Writing an Addon](../development/writing-an-addon.md)
|
||||||
|
- [Package.xml Schema Extensions](../development/package-xml-schema.md)
|
||||||
|
- [Create Module Bootstrap](./create-module-bootstrap.md)
|
||||||
@@ -1,18 +1,40 @@
|
|||||||
"""Deferred dock panel registration helper.
|
"""Dock panel registration helper.
|
||||||
|
|
||||||
Replaces the manual ``QTimer.singleShot()`` + duplicate-check +
|
Routes through the ``kcsdk`` C++ module (IPanelProvider / DockWindowManager)
|
||||||
try/except pattern used in ``src/Mod/Create/InitGui.py``.
|
when available, falling back to direct PySide QDockWidget creation for
|
||||||
|
backwards compatibility.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import FreeCAD
|
import FreeCAD
|
||||||
|
|
||||||
|
# Try to import the C++ SDK module; None if not yet built/installed.
|
||||||
|
try:
|
||||||
|
import kcsdk as _kcsdk
|
||||||
|
except ImportError:
|
||||||
|
_kcsdk = None
|
||||||
|
|
||||||
_AREA_MAP = {
|
_AREA_MAP = {
|
||||||
"left": 1, # Qt.LeftDockWidgetArea
|
"left": 1, # Qt.LeftDockWidgetArea / DockArea.Left
|
||||||
"right": 2, # Qt.RightDockWidgetArea
|
"right": 2, # Qt.RightDockWidgetArea / DockArea.Right
|
||||||
"top": 4, # Qt.TopDockWidgetArea
|
"top": 4, # Qt.TopDockWidgetArea / DockArea.Top
|
||||||
"bottom": 8, # Qt.BottomDockWidgetArea
|
"bottom": 8, # Qt.BottomDockWidgetArea / DockArea.Bottom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_DOCK_AREA_MAP = None # lazily populated from kcsdk
|
||||||
|
|
||||||
|
|
||||||
|
def _get_dock_area(area_str):
|
||||||
|
"""Convert area string to kcsdk.DockArea enum value."""
|
||||||
|
global _DOCK_AREA_MAP
|
||||||
|
if _DOCK_AREA_MAP is None:
|
||||||
|
_DOCK_AREA_MAP = {
|
||||||
|
"left": _kcsdk.DockArea.Left,
|
||||||
|
"right": _kcsdk.DockArea.Right,
|
||||||
|
"top": _kcsdk.DockArea.Top,
|
||||||
|
"bottom": _kcsdk.DockArea.Bottom,
|
||||||
|
}
|
||||||
|
return _DOCK_AREA_MAP.get(area_str)
|
||||||
|
|
||||||
|
|
||||||
def register_dock_panel(object_name, title, widget_factory, area="right", delay_ms=0):
|
def register_dock_panel(object_name, title, widget_factory, area="right", delay_ms=0):
|
||||||
"""Register a dock panel, optionally deferred.
|
"""Register a dock panel, optionally deferred.
|
||||||
@@ -38,15 +60,62 @@ def register_dock_panel(object_name, title, widget_factory, area="right", delay_
|
|||||||
if not callable(widget_factory):
|
if not callable(widget_factory):
|
||||||
raise TypeError("widget_factory must be callable")
|
raise TypeError("widget_factory must be callable")
|
||||||
|
|
||||||
qt_area = _AREA_MAP.get(area)
|
if area not in _AREA_MAP:
|
||||||
if qt_area is None:
|
|
||||||
raise ValueError(f"area must be one of {list(_AREA_MAP)}, got {area!r}")
|
raise ValueError(f"area must be one of {list(_AREA_MAP)}, got {area!r}")
|
||||||
|
|
||||||
|
if _kcsdk is not None:
|
||||||
|
_register_via_kcsdk(object_name, title, widget_factory, area, delay_ms)
|
||||||
|
else:
|
||||||
|
_register_via_pyside(object_name, title, widget_factory, area, delay_ms)
|
||||||
|
|
||||||
|
|
||||||
|
def _register_via_kcsdk(object_name, title, widget_factory, area, delay_ms):
|
||||||
|
"""Register using the C++ SDK panel provider system."""
|
||||||
|
dock_area = _get_dock_area(area)
|
||||||
|
|
||||||
|
class _AnonymousProvider(_kcsdk.IPanelProvider):
|
||||||
|
def id(self):
|
||||||
|
return object_name
|
||||||
|
|
||||||
|
def title(self):
|
||||||
|
return title
|
||||||
|
|
||||||
|
def create_widget(self):
|
||||||
|
return widget_factory()
|
||||||
|
|
||||||
|
def preferred_area(self):
|
||||||
|
return dock_area
|
||||||
|
|
||||||
|
try:
|
||||||
|
_kcsdk.register_panel(_AnonymousProvider())
|
||||||
|
|
||||||
|
def _create():
|
||||||
|
try:
|
||||||
|
_kcsdk.create_panel(object_name)
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintLog(
|
||||||
|
f"kindred_sdk: Panel '{object_name}' creation failed: {e}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
from PySide.QtCore import QTimer
|
||||||
|
|
||||||
|
QTimer.singleShot(max(0, delay_ms), _create)
|
||||||
|
except Exception as e:
|
||||||
|
FreeCAD.Console.PrintLog(
|
||||||
|
f"kindred_sdk: kcsdk panel registration failed for '{object_name}', "
|
||||||
|
f"falling back: {e}\n"
|
||||||
|
)
|
||||||
|
_register_via_pyside(object_name, title, widget_factory, area, delay_ms)
|
||||||
|
|
||||||
|
|
||||||
|
def _register_via_pyside(object_name, title, widget_factory, area, delay_ms):
|
||||||
|
"""Legacy fallback: create dock widget directly via PySide."""
|
||||||
|
qt_area = _AREA_MAP[area]
|
||||||
|
|
||||||
def _create():
|
def _create():
|
||||||
try:
|
try:
|
||||||
from PySide import QtCore, QtWidgets
|
|
||||||
|
|
||||||
import FreeCADGui
|
import FreeCADGui
|
||||||
|
from PySide import QtCore, QtWidgets
|
||||||
|
|
||||||
mw = FreeCADGui.getMainWindow()
|
mw = FreeCADGui.getMainWindow()
|
||||||
if mw is None:
|
if mw is None:
|
||||||
@@ -61,7 +130,9 @@ def register_dock_panel(object_name, title, widget_factory, area="right", delay_
|
|||||||
panel.setWidget(widget)
|
panel.setWidget(widget)
|
||||||
mw.addDockWidget(QtCore.Qt.DockWidgetArea(qt_area), panel)
|
mw.addDockWidget(QtCore.Qt.DockWidgetArea(qt_area), panel)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
FreeCAD.Console.PrintLog(f"kindred_sdk: Dock panel '{object_name}' skipped: {e}\n")
|
FreeCAD.Console.PrintLog(
|
||||||
|
f"kindred_sdk: Dock panel '{object_name}' skipped: {e}\n"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PySide.QtCore import QTimer
|
from PySide.QtCore import QTimer
|
||||||
|
|||||||
@@ -135,15 +135,51 @@ _cache = {}
|
|||||||
_PALETTES_DIR = os.path.join(os.path.dirname(__file__), "palettes")
|
_PALETTES_DIR = os.path.join(os.path.dirname(__file__), "palettes")
|
||||||
|
|
||||||
|
|
||||||
|
def _kcsdk_available():
|
||||||
|
"""Return the kcsdk module if available, else None."""
|
||||||
|
try:
|
||||||
|
import kcsdk
|
||||||
|
|
||||||
|
return kcsdk
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def load_palette(name="catppuccin-mocha"):
|
def load_palette(name="catppuccin-mocha"):
|
||||||
"""Load a named palette from the ``palettes/`` directory.
|
"""Load a named palette from the ``palettes/`` directory.
|
||||||
|
|
||||||
|
When the C++ ``kcsdk`` module is available (GUI mode), delegates to
|
||||||
|
``kcsdk.load_palette()`` and builds a ``Palette`` from the C++ token
|
||||||
|
map. Falls back to the Python YAML loader for console mode.
|
||||||
|
|
||||||
Results are cached; subsequent calls with the same *name* return
|
Results are cached; subsequent calls with the same *name* return
|
||||||
the same ``Palette`` instance.
|
the same ``Palette`` instance.
|
||||||
"""
|
"""
|
||||||
if name in _cache:
|
if name in _cache:
|
||||||
return _cache[name]
|
return _cache[name]
|
||||||
|
|
||||||
|
# Try C++ backend first
|
||||||
|
kcsdk = _kcsdk_available()
|
||||||
|
if kcsdk is not None:
|
||||||
|
try:
|
||||||
|
if kcsdk.load_palette(name):
|
||||||
|
tokens = kcsdk.theme_tokens()
|
||||||
|
# Separate colors from roles by checking if the token
|
||||||
|
# existed in the original colors set. Since the C++ engine
|
||||||
|
# merges them, we rebuild by loading the YAML for metadata.
|
||||||
|
# Simpler approach: use all tokens as colors (roles are
|
||||||
|
# already resolved to hex values in the C++ engine).
|
||||||
|
palette = Palette(
|
||||||
|
name=name,
|
||||||
|
slug=name,
|
||||||
|
colors=tokens,
|
||||||
|
roles={},
|
||||||
|
)
|
||||||
|
_cache[name] = palette
|
||||||
|
return palette
|
||||||
|
except Exception:
|
||||||
|
pass # Fall through to Python loader
|
||||||
|
|
||||||
path = os.path.join(_PALETTES_DIR, f"{name}.yaml")
|
path = os.path.join(_PALETTES_DIR, f"{name}.yaml")
|
||||||
if not os.path.isfile(path):
|
if not os.path.isfile(path):
|
||||||
FreeCAD.Console.PrintWarning(f"kindred_sdk: Palette file not found: {path}\n")
|
FreeCAD.Console.PrintWarning(f"kindred_sdk: Palette file not found: {path}\n")
|
||||||
@@ -152,7 +188,9 @@ def load_palette(name="catppuccin-mocha"):
|
|||||||
try:
|
try:
|
||||||
raw = _load_yaml(path)
|
raw = _load_yaml(path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
FreeCAD.Console.PrintWarning(f"kindred_sdk: Failed to load palette '{name}': {e}\n")
|
FreeCAD.Console.PrintWarning(
|
||||||
|
f"kindred_sdk: Failed to load palette '{name}': {e}\n"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
palette = Palette(
|
palette = Palette(
|
||||||
@@ -172,9 +210,20 @@ def load_palette(name="catppuccin-mocha"):
|
|||||||
def get_theme_tokens(name="catppuccin-mocha"):
|
def get_theme_tokens(name="catppuccin-mocha"):
|
||||||
"""Return a dict of ``{token_name: "#hex"}`` for all colors in a palette.
|
"""Return a dict of ``{token_name: "#hex"}`` for all colors in a palette.
|
||||||
|
|
||||||
This is a convenience shorthand for ``load_palette(name).colors``.
|
When the C++ ``kcsdk`` module is available, delegates directly to
|
||||||
|
``kcsdk.theme_tokens()`` for best performance. Falls back to the
|
||||||
|
Python palette loader otherwise.
|
||||||
|
|
||||||
Returns a copy so callers cannot mutate the cached palette.
|
Returns a copy so callers cannot mutate the cached palette.
|
||||||
"""
|
"""
|
||||||
|
kcsdk = _kcsdk_available()
|
||||||
|
if kcsdk is not None:
|
||||||
|
try:
|
||||||
|
kcsdk.load_palette(name)
|
||||||
|
return dict(kcsdk.theme_tokens())
|
||||||
|
except Exception:
|
||||||
|
pass # Fall through to Python loader
|
||||||
|
|
||||||
palette = load_palette(name)
|
palette = load_palette(name)
|
||||||
if palette is None:
|
if palette is None:
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -124,6 +124,10 @@ EditingContextResolver::EditingContextResolver()
|
|||||||
app.signalActivatedViewProvider.connect(
|
app.signalActivatedViewProvider.connect(
|
||||||
[this](const ViewProviderDocumentObject*, const char*) { refresh(); }
|
[this](const ViewProviderDocumentObject*, const char*) { refresh(); }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Resolve the initial context so currentContext() returns a valid state
|
||||||
|
// before any signals fire (e.g. when BreadcrumbToolBar queries on creation).
|
||||||
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
EditingContextResolver::~EditingContextResolver()
|
EditingContextResolver::~EditingContextResolver()
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
set(KCSDK_SRCS
|
set(KCSDK_SRCS
|
||||||
KCSDKGlobal.h
|
KCSDKGlobal.h
|
||||||
Types.h
|
Types.h
|
||||||
|
IPanelProvider.h
|
||||||
|
WidgetBridge.h
|
||||||
|
WidgetBridge.cpp
|
||||||
|
ThemeEngine.h
|
||||||
|
ThemeEngine.cpp
|
||||||
SDKRegistry.h
|
SDKRegistry.h
|
||||||
SDKRegistry.cpp
|
SDKRegistry.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
80
src/Gui/SDK/IPanelProvider.h
Normal file
80
src/Gui/SDK/IPanelProvider.h
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
/****************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (c) 2025 Kindred Systems <development@kindred-systems.com> *
|
||||||
|
* *
|
||||||
|
* This file is part of FreeCAD. *
|
||||||
|
* *
|
||||||
|
* FreeCAD is free software: you can redistribute it and/or modify it *
|
||||||
|
* under the terms of the GNU Lesser General Public License as *
|
||||||
|
* published by the Free Software Foundation, either version 2.1 of the *
|
||||||
|
* License, or (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* FreeCAD is distributed in the hope that it will be useful, but *
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||||
|
* Lesser General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU Lesser General Public *
|
||||||
|
* License along with FreeCAD. If not, see *
|
||||||
|
* <https://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
***************************************************************************/
|
||||||
|
|
||||||
|
#ifndef KCSDK_IPANELPROVIDER_H
|
||||||
|
#define KCSDK_IPANELPROVIDER_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "KCSDKGlobal.h"
|
||||||
|
#include "Types.h"
|
||||||
|
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
|
namespace KCSDK
|
||||||
|
{
|
||||||
|
|
||||||
|
/// Abstract interface for addon-provided dock panels.
|
||||||
|
///
|
||||||
|
/// Addons implement this interface to register dock panel factories with
|
||||||
|
/// the SDK registry. The registry calls create_widget() once to
|
||||||
|
/// instantiate the panel and embeds the result in a QDockWidget managed
|
||||||
|
/// by DockWindowManager.
|
||||||
|
class KCSDKExport IPanelProvider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IPanelProvider() = default;
|
||||||
|
|
||||||
|
/// Unique panel identifier (e.g. "silo.auth", "silo.activity").
|
||||||
|
virtual std::string id() const = 0;
|
||||||
|
|
||||||
|
/// Title displayed in the dock widget title bar.
|
||||||
|
virtual std::string title() const = 0;
|
||||||
|
|
||||||
|
/// Create the panel widget. Called exactly once by the registry.
|
||||||
|
/// Ownership of the returned widget transfers to the caller.
|
||||||
|
virtual QWidget* create_widget() = 0;
|
||||||
|
|
||||||
|
/// Preferred dock area. Default: Right.
|
||||||
|
virtual DockArea preferred_area() const
|
||||||
|
{
|
||||||
|
return DockArea::Right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether visibility persists across sessions. Default: Session.
|
||||||
|
virtual PanelPersistence persistence() const
|
||||||
|
{
|
||||||
|
return PanelPersistence::Session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Editing context affinity. If non-empty, the panel is only shown
|
||||||
|
/// when the named context is active. Default: "" (always visible).
|
||||||
|
virtual std::string context_affinity() const
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace KCSDK
|
||||||
|
|
||||||
|
#endif // KCSDK_IPANELPROVIDER_H
|
||||||
@@ -22,11 +22,13 @@
|
|||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
|
|
||||||
#include "SDKRegistry.h"
|
#include "SDKRegistry.h"
|
||||||
|
#include "IPanelProvider.h"
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
|
||||||
#include <Base/Console.h>
|
#include <Base/Console.h>
|
||||||
|
#include <Gui/DockWindowManager.h>
|
||||||
#include <Gui/EditingContext.h>
|
#include <Gui/EditingContext.h>
|
||||||
|
|
||||||
namespace KCSDK
|
namespace KCSDK
|
||||||
@@ -88,7 +90,12 @@ SDKRegistry::~SDKRegistry() = default;
|
|||||||
std::vector<std::string> SDKRegistry::available() const
|
std::vector<std::string> SDKRegistry::available() const
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mutex_);
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
return {};
|
std::vector<std::string> result;
|
||||||
|
result.reserve(panels_.size());
|
||||||
|
for (const auto& [id, _] : panels_) {
|
||||||
|
result.push_back(id);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Editing context API ----------------------------------------------------
|
// -- Editing context API ----------------------------------------------------
|
||||||
@@ -154,4 +161,95 @@ void SDKRegistry::refresh()
|
|||||||
Gui::EditingContextResolver::instance()->refresh();
|
Gui::EditingContextResolver::instance()->refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- Panel provider API -----------------------------------------------------
|
||||||
|
|
||||||
|
void SDKRegistry::registerPanel(std::unique_ptr<IPanelProvider> provider)
|
||||||
|
{
|
||||||
|
if (!provider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::string id = provider->id();
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
panels_[id] = std::move(provider);
|
||||||
|
Base::Console().log("KCSDK: registered panel provider '%s'\n", id.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDKRegistry::unregisterPanel(const std::string& id)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
auto it = panels_.find(id);
|
||||||
|
if (it == panels_.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the dock widget if it was created.
|
||||||
|
auto* dwm = Gui::DockWindowManager::instance();
|
||||||
|
if (dwm) {
|
||||||
|
dwm->removeDockWindow(id.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
panels_.erase(it);
|
||||||
|
Base::Console().log("KCSDK: unregistered panel provider '%s'\n", id.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDKRegistry::createPanel(const std::string& id)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
auto it = panels_.find(id);
|
||||||
|
if (it == panels_.end()) {
|
||||||
|
Base::Console().warning("KCSDK: no panel provider '%s' registered\n", id.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* dwm = Gui::DockWindowManager::instance();
|
||||||
|
if (!dwm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if already created.
|
||||||
|
if (dwm->getDockWindow(id.c_str())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IPanelProvider* provider = it->second.get();
|
||||||
|
QWidget* widget = provider->create_widget();
|
||||||
|
if (!widget) {
|
||||||
|
Base::Console().warning("KCSDK: panel '%s' create_widget() returned null\n",
|
||||||
|
id.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto qtArea = static_cast<Qt::DockWidgetArea>(provider->preferred_area());
|
||||||
|
dwm->addDockWindow(id.c_str(), widget, qtArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDKRegistry::createAllPanels()
|
||||||
|
{
|
||||||
|
// Collect IDs under lock, then create outside to avoid recursive locking.
|
||||||
|
std::vector<std::string> ids;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
ids.reserve(panels_.size());
|
||||||
|
for (const auto& [id, _] : panels_) {
|
||||||
|
ids.push_back(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& id : ids) {
|
||||||
|
createPanel(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> SDKRegistry::registeredPanels() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
std::vector<std::string> result;
|
||||||
|
result.reserve(panels_.size());
|
||||||
|
for (const auto& [id, _] : panels_) {
|
||||||
|
result.push_back(id);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace KCSDK
|
} // namespace KCSDK
|
||||||
|
|||||||
@@ -24,8 +24,10 @@
|
|||||||
#ifndef KCSDK_SDKREGISTRY_H
|
#ifndef KCSDK_SDKREGISTRY_H
|
||||||
#define KCSDK_SDKREGISTRY_H
|
#define KCSDK_SDKREGISTRY_H
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "KCSDKGlobal.h"
|
#include "KCSDKGlobal.h"
|
||||||
@@ -34,6 +36,8 @@
|
|||||||
namespace KCSDK
|
namespace KCSDK
|
||||||
{
|
{
|
||||||
|
|
||||||
|
class IPanelProvider;
|
||||||
|
|
||||||
/// Current KCSDK API major version. Addons should check this at load time.
|
/// Current KCSDK API major version. Addons should check this at load time.
|
||||||
constexpr int API_VERSION_MAJOR = 1;
|
constexpr int API_VERSION_MAJOR = 1;
|
||||||
|
|
||||||
@@ -81,6 +85,23 @@ public:
|
|||||||
/// Force re-resolution of the editing context.
|
/// Force re-resolution of the editing context.
|
||||||
void refresh();
|
void refresh();
|
||||||
|
|
||||||
|
// -- Panel provider API ------------------------------------------------
|
||||||
|
|
||||||
|
/// Register a dock panel provider. Ownership transfers to the registry.
|
||||||
|
void registerPanel(std::unique_ptr<IPanelProvider> provider);
|
||||||
|
|
||||||
|
/// Remove a registered panel provider and its dock widget (if created).
|
||||||
|
void unregisterPanel(const std::string& id);
|
||||||
|
|
||||||
|
/// Instantiate the dock widget for a registered panel.
|
||||||
|
void createPanel(const std::string& id);
|
||||||
|
|
||||||
|
/// Instantiate dock widgets for all registered panels.
|
||||||
|
void createAllPanels();
|
||||||
|
|
||||||
|
/// Return IDs of all registered panel providers.
|
||||||
|
std::vector<std::string> registeredPanels() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SDKRegistry();
|
SDKRegistry();
|
||||||
|
|
||||||
@@ -90,6 +111,7 @@ private:
|
|||||||
SDKRegistry& operator=(SDKRegistry&&) = delete;
|
SDKRegistry& operator=(SDKRegistry&&) = delete;
|
||||||
|
|
||||||
mutable std::mutex mutex_;
|
mutable std::mutex mutex_;
|
||||||
|
std::unordered_map<std::string, std::unique_ptr<IPanelProvider>> panels_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace KCSDK
|
} // namespace KCSDK
|
||||||
|
|||||||
272
src/Gui/SDK/ThemeEngine.cpp
Normal file
272
src/Gui/SDK/ThemeEngine.cpp
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
/****************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (c) 2025 Kindred Systems <development@kindred-systems.com> *
|
||||||
|
* *
|
||||||
|
* This file is part of FreeCAD. *
|
||||||
|
* *
|
||||||
|
* FreeCAD is free software: you can redistribute it and/or modify it *
|
||||||
|
* under the terms of the GNU Lesser General Public License as *
|
||||||
|
* published by the Free Software Foundation, either version 2.1 of the *
|
||||||
|
* License, or (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* FreeCAD is distributed in the hope that it will be useful, but *
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||||
|
* Lesser General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU Lesser General Public *
|
||||||
|
* License along with FreeCAD. If not, see *
|
||||||
|
* <https://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
***************************************************************************/
|
||||||
|
|
||||||
|
#include "ThemeEngine.h"
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <regex>
|
||||||
|
|
||||||
|
#include <App/Application.h>
|
||||||
|
#include <Base/Console.h>
|
||||||
|
|
||||||
|
namespace KCSDK
|
||||||
|
{
|
||||||
|
|
||||||
|
// -- Singleton --------------------------------------------------------------
|
||||||
|
|
||||||
|
ThemeEngine& ThemeEngine::instance()
|
||||||
|
{
|
||||||
|
static ThemeEngine engine;
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Path resolution --------------------------------------------------------
|
||||||
|
|
||||||
|
std::string ThemeEngine::resolvePalettePath(const std::string& name)
|
||||||
|
{
|
||||||
|
std::string home = App::Application::getHomePath();
|
||||||
|
return home + "Mod/Create/kindred_sdk/palettes/" + name + ".yaml";
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Minimal YAML parser ----------------------------------------------------
|
||||||
|
|
||||||
|
bool ThemeEngine::parseYaml(const std::string& path, Palette& out)
|
||||||
|
{
|
||||||
|
std::ifstream file(path);
|
||||||
|
if (!file.is_open()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.colors.clear();
|
||||||
|
out.roles.clear();
|
||||||
|
out.name.clear();
|
||||||
|
out.slug.clear();
|
||||||
|
|
||||||
|
std::string currentSection;
|
||||||
|
std::string line;
|
||||||
|
|
||||||
|
while (std::getline(file, line)) {
|
||||||
|
// Strip trailing whitespace (including \r on Windows)
|
||||||
|
while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t')) {
|
||||||
|
line.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip blank lines and comments
|
||||||
|
if (line.empty() || line[0] == '#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect indentation
|
||||||
|
std::size_t indent = 0;
|
||||||
|
while (indent < line.size() && (line[indent] == ' ' || line[indent] == '\t')) {
|
||||||
|
++indent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the colon separator
|
||||||
|
auto colonPos = line.find(':', indent);
|
||||||
|
if (colonPos == std::string::npos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract key
|
||||||
|
std::string key = line.substr(indent, colonPos - indent);
|
||||||
|
// Trim trailing whitespace from key
|
||||||
|
while (!key.empty() && (key.back() == ' ' || key.back() == '\t')) {
|
||||||
|
key.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract value (everything after ": ")
|
||||||
|
std::string value;
|
||||||
|
std::size_t valueStart = colonPos + 1;
|
||||||
|
while (valueStart < line.size() && line[valueStart] == ' ') {
|
||||||
|
++valueStart;
|
||||||
|
}
|
||||||
|
if (valueStart < line.size()) {
|
||||||
|
value = line.substr(valueStart);
|
||||||
|
// Strip surrounding quotes
|
||||||
|
if (value.size() >= 2
|
||||||
|
&& ((value.front() == '"' && value.back() == '"')
|
||||||
|
|| (value.front() == '\'' && value.back() == '\''))) {
|
||||||
|
value = value.substr(1, value.size() - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indent == 0) {
|
||||||
|
// Top-level key
|
||||||
|
if (value.empty()) {
|
||||||
|
// Start of a nested section
|
||||||
|
currentSection = key;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (key == "name") {
|
||||||
|
out.name = value;
|
||||||
|
}
|
||||||
|
else if (key == "slug") {
|
||||||
|
out.slug = value;
|
||||||
|
}
|
||||||
|
currentSection.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!currentSection.empty()) {
|
||||||
|
// Nested key within a section
|
||||||
|
if (currentSection == "colors") {
|
||||||
|
out.colors[key] = value;
|
||||||
|
}
|
||||||
|
else if (currentSection == "roles") {
|
||||||
|
// Roles map semantic names to color names — resolve to hex
|
||||||
|
auto it = out.colors.find(value);
|
||||||
|
if (it != out.colors.end()) {
|
||||||
|
out.roles[key] = it->second;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Store the raw name; will remain unresolved
|
||||||
|
out.roles[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Public API -------------------------------------------------------------
|
||||||
|
|
||||||
|
bool ThemeEngine::loadPalette(const std::string& name)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
// Return cached if already loaded
|
||||||
|
if (cache_.count(name)) {
|
||||||
|
activePalette_ = name;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string path = resolvePalettePath(name);
|
||||||
|
Palette palette;
|
||||||
|
if (!parseYaml(path, palette)) {
|
||||||
|
Base::Console().warning("KCSDK: palette file not found: %s\n", path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (palette.name.empty()) {
|
||||||
|
palette.name = name;
|
||||||
|
}
|
||||||
|
if (palette.slug.empty()) {
|
||||||
|
palette.slug = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
Base::Console().log("KCSDK: loaded palette '%s' (%zu colors, %zu roles)\n",
|
||||||
|
palette.name.c_str(), palette.colors.size(), palette.roles.size());
|
||||||
|
|
||||||
|
cache_[name] = std::move(palette);
|
||||||
|
activePalette_ = name;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ThemeEngine::getColor(const std::string& token) const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
auto cacheIt = cache_.find(activePalette_);
|
||||||
|
if (cacheIt == cache_.end()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const Palette& pal = cacheIt->second;
|
||||||
|
|
||||||
|
// Check roles first, then raw colors (matching Python Palette.get())
|
||||||
|
auto roleIt = pal.roles.find(token);
|
||||||
|
if (roleIt != pal.roles.end()) {
|
||||||
|
return roleIt->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto colorIt = pal.colors.find(token);
|
||||||
|
if (colorIt != pal.colors.end()) {
|
||||||
|
return colorIt->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::string> ThemeEngine::allTokens() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
|
||||||
|
auto cacheIt = cache_.find(activePalette_);
|
||||||
|
if (cacheIt == cache_.end()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const Palette& pal = cacheIt->second;
|
||||||
|
|
||||||
|
// Start with colors, overlay roles (roles take precedence for same-named keys)
|
||||||
|
std::unordered_map<std::string, std::string> result = pal.colors;
|
||||||
|
for (const auto& [key, value] : pal.roles) {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ThemeEngine::formatQss(const std::string& templateStr) const
|
||||||
|
{
|
||||||
|
auto tokens = allTokens();
|
||||||
|
if (tokens.empty()) {
|
||||||
|
return templateStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::regex pattern(R"(\{([a-z][a-z0-9_.]*)\})");
|
||||||
|
|
||||||
|
std::string result;
|
||||||
|
auto begin = std::sregex_iterator(templateStr.begin(), templateStr.end(), pattern);
|
||||||
|
auto end = std::sregex_iterator();
|
||||||
|
|
||||||
|
std::size_t lastPos = 0;
|
||||||
|
for (auto it = begin; it != end; ++it) {
|
||||||
|
const auto& match = *it;
|
||||||
|
// Append text before this match
|
||||||
|
result.append(templateStr, lastPos, match.position() - lastPos);
|
||||||
|
|
||||||
|
std::string key = match[1].str();
|
||||||
|
auto tokenIt = tokens.find(key);
|
||||||
|
if (tokenIt != tokens.end()) {
|
||||||
|
result.append(tokenIt->second);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Leave unknown tokens as-is
|
||||||
|
result.append(match[0].str());
|
||||||
|
}
|
||||||
|
lastPos = match.position() + match.length();
|
||||||
|
}
|
||||||
|
// Append remaining text
|
||||||
|
result.append(templateStr, lastPos);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ThemeEngine::activePaletteName() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
return activePalette_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace KCSDK
|
||||||
95
src/Gui/SDK/ThemeEngine.h
Normal file
95
src/Gui/SDK/ThemeEngine.h
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
/****************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (c) 2025 Kindred Systems <development@kindred-systems.com> *
|
||||||
|
* *
|
||||||
|
* This file is part of FreeCAD. *
|
||||||
|
* *
|
||||||
|
* FreeCAD is free software: you can redistribute it and/or modify it *
|
||||||
|
* under the terms of the GNU Lesser General Public License as *
|
||||||
|
* published by the Free Software Foundation, either version 2.1 of the *
|
||||||
|
* License, or (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* FreeCAD is distributed in the hope that it will be useful, but *
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||||
|
* Lesser General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU Lesser General Public *
|
||||||
|
* License along with FreeCAD. If not, see *
|
||||||
|
* <https://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
***************************************************************************/
|
||||||
|
|
||||||
|
#ifndef KCSDK_THEMEENGINE_H
|
||||||
|
#define KCSDK_THEMEENGINE_H
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "KCSDKGlobal.h"
|
||||||
|
|
||||||
|
namespace KCSDK
|
||||||
|
{
|
||||||
|
|
||||||
|
/// A loaded color palette with raw color tokens and semantic roles.
|
||||||
|
struct Palette
|
||||||
|
{
|
||||||
|
std::string name;
|
||||||
|
std::string slug;
|
||||||
|
std::unordered_map<std::string, std::string> colors;
|
||||||
|
std::unordered_map<std::string, std::string> roles;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Singleton theme engine that loads YAML palettes and provides color lookup.
|
||||||
|
///
|
||||||
|
/// Palette files use a minimal YAML subset (flat key-value pairs with one
|
||||||
|
/// level of nesting) matching the format in
|
||||||
|
/// ``mods/sdk/kindred_sdk/palettes/catppuccin-mocha.yaml``.
|
||||||
|
///
|
||||||
|
/// Thread safety: all public methods are internally synchronized.
|
||||||
|
class KCSDKExport ThemeEngine
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static ThemeEngine& instance();
|
||||||
|
|
||||||
|
/// Load a named palette from the palettes directory.
|
||||||
|
/// Returns false if the file was not found or could not be parsed.
|
||||||
|
bool loadPalette(const std::string& name = "catppuccin-mocha");
|
||||||
|
|
||||||
|
/// Look up a color by role first, then by raw color name.
|
||||||
|
/// Returns the hex string (e.g. "#89b4fa") or empty string if not found.
|
||||||
|
std::string getColor(const std::string& token) const;
|
||||||
|
|
||||||
|
/// Return all color tokens as {name: "#hex"} (colors + resolved roles).
|
||||||
|
std::unordered_map<std::string, std::string> allTokens() const;
|
||||||
|
|
||||||
|
/// Substitute {token} placeholders in a QSS template string.
|
||||||
|
/// Unknown tokens are left as-is.
|
||||||
|
std::string formatQss(const std::string& templateStr) const;
|
||||||
|
|
||||||
|
/// Return the name of the currently active palette, or empty if none.
|
||||||
|
std::string activePaletteName() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ThemeEngine() = default;
|
||||||
|
~ThemeEngine() = default;
|
||||||
|
|
||||||
|
ThemeEngine(const ThemeEngine&) = delete;
|
||||||
|
ThemeEngine& operator=(const ThemeEngine&) = delete;
|
||||||
|
|
||||||
|
/// Parse a minimal YAML palette file into a Palette struct.
|
||||||
|
static bool parseYaml(const std::string& path, Palette& out);
|
||||||
|
|
||||||
|
/// Resolve the filesystem path to a named palette.
|
||||||
|
static std::string resolvePalettePath(const std::string& name);
|
||||||
|
|
||||||
|
mutable std::mutex mutex_;
|
||||||
|
std::unordered_map<std::string, Palette> cache_;
|
||||||
|
std::string activePalette_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace KCSDK
|
||||||
|
|
||||||
|
#endif // KCSDK_THEMEENGINE_H
|
||||||
@@ -63,6 +63,22 @@ struct KCSDKExport ContextSnapshot
|
|||||||
std::vector<std::string> breadcrumbColors;
|
std::vector<std::string> breadcrumbColors;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Dock widget area. Values match Qt::DockWidgetArea for direct casting.
|
||||||
|
enum class DockArea
|
||||||
|
{
|
||||||
|
Left = 1,
|
||||||
|
Right = 2,
|
||||||
|
Top = 4,
|
||||||
|
Bottom = 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Whether a dock panel's visibility persists across sessions.
|
||||||
|
enum class PanelPersistence
|
||||||
|
{
|
||||||
|
Session, ///< Visible until application close.
|
||||||
|
Persistent, ///< Saved to preferences and restored on next launch.
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace KCSDK
|
} // namespace KCSDK
|
||||||
|
|
||||||
#endif // KCSDK_TYPES_H
|
#endif // KCSDK_TYPES_H
|
||||||
|
|||||||
48
src/Gui/SDK/WidgetBridge.cpp
Normal file
48
src/Gui/SDK/WidgetBridge.cpp
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
/****************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (c) 2025 Kindred Systems <development@kindred-systems.com> *
|
||||||
|
* *
|
||||||
|
* This file is part of FreeCAD. *
|
||||||
|
* *
|
||||||
|
* FreeCAD is free software: you can redistribute it and/or modify it *
|
||||||
|
* under the terms of the GNU Lesser General Public License as *
|
||||||
|
* published by the Free Software Foundation, either version 2.1 of the *
|
||||||
|
* License, or (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* FreeCAD is distributed in the hope that it will be useful, but *
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||||
|
* Lesser General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU Lesser General Public *
|
||||||
|
* License along with FreeCAD. If not, see *
|
||||||
|
* <https://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
***************************************************************************/
|
||||||
|
|
||||||
|
#include "WidgetBridge.h"
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include <CXX/Objects.hxx>
|
||||||
|
|
||||||
|
#include <Gui/PythonWrapper.h>
|
||||||
|
|
||||||
|
namespace KCSDK
|
||||||
|
{
|
||||||
|
|
||||||
|
QWidget* WidgetBridge::toQWidget(PyObject* pyWidget)
|
||||||
|
{
|
||||||
|
if (!pyWidget) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Gui::PythonWrapper wrap;
|
||||||
|
wrap.loadWidgetsModule();
|
||||||
|
|
||||||
|
QObject* obj = wrap.toQObject(Py::Object(pyWidget));
|
||||||
|
return qobject_cast<QWidget*>(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace KCSDK
|
||||||
50
src/Gui/SDK/WidgetBridge.h
Normal file
50
src/Gui/SDK/WidgetBridge.h
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
/****************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (c) 2025 Kindred Systems <development@kindred-systems.com> *
|
||||||
|
* *
|
||||||
|
* This file is part of FreeCAD. *
|
||||||
|
* *
|
||||||
|
* FreeCAD is free software: you can redistribute it and/or modify it *
|
||||||
|
* under the terms of the GNU Lesser General Public License as *
|
||||||
|
* published by the Free Software Foundation, either version 2.1 of the *
|
||||||
|
* License, or (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* FreeCAD is distributed in the hope that it will be useful, but *
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||||
|
* Lesser General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU Lesser General Public *
|
||||||
|
* License along with FreeCAD. If not, see *
|
||||||
|
* <https://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
***************************************************************************/
|
||||||
|
|
||||||
|
#ifndef KCSDK_WIDGETBRIDGE_H
|
||||||
|
#define KCSDK_WIDGETBRIDGE_H
|
||||||
|
|
||||||
|
#include "KCSDKGlobal.h"
|
||||||
|
|
||||||
|
struct _object;
|
||||||
|
using PyObject = _object;
|
||||||
|
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
|
namespace KCSDK
|
||||||
|
{
|
||||||
|
|
||||||
|
/// Utility for converting between PySide QWidget objects and C++ QWidget*.
|
||||||
|
///
|
||||||
|
/// Uses Gui::PythonWrapper (Shiboken) internally.
|
||||||
|
class KCSDKExport WidgetBridge
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/// Extract a C++ QWidget* from a PySide QWidget PyObject.
|
||||||
|
/// Returns nullptr if the conversion fails.
|
||||||
|
static QWidget* toQWidget(PyObject* pyWidget);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace KCSDK
|
||||||
|
|
||||||
|
#endif // KCSDK_WIDGETBRIDGE_H
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
set(KCSDKPy_SRCS
|
set(KCSDKPy_SRCS
|
||||||
kcsdk_py.cpp
|
kcsdk_py.cpp
|
||||||
|
PyIPanelProvider.h
|
||||||
|
PyProviderHolder.h
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(kcsdk_py SHARED ${KCSDKPy_SRCS})
|
add_library(kcsdk_py SHARED ${KCSDKPy_SRCS})
|
||||||
|
|||||||
83
src/Gui/SDK/bindings/PyIPanelProvider.h
Normal file
83
src/Gui/SDK/bindings/PyIPanelProvider.h
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
/****************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (c) 2025 Kindred Systems <development@kindred-systems.com> *
|
||||||
|
* *
|
||||||
|
* This file is part of FreeCAD. *
|
||||||
|
* *
|
||||||
|
* FreeCAD is free software: you can redistribute it and/or modify it *
|
||||||
|
* under the terms of the GNU Lesser General Public License as *
|
||||||
|
* published by the Free Software Foundation, either version 2.1 of the *
|
||||||
|
* License, or (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* FreeCAD is distributed in the hope that it will be useful, but *
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||||
|
* Lesser General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU Lesser General Public *
|
||||||
|
* License along with FreeCAD. If not, see *
|
||||||
|
* <https://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
***************************************************************************/
|
||||||
|
|
||||||
|
#ifndef KCSDK_PYIPANELPROVIDER_H
|
||||||
|
#define KCSDK_PYIPANELPROVIDER_H
|
||||||
|
|
||||||
|
#include <pybind11/pybind11.h>
|
||||||
|
#include <pybind11/stl.h>
|
||||||
|
|
||||||
|
#include <Gui/SDK/IPanelProvider.h>
|
||||||
|
|
||||||
|
namespace KCSDK
|
||||||
|
{
|
||||||
|
|
||||||
|
/// pybind11 trampoline class for IPanelProvider.
|
||||||
|
/// Enables Python subclasses that override virtual methods.
|
||||||
|
class PyIPanelProvider : public IPanelProvider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using IPanelProvider::IPanelProvider;
|
||||||
|
|
||||||
|
// ── Pure virtuals ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
std::string id() const override
|
||||||
|
{
|
||||||
|
PYBIND11_OVERRIDE_PURE(std::string, IPanelProvider, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string title() const override
|
||||||
|
{
|
||||||
|
PYBIND11_OVERRIDE_PURE(std::string, IPanelProvider, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create_widget() is NOT overridden here — pybind11 cannot handle
|
||||||
|
// QWidget* (no type_caster for Qt types). Python dispatch for
|
||||||
|
// create_widget() goes through PyProviderHolder which calls
|
||||||
|
// obj_.attr("create_widget")() and converts via WidgetBridge.
|
||||||
|
QWidget* create_widget() override
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Virtuals with defaults ─────────────────────────────────────
|
||||||
|
|
||||||
|
DockArea preferred_area() const override
|
||||||
|
{
|
||||||
|
PYBIND11_OVERRIDE(DockArea, IPanelProvider, preferred_area);
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelPersistence persistence() const override
|
||||||
|
{
|
||||||
|
PYBIND11_OVERRIDE(PanelPersistence, IPanelProvider, persistence);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string context_affinity() const override
|
||||||
|
{
|
||||||
|
PYBIND11_OVERRIDE(std::string, IPanelProvider, context_affinity);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace KCSDK
|
||||||
|
|
||||||
|
#endif // KCSDK_PYIPANELPROVIDER_H
|
||||||
107
src/Gui/SDK/bindings/PyProviderHolder.h
Normal file
107
src/Gui/SDK/bindings/PyProviderHolder.h
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
/****************************************************************************
|
||||||
|
* *
|
||||||
|
* Copyright (c) 2025 Kindred Systems <development@kindred-systems.com> *
|
||||||
|
* *
|
||||||
|
* This file is part of FreeCAD. *
|
||||||
|
* *
|
||||||
|
* FreeCAD is free software: you can redistribute it and/or modify it *
|
||||||
|
* under the terms of the GNU Lesser General Public License as *
|
||||||
|
* published by the Free Software Foundation, either version 2.1 of the *
|
||||||
|
* License, or (at your option) any later version. *
|
||||||
|
* *
|
||||||
|
* FreeCAD is distributed in the hope that it will be useful, but *
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||||
|
* Lesser General Public License for more details. *
|
||||||
|
* *
|
||||||
|
* You should have received a copy of the GNU Lesser General Public *
|
||||||
|
* License along with FreeCAD. If not, see *
|
||||||
|
* <https://www.gnu.org/licenses/>. *
|
||||||
|
* *
|
||||||
|
***************************************************************************/
|
||||||
|
|
||||||
|
#ifndef KCSDK_PYPROVIDERHOLDER_H
|
||||||
|
#define KCSDK_PYPROVIDERHOLDER_H
|
||||||
|
|
||||||
|
#include <pybind11/pybind11.h>
|
||||||
|
|
||||||
|
#include <Gui/SDK/IPanelProvider.h>
|
||||||
|
#include <Gui/SDK/WidgetBridge.h>
|
||||||
|
|
||||||
|
namespace py = pybind11;
|
||||||
|
|
||||||
|
namespace KCSDK
|
||||||
|
{
|
||||||
|
|
||||||
|
/// GIL-safe forwarding wrapper that holds a Python IPanelProvider instance.
|
||||||
|
///
|
||||||
|
/// Stores the py::object to prevent garbage collection. Acquires the GIL
|
||||||
|
/// before every call into Python. For create_widget(), the Python return
|
||||||
|
/// (a PySide QWidget) is converted to a C++ QWidget* via WidgetBridge.
|
||||||
|
///
|
||||||
|
/// Follows the PySolverHolder pattern from kcsolve_py.cpp.
|
||||||
|
class PyProviderHolder : public IPanelProvider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit PyProviderHolder(py::object obj)
|
||||||
|
: obj_(std::move(obj))
|
||||||
|
{
|
||||||
|
provider_ = obj_.cast<IPanelProvider*>();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string id() const override
|
||||||
|
{
|
||||||
|
py::gil_scoped_acquire gil;
|
||||||
|
return provider_->id();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string title() const override
|
||||||
|
{
|
||||||
|
py::gil_scoped_acquire gil;
|
||||||
|
return provider_->title();
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget* create_widget() override
|
||||||
|
{
|
||||||
|
py::gil_scoped_acquire gil;
|
||||||
|
try {
|
||||||
|
// Call the Python create_widget() which returns a PySide QWidget.
|
||||||
|
py::object pyWidget = obj_.attr("create_widget")();
|
||||||
|
if (pyWidget.is_none()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return WidgetBridge::toQWidget(pyWidget.ptr());
|
||||||
|
}
|
||||||
|
catch (py::error_already_set& e) {
|
||||||
|
e.discard_as_unraisable(__func__);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DockArea preferred_area() const override
|
||||||
|
{
|
||||||
|
py::gil_scoped_acquire gil;
|
||||||
|
return provider_->preferred_area();
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelPersistence persistence() const override
|
||||||
|
{
|
||||||
|
py::gil_scoped_acquire gil;
|
||||||
|
return provider_->persistence();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string context_affinity() const override
|
||||||
|
{
|
||||||
|
py::gil_scoped_acquire gil;
|
||||||
|
return provider_->context_affinity();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
py::object obj_; ///< Prevents Python GC — keeps reference alive.
|
||||||
|
IPanelProvider* provider_; ///< Raw pointer into trampoline inside obj_.
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace KCSDK
|
||||||
|
|
||||||
|
#endif // KCSDK_PYPROVIDERHOLDER_H
|
||||||
@@ -25,9 +25,14 @@
|
|||||||
#include <pybind11/stl.h>
|
#include <pybind11/stl.h>
|
||||||
#include <pybind11/functional.h>
|
#include <pybind11/functional.h>
|
||||||
|
|
||||||
|
#include <Gui/SDK/IPanelProvider.h>
|
||||||
#include <Gui/SDK/SDKRegistry.h>
|
#include <Gui/SDK/SDKRegistry.h>
|
||||||
|
#include <Gui/SDK/ThemeEngine.h>
|
||||||
#include <Gui/SDK/Types.h>
|
#include <Gui/SDK/Types.h>
|
||||||
|
|
||||||
|
#include "PyIPanelProvider.h"
|
||||||
|
#include "PyProviderHolder.h"
|
||||||
|
|
||||||
namespace py = pybind11;
|
namespace py = pybind11;
|
||||||
using namespace KCSDK;
|
using namespace KCSDK;
|
||||||
|
|
||||||
@@ -177,4 +182,116 @@ PYBIND11_MODULE(kcsdk, m)
|
|||||||
SDKRegistry::instance().refresh();
|
SDKRegistry::instance().refresh();
|
||||||
},
|
},
|
||||||
"Force re-resolution of the editing context.");
|
"Force re-resolution of the editing context.");
|
||||||
|
|
||||||
|
// -- Enums --------------------------------------------------------------
|
||||||
|
|
||||||
|
py::enum_<DockArea>(m, "DockArea")
|
||||||
|
.value("Left", DockArea::Left)
|
||||||
|
.value("Right", DockArea::Right)
|
||||||
|
.value("Top", DockArea::Top)
|
||||||
|
.value("Bottom", DockArea::Bottom);
|
||||||
|
|
||||||
|
py::enum_<PanelPersistence>(m, "PanelPersistence")
|
||||||
|
.value("Session", PanelPersistence::Session)
|
||||||
|
.value("Persistent", PanelPersistence::Persistent);
|
||||||
|
|
||||||
|
// -- Panel provider API -------------------------------------------------
|
||||||
|
|
||||||
|
py::class_<IPanelProvider, PyIPanelProvider>(m, "IPanelProvider")
|
||||||
|
.def(py::init<>())
|
||||||
|
.def("id", &IPanelProvider::id)
|
||||||
|
.def("title", &IPanelProvider::title)
|
||||||
|
// create_widget() is not bound directly — Python subclasses override
|
||||||
|
// it and return a PySide QWidget. The C++ side invokes it via
|
||||||
|
// PyProviderHolder which handles the PySide→QWidget* conversion
|
||||||
|
// through WidgetBridge.
|
||||||
|
.def("preferred_area", &IPanelProvider::preferred_area)
|
||||||
|
.def("persistence", &IPanelProvider::persistence)
|
||||||
|
.def("context_affinity", &IPanelProvider::context_affinity);
|
||||||
|
|
||||||
|
m.def("register_panel",
|
||||||
|
[](py::object provider) {
|
||||||
|
auto holder = std::make_unique<PyProviderHolder>(std::move(provider));
|
||||||
|
SDKRegistry::instance().registerPanel(std::move(holder));
|
||||||
|
},
|
||||||
|
py::arg("provider"),
|
||||||
|
"Register a dock panel provider.\n\n"
|
||||||
|
"Parameters\n"
|
||||||
|
"----------\n"
|
||||||
|
"provider : IPanelProvider\n"
|
||||||
|
" Panel provider instance implementing id(), title(), create_widget().");
|
||||||
|
|
||||||
|
m.def("unregister_panel",
|
||||||
|
[](const std::string& id) {
|
||||||
|
SDKRegistry::instance().unregisterPanel(id);
|
||||||
|
},
|
||||||
|
py::arg("id"),
|
||||||
|
"Remove a registered panel provider and its dock widget.");
|
||||||
|
|
||||||
|
m.def("create_panel",
|
||||||
|
[](const std::string& id) {
|
||||||
|
SDKRegistry::instance().createPanel(id);
|
||||||
|
},
|
||||||
|
py::arg("id"),
|
||||||
|
"Instantiate the dock widget for a registered panel.");
|
||||||
|
|
||||||
|
m.def("create_all_panels",
|
||||||
|
[]() {
|
||||||
|
SDKRegistry::instance().createAllPanels();
|
||||||
|
},
|
||||||
|
"Instantiate dock widgets for all registered panels.");
|
||||||
|
|
||||||
|
m.def("registered_panels",
|
||||||
|
[]() {
|
||||||
|
return SDKRegistry::instance().registeredPanels();
|
||||||
|
},
|
||||||
|
"Return IDs of all registered panel providers.");
|
||||||
|
|
||||||
|
// -- Theme engine API ---------------------------------------------------
|
||||||
|
|
||||||
|
m.def("theme_color",
|
||||||
|
[](const std::string& token) {
|
||||||
|
auto& engine = ThemeEngine::instance();
|
||||||
|
if (engine.activePaletteName().empty()) {
|
||||||
|
engine.loadPalette();
|
||||||
|
}
|
||||||
|
return engine.getColor(token);
|
||||||
|
},
|
||||||
|
py::arg("token"),
|
||||||
|
"Look up a color by role or name.\n\n"
|
||||||
|
"Returns the hex string (e.g. \"#89b4fa\") or empty string if not found.\n"
|
||||||
|
"Auto-loads the default palette on first call.");
|
||||||
|
|
||||||
|
m.def("theme_tokens",
|
||||||
|
[]() {
|
||||||
|
auto& engine = ThemeEngine::instance();
|
||||||
|
if (engine.activePaletteName().empty()) {
|
||||||
|
engine.loadPalette();
|
||||||
|
}
|
||||||
|
return engine.allTokens();
|
||||||
|
},
|
||||||
|
"Return all color tokens as {name: \"#hex\"}.\n\n"
|
||||||
|
"Includes both raw colors and resolved semantic roles.\n"
|
||||||
|
"Auto-loads the default palette on first call.");
|
||||||
|
|
||||||
|
m.def("format_qss",
|
||||||
|
[](const std::string& templateStr) {
|
||||||
|
auto& engine = ThemeEngine::instance();
|
||||||
|
if (engine.activePaletteName().empty()) {
|
||||||
|
engine.loadPalette();
|
||||||
|
}
|
||||||
|
return engine.formatQss(templateStr);
|
||||||
|
},
|
||||||
|
py::arg("template_str"),
|
||||||
|
"Substitute {token} placeholders in a QSS template.\n\n"
|
||||||
|
"Both raw color names ({blue}) and dotted role names\n"
|
||||||
|
"({accent.primary}) are supported. Unknown tokens are left as-is.\n"
|
||||||
|
"Auto-loads the default palette on first call.");
|
||||||
|
|
||||||
|
m.def("load_palette",
|
||||||
|
[](const std::string& name) {
|
||||||
|
return ThemeEngine::instance().loadPalette(name);
|
||||||
|
},
|
||||||
|
py::arg("name") = "catppuccin-mocha",
|
||||||
|
"Load a named palette. Returns True on success.");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user