Helpviewer/History/HistoryCompleter.py

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

eric ide

mercurial