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() |