You are here: Home V2 Software Software More ... Developer Notes Memops Code Generation Undo Proposal

Undo Proposal

A proposal for an UNDO system for the API.
  • Undo would require a circular array to track the operations to undo. I would propose a Python deque (http://www.python.org/doc/2.5.2/lib/deque-objects.html), which is a double-ended queue.

  • The simplest organisation would be to put the the actual undo information into lists, and to put the lists into the deque.  Each list would then correspond to a waypoint.

    • The API functions would have to do as follows:

if undoDeque.isActive:
  if undoDeque.newWaypoint:
undoDeque.setWayPoint()
undoDeque[-1].extend(undoStepTuple)
    • It might be easiest to keep the undoDeque full to capacity with empty lists,

    • Operations that were not undoable would call clearUndo()

    • We could do redo without much extra hassle - we would just have to keep an empty list on the deque to separate the undo and redo steps

    • We would then need the following interface:

Class UndoDeque(deque):
  __slots__ = ('isActive', _newWaypoint')
 
  def __init__(self, maxUndoDepth=10, isActive=True):
    if maxUndoDepth < 3:
      raise Exception("UndoDeque maxUndoDepth must be at least 3")
   self.isActive = isActive
    self._newWaypoint = False
    for ii in range(maxUndoDepth + 1):
     self.append([])

  def setWaypoint(self):
   if self.isActive:
    if self[0]:
      # There was redo information - clear it
        for ll in self:
         if ll:
           del ll[:]
         else:
           break
      elif self[1]:
       # that was the last free slot - free another one
       del self[1][:]
      #
     self._newWaypoint = False
     self.rotate(-1)
 
  def undoWaypoint(self):
   if self.isActive and self[-1]:
     undoInf = self.pop()
     self.append(list())
     self._newWaypoint = False
     self.doUndo(undoInf)
     self.rotate(1)
    self._newWaypoint = True

  def redoWaypoint(self):
    if self.isActive and self[0]:
     undoInf = self.popleft()
     self.append(list())
     self._newWaypoint = False
     self.doUndo(undoInf)
     self._newWaypoint = True

  def canUndo(self):
   return (self.isActive and bool self[-1])

  def canRedo(self):
    return (self.isActive and bool self[0])

  def clearUndo(self):
    if self.isActive:
      for ll in self:
        del ll[:]

  def activate(self):
    self.isActive = True

  def deActivate(self):
    self.clearUndo() 
    self.isActive = False

  • The doUndo function and the actual undo information could be done in several ways.

    • We would need to indicate the object to work on, the method to call, and the parameters to pass in
      something like
      myObjId, 'setName', 'previousName'
      myObjId,' addResonanceSet', resonanceSet Id
      myObjId, 'delete', None
      myParentId, 'newChild', dictOfValues
      etc.

    • Making a list of tuples would require lots of memory just for the tuples - I would prefer to unroll the tuples and  simply pop the required information off one by one.

    • Having actual objects in the undo Information would be a problem - if the object was later deleted we could not recreate it in the same memory slot. That would mean that we could not later undo operations on it, or that we would need a 'revive' function on objects.

    • I would prefer using an identifier for the object.

      • We could use the expanded key, but that would be quite long and slow.

      • Alternatively we could use a new quicker identifier - that would require us to keep a mapping dictionary to allow for fast finding of objects. We are anyway planning to add a persistent object _ID, that is unique under a given TopObject, in order to make the XML data files diffable. If we did that now, we could use a combination of TopObject and identifier to indicate the object, either for the object to operate on or for values, when e.g. undoing a setLink operation.

    • The doUndo function would be one difficulty. We would get some hassle setting up code to identify the required objects everywhere, including inside the parameter dicts passed to recreated objects, but at least the results should work.

    • Another problem would be cascading deletes. We would need to determine an order of recreation that would always work. Should be doable.

  • To make the whole thing work we would need more:

    • Integrate setWaypoint calls in the code using the API.

    • Make sure we identify all functions (topObject delete, API bypassing, object merge, ...) that should clear the undoDeque.

  • If someone wanted to undo data model operations one at a time, it would be possible to define an undoSingleStep function that undid one step from the undoDeque[-1] list. Hard to see who would want it, though.


Here is how we might write a doUndo function. It assumes that objects are indicated as topObject,objId pairs. The structure of teh undoInf list follows from the code.
def doUndo(self, undoInf):

while undoInf:
topObj = undoInf.pop()
objId = undoInf.pop()
obj = topObj.getById(objId)

func = undoInf.pop()

if func = 'del':
# obj.delete
obj.delete()

else:
info = undoInf.pop()
value = undoInf.pop()

if func.startswith('new'):
# parent.newChild
link1set, linkNset = info
for key,val in value:
if val:
if key in link1set:
value[key] = val[0].getById(val[1])
elif key in linkNset:
ll = val
val = value[key] = list()
while ll:
topObj = ll.pop()
objId = ll.pop()
val.append(topObj.getById(objId))
#
getattr(obj, func)(**value)

else:
# obj. set, add, remove
if info == 'link1':
# link, hicard == 1
value = value[0].getById(value[1])

elif info == 'linkN':
# link, hicard != 1
ll = value
value = list()
while ll:
topObj = ll.pop()
objId = ll.pop()
value.append(topObj.getById(objId))

else:
# attribute or empty link
pass
#
getattr(obj, func)(value)