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:
110
docs/src/reference/configuration.md
Normal file
110
docs/src/reference/configuration.md
Normal 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.
|
||||
142
docs/src/reference/cpp-command-origin.md
Normal file
142
docs/src/reference/cpp-command-origin.md
Normal 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
|
||||
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
|
||||
257
docs/src/reference/cpp-file-origin-python.md
Normal file
257
docs/src/reference/cpp-file-origin-python.md
Normal 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
|
||||
199
docs/src/reference/cpp-file-origin.md
Normal file
199
docs/src/reference/cpp-file-origin.md
Normal 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)
|
||||
218
docs/src/reference/cpp-local-file-origin.md
Normal file
218
docs/src/reference/cpp-local-file-origin.md
Normal 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
|
||||
226
docs/src/reference/cpp-origin-manager.md
Normal file
226
docs/src/reference/cpp-origin-manager.md
Normal 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
|
||||
224
docs/src/reference/cpp-origin-selector-widget.md
Normal file
224
docs/src/reference/cpp-origin-selector-widget.md
Normal 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
|
||||
53
docs/src/reference/glossary.md
Normal file
53
docs/src/reference/glossary.md
Normal 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> |
|
||||
Reference in New Issue
Block a user