File: treeselection.py

package info (click to toggle)
treeline 3.1.5-1.1
  • links: PTS
  • area: main
  • in suites: trixie
  • size: 6,508 kB
  • sloc: python: 20,489; javascript: 998; makefile: 54
file content (265 lines) | stat: -rw-r--r-- 9,754 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
#!/usr/bin/env python3

#******************************************************************************
# treeselection.py, provides a class for the tree view's selection model
#
# TreeLine, an information storage program
# Copyright (C) 2018, Douglas W. Bell
#
# This is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License, either Version 2 or any later
# version.  This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY.  See the included LICENSE file for details.
#******************************************************************************

import collections
import json
from PyQt5.QtCore import QItemSelectionModel, QMimeData
from PyQt5.QtGui import QClipboard
from PyQt5.QtWidgets import QApplication
import treestructure
import treespotlist
import globalref


_maxHistoryLength = 10

class TreeSelection(QItemSelectionModel):
    """Class override for the tree view's selection model.

    Provides methods for easier access to selected nodes.
    """
    def __init__(self, model, parent=None):
        """Initialize the selection model.

        Arguments:
            model -- the model for view data
            parent -- the parent tree view
        """
        super().__init__(model, parent)
        self.modelRef = model
        self.tempExpandedSpots = []
        self.prevSpots = []
        self.nextSpots = []
        self.restoreFlag = False
        self.selectionChanged.connect(self.updateSelectLists)

    def selectedCount(self):
        """Return the number of selected spots.
        """
        return len(self.selectedIndexes())

    def selectedSpots(self):
        """Return a SpotList of selected spots, sorted in tree order.
        """
        return treespotlist.TreeSpotList([index.internalPointer() for index in
                                          self.selectedIndexes()])

    def selectedBranchSpots(self):
        """Return a SpotList of spots at the top of selected branches.

        Remvoves any duplicate spots that are already covered by the branches.
        """
        spots = self.selectedSpots()
        spotSet = set(spots)
        return treespotlist.TreeSpotList([spot for spot in spots if
                                          spot.parentSpotSet().
                                          isdisjoint(spotSet)])

    def selectedNodes(self):
        """Return a list of the currently selected tree nodes.

        Removes any duplicate (cloned) nodes.
        """
        tmpDict = collections.OrderedDict()
        for spot in self.selectedSpots():
            node = spot.nodeRef
            tmpDict[node.uId] = node
        return list(tmpDict.values())

    def selectedBranches(self):
        """Return a list of nodes at the top of selected branches.

        Remvoves any duplicates that are already covered by the branches.
        """
        tmpDict = collections.OrderedDict()
        for spot in self.selectedBranchSpots():
            node = spot.nodeRef
            tmpDict[node.uId] = node
        return list(tmpDict.values())

    def currentSpot(self):
        """Return the current tree spot.

        Can raise AttributeError if no spot is current.
        """
        return self.currentIndex().internalPointer()

    def currentNode(self):
        """Return the current tree node.

        Can raise AttributeError if no node is current.
        """
        return self.currentSpot().nodeRef

    def selectSpots(self, spotList, signalUpdate=True, expandParents=False):
        """Clear the current selection and select the given spots.

        Arguments:
            spotList -- the spots to select
            signalUpdate -- if False, block normal select update signals
            expandParents -- open parent spots to make selection visible
        """
        if expandParents:
            treeView = (globalref.mainControl.activeControl.activeWindow.
                        treeView)
            for spot in self.tempExpandedSpots:
                treeView.collapseSpot(spot)
            self.tempExpandedSpots = []
            for spot in spotList:
                parent = spot.parentSpot
                while parent.parentSpot:
                    if not treeView.isSpotExpanded(parent):
                        treeView.expandSpot(parent)
                        self.tempExpandedSpots.append(parent)
                    parent = parent.parentSpot
        if not signalUpdate:
            self.blockSignals(True)
            self.addToHistory(spotList)
        self.clear()
        if spotList:
            for spot in spotList:
                self.select(spot.index(self.modelRef),
                            QItemSelectionModel.Select)
            self.setCurrentIndex(spotList[0].index(self.modelRef),
                                 QItemSelectionModel.Current)
        self.blockSignals(False)

    def selectNodeById(self, nodeId):
        """Select the first spot from the given node ID.

        Return True on success.
        Arguments:
            nodeId -- the ID of the node to select
        """
        try:
            node = self.modelRef.treeStructure.nodeDict[nodeId]
            self.selectSpots([node.spotByNumber(0)], True, True)
        except KeyError:
            return False
        return True

    def setCurrentSpot(self, spot):
        """Set the current spot.

        Arguments:
            spot -- the spot to make current
        """
        self.blockSignals(True)
        self.setCurrentIndex(spot.index(self.modelRef),
                             QItemSelectionModel.Current)
        self.blockSignals(False)

    def copySelectedNodes(self):
        """Copy these node branches to the clipboard.
        """
        nodes = self.selectedBranches()
        if not nodes:
            return
        clip = QApplication.clipboard()
        if clip.supportsSelection():
            titleList = []
            for node in nodes:
                titleList.extend(node.exportTitleText())
            clip.setText('\n'.join(titleList), QClipboard.Selection)
        struct = treestructure.TreeStructure(topNodes=nodes, addSpots=False)
        generics = {formatRef.genericType for formatRef in
                    struct.treeFormats.values() if formatRef.genericType}
        for generic in generics:
            genericRef = self.modelRef.treeStructure.treeFormats[generic]
            struct.treeFormats.addTypeIfMissing(genericRef)
            for formatRef in genericRef.derivedTypes:
                struct.treeFormats.addTypeIfMissing(formatRef)
        data = struct.fileData()
        dataStr = json.dumps(data, indent=0, sort_keys=True)
        mime = QMimeData()
        mime.setData('application/json', bytes(dataStr, encoding='utf-8'))
        clip.setMimeData(mime)

    def restorePrevSelect(self):
        """Go back to the most recent saved selection.
        """
        self.validateHistory()
        if len(self.prevSpots) > 1:
            del self.prevSpots[-1]
            oldSelect = self.selectedSpots()
            if oldSelect and (not self.nextSpots or
                              oldSelect != self.nextSpots[-1]):
                self.nextSpots.append(oldSelect)
            self.restoreFlag = True
            self.selectSpots(self.prevSpots[-1], expandParents=True)
            self.restoreFlag = False

    def restoreNextSelect(self):
        """Go forward to the most recent saved selection.
        """
        self.validateHistory()
        if self.nextSpots:
            select = self.nextSpots.pop(-1)
            if select and (not self.prevSpots or
                           select != self.prevSpots[-1]):
                self.prevSpots.append(select)
            self.restoreFlag = True
            self.selectSpots(select, expandParents=True)
            self.restoreFlag = False

    def addToHistory(self, spots):
        """Add given spots to previous select list.

        Arguments:
            spots -- a list of spots to be added
        """
        if spots and not self.restoreFlag and (not self.prevSpots or
                                               spots != self.prevSpots[-1]):
            self.prevSpots.append(spots)
            if len(self.prevSpots) > _maxHistoryLength:
                del self.prevSpots[:2]
            self.nextSpots = []

    def validateHistory(self):
        """Clear invalid items from history lists.
        """
        for histList in (self.prevSpots, self.nextSpots):
            for spots in histList:
                spots[:] = [spot for spot in spots if spot.isValid()]
            histList[:] = [spots for spots in histList if spots]

    def updateSelectLists(self):
        """Update history after a selection change.
        """
        self.addToHistory(self.selectedSpots())

    def selectTitleMatch(self, searchText, forward=True, includeCurrent=False):
        """Select a node with a title matching the search text.

        Returns True if found, otherwise False.
        Arguments:
            searchText -- the text to look for
            forward -- next if True, previous if False
            includeCurrent -- look in current node if True
        """
        searchText = searchText.lower()
        currentSpot = self.currentSpot()
        spot = currentSpot
        while True:
            if not includeCurrent:
                if forward:
                    spot = spot.nextTreeSpot(True)
                else:
                    spot = spot.prevTreeSpot(True)
                if spot is currentSpot:
                    return False
            includeCurrent = False
            if searchText in spot.nodeRef.title().lower():
                self.selectSpots([spot], True, True)
                return True