4377 lines
150 KiB
C++
4377 lines
150 KiB
C++
/***************************************************************************
|
|
* 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 *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
#include "PreCompiled.h"
|
|
|
|
#ifndef _PreComp_
|
|
#include <bitset>
|
|
#include <stack>
|
|
#include <boost/filesystem.hpp>
|
|
#include <deque>
|
|
#include <iostream>
|
|
#include <utility>
|
|
#include <set>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <map>
|
|
#include <vector>
|
|
#include <list>
|
|
#include <algorithm>
|
|
#include <filesystem>
|
|
#endif
|
|
|
|
#include <boost/algorithm/string.hpp>
|
|
#include <boost/bimap.hpp>
|
|
#include <boost/graph/strong_components.hpp>
|
|
|
|
#include <boost/regex.hpp>
|
|
#include <random>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
|
|
#include <QCryptographicHash>
|
|
#include <QCoreApplication>
|
|
|
|
#include <App/DocumentPy.h>
|
|
#include <Base/Interpreter.h>
|
|
#include <Base/Console.h>
|
|
#include <Base/Exception.h>
|
|
#include <Base/FileInfo.h>
|
|
#include <Base/TimeInfo.h>
|
|
#include <Base/Reader.h>
|
|
#include <Base/Writer.h>
|
|
#include <Base/Profiler.h>
|
|
#include <Base/Tools.h>
|
|
#include <Base/Uuid.h>
|
|
#include <Base/Sequencer.h>
|
|
#include <Base/Stream.h>
|
|
#include <Base/UnitsApi.h>
|
|
|
|
#include "Document.h"
|
|
#include "private/DocumentP.h"
|
|
#include "Application.h"
|
|
#include "AutoTransaction.h"
|
|
#include "ExpressionParser.h"
|
|
#include "GeoFeature.h"
|
|
#include "License.h"
|
|
#include "Link.h"
|
|
#include "MergeDocuments.h"
|
|
#include "StringHasher.h"
|
|
#include "Transactions.h"
|
|
|
|
#ifdef _MSC_VER
|
|
#include <zipios++/zipios-config.h>
|
|
#endif
|
|
#include <zipios++/zipfile.h>
|
|
#include <zipios++/zipinputstream.h>
|
|
#include <zipios++/zipoutputstream.h>
|
|
#include <zipios++/meta-iostreams.h>
|
|
|
|
|
|
FC_LOG_LEVEL_INIT("App", true, true, true)
|
|
|
|
using Base::Console;
|
|
using Base::streq;
|
|
using Base::Writer;
|
|
using namespace App;
|
|
using namespace std;
|
|
using namespace boost;
|
|
using namespace zipios;
|
|
|
|
#if FC_DEBUG
|
|
#define FC_LOGFEATUREUPDATE
|
|
#endif
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace App
|
|
{
|
|
|
|
static bool globalIsRestoring;
|
|
static bool globalIsRelabeling;
|
|
|
|
DocumentP::DocumentP()
|
|
{
|
|
Hasher = new StringHasher;
|
|
static std::random_device _RD;
|
|
static std::mt19937 _RGEN(_RD());
|
|
static std::uniform_int_distribution<> _RDIST(0, 5000);
|
|
// Set some random offset to reduce likelihood of ID collision when
|
|
// copying shape from other document. It is probably better to randomize
|
|
// on each object ID.
|
|
lastObjectId = _RDIST(_RGEN);
|
|
activeObject = nullptr;
|
|
activeUndoTransaction = nullptr;
|
|
iTransactionMode = 0;
|
|
rollback = false;
|
|
undoing = false;
|
|
committing = false;
|
|
opentransaction = false;
|
|
StatusBits.set((size_t)Document::Closable, true);
|
|
StatusBits.set((size_t)Document::KeepTrailingDigits, true);
|
|
StatusBits.set((size_t)Document::Restoring, false);
|
|
iUndoMode = 0;
|
|
UndoMemSize = 0;
|
|
UndoMaxStackSize = 20;
|
|
}
|
|
|
|
} // namespace App
|
|
|
|
PROPERTY_SOURCE(App::Document, App::PropertyContainer)
|
|
|
|
bool Document::testStatus(Status pos) const
|
|
{
|
|
return d->StatusBits.test((size_t)pos);
|
|
}
|
|
|
|
void Document::setStatus(Status pos, bool on)
|
|
{
|
|
d->StatusBits.set((size_t)pos, on);
|
|
}
|
|
|
|
// bool _has_cycle_dfs(const DependencyList & g, vertex_t u, default_color_type * color)
|
|
//{
|
|
// color[u] = gray_color;
|
|
// graph_traits < DependencyList >::adjacency_iterator vi, vi_end;
|
|
// for (tie(vi, vi_end) = adjacent_vertices(u, g); vi != vi_end; ++vi)
|
|
// if (color[*vi] == white_color)
|
|
// if (has_cycle_dfs(g, *vi, color))
|
|
// return true; // cycle detected, return immediately
|
|
// else if (color[*vi] == gray_color) // *vi is an ancestor!
|
|
// return true;
|
|
// color[u] = black_color;
|
|
// return false;
|
|
// }
|
|
|
|
bool Document::checkOnCycle()
|
|
{
|
|
#if 0
|
|
std::vector < default_color_type > color(num_vertices(_DepList), white_color);
|
|
graph_traits < DependencyList >::vertex_iterator vi, vi_end;
|
|
for (tie(vi, vi_end) = vertices(_DepList); vi != vi_end; ++vi)
|
|
if (color[*vi] == white_color)
|
|
if (_has_cycle_dfs(_DepList, *vi, &color[0]))
|
|
return true;
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
bool Document::undo(int id)
|
|
{
|
|
if (d->iUndoMode) {
|
|
if (id) {
|
|
auto it = mUndoMap.find(id);
|
|
if (it == mUndoMap.end()) {
|
|
return false;
|
|
}
|
|
if (it->second != d->activeUndoTransaction) {
|
|
while (!mUndoTransactions.empty() && mUndoTransactions.back() != it->second) {
|
|
undo(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (d->activeUndoTransaction) {
|
|
_commitTransaction(true);
|
|
}
|
|
if (mUndoTransactions.empty()) {
|
|
return false;
|
|
}
|
|
// redo
|
|
d->activeUndoTransaction = new Transaction(mUndoTransactions.back()->getID());
|
|
d->activeUndoTransaction->Name = mUndoTransactions.back()->Name;
|
|
|
|
{
|
|
Base::FlagToggler<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 (auto& obj : d->objectArray) {
|
|
if (obj->testStatus(ObjectStatus::PendingTransactionUpdate)) {
|
|
obj->onUndoRedoFinished();
|
|
obj->setStatus(ObjectStatus::PendingTransactionUpdate, false);
|
|
}
|
|
}
|
|
|
|
signalUndo(*this); // now signal the undo
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool Document::redo(int id)
|
|
{
|
|
if (d->iUndoMode) {
|
|
if (id) {
|
|
auto it = mRedoMap.find(id);
|
|
if (it == mRedoMap.end()) {
|
|
return false;
|
|
}
|
|
while (!mRedoTransactions.empty() && mRedoTransactions.back() != it->second) {
|
|
redo(0);
|
|
}
|
|
}
|
|
|
|
if (d->activeUndoTransaction) {
|
|
_commitTransaction(true);
|
|
}
|
|
|
|
assert(mRedoTransactions.size() != 0);
|
|
|
|
// undo
|
|
d->activeUndoTransaction = new Transaction(mRedoTransactions.back()->getID());
|
|
d->activeUndoTransaction->Name = mRedoTransactions.back()->Name;
|
|
|
|
// do the redo
|
|
{
|
|
Base::FlagToggler<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 (auto& obj : d->objectArray) {
|
|
if (obj->testStatus(ObjectStatus::PendingTransactionUpdate)) {
|
|
obj->onUndoRedoFinished();
|
|
obj->setStatus(ObjectStatus::PendingTransactionUpdate, false);
|
|
}
|
|
}
|
|
|
|
signalRedo(*this);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void Document::addOrRemovePropertyOfObject(TransactionalObject* obj, Property* prop, bool add)
|
|
{
|
|
if (!prop || !obj || !obj->isAttachedToDocument()) {
|
|
return;
|
|
}
|
|
if (d->iUndoMode && !isPerformingTransaction() && !d->activeUndoTransaction) {
|
|
if (!testStatus(Restoring) || testStatus(Importing)) {
|
|
int tid = 0;
|
|
const char* name = GetApplication().getActiveTransaction(&tid);
|
|
if (name && tid > 0) {
|
|
_openTransaction(name, tid);
|
|
}
|
|
}
|
|
}
|
|
if (d->activeUndoTransaction && !d->rollback) {
|
|
d->activeUndoTransaction->addOrRemoveProperty(obj, prop, add);
|
|
}
|
|
}
|
|
|
|
bool Document::isPerformingTransaction() const
|
|
{
|
|
return d->undoing || d->rollback;
|
|
}
|
|
|
|
std::vector<std::string> Document::getAvailableUndoNames() const
|
|
{
|
|
std::vector<std::string> vList;
|
|
if (d->activeUndoTransaction) {
|
|
vList.push_back(d->activeUndoTransaction->Name);
|
|
}
|
|
for (std::list<Transaction*>::const_reverse_iterator 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 (std::list<Transaction*>::const_reverse_iterator It = mRedoTransactions.rbegin();
|
|
It != mRedoTransactions.rend();
|
|
++It) {
|
|
vList.push_back((**It).Name);
|
|
}
|
|
return vList;
|
|
}
|
|
|
|
void Document::openTransaction(const char* name)
|
|
{
|
|
if (isPerformingTransaction() || d->committing) {
|
|
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
|
|
FC_WARN("Cannot open transaction while transacting");
|
|
}
|
|
return;
|
|
}
|
|
|
|
GetApplication().setActiveTransaction(name ? name : "<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) {
|
|
// Avoid recursive calls that is possible while
|
|
// clearing the redo transactions and will cause
|
|
// a double deletion of some transaction and thus
|
|
// a segmentation fault
|
|
if (d->opentransaction) {
|
|
return 0;
|
|
}
|
|
Base::FlagToggler<> flag(d->opentransaction);
|
|
|
|
if (id && mUndoMap.find(id) != mUndoMap.end()) {
|
|
throw Base::RuntimeError("invalid transaction id");
|
|
}
|
|
if (d->activeUndoTransaction) {
|
|
_commitTransaction(true);
|
|
}
|
|
_clearRedos();
|
|
|
|
d->activeUndoTransaction = new Transaction(id);
|
|
if (!name) {
|
|
name = "<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, int id)
|
|
{
|
|
if (name && d->activeUndoTransaction && d->activeUndoTransaction->getID() == id) {
|
|
if (boost::starts_with(d->activeUndoTransaction->Name, "-> ")) {
|
|
d->activeUndoTransaction->Name.resize(3);
|
|
}
|
|
else {
|
|
d->activeUndoTransaction->Name.clear();
|
|
}
|
|
d->activeUndoTransaction->Name += name;
|
|
}
|
|
}
|
|
|
|
void Document::_checkTransaction(DocumentObject* pcDelObj, const Property* What, int line)
|
|
{
|
|
// if the undo is active but no transaction open, open one!
|
|
if (d->iUndoMode && !isPerformingTransaction()) {
|
|
if (!d->activeUndoTransaction) {
|
|
if (!testStatus(Restoring) || testStatus(Importing)) {
|
|
int tid = 0;
|
|
const char* name = GetApplication().getActiveTransaction(&tid);
|
|
if (name && tid > 0) {
|
|
bool ignore = false;
|
|
if (What && What->testStatus(Property::NoModify)) {
|
|
ignore = true;
|
|
}
|
|
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
|
|
if (What) {
|
|
FC_LOG((ignore ? "ignore" : "auto")
|
|
<< " transaction (" << line << ") '" << What->getFullName());
|
|
}
|
|
else {
|
|
FC_LOG((ignore ? "ignore" : "auto") << " transaction (" << line << ") '"
|
|
<< name << "' in " << getName());
|
|
}
|
|
}
|
|
if (!ignore) {
|
|
_openTransaction(name, tid);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (!pcDelObj) {
|
|
return;
|
|
}
|
|
// When the object is going to be deleted we have to check if it has already been added
|
|
// to the undo transactions
|
|
std::list<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()
|
|
{
|
|
if (isPerformingTransaction() || d->committing) {
|
|
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
|
|
FC_WARN("Cannot commit transaction while transacting");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (d->activeUndoTransaction) {
|
|
GetApplication().closeActiveTransaction(false, d->activeUndoTransaction->getID());
|
|
}
|
|
}
|
|
|
|
void Document::_commitTransaction(bool notify)
|
|
{
|
|
if (isPerformingTransaction()) {
|
|
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
|
|
FC_WARN("Cannot commit transaction while transacting");
|
|
}
|
|
return;
|
|
}
|
|
else if (d->committing) {
|
|
// for a recursive call return without printing a warning
|
|
return;
|
|
}
|
|
|
|
if (d->activeUndoTransaction) {
|
|
Base::FlagToggler<> flag(d->committing);
|
|
Application::TransactionSignaller signaller(false, true);
|
|
int id = d->activeUndoTransaction->getID();
|
|
mUndoTransactions.push_back(d->activeUndoTransaction);
|
|
d->activeUndoTransaction = nullptr;
|
|
// check the stack for the limits
|
|
if (mUndoTransactions.size() > d->UndoMaxStackSize) {
|
|
mUndoMap.erase(mUndoTransactions.front()->getID());
|
|
delete mUndoTransactions.front();
|
|
mUndoTransactions.pop_front();
|
|
}
|
|
signalCommitTransaction(*this);
|
|
|
|
// closeActiveTransaction() may call again _commitTransaction()
|
|
if (notify) {
|
|
GetApplication().closeActiveTransaction(false, id);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Document::abortTransaction()
|
|
{
|
|
if (isPerformingTransaction() || d->committing) {
|
|
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
|
|
FC_WARN("Cannot abort transaction while transacting");
|
|
}
|
|
return;
|
|
}
|
|
if (d->activeUndoTransaction) {
|
|
GetApplication().closeActiveTransaction(true, d->activeUndoTransaction->getID());
|
|
}
|
|
}
|
|
|
|
void Document::_abortTransaction()
|
|
{
|
|
if (isPerformingTransaction() || d->committing) {
|
|
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
|
|
FC_WARN("Cannot abort transaction while transacting");
|
|
}
|
|
}
|
|
|
|
if (d->activeUndoTransaction) {
|
|
Base::FlagToggler<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
|
|
{
|
|
if (d->activeUndoTransaction) {
|
|
return true;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
int Document::getTransactionID(bool undo, unsigned pos) const
|
|
{
|
|
if (undo) {
|
|
if (d->activeUndoTransaction) {
|
|
if (pos == 0) {
|
|
return d->activeUndoTransaction->getID();
|
|
}
|
|
--pos;
|
|
}
|
|
if (pos >= mUndoTransactions.size()) {
|
|
return 0;
|
|
}
|
|
auto rit = mUndoTransactions.rbegin();
|
|
for (; pos; ++rit, --pos) {
|
|
continue;
|
|
}
|
|
return (*rit)->getID();
|
|
}
|
|
if (pos >= mRedoTransactions.size()) {
|
|
return 0;
|
|
}
|
|
auto rit = mRedoTransactions.rbegin();
|
|
for (; pos; ++rit, --pos) {}
|
|
return (*rit)->getID();
|
|
}
|
|
|
|
bool Document::isTransactionEmpty() const
|
|
{
|
|
if (d->activeUndoTransaction) {
|
|
// Transactions are now only created when there are actual changes.
|
|
// Empty transaction is now significant for marking external changes. It
|
|
// is used to match ID with transactions in external documents and
|
|
// trigger undo/redo there.
|
|
|
|
// return d->activeUndoTransaction->isEmpty();
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void Document::clearDocument()
|
|
{
|
|
d->activeObject = nullptr;
|
|
|
|
if (!d->objectArray.empty()) {
|
|
GetApplication().signalDeleteDocument(*this);
|
|
d->clearDocument();
|
|
GetApplication().signalNewDocument(*this, false);
|
|
}
|
|
|
|
Base::FlagToggler<> flag(globalIsRestoring, false);
|
|
|
|
setStatus(Document::PartialDoc, false);
|
|
|
|
d->clearRecomputeLog();
|
|
d->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(int id) const
|
|
{
|
|
if (id) {
|
|
auto it = mUndoMap.find(id);
|
|
if (it == mUndoMap.end()) {
|
|
return 0;
|
|
}
|
|
int i = 0;
|
|
if (d->activeUndoTransaction) {
|
|
++i;
|
|
if (d->activeUndoTransaction->getID() == id) {
|
|
return i;
|
|
}
|
|
}
|
|
auto rit = mUndoTransactions.rbegin();
|
|
for (; rit != mUndoTransactions.rend() && *rit != it->second; ++rit) {
|
|
++i;
|
|
}
|
|
assert(rit != mUndoTransactions.rend());
|
|
return i + 1;
|
|
}
|
|
if (d->activeUndoTransaction) {
|
|
return static_cast<int>(mUndoTransactions.size() + 1);
|
|
}
|
|
else {
|
|
return static_cast<int>(mUndoTransactions.size());
|
|
}
|
|
}
|
|
|
|
int Document::getAvailableRedos(int id) const
|
|
{
|
|
if (id) {
|
|
auto it = mRedoMap.find(id);
|
|
if (it == mRedoMap.end()) {
|
|
return 0;
|
|
}
|
|
int i = 0;
|
|
for (auto rit = mRedoTransactions.rbegin(); *rit != it->second; ++rit) {
|
|
++i;
|
|
}
|
|
assert(i < (int)mRedoTransactions.size());
|
|
return i + 1;
|
|
}
|
|
return static_cast<int>(mRedoTransactions.size());
|
|
}
|
|
|
|
void Document::setUndoMode(int iMode)
|
|
{
|
|
if (d->iUndoMode && !iMode) {
|
|
clearUndos();
|
|
}
|
|
|
|
d->iUndoMode = iMode;
|
|
}
|
|
|
|
int Document::getUndoMode() const
|
|
{
|
|
return d->iUndoMode;
|
|
}
|
|
|
|
unsigned int Document::getUndoMemSize() const
|
|
{
|
|
return d->UndoMemSize;
|
|
}
|
|
|
|
void Document::setUndoLimit(unsigned int UndoMemSize)
|
|
{
|
|
d->UndoMemSize = UndoMemSize;
|
|
}
|
|
|
|
void Document::setMaxUndoStackSize(unsigned int UndoMaxStackSize)
|
|
{
|
|
d->UndoMaxStackSize = UndoMaxStackSize;
|
|
}
|
|
|
|
unsigned int Document::getMaxUndoStackSize() const
|
|
{
|
|
return d->UndoMaxStackSize;
|
|
}
|
|
|
|
void Document::onBeforeChange(const Property* prop)
|
|
{
|
|
if (prop == &Label) {
|
|
oldLabel = Label.getValue();
|
|
}
|
|
signalBeforeChange(*this, *prop);
|
|
}
|
|
|
|
void Document::onChanged(const Property* prop)
|
|
{
|
|
signalChanged(*this, *prop);
|
|
|
|
// the Name property is a label for display purposes
|
|
if (prop == &Label) {
|
|
Base::FlagToggler<> flag(globalIsRelabeling);
|
|
App::GetApplication().signalRelabelDocument(*this);
|
|
}
|
|
else if (prop == &ShowHidden) {
|
|
App::GetApplication().signalShowHidden(*this);
|
|
}
|
|
else if (prop == &Uid) {
|
|
std::string new_dir =
|
|
getTransientDirectoryName(this->Uid.getValueStr(), this->FileName.getStrValue());
|
|
std::string old_dir = this->TransientDir.getStrValue();
|
|
Base::FileInfo TransDirNew(new_dir);
|
|
Base::FileInfo TransDirOld(old_dir);
|
|
// this directory should not exist
|
|
if (!TransDirNew.exists()) {
|
|
if (TransDirOld.exists()) {
|
|
if (!TransDirOld.renameFile(new_dir.c_str())) {
|
|
Base::Console().warning("Failed to rename '%s' to '%s'\n",
|
|
old_dir.c_str(),
|
|
new_dir.c_str());
|
|
}
|
|
else {
|
|
this->TransientDir.setValue(new_dir);
|
|
}
|
|
}
|
|
else {
|
|
if (!TransDirNew.createDirectories()) {
|
|
Base::Console().warning("Failed to create '%s'\n", new_dir.c_str());
|
|
}
|
|
else {
|
|
this->TransientDir.setValue(new_dir);
|
|
}
|
|
}
|
|
}
|
|
// when reloading an existing document the transient directory doesn't change
|
|
// so we must avoid to generate a new uuid
|
|
else if (TransDirNew.filePath() != TransDirOld.filePath()) {
|
|
// make sure that the uuid is unique
|
|
std::string uuid = this->Uid.getValueStr();
|
|
Base::Uuid id;
|
|
Base::Console().warning("Document with the UUID '%s' already exists, change to '%s'\n",
|
|
uuid.c_str(),
|
|
id.getValue().c_str());
|
|
// recursive call of onChanged()
|
|
this->Uid.setValue(id);
|
|
}
|
|
}
|
|
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<App::DocumentObject>()) {
|
|
signalBeforeChangeObject(*static_cast<const App::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(int iMode)
|
|
{
|
|
d->iTransactionMode = iMode;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
// constructor
|
|
//--------------------------------------------------------------------------
|
|
Document::Document(const char* documentName)
|
|
: myName(documentName)
|
|
{
|
|
// Remark: In a constructor we should never increment a Python object as we cannot be sure
|
|
// if the Python interpreter gets a reference of it. E.g. if we increment but Python don't
|
|
// get a reference then the object wouldn't get deleted in the destructor.
|
|
// So, we must increment only if the interpreter gets a reference.
|
|
// Remark: We force the document Python object to own the DocumentPy instance, thus we don't
|
|
// have to care about ref counting any more.
|
|
setAutoCreated(false);
|
|
d = new DocumentP;
|
|
Base::PyGILStateLocker lock;
|
|
d->DocumentPythonObject = Py::Object(new DocumentPy(this), true);
|
|
|
|
#ifdef FC_LOGUPDATECHAIN
|
|
Console().log("+App::Document: %p\n", this);
|
|
#endif
|
|
std::string CreationDateString = Base::Tools::currentDateTimeString();
|
|
std::string Author = App::GetApplication()
|
|
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
|
|
->GetASCII("prefAuthor", "");
|
|
std::string AuthorComp =
|
|
App::GetApplication()
|
|
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
|
|
->GetASCII("prefCompany", "");
|
|
ADD_PROPERTY_TYPE(Label, ("Unnamed"), 0, Prop_None, "The name of the document");
|
|
ADD_PROPERTY_TYPE(FileName,
|
|
(""),
|
|
0,
|
|
PropertyType(Prop_Transient | Prop_ReadOnly),
|
|
"The path to the file where the document is saved to");
|
|
ADD_PROPERTY_TYPE(CreatedBy, (Author.c_str()), 0, Prop_None, "The creator of the document");
|
|
ADD_PROPERTY_TYPE(CreationDate,
|
|
(CreationDateString.c_str()),
|
|
0,
|
|
Prop_ReadOnly,
|
|
"Date of creation");
|
|
ADD_PROPERTY_TYPE(LastModifiedBy, (""), 0, Prop_None, 0);
|
|
ADD_PROPERTY_TYPE(LastModifiedDate, ("Unknown"), 0, Prop_ReadOnly, "Date of last modification");
|
|
ADD_PROPERTY_TYPE(Company,
|
|
(AuthorComp.c_str()),
|
|
0,
|
|
Prop_None,
|
|
"Additional tag to save the name of the company");
|
|
ADD_PROPERTY_TYPE(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 =
|
|
App::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 {App::GetApplication().GetParameterGroupByPath(
|
|
"User parameter:BaseApp/Preferences/Document")};
|
|
auto index = static_cast<int>(paramGrp->GetInt("prefLicenseType", 0));
|
|
const char* name = "";
|
|
const char* url = "";
|
|
std::string licenseUrl = "";
|
|
if (index >= 0 && index < App::countOfLicenses) {
|
|
name = App::licenseItems.at(index).at(App::posnOfFullName);
|
|
url = App::licenseItems.at(index).at(App::posnOfUrl);
|
|
licenseUrl = (paramGrp->GetASCII("prefLicenseUrl", url));
|
|
}
|
|
ADD_PROPERTY_TYPE(License, (name), 0, Prop_None, "License string of the Item");
|
|
ADD_PROPERTY_TYPE(LicenseURL,
|
|
(licenseUrl.c_str()),
|
|
0,
|
|
Prop_None,
|
|
"URL to the license text/contract");
|
|
ADD_PROPERTY_TYPE(ShowHidden,
|
|
(false),
|
|
0,
|
|
PropertyType(Prop_None),
|
|
"Whether to show hidden object items in the tree view");
|
|
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()
|
|
{
|
|
#ifdef FC_LOGUPDATECHAIN
|
|
Console().log("-App::Document: %s %p\n", getName(), this);
|
|
#endif
|
|
|
|
try {
|
|
clearUndos();
|
|
}
|
|
catch (const boost::exception&) {
|
|
}
|
|
|
|
#ifdef FC_LOGUPDATECHAIN
|
|
Console().log("-Delete Features of %s \n", getName());
|
|
#endif
|
|
|
|
d->clearDocument();
|
|
|
|
// Remark: The API of Py::Object has been changed to set whether the wrapper owns the passed
|
|
// Python object or not. In the constructor we forced the wrapper to own the object so we need
|
|
// not to dec'ref the Python object any more.
|
|
// But we must still invalidate the Python object because it doesn't need to be
|
|
// destructed right now because the interpreter can own several references to it.
|
|
Base::PyGILStateLocker lock;
|
|
Base::PyObjectBase* doc = static_cast<Base::PyObjectBase*>(d->DocumentPythonObject.ptr());
|
|
// Call before decrementing the reference counter, otherwise a heap error can occur
|
|
doc->setInvalid();
|
|
|
|
// remove Transient directory
|
|
try {
|
|
Base::FileInfo TransDir(TransientDir.getValue());
|
|
TransDir.deleteDirectoryRecursive();
|
|
}
|
|
catch (const Base::Exception& e) {
|
|
std::cerr << "Removing transient directory failed: " << e.what() << std::endl;
|
|
}
|
|
delete d;
|
|
}
|
|
|
|
std::string Document::getTransientDirectoryName(const std::string& uuid,
|
|
const std::string& filename) const
|
|
{
|
|
// Create a directory name of the form: {ExeName}_Doc_{UUID}_{HASH}_{PID}
|
|
std::stringstream out;
|
|
QCryptographicHash hash(QCryptographicHash::Sha1);
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 3, 0)
|
|
hash.addData(filename.c_str(), filename.size());
|
|
#else
|
|
hash.addData(QByteArrayView(filename.c_str(), filename.size()));
|
|
#endif
|
|
out << App::Application::getUserCachePath() << App::Application::getExecutableName() << "_Doc_"
|
|
<< uuid << "_" << hash.result().toHex().left(6).constData() << "_"
|
|
<< App::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=")"
|
|
<< App::Application::Config()["BuildVersionMajor"] << "."
|
|
<< App::Application::Config()["BuildVersionMinor"] << "R"
|
|
<< App::Application::Config()["BuildRevision"] << "\" FileVersion=\""
|
|
<< writer.getFileVersion() << "\" StringHasher=\"1\">\n";
|
|
|
|
writer.incInd();
|
|
|
|
d->Hasher->setPersistenceFileName("StringHasher.Table");
|
|
for (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>" << endl;
|
|
}
|
|
|
|
void Document::Restore(Base::XMLReader& reader)
|
|
{
|
|
int i, Cnt;
|
|
d->hashers.clear();
|
|
d->touchedObjs.clear();
|
|
addStringHasher(d->Hasher);
|
|
setStatus(Document::PartialDoc, false);
|
|
|
|
reader.readElement("Document");
|
|
long scheme = reader.getAttribute<long>("SchemaVersion");
|
|
reader.DocumentSchema = scheme;
|
|
if (reader.hasAttribute("ProgramVersion")) {
|
|
reader.ProgramVersion = reader.getAttribute<const char*>("ProgramVersion");
|
|
}
|
|
else {
|
|
reader.ProgramVersion = "pre-0.14";
|
|
}
|
|
if (reader.hasAttribute("FileVersion")) {
|
|
reader.FileVersion = 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.
|
|
std::string FilePath = FileName.getValue();
|
|
std::string DocLabel = Label.getValue();
|
|
|
|
// read the Document Properties, when reading in Uid the transient directory gets renamed
|
|
// automatically
|
|
PropertyContainer::Restore(reader);
|
|
|
|
// We must restore the correct 'FileName' property again because the stored
|
|
// value could be invalid.
|
|
FileName.setValue(FilePath.c_str());
|
|
Label.setValue(DocLabel.c_str());
|
|
|
|
// SchemeVersion "2"
|
|
if (scheme == 2) {
|
|
// read the feature types
|
|
reader.readElement("Features");
|
|
Cnt = reader.getAttribute<long>("Count");
|
|
for (i = 0; i < Cnt; 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");
|
|
Cnt = reader.getAttribute<long>("Count");
|
|
for (i = 0; i < Cnt; 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, (int)d->hashers.size()));
|
|
if (ret.second) {
|
|
hasher->clearMarks();
|
|
}
|
|
return std::make_pair(ret.second, ret.first->second);
|
|
}
|
|
|
|
StringHasherRef Document::getStringHasher(int idx) const
|
|
{
|
|
StringHasherRef hasher;
|
|
if (idx < 0) {
|
|
if (UseHasher.getValue()) {
|
|
return d->Hasher;
|
|
}
|
|
return hasher;
|
|
}
|
|
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 App::DocumentObject*> objs;
|
|
};
|
|
|
|
static DocExportStatus _ExportStatus;
|
|
|
|
// Exception-safe exporting status setter
|
|
class DocumentExporting
|
|
{
|
|
public:
|
|
explicit DocumentExporting(const std::vector<App::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 App::DocumentObject* obj) const
|
|
{
|
|
if (_ExportStatus.status != Document::NotExporting
|
|
&& (!obj || _ExportStatus.objs.find(obj) != _ExportStatus.objs.end())) {
|
|
return _ExportStatus.status;
|
|
}
|
|
return Document::NotExporting;
|
|
}
|
|
|
|
void Document::exportObjects(const std::vector<App::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'?>" << endl;
|
|
writer.Stream() << R"(<Document SchemaVersion="4" ProgramVersion=")"
|
|
<< App::Application::Config()["BuildVersionMajor"] << "."
|
|
<< App::Application::Config()["BuildVersionMinor"] << "R"
|
|
<< App::Application::Config()["BuildRevision"] << R"(" FileVersion="1">)"
|
|
<< endl;
|
|
// Add this block to have the same layout as for normal documents
|
|
writer.Stream() << "<Properties Count=\"0\">" << endl;
|
|
writer.Stream() << "</Properties>" << endl;
|
|
|
|
// writing the object types
|
|
writeObjects(obj, writer);
|
|
writer.Stream() << "</Document>" << endl;
|
|
|
|
// Hook for others to add further data.
|
|
signalExportObjects(obj, writer);
|
|
|
|
// write additional files
|
|
writer.writeFiles();
|
|
d->hashers.clear();
|
|
}
|
|
|
|
#define FC_ATTR_DEPENDENCIES "Dependencies"
|
|
#define FC_ELEMENT_OBJECT_DEPS "ObjectDeps"
|
|
#define FC_ATTR_DEP_COUNT "Count"
|
|
#define FC_ATTR_DEP_OBJ_NAME "Name"
|
|
#define FC_ATTR_DEP_ALLOW_PARTIAL "AllowPartial"
|
|
#define FC_ELEMENT_OBJECT_DEP "Dep"
|
|
|
|
void Document::writeObjects(const std::vector<App::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)) {
|
|
writer.Stream() << "\" " FC_ATTR_DEPENDENCIES "=\"1";
|
|
}
|
|
writer.Stream() << "\">" << endl;
|
|
|
|
writer.incInd(); // indentation for 'Object type'
|
|
|
|
if (!isExporting(nullptr)) {
|
|
for (auto o : obj) {
|
|
const auto& outList =
|
|
o->getOutList(DocumentObject::OutListNoHidden | DocumentObject::OutListNoXLinked);
|
|
writer.Stream() << writer.ind()
|
|
<< "<" FC_ELEMENT_OBJECT_DEPS " " FC_ATTR_DEP_OBJ_NAME "=\""
|
|
<< o->getNameInDocument() << "\" " FC_ATTR_DEP_COUNT "=\""
|
|
<< outList.size();
|
|
if (outList.empty()) {
|
|
writer.Stream() << "\"/>" << endl;
|
|
continue;
|
|
}
|
|
int partial = o->canLoadPartial();
|
|
if (partial > 0) {
|
|
writer.Stream() << "\" " FC_ATTR_DEP_ALLOW_PARTIAL << "=\"" << partial;
|
|
}
|
|
writer.Stream() << "\">" << endl;
|
|
writer.incInd();
|
|
for (auto dep : outList) {
|
|
auto name = dep ? dep->getNameInDocument() : "";
|
|
writer.Stream() << writer.ind()
|
|
<< "<" FC_ELEMENT_OBJECT_DEP " " FC_ATTR_DEP_OBJ_NAME "=\""
|
|
<< (name ? name : "") << "\"/>" << endl;
|
|
}
|
|
writer.decInd();
|
|
writer.Stream() << writer.ind() << "</" FC_ELEMENT_OBJECT_DEPS ">" << endl;
|
|
}
|
|
}
|
|
|
|
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\" ";
|
|
auto desc = getErrorDescription(*it);
|
|
if (desc) {
|
|
writer.Stream() << "Error=\"" << Property::encodeAttribute(desc) << "\" ";
|
|
}
|
|
}
|
|
writer.Stream() << "/>" << endl;
|
|
}
|
|
|
|
writer.decInd(); // indentation for 'Object type'
|
|
writer.Stream() << writer.ind() << "</Objects>" << endl;
|
|
|
|
// writing the features itself
|
|
writer.Stream() << writer.ind() << "<ObjectData Count=\"" << obj.size() << "\">" << endl;
|
|
|
|
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() << ">" << endl;
|
|
(*it)->Save(writer);
|
|
writer.Stream() << writer.ind() << "</Object>" << endl;
|
|
}
|
|
|
|
writer.decInd(); // indentation for 'Object name'
|
|
writer.Stream() << writer.ind() << "</ObjectData>" << endl;
|
|
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)
|
|
{
|
|
auto it = deps.find(name);
|
|
if (it == deps.end()) {
|
|
objs.emplace(name, true);
|
|
return;
|
|
}
|
|
if (it->second.canLoadPartial) {
|
|
if (it->second.canLoadPartial == 1) {
|
|
// canLoadPartial==1 means all its children will be created but not
|
|
// restored, i.e. exists as if newly created object, and therefore no
|
|
// need to load dependency of the children
|
|
for (auto& dep : it->second.deps) {
|
|
objs.emplace(dep, false);
|
|
}
|
|
objs.emplace(name, true);
|
|
}
|
|
else {
|
|
objs.emplace(name, false);
|
|
}
|
|
return;
|
|
}
|
|
objs[name] = true;
|
|
// If cannot load partial, then recurse to load all children dependency
|
|
for (auto& dep : it->second.deps) {
|
|
auto it = objs.find(dep);
|
|
if (it != objs.end() && it->second) {
|
|
continue;
|
|
}
|
|
_loadDeps(dep, objs, deps);
|
|
}
|
|
}
|
|
|
|
std::vector<App::DocumentObject*> Document::readObjects(Base::XMLReader& reader)
|
|
{
|
|
d->touchedObjs.clear();
|
|
bool keepDigits = testStatus(Document::KeepTrailingDigits);
|
|
setStatus(Document::KeepTrailingDigits, !reader.doNameMapping());
|
|
std::vector<App::DocumentObject*> objs;
|
|
|
|
|
|
// read the object types
|
|
reader.readElement("Objects");
|
|
int Cnt = reader.getAttribute<long>("Count");
|
|
|
|
if (!reader.hasAttribute(FC_ATTR_DEPENDENCIES)) {
|
|
d->partialLoadObjects.clear();
|
|
}
|
|
else if (!d->partialLoadObjects.empty()) {
|
|
std::unordered_map<std::string, DepInfo> deps;
|
|
for (int i = 0; i < Cnt; i++) {
|
|
reader.readElement(FC_ELEMENT_OBJECT_DEPS);
|
|
int dcount = reader.getAttribute<long>(FC_ATTR_DEP_COUNT);
|
|
if (!dcount) {
|
|
continue;
|
|
}
|
|
auto& info = deps[reader.getAttribute<const char*>(FC_ATTR_DEP_OBJ_NAME)];
|
|
if (reader.hasAttribute(FC_ATTR_DEP_ALLOW_PARTIAL)) {
|
|
info.canLoadPartial = reader.getAttribute<long>(FC_ATTR_DEP_ALLOW_PARTIAL);
|
|
}
|
|
for (int j = 0; j < dcount; ++j) {
|
|
reader.readElement(FC_ELEMENT_OBJECT_DEP);
|
|
const char* name = reader.getAttribute<const char*>(FC_ATTR_DEP_OBJ_NAME);
|
|
if (!Base::Tools::isNullOrEmpty(name)) {
|
|
info.deps.insert(name);
|
|
}
|
|
}
|
|
reader.readEndElement(FC_ELEMENT_OBJECT_DEPS);
|
|
}
|
|
std::vector<std::string> objs;
|
|
objs.reserve(d->partialLoadObjects.size());
|
|
for (auto& v : d->partialLoadObjects) {
|
|
objs.emplace_back(v.first.c_str());
|
|
}
|
|
for (auto& name : objs) {
|
|
_loadDeps(name, d->partialLoadObjects, deps);
|
|
}
|
|
if (Cnt > (int)d->partialLoadObjects.size()) {
|
|
setStatus(Document::PartialDoc, true);
|
|
}
|
|
else {
|
|
for (auto& v : d->partialLoadObjects) {
|
|
if (!v.second) {
|
|
setStatus(Document::PartialDoc, true);
|
|
break;
|
|
}
|
|
}
|
|
if (!testStatus(Document::PartialDoc)) {
|
|
d->partialLoadObjects.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
long lastId = 0;
|
|
for (int i = 0; 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;
|
|
if (pos != std::string::npos) {
|
|
_obj_name = name.substr(0, pos);
|
|
obj_name = _obj_name.c_str();
|
|
}
|
|
else {
|
|
obj_name = name.c_str();
|
|
}
|
|
|
|
try {
|
|
// Use name from XML as is and do NOT remove trailing digits because
|
|
// otherwise we may cause a dependency to itself
|
|
// Example: Object 'Cut001' references object 'Cut' and removing the
|
|
// digits we make an object 'Cut' referencing itself.
|
|
App::DocumentObject* obj =
|
|
addObject(type.c_str(), obj_name, /*isNew=*/false, viewType.c_str(), partial);
|
|
if (obj) {
|
|
if (lastId < obj->_Id) {
|
|
lastId = obj->_Id;
|
|
}
|
|
objs.push_back(obj);
|
|
// use this name for the later access because an object with
|
|
// the given name may already exist
|
|
reader.addName(name.c_str(), obj->getNameInDocument());
|
|
|
|
// restore touch/error status flags
|
|
if (reader.hasAttribute("Touched")) {
|
|
if (reader.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 = reader.getAttribute<long>("Count");
|
|
for (int i = 0; i < Cnt; i++) {
|
|
reader.readElement("Object");
|
|
std::string name = reader.getName(reader.getAttribute<const char*>("name"));
|
|
DocumentObject* pObj = getObject(name.c_str());
|
|
if (pObj
|
|
&& !pObj->testStatus(
|
|
App::PartialObject)) { // check if this feature has been registered
|
|
pObj->setStatus(ObjectStatus::Restore, true);
|
|
try {
|
|
FC_TRACE("restoring " << pObj->getFullName());
|
|
pObj->Restore(reader);
|
|
}
|
|
// Try to continue only for certain exception types if not handled
|
|
// by the feature type. For all other exception types abort the process.
|
|
catch (const Base::UnicodeError& e) {
|
|
e.reportException();
|
|
}
|
|
catch (const Base::ValueError& e) {
|
|
e.reportException();
|
|
}
|
|
catch (const Base::IndexError& e) {
|
|
e.reportException();
|
|
}
|
|
catch (const Base::RuntimeError& e) {
|
|
e.reportException();
|
|
}
|
|
catch (const Base::XMLAttributeError& e) {
|
|
e.reportException();
|
|
}
|
|
|
|
pObj->setStatus(ObjectStatus::Restore, false);
|
|
|
|
if (reader.testStatus(Base::XMLReader::ReaderStatus::PartialRestoreInDocumentObject)) {
|
|
Base::Console().error("Object \"%s\" was subject to a partial restore. As a result "
|
|
"geometry may have changed or be incomplete.\n",
|
|
name.c_str());
|
|
reader.clearPartialRestoreDocumentObject();
|
|
}
|
|
}
|
|
reader.readEndElement("Object");
|
|
}
|
|
reader.readEndElement("ObjectData");
|
|
|
|
return objs;
|
|
}
|
|
|
|
void Document::addRecomputeObject(DocumentObject* obj)
|
|
{
|
|
if (testStatus(Status::Restoring) && obj) {
|
|
setStatus(Status::RecomputeOnRestore, true);
|
|
d->touchedObjs.insert(obj);
|
|
obj->touch();
|
|
}
|
|
}
|
|
|
|
std::vector<App::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");
|
|
long scheme = reader.getAttribute<long>("SchemaVersion");
|
|
reader.DocumentSchema = scheme;
|
|
if (reader.hasAttribute("ProgramVersion")) {
|
|
reader.ProgramVersion = reader.getAttribute<const char*>("ProgramVersion");
|
|
}
|
|
else {
|
|
reader.ProgramVersion = "pre-0.14";
|
|
}
|
|
if (reader.hasAttribute("FileVersion")) {
|
|
reader.FileVersion = reader.getAttribute<unsigned long>("FileVersion");
|
|
}
|
|
else {
|
|
reader.FileVersion = 0;
|
|
}
|
|
|
|
std::vector<App::DocumentObject*> objs = readObjects(reader);
|
|
for (auto o : objs) {
|
|
if (o && o->isAttachedToDocument()) {
|
|
o->setStatus(App::ObjImporting, true);
|
|
FC_LOG("importing " << o->getFullName());
|
|
if (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 (auto o : objs) {
|
|
if (o && o->isAttachedToDocument()) {
|
|
o->setStatus(App::ObjImporting, false);
|
|
}
|
|
}
|
|
|
|
d->hashers.clear();
|
|
return objs;
|
|
}
|
|
|
|
unsigned int Document::getMemSize() const
|
|
{
|
|
unsigned int size = 0;
|
|
|
|
// size of the DocObjects in the document
|
|
std::vector<DocumentObject*>::const_iterator it;
|
|
for (it = d->objectArray.begin(); it != d->objectArray.end(); ++it) {
|
|
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 (App::GetApplication()
|
|
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
|
|
->GetBool("CheckExtension", true)) {
|
|
const char* ext = strrchr(file, '.');
|
|
if (!ext || !boost::iequals(ext + 1, "fcstd")) {
|
|
if (ext && ext[1] == 0) {
|
|
fn += "FCStd";
|
|
}
|
|
else {
|
|
fn += ".FCStd";
|
|
}
|
|
}
|
|
}
|
|
return fn;
|
|
}
|
|
|
|
bool Document::saveAs(const char* _file)
|
|
{
|
|
std::string file = checkFileName(_file);
|
|
Base::FileInfo fi(file.c_str());
|
|
if (this->FileName.getStrValue() != file) {
|
|
this->FileName.setValue(file);
|
|
this->Label.setValue(fi.fileNamePure());
|
|
this->Uid.touch(); // this forces a rename of the transient directory
|
|
}
|
|
|
|
return save();
|
|
}
|
|
|
|
bool Document::saveCopy(const char* _file) const
|
|
{
|
|
std::string file = checkFileName(_file);
|
|
if (this->FileName.getStrValue() != file) {
|
|
bool result = saveToFile(file.c_str());
|
|
return result;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Save the document under the name it has been opened
|
|
bool Document::save()
|
|
{
|
|
if (testStatus(Document::PartialDoc)) {
|
|
FC_ERR("Partial loaded document '" << Label.getValue() << "' cannot be saved");
|
|
// TODO We don't make this a fatal error and return 'true' to make it possible to
|
|
// save other documents that depends on this partial opened document. We need better
|
|
// handling to avoid touching partial documents.
|
|
return true;
|
|
}
|
|
|
|
if (*(FileName.getValue()) != '\0') {
|
|
// Save the name of the tip object in order to handle in Restore()
|
|
if (Tip.getValue()) {
|
|
TipName.setValue(Tip.getValue()->getNameInDocument());
|
|
}
|
|
|
|
std::string LastModifiedDateString = Base::Tools::currentDateTimeString();
|
|
LastModifiedDate.setValue(LastModifiedDateString.c_str());
|
|
// set author if needed
|
|
bool saveAuthor =
|
|
App::GetApplication()
|
|
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
|
|
->GetBool("prefSetAuthorOnSave", false);
|
|
if (saveAuthor) {
|
|
std::string Author =
|
|
App::GetApplication()
|
|
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
|
|
->GetASCII("prefAuthor", "");
|
|
LastModifiedBy.setValue(Author.c_str());
|
|
}
|
|
|
|
return saveToFile(FileName.getValue());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
namespace App
|
|
{
|
|
// Helper class to handle different backup policies
|
|
class BackupPolicy
|
|
{
|
|
public:
|
|
enum Policy
|
|
{
|
|
Standard,
|
|
TimeStamp
|
|
};
|
|
BackupPolicy()
|
|
{
|
|
policy = Standard;
|
|
numberOfFiles = 1;
|
|
useFCBakExtension = true;
|
|
saveBackupDateFormat = "%Y%m%d-%H%M%S";
|
|
}
|
|
~BackupPolicy() = default;
|
|
void setPolicy(Policy p)
|
|
{
|
|
policy = p;
|
|
}
|
|
void setNumberOfFiles(int count)
|
|
{
|
|
numberOfFiles = count;
|
|
}
|
|
void useBackupExtension(bool on)
|
|
{
|
|
useFCBakExtension = on;
|
|
}
|
|
void setDateFormat(const std::string& fmt)
|
|
{
|
|
saveBackupDateFormat = fmt;
|
|
}
|
|
void apply(const std::string& sourcename, const std::string& targetname)
|
|
{
|
|
switch (policy) {
|
|
case Standard:
|
|
applyStandard(sourcename, targetname);
|
|
break;
|
|
case TimeStamp:
|
|
applyTimeStamp(sourcename, targetname);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private:
|
|
void applyStandard(const std::string& sourcename, const std::string& targetname)
|
|
{
|
|
// if saving the project data succeeded rename to the actual file name
|
|
Base::FileInfo fi(targetname);
|
|
if (fi.exists()) {
|
|
if (numberOfFiles > 0) {
|
|
int nSuff = 0;
|
|
std::string fn = fi.fileName();
|
|
Base::FileInfo di(fi.dirPath());
|
|
std::vector<Base::FileInfo> backup;
|
|
std::vector<Base::FileInfo> files = di.getDirectoryContent();
|
|
for (const Base::FileInfo& it : files) {
|
|
std::string file = it.fileName();
|
|
if (file.substr(0, fn.length()) == fn) {
|
|
// starts with the same file name
|
|
std::string suf(file.substr(fn.length()));
|
|
if (!suf.empty()) {
|
|
std::string::size_type nPos = suf.find_first_not_of("0123456789");
|
|
if (nPos == std::string::npos) {
|
|
// store all backup files
|
|
backup.push_back(it);
|
|
nSuff = std::max<int>(nSuff, std::atol(suf.c_str()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!backup.empty() && (int)backup.size() >= numberOfFiles) {
|
|
// delete the oldest backup file we found
|
|
Base::FileInfo del = backup.front();
|
|
for (const Base::FileInfo& it : backup) {
|
|
if (it.lastModified() < del.lastModified()) {
|
|
del = it;
|
|
}
|
|
}
|
|
|
|
del.deleteFile();
|
|
fn = del.filePath();
|
|
}
|
|
else {
|
|
// create a new backup file
|
|
std::stringstream str;
|
|
str << fi.filePath() << (nSuff + 1);
|
|
fn = str.str();
|
|
}
|
|
|
|
if (!fi.renameFile(fn.c_str())) {
|
|
Base::Console().warning("Cannot rename project file to backup file\n");
|
|
}
|
|
}
|
|
else {
|
|
fi.deleteFile();
|
|
}
|
|
}
|
|
|
|
Base::FileInfo tmp(sourcename);
|
|
if (!tmp.renameFile(targetname.c_str())) {
|
|
throw Base::FileException("Cannot rename tmp save file to project file",
|
|
Base::FileInfo(targetname));
|
|
}
|
|
}
|
|
void applyTimeStamp(const std::string& sourcename, const std::string& targetname)
|
|
{
|
|
Base::FileInfo fi(targetname);
|
|
|
|
std::string fn = sourcename;
|
|
std::string ext = fi.extension();
|
|
std::string bn; // full path with no extension but with "."
|
|
std::string pbn; // base name of the project + "."
|
|
if (!ext.empty()) {
|
|
bn = fi.filePath().substr(0, fi.filePath().length() - ext.length());
|
|
pbn = fi.fileName().substr(0, fi.fileName().length() - ext.length());
|
|
}
|
|
else {
|
|
bn = fi.filePath() + ".";
|
|
pbn = fi.fileName() + ".";
|
|
}
|
|
|
|
bool backupManagementError = false; // Note error and report at the end
|
|
if (fi.exists()) {
|
|
if (numberOfFiles > 0) {
|
|
// replace . by - in format to avoid . between base name and extension
|
|
boost::replace_all(saveBackupDateFormat, ".", "-");
|
|
{
|
|
// Remove all extra backups
|
|
std::string fn = fi.fileName();
|
|
Base::FileInfo di(fi.dirPath());
|
|
std::vector<Base::FileInfo> backup;
|
|
std::vector<Base::FileInfo> files = di.getDirectoryContent();
|
|
for (const Base::FileInfo& it : files) {
|
|
if (it.isFile()) {
|
|
std::string file = it.fileName();
|
|
std::string fext = it.extension();
|
|
std::string fextUp = fext;
|
|
std::transform(fextUp.begin(),
|
|
fextUp.end(),
|
|
fextUp.begin(),
|
|
(int (*)(int))toupper);
|
|
// re-enforcing identification of the backup file
|
|
|
|
|
|
// old case : the name starts with the full name of the project and
|
|
// follows with numbers
|
|
if ((startsWith(file, fn) && (file.length() > fn.length())
|
|
&& checkDigits(file.substr(fn.length())))
|
|
||
|
|
// .FCBak case : The bame starts with the base name of the project +
|
|
// "."
|
|
// + complement with no "." + ".FCBak"
|
|
((fextUp == "FCBAK") && startsWith(file, pbn)
|
|
&& (checkValidComplement(file, pbn, fext)))) {
|
|
backup.push_back(it);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!backup.empty() && (int)backup.size() >= numberOfFiles) {
|
|
std::sort(backup.begin(), backup.end(), fileComparisonByDate);
|
|
// delete the oldest backup file we found
|
|
// Base::FileInfo del = backup.front();
|
|
int nb = 0;
|
|
for (Base::FileInfo& it : backup) {
|
|
nb++;
|
|
if (nb >= numberOfFiles) {
|
|
try {
|
|
if (!it.deleteFile()) {
|
|
backupManagementError = true;
|
|
Base::Console().warning("Cannot remove backup file : %s\n",
|
|
it.fileName().c_str());
|
|
}
|
|
}
|
|
catch (...) {
|
|
backupManagementError = true;
|
|
Base::Console().warning("Cannot remove backup file : %s\n",
|
|
it.fileName().c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} // end remove backup
|
|
|
|
// create a new backup file
|
|
{
|
|
int ext = 1;
|
|
if (useFCBakExtension) {
|
|
std::stringstream str;
|
|
Base::TimeInfo ti = fi.lastModified();
|
|
time_t s = ti.getTime_t();
|
|
struct tm* timeinfo = localtime(&s);
|
|
char buffer[100];
|
|
|
|
strftime(buffer, sizeof(buffer), saveBackupDateFormat.c_str(), timeinfo);
|
|
str << bn << buffer;
|
|
|
|
fn = str.str();
|
|
bool done = false;
|
|
|
|
if ((fn.empty()) || (fn[fn.length() - 1] == ' ')
|
|
|| (fn[fn.length() - 1] == '-')) {
|
|
if (fn[fn.length() - 1] == ' ') {
|
|
fn = fn.substr(0, fn.length() - 1);
|
|
}
|
|
}
|
|
else {
|
|
if (!renameFileNoErase(fi, fn + ".FCBak")) {
|
|
fn = fn + "-";
|
|
}
|
|
else {
|
|
done = true;
|
|
}
|
|
}
|
|
|
|
if (!done) {
|
|
while (ext < numberOfFiles + 10) {
|
|
if (renameFileNoErase(fi, fn + std::to_string(ext) + ".FCBak")) {
|
|
break;
|
|
}
|
|
ext++;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// changed but simpler and solves also the delay sometimes introduced by
|
|
// google drive
|
|
while (ext < numberOfFiles + 10) {
|
|
// linux just replace the file if exists, and then the existence is to
|
|
// be tested before rename
|
|
if (renameFileNoErase(fi, fi.filePath() + std::to_string(ext))) {
|
|
break;
|
|
}
|
|
ext++;
|
|
}
|
|
}
|
|
|
|
if (ext >= numberOfFiles + 10) {
|
|
Base::Console().error(
|
|
"File not saved: Cannot rename project file to backup file\n");
|
|
// throw Base::FileException("File not saved: Cannot rename project file to
|
|
// backup file", fi);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
try {
|
|
fi.deleteFile();
|
|
}
|
|
catch (...) {
|
|
Base::Console().warning("Cannot remove backup file: %s\n",
|
|
fi.fileName().c_str());
|
|
backupManagementError = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
Base::FileInfo tmp(sourcename);
|
|
if (!tmp.renameFile(targetname.c_str())) {
|
|
throw Base::FileException(
|
|
"Save interrupted: Cannot rename temporary file to project file",
|
|
tmp);
|
|
}
|
|
|
|
if (backupManagementError) {
|
|
throw Base::FileException(
|
|
"Warning: Save complete, but error while managing backup history.",
|
|
fi);
|
|
}
|
|
}
|
|
static bool fileComparisonByDate(const Base::FileInfo& i, const Base::FileInfo& j)
|
|
{
|
|
return (i.lastModified() > j.lastModified());
|
|
}
|
|
bool startsWith(const std::string& st1, const std::string& st2) const
|
|
{
|
|
return st1.substr(0, st2.length()) == st2;
|
|
}
|
|
bool checkValidString(const std::string& cmpl, const boost::regex& e) const
|
|
{
|
|
boost::smatch what;
|
|
bool res = boost::regex_search(cmpl, what, e);
|
|
return res;
|
|
}
|
|
bool checkValidComplement(const std::string& file,
|
|
const std::string& pbn,
|
|
const std::string& ext) const
|
|
{
|
|
std::string cmpl =
|
|
file.substr(pbn.length(), file.length() - pbn.length() - ext.length() - 1);
|
|
boost::regex e(R"(^[^.]*$)");
|
|
return checkValidString(cmpl, e);
|
|
}
|
|
bool checkDigits(const std::string& cmpl) const
|
|
{
|
|
boost::regex e(R"(^[0-9]*$)");
|
|
return checkValidString(cmpl, e);
|
|
}
|
|
bool renameFileNoErase(Base::FileInfo fi, const std::string& newName)
|
|
{
|
|
// linux just replaces the file if it exists, so the existence is to be tested before rename
|
|
Base::FileInfo nf(newName);
|
|
if (!nf.exists()) {
|
|
return fi.renameFile(newName.c_str());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private:
|
|
Policy policy;
|
|
int numberOfFiles;
|
|
bool useFCBakExtension;
|
|
std::string saveBackupDateFormat;
|
|
};
|
|
} // namespace App
|
|
|
|
bool Document::saveToFile(const char* filename) const
|
|
{
|
|
signalStartSave(*this, filename);
|
|
|
|
auto hGrp = App::GetApplication().GetParameterGroupByPath(
|
|
"User parameter:BaseApp/Preferences/Document");
|
|
int compression = hGrp->GetInt("CompressionLevel", 7);
|
|
compression = Base::clamp<int>(compression, Z_NO_COMPRESSION, Z_BEST_COMPRESSION);
|
|
|
|
bool policy = App::GetApplication()
|
|
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
|
|
->GetBool("BackupPolicy", true);
|
|
|
|
auto canonical_path = [](const char* filename) {
|
|
try {
|
|
#ifdef FC_OS_WIN32
|
|
QString utf8Name = QString::fromUtf8(filename);
|
|
auto realpath = fs::weakly_canonical(fs::absolute(fs::path(utf8Name.toStdWString())));
|
|
std::string nativePath = QString::fromStdWString(realpath.native()).toStdString();
|
|
#else
|
|
auto realpath = fs::weakly_canonical(fs::absolute(fs::path(filename)));
|
|
std::string nativePath = realpath.native();
|
|
#endif
|
|
// In case some folders in the path do not exist
|
|
auto parentPath = realpath.parent_path();
|
|
fs::create_directories(parentPath);
|
|
|
|
return nativePath;
|
|
}
|
|
catch (const std::exception&) {
|
|
#ifdef FC_OS_WIN32
|
|
QString utf8Name = QString::fromUtf8(filename);
|
|
auto parentPath = fs::absolute(fs::path(utf8Name.toStdWString())).parent_path();
|
|
#else
|
|
auto parentPath = fs::absolute(fs::path(filename)).parent_path();
|
|
#endif
|
|
fs::create_directories(parentPath);
|
|
|
|
return std::string(filename);
|
|
}
|
|
};
|
|
|
|
// realpath is canonical filename i.e. without symlink
|
|
std::string nativePath = canonical_path(filename);
|
|
|
|
// make a tmp. file where to save the project data first and then rename to
|
|
// the actual file name. This may be useful if overwriting an existing file
|
|
// fails so that the data of the work up to now isn't lost.
|
|
std::string uuid = Base::Uuid::createUuid();
|
|
std::string fn = nativePath;
|
|
if (policy) {
|
|
fn += ".";
|
|
fn += uuid;
|
|
}
|
|
Base::FileInfo tmp(fn);
|
|
|
|
|
|
// open extra scope to close ZipWriter properly
|
|
{
|
|
Base::ofstream file(tmp, std::ios::out | std::ios::binary);
|
|
|
|
Base::ZipWriter writer(file);
|
|
if (!file.is_open()) {
|
|
throw Base::FileException("Failed to open file", tmp);
|
|
}
|
|
|
|
writer.setComment("FreeCAD Document");
|
|
writer.setLevel(compression);
|
|
writer.putNextEntry("Document.xml");
|
|
|
|
if (hGrp->GetBool("SaveBinaryBrep", false)) {
|
|
writer.setMode("BinaryBrep");
|
|
}
|
|
|
|
writer.Stream() << "<?xml version='1.0' encoding='utf-8'?>" << endl
|
|
<< "<!--" << endl
|
|
<< " FreeCAD Document, see https://www.freecad.org for more information..."
|
|
<< endl
|
|
<< "-->" << endl;
|
|
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 = App::GetApplication()
|
|
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
|
|
->GetInt("CountBackupFiles", 1);
|
|
bool backup = App::GetApplication()
|
|
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
|
|
->GetBool("CreateBackupFiles", true);
|
|
if (!backup) {
|
|
count_bak = -1;
|
|
}
|
|
bool useFCBakExtension =
|
|
App::GetApplication()
|
|
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
|
|
->GetBool("UseFCBakExtension", true);
|
|
std::string saveBackupDateFormat =
|
|
App::GetApplication()
|
|
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
|
|
->GetASCII("SaveBackupDateFormat", "%Y%m%d-%H%M%S");
|
|
|
|
BackupPolicy policy;
|
|
if (useFCBakExtension) {
|
|
policy.setPolicy(BackupPolicy::TimeStamp);
|
|
policy.useBackupExtension(useFCBakExtension);
|
|
policy.setDateFormat(saveBackupDateFormat);
|
|
}
|
|
else {
|
|
policy.setPolicy(BackupPolicy::Standard);
|
|
}
|
|
policy.setNumberOfFiles(count_bak);
|
|
policy.apply(fn, nativePath);
|
|
}
|
|
|
|
signalFinishSave(*this, filename);
|
|
|
|
return true;
|
|
}
|
|
|
|
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(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<App::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 (...) {
|
|
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;
|
|
std::string errMsg;
|
|
if (link && (res = link->checkRestore(&errMsg))) {
|
|
d->touchedObjs.insert(obj);
|
|
if (res == 1 || checkPartial) {
|
|
FC_WARN(obj->getFullName() << '.' << prop->getName() << ": " << errMsg);
|
|
setStatus(Document::LinkStampChanged, true);
|
|
if (checkPartial) {
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
FC_ERR(obj->getFullName() << '.' << prop->getName() << ": " << errMsg);
|
|
d->addRecomputeLog(errMsg, obj);
|
|
setStatus(Document::PartialRestore, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (checkPartial && !d->touchedObjs.empty()) {
|
|
// partial document touched, signal full reload
|
|
return false;
|
|
}
|
|
else if (!d->touchedObjs.contains(obj)) {
|
|
obj->purgeTouched();
|
|
}
|
|
|
|
signalFinishRestoreObject(*obj);
|
|
}
|
|
|
|
d->touchedObjs.clear();
|
|
return true;
|
|
}
|
|
|
|
bool Document::isSaved() const
|
|
{
|
|
std::string name = FileName.getValue();
|
|
return !name.empty();
|
|
}
|
|
|
|
/** Label is the visible name of a document shown e.g. in the windows title
|
|
* or in the tree view. The label almost (but not always e.g. if you manually change it)
|
|
* matches with the file name where the document is stored to.
|
|
* In contrast to Label the method getName() returns the internal name of the document that only
|
|
* matches with Label when loading or creating a document because then both are set to the same
|
|
* value. Since the internal name cannot be changed during runtime it must differ from the Label
|
|
* after saving the document the first time or saving it under a new file name.
|
|
* @ note More than one document can have the same label name.
|
|
* @ note The internal is always guaranteed to be unique because @ref Application::newDocument()
|
|
* checks for a document with the same name and makes it unique if needed. Hence you cannot rely on
|
|
* that the internal name matches with the name you passed to Application::newDoument(). You should
|
|
* use the method getName() instead.
|
|
*/
|
|
const char* Document::getName() const
|
|
{
|
|
// return GetApplication().getDocumentName(this);
|
|
return myName.c_str();
|
|
}
|
|
|
|
std::string Document::getFullName() const
|
|
{
|
|
return myName;
|
|
}
|
|
|
|
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()
|
|
{
|
|
for (auto It : d->objectArray) {
|
|
It->purgeTouched();
|
|
}
|
|
}
|
|
|
|
bool Document::isTouched() const
|
|
{
|
|
for (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(bool c)
|
|
{
|
|
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,
|
|
int options,
|
|
int maxCount,
|
|
const std::vector<DocumentObject*>& objs) const
|
|
{
|
|
std::map<const App::DocumentObject*, std::vector<App::DocumentObject*>> linkMap;
|
|
|
|
for (auto o : !objs.empty() ? objs : d->objectArray) {
|
|
if (o == obj) {
|
|
continue;
|
|
}
|
|
auto linked = o;
|
|
if (options & GetLinkArrayElement) {
|
|
linked = o->getLinkedObject(false);
|
|
}
|
|
else {
|
|
auto ext = o->getExtensionByType<LinkBaseExtension>(true);
|
|
if (ext) {
|
|
linked = ext->getTrueLinkedObject(false, nullptr, 0, true);
|
|
}
|
|
else {
|
|
linked = o->getLinkedObject(false);
|
|
}
|
|
}
|
|
|
|
if (linked && linked != o) {
|
|
if (options & GetLinkRecursive) {
|
|
linkMap[linked].push_back(o);
|
|
}
|
|
else if (linked == obj || !obj) {
|
|
if ((options & GetLinkExternal) && linked->getDocument() == o->getDocument()) {
|
|
continue;
|
|
}
|
|
else if (options & GetLinkedObject) {
|
|
links.insert(linked);
|
|
}
|
|
else {
|
|
links.insert(o);
|
|
}
|
|
if (maxCount && maxCount <= (int)links.size()) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!(options & GetLinkRecursive)) {
|
|
return;
|
|
}
|
|
|
|
std::vector<const DocumentObject*> current(1, obj);
|
|
for (int depth = 0; !current.empty(); ++depth) {
|
|
if (!GetApplication().checkLinkDepth(depth, MessageOption::Error)) {
|
|
break;
|
|
}
|
|
std::vector<const DocumentObject*> next;
|
|
for (const App::DocumentObject* o : current) {
|
|
auto iter = linkMap.find(o);
|
|
if (iter == linkMap.end()) {
|
|
continue;
|
|
}
|
|
for (App::DocumentObject* link : iter->second) {
|
|
if (links.insert(link).second) {
|
|
if (maxCount && maxCount <= (int)links.size()) {
|
|
return;
|
|
}
|
|
next.push_back(link);
|
|
}
|
|
}
|
|
}
|
|
current = std::move(next);
|
|
}
|
|
return;
|
|
}
|
|
|
|
bool Document::hasLinksTo(const DocumentObject* obj) const
|
|
{
|
|
std::set<DocumentObject*> links;
|
|
getLinksTo(links, obj, 0, 1);
|
|
return !links.empty();
|
|
}
|
|
|
|
std::vector<App::DocumentObject*> Document::getInList(const DocumentObject* me) const
|
|
{
|
|
// result list
|
|
std::vector<App::DocumentObject*> result;
|
|
// go through all objects
|
|
for (const auto& It : d->objectMap) {
|
|
// get the outList and search if me is in that list
|
|
std::vector<DocumentObject*> OutList = It.second->getOutList();
|
|
for (auto obj : OutList) {
|
|
if (obj && obj == me) {
|
|
// add the parent object
|
|
result.push_back(It.second);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// This function unifies the old _rebuildDependencyList() and
|
|
// getDependencyList(). The algorithm basically obtains the object dependency
|
|
// by recrusivly visiting the OutList of each object in the given object array.
|
|
// It makes sure to call getOutList() of each object once and only once, which
|
|
// makes it much more efficient than calling getRecursiveOutList() on each
|
|
// individual object.
|
|
//
|
|
// The problem with the original algorithm is that, it assumes the objects
|
|
// inside any OutList are all within the given object array, so it does not
|
|
// recursively call getOutList() on those dependent objects inside. This
|
|
// assumption is broken by the introduction of PropertyXLink which can link to
|
|
// external object.
|
|
//
|
|
static void _buildDependencyList(const std::vector<App::DocumentObject*>& objectArray,
|
|
int options,
|
|
std::vector<App::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();
|
|
}
|
|
|
|
int op = (options & Document::DepNoXLinked) ? DocumentObject::OutListNoXLinked : 0;
|
|
for (auto obj : objectArray) {
|
|
objs.push_back(obj);
|
|
while (!objs.empty()) {
|
|
auto obj = objs.front();
|
|
objs.pop_front();
|
|
if (!obj || !obj->isAttachedToDocument()) {
|
|
continue;
|
|
}
|
|
|
|
auto it = outLists.find(obj);
|
|
if (it != outLists.end()) {
|
|
continue;
|
|
}
|
|
|
|
if (touchCheck) {
|
|
if (obj->isTouched() || obj->mustExecute()) {
|
|
// early termination on touch check
|
|
*touchCheck = true;
|
|
return;
|
|
}
|
|
}
|
|
if (depObjs) {
|
|
depObjs->push_back(obj);
|
|
}
|
|
if (objectMap && depList) {
|
|
(*objectMap)[obj] = add_vertex(*depList);
|
|
}
|
|
|
|
auto& outList = outLists[obj];
|
|
outList = obj->getOutList(op);
|
|
objs.insert(objs.end(), outList.begin(), outList.end());
|
|
}
|
|
}
|
|
|
|
if (objectMap && depList) {
|
|
for (const auto& v : outLists) {
|
|
for (auto obj : v.second) {
|
|
if (obj && obj->isAttachedToDocument()) {
|
|
add_edge((*objectMap)[v.first], (*objectMap)[obj], *depList);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<App::DocumentObject*>
|
|
Document::getDependencyList(const std::vector<App::DocumentObject*>& objectArray, int options)
|
|
{
|
|
std::vector<App::DocumentObject*> ret;
|
|
if (!(options & DepSort)) {
|
|
_buildDependencyList(objectArray, options, &ret, nullptr, nullptr);
|
|
return ret;
|
|
}
|
|
|
|
DependencyList depList;
|
|
std::map<DocumentObject*, Vertex> objectMap;
|
|
std::map<Vertex, DocumentObject*> vertexMap;
|
|
|
|
_buildDependencyList(objectArray, 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) {
|
|
// 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 << std::endl;
|
|
for (auto& v : components) {
|
|
if (v.second.size() == 1) {
|
|
// For components with only one member, we still need to
|
|
// check if there it is self looping.
|
|
auto it = vertexMap.find(v.second[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 << std::endl << it->second->getFullName() << std::endl;
|
|
break;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
// For components with more than one member, they form a loop together
|
|
for (size_t i = 0; i < v.second.size(); ++i) {
|
|
auto it = vertexMap.find(v.second[i]);
|
|
if (it == vertexMap.end()) {
|
|
continue;
|
|
}
|
|
if (i % 6 == 0) {
|
|
ss << std::endl;
|
|
}
|
|
ss << it->second->getFullName() << ", ";
|
|
}
|
|
ss << std::endl;
|
|
}
|
|
FC_ERR(ss.str());
|
|
FC_THROWM(Base::RuntimeError, e.what());
|
|
}
|
|
FC_ERR(e.what());
|
|
ret = DocumentP::partialTopologicalSort(objectArray);
|
|
std::reverse(ret.begin(), ret.end());
|
|
return ret;
|
|
}
|
|
|
|
for (std::list<Vertex>::reverse_iterator i = make_order.rbegin(); i != make_order.rend(); ++i) {
|
|
ret.push_back(vertexMap[*i]);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
std::vector<App::Document*> Document::getDependentDocuments(bool sort)
|
|
{
|
|
return getDependentDocuments({this}, sort);
|
|
}
|
|
|
|
std::vector<App::Document*> Document::getDependentDocuments(std::vector<App::Document*> pending,
|
|
bool sort)
|
|
{
|
|
DependencyList depList;
|
|
std::map<Document*, Vertex> docMap;
|
|
std::map<Vertex, Document*> vertexMap;
|
|
|
|
std::vector<App::Document*> ret;
|
|
if (pending.empty()) {
|
|
return ret;
|
|
}
|
|
|
|
auto outLists = PropertyXLink::getDocumentOutList();
|
|
std::set<App::Document*> docs;
|
|
docs.insert(pending.begin(), pending.end());
|
|
if (sort) {
|
|
for (auto doc : pending) {
|
|
docMap[doc] = add_vertex(depList);
|
|
}
|
|
}
|
|
while (!pending.empty()) {
|
|
auto doc = pending.back();
|
|
pending.pop_back();
|
|
|
|
auto it = outLists.find(doc);
|
|
if (it == outLists.end()) {
|
|
continue;
|
|
}
|
|
|
|
auto& vertex = docMap[doc];
|
|
for (auto depDoc : it->second) {
|
|
if (docs.insert(depDoc).second) {
|
|
pending.push_back(depDoc);
|
|
if (sort) {
|
|
docMap[depDoc] = add_vertex(depList);
|
|
}
|
|
}
|
|
add_edge(vertex, docMap[depDoc], depList);
|
|
}
|
|
}
|
|
|
|
if (!sort) {
|
|
ret.insert(ret.end(), docs.begin(), docs.end());
|
|
return ret;
|
|
}
|
|
|
|
std::list<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<App::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<App::ObjectIdentifier, App::ObjectIdentifier>& paths,
|
|
const std::function<bool(const App::DocumentObject*)>& selector)
|
|
{
|
|
std::map<App::ObjectIdentifier, App::ObjectIdentifier> extendedPaths;
|
|
|
|
std::map<App::ObjectIdentifier, App::ObjectIdentifier>::const_iterator it = paths.begin();
|
|
while (it != paths.end()) {
|
|
extendedPaths[it->first.canonicalPath()] = it->second.canonicalPath();
|
|
++it;
|
|
}
|
|
|
|
for (auto it : d->objectArray) {
|
|
if (selector(it)) {
|
|
it->renameObjectIdentifiers(extendedPaths);
|
|
}
|
|
}
|
|
}
|
|
|
|
int Document::recompute(const std::vector<App::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);
|
|
signalBeforeRecompute(*this);
|
|
|
|
#if 0
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// FIXME Comment by Realthunder:
|
|
// the topologicalSrot() below cannot handle partial recompute, haven't got
|
|
// time to figure out the code yet, simply use back boost::topological_sort
|
|
// for now, that is, rely on getDependencyList() to do the sorting. The
|
|
// downside is, it didn't take advantage of the ready built InList, nor will
|
|
// it report for cyclic dependency.
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
// get the sorted vector of all dependent objects and go though it from the end
|
|
auto depObjs = getDependencyList(objs.empty()?d->objectArray:objs);
|
|
vector<DocumentObject*> topoSortedObjects = topologicalSort(depObjs);
|
|
if (topoSortedObjects.size() != depObjs.size()){
|
|
cerr << "App::Document::recompute(): cyclic dependency detected" << endl;
|
|
topoSortedObjects = d->partialTopologicalSort(depObjs);
|
|
}
|
|
std::reverse(topoSortedObjects.begin(),topoSortedObjects.end());
|
|
#else
|
|
auto topoSortedObjects =
|
|
getDependencyList(objs.empty() ? d->objectArray : objs, DepSort | options);
|
|
#endif
|
|
for (auto obj : topoSortedObjects) {
|
|
obj->setStatus(ObjectStatus::PendingRecompute, true);
|
|
}
|
|
|
|
ParameterGrp::handle hGrp =
|
|
GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document");
|
|
bool canAbort = hGrp->GetBool("CanAbortRecompute", true);
|
|
|
|
std::set<App::DocumentObject*> filter;
|
|
size_t idx = 0;
|
|
|
|
FC_TIME_INIT(t2);
|
|
|
|
try {
|
|
// maximum two passes to allow some form of dependency inversion
|
|
for (int passes = 0; passes < 2 && idx < 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) {
|
|
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) objs;
|
|
objs.swap(doc->d->pendingRemove);
|
|
for (auto& o : objs) {
|
|
try {
|
|
if (auto obj = o.getObject()) {
|
|
obj->getDocument()->removeObject(obj->getNameInDocument());
|
|
}
|
|
}
|
|
catch (Base::Exception& e) {
|
|
e.reportException();
|
|
FC_ERR("error when removing object " << o.getDocumentName() << '#'
|
|
<< o.getObjectName());
|
|
}
|
|
}
|
|
}
|
|
return objectCount;
|
|
}
|
|
|
|
/*!
|
|
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<App::DocumentObject*>
|
|
DocumentP::partialTopologicalSort(const std::vector<App::DocumentObject*>& objects)
|
|
{
|
|
vector<App::DocumentObject*> ret;
|
|
ret.reserve(objects.size());
|
|
// pairs of input and output degree
|
|
map<App::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<App::DocumentObject*> degIn;
|
|
std::list<App::DocumentObject*> degOut;
|
|
|
|
bool removeVertex = true;
|
|
while (removeVertex) {
|
|
removeVertex = false;
|
|
|
|
// try input degree
|
|
auto degInIt = find_if(countMap.begin(),
|
|
countMap.end(),
|
|
[](pair<App::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& countIt : countMap) {
|
|
if (countIt.second.first < 0) {
|
|
countIt.second.second = -1;
|
|
}
|
|
}
|
|
|
|
removeVertex = degIn.size() != objects.size();
|
|
while (removeVertex) {
|
|
removeVertex = false;
|
|
|
|
auto degOutIt = find_if(countMap.begin(),
|
|
countMap.end(),
|
|
[](pair<App::DocumentObject*, pair<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<App::DocumentObject*>
|
|
DocumentP::topologicalSort(const std::vector<App::DocumentObject*>& objects) const
|
|
{
|
|
// topological sort algorithm described here:
|
|
// https://de.wikipedia.org/wiki/Topologische_Sortierung#Algorithmus_f.C3.BCr_das_Topologische_Sortieren
|
|
vector<App::DocumentObject*> ret;
|
|
ret.reserve(objects.size());
|
|
map<App::DocumentObject*, int> countMap;
|
|
|
|
for (auto objectIt : objects) {
|
|
// We now support externally linked objects
|
|
// if(!obj->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<App::DocumentObject*, int> count) -> bool {
|
|
return count.second == 0;
|
|
});
|
|
|
|
if (rootObjeIt == countMap.end()) {
|
|
cerr << "Document::topologicalSort: cyclic dependency detected (no root object)" << endl;
|
|
return ret;
|
|
}
|
|
|
|
while (rootObjeIt != countMap.end()) {
|
|
rootObjeIt->second = rootObjeIt->second - 1;
|
|
|
|
// we need outlist with unique entries
|
|
auto out = rootObjeIt->first->getOutList();
|
|
std::sort(out.begin(), out.end());
|
|
out.erase(std::unique(out.begin(), out.end()), out.end());
|
|
|
|
for (auto outListIt : out) {
|
|
auto outListMapIt = countMap.find(outListIt);
|
|
if (outListMapIt != countMap.end()) {
|
|
outListMapIt->second = outListMapIt->second - 1;
|
|
}
|
|
}
|
|
ret.push_back(rootObjeIt->first);
|
|
|
|
rootObjeIt = find_if(countMap.begin(),
|
|
countMap.end(),
|
|
[](pair<App::DocumentObject*, int> count) -> bool {
|
|
return count.second == 0;
|
|
});
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
std::vector<App::DocumentObject*> Document::topologicalSort() const
|
|
{
|
|
return d->topologicalSort(d->objectArray);
|
|
}
|
|
|
|
const char* Document::getErrorDescription(const App::DocumentObject* Obj) const
|
|
{
|
|
return d->findRecomputeLog(Obj);
|
|
}
|
|
|
|
// call the recompute of the Feature and handle the exceptions and errors.
|
|
int Document::_recomputeFeature(DocumentObject* Feat)
|
|
{
|
|
FC_LOG("Recomputing " << Feat->getFullName());
|
|
|
|
DocumentObjectExecReturn* returnCode = nullptr;
|
|
try {
|
|
returnCode = Feat->ExpressionEngine.execute(PropertyExpressionEngine::ExecuteNonOutput);
|
|
if (returnCode == DocumentObject::StdReturn) {
|
|
returnCode = Feat->recompute();
|
|
if (returnCode == DocumentObject::StdReturn) {
|
|
returnCode =
|
|
Feat->ExpressionEngine.execute(PropertyExpressionEngine::ExecuteOutput);
|
|
}
|
|
}
|
|
}
|
|
catch (Base::AbortException& e) {
|
|
e.reportException();
|
|
FC_LOG("Failed to recompute " << Feat->getFullName() << ": " << e.what());
|
|
d->addRecomputeLog("User abort", Feat);
|
|
return -1;
|
|
}
|
|
catch (const Base::MemoryException& e) {
|
|
FC_ERR("Memory exception in " << Feat->getFullName() << " thrown: " << e.what());
|
|
d->addRecomputeLog("Out of memory exception", Feat);
|
|
return 1;
|
|
}
|
|
catch (Base::Exception& e) {
|
|
e.reportException();
|
|
FC_LOG("Failed to recompute " << Feat->getFullName() << ": " << e.what());
|
|
d->addRecomputeLog(e.what(), Feat);
|
|
return 1;
|
|
}
|
|
catch (std::exception& e) {
|
|
FC_ERR("Exception in " << Feat->getFullName() << " thrown: " << e.what());
|
|
d->addRecomputeLog(e.what(), Feat);
|
|
return 1;
|
|
}
|
|
#ifndef FC_DEBUG
|
|
catch (...) {
|
|
FC_ERR("Unknown exception in " << Feat->getFullName() << " thrown");
|
|
d->addRecomputeLog("Unknown exception!", Feat);
|
|
return 1;
|
|
}
|
|
#endif
|
|
|
|
if (returnCode == DocumentObject::StdReturn) {
|
|
Feat->resetError();
|
|
}
|
|
else {
|
|
returnCode->Which = Feat;
|
|
d->addRecomputeLog(returnCode);
|
|
FC_LOG("Failed to recompute " << Feat->getFullName() << ": " << returnCode->Why);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool Document::recomputeFeature(DocumentObject* 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;
|
|
}
|
|
else {
|
|
_recomputeFeature(feature);
|
|
signalRecomputedObject(*feature);
|
|
return feature->isValid();
|
|
}
|
|
}
|
|
|
|
DocumentObject* Document::addObject(const char* sType,
|
|
const char* pObjectName,
|
|
bool isNew,
|
|
const char* viewType,
|
|
bool isPartial)
|
|
{
|
|
const Base::Type type =
|
|
Base::Type::getTypeIfDerivedFrom(sType, App::DocumentObject::getClassTypeId(), true);
|
|
if (type.isBad()) {
|
|
std::stringstream str;
|
|
str << "'" << sType << "' is not a document object type";
|
|
throw Base::TypeError(str.str());
|
|
}
|
|
|
|
void* typeInstance = type.createInstance();
|
|
if (!typeInstance) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto* pcObject = static_cast<App::DocumentObject*>(typeInstance);
|
|
pcObject->setDocument(this);
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback) {
|
|
// Undo stuff
|
|
_checkTransaction(nullptr, nullptr, __LINE__);
|
|
if (d->activeUndoTransaction) {
|
|
d->activeUndoTransaction->addObjectDel(pcObject);
|
|
}
|
|
}
|
|
|
|
// get Unique name
|
|
const bool hasName = !Base::Tools::isNullOrEmpty(pObjectName);
|
|
const string ObjectName = getUniqueObjectName(hasName ? pObjectName : type.getName());
|
|
|
|
d->activeObject = pcObject;
|
|
|
|
// insert in the name map
|
|
d->objectMap[ObjectName] = pcObject;
|
|
d->objectNameManager.addExactName(ObjectName);
|
|
// generate object id and add to id map;
|
|
pcObject->_Id = ++d->lastObjectId;
|
|
d->objectIdMap[pcObject->_Id] = pcObject;
|
|
// cache the pointer to the name string in the Object (for performance of
|
|
// DocumentObject::getNameInDocument())
|
|
pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first);
|
|
// insert in the vector
|
|
d->objectArray.push_back(pcObject);
|
|
// Register the current Label even though it is (probably) about to change
|
|
registerLabel(pcObject->Label.getStrValue());
|
|
|
|
// If we are restoring, don't set the Label object now; it will be restored later. This is to
|
|
// avoid potential duplicate label conflicts later.
|
|
if (!d->StatusBits.test(Restoring)) {
|
|
pcObject->Label.setValue(ObjectName);
|
|
}
|
|
|
|
// Call the object-specific initialization
|
|
if (!d->undoing && !d->rollback && isNew) {
|
|
pcObject->setupObject();
|
|
}
|
|
|
|
// mark the object as new (i.e. set status bit 2) and send the signal
|
|
pcObject->setStatus(ObjectStatus::New, true);
|
|
|
|
pcObject->setStatus(ObjectStatus::PartialObject, isPartial);
|
|
|
|
if (Base::Tools::isNullOrEmpty(viewType)) {
|
|
viewType = pcObject->getViewProviderNameOverride();
|
|
}
|
|
|
|
if (!Base::Tools::isNullOrEmpty(viewType)) {
|
|
pcObject->_pcViewProviderName = viewType;
|
|
}
|
|
|
|
signalNewObject(*pcObject);
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback && d->activeUndoTransaction) {
|
|
signalTransactionAppend(*pcObject, d->activeUndoTransaction);
|
|
}
|
|
|
|
signalActivatedObject(*pcObject);
|
|
|
|
// return the Object
|
|
return pcObject;
|
|
}
|
|
|
|
std::vector<DocumentObject*>
|
|
Document::addObjects(const char* sType, const std::vector<std::string>& objectNames, bool isNew)
|
|
{
|
|
Base::Type type =
|
|
Base::Type::getTypeIfDerivedFrom(sType, App::DocumentObject::getClassTypeId(), true);
|
|
if (type.isBad()) {
|
|
std::stringstream str;
|
|
str << "'" << sType << "' is not a document object type";
|
|
throw Base::TypeError(str.str());
|
|
}
|
|
|
|
std::vector<DocumentObject*> objects;
|
|
objects.resize(objectNames.size());
|
|
std::generate(objects.begin(), objects.end(), [&] {
|
|
return static_cast<App::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) {
|
|
auto index = std::distance(objects.begin(), it);
|
|
App::DocumentObject* pcObject = *it;
|
|
pcObject->setDocument(this);
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback) {
|
|
// Undo stuff
|
|
_checkTransaction(nullptr, nullptr, __LINE__);
|
|
if (d->activeUndoTransaction) {
|
|
d->activeUndoTransaction->addObjectDel(pcObject);
|
|
}
|
|
}
|
|
|
|
// get unique name. We don't use getUniqueObjectName because it takes a char* not a std::string
|
|
std::string ObjectName = objectNames[index];
|
|
if (ObjectName.empty()) {
|
|
ObjectName = sType;
|
|
}
|
|
ObjectName = Base::Tools::getIdentifier(ObjectName);
|
|
if (d->objectNameManager.containsName(ObjectName)) {
|
|
ObjectName = d->objectNameManager.makeUniqueName(ObjectName, 3);
|
|
}
|
|
|
|
// insert in the name map
|
|
d->objectMap[ObjectName] = pcObject;
|
|
d->objectNameManager.addExactName(ObjectName);
|
|
// generate object id and add to id map;
|
|
pcObject->_Id = ++d->lastObjectId;
|
|
d->objectIdMap[pcObject->_Id] = pcObject;
|
|
// cache the pointer to the name string in the Object (for performance of
|
|
// DocumentObject::getNameInDocument())
|
|
pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first);
|
|
// insert in the vector
|
|
d->objectArray.push_back(pcObject);
|
|
// Register the current Label even though it is about to change
|
|
registerLabel(pcObject->Label.getStrValue());
|
|
|
|
pcObject->Label.setValue(ObjectName);
|
|
|
|
// Call the object-specific initialization
|
|
if (!d->undoing && !d->rollback && isNew) {
|
|
pcObject->setupObject();
|
|
}
|
|
|
|
// mark the object as new (i.e. set status bit 2) and send the signal
|
|
pcObject->setStatus(ObjectStatus::New, true);
|
|
|
|
const char* viewType = pcObject->getViewProviderNameOverride();
|
|
pcObject->_pcViewProviderName = viewType ? viewType : "";
|
|
|
|
signalNewObject(*pcObject);
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback && d->activeUndoTransaction) {
|
|
signalTransactionAppend(*pcObject, d->activeUndoTransaction);
|
|
}
|
|
}
|
|
|
|
if (!objects.empty()) {
|
|
d->activeObject = objects.back();
|
|
signalActivatedObject(*objects.back());
|
|
}
|
|
|
|
return objects;
|
|
}
|
|
|
|
void Document::addObject(DocumentObject* pcObject, const char* pObjectName)
|
|
{
|
|
if (pcObject->getDocument()) {
|
|
throw Base::RuntimeError("Document object is already added to a document");
|
|
}
|
|
|
|
pcObject->setDocument(this);
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback) {
|
|
// Undo stuff
|
|
_checkTransaction(nullptr, nullptr, __LINE__);
|
|
if (d->activeUndoTransaction) {
|
|
d->activeUndoTransaction->addObjectDel(pcObject);
|
|
}
|
|
}
|
|
|
|
// get unique name
|
|
string ObjectName;
|
|
if (!Base::Tools::isNullOrEmpty(pObjectName)) {
|
|
ObjectName = getUniqueObjectName(pObjectName);
|
|
}
|
|
else {
|
|
ObjectName = getUniqueObjectName(pcObject->getTypeId().getName());
|
|
}
|
|
|
|
d->activeObject = pcObject;
|
|
|
|
// insert in the name map
|
|
d->objectMap[ObjectName] = pcObject;
|
|
d->objectNameManager.addExactName(ObjectName);
|
|
// generate object id and add to id map;
|
|
if (!pcObject->_Id) {
|
|
pcObject->_Id = ++d->lastObjectId;
|
|
}
|
|
d->objectIdMap[pcObject->_Id] = pcObject;
|
|
// cache the pointer to the name string in the Object (for performance of
|
|
// DocumentObject::getNameInDocument())
|
|
pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first);
|
|
// insert in the vector
|
|
d->objectArray.push_back(pcObject);
|
|
// Register the current Label even though it is about to change
|
|
registerLabel(pcObject->Label.getStrValue());
|
|
|
|
pcObject->Label.setValue(ObjectName);
|
|
|
|
// mark the object as new (i.e. set status bit 2) and send the signal
|
|
pcObject->setStatus(ObjectStatus::New, true);
|
|
|
|
const char* viewType = pcObject->getViewProviderNameOverride();
|
|
pcObject->_pcViewProviderName = viewType ? viewType : "";
|
|
|
|
signalNewObject(*pcObject);
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback && d->activeUndoTransaction) {
|
|
signalTransactionAppend(*pcObject, d->activeUndoTransaction);
|
|
}
|
|
|
|
signalActivatedObject(*pcObject);
|
|
}
|
|
|
|
void Document::_addObject(DocumentObject* pcObject, const char* pObjectName)
|
|
{
|
|
std::string ObjectName = getUniqueObjectName(pObjectName);
|
|
d->objectMap[ObjectName] = pcObject;
|
|
d->objectNameManager.addExactName(ObjectName);
|
|
// generate object id and add to id map;
|
|
if (!pcObject->_Id) {
|
|
pcObject->_Id = ++d->lastObjectId;
|
|
}
|
|
d->objectIdMap[pcObject->_Id] = pcObject;
|
|
d->objectArray.push_back(pcObject);
|
|
registerLabel(pcObject->Label.getStrValue());
|
|
// cache the pointer to the name string in the Object (for performance of
|
|
// DocumentObject::getNameInDocument())
|
|
pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first);
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback) {
|
|
// Undo stuff
|
|
_checkTransaction(nullptr, nullptr, __LINE__);
|
|
if (d->activeUndoTransaction) {
|
|
d->activeUndoTransaction->addObjectDel(pcObject);
|
|
}
|
|
}
|
|
|
|
const char* viewType = pcObject->getViewProviderNameOverride();
|
|
pcObject->_pcViewProviderName = viewType ? viewType : "";
|
|
|
|
// send the signal
|
|
signalNewObject(*pcObject);
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback && d->activeUndoTransaction) {
|
|
signalTransactionAppend(*pcObject, d->activeUndoTransaction);
|
|
}
|
|
|
|
d->activeObject = pcObject;
|
|
signalActivatedObject(*pcObject);
|
|
}
|
|
|
|
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);
|
|
|
|
// name not found?
|
|
if (pos == d->objectMap.end()) {
|
|
return;
|
|
}
|
|
|
|
if (pos->second->testStatus(ObjectStatus::PendingRecompute)) {
|
|
// TODO: shall we allow removal if there is active undo transaction?
|
|
FC_MSG("pending remove of " << sName << " after recomputing document " << getName());
|
|
d->pendingRemove.emplace_back(pos->second);
|
|
return;
|
|
}
|
|
|
|
TransactionLocker tlock;
|
|
|
|
_checkTransaction(pos->second, nullptr, __LINE__);
|
|
|
|
if (d->activeObject == pos->second) {
|
|
d->activeObject = nullptr;
|
|
}
|
|
|
|
// Mark the object as about to be deleted
|
|
pos->second->setStatus(ObjectStatus::Remove, true);
|
|
if (!d->undoing && !d->rollback) {
|
|
pos->second->unsetupObject();
|
|
}
|
|
|
|
signalDeletedObject(*(pos->second));
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback && d->activeUndoTransaction) {
|
|
// in this case transaction delete or save the object
|
|
signalTransactionRemove(*pos->second, d->activeUndoTransaction);
|
|
}
|
|
else {
|
|
// if not saved in undo -> delete object
|
|
signalTransactionRemove(*pos->second, 0);
|
|
}
|
|
|
|
// Before deleting we must nullify all dependent objects
|
|
breakDependency(pos->second, true);
|
|
|
|
// and remove the tip if needed
|
|
if (Tip.getValue() && strcmp(Tip.getValue()->getNameInDocument(), sName) == 0) {
|
|
Tip.setValue(nullptr);
|
|
TipName.setValue("");
|
|
}
|
|
|
|
// remove the ID before possibly deleting the object
|
|
d->objectIdMap.erase(pos->second->_Id);
|
|
// Unset the bit to be on the safe side
|
|
pos->second->setStatus(ObjectStatus::Remove, false);
|
|
unregisterLabel(pos->second->Label.getStrValue());
|
|
|
|
// do no transactions if we do a rollback!
|
|
std::unique_ptr<DocumentObject> tobedestroyed;
|
|
if (!d->rollback) {
|
|
// Undo stuff
|
|
if (d->activeUndoTransaction) {
|
|
// in this case transaction delete or save the object
|
|
d->activeUndoTransaction->addObjectNew(pos->second);
|
|
}
|
|
else {
|
|
// if not saved in undo -> delete object later
|
|
std::unique_ptr<DocumentObject> delobj(pos->second);
|
|
tobedestroyed.swap(delobj);
|
|
tobedestroyed->setStatus(ObjectStatus::Destroy, true);
|
|
}
|
|
}
|
|
|
|
for (std::vector<DocumentObject*>::iterator obj = d->objectArray.begin();
|
|
obj != d->objectArray.end();
|
|
++obj) {
|
|
if (*obj == pos->second) {
|
|
d->objectArray.erase(obj);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// In case the object gets deleted the pointer must be nullified
|
|
if (tobedestroyed) {
|
|
tobedestroyed->pcNameInDocument = nullptr;
|
|
}
|
|
d->objectNameManager.removeExactName(pos->first);
|
|
d->objectMap.erase(pos);
|
|
}
|
|
|
|
/// Remove an object out of the document (internal)
|
|
void Document::_removeObject(DocumentObject* pcObject)
|
|
{
|
|
if (testStatus(Document::Recomputing)) {
|
|
FC_ERR("Cannot delete " << pcObject->getFullName() << " while recomputing");
|
|
return;
|
|
}
|
|
|
|
TransactionLocker tlock;
|
|
|
|
// TODO Refactoring: share code with Document::removeObject() (2015-09-01, Fat-Zer)
|
|
_checkTransaction(pcObject, nullptr, __LINE__);
|
|
|
|
auto pos = d->objectMap.find(pcObject->getNameInDocument());
|
|
if (pos == d->objectMap.end()) {
|
|
FC_ERR("Internal error, could not find " << pcObject->getFullName() << " to remove");
|
|
}
|
|
|
|
if (!d->rollback && d->activeUndoTransaction && pos->second->hasChildElement()) {
|
|
// Preserve link group children global visibility. See comments in
|
|
// removeObject() for more details.
|
|
for (auto& sub : pos->second->getSubObjects()) {
|
|
if (sub.empty()) {
|
|
continue;
|
|
}
|
|
if (sub[sub.size() - 1] != '.') {
|
|
sub += '.';
|
|
}
|
|
auto sobj = pos->second->getSubObject(sub.c_str());
|
|
if (sobj && sobj->getDocument() == this && !sobj->Visibility.getValue()) {
|
|
d->activeUndoTransaction->addObjectChange(sobj, &sobj->Visibility);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (d->activeObject == pcObject) {
|
|
d->activeObject = nullptr;
|
|
}
|
|
|
|
// Mark the object as about to be removed
|
|
pcObject->setStatus(ObjectStatus::Remove, true);
|
|
if (!d->undoing && !d->rollback) {
|
|
pcObject->unsetupObject();
|
|
}
|
|
signalDeletedObject(*pcObject);
|
|
// TODO Check me if it's needed (2015-09-01, Fat-Zer)
|
|
|
|
// remove the tip if needed
|
|
if (Tip.getValue() == pcObject) {
|
|
Tip.setValue(nullptr);
|
|
TipName.setValue("");
|
|
}
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback && d->activeUndoTransaction) {
|
|
// Undo stuff
|
|
signalTransactionRemove(*pcObject, d->activeUndoTransaction);
|
|
breakDependency(pcObject, true);
|
|
d->activeUndoTransaction->addObjectNew(pcObject);
|
|
}
|
|
else {
|
|
// for a rollback delete the object
|
|
signalTransactionRemove(*pcObject, 0);
|
|
breakDependency(pcObject, true);
|
|
}
|
|
// TODO: Transaction::addObjectName could potentially have freed (deleted) pcObject so some of the following
|
|
// code may be dereferencing a pointer to a deleted object which is not legal. if (d->rollback) this does not occur
|
|
// and instead pcObject is deleted at the end of this function.
|
|
// This either should be fixed, perhaps by moving the following lines up in the code,
|
|
// or there should be a comment explaining why the object will never be deleted because of the logic that got us here.
|
|
|
|
// 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(pos->second->Label.getStrValue());
|
|
d->objectMap.erase(pos);
|
|
|
|
for (std::vector<DocumentObject*>::iterator it = d->objectArray.begin();
|
|
it != d->objectArray.end();
|
|
++it) {
|
|
if (*it == pcObject) {
|
|
d->objectArray.erase(it);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// for a rollback delete the object
|
|
if (d->rollback) {
|
|
pcObject->setStatus(ObjectStatus::Destroy, true);
|
|
delete pcObject;
|
|
}
|
|
}
|
|
|
|
void Document::breakDependency(DocumentObject* pcObject, bool clear)
|
|
{
|
|
// Nullify all dependent objects
|
|
PropertyLinkBase::breakLinks(pcObject, d->objectArray, clear);
|
|
}
|
|
|
|
std::vector<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<App::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(App::Application::getTempFileName());
|
|
Base::ofstream ostr(fi, std::ios::out | std::ios::binary);
|
|
exportObjects(deps, ostr);
|
|
ostr.close();
|
|
|
|
Base::ifstream istr(fi, std::ios::in | std::ios::binary);
|
|
imported = md.importObjects(istr);
|
|
}
|
|
|
|
if (returnAll || imported.size() != deps.size()) {
|
|
return imported;
|
|
}
|
|
|
|
std::unordered_map<App::DocumentObject*, size_t> indices;
|
|
size_t i = 0;
|
|
for (auto o : deps) {
|
|
indices[o] = i++;
|
|
}
|
|
std::vector<App::DocumentObject*> result;
|
|
result.reserve(objs.size());
|
|
for (auto o : objs) {
|
|
result.push_back(imported[indices[o]]);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::vector<App::DocumentObject*>
|
|
Document::importLinks(const std::vector<App::DocumentObject*>& objArray)
|
|
{
|
|
std::set<App::DocumentObject*> links;
|
|
getLinksTo(links, nullptr, GetLinkExternal, 0, objArray);
|
|
|
|
std::vector<App::DocumentObject*> objs;
|
|
objs.insert(objs.end(), links.begin(), links.end());
|
|
objs = App::Document::getDependencyList(objs);
|
|
if (objs.empty()) {
|
|
FC_ERR("nothing to import");
|
|
return objs;
|
|
}
|
|
|
|
for (auto it = objs.begin(); it != objs.end();) {
|
|
auto obj = *it;
|
|
if (obj->getDocument() == this) {
|
|
it = objs.erase(it);
|
|
continue;
|
|
}
|
|
++it;
|
|
if (obj->testStatus(App::PartialObject)) {
|
|
throw Base::RuntimeError(
|
|
"Cannot import partial loaded object. Please reload the current document");
|
|
}
|
|
}
|
|
|
|
Base::FileInfo fi(App::Application::getTempFileName());
|
|
{
|
|
// save stuff to temp file
|
|
Base::ofstream str(fi, std::ios::out | std::ios::binary);
|
|
MergeDocuments mimeView(this);
|
|
exportObjects(objs, str);
|
|
str.close();
|
|
}
|
|
Base::ifstream str(fi, std::ios::in | std::ios::binary);
|
|
MergeDocuments mimeView(this);
|
|
objs = mimeView.importObjects(str);
|
|
str.close();
|
|
fi.deleteFile();
|
|
|
|
const auto& nameMap = mimeView.getNameMap();
|
|
|
|
// First, find all link type properties that needs to be changed
|
|
std::map<App::Property*, std::unique_ptr<App::Property>> propMap;
|
|
std::vector<App::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 objs;
|
|
}
|
|
|
|
DocumentObject* Document::moveObject(DocumentObject* obj, bool recursive)
|
|
{
|
|
if (!obj) {
|
|
return nullptr;
|
|
}
|
|
Document* that = obj->getDocument();
|
|
if (that == this) {
|
|
return nullptr; // nothing todo
|
|
}
|
|
|
|
// True object move without copy is only safe when undo is off on both
|
|
// documents.
|
|
if (!recursive && !d->iUndoMode && !that->d->iUndoMode && !that->d->rollback) {
|
|
// all object of the other document that refer to this object must be nullified
|
|
that->breakDependency(obj, false);
|
|
std::string objname = getUniqueObjectName(obj->getNameInDocument());
|
|
that->_removeObject(obj);
|
|
this->_addObject(obj, objname.c_str());
|
|
obj->setDocument(this);
|
|
return obj;
|
|
}
|
|
|
|
std::vector<App::DocumentObject*> deps;
|
|
if (recursive) {
|
|
deps = getDependencyList({obj}, DepNoXLinked | DepSort);
|
|
}
|
|
else {
|
|
deps.push_back(obj);
|
|
}
|
|
|
|
auto objs = copyObject(deps, false);
|
|
if (objs.empty()) {
|
|
return nullptr;
|
|
}
|
|
// Some object may delete its children if deleted, so we collect the IDs
|
|
// or all depending objects for safety reason.
|
|
std::vector<int> ids;
|
|
ids.reserve(deps.size());
|
|
for (auto o : deps) {
|
|
ids.push_back(o->getID());
|
|
}
|
|
|
|
// We only remove object if it is the moving object or it has no
|
|
// depending objects, i.e. an empty inList, which is why we need to
|
|
// iterate the depending list backwards.
|
|
for (auto iter = ids.rbegin(); iter != ids.rend(); ++iter) {
|
|
auto o = that->getObjectByID(*iter);
|
|
if (!o) {
|
|
continue;
|
|
}
|
|
if (iter == ids.rbegin() || o->getInList().empty()) {
|
|
that->removeObject(o->getNameInDocument());
|
|
}
|
|
}
|
|
return objs.back();
|
|
}
|
|
|
|
DocumentObject* Document::getActiveObject() const
|
|
{
|
|
return d->activeObject;
|
|
}
|
|
|
|
DocumentObject* Document::getObject(const char* Name) const
|
|
{
|
|
auto pos = d->objectMap.find(Name);
|
|
|
|
if (pos != d->objectMap.end()) {
|
|
return pos->second;
|
|
}
|
|
else {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
DocumentObject* Document::getObjectByID(long id) const
|
|
{
|
|
auto it = d->objectIdMap.find(id);
|
|
if (it != d->objectIdMap.end()) {
|
|
return it->second;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
// Note: This method is only used in Tree.cpp slotChangeObject(), see explanation there
|
|
bool Document::isIn(const DocumentObject* pFeat) const
|
|
{
|
|
for (const auto& pos : d->objectMap) {
|
|
if (pos.second == pFeat) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
const char* Document::getObjectName(DocumentObject* pFeat) const
|
|
{
|
|
for (const auto& pos : d->objectMap) {
|
|
if (pos.second == pFeat) {
|
|
return pos.first.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,
|
|
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, rx_label;
|
|
|
|
if (objname) {
|
|
rx_name.set_expression(objname);
|
|
}
|
|
|
|
if (label) {
|
|
rx_label.set_expression(label);
|
|
}
|
|
|
|
std::vector<DocumentObject*> Objects;
|
|
DocumentObject* found = nullptr;
|
|
for (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
|
|
{
|
|
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<App::DocumentObject*> Document::getRootObjects() const
|
|
{
|
|
std::vector<App::DocumentObject*> ret;
|
|
|
|
for (auto objectIt : d->objectArray) {
|
|
if (objectIt->getInList().empty()) {
|
|
ret.push_back(objectIt);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
std::vector<App::DocumentObject*> Document::getRootObjectsIgnoreLinks() const
|
|
{
|
|
std::vector<App::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(), [](App::DocumentObject* obj) {
|
|
return obj->isDerivedFrom<App::Link>();
|
|
});
|
|
}
|
|
|
|
if (noParents) {
|
|
ret.push_back(objectIt);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
void DocumentP::findAllPathsAt(const std::vector<Node>& all_nodes,
|
|
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++) {
|
|
findAllPathsAt(all_nodes, all_nodes[id][i], all_paths, tmp);
|
|
}
|
|
}
|
|
|
|
std::vector<std::list<App::DocumentObject*>>
|
|
Document::getPathsByOutList(const App::DocumentObject* from, const App::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) {
|
|
DocumentObject* obj = d->objectArray[i];
|
|
std::vector<DocumentObject*> outList = obj->getOutList();
|
|
for (auto it : outList) {
|
|
all_nodes[i].push_back(indexMap[it]);
|
|
}
|
|
}
|
|
|
|
std::vector<std::list<App::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, false, nullptr, nullptr, nullptr, &touched);
|
|
return touched;
|
|
}
|
|
|
|
for (auto It : d->objectArray) {
|
|
if (It->isTouched() || It->mustExecute() == 1) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|