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
8.8 KiB
FileOriginPython — Python-to-C++ Bridge
Header:
src/Gui/FileOriginPython.hImplementation:src/Gui/FileOriginPython.cppPython binding:src/Gui/ApplicationPy.cppNamespace:Gui
FileOriginPython is an Adapter that wraps a Python object and presents
it as a C++ FileOrigin. This is how Python
addons (like SiloOrigin) integrate with the C++
OriginManager without compiling any C++ code.
Registration from Python
The bridge is exposed to Python through three FreeCADGui module
functions:
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)
- Checks that
objis not already registered (duplicate check viafindOrigin). - Constructs a
FileOriginPythonwrapper aroundobj. - Calls
obj.id()immediately and caches the result — the ID must be non-empty or registration fails. - Passes the wrapper to
OriginManager::instance()->registerOrigin(wrapper). - If
registerOriginfails (e.g., duplicate ID), the wrapper is removed from the internal instances list (OriginManager already deleted it).
FreeCADGui.removeOrigin(obj)
- Finds the wrapper via
findOrigin(obj). - Removes it from the internal
_instancesvector. - Calls
OriginManager::instance()->unregisterOrigin(id), which deletes the wrapper.
Typical Usage (from SiloOrigin)
# 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:
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:
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
FileOriginPythonholds aPy::Objectreference to the Python instance, preventing garbage collection.OriginManagerowns the wrapper viastd::unique_ptr.- The static
_instancesvector providesfindOrigin()lookup but does not own the pointers. - On
removeOrigin(): the wrapper is removed from_instances, thenOriginManager::unregisterOrigin()deletes the wrapper, which releases thePy::Objectreference.
See Also
- FileOrigin Interface — the abstract interface being adapted
- OriginManager — where the wrapped origin gets registered
- CommandOrigin — commands that dispatch through this bridge
- Creating a Custom Origin (Python) — step-by-step guide using this bridge