1#!/usr/bin/env python3
2
3#******************************************************************************
4# treespotlist.py, provides a class to do operations on groups of spots
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 collections
16import operator
17from PyQt5.QtWidgets import QApplication
18import treestructure
19import undo
20
21
22class TreeSpotList(list):
23    """Class to do operations on groups of spots.
24
25    Stores a list of nodes.
26    """
27    def __init__(self, spotList=None, sortSpots=True):
28        """Initialize a tree spot group.
29
30        Arguments:
31            spotList -- the initial list of spots
32            sortSpots -- if True sort the spots in tree order
33        """
34        super().__init__()
35        if spotList:
36            self[:] = spotList
37            if sortSpots:
38                self.sort(key=operator.methodcaller('sortKey'))
39
40    def relatedNodes(self):
41        """Return a list of nodes related to these spots.
42
43        Removes any duplicate (cloned) nodes.
44        """
45        tmpDict = collections.OrderedDict()
46        for spot in self:
47            node = spot.nodeRef
48            tmpDict[node.uId] = node
49        return list(tmpDict.values())
50
51    def pasteChild(self, treeStruct, treeView):
52        """Paste child nodes from the clipbaord.
53
54        Return True on success.
55        Arguments:
56            treeStruct -- a ref to the existing tree structure
57            treeView -- a ref to the tree view for expanding nodes
58        """
59        mimeData = QApplication.clipboard().mimeData()
60        parentNodes = self.relatedNodes()
61        if not parentNodes:
62            parentNodes = [treeStruct]
63        undoObj = undo.ChildListUndo(treeStruct.undoList, parentNodes,
64                                     treeFormats=treeStruct.treeFormats)
65        for parent in parentNodes:
66            newStruct = treestructure.structFromMimeData(mimeData)
67            if not newStruct:
68                treeStruct.undoList.removeLastUndo(undoObj)
69                return False
70            newStruct.replaceDuplicateIds(treeStruct.nodeDict)
71            treeStruct.addNodesFromStruct(newStruct, parent)
72        for spot in self:
73            treeView.expandSpot(spot)
74        return True
75
76    def pasteSibling(self, treeStruct, insertBefore=True):
77        """Paste a sibling at the these spots.
78
79        Return True on success.
80        Arguments:
81            treeStruct -- a ref to the existing tree structure
82            insertBefore -- if True, insert before these nodes, o/w after
83        """
84        mimeData = QApplication.clipboard().mimeData()
85        parentNodes = [spot.parentSpot.nodeRef for spot in self]
86        undoObj = undo.ChildListUndo(treeStruct.undoList, parentNodes,
87                                     treeFormats=treeStruct.treeFormats)
88        for spot in self:
89            newStruct = treestructure.structFromMimeData(mimeData)
90            if not newStruct:
91                treeStruct.undoList.removeLastUndo(undoObj)
92                return False
93            newStruct.replaceDuplicateIds(treeStruct.nodeDict)
94            parent = spot.parentSpot.nodeRef
95            pos = parent.childList.index(spot.nodeRef)
96            if not insertBefore:
97                pos += 1
98            treeStruct.addNodesFromStruct(newStruct, parent, pos)
99        return True
100
101    def pasteCloneChild(self, treeStruct, treeView):
102        """Paste child clones from the clipbaord.
103
104        Return True on success.
105        Arguments:
106            treeStruct -- a ref to the existing tree structure
107            treeView -- a ref to the tree view for expanding nodes
108        """
109        mimeData = QApplication.clipboard().mimeData()
110        newStruct = treestructure.structFromMimeData(mimeData)
111        if not newStruct:
112            return False
113        try:
114            existNodes = [treeStruct.nodeDict[node.uId] for node in
115                          newStruct.childList]
116        except KeyError:
117            return False   # nodes copied from other file
118        parentNodes = self.relatedNodes()
119        if not parentNodes:
120            parentNodes = [treeStruct]
121        for parent in parentNodes:
122            if not parent.ancestors().isdisjoint(set(existNodes)):
123                return False   # circular ref
124            for node in existNodes:
125                if parent in node.parents():
126                    return False   # identical siblings
127        undoObj = undo.ChildListUndo(treeStruct.undoList, parentNodes,
128                                     treeFormats=treeStruct.treeFormats)
129        for parent in parentNodes:
130            for node in existNodes:
131                parent.childList.append(node)
132                node.addSpotRef(parent)
133        for spot in self:
134            treeView.expandSpot(spot)
135        return True
136
137    def pasteCloneSibling(self, treeStruct, insertBefore=True):
138        """Paste sibling clones at the these spots.
139
140        Return True on success.
141        Arguments:
142            treeStruct -- a ref to the existing tree structure
143            insertBefore -- if True, insert before these nodes, o/w after
144        """
145        mimeData = QApplication.clipboard().mimeData()
146        newStruct = treestructure.structFromMimeData(mimeData)
147        if not newStruct:
148            return False
149        try:
150            existNodes = [treeStruct.nodeDict[node.uId] for node in
151                          newStruct.childList]
152        except KeyError:
153            return False   # nodes copied from other file
154        parentNodes = [spot.parentSpot.nodeRef for spot in self]
155        for parent in parentNodes:
156            if not parent.ancestors().isdisjoint(set(existNodes)):
157                return False   # circular ref
158            for node in existNodes:
159                if parent in node.parents():
160                    return False   # identical siblings
161        undoObj = undo.ChildListUndo(treeStruct.undoList, parentNodes,
162                                     treeFormats=treeStruct.treeFormats)
163        for spot in self:
164            parent = spot.parentSpot.nodeRef
165            pos = parent.childList.index(spot.nodeRef)
166            if not insertBefore:
167                pos += 1
168            for node in existNodes:
169                parent.childList.insert(pos, node)
170                node.addSpotRef(parent)
171        return True
172
173    def addChild(self, treeStruct, treeView):
174        """Add new child to these spots.
175
176        Return the new spots.
177        Arguments:
178            treeStruct -- a ref to the existing tree structure
179            treeView -- a ref to the tree view for expanding nodes
180        """
181        selSpots = self
182        if not selSpots:
183            selSpots = list(treeStruct.spotRefs)
184        undo.ChildListUndo(treeStruct.undoList, [spot.nodeRef for spot in
185                                                 selSpots])
186        newSpots = []
187        for spot in selSpots:
188            newNode = spot.nodeRef.addNewChild(treeStruct)
189            newSpots.append(newNode.matchedSpot(spot))
190            if spot.parentSpot:  # can't expand root struct spot
191                treeView.expandSpot(spot)
192        return newSpots
193
194    def insertSibling(self, treeStruct, insertBefore=True):
195        """Insert a new sibling node at these nodes.
196
197        Return the new spots.
198        Arguments:
199            treeStruct -- a ref to the existing tree structure
200            insertBefore -- if True, insert before these nodes, o/w after
201        """
202        undo.ChildListUndo(treeStruct.undoList, [spot.parentSpot.nodeRef for
203                                                 spot in self])
204        newSpots = []
205        for spot in self:
206            newNode = spot.parentSpot.nodeRef.addNewChild(treeStruct,
207                                                          spot.nodeRef,
208                                                          insertBefore)
209            newSpots.append(newNode.matchedSpot(spot.parentSpot))
210        return newSpots
211
212    def delete(self, treeStruct):
213        """Delete these spots, return a new spot to select.
214
215        Arguments:
216            treeStruct -- a ref to the existing tree structure
217        """
218        # gather next selected node in decreasing order of desirability
219        nextSel = [spot.nextSiblingSpot() for spot in self]
220        nextSel.extend([spot.prevSiblingSpot() for spot in self])
221        nextSel.extend([spot.parentSpot for spot in self])
222        while (not nextSel[0] or not nextSel[0].parentSpot or
223               nextSel[0] in self):
224            del nextSel[0]
225        spotSet = set(self)
226        branchSpots = [spot for spot in self if
227                       spot.parentSpotSet().isdisjoint(spotSet)]
228        undoParents = {spot.parentSpot.nodeRef for spot in branchSpots}
229        undo.ChildListUndo(treeStruct.undoList, list(undoParents))
230        for spot in branchSpots:
231            treeStruct.deleteNodeSpot(spot)
232        return nextSel[0]
233
234    def indent(self, treeStruct):
235        """Indent these spots.
236
237        Makes them children of their previous siblings.
238        Return the new spots.
239        Arguments:
240            treeStruct -- a ref to the existing tree structure
241        """
242        undoSpots = ([spot.parentSpot for spot in self] +
243                     [spot.prevSiblingSpot() for spot in self])
244        undo.ChildListUndo(treeStruct.undoList, [spot.nodeRef for spot in
245                                                 undoSpots])
246        newSpots = []
247        for spot in self:
248            node = spot.nodeRef
249            newParentSpot = spot.prevSiblingSpot()
250            node.changeParent(spot.parentSpot, newParentSpot)
251            newSpots.append(node.matchedSpot(newParentSpot))
252        return newSpots
253
254    def unindent(self, treeStruct):
255        """Unindent these spots.
256
257        Makes them their parent's next sibling.
258        Return the new spots.
259        Arguments:
260            treeStruct -- a ref to the existing tree structure
261        """
262        undoSpots = [spot.parentSpot for spot in self]
263        undoSpots.extend([spot.parentSpot for spot in undoSpots])
264        undo.ChildListUndo(treeStruct.undoList, [spot.nodeRef for spot in
265                                                 undoSpots])
266        newSpots = []
267        for spot in reversed(self):
268            node = spot.nodeRef
269            oldParentSpot = spot.parentSpot
270            newParentSpot = oldParentSpot.parentSpot
271            pos = (newParentSpot.nodeRef.childList.index(oldParentSpot.nodeRef)
272                   + 1)
273            node.changeParent(oldParentSpot, newParentSpot, pos)
274            newSpots.append(node.matchedSpot(newParentSpot))
275        return newSpots
276
277    def move(self, treeStruct, up=True):
278        """Move these spots up or down by one item.
279
280        Arguments:
281            treeStruct -- a ref to the existing tree structure
282            up -- if True move up, o/w down
283        """
284        undo.ChildListUndo(treeStruct.undoList, [spot.parentSpot.nodeRef
285                                                 for spot in self])
286        if not up:
287            self.reverse()
288        for spot in self:
289            parent = spot.parentSpot.nodeRef
290            pos = parent.childList.index(spot.nodeRef)
291            del parent.childList[pos]
292            pos = pos - 1 if up else pos + 1
293            parent.childList.insert(pos, spot.nodeRef)
294
295    def moveToEnd(self, treeStruct, first=True):
296        """Move these spots to the first or last position.
297
298        Arguments:
299            treeStruct -- a ref to the existing tree structure
300            first -- if True move to first position, o/w last
301        """
302        undo.ChildListUndo(treeStruct.undoList, [spot.parentSpot.nodeRef
303                                                 for spot in self])
304        if first:
305            self.reverse()
306        for spot in self:
307            parent = spot.parentSpot.nodeRef
308            parent.childList.remove(spot.nodeRef)
309            if first:
310                parent.childList.insert(0, spot.nodeRef)
311            else:
312                parent.childList.append(spot.nodeRef)
313