5 |
5 |
6 """ |
6 """ |
7 Module implementing a special completer for the history. |
7 Module implementing a special completer for the history. |
8 """ |
8 """ |
9 |
9 |
10 |
10 import re |
11 from PyQt5.QtCore import Qt, QRegExp, QTimer, QSortFilterProxyModel |
11 |
|
12 from PyQt5.QtCore import Qt, QTimer, QSortFilterProxyModel |
12 from PyQt5.QtWidgets import QTableView, QAbstractItemView, QCompleter |
13 from PyQt5.QtWidgets import QTableView, QAbstractItemView, QCompleter |
13 |
14 |
14 from .HistoryModel import HistoryModel |
15 from .HistoryModel import HistoryModel |
15 from .HistoryFilterModel import HistoryFilterModel |
16 from .HistoryFilterModel import HistoryFilterModel |
16 |
17 |
74 @param parent reference to the parent object (QObject) |
75 @param parent reference to the parent object (QObject) |
75 """ |
76 """ |
76 super(HistoryCompletionModel, self).__init__(parent) |
77 super(HistoryCompletionModel, self).__init__(parent) |
77 |
78 |
78 self.__searchString = "" |
79 self.__searchString = "" |
79 self.__searchMatcher = QRegExp( |
80 self.__searchMatcher = None |
80 "", Qt.CaseInsensitive, QRegExp.FixedString) |
81 self.__wordMatcher = None |
81 self.__wordMatcher = QRegExp("", Qt.CaseInsensitive) |
|
82 self.__isValid = False |
82 self.__isValid = False |
83 |
83 |
84 self.setDynamicSortFilter(True) |
84 self.setDynamicSortFilter(True) |
85 |
85 |
86 def data(self, index, role=Qt.DisplayRole): |
86 def data(self, index, role=Qt.DisplayRole): |
113 |
113 |
114 @return current search string (string) |
114 @return current search string (string) |
115 """ |
115 """ |
116 return self.__searchString |
116 return self.__searchString |
117 |
117 |
118 def setSearchString(self, string): |
118 def setSearchString(self, sstring): |
119 """ |
119 """ |
120 Public method to set the current search string. |
120 Public method to set the current search string. |
121 |
121 |
122 @param string new search string (string) |
122 @param sstring new search string (string) |
123 """ |
123 """ |
124 if string == self.__searchString: |
124 if sstring != self.__searchString: |
125 return |
125 self.__searchString = sstring |
126 |
126 self.__searchMatcher = re.compile( |
127 self.__searchString = string |
127 re.escape(self.__searchString), re.IGNORECASE) |
128 self.__searchMatcher.setPattern(self.__searchString) |
128 self.__wordMatcher = re.compile( |
129 self.__wordMatcher.setPattern( |
129 r"\b" + re.escape(self.__searchString), re.IGNORECASE) |
130 "\\b" + QRegExp.escape(self.__searchString)) |
130 self.invalidateFilter() |
131 self.invalidateFilter() |
|
132 |
131 |
133 def isValid(self): |
132 def isValid(self): |
134 """ |
133 """ |
135 Public method to check the model for validity. |
134 Public method to check the model for validity. |
136 |
135 |
159 |
158 |
160 @param sourceRow row number in the source model (integer) |
159 @param sourceRow row number in the source model (integer) |
161 @param sourceParent index of the source item (QModelIndex) |
160 @param sourceParent index of the source item (QModelIndex) |
162 @return flag indicating acceptance (boolean) |
161 @return flag indicating acceptance (boolean) |
163 """ |
162 """ |
164 # Do a case-insensitive substring match against both the url and title. |
163 if self.__searchMatcher is not None: |
165 # It's already ensured, that the user doesn't accidentally use regexp |
164 # Do a case-insensitive substring match against both the url and |
166 # metacharacters (s. setSearchString()). |
165 # title. It's already ensured, that the user doesn't accidentally |
167 idx = self.sourceModel().index(sourceRow, 0, sourceParent) |
166 # use regexp metacharacters (s. setSearchString()). |
168 |
167 idx = self.sourceModel().index(sourceRow, 0, sourceParent) |
169 url = self.sourceModel().data(idx, HistoryModel.UrlStringRole) |
168 |
170 if self.__searchMatcher.indexIn(url) != -1: |
169 url = self.sourceModel().data(idx, HistoryModel.UrlStringRole) |
171 return True |
170 if self.__searchMatcher.search(url) is not None: |
172 |
171 return True |
173 title = self.sourceModel().data(idx, HistoryModel.TitleRole) |
172 |
174 if self.__searchMatcher.indexIn(title) != -1: |
173 title = self.sourceModel().data(idx, HistoryModel.TitleRole) |
175 return True |
174 if self.__searchMatcher.search(title) is not None: |
|
175 return True |
176 |
176 |
177 return False |
177 return False |
178 |
178 |
179 def lessThan(self, left, right): |
179 def lessThan(self, left, right): |
180 """ |
180 """ |
186 "slashdot.org". However, it only looks for the string in the host name, |
186 "slashdot.org". However, it only looks for the string in the host name, |
187 not the entire URL, since while it makes sense to e.g. give |
187 not the entire URL, since while it makes sense to e.g. give |
188 "www.phoronix.com" a bonus for "ph", it does NOT make sense to give |
188 "www.phoronix.com" a bonus for "ph", it does NOT make sense to give |
189 "www.yadda.com/foo.php" the bonus. |
189 "www.yadda.com/foo.php" the bonus. |
190 |
190 |
191 @param left index of left item (QModelIndex) |
191 @param left index of left item |
192 @param right index of right item (QModelIndex) |
192 @type QModelIndex |
193 @return true, if left is less than right (boolean) |
193 @param right index of right item |
|
194 @type QModelIndex |
|
195 @return true, if left is less than right |
|
196 @rtype bool |
194 """ |
197 """ |
195 frequency_L = self.sourceModel().data( |
198 frequency_L = self.sourceModel().data( |
196 left, HistoryFilterModel.FrequencyRole) |
199 left, HistoryFilterModel.FrequencyRole) |
197 url_L = self.sourceModel().data(left, HistoryModel.UrlRole).host() |
200 url_L = self.sourceModel().data(left, HistoryModel.UrlRole).host() |
198 title_L = self.sourceModel().data(left, HistoryModel.TitleRole) |
201 title_L = self.sourceModel().data(left, HistoryModel.TitleRole) |
199 |
202 |
200 if ( |
203 if ( |
201 self.__wordMatcher.indexIn(url_L) != -1 or |
204 self.__wordMatcher is not None and |
202 self.__wordMatcher.indexIn(title_L) != -1 |
205 (bool(self.__wordMatcher.search(url_L)) or |
|
206 bool(self.__wordMatcher.search(title_L))) |
203 ): |
207 ): |
204 frequency_L *= 2 |
208 frequency_L *= 2 |
205 |
209 |
206 frequency_R = self.sourceModel().data( |
210 frequency_R = self.sourceModel().data( |
207 right, HistoryFilterModel.FrequencyRole) |
211 right, HistoryFilterModel.FrequencyRole) |
208 url_R = self.sourceModel().data(right, HistoryModel.UrlRole).host() |
212 url_R = self.sourceModel().data(right, HistoryModel.UrlRole).host() |
209 title_R = self.sourceModel().data(right, HistoryModel.TitleRole) |
213 title_R = self.sourceModel().data(right, HistoryModel.TitleRole) |
210 |
214 |
211 if ( |
215 if ( |
212 self.__wordMatcher.indexIn(url_R) != -1 or |
216 self.__wordMatcher is not None and |
213 self.__wordMatcher.indexIn(title_R) != -1 |
217 (bool(self.__wordMatcher.search(url_R)) or |
|
218 bool(self.__wordMatcher.search(title_R))) |
214 ): |
219 ): |
215 frequency_R *= 2 |
220 frequency_R *= 2 |
216 |
221 |
217 # Sort results in descending frequency-derived score. |
222 # Sort results in descending frequency-derived score. |
218 return frequency_R < frequency_L |
223 return frequency_R < frequency_L |