docs(c++): LocalFileOrigin implementation guide

Covers ownership-by-exclusion algorithm, document identity, open/save
workflows, capability flags, connection state, OriginManager integration,
and unregister protection.

Closes #131
This commit is contained in:
2026-02-10 08:14:29 -06:00
parent 77e5063b95
commit 8fd664d509
2 changed files with 219 additions and 0 deletions

View File

@@ -50,6 +50,7 @@
# C++ API Reference
- [FileOrigin Interface](./reference/cpp-file-origin.md)
- [LocalFileOrigin](./reference/cpp-local-file-origin.md)
- [OriginManager](./reference/cpp-origin-manager.md)
- [CommandOrigin](./reference/cpp-command-origin.md)
- [FileOriginPython Bridge](./reference/cpp-file-origin-python.md)

View File

@@ -0,0 +1,218 @@
# LocalFileOrigin
`LocalFileOrigin` is the built-in, default `FileOrigin` implementation. It handles documents stored on the local filesystem without external tracking or synchronisation.
- **Header:** `src/Gui/FileOrigin.h`
- **Source:** `src/Gui/FileOrigin.cpp`
- **Origin ID:** `"local"`
- **Display name:** `"Local Files"` (nickname `"Local"`)
- **Type:** `OriginType::Local`
## Design principles
1. **Identity is the file path.** A document's identity under LocalFileOrigin is its `FileName` property — the absolute path on disk.
2. **Ownership by exclusion.** LocalFileOrigin claims every document that is *not* claimed by a PLM origin. It detects this by scanning for the `SiloItemId` property; if no object in the document has it, the document is local.
3. **Always available.** LocalFileOrigin is created by `OriginManager` at startup and cannot be unregistered. It is the mandatory fallback origin.
4. **Thin delegation.** Most operations delegate directly to `App::GetApplication()` or `App::Document` methods. LocalFileOrigin adds no persistence layer of its own.
## Identity and capabilities
```cpp
std::string id() const override; // "local"
std::string name() const override; // "Local Files"
std::string nickname() const override; // "Local"
QIcon icon() const override; // theme icon "document-new"
OriginType type() const override; // OriginType::Local
```
### Capability flags
All capability queries return `false`:
| Method | Returns | Reason |
|--------|---------|--------|
| `tracksExternally()` | `false` | No external database or server |
| `requiresAuthentication()` | `false` | No login required |
| `supportsRevisions()` | `false` | No revision history |
| `supportsBOM()` | `false` | No Bill of Materials |
| `supportsPartNumbers()` | `false` | No part number system |
| `supportsAssemblies()` | `false` | No native assembly tracking |
These flags control which toolbar buttons and menu items are enabled. With all flags false, the origin toolbar commands (Commit, Pull, Push, Info, BOM) are disabled for local documents.
### Connection state
LocalFileOrigin uses the base-class defaults — it is always `ConnectionState::Connected`:
| Method | Behaviour |
|--------|-----------|
| `connectionState()` | Returns `ConnectionState::Connected` |
| `connect()` | Returns `true` immediately |
| `disconnect()` | No-op |
| `signalConnectionStateChanged` | Never emitted |
## Ownership detection
```cpp
bool ownsDocument(App::Document* doc) const override;
```
**Algorithm:**
1. Return `false` if `doc` is null.
2. Iterate every object in the document (`doc->getObjects()`).
3. For each object, check `obj->getPropertyByName("SiloItemId")`.
4. If **any** object has the property, return `false` — the document belongs to a PLM origin.
5. If **no** object has it, return `true`.
The constant `SiloItemId` is defined as a `static const char*` in `FileOrigin.cpp`.
**Edge cases:**
- An empty document (no objects) is owned by LocalFileOrigin.
- A document where only some objects have `SiloItemId` is still considered PLM-owned.
- Performance is O(n) where n is the number of objects in the document.
### Ownership resolution order
`OriginManager::findOwningOrigin()` checks non-local origins first, then falls back to LocalFileOrigin. This ensures PLM origins with specific ownership criteria take priority:
```
for each registered origin (excluding "local"):
if origin.ownsDocument(doc) → return that origin
if localOrigin.ownsDocument(doc) → return localOrigin
return nullptr
```
Results are cached in `OriginManager::_documentOrigins` for O(1) subsequent lookups.
## Document identity
```cpp
std::string documentIdentity(App::Document* doc) const override;
std::string documentDisplayId(App::Document* doc) const override;
```
Both return the same value: the document's `FileName` property (its absolute filesystem path). Returns an empty string if the document is null, not owned, or has never been saved.
For PLM origins these would differ — `documentIdentity` might return a UUID while `documentDisplayId` returns a part number. For local files, the path serves both purposes.
## Document operations
### Creating documents
```cpp
App::Document* newDocument(const std::string& name = "") override;
```
Delegates to `App::GetApplication().newDocument()`. If `name` is empty, defaults to `"Unnamed"`. The returned document has no `FileName` set until saved.
### Opening documents
```cpp
App::Document* openDocument(const std::string& identity) override;
App::Document* openDocumentInteractive() override;
```
**`openDocument`** — Non-interactive. Takes a file path, delegates to `App::GetApplication().openDocument()`. Returns `nullptr` if the path is empty or the file cannot be loaded.
**`openDocumentInteractive`** — Shows a file dialog with the following behaviour:
1. Builds a format filter list from FreeCAD's registered import types, with `.FCStd` first.
2. Shows `FileDialog::getOpenFileNames()` (multi-file selection).
3. For each selected file, uses `SelectModule::importHandler()` to determine the loader module.
4. Calls `Application::Instance->open()` with the `UserInitiatedOpenDocument` flag set.
5. Runs `checkPartialRestore()` and `checkRestoreError()` for validation.
6. Returns the last loaded document, or `nullptr` if the dialog was cancelled.
### Saving documents
```cpp
bool saveDocument(App::Document* doc) override;
bool saveDocumentAs(App::Document* doc, const std::string& newIdentity) override;
bool saveDocumentAsInteractive(App::Document* doc) override;
```
**`saveDocument`** — Requires that the document already has a `FileName` set. If the path is empty (never-saved document), returns `false`. Otherwise calls `doc->save()`.
**`saveDocumentAs`** — Non-interactive. Calls `doc->saveAs(newIdentity)` with the given path. Updates the document's `FileName` property as a side effect.
**`saveDocumentAsInteractive`** — Gets the `Gui::Document` wrapper and delegates to `guiDoc->saveAs()`, which shows a save dialog. Returns `false` if the GUI document cannot be found or the user cancels.
**Save workflow pattern:**
```
First save: saveDocument() → false (no path)
→ command layer calls saveDocumentAsInteractive()
→ user picks path → saveAs()
Later saves: saveDocument() → true (path exists)
→ doc->save()
```
### PLM operations (not implemented)
These inherited base-class methods are no-ops for LocalFileOrigin:
| Method | Returns | Behaviour |
|--------|---------|-----------|
| `commitDocument(doc)` | `false` | No external commits |
| `pullDocument(doc)` | `false` | No remote to pull from |
| `pushDocument(doc)` | `false` | No remote to push to |
| `showInfo(doc)` | void | No-op |
| `showBOM(doc)` | void | No-op |
## OriginManager integration
### Lifecycle
LocalFileOrigin is created in the `OriginManager` constructor via `ensureLocalOrigin()`:
```cpp
auto localOrigin = std::make_unique<LocalFileOrigin>();
_origins[LOCAL_ORIGIN_ID] = std::move(localOrigin);
_currentOriginId = LOCAL_ORIGIN_ID;
```
It is set as the default current origin. It is the only origin that exists before any Python workbenches load.
### Unregister protection
`OriginManager::unregisterOrigin()` explicitly blocks removal of the local origin:
```cpp
if (id == LOCAL_ORIGIN_ID) {
Base::Console().warning(
"OriginManager: Cannot unregister built-in local origin\n");
return false;
}
```
### Preference persistence
The current origin ID is saved to `User parameter:BaseApp/Preferences/General/Origin` under the key `CurrentOriginId`. On restart, if the saved ID is not `"local"` but the corresponding origin hasn't been registered yet, LocalFileOrigin remains the active origin until a Python workbench registers the saved origin.
## Constants
Defined in `FileOrigin.cpp`:
```cpp
static const char* SILO_ITEM_ID_PROP = "SiloItemId";
```
Defined in `OriginManager.cpp`:
```cpp
static const char* LOCAL_ORIGIN_ID = "local";
static const char* PREF_PATH = "User parameter:BaseApp/Preferences/General/Origin";
static const char* PREF_CURRENT_ORIGIN = "CurrentOriginId";
```
## See also
- [FileOrigin Interface](./cpp-file-origin.md) — abstract base class
- [OriginManager](./cpp-origin-manager.md) — singleton registry and document resolution
- [FileOriginPython Bridge](./cpp-file-origin-python.md) — Python adapter for custom origins
- [SiloOrigin](./python-silo-origin.md) — PLM origin that contrasts with LocalFileOrigin