From 1561dff860e5beba1b77fabf864482b0f90c3e8e Mon Sep 17 00:00:00 2001 From: forbes-0023 Date: Tue, 10 Feb 2026 08:27:01 -0600 Subject: [PATCH] docs(c++): guide to creating a custom origin in C++ Step-by-step walkthrough covering class definition, identity methods, ownership detection with tracking properties, document operations, capability flags, extended PLM operations, connection lifecycle, OriginManager registration, and CMake build integration. Closes #136 --- docs/src/SUMMARY.md | 1 + docs/src/reference/cpp-custom-origin-guide.md | 537 ++++++++++++++++++ 2 files changed, 538 insertions(+) create mode 100644 docs/src/reference/cpp-custom-origin-guide.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 770d3f4f86..697e8f20da 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -55,3 +55,4 @@ - [CommandOrigin](./reference/cpp-command-origin.md) - [OriginSelectorWidget](./reference/cpp-origin-selector-widget.md) - [FileOriginPython Bridge](./reference/cpp-file-origin-python.md) +- [Creating a Custom Origin (C++)](./reference/cpp-custom-origin-guide.md) diff --git a/docs/src/reference/cpp-custom-origin-guide.md b/docs/src/reference/cpp-custom-origin-guide.md new file mode 100644 index 0000000000..36b804aa4b --- /dev/null +++ b/docs/src/reference/cpp-custom-origin-guide.md @@ -0,0 +1,537 @@ +# 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