Files
create/src/Gui/Document.cpp
Chris Hennes 71104a08e7 Core: Convert transparency to alpha (#24891)
* Core: Convert transparency to alpha

Create new `Base::getVersion()` function for extracting a program
version enumeration given a version string.

Convert transparency to alpha value for old project files.

* Base/App: Address review comments

---------

Co-authored-by: wmayer <wmayer@freecad.org>
2025-11-17 17:15:19 +01:00

3083 lines
104 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/***************************************************************************
* Copyright (c) 2004 Jürgen Riegel <juergen.riegel@web.de> *
* *
* This file is part of the FreeCAD CAx development system. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Library General Public *
* License as published by the Free Software Foundation; either *
* version 2 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this library; see the file COPYING.LIB. If not, *
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
#include <tuple>
#include <memory>
#include <list>
#include <string>
#include <map>
#include <set>
#include <unordered_map>
#include <vector>
#include <cctype>
#include <mutex>
#include <QApplication>
#include <QFileInfo>
#include <QMessageBox>
#include <QOpenGLWidget>
#include <QTextStream>
#include <QTimer>
#include <QThread>
#include <QStatusBar>
#include <Inventor/actions/SoSearchAction.h>
#include <Inventor/nodes/SoSeparator.h>
#include <App/AutoTransaction.h>
#include <App/Document.h>
#include <App/DocumentObject.h>
#include <App/DocumentObjectGroup.h>
#include <App/Transactions.h>
#include <App/ElementNamingUtils.h>
#include <Base/Console.h>
#include <Base/Exception.h>
#include <Base/Matrix.h>
#include <Base/Reader.h>
#include <Base/Writer.h>
#include <Base/Tools.h>
#include "Document.h"
#include "DocumentPy.h"
#include "Application.h"
#include "Command.h"
#include "Control.h"
#include "FileDialog.h"
#include "MainWindow.h"
#include "MDIView.h"
#include "NotificationArea.h"
#include "Selection.h"
#include "Thumbnail.h"
#include "Tree.h"
#include "View3DInventor.h"
#include "View3DInventorViewer.h"
#include "ViewProviderDocumentObject.h"
#include "ViewProviderDocumentObjectGroup.h"
#include "WaitCursor.h"
FC_LOG_LEVEL_INIT("Gui", true, true)
using namespace Gui;
namespace sp = std::placeholders;
namespace Gui
{
// Pimpl class
struct DocumentP
{
Thumbnail thumb;
int _iWinCount;
int _iDocId;
bool _isClosing;
bool _isModified;
bool _isTransacting;
bool _changeViewTouchDocument;
bool _editWantsRestore;
bool _editWantsRestorePrevious;
int _editMode;
int _editModePrevious;
ViewProvider* _editViewProvider;
ViewProvider* _editViewProviderPrevious;
App::DocumentObject* _editingObject;
ViewProviderDocumentObject* _editViewProviderParent;
std::string _editSubname;
std::string _editSubElement;
Base::Matrix4D _editingTransform;
View3DInventorViewer* _editingViewer;
std::set<const App::DocumentObject*> _editObjs;
Application* _pcAppWnd;
// the doc/Document
App::Document* _pcDocument;
/// List of all registered views
std::list<Gui::BaseView*> baseViews;
/// List of all registered views
std::list<Gui::BaseView*> passiveViews;
std::map<const App::DocumentObject*, ViewProviderDocumentObject*> _ViewProviderMap;
std::map<SoSeparator*, ViewProviderDocumentObject*> _CoinMap;
std::map<std::string, ViewProvider*> _ViewProviderMapAnnotation;
std::list<ViewProviderDocumentObject*> _redoViewProviders;
using Connection = boost::signals2::connection;
Connection connectNewObject;
Connection connectDelObject;
Connection connectCngObject;
Connection connectRenObject;
Connection connectActObject;
Connection connectSaveDocument;
Connection connectRestDocument;
Connection connectStartLoadDocument;
Connection connectFinishLoadDocument;
Connection connectShowHidden;
Connection connectFinishRestoreDocument;
Connection connectFinishRestoreObject;
Connection connectExportObjects;
Connection connectImportObjects;
Connection connectFinishImportObjects;
Connection connectUndoDocument;
Connection connectRedoDocument;
Connection connectRecomputed;
Connection connectSkipRecompute;
Connection connectTransactionAppend;
Connection connectTransactionRemove;
Connection connectTouchedObject;
Connection connectChangePropertyEditor;
Connection connectChangeDocument;
using ConnectionBlock = boost::signals2::shared_connection_block;
ConnectionBlock connectActObjectBlocker;
ConnectionBlock connectChangeDocumentBlocker;
static ViewProviderDocumentObject* throwIfCastFails(ViewProvider* p)
{
if (auto vp = freecad_cast<ViewProviderDocumentObject*>(p)) {
return vp;
}
throw Base::RuntimeError("cannot edit non ViewProviderDocumentObject");
}
static App::DocumentObject* tryGetObject(ViewProviderDocumentObject* vp)
{
auto obj = vp->getObject();
if (!obj->isAttachedToDocument()) {
throw Base::RuntimeError("cannot edit detached object");
}
return obj;
}
void throwIfNotInMap(App::DocumentObject* obj, App::Document* doc) const
{
if (_ViewProviderMap.find(obj) == _ViewProviderMap.end()) {
// We can actually support editing external object, by calling
// View3DInventViewer::setupEditingRoot() before exiting from
// ViewProvider::setEditViewer(), which transfer all child node of the view
// provider into an editing node inside the viewer of this document. And
// that's may actually be the case, as the subname referenced sub object
// is allowed to be in other documents.
//
// We just disabling editing external parent object here, for bug
// tracking purpose. Because, bringing an unrelated external object to
// the current view for editing will confuse user, and is certainly a
// bug. By right, the top parent object should always belong to the
// editing document, and the actually editing sub object can be
// external.
//
// So, you can either call setEdit() with subname set to 0, which cause
// the code above to auto detect selection context, and dispatch the
// editing call to the correct document. Or, supply subname yourself,
// and make sure you get the document right.
//
std::stringstream str;
str << "cannot edit object '" << obj->getNameInDocument() << "': not found in document "
<< "'" << doc->getName() << "'";
throw Base::RuntimeError(str.str());
}
}
App::DocumentObject* tryGetSubObject(App::DocumentObject* obj, const char* subname)
{
_editingTransform = Base::Matrix4D();
auto sobj = obj->getSubObject(subname, nullptr, &_editingTransform);
if (!sobj || !sobj->isAttachedToDocument()) {
std::stringstream str;
str << "Invalid sub object '" << obj->getFullName() << '.' << (subname ? subname : "")
<< "'";
throw Base::RuntimeError(str.str());
}
return sobj;
}
ViewProviderDocumentObject* tryGetSubViewProvider(
ViewProviderDocumentObject* vp,
App::DocumentObject* obj,
App::DocumentObject* sobj
) const
{
auto svp = vp;
if (sobj != obj) {
svp = freecad_cast<ViewProviderDocumentObject*>(
Application::Instance->getViewProvider(sobj)
);
if (!svp) {
std::stringstream str;
str << "Cannot edit '" << sobj->getFullName() << "' without view provider";
throw Base::RuntimeError(str.str());
}
}
return svp;
}
void setParentViewProvider(ViewProviderDocumentObject* vp)
{
_editViewProviderParent = vp;
}
void clearSubElement()
{
_editSubElement.clear();
_editSubname.clear();
}
void findElementName(const char* subname)
{
if (subname) {
const char* element = Data::findElementName(subname);
if (element) {
_editSubname = std::string(subname, element - subname);
_editSubElement = element;
}
else {
_editSubname = subname;
}
}
}
void findSubObjectList(App::DocumentObject* obj, const char* subname)
{
auto sobjs = obj->getSubObjectList(subname);
_editObjs.clear();
_editObjs.insert(sobjs.begin(), sobjs.end());
}
bool tryStartEditing(
ViewProviderDocumentObject* vp,
App::DocumentObject* obj,
const char* subname,
int ModNum
)
{
auto sobj = tryGetSubObject(obj, subname);
auto svp = tryGetSubViewProvider(vp, obj, sobj);
setParentViewProvider(vp);
clearSubElement();
findElementName(subname);
findSubObjectList(obj, subname);
return tryStartEditing(svp, sobj, ModNum);
}
bool tryStartEditing(ViewProviderDocumentObject* svp, App::DocumentObject* sobj, int ModNum)
{
_editingObject = sobj;
_editMode = ModNum;
_editViewProvider = svp->startEditing(ModNum);
if (!_editViewProvider) {
_editViewProviderParent = nullptr;
_editObjs.clear();
_editingObject = nullptr;
FC_LOG("object '" << sobj->getFullName() << "' refuse to edit");
return false;
}
return true;
}
void setEditingViewerIfPossible(View3DInventor* view3d, int ModNum)
{
if (view3d) {
view3d->getViewer()->setEditingViewProvider(_editViewProvider, ModNum);
_editingViewer = view3d->getViewer();
}
}
void signalEditMode()
{
if (auto vpd = freecad_cast<ViewProviderDocumentObject*>(_editViewProvider)) {
vpd->getDocument()->signalInEdit(*vpd);
}
}
void setDocumentNameOfTaskDialog(App::Document* doc)
{
Gui::TaskView::TaskDialog* dlg = Gui::Control().activeDialog();
if (dlg) {
dlg->setDocumentName(doc->getName());
}
}
};
class ParentFinder
{
public:
ParentFinder(App::DocumentObject* obj, ViewProviderDocumentObject* vp, const std::string& subname)
: obj {obj}
, vp {vp}
, subname {subname}
{}
App::DocumentObject* getObject() const
{
return obj;
}
ViewProviderDocumentObject* getViewProvider() const
{
return vp;
}
std::string getSubname() const
{
return subname;
}
bool findParent()
{
auto result = findParentAndSubName(obj);
App::DocumentObject* parentObj = std::get<0>(result);
if (parentObj) {
subname = std::get<1>(result);
obj = parentObj;
vp = findParentObject(parentObj, subname.c_str());
return true;
}
return false;
}
private:
static std::tuple<App::DocumentObject*, std::string> findParentAndSubName(App::DocumentObject* obj)
{
// No subname reference is given, we try to extract one from the current
// selection in order to obtain the correct transformation matrix below
auto sels = Gui::Selection().getCompleteSelection(ResolveMode::NoResolve);
App::DocumentObject* parentObj = nullptr;
std::string _subname;
for (auto& sel : sels) {
if (!sel.pObject || !sel.pObject->isAttachedToDocument()) {
continue;
}
if (!parentObj) {
parentObj = sel.pObject;
}
else if (parentObj != sel.pObject) {
FC_LOG("Cannot deduce subname for editing, more than one parent?");
parentObj = nullptr;
break;
}
auto sobj = parentObj->getSubObject(sel.SubName);
if (!sobj || (sobj != obj && sobj->getLinkedObject(true) != obj)) {
FC_LOG("Cannot deduce subname for editing, subname mismatch");
parentObj = nullptr;
break;
}
_subname = sel.SubName;
}
return std::make_tuple(parentObj, _subname);
}
static Gui::ViewProviderDocumentObject* findParentObject(
App::DocumentObject* parentObj,
const char* subname
)
{
FC_LOG("deduced editing reference " << parentObj->getFullName() << '.' << subname);
auto vp = freecad_cast<ViewProviderDocumentObject*>(
Application::Instance->getViewProvider(parentObj)
);
if (!vp || !vp->getDocument()) {
throw Base::RuntimeError("invalid view provider for parent object");
}
return vp;
}
private:
App::DocumentObject* obj;
ViewProviderDocumentObject* vp;
std::string subname;
};
} // namespace Gui
/* TRANSLATOR Gui::Document */
/// @namespace Gui @class Document
int Document::_iDocCount = 0;
Document::Document(App::Document* pcDocument, Application* app)
{
d = new DocumentP;
d->_iWinCount = 1;
// new instance
d->_iDocId = (++_iDocCount);
d->_isClosing = false;
d->_isModified = false;
d->_isTransacting = false;
d->_pcAppWnd = app;
d->_pcDocument = pcDocument;
d->_editViewProvider = nullptr;
d->_editViewProviderPrevious = nullptr;
d->_editingObject = nullptr;
d->_editViewProviderParent = nullptr;
d->_editingViewer = nullptr;
d->_editMode = 0;
d->_editModePrevious = 0;
d->_editWantsRestore = false;
d->_editWantsRestorePrevious = false;
// NOLINTBEGIN
// Setup the connections
d->connectNewObject = pcDocument->signalNewObject.connect(
std::bind(&Gui::Document::slotNewObject, this, sp::_1)
);
d->connectDelObject = pcDocument->signalDeletedObject.connect(
std::bind(&Gui::Document::slotDeletedObject, this, sp::_1)
);
d->connectCngObject = pcDocument->signalChangedObject.connect(
std::bind(&Gui::Document::slotChangedObject, this, sp::_1, sp::_2)
);
d->connectRenObject = pcDocument->signalRelabelObject.connect(
std::bind(&Gui::Document::slotRelabelObject, this, sp::_1)
);
d->connectActObject = pcDocument->signalActivatedObject.connect(
std::bind(&Gui::Document::slotActivatedObject, this, sp::_1)
);
d->connectActObjectBlocker = boost::signals2::shared_connection_block(d->connectActObject, false);
d->connectSaveDocument = pcDocument->signalSaveDocument.connect(
std::bind(&Gui::Document::Save, this, sp::_1)
);
d->connectRestDocument = pcDocument->signalRestoreDocument.connect(
std::bind(&Gui::Document::Restore, this, sp::_1)
);
d->connectStartLoadDocument = App::GetApplication().signalStartRestoreDocument.connect(
std::bind(&Gui::Document::slotStartRestoreDocument, this, sp::_1)
);
d->connectFinishLoadDocument = App::GetApplication().signalFinishRestoreDocument.connect(
std::bind(&Gui::Document::slotFinishRestoreDocument, this, sp::_1)
);
d->connectShowHidden = App::GetApplication().signalShowHidden.connect(
std::bind(&Gui::Document::slotShowHidden, this, sp::_1)
);
d->connectChangePropertyEditor = pcDocument->signalChangePropertyEditor.connect(
std::bind(&Gui::Document::slotChangePropertyEditor, this, sp::_1, sp::_2)
);
d->connectChangeDocument
= d->_pcDocument->signalChanged.connect // use the same slot function
(std::bind(&Gui::Document::slotChangePropertyEditor, this, sp::_1, sp::_2));
d->connectChangeDocumentBlocker
= boost::signals2::shared_connection_block(d->connectChangeDocument, true);
d->connectFinishRestoreObject = pcDocument->signalFinishRestoreObject.connect(
std::bind(&Gui::Document::slotFinishRestoreObject, this, sp::_1)
);
d->connectExportObjects = pcDocument->signalExportViewObjects.connect(
std::bind(&Gui::Document::exportObjects, this, sp::_1, sp::_2)
);
d->connectImportObjects = pcDocument->signalImportViewObjects.connect(
std::bind(&Gui::Document::importObjects, this, sp::_1, sp::_2, sp::_3)
);
d->connectFinishImportObjects = pcDocument->signalFinishImportObjects.connect(
std::bind(&Gui::Document::slotFinishImportObjects, this, sp::_1)
);
d->connectUndoDocument = pcDocument->signalUndo.connect(
std::bind(&Gui::Document::slotUndoDocument, this, sp::_1)
);
d->connectRedoDocument = pcDocument->signalRedo.connect(
std::bind(&Gui::Document::slotRedoDocument, this, sp::_1)
);
d->connectRecomputed = pcDocument->signalRecomputed.connect(
std::bind(&Gui::Document::slotRecomputed, this, sp::_1)
);
d->connectSkipRecompute = pcDocument->signalSkipRecompute.connect(
std::bind(&Gui::Document::slotSkipRecompute, this, sp::_1, sp::_2)
);
d->connectTouchedObject = pcDocument->signalTouchedObject.connect(
std::bind(&Gui::Document::slotTouchedObject, this, sp::_1)
);
d->connectTransactionAppend = pcDocument->signalTransactionAppend.connect(
std::bind(&Gui::Document::slotTransactionAppend, this, sp::_1, sp::_2)
);
d->connectTransactionRemove = pcDocument->signalTransactionRemove.connect(
std::bind(&Gui::Document::slotTransactionRemove, this, sp::_1, sp::_2)
);
// NOLINTEND
pcDocument->setPreRecomputeHook([this] { callSignalBeforeRecompute(); });
// pointer to the python class
// NOTE: As this Python object doesn't get returned to the interpreter we
// mustn't increment it (Werner Jan-12-2006)
Base::PyGILStateLocker lock;
_pcDocPy = new Gui::DocumentPy(this);
ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Document"
);
if (hGrp->GetBool("UsingUndo", true)) {
d->_pcDocument->setUndoMode(1);
// set the maximum stack size
d->_pcDocument->setMaxUndoStackSize(hGrp->GetInt("MaxUndoSize", 20));
}
d->_changeViewTouchDocument = hGrp->GetBool("ChangeViewProviderTouchDocument", true);
}
Document::~Document()
{
// disconnect everything to avoid to be double-deleted
// in case an exception is raised somewhere
d->connectNewObject.disconnect();
d->connectDelObject.disconnect();
d->connectCngObject.disconnect();
d->connectRenObject.disconnect();
d->connectActObject.disconnect();
d->connectSaveDocument.disconnect();
d->connectRestDocument.disconnect();
d->connectStartLoadDocument.disconnect();
d->connectFinishLoadDocument.disconnect();
d->connectShowHidden.disconnect();
d->connectFinishRestoreObject.disconnect();
d->connectExportObjects.disconnect();
d->connectImportObjects.disconnect();
d->connectFinishImportObjects.disconnect();
d->connectUndoDocument.disconnect();
d->connectRedoDocument.disconnect();
d->connectRecomputed.disconnect();
d->connectSkipRecompute.disconnect();
d->connectTransactionAppend.disconnect();
d->connectTransactionRemove.disconnect();
d->connectTouchedObject.disconnect();
d->connectChangePropertyEditor.disconnect();
d->connectChangeDocument.disconnect();
// e.g. if document gets closed from within a Python command
d->_isClosing = true;
// calls Document::detachView() and alter the view list
std::list<Gui::BaseView*> temp = d->baseViews;
for (auto& it : temp) {
it->deleteSelf();
}
std::map<const App::DocumentObject*, ViewProviderDocumentObject*>::iterator jt;
for (jt = d->_ViewProviderMap.begin(); jt != d->_ViewProviderMap.end(); ++jt) {
delete jt->second;
}
std::map<std::string, ViewProvider*>::iterator it2;
for (it2 = d->_ViewProviderMapAnnotation.begin(); it2 != d->_ViewProviderMapAnnotation.end();
++it2) {
delete it2->second;
}
// remove the reference from the object
Base::PyGILStateLocker lock;
_pcDocPy->setInvalid();
_pcDocPy->DecRef();
delete d;
}
//*****************************************************************************************************
// 3D viewer handling
//*****************************************************************************************************
bool Document::setEdit(Gui::ViewProvider* p, int ModNum, const char* subname)
{
try {
return trySetEdit(p, ModNum, subname);
}
catch (const Base::Exception& e) {
FC_ERR("" << e.what());
return false;
}
}
void Document::resetIfEditing()
{
// Fix regression: https://forum.freecad.org/viewtopic.php?f=19&t=43629&p=371972#p371972
// When an object is already in edit mode a subsequent call for editing is only possible
// when resetting the currently edited object.
if (d->_editViewProvider) {
_resetEdit();
}
}
View3DInventor* Document::openEditingView3D(const ViewProviderDocumentObject* vp)
{
auto view3d = dynamic_cast<View3DInventor*>(getActiveView());
// if the currently active view is not the 3d view search for it and activate it
if (view3d) {
getMainWindow()->setActiveWindow(view3d);
}
else {
view3d = dynamic_cast<View3DInventor*>(setActiveView(vp));
}
return view3d;
}
View3DInventor* Document::openEditingView3D(const App::DocumentObject* obj)
{
if (auto vp
= freecad_cast<ViewProviderDocumentObject*>(Application::Instance->getViewProvider(obj))) {
return openEditingView3D(vp);
}
return nullptr;
}
bool Document::trySetEdit(Gui::ViewProvider* p, int ModNum, const char* subname)
{
auto vp = DocumentP::throwIfCastFails(p);
resetIfEditing();
auto obj = DocumentP::tryGetObject(vp);
std::string _subname = subname ? subname : "";
if (_subname.empty()) {
ParentFinder finder(obj, vp, _subname);
if (finder.findParent()) {
_subname = finder.getSubname();
obj = finder.getObject();
vp = finder.getViewProvider();
if (vp->getDocument() != this) {
return vp->getDocument()->setEdit(vp, ModNum, _subname.c_str());
}
}
}
d->throwIfNotInMap(obj, getDocument());
Application::Instance->setEditDocument(this);
if (!d->tryStartEditing(vp, obj, _subname.c_str(), ModNum)) {
d->setDocumentNameOfTaskDialog(getDocument());
return false;
}
d->setDocumentNameOfTaskDialog(getDocument());
auto view3d = openEditingView3D(vp);
d->setEditingViewerIfPossible(view3d, ModNum);
d->signalEditMode();
App::AutoTransaction::setEnable(false);
return true;
}
const Base::Matrix4D& Document::getEditingTransform() const
{
return d->_editingTransform;
}
void Document::setEditingTransform(const Base::Matrix4D& mat)
{
d->_editObjs.clear();
d->_editingTransform = mat;
auto activeView = dynamic_cast<View3DInventor*>(getActiveView());
if (activeView) {
activeView->getViewer()->setEditingTransform(mat);
}
}
void Document::resetEdit()
{
bool vpIsNotNull = d->_editViewProvider != nullptr;
bool vpHasChanged = d->_editViewProvider != d->_editViewProviderPrevious;
int modeToRestore = d->_editModePrevious;
Gui::ViewProvider* vpToRestore = d->_editViewProviderPrevious;
bool shouldRestorePrevious = d->_editWantsRestorePrevious;
Application::Instance->setEditDocument(nullptr);
if (vpIsNotNull && vpHasChanged && shouldRestorePrevious) {
setEdit(vpToRestore, modeToRestore);
}
}
void Document::_resetEdit()
{
std::list<Gui::BaseView*>::iterator it;
if (d->_editViewProvider) {
for (it = d->baseViews.begin(); it != d->baseViews.end(); ++it) {
auto activeView = dynamic_cast<View3DInventor*>(*it);
if (activeView) {
activeView->getViewer()->resetEditingViewProvider();
}
}
d->_editViewProvider->finishEditing();
d->_editViewProviderPrevious = d->_editViewProvider;
d->_editModePrevious = d->_editMode;
d->_editWantsRestorePrevious = d->_editWantsRestore;
d->_editWantsRestore = false;
// Have to check d->_editViewProvider below, because there is a chance
// the editing object gets deleted inside the above call to
// 'finishEditing()', which will trigger our slotDeletedObject(), which
// nullifies _editViewProvider.
if (d->_editViewProvider
&& d->_editViewProvider->isDerivedFrom<ViewProviderDocumentObject>()) {
auto vpd = static_cast<ViewProviderDocumentObject*>(d->_editViewProvider);
vpd->getDocument()->signalResetEdit(*vpd);
}
d->_editViewProvider = nullptr;
// The logic below is not necessary anymore, because this method is
// changed into a private one, _resetEdit(). And the exposed
// resetEdit() above calls into Application->setEditDocument(0) which
// will prevent recursive calling.
App::GetApplication().closeActiveTransaction();
}
d->_editViewProviderParent = nullptr;
d->_editingViewer = nullptr;
d->_editObjs.clear();
d->_editingObject = nullptr;
if (Application::Instance->editDocument() == this) {
Application::Instance->setEditDocument(nullptr);
}
}
ViewProvider* Document::getInEdit(
ViewProviderDocumentObject** parentVp,
std::string* subname,
int* mode,
std::string* subelement
) const
{
if (parentVp) {
*parentVp = d->_editViewProviderParent;
}
if (subname) {
*subname = d->_editSubname;
}
if (subelement) {
*subelement = d->_editSubElement;
}
if (mode) {
*mode = d->_editMode;
}
if (d->_editViewProvider) {
// there is only one 3d view which is in edit mode
auto activeView = dynamic_cast<View3DInventor*>(getActiveView());
if (activeView && activeView->getViewer()->isEditingViewProvider()) {
return d->_editViewProvider;
}
}
return nullptr;
}
void Document::setInEdit(ViewProviderDocumentObject* parentVp, const char* subname)
{
if (d->_editViewProvider) {
d->_editViewProviderParent = parentVp;
d->_editSubname = subname ? subname : "";
}
}
void Document::setAnnotationViewProvider(const char* name, ViewProvider* pcProvider)
{
std::list<Gui::BaseView*>::iterator vIt;
// already in ?
std::map<std::string, ViewProvider*>::iterator it = d->_ViewProviderMapAnnotation.find(name);
if (it != d->_ViewProviderMapAnnotation.end()) {
removeAnnotationViewProvider(name);
}
// add
d->_ViewProviderMapAnnotation[name] = pcProvider;
// cycling to all views of the document
for (vIt = d->baseViews.begin(); vIt != d->baseViews.end(); ++vIt) {
auto activeView = dynamic_cast<View3DInventor*>(*vIt);
if (activeView) {
activeView->getViewer()->addViewProvider(pcProvider);
}
}
}
void Document::setEditRestore(bool askRestore)
{
d->_editWantsRestore = askRestore;
}
ViewProvider* Document::getAnnotationViewProvider(const char* name) const
{
std::map<std::string, ViewProvider*>::const_iterator it = d->_ViewProviderMapAnnotation.find(name);
return ((it != d->_ViewProviderMapAnnotation.end()) ? it->second : 0);
}
bool Document::isAnnotationViewProvider(const ViewProvider* vp) const
{
std::map<std::string, ViewProvider*>::const_iterator it;
for (it = d->_ViewProviderMapAnnotation.begin(); it != d->_ViewProviderMapAnnotation.end(); ++it) {
if (it->second == vp) {
return true;
}
}
return false;
}
ViewProvider* Document::takeAnnotationViewProvider(const char* name)
{
auto it = d->_ViewProviderMapAnnotation.find(name);
if (it == d->_ViewProviderMapAnnotation.end()) {
return nullptr;
}
ViewProvider* vp = it->second;
d->_ViewProviderMapAnnotation.erase(it);
// cycling to all views of the document
for (auto vIt : d->baseViews) {
if (auto activeView = dynamic_cast<View3DInventor*>(vIt)) {
activeView->getViewer()->removeViewProvider(vp);
}
}
return vp;
}
void Document::removeAnnotationViewProvider(const char* name)
{
delete takeAnnotationViewProvider(name);
}
ViewProvider* Document::getViewProvider(const App::DocumentObject* Feat) const
{
std::map<const App::DocumentObject*, ViewProviderDocumentObject*>::const_iterator it
= d->_ViewProviderMap.find(Feat);
return ((it != d->_ViewProviderMap.end()) ? it->second : 0);
}
std::vector<ViewProvider*> Document::getViewProvidersOfType(const Base::Type& typeId) const
{
std::vector<ViewProvider*> Objects;
for (std::map<const App::DocumentObject*, ViewProviderDocumentObject*>::const_iterator it
= d->_ViewProviderMap.begin();
it != d->_ViewProviderMap.end();
++it) {
if (it->second->isDerivedFrom(typeId)) {
Objects.push_back(it->second);
}
}
return Objects;
}
ViewProvider* Document::getViewProviderByName(const char* name) const
{
// first check on feature name
App::DocumentObject* pcFeat = getDocument()->getObject(name);
if (pcFeat) {
std::map<const App::DocumentObject*, ViewProviderDocumentObject*>::const_iterator it
= d->_ViewProviderMap.find(pcFeat);
if (it != d->_ViewProviderMap.end()) {
return it->second;
}
}
else {
// then try annotation name
std::map<std::string, ViewProvider*>::const_iterator it2
= d->_ViewProviderMapAnnotation.find(name);
if (it2 != d->_ViewProviderMapAnnotation.end()) {
return it2->second;
}
}
return nullptr;
}
bool Document::isShow(const char* name)
{
ViewProvider* pcProv = getViewProviderByName(name);
return pcProv ? pcProv->isShow() : false;
}
/// put the feature in show
void Document::setShow(const char* name)
{
ViewProvider* pcProv = getViewProviderByName(name);
if (pcProv && pcProv->isDerivedFrom<ViewProviderDocumentObject>()) {
static_cast<ViewProviderDocumentObject*>(pcProv)->Visibility.setValue(true);
}
}
/// set the feature in Noshow
void Document::setHide(const char* name)
{
ViewProvider* pcProv = getViewProviderByName(name);
if (pcProv && pcProv->isDerivedFrom<ViewProviderDocumentObject>()) {
static_cast<ViewProviderDocumentObject*>(pcProv)->Visibility.setValue(false);
}
}
/// set the feature in Noshow
void Document::setPos(const char* name, const Base::Matrix4D& rclMtrx)
{
ViewProvider* pcProv = getViewProviderByName(name);
if (pcProv) {
pcProv->setTransformation(rclMtrx);
}
}
//*****************************************************************************************************
// Document
//*****************************************************************************************************
void Document::slotNewObject(const App::DocumentObject& Obj)
{
auto pcProvider = static_cast<ViewProviderDocumentObject*>(getViewProvider(&Obj));
if (!pcProvider) {
std::string cName = Obj.getViewProviderNameStored();
for (;;) {
if (cName.empty()) {
// handle document object with no view provider specified
FC_LOG(Obj.getFullName() << " has no view provider specified");
return;
}
Base::Type type = Base::Type::getTypeIfDerivedFrom(
cName.c_str(),
ViewProviderDocumentObject::getClassTypeId(),
true
);
pcProvider = static_cast<ViewProviderDocumentObject*>(type.createInstance());
// createInstance could return a null pointer
if (!pcProvider) {
// type not derived from ViewProviderDocumentObject!!!
FC_ERR("Invalid view provider type '" << cName << "' for " << Obj.getFullName());
return;
}
else if (cName != Obj.getViewProviderName() && !pcProvider->allowOverride(Obj)) {
FC_WARN("View provider type '" << cName << "' does not support " << Obj.getFullName());
delete pcProvider;
pcProvider = nullptr;
cName = Obj.getViewProviderName();
}
else {
break;
}
}
setModified(true);
d->_ViewProviderMap[&Obj] = pcProvider;
d->_CoinMap[pcProvider->getRoot()] = pcProvider;
pcProvider->setStatus(Gui::ViewStatus::TouchDocument, d->_changeViewTouchDocument);
try {
// if successfully created set the right name and calculate the view
// FIXME: Consider to change argument of attach() to const pointer
pcProvider->attach(const_cast<App::DocumentObject*>(&Obj));
pcProvider->updateView();
pcProvider->setActiveMode();
}
catch (const Base::MemoryException& e) {
FC_ERR("Memory exception in " << Obj.getFullName() << " thrown: " << e.what());
}
catch (Base::Exception& e) {
e.reportException();
}
#ifndef FC_DEBUG
catch (...) {
FC_ERR("Unknown exception in Feature " << Obj.getFullName() << " thrown");
}
#endif
}
else {
try {
pcProvider->reattach(const_cast<App::DocumentObject*>(&Obj));
}
catch (Base::Exception& e) {
e.reportException();
}
}
if (pcProvider) {
std::list<Gui::BaseView*>::iterator vIt;
// cycling to all views of the document
for (vIt = d->baseViews.begin(); vIt != d->baseViews.end(); ++vIt) {
auto activeView = dynamic_cast<View3DInventor*>(*vIt);
if (activeView) {
activeView->getViewer()->addViewProvider(pcProvider);
}
}
// adding to the tree
signalNewObject(*pcProvider);
pcProvider->pcDocument = this;
// it is possible that a new viewprovider already claims children
handleChildren3D(pcProvider);
if (d->_isTransacting) {
d->_redoViewProviders.push_back(pcProvider);
}
}
}
void Document::slotDeletedObject(const App::DocumentObject& Obj)
{
std::list<Gui::BaseView*>::iterator vIt;
setModified(true);
// cycling to all views of the document
ViewProvider* viewProvider = getViewProvider(&Obj);
if (!viewProvider) {
return;
}
if (d->_editViewProvider == viewProvider || d->_editViewProviderParent == viewProvider) {
_resetEdit();
}
else if (Application::Instance->editDocument()) {
auto editDoc = Application::Instance->editDocument();
if (editDoc->d->_editViewProvider == viewProvider
|| editDoc->d->_editViewProviderParent == viewProvider) {
Application::Instance->setEditDocument(nullptr);
}
}
handleChildren3D(viewProvider, true);
if (viewProvider && viewProvider->isDerivedFrom(ViewProviderDocumentObject::getClassTypeId())) {
// go through the views
for (vIt = d->baseViews.begin(); vIt != d->baseViews.end(); ++vIt) {
auto activeView = dynamic_cast<View3DInventor*>(*vIt);
if (activeView) {
activeView->getViewer()->removeViewProvider(viewProvider);
}
}
// removing from tree
signalDeletedObject(*(static_cast<ViewProviderDocumentObject*>(viewProvider)));
}
viewProvider->beforeDelete();
}
void Document::beforeDelete()
{
auto editDoc = Application::Instance->editDocument();
if (editDoc) {
auto vp = freecad_cast<ViewProviderDocumentObject*>(editDoc->d->_editViewProvider);
auto vpp = freecad_cast<ViewProviderDocumentObject*>(editDoc->d->_editViewProviderParent);
if (editDoc == this || (vp && vp->getDocument() == this)
|| (vpp && vpp->getDocument() == this)) {
Application::Instance->setEditDocument(nullptr);
}
}
for (auto& v : d->_ViewProviderMap) {
v.second->beforeDelete();
}
}
void Document::slotChangedObject(const App::DocumentObject& Obj, const App::Property& Prop)
{
ViewProvider* viewProvider = getViewProvider(&Obj);
if (viewProvider) {
try {
viewProvider->update(&Prop);
if (d->_editingViewer && d->_editingObject && d->_editViewProviderParent
&& (Prop.isDerivedFrom<App::PropertyPlacement>()
// Issue ID 0004230 : getName() can return null in which case strstr() crashes
|| (Prop.getName() && strstr(Prop.getName(), "Scale")))
&& d->_editObjs.contains(&Obj)) {
Base::Matrix4D mat;
auto sobj = d->_editViewProviderParent->getObject()
->getSubObject(d->_editSubname.c_str(), nullptr, &mat);
if (sobj == d->_editingObject && d->_editingTransform != mat) {
d->_editingTransform = mat;
d->_editingViewer->setEditingTransform(d->_editingTransform);
}
}
}
catch (const Base::MemoryException& e) {
FC_ERR("Memory exception in " << Obj.getFullName() << " thrown: " << e.what());
}
catch (Base::Exception& e) {
e.reportException();
}
catch (const std::exception& e) {
FC_ERR("C++ exception in " << Obj.getFullName() << " thrown " << e.what());
}
catch (...) {
FC_ERR("Cannot update representation for " << Obj.getFullName());
}
handleChildren3D(viewProvider);
if (viewProvider->isDerivedFrom<ViewProviderDocumentObject>()) {
signalChangedObject(static_cast<ViewProviderDocumentObject&>(*viewProvider), Prop);
}
}
// a property of an object has changed
if (!Prop.testStatus(App::Property::NoModify) && !isModified()) {
FC_LOG(Prop.getFullName() << " modified");
setModified(true);
}
getMainWindow()->updateActions(true);
}
void Document::slotRelabelObject(const App::DocumentObject& Obj)
{
ViewProvider* viewProvider = getViewProvider(&Obj);
if (viewProvider && viewProvider->isDerivedFrom<ViewProviderDocumentObject>()) {
signalRelabelObject(*(static_cast<ViewProviderDocumentObject*>(viewProvider)));
}
}
void Document::slotTransactionAppend(const App::DocumentObject& obj, App::Transaction* transaction)
{
ViewProvider* viewProvider = getViewProvider(&obj);
if (viewProvider && viewProvider->isDerivedFrom<ViewProviderDocumentObject>()) {
transaction->addObjectDel(viewProvider);
}
}
void Document::slotTransactionRemove(const App::DocumentObject& obj, App::Transaction* transaction)
{
std::map<const App::DocumentObject*, ViewProviderDocumentObject*>::const_iterator it
= d->_ViewProviderMap.find(&obj);
if (it != d->_ViewProviderMap.end()) {
ViewProvider* viewProvider = it->second;
auto itC = d->_CoinMap.find(viewProvider->getRoot());
if (itC != d->_CoinMap.end()) {
d->_CoinMap.erase(itC);
}
d->_ViewProviderMap.erase(&obj);
// transaction being a nullptr indicates that undo/redo is off and the object
// can be safely deleted
if (transaction) {
transaction->addObjectNew(viewProvider);
}
else {
delete viewProvider;
}
}
}
void Document::slotActivatedObject(const App::DocumentObject& Obj)
{
ViewProvider* viewProvider = getViewProvider(&Obj);
if (viewProvider && viewProvider->isDerivedFrom<ViewProviderDocumentObject>()) {
signalActivatedObject(*(static_cast<ViewProviderDocumentObject*>(viewProvider)));
}
}
void Document::slotUndoDocument(const App::Document& doc)
{
if (d->_pcDocument != &doc) {
return;
}
signalUndoDocument(*this);
getMainWindow()->updateActions();
}
void Document::slotRedoDocument(const App::Document& doc)
{
if (d->_pcDocument != &doc) {
return;
}
signalRedoDocument(*this);
getMainWindow()->updateActions();
}
void Document::slotRecomputed(const App::Document& doc)
{
if (d->_pcDocument != &doc) {
return;
}
getMainWindow()->updateActions();
TreeWidget::updateStatus();
}
// This function is called when some asks to recompute a document that is marked
// as 'SkipRecompute'. We'll check if we are the current document, and if either
// not given an explicit recomputing object list, or the given single object is
// the eidting object or the active object. If the conditions are met, we'll
// force recompute only that object and all its dependent objects.
void Document::slotSkipRecompute(const App::Document& doc, const std::vector<App::DocumentObject*>& objs)
{
if (d->_pcDocument != &doc) {
return;
}
if (objs.size() > 1 || App::GetApplication().getActiveDocument() != &doc
|| !doc.testStatus(App::Document::AllowPartialRecompute)) {
return;
}
App::DocumentObject* obj = nullptr;
auto editDoc = Application::Instance->editDocument();
if (editDoc) {
auto vp = freecad_cast<ViewProviderDocumentObject*>(editDoc->getInEdit());
if (vp) {
obj = vp->getObject();
}
}
if (!obj) {
obj = doc.getActiveObject();
}
if (!obj || !obj->isAttachedToDocument() || (!objs.empty() && objs.front() != obj)) {
return;
}
obj->recomputeFeature(true);
}
void Document::slotTouchedObject(const App::DocumentObject& Obj)
{
getMainWindow()->updateActions(true);
if (!isModified()) {
FC_LOG(Obj.getFullName() << " touched");
setModified(true);
}
}
// helper that guarantees signalBeforeRecompute call is executed in the GUI thread and
// that the worker waits until it finishes
void Document::callSignalBeforeRecompute()
{
auto invokeSignalBeforeRecompute = [this] {
// this runs in the GUI thread
this->getDocument()->signalBeforeRecompute(*this->getDocument());
};
if (QThread::currentThread() == qApp->thread()) {
// already on GUI thread no hop, just call it
invokeSignalBeforeRecompute();
}
else {
// hop to GUI and *block* until it returns
QMetaObject::invokeMethod(
qApp,
std::move(invokeSignalBeforeRecompute),
Qt::BlockingQueuedConnection
);
}
}
void Document::addViewProvider(Gui::ViewProviderDocumentObject* vp)
{
// Hint: The undo/redo first adds the view provider to the Gui
// document before adding the objects to the App document.
// the view provider is added by TransactionViewProvider and an
// object can be there only once
assert(d->_ViewProviderMap.find(vp->getObject()) == d->_ViewProviderMap.end());
vp->setStatus(Detach, false);
d->_ViewProviderMap[vp->getObject()] = vp;
d->_CoinMap[vp->getRoot()] = vp;
}
void Document::setModified(bool b)
{
if (d->_isModified == b) {
return;
}
d->_isModified = b;
std::list<MDIView*> mdis = getMDIViews();
for (auto& mdi : mdis) {
mdi->setWindowModified(b);
}
}
bool Document::isModified() const
{
return d->_isModified;
}
bool Document::isAboutToClose() const
{
return d->_isClosing;
}
ViewProviderDocumentObject* Document::getViewProviderByPathFromTail(SoPath* path) const
{
// Get the lowest root node in the pick path!
for (int i = 0; i < path->getLength(); i++) {
SoNode* node = path->getNodeFromTail(i);
if (node->isOfType(SoSeparator::getClassTypeId())) {
auto it = d->_CoinMap.find(static_cast<SoSeparator*>(node));
if (it != d->_CoinMap.end()) {
return it->second;
}
}
}
return nullptr;
}
ViewProviderDocumentObject* Document::getViewProviderByPathFromHead(SoPath* path) const
{
for (int i = 0; i < path->getLength(); i++) {
SoNode* node = path->getNode(i);
if (node->isOfType(SoSeparator::getClassTypeId())) {
auto it = d->_CoinMap.find(static_cast<SoSeparator*>(node));
if (it != d->_CoinMap.end()) {
return it->second;
}
}
}
return nullptr;
}
ViewProviderDocumentObject* Document::getViewProvider(SoNode* node) const
{
if (!node || !node->isOfType(SoSeparator::getClassTypeId())) {
return nullptr;
}
auto it = d->_CoinMap.find(static_cast<SoSeparator*>(node));
if (it != d->_CoinMap.end()) {
return it->second;
}
return nullptr;
}
std::vector<std::pair<ViewProviderDocumentObject*, int>> Document::getViewProvidersByPath(
SoPath* path
) const
{
std::vector<std::pair<ViewProviderDocumentObject*, int>> ret;
for (int i = 0; i < path->getLength(); i++) {
SoNode* node = path->getNodeFromTail(i);
if (node->isOfType(SoSeparator::getClassTypeId())) {
auto it = d->_CoinMap.find(static_cast<SoSeparator*>(node));
if (it != d->_CoinMap.end()) {
ret.emplace_back(it->second, i);
}
}
}
return ret;
}
App::Document* Document::getDocument() const
{
return d->_pcDocument;
}
static bool checkCanonicalPath(const std::map<App::Document*, bool>& docs)
{
std::map<QString, std::vector<App::Document*>> paths;
bool warn = false;
for (auto doc : App::GetApplication().getDocuments()) {
QFileInfo info(QString::fromUtf8(doc->FileName.getValue()));
auto& d = paths[info.canonicalFilePath()];
d.push_back(doc);
if (!warn && d.size() > 1) {
if (docs.contains(d.front()) || docs.contains(d.back())) {
warn = true;
}
}
}
if (!warn) {
return true;
}
QString msg;
QTextStream ts(&msg);
ts << QObject::tr("Identical physical path detected. It may cause unwanted overwrite of existing document!\n\n")
<< QObject::tr("Are you sure you want to continue?");
auto docName = [](App::Document* doc) -> QString {
if (doc->Label.getStrValue() == doc->getName()) {
return QString::fromLatin1(doc->getName());
}
return QStringLiteral("%1 (%2)").arg(
QString::fromUtf8(doc->Label.getValue()),
QString::fromLatin1(doc->getName())
);
};
int count = 0;
for (auto& v : paths) {
if (v.second.size() <= 1) {
continue;
}
for (auto doc : v.second) {
if (docs.contains(doc)) {
FC_WARN("Physical path: " << v.first.toUtf8().constData());
for (auto d : v.second) {
FC_WARN(
" Document: " << docName(d).toUtf8().constData() << ": "
<< d->FileName.getValue()
);
}
if (count == 3) {
ts << "\n\n" << QObject::tr("Check report view for more…");
}
else if (count < 3) {
ts << "\n\n"
<< QObject::tr("Physical path:") << ' ' << v.first << "\n"
<< QObject::tr("Document:") << ' ' << docName(doc) << "\n "
<< QObject::tr("Path:") << ' ' << QString::fromUtf8(doc->FileName.getValue());
for (auto d : v.second) {
if (d == doc) {
continue;
}
ts << "\n"
<< QObject::tr("Document:") << ' ' << docName(d) << "\n "
<< QObject::tr("Path:") << ' '
<< QString::fromUtf8(d->FileName.getValue());
}
}
++count;
break;
}
}
}
int ret = QMessageBox::warning(
getMainWindow(),
QObject::tr("Identical physical path"),
msg,
QMessageBox::Yes,
QMessageBox::No
);
return ret == QMessageBox::Yes;
}
bool Document::askIfSavingFailed(const QString& error)
{
int ret = QMessageBox::question(
getMainWindow(),
QObject::tr("Could not save document"),
QObject::tr(
"There was an issue trying to save the file. "
"This may be because some of the parent folders do not exist, "
"or you do not have sufficient permissions, "
"or for other reasons. Error details:\n\n\"%1\"\n\n"
"Would you like to save the file with a different name?"
)
.arg(error),
QMessageBox::Yes,
QMessageBox::No
);
if (ret == QMessageBox::No) {
// TODO: Understand what exactly is supposed to be returned here
getMainWindow()->showMessage(QObject::tr("Saving aborted"), 2000);
return false;
}
else if (ret == QMessageBox::Yes) {
return saveAs();
}
return false;
}
/// Save the document
bool Document::save()
{
if (d->_pcDocument->isSaved()) {
try {
std::vector<App::Document*> docs;
std::map<App::Document*, bool> dmap;
try {
docs = getDocument()->getDependentDocuments();
for (auto it = docs.begin(); it != docs.end();) {
App::Document* doc = *it;
if (doc == getDocument()) {
dmap[doc] = doc->mustExecute();
++it;
continue;
}
auto gdoc = Application::Instance->getDocument(doc);
if ((gdoc && !gdoc->isModified()) || doc->testStatus(App::Document::PartialDoc)
|| doc->testStatus(App::Document::TempDoc)) {
it = docs.erase(it);
continue;
}
dmap[doc] = doc->mustExecute();
++it;
}
}
catch (const Base::RuntimeError& e) {
FC_ERR(e.what());
docs = {getDocument()};
dmap.clear();
dmap[getDocument()] = getDocument()->mustExecute();
}
if (docs.size() > 1) {
int ret = QMessageBox::question(
getMainWindow(),
QObject::tr("Save dependent files"),
QObject::tr(
"The file contains external dependencies. "
"Do you want to save the dependent files, too?"
),
QMessageBox::Yes,
QMessageBox::No
);
if (ret != QMessageBox::Yes) {
docs = {getDocument()};
dmap.clear();
dmap[getDocument()] = getDocument()->mustExecute();
}
}
if (!checkCanonicalPath(dmap)) {
return false;
}
Gui::WaitCursor wc;
// save all documents
for (auto doc : docs) {
// Changed 'mustExecute' status may be triggered by saving external document
if (!dmap[doc] && doc->mustExecute()) {
App::AutoTransaction trans("Recompute");
Command::doCommand(
Command::Doc,
"App.getDocument(\"%s\").recompute()",
doc->getName()
);
}
Command::doCommand(Command::Doc, "App.getDocument(\"%s\").save()", doc->getName());
auto gdoc = Application::Instance->getDocument(doc);
if (gdoc) {
gdoc->setModified(false);
}
}
}
catch (const Base::FileException& e) {
e.reportException();
return askIfSavingFailed(QString::fromUtf8(e.what()));
}
catch (const Base::Exception& e) {
QMessageBox::critical(
getMainWindow(),
QObject::tr("Saving document failed"),
QString::fromLatin1(e.what())
);
return false;
}
return true;
}
else {
return saveAs();
}
}
/// Save the document under a new file name
bool Document::saveAs()
{
getMainWindow()->showMessage(QObject::tr("Save document under new filename…"));
QString exe = qApp->applicationName();
QString name = QString::fromUtf8(getDocument()->FileName.getValue());
if (name.isEmpty()) {
name = QString::fromUtf8(getDocument()->Label.getValue());
}
QString fn = FileDialog::getSaveFileName(
getMainWindow(),
QObject::tr("Save %1 Document").arg(exe),
name,
QStringLiteral("%1 %2 (*.FCStd)").arg(exe, QObject::tr("Document"))
);
if (!fn.isEmpty()) {
QFileInfo fi;
fi.setFile(fn);
const char* DocName = App::GetApplication().getDocumentName(getDocument());
// save as new file name
try {
Gui::WaitCursor wc;
std::string escapedstr = Base::Tools::escapedUnicodeFromUtf8(fn.toUtf8());
escapedstr = Base::Tools::escapeEncodeFilename(escapedstr);
Command::doCommand(
Command::Doc,
"App.getDocument(\"%s\").saveAs(u\"%s\")",
DocName,
escapedstr.c_str()
);
// App::Document::saveAs() may modify the passed file name
fi.setFile(QString::fromUtf8(d->_pcDocument->FileName.getValue()));
setModified(false);
getMainWindow()->appendRecentFile(fi.filePath());
}
catch (const Base::FileException& e) {
e.reportException();
return askIfSavingFailed(QString::fromUtf8(e.what()));
}
catch (const Base::Exception& e) {
QMessageBox::critical(
getMainWindow(),
QObject::tr("Saving document failed"),
QString::fromLatin1(e.what())
);
}
return true;
}
else {
getMainWindow()->showMessage(QObject::tr("Saving aborted"), 2000);
return false;
}
}
void Document::saveAll()
{
std::vector<App::Document*> docs;
try {
docs = App::Document::getDependentDocuments(App::GetApplication().getDocuments(), true);
}
catch (Base::Exception& e) {
e.reportException();
int ret = QMessageBox::critical(
getMainWindow(),
QObject::tr("Failed to save document"),
QObject::tr("Documents contains cyclic dependencies. Do you still want to save them?"),
QMessageBox::Yes,
QMessageBox::No
);
if (ret != QMessageBox::Yes) {
return;
}
docs = App::GetApplication().getDocuments();
}
std::map<App::Document*, bool> dmap;
for (auto doc : docs) {
if (doc->testStatus(App::Document::PartialDoc) || doc->testStatus(App::Document::TempDoc)) {
continue;
}
dmap[doc] = doc->mustExecute();
}
if (!checkCanonicalPath(dmap)) {
return;
}
for (auto doc : docs) {
if (doc->testStatus(App::Document::PartialDoc) || doc->testStatus(App::Document::TempDoc)) {
continue;
}
auto gdoc = Application::Instance->getDocument(doc);
if (!gdoc) {
continue;
}
if (!doc->isSaved()) {
if (!gdoc->saveAs()) {
break;
}
}
Gui::WaitCursor wc;
try {
// Changed 'mustExecute' status may be triggered by saving external document
if (!dmap[doc] && doc->mustExecute()) {
App::AutoTransaction trans("Recompute");
Command::doCommand(Command::Doc, "App.getDocument('%s').recompute()", doc->getName());
}
Command::doCommand(Command::Doc, "App.getDocument('%s').save()", doc->getName());
gdoc->setModified(false);
}
catch (const Base::Exception& e) {
QMessageBox::critical(
getMainWindow(),
QObject::tr("Failed to save document")
+ QStringLiteral(": %1").arg(QString::fromUtf8(doc->getName())),
QString::fromLatin1(e.what())
);
break;
}
}
}
/// Save a copy of the document under a new file name
bool Document::saveCopy()
{
getMainWindow()->showMessage(QObject::tr("Save a copy of the document under new filename…"));
QString exe = qApp->applicationName();
QString fn = FileDialog::getSaveFileName(
getMainWindow(),
QObject::tr("Save %1 Document").arg(exe),
QString::fromUtf8(getDocument()->FileName.getValue()),
QObject::tr("%1 document (*.FCStd)").arg(exe)
);
if (!fn.isEmpty()) {
const char* DocName = App::GetApplication().getDocumentName(getDocument());
// save as new file name
Gui::WaitCursor wc;
QString pyfn = Base::Tools::escapeEncodeFilename(fn);
Command::doCommand(
Command::Doc,
"App.getDocument(\"%s\").saveCopy(\"%s\")",
DocName,
(const char*)pyfn.toUtf8()
);
return true;
}
else {
getMainWindow()->showMessage(QObject::tr("Saving aborted"), 2000);
return false;
}
}
unsigned int Document::getMemSize() const
{
unsigned int size = 0;
// size of the view providers in the document
std::map<const App::DocumentObject*, ViewProviderDocumentObject*>::const_iterator it;
for (it = d->_ViewProviderMap.begin(); it != d->_ViewProviderMap.end(); ++it) {
size += it->second->getMemSize();
}
return size;
}
/**
* Adds a separate XML file to the projects file that contains information about the view providers.
*/
void Document::Save(Base::Writer& writer) const
{
// It's only possible to add extra information if force of XML is disabled
if (!writer.isForceXML()) {
writer.addFile("GuiDocument.xml", this);
ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Document"
);
if (hGrp->GetBool("SaveThumbnail", true)) {
int size = hGrp->GetInt("ThumbnailSize", 256);
size = Base::clamp<int>(size, 64, 512);
std::list<MDIView*> mdi = getMDIViews();
for (const auto& it : mdi) {
if (it->isDerivedFrom<View3DInventor>()) {
View3DInventorViewer* view = static_cast<View3DInventor*>(it)->getViewer();
d->thumb.setFileName(d->_pcDocument->FileName.getValue());
d->thumb.setSize(size);
d->thumb.setViewer(view);
d->thumb.Save(writer);
break;
}
}
}
}
}
/**
* Loads a separate XML file from the projects file with information about the view providers.
*/
void Document::Restore(Base::XMLReader& reader)
{
reader.addFile("GuiDocument.xml", this);
// hide all elements to avoid to update the 3d view when loading data files
// RestoreDocFile then restores the visibility status again
std::map<const App::DocumentObject*, ViewProviderDocumentObject*>::iterator it;
for (it = d->_ViewProviderMap.begin(); it != d->_ViewProviderMap.end(); ++it) {
it->second->startRestoring();
it->second->setStatus(Gui::isRestoring, true);
}
}
/**
* Restores the properties of the view providers.
*/
void Document::RestoreDocFile(Base::Reader& reader)
{
// We must create an XML parser to read from the input stream
std::shared_ptr<Base::XMLReader> localreader
= std::make_shared<Base::XMLReader>("GuiDocument.xml", reader);
localreader->FileVersion = reader.getFileVersion();
localreader->readElement("Document");
long scheme = localreader->getAttribute<long>("SchemaVersion");
localreader->DocumentSchema = scheme;
localreader->ProgramVersion = d->_pcDocument->getProgramVersion();
bool hasExpansion = localreader->hasAttribute("HasExpansion");
if (hasExpansion) {
auto tree = TreeWidget::instance();
if (tree) {
auto docItem = tree->getDocumentItem(this);
if (docItem) {
docItem->Restore(*localreader);
}
}
}
// At this stage all the document objects and their associated view providers exist.
// Now we must restore the properties of the view providers only.
//
// SchemeVersion "1"
if (scheme == 1) {
// read the viewproviders itself
localreader->readElement("ViewProviderData");
int Cnt = localreader->getAttribute<long>("Count");
for (int i = 0; i < Cnt; i++) {
localreader->readElement("ViewProvider");
std::string name = localreader->getAttribute<const char*>("name");
bool expanded = false;
if (!hasExpansion && localreader->hasAttribute("expanded")) {
const char* attr = localreader->getAttribute<const char*>("expanded");
if (strcmp(attr, "1") == 0) {
expanded = true;
}
}
int treeRank = -1;
if (localreader->hasAttribute("treeRank")) {
treeRank = localreader->getAttribute<int>("treeRank");
}
auto pObj = freecad_cast<ViewProviderDocumentObject*>(getViewProviderByName(name.c_str()));
// check if this feature has been registered
if (pObj) {
pObj->Restore(*localreader);
}
if (pObj && treeRank >= 0) {
pObj->setTreeRank(treeRank);
}
if (pObj && expanded) {
this->signalExpandObject(*pObj, TreeItemMode::ExpandItem, 0, 0);
}
localreader->readEndElement("ViewProvider");
}
localreader->readEndElement("ViewProviderData");
// read camera settings
localreader->readElement("Camera");
const char* ppReturn = localreader->getAttribute<const char*>("settings");
cameraSettings.clear();
if (!Base::Tools::isNullOrEmpty(ppReturn)) {
saveCameraSettings(ppReturn);
try {
const char** pReturnIgnore = nullptr;
std::list<MDIView*> mdi = getMDIViews();
for (const auto& it : mdi) {
if (it->onHasMsg("SetCamera")) {
it->onMsg(cameraSettings.c_str(), pReturnIgnore);
}
}
}
catch (const Base::Exception& e) {
Base::Console().error("%s\n", e.what());
}
}
}
reader.initLocalReader(localreader);
// reset modified flag
setModified(false);
}
void Document::slotStartRestoreDocument(const App::Document& doc)
{
if (d->_pcDocument != &doc) {
return;
}
// disable this signal while loading a document
d->connectActObjectBlocker.block();
}
void Document::slotFinishRestoreObject(const App::DocumentObject& obj)
{
auto vpd = freecad_cast<ViewProviderDocumentObject*>(getViewProvider(&obj));
if (vpd) {
vpd->setStatus(Gui::isRestoring, false);
vpd->finishRestoring();
if (!vpd->canAddToSceneGraph()) {
toggleInSceneGraph(vpd);
}
}
}
void Document::slotFinishRestoreDocument(const App::Document& doc)
{
if (d->_pcDocument != &doc) {
return;
}
d->connectActObjectBlocker.unblock();
App::DocumentObject* act = doc.getActiveObject();
if (act) {
ViewProvider* viewProvider = getViewProvider(act);
if (viewProvider && viewProvider->isDerivedFrom<ViewProviderDocumentObject>()) {
signalActivatedObject(*(static_cast<ViewProviderDocumentObject*>(viewProvider)));
}
}
// reset modified flag
setModified(doc.testStatus(App::Document::LinkStampChanged));
}
void Document::slotShowHidden(const App::Document& doc)
{
if (d->_pcDocument != &doc) {
return;
}
Application::Instance->signalShowHidden(*this);
}
/**
* Saves the properties of the view providers.
*/
void Document::SaveDocFile(Base::Writer& writer) const
{
writer.Stream() << "<?xml version='1.0' encoding='utf-8'?>" << std::endl
<< "<!--" << std::endl
<< " FreeCAD Document, see https://www.freecad.org for more information…"
<< std::endl
<< "-->" << std::endl;
writer.Stream() << "<Document SchemaVersion=\"1\"";
writer.incInd();
auto tree = TreeWidget::instance();
bool hasExpansion = false;
if (tree) {
auto docItem = tree->getDocumentItem(this);
if (docItem) {
hasExpansion = true;
writer.Stream() << " HasExpansion=\"1\">" << std::endl;
docItem->Save(writer);
}
}
if (!hasExpansion) {
writer.Stream() << ">" << std::endl;
}
// writing the view provider names itself
writer.Stream() << writer.ind() << "<ViewProviderData Count=\"" << d->_ViewProviderMap.size()
<< "\">" << std::endl;
bool xml = writer.isForceXML();
// writer.setForceXML(true);
writer.incInd(); // indentation for 'ViewProvider name'
for (const auto& it : d->_ViewProviderMap) {
const App::DocumentObject* doc = it.first;
ViewProviderDocumentObject* obj = it.second;
writer.Stream() << writer.ind() << "<ViewProvider name=\"" << doc->getNameInDocument() << "\""
<< " expanded=\"" << (doc->testStatus(App::Expand) ? 1 : 0) << "\""
<< " treeRank=\"" << obj->getTreeRank() << "\"";
if (obj->hasExtensions()) {
writer.Stream() << " Extensions=\"True\"";
}
writer.Stream() << ">" << std::endl;
obj->Save(writer);
writer.Stream() << writer.ind() << "</ViewProvider>" << std::endl;
}
writer.setForceXML(xml);
writer.decInd(); // indentation for 'ViewProvider name'
writer.Stream() << writer.ind() << "</ViewProviderData>" << std::endl;
writer.decInd(); // indentation for 'ViewProviderData Count'
// save camera settings
std::list<MDIView*> mdi = getMDIViews();
for (const auto& it : mdi) {
if (it->onHasMsg("GetCamera")) {
const char* ppReturn = nullptr;
it->onMsg("GetCamera", &ppReturn);
if (saveCameraSettings(ppReturn)) {
break;
}
}
}
writer.incInd(); // indentation for camera settings
writer.Stream() << writer.ind() << "<Camera settings=\"" << encodeAttribute(getCameraSettings())
<< "\"/>\n";
writer.decInd(); // indentation for camera settings
writer.Stream() << "</Document>" << std::endl;
}
void Document::exportObjects(const std::vector<App::DocumentObject*>& obj, Base::Writer& writer)
{
writer.Stream() << "<?xml version='1.0' encoding='utf-8'?>" << std::endl;
writer.Stream() << "<Document SchemaVersion=\"1\">" << std::endl;
std::map<const App::DocumentObject*, ViewProvider*> views;
for (const auto& it : obj) {
Document* doc = Application::Instance->getDocument(it->getDocument());
if (doc) {
ViewProvider* vp = doc->getViewProvider(it);
if (vp) {
views[it] = vp;
}
}
}
// writing the view provider names itself
writer.incInd(); // indentation for 'ViewProviderData Count'
writer.Stream() << writer.ind() << "<ViewProviderData Count=\"" << views.size() << "\">"
<< std::endl;
bool xml = writer.isForceXML();
// writer.setForceXML(true);
writer.incInd(); // indentation for 'ViewProvider name'
std::map<const App::DocumentObject*, ViewProvider*>::const_iterator jt;
for (jt = views.begin(); jt != views.end(); ++jt) {
const App::DocumentObject* doc = jt->first;
ViewProvider* vp = jt->second;
writer.Stream() << writer.ind() << "<ViewProvider name=\"" << doc->getExportName() << "\" "
<< "expanded=\"" << (doc->testStatus(App::Expand) ? 1 : 0) << "\"";
if (vp->hasExtensions()) {
writer.Stream() << " Extensions=\"True\"";
}
writer.Stream() << ">" << std::endl;
vp->Save(writer);
writer.Stream() << writer.ind() << "</ViewProvider>" << std::endl;
}
writer.setForceXML(xml);
writer.decInd(); // indentation for 'ViewProvider name'
writer.Stream() << writer.ind() << "</ViewProviderData>" << std::endl;
writer.decInd(); // indentation for 'ViewProviderData Count'
writer.incInd(); // indentation for camera settings
writer.Stream() << writer.ind() << "<Camera settings=\"\"/>" << std::endl;
writer.decInd(); // indentation for camera settings
writer.Stream() << "</Document>" << std::endl;
}
void Document::importObjects(
const std::vector<App::DocumentObject*>& obj,
Base::Reader& reader,
const std::map<std::string, std::string>& nameMapping
)
{
// We must create an XML parser to read from the input stream
std::shared_ptr<Base::XMLReader> localreader
= std::make_shared<Base::XMLReader>("GuiDocument.xml", reader);
localreader->readElement("Document");
long scheme = localreader->getAttribute<long>("SchemaVersion");
// At this stage all the document objects and their associated view providers exist.
// Now we must restore the properties of the view providers only.
//
// SchemeVersion "1"
if (scheme == 1) {
// read the viewproviders itself
localreader->readElement("ViewProviderData");
int Cnt = localreader->getAttribute<long>("Count");
auto it = obj.begin();
for (int i = 0; i < Cnt && it != obj.end(); ++i, ++it) {
// The stored name usually doesn't match with the current name anymore
// thus we try to match by type. This should work because the order of
// objects should not have changed
localreader->readElement("ViewProvider");
std::string name = localreader->getAttribute<const char*>("name");
auto jt = nameMapping.find(name);
if (jt != nameMapping.end()) {
name = jt->second;
}
bool expanded = false;
if (localreader->hasAttribute("expanded")) {
const char* attr = localreader->getAttribute<const char*>("expanded");
if (strcmp(attr, "1") == 0) {
expanded = true;
}
}
Gui::ViewProvider* pObj = this->getViewProviderByName(name.c_str());
if (pObj) {
pObj->setStatus(Gui::isRestoring, true);
auto vpd = freecad_cast<ViewProviderDocumentObject*>(pObj);
if (vpd) {
vpd->startRestoring();
}
pObj->Restore(*localreader);
if (expanded && vpd) {
this->signalExpandObject(*vpd, TreeItemMode::ExpandItem, 0, 0);
}
}
localreader->readEndElement("ViewProvider");
if (it == obj.end()) {
break;
}
}
localreader->readEndElement("ViewProviderData");
}
localreader->readEndElement("Document");
// In the file GuiDocument.xml new data files might be added
if (localreader->hasFilenames()) {
reader.initLocalReader(localreader);
}
}
void Document::slotFinishImportObjects(const std::vector<App::DocumentObject*>& objs)
{
(void)objs;
// finishRestoring() is now triggered by signalFinishRestoreObject
//
// for(auto obj : objs) {
// auto vp = getViewProvider(obj);
// if(!vp) continue;
// vp->setStatus(Gui::isRestoring,false);
// auto vpd = freecad_cast<ViewProviderDocumentObject*>(vp);
// if(vpd) vpd->finishRestoring();
// }
}
void Document::addRootObjectsToGroup(
const std::vector<App::DocumentObject*>& obj,
App::DocumentObjectGroup* grp
)
{
std::map<App::DocumentObject*, bool> rootMap;
for (const auto it : obj) {
rootMap[it] = true;
}
// get the view providers and check which objects are children
for (const auto it : obj) {
Gui::ViewProvider* vp = getViewProvider(it);
if (vp) {
std::vector<App::DocumentObject*> child = vp->claimChildren();
for (const auto& jt : child) {
auto kt = rootMap.find(jt);
if (kt != rootMap.end()) {
kt->second = false;
}
}
}
}
// all objects that are not children of other objects can be added to the group
for (const auto& it : rootMap) {
if (it.second) {
grp->addObject(it.first);
}
}
}
MDIView* Document::createView(const Base::Type& typeId, CreateViewMode mode)
{
if (!typeId.isDerivedFrom(MDIView::getClassTypeId())) {
return nullptr;
}
std::list<MDIView*> theViews = this->getMDIViewsOfType(typeId);
if (typeId == View3DInventor::getClassTypeId()) {
QOpenGLWidget* shareWidget = nullptr;
// VBO rendering doesn't work correctly when we don't share the OpenGL widgets
if (!theViews.empty()) {
auto firstView = static_cast<View3DInventor*>(theViews.front());
shareWidget = qobject_cast<QOpenGLWidget*>(firstView->getViewer()->getGLWidget());
const char* ppReturn = nullptr;
firstView->onMsg("GetCamera", &ppReturn);
saveCameraSettings(ppReturn);
}
auto view3D = new View3DInventor(this, getMainWindow(), shareWidget);
if (!theViews.empty()) {
auto firstView = static_cast<View3DInventor*>(theViews.front());
std::string overrideMode = firstView->getViewer()->getOverrideMode();
view3D->getViewer()->setOverrideMode(overrideMode);
}
// attach the viewproviders. we need to make sure that we only attach the toplevel ones
// and not viewproviders which are claimed by other providers. To ensure this we first
// add all providers and then remove the ones already claimed
std::map<const App::DocumentObject*, ViewProviderDocumentObject*>::const_iterator It1;
std::vector<App::DocumentObject*> child_vps;
for (It1 = d->_ViewProviderMap.begin(); It1 != d->_ViewProviderMap.end(); ++It1) {
view3D->getViewer()->addViewProvider(It1->second);
std::vector<App::DocumentObject*> children = It1->second->claimChildren3D();
child_vps.insert(child_vps.end(), children.begin(), children.end());
}
std::map<std::string, ViewProvider*>::const_iterator It2;
for (It2 = d->_ViewProviderMapAnnotation.begin(); It2 != d->_ViewProviderMapAnnotation.end();
++It2) {
view3D->getViewer()->addViewProvider(It2->second);
std::vector<App::DocumentObject*> children = It2->second->claimChildren3D();
child_vps.insert(child_vps.end(), children.begin(), children.end());
}
for (App::DocumentObject* obj : child_vps) {
view3D->getViewer()->removeViewProvider(getViewProvider(obj));
}
// When cloning the view, don't increment the window counter as the old view will be deleted
// shortly after.
if (mode != CreateViewMode::Clone) {
const char* name = getDocument()->Label.getValue();
QString title
= QStringLiteral("%1 : %2[*]").arg(QString::fromUtf8(name)).arg(d->_iWinCount++);
view3D->setWindowTitle(title);
}
view3D->setWindowModified(this->isModified());
view3D->resize(400, 300);
if (!cameraSettings.empty()) {
const char* ppReturn = nullptr;
view3D->onMsg(cameraSettings.c_str(), &ppReturn);
}
// When cloning the view, don't add the view to the main window. The whole purpose of the
// workaround using cloned views is that the view can be shown in undocked/fullscreen mode
// without having been docked before.
if (mode != CreateViewMode::Clone) {
getMainWindow()->addWindow(view3D);
}
view3D->getViewer()->redraw();
return view3D;
}
return nullptr;
}
const char* Document::getCameraSettings() const
{
return cameraSettings.size() > 10 ? cameraSettings.c_str() + 10 : cameraSettings.c_str();
}
bool Document::saveCameraSettings(const char* settings) const
{
if (!settings) {
return false;
}
// skip starting comment lines
bool skipping = false;
char c = *settings;
for (; c; c = *(++settings)) {
if (skipping) {
if (c == '\n') {
skipping = false;
}
}
else if (c == '#') {
skipping = true;
}
else if (!std::isspace(c)) {
break;
}
}
if (!c) {
return false;
}
cameraSettings = std::string("SetCamera ") + settings;
return true;
}
void Document::attachView(Gui::BaseView* pcView, bool bPassiv)
{
if (!bPassiv) {
d->baseViews.push_back(pcView);
}
else {
d->passiveViews.push_back(pcView);
}
}
void Document::detachView(Gui::BaseView* pcView, bool bPassiv)
{
if (bPassiv) {
if (find(d->passiveViews.begin(), d->passiveViews.end(), pcView) != d->passiveViews.end()) {
d->passiveViews.remove(pcView);
}
}
else {
if (find(d->baseViews.begin(), d->baseViews.end(), pcView) != d->baseViews.end()) {
d->baseViews.remove(pcView);
}
// last view?
if (d->baseViews.empty()) {
// decouple a passive view
std::list<Gui::BaseView*>::iterator it = d->passiveViews.begin();
while (it != d->passiveViews.end()) {
(*it)->setDocument(nullptr);
it = d->passiveViews.begin();
}
// is already closing the document, and is not linked by other documents
if (!d->_isClosing && App::PropertyXLink::getDocumentInList(getDocument()).empty()) {
d->_pcAppWnd->onLastWindowClosed(this);
}
}
}
}
void Document::onUpdate()
{
#ifdef FC_LOGUPDATECHAIN
Base::Console().log("Acti: Gui::Document::onUpdate()");
#endif
std::list<Gui::BaseView*>::iterator it;
for (it = d->baseViews.begin(); it != d->baseViews.end(); ++it) {
(*it)->onUpdate();
}
for (it = d->passiveViews.begin(); it != d->passiveViews.end(); ++it) {
(*it)->onUpdate();
}
}
void Document::onRelabel()
{
#ifdef FC_LOGUPDATECHAIN
Base::Console().log("Acti: Gui::Document::onRelabel()");
#endif
std::list<Gui::BaseView*>::iterator it;
for (it = d->baseViews.begin(); it != d->baseViews.end(); ++it) {
(*it)->onRelabel(this);
}
for (it = d->passiveViews.begin(); it != d->passiveViews.end(); ++it) {
(*it)->onRelabel(this);
}
d->connectChangeDocumentBlocker.unblock();
}
bool Document::isLastView()
{
if (d->baseViews.size() <= 1) {
return true;
}
return false;
}
/**
* This method checks if the document can be closed. It checks on
* the save state of the document and is able to abort the closing.
*/
bool Document::canClose(bool checkModify, bool checkLink)
{
if (d->_isClosing) {
return true;
}
if (!getDocument()->isClosable()) {
QMessageBox::warning(
getActiveView(),
QObject::tr("Document not closable"),
QObject::tr("The document is not closable for the moment.")
);
return false;
}
// else if (!Gui::Control().isAllowedAlterDocument()) {
// std::string name = Gui::Control().activeDialog()->getDocumentName();
// if (name == this->getDocument()->getName()) {
// QMessageBox::warning(getActiveView(),
// QObject::tr("Document not closable"),
// QObject::tr("The document is in editing mode and thus cannot be closed for the
// moment.\n"
// "You either have to finish or cancel the editing in the task panel."));
// Gui::TaskView::TaskDialog* dlg = Gui::Control().activeDialog();
// if (dlg) Gui::Control().showDialog(dlg);
// return false;
// }
// }
if (checkLink && !App::PropertyXLink::getDocumentInList(getDocument()).empty()) {
return true;
}
if (getDocument()->testStatus(App::Document::TempDoc)) {
return true;
}
bool ok = true;
if (checkModify && isModified() && !getDocument()->testStatus(App::Document::PartialDoc)) {
int res = getMainWindow()->confirmSave(getDocument(), getActiveView());
switch (res) {
case MainWindow::ConfirmSaveResult::Cancel:
ok = false;
break;
case MainWindow::ConfirmSaveResult::SaveAll:
case MainWindow::ConfirmSaveResult::Save:
ok = save();
if (!ok) {
const QString docName = QString::fromStdString(getDocument()->Label.getStrValue());
const QString text
= (!docName.isEmpty()
? QObject::tr("Failed to save document '%1'. Would you like to cancel the closure?")
.arg(docName)
: QObject::tr(
"Document saving failed. Would you like to cancel the closure?"
));
int ret = QMessageBox::question(
getActiveView(),
QObject::tr("Unable to save document"),
text,
QMessageBox::Discard | QMessageBox::Cancel,
QMessageBox::Discard
);
if (ret == QMessageBox::Discard) {
ok = true;
}
}
break;
case MainWindow::ConfirmSaveResult::DiscardAll:
case MainWindow::ConfirmSaveResult::Discard:
ok = true;
break;
}
}
if (ok) {
// If a task dialog is open that doesn't allow other commands to modify
// the document it must be closed by resetting the edit mode of the
// corresponding view provider.
if (!Gui::Control().isAllowedAlterDocument()) {
std::string name = Gui::Control().activeDialog()->getDocumentName();
if (name == this->getDocument()->getName()) {
// getInEdit() only checks if the currently active MDI view is
// a 3D view and that it is in edit mode. However, when closing a
// document then the edit mode must be reset independent of the
// active view.
if (d->_editViewProvider) {
this->_resetEdit();
}
}
}
}
return ok;
}
std::list<MDIView*> Document::getMDIViews() const
{
std::list<MDIView*> views;
for (std::list<BaseView*>::const_iterator it = d->baseViews.begin(); it != d->baseViews.end();
++it) {
auto view = dynamic_cast<MDIView*>(*it);
if (view) {
views.push_back(view);
}
}
return views;
}
std::list<MDIView*> Document::getMDIViewsOfType(const Base::Type& typeId) const
{
std::list<MDIView*> views;
for (std::list<BaseView*>::const_iterator it = d->baseViews.begin(); it != d->baseViews.end();
++it) {
auto view = dynamic_cast<MDIView*>(*it);
if (view && view->isDerivedFrom(typeId)) {
views.push_back(view);
}
}
return views;
}
/// send messages to the active view
bool Document::sendMsgToViews(const char* pMsg)
{
std::list<Gui::BaseView*>::iterator it;
const char** pReturnIgnore = nullptr;
for (it = d->baseViews.begin(); it != d->baseViews.end(); ++it) {
if ((*it)->onMsg(pMsg, pReturnIgnore)) {
return true;
}
}
for (it = d->passiveViews.begin(); it != d->passiveViews.end(); ++it) {
if ((*it)->onMsg(pMsg, pReturnIgnore)) {
return true;
}
}
return false;
}
bool Document::sendMsgToFirstView(const Base::Type& typeId, const char* pMsg, const char** ppReturn)
{
// first try the active view
Gui::MDIView* view = getActiveView();
if (view && view->isDerivedFrom(typeId)) {
if (view->onMsg(pMsg, ppReturn)) {
return true;
}
}
// now try the other views
std::list<Gui::MDIView*> views = getMDIViewsOfType(typeId);
for (const auto& it : views) {
if ((it != view) && it->onMsg(pMsg, ppReturn)) {
return true;
}
}
return false;
}
/// Getter for the active view
MDIView* Document::getActiveView() const
{
// get the main window's active view
MDIView* active = getMainWindow()->activeWindow();
// get all MDI views of the document
std::list<MDIView*> mdis = getMDIViews();
// check whether the active view is part of this document
bool ok = false;
for (const auto& mdi : mdis) {
if (mdi == active) {
ok = true;
break;
}
}
if (ok) {
return active;
}
// the active view is not part of this document, just use the last view
const auto& windows = Gui::getMainWindow()->windows();
for (auto rit = mdis.rbegin(); rit != mdis.rend(); ++rit) {
// Some view is removed from window list for some reason, e.g. TechDraw
// hidden page has view but not in the list. By right, the view will
// self delete, but not the case for TechDraw, especially during
// document restore.
if (windows.contains(*rit) || (*rit)->isDerivedFrom<View3DInventor>()) {
return *rit;
}
}
return nullptr;
}
MDIView* Document::setActiveView(const ViewProviderDocumentObject* vp, Base::Type typeId)
{
MDIView* view = nullptr;
if (!vp) {
view = getActiveView();
}
else {
view = vp->getMDIView();
if (!view) {
auto obj = vp->getObject();
if (!obj) {
view = getActiveView();
}
else {
auto linked = obj->getLinkedObject(true);
if (linked != obj) {
auto vpLinked = freecad_cast<ViewProviderDocumentObject*>(
Application::Instance->getViewProvider(linked)
);
if (vpLinked) {
view = vpLinked->getMDIView();
}
}
if (!view && typeId.isBad()) {
MDIView* active = getActiveView();
if (active && active->containsViewProvider(vp)) {
view = active;
}
else {
typeId = View3DInventor::getClassTypeId();
}
}
}
}
}
if (!view || (!typeId.isBad() && !view->isDerivedFrom(typeId))) {
view = nullptr;
for (auto* v : d->baseViews) {
if (v->isDerivedFrom<MDIView>() && (typeId.isBad() || v->isDerivedFrom(typeId))) {
view = static_cast<MDIView*>(v);
break;
}
}
}
if (!view && !typeId.isBad()) {
view = createView(typeId);
}
if (view) {
getMainWindow()->setActiveWindow(view);
}
return view;
}
/**
* @brief Document::setActiveWindow
* If this document is active and the view is part of it then it will be
* activated. If the document is not active or if the view is already active
* nothing is done.
* @param view
*/
void Document::setActiveWindow(Gui::MDIView* view)
{
// get the main window's active view
MDIView* active = getMainWindow()->activeWindow();
// view is already active
if (active == view) {
return;
}
// get all MDI views of the document
std::list<MDIView*> mdis = getMDIViews();
// this document is not active
if (std::ranges::find(mdis, active) == mdis.end()) {
return;
}
// the view is not part of the document
if (std::ranges::find(mdis, view) == mdis.end()) {
return;
}
getMainWindow()->setActiveWindow(view);
}
Gui::MDIView* Document::getViewOfNode(SoNode* node) const
{
std::list<MDIView*> mdis = getMDIViewsOfType(View3DInventor::getClassTypeId());
for (const auto& mdi : mdis) {
auto view = static_cast<View3DInventor*>(mdi);
if (view->getViewer()->searchNode(node)) {
return mdi;
}
}
return nullptr;
}
Gui::MDIView* Document::getViewOfViewProvider(const Gui::ViewProvider* vp) const
{
return getViewOfNode(vp->getRoot());
}
Gui::MDIView* Document::getEditingViewOfViewProvider(Gui::ViewProvider* vp) const
{
std::list<MDIView*> mdis = getMDIViewsOfType(View3DInventor::getClassTypeId());
for (const auto& mdi : mdis) {
auto view = static_cast<View3DInventor*>(mdi);
View3DInventorViewer* viewer = view->getViewer();
// there is only one 3d view which is in edit mode
if (viewer->hasViewProvider(vp) && viewer->isEditingViewProvider()) {
return mdi;
}
}
return nullptr;
}
//--------------------------------------------------------------------------
// UNDO REDO transaction handling
//--------------------------------------------------------------------------
/** Open a new Undo transaction on the active document
* This method opens a new UNDO transaction on the active document. This transaction
* will later appear in the UNDO/REDO dialog with the name of the command. If the user
* recall the transaction everything changed on the document between OpenCommand() and
* CommitCommand will be undone (or redone). You can use an alternative name for the
* operation default is the command name.
* @see CommitCommand(),AbortCommand()
*/
void Document::openCommand(const char* sName)
{
getDocument()->openTransaction(sName);
}
void Document::commitCommand()
{
getDocument()->commitTransaction();
}
void Document::abortCommand()
{
getDocument()->abortTransaction();
}
bool Document::hasPendingCommand() const
{
return getDocument()->hasPendingTransaction();
}
/// Get a string vector with the 'Undo' actions
std::vector<std::string> Document::getUndoVector() const
{
return getDocument()->getAvailableUndoNames();
}
/// Get a string vector with the 'Redo' actions
std::vector<std::string> Document::getRedoVector() const
{
return getDocument()->getAvailableRedoNames();
}
bool Document::checkTransactionID(bool undo, int iSteps)
{
if (!iSteps) {
return false;
}
std::vector<int> ids;
for (int i = 0; i < iSteps; i++) {
int id = getDocument()->getTransactionID(undo, i);
if (!id) {
break;
}
ids.push_back(id);
}
std::set<App::Document*> prompts;
std::map<App::Document*, int> dmap;
for (auto doc : App::GetApplication().getDocuments()) {
if (doc == getDocument()) {
continue;
}
for (auto id : ids) {
int steps = undo ? doc->getAvailableUndos(id) : doc->getAvailableRedos(id);
if (!steps) {
continue;
}
int& currentSteps = dmap[doc];
if (currentSteps + 1 != steps) {
prompts.insert(doc);
}
if (currentSteps < steps) {
currentSteps = steps;
}
}
}
if (!prompts.empty()) {
std::ostringstream str;
int i = 0;
for (auto doc : prompts) {
if (i++ == 5) {
str << "...\n";
break;
}
str << " " << doc->getName() << "\n";
}
int ret = QMessageBox::warning(
getMainWindow(),
undo ? QObject::tr("Undo") : QObject::tr("Redo"),
QStringLiteral("%1,\n%2%3")
.arg(
QObject::tr(
"There are grouped transactions in the following documents with "
"other preceding transactions"
),
QString::fromStdString(str.str()),
QObject::tr(
"Choose 'Yes' to roll back all preceding transactions.\n"
"Choose 'No' to roll back in the active document only.\n"
"Choose 'Abort' to abort"
)
),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Abort,
QMessageBox::Yes
);
if (ret == QMessageBox::Abort) {
return false;
}
if (ret == QMessageBox::No) {
return true;
}
}
for (auto& v : dmap) {
for (int i = 0; i < v.second; ++i) {
if (undo) {
v.first->undo();
}
else {
v.first->redo();
}
}
}
return true;
}
bool Document::isPerformingTransaction() const
{
return d->_isTransacting;
}
/// Will UNDO one or more steps
void Document::undo(int iSteps)
{
Base::FlagToggler<> flag(d->_isTransacting);
if (!checkTransactionID(true, iSteps)) {
return;
}
for (int i = 0; i < iSteps; i++) {
getDocument()->undo();
}
App::GetApplication().signalUndo();
}
/// Will REDO one or more steps
void Document::redo(int iSteps)
{
Base::FlagToggler<> flag(d->_isTransacting);
if (!checkTransactionID(false, iSteps)) {
return;
}
for (int i = 0; i < iSteps; i++) {
getDocument()->redo();
}
App::GetApplication().signalRedo();
for (auto it : d->_redoViewProviders) {
handleChildren3D(it);
}
d->_redoViewProviders.clear();
}
PyObject* Document::getPyObject()
{
_pcDocPy->IncRef();
return _pcDocPy;
}
void Document::handleChildren3D(ViewProvider* viewProvider, bool deleting)
{
// check for children
if (viewProvider && viewProvider->getChildRoot()) {
std::vector<App::DocumentObject*> children = viewProvider->claimChildren3D();
SoGroup* childGroup = viewProvider->getChildRoot();
SoGroup* frontGroup = viewProvider->getFrontRoot();
SoGroup* backGroup = viewProvider->getFrontRoot();
// size not the same -> build up the list new
if (deleting || childGroup->getNumChildren() != static_cast<int>(children.size())) {
std::set<ViewProviderDocumentObject*> oldChildren;
for (int i = 0, count = childGroup->getNumChildren(); i < count; ++i) {
auto it = d->_CoinMap.find(static_cast<SoSeparator*>(childGroup->getChild(i)));
if (it == d->_CoinMap.end()) {
continue;
}
oldChildren.insert(it->second);
}
Gui::coinRemoveAllChildren(childGroup);
Gui::coinRemoveAllChildren(frontGroup);
Gui::coinRemoveAllChildren(backGroup);
if (!deleting) {
for (const auto& it : children) {
if (auto ChildViewProvider
= dynamic_cast<ViewProviderDocumentObject*>(getViewProvider(it))) {
auto itOld = oldChildren.find(ChildViewProvider);
if (itOld != oldChildren.end()) {
oldChildren.erase(itOld);
}
if (SoSeparator* childRootNode = ChildViewProvider->getRoot()) {
if (childRootNode == childGroup) {
Base::Console().warning(
"Document::handleChildren3D: Do not add "
"group of '%s' to itself\n",
it->getNameInDocument()
);
}
else if (childGroup) {
childGroup->addChild(childRootNode);
}
}
if (SoSeparator* childFrontNode = ChildViewProvider->getFrontRoot()) {
if (childFrontNode == frontGroup) {
Base::Console().warning(
"Document::handleChildren3D: Do not add "
"foreground group of '%s' to itself\n",
it->getNameInDocument()
);
}
else if (frontGroup) {
frontGroup->addChild(childFrontNode);
}
}
if (SoSeparator* childBackNode = ChildViewProvider->getBackRoot()) {
if (childBackNode == backGroup) {
Base::Console().warning(
"Document::handleChildren3D: Do not add "
"background group of '%s' to itself\n",
it->getNameInDocument()
);
}
else if (backGroup) {
backGroup->addChild(childBackNode);
}
}
// cycling to all views of the document to remove the viewprovider from the
// viewer itself
for (Gui::BaseView* vIt : d->baseViews) {
auto activeView = dynamic_cast<View3DInventor*>(vIt);
if (activeView
&& activeView->getViewer()->hasViewProvider(ChildViewProvider)) {
// @Note hasViewProvider()
// remove the viewprovider serves the purpose of detaching the
// inventor nodes from the top level root in the viewer. However, if
// some of the children were grouped beneath the object earlier they
// are not anymore part of the toplevel inventor node. we need to
// check for that.
activeView->getViewer()->removeViewProvider(ChildViewProvider);
}
}
}
}
}
// add the remaining old children back to toplevel invertor node
for (auto vpd : oldChildren) {
auto obj = vpd->getObject();
if (!obj || !obj->isAttachedToDocument()) {
continue;
}
for (BaseView* view : d->baseViews) {
auto activeView = dynamic_cast<View3DInventor*>(view);
if (activeView && !activeView->getViewer()->hasViewProvider(vpd)) {
activeView->getViewer()->addViewProvider(vpd);
}
}
}
}
}
}
void Document::toggleInSceneGraph(ViewProvider* vp)
{
// FIXME: What's the point of having this function?
//
for (auto view : d->baseViews) {
auto activeView = dynamic_cast<View3DInventor*>(view);
if (!activeView) {
continue;
}
auto root = vp->getRoot();
if (!root) {
continue;
}
auto scenegraph = dynamic_cast<SoGroup*>(activeView->getViewer()->getSceneGraph());
if (!scenegraph) {
continue;
}
// If it cannot be added then only check the top-level nodes
if (!vp->canAddToSceneGraph()) {
int idx = scenegraph->findChild(root);
if (idx >= 0) {
scenegraph->removeChild(idx);
}
}
else {
// Do a deep search of the scene because the root node
// isn't necessarily a top-level node when claimed by
// another view provider.
// This is to avoid to add a node twice to the scene.
SoSearchAction sa;
sa.setNode(root);
sa.setSearchingAll(false);
sa.apply(scenegraph);
SoPath* path = sa.getPath();
if (!path) {
scenegraph->addChild(root);
}
}
}
}
void Document::slotChangePropertyEditor(const App::Document& doc, const App::Property& Prop)
{
if (getDocument() == &doc) {
FC_LOG(Prop.getFullName() << " editor changed");
setModified(true);
getMainWindow()->setUserSchema(doc.UnitSystem.getValue());
}
}
std::vector<App::DocumentObject*> Document::getTreeRootObjects() const
{
std::vector<App::DocumentObject*> docObjects = d->_pcDocument->getObjects();
std::unordered_map<App::DocumentObject*, bool> rootMap;
for (auto it : docObjects) {
rootMap[it] = true;
}
for (auto obj : docObjects) {
ViewProvider* vp = Application::Instance->getViewProvider(obj);
if (!vp) {
continue;
}
std::vector<App::DocumentObject*> children = vp->claimChildren();
for (auto child : children) {
rootMap[child] = false;
}
}
std::vector<App::DocumentObject*> rootObjs;
for (const auto& it : rootMap) {
if (it.second) {
rootObjs.push_back(it.first);
}
}
return rootObjs;
}