WebBrowser/History/HistoryCompleter.py

branch
QtWebEngine
changeset 4734
ce0b1f024da9
parent 4631
5c1a96925da4
child 5389
9b1c800daff3
equal deleted inserted replaced
4733:ae291a307ea6 4734:ce0b1f024da9
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009 - 2016 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a special completer for the history.
8 """
9
10 from __future__ import unicode_literals
11
12 from PyQt5.QtCore import Qt, QRegExp, QTimer, QSortFilterProxyModel
13 from PyQt5.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(HistoryCompletionView, self).__init__(parent)
30
31 self.horizontalHeader().hide()
32 self.verticalHeader().hide()
33
34 self.setShowGrid(False)
35
36 self.setSelectionBehavior(QAbstractItemView.SelectRows)
37 self.setSelectionMode(QAbstractItemView.SingleSelection)
38 self.setTextElideMode(Qt.ElideRight)
39
40 metrics = self.fontMetrics()
41 self.verticalHeader().setDefaultSectionSize(metrics.height())
42
43 def resizeEvent(self, evt):
44 """
45 Protected method handling resize events.
46
47 @param evt reference to the resize event (QResizeEvent)
48 """
49 self.horizontalHeader().resizeSection(0, 0.65 * self.width())
50 self.horizontalHeader().setStretchLastSection(True)
51
52 super(HistoryCompletionView, self).resizeEvent(evt)
53
54 def sizeHintForRow(self, row):
55 """
56 Public method to give a size hint for rows.
57
58 @param row row number (integer)
59 @return desired row height (integer)
60 """
61 metrics = self.fontMetrics()
62 return metrics.height()
63
64
65 class HistoryCompletionModel(QSortFilterProxyModel):
66 """
67 Class implementing a special model for history based completions.
68 """
69 HistoryCompletionRole = HistoryFilterModel.MaxRole + 1
70
71 def __init__(self, parent=None):
72 """
73 Constructor
74
75 @param parent reference to the parent object (QObject)
76 """
77 super(HistoryCompletionModel, self).__init__(parent)
78
79 self.__searchString = ""
80 self.__searchMatcher = QRegExp(
81 "", Qt.CaseInsensitive, QRegExp.FixedString)
82 self.__wordMatcher = QRegExp("", Qt.CaseInsensitive)
83 self.__isValid = False
84
85 self.setDynamicSortFilter(True)
86
87 def data(self, index, role=Qt.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.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, string):
120 """
121 Public method to set the current search string.
122
123 @param string new search string (string)
124 """
125 if string == self.__searchString:
126 return
127
128 self.__searchString = string
129 self.__searchMatcher.setPattern(self.__searchString)
130 self.__wordMatcher.setPattern(
131 "\\b" + QRegExp.escape(self.__searchString))
132 self.invalidateFilter()
133
134 def isValid(self):
135 """
136 Public method to check the model for validity.
137
138 @return flag indicating a valid status (boolean)
139 """
140 return self.__isValid
141
142 def setValid(self, valid):
143 """
144 Public method to set the model's validity.
145
146 @param valid flag indicating the new valid status (boolean)
147 """
148 if valid == self.__isValid:
149 return
150
151 self.__isValid = valid
152
153 # tell the history completer that the model has changed
154 self.dataChanged.emit(self.index(0, 0), self.index(0,
155 self.rowCount() - 1))
156
157 def filterAcceptsRow(self, sourceRow, sourceParent):
158 """
159 Public method to determine, if the row is acceptable.
160
161 @param sourceRow row number in the source model (integer)
162 @param sourceParent index of the source item (QModelIndex)
163 @return flag indicating acceptance (boolean)
164 """
165 # Do a case-insensitive substring match against both the url and title.
166 # It's already ensured, that the user doesn't accidentally use regexp
167 # metacharacters (s. setSearchString()).
168 idx = self.sourceModel().index(sourceRow, 0, sourceParent)
169
170 url = self.sourceModel().data(idx, HistoryModel.UrlStringRole)
171 if self.__searchMatcher.indexIn(url) != -1:
172 return True
173
174 title = self.sourceModel().data(idx, HistoryModel.TitleRole)
175 if self.__searchMatcher.indexIn(title) != -1:
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 (QModelIndex)
193 @param right index of right item (QModelIndex)
194 @return true, if left is less than right (boolean)
195 """
196 frequency_L = \
197 self.sourceModel().data(left, HistoryFilterModel.FrequencyRole)
198 url_L = self.sourceModel().data(left, HistoryModel.UrlRole).host()
199 title_L = self.sourceModel().data(left, HistoryModel.TitleRole)
200
201 if self.__wordMatcher.indexIn(url_L) != -1 or \
202 self.__wordMatcher.indexIn(title_L) != -1:
203 frequency_L *= 2
204
205 frequency_R = \
206 self.sourceModel().data(right, HistoryFilterModel.FrequencyRole)
207 url_R = self.sourceModel().data(right, HistoryModel.UrlRole).host()
208 title_R = self.sourceModel().data(right, HistoryModel.TitleRole)
209
210 if self.__wordMatcher.indexIn(url_R) != -1 or \
211 self.__wordMatcher.indexIn(title_R) != -1:
212 frequency_R *= 2
213
214 # Sort results in descending frequency-derived score.
215 return frequency_R < frequency_L
216
217
218 class HistoryCompleter(QCompleter):
219 """
220 Class implementing a completer for the browser history.
221 """
222 def __init__(self, model, parent=None):
223 """
224 Constructor
225
226 @param model reference to the model (QAbstractItemModel)
227 @param parent reference to the parent object (QObject)
228 """
229 super(HistoryCompleter, self).__init__(model, parent)
230
231 self.setPopup(HistoryCompletionView())
232
233 # Completion should be against the faked role.
234 self.setCompletionRole(HistoryCompletionModel.HistoryCompletionRole)
235
236 # Since the completion role is faked, advantage of the sorted-model
237 # optimizations in QCompleter can be taken.
238 self.setCaseSensitivity(Qt.CaseSensitive)
239 self.setModelSorting(QCompleter.CaseSensitivelySortedModel)
240
241 self.__searchString = ""
242 self.__filterTimer = QTimer(self)
243 self.__filterTimer.setSingleShot(True)
244 self.__filterTimer.timeout.connect(self.__updateFilter)
245
246 def pathFromIndex(self, idx):
247 """
248 Public method to get a path for a given index.
249
250 @param idx reference to the index (QModelIndex)
251 @return the actual URL from the history (string)
252 """
253 return self.model().data(idx, HistoryModel.UrlStringRole)
254
255 def splitPath(self, path):
256 """
257 Public method to split the given path into strings, that are used to
258 match at each level in the model.
259
260 @param path path to be split (string)
261 @return list of path elements (list of strings)
262 """
263 if path == self.__searchString:
264 return ["t"]
265
266 # Queue an update to the search string. Wait a bit, so that if the user
267 # is quickly typing, the completer doesn't try to complete until they
268 # pause.
269 if self.__filterTimer.isActive():
270 self.__filterTimer.stop()
271 self.__filterTimer.start(150)
272
273 # If the previous search results are not a superset of the current
274 # search results, tell the model that it is not valid yet.
275 if not path.startswith(self.__searchString):
276 self.model().setValid(False)
277
278 self.__searchString = path
279
280 # The actual filtering is done by the HistoryCompletionModel. Just
281 # return a short dummy here so that QCompleter thinks everything
282 # matched.
283 return ["t"]
284
285 def __updateFilter(self):
286 """
287 Private slot to update the search string.
288 """
289 completionModel = self.model()
290
291 # Tell the HistoryCompletionModel about the new search string.
292 completionModel.setSearchString(self.__searchString)
293
294 # Sort the model.
295 completionModel.sort(0)
296
297 # Mark it valid.
298 completionModel.setValid(True)
299
300 # Now update the QCompleter widget, but only if the user is still
301 # typing a URL.
302 if self.widget() is not None and self.widget().hasFocus():
303 self.complete()

eric ide

mercurial