Updates to undo/redo logic:

1. Replace single transaction list with separate undo/redo deques
2. Update UndoHistory GUI to work with new queue setup
3. Add test to txn.py for empty transaction list


svn: r15427
This commit is contained in:
Gerald Britton 2010-05-20 18:32:08 +00:00
parent 6da2ade5e3
commit cd8ded4b37
3 changed files with 50 additions and 88 deletions

View File

@ -111,7 +111,6 @@ class UndoHistory(ManagedWindow.ManagedWindow):
self.show() self.show()
def _selection_changed(self, obj): def _selection_changed(self, obj):
assert self.undodb.undo_count == self.undodb.undoindex + 1
(model, node) = self.selection.get_selected() (model, node) = self.selection.get_selected()
if not node: if not node:
return return
@ -119,18 +118,18 @@ class UndoHistory(ManagedWindow.ManagedWindow):
start = min(path[0], self.undodb.undo_count) start = min(path[0], self.undodb.undo_count)
end = max(path[0], self.undodb.undo_count) end = max(path[0], self.undodb.undo_count)
self._paint_rows(0, len(self.model)-1, False) self._paint_rows(0, len(self.model) - 1, False)
self._paint_rows(start, end, True) self._paint_rows(start, end, True)
if path[0] < self.undodb.undo_count: if path[0] < self.undodb.undo_count:
# This transaction is an undo candidate # This transaction is an undo candidate
self.redo_button.set_sensitive(False) self.redo_button.set_sensitive(False)
self.undo_button.set_sensitive(self.undodb.undo_available()) self.undo_button.set_sensitive(self.undodb.undo_count)
else: # path[0] >= self.undodb.undo_count: else: # path[0] >= self.undodb.undo_count:
# This transaction is an redo candidate # This transaction is an redo candidate
self.undo_button.set_sensitive(False) self.undo_button.set_sensitive(False)
self.redo_button.set_sensitive(self.undodb.redo_available()) self.redo_button.set_sensitive(self.undodb.redo_count)
def _paint_rows(self, start, end, selected=False): def _paint_rows(self, start, end, selected=False):
if selected: if selected:
@ -144,7 +143,6 @@ class UndoHistory(ManagedWindow.ManagedWindow):
self.model.set(the_iter, 3, bg) self.model.set(the_iter, 3, bg)
def _response(self, obj, response_id): def _response(self, obj, response_id):
assert self.undodb.undo_count == self.undodb.undoindex + 1
if response_id == gtk.RESPONSE_CLOSE: if response_id == gtk.RESPONSE_CLOSE:
self.close(obj) self.close(obj)
@ -154,7 +152,7 @@ class UndoHistory(ManagedWindow.ManagedWindow):
if not node: if not node:
return return
path = self.model.get_path(node) path = self.model.get_path(node)
nsteps = path[0]-self.undodb.undo_count-1 nsteps = path[0] - self.undodb.undo_count - 1
self._move(nsteps or -1) self._move(nsteps or -1)
elif response_id == gtk.RESPONSE_ACCEPT: elif response_id == gtk.RESPONSE_ACCEPT:
@ -163,7 +161,7 @@ class UndoHistory(ManagedWindow.ManagedWindow):
if not node: if not node:
return return
path = self.model.get_path(node) path = self.model.get_path(node)
nsteps = path[0]-self.undodb.undo_count nsteps = path[0] - self.undodb.undo_count
self._move(nsteps or 1) self._move(nsteps or 1)
elif response_id == gtk.RESPONSE_APPLY: elif response_id == gtk.RESPONSE_APPLY:
@ -191,7 +189,7 @@ class UndoHistory(ManagedWindow.ManagedWindow):
self.db.redo_callback(None) self.db.redo_callback(None)
def _move(self, steps=-1): def _move(self, steps=-1):
if steps == 0 : if steps == 0:
return return
func = self.db.undo if steps < 0 else self.db.redo func = self.db.undo if steps < 0 else self.db.redo
@ -201,14 +199,13 @@ class UndoHistory(ManagedWindow.ManagedWindow):
def _update_ui(self): def _update_ui(self):
self._paint_rows(0, len(self.model)-1, False) self._paint_rows(0, len(self.model)-1, False)
self.undo_button.set_sensitive(self.undodb.undo_available()) self.undo_button.set_sensitive(self.undodb.undo_count)
self.redo_button.set_sensitive(self.undodb.redo_available()) self.redo_button.set_sensitive(self.undodb.redo_count)
self.clear_button.set_sensitive( self.clear_button.set_sensitive(
self.undodb.undo_available() or self.undodb.redo_available() self.undodb.undo_count or self.undodb.redo_count
) )
def _build_model(self): def _build_model(self):
assert self.undodb.undoindex+1 == len(self.undodb.undoq)
self.model.clear() self.model.clear()
fg = bg = None fg = bg = None

View File

@ -169,6 +169,9 @@ class DbTxn(defaultdict):
While the list is an arbitrary index of integers, it can be used While the list is an arbitrary index of integers, it can be used
to indicate record numbers for a database. to indicate record numbers for a database.
""" """
if self.first is None or self.last is None:
return []
if not reverse: if not reverse:
return xrange(self.first, self.last+1) return xrange(self.first, self.last+1)
else: else:

View File

@ -54,8 +54,8 @@ import Errors
DBERRS = (db.DBRunRecoveryError, db.DBAccessError, DBERRS = (db.DBRunRecoveryError, db.DBAccessError,
db.DBPageNotFoundError, db.DBInvalidArgError) db.DBPageNotFoundError, db.DBInvalidArgError)
_SIGBASE = ('person', 'family', 'source', 'event', 'media', 'place', _SIGBASE = ('person', 'family', 'source', 'event', 'media',
'repository', 'reference', 'note', 'undoq', 'redoq') 'place', 'repository', 'reference', 'note')
#------------------------------------------------------------------------- #-------------------------------------------------------------------------
# #
# DbUndo class # DbUndo class
@ -67,15 +67,18 @@ class DbUndo(object):
for use with a real backend. for use with a real backend.
""" """
__slots__ = ['undodb', 'db', 'mapbase', 'translist', 'undoindex', __slots__ = ('undodb', 'db', 'mapbase', 'undo_history_timestamp',
'undo_history_timestamp', 'txn'] 'txn', 'undoq', 'redoq')
def __init__(self, grampsdb): def __init__(self, grampsdb):
""" """
Class constructor. Set up main instance variables Class constructor. Set up main instance variables
""" """
self.db = grampsdb self.db = grampsdb
self.clear() self.undoq = deque()
self.redoq = deque()
self.undo_history_timestamp = time.time()
self.txn = None
self.mapbase = ( self.mapbase = (
self.db.person_map, self.db.person_map,
self.db.family_map, self.db.family_map,
@ -92,10 +95,8 @@ class DbUndo(object):
""" """
Clear the undo/redo list (but not the backing storage) Clear the undo/redo list (but not the backing storage)
""" """
self.undoq = deque() self.undoq.clear()
self.redoq = deque() self.redoq.clear()
self.translist = []
self.undoindex = -1
self.undo_history_timestamp = time.time() self.undo_history_timestamp = time.time()
self.txn = None self.txn = None
@ -162,78 +163,47 @@ class DbUndo(object):
""" """
txn.set_description(msg) txn.set_description(msg)
txn.timestamp = time.time() txn.timestamp = time.time()
# If we're within our undo limit, add this transaction
self.undoq.append(txn) self.undoq.append(txn)
self.undoindex += 1
if self.undoindex < DBUNDO:
if self.undoindex >= len(self.translist):
self.translist.append(txn)
else:
self.translist[self.undoindex] = txn
del self.translist[self.undoindex+1:]
self.redoq.clear()
# Otherwise, we've exceeded our undo limit
else:
self.db.abort_possible = False
self.undo_history_timestamp = time.time()
self.translist[-1] = txn
self.redoq.clear()
def undo_available(self):
"""
Return boolean of whether or not there's a possibility of undo.
"""
#print "Undo available:", bool(self.undoq)
return len(self.undoq)
if 0 <= self.undoindex < len(self.translist):
return True
return False
def redo_available(self):
"""
Return boolean of whether or not there's a possibility of redo.
"""
#print "Redo available:", bool(self.redoq)
return len(self.redoq)
if 0 <= self.undoindex+1 < len(self.translist):
return True
return False
def undo(self, update_history=True): def undo(self, update_history=True):
""" """
Undo a previously committed transaction Undo a previously committed transaction
""" """
if self.db.readonly or not self.undo_available(): if self.db.readonly or self.undo_count == 0:
return False return False
return self.__undoredo(update_history, self.__undo) return self.__undo(update_history)
def redo(self, update_history=True): def redo(self, update_history=True):
""" """
Redo a previously committed, then undone, transaction Redo a previously committed, then undone, transaction
""" """
if self.db.readonly or not self.redo_available(): if self.db.readonly or self.redo_count == 0:
return False return False
return self.__undoredo(update_history, self.__redo) return self.__redo(update_history)
def __undoredo(self, update_history, func): def undoredo(func):
""" """
Helper method used by both undo and redo methods. Decorator function to wrap undo and redo operations within a bsddb
transaction. It also catches bsddb errors and raises an exception
as appropriate
""" """
try: def try_(self, *args, **kwargs):
with BSDDBTxn(self.db.env) as txn: try:
self.txn = self.db.txn = txn.txn with BSDDBTxn(self.db.env) as txn:
status = func(update_history) self.txn = self.db.txn = txn.txn
if not status: status = func(self, *args, **kwargs)
txn.abort() if not status:
self.db.txn = None txn.abort()
return status self.db.txn = None
return status
except DBERRS, msg: except DBERRS, msg:
self.db._log_error() self.db._log_error()
raise Errors.DbError(msg) raise Errors.DbError(msg)
return try_
@undoredo
def __undo(self, update_history=True): def __undo(self, update_history=True):
""" """
Access the last committed transaction, and revert the data to the Access the last committed transaction, and revert the data to the
@ -241,11 +211,8 @@ class DbUndo(object):
""" """
txn = self.undoq.pop() txn = self.undoq.pop()
self.redoq.append(txn) self.redoq.append(txn)
#transaction = self.translist[self.undoindex]
#assert transaction == txn
transaction = txn transaction = txn
db = self.db db = self.db
self.undoindex -= 1
subitems = transaction.get_recnos(reverse=True) subitems = transaction.get_recnos(reverse=True)
# Process all records in the transaction # Process all records in the transaction
@ -260,7 +227,7 @@ class DbUndo(object):
db.emit, _SIGBASE[key]) db.emit, _SIGBASE[key])
# Notify listeners # Notify listeners
if db.undo_callback: if db.undo_callback:
if self.undo_available(): if self.undo_count > 0:
db.undo_callback(_("_Undo %s") db.undo_callback(_("_Undo %s")
% transaction.get_description()) % transaction.get_description())
else: else:
@ -274,6 +241,7 @@ class DbUndo(object):
db.undo_history_callback() db.undo_history_callback()
return True return True
@undoredo
def __redo(self, db=None, update_history=True): def __redo(self, db=None, update_history=True):
""" """
Access the last undone transaction, and revert the data to the state Access the last undone transaction, and revert the data to the state
@ -281,9 +249,6 @@ class DbUndo(object):
""" """
txn = self.redoq.pop() txn = self.redoq.pop()
self.undoq.append(txn) self.undoq.append(txn)
self.undoindex += 1
#transaction = self.translist[self.undoindex]
#assert transaction == txn
transaction = txn transaction = txn
db = self.db db = self.db
subitems = transaction.get_recnos() subitems = transaction.get_recnos()
@ -304,8 +269,7 @@ class DbUndo(object):
% transaction.get_description()) % transaction.get_description())
if db.redo_callback: if db.redo_callback:
if len(self.redoq) > 1: if self.redo_count > 1:
#new_transaction = self.translist[self.undoindex+1]
new_transaction = self.redoq[-2] new_transaction = self.redoq[-2]
db.redo_callback(_("_Redo %s") db.redo_callback(_("_Redo %s")
% new_transaction.get_description()) % new_transaction.get_description())
@ -351,10 +315,8 @@ class DbUndo(object):
self.db._log_error() self.db._log_error()
raise Errors.DbError(msg) raise Errors.DbError(msg)
@property undo_count = property(lambda self:len(self.undoq))
def undo_count(self): redo_count = property(lambda self:len(self.redoq))
"""Number of undo requests in the queue"""
return len(self.undoq)
class DbUndoList(DbUndo): class DbUndoList(DbUndo):
""" """