src/eric7/WebBrowser/History/HistoryCompleter.py

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

eric ide

mercurial