From 2b7f53b330e25b0bccf5e045798110d981ba7e82 Mon Sep 17 00:00:00 2001 From: Syres916 <46537884+Syres916@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:45:49 +0000 Subject: [PATCH 1/6] [Sketcher] fix compiler warnings --- src/Mod/Sketcher/Gui/TaskSketcherElements.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Mod/Sketcher/Gui/TaskSketcherElements.cpp b/src/Mod/Sketcher/Gui/TaskSketcherElements.cpp index c7c7d04605ff..01bb88425c30 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherElements.cpp +++ b/src/Mod/Sketcher/Gui/TaskSketcherElements.cpp @@ -91,13 +91,9 @@ QT_TRANSLATE_NOOP("SketcherGui::ElementView", "Select Vertical Axis"); /// ACTSONSELECTION is a true/false value to activate the command only if a selection is made #define CONTEXT_ITEM(ICONSTR, NAMESTR, CMDSTR, FUNC, ACTSONSELECTION) \ QIcon icon_##FUNC(Gui::BitmapFactory().pixmap(ICONSTR)); \ - QAction* constr_##FUNC = menu.addAction( \ - icon_##FUNC, \ - tr(NAMESTR), \ - this, \ - SLOT(FUNC()), \ - QKeySequence(QString::fromUtf8( \ - Gui::Application::Instance->commandManager().getCommandByName(CMDSTR)->getAccel()))); \ + QAction* constr_##FUNC = menu.addAction(icon_##FUNC, tr(NAMESTR), this, SLOT(FUNC())); \ + constr_##FUNC->setShortcut(QKeySequence(QString::fromUtf8( \ + Gui::Application::Instance->commandManager().getCommandByName(CMDSTR)->getAccel()))); \ if (ACTSONSELECTION) \ constr_##FUNC->setEnabled(!items.isEmpty()); \ else \ @@ -686,8 +682,8 @@ void ElementView::contextMenuEvent(QContextMenuEvent* event) menu.addSeparator(); - QAction* remove = menu.addAction( - tr("Delete"), this, &ElementView::deleteSelectedItems, QKeySequence(QKeySequence::Delete)); + QAction* remove = menu.addAction(tr("Delete"), this, &ElementView::deleteSelectedItems); + remove->setShortcut(QKeySequence(QKeySequence::Delete)); remove->setEnabled(!items.isEmpty()); menu.menuAction()->setIconVisibleInMenu(true); From f9189fa10321d1c396d15f9150da87b91bd2a987 Mon Sep 17 00:00:00 2001 From: Syres916 <46537884+Syres916@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:50:04 +0000 Subject: [PATCH 2/6] [Sketcher] Fix compiler warnings --- .../Sketcher/Gui/TaskSketcherConstraints.cpp | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/src/Mod/Sketcher/Gui/TaskSketcherConstraints.cpp b/src/Mod/Sketcher/Gui/TaskSketcherConstraints.cpp index 04863797a21d..b8281c6e582b 100644 --- a/src/Mod/Sketcher/Gui/TaskSketcherConstraints.cpp +++ b/src/Mod/Sketcher/Gui/TaskSketcherConstraints.cpp @@ -62,7 +62,7 @@ namespace bp = boost::placeholders; // Translation block for context menu: do not remove #if 0 -QT_TRANSLATE_NOOP("SketcherGui::ConstraintView", "Select Elements"); + QT_TRANSLATE_NOOP("SketcherGui::ConstraintView", "Select Elements"); #endif /// Inserts a QAction into an existing menu @@ -73,13 +73,9 @@ QT_TRANSLATE_NOOP("SketcherGui::ConstraintView", "Select Elements"); /// ACTSONSELECTION is a true/false value to activate the command only if a selection is made #define CONTEXT_ITEM(ICONSTR, NAMESTR, CMDSTR, FUNC, ACTSONSELECTION) \ QIcon icon_##FUNC(Gui::BitmapFactory().pixmap(ICONSTR)); \ - QAction* constr_##FUNC = menu.addAction( \ - icon_##FUNC, \ - tr(NAMESTR), \ - this, \ - SLOT(FUNC()), \ - QKeySequence(QString::fromUtf8( \ - Gui::Application::Instance->commandManager().getCommandByName(CMDSTR)->getAccel()))); \ + QAction* constr_##FUNC = menu.addAction(icon_##FUNC, tr(NAMESTR), this, SLOT(FUNC())); \ + constr_##FUNC->setShortcut(QKeySequence(QString::fromUtf8( \ + Gui::Application::Instance->commandManager().getCommandByName(CMDSTR)->getAccel()))); \ if (ACTSONSELECTION) \ constr_##FUNC->setEnabled(!items.isEmpty()); \ else \ @@ -586,24 +582,18 @@ void ConstraintView::contextMenuEvent(QContextMenuEvent* event) doSelectConstraints, true) - QAction* rename = menu.addAction(tr("Rename"), - this, - &ConstraintView::renameCurrentItem + QAction* rename = menu.addAction(tr("Rename"), this, &ConstraintView::renameCurrentItem); #ifndef Q_OS_MAC// on Mac F2 doesn't seem to trigger an edit signal - , - QKeySequence(Qt::Key_F2) + rename->setShortcut(QKeySequence(Qt::Key_F2)); #endif - ); rename->setEnabled(item != nullptr); QAction* center = menu.addAction(tr("Center sketch"), this, &ConstraintView::centerSelectedItems); center->setEnabled(item != nullptr); - QAction* remove = menu.addAction(tr("Delete"), - this, - &ConstraintView::deleteSelectedItems, - QKeySequence(QKeySequence::Delete)); + QAction* remove = menu.addAction(tr("Delete"), this, &ConstraintView::deleteSelectedItems); + remove->setShortcut(QKeySequence(QKeySequence::Delete)); remove->setEnabled(!items.isEmpty()); QAction* swap = menu.addAction( @@ -1437,7 +1427,9 @@ void TaskSketcherConstraints::OnChange(Base::Subject& rCaller, cons } if (actNum >= 0) { assert(actNum < static_cast(ui->settingsButton->actions().size())); - qAsConst(ui->settingsButton)->actions()[actNum]->setChecked(hGrp->GetBool(rcReason, false)); + std::as_const(ui->settingsButton) + ->actions()[actNum] + ->setChecked(hGrp->GetBool(rcReason, false)); } } From 7d56de9b9e5504758f0d3767ccb58d954d613e86 Mon Sep 17 00:00:00 2001 From: Syres916 <46537884+Syres916@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:51:19 +0000 Subject: [PATCH 3/6] [Part] Fix compiler warnings --- src/Mod/Part/App/AppPartPy.cpp | 29 +++-------------------------- src/Mod/Part/App/FT2FC.cpp | 18 ++++++++---------- src/Mod/Part/App/FT2FC.h | 4 ++-- 3 files changed, 13 insertions(+), 38 deletions(-) diff --git a/src/Mod/Part/App/AppPartPy.cpp b/src/Mod/Part/App/AppPartPy.cpp index 98960575fe1b..61a0c86fd44e 100644 --- a/src/Mod/Part/App/AppPartPy.cpp +++ b/src/Mod/Part/App/AppPartPy.cpp @@ -1937,7 +1937,7 @@ class Module : public Py::ExtensionModule double height; double track = 0; - Py_UNICODE *unichars = nullptr; + Py_UCS4 *unichars = nullptr; Py_ssize_t pysize; PyObject *CharList; @@ -1970,34 +1970,11 @@ class Module : public Py::ExtensionModule } pysize = PyUnicode_GetLength(p); -#if PY_VERSION_HEX < 0x03090000 - unichars = PyUnicode_AS_UNICODE(p); -#else -#ifdef FC_OS_WIN32 - //PyUNICODE is only 16 bits on Windows (wchar_t), so passing 32 bit UCS4 - //will result in unknown glyph in even positions, and wrong characters in - //odd positions. - unichars = (Py_UNICODE*)PyUnicode_AsWideCharString(p, &pysize); -#else - unichars = (Py_UNICODE *)PyUnicode_AsUCS4Copy(p); -#endif -#endif + unichars = PyUnicode_AsUCS4Copy(p); } else if (PyUnicode_Check(intext)) { pysize = PyUnicode_GetLength(intext); - -#if PY_VERSION_HEX < 0x03090000 - unichars = PyUnicode_AS_UNICODE(intext); -#else -#ifdef FC_OS_WIN32 - //PyUNICODE is only 16 bits on Windows (wchar_t), so passing 32 bit UCS4 - //will result in unknown glyph in even positions, and wrong characters in - //odd positions. - unichars = (Py_UNICODE*)PyUnicode_AsWideCharString(intext, &pysize); -#else - unichars = (Py_UNICODE *)PyUnicode_AsUCS4Copy(intext); -#endif -#endif + unichars = PyUnicode_AsUCS4Copy(intext); } else { throw Py::TypeError("** makeWireString bad text parameter"); diff --git a/src/Mod/Part/App/FT2FC.cpp b/src/Mod/Part/App/FT2FC.cpp index 31fecd7422a0..76ca6959cfaa 100644 --- a/src/Mod/Part/App/FT2FC.cpp +++ b/src/Mod/Part/App/FT2FC.cpp @@ -76,16 +76,14 @@ using namespace Part; -using UNICHAR = unsigned long; // ul is FT2's codepoint type <=> Py_UNICODE2/4 - // Private function prototypes -PyObject* getGlyphContours(FT_Face FTFace, UNICHAR currchar, double PenPos, double Scale,int charNum, double tracking); -FT_Vector getKerning(FT_Face FTFace, UNICHAR lc, UNICHAR rc); +PyObject* getGlyphContours(FT_Face FTFace, FT_ULong currchar, double PenPos, double Scale,int charNum, double tracking); +FT_Vector getKerning(FT_Face FTFace, FT_ULong lc, FT_ULong rc); TopoDS_Wire edgesToWire(std::vector Edges); int calcClockDir(std::vector points); // for compatibility with old version - separate path & filename -PyObject* FT2FC(const Py_UNICODE *PyUString, +PyObject* FT2FC(const Py_UCS4 *PyUString, const size_t length, const char *FontPath, const char *FontName, @@ -99,7 +97,7 @@ PyObject* FT2FC(const Py_UNICODE *PyUString, } // get string's wires (contours) in FC/OCC coords -PyObject* FT2FC(const Py_UNICODE *PyUString, +PyObject* FT2FC(const Py_UCS4 *PyUString, const size_t length, const char *FontSpec, const double stringheight, // fc coords @@ -114,7 +112,7 @@ PyObject* FT2FC(const Py_UNICODE *PyUString, std::stringstream ErrorMsg; double PenPos = 0, scalefactor; - UNICHAR prevchar = 0, currchar = 0; + FT_ULong prevchar = 0, currchar = 0; int cadv; size_t i; Py::List CharList; @@ -215,7 +213,7 @@ struct FTDC_Ctx { std::vector wDir; std::vector Edges; std::vector polyPoints; - UNICHAR currchar; + FT_ULong currchar; FT_Vector LastVert; Handle(Geom_Surface) surf; }; @@ -321,7 +319,7 @@ static FT_Outline_Funcs FTcbFuncs = { //********** FT2FC Helpers // get glyph outline in wires -PyObject* getGlyphContours(FT_Face FTFace, UNICHAR currchar, double PenPos, double Scale, int charNum, double tracking) { +PyObject* getGlyphContours(FT_Face FTFace, FT_ULong currchar, double PenPos, double Scale, int charNum, double tracking) { FT_Error error = 0; std::stringstream ErrorMsg; gp_Pnt origin = gp_Pnt(0.0,0.0,0.0); @@ -393,7 +391,7 @@ PyObject* getGlyphContours(FT_Face FTFace, UNICHAR currchar, double PenPos, doub // get kerning values for this char pair //TODO: should check FT_HASKERNING flag? returns (0,0) if no kerning? -FT_Vector getKerning(FT_Face FTFace, UNICHAR lc, UNICHAR rc) { +FT_Vector getKerning(FT_Face FTFace, FT_ULong lc, FT_ULong rc) { FT_Vector retXY; FT_Error error; std::stringstream ErrorMsg; diff --git a/src/Mod/Part/App/FT2FC.h b/src/Mod/Part/App/FT2FC.h index f7b8eb4968d8..907039496de9 100644 --- a/src/Mod/Part/App/FT2FC.h +++ b/src/Mod/Part/App/FT2FC.h @@ -30,14 +30,14 @@ #ifndef FT2FC_H #define FT2FC_H // public functions -PyObject* FT2FC(const Py_UNICODE *unichars, +PyObject* FT2FC(const Py_UCS4 *unichars, const size_t length, const char *FontPath, const char *FontName, const double stringheight, const double tracking); -PyObject* FT2FC(const Py_UNICODE *unichars, +PyObject* FT2FC(const Py_UCS4 *unichars, const size_t length, const char *FontSpec, const double stringheight, From 93a507379a7cc00813361a79f028a7f215564a5e Mon Sep 17 00:00:00 2001 From: Syres916 <46537884+Syres916@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:52:38 +0000 Subject: [PATCH 4/6] [App] Fix compiler warnings --- src/App/Document.cpp | 8007 +++++++++++++++++++------------------ src/App/Document.h | 2 + src/App/DocumentPyImp.cpp | 39 +- 3 files changed, 4040 insertions(+), 4008 deletions(-) diff --git a/src/App/Document.cpp b/src/App/Document.cpp index 52ba468f3456..95d45329d296 100644 --- a/src/App/Document.cpp +++ b/src/App/Document.cpp @@ -1,3997 +1,4010 @@ -/*************************************************************************** - * Copyright (c) 2002 Jürgen Riegel * - * * - * 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 * - * * - ***************************************************************************/ - - -/*! -\defgroup Document Document -\ingroup APP -\brief The Base class of the FreeCAD Document - -This (besides the App::Application class) is the most important class in FreeCAD. -It contains all the data of the opened, saved, or newly created FreeCAD Document. -The App::Document manages the Undo and Redo mechanism and the linking of documents. - -\namespace App \class App::Document -This is besides the Application class the most important class in FreeCAD -It contains all the data of the opened, saved or newly created FreeCAD Document. -The Document manage the Undo and Redo mechanism and the linking of documents. - -Note: the documents are not free objects. They are completely handled by the -App::Application. Only the Application can Open or destroy a document. - -\section Exception Exception handling -As the document is the main data structure of FreeCAD we have to take a close -look at how Exceptions affect the integrity of the App::Document. - -\section UndoRedo Undo Redo an Transactions -Undo Redo handling is one of the major mechanism of a document in terms of -user friendliness and speed (no one will wait for Undo too long). - -\section Dependency Graph and dependency handling -The FreeCAD document handles the dependencies of its DocumentObjects with -an adjacence list. This gives the opportunity to calculate the shortest -recompute path. Also, it enables more complicated dependencies beyond trees. - -@see App::Application -@see App::DocumentObject -*/ - -#include "PreCompiled.h" - -#ifndef _PreComp_ -# include -# include -# include -#endif - -#include -#include - -#ifdef USE_OLD_DAG -#include -#include -#include -#include -#endif //USE_OLD_DAG - -#include -#include -#include -#include - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "Document.h" -#include "private/DocumentP.h" -#include "Application.h" -#include "AutoTransaction.h" -#include "ExpressionParser.h" -#include "GeoFeature.h" -#include "License.h" -#include "Link.h" -#include "MergeDocuments.h" -#include "Transactions.h" - -#ifdef _MSC_VER -#include -#endif -#include -#include -#include -#include - - -FC_LOG_LEVEL_INIT("App", true, true, true) - -using Base::Console; -using Base::streq; -using Base::Writer; -using namespace App; -using namespace std; -using namespace boost; -using namespace zipios; - -#if FC_DEBUG -# define FC_LOGFEATUREUPDATE -#endif - -namespace fs = boost::filesystem; - -namespace App { - -static bool globalIsRestoring; -static bool globalIsRelabeling; - -DocumentP::DocumentP() -{ - static std::random_device _RD; - static std::mt19937 _RGEN(_RD()); - static std::uniform_int_distribution<> _RDIST(0, 5000); - // Set some random offset to reduce likelihood of ID collision when - // copying shape from other document. It is probably better to randomize - // on each object ID. - lastObjectId = _RDIST(_RGEN); - activeObject = nullptr; - activeUndoTransaction = nullptr; - iTransactionMode = 0; - rollback = false; - undoing = false; - committing = false; - opentransaction = false; - StatusBits.set((size_t)Document::Closable, true); - StatusBits.set((size_t)Document::KeepTrailingDigits, true); - StatusBits.set((size_t)Document::Restoring, false); - iUndoMode = 0; - UndoMemSize = 0; - UndoMaxStackSize = 20; -} - -} // namespace App - -PROPERTY_SOURCE(App::Document, App::PropertyContainer) - -bool Document::testStatus(Status pos) const -{ - return d->StatusBits.test((size_t)pos); -} - -void Document::setStatus(Status pos, bool on) -{ - d->StatusBits.set((size_t)pos, on); -} - -//bool _has_cycle_dfs(const DependencyList & g, vertex_t u, default_color_type * color) -//{ -// color[u] = gray_color; -// graph_traits < DependencyList >::adjacency_iterator vi, vi_end; -// for (tie(vi, vi_end) = adjacent_vertices(u, g); vi != vi_end; ++vi) -// if (color[*vi] == white_color) -// if (has_cycle_dfs(g, *vi, color)) -// return true; // cycle detected, return immediately -// else if (color[*vi] == gray_color) // *vi is an ancestor! -// return true; -// color[u] = black_color; -// return false; -//} - -bool Document::checkOnCycle() -{ -#if 0 - std::vector < default_color_type > color(num_vertices(_DepList), white_color); - graph_traits < DependencyList >::vertex_iterator vi, vi_end; - for (tie(vi, vi_end) = vertices(_DepList); vi != vi_end; ++vi) - if (color[*vi] == white_color) - if (_has_cycle_dfs(_DepList, *vi, &color[0])) - return true; -#endif - return false; -} - -bool Document::undo(int id) -{ - if (d->iUndoMode) { - if(id) { - auto it = mUndoMap.find(id); - if(it == mUndoMap.end()) - return false; - if(it->second != d->activeUndoTransaction) { - while(!mUndoTransactions.empty() && mUndoTransactions.back()!=it->second) - undo(0); - } - } - - if (d->activeUndoTransaction) - _commitTransaction(true); - if (mUndoTransactions.empty()) - return false; - // redo - d->activeUndoTransaction = new Transaction(mUndoTransactions.back()->getID()); - d->activeUndoTransaction->Name = mUndoTransactions.back()->Name; - - { - Base::FlagToggler flag(d->undoing); - // applying the undo - mUndoTransactions.back()->apply(*this,false); - - // save the redo - mRedoMap[d->activeUndoTransaction->getID()] = d->activeUndoTransaction; - mRedoTransactions.push_back(d->activeUndoTransaction); - d->activeUndoTransaction = nullptr; - - mUndoMap.erase(mUndoTransactions.back()->getID()); - delete mUndoTransactions.back(); - mUndoTransactions.pop_back(); - - } - - for(auto & obj : d->objectArray) { - if(obj->testStatus(ObjectStatus::PendingTransactionUpdate)) { - obj->onUndoRedoFinished(); - obj->setStatus(ObjectStatus::PendingTransactionUpdate,false); - } - } - - signalUndo(*this); // now signal the undo - - return true; - } - - return false; -} - -bool Document::redo(int id) -{ - if (d->iUndoMode) { - if(id) { - auto it = mRedoMap.find(id); - if(it == mRedoMap.end()) - return false; - while(!mRedoTransactions.empty() && mRedoTransactions.back()!=it->second) - redo(0); - } - - if (d->activeUndoTransaction) - _commitTransaction(true); - - assert(mRedoTransactions.size()!=0); - - // undo - d->activeUndoTransaction = new Transaction(mRedoTransactions.back()->getID()); - d->activeUndoTransaction->Name = mRedoTransactions.back()->Name; - - // do the redo - { - Base::FlagToggler flag(d->undoing); - mRedoTransactions.back()->apply(*this,true); - - mUndoMap[d->activeUndoTransaction->getID()] = d->activeUndoTransaction; - mUndoTransactions.push_back(d->activeUndoTransaction); - d->activeUndoTransaction = nullptr; - - mRedoMap.erase(mRedoTransactions.back()->getID()); - delete mRedoTransactions.back(); - mRedoTransactions.pop_back(); - } - - for(auto & obj : d->objectArray) { - if(obj->testStatus(ObjectStatus::PendingTransactionUpdate)) { - obj->onUndoRedoFinished(); - obj->setStatus(ObjectStatus::PendingTransactionUpdate,false); - } - } - - signalRedo(*this); - return true; - } - - return false; -} - -void Document::addOrRemovePropertyOfObject(TransactionalObject* obj, Property *prop, bool add) -{ - if (!prop || !obj || !obj->isAttachedToDocument()) - return; - if(d->iUndoMode && !isPerformingTransaction() && !d->activeUndoTransaction) { - if(!testStatus(Restoring) || testStatus(Importing)) { - int tid=0; - const char *name = GetApplication().getActiveTransaction(&tid); - if(name && tid>0) - _openTransaction(name,tid); - } - } - if (d->activeUndoTransaction && !d->rollback) - d->activeUndoTransaction->addOrRemoveProperty(obj, prop, add); -} - -bool Document::isPerformingTransaction() const -{ - return d->undoing || d->rollback; -} - -std::vector Document::getAvailableUndoNames() const -{ - std::vector vList; - if (d->activeUndoTransaction) - vList.push_back(d->activeUndoTransaction->Name); - for (std::list::const_reverse_iterator It=mUndoTransactions.rbegin();It!=mUndoTransactions.rend();++It) - vList.push_back((**It).Name); - return vList; -} - -std::vector Document::getAvailableRedoNames() const -{ - std::vector vList; - for (std::list::const_reverse_iterator It=mRedoTransactions.rbegin();It!=mRedoTransactions.rend();++It) - vList.push_back((**It).Name); - return vList; -} - -void Document::openTransaction(const char* name) { - if(isPerformingTransaction() || d->committing) { - if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) - FC_WARN("Cannot open transaction while transacting"); - return; - } - - GetApplication().setActiveTransaction(name?name:""); -} - -int Document::_openTransaction(const char* name, int id) -{ - if(isPerformingTransaction() || d->committing) { - if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) - FC_WARN("Cannot open transaction while transacting"); - return 0; - } - - if (d->iUndoMode) { - // Avoid recursive calls that is possible while - // clearing the redo transactions and will cause - // a double deletion of some transaction and thus - // a segmentation fault - if (d->opentransaction) - return 0; - Base::FlagToggler<> flag(d->opentransaction); - - if(id && mUndoMap.find(id)!=mUndoMap.end()) - throw Base::RuntimeError("invalid transaction id"); - if (d->activeUndoTransaction) - _commitTransaction(true); - _clearRedos(); - - d->activeUndoTransaction = new Transaction(id); - if (!name) - name = ""; - d->activeUndoTransaction->Name = name; - mUndoMap[d->activeUndoTransaction->getID()] = d->activeUndoTransaction; - id = d->activeUndoTransaction->getID(); - - signalOpenTransaction(*this, name); - - auto &app = GetApplication(); - auto activeDoc = app.getActiveDocument(); - if(activeDoc && - activeDoc!=this && - !activeDoc->hasPendingTransaction()) - { - std::string aname("-> "); - aname += d->activeUndoTransaction->Name; - FC_LOG("auto transaction " << getName() << " -> " << activeDoc->getName()); - activeDoc->_openTransaction(aname.c_str(),id); - } - return id; - } - return 0; -} - -void Document::renameTransaction(const char *name, int id) { - if(name && d->activeUndoTransaction && d->activeUndoTransaction->getID()==id) { - if(boost::starts_with(d->activeUndoTransaction->Name, "-> ")) - d->activeUndoTransaction->Name.resize(3); - else - d->activeUndoTransaction->Name.clear(); - d->activeUndoTransaction->Name += name; - } -} - -void Document::_checkTransaction(DocumentObject* pcDelObj, const Property *What, int line) -{ - // if the undo is active but no transaction open, open one! - if (d->iUndoMode && !isPerformingTransaction()) { - if (!d->activeUndoTransaction) { - if(!testStatus(Restoring) || testStatus(Importing)) { - int tid=0; - const char *name = GetApplication().getActiveTransaction(&tid); - if(name && tid>0) { - bool ignore = false; - if(What && What->testStatus(Property::NoModify)) - ignore = true; - if(FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { - if(What) - FC_LOG((ignore?"ignore":"auto") << " transaction (" - << line << ") '" << What->getFullName()); - else - FC_LOG((ignore?"ignore":"auto") <<" transaction (" - << line << ") '" << name << "' in " << getName()); - } - if(!ignore) - _openTransaction(name,tid); - return; - } - } - if(!pcDelObj) - return; - // When the object is going to be deleted we have to check if it has already been added to - // the undo transactions - std::list::iterator it; - for (it = mUndoTransactions.begin(); it != mUndoTransactions.end(); ++it) { - if ((*it)->hasObject(pcDelObj)) { - _openTransaction("Delete"); - break; - } - } - } - } -} - -void Document::_clearRedos() -{ - if(isPerformingTransaction() || d->committing) { - FC_ERR("Cannot clear redo while transacting"); - return; - } - - mRedoMap.clear(); - while (!mRedoTransactions.empty()) { - delete mRedoTransactions.back(); - mRedoTransactions.pop_back(); - } -} - -void Document::commitTransaction() { - if(isPerformingTransaction() || d->committing) { - if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) - FC_WARN("Cannot commit transaction while transacting"); - return; - } - - if (d->activeUndoTransaction) - GetApplication().closeActiveTransaction(false,d->activeUndoTransaction->getID()); -} - -void Document::_commitTransaction(bool notify) -{ - if (isPerformingTransaction()) { - if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) - FC_WARN("Cannot commit transaction while transacting"); - return; - } - else if (d->committing) { - // for a recursive call return without printing a warning - return; - } - - if (d->activeUndoTransaction) { - Base::FlagToggler<> flag(d->committing); - Application::TransactionSignaller signaller(false,true); - int id = d->activeUndoTransaction->getID(); - mUndoTransactions.push_back(d->activeUndoTransaction); - d->activeUndoTransaction = nullptr; - // check the stack for the limits - if(mUndoTransactions.size() > d->UndoMaxStackSize){ - mUndoMap.erase(mUndoTransactions.front()->getID()); - delete mUndoTransactions.front(); - mUndoTransactions.pop_front(); - } - signalCommitTransaction(*this); - - // closeActiveTransaction() may call again _commitTransaction() - if (notify) - GetApplication().closeActiveTransaction(false,id); - } -} - -void Document::abortTransaction() { - if(isPerformingTransaction() || d->committing) { - if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) - FC_WARN("Cannot abort transaction while transacting"); - return; - } - if (d->activeUndoTransaction) - GetApplication().closeActiveTransaction(true,d->activeUndoTransaction->getID()); -} - -void Document::_abortTransaction() -{ - if(isPerformingTransaction() || d->committing) { - if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) - FC_WARN("Cannot abort transaction while transacting"); - } - - if (d->activeUndoTransaction) { - Base::FlagToggler flag(d->rollback); - Application::TransactionSignaller signaller(true,true); - - // applying the so far made changes - d->activeUndoTransaction->apply(*this,false); - - // destroy the undo - mUndoMap.erase(d->activeUndoTransaction->getID()); - delete d->activeUndoTransaction; - d->activeUndoTransaction = nullptr; - signalAbortTransaction(*this); - } -} - -bool Document::hasPendingTransaction() const -{ - if (d->activeUndoTransaction) - return true; - else - return false; -} - -int Document::getTransactionID(bool undo, unsigned pos) const { - if(undo) { - if(d->activeUndoTransaction) { - if(pos == 0) - return d->activeUndoTransaction->getID(); - --pos; - } - if(pos>=mUndoTransactions.size()) - return 0; - auto rit = mUndoTransactions.rbegin(); - for(;pos;++rit,--pos) - continue; - return (*rit)->getID(); - } - if(pos>=mRedoTransactions.size()) - return 0; - auto rit = mRedoTransactions.rbegin(); - for(;pos;++rit,--pos); - return (*rit)->getID(); -} - -bool Document::isTransactionEmpty() const -{ - if (d->activeUndoTransaction) { - // Transactions are now only created when there are actual changes. - // Empty transaction is now significant for marking external changes. It - // is used to match ID with transactions in external documents and - // trigger undo/redo there. - - // return d->activeUndoTransaction->isEmpty(); - - return false; - } - - return true; -} - -void Document::clearDocument() -{ - d->activeObject = nullptr; - - if (!d->objectArray.empty()) { - GetApplication().signalDeleteDocument(*this); - d->clearDocument(); - GetApplication().signalNewDocument(*this,false); - } - - Base::FlagToggler<> flag(globalIsRestoring, false); - - setStatus(Document::PartialDoc,false); - - d->clearRecomputeLog(); - d->objectArray.clear(); - d->objectMap.clear(); - d->objectIdMap.clear(); - d->lastObjectId = 0; -} - - -void Document::clearUndos() -{ - if(isPerformingTransaction() || d->committing) { - FC_ERR("Cannot clear undos while transacting"); - return; - } - - if (d->activeUndoTransaction) - _commitTransaction(true); - - mUndoMap.clear(); - - // When cleaning up the undo stack we must delete the transactions from front - // to back because a document object can appear in several transactions but - // once removed from the document the object can never ever appear in any later - // transaction. Since the document object may be also deleted when the transaction - // is deleted we must make sure not access an object once it's destroyed. Thus, we - // go from front to back and not the other way round. - while (!mUndoTransactions.empty()) { - delete mUndoTransactions.front(); - mUndoTransactions.pop_front(); - } - //while (!mUndoTransactions.empty()) { - // delete mUndoTransactions.back(); - // mUndoTransactions.pop_back(); - //} - - _clearRedos(); -} - -int Document::getAvailableUndos(int id) const -{ - if(id) { - auto it = mUndoMap.find(id); - if(it == mUndoMap.end()) - return 0; - int i = 0; - if(d->activeUndoTransaction) { - ++i; - if(d->activeUndoTransaction->getID()==id) - return i; - } - auto rit = mUndoTransactions.rbegin(); - for(;rit!=mUndoTransactions.rend()&&*rit!=it->second;++rit) - ++i; - assert(rit!=mUndoTransactions.rend()); - return i+1; - } - if (d->activeUndoTransaction) - return static_cast(mUndoTransactions.size() + 1); - else - return static_cast(mUndoTransactions.size()); -} - -int Document::getAvailableRedos(int id) const -{ - if(id) { - auto it = mRedoMap.find(id); - if(it == mRedoMap.end()) - return 0; - int i = 0; - for(auto rit=mRedoTransactions.rbegin();*rit!=it->second;++rit) - ++i; - assert(i<(int)mRedoTransactions.size()); - return i+1; - } - return static_cast(mRedoTransactions.size()); -} - -void Document::setUndoMode(int iMode) -{ - if (d->iUndoMode && !iMode) - clearUndos(); - - d->iUndoMode = iMode; -} - -int Document::getUndoMode() const -{ - return d->iUndoMode; -} - -unsigned int Document::getUndoMemSize () const -{ - return d->UndoMemSize; -} - -void Document::setUndoLimit(unsigned int UndoMemSize) -{ - d->UndoMemSize = UndoMemSize; -} - -void Document::setMaxUndoStackSize(unsigned int UndoMaxStackSize) -{ - d->UndoMaxStackSize = UndoMaxStackSize; -} - -unsigned int Document::getMaxUndoStackSize()const -{ - return d->UndoMaxStackSize; -} - -void Document::onBeforeChange(const Property* prop) -{ - if(prop == &Label) - oldLabel = Label.getValue(); - signalBeforeChange(*this, *prop); -} - -void Document::onChanged(const Property* prop) -{ - signalChanged(*this, *prop); - - // the Name property is a label for display purposes - if (prop == &Label) { - Base::FlagToggler<> flag(globalIsRelabeling); - App::GetApplication().signalRelabelDocument(*this); - } else if(prop == &ShowHidden) { - App::GetApplication().signalShowHidden(*this); - } else if (prop == &Uid) { - std::string new_dir = getTransientDirectoryName(this->Uid.getValueStr(),this->FileName.getStrValue()); - std::string old_dir = this->TransientDir.getStrValue(); - Base::FileInfo TransDirNew(new_dir); - Base::FileInfo TransDirOld(old_dir); - // this directory should not exist - if (!TransDirNew.exists()) { - if (TransDirOld.exists()) { - if (!TransDirOld.renameFile(new_dir.c_str())) - Base::Console().Warning("Failed to rename '%s' to '%s'\n", old_dir.c_str(), new_dir.c_str()); - else - this->TransientDir.setValue(new_dir); - } - else { - if (!TransDirNew.createDirectories()) - Base::Console().Warning("Failed to create '%s'\n", new_dir.c_str()); - else - this->TransientDir.setValue(new_dir); - } - } - // when reloading an existing document the transient directory doesn't change - // so we must avoid to generate a new uuid - else if (TransDirNew.filePath() != TransDirOld.filePath()) { - // make sure that the uuid is unique - std::string uuid = this->Uid.getValueStr(); - Base::Uuid id; - Base::Console().Warning("Document with the UUID '%s' already exists, change to '%s'\n", - uuid.c_str(), id.getValue().c_str()); - // recursive call of onChanged() - this->Uid.setValue(id); - } - } -} - -void Document::onBeforeChangeProperty(const TransactionalObject *Who, const Property *What) -{ - if(Who->isDerivedFrom(App::DocumentObject::getClassTypeId())) - signalBeforeChangeObject(*static_cast(Who), *What); - if(!d->rollback && !globalIsRelabeling) { - _checkTransaction(nullptr, What, __LINE__); - if (d->activeUndoTransaction) - d->activeUndoTransaction->addObjectChange(Who, What); - } -} - -void Document::onChangedProperty(const DocumentObject *Who, const Property *What) -{ - signalChangedObject(*Who, *What); -} - -void Document::setTransactionMode(int iMode) -{ - d->iTransactionMode = iMode; -} - -//-------------------------------------------------------------------------- -// constructor -//-------------------------------------------------------------------------- -Document::Document(const char* documentName) - : myName(documentName) -{ - // Remark: In a constructor we should never increment a Python object as we cannot be sure - // if the Python interpreter gets a reference of it. E.g. if we increment but Python don't - // get a reference then the object wouldn't get deleted in the destructor. - // 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. - d = new DocumentP; - d->DocumentPythonObject = Py::Object(new DocumentPy(this), true); - -#ifdef FC_LOGUPDATECHAIN - Console().Log("+App::Document: %p\n", this); -#endif - std::string CreationDateString = Base::TimeInfo::currentDateTimeString(); - std::string Author = App::GetApplication() - .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document") - ->GetASCII("prefAuthor", ""); - std::string AuthorComp = - App::GetApplication() - .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document") - ->GetASCII("prefCompany", ""); - ADD_PROPERTY_TYPE(Label, ("Unnamed"), 0, Prop_None, "The name of the document"); - ADD_PROPERTY_TYPE(FileName, - (""), - 0, - PropertyType(Prop_Transient | Prop_ReadOnly), - "The path to the file where the document is saved to"); - ADD_PROPERTY_TYPE(CreatedBy, (Author.c_str()), 0, Prop_None, "The creator of the document"); - ADD_PROPERTY_TYPE( - CreationDate, (CreationDateString.c_str()), 0, Prop_ReadOnly, "Date of creation"); - ADD_PROPERTY_TYPE(LastModifiedBy, (""), 0, Prop_None, 0); - ADD_PROPERTY_TYPE(LastModifiedDate, ("Unknown"), 0, Prop_ReadOnly, "Date of last modification"); - ADD_PROPERTY_TYPE(Company, - (AuthorComp.c_str()), - 0, - Prop_None, - "Additional tag to save the name of the company"); - ADD_PROPERTY_TYPE(Comment, (""), 0, Prop_None, "Additional tag to save a comment"); - ADD_PROPERTY_TYPE(Meta, (), 0, Prop_None, "Map with additional meta information"); - ADD_PROPERTY_TYPE(Material, (), 0, Prop_None, "Map with material properties"); - // create the uuid for the document - Base::Uuid id; - ADD_PROPERTY_TYPE(Id, (""), 0, Prop_None, "ID of the document"); - ADD_PROPERTY_TYPE(Uid, (id), 0, Prop_ReadOnly, "UUID of the document"); - - // license stuff - auto paramGrp {App::GetApplication().GetParameterGroupByPath( - "User parameter:BaseApp/Preferences/Document")}; - auto index = static_cast(paramGrp->GetInt("prefLicenseType", 0)); - const char* name = ""; - const char* url = ""; - std::string licenseUrl = ""; - if (index >= 0 && index < App::countOfLicenses) { - name = App::licenseItems.at(index).at(App::posnOfFullName); - url = App::licenseItems.at(index).at(App::posnOfUrl); - licenseUrl = (paramGrp->GetASCII("prefLicenseUrl", url)); - } - ADD_PROPERTY_TYPE(License, (name), 0, Prop_None, "License string of the Item"); - ADD_PROPERTY_TYPE( - LicenseURL, (licenseUrl.c_str()), 0, Prop_None, "URL to the license text/contract"); - ADD_PROPERTY_TYPE(ShowHidden, - (false), - 0, - PropertyType(Prop_None), - "Whether to show hidden object items in the tree view"); - - // this creates and sets 'TransientDir' in onChanged() - ADD_PROPERTY_TYPE(TransientDir, - (""), - 0, - PropertyType(Prop_Transient | Prop_ReadOnly), - "Transient directory, where the files live while the document is open"); - ADD_PROPERTY_TYPE( - Tip, (nullptr), 0, PropertyType(Prop_Transient), "Link of the tip object of the document"); - ADD_PROPERTY_TYPE(TipName, - (""), - 0, - PropertyType(Prop_Hidden | Prop_ReadOnly), - "Link of the tip object of the document"); - Uid.touch(); -} - -Document::~Document() -{ -#ifdef FC_LOGUPDATECHAIN - Console().Log("-App::Document: %s %p\n",getName(), this); -#endif - - try { - clearUndos(); - } - catch (const boost::exception&) { - } - -#ifdef FC_LOGUPDATECHAIN - Console().Log("-Delete Features of %s \n",getName()); -#endif - - d->clearDocument(); - - // Remark: The API of Py::Object has been changed to set whether the wrapper owns the passed - // Python object or not. In the constructor we forced the wrapper to own the object so we need - // not to dec'ref the Python object any more. - // But we must still invalidate the Python object because it doesn't need to be - // destructed right now because the interpreter can own several references to it. - Base::PyGILStateLocker lock; - Base::PyObjectBase* doc = static_cast(d->DocumentPythonObject.ptr()); - // Call before decrementing the reference counter, otherwise a heap error can occur - doc->setInvalid(); - - // remove Transient directory - try { - Base::FileInfo TransDir(TransientDir.getValue()); - TransDir.deleteDirectoryRecursive(); - } - catch (const Base::Exception& e) { - std::cerr << "Removing transient directory failed: " << e.what() << std::endl; - } - delete d; -} - -std::string Document::getTransientDirectoryName(const std::string& uuid, const std::string& filename) const -{ - // Create a directory name of the form: {ExeName}_Doc_{UUID}_{HASH}_{PID} - std::stringstream s; - QCryptographicHash hash(QCryptographicHash::Sha1); - hash.addData(filename.c_str(), filename.size()); - s << App::Application::getUserCachePath() << App::Application::getExecutableName() - << "_Doc_" << uuid - << "_" << hash.result().toHex().left(6).constData() - << "_" << QCoreApplication::applicationPid(); - return s.str(); -} - -//-------------------------------------------------------------------------- -// Exported functions -//-------------------------------------------------------------------------- - -void Document::Save (Base::Writer &writer) const -{ - writer.Stream() << "" << endl; - - PropertyContainer::Save(writer); - - // writing the features types - writeObjects(d->objectArray, writer); - writer.Stream() << "" << endl; -} - -void Document::Restore(Base::XMLReader &reader) -{ - int i,Cnt; - d->touchedObjs.clear(); - setStatus(Document::PartialDoc,false); - - reader.readElement("Document"); - long scheme = reader.getAttributeAsInteger("SchemaVersion"); - reader.DocumentSchema = scheme; - if (reader.hasAttribute("ProgramVersion")) { - reader.ProgramVersion = reader.getAttribute("ProgramVersion"); - } else { - reader.ProgramVersion = "pre-0.14"; - } - if (reader.hasAttribute("FileVersion")) { - reader.FileVersion = reader.getAttributeAsUnsigned("FileVersion"); - } else { - reader.FileVersion = 0; - } - - // When this document was created the FileName and Label properties - // were set to the absolute path or file name, respectively. To save - // the document to the file it was loaded from or to show the file name - // in the tree view we must restore them after loading the file because - // they will be overridden. - // Note: This does not affect the internal name of the document in any way - // that is kept in Application. - std::string FilePath = FileName.getValue(); - std::string DocLabel = Label.getValue(); - - // read the Document Properties, when reading in Uid the transient directory gets renamed automatically - PropertyContainer::Restore(reader); - - // We must restore the correct 'FileName' property again because the stored - // value could be invalid. - FileName.setValue(FilePath.c_str()); - Label.setValue(DocLabel.c_str()); - - // SchemeVersion "2" - if ( scheme == 2 ) { - // read the feature types - reader.readElement("Features"); - Cnt = reader.getAttributeAsInteger("Count"); - for (i=0 ;isetStatus(ObjectStatus::Restore, true); - pObj->Restore(reader); - pObj->setStatus(ObjectStatus::Restore, false); - } - reader.readEndElement("Feature"); - } - reader.readEndElement("FeatureData"); - } // SchemeVersion "3" or higher - else if ( scheme >= 3 ) { - // read the feature types - readObjects(reader); - - // tip object handling. First the whole document has to be read, then we - // can restore the Tip link out of the TipName Property: - Tip.setValue(getObject(TipName.getValue())); - } - - reader.readEndElement("Document"); -} - -struct DocExportStatus { - Document::ExportStatus status; - std::set objs; -}; - -static DocExportStatus _ExportStatus; - -// Exception-safe exporting status setter -class DocumentExporting { -public: - explicit DocumentExporting(const std::vector &objs) { - _ExportStatus.status = Document::Exporting; - _ExportStatus.objs.insert(objs.begin(),objs.end()); - } - - ~DocumentExporting() { - _ExportStatus.status = Document::NotExporting; - _ExportStatus.objs.clear(); - } -}; - -// The current implementation choose to use a static variable for exporting -// status because we can be exporting multiple objects from multiple documents -// at the same time. I see no benefits in distinguish which documents are -// exporting, so just use a static variable for global status. But the -// implementation can easily be changed here if necessary. -Document::ExportStatus Document::isExporting(const App::DocumentObject *obj) const { - if(_ExportStatus.status!=Document::NotExporting && - (!obj || _ExportStatus.objs.find(obj)!=_ExportStatus.objs.end())) - return _ExportStatus.status; - return Document::NotExporting; -} - -void Document::exportObjects(const std::vector& obj, std::ostream& out) { - - DocumentExporting exporting(obj); - - if(FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { - for(auto o : obj) { - if(o && o->getNameInDocument()) { - FC_LOG("exporting " << o->getFullName()); - if (!o->getPropertyByName("_ObjectUUID")) { - auto prop = static_cast(o->addDynamicProperty( - "App::PropertyUUID", "_ObjectUUID", nullptr, nullptr, - Prop_Output | Prop_Hidden)); - prop->setValue(Base::Uuid::createUuid()); - } - } - } - } - - Base::ZipWriter writer(out); - writer.putNextEntry("Document.xml"); - writer.Stream() << "" << endl; - writer.Stream() << "" << endl; - // Add this block to have the same layout as for normal documents - writer.Stream() << "" << endl; - writer.Stream() << "" << endl; - - // writing the object types - writeObjects(obj, writer); - writer.Stream() << "" << endl; - - // Hook for others to add further data. - signalExportObjects(obj, writer); - - // write additional files - writer.writeFiles(); -} - -#define FC_ATTR_DEPENDENCIES "Dependencies" -#define FC_ELEMENT_OBJECT_DEPS "ObjectDeps" -#define FC_ATTR_DEP_COUNT "Count" -#define FC_ATTR_DEP_OBJ_NAME "Name" -#define FC_ATTR_DEP_ALLOW_PARTIAL "AllowPartial" -#define FC_ELEMENT_OBJECT_DEP "Dep" - -void Document::writeObjects(const std::vector& obj, - Base::Writer &writer) const -{ - // writing the features types - writer.incInd(); // indentation for 'Objects count' - writer.Stream() << writer.ind() << "" << endl; - - writer.incInd(); // indentation for 'Object type' - - if(!isExporting(nullptr)) { - for(auto o : obj) { - const auto &outList = o->getOutList(DocumentObject::OutListNoHidden - | DocumentObject::OutListNoXLinked); - writer.Stream() << writer.ind() - << "<" FC_ELEMENT_OBJECT_DEPS " " FC_ATTR_DEP_OBJ_NAME "=\"" - << o->getNameInDocument() << "\" " FC_ATTR_DEP_COUNT "=\"" << outList.size(); - if(outList.empty()) { - writer.Stream() << "\"/>" << endl; - continue; - } - int partial = o->canLoadPartial(); - if(partial>0) - writer.Stream() << "\" " FC_ATTR_DEP_ALLOW_PARTIAL << "=\"" << partial; - writer.Stream() << "\">" << endl; - writer.incInd(); - for(auto dep : outList) { - auto name = dep?dep->getNameInDocument():""; - writer.Stream() << writer.ind() << "<" FC_ELEMENT_OBJECT_DEP " " - FC_ATTR_DEP_OBJ_NAME "=\"" << (name?name:"") << "\"/>" << endl; - } - writer.decInd(); - writer.Stream() << writer.ind() << "" << endl; - } - } - - std::vector::const_iterator it; - for (it = obj.begin(); it != obj.end(); ++it) { - writer.Stream() << writer.ind() << "getTypeId().getName() << "\" " - << "name=\"" << (*it)->getExportName() << "\" " - << "id=\"" << (*it)->getID() << "\" "; - - // Only write out custom view provider types - std::string viewType = (*it)->getViewProviderNameStored(); - if (viewType != (*it)->getViewProviderName()) - writer.Stream() << "ViewType=\"" << viewType << "\" "; - - // See DocumentObjectPy::getState - if ((*it)->testStatus(ObjectStatus::Touch)) - writer.Stream() << "Touched=\"1\" "; - if ((*it)->testStatus(ObjectStatus::Error)) { - writer.Stream() << "Invalid=\"1\" "; - auto desc = getErrorDescription(*it); - if(desc) - writer.Stream() << "Error=\"" << Property::encodeAttribute(desc) << "\" "; - } - writer.Stream() << "/>" << endl; - } - - writer.decInd(); // indentation for 'Object type' - writer.Stream() << writer.ind() << "" << endl; - - // writing the features itself - writer.Stream() << writer.ind() << "" << endl; - - writer.incInd(); // indentation for 'Object name' - for (it = obj.begin(); it != obj.end(); ++it) { - writer.Stream() << writer.ind() << "getExportName() << "\""; - if((*it)->hasExtensions()) - writer.Stream() << " Extensions=\"True\""; - - writer.Stream() << ">" << endl; - (*it)->Save(writer); - writer.Stream() << writer.ind() << "" << endl; - } - - writer.decInd(); // indentation for 'Object name' - writer.Stream() << writer.ind() << "" << endl; - writer.decInd(); // indentation for 'Objects count' -} - -struct DepInfo { - std::unordered_set deps; - int canLoadPartial = 0; -}; - -static void _loadDeps(const std::string &name, - std::unordered_map &objs, - const std::unordered_map &deps) -{ - auto it = deps.find(name); - if(it == deps.end()) { - objs.emplace(name,true); - return; - } - if(it->second.canLoadPartial) { - if(it->second.canLoadPartial == 1) { - // canLoadPartial==1 means all its children will be created but not - // restored, i.e. exists as if newly created object, and therefore no - // need to load dependency of the children - for(auto &dep : it->second.deps) - objs.emplace(dep,false); - objs.emplace(name,true); - }else - objs.emplace(name,false); - return; - } - objs[name] = true; - // If cannot load partial, then recurse to load all children dependency - for(auto &dep : it->second.deps) { - auto it = objs.find(dep); - if(it!=objs.end() && it->second) - continue; - _loadDeps(dep,objs,deps); - } -} - -std::vector -Document::readObjects(Base::XMLReader& reader) -{ - d->touchedObjs.clear(); - bool keepDigits = testStatus(Document::KeepTrailingDigits); - setStatus(Document::KeepTrailingDigits, !reader.doNameMapping()); - std::vector objs; - - - // read the object types - reader.readElement("Objects"); - int Cnt = reader.getAttributeAsInteger("Count"); - - if(!reader.hasAttribute(FC_ATTR_DEPENDENCIES)) - d->partialLoadObjects.clear(); - else if(!d->partialLoadObjects.empty()) { - std::unordered_map deps; - for (int i=0 ;i objs; - objs.reserve(d->partialLoadObjects.size()); - for(auto &v : d->partialLoadObjects) - objs.emplace_back(v.first.c_str()); - for(auto &name : objs) - _loadDeps(name,d->partialLoadObjects,deps); - if(Cnt > (int)d->partialLoadObjects.size()) - setStatus(Document::PartialDoc,true); - else { - for(auto &v : d->partialLoadObjects) { - if(!v.second) { - setStatus(Document::PartialDoc,true); - break; - } - } - if(!testStatus(Document::PartialDoc)) - d->partialLoadObjects.clear(); - } - } - - long lastId = 0; - for (int i=0 ;ipartialLoadObjects.empty()) { - auto it = d->partialLoadObjects.find(name); - if(it == d->partialLoadObjects.end()) - continue; - partial = !it->second; - } - - if(!testStatus(Status::Importing) && reader.hasAttribute("id")) { - // if not importing, then temporary reset lastObjectId and make the - // following addObject() generate the correct id for this object. - d->lastObjectId = reader.getAttributeAsInteger("id")-1; - } - - // To prevent duplicate name when export/import of objects from - // external documents, we append those external object name with - // @. Before importing (here means we are called by - // importObjects), we shall strip the postfix. What the caller - // (MergeDocument) sees is still the unstripped name mapped to a new - // internal name, and the rest of the link properties will be able to - // correctly unmap the names. - auto pos = name.find('@'); - std::string _obj_name; - const char *obj_name; - if(pos!=std::string::npos) { - _obj_name = name.substr(0,pos); - obj_name = _obj_name.c_str(); - }else - obj_name = name.c_str(); - - try { - // Use name from XML as is and do NOT remove trailing digits because - // otherwise we may cause a dependency to itself - // Example: Object 'Cut001' references object 'Cut' and removing the - // digits we make an object 'Cut' referencing itself. - App::DocumentObject* obj = addObject(type.c_str(), obj_name, /*isNew=*/ false, viewType.c_str(), partial); - if (obj) { - if(lastId < obj->_Id) - lastId = obj->_Id; - objs.push_back(obj); - // use this name for the later access because an object with - // the given name may already exist - reader.addName(name.c_str(), obj->getNameInDocument()); - - // restore touch/error status flags - if (reader.hasAttribute("Touched")) { - if(reader.getAttributeAsInteger("Touched") != 0) - d->touchedObjs.insert(obj); - } - if (reader.hasAttribute("Invalid")) { - obj->setStatus(ObjectStatus::Error, reader.getAttributeAsInteger("Invalid") != 0); - if(obj->isError() && reader.hasAttribute("Error")) - d->addRecomputeLog(reader.getAttribute("Error"),obj); - } - } - } - catch (const Base::Exception& e) { - Base::Console().Error("Cannot create object '%s': (%s)\n", name.c_str(), e.what()); - } - } - if(!testStatus(Status::Importing)) - d->lastObjectId = lastId; - - reader.readEndElement("Objects"); - setStatus(Document::KeepTrailingDigits, keepDigits); - - // read the features itself - reader.clearPartialRestoreDocumentObject(); - reader.readElement("ObjectData"); - Cnt = reader.getAttributeAsInteger("Count"); - for (int i=0 ;itestStatus(App::PartialObject)) { // check if this feature has been registered - pObj->setStatus(ObjectStatus::Restore, true); - try { - FC_TRACE("restoring " << pObj->getFullName()); - pObj->Restore(reader); - } - // Try to continue only for certain exception types if not handled - // by the feature type. For all other exception types abort the process. - catch (const Base::UnicodeError &e) { - e.ReportException(); - } - catch (const Base::ValueError &e) { - e.ReportException(); - } - catch (const Base::IndexError &e) { - e.ReportException(); - } - catch (const Base::RuntimeError &e) { - e.ReportException(); - } - catch (const Base::XMLAttributeError &e) { - e.ReportException(); - } - - pObj->setStatus(ObjectStatus::Restore, false); - - if (reader.testStatus(Base::XMLReader::ReaderStatus::PartialRestoreInDocumentObject)) { - Base::Console().Error("Object \"%s\" was subject to a partial restore. As a result geometry may have changed or be incomplete.\n",name.c_str()); - reader.clearPartialRestoreDocumentObject(); - } - } - reader.readEndElement("Object"); - } - reader.readEndElement("ObjectData"); - - return objs; -} - -void Document::addRecomputeObject(DocumentObject *obj) { - if(testStatus(Status::Restoring) && obj) { - d->touchedObjs.insert(obj); - obj->touch(); - } -} - -std::vector -Document::importObjects(Base::XMLReader& reader) -{ - Base::FlagToggler<> flag(globalIsRestoring, false); - Base::ObjectStatusLocker restoreBit(Status::Restoring, this); - Base::ObjectStatusLocker restoreBit2(Status::Importing, this); - ExpressionParser::ExpressionImporter expImporter(reader); - reader.readElement("Document"); - long scheme = reader.getAttributeAsInteger("SchemaVersion"); - reader.DocumentSchema = scheme; - if (reader.hasAttribute("ProgramVersion")) { - reader.ProgramVersion = reader.getAttribute("ProgramVersion"); - } else { - reader.ProgramVersion = "pre-0.14"; - } - if (reader.hasAttribute("FileVersion")) { - reader.FileVersion = reader.getAttributeAsUnsigned("FileVersion"); - } else { - reader.FileVersion = 0; - } - - std::vector objs = readObjects(reader); - for(auto o : objs) { - if(o && o->getNameInDocument()) { - o->setStatus(App::ObjImporting,true); - FC_LOG("importing " << o->getFullName()); - if (auto propUUID = Base::freecad_dynamic_cast( - o->getPropertyByName("_ObjectUUID"))) - { - auto propSource = Base::freecad_dynamic_cast( - o->getPropertyByName("_SourceUUID")); - if (!propSource) - propSource = static_cast(o->addDynamicProperty( - "App::PropertyUUID", "_SourceUUID", nullptr, nullptr, - Prop_Output | Prop_Hidden)); - if (propSource) - propSource->setValue(propUUID->getValue()); - propUUID->setValue(Base::Uuid::createUuid()); - } - } - } - - reader.readEndElement("Document"); - - signalImportObjects(objs, reader); - afterRestore(objs,true); - - signalFinishImportObjects(objs); - - for(auto o : objs) { - if(o && o->getNameInDocument()) - o->setStatus(App::ObjImporting,false); - } - - return objs; -} - -unsigned int Document::getMemSize () const -{ - unsigned int size = 0; - - // size of the DocObjects in the document - std::vector::const_iterator it; - for (it = d->objectArray.begin(); it != d->objectArray.end(); ++it) - size += (*it)->getMemSize(); - - // size of the document properties... - size += PropertyContainer::getMemSize(); - - // Undo Redo size - size += getUndoMemSize(); - - return size; -} - -static std::string checkFileName(const char *file) { - std::string fn(file); - - // Append extension if missing. This option is added for security reason, so - // that the user won't accidentally overwrite other file that may be critical. - if(App::GetApplication().GetParameterGroupByPath - ("User parameter:BaseApp/Preferences/Document")->GetBool("CheckExtension",true)) - { - const char *ext = strrchr(file,'.'); - if(!ext || !boost::iequals(ext+1,"fcstd")) { - if(ext && ext[1] == 0) - fn += "FCStd"; - else - fn += ".FCStd"; - } - } - return fn; -} - -bool Document::saveAs(const char* _file) -{ - std::string file = checkFileName(_file); - Base::FileInfo fi(file.c_str()); - if (this->FileName.getStrValue() != file) { - this->FileName.setValue(file); - this->Label.setValue(fi.fileNamePure()); - this->Uid.touch(); // this forces a rename of the transient directory - } - - return save(); -} - -bool Document::saveCopy(const char* _file) const -{ - std::string file = checkFileName(_file); - if (this->FileName.getStrValue() != file) { - bool result = saveToFile(file.c_str()); - return result; - } - return false; -} - -// Save the document under the name it has been opened -bool Document::save () -{ - if(testStatus(Document::PartialDoc)) { - FC_ERR("Partial loaded document '" << Label.getValue() << "' cannot be saved"); - // TODO We don't make this a fatal error and return 'true' to make it possible to - // save other documents that depends on this partial opened document. We need better - // handling to avoid touching partial documents. - return true; - } - - if (*(FileName.getValue()) != '\0') { - // Save the name of the tip object in order to handle in Restore() - if (Tip.getValue()) { - TipName.setValue(Tip.getValue()->getNameInDocument()); - } - - std::string LastModifiedDateString = Base::TimeInfo::currentDateTimeString(); - LastModifiedDate.setValue(LastModifiedDateString.c_str()); - // set author if needed - bool saveAuthor = App::GetApplication().GetParameterGroupByPath - ("User parameter:BaseApp/Preferences/Document")->GetBool("prefSetAuthorOnSave",false); - if (saveAuthor) { - std::string Author = App::GetApplication().GetParameterGroupByPath - ("User parameter:BaseApp/Preferences/Document")->GetASCII("prefAuthor",""); - LastModifiedBy.setValue(Author.c_str()); - } - - return saveToFile(FileName.getValue()); - } - - return false; -} - -namespace App { -// Helper class to handle different backup policies -class BackupPolicy { -public: - enum Policy { - Standard, - TimeStamp - }; - BackupPolicy() { - policy = Standard; - numberOfFiles = 1; - useFCBakExtension = true; - saveBackupDateFormat = "%Y%m%d-%H%M%S"; - } - ~BackupPolicy() = default; - void setPolicy(Policy p) { - policy = p; - } - void setNumberOfFiles(int count) { - numberOfFiles = count; - } - void useBackupExtension(bool on) { - useFCBakExtension = on; - } - void setDateFormat(const std::string& fmt) { - saveBackupDateFormat = fmt; - } - void apply(const std::string& sourcename, const std::string& targetname) { - switch (policy) { - case Standard: - applyStandard(sourcename, targetname); - break; - case TimeStamp: - applyTimeStamp(sourcename, targetname); - break; - } - } - -private: - void applyStandard(const std::string& sourcename, const std::string& targetname) { - // if saving the project data succeeded rename to the actual file name - Base::FileInfo fi(targetname); - if (fi.exists()) { - if (numberOfFiles > 0) { - int nSuff = 0; - std::string fn = fi.fileName(); - Base::FileInfo di(fi.dirPath()); - std::vector backup; - std::vector files = di.getDirectoryContent(); - for (std::vector::iterator it = files.begin(); it != files.end(); ++it) { - std::string file = it->fileName(); - if (file.substr(0,fn.length()) == fn) { - // starts with the same file name - std::string suf(file.substr(fn.length())); - if (!suf.empty()) { - std::string::size_type nPos = suf.find_first_not_of("0123456789"); - if (nPos==std::string::npos) { - // store all backup files - backup.push_back(*it); - nSuff = std::max(nSuff, std::atol(suf.c_str())); - } - } - } - } - - if (!backup.empty() && (int)backup.size() >= numberOfFiles) { - // delete the oldest backup file we found - Base::FileInfo del = backup.front(); - for (std::vector::iterator it = backup.begin(); it != backup.end(); ++it) { - if (it->lastModified() < del.lastModified()) - del = *it; - } - - del.deleteFile(); - fn = del.filePath(); - } - else { - // create a new backup file - std::stringstream str; - str << fi.filePath() << (nSuff + 1); - fn = str.str(); - } - - if (!fi.renameFile(fn.c_str())) - Base::Console().Warning("Cannot rename project file to backup file\n"); - } - else { - fi.deleteFile(); - } - } - - Base::FileInfo tmp(sourcename); - if (!tmp.renameFile(targetname.c_str())) { - throw Base::FileException( - "Cannot rename tmp save file to project file", targetname); - } - } - void applyTimeStamp(const std::string& sourcename, const std::string& targetname) { - Base::FileInfo fi(targetname); - - std::string fn = sourcename; - std::string ext = fi.extension(); - std::string bn; // full path with no extension but with "." - std::string pbn; // base name of the project + "." - if (!ext.empty()) { - bn = fi.filePath().substr(0, fi.filePath().length() - ext.length()); - pbn = fi.fileName().substr(0, fi.fileName().length() - ext.length()); - } - else { - bn = fi.filePath() + "."; - pbn = fi.fileName() + "."; - } - - bool backupManagementError = false; // Note error and report at the end - if (fi.exists()) { - if (numberOfFiles > 0) { - // replace . by - in format to avoid . between base name and extension - boost::replace_all(saveBackupDateFormat, ".", "-"); - { - // Remove all extra backups - std::string fn = fi.fileName(); - Base::FileInfo di(fi.dirPath()); - std::vector backup; - std::vector files = di.getDirectoryContent(); - for (std::vector::iterator it = files.begin(); it != files.end(); ++it) { - if (it->isFile()) { - std::string file = it->fileName(); - std::string fext = it->extension(); - std::string fextUp = fext; - std::transform(fextUp.begin(), fextUp.end(), fextUp.begin(),(int (*)(int))toupper); - // re-enforcing identification of the backup file - - - // old case : the name starts with the full name of the project and follows with numbers - if ((startsWith(file, fn) && - (file.length() > fn.length()) && - checkDigits(file.substr(fn.length()))) || - // .FCBak case : The bame starts with the base name of the project + "." - // + complement with no "." + ".FCBak" - ((fextUp == "FCBAK") && startsWith(file, pbn) && - (checkValidComplement(file, pbn, fext)))) { - backup.push_back(*it); - } - } - } - - if (!backup.empty() && (int)backup.size() >= numberOfFiles) { - std::sort (backup.begin(), backup.end(), fileComparisonByDate); - // delete the oldest backup file we found - // Base::FileInfo del = backup.front(); - int nb = 0; - for (std::vector::iterator it = backup.begin(); it != backup.end(); ++it) { - nb++; - if (nb >= numberOfFiles) { - try { - if (!it->deleteFile()) { - backupManagementError = true; - Base::Console().Warning("Cannot remove backup file : %s\n", it->fileName().c_str()); - } - } - catch (...) { - backupManagementError = true; - Base::Console().Warning("Cannot remove backup file : %s\n", it->fileName().c_str()); - } - } - } - - } - } //end remove backup - - // create a new backup file - { - int ext = 1; - if (useFCBakExtension) { - std::stringstream str; - Base::TimeInfo ti = fi.lastModified(); - time_t s =ti.getSeconds(); - struct tm * timeinfo = localtime(& s); - char buffer[100]; - - strftime(buffer,sizeof(buffer),saveBackupDateFormat.c_str(),timeinfo); - str << bn << buffer ; - - fn = str.str(); - bool done = false; - - if ((fn.empty()) || (fn[fn.length()-1] == ' ') || (fn[fn.length()-1] == '-')) { - if (fn[fn.length()-1] == ' ') { - fn = fn.substr(0,fn.length()-1); - } - } - else { - if (!renameFileNoErase(fi, fn+".FCBak")) { - fn = fn + "-"; - } - else { - done = true; - } - } - - if (!done) { - while (ext < numberOfFiles + 10) { - if (renameFileNoErase(fi, fn+std::to_string(ext)+".FCBak")) - break; - ext++; - } - } - } - else { - // changed but simpler and solves also the delay sometimes introduced by google drive - while (ext < numberOfFiles + 10) { - // linux just replace the file if exists, and then the existence is to be tested before rename - if (renameFileNoErase(fi, fi.filePath()+std::to_string(ext))) - break; - ext++; - } - } - - if (ext >= numberOfFiles + 10) { - Base::Console().Error("File not saved: Cannot rename project file to backup file\n"); - //throw Base::FileException("File not saved: Cannot rename project file to backup file", fi); - } - } - } - else { - try { - fi.deleteFile(); - } - catch (...) { - Base::Console().Warning("Cannot remove backup file: %s\n", fi.fileName().c_str()); - backupManagementError = true; - } - } - } - - Base::FileInfo tmp(sourcename); - if (!tmp.renameFile(targetname.c_str())) { - throw Base::FileException( - "Save interrupted: Cannot rename temporary file to project file", tmp); - } - - if (backupManagementError) { - throw Base::FileException("Warning: Save complete, but error while managing backup history.", fi); - } - } - static bool fileComparisonByDate(const Base::FileInfo& i, - const Base::FileInfo& j) { - return (i.lastModified()>j.lastModified()); - } - bool startsWith(const std::string& st1, - const std::string& st2) const { - return st1.substr(0,st2.length()) == st2; - } - bool checkValidString (const std::string& cmpl, const boost::regex& e) const { - boost::smatch what; - bool res = boost::regex_search (cmpl,what,e); - return res; - } - bool checkValidComplement(const std::string& file, const std::string& pbn, const std::string& ext) const { - std::string cmpl = file.substr(pbn.length(),file.length()- pbn.length() - ext.length()-1); - boost::regex e (R"(^[^.]*$)"); - return checkValidString(cmpl,e); - } - bool checkDigits (const std::string& cmpl) const { - boost::regex e (R"(^[0-9]*$)"); - return checkValidString(cmpl,e); - } - bool renameFileNoErase(Base::FileInfo fi, const std::string& newName) { - // linux just replaces the file if it exists, so the existence is to be tested before rename - Base::FileInfo nf(newName); - if (!nf.exists()) { - return fi.renameFile(newName.c_str()); - } - return false; - } - -private: - Policy policy; - int numberOfFiles; - bool useFCBakExtension; - std::string saveBackupDateFormat; -}; -} - -bool Document::saveToFile(const char* filename) const -{ - signalStartSave(*this, filename); - - auto hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document"); - int compression = hGrp->GetInt("CompressionLevel",3); - compression = Base::clamp(compression, Z_NO_COMPRESSION, Z_BEST_COMPRESSION); - - bool policy = App::GetApplication().GetParameterGroupByPath - ("User parameter:BaseApp/Preferences/Document")->GetBool("BackupPolicy",true); - - auto canonical_path = [](const char* filename) { - try { -#ifdef FC_OS_WIN32 - QString utf8Name = QString::fromUtf8(filename); - auto realpath = fs::weakly_canonical(fs::absolute(fs::path(utf8Name.toStdWString()))); - std::string nativePath = QString::fromStdWString(realpath.native()).toStdString(); -#else - auto realpath = fs::weakly_canonical(fs::absolute(fs::path(filename))); - std::string nativePath = realpath.native(); -#endif - // In case some folders in the path do not exist - auto parentPath = realpath.parent_path(); - fs::create_directories(parentPath); - - return nativePath; - } - catch (const std::exception&) { -#ifdef FC_OS_WIN32 - QString utf8Name = QString::fromUtf8(filename); - auto parentPath = fs::absolute(fs::path(utf8Name.toStdWString())).parent_path(); -#else - auto parentPath = fs::absolute(fs::path(filename)).parent_path(); -#endif - fs::create_directories(parentPath); - - return std::string(filename); - } - }; - - //realpath is canonical filename i.e. without symlink - std::string nativePath = canonical_path(filename); - - // make a tmp. file where to save the project data first and then rename to - // the actual file name. This may be useful if overwriting an existing file - // fails so that the data of the work up to now isn't lost. - std::string uuid = Base::Uuid::createUuid(); - std::string fn = nativePath; - if (policy) { - fn += "."; - fn += uuid; - } - Base::FileInfo tmp(fn); - - - // open extra scope to close ZipWriter properly - { - Base::ofstream file(tmp, std::ios::out | std::ios::binary); - Base::ZipWriter writer(file); - if (!file.is_open()) { - throw Base::FileException("Failed to open file", tmp); - } - - writer.setComment("FreeCAD Document"); - writer.setLevel(compression); - writer.putNextEntry("Document.xml"); - - if (hGrp->GetBool("SaveBinaryBrep", false)) - writer.setMode("BinaryBrep"); - - writer.Stream() << "" << endl - << "" << endl; - Document::Save(writer); - - // Special handling for Gui document. - signalSaveDocument(writer); - - // write additional files - writer.writeFiles(); - - if (writer.hasErrors()) { - throw Base::FileException("Failed to write all data to file", tmp); - } - - GetApplication().signalSaveDocument(*this); - } - - if (policy) { - // if saving the project data succeeded rename to the actual file name - int count_bak = App::GetApplication().GetParameterGroupByPath - ("User parameter:BaseApp/Preferences/Document")->GetInt("CountBackupFiles",1); - bool backup = App::GetApplication().GetParameterGroupByPath - ("User parameter:BaseApp/Preferences/Document")->GetBool("CreateBackupFiles",true); - if (!backup) { - count_bak = -1; - } - bool useFCBakExtension = App::GetApplication().GetParameterGroupByPath - ("User parameter:BaseApp/Preferences/Document")->GetBool("UseFCBakExtension",true); - std::string saveBackupDateFormat = App::GetApplication().GetParameterGroupByPath - ("User parameter:BaseApp/Preferences/Document")->GetASCII("SaveBackupDateFormat","%Y%m%d-%H%M%S"); - - BackupPolicy policy; - if (useFCBakExtension) { - policy.setPolicy(BackupPolicy::TimeStamp); - policy.useBackupExtension(useFCBakExtension); - policy.setDateFormat(saveBackupDateFormat); - } - else { - policy.setPolicy(BackupPolicy::Standard); - } - policy.setNumberOfFiles(count_bak); - policy.apply(fn, nativePath); - } - - signalFinishSave(*this, filename); - - return true; -} - -bool Document::isAnyRestoring() { - return globalIsRestoring; -} - -// Open the document -void Document::restore (const char *filename, - bool delaySignal, const std::vector &objNames) -{ - clearUndos(); - d->activeObject = nullptr; - - bool signal = false; - Document *activeDoc = GetApplication().getActiveDocument(); - if (!d->objectArray.empty()) { - signal = true; - GetApplication().signalDeleteDocument(*this); - d->clearDocument(); - } - - Base::FlagToggler<> flag(globalIsRestoring, false); - - setStatus(Document::PartialDoc,false); - - d->clearRecomputeLog(); - d->objectArray.clear(); - d->objectMap.clear(); - d->objectIdMap.clear(); - d->lastObjectId = 0; - - if(signal) { - GetApplication().signalNewDocument(*this,true); - if(activeDoc == this) - GetApplication().setActiveDocument(this); - } - - if(!filename) - filename = FileName.getValue(); - Base::FileInfo fi(filename); - Base::ifstream file(fi, std::ios::in | std::ios::binary); - std::streambuf* buf = file.rdbuf(); - std::streamoff size = buf->pubseekoff(0, std::ios::end, std::ios::in); - buf->pubseekoff(0, std::ios::beg, std::ios::in); - if (size < 22) // an empty zip archive has 22 bytes - throw Base::FileException("Invalid project file",filename); - - zipios::ZipInputStream zipstream(file); - Base::XMLReader reader(filename, zipstream); - - if (!reader.isValid()) - throw Base::FileException("Error reading compression file",filename); - - GetApplication().signalStartRestoreDocument(*this); - setStatus(Document::Restoring, true); - - d->partialLoadObjects.clear(); - for(auto &name : objNames) - d->partialLoadObjects.emplace(name,true); - try { - Document::Restore(reader); - } catch (const Base::Exception& e) { - Base::Console().Error("Invalid Document.xml: %s\n", e.what()); - setStatus(Document::RestoreError, true); - } - - d->partialLoadObjects.clear(); - d->programVersion = reader.ProgramVersion; - - // Special handling for Gui document, the view representations must already - // exist, what is done in Restore(). - // Note: This file doesn't need to be available if the document has been created - // without GUI. But if available then follow after all data files of the App document. - signalRestoreDocument(reader); - reader.readFiles(zipstream); - - if (reader.testStatus(Base::XMLReader::ReaderStatus::PartialRestore)) { - setStatus(Document::PartialRestore, true); - Base::Console().Error("There were errors while loading the file. Some data might have been modified or not recovered at all. Look above for more specific information about the objects involved.\n"); - } - - if(!delaySignal) - afterRestore(true); -} - -bool Document::afterRestore(bool checkPartial) { - Base::FlagToggler<> flag(globalIsRestoring, false); - if(!afterRestore(d->objectArray,checkPartial)) { - FC_WARN("Reload partial document " << getName()); - GetApplication().signalPendingReloadDocument(*this); - return false; - } - GetApplication().signalFinishRestoreDocument(*this); - setStatus(Document::Restoring, false); - return true; -} - -bool Document::afterRestore(const std::vector &objArray, bool checkPartial) -{ - checkPartial = checkPartial && testStatus(Document::PartialDoc); - if(checkPartial && !d->touchedObjs.empty()) - return false; - - // some link type property cannot restore link information until other - // objects has been restored. For example, PropertyExpressionEngine and - // PropertySheet with expression containing label reference. So we add the - // Property::afterRestore() interface to let them sort it out. Note, this - // API is not called in object dedpenency order, because the order - // information is not ready yet. - std::map > propMap; - for(auto obj : objArray) { - auto &props = propMap[obj]; - obj->getPropertyList(props); - for(auto prop : props) { - try { - prop->afterRestore(); - } catch (const Base::Exception& e) { - FC_ERR("Failed to restore " << obj->getFullName() - << '.' << prop->getName() << ": " << e.what()); - } - } - } - - if(checkPartial && !d->touchedObjs.empty()) { - // partial document touched, signal full reload - return false; - } - - std::set objSet(objArray.begin(),objArray.end()); - auto objs = getDependencyList(objArray.empty()?d->objectArray:objArray,DepSort); - for (auto obj : objs) { - if(objSet.find(obj)==objSet.end()) - continue; - try { - for(auto prop : propMap[obj]) - prop->onContainerRestored(); - bool touched = false; - auto returnCode = obj->ExpressionEngine.execute( - PropertyExpressionEngine::ExecuteOnRestore,&touched); - if(returnCode!=DocumentObject::StdReturn) { - FC_ERR("Expression engine failed to restore " << obj->getFullName() << ": " << returnCode->Why); - d->addRecomputeLog(returnCode); - } - obj->onDocumentRestored(); - if(touched) - d->touchedObjs.insert(obj); - } - catch (const Base::Exception& e) { - d->addRecomputeLog(e.what(),obj); - FC_ERR("Failed to restore " << obj->getFullName() << ": " << e.what()); - } - catch (std::exception &e) { - d->addRecomputeLog(e.what(),obj); - FC_ERR("Failed to restore " << obj->getFullName() << ": " << e.what()); - } - catch (...) { - d->addRecomputeLog("Unknown exception on restore",obj); - FC_ERR("Failed to restore " << obj->getFullName() << ": " << "unknown exception"); - } - if(obj->isValid()) { - auto &props = propMap[obj]; - props.clear(); - // refresh properties in case the object changes its property list - obj->getPropertyList(props); - for(auto prop : props) { - auto link = Base::freecad_dynamic_cast(prop); - int res; - std::string errMsg; - if(link && (res=link->checkRestore(&errMsg))) { - d->touchedObjs.insert(obj); - if(res==1 || checkPartial) { - FC_WARN(obj->getFullName() << '.' << prop->getName() << ": " << errMsg); - setStatus(Document::LinkStampChanged, true); - if(checkPartial) - return false; - } else { - FC_ERR(obj->getFullName() << '.' << prop->getName() << ": " << errMsg); - d->addRecomputeLog(errMsg,obj); - setStatus(Document::PartialRestore, true); - } - } - } - } - - if(checkPartial && !d->touchedObjs.empty()) { - // partial document touched, signal full reload - return false; - } else if(!d->touchedObjs.count(obj)) - obj->purgeTouched(); - - signalFinishRestoreObject(*obj); - } - - d->touchedObjs.clear(); - return true; -} - -bool Document::isSaved() const -{ - std::string name = FileName.getValue(); - return !name.empty(); -} - -/** Label is the visible name of a document shown e.g. in the windows title - * or in the tree view. The label almost (but not always e.g. if you manually change it) - * matches with the file name where the document is stored to. - * In contrast to Label the method getName() returns the internal name of the document that only - * matches with Label when loading or creating a document because then both are set to the same value. - * Since the internal name cannot be changed during runtime it must differ from the Label after saving - * the document the first time or saving it under a new file name. - * @ note More than one document can have the same label name. - * @ note The internal is always guaranteed to be unique because @ref Application::newDocument() checks - * for a document with the same name and makes it unique if needed. Hence you cannot rely on that the - * internal name matches with the name you passed to Application::newDoument(). You should use the - * method getName() instead. - */ -const char* Document::getName() const -{ - // return GetApplication().getDocumentName(this); - return myName.c_str(); -} - -std::string Document::getFullName() const { - return myName; -} - -const char* Document::getProgramVersion() const -{ - return d->programVersion.c_str(); -} - -const char* Document::getFileName() const -{ - return testStatus(TempDoc) ? TransientDir.getValue() - : FileName.getValue(); -} - -/// Remove all modifications. After this call The document becomes valid again. -void Document::purgeTouched() -{ - for (std::vector::iterator It = d->objectArray.begin();It != d->objectArray.end();++It) - (*It)->purgeTouched(); -} - -bool Document::isTouched() const -{ - for (std::vector::const_iterator It = d->objectArray.begin();It != d->objectArray.end();++It) - if ((*It)->isTouched()) - return true; - return false; -} - -vector Document::getTouched() const -{ - vector result; - - for (std::vector::const_iterator It = d->objectArray.begin();It != d->objectArray.end();++It) - if ((*It)->isTouched()) - result.push_back(*It); - - return result; -} - -void Document::setClosable(bool c) -{ - setStatus(Document::Closable, c); -} - -bool Document::isClosable() const -{ - return testStatus(Document::Closable); -} - -int Document::countObjects() const -{ - return static_cast(d->objectArray.size()); -} - -void Document::getLinksTo(std::set &links, - const DocumentObject *obj, int options, int maxCount, - const std::vector &objs) const -{ - std::map > linkMap; - - for(auto o : !objs.empty() ? objs : d->objectArray) { - if (o == obj) - continue; - auto linked = o; - if (options & GetLinkArrayElement) { - linked = o->getLinkedObject(false); - } - else { - auto ext = o->getExtensionByType(true); - if(ext) - linked = ext->getTrueLinkedObject(false,nullptr,0,true); - else - linked = o->getLinkedObject(false); - } - - if(linked && linked!=o) { - if(options & GetLinkRecursive) - linkMap[linked].push_back(o); - else if(linked == obj || !obj) { - if((options & GetLinkExternal) - && linked->getDocument()==o->getDocument()) - continue; - else if(options & GetLinkedObject) - links.insert(linked); - else - links.insert(o); - if(maxCount && maxCount<=(int)links.size()) - return; - } - } - } - - if(!(options & GetLinkRecursive)) - return; - - std::vector current(1,obj); - for(int depth=0;!current.empty();++depth) { - if(!GetApplication().checkLinkDepth(depth, MessageOption::Error)) - break; - std::vector next; - for(const App::DocumentObject *o : current) { - auto iter = linkMap.find(o); - if(iter==linkMap.end()) - continue; - for (App::DocumentObject *link : iter->second) { - if (links.insert(link).second) { - if(maxCount && maxCount<=(int)links.size()) - return; - next.push_back(link); - } - } - } - current = std::move(next); - } - return; -} - -bool Document::hasLinksTo(const DocumentObject *obj) const { - std::set links; - getLinksTo(links,obj,0,1); - return !links.empty(); -} - -std::vector Document::getInList(const DocumentObject* me) const -{ - // result list - std::vector result; - // go through all objects - for (auto It = d->objectMap.begin(); It != d->objectMap.end();++It) { - // get the outList and search if me is in that list - std::vector OutList = It->second->getOutList(); - for (std::vector::const_iterator It2=OutList.begin();It2!=OutList.end();++It2) - if (*It2 && *It2 == me) - // add the parent object - result.push_back(It->second); - } - return result; -} - -// This function unifies the old _rebuildDependencyList() and -// getDependencyList(). The algorithm basically obtains the object dependency -// by recrusivly visiting the OutList of each object in the given object array. -// It makes sure to call getOutList() of each object once and only once, which -// makes it much more efficient than calling getRecursiveOutList() on each -// individual object. -// -// The problem with the original algorithm is that, it assumes the objects -// inside any OutList are all within the given object array, so it does not -// recursively call getOutList() on those dependent objects inside. This -// assumption is broken by the introduction of PropertyXLink which can link to -// external object. -// -static void _buildDependencyList(const std::vector &objectArray, - int options, std::vector *depObjs, - DependencyList *depList, std::map *objectMap, - bool *touchCheck = nullptr) -{ - std::map > outLists; - std::deque objs; - - if(objectMap) objectMap->clear(); - if(depList) depList->clear(); - - int op = (options & Document::DepNoXLinked)?DocumentObject::OutListNoXLinked:0; - for (auto obj : objectArray) { - objs.push_back(obj); - while(!objs.empty()) { - auto obj = objs.front(); - objs.pop_front(); - if(!obj || !obj->getNameInDocument()) - continue; - - auto it = outLists.find(obj); - if(it!=outLists.end()) - continue; - - if(touchCheck) { - if(obj->isTouched() || obj->mustExecute()) { - // early termination on touch check - *touchCheck = true; - return; - } - } - if(depObjs) depObjs->push_back(obj); - if(objectMap && depList) - (*objectMap)[obj] = add_vertex(*depList); - - auto &outList = outLists[obj]; - outList = obj->getOutList(op); - objs.insert(objs.end(),outList.begin(),outList.end()); - } - } - - if(objectMap && depList) { - for (const auto &v : outLists) { - for(auto obj : v.second) { - if(obj && obj->getNameInDocument()) - add_edge((*objectMap)[v.first],(*objectMap)[obj],*depList); - } - } - } -} - -std::vector Document::getDependencyList( - const std::vector& objectArray, int options) -{ - std::vector ret; - if(!(options & DepSort)) { - _buildDependencyList(objectArray,options,&ret,nullptr,nullptr); - return ret; - } - - DependencyList depList; - std::map objectMap; - std::map vertexMap; - - _buildDependencyList(objectArray,options,nullptr,&depList,&objectMap); - - for(auto &v : objectMap) - vertexMap[v.second] = v.first; - - std::list make_order; - try { - boost::topological_sort(depList, std::front_inserter(make_order)); - } catch (const std::exception& e) { - if(options & DepNoCycle) { - // Use boost::strong_components to find cycles. It groups strongly - // connected vertices as components, and therefore each component - // forms a cycle. - std::vector c(vertexMap.size()); - std::map > components; - boost::strong_components(depList,boost::make_iterator_property_map( - c.begin(),boost::get(boost::vertex_index,depList),c[0])); - for(size_t i=0;isecond->getOutList()) { - if(obj == it->second) { - ss << std::endl << it->second->getFullName() << std::endl; - break; - } - } - continue; - } - // For components with more than one member, they form a loop together - for(size_t i=0;isecond->getFullName() << ", "; - } - ss << std::endl; - } - FC_ERR(ss.str()); - FC_THROWM(Base::RuntimeError, e.what()); - } - FC_ERR(e.what()); - ret = DocumentP::partialTopologicalSort(objectArray); - std::reverse(ret.begin(),ret.end()); - return ret; - } - - for (std::list::reverse_iterator i = make_order.rbegin();i != make_order.rend(); ++i) - ret.push_back(vertexMap[*i]); - return ret; -} - -std::vector Document::getDependentDocuments(bool sort) { - return getDependentDocuments({this},sort); -} - -std::vector Document::getDependentDocuments( - std::vector pending, bool sort) -{ - DependencyList depList; - std::map docMap; - std::map vertexMap; - - std::vector ret; - if(pending.empty()) - return ret; - - auto outLists = PropertyXLink::getDocumentOutList(); - std::set docs; - docs.insert(pending.begin(),pending.end()); - if(sort) { - for(auto doc : pending) - docMap[doc] = add_vertex(depList); - } - while(!pending.empty()) { - auto doc = pending.back(); - pending.pop_back(); - - auto it = outLists.find(doc); - if(it == outLists.end()) - continue; - - auto &vertex = docMap[doc]; - for(auto depDoc : it->second) { - if(docs.insert(depDoc).second) { - pending.push_back(depDoc); - if(sort) - docMap[depDoc] = add_vertex(depList); - } - add_edge(vertex,docMap[depDoc],depList); - } - } - - if(!sort) { - ret.insert(ret.end(),docs.begin(),docs.end()); - return ret; - } - - std::list make_order; - try { - boost::topological_sort(depList, std::front_inserter(make_order)); - } catch (const std::exception& e) { - std::string msg("Document::getDependentDocuments: "); - msg += e.what(); - throw Base::RuntimeError(msg); - } - - for(auto &v : docMap) - vertexMap[v.second] = v.first; - for (auto rIt=make_order.rbegin(); rIt!=make_order.rend(); ++rIt) - ret.push_back(vertexMap[*rIt]); - return ret; -} - -void Document::_rebuildDependencyList(const std::vector &objs) -{ -#ifdef USE_OLD_DAG - _buildDependencyList(objs.empty()?d->objectArray:objs,false,0,&d->DepList,&d->VertexObjectList); -#else - (void)objs; -#endif -} - -/** - * @brief Signal that object identifiers, typically a property or document object has been renamed. - * - * This function iterates through all document object in the document, and calls its - * renameObjectIdentifiers functions. - * - * @param paths Map with current and new names - */ - -void Document::renameObjectIdentifiers(const std::map &paths, const std::function & selector) -{ - std::map extendedPaths; - - std::map::const_iterator it = paths.begin(); - while (it != paths.end()) { - extendedPaths[it->first.canonicalPath()] = it->second.canonicalPath(); - ++it; - } - - for (std::vector::iterator it = d->objectArray.begin(); it != d->objectArray.end(); ++it) - if (selector(*it)) - (*it)->renameObjectIdentifiers(extendedPaths); -} - -#ifdef USE_OLD_DAG -int Document::recompute(const std::vector &objs, bool force) -{ - if (testStatus(Document::Recomputing)) { - // this is clearly a bug in the calling instance - throw Base::RuntimeError("Nested recomputes of a document are not allowed"); - } - - int objectCount = 0; - - // The 'SkipRecompute' flag can be (tmp.) set to avoid too many - // time expensive recomputes - if(!force && testStatus(Document::SkipRecompute)) - return 0; - - Base::ObjectStatusLocker exe(Document::Recomputing, this); - - // delete recompute log - d->clearRecomputeLog(); - - // updates the dependency graph - _rebuildDependencyList(objs); - - std::list make_order; - DependencyList::out_edge_iterator j, jend; - - try { - // this sort gives the execute - boost::topological_sort(d->DepList, std::front_inserter(make_order)); - } - catch (const std::exception& e) { - std::cerr << "Document::recompute: " << e.what() << std::endl; - return -1; - } - - // caching vertex to DocObject - for (std::map::const_iterator It1= d->VertexObjectList.begin();It1 != d->VertexObjectList.end(); ++It1) - d->vertexMap[It1->second] = It1->first; - -#ifdef FC_LOGFEATUREUPDATE - std::clog << "make ordering: " << std::endl; -#endif - - std::set recomputeList; - - for (std::list::reverse_iterator i = make_order.rbegin();i != make_order.rend(); ++i) { - DocumentObject* Cur = d->vertexMap[*i]; - // Because of PropertyXLink, we should account for external objects - // TODO: make sure it is safe to rely on getNameInDocument() to check if - // object is in the document. If it crashes, then we should fix the code - // to properly nullify getNameInDocument(), rather than revert back to - // the inefficient isIn() - // if (!Cur || !isIn(Cur)) continue; - if (!Cur || !Cur->getNameInDocument()) continue; -#ifdef FC_LOGFEATUREUPDATE - std::clog << Cur->getNameInDocument() << " dep on:" ; -#endif - bool NeedUpdate = false; - - // ask the object if it should be recomputed - if (Cur->mustExecute() == 1 || Cur->ExpressionEngine.depsAreTouched()) { -#ifdef FC_LOGFEATUREUPDATE - std::clog << "[touched]"; -#endif - NeedUpdate = true; - } - else {// if (Cur->mustExecute() == -1) - // update if one of the dependencies is touched - for (boost::tie(j, jend) = out_edges(*i, d->DepList); j != jend; ++j) { - DocumentObject* Test = d->vertexMap[target(*j, d->DepList)]; - - if (!Test) continue; -#ifdef FC_LOGFEATUREUPDATE - std::clog << " " << Test->getNameInDocument(); -#endif - if (Test->isTouched()) { - NeedUpdate = true; -#ifdef FC_LOGFEATUREUPDATE - std::clog << "[touched]"; -#endif - } - } - } - // if one touched recompute - if (NeedUpdate) { - Cur->touch(); -#ifdef FC_LOGFEATUREUPDATE - std::clog << " => Recompute feature"; -#endif - recomputeList.insert(Cur); - } -#ifdef FC_LOGFEATUREUPDATE - std::clog << std::endl; -#endif - } - -#ifdef FC_LOGFEATUREUPDATE - std::clog << "Have to recompute the following document objects" << std::endl; - for (std::set::const_iterator it = recomputeList.begin(); it != recomputeList.end(); ++it) { - std::clog << " " << (*it)->getNameInDocument() << std::endl; - } -#endif - - for (std::list::reverse_iterator i = make_order.rbegin();i != make_order.rend(); ++i) { - DocumentObject* Cur = d->vertexMap[*i]; - if (!Cur || !isIn(Cur)) continue; - - if (recomputeList.find(Cur) != recomputeList.end() || - Cur->ExpressionEngine.depsAreTouched()) { - if ( _recomputeFeature(Cur)) { - // if something happened break execution of recompute - d->vertexMap.clear(); - return -1; - } - signalRecomputedObject(*Cur); - ++objectCount; - } - } - - // reset all touched - for (std::map::iterator it = d->vertexMap.begin(); it != d->vertexMap.end(); ++it) { - // TODO: check the TODO comments above for details - // if ((it->second) && isIn(it->second)) - if ((it->second) && it->second->getNameInDocument()) - it->second->purgeTouched(); - } - d->vertexMap.clear(); - - signalRecomputed(*this); - - return objectCount; -} - -#else //ifdef USE_OLD_DAG - -int Document::recompute(const std::vector &objs, bool force, bool *hasError, int options) -{ - if (d->undoing || d->rollback) { - if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) - FC_WARN("Ignore document recompute on undo/redo"); - return 0; - } - - int objectCount = 0; - if (testStatus(Document::PartialDoc)) { - if(mustExecute()) - FC_WARN("Please reload partial document '" << Label.getValue() << "' for recomputation."); - return 0; - } - if (testStatus(Document::Recomputing)) { - // this is clearly a bug in the calling instance - FC_ERR("Recursive calling of recompute for document " << getName()); - return 0; - } - // The 'SkipRecompute' flag can be (tmp.) set to avoid too many - // time expensive recomputes - if(!force && testStatus(Document::SkipRecompute)) { - signalSkipRecompute(*this,objs); - return 0; - } - - // delete recompute log - d->clearRecomputeLog(); - - FC_TIME_INIT(t); - - Base::ObjectStatusLocker exe(Document::Recomputing, this); - signalBeforeRecompute(*this); - -#if 0 - ////////////////////////////////////////////////////////////////////////// - // FIXME Comment by Realthunder: - // the topologicalSrot() below cannot handle partial recompute, haven't got - // time to figure out the code yet, simply use back boost::topological_sort - // for now, that is, rely on getDependencyList() to do the sorting. The - // downside is, it didn't take advantage of the ready built InList, nor will - // it report for cyclic dependency. - ////////////////////////////////////////////////////////////////////////// - - // get the sorted vector of all dependent objects and go though it from the end - auto depObjs = getDependencyList(objs.empty()?d->objectArray:objs); - vector topoSortedObjects = topologicalSort(depObjs); - if (topoSortedObjects.size() != depObjs.size()){ - cerr << "App::Document::recompute(): cyclic dependency detected" << endl; - topoSortedObjects = d->partialTopologicalSort(depObjs); - } - std::reverse(topoSortedObjects.begin(),topoSortedObjects.end()); -#else - auto topoSortedObjects = getDependencyList(objs.empty()?d->objectArray:objs,DepSort|options); -#endif - for(auto obj : topoSortedObjects) - obj->setStatus(ObjectStatus::PendingRecompute,true); - - ParameterGrp::handle hGrp = GetApplication().GetParameterGroupByPath( - "User parameter:BaseApp/Preferences/Document"); - bool canAbort = hGrp->GetBool("CanAbortRecompute",true); - - std::set filter; - size_t idx = 0; - - FC_TIME_INIT(t2); - - try { - // maximum two passes to allow some form of dependency inversion - for(int passes=0; passes<2 && idx seq; - if(canAbort) - seq.reset(new Base::SequencerLauncher("Recompute...", topoSortedObjects.size())); - FC_LOG("Recompute pass " << passes); - for (; idx < topoSortedObjects.size(); ++idx) { - auto obj = topoSortedObjects[idx]; - if(!obj->getNameInDocument() || filter.find(obj)!=filter.end()) - continue; - // ask the object if it should be recomputed - bool doRecompute = false; - if (obj->mustRecompute()) { - doRecompute = true; - ++objectCount; - int res = _recomputeFeature(obj); - if(res) { - if(hasError) - *hasError = true; - if(res < 0) { - passes = 2; - break; - } - // if something happened filter all object in its - // inListRecursive from the queue then proceed - obj->getInListEx(filter,true); - filter.insert(obj); - continue; - } - } - if(obj->isTouched() || doRecompute) { - signalRecomputedObject(*obj); - obj->purgeTouched(); - // set all dependent object touched to force recompute - for (auto inObjIt : obj->getInList()) - inObjIt->enforceRecompute(); - } - if (seq) - seq->next(true); - } - // check if all objects are recomputed but still thouched - for (size_t i=0;isetStatus(ObjectStatus::Recompute2,false); - if(!filter.count(obj) && obj->isTouched()) { - if(passes>0) - FC_ERR(obj->getFullName() << " still touched after recompute"); - else{ - FC_LOG(obj->getFullName() << " still touched after recompute"); - if(idx>=topoSortedObjects.size()) { - // let's start the next pass on the first touched object - idx = i; - } - obj->setStatus(ObjectStatus::Recompute2,true); - } - } - } - } - }catch(Base::Exception &e) { - e.ReportException(); - } - - FC_TIME_LOG(t2, "Recompute"); - - for(auto obj : topoSortedObjects) { - if(!obj->getNameInDocument()) - continue; - obj->setStatus(ObjectStatus::PendingRecompute,false); - obj->setStatus(ObjectStatus::Recompute2,false); - } - - signalRecomputed(*this,topoSortedObjects); - - FC_TIME_LOG(t,"Recompute total"); - - if (!d->_RecomputeLog.empty()) { - if (!testStatus(Status::IgnoreErrorOnRecompute)) - Base::Console().Error("Recompute failed!\n"); - } - - for (auto doc : GetApplication().getDocuments()) { - decltype(doc->d->pendingRemove) objs; - objs.swap(doc->d->pendingRemove); - for(auto &o : objs) { - try { - if (auto obj = o.getObject()) { - obj->getDocument()->removeObject(obj->getNameInDocument()); - } - } catch (Base::Exception & e) { - e.ReportException(); - FC_ERR("error when removing object " << o.getDocumentName() << '#' << o.getObjectName()); - } - } - } - return objectCount; -} - -#endif // USE_OLD_DAG - -/*! - Does almost the same as topologicalSort() until no object with an input degree of zero - can be found. It then searches for objects with an output degree of zero until neither - an object with input or output degree can be found. The remaining objects form one or - multiple cycles. - An alternative to this method might be: - https://en.wikipedia.org/wiki/Tarjan%E2%80%99s_strongly_connected_components_algorithm - */ -std::vector DocumentP::partialTopologicalSort( - const std::vector& objects) -{ - vector < App::DocumentObject* > ret; - ret.reserve(objects.size()); - // pairs of input and output degree - map < App::DocumentObject*, std::pair > countMap; - - for (auto objectIt : objects) { - //we need inlist with unique entries - auto in = objectIt->getInList(); - std::sort(in.begin(), in.end()); - in.erase(std::unique(in.begin(), in.end()), in.end()); - - //we need outlist with unique entries - auto out = objectIt->getOutList(); - std::sort(out.begin(), out.end()); - out.erase(std::unique(out.begin(), out.end()), out.end()); - - countMap[objectIt] = std::make_pair(in.size(), out.size()); - } - - std::list degIn; - std::list degOut; - - bool removeVertex = true; - while (removeVertex) { - removeVertex = false; - - // try input degree - auto degInIt = find_if(countMap.begin(), countMap.end(), - [](pair< App::DocumentObject*, pair > vertex)->bool { - return vertex.second.first == 0; - }); - - if (degInIt != countMap.end()) { - removeVertex = true; - degIn.push_back(degInIt->first); - degInIt->second.first = degInIt->second.first - 1; - - //we need outlist with unique entries - auto out = degInIt->first->getOutList(); - std::sort(out.begin(), out.end()); - out.erase(std::unique(out.begin(), out.end()), out.end()); - - for (auto outListIt : out) { - auto outListMapIt = countMap.find(outListIt); - if (outListMapIt != countMap.end()) - outListMapIt->second.first = outListMapIt->second.first - 1; - } - } - } - - // make the output degree negative if input degree is negative - // to mark the vertex as processed - for (auto& countIt : countMap) { - if (countIt.second.first < 0) { - countIt.second.second = -1; - } - } - - removeVertex = degIn.size() != objects.size(); - while (removeVertex) { - removeVertex = false; - - auto degOutIt = find_if(countMap.begin(), countMap.end(), - [](pair< App::DocumentObject*, pair > vertex)->bool { - return vertex.second.second == 0; - }); - - if (degOutIt != countMap.end()) { - removeVertex = true; - degOut.push_front(degOutIt->first); - degOutIt->second.second = degOutIt->second.second - 1; - - //we need inlist with unique entries - auto in = degOutIt->first->getInList(); - std::sort(in.begin(), in.end()); - in.erase(std::unique(in.begin(), in.end()), in.end()); - - for (auto inListIt : in) { - auto inListMapIt = countMap.find(inListIt); - if (inListMapIt != countMap.end()) - inListMapIt->second.second = inListMapIt->second.second - 1; - } - } - } - - // at this point we have no root object any more - for (auto countIt : countMap) { - if (countIt.second.first > 0 && countIt.second.second > 0) { - degIn.push_back(countIt.first); - } - } - - ret.insert(ret.end(), degIn.begin(), degIn.end()); - ret.insert(ret.end(), degOut.begin(), degOut.end()); - - return ret; -} - -std::vector DocumentP::topologicalSort(const std::vector& objects) const -{ - // topological sort algorithm described here: - // https://de.wikipedia.org/wiki/Topologische_Sortierung#Algorithmus_f.C3.BCr_das_Topologische_Sortieren - vector < App::DocumentObject* > ret; - ret.reserve(objects.size()); - map < App::DocumentObject*,int > countMap; - - for (auto objectIt : objects) { - // We now support externally linked objects - // if(!obj->getNameInDocument() || obj->getDocument()!=this) - if(!objectIt->getNameInDocument()) - continue; - //we need inlist with unique entries - auto in = objectIt->getInList(); - std::sort(in.begin(), in.end()); - in.erase(std::unique(in.begin(), in.end()), in.end()); - - countMap[objectIt] = in.size(); - } - - auto rootObjeIt = find_if(countMap.begin(), countMap.end(), [](pair < App::DocumentObject*, int > count)->bool { - return count.second == 0; - }); - - if (rootObjeIt == countMap.end()){ - cerr << "Document::topologicalSort: cyclic dependency detected (no root object)" << endl; - return ret; - } - - while (rootObjeIt != countMap.end()){ - rootObjeIt->second = rootObjeIt->second - 1; - - //we need outlist with unique entries - auto out = rootObjeIt->first->getOutList(); - std::sort(out.begin(), out.end()); - out.erase(std::unique(out.begin(), out.end()), out.end()); - - for (auto outListIt : out) { - auto outListMapIt = countMap.find(outListIt); - if (outListMapIt != countMap.end()) - outListMapIt->second = outListMapIt->second - 1; - } - ret.push_back(rootObjeIt->first); - - rootObjeIt = find_if(countMap.begin(), countMap.end(), [](pair < App::DocumentObject*, int > count)->bool { - return count.second == 0; - }); - } - - return ret; -} - -std::vector Document::topologicalSort() const -{ - return d->topologicalSort(d->objectArray); -} - -const char * Document::getErrorDescription(const App::DocumentObject*Obj) const -{ - return d->findRecomputeLog(Obj); -} - -// call the recompute of the Feature and handle the exceptions and errors. -int Document::_recomputeFeature(DocumentObject* Feat) -{ - FC_LOG("Recomputing " << Feat->getFullName()); - - DocumentObjectExecReturn *returnCode = nullptr; - try { - returnCode = Feat->ExpressionEngine.execute(PropertyExpressionEngine::ExecuteNonOutput); - if (returnCode == DocumentObject::StdReturn) { - returnCode = Feat->recompute(); - if(returnCode == DocumentObject::StdReturn) - returnCode = Feat->ExpressionEngine.execute(PropertyExpressionEngine::ExecuteOutput); - } - } - catch(Base::AbortException &e){ - e.ReportException(); - FC_LOG("Failed to recompute " << Feat->getFullName() << ": " << e.what()); - d->addRecomputeLog("User abort",Feat); - return -1; - } - catch (const Base::MemoryException& e) { - FC_ERR("Memory exception in " << Feat->getFullName() << " thrown: " << e.what()); - d->addRecomputeLog("Out of memory exception",Feat); - return 1; - } - catch (Base::Exception &e) { - e.ReportException(); - FC_LOG("Failed to recompute " << Feat->getFullName() << ": " << e.what()); - d->addRecomputeLog(e.what(),Feat); - return 1; - } - catch (std::exception &e) { - FC_ERR("exception in " << Feat->getFullName() << " thrown: " << e.what()); - d->addRecomputeLog(e.what(),Feat); - return 1; - } -#ifndef FC_DEBUG - catch (...) { - FC_ERR("Unknown exception in " << Feat->getFullName() << " thrown"); - d->addRecomputeLog("Unknown exception!",Feat); - return 1; - } -#endif - - if (returnCode == DocumentObject::StdReturn) { - Feat->resetError(); - } - else { - returnCode->Which = Feat; - d->addRecomputeLog(returnCode); - FC_LOG("Failed to recompute " << Feat->getFullName() << ": " << returnCode->Why); - return 1; - } - return 0; -} - -bool Document::recomputeFeature(DocumentObject* Feat, bool recursive) -{ - // delete recompute log - d->clearRecomputeLog(Feat); - - // verify that the feature is (active) part of the document - if (Feat->getNameInDocument()) { - if(recursive) { - bool hasError = false; - recompute({Feat},true,&hasError); - return !hasError; - } else { - _recomputeFeature(Feat); - signalRecomputedObject(*Feat); - return Feat->isValid(); - } - }else - return false; -} - -DocumentObject * Document::addObject(const char* sType, const char* pObjectName, - bool isNew, const char* viewType, bool isPartial) -{ - Base::Type type = Base::Type::getTypeIfDerivedFrom(sType, App::DocumentObject::getClassTypeId(), true); - if (type.isBad()) { - std::stringstream str; - str << "'" << sType << "' is not a document object type"; - throw Base::TypeError(str.str()); - } - - void* typeInstance = type.createInstance(); - if (!typeInstance) - return nullptr; - - App::DocumentObject* pcObject = static_cast(typeInstance); - - pcObject->setDocument(this); - - // do no transactions if we do a rollback! - if (!d->rollback) { - // Undo stuff - _checkTransaction(nullptr,nullptr,__LINE__); - if (d->activeUndoTransaction) - d->activeUndoTransaction->addObjectDel(pcObject); - } - - // get Unique name - string ObjectName; - - if (pObjectName && pObjectName[0] != '\0') - ObjectName = getUniqueObjectName(pObjectName); - else - ObjectName = getUniqueObjectName(sType); - - - d->activeObject = pcObject; - - // insert in the name map - d->objectMap[ObjectName] = pcObject; - // generate object id and add to id map; - pcObject->_Id = ++d->lastObjectId; - d->objectIdMap[pcObject->_Id] = pcObject; - // cache the pointer to the name string in the Object (for performance of DocumentObject::getNameInDocument()) - pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first); - // insert in the vector - d->objectArray.push_back(pcObject); - - // If we are restoring, don't set the Label object now; it will be restored later. This is to avoid potential duplicate - // label conflicts later. - if (!d->StatusBits.test(Restoring)) - pcObject->Label.setValue( ObjectName ); - - // Call the object-specific initialization - if (!d->undoing && !d->rollback && isNew) { - pcObject->setupObject (); - } - - // mark the object as new (i.e. set status bit 2) and send the signal - pcObject->setStatus(ObjectStatus::New, true); - - pcObject->setStatus(ObjectStatus::PartialObject, isPartial); - - if (!viewType || viewType[0] == '\0') - viewType = pcObject->getViewProviderNameOverride(); - - if (viewType && viewType[0] != '\0') - pcObject->_pcViewProviderName = viewType; - - signalNewObject(*pcObject); - - // do no transactions if we do a rollback! - if (!d->rollback && d->activeUndoTransaction) { - signalTransactionAppend(*pcObject, d->activeUndoTransaction); - } - - signalActivatedObject(*pcObject); - - // return the Object - return pcObject; -} - -std::vector Document::addObjects(const char* sType, const std::vector& objectNames, bool isNew) -{ - Base::Type type = Base::Type::getTypeIfDerivedFrom(sType, App::DocumentObject::getClassTypeId(), true); - if (type.isBad()) { - std::stringstream str; - str << "'" << sType << "' is not a document object type"; - throw Base::TypeError(str.str()); - } - - std::vector objects; - objects.resize(objectNames.size()); - std::generate(objects.begin(), objects.end(), - [&]{ return static_cast(type.createInstance()); }); - // the type instance could be a null pointer, it is enough to check the first element - if (!objects.empty() && !objects[0]) { - objects.clear(); - return objects; - } - - // get all existing object names - std::vector reservedNames; - reservedNames.reserve(d->objectMap.size()); - for (auto pos = d->objectMap.begin();pos != d->objectMap.end();++pos) { - reservedNames.push_back(pos->first); - } - - for (auto it = objects.begin(); it != objects.end(); ++it) { - auto index = std::distance(objects.begin(), it); - App::DocumentObject* pcObject = *it; - pcObject->setDocument(this); - - // do no transactions if we do a rollback! - if (!d->rollback) { - // Undo stuff - _checkTransaction(nullptr,nullptr,__LINE__); - if (d->activeUndoTransaction) { - d->activeUndoTransaction->addObjectDel(pcObject); - } - } - - // get unique name - std::string ObjectName = objectNames[index]; - if (ObjectName.empty()) - ObjectName = sType; - ObjectName = Base::Tools::getIdentifier(ObjectName); - if (d->objectMap.find(ObjectName) != d->objectMap.end()) { - // remove also trailing digits from clean name which is to avoid to create lengthy names - // like 'Box001001' - if (!testStatus(KeepTrailingDigits)) { - std::string::size_type index = ObjectName.find_last_not_of("0123456789"); - if (index+1 < ObjectName.size()) { - ObjectName = ObjectName.substr(0,index+1); - } - } - - ObjectName = Base::Tools::getUniqueName(ObjectName, reservedNames, 3); - } - - reservedNames.push_back(ObjectName); - - // insert in the name map - d->objectMap[ObjectName] = pcObject; - // generate object id and add to id map; - pcObject->_Id = ++d->lastObjectId; - d->objectIdMap[pcObject->_Id] = pcObject; - // cache the pointer to the name string in the Object (for performance of DocumentObject::getNameInDocument()) - pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first); - // insert in the vector - d->objectArray.push_back(pcObject); - - pcObject->Label.setValue(ObjectName); - - // Call the object-specific initialization - if (!d->undoing && !d->rollback && isNew) { - pcObject->setupObject(); - } - - // mark the object as new (i.e. set status bit 2) and send the signal - pcObject->setStatus(ObjectStatus::New, true); - - const char *viewType = pcObject->getViewProviderNameOverride(); - pcObject->_pcViewProviderName = viewType ? viewType : ""; - - signalNewObject(*pcObject); - - // do no transactions if we do a rollback! - if (!d->rollback && d->activeUndoTransaction) { - signalTransactionAppend(*pcObject, d->activeUndoTransaction); - } - } - - if (!objects.empty()) { - d->activeObject = objects.back(); - signalActivatedObject(*objects.back()); - } - - return objects; -} - -void Document::addObject(DocumentObject* pcObject, const char* pObjectName) -{ - if (pcObject->getDocument()) { - throw Base::RuntimeError("Document object is already added to a document"); - } - - pcObject->setDocument(this); - - // do no transactions if we do a rollback! - if (!d->rollback) { - // Undo stuff - _checkTransaction(nullptr,nullptr,__LINE__); - if (d->activeUndoTransaction) - d->activeUndoTransaction->addObjectDel(pcObject); - } - - // get unique name - string ObjectName; - if (pObjectName && pObjectName[0] != '\0') - ObjectName = getUniqueObjectName(pObjectName); - else - ObjectName = getUniqueObjectName(pcObject->getTypeId().getName()); - - d->activeObject = pcObject; - - // insert in the name map - d->objectMap[ObjectName] = pcObject; - // generate object id and add to id map; - if(!pcObject->_Id) pcObject->_Id = ++d->lastObjectId; - d->objectIdMap[pcObject->_Id] = pcObject; - // cache the pointer to the name string in the Object (for performance of DocumentObject::getNameInDocument()) - pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first); - // insert in the vector - d->objectArray.push_back(pcObject); - - pcObject->Label.setValue( ObjectName ); - - // mark the object as new (i.e. set status bit 2) and send the signal - pcObject->setStatus(ObjectStatus::New, true); - - const char *viewType = pcObject->getViewProviderNameOverride(); - pcObject->_pcViewProviderName = viewType ? viewType : ""; - - signalNewObject(*pcObject); - - // do no transactions if we do a rollback! - if (!d->rollback && d->activeUndoTransaction) { - signalTransactionAppend(*pcObject, d->activeUndoTransaction); - } - - signalActivatedObject(*pcObject); -} - -void Document::_addObject(DocumentObject* pcObject, const char* pObjectName) -{ - std::string ObjectName = getUniqueObjectName(pObjectName); - d->objectMap[ObjectName] = pcObject; - // generate object id and add to id map; - if(!pcObject->_Id) pcObject->_Id = ++d->lastObjectId; - d->objectIdMap[pcObject->_Id] = pcObject; - d->objectArray.push_back(pcObject); - // cache the pointer to the name string in the Object (for performance of DocumentObject::getNameInDocument()) - pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first); - - // do no transactions if we do a rollback! - if (!d->rollback) { - // Undo stuff - _checkTransaction(nullptr,nullptr,__LINE__); - if (d->activeUndoTransaction) - d->activeUndoTransaction->addObjectDel(pcObject); - } - - const char *viewType = pcObject->getViewProviderNameOverride(); - pcObject->_pcViewProviderName = viewType ? viewType : ""; - - // send the signal - signalNewObject(*pcObject); - - // do no transactions if we do a rollback! - if (!d->rollback && d->activeUndoTransaction) { - signalTransactionAppend(*pcObject, d->activeUndoTransaction); - } - - d->activeObject = pcObject; - signalActivatedObject(*pcObject); -} - -/// Remove an object out of the document -void Document::removeObject(const char* sName) -{ - auto pos = d->objectMap.find(sName); - - // name not found? - if (pos == d->objectMap.end()) - return; - - if (pos->second->testStatus(ObjectStatus::PendingRecompute)) { - // TODO: shall we allow removal if there is active undo transaction? - FC_MSG("pending remove of " << sName << " after recomputing document " << getName()); - d->pendingRemove.emplace_back(pos->second); - return; - } - - TransactionLocker tlock; - - _checkTransaction(pos->second,nullptr,__LINE__); - - if (d->activeObject == pos->second) - d->activeObject = nullptr; - - // Mark the object as about to be deleted - pos->second->setStatus(ObjectStatus::Remove, true); - if (!d->undoing && !d->rollback) { - pos->second->unsetupObject(); - } - - signalDeletedObject(*(pos->second)); - - // do no transactions if we do a rollback! - if (!d->rollback && d->activeUndoTransaction) { - // in this case transaction delete or save the object - signalTransactionRemove(*pos->second, d->activeUndoTransaction); - } - else { - // if not saved in undo -> delete object - signalTransactionRemove(*pos->second, 0); - } - -#ifdef USE_OLD_DAG - if (!d->vertexMap.empty()) { - // recompute of document is running - for (std::map::iterator it = d->vertexMap.begin(); it != d->vertexMap.end(); ++it) { - if (it->second == pos->second) { - it->second = 0; // just nullify the pointer - break; - } - } - } -#endif //USE_OLD_DAG - - // Before deleting we must nullify all dependent objects - breakDependency(pos->second, true); - - //and remove the tip if needed - if (Tip.getValue() && strcmp(Tip.getValue()->getNameInDocument(), sName)==0) { - Tip.setValue(nullptr); - TipName.setValue(""); - } - - // remove the ID before possibly deleting the object - d->objectIdMap.erase(pos->second->_Id); - // Unset the bit to be on the safe side - pos->second->setStatus(ObjectStatus::Remove, false); - - // do no transactions if we do a rollback! - std::unique_ptr tobedestroyed; - if (!d->rollback) { - // Undo stuff - if (d->activeUndoTransaction) { - // in this case transaction delete or save the object - d->activeUndoTransaction->addObjectNew(pos->second); - } - else { - // if not saved in undo -> delete object later - std::unique_ptr delobj(pos->second); - tobedestroyed.swap(delobj); - tobedestroyed->setStatus(ObjectStatus::Destroy, true); - } - } - - for (std::vector::iterator obj = d->objectArray.begin(); obj != d->objectArray.end(); ++obj) { - if (*obj == pos->second) { - d->objectArray.erase(obj); - break; - } - } - - // In case the object gets deleted the pointer must be nullified - if (tobedestroyed) { - tobedestroyed->pcNameInDocument = nullptr; - } - d->objectMap.erase(pos); -} - -/// Remove an object out of the document (internal) -void Document::_removeObject(DocumentObject* pcObject) -{ - if (testStatus(Document::Recomputing)) { - FC_ERR("Cannot delete " << pcObject->getFullName() << " while recomputing"); - return; - } - - TransactionLocker tlock; - - // TODO Refactoring: share code with Document::removeObject() (2015-09-01, Fat-Zer) - _checkTransaction(pcObject,nullptr,__LINE__); - - auto pos = d->objectMap.find(pcObject->getNameInDocument()); - - if(!d->rollback && d->activeUndoTransaction && pos->second->hasChildElement()) { - // Preserve link group children global visibility. See comments in - // removeObject() for more details. - for(auto &sub : pos->second->getSubObjects()) { - if(sub.empty()) - continue; - if(sub[sub.size()-1]!='.') - sub += '.'; - auto sobj = pos->second->getSubObject(sub.c_str()); - if(sobj && sobj->getDocument()==this && !sobj->Visibility.getValue()) - d->activeUndoTransaction->addObjectChange(sobj,&sobj->Visibility); - } - } - - if (d->activeObject == pcObject) - d->activeObject = nullptr; - - // Mark the object as about to be removed - pcObject->setStatus(ObjectStatus::Remove, true); - if (!d->undoing && !d->rollback) { - pcObject->unsetupObject(); - } - signalDeletedObject(*pcObject); - // TODO Check me if it's needed (2015-09-01, Fat-Zer) - - //remove the tip if needed - if (Tip.getValue() == pcObject) { - Tip.setValue(nullptr); - TipName.setValue(""); - } - - // do no transactions if we do a rollback! - if (!d->rollback && d->activeUndoTransaction) { - // Undo stuff - signalTransactionRemove(*pcObject, d->activeUndoTransaction); - d->activeUndoTransaction->addObjectNew(pcObject); - } - else { - // for a rollback delete the object - signalTransactionRemove(*pcObject, 0); - breakDependency(pcObject, true); - } - - // remove from map - pcObject->setStatus(ObjectStatus::Remove, false); // Unset the bit to be on the safe side - d->objectIdMap.erase(pcObject->_Id); - d->objectMap.erase(pos); - - for (std::vector::iterator it = d->objectArray.begin(); it != d->objectArray.end(); ++it) { - if (*it == pcObject) { - d->objectArray.erase(it); - break; - } - } - - // for a rollback delete the object - if (d->rollback) { - pcObject->setStatus(ObjectStatus::Destroy, true); - delete pcObject; - } -} - -void Document::breakDependency(DocumentObject* pcObject, bool clear) -{ - // Nullify all dependent objects - PropertyLinkBase::breakLinks(pcObject,d->objectArray,clear); -} - -std::vector Document::copyObject( - const std::vector &objs, bool recursive, bool returnAll) -{ - std::vector deps; - if(!recursive) - deps = objs; - else - deps = getDependencyList(objs,DepNoXLinked|DepSort); - - if (!testStatus(TempDoc) && !isSaved() && PropertyXLink::hasXLink(deps)) { - throw Base::RuntimeError( - "Document must be saved at least once before link to external objects"); - } - - MergeDocuments md(this); - // if not copying recursively then suppress possible warnings - md.setVerbose(recursive); - - unsigned int memsize=1000; // ~ for the meta-information - for (std::vector::iterator it = deps.begin(); it != deps.end(); ++it) - memsize += (*it)->getMemSize(); - - // if less than ~10 MB - bool use_buffer=(memsize < 0xA00000); - QByteArray res; - try { - res.reserve(memsize); - } - catch (const Base::MemoryException&) { - use_buffer = false; - } - - std::vector imported; - if (use_buffer) { - Base::ByteArrayOStreambuf obuf(res); - std::ostream ostr(&obuf); - exportObjects(deps, ostr); - - Base::ByteArrayIStreambuf ibuf(res); - std::istream istr(nullptr); - istr.rdbuf(&ibuf); - imported = md.importObjects(istr); - } else { - static Base::FileInfo fi(App::Application::getTempFileName()); - Base::ofstream ostr(fi, std::ios::out | std::ios::binary); - exportObjects(deps, ostr); - ostr.close(); - - Base::ifstream istr(fi, std::ios::in | std::ios::binary); - imported = md.importObjects(istr); - } - - if (returnAll || imported.size()!=deps.size()) - return imported; - - std::unordered_map indices; - size_t i=0; - for(auto o : deps) - indices[o] = i++; - std::vector result; - result.reserve(objs.size()); - for(auto o : objs) - result.push_back(imported[indices[o]]); - return result; -} - -std::vector -Document::importLinks(const std::vector &objArray) -{ - std::set links; - getLinksTo(links,nullptr,GetLinkExternal,0,objArray); - - std::vector objs; - objs.insert(objs.end(),links.begin(),links.end()); - objs = App::Document::getDependencyList(objs); - if(objs.empty()) { - FC_ERR("nothing to import"); - return objs; - } - - for(auto it=objs.begin();it!=objs.end();) { - auto obj = *it; - if(obj->getDocument() == this) { - it = objs.erase(it); - continue; - } - ++it; - if(obj->testStatus(App::PartialObject)) { - throw Base::RuntimeError( - "Cannot import partial loaded object. Please reload the current document"); - } - } - - Base::FileInfo fi(App::Application::getTempFileName()); - { - // save stuff to temp file - Base::ofstream str(fi, std::ios::out | std::ios::binary); - MergeDocuments mimeView(this); - exportObjects(objs, str); - str.close(); - } - Base::ifstream str(fi, std::ios::in | std::ios::binary); - MergeDocuments mimeView(this); - objs = mimeView.importObjects(str); - str.close(); - fi.deleteFile(); - - const auto &nameMap = mimeView.getNameMap(); - - // First, find all link type properties that needs to be changed - std::map > propMap; - std::vector propList; - for(auto obj : links) { - propList.clear(); - obj->getPropertyList(propList); - for(auto prop : propList) { - auto linkProp = Base::freecad_dynamic_cast(prop); - if(linkProp && !prop->testStatus(Property::Immutable) && !obj->isReadOnly(prop)) { - auto copy = linkProp->CopyOnImportExternal(nameMap); - if(copy) - propMap[linkProp].reset(copy); - } - } - } - - // Then change them in one go. Note that we don't make change in previous - // loop, because a changed link property may break other depending link - // properties, e.g. a link sub referring to some sub object of an xlink, If - // that sub object is imported with a different name, and xlink is changed - // before this link sub, it will break. - for(auto &v : propMap) - v.first->Paste(*v.second); - - return objs; -} - -DocumentObject* Document::moveObject(DocumentObject* obj, bool recursive) -{ - if(!obj) - return nullptr; - Document* that = obj->getDocument(); - if (that == this) - return nullptr; // nothing todo - - // True object move without copy is only safe when undo is off on both - // documents. - if(!recursive && !d->iUndoMode && !that->d->iUndoMode && !that->d->rollback) { - // all object of the other document that refer to this object must be nullified - that->breakDependency(obj, false); - std::string objname = getUniqueObjectName(obj->getNameInDocument()); - that->_removeObject(obj); - this->_addObject(obj, objname.c_str()); - obj->setDocument(this); - return obj; - } - - std::vector deps; - if(recursive) - deps = getDependencyList({obj},DepNoXLinked|DepSort); - else - deps.push_back(obj); - - auto objs = copyObject(deps,false); - if(objs.empty()) - return nullptr; - // Some object may delete its children if deleted, so we collect the IDs - // or all depending objects for safety reason. - std::vector ids; - ids.reserve(deps.size()); - for(auto o : deps) - ids.push_back(o->getID()); - - // We only remove object if it is the moving object or it has no - // depending objects, i.e. an empty inList, which is why we need to - // iterate the depending list backwards. - for(auto iter=ids.rbegin();iter!=ids.rend();++iter) { - auto o = that->getObjectByID(*iter); - if(!o) continue; - if(iter==ids.rbegin() - || o->getInList().empty()) - that->removeObject(o->getNameInDocument()); - } - return objs.back(); -} - -DocumentObject * Document::getActiveObject() const -{ - return d->activeObject; -} - -DocumentObject * Document::getObject(const char *Name) const -{ - auto pos = d->objectMap.find(Name); - - if (pos != d->objectMap.end()) - return pos->second; - else - return nullptr; -} - -DocumentObject * Document::getObjectByID(long id) const -{ - auto it = d->objectIdMap.find(id); - if(it!=d->objectIdMap.end()) - return it->second; - return nullptr; -} - - -// Note: This method is only used in Tree.cpp slotChangeObject(), see explanation there -bool Document::isIn(const DocumentObject *pFeat) const -{ - for (auto o = d->objectMap.begin(); o != d->objectMap.end(); ++o) { - if (o->second == pFeat) - return true; - } - - return false; -} - -const char * Document::getObjectName(DocumentObject *pFeat) const -{ - for (auto pos = d->objectMap.begin();pos != d->objectMap.end();++pos) { - if (pos->second == pFeat) - return pos->first.c_str(); - } - - return nullptr; -} - -std::string Document::getUniqueObjectName(const char *Name) const -{ - if (!Name || *Name == '\0') - return std::string(); - std::string CleanName = Base::Tools::getIdentifier(Name); - - // name in use? - auto pos = d->objectMap.find(CleanName); - - if (pos == d->objectMap.end()) { - // if not, name is OK - return CleanName; - } - else { - // remove also trailing digits from clean name which is to avoid to create lengthy names - // like 'Box001001' - if (!testStatus(KeepTrailingDigits)) { - std::string::size_type index = CleanName.find_last_not_of("0123456789"); - if (index+1 < CleanName.size()) { - CleanName = CleanName.substr(0,index+1); - } - } - - std::vector names; - names.reserve(d->objectMap.size()); - for (pos = d->objectMap.begin();pos != d->objectMap.end();++pos) { - names.push_back(pos->first); - } - return Base::Tools::getUniqueName(CleanName, names, 3); - } -} - -std::string Document::getStandardObjectName(const char *Name, int d) const -{ - std::vector mm = getObjects(); - std::vector labels; - labels.reserve(mm.size()); - - for (std::vector::const_iterator it = mm.begin(); it != mm.end(); ++it) { - std::string label = (*it)->Label.getValue(); - labels.push_back(label); - } - return Base::Tools::getUniqueName(Name, labels, d); -} - -std::vector Document::getDependingObjects() const -{ - return getDependencyList(d->objectArray); -} - -const std::vector &Document::getObjects() const -{ - return d->objectArray; -} - - -std::vector Document::getObjectsOfType(const Base::Type& typeId) const -{ - std::vector Objects; - for (std::vector::const_iterator it = d->objectArray.begin(); it != d->objectArray.end(); ++it) { - if ((*it)->getTypeId().isDerivedFrom(typeId)) - Objects.push_back(*it); - } - return Objects; -} - -std::vector< DocumentObject* > Document::getObjectsWithExtension(const Base::Type& typeId, bool derived) const { - - std::vector Objects; - for (std::vector::const_iterator it = d->objectArray.begin(); it != d->objectArray.end(); ++it) { - if ((*it)->hasExtension(typeId, derived)) - Objects.push_back(*it); - } - return Objects; -} - - -std::vector Document::findObjects(const Base::Type& typeId, const char* objname, const char* label) const -{ - boost::cmatch what; - boost::regex rx_name, rx_label; - - if (objname) - rx_name.set_expression(objname); - - if (label) - rx_label.set_expression(label); - - std::vector Objects; - DocumentObject* found = nullptr; - for (std::vector::const_iterator it = d->objectArray.begin(); it != d->objectArray.end(); ++it) { - if ((*it)->getTypeId().isDerivedFrom(typeId)) { - found = *it; - - if (!rx_name.empty() && !boost::regex_search((*it)->getNameInDocument(), what, rx_name)) - found = nullptr; - - if (!rx_label.empty() && !boost::regex_search((*it)->Label.getValue(), what, rx_label)) - found = nullptr; - - if (found) - Objects.push_back(found); - } - } - return Objects; -} - -int Document::countObjectsOfType(const Base::Type& typeId) const -{ - int ct=0; - for (auto it = d->objectMap.begin(); it != d->objectMap.end(); ++it) { - if (it->second->getTypeId().isDerivedFrom(typeId)) - ct++; - } - - return ct; -} - -PyObject * Document::getPyObject() -{ - return Py::new_reference_to(d->DocumentPythonObject); -} - -std::vector Document::getRootObjects() const -{ - std::vector < App::DocumentObject* > ret; - - for (auto objectIt : d->objectArray) { - if (objectIt->getInList().empty()) - ret.push_back(objectIt); - } - - return ret; -} - -void DocumentP::findAllPathsAt(const std::vector &all_nodes, size_t id, - std::vector &all_paths, Path tmp) -{ - if (std::find(tmp.begin(), tmp.end(), id) != tmp.end()) { - Path tmp2(tmp); - tmp2.push_back(id); - all_paths.push_back(tmp2); - return; // a cycle - } - - tmp.push_back(id); - if (all_nodes[id].empty()) { - all_paths.push_back(tmp); - return; - } - - for (size_t i=0; i < all_nodes[id].size(); i++) { - Path tmp2(tmp); - findAllPathsAt(all_nodes, all_nodes[id][i], all_paths, tmp2); - } -} - -std::vector > -Document::getPathsByOutList(const App::DocumentObject* from, const App::DocumentObject* to) const -{ - std::map indexMap; - for (size_t i=0; iobjectArray.size(); ++i) { - indexMap[d->objectArray[i]] = i; - } - - std::vector all_nodes(d->objectArray.size()); - for (size_t i=0; iobjectArray.size(); ++i) { - DocumentObject* obj = d->objectArray[i]; - std::vector outList = obj->getOutList(); - for (auto it : outList) { - all_nodes[i].push_back(indexMap[it]); - } - } - - std::vector > array; - if (from == to) - return array; - - size_t index_from = indexMap[from]; - size_t index_to = indexMap[to]; - Path tmp; - std::vector all_paths; - DocumentP::findAllPathsAt(all_nodes, index_from, all_paths, tmp); - - for (std::vector::iterator it = all_paths.begin(); it != all_paths.end(); ++it) { - Path::iterator jt = std::find(it->begin(), it->end(), index_to); - if (jt != it->end()) { - std::list path; - for (Path::iterator kt = it->begin(); kt != jt; ++kt) { - path.push_back(d->objectArray[*kt]); - } - - path.push_back(d->objectArray[*jt]); - array.push_back(path); - } - } - - // remove duplicates - std::sort(array.begin(), array.end()); - array.erase(std::unique(array.begin(), array.end()), array.end()); - - return array; -} - -bool Document::mustExecute() const -{ - if(PropertyXLink::hasXLink(this)) { - bool touched = false; - _buildDependencyList(d->objectArray,false,nullptr,nullptr,nullptr,&touched); - return touched; - } - - for (std::vector::const_iterator It = d->objectArray.begin();It != d->objectArray.end();++It) - if ((*It)->isTouched() || (*It)->mustExecute()==1) - return true; - return false; -} +/*************************************************************************** + * Copyright (c) 2002 Jürgen Riegel * + * * + * 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 * + * * + ***************************************************************************/ + + +/*! +\defgroup Document Document +\ingroup APP +\brief The Base class of the FreeCAD Document + +This (besides the App::Application class) is the most important class in FreeCAD. +It contains all the data of the opened, saved, or newly created FreeCAD Document. +The App::Document manages the Undo and Redo mechanism and the linking of documents. + +\namespace App \class App::Document +This is besides the Application class the most important class in FreeCAD +It contains all the data of the opened, saved or newly created FreeCAD Document. +The Document manage the Undo and Redo mechanism and the linking of documents. + +Note: the documents are not free objects. They are completely handled by the +App::Application. Only the Application can Open or destroy a document. + +\section Exception Exception handling +As the document is the main data structure of FreeCAD we have to take a close +look at how Exceptions affect the integrity of the App::Document. + +\section UndoRedo Undo Redo an Transactions +Undo Redo handling is one of the major mechanism of a document in terms of +user friendliness and speed (no one will wait for Undo too long). + +\section Dependency Graph and dependency handling +The FreeCAD document handles the dependencies of its DocumentObjects with +an adjacence list. This gives the opportunity to calculate the shortest +recompute path. Also, it enables more complicated dependencies beyond trees. + +@see App::Application +@see App::DocumentObject +*/ + +#include "PreCompiled.h" + +#ifndef _PreComp_ +# include +# include +# include +#endif + +#include +#include + +#ifdef USE_OLD_DAG +#include +#include +#include +#include +#endif //USE_OLD_DAG + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Document.h" +#include "private/DocumentP.h" +#include "Application.h" +#include "AutoTransaction.h" +#include "ExpressionParser.h" +#include "GeoFeature.h" +#include "License.h" +#include "Link.h" +#include "MergeDocuments.h" +#include "Transactions.h" + +#ifdef _MSC_VER +#include +#endif +#include +#include +#include +#include + + +FC_LOG_LEVEL_INIT("App", true, true, true) + +using Base::Console; +using Base::streq; +using Base::Writer; +using namespace App; +using namespace std; +using namespace boost; +using namespace zipios; + +#if FC_DEBUG +# define FC_LOGFEATUREUPDATE +#endif + +namespace fs = boost::filesystem; + +namespace App { + +static bool globalIsRestoring; +static bool globalIsRelabeling; + +DocumentP::DocumentP() +{ + static std::random_device _RD; + static std::mt19937 _RGEN(_RD()); + static std::uniform_int_distribution<> _RDIST(0, 5000); + // Set some random offset to reduce likelihood of ID collision when + // copying shape from other document. It is probably better to randomize + // on each object ID. + lastObjectId = _RDIST(_RGEN); + activeObject = nullptr; + activeUndoTransaction = nullptr; + iTransactionMode = 0; + rollback = false; + undoing = false; + committing = false; + opentransaction = false; + StatusBits.set((size_t)Document::Closable, true); + StatusBits.set((size_t)Document::KeepTrailingDigits, true); + StatusBits.set((size_t)Document::Restoring, false); + iUndoMode = 0; + UndoMemSize = 0; + UndoMaxStackSize = 20; +} + +} // namespace App + +PROPERTY_SOURCE(App::Document, App::PropertyContainer) + +bool Document::testStatus(Status pos) const +{ + return d->StatusBits.test((size_t)pos); +} + +void Document::setStatus(Status pos, bool on) +{ + d->StatusBits.set((size_t)pos, on); +} + +//bool _has_cycle_dfs(const DependencyList & g, vertex_t u, default_color_type * color) +//{ +// color[u] = gray_color; +// graph_traits < DependencyList >::adjacency_iterator vi, vi_end; +// for (tie(vi, vi_end) = adjacent_vertices(u, g); vi != vi_end; ++vi) +// if (color[*vi] == white_color) +// if (has_cycle_dfs(g, *vi, color)) +// return true; // cycle detected, return immediately +// else if (color[*vi] == gray_color) // *vi is an ancestor! +// return true; +// color[u] = black_color; +// return false; +//} + +bool Document::checkOnCycle() +{ +#if 0 + std::vector < default_color_type > color(num_vertices(_DepList), white_color); + graph_traits < DependencyList >::vertex_iterator vi, vi_end; + for (tie(vi, vi_end) = vertices(_DepList); vi != vi_end; ++vi) + if (color[*vi] == white_color) + if (_has_cycle_dfs(_DepList, *vi, &color[0])) + return true; +#endif + return false; +} + +bool Document::undo(int id) +{ + if (d->iUndoMode) { + if(id) { + auto it = mUndoMap.find(id); + if(it == mUndoMap.end()) + return false; + if(it->second != d->activeUndoTransaction) { + while(!mUndoTransactions.empty() && mUndoTransactions.back()!=it->second) + undo(0); + } + } + + if (d->activeUndoTransaction) + _commitTransaction(true); + if (mUndoTransactions.empty()) + return false; + // redo + d->activeUndoTransaction = new Transaction(mUndoTransactions.back()->getID()); + d->activeUndoTransaction->Name = mUndoTransactions.back()->Name; + + { + Base::FlagToggler flag(d->undoing); + // applying the undo + mUndoTransactions.back()->apply(*this,false); + + // save the redo + mRedoMap[d->activeUndoTransaction->getID()] = d->activeUndoTransaction; + mRedoTransactions.push_back(d->activeUndoTransaction); + d->activeUndoTransaction = nullptr; + + mUndoMap.erase(mUndoTransactions.back()->getID()); + delete mUndoTransactions.back(); + mUndoTransactions.pop_back(); + + } + + for(auto & obj : d->objectArray) { + if(obj->testStatus(ObjectStatus::PendingTransactionUpdate)) { + obj->onUndoRedoFinished(); + obj->setStatus(ObjectStatus::PendingTransactionUpdate,false); + } + } + + signalUndo(*this); // now signal the undo + + return true; + } + + return false; +} + +bool Document::redo(int id) +{ + if (d->iUndoMode) { + if(id) { + auto it = mRedoMap.find(id); + if(it == mRedoMap.end()) + return false; + while(!mRedoTransactions.empty() && mRedoTransactions.back()!=it->second) + redo(0); + } + + if (d->activeUndoTransaction) + _commitTransaction(true); + + assert(mRedoTransactions.size()!=0); + + // undo + d->activeUndoTransaction = new Transaction(mRedoTransactions.back()->getID()); + d->activeUndoTransaction->Name = mRedoTransactions.back()->Name; + + // do the redo + { + Base::FlagToggler flag(d->undoing); + mRedoTransactions.back()->apply(*this,true); + + mUndoMap[d->activeUndoTransaction->getID()] = d->activeUndoTransaction; + mUndoTransactions.push_back(d->activeUndoTransaction); + d->activeUndoTransaction = nullptr; + + mRedoMap.erase(mRedoTransactions.back()->getID()); + delete mRedoTransactions.back(); + mRedoTransactions.pop_back(); + } + + for(auto & obj : d->objectArray) { + if(obj->testStatus(ObjectStatus::PendingTransactionUpdate)) { + obj->onUndoRedoFinished(); + obj->setStatus(ObjectStatus::PendingTransactionUpdate,false); + } + } + + signalRedo(*this); + return true; + } + + return false; +} + +void Document::addOrRemovePropertyOfObject(TransactionalObject* obj, Property *prop, bool add) +{ + if (!prop || !obj || !obj->isAttachedToDocument()) + return; + if(d->iUndoMode && !isPerformingTransaction() && !d->activeUndoTransaction) { + if(!testStatus(Restoring) || testStatus(Importing)) { + int tid=0; + const char *name = GetApplication().getActiveTransaction(&tid); + if(name && tid>0) + _openTransaction(name,tid); + } + } + if (d->activeUndoTransaction && !d->rollback) + d->activeUndoTransaction->addOrRemoveProperty(obj, prop, add); +} + +bool Document::isPerformingTransaction() const +{ + return d->undoing || d->rollback; +} + +std::vector Document::getAvailableUndoNames() const +{ + std::vector vList; + if (d->activeUndoTransaction) + vList.push_back(d->activeUndoTransaction->Name); + for (std::list::const_reverse_iterator It=mUndoTransactions.rbegin();It!=mUndoTransactions.rend();++It) + vList.push_back((**It).Name); + return vList; +} + +std::vector Document::getAvailableRedoNames() const +{ + std::vector vList; + for (std::list::const_reverse_iterator It=mRedoTransactions.rbegin();It!=mRedoTransactions.rend();++It) + vList.push_back((**It).Name); + return vList; +} + +void Document::openTransaction(const char* name) { + if(isPerformingTransaction() || d->committing) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) + FC_WARN("Cannot open transaction while transacting"); + return; + } + + GetApplication().setActiveTransaction(name?name:""); +} + +int Document::_openTransaction(const char* name, int id) +{ + if(isPerformingTransaction() || d->committing) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) + FC_WARN("Cannot open transaction while transacting"); + return 0; + } + + if (d->iUndoMode) { + // Avoid recursive calls that is possible while + // clearing the redo transactions and will cause + // a double deletion of some transaction and thus + // a segmentation fault + if (d->opentransaction) + return 0; + Base::FlagToggler<> flag(d->opentransaction); + + if(id && mUndoMap.find(id)!=mUndoMap.end()) + throw Base::RuntimeError("invalid transaction id"); + if (d->activeUndoTransaction) + _commitTransaction(true); + _clearRedos(); + + d->activeUndoTransaction = new Transaction(id); + if (!name) + name = ""; + d->activeUndoTransaction->Name = name; + mUndoMap[d->activeUndoTransaction->getID()] = d->activeUndoTransaction; + id = d->activeUndoTransaction->getID(); + + signalOpenTransaction(*this, name); + + auto &app = GetApplication(); + auto activeDoc = app.getActiveDocument(); + if(activeDoc && + activeDoc!=this && + !activeDoc->hasPendingTransaction()) + { + std::string aname("-> "); + aname += d->activeUndoTransaction->Name; + FC_LOG("auto transaction " << getName() << " -> " << activeDoc->getName()); + activeDoc->_openTransaction(aname.c_str(),id); + } + return id; + } + return 0; +} + +void Document::renameTransaction(const char *name, int id) { + if(name && d->activeUndoTransaction && d->activeUndoTransaction->getID()==id) { + if(boost::starts_with(d->activeUndoTransaction->Name, "-> ")) + d->activeUndoTransaction->Name.resize(3); + else + d->activeUndoTransaction->Name.clear(); + d->activeUndoTransaction->Name += name; + } +} + +void Document::_checkTransaction(DocumentObject* pcDelObj, const Property *What, int line) +{ + // if the undo is active but no transaction open, open one! + if (d->iUndoMode && !isPerformingTransaction()) { + if (!d->activeUndoTransaction) { + if(!testStatus(Restoring) || testStatus(Importing)) { + int tid=0; + const char *name = GetApplication().getActiveTransaction(&tid); + if(name && tid>0) { + bool ignore = false; + if(What && What->testStatus(Property::NoModify)) + ignore = true; + if(FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { + if(What) + FC_LOG((ignore?"ignore":"auto") << " transaction (" + << line << ") '" << What->getFullName()); + else + FC_LOG((ignore?"ignore":"auto") <<" transaction (" + << line << ") '" << name << "' in " << getName()); + } + if(!ignore) + _openTransaction(name,tid); + return; + } + } + if(!pcDelObj) + return; + // When the object is going to be deleted we have to check if it has already been added to + // the undo transactions + std::list::iterator it; + for (it = mUndoTransactions.begin(); it != mUndoTransactions.end(); ++it) { + if ((*it)->hasObject(pcDelObj)) { + _openTransaction("Delete"); + break; + } + } + } + } +} + +void Document::_clearRedos() +{ + if(isPerformingTransaction() || d->committing) { + FC_ERR("Cannot clear redo while transacting"); + return; + } + + mRedoMap.clear(); + while (!mRedoTransactions.empty()) { + delete mRedoTransactions.back(); + mRedoTransactions.pop_back(); + } +} + +void Document::commitTransaction() { + if(isPerformingTransaction() || d->committing) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) + FC_WARN("Cannot commit transaction while transacting"); + return; + } + + if (d->activeUndoTransaction) + GetApplication().closeActiveTransaction(false,d->activeUndoTransaction->getID()); +} + +void Document::_commitTransaction(bool notify) +{ + if (isPerformingTransaction()) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) + FC_WARN("Cannot commit transaction while transacting"); + return; + } + else if (d->committing) { + // for a recursive call return without printing a warning + return; + } + + if (d->activeUndoTransaction) { + Base::FlagToggler<> flag(d->committing); + Application::TransactionSignaller signaller(false,true); + int id = d->activeUndoTransaction->getID(); + mUndoTransactions.push_back(d->activeUndoTransaction); + d->activeUndoTransaction = nullptr; + // check the stack for the limits + if(mUndoTransactions.size() > d->UndoMaxStackSize){ + mUndoMap.erase(mUndoTransactions.front()->getID()); + delete mUndoTransactions.front(); + mUndoTransactions.pop_front(); + } + signalCommitTransaction(*this); + + // closeActiveTransaction() may call again _commitTransaction() + if (notify) + GetApplication().closeActiveTransaction(false,id); + } +} + +void Document::abortTransaction() { + if(isPerformingTransaction() || d->committing) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) + FC_WARN("Cannot abort transaction while transacting"); + return; + } + if (d->activeUndoTransaction) + GetApplication().closeActiveTransaction(true,d->activeUndoTransaction->getID()); +} + +void Document::_abortTransaction() +{ + if(isPerformingTransaction() || d->committing) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) + FC_WARN("Cannot abort transaction while transacting"); + } + + if (d->activeUndoTransaction) { + Base::FlagToggler flag(d->rollback); + Application::TransactionSignaller signaller(true,true); + + // applying the so far made changes + d->activeUndoTransaction->apply(*this,false); + + // destroy the undo + mUndoMap.erase(d->activeUndoTransaction->getID()); + delete d->activeUndoTransaction; + d->activeUndoTransaction = nullptr; + signalAbortTransaction(*this); + } +} + +bool Document::hasPendingTransaction() const +{ + if (d->activeUndoTransaction) + return true; + else + return false; +} + +int Document::getTransactionID(bool undo, unsigned pos) const { + if(undo) { + if(d->activeUndoTransaction) { + if(pos == 0) + return d->activeUndoTransaction->getID(); + --pos; + } + if(pos>=mUndoTransactions.size()) + return 0; + auto rit = mUndoTransactions.rbegin(); + for(;pos;++rit,--pos) + continue; + return (*rit)->getID(); + } + if(pos>=mRedoTransactions.size()) + return 0; + auto rit = mRedoTransactions.rbegin(); + for(;pos;++rit,--pos); + return (*rit)->getID(); +} + +bool Document::isTransactionEmpty() const +{ + if (d->activeUndoTransaction) { + // Transactions are now only created when there are actual changes. + // Empty transaction is now significant for marking external changes. It + // is used to match ID with transactions in external documents and + // trigger undo/redo there. + + // return d->activeUndoTransaction->isEmpty(); + + return false; + } + + return true; +} + +void Document::clearDocument() +{ + d->activeObject = nullptr; + + if (!d->objectArray.empty()) { + GetApplication().signalDeleteDocument(*this); + d->clearDocument(); + GetApplication().signalNewDocument(*this,false); + } + + Base::FlagToggler<> flag(globalIsRestoring, false); + + setStatus(Document::PartialDoc,false); + + d->clearRecomputeLog(); + d->objectArray.clear(); + d->objectMap.clear(); + d->objectIdMap.clear(); + d->lastObjectId = 0; +} + + +void Document::clearUndos() +{ + if(isPerformingTransaction() || d->committing) { + FC_ERR("Cannot clear undos while transacting"); + return; + } + + if (d->activeUndoTransaction) + _commitTransaction(true); + + mUndoMap.clear(); + + // When cleaning up the undo stack we must delete the transactions from front + // to back because a document object can appear in several transactions but + // once removed from the document the object can never ever appear in any later + // transaction. Since the document object may be also deleted when the transaction + // is deleted we must make sure not access an object once it's destroyed. Thus, we + // go from front to back and not the other way round. + while (!mUndoTransactions.empty()) { + delete mUndoTransactions.front(); + mUndoTransactions.pop_front(); + } + //while (!mUndoTransactions.empty()) { + // delete mUndoTransactions.back(); + // mUndoTransactions.pop_back(); + //} + + _clearRedos(); +} + +int Document::getAvailableUndos(int id) const +{ + if(id) { + auto it = mUndoMap.find(id); + if(it == mUndoMap.end()) + return 0; + int i = 0; + if(d->activeUndoTransaction) { + ++i; + if(d->activeUndoTransaction->getID()==id) + return i; + } + auto rit = mUndoTransactions.rbegin(); + for(;rit!=mUndoTransactions.rend()&&*rit!=it->second;++rit) + ++i; + assert(rit!=mUndoTransactions.rend()); + return i+1; + } + if (d->activeUndoTransaction) + return static_cast(mUndoTransactions.size() + 1); + else + return static_cast(mUndoTransactions.size()); +} + +int Document::getAvailableRedos(int id) const +{ + if(id) { + auto it = mRedoMap.find(id); + if(it == mRedoMap.end()) + return 0; + int i = 0; + for(auto rit=mRedoTransactions.rbegin();*rit!=it->second;++rit) + ++i; + assert(i<(int)mRedoTransactions.size()); + return i+1; + } + return static_cast(mRedoTransactions.size()); +} + +void Document::setUndoMode(int iMode) +{ + if (d->iUndoMode && !iMode) + clearUndos(); + + d->iUndoMode = iMode; +} + +int Document::getUndoMode() const +{ + return d->iUndoMode; +} + +unsigned int Document::getUndoMemSize () const +{ + return d->UndoMemSize; +} + +void Document::setUndoLimit(unsigned int UndoMemSize) +{ + d->UndoMemSize = UndoMemSize; +} + +void Document::setMaxUndoStackSize(unsigned int UndoMaxStackSize) +{ + d->UndoMaxStackSize = UndoMaxStackSize; +} + +unsigned int Document::getMaxUndoStackSize()const +{ + return d->UndoMaxStackSize; +} + +void Document::onBeforeChange(const Property* prop) +{ + if(prop == &Label) + oldLabel = Label.getValue(); + signalBeforeChange(*this, *prop); +} + +void Document::onChanged(const Property* prop) +{ + signalChanged(*this, *prop); + + // the Name property is a label for display purposes + if (prop == &Label) { + Base::FlagToggler<> flag(globalIsRelabeling); + App::GetApplication().signalRelabelDocument(*this); + } else if(prop == &ShowHidden) { + App::GetApplication().signalShowHidden(*this); + } else if (prop == &Uid) { + std::string new_dir = getTransientDirectoryName(this->Uid.getValueStr(),this->FileName.getStrValue()); + std::string old_dir = this->TransientDir.getStrValue(); + Base::FileInfo TransDirNew(new_dir); + Base::FileInfo TransDirOld(old_dir); + // this directory should not exist + if (!TransDirNew.exists()) { + if (TransDirOld.exists()) { + if (!TransDirOld.renameFile(new_dir.c_str())) + Base::Console().Warning("Failed to rename '%s' to '%s'\n", old_dir.c_str(), new_dir.c_str()); + else + this->TransientDir.setValue(new_dir); + } + else { + if (!TransDirNew.createDirectories()) + Base::Console().Warning("Failed to create '%s'\n", new_dir.c_str()); + else + this->TransientDir.setValue(new_dir); + } + } + // when reloading an existing document the transient directory doesn't change + // so we must avoid to generate a new uuid + else if (TransDirNew.filePath() != TransDirOld.filePath()) { + // make sure that the uuid is unique + std::string uuid = this->Uid.getValueStr(); + Base::Uuid id; + Base::Console().Warning("Document with the UUID '%s' already exists, change to '%s'\n", + uuid.c_str(), id.getValue().c_str()); + // recursive call of onChanged() + this->Uid.setValue(id); + } + } +} + +void Document::onBeforeChangeProperty(const TransactionalObject *Who, const Property *What) +{ + if(Who->isDerivedFrom(App::DocumentObject::getClassTypeId())) + signalBeforeChangeObject(*static_cast(Who), *What); + if(!d->rollback && !globalIsRelabeling) { + _checkTransaction(nullptr, What, __LINE__); + if (d->activeUndoTransaction) + d->activeUndoTransaction->addObjectChange(Who, What); + } +} + +void Document::onChangedProperty(const DocumentObject *Who, const Property *What) +{ + signalChangedObject(*Who, *What); +} + +void Document::setTransactionMode(int iMode) +{ + d->iTransactionMode = iMode; +} + +//-------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------- +Document::Document(const char* documentName) + : myName(documentName) +{ + // Remark: In a constructor we should never increment a Python object as we cannot be sure + // if the Python interpreter gets a reference of it. E.g. if we increment but Python don't + // get a reference then the object wouldn't get deleted in the destructor. + // 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. + d = new DocumentP; + d->DocumentPythonObject = Py::Object(new DocumentPy(this), true); + +#ifdef FC_LOGUPDATECHAIN + Console().Log("+App::Document: %p\n", this); +#endif + std::string CreationDateString = Base::TimeInfo::currentDateTimeString(); + std::string Author = App::GetApplication() + .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document") + ->GetASCII("prefAuthor", ""); + std::string AuthorComp = + App::GetApplication() + .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document") + ->GetASCII("prefCompany", ""); + ADD_PROPERTY_TYPE(Label, ("Unnamed"), 0, Prop_None, "The name of the document"); + ADD_PROPERTY_TYPE(FileName, + (""), + 0, + PropertyType(Prop_Transient | Prop_ReadOnly), + "The path to the file where the document is saved to"); + ADD_PROPERTY_TYPE(CreatedBy, (Author.c_str()), 0, Prop_None, "The creator of the document"); + ADD_PROPERTY_TYPE( + CreationDate, (CreationDateString.c_str()), 0, Prop_ReadOnly, "Date of creation"); + ADD_PROPERTY_TYPE(LastModifiedBy, (""), 0, Prop_None, 0); + ADD_PROPERTY_TYPE(LastModifiedDate, ("Unknown"), 0, Prop_ReadOnly, "Date of last modification"); + ADD_PROPERTY_TYPE(Company, + (AuthorComp.c_str()), + 0, + Prop_None, + "Additional tag to save the name of the company"); + ADD_PROPERTY_TYPE(Comment, (""), 0, Prop_None, "Additional tag to save a comment"); + ADD_PROPERTY_TYPE(Meta, (), 0, Prop_None, "Map with additional meta information"); + ADD_PROPERTY_TYPE(Material, (), 0, Prop_None, "Map with material properties"); + // create the uuid for the document + Base::Uuid id; + ADD_PROPERTY_TYPE(Id, (""), 0, Prop_None, "ID of the document"); + ADD_PROPERTY_TYPE(Uid, (id), 0, Prop_ReadOnly, "UUID of the document"); + + // license stuff + auto paramGrp {App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Document")}; + auto index = static_cast(paramGrp->GetInt("prefLicenseType", 0)); + const char* name = ""; + const char* url = ""; + std::string licenseUrl = ""; + if (index >= 0 && index < App::countOfLicenses) { + name = App::licenseItems.at(index).at(App::posnOfFullName); + url = App::licenseItems.at(index).at(App::posnOfUrl); + licenseUrl = (paramGrp->GetASCII("prefLicenseUrl", url)); + } + ADD_PROPERTY_TYPE(License, (name), 0, Prop_None, "License string of the Item"); + ADD_PROPERTY_TYPE( + LicenseURL, (licenseUrl.c_str()), 0, Prop_None, "URL to the license text/contract"); + ADD_PROPERTY_TYPE(ShowHidden, + (false), + 0, + PropertyType(Prop_None), + "Whether to show hidden object items in the tree view"); + + // this creates and sets 'TransientDir' in onChanged() + ADD_PROPERTY_TYPE(TransientDir, + (""), + 0, + PropertyType(Prop_Transient | Prop_ReadOnly), + "Transient directory, where the files live while the document is open"); + ADD_PROPERTY_TYPE( + Tip, (nullptr), 0, PropertyType(Prop_Transient), "Link of the tip object of the document"); + ADD_PROPERTY_TYPE(TipName, + (""), + 0, + PropertyType(Prop_Hidden | Prop_ReadOnly), + "Link of the tip object of the document"); + Uid.touch(); +} + +Document::~Document() +{ +#ifdef FC_LOGUPDATECHAIN + Console().Log("-App::Document: %s %p\n",getName(), this); +#endif + + try { + clearUndos(); + } + catch (const boost::exception&) { + } + +#ifdef FC_LOGUPDATECHAIN + Console().Log("-Delete Features of %s \n",getName()); +#endif + + d->clearDocument(); + + // Remark: The API of Py::Object has been changed to set whether the wrapper owns the passed + // Python object or not. In the constructor we forced the wrapper to own the object so we need + // not to dec'ref the Python object any more. + // But we must still invalidate the Python object because it doesn't need to be + // destructed right now because the interpreter can own several references to it. + Base::PyGILStateLocker lock; + Base::PyObjectBase* doc = static_cast(d->DocumentPythonObject.ptr()); + // Call before decrementing the reference counter, otherwise a heap error can occur + doc->setInvalid(); + + // remove Transient directory + try { + Base::FileInfo TransDir(TransientDir.getValue()); + TransDir.deleteDirectoryRecursive(); + } + catch (const Base::Exception& e) { + std::cerr << "Removing transient directory failed: " << e.what() << std::endl; + } + delete d; +} + +std::string Document::getTransientDirectoryName(const std::string& uuid, const std::string& filename) const +{ + // Create a directory name of the form: {ExeName}_Doc_{UUID}_{HASH}_{PID} + std::stringstream s; + QCryptographicHash hash(QCryptographicHash::Sha1); +#if QT_VERSION < QT_VERSION_CHECK(6, 3, 0) + hash.addData(filename.c_str(), filename.size()); +#else + hash.addData(QByteArrayView(filename.c_str(), filename.size())); +#endif + s << App::Application::getUserCachePath() << App::Application::getExecutableName() + << "_Doc_" << uuid + << "_" << hash.result().toHex().left(6).constData() + << "_" << QCoreApplication::applicationPid(); + return s.str(); +} + +//-------------------------------------------------------------------------- +// Exported functions +//-------------------------------------------------------------------------- + +void Document::Save (Base::Writer &writer) const +{ + writer.Stream() << "" << endl; + + PropertyContainer::Save(writer); + + // writing the features types + writeObjects(d->objectArray, writer); + writer.Stream() << "" << endl; +} + +void Document::Restore(Base::XMLReader &reader) +{ + int i,Cnt; + d->touchedObjs.clear(); + setStatus(Document::PartialDoc,false); + + reader.readElement("Document"); + long scheme = reader.getAttributeAsInteger("SchemaVersion"); + reader.DocumentSchema = scheme; + if (reader.hasAttribute("ProgramVersion")) { + reader.ProgramVersion = reader.getAttribute("ProgramVersion"); + } else { + reader.ProgramVersion = "pre-0.14"; + } + if (reader.hasAttribute("FileVersion")) { + reader.FileVersion = reader.getAttributeAsUnsigned("FileVersion"); + } else { + reader.FileVersion = 0; + } + + // When this document was created the FileName and Label properties + // were set to the absolute path or file name, respectively. To save + // the document to the file it was loaded from or to show the file name + // in the tree view we must restore them after loading the file because + // they will be overridden. + // Note: This does not affect the internal name of the document in any way + // that is kept in Application. + std::string FilePath = FileName.getValue(); + std::string DocLabel = Label.getValue(); + + // read the Document Properties, when reading in Uid the transient directory gets renamed automatically + PropertyContainer::Restore(reader); + + // We must restore the correct 'FileName' property again because the stored + // value could be invalid. + FileName.setValue(FilePath.c_str()); + Label.setValue(DocLabel.c_str()); + + // SchemeVersion "2" + if ( scheme == 2 ) { + // read the feature types + reader.readElement("Features"); + Cnt = reader.getAttributeAsInteger("Count"); + for (i=0 ;isetStatus(ObjectStatus::Restore, true); + pObj->Restore(reader); + pObj->setStatus(ObjectStatus::Restore, false); + } + reader.readEndElement("Feature"); + } + reader.readEndElement("FeatureData"); + } // SchemeVersion "3" or higher + else if ( scheme >= 3 ) { + // read the feature types + readObjects(reader); + + // tip object handling. First the whole document has to be read, then we + // can restore the Tip link out of the TipName Property: + Tip.setValue(getObject(TipName.getValue())); + } + + reader.readEndElement("Document"); +} + +struct DocExportStatus { + Document::ExportStatus status; + std::set objs; +}; + +static DocExportStatus _ExportStatus; + +// Exception-safe exporting status setter +class DocumentExporting { +public: + explicit DocumentExporting(const std::vector &objs) { + _ExportStatus.status = Document::Exporting; + _ExportStatus.objs.insert(objs.begin(),objs.end()); + } + + ~DocumentExporting() { + _ExportStatus.status = Document::NotExporting; + _ExportStatus.objs.clear(); + } +}; + +// The current implementation choose to use a static variable for exporting +// status because we can be exporting multiple objects from multiple documents +// at the same time. I see no benefits in distinguish which documents are +// exporting, so just use a static variable for global status. But the +// implementation can easily be changed here if necessary. +Document::ExportStatus Document::isExporting(const App::DocumentObject *obj) const { + if(_ExportStatus.status!=Document::NotExporting && + (!obj || _ExportStatus.objs.find(obj)!=_ExportStatus.objs.end())) + return _ExportStatus.status; + return Document::NotExporting; +} + +void Document::exportObjects(const std::vector& obj, std::ostream& out) { + + DocumentExporting exporting(obj); + + if(FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { + for(auto o : obj) { + if(o && o->getNameInDocument()) { + FC_LOG("exporting " << o->getFullName()); + if (!o->getPropertyByName("_ObjectUUID")) { + auto prop = static_cast(o->addDynamicProperty( + "App::PropertyUUID", "_ObjectUUID", nullptr, nullptr, + Prop_Output | Prop_Hidden)); + prop->setValue(Base::Uuid::createUuid()); + } + } + } + } + + Base::ZipWriter writer(out); + writer.putNextEntry("Document.xml"); + writer.Stream() << "" << endl; + writer.Stream() << "" << endl; + // Add this block to have the same layout as for normal documents + writer.Stream() << "" << endl; + writer.Stream() << "" << endl; + + // writing the object types + writeObjects(obj, writer); + writer.Stream() << "" << endl; + + // Hook for others to add further data. + signalExportObjects(obj, writer); + + // write additional files + writer.writeFiles(); +} + +#define FC_ATTR_DEPENDENCIES "Dependencies" +#define FC_ELEMENT_OBJECT_DEPS "ObjectDeps" +#define FC_ATTR_DEP_COUNT "Count" +#define FC_ATTR_DEP_OBJ_NAME "Name" +#define FC_ATTR_DEP_ALLOW_PARTIAL "AllowPartial" +#define FC_ELEMENT_OBJECT_DEP "Dep" + +void Document::writeObjects(const std::vector& obj, + Base::Writer &writer) const +{ + // writing the features types + writer.incInd(); // indentation for 'Objects count' + writer.Stream() << writer.ind() << "" << endl; + + writer.incInd(); // indentation for 'Object type' + + if(!isExporting(nullptr)) { + for(auto o : obj) { + const auto &outList = o->getOutList(DocumentObject::OutListNoHidden + | DocumentObject::OutListNoXLinked); + writer.Stream() << writer.ind() + << "<" FC_ELEMENT_OBJECT_DEPS " " FC_ATTR_DEP_OBJ_NAME "=\"" + << o->getNameInDocument() << "\" " FC_ATTR_DEP_COUNT "=\"" << outList.size(); + if(outList.empty()) { + writer.Stream() << "\"/>" << endl; + continue; + } + int partial = o->canLoadPartial(); + if(partial>0) + writer.Stream() << "\" " FC_ATTR_DEP_ALLOW_PARTIAL << "=\"" << partial; + writer.Stream() << "\">" << endl; + writer.incInd(); + for(auto dep : outList) { + auto name = dep?dep->getNameInDocument():""; + writer.Stream() << writer.ind() << "<" FC_ELEMENT_OBJECT_DEP " " + FC_ATTR_DEP_OBJ_NAME "=\"" << (name?name:"") << "\"/>" << endl; + } + writer.decInd(); + writer.Stream() << writer.ind() << "" << endl; + } + } + + std::vector::const_iterator it; + for (it = obj.begin(); it != obj.end(); ++it) { + writer.Stream() << writer.ind() << "getTypeId().getName() << "\" " + << "name=\"" << (*it)->getExportName() << "\" " + << "id=\"" << (*it)->getID() << "\" "; + + // Only write out custom view provider types + std::string viewType = (*it)->getViewProviderNameStored(); + if (viewType != (*it)->getViewProviderName()) + writer.Stream() << "ViewType=\"" << viewType << "\" "; + + // See DocumentObjectPy::getState + if ((*it)->testStatus(ObjectStatus::Touch)) + writer.Stream() << "Touched=\"1\" "; + if ((*it)->testStatus(ObjectStatus::Error)) { + writer.Stream() << "Invalid=\"1\" "; + auto desc = getErrorDescription(*it); + if(desc) + writer.Stream() << "Error=\"" << Property::encodeAttribute(desc) << "\" "; + } + writer.Stream() << "/>" << endl; + } + + writer.decInd(); // indentation for 'Object type' + writer.Stream() << writer.ind() << "" << endl; + + // writing the features itself + writer.Stream() << writer.ind() << "" << endl; + + writer.incInd(); // indentation for 'Object name' + for (it = obj.begin(); it != obj.end(); ++it) { + writer.Stream() << writer.ind() << "getExportName() << "\""; + if((*it)->hasExtensions()) + writer.Stream() << " Extensions=\"True\""; + + writer.Stream() << ">" << endl; + (*it)->Save(writer); + writer.Stream() << writer.ind() << "" << endl; + } + + writer.decInd(); // indentation for 'Object name' + writer.Stream() << writer.ind() << "" << endl; + writer.decInd(); // indentation for 'Objects count' +} + +struct DepInfo { + std::unordered_set deps; + int canLoadPartial = 0; +}; + +static void _loadDeps(const std::string &name, + std::unordered_map &objs, + const std::unordered_map &deps) +{ + auto it = deps.find(name); + if(it == deps.end()) { + objs.emplace(name,true); + return; + } + if(it->second.canLoadPartial) { + if(it->second.canLoadPartial == 1) { + // canLoadPartial==1 means all its children will be created but not + // restored, i.e. exists as if newly created object, and therefore no + // need to load dependency of the children + for(auto &dep : it->second.deps) + objs.emplace(dep,false); + objs.emplace(name,true); + }else + objs.emplace(name,false); + return; + } + objs[name] = true; + // If cannot load partial, then recurse to load all children dependency + for(auto &dep : it->second.deps) { + auto it = objs.find(dep); + if(it!=objs.end() && it->second) + continue; + _loadDeps(dep,objs,deps); + } +} + +std::vector +Document::readObjects(Base::XMLReader& reader) +{ + d->touchedObjs.clear(); + bool keepDigits = testStatus(Document::KeepTrailingDigits); + setStatus(Document::KeepTrailingDigits, !reader.doNameMapping()); + std::vector objs; + + + // read the object types + reader.readElement("Objects"); + int Cnt = reader.getAttributeAsInteger("Count"); + + if(!reader.hasAttribute(FC_ATTR_DEPENDENCIES)) + d->partialLoadObjects.clear(); + else if(!d->partialLoadObjects.empty()) { + std::unordered_map deps; + for (int i=0 ;i objs; + objs.reserve(d->partialLoadObjects.size()); + for(auto &v : d->partialLoadObjects) + objs.emplace_back(v.first.c_str()); + for(auto &name : objs) + _loadDeps(name,d->partialLoadObjects,deps); + if(Cnt > (int)d->partialLoadObjects.size()) + setStatus(Document::PartialDoc,true); + else { + for(auto &v : d->partialLoadObjects) { + if(!v.second) { + setStatus(Document::PartialDoc,true); + break; + } + } + if(!testStatus(Document::PartialDoc)) + d->partialLoadObjects.clear(); + } + } + + long lastId = 0; + for (int i=0 ;ipartialLoadObjects.empty()) { + auto it = d->partialLoadObjects.find(name); + if(it == d->partialLoadObjects.end()) + continue; + partial = !it->second; + } + + if(!testStatus(Status::Importing) && reader.hasAttribute("id")) { + // if not importing, then temporary reset lastObjectId and make the + // following addObject() generate the correct id for this object. + d->lastObjectId = reader.getAttributeAsInteger("id")-1; + } + + // To prevent duplicate name when export/import of objects from + // external documents, we append those external object name with + // @. Before importing (here means we are called by + // importObjects), we shall strip the postfix. What the caller + // (MergeDocument) sees is still the unstripped name mapped to a new + // internal name, and the rest of the link properties will be able to + // correctly unmap the names. + auto pos = name.find('@'); + std::string _obj_name; + const char *obj_name; + if(pos!=std::string::npos) { + _obj_name = name.substr(0,pos); + obj_name = _obj_name.c_str(); + }else + obj_name = name.c_str(); + + try { + // Use name from XML as is and do NOT remove trailing digits because + // otherwise we may cause a dependency to itself + // Example: Object 'Cut001' references object 'Cut' and removing the + // digits we make an object 'Cut' referencing itself. + App::DocumentObject* obj = addObject(type.c_str(), obj_name, /*isNew=*/ false, viewType.c_str(), partial); + if (obj) { + if(lastId < obj->_Id) + lastId = obj->_Id; + objs.push_back(obj); + // use this name for the later access because an object with + // the given name may already exist + reader.addName(name.c_str(), obj->getNameInDocument()); + + // restore touch/error status flags + if (reader.hasAttribute("Touched")) { + if(reader.getAttributeAsInteger("Touched") != 0) + d->touchedObjs.insert(obj); + } + if (reader.hasAttribute("Invalid")) { + obj->setStatus(ObjectStatus::Error, reader.getAttributeAsInteger("Invalid") != 0); + if(obj->isError() && reader.hasAttribute("Error")) + d->addRecomputeLog(reader.getAttribute("Error"),obj); + } + } + } + catch (const Base::Exception& e) { + Base::Console().Error("Cannot create object '%s': (%s)\n", name.c_str(), e.what()); + } + } + if(!testStatus(Status::Importing)) + d->lastObjectId = lastId; + + reader.readEndElement("Objects"); + setStatus(Document::KeepTrailingDigits, keepDigits); + + // read the features itself + reader.clearPartialRestoreDocumentObject(); + reader.readElement("ObjectData"); + Cnt = reader.getAttributeAsInteger("Count"); + for (int i=0 ;itestStatus(App::PartialObject)) { // check if this feature has been registered + pObj->setStatus(ObjectStatus::Restore, true); + try { + FC_TRACE("restoring " << pObj->getFullName()); + pObj->Restore(reader); + } + // Try to continue only for certain exception types if not handled + // by the feature type. For all other exception types abort the process. + catch (const Base::UnicodeError &e) { + e.ReportException(); + } + catch (const Base::ValueError &e) { + e.ReportException(); + } + catch (const Base::IndexError &e) { + e.ReportException(); + } + catch (const Base::RuntimeError &e) { + e.ReportException(); + } + catch (const Base::XMLAttributeError &e) { + e.ReportException(); + } + + pObj->setStatus(ObjectStatus::Restore, false); + + if (reader.testStatus(Base::XMLReader::ReaderStatus::PartialRestoreInDocumentObject)) { + Base::Console().Error("Object \"%s\" was subject to a partial restore. As a result geometry may have changed or be incomplete.\n",name.c_str()); + reader.clearPartialRestoreDocumentObject(); + } + } + reader.readEndElement("Object"); + } + reader.readEndElement("ObjectData"); + + return objs; +} + +void Document::addRecomputeObject(DocumentObject *obj) { + if(testStatus(Status::Restoring) && obj) { + d->touchedObjs.insert(obj); + obj->touch(); + } +} + +std::vector +Document::importObjects(Base::XMLReader& reader) +{ + Base::FlagToggler<> flag(globalIsRestoring, false); + Base::ObjectStatusLocker restoreBit(Status::Restoring, this); + Base::ObjectStatusLocker restoreBit2(Status::Importing, this); + ExpressionParser::ExpressionImporter expImporter(reader); + reader.readElement("Document"); + long scheme = reader.getAttributeAsInteger("SchemaVersion"); + reader.DocumentSchema = scheme; + if (reader.hasAttribute("ProgramVersion")) { + reader.ProgramVersion = reader.getAttribute("ProgramVersion"); + } else { + reader.ProgramVersion = "pre-0.14"; + } + if (reader.hasAttribute("FileVersion")) { + reader.FileVersion = reader.getAttributeAsUnsigned("FileVersion"); + } else { + reader.FileVersion = 0; + } + + std::vector objs = readObjects(reader); + for(auto o : objs) { + if(o && o->getNameInDocument()) { + o->setStatus(App::ObjImporting,true); + FC_LOG("importing " << o->getFullName()); + if (auto propUUID = Base::freecad_dynamic_cast( + o->getPropertyByName("_ObjectUUID"))) + { + auto propSource = Base::freecad_dynamic_cast( + o->getPropertyByName("_SourceUUID")); + if (!propSource) + propSource = static_cast(o->addDynamicProperty( + "App::PropertyUUID", "_SourceUUID", nullptr, nullptr, + Prop_Output | Prop_Hidden)); + if (propSource) + propSource->setValue(propUUID->getValue()); + propUUID->setValue(Base::Uuid::createUuid()); + } + } + } + + reader.readEndElement("Document"); + + signalImportObjects(objs, reader); + afterRestore(objs,true); + + signalFinishImportObjects(objs); + + for(auto o : objs) { + if(o && o->getNameInDocument()) + o->setStatus(App::ObjImporting,false); + } + + return objs; +} + +unsigned int Document::getMemSize () const +{ + unsigned int size = 0; + + // size of the DocObjects in the document + std::vector::const_iterator it; + for (it = d->objectArray.begin(); it != d->objectArray.end(); ++it) + size += (*it)->getMemSize(); + + // size of the document properties... + size += PropertyContainer::getMemSize(); + + // Undo Redo size + size += getUndoMemSize(); + + return size; +} + +static std::string checkFileName(const char *file) { + std::string fn(file); + + // Append extension if missing. This option is added for security reason, so + // that the user won't accidentally overwrite other file that may be critical. + if(App::GetApplication().GetParameterGroupByPath + ("User parameter:BaseApp/Preferences/Document")->GetBool("CheckExtension",true)) + { + const char *ext = strrchr(file,'.'); + if(!ext || !boost::iequals(ext+1,"fcstd")) { + if(ext && ext[1] == 0) + fn += "FCStd"; + else + fn += ".FCStd"; + } + } + return fn; +} + +bool Document::saveAs(const char* _file) +{ + std::string file = checkFileName(_file); + Base::FileInfo fi(file.c_str()); + if (this->FileName.getStrValue() != file) { + this->FileName.setValue(file); + this->Label.setValue(fi.fileNamePure()); + this->Uid.touch(); // this forces a rename of the transient directory + } + + return save(); +} + +bool Document::saveCopy(const char* _file) const +{ + std::string file = checkFileName(_file); + if (this->FileName.getStrValue() != file) { + bool result = saveToFile(file.c_str()); + return result; + } + return false; +} + +// Save the document under the name it has been opened +bool Document::save () +{ + if(testStatus(Document::PartialDoc)) { + FC_ERR("Partial loaded document '" << Label.getValue() << "' cannot be saved"); + // TODO We don't make this a fatal error and return 'true' to make it possible to + // save other documents that depends on this partial opened document. We need better + // handling to avoid touching partial documents. + return true; + } + + if (*(FileName.getValue()) != '\0') { + // Save the name of the tip object in order to handle in Restore() + if (Tip.getValue()) { + TipName.setValue(Tip.getValue()->getNameInDocument()); + } + + std::string LastModifiedDateString = Base::TimeInfo::currentDateTimeString(); + LastModifiedDate.setValue(LastModifiedDateString.c_str()); + // set author if needed + bool saveAuthor = App::GetApplication().GetParameterGroupByPath + ("User parameter:BaseApp/Preferences/Document")->GetBool("prefSetAuthorOnSave",false); + if (saveAuthor) { + std::string Author = App::GetApplication().GetParameterGroupByPath + ("User parameter:BaseApp/Preferences/Document")->GetASCII("prefAuthor",""); + LastModifiedBy.setValue(Author.c_str()); + } + + return saveToFile(FileName.getValue()); + } + + return false; +} + +namespace App { +// Helper class to handle different backup policies +class BackupPolicy { +public: + enum Policy { + Standard, + TimeStamp + }; + BackupPolicy() { + policy = Standard; + numberOfFiles = 1; + useFCBakExtension = true; + saveBackupDateFormat = "%Y%m%d-%H%M%S"; + } + ~BackupPolicy() = default; + void setPolicy(Policy p) { + policy = p; + } + void setNumberOfFiles(int count) { + numberOfFiles = count; + } + void useBackupExtension(bool on) { + useFCBakExtension = on; + } + void setDateFormat(const std::string& fmt) { + saveBackupDateFormat = fmt; + } + void apply(const std::string& sourcename, const std::string& targetname) { + switch (policy) { + case Standard: + applyStandard(sourcename, targetname); + break; + case TimeStamp: + applyTimeStamp(sourcename, targetname); + break; + } + } + +private: + void applyStandard(const std::string& sourcename, const std::string& targetname) { + // if saving the project data succeeded rename to the actual file name + Base::FileInfo fi(targetname); + if (fi.exists()) { + if (numberOfFiles > 0) { + int nSuff = 0; + std::string fn = fi.fileName(); + Base::FileInfo di(fi.dirPath()); + std::vector backup; + std::vector files = di.getDirectoryContent(); + for (std::vector::iterator it = files.begin(); it != files.end(); ++it) { + std::string file = it->fileName(); + if (file.substr(0,fn.length()) == fn) { + // starts with the same file name + std::string suf(file.substr(fn.length())); + if (!suf.empty()) { + std::string::size_type nPos = suf.find_first_not_of("0123456789"); + if (nPos==std::string::npos) { + // store all backup files + backup.push_back(*it); + nSuff = std::max(nSuff, std::atol(suf.c_str())); + } + } + } + } + + if (!backup.empty() && (int)backup.size() >= numberOfFiles) { + // delete the oldest backup file we found + Base::FileInfo del = backup.front(); + for (std::vector::iterator it = backup.begin(); it != backup.end(); ++it) { + if (it->lastModified() < del.lastModified()) + del = *it; + } + + del.deleteFile(); + fn = del.filePath(); + } + else { + // create a new backup file + std::stringstream str; + str << fi.filePath() << (nSuff + 1); + fn = str.str(); + } + + if (!fi.renameFile(fn.c_str())) + Base::Console().Warning("Cannot rename project file to backup file\n"); + } + else { + fi.deleteFile(); + } + } + + Base::FileInfo tmp(sourcename); + if (!tmp.renameFile(targetname.c_str())) { + throw Base::FileException( + "Cannot rename tmp save file to project file", targetname); + } + } + void applyTimeStamp(const std::string& sourcename, const std::string& targetname) { + Base::FileInfo fi(targetname); + + std::string fn = sourcename; + std::string ext = fi.extension(); + std::string bn; // full path with no extension but with "." + std::string pbn; // base name of the project + "." + if (!ext.empty()) { + bn = fi.filePath().substr(0, fi.filePath().length() - ext.length()); + pbn = fi.fileName().substr(0, fi.fileName().length() - ext.length()); + } + else { + bn = fi.filePath() + "."; + pbn = fi.fileName() + "."; + } + + bool backupManagementError = false; // Note error and report at the end + if (fi.exists()) { + if (numberOfFiles > 0) { + // replace . by - in format to avoid . between base name and extension + boost::replace_all(saveBackupDateFormat, ".", "-"); + { + // Remove all extra backups + std::string fn = fi.fileName(); + Base::FileInfo di(fi.dirPath()); + std::vector backup; + std::vector files = di.getDirectoryContent(); + for (std::vector::iterator it = files.begin(); it != files.end(); ++it) { + if (it->isFile()) { + std::string file = it->fileName(); + std::string fext = it->extension(); + std::string fextUp = fext; + std::transform(fextUp.begin(), fextUp.end(), fextUp.begin(),(int (*)(int))toupper); + // re-enforcing identification of the backup file + + + // old case : the name starts with the full name of the project and follows with numbers + if ((startsWith(file, fn) && + (file.length() > fn.length()) && + checkDigits(file.substr(fn.length()))) || + // .FCBak case : The bame starts with the base name of the project + "." + // + complement with no "." + ".FCBak" + ((fextUp == "FCBAK") && startsWith(file, pbn) && + (checkValidComplement(file, pbn, fext)))) { + backup.push_back(*it); + } + } + } + + if (!backup.empty() && (int)backup.size() >= numberOfFiles) { + std::sort (backup.begin(), backup.end(), fileComparisonByDate); + // delete the oldest backup file we found + // Base::FileInfo del = backup.front(); + int nb = 0; + for (std::vector::iterator it = backup.begin(); it != backup.end(); ++it) { + nb++; + if (nb >= numberOfFiles) { + try { + if (!it->deleteFile()) { + backupManagementError = true; + Base::Console().Warning("Cannot remove backup file : %s\n", it->fileName().c_str()); + } + } + catch (...) { + backupManagementError = true; + Base::Console().Warning("Cannot remove backup file : %s\n", it->fileName().c_str()); + } + } + } + + } + } //end remove backup + + // create a new backup file + { + int ext = 1; + if (useFCBakExtension) { + std::stringstream str; + Base::TimeInfo ti = fi.lastModified(); + time_t s =ti.getSeconds(); + struct tm * timeinfo = localtime(& s); + char buffer[100]; + + strftime(buffer,sizeof(buffer),saveBackupDateFormat.c_str(),timeinfo); + str << bn << buffer ; + + fn = str.str(); + bool done = false; + + if ((fn.empty()) || (fn[fn.length()-1] == ' ') || (fn[fn.length()-1] == '-')) { + if (fn[fn.length()-1] == ' ') { + fn = fn.substr(0,fn.length()-1); + } + } + else { + if (!renameFileNoErase(fi, fn+".FCBak")) { + fn = fn + "-"; + } + else { + done = true; + } + } + + if (!done) { + while (ext < numberOfFiles + 10) { + if (renameFileNoErase(fi, fn+std::to_string(ext)+".FCBak")) + break; + ext++; + } + } + } + else { + // changed but simpler and solves also the delay sometimes introduced by google drive + while (ext < numberOfFiles + 10) { + // linux just replace the file if exists, and then the existence is to be tested before rename + if (renameFileNoErase(fi, fi.filePath()+std::to_string(ext))) + break; + ext++; + } + } + + if (ext >= numberOfFiles + 10) { + Base::Console().Error("File not saved: Cannot rename project file to backup file\n"); + //throw Base::FileException("File not saved: Cannot rename project file to backup file", fi); + } + } + } + else { + try { + fi.deleteFile(); + } + catch (...) { + Base::Console().Warning("Cannot remove backup file: %s\n", fi.fileName().c_str()); + backupManagementError = true; + } + } + } + + Base::FileInfo tmp(sourcename); + if (!tmp.renameFile(targetname.c_str())) { + throw Base::FileException( + "Save interrupted: Cannot rename temporary file to project file", tmp); + } + + if (backupManagementError) { + throw Base::FileException("Warning: Save complete, but error while managing backup history.", fi); + } + } + static bool fileComparisonByDate(const Base::FileInfo& i, + const Base::FileInfo& j) { + return (i.lastModified()>j.lastModified()); + } + bool startsWith(const std::string& st1, + const std::string& st2) const { + return st1.substr(0,st2.length()) == st2; + } + bool checkValidString (const std::string& cmpl, const boost::regex& e) const { + boost::smatch what; + bool res = boost::regex_search (cmpl,what,e); + return res; + } + bool checkValidComplement(const std::string& file, const std::string& pbn, const std::string& ext) const { + std::string cmpl = file.substr(pbn.length(),file.length()- pbn.length() - ext.length()-1); + boost::regex e (R"(^[^.]*$)"); + return checkValidString(cmpl,e); + } + bool checkDigits (const std::string& cmpl) const { + boost::regex e (R"(^[0-9]*$)"); + return checkValidString(cmpl,e); + } + bool renameFileNoErase(Base::FileInfo fi, const std::string& newName) { + // linux just replaces the file if it exists, so the existence is to be tested before rename + Base::FileInfo nf(newName); + if (!nf.exists()) { + return fi.renameFile(newName.c_str()); + } + return false; + } + +private: + Policy policy; + int numberOfFiles; + bool useFCBakExtension; + std::string saveBackupDateFormat; +}; +} + +bool Document::saveToFile(const char* filename) const +{ + signalStartSave(*this, filename); + + auto hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document"); + int compression = hGrp->GetInt("CompressionLevel",3); + compression = Base::clamp(compression, Z_NO_COMPRESSION, Z_BEST_COMPRESSION); + + bool policy = App::GetApplication().GetParameterGroupByPath + ("User parameter:BaseApp/Preferences/Document")->GetBool("BackupPolicy",true); + + auto canonical_path = [](const char* filename) { + try { +#ifdef FC_OS_WIN32 + QString utf8Name = QString::fromUtf8(filename); + auto realpath = fs::weakly_canonical(fs::absolute(fs::path(utf8Name.toStdWString()))); + std::string nativePath = QString::fromStdWString(realpath.native()).toStdString(); +#else + auto realpath = fs::weakly_canonical(fs::absolute(fs::path(filename))); + std::string nativePath = realpath.native(); +#endif + // In case some folders in the path do not exist + auto parentPath = realpath.parent_path(); + fs::create_directories(parentPath); + + return nativePath; + } + catch (const std::exception&) { +#ifdef FC_OS_WIN32 + QString utf8Name = QString::fromUtf8(filename); + auto parentPath = fs::absolute(fs::path(utf8Name.toStdWString())).parent_path(); +#else + auto parentPath = fs::absolute(fs::path(filename)).parent_path(); +#endif + fs::create_directories(parentPath); + + return std::string(filename); + } + }; + + //realpath is canonical filename i.e. without symlink + std::string nativePath = canonical_path(filename); + + // make a tmp. file where to save the project data first and then rename to + // the actual file name. This may be useful if overwriting an existing file + // fails so that the data of the work up to now isn't lost. + std::string uuid = Base::Uuid::createUuid(); + std::string fn = nativePath; + if (policy) { + fn += "."; + fn += uuid; + } + Base::FileInfo tmp(fn); + + + // open extra scope to close ZipWriter properly + { + Base::ofstream file(tmp, std::ios::out | std::ios::binary); + Base::ZipWriter writer(file); + if (!file.is_open()) { + throw Base::FileException("Failed to open file", tmp); + } + + writer.setComment("FreeCAD Document"); + writer.setLevel(compression); + writer.putNextEntry("Document.xml"); + + if (hGrp->GetBool("SaveBinaryBrep", false)) + writer.setMode("BinaryBrep"); + + writer.Stream() << "" << endl + << "" << endl; + Document::Save(writer); + + // Special handling for Gui document. + signalSaveDocument(writer); + + // write additional files + writer.writeFiles(); + + if (writer.hasErrors()) { + throw Base::FileException("Failed to write all data to file", tmp); + } + + GetApplication().signalSaveDocument(*this); + } + + if (policy) { + // if saving the project data succeeded rename to the actual file name + int count_bak = App::GetApplication().GetParameterGroupByPath + ("User parameter:BaseApp/Preferences/Document")->GetInt("CountBackupFiles",1); + bool backup = App::GetApplication().GetParameterGroupByPath + ("User parameter:BaseApp/Preferences/Document")->GetBool("CreateBackupFiles",true); + if (!backup) { + count_bak = -1; + } + bool useFCBakExtension = App::GetApplication().GetParameterGroupByPath + ("User parameter:BaseApp/Preferences/Document")->GetBool("UseFCBakExtension",true); + std::string saveBackupDateFormat = App::GetApplication().GetParameterGroupByPath + ("User parameter:BaseApp/Preferences/Document")->GetASCII("SaveBackupDateFormat","%Y%m%d-%H%M%S"); + + BackupPolicy policy; + if (useFCBakExtension) { + policy.setPolicy(BackupPolicy::TimeStamp); + policy.useBackupExtension(useFCBakExtension); + policy.setDateFormat(saveBackupDateFormat); + } + else { + policy.setPolicy(BackupPolicy::Standard); + } + policy.setNumberOfFiles(count_bak); + policy.apply(fn, nativePath); + } + + signalFinishSave(*this, filename); + + return true; +} + +bool Document::isAnyRestoring() { + return globalIsRestoring; +} + +// Open the document +void Document::restore (const char *filename, + bool delaySignal, const std::vector &objNames) +{ + clearUndos(); + d->activeObject = nullptr; + + bool signal = false; + Document *activeDoc = GetApplication().getActiveDocument(); + if (!d->objectArray.empty()) { + signal = true; + GetApplication().signalDeleteDocument(*this); + d->clearDocument(); + } + + Base::FlagToggler<> flag(globalIsRestoring, false); + + setStatus(Document::PartialDoc,false); + + d->clearRecomputeLog(); + d->objectArray.clear(); + d->objectMap.clear(); + d->objectIdMap.clear(); + d->lastObjectId = 0; + + if(signal) { + GetApplication().signalNewDocument(*this,true); + if(activeDoc == this) + GetApplication().setActiveDocument(this); + } + + if(!filename) + filename = FileName.getValue(); + Base::FileInfo fi(filename); + Base::ifstream file(fi, std::ios::in | std::ios::binary); + std::streambuf* buf = file.rdbuf(); + std::streamoff size = buf->pubseekoff(0, std::ios::end, std::ios::in); + buf->pubseekoff(0, std::ios::beg, std::ios::in); + if (size < 22) // an empty zip archive has 22 bytes + throw Base::FileException("Invalid project file",filename); + + zipios::ZipInputStream zipstream(file); + Base::XMLReader reader(filename, zipstream); + + if (!reader.isValid()) + throw Base::FileException("Error reading compression file",filename); + + GetApplication().signalStartRestoreDocument(*this); + setStatus(Document::Restoring, true); + + d->partialLoadObjects.clear(); + for(auto &name : objNames) + d->partialLoadObjects.emplace(name,true); + try { + Document::Restore(reader); + } catch (const Base::Exception& e) { + Base::Console().Error("Invalid Document.xml: %s\n", e.what()); + setStatus(Document::RestoreError, true); + } + + d->partialLoadObjects.clear(); + d->programVersion = reader.ProgramVersion; + + // Special handling for Gui document, the view representations must already + // exist, what is done in Restore(). + // Note: This file doesn't need to be available if the document has been created + // without GUI. But if available then follow after all data files of the App document. + signalRestoreDocument(reader); + reader.readFiles(zipstream); + + if (reader.testStatus(Base::XMLReader::ReaderStatus::PartialRestore)) { + setStatus(Document::PartialRestore, true); + Base::Console().Error("There were errors while loading the file. Some data might have been modified or not recovered at all. Look above for more specific information about the objects involved.\n"); + } + + if(!delaySignal) + afterRestore(true); +} + +bool Document::afterRestore(bool checkPartial) { + Base::FlagToggler<> flag(globalIsRestoring, false); + if(!afterRestore(d->objectArray,checkPartial)) { + FC_WARN("Reload partial document " << getName()); + GetApplication().signalPendingReloadDocument(*this); + return false; + } + GetApplication().signalFinishRestoreDocument(*this); + setStatus(Document::Restoring, false); + return true; +} + +bool Document::afterRestore(const std::vector &objArray, bool checkPartial) +{ + checkPartial = checkPartial && testStatus(Document::PartialDoc); + if(checkPartial && !d->touchedObjs.empty()) + return false; + + // some link type property cannot restore link information until other + // objects has been restored. For example, PropertyExpressionEngine and + // PropertySheet with expression containing label reference. So we add the + // Property::afterRestore() interface to let them sort it out. Note, this + // API is not called in object dedpenency order, because the order + // information is not ready yet. + std::map > propMap; + for(auto obj : objArray) { + auto &props = propMap[obj]; + obj->getPropertyList(props); + for(auto prop : props) { + try { + prop->afterRestore(); + } catch (const Base::Exception& e) { + FC_ERR("Failed to restore " << obj->getFullName() + << '.' << prop->getName() << ": " << e.what()); + } + } + } + + if(checkPartial && !d->touchedObjs.empty()) { + // partial document touched, signal full reload + return false; + } + + std::set objSet(objArray.begin(),objArray.end()); + auto objs = getDependencyList(objArray.empty()?d->objectArray:objArray,DepSort); + for (auto obj : objs) { + if(objSet.find(obj)==objSet.end()) + continue; + try { + for(auto prop : propMap[obj]) + prop->onContainerRestored(); + bool touched = false; + auto returnCode = obj->ExpressionEngine.execute( + PropertyExpressionEngine::ExecuteOnRestore,&touched); + if(returnCode!=DocumentObject::StdReturn) { + FC_ERR("Expression engine failed to restore " << obj->getFullName() << ": " << returnCode->Why); + d->addRecomputeLog(returnCode); + } + obj->onDocumentRestored(); + if(touched) + d->touchedObjs.insert(obj); + } + catch (const Base::Exception& e) { + d->addRecomputeLog(e.what(),obj); + FC_ERR("Failed to restore " << obj->getFullName() << ": " << e.what()); + } + catch (std::exception &e) { + d->addRecomputeLog(e.what(),obj); + FC_ERR("Failed to restore " << obj->getFullName() << ": " << e.what()); + } + catch (...) { + d->addRecomputeLog("Unknown exception on restore",obj); + FC_ERR("Failed to restore " << obj->getFullName() << ": " << "unknown exception"); + } + if(obj->isValid()) { + auto &props = propMap[obj]; + props.clear(); + // refresh properties in case the object changes its property list + obj->getPropertyList(props); + for(auto prop : props) { + auto link = Base::freecad_dynamic_cast(prop); + int res; + std::string errMsg; + if(link && (res=link->checkRestore(&errMsg))) { + d->touchedObjs.insert(obj); + if(res==1 || checkPartial) { + FC_WARN(obj->getFullName() << '.' << prop->getName() << ": " << errMsg); + setStatus(Document::LinkStampChanged, true); + if(checkPartial) + return false; + } else { + FC_ERR(obj->getFullName() << '.' << prop->getName() << ": " << errMsg); + d->addRecomputeLog(errMsg,obj); + setStatus(Document::PartialRestore, true); + } + } + } + } + + if(checkPartial && !d->touchedObjs.empty()) { + // partial document touched, signal full reload + return false; + } else if(!d->touchedObjs.count(obj)) + obj->purgeTouched(); + + signalFinishRestoreObject(*obj); + } + + d->touchedObjs.clear(); + return true; +} + +bool Document::isSaved() const +{ + std::string name = FileName.getValue(); + return !name.empty(); +} + +/** Label is the visible name of a document shown e.g. in the windows title + * or in the tree view. The label almost (but not always e.g. if you manually change it) + * matches with the file name where the document is stored to. + * In contrast to Label the method getName() returns the internal name of the document that only + * matches with Label when loading or creating a document because then both are set to the same value. + * Since the internal name cannot be changed during runtime it must differ from the Label after saving + * the document the first time or saving it under a new file name. + * @ note More than one document can have the same label name. + * @ note The internal is always guaranteed to be unique because @ref Application::newDocument() checks + * for a document with the same name and makes it unique if needed. Hence you cannot rely on that the + * internal name matches with the name you passed to Application::newDoument(). You should use the + * method getName() instead. + */ +const char* Document::getName() const +{ + // return GetApplication().getDocumentName(this); + return myName.c_str(); +} + +std::string Document::getFullName() const { + return myName; +} + +const char* Document::getProgramVersion() const +{ + return d->programVersion.c_str(); +} + +const char* Document::getFileName() const +{ + return testStatus(TempDoc) ? TransientDir.getValue() + : FileName.getValue(); +} + +/// Remove all modifications. After this call The document becomes valid again. +void Document::purgeTouched() +{ + for (std::vector::iterator It = d->objectArray.begin();It != d->objectArray.end();++It) + (*It)->purgeTouched(); +} + +bool Document::isTouched() const +{ + for (std::vector::const_iterator It = d->objectArray.begin();It != d->objectArray.end();++It) + if ((*It)->isTouched()) + return true; + return false; +} + +vector Document::getTouched() const +{ + vector result; + + for (std::vector::const_iterator It = d->objectArray.begin();It != d->objectArray.end();++It) + if ((*It)->isTouched()) + result.push_back(*It); + + return result; +} + +void Document::setClosable(bool c) +{ + setStatus(Document::Closable, c); +} + +bool Document::isClosable() const +{ + return testStatus(Document::Closable); +} + +int Document::countObjects() const +{ + return static_cast(d->objectArray.size()); +} + +void Document::getLinksTo(std::set &links, + const DocumentObject *obj, int options, int maxCount, + const std::vector &objs) const +{ + std::map > linkMap; + + for(auto o : !objs.empty() ? objs : d->objectArray) { + if (o == obj) + continue; + auto linked = o; + if (options & GetLinkArrayElement) { + linked = o->getLinkedObject(false); + } + else { + auto ext = o->getExtensionByType(true); + if(ext) + linked = ext->getTrueLinkedObject(false,nullptr,0,true); + else + linked = o->getLinkedObject(false); + } + + if(linked && linked!=o) { + if(options & GetLinkRecursive) + linkMap[linked].push_back(o); + else if(linked == obj || !obj) { + if((options & GetLinkExternal) + && linked->getDocument()==o->getDocument()) + continue; + else if(options & GetLinkedObject) + links.insert(linked); + else + links.insert(o); + if(maxCount && maxCount<=(int)links.size()) + return; + } + } + } + + if(!(options & GetLinkRecursive)) + return; + + std::vector current(1,obj); + for(int depth=0;!current.empty();++depth) { + if(!GetApplication().checkLinkDepth(depth, MessageOption::Error)) + break; + std::vector next; + for(const App::DocumentObject *o : current) { + auto iter = linkMap.find(o); + if(iter==linkMap.end()) + continue; + for (App::DocumentObject *link : iter->second) { + if (links.insert(link).second) { + if(maxCount && maxCount<=(int)links.size()) + return; + next.push_back(link); + } + } + } + current = std::move(next); + } + return; +} + +bool Document::hasLinksTo(const DocumentObject *obj) const { + std::set links; + getLinksTo(links,obj,0,1); + return !links.empty(); +} + +std::vector Document::getInList(const DocumentObject* me) const +{ + // result list + std::vector result; + // go through all objects + for (auto It = d->objectMap.begin(); It != d->objectMap.end();++It) { + // get the outList and search if me is in that list + std::vector OutList = It->second->getOutList(); + for (std::vector::const_iterator It2=OutList.begin();It2!=OutList.end();++It2) + if (*It2 && *It2 == me) + // add the parent object + result.push_back(It->second); + } + return result; +} + +// This function unifies the old _rebuildDependencyList() and +// getDependencyList(). The algorithm basically obtains the object dependency +// by recrusivly visiting the OutList of each object in the given object array. +// It makes sure to call getOutList() of each object once and only once, which +// makes it much more efficient than calling getRecursiveOutList() on each +// individual object. +// +// The problem with the original algorithm is that, it assumes the objects +// inside any OutList are all within the given object array, so it does not +// recursively call getOutList() on those dependent objects inside. This +// assumption is broken by the introduction of PropertyXLink which can link to +// external object. +// +static void _buildDependencyList(const std::vector &objectArray, + int options, std::vector *depObjs, + DependencyList *depList, std::map *objectMap, + bool *touchCheck = nullptr) +{ + std::map > outLists; + std::deque objs; + + if(objectMap) objectMap->clear(); + if(depList) depList->clear(); + + int op = (options & Document::DepNoXLinked)?DocumentObject::OutListNoXLinked:0; + for (auto obj : objectArray) { + objs.push_back(obj); + while(!objs.empty()) { + auto obj = objs.front(); + objs.pop_front(); + if(!obj || !obj->getNameInDocument()) + continue; + + auto it = outLists.find(obj); + if(it!=outLists.end()) + continue; + + if(touchCheck) { + if(obj->isTouched() || obj->mustExecute()) { + // early termination on touch check + *touchCheck = true; + return; + } + } + if(depObjs) depObjs->push_back(obj); + if(objectMap && depList) + (*objectMap)[obj] = add_vertex(*depList); + + auto &outList = outLists[obj]; + outList = obj->getOutList(op); + objs.insert(objs.end(),outList.begin(),outList.end()); + } + } + + if(objectMap && depList) { + for (const auto &v : outLists) { + for(auto obj : v.second) { + if(obj && obj->getNameInDocument()) + add_edge((*objectMap)[v.first],(*objectMap)[obj],*depList); + } + } + } +} + +std::vector Document::getDependencyList( + const std::vector& objectArray, int options) +{ + std::vector ret; + if(!(options & DepSort)) { + _buildDependencyList(objectArray,options,&ret,nullptr,nullptr); + return ret; + } + + DependencyList depList; + std::map objectMap; + std::map vertexMap; + + _buildDependencyList(objectArray,options,nullptr,&depList,&objectMap); + + for(auto &v : objectMap) + vertexMap[v.second] = v.first; + + std::list make_order; + try { + boost::topological_sort(depList, std::front_inserter(make_order)); + } catch (const std::exception& e) { + if(options & DepNoCycle) { + // Use boost::strong_components to find cycles. It groups strongly + // connected vertices as components, and therefore each component + // forms a cycle. + std::vector c(vertexMap.size()); + std::map > components; + boost::strong_components(depList,boost::make_iterator_property_map( + c.begin(),boost::get(boost::vertex_index,depList),c[0])); + for(size_t i=0;isecond->getOutList()) { + if(obj == it->second) { + ss << std::endl << it->second->getFullName() << std::endl; + break; + } + } + continue; + } + // For components with more than one member, they form a loop together + for(size_t i=0;isecond->getFullName() << ", "; + } + ss << std::endl; + } + FC_ERR(ss.str()); + FC_THROWM(Base::RuntimeError, e.what()); + } + FC_ERR(objectArray.front()->getFullName() + << " (" << objectArray.front()->Label.getValue() << ") : " << e.what()); + ret = DocumentP::partialTopologicalSort(objectArray); + std::reverse(ret.begin(),ret.end()); + return ret; + } + + for (std::list::reverse_iterator i = make_order.rbegin();i != make_order.rend(); ++i) + ret.push_back(vertexMap[*i]); + return ret; +} + +std::vector Document::getDependentDocuments(bool sort) { + return getDependentDocuments({this},sort); +} + +std::vector Document::getDependentDocuments( + std::vector pending, bool sort) +{ + DependencyList depList; + std::map docMap; + std::map vertexMap; + + std::vector ret; + if(pending.empty()) + return ret; + + auto outLists = PropertyXLink::getDocumentOutList(); + std::set docs; + docs.insert(pending.begin(),pending.end()); + if(sort) { + for(auto doc : pending) + docMap[doc] = add_vertex(depList); + } + while(!pending.empty()) { + auto doc = pending.back(); + pending.pop_back(); + + auto it = outLists.find(doc); + if(it == outLists.end()) + continue; + + auto &vertex = docMap[doc]; + for(auto depDoc : it->second) { + if(docs.insert(depDoc).second) { + pending.push_back(depDoc); + if(sort) + docMap[depDoc] = add_vertex(depList); + } + add_edge(vertex,docMap[depDoc],depList); + } + } + + if(!sort) { + ret.insert(ret.end(),docs.begin(),docs.end()); + return ret; + } + + std::list make_order; + try { + boost::topological_sort(depList, std::front_inserter(make_order)); + } catch (const std::exception& e) { + std::string msg("Document::getDependentDocuments: "); + msg += e.what(); + throw Base::RuntimeError(msg); + } + + for(auto &v : docMap) + vertexMap[v.second] = v.first; + for (auto rIt=make_order.rbegin(); rIt!=make_order.rend(); ++rIt) + ret.push_back(vertexMap[*rIt]); + return ret; +} + +void Document::_rebuildDependencyList(const std::vector &objs) +{ +#ifdef USE_OLD_DAG + _buildDependencyList(objs.empty()?d->objectArray:objs,false,0,&d->DepList,&d->VertexObjectList); +#else + (void)objs; +#endif +} + +/** + * @brief Signal that object identifiers, typically a property or document object has been renamed. + * + * This function iterates through all document object in the document, and calls its + * renameObjectIdentifiers functions. + * + * @param paths Map with current and new names + */ + +void Document::renameObjectIdentifiers(const std::map &paths, const std::function & selector) +{ + std::map extendedPaths; + + std::map::const_iterator it = paths.begin(); + while (it != paths.end()) { + extendedPaths[it->first.canonicalPath()] = it->second.canonicalPath(); + ++it; + } + + for (std::vector::iterator it = d->objectArray.begin(); it != d->objectArray.end(); ++it) + if (selector(*it)) + (*it)->renameObjectIdentifiers(extendedPaths); +} + +#ifdef USE_OLD_DAG +int Document::recompute(const std::vector &objs, bool force) +{ + if (testStatus(Document::Recomputing)) { + // this is clearly a bug in the calling instance + throw Base::RuntimeError("Nested recomputes of a document are not allowed"); + } + + int objectCount = 0; + + // The 'SkipRecompute' flag can be (tmp.) set to avoid too many + // time expensive recomputes + if(!force && testStatus(Document::SkipRecompute)) + return 0; + + Base::ObjectStatusLocker exe(Document::Recomputing, this); + + // delete recompute log + d->clearRecomputeLog(); + + // updates the dependency graph + _rebuildDependencyList(objs); + + std::list make_order; + DependencyList::out_edge_iterator j, jend; + + try { + // this sort gives the execute + boost::topological_sort(d->DepList, std::front_inserter(make_order)); + } + catch (const std::exception& e) { + std::cerr << "Document::recompute: " << e.what() << std::endl; + return -1; + } + + // caching vertex to DocObject + for (std::map::const_iterator It1= d->VertexObjectList.begin();It1 != d->VertexObjectList.end(); ++It1) + d->vertexMap[It1->second] = It1->first; + +#ifdef FC_LOGFEATUREUPDATE + std::clog << "make ordering: " << std::endl; +#endif + + std::set recomputeList; + + for (std::list::reverse_iterator i = make_order.rbegin();i != make_order.rend(); ++i) { + DocumentObject* Cur = d->vertexMap[*i]; + // Because of PropertyXLink, we should account for external objects + // TODO: make sure it is safe to rely on getNameInDocument() to check if + // object is in the document. If it crashes, then we should fix the code + // to properly nullify getNameInDocument(), rather than revert back to + // the inefficient isIn() + // if (!Cur || !isIn(Cur)) continue; + if (!Cur || !Cur->getNameInDocument()) continue; +#ifdef FC_LOGFEATUREUPDATE + std::clog << Cur->getNameInDocument() << " dep on:" ; +#endif + bool NeedUpdate = false; + + // ask the object if it should be recomputed + if (Cur->mustExecute() == 1 || Cur->ExpressionEngine.depsAreTouched()) { +#ifdef FC_LOGFEATUREUPDATE + std::clog << "[touched]"; +#endif + NeedUpdate = true; + } + else {// if (Cur->mustExecute() == -1) + // update if one of the dependencies is touched + for (boost::tie(j, jend) = out_edges(*i, d->DepList); j != jend; ++j) { + DocumentObject* Test = d->vertexMap[target(*j, d->DepList)]; + + if (!Test) continue; +#ifdef FC_LOGFEATUREUPDATE + std::clog << " " << Test->getNameInDocument(); +#endif + if (Test->isTouched()) { + NeedUpdate = true; +#ifdef FC_LOGFEATUREUPDATE + std::clog << "[touched]"; +#endif + } + } + } + // if one touched recompute + if (NeedUpdate) { + Cur->touch(); +#ifdef FC_LOGFEATUREUPDATE + std::clog << " => Recompute feature"; +#endif + recomputeList.insert(Cur); + } +#ifdef FC_LOGFEATUREUPDATE + std::clog << std::endl; +#endif + } + +#ifdef FC_LOGFEATUREUPDATE + std::clog << "Have to recompute the following document objects" << std::endl; + for (std::set::const_iterator it = recomputeList.begin(); it != recomputeList.end(); ++it) { + std::clog << " " << (*it)->getNameInDocument() << std::endl; + } +#endif + + for (std::list::reverse_iterator i = make_order.rbegin();i != make_order.rend(); ++i) { + DocumentObject* Cur = d->vertexMap[*i]; + if (!Cur || !isIn(Cur)) continue; + + if (recomputeList.find(Cur) != recomputeList.end() || + Cur->ExpressionEngine.depsAreTouched()) { + if ( _recomputeFeature(Cur)) { + // if something happened break execution of recompute + d->vertexMap.clear(); + return -1; + } + signalRecomputedObject(*Cur); + ++objectCount; + } + } + + // reset all touched + for (std::map::iterator it = d->vertexMap.begin(); it != d->vertexMap.end(); ++it) { + // TODO: check the TODO comments above for details + // if ((it->second) && isIn(it->second)) + if ((it->second) && it->second->getNameInDocument()) + it->second->purgeTouched(); + } + d->vertexMap.clear(); + + signalRecomputed(*this); + + return objectCount; +} + +#else //ifdef USE_OLD_DAG + +int Document::recompute(const std::vector &objs, bool force, bool *hasError, int options) +{ + if (d->undoing || d->rollback) { + if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) + FC_WARN("Ignore document recompute on undo/redo"); + return 0; + } + + int objectCount = 0; + if (testStatus(Document::PartialDoc)) { + if(mustExecute()) + FC_WARN("Please reload partial document '" << Label.getValue() << "' for recomputation."); + return 0; + } + if (testStatus(Document::Recomputing)) { + // this is clearly a bug in the calling instance + FC_ERR("Recursive calling of recompute for document " << getName()); + return 0; + } + // The 'SkipRecompute' flag can be (tmp.) set to avoid too many + // time expensive recomputes + if(!force && testStatus(Document::SkipRecompute)) { + signalSkipRecompute(*this,objs); + return 0; + } + + // delete recompute log + d->clearRecomputeLog(); + + FC_TIME_INIT(t); + + Base::ObjectStatusLocker exe(Document::Recomputing, this); + signalBeforeRecompute(*this); + +#if 0 + ////////////////////////////////////////////////////////////////////////// + // FIXME Comment by Realthunder: + // the topologicalSrot() below cannot handle partial recompute, haven't got + // time to figure out the code yet, simply use back boost::topological_sort + // for now, that is, rely on getDependencyList() to do the sorting. The + // downside is, it didn't take advantage of the ready built InList, nor will + // it report for cyclic dependency. + ////////////////////////////////////////////////////////////////////////// + + // get the sorted vector of all dependent objects and go though it from the end + auto depObjs = getDependencyList(objs.empty()?d->objectArray:objs); + vector topoSortedObjects = topologicalSort(depObjs); + if (topoSortedObjects.size() != depObjs.size()){ + cerr << "App::Document::recompute(): cyclic dependency detected" << endl; + topoSortedObjects = d->partialTopologicalSort(depObjs); + } + std::reverse(topoSortedObjects.begin(),topoSortedObjects.end()); +#else + auto topoSortedObjects = getDependencyList(objs.empty()?d->objectArray:objs,DepSort|options); +#endif + for(auto obj : topoSortedObjects) + obj->setStatus(ObjectStatus::PendingRecompute,true); + + ParameterGrp::handle hGrp = GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Document"); + bool canAbort = hGrp->GetBool("CanAbortRecompute",true); + + std::set filter; + size_t idx = 0; + + FC_TIME_INIT(t2); + + try { + // maximum two passes to allow some form of dependency inversion + for(int passes=0; passes<2 && idx seq; + if(canAbort) + seq.reset(new Base::SequencerLauncher("Recompute...", topoSortedObjects.size())); + FC_LOG("Recompute pass " << passes); + for (; idx < topoSortedObjects.size(); ++idx) { + auto obj = topoSortedObjects[idx]; + if(!obj->getNameInDocument() || filter.find(obj)!=filter.end()) + continue; + // ask the object if it should be recomputed + bool doRecompute = false; + if (obj->mustRecompute()) { + doRecompute = true; + ++objectCount; + int res = _recomputeFeature(obj); + if(res) { + if(hasError) + *hasError = true; + if(res < 0) { + passes = 2; + break; + } + // if something happened filter all object in its + // inListRecursive from the queue then proceed + obj->getInListEx(filter,true); + filter.insert(obj); + continue; + } + } + if(obj->isTouched() || doRecompute) { + signalRecomputedObject(*obj); + obj->purgeTouched(); + // set all dependent object touched to force recompute + for (auto inObjIt : obj->getInList()) + inObjIt->enforceRecompute(); + } + if (seq) + seq->next(true); + } + // check if all objects are recomputed but still thouched + for (size_t i=0;isetStatus(ObjectStatus::Recompute2,false); + if(!filter.count(obj) && obj->isTouched()) { + if(passes>0) + FC_ERR(obj->getFullName() << " still touched after recompute"); + else{ + FC_LOG(obj->getFullName() << " still touched after recompute"); + if(idx>=topoSortedObjects.size()) { + // let's start the next pass on the first touched object + idx = i; + } + obj->setStatus(ObjectStatus::Recompute2,true); + } + } + } + } + }catch(Base::Exception &e) { + e.ReportException(); + } + + FC_TIME_LOG(t2, "Recompute"); + + for(auto obj : topoSortedObjects) { + if(!obj->getNameInDocument()) + continue; + obj->setStatus(ObjectStatus::PendingRecompute,false); + obj->setStatus(ObjectStatus::Recompute2,false); + } + + signalRecomputed(*this,topoSortedObjects); + + FC_TIME_LOG(t,"Recompute total"); + + if (!d->_RecomputeLog.empty()) { + if (!testStatus(Status::IgnoreErrorOnRecompute)) + Base::Console().Error("Recompute failed!\n"); + } + + for (auto doc : GetApplication().getDocuments()) { + decltype(doc->d->pendingRemove) objs; + objs.swap(doc->d->pendingRemove); + for(auto &o : objs) { + try { + if (auto obj = o.getObject()) { + obj->getDocument()->removeObject(obj->getNameInDocument()); + } + } catch (Base::Exception & e) { + e.ReportException(); + FC_ERR("error when removing object " << o.getDocumentName() << '#' << o.getObjectName()); + } + } + } + return objectCount; +} + +#endif // USE_OLD_DAG + +/*! + Does almost the same as topologicalSort() until no object with an input degree of zero + can be found. It then searches for objects with an output degree of zero until neither + an object with input or output degree can be found. The remaining objects form one or + multiple cycles. + An alternative to this method might be: + https://en.wikipedia.org/wiki/Tarjan%E2%80%99s_strongly_connected_components_algorithm + */ +std::vector DocumentP::partialTopologicalSort( + const std::vector& objects) +{ + vector < App::DocumentObject* > ret; + ret.reserve(objects.size()); + // pairs of input and output degree + map < App::DocumentObject*, std::pair > countMap; + + for (auto objectIt : objects) { + //we need inlist with unique entries + auto in = objectIt->getInList(); + std::sort(in.begin(), in.end()); + in.erase(std::unique(in.begin(), in.end()), in.end()); + + //we need outlist with unique entries + auto out = objectIt->getOutList(); + std::sort(out.begin(), out.end()); + out.erase(std::unique(out.begin(), out.end()), out.end()); + + countMap[objectIt] = std::make_pair(in.size(), out.size()); + } + + std::list degIn; + std::list degOut; + + bool removeVertex = true; + while (removeVertex) { + removeVertex = false; + + // try input degree + auto degInIt = find_if(countMap.begin(), countMap.end(), + [](pair< App::DocumentObject*, pair > vertex)->bool { + return vertex.second.first == 0; + }); + + if (degInIt != countMap.end()) { + removeVertex = true; + degIn.push_back(degInIt->first); + degInIt->second.first = degInIt->second.first - 1; + + //we need outlist with unique entries + auto out = degInIt->first->getOutList(); + std::sort(out.begin(), out.end()); + out.erase(std::unique(out.begin(), out.end()), out.end()); + + for (auto outListIt : out) { + auto outListMapIt = countMap.find(outListIt); + if (outListMapIt != countMap.end()) + outListMapIt->second.first = outListMapIt->second.first - 1; + } + } + } + + // make the output degree negative if input degree is negative + // to mark the vertex as processed + for (auto& countIt : countMap) { + if (countIt.second.first < 0) { + countIt.second.second = -1; + } + } + + removeVertex = degIn.size() != objects.size(); + while (removeVertex) { + removeVertex = false; + + auto degOutIt = find_if(countMap.begin(), countMap.end(), + [](pair< App::DocumentObject*, pair > vertex)->bool { + return vertex.second.second == 0; + }); + + if (degOutIt != countMap.end()) { + removeVertex = true; + degOut.push_front(degOutIt->first); + degOutIt->second.second = degOutIt->second.second - 1; + + //we need inlist with unique entries + auto in = degOutIt->first->getInList(); + std::sort(in.begin(), in.end()); + in.erase(std::unique(in.begin(), in.end()), in.end()); + + for (auto inListIt : in) { + auto inListMapIt = countMap.find(inListIt); + if (inListMapIt != countMap.end()) + inListMapIt->second.second = inListMapIt->second.second - 1; + } + } + } + + // at this point we have no root object any more + for (auto countIt : countMap) { + if (countIt.second.first > 0 && countIt.second.second > 0) { + degIn.push_back(countIt.first); + } + } + + ret.insert(ret.end(), degIn.begin(), degIn.end()); + ret.insert(ret.end(), degOut.begin(), degOut.end()); + + return ret; +} + +std::vector DocumentP::topologicalSort(const std::vector& objects) const +{ + // topological sort algorithm described here: + // https://de.wikipedia.org/wiki/Topologische_Sortierung#Algorithmus_f.C3.BCr_das_Topologische_Sortieren + vector < App::DocumentObject* > ret; + ret.reserve(objects.size()); + map < App::DocumentObject*,int > countMap; + + for (auto objectIt : objects) { + // We now support externally linked objects + // if(!obj->getNameInDocument() || obj->getDocument()!=this) + if(!objectIt->getNameInDocument()) + continue; + //we need inlist with unique entries + auto in = objectIt->getInList(); + std::sort(in.begin(), in.end()); + in.erase(std::unique(in.begin(), in.end()), in.end()); + + countMap[objectIt] = in.size(); + } + + auto rootObjeIt = find_if(countMap.begin(), countMap.end(), [](pair < App::DocumentObject*, int > count)->bool { + return count.second == 0; + }); + + if (rootObjeIt == countMap.end()){ + cerr << "Document::topologicalSort: cyclic dependency detected (no root object)" << endl; + return ret; + } + + while (rootObjeIt != countMap.end()){ + rootObjeIt->second = rootObjeIt->second - 1; + + //we need outlist with unique entries + auto out = rootObjeIt->first->getOutList(); + std::sort(out.begin(), out.end()); + out.erase(std::unique(out.begin(), out.end()), out.end()); + + for (auto outListIt : out) { + auto outListMapIt = countMap.find(outListIt); + if (outListMapIt != countMap.end()) + outListMapIt->second = outListMapIt->second - 1; + } + ret.push_back(rootObjeIt->first); + + rootObjeIt = find_if(countMap.begin(), countMap.end(), [](pair < App::DocumentObject*, int > count)->bool { + return count.second == 0; + }); + } + + return ret; +} + +std::vector Document::topologicalSort() const +{ + return d->topologicalSort(d->objectArray); +} + +const char * Document::getErrorDescription(const App::DocumentObject*Obj) const +{ + return d->findRecomputeLog(Obj); +} + +// call the recompute of the Feature and handle the exceptions and errors. +int Document::_recomputeFeature(DocumentObject* Feat) +{ + FC_LOG("Recomputing " << Feat->getFullName()); + + DocumentObjectExecReturn *returnCode = nullptr; + try { + returnCode = Feat->ExpressionEngine.execute(PropertyExpressionEngine::ExecuteNonOutput); + if (returnCode == DocumentObject::StdReturn) { + returnCode = Feat->recompute(); + if(returnCode == DocumentObject::StdReturn) + returnCode = Feat->ExpressionEngine.execute(PropertyExpressionEngine::ExecuteOutput); + } + } + catch(Base::AbortException &e){ + e.ReportException(); + FC_LOG("Failed to recompute " << Feat->getFullName() << ": " << e.what()); + d->addRecomputeLog("User abort",Feat); + return -1; + } + catch (const Base::MemoryException& e) { + FC_ERR("Memory exception in " << Feat->getFullName() << " thrown: " << e.what()); + d->addRecomputeLog("Out of memory exception",Feat); + return 1; + } + catch (Base::Exception &e) { + e.ReportException(); + FC_LOG("Failed to recompute " << Feat->getFullName() << ": " << e.what()); + d->addRecomputeLog(e.what(),Feat); + return 1; + } + catch (std::exception &e) { + FC_ERR("exception in " << Feat->getFullName() << " thrown: " << e.what()); + d->addRecomputeLog(e.what(),Feat); + return 1; + } +#ifndef FC_DEBUG + catch (...) { + FC_ERR("Unknown exception in " << Feat->getFullName() << " thrown"); + d->addRecomputeLog("Unknown exception!",Feat); + return 1; + } +#endif + + if (returnCode == DocumentObject::StdReturn) { + Feat->resetError(); + } + else { + returnCode->Which = Feat; + d->addRecomputeLog(returnCode); + FC_LOG("Failed to recompute " << Feat->getFullName() << ": " << returnCode->Why); + return 1; + } + return 0; +} + +bool Document::recomputeFeature(DocumentObject* Feat, bool recursive) +{ + // delete recompute log + d->clearRecomputeLog(Feat); + + // verify that the feature is (active) part of the document + if (Feat->getNameInDocument()) { + if(recursive) { + bool hasError = false; + recompute({Feat},true,&hasError); + return !hasError; + } else { + _recomputeFeature(Feat); + signalRecomputedObject(*Feat); + return Feat->isValid(); + } + }else + return false; +} + +DocumentObject * Document::addObject(const char* sType, const char* pObjectName, + bool isNew, const char* viewType, bool isPartial) +{ + Base::Type type = Base::Type::getTypeIfDerivedFrom(sType, App::DocumentObject::getClassTypeId(), true); + if (type.isBad()) { + std::stringstream str; + str << "'" << sType << "' is not a document object type"; + throw Base::TypeError(str.str()); + } + + void* typeInstance = type.createInstance(); + if (!typeInstance) + return nullptr; + + App::DocumentObject* pcObject = static_cast(typeInstance); + + pcObject->setDocument(this); + + // do no transactions if we do a rollback! + if (!d->rollback) { + // Undo stuff + _checkTransaction(nullptr,nullptr,__LINE__); + if (d->activeUndoTransaction) + d->activeUndoTransaction->addObjectDel(pcObject); + } + + // get Unique name + string ObjectName; + + if (pObjectName && pObjectName[0] != '\0') + ObjectName = getUniqueObjectName(pObjectName); + else + ObjectName = getUniqueObjectName(sType); + + + d->activeObject = pcObject; + + // insert in the name map + d->objectMap[ObjectName] = pcObject; + // generate object id and add to id map; + pcObject->_Id = ++d->lastObjectId; + d->objectIdMap[pcObject->_Id] = pcObject; + // cache the pointer to the name string in the Object (for performance of DocumentObject::getNameInDocument()) + pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first); + // insert in the vector + d->objectArray.push_back(pcObject); + + // If we are restoring, don't set the Label object now; it will be restored later. This is to avoid potential duplicate + // label conflicts later. + if (!d->StatusBits.test(Restoring)) + pcObject->Label.setValue( ObjectName ); + + // Call the object-specific initialization + if (!d->undoing && !d->rollback && isNew) { + pcObject->setupObject (); + } + + // mark the object as new (i.e. set status bit 2) and send the signal + pcObject->setStatus(ObjectStatus::New, true); + + pcObject->setStatus(ObjectStatus::PartialObject, isPartial); + + if (!viewType || viewType[0] == '\0') + viewType = pcObject->getViewProviderNameOverride(); + + if (viewType && viewType[0] != '\0') + pcObject->_pcViewProviderName = viewType; + + signalNewObject(*pcObject); + + // do no transactions if we do a rollback! + if (!d->rollback && d->activeUndoTransaction) { + signalTransactionAppend(*pcObject, d->activeUndoTransaction); + } + + signalActivatedObject(*pcObject); + + // return the Object + return pcObject; +} + +std::vector Document::addObjects(const char* sType, const std::vector& objectNames, bool isNew) +{ + Base::Type type = Base::Type::getTypeIfDerivedFrom(sType, App::DocumentObject::getClassTypeId(), true); + if (type.isBad()) { + std::stringstream str; + str << "'" << sType << "' is not a document object type"; + throw Base::TypeError(str.str()); + } + + std::vector objects; + objects.resize(objectNames.size()); + std::generate(objects.begin(), objects.end(), + [&]{ return static_cast(type.createInstance()); }); + // the type instance could be a null pointer, it is enough to check the first element + if (!objects.empty() && !objects[0]) { + objects.clear(); + return objects; + } + + // get all existing object names + std::vector reservedNames; + reservedNames.reserve(d->objectMap.size()); + for (auto pos = d->objectMap.begin();pos != d->objectMap.end();++pos) { + reservedNames.push_back(pos->first); + } + + for (auto it = objects.begin(); it != objects.end(); ++it) { + auto index = std::distance(objects.begin(), it); + App::DocumentObject* pcObject = *it; + pcObject->setDocument(this); + + // do no transactions if we do a rollback! + if (!d->rollback) { + // Undo stuff + _checkTransaction(nullptr,nullptr,__LINE__); + if (d->activeUndoTransaction) { + d->activeUndoTransaction->addObjectDel(pcObject); + } + } + + // get unique name + std::string ObjectName = objectNames[index]; + if (ObjectName.empty()) + ObjectName = sType; + ObjectName = Base::Tools::getIdentifier(ObjectName); + if (d->objectMap.find(ObjectName) != d->objectMap.end()) { + // remove also trailing digits from clean name which is to avoid to create lengthy names + // like 'Box001001' + if (!testStatus(KeepTrailingDigits)) { + std::string::size_type index = ObjectName.find_last_not_of("0123456789"); + if (index+1 < ObjectName.size()) { + ObjectName = ObjectName.substr(0,index+1); + } + } + + ObjectName = Base::Tools::getUniqueName(ObjectName, reservedNames, 3); + } + + reservedNames.push_back(ObjectName); + + // insert in the name map + d->objectMap[ObjectName] = pcObject; + // generate object id and add to id map; + pcObject->_Id = ++d->lastObjectId; + d->objectIdMap[pcObject->_Id] = pcObject; + // cache the pointer to the name string in the Object (for performance of DocumentObject::getNameInDocument()) + pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first); + // insert in the vector + d->objectArray.push_back(pcObject); + + pcObject->Label.setValue(ObjectName); + + // Call the object-specific initialization + if (!d->undoing && !d->rollback && isNew) { + pcObject->setupObject(); + } + + // mark the object as new (i.e. set status bit 2) and send the signal + pcObject->setStatus(ObjectStatus::New, true); + + const char *viewType = pcObject->getViewProviderNameOverride(); + pcObject->_pcViewProviderName = viewType ? viewType : ""; + + signalNewObject(*pcObject); + + // do no transactions if we do a rollback! + if (!d->rollback && d->activeUndoTransaction) { + signalTransactionAppend(*pcObject, d->activeUndoTransaction); + } + } + + if (!objects.empty()) { + d->activeObject = objects.back(); + signalActivatedObject(*objects.back()); + } + + return objects; +} + +void Document::addObject(DocumentObject* pcObject, const char* pObjectName) +{ + if (pcObject->getDocument()) { + throw Base::RuntimeError("Document object is already added to a document"); + } + + pcObject->setDocument(this); + + // do no transactions if we do a rollback! + if (!d->rollback) { + // Undo stuff + _checkTransaction(nullptr,nullptr,__LINE__); + if (d->activeUndoTransaction) + d->activeUndoTransaction->addObjectDel(pcObject); + } + + // get unique name + string ObjectName; + if (pObjectName && pObjectName[0] != '\0') + ObjectName = getUniqueObjectName(pObjectName); + else + ObjectName = getUniqueObjectName(pcObject->getTypeId().getName()); + + d->activeObject = pcObject; + + // insert in the name map + d->objectMap[ObjectName] = pcObject; + // generate object id and add to id map; + if(!pcObject->_Id) pcObject->_Id = ++d->lastObjectId; + d->objectIdMap[pcObject->_Id] = pcObject; + // cache the pointer to the name string in the Object (for performance of DocumentObject::getNameInDocument()) + pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first); + // insert in the vector + d->objectArray.push_back(pcObject); + + pcObject->Label.setValue( ObjectName ); + + // mark the object as new (i.e. set status bit 2) and send the signal + pcObject->setStatus(ObjectStatus::New, true); + + const char *viewType = pcObject->getViewProviderNameOverride(); + pcObject->_pcViewProviderName = viewType ? viewType : ""; + + signalNewObject(*pcObject); + + // do no transactions if we do a rollback! + if (!d->rollback && d->activeUndoTransaction) { + signalTransactionAppend(*pcObject, d->activeUndoTransaction); + } + + signalActivatedObject(*pcObject); +} + +void Document::_addObject(DocumentObject* pcObject, const char* pObjectName) +{ + std::string ObjectName = getUniqueObjectName(pObjectName); + d->objectMap[ObjectName] = pcObject; + // generate object id and add to id map; + if(!pcObject->_Id) pcObject->_Id = ++d->lastObjectId; + d->objectIdMap[pcObject->_Id] = pcObject; + d->objectArray.push_back(pcObject); + // cache the pointer to the name string in the Object (for performance of DocumentObject::getNameInDocument()) + pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first); + + // do no transactions if we do a rollback! + if (!d->rollback) { + // Undo stuff + _checkTransaction(nullptr,nullptr,__LINE__); + if (d->activeUndoTransaction) + d->activeUndoTransaction->addObjectDel(pcObject); + } + + const char *viewType = pcObject->getViewProviderNameOverride(); + pcObject->_pcViewProviderName = viewType ? viewType : ""; + + // send the signal + signalNewObject(*pcObject); + + // do no transactions if we do a rollback! + if (!d->rollback && d->activeUndoTransaction) { + signalTransactionAppend(*pcObject, d->activeUndoTransaction); + } + + d->activeObject = pcObject; + signalActivatedObject(*pcObject); +} + +/// Remove an object out of the document +void Document::removeObject(const DocumentObject* object) +{ + if (object->getDocument() == this) { + removeObject(object->getNameInDocument()); + } +} + +/// Remove an object out of the document +void Document::removeObject(const char* sName) +{ + auto pos = d->objectMap.find(sName); + + // name not found? + if (pos == d->objectMap.end()) + return; + + if (pos->second->testStatus(ObjectStatus::PendingRecompute)) { + // TODO: shall we allow removal if there is active undo transaction? + FC_MSG("pending remove of " << sName << " after recomputing document " << getName()); + d->pendingRemove.emplace_back(pos->second); + return; + } + + TransactionLocker tlock; + + _checkTransaction(pos->second,nullptr,__LINE__); + + if (d->activeObject == pos->second) + d->activeObject = nullptr; + + // Mark the object as about to be deleted + pos->second->setStatus(ObjectStatus::Remove, true); + if (!d->undoing && !d->rollback) { + pos->second->unsetupObject(); + } + + signalDeletedObject(*(pos->second)); + + // do no transactions if we do a rollback! + if (!d->rollback && d->activeUndoTransaction) { + // in this case transaction delete or save the object + signalTransactionRemove(*pos->second, d->activeUndoTransaction); + } + else { + // if not saved in undo -> delete object + signalTransactionRemove(*pos->second, 0); + } + +#ifdef USE_OLD_DAG + if (!d->vertexMap.empty()) { + // recompute of document is running + for (std::map::iterator it = d->vertexMap.begin(); it != d->vertexMap.end(); ++it) { + if (it->second == pos->second) { + it->second = 0; // just nullify the pointer + break; + } + } + } +#endif //USE_OLD_DAG + + // Before deleting we must nullify all dependent objects + breakDependency(pos->second, true); + + //and remove the tip if needed + if (Tip.getValue() && strcmp(Tip.getValue()->getNameInDocument(), sName)==0) { + Tip.setValue(nullptr); + TipName.setValue(""); + } + + // remove the ID before possibly deleting the object + d->objectIdMap.erase(pos->second->_Id); + // Unset the bit to be on the safe side + pos->second->setStatus(ObjectStatus::Remove, false); + + // do no transactions if we do a rollback! + std::unique_ptr tobedestroyed; + if (!d->rollback) { + // Undo stuff + if (d->activeUndoTransaction) { + // in this case transaction delete or save the object + d->activeUndoTransaction->addObjectNew(pos->second); + } + else { + // if not saved in undo -> delete object later + std::unique_ptr delobj(pos->second); + tobedestroyed.swap(delobj); + tobedestroyed->setStatus(ObjectStatus::Destroy, true); + } + } + + for (std::vector::iterator obj = d->objectArray.begin(); obj != d->objectArray.end(); ++obj) { + if (*obj == pos->second) { + d->objectArray.erase(obj); + break; + } + } + + // In case the object gets deleted the pointer must be nullified + if (tobedestroyed) { + tobedestroyed->pcNameInDocument = nullptr; + } + d->objectMap.erase(pos); +} + +/// Remove an object out of the document (internal) +void Document::_removeObject(DocumentObject* pcObject) +{ + if (testStatus(Document::Recomputing)) { + FC_ERR("Cannot delete " << pcObject->getFullName() << " while recomputing"); + return; + } + + TransactionLocker tlock; + + // TODO Refactoring: share code with Document::removeObject() (2015-09-01, Fat-Zer) + _checkTransaction(pcObject,nullptr,__LINE__); + + auto pos = d->objectMap.find(pcObject->getNameInDocument()); + + if(!d->rollback && d->activeUndoTransaction && pos->second->hasChildElement()) { + // Preserve link group children global visibility. See comments in + // removeObject() for more details. + for(auto &sub : pos->second->getSubObjects()) { + if(sub.empty()) + continue; + if(sub[sub.size()-1]!='.') + sub += '.'; + auto sobj = pos->second->getSubObject(sub.c_str()); + if(sobj && sobj->getDocument()==this && !sobj->Visibility.getValue()) + d->activeUndoTransaction->addObjectChange(sobj,&sobj->Visibility); + } + } + + if (d->activeObject == pcObject) + d->activeObject = nullptr; + + // Mark the object as about to be removed + pcObject->setStatus(ObjectStatus::Remove, true); + if (!d->undoing && !d->rollback) { + pcObject->unsetupObject(); + } + signalDeletedObject(*pcObject); + // TODO Check me if it's needed (2015-09-01, Fat-Zer) + + //remove the tip if needed + if (Tip.getValue() == pcObject) { + Tip.setValue(nullptr); + TipName.setValue(""); + } + + // do no transactions if we do a rollback! + if (!d->rollback && d->activeUndoTransaction) { + // Undo stuff + signalTransactionRemove(*pcObject, d->activeUndoTransaction); + d->activeUndoTransaction->addObjectNew(pcObject); + } + else { + // for a rollback delete the object + signalTransactionRemove(*pcObject, 0); + breakDependency(pcObject, true); + } + + // remove from map + pcObject->setStatus(ObjectStatus::Remove, false); // Unset the bit to be on the safe side + d->objectIdMap.erase(pcObject->_Id); + d->objectMap.erase(pos); + + for (std::vector::iterator it = d->objectArray.begin(); it != d->objectArray.end(); ++it) { + if (*it == pcObject) { + d->objectArray.erase(it); + break; + } + } + + // for a rollback delete the object + if (d->rollback) { + pcObject->setStatus(ObjectStatus::Destroy, true); + delete pcObject; + } +} + +void Document::breakDependency(DocumentObject* pcObject, bool clear) +{ + // Nullify all dependent objects + PropertyLinkBase::breakLinks(pcObject,d->objectArray,clear); +} + +std::vector Document::copyObject( + const std::vector &objs, bool recursive, bool returnAll) +{ + std::vector deps; + if(!recursive) + deps = objs; + else + deps = getDependencyList(objs,DepNoXLinked|DepSort); + + if (!testStatus(TempDoc) && !isSaved() && PropertyXLink::hasXLink(deps)) { + throw Base::RuntimeError( + "Document must be saved at least once before link to external objects"); + } + + MergeDocuments md(this); + // if not copying recursively then suppress possible warnings + md.setVerbose(recursive); + + unsigned int memsize=1000; // ~ for the meta-information + for (std::vector::iterator it = deps.begin(); it != deps.end(); ++it) + memsize += (*it)->getMemSize(); + + // if less than ~10 MB + bool use_buffer=(memsize < 0xA00000); + QByteArray res; + try { + res.reserve(memsize); + } + catch (const Base::MemoryException&) { + use_buffer = false; + } + + std::vector imported; + if (use_buffer) { + Base::ByteArrayOStreambuf obuf(res); + std::ostream ostr(&obuf); + exportObjects(deps, ostr); + + Base::ByteArrayIStreambuf ibuf(res); + std::istream istr(nullptr); + istr.rdbuf(&ibuf); + imported = md.importObjects(istr); + } else { + static Base::FileInfo fi(App::Application::getTempFileName()); + Base::ofstream ostr(fi, std::ios::out | std::ios::binary); + exportObjects(deps, ostr); + ostr.close(); + + Base::ifstream istr(fi, std::ios::in | std::ios::binary); + imported = md.importObjects(istr); + } + + if (returnAll || imported.size()!=deps.size()) + return imported; + + std::unordered_map indices; + size_t i=0; + for(auto o : deps) + indices[o] = i++; + std::vector result; + result.reserve(objs.size()); + for(auto o : objs) + result.push_back(imported[indices[o]]); + return result; +} + +std::vector +Document::importLinks(const std::vector &objArray) +{ + std::set links; + getLinksTo(links,nullptr,GetLinkExternal,0,objArray); + + std::vector objs; + objs.insert(objs.end(),links.begin(),links.end()); + objs = App::Document::getDependencyList(objs); + if(objs.empty()) { + FC_ERR("nothing to import"); + return objs; + } + + for(auto it=objs.begin();it!=objs.end();) { + auto obj = *it; + if(obj->getDocument() == this) { + it = objs.erase(it); + continue; + } + ++it; + if(obj->testStatus(App::PartialObject)) { + throw Base::RuntimeError( + "Cannot import partial loaded object. Please reload the current document"); + } + } + + Base::FileInfo fi(App::Application::getTempFileName()); + { + // save stuff to temp file + Base::ofstream str(fi, std::ios::out | std::ios::binary); + MergeDocuments mimeView(this); + exportObjects(objs, str); + str.close(); + } + Base::ifstream str(fi, std::ios::in | std::ios::binary); + MergeDocuments mimeView(this); + objs = mimeView.importObjects(str); + str.close(); + fi.deleteFile(); + + const auto &nameMap = mimeView.getNameMap(); + + // First, find all link type properties that needs to be changed + std::map > propMap; + std::vector propList; + for(auto obj : links) { + propList.clear(); + obj->getPropertyList(propList); + for(auto prop : propList) { + auto linkProp = Base::freecad_dynamic_cast(prop); + if(linkProp && !prop->testStatus(Property::Immutable) && !obj->isReadOnly(prop)) { + auto copy = linkProp->CopyOnImportExternal(nameMap); + if(copy) + propMap[linkProp].reset(copy); + } + } + } + + // Then change them in one go. Note that we don't make change in previous + // loop, because a changed link property may break other depending link + // properties, e.g. a link sub referring to some sub object of an xlink, If + // that sub object is imported with a different name, and xlink is changed + // before this link sub, it will break. + for(auto &v : propMap) + v.first->Paste(*v.second); + + return objs; +} + +DocumentObject* Document::moveObject(DocumentObject* obj, bool recursive) +{ + if(!obj) + return nullptr; + Document* that = obj->getDocument(); + if (that == this) + return nullptr; // nothing todo + + // True object move without copy is only safe when undo is off on both + // documents. + if(!recursive && !d->iUndoMode && !that->d->iUndoMode && !that->d->rollback) { + // all object of the other document that refer to this object must be nullified + that->breakDependency(obj, false); + std::string objname = getUniqueObjectName(obj->getNameInDocument()); + that->_removeObject(obj); + this->_addObject(obj, objname.c_str()); + obj->setDocument(this); + return obj; + } + + std::vector deps; + if(recursive) + deps = getDependencyList({obj},DepNoXLinked|DepSort); + else + deps.push_back(obj); + + auto objs = copyObject(deps,false); + if(objs.empty()) + return nullptr; + // Some object may delete its children if deleted, so we collect the IDs + // or all depending objects for safety reason. + std::vector ids; + ids.reserve(deps.size()); + for(auto o : deps) + ids.push_back(o->getID()); + + // We only remove object if it is the moving object or it has no + // depending objects, i.e. an empty inList, which is why we need to + // iterate the depending list backwards. + for(auto iter=ids.rbegin();iter!=ids.rend();++iter) { + auto o = that->getObjectByID(*iter); + if(!o) continue; + if(iter==ids.rbegin() + || o->getInList().empty()) + that->removeObject(o->getNameInDocument()); + } + return objs.back(); +} + +DocumentObject * Document::getActiveObject() const +{ + return d->activeObject; +} + +DocumentObject * Document::getObject(const char *Name) const +{ + auto pos = d->objectMap.find(Name); + + if (pos != d->objectMap.end()) + return pos->second; + else + return nullptr; +} + +DocumentObject * Document::getObjectByID(long id) const +{ + auto it = d->objectIdMap.find(id); + if(it!=d->objectIdMap.end()) + return it->second; + return nullptr; +} + + +// Note: This method is only used in Tree.cpp slotChangeObject(), see explanation there +bool Document::isIn(const DocumentObject *pFeat) const +{ + for (auto o = d->objectMap.begin(); o != d->objectMap.end(); ++o) { + if (o->second == pFeat) + return true; + } + + return false; +} + +const char * Document::getObjectName(DocumentObject *pFeat) const +{ + for (auto pos = d->objectMap.begin();pos != d->objectMap.end();++pos) { + if (pos->second == pFeat) + return pos->first.c_str(); + } + + return nullptr; +} + +std::string Document::getUniqueObjectName(const char *Name) const +{ + if (!Name || *Name == '\0') + return std::string(); + std::string CleanName = Base::Tools::getIdentifier(Name); + + // name in use? + auto pos = d->objectMap.find(CleanName); + + if (pos == d->objectMap.end()) { + // if not, name is OK + return CleanName; + } + else { + // remove also trailing digits from clean name which is to avoid to create lengthy names + // like 'Box001001' + if (!testStatus(KeepTrailingDigits)) { + std::string::size_type index = CleanName.find_last_not_of("0123456789"); + if (index+1 < CleanName.size()) { + CleanName = CleanName.substr(0,index+1); + } + } + + std::vector names; + names.reserve(d->objectMap.size()); + for (pos = d->objectMap.begin();pos != d->objectMap.end();++pos) { + names.push_back(pos->first); + } + return Base::Tools::getUniqueName(CleanName, names, 3); + } +} + +std::string Document::getStandardObjectName(const char *Name, int d) const +{ + std::vector mm = getObjects(); + std::vector labels; + labels.reserve(mm.size()); + + for (std::vector::const_iterator it = mm.begin(); it != mm.end(); ++it) { + std::string label = (*it)->Label.getValue(); + labels.push_back(label); + } + return Base::Tools::getUniqueName(Name, labels, d); +} + +std::vector Document::getDependingObjects() const +{ + return getDependencyList(d->objectArray); +} + +const std::vector &Document::getObjects() const +{ + return d->objectArray; +} + + +std::vector Document::getObjectsOfType(const Base::Type& typeId) const +{ + std::vector Objects; + for (std::vector::const_iterator it = d->objectArray.begin(); it != d->objectArray.end(); ++it) { + if ((*it)->getTypeId().isDerivedFrom(typeId)) + Objects.push_back(*it); + } + return Objects; +} + +std::vector< DocumentObject* > Document::getObjectsWithExtension(const Base::Type& typeId, bool derived) const { + + std::vector Objects; + for (std::vector::const_iterator it = d->objectArray.begin(); it != d->objectArray.end(); ++it) { + if ((*it)->hasExtension(typeId, derived)) + Objects.push_back(*it); + } + return Objects; +} + + +std::vector Document::findObjects(const Base::Type& typeId, const char* objname, const char* label) const +{ + boost::cmatch what; + boost::regex rx_name, rx_label; + + if (objname) + rx_name.set_expression(objname); + + if (label) + rx_label.set_expression(label); + + std::vector Objects; + DocumentObject* found = nullptr; + for (std::vector::const_iterator it = d->objectArray.begin(); it != d->objectArray.end(); ++it) { + if ((*it)->getTypeId().isDerivedFrom(typeId)) { + found = *it; + + if (!rx_name.empty() && !boost::regex_search((*it)->getNameInDocument(), what, rx_name)) + found = nullptr; + + if (!rx_label.empty() && !boost::regex_search((*it)->Label.getValue(), what, rx_label)) + found = nullptr; + + if (found) + Objects.push_back(found); + } + } + return Objects; +} + +int Document::countObjectsOfType(const Base::Type& typeId) const +{ + int ct=0; + for (auto it = d->objectMap.begin(); it != d->objectMap.end(); ++it) { + if (it->second->getTypeId().isDerivedFrom(typeId)) + ct++; + } + + return ct; +} + +PyObject * Document::getPyObject() +{ + return Py::new_reference_to(d->DocumentPythonObject); +} + +std::vector Document::getRootObjects() const +{ + std::vector < App::DocumentObject* > ret; + + for (auto objectIt : d->objectArray) { + if (objectIt->getInList().empty()) + ret.push_back(objectIt); + } + + return ret; +} + +void DocumentP::findAllPathsAt(const std::vector &all_nodes, size_t id, + std::vector &all_paths, Path tmp) +{ + if (std::find(tmp.begin(), tmp.end(), id) != tmp.end()) { + Path tmp2(tmp); + tmp2.push_back(id); + all_paths.push_back(tmp2); + return; // a cycle + } + + tmp.push_back(id); + if (all_nodes[id].empty()) { + all_paths.push_back(tmp); + return; + } + + for (size_t i=0; i < all_nodes[id].size(); i++) { + Path tmp2(tmp); + findAllPathsAt(all_nodes, all_nodes[id][i], all_paths, tmp2); + } +} + +std::vector > +Document::getPathsByOutList(const App::DocumentObject* from, const App::DocumentObject* to) const +{ + std::map indexMap; + for (size_t i=0; iobjectArray.size(); ++i) { + indexMap[d->objectArray[i]] = i; + } + + std::vector all_nodes(d->objectArray.size()); + for (size_t i=0; iobjectArray.size(); ++i) { + DocumentObject* obj = d->objectArray[i]; + std::vector outList = obj->getOutList(); + for (auto it : outList) { + all_nodes[i].push_back(indexMap[it]); + } + } + + std::vector > array; + if (from == to) + return array; + + size_t index_from = indexMap[from]; + size_t index_to = indexMap[to]; + Path tmp; + std::vector all_paths; + DocumentP::findAllPathsAt(all_nodes, index_from, all_paths, tmp); + + for (std::vector::iterator it = all_paths.begin(); it != all_paths.end(); ++it) { + Path::iterator jt = std::find(it->begin(), it->end(), index_to); + if (jt != it->end()) { + std::list path; + for (Path::iterator kt = it->begin(); kt != jt; ++kt) { + path.push_back(d->objectArray[*kt]); + } + + path.push_back(d->objectArray[*jt]); + array.push_back(path); + } + } + + // remove duplicates + std::sort(array.begin(), array.end()); + array.erase(std::unique(array.begin(), array.end()), array.end()); + + return array; +} + +bool Document::mustExecute() const +{ + if(PropertyXLink::hasXLink(this)) { + bool touched = false; + _buildDependencyList(d->objectArray,false,nullptr,nullptr,nullptr,&touched); + return touched; + } + + for (std::vector::const_iterator It = d->objectArray.begin();It != d->objectArray.end();++It) + if ((*It)->isTouched() || (*It)->mustExecute()==1) + return true; + return false; +} diff --git a/src/App/Document.h b/src/App/Document.h index ad202a31d3e0..5162e48f76a3 100644 --- a/src/App/Document.h +++ b/src/App/Document.h @@ -255,6 +255,8 @@ class AppExport Document : public App::PropertyContainer std::vectoraddObjects(const char* sType, const std::vector& objectNames, bool isNew=true); /// Remove a feature out of the document void removeObject(const char* sName); + /// Remove a feature out of the document + void removeObject(const DocumentObject* object); /** Add an existing feature with sName (ASCII) to this document and set it active. * Unicode names are set through the Label property. * This is an overloaded function of the function above and can be used to create diff --git a/src/App/DocumentPyImp.cpp b/src/App/DocumentPyImp.cpp index 874b2a8c5a39..84234864bfba 100644 --- a/src/App/DocumentPyImp.cpp +++ b/src/App/DocumentPyImp.cpp @@ -320,21 +320,38 @@ PyObject* DocumentPy::addObject(PyObject *args, PyObject *kwd) PyObject* DocumentPy::removeObject(PyObject *args) { - char *sName; - if (!PyArg_ParseTuple(args, "s",&sName)) - return nullptr; - - - DocumentObject *pcFtr = getDocumentPtr()->getObject(sName); - if (pcFtr) { - getDocumentPtr()->removeObject( sName ); - Py_Return; - } - else { + char* sName {}; + if (PyArg_ParseTuple(args, "s", &sName)) { + DocumentObject* object = getDocumentPtr()->getObject(sName); + if (object) { + getDocumentPtr()->removeObject(sName); + Py_Return; + } std::stringstream str; str << "No document object found with name '" << sName << "'" << std::ends; throw Py::ValueError(str.str()); } + PyErr_Clear(); + PyObject* objpy {}; + if (PyArg_ParseTuple(args, "O!", &App::DocumentObjectPy::Type, &objpy)) { + DocumentObject* object = static_cast(objpy)->getDocumentObjectPtr(); + if (!object) { + PyErr_Format(PyExc_RuntimeError, "Invalid document object"); + return nullptr; + } + + if (object->getDocument() == getDocumentPtr()) { + getDocumentPtr()->removeObject(object); + Py_Return; + } + + std::stringstream str; + str << "Document object is not part of this document"; + throw Py::ValueError(str.str()); + } + + PyErr_SetString(PyExc_TypeError, "Expect str or DocumentObject"); + return nullptr; } PyObject* DocumentPy::copyObject(PyObject *args) From f0470e1ca804ccdcaede7947a22626ff24f97fb6 Mon Sep 17 00:00:00 2001 From: Syres916 <46537884+Syres916@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:53:51 +0000 Subject: [PATCH 5/6] [Base] Fix compiler warnings --- src/Base/BoundBoxPy.xml | 518 +++++++++++++++++++-------------------- src/Base/Interpreter.cpp | 449 ++++++++++++++++++--------------- src/Base/PyObjectBase.h | 24 +- src/Base/QuantityPy.xml | 28 +-- src/Base/RotationPy.xml | 13 +- src/Base/SmartPtrPy.cpp | 2 + src/Base/TypePy.xml | 9 +- src/Base/UnitPy.xml | 17 +- 8 files changed, 541 insertions(+), 519 deletions(-) diff --git a/src/Base/BoundBoxPy.xml b/src/Base/BoundBoxPy.xml index f7917f5cce0e..56b138f82604 100644 --- a/src/Base/BoundBoxPy.xml +++ b/src/Base/BoundBoxPy.xml @@ -1,259 +1,259 @@ - - - - - - This is the BoundBox export class - Base.BoundBox class.\n -This class represents a bounding box. -A bounding box is a rectangular cuboid which is a way to describe outer -boundaries and is obtained from a lot of 3D types. -It is often used to check if a 3D entity lies in the range of another object. -Checking for bounding interference first can save a lot of computing time! -An invalid BoundBox is represented by inconsistent values at each direction: -The maximum float value of the system at the minimum coordinates, and the -opposite value at the maximum coordinates.\n -The following constructors are supported:\n -BoundBox() -Empty constructor. Returns an invalid BoundBox.\n -BoundBox(boundBox) -Copy constructor. -boundBox : Base.BoundBox\n -BoundBox(xMin, yMin=0, zMin=0, xMax=0, yMax=0, zMax=0) -Define from the minimum and maximum values at each direction. -xMin : float\n Minimum value at x-coordinate. -yMin : float\n Minimum value at y-coordinate. -zMin : float\n Minimum value at z-coordinate. -xMax : float\n Maximum value at x-coordinate. -yMax : float\n Maximum value at y-coordinate. -zMax : float\n Maximum value at z-coordinate.\n -App.BoundBox(min, max) -Define from two containers representing the minimum and maximum values of the -coordinates in each direction. -min : Base.Vector, tuple\n Minimum values of the coordinates. -max : Base.Vector, tuple\n Maximum values of the coordinates. - - - - setVoid() -> None\n -Invalidate the bounding box. - - - - - isValid() -> bool\n -Checks if the bounding box is valid. - - - - - add(minMax) -> None -add(x, y, z) -> None\n -Increase the maximum values or decrease the minimum values of this BoundBox by -replacing the current values with the given values, so the bounding box can grow -but not shrink.\n -minMax : Base.Vector, tuple\n Values to enlarge at each direction. -x : float\n Value to enlarge at x-direction. -y : float\n Value to enlarge at y-direction. -z : float\n Value to enlarge at z-direction. - - - - - getPoint(index) ->Base.Vector\n -Get the point of the given index. -The index must be in the range of [0, 7].\n -index : int - - - - - getEdge(index) -> tuple of Base.Vector\n -Get the edge points of the given index. -The index must be in the range of [0, 11].\n -index : int - - - - - closestPoint(point) -> Base.Vector -closestPoint(x, y, z) -> Base.Vector\n -Get the closest point of the bounding box to the given point.\n -point : Base.Vector, tuple\n Coordinates of the given point. -x : float\n X-coordinate of the given point. -y : float\n Y-coordinate of the given point. -z : float\n Z-coordinate of the given point. - - - - - intersect(boundBox2) -> bool -intersect(base, dir) -> bool\n -Checks if the given object intersects with this bounding box. That can be -another bounding box or a line specified by base and direction.\n -boundBox2 : Base.BoundBox -base : Base.Vector, tuple -dir : Base.Vector, tuple - - - - - intersected(boundBox2) -> Base.BoundBox\n -Returns the intersection of this and the given bounding box.\n -boundBox2 : Base.BoundBox - - - - - united(boundBox2) -> Base.BoundBox\n -Returns the union of this and the given bounding box.\n -boundBox2 : Base.BoundBox - - - - - enlarge(variation) -> None\n -Decrease the minimum values and increase the maximum values by the given value. -A negative value shrinks the bounding box.\n -variation : float - - - - - - getIntersectionPoint(base, dir, epsilon=0.0001) -> Base.Vector\n -Calculate the intersection point of a line with the bounding box. -The base point must lie inside the bounding box, if not an exception is thrown.\n -base : Base.Vector\n Base point of the line. -dir : Base.Vector\n Direction of the line. -epsilon : float\n Bounding box size tolerance. - - - - - move(displacement) -> None -move(x, y, z) -> None\n -Move the bounding box by the given values.\n -displacement : Base.Vector, tuple\n Displacement at each direction. -x : float\n Displacement at x-direction. -y : float\n Displacement at y-direction. -z : float\n Displacement at z-direction. - - - - - scale(factor) -> None -scale(x, y, z) -> None\n -Scale the bounding box by the given values.\n -factor : Base.Vector, tuple\n Factor scale at each direction. -x : float\n Scale at x-direction. -y : float\n Scale at y-direction. -z : float\n Scale at z-direction. - - - - - transformed(matrix) -> Base.BoundBox\n -Returns a new BoundBox containing the transformed rectangular cuboid -represented by this BoundBox.\n -matrix : Base.Matrix\n Transformation matrix. - - - - - isCutPlane(base, normal) -> bool\n -Check if the plane specified by base and normal intersects (cuts) this bounding -box.\n -base : Base.Vector -normal : Base.Vector - - - - - isInside(object) -> bool -isInside(x, y, z) -> bool\n -Check if a point or a bounding box is inside this bounding box.\n -object : Base.Vector, Base.BoundBox\n Object to check if it is inside this bounding box. -x : float\n X-coordinate of the point to check. -y : float\n Y-coordinate of the point to check. -z : float\n Z-coordinate of the point to check. - - - - - Center point of the bounding box. - - - - - - The maximum x boundary position. - - - - - - The maximum y boundary position. - - - - - - The maximum z boundary position. - - - - - - The minimum x boundary position. - - - - - - The minimum y boundary position. - - - - - - The minimum z boundary position. - - - - - - Length of the bounding box in x direction. - - - - - - Length of the bounding box in y direction. - - - - - - Length of the bounding box in z direction. - - - - - - Diagonal length of the bounding box. - - - - - + + + + + + This is the BoundBox export class + Base.BoundBox class.\n +This class represents a bounding box. +A bounding box is a rectangular cuboid which is a way to describe outer +boundaries and is obtained from a lot of 3D types. +It is often used to check if a 3D entity lies in the range of another object. +Checking for bounding interference first can save a lot of computing time! +An invalid BoundBox is represented by inconsistent values at each direction: +The maximum float value of the system at the minimum coordinates, and the +opposite value at the maximum coordinates.\n +The following constructors are supported:\n +BoundBox() +Empty constructor. Returns an invalid BoundBox.\n +BoundBox(boundBox) +Copy constructor. +boundBox : Base.BoundBox\n +BoundBox(xMin, yMin=0, zMin=0, xMax=0, yMax=0, zMax=0) +Define from the minimum and maximum values at each direction. +xMin : float\n Minimum value at x-coordinate. +yMin : float\n Minimum value at y-coordinate. +zMin : float\n Minimum value at z-coordinate. +xMax : float\n Maximum value at x-coordinate. +yMax : float\n Maximum value at y-coordinate. +zMax : float\n Maximum value at z-coordinate.\n +App.BoundBox(min, max) +Define from two containers representing the minimum and maximum values of the +coordinates in each direction. +min : Base.Vector, tuple\n Minimum values of the coordinates. +max : Base.Vector, tuple\n Maximum values of the coordinates. + + + + setVoid() -> None\n +Invalidate the bounding box. + + + + + isValid() -> bool\n +Checks if the bounding box is valid. + + + + + add(minMax) -> None +add(x, y, z) -> None\n +Increase the maximum values or decrease the minimum values of this BoundBox by +replacing the current values with the given values, so the bounding box can grow +but not shrink.\n +minMax : Base.Vector, tuple\n Values to enlarge at each direction. +x : float\n Value to enlarge at x-direction. +y : float\n Value to enlarge at y-direction. +z : float\n Value to enlarge at z-direction. + + + + + getPoint(index) ->Base.Vector\n +Get the point of the given index. +The index must be in the range of [0, 7].\n +index : int + + + + + getEdge(index) -> tuple of Base.Vector\n +Get the edge points of the given index. +The index must be in the range of [0, 11].\n +index : int + + + + + closestPoint(point) -> Base.Vector +closestPoint(x, y, z) -> Base.Vector\n +Get the closest point of the bounding box to the given point.\n +point : Base.Vector, tuple\n Coordinates of the given point. +x : float\n X-coordinate of the given point. +y : float\n Y-coordinate of the given point. +z : float\n Z-coordinate of the given point. + + + + + intersect(boundBox2) -> bool +intersect(base, dir) -> bool\n +Checks if the given object intersects with this bounding box. That can be +another bounding box or a line specified by base and direction.\n +boundBox2 : Base.BoundBox +base : Base.Vector, tuple +dir : Base.Vector, tuple + + + + + intersected(boundBox2) -> Base.BoundBox\n +Returns the intersection of this and the given bounding box.\n +boundBox2 : Base.BoundBox + + + + + united(boundBox2) -> Base.BoundBox\n +Returns the union of this and the given bounding box.\n +boundBox2 : Base.BoundBox + + + + + enlarge(variation) -> None\n +Decrease the minimum values and increase the maximum values by the given value. +A negative value shrinks the bounding box.\n +variation : float + + + + + + getIntersectionPoint(base, dir, epsilon=0.0001) -> Base.Vector\n +Calculate the intersection point of a line with the bounding box. +The base point must lie inside the bounding box, if not an exception is thrown.\n +base : Base.Vector\n Base point of the line. +dir : Base.Vector\n Direction of the line. +epsilon : float\n Bounding box size tolerance. + + + + + move(displacement) -> None +move(x, y, z) -> None\n +Move the bounding box by the given values.\n +displacement : Base.Vector, tuple\n Displacement at each direction. +x : float\n Displacement at x-direction. +y : float\n Displacement at y-direction. +z : float\n Displacement at z-direction. + + + + + scale(factor) -> None +scale(x, y, z) -> None\n +Scale the bounding box by the given values.\n +factor : Base.Vector, tuple\n Factor scale at each direction. +x : float\n Scale at x-direction. +y : float\n Scale at y-direction. +z : float\n Scale at z-direction. + + + + + transformed(matrix) -> Base.BoundBox\n +Returns a new BoundBox containing the transformed rectangular cuboid +represented by this BoundBox.\n +matrix : Base.Matrix\n Transformation matrix. + + + + + isCutPlane(base, normal) -> bool\n +Check if the plane specified by base and normal intersects (cuts) this bounding +box.\n +base : Base.Vector +normal : Base.Vector + + + + + isInside(object) -> bool +isInside(x, y, z) -> bool\n +Check if a point or a bounding box is inside this bounding box.\n +object : Base.Vector, Base.BoundBox\n Object to check if it is inside this bounding box. +x : float\n X-coordinate of the point to check. +y : float\n Y-coordinate of the point to check. +z : float\n Z-coordinate of the point to check. + + + + + Center point of the bounding box. + + + + + + The maximum x boundary position. + + + + + + The maximum y boundary position. + + + + + + The maximum z boundary position. + + + + + + The minimum x boundary position. + + + + + + The minimum y boundary position. + + + + + + The minimum z boundary position. + + + + + + Length of the bounding box in x direction. + + + + + + Length of the bounding box in y direction. + + + + + + Length of the bounding box in z direction. + + + + + + Diagonal length of the bounding box. + + + + + \ No newline at end of file diff --git a/src/Base/Interpreter.cpp b/src/Base/Interpreter.cpp index 4e5c149584a6..6422a9efb1a6 100644 --- a/src/Base/Interpreter.cpp +++ b/src/Base/Interpreter.cpp @@ -39,12 +39,13 @@ #include "Stream.h" -char format2[1024]; //Warning! Can't go over 512 characters!!! +char format2[1024]; // Warning! Can't go over 512 characters!!! unsigned int format2_len = 1024; using namespace Base; -PyException::PyException(const Py::Object &obj) { +PyException::PyException(const Py::Object& obj) +{ _sErrMsg = obj.as_string(); // WARNING: we are assuming that python type object will never be // destroyed, so we don't keep reference here to save book-keeping in @@ -55,13 +56,13 @@ PyException::PyException(const Py::Object &obj) { PyException::PyException() { - PP_Fetch_Error_Text(); /* fetch (and clear) exception */ + PP_Fetch_Error_Text(); /* fetch (and clear) exception */ setPyObject(PP_PyDict_Object); std::string prefix = PP_last_error_type; /* exception name text */ -// prefix += ": "; - std::string error = PP_last_error_info; /* exception data text */ + // prefix += ": "; + std::string error = PP_last_error_info; /* exception data text */ #if 0 // The Python exceptions might be thrown from nested functions, so take // into account not to add the same prefix several times @@ -82,16 +83,15 @@ PyException::PyException() // our copy constructor and destructor Py_DECREF(PP_last_exception_type); PP_last_exception_type = nullptr; - } - _stackTrace = PP_last_error_trace; /* exception traceback text */ + _stackTrace = PP_last_error_trace; /* exception traceback text */ // This should be done in the constructor because when doing // in the destructor it's not always clear when it is called // and thus may clear a Python exception when it should not. PyGILStateLocker locker; - PyErr_Clear(); // must be called to keep Python interpreter in a valid state (Werner) + PyErr_Clear(); // must be called to keep Python interpreter in a valid state (Werner) } PyException::~PyException() throw() = default; @@ -103,7 +103,8 @@ void PyException::ThrowException() myexcp.raiseException(); } -void PyException::raiseException() { +void PyException::raiseException() +{ PyGILStateLocker locker; if (PP_PyDict_Object) { // delete the Python dict upon destruction of edict @@ -111,11 +112,12 @@ void PyException::raiseException() { PP_PyDict_Object = nullptr; std::string exceptionname; - if (_exceptionType == Base::PyExc_FC_FreeCADAbort) - edict.setItem("sclassname", - Py::String(typeid(Base::AbortException).name())); - if (_isReported) + if (_exceptionType == Base::PyExc_FC_FreeCADAbort) { + edict.setItem("sclassname", Py::String(typeid(Base::AbortException).name())); + } + if (_isReported) { edict.setItem("breported", Py::True()); + } Base::ExceptionFactory::Instance().raiseException(edict.ptr()); } @@ -128,21 +130,22 @@ void PyException::raiseException() { throw *this; } -void PyException::ReportException () const +void PyException::ReportException() const { if (!_isReported) { _isReported = true; - Base::Console().DeveloperError("pyException","%s%s: %s\n", - _stackTrace.c_str(), _errorType.c_str(), what()); + Base::Console().DeveloperError("pyException", + "%s%s: %s\n", + _stackTrace.c_str(), + _errorType.c_str(), + what()); } } void PyException::setPyException() const { std::stringstream str; - str << getStackTrace() - << getErrorType() - << ": " << what(); + str << getStackTrace() << getErrorType() << ": " << what(); PyErr_SetString(getPyExceptionType(), str.str().c_str()); } @@ -160,8 +163,8 @@ SystemExitException::SystemExitException() // sys.exit() | 1 | "System Exit" long int errCode = 1; - std::string errMsg = "System exit"; - PyObject *type, *value, *traceback, *code; + std::string errMsg = "System exit"; + PyObject *type, *value, *traceback, *code; PyGILStateLocker locker; PyErr_Fetch(&type, &value, &traceback); @@ -170,36 +173,37 @@ SystemExitException::SystemExitException() if (value) { code = PyObject_GetAttrString(value, "code"); if (code && value != Py_None) { - Py_DECREF(value); - value = code; + Py_DECREF(value); + value = code; } if (PyLong_Check(value)) { errCode = PyLong_AsLong(value); } else { - const char *str = PyUnicode_AsUTF8(value); - if (str) + const char* str = PyUnicode_AsUTF8(value); + if (str) { errMsg = errMsg + ": " + str; + } } } - _sErrMsg = errMsg; + _sErrMsg = errMsg; _exitCode = errCode; } // --------------------------------------------------------- // Fixes #0000831: python print causes File descriptor error on windows -class PythonStdOutput : public Py::PythonExtension +class PythonStdOutput: public Py::PythonExtension { public: static void init_type() { behaviors().name("PythonStdOutput"); behaviors().doc("Python standard output"); - add_varargs_method("write",&PythonStdOutput::write,"write()"); - add_varargs_method("flush",&PythonStdOutput::flush,"flush()"); + add_varargs_method("write", &PythonStdOutput::write, "write()"); + add_varargs_method("flush", &PythonStdOutput::flush, "flush()"); } PythonStdOutput() = default; @@ -225,27 +229,30 @@ InterpreterSingleton::InterpreterSingleton() InterpreterSingleton::~InterpreterSingleton() = default; -std::string InterpreterSingleton::runString(const char *sCmd) +std::string InterpreterSingleton::runString(const char* sCmd) { - PyObject *module, *dict, *presult; /* "exec code in d, d" */ + PyObject *module, *dict, *presult; /* "exec code in d, d" */ PyGILStateLocker locker; - module = PP_Load_Module("__main__"); /* get module, init python */ - if (!module) - throw PyException(); /* not incref'd */ - dict = PyModule_GetDict(module); /* get dict namespace */ - if (!dict) - throw PyException(); /* not incref'd */ + module = PP_Load_Module("__main__"); /* get module, init python */ + if (!module) { + throw PyException(); /* not incref'd */ + } + dict = PyModule_GetDict(module); /* get dict namespace */ + if (!dict) { + throw PyException(); /* not incref'd */ + } presult = PyRun_String(sCmd, Py_file_input, dict, dict); /* eval direct */ if (!presult) { - if (PyErr_ExceptionMatches(PyExc_SystemExit)) + if (PyErr_ExceptionMatches(PyExc_SystemExit)) { throw SystemExitException(); + } else { PyException::ThrowException(); - return std::string(); // just to quieten code analyzers - //throw PyException(); + return std::string(); // just to quieten code analyzers + // throw PyException(); } } @@ -272,7 +279,9 @@ std::string InterpreterSingleton::runString(const char *sCmd) * if the error occurs after changing it inside the script. */ -std::string InterpreterSingleton::runStringWithKey(const char *psCmd, const char *key, const char *key_initial_value) +std::string InterpreterSingleton::runStringWithKey(const char* psCmd, + const char* key, + const char* key_initial_value) { PyGILStateLocker locker; Py::Module module("__main__"); @@ -281,46 +290,52 @@ std::string InterpreterSingleton::runStringWithKey(const char *psCmd, const char Py::String initial_value(key_initial_value); localDictionary.setItem(key, initial_value); - PyObject* presult = PyRun_String(psCmd, Py_file_input, globalDictionary.ptr(), localDictionary.ptr()); + PyObject* presult = + PyRun_String(psCmd, Py_file_input, globalDictionary.ptr(), localDictionary.ptr()); if (!presult) { if (PyErr_ExceptionMatches(PyExc_SystemExit)) { throw SystemExitException(); } else { PyException::ThrowException(); - return std::string(); // just to quieten code analyzers + return std::string(); // just to quieten code analyzers } } Py_DECREF(presult); Py::Object key_return_value = localDictionary.getItem(key); - if (!key_return_value.isString()) + if (!key_return_value.isString()) { key_return_value = key_return_value.str(); + } Py::Bytes str = Py::String(key_return_value).encode("utf-8", "~E~"); std::string result = static_cast(str); return result; } -Py::Object InterpreterSingleton::runStringObject(const char *sCmd) +Py::Object InterpreterSingleton::runStringObject(const char* sCmd) { - PyObject *module, *dict, *presult; /* "exec code in d, d" */ + PyObject *module, *dict, *presult; /* "exec code in d, d" */ PyGILStateLocker locker; - module = PP_Load_Module("__main__"); /* get module, init python */ - if (!module) - throw PyException(); /* not incref'd */ - dict = PyModule_GetDict(module); /* get dict namespace */ - if (!dict) - throw PyException(); /* not incref'd */ + module = PP_Load_Module("__main__"); /* get module, init python */ + if (!module) { + throw PyException(); /* not incref'd */ + } + dict = PyModule_GetDict(module); /* get dict namespace */ + if (!dict) { + throw PyException(); /* not incref'd */ + } presult = PyRun_String(sCmd, Py_eval_input, dict, dict); /* eval direct */ if (!presult) { - if (PyErr_ExceptionMatches(PyExc_SystemExit)) + if (PyErr_ExceptionMatches(PyExc_SystemExit)) { throw SystemExitException(); - else + } + else { throw PyException(); + } } return Py::asObject(presult); @@ -334,16 +349,18 @@ void InterpreterSingleton::systemExit() PyErr_Fetch(&exception, &value, &tb); fflush(stdout); - if (!value || value == Py_None) + if (!value || value == Py_None) { goto done; + } if (PyExceptionInstance_Check(value)) { /* The error code should be in the `code' attribute. */ - PyObject *code = PyObject_GetAttrString(value, "code"); + PyObject* code = PyObject_GetAttrString(value, "code"); if (code) { Py_DECREF(value); value = code; - if (value == Py_None) + if (value == Py_None) { goto done; + } } /* If we failed to dig out the 'code' attribute, just let the else clause below print the error. */ @@ -368,17 +385,19 @@ void InterpreterSingleton::systemExit() /* NOTREACHED */ } -void InterpreterSingleton::runInteractiveString(const char *sCmd) +void InterpreterSingleton::runInteractiveString(const char* sCmd) { - PyObject *module, *dict, *presult; /* "exec code in d, d" */ + PyObject *module, *dict, *presult; /* "exec code in d, d" */ PyGILStateLocker locker; - module = PP_Load_Module("__main__"); /* get module, init python */ - if (!module) - throw PyException(); /* not incref'd */ - dict = PyModule_GetDict(module); /* get dict namespace */ - if (!dict) - throw PyException(); /* not incref'd */ + module = PP_Load_Module("__main__"); /* get module, init python */ + if (!module) { + throw PyException(); /* not incref'd */ + } + dict = PyModule_GetDict(module); /* get dict namespace */ + if (!dict) { + throw PyException(); /* not incref'd */ + } presult = PyRun_String(sCmd, Py_single_input, dict, dict); /* eval direct */ if (!presult) { @@ -390,33 +409,36 @@ void InterpreterSingleton::runInteractiveString(const char *sCmd) PyObject *errobj, *errdata, *errtraceback; PyErr_Fetch(&errobj, &errdata, &errtraceback); - RuntimeError exc(""); // do not use PyException since this clears the error indicator + RuntimeError exc(""); // do not use PyException since this clears the error indicator if (errdata) { - if (PyUnicode_Check(errdata)) + if (PyUnicode_Check(errdata)) { exc.setMessage(PyUnicode_AsUTF8(errdata)); + } } PyErr_Restore(errobj, errdata, errtraceback); - if (PyErr_Occurred()) + if (PyErr_Occurred()) { PyErr_Print(); + } throw exc; } - else + else { Py_DECREF(presult); + } } -void InterpreterSingleton::runFile(const char*pxFileName, bool local) +void InterpreterSingleton::runFile(const char* pxFileName, bool local) { #ifdef FC_OS_WIN32 FileInfo fi(pxFileName); - FILE *fp = _wfopen(fi.toStdWString().c_str(),L"r"); + FILE* fp = _wfopen(fi.toStdWString().c_str(), L"r"); #else - FILE *fp = fopen(pxFileName,"r"); + FILE* fp = fopen(pxFileName, "r"); #endif if (fp) { PyGILStateLocker locker; - //std::string encoding = PyUnicode_GetDefaultEncoding(); - //PyUnicode_SetDefaultEncoding("utf-8"); - //PyUnicode_SetDefaultEncoding(encoding.c_str()); + // std::string encoding = PyUnicode_GetDefaultEncoding(); + // PyUnicode_SetDefaultEncoding("utf-8"); + // PyUnicode_SetDefaultEncoding(encoding.c_str()); PyObject *module, *dict; module = PyImport_AddModule("__main__"); dict = PyModule_GetDict(module); @@ -424,11 +446,11 @@ void InterpreterSingleton::runFile(const char*pxFileName, bool local) dict = PyDict_Copy(dict); } else { - Py_INCREF(dict); // avoid to further distinguish between local and global dict + Py_INCREF(dict); // avoid to further distinguish between local and global dict } if (!PyDict_GetItemString(dict, "__file__")) { - PyObject *pyObj = PyUnicode_FromString(pxFileName); + PyObject* pyObj = PyUnicode_FromString(pxFileName); if (!pyObj) { fclose(fp); Py_DECREF(dict); @@ -443,15 +465,17 @@ void InterpreterSingleton::runFile(const char*pxFileName, bool local) Py_DECREF(pyObj); } - PyObject *result = PyRun_File(fp, pxFileName, Py_file_input, dict, dict); + PyObject* result = PyRun_File(fp, pxFileName, Py_file_input, dict, dict); fclose(fp); Py_DECREF(dict); if (!result) { - if (PyErr_ExceptionMatches(PyExc_SystemExit)) + if (PyErr_ExceptionMatches(PyExc_SystemExit)) { throw SystemExitException(); - else + } + else { throw PyException(); + } } Py_DECREF(result); } @@ -463,17 +487,19 @@ void InterpreterSingleton::runFile(const char*pxFileName, bool local) bool InterpreterSingleton::loadModule(const char* psModName) { // buffer acrobatics - //PyBuf ModName(psModName); - PyObject *module; + // PyBuf ModName(psModName); + PyObject* module; PyGILStateLocker locker; module = PP_Load_Module(psModName); if (!module) { - if (PyErr_ExceptionMatches(PyExc_SystemExit)) + if (PyErr_ExceptionMatches(PyExc_SystemExit)) { throw SystemExitException(); - else + } + else { throw PyException(); + } } return true; @@ -489,22 +515,23 @@ void InterpreterSingleton::cleanupModules() { // This is only needed to make the address sanitizer happy #if defined(__has_feature) -# if __has_feature(address_sanitizer) +#if __has_feature(address_sanitizer) for (auto it : _modules) { delete it; } _modules.clear(); -# endif +#endif #endif } -void InterpreterSingleton::addType(PyTypeObject* Type,PyObject* Module, const char * Name) +void InterpreterSingleton::addType(PyTypeObject* Type, PyObject* Module, const char* Name) { // NOTE: To finish the initialization of our own type objects we must // call PyType_Ready, otherwise we run into a segmentation fault, later on. // This function is responsible for adding inherited slots from a type's base class. - if (PyType_Ready(Type) < 0) + if (PyType_Ready(Type) < 0) { return; + } PyModule_AddObject(Module, Name, Base::getTypeAsObject(Type)); } @@ -546,11 +573,11 @@ std::string InterpreterSingleton::getPythonPath() std::string InterpreterSingleton::init(int argc, char* argv[]) { if (!Py_IsInitialized()) { - Py_SetProgramName(Py_DecodeLocale(argv[0],nullptr)); - // There is a serious bug in VS from 2010 until 2013 where the file descriptor for stdin, stdout or stderr - // returns a valid value for GUI applications (i.e. subsystem = Windows) where it shouldn't. - // This causes Python to fail during initialization. - // A workaround is to use freopen on stdin, stdout and stderr. See the class Redirection inside main() + Py_SetProgramName(Py_DecodeLocale(argv[0], nullptr)); + // There is a serious bug in VS from 2010 until 2013 where the file descriptor for stdin, + // stdout or stderr returns a valid value for GUI applications (i.e. subsystem = Windows) + // where it shouldn't. This causes Python to fail during initialization. A workaround is to + // use freopen on stdin, stdout and stderr. See the class Redirection inside main() // https://bugs.python.org/issue17797#msg197474 // Py_Initialize(); @@ -558,39 +585,40 @@ std::string InterpreterSingleton::init(int argc, char* argv[]) if (virtualenv) { PyRun_SimpleString( "# Check for virtualenv, and activate if present.\n" - "# See https://virtualenv.pypa.io/en/latest/userguide/#using-virtualenv-without-bin-python\n" + "# See " + "https://virtualenv.pypa.io/en/latest/" + "#using-virtualenv-without-bin-python\n" "import os\n" "import sys\n" "base_path = os.getenv(\"VIRTUAL_ENV\")\n" "if not base_path is None:\n" " activate_this = os.path.join(base_path, \"bin\", \"activate_this.py\")\n" - " exec(open(activate_this).read(), {'__file__':activate_this})\n" - ); + " exec(open(activate_this).read(), {'__file__':activate_this})\n"); } -#if PY_VERSION_HEX < 0x03090000 - PyEval_InitThreads(); -#endif - size_t size = argc; - static std::vector _argv(size); + static std::vector _argv(size); for (int i = 0; i < argc; i++) { - _argv[i] = Py_DecodeLocale(argv[i],nullptr); + _argv[i] = Py_DecodeLocale(argv[i], nullptr); } PySys_SetArgv(argc, _argv.data()); PythonStdOutput::init_type(); this->_global = PyEval_SaveThread(); } - return getPythonPath(); + PyGILStateLocker lock; + return Py_EncodeLocale(Py_GetPath(), nullptr); } #else -namespace { -void initInterpreter(int argc,char *argv[]) +namespace +{ +void initInterpreter(int argc, char* argv[]) { PyStatus status; PyConfig config; PyConfig_InitIsolatedConfig(&config); + config.isolated = 0; + config.user_site_directory = 1; status = PyConfig_SetBytesArgv(&config, argc, argv); if (PyStatus_Exception(status)) { @@ -602,24 +630,24 @@ void initInterpreter(int argc,char *argv[]) throw Base::RuntimeError("Failed to init from config"); } - PyConfig_Clear(&config); - - Py_Initialize(); + // If FreeCAD was run from within a Python virtual environment, ensure that the site-packages + // directory from that environment is used. const char* virtualenv = getenv("VIRTUAL_ENV"); if (virtualenv) { - PyRun_SimpleString( - "# Check for virtualenv, and activate if present.\n" - "# See https://virtualenv.pypa.io/en/latest/userguide/#using-virtualenv-without-bin-python\n" - "import os\n" - "import sys\n" - "base_path = os.getenv(\"VIRTUAL_ENV\")\n" - "if not base_path is None:\n" - " activate_this = os.path.join(base_path, \"bin\", \"activate_this.py\")\n" - " exec(open(activate_this).read(), {'__file__':activate_this})\n" - ); + std::wstringstream ss; + PyConfig_Read(&config); + ss << virtualenv << L"/lib/python" << PY_MAJOR_VERSION << "." << PY_MINOR_VERSION + << "/site-packages"; + PyObject* venvLocation = PyUnicode_FromWideChar(ss.str().c_str(), ss.str().size()); + PyObject* path = PySys_GetObject("path"); + PyList_Append(path, venvLocation); } + + PyConfig_Clear(&config); + + Py_Initialize(); } -} +} // namespace std::string InterpreterSingleton::init(int argc, char* argv[]) { try { @@ -629,11 +657,9 @@ std::string InterpreterSingleton::init(int argc, char* argv[]) PythonStdOutput::init_type(); this->_global = PyEval_SaveThread(); } - - PyGILStateLocker lock; - return Py_EncodeLocale(Py_GetPath(),nullptr); + return getPythonPath(); } - catch (const Base::Exception& e) { + catch (const Exception& e) { e.ReportException(); throw; } @@ -650,7 +676,7 @@ void InterpreterSingleton::replaceStdOutput() int InterpreterSingleton::cleanup(void (*func)()) { - return Py_AtExit( func ); + return Py_AtExit(func); } void InterpreterSingleton::finalize() @@ -664,14 +690,14 @@ void InterpreterSingleton::finalize() } } -void InterpreterSingleton::runStringArg(const char * psCom,...) +void InterpreterSingleton::runStringArg(const char* psCom, ...) { // va stuff va_list namelessVars; va_start(namelessVars, psCom); // Get the "..." vars int len = vsnprintf(format2, format2_len, psCom, namelessVars); va_end(namelessVars); - if ( len == -1 ) { + if (len == -1) { // argument too long assert(false); } @@ -682,13 +708,14 @@ void InterpreterSingleton::runStringArg(const char * psCom,...) // Singleton: -InterpreterSingleton * InterpreterSingleton::_pcSingleton = nullptr; +InterpreterSingleton* InterpreterSingleton::_pcSingleton = nullptr; -InterpreterSingleton & InterpreterSingleton::Instance() +InterpreterSingleton& InterpreterSingleton::Instance() { // not initialized! - if (!_pcSingleton) + if (!_pcSingleton) { _pcSingleton = new InterpreterSingleton(); + } return *_pcSingleton; } @@ -700,7 +727,7 @@ void InterpreterSingleton::Destruct() _pcSingleton = nullptr; } -int InterpreterSingleton::runCommandLine(const char *prompt) +int InterpreterSingleton::runCommandLine(const char* prompt) { PyGILStateLocker locker; return PP_Run_Command_Line(prompt); @@ -710,51 +737,57 @@ int InterpreterSingleton::runCommandLine(const char *prompt) * Runs a member method of an object with no parameter and no return value * void (void). There are other methods to run with returns */ -void InterpreterSingleton::runMethodVoid(PyObject *pobject, const char *method) +void InterpreterSingleton::runMethodVoid(PyObject* pobject, const char* method) { PyGILStateLocker locker; - if (PP_Run_Method(pobject , // object - method, // run method - nullptr, // no return type - nullptr, // so no return object - "()") // no arguments - != 0) + if (PP_Run_Method(pobject, // object + method, // run method + nullptr, // no return type + nullptr, // so no return object + "()") // no arguments + != 0) { throw PyException(/*"Error running InterpreterSingleton::RunMethodVoid()"*/); - + } } -PyObject* InterpreterSingleton::runMethodObject(PyObject *pobject, const char *method) +PyObject* InterpreterSingleton::runMethodObject(PyObject* pobject, const char* method) { - PyObject *pcO; + PyObject* pcO; PyGILStateLocker locker; - if (PP_Run_Method(pobject , // object - method, // run method - "O", // return type - &pcO, // return object - "()") // no arguments - != 0) + if (PP_Run_Method(pobject, // object + method, // run method + "O", // return type + &pcO, // return object + "()") // no arguments + != 0) { throw PyException(); + } return pcO; } -void InterpreterSingleton::runMethod(PyObject *pobject, const char *method, - const char *resfmt, void *cresult, /* convert to c/c++ */ - const char *argfmt, ... ) /* convert to python */ +void InterpreterSingleton::runMethod(PyObject* pobject, + const char* method, + const char* resfmt, + void* cresult, /* convert to c/c++ */ + const char* argfmt, + ...) /* convert to python */ { PyObject *pmeth, *pargs, *presult; - va_list argslist; /* "pobject.method(args)" */ + va_list argslist; /* "pobject.method(args)" */ va_start(argslist, argfmt); PyGILStateLocker locker; pmeth = PyObject_GetAttrString(pobject, method); - if (!pmeth) { /* get callable object */ + if (!pmeth) { /* get callable object */ va_end(argslist); - throw AttributeError("Error running InterpreterSingleton::RunMethod() method not defined"); /* bound method? has self */ + throw AttributeError( + "Error running InterpreterSingleton::RunMethod() method not defined"); /* bound method? + has self */ } - pargs = Py_VaBuildValue(argfmt, argslist); /* args: c->python */ + pargs = Py_VaBuildValue(argfmt, argslist); /* args: c->python */ va_end(argslist); if (!pargs) { @@ -763,31 +796,35 @@ void InterpreterSingleton::runMethod(PyObject *pobject, const char *method, } #if PY_VERSION_HEX < 0x03090000 - presult = PyEval_CallObject(pmeth, pargs); /* run interpreter */ + presult = PyEval_CallObject(pmeth, pargs); /* run interpreter */ #else - presult = PyObject_CallObject(pmeth, pargs); /* run interpreter */ + presult = PyObject_CallObject(pmeth, pargs); /* run interpreter */ #endif Py_DECREF(pmeth); Py_DECREF(pargs); - if (PP_Convert_Result(presult, resfmt, cresult)!= 0) { - if ( PyErr_Occurred() ) + if (PP_Convert_Result(presult, resfmt, cresult) != 0) { + if (PyErr_Occurred()) { PyErr_Print(); - throw RuntimeError("Error running InterpreterSingleton::RunMethod() exception in called method"); + } + throw RuntimeError( + "Error running InterpreterSingleton::RunMethod() exception in called method"); } } -PyObject * InterpreterSingleton::getValue(const char * key, const char * result_var) +PyObject* InterpreterSingleton::getValue(const char* key, const char* result_var) { - PyObject *module, *dict, *presult; /* "exec code in d, d" */ + PyObject *module, *dict, *presult; /* "exec code in d, d" */ PyGILStateLocker locker; - module = PP_Load_Module("__main__"); /* get module, init python */ - if (!module) - throw PyException(); /* not incref'd */ - dict = PyModule_GetDict(module); /* get dict namespace */ - if (!dict) - throw PyException(); /* not incref'd */ + module = PP_Load_Module("__main__"); /* get module, init python */ + if (!module) { + throw PyException(); /* not incref'd */ + } + dict = PyModule_GetDict(module); /* get dict namespace */ + if (!dict) { + throw PyException(); /* not incref'd */ + } presult = PyRun_String(key, Py_file_input, dict, dict); /* eval direct */ @@ -801,45 +838,41 @@ PyObject * InterpreterSingleton::getValue(const char * key, const char * result_ void InterpreterSingleton::dbgObserveFile(const char* sFileName) { - if (sFileName) + if (sFileName) { _cDebugFileName = sFileName; - else + } + else { _cDebugFileName = ""; + } } void InterpreterSingleton::dbgSetBreakPoint(unsigned int /*uiLineNumber*/) -{ - -} +{} void InterpreterSingleton::dbgUnsetBreakPoint(unsigned int /*uiLineNumber*/) -{ - -} +{} void InterpreterSingleton::dbgStep() -{ - -} +{} const std::string InterpreterSingleton::strToPython(const char* Str) { std::string result; - const char *It=Str; + const char* It = Str; while (*It != '\0') { switch (*It) { - case '\\': - result += "\\\\"; - break; - case '\"': - result += "\\\""; - break; - case '\'': - result += "\\\'"; - break; - default: - result += *It; + case '\\': + result += "\\\\"; + break; + case '\"': + result += "\\\""; + break; + case '\'': + result += "\\\'"; + break; + default: + result += *It; } It++; } @@ -860,8 +893,9 @@ int getSWIGVersionFromModule(const std::string& module) try { // Get the module and check its __file__ attribute Py::Dict dict(PyImport_GetModuleDict()); - if (!dict.hasKey(module)) + if (!dict.hasKey(module)) { return 0; + } Py::Module mod(module); Py::String file(mod.getAttr("__file__")); std::string filename = (std::string)file; @@ -880,7 +914,7 @@ int getSWIGVersionFromModule(const std::string& module) int major = std::atoi(what[1].first); int minor = std::atoi(what[2].first); int micro = std::atoi(what[3].first); - int version = (major<<16)+(minor<<8)+micro; + int version = (major << 16) + (minor << 8) + micro; moduleMap[module] = version; return version; } @@ -898,18 +932,22 @@ int getSWIGVersionFromModule(const std::string& module) } #if (defined(HAVE_SWIG) && (HAVE_SWIG == 1)) -namespace Swig_python { +namespace Swig_python +{ extern int createSWIGPointerObj_T(const char* TypeName, void* obj, PyObject** ptr, int own); extern int convertSWIGPointerObj_T(const char* TypeName, PyObject* obj, void** ptr, int flags); extern void cleanupSWIG_T(const char* TypeName); extern int getSWIGPointerTypeObj_T(const char* TypeName, PyTypeObject** ptr); -} +} // namespace Swig_python #endif -PyObject* InterpreterSingleton::createSWIGPointerObj(const char* Module, const char* TypeName, void* Pointer, int own) +PyObject* InterpreterSingleton::createSWIGPointerObj(const char* Module, + const char* TypeName, + void* Pointer, + int own) { int result = 0; - PyObject* proxy=nullptr; + PyObject* proxy = nullptr; PyGILStateLocker locker; (void)Module; #if (defined(HAVE_SWIG) && (HAVE_SWIG == 1)) @@ -918,17 +956,22 @@ PyObject* InterpreterSingleton::createSWIGPointerObj(const char* Module, const c (void)TypeName; (void)Pointer; (void)own; - result = -1; // indicates error + result = -1; // indicates error #endif - if (result == 0) + if (result == 0) { return proxy; + } // none of the SWIG's succeeded throw Base::RuntimeError("No SWIG wrapped library loaded"); } -bool InterpreterSingleton::convertSWIGPointerObj(const char* Module, const char* TypeName, PyObject* obj, void** ptr, int flags) +bool InterpreterSingleton::convertSWIGPointerObj(const char* Module, + const char* TypeName, + PyObject* obj, + void** ptr, + int flags) { int result = 0; PyGILStateLocker locker; @@ -940,11 +983,12 @@ bool InterpreterSingleton::convertSWIGPointerObj(const char* Module, const char* (void)obj; (void)ptr; (void)flags; - result = -1; // indicates error + result = -1; // indicates error #endif - if (result == 0) + if (result == 0) { return true; + } // none of the SWIG's succeeded throw Base::RuntimeError("No SWIG wrapped library loaded"); @@ -970,11 +1014,12 @@ PyTypeObject* InterpreterSingleton::getSWIGPointerTypeObj(const char* Module, co result = Swig_python::getSWIGPointerTypeObj_T(TypeName, &proxy); #else (void)TypeName; - result = -1; // indicates error + result = -1; // indicates error #endif - if (result == 0) + if (result == 0) { return proxy; + } // none of the SWIG's succeeded throw Base::RuntimeError("No SWIG wrapped library loaded"); diff --git a/src/Base/PyObjectBase.h b/src/Base/PyObjectBase.h index 5143c59bafd2..a738c6219693 100644 --- a/src/Base/PyObjectBase.h +++ b/src/Base/PyObjectBase.h @@ -25,36 +25,16 @@ // Std. configurations -// (re-)defined in pyconfig.h -#if defined (_POSIX_C_SOURCE) -# undef _POSIX_C_SOURCE -#endif -#if defined (_XOPEN_SOURCE) -# undef _XOPEN_SOURCE -#endif - -// needed header -#undef slots -#include -#ifdef FC_OS_MACOSX -#undef toupper -#undef tolower -#undef isupper -#undef islower -#undef isspace -#undef isalpha -#undef isalnum -#endif -#define slots #include #include -#include "Exception.h" #ifndef PYCXX_PYTHON_2TO3 #define PYCXX_PYTHON_2TO3 #endif + #include +#include "Exception.h" /** Python static class macro for definition * sets up a static function entry in a class inheriting diff --git a/src/Base/QuantityPy.xml b/src/Base/QuantityPy.xml index 881b58a70b68..0a874e8b3da4 100644 --- a/src/Base/QuantityPy.xml +++ b/src/Base/QuantityPy.xml @@ -29,38 +29,36 @@ Quantity(string) -- arbitrary mixture of numbers and chars defining a Quantity - - toStr([decimals]) - returns a string representation rounded to number of decimals. If no decimals are specified then - the internal precision is used + toStr([decimals]) + +Returns a string representation rounded to number of decimals. If no decimals are specified then +the internal precision is used - returns a quantity with the translation factor and a string with the prevered unit + Returns a quantity with the translation factor and a string with the prevered unit - - returns a floating point value as the provided unit + Returns a floating point value as the provided unit - Following parameters are allowed: - getValueAs('m/s') # unit string to parse - getValueAs(2.45,1) # translation value and unit signature - getValueAs(FreeCAD.Units.Pascal) # predefined standard units - getValueAs(Qantity('N/m^2')) # a quantity - getValueAs(Unit(0,1,0,0,0,0,0,0)) # a unit +Following parameters are allowed: +getValueAs('m/s') # unit string to parse +getValueAs(2.45,1) # translation value and unit signature +getValueAs(FreeCAD.Units.Pascal) # predefined standard units +getValueAs(Qantity('N/m^2')) # a quantity +getValueAs(Unit(0,1,0,0,0,0,0,0)) # a unit - -Return the Integral closest to x, rounding half toward even. + Returns the Integral closest to x, rounding half toward even. When an argument is passed, work like built-in round(x, ndigits). diff --git a/src/Base/RotationPy.xml b/src/Base/RotationPy.xml index 56df46d65520..19aa31f0f4c6 100644 --- a/src/Base/RotationPy.xml +++ b/src/Base/RotationPy.xml @@ -75,7 +75,7 @@ coef : sequence of float Sets the rotation to its inverse. - + inverted() -> Base.Rotation\n Returns the inverse of the rotation. @@ -190,12 +190,11 @@ tol : float\n Tolerance used to check for identity. - - public: - RotationPy(const Rotation & mat, PyTypeObject *T = &Type) - :PyObjectBase(new Rotation(mat),T){} - Rotation value() const - { return *(getRotationPtr()); } + public: + RotationPy(const Rotation & mat, PyTypeObject *T = &Type) + :PyObjectBase(new Rotation(mat),T){} + Rotation value() const + { return *(getRotationPtr()); } diff --git a/src/Base/SmartPtrPy.cpp b/src/Base/SmartPtrPy.cpp index 1a6bdf860444..ec67705b4086 100644 --- a/src/Base/SmartPtrPy.cpp +++ b/src/Base/SmartPtrPy.cpp @@ -26,6 +26,7 @@ #endif #include "SmartPtrPy.h" +#include "Interpreter.h" #include @@ -42,6 +43,7 @@ namespace Py { void SmartPtr::release() { + Base::PyGILStateLocker lock; Py::_XDECREF( p ); p = nullptr; } diff --git a/src/Base/TypePy.xml b/src/Base/TypePy.xml index e8fba162e3bc..10214a6ad737 100644 --- a/src/Base/TypePy.xml +++ b/src/Base/TypePy.xml @@ -17,11 +17,10 @@ namespace Base { This is the Type class - Base.BaseType class.\n -This class is not intended to create instances of itself, but to get information -from the different types and create instances of them. -Regarding instantiation, this is possible in cases that inherit from the -Base::BaseClass class and are not abstract classes. + BaseTypePy class. + +This class provides functionality related to type management in the Base module. It's not intended for direct instantiation but for accessing type information and creating instances of various types. +Instantiation is possible for classes that inherit from the Base::BaseClass class and are not abstract. diff --git a/src/Base/UnitPy.xml b/src/Base/UnitPy.xml index 09ac93288988..866b3536c8de 100644 --- a/src/Base/UnitPy.xml +++ b/src/Base/UnitPy.xml @@ -15,16 +15,15 @@ FatherNamespace="Base"> - - Unit - defines a unit type, calculate and compare. + Unit +defines a unit type, calculate and compare. - The following constructors are supported: - Unit() -- empty constructor - Unit(i1,i2,i3,i4,i5,i6,i7,i8) -- unit signature - Unit(Quantity) -- copy unit from Quantity - Unit(Unit) -- copy constructor - Unit(string) -- parse the string for units +The following constructors are supported: +Unit() -- empty constructor +Unit(i1,i2,i3,i4,i5,i6,i7,i8) -- unit signature +Unit(Quantity) -- copy unit from Quantity +Unit(Unit) -- copy constructor +Unit(string) -- parse the string for units Unit From 8685a68d043e0badec02d3e0ca818de12dd04395 Mon Sep 17 00:00:00 2001 From: Syres916 <46537884+Syres916@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:57:10 +0000 Subject: [PATCH 6/6] [Gui] Fix compiler warnings --- src/Gui/PythonEditor.cpp | 938 ++++++++++---------- src/Gui/ReportView.cpp | 1804 +++++++++++++++++++------------------- 2 files changed, 1391 insertions(+), 1351 deletions(-) diff --git a/src/Gui/PythonEditor.cpp b/src/Gui/PythonEditor.cpp index b589fbdb04a7..67f019d08958 100644 --- a/src/Gui/PythonEditor.cpp +++ b/src/Gui/PythonEditor.cpp @@ -1,471 +1,467 @@ -/*************************************************************************** - * Copyright (c) 2004 Werner Mayer * - * * - * 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" -#ifndef _PreComp_ -# include -# include -# include -# include -# include -#endif - -#include - -#include "PythonEditor.h" -#include "Application.h" -#include "BitmapFactory.h" -#include "Macro.h" -#include "PythonDebugger.h" - - -using namespace Gui; - -namespace Gui { -struct PythonEditorP -{ - int debugLine; - QRect debugRect; - QPixmap breakpoint; - QPixmap debugMarker; - QString filename; - PythonDebugger* debugger; - PythonEditorP() - : debugLine(-1), - breakpoint(BitmapFactory().iconFromTheme("breakpoint").pixmap(16,16)), - debugMarker(BitmapFactory().iconFromTheme("debug-marker").pixmap(16,16)) - { - debugger = Application::Instance->macroManager()->debugger(); - } -}; -} // namespace Gui - -/* TRANSLATOR Gui::PythonEditor */ - -/** - * Constructs a PythonEditor which is a child of 'parent' and does the - * syntax highlighting for the Python language. - */ -PythonEditor::PythonEditor(QWidget* parent) - : TextEditor(parent) -{ - d = new PythonEditorP(); - this->setSyntaxHighlighter(new PythonSyntaxHighlighter(this)); - - // set acelerators - auto comment = new QShortcut(this); - comment->setKey(QKeySequence(QString::fromLatin1("ALT+C"))); - - auto uncomment = new QShortcut(this); - uncomment->setKey(QKeySequence(QString::fromLatin1("ALT+U"))); - - connect(comment, &QShortcut::activated, this, &PythonEditor::onComment); - connect(uncomment, &QShortcut::activated, this, &PythonEditor::onUncomment); -} - -/** Destroys the object and frees any allocated resources */ -PythonEditor::~PythonEditor() -{ - delete d; -} - -void PythonEditor::setFileName(const QString& fn) -{ - d->filename = fn; -} - -void PythonEditor::startDebug() -{ - if (d->debugger->start()) { - d->debugger->runFile(d->filename); - d->debugger->stop(); - } -} - -void PythonEditor::toggleBreakpoint() -{ - QTextCursor cursor = textCursor(); - int line = cursor.blockNumber() + 1; - d->debugger->toggleBreakpoint(line, d->filename); - getMarker()->update(); -} - -void PythonEditor::showDebugMarker(int line) -{ - d->debugLine = line; - getMarker()->update(); - QTextCursor cursor = textCursor(); - cursor.movePosition(QTextCursor::StartOfBlock); - int cur = cursor.blockNumber() + 1; - if (cur > line) { - for (int i=line; idebugLine = -1; - getMarker()->update(); -} - -void PythonEditor::drawMarker(int line, int x, int y, QPainter* p) -{ - Breakpoint bp = d->debugger->getBreakpoint(d->filename); - if (bp.checkLine(line)) { - p->drawPixmap(x, y, d->breakpoint); - } - if (d->debugLine == line) { - p->drawPixmap(x, y+2, d->debugMarker); - d->debugRect = QRect(x, y+2, d->debugMarker.width(), d->debugMarker.height()); - } -} - -void PythonEditor::contextMenuEvent ( QContextMenuEvent * e ) -{ - QMenu* menu = createStandardContextMenu(); - if (!isReadOnly()) { - menu->addSeparator(); - menu->addAction( tr("Comment"), this, &PythonEditor::onComment, QKeySequence(QString::fromLatin1("ALT+C"))); - menu->addAction( tr("Uncomment"), this, &PythonEditor::onUncomment, QKeySequence(QString::fromLatin1("ALT+U"))); - } - - menu->exec(e->globalPos()); - delete menu; -} - -void PythonEditor::onComment() -{ - QTextCursor cursor = textCursor(); - int selStart = cursor.selectionStart(); - int selEnd = cursor.selectionEnd(); - QTextBlock block; - cursor.beginEditBlock(); - for (block = document()->begin(); block.isValid(); block = block.next()) { - int pos = block.position(); - int off = block.length()-1; - // at least one char of the block is part of the selection - if ( pos >= selStart || pos+off >= selStart) { - if ( pos+1 > selEnd ) - break; // end of selection reached - cursor.setPosition(block.position()); - cursor.insertText(QLatin1String("#")); - selEnd++; - } - } - - cursor.endEditBlock(); -} - -void PythonEditor::onUncomment() -{ - QTextCursor cursor = textCursor(); - int selStart = cursor.selectionStart(); - int selEnd = cursor.selectionEnd(); - QTextBlock block; - cursor.beginEditBlock(); - for (block = document()->begin(); block.isValid(); block = block.next()) { - int pos = block.position(); - int off = block.length()-1; - // at least one char of the block is part of the selection - if ( pos >= selStart || pos+off >= selStart) { - if ( pos+1 > selEnd ) - break; // end of selection reached - if (block.text().startsWith(QLatin1String("#"))) { - cursor.setPosition(block.position()); - cursor.deleteChar(); - selEnd--; - } - } - } - - cursor.endEditBlock(); -} - -// ------------------------------------------------------------------------ - -namespace Gui { -class PythonSyntaxHighlighterP -{ -public: - PythonSyntaxHighlighterP() - { - keywords << QLatin1String("and") << QLatin1String("as") - << QLatin1String("assert") << QLatin1String("break") - << QLatin1String("class") << QLatin1String("continue") - << QLatin1String("def") << QLatin1String("del") - << QLatin1String("elif") << QLatin1String("else") - << QLatin1String("except") << QLatin1String("exec") - << QLatin1String("False") << QLatin1String("finally") - << QLatin1String("for") << QLatin1String("from") - << QLatin1String("global") << QLatin1String("if") - << QLatin1String("import") << QLatin1String("in") - << QLatin1String("is") << QLatin1String("lambda") - << QLatin1String("None") << QLatin1String("nonlocal") - << QLatin1String("not") << QLatin1String("or") - << QLatin1String("pass") << QLatin1String("print") - << QLatin1String("raise") << QLatin1String("return") - << QLatin1String("True") << QLatin1String("try") - << QLatin1String("while") << QLatin1String("with") - << QLatin1String("yield"); - } - - QStringList keywords; -}; -} // namespace Gui - -/** - * Constructs a Python syntax highlighter. - */ -PythonSyntaxHighlighter::PythonSyntaxHighlighter(QObject* parent) - : SyntaxHighlighter(parent) -{ - d = new PythonSyntaxHighlighterP; -} - -/** Destroys this object. */ -PythonSyntaxHighlighter::~PythonSyntaxHighlighter() -{ - delete d; -} - -/** - * Detects all kinds of text to highlight them in the correct color. - */ -void PythonSyntaxHighlighter::highlightBlock (const QString & text) -{ - int i = 0; - QChar prev, ch; - - const int Standard = 0; // Standard text - const int Digit = 1; // Digits - const int Comment = 2; // Comment begins with # - const int Literal1 = 3; // String literal beginning with " - const int Literal2 = 4; // Other string literal beginning with ' - const int Blockcomment1 = 5; // Block comments beginning and ending with """ - const int Blockcomment2 = 6; // Other block comments beginning and ending with ''' - const int ClassName = 7; // Text after the keyword class - const int DefineName = 8; // Text after the keyword def - - int endStateOfLastPara = previousBlockState(); - if (endStateOfLastPara < 0 || endStateOfLastPara > maximumUserState()) - endStateOfLastPara = Standard; - - while ( i < text.length() ) - { - ch = text.at( i ); - - switch ( endStateOfLastPara ) - { - case Standard: - { - switch ( ch.unicode() ) - { - case '#': - { - // begin a comment - setFormat( i, 1, this->colorByType(SyntaxHighlighter::Comment)); - endStateOfLastPara=Comment; - } break; - case '"': - { - // Begin either string literal or block comment - if ((i>=2) && text.at(i-1) == QLatin1Char('"') && - text.at(i-2) == QLatin1Char('"')) - { - setFormat( i-2, 3, this->colorByType(SyntaxHighlighter::BlockComment)); - endStateOfLastPara=Blockcomment1; - } - else - { - setFormat( i, 1, this->colorByType(SyntaxHighlighter::String)); - endStateOfLastPara=Literal1; - } - } break; - case '\'': - { - // Begin either string literal or block comment - if ((i>=2) && text.at(i-1) == QLatin1Char('\'') && - text.at(i-2) == QLatin1Char('\'')) - { - setFormat( i-2, 3, this->colorByType(SyntaxHighlighter::BlockComment)); - endStateOfLastPara=Blockcomment2; - } - else - { - setFormat( i, 1, this->colorByType(SyntaxHighlighter::String)); - endStateOfLastPara=Literal2; - } - } break; - case ' ': - case '\t': - { - // ignore whitespaces - } break; - case '(': case ')': case '[': case ']': - case '+': case '-': case '*': case '/': - case ':': case '%': case '^': case '~': - case '!': case '=': case '<': case '>': // possibly two characters - { - setFormat(i, 1, this->colorByType(SyntaxHighlighter::Operator)); - endStateOfLastPara=Standard; - } break; - default: - { - // Check for normal text - if ( ch.isLetter() || ch == QLatin1Char('_') ) - { - QString buffer; - int j=i; - while ( ch.isLetterOrNumber() || ch == QLatin1Char('_') ) { - buffer += ch; - ++j; - if (j >= text.length()) - break; // end of text - ch = text.at(j); - } - - if ( d->keywords.contains( buffer ) != 0 ) { - if ( buffer == QLatin1String("def")) - endStateOfLastPara = DefineName; - else if ( buffer == QLatin1String("class")) - endStateOfLastPara = ClassName; - - QTextCharFormat keywordFormat; - keywordFormat.setForeground(this->colorByType(SyntaxHighlighter::Keyword)); - keywordFormat.setFontWeight(QFont::Bold); - setFormat( i, buffer.length(), keywordFormat); - } - else { - setFormat( i, buffer.length(),this->colorByType(SyntaxHighlighter::Text)); - } - - // increment i - if ( !buffer.isEmpty() ) - i = j-1; - } - // this is the beginning of a number - else if ( ch.isDigit() ) - { - setFormat(i, 1, this->colorByType(SyntaxHighlighter::Number)); - endStateOfLastPara=Digit; - } - // probably an operator - else if ( ch.isSymbol() || ch.isPunct() ) - { - setFormat( i, 1, this->colorByType(SyntaxHighlighter::Operator)); - } - } - } - } break; - case Comment: - { - setFormat( i, 1, this->colorByType(SyntaxHighlighter::Comment)); - } break; - case Literal1: - { - setFormat( i, 1, this->colorByType(SyntaxHighlighter::String)); - if ( ch == QLatin1Char('"') ) - endStateOfLastPara = Standard; - } break; - case Literal2: - { - setFormat( i, 1, this->colorByType(SyntaxHighlighter::String)); - if ( ch == QLatin1Char('\'') ) - endStateOfLastPara = Standard; - } break; - case Blockcomment1: - { - setFormat( i, 1, this->colorByType(SyntaxHighlighter::BlockComment)); - if ( i>=2 && ch == QLatin1Char('"') && - text.at(i-1) == QLatin1Char('"') && - text.at(i-2) == QLatin1Char('"')) - endStateOfLastPara = Standard; - } break; - case Blockcomment2: - { - setFormat( i, 1, this->colorByType(SyntaxHighlighter::BlockComment)); - if ( i>=2 && ch == QLatin1Char('\'') && - text.at(i-1) == QLatin1Char('\'') && - text.at(i-2) == QLatin1Char('\'')) - endStateOfLastPara = Standard; - } break; - case DefineName: - { - if ( ch.isLetterOrNumber() || ch == QLatin1Char(' ') || ch == QLatin1Char('_') ) - { - setFormat( i, 1, this->colorByType(SyntaxHighlighter::Defname)); - } - else - { - if ( ch.isSymbol() || ch.isPunct() ) - setFormat(i, 1, this->colorByType(SyntaxHighlighter::Operator)); - endStateOfLastPara = Standard; - } - } break; - case ClassName: - { - if ( ch.isLetterOrNumber() || ch == QLatin1Char(' ') || ch == QLatin1Char('_') ) - { - setFormat( i, 1, this->colorByType(SyntaxHighlighter::Classname)); - } - else - { - if (ch.isSymbol() || ch.isPunct() ) - setFormat( i, 1, this->colorByType(SyntaxHighlighter::Operator)); - endStateOfLastPara = Standard; - } - } break; - case Digit: - { - if (ch.isDigit() || ch == QLatin1Char('.')) - { - setFormat( i, 1, this->colorByType(SyntaxHighlighter::Number)); - } - else - { - if ( ch.isSymbol() || ch.isPunct() ) - setFormat( i, 1, this->colorByType(SyntaxHighlighter::Operator)); - endStateOfLastPara = Standard; - } - }break; - } - - prev = ch; - i++; - } - - // only block comments can have several lines - if ( endStateOfLastPara != Blockcomment1 && endStateOfLastPara != Blockcomment2 ) - { - endStateOfLastPara = Standard ; - } - - setCurrentBlockState(endStateOfLastPara); -} - -#include "moc_PythonEditor.cpp" +/*************************************************************************** + * Copyright (c) 2004 Werner Mayer * + * * + * 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" +#ifndef _PreComp_ +#include +#include +#include +#include +#include +#endif + +#include + +#include "PythonEditor.h" +#include "Application.h" +#include "BitmapFactory.h" +#include "Macro.h" +#include "PythonDebugger.h" + + +using namespace Gui; + +namespace Gui +{ +struct PythonEditorP +{ + int debugLine; + QRect debugRect; + QPixmap breakpoint; + QPixmap debugMarker; + QString filename; + PythonDebugger* debugger; + PythonEditorP() + : debugLine(-1) + , breakpoint(BitmapFactory().iconFromTheme("breakpoint").pixmap(16, 16)) + , debugMarker(BitmapFactory().iconFromTheme("debug-marker").pixmap(16, 16)) + { + debugger = Application::Instance->macroManager()->debugger(); + } +}; +} // namespace Gui + +/* TRANSLATOR Gui::PythonEditor */ + +/** + * Constructs a PythonEditor which is a child of 'parent' and does the + * syntax highlighting for the Python language. + */ +PythonEditor::PythonEditor(QWidget* parent) + : TextEditor(parent) +{ + d = new PythonEditorP(); + this->setSyntaxHighlighter(new PythonSyntaxHighlighter(this)); + + // set acelerators + auto comment = new QShortcut(this); + comment->setKey(QKeySequence(QString::fromLatin1("ALT+C"))); + + auto uncomment = new QShortcut(this); + uncomment->setKey(QKeySequence(QString::fromLatin1("ALT+U"))); + + connect(comment, &QShortcut::activated, this, &PythonEditor::onComment); + connect(uncomment, &QShortcut::activated, this, &PythonEditor::onUncomment); +} + +/** Destroys the object and frees any allocated resources */ +PythonEditor::~PythonEditor() +{ + delete d; +} + +void PythonEditor::setFileName(const QString& fn) +{ + d->filename = fn; +} + +void PythonEditor::startDebug() +{ + if (d->debugger->start()) { + d->debugger->runFile(d->filename); + d->debugger->stop(); + } +} + +void PythonEditor::toggleBreakpoint() +{ + QTextCursor cursor = textCursor(); + int line = cursor.blockNumber() + 1; + d->debugger->toggleBreakpoint(line, d->filename); + getMarker()->update(); +} + +void PythonEditor::showDebugMarker(int line) +{ + d->debugLine = line; + getMarker()->update(); + QTextCursor cursor = textCursor(); + cursor.movePosition(QTextCursor::StartOfBlock); + int cur = cursor.blockNumber() + 1; + if (cur > line) { + for (int i = line; i < cur; i++) { + cursor.movePosition(QTextCursor::Up); + } + } + else if (cur < line) { + for (int i = cur; i < line; i++) { + cursor.movePosition(QTextCursor::Down); + } + } + setTextCursor(cursor); +} + +void PythonEditor::hideDebugMarker() +{ + d->debugLine = -1; + getMarker()->update(); +} + +void PythonEditor::drawMarker(int line, int x, int y, QPainter* p) +{ + Breakpoint bp = d->debugger->getBreakpoint(d->filename); + if (bp.checkLine(line)) { + p->drawPixmap(x, y, d->breakpoint); + } + if (d->debugLine == line) { + p->drawPixmap(x, y + 2, d->debugMarker); + d->debugRect = QRect(x, y + 2, d->debugMarker.width(), d->debugMarker.height()); + } +} + +void PythonEditor::contextMenuEvent(QContextMenuEvent* e) +{ + QMenu* menu = createStandardContextMenu(); + if (!isReadOnly()) { + menu->addSeparator(); + QAction* comment = menu->addAction(tr("Comment"), this, &PythonEditor::onComment); + comment->setShortcut(QKeySequence(QStringLiteral("ALT+C"))); + QAction* uncomment = menu->addAction(tr("Uncomment"), this, &PythonEditor::onUncomment); + uncomment->setShortcut(QKeySequence(QStringLiteral("ALT+U"))); + } + + menu->exec(e->globalPos()); + delete menu; +} + +void PythonEditor::onComment() +{ + QTextCursor cursor = textCursor(); + int selStart = cursor.selectionStart(); + int selEnd = cursor.selectionEnd(); + QTextBlock block; + cursor.beginEditBlock(); + for (block = document()->begin(); block.isValid(); block = block.next()) { + int pos = block.position(); + int off = block.length() - 1; + // at least one char of the block is part of the selection + if (pos >= selStart || pos + off >= selStart) { + if (pos + 1 > selEnd) { + break; // end of selection reached + } + cursor.setPosition(block.position()); + cursor.insertText(QLatin1String("#")); + selEnd++; + } + } + + cursor.endEditBlock(); +} + +void PythonEditor::onUncomment() +{ + QTextCursor cursor = textCursor(); + int selStart = cursor.selectionStart(); + int selEnd = cursor.selectionEnd(); + QTextBlock block; + cursor.beginEditBlock(); + for (block = document()->begin(); block.isValid(); block = block.next()) { + int pos = block.position(); + int off = block.length() - 1; + // at least one char of the block is part of the selection + if (pos >= selStart || pos + off >= selStart) { + if (pos + 1 > selEnd) { + break; // end of selection reached + } + if (block.text().startsWith(QLatin1String("#"))) { + cursor.setPosition(block.position()); + cursor.deleteChar(); + selEnd--; + } + } + } + + cursor.endEditBlock(); +} + +// ------------------------------------------------------------------------ + +namespace Gui +{ +class PythonSyntaxHighlighterP +{ +public: + PythonSyntaxHighlighterP() + { + keywords << QLatin1String("and") << QLatin1String("as") << QLatin1String("assert") + << QLatin1String("break") << QLatin1String("class") << QLatin1String("continue") + << QLatin1String("def") << QLatin1String("del") << QLatin1String("elif") + << QLatin1String("else") << QLatin1String("except") << QLatin1String("exec") + << QLatin1String("False") << QLatin1String("finally") << QLatin1String("for") + << QLatin1String("from") << QLatin1String("global") << QLatin1String("if") + << QLatin1String("import") << QLatin1String("in") << QLatin1String("is") + << QLatin1String("lambda") << QLatin1String("None") << QLatin1String("nonlocal") + << QLatin1String("not") << QLatin1String("or") << QLatin1String("pass") + << QLatin1String("print") << QLatin1String("raise") << QLatin1String("return") + << QLatin1String("True") << QLatin1String("try") << QLatin1String("while") + << QLatin1String("with") << QLatin1String("yield"); + } + + QStringList keywords; +}; +} // namespace Gui + +/** + * Constructs a Python syntax highlighter. + */ +PythonSyntaxHighlighter::PythonSyntaxHighlighter(QObject* parent) + : SyntaxHighlighter(parent) +{ + d = new PythonSyntaxHighlighterP; +} + +/** Destroys this object. */ +PythonSyntaxHighlighter::~PythonSyntaxHighlighter() +{ + delete d; +} + +/** + * Detects all kinds of text to highlight them in the correct color. + */ +void PythonSyntaxHighlighter::highlightBlock(const QString& text) +{ + int i = 0; + QChar prev, ch; + + const int Standard = 0; // Standard text + const int Digit = 1; // Digits + const int Comment = 2; // Comment begins with # + const int Literal1 = 3; // String literal beginning with " + const int Literal2 = 4; // Other string literal beginning with ' + const int Blockcomment1 = 5; // Block comments beginning and ending with """ + const int Blockcomment2 = 6; // Other block comments beginning and ending with ''' + const int ClassName = 7; // Text after the keyword class + const int DefineName = 8; // Text after the keyword def + + int endStateOfLastPara = previousBlockState(); + if (endStateOfLastPara < 0 || endStateOfLastPara > maximumUserState()) { + endStateOfLastPara = Standard; + } + + while (i < text.length()) { + ch = text.at(i); + + switch (endStateOfLastPara) { + case Standard: { + switch (ch.unicode()) { + case '#': { + // begin a comment + setFormat(i, 1, this->colorByType(SyntaxHighlighter::Comment)); + endStateOfLastPara = Comment; + } break; + case '"': { + // Begin either string literal or block comment + if ((i >= 2) && text.at(i - 1) == QLatin1Char('"') + && text.at(i - 2) == QLatin1Char('"')) { + setFormat(i - 2, 3, this->colorByType(SyntaxHighlighter::BlockComment)); + endStateOfLastPara = Blockcomment1; + } + else { + setFormat(i, 1, this->colorByType(SyntaxHighlighter::String)); + endStateOfLastPara = Literal1; + } + } break; + case '\'': { + // Begin either string literal or block comment + if ((i >= 2) && text.at(i - 1) == QLatin1Char('\'') + && text.at(i - 2) == QLatin1Char('\'')) { + setFormat(i - 2, 3, this->colorByType(SyntaxHighlighter::BlockComment)); + endStateOfLastPara = Blockcomment2; + } + else { + setFormat(i, 1, this->colorByType(SyntaxHighlighter::String)); + endStateOfLastPara = Literal2; + } + } break; + case ' ': + case '\t': { + // ignore whitespaces + } break; + case '(': + case ')': + case '[': + case ']': + case '+': + case '-': + case '*': + case '/': + case ':': + case '%': + case '^': + case '~': + case '!': + case '=': + case '<': + case '>': // possibly two characters + { + setFormat(i, 1, this->colorByType(SyntaxHighlighter::Operator)); + endStateOfLastPara = Standard; + } break; + default: { + // Check for normal text + if (ch.isLetter() || ch == QLatin1Char('_')) { + QString buffer; + int j = i; + while (ch.isLetterOrNumber() || ch == QLatin1Char('_')) { + buffer += ch; + ++j; + if (j >= text.length()) { + break; // end of text + } + ch = text.at(j); + } + + if (d->keywords.contains(buffer) != 0) { + if (buffer == QLatin1String("def")) { + endStateOfLastPara = DefineName; + } + else if (buffer == QLatin1String("class")) { + endStateOfLastPara = ClassName; + } + + QTextCharFormat keywordFormat; + keywordFormat.setForeground( + this->colorByType(SyntaxHighlighter::Keyword)); + keywordFormat.setFontWeight(QFont::Bold); + setFormat(i, buffer.length(), keywordFormat); + } + else { + setFormat(i, + buffer.length(), + this->colorByType(SyntaxHighlighter::Text)); + } + + // increment i + if (!buffer.isEmpty()) { + i = j - 1; + } + } + // this is the beginning of a number + else if (ch.isDigit()) { + setFormat(i, 1, this->colorByType(SyntaxHighlighter::Number)); + endStateOfLastPara = Digit; + } + // probably an operator + else if (ch.isSymbol() || ch.isPunct()) { + setFormat(i, 1, this->colorByType(SyntaxHighlighter::Operator)); + } + } + } + } break; + case Comment: { + setFormat(i, 1, this->colorByType(SyntaxHighlighter::Comment)); + } break; + case Literal1: { + setFormat(i, 1, this->colorByType(SyntaxHighlighter::String)); + if (ch == QLatin1Char('"')) { + endStateOfLastPara = Standard; + } + } break; + case Literal2: { + setFormat(i, 1, this->colorByType(SyntaxHighlighter::String)); + if (ch == QLatin1Char('\'')) { + endStateOfLastPara = Standard; + } + } break; + case Blockcomment1: { + setFormat(i, 1, this->colorByType(SyntaxHighlighter::BlockComment)); + if (i >= 2 && ch == QLatin1Char('"') && text.at(i - 1) == QLatin1Char('"') + && text.at(i - 2) == QLatin1Char('"')) { + endStateOfLastPara = Standard; + } + } break; + case Blockcomment2: { + setFormat(i, 1, this->colorByType(SyntaxHighlighter::BlockComment)); + if (i >= 2 && ch == QLatin1Char('\'') && text.at(i - 1) == QLatin1Char('\'') + && text.at(i - 2) == QLatin1Char('\'')) { + endStateOfLastPara = Standard; + } + } break; + case DefineName: { + if (ch.isLetterOrNumber() || ch == QLatin1Char(' ') || ch == QLatin1Char('_')) { + setFormat(i, 1, this->colorByType(SyntaxHighlighter::Defname)); + } + else { + if (ch.isSymbol() || ch.isPunct()) { + setFormat(i, 1, this->colorByType(SyntaxHighlighter::Operator)); + } + endStateOfLastPara = Standard; + } + } break; + case ClassName: { + if (ch.isLetterOrNumber() || ch == QLatin1Char(' ') || ch == QLatin1Char('_')) { + setFormat(i, 1, this->colorByType(SyntaxHighlighter::Classname)); + } + else { + if (ch.isSymbol() || ch.isPunct()) { + setFormat(i, 1, this->colorByType(SyntaxHighlighter::Operator)); + } + endStateOfLastPara = Standard; + } + } break; + case Digit: { + if (ch.isDigit() || ch == QLatin1Char('.')) { + setFormat(i, 1, this->colorByType(SyntaxHighlighter::Number)); + } + else { + if (ch.isSymbol() || ch.isPunct()) { + setFormat(i, 1, this->colorByType(SyntaxHighlighter::Operator)); + } + endStateOfLastPara = Standard; + } + } break; + } + + prev = ch; + i++; + } + + // only block comments can have several lines + if (endStateOfLastPara != Blockcomment1 && endStateOfLastPara != Blockcomment2) { + endStateOfLastPara = Standard; + } + + setCurrentBlockState(endStateOfLastPara); +} + +#include "moc_PythonEditor.cpp" diff --git a/src/Gui/ReportView.cpp b/src/Gui/ReportView.cpp index 27947f2a5fce..2d1020453667 100644 --- a/src/Gui/ReportView.cpp +++ b/src/Gui/ReportView.cpp @@ -1,880 +1,924 @@ -/*************************************************************************** - * Copyright (c) 2004 Werner Mayer * - * * - * 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" -#ifndef _PreComp_ -# include -# include -# include -# include -# include -# include -# include -#endif - -#include -#include - -#include "ReportView.h" -#include "Application.h" -#include "BitmapFactory.h" -#include "DockWindowManager.h" -#include "FileDialog.h" -#include "PythonConsole.h" -#include "PythonConsolePy.h" -#include "Tools.h" - - -using namespace Gui; -using namespace Gui::DockWnd; - -/* TRANSLATOR Gui::DockWnd::ReportView */ - -/** - * Constructs a ReportView which is a child of 'parent', with the - * name 'name' and widget flags set to 'f' - */ -ReportView::ReportView( QWidget* parent ) - : QWidget(parent) -{ - setObjectName(QLatin1String("ReportOutput")); - - resize( 529, 162 ); - auto tabLayout = new QGridLayout( this ); - tabLayout->setSpacing( 0 ); - tabLayout->setContentsMargins( 0, 0, 0, 0 ); - - tabWidget = new QTabWidget( this ); - tabWidget->setObjectName(QString::fromUtf8("tabWidget")); - tabWidget->setTabPosition(QTabWidget::South); - tabWidget->setTabShape(QTabWidget::Rounded); - tabLayout->addWidget( tabWidget, 0, 0 ); - - - // create the output window for 'Report view' - tabOutput = new ReportOutput(); - tabOutput->setWindowTitle(tr("Output")); - tabOutput->setWindowIcon(BitmapFactory().pixmap("MacroEditor")); - int output = tabWidget->addTab(tabOutput, tabOutput->windowTitle()); - tabWidget->setTabIcon(output, tabOutput->windowIcon()); - - // create the python console - tabPython = new PythonConsole(); - tabPython->setWordWrapMode(QTextOption::NoWrap); - tabPython->setWindowTitle(tr("Python console")); - tabPython->setWindowIcon(BitmapFactory().iconFromTheme("applications-python")); - int python = tabWidget->addTab(tabPython, tabPython->windowTitle()); - tabWidget->setTabIcon(python, tabPython->windowIcon()); - tabWidget->setCurrentIndex(0); - - // raise the tab page set in the preferences - ParameterGrp::handle hGrp = WindowParameter::getDefaultParameter()->GetGroup("General"); - int index = hGrp->GetInt("AutoloadTab", 0); - tabWidget->setCurrentIndex(index); -} - -/** - * Destroys the object and frees any allocated resources - */ -ReportView::~ReportView() -{ - // no need to delete child widgets, Qt does it all for us -} - -void ReportView::changeEvent(QEvent *e) -{ - QWidget::changeEvent(e); - if (e->type() == QEvent::LanguageChange) { - tabOutput->setWindowTitle(tr("Output")); - tabPython->setWindowTitle(tr("Python console")); - for (int i=0; icount();i++) - tabWidget->setTabText(i, tabWidget->widget(i)->windowTitle()); - } -} - -// ---------------------------------------------------------- - -namespace Gui { -struct TextBlockData : public QTextBlockUserData -{ - struct State { - int length; - ReportHighlighter::Paragraph type; - }; - QVector block; -}; -} - -ReportHighlighter::ReportHighlighter(QTextEdit* edit) - : QSyntaxHighlighter(edit), type(Message) -{ - QPalette pal = edit->palette(); - txtCol = pal.windowText().color(); - logCol = Qt::blue; - warnCol = QColor(255, 170, 0); - errCol = Qt::red; -} - -ReportHighlighter::~ReportHighlighter() -{ -} - -void ReportHighlighter::highlightBlock (const QString & text) -{ - if (text.isEmpty()) - return; - auto ud = static_cast(this->currentBlockUserData()); - if (!ud) { - ud = new TextBlockData; - this->setCurrentBlockUserData(ud); - } - - TextBlockData::State b; - b.length = text.length(); - b.type = this->type; - ud->block.append(b); - - QVector block = ud->block; - int start = 0; - for (const auto & it : block) { - switch (it.type) - { - case Message: - setFormat(start, it.length-start, txtCol); - break; - case Warning: - setFormat(start, it.length-start, warnCol); - break; - case Error: - setFormat(start, it.length-start, errCol); - break; - case LogText: - setFormat(start, it.length-start, logCol); - break; - case Critical: - setFormat(start, it.length-start, criticalCol); - break; - default: - break; - } - - start = it.length; - } -} - -void ReportHighlighter::setParagraphType(ReportHighlighter::Paragraph t) -{ - type = t; -} - -void ReportHighlighter::setTextColor( const QColor& col ) -{ - txtCol = col; -} - -void ReportHighlighter::setLogColor( const QColor& col ) -{ - logCol = col; -} - -void ReportHighlighter::setWarningColor( const QColor& col ) -{ - warnCol = col; -} - -void ReportHighlighter::setErrorColor( const QColor& col ) -{ - errCol = col; -} - -void ReportHighlighter::setCriticalColor( const QColor& col ) -{ - criticalCol = col; -} - -// ---------------------------------------------------------------------------- - -namespace Gui { -class ReportOutputParameter -{ -public: - static bool showOnLogMessage() - { - return getGroup()->GetBool("checkShowReportViewOnLogMessage", false); - } - static void toggleShowOnLogMessage() - { - bool show = showOnLogMessage(); - getGroup()->SetBool("checkShowReportViewOnLogMessage", !show); - } - static bool showOnMessage() - { - return getGroup()->GetBool("checkShowReportViewOnNormalMessage", false); - } - static void toggleShowOnMessage() - { - bool show = showOnMessage(); - getGroup()->SetBool("checkShowReportViewOnNormalMessage", !show); - } - static bool showOnWarning() - { - return getGroup()->GetBool("checkShowReportViewOnWarning", false); - } - static void toggleShowOnWarning() - { - bool show = showOnWarning(); - getGroup()->SetBool("checkShowReportViewOnWarning", !show); - } - static bool showOnError() - { - return getGroup()->GetBool("checkShowReportViewOnError", true); - } - static void toggleShowOnError() - { - bool show = showOnError(); - getGroup()->SetBool("checkShowReportViewOnError", !show); - } - static bool showOnCritical() - { - return getGroup()->GetBool("checkShowReportViewOnCritical", false); - } - static void toggleShowOnCritical() - { - bool show = showOnMessage(); - getGroup()->SetBool("checkShowReportViewOnCritical", !show); - } - -private: - static ParameterGrp::handle getGroup() - { - return App::GetApplication().GetUserParameter(). - GetGroup("BaseApp")->GetGroup("Preferences")->GetGroup("OutputWindow"); - } -}; -} - -// ---------------------------------------------------------- - -/** - * The CustomReportEvent class is used to send report events in the methods Log(), - * Error(), Warning() and Message() of the ReportOutput class to itself instead of - * printing the messages directly in its text view. - * - * This makes the methods Log(), Error(), Warning() and Message() thread-safe. - * @author Werner Mayer - */ -class CustomReportEvent : public QEvent -{ -public: - CustomReportEvent(ReportHighlighter::Paragraph p, const QString& s) - : QEvent(QEvent::Type(QEvent::User)) - { par = p; msg = s;} - ~CustomReportEvent() override - { } - const QString& message() const - { return msg; } - ReportHighlighter::Paragraph messageType() const - { return par; } -private: - ReportHighlighter::Paragraph par; - QString msg; -}; - -// ---------------------------------------------------------- - -/** - * The ReportOutputObserver class is used to check if messages sent to the - * report view are warnings or errors, and if so and if the user has not - * disabled this in preferences, the report view is toggled on so the - * user always gets the warnings/errors - */ - -ReportOutputObserver::ReportOutputObserver(ReportOutput *report) - : QObject(report) -{ - this->reportView = report; -} - -void ReportOutputObserver::showReportView() -{ - // get the QDockWidget parent of the report view - DockWindowManager::instance()->activate(reportView); -} - -bool ReportOutputObserver::eventFilter(QObject *obj, QEvent *event) -{ - if (event->type() == QEvent::User && obj == reportView.data()) { - auto cr = dynamic_cast(event); - if (cr) { - ReportHighlighter::Paragraph msgType = cr->messageType(); - if (msgType == ReportHighlighter::Warning) { - if (ReportOutputParameter::showOnWarning()) { - showReportView(); - } - } - else if (msgType == ReportHighlighter::Error) { - if (ReportOutputParameter::showOnError()) { - showReportView(); - } - } - else if (msgType == ReportHighlighter::Message) { - if (ReportOutputParameter::showOnMessage()) { - showReportView(); - } - } - else if (msgType == ReportHighlighter::LogText) { - if (ReportOutputParameter::showOnLogMessage()) { - showReportView(); - } - } - else if (msgType == ReportHighlighter::Critical) { - if (ReportOutputParameter::showOnCritical()) { - showReportView(); - } - } - } - return false; //true would prevent the messages reaching the report view - } - - // standard event processing - return QObject::eventFilter(obj, event); -} - -// ---------------------------------------------------------- - -class ReportOutput::Data -{ -public: - Data() - { - if (!default_stdout) { - Base::PyGILStateLocker lock; - default_stdout = PySys_GetObject("stdout"); - replace_stdout = new OutputStdout(); - redirected_stdout = false; - } - - if (!default_stderr) { - Base::PyGILStateLocker lock; - default_stderr = PySys_GetObject("stderr"); - replace_stderr = new OutputStderr(); - redirected_stderr = false; - } - } - ~Data() - { - if (replace_stdout) { - Py_DECREF(replace_stdout); - replace_stdout = nullptr; - } - - if (replace_stderr) { - Py_DECREF(replace_stderr); - replace_stderr = nullptr; - } - } - - // make them static because redirection should done only once - static bool redirected_stdout; - static PyObject* default_stdout; - static PyObject* replace_stdout; - - static bool redirected_stderr; - static PyObject* default_stderr; - static PyObject* replace_stderr; -#ifdef FC_DEBUG - long logMessageSize = 0; -#else - long logMessageSize = 2048; -#endif -}; - -bool ReportOutput::Data::redirected_stdout = false; -PyObject* ReportOutput::Data::default_stdout = nullptr; -PyObject* ReportOutput::Data::replace_stdout = nullptr; - -bool ReportOutput::Data::redirected_stderr = false; -PyObject* ReportOutput::Data::default_stderr = nullptr; -PyObject* ReportOutput::Data::replace_stderr = nullptr; - -/* TRANSLATOR Gui::DockWnd::ReportOutput */ - -/** - * Constructs a ReportOutput which is a child of 'parent', with the - * name 'name' and widget flags set to 'f' - */ -ReportOutput::ReportOutput(QWidget* parent) - : QTextEdit(parent) - , WindowParameter("OutputWindow") - , d(new Data) - , gotoEnd(false) - , blockStart(true) -{ - bLog = false; - reportHl = new ReportHighlighter(this); - - restoreFont(); - setReadOnly(true); - clear(); - setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - Base::Console().AttachObserver(this); - getWindowParameter()->Attach(this); - getWindowParameter()->NotifyAll(); - // do this explicitly because the keys below might not yet be part of a group - getWindowParameter()->Notify("RedirectPythonOutput"); - getWindowParameter()->Notify("RedirectPythonErrors"); - - _prefs = WindowParameter::getDefaultParameter()->GetGroup("Editor"); - _prefs->Attach(this); - _prefs->Notify("FontSize"); - - messageSize = _prefs->GetInt("LogMessageSize", d->logMessageSize); - - // scroll to bottom at startup to make sure that last appended text is visible - ensureCursorVisible(); -} - -/** - * Destroys the object and frees any allocated resources - */ -ReportOutput::~ReportOutput() -{ - getWindowParameter()->Detach(this); - _prefs->Detach(this); - Base::Console().DetachObserver(this); - delete reportHl; - delete d; -} - -void ReportOutput::restoreFont() -{ - QFont serifFont(QLatin1String("Courier"), 10, QFont::Normal); - setFont(serifFont); -} - -void ReportOutput::SendLog(const std::string& notifiername, const std::string& msg, Base::LogStyle level, - Base::IntendedRecipient recipient, Base::ContentType content) -{ - (void) notifiername; - - // Do not log translated messages, or messages intended only to the user to the Report View - if( recipient == Base::IntendedRecipient::User || - content == Base::ContentType::Translated) - return; - - ReportHighlighter::Paragraph style = ReportHighlighter::LogText; - switch (level) { - case Base::LogStyle::Warning: - style = ReportHighlighter::Warning; - break; - case Base::LogStyle::Message: - style = ReportHighlighter::Message; - break; - case Base::LogStyle::Error: - style = ReportHighlighter::Error; - break; - case Base::LogStyle::Log: - style = ReportHighlighter::LogText; - break; - case Base::LogStyle::Critical: - style = ReportHighlighter::Critical; - break; - default: - break; - } - - QString qMsg = QString::fromUtf8(msg.c_str()); - - // This truncates log messages that are too long - if (style == ReportHighlighter::LogText) { - if (messageSize > 0 && qMsg.size()>messageSize) { - qMsg.truncate(messageSize); - qMsg += QString::fromLatin1("...\n"); - } - } - - // Send the event to itself to allow thread-safety. Qt will delete it when done. - auto ev = new CustomReportEvent(style, qMsg); - QApplication::postEvent(this, ev); -} - -void ReportOutput::customEvent ( QEvent* ev ) -{ - // Appends the text stored in the event to the text view - if ( ev->type() == QEvent::User ) { - auto ce = static_cast(ev); - reportHl->setParagraphType(ce->messageType()); - - bool showTimecode = getWindowParameter()->GetBool("checkShowReportTimecode", true); - QString text = ce->message(); - - // The time code can only be set when the cursor is at the block start - if (showTimecode && blockStart) { - QTime time = QTime::currentTime(); - text.prepend(time.toString(QLatin1String("hh:mm:ss "))); - } - - QTextCursor cursor(this->document()); - cursor.beginEditBlock(); - cursor.movePosition(QTextCursor::End); - cursor.insertText(text); - cursor.endEditBlock(); - - blockStart = cursor.atBlockStart(); - if (gotoEnd) { - setTextCursor(cursor); - } - ensureCursorVisible(); - } -} - - -bool ReportOutput::event(QEvent* event) -{ - if (event && event->type() == QEvent::ShortcutOverride) { - auto kevent = static_cast(event); - if (kevent == QKeySequence::Copy) - kevent->accept(); - } - return QTextEdit::event(event); -} - -void ReportOutput::changeEvent(QEvent *ev) -{ - if (ev->type() == QEvent::StyleChange) { - QPalette pal = qApp->palette(); - QColor color = pal.windowText().color(); - unsigned int text = App::Color::asPackedRGB(color); - auto value = static_cast(text); - // if this parameter is not already set use the style's window text color - value = getWindowParameter()->GetUnsigned("colorText", value); - getWindowParameter()->SetUnsigned("colorText", value); - } - QTextEdit::changeEvent(ev); -} - -void ReportOutput::contextMenuEvent ( QContextMenuEvent * e ) -{ - bool bShowOnLog = ReportOutputParameter::showOnLogMessage(); - bool bShowOnNormal = ReportOutputParameter::showOnMessage(); - bool bShowOnWarn = ReportOutputParameter::showOnWarning(); - bool bShowOnError = ReportOutputParameter::showOnError(); - bool bShowOnCritical = ReportOutputParameter::showOnCritical(); - - auto menu = new QMenu(this); - auto optionMenu = new QMenu( menu ); - optionMenu->setTitle(tr("Options")); - menu->addMenu(optionMenu); - menu->addSeparator(); - - auto displayMenu = new QMenu(optionMenu); - displayMenu->setTitle(tr("Display message types")); - optionMenu->addMenu(displayMenu); - - QAction* logMsg = displayMenu->addAction(tr("Normal messages"), this, &ReportOutput::onToggleNormalMessage); - logMsg->setCheckable(true); - logMsg->setChecked(bMsg); - - QAction* logAct = displayMenu->addAction(tr("Log messages"), this, &ReportOutput::onToggleLogMessage); - logAct->setCheckable(true); - logAct->setChecked(bLog); - - QAction* wrnAct = displayMenu->addAction(tr("Warnings"), this, &ReportOutput::onToggleWarning); - wrnAct->setCheckable(true); - wrnAct->setChecked(bWrn); - - QAction* errAct = displayMenu->addAction(tr("Errors"), this, &ReportOutput::onToggleError); - errAct->setCheckable(true); - errAct->setChecked(bErr); - - QAction* logCritical = displayMenu->addAction(tr("Critical messages"), this, &ReportOutput::onToggleCritical); - logCritical->setCheckable(true); - logCritical->setChecked(bCritical); - - auto showOnMenu = new QMenu (optionMenu); - showOnMenu->setTitle(tr("Show Report view on")); - optionMenu->addMenu(showOnMenu); - - QAction* showNormAct = showOnMenu->addAction(tr("Normal messages"), this, &ReportOutput::onToggleShowReportViewOnNormalMessage); - showNormAct->setCheckable(true); - showNormAct->setChecked(bShowOnNormal); - - QAction* showLogAct = showOnMenu->addAction(tr("Log messages"), this, &ReportOutput::onToggleShowReportViewOnLogMessage); - showLogAct->setCheckable(true); - showLogAct->setChecked(bShowOnLog); - - QAction* showWrnAct = showOnMenu->addAction(tr("Warnings"), this, &ReportOutput::onToggleShowReportViewOnWarning); - showWrnAct->setCheckable(true); - showWrnAct->setChecked(bShowOnWarn); - - QAction* showErrAct = showOnMenu->addAction(tr("Errors"), this, &ReportOutput::onToggleShowReportViewOnError); - showErrAct->setCheckable(true); - showErrAct->setChecked(bShowOnError); - - QAction* showCriticalAct = showOnMenu->addAction(tr("Critical messages"), this, SLOT(onToggleShowReportViewOnCritical())); - showCriticalAct->setCheckable(true); - showCriticalAct->setChecked(bShowOnCritical); - - optionMenu->addSeparator(); - - QAction* stdoutAct = optionMenu->addAction(tr("Redirect Python output"), this, &ReportOutput::onToggleRedirectPythonStdout); - stdoutAct->setCheckable(true); - stdoutAct->setChecked(d->redirected_stdout); - - QAction* stderrAct = optionMenu->addAction(tr("Redirect Python errors"), this, &ReportOutput::onToggleRedirectPythonStderr); - stderrAct->setCheckable(true); - stderrAct->setChecked(d->redirected_stderr); - - optionMenu->addSeparator(); - QAction* botAct = optionMenu->addAction(tr("Go to end"), this, &ReportOutput::onToggleGoToEnd); - botAct->setCheckable(true); - botAct->setChecked(gotoEnd); - - // Use Qt's internal translation of the Copy & Select All commands - const char* context = "QWidgetTextControl"; - QString copyStr = QCoreApplication::translate(context, "&Copy"); - QAction* copy = menu->addAction(copyStr, this, &ReportOutput::copy, QKeySequence(QKeySequence::Copy)); - copy->setEnabled(textCursor().hasSelection()); - QIcon icon = QIcon::fromTheme(QString::fromLatin1("edit-copy")); - if (!icon.isNull()) - copy->setIcon(icon); - - menu->addSeparator(); - QString selectStr = QCoreApplication::translate(context, "Select All"); - menu->addAction(selectStr, this, &ReportOutput::selectAll, QKeySequence(QKeySequence::SelectAll)); - - menu->addAction(tr("Clear"), this, &ReportOutput::clear); - menu->addSeparator(); - menu->addAction(tr("Save As..."), this, &ReportOutput::onSaveAs); - - menu->exec(e->globalPos()); - delete menu; -} - -void ReportOutput::onSaveAs() -{ - QString fn = QFileDialog::getSaveFileName(this, tr("Save Report Output"), QString(), - QString::fromLatin1("%1 (*.txt *.log)").arg(tr("Plain Text Files"))); - if (!fn.isEmpty()) { - QFileInfo fi(fn); - if (fi.completeSuffix().isEmpty()) - fn += QLatin1String(".log"); - QFile f(fn); - if (f.open(QIODevice::WriteOnly)) { - QTextStream t (&f); - t << toPlainText(); - f.close(); - } - } -} - -bool ReportOutput::isError() const -{ - return bErr; -} - -bool ReportOutput::isWarning() const -{ - return bWrn; -} - -bool ReportOutput::isLogMessage() const -{ - return bLog; -} - -bool ReportOutput::isNormalMessage() const -{ - return bMsg; -} - - -bool ReportOutput::isCritical() const -{ - return bCritical; -} - -void ReportOutput::onToggleError() -{ - bErr = bErr ? false : true; - getWindowParameter()->SetBool( "checkError", bErr ); -} - -void ReportOutput::onToggleWarning() -{ - bWrn = bWrn ? false : true; - getWindowParameter()->SetBool( "checkWarning", bWrn ); -} - -void ReportOutput::onToggleLogMessage() -{ - bLog = bLog ? false : true; - getWindowParameter()->SetBool( "checkLogging", bLog ); -} - -void ReportOutput::onToggleNormalMessage() -{ - bMsg = bMsg ? false : true; - getWindowParameter()->SetBool( "checkMessage", bMsg ); -} - -void ReportOutput::onToggleCritical() -{ - bCritical = bCritical ? false : true; - getWindowParameter()->SetBool( "checkCritical", bCritical ); -} - -void ReportOutput::onToggleShowReportViewOnWarning() -{ - ReportOutputParameter::toggleShowOnWarning(); -} - -void ReportOutput::onToggleShowReportViewOnError() -{ - ReportOutputParameter::toggleShowOnError(); -} - -void ReportOutput::onToggleShowReportViewOnNormalMessage() -{ - ReportOutputParameter::toggleShowOnMessage(); -} - -void ReportOutput::onToggleShowReportViewOnCritical() -{ - ReportOutputParameter::toggleShowOnCritical(); -} - -void ReportOutput::onToggleShowReportViewOnLogMessage() -{ - ReportOutputParameter::toggleShowOnLogMessage(); -} - -void ReportOutput::onToggleRedirectPythonStdout() -{ - if (d->redirected_stdout) { - d->redirected_stdout = false; - Base::PyGILStateLocker lock; - PySys_SetObject("stdout", d->default_stdout); - } - else { - d->redirected_stdout = true; - Base::PyGILStateLocker lock; - PySys_SetObject("stdout", d->replace_stdout); - } - - getWindowParameter()->SetBool("RedirectPythonOutput", d->redirected_stdout); -} - -void ReportOutput::onToggleRedirectPythonStderr() -{ - if (d->redirected_stderr) { - d->redirected_stderr = false; - Base::PyGILStateLocker lock; - PySys_SetObject("stderr", d->default_stderr); - } - else { - d->redirected_stderr = true; - Base::PyGILStateLocker lock; - PySys_SetObject("stderr", d->replace_stderr); - } - - getWindowParameter()->SetBool("RedirectPythonErrors", d->redirected_stderr); -} - -void ReportOutput::onToggleGoToEnd() -{ - gotoEnd = gotoEnd ? false : true; - getWindowParameter()->SetBool( "checkGoToEnd", gotoEnd ); -} - -void ReportOutput::OnChange(Base::Subject &rCaller, const char * sReason) -{ - ParameterGrp& rclGrp = ((ParameterGrp&)rCaller); - if (strcmp(sReason, "checkLogging") == 0) { - bLog = rclGrp.GetBool( sReason, bLog ); - } - else if (strcmp(sReason, "checkWarning") == 0) { - bWrn = rclGrp.GetBool( sReason, bWrn ); - } - else if (strcmp(sReason, "checkError") == 0) { - bErr = rclGrp.GetBool( sReason, bErr ); - } - else if (strcmp(sReason, "checkMessage") == 0) { - bMsg = rclGrp.GetBool( sReason, bMsg ); - } - else if (strcmp(sReason, "checkCritical") == 0) { - bMsg = rclGrp.GetBool( sReason, bMsg ); - } - else if (strcmp(sReason, "colorText") == 0) { - unsigned long col = rclGrp.GetUnsigned( sReason ); - reportHl->setTextColor(App::Color::fromPackedRGB(col)); - } - else if (strcmp(sReason, "colorCriticalText") == 0) { - unsigned long col = rclGrp.GetUnsigned( sReason ); - reportHl->setTextColor( QColor( (col >> 24) & 0xff,(col >> 16) & 0xff,(col >> 8) & 0xff) ); - } - else if (strcmp(sReason, "colorLogging") == 0) { - unsigned long col = rclGrp.GetUnsigned( sReason ); - reportHl->setLogColor(App::Color::fromPackedRGB(col)); - } - else if (strcmp(sReason, "colorWarning") == 0) { - unsigned long col = rclGrp.GetUnsigned( sReason ); - reportHl->setWarningColor(App::Color::fromPackedRGB(col)); - } - else if (strcmp(sReason, "colorError") == 0) { - unsigned long col = rclGrp.GetUnsigned( sReason ); - reportHl->setErrorColor(App::Color::fromPackedRGB(col)); - } - else if (strcmp(sReason, "checkGoToEnd") == 0) { - gotoEnd = rclGrp.GetBool(sReason, gotoEnd); - } - else if (strcmp(sReason, "FontSize") == 0 || strcmp(sReason, "Font") == 0) { - int fontSize = rclGrp.GetInt("FontSize", 10); - QString fontFamily = QString::fromLatin1(rclGrp.GetASCII("Font", "Courier").c_str()); - - QFont font(fontFamily, fontSize); - setFont(font); - QFontMetrics metric(font); - int width = QtTools::horizontalAdvance(metric, QLatin1String("0000")); -#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) - setTabStopWidth(width); -#else - setTabStopDistance(width); -#endif - } - else if (strcmp(sReason, "RedirectPythonOutput") == 0) { - bool checked = rclGrp.GetBool(sReason, true); - if (checked != d->redirected_stdout) - onToggleRedirectPythonStdout(); - } - else if (strcmp(sReason, "RedirectPythonErrors") == 0) { - bool checked = rclGrp.GetBool(sReason, true); - if (checked != d->redirected_stderr) - onToggleRedirectPythonStderr(); - } - else if (strcmp(sReason, "LogMessageSize") == 0) { - messageSize = rclGrp.GetInt(sReason, d->logMessageSize); - } -} - -#include "moc_ReportView.cpp" +/*************************************************************************** + * Copyright (c) 2004 Werner Mayer * + * * + * 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" +#ifndef _PreComp_ +#include +#include +#include +#include +#include +#include +#include +#endif + +#include +#include + +#include "ReportView.h" +#include "Application.h" +#include "BitmapFactory.h" +#include "DockWindowManager.h" +#include "FileDialog.h" +#include "PythonConsole.h" +#include "PythonConsolePy.h" +#include "Tools.h" + + +using namespace Gui; +using namespace Gui::DockWnd; + +/* TRANSLATOR Gui::DockWnd::ReportView */ + +/** + * Constructs a ReportView which is a child of 'parent', with the + * name 'name' and widget flags set to 'f' + */ +ReportView::ReportView(QWidget* parent) + : QWidget(parent) +{ + setObjectName(QLatin1String("ReportOutput")); + + resize(529, 162); + auto tabLayout = new QGridLayout(this); + tabLayout->setSpacing(0); + tabLayout->setContentsMargins(0, 0, 0, 0); + + tabWidget = new QTabWidget(this); + tabWidget->setObjectName(QString::fromUtf8("tabWidget")); + tabWidget->setTabPosition(QTabWidget::South); + tabWidget->setTabShape(QTabWidget::Rounded); + tabLayout->addWidget(tabWidget, 0, 0); + + + // create the output window for 'Report view' + tabOutput = new ReportOutput(); + tabOutput->setWindowTitle(tr("Output")); + tabOutput->setWindowIcon(BitmapFactory().pixmap("MacroEditor")); + int output = tabWidget->addTab(tabOutput, tabOutput->windowTitle()); + tabWidget->setTabIcon(output, tabOutput->windowIcon()); + + // create the python console + tabPython = new PythonConsole(); + tabPython->setWordWrapMode(QTextOption::NoWrap); + tabPython->setWindowTitle(tr("Python console")); + tabPython->setWindowIcon(BitmapFactory().iconFromTheme("applications-python")); + int python = tabWidget->addTab(tabPython, tabPython->windowTitle()); + tabWidget->setTabIcon(python, tabPython->windowIcon()); + tabWidget->setCurrentIndex(0); + + // raise the tab page set in the preferences + ParameterGrp::handle hGrp = WindowParameter::getDefaultParameter()->GetGroup("General"); + int index = hGrp->GetInt("AutoloadTab", 0); + tabWidget->setCurrentIndex(index); +} + +/** + * Destroys the object and frees any allocated resources + */ +ReportView::~ReportView() +{ + // no need to delete child widgets, Qt does it all for us +} + +void ReportView::changeEvent(QEvent* e) +{ + QWidget::changeEvent(e); + if (e->type() == QEvent::LanguageChange) { + tabOutput->setWindowTitle(tr("Output")); + tabPython->setWindowTitle(tr("Python console")); + for (int i = 0; i < tabWidget->count(); i++) { + tabWidget->setTabText(i, tabWidget->widget(i)->windowTitle()); + } + } +} + +// ---------------------------------------------------------- + +namespace Gui +{ +struct TextBlockData: public QTextBlockUserData +{ + struct State + { + int length; + ReportHighlighter::Paragraph type; + }; + QVector block; +}; +} // namespace Gui + +ReportHighlighter::ReportHighlighter(QTextEdit* edit) + : QSyntaxHighlighter(edit) + , type(Message) +{ + QPalette pal = edit->palette(); + txtCol = pal.windowText().color(); + logCol = Qt::blue; + warnCol = QColor(255, 170, 0); + errCol = Qt::red; +} + +ReportHighlighter::~ReportHighlighter() +{} + +void ReportHighlighter::highlightBlock(const QString& text) +{ + if (text.isEmpty()) { + return; + } + auto ud = static_cast(this->currentBlockUserData()); + if (!ud) { + ud = new TextBlockData; + this->setCurrentBlockUserData(ud); + } + + TextBlockData::State b; + b.length = text.length(); + b.type = this->type; + ud->block.append(b); + + QVector block = ud->block; + int start = 0; + for (const auto& it : block) { + switch (it.type) { + case Message: + setFormat(start, it.length - start, txtCol); + break; + case Warning: + setFormat(start, it.length - start, warnCol); + break; + case Error: + setFormat(start, it.length - start, errCol); + break; + case LogText: + setFormat(start, it.length - start, logCol); + break; + case Critical: + setFormat(start, it.length - start, criticalCol); + break; + default: + break; + } + + start = it.length; + } +} + +void ReportHighlighter::setParagraphType(ReportHighlighter::Paragraph t) +{ + type = t; +} + +void ReportHighlighter::setTextColor(const QColor& col) +{ + txtCol = col; +} + +void ReportHighlighter::setLogColor(const QColor& col) +{ + logCol = col; +} + +void ReportHighlighter::setWarningColor(const QColor& col) +{ + warnCol = col; +} + +void ReportHighlighter::setErrorColor(const QColor& col) +{ + errCol = col; +} + +void ReportHighlighter::setCriticalColor(const QColor& col) +{ + criticalCol = col; +} + +// ---------------------------------------------------------------------------- + +namespace Gui +{ +class ReportOutputParameter +{ +public: + static bool showOnLogMessage() + { + return getGroup()->GetBool("checkShowReportViewOnLogMessage", false); + } + static void toggleShowOnLogMessage() + { + bool show = showOnLogMessage(); + getGroup()->SetBool("checkShowReportViewOnLogMessage", !show); + } + static bool showOnMessage() + { + return getGroup()->GetBool("checkShowReportViewOnNormalMessage", false); + } + static void toggleShowOnMessage() + { + bool show = showOnMessage(); + getGroup()->SetBool("checkShowReportViewOnNormalMessage", !show); + } + static bool showOnWarning() + { + return getGroup()->GetBool("checkShowReportViewOnWarning", false); + } + static void toggleShowOnWarning() + { + bool show = showOnWarning(); + getGroup()->SetBool("checkShowReportViewOnWarning", !show); + } + static bool showOnError() + { + return getGroup()->GetBool("checkShowReportViewOnError", true); + } + static void toggleShowOnError() + { + bool show = showOnError(); + getGroup()->SetBool("checkShowReportViewOnError", !show); + } + static bool showOnCritical() + { + return getGroup()->GetBool("checkShowReportViewOnCritical", false); + } + static void toggleShowOnCritical() + { + bool show = showOnMessage(); + getGroup()->SetBool("checkShowReportViewOnCritical", !show); + } + +private: + static ParameterGrp::handle getGroup() + { + return App::GetApplication() + .GetUserParameter() + .GetGroup("BaseApp") + ->GetGroup("Preferences") + ->GetGroup("OutputWindow"); + } +}; +} // namespace Gui + +// ---------------------------------------------------------- + +/** + * The CustomReportEvent class is used to send report events in the methods Log(), + * Error(), Warning() and Message() of the ReportOutput class to itself instead of + * printing the messages directly in its text view. + * + * This makes the methods Log(), Error(), Warning() and Message() thread-safe. + * @author Werner Mayer + */ +class CustomReportEvent: public QEvent +{ +public: + CustomReportEvent(ReportHighlighter::Paragraph p, const QString& s) + : QEvent(QEvent::Type(QEvent::User)) + { + par = p; + msg = s; + } + ~CustomReportEvent() override + {} + const QString& message() const + { + return msg; + } + ReportHighlighter::Paragraph messageType() const + { + return par; + } + +private: + ReportHighlighter::Paragraph par; + QString msg; +}; + +// ---------------------------------------------------------- + +/** + * The ReportOutputObserver class is used to check if messages sent to the + * report view are warnings or errors, and if so and if the user has not + * disabled this in preferences, the report view is toggled on so the + * user always gets the warnings/errors + */ + +ReportOutputObserver::ReportOutputObserver(ReportOutput* report) + : QObject(report) +{ + this->reportView = report; +} + +void ReportOutputObserver::showReportView() +{ + // get the QDockWidget parent of the report view + DockWindowManager::instance()->activate(reportView); +} + +bool ReportOutputObserver::eventFilter(QObject* obj, QEvent* event) +{ + if (event->type() == QEvent::User && obj == reportView.data()) { + auto cr = dynamic_cast(event); + if (cr) { + ReportHighlighter::Paragraph msgType = cr->messageType(); + if (msgType == ReportHighlighter::Warning) { + if (ReportOutputParameter::showOnWarning()) { + showReportView(); + } + } + else if (msgType == ReportHighlighter::Error) { + if (ReportOutputParameter::showOnError()) { + showReportView(); + } + } + else if (msgType == ReportHighlighter::Message) { + if (ReportOutputParameter::showOnMessage()) { + showReportView(); + } + } + else if (msgType == ReportHighlighter::LogText) { + if (ReportOutputParameter::showOnLogMessage()) { + showReportView(); + } + } + else if (msgType == ReportHighlighter::Critical) { + if (ReportOutputParameter::showOnCritical()) { + showReportView(); + } + } + } + return false; // true would prevent the messages reaching the report view + } + + // standard event processing + return QObject::eventFilter(obj, event); +} + +// ---------------------------------------------------------- + +class ReportOutput::Data +{ +public: + Data() + { + if (!default_stdout) { + Base::PyGILStateLocker lock; + default_stdout = PySys_GetObject("stdout"); + replace_stdout = new OutputStdout(); + redirected_stdout = false; + } + + if (!default_stderr) { + Base::PyGILStateLocker lock; + default_stderr = PySys_GetObject("stderr"); + replace_stderr = new OutputStderr(); + redirected_stderr = false; + } + } + ~Data() + { + if (replace_stdout) { + Py_DECREF(replace_stdout); + replace_stdout = nullptr; + } + + if (replace_stderr) { + Py_DECREF(replace_stderr); + replace_stderr = nullptr; + } + } + + // make them static because redirection should done only once + static bool redirected_stdout; + static PyObject* default_stdout; + static PyObject* replace_stdout; + + static bool redirected_stderr; + static PyObject* default_stderr; + static PyObject* replace_stderr; +#ifdef FC_DEBUG + long logMessageSize = 0; +#else + long logMessageSize = 2048; +#endif +}; + +bool ReportOutput::Data::redirected_stdout = false; +PyObject* ReportOutput::Data::default_stdout = nullptr; +PyObject* ReportOutput::Data::replace_stdout = nullptr; + +bool ReportOutput::Data::redirected_stderr = false; +PyObject* ReportOutput::Data::default_stderr = nullptr; +PyObject* ReportOutput::Data::replace_stderr = nullptr; + +/* TRANSLATOR Gui::DockWnd::ReportOutput */ + +/** + * Constructs a ReportOutput which is a child of 'parent', with the + * name 'name' and widget flags set to 'f' + */ +ReportOutput::ReportOutput(QWidget* parent) + : QTextEdit(parent) + , WindowParameter("OutputWindow") + , d(new Data) + , gotoEnd(false) + , blockStart(true) +{ + bLog = false; + reportHl = new ReportHighlighter(this); + + restoreFont(); + setReadOnly(true); + clear(); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + Base::Console().AttachObserver(this); + getWindowParameter()->Attach(this); + getWindowParameter()->NotifyAll(); + // do this explicitly because the keys below might not yet be part of a group + getWindowParameter()->Notify("RedirectPythonOutput"); + getWindowParameter()->Notify("RedirectPythonErrors"); + + _prefs = WindowParameter::getDefaultParameter()->GetGroup("Editor"); + _prefs->Attach(this); + _prefs->Notify("FontSize"); + + messageSize = _prefs->GetInt("LogMessageSize", d->logMessageSize); + + // scroll to bottom at startup to make sure that last appended text is visible + ensureCursorVisible(); +} + +/** + * Destroys the object and frees any allocated resources + */ +ReportOutput::~ReportOutput() +{ + getWindowParameter()->Detach(this); + _prefs->Detach(this); + Base::Console().DetachObserver(this); + delete reportHl; + delete d; +} + +void ReportOutput::restoreFont() +{ + QFont serifFont(QLatin1String("Courier"), 10, QFont::Normal); + setFont(serifFont); +} + +void ReportOutput::SendLog(const std::string& notifiername, + const std::string& msg, + Base::LogStyle level, + Base::IntendedRecipient recipient, + Base::ContentType content) +{ + (void)notifiername; + + // Do not log translated messages, or messages intended only to the user to the Report View + if (recipient == Base::IntendedRecipient::User || content == Base::ContentType::Translated) { + return; + } + + ReportHighlighter::Paragraph style = ReportHighlighter::LogText; + switch (level) { + case Base::LogStyle::Warning: + style = ReportHighlighter::Warning; + break; + case Base::LogStyle::Message: + style = ReportHighlighter::Message; + break; + case Base::LogStyle::Error: + style = ReportHighlighter::Error; + break; + case Base::LogStyle::Log: + style = ReportHighlighter::LogText; + break; + case Base::LogStyle::Critical: + style = ReportHighlighter::Critical; + break; + default: + break; + } + + QString qMsg = QString::fromUtf8(msg.c_str()); + + // This truncates log messages that are too long + if (style == ReportHighlighter::LogText) { + if (messageSize > 0 && qMsg.size() > messageSize) { + qMsg.truncate(messageSize); + qMsg += QString::fromLatin1("...\n"); + } + } + + // Send the event to itself to allow thread-safety. Qt will delete it when done. + auto ev = new CustomReportEvent(style, qMsg); + QApplication::postEvent(this, ev); +} + +void ReportOutput::customEvent(QEvent* ev) +{ + // Appends the text stored in the event to the text view + if (ev->type() == QEvent::User) { + auto ce = static_cast(ev); + reportHl->setParagraphType(ce->messageType()); + + bool showTimecode = getWindowParameter()->GetBool("checkShowReportTimecode", true); + QString text = ce->message(); + + // The time code can only be set when the cursor is at the block start + if (showTimecode && blockStart) { + QTime time = QTime::currentTime(); + text.prepend(time.toString(QLatin1String("hh:mm:ss "))); + } + + QTextCursor cursor(this->document()); + cursor.beginEditBlock(); + cursor.movePosition(QTextCursor::End); + cursor.insertText(text); + cursor.endEditBlock(); + + blockStart = cursor.atBlockStart(); + if (gotoEnd) { + setTextCursor(cursor); + } + ensureCursorVisible(); + } +} + + +bool ReportOutput::event(QEvent* event) +{ + if (event && event->type() == QEvent::ShortcutOverride) { + auto kevent = static_cast(event); + if (kevent == QKeySequence::Copy) { + kevent->accept(); + } + } + return QTextEdit::event(event); +} + +void ReportOutput::changeEvent(QEvent* ev) +{ + if (ev->type() == QEvent::StyleChange) { + QPalette pal = qApp->palette(); + QColor color = pal.windowText().color(); + unsigned int text = App::Color::asPackedRGB(color); + auto value = static_cast(text); + // if this parameter is not already set use the style's window text color + value = getWindowParameter()->GetUnsigned("colorText", value); + getWindowParameter()->SetUnsigned("colorText", value); + } + QTextEdit::changeEvent(ev); +} + +void ReportOutput::contextMenuEvent(QContextMenuEvent* e) +{ + bool bShowOnLog = ReportOutputParameter::showOnLogMessage(); + bool bShowOnNormal = ReportOutputParameter::showOnMessage(); + bool bShowOnWarn = ReportOutputParameter::showOnWarning(); + bool bShowOnError = ReportOutputParameter::showOnError(); + bool bShowOnCritical = ReportOutputParameter::showOnCritical(); + + auto menu = new QMenu(this); + auto optionMenu = new QMenu(menu); + optionMenu->setTitle(tr("Options")); + menu->addMenu(optionMenu); + menu->addSeparator(); + + auto displayMenu = new QMenu(optionMenu); + displayMenu->setTitle(tr("Display message types")); + optionMenu->addMenu(displayMenu); + + QAction* logMsg = + displayMenu->addAction(tr("Normal messages"), this, &ReportOutput::onToggleNormalMessage); + logMsg->setCheckable(true); + logMsg->setChecked(bMsg); + + QAction* logAct = + displayMenu->addAction(tr("Log messages"), this, &ReportOutput::onToggleLogMessage); + logAct->setCheckable(true); + logAct->setChecked(bLog); + + QAction* wrnAct = displayMenu->addAction(tr("Warnings"), this, &ReportOutput::onToggleWarning); + wrnAct->setCheckable(true); + wrnAct->setChecked(bWrn); + + QAction* errAct = displayMenu->addAction(tr("Errors"), this, &ReportOutput::onToggleError); + errAct->setCheckable(true); + errAct->setChecked(bErr); + + QAction* logCritical = + displayMenu->addAction(tr("Critical messages"), this, &ReportOutput::onToggleCritical); + logCritical->setCheckable(true); + logCritical->setChecked(bCritical); + + auto showOnMenu = new QMenu(optionMenu); + showOnMenu->setTitle(tr("Show Report view on")); + optionMenu->addMenu(showOnMenu); + + QAction* showNormAct = + showOnMenu->addAction(tr("Normal messages"), + this, + &ReportOutput::onToggleShowReportViewOnNormalMessage); + showNormAct->setCheckable(true); + showNormAct->setChecked(bShowOnNormal); + + QAction* showLogAct = showOnMenu->addAction(tr("Log messages"), + this, + &ReportOutput::onToggleShowReportViewOnLogMessage); + showLogAct->setCheckable(true); + showLogAct->setChecked(bShowOnLog); + + QAction* showWrnAct = + showOnMenu->addAction(tr("Warnings"), this, &ReportOutput::onToggleShowReportViewOnWarning); + showWrnAct->setCheckable(true); + showWrnAct->setChecked(bShowOnWarn); + + QAction* showErrAct = + showOnMenu->addAction(tr("Errors"), this, &ReportOutput::onToggleShowReportViewOnError); + showErrAct->setCheckable(true); + showErrAct->setChecked(bShowOnError); + + QAction* showCriticalAct = showOnMenu->addAction(tr("Critical messages"), + this, + SLOT(onToggleShowReportViewOnCritical())); + showCriticalAct->setCheckable(true); + showCriticalAct->setChecked(bShowOnCritical); + + optionMenu->addSeparator(); + + QAction* stdoutAct = optionMenu->addAction(tr("Redirect Python output"), + this, + &ReportOutput::onToggleRedirectPythonStdout); + stdoutAct->setCheckable(true); + stdoutAct->setChecked(d->redirected_stdout); + + QAction* stderrAct = optionMenu->addAction(tr("Redirect Python errors"), + this, + &ReportOutput::onToggleRedirectPythonStderr); + stderrAct->setCheckable(true); + stderrAct->setChecked(d->redirected_stderr); + + optionMenu->addSeparator(); + QAction* botAct = optionMenu->addAction(tr("Go to end"), this, &ReportOutput::onToggleGoToEnd); + botAct->setCheckable(true); + botAct->setChecked(gotoEnd); + + // Use Qt's internal translation of the Copy & Select All commands + const char* context = "QWidgetTextControl"; + QString copyStr = QCoreApplication::translate(context, "&Copy"); + QAction* copy = menu->addAction(copyStr, this, &ReportOutput::copy); + copy->setShortcut(QKeySequence(QKeySequence::Copy)); + copy->setEnabled(textCursor().hasSelection()); + QIcon icon = QIcon::fromTheme(QString::fromLatin1("edit-copy")); + if (!icon.isNull()) { + copy->setIcon(icon); + } + + menu->addSeparator(); + QString selectStr = QCoreApplication::translate(context, "Select All"); + QAction* select = menu->addAction(selectStr, this, &ReportOutput::selectAll); + select->setShortcut(QKeySequence(QKeySequence::SelectAll)); + + menu->addAction(tr("Clear"), this, &ReportOutput::clear); + menu->addSeparator(); + menu->addAction(tr("Save As..."), this, &ReportOutput::onSaveAs); + + menu->exec(e->globalPos()); + delete menu; +} + +void ReportOutput::onSaveAs() +{ + QString fn = QFileDialog::getSaveFileName( + this, + tr("Save Report Output"), + QString(), + QString::fromLatin1("%1 (*.txt *.log)").arg(tr("Plain Text Files"))); + if (!fn.isEmpty()) { + QFileInfo fi(fn); + if (fi.completeSuffix().isEmpty()) { + fn += QLatin1String(".log"); + } + QFile f(fn); + if (f.open(QIODevice::WriteOnly)) { + QTextStream t(&f); + t << toPlainText(); + f.close(); + } + } +} + +bool ReportOutput::isError() const +{ + return bErr; +} + +bool ReportOutput::isWarning() const +{ + return bWrn; +} + +bool ReportOutput::isLogMessage() const +{ + return bLog; +} + +bool ReportOutput::isNormalMessage() const +{ + return bMsg; +} + + +bool ReportOutput::isCritical() const +{ + return bCritical; +} + +void ReportOutput::onToggleError() +{ + bErr = bErr ? false : true; + getWindowParameter()->SetBool("checkError", bErr); +} + +void ReportOutput::onToggleWarning() +{ + bWrn = bWrn ? false : true; + getWindowParameter()->SetBool("checkWarning", bWrn); +} + +void ReportOutput::onToggleLogMessage() +{ + bLog = bLog ? false : true; + getWindowParameter()->SetBool("checkLogging", bLog); +} + +void ReportOutput::onToggleNormalMessage() +{ + bMsg = bMsg ? false : true; + getWindowParameter()->SetBool("checkMessage", bMsg); +} + +void ReportOutput::onToggleCritical() +{ + bCritical = bCritical ? false : true; + getWindowParameter()->SetBool("checkCritical", bCritical); +} + +void ReportOutput::onToggleShowReportViewOnWarning() +{ + ReportOutputParameter::toggleShowOnWarning(); +} + +void ReportOutput::onToggleShowReportViewOnError() +{ + ReportOutputParameter::toggleShowOnError(); +} + +void ReportOutput::onToggleShowReportViewOnNormalMessage() +{ + ReportOutputParameter::toggleShowOnMessage(); +} + +void ReportOutput::onToggleShowReportViewOnCritical() +{ + ReportOutputParameter::toggleShowOnCritical(); +} + +void ReportOutput::onToggleShowReportViewOnLogMessage() +{ + ReportOutputParameter::toggleShowOnLogMessage(); +} + +void ReportOutput::onToggleRedirectPythonStdout() +{ + if (d->redirected_stdout) { + d->redirected_stdout = false; + Base::PyGILStateLocker lock; + PySys_SetObject("stdout", d->default_stdout); + } + else { + d->redirected_stdout = true; + Base::PyGILStateLocker lock; + PySys_SetObject("stdout", d->replace_stdout); + } + + getWindowParameter()->SetBool("RedirectPythonOutput", d->redirected_stdout); +} + +void ReportOutput::onToggleRedirectPythonStderr() +{ + if (d->redirected_stderr) { + d->redirected_stderr = false; + Base::PyGILStateLocker lock; + PySys_SetObject("stderr", d->default_stderr); + } + else { + d->redirected_stderr = true; + Base::PyGILStateLocker lock; + PySys_SetObject("stderr", d->replace_stderr); + } + + getWindowParameter()->SetBool("RedirectPythonErrors", d->redirected_stderr); +} + +void ReportOutput::onToggleGoToEnd() +{ + gotoEnd = gotoEnd ? false : true; + getWindowParameter()->SetBool("checkGoToEnd", gotoEnd); +} + +void ReportOutput::OnChange(Base::Subject& rCaller, const char* sReason) +{ + ParameterGrp& rclGrp = ((ParameterGrp&)rCaller); + if (strcmp(sReason, "checkLogging") == 0) { + bLog = rclGrp.GetBool(sReason, bLog); + } + else if (strcmp(sReason, "checkWarning") == 0) { + bWrn = rclGrp.GetBool(sReason, bWrn); + } + else if (strcmp(sReason, "checkError") == 0) { + bErr = rclGrp.GetBool(sReason, bErr); + } + else if (strcmp(sReason, "checkMessage") == 0) { + bMsg = rclGrp.GetBool(sReason, bMsg); + } + else if (strcmp(sReason, "checkCritical") == 0) { + bMsg = rclGrp.GetBool(sReason, bMsg); + } + else if (strcmp(sReason, "colorText") == 0) { + unsigned long col = rclGrp.GetUnsigned(sReason); + reportHl->setTextColor(App::Color::fromPackedRGB(col)); + } + else if (strcmp(sReason, "colorCriticalText") == 0) { + unsigned long col = rclGrp.GetUnsigned(sReason); + reportHl->setTextColor(QColor((col >> 24) & 0xff, (col >> 16) & 0xff, (col >> 8) & 0xff)); + } + else if (strcmp(sReason, "colorLogging") == 0) { + unsigned long col = rclGrp.GetUnsigned(sReason); + reportHl->setLogColor(App::Color::fromPackedRGB(col)); + } + else if (strcmp(sReason, "colorWarning") == 0) { + unsigned long col = rclGrp.GetUnsigned(sReason); + reportHl->setWarningColor(App::Color::fromPackedRGB(col)); + } + else if (strcmp(sReason, "colorError") == 0) { + unsigned long col = rclGrp.GetUnsigned(sReason); + reportHl->setErrorColor(App::Color::fromPackedRGB(col)); + } + else if (strcmp(sReason, "checkGoToEnd") == 0) { + gotoEnd = rclGrp.GetBool(sReason, gotoEnd); + } + else if (strcmp(sReason, "FontSize") == 0 || strcmp(sReason, "Font") == 0) { + int fontSize = rclGrp.GetInt("FontSize", 10); + QString fontFamily = QString::fromLatin1(rclGrp.GetASCII("Font", "Courier").c_str()); + + QFont font(fontFamily, fontSize); + setFont(font); + QFontMetrics metric(font); + int width = QtTools::horizontalAdvance(metric, QLatin1String("0000")); +#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) + setTabStopWidth(width); +#else + setTabStopDistance(width); +#endif + } + else if (strcmp(sReason, "RedirectPythonOutput") == 0) { + bool checked = rclGrp.GetBool(sReason, true); + if (checked != d->redirected_stdout) { + onToggleRedirectPythonStdout(); + } + } + else if (strcmp(sReason, "RedirectPythonErrors") == 0) { + bool checked = rclGrp.GetBool(sReason, true); + if (checked != d->redirected_stderr) { + onToggleRedirectPythonStderr(); + } + } + else if (strcmp(sReason, "LogMessageSize") == 0) { + messageSize = rclGrp.GetInt(sReason, d->logMessageSize); + } +} + +#include "moc_ReportView.cpp"