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
This commit is contained in:
2026-02-10 08:27:01 -06:00
parent c25aa17591
commit 1561dff860
2 changed files with 538 additions and 0 deletions

View File

@@ -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)

View File

@@ -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<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.
```cpp
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.
```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/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](./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