1#!/usr/bin/env python3 2 3#****************************************************************************** 4# undo.py, provides a classes to store and execute undo & redo operations 5# 6# TreeLine, an information storage program 7# Copyright (C) 2018, Douglas W. Bell 8# 9# This is free software; you can redistribute it and/or modify it under the 10# terms of the GNU General Public License, either Version 2 or any later 11# version. This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY. See the included LICENSE file for details. 13#****************************************************************************** 14 15import copy 16import treenode 17import globalref 18 19 20class UndoRedoList(list): 21 """Stores undo or redo objects. 22 """ 23 def __init__(self, action, localControlRef): 24 """Initialize the undo or redo storage. 25 26 Set the number of stored levels based on the user option. 27 Arguments: 28 action -- the Qt action for undo/redo menus 29 localControlRef -- ref control class for selections, modified, etc. 30 """ 31 super().__init__() 32 self.action = action 33 self.action.setEnabled(False) 34 self.localControlRef = localControlRef 35 self.levels = globalref.genOptions['UndoLevels'] 36 self.altListRef = None # holds a ref to redo or undo list 37 38 def addUndoObj(self, undoObject, clearRedo=True): 39 """Add the given undo or redo object to the list. 40 41 Arguments: 42 undoObject -- the object to be added 43 clearRedo -- if true, clear redo list (can't redo after changes) 44 """ 45 self.append(undoObject) 46 del self[:-self.levels] 47 if self.levels == 0: 48 del self[:] 49 self.action.setEnabled(len(self) > 0) 50 if clearRedo and self.altListRef: 51 self.altListRef.clearList() 52 53 def clearList(self): 54 """Empty the undo/redo list, primarily for no redo after a change. 55 """ 56 del self[:] 57 self.action.setEnabled(False) 58 59 def setNumLevels(self): 60 """Change number of stored undo levels to the stored option. 61 """ 62 self.levels = globalref.genOptions['UndoLevels'] 63 del self[:-self.levels] 64 if self.levels == 0: 65 del self[:] 66 self.action.setEnabled(len(self) > 0) 67 68 def removeLastUndo(self, undoObject): 69 """Remove the last undo object if it matches the given object. 70 71 Arguments: 72 undoObject -- the object to be removed 73 """ 74 if self[-1] is undoObject: 75 del self[-1] 76 self.action.setEnabled(len(self) > 0) 77 78 def undo(self): 79 """Save current state to altListRef and restore the last saved state. 80 81 Remove the last undo item from the list. 82 Restore the previous selection and saved doc modified state. 83 """ 84 # # clear selection to avoid crash due to invalid selection: 85 # self.localControlRef.currentSelectionModel().selectSpots([], False) 86 item = self.pop() 87 item.undo(self.altListRef) 88 selectSpots = [node.spotByNumber(num) for (node, num) in 89 item.selectedTuples] 90 self.localControlRef.currentSelectionModel().selectSpots(selectSpots, 91 False) 92 self.localControlRef.setModified(item.modified) 93 self.action.setEnabled(len(self) > 0) 94 95 96class UndoBase: 97 """Abstract base class for undo objects. 98 """ 99 def __init__(self, localControlRef): 100 """Initialize data storage, selected nodes and doc modified status. 101 102 Arguments: 103 localControlRef -- ref control class for selections, modified, etc. 104 """ 105 self.dataList = [] 106 self.treeStructRef = localControlRef.structure 107 self.selectedTuples = [(spot.nodeRef, spot.instanceNumber()) 108 for spot in 109 localControlRef.currentSelectionModel(). 110 selectedSpots()] 111 self.modified = localControlRef.modified 112 113 114class DataUndo(UndoBase): 115 """Info for undo/redo of tree node data changes. 116 """ 117 def __init__(self, listRef, nodes, addChildren=False, addBranch=False, 118 skipSame=False, fieldRef='', notRedo=True): 119 """Create the data undo class and add it to the undoStore. 120 121 Can't use skipSame if addChildren or addBranch are True. 122 Arguments: 123 listRef -- a ref to the undo/redo list this gets added to 124 nodes -- a node or a list of nodes to back up 125 addChildren -- if True, include child nodes 126 addBranch -- if True, include all branch nodes (ignores addChildren 127 skipSame -- if true, don't add an undo that is similar to the last 128 fieldRef -- optional field name ref to check for similar changes 129 notRedo -- if True, clear redo list (after changes) 130 """ 131 super().__init__(listRef.localControlRef) 132 if not isinstance(nodes, list): 133 nodes = [nodes] 134 if (skipSame and listRef and isinstance(listRef[-1], DataUndo) and 135 len(listRef[-1].dataList) == 1 and len(nodes) == 1 and 136 nodes[0] == listRef[-1].dataList[0][0] and 137 fieldRef == listRef[-1].dataList[0][2]): 138 return 139 for node in nodes: 140 if addBranch: 141 for child in node.descendantGen(): 142 self.dataList.append((child, child.data.copy(), '')) 143 else: 144 self.dataList.append((node, node.data.copy(), fieldRef)) 145 if addChildren: 146 for child in node.childList: 147 self.dataList.append((child, child.data.copy(), '')) 148 listRef.addUndoObj(self, notRedo) 149 150 def undo(self, redoRef): 151 """Save current state to redoRef and restore saved state. 152 153 Arguments: 154 redoRef -- the redo list where the current state is saved 155 """ 156 if redoRef != None: 157 DataUndo(redoRef, [data[0] for data in self.dataList], False, 158 False, False, '', False) 159 for node, data, fieldRef in self.dataList: 160 node.data = data 161 162 163class ChildListUndo(UndoBase): 164 """Info for undo/redo of tree node child lists. 165 """ 166 def __init__(self, listRef, nodes, addBranch=False, treeFormats=None, 167 skipSame=False, notRedo=True): 168 """Create the child list undo class and add it to the undoStore. 169 170 Also stores data formats if given. 171 Can't use skipSame if addBranch is True. 172 Arguments: 173 listRef -- a ref to the undo/redo list this gets added to 174 nodes -- a parent node or a list of parents to save children 175 addBranch -- if True, include all branch nodes 176 treeFormats -- the format data to store 177 skipSame -- if true, don't add an undo that is similar to the last 178 notRedo -- if True, clear redo list (after changes) 179 """ 180 super().__init__(listRef.localControlRef) 181 if not isinstance(nodes, list): 182 nodes = [nodes] 183 if (skipSame and listRef and isinstance(listRef[-1], ChildListUndo) 184 and len(listRef[-1].dataList) == 1 and len(nodes) == 1 and 185 nodes[0] == listRef[-1].dataList[0][0]): 186 return 187 self.addBranch = addBranch 188 self.treeFormats = None 189 if treeFormats: 190 self.treeFormats = copy.deepcopy(treeFormats) 191 for node in nodes: 192 if addBranch: 193 for child in node.descendantGen(): 194 self.dataList.append((child, child.childList[:])) 195 else: 196 self.dataList.append((node, node.childList[:])) 197 listRef.addUndoObj(self, notRedo) 198 199 def undo(self, redoRef): 200 """Save current state to redoRef and restore saved state. 201 202 Arguments: 203 redoRef -- the redo list where the current state is saved 204 """ 205 if redoRef != None: 206 formats = None 207 if self.treeFormats: 208 formats = self.treeStructRef.treeFormats 209 ChildListUndo(redoRef, [data[0] for data in self.dataList], False, 210 formats, False, False) 211 if self.treeFormats: 212 self.treeStructRef.configDialogFormats = self.treeFormats 213 self.treeStructRef.applyConfigDialogFormats(False) 214 globalref.mainControl.updateConfigDialog() 215 newNodes = set() 216 oldNodes = set() 217 for node, childList in self.dataList: 218 origChildren = set(node.childList) 219 children = set(childList) 220 newNodes = newNodes | (children - origChildren) 221 oldNodes = oldNodes | (origChildren - children) 222 for node, childList in self.dataList: 223 node.childList = childList 224 self.treeStructRef.rebuildNodeDict() # slow but reliable 225 for oldNode in oldNodes: 226 oldNode.removeInvalidSpotRefs() 227 for node, childList in self.dataList: 228 for child in childList: 229 if child in newNodes: 230 child.addSpotRef(node) 231 232 233class ChildDataUndo(UndoBase): 234 """Info for undo/redo of tree node child data and lists. 235 """ 236 def __init__(self, listRef, nodes, addBranch=False, treeFormats=None, 237 notRedo=True): 238 """Create the child data undo class and add it to the undoStore. 239 240 Arguments: 241 listRef -- a ref to the undo/redo list this gets added to 242 nodes -- a parent node or a list of parents to save children 243 addBranch -- if True, include all branch nodes 244 treeFormats -- the format data to store 245 notRedo -- if True, clear redo list (after changes) 246 """ 247 super().__init__(listRef.localControlRef) 248 if not isinstance(nodes, list): 249 nodes = [nodes] 250 self.addBranch = addBranch 251 self.treeFormats = None 252 if treeFormats: 253 self.treeFormats = copy.deepcopy(treeFormats) 254 for parent in nodes: 255 if addBranch: 256 for node in parent.descendantGen(): 257 self.dataList.append((node, node.data.copy(), 258 node.childList[:])) 259 else: 260 self.dataList.append((parent, parent.data.copy(), 261 parent.childList[:])) 262 for node in parent.childList: 263 self.dataList.append((node, node.data.copy(), 264 node.childList[:])) 265 listRef.addUndoObj(self, notRedo) 266 267 def undo(self, redoRef): 268 """Save current state to redoRef and restore saved state. 269 270 Arguments: 271 redoRef -- the redo list where the current state is saved 272 """ 273 if redoRef != None: 274 formats = None 275 if self.treeFormats: 276 formats = self.treeStructRef.treeFormats 277 ChildDataUndo(redoRef, [data[0] for data in self.dataList], False, 278 formats, False) 279 if self.treeFormats: 280 self.treeStructRef.configDialogFormats = self.treeFormats 281 self.treeStructRef.applyConfigDialogFormats(False) 282 globalref.mainControl.updateConfigDialog() 283 newNodes = set() 284 oldNodes = set() 285 for node, data, childList in self.dataList: 286 origChildren = set(node.childList) 287 children = set(childList) 288 newNodes = newNodes | (children - origChildren) 289 oldNodes = oldNodes | (origChildren - children) 290 for node, data, childList in self.dataList: 291 node.childList = childList 292 node.data = data 293 self.treeStructRef.rebuildNodeDict() # slow but reliable 294 for newNode in newNodes.copy(): 295 for child in newNode.descendantGen(): 296 newNodes.add(child) 297 for oldNode in oldNodes: 298 oldNode.removeInvalidSpotRefs() 299 for node, data, childList in self.dataList: 300 for child in childList: 301 if child in newNodes: 302 child.addSpotRef(node, not self.addBranch) 303 304 305class TypeUndo(UndoBase): 306 """Info for undo/redo of tree node type name changes. 307 308 Also saves node data to cover blank node title replacement and 309 initial data settings. 310 """ 311 def __init__(self, listRef, nodes, notRedo=True): 312 """Create the data undo class and add it to the undoStore. 313 314 Arguments: 315 listRef -- a ref to the undo/redo list this gets added to 316 nodes -- a node or a list of nodes to back up 317 notRedo -- if True, add clones and clear redo list (after changes) 318 """ 319 super().__init__(listRef.localControlRef) 320 if not isinstance(nodes, list): 321 nodes = [nodes] 322 for node in nodes: 323 self.dataList.append((node, node.formatRef.name, node.data.copy())) 324 listRef.addUndoObj(self, notRedo) 325 326 def undo(self, redoRef): 327 """Save current state to redoRef and restore saved state. 328 329 Arguments: 330 redoRef -- the redo list where the current state is saved 331 """ 332 if redoRef != None: 333 TypeUndo(redoRef, [data[0] for data in self.dataList], False) 334 for node, formatName, data in self.dataList: 335 node.formatRef = self.treeStructRef.treeFormats[formatName] 336 node.data = data 337 338 339class FormatUndo(UndoBase): 340 """Info for undo/redo of tree node type format changes. 341 """ 342 def __init__(self, listRef, origTreeFormats, newTreeFormats, 343 notRedo=True): 344 """Create the data undo class and add it to the undoStore. 345 346 Arguments: 347 listRef -- a ref to the undo/redo list this gets added to 348 origTreeFormats -- the format data to store 349 newTreeFormats -- the replacement format, contains rename dicts 350 notRedo -- if True, clear redo list (after changes) 351 """ 352 super().__init__(listRef.localControlRef) 353 self.treeFormats = copy.deepcopy(origTreeFormats) 354 self.treeFormats.fieldRenameDict = {} 355 for typeName, fieldDict in newTreeFormats.fieldRenameDict.items(): 356 self.treeFormats.fieldRenameDict[typeName] = {} 357 for oldName, newName in fieldDict.items(): 358 self.treeFormats.fieldRenameDict[typeName][newName] = oldName 359 self.treeFormats.typeRenameDict = {} 360 for oldName, newName in newTreeFormats.typeRenameDict.items(): 361 self.treeFormats.typeRenameDict[newName] = oldName 362 if newName in self.treeFormats.fieldRenameDict: 363 self.treeFormats.fieldRenameDict[oldName] = (self.treeFormats. 364 fieldRenameDict[newName]) 365 del self.treeFormats.fieldRenameDict[newName] 366 listRef.addUndoObj(self, notRedo) 367 368 def undo(self, redoRef): 369 """Save current state to redoRef and restore saved state. 370 371 Arguments: 372 redoRef -- the redo list where the current state is saved 373 """ 374 if redoRef != None: 375 FormatUndo(redoRef, self.treeStructRef.treeFormats, 376 self.treeFormats, False) 377 self.treeStructRef.configDialogFormats = self.treeFormats 378 self.treeStructRef.applyConfigDialogFormats(False) 379 globalref.mainControl.updateConfigDialog() 380 381 382class ParamUndo(UndoBase): 383 """Info for undo/redo of any variable parameter. 384 """ 385 def __init__(self, listRef, varList, notRedo=True): 386 """Create the data undo class and add it to the undoStore. 387 388 Arguments: 389 listRef -- a ref to the undo/redo list this gets added to 390 varList - list of tuples, variable's owner and variable's name 391 notRedo -- if True, clear redo list (after changes) 392 """ 393 super().__init__(listRef.localControlRef) 394 for varOwner, varName in varList: 395 value = varOwner.__dict__[varName] 396 self.dataList.append((varOwner, varName, value)) 397 listRef.addUndoObj(self, notRedo) 398 399 def undo(self, redoRef): 400 """Save current state to redoRef and restore saved state. 401 402 Arguments: 403 redoRef -- the redo list where the current state is saved 404 """ 405 if redoRef != None: 406 ParamUndo(redoRef, [item[:2] for item in self.dataList], False) 407 for varOwner, varName, value in self.dataList: 408 varOwner.__dict__[varName] = value 409 410 411class StateSettingUndo(UndoBase): 412 """Info for undo/redo of objects with get/set functions for attributes. 413 """ 414 def __init__(self, listRef, getFunction, setFunction, notRedo=True): 415 """Create the data undo class and add it to the undoStore. 416 417 Arguments: 418 listRef -- a ref to the undo/redo list this gets added to 419 getFunction -- a function ref that returns a state variable 420 setFunction -- a function ref that restores from the state varible 421 notRedo -- if True, clear redo list (after changes) 422 """ 423 super().__init__(listRef.localControlRef) 424 self.getFunction = getFunction 425 self.setFunction = setFunction 426 self.data = getFunction() 427 listRef.addUndoObj(self, notRedo) 428 429 def undo(self, redoRef): 430 """Save current state to redoRef and restore saved state. 431 432 Arguments: 433 redoRef -- the redo list where the current state is saved 434 """ 435 if redoRef != None: 436 StateSettingUndo(redoRef, self.getFunction, self.setFunction, 437 False) 438 self.setFunction(self.data) 439