Merge lp:~uriboni/webbrowser-app/bookmarks-in-suggestions into lp:webbrowser-app
- bookmarks-in-suggestions
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Olivier Tilloy | ||||
Approved revision: | 995 | ||||
Merged at revision: | 973 | ||||
Proposed branch: | lp:~uriboni/webbrowser-app/bookmarks-in-suggestions | ||||
Merge into: | lp:webbrowser-app | ||||
Diff against target: |
1185 lines (+555/-335) 14 files modified
src/app/webbrowser/Browser.qml (+29/-5) src/app/webbrowser/CMakeLists.txt (+1/-1) src/app/webbrowser/Suggestion.qml (+81/-0) src/app/webbrowser/Suggestions.qml (+57/-85) src/app/webbrowser/history-matches-model.cpp (+0/-92) src/app/webbrowser/history-matches-model.h (+0/-62) src/app/webbrowser/suggestions-filter-model.cpp (+135/-0) src/app/webbrowser/suggestions-filter-model.h (+72/-0) src/app/webbrowser/webbrowser-app.cpp (+2/-2) tests/autopilot/webbrowser_app/emulators/browser.py (+3/-5) tests/autopilot/webbrowser_app/tests/test_suggestions.py (+93/-28) tests/unittests/CMakeLists.txt (+1/-1) tests/unittests/suggestions-filter-model/CMakeLists.txt (+2/-2) tests/unittests/suggestions-filter-model/tst_SuggestionsFilterModelTests.cpp (+79/-52) |
||||
To merge this branch: | bzr merge lp:~uriboni/webbrowser-app/bookmarks-in-suggestions | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Olivier Tilloy | Approve | ||
PS Jenkins bot | continuous-integration | Approve | |
Review via email: mp+256459@code.launchpad.net |
Commit message
Include bookmark results in the suggestions list
Description of the change
Include bookmark results in the suggestions list.
Some notes:
- the design document specifies 8 max total suggestions, of which essentially 2 history, 2 bookmarks and 4 search results. since we don't do search results yet for now i set the limits to 4 history and 4 bookmarks.
- we had discussed not using LimitProxyModel, however it ends up being actually useful for this use case and working well. the alternative would have been to hand pick the items from the results to build a new list, but it proved very non-declarative and clunky. we might remove it in a further iteration by creating a c++ model that merges and limits the various results from suggestion sources (which would also solve the Suggestions.count property being slightly hackish), but I would leave that for later when we get the search engine suggestions as well.
Ugo Riboni (uriboni) wrote : | # |
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:981
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
I haven’t reviewed the code yet, but I’m seeing one functional issue here (it looks good and works well otherwise): testing on desktop, if I decrease the height of the window, then start typing in the address bar so that I have more results than fit of the screen, I can’t scroll them into view (I can see that they are there by dragging the list view, but when releasing they spring back out of sight).
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:982
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
In src/app/
- updateSearchRoles is not reimplemented from QSortFilterProx
In src/app/
- In SuggestionsFilt
- In SuggestionsFilt
In tests/autopilot
- In get_ordered_
In tests/unittests
- In shouldMatch, there’s a leftover qDebug()
In src/app/
- Can’t you simply make models a var? If it was a javascript array instead of being a QML list, you could use the Array.reduce() function to compute the total count, thus inlining the function (and potentially making it faster to run)
In src/app/
- The call to highlightTerms() breaks encapsulation. The definition of the function should probably be moved to this file.
Ugo Riboni (uriboni) wrote : | # |
Everything else not included in this reply has been fixed as requested
> - In SuggestionsFilt
> entries for which not all terms match in at least one of the roles, but I
> think it should be more tolerant, and match an entry if all the terms are
> found in at least one of the roles (i.e. for two search terms, if the first
> one matches only the first role and the second one matches only the second
> role, the entry should be included)
Ok, the previous implementation was restrictive so I did not change it. Fixed as you request now.
> In src/app/
>
> - Can’t you simply make models a var? If it was a javascript array instead of
> being a QML list, you could use the Array.reduce() function to compute the
> total count, thus inlining the function (and potentially making it faster to
> run)
No, if I make the model a var, and keep the same assignment code in Browser.qml, I get this: "Cannot assign multiple values to a singular property" when trying to assign a list to it.
To use a var, and thus a JS array as the list of models, with all the functional programming goodies it comes with, you have to use a slightly more cumbersome syntax on the assignment, but I think it is worth it.
Judge by yourself with the change I proposed.
> In src/app/
>
> - The call to highlightTerms() breaks encapsulation. The definition of the
> function should probably be moved to this file.
Actually it was a mistake to call it from inside the Suggestion item itself, as it was already called in Suggestions.qml
Olivier Tilloy (osomon) wrote : | # |
615 + if (foundTerms.count() == m_terms.count()) {
I can see one possible corner case where this will fail: if m_terms contains doublons (e.g. if for whatever reason I typed in the address bar "ubuntu ubuntu"). This is arguably not a very important use case, but I think it’s easy enough to fix regardless, by comparing to QSet<QString>
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:984
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
A suggestion (pun intended):
In tests/autopilot
Care to remove it, and rename get_ordered_
Olivier Tilloy (osomon) wrote : | # |
I ran a coverage report on this branch, and there’s a couple of things that are not covered by the unit tests (but it’s easy to fix):
- in SuggestionsFilt
- SuggestionsFilt
With the above fixed, coverage for SuggestionsFilt
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:990
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
None: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:993
http://
Executed test runs:
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Olivier Tilloy (osomon) wrote : | # |
Looks good to me now, thanks!
Olivier Tilloy (osomon) wrote : | # |
When merging the latest trunk (lp:webbrowser-app) into this branch, it fails to build because includes were re-ordered in webbrowser-app.cpp, and when merging history-
Preview Diff
1 | === modified file 'src/app/webbrowser/Browser.qml' |
2 | --- src/app/webbrowser/Browser.qml 2015-04-08 20:44:21 +0000 |
3 | +++ src/app/webbrowser/Browser.qml 2015-04-22 10:06:25 +0000 |
4 | @@ -257,6 +257,7 @@ |
5 | } |
6 | |
7 | Suggestions { |
8 | + id: suggestionsList |
9 | opacity: ((chrome.state == "shown") && chrome.activeFocus && (count > 0) && !chrome.drawerOpen) ? 1.0 : 0.0 |
10 | Behavior on opacity { |
11 | UbuntuNumberAnimation {} |
12 | @@ -267,11 +268,34 @@ |
13 | horizontalCenter: parent.horizontalCenter |
14 | } |
15 | width: chrome.width - units.gu(5) |
16 | - height: enabled ? Math.min(contentHeight, tabContainer.height - units.gu(2)) : 0 |
17 | - model: HistoryMatchesModel { |
18 | - sourceModel: browser.historyModel |
19 | - query: chrome.text |
20 | - } |
21 | + height: enabled ? Math.min(contentHeight, tabContainer.height - chrome.height - units.gu(2)) : 0 |
22 | + |
23 | + searchTerms: chrome.text.split(/\s+/g).filter(function(term) { return term.length > 0 }) |
24 | + |
25 | + models: [historySuggestions, bookmarksSuggestions] |
26 | + |
27 | + LimitProxyModel { |
28 | + id: historySuggestions |
29 | + limit: 4 |
30 | + property string icon: "history" |
31 | + sourceModel: SuggestionsFilterModel { |
32 | + sourceModel: browser.historyModel |
33 | + terms: suggestionsList.searchTerms |
34 | + searchFields: ["url", "title"] |
35 | + } |
36 | + } |
37 | + |
38 | + LimitProxyModel { |
39 | + id: bookmarksSuggestions |
40 | + limit: 4 |
41 | + property string icon: "non-starred" |
42 | + sourceModel: SuggestionsFilterModel { |
43 | + sourceModel: browser.bookmarksModel |
44 | + terms: suggestionsList.searchTerms |
45 | + searchFields: ["url", "title"] |
46 | + } |
47 | + } |
48 | + |
49 | onSelected: { |
50 | browser.currentWebview.url = url |
51 | browser.currentWebview.forceActiveFocus() |
52 | |
53 | === modified file 'src/app/webbrowser/CMakeLists.txt' |
54 | --- src/app/webbrowser/CMakeLists.txt 2015-04-09 13:59:02 +0000 |
55 | +++ src/app/webbrowser/CMakeLists.txt 2015-04-22 10:06:25 +0000 |
56 | @@ -11,10 +11,10 @@ |
57 | history-domain-model.cpp |
58 | history-domainlist-chronological-model.cpp |
59 | history-domainlist-model.cpp |
60 | - history-matches-model.cpp |
61 | history-model.cpp |
62 | history-timeframe-model.cpp |
63 | limit-proxy-model.cpp |
64 | + suggestions-filter-model.cpp |
65 | tabs-model.cpp |
66 | top-sites-model.cpp |
67 | ) |
68 | |
69 | === added file 'src/app/webbrowser/Suggestion.qml' |
70 | --- src/app/webbrowser/Suggestion.qml 1970-01-01 00:00:00 +0000 |
71 | +++ src/app/webbrowser/Suggestion.qml 2015-04-22 10:06:25 +0000 |
72 | @@ -0,0 +1,81 @@ |
73 | +/* |
74 | + * Copyright 2015 Canonical Ltd. |
75 | + * |
76 | + * This file is part of webbrowser-app. |
77 | + * |
78 | + * webbrowser-app is free software; you can redistribute it and/or modify |
79 | + * it under the terms of the GNU General Public License as published by |
80 | + * the Free Software Foundation; version 3. |
81 | + * |
82 | + * webbrowser-app is distributed in the hope that it will be useful, |
83 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
84 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
85 | + * GNU General Public License for more details. |
86 | + * |
87 | + * You should have received a copy of the GNU General Public License |
88 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
89 | + */ |
90 | + |
91 | +import QtQuick 2.0 |
92 | +import Ubuntu.Components 1.1 |
93 | +import Ubuntu.Components.ListItems 1.0 as ListItem |
94 | + |
95 | +// Not using ListItem.Subtitled because it’s not themable, |
96 | +// and we want the subText to be on one line only. |
97 | +ListItem.Base { |
98 | + property alias title: label.text |
99 | + property alias subtitle: subLabel.text |
100 | + property alias icon: icon.name |
101 | + property url url |
102 | + |
103 | + signal selected(url url) |
104 | + |
105 | + __height: Math.max(middleVisuals.height, units.gu(6)) |
106 | + // disable focus handling |
107 | + activeFocusOnPress: false |
108 | + |
109 | + Item { |
110 | + id: middleVisuals |
111 | + anchors { |
112 | + left: parent.left |
113 | + right: parent.right |
114 | + verticalCenter: parent.verticalCenter |
115 | + } |
116 | + height: label.height + subLabel.height |
117 | + |
118 | + Icon { |
119 | + id: icon |
120 | + anchors { |
121 | + verticalCenter: parent.verticalCenter |
122 | + left: parent.left |
123 | + } |
124 | + width: units.gu(2) |
125 | + height: units.gu(2) |
126 | + } |
127 | + |
128 | + Label { |
129 | + id: label |
130 | + anchors { |
131 | + top: parent.top |
132 | + left: icon.right |
133 | + leftMargin: units.gu(2) |
134 | + right: parent.right |
135 | + } |
136 | + elide: Text.ElideRight |
137 | + } |
138 | + |
139 | + Label { |
140 | + id: subLabel |
141 | + anchors { |
142 | + top: label.bottom |
143 | + left: icon.right |
144 | + leftMargin: units.gu(2) |
145 | + right: parent.right |
146 | + } |
147 | + fontSize: "small" |
148 | + elide: Text.ElideRight |
149 | + } |
150 | + } |
151 | + |
152 | + onClicked: selected(url) |
153 | +} |
154 | |
155 | === modified file 'src/app/webbrowser/Suggestions.qml' |
156 | --- src/app/webbrowser/Suggestions.qml 2014-08-26 08:53:28 +0000 |
157 | +++ src/app/webbrowser/Suggestions.qml 2015-04-22 10:06:25 +0000 |
158 | @@ -18,14 +18,14 @@ |
159 | |
160 | import QtQuick 2.0 |
161 | import Ubuntu.Components 1.1 |
162 | -import Ubuntu.Components.ListItems 1.0 as ListItem |
163 | |
164 | Rectangle { |
165 | id: suggestions |
166 | |
167 | - property alias model: listview.model |
168 | - property alias count: listview.count |
169 | - property alias contentHeight: listview.contentHeight |
170 | + property var searchTerms |
171 | + property var models |
172 | + property int count: models.reduce(countItems, 0) |
173 | + property alias contentHeight: suggestionsList.contentHeight |
174 | |
175 | signal selected(url url) |
176 | |
177 | @@ -38,92 +38,64 @@ |
178 | clip: true |
179 | |
180 | ListView { |
181 | - id: listview |
182 | - |
183 | - anchors { |
184 | - top: parent.top |
185 | - left: parent.left |
186 | - right: parent.right |
187 | - } |
188 | - height: parent.height |
189 | - |
190 | - delegate: ListItem.Base { |
191 | - // Not using ListItem.Subtitled because it’s not themable, |
192 | - // and we want the subText to be on one line only. |
193 | - |
194 | - property alias text: label.text |
195 | - property alias subText: subLabel.text |
196 | - |
197 | - showDivider: index < (listview.count - 1) |
198 | - |
199 | - __height: Math.max(middleVisuals.height, units.gu(6)) |
200 | - // disable focus handling |
201 | - activeFocusOnPress: false |
202 | - |
203 | - Item { |
204 | - id: middleVisuals |
205 | - anchors { |
206 | - left: parent.left |
207 | - right: parent.right |
208 | - verticalCenter: parent.verticalCenter |
209 | - } |
210 | - height: childrenRect.height + label.anchors.topMargin + subLabel.anchors.bottomMargin |
211 | - |
212 | - Label { |
213 | - id: label |
214 | - anchors { |
215 | - top: parent.top |
216 | - left: parent.left |
217 | - right: parent.right |
218 | - } |
219 | - elide: Text.ElideRight |
220 | - text: highlightTerms(title, suggestions.model.terms) |
221 | - } |
222 | - |
223 | - Label { |
224 | - id: subLabel |
225 | - anchors { |
226 | - left: parent.left |
227 | - right: parent.right |
228 | - top: label.bottom |
229 | - } |
230 | - fontSize: "small" |
231 | - elide: Text.ElideRight |
232 | - text: highlightTerms(url, suggestions.model.terms) |
233 | - } |
234 | - } |
235 | - |
236 | - onClicked: suggestions.selected(url) |
237 | - |
238 | - function escapeTerm(term) { |
239 | - // Build a regular expression suitable for highlighting a term |
240 | - // in a case-insensitive manner and globally, by escaping |
241 | - // special characters (a simpler version of preg_quote). |
242 | - var escaped = term.replace(/[().?]/g, '\\$&') |
243 | - return new RegExp(escaped, 'ig') |
244 | - } |
245 | - |
246 | - function highlightTerms(text, terms) { |
247 | - // Highlight the matching terms (bold and Ubuntu orange) |
248 | - if (text === undefined) { |
249 | - return '' |
250 | - } |
251 | - var highlighted = text.toString() |
252 | - var count = terms.length |
253 | - var highlight = '<b><font color="%1">$&</font></b>'.arg(UbuntuColors.orange) |
254 | - for (var i = 0; i < count; ++i) { |
255 | - var term = terms[i] |
256 | - highlighted = highlighted.replace(escapeTerm(term), highlight) |
257 | - } |
258 | - highlighted = highlighted.replace(new RegExp('&', 'g'), '&') |
259 | - highlighted = '<html>' + highlighted + '</html>' |
260 | - return highlighted |
261 | + id: suggestionsList |
262 | + anchors.fill: parent |
263 | + |
264 | + model: suggestions.models |
265 | + delegate: Column { |
266 | + width: suggestionsList.width |
267 | + height: childrenRect.height |
268 | + |
269 | + Repeater { |
270 | + id: suggestionsSource |
271 | + model: modelData |
272 | + property int firstItemIndex: models.slice(0, index).reduce(countItems, 0) |
273 | + |
274 | + delegate: Suggestion { |
275 | + width: suggestionsList.width |
276 | + showDivider: suggestionsSource.firstItemIndex + index < |
277 | + suggestions.count - 1 |
278 | + |
279 | + title: highlightTerms(model.title) |
280 | + subtitle: highlightTerms(model.url) |
281 | + url: model.url |
282 | + icon: suggestionsSource.model.icon |
283 | + |
284 | + onSelected: suggestions.selected(url) |
285 | + } |
286 | } |
287 | } |
288 | } |
289 | |
290 | Scrollbar { |
291 | - flickableItem: listview |
292 | + flickableItem: suggestionsList |
293 | align: Qt.AlignTrailing |
294 | } |
295 | + |
296 | + function escapeTerm(term) { |
297 | + // Build a regular expression suitable for highlighting a term |
298 | + // in a case-insensitive manner and globally, by escaping |
299 | + // special characters (a simpler version of preg_quote). |
300 | + var escaped = term.replace(/[().?]/g, '\\$&') |
301 | + return new RegExp(escaped, 'ig') |
302 | + } |
303 | + |
304 | + function highlightTerms(text) { |
305 | + // Highlight the matching terms (bold and Ubuntu orange) |
306 | + if (text === undefined) { |
307 | + return '' |
308 | + } |
309 | + var highlighted = text.toString() |
310 | + var count = searchTerms.length |
311 | + var highlight = '<b><font color="%1">$&</font></b>'.arg(UbuntuColors.orange) |
312 | + for (var i = 0; i < count; ++i) { |
313 | + var term = searchTerms[i] |
314 | + highlighted = highlighted.replace(escapeTerm(term), highlight) |
315 | + } |
316 | + highlighted = highlighted.replace(new RegExp('&', 'g'), '&') |
317 | + highlighted = '<html>' + highlighted + '</html>' |
318 | + return highlighted |
319 | + } |
320 | + |
321 | + function countItems(total, model) { return total + model.count } |
322 | } |
323 | |
324 | === removed file 'src/app/webbrowser/history-matches-model.cpp' |
325 | --- src/app/webbrowser/history-matches-model.cpp 2014-03-12 10:59:54 +0000 |
326 | +++ src/app/webbrowser/history-matches-model.cpp 1970-01-01 00:00:00 +0000 |
327 | @@ -1,92 +0,0 @@ |
328 | -/* |
329 | - * Copyright 2013 Canonical Ltd. |
330 | - * |
331 | - * This file is part of webbrowser-app. |
332 | - * |
333 | - * webbrowser-app is free software; you can redistribute it and/or modify |
334 | - * it under the terms of the GNU General Public License as published by |
335 | - * the Free Software Foundation; version 3. |
336 | - * |
337 | - * webbrowser-app is distributed in the hope that it will be useful, |
338 | - * but WITHOUT ANY WARRANTY; without even the implied warranty of |
339 | - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
340 | - * GNU General Public License for more details. |
341 | - * |
342 | - * You should have received a copy of the GNU General Public License |
343 | - * along with this program. If not, see <http://www.gnu.org/licenses/>. |
344 | - */ |
345 | - |
346 | -#include "history-matches-model.h" |
347 | -#include "history-model.h" |
348 | - |
349 | -// Qt |
350 | -#include <QtCore/QRegExp> |
351 | - |
352 | -/*! |
353 | - \class HistoryMatchesModel |
354 | - \brief Proxy model that filters the contents of the history model |
355 | - based on a query string |
356 | - |
357 | - HistoryMatchesModel is a proxy model that filters the contents of a |
358 | - HistoryModel based on a query string. |
359 | - |
360 | - The query string may contain several terms (or words). |
361 | - |
362 | - An entry in the history model matches if all the terms are contained in |
363 | - either its URL or its title (inclusive OR). |
364 | -*/ |
365 | -HistoryMatchesModel::HistoryMatchesModel(QObject* parent) |
366 | - : QSortFilterProxyModel(parent) |
367 | -{ |
368 | -} |
369 | - |
370 | -HistoryModel* HistoryMatchesModel::sourceModel() const |
371 | -{ |
372 | - return qobject_cast<HistoryModel*>(QSortFilterProxyModel::sourceModel()); |
373 | -} |
374 | - |
375 | -void HistoryMatchesModel::setSourceModel(HistoryModel* sourceModel) |
376 | -{ |
377 | - if (sourceModel != this->sourceModel()) { |
378 | - QSortFilterProxyModel::setSourceModel(sourceModel); |
379 | - Q_EMIT sourceModelChanged(); |
380 | - } |
381 | -} |
382 | - |
383 | -const QString& HistoryMatchesModel::query() const |
384 | -{ |
385 | - return m_query; |
386 | -} |
387 | - |
388 | -void HistoryMatchesModel::setQuery(const QString& query) |
389 | -{ |
390 | - if (query != m_query) { |
391 | - m_query = query; |
392 | - m_terms = query.split(QRegExp("\\s+"), QString::SkipEmptyParts); |
393 | - invalidateFilter(); |
394 | - Q_EMIT queryChanged(); |
395 | - Q_EMIT termsChanged(); |
396 | - } |
397 | -} |
398 | - |
399 | -const QStringList& HistoryMatchesModel::terms() const |
400 | -{ |
401 | - return m_terms; |
402 | -} |
403 | - |
404 | -bool HistoryMatchesModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const |
405 | -{ |
406 | - if (m_terms.isEmpty()) { |
407 | - return false; |
408 | - } |
409 | - QModelIndex index = sourceModel()->index(source_row, 0, source_parent); |
410 | - QString url = sourceModel()->data(index, HistoryModel::Url).toUrl().toString(); |
411 | - QString title = sourceModel()->data(index, HistoryModel::Title).toString(); |
412 | - Q_FOREACH (const QString& term, m_terms) { |
413 | - if (!url.contains(term, Qt::CaseInsensitive) && |
414 | - !title.contains(term, Qt::CaseInsensitive)) { |
415 | - return false; |
416 | - } |
417 | - } |
418 | - return true; |
419 | -} |
420 | |
421 | === removed file 'src/app/webbrowser/history-matches-model.h' |
422 | --- src/app/webbrowser/history-matches-model.h 2014-03-12 10:59:54 +0000 |
423 | +++ src/app/webbrowser/history-matches-model.h 1970-01-01 00:00:00 +0000 |
424 | @@ -1,62 +0,0 @@ |
425 | -/* |
426 | - * Copyright 2013 Canonical Ltd. |
427 | - * |
428 | - * This file is part of webbrowser-app. |
429 | - * |
430 | - * webbrowser-app is free software; you can redistribute it and/or modify |
431 | - * it under the terms of the GNU General Public License as published by |
432 | - * the Free Software Foundation; version 3. |
433 | - * |
434 | - * webbrowser-app is distributed in the hope that it will be useful, |
435 | - * but WITHOUT ANY WARRANTY; without even the implied warranty of |
436 | - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
437 | - * GNU General Public License for more details. |
438 | - * |
439 | - * You should have received a copy of the GNU General Public License |
440 | - * along with this program. If not, see <http://www.gnu.org/licenses/>. |
441 | - */ |
442 | - |
443 | -#ifndef __HISTORY_MATCHES_MODEL_H__ |
444 | -#define __HISTORY_MATCHES_MODEL_H__ |
445 | - |
446 | -// Qt |
447 | -#include <QtCore/QSortFilterProxyModel> |
448 | -#include <QtCore/QString> |
449 | -#include <QtCore/QStringList> |
450 | - |
451 | -class HistoryModel; |
452 | - |
453 | -class HistoryMatchesModel : public QSortFilterProxyModel |
454 | -{ |
455 | - Q_OBJECT |
456 | - |
457 | - Q_PROPERTY(HistoryModel* sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged) |
458 | - Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged) |
459 | - Q_PROPERTY(QStringList terms READ terms NOTIFY termsChanged) |
460 | - |
461 | -public: |
462 | - HistoryMatchesModel(QObject* parent=0); |
463 | - |
464 | - HistoryModel* sourceModel() const; |
465 | - void setSourceModel(HistoryModel* sourceModel); |
466 | - |
467 | - const QString& query() const; |
468 | - void setQuery(const QString& query); |
469 | - |
470 | - const QStringList& terms() const; |
471 | - |
472 | -Q_SIGNALS: |
473 | - void sourceModelChanged() const; |
474 | - void queryChanged() const; |
475 | - void termsChanged() const; |
476 | - |
477 | -protected: |
478 | - // reimplemented from QSortFilterProxyModel |
479 | - bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; |
480 | - |
481 | -private: |
482 | - QString m_query; |
483 | - QStringList m_terms; |
484 | -}; |
485 | - |
486 | -#endif // __HISTORY_MATCHES_MODEL_H__ |
487 | |
488 | === added file 'src/app/webbrowser/suggestions-filter-model.cpp' |
489 | --- src/app/webbrowser/suggestions-filter-model.cpp 1970-01-01 00:00:00 +0000 |
490 | +++ src/app/webbrowser/suggestions-filter-model.cpp 2015-04-22 10:06:25 +0000 |
491 | @@ -0,0 +1,135 @@ |
492 | +/* |
493 | + * Copyright 2015 Canonical Ltd. |
494 | + * |
495 | + * This file is part of webbrowser-app. |
496 | + * |
497 | + * webbrowser-app is free software; you can redistribute it and/or modify |
498 | + * it under the terms of the GNU General Public License as published by |
499 | + * the Free Software Foundation; version 3. |
500 | + * |
501 | + * webbrowser-app is distributed in the hope that it will be useful, |
502 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
503 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
504 | + * GNU General Public License for more details. |
505 | + * |
506 | + * You should have received a copy of the GNU General Public License |
507 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
508 | + */ |
509 | + |
510 | +#include "suggestions-filter-model.h" |
511 | + |
512 | +#include <QtCore/QDebug> |
513 | +#include <QtCore/QSet> |
514 | + |
515 | +/*! |
516 | + \class SuggestionsFilterModel |
517 | + \brief Proxy model that filters the contents of a model based on a list of |
518 | + keywords applied to multiple fields. |
519 | + |
520 | + SuggestionsFilterModel is a proxy model that filters the contents of a |
521 | + model based on a list of terms string that is applied to multiple user |
522 | + defined fields (matching role names in the source model). |
523 | + |
524 | + An item in the source model is returned by this model if all the search |
525 | + terms are contained across all of the item's fields (i.e. given two search |
526 | + terms, if one is found in a field and the other in a different field, then |
527 | + the item will be returned) |
528 | +*/ |
529 | +SuggestionsFilterModel::SuggestionsFilterModel(QObject* parent) |
530 | + : QSortFilterProxyModel(parent) |
531 | +{ |
532 | +} |
533 | + |
534 | +QVariant SuggestionsFilterModel::sourceModel() const |
535 | +{ |
536 | + QAbstractItemModel* source = QSortFilterProxyModel::sourceModel(); |
537 | + return (source) ? QVariant::fromValue(source) : QVariant(); |
538 | +} |
539 | + |
540 | +void SuggestionsFilterModel::setSourceModel(QVariant sourceModel) |
541 | +{ |
542 | + QAbstractItemModel* currentSource = QSortFilterProxyModel::sourceModel(); |
543 | + QAbstractItemModel* newSource = qvariant_cast<QAbstractItemModel*>(sourceModel); |
544 | + if (newSource != currentSource) { |
545 | + updateSearchRoles(newSource); |
546 | + QSortFilterProxyModel::setSourceModel(newSource); |
547 | + Q_EMIT sourceModelChanged(); |
548 | + Q_EMIT countChanged(); |
549 | + } |
550 | +} |
551 | + |
552 | +void SuggestionsFilterModel::setTerms(const QStringList& terms) |
553 | +{ |
554 | + if (terms != m_terms) { |
555 | + m_terms = terms; |
556 | + invalidateFilter(); |
557 | + Q_EMIT termsChanged(); |
558 | + Q_EMIT countChanged(); |
559 | + } |
560 | +} |
561 | + |
562 | +const QStringList& SuggestionsFilterModel::terms() const |
563 | +{ |
564 | + return m_terms; |
565 | +} |
566 | + |
567 | +void SuggestionsFilterModel::setSearchFields(const QStringList& searchFields) |
568 | +{ |
569 | + if (searchFields != m_searchFields) { |
570 | + m_searchFields = searchFields; |
571 | + updateSearchRoles(QSortFilterProxyModel::sourceModel()); |
572 | + invalidateFilter(); |
573 | + Q_EMIT searchFieldsChanged(); |
574 | + Q_EMIT countChanged(); |
575 | + } |
576 | +} |
577 | + |
578 | +const QStringList& SuggestionsFilterModel::searchFields() const |
579 | +{ |
580 | + return m_searchFields; |
581 | +} |
582 | + |
583 | +void SuggestionsFilterModel::updateSearchRoles(const QAbstractItemModel* model) { |
584 | + m_searchRoles.clear(); |
585 | + if (model) { |
586 | + Q_FOREACH(const QString& field, m_searchFields) { |
587 | + int role = model->roleNames().key(field.toUtf8(), -1); |
588 | + if (role != -1) { |
589 | + m_searchRoles.append(role); |
590 | + } else { |
591 | + qWarning() << "Source model does not have role matching field:" << field; |
592 | + } |
593 | + } |
594 | + } |
595 | +} |
596 | + |
597 | +bool SuggestionsFilterModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const |
598 | +{ |
599 | + if (m_terms.isEmpty() || m_searchFields.isEmpty()) { |
600 | + return false; |
601 | + } |
602 | + |
603 | + QAbstractItemModel* source = QSortFilterProxyModel::sourceModel(); |
604 | + QModelIndex index = source->index(source_row, 0, source_parent); |
605 | + |
606 | + QSet<QString> foundTerms; |
607 | + int searchTermsCount = QSet<QString>::fromList(m_terms).count(); |
608 | + |
609 | + Q_FOREACH(int role, m_searchRoles) { |
610 | + QString value = source->data(index, role).toString(); |
611 | + Q_FOREACH (const QString& term, m_terms) { |
612 | + if (value.contains(term, Qt::CaseInsensitive)) { |
613 | + foundTerms.insert(term); |
614 | + } |
615 | + if (foundTerms.count() == searchTermsCount) { |
616 | + return true; |
617 | + } |
618 | + } |
619 | + } |
620 | + return false; |
621 | +} |
622 | + |
623 | +int SuggestionsFilterModel::count() const |
624 | +{ |
625 | + return rowCount(); |
626 | +} |
627 | |
628 | === added file 'src/app/webbrowser/suggestions-filter-model.h' |
629 | --- src/app/webbrowser/suggestions-filter-model.h 1970-01-01 00:00:00 +0000 |
630 | +++ src/app/webbrowser/suggestions-filter-model.h 2015-04-22 10:06:25 +0000 |
631 | @@ -0,0 +1,72 @@ |
632 | +/* |
633 | + * Copyright 2015 Canonical Ltd. |
634 | + * |
635 | + * This file is part of webbrowser-app. |
636 | + * |
637 | + * webbrowser-app is free software; you can redistribute it and/or modify |
638 | + * it under the terms of the GNU General Public License as published by |
639 | + * the Free Software Foundation; version 3. |
640 | + * |
641 | + * webbrowser-app is distributed in the hope that it will be useful, |
642 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
643 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
644 | + * GNU General Public License for more details. |
645 | + * |
646 | + * You should have received a copy of the GNU General Public License |
647 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
648 | + */ |
649 | + |
650 | +#ifndef SUGGESTIONSFILTERMODEL_H |
651 | +#define SUGGESTIONSFILTERMODEL_H |
652 | + |
653 | +// Qt |
654 | +#include <QtCore/QAbstractItemModel> |
655 | +#include <QtCore/QList> |
656 | +#include <QtCore/QSortFilterProxyModel> |
657 | +#include <QtCore/QString> |
658 | +#include <QtCore/QStringList> |
659 | +#include <QtCore/QVariant> |
660 | + |
661 | +class SuggestionsFilterModel : public QSortFilterProxyModel |
662 | +{ |
663 | + Q_OBJECT |
664 | + |
665 | + Q_PROPERTY(QVariant sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged) |
666 | + Q_PROPERTY(QStringList terms READ terms WRITE setTerms NOTIFY termsChanged) |
667 | + Q_PROPERTY(QStringList searchFields READ searchFields WRITE setSearchFields NOTIFY searchFieldsChanged) |
668 | + Q_PROPERTY(int count READ count NOTIFY countChanged) |
669 | + |
670 | +public: |
671 | + SuggestionsFilterModel(QObject* parent=0); |
672 | + |
673 | + QVariant sourceModel() const; |
674 | + void setSourceModel(QVariant sourceModel); |
675 | + |
676 | + int count() const; |
677 | + |
678 | + const QStringList& terms() const; |
679 | + void setTerms(const QStringList&); |
680 | + |
681 | + const QStringList& searchFields() const; |
682 | + void setSearchFields(const QStringList&); |
683 | + |
684 | +Q_SIGNALS: |
685 | + void sourceModelChanged() const; |
686 | + void termsChanged() const; |
687 | + void searchFieldsChanged() const; |
688 | + void countChanged() const; |
689 | + |
690 | +protected: |
691 | + // reimplemented from QSortFilterProxyModel |
692 | + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; |
693 | + |
694 | +private: |
695 | + void updateSearchRoles(const QAbstractItemModel* model); |
696 | + |
697 | + QStringList m_terms; |
698 | + QStringList m_searchFields; |
699 | + QList<int> m_searchRoles; |
700 | +}; |
701 | + |
702 | + |
703 | +#endif // SUGGESTIONSFILTERMODEL_H |
704 | |
705 | === modified file 'src/app/webbrowser/webbrowser-app.cpp' |
706 | --- src/app/webbrowser/webbrowser-app.cpp 2015-04-09 14:05:35 +0000 |
707 | +++ src/app/webbrowser/webbrowser-app.cpp 2015-04-22 10:06:25 +0000 |
708 | @@ -22,11 +22,11 @@ |
709 | #include "file-operations.h" |
710 | #include "history-domainlist-chronological-model.h" |
711 | #include "history-domainlist-model.h" |
712 | -#include "history-matches-model.h" |
713 | #include "history-model.h" |
714 | #include "history-timeframe-model.h" |
715 | #include "limit-proxy-model.h" |
716 | #include "searchengine.h" |
717 | +#include "suggestions-filter-model.h" |
718 | #include "tabs-model.h" |
719 | #include "top-sites-model.h" |
720 | #include "webbrowser-app.h" |
721 | @@ -90,7 +90,6 @@ |
722 | |
723 | const char* uri = "webbrowserapp.private"; |
724 | qmlRegisterType<HistoryModel>(uri, 0, 1, "HistoryModel"); |
725 | - qmlRegisterType<HistoryMatchesModel>(uri, 0, 1, "HistoryMatchesModel"); |
726 | qmlRegisterType<HistoryTimeframeModel>(uri, 0, 1, "HistoryTimeframeModel"); |
727 | qmlRegisterType<TopSitesModel>(uri, 0 , 1, "TopSitesModel"); |
728 | qmlRegisterType<HistoryDomainListModel>(uri, 0, 1, "HistoryDomainListModel"); |
729 | @@ -101,6 +100,7 @@ |
730 | qmlRegisterSingletonType<FileOperations>(uri, 0, 1, "FileOperations", FileOperations_singleton_factory); |
731 | qmlRegisterType<SearchEngine>(uri, 0, 1, "SearchEngine"); |
732 | qmlRegisterSingletonType<CacheDeleter>(uri, 0, 1, "CacheDeleter", CacheDeleter_singleton_factory); |
733 | + qmlRegisterType<SuggestionsFilterModel>(uri, 0, 1, "SuggestionsFilterModel"); |
734 | |
735 | if (BrowserApplication::initialize("webbrowser/webbrowser-app.qml")) { |
736 | m_window->setProperty("newSession", m_arguments.contains("--new-session")); |
737 | |
738 | === modified file 'tests/autopilot/webbrowser_app/emulators/browser.py' |
739 | --- tests/autopilot/webbrowser_app/emulators/browser.py 2015-04-07 13:05:03 +0000 |
740 | +++ tests/autopilot/webbrowser_app/emulators/browser.py 2015-04-22 10:06:25 +0000 |
741 | @@ -197,11 +197,9 @@ |
742 | |
743 | class Suggestions(uitk.UbuntuUIToolkitCustomProxyObjectBase): |
744 | |
745 | - def get_list(self): |
746 | - return self.select_single("QQuickListView") |
747 | - |
748 | - def get_entries(self): |
749 | - return self.get_list().select_many("Base") |
750 | + def get_ordered_entries(self): |
751 | + return sorted(self.select_many("Suggestion"), |
752 | + key=lambda item: item.globalRect.y) |
753 | |
754 | |
755 | class GeolocationPermissionRequest(uitk.UbuntuUIToolkitCustomProxyObjectBase): |
756 | |
757 | === renamed file 'tests/autopilot/webbrowser_app/tests/test_history.py' => 'tests/autopilot/webbrowser_app/tests/test_suggestions.py' |
758 | --- tests/autopilot/webbrowser_app/tests/test_history.py 2015-03-23 07:49:19 +0000 |
759 | +++ tests/autopilot/webbrowser_app/tests/test_suggestions.py 2015-04-22 10:06:25 +0000 |
760 | @@ -25,12 +25,17 @@ |
761 | from webbrowser_app.tests import StartOpenRemotePageTestCaseBase |
762 | |
763 | |
764 | -class PrepopulatedHistoryDatabaseTestCaseBase(StartOpenRemotePageTestCaseBase): |
765 | +class PrepopulatedDatabaseTestCaseBase(StartOpenRemotePageTestCaseBase): |
766 | |
767 | - """Helper test class that pre-populates the history database.""" |
768 | + """Helper test class that pre-populates history and bookmarks databases.""" |
769 | |
770 | def setUp(self): |
771 | self.clear_datadir() |
772 | + self.populate_history() |
773 | + self.populate_bookmarks() |
774 | + super(PrepopulatedDatabaseTestCaseBase, self).setUp() |
775 | + |
776 | + def populate_history(self): |
777 | db_path = os.path.join(os.path.expanduser("~"), ".local", "share", |
778 | "webbrowser-app", "history.sqlite") |
779 | connection = sqlite3.connect(db_path) |
780 | @@ -54,7 +59,9 @@ |
781 | "example - Google Search"), |
782 | ("http://example.iana.org/", "iana.org", "Example Domain"), |
783 | ("http://www.iana.org/domains/special", "iana.org", |
784 | - "IANA — Special Use Domains") |
785 | + "IANA — Special Use Domains"), |
786 | + ("http://doc.qt.io/qt-5/qtqml-index.html", "qt.io", |
787 | + "Qt QML 5.4 - Qt Documentation") |
788 | ] |
789 | for i, row in enumerate(rows): |
790 | visits = random.randint(1, 5) |
791 | @@ -65,12 +72,47 @@ |
792 | connection.execute(query) |
793 | connection.commit() |
794 | connection.close() |
795 | - super(PrepopulatedHistoryDatabaseTestCaseBase, self).setUp() |
796 | - |
797 | - |
798 | -class TestHistorySuggestions(PrepopulatedHistoryDatabaseTestCaseBase): |
799 | - |
800 | - """Test the address bar suggestions based on navigation history.""" |
801 | + |
802 | + def populate_bookmarks(self): |
803 | + db_path = os.path.join(os.path.expanduser("~"), ".local", "share", |
804 | + "webbrowser-app", "bookmarks.sqlite") |
805 | + connection = sqlite3.connect(db_path) |
806 | + connection.execute("""CREATE TABLE IF NOT EXISTS bookmarks |
807 | + (url VARCHAR, title VARCHAR, icon VARCHAR, |
808 | + created INTEGER);""") |
809 | + rows = [ |
810 | + ("http://www.rsc.org/periodic-table/element/24/chromium", |
811 | + "Chromium - Element Information"), |
812 | + ("http://www.rsc.org/periodic-table/element/77/iridium", |
813 | + "Iridium - Element Information"), |
814 | + ("http://www.rsc.org/periodic-table/element/31/gallium", |
815 | + "Gallium - Element Information"), |
816 | + ("http://www.rsc.org/periodic-table/element/116/livermorium", |
817 | + "Livermorium - Element Information"), |
818 | + ("http://www.rsc.org/periodic-table/element/62/samarium", |
819 | + "Samarium - Element Information"), |
820 | + ("http://en.wikipedia.org/wiki/Linux", |
821 | + "Linux - Wikipedia, the free encyclopedia"), |
822 | + ("https://www.linux.com/", |
823 | + "Linux.com | The source for Linux information"), |
824 | + ("http://doc.qt.io/qt-5/qtqml-index.html", |
825 | + "Qt QML 5.4 - Qt Documentation") |
826 | + ] |
827 | + |
828 | + for i, row in enumerate(rows): |
829 | + timestamp = int(time.time()) - i * 10 |
830 | + query = "INSERT INTO bookmarks \ |
831 | + VALUES ('{}', '{}', '', {});" |
832 | + query = query.format(row[0], row[1], timestamp) |
833 | + connection.execute(query) |
834 | + |
835 | + connection.commit() |
836 | + connection.close() |
837 | + |
838 | + |
839 | +class TestSuggestions(PrepopulatedDatabaseTestCaseBase): |
840 | + |
841 | + """Test the address bar suggestions (based on history and bookmarks).""" |
842 | |
843 | def setUp(self): |
844 | super().setUp() |
845 | @@ -85,24 +127,49 @@ |
846 | self.assertThat(suggestions.opacity, Eventually(Equals(0))) |
847 | |
848 | def test_show_list_of_suggestions(self): |
849 | - listview = self.main_window.get_suggestions().get_list() |
850 | + suggestions = self.main_window.get_suggestions() |
851 | self.assert_suggestions_eventually_hidden() |
852 | self.assert_suggestions_eventually_hidden() |
853 | self.address_bar.focus() |
854 | self.assert_suggestions_eventually_shown() |
855 | - self.assertThat(listview.count, Eventually(Equals(1))) |
856 | + self.assertThat(suggestions.count, Eventually(Equals(1))) |
857 | self.address_bar.clear() |
858 | self.assert_suggestions_eventually_hidden() |
859 | - self.address_bar.write('u') |
860 | - self.assert_suggestions_eventually_shown() |
861 | - self.assertThat(listview.count, Eventually(Equals(6))) |
862 | - self.address_bar.write('b', clear=False) |
863 | - self.assertThat(listview.count, Eventually(Equals(5))) |
864 | - self.address_bar.write('leh', clear=False) |
865 | - self.assertThat(listview.count, Eventually(Equals(0))) |
866 | - self.address_bar.clear() |
867 | + |
868 | + def test_list_of_suggestions_case_insensitive(self): |
869 | + suggestions = self.main_window.get_suggestions() |
870 | self.address_bar.write('xaMPL') |
871 | - self.assertThat(listview.count, Eventually(Equals(2))) |
872 | + self.assertThat(suggestions.count, Eventually(Equals(2))) |
873 | + |
874 | + def test_list_of_suggestions_history_limits(self): |
875 | + suggestions = self.main_window.get_suggestions() |
876 | + self.address_bar.write('ubuntu') |
877 | + self.assert_suggestions_eventually_shown() |
878 | + self.assertThat(suggestions.count, Eventually(Equals(4))) |
879 | + self.address_bar.write('bleh', clear=False) |
880 | + self.assertThat(suggestions.count, Eventually(Equals(0))) |
881 | + self.address_bar.write('iana') |
882 | + self.assertThat(suggestions.count, Eventually(Equals(2))) |
883 | + |
884 | + def test_list_of_suggestions_bookmark_limits(self): |
885 | + suggestions = self.main_window.get_suggestions() |
886 | + self.address_bar.write('element') |
887 | + self.assert_suggestions_eventually_shown() |
888 | + self.assertThat(suggestions.count, Eventually(Equals(4))) |
889 | + self.address_bar.write('bleh', clear=False) |
890 | + self.assertThat(suggestions.count, Eventually(Equals(0))) |
891 | + self.address_bar.write('linux') |
892 | + self.assertThat(suggestions.count, Eventually(Equals(2))) |
893 | + |
894 | + def test_list_of_suggestions_order(self): |
895 | + suggestions = self.main_window.get_suggestions() |
896 | + self.address_bar.write('QML') |
897 | + self.assert_suggestions_eventually_shown() |
898 | + self.assertThat(suggestions.count, Eventually(Equals(2))) |
899 | + entries = suggestions.get_ordered_entries() |
900 | + self.assertThat(len(entries), Equals(2)) |
901 | + self.assertThat(entries[0].icon, Equals("history")) |
902 | + self.assertThat(entries[1].icon, Equals("non-starred")) |
903 | |
904 | def test_clear_address_bar_dismisses_suggestions(self): |
905 | self.address_bar.focus() |
906 | @@ -140,18 +207,17 @@ |
907 | |
908 | def test_select_suggestion(self): |
909 | suggestions = self.main_window.get_suggestions() |
910 | - listview = suggestions.get_list() |
911 | self.address_bar.focus() |
912 | self.assert_suggestions_eventually_shown() |
913 | self.address_bar.clear() |
914 | self.address_bar.write('ubuntu') |
915 | self.assert_suggestions_eventually_shown() |
916 | - self.assertThat(listview.count, Eventually(Equals(5))) |
917 | - entries = suggestions.get_entries() |
918 | + self.assertThat(suggestions.count, Eventually(Equals(4))) |
919 | + entries = suggestions.get_ordered_entries() |
920 | highlight = '<b><font color="#dd4814">Ubuntu</font></b>' |
921 | url = "http://en.wikipedia.org/wiki/{}_(operating_system)" |
922 | url = url.format(highlight) |
923 | - entries = [entry for entry in entries if url in entry.subText] |
924 | + entries = [entry for entry in entries if url in entry.subtitle] |
925 | entry = entries[0] if len(entries) == 1 else None |
926 | self.assertIsNotNone(entry) |
927 | self.pointing_device.click_object(entry) |
928 | @@ -165,9 +231,8 @@ |
929 | self.address_bar.write('(phil') |
930 | self.assert_suggestions_eventually_shown() |
931 | suggestions = self.main_window.get_suggestions() |
932 | - listview = suggestions.get_list() |
933 | - self.assertThat(listview.count, Eventually(Equals(1))) |
934 | - entry = suggestions.get_entries()[0] |
935 | + self.assertThat(suggestions.count, Eventually(Equals(1))) |
936 | + entry = suggestions.get_ordered_entries()[0] |
937 | highlight = '<b><font color="#dd4814">(phil</font></b>' |
938 | url = "http://en.wikipedia.org/wiki/Ubuntu_{}osophy)".format(highlight) |
939 | - self.assertThat(entry.subText, Contains(url)) |
940 | + self.assertThat(entry.subtitle, Contains(url)) |
941 | |
942 | === modified file 'tests/unittests/CMakeLists.txt' |
943 | --- tests/unittests/CMakeLists.txt 2015-04-08 22:36:49 +0000 |
944 | +++ tests/unittests/CMakeLists.txt 2015-04-22 10:06:25 +0000 |
945 | @@ -2,13 +2,13 @@ |
946 | add_subdirectory(qml) |
947 | add_subdirectory(domain-utils) |
948 | add_subdirectory(history-model) |
949 | -add_subdirectory(history-matches-model) |
950 | add_subdirectory(history-timeframe-model) |
951 | add_subdirectory(history-domain-model) |
952 | add_subdirectory(history-domainlist-model) |
953 | add_subdirectory(history-domainlist-chronological-model) |
954 | add_subdirectory(top-sites-model) |
955 | add_subdirectory(session-utils) |
956 | +add_subdirectory(suggestions-filter-model) |
957 | add_subdirectory(tabs-model) |
958 | add_subdirectory(bookmarks-model) |
959 | add_subdirectory(limit-proxy-model) |
960 | |
961 | === renamed directory 'tests/unittests/history-matches-model' => 'tests/unittests/suggestions-filter-model' |
962 | === modified file 'tests/unittests/suggestions-filter-model/CMakeLists.txt' |
963 | --- tests/unittests/history-matches-model/CMakeLists.txt 2014-03-17 12:44:23 +0000 |
964 | +++ tests/unittests/suggestions-filter-model/CMakeLists.txt 2015-04-22 10:06:25 +0000 |
965 | @@ -1,5 +1,5 @@ |
966 | -set(TEST tst_HistoryMatchesModelTests) |
967 | -add_executable(${TEST} tst_HistoryMatchesModelTests.cpp) |
968 | +set(TEST tst_SuggestionsFilterModelTests) |
969 | +add_executable(${TEST} tst_SuggestionsFilterModelTests.cpp) |
970 | include_directories(${webbrowser-app_SOURCE_DIR}) |
971 | qt5_use_modules(${TEST} Test) |
972 | target_link_libraries(${TEST} webbrowser-app-models) |
973 | |
974 | === renamed file 'tests/unittests/history-matches-model/tst_HistoryMatchesModelTests.cpp' => 'tests/unittests/suggestions-filter-model/tst_SuggestionsFilterModelTests.cpp' |
975 | --- tests/unittests/history-matches-model/tst_HistoryMatchesModelTests.cpp 2013-08-02 13:52:58 +0000 |
976 | +++ tests/unittests/suggestions-filter-model/tst_SuggestionsFilterModelTests.cpp 2015-04-22 10:06:25 +0000 |
977 | @@ -22,24 +22,23 @@ |
978 | |
979 | // local |
980 | #include "history-model.h" |
981 | -#include "history-matches-model.h" |
982 | - |
983 | - |
984 | -class HistoryMatchesModelTests : public QObject |
985 | +#include "suggestions-filter-model.h" |
986 | + |
987 | +class SuggestionsFilterModelTests : public QObject |
988 | { |
989 | Q_OBJECT |
990 | |
991 | private: |
992 | HistoryModel* model; |
993 | - HistoryMatchesModel* matches; |
994 | + SuggestionsFilterModel* matches; |
995 | |
996 | private Q_SLOTS: |
997 | void init() |
998 | { |
999 | model = new HistoryModel; |
1000 | model->setDatabasePath(":memory:"); |
1001 | - matches = new HistoryMatchesModel; |
1002 | - matches->setSourceModel(model); |
1003 | + matches = new SuggestionsFilterModel; |
1004 | + matches->setSourceModel(QVariant::fromValue(model)); |
1005 | } |
1006 | |
1007 | void cleanup() |
1008 | @@ -56,45 +55,58 @@ |
1009 | void shouldNotifyWhenChangingSourceModel() |
1010 | { |
1011 | QSignalSpy spy(matches, SIGNAL(sourceModelChanged())); |
1012 | - matches->setSourceModel(model); |
1013 | + matches->setSourceModel(QVariant::fromValue(model)); |
1014 | QVERIFY(spy.isEmpty()); |
1015 | HistoryModel* model2 = new HistoryModel; |
1016 | - matches->setSourceModel(model2); |
1017 | + matches->setSourceModel(QVariant::fromValue(model2)); |
1018 | QCOMPARE(spy.count(), 1); |
1019 | - QCOMPARE(matches->sourceModel(), model2); |
1020 | - matches->setSourceModel(0); |
1021 | + QCOMPARE(matches->sourceModel(), QVariant::fromValue(model2)); |
1022 | + matches->setSourceModel(QVariant()); |
1023 | QCOMPARE(spy.count(), 2); |
1024 | - QCOMPARE(matches->sourceModel(), (HistoryModel*) 0); |
1025 | + QVERIFY(matches->sourceModel().isNull()); |
1026 | delete model2; |
1027 | } |
1028 | |
1029 | - void shouldBeEmptyWhenQueryIsEmpty() |
1030 | + void shouldBeEmptyWhileTermsAndFieldsEmpty() |
1031 | { |
1032 | model->add(QUrl("http://example.org"), "Example Domain", QUrl()); |
1033 | model->add(QUrl("http://example.com"), "Example Domain", QUrl()); |
1034 | QCOMPARE(matches->rowCount(), 0); |
1035 | } |
1036 | |
1037 | - void shouldRecordQuery() |
1038 | - { |
1039 | - QVERIFY(matches->query().isEmpty()); |
1040 | - matches->setQuery("example"); |
1041 | - QCOMPARE(matches->query(), QString("example")); |
1042 | - } |
1043 | - |
1044 | - void shouldMatchUrl() |
1045 | + void shouldRecordTerms() |
1046 | + { |
1047 | + QVERIFY(matches->terms().isEmpty()); |
1048 | + QStringList terms; |
1049 | + terms.append("example"); |
1050 | + matches->setTerms(terms); |
1051 | + QCOMPARE(matches->terms(), terms); |
1052 | + } |
1053 | + |
1054 | + void shouldRecordSearchFields() |
1055 | + { |
1056 | + QVERIFY(matches->searchFields().isEmpty()); |
1057 | + QStringList fields; |
1058 | + fields.append("url"); |
1059 | + matches->setSearchFields(fields); |
1060 | + QCOMPARE(matches->searchFields(), fields); |
1061 | + } |
1062 | + |
1063 | + void shouldMatch() |
1064 | { |
1065 | model->add(QUrl("http://example.org"), "Example Domain", QUrl()); |
1066 | model->add(QUrl("http://example.com"), "Example Domain", QUrl()); |
1067 | - matches->setQuery("example"); |
1068 | + matches->setTerms(QStringList({"example"})); |
1069 | + matches->setSearchFields(QStringList({"url", "title"})); |
1070 | QCOMPARE(matches->rowCount(), 2); |
1071 | } |
1072 | |
1073 | - void shouldMatchTitle() |
1074 | + void shouldMatchSecondField() |
1075 | { |
1076 | model->add(QUrl("http://example.org"), "Example Domain", QUrl()); |
1077 | model->add(QUrl("http://example.com"), "Example Domain", QUrl()); |
1078 | - matches->setQuery("domain"); |
1079 | + matches->setTerms(QStringList({"domain"})); |
1080 | + matches->setSearchFields(QStringList({"url", "title"})); |
1081 | QCOMPARE(matches->rowCount(), 2); |
1082 | } |
1083 | |
1084 | @@ -104,58 +116,73 @@ |
1085 | model->add(QUrl("http://ubuntu.com"), "Home | Ubuntu", QUrl()); |
1086 | model->add(QUrl("http://example.com"), "Example Domain", QUrl()); |
1087 | model->add(QUrl("http://wikipedia.org"), "Wikipedia", QUrl()); |
1088 | - matches->setQuery("example"); |
1089 | + matches->setTerms(QStringList({"domain"})); |
1090 | + matches->setSearchFields(QStringList({"url", "title"})); |
1091 | QCOMPARE(matches->rowCount(), 2); |
1092 | } |
1093 | |
1094 | - void shouldUpdateResultsWhenQueryChanges() |
1095 | + void shouldUpdateResultsWhenTermsChange() |
1096 | { |
1097 | model->add(QUrl("http://example.org"), "Example Domain", QUrl()); |
1098 | model->add(QUrl("http://ubuntu.com"), "Home | Ubuntu", QUrl()); |
1099 | model->add(QUrl("http://wikipedia.org"), "Wikipedia", QUrl()); |
1100 | model->add(QUrl("http://ubuntu.com/download"), "Download Ubuntu | Ubuntu", QUrl()); |
1101 | - matches->setQuery("ubuntu"); |
1102 | + matches->setSearchFields(QStringList({"url", "title"})); |
1103 | + matches->setTerms(QStringList({"ubuntu"})); |
1104 | QCOMPARE(matches->rowCount(), 2); |
1105 | - matches->setQuery("wiki"); |
1106 | + matches->setTerms(QStringList({"wiki"})); |
1107 | QCOMPARE(matches->rowCount(), 1); |
1108 | - matches->setQuery(""); |
1109 | + matches->setTerms(QStringList()); |
1110 | QCOMPARE(matches->rowCount(), 0); |
1111 | } |
1112 | |
1113 | - void shouldUpdateResultsWhenHistoryChanges() |
1114 | + void shouldUpdateResultsWhenSourceModelUpdates() |
1115 | { |
1116 | model->add(QUrl("http://example.org"), "Example Domain", QUrl()); |
1117 | model->add(QUrl("http://wikipedia.org"), "Wikipedia", QUrl()); |
1118 | - matches->setQuery("ubuntu"); |
1119 | + matches->setTerms(QStringList({"ubuntu"})); |
1120 | + matches->setSearchFields(QStringList({"url"})); |
1121 | QCOMPARE(matches->rowCount(), 0); |
1122 | model->add(QUrl("http://ubuntu.com"), "Home | Ubuntu", QUrl()); |
1123 | QCOMPARE(matches->rowCount(), 1); |
1124 | } |
1125 | |
1126 | - void shouldExtractTermsFromQuery() |
1127 | - { |
1128 | - matches->setQuery("ubuntu"); |
1129 | - QCOMPARE(matches->terms(), QStringList() << "ubuntu"); |
1130 | - matches->setQuery("download ubuntu"); |
1131 | - QCOMPARE(matches->terms(), QStringList() << "download" << "ubuntu"); |
1132 | - matches->setQuery(" ubuntu touch "); |
1133 | - QCOMPARE(matches->terms(), QStringList() << "ubuntu" << "touch"); |
1134 | - matches->setQuery("ubuntu+touch"); |
1135 | - QCOMPARE(matches->terms(), QStringList() << "ubuntu+touch"); |
1136 | - } |
1137 | - |
1138 | void shouldMatchAllTerms() |
1139 | { |
1140 | model->add(QUrl("http://example.org"), "Example Domain", QUrl()); |
1141 | - model->add(QUrl("http://ubuntu.com"), "Home | Ubuntu", QUrl()); |
1142 | - model->add(QUrl("http://wikipedia.org"), "Wikipedia", QUrl()); |
1143 | - model->add(QUrl("http://ubuntu.com/download"), "Download Ubuntu | Ubuntu", QUrl()); |
1144 | - matches->setQuery("ubuntu home"); |
1145 | - QCOMPARE(matches->rowCount(), 1); |
1146 | - matches->setQuery("ubuntu wiki"); |
1147 | - QCOMPARE(matches->rowCount(), 0); |
1148 | + model->add(QUrl("http://example.com"), "Example Domain", QUrl()); |
1149 | + matches->setTerms(QStringList({"example", "org"})); |
1150 | + matches->setSearchFields(QStringList({"url", "title"})); |
1151 | + QCOMPARE(matches->rowCount(), 1); |
1152 | + } |
1153 | + |
1154 | + void shouldMatchTermsInDifferentFields() |
1155 | + { |
1156 | + model->add(QUrl("http://example.org"), "Example Domain", QUrl()); |
1157 | + model->add(QUrl("http://example.com"), "Example Domain", QUrl()); |
1158 | + matches->setTerms(QStringList({"org", "domain"})); |
1159 | + matches->setSearchFields(QStringList({"url", "title"})); |
1160 | + QCOMPARE(matches->rowCount(), 1); |
1161 | + } |
1162 | + |
1163 | + void shouldMatchDuplicateTerms() |
1164 | + { |
1165 | + model->add(QUrl("http://example.org"), "Example Domain", QUrl()); |
1166 | + model->add(QUrl("http://example.com"), "Example Domain", QUrl()); |
1167 | + matches->setTerms(QStringList({"org", "org", "org", "org"})); |
1168 | + matches->setSearchFields(QStringList({"url", "title"})); |
1169 | + QCOMPARE(matches->rowCount(), 1); |
1170 | + } |
1171 | + |
1172 | + void shouldWarnOnInvalidFields() |
1173 | + { |
1174 | + QTest::ignoreMessage(QtWarningMsg, "Source model does not have role matching field: \"foo\""); |
1175 | + model->add(QUrl("http://example.org"), "Example Domain", QUrl()); |
1176 | + matches->setTerms(QStringList({"org"})); |
1177 | + matches->setSearchFields(QStringList({"url", "foo"})); |
1178 | + QCOMPARE(matches->count(), 1); |
1179 | } |
1180 | }; |
1181 | |
1182 | -QTEST_MAIN(HistoryMatchesModelTests) |
1183 | -#include "tst_HistoryMatchesModelTests.moc" |
1184 | +QTEST_MAIN(SuggestionsFilterModelTests) |
1185 | +#include "tst_SuggestionsFilterModelTests.moc" |
Included a bunch of inline comments to clarify on pieces of the patch.