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:
578
src/Gui/FileOriginPython.cpp
Normal file
578
src/Gui/FileOriginPython.cpp
Normal file
@@ -0,0 +1,578 @@
|
||||
/***************************************************************************
|
||||
* 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 <Base/Console.h>
|
||||
#include <Base/Interpreter.h>
|
||||
#include <Base/PyObjectBase.h>
|
||||
|
||||
#include "FileOriginPython.h"
|
||||
#include "OriginManager.h"
|
||||
#include "BitmapFactory.h"
|
||||
|
||||
|
||||
namespace Gui {
|
||||
|
||||
std::vector<FileOriginPython*> FileOriginPython::_instances;
|
||||
|
||||
void FileOriginPython::addOrigin(const Py::Object& obj)
|
||||
{
|
||||
// Check if already registered
|
||||
if (findOrigin(obj)) {
|
||||
Base::Console().Warning("FileOriginPython: Origin already registered\n");
|
||||
return;
|
||||
}
|
||||
|
||||
auto* origin = new FileOriginPython(obj);
|
||||
|
||||
// Cache the ID immediately for registration
|
||||
origin->_cachedId = origin->callStringMethod("id");
|
||||
if (origin->_cachedId.empty()) {
|
||||
Base::Console().Error("FileOriginPython: Origin must have non-empty id()\n");
|
||||
delete origin;
|
||||
return;
|
||||
}
|
||||
|
||||
_instances.push_back(origin);
|
||||
|
||||
// Register with OriginManager
|
||||
if (!OriginManager::instance()->registerOrigin(origin)) {
|
||||
// Registration failed - remove from our instances list
|
||||
// (registerOrigin already deleted the origin)
|
||||
_instances.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
void FileOriginPython::removeOrigin(const Py::Object& obj)
|
||||
{
|
||||
FileOriginPython* origin = findOrigin(obj);
|
||||
if (!origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string originId = origin->_cachedId;
|
||||
|
||||
// Remove from instances list
|
||||
auto it = std::find(_instances.begin(), _instances.end(), origin);
|
||||
if (it != _instances.end()) {
|
||||
_instances.erase(it);
|
||||
}
|
||||
|
||||
// Unregister from OriginManager (this will delete the origin)
|
||||
OriginManager::instance()->unregisterOrigin(originId);
|
||||
}
|
||||
|
||||
FileOriginPython* FileOriginPython::findOrigin(const Py::Object& obj)
|
||||
{
|
||||
for (auto* instance : _instances) {
|
||||
if (instance->_inst == obj) {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FileOriginPython::FileOriginPython(const Py::Object& obj)
|
||||
: _inst(obj)
|
||||
{
|
||||
}
|
||||
|
||||
FileOriginPython::~FileOriginPython() = default;
|
||||
|
||||
Py::Object FileOriginPython::callMethod(const char* method) const
|
||||
{
|
||||
return callMethod(method, Py::Tuple());
|
||||
}
|
||||
|
||||
Py::Object FileOriginPython::callMethod(const char* method, const Py::Tuple& args) const
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr(method)) {
|
||||
Py::Callable func(_inst.getAttr(method));
|
||||
return func.apply(args);
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
return Py::None();
|
||||
}
|
||||
|
||||
bool FileOriginPython::callBoolMethod(const char* method, bool defaultValue) const
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr(method)) {
|
||||
Py::Callable func(_inst.getAttr(method));
|
||||
Py::Object result = func.apply(Py::Tuple());
|
||||
if (result.isBoolean()) {
|
||||
return Py::Boolean(result);
|
||||
}
|
||||
if (result.isNumeric()) {
|
||||
return Py::Long(result) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
std::string FileOriginPython::callStringMethod(const char* method, const std::string& defaultValue) const
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr(method)) {
|
||||
Py::Callable func(_inst.getAttr(method));
|
||||
Py::Object result = func.apply(Py::Tuple());
|
||||
if (result.isString()) {
|
||||
return Py::String(result).as_std_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
Py::Object FileOriginPython::getDocPyObject(App::Document* doc) const
|
||||
{
|
||||
if (!doc) {
|
||||
return Py::None();
|
||||
}
|
||||
return Py::asObject(doc->getPyObject());
|
||||
}
|
||||
|
||||
// Identity methods
|
||||
std::string FileOriginPython::id() const
|
||||
{
|
||||
return _cachedId.empty() ? callStringMethod("id") : _cachedId;
|
||||
}
|
||||
|
||||
std::string FileOriginPython::name() const
|
||||
{
|
||||
return callStringMethod("name", id());
|
||||
}
|
||||
|
||||
std::string FileOriginPython::nickname() const
|
||||
{
|
||||
return callStringMethod("nickname", name());
|
||||
}
|
||||
|
||||
QIcon FileOriginPython::icon() const
|
||||
{
|
||||
std::string iconName = callStringMethod("icon", "document-new");
|
||||
return BitmapFactory().iconFromTheme(iconName.c_str());
|
||||
}
|
||||
|
||||
OriginType FileOriginPython::type() const
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr("type")) {
|
||||
Py::Callable func(_inst.getAttr("type"));
|
||||
Py::Object result = func.apply(Py::Tuple());
|
||||
if (result.isNumeric()) {
|
||||
int t = static_cast<int>(Py::Long(result));
|
||||
if (t >= 0 && t <= static_cast<int>(OriginType::Custom)) {
|
||||
return static_cast<OriginType>(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
return OriginType::Custom;
|
||||
}
|
||||
|
||||
// Workflow characteristics
|
||||
bool FileOriginPython::tracksExternally() const
|
||||
{
|
||||
return callBoolMethod("tracksExternally", false);
|
||||
}
|
||||
|
||||
bool FileOriginPython::requiresAuthentication() const
|
||||
{
|
||||
return callBoolMethod("requiresAuthentication", false);
|
||||
}
|
||||
|
||||
// Capability queries
|
||||
bool FileOriginPython::supportsRevisions() const
|
||||
{
|
||||
return callBoolMethod("supportsRevisions", false);
|
||||
}
|
||||
|
||||
bool FileOriginPython::supportsBOM() const
|
||||
{
|
||||
return callBoolMethod("supportsBOM", false);
|
||||
}
|
||||
|
||||
bool FileOriginPython::supportsPartNumbers() const
|
||||
{
|
||||
return callBoolMethod("supportsPartNumbers", false);
|
||||
}
|
||||
|
||||
bool FileOriginPython::supportsAssemblies() const
|
||||
{
|
||||
return callBoolMethod("supportsAssemblies", false);
|
||||
}
|
||||
|
||||
// Connection state
|
||||
ConnectionState FileOriginPython::connectionState() const
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr("connectionState")) {
|
||||
Py::Callable func(_inst.getAttr("connectionState"));
|
||||
Py::Object result = func.apply(Py::Tuple());
|
||||
if (result.isNumeric()) {
|
||||
int s = static_cast<int>(Py::Long(result));
|
||||
if (s >= 0 && s <= static_cast<int>(ConnectionState::Error)) {
|
||||
return static_cast<ConnectionState>(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
return ConnectionState::Connected;
|
||||
}
|
||||
|
||||
bool FileOriginPython::connect()
|
||||
{
|
||||
return callBoolMethod("connect", true);
|
||||
}
|
||||
|
||||
void FileOriginPython::disconnect()
|
||||
{
|
||||
callMethod("disconnect");
|
||||
}
|
||||
|
||||
// Document identity
|
||||
std::string FileOriginPython::documentIdentity(App::Document* doc) const
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr("documentIdentity")) {
|
||||
Py::Callable func(_inst.getAttr("documentIdentity"));
|
||||
Py::Tuple args(1);
|
||||
args.setItem(0, getDocPyObject(doc));
|
||||
Py::Object result = func.apply(args);
|
||||
if (result.isString()) {
|
||||
return Py::String(result).as_std_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string FileOriginPython::documentDisplayId(App::Document* doc) const
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr("documentDisplayId")) {
|
||||
Py::Callable func(_inst.getAttr("documentDisplayId"));
|
||||
Py::Tuple args(1);
|
||||
args.setItem(0, getDocPyObject(doc));
|
||||
Py::Object result = func.apply(args);
|
||||
if (result.isString()) {
|
||||
return Py::String(result).as_std_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
return documentIdentity(doc);
|
||||
}
|
||||
|
||||
bool FileOriginPython::ownsDocument(App::Document* doc) const
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr("ownsDocument")) {
|
||||
Py::Callable func(_inst.getAttr("ownsDocument"));
|
||||
Py::Tuple args(1);
|
||||
args.setItem(0, getDocPyObject(doc));
|
||||
Py::Object result = func.apply(args);
|
||||
if (result.isBoolean()) {
|
||||
return Py::Boolean(result);
|
||||
}
|
||||
if (result.isNumeric()) {
|
||||
return Py::Long(result) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FileOriginPython::syncProperties(App::Document* doc)
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr("syncProperties")) {
|
||||
Py::Callable func(_inst.getAttr("syncProperties"));
|
||||
Py::Tuple args(1);
|
||||
args.setItem(0, getDocPyObject(doc));
|
||||
Py::Object result = func.apply(args);
|
||||
if (result.isBoolean()) {
|
||||
return Py::Boolean(result);
|
||||
}
|
||||
if (result.isNumeric()) {
|
||||
return Py::Long(result) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Core document operations
|
||||
App::Document* FileOriginPython::newDocument(const std::string& name)
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr("newDocument")) {
|
||||
Py::Callable func(_inst.getAttr("newDocument"));
|
||||
Py::Tuple args(1);
|
||||
args.setItem(0, Py::String(name));
|
||||
Py::Object result = func.apply(args);
|
||||
if (!result.isNone()) {
|
||||
// Extract App::Document* from Python object
|
||||
if (PyObject_TypeCheck(result.ptr(), &(App::DocumentPy::Type))) {
|
||||
return static_cast<App::DocumentPy*>(result.ptr())->getDocumentPtr();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
App::Document* FileOriginPython::openDocument(const std::string& identity)
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr("openDocument")) {
|
||||
Py::Callable func(_inst.getAttr("openDocument"));
|
||||
Py::Tuple args(1);
|
||||
args.setItem(0, Py::String(identity));
|
||||
Py::Object result = func.apply(args);
|
||||
if (!result.isNone()) {
|
||||
if (PyObject_TypeCheck(result.ptr(), &(App::DocumentPy::Type))) {
|
||||
return static_cast<App::DocumentPy*>(result.ptr())->getDocumentPtr();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool FileOriginPython::saveDocument(App::Document* doc)
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr("saveDocument")) {
|
||||
Py::Callable func(_inst.getAttr("saveDocument"));
|
||||
Py::Tuple args(1);
|
||||
args.setItem(0, getDocPyObject(doc));
|
||||
Py::Object result = func.apply(args);
|
||||
if (result.isBoolean()) {
|
||||
return Py::Boolean(result);
|
||||
}
|
||||
if (result.isNumeric()) {
|
||||
return Py::Long(result) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FileOriginPython::saveDocumentAs(App::Document* doc, const std::string& newIdentity)
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr("saveDocumentAs")) {
|
||||
Py::Callable func(_inst.getAttr("saveDocumentAs"));
|
||||
Py::Tuple args(2);
|
||||
args.setItem(0, getDocPyObject(doc));
|
||||
args.setItem(1, Py::String(newIdentity));
|
||||
Py::Object result = func.apply(args);
|
||||
if (result.isBoolean()) {
|
||||
return Py::Boolean(result);
|
||||
}
|
||||
if (result.isNumeric()) {
|
||||
return Py::Long(result) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extended operations
|
||||
bool FileOriginPython::commitDocument(App::Document* doc)
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr("commitDocument")) {
|
||||
Py::Callable func(_inst.getAttr("commitDocument"));
|
||||
Py::Tuple args(1);
|
||||
args.setItem(0, getDocPyObject(doc));
|
||||
Py::Object result = func.apply(args);
|
||||
if (result.isBoolean()) {
|
||||
return Py::Boolean(result);
|
||||
}
|
||||
if (result.isNumeric()) {
|
||||
return Py::Long(result) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FileOriginPython::pullDocument(App::Document* doc)
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr("pullDocument")) {
|
||||
Py::Callable func(_inst.getAttr("pullDocument"));
|
||||
Py::Tuple args(1);
|
||||
args.setItem(0, getDocPyObject(doc));
|
||||
Py::Object result = func.apply(args);
|
||||
if (result.isBoolean()) {
|
||||
return Py::Boolean(result);
|
||||
}
|
||||
if (result.isNumeric()) {
|
||||
return Py::Long(result) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FileOriginPython::pushDocument(App::Document* doc)
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr("pushDocument")) {
|
||||
Py::Callable func(_inst.getAttr("pushDocument"));
|
||||
Py::Tuple args(1);
|
||||
args.setItem(0, getDocPyObject(doc));
|
||||
Py::Object result = func.apply(args);
|
||||
if (result.isBoolean()) {
|
||||
return Py::Boolean(result);
|
||||
}
|
||||
if (result.isNumeric()) {
|
||||
return Py::Long(result) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void FileOriginPython::showInfo(App::Document* doc)
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr("showInfo")) {
|
||||
Py::Callable func(_inst.getAttr("showInfo"));
|
||||
Py::Tuple args(1);
|
||||
args.setItem(0, getDocPyObject(doc));
|
||||
func.apply(args);
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
}
|
||||
|
||||
void FileOriginPython::showBOM(App::Document* doc)
|
||||
{
|
||||
Base::PyGILStateLocker lock;
|
||||
try {
|
||||
if (_inst.hasAttr("showBOM")) {
|
||||
Py::Callable func(_inst.getAttr("showBOM"));
|
||||
Py::Tuple args(1);
|
||||
args.setItem(0, getDocPyObject(doc));
|
||||
func.apply(args);
|
||||
}
|
||||
}
|
||||
catch (Py::Exception&) {
|
||||
Base::PyException e;
|
||||
e.ReportException();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Gui
|
||||
Reference in New Issue
Block a user