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