Download src/App/Document.cpp from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 136 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/App/Document.cpp
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/App/Document.cpp
-
curl -L -o Document.cpp https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/App/Document.cpp
136 kB
| // SPDX-License-Identifier: LGPL-2.1-or-later | |
| /*************************************************************************** | |
| * Copyright (c) 2002 Jürgen Riegel <juergen.riegel@web.de> * | |
| * * | |
| * 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 * | |
| * * | |
| ***************************************************************************/ | |
| 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; | |
| namespace fs = std::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); | |
| StatusBits.set((size_t)Document::Closable, true); | |
| StatusBits.set((size_t)Document::KeepTrailingDigits, true); | |
| StatusBits.set((size_t)Document::Restoring, false); | |
| } | |
| } // namespace App | |
| PROPERTY_SOURCE(App::Document, App::PropertyContainer) | |
| bool Document::testStatus(const Status pos) const | |
| { | |
| return d->StatusBits.test(static_cast<size_t>(pos)); | |
| } | |
| void Document::setStatus(const Status pos, const bool on) // NOLINT | |
| { | |
| d->StatusBits.set(static_cast<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() | |
| { | |
| return false; | |
| } | |
| bool Document::undo(const int id) | |
| { | |
| if (d->iUndoMode != 0) { | |
| if (id != 0) { | |
| const 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<bool> 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 (const 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(const int id) | |
| { | |
| if (d->iUndoMode != 0) { | |
| if (id != 0) { | |
| const 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<bool> 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 (const auto& obj : d->objectArray) { | |
| if (obj->testStatus(ObjectStatus::PendingTransactionUpdate)) { | |
| obj->onUndoRedoFinished(); | |
| obj->setStatus(ObjectStatus::PendingTransactionUpdate, false); | |
| } | |
| } | |
| signalRedo(*this); | |
| return true; | |
| } | |
| return false; | |
| } | |
| void Document::changePropertyOfObject(TransactionalObject* obj, | |
| const Property* prop, | |
| const std::function<void()>& changeFunc) | |
| { | |
| if (!prop || !obj || !obj->isAttachedToDocument()) { | |
| return; | |
| } | |
| if ((d->iUndoMode != 0) && !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) { | |
| changeFunc(); | |
| } | |
| } | |
| void Document::renamePropertyOfObject(TransactionalObject* obj, | |
| const Property* prop, const char* oldName) | |
| { | |
| changePropertyOfObject(obj, prop, [this, obj, prop, oldName]() { | |
| d->activeUndoTransaction->renameProperty(obj, prop, oldName); | |
| }); | |
| } | |
| void Document::addOrRemovePropertyOfObject(TransactionalObject* obj, | |
| const Property* prop, const bool add) | |
| { | |
| changePropertyOfObject(obj, prop, [this, obj, prop, add]() { | |
| d->activeUndoTransaction->addOrRemoveProperty(obj, prop, add); | |
| }); | |
| } | |
| bool Document::isPerformingTransaction() const | |
| { | |
| return d->undoing || d->rollback; | |
| } | |
| std::vector<std::string> Document::getAvailableUndoNames() const | |
| { | |
| std::vector<std::string> vList; | |
| if (d->activeUndoTransaction) { | |
| vList.push_back(d->activeUndoTransaction->Name); | |
| } | |
| for (auto It = mUndoTransactions.rbegin(); | |
| It != mUndoTransactions.rend(); | |
| ++It) { | |
| vList.push_back((*It)->Name); | |
| } | |
| return vList; | |
| } | |
| std::vector<std::string> Document::getAvailableRedoNames() const | |
| { | |
| std::vector<std::string> vList; | |
| for (auto It = mRedoTransactions.rbegin(); | |
| It != mRedoTransactions.rend(); | |
| ++It) { | |
| vList.push_back((*It)->Name); | |
| } | |
| return vList; | |
| } | |
| void Document::openTransaction(const char* name) // NOLINT | |
| { | |
| if (isPerformingTransaction() || d->committing) { | |
| if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { | |
| FC_WARN("Cannot open transaction while transacting"); | |
| } | |
| return; | |
| } | |
| GetApplication().setActiveTransaction(name ? name : "<empty>"); | |
| } | |
| 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 != 0) { | |
| // 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 != 0) && 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 = "<empty>"; | |
| } | |
| 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, const int id) const | |
| { | |
| 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 != 0) && !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<Transaction*>::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() // NOLINT | |
| { | |
| 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(const bool notify) | |
| { | |
| if (isPerformingTransaction()) { | |
| if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { | |
| FC_WARN("Cannot commit transaction while transacting"); | |
| } | |
| return; | |
| } | |
| 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); | |
| const 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() const | |
| { | |
| 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<bool> 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 | |
| { | |
| return d->activeUndoTransaction != nullptr; | |
| } | |
| int Document::getTransactionID(const 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 != 0U; ++rit, --pos) {} | |
| return (*rit)->getID(); | |
| } | |
| if (pos >= mRedoTransactions.size()) { | |
| return 0; | |
| } | |
| auto rit = mRedoTransactions.rbegin(); | |
| for (; pos != 0U; ++rit, --pos) {} | |
| return (*rit)->getID(); | |
| } | |
| bool Document::isTransactionEmpty() const | |
| { | |
| return !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(); | |
| } | |
| void Document::clearDocument() // NOLINT | |
| { | |
| 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->objectLabelManager.clear(); | |
| d->objectArray.clear(); | |
| d->objectMap.clear(); | |
| d->objectNameManager.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(const int id) const | |
| { | |
| if (id != 0) { | |
| const 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<int>(mUndoTransactions.size() + 1); | |
| } | |
| return static_cast<int>(mUndoTransactions.size()); | |
| } | |
| int Document::getAvailableRedos(const int id) const | |
| { | |
| if (id != 0) { | |
| const 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 < static_cast<int>(mRedoTransactions.size())); | |
| return i + 1; | |
| } | |
| return static_cast<int>(mRedoTransactions.size()); | |
| } | |
| void Document::setUndoMode(const int iMode) | |
| { | |
| if ((d->iUndoMode != 0) && (iMode == 0)) { | |
| clearUndos(); | |
| } | |
| d->iUndoMode = iMode; | |
| } | |
| int Document::getUndoMode() const | |
| { | |
| return d->iUndoMode; | |
| } | |
| unsigned int Document::getUndoMemSize() const | |
| { | |
| return d->UndoMemSize; | |
| } | |
| void Document::setUndoLimit(const unsigned int UndoMemSize) // NOLINT | |
| { | |
| d->UndoMemSize = UndoMemSize; | |
| } | |
| void Document::setMaxUndoStackSize(const unsigned int UndoMaxStackSize) // NOLINT | |
| { | |
| 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); | |
| GetApplication().signalRelabelDocument(*this); | |
| } | |
| else if (prop == &ShowHidden) { | |
| 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); | |
| } | |
| } | |
| else if (prop == &UseHasher) { | |
| for (auto obj : d->objectArray) { | |
| auto geofeature = freecad_cast<GeoFeature*>(obj); | |
| if (geofeature && geofeature->getPropertyOfGeometry()) { | |
| geofeature->enforceRecompute(); | |
| } | |
| } | |
| } | |
| } | |
| void Document::onBeforeChangeProperty(const TransactionalObject* Who, const Property* What) | |
| { | |
| if (Who->isDerivedFrom<DocumentObject>()) { | |
| signalBeforeChangeObject(*static_cast<const DocumentObject*>(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(const int iMode) // NOLINT | |
| { | |
| d->iTransactionMode = iMode; | |
| } | |
| //-------------------------------------------------------------------------- | |
| // constructor | |
| //-------------------------------------------------------------------------- | |
| Document::Document(const char* documentName) | |
| : d(new DocumentP), 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. | |
| setAutoCreated(false); | |
| Base::PyGILStateLocker lock; | |
| d->DocumentPythonObject = Py::Object(new DocumentPy(this), true); | |
| Console().log("+App::Document: %p\n", this); | |
| std::string CreationDateString = Base::Tools::currentDateTimeString(); | |
| std::string Author = GetApplication() | |
| .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document") | |
| ->GetASCII("prefAuthor", ""); | |
| std::string AuthorComp = | |
| GetApplication() | |
| .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document") | |
| ->GetASCII("prefCompany", ""); | |
| ADD_PROPERTY_TYPE(Label, ("Unnamed"), 0, Prop_ReadOnly, "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(UnitSystem, (""), 0, Prop_None, "Unit system to use in this project"); | |
| // Set up the possible enum values for the unit system | |
| UnitSystem.setEnums(Base::UnitsApi::getDescriptions()); | |
| // Get the preferences/General unit system as the default for a new document | |
| ParameterGrp::handle hGrpu = | |
| GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Units"); | |
| UnitSystem.setValue(hGrpu->GetInt("UserSchema", 0)); | |
| 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 {GetApplication().GetParameterGroupByPath( | |
| "User parameter:BaseApp/Preferences/Document")}; | |
| auto index = static_cast<int>(paramGrp->GetInt("prefLicenseType", 0)); | |
| auto name = ""; | |
| std::string licenseUrl = ""; | |
| if (index >= 0 && index < countOfLicenses) { | |
| name = licenseItems.at(index).at(posnOfFullName); | |
| auto url = licenseItems.at(index).at(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"); | |
| ADD_PROPERTY_TYPE(UseHasher, | |
| (true), | |
| 0, | |
| PropertyType(Prop_Hidden), | |
| "Whether to use hasher on topological naming"); | |
| // 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() | |
| { | |
| Console().log("-App::Document: %s %p\n", getName(), this); | |
| try { | |
| clearUndos(); | |
| } | |
| catch (const boost::exception&) { | |
| } | |
| Console().log("-Delete Features of %s \n", getName()); | |
| 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; | |
| auto* doc = static_cast<Base::PyObjectBase*>(d->DocumentPythonObject.ptr()); | |
| // Call before decrementing the reference counter, otherwise a heap error can occur | |
| doc->setInvalid(); | |
| // remove Transient directory | |
| try { | |
| const Base::FileInfo TransDir(TransientDir.getValue()); | |
| TransDir.deleteDirectoryRecursive(); | |
| } | |
| catch (const Base::Exception& e) { | |
| std::cerr << "Removing transient directory failed: " << e.what() << '\n'; | |
| } | |
| 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 out; | |
| QCryptographicHash hash(QCryptographicHash::Sha1); | |
| hash.addData(filename.c_str(), filename.size()); | |
| hash.addData(QByteArrayView(filename.c_str(), filename.size())); | |
| out << Application::getUserCachePath() << Application::getExecutableName() << "_Doc_" | |
| << uuid << "_" << hash.result().toHex().left(6).constData() << "_" | |
| << Application::applicationPid(); | |
| return out.str(); | |
| } | |
| //-------------------------------------------------------------------------- | |
| // Exported functions | |
| //-------------------------------------------------------------------------- | |
| void Document::Save(Base::Writer& writer) const | |
| { | |
| d->hashers.clear(); | |
| addStringHasher(d->Hasher); | |
| writer.Stream() << R"(<Document SchemaVersion="4" ProgramVersion=")" | |
| << Application::Config()["BuildVersionMajor"] << "." | |
| << Application::Config()["BuildVersionMinor"] << "R" | |
| << Application::Config()["BuildRevision"] << "\" FileVersion=\"" | |
| << writer.getFileVersion() << "\" StringHasher=\"1\">\n"; | |
| writer.incInd(); | |
| d->Hasher->setPersistenceFileName("StringHasher.Table"); | |
| for (const auto o : d->objectArray) { | |
| o->beforeSave(); | |
| } | |
| beforeSave(); | |
| d->Hasher->Save(writer); | |
| writer.decInd(); | |
| PropertyContainer::Save(writer); | |
| // writing the features types | |
| writeObjects(d->objectArray, writer); | |
| writer.Stream() << "</Document>" << '\n'; | |
| } | |
| void Document::Restore(Base::XMLReader& reader) | |
| { | |
| d->hashers.clear(); | |
| d->touchedObjs.clear(); | |
| addStringHasher(d->Hasher); | |
| setStatus(Document::PartialDoc, false); | |
| reader.readElement("Document"); | |
| const long scheme = reader.getAttribute<long>("SchemaVersion"); | |
| reader.DocumentSchema = static_cast<int>(scheme); | |
| if (reader.hasAttribute("ProgramVersion")) { | |
| reader.ProgramVersion = reader.getAttribute<const char*>("ProgramVersion"); | |
| } | |
| else { | |
| reader.ProgramVersion = "pre-0.14"; | |
| } | |
| if (reader.hasAttribute("FileVersion")) { | |
| reader.FileVersion = static_cast<int>(reader.getAttribute<unsigned long>("FileVersion")); | |
| } | |
| else { | |
| reader.FileVersion = 0; | |
| } | |
| if (reader.hasAttribute("StringHasher")) { | |
| d->Hasher->Restore(reader); | |
| } | |
| else { | |
| d->Hasher->clear(); | |
| } | |
| // 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. | |
| const std::string FilePath = FileName.getValue(); | |
| const 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"); | |
| for (auto i = 0; i < reader.getAttribute<long>("Count"); i++) { | |
| reader.readElement("Feature"); | |
| string type = reader.getAttribute<const char*>("type"); | |
| string name = reader.getAttribute<const char*>("name"); | |
| try { | |
| addObject(type.c_str(), name.c_str(), /*isNew=*/false); | |
| } | |
| catch (Base::Exception&) { | |
| Base::Console().message("Cannot create object '%s'\n", name.c_str()); | |
| } | |
| } | |
| reader.readEndElement("Features"); | |
| // read the features itself | |
| reader.readElement("FeatureData"); | |
| for (auto i = 0; i < reader.getAttribute<long>("Count"); i++) { | |
| reader.readElement("Feature"); | |
| string name = reader.getAttribute<const char*>("name"); | |
| DocumentObject* pObj = getObject(name.c_str()); | |
| if (pObj) { // check if this feature has been registered | |
| pObj->setStatus(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"); | |
| } | |
| void DocumentP::checkStringHasher(const Base::XMLReader& reader) | |
| { | |
| if (reader.hasReadFailed("StringHasher.Table.txt")) { | |
| Base::Console().error(QT_TRANSLATE_NOOP( | |
| "Notifications", | |
| "\nIt is recommended that the user right-click the root of " | |
| "the document and select Mark to recompute.\n" | |
| "The user should then click the Refresh button in the main toolbar.\n")); | |
| } | |
| } | |
| std::pair<bool, int> Document::addStringHasher(const StringHasherRef& hasher) const | |
| { | |
| if (!hasher) { | |
| return std::make_pair(false, 0); | |
| } | |
| auto ret = | |
| d->hashers.left.insert(HasherMap::left_map::value_type(hasher, static_cast<int>(d->hashers.size()))); | |
| if (ret.second) { | |
| hasher->clearMarks(); | |
| } | |
| return std::make_pair(ret.second, ret.first->second); | |
| } | |
| StringHasherRef Document::getStringHasher(const int idx) const | |
| { | |
| StringHasherRef hasher; | |
| if (idx < 0) { | |
| if (UseHasher.getValue()) { | |
| return d->Hasher; | |
| } | |
| return hasher; | |
| } | |
| const auto it = d->hashers.right.find(idx); | |
| if (it == d->hashers.right.end()) { | |
| hasher = new StringHasher; | |
| d->hashers.right.insert(HasherMap::right_map::value_type(idx, hasher)); | |
| } | |
| else { | |
| hasher = it->second; | |
| } | |
| return hasher; | |
| } | |
| struct DocExportStatus | |
| { | |
| Document::ExportStatus status; | |
| std::set<const DocumentObject*> objs; | |
| }; | |
| static DocExportStatus exportStatus; | |
| // Exception-safe exporting status setter | |
| class DocumentExporting | |
| { | |
| public: | |
| explicit DocumentExporting(const std::vector<DocumentObject*>& 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 DocumentObject* obj) const | |
| { | |
| if (exportStatus.status != Document::NotExporting | |
| && ((obj == nullptr) || exportStatus.objs.find(obj) != exportStatus.objs.end())) { | |
| return exportStatus.status; | |
| } | |
| return Document::NotExporting; | |
| } | |
| ExportInfo Document::exportInfo() const | |
| { | |
| return d->exportInfo; | |
| } | |
| void Document::setExportInfo(const ExportInfo& info) | |
| { | |
| d->exportInfo = info; | |
| } | |
| void Document::exportObjects(const std::vector<DocumentObject*>& obj, std::ostream& out) | |
| { | |
| DocumentExporting exporting(obj); | |
| d->hashers.clear(); | |
| if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { | |
| for (auto o : obj) { | |
| if (o && o->isAttachedToDocument()) { | |
| FC_LOG("exporting " << o->getFullName()); | |
| if (!o->getPropertyByName("_ObjectUUID")) { | |
| auto prop = static_cast<PropertyUUID*>( | |
| 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() << "<?xml version='1.0' encoding='utf-8'?>" << '\n'; | |
| writer.Stream() << R"(<Document SchemaVersion="4" ProgramVersion=")" | |
| << Application::Config()["BuildVersionMajor"] << "." | |
| << Application::Config()["BuildVersionMinor"] << "R" | |
| << Application::Config()["BuildRevision"] << R"(" FileVersion="1">)" | |
| << '\n'; | |
| // Add this block to have the same layout as for normal documents | |
| writer.Stream() << "<Properties Count=\"0\">" << '\n'; | |
| writer.Stream() << "</Properties>" << '\n'; | |
| // writing the object types | |
| writeObjects(obj, writer); | |
| writer.Stream() << "</Document>" << '\n'; | |
| // Hook for others to add further data. | |
| signalExportObjects(obj, writer); | |
| // write additional files | |
| writer.writeFiles(); | |
| d->hashers.clear(); | |
| } | |
| constexpr auto fcAttrDependencies {"Dependencies"}; | |
| constexpr auto fcElementObjectDeps {"ObjectDeps"}; | |
| constexpr auto fcAttrDepCount {"Count"}; | |
| constexpr auto fcAttrDepObjName {"Name"}; | |
| constexpr auto fcAttrDepAllowPartial {"AllowPartial"}; | |
| constexpr auto fcElementObjectDep {"Dep"}; | |
| void Document::writeObjects(const std::vector<DocumentObject*>& obj, | |
| Base::Writer& writer) const | |
| { | |
| // writing the features types | |
| writer.incInd(); // indentation for 'Objects count' | |
| writer.Stream() << writer.ind() << "<Objects Count=\"" << obj.size(); | |
| if (isExporting(nullptr) == 0U) { | |
| writer.Stream() << "\" " << fcAttrDependencies << "=\"1"; | |
| } | |
| writer.Stream() << "\">" << '\n'; | |
| writer.incInd(); // indentation for 'Object type' | |
| if (isExporting(nullptr) == 0U) { | |
| for (const auto o : obj) { | |
| const auto& outList = | |
| o->getOutList(DocumentObject::OutListNoHidden | DocumentObject::OutListNoXLinked); | |
| writer.Stream() << writer.ind() | |
| << "<" << fcElementObjectDeps << " " << fcAttrDepObjName << "=\"" | |
| << o->getNameInDocument() << "\" " << fcAttrDepCount << "=\"" | |
| << outList.size(); | |
| if (outList.empty()) { | |
| writer.Stream() << "\"/>" << '\n'; | |
| continue; | |
| } | |
| const int partial = o->canLoadPartial(); | |
| if (partial > 0) { | |
| writer.Stream() << "\" " << fcAttrDepAllowPartial << "=\"" << partial; | |
| } | |
| writer.Stream() << "\">" << '\n'; | |
| writer.incInd(); | |
| for (const auto dep : outList) { | |
| const auto name = dep ? dep->getNameInDocument() : ""; | |
| writer.Stream() << writer.ind() | |
| << "<" << fcElementObjectDep << " " << fcAttrDepObjName << "=\"" | |
| << (name ? name : "") << "\"/>" << '\n'; | |
| } | |
| writer.decInd(); | |
| writer.Stream() << writer.ind() << "</" << fcElementObjectDeps << ">" << '\n'; | |
| } | |
| } | |
| std::vector<DocumentObject*>::const_iterator it; | |
| for (it = obj.begin(); it != obj.end(); ++it) { | |
| writer.Stream() << writer.ind() << "<Object " | |
| << "type=\"" << (*it)->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\" "; | |
| const auto desc = getErrorDescription(*it); | |
| if (desc) { | |
| writer.Stream() << "Error=\"" << Property::encodeAttribute(desc) << "\" "; | |
| } | |
| } | |
| writer.Stream() << "/>" << '\n'; | |
| } | |
| writer.decInd(); // indentation for 'Object type' | |
| writer.Stream() << writer.ind() << "</Objects>" << '\n'; | |
| // writing the features itself | |
| writer.Stream() << writer.ind() << "<ObjectData Count=\"" << obj.size() << "\">" << '\n'; | |
| writer.incInd(); // indentation for 'Object name' | |
| for (it = obj.begin(); it != obj.end(); ++it) { | |
| writer.Stream() << writer.ind() << "<Object name=\"" << (*it)->getExportName() << "\""; | |
| if ((*it)->hasExtensions()) { | |
| writer.Stream() << " Extensions=\"True\""; | |
| } | |
| writer.Stream() << ">" << '\n'; | |
| (*it)->Save(writer); | |
| writer.Stream() << writer.ind() << "</Object>" << '\n'; | |
| } | |
| writer.decInd(); // indentation for 'Object name' | |
| writer.Stream() << writer.ind() << "</ObjectData>" << '\n'; | |
| writer.decInd(); // indentation for 'Objects count' | |
| } | |
| struct DepInfo | |
| { | |
| std::unordered_set<std::string> deps; | |
| int canLoadPartial = 0; | |
| }; | |
| static void loadDeps(const std::string& name, | |
| std::unordered_map<std::string, bool>& objs, | |
| const std::unordered_map<std::string, DepInfo>& deps) | |
| { | |
| const auto it = deps.find(name); | |
| if (it == deps.end()) { | |
| objs.emplace(name, true); | |
| return; | |
| } | |
| if (it->second.canLoadPartial != 0) { | |
| 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) { | |
| if (auto found = objs.find(dep); found != objs.end() && found->second) { | |
| continue; | |
| } | |
| loadDeps(dep, objs, deps); | |
| } | |
| } | |
| std::vector<DocumentObject*> Document::readObjects(Base::XMLReader& reader) | |
| { | |
| d->touchedObjs.clear(); | |
| bool keepDigits = testStatus(Document::KeepTrailingDigits); | |
| setStatus(Document::KeepTrailingDigits, !reader.doNameMapping()); | |
| std::vector<DocumentObject*> objs; | |
| // read the object types | |
| reader.readElement("Objects"); | |
| int Cnt = static_cast<int>(reader.getAttribute<long>("Count")); | |
| if (!reader.hasAttribute(fcAttrDependencies)) { | |
| d->partialLoadObjects.clear(); | |
| } | |
| else if (!d->partialLoadObjects.empty()) { | |
| std::unordered_map<std::string, DepInfo> deps; | |
| for (int i = 0; i < Cnt; i++) { | |
| reader.readElement(fcElementObjectDeps); | |
| int dcount = static_cast<int>(reader.getAttribute<long>(fcAttrDepCount)); | |
| if (dcount == 0) { | |
| continue; | |
| } | |
| auto& info = deps[reader.getAttribute<const char*>(fcAttrDepObjName)]; | |
| if (reader.hasAttribute(fcAttrDepAllowPartial)) { | |
| info.canLoadPartial = | |
| static_cast<int>(reader.getAttribute<long>(fcAttrDepAllowPartial)); | |
| } | |
| for (int j = 0; j < dcount; ++j) { | |
| reader.readElement(fcElementObjectDep); | |
| const char* name = reader.getAttribute<const char*>(fcAttrDepObjName); | |
| if (!Base::Tools::isNullOrEmpty(name)) { | |
| info.deps.insert(name); | |
| } | |
| } | |
| reader.readEndElement(fcElementObjectDeps); | |
| } | |
| std::vector<std::string> strings; | |
| strings.reserve(d->partialLoadObjects.size()); | |
| for (auto& v : d->partialLoadObjects) { | |
| strings.emplace_back(v.first.c_str()); | |
| } | |
| for (auto& name : strings) { | |
| loadDeps(name, d->partialLoadObjects, deps); | |
| } | |
| if (Cnt > static_cast<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; i < Cnt; i++) { | |
| reader.readElement("Object"); | |
| std::string type = reader.getAttribute<const char*>("type"); | |
| std::string name = reader.getAttribute<const char*>("name"); | |
| std::string viewType = | |
| reader.hasAttribute("ViewType") ? reader.getAttribute<const char*>("ViewType") : ""; | |
| bool partial = false; | |
| if (!d->partialLoadObjects.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.getAttribute<long>("id") - 1; | |
| } | |
| // To prevent duplicate name when export/import of objects from | |
| // external documents, we append those external object name with | |
| // @<document name>. 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 {nullptr}; | |
| 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. | |
| 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.getAttribute<long>("Touched") != 0) { | |
| d->touchedObjs.insert(obj); | |
| } | |
| } | |
| if (reader.hasAttribute("Invalid")) { | |
| obj->setStatus(ObjectStatus::Error, | |
| reader.getAttribute<bool>("Invalid")); | |
| if (obj->isError() && reader.hasAttribute("Error")) { | |
| d->addRecomputeLog(reader.getAttribute<const char*>("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 = static_cast<int>(reader.getAttribute<long>("Count")); | |
| for (int i = 0; i < Cnt; i++) { | |
| reader.readElement("Object"); | |
| std::string name = reader.getName(reader.getAttribute<const char*>("name")); | |
| if (DocumentObject* pObj = getObject(name.c_str()); pObj | |
| && !pObj->testStatus( | |
| 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) // NOLINT | |
| { | |
| if (testStatus(Status::Restoring) && obj) { | |
| setStatus(Status::RecomputeOnRestore, true); | |
| d->touchedObjs.insert(obj); | |
| obj->touch(); | |
| } | |
| } | |
| std::vector<DocumentObject*> Document::importObjects(Base::XMLReader& reader) | |
| { | |
| d->hashers.clear(); | |
| Base::FlagToggler<> flag(globalIsRestoring, false); | |
| Base::ObjectStatusLocker<Status, Document> restoreBit(Status::Restoring, this); | |
| Base::ObjectStatusLocker<Status, Document> restoreBit2(Status::Importing, this); | |
| ExpressionParser::ExpressionImporter expImporter(reader); | |
| reader.readElement("Document"); | |
| const long scheme = reader.getAttribute<long>("SchemaVersion"); | |
| reader.DocumentSchema = static_cast<int>(scheme); | |
| if (reader.hasAttribute("ProgramVersion")) { | |
| reader.ProgramVersion = reader.getAttribute<const char*>("ProgramVersion"); | |
| } | |
| else { | |
| reader.ProgramVersion = "pre-0.14"; | |
| } | |
| if (reader.hasAttribute("FileVersion")) { | |
| reader.FileVersion = static_cast<int>(reader.getAttribute<unsigned long>("FileVersion")); | |
| } | |
| else { | |
| reader.FileVersion = 0; | |
| } | |
| std::vector<DocumentObject*> objs = readObjects(reader); | |
| for (const auto o : objs) { | |
| if (o && o->isAttachedToDocument()) { | |
| o->setStatus(ObjImporting, true); | |
| FC_LOG("importing " << o->getFullName()); | |
| if (const auto propUUID = | |
| freecad_cast<PropertyUUID*>(o->getPropertyByName("_ObjectUUID"))) { | |
| auto propSource = | |
| freecad_cast<PropertyUUID*>(o->getPropertyByName("_SourceUUID")); | |
| if (!propSource) { | |
| propSource = static_cast<PropertyUUID*>( | |
| 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 (const auto o : objs) { | |
| if (o && o->isAttachedToDocument()) { | |
| o->setStatus(ObjImporting, false); | |
| } | |
| } | |
| d->hashers.clear(); | |
| return objs; | |
| } | |
| unsigned int Document::getMemSize() const | |
| { | |
| unsigned int size = 0; | |
| // size of the DocObjects in the document | |
| for (const auto & it : d->objectArray) { | |
| size += it->getMemSize(); | |
| } | |
| size += d->Hasher->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 (GetApplication() | |
| .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document") | |
| ->GetBool("CheckExtension", true)) { | |
| const char* ext = strrchr(file, '.'); | |
| if ((ext == nullptr) || !boost::iequals(ext + 1, "fcstd")) { | |
| if (ext && ext[1] == 0) { | |
| fn += "FCStd"; | |
| } | |
| else { | |
| fn += ".FCStd"; | |
| } | |
| } | |
| } | |
| return fn; | |
| } | |
| bool Document::saveAs(const char* _file) | |
| { | |
| const std::string file = checkFileName(_file); | |
| const 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 | |
| { | |
| const std::string checked = checkFileName(file); | |
| return this->FileName.getStrValue() != checked ? saveToFile(checked.c_str()) : 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()); | |
| } | |
| const std::string LastModifiedDateString = Base::Tools::currentDateTimeString(); | |
| LastModifiedDate.setValue(LastModifiedDateString.c_str()); | |
| // set author if needed | |
| const bool saveAuthor = | |
| GetApplication() | |
| .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document") | |
| ->GetBool("prefSetAuthorOnSave", false); | |
| if (saveAuthor) { | |
| const std::string Author = | |
| GetApplication() | |
| .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document") | |
| ->GetASCII("prefAuthor", ""); | |
| LastModifiedBy.setValue(Author.c_str()); | |
| } | |
| return saveToFile(FileName.getValue()); | |
| } | |
| return false; | |
| } | |
| bool Document::saveToFile(const char* filename) const | |
| { | |
| signalStartSave(*this, filename); | |
| auto hGrp = GetApplication().GetParameterGroupByPath( | |
| "User parameter:BaseApp/Preferences/Document"); | |
| int compression = static_cast<int>(hGrp->GetInt("CompressionLevel", 7)); | |
| compression = Base::clamp<int>(compression, Z_NO_COMPRESSION, Z_BEST_COMPRESSION); | |
| bool policy = GetApplication() | |
| .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document") | |
| ->GetBool("BackupPolicy", true); | |
| auto canonical_path = [](const char* filename) { | |
| try { | |
| QString utf8Name = QString::fromUtf8(filename); | |
| auto realpath = fs::weakly_canonical(fs::absolute(fs::path(utf8Name.toStdWString()))); | |
| std::string nativePath = QString::fromStdWString(realpath.native()).toStdString(); | |
| auto realpath = fs::weakly_canonical(fs::absolute(fs::path(filename))); | |
| std::string nativePath = realpath.native(); | |
| // 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&) { | |
| QString utf8Name = QString::fromUtf8(filename); | |
| auto parentPath = fs::absolute(fs::path(utf8Name.toStdWString())).parent_path(); | |
| auto parentPath = fs::absolute(fs::path(filename)).parent_path(); | |
| fs::create_directories(parentPath); | |
| return std::string(filename); | |
| } | |
| }; | |
| // realpath is canonical filename i.e. without symlink | |
| std::string nativePath = canonical_path(filename); | |
| // check if file is writeable, then block the save if it is not. | |
| Base::FileInfo originalFileInfo(nativePath); | |
| if (originalFileInfo.exists() && !originalFileInfo.isWritable()) { | |
| throw Base::FileException("Unable to save document because file is marked as read-only or write permission is not available.", originalFileInfo); | |
| } | |
| // 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; | |
| } | |
| // open extra scope to close ZipWriter properly | |
| { | |
| Base::FileInfo tmp(fn); | |
| 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() << "<?xml version='1.0' encoding='utf-8'?>" << '\n' | |
| << "<!--" << '\n' | |
| << " FreeCAD Document, see https://www.freecad.org for more information..." | |
| << '\n' | |
| << "-->" << '\n'; | |
| Document::Save(writer); | |
| // Special handling for Gui document. | |
| signalSaveDocument(writer); | |
| // write additional files | |
| writer.writeFiles(); | |
| if (writer.hasErrors()) { | |
| // retrieve Writer error strings | |
| std::stringstream message; | |
| message << "Failed to write all data to file "; | |
| message << writer.getErrors().front(); | |
| throw Base::FileException(message.str().c_str(), tmp); | |
| } | |
| GetApplication().signalSaveDocument(*this); | |
| } | |
| if (policy) { | |
| // if saving the project data succeeded rename to the actual file name | |
| int count_bak = static_cast<int>(GetApplication() | |
| .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document") | |
| ->GetInt("CountBackupFiles", 1)); | |
| bool backup = GetApplication() | |
| .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document") | |
| ->GetBool("CreateBackupFiles", true); | |
| if (!backup) { | |
| count_bak = -1; | |
| } | |
| bool useFCBakExtension = | |
| GetApplication() | |
| .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document") | |
| ->GetBool("UseFCBakExtension", true); | |
| std::string saveBackupDateFormat = | |
| GetApplication() | |
| .GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document") | |
| ->GetASCII("SaveBackupDateFormat", "%Y%m%d-%H%M%S"); | |
| BackupPolicy backupPolicy; | |
| if (useFCBakExtension) { | |
| backupPolicy.setPolicy(BackupPolicy::TimeStamp); | |
| backupPolicy.useBackupExtension(useFCBakExtension); | |
| backupPolicy.setDateFormat(saveBackupDateFormat); | |
| } | |
| else { | |
| backupPolicy.setPolicy(BackupPolicy::Standard); | |
| } | |
| backupPolicy.setNumberOfFiles(count_bak); | |
| backupPolicy.apply(fn, nativePath); | |
| } | |
| signalFinishSave(*this, filename); | |
| return true; | |
| } | |
| void Document::registerLabel(const std::string& newLabel) | |
| { | |
| if (!newLabel.empty()) { | |
| d->objectLabelManager.addExactName(newLabel); | |
| } | |
| } | |
| void Document::unregisterLabel(const std::string& oldLabel) | |
| { | |
| if (!oldLabel.empty()) { | |
| d->objectLabelManager.removeExactName(oldLabel); | |
| } | |
| } | |
| bool Document::containsLabel(const std::string& label) | |
| { | |
| return d->objectLabelManager.containsName(label); | |
| } | |
| std::string Document::makeUniqueLabel(const std::string& modelLabel) | |
| { | |
| if (modelLabel.empty()) { | |
| return {}; | |
| } | |
| return d->objectLabelManager.makeUniqueName(modelLabel, 3); | |
| } | |
| bool Document::isAnyRestoring() | |
| { | |
| return globalIsRestoring; | |
| } | |
| // Open the document | |
| void Document::restore(const char* filename, | |
| bool delaySignal, | |
| const std::vector<std::string>& 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->objectLabelManager.clear(); | |
| d->objectArray.clear(); | |
| d->objectNameManager.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); | |
| DocumentP::checkStringHasher(reader); | |
| 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(const 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<DocumentObject*>& 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<DocumentObject*, std::vector<Property*>> 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<DocumentObject*> 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 (...) { | |
| // If a Python exception occurred, it must be cleared immediately. | |
| // Otherwise, the interpreter remains in a dirty state, causing | |
| // Segfaults later when FreeCAD interacts with Python. | |
| if (PyErr_Occurred()) { | |
| Base::Console().error("Python error during object restore:\n"); | |
| PyErr_Print(); // Print the traceback to stderr/Console | |
| PyErr_Clear(); // Reset the interpreter state | |
| } | |
| 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 = freecad_cast<PropertyLinkBase*>(prop); | |
| int res {0}; | |
| std::string errMsg; | |
| if (link && ((res = link->checkRestore(&errMsg)) != 0)) { | |
| 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; | |
| } | |
| if (!d->touchedObjs.contains(obj)) { | |
| obj->purgeTouched(); | |
| } | |
| signalFinishRestoreObject(*obj); | |
| } | |
| d->touchedObjs.clear(); | |
| return true; | |
| } | |
| bool Document::isSaved() const | |
| { | |
| 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; | |
| } | |
| void Document::setAutoCreated(bool value) { | |
| autoCreated = value; | |
| } | |
| bool Document::isAutoCreated() const { | |
| return autoCreated; | |
| } | |
| 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() // NOLINT | |
| { | |
| for (const auto It : d->objectArray) { | |
| It->purgeTouched(); | |
| } | |
| } | |
| bool Document::isTouched() const | |
| { | |
| for (const auto It : d->objectArray) { | |
| if (It->isTouched()) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| vector<DocumentObject*> Document::getTouched() const | |
| { | |
| vector<DocumentObject*> result; | |
| for (auto It : d->objectArray) { | |
| if (It->isTouched()) { | |
| result.push_back(It); | |
| } | |
| } | |
| return result; | |
| } | |
| void Document::setClosable(const bool c) // NOLINT | |
| { | |
| setStatus(Document::Closable, c); | |
| } | |
| bool Document::isClosable() const | |
| { | |
| return testStatus(Document::Closable); | |
| } | |
| int Document::countObjects() const | |
| { | |
| return static_cast<int>(d->objectArray.size()); | |
| } | |
| void Document::getLinksTo(std::set<DocumentObject*>& links, | |
| const DocumentObject* obj, | |
| const int options, | |
| const int maxCount, | |
| const std::vector<DocumentObject*>& objs) const | |
| { | |
| std::map<const DocumentObject*, std::vector<DocumentObject*>> linkMap; | |
| for (auto o : !objs.empty() ? objs : d->objectArray) { | |
| if (o == obj) { | |
| continue; | |
| } | |
| auto linked = o; | |
| if ((options & GetLinkArrayElement) != 0) { | |
| linked = o->getLinkedObject(false); | |
| } | |
| else { | |
| const auto ext = o->getExtensionByType<LinkBaseExtension>(true); | |
| linked = | |
| ext ? ext->getTrueLinkedObject(false, nullptr, 0, true) : o->getLinkedObject(false); | |
| } | |
| if (linked && linked != o) { | |
| if ((options & GetLinkRecursive) != 0) { | |
| linkMap[linked].push_back(o); | |
| } | |
| else if (linked == obj || !obj) { | |
| if (((options & GetLinkExternal) != 0) && linked->getDocument() == o->getDocument()) { | |
| continue; | |
| } | |
| if ((options & GetLinkedObject) != 0) { | |
| links.insert(linked); | |
| } | |
| else { | |
| links.insert(o); | |
| } | |
| if ((maxCount != 0) && maxCount <= static_cast<int>(links.size())) { | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| if ((options & GetLinkRecursive) == 0) { | |
| return; | |
| } | |
| std::vector<const DocumentObject*> current(1, obj); | |
| for (int depth = 0; !current.empty(); ++depth) { | |
| if (GetApplication().checkLinkDepth(depth, MessageOption::Error) == 0) { | |
| break; | |
| } | |
| std::vector<const DocumentObject*> next; | |
| for (const DocumentObject* o : current) { | |
| auto iter = linkMap.find(o); | |
| if (iter == linkMap.end()) { | |
| continue; | |
| } | |
| for (DocumentObject* link : iter->second) { | |
| if (links.insert(link).second) { | |
| if ((maxCount != 0) && maxCount <= static_cast<int>(links.size())) { | |
| return; | |
| } | |
| next.push_back(link); | |
| } | |
| } | |
| } | |
| current = std::move(next); | |
| } | |
| } | |
| bool Document::hasLinksTo(const DocumentObject* obj) const | |
| { | |
| std::set<DocumentObject*> links; | |
| getLinksTo(links, obj, 0, 1); | |
| return !links.empty(); | |
| } | |
| std::vector<DocumentObject*> Document::getInList(const DocumentObject* me) const | |
| { | |
| // result list | |
| std::vector<DocumentObject*> result; | |
| // go through all objects | |
| for (const auto& [name, object] : d->objectMap) { | |
| // get the outList and search if me is in that list | |
| std::vector<DocumentObject*> OutList = object->getOutList(); | |
| for (const auto obj : OutList) { | |
| if (obj && obj == me) { | |
| // add the parent object | |
| result.push_back(object); | |
| } | |
| } | |
| } | |
| 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<DocumentObject*>& objectArray, | |
| const int options, | |
| std::vector<DocumentObject*>* depObjs, | |
| DependencyList* depList, | |
| std::map<DocumentObject*, Vertex>* objectMap, | |
| bool* touchCheck = nullptr) | |
| { | |
| std::map<DocumentObject*, std::vector<DocumentObject*>> outLists; | |
| std::deque<DocumentObject*> objs; | |
| if (objectMap) { | |
| objectMap->clear(); | |
| } | |
| if (depList) { | |
| depList->clear(); | |
| } | |
| const int op = ((options & Document::DepNoXLinked) != 0) ? DocumentObject::OutListNoXLinked : 0; | |
| for (auto obj : objectArray) { | |
| objs.push_back(obj); | |
| while (!objs.empty()) { | |
| auto objF = objs.front(); | |
| objs.pop_front(); | |
| if (!objF || !objF->isAttachedToDocument()) { | |
| continue; | |
| } | |
| auto it = outLists.find(objF); | |
| if (it != outLists.end()) { | |
| continue; | |
| } | |
| if (touchCheck) { | |
| if (objF->isTouched() || (objF->mustExecute() != 0)) { | |
| // early termination on touch check | |
| *touchCheck = true; | |
| return; | |
| } | |
| } | |
| if (depObjs) { | |
| depObjs->push_back(objF); | |
| } | |
| if (objectMap && depList) { | |
| (*objectMap)[objF] = add_vertex(*depList); | |
| } | |
| auto& outList = outLists[objF]; | |
| outList = objF->getOutList(op); | |
| objs.insert(objs.end(), outList.begin(), outList.end()); | |
| } | |
| } | |
| if (objectMap && depList) { | |
| for (const auto& [key, objects] : outLists) { | |
| for (auto obj : objects) { | |
| if (obj && obj->isAttachedToDocument()) { | |
| add_edge((*objectMap)[key], (*objectMap)[obj], *depList); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| std::vector<DocumentObject*> | |
| Document::getDependencyList(const std::vector<DocumentObject*>& objs, int options) | |
| { | |
| std::vector<DocumentObject*> ret; | |
| if ((options & DepSort) == 0) { | |
| buildDependencyList(objs, options, &ret, nullptr, nullptr); | |
| return ret; | |
| } | |
| DependencyList depList; | |
| std::map<DocumentObject*, Vertex> objectMap; | |
| std::map<Vertex, DocumentObject*> vertexMap; | |
| buildDependencyList(objs, options, nullptr, &depList, &objectMap); | |
| for (auto& v : objectMap) { | |
| vertexMap[v.second] = v.first; | |
| } | |
| std::list<Vertex> make_order; | |
| try { | |
| boost::topological_sort(depList, std::front_inserter(make_order)); | |
| } | |
| catch (const std::exception& e) { | |
| if ((options & DepNoCycle) != 0) { | |
| // Use boost::strong_components to find cycles. It groups strongly | |
| // connected vertices as components, and therefore each component | |
| // forms a cycle. | |
| std::vector<int> c(vertexMap.size()); | |
| std::map<int, std::vector<Vertex>> 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; i < c.size(); ++i) { | |
| components[c[i]].push_back(i); | |
| } | |
| FC_ERR("Dependency cycles: "); | |
| std::ostringstream ss; | |
| ss << '\n'; | |
| for (auto& [key, vertexes] : components) { | |
| if (vertexes.size() == 1) { | |
| // For components with only one member, we still need to | |
| // check if there it is self looping. | |
| auto it = vertexMap.find(vertexes[0]); | |
| if (it == vertexMap.end()) { | |
| continue; | |
| } | |
| // Try search the object in its own out list | |
| for (auto obj : it->second->getOutList()) { | |
| if (obj == it->second) { | |
| ss << '\n' << it->second->getFullName() << '\n'; | |
| break; | |
| } | |
| } | |
| continue; | |
| } | |
| // For components with more than one member, they form a loop together | |
| for (size_t i = 0; i < vertexes.size(); ++i) { | |
| auto it = vertexMap.find(vertexes[i]); | |
| if (it == vertexMap.end()) { | |
| continue; | |
| } | |
| if (i % 6 == 0) { | |
| ss << '\n'; | |
| } | |
| ss << it->second->getFullName() << ", "; | |
| } | |
| ss << '\n'; | |
| } | |
| FC_ERR(ss.str()); | |
| FC_THROWM(Base::RuntimeError, e.what()); | |
| } | |
| FC_ERR(e.what()); | |
| ret = DocumentP::partialTopologicalSort(objs); | |
| std::reverse(ret.begin(), ret.end()); | |
| return ret; | |
| } | |
| for (auto i = make_order.rbegin(); i != make_order.rend(); ++i) { | |
| ret.push_back(vertexMap[*i]); | |
| } | |
| return ret; | |
| } | |
| std::vector<Document*> Document::getDependentDocuments(const bool sort) | |
| { | |
| return getDependentDocuments({this}, sort); | |
| } | |
| std::vector<Document*> Document::getDependentDocuments(std::vector<Document*> docs, | |
| const bool sort) | |
| { | |
| DependencyList depList; | |
| std::map<Document*, Vertex> docMap; | |
| std::map<Vertex, Document*> vertexMap; | |
| std::vector<Document*> ret; | |
| if (docs.empty()) { | |
| return ret; | |
| } | |
| auto outLists = PropertyXLink::getDocumentOutList(); | |
| std::set<Document*> docSet; | |
| docSet.insert(docs.begin(), docs.end()); | |
| if (sort) { | |
| for (auto doc : docs) { | |
| docMap[doc] = add_vertex(depList); | |
| } | |
| } | |
| while (!docs.empty()) { | |
| auto doc = docs.back(); | |
| docs.pop_back(); | |
| auto it = outLists.find(doc); | |
| if (it == outLists.end()) { | |
| continue; | |
| } | |
| const auto& vertex = docMap[doc]; | |
| for (auto depDoc : it->second) { | |
| if (docSet.insert(depDoc).second) { | |
| docs.push_back(depDoc); | |
| if (sort) { | |
| docMap[depDoc] = add_vertex(depList); | |
| } | |
| } | |
| add_edge(vertex, docMap[depDoc], depList); | |
| } | |
| } | |
| if (!sort) { | |
| ret.insert(ret.end(), docSet.begin(), docSet.end()); | |
| return ret; | |
| } | |
| std::list<Vertex> 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<DocumentObject*>& objs) | |
| { | |
| (void)objs; | |
| } | |
| /** | |
| * @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<ObjectIdentifier, ObjectIdentifier>& paths, | |
| const std::function<bool(const DocumentObject*)>& selector) // NOLINT | |
| { | |
| std::map<ObjectIdentifier, ObjectIdentifier> extendedPaths; | |
| auto it = paths.begin(); | |
| while (it != paths.end()) { | |
| extendedPaths[it->first.canonicalPath()] = it->second.canonicalPath(); | |
| ++it; | |
| } | |
| for (const auto object : d->objectArray) { | |
| if (selector(object)) { | |
| object->renameObjectIdentifiers(extendedPaths); | |
| } | |
| } | |
| } | |
| void Document::setPreRecomputeHook(const PreRecomputeHook& hook) | |
| { | |
| d->_preRecomputeHook = hook; | |
| } | |
| int Document::recompute(const std::vector<DocumentObject*>& objs, | |
| bool force, | |
| bool* hasError, | |
| int options) | |
| { | |
| ZoneScoped; | |
| 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<Document::Status, Document> exe(Document::Recomputing, this); | |
| // This will hop into the main thread, fire signalBeforeRecompute(), | |
| // and *block* the worker until the main thread is done, avoiding races | |
| // between any running Python code and the rest of the recompute call. | |
| if (d->_preRecomputeHook) { | |
| d->_preRecomputeHook(); | |
| } | |
| ////////////////////////////////////////////////////////////////////////// | |
| // 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<DocumentObject*> topoSortedObjects = topologicalSort(depObjs); | |
| if (topoSortedObjects.size() != depObjs.size()){ | |
| cerr << "Document::recompute(): cyclic dependency detected" << '\n'; | |
| topoSortedObjects = d->partialTopologicalSort(depObjs); | |
| } | |
| std::reverse(topoSortedObjects.begin(),topoSortedObjects.end()); | |
| */ | |
| // alt: | |
| auto topoSortedObjects = | |
| getDependencyList(objs.empty() ? d->objectArray : objs, DepSort | options); | |
| 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); | |
| FC_TIME_INIT(t2); | |
| try { | |
| std::set<DocumentObject*> filter; | |
| size_t idx = 0; | |
| // maximum two passes to allow some form of dependency inversion | |
| for (int passes = 0; passes < 2 && idx < topoSortedObjects.size(); ++passes) { | |
| std::unique_ptr<Base::SequencerLauncher> seq; | |
| if (canAbort) { | |
| seq = std::make_unique<Base::SequencerLauncher>("Recompute...", | |
| topoSortedObjects.size()); | |
| } | |
| FC_LOG("Recompute pass " << passes); | |
| for (; idx < topoSortedObjects.size(); ++idx) { | |
| auto obj = topoSortedObjects[idx]; | |
| if (!obj->isAttachedToDocument() || 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 != 0) { | |
| 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; i < topoSortedObjects.size(); ++i) { | |
| auto obj = topoSortedObjects[i]; | |
| obj->setStatus(ObjectStatus::Recompute2, false); | |
| if (!filter.contains(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->isAttachedToDocument()) { | |
| 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)) { | |
| for (auto it : topoSortedObjects) { | |
| if (it->isError()) { | |
| const char* text = getErrorDescription(it); | |
| if (text) { | |
| Base::Console().error("%s: %s\n", it->Label.getValue(), text); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| for (auto doc : GetApplication().getDocuments()) { | |
| decltype(doc->d->pendingRemove) objects; | |
| objects.swap(doc->d->pendingRemove); | |
| for (auto& o : objects) { | |
| 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; | |
| } | |
| /*! | |
| 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<DocumentObject*> | |
| DocumentP::partialTopologicalSort(const std::vector<DocumentObject*>& objects) | |
| { | |
| vector<DocumentObject*> ret; | |
| ret.reserve(objects.size()); | |
| // pairs of input and output degree | |
| map<DocumentObject*, std::pair<int, int>> 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<DocumentObject*> degIn; | |
| std::list<DocumentObject*> degOut; | |
| bool removeVertex = true; | |
| while (removeVertex) { | |
| removeVertex = false; | |
| // try input degree | |
| auto degInIt = find_if(countMap.begin(), | |
| countMap.end(), | |
| [](pair<DocumentObject*, pair<int, int>> 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& [obj, pair] : countMap) { | |
| if (pair.first < 0) { | |
| pair.second = -1; | |
| } | |
| } | |
| removeVertex = degIn.size() != objects.size(); | |
| while (removeVertex) { | |
| removeVertex = false; | |
| auto degOutIt = find_if(countMap.begin(), | |
| countMap.end(), | |
| [](pair<DocumentObject*, pair<int, int>> 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<DocumentObject*> | |
| DocumentP::topologicalSort(const std::vector<DocumentObject*>& objects) const | |
| { | |
| // topological sort algorithm described here: | |
| // https://de.wikipedia.org/wiki/Topologische_Sortierung#Algorithmus_f.C3.BCr_das_Topologische_Sortieren | |
| vector<DocumentObject*> ret; | |
| ret.reserve(objects.size()); | |
| map<DocumentObject*, int> countMap; | |
| for (auto objectIt : objects) { | |
| // We now support externally linked objects | |
| // if(!obj->isAttachedToDocument() || obj->getDocument()!=this) | |
| if (!objectIt->isAttachedToDocument()) { | |
| 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<DocumentObject*, int> count) -> bool { | |
| return count.second == 0; | |
| }); | |
| if (rootObjeIt == countMap.end()) { | |
| cerr << "Document::topologicalSort: cyclic dependency detected (no root object)" << '\n'; | |
| 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<DocumentObject*, int> count) -> bool { | |
| return count.second == 0; | |
| }); | |
| } | |
| return ret; | |
| } | |
| std::vector<DocumentObject*> Document::topologicalSort() const | |
| { | |
| return d->topologicalSort(d->objectArray); | |
| } | |
| const char* Document::getErrorDescription(const DocumentObject* Obj) const | |
| { | |
| return d->findRecomputeLog(Obj); | |
| } | |
| // call the recompute of the Feature and handle the exceptions and errors. | |
| int Document::_recomputeFeature(DocumentObject* Feat) // NOLINT | |
| { | |
| 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; | |
| } | |
| catch (...) { | |
| FC_ERR("Unknown exception in " << Feat->getFullName() << " thrown"); | |
| d->addRecomputeLog("Unknown exception!", Feat); | |
| return 1; | |
| } | |
| 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* feature, bool recursive) | |
| { | |
| // delete recompute log | |
| d->clearRecomputeLog(feature); | |
| // verify that the feature is (active) part of the document | |
| if (!feature->isAttachedToDocument()) { | |
| return false; | |
| } | |
| if (recursive) { | |
| bool hasError = false; | |
| recompute({feature}, true, &hasError); | |
| return !hasError; | |
| } | |
| _recomputeFeature(feature); | |
| signalRecomputedObject(*feature); | |
| return feature->isValid(); | |
| } | |
| DocumentObject* Document::addObject(const char* sType, | |
| const char* pObjectName, | |
| const bool isNew, | |
| const char* viewType, | |
| const bool isPartial) | |
| { | |
| const Base::Type type = | |
| Base::Type::getTypeIfDerivedFrom(sType, DocumentObject::getClassTypeId(), true); | |
| if (type.isBad()) { | |
| std::stringstream str; | |
| str << "Document::addObject: '" << sType << "' is not a document object type"; | |
| throw Base::TypeError(str.str()); | |
| } | |
| void* typeInstance = type.createInstance(); | |
| if (!typeInstance) { | |
| return nullptr; | |
| } | |
| auto* pcObject = static_cast<DocumentObject*>(typeInstance); | |
| pcObject->setDocument(this); | |
| _addObject(pcObject, | |
| pObjectName, | |
| AddObjectOption::SetNewStatus | |
| | (isPartial ? AddObjectOption::SetPartialStatus : AddObjectOption::UnsetPartialStatus) | |
| | (isNew ? AddObjectOption::DoSetup : AddObjectOption::None) | |
| | AddObjectOption::ActivateObject, | |
| viewType); | |
| // return the Object | |
| return pcObject; | |
| } | |
| std::vector<DocumentObject*> | |
| Document::addObjects(const char* sType, const std::vector<std::string>& objectNames, bool isNew) | |
| { | |
| Base::Type type = | |
| Base::Type::getTypeIfDerivedFrom(sType, DocumentObject::getClassTypeId(), true); | |
| if (type.isBad()) { | |
| std::stringstream str; | |
| str << "'" << sType << "' is not a document object type"; | |
| throw Base::TypeError(str.str()); | |
| } | |
| std::vector<DocumentObject*> objects; | |
| objects.resize(objectNames.size()); | |
| std::generate(objects.begin(), objects.end(), [&] { | |
| return static_cast<DocumentObject*>(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; | |
| } | |
| for (auto it = objects.begin(); it != objects.end(); ++it) { | |
| size_t index = std::distance(objects.begin(), it); | |
| DocumentObject* pcObject = *it; | |
| pcObject->setDocument(this); | |
| // Add the object but only activate the last one | |
| bool isLast = index == (objects.size() - 1); | |
| _addObject(pcObject, | |
| objectNames[index].c_str(), | |
| AddObjectOption::SetNewStatus | |
| | (isNew ? AddObjectOption::DoSetup : AddObjectOption::None) | |
| | (isLast ? AddObjectOption::ActivateObject : AddObjectOption::None)); | |
| } | |
| 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); | |
| _addObject(pcObject, pObjectName, AddObjectOption::SetNewStatus | AddObjectOption::ActivateObject); | |
| } | |
| void Document::_addObject(DocumentObject* pcObject, const char* pObjectName, AddObjectOptions options, const char* viewType) | |
| { | |
| // get unique name | |
| string ObjectName; | |
| if (!Base::Tools::isNullOrEmpty(pObjectName)) { | |
| ObjectName = getUniqueObjectName(pObjectName); | |
| } | |
| else { | |
| ObjectName = getUniqueObjectName(pcObject->getTypeId().getName()); | |
| } | |
| // insert in the name map | |
| d->objectMap[ObjectName] = pcObject; | |
| d->objectNameManager.addExactName(ObjectName); | |
| // cache the pointer to the name string in the Object (for performance of | |
| // DocumentObject::getNameInDocument()) | |
| pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first); | |
| // Register the current Label even though it might be about to change | |
| registerLabel(pcObject->Label.getStrValue()); | |
| // generate object id and add to id map + object array | |
| if (pcObject->_Id == 0) { | |
| pcObject->_Id = ++d->lastObjectId; | |
| } | |
| d->objectIdMap[pcObject->_Id] = pcObject; | |
| d->objectArray.push_back(pcObject); | |
| // do no transactions if we do a rollback! | |
| if (!d->rollback) { | |
| // Undo stuff | |
| _checkTransaction(nullptr, nullptr, __LINE__); | |
| if (d->activeUndoTransaction) { | |
| d->activeUndoTransaction->addObjectDel(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 (options.testFlag(AddObjectOption::SetNewStatus) && !d->StatusBits.test(Restoring)) { | |
| pcObject->Label.setValue(ObjectName); | |
| } | |
| // Call the object-specific initialization | |
| if (!isPerformingTransaction() && options.testFlag(AddObjectOption::DoSetup)) { | |
| pcObject->setupObject(); | |
| } | |
| if (options.testFlag(AddObjectOption::SetNewStatus)) { | |
| pcObject->setStatus(ObjectStatus::New, true); | |
| } | |
| if (options.testFlag(AddObjectOption::SetPartialStatus) || options.testFlag(AddObjectOption::UnsetPartialStatus)) { | |
| pcObject->setStatus(ObjectStatus::PartialObject, options.testFlag(AddObjectOption::SetPartialStatus)); | |
| } | |
| if (Base::Tools::isNullOrEmpty(viewType)) { | |
| 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 (options.testFlag(AddObjectOption::ActivateObject)) { | |
| d->activeObject = pcObject; | |
| signalActivatedObject(*pcObject); | |
| } | |
| } | |
| bool Document::containsObject(const DocumentObject* pcObject) const | |
| { | |
| // We could look for the object in objectMap (keyed by object name), | |
| // or search in objectArray (a O(n) vector search) but looking by Id | |
| // in objectIdMap would be fastest. | |
| auto found = d->objectIdMap.find(pcObject->getID()); | |
| return found != d->objectIdMap.end() && found->second == pcObject; | |
| } | |
| /// Remove an object out of the document | |
| void Document::removeObject(const char* sName) | |
| { | |
| auto pos = d->objectMap.find(sName); | |
| if (pos == d->objectMap.end()){ | |
| FC_MSG("Object " << sName << " already deleted in document " << getName()); | |
| 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; | |
| } | |
| _removeObject(pos->second, RemoveObjectOption::MayRemoveWhileRecomputing | RemoveObjectOption::MayDestroyOutOfTransaction); | |
| } | |
| void Document::_removeObject(DocumentObject* pcObject, RemoveObjectOptions options) | |
| { | |
| if (!options.testFlag(RemoveObjectOption::MayRemoveWhileRecomputing) && testStatus(Document::Recomputing)) { | |
| FC_ERR("Cannot delete " << pcObject->getFullName() << " while recomputing"); | |
| return; | |
| } | |
| TransactionLocker tlock; | |
| _checkTransaction(pcObject, nullptr, __LINE__); | |
| auto pos = d->objectMap.find(pcObject->getNameInDocument()); | |
| if (pos == d->objectMap.end()) { | |
| FC_ERR("Internal error, could not find " << pcObject->getFullName() << " to remove"); | |
| } | |
| if (options.testFlag(RemoveObjectOption::PreserveChildrenVisibility) | |
| && !d->rollback && d->activeUndoTransaction && pcObject->hasChildElement()) { | |
| // Preserve link group sub object global visibilities. Normally those | |
| // claimed object should be hidden in global coordinate space. However, | |
| // when the group is deleted, the user will naturally try to show the | |
| // children, which may now in the global space. When the parent is | |
| // undeleted, having its children shown in both the local and global | |
| // coordinate space is very confusing. Hence, we preserve the visibility | |
| // here | |
| for (auto& sub : pcObject->getSubObjects()) { | |
| if (sub.empty()) { | |
| continue; | |
| } | |
| if (sub[sub.size() - 1] != '.') { | |
| sub += '.'; | |
| } | |
| auto sobj = pcObject->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); | |
| signalTransactionRemove(*pcObject, d->rollback ? nullptr : d->activeUndoTransaction); | |
| breakDependency(pcObject, true); | |
| // 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(""); | |
| } | |
| // remove from map | |
| pcObject->setStatus(ObjectStatus::Remove, false); // Unset the bit to be on the safe side | |
| d->objectIdMap.erase(pcObject->_Id); | |
| d->objectNameManager.removeExactName(pos->first); | |
| unregisterLabel(pcObject->Label.getStrValue()); | |
| // do no transactions if we do a rollback! | |
| if (!d->rollback && d->activeUndoTransaction) { | |
| d->activeUndoTransaction->addObjectNew(pcObject); | |
| } | |
| std::unique_ptr<DocumentObject> tobedestroyed; | |
| if ((options.testFlag(RemoveObjectOption::MayDestroyOutOfTransaction) && !d->rollback && !d->activeUndoTransaction) | |
| || (options.testFlag(RemoveObjectOption::DestroyOnRollback) && d->rollback)) { | |
| // if not saved in undo -> delete object later | |
| std::unique_ptr<DocumentObject> delobj(pos->second); | |
| tobedestroyed.swap(delobj); | |
| tobedestroyed->setStatus(ObjectStatus::Destroy, true); | |
| } | |
| for (auto it = d->objectArray.begin(); | |
| it != d->objectArray.end(); | |
| ++it) { | |
| if (*it == pcObject) { | |
| d->objectArray.erase(it); | |
| break; | |
| } | |
| } | |
| // In case the object gets deleted the pointer must be nullified | |
| if (tobedestroyed) { | |
| tobedestroyed->pcNameInDocument = nullptr; | |
| } | |
| // Erase last to avoid invalidating pcObject->pcNameInDocument | |
| // when it is still needed in Transaction::addObjectNew | |
| d->objectMap.erase(pos); | |
| } | |
| void Document::breakDependency(DocumentObject* pcObject, const bool clear) // NOLINT | |
| { | |
| // Nullify all dependent objects | |
| PropertyLinkBase::breakLinks(pcObject, d->objectArray, clear); | |
| } | |
| std::vector<DocumentObject*> | |
| Document::copyObject(const std::vector<DocumentObject*>& objs, bool recursive, bool returnAll) | |
| { | |
| std::vector<DocumentObject*> 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 (auto it : deps) { | |
| 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<DocumentObject*> 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(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<DocumentObject*, size_t> indices; | |
| size_t i = 0; | |
| for (auto o : deps) { | |
| indices[o] = i++; | |
| } | |
| std::vector<DocumentObject*> result; | |
| result.reserve(objs.size()); | |
| for (auto o : objs) { | |
| result.push_back(imported[indices[o]]); | |
| } | |
| return result; | |
| } | |
| std::vector<DocumentObject*> | |
| Document::importLinks(const std::vector<DocumentObject*>& objs) | |
| { | |
| std::set<DocumentObject*> links; | |
| getLinksTo(links, nullptr, GetLinkExternal, 0, objs); | |
| std::vector<DocumentObject*> vecObjs; | |
| vecObjs.insert(vecObjs.end(), links.begin(), links.end()); | |
| std::vector<DocumentObject*> depObjs = getDependencyList(vecObjs); | |
| if (depObjs.empty()) { | |
| FC_ERR("nothing to import"); | |
| return depObjs; | |
| } | |
| for (auto it = depObjs.begin(); it != depObjs.end();) { | |
| auto obj = *it; | |
| if (obj->getDocument() == this) { | |
| it = depObjs.erase(it); | |
| continue; | |
| } | |
| ++it; | |
| if (obj->testStatus(PartialObject)) { | |
| throw Base::RuntimeError( | |
| "Cannot import partial loaded object. Please reload the current document"); | |
| } | |
| } | |
| Base::FileInfo fi(Application::getTempFileName()); | |
| { | |
| // save stuff to temp file | |
| Base::ofstream str(fi, std::ios::out | std::ios::binary); | |
| MergeDocuments mimeView(this); | |
| exportObjects(depObjs, str); | |
| str.close(); | |
| } | |
| Base::ifstream str(fi, std::ios::in | std::ios::binary); | |
| MergeDocuments mimeView(this); | |
| depObjs = 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<Property*, std::unique_ptr<Property>> propMap; | |
| std::vector<Property*> propList; | |
| for (auto obj : links) { | |
| propList.clear(); | |
| obj->getPropertyList(propList); | |
| for (auto prop : propList) { | |
| auto linkProp = freecad_cast<PropertyLinkBase*>(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 depObjs; | |
| } | |
| DocumentObject* Document::moveObject(DocumentObject* obj, const 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 == 0) && (that->d->iUndoMode == 0) && !that->d->rollback) { | |
| // all object of the other document that refer to this object must be nullified | |
| that->breakDependency(obj, false); | |
| const std::string objname = getUniqueObjectName(obj->getNameInDocument()); | |
| that->_removeObject(obj); | |
| this->_addObject(obj, objname.c_str()); | |
| obj->setDocument(this); | |
| return obj; | |
| } | |
| std::vector<DocumentObject*> deps; | |
| if (recursive) { | |
| deps = getDependencyList({obj}, DepNoXLinked | DepSort); | |
| } | |
| else { | |
| deps.push_back(obj); | |
| } | |
| const 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<int> ids; | |
| ids.reserve(deps.size()); | |
| for (const auto o : deps) { | |
| ids.push_back(static_cast<int>(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) { | |
| const 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 | |
| { | |
| const auto pos = d->objectMap.find(Name); | |
| return pos != d->objectMap.end() ?pos->second:nullptr; | |
| } | |
| DocumentObject* Document::getObjectByID(const long id) const | |
| { | |
| const auto it = d->objectIdMap.find(id); | |
| return it != d->objectIdMap.end() ?it->second:nullptr; | |
| } | |
| // Note: This method is only used in Tree.cpp slotChangeObject(), see explanation there | |
| bool Document::isIn(const DocumentObject* pFeat) const | |
| { | |
| for (const auto& [key, object] : d->objectMap) { | |
| if (object == pFeat) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| const char* Document::getObjectName(const DocumentObject* pFeat) const | |
| { | |
| for (const auto& [key, object] : d->objectMap) { | |
| if (object == pFeat) { | |
| return key.c_str(); | |
| } | |
| } | |
| return nullptr; | |
| } | |
| std::string Document::getUniqueObjectName(const char* proposedName) const | |
| { | |
| if (!proposedName || *proposedName == '\0') { | |
| return {}; | |
| } | |
| std::string cleanName = Base::Tools::getIdentifier(proposedName); | |
| if (!d->objectNameManager.containsName(cleanName)) { | |
| // Not in use yet, name is OK | |
| return cleanName; | |
| } | |
| return d->objectNameManager.makeUniqueName(cleanName, 3); | |
| } | |
| bool | |
| Document::haveSameBaseName(const std::string& name, const std::string& label) | |
| { | |
| // Both Labels and Names use the same decomposition rules for names, | |
| // i.e. the default one supplied by UniqueNameManager, so we can use either | |
| // of the name managers to do this test. | |
| return d->objectNameManager.haveSameBaseName(name, label); | |
| } | |
| std::string Document::getStandardObjectLabel(const char* modelName, int digitCount) const | |
| { | |
| return d->objectLabelManager.makeUniqueName(modelName, digitCount); | |
| } | |
| std::vector<DocumentObject*> Document::getDependingObjects() const | |
| { | |
| return getDependencyList(d->objectArray); | |
| } | |
| const std::vector<DocumentObject*>& Document::getObjects() const | |
| { | |
| return d->objectArray; | |
| } | |
| std::vector<DocumentObject*> Document::getObjectsOfType(const Base::Type& typeId) const | |
| { | |
| std::vector<DocumentObject*> Objects; | |
| for (auto it : d->objectArray) { | |
| if (it->isDerivedFrom(typeId)) { | |
| Objects.push_back(it); | |
| } | |
| } | |
| return Objects; | |
| } | |
| std::vector<DocumentObject*> Document::getObjectsOfType(const std::vector<Base::Type>& types) const | |
| { | |
| std::vector<DocumentObject*> Objects; | |
| for (auto it : d->objectArray) { | |
| for (auto& typeId : types) { | |
| if (it->isDerivedFrom(typeId)) { | |
| Objects.push_back(it); | |
| break; // Prevent adding several times the same object. | |
| } | |
| } | |
| } | |
| return Objects; | |
| } | |
| std::vector<DocumentObject*> Document::getObjectsWithExtension(const Base::Type& typeId, | |
| const bool derived) const | |
| { | |
| std::vector<DocumentObject*> Objects; | |
| for (auto it : d->objectArray) { | |
| if (it->hasExtension(typeId, derived)) { | |
| Objects.push_back(it); | |
| } | |
| } | |
| return Objects; | |
| } | |
| std::vector<DocumentObject*> | |
| Document::findObjects(const Base::Type& typeId, const char* objname, const char* label) const | |
| { | |
| boost::cmatch what; | |
| boost::regex rx_name; | |
| boost::regex rx_label; | |
| if (objname) { | |
| rx_name.set_expression(objname); | |
| } | |
| if (label) { | |
| rx_label.set_expression(label); | |
| } | |
| std::vector<DocumentObject*> Objects; | |
| DocumentObject* found = nullptr; | |
| for (const auto it : d->objectArray) { | |
| if (it->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 | |
| { | |
| return std::count_if(d->objectMap.begin(), d->objectMap.end(), [&](const auto& it) { | |
| return it.second->isDerivedFrom(typeId); | |
| }); | |
| } | |
| int Document::countObjectsOfType(const char* typeName) const | |
| { | |
| const Base::Type type = Base::Type::fromName(typeName); | |
| return type.isBad() ? 0 : countObjectsOfType(type); | |
| } | |
| PyObject* Document::getPyObject() | |
| { | |
| return Py::new_reference_to(d->DocumentPythonObject); | |
| } | |
| std::vector<DocumentObject*> Document::getRootObjects() const | |
| { | |
| std::vector<DocumentObject*> ret; | |
| for (auto objectIt : d->objectArray) { | |
| if (objectIt->getInList().empty()) { | |
| ret.push_back(objectIt); | |
| } | |
| } | |
| return ret; | |
| } | |
| std::vector<DocumentObject*> Document::getRootObjectsIgnoreLinks() const | |
| { | |
| std::vector<DocumentObject*> ret; | |
| for (const auto &objectIt : d->objectArray) { | |
| auto list = objectIt->getInList(); | |
| bool noParents = list.empty(); | |
| if (!noParents) { | |
| // App::Document getRootObjects returns the root objects of the dependency graph. | |
| // So if an object is referenced by an App::Link, it will not be returned by that | |
| // function. So here, as we want the tree-root level objects, we check if all the | |
| // parents are links. In which case it's still a root object. | |
| noParents = std::all_of(list.cbegin(), list.cend(), [](DocumentObject* obj) { | |
| return obj->isDerivedFrom<Link>(); | |
| }); | |
| } | |
| if (noParents) { | |
| ret.push_back(objectIt); | |
| } | |
| } | |
| return ret; | |
| } | |
| void DocumentP::findAllPathsAt(const std::vector<Node>& all_nodes, | |
| const size_t id, | |
| std::vector<Path>& all_paths, | |
| Path tmp) | |
| { | |
| if (std::ranges::find(tmp, id) != tmp.end()) { | |
| tmp.push_back(id); | |
| all_paths.push_back(std::move(tmp)); | |
| return; // a cycle | |
| } | |
| tmp.push_back(id); | |
| if (all_nodes[id].empty()) { | |
| all_paths.push_back(std::move(tmp)); | |
| return; | |
| } | |
| for (size_t i = 0; i < all_nodes[id].size(); i++) { | |
| const Path& tmp2(tmp); | |
| findAllPathsAt(all_nodes, all_nodes[id][i], all_paths, tmp2); | |
| } | |
| } | |
| std::vector<std::list<DocumentObject*>> | |
| Document::getPathsByOutList(const DocumentObject* from, const DocumentObject* to) const | |
| { | |
| std::map<const DocumentObject*, size_t> indexMap; | |
| for (size_t i = 0; i < d->objectArray.size(); ++i) { | |
| indexMap[d->objectArray[i]] = i; | |
| } | |
| std::vector<Node> all_nodes(d->objectArray.size()); | |
| for (size_t i = 0; i < d->objectArray.size(); ++i) { | |
| const DocumentObject* obj = d->objectArray[i]; | |
| std::vector<DocumentObject*> outList = obj->getOutList(); | |
| for (const auto it : outList) { | |
| all_nodes[i].push_back(indexMap[it]); | |
| } | |
| } | |
| std::vector<std::list<DocumentObject*>> array; | |
| if (from == to) { | |
| return array; | |
| } | |
| size_t index_from = indexMap[from]; | |
| size_t index_to = indexMap[to]; | |
| std::vector<Path> all_paths; | |
| DocumentP::findAllPathsAt(all_nodes, index_from, all_paths, Path()); | |
| for (const Path& it : all_paths) { | |
| auto jt = std::ranges::find(it, index_to); | |
| if (jt != it.end()) { | |
| array.push_back({}); | |
| auto& path = array.back(); | |
| for (auto kt = it.begin(); kt != jt; ++kt) { | |
| path.push_back(d->objectArray[*kt]); | |
| } | |
| path.push_back(d->objectArray[*jt]); | |
| } | |
| } | |
| // 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, 0, nullptr, nullptr, nullptr, &touched); | |
| return touched; | |
| } | |
| for (const auto It : d->objectArray) { | |
| if (It->isTouched() || It->mustExecute() == 1) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |