/***************************************************************************
 *   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);

    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) {
                resetIfEditing();

                return vp->getDocument()->setEdit(vp, ModNum, _subname.c_str());
            }
        }
    }

    // Fix for #13852: When switching edit directly between sketches, resetIfEditing()
    // triggers unsetEdit() on the previous sketch which restores its selection.
    // This clobbers the selection of the new sketch that ParentFinder relies on.
    // Moving resetIfEditing() after ParentFinder ensures we resolve the parent context correctly
    // using the current selection before closing the previous edit.
    resetIfEditing();

    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::fromUtf8(doc->getName());
        }
        return QStringLiteral("%1 (%2)").arg(
            QString::fromUtf8(doc->Label.getValue()),
            QString::fromUtf8(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();

            View3DInventorViewer* view = nullptr;
            for (const auto& it : mdi) {
                if (it->isDerivedFrom<View3DInventor>()) {
                    view = static_cast<View3DInventor*>(it)->getViewer();
                    break;
                }
            }

            d->thumb.setFileName(d->_pcDocument->FileName.getValue());
            d->thumb.setSize(size);
            d->thumb.setViewer(view);
            d->thumb.Save(writer);
        }
    }
}

/**
 * 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;
}
