1#!/usr/bin/env python3
2
3#******************************************************************************
4# treespot.py, provides a class to store locations of tree node instances
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 sys
16import operator
17
18
19class TreeSpot:
20    """Class to store location info for tree node instances.
21
22    Used to generate breadcrumb navigation and interface with tree views.
23    A spot without a parent spot is an imaginary root spot, wihout a real node.
24    """
25    def __init__(self, nodeRef, parentSpot):
26        """Initialize a tree spot.
27
28        Arguments:
29            nodeRef -- reference to the associated tree node
30            parentSpot -- the parent TreeSpot object
31        """
32        self.nodeRef = nodeRef
33        self.parentSpot = parentSpot
34
35    def index(self, modelRef):
36        """Returns the index of this spot in the tree model.
37
38        Arguments:
39            modelRef -- a ref to the tree model
40        """
41        return modelRef.createIndex(self.row(), 0, self)
42
43    def row(self):
44        """Return the rank of this spot in its parent's child list.
45
46        Should never be called from the imaginary root spot.
47        """
48        try:
49            return self.parentSpot.nodeRef.childList.index(self.nodeRef)
50        except ValueError:
51            return 0  #  avoid error message from interim view updates
52
53    def instanceNumber(self):
54        """Return this spot's rank in the node's spot list.
55        """
56        spotList = sorted(list(self.nodeRef.spotRefs),
57                          key=operator.methodcaller('sortKey'))
58        return spotList.index(self)
59
60    def spotId(self):
61        """Return a spot ID string, in the form "nodeID:spotInstance".
62        """
63        return '{0}:{1:d}'.format(self.nodeRef.uId, self.instanceNumber())
64
65    def isValid(self):
66        """Return True if spot references and all parents are valid.
67        """
68        spot = self
69        while spot.parentSpot:
70            if not (spot in spot.nodeRef.spotRefs and
71                    spot.nodeRef in spot.parentSpot.nodeRef.childList):
72                return False
73            spot = spot.parentSpot
74        if not spot in spot.nodeRef.spotRefs:
75            return False
76        return True
77
78    def spotDescendantGen(self):
79        """Return a generator to step through all spots in this branch.
80
81        Includes self.
82        """
83        yield self
84        for childSpot in self.childSpots():
85            for spot in childSpot.spotDescendantGen():
86                yield spot
87
88    def spotDescendantOnlyGen(self):
89        """Return a generator to step through the spots in this branch.
90
91        Does not include self.
92        """
93        for childSpot in self.childSpots():
94            yield childSpot
95            for spot in childSpot.spotDescendantGen():
96                yield spot
97
98    def expandedSpotDescendantGen(self, treeView):
99        """Return a generator to step through expanded spots in this branch.
100
101        Does not include root spot.
102        Arguments:
103            treeView -- a ref to the treeview
104        """
105        for childSpot in self.childSpots():
106            if treeView.isSpotExpanded(childSpot):
107                yield childSpot
108                for spot in childSpot.expandedSpotDescendantGen(treeView):
109                    yield spot
110
111    def levelSpotDescendantGen(self, treeView, includeRoot=True, maxLevel=None,
112                               openOnly=False, initLevel=0):
113        """Return generator with (spot, level) tuples for this branch.
114
115        Arguments:
116            treeView -- a ref to the treeview, requiired to check if open
117            includeRoot -- if True, the root spot is included
118            maxLevel -- the max number of levels to return (no limit if none)
119            openOnly -- if True, only include children open in the given view
120            initLevel -- the level number to start with
121        """
122        if maxLevel == None:
123            maxLevel = sys.maxsize
124        if includeRoot:
125            yield (self, initLevel)
126            initLevel += 1
127        if initLevel < maxLevel and (not openOnly or
128                                     treeView.isSpotExpanded(self)):
129            for childSpot in self.childSpots():
130                for spot, level in childSpot.levelSpotDescendantGen(treeView,
131                                                                    True,
132                                                                    maxLevel,
133                                                                    openOnly,
134                                                                    initLevel):
135                    yield (spot, level)
136
137    def childSpots(self):
138        """Return a list of immediate child spots.
139        """
140        return [childNode.matchedSpot(self) for childNode in
141                self.nodeRef.childList]
142
143    def prevSiblingSpot(self):
144        """Return the nearest previous sibling spot or None.
145        """
146        if self.parentSpot:
147            pos = self.row()
148            if pos > 0:
149                node = self.parentSpot.nodeRef.childList[pos - 1]
150                return node.matchedSpot(self.parentSpot)
151        return None
152
153    def nextSiblingSpot(self):
154        """Return the nearest next sibling spot or None.
155        """
156        if self.parentSpot:
157            childList = self.parentSpot.nodeRef.childList
158            pos = self.row() + 1
159            if pos < len(childList):
160                return childList[pos].matchedSpot(self.parentSpot)
161        return None
162
163    def prevTreeSpot(self, loop=False):
164        """Return the previous node in the tree order.
165
166        Return None at the start of the tree unless loop is true.
167        Arguments:
168            loop -- return the last node of the tree after the first if true
169        """
170        sibling = self.prevSiblingSpot()
171        if sibling:
172            return sibling.lastDescendantSpot()
173        if self.parentSpot.parentSpot:
174            return self.parentSpot
175        elif loop:
176            return self.rootSpot().lastDescendantSpot()
177        return None
178
179    def nextTreeSpot(self, loop=False):
180        """Return the next node in the tree order.
181
182        Return None at the end of the tree unless loop is true.
183        Arguments:
184            loop -- return the root node at the end of the tree if true
185        """
186        if self.nodeRef.childList:
187            return self.nodeRef.childList[0].matchedSpot(self)
188        ancestor = self
189        while ancestor.parentSpot:
190            sibling = ancestor.nextSiblingSpot()
191            if sibling:
192                return sibling
193            ancestor = ancestor.parentSpot
194        if loop:
195            return ancestor.nodeRef.childList[0].matchedSpot(ancestor)
196        return None
197
198    def lastDescendantSpot(self):
199        """Return the last spot of this spots's branch (last in tree order).
200        """
201        spot = self
202        while spot.nodeRef.childList:
203            spot = spot.nodeRef.childList[-1].matchedSpot(spot)
204        return spot
205
206    def spotChain(self):
207        """Return a list of parent spots, including self.
208        """
209        chain = []
210        spot = self
211        while spot.parentSpot:
212            chain.insert(0, spot)
213            spot = spot.parentSpot
214        return chain
215
216    def parentSpotSet(self):
217        """Return a set of ancestor spots, not including self.
218        """
219        result = set()
220        spot = self.parentSpot
221        while spot.parentSpot:
222            result.add(spot)
223            spot = spot.parentSpot
224        return result
225
226    def rootSpot(self):
227        """Return the root spot that references the tree structure.
228        """
229        spot = self
230        while spot.parentSpot:
231            spot = spot.parentSpot
232        return spot
233
234    def sortKey(self):
235        """Return a tuple of parent row positions for sorting in tree order.
236        """
237        positions = []
238        spot = self
239        while spot.parentSpot:
240            positions.insert(0, spot.row())
241            spot = spot.parentSpot
242        return tuple(positions)
243