From 05f0c606d5e607abfedcdec0e3a69489e66d091e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Neves?= Date: Mon, 28 Apr 2025 16:46:52 +0100 Subject: [PATCH] Core: Fixed a bug where an empty document disappears (#20554) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Core: Fixed a bug where an empty document disappears Created a flag named autoCreated to distinguish an autoCreated document created in the startup from a manually document. Implemented a setter and a getter for this new flag. Added a codition that verifies if a document is autoCreated when opening another document to close it in the correct case. Implemented unit tests for theses cases. Fixes #19868. Signed-off-by: João Neves * Tests: Fix failing auto-created document tests Signed-off-by: João Neves * Tests: moved created tests to the existing Document test framework. Signed-off-by: João Neves --------- Signed-off-by: João Neves --- src/App/Document.cpp | 9 ++++++++ src/App/Document.h | 5 +++++ src/App/Document.pyi | 12 +++++++++++ src/App/DocumentPyImp.cpp | 21 +++++++++++++++++++ src/Gui/Application.cpp | 2 +- src/Gui/MainWindow.cpp | 3 +++ src/Mod/Test/Document.py | 43 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/App/Document.cpp b/src/App/Document.cpp index bc195831b4..01d1e3c3ac 100644 --- a/src/App/Document.cpp +++ b/src/App/Document.cpp @@ -840,6 +840,7 @@ Document::Document(const char* documentName) // So, we must increment only if the interpreter gets a reference. // Remark: We force the document Python object to own the DocumentPy instance, thus we don't // have to care about ref counting any more. + setAutoCreated(false); d = new DocumentP; Base::PyGILStateLocker lock; d->DocumentPythonObject = Py::Object(new DocumentPy(this), true); @@ -2505,6 +2506,14 @@ std::string Document::getFullName() const return myName; } +void Document::setAutoCreated(bool value) { + autoCreated = value; +} + +bool Document::isAutoCreated() const { + return autoCreated; +} + const char* Document::getProgramVersion() const { return d->programVersion.c_str(); diff --git a/src/App/Document.h b/src/App/Document.h index a7f9574df0..1d4bfb727d 100644 --- a/src/App/Document.h +++ b/src/App/Document.h @@ -389,6 +389,10 @@ public: void setClosable(bool); /// check whether the document can be closed bool isClosable() const; + /// set the document to autoCreated, this is off by default. + void setAutoCreated(bool); + /// check whether the document is autoCreated. + bool isAutoCreated() const; /** Recompute touched features and return the number of recalculated features * * @param objs: specify a sub set of objects to recompute. If empty, then @@ -674,6 +678,7 @@ private: std::string oldLabel; std::string myName; + bool autoCreated; // Flag to know if the document was automatically created at startup }; template diff --git a/src/App/Document.pyi b/src/App/Document.pyi index bd7e59f86e..23affaf828 100644 --- a/src/App/Document.pyi +++ b/src/App/Document.pyi @@ -314,6 +314,18 @@ class Document(PropertyContainer): """ ... + def setAutoCreated(self, autoCreated: bool) -> None: + """ + Set a flag that indicates if a document is autoCreated + """ + ... + + def isAutoCreated(self) -> bool: + """ + Check if the document is autoCreated. The default value is False + """ + ... + def recompute(self, objs: Sequence[DocumentObject] = None) -> int: """ recompute(objs=None): Recompute the document and returns the amount of recomputed features diff --git a/src/App/DocumentPyImp.cpp b/src/App/DocumentPyImp.cpp index cb6d47a29e..37c9829f8b 100644 --- a/src/App/DocumentPyImp.cpp +++ b/src/App/DocumentPyImp.cpp @@ -660,6 +660,27 @@ PyObject* DocumentPy::isClosable(PyObject* args) return Py::new_reference_to(Py::Boolean(ok)); } +PyObject *DocumentPy::setAutoCreated(PyObject *args) +{ + PyObject *autoCreated; + if (!PyArg_ParseTuple(args, "O!", &PyBool_Type, &autoCreated)) { + return nullptr; + } + bool value = (autoCreated == Py_True); + getDocumentPtr()->setAutoCreated(value); + + Py_RETURN_NONE; +} + +PyObject *DocumentPy::isAutoCreated(PyObject *args) +{ + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + bool ok = getDocumentPtr()->isAutoCreated(); + return Py::new_reference_to(Py::Boolean(ok)); +} + PyObject* DocumentPy::recompute(PyObject* args) { PyObject* pyobjs = Py_None; diff --git a/src/Gui/Application.cpp b/src/Gui/Application.cpp index f7680c8b91..bf26e49e1b 100644 --- a/src/Gui/Application.cpp +++ b/src/Gui/Application.cpp @@ -631,7 +631,7 @@ void Application::open(const char* FileName, const char* Module) // in case of an automatically created empty document at startup App::Document* act = App::GetApplication().getActiveDocument(); Gui::Document* gui = this->getDocument(act); - if (act && act->countObjects() == 0 && gui && !gui->isModified()) { + if (act && act->countObjects() == 0 && gui && !gui->isModified() && act->isAutoCreated()) { Command::doCommand(Command::App, "App.closeDocument('%s')", act->getName()); qApp->processEvents(); // an update is needed otherwise the new view isn't shown } diff --git a/src/Gui/MainWindow.cpp b/src/Gui/MainWindow.cpp index d10a117dd3..dc0ae0f536 100644 --- a/src/Gui/MainWindow.cpp +++ b/src/Gui/MainWindow.cpp @@ -1564,6 +1564,9 @@ void MainWindow::delayedStartup() if (hGrp->GetBool("CreateNewDoc", false)) { if (App::GetApplication().getDocuments().empty()){ Application::Instance->commandManager().runCommandByName("Std_New"); + // This document is autoCreated + App::Document* newDoc = App::GetApplication().getActiveDocument(); + newDoc->setAutoCreated(true); } } diff --git a/src/Mod/Test/Document.py b/src/Mod/Test/Document.py index baf9621aa6..dd58b95098 100644 --- a/src/Mod/Test/Document.py +++ b/src/Mod/Test/Document.py @@ -2664,3 +2664,46 @@ class FeatureTestAttribute(unittest.TestCase): def tearDown(self): FreeCAD.closeDocument("TestAttribute") + + +class DocumentAutoCreatedCases(unittest.TestCase): + def setUp(self): + self.doc = FreeCAD.newDocument("TestDoc") + + def tearDown(self): + for doc_name in FreeCAD.listDocuments().keys(): + FreeCAD.closeDocument(doc_name) + + def test_set_get_auto_created(self): + self.doc.setAutoCreated(True) + self.assertTrue(self.doc.isAutoCreated(), "autoCreated flag should be True") + + self.doc.setAutoCreated(False) + self.assertFalse(self.doc.isAutoCreated(), "autoCreated flag should be False") + + def test_auto_created_document_closes_on_opening_existing_document(self): + self.doc.setAutoCreated(True) + self.assertEqual(len(self.doc.Objects), 0) + saved_doc = FreeCAD.newDocument("SavedDoc") + file_path = tempfile.gettempdir() + os.sep + "SavedDoc.FCStd" + saved_doc.saveAs(file_path) + FreeCAD.closeDocument("SavedDoc") + FreeCAD.setActiveDocument("TestDoc") + FreeCAD.open(file_path) + if self.doc.isAutoCreated() and len(self.doc.Objects) == 0: + FreeCAD.closeDocument("TestDoc") + self.assertNotIn("TestDoc", FreeCAD.listDocuments()) + + def test_manual_document_does_not_close_on_opening_existing_document(self): + self.assertFalse(self.doc.isAutoCreated()) + self.assertEqual(len(self.doc.Objects), 0) + saved_doc = FreeCAD.newDocument("SavedDoc") + file_path = tempfile.gettempdir() + os.sep + "SavedDoc.FCStd" + saved_doc.saveAs(file_path) + FreeCAD.closeDocument("SavedDoc") + FreeCAD.setActiveDocument("TestDoc") + FreeCAD.open(file_path) + if self.doc.isAutoCreated() and len(self.doc.Objects) == 0: + FreeCAD.closeDocument("TestDoc") + self.assertIn("TestDoc", FreeCAD.listDocuments()) + self.assertIn("SavedDoc", FreeCAD.listDocuments())