Files
create/docs/src/reference/cpp-custom-origin-guide.md
forbes 87a0af0b0f phase 1: copy Kindred-only files onto upstream/main (FreeCAD 1.2.0-dev)
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
2026-02-13 14:03:58 -06:00

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:

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 true only for documents that have your tracking marker.
  • The built-in LocalFileOrigin claims 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:

  1. OriginCmdCommit::isActive() checks origin->supportsRevisions() — if false, the button is greyed out.
  2. OriginCmdCommit::activated() calls OriginManager::instance()->findOwningOrigin(doc) to get the origin for the active document.
  3. It then calls origin->commitDocument(doc).
  4. 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.
  • OriginManager takes ownership of the pointer via std::unique_ptr.
  • The origin appears in OriginSelectorWidget immediately (the widget listens to signalOriginRegistered).
  • 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