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:
2026-02-05 13:17:23 -06:00
parent 1c309a0ca8
commit 7535a48ec4
10 changed files with 1700 additions and 0 deletions

View File

@@ -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;
}