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 "Workbench.h"
|
||||||
#include "WorkbenchManager.h"
|
#include "WorkbenchManager.h"
|
||||||
#include "WorkbenchManipulator.h"
|
#include "WorkbenchManipulator.h"
|
||||||
|
#include "OriginManager.h"
|
||||||
#include "WidgetFactory.h"
|
#include "WidgetFactory.h"
|
||||||
#include "3Dconnexion/navlib/NavlibInterface.h"
|
#include "3Dconnexion/navlib/NavlibInterface.h"
|
||||||
#include "Inventor/SoFCPlacementIndicatorKit.h"
|
#include "Inventor/SoFCPlacementIndicatorKit.h"
|
||||||
@@ -691,6 +692,9 @@ Application::Application(bool GUIenabled)
|
|||||||
if (GUIenabled) {
|
if (GUIenabled) {
|
||||||
createStandardOperations();
|
createStandardOperations();
|
||||||
MacroCommand::load();
|
MacroCommand::load();
|
||||||
|
|
||||||
|
// Initialize the origin manager (creates local origin by default)
|
||||||
|
OriginManager::instance();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// clang-format on
|
// clang-format on
|
||||||
@@ -702,6 +706,7 @@ Application::~Application()
|
|||||||
delete pNavlibInterface;
|
delete pNavlibInterface;
|
||||||
#endif
|
#endif
|
||||||
WorkbenchManager::destruct();
|
WorkbenchManager::destruct();
|
||||||
|
OriginManager::destruct();
|
||||||
WorkbenchManipulator::removeAll();
|
WorkbenchManipulator::removeAll();
|
||||||
SelectionSingleton::destruct();
|
SelectionSingleton::destruct();
|
||||||
Translator::destruct();
|
Translator::destruct();
|
||||||
|
|||||||
@@ -66,6 +66,8 @@
|
|||||||
#include "Workbench.h"
|
#include "Workbench.h"
|
||||||
#include "WorkbenchManager.h"
|
#include "WorkbenchManager.h"
|
||||||
#include "WorkbenchManipulatorPython.h"
|
#include "WorkbenchManipulatorPython.h"
|
||||||
|
#include "FileOriginPython.h"
|
||||||
|
#include "OriginManager.h"
|
||||||
#include "Inventor/MarkerBitmaps.h"
|
#include "Inventor/MarkerBitmaps.h"
|
||||||
#include "Language/Translator.h"
|
#include "Language/Translator.h"
|
||||||
|
|
||||||
@@ -550,6 +552,50 @@ PyMethodDef ApplicationPy::Methods[] = {
|
|||||||
METH_VARARGS,
|
METH_VARARGS,
|
||||||
"resumeWaitCursor() -> None\n\n"
|
"resumeWaitCursor() -> None\n\n"
|
||||||
"Resumes the application's wait cursor and event filter."},
|
"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 */
|
{nullptr, nullptr, 0, nullptr} /* Sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1966,3 +2012,130 @@ PyObject* ApplicationPy::sResumeWaitCursor(PyObject* /*self*/, PyObject* args)
|
|||||||
WaitCursor::resume();
|
WaitCursor::resume();
|
||||||
Py_RETURN_NONE;
|
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* sAddWbManipulator (PyObject *self,PyObject *args);
|
||||||
static PyObject* sRemoveWbManipulator (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* sListUserEditModes (PyObject *self,PyObject *args);
|
||||||
static PyObject* sGetUserEditMode (PyObject *self,PyObject *args);
|
static PyObject* sGetUserEditMode (PyObject *self,PyObject *args);
|
||||||
static PyObject* sSetUserEditMode (PyObject *self,PyObject *args);
|
static PyObject* sSetUserEditMode (PyObject *self,PyObject *args);
|
||||||
|
|||||||
@@ -1317,6 +1317,7 @@ SET(Workbench_CPP_SRCS
|
|||||||
OverlayManager.cpp
|
OverlayManager.cpp
|
||||||
OverlayWidgets.cpp
|
OverlayWidgets.cpp
|
||||||
MenuManager.cpp
|
MenuManager.cpp
|
||||||
|
OriginManager.cpp
|
||||||
PythonWorkbenchPyImp.cpp
|
PythonWorkbenchPyImp.cpp
|
||||||
ToolBarAreaWidget.cpp
|
ToolBarAreaWidget.cpp
|
||||||
ToolBarManager.cpp
|
ToolBarManager.cpp
|
||||||
@@ -1334,6 +1335,7 @@ SET(Workbench_SRCS
|
|||||||
OverlayManager.h
|
OverlayManager.h
|
||||||
OverlayWidgets.h
|
OverlayWidgets.h
|
||||||
MenuManager.h
|
MenuManager.h
|
||||||
|
OriginManager.h
|
||||||
ToolBarAreaWidget.h
|
ToolBarAreaWidget.h
|
||||||
ToolBarManager.h
|
ToolBarManager.h
|
||||||
ToolBoxManager.h
|
ToolBoxManager.h
|
||||||
@@ -1374,6 +1376,8 @@ SET(FreeCADGui_CPP_SRCS
|
|||||||
DocumentObserver.cpp
|
DocumentObserver.cpp
|
||||||
DocumentObserverPython.cpp
|
DocumentObserverPython.cpp
|
||||||
EditableDatumLabel.cpp
|
EditableDatumLabel.cpp
|
||||||
|
FileOrigin.cpp
|
||||||
|
FileOriginPython.cpp
|
||||||
ExpressionBinding.cpp
|
ExpressionBinding.cpp
|
||||||
ExpressionBindingPy.cpp
|
ExpressionBindingPy.cpp
|
||||||
GraphicsViewZoom.cpp
|
GraphicsViewZoom.cpp
|
||||||
@@ -1414,6 +1418,8 @@ SET(FreeCADGui_SRCS
|
|||||||
DocumentObserver.h
|
DocumentObserver.h
|
||||||
DocumentObserverPython.h
|
DocumentObserverPython.h
|
||||||
EditableDatumLabel.h
|
EditableDatumLabel.h
|
||||||
|
FileOrigin.h
|
||||||
|
FileOriginPython.h
|
||||||
ExpressionBinding.h
|
ExpressionBinding.h
|
||||||
ExpressionBindingPy.h
|
ExpressionBindingPy.h
|
||||||
ExpressionCompleter.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