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:
239
src/Gui/OriginManager.cpp
Normal file
239
src/Gui/OriginManager.cpp
Normal file
@@ -0,0 +1,239 @@
|
||||
/***************************************************************************
|
||||
* 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 <Base/Console.h>
|
||||
|
||||
#include "OriginManager.h"
|
||||
#include "FileOrigin.h"
|
||||
|
||||
|
||||
namespace Gui {
|
||||
|
||||
// Preferences path for origin settings
|
||||
static const char* PREF_PATH = "User parameter:BaseApp/Preferences/General/Origin";
|
||||
static const char* PREF_CURRENT_ORIGIN = "CurrentOriginId";
|
||||
|
||||
// Built-in origin ID that cannot be unregistered
|
||||
static const char* LOCAL_ORIGIN_ID = "local";
|
||||
|
||||
|
||||
OriginManager* OriginManager::_instance = nullptr;
|
||||
|
||||
OriginManager* OriginManager::instance()
|
||||
{
|
||||
if (!_instance) {
|
||||
_instance = new OriginManager();
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
|
||||
void OriginManager::destruct()
|
||||
{
|
||||
delete _instance;
|
||||
_instance = nullptr;
|
||||
}
|
||||
|
||||
OriginManager::OriginManager()
|
||||
{
|
||||
ensureLocalOrigin();
|
||||
loadPreferences();
|
||||
}
|
||||
|
||||
OriginManager::~OriginManager()
|
||||
{
|
||||
savePreferences();
|
||||
_origins.clear();
|
||||
}
|
||||
|
||||
void OriginManager::ensureLocalOrigin()
|
||||
{
|
||||
// Create the built-in local filesystem origin
|
||||
auto localOrigin = std::make_unique<LocalFileOrigin>();
|
||||
_origins[LOCAL_ORIGIN_ID] = std::move(localOrigin);
|
||||
_currentOriginId = LOCAL_ORIGIN_ID;
|
||||
}
|
||||
|
||||
void OriginManager::loadPreferences()
|
||||
{
|
||||
try {
|
||||
auto hGrp = App::GetApplication().GetParameterGroupByPath(PREF_PATH);
|
||||
std::string savedOriginId = hGrp->GetASCII(PREF_CURRENT_ORIGIN, LOCAL_ORIGIN_ID);
|
||||
|
||||
// Only use saved origin if it's registered
|
||||
if (_origins.find(savedOriginId) != _origins.end()) {
|
||||
_currentOriginId = savedOriginId;
|
||||
}
|
||||
}
|
||||
catch (...) {
|
||||
// Ignore preference loading errors
|
||||
_currentOriginId = LOCAL_ORIGIN_ID;
|
||||
}
|
||||
}
|
||||
|
||||
void OriginManager::savePreferences()
|
||||
{
|
||||
try {
|
||||
auto hGrp = App::GetApplication().GetParameterGroupByPath(PREF_PATH);
|
||||
hGrp->SetASCII(PREF_CURRENT_ORIGIN, _currentOriginId.c_str());
|
||||
}
|
||||
catch (...) {
|
||||
// Ignore preference saving errors
|
||||
}
|
||||
}
|
||||
|
||||
bool OriginManager::registerOrigin(FileOrigin* origin)
|
||||
{
|
||||
if (!origin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string originId = origin->id();
|
||||
if (originId.empty()) {
|
||||
Base::Console().Warning("OriginManager: Cannot register origin with empty ID\n");
|
||||
delete origin;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if ID already in use
|
||||
if (_origins.find(originId) != _origins.end()) {
|
||||
Base::Console().Warning("OriginManager: Origin '%s' already registered\n", originId.c_str());
|
||||
delete origin;
|
||||
return false;
|
||||
}
|
||||
|
||||
_origins[originId] = std::unique_ptr<FileOrigin>(origin);
|
||||
Base::Console().Log("OriginManager: Registered origin '%s'\n", originId.c_str());
|
||||
|
||||
signalOriginRegistered(originId);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OriginManager::unregisterOrigin(const std::string& id)
|
||||
{
|
||||
// Cannot unregister the built-in local origin
|
||||
if (id == LOCAL_ORIGIN_ID) {
|
||||
Base::Console().Warning("OriginManager: Cannot unregister built-in local origin\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto it = _origins.find(id);
|
||||
if (it == _origins.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If unregistering the current origin, switch to local
|
||||
if (_currentOriginId == id) {
|
||||
_currentOriginId = LOCAL_ORIGIN_ID;
|
||||
signalCurrentOriginChanged(_currentOriginId);
|
||||
}
|
||||
|
||||
_origins.erase(it);
|
||||
Base::Console().Log("OriginManager: Unregistered origin '%s'\n", id.c_str());
|
||||
|
||||
signalOriginUnregistered(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> OriginManager::originIds() const
|
||||
{
|
||||
std::vector<std::string> ids;
|
||||
ids.reserve(_origins.size());
|
||||
for (const auto& pair : _origins) {
|
||||
ids.push_back(pair.first);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
FileOrigin* OriginManager::getOrigin(const std::string& id) const
|
||||
{
|
||||
auto it = _origins.find(id);
|
||||
if (it != _origins.end()) {
|
||||
return it->second.get();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FileOrigin* OriginManager::currentOrigin() const
|
||||
{
|
||||
auto it = _origins.find(_currentOriginId);
|
||||
if (it != _origins.end()) {
|
||||
return it->second.get();
|
||||
}
|
||||
// Fallback to local (should never happen)
|
||||
return _origins.at(LOCAL_ORIGIN_ID).get();
|
||||
}
|
||||
|
||||
std::string OriginManager::currentOriginId() const
|
||||
{
|
||||
return _currentOriginId;
|
||||
}
|
||||
|
||||
bool OriginManager::setCurrentOrigin(const std::string& id)
|
||||
{
|
||||
if (_origins.find(id) == _origins.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_currentOriginId != id) {
|
||||
_currentOriginId = id;
|
||||
savePreferences();
|
||||
signalCurrentOriginChanged(_currentOriginId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
FileOrigin* OriginManager::findOwningOrigin(App::Document* doc) const
|
||||
{
|
||||
if (!doc) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check each origin to see if it owns this document
|
||||
// Start with non-local origins since they have specific ownership criteria
|
||||
for (const auto& pair : _origins) {
|
||||
if (pair.first == LOCAL_ORIGIN_ID) {
|
||||
continue; // Check local last as fallback
|
||||
}
|
||||
if (pair.second->ownsDocument(doc)) {
|
||||
return pair.second.get();
|
||||
}
|
||||
}
|
||||
|
||||
// If no PLM origin claims it, check local
|
||||
auto localIt = _origins.find(LOCAL_ORIGIN_ID);
|
||||
if (localIt != _origins.end() && localIt->second->ownsDocument(doc)) {
|
||||
return localIt->second.get();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FileOrigin* OriginManager::originForNewDocument() const
|
||||
{
|
||||
return currentOrigin();
|
||||
}
|
||||
|
||||
} // namespace Gui
|
||||
Reference in New Issue
Block a user