From de4651bc99beac2d904504b5b0fc1d73bc77304d Mon Sep 17 00:00:00 2001 From: "Zheng, Lei" Date: Fri, 28 Jun 2019 17:45:27 +0800 Subject: [PATCH] App: transaction related API changes Introduce a new concept of transaction ID. Each transaction must be unique inside the document. Multiple transactions from different documents can be grouped together with the same transaction ID. This makes it possible to undo/redo single operation that contains changes from multiple documents due to external linking. Application: * get/set/closeActiveTransaction() is used to setup potential transactions with a given name. The transaction is only created when there is actual changes. If objects from multiple documents are changed under the same active transaction, they will have the same trasnaction ID, and can be undo/redo togtether later. * signalUndo/signalRedo, new signals triggered once after an undo/redo operation. Unlike signalUndo/RedoDocument, these signals will only be triggered once even if there may be multiple documents involved during undo/redo. * signal(Before)CloseTransaction, new signals triggered before/after an actual transaction is created or aborted. AutoTransaction: * Helper class to enable automatic management of transactions. See class document for more details. This class will be used by Gui::Command in later patches to allow better automation of transactions in command. Document: * open/commit/abortTransaction() are now redirected to call Application::get/set/closeActiveTransaction() instead. * _openTransaction() is added to do the real creation of transaction. * _checkTransaction() is modified to create transaction on actual change of any property. * getTransactionID() is used to find out the position of a transaction with a given ID. When triggering undo in external document, it may be necessary to perform multi-step undo/redo in order to match for the transaction ID. Transaction/TransactionObject: * Various changes for the new transaction ID concept. * Support undo/redo add/remove dynamic property --- src/App/Application.cpp | 156 ++++++++++++++++++ src/App/Application.h | 113 +++++++++++++ src/App/ApplicationPy.cpp | 56 +++++++ src/App/Document.cpp | 271 +++++++++++++++++++++++++++----- src/App/Document.h | 62 ++++++-- src/App/DocumentPy.xml | 21 ++- src/App/DocumentPyImp.cpp | 8 + src/App/TransactionalObject.cpp | 15 -- src/App/TransactionalObject.h | 7 - src/App/Transactions.cpp | 256 +++++++++++++++++++----------- src/App/Transactions.h | 43 +++-- 11 files changed, 835 insertions(+), 173 deletions(-) diff --git a/src/App/Application.cpp b/src/App/Application.cpp index 871d0b1cb4..c5a69897a2 100644 --- a/src/App/Application.cpp +++ b/src/App/Application.cpp @@ -251,6 +251,7 @@ init_freecad_module(void) Application::Application(std::map &mConfig) : _mConfig(mConfig), _pActiveDoc(0), _objCount(-1) + , _activeTransactionID(0), _activeTransactionGuard(0), _activeTransactionTmpName(false) { //_hApp = new ApplicationOCC; mpcPramManager["System parameter"] = _pcSysParamMngr; @@ -648,6 +649,161 @@ void Application::setActiveDocument(const char *Name) } } +AutoTransaction::AutoTransaction(const char *name, bool tmpName) { + auto &app = GetApplication(); + if(name && app._activeTransactionGuard>=0) { + if(!app.getActiveTransaction() + || (!tmpName && app._activeTransactionTmpName)) + { + FC_LOG("auto transaction '" << name << "', " << tmpName); + tid = app.setActiveTransaction(name); + app._activeTransactionTmpName = tmpName; + } + } + // We use negative transaction guard to disable auto transaction from here + // and any stack below. This is to support user setting active transaction + // before having any existing AutoTransaction on stack, or 'persist' + // transaction that can out live AutoTransaction. + if(app._activeTransactionGuard<0) + --app._activeTransactionGuard; + else if(tid || app._activeTransactionGuard>0) + ++app._activeTransactionGuard; + else if(app.getActiveTransaction()) { + FC_LOG("auto transaction disabled because of '" << app._activeTransactionName << "'"); + --app._activeTransactionGuard; + } else + ++app._activeTransactionGuard; + FC_TRACE("construct auto Transaction " << app._activeTransactionGuard); +} + +AutoTransaction::~AutoTransaction() { + auto &app = GetApplication(); + FC_TRACE("before destruct auto Transaction " << app._activeTransactionGuard); + if(app._activeTransactionGuard<0) + ++app._activeTransactionGuard; + else if(!app._activeTransactionGuard) { +#ifdef FC_DEBUG + FC_ERR("Transaction guard error"); +#endif + } else if(--app._activeTransactionGuard == 0) { + try { + // We don't call close() here, because close() only closes + // transaction that we opened during construction time. However, + // when _activeTransactionGuard reaches zero here, we are supposed + // to close any transaction opened. + app.closeActiveTransaction(); + } catch(Base::Exception &e) { + e.ReportException(); + } catch(...) + {} + } + FC_TRACE("destruct auto Transaction " << app._activeTransactionGuard); +} + +void AutoTransaction::close(bool abort) { + if(tid || abort) { + GetApplication().closeActiveTransaction(abort,abort?0:tid); + tid = 0; + } +} + +void AutoTransaction::setEnable(bool enable) { + auto &app = GetApplication(); + if(!app._activeTransactionGuard) + return; + if((enable && app._activeTransactionGuard>0) + || (!enable && app._activeTransactionGuard<0)) + return; + app._activeTransactionGuard = -app._activeTransactionGuard; + FC_TRACE("toggle auto Transaction " << app._activeTransactionGuard); + if(!enable && app._activeTransactionTmpName) { + bool close = true; + for(auto &v : app.DocMap) { + if(v.second->hasPendingTransaction()) { + close = false; + break; + } + } + if(close) + app.closeActiveTransaction(); + } +} + +int Application::setActiveTransaction(const char *name, bool persist) { + if(!name || !name[0]) + name = "Command"; + + if(_activeTransactionGuard>0 && getActiveTransaction()) { + if(_activeTransactionTmpName) { + FC_LOG("transaction rename to '" << name << "'"); + for(auto &v : DocMap) + v.second->renameTransaction(name,_activeTransactionID); + }else + return 0; + }else{ + FC_LOG("set active transaction '" << name << "'"); + _activeTransactionID = 0; + for(auto &v : DocMap) + v.second->_commitTransaction(); + _activeTransactionID = Transaction::getNewID(); + } + _activeTransactionTmpName = false; + _activeTransactionName = name; + if(persist) + AutoTransaction::setEnable(false); + return _activeTransactionID; +} + +const char *Application::getActiveTransaction(int *id) const { + int tid = 0; + if(Transaction::getLastID() == _activeTransactionID) + tid = _activeTransactionID; + if(id) *id = tid; + return tid?_activeTransactionName.c_str():0; +} + +void Application::closeActiveTransaction(bool abort, int id) { + if(!id) id = _activeTransactionID; + if(!id) return; + + if(_activeTransactionGuard>0 && !abort) { + FC_LOG("ignore close transaction"); + return; + } + + FC_LOG("close transaction '" << _activeTransactionName << "' " << abort); + _activeTransactionID = 0; + + TransactionSignaller siganller(abort,false); + for(auto &v : DocMap) { + if(v.second->getTransactionID(true) != id) + continue; + if(abort) + v.second->_abortTransaction(); + else + v.second->_commitTransaction(); + } +} + +static int _TransSignalCount; +static bool _TransSignalled; +Application::TransactionSignaller::TransactionSignaller(bool abort, bool signal) + :abort(abort) +{ + ++_TransSignalCount; + if(signal && !_TransSignalled) { + _TransSignalled = true; + GetApplication().signalBeforeCloseTransaction(abort); + } +} + +Application::TransactionSignaller::~TransactionSignaller() { + if(--_TransSignalCount == 0 && _TransSignalled) { + _TransSignalled = false; + GetApplication().signalCloseTransaction(abort); + } +} + const char* Application::getHomePath(void) const { return _mConfig["AppHomePath"].c_str(); diff --git a/src/App/Application.h b/src/App/Application.h index b38e0daa7e..255fb0c541 100644 --- a/src/App/Application.h +++ b/src/App/Application.h @@ -48,6 +48,7 @@ class Document; class DocumentObject; class ApplicationObserver; class Property; +class AutoTransaction; enum GetLinkOption { /// Get all links (both directly and in directly) linked to the given object @@ -105,6 +106,37 @@ public: void setActiveDocument(const char *Name); /// close all documents (without saving) void closeAllDocuments(void); + /** @name Application-wide trandaction setting */ + //@{ + /** Setup a pending application-wide active transaction + * + * @param name: new transaction name + * @param persist: by default, if the calling code is inside any invokation + * of a command, it will be auto closed once all command within the current + * stack exists. To disable auto closing, set persist=true + * + * @return The new transaction ID. + * + * Call this function to setup an application-wide transaction. All current + * pending transactions of opening documents will be commited first. + * However, no new transaction is created by this call. Any subsequent + * changes in any current opening document will auto create a transaction + * with the given name and ID. If more than one document is changed, the + * transactions will share the same ID, and will be undo/redo together. + */ + int setActiveTransaction(const char *name, bool persist=false); + /// Return the current active transaction name and ID + const char *getActiveTransaction(int *tid=0) const; + /** Commit/abort current active transactions + * + * @param abort: whether to abort or commit the transactions + * + * Bsides calling this function directly, it will be called by automatically + * if 1) any new transaction is created with a different ID, or 2) any + * transaction with the current active transaction ID is either commited or + * aborted + */ + void closeActiveTransaction(bool abort=false, int id=0); //@} /** @name Signals of the Application */ @@ -133,8 +165,16 @@ public: boost::signals2::signal signalFinishSaveDocument; /// signal on undo in document boost::signals2::signal signalUndoDocument; + /// signal on application wide undo + boost::signals2::signal signalUndo; /// signal on redo in document boost::signals2::signal signalRedoDocument; + /// signal on application wide redo + boost::signals2::signal signalRedo; + /// signal before close/abort active transaction + boost::signals2::signal signalBeforeCloseTransaction; + /// signal after close/abort active transaction + boost::signals2::signal signalCloseTransaction; /// signal on show hidden items boost::signals2::signal signalShowHidden; //@} @@ -345,6 +385,15 @@ protected: void slotChangePropertyEditor(const App::Document&, const App::Property &); //@} + /// Helper class for App::Document to signal on close/abort transaction + class AppExport TransactionSignaller { + public: + TransactionSignaller(bool abort,bool signal); + ~TransactionSignaller(); + private: + bool abort; + }; + private: /// Constructor Application(std::map &mConfig); @@ -397,6 +446,9 @@ private: static PyObject *sCheckLinkDepth (PyObject *self,PyObject *args); static PyObject *sGetLinksTo (PyObject *self,PyObject *args); + static PyObject *sSetActiveTransaction (PyObject *self,PyObject *args); + static PyObject *sGetActiveTransaction (PyObject *self,PyObject *args); + static PyObject *sCloseActiveTransaction(PyObject *self,PyObject *args); static PyMethodDef Methods[]; friend class ApplicationObserver; @@ -448,6 +500,13 @@ private: // for estimate max link depth int _objCount; + friend class AutoTransaction; + + std::string _activeTransactionName; + int _activeTransactionID; + int _activeTransactionGuard; + bool _activeTransactionTmpName; + static Base::ConsoleObserverStd *_pConsoleObserverStd; static Base::ConsoleObserverFile *_pConsoleObserverFile; }; @@ -457,6 +516,60 @@ inline App::Application &GetApplication(void){ return *App::Application::_pcSingleton; } +/// Helper class to manager transaction (i.e. undo/redo) +class AppExport AutoTransaction { +private: + /// Private new operator to prevent heap allocation + void* operator new(size_t size); + +public: + /** Construtor + * + * @param name: optional new transaction name on construction + * @param tmpName: if true and a new transaction is setup, the name given is + * considered as temperary, and subsequent construction of this class (or + * calling Application::setActiveTransaction()) can override the transaction + * name. + * + * The constructor increments an internal counter + * (Application::_activeTransactionGuard). The counter prevents any new + * active transaction being setup. It also prevents close (i.e. commits) the + * current active transaction until it reaches zero. It does not have any + * effect on aborting transaction, though. + */ + AutoTransaction(const char *name=0, bool tmpName=false); + + /** Destructor + * + * This destructor decrease an internal counter + * (Application::_activeTransactionGuard), and will commit any current + * active transaction when the counter reaches zero. + */ + ~AutoTransaction(); + + /** Close or abort the transaction + * + * This function can be used to explicitly close (i.e. commit) the + * transaction, if the current transaction ID matches the one created inside + * the constructor. For aborting, it will abort any current transaction + */ + void close(bool abort=false); + + /** Enable/Disable any AutoTransaction instance in the current stack + * + * Once disabled, any empty temperary named transaction is closed. If there + * are non-empty or non-temperary named active transaction, it will not be + * auto closed. + * + * This function may be used in, for example, Gui::Document::setEdit() to + * allow a transaction live past any command scope. + */ + static void setEnable(bool enable); + +private: + int tid = 0; +}; + } // namespace App diff --git a/src/App/ApplicationPy.cpp b/src/App/ApplicationPy.cpp index 5fcaa6a2e1..89cef1be04 100644 --- a/src/App/ApplicationPy.cpp +++ b/src/App/ApplicationPy.cpp @@ -151,6 +151,19 @@ PyMethodDef Application::Methods[] = { "getLinksTo(obj,options=0,maxCount=0) -- return the objects linked to 'obj'\n\n" "options: 1: recursive, 2: check link array. Options can combine.\n" "maxCount: to limit the number of links returned\n"}, + {"setActiveTransaction", (PyCFunction) Application::sSetActiveTransaction, METH_VARARGS, + "setActiveTransaction(name, persist=False) -- setup active transaction with the given name\n\n" + "name: the transaction name\n" + "persist(False): by default, if the calling code is inside any invokation of a command, it\n" + " will be auto closed once all command within the current stack exists. To\n" + " disable auto closing, set persist=True\n" + "Returns the transaction ID for the active transaction. An application-wide\n" + "active transaction causes any document changes to open a transaction with\n" + "the given name and ID."}, + {"getActiveTransaction", (PyCFunction) Application::sGetActiveTransaction, METH_VARARGS, + "getActiveTransaction() -> (name,id) return the current active transaction name and ID"}, + {"closeActiveTransaction", (PyCFunction) Application::sCloseActiveTransaction, METH_VARARGS, + "closeActiveTransaction(abort=False) -- commit or abort current active transaction"}, {NULL, NULL, 0, NULL} /* Sentinel */ }; @@ -806,3 +819,46 @@ PyObject *Application::sGetLinksTo(PyObject * /*self*/, PyObject *args) }PY_CATCH; } +PyObject *Application::sSetActiveTransaction(PyObject * /*self*/, PyObject *args) +{ + char *name; + PyObject *persist = Py_False; + if (!PyArg_ParseTuple(args, "s|O", &name,&persist)) + return 0; + + PY_TRY { + Py::Int ret(GetApplication().setActiveTransaction(name,PyObject_IsTrue(persist))); + return Py::new_reference_to(ret); + }PY_CATCH; +} + +PyObject *Application::sGetActiveTransaction(PyObject * /*self*/, PyObject *args) +{ + if (!PyArg_ParseTuple(args, "")) + return 0; + + PY_TRY { + int id = 0; + const char *name = GetApplication().getActiveTransaction(&id); + if(!name || id<=0) + Py_Return; + Py::Tuple ret(2); + ret.setItem(0,Py::String(name)); + ret.setItem(1,Py::Int(id)); + return Py::new_reference_to(ret); + }PY_CATCH; +} + +PyObject *Application::sCloseActiveTransaction(PyObject * /*self*/, PyObject *args) +{ + PyObject *abort = Py_False; + int id = 0; + if (!PyArg_ParseTuple(args, "|Oi", &abort,&id)) + return 0; + + PY_TRY { + GetApplication().closeActiveTransaction(PyObject_IsTrue(abort),id); + Py_Return; + } PY_CATCH; +} + diff --git a/src/App/Document.cpp b/src/App/Document.cpp index 134201f731..5b5d07b01a 100644 --- a/src/App/Document.cpp +++ b/src/App/Document.cpp @@ -121,6 +121,10 @@ recompute path. Also, it enables more complicated dependencies beyond trees. #include "GeoFeatureGroupExtension.h" #include "Origin.h" #include "OriginGroupExtension.h" +#include "Link.h" +#include "GeoFeature.h" + +FC_LOG_LEVEL_INIT("App", true, true, true); using Base::Console; using Base::streq; @@ -152,6 +156,7 @@ typedef std::vector Path; namespace App { +static bool _IsRelabeling; // Pimpl class struct DocumentP { @@ -881,26 +886,38 @@ bool Document::checkOnCycle(void) return false; } -bool Document::undo(void) +bool Document::undo(int id) { if (d->iUndoMode) { + if(id) { + auto it = mUndoMap.find(id); + if(it == mUndoMap.end()) + return false; + if(it->second != d->activeUndoTransaction) { + while(mUndoTransactions.size() && mUndoTransactions.back()!=it->second) + undo(0); + } + } + if (d->activeUndoTransaction) - commitTransaction(); - else if (mUndoTransactions.empty()) + _commitTransaction(true); + if (mUndoTransactions.empty()) return false; // redo - d->activeUndoTransaction = new Transaction(); + d->activeUndoTransaction = new Transaction(mUndoTransactions.back()->getID()); d->activeUndoTransaction->Name = mUndoTransactions.back()->Name; - d->undoing = true; + + Base::FlagToggler flag(d->undoing); // applying the undo mUndoTransactions.back()->apply(*this,false); - d->undoing = false; // save the redo + mRedoMap[d->activeUndoTransaction->getID()] = d->activeUndoTransaction; mRedoTransactions.push_back(d->activeUndoTransaction); d->activeUndoTransaction = 0; + mUndoMap.erase(mUndoTransactions.back()->getID()); delete mUndoTransactions.back(); mUndoTransactions.pop_back(); @@ -911,25 +928,35 @@ bool Document::undo(void) return false; } -bool Document::redo(void) +bool Document::redo(int id) { if (d->iUndoMode) { + if(id) { + auto it = mRedoMap.find(id); + if(it == mRedoMap.end()) + return false; + while(mRedoTransactions.size() && mRedoTransactions.back()!=it->second) + redo(0); + } + if (d->activeUndoTransaction) - commitTransaction(); + _commitTransaction(true); assert(mRedoTransactions.size()!=0); // undo - d->activeUndoTransaction = new Transaction(); + d->activeUndoTransaction = new Transaction(mRedoTransactions.back()->getID()); d->activeUndoTransaction->Name = mRedoTransactions.back()->Name; // do the redo - d->undoing = true; + Base::FlagToggler flag(d->undoing); mRedoTransactions.back()->apply(*this,true); - d->undoing = false; + + mUndoMap[d->activeUndoTransaction->getID()] = d->activeUndoTransaction; mUndoTransactions.push_back(d->activeUndoTransaction); d->activeUndoTransaction = 0; + mRedoMap.erase(mRedoTransactions.back()->getID()); delete mRedoTransactions.back(); mRedoTransactions.pop_back(); @@ -979,34 +1006,100 @@ std::vector Document::getAvailableRedoNames() const return vList; } -void Document::openTransaction(const char* name) +void Document::openTransaction(const char* name) { + if(isPerformingTransaction()) { + FC_WARN("Cannot open transaction while transacting"); + return; + } + + GetApplication().setActiveTransaction(name?name:""); +} + +int Document::_openTransaction(const char* name, int id) { + if(isPerformingTransaction()) { + FC_WARN("Cannot open transaction while transacting"); + return 0; + } + if (d->iUndoMode) { + if(id && mUndoMap.find(id)!=mUndoMap.end()) + throw Base::RuntimeError("invalid transaction id"); if (d->activeUndoTransaction) - commitTransaction(); + _commitTransaction(true); _clearRedos(); - d->activeUndoTransaction = new Transaction(); - if (name) - d->activeUndoTransaction->Name = name; - else - d->activeUndoTransaction->Name = ""; + d->activeUndoTransaction = new Transaction(id); + if (!name) + name = ""; + d->activeUndoTransaction->Name = name; + mUndoMap[d->activeUndoTransaction->getID()] = d->activeUndoTransaction; + id = d->activeUndoTransaction->getID(); - signalOpenTransaction(*this, d->activeUndoTransaction->Name); + signalOpenTransaction(*this, name); + + auto &app = GetApplication(); + auto activeDoc = app.getActiveDocument(); + if(activeDoc && + activeDoc!=this && + !activeDoc->hasPendingTransaction()) + { + std::string aname("-> "); + aname += d->activeUndoTransaction->Name; + FC_LOG("auto transaction " << getName() << " -> " << activeDoc->getName()); + activeDoc->_openTransaction(aname.c_str(),id); + } + return id; + } + return 0; +} + +void Document::renameTransaction(const char *name, int id) { + if(name && d->activeUndoTransaction && d->activeUndoTransaction->getID()==id) { + if(boost::starts_with(d->activeUndoTransaction->Name, "-> ")) + d->activeUndoTransaction->Name.resize(3); + else + d->activeUndoTransaction->Name.clear(); + d->activeUndoTransaction->Name += name; } } -void Document::_checkTransaction(DocumentObject* pcObject) +void Document::_checkTransaction(DocumentObject* pcDelObj, const Property *What, int line) { // if the undo is active but no transaction open, open one! - if (d->iUndoMode) { + if (d->iUndoMode && !isPerformingTransaction()) { if (!d->activeUndoTransaction) { + if(!testStatus(Restoring) || testStatus(Importing)) { + int tid=0; + const char *name = GetApplication().getActiveTransaction(&tid); + if(name && tid>0) { + bool ignore = false; + if(What) { + auto parent = What->getContainer(); + auto parentObj = Base::freecad_dynamic_cast(parent); + if(!parentObj || What->testStatus(Property::NoModify)) + ignore = true; + } + if(FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { + if(What) + FC_LOG((ignore?"ignore":"auto") << " transaction (" + << line << ") '" << What->getFullName()); + else + FC_LOG((ignore?"ignore":"auto") <<" transaction (" + << line << ") '" << name << "' in " << getName()); + } + if(!ignore) + _openTransaction(name,tid); + return; + } + } + if(!pcDelObj) return; // When the object is going to be deleted we have to check if it has already been added to // the undo transactions std::list::iterator it; for (it = mUndoTransactions.begin(); it != mUndoTransactions.end(); ++it) { - if ((*it)->hasObject(pcObject)) { - openTransaction(); + if ((*it)->hasObject(pcDelObj)) { + _openTransaction("Delete"); break; } } @@ -1016,35 +1109,79 @@ void Document::_checkTransaction(DocumentObject* pcObject) void Document::_clearRedos() { + if(isPerformingTransaction()) { + FC_ERR("Cannot clear redo while transacting"); + return; + } + + mRedoMap.clear(); while (!mRedoTransactions.empty()) { delete mRedoTransactions.back(); mRedoTransactions.pop_back(); } } -void Document::commitTransaction() +void Document::commitTransaction() { + if(isPerformingTransaction()) { + FC_WARN("Cannot commit transaction while transacting"); + return; + } + + if (d->activeUndoTransaction) + GetApplication().closeActiveTransaction(false,d->activeUndoTransaction->getID()); +} + +void Document::_commitTransaction(bool notify) { + if(isPerformingTransaction()) { + FC_WARN("Cannot commit transaction while transacting"); + return; + } + if (d->activeUndoTransaction) { + Application::TransactionSignaller signaller(false,true); + int id = d->activeUndoTransaction->getID(); mUndoTransactions.push_back(d->activeUndoTransaction); d->activeUndoTransaction = 0; // check the stack for the limits if(mUndoTransactions.size() > d->UndoMaxStackSize){ + mUndoMap.erase(mUndoTransactions.front()->getID()); delete mUndoTransactions.front(); mUndoTransactions.pop_front(); } signalCommitTransaction(*this); + + if(notify) + GetApplication().closeActiveTransaction(false,id); } } -void Document::abortTransaction() +void Document::abortTransaction() { + if(isPerformingTransaction()) { + FC_WARN("Cannot abort transaction while transacting"); + return; + } + if (d->activeUndoTransaction) + GetApplication().closeActiveTransaction(true,d->activeUndoTransaction->getID()); +} + +void Document::_abortTransaction() { + if(isPerformingTransaction()) { + FC_WARN("Cannot abort transaction while transacting"); + return; + } + if (d->activeUndoTransaction) { - d->rollback = true; - // applying the so far made changes - d->activeUndoTransaction->apply(*this,false); - d->rollback = false; + Application::TransactionSignaller signaller(true,true); + { + Base::FlagToggler flag(d->rollback); + // applying the so far made changes + d->activeUndoTransaction->apply(*this,false); + } // destroy the undo + mUndoMap.erase(d->activeUndoTransaction->getID()); delete d->activeUndoTransaction; d->activeUndoTransaction = 0; signalAbortTransaction(*this); @@ -1059,6 +1196,26 @@ bool Document::hasPendingTransaction() const return false; } +int Document::getTransactionID(bool undo, unsigned pos) const { + if(undo) { + if(d->activeUndoTransaction) { + if(pos == 0) + return d->activeUndoTransaction->getID(); + --pos; + } + if(pos>=mUndoTransactions.size()) + return 0; + auto rit = mUndoTransactions.rbegin(); + for(;pos;++rit,--pos); + return (*rit)->getID(); + } + if(pos>=mRedoTransactions.size()) + return 0; + auto rit = mRedoTransactions.rbegin(); + for(;pos;++rit,--pos); + return (*rit)->getID(); +} + bool Document::isTransactionEmpty() const { if (d->activeUndoTransaction) { @@ -1070,8 +1227,15 @@ bool Document::isTransactionEmpty() const void Document::clearUndos() { + if(isPerformingTransaction()) { + FC_ERR("Cannot clear undos while transacting"); + return; + } + if (d->activeUndoTransaction) - commitTransaction(); + _commitTransaction(true); + + mUndoMap.clear(); // When cleaning up the undo stack we must delete the transactions from front // to back because a document object can appear in several transactions but @@ -1091,16 +1255,40 @@ void Document::clearUndos() _clearRedos(); } -int Document::getAvailableUndos() const +int Document::getAvailableUndos(int id) const { + if(id) { + auto it = mUndoMap.find(id); + if(it == mUndoMap.end()) + return 0; + int i = 0; + if(d->activeUndoTransaction) { + ++i; + if(d->activeUndoTransaction->getID()==id) + return i; + } + auto rit = mUndoTransactions.rbegin(); + for(;rit!=mUndoTransactions.rend()&&*rit!=it->second;++rit,++i); + assert(rit!=mUndoTransactions.rend()); + return i+1; + } if (d->activeUndoTransaction) return static_cast(mUndoTransactions.size() + 1); else return static_cast(mUndoTransactions.size()); } -int Document::getAvailableRedos() const +int Document::getAvailableRedos(int id) const { + if(id) { + auto it = mRedoMap.find(id); + if(it == mRedoMap.end()) + return 0; + int i = 0; + for(auto rit=mRedoTransactions.rbegin();*rit!=it->second;++rit,++i); + assert(i<(int)mRedoTransactions.size()); + return i+1; + } return static_cast(mRedoTransactions.size()); } @@ -1139,6 +1327,8 @@ unsigned int Document::getMaxUndoStackSize(void)const void Document::onBeforeChange(const Property* prop) { + if(prop == &Label) + oldLabel = Label.getValue(); signalBeforeChange(*this, *prop); } @@ -1148,6 +1338,7 @@ void Document::onChanged(const Property* prop) // the Name property is a label for display purposes if (prop == &Label) { + Base::FlagToggler<> flag(_IsRelabeling); App::GetApplication().signalRelabelDocument(*this); } else if(prop == &ShowHidden) { App::GetApplication().signalShowHidden(*this); @@ -1189,9 +1380,11 @@ void Document::onBeforeChangeProperty(const TransactionalObject *Who, const Prop { if(Who->isDerivedFrom(App::DocumentObject::getClassTypeId())) signalBeforeChangeObject(*static_cast(Who), *What); - - if (d->activeUndoTransaction && !d->rollback) - d->activeUndoTransaction->addObjectChange(Who,What); + if(!d->rollback && !_IsRelabeling) { + _checkTransaction(0,What,__LINE__); + if (d->activeUndoTransaction) + d->activeUndoTransaction->addObjectChange(Who,What); + } } void Document::onChangedProperty(const DocumentObject *Who, const Property *What) @@ -2736,6 +2929,7 @@ DocumentObject * Document::addObject(const char* sType, const char* pObjectName, // do no transactions if we do a rollback! if (!d->rollback) { // Undo stuff + _checkTransaction(0,0,__LINE__); if (d->activeUndoTransaction) d->activeUndoTransaction->addObjectDel(pcObject); } @@ -2822,6 +3016,7 @@ std::vector Document::addObjects(const char* sType, const std: // do no transactions if we do a rollback! if (!d->rollback) { // Undo stuff + _checkTransaction(0,0,__LINE__); if (d->activeUndoTransaction) { d->activeUndoTransaction->addObjectDel(pcObject); } @@ -2897,6 +3092,7 @@ void Document::addObject(DocumentObject* pcObject, const char* pObjectName) // do no transactions if we do a rollback! if (!d->rollback) { // Undo stuff + _checkTransaction(0,0,__LINE__); if (d->activeUndoTransaction) d->activeUndoTransaction->addObjectDel(pcObject); } @@ -2952,6 +3148,7 @@ void Document::_addObject(DocumentObject* pcObject, const char* pObjectName) // do no transactions if we do a rollback! if (!d->rollback) { // Undo stuff + _checkTransaction(0,0,__LINE__); if (d->activeUndoTransaction) d->activeUndoTransaction->addObjectDel(pcObject); } @@ -2980,7 +3177,7 @@ void Document::removeObject(const char* sName) if (pos == d->objectMap.end()) return; - _checkTransaction(pos->second); + _checkTransaction(pos->second,0,__LINE__); if (d->activeObject == pos->second) d->activeObject = 0; @@ -3056,7 +3253,7 @@ void Document::removeObject(const char* sName) void Document::_removeObject(DocumentObject* pcObject) { // TODO Refactoring: share code with Document::removeObject() (2015-09-01, Fat-Zer) - _checkTransaction(pcObject); + _checkTransaction(pcObject,0,__LINE__); auto pos = d->objectMap.find(pcObject->getNameInDocument()); diff --git a/src/App/Document.h b/src/App/Document.h index a24a389ca2..e7b256d7a8 100644 --- a/src/App/Document.h +++ b/src/App/Document.h @@ -298,7 +298,22 @@ public: //@} - /** @name methods for the UNDO REDO and Transaction handling */ + /** @name methods for the UNDO REDO and Transaction handling + * + * Introduce a new concept of transaction ID. Each transaction must be + * unique inside the document. Multiple transactions from different + * documents can be grouped together with the same transaction ID. + * + * When undo, Gui component can query getAvailableUndo(id) to see if it is + * possible to undo with a given ID. If there more than one undo + * transactions, meaning that there are other transactions before the given + * ID. The Gui component shall ask user if he wants to undo multiple steps. + * And if the user agrees, call undo(id) to unroll all transaction before + * and including the the one with the give ID. Same apllies for redo. + * + * The new transaction ID describe here is fully backward compatible. + * Calling the APIs with a default id=0 gives the original behavior. + */ //@{ /// switch the level of Undo/Redo void setUndoMode(int iMode); @@ -306,14 +321,25 @@ public: int getUndoMode(void) const; /// switch the transaction mode void setTransactionMode(int iMode); - /// Open a new command Undo/Redo, an UTF-8 name can be specified + /** Open a new command Undo/Redo, an UTF-8 name can be specified + * + * @param name: transaction name + * + * This function calls App::Application::setActiveTransaction(name) instead + * to setup a potential transaction which will only be created if there is + * actual changes. + */ void openTransaction(const char* name=0); - // Commit the Command transaction. Do nothing If there is no Command transaction open. + /// Rename the current transaction if the id matches + void renameTransaction(const char *name, int id); + /// Commit the Command transaction. Do nothing If there is no Command transaction open. void commitTransaction(); /// Abort the actually running transaction. void abortTransaction(); /// Check if a transaction is open bool hasPendingTransaction() const; + /// Return the undo/redo transaction ID starting from the back + int getTransactionID(bool undo, unsigned pos=0) const; /// Check if a transaction is open and its list is empty. /// If no transaction is open true is returned. bool isTransactionEmpty() const; @@ -328,17 +354,17 @@ public: /// Remove all stored Undos and Redos void clearUndos(); /// Returns the number of stored Undos. If greater than 0 Undo will be effective. - int getAvailableUndos() const; + int getAvailableUndos(int id=0) const; /// Returns a list of the Undo names std::vector getAvailableUndoNames() const; /// Will UNDO one step, returns False if no undo was done (Undos == 0). - bool undo(); + bool undo(int id=0); /// Returns the number of stored Redos. If greater than 0 Redo will be effective. - int getAvailableRedos() const; + int getAvailableRedos(int id=0) const; /// Returns a list of the Redo names. std::vector getAvailableRedoNames() const; /// Will REDO one step, returns False if no redo was done (Redos == 0). - bool redo() ; + bool redo(int id=0) ; /// returns true if the document is in an Transaction phase, e.g. currently performing a redo/undo or rollback bool isPerformingTransaction() const; /// \internal add or remove property from a transactional object @@ -409,7 +435,7 @@ protected: void _removeObject(DocumentObject* pcObject); void _addObject(DocumentObject* pcObject, const char* pObjectName); /// checks if a valid transaction is open - void _checkTransaction(DocumentObject* pcObject); + void _checkTransaction(DocumentObject* pcDelObj, const Property *What, int line); void breakDependency(DocumentObject* pcObject, bool clear); std::vector readObjects(Base::XMLReader& reader); void writeObjects(const std::vector&, Base::Writer &writer) const; @@ -430,18 +456,34 @@ protected: void _rebuildDependencyList(void); std::string getTransientDirectoryName(const std::string& uuid, const std::string& filename) const; + /** Open a new command Undo/Redo, an UTF-8 name can be specified + * + * @param name: transaction name + * @param id: transaction ID, if 0 then the ID is auto generated. + * + * @return: Return the ID of the new transaction. + * + * This function creates an actual transaction regardless of Application + * AutoTransaction setting. + */ + int _openTransaction(const char* name=0, int id=0); + /// Internally called by App::Application to commit the Command transaction. + void _commitTransaction(bool notify=false); + /// Internally called by App::Application to abort the running transaction. + void _abortTransaction(); private: // # Data Member of the document +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ std::list mUndoTransactions; + std::map mUndoMap; std::list mRedoTransactions; - // recompute log - std::vector _RecomputeLog; + std::map mRedoMap; // pointer to the python class Py::Object DocumentPythonObject; struct DocumentP* d; + std::string oldLabel; std::string myName; }; diff --git a/src/App/DocumentPy.xml b/src/App/DocumentPy.xml index eea4e29568..055740b4e8 100644 --- a/src/App/DocumentPy.xml +++ b/src/App/DocumentPy.xml @@ -50,7 +50,14 @@ - Open a new Undo/Redo transaction. + openTransaction(name) - Open a new Undo/Redo transaction. + +This function no long creates a new transaction, but calls +FreeCAD.setActiveTransaction(name) instead, which will auto creates a +transaction with the given name when any change happed in any opened document. +If more than one document is changed, all newly created transactions will have +the same internal ID and will be undo/redo together. + @@ -232,6 +239,18 @@ Both parameters are optional. + + + Check if there is a pending transaction + + + + + + Indicate whether the document is undoing/redoing + + + diff --git a/src/App/DocumentPyImp.cpp b/src/App/DocumentPyImp.cpp index a3212c7c11..255d10188e 100644 --- a/src/App/DocumentPyImp.cpp +++ b/src/App/DocumentPyImp.cpp @@ -373,6 +373,10 @@ PyObject* DocumentPy::commitTransaction(PyObject * args) Py_Return; } +Py::Boolean DocumentPy::getHasPendingTransaction() const { + return Py::Boolean(getDocumentPtr()->hasPendingTransaction()); +} + PyObject* DocumentPy::undo(PyObject * args) { if (!PyArg_ParseTuple(args, "")) // convert args: Python->C @@ -723,5 +727,9 @@ PyObject* DocumentPy::getLinksTo(PyObject *args) ret.setItem(i++,Py::Object(o->getPyObject(),true)); return Py::new_reference_to(ret); }PY_CATCH +Py::Boolean DocumentPy::getTransacting() const { + return Py::Boolean(getDocumentPtr()->isPerformingTransaction()); +} + } diff --git a/src/App/TransactionalObject.cpp b/src/App/TransactionalObject.cpp index a5729edd5b..036802327f 100644 --- a/src/App/TransactionalObject.cpp +++ b/src/App/TransactionalObject.cpp @@ -61,18 +61,3 @@ void TransactionalObject::onBeforeChangeProperty(Document *doc, const Property * doc->onBeforeChangeProperty(this, prop); } -App::Property* TransactionalObject::addDynamicProperty(const char*, const char*, - const char*, const char*, - short, bool, bool) -{ - std::stringstream str; - str << "Type " << this->getTypeId().getName() << " cannot dynamically add properties"; - throw Base::RuntimeError(str.str()); -} - -bool TransactionalObject::removeDynamicProperty(const char*) -{ - std::stringstream str; - str << "Type " << this->getTypeId().getName() << " cannot dynamically remove properties"; - throw Base::RuntimeError(str.str()); -} diff --git a/src/App/TransactionalObject.h b/src/App/TransactionalObject.h index 50e0747ac5..bd860835e3 100644 --- a/src/App/TransactionalObject.h +++ b/src/App/TransactionalObject.h @@ -44,13 +44,6 @@ public: virtual ~TransactionalObject(); virtual bool isAttachedToDocument() const; virtual const char* detachFromDocument(); - - virtual App::Property* addDynamicProperty( - const char*, const char* = 0, - const char* = 0, const char* = 0, - short = 0, bool = false, bool = false); - virtual bool removeDynamicProperty(const char*); - protected: void onBeforeChangeProperty(Document *doc, const Property *prop); }; diff --git a/src/App/Transactions.cpp b/src/App/Transactions.cpp index d147a8fcff..9c1af58038 100644 --- a/src/App/Transactions.cpp +++ b/src/App/Transactions.cpp @@ -27,16 +27,21 @@ # include #endif +#include + /// Here the FreeCAD includes sorted by Base,App,Gui...... #include using Base::Writer; #include using Base::XMLReader; +#include #include "Transactions.h" #include "Property.h" #include "Document.h" #include "DocumentObject.h" +FC_LOG_LEVEL_INIT("App",true,true); + using namespace App; using namespace std; @@ -45,14 +50,10 @@ TYPESYSTEM_SOURCE(App::Transaction, Base::Persistence) //************************************************************************** // Construction/Destruction -Transaction::Transaction() - : iPos(0) -{ -} - -Transaction::Transaction(int pos) - : iPos(pos) +Transaction::Transaction(int id) { + if(!id) id = getNewID(); + transID = id; } /** @@ -61,8 +62,8 @@ Transaction::Transaction(int pos) */ Transaction::~Transaction() { - TransactionList::iterator It; - for (It= _Objects.begin();It!=_Objects.end();++It) { + auto &index = _Objects.get<0>(); + for (auto It= index.begin();It!=index.end();++It) { if (It->second->status == TransactionObject::New) { // If an object has been removed from the document the transaction // status is 'New'. The 'pcNameInDocument' member serves as criterion @@ -95,6 +96,19 @@ Transaction::~Transaction() } } +static std::atomic _TransactionID; + +int Transaction::getNewID() { + int id = ++_TransactionID; + if(id) return id; + // wrap around? really? + return ++_TransactionID; +} + +int Transaction::getLastID() { + return _TransactionID; +} + unsigned int Transaction::getMemSize (void) const { return 0; @@ -110,34 +124,39 @@ void Transaction::Restore(Base::XMLReader &/*reader*/) assert(0); } +int Transaction::getID(void) const +{ + return transID; +} + bool Transaction::isEmpty() const { return _Objects.empty(); } -int Transaction::getPos(void) const -{ - return iPos; -} - bool Transaction::hasObject(const TransactionalObject *Obj) const { - TransactionList::const_iterator it; - for (it = _Objects.begin(); it != _Objects.end(); ++it) { - if (it->first == Obj) - return true; - } - - return false; + return !!_Objects.get<1>().count(Obj); } -void Transaction::removeProperty(TransactionalObject *Obj, - const Property* pcProp) +void Transaction::addOrRemoveProperty(TransactionalObject *Obj, + const Property* pcProp, bool add) { - for (auto it : _Objects) { - if (it.first == Obj) - it.second->removeProperty(pcProp); + auto &index = _Objects.get<1>(); + auto pos = index.find(Obj); + + TransactionObject *To; + + if (pos != index.end()) { + To = pos->second; } + else { + To = TransactionFactory::instance().createTransaction(Obj->getTypeId()); + To->status = TransactionObject::Chn; + index.emplace(Obj,To); + } + + To->addOrRemoveProperty(pcProp,add); } //************************************************************************** @@ -146,93 +165,90 @@ void Transaction::removeProperty(TransactionalObject *Obj, void Transaction::apply(Document &Doc, bool forward) { - TransactionList::iterator It; - //for (It= _Objects.begin();It!=_Objects.end();++It) - // It->second->apply(Doc,const_cast(It->first)); - for (It= _Objects.begin();It!=_Objects.end();++It) - It->second->applyDel(Doc, const_cast(It->first)); - for (It= _Objects.begin();It!=_Objects.end();++It) - It->second->applyNew(Doc, const_cast(It->first)); - for (It= _Objects.begin();It!=_Objects.end();++It) - It->second->applyChn(Doc, const_cast(It->first), forward); + std::string errMsg; + try { + auto &index = _Objects.get<0>(); + for(auto &info : index) + info.second->applyDel(Doc, const_cast(info.first)); + for(auto &info : index) + info.second->applyNew(Doc, const_cast(info.first)); + for(auto &info : index) + info.second->applyChn(Doc, const_cast(info.first), forward); + }catch(Base::Exception &e) { + e.ReportException(); + errMsg = e.what(); + }catch(std::exception &e) { + errMsg = e.what(); + }catch(...) { + errMsg = "Unknown exception"; + } + if(errMsg.size()) { + FC_ERR("Exception on " << (forward?"redo":"undo") << " '" + << Name << "':" << errMsg); + } } void Transaction::addObjectNew(TransactionalObject *Obj) { - TransactionList::iterator pos = _Objects.end(); - for (TransactionList::iterator it = _Objects.begin(); it != _Objects.end(); ++it) { - if (it->first == Obj) { - pos = it; - break; - } - } - - if (pos != _Objects.end()) { + auto &index = _Objects.get<1>(); + auto pos = index.find(Obj); + if (pos != index.end()) { if (pos->second->status == TransactionObject::Del) { delete pos->second; delete pos->first; - _Objects.erase(pos); + index.erase(pos); } else { pos->second->status = TransactionObject::New; pos->second->_NameInDocument = Obj->detachFromDocument(); // move item at the end to make sure the order of removal is kept - _Objects.splice(_Objects.end(), _Objects, pos); + auto &seq = _Objects.get<0>(); + seq.relocate(seq.end(),_Objects.project<0>(pos)); } } else { TransactionObject *To = TransactionFactory::instance().createTransaction(Obj->getTypeId()); To->status = TransactionObject::New; To->_NameInDocument = Obj->detachFromDocument(); - _Objects.push_back(std::make_pair(Obj, To)); + index.emplace(Obj,To); } } void Transaction::addObjectDel(const TransactionalObject *Obj) { - TransactionList::iterator pos = _Objects.end(); - for (TransactionList::iterator it = _Objects.begin(); it != _Objects.end(); ++it) { - if (it->first == Obj) { - pos = it; - break; - } - } + auto &index = _Objects.get<1>(); + auto pos = index.find(Obj); // is it created in this transaction ? - if (pos != _Objects.end() && pos->second->status == TransactionObject::New) { + if (pos != index.end() && pos->second->status == TransactionObject::New) { // remove completely from transaction delete pos->second; - _Objects.erase(pos); + index.erase(pos); } - else if (pos != _Objects.end() && pos->second->status == TransactionObject::Chn) { + else if (pos != index.end() && pos->second->status == TransactionObject::Chn) { pos->second->status = TransactionObject::Del; } else { TransactionObject *To = TransactionFactory::instance().createTransaction(Obj->getTypeId()); - _Objects.push_back(std::make_pair(Obj, To)); To->status = TransactionObject::Del; + index.emplace(Obj,To); } } void Transaction::addObjectChange(const TransactionalObject *Obj, const Property *Prop) { - TransactionList::iterator pos = _Objects.end(); - for (TransactionList::iterator it = _Objects.begin(); it != _Objects.end(); ++it) { - if (it->first == Obj) { - pos = it; - break; - } - } + auto &index = _Objects.get<1>(); + auto pos = index.find(Obj); TransactionObject *To; - if (pos != _Objects.end()) { + if (pos != index.end()) { To = pos->second; } else { To = TransactionFactory::instance().createTransaction(Obj->getTypeId()); - _Objects.push_back(std::make_pair(Obj, To)); To->status = TransactionObject::Chn; + index.emplace(Obj,To); } To->setProperty(Prop); @@ -264,9 +280,8 @@ TransactionObject::TransactionObject() */ TransactionObject::~TransactionObject() { - std::map::const_iterator It; - for (It=_PropChangeMap.begin();It!=_PropChangeMap.end();++It) - delete It->second; + for(auto &v : _PropChangeMap) + delete v.second.property; } void TransactionObject::applyDel(Document & /*Doc*/, TransactionalObject * /*pcObj*/) @@ -277,38 +292,97 @@ void TransactionObject::applyNew(Document & /*Doc*/, TransactionalObject * /*pcO { } -void TransactionObject::applyChn(Document & /*Doc*/, TransactionalObject * /*pcObj*/, bool Forward) +void TransactionObject::applyChn(Document & /*Doc*/, TransactionalObject *pcObj, bool Forward) { if (status == New || status == Chn) { - // apply changes if any - if (!Forward) { - std::map::const_reverse_iterator It; - std::map::const_reverse_iterator rendIt = _PropChangeMap.rend(); - for (It = _PropChangeMap.rbegin(); It != rendIt; ++It) - const_cast(It->first)->Paste(*(It->second)); - } - else { - std::map::const_iterator It; - std::map::const_iterator endIt = _PropChangeMap.end(); - for (It = _PropChangeMap.begin(); It != endIt; ++It) - const_cast(It->first)->Paste(*(It->second)); + // Property change order is not preserved, as it is recursive in nature + for(auto &v : _PropChangeMap) { + auto &data = v.second; + auto prop = const_cast(v.first); + + if(!data.property) { + // here means we are undoing/redoing and property add operation + pcObj->removeDynamicProperty(v.second.name.c_str()); + continue; + } + + // getPropertyName() is specially coded to be safe even if prop has + // been destroies. We must prepare for the case where user removed + // a dynamic property but does not recordered as transaction. + auto name = pcObj->getPropertyName(prop); + if(!name) { + // Here means the original property is not found, probably removed + if(v.second.name.empty()) { + // not a dynamic property, nothing to do + continue; + } + + // It is possible for the dynamic property to be removed and + // restored. But since restoring property is actually creating + // a new property, the property key inside redo stack will not + // match. So we search by name first. + prop = pcObj->getDynamicPropertyByName(v.second.name.c_str()); + if(!prop) { + // Still not found, re-create the property + prop = pcObj->addDynamicProperty( + data.property->getTypeId().getName(), + v.second.name.c_str(), data.group.c_str(), data.doc.c_str(), + data.attr, data.readonly, data.hidden); + if(!prop) + continue; + prop->setStatusValue(data.property->getStatus()); + } + } + // Because we now allow undo/redo dynamic property adding/removing, + // we have to enforce property type checking before calling Copy/Paste. + if(data.property->getTypeId() != prop->getTypeId()) { + FC_WARN("Cannot " << (Forward?"redo":"undo") + << " change of property " << prop->getName() + << " because of type change: " + << data.property->getTypeId().getName() + << " -> " << prop->getTypeId().getName()); + continue; + } + prop->Paste(*data.property); } } } void TransactionObject::setProperty(const Property* pcProp) { - std::map::iterator pos = _PropChangeMap.find(pcProp); - if (pos == _PropChangeMap.end()) - _PropChangeMap[pcProp] = pcProp->Copy(); + auto &data = _PropChangeMap[pcProp]; + if(!data.property && data.name.empty()) { + data = pcProp->getContainer()->getDynamicPropertyData(pcProp); + data.property = pcProp->Copy(); + data.property->setStatusValue(pcProp->getStatus()); + } } -void TransactionObject::removeProperty(const Property* pcProp) +void TransactionObject::addOrRemoveProperty(const Property* pcProp, bool add) { - std::map::iterator pos = _PropChangeMap.find(pcProp); - if (pos != _PropChangeMap.end()) { - delete pos->second; - _PropChangeMap.erase(pos); + (void)add; + if(!pcProp || !pcProp->getContainer()) + return; + + auto &data = _PropChangeMap[pcProp]; + if(data.name.size()) { + if(!add && !data.property) { + // this means add and remove the same property inside a single + // transaction, so they cancel each other out. + _PropChangeMap.erase(pcProp); + } + return; + } + if(data.property) { + delete data.property; + data.property = 0; + } + data = pcProp->getContainer()->getDynamicPropertyData(pcProp); + if(add) + data.property = 0; + else { + data.property = pcProp->Copy(); + data.property->setStatusValue(pcProp->getStatus()); } } diff --git a/src/App/Transactions.h b/src/App/Transactions.h index 3f135504e1..7d0e619a77 100644 --- a/src/App/Transactions.h +++ b/src/App/Transactions.h @@ -26,6 +26,7 @@ #include #include +#include namespace App { @@ -44,11 +45,16 @@ class AppExport Transaction : public Base::Persistence TYPESYSTEM_HEADER(); public: + /** Construction + * + * @param id: transaction id. If zero, then it will be generated + * automatically as a monotonically increasing index across the entire + * application. User can pass in a transaction id to group multiple + * transactions from different document, so that they can be undo/redo + * together. + */ + Transaction(int id = 0); /// Construction - Transaction(); - /// Construction - Transaction(int pos); - /// Destruction virtual ~Transaction(); /// apply the content to the document @@ -62,22 +68,35 @@ public: /// This method is used to restore properties from an XML document. virtual void Restore(Base::XMLReader &reader); + /// Return the transaction ID + int getID(void) const; + + /// Generate a new unique transaction ID + static int getNewID(void); + static int getLastID(void); + /// Returns true if the transaction list is empty; otherwise returns false. bool isEmpty() const; - /// get the position in the transaction history - int getPos(void) const; /// check if this object is used in a transaction bool hasObject(const TransactionalObject *Obj) const; - void removeProperty(TransactionalObject *Obj, const Property* pcProp); + void addOrRemoveProperty(TransactionalObject *Obj, const Property* pcProp, bool add); void addObjectNew(TransactionalObject *Obj); void addObjectDel(const TransactionalObject *Obj); void addObjectChange(const TransactionalObject *Obj, const Property *Prop); private: - int iPos; - typedef std::list > TransactionList; - TransactionList _Objects; + int transID; + typedef std::pair Info; + bmi::multi_index_container< + Info, + bmi::indexed_by< + bmi::sequenced<>, + bmi::hashed_unique< + bmi::member + > + > + > _Objects; }; /** Represents an entry for an object in a Transaction @@ -97,7 +116,7 @@ public: virtual void applyChn(Document &Doc, TransactionalObject *pcObj, bool Forward); void setProperty(const Property* pcProp); - void removeProperty(const Property* pcProp); + void addOrRemoveProperty(const Property* pcProp, bool add); virtual unsigned int getMemSize (void) const; virtual void Save (Base::Writer &writer) const; @@ -108,7 +127,7 @@ public: protected: enum Status {New,Del,Chn} status; - std::map _PropChangeMap; + std::unordered_map _PropChangeMap; std::string _NameInDocument; };