src/eric7/WebBrowser/History/HistoryCompleter.py

branch
eric7
changeset 9221
bf71ee032bb4
parent 9209
b99e7fd55fd3
child 9473
3f23dbf37dbe
equal deleted inserted replaced
9220:e9e7eca7efee 9221:bf71ee032bb4
18 18
19 class HistoryCompletionView(QTableView): 19 class HistoryCompletionView(QTableView):
20 """ 20 """
21 Class implementing a special completer view for history based completions. 21 Class implementing a special completer view for history based completions.
22 """ 22 """
23
23 def __init__(self, parent=None): 24 def __init__(self, parent=None):
24 """ 25 """
25 Constructor 26 Constructor
26 27
27 @param parent reference to the parent widget (QWidget) 28 @param parent reference to the parent widget (QWidget)
28 """ 29 """
29 super().__init__(parent) 30 super().__init__(parent)
30 31
31 self.horizontalHeader().hide() 32 self.horizontalHeader().hide()
32 self.verticalHeader().hide() 33 self.verticalHeader().hide()
33 34
34 self.setShowGrid(False) 35 self.setShowGrid(False)
35 36
36 self.setSelectionBehavior( 37 self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
37 QAbstractItemView.SelectionBehavior.SelectRows)
38 self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) 38 self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
39 self.setTextElideMode(Qt.TextElideMode.ElideRight) 39 self.setTextElideMode(Qt.TextElideMode.ElideRight)
40 40
41 metrics = self.fontMetrics() 41 metrics = self.fontMetrics()
42 self.verticalHeader().setDefaultSectionSize(metrics.height()) 42 self.verticalHeader().setDefaultSectionSize(metrics.height())
43 43
44 def resizeEvent(self, evt): 44 def resizeEvent(self, evt):
45 """ 45 """
46 Protected method handling resize events. 46 Protected method handling resize events.
47 47
48 @param evt reference to the resize event (QResizeEvent) 48 @param evt reference to the resize event (QResizeEvent)
49 """ 49 """
50 self.horizontalHeader().resizeSection(0, int(0.65 * self.width())) 50 self.horizontalHeader().resizeSection(0, int(0.65 * self.width()))
51 self.horizontalHeader().setStretchLastSection(True) 51 self.horizontalHeader().setStretchLastSection(True)
52 52
53 super().resizeEvent(evt) 53 super().resizeEvent(evt)
54 54
55 def sizeHintForRow(self, row): 55 def sizeHintForRow(self, row):
56 """ 56 """
57 Public method to give a size hint for rows. 57 Public method to give a size hint for rows.
58 58
59 @param row row number (integer) 59 @param row row number (integer)
60 @return desired row height (integer) 60 @return desired row height (integer)
61 """ 61 """
62 metrics = self.fontMetrics() 62 metrics = self.fontMetrics()
63 return metrics.height() 63 return metrics.height()
65 65
66 class HistoryCompletionModel(QSortFilterProxyModel): 66 class HistoryCompletionModel(QSortFilterProxyModel):
67 """ 67 """
68 Class implementing a special model for history based completions. 68 Class implementing a special model for history based completions.
69 """ 69 """
70
70 HistoryCompletionRole = HistoryFilterModel.MaxRole + 1 71 HistoryCompletionRole = HistoryFilterModel.MaxRole + 1
71 72
72 def __init__(self, parent=None): 73 def __init__(self, parent=None):
73 """ 74 """
74 Constructor 75 Constructor
75 76
76 @param parent reference to the parent object (QObject) 77 @param parent reference to the parent object (QObject)
77 """ 78 """
78 super().__init__(parent) 79 super().__init__(parent)
79 80
80 self.__searchString = "" 81 self.__searchString = ""
81 self.__searchMatcher = None 82 self.__searchMatcher = None
82 self.__wordMatcher = None 83 self.__wordMatcher = None
83 self.__isValid = False 84 self.__isValid = False
84 85
85 self.setDynamicSortFilter(True) 86 self.setDynamicSortFilter(True)
86 87
87 def data(self, index, role=Qt.ItemDataRole.DisplayRole): 88 def data(self, index, role=Qt.ItemDataRole.DisplayRole):
88 """ 89 """
89 Public method to get data from the model. 90 Public method to get data from the model.
90 91
91 @param index index of history entry to get data for (QModelIndex) 92 @param index index of history entry to get data for (QModelIndex)
92 @param role data role (integer) 93 @param role data role (integer)
93 @return history entry data 94 @return history entry data
94 """ 95 """
95 # If the model is valid, tell QCompleter that everything we have 96 # If the model is valid, tell QCompleter that everything we have
97 if role == self.HistoryCompletionRole and index.isValid(): 98 if role == self.HistoryCompletionRole and index.isValid():
98 if self.isValid(): 99 if self.isValid():
99 return "t" 100 return "t"
100 else: 101 else:
101 return "f" 102 return "f"
102 103
103 if role == Qt.ItemDataRole.DisplayRole: 104 if role == Qt.ItemDataRole.DisplayRole:
104 if index.column() == 0: 105 if index.column() == 0:
105 role = HistoryModel.UrlStringRole 106 role = HistoryModel.UrlStringRole
106 else: 107 else:
107 role = HistoryModel.TitleRole 108 role = HistoryModel.TitleRole
108 109
109 return QSortFilterProxyModel.data(self, index, role) 110 return QSortFilterProxyModel.data(self, index, role)
110 111
111 def searchString(self): 112 def searchString(self):
112 """ 113 """
113 Public method to get the current search string. 114 Public method to get the current search string.
114 115
115 @return current search string (string) 116 @return current search string (string)
116 """ 117 """
117 return self.__searchString 118 return self.__searchString
118 119
119 def setSearchString(self, sstring): 120 def setSearchString(self, sstring):
120 """ 121 """
121 Public method to set the current search string. 122 Public method to set the current search string.
122 123
123 @param sstring new search string (string) 124 @param sstring new search string (string)
124 """ 125 """
125 if sstring != self.__searchString: 126 if sstring != self.__searchString:
126 self.__searchString = sstring 127 self.__searchString = sstring
127 self.__searchMatcher = re.compile( 128 self.__searchMatcher = re.compile(
128 re.escape(self.__searchString), re.IGNORECASE) 129 re.escape(self.__searchString), re.IGNORECASE
130 )
129 self.__wordMatcher = re.compile( 131 self.__wordMatcher = re.compile(
130 r"\b" + re.escape(self.__searchString), re.IGNORECASE) 132 r"\b" + re.escape(self.__searchString), re.IGNORECASE
133 )
131 self.invalidateFilter() 134 self.invalidateFilter()
132 135
133 def isValid(self): 136 def isValid(self):
134 """ 137 """
135 Public method to check the model for validity. 138 Public method to check the model for validity.
136 139
137 @return flag indicating a valid status (boolean) 140 @return flag indicating a valid status (boolean)
138 """ 141 """
139 return self.__isValid 142 return self.__isValid
140 143
141 def setValid(self, valid): 144 def setValid(self, valid):
142 """ 145 """
143 Public method to set the model's validity. 146 Public method to set the model's validity.
144 147
145 @param valid flag indicating the new valid status (boolean) 148 @param valid flag indicating the new valid status (boolean)
146 """ 149 """
147 if valid == self.__isValid: 150 if valid == self.__isValid:
148 return 151 return
149 152
150 self.__isValid = valid 153 self.__isValid = valid
151 154
152 # tell the history completer that the model has changed 155 # tell the history completer that the model has changed
153 self.dataChanged.emit(self.index(0, 0), self.index(0, 156 self.dataChanged.emit(self.index(0, 0), self.index(0, self.rowCount() - 1))
154 self.rowCount() - 1)) 157
155
156 def filterAcceptsRow(self, sourceRow, sourceParent): 158 def filterAcceptsRow(self, sourceRow, sourceParent):
157 """ 159 """
158 Public method to determine, if the row is acceptable. 160 Public method to determine, if the row is acceptable.
159 161
160 @param sourceRow row number in the source model (integer) 162 @param sourceRow row number in the source model (integer)
161 @param sourceParent index of the source item (QModelIndex) 163 @param sourceParent index of the source item (QModelIndex)
162 @return flag indicating acceptance (boolean) 164 @return flag indicating acceptance (boolean)
163 """ 165 """
164 if self.__searchMatcher is not None: 166 if self.__searchMatcher is not None:
165 # Do a case-insensitive substring match against both the url and 167 # Do a case-insensitive substring match against both the url and
166 # title. It's already ensured, that the user doesn't accidentally 168 # title. It's already ensured, that the user doesn't accidentally
167 # use regexp metacharacters (s. setSearchString()). 169 # use regexp metacharacters (s. setSearchString()).
168 idx = self.sourceModel().index(sourceRow, 0, sourceParent) 170 idx = self.sourceModel().index(sourceRow, 0, sourceParent)
169 171
170 url = self.sourceModel().data(idx, HistoryModel.UrlStringRole) 172 url = self.sourceModel().data(idx, HistoryModel.UrlStringRole)
171 if self.__searchMatcher.search(url) is not None: 173 if self.__searchMatcher.search(url) is not None:
172 return True 174 return True
173 175
174 title = self.sourceModel().data(idx, HistoryModel.TitleRole) 176 title = self.sourceModel().data(idx, HistoryModel.TitleRole)
175 if self.__searchMatcher.search(title) is not None: 177 if self.__searchMatcher.search(title) is not None:
176 return True 178 return True
177 179
178 return False 180 return False
179 181
180 def lessThan(self, left, right): 182 def lessThan(self, left, right):
181 """ 183 """
182 Public method used to sort the displayed items. 184 Public method used to sort the displayed items.
183 185
184 It implements a special sorting function based on the history entry's 186 It implements a special sorting function based on the history entry's
185 frequency giving a bonus to hits that match on a word boundary so that 187 frequency giving a bonus to hits that match on a word boundary so that
186 e.g. "dot.python-projects.org" is a better result for typing "dot" than 188 e.g. "dot.python-projects.org" is a better result for typing "dot" than
187 "slashdot.org". However, it only looks for the string in the host name, 189 "slashdot.org". However, it only looks for the string in the host name,
188 not the entire URL, since while it makes sense to e.g. give 190 not the entire URL, since while it makes sense to e.g. give
189 "www.phoronix.com" a bonus for "ph", it does NOT make sense to give 191 "www.phoronix.com" a bonus for "ph", it does NOT make sense to give
190 "www.yadda.com/foo.php" the bonus. 192 "www.yadda.com/foo.php" the bonus.
191 193
192 @param left index of left item 194 @param left index of left item
193 @type QModelIndex 195 @type QModelIndex
194 @param right index of right item 196 @param right index of right item
195 @type QModelIndex 197 @type QModelIndex
196 @return true, if left is less than right 198 @return true, if left is less than right
197 @rtype bool 199 @rtype bool
198 """ 200 """
199 frequency_L = self.sourceModel().data( 201 frequency_L = self.sourceModel().data(left, HistoryFilterModel.FrequencyRole)
200 left, HistoryFilterModel.FrequencyRole)
201 url_L = self.sourceModel().data(left, HistoryModel.UrlRole).host() 202 url_L = self.sourceModel().data(left, HistoryModel.UrlRole).host()
202 title_L = self.sourceModel().data(left, HistoryModel.TitleRole) 203 title_L = self.sourceModel().data(left, HistoryModel.TitleRole)
203 204
204 if ( 205 if self.__wordMatcher is not None and (
205 self.__wordMatcher is not None and 206 bool(self.__wordMatcher.search(url_L))
206 (bool(self.__wordMatcher.search(url_L)) or 207 or bool(self.__wordMatcher.search(title_L))
207 bool(self.__wordMatcher.search(title_L)))
208 ): 208 ):
209 frequency_L *= 2 209 frequency_L *= 2
210 210
211 frequency_R = self.sourceModel().data( 211 frequency_R = self.sourceModel().data(right, HistoryFilterModel.FrequencyRole)
212 right, HistoryFilterModel.FrequencyRole)
213 url_R = self.sourceModel().data(right, HistoryModel.UrlRole).host() 212 url_R = self.sourceModel().data(right, HistoryModel.UrlRole).host()
214 title_R = self.sourceModel().data(right, HistoryModel.TitleRole) 213 title_R = self.sourceModel().data(right, HistoryModel.TitleRole)
215 214
216 if ( 215 if self.__wordMatcher is not None and (
217 self.__wordMatcher is not None and 216 bool(self.__wordMatcher.search(url_R))
218 (bool(self.__wordMatcher.search(url_R)) or 217 or bool(self.__wordMatcher.search(title_R))
219 bool(self.__wordMatcher.search(title_R)))
220 ): 218 ):
221 frequency_R *= 2 219 frequency_R *= 2
222 220
223 # Sort results in descending frequency-derived score. 221 # Sort results in descending frequency-derived score.
224 return frequency_R < frequency_L 222 return frequency_R < frequency_L
225 223
226 224
227 class HistoryCompleter(QCompleter): 225 class HistoryCompleter(QCompleter):
228 """ 226 """
229 Class implementing a completer for the browser history. 227 Class implementing a completer for the browser history.
230 """ 228 """
229
231 def __init__(self, model, parent=None): 230 def __init__(self, model, parent=None):
232 """ 231 """
233 Constructor 232 Constructor
234 233
235 @param model reference to the model (QAbstractItemModel) 234 @param model reference to the model (QAbstractItemModel)
236 @param parent reference to the parent object (QObject) 235 @param parent reference to the parent object (QObject)
237 """ 236 """
238 super().__init__(model, parent) 237 super().__init__(model, parent)
239 238
240 self.setPopup(HistoryCompletionView()) 239 self.setPopup(HistoryCompletionView())
241 240
242 # Completion should be against the faked role. 241 # Completion should be against the faked role.
243 self.setCompletionRole(HistoryCompletionModel.HistoryCompletionRole) 242 self.setCompletionRole(HistoryCompletionModel.HistoryCompletionRole)
244 243
245 # Since the completion role is faked, advantage of the sorted-model 244 # Since the completion role is faked, advantage of the sorted-model
246 # optimizations in QCompleter can be taken. 245 # optimizations in QCompleter can be taken.
247 self.setCaseSensitivity(Qt.CaseSensitivity.CaseSensitive) 246 self.setCaseSensitivity(Qt.CaseSensitivity.CaseSensitive)
248 self.setModelSorting( 247 self.setModelSorting(QCompleter.ModelSorting.CaseSensitivelySortedModel)
249 QCompleter.ModelSorting.CaseSensitivelySortedModel) 248
250
251 self.__searchString = "" 249 self.__searchString = ""
252 self.__filterTimer = QTimer(self) 250 self.__filterTimer = QTimer(self)
253 self.__filterTimer.setSingleShot(True) 251 self.__filterTimer.setSingleShot(True)
254 self.__filterTimer.timeout.connect(self.__updateFilter) 252 self.__filterTimer.timeout.connect(self.__updateFilter)
255 253
256 def pathFromIndex(self, idx): 254 def pathFromIndex(self, idx):
257 """ 255 """
258 Public method to get a path for a given index. 256 Public method to get a path for a given index.
259 257
260 @param idx reference to the index (QModelIndex) 258 @param idx reference to the index (QModelIndex)
261 @return the actual URL from the history (string) 259 @return the actual URL from the history (string)
262 """ 260 """
263 return self.model().data(idx, HistoryModel.UrlStringRole) 261 return self.model().data(idx, HistoryModel.UrlStringRole)
264 262
265 def splitPath(self, path): 263 def splitPath(self, path):
266 """ 264 """
267 Public method to split the given path into strings, that are used to 265 Public method to split the given path into strings, that are used to
268 match at each level in the model. 266 match at each level in the model.
269 267
270 @param path path to be split (string) 268 @param path path to be split (string)
271 @return list of path elements (list of strings) 269 @return list of path elements (list of strings)
272 """ 270 """
273 if path == self.__searchString: 271 if path == self.__searchString:
274 return ["t"] 272 return ["t"]
275 273
276 # Queue an update to the search string. Wait a bit, so that if the user 274 # Queue an update to the search string. Wait a bit, so that if the user
277 # is quickly typing, the completer doesn't try to complete until they 275 # is quickly typing, the completer doesn't try to complete until they
278 # pause. 276 # pause.
279 if self.__filterTimer.isActive(): 277 if self.__filterTimer.isActive():
280 self.__filterTimer.stop() 278 self.__filterTimer.stop()
281 self.__filterTimer.start(150) 279 self.__filterTimer.start(150)
282 280
283 # If the previous search results are not a superset of the current 281 # If the previous search results are not a superset of the current
284 # search results, tell the model that it is not valid yet. 282 # search results, tell the model that it is not valid yet.
285 if not path.startswith(self.__searchString): 283 if not path.startswith(self.__searchString):
286 self.model().setValid(False) 284 self.model().setValid(False)
287 285
288 self.__searchString = path 286 self.__searchString = path
289 287
290 # The actual filtering is done by the HistoryCompletionModel. Just 288 # The actual filtering is done by the HistoryCompletionModel. Just
291 # return a short dummy here so that QCompleter thinks everything 289 # return a short dummy here so that QCompleter thinks everything
292 # matched. 290 # matched.
293 return ["t"] 291 return ["t"]
294 292
295 def __updateFilter(self): 293 def __updateFilter(self):
296 """ 294 """
297 Private slot to update the search string. 295 Private slot to update the search string.
298 """ 296 """
299 completionModel = self.model() 297 completionModel = self.model()
300 298
301 # Tell the HistoryCompletionModel about the new search string. 299 # Tell the HistoryCompletionModel about the new search string.
302 completionModel.setSearchString(self.__searchString) 300 completionModel.setSearchString(self.__searchString)
303 301
304 # Sort the model. 302 # Sort the model.
305 completionModel.sort(0) 303 completionModel.sort(0)
306 304
307 # Mark it valid. 305 # Mark it valid.
308 completionModel.setValid(True) 306 completionModel.setValid(True)
309 307
310 # Now update the QCompleter widget, but only if the user is still 308 # Now update the QCompleter widget, but only if the user is still
311 # typing a URL. 309 # typing a URL.
312 if self.widget() is not None and self.widget().hasFocus(): 310 if self.widget() is not None and self.widget().hasFocus():
313 self.complete() 311 self.complete()

eric ide

mercurial