From 7535a48ec4cd01aef906a68ad7aff69f9cdcc4f8 Mon Sep 17 00:00:00 2001 From: forbes-0023 Date: Thu, 5 Feb 2026 13:17:23 -0600 Subject: [PATCH] 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 --- src/Gui/Application.cpp | 5 + src/Gui/ApplicationPy.cpp | 173 +++++++++++ src/Gui/ApplicationPy.h | 8 + src/Gui/CMakeLists.txt | 6 + src/Gui/FileOrigin.cpp | 128 ++++++++ src/Gui/FileOrigin.h | 259 ++++++++++++++++ src/Gui/FileOriginPython.cpp | 578 +++++++++++++++++++++++++++++++++++ src/Gui/FileOriginPython.h | 146 +++++++++ src/Gui/OriginManager.cpp | 239 +++++++++++++++ src/Gui/OriginManager.h | 158 ++++++++++ 10 files changed, 1700 insertions(+) create mode 100644 src/Gui/FileOrigin.cpp create mode 100644 src/Gui/FileOrigin.h create mode 100644 src/Gui/FileOriginPython.cpp create mode 100644 src/Gui/FileOriginPython.h create mode 100644 src/Gui/OriginManager.cpp create mode 100644 src/Gui/OriginManager.h diff --git a/src/Gui/Application.cpp b/src/Gui/Application.cpp index 20abafd6f4..ac7eda9c4d 100644 --- a/src/Gui/Application.cpp +++ b/src/Gui/Application.cpp @@ -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(); diff --git a/src/Gui/ApplicationPy.cpp b/src/Gui/ApplicationPy.cpp index a93b009774..5686d8f52a 100644 --- a/src/Gui/ApplicationPy.cpp +++ b/src/Gui/ApplicationPy.cpp @@ -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(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(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(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(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; +} diff --git a/src/Gui/ApplicationPy.h b/src/Gui/ApplicationPy.h index 203c346b51..e92211e8db 100644 --- a/src/Gui/ApplicationPy.h +++ b/src/Gui/ApplicationPy.h @@ -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); diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index 64b1723df0..da3115b7e8 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -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 diff --git a/src/Gui/FileOrigin.cpp b/src/Gui/FileOrigin.cpp new file mode 100644 index 0000000000..0ed5c9ba81 --- /dev/null +++ b/src/Gui/FileOrigin.cpp @@ -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 +#include +#include +#include + +#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 diff --git a/src/Gui/FileOrigin.h b/src/Gui/FileOrigin.h new file mode 100644 index 0000000000..26323e8d18 --- /dev/null +++ b/src/Gui/FileOrigin.h @@ -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 +#include +#include +#include + +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 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 diff --git a/src/Gui/FileOriginPython.cpp b/src/Gui/FileOriginPython.cpp new file mode 100644 index 0000000000..b5e7c2719d --- /dev/null +++ b/src/Gui/FileOriginPython.cpp @@ -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 +#include +#include +#include +#include + +#include "FileOriginPython.h" +#include "OriginManager.h" +#include "BitmapFactory.h" + + +namespace Gui { + +std::vector 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(Py::Long(result)); + if (t >= 0 && t <= static_cast(OriginType::Custom)) { + return static_cast(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(Py::Long(result)); + if (s >= 0 && s <= static_cast(ConnectionState::Error)) { + return static_cast(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(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(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 diff --git a/src/Gui/FileOriginPython.h b/src/Gui/FileOriginPython.h new file mode 100644 index 0000000000..1348e9022d --- /dev/null +++ b/src/Gui/FileOriginPython.h @@ -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 +#include +#include +#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 _instances; +}; + +} // namespace Gui + +#endif // GUI_FILEORIGINPYTHON_H diff --git a/src/Gui/OriginManager.cpp b/src/Gui/OriginManager.cpp new file mode 100644 index 0000000000..28add9234b --- /dev/null +++ b/src/Gui/OriginManager.cpp @@ -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 +#include + +#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(); + _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(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 OriginManager::originIds() const +{ + std::vector 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 diff --git a/src/Gui/OriginManager.h b/src/Gui/OriginManager.h new file mode 100644 index 0000000000..3c396d9d33 --- /dev/null +++ b/src/Gui/OriginManager.h @@ -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 +#include +#include +#include +#include +#include + +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 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 signalOriginRegistered; + /** Emitted when an origin is unregistered */ + fastsignals::signal signalOriginUnregistered; + /** Emitted when current origin changes */ + fastsignals::signal signalCurrentOriginChanged; + //@} + +protected: + OriginManager(); + ~OriginManager(); + +private: + void loadPreferences(); + void savePreferences(); + void ensureLocalOrigin(); + + static OriginManager* _instance; + std::map> _origins; + std::string _currentOriginId; +}; + +} // namespace Gui + +#endif // GUI_ORIGINMANAGER_H