1#!/usr/bin/env python3
2
3#******************************************************************************
4# treeselection.py, provides a class for the tree view's selection model
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 json
17from PyQt5.QtCore import QItemSelectionModel, QMimeData
18from PyQt5.QtGui import QClipboard
19from PyQt5.QtWidgets import QApplication
20import treestructure
21import treespotlist
22import globalref
23
24
25_maxHistoryLength = 10
26
27class TreeSelection(QItemSelectionModel):
28    """Class override for the tree view's selection model.
29
30    Provides methods for easier access to selected nodes.
31    """
32    def __init__(self, model, parent=None):
33        """Initialize the selection model.
34
35        Arguments:
36            model -- the model for view data
37            parent -- the parent tree view
38        """
39        super().__init__(model, parent)
40        self.modelRef = model
41        self.tempExpandedSpots = []
42        self.prevSpots = []
43        self.nextSpots = []
44        self.restoreFlag = False
45        self.selectionChanged.connect(self.updateSelectLists)
46
47    def selectedCount(self):
48        """Return the number of selected spots.
49        """
50        return len(self.selectedIndexes())
51
52    def selectedSpots(self):
53        """Return a SpotList of selected spots, sorted in tree order.
54        """
55        return treespotlist.TreeSpotList([index.internalPointer() for index in
56                                          self.selectedIndexes()])
57
58    def selectedBranchSpots(self):
59        """Return a SpotList of spots at the top of selected branches.
60
61        Remvoves any duplicate spots that are already covered by the branches.
62        """
63        spots = self.selectedSpots()
64        spotSet = set(spots)
65        return treespotlist.TreeSpotList([spot for spot in spots if
66                                          spot.parentSpotSet().
67                                          isdisjoint(spotSet)])
68
69    def selectedNodes(self):
70        """Return a list of the currently selected tree nodes.
71
72        Removes any duplicate (cloned) nodes.
73        """
74        tmpDict = collections.OrderedDict()
75        for spot in self.selectedSpots():
76            node = spot.nodeRef
77            tmpDict[node.uId] = node
78        return list(tmpDict.values())
79
80    def selectedBranches(self):
81        """Return a list of nodes at the top of selected branches.
82
83        Remvoves any duplicates that are already covered by the branches.
84        """
85        tmpDict = collections.OrderedDict()
86        for spot in self.selectedBranchSpots():
87            node = spot.nodeRef
88            tmpDict[node.uId] = node
89        return list(tmpDict.values())
90
91    def currentSpot(self):
92        """Return the current tree spot.
93
94        Can raise AttributeError if no spot is current.
95        """
96        return self.currentIndex().internalPointer()
97
98    def currentNode(self):
99        """Return the current tree node.
100
101        Can raise AttributeError if no node is current.
102        """
103        return self.currentSpot().nodeRef
104
105    def selectSpots(self, spotList, signalUpdate=True, expandParents=False):
106        """Clear the current selection and select the given spots.
107
108        Arguments:
109            spotList -- the spots to select
110            signalUpdate -- if False, block normal select update signals
111            expandParents -- open parent spots to make selection visible
112        """
113        if expandParents:
114            treeView = (globalref.mainControl.activeControl.activeWindow.
115                        treeView)
116            for spot in self.tempExpandedSpots:
117                treeView.collapseSpot(spot)
118            self.tempExpandedSpots = []
119            for spot in spotList:
120                parent = spot.parentSpot
121                while parent.parentSpot:
122                    if not treeView.isSpotExpanded(parent):
123                        treeView.expandSpot(parent)
124                        self.tempExpandedSpots.append(parent)
125                    parent = parent.parentSpot
126        if not signalUpdate:
127            self.blockSignals(True)
128            self.addToHistory(spotList)
129        self.clear()
130        if spotList:
131            for spot in spotList:
132                self.select(spot.index(self.modelRef),
133                            QItemSelectionModel.Select)
134            self.setCurrentIndex(spotList[0].index(self.modelRef),
135                                 QItemSelectionModel.Current)
136        self.blockSignals(False)
137
138    def selectNodeById(self, nodeId):
139        """Select the first spot from the given node ID.
140
141        Return True on success.
142        Arguments:
143            nodeId -- the ID of the node to select
144        """
145        try:
146            node = self.modelRef.treeStructure.nodeDict[nodeId]
147            self.selectSpots([node.spotByNumber(0)], True, True)
148        except KeyError:
149            return False
150        return True
151
152    def setCurrentSpot(self, spot):
153        """Set the current spot.
154
155        Arguments:
156            spot -- the spot to make current
157        """
158        self.blockSignals(True)
159        self.setCurrentIndex(spot.index(self.modelRef),
160                             QItemSelectionModel.Current)
161        self.blockSignals(False)
162
163    def copySelectedNodes(self):
164        """Copy these node branches to the clipboard.
165        """
166        nodes = self.selectedBranches()
167        if not nodes:
168            return
169        clip = QApplication.clipboard()
170        if clip.supportsSelection():
171            titleList = []
172            for node in nodes:
173                titleList.extend(node.exportTitleText())
174            clip.setText('\n'.join(titleList), QClipboard.Selection)
175        struct = treestructure.TreeStructure(topNodes=nodes, addSpots=False)
176        generics = {formatRef.genericType for formatRef in
177                    struct.treeFormats.values() if formatRef.genericType}
178        for generic in generics:
179            genericRef = self.modelRef.treeStructure.treeFormats[generic]
180            struct.treeFormats.addTypeIfMissing(genericRef)
181            for formatRef in genericRef.derivedTypes:
182                struct.treeFormats.addTypeIfMissing(formatRef)
183        data = struct.fileData()
184        dataStr = json.dumps(data, indent=0, sort_keys=True)
185        mime = QMimeData()
186        mime.setData('application/json', bytes(dataStr, encoding='utf-8'))
187        clip.setMimeData(mime)
188
189    def restorePrevSelect(self):
190        """Go back to the most recent saved selection.
191        """
192        self.validateHistory()
193        if len(self.prevSpots) > 1:
194            del self.prevSpots[-1]
195            oldSelect = self.selectedSpots()
196            if oldSelect and (not self.nextSpots or
197                              oldSelect != self.nextSpots[-1]):
198                self.nextSpots.append(oldSelect)
199            self.restoreFlag = True
200            self.selectSpots(self.prevSpots[-1], expandParents=True)
201            self.restoreFlag = False
202
203    def restoreNextSelect(self):
204        """Go forward to the most recent saved selection.
205        """
206        self.validateHistory()
207        if self.nextSpots:
208            select = self.nextSpots.pop(-1)
209            if select and (not self.prevSpots or
210                           select != self.prevSpots[-1]):
211                self.prevSpots.append(select)
212            self.restoreFlag = True
213            self.selectSpots(select, expandParents=True)
214            self.restoreFlag = False
215
216    def addToHistory(self, spots):
217        """Add given spots to previous select list.
218
219        Arguments:
220            spots -- a list of spots to be added
221        """
222        if spots and not self.restoreFlag and (not self.prevSpots or
223                                               spots != self.prevSpots[-1]):
224            self.prevSpots.append(spots)
225            if len(self.prevSpots) > _maxHistoryLength:
226                del self.prevSpots[:2]
227            self.nextSpots = []
228
229    def validateHistory(self):
230        """Clear invalid items from history lists.
231        """
232        for histList in (self.prevSpots, self.nextSpots):
233            for spots in histList:
234                spots[:] = [spot for spot in spots if spot.isValid()]
235            histList[:] = [spots for spots in histList if spots]
236
237    def updateSelectLists(self):
238        """Update history after a selection change.
239        """
240        self.addToHistory(self.selectedSpots())
241
242    def selectTitleMatch(self, searchText, forward=True, includeCurrent=False):
243        """Select a node with a title matching the search text.
244
245        Returns True if found, otherwise False.
246        Arguments:
247            searchText -- the text to look for
248            forward -- next if True, previous if False
249            includeCurrent -- look in current node if True
250        """
251        searchText = searchText.lower()
252        currentSpot = self.currentSpot()
253        spot = currentSpot
254        while True:
255            if not includeCurrent:
256                if forward:
257                    spot = spot.nextTreeSpot(True)
258                else:
259                    spot = spot.prevTreeSpot(True)
260                if spot is currentSpot:
261                    return False
262            includeCurrent = False
263            if searchText in spot.nodeRef.title().lower():
264                self.selectSpots([spot], True, True)
265                return True
266