# 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](./python-custom-origin-guide.md). ## Prerequisites Familiarity with: - [FileOrigin Interface](./cpp-file-origin.md) — the abstract base class - [OriginManager](./cpp-origin-manager.md) — registration and document resolution - [CommandOrigin](./cpp-command-origin.md) — 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 ```cpp 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/`: ```cpp // 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()`. ```cpp 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. ```cpp 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: ```cpp 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( obj->getPropertyByName(MY_TRACKING_PROP)); if (!prop) { prop = static_cast( 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. ```cpp std::string MyOrigin::documentIdentity(App::Document* doc) const { if (!doc || !ownsDocument(doc)) { return {}; } for (auto* obj : doc->getObjects()) { auto* prop = dynamic_cast( 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( 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. ```cpp 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 ```cpp 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 ```cpp 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: ```cpp 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` | ```cpp 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: ```cpp 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: ```cpp #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): ```cpp 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`: ```cmake SET(Gui_SRCS # ... existing files ... MyOrigin.cpp MyOrigin.h ) ``` Include dependencies in your source file: ```cpp #include "MyOrigin.h" #include "BitmapFactory.h" // Icons #include "Application.h" // Gui::Application #include "FileDialog.h" // File dialogs #include "MainWindow.h" // getMainWindow() #include // App::GetApplication() #include // App::Document #include // 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](./cpp-file-origin.md) — complete API reference - [LocalFileOrigin](./cpp-local-file-origin.md) — reference implementation (simplest origin) - [FileOriginPython Bridge](./cpp-file-origin-python.md) — how Python origins connect to the C++ layer - [Creating a Custom Origin in Python](./python-custom-origin-guide.md) — Python alternative (no rebuild needed) - [OriginManager](./cpp-origin-manager.md) — registration and document resolution - [OriginSelectorWidget](./cpp-origin-selector-widget.md) — toolbar UI integration - [CommandOrigin](./cpp-command-origin.md) — command dispatch