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

@@ -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();

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

View File

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

View File

@@ -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
View 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
View 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

View 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
View 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
View 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
View 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