From 8fd664d5090b97f6396e758f898d7325914b6f3b Mon Sep 17 00:00:00 2001 From: forbes-0023 Date: Tue, 10 Feb 2026 08:14:29 -0600 Subject: [PATCH] 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 --- docs/src/SUMMARY.md | 1 + docs/src/reference/cpp-local-file-origin.md | 218 ++++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 docs/src/reference/cpp-local-file-origin.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 467f53a381..6e5c126588 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -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) diff --git a/docs/src/reference/cpp-local-file-origin.md b/docs/src/reference/cpp-local-file-origin.md new file mode 100644 index 0000000000..ce7d5ab611 --- /dev/null +++ b/docs/src/reference/cpp-local-file-origin.md @@ -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(); +_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