Skip to content

Commit

Permalink
App: transaction related API changes
Browse files Browse the repository at this point in the history
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
  • Loading branch information
realthunder committed Jul 16, 2019
1 parent 819c807 commit d26cb0f
Show file tree
Hide file tree
Showing 11 changed files with 834 additions and 172 deletions.
156 changes: 156 additions & 0 deletions src/App/Application.cpp
Expand Up @@ -251,6 +251,7 @@ init_freecad_module(void)

Application::Application(std::map<std::string,std::string> &mConfig)
: _mConfig(mConfig), _pActiveDoc(0), _objCount(-1)
, _activeTransactionID(0), _activeTransactionGuard(0), _activeTransactionTmpName(false)
{
//_hApp = new ApplicationOCC;
mpcPramManager["System parameter"] = _pcSysParamMngr;
Expand Down Expand Up @@ -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();
Expand Down
113 changes: 113 additions & 0 deletions src/App/Application.h
Expand Up @@ -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
Expand Down Expand Up @@ -105,6 +106,37 @@ class AppExport Application
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 */
Expand Down Expand Up @@ -133,8 +165,16 @@ class AppExport Application
boost::signals2::signal<void (const Document&, const std::string&)> signalFinishSaveDocument;
/// signal on undo in document
boost::signals2::signal<void (const Document&)> signalUndoDocument;
/// signal on application wide undo
boost::signals2::signal<void ()> signalUndo;
/// signal on redo in document
boost::signals2::signal<void (const Document&)> signalRedoDocument;
/// signal on application wide redo
boost::signals2::signal<void ()> signalRedo;
/// signal before close/abort active transaction
boost::signals2::signal<void (bool)> signalBeforeCloseTransaction;
/// signal after close/abort active transaction
boost::signals2::signal<void (bool)> signalCloseTransaction;
/// signal on show hidden items
boost::signals2::signal<void (const Document&)> signalShowHidden;
//@}
Expand Down Expand Up @@ -345,6 +385,15 @@ class AppExport Application
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<std::string,std::string> &mConfig);
Expand Down Expand Up @@ -397,6 +446,9 @@ class AppExport Application
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;
Expand Down Expand Up @@ -448,6 +500,13 @@ class AppExport Application
// 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;
};
Expand All @@ -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


Expand Down

0 comments on commit d26cb0f

Please sign in to comment.