feat(gui): add origin abstraction layer for unified file operations

Implements Issue #9: Origin abstraction layer

This commit introduces a foundational abstraction for document origins,
enabling FreeCAD to work with different storage backends (local filesystem,
Silo PLM, future cloud services) through a unified interface.

## Core Components

### FileOrigin Abstract Base Class (FileOrigin.h/cpp)
- Defines interface for document origin handlers
- Identity methods: id(), name(), nickname(), icon(), type()
- Workflow characteristics: tracksExternally(), requiresAuthentication()
- Capability queries: supportsRevisions(), supportsBOM(), supportsPartNumbers()
- Connection state management with fastsignals notifications
- Document identity: documentIdentity() returns UUID, documentDisplayId() for display
- Property sync: syncProperties() for bidirectional database sync
- Core operations: newDocument(), openDocument(), saveDocument(), saveDocumentAs()
- Extended PLM operations: commitDocument(), pullDocument(), pushDocument(), etc.

### LocalFileOrigin Implementation
- Default origin for local filesystem documents
- ownsDocument(): Returns true if document has NO SiloItemId property
- Wraps existing FreeCAD file operations (App::GetApplication())

### OriginManager Singleton (OriginManager.h/cpp)
- Follows WorkbenchManager pattern (instance()/destruct())
- Manages registered FileOrigin instances
- Tracks current origin selection with persistence
- Provides document-to-origin resolution via findOwningOrigin()
- Emits signals: signalOriginRegistered, signalOriginUnregistered,
  signalCurrentOriginChanged
- Preferences stored at: User parameter:BaseApp/Preferences/General/Origin

### Python Bindings (FileOriginPython.h/cpp)
- Adapts Python objects to FileOrigin C++ interface
- Enables Silo addon to implement origins in Python
- Thread-safe with Base::PyGILStateLocker
- Static addOrigin()/removeOrigin() for registration

### Python API (ApplicationPy.cpp)
- FreeCADGui.addOrigin(obj) - Register Python origin
- FreeCADGui.removeOrigin(obj) - Unregister Python origin
- FreeCADGui.getOrigin(id) - Get origin info as dict
- FreeCADGui.listOrigins() - List all registered origin IDs
- FreeCADGui.activeOrigin() - Get current origin info
- FreeCADGui.setActiveOrigin(id) - Set active origin

## Design Decisions

1. **UUID Tracking**: Documents tracked by SiloItemId (immutable UUID),
   SiloPartNumber used for human-readable display only

2. **Ownership by Properties**: Origin ownership determined by document
   properties (SiloItemId), not file path location

3. **Local Storage Always**: All documents saved locally; origins change
   workflow and identity model, not storage location

4. **Property Syncing**: syncProperties() enables bidirectional sync of
   document metadata with database (Description, SourcingType, etc.)

## Files Added
- src/Gui/FileOrigin.h
- src/Gui/FileOrigin.cpp
- src/Gui/FileOriginPython.h
- src/Gui/FileOriginPython.cpp
- src/Gui/OriginManager.h
- src/Gui/OriginManager.cpp

## Files Modified
- src/Gui/CMakeLists.txt - Added new source files
- src/Gui/Application.cpp - Initialize/destruct OriginManager
- src/Gui/ApplicationPy.h - Added Python method declarations
- src/Gui/ApplicationPy.cpp - Added Python method implementations

Refs: #9
This commit is contained in:
2026-02-05 13:17:23 -06:00
parent 1c309a0ca8
commit 7535a48ec4
10 changed files with 1700 additions and 0 deletions

128
src/Gui/FileOrigin.cpp Normal file
View File

@@ -0,0 +1,128 @@
/***************************************************************************
* Copyright (c) 2025 Kindred Systems *
* *
* This file is part of the FreeCAD CAx development system. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Library General Public *
* License as published by the Free Software Foundation; either *
* version 2 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this library; see the file COPYING.LIB. If not, *
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
#include "PreCompiled.h"
#include <App/Application.h>
#include <App/Document.h>
#include <App/DocumentObject.h>
#include <App/PropertyStandard.h>
#include "FileOrigin.h"
#include "BitmapFactory.h"
#include "Document.h"
#include "Application.h"
namespace Gui {
// Property name used by PLM origins (Silo) to mark tracked documents
static const char* SILO_ITEM_ID_PROP = "SiloItemId";
//===========================================================================
// LocalFileOrigin
//===========================================================================
LocalFileOrigin::LocalFileOrigin()
{
}
QIcon LocalFileOrigin::icon() const
{
return BitmapFactory().iconFromTheme("document-new");
}
std::string LocalFileOrigin::documentIdentity(App::Document* doc) const
{
if (!doc || !ownsDocument(doc)) {
return {};
}
return doc->FileName.getValue();
}
std::string LocalFileOrigin::documentDisplayId(App::Document* doc) const
{
// For local files, identity and display ID are the same (file path)
return documentIdentity(doc);
}
bool LocalFileOrigin::ownsDocument(App::Document* doc) const
{
if (!doc) {
return false;
}
// Local origin owns documents that do NOT have PLM tracking properties.
// Check all objects for SiloItemId property - if any have it,
// this document is owned by a PLM origin, not local.
for (auto* obj : doc->getObjects()) {
if (obj->getPropertyByName(SILO_ITEM_ID_PROP)) {
return false;
}
}
return true;
}
App::Document* LocalFileOrigin::newDocument(const std::string& name)
{
std::string docName = name.empty() ? "Unnamed" : name;
return App::GetApplication().newDocument(docName.c_str(), docName.c_str());
}
App::Document* LocalFileOrigin::openDocument(const std::string& identity)
{
if (identity.empty()) {
return nullptr;
}
return App::GetApplication().openDocument(identity.c_str());
}
bool LocalFileOrigin::saveDocument(App::Document* doc)
{
if (!doc) {
return false;
}
// If document has never been saved, we need a path
const char* fileName = doc->FileName.getValue();
if (!fileName || fileName[0] == '\0') {
// No file name set - would need UI interaction for Save As
// This will be handled by the command layer
return false;
}
return doc->save();
}
bool LocalFileOrigin::saveDocumentAs(App::Document* doc, const std::string& newIdentity)
{
if (!doc || newIdentity.empty()) {
return false;
}
return doc->saveAs(newIdentity.c_str());
}
} // namespace Gui