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
18 KiB
Creating a Custom Origin in C++
This guide walks through implementing and registering a new FileOrigin backend in C++. By the end you will have a working origin that appears in the toolbar selector, responds to File menu commands, and integrates with the command dispatch system.
For the Python equivalent, see Creating a Custom Origin in Python.
Prerequisites
Familiarity with:
- FileOrigin Interface — the abstract base class
- OriginManager — registration and document resolution
- CommandOrigin — how commands dispatch to origins
What an origin does
An origin defines how documents are tracked and stored. It answers:
- Does this document belong to me? (
ownsDocument) - What is this document's identity? (
documentIdentity) - How do I create, open, and save documents? (6 document operations)
- What extended capabilities do I support? (revisions, BOM, part numbers)
Once registered, the origin appears in the toolbar selector and all File/Origin commands automatically dispatch to it for documents it owns.
Step 1: Choose your origin type
enum class OriginType { Local, PLM, Cloud, Custom };
| Type | Use when | tracksExternally |
requiresAuthentication |
|---|---|---|---|
Local |
Filesystem storage, no sync | false |
false |
PLM |
Database-backed with revisions (Silo, Teamcenter, Windchill) | true |
usually true |
Cloud |
Remote file storage (S3, OneDrive, Google Drive) | true |
usually true |
Custom |
Anything else | your choice | your choice |
Step 2: Define the class
Create a header and source file in src/Gui/:
// src/Gui/MyOrigin.h
#ifndef GUI_MY_ORIGIN_H
#define GUI_MY_ORIGIN_H
#include "FileOrigin.h"
namespace Gui {
class MyOrigin : public FileOrigin
{
public:
MyOrigin();
~MyOrigin() override = default;
// --- Identity (required) ---
std::string id() const override;
std::string name() const override;
std::string nickname() const override;
QIcon icon() const override;
OriginType type() const override;
// --- Ownership (required) ---
bool ownsDocument(App::Document* doc) const override;
std::string documentIdentity(App::Document* doc) const override;
std::string documentDisplayId(App::Document* doc) const override;
// --- Document operations (required) ---
App::Document* newDocument(const std::string& name = "") override;
App::Document* openDocument(const std::string& identity) override;
App::Document* openDocumentInteractive() override;
bool saveDocument(App::Document* doc) override;
bool saveDocumentAs(App::Document* doc,
const std::string& newIdentity) override;
bool saveDocumentAsInteractive(App::Document* doc) override;
// --- Characteristics (override defaults) ---
bool tracksExternally() const override;
bool requiresAuthentication() const override;
// --- Capabilities (override if supported) ---
bool supportsRevisions() const override;
bool supportsBOM() const override;
bool supportsPartNumbers() const override;
// --- Extended operations (implement if capabilities are true) ---
bool commitDocument(App::Document* doc) override;
bool pullDocument(App::Document* doc) override;
bool pushDocument(App::Document* doc) override;
void showInfo(App::Document* doc) override;
void showBOM(App::Document* doc) override;
// --- Connection (override for authenticated origins) ---
ConnectionState connectionState() const override;
bool connect() override;
void disconnect() override;
};
} // namespace Gui
#endif
Step 3: Implement identity methods
Every origin needs a unique id() (lowercase, alphanumeric with hyphens), a human-readable name(), a short nickname() for toolbar display, an icon(), and a type().
std::string MyOrigin::id() const { return "my-plm"; }
std::string MyOrigin::name() const { return "My PLM System"; }
std::string MyOrigin::nickname() const { return "My PLM"; }
OriginType MyOrigin::type() const { return OriginType::PLM; }
QIcon MyOrigin::icon() const
{
return BitmapFactory().iconFromTheme("server-database");
}
The nickname() appears in the toolbar button (keep it under ~15 characters). The full name() appears in tooltips.
Step 4: Implement ownership detection
ownsDocument() is called by OriginManager::findOwningOrigin() to determine which origin owns a given document. The standard pattern is to check for a tracking property on objects in the document.
static const char* MY_TRACKING_PROP = "MyPlmItemId";
bool MyOrigin::ownsDocument(App::Document* doc) const
{
if (!doc) {
return false;
}
for (auto* obj : doc->getObjects()) {
if (obj->getPropertyByName(MY_TRACKING_PROP)) {
return true;
}
}
return false;
}
Key rules:
- Return
trueonly for documents that have your tracking marker. - The built-in
LocalFileOriginclaims documents by exclusion — it owns anything that no other origin claims. Your origin must positively identify its documents. - OriginManager checks non-local origins first, then falls back to local. Your
ownsDocument()takes priority. - Keep the check fast — it runs on every document open and on command activation queries.
Setting the tracking property
When your origin creates or first saves a document, add the tracking property:
void MyOrigin::markDocument(App::Document* doc, const std::string& itemId)
{
// Add to the first object, or create a marker object
auto* obj = doc->getObjects().empty()
? doc->addObject("App::DocumentObject", "MyPlm_Marker")
: doc->getObjects().front();
auto* prop = dynamic_cast<App::PropertyString*>(
obj->getPropertyByName(MY_TRACKING_PROP));
if (!prop) {
prop = static_cast<App::PropertyString*>(
obj->addDynamicProperty("App::PropertyString", MY_TRACKING_PROP));
}
prop->setValue(itemId.c_str());
}
Step 5: Implement document identity
Two methods distinguish machine identity from display identity:
| Method | Purpose | Example (local) | Example (PLM) |
|---|---|---|---|
documentIdentity() |
Immutable tracking key | /home/user/part.FCStd |
550e8400-... (UUID) |
documentDisplayId() |
Human-readable label | /home/user/part.FCStd |
WHEEL-001-RevC |
documentIdentity() must be stable — the same document must always produce the same identity. openDocument(identity) must be able to reopen the document from this value.
std::string MyOrigin::documentIdentity(App::Document* doc) const
{
if (!doc || !ownsDocument(doc)) {
return {};
}
for (auto* obj : doc->getObjects()) {
auto* prop = dynamic_cast<App::PropertyString*>(
obj->getPropertyByName(MY_TRACKING_PROP));
if (prop) {
return prop->getValue();
}
}
return {};
}
std::string MyOrigin::documentDisplayId(App::Document* doc) const
{
if (!doc || !ownsDocument(doc)) {
return {};
}
// Show a friendly part number instead of UUID
for (auto* obj : doc->getObjects()) {
auto* prop = dynamic_cast<App::PropertyString*>(
obj->getPropertyByName("PartNumber"));
if (prop && prop->getValue()[0] != '\0') {
return prop->getValue();
}
}
// Fall back to identity
return documentIdentity(doc);
}
Step 6: Implement document operations
All six document operations are pure virtual and must be implemented.
newDocument
Called when the user creates a new document while your origin is active.
App::Document* MyOrigin::newDocument(const std::string& name)
{
// For a PLM origin, you might show a part creation dialog first
MyPartDialog dlg(getMainWindow());
if (dlg.exec() != QDialog::Accepted) {
return nullptr;
}
std::string docName = name.empty() ? "Unnamed" : name;
App::Document* doc = App::GetApplication().newDocument(docName.c_str());
// Mark the document as ours
markDocument(doc, dlg.generatedUUID());
return doc;
}
openDocument / openDocumentInteractive
App::Document* MyOrigin::openDocument(const std::string& identity)
{
if (identity.empty()) {
return nullptr;
}
// Download file from backend if not cached locally
std::string localPath = fetchFromBackend(identity);
if (localPath.empty()) {
return nullptr;
}
return App::GetApplication().openDocument(localPath.c_str());
}
App::Document* MyOrigin::openDocumentInteractive()
{
// Show a search/browse dialog
MySearchDialog dlg(getMainWindow());
if (dlg.exec() != QDialog::Accepted) {
return nullptr;
}
return openDocument(dlg.selectedIdentity());
}
saveDocument / saveDocumentAs / saveDocumentAsInteractive
bool MyOrigin::saveDocument(App::Document* doc)
{
if (!doc) {
return false;
}
const char* path = doc->FileName.getValue();
if (!path || path[0] == '\0') {
return false; // No path yet — caller will use saveDocumentAsInteractive
}
if (!doc->save()) {
return false;
}
// Sync metadata to backend
return syncToBackend(doc);
}
bool MyOrigin::saveDocumentAs(App::Document* doc,
const std::string& newIdentity)
{
if (!doc || newIdentity.empty()) {
return false;
}
std::string localPath = localPathForIdentity(newIdentity);
if (!doc->saveAs(localPath.c_str())) {
return false;
}
markDocument(doc, newIdentity);
return syncToBackend(doc);
}
bool MyOrigin::saveDocumentAsInteractive(App::Document* doc)
{
if (!doc) {
return false;
}
Gui::Document* guiDoc = Application::Instance->getDocument(doc);
if (!guiDoc) {
return false;
}
return guiDoc->saveAs();
}
Save workflow: When the user presses Ctrl+S, the command layer calls saveDocument(). If it returns false (no path set), the command layer automatically falls through to saveDocumentAsInteractive().
Step 7: Implement capabilities and extended operations
Override capability flags to enable the corresponding toolbar commands:
bool MyOrigin::supportsRevisions() const { return true; }
bool MyOrigin::supportsBOM() const { return true; }
bool MyOrigin::supportsPartNumbers() const { return true; }
Then implement the operations they gate:
| Flag | Enables commands | Methods to implement |
|---|---|---|
supportsRevisions() |
Origin_Commit, Origin_Pull, Origin_Push | commitDocument, pullDocument, pushDocument |
supportsBOM() |
Origin_BOM | showBOM |
supportsPartNumbers() |
Origin_Info | showInfo |
bool MyOrigin::commitDocument(App::Document* doc)
{
if (!doc) return false;
// Show commit dialog, upload revision
MyCommitDialog dlg(getMainWindow(), doc);
if (dlg.exec() != QDialog::Accepted) return false;
return uploadRevision(documentIdentity(doc), dlg.message());
}
bool MyOrigin::pullDocument(App::Document* doc)
{
if (!doc) return false;
// Show revision picker, download selected revision
MyRevisionDialog dlg(getMainWindow(), documentIdentity(doc));
if (dlg.exec() != QDialog::Accepted) return false;
return downloadRevision(doc, dlg.selectedRevisionId());
}
bool MyOrigin::pushDocument(App::Document* doc)
{
if (!doc) return false;
if (!doc->save()) return false;
return uploadCurrentState(documentIdentity(doc));
}
void MyOrigin::showInfo(App::Document* doc)
{
if (!doc) return;
MyInfoDialog dlg(getMainWindow(), doc);
dlg.exec();
}
void MyOrigin::showBOM(App::Document* doc)
{
if (!doc) return;
MyBOMDialog dlg(getMainWindow(), doc);
dlg.exec();
}
Commands that are not supported simply remain at their base-class defaults (return false / no-op). The toolbar buttons for unsupported commands are automatically greyed out.
How command dispatch works
When the user clicks Origin_Commit:
OriginCmdCommit::isActive()checksorigin->supportsRevisions()— iffalse, the button is greyed out.OriginCmdCommit::activated()callsOriginManager::instance()->findOwningOrigin(doc)to get the origin for the active document.- It then calls
origin->commitDocument(doc). - Your implementation runs.
No routing code is needed — the command system handles dispatch automatically based on document ownership.
Step 8: Implement connection lifecycle (authenticated origins)
If your origin requires authentication, override the connection methods:
bool MyOrigin::requiresAuthentication() const { return true; }
ConnectionState MyOrigin::connectionState() const
{
return m_connectionState; // private member
}
bool MyOrigin::connect()
{
m_connectionState = ConnectionState::Connecting;
signalConnectionStateChanged(ConnectionState::Connecting);
// Show login dialog or attempt token-based auth
MyLoginDialog dlg(getMainWindow());
if (dlg.exec() != QDialog::Accepted) {
m_connectionState = ConnectionState::Disconnected;
signalConnectionStateChanged(ConnectionState::Disconnected);
return false;
}
if (!authenticateWithServer(dlg.credentials())) {
m_connectionState = ConnectionState::Error;
signalConnectionStateChanged(ConnectionState::Error);
return false;
}
m_connectionState = ConnectionState::Connected;
signalConnectionStateChanged(ConnectionState::Connected);
return true;
}
void MyOrigin::disconnect()
{
invalidateSession();
m_connectionState = ConnectionState::Disconnected;
signalConnectionStateChanged(ConnectionState::Disconnected);
}
Connection state lifecycle:
Disconnected ──connect()──→ Connecting ──success──→ Connected
──failure──→ Error
Connected ──disconnect()──→ Disconnected
Error ──connect()──→ Connecting ──...
The OriginSelectorWidget listens to signalConnectionStateChanged and:
- Adds a red overlay for
Disconnected - Adds a yellow overlay for
Error - Shows no overlay for
Connected
When the user selects a disconnected origin in the toolbar, the widget calls connect() automatically. If connect() returns false, the selection reverts to the previous origin.
Step 9: Register with OriginManager
Call registerOrigin() during module initialisation:
#include "OriginManager.h"
#include "MyOrigin.h"
void initMyOriginModule()
{
auto* origin = new MyOrigin();
if (!OriginManager::instance()->registerOrigin(origin)) {
Base::Console().error("Failed to register MyOrigin\n");
// origin is deleted by OriginManager on failure
}
}
Registration rules:
id()must be unique. Registration fails if the ID already exists.OriginManagertakes ownership of the pointer viastd::unique_ptr.- The origin appears in
OriginSelectorWidgetimmediately (the widget listens tosignalOriginRegistered). - The built-in
"local"origin cannot be replaced.
Unregistration (if your module is unloaded):
OriginManager::instance()->unregisterOrigin("my-plm");
If the unregistered origin was the current origin, OriginManager switches back to "local".
Step 10: Build integration
Add your files to src/Gui/CMakeLists.txt:
SET(Gui_SRCS
# ... existing files ...
MyOrigin.cpp
MyOrigin.h
)
Include dependencies in your source file:
#include "MyOrigin.h"
#include "BitmapFactory.h" // Icons
#include "Application.h" // Gui::Application
#include "FileDialog.h" // File dialogs
#include "MainWindow.h" // getMainWindow()
#include <App/Application.h> // App::GetApplication()
#include <App/Document.h> // App::Document
#include <Base/Console.h> // Logging
No changes are needed to CommandOrigin.cpp, OriginSelectorWidget.cpp, or Workbench.cpp — they discover origins dynamically through OriginManager.
Implementation checklist
| Phase | Task | Status |
|---|---|---|
| Identity | id(), name(), nickname(), icon(), type() |
|
| Ownership | ownsDocument() with tracking property check |
|
| Identity | documentIdentity(), documentDisplayId() |
|
| Tracking | Set tracking property on new/first-save | |
| Operations | newDocument(), openDocument(), openDocumentInteractive() |
|
| Operations | saveDocument(), saveDocumentAs(), saveDocumentAsInteractive() |
|
| Characteristics | tracksExternally(), requiresAuthentication() |
|
| Capabilities | supportsRevisions(), supportsBOM(), supportsPartNumbers() |
|
| Extended ops | commitDocument(), pullDocument(), pushDocument() (if revisions) |
|
| Extended ops | showInfo() (if part numbers), showBOM() (if BOM) |
|
| Connection | connectionState(), connect(), disconnect() (if auth) |
|
| Registration | registerOrigin() call in module init |
|
| Build | Source files added to CMakeLists.txt |
See also
- FileOrigin Interface — complete API reference
- LocalFileOrigin — reference implementation (simplest origin)
- FileOriginPython Bridge — how Python origins connect to the C++ layer
- Creating a Custom Origin in Python — Python alternative (no rebuild needed)
- OriginManager — registration and document resolution
- OriginSelectorWidget — toolbar UI integration
- CommandOrigin — command dispatch