phase 1: copy Kindred-only files onto upstream/main (FreeCAD 1.2.0-dev)

Wholesale copy of all Kindred Create additions that don't conflict with
upstream FreeCAD code:

- kindred-icons/ (1444 Catppuccin Mocha SVG icon overrides)
- src/Mod/Create/ (Kindred Create workbench)
- src/Gui/ Kindred source files (FileOrigin, OriginManager,
  OriginSelectorWidget, CommandOrigin, BreadcrumbToolBar, EditingContext)
- src/Gui/Icons/ (Kindred branding and silo icons)
- src/Gui/PreferencePacks/KindredCreate/
- src/Gui/Stylesheets/ (KindredCreate.qss, images_dark-light/)
- package/ (rattler-build recipe)
- docs/ (architecture, guides, specifications)
- .gitea/ (CI workflows, issue templates)
- mods/silo, mods/ztools submodules
- .gitmodules (Kindred submodule URLs)
- resources/ (kindred-create.desktop, kindred-create.xml)
- banner-logo-light.png, CONTRIBUTING.md
This commit is contained in:
forbes
2026-02-13 14:03:58 -06:00
parent 5d81f8ac16
commit 87a0af0b0f
1566 changed files with 32071 additions and 6155 deletions

View File

@@ -0,0 +1,110 @@
# Configuration
## Silo workbench
### FreeCAD parameters
Stored in `User parameter:BaseApp/Preferences/Mod/KindredSilo`:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `ApiUrl` | String | (empty) | Silo server API endpoint URL |
| `SslVerify` | Bool | true | Verify SSL certificates when connecting to server |
| `CaCertPath` | String | (empty) | Path to custom CA certificate for self-signed certs |
| `ApiToken` | String | (empty) | Stored authentication token (set by `Silo_Auth`) |
| `FirstStartChecked` | Bool | false | Whether the first-start settings prompt has been shown |
| `ProjectsDir` | String | `~/projects` | Local directory for checked-out CAD files |
### Environment variables
These override the FreeCAD parameter values when set:
| Variable | Default | Description |
|----------|---------|-------------|
| `SILO_API_URL` | `http://localhost:8080/api` | Silo server API endpoint |
| `SILO_PROJECTS_DIR` | `~/projects` | Local directory for checked-out files |
### Keyboard shortcuts
Recommended shortcuts (prompted on first workbench activation):
| Shortcut | Command |
|----------|---------|
| Ctrl+O | `Silo_Open` — Search and open items |
| Ctrl+N | `Silo_New` — Schema-driven item creation form |
| Ctrl+S | `Silo_Save` — Save locally and upload |
| Ctrl+Shift+S | `Silo_Commit` — Save with revision comment |
## Update checker
Stored in `User parameter:BaseApp/Preferences/Mod/KindredCreate/Update`:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `CheckEnabled` | Bool | true | Enable or disable update checks |
| `CheckIntervalDays` | Int | 1 | Minimum days between checks |
| `LastCheckTimestamp` | String | (empty) | ISO 8601 timestamp of last successful check |
| `SkippedVersion` | String | (empty) | Version the user chose to skip |
The checker queries:
```
https://git.kindred-systems.com/api/v1/repos/kindred/create/releases?limit=10
```
It compares the current version (injected at build time via `version.py.in`) against the latest non-draft, non-prerelease tag. The `latest` rolling tag is ignored. Checks run 10 seconds after GUI startup.
To disable: set `CheckEnabled` to `false` in FreeCAD preferences, or set `CheckIntervalDays` to `0` for on-demand only.
## Theme
The default theme is **Catppuccin Mocha** applied via `KindredCreate.qss`.
| Setting | Location |
|---------|----------|
| Canonical stylesheet | `src/Gui/Stylesheets/KindredCreate.qss` |
| Preference pack | `src/Gui/PreferencePacks/KindredCreate/` |
| Default theme name | `coal` (in mdBook docs) / `KindredCreate` (in app) |
To switch themes: **Edit > Preferences > General > Stylesheet** and select a different `.qss` file.
The preference pack is synced from the canonical stylesheet at build time via CMake's `configure_file()`. Edits should be made to the canonical file, not the preference pack copy.
## Build configuration
### Version constants
Defined in the root `CMakeLists.txt`:
| Constant | Value | Description |
|----------|-------|-------------|
| `KINDRED_CREATE_VERSION` | `0.1.0` | Kindred Create version |
| `FREECAD_VERSION` | `1.0.0` | FreeCAD base version |
These are injected into `src/Mod/Create/version.py` at build time via `version.py.in`.
### CMake presets
Defined in `CMakePresets.json`:
| Preset | Platform | Build type |
|--------|----------|------------|
| `conda-linux-debug` | Linux | Debug |
| `conda-linux-release` | Linux | Release |
| `conda-macos-debug` | macOS | Debug |
| `conda-macos-release` | macOS | Release |
| `conda-windows-debug` | Windows | Debug |
| `conda-windows-release` | Windows | Release |
### ccache
| Setting | Value |
|---------|-------|
| Max size | 4 GB |
| Compression | zlib level 6 |
| Sloppiness | `include_file_ctime,include_file_mtime,pch_defines,time_macros` |
ccache is auto-detected by CMake at configure time. Clear with `ccache -C`.
## Silo server
Server configuration is documented in the dedicated [Silo Server Configuration](../silo-server/CONFIGURATION.md) reference, which covers all YAML config sections (database, storage, auth, server, schemas) with full option tables and examples.

View File

@@ -0,0 +1,142 @@
# CommandOrigin — File Menu Origin Commands
> **Source:** `src/Gui/CommandOrigin.cpp`
> **Namespace:** `Gui`
> **Registration:** `Gui::CreateOriginCommands()`
Five FreeCAD commands expose the extended operations defined by
[FileOrigin](./cpp-file-origin.md). Each command follows the same
pattern: look up the owning origin for the active document via
[OriginManager](./cpp-origin-manager.md), check whether that origin
advertises the required capability, and dispatch to the corresponding
`FileOrigin` virtual method.
## Command Table
| Command ID | Menu Text | Shortcut | Capability Gate | Dispatches To | Icon |
|-----------|-----------|----------|-----------------|---------------|------|
| `Origin_Commit` | &Commit | `Ctrl+Shift+C` | `supportsRevisions()` | `commitDocument(doc)` | `silo-commit` |
| `Origin_Pull` | &Pull | `Ctrl+Shift+P` | `supportsRevisions()` | `pullDocument(doc)` | `silo-pull` |
| `Origin_Push` | Pu&sh | `Ctrl+Shift+U` | `supportsRevisions()` | `pushDocument(doc)` | `silo-push` |
| `Origin_Info` | &Info | — | `supportsPartNumbers()` | `showInfo(doc)` | `silo-info` |
| `Origin_BOM` | &Bill of Materials | — | `supportsBOM()` | `showBOM(doc)` | `silo-bom` |
All commands belong to the `"File"` group.
## Activation Pattern
Every command shares the same `isActive()` / `activated()` structure:
```
isActive():
doc = App::GetApplication().getActiveDocument()
if doc is null → return false
origin = OriginManager::instance()->findOwningOrigin(doc)
return origin != null AND origin->supportsXxx()
activated():
doc = App::GetApplication().getActiveDocument()
if doc is null → return
origin = OriginManager::instance()->findOwningOrigin(doc)
if origin AND origin->supportsXxx():
origin->xxxDocument(doc)
```
This means the commands **automatically grey out** in the menu when:
- No document is open.
- The active document is owned by an origin that doesn't support the
capability (e.g., a local document has no Commit/Pull/Push).
For a local-only document, all five commands are inactive. When a Silo
document is active, all five become available because
[SiloOrigin](../reference/cpp-file-origin.md) returns `true` for
`supportsRevisions()`, `supportsBOM()`, and `supportsPartNumbers()`.
## Ownership Resolution
The commands use `findOwningOrigin(doc)` rather than `currentOrigin()`
because the active document may belong to a **different** origin than the
one currently selected in the toolbar. For example, a user might have
Silo selected as the current origin but be viewing a local document in
another tab — the commands correctly detect that the local document has
no revision support.
See [OriginManager § Document-Origin Resolution](./cpp-origin-manager.md#document-origin-resolution)
for the full lookup algorithm.
## Command Type Flags
| Command | `eType` | Meaning |
|---------|---------|---------|
| Commit | `AlterDoc` | Marks document as modified (undo integration) |
| Pull | `AlterDoc` | Marks document as modified |
| Push | `AlterDoc` | Marks document as modified |
| Info | `0` | Read-only, no undo integration |
| BOM | `0` | Read-only, no undo integration |
`AlterDoc` commands participate in FreeCAD's transaction/undo system.
Info and BOM are view-only dialogs that don't modify the document.
## Registration
All five commands are registered in a single function called during
application startup:
```cpp
void Gui::CreateOriginCommands()
{
CommandManager& rcCmdMgr = Application::Instance->commandManager();
rcCmdMgr.addCommand(new OriginCmdCommit());
rcCmdMgr.addCommand(new OriginCmdPull());
rcCmdMgr.addCommand(new OriginCmdPush());
rcCmdMgr.addCommand(new OriginCmdInfo());
rcCmdMgr.addCommand(new OriginCmdBOM());
}
```
This is called from the Gui module initialization alongside other
command registration functions (`CreateDocCommands`,
`CreateMacroCommands`, etc.).
## What the Commands Actually Do
The C++ commands are **thin dispatchers** — they contain no business
logic. The actual work happens in the origin implementation:
- **Local origin** — all five extended methods are no-ops (defaults from
`FileOrigin` base class return `false` or do nothing).
- **Silo origin** (Python) — each method delegates to the corresponding
`Silo_*` FreeCAD command:
| C++ dispatch | Python SiloOrigin method | Delegates to |
|-------------|-------------------------|--------------|
| `commitDocument()` | `SiloOrigin.commitDocument()` | `Silo_Commit` command |
| `pullDocument()` | `SiloOrigin.pullDocument()` | `Silo_Pull` command |
| `pushDocument()` | `SiloOrigin.pushDocument()` | `Silo_Push` command |
| `showInfo()` | `SiloOrigin.showInfo()` | `Silo_Info` command |
| `showBOM()` | `SiloOrigin.showBOM()` | `Silo_BOM` command |
The call chain for a Commit, for example:
```
User clicks File > Commit (or Ctrl+Shift+C)
→ OriginCmdCommit::activated()
→ OriginManager::findOwningOrigin(doc) [C++]
→ SiloOrigin.ownsDocument(doc) [Python via bridge]
→ origin->commitDocument(doc) [C++ virtual]
→ FileOriginPython::commitDocument(doc) [bridge]
→ SiloOrigin.commitDocument(doc) [Python]
→ FreeCADGui.runCommand("Silo_Commit") [Python command]
```
## See Also
- [FileOrigin Interface](./cpp-file-origin.md) — defines the virtual
methods these commands dispatch to
- [OriginManager](./cpp-origin-manager.md) — provides
`findOwningOrigin()` used by every command
- [FileOriginPython](./cpp-file-origin-python.md) — bridges the dispatch
from C++ to Python origins
- [SiloOrigin adapter](./cpp-file-origin.md) — Python implementation
that handles the actual Silo operations

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

View File

@@ -0,0 +1,257 @@
# FileOriginPython — Python-to-C++ Bridge
> **Header:** `src/Gui/FileOriginPython.h`
> **Implementation:** `src/Gui/FileOriginPython.cpp`
> **Python binding:** `src/Gui/ApplicationPy.cpp`
> **Namespace:** `Gui`
`FileOriginPython` is an Adapter that wraps a Python object and presents
it as a C++ [FileOrigin](./cpp-file-origin.md). This is how Python
addons (like SiloOrigin) integrate with the C++
[OriginManager](./cpp-origin-manager.md) without compiling any C++ code.
## Registration from Python
The bridge is exposed to Python through three `FreeCADGui` module
functions:
```python
import FreeCADGui
FreeCADGui.addOrigin(obj) # register a Python origin
FreeCADGui.removeOrigin(obj) # unregister it
FreeCADGui.getOrigin(obj) # look up the wrapper (or None)
```
### `FreeCADGui.addOrigin(obj)`
1. Checks that `obj` is not already registered (duplicate check via
`findOrigin`).
2. Constructs a `FileOriginPython` wrapper around `obj`.
3. Calls `obj.id()` immediately and caches the result — the ID must be
non-empty or registration fails.
4. Passes the wrapper to
`OriginManager::instance()->registerOrigin(wrapper)`.
5. If `registerOrigin` fails (e.g., duplicate ID), the wrapper is removed
from the internal instances list (OriginManager already deleted it).
### `FreeCADGui.removeOrigin(obj)`
1. Finds the wrapper via `findOrigin(obj)`.
2. Removes it from the internal `_instances` vector.
3. Calls `OriginManager::instance()->unregisterOrigin(id)`, which deletes
the wrapper.
### Typical Usage (from SiloOrigin)
```python
# mods/silo/freecad/silo_origin.py
class SiloOrigin:
def id(self): return "silo"
def name(self): return "Kindred Silo"
# ... remaining interface methods ...
_silo_origin = SiloOrigin()
def register_silo_origin():
FreeCADGui.addOrigin(_silo_origin)
def unregister_silo_origin():
FreeCADGui.removeOrigin(_silo_origin)
```
This is called from `InitGui.py` via a deferred `QTimer.singleShot`
(1500 ms after GUI init) to ensure the Gui module is fully loaded.
## Method Dispatch
Every C++ `FileOrigin` virtual method is implemented by delegating to the
corresponding Python method on the wrapped object. Three internal
helpers handle the marshalling:
| Helper | Signature | Used For |
|--------|-----------|----------|
| `callStringMethod` | `(name, default) → std::string` | `id()`, `name()`, `nickname()`, `icon()` |
| `callBoolMethod` | `(name, default) → bool` | All `supportsXxx()`, `tracksExternally()`, etc. |
| `callMethod` | `(name, args) → Py::Object` | Everything else (document ops, state queries) |
### Dispatch Pattern
Each overridden method follows the same structure:
```
1. Acquire GIL ← Base::PyGILStateLocker lock
2. Check if Python object has attr ← _inst.hasAttr("methodName")
3. If missing → return default ← graceful degradation
4. Build Py::Tuple args ← marshal C++ args to Python
5. Call Python method ← func.apply(args)
6. Convert result ← Py::String/Boolean/Long → C++ type
7. Return ← release GIL on scope exit
```
If the Python method is missing, the bridge returns sensible defaults
(empty string, `false`, `nullptr`). If the Python method raises an
exception, it is caught, reported to the FreeCAD console via
`Base::PyException::reportException()`, and the default is returned.
### Document Argument Marshalling
Methods that take `App::Document*` convert it to a Python object using:
```cpp
Py::Object FileOriginPython::getDocPyObject(App::Document* doc) const
{
if (!doc) return Py::None();
return Py::asObject(doc->getPyObject());
}
```
This returns the standard `App.Document` Python wrapper so the Python
origin can call `doc.Objects`, `doc.FileName`, `doc.save()`, etc.
Methods that **return** an `App::Document*` (like `newDocument`,
`openDocument`) check the return value with:
```cpp
if (PyObject_TypeCheck(result.ptr(), &(App::DocumentPy::Type))) {
return static_cast<App::DocumentPy*>(result.ptr())->getDocumentPtr();
}
```
If the Python method returns `None` or a non-Document object, the bridge
returns `nullptr`.
## GIL Management
Every method acquires the GIL (`Base::PyGILStateLocker`) before touching
any Python objects. This is critical because the C++ origin methods can
be called from:
- The main Qt event loop (menu clicks, toolbar actions)
- `OriginManager::findOwningOrigin()` during document operations
- Signal handlers (fastsignals callbacks)
All of these may run on the main thread while the GIL is not held.
## Enum Mapping
Python origins return integers for enum values:
### `OriginType` (from `type()`)
| Python `int` | C++ Enum |
|-------------|----------|
| `0` | `OriginType::Local` |
| `1` | `OriginType::PLM` |
| `2` | `OriginType::Cloud` |
| `3` | `OriginType::Custom` |
### `ConnectionState` (from `connectionState()`)
| Python `int` | C++ Enum |
|-------------|----------|
| `0` | `ConnectionState::Disconnected` |
| `1` | `ConnectionState::Connecting` |
| `2` | `ConnectionState::Connected` |
| `3` | `ConnectionState::Error` |
Out-of-range values fall back to `OriginType::Custom` and
`ConnectionState::Connected` respectively.
## Icon Resolution
The `icon()` override calls `callStringMethod("icon")` to get a string
name, then passes it to `BitmapFactory().iconFromTheme(name)`. The
Python origin returns an icon name (e.g., `"silo"`) rather than a QIcon
object.
## Required Python Interface
Methods the Python object **must** implement (called unconditionally):
| Method | Signature | Purpose |
|--------|-----------|---------|
| `id()` | `→ str` | Unique origin ID (cached on registration) |
| `name()` | `→ str` | Display name |
| `nickname()` | `→ str` | Short toolbar label |
| `icon()` | `→ str` | Icon name for `BitmapFactory` |
| `type()` | `→ int` | `OriginType` enum value |
| `tracksExternally()` | `→ bool` | Whether origin syncs externally |
| `requiresAuthentication()` | `→ bool` | Whether login is needed |
| `ownsDocument(doc)` | `→ bool` | Ownership detection |
| `documentIdentity(doc)` | `→ str` | Immutable tracking key |
| `documentDisplayId(doc)` | `→ str` | Human-readable ID |
Methods the Python object **may** implement (checked with `hasAttr`):
| Method | Signature | Default |
|--------|-----------|---------|
| `supportsRevisions()` | `→ bool` | `False` |
| `supportsBOM()` | `→ bool` | `False` |
| `supportsPartNumbers()` | `→ bool` | `False` |
| `supportsAssemblies()` | `→ bool` | `False` |
| `connectionState()` | `→ int` | `2` (Connected) |
| `connect()` | `→ bool` | `True` |
| `disconnect()` | `→ None` | no-op |
| `syncProperties(doc)` | `→ bool` | `True` |
| `newDocument(name)` | `→ Document` | `None` |
| `openDocument(identity)` | `→ Document` | `None` |
| `openDocumentInteractive()` | `→ Document` | `None` |
| `saveDocument(doc)` | `→ bool` | `False` |
| `saveDocumentAs(doc, id)` | `→ bool` | `False` |
| `saveDocumentAsInteractive(doc)` | `→ bool` | `False` |
| `commitDocument(doc)` | `→ bool` | `False` |
| `pullDocument(doc)` | `→ bool` | `False` |
| `pushDocument(doc)` | `→ bool` | `False` |
| `showInfo(doc)` | `→ None` | no-op |
| `showBOM(doc)` | `→ None` | no-op |
## Error Handling
All Python exceptions are caught and reported to the FreeCAD console.
The bridge **never** propagates a Python exception into C++ — callers
always receive a safe default value.
```
[Python exception in SiloOrigin.ownsDocument]
→ Base::PyException::reportException()
→ FreeCAD Console: "Python error: ..."
→ return false
```
This prevents a buggy Python origin from crashing the application.
## Lifetime and Ownership
```
FreeCADGui.addOrigin(py_obj)
├─ FileOriginPython(py_obj) ← wraps, holds Py::Object ref
│ └─ _inst = py_obj ← prevents Python GC
├─ _instances.push_back(wrapper) ← static vector for findOrigin()
└─ OriginManager::registerOrigin(wrapper)
└─ unique_ptr<FileOrigin> ← OriginManager owns the wrapper
```
- `FileOriginPython` holds a `Py::Object` reference to the Python
instance, preventing garbage collection.
- `OriginManager` owns the wrapper via `std::unique_ptr`.
- The static `_instances` vector provides `findOrigin()` lookup but does
**not** own the pointers.
- On `removeOrigin()`: the wrapper is removed from `_instances`, then
`OriginManager::unregisterOrigin()` deletes the wrapper, which releases
the `Py::Object` reference.
## See Also
- [FileOrigin Interface](./cpp-file-origin.md) — the abstract interface
being adapted
- [OriginManager](./cpp-origin-manager.md) — where the wrapped origin
gets registered
- [CommandOrigin](./cpp-command-origin.md) — commands that dispatch
through this bridge
- [Creating a Custom Origin (Python)](../guide/custom-origin-python.md)
— step-by-step guide using this bridge

View File

@@ -0,0 +1,199 @@
# FileOrigin — Abstract Interface
> **Header:** `src/Gui/FileOrigin.h`
> **Implementation:** `src/Gui/FileOrigin.cpp`
> **Namespace:** `Gui`
`FileOrigin` is the abstract base class that all document storage backends
implement. It defines the contract for creating, opening, saving, and
tracking FreeCAD documents through a pluggable origin system.
## Key Design Principle
Origins do **not** change where files are stored — all documents are always
saved to the local filesystem. Origins change the **workflow** and
**identity model**:
| Origin | Identity | Tracking | Authentication |
|--------|----------|----------|----------------|
| Local | File path | None | None |
| PLM (Silo) | Database UUID | External DB + MinIO | Required |
| Cloud | URL / key | External service | Required |
## Enums
### `OriginType`
```cpp
enum class OriginType {
Local, // Local filesystem storage
PLM, // Product Lifecycle Management system (e.g., Silo)
Cloud, // Generic cloud storage
Custom // User-defined origin type
};
```
### `ConnectionState`
```cpp
enum class ConnectionState {
Disconnected, // Not connected
Connecting, // Connection in progress
Connected, // Successfully connected
Error // Connection error occurred
};
```
Used by the [OriginSelectorWidget](./cpp-origin-selector-widget.md) to
render status overlays on origin icons.
## Pure Virtual Methods (must override)
Every `FileOrigin` subclass must implement these methods.
### Identity
| Method | Return | Purpose |
|--------|--------|---------|
| `id()` | `std::string` | Unique origin ID (`"local"`, `"silo"`, …) |
| `name()` | `std::string` | Display name for menus (`"Local Files"`, `"Kindred Silo"`) |
| `nickname()` | `std::string` | Short label for toolbar (`"Local"`, `"Silo"`) |
| `icon()` | `QIcon` | Icon for UI representation |
| `type()` | `OriginType` | Classification enum |
### Workflow Characteristics
| Method | Return | Purpose |
|--------|--------|---------|
| `tracksExternally()` | `bool` | `true` if origin syncs to a remote system |
| `requiresAuthentication()` | `bool` | `true` if origin needs login |
### Document Identity
| Method | Return | Purpose |
|--------|--------|---------|
| `documentIdentity(doc)` | `std::string` | Immutable tracking key. File path for Local, UUID for PLM. |
| `documentDisplayId(doc)` | `std::string` | Human-readable ID. File path for Local, part number for PLM. |
| `ownsDocument(doc)` | `bool` | Whether this origin owns the document. |
**Ownership detection** is the mechanism that determines which origin
manages a given document. The
[OriginManager](./cpp-origin-manager.md) calls `ownsDocument()` on each
registered origin to resolve ownership.
- `LocalFileOrigin` owns documents that have **no** `SiloItemId` property
on any object.
- `SiloOrigin` (Python) owns documents where any object **has** a
`SiloItemId` property.
### Core Document Operations
| Method | Parameters | Return | Purpose |
|--------|-----------|--------|---------|
| `newDocument` | `name = ""` | `App::Document*` | Create a new document |
| `openDocument` | `identity` | `App::Document*` | Open by identity (non-interactive) |
| `openDocumentInteractive` | — | `App::Document*` | Open via dialog (file picker / search) |
| `saveDocument` | `doc` | `bool` | Save document |
| `saveDocumentAs` | `doc, newIdentity` | `bool` | Save with new identity |
| `saveDocumentAsInteractive` | `doc` | `bool` | Save via dialog |
Returns `nullptr` / `false` on failure or cancellation.
## Virtual Methods with Defaults (optional overrides)
### Capability Queries
These default to `false`. Override to advertise capabilities that the
[CommandOrigin](./cpp-command-origin.md) commands check before enabling
menu items.
| Method | Default | Enables |
|--------|---------|---------|
| `supportsRevisions()` | `false` | Commit / Pull / Push commands |
| `supportsBOM()` | `false` | BOM command |
| `supportsPartNumbers()` | `false` | Info command |
| `supportsAssemblies()` | `false` | (reserved for future use) |
### Connection State
| Method | Default | Purpose |
|--------|---------|---------|
| `connectionState()` | `Connected` | Current connection state |
| `connect()` | `return true` | Attempt to connect / authenticate |
| `disconnect()` | no-op | Disconnect from origin |
### Extended PLM Operations
These default to no-op / `false`. Override in PLM origins.
| Method | Default | Purpose |
|--------|---------|---------|
| `commitDocument(doc)` | `false` | Create a versioned snapshot |
| `pullDocument(doc)` | `false` | Fetch latest from remote |
| `pushDocument(doc)` | `false` | Upload changes to remote |
| `showInfo(doc)` | no-op | Show metadata dialog |
| `showBOM(doc)` | no-op | Show Bill of Materials dialog |
| `syncProperties(doc)` | `true` | Push property changes to backend |
## Signal
```cpp
fastsignals::signal<void(ConnectionState)> signalConnectionStateChanged;
```
Origins must emit this signal when their connection state changes. The
`OriginSelectorWidget` subscribes to it to update the toolbar icon
overlay (green = connected, red X = disconnected, warning = error).
## Construction and Lifetime
- `FileOrigin` is non-copyable (deleted copy constructor and assignment
operator).
- The protected default constructor prevents direct instantiation.
- Instances are owned by `OriginManager` via `std::unique_ptr`.
- Python origins are wrapped by `FileOriginPython` (see
[Python-C++ bridge](./cpp-file-origin-python.md)).
## LocalFileOrigin
`LocalFileOrigin` is the built-in concrete implementation that ships with
Kindred Create. It is always registered by `OriginManager` as the
`"local"` origin and serves as the universal fallback.
### Behavior Summary
| Method | Behavior |
|--------|----------|
| `ownsDocument` | Returns `true` if **no** object has a `SiloItemId` property |
| `documentIdentity` | Returns `doc->FileName` (full path) |
| `newDocument` | Delegates to `App::GetApplication().newDocument()` |
| `openDocument` | Delegates to `App::GetApplication().openDocument()` |
| `openDocumentInteractive` | Shows standard FreeCAD file-open dialog with format filters |
| `saveDocument` | Calls `doc->save()`, returns `false` if no filename set |
| `saveDocumentAs` | Calls `doc->saveAs()` |
| `saveDocumentAsInteractive` | Calls `Gui::Document::saveAs()` (shows save dialog) |
### Ownership Detection Algorithm
```
for each object in document:
if object has property "SiloItemId":
return false ← owned by PLM, not local
return true ← local owns this document
```
This negative-match approach means `LocalFileOrigin` is the **universal
fallback** — it owns any document that no other origin claims.
## See Also
- [OriginManager](./cpp-origin-manager.md) — singleton registry that
manages origin instances
- [FileOriginPython](./cpp-file-origin-python.md) — bridge for
implementing origins in Python
- [CommandOrigin](./cpp-command-origin.md) — File menu commands that
dispatch to origins
- [OriginSelectorWidget](./cpp-origin-selector-widget.md) — toolbar
dropdown for switching origins
- [Creating a Custom Origin (C++)](../guide/custom-origin-cpp.md)
- [Creating a Custom Origin (Python)](../guide/custom-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

View File

@@ -0,0 +1,226 @@
# OriginManager — Singleton Registry
> **Header:** `src/Gui/OriginManager.h`
> **Implementation:** `src/Gui/OriginManager.cpp`
> **Namespace:** `Gui`
`OriginManager` is the central singleton that tracks all registered
[FileOrigin](./cpp-file-origin.md) instances, maintains the user's current
origin selection, and resolves which origin owns a given document.
## Lifecycle
```
Application startup
├─ OriginManager::instance() ← singleton created
│ ├─ ensureLocalOrigin() ← "local" origin always exists
│ └─ loadPreferences() ← restore last-used origin from prefs
├─ Python addons register origins ← e.g. SiloOrigin via FreeCADGui.addOrigin()
├─ ... application runs ...
└─ OriginManager::destruct() ← saves prefs, deletes all origins
```
The `"local"` origin is created in the constructor and **cannot be
unregistered**. It serves as the universal fallback.
## Singleton Access
```cpp
// Get the instance (created on first call)
OriginManager* mgr = OriginManager::instance();
// Destroy at application shutdown
OriginManager::destruct();
```
## Origin Registration
### `registerOrigin(FileOrigin* origin) → bool`
Register a new origin. The manager takes **ownership** via
`std::unique_ptr` — do not delete the pointer after registration.
- Returns `false` and deletes the origin if:
- `origin` is `nullptr`
- `origin->id()` is empty
- An origin with the same ID is already registered
- Emits `signalOriginRegistered(id)` on success.
```cpp
auto* cloud = new MyCloudOrigin();
bool ok = OriginManager::instance()->registerOrigin(cloud);
// cloud is now owned by OriginManager — do not delete it
```
### `unregisterOrigin(const std::string& id) → bool`
Remove and delete an origin by ID.
- Returns `false` if:
- `id` is `"local"` (built-in, cannot be removed)
- No origin with that ID exists
- If the unregistered origin was the current origin, the current origin
automatically reverts to `"local"`.
- Emits `signalOriginUnregistered(id)` on success.
### `getOrigin(const std::string& id) → FileOrigin*`
Look up an origin by ID. Returns `nullptr` if not found.
### `originIds() → std::vector<std::string>`
Returns all registered origin IDs. Order is the `std::map` key order
(alphabetical).
## Current Origin Selection
The "current origin" determines which backend is used for **new
documents** (File > New) and appears selected in the
[OriginSelectorWidget](./cpp-origin-selector-widget.md) toolbar dropdown.
### `currentOrigin() → FileOrigin*`
Returns the currently selected origin. Never returns `nullptr` — falls
back to `"local"` if the stored ID is somehow invalid.
### `currentOriginId() → std::string`
Returns the current origin's ID string.
### `setCurrentOrigin(const std::string& id) → bool`
Switch the current origin. Returns `false` if the ID is not registered.
- Persists the selection to FreeCAD preferences at
`User parameter:BaseApp/Preferences/General/Origin/CurrentOriginId`.
- Emits `signalCurrentOriginChanged(id)` when the selection actually
changes (no signal if setting to the already-current origin).
### `originForNewDocument() → FileOrigin*`
Convenience method — returns `currentOrigin()`. Called by the File > New
command to determine which origin should handle new document creation.
## Document-Origin Resolution
When FreeCAD needs to know which origin owns a document (e.g., for
File > Save), it uses the resolution chain below.
### `originForDocument(App::Document* doc) → FileOrigin*`
Primary lookup method. Resolution order:
1. **Explicit association** — check the `_documentOrigins` cache for a
prior `setDocumentOrigin()` call.
2. **Ownership detection** — call `findOwningOrigin(doc)` to scan all
origins.
3. **Cache the result** — store in `_documentOrigins` for future lookups.
Returns `nullptr` only if no origin claims the document (should not
happen in practice since `LocalFileOrigin` is the universal fallback).
### `findOwningOrigin(App::Document* doc) → FileOrigin*`
Scans all registered origins by calling `ownsDocument(doc)` on each.
**Algorithm:**
```
1. For each origin where id ≠ "local":
if origin->ownsDocument(doc):
return origin ← PLM/Cloud origin claims it
2. if localOrigin->ownsDocument(doc):
return localOrigin ← fallback to local
3. return nullptr ← no owner (shouldn't happen)
```
Non-local origins are checked **first** because they have specific
ownership criteria (e.g., presence of `SiloItemId` property). The local
origin uses negative-match logic (owns anything not claimed by others),
so it must be checked last to avoid false positives.
### `setDocumentOrigin(doc, origin)`
Explicitly associate a document with an origin. Used when creating new
documents to mark them with the origin that created them.
- Passing `origin = nullptr` clears the association.
- Emits `signalDocumentOriginChanged(doc, originId)`.
### `clearDocumentOrigin(doc)`
Remove a document from the association cache. Called when a document is
closed to prevent stale pointers.
## Signals
All signals use the [fastsignals](https://github.com/nicktrandafil/fastsignals)
library (not Qt signals).
| Signal | Parameters | When |
|--------|-----------|------|
| `signalOriginRegistered` | `const std::string& id` | After a new origin is registered |
| `signalOriginUnregistered` | `const std::string& id` | After an origin is removed |
| `signalCurrentOriginChanged` | `const std::string& id` | After the user switches the current origin |
| `signalDocumentOriginChanged` | `App::Document*, const std::string& id` | After a document-origin association changes |
### Subscribers
- **[OriginSelectorWidget](./cpp-origin-selector-widget.md)** subscribes
to `signalOriginRegistered`, `signalOriginUnregistered`, and
`signalCurrentOriginChanged` to rebuild the dropdown menu and update
the toolbar button.
- **[CommandOrigin](./cpp-command-origin.md)** commands query the manager
on each `isActive()` call to check document ownership and origin
capabilities.
## Preference Persistence
| Path | Key | Type | Default |
|------|-----|------|---------|
| `User parameter:BaseApp/Preferences/General/Origin` | `CurrentOriginId` | ASCII string | `"local"` |
Loaded in the constructor, saved on destruction and on each
`setCurrentOrigin()` call. If the saved origin ID is not registered at
load time (e.g., the Silo addon hasn't loaded yet), the manager falls
back to `"local"` and will re-check when origins are registered later.
## Memory Model
```
OriginManager (singleton)
├─ _origins: std::map<string, std::unique_ptr<FileOrigin>>
│ ├─ "local" → LocalFileOrigin (always present)
│ ├─ "silo" → FileOriginPython (wraps Python SiloOrigin)
│ └─ ... → other registered origins
├─ _currentOriginId: std::string
└─ _documentOrigins: std::map<App::Document*, string>
├─ doc1 → "silo"
├─ doc2 → "local"
└─ ... (cache, cleared on document close)
```
- Origins are owned exclusively by the manager via `unique_ptr`.
- The document-origin map uses raw `App::Document*` pointers. Callers
**must** call `clearDocumentOrigin()` when a document is destroyed to
prevent dangling pointers.
## See Also
- [FileOrigin Interface](./cpp-file-origin.md) — the abstract interface
that registered origins implement
- [FileOriginPython](./cpp-file-origin-python.md) — bridge for
Python-implemented origins
- [CommandOrigin](./cpp-command-origin.md) — commands that dispatch to
origins via this manager
- [OriginSelectorWidget](./cpp-origin-selector-widget.md) — toolbar UI
driven by this manager's signals

View File

@@ -0,0 +1,224 @@
# OriginSelectorWidget
`OriginSelectorWidget` is a toolbar dropdown that lets users switch between registered file origins (Local Files, Silo, etc.). It appears as the first item in the File toolbar across all workbenches.
- **Header:** `src/Gui/OriginSelectorWidget.h`
- **Source:** `src/Gui/OriginSelectorWidget.cpp`
- **Base class:** `QToolButton`
- **Command ID:** `Std_Origin`
## Widget appearance
The button shows the current origin's nickname and icon. Clicking opens a dropdown menu listing all registered origins with a checkmark on the active one.
```
┌────────────────┐
│ Local ▼ │ ← nickname + icon, InstantPopup mode
└────────────────┘
Dropdown:
✓ Local ← checked = current origin
Kindred Silo ← disconnected origins show status overlay
──────────────────
Manage Origins... ← opens OriginManagerDialog
```
### Size constraints
| Property | Value |
|----------|-------|
| Popup mode | `QToolButton::InstantPopup` |
| Button style | `Qt::ToolButtonTextBesideIcon` |
| Minimum width | 80 px |
| Maximum width | 160 px |
| Size policy | `Preferred, Fixed` |
## Lifecycle
### Construction
The constructor calls four setup methods in order:
1. **`setupUi()`** — Configures the button, creates `m_menu` (QMenu) and `m_originActions` (exclusive QActionGroup). Connects the action group's `triggered` signal to `onOriginActionTriggered`.
2. **`connectSignals()`** — Subscribes to three OriginManager fastsignals via `scoped_connection` objects.
3. **`rebuildMenu()`** — Populates the menu from the current OriginManager state.
4. **`updateDisplay()`** — Sets the button text, icon, and tooltip to match the current origin.
### Destruction
The destructor calls `disconnectSignals()`, which explicitly disconnects the three `fastsignals::scoped_connection` members. The scoped connections would auto-disconnect on destruction regardless, but explicit disconnection prevents any signal delivery during teardown.
## Signal connections
The widget subscribes to three `OriginManager` fastsignals:
| OriginManager signal | Widget handler | Response |
|----------------------|----------------|----------|
| `signalOriginRegistered` | `onOriginRegistered(id)` | Rebuild menu |
| `signalOriginUnregistered` | `onOriginUnregistered(id)` | Rebuild menu |
| `signalCurrentOriginChanged` | `onCurrentOriginChanged(id)` | Update display + menu checkmarks |
Connections are stored as `fastsignals::scoped_connection` members for RAII lifetime management:
```cpp
fastsignals::scoped_connection m_connRegistered;
fastsignals::scoped_connection m_connUnregistered;
fastsignals::scoped_connection m_connChanged;
```
## Menu population
`rebuildMenu()` rebuilds the entire dropdown from scratch each time an origin is registered or unregistered:
1. Clear `m_menu` and remove all actions from `m_originActions`.
2. Iterate `OriginManager::originIds()` (returns `std::vector<std::string>` in registration order).
3. For each origin ID, create a `QAction` with:
- Icon from `iconForOrigin(origin)` (with connection-state overlay)
- Text from `origin->nickname()`
- Tooltip from `origin->name()`
- `setCheckable(true)`, checked if this is the current origin
- Data set to the origin ID string
4. Add a separator.
5. Add a "Manage Origins..." action with the `preferences-system` theme icon, connected to `onManageOriginsClicked`.
Origins appear in the menu in the order returned by `OriginManager::originIds()`. The local origin is always first (registered at startup), followed by Python-registered origins in registration order.
## Origin selection
When the user clicks an origin in the dropdown, `onOriginActionTriggered(QAction*)` runs:
1. Extract the origin ID from `action->data()`.
2. Look up the `FileOrigin` from OriginManager.
3. **Authentication gate:** If `origin->requiresAuthentication()` is true and the connection state is `Disconnected` or `Error`:
- Call `origin->connect()`.
- If connection fails, revert the menu checkmark to the previous origin and return without changing.
4. On success, call `OriginManager::setCurrentOrigin(originId)`.
This means selecting a disconnected PLM origin triggers an automatic reconnection attempt. The user sees no change if the connection fails.
## Icon overlays
`iconForOrigin(FileOrigin*)` generates a display icon with optional connection-state indicators:
| Connection state | Overlay | Position |
|------------------|---------|----------|
| `Connected` | None | — |
| `Connecting` | None (TODO: animated) | — |
| `Disconnected` | `dagViewFail` (8x8 px, red) | Bottom-right |
| `Error` | `Warning` (8x8 px, yellow) | Bottom-right |
Overlays are only applied to origins where `requiresAuthentication()` returns `true`. Local origins never get overlays. The merge uses `BitmapFactoryInst::mergePixmap` with `BottomRight` placement.
## Display updates
`updateDisplay()` sets the button face to reflect the current origin:
- **Text:** `origin->nickname()` (e.g. "Local", "Silo")
- **Icon:** Result of `iconForOrigin(origin)` (with possible overlay)
- **Tooltip:** `origin->name()` (e.g. "Local Files", "Kindred Silo")
- **No origin:** Text becomes "No Origin", icon and tooltip are cleared
This method runs on construction, on `signalCurrentOriginChanged`, and after the Manage Origins dialog closes.
## Command wrapper
The widget is exposed to the command/toolbar system through two classes.
### StdCmdOrigin
Defined in `src/Gui/CommandStd.cpp` using `DEF_STD_CMD_AC(StdCmdOrigin)`:
```cpp
StdCmdOrigin::StdCmdOrigin()
: Command("Std_Origin")
{
sGroup = "File";
sMenuText = QT_TR_NOOP("&Origin");
sToolTipText = QT_TR_NOOP("Select file origin (Local Files, Silo, etc.)");
sWhatsThis = "Std_Origin";
sStatusTip = sToolTipText;
sPixmap = "folder";
eType = 0;
}
```
| Property | Value |
|----------|-------|
| Command ID | `Std_Origin` |
| Menu group | `File` |
| Icon | `folder` |
| `isActive()` | Always `true` |
| `activated()` | No-op (widget handles interaction) |
`createAction()` returns an `OriginSelectorAction` instance.
### OriginSelectorAction
Defined in `src/Gui/Action.h` / `Action.cpp`. Bridges the command system and the widget:
```cpp
void OriginSelectorAction::addTo(QWidget* widget)
{
if (widget->inherits("QToolBar")) {
auto* selector = new OriginSelectorWidget(widget);
static_cast<QToolBar*>(widget)->addWidget(selector);
} else {
widget->addAction(action());
}
}
```
When added to a `QToolBar`, it instantiates an `OriginSelectorWidget`. When added to a menu or other container, it falls back to adding a plain `QAction`.
## Toolbar integration
`StdWorkbench::setupToolBars()` in `src/Gui/Workbench.cpp` places the widget:
```cpp
auto file = new ToolBarItem(root);
file->setCommand("File");
*file << "Std_Origin" // ← origin selector (first)
<< "Std_New"
<< "Std_Open"
<< "Std_Save";
```
The widget appears as the **first item** in the File toolbar. Because `StdWorkbench` is the base workbench, this placement is inherited by all workbenches (Part Design, Assembly, Silo, etc.).
A separate "Origin Tools" toolbar follows with PLM-specific commands:
```cpp
auto originTools = new ToolBarItem(root);
originTools->setCommand("Origin Tools");
*originTools << "Origin_Commit" << "Origin_Pull" << "Origin_Push"
<< "Separator"
<< "Origin_Info" << "Origin_BOM";
```
These commands auto-disable based on the current origin's capability flags (`supportsRevisions`, `supportsBOM`, etc.).
## Member variables
```cpp
QMenu* m_menu; // dropdown menu
QActionGroup* m_originActions; // exclusive checkmark group
QAction* m_manageAction; // "Manage Origins..." action
fastsignals::scoped_connection m_connRegistered; // signalOriginRegistered
fastsignals::scoped_connection m_connUnregistered; // signalOriginUnregistered
fastsignals::scoped_connection m_connChanged; // signalCurrentOriginChanged
```
## Behavioural notes
- **Origin scope is global, not per-document.** Switching origin affects all subsequent New/Open/Save operations. Existing open documents retain their origin association via `OriginManager::_documentOrigins`.
- **Menu rebuilds are full rebuilds.** On any registration/unregistration event, the entire menu is cleared and rebuilt. This is simple and correct — origin counts are small (typically 2-3).
- **No document-switch tracking.** The widget does not respond to active document changes. The current origin is a user preference, not derived from the active document.
- **Thread safety.** All operations assume the Qt main thread. Signal emissions from OriginManager are synchronous.
## See also
- [FileOrigin Interface](./cpp-file-origin.md) — abstract base class
- [LocalFileOrigin](./cpp-local-file-origin.md) — built-in default origin
- [OriginManager](./cpp-origin-manager.md) — singleton registry and signal source
- [CommandOrigin](./cpp-command-origin.md) — PLM commands in the Origin Tools toolbar

View File

@@ -0,0 +1,53 @@
# Glossary
## Terms
**BOM** — Bill of Materials. A structured list of components in an assembly with part numbers, quantities, and reference designators.
**Catppuccin Mocha** — A dark color palette used for Kindred Create's theme. Part of the [Catppuccin](https://github.com/catppuccin/catppuccin) color scheme family.
**Datum** — Reference geometry (plane, axis, or point) used as a construction aid for modeling. Not a physical shape — used to position features relative to abstract references.
**FCStd** — FreeCAD's standard document format. A ZIP archive containing XML model trees, BREP geometry, thumbnails, and embedded data.
**FileOrigin** — Abstract C++ interface in `src/Gui/` that defines a pluggable file backend. Implementations: `LocalFileOrigin` (filesystem) and `SiloOrigin` (database).
**FreeCAD** — Open-source parametric 3D CAD platform. Kindred Create is based on FreeCAD v1.0.0. Website: <https://www.freecad.org>
**Kindred Create** — The full application: a FreeCAD fork plus Kindred's addon workbenches, theme, and tooling.
**Manipulator** — FreeCAD mechanism for injecting commands from one workbench into another's menus and toolbars. Used by ztools to add commands to PartDesign.
**MinIO** — S3-compatible object storage server. Used by Silo to store binary `.FCStd` files.
**OndselSolver** — Lagrangian constraint solver for the Assembly workbench. Vendored as a submodule from a Kindred fork.
**Origin** — In Kindred Create context: the pluggable file backend system. In FreeCAD context: the coordinate system origin (X/Y/Z axes and planes) of a Part or Body.
**pixi** — Conda-based dependency manager and task runner. Used for all build operations. Website: <https://pixi.sh>
**Preference pack** — FreeCAD mechanism for bundling theme settings, preferences, and stylesheets into an installable package.
**QSS** — Qt Style Sheet. A CSS-like language for styling Qt widgets. Kindred Create's theme is defined in `KindredCreate.qss`.
**rattler-build** — Cross-platform package build tool from the conda ecosystem. Used to create AppImage, DMG, and NSIS installer bundles.
**Silo** — Kindred's parts database system. Consists of a Go server, FreeCAD workbench, and shared Python client library.
**SSE** — Server-Sent Events. HTTP-based protocol for real-time server-to-client notifications. Used by Silo for the activity feed.
**Workbench** — FreeCAD's plugin/module system. Each workbench provides a set of tools, menus, and toolbars for a specific task domain.
**ztools** — Kindred's unified workbench combining Part Design, Assembly, and Sketcher tools with custom datum creation and assembly patterns.
## Repository URLs
| Repository | URL |
|------------|-----|
| Kindred Create | <https://git.kindred-systems.com/kindred/create> |
| ztools | <https://git.kindred-systems.com/forbes/ztools> |
| silo-mod | <https://git.kindred-systems.com/kindred/silo-mod> |
| OndselSolver | <https://git.kindred-systems.com/kindred/solver> |
| GSL | <https://github.com/microsoft/GSL> |
| AddonManager | <https://github.com/FreeCAD/AddonManager> |
| googletest | <https://github.com/google/googletest> |