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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user