4740 lines
176 KiB
C++
4740 lines
176 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 *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
|
|
/*!
|
|
\defgroup Document Document
|
|
\ingroup APP
|
|
\brief The Base class of the FreeCAD Document
|
|
|
|
This (besides the App::Application class) is the most important class in FreeCAD.
|
|
It contains all the data of the opened, saved, or newly created FreeCAD Document.
|
|
The App::Document manages the Undo and Redo mechanism and the linking of documents.
|
|
|
|
\namespace App \class App::Document
|
|
This is besides the Application class the most important class in FreeCAD
|
|
It contains all the data of the opened, saved or newly created FreeCAD Document.
|
|
The Document manage the Undo and Redo mechanism and the linking of documents.
|
|
|
|
Note: the documents are not free objects. They are completely handled by the
|
|
App::Application. Only the Application can Open or destroy a document.
|
|
|
|
\section Exception Exception handling
|
|
As the document is the main data structure of FreeCAD we have to take a close
|
|
look at how Exceptions affect the integrity of the App::Document.
|
|
|
|
\section UndoRedo Undo Redo an Transactions
|
|
Undo Redo handling is one of the major mechanism of a document in terms of
|
|
user friendliness and speed (no one will wait for Undo too long).
|
|
|
|
\section Dependency Graph and dependency handling
|
|
The FreeCAD document handles the dependencies of its DocumentObjects with
|
|
an adjacence list. This gives the opportunity to calculate the shortest
|
|
recompute path. Also, it enables more complicated dependencies beyond trees.
|
|
|
|
@see App::Application
|
|
@see App::DocumentObject
|
|
*/
|
|
|
|
#include "PreCompiled.h"
|
|
|
|
#ifndef _PreComp_
|
|
# include <bitset>
|
|
# include <stack>
|
|
# include <boost/filesystem.hpp>
|
|
#endif
|
|
|
|
#include <boost/algorithm/string.hpp>
|
|
#include <boost/graph/graphviz.hpp>
|
|
#include <boost/graph/strong_components.hpp>
|
|
|
|
#ifdef USE_OLD_DAG
|
|
#include <boost/graph/topological_sort.hpp>
|
|
#include <boost/graph/depth_first_search.hpp>
|
|
#include <boost/graph/dijkstra_shortest_paths.hpp>
|
|
#include <boost/graph/visitors.hpp>
|
|
#endif //USE_OLD_DAG
|
|
|
|
#include <boost/regex.hpp>
|
|
#include <random>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
|
|
#include <QCryptographicHash>
|
|
#include <QCoreApplication>
|
|
|
|
#include <App/DocumentPy.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/Tools.h>
|
|
#include <Base/Uuid.h>
|
|
#include <Base/Sequencer.h>
|
|
#include <Base/Stream.h>
|
|
|
|
#include "Document.h"
|
|
#include "Application.h"
|
|
#include "AutoTransaction.h"
|
|
#include "DocumentObserver.h"
|
|
#include "DocumentObject.h"
|
|
#include "ExpressionParser.h"
|
|
#include "GeoFeature.h"
|
|
#include "GeoFeatureGroupExtension.h"
|
|
#include "Link.h"
|
|
#include "MergeDocuments.h"
|
|
#include "Origin.h"
|
|
#include "OriginGroupExtension.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 = boost::filesystem;
|
|
|
|
// using VertexProperty = boost::property<boost::vertex_root_t, DocumentObject* >;
|
|
using DependencyList = boost::adjacency_list <
|
|
boost::vecS, // class OutEdgeListS : a Sequence or an AssociativeContainer
|
|
boost::vecS, // class VertexListS : a Sequence or a RandomAccessContainer
|
|
boost::directedS, // class DirectedS : This is a directed graph
|
|
boost::no_property, // class VertexProperty:
|
|
boost::no_property, // class EdgeProperty:
|
|
boost::no_property, // class GraphProperty:
|
|
boost::listS // class EdgeListS:
|
|
>;
|
|
using Traits = boost::graph_traits<DependencyList>;
|
|
using Vertex = Traits::vertex_descriptor;
|
|
using Edge = Traits::edge_descriptor;
|
|
using Node = std::vector <size_t>;
|
|
using Path = std::vector <size_t>;
|
|
|
|
namespace App {
|
|
|
|
static bool globalIsRestoring;
|
|
static bool globalIsRelabeling;
|
|
// Pimpl class
|
|
struct DocumentP
|
|
{
|
|
// Array to preserve the creation order of created objects
|
|
std::vector<DocumentObject*> objectArray;
|
|
std::unordered_set<App::DocumentObject*> touchedObjs;
|
|
std::unordered_map<std::string, DocumentObject*> objectMap;
|
|
std::unordered_map<long, DocumentObject*> objectIdMap;
|
|
std::unordered_map<std::string, bool> partialLoadObjects;
|
|
std::vector<DocumentObjectT> pendingRemove;
|
|
long lastObjectId;
|
|
DocumentObject* activeObject;
|
|
Transaction *activeUndoTransaction;
|
|
// pointer to the python class
|
|
Py::Object DocumentPythonObject;
|
|
int iTransactionMode;
|
|
bool rollback;
|
|
bool undoing; ///< document in the middle of undo or redo
|
|
bool committing;
|
|
bool opentransaction;
|
|
std::bitset<32> StatusBits;
|
|
int iUndoMode;
|
|
unsigned int UndoMemSize;
|
|
unsigned int UndoMaxStackSize;
|
|
std::string programVersion;
|
|
#ifdef USE_OLD_DAG
|
|
DependencyList DepList;
|
|
std::map<DocumentObject*, Vertex> VertexObjectList;
|
|
std::map<Vertex, DocumentObject*> vertexMap;
|
|
#endif //USE_OLD_DAG
|
|
std::multimap<const App::DocumentObject*,
|
|
std::unique_ptr<App::DocumentObjectExecReturn> > _RecomputeLog;
|
|
|
|
DocumentP() {
|
|
static std::random_device _RD;
|
|
static std::mt19937 _RGEN(_RD());
|
|
static std::uniform_int_distribution<> _RDIST(0, 5000);
|
|
// Set some random offset to reduce likelihood of ID collision when
|
|
// copying shape from other document. It is probably better to randomize
|
|
// on each object ID.
|
|
lastObjectId = _RDIST(_RGEN);
|
|
activeObject = nullptr;
|
|
activeUndoTransaction = nullptr;
|
|
iTransactionMode = 0;
|
|
rollback = false;
|
|
undoing = false;
|
|
committing = false;
|
|
opentransaction = false;
|
|
StatusBits.set((size_t)Document::Closable, true);
|
|
StatusBits.set((size_t)Document::KeepTrailingDigits, true);
|
|
StatusBits.set((size_t)Document::Restoring, false);
|
|
iUndoMode = 0;
|
|
UndoMemSize = 0;
|
|
UndoMaxStackSize = 20;
|
|
}
|
|
|
|
void addRecomputeLog(const char *why, App::DocumentObject *obj) {
|
|
addRecomputeLog(new DocumentObjectExecReturn(why, obj));
|
|
}
|
|
|
|
void addRecomputeLog(const std::string &why, App::DocumentObject *obj) {
|
|
addRecomputeLog(new DocumentObjectExecReturn(why, obj));
|
|
}
|
|
|
|
void addRecomputeLog(DocumentObjectExecReturn *returnCode) {
|
|
if(!returnCode->Which) {
|
|
delete returnCode;
|
|
return;
|
|
}
|
|
_RecomputeLog.emplace(returnCode->Which, std::unique_ptr<DocumentObjectExecReturn>(returnCode));
|
|
returnCode->Which->setStatus(ObjectStatus::Error, true);
|
|
}
|
|
|
|
void clearRecomputeLog(const App::DocumentObject *obj=nullptr) {
|
|
if(!obj)
|
|
_RecomputeLog.clear();
|
|
else
|
|
_RecomputeLog.erase(obj);
|
|
}
|
|
|
|
void clearDocument() {
|
|
objectArray.clear();
|
|
for(auto &v : objectMap) {
|
|
v.second->setStatus(ObjectStatus::Destroy, true);
|
|
delete(v.second);
|
|
v.second = nullptr;
|
|
}
|
|
objectMap.clear();
|
|
objectIdMap.clear();
|
|
}
|
|
|
|
const char *findRecomputeLog(const App::DocumentObject *obj) {
|
|
auto range = _RecomputeLog.equal_range(obj);
|
|
if(range.first == range.second)
|
|
return nullptr;
|
|
return (--range.second)->second->Why.c_str();
|
|
}
|
|
|
|
static
|
|
void findAllPathsAt(const std::vector <Node> &all_nodes, size_t id,
|
|
std::vector <Path> &all_paths, Path tmp);
|
|
std::vector<App::DocumentObject*>
|
|
topologicalSort(const std::vector<App::DocumentObject*>& objects) const;
|
|
std::vector<App::DocumentObject*>
|
|
static partialTopologicalSort(const std::vector<App::DocumentObject*>& objects);
|
|
};
|
|
|
|
} // 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);
|
|
}
|
|
|
|
void Document::writeDependencyGraphViz(std::ostream &out)
|
|
{
|
|
// // caching vertex to DocObject
|
|
//std::map<Vertex,DocumentObject*> VertexMap;
|
|
//for(std::map<DocumentObject*,Vertex>::const_iterator It1= _DepConMap.begin();It1 != _DepConMap.end(); ++It1)
|
|
// VertexMap[It1->second] = It1->first;
|
|
|
|
out << "digraph G {" << std::endl;
|
|
out << "\tordering=out;" << std::endl;
|
|
out << "\tnode [shape = box];" << std::endl;
|
|
|
|
for (const auto &It : d->objectMap) {
|
|
out << "\t" << It.first << ";" << std::endl;
|
|
std::vector<DocumentObject*> OutList = It.second->getOutList();
|
|
for (const auto &It2 : OutList)
|
|
if (It2)
|
|
out << "\t" << It.first << "->" << It2->getNameInDocument() << ";" << std::endl;
|
|
}
|
|
|
|
/*
|
|
graph_traits<DependencyList>::edge_iterator ei, ei_end;
|
|
for (tie(ei,ei_end) = edges(_DepList); ei != ei_end; ++ei)
|
|
out << "\t"
|
|
<< VertexMap[source(*ei, _DepList)]->getNameInDocument()
|
|
<< " -> "
|
|
<< VertexMap[target(*ei, _DepList)]->getNameInDocument()
|
|
<< ";" << endl;
|
|
*/
|
|
out << "}" << endl;
|
|
}
|
|
|
|
void Document::exportGraphviz(std::ostream& out) const
|
|
{
|
|
/* Type defs for a graph with graphviz attributes */
|
|
using GraphvizAttributes = std::map<std::string, std::string>;
|
|
using Graph = boost::subgraph< adjacency_list<vecS, vecS, directedS,
|
|
property<vertex_attribute_t, GraphvizAttributes>,
|
|
property<edge_index_t, int, property<edge_attribute_t, GraphvizAttributes> >,
|
|
property<graph_name_t, std::string,
|
|
property<graph_graph_attribute_t, GraphvizAttributes,
|
|
property<graph_vertex_attribute_t, GraphvizAttributes,
|
|
property<graph_edge_attribute_t, GraphvizAttributes>
|
|
> > > > >;
|
|
|
|
/**
|
|
* @brief The GraphCreator class
|
|
*
|
|
* This class creates the dependency graph for a document.
|
|
*
|
|
*/
|
|
|
|
class GraphCreator {
|
|
public:
|
|
|
|
explicit GraphCreator(struct DocumentP* _d) : d(_d), vertex_no(0), seed(std::random_device()()), distribution(0,255) {
|
|
build();
|
|
}
|
|
|
|
const Graph & getGraph() const { return DepList; }
|
|
|
|
private:
|
|
|
|
void build() {
|
|
// Set attribute(s) for main graph
|
|
get_property(DepList, graph_graph_attribute)["compound"] = "true";
|
|
|
|
addSubgraphs();
|
|
buildAdjacencyList();
|
|
addEdges();
|
|
markCycles();
|
|
markOutOfScopeLinks();
|
|
}
|
|
|
|
/**
|
|
* @brief getId returns a canonical string for a DocumentObject.
|
|
* @param docObj Document object to get an ID from
|
|
* @return A string
|
|
*/
|
|
|
|
std::string getId(const DocumentObject * docObj) {
|
|
return std::string((docObj)->getDocument()->getName()) + "#" + docObj->getNameInDocument();
|
|
}
|
|
|
|
/**
|
|
* @brief getId returns a canonical string for an ObjectIdentifier;
|
|
* @param path
|
|
* @return A string
|
|
*/
|
|
|
|
std::string getId(const ObjectIdentifier & path) {
|
|
DocumentObject * docObj = path.getDocumentObject();
|
|
if (!docObj)
|
|
return std::string();
|
|
|
|
return std::string((docObj)->getDocument()->getName()) + "#" + docObj->getNameInDocument() + "." + path.getPropertyName() + path.getSubPathStr();
|
|
}
|
|
|
|
std::string getClusterName(const DocumentObject * docObj) const {
|
|
return std::string("cluster") + docObj->getNameInDocument();
|
|
}
|
|
|
|
void setGraphLabel(Graph& g, const DocumentObject* obj) const {
|
|
std::string name(obj->getNameInDocument());
|
|
std::string label(obj->Label.getValue());
|
|
if (name == label)
|
|
get_property(g, graph_graph_attribute)["label"] = name;
|
|
else
|
|
get_property(g, graph_graph_attribute)["label"] = name + "\n(" + label + ")";
|
|
}
|
|
|
|
/**
|
|
* @brief setGraphAttributes Set graph attributes on a subgraph for a DocumentObject node.
|
|
* @param obj DocumentObject
|
|
*/
|
|
|
|
void setGraphAttributes(const DocumentObject * obj) {
|
|
assert(GraphList.find(obj) != GraphList.end());
|
|
get_property(*GraphList[obj], graph_name) = getClusterName(obj);
|
|
|
|
get_property(*GraphList[obj], graph_graph_attribute)["bgcolor"] = "#e0e0e0";
|
|
|
|
get_property(*GraphList[obj], graph_graph_attribute)["style"] = "rounded,filled";
|
|
setGraphLabel(*GraphList[obj], obj);
|
|
}
|
|
|
|
/**
|
|
* @brief setPropertyVertexAttributes Set vertex attributes for a Property node in a graph.
|
|
* @param g Graph
|
|
* @param vertex Property node
|
|
* @param name Name of node
|
|
*/
|
|
|
|
void setPropertyVertexAttributes(Graph & g, Vertex vertex, const std::string & name) {
|
|
get(vertex_attribute, g)[vertex]["label"] = name;
|
|
get(vertex_attribute, g)[vertex]["shape"] = "box";
|
|
get(vertex_attribute, g)[vertex]["style"] = "dashed";
|
|
get(vertex_attribute, g)[vertex]["fontsize"] = "8pt";
|
|
}
|
|
|
|
/**
|
|
* @brief addExpressionSubgraphIfNeeded Add a subgraph to the main graph if it is needed, i.e. there are defined at least one
|
|
* expression in the document object, or other objects are referencing properties in it.
|
|
* @param obj DocumentObject to assess.
|
|
* @param CSSubgraphs Boolean if the GeoFeatureGroups are created as subgraphs
|
|
*/
|
|
|
|
void addExpressionSubgraphIfNeeded(DocumentObject * obj, bool CSsubgraphs) {
|
|
|
|
auto expressions = obj->ExpressionEngine.getExpressions();
|
|
|
|
if (!expressions.empty()) {
|
|
|
|
Graph* graph = nullptr;
|
|
graph = &DepList;
|
|
if (CSsubgraphs) {
|
|
auto group = GeoFeatureGroupExtension::getGroupOfObject(obj);
|
|
if (group) {
|
|
auto it = GraphList.find(group);
|
|
if (it != GraphList.end())
|
|
graph = it->second;
|
|
}
|
|
}
|
|
|
|
// If documentObject has an expression, create a subgraph for it
|
|
if (graph && !GraphList[obj]) {
|
|
GraphList[obj] = &graph->create_subgraph();
|
|
setGraphAttributes(obj);
|
|
}
|
|
|
|
// Create subgraphs for all document objects that it depends on; it will depend on some property there
|
|
for (const auto &expr : expressions) {
|
|
std::map<ObjectIdentifier, bool> deps;
|
|
|
|
expr.second->getIdentifiers(deps);
|
|
|
|
for (const auto &dep : deps) {
|
|
if (dep.second)
|
|
continue;
|
|
DocumentObject * o = dep.first.getDocumentObject();
|
|
|
|
// Doesn't exist already?
|
|
if (o && !GraphList[o]) {
|
|
|
|
if (CSsubgraphs) {
|
|
auto group = GeoFeatureGroupExtension::getGroupOfObject(o);
|
|
auto graph2 = group ? GraphList[group] : &DepList;
|
|
if (graph2) {
|
|
GraphList[o] = &graph2->create_subgraph();
|
|
setGraphAttributes(o);
|
|
}
|
|
}
|
|
else if (graph) {
|
|
GraphList[o] = &graph->create_subgraph();
|
|
setGraphAttributes(o);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief add Add @docObj to the graph, including all expressions (and dependencies) it includes.
|
|
* @param docObj The document object to add.
|
|
* @param name Name of node.
|
|
*/
|
|
|
|
void add(DocumentObject *docObj, const std::string &name, const std::string &label, bool CSSubgraphs)
|
|
{
|
|
|
|
//don't add objects twice
|
|
if (std::find(objects.begin(), objects.end(), docObj) != objects.end())
|
|
return;
|
|
|
|
//find the correct graph to add the vertex to. Check first expression graphs, afterwards
|
|
//the parent CS and origin graphs
|
|
Graph *sgraph = GraphList[docObj];
|
|
if (CSSubgraphs) {
|
|
if (!sgraph) {
|
|
auto group = GeoFeatureGroupExtension::getGroupOfObject(docObj);
|
|
if (group) {
|
|
if (docObj->isDerivedFrom(App::OriginFeature::getClassTypeId()))
|
|
sgraph = GraphList[group->getExtensionByType<OriginGroupExtension>()->Origin.getValue()];
|
|
else
|
|
sgraph = GraphList[group];
|
|
}
|
|
}
|
|
if (!sgraph) {
|
|
if (docObj->isDerivedFrom(OriginFeature::getClassTypeId()))
|
|
sgraph = GraphList[static_cast<OriginFeature *>(docObj)->getOrigin()];
|
|
}
|
|
}
|
|
if (!sgraph)
|
|
sgraph = &DepList;
|
|
|
|
// Keep a list of all added document objects.
|
|
objects.insert(docObj);
|
|
|
|
// Add vertex to graph. Track global and local index
|
|
LocalVertexList[getId(docObj)] = add_vertex(*sgraph);
|
|
GlobalVertexList[getId(docObj)] = vertex_no++;
|
|
|
|
// If node is in main graph, style it with rounded corners. If not, make it invisible.
|
|
if (!GraphList[docObj]) {
|
|
get(vertex_attribute, *sgraph)[LocalVertexList[getId(docObj)]]["style"] = "filled";
|
|
get(vertex_attribute, *sgraph)[LocalVertexList[getId(docObj)]]["shape"] = "Mrecord";
|
|
// Set node label
|
|
if (name == label)
|
|
get(vertex_attribute, *sgraph)[LocalVertexList[getId(docObj)]]["label"] = name;
|
|
else
|
|
get(vertex_attribute, *sgraph)[LocalVertexList[getId(docObj)]]["label"] = name + "\n(" + label + ")";
|
|
}
|
|
else {
|
|
get(vertex_attribute, *sgraph)[LocalVertexList[getId(docObj)]]["style"] = "invis";
|
|
get(vertex_attribute, *sgraph)[LocalVertexList[getId(docObj)]]["fixedsize"] = "true";
|
|
get(vertex_attribute, *sgraph)[LocalVertexList[getId(docObj)]]["width"] = "0";
|
|
get(vertex_attribute, *sgraph)[LocalVertexList[getId(docObj)]]["height"] = "0";
|
|
}
|
|
|
|
// Add expressions and its dependencies
|
|
auto expressions{docObj->ExpressionEngine.getExpressions()};
|
|
for (const auto &expr : expressions) {
|
|
auto found = std::as_const(GlobalVertexList).find(getId(expr.first));
|
|
if (found == GlobalVertexList.end()) {
|
|
int vid = LocalVertexList[getId(expr.first)] = add_vertex(*sgraph);
|
|
GlobalVertexList[getId(expr.first)] = vertex_no++;
|
|
setPropertyVertexAttributes(*sgraph, vid, expr.first.toString());
|
|
}
|
|
}
|
|
|
|
// Add all dependencies
|
|
for (const auto &expression : expressions) {
|
|
// Get dependencies
|
|
std::map<ObjectIdentifier, bool> deps;
|
|
expression.second->getIdentifiers(deps);
|
|
|
|
// Create subgraphs for all documentobjects that it depends on; it will depend on some property there
|
|
for (const auto &dep : deps) {
|
|
if (dep.second) {
|
|
continue;
|
|
}
|
|
DocumentObject *depObjDoc = dep.first.getDocumentObject();
|
|
auto found = GlobalVertexList.find(getId(dep.first));
|
|
|
|
if (found == GlobalVertexList.end()) {
|
|
Graph *depSgraph = GraphList[depObjDoc] ? GraphList[depObjDoc] : &DepList;
|
|
|
|
LocalVertexList[getId(dep.first)] = add_vertex(*depSgraph);
|
|
GlobalVertexList[getId(dep.first)] = vertex_no++;
|
|
setPropertyVertexAttributes(*depSgraph, LocalVertexList[getId(dep.first)], dep.first.getPropertyName() + dep.first.getSubPathStr());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void recursiveCSSubgraphs(DocumentObject* cs, DocumentObject* parent) {
|
|
|
|
auto graph = parent ? GraphList[parent] : &DepList;
|
|
// check if the value for the key 'parent' is null
|
|
if (!graph)
|
|
return;
|
|
auto& sub = graph->create_subgraph();
|
|
GraphList[cs] = ⊂
|
|
get_property(sub, graph_name) = getClusterName(cs);
|
|
|
|
//build random color string
|
|
std::stringstream stream;
|
|
stream << "#" << std::setfill('0') << std::setw(2)<< std::hex << distribution(seed)
|
|
<< std::setfill('0') << std::setw(2)<< std::hex << distribution(seed)
|
|
<< std::setfill('0') << std::setw(2)<< std::hex << distribution(seed) << 80;
|
|
std::string result(stream.str());
|
|
|
|
get_property(sub, graph_graph_attribute)["bgcolor"] = result;
|
|
get_property(sub, graph_graph_attribute)["style"] = "rounded,filled";
|
|
setGraphLabel(sub, cs);
|
|
|
|
for(auto obj : cs->getOutList()) {
|
|
if (obj->hasExtension(GeoFeatureGroupExtension::getExtensionClassTypeId())) {
|
|
// in case of dependencies loops check if obj is already part of the
|
|
// map to avoid infinite recursions
|
|
auto it = GraphList.find(obj);
|
|
if (it == GraphList.end())
|
|
recursiveCSSubgraphs(obj, cs);
|
|
}
|
|
}
|
|
|
|
//setup the origin if available
|
|
if(cs->hasExtension(App::OriginGroupExtension::getExtensionClassTypeId())) {
|
|
auto origin = cs->getExtensionByType<OriginGroupExtension>()->Origin.getValue();
|
|
if (!origin) {
|
|
std::cerr << "Origin feature not found" << std::endl;
|
|
return;
|
|
}
|
|
auto& osub = sub.create_subgraph();
|
|
GraphList[origin] = &osub;
|
|
get_property(osub, graph_name) = getClusterName(origin);
|
|
get_property(osub, graph_graph_attribute)["bgcolor"] = "none";
|
|
setGraphLabel(osub, origin);
|
|
}
|
|
}
|
|
|
|
void addSubgraphs() {
|
|
|
|
ParameterGrp::handle depGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/DependencyGraph");
|
|
bool CSSubgraphs = depGrp->GetBool("GeoFeatureSubgraphs", true);
|
|
|
|
if(CSSubgraphs) {
|
|
//first build up the coordinate system subgraphs
|
|
for (auto objectIt : d->objectArray) {
|
|
// do not require an empty inlist (#0003465: Groups breaking dependency graph)
|
|
// App::Origin now has the GeoFeatureGroupExtension but it should not move its
|
|
// group symbol outside its parent
|
|
if (!objectIt->isDerivedFrom(Origin::getClassTypeId()) &&
|
|
objectIt->hasExtension(GeoFeatureGroupExtension::getExtensionClassTypeId()))
|
|
recursiveCSSubgraphs(objectIt, nullptr);
|
|
}
|
|
}
|
|
|
|
// Internal document objects
|
|
for (auto It = d->objectMap.begin(); It != d->objectMap.end();++It)
|
|
addExpressionSubgraphIfNeeded(It->second, CSSubgraphs);
|
|
|
|
// Add external document objects
|
|
for (auto It = d->objectMap.begin(); It != d->objectMap.end();++It) {
|
|
std::vector<DocumentObject*> OutList = It->second->getOutList();
|
|
for (std::vector<DocumentObject*>::const_iterator It2=OutList.begin();It2!=OutList.end();++It2) {
|
|
if (*It2) {
|
|
std::map<std::string,Vertex>::const_iterator item = GlobalVertexList.find(getId(*It2));
|
|
|
|
if (item == GlobalVertexList.end())
|
|
addExpressionSubgraphIfNeeded(*It2, CSSubgraphs);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Filling up the adjacency List
|
|
void buildAdjacencyList() {
|
|
|
|
ParameterGrp::handle depGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/DependencyGraph");
|
|
bool CSSubgraphs = depGrp->GetBool("GeoFeatureSubgraphs", true);
|
|
|
|
// Add internal document objects
|
|
for (auto It = d->objectMap.begin(); It != d->objectMap.end();++It)
|
|
add(It->second, It->second->getNameInDocument(), It->second->Label.getValue(), CSSubgraphs);
|
|
|
|
// Add external document objects
|
|
for (auto It = d->objectMap.begin(); It != d->objectMap.end();++It) {
|
|
std::vector<DocumentObject*> OutList = It->second->getOutList();
|
|
for (std::vector<DocumentObject*>::const_iterator It2=OutList.begin();It2!=OutList.end();++It2) {
|
|
if (*It2) {
|
|
std::map<std::string,Vertex>::const_iterator item = GlobalVertexList.find(getId(*It2));
|
|
|
|
if (item == GlobalVertexList.end())
|
|
add(*It2,
|
|
std::string((*It2)->getDocument()->getName()) + "#" + (*It2)->getNameInDocument(),
|
|
std::string((*It2)->getDocument()->getName()) + "#" + (*It2)->Label.getValue(),
|
|
CSSubgraphs);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void addEdges() {
|
|
// Get edge properties for main graph
|
|
const boost::property_map<Graph, boost::edge_attribute_t>::type& edgeAttrMap = boost::get(boost::edge_attribute, DepList);
|
|
|
|
// Track edges between document objects connected by expression dependencies
|
|
std::set<std::pair<const DocumentObject*, const DocumentObject*> > existingEdges;
|
|
|
|
// Add edges between properties
|
|
for (const auto &docObj : objects) {
|
|
|
|
// Add expressions and its dependencies
|
|
auto expressions = docObj->ExpressionEngine.getExpressions();
|
|
for (const auto &expr : expressions) {
|
|
std::map<ObjectIdentifier, bool> deps;
|
|
expr.second->getIdentifiers(deps);
|
|
|
|
// Create subgraphs for all documentobjects that it depends on; it will depend on some property there
|
|
for (const auto &dep : deps) {
|
|
if (dep.second)
|
|
continue;
|
|
DocumentObject * depObjDoc = dep.first.getDocumentObject();
|
|
Edge edge;
|
|
bool inserted;
|
|
|
|
tie(edge, inserted) = add_edge(GlobalVertexList[getId(expr.first)], GlobalVertexList[getId(dep.first)], DepList);
|
|
|
|
// Add this edge to the set of all expression generated edges
|
|
existingEdges.insert(std::make_pair(docObj, depObjDoc));
|
|
|
|
// Edges between properties should be a bit smaller, and dashed
|
|
edgeAttrMap[edge]["arrowsize"] = "0.5";
|
|
edgeAttrMap[edge]["style"] = "dashed";
|
|
}
|
|
}
|
|
}
|
|
|
|
ParameterGrp::handle depGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/DependencyGraph");
|
|
bool omitGeoFeatureGroups = depGrp->GetBool("GeoFeatureSubgraphs", true);
|
|
|
|
// Add edges between document objects
|
|
for (auto It = d->objectMap.begin(); It != d->objectMap.end();++It) {
|
|
|
|
if(omitGeoFeatureGroups) {
|
|
//coordinate systems are represented by subgraphs
|
|
if(It->second->hasExtension(GeoFeatureGroupExtension::getExtensionClassTypeId()))
|
|
continue;
|
|
|
|
//as well as origins
|
|
if(It->second->isDerivedFrom(Origin::getClassTypeId()))
|
|
continue;
|
|
}
|
|
|
|
std::map<DocumentObject*, int> dups;
|
|
std::vector<DocumentObject*> OutList = It->second->getOutList();
|
|
const DocumentObject * docObj = It->second;
|
|
|
|
for (std::vector<DocumentObject*>::const_iterator It2=OutList.begin();It2!=OutList.end();++It2) {
|
|
if (*It2) {
|
|
|
|
// Count duplicate edges
|
|
bool inserted = edge(GlobalVertexList[getId(docObj)], GlobalVertexList[getId(*It2)], DepList).second;
|
|
if (inserted) {
|
|
dups[*It2]++;
|
|
continue;
|
|
}
|
|
|
|
// Skip edge if an expression edge already exists
|
|
if (existingEdges.find(std::make_pair(docObj, *It2)) != existingEdges.end())
|
|
continue;
|
|
|
|
// Add edge
|
|
|
|
Edge edge;
|
|
|
|
tie(edge, inserted) = add_edge(GlobalVertexList[getId(docObj)], GlobalVertexList[getId(*It2)], DepList);
|
|
|
|
// Set properties to make arrows go between subgraphs if needed
|
|
if (GraphList[docObj])
|
|
edgeAttrMap[edge]["ltail"] = getClusterName(docObj);
|
|
if (GraphList[*It2])
|
|
edgeAttrMap[edge]["lhead"] = getClusterName(*It2);
|
|
}
|
|
}
|
|
|
|
// Set labels for duplicate edges
|
|
for (std::map<DocumentObject*, int>::const_iterator It2 = dups.begin(); It2 != dups.end(); ++It2) {
|
|
Edge e(edge(GlobalVertexList[getId(It->second)], GlobalVertexList[getId(It2->first)], DepList).first);
|
|
std::stringstream s;
|
|
s << " " << (It2->second + 1) << "x";
|
|
edgeAttrMap[e]["label"] = s.str();
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
using EdgeMap = std::unordered_multimap<Vertex, Edge>;
|
|
|
|
void removeEdges(EdgeMap & in_edges,
|
|
EdgeMap & out_edges,
|
|
std::pair<EdgeMap::iterator, EdgeMap::iterator > i_pair,
|
|
std::function<Vertex (const Edge&)> select_vertex) {
|
|
auto i = i_pair.first;
|
|
|
|
while (i != i_pair.second) {
|
|
// Remove from in edges in other nodes
|
|
auto in_i_pair = in_edges.equal_range(select_vertex(i->second));
|
|
auto in_i = in_i_pair.first;
|
|
|
|
while (in_i != in_i_pair.second) {
|
|
if (in_i->second == i->second)
|
|
in_i = in_edges.erase(in_i);
|
|
else
|
|
++in_i;
|
|
}
|
|
|
|
// Remove node from out_edges
|
|
i = out_edges.erase(i);
|
|
}
|
|
}
|
|
|
|
#if defined(__clang__)
|
|
#elif defined (__GNUC__)
|
|
# pragma GCC diagnostic push
|
|
# pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
|
|
#endif
|
|
|
|
void markCycles() {
|
|
bool changed = true;
|
|
std::unordered_set<Vertex> in_use;
|
|
EdgeMap in_edges;
|
|
EdgeMap out_edges;
|
|
|
|
// Add all vertices to the in_use set
|
|
graph_traits<Graph>::vertex_iterator vi, vi_end;
|
|
tie(vi, vi_end) = vertices(DepList);
|
|
for (; vi != vi_end; ++vi)
|
|
in_use.insert(*vi);
|
|
|
|
// Add all edges to the in_edges and out_edges multimaps
|
|
graph_traits<Graph>::edge_iterator ei, ei_end;
|
|
tie(ei, ei_end) = edges(DepList);
|
|
for (; ei != ei_end; ++ei) {
|
|
in_edges.insert(std::make_pair<Vertex, Edge>(target(*ei, DepList), *ei));
|
|
out_edges.insert(std::make_pair<Vertex, Edge>(source(*ei, DepList), *ei));
|
|
}
|
|
|
|
// Go through dependency graph and remove nodes with either no input or output
|
|
// A normal DAG without any cycles will get all its edges removed.
|
|
// If one or more cycles exist in the graph, there will remain nodes with
|
|
// both in and out edges.
|
|
|
|
while (changed) {
|
|
auto uvi = in_use.begin();
|
|
auto uvi_end = in_use.end();
|
|
|
|
// Flag that no changes has occurred so far. If the loop goes through
|
|
// without this flag being set to true, we are done.
|
|
changed = false;
|
|
|
|
while (uvi != uvi_end) {
|
|
auto i_in_deg_pair = in_edges.equal_range(*uvi);
|
|
auto i_out_deg_pair = out_edges.equal_range(*uvi);
|
|
|
|
if (i_in_deg_pair.first == in_edges.end() && i_out_deg_pair.first == out_edges.end()) {
|
|
uvi = in_use.erase(uvi);
|
|
continue;
|
|
}
|
|
|
|
// Remove out edges of nodes that don't have a single edge in
|
|
if (i_in_deg_pair.first == in_edges.end()) {
|
|
removeEdges(in_edges, out_edges, i_out_deg_pair, [&](Edge e) { return target(e, DepList); });
|
|
changed = true;
|
|
i_out_deg_pair = out_edges.equal_range(*uvi);
|
|
}
|
|
|
|
// Remove in edges of nodes that don't have a single edge out
|
|
if (i_out_deg_pair.first == out_edges.end()) {
|
|
removeEdges(out_edges, in_edges, i_in_deg_pair, [&](Edge e) { return source(e, DepList); });
|
|
changed = true;
|
|
}
|
|
|
|
++uvi;
|
|
}
|
|
}
|
|
|
|
// Update colors in graph
|
|
const boost::property_map<Graph, boost::edge_attribute_t>::type& edgeAttrMap = boost::get(boost::edge_attribute, DepList);
|
|
for (auto ei = out_edges.begin(), ei_end = out_edges.end(); ei != ei_end; ++ei)
|
|
edgeAttrMap[ei->second]["color"] = "red";
|
|
}
|
|
|
|
#if defined(__clang__)
|
|
#elif defined (__GNUC__)
|
|
# pragma GCC diagnostic pop
|
|
#endif
|
|
|
|
void markOutOfScopeLinks() {
|
|
const boost::property_map<Graph, boost::edge_attribute_t>::type& edgeAttrMap = boost::get(boost::edge_attribute, DepList);
|
|
|
|
for( auto obj : objects) {
|
|
|
|
std::vector<App::DocumentObject*> invalids;
|
|
GeoFeatureGroupExtension::getInvalidLinkObjects(obj, invalids);
|
|
//isLinkValid returns true for non-link properties
|
|
for(auto linkedObj : invalids) {
|
|
|
|
auto res = edge(GlobalVertexList[getId(obj)], GlobalVertexList[getId(linkedObj)], DepList);
|
|
if(res.second)
|
|
edgeAttrMap[res.first]["color"] = "red";
|
|
}
|
|
}
|
|
}
|
|
|
|
const struct DocumentP* d;
|
|
Graph DepList;
|
|
int vertex_no;
|
|
std::map<std::string, Vertex> LocalVertexList;
|
|
std::map<std::string, Vertex> GlobalVertexList;
|
|
std::set<const DocumentObject*> objects;
|
|
std::map<const DocumentObject*, Graph*> GraphList;
|
|
//random color generation
|
|
std::mt19937 seed;
|
|
std::uniform_int_distribution<int> distribution;
|
|
};
|
|
|
|
GraphCreator g(d);
|
|
|
|
boost::write_graphviz(out, g.getGraph());
|
|
}
|
|
|
|
//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()
|
|
{/*
|
|
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; */
|
|
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);
|
|
return (*rit)->getID();
|
|
}
|
|
if(pos>=mRedoTransactions.size())
|
|
return 0;
|
|
auto rit = mRedoTransactions.rbegin();
|
|
for(;pos;++rit,--pos);
|
|
return (*rit)->getID();
|
|
}
|
|
|
|
bool Document::isTransactionEmpty() const
|
|
{
|
|
if (d->activeUndoTransaction) {
|
|
// Transactions are now only created when there are actual changes.
|
|
// Empty transaction is now significant for marking external changes. It
|
|
// is used to match ID with transactions in external documents and
|
|
// trigger undo/redo there.
|
|
|
|
// return d->activeUndoTransaction->isEmpty();
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void Document::clearDocument()
|
|
{
|
|
d->activeObject = nullptr;
|
|
|
|
if (!d->objectArray.empty()) {
|
|
GetApplication().signalDeleteDocument(*this);
|
|
d->clearDocument();
|
|
GetApplication().signalNewDocument(*this,false);
|
|
}
|
|
|
|
Base::FlagToggler<> flag(globalIsRestoring, false);
|
|
|
|
setStatus(Document::PartialDoc,false);
|
|
|
|
d->clearRecomputeLog();
|
|
d->objectArray.clear();
|
|
d->objectMap.clear();
|
|
d->objectIdMap.clear();
|
|
d->lastObjectId = 0;
|
|
}
|
|
|
|
|
|
void Document::clearUndos()
|
|
{
|
|
if(isPerformingTransaction() || d->committing) {
|
|
FC_ERR("Cannot clear undos while transacting");
|
|
return;
|
|
}
|
|
|
|
if (d->activeUndoTransaction)
|
|
_commitTransaction(true);
|
|
|
|
mUndoMap.clear();
|
|
|
|
// When cleaning up the undo stack we must delete the transactions from front
|
|
// to back because a document object can appear in several transactions but
|
|
// once removed from the document the object can never ever appear in any later
|
|
// transaction. Since the document object may be also deleted when the transaction
|
|
// is deleted we must make sure not access an object once it's destroyed. Thus, we
|
|
// go from front to back and not the other way round.
|
|
while (!mUndoTransactions.empty()) {
|
|
delete mUndoTransactions.front();
|
|
mUndoTransactions.pop_front();
|
|
}
|
|
//while (!mUndoTransactions.empty()) {
|
|
// delete mUndoTransactions.back();
|
|
// mUndoTransactions.pop_back();
|
|
//}
|
|
|
|
_clearRedos();
|
|
}
|
|
|
|
int Document::getAvailableUndos(int id) const
|
|
{
|
|
if(id) {
|
|
auto it = mUndoMap.find(id);
|
|
if(it == mUndoMap.end())
|
|
return 0;
|
|
int i = 0;
|
|
if(d->activeUndoTransaction) {
|
|
++i;
|
|
if(d->activeUndoTransaction->getID()==id)
|
|
return i;
|
|
}
|
|
auto rit = mUndoTransactions.rbegin();
|
|
for(;rit!=mUndoTransactions.rend()&&*rit!=it->second;++rit)
|
|
++i;
|
|
assert(rit!=mUndoTransactions.rend());
|
|
return i+1;
|
|
}
|
|
if (d->activeUndoTransaction)
|
|
return static_cast<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);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Document::onBeforeChangeProperty(const TransactionalObject *Who, const Property *What)
|
|
{
|
|
if(Who->isDerivedFrom(App::DocumentObject::getClassTypeId()))
|
|
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 *name)
|
|
: myName(name)
|
|
{
|
|
// Remark: In a constructor we should never increment a Python object as we cannot be sure
|
|
// if the Python interpreter gets a reference of it. E.g. if we increment but Python don't
|
|
// get a reference then the object wouldn't get deleted in the destructor.
|
|
// So, we must increment only if the interpreter gets a reference.
|
|
// Remark: We force the document Python object to own the DocumentPy instance, thus we don't
|
|
// have to care about ref counting any more.
|
|
d = new DocumentP;
|
|
d->DocumentPythonObject = Py::Object(new DocumentPy(this), true);
|
|
|
|
#ifdef FC_LOGUPDATECHAIN
|
|
Console().Log("+App::Document: %p\n", this);
|
|
#endif
|
|
std::string CreationDateString = Base::TimeInfo::currentDateTimeString();
|
|
std::string Author = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")->GetASCII("prefAuthor", "");
|
|
std::string AuthorComp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")->GetASCII("prefCompany", "");
|
|
ADD_PROPERTY_TYPE(Label, ("Unnamed"), 0, Prop_None, "The name of the document");
|
|
ADD_PROPERTY_TYPE(FileName, (""), 0, PropertyType(Prop_Transient | Prop_ReadOnly), "The path to the file where the document is saved to");
|
|
ADD_PROPERTY_TYPE(CreatedBy, (Author.c_str()), 0, Prop_None, "The creator of the document");
|
|
ADD_PROPERTY_TYPE(CreationDate, (CreationDateString.c_str()), 0, Prop_ReadOnly, "Date of creation");
|
|
ADD_PROPERTY_TYPE(LastModifiedBy, (""), 0, Prop_None, 0);
|
|
ADD_PROPERTY_TYPE(LastModifiedDate, ("Unknown"), 0, Prop_ReadOnly, "Date of last modification");
|
|
ADD_PROPERTY_TYPE(Company, (AuthorComp.c_str()), 0, Prop_None, "Additional tag to save the name of the company");
|
|
ADD_PROPERTY_TYPE(Comment, (""), 0, Prop_None, "Additional tag to save a comment");
|
|
ADD_PROPERTY_TYPE(Meta, (), 0, Prop_None, "Map with additional meta information");
|
|
ADD_PROPERTY_TYPE(Material, (), 0, Prop_None, "Map with material properties");
|
|
// create the uuid for the document
|
|
Base::Uuid id;
|
|
ADD_PROPERTY_TYPE(Id, (""), 0, Prop_None, "ID of the document");
|
|
ADD_PROPERTY_TYPE(Uid, (id), 0, Prop_ReadOnly, "UUID of the document");
|
|
|
|
// license stuff
|
|
ADD_PROPERTY_TYPE(License, ("CC-BY 3.0"), 0, Prop_None, "License string of the Item");
|
|
ADD_PROPERTY_TYPE(LicenseURL, ("https://creativecommons.org/licenses/by/3.0/"), 0, Prop_None, "URL to the license text/contract");
|
|
|
|
// license stuff
|
|
int licenseId = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")->GetInt("prefLicenseType", 0);
|
|
std::string license;
|
|
std::string licenseUrl;
|
|
switch (licenseId) {
|
|
case 0:
|
|
license = "All rights reserved";
|
|
licenseUrl = "https://en.wikipedia.org/wiki/All_rights_reserved";
|
|
break;
|
|
case 1:
|
|
license = "Creative Commons Attribution";
|
|
licenseUrl = "https://creativecommons.org/licenses/by/4.0/";
|
|
break;
|
|
case 2:
|
|
license = "Creative Commons Attribution-ShareAlike";
|
|
licenseUrl = "https://creativecommons.org/licenses/by-sa/4.0/";
|
|
break;
|
|
case 3:
|
|
license = "Creative Commons Attribution-NoDerivatives";
|
|
licenseUrl = "https://creativecommons.org/licenses/by-nd/4.0/";
|
|
break;
|
|
case 4:
|
|
license = "Creative Commons Attribution-NonCommercial";
|
|
licenseUrl = "https://creativecommons.org/licenses/by-nc/4.0/";
|
|
break;
|
|
case 5:
|
|
license = "Creative Commons Attribution-NonCommercial-ShareAlike";
|
|
licenseUrl = "https://creativecommons.org/licenses/by-nc-sa/4.0/";
|
|
break;
|
|
case 6:
|
|
license = "Creative Commons Attribution-NonCommercial-NoDerivatives";
|
|
licenseUrl = "https://creativecommons.org/licenses/by-nc-nd/4.0/";
|
|
break;
|
|
case 7:
|
|
license = "Public Domain";
|
|
licenseUrl = "https://en.wikipedia.org/wiki/Public_domain";
|
|
break;
|
|
case 8:
|
|
license = "FreeArt";
|
|
licenseUrl = "https://artlibre.org/licence/lal";
|
|
break;
|
|
default:
|
|
license = "Other";
|
|
break;
|
|
}
|
|
|
|
licenseUrl = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")->GetASCII("prefLicenseUrl", licenseUrl.c_str());
|
|
|
|
ADD_PROPERTY_TYPE(License, (license.c_str()), 0, Prop_None, "License string of the Item");
|
|
ADD_PROPERTY_TYPE(LicenseURL, (licenseUrl.c_str()), 0, Prop_None, "URL to the license text/contract");
|
|
ADD_PROPERTY_TYPE(ShowHidden, (false), 0, PropertyType(Prop_None),
|
|
"Whether to show hidden object items in the tree view");
|
|
|
|
// this creates and sets 'TransientDir' in onChanged()
|
|
ADD_PROPERTY_TYPE(TransientDir, (""), 0, PropertyType(Prop_Transient | Prop_ReadOnly),
|
|
"Transient directory, where the files live while the document is open");
|
|
ADD_PROPERTY_TYPE(Tip, (nullptr), 0, PropertyType(Prop_Transient),
|
|
"Link of the tip object of the document");
|
|
ADD_PROPERTY_TYPE(TipName, (""), 0, PropertyType(Prop_Hidden | Prop_ReadOnly),
|
|
"Link of the tip object of the document");
|
|
Uid.touch();
|
|
}
|
|
|
|
Document::~Document()
|
|
{
|
|
#ifdef FC_LOGUPDATECHAIN
|
|
Console().Log("-App::Document: %s %p\n",getName(), this);
|
|
#endif
|
|
|
|
try {
|
|
clearUndos();
|
|
}
|
|
catch (const boost::exception&) {
|
|
}
|
|
|
|
#ifdef FC_LOGUPDATECHAIN
|
|
Console().Log("-Delete Features of %s \n",getName());
|
|
#endif
|
|
|
|
d->clearDocument();
|
|
|
|
// Remark: The API of Py::Object has been changed to set whether the wrapper owns the passed
|
|
// Python object or not. In the constructor we forced the wrapper to own the object so we need
|
|
// not to dec'ref the Python object any more.
|
|
// But we must still invalidate the Python object because it doesn't need to be
|
|
// destructed right now because the interpreter can own several references to it.
|
|
Base::PyGILStateLocker lock;
|
|
Base::PyObjectBase* doc = static_cast<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 s;
|
|
QCryptographicHash hash(QCryptographicHash::Sha1);
|
|
hash.addData(filename.c_str(), filename.size());
|
|
s << App::Application::getUserCachePath() << App::Application::getExecutableName()
|
|
<< "_Doc_" << uuid
|
|
<< "_" << hash.result().toHex().left(6).constData()
|
|
<< "_" << QCoreApplication::applicationPid();
|
|
return s.str();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Exported functions
|
|
//--------------------------------------------------------------------------
|
|
|
|
void Document::Save (Base::Writer &writer) const
|
|
{
|
|
writer.Stream() << "<Document SchemaVersion=\"4\" ProgramVersion=\""
|
|
<< App::Application::Config()["BuildVersionMajor"] << "."
|
|
<< App::Application::Config()["BuildVersionMinor"] << "R"
|
|
<< App::Application::Config()["BuildRevision"]
|
|
<< "\" FileVersion=\"" << writer.getFileVersion() << "\">" << endl;
|
|
|
|
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->touchedObjs.clear();
|
|
setStatus(Document::PartialDoc,false);
|
|
|
|
reader.readElement("Document");
|
|
long scheme = reader.getAttributeAsInteger("SchemaVersion");
|
|
reader.DocumentSchema = scheme;
|
|
if (reader.hasAttribute("ProgramVersion")) {
|
|
reader.ProgramVersion = reader.getAttribute("ProgramVersion");
|
|
} else {
|
|
reader.ProgramVersion = "pre-0.14";
|
|
}
|
|
if (reader.hasAttribute("FileVersion")) {
|
|
reader.FileVersion = reader.getAttributeAsUnsigned("FileVersion");
|
|
} else {
|
|
reader.FileVersion = 0;
|
|
}
|
|
|
|
// When this document was created the FileName and Label properties
|
|
// were set to the absolute path or file name, respectively. To save
|
|
// the document to the file it was loaded from or to show the file name
|
|
// in the tree view we must restore them after loading the file because
|
|
// they will be overridden.
|
|
// Note: This does not affect the internal name of the document in any way
|
|
// that is kept in Application.
|
|
std::string FilePath = FileName.getValue();
|
|
std::string DocLabel = Label.getValue();
|
|
|
|
// read the Document Properties, when reading in Uid the transient directory gets renamed automatically
|
|
PropertyContainer::Restore(reader);
|
|
|
|
// We must restore the correct 'FileName' property again because the stored
|
|
// value could be invalid.
|
|
FileName.setValue(FilePath.c_str());
|
|
Label.setValue(DocLabel.c_str());
|
|
|
|
// SchemeVersion "2"
|
|
if ( scheme == 2 ) {
|
|
// read the feature types
|
|
reader.readElement("Features");
|
|
Cnt = reader.getAttributeAsInteger("Count");
|
|
for (i=0 ;i<Cnt ;i++) {
|
|
reader.readElement("Feature");
|
|
string type = reader.getAttribute("type");
|
|
string name = reader.getAttribute("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.getAttributeAsInteger("Count");
|
|
for (i=0 ;i<Cnt ;i++) {
|
|
reader.readElement("Feature");
|
|
string name = reader.getAttribute("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");
|
|
}
|
|
|
|
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);
|
|
|
|
if(FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
|
|
for(auto o : obj) {
|
|
if(o && o->getNameInDocument()) {
|
|
FC_LOG("exporting " << o->getFullName());
|
|
if (!o->getPropertyByName("_ObjectUUID")) {
|
|
auto prop = static_cast<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() << "<Document SchemaVersion=\"4\" ProgramVersion=\""
|
|
<< App::Application::Config()["BuildVersionMajor"] << "."
|
|
<< App::Application::Config()["BuildVersionMinor"] << "R"
|
|
<< App::Application::Config()["BuildRevision"]
|
|
<< "\" 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();
|
|
}
|
|
|
|
#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.getAttributeAsInteger("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.getAttributeAsInteger(FC_ATTR_DEP_COUNT);
|
|
if(!dcount)
|
|
continue;
|
|
auto &info = deps[reader.getAttribute(FC_ATTR_DEP_OBJ_NAME)];
|
|
if(reader.hasAttribute(FC_ATTR_DEP_ALLOW_PARTIAL))
|
|
info.canLoadPartial = reader.getAttributeAsInteger(FC_ATTR_DEP_ALLOW_PARTIAL);
|
|
for(int j=0;j<dcount;++j) {
|
|
reader.readElement(FC_ELEMENT_OBJECT_DEP);
|
|
const char *name = reader.getAttribute(FC_ATTR_DEP_OBJ_NAME);
|
|
if(name && name[0])
|
|
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("type");
|
|
std::string name = reader.getAttribute("name");
|
|
std::string viewType = reader.hasAttribute("ViewType")?reader.getAttribute("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.getAttributeAsInteger("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.getAttributeAsInteger("Touched") != 0)
|
|
d->touchedObjs.insert(obj);
|
|
}
|
|
if (reader.hasAttribute("Invalid")) {
|
|
obj->setStatus(ObjectStatus::Error, reader.getAttributeAsInteger("Invalid") != 0);
|
|
if(obj->isError() && reader.hasAttribute("Error"))
|
|
d->addRecomputeLog(reader.getAttribute("Error"),obj);
|
|
}
|
|
}
|
|
}
|
|
catch (const Base::Exception& e) {
|
|
Base::Console().Error("Cannot create object '%s': (%s)\n", name.c_str(), e.what());
|
|
}
|
|
}
|
|
if(!testStatus(Status::Importing))
|
|
d->lastObjectId = lastId;
|
|
|
|
reader.readEndElement("Objects");
|
|
setStatus(Document::KeepTrailingDigits, keepDigits);
|
|
|
|
// read the features itself
|
|
reader.clearPartialRestoreDocumentObject();
|
|
reader.readElement("ObjectData");
|
|
Cnt = reader.getAttributeAsInteger("Count");
|
|
for (int i=0 ;i<Cnt ;i++) {
|
|
reader.readElement("Object");
|
|
std::string name = reader.getName(reader.getAttribute("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) {
|
|
d->touchedObjs.insert(obj);
|
|
obj->touch();
|
|
}
|
|
}
|
|
|
|
std::vector<App::DocumentObject*>
|
|
Document::importObjects(Base::XMLReader& reader)
|
|
{
|
|
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.getAttributeAsInteger("SchemaVersion");
|
|
reader.DocumentSchema = scheme;
|
|
if (reader.hasAttribute("ProgramVersion")) {
|
|
reader.ProgramVersion = reader.getAttribute("ProgramVersion");
|
|
} else {
|
|
reader.ProgramVersion = "pre-0.14";
|
|
}
|
|
if (reader.hasAttribute("FileVersion")) {
|
|
reader.FileVersion = reader.getAttributeAsUnsigned("FileVersion");
|
|
} else {
|
|
reader.FileVersion = 0;
|
|
}
|
|
|
|
std::vector<App::DocumentObject*> objs = readObjects(reader);
|
|
for(auto o : objs) {
|
|
if(o && o->getNameInDocument()) {
|
|
o->setStatus(App::ObjImporting,true);
|
|
FC_LOG("importing " << o->getFullName());
|
|
if (auto propUUID = Base::freecad_dynamic_cast<PropertyUUID>(
|
|
o->getPropertyByName("_ObjectUUID")))
|
|
{
|
|
auto propSource = Base::freecad_dynamic_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->getNameInDocument())
|
|
o->setStatus(App::ObjImporting,false);
|
|
}
|
|
|
|
return objs;
|
|
}
|
|
|
|
unsigned int Document::getMemSize () const
|
|
{
|
|
unsigned int size = 0;
|
|
|
|
// size of the DocObjects in the document
|
|
std::vector<DocumentObject*>::const_iterator it;
|
|
for (it = d->objectArray.begin(); it != d->objectArray.end(); ++it)
|
|
size += (*it)->getMemSize();
|
|
|
|
// size of the document properties...
|
|
size += PropertyContainer::getMemSize();
|
|
|
|
// Undo Redo size
|
|
size += getUndoMemSize();
|
|
|
|
return size;
|
|
}
|
|
|
|
static std::string checkFileName(const char *file) {
|
|
std::string fn(file);
|
|
|
|
// Append extension if missing. This option is added for security reason, so
|
|
// that the user won't accidentally overwrite other file that may be critical.
|
|
if(App::GetApplication().GetParameterGroupByPath
|
|
("User parameter:BaseApp/Preferences/Document")->GetBool("CheckExtension",true))
|
|
{
|
|
const char *ext = strrchr(file,'.');
|
|
if(!ext || !boost::iequals(ext+1,"fcstd")) {
|
|
if(ext && ext[1] == 0)
|
|
fn += "FCStd";
|
|
else
|
|
fn += ".FCStd";
|
|
}
|
|
}
|
|
return fn;
|
|
}
|
|
|
|
bool Document::saveAs(const char* _file)
|
|
{
|
|
std::string file = checkFileName(_file);
|
|
Base::FileInfo fi(file.c_str());
|
|
if (this->FileName.getStrValue() != file) {
|
|
this->FileName.setValue(file);
|
|
this->Label.setValue(fi.fileNamePure());
|
|
this->Uid.touch(); // this forces a rename of the transient directory
|
|
}
|
|
|
|
return save();
|
|
}
|
|
|
|
bool Document::saveCopy(const char* _file) const
|
|
{
|
|
std::string file = checkFileName(_file);
|
|
if (this->FileName.getStrValue() != file) {
|
|
bool result = saveToFile(file.c_str());
|
|
return result;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Save the document under the name it has been opened
|
|
bool Document::save ()
|
|
{
|
|
if(testStatus(Document::PartialDoc)) {
|
|
FC_ERR("Partial loaded document '" << Label.getValue() << "' cannot be saved");
|
|
// TODO We don't make this a fatal error and return 'true' to make it possible to
|
|
// save other documents that depends on this partial opened document. We need better
|
|
// handling to avoid touching partial documents.
|
|
return true;
|
|
}
|
|
|
|
if (*(FileName.getValue()) != '\0') {
|
|
// Save the name of the tip object in order to handle in Restore()
|
|
if (Tip.getValue()) {
|
|
TipName.setValue(Tip.getValue()->getNameInDocument());
|
|
}
|
|
|
|
std::string LastModifiedDateString = Base::TimeInfo::currentDateTimeString();
|
|
LastModifiedDate.setValue(LastModifiedDateString.c_str());
|
|
// set author if needed
|
|
bool saveAuthor = App::GetApplication().GetParameterGroupByPath
|
|
("User parameter:BaseApp/Preferences/Document")->GetBool("prefSetAuthorOnSave",false);
|
|
if (saveAuthor) {
|
|
std::string Author = App::GetApplication().GetParameterGroupByPath
|
|
("User parameter:BaseApp/Preferences/Document")->GetASCII("prefAuthor","");
|
|
LastModifiedBy.setValue(Author.c_str());
|
|
}
|
|
|
|
return saveToFile(FileName.getValue());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
namespace App {
|
|
// Helper class to handle different backup policies
|
|
class BackupPolicy {
|
|
public:
|
|
enum Policy {
|
|
Standard,
|
|
TimeStamp
|
|
};
|
|
BackupPolicy() {
|
|
policy = Standard;
|
|
numberOfFiles = 1;
|
|
useFCBakExtension = false;
|
|
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 (std::vector<Base::FileInfo>::iterator it = files.begin(); it != files.end(); ++it) {
|
|
std::string file = it->fileName();
|
|
if (file.substr(0,fn.length()) == fn) {
|
|
// starts with the same file name
|
|
std::string suf(file.substr(fn.length()));
|
|
if (!suf.empty()) {
|
|
std::string::size_type nPos = suf.find_first_not_of("0123456789");
|
|
if (nPos==std::string::npos) {
|
|
// store all backup files
|
|
backup.push_back(*it);
|
|
nSuff = std::max<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 (std::vector<Base::FileInfo>::iterator it = backup.begin(); it != backup.end(); ++it) {
|
|
if (it->lastModified() < del.lastModified())
|
|
del = *it;
|
|
}
|
|
|
|
del.deleteFile();
|
|
fn = del.filePath();
|
|
}
|
|
else {
|
|
// create a new backup file
|
|
std::stringstream str;
|
|
str << fi.filePath() << (nSuff + 1);
|
|
fn = str.str();
|
|
}
|
|
|
|
if (!fi.renameFile(fn.c_str()))
|
|
Base::Console().Warning("Cannot rename project file to backup file\n");
|
|
}
|
|
else {
|
|
fi.deleteFile();
|
|
}
|
|
}
|
|
|
|
Base::FileInfo tmp(sourcename);
|
|
if (!tmp.renameFile(targetname.c_str())) {
|
|
throw Base::FileException(
|
|
"Cannot rename tmp save file to project file", targetname);
|
|
}
|
|
}
|
|
void applyTimeStamp(const std::string& sourcename, const std::string& targetname) {
|
|
Base::FileInfo fi(targetname);
|
|
|
|
std::string fn = sourcename;
|
|
std::string ext = fi.extension();
|
|
std::string bn; // full path with no extension but with "."
|
|
std::string pbn; // base name of the project + "."
|
|
if (!ext.empty()) {
|
|
bn = fi.filePath().substr(0, fi.filePath().length() - ext.length());
|
|
pbn = fi.fileName().substr(0, fi.fileName().length() - ext.length());
|
|
}
|
|
else {
|
|
bn = fi.filePath() + ".";
|
|
pbn = fi.fileName() + ".";
|
|
}
|
|
|
|
bool backupManagementError = false; // Note error and report at the end
|
|
if (fi.exists()) {
|
|
if (numberOfFiles > 0) {
|
|
// replace . by - in format to avoid . between base name and extension
|
|
boost::replace_all(saveBackupDateFormat, ".", "-");
|
|
{
|
|
// Remove all extra backups
|
|
std::string fn = fi.fileName();
|
|
Base::FileInfo di(fi.dirPath());
|
|
std::vector<Base::FileInfo> backup;
|
|
std::vector<Base::FileInfo> files = di.getDirectoryContent();
|
|
for (std::vector<Base::FileInfo>::iterator it = files.begin(); it != files.end(); ++it) {
|
|
if (it->isFile()) {
|
|
std::string file = it->fileName();
|
|
std::string fext = it->extension();
|
|
std::string fextUp = fext;
|
|
std::transform(fextUp.begin(), fextUp.end(), fextUp.begin(),(int (*)(int))toupper);
|
|
// re-enforcing identification of the backup file
|
|
|
|
|
|
// old case : the name starts with the full name of the project and follows with numbers
|
|
if ((startsWith(file, fn) &&
|
|
(file.length() > fn.length()) &&
|
|
checkDigits(file.substr(fn.length()))) ||
|
|
// .FCBak case : The bame starts with the base name of the project + "."
|
|
// + complement with no "." + ".FCBak"
|
|
((fextUp == "FCBAK") && startsWith(file, pbn) &&
|
|
(checkValidComplement(file, pbn, fext)))) {
|
|
backup.push_back(*it);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!backup.empty() && (int)backup.size() >= numberOfFiles) {
|
|
std::sort (backup.begin(), backup.end(), fileComparisonByDate);
|
|
// delete the oldest backup file we found
|
|
// Base::FileInfo del = backup.front();
|
|
int nb = 0;
|
|
for (std::vector<Base::FileInfo>::iterator it = backup.begin(); it != backup.end(); ++it) {
|
|
nb++;
|
|
if (nb >= numberOfFiles) {
|
|
try {
|
|
if (!it->deleteFile()) {
|
|
backupManagementError = true;
|
|
Base::Console().Warning("Cannot remove backup file : %s\n", it->fileName().c_str());
|
|
}
|
|
}
|
|
catch (...) {
|
|
backupManagementError = true;
|
|
Base::Console().Warning("Cannot remove backup file : %s\n", it->fileName().c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
} //end remove backup
|
|
|
|
// create a new backup file
|
|
{
|
|
int ext = 1;
|
|
if (useFCBakExtension) {
|
|
std::stringstream str;
|
|
Base::TimeInfo ti = fi.lastModified();
|
|
time_t s =ti.getSeconds();
|
|
struct tm * timeinfo = localtime(& s);
|
|
char buffer[100];
|
|
|
|
strftime(buffer,sizeof(buffer),saveBackupDateFormat.c_str(),timeinfo);
|
|
str << bn << buffer ;
|
|
|
|
fn = str.str();
|
|
bool done = false;
|
|
|
|
if ((fn.empty()) || (fn[fn.length()-1] == ' ') || (fn[fn.length()-1] == '-')) {
|
|
if (fn[fn.length()-1] == ' ') {
|
|
fn = fn.substr(0,fn.length()-1);
|
|
}
|
|
}
|
|
else {
|
|
if (!renameFileNoErase(fi, fn+".FCBak")) {
|
|
fn = fn + "-";
|
|
}
|
|
else {
|
|
done = true;
|
|
}
|
|
}
|
|
|
|
if (!done) {
|
|
while (ext < numberOfFiles + 10) {
|
|
if (renameFileNoErase(fi, fn+std::to_string(ext)+".FCBak"))
|
|
break;
|
|
ext++;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// changed but simpler and solves also the delay sometimes introduced by google drive
|
|
while (ext < numberOfFiles + 10) {
|
|
// linux just replace the file if exists, and then the existence is to be tested before rename
|
|
if (renameFileNoErase(fi, fi.filePath()+std::to_string(ext)))
|
|
break;
|
|
ext++;
|
|
}
|
|
}
|
|
|
|
if (ext >= numberOfFiles + 10) {
|
|
Base::Console().Error("File not saved: Cannot rename project file to backup file\n");
|
|
//throw Base::FileException("File not saved: Cannot rename project file to backup file", fi);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
try {
|
|
fi.deleteFile();
|
|
}
|
|
catch (...) {
|
|
Base::Console().Warning("Cannot remove backup file: %s\n", fi.fileName().c_str());
|
|
backupManagementError = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
Base::FileInfo tmp(sourcename);
|
|
if (!tmp.renameFile(targetname.c_str())) {
|
|
throw Base::FileException(
|
|
"Save interrupted: Cannot rename temporary file to project file", tmp);
|
|
}
|
|
|
|
if (backupManagementError) {
|
|
throw Base::FileException("Warning: Save complete, but error while managing backup history.", fi);
|
|
}
|
|
}
|
|
static bool fileComparisonByDate(const Base::FileInfo& i,
|
|
const Base::FileInfo& j) {
|
|
return (i.lastModified()>j.lastModified());
|
|
}
|
|
bool startsWith(const std::string& st1,
|
|
const std::string& st2) const {
|
|
return st1.substr(0,st2.length()) == st2;
|
|
}
|
|
bool checkValidString (const std::string& cmpl, const boost::regex& e) const {
|
|
boost::smatch what;
|
|
bool res = boost::regex_search (cmpl,what,e);
|
|
return res;
|
|
}
|
|
bool checkValidComplement(const std::string& file, const std::string& pbn, const std::string& ext) const {
|
|
std::string cmpl = file.substr(pbn.length(),file.length()- pbn.length() - ext.length()-1);
|
|
boost::regex e (R"(^[^.]*$)");
|
|
return checkValidString(cmpl,e);
|
|
}
|
|
bool checkDigits (const std::string& cmpl) const {
|
|
boost::regex e (R"(^[0-9]*$)");
|
|
return checkValidString(cmpl,e);
|
|
}
|
|
bool renameFileNoErase(Base::FileInfo fi, const std::string& newName) {
|
|
// linux just replaces the file if it exists, so the existence is to be tested before rename
|
|
Base::FileInfo nf(newName);
|
|
if (!nf.exists()) {
|
|
return fi.renameFile(newName.c_str());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private:
|
|
Policy policy;
|
|
int numberOfFiles;
|
|
bool useFCBakExtension;
|
|
std::string saveBackupDateFormat;
|
|
};
|
|
}
|
|
|
|
bool Document::saveToFile(const char* filename) const
|
|
{
|
|
signalStartSave(*this, filename);
|
|
|
|
auto hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document");
|
|
int compression = hGrp->GetInt("CompressionLevel",3);
|
|
compression = Base::clamp<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.freecadweb.org for more information..." << endl
|
|
<< "-->" << endl;
|
|
Document::Save(writer);
|
|
|
|
// Special handling for Gui document.
|
|
signalSaveDocument(writer);
|
|
|
|
// write additional files
|
|
writer.writeFiles();
|
|
|
|
if (writer.hasErrors()) {
|
|
throw Base::FileException("Failed to write all data to file", tmp);
|
|
}
|
|
|
|
GetApplication().signalSaveDocument(*this);
|
|
}
|
|
|
|
if (policy) {
|
|
// if saving the project data succeeded rename to the actual file name
|
|
int count_bak = App::GetApplication().GetParameterGroupByPath
|
|
("User parameter:BaseApp/Preferences/Document")->GetInt("CountBackupFiles",1);
|
|
bool backup = App::GetApplication().GetParameterGroupByPath
|
|
("User parameter:BaseApp/Preferences/Document")->GetBool("CreateBackupFiles",true);
|
|
if (!backup) {
|
|
count_bak = -1;
|
|
}
|
|
bool useFCBakExtension = App::GetApplication().GetParameterGroupByPath
|
|
("User parameter:BaseApp/Preferences/Document")->GetBool("UseFCBakExtension",false);
|
|
std::string saveBackupDateFormat = App::GetApplication().GetParameterGroupByPath
|
|
("User parameter:BaseApp/Preferences/Document")->GetASCII("SaveBackupDateFormat","%Y%m%d-%H%M%S");
|
|
|
|
BackupPolicy policy;
|
|
if (useFCBakExtension) {
|
|
policy.setPolicy(BackupPolicy::TimeStamp);
|
|
policy.useBackupExtension(useFCBakExtension);
|
|
policy.setDateFormat(saveBackupDateFormat);
|
|
}
|
|
else {
|
|
policy.setPolicy(BackupPolicy::Standard);
|
|
}
|
|
policy.setNumberOfFiles(count_bak);
|
|
policy.apply(fn, nativePath);
|
|
}
|
|
|
|
signalFinishSave(*this, filename);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Document::isAnyRestoring() {
|
|
return globalIsRestoring;
|
|
}
|
|
|
|
// Open the document
|
|
void Document::restore (const char *filename,
|
|
bool delaySignal, const std::vector<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->objectArray.clear();
|
|
d->objectMap.clear();
|
|
d->objectIdMap.clear();
|
|
d->lastObjectId = 0;
|
|
|
|
if(signal) {
|
|
GetApplication().signalNewDocument(*this,true);
|
|
if(activeDoc == this)
|
|
GetApplication().setActiveDocument(this);
|
|
}
|
|
|
|
if(!filename)
|
|
filename = FileName.getValue();
|
|
Base::FileInfo fi(filename);
|
|
Base::ifstream file(fi, std::ios::in | std::ios::binary);
|
|
std::streambuf* buf = file.rdbuf();
|
|
std::streamoff size = buf->pubseekoff(0, std::ios::end, std::ios::in);
|
|
buf->pubseekoff(0, std::ios::beg, std::ios::in);
|
|
if (size < 22) // an empty zip archive has 22 bytes
|
|
throw Base::FileException("Invalid project file",filename);
|
|
|
|
zipios::ZipInputStream zipstream(file);
|
|
Base::XMLReader reader(filename, zipstream);
|
|
|
|
if (!reader.isValid())
|
|
throw Base::FileException("Error reading compression file",filename);
|
|
|
|
GetApplication().signalStartRestoreDocument(*this);
|
|
setStatus(Document::Restoring, true);
|
|
|
|
d->partialLoadObjects.clear();
|
|
for(auto &name : objNames)
|
|
d->partialLoadObjects.emplace(name,true);
|
|
try {
|
|
Document::Restore(reader);
|
|
} catch (const Base::Exception& e) {
|
|
Base::Console().Error("Invalid Document.xml: %s\n", e.what());
|
|
setStatus(Document::RestoreError, true);
|
|
}
|
|
|
|
d->partialLoadObjects.clear();
|
|
d->programVersion = reader.ProgramVersion;
|
|
|
|
// Special handling for Gui document, the view representations must already
|
|
// exist, what is done in Restore().
|
|
// Note: This file doesn't need to be available if the document has been created
|
|
// without GUI. But if available then follow after all data files of the App document.
|
|
signalRestoreDocument(reader);
|
|
reader.readFiles(zipstream);
|
|
|
|
if (reader.testStatus(Base::XMLReader::ReaderStatus::PartialRestore)) {
|
|
setStatus(Document::PartialRestore, true);
|
|
Base::Console().Error("There were errors while loading the file. Some data might have been modified or not recovered at all. Look above for more specific information about the objects involved.\n");
|
|
}
|
|
|
|
if(!delaySignal)
|
|
afterRestore(true);
|
|
}
|
|
|
|
bool Document::afterRestore(bool checkPartial) {
|
|
Base::FlagToggler<> flag(globalIsRestoring, false);
|
|
if(!afterRestore(d->objectArray,checkPartial)) {
|
|
FC_WARN("Reload partial document " << getName());
|
|
GetApplication().signalPendingReloadDocument(*this);
|
|
return false;
|
|
}
|
|
GetApplication().signalFinishRestoreDocument(*this);
|
|
setStatus(Document::Restoring, false);
|
|
return true;
|
|
}
|
|
|
|
bool Document::afterRestore(const std::vector<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 = Base::freecad_dynamic_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.count(obj))
|
|
obj->purgeTouched();
|
|
|
|
signalFinishRestoreObject(*obj);
|
|
}
|
|
|
|
d->touchedObjs.clear();
|
|
return true;
|
|
}
|
|
|
|
bool Document::isSaved() const
|
|
{
|
|
std::string name = FileName.getValue();
|
|
return !name.empty();
|
|
}
|
|
|
|
/** Label is the visible name of a document shown e.g. in the windows title
|
|
* or in the tree view. The label almost (but not always e.g. if you manually change it)
|
|
* matches with the file name where the document is stored to.
|
|
* In contrast to Label the method getName() returns the internal name of the document that only
|
|
* matches with Label when loading or creating a document because then both are set to the same value.
|
|
* Since the internal name cannot be changed during runtime it must differ from the Label after saving
|
|
* the document the first time or saving it under a new file name.
|
|
* @ note More than one document can have the same label name.
|
|
* @ note The internal is always guaranteed to be unique because @ref Application::newDocument() checks
|
|
* for a document with the same name and makes it unique if needed. Hence you cannot rely on that the
|
|
* internal name matches with the name you passed to Application::newDoument(). You should use the
|
|
* method getName() instead.
|
|
*/
|
|
const char* Document::getName() const
|
|
{
|
|
// return GetApplication().getDocumentName(this);
|
|
return myName.c_str();
|
|
}
|
|
|
|
std::string Document::getFullName() const {
|
|
return myName;
|
|
}
|
|
|
|
const char* Document::getProgramVersion() const
|
|
{
|
|
return d->programVersion.c_str();
|
|
}
|
|
|
|
const char* Document::getFileName() const
|
|
{
|
|
return testStatus(TempDoc) ? TransientDir.getValue()
|
|
: FileName.getValue();
|
|
}
|
|
|
|
/// Remove all modifications. After this call The document becomes valid again.
|
|
void Document::purgeTouched()
|
|
{
|
|
for (std::vector<DocumentObject*>::iterator It = d->objectArray.begin();It != d->objectArray.end();++It)
|
|
(*It)->purgeTouched();
|
|
}
|
|
|
|
bool Document::isTouched() const
|
|
{
|
|
for (std::vector<DocumentObject*>::const_iterator It = d->objectArray.begin();It != d->objectArray.end();++It)
|
|
if ((*It)->isTouched())
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
vector<DocumentObject*> Document::getTouched() const
|
|
{
|
|
vector<DocumentObject*> result;
|
|
|
|
for (std::vector<DocumentObject*>::const_iterator It = d->objectArray.begin();It != d->objectArray.end();++It)
|
|
if ((*It)->isTouched())
|
|
result.push_back(*It);
|
|
|
|
return result;
|
|
}
|
|
|
|
void Document::setClosable(bool c)
|
|
{
|
|
setStatus(Document::Closable, c);
|
|
}
|
|
|
|
bool Document::isClosable() const
|
|
{
|
|
return testStatus(Document::Closable);
|
|
}
|
|
|
|
int Document::countObjects() const
|
|
{
|
|
return static_cast<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,true))
|
|
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 (auto It = d->objectMap.begin(); It != d->objectMap.end();++It) {
|
|
// get the outList and search if me is in that list
|
|
std::vector<DocumentObject*> OutList = It->second->getOutList();
|
|
for (std::vector<DocumentObject*>::const_iterator It2=OutList.begin();It2!=OutList.end();++It2)
|
|
if (*It2 && *It2 == me)
|
|
// add the parent object
|
|
result.push_back(It->second);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// This function unifies the old _rebuildDependencyList() and
|
|
// getDependencyList(). The algorithm basically obtains the object dependency
|
|
// by recrusivly visiting the OutList of each object in the given object array.
|
|
// It makes sure to call getOutList() of each object once and only once, which
|
|
// makes it much more efficient than calling getRecursiveOutList() on each
|
|
// individual object.
|
|
//
|
|
// The problem with the original algorithm is that, it assumes the objects
|
|
// inside any OutList are all within the given object array, so it does not
|
|
// recursively call getOutList() on those dependent objects inside. This
|
|
// assumption is broken by the introduction of PropertyXLink which can link to
|
|
// external object.
|
|
//
|
|
static void _buildDependencyList(const std::vector<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->getNameInDocument())
|
|
continue;
|
|
|
|
auto it = outLists.find(obj);
|
|
if(it!=outLists.end())
|
|
continue;
|
|
|
|
if(touchCheck) {
|
|
if(obj->isTouched() || obj->mustExecute()) {
|
|
// early termination on touch check
|
|
*touchCheck = true;
|
|
return;
|
|
}
|
|
}
|
|
if(depObjs) depObjs->push_back(obj);
|
|
if(objectMap && depList)
|
|
(*objectMap)[obj] = add_vertex(*depList);
|
|
|
|
auto &outList = outLists[obj];
|
|
outList = obj->getOutList(op);
|
|
objs.insert(objs.end(),outList.begin(),outList.end());
|
|
}
|
|
}
|
|
|
|
if(objectMap && depList) {
|
|
for (const auto &v : outLists) {
|
|
for(auto obj : v.second) {
|
|
if(obj && obj->getNameInDocument())
|
|
add_edge((*objectMap)[v.first],(*objectMap)[obj],*depList);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<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)
|
|
{
|
|
#ifdef USE_OLD_DAG
|
|
_buildDependencyList(objs.empty()?d->objectArray:objs,false,0,&d->DepList,&d->VertexObjectList);
|
|
#else
|
|
(void)objs;
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* @brief Signal that object identifiers, typically a property or document object has been renamed.
|
|
*
|
|
* This function iterates through all document object in the document, and calls its
|
|
* renameObjectIdentifiers functions.
|
|
*
|
|
* @param paths Map with current and new names
|
|
*/
|
|
|
|
void Document::renameObjectIdentifiers(const std::map<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 (std::vector<DocumentObject*>::iterator it = d->objectArray.begin(); it != d->objectArray.end(); ++it)
|
|
if (selector(*it))
|
|
(*it)->renameObjectIdentifiers(extendedPaths);
|
|
}
|
|
|
|
#ifdef USE_OLD_DAG
|
|
int Document::recompute(const std::vector<App::DocumentObject*> &objs, bool force)
|
|
{
|
|
if (testStatus(Document::Recomputing)) {
|
|
// this is clearly a bug in the calling instance
|
|
throw Base::RuntimeError("Nested recomputes of a document are not allowed");
|
|
}
|
|
|
|
int objectCount = 0;
|
|
|
|
// The 'SkipRecompute' flag can be (tmp.) set to avoid too many
|
|
// time expensive recomputes
|
|
if(!force && testStatus(Document::SkipRecompute))
|
|
return 0;
|
|
|
|
Base::ObjectStatusLocker<Document::Status, Document> exe(Document::Recomputing, this);
|
|
|
|
// delete recompute log
|
|
d->clearRecomputeLog();
|
|
|
|
// updates the dependency graph
|
|
_rebuildDependencyList(objs);
|
|
|
|
std::list<Vertex> make_order;
|
|
DependencyList::out_edge_iterator j, jend;
|
|
|
|
try {
|
|
// this sort gives the execute
|
|
boost::topological_sort(d->DepList, std::front_inserter(make_order));
|
|
}
|
|
catch (const std::exception& e) {
|
|
std::cerr << "Document::recompute: " << e.what() << std::endl;
|
|
return -1;
|
|
}
|
|
|
|
// caching vertex to DocObject
|
|
for (std::map<DocumentObject*,Vertex>::const_iterator It1= d->VertexObjectList.begin();It1 != d->VertexObjectList.end(); ++It1)
|
|
d->vertexMap[It1->second] = It1->first;
|
|
|
|
#ifdef FC_LOGFEATUREUPDATE
|
|
std::clog << "make ordering: " << std::endl;
|
|
#endif
|
|
|
|
std::set<DocumentObject*> recomputeList;
|
|
|
|
for (std::list<Vertex>::reverse_iterator i = make_order.rbegin();i != make_order.rend(); ++i) {
|
|
DocumentObject* Cur = d->vertexMap[*i];
|
|
// Because of PropertyXLink, we should account for external objects
|
|
// TODO: make sure it is safe to rely on getNameInDocument() to check if
|
|
// object is in the document. If it crashes, then we should fix the code
|
|
// to properly nullify getNameInDocument(), rather than revert back to
|
|
// the inefficient isIn()
|
|
// if (!Cur || !isIn(Cur)) continue;
|
|
if (!Cur || !Cur->getNameInDocument()) continue;
|
|
#ifdef FC_LOGFEATUREUPDATE
|
|
std::clog << Cur->getNameInDocument() << " dep on:" ;
|
|
#endif
|
|
bool NeedUpdate = false;
|
|
|
|
// ask the object if it should be recomputed
|
|
if (Cur->mustExecute() == 1 || Cur->ExpressionEngine.depsAreTouched()) {
|
|
#ifdef FC_LOGFEATUREUPDATE
|
|
std::clog << "[touched]";
|
|
#endif
|
|
NeedUpdate = true;
|
|
}
|
|
else {// if (Cur->mustExecute() == -1)
|
|
// update if one of the dependencies is touched
|
|
for (boost::tie(j, jend) = out_edges(*i, d->DepList); j != jend; ++j) {
|
|
DocumentObject* Test = d->vertexMap[target(*j, d->DepList)];
|
|
|
|
if (!Test) continue;
|
|
#ifdef FC_LOGFEATUREUPDATE
|
|
std::clog << " " << Test->getNameInDocument();
|
|
#endif
|
|
if (Test->isTouched()) {
|
|
NeedUpdate = true;
|
|
#ifdef FC_LOGFEATUREUPDATE
|
|
std::clog << "[touched]";
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
// if one touched recompute
|
|
if (NeedUpdate) {
|
|
Cur->touch();
|
|
#ifdef FC_LOGFEATUREUPDATE
|
|
std::clog << " => Recompute feature";
|
|
#endif
|
|
recomputeList.insert(Cur);
|
|
}
|
|
#ifdef FC_LOGFEATUREUPDATE
|
|
std::clog << std::endl;
|
|
#endif
|
|
}
|
|
|
|
#ifdef FC_LOGFEATUREUPDATE
|
|
std::clog << "Have to recompute the following document objects" << std::endl;
|
|
for (std::set<DocumentObject*>::const_iterator it = recomputeList.begin(); it != recomputeList.end(); ++it) {
|
|
std::clog << " " << (*it)->getNameInDocument() << std::endl;
|
|
}
|
|
#endif
|
|
|
|
for (std::list<Vertex>::reverse_iterator i = make_order.rbegin();i != make_order.rend(); ++i) {
|
|
DocumentObject* Cur = d->vertexMap[*i];
|
|
if (!Cur || !isIn(Cur)) continue;
|
|
|
|
if (recomputeList.find(Cur) != recomputeList.end() ||
|
|
Cur->ExpressionEngine.depsAreTouched()) {
|
|
if ( _recomputeFeature(Cur)) {
|
|
// if something happened break execution of recompute
|
|
d->vertexMap.clear();
|
|
return -1;
|
|
}
|
|
signalRecomputedObject(*Cur);
|
|
++objectCount;
|
|
}
|
|
}
|
|
|
|
// reset all touched
|
|
for (std::map<Vertex,DocumentObject*>::iterator it = d->vertexMap.begin(); it != d->vertexMap.end(); ++it) {
|
|
// TODO: check the TODO comments above for details
|
|
// if ((it->second) && isIn(it->second))
|
|
if ((it->second) && it->second->getNameInDocument())
|
|
it->second->purgeTouched();
|
|
}
|
|
d->vertexMap.clear();
|
|
|
|
signalRecomputed(*this);
|
|
|
|
return objectCount;
|
|
}
|
|
|
|
#else //ifdef USE_OLD_DAG
|
|
|
|
int Document::recompute(const std::vector<App::DocumentObject*> &objs, bool force, bool *hasError, int options)
|
|
{
|
|
if (d->undoing || d->rollback) {
|
|
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG))
|
|
FC_WARN("Ignore document recompute on undo/redo");
|
|
return 0;
|
|
}
|
|
|
|
int objectCount = 0;
|
|
if (testStatus(Document::PartialDoc)) {
|
|
if(mustExecute())
|
|
FC_WARN("Please reload partial document '" << Label.getValue() << "' for recomputation.");
|
|
return 0;
|
|
}
|
|
if (testStatus(Document::Recomputing)) {
|
|
// this is clearly a bug in the calling instance
|
|
FC_ERR("Recursive calling of recompute for document " << getName());
|
|
return 0;
|
|
}
|
|
// The 'SkipRecompute' flag can be (tmp.) set to avoid too many
|
|
// time expensive recomputes
|
|
if(!force && testStatus(Document::SkipRecompute)) {
|
|
signalSkipRecompute(*this,objs);
|
|
return 0;
|
|
}
|
|
|
|
// delete recompute log
|
|
d->clearRecomputeLog();
|
|
|
|
FC_TIME_INIT(t);
|
|
|
|
Base::ObjectStatusLocker<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.reset(new Base::SequencerLauncher("Recompute...", topoSortedObjects.size()));
|
|
FC_LOG("Recompute pass " << passes);
|
|
for (; idx < topoSortedObjects.size(); ++idx) {
|
|
auto obj = topoSortedObjects[idx];
|
|
if(!obj->getNameInDocument() || filter.find(obj)!=filter.end())
|
|
continue;
|
|
// ask the object if it should be recomputed
|
|
bool doRecompute = false;
|
|
if (obj->mustRecompute()) {
|
|
doRecompute = true;
|
|
++objectCount;
|
|
int res = _recomputeFeature(obj);
|
|
if(res) {
|
|
if(hasError)
|
|
*hasError = true;
|
|
if(res < 0) {
|
|
passes = 2;
|
|
break;
|
|
}
|
|
// if something happened filter all object in its
|
|
// inListRecursive from the queue then proceed
|
|
obj->getInListEx(filter,true);
|
|
filter.insert(obj);
|
|
continue;
|
|
}
|
|
}
|
|
if(obj->isTouched() || doRecompute) {
|
|
signalRecomputedObject(*obj);
|
|
obj->purgeTouched();
|
|
// set all dependent object touched to force recompute
|
|
for (auto inObjIt : obj->getInList())
|
|
inObjIt->enforceRecompute();
|
|
}
|
|
if (seq)
|
|
seq->next(true);
|
|
}
|
|
// check if all objects are recomputed but still thouched
|
|
for (size_t i=0;i<topoSortedObjects.size();++i) {
|
|
auto obj = topoSortedObjects[i];
|
|
obj->setStatus(ObjectStatus::Recompute2,false);
|
|
if(!filter.count(obj) && obj->isTouched()) {
|
|
if(passes>0)
|
|
FC_ERR(obj->getFullName() << " still touched after recompute");
|
|
else{
|
|
FC_LOG(obj->getFullName() << " still touched after recompute");
|
|
if(idx>=topoSortedObjects.size()) {
|
|
// let's start the next pass on the first touched object
|
|
idx = i;
|
|
}
|
|
obj->setStatus(ObjectStatus::Recompute2,true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}catch(Base::Exception &e) {
|
|
e.ReportException();
|
|
}
|
|
|
|
FC_TIME_LOG(t2, "Recompute");
|
|
|
|
for(auto obj : topoSortedObjects) {
|
|
if(!obj->getNameInDocument())
|
|
continue;
|
|
obj->setStatus(ObjectStatus::PendingRecompute,false);
|
|
obj->setStatus(ObjectStatus::Recompute2,false);
|
|
}
|
|
|
|
signalRecomputed(*this,topoSortedObjects);
|
|
|
|
FC_TIME_LOG(t,"Recompute total");
|
|
|
|
if (!d->_RecomputeLog.empty()) {
|
|
d->pendingRemove.clear();
|
|
if (!testStatus(Status::IgnoreErrorOnRecompute))
|
|
Base::Console().Error("Recompute failed! Please check report view.\n");
|
|
}
|
|
else {
|
|
for(auto &o : d->pendingRemove) {
|
|
auto obj = o.getObject();
|
|
if (obj)
|
|
obj->getDocument()->removeObject(obj->getNameInDocument());
|
|
}
|
|
}
|
|
|
|
return objectCount;
|
|
}
|
|
|
|
#endif // USE_OLD_DAG
|
|
|
|
/*!
|
|
Does almost the same as topologicalSort() until no object with an input degree of zero
|
|
can be found. It then searches for objects with an output degree of zero until neither
|
|
an object with input or output degree can be found. The remaining objects form one or
|
|
multiple cycles.
|
|
An alternative to this method might be:
|
|
https://en.wikipedia.org/wiki/Tarjan%E2%80%99s_strongly_connected_components_algorithm
|
|
*/
|
|
std::vector<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->getNameInDocument() || obj->getDocument()!=this)
|
|
if(!objectIt->getNameInDocument())
|
|
continue;
|
|
//we need inlist with unique entries
|
|
auto in = objectIt->getInList();
|
|
std::sort(in.begin(), in.end());
|
|
in.erase(std::unique(in.begin(), in.end()), in.end());
|
|
|
|
countMap[objectIt] = in.size();
|
|
}
|
|
|
|
auto rootObjeIt = find_if(countMap.begin(), countMap.end(), [](pair < App::DocumentObject*, int > count)->bool {
|
|
return count.second == 0;
|
|
});
|
|
|
|
if (rootObjeIt == countMap.end()){
|
|
cerr << "Document::topologicalSort: cyclic dependency detected (no root object)" << endl;
|
|
return ret;
|
|
}
|
|
|
|
while (rootObjeIt != countMap.end()){
|
|
rootObjeIt->second = rootObjeIt->second - 1;
|
|
|
|
//we need outlist with unique entries
|
|
auto out = rootObjeIt->first->getOutList();
|
|
std::sort(out.begin(), out.end());
|
|
out.erase(std::unique(out.begin(), out.end()), out.end());
|
|
|
|
for (auto outListIt : out) {
|
|
auto outListMapIt = countMap.find(outListIt);
|
|
if (outListMapIt != countMap.end())
|
|
outListMapIt->second = outListMapIt->second - 1;
|
|
}
|
|
ret.push_back(rootObjeIt->first);
|
|
|
|
rootObjeIt = find_if(countMap.begin(), countMap.end(), [](pair < App::DocumentObject*, int > count)->bool {
|
|
return count.second == 0;
|
|
});
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
std::vector<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* Feat, bool recursive)
|
|
{
|
|
// delete recompute log
|
|
d->clearRecomputeLog(Feat);
|
|
|
|
// verify that the feature is (active) part of the document
|
|
if (Feat->getNameInDocument()) {
|
|
if(recursive) {
|
|
bool hasError = false;
|
|
recompute({Feat},true,&hasError);
|
|
return !hasError;
|
|
} else {
|
|
_recomputeFeature(Feat);
|
|
signalRecomputedObject(*Feat);
|
|
return Feat->isValid();
|
|
}
|
|
}else
|
|
return false;
|
|
}
|
|
|
|
DocumentObject * Document::addObject(const char* sType, const char* pObjectName,
|
|
bool isNew, const char* viewType, bool isPartial)
|
|
{
|
|
Base::Type type = Base::Type::getTypeIfDerivedFrom(sType, App::DocumentObject::getClassTypeId(), true);
|
|
if (type.isBad()) {
|
|
std::stringstream str;
|
|
str << "'" << sType << "' is not a document object type";
|
|
throw Base::TypeError(str.str());
|
|
}
|
|
|
|
void* typeInstance = type.createInstance();
|
|
if (!typeInstance)
|
|
return nullptr;
|
|
|
|
App::DocumentObject* pcObject = static_cast<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
|
|
string ObjectName;
|
|
|
|
if (pObjectName && pObjectName[0] != '\0')
|
|
ObjectName = getUniqueObjectName(pObjectName);
|
|
else
|
|
ObjectName = getUniqueObjectName(sType);
|
|
|
|
|
|
d->activeObject = pcObject;
|
|
|
|
// insert in the name map
|
|
d->objectMap[ObjectName] = pcObject;
|
|
// generate object id and add to id map;
|
|
pcObject->_Id = ++d->lastObjectId;
|
|
d->objectIdMap[pcObject->_Id] = pcObject;
|
|
// cache the pointer to the name string in the Object (for performance of DocumentObject::getNameInDocument())
|
|
pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first);
|
|
// insert in the vector
|
|
d->objectArray.push_back(pcObject);
|
|
|
|
// If we are restoring, don't set the Label object now; it will be restored later. This is to avoid potential duplicate
|
|
// label conflicts later.
|
|
if (!d->StatusBits.test(Restoring))
|
|
pcObject->Label.setValue( ObjectName );
|
|
|
|
// Call the object-specific initialization
|
|
if (!d->undoing && !d->rollback && isNew) {
|
|
pcObject->setupObject ();
|
|
}
|
|
|
|
// mark the object as new (i.e. set status bit 2) and send the signal
|
|
pcObject->setStatus(ObjectStatus::New, true);
|
|
|
|
pcObject->setStatus(ObjectStatus::PartialObject, isPartial);
|
|
|
|
if (!viewType || viewType[0] == '\0')
|
|
viewType = pcObject->getViewProviderNameOverride();
|
|
|
|
if (viewType && viewType[0] != '\0')
|
|
pcObject->_pcViewProviderName = viewType;
|
|
|
|
signalNewObject(*pcObject);
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback && d->activeUndoTransaction) {
|
|
signalTransactionAppend(*pcObject, d->activeUndoTransaction);
|
|
}
|
|
|
|
signalActivatedObject(*pcObject);
|
|
|
|
// return the Object
|
|
return pcObject;
|
|
}
|
|
|
|
std::vector<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;
|
|
}
|
|
|
|
// get all existing object names
|
|
std::vector<std::string> reservedNames;
|
|
reservedNames.reserve(d->objectMap.size());
|
|
for (auto pos = d->objectMap.begin();pos != d->objectMap.end();++pos) {
|
|
reservedNames.push_back(pos->first);
|
|
}
|
|
|
|
for (auto it = objects.begin(); it != objects.end(); ++it) {
|
|
auto index = std::distance(objects.begin(), it);
|
|
App::DocumentObject* pcObject = *it;
|
|
pcObject->setDocument(this);
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback) {
|
|
// Undo stuff
|
|
_checkTransaction(nullptr,nullptr,__LINE__);
|
|
if (d->activeUndoTransaction) {
|
|
d->activeUndoTransaction->addObjectDel(pcObject);
|
|
}
|
|
}
|
|
|
|
// get unique name
|
|
std::string ObjectName = objectNames[index];
|
|
if (ObjectName.empty())
|
|
ObjectName = sType;
|
|
ObjectName = Base::Tools::getIdentifier(ObjectName);
|
|
if (d->objectMap.find(ObjectName) != d->objectMap.end()) {
|
|
// remove also trailing digits from clean name which is to avoid to create lengthy names
|
|
// like 'Box001001'
|
|
if (!testStatus(KeepTrailingDigits)) {
|
|
std::string::size_type index = ObjectName.find_last_not_of("0123456789");
|
|
if (index+1 < ObjectName.size()) {
|
|
ObjectName = ObjectName.substr(0,index+1);
|
|
}
|
|
}
|
|
|
|
ObjectName = Base::Tools::getUniqueName(ObjectName, reservedNames, 3);
|
|
}
|
|
|
|
reservedNames.push_back(ObjectName);
|
|
|
|
// insert in the name map
|
|
d->objectMap[ObjectName] = pcObject;
|
|
// generate object id and add to id map;
|
|
pcObject->_Id = ++d->lastObjectId;
|
|
d->objectIdMap[pcObject->_Id] = pcObject;
|
|
// cache the pointer to the name string in the Object (for performance of DocumentObject::getNameInDocument())
|
|
pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first);
|
|
// insert in the vector
|
|
d->objectArray.push_back(pcObject);
|
|
|
|
pcObject->Label.setValue(ObjectName);
|
|
|
|
// Call the object-specific initialization
|
|
if (!d->undoing && !d->rollback && isNew) {
|
|
pcObject->setupObject();
|
|
}
|
|
|
|
// mark the object as new (i.e. set status bit 2) and send the signal
|
|
pcObject->setStatus(ObjectStatus::New, true);
|
|
|
|
const char *viewType = pcObject->getViewProviderNameOverride();
|
|
pcObject->_pcViewProviderName = viewType ? viewType : "";
|
|
|
|
signalNewObject(*pcObject);
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback && d->activeUndoTransaction) {
|
|
signalTransactionAppend(*pcObject, d->activeUndoTransaction);
|
|
}
|
|
}
|
|
|
|
if (!objects.empty()) {
|
|
d->activeObject = objects.back();
|
|
signalActivatedObject(*objects.back());
|
|
}
|
|
|
|
return objects;
|
|
}
|
|
|
|
void Document::addObject(DocumentObject* pcObject, const char* pObjectName)
|
|
{
|
|
if (pcObject->getDocument()) {
|
|
throw Base::RuntimeError("Document object is already added to a document");
|
|
}
|
|
|
|
pcObject->setDocument(this);
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback) {
|
|
// Undo stuff
|
|
_checkTransaction(nullptr,nullptr,__LINE__);
|
|
if (d->activeUndoTransaction)
|
|
d->activeUndoTransaction->addObjectDel(pcObject);
|
|
}
|
|
|
|
// get unique name
|
|
string ObjectName;
|
|
if (pObjectName && pObjectName[0] != '\0')
|
|
ObjectName = getUniqueObjectName(pObjectName);
|
|
else
|
|
ObjectName = getUniqueObjectName(pcObject->getTypeId().getName());
|
|
|
|
d->activeObject = pcObject;
|
|
|
|
// insert in the name map
|
|
d->objectMap[ObjectName] = pcObject;
|
|
// generate object id and add to id map;
|
|
if(!pcObject->_Id) pcObject->_Id = ++d->lastObjectId;
|
|
d->objectIdMap[pcObject->_Id] = pcObject;
|
|
// cache the pointer to the name string in the Object (for performance of DocumentObject::getNameInDocument())
|
|
pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first);
|
|
// insert in the vector
|
|
d->objectArray.push_back(pcObject);
|
|
|
|
pcObject->Label.setValue( ObjectName );
|
|
|
|
// mark the object as new (i.e. set status bit 2) and send the signal
|
|
pcObject->setStatus(ObjectStatus::New, true);
|
|
|
|
const char *viewType = pcObject->getViewProviderNameOverride();
|
|
pcObject->_pcViewProviderName = viewType ? viewType : "";
|
|
|
|
signalNewObject(*pcObject);
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback && d->activeUndoTransaction) {
|
|
signalTransactionAppend(*pcObject, d->activeUndoTransaction);
|
|
}
|
|
|
|
signalActivatedObject(*pcObject);
|
|
}
|
|
|
|
void Document::_addObject(DocumentObject* pcObject, const char* pObjectName)
|
|
{
|
|
std::string ObjectName = getUniqueObjectName(pObjectName);
|
|
d->objectMap[ObjectName] = pcObject;
|
|
// generate object id and add to id map;
|
|
if(!pcObject->_Id) pcObject->_Id = ++d->lastObjectId;
|
|
d->objectIdMap[pcObject->_Id] = pcObject;
|
|
d->objectArray.push_back(pcObject);
|
|
// cache the pointer to the name string in the Object (for performance of DocumentObject::getNameInDocument())
|
|
pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first);
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback) {
|
|
// Undo stuff
|
|
_checkTransaction(nullptr,nullptr,__LINE__);
|
|
if (d->activeUndoTransaction)
|
|
d->activeUndoTransaction->addObjectDel(pcObject);
|
|
}
|
|
|
|
const char *viewType = pcObject->getViewProviderNameOverride();
|
|
pcObject->_pcViewProviderName = viewType ? viewType : "";
|
|
|
|
// send the signal
|
|
signalNewObject(*pcObject);
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback && d->activeUndoTransaction) {
|
|
signalTransactionAppend(*pcObject, d->activeUndoTransaction);
|
|
}
|
|
|
|
d->activeObject = pcObject;
|
|
signalActivatedObject(*pcObject);
|
|
}
|
|
|
|
/// Remove an object out of the document
|
|
void Document::removeObject(const char* sName)
|
|
{
|
|
auto pos = d->objectMap.find(sName);
|
|
|
|
// name not found?
|
|
if (pos == d->objectMap.end())
|
|
return;
|
|
|
|
if (pos->second->testStatus(ObjectStatus::PendingRecompute)) {
|
|
// TODO: shall we allow removal if there is active undo transaction?
|
|
FC_LOG("pending remove of " << sName << " after recomputing document " << getName());
|
|
d->pendingRemove.emplace_back(pos->second);
|
|
return;
|
|
}
|
|
|
|
TransactionLocker tlock;
|
|
|
|
_checkTransaction(pos->second,nullptr,__LINE__);
|
|
|
|
if (d->activeObject == pos->second)
|
|
d->activeObject = nullptr;
|
|
|
|
// Mark the object as about to be deleted
|
|
pos->second->setStatus(ObjectStatus::Remove, true);
|
|
if (!d->undoing && !d->rollback) {
|
|
pos->second->unsetupObject();
|
|
}
|
|
|
|
signalDeletedObject(*(pos->second));
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback && d->activeUndoTransaction) {
|
|
// in this case transaction delete or save the object
|
|
signalTransactionRemove(*pos->second, d->activeUndoTransaction);
|
|
}
|
|
else {
|
|
// if not saved in undo -> delete object
|
|
signalTransactionRemove(*pos->second, 0);
|
|
}
|
|
|
|
#ifdef USE_OLD_DAG
|
|
if (!d->vertexMap.empty()) {
|
|
// recompute of document is running
|
|
for (std::map<Vertex,DocumentObject*>::iterator it = d->vertexMap.begin(); it != d->vertexMap.end(); ++it) {
|
|
if (it->second == pos->second) {
|
|
it->second = 0; // just nullify the pointer
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
#endif //USE_OLD_DAG
|
|
|
|
// Before deleting we must nullify all dependent objects
|
|
breakDependency(pos->second, true);
|
|
|
|
//and remove the tip if needed
|
|
if (Tip.getValue() && strcmp(Tip.getValue()->getNameInDocument(), sName)==0) {
|
|
Tip.setValue(nullptr);
|
|
TipName.setValue("");
|
|
}
|
|
|
|
// remove the ID before possibly deleting the object
|
|
d->objectIdMap.erase(pos->second->_Id);
|
|
// Unset the bit to be on the safe side
|
|
pos->second->setStatus(ObjectStatus::Remove, false);
|
|
|
|
// do no transactions if we do a rollback!
|
|
std::unique_ptr<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->objectMap.erase(pos);
|
|
}
|
|
|
|
/// Remove an object out of the document (internal)
|
|
void Document::_removeObject(DocumentObject* pcObject)
|
|
{
|
|
if (testStatus(Document::Recomputing)) {
|
|
FC_ERR("Cannot delete " << pcObject->getFullName() << " while recomputing");
|
|
return;
|
|
}
|
|
|
|
TransactionLocker tlock;
|
|
|
|
// TODO Refactoring: share code with Document::removeObject() (2015-09-01, Fat-Zer)
|
|
_checkTransaction(pcObject,nullptr,__LINE__);
|
|
|
|
auto pos = d->objectMap.find(pcObject->getNameInDocument());
|
|
|
|
if(!d->rollback && d->activeUndoTransaction && pos->second->hasChildElement()) {
|
|
// Preserve link group children global visibility. See comments in
|
|
// removeObject() for more details.
|
|
for(auto &sub : pos->second->getSubObjects()) {
|
|
if(sub.empty())
|
|
continue;
|
|
if(sub[sub.size()-1]!='.')
|
|
sub += '.';
|
|
auto sobj = pos->second->getSubObject(sub.c_str());
|
|
if(sobj && sobj->getDocument()==this && !sobj->Visibility.getValue())
|
|
d->activeUndoTransaction->addObjectChange(sobj,&sobj->Visibility);
|
|
}
|
|
}
|
|
|
|
if (d->activeObject == pcObject)
|
|
d->activeObject = nullptr;
|
|
|
|
// Mark the object as about to be removed
|
|
pcObject->setStatus(ObjectStatus::Remove, true);
|
|
if (!d->undoing && !d->rollback) {
|
|
pcObject->unsetupObject();
|
|
}
|
|
signalDeletedObject(*pcObject);
|
|
// TODO Check me if it's needed (2015-09-01, Fat-Zer)
|
|
|
|
//remove the tip if needed
|
|
if (Tip.getValue() == pcObject) {
|
|
Tip.setValue(nullptr);
|
|
TipName.setValue("");
|
|
}
|
|
|
|
// do no transactions if we do a rollback!
|
|
if (!d->rollback && d->activeUndoTransaction) {
|
|
// Undo stuff
|
|
signalTransactionRemove(*pcObject, d->activeUndoTransaction);
|
|
d->activeUndoTransaction->addObjectNew(pcObject);
|
|
}
|
|
else {
|
|
// for a rollback delete the object
|
|
signalTransactionRemove(*pcObject, 0);
|
|
breakDependency(pcObject, true);
|
|
}
|
|
|
|
// remove from map
|
|
pcObject->setStatus(ObjectStatus::Remove, false); // Unset the bit to be on the safe side
|
|
d->objectIdMap.erase(pcObject->_Id);
|
|
d->objectMap.erase(pos);
|
|
|
|
for (std::vector<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 (std::vector<App::DocumentObject*>::iterator it = deps.begin(); it != deps.end(); ++it)
|
|
memsize += (*it)->getMemSize();
|
|
|
|
// if less than ~10 MB
|
|
bool use_buffer=(memsize < 0xA00000);
|
|
QByteArray res;
|
|
try {
|
|
res.reserve(memsize);
|
|
}
|
|
catch (const Base::MemoryException&) {
|
|
use_buffer = false;
|
|
}
|
|
|
|
std::vector<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 = Base::freecad_dynamic_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 (auto o = d->objectMap.begin(); o != d->objectMap.end(); ++o) {
|
|
if (o->second == pFeat)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
const char * Document::getObjectName(DocumentObject *pFeat) const
|
|
{
|
|
for (auto pos = d->objectMap.begin();pos != d->objectMap.end();++pos) {
|
|
if (pos->second == pFeat)
|
|
return pos->first.c_str();
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
std::string Document::getUniqueObjectName(const char *Name) const
|
|
{
|
|
if (!Name || *Name == '\0')
|
|
return std::string();
|
|
std::string CleanName = Base::Tools::getIdentifier(Name);
|
|
|
|
// name in use?
|
|
auto pos = d->objectMap.find(CleanName);
|
|
|
|
if (pos == d->objectMap.end()) {
|
|
// if not, name is OK
|
|
return CleanName;
|
|
}
|
|
else {
|
|
// remove also trailing digits from clean name which is to avoid to create lengthy names
|
|
// like 'Box001001'
|
|
if (!testStatus(KeepTrailingDigits)) {
|
|
std::string::size_type index = CleanName.find_last_not_of("0123456789");
|
|
if (index+1 < CleanName.size()) {
|
|
CleanName = CleanName.substr(0,index+1);
|
|
}
|
|
}
|
|
|
|
std::vector<std::string> names;
|
|
names.reserve(d->objectMap.size());
|
|
for (pos = d->objectMap.begin();pos != d->objectMap.end();++pos) {
|
|
names.push_back(pos->first);
|
|
}
|
|
return Base::Tools::getUniqueName(CleanName, names, 3);
|
|
}
|
|
}
|
|
|
|
std::string Document::getStandardObjectName(const char *Name, int d) const
|
|
{
|
|
std::vector<App::DocumentObject*> mm = getObjects();
|
|
std::vector<std::string> labels;
|
|
labels.reserve(mm.size());
|
|
|
|
for (std::vector<App::DocumentObject*>::const_iterator it = mm.begin(); it != mm.end(); ++it) {
|
|
std::string label = (*it)->Label.getValue();
|
|
labels.push_back(label);
|
|
}
|
|
return Base::Tools::getUniqueName(Name, labels, d);
|
|
}
|
|
|
|
std::vector<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 (std::vector<DocumentObject*>::const_iterator it = d->objectArray.begin(); it != d->objectArray.end(); ++it) {
|
|
if ((*it)->getTypeId().isDerivedFrom(typeId))
|
|
Objects.push_back(*it);
|
|
}
|
|
return Objects;
|
|
}
|
|
|
|
std::vector< DocumentObject* > Document::getObjectsWithExtension(const Base::Type& typeId, bool derived) const {
|
|
|
|
std::vector<DocumentObject*> Objects;
|
|
for (std::vector<DocumentObject*>::const_iterator it = d->objectArray.begin(); it != d->objectArray.end(); ++it) {
|
|
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 (std::vector<DocumentObject*>::const_iterator it = d->objectArray.begin(); it != d->objectArray.end(); ++it) {
|
|
if ((*it)->getTypeId().isDerivedFrom(typeId)) {
|
|
found = *it;
|
|
|
|
if (!rx_name.empty() && !boost::regex_search((*it)->getNameInDocument(), what, rx_name))
|
|
found = nullptr;
|
|
|
|
if (!rx_label.empty() && !boost::regex_search((*it)->Label.getValue(), what, rx_label))
|
|
found = nullptr;
|
|
|
|
if (found)
|
|
Objects.push_back(found);
|
|
}
|
|
}
|
|
return Objects;
|
|
}
|
|
|
|
int Document::countObjectsOfType(const Base::Type& typeId) const
|
|
{
|
|
int ct=0;
|
|
for (auto it = d->objectMap.begin(); it != d->objectMap.end(); ++it) {
|
|
if (it->second->getTypeId().isDerivedFrom(typeId))
|
|
ct++;
|
|
}
|
|
|
|
return ct;
|
|
}
|
|
|
|
PyObject * Document::getPyObject()
|
|
{
|
|
return Py::new_reference_to(d->DocumentPythonObject);
|
|
}
|
|
|
|
std::vector<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;
|
|
}
|
|
|
|
void DocumentP::findAllPathsAt(const std::vector <Node> &all_nodes, size_t id,
|
|
std::vector <Path> &all_paths, Path tmp)
|
|
{
|
|
if (std::find(tmp.begin(), tmp.end(), id) != tmp.end()) {
|
|
Path tmp2(tmp);
|
|
tmp2.push_back(id);
|
|
all_paths.push_back(tmp2);
|
|
return; // a cycle
|
|
}
|
|
|
|
tmp.push_back(id);
|
|
if (all_nodes[id].empty()) {
|
|
all_paths.push_back(tmp);
|
|
return;
|
|
}
|
|
|
|
for (size_t i=0; i < all_nodes[id].size(); i++) {
|
|
Path tmp2(tmp);
|
|
findAllPathsAt(all_nodes, all_nodes[id][i], all_paths, tmp2);
|
|
}
|
|
}
|
|
|
|
std::vector<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];
|
|
Path tmp;
|
|
std::vector<Path> all_paths;
|
|
DocumentP::findAllPathsAt(all_nodes, index_from, all_paths, tmp);
|
|
|
|
for (std::vector<Path>::iterator it = all_paths.begin(); it != all_paths.end(); ++it) {
|
|
Path::iterator jt = std::find(it->begin(), it->end(), index_to);
|
|
if (jt != it->end()) {
|
|
std::list<App::DocumentObject*> path;
|
|
for (Path::iterator kt = it->begin(); kt != jt; ++kt) {
|
|
path.push_back(d->objectArray[*kt]);
|
|
}
|
|
|
|
path.push_back(d->objectArray[*jt]);
|
|
array.push_back(path);
|
|
}
|
|
}
|
|
|
|
// remove duplicates
|
|
std::sort(array.begin(), array.end());
|
|
array.erase(std::unique(array.begin(), array.end()), array.end());
|
|
|
|
return array;
|
|
}
|
|
|
|
bool Document::mustExecute() const
|
|
{
|
|
if(PropertyXLink::hasXLink(this)) {
|
|
bool touched = false;
|
|
_buildDependencyList(d->objectArray,false,nullptr,nullptr,nullptr,&touched);
|
|
return touched;
|
|
}
|
|
|
|
for (std::vector<DocumentObject*>::const_iterator It = d->objectArray.begin();It != d->objectArray.end();++It)
|
|
if ((*It)->isTouched() || (*It)->mustExecute()==1)
|
|
return true;
|
|
return false;
|
|
}
|