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:
@@ -137,6 +137,7 @@
|
||||
#include "Workbench.h"
|
||||
#include "WorkbenchManager.h"
|
||||
#include "WorkbenchManipulator.h"
|
||||
#include "OriginManager.h"
|
||||
#include "WidgetFactory.h"
|
||||
#include "3Dconnexion/navlib/NavlibInterface.h"
|
||||
#include "Inventor/SoFCPlacementIndicatorKit.h"
|
||||
@@ -691,6 +692,9 @@ Application::Application(bool GUIenabled)
|
||||
if (GUIenabled) {
|
||||
createStandardOperations();
|
||||
MacroCommand::load();
|
||||
|
||||
// Initialize the origin manager (creates local origin by default)
|
||||
OriginManager::instance();
|
||||
}
|
||||
}
|
||||
// clang-format on
|
||||
@@ -702,6 +706,7 @@ Application::~Application()
|
||||
delete pNavlibInterface;
|
||||
#endif
|
||||
WorkbenchManager::destruct();
|
||||
OriginManager::destruct();
|
||||
WorkbenchManipulator::removeAll();
|
||||
SelectionSingleton::destruct();
|
||||
Translator::destruct();
|
||||
|
||||
@@ -66,6 +66,8 @@
|
||||
#include "Workbench.h"
|
||||
#include "WorkbenchManager.h"
|
||||
#include "WorkbenchManipulatorPython.h"
|
||||
#include "FileOriginPython.h"
|
||||
#include "OriginManager.h"
|
||||
#include "Inventor/MarkerBitmaps.h"
|
||||
#include "Language/Translator.h"
|
||||
|
||||
@@ -550,6 +552,50 @@ PyMethodDef ApplicationPy::Methods[] = {
|
||||
METH_VARARGS,
|
||||
"resumeWaitCursor() -> None\n\n"
|
||||
"Resumes the application's wait cursor and event filter."},
|
||||
{"addOrigin",
|
||||
(PyCFunction)ApplicationPy::sAddOrigin,
|
||||
METH_VARARGS,
|
||||
"addOrigin(obj) -> None\n"
|
||||
"\n"
|
||||
"Register a Python origin handler for document operations.\n"
|
||||
"\n"
|
||||
"obj : object\n Origin object implementing the FileOrigin interface."},
|
||||
{"removeOrigin",
|
||||
(PyCFunction)ApplicationPy::sRemoveOrigin,
|
||||
METH_VARARGS,
|
||||
"removeOrigin(obj) -> None\n"
|
||||
"\n"
|
||||
"Unregister a Python origin handler.\n"
|
||||
"\n"
|
||||
"obj : object\n Origin object to unregister."},
|
||||
{"getOrigin",
|
||||
(PyCFunction)ApplicationPy::sGetOrigin,
|
||||
METH_VARARGS,
|
||||
"getOrigin(id) -> object or None\n"
|
||||
"\n"
|
||||
"Get an origin by its ID.\n"
|
||||
"\n"
|
||||
"id : str\n The origin ID."},
|
||||
{"listOrigins",
|
||||
(PyCFunction)ApplicationPy::sListOrigins,
|
||||
METH_VARARGS,
|
||||
"listOrigins() -> list\n"
|
||||
"\n"
|
||||
"Get a list of all registered origin IDs."},
|
||||
{"activeOrigin",
|
||||
(PyCFunction)ApplicationPy::sActiveOrigin,
|
||||
METH_VARARGS,
|
||||
"activeOrigin() -> object or None\n"
|
||||
"\n"
|
||||
"Get the currently active origin."},
|
||||
{"setActiveOrigin",
|
||||
(PyCFunction)ApplicationPy::sSetActiveOrigin,
|
||||
METH_VARARGS,
|
||||
"setActiveOrigin(id) -> bool\n"
|
||||
"\n"
|
||||
"Set the active origin by ID.\n"
|
||||
"\n"
|
||||
"id : str\n The origin ID to activate."},
|
||||
{nullptr, nullptr, 0, nullptr} /* Sentinel */
|
||||
};
|
||||
|
||||
@@ -1966,3 +2012,130 @@ PyObject* ApplicationPy::sResumeWaitCursor(PyObject* /*self*/, PyObject* args)
|
||||
WaitCursor::resume();
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Origin management methods
|
||||
|
||||
PyObject* ApplicationPy::sAddOrigin(PyObject* /*self*/, PyObject* args)
|
||||
{
|
||||
PyObject* o = nullptr;
|
||||
if (!PyArg_ParseTuple(args, "O", &o)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PY_TRY
|
||||
{
|
||||
FileOriginPython::addOrigin(Py::Object(o));
|
||||
Py_Return;
|
||||
}
|
||||
PY_CATCH;
|
||||
}
|
||||
|
||||
PyObject* ApplicationPy::sRemoveOrigin(PyObject* /*self*/, PyObject* args)
|
||||
{
|
||||
PyObject* o = nullptr;
|
||||
if (!PyArg_ParseTuple(args, "O", &o)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PY_TRY
|
||||
{
|
||||
FileOriginPython::removeOrigin(Py::Object(o));
|
||||
Py_Return;
|
||||
}
|
||||
PY_CATCH;
|
||||
}
|
||||
|
||||
PyObject* ApplicationPy::sGetOrigin(PyObject* /*self*/, PyObject* args)
|
||||
{
|
||||
const char* originId = nullptr;
|
||||
if (!PyArg_ParseTuple(args, "s", &originId)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PY_TRY
|
||||
{
|
||||
FileOrigin* origin = OriginManager::instance()->getOrigin(originId);
|
||||
if (!origin) {
|
||||
Py_Return; // Return None if not found
|
||||
}
|
||||
|
||||
// Return origin info as a dictionary
|
||||
Py::Dict result;
|
||||
result.setItem("id", Py::String(origin->id()));
|
||||
result.setItem("name", Py::String(origin->name()));
|
||||
result.setItem("nickname", Py::String(origin->nickname()));
|
||||
result.setItem("type", Py::Long(static_cast<int>(origin->type())));
|
||||
result.setItem("tracksExternally", Py::Boolean(origin->tracksExternally()));
|
||||
result.setItem("requiresAuthentication", Py::Boolean(origin->requiresAuthentication()));
|
||||
result.setItem("supportsRevisions", Py::Boolean(origin->supportsRevisions()));
|
||||
result.setItem("supportsBOM", Py::Boolean(origin->supportsBOM()));
|
||||
result.setItem("supportsPartNumbers", Py::Boolean(origin->supportsPartNumbers()));
|
||||
result.setItem("connectionState", Py::Long(static_cast<int>(origin->connectionState())));
|
||||
|
||||
return Py::new_reference_to(result);
|
||||
}
|
||||
PY_CATCH;
|
||||
}
|
||||
|
||||
PyObject* ApplicationPy::sListOrigins(PyObject* /*self*/, PyObject* args)
|
||||
{
|
||||
if (!PyArg_ParseTuple(args, "")) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PY_TRY
|
||||
{
|
||||
Py::List result;
|
||||
for (const auto& id : OriginManager::instance()->originIds()) {
|
||||
result.append(Py::String(id));
|
||||
}
|
||||
return Py::new_reference_to(result);
|
||||
}
|
||||
PY_CATCH;
|
||||
}
|
||||
|
||||
PyObject* ApplicationPy::sActiveOrigin(PyObject* /*self*/, PyObject* args)
|
||||
{
|
||||
if (!PyArg_ParseTuple(args, "")) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PY_TRY
|
||||
{
|
||||
FileOrigin* origin = OriginManager::instance()->currentOrigin();
|
||||
if (!origin) {
|
||||
Py_Return;
|
||||
}
|
||||
|
||||
// Return origin info as a dictionary
|
||||
Py::Dict result;
|
||||
result.setItem("id", Py::String(origin->id()));
|
||||
result.setItem("name", Py::String(origin->name()));
|
||||
result.setItem("nickname", Py::String(origin->nickname()));
|
||||
result.setItem("type", Py::Long(static_cast<int>(origin->type())));
|
||||
result.setItem("tracksExternally", Py::Boolean(origin->tracksExternally()));
|
||||
result.setItem("requiresAuthentication", Py::Boolean(origin->requiresAuthentication()));
|
||||
result.setItem("supportsRevisions", Py::Boolean(origin->supportsRevisions()));
|
||||
result.setItem("supportsBOM", Py::Boolean(origin->supportsBOM()));
|
||||
result.setItem("supportsPartNumbers", Py::Boolean(origin->supportsPartNumbers()));
|
||||
result.setItem("connectionState", Py::Long(static_cast<int>(origin->connectionState())));
|
||||
|
||||
return Py::new_reference_to(result);
|
||||
}
|
||||
PY_CATCH;
|
||||
}
|
||||
|
||||
PyObject* ApplicationPy::sSetActiveOrigin(PyObject* /*self*/, PyObject* args)
|
||||
{
|
||||
const char* originId = nullptr;
|
||||
if (!PyArg_ParseTuple(args, "s", &originId)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PY_TRY
|
||||
{
|
||||
bool success = OriginManager::instance()->setCurrentOrigin(originId);
|
||||
return Py::new_reference_to(Py::Boolean(success));
|
||||
}
|
||||
PY_CATCH;
|
||||
}
|
||||
|
||||
@@ -108,6 +108,14 @@ public:
|
||||
static PyObject* sAddWbManipulator (PyObject *self,PyObject *args);
|
||||
static PyObject* sRemoveWbManipulator (PyObject *self,PyObject *args);
|
||||
|
||||
// Origin management
|
||||
static PyObject* sAddOrigin (PyObject *self,PyObject *args);
|
||||
static PyObject* sRemoveOrigin (PyObject *self,PyObject *args);
|
||||
static PyObject* sGetOrigin (PyObject *self,PyObject *args);
|
||||
static PyObject* sListOrigins (PyObject *self,PyObject *args);
|
||||
static PyObject* sActiveOrigin (PyObject *self,PyObject *args);
|
||||
static PyObject* sSetActiveOrigin (PyObject *self,PyObject *args);
|
||||
|
||||
static PyObject* sListUserEditModes (PyObject *self,PyObject *args);
|
||||
static PyObject* sGetUserEditMode (PyObject *self,PyObject *args);
|
||||
static PyObject* sSetUserEditMode (PyObject *self,PyObject *args);
|
||||
|
||||
@@ -1317,6 +1317,7 @@ SET(Workbench_CPP_SRCS
|
||||
OverlayManager.cpp
|
||||
OverlayWidgets.cpp
|
||||
MenuManager.cpp
|
||||
OriginManager.cpp
|
||||
PythonWorkbenchPyImp.cpp
|
||||
ToolBarAreaWidget.cpp
|
||||
ToolBarManager.cpp
|
||||
@@ -1334,6 +1335,7 @@ SET(Workbench_SRCS
|
||||
OverlayManager.h
|
||||
OverlayWidgets.h
|
||||
MenuManager.h
|
||||
OriginManager.h
|
||||
ToolBarAreaWidget.h
|
||||
ToolBarManager.h
|
||||
ToolBoxManager.h
|
||||
@@ -1374,6 +1376,8 @@ SET(FreeCADGui_CPP_SRCS
|
||||
DocumentObserver.cpp
|
||||
DocumentObserverPython.cpp
|
||||
EditableDatumLabel.cpp
|
||||
FileOrigin.cpp
|
||||
FileOriginPython.cpp
|
||||
ExpressionBinding.cpp
|
||||
ExpressionBindingPy.cpp
|
||||
GraphicsViewZoom.cpp
|
||||
@@ -1414,6 +1418,8 @@ SET(FreeCADGui_SRCS
|
||||
DocumentObserver.h
|
||||
DocumentObserverPython.h
|
||||
EditableDatumLabel.h
|
||||
FileOrigin.h
|
||||
FileOriginPython.h
|
||||
ExpressionBinding.h
|
||||
ExpressionBindingPy.h
|
||||
ExpressionCompleter.h
|
||||
|
||||
128
src/Gui/FileOrigin.cpp
Normal file
128
src/Gui/FileOrigin.cpp
Normal 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
|
||||
259
src/Gui/FileOrigin.h
Normal file
259
src/Gui/FileOrigin.h
Normal file
@@ -0,0 +1,259 @@
|
||||
/***************************************************************************
|
||||
* 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 *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef GUI_FILEORIGIN_H
|
||||
#define GUI_FILEORIGIN_H
|
||||
|
||||
#include <string>
|
||||
#include <QIcon>
|
||||
#include <FCGlobal.h>
|
||||
#include <fastsignals/signal.h>
|
||||
|
||||
namespace App {
|
||||
class Document;
|
||||
}
|
||||
|
||||
namespace Gui {
|
||||
|
||||
/**
|
||||
* @brief Classification of origin types
|
||||
*/
|
||||
enum class OriginType {
|
||||
Local, ///< Local filesystem storage
|
||||
PLM, ///< Product Lifecycle Management system (e.g., Silo)
|
||||
Cloud, ///< Generic cloud storage
|
||||
Custom ///< User-defined origin type
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Connection state for origins that require network access
|
||||
*/
|
||||
enum class ConnectionState {
|
||||
Disconnected, ///< Not connected
|
||||
Connecting, ///< Connection in progress
|
||||
Connected, ///< Successfully connected
|
||||
Error ///< Connection error occurred
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Abstract base class for document origin handlers
|
||||
*
|
||||
* FileOrigin provides an interface for different storage backends
|
||||
* that can handle FreeCAD documents. Each origin defines workflows
|
||||
* for creating, opening, saving, and managing documents.
|
||||
*
|
||||
* Key insight: Origins don't change where files are stored - all documents
|
||||
* are always saved locally. Origins change the workflow and identity model:
|
||||
* - Local: Document identity = file path, no external tracking
|
||||
* - PLM: Document identity = database UUID, syncs with external system
|
||||
*/
|
||||
class GuiExport FileOrigin
|
||||
{
|
||||
public:
|
||||
virtual ~FileOrigin() = default;
|
||||
|
||||
///@name Identity Methods
|
||||
//@{
|
||||
/** Unique identifier for this origin instance */
|
||||
virtual std::string id() const = 0;
|
||||
/** Display name for UI */
|
||||
virtual std::string name() const = 0;
|
||||
/** Short nickname for compact UI elements (e.g., toolbar) */
|
||||
virtual std::string nickname() const = 0;
|
||||
/** Icon for UI representation */
|
||||
virtual QIcon icon() const = 0;
|
||||
/** Origin type classification */
|
||||
virtual OriginType type() const = 0;
|
||||
//@}
|
||||
|
||||
///@name Workflow Characteristics
|
||||
//@{
|
||||
/** Whether this origin tracks documents externally (e.g., in a database) */
|
||||
virtual bool tracksExternally() const = 0;
|
||||
/** Whether this origin requires user authentication */
|
||||
virtual bool requiresAuthentication() const = 0;
|
||||
//@}
|
||||
|
||||
///@name Capability Queries
|
||||
//@{
|
||||
/** Whether this origin supports revision history */
|
||||
virtual bool supportsRevisions() const { return false; }
|
||||
/** Whether this origin supports Bill of Materials */
|
||||
virtual bool supportsBOM() const { return false; }
|
||||
/** Whether this origin supports part numbers */
|
||||
virtual bool supportsPartNumbers() const { return false; }
|
||||
/** Whether this origin supports assemblies natively */
|
||||
virtual bool supportsAssemblies() const { return false; }
|
||||
//@}
|
||||
|
||||
///@name Connection State
|
||||
//@{
|
||||
/** Get current connection state */
|
||||
virtual ConnectionState connectionState() const { return ConnectionState::Connected; }
|
||||
/** Attempt to connect/authenticate */
|
||||
virtual bool connect() { return true; }
|
||||
/** Disconnect from origin */
|
||||
virtual void disconnect() {}
|
||||
/** Signal emitted when connection state changes */
|
||||
fastsignals::signal<void(ConnectionState)> signalConnectionStateChanged;
|
||||
//@}
|
||||
|
||||
///@name Document Identity
|
||||
//@{
|
||||
/**
|
||||
* Get document identity string (path for local, UUID for PLM)
|
||||
* This is the immutable tracking key for the document.
|
||||
* @param doc The App document to get identity for
|
||||
* @return Identity string or empty if not owned by this origin
|
||||
*/
|
||||
virtual std::string documentIdentity(App::Document* doc) const = 0;
|
||||
|
||||
/**
|
||||
* Get human-readable document identity (path for local, part number for PLM)
|
||||
* This is for display purposes in the UI.
|
||||
* @param doc The App document
|
||||
* @return Display identity string or empty if not owned
|
||||
*/
|
||||
virtual std::string documentDisplayId(App::Document* doc) const = 0;
|
||||
|
||||
/**
|
||||
* Check if this origin owns the given document.
|
||||
* Ownership is determined by document properties, not file path.
|
||||
* @param doc The document to check
|
||||
* @return true if this origin owns the document
|
||||
*/
|
||||
virtual bool ownsDocument(App::Document* doc) const = 0;
|
||||
//@}
|
||||
|
||||
///@name Property Synchronization
|
||||
//@{
|
||||
/**
|
||||
* Sync document properties to the origin backend.
|
||||
* For local origin this is a no-op. For PLM origins this pushes
|
||||
* property changes to the database.
|
||||
* @param doc The document to sync
|
||||
* @return true if sync succeeded
|
||||
*/
|
||||
virtual bool syncProperties(App::Document* doc) { (void)doc; return true; }
|
||||
//@}
|
||||
|
||||
///@name Core Document Operations
|
||||
//@{
|
||||
/**
|
||||
* Create a new document managed by this origin.
|
||||
* Local: Creates empty document
|
||||
* PLM: Shows part creation form
|
||||
* @param name Optional document name
|
||||
* @return The created document or nullptr on failure
|
||||
*/
|
||||
virtual App::Document* newDocument(const std::string& name = "") = 0;
|
||||
|
||||
/**
|
||||
* Open a document by identity.
|
||||
* Local: Opens file at path
|
||||
* PLM: Opens document by UUID (downloads if needed)
|
||||
* @param identity Document identity (path or UUID)
|
||||
* @return The opened document or nullptr on failure
|
||||
*/
|
||||
virtual App::Document* openDocument(const std::string& identity) = 0;
|
||||
|
||||
/**
|
||||
* Save the document.
|
||||
* Local: Saves to disk
|
||||
* PLM: Saves to disk and syncs with external system
|
||||
* @param doc The document to save
|
||||
* @return true if save succeeded
|
||||
*/
|
||||
virtual bool saveDocument(App::Document* doc) = 0;
|
||||
|
||||
/**
|
||||
* Save document with new identity.
|
||||
* Local: File picker for new path
|
||||
* PLM: Migration or copy workflow
|
||||
* @param doc The document to save
|
||||
* @param newIdentity New identity (path or part number)
|
||||
* @return true if save succeeded
|
||||
*/
|
||||
virtual bool saveDocumentAs(App::Document* doc, const std::string& newIdentity) = 0;
|
||||
//@}
|
||||
|
||||
///@name Extended Operations (PLM-specific, default to no-op)
|
||||
//@{
|
||||
/** Commit document changes to external system */
|
||||
virtual bool commitDocument(App::Document* doc) { (void)doc; return false; }
|
||||
/** Pull latest changes from external system */
|
||||
virtual bool pullDocument(App::Document* doc) { (void)doc; return false; }
|
||||
/** Push local changes to external system */
|
||||
virtual bool pushDocument(App::Document* doc) { (void)doc; return false; }
|
||||
/** Show document info dialog */
|
||||
virtual void showInfo(App::Document* doc) { (void)doc; }
|
||||
/** Show Bill of Materials dialog */
|
||||
virtual void showBOM(App::Document* doc) { (void)doc; }
|
||||
//@}
|
||||
|
||||
protected:
|
||||
FileOrigin() = default;
|
||||
|
||||
// Non-copyable
|
||||
FileOrigin(const FileOrigin&) = delete;
|
||||
FileOrigin& operator=(const FileOrigin&) = delete;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @brief Local filesystem origin - default origin for local files
|
||||
*
|
||||
* This is the default origin that handles documents stored on the
|
||||
* local filesystem without any external tracking or synchronization.
|
||||
*/
|
||||
class GuiExport LocalFileOrigin : public FileOrigin
|
||||
{
|
||||
public:
|
||||
LocalFileOrigin();
|
||||
~LocalFileOrigin() override = default;
|
||||
|
||||
// Identity
|
||||
std::string id() const override { return "local"; }
|
||||
std::string name() const override { return "Local Files"; }
|
||||
std::string nickname() const override { return "Local"; }
|
||||
QIcon icon() const override;
|
||||
OriginType type() const override { return OriginType::Local; }
|
||||
|
||||
// Characteristics
|
||||
bool tracksExternally() const override { return false; }
|
||||
bool requiresAuthentication() const override { return false; }
|
||||
|
||||
// Document identity
|
||||
std::string documentIdentity(App::Document* doc) const override;
|
||||
std::string documentDisplayId(App::Document* doc) const override;
|
||||
bool ownsDocument(App::Document* doc) const override;
|
||||
|
||||
// Document operations
|
||||
App::Document* newDocument(const std::string& name = "") override;
|
||||
App::Document* openDocument(const std::string& identity) override;
|
||||
bool saveDocument(App::Document* doc) override;
|
||||
bool saveDocumentAs(App::Document* doc, const std::string& newIdentity) override;
|
||||
};
|
||||
|
||||
} // namespace Gui
|
||||
|
||||
#endif // GUI_FILEORIGIN_H
|
||||
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
|
||||
146
src/Gui/FileOriginPython.h
Normal file
146
src/Gui/FileOriginPython.h
Normal file
@@ -0,0 +1,146 @@
|
||||
/***************************************************************************
|
||||
* 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 *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef GUI_FILEORIGINPYTHON_H
|
||||
#define GUI_FILEORIGINPYTHON_H
|
||||
|
||||
#include <vector>
|
||||
#include <FCGlobal.h>
|
||||
#include <CXX/Objects.hxx>
|
||||
#include "FileOrigin.h"
|
||||
|
||||
namespace Gui {
|
||||
|
||||
/**
|
||||
* @brief Wrapper that adapts a Python object to the FileOrigin interface
|
||||
*
|
||||
* This allows Python addons (like Silo) to implement origins in Python
|
||||
* while integrating with the C++ OriginManager.
|
||||
*
|
||||
* The Python object should implement the following methods:
|
||||
* - id() -> str
|
||||
* - name() -> str
|
||||
* - nickname() -> str
|
||||
* - icon() -> str (icon name for BitmapFactory)
|
||||
* - type() -> int (OriginType enum value)
|
||||
* - tracksExternally() -> bool
|
||||
* - requiresAuthentication() -> bool
|
||||
* - ownsDocument(doc) -> bool
|
||||
* - documentIdentity(doc) -> str
|
||||
* - documentDisplayId(doc) -> str
|
||||
*
|
||||
* Optional methods:
|
||||
* - supportsRevisions() -> bool
|
||||
* - supportsBOM() -> bool
|
||||
* - supportsPartNumbers() -> bool
|
||||
* - supportsAssemblies() -> bool
|
||||
* - connectionState() -> int
|
||||
* - connect() -> bool
|
||||
* - disconnect() -> None
|
||||
* - syncProperties(doc) -> bool
|
||||
* - newDocument(name) -> Document
|
||||
* - openDocument(identity) -> Document
|
||||
* - saveDocument(doc) -> bool
|
||||
* - saveDocumentAs(doc, newIdentity) -> bool
|
||||
* - commitDocument(doc) -> bool
|
||||
* - pullDocument(doc) -> bool
|
||||
* - pushDocument(doc) -> bool
|
||||
* - showInfo(doc) -> None
|
||||
* - showBOM(doc) -> None
|
||||
*/
|
||||
class GuiExport FileOriginPython : public FileOrigin
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Register a Python object as an origin.
|
||||
* The Python object should implement the FileOrigin interface methods.
|
||||
* @param obj The Python object implementing the origin interface
|
||||
*/
|
||||
static void addOrigin(const Py::Object& obj);
|
||||
|
||||
/**
|
||||
* Unregister a Python origin by its Python object.
|
||||
* @param obj The Python object to unregister
|
||||
*/
|
||||
static void removeOrigin(const Py::Object& obj);
|
||||
|
||||
/**
|
||||
* Find a registered Python origin by its Python object.
|
||||
* @param obj The Python object to find
|
||||
* @return The FileOriginPython wrapper or nullptr
|
||||
*/
|
||||
static FileOriginPython* findOrigin(const Py::Object& obj);
|
||||
|
||||
// FileOrigin interface - delegates to Python
|
||||
std::string id() const override;
|
||||
std::string name() const override;
|
||||
std::string nickname() const override;
|
||||
QIcon icon() const override;
|
||||
OriginType type() const override;
|
||||
|
||||
bool tracksExternally() const override;
|
||||
bool requiresAuthentication() const override;
|
||||
bool supportsRevisions() const override;
|
||||
bool supportsBOM() const override;
|
||||
bool supportsPartNumbers() const override;
|
||||
bool supportsAssemblies() const override;
|
||||
|
||||
ConnectionState connectionState() const override;
|
||||
bool connect() override;
|
||||
void disconnect() override;
|
||||
|
||||
std::string documentIdentity(App::Document* doc) const override;
|
||||
std::string documentDisplayId(App::Document* doc) const override;
|
||||
bool ownsDocument(App::Document* doc) const override;
|
||||
bool syncProperties(App::Document* doc) override;
|
||||
|
||||
App::Document* newDocument(const std::string& name = "") override;
|
||||
App::Document* openDocument(const std::string& identity) override;
|
||||
bool saveDocument(App::Document* doc) override;
|
||||
bool saveDocumentAs(App::Document* doc, const std::string& newIdentity) override;
|
||||
|
||||
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;
|
||||
|
||||
private:
|
||||
explicit FileOriginPython(const Py::Object& obj);
|
||||
~FileOriginPython() override;
|
||||
|
||||
// Helper to call Python methods safely
|
||||
Py::Object callMethod(const char* method) const;
|
||||
Py::Object callMethod(const char* method, const Py::Tuple& args) const;
|
||||
bool callBoolMethod(const char* method, bool defaultValue = false) const;
|
||||
std::string callStringMethod(const char* method, const std::string& defaultValue = "") const;
|
||||
Py::Object getDocPyObject(App::Document* doc) const;
|
||||
|
||||
Py::Object _inst;
|
||||
std::string _cachedId; // Cache the ID since it's used for registration
|
||||
|
||||
static std::vector<FileOriginPython*> _instances;
|
||||
};
|
||||
|
||||
} // namespace Gui
|
||||
|
||||
#endif // GUI_FILEORIGINPYTHON_H
|
||||
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
|
||||
158
src/Gui/OriginManager.h
Normal file
158
src/Gui/OriginManager.h
Normal file
@@ -0,0 +1,158 @@
|
||||
/***************************************************************************
|
||||
* 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 *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef GUI_ORIGINMANAGER_H
|
||||
#define GUI_ORIGINMANAGER_H
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <FCGlobal.h>
|
||||
#include <fastsignals/signal.h>
|
||||
|
||||
namespace App {
|
||||
class Document;
|
||||
}
|
||||
|
||||
namespace Gui {
|
||||
|
||||
class FileOrigin;
|
||||
|
||||
/**
|
||||
* @brief Singleton manager for document origins
|
||||
*
|
||||
* OriginManager tracks all registered FileOrigin instances and maintains
|
||||
* the current origin selection. It provides lookup methods and signals
|
||||
* for UI updates.
|
||||
*
|
||||
* The manager always has at least one origin: the local filesystem origin,
|
||||
* which is created automatically on initialization.
|
||||
*/
|
||||
class GuiExport OriginManager
|
||||
{
|
||||
public:
|
||||
/** Get the singleton instance */
|
||||
static OriginManager* instance();
|
||||
/** Destroy the singleton instance */
|
||||
static void destruct();
|
||||
|
||||
///@name Origin Registration
|
||||
//@{
|
||||
/**
|
||||
* Register an origin with the manager.
|
||||
* The manager takes ownership of the origin.
|
||||
* @param origin The origin to register
|
||||
* @return true if successfully registered (ID not already in use)
|
||||
*/
|
||||
bool registerOrigin(FileOrigin* origin);
|
||||
|
||||
/**
|
||||
* Unregister and delete an origin.
|
||||
* Cannot unregister the built-in "local" origin.
|
||||
* @param id The origin ID to unregister
|
||||
* @return true if successfully unregistered
|
||||
*/
|
||||
bool unregisterOrigin(const std::string& id);
|
||||
|
||||
/**
|
||||
* Get all registered origin IDs.
|
||||
* @return Vector of origin ID strings
|
||||
*/
|
||||
std::vector<std::string> originIds() const;
|
||||
|
||||
/**
|
||||
* Get origin by ID.
|
||||
* @param id The origin ID
|
||||
* @return The origin or nullptr if not found
|
||||
*/
|
||||
FileOrigin* getOrigin(const std::string& id) const;
|
||||
//@}
|
||||
|
||||
///@name Current Origin Selection
|
||||
//@{
|
||||
/**
|
||||
* Get the currently selected origin.
|
||||
* @return The current origin (never nullptr)
|
||||
*/
|
||||
FileOrigin* currentOrigin() const;
|
||||
|
||||
/**
|
||||
* Get the current origin ID.
|
||||
* @return The current origin's ID
|
||||
*/
|
||||
std::string currentOriginId() const;
|
||||
|
||||
/**
|
||||
* Set the current origin by ID.
|
||||
* @param id The origin ID to select
|
||||
* @return true if origin was found and selected
|
||||
*/
|
||||
bool setCurrentOrigin(const std::string& id);
|
||||
//@}
|
||||
|
||||
///@name Document Origin Resolution
|
||||
//@{
|
||||
/**
|
||||
* Find which origin owns a document.
|
||||
* Iterates through all origins to find one that claims ownership
|
||||
* based on document properties.
|
||||
* @param doc The document to check
|
||||
* @return The owning origin or nullptr if unowned
|
||||
*/
|
||||
FileOrigin* findOwningOrigin(App::Document* doc) const;
|
||||
|
||||
/**
|
||||
* Get the appropriate origin for a new document.
|
||||
* Returns the current origin.
|
||||
* @return The origin to use for new documents
|
||||
*/
|
||||
FileOrigin* originForNewDocument() const;
|
||||
//@}
|
||||
|
||||
///@name Signals
|
||||
//@{
|
||||
/** Emitted when an origin is registered */
|
||||
fastsignals::signal<void(const std::string&)> signalOriginRegistered;
|
||||
/** Emitted when an origin is unregistered */
|
||||
fastsignals::signal<void(const std::string&)> signalOriginUnregistered;
|
||||
/** Emitted when current origin changes */
|
||||
fastsignals::signal<void(const std::string&)> signalCurrentOriginChanged;
|
||||
//@}
|
||||
|
||||
protected:
|
||||
OriginManager();
|
||||
~OriginManager();
|
||||
|
||||
private:
|
||||
void loadPreferences();
|
||||
void savePreferences();
|
||||
void ensureLocalOrigin();
|
||||
|
||||
static OriginManager* _instance;
|
||||
std::map<std::string, std::unique_ptr<FileOrigin>> _origins;
|
||||
std::string _currentOriginId;
|
||||
};
|
||||
|
||||
} // namespace Gui
|
||||
|
||||
#endif // GUI_ORIGINMANAGER_H
|
||||
Reference in New Issue
Block a user