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
This commit is contained in:
Zheng, Lei
2019-06-28 17:45:27 +08:00
committed by wmayer
parent be6ec3fdfc
commit de4651bc99
11 changed files with 835 additions and 173 deletions

View File

@@ -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 <size_t> 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<bool> 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<bool> 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<std::string> 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:"<empty>");
}
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 = "<empty>";
d->activeUndoTransaction = new Transaction(id);
if (!name)
name = "<empty>";
d->activeUndoTransaction->Name = name;
mUndoMap[d->activeUndoTransaction->getID()] = d->activeUndoTransaction;
id = d->activeUndoTransaction->getID();
signalOpenTransaction(*this, 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<DocumentObject>(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<Transaction*>::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<bool> 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<int>(mUndoTransactions.size() + 1);
else
return static_cast<int>(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<int>(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<const App::DocumentObject*>(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<DocumentObject *> 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());