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:
@@ -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)
|
||||
|
||||
537
docs/src/reference/cpp-custom-origin-guide.md
Normal file
537
docs/src/reference/cpp-custom-origin-guide.md
Normal 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
|
||||
Reference in New Issue
Block a user