Wholesale copy of all Kindred Create additions that don't conflict with upstream FreeCAD code: - kindred-icons/ (1444 Catppuccin Mocha SVG icon overrides) - src/Mod/Create/ (Kindred Create workbench) - src/Gui/ Kindred source files (FileOrigin, OriginManager, OriginSelectorWidget, CommandOrigin, BreadcrumbToolBar, EditingContext) - src/Gui/Icons/ (Kindred branding and silo icons) - src/Gui/PreferencePacks/KindredCreate/ - src/Gui/Stylesheets/ (KindredCreate.qss, images_dark-light/) - package/ (rattler-build recipe) - docs/ (architecture, guides, specifications) - .gitea/ (CI workflows, issue templates) - mods/silo, mods/ztools submodules - .gitmodules (Kindred submodule URLs) - resources/ (kindred-create.desktop, kindred-create.xml) - banner-logo-light.png, CONTRIBUTING.md
258 lines
8.8 KiB
Markdown
258 lines
8.8 KiB
Markdown
# FileOriginPython — Python-to-C++ Bridge
|
|
|
|
> **Header:** `src/Gui/FileOriginPython.h`
|
|
> **Implementation:** `src/Gui/FileOriginPython.cpp`
|
|
> **Python binding:** `src/Gui/ApplicationPy.cpp`
|
|
> **Namespace:** `Gui`
|
|
|
|
`FileOriginPython` is an Adapter that wraps a Python object and presents
|
|
it as a C++ [FileOrigin](./cpp-file-origin.md). This is how Python
|
|
addons (like SiloOrigin) integrate with the C++
|
|
[OriginManager](./cpp-origin-manager.md) without compiling any C++ code.
|
|
|
|
## Registration from Python
|
|
|
|
The bridge is exposed to Python through three `FreeCADGui` module
|
|
functions:
|
|
|
|
```python
|
|
import FreeCADGui
|
|
|
|
FreeCADGui.addOrigin(obj) # register a Python origin
|
|
FreeCADGui.removeOrigin(obj) # unregister it
|
|
FreeCADGui.getOrigin(obj) # look up the wrapper (or None)
|
|
```
|
|
|
|
### `FreeCADGui.addOrigin(obj)`
|
|
|
|
1. Checks that `obj` is not already registered (duplicate check via
|
|
`findOrigin`).
|
|
2. Constructs a `FileOriginPython` wrapper around `obj`.
|
|
3. Calls `obj.id()` immediately and caches the result — the ID must be
|
|
non-empty or registration fails.
|
|
4. Passes the wrapper to
|
|
`OriginManager::instance()->registerOrigin(wrapper)`.
|
|
5. If `registerOrigin` fails (e.g., duplicate ID), the wrapper is removed
|
|
from the internal instances list (OriginManager already deleted it).
|
|
|
|
### `FreeCADGui.removeOrigin(obj)`
|
|
|
|
1. Finds the wrapper via `findOrigin(obj)`.
|
|
2. Removes it from the internal `_instances` vector.
|
|
3. Calls `OriginManager::instance()->unregisterOrigin(id)`, which deletes
|
|
the wrapper.
|
|
|
|
### Typical Usage (from SiloOrigin)
|
|
|
|
```python
|
|
# mods/silo/freecad/silo_origin.py
|
|
|
|
class SiloOrigin:
|
|
def id(self): return "silo"
|
|
def name(self): return "Kindred Silo"
|
|
# ... remaining interface methods ...
|
|
|
|
_silo_origin = SiloOrigin()
|
|
|
|
def register_silo_origin():
|
|
FreeCADGui.addOrigin(_silo_origin)
|
|
|
|
def unregister_silo_origin():
|
|
FreeCADGui.removeOrigin(_silo_origin)
|
|
```
|
|
|
|
This is called from `InitGui.py` via a deferred `QTimer.singleShot`
|
|
(1500 ms after GUI init) to ensure the Gui module is fully loaded.
|
|
|
|
## Method Dispatch
|
|
|
|
Every C++ `FileOrigin` virtual method is implemented by delegating to the
|
|
corresponding Python method on the wrapped object. Three internal
|
|
helpers handle the marshalling:
|
|
|
|
| Helper | Signature | Used For |
|
|
|--------|-----------|----------|
|
|
| `callStringMethod` | `(name, default) → std::string` | `id()`, `name()`, `nickname()`, `icon()` |
|
|
| `callBoolMethod` | `(name, default) → bool` | All `supportsXxx()`, `tracksExternally()`, etc. |
|
|
| `callMethod` | `(name, args) → Py::Object` | Everything else (document ops, state queries) |
|
|
|
|
### Dispatch Pattern
|
|
|
|
Each overridden method follows the same structure:
|
|
|
|
```
|
|
1. Acquire GIL ← Base::PyGILStateLocker lock
|
|
2. Check if Python object has attr ← _inst.hasAttr("methodName")
|
|
3. If missing → return default ← graceful degradation
|
|
4. Build Py::Tuple args ← marshal C++ args to Python
|
|
5. Call Python method ← func.apply(args)
|
|
6. Convert result ← Py::String/Boolean/Long → C++ type
|
|
7. Return ← release GIL on scope exit
|
|
```
|
|
|
|
If the Python method is missing, the bridge returns sensible defaults
|
|
(empty string, `false`, `nullptr`). If the Python method raises an
|
|
exception, it is caught, reported to the FreeCAD console via
|
|
`Base::PyException::reportException()`, and the default is returned.
|
|
|
|
### Document Argument Marshalling
|
|
|
|
Methods that take `App::Document*` convert it to a Python object using:
|
|
|
|
```cpp
|
|
Py::Object FileOriginPython::getDocPyObject(App::Document* doc) const
|
|
{
|
|
if (!doc) return Py::None();
|
|
return Py::asObject(doc->getPyObject());
|
|
}
|
|
```
|
|
|
|
This returns the standard `App.Document` Python wrapper so the Python
|
|
origin can call `doc.Objects`, `doc.FileName`, `doc.save()`, etc.
|
|
|
|
Methods that **return** an `App::Document*` (like `newDocument`,
|
|
`openDocument`) check the return value with:
|
|
|
|
```cpp
|
|
if (PyObject_TypeCheck(result.ptr(), &(App::DocumentPy::Type))) {
|
|
return static_cast<App::DocumentPy*>(result.ptr())->getDocumentPtr();
|
|
}
|
|
```
|
|
|
|
If the Python method returns `None` or a non-Document object, the bridge
|
|
returns `nullptr`.
|
|
|
|
## GIL Management
|
|
|
|
Every method acquires the GIL (`Base::PyGILStateLocker`) before touching
|
|
any Python objects. This is critical because the C++ origin methods can
|
|
be called from:
|
|
|
|
- The main Qt event loop (menu clicks, toolbar actions)
|
|
- `OriginManager::findOwningOrigin()` during document operations
|
|
- Signal handlers (fastsignals callbacks)
|
|
|
|
All of these may run on the main thread while the GIL is not held.
|
|
|
|
## Enum Mapping
|
|
|
|
Python origins return integers for enum values:
|
|
|
|
### `OriginType` (from `type()`)
|
|
|
|
| Python `int` | C++ Enum |
|
|
|-------------|----------|
|
|
| `0` | `OriginType::Local` |
|
|
| `1` | `OriginType::PLM` |
|
|
| `2` | `OriginType::Cloud` |
|
|
| `3` | `OriginType::Custom` |
|
|
|
|
### `ConnectionState` (from `connectionState()`)
|
|
|
|
| Python `int` | C++ Enum |
|
|
|-------------|----------|
|
|
| `0` | `ConnectionState::Disconnected` |
|
|
| `1` | `ConnectionState::Connecting` |
|
|
| `2` | `ConnectionState::Connected` |
|
|
| `3` | `ConnectionState::Error` |
|
|
|
|
Out-of-range values fall back to `OriginType::Custom` and
|
|
`ConnectionState::Connected` respectively.
|
|
|
|
## Icon Resolution
|
|
|
|
The `icon()` override calls `callStringMethod("icon")` to get a string
|
|
name, then passes it to `BitmapFactory().iconFromTheme(name)`. The
|
|
Python origin returns an icon name (e.g., `"silo"`) rather than a QIcon
|
|
object.
|
|
|
|
## Required Python Interface
|
|
|
|
Methods the Python object **must** implement (called unconditionally):
|
|
|
|
| Method | Signature | Purpose |
|
|
|--------|-----------|---------|
|
|
| `id()` | `→ str` | Unique origin ID (cached on registration) |
|
|
| `name()` | `→ str` | Display name |
|
|
| `nickname()` | `→ str` | Short toolbar label |
|
|
| `icon()` | `→ str` | Icon name for `BitmapFactory` |
|
|
| `type()` | `→ int` | `OriginType` enum value |
|
|
| `tracksExternally()` | `→ bool` | Whether origin syncs externally |
|
|
| `requiresAuthentication()` | `→ bool` | Whether login is needed |
|
|
| `ownsDocument(doc)` | `→ bool` | Ownership detection |
|
|
| `documentIdentity(doc)` | `→ str` | Immutable tracking key |
|
|
| `documentDisplayId(doc)` | `→ str` | Human-readable ID |
|
|
|
|
Methods the Python object **may** implement (checked with `hasAttr`):
|
|
|
|
| Method | Signature | Default |
|
|
|--------|-----------|---------|
|
|
| `supportsRevisions()` | `→ bool` | `False` |
|
|
| `supportsBOM()` | `→ bool` | `False` |
|
|
| `supportsPartNumbers()` | `→ bool` | `False` |
|
|
| `supportsAssemblies()` | `→ bool` | `False` |
|
|
| `connectionState()` | `→ int` | `2` (Connected) |
|
|
| `connect()` | `→ bool` | `True` |
|
|
| `disconnect()` | `→ None` | no-op |
|
|
| `syncProperties(doc)` | `→ bool` | `True` |
|
|
| `newDocument(name)` | `→ Document` | `None` |
|
|
| `openDocument(identity)` | `→ Document` | `None` |
|
|
| `openDocumentInteractive()` | `→ Document` | `None` |
|
|
| `saveDocument(doc)` | `→ bool` | `False` |
|
|
| `saveDocumentAs(doc, id)` | `→ bool` | `False` |
|
|
| `saveDocumentAsInteractive(doc)` | `→ bool` | `False` |
|
|
| `commitDocument(doc)` | `→ bool` | `False` |
|
|
| `pullDocument(doc)` | `→ bool` | `False` |
|
|
| `pushDocument(doc)` | `→ bool` | `False` |
|
|
| `showInfo(doc)` | `→ None` | no-op |
|
|
| `showBOM(doc)` | `→ None` | no-op |
|
|
|
|
## Error Handling
|
|
|
|
All Python exceptions are caught and reported to the FreeCAD console.
|
|
The bridge **never** propagates a Python exception into C++ — callers
|
|
always receive a safe default value.
|
|
|
|
```
|
|
[Python exception in SiloOrigin.ownsDocument]
|
|
→ Base::PyException::reportException()
|
|
→ FreeCAD Console: "Python error: ..."
|
|
→ return false
|
|
```
|
|
|
|
This prevents a buggy Python origin from crashing the application.
|
|
|
|
## Lifetime and Ownership
|
|
|
|
```
|
|
FreeCADGui.addOrigin(py_obj)
|
|
│
|
|
├─ FileOriginPython(py_obj) ← wraps, holds Py::Object ref
|
|
│ └─ _inst = py_obj ← prevents Python GC
|
|
│
|
|
├─ _instances.push_back(wrapper) ← static vector for findOrigin()
|
|
│
|
|
└─ OriginManager::registerOrigin(wrapper)
|
|
└─ unique_ptr<FileOrigin> ← OriginManager owns the wrapper
|
|
```
|
|
|
|
- `FileOriginPython` holds a `Py::Object` reference to the Python
|
|
instance, preventing garbage collection.
|
|
- `OriginManager` owns the wrapper via `std::unique_ptr`.
|
|
- The static `_instances` vector provides `findOrigin()` lookup but does
|
|
**not** own the pointers.
|
|
- On `removeOrigin()`: the wrapper is removed from `_instances`, then
|
|
`OriginManager::unregisterOrigin()` deletes the wrapper, which releases
|
|
the `Py::Object` reference.
|
|
|
|
## See Also
|
|
|
|
- [FileOrigin Interface](./cpp-file-origin.md) — the abstract interface
|
|
being adapted
|
|
- [OriginManager](./cpp-origin-manager.md) — where the wrapped origin
|
|
gets registered
|
|
- [CommandOrigin](./cpp-command-origin.md) — commands that dispatch
|
|
through this bridge
|
|
- [Creating a Custom Origin (Python)](../guide/custom-origin-python.md)
|
|
— step-by-step guide using this bridge
|