1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
|
#
# THIS FILE IS PART OF THE JOKOSHER PROJECT AND LICENSED UNDER THE GPL. SEE
# THE 'COPYING' FILE FOR DETAILS
#
# UndoSystem.py
#
# Contains the decorator needed to allow other classes to hook specific
# function calls into the undo stack.
#
#=========================================================================
def UndoCommand(*command, **command_options):
"""
Decorates functions, enabling them to be logged in the undo stack.
The decorating process is transparent to the clients.
Parameters:
command -- the undo command list of strings.
command_options -- key-value parameters to change options.
Returns:
an UndoFunction which decorates the original function.
"""
def UndoFunction(func):
"""
This is the actual decorator function. When decorated,
this function will be called with func as the function to
be decorated.
Parameters:
func -- the function to be decorated.
Returns:
an UndoWrapper to replace the function, so that when it
is called, UndoWrapper will be called instead, and will:
1)log the function call to the undo stack, and
2)call the function originally wanted.
"""
def UndoWrapper(funcSelf, *args, **kwargs):
"""
This function will wrap and take the place of the function
that is being decorated. All arguments to the original function
will be saved, and sent to the decorated function call.
The funcSelf value must be the first parameter, because
the first parameter will always be self, and it carries a
reference to the decorated function's class.
Considerations:
All decorated undo functions *must* be in a class or this will fail.
Parameters:
funcSelf -- reference to the decorated function's class.
*args -- parameters meant for the decorated function.
**kwargs -- dictionary of keyword:value parameters
containing the optional _undoAction_ parameter.
_undoAction_ -- has to be passed as a key:value pair inside kwargs.
The AtomicUndoAction object to append the
command to or None to create a default
AtomicUndoAction with only the one command.
Returns:
the wrapped function resulting value.
"""
if kwargs.has_key("_undoAction_"):
atomicUndoObject = kwargs["_undoAction_"]
del kwargs["_undoAction_"]
else:
atomicUndoObject = None
do_incremental_save = True
if command_options.has_key("incremental_save"):
do_incremental_save = command_options["incremental_save"]
try:
result = func(funcSelf, *args, **kwargs)
except CancelUndoCommand, e:
return e.result
project = None
if isinstance(funcSelf, Project.Project):
project = funcSelf
objectString = "P"
elif isinstance(funcSelf, Instrument.Instrument):
project = funcSelf.project
objectString = "I%d" % funcSelf.id
elif isinstance(funcSelf, Event.Event):
project = funcSelf.instrument.project
objectString = "E%d" % funcSelf.id
if do_incremental_save:
inc = IncrementalSave.Action(objectString, func.__name__, args, kwargs)
project.SaveIncrementalAction(inc)
# testing: make sure loading produces an identical result
assert inc.StoreToString() == IncrementalSave.Action.LoadFromString(inc.StoreToString()).StoreToString()
if not atomicUndoObject and project:
atomicUndoObject = project.NewAtomicUndoAction()
if atomicUndoObject:
paramList = []
for param in command[1:]:
try:
value = getattr(funcSelf, param)
except:
continue
else:
paramList.append(value)
atomicUndoObject.AddUndoCommand(objectString, command[0], paramList)
return result
#_____________________________________________________________________
UndoWrapper.wrapped_func = func
return UndoWrapper
#_____________________________________________________________________
return UndoFunction
#_____________________________________________________________________
#=========================================================================
"""
These import statements *must* be placed below the UndoCommand function because
decorators are called at import-time to decorate other functions. Project, Instrument
and Event classes all use the UndoCommand decorator. Therefore importing any of those
modules before UndoCommand is defined will cause a cyclic dependency in which
Event depends on UndoSystem and UndoSystem depends on Event. A cyclic import
dependency will stop the program before it even starts.
"""
import ProjectManager, Globals, Utils
import Project, Event, Instrument
import IncrementalSave
import xml.dom.minidom as xml
#=========================================================================
class CancelUndoCommand(Exception):
"""
This exception can be thrown by a decorated undo
function in order to tell the undo system to not
log the current action. This is useful if something
in the function fails and the action that would have
been logged to the undo stack was never actually completed.
"""
def __init__(self, result=None):
"""
Creates a new instance of CancelUndoCommand.
Parameters:
result -- value the wrapped function intended to return,
but failed and called this exception.
"""
Exception.__init__(self)
self.result = result
#_____________________________________________________________________
#=========================================================================
class AtomicUndoAction:
"""
Contains several undo commands to be treated as a single undoable operation.
Example:
When deleting several Instruments at once, an AtomicUndoAction
containing the commands to resurrect the Instruments will be created.
When the user requests an undo operation, all of the commands stored
in this object will be rolled back, making the operation appear to be
atomic from the user's perspective.
"""
#_____________________________________________________________________
def __init__(self):
"""
Creates a new AtomicUndoAction instance.
"""
self.commandList = []
#_____________________________________________________________________
def AddUndoCommand(self, objectString, function, paramList):
"""
Adds a new undo command to this AtomicUndoAction.
Example:
The parameters passed to this function:
"E2", "Move", [1, 2]
means
'Call Move(1, 2)' on the Event with ID=2
Parameters:
objectString -- the string representing the object and its ID
(ie "E2" for Event with ID == 2).
function -- the name of the function to be called on the object.
paramList -- a list of values to be passed to the function as parameters.
Key, value parameters are not supported.
"""
newTuple = (objectString, function, paramList)
self.commandList.append(newTuple)
Globals.debug("LOG COMMAND: ", newTuple, "from", id(self))
#_____________________________________________________________________
def GetUndoCommands(self):
"""
Obtains the list of undo commands held by this AtomicUndoAction.
Returns:
a list of tuples, each of which contains a single undo command.
"""
return self.commandList
#_____________________________________________________________________
def StoreToXML(self, doc, node):
"""
Stores this instance of AtomicUndoAction into an XML node.
Example:
doc = xml.Document()
node = doc.createElement("Action")
doc.appendChild(node)
StoreToXml(doc, node)
will save this AtomicUndoAction in doc, inside node.
Parameters:
doc -- XML document to save this AtomicUndoAction into.
node -- XML node to store this AtomicUndoAction under.
This node's name should be "Action".
"""
for cmd in self.GetUndoCommands():
commandXML = doc.createElement("Command")
node.appendChild(commandXML)
commandXML.setAttribute("object", cmd[0])
commandXML.setAttribute("function", cmd[1])
Utils.StoreListToXML(doc, commandXML, cmd[2], "Parameter")
#_____________________________________________________________________
#=========================================================================
|