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