Merge lp:~uriboni/webbrowser-app/search-history into lp:webbrowser-app

Proposed by Ugo Riboni on 2015-09-14
Status: Merged
Approved by: Olivier Tilloy on 2015-09-18
Approved revision: 1040
Merged at revision: 1190
Proposed branch: lp:~uriboni/webbrowser-app/search-history
Merge into: lp:webbrowser-app
Prerequisite: lp:~osomon/webbrowser-app/drawer-menu-actions-visible
Diff against target: 1883 lines (+776/-186)
25 files modified
src/app/webbrowser/Browser.qml (+8/-4)
src/app/webbrowser/CMakeLists.txt (+1/-1)
src/app/webbrowser/Highlight.js (+46/-0)
src/app/webbrowser/HistoryViewWide.qml (+155/-44)
src/app/webbrowser/Suggestions.qml (+4/-26)
src/app/webbrowser/ToolbarAction.qml (+1/-1)
src/app/webbrowser/history-lastvisitdate-model.cpp (+59/-11)
src/app/webbrowser/history-lastvisitdate-model.h (+8/-5)
src/app/webbrowser/history-lastvisitdatelist-model.cpp (+40/-11)
src/app/webbrowser/history-lastvisitdatelist-model.h (+7/-7)
src/app/webbrowser/history-model.h (+8/-7)
src/app/webbrowser/history-timeframe-model.cpp (+7/-0)
src/app/webbrowser/history-timeframe-model.h (+1/-0)
src/app/webbrowser/text-search-filter-model.cpp (+19/-16)
src/app/webbrowser/text-search-filter-model.h (+5/-5)
src/app/webbrowser/webbrowser-app.cpp (+2/-2)
tests/autopilot/webbrowser_app/tests/test_findinpage.py (+14/-0)
tests/unittests/CMakeLists.txt (+1/-1)
tests/unittests/history-lastvisitdate-model/tst_HistoryLastVisitDateModelTests.cpp (+28/-6)
tests/unittests/history-lastvisitdatelist-model/tst_HistoryLastVisitDateListModelTests.cpp (+27/-10)
tests/unittests/qml/CMakeLists.txt (+1/-0)
tests/unittests/qml/tst_HistoryViewWide.qml (+277/-15)
tests/unittests/qml/tst_QmlTests.cpp (+40/-1)
tests/unittests/text-search-filter-model/CMakeLists.txt (+2/-2)
tests/unittests/text-search-filter-model/tst_TextSearchFilterModelTests.cpp (+15/-11)
To merge this branch: bzr merge lp:~uriboni/webbrowser-app/search-history
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration 2015-09-14 Needs Fixing on 2015-09-18
Olivier Tilloy 2015-09-14 Approve on 2015-09-17
Riccardo Padovani 2015-09-14 Pending
Review via email: mp+270929@code.launchpad.net

This proposal supersedes a proposal from 2015-08-20.

Commit Message

Implement search within the history view (only from the widescreen version)

Description of the Change

Implement search within the history view (only from the widescreen version)

NOTE to reviewer: the original author of the HistoryViewWide.qml file seems to have used a mix of different indentation styles and also in some cases left blank lines with spaces. I had my editor setup to automatically remove these and enforce the standard QML style, and noticed this too late. I could go back and revert these changes but I would rather not to.

To post a comment you must log in.
Riccardo Padovani (rpadovani) wrote : Posted in a previous version of this proposal

Thanks for working on this.
Unfortunately, when I press CTRL+F in the history view it doesn't activate the search in the history view, but in the page behind. I think it's a problem of focus...

review: Needs Fixing
Ugo Riboni (uriboni) wrote : Posted in a previous version of this proposal

> Thanks for working on this.
> Unfortunately, when I press CTRL+F in the history view it doesn't activate the
> search in the history view, but in the page behind. I think it's a problem of
> focus...

I can not reproduce this problem. When I press CTRL+F in the history view (widescreen) the search is activated correctly.

I updated the branch to match current trunk, but it was already working before I did that.

Riccardo Padovani (rpadovani) wrote : Posted in a previous version of this proposal

Now it works - probably I did something wrong last time, sorry.

Anyway, another thing:
- Open history
- Select a day (like today)
- Search for a string that matches in others days but not today
- You have this result[0] instead of this[1] (it highlights the day but there are no results)

[0]: http://people.ubuntu.com/~rpadovani/touch/browser/Screenshot%20from%202015-09-03%2011-26-10.png
[1]: http://people.ubuntu.com/~rpadovani/touch/browser/Screenshot%20from%202015-09-03%2011-27-03.png

review: Needs Fixing
Olivier Tilloy (osomon) wrote : Posted in a previous version of this proposal

Please revert the changes to po/webbrowser-app.pot.

review: Needs Fixing
Olivier Tilloy (osomon) wrote : Posted in a previous version of this proposal

 - Ctrl+H to open the history view
 - Ctrl+F to activate search
 - type in a few characters that will match at least one URL
 - down arrow key to select the first match
 - return/enter to open the URL in a new tab

 -> doesn’t work

review: Needs Fixing (functional)
Olivier Tilloy (osomon) wrote : Posted in a previous version of this proposal

Thanks for the fix. I’m seeing another weird behaviour here:

 - open the history view
 - in the left panel, select a date
 - Ctrl+F to search, enter characters that don’t match any URL for that date, the date disappears from the left panel and the last entry in the panel is selected.

In that case, if no entry matched in the selected date, I would expect the search to fall back on "All History", not just a random date.

review: Needs Fixing
Olivier Tilloy (osomon) wrote : Posted in a previous version of this proposal

Another minor issue:

 - Ctrl+H to open history view
 - Ctrl+F to start searching, this focuses the search field as expected
 - Down arrow key, then Left arrow key, then Down arrow key a few times to select a given date
 - From there it seems there is no way to focus again the search field (to search for entries in that specific date) without having to press the up arrow key a few times, thus de-selecting this date. I think pressing Ctrl+F again while there should focus the search field.

review: Needs Fixing
Olivier Tilloy (osomon) wrote : Posted in a previous version of this proposal
Download full text (3.7 KiB)

> When a cetain date is selected and the current search returns no
> results for that date, move back to the "all dates" block

This doesn’t seem to always work. In some cases, a more recent date than the one I initially selected, which has matching results, gets selected (just tested with 1st of sept, no matches for a given search string, but 2nd of sept gets selected instead of all history).
I think monitoring onCountChanged on the list in the right panel is not reliable, you should probably record which date was selected, and if it disappears from the left panel, then reset the current index.

In the visual spec (https://docs.google.com/presentation/d/1P6A7ZsI03sPfuI9vzPeC0VcUfIgugxnU4QyObH6UNTs/edit#slide=id.g75d5dd944_091), the search field has a magnifier icon always displayed on the left, and the field spans the width of the right panel.

In the history view, if I long-press on an item to enter multi-selection mode, then press Ctrl+F, nothing seemingly happens, but after closing the history view find-in-page is activated on the current tab. Ctrl+F events should be swallowed when in multi-selection mode. Or maybe they should exit multi-selection mode and activate search in history. In any case they shouldn’t have an effect on a view that’s not currently visible.

Related to the above comment (and not specific to this MR, but could be fixed together with it): if find in page was activated on the current page, I think it should be turned off when opening the history view.

In Highlight.js, only the highlightTerms function should be public, the rest (global vars and helper functions) shouldn’t be exposed, can’t they be defined in the body of highlightTerms?

In HistoryViewWide.qml, there is a typo in a comment: s/TestSearchFilterModel/TextSearchFilterModel/.

A note on the modifications you made to HistoryLastVisitDateModel: if it wasn’t for bug #1495641, this model could entirely be replaced by a simple SortFilterModel, which is what I did here: http://bazaar.launchpad.net/~osomon/webbrowser-app/use-qml-SortFilterModel/revision/1144. Until my fix for the bug in the UITK lands, your changes look good.

In history-lastvisitdatelist-model.h, includes are not ordered alphabetically.

Is the first change in history-timeframe-model.cpp really necessary? Doesn’t QSortFilterProxyModel::setSourceModel() imply a model reset notification?

In tst_HistoryLastVisitDateListModelTests.cpp, in shouldUpdateWhenChangingSourceModel(), timeframe2 is never deleted (the issue was there already, not introduced by your changes). Can it be instantiated on the stack, or deleted at the end of the method?

Can HistoryModelTest be renamed 'HistoryModelMock' ?

In HistoryModelTest::addByDate(), when inserting a new entry, the number of visits is initially set to 1 (with the call to add(…)), and then bumped to 2. Incrementing entry.visits should be done only when it’s not a new entry.

"efficiency is not important": I would re-word that to "efficiency is not critical". Efficiency is always important :)

In tests/unittests/qml/tst_QmlTests.cpp, the 'HistoryModelTest' type should be registered under "webbrowsertest.private", not under "webbrowserapp.private"...

Read more...

review: Needs Fixing
1021. By Ugo Riboni on 2015-09-16

Merge changes from trunk

Ugo Riboni (uriboni) wrote :

Fixed unless otherwise commented below

> In the history view, if I long-press on an item to enter multi-selection mode, then press Ctrl+F, nothing seemingly > happens, but after closing the history view find-in-page is activated on the current tab. Ctrl+F events should be
> swallowed when in multi-selection mode. Or maybe they should exit multi-selection mode and activate search in
> history. In any case they shouldn’t have an effect on a view that’s not currently visible.

Exiting select mode and entering search mode would be confusing and not symmetric, since when search mode you can't long press on items to switch to select mode.
So I am swallowing the key combo event instead.

> Related to the above comment (and not specific to this MR, but could be fixed together with it): if find in page
> was activated on the current page, I think it should be turned off when opening the history view.

Done and also exited the find in page mode when settings is shown (since when transitioning back from settings I found it really jarring to see the find in page box there)

> A note on the modifications you made to HistoryLastVisitDateModel: if it wasn’t for bug #1495641, this model could
> entirely be replaced by a simple SortFilterModel, which is what I did here: http://bazaar.launchpad.net/~osomon
> /webbrowser-app/use-qml-SortFilterModel/revision/1144. Until my fix for the bug in the UITK lands, your changes
> look good.

Since the change in UITK made it to the overlay PPA already, I switched to using SortFilterModel.
I have not removed the HistoryLastVisitDateModel sources and tests, as I think it is better done in a separate MR (like one based the branch you link above)

> Is the first change in history-timeframe-model.cpp really necessary? Doesn’t
> QSortFilterProxyModel::setSourceModel() imply a model reset notification?

I thought so too, but apparently not. And the documentation does not say so either.

> In tst_HistoryViewWide.qml:
> - in init(), I don’t think a call to historyViewWide.forceActiveFocus() is needed, instead you can just set
> "focus: true" on the Loader (which will forward focus as a Loader is a FocusScope)

Setting focus:true only on the Loader is not enough. On the loader and on the item inside works.

1022. By Ugo Riboni on 2015-09-16

Use a simple SortFilterModel to filter by date intead of a custom model.
Not removing the model for now as it is better done in another MR.

1023. By Ugo Riboni on 2015-09-16

Properly detect when the current index in the dates list has changed automatically as result of a search, and reset index to "all dates" if it has moved to a date that is not in the list anymore.

1024. By Ugo Riboni on 2015-09-16

Intercept the Ctrl+F keypress when in select mode, so that it does not activate find in page in the tab below

1025. By Ugo Riboni on 2015-09-16

Deactivate find in page mode when entering history and settings

1026. By Ugo Riboni on 2015-09-16

Move internal highlight functions and variables within the scope of the public one

1027. By Ugo Riboni on 2015-09-16

Visually adapt the search box to match the visual spec

1028. By Ugo Riboni on 2015-09-16

Fix order of includes

1029. By Ugo Riboni on 2015-09-16

Clean up more properly in unit test

1030. By Ugo Riboni on 2015-09-16

Rename mock class

1031. By Ugo Riboni on 2015-09-16

More correctly implement HistoryModelMock::addByDate

1032. By Ugo Riboni on 2015-09-16

Register mock model in the correct namespace

1033. By Ugo Riboni on 2015-09-16

Remove useless code and focus via built-in properties instead of forcing on test init

1034. By Ugo Riboni on 2015-09-16

Verify text field is not initially visible before entering search mode

1035. By Ugo Riboni on 2015-09-16

Revert previous change, as the bug allowing us to use SortFilterModel.get() properly hasn't landed yet in the UITK (a similar fix had, and I got confused)

1036. By Ugo Riboni on 2015-09-16

Jump back to the search box when pressing UP from the dates list

1037. By Ugo Riboni on 2015-09-16

Correctly take into account which item is initially selected when focus is given via focus property before items are inserted, instead of being forced after items have been inserted

Ugo Riboni (uriboni) wrote :

Note: bug #1495641 has been merged but not released yet. So I reverted that specific change.

Olivier Tilloy (osomon) wrote :

In test_search_button, 'var searchQuery = findChild(historyViewWide, "searchQuery")' is duplicated.

In HistoryViewWide.qml, there is still a typo in a comment: s/TestSearchFilterModel/TextSearchFilterModel/.

Olivier Tilloy (osomon) wrote :

The following block was kept:

  onCountChanged: {
    // when the list becomes empty as a result of searching for
    // something that doesn't exist in the current date, we
    // switch to the "All dates" block
    if (count === 0) lastVisitDateListView.currentIndex = 0
  }

Is it still used now that you added explicit tracking of the selected date when searching? Shouldn’t it be removed?

1038. By Ugo Riboni on 2015-09-17

Fix typo. Remove duplicate variable declaration in unit test.

Olivier Tilloy (osomon) wrote :

Property 'terms' of 'searchQuery' should be marked readonly.

In tests/unittests/qml/tst_HistoryViewWide.qml, the import of webbrowserapp.private 0.1 seems to be useless.

1039. By Ugo Riboni on 2015-09-17

Remove code that is not needed anymore

1040. By Ugo Riboni on 2015-09-17

Mark readonly property as readonly

Olivier Tilloy (osomon) wrote :

Looks good to me now, thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/app/webbrowser/Browser.qml'
2--- src/app/webbrowser/Browser.qml 2015-09-13 21:24:56 +0000
3+++ src/app/webbrowser/Browser.qml 2015-09-17 15:38:35 +0000
4@@ -429,6 +429,7 @@
5 onTriggered: {
6 settingsComponent.createObject(settingsContainer)
7 settingsContainer.focus = true
8+ chrome.findInPageMode = false
9 }
10 }
11 ]
12@@ -480,16 +481,17 @@
13 Keys.onUpPressed: chrome.focus = true
14 Keys.onEscapePressed: internal.resetFocus()
15
16- models: [historySuggestions,
17+ models: searchTerms && searchTerms.length > 0 ?
18+ [historySuggestions,
19 bookmarksSuggestions,
20- searchSuggestions.limit(4)]
21+ searchSuggestions.limit(4)] : []
22
23 LimitProxyModel {
24 id: historySuggestions
25 limit: 2
26 readonly property string icon: "history"
27 readonly property bool displayUrl: true
28- sourceModel: SuggestionsFilterModel {
29+ sourceModel: TextSearchFilterModel {
30 sourceModel: browser.historyModel
31 terms: suggestionsList.searchTerms
32 searchFields: ["url", "title"]
33@@ -501,7 +503,7 @@
34 limit: 2
35 readonly property string icon: "non-starred"
36 readonly property bool displayUrl: true
37- sourceModel: SuggestionsFilterModel {
38+ sourceModel: TextSearchFilterModel {
39 sourceModel: browser.bookmarksModel
40 terms: suggestionsList.searchTerms
41 searchFields: ["url", "title"]
42@@ -775,6 +777,8 @@
43
44 Keys.onEscapePressed: historyViewLoader.active = false
45
46+ onActiveChanged: if (active) chrome.findInPageMode = false
47+
48 Timer {
49 id: historyViewTimer
50 // Set the model asynchronously to ensure
51
52=== modified file 'src/app/webbrowser/CMakeLists.txt'
53--- src/app/webbrowser/CMakeLists.txt 2015-07-16 00:42:10 +0000
54+++ src/app/webbrowser/CMakeLists.txt 2015-09-17 15:38:35 +0000
55@@ -21,8 +21,8 @@
56 history-model.cpp
57 history-timeframe-model.cpp
58 limit-proxy-model.cpp
59- suggestions-filter-model.cpp
60 tabs-model.cpp
61+ text-search-filter-model.cpp
62 top-sites-model.cpp
63 )
64
65
66=== added file 'src/app/webbrowser/Highlight.js'
67--- src/app/webbrowser/Highlight.js 1970-01-01 00:00:00 +0000
68+++ src/app/webbrowser/Highlight.js 2015-09-17 15:38:35 +0000
69@@ -0,0 +1,46 @@
70+/*
71+ * Copyright 2015 Canonical Ltd.
72+ *
73+ * This file is part of webbrowser-app.
74+ *
75+ * webbrowser-app is free software; you can redistribute it and/or modify
76+ * it under the terms of the GNU General Public License as published by
77+ * the Free Software Foundation; version 3.
78+ *
79+ * webbrowser-app is distributed in the hope that it will be useful,
80+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
81+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
82+ * GNU General Public License for more details.
83+ *
84+ * You should have received a copy of the GNU General Public License
85+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
86+ */
87+
88+function highlightTerms(text, terms) {
89+ var termsRe
90+ var highlight = '<font color="#752571">$&</font>'
91+ var searchTerms = []
92+
93+ function escapeTerm(term) {
94+ // Escape special characters in a search term
95+ // (a simpler version of preg_quote).
96+ return term.replace(/[().?+|*^$]/g, '\\$&')
97+ }
98+
99+ function setSearchTerms(terms) {
100+ searchTerms = terms
101+ termsRe = new RegExp(terms.map(escapeTerm).join('|'), 'ig')
102+ }
103+
104+ // Highlight the matching terms in a case-insensitive manner
105+ if (text && text.toString()) {
106+ if (searchTerms !== terms) setSearchTerms(terms)
107+ if (searchTerms.length == 0) return text
108+ var highlighted = text.toString().replace(termsRe, highlight)
109+ highlighted = highlighted.replace(new RegExp('&', 'g'), '&amp;')
110+ highlighted = '<html>' + highlighted + '</html>'
111+ return highlighted
112+ } else {
113+ return ""
114+ }
115+}
116
117=== modified file 'src/app/webbrowser/HistoryViewWide.qml'
118--- src/app/webbrowser/HistoryViewWide.qml 2015-08-18 11:15:13 +0000
119+++ src/app/webbrowser/HistoryViewWide.qml 2015-09-17 15:38:35 +0000
120@@ -20,11 +20,21 @@
121 import Ubuntu.Components 1.3
122 import Ubuntu.Components.ListItems 1.3 as ListItems
123 import webbrowserapp.private 0.1
124+import "Highlight.js" as Highlight
125
126 FocusScope {
127 id: historyViewWide
128
129- property alias historyModel: historyTimeframeModel.sourceModel
130+ property alias historyModel: historySearchModel.sourceModel
131+ property bool searchMode: false
132+ readonly property bool selectMode: urlsListView.ViewItems.selectMode
133+ onSearchModeChanged: {
134+ if (searchMode) searchQuery.focus = true
135+ else {
136+ searchQuery.text = ""
137+ urlsListView.focus = true
138+ }
139+ }
140
141 signal done()
142 signal historyEntryClicked(url url)
143@@ -32,12 +42,22 @@
144
145 Keys.onLeftPressed: lastVisitDateListView.forceActiveFocus()
146 Keys.onRightPressed: urlsListView.forceActiveFocus()
147+ Keys.onUpPressed: if (searchMode) searchQuery.focus = true
148+ Keys.onPressed: {
149+ if (event.modifiers === Qt.ControlModifier && event.key === Qt.Key_F) {
150+ if (searchMode) searchQuery.focus = true
151+ else {
152+ if (!selectMode) searchMode = true
153+ else event.accepted = true
154+ }
155+ }
156+ }
157 Keys.onDeletePressed: {
158 if (urlsListView.ViewItems.selectMode) {
159 internal.removeSelected()
160 } else {
161 if (urlsListView.activeFocus) {
162- historyViewWide.historyModel.removeEntryByUrl(urlsListView.currentItem.url)
163+ historyViewWide.historyModel.removeEntryByUrl(urlsListView.currentItem.siteUrl)
164 if (urlsListView.count == 0) {
165 lastVisitDateListView.currentIndex = 0
166 }
167@@ -62,10 +82,12 @@
168 anchors.fill: parent
169 }
170
171- HistoryTimeframeModel {
172- id: historyTimeframeModel
173+ TextSearchFilterModel {
174+ id: historySearchModel
175+ searchFields: ["title", "url"]
176+ terms: searchQuery.terms
177 }
178-
179+
180 Row {
181 id: historyViewWideRow
182 anchors {
183@@ -77,7 +99,7 @@
184 }
185
186 spacing: units.gu(1)
187-
188+
189 Item {
190 width: units.gu(40)
191 height: parent.height
192@@ -95,27 +117,61 @@
193 }
194 urlsListView.ViewItems.selectedIndices = []
195 }
196-
197+
198+ // Manually track the current date, so that we can detect when
199+ // the ListView automatically changes the currentItem as result
200+ // of a change in the model that removes the currentItem.
201+ // When this happens, we reset the currentItem to "all dates".
202+ property date currentDate
203+
204+ // Ignore currentItemChanged signals while we are changing the
205+ // currentIndex manually (as a result of either UP and DOWN key
206+ // presses, or clicking on items)
207+ // Any other emission of currentItemChanged will therefore be
208+ // from ListView changing it automatically.
209+ function explicitlyChangeCurrentIndex(changeAction) {
210+ explicitlySettingCurrentIndex = true
211+ changeAction()
212+ explicitlySettingCurrentIndex = false
213+ currentDate = currentItem.lastVisitDate
214+ }
215+ property bool explicitlySettingCurrentIndex: false
216+ Keys.onDownPressed: explicitlyChangeCurrentIndex(incrementCurrentIndex)
217+ Keys.onUpPressed: explicitlyChangeCurrentIndex(function() {
218+ if (lastVisitDateListView.currentIndex == 0 && searchMode) {
219+ searchQuery.focus = true
220+ } else {
221+ lastVisitDateListView.decrementCurrentIndex()
222+ }
223+ })
224+
225+ onCurrentItemChanged: {
226+ if (explicitlySettingCurrentIndex) return;
227+ if (currentItem.lastVisitDate.valueOf() !== currentDate.valueOf()) {
228+ currentIndex = 0
229+ }
230+ }
231+
232 model: HistoryLastVisitDateListModel {
233- sourceModel: historyTimeframeModel
234+ sourceModel: historyLastVisitDateModel.sourceModel
235 }
236-
237+
238 delegate: ListItem {
239 objectName: "lastVisitDateDelegate"
240
241 property var lastVisitDate: model.lastVisitDate
242-
243+
244 anchors {
245 left: parent.left
246 right: parent.right
247 rightMargin: units.gu(1)
248 }
249-
250+
251 width: parent.width
252 height: units.gu(4)
253-
254+
255 color: lastVisitDateListView.currentIndex == index ? highlightColor : "transparent"
256-
257+
258 Label {
259 objectName: "lastVisitDateDelegateLabel"
260
261@@ -125,9 +181,9 @@
262 topMargin: units.gu(1)
263 leftMargin: units.gu(2)
264 }
265-
266+
267 height: parent.height
268-
269+
270 text: {
271 if (!lastVisitDate.isValid()) {
272 return i18n.tr("All History")
273@@ -135,14 +191,14 @@
274
275 var today = new Date()
276 today.setHours(0, 0, 0, 0)
277-
278+
279 var yesterday = new Date()
280 yesterday.setDate(yesterday.getDate() - 1)
281 yesterday.setHours(0, 0, 0, 0)
282-
283+
284 var entryDate = new Date(lastVisitDate)
285 entryDate.setHours(0, 0, 0, 0)
286-
287+
288 if (entryDate.getTime() == today.getTime()) {
289 return i18n.tr("Today")
290 } else if (entryDate.getTime() == yesterday.getTime()) {
291@@ -150,12 +206,12 @@
292 }
293 return Qt.formatDate(lastVisitDate, Qt.DefaultLocaleLongDate)
294 }
295-
296+
297 fontSize: "small"
298 color: lastVisitDateListView.currentIndex == index ? UbuntuColors.orange : UbuntuColors.darkGrey
299 }
300-
301- onClicked: lastVisitDateListView.currentIndex = index
302+
303+ onClicked: ListView.view.explicitlyChangeCurrentIndex(function() { ListView.view.currentIndex = index })
304 }
305 }
306
307@@ -174,15 +230,19 @@
308 objectName: "urlsListView"
309
310 anchors.fill: parent
311-
312+
313 Keys.onReturnPressed: historyEntrySelected()
314 Keys.onEnterPressed: historyEntrySelected()
315
316 model: HistoryLastVisitDateModel {
317 id: historyLastVisitDateModel
318- sourceModel: historyTimeframeModel
319+ // Until a valid HistoryModel is assigned the TextSearchFilterModel
320+ // will not report role names, and the HistoryLastVisit*Models will emit warnings
321+ // since they need a dateLastVisit role to be present.
322+ // We avoid this by assigning the sourceModel only when HistoryModel is ready.
323+ sourceModel: historyModel ? historySearchModel : undefined
324 }
325-
326+
327 clip: true
328
329 onModelChanged: urlsListView.currentIndex = -1
330@@ -199,10 +259,10 @@
331 if (urlsListView.ViewItems.selectMode) {
332 currentItem.selected = !currentItem.selected
333 } else {
334- historyViewWide.historyEntryClicked(currentItem.url)
335+ historyViewWide.historyEntryClicked(currentItem.siteUrl)
336 }
337 }
338-
339+
340 // Only use sections for "All History" history list
341 section.property: historyLastVisitDateModel.lastVisitDate.isValid() ? "" : "lastVisitDate"
342 section.delegate: HistorySectionDelegate {
343@@ -211,22 +271,26 @@
344 anchors.leftMargin: units.gu(2)
345 todaySectionTitle: i18n.tr("Today")
346 }
347-
348+
349 delegate: UrlDelegate{
350+ objectName: "historyDelegate"
351 width: parent.width - units.gu(1)
352 height: units.gu(5)
353-
354+
355 color: urlsListView.currentIndex == index ? highlightColor : "transparent"
356-
357+
358+ property url siteUrl: model.url
359+
360 icon: model.icon
361- title: model.title ? model.title : model.url
362- url: model.url
363-
364+ title: Highlight.highlightTerms(model.title ? model.title : model.url, searchQuery.terms)
365+ url: Highlight.highlightTerms(model.url, searchQuery.terms)
366+
367 headerComponent: Component {
368 Item {
369+ objectName: "historySectionDelegate"
370 height: units.gu(3)
371 width: timeLabel.width
372-
373+
374 Label {
375 id: timeLabel
376 anchors.centerIn: parent
377@@ -235,15 +299,15 @@
378 }
379 }
380 }
381-
382- onClicked: {
383+
384+ onClicked: {
385 if (selectMode) {
386 selected = !selected
387 } else {
388 historyViewWide.historyEntryClicked(model.url)
389 }
390 }
391-
392+
393 onRemoved: {
394 historyViewWide.historyModel.removeEntryByUrl(model.url)
395 if (urlsListView.count == 0) {
396@@ -252,6 +316,7 @@
397 }
398
399 onPressAndHold: {
400+ if (historyViewWide.searchMode) return
401 selectMode = !selectMode
402 if (selectMode) {
403 urlsListView.ViewItems.selectedIndices = [index]
404@@ -279,7 +344,8 @@
405 }
406
407 Label {
408- visible: !urlsListView.ViewItems.selectMode
409+ visible: !urlsListView.ViewItems.selectMode &&
410+ !historyViewWide.searchMode
411
412 anchors {
413 top: parent.top
414@@ -288,13 +354,14 @@
415 leftMargin: units.gu(2)
416 }
417
418- text: i18n.tr("History")
419+ text: i18n.tr("History")
420 }
421
422 ToolbarAction {
423 objectName: "backButton"
424
425- visible: urlsListView.ViewItems.selectMode
426+ visible: urlsListView.ViewItems.selectMode ||
427+ historyViewWide.searchMode
428
429 anchors {
430 top: parent.top
431@@ -302,12 +369,16 @@
432 leftMargin: units.gu(2)
433 }
434 height: parent.height - units.gu(2)
435-
436+
437 iconName: "back"
438 text: i18n.tr("Cancel")
439
440 onClicked: {
441- urlsListView.ViewItems.selectMode = false
442+ if (historyViewWide.searchMode) {
443+ historyViewWide.searchMode = false
444+ } else {
445+ urlsListView.ViewItems.selectMode = false
446+ }
447 lastVisitDateListView.forceActiveFocus()
448 }
449 }
450@@ -323,7 +394,7 @@
451 rightMargin: units.gu(2)
452 }
453 height: parent.height - units.gu(2)
454-
455+
456 iconName: "select"
457 text: i18n.tr("Select all")
458
459@@ -349,12 +420,52 @@
460 onClicked: internal.removeSelected()
461 }
462
463+ TextField {
464+ id: searchQuery
465+ objectName: "searchQuery"
466+ anchors {
467+ verticalCenter: parent.verticalCenter
468+ right: parent.right
469+ rightMargin: units.gu(2)
470+ }
471+ width: urlsListView.width
472+ primaryItem: Icon {
473+ height: parent.height - units.gu(2)
474+ width: height
475+ name: "search"
476+ }
477+ hasClearButton: true
478+ placeholderText: i18n.tr("search history")
479+ visible: historyViewWide.searchMode
480+ readonly property var terms: text.split(/\s+/g).filter(function(term) { return term.length > 0 })
481+
482+ Keys.onEscapePressed: historyViewWide.searchMode = false
483+ Keys.onDownPressed: urlsListView.focus = true
484+ }
485+
486+ ToolbarAction {
487+ id: searchButton
488+ iconName: "search"
489+ objectName: "searchButton"
490+ visible: !urlsListView.ViewItems.selectMode &&
491+ !historyViewWide.searchMode
492+ anchors {
493+ verticalCenter: parent.verticalCenter
494+ right: parent.right
495+ rightMargin: units.gu(3.5)
496+ }
497+ height: parent.height - units.gu(2)
498+ onClicked: {
499+ historyViewWide.searchMode = true
500+ searchQuery.forceActiveFocus()
501+ }
502+ }
503+
504 ListItems.ThinDivider {
505 anchors {
506 left: parent.left
507 right: parent.right
508 bottom: parent.bottom
509- bottomMargin: units.gu(1)
510 }
511 }
512 }
513@@ -428,7 +539,7 @@
514 }
515
516 if (urlsListView.count == urls.length) {
517- lastVisitDateListView.currentIndex = 0
518+ lastVisitDateListView.currentIndex = 0
519 }
520
521 urlsListView.ViewItems.selectMode = false
522
523=== modified file 'src/app/webbrowser/Suggestions.qml'
524--- src/app/webbrowser/Suggestions.qml 2015-08-11 11:18:43 +0000
525+++ src/app/webbrowser/Suggestions.qml 2015-09-17 15:38:35 +0000
526@@ -18,6 +18,7 @@
527
528 import QtQuick 2.4
529 import Ubuntu.Components 1.3
530+import "Highlight.js" as Highlight
531
532 FocusScope {
533 id: suggestions
534@@ -71,8 +72,9 @@
535 width: suggestionsList.width
536 showDivider: index < model.length - 1
537
538- title: selected ? modelData.title : internal.highlightTerms(modelData.title)
539- subtitle: modelData.displayUrl ? (selected ? modelData.url : internal.highlightTerms(modelData.url)) : ""
540+ title: selected ? modelData.title : Highlight.highlightTerms(modelData.title, searchTerms)
541+ subtitle: modelData.displayUrl ? (selected ? modelData.url :
542+ Highlight.highlightTerms(modelData.url, searchTerms)) : ""
543 icon: modelData.icon
544 selected: suggestionsList.activeFocus && ListView.isCurrentItem
545
546@@ -88,30 +90,6 @@
547 QtObject {
548 id: internal
549
550- readonly property var termsRe: new RegExp(searchTerms.map(escapeTerm).join('|'), 'ig')
551- readonly property string highlight: '<font color="%1">$&</font>'.arg("#752571")
552-
553- function escapeTerm(term) {
554- // Escape special characters in a search term
555- // (a simpler version of preg_quote).
556- return term.replace(/[().?+|*^$]/g, '\\$&')
557- }
558-
559- function highlightTerms(text) {
560- // Highlight the matching terms in a case-insensitive manner
561- if (text && text.toString()) {
562- if (searchTerms.length == 0) {
563- return text
564- }
565- var highlighted = text.toString().replace(termsRe, highlight)
566- highlighted = highlighted.replace(new RegExp('&', 'g'), '&amp;')
567- highlighted = '<html>' + highlighted + '</html>'
568- return highlighted
569- } else {
570- return ""
571- }
572- }
573-
574 function countItems(total, model) {
575 return total + (model.hasOwnProperty("length") ? model.length : model.count)
576 }
577
578=== modified file 'src/app/webbrowser/ToolbarAction.qml'
579--- src/app/webbrowser/ToolbarAction.qml 2015-08-10 15:22:00 +0000
580+++ src/app/webbrowser/ToolbarAction.qml 2015-09-17 15:38:35 +0000
581@@ -24,7 +24,7 @@
582 property alias text: label.text
583
584 opacity: enabled ? 1.0 : 0.3
585- width: label.paintedWidth
586+ width: Math.max(label.paintedWidth, icon.width)
587
588 Item {
589 anchors {
590
591=== modified file 'src/app/webbrowser/history-lastvisitdate-model.cpp'
592--- src/app/webbrowser/history-lastvisitdate-model.cpp 2015-07-14 22:16:41 +0000
593+++ src/app/webbrowser/history-lastvisitdate-model.cpp 2015-09-17 15:38:35 +0000
594@@ -21,35 +21,61 @@
595 #include "history-timeframe-model.h"
596
597 // Qt
598+#include <QtCore/QDebug>
599 #include <QtCore/QUrl>
600
601 /*!
602 \class HistoryLastVisitDateModel
603- \brief Proxy model that filters the contents of a history model
604- based on last visit date
605+ \brief Proxy model that filters the contents of a model based on last
606+ visit date
607
608 HistoryLastVisitDateModel is a proxy model that filters the contents
609- of a history model based on the last visit date.
610+ of any QAbstractItemModel-derived model based on a role called
611+ "lastVisitDate".
612
613 An entry in the history model matches if the last visit date equals
614 the filter visit date.
615
616- When no visit date is set, all entries match.
617+ When no visit date is set, all entries match. If the model does not have
618+ the "lastVisitDate" role, then no entries are returned if a filter visit
619+ date is set, otherwise all entries match.
620 */
621 HistoryLastVisitDateModel::HistoryLastVisitDateModel(QObject* parent)
622 : QSortFilterProxyModel(parent)
623 {
624 }
625
626-HistoryTimeframeModel* HistoryLastVisitDateModel::sourceModel() const
627+QVariant HistoryLastVisitDateModel::sourceModel() const
628 {
629- return qobject_cast<HistoryTimeframeModel*>(QSortFilterProxyModel::sourceModel());
630+ QAbstractItemModel* model = QSortFilterProxyModel::sourceModel();
631+ return (model) ? QVariant::fromValue(model) : QVariant();
632 }
633
634-void HistoryLastVisitDateModel::setSourceModel(HistoryTimeframeModel* sourceModel)
635+void HistoryLastVisitDateModel::setSourceModel(QVariant sourceModel)
636 {
637- if (sourceModel != this->sourceModel()) {
638- QSortFilterProxyModel::setSourceModel(sourceModel);
639+ QAbstractItemModel* newSourceModel = qvariant_cast<QAbstractItemModel*>(sourceModel);
640+ if (sourceModel.isValid() && newSourceModel == 0) {
641+ qWarning() << "Only QAbstractItemModel-derived instances are allowed as"
642+ << "source models";
643+ }
644+
645+ if (newSourceModel != QSortFilterProxyModel::sourceModel()) {
646+ beginResetModel();
647+
648+ QAbstractItemModel* currentModel = QSortFilterProxyModel::sourceModel();
649+ if (currentModel != 0) {
650+ currentModel->disconnect(this);
651+ }
652+ QSortFilterProxyModel::setSourceModel(newSourceModel);
653+ updateSourceModelRole();
654+
655+ if (newSourceModel != 0) {
656+ connect(newSourceModel, SIGNAL(modelReset()), SLOT(updateSourceModelRole()));
657+ connect(newSourceModel, SIGNAL(layoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)),
658+ SLOT(updateSourceModelRole()));
659+ }
660+
661+ endResetModel();
662 Q_EMIT sourceModelChanged();
663 }
664 }
665@@ -88,6 +114,28 @@
666 if (m_lastVisitDate.isNull()) {
667 return true;
668 }
669- QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
670- return m_lastVisitDate == sourceModel()->data(index, HistoryModel::LastVisitDate).toDate();
671+
672+ if (m_sourceModelRole == -1) {
673+ return false;
674+ }
675+
676+ QAbstractItemModel* model = QSortFilterProxyModel::sourceModel();
677+ if (model) {
678+ QModelIndex index = model->index(source_row, 0, source_parent);
679+ return m_lastVisitDate == model->data(index, m_sourceModelRole).toDate();
680+ } else {
681+ return false;
682+ }
683+}
684+
685+void HistoryLastVisitDateModel::updateSourceModelRole()
686+{
687+ QAbstractItemModel* sourceModel = QSortFilterProxyModel::sourceModel();
688+ if (sourceModel && sourceModel->roleNames().count() > 0) {
689+ m_sourceModelRole = sourceModel->roleNames().key("lastVisitDate", -1);
690+ if (m_sourceModelRole == -1) {
691+ qWarning() << "No results will be returned because the sourceModel"
692+ << "does not have a role named \"lastVisitDate\"";
693+ }
694+ }
695 }
696
697=== modified file 'src/app/webbrowser/history-lastvisitdate-model.h'
698--- src/app/webbrowser/history-lastvisitdate-model.h 2015-08-10 14:21:37 +0000
699+++ src/app/webbrowser/history-lastvisitdate-model.h 2015-09-17 15:38:35 +0000
700@@ -24,21 +24,20 @@
701 #include <QtCore/QSortFilterProxyModel>
702 #include <QtCore/QString>
703 #include <QtCore/QUrl>
704-
705-class HistoryTimeframeModel;
706+#include <QtCore/QVariant>
707
708 class HistoryLastVisitDateModel : public QSortFilterProxyModel
709 {
710 Q_OBJECT
711
712- Q_PROPERTY(HistoryTimeframeModel* sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged)
713+ Q_PROPERTY(QVariant sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged)
714 Q_PROPERTY(QDate lastVisitDate READ lastVisitDate WRITE setLastVisitDate NOTIFY lastVisitDateChanged)
715
716 public:
717 HistoryLastVisitDateModel(QObject* parent=0);
718
719- HistoryTimeframeModel* sourceModel() const;
720- void setSourceModel(HistoryTimeframeModel* sourceModel);
721+ QVariant sourceModel() const;
722+ void setSourceModel(QVariant sourceModel);
723
724 const QDate& lastVisitDate() const;
725 Q_INVOKABLE void setLastVisitDate(const QDate& lastVisitDate);
726@@ -53,8 +52,12 @@
727 // reimplemented from QSortFilterProxyModel
728 bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const;
729
730+private Q_SLOTS:
731+ void updateSourceModelRole();
732+
733 private:
734 QDate m_lastVisitDate;
735+ int m_sourceModelRole;
736 };
737
738 #endif // __HISTORY_LASTVISITDATE_MODEL_H__
739
740=== modified file 'src/app/webbrowser/history-lastvisitdatelist-model.cpp'
741--- src/app/webbrowser/history-lastvisitdatelist-model.cpp 2015-08-11 16:51:54 +0000
742+++ src/app/webbrowser/history-lastvisitdatelist-model.cpp 2015-09-17 15:38:35 +0000
743@@ -21,6 +21,7 @@
744 #include "history-timeframe-model.h"
745
746 // Qt
747+#include <QtCore/QDebug>
748 #include <QtCore/QStringList>
749
750 /*!
751@@ -28,10 +29,13 @@
752 \brief List model that exposes a list of all last visit dates from history
753
754 HistoryLastVisitiDateListModel is a list model that exposes all last visit
755- dates from a HistoryTimeframeModel. Each item in the list has one single
756- role: 'lastVisitDate' for a date in which there is at least one url visited
757- on the source model. A special entry is added to the begining of the list
758- to represent all dates.
759+ dates from the source model. Each item has one single role: 'lastVisitDate'
760+ for a date in which there is at least one url visited on the source model.
761+ A special entry is added to the begining of the list to represent all dates.
762+
763+ The source model needs to expose a role named 'lastVisitDate', from which
764+ the input dates will be read. If such role is not present, this model will
765+ not expose any dates.
766 */
767 HistoryLastVisitDateListModel::HistoryLastVisitDateListModel(QObject* parent)
768 : QAbstractListModel(parent)
769@@ -74,20 +78,29 @@
770 }
771 }
772
773-HistoryTimeframeModel* HistoryLastVisitDateListModel::sourceModel() const
774+QVariant HistoryLastVisitDateListModel::sourceModel() const
775 {
776- return m_sourceModel;
777+ return (m_sourceModel) ? QVariant::fromValue(m_sourceModel) : QVariant();
778 }
779
780-void HistoryLastVisitDateListModel::setSourceModel(HistoryTimeframeModel* sourceModel)
781+void HistoryLastVisitDateListModel::setSourceModel(QVariant sourceModel)
782 {
783- if (sourceModel != m_sourceModel) {
784+ QAbstractItemModel* newSourceModel = qvariant_cast<QAbstractItemModel*>(sourceModel);
785+ if (sourceModel.isValid() && newSourceModel == 0) {
786+ qWarning() << "Only QAbstractItemModel-derived instances are allowed as"
787+ << "source models";
788+ }
789+
790+ if (newSourceModel != m_sourceModel) {
791 beginResetModel();
792 if (m_sourceModel != 0) {
793 m_sourceModel->disconnect(this);
794 }
795 clearLastVisitDates();
796- m_sourceModel = sourceModel;
797+
798+ m_sourceModel = newSourceModel;
799+ updateSourceModelRole();
800+
801 populateModel();
802 if (m_sourceModel != 0) {
803 connect(m_sourceModel, SIGNAL(rowsInserted(const QModelIndex&, int, int)),
804@@ -157,14 +170,26 @@
805 if (m_lastVisitDates.isEmpty()) {
806 // Remove the default entry if model is empty
807 beginRemoveRows(QModelIndex(), 0, 0);
808- m_orderedDates.clear();
809+ m_orderedDates.clear();
810 endRemoveRows();
811 }
812 }
813
814+void HistoryLastVisitDateListModel::updateSourceModelRole()
815+{
816+ if (m_sourceModel && m_sourceModel->roleNames().count() > 0) {
817+ m_sourceModelRole = m_sourceModel->roleNames().key("lastVisitDate", -1);
818+ if (m_sourceModelRole == -1) {
819+ qWarning() << "No results will be returned because the sourceModel"
820+ << "does not have a role named \"lastVisitDate\"";
821+ }
822+ }
823+}
824+
825 void HistoryLastVisitDateListModel::onModelReset()
826 {
827 beginResetModel();
828+ updateSourceModelRole();
829 clearLastVisitDates();
830 populateModel();
831 endResetModel();
832@@ -172,7 +197,11 @@
833
834 void HistoryLastVisitDateListModel::insertNewHistoryEntry(QPersistentModelIndex* index, bool notify)
835 {
836- QDate lastVisitDate = index->data(HistoryModel::LastVisitDate).toDate();
837+ if (m_sourceModelRole == -1) {
838+ return;
839+ }
840+
841+ QDate lastVisitDate = index->data(m_sourceModelRole).toDate();
842 if (!m_lastVisitDates.contains(lastVisitDate)) {
843 if (m_orderedDates.isEmpty()) {
844 // Add default entry to represent all dates
845
846=== modified file 'src/app/webbrowser/history-lastvisitdatelist-model.h'
847--- src/app/webbrowser/history-lastvisitdatelist-model.h 2015-08-10 13:51:19 +0000
848+++ src/app/webbrowser/history-lastvisitdatelist-model.h 2015-09-17 15:38:35 +0000
849@@ -22,16 +22,14 @@
850 // Qt
851 #include <QtCore/QAbstractListModel>
852 #include <QtCore/QMap>
853+#include <QtCore/QSortFilterProxyModel>
854 #include <QtCore/QString>
855
856-class HistoryLastVisitDateModel;
857-class HistoryTimeframeModel;
858-
859 class HistoryLastVisitDateListModel : public QAbstractListModel
860 {
861 Q_OBJECT
862
863- Q_PROPERTY(HistoryTimeframeModel* sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged)
864+ Q_PROPERTY(QVariant sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged)
865
866 Q_ENUMS(Roles)
867
868@@ -48,8 +46,8 @@
869 int rowCount(const QModelIndex& parent=QModelIndex()) const;
870 QVariant data(const QModelIndex& index, int role) const;
871
872- HistoryTimeframeModel* sourceModel() const;
873- void setSourceModel(HistoryTimeframeModel* sourceModel);
874+ QVariant sourceModel() const;
875+ void setSourceModel(QVariant sourceModel);
876
877 Q_SIGNALS:
878 void sourceModelChanged() const;
879@@ -60,13 +58,15 @@
880 void onModelReset();
881
882 private:
883- HistoryTimeframeModel* m_sourceModel;
884+ QAbstractItemModel* m_sourceModel;
885+ int m_sourceModelRole;
886 QMap<QDate, QList<QPersistentModelIndex*>*> m_lastVisitDates;
887 QList<QDate> m_orderedDates;
888
889 void clearLastVisitDates();
890 void populateModel();
891 void insertNewHistoryEntry(QPersistentModelIndex* index, bool notify);
892+ void updateSourceModelRole();
893 };
894
895 #endif // __HISTORY_LASTVISITDATELIST_MODEL_H__
896
897=== modified file 'src/app/webbrowser/history-model.h'
898--- src/app/webbrowser/history-model.h 2015-08-17 19:26:47 +0000
899+++ src/app/webbrowser/history-model.h 2015-09-17 15:38:35 +0000
900@@ -73,12 +73,7 @@
901 void databasePathChanged() const;
902 void rowCountChanged();
903
904-private:
905- QMutex m_dbMutex;
906- QSqlDatabase m_database;
907-
908- QList<QUrl> m_hiddenEntries;
909-
910+protected:
911 struct HistoryEntry {
912 QUrl url;
913 QString domain;
914@@ -90,6 +85,13 @@
915 };
916 QList<HistoryEntry> m_entries;
917 int getEntryIndex(const QUrl& url) const;
918+ void updateExistingEntryInDatabase(const HistoryEntry& entry);
919+
920+private:
921+ QMutex m_dbMutex;
922+ QSqlDatabase m_database;
923+
924+ QList<QUrl> m_hiddenEntries;
925
926 void resetDatabase(const QString& databaseName);
927 void createOrAlterDatabaseSchema();
928@@ -97,7 +99,6 @@
929 void removeByIndex(int index);
930 void insertNewEntryInDatabase(const HistoryEntry& entry);
931 void insertNewEntryInHiddenDatabase(const QUrl& url);
932- void updateExistingEntryInDatabase(const HistoryEntry& entry);
933 void removeEntryFromDatabaseByUrl(const QUrl& url);
934 void removeEntryFromHiddenDatabaseByUrl(const QUrl& url);
935 void removeEntriesFromDatabaseByDate(const QDate& date);
936
937=== modified file 'src/app/webbrowser/history-timeframe-model.cpp'
938--- src/app/webbrowser/history-timeframe-model.cpp 2014-03-12 10:59:54 +0000
939+++ src/app/webbrowser/history-timeframe-model.cpp 2015-09-17 15:38:35 +0000
940@@ -44,7 +44,9 @@
941 void HistoryTimeframeModel::setSourceModel(HistoryModel* sourceModel)
942 {
943 if (sourceModel != this->sourceModel()) {
944+ beginResetModel();
945 QSortFilterProxyModel::setSourceModel(sourceModel);
946+ endResetModel();
947 Q_EMIT sourceModelChanged();
948 }
949 }
950@@ -89,3 +91,8 @@
951 }
952 return true;
953 }
954+
955+QHash<int, QByteArray> HistoryTimeframeModel::roleNames() const
956+{
957+ return (sourceModel()) ? sourceModel()->roleNames() : QHash<int, QByteArray>();
958+}
959
960=== modified file 'src/app/webbrowser/history-timeframe-model.h'
961--- src/app/webbrowser/history-timeframe-model.h 2014-03-12 10:59:54 +0000
962+++ src/app/webbrowser/history-timeframe-model.h 2015-09-17 15:38:35 +0000
963@@ -53,6 +53,7 @@
964 protected:
965 // reimplemented from QSortFilterProxyModel
966 bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const;
967+ QHash<int, QByteArray> roleNames() const;
968
969 private:
970 QDateTime m_start;
971
972=== renamed file 'src/app/webbrowser/suggestions-filter-model.cpp' => 'src/app/webbrowser/text-search-filter-model.cpp'
973--- src/app/webbrowser/suggestions-filter-model.cpp 2015-04-16 17:48:55 +0000
974+++ src/app/webbrowser/text-search-filter-model.cpp 2015-09-17 15:38:35 +0000
975@@ -16,37 +16,40 @@
976 * along with this program. If not, see <http://www.gnu.org/licenses/>.
977 */
978
979-#include "suggestions-filter-model.h"
980+#include "text-search-filter-model.h"
981
982 #include <QtCore/QDebug>
983 #include <QtCore/QSet>
984
985 /*!
986- \class SuggestionsFilterModel
987+ \class TextSearchFilterModel
988 \brief Proxy model that filters the contents of a model based on a list of
989 keywords applied to multiple fields.
990
991- SuggestionsFilterModel is a proxy model that filters the contents of a
992+ TextSearchFilterModel is a proxy model that filters the contents of a
993 model based on a list of terms string that is applied to multiple user
994 defined fields (matching role names in the source model).
995
996 An item in the source model is returned by this model if all the search
997- terms are contained across all of the item's fields (i.e. given two search
998+ terms are contained in any of the item's fields (i.e. given two search
999 terms, if one is found in a field and the other in a different field, then
1000- the item will be returned)
1001+ the item will be returned, but it will not be returned if only one is found)
1002+
1003+ If no searchTerms and/or no searchFields are present, all entries from the
1004+ source model are returned.
1005 */
1006-SuggestionsFilterModel::SuggestionsFilterModel(QObject* parent)
1007+TextSearchFilterModel::TextSearchFilterModel(QObject* parent)
1008 : QSortFilterProxyModel(parent)
1009 {
1010 }
1011
1012-QVariant SuggestionsFilterModel::sourceModel() const
1013+QVariant TextSearchFilterModel::sourceModel() const
1014 {
1015 QAbstractItemModel* source = QSortFilterProxyModel::sourceModel();
1016 return (source) ? QVariant::fromValue(source) : QVariant();
1017 }
1018
1019-void SuggestionsFilterModel::setSourceModel(QVariant sourceModel)
1020+void TextSearchFilterModel::setSourceModel(QVariant sourceModel)
1021 {
1022 QAbstractItemModel* currentSource = QSortFilterProxyModel::sourceModel();
1023 QAbstractItemModel* newSource = qvariant_cast<QAbstractItemModel*>(sourceModel);
1024@@ -58,7 +61,7 @@
1025 }
1026 }
1027
1028-void SuggestionsFilterModel::setTerms(const QStringList& terms)
1029+void TextSearchFilterModel::setTerms(const QStringList& terms)
1030 {
1031 if (terms != m_terms) {
1032 m_terms = terms;
1033@@ -68,12 +71,12 @@
1034 }
1035 }
1036
1037-const QStringList& SuggestionsFilterModel::terms() const
1038+const QStringList& TextSearchFilterModel::terms() const
1039 {
1040 return m_terms;
1041 }
1042
1043-void SuggestionsFilterModel::setSearchFields(const QStringList& searchFields)
1044+void TextSearchFilterModel::setSearchFields(const QStringList& searchFields)
1045 {
1046 if (searchFields != m_searchFields) {
1047 m_searchFields = searchFields;
1048@@ -84,12 +87,12 @@
1049 }
1050 }
1051
1052-const QStringList& SuggestionsFilterModel::searchFields() const
1053+const QStringList& TextSearchFilterModel::searchFields() const
1054 {
1055 return m_searchFields;
1056 }
1057
1058-void SuggestionsFilterModel::updateSearchRoles(const QAbstractItemModel* model) {
1059+void TextSearchFilterModel::updateSearchRoles(const QAbstractItemModel* model) {
1060 m_searchRoles.clear();
1061 if (model) {
1062 Q_FOREACH(const QString& field, m_searchFields) {
1063@@ -103,10 +106,10 @@
1064 }
1065 }
1066
1067-bool SuggestionsFilterModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
1068+bool TextSearchFilterModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
1069 {
1070 if (m_terms.isEmpty() || m_searchFields.isEmpty()) {
1071- return false;
1072+ return true;
1073 }
1074
1075 QAbstractItemModel* source = QSortFilterProxyModel::sourceModel();
1076@@ -129,7 +132,7 @@
1077 return false;
1078 }
1079
1080-int SuggestionsFilterModel::count() const
1081+int TextSearchFilterModel::count() const
1082 {
1083 return rowCount();
1084 }
1085
1086=== renamed file 'src/app/webbrowser/suggestions-filter-model.h' => 'src/app/webbrowser/text-search-filter-model.h'
1087--- src/app/webbrowser/suggestions-filter-model.h 2015-04-16 15:59:20 +0000
1088+++ src/app/webbrowser/text-search-filter-model.h 2015-09-17 15:38:35 +0000
1089@@ -16,8 +16,8 @@
1090 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1091 */
1092
1093-#ifndef SUGGESTIONSFILTERMODEL_H
1094-#define SUGGESTIONSFILTERMODEL_H
1095+#ifndef TEXTSEARCHFILTERMODEL_H
1096+#define TEXTSEARCHFILTERMODEL_H
1097
1098 // Qt
1099 #include <QtCore/QAbstractItemModel>
1100@@ -27,7 +27,7 @@
1101 #include <QtCore/QStringList>
1102 #include <QtCore/QVariant>
1103
1104-class SuggestionsFilterModel : public QSortFilterProxyModel
1105+class TextSearchFilterModel : public QSortFilterProxyModel
1106 {
1107 Q_OBJECT
1108
1109@@ -37,7 +37,7 @@
1110 Q_PROPERTY(int count READ count NOTIFY countChanged)
1111
1112 public:
1113- SuggestionsFilterModel(QObject* parent=0);
1114+ TextSearchFilterModel(QObject* parent=0);
1115
1116 QVariant sourceModel() const;
1117 void setSourceModel(QVariant sourceModel);
1118@@ -69,4 +69,4 @@
1119 };
1120
1121
1122-#endif // SUGGESTIONSFILTERMODEL_H
1123+#endif // TEXTSEARCHFILTERMODEL_H
1124
1125=== modified file 'src/app/webbrowser/webbrowser-app.cpp'
1126--- src/app/webbrowser/webbrowser-app.cpp 2015-08-10 14:21:37 +0000
1127+++ src/app/webbrowser/webbrowser-app.cpp 2015-09-17 15:38:35 +0000
1128@@ -29,7 +29,7 @@
1129 #include "history-timeframe-model.h"
1130 #include "limit-proxy-model.h"
1131 #include "searchengine.h"
1132-#include "suggestions-filter-model.h"
1133+#include "text-search-filter-model.h"
1134 #include "tabs-model.h"
1135 #include "top-sites-model.h"
1136 #include "webbrowser-app.h"
1137@@ -81,7 +81,7 @@
1138 qmlRegisterSingletonType<FileOperations>(uri, 0, 1, "FileOperations", FileOperations_singleton_factory);
1139 qmlRegisterType<SearchEngine>(uri, 0, 1, "SearchEngine");
1140 qmlRegisterSingletonType<CacheDeleter>(uri, 0, 1, "CacheDeleter", CacheDeleter_singleton_factory);
1141- qmlRegisterType<SuggestionsFilterModel>(uri, 0, 1, "SuggestionsFilterModel");
1142+ qmlRegisterType<TextSearchFilterModel>(uri, 0, 1, "TextSearchFilterModel");
1143
1144 if (BrowserApplication::initialize("webbrowser/webbrowser-app.qml")) {
1145 QStringList searchEnginesSearchPaths;
1146
1147=== modified file 'tests/autopilot/webbrowser_app/tests/test_findinpage.py'
1148--- tests/autopilot/webbrowser_app/tests/test_findinpage.py 2015-08-14 09:25:38 +0000
1149+++ tests/autopilot/webbrowser_app/tests/test_findinpage.py 2015-09-17 15:38:35 +0000
1150@@ -155,6 +155,20 @@
1151 self.pointing_device.click_object(webview)
1152 self.assertThat(bar.findInPageMode, Eventually(Equals(False)))
1153
1154+ def test_history_exits_findinpage_mode(self):
1155+ bar = self.chrome.address_bar
1156+ self.activate_find_in_page(False)
1157+ self.assertThat(bar.findInPageMode, Eventually(Equals(True)))
1158+ self.open_history()
1159+ self.assertThat(bar.findInPageMode, Eventually(Equals(False)))
1160+
1161+ def test_settings_exits_findinpage_mode(self):
1162+ bar = self.chrome.address_bar
1163+ self.activate_find_in_page(False)
1164+ self.assertThat(bar.findInPageMode, Eventually(Equals(True)))
1165+ self.open_settings()
1166+ self.assertThat(bar.findInPageMode, Eventually(Equals(False)))
1167+
1168 def test_find_in_page_not_in_menu_in_new_tab(self):
1169 if not self.main_window.wide:
1170 self.open_tabs_view()
1171
1172=== modified file 'tests/unittests/CMakeLists.txt'
1173--- tests/unittests/CMakeLists.txt 2015-07-07 20:50:04 +0000
1174+++ tests/unittests/CMakeLists.txt 2015-09-17 15:38:35 +0000
1175@@ -10,7 +10,6 @@
1176 add_subdirectory(history-lastvisitdatelist-model)
1177 add_subdirectory(top-sites-model)
1178 add_subdirectory(session-utils)
1179-add_subdirectory(suggestions-filter-model)
1180 add_subdirectory(tabs-model)
1181 add_subdirectory(bookmarks-model)
1182 add_subdirectory(bookmarks-folder-model)
1183@@ -24,3 +23,4 @@
1184 add_subdirectory(webapp-container-hook)
1185 add_subdirectory(intent-filter)
1186 add_subdirectory(search-engine)
1187+add_subdirectory(text-search-filter-model)
1188
1189=== modified file 'tests/unittests/history-lastvisitdate-model/tst_HistoryLastVisitDateModelTests.cpp'
1190--- tests/unittests/history-lastvisitdate-model/tst_HistoryLastVisitDateModelTests.cpp 2015-08-04 02:14:53 +0000
1191+++ tests/unittests/history-lastvisitdate-model/tst_HistoryLastVisitDateModelTests.cpp 2015-09-17 15:38:35 +0000
1192@@ -21,6 +21,7 @@
1193 #include <QtTest/QtTest>
1194
1195 // local
1196+#include "bookmarks-model.h"
1197 #include "history-lastvisitdate-model.h"
1198 #include "history-model.h"
1199 #include "history-timeframe-model.h"
1200@@ -43,7 +44,7 @@
1201 timeframe = new HistoryTimeframeModel;
1202 timeframe->setSourceModel(history);
1203 model = new HistoryLastVisitDateModel;
1204- model->setSourceModel(timeframe);
1205+ model->setSourceModel(QVariant::fromValue(timeframe));
1206 }
1207
1208 void cleanup()
1209@@ -61,15 +62,36 @@
1210 void shouldNotifyWhenChangingSourceModel()
1211 {
1212 QSignalSpy spy(model, SIGNAL(sourceModelChanged()));
1213- model->setSourceModel(timeframe);
1214+ model->setSourceModel(QVariant::fromValue(timeframe));
1215 QVERIFY(spy.isEmpty());
1216+
1217 HistoryTimeframeModel* timeframe2 = new HistoryTimeframeModel(model);
1218- model->setSourceModel(timeframe2);
1219+ model->setSourceModel(QVariant::fromValue(timeframe2));
1220 QCOMPARE(spy.count(), 1);
1221- QCOMPARE(model->sourceModel(), timeframe2);
1222- model->setSourceModel(0);
1223+ QCOMPARE(model->sourceModel(), QVariant::fromValue(timeframe2));
1224+
1225+ model->setSourceModel(QVariant());
1226 QCOMPARE(spy.count(), 2);
1227- QCOMPARE(model->sourceModel(), (HistoryTimeframeModel*) 0);
1228+ QVERIFY(!model->sourceModel().isValid());
1229+
1230+ QTest::ignoreMessage(QtWarningMsg, "Only QAbstractItemModel-derived instances are allowed as source models");
1231+ model->setSourceModel(QVariant::fromValue(QString("not a model")));
1232+ QVERIFY(!model->sourceModel().isValid());
1233+ QCOMPARE(model->rowCount(), 0);
1234+ QCOMPARE(spy.count(), 2); // model is still invalid internally so no signal emitted
1235+
1236+ QTest::ignoreMessage(QtWarningMsg, "No results will be returned because the sourceModel does not have a role named \"lastVisitDate\"");
1237+ BookmarksModel bookmarks;
1238+ bookmarks.setDatabasePath(":memory:");
1239+ bookmarks.add(QUrl("http://example.org/"), "Example Domain", QUrl(), "");
1240+ model->setSourceModel(QVariant::fromValue(&bookmarks));
1241+ QCOMPARE(spy.count(), 3);
1242+ // with no filter, all entries match even if the model doesn't have the lastVisitDate role
1243+ QCOMPARE(model->rowCount(), 1);
1244+ model->setLastVisitDate(QDate::currentDate());
1245+ QCOMPARE(model->rowCount(), 0);
1246+
1247+ delete timeframe2;
1248 }
1249
1250 void shouldNotifyWhenChangingLastVisitDate()
1251
1252=== modified file 'tests/unittests/history-lastvisitdatelist-model/tst_HistoryLastVisitDateListModelTests.cpp'
1253--- tests/unittests/history-lastvisitdatelist-model/tst_HistoryLastVisitDateListModelTests.cpp 2015-08-11 16:51:54 +0000
1254+++ tests/unittests/history-lastvisitdatelist-model/tst_HistoryLastVisitDateListModelTests.cpp 2015-09-17 15:38:35 +0000
1255@@ -21,6 +21,7 @@
1256 #include <QtTest/QtTest>
1257
1258 // local
1259+#include "bookmarks-model.h"
1260 #include "domain-utils.h"
1261 #include "history-lastvisitdatelist-model.h"
1262 #include "history-model.h"
1263@@ -110,7 +111,7 @@
1264 m_entries.removeAt(i);
1265 endRemoveRows();
1266 Q_EMIT rowCountChanged();
1267- }
1268+ }
1269 }
1270 }
1271
1272@@ -146,6 +147,7 @@
1273 MockHistoryModel* mockHistory;
1274 HistoryTimeframeModel* timeframe;
1275 HistoryLastVisitDateListModel* model;
1276+ BookmarksModel* bookmarks;
1277
1278 private Q_SLOTS:
1279 void init()
1280@@ -155,7 +157,9 @@
1281 timeframe = new HistoryTimeframeModel;
1282 timeframe->setSourceModel(mockHistory);
1283 model = new HistoryLastVisitDateListModel;
1284- model->setSourceModel(timeframe);
1285+ model->setSourceModel(QVariant::fromValue(timeframe));
1286+ bookmarks = new BookmarksModel;
1287+ bookmarks->setDatabasePath(":memory:");
1288 }
1289
1290 void cleanup()
1291@@ -163,6 +167,7 @@
1292 delete model;
1293 delete timeframe;
1294 delete mockHistory;
1295+ delete bookmarks;
1296 }
1297
1298 void shouldBeInitiallyEmpty()
1299@@ -270,7 +275,7 @@
1300 {
1301 QDateTime dt1 = QDateTime(QDate(1970, 1, 1), QTime(6, 0, 0));
1302 QDateTime dt2 = QDateTime(QDate(1970, 1, 2), QTime(6, 0, 0));
1303-
1304+
1305 mockHistory->add(QUrl("http://example.com/"), "Example Domain", "example.com", QUrl(), dt1);
1306 mockHistory->add(QUrl("http://example.org/"), "Example Domain", "example.org", QUrl(), dt2);
1307 QTest::qWait(100);
1308@@ -288,7 +293,7 @@
1309 QSignalSpy spyRowsRemoved(model, SIGNAL(rowsRemoved(const QModelIndex&, int, int)));
1310 QDateTime dt1 = QDateTime(QDate(1970, 1, 1), QTime(6, 0, 0));
1311 QDateTime dt2 = QDateTime(QDate(1970, 1, 2), QTime(6, 0, 0));
1312-
1313+
1314 mockHistory->add(QUrl("http://example.com/"), "Example Domain", "example.com", QUrl(), dt1);
1315 mockHistory->add(QUrl("http://example.org/"), "Example Domain", "example.org", QUrl(), dt2);
1316 QCOMPARE(model->rowCount(), 3);
1317@@ -308,21 +313,33 @@
1318 mockHistory->add(QUrl("http://example.net/"), "Example Domain", "example.net", QUrl(), dt3);
1319 QCOMPARE(model->rowCount(), 4);
1320
1321- model->setSourceModel(timeframe);
1322+ model->setSourceModel(QVariant::fromValue(timeframe));
1323 QVERIFY(spy.isEmpty());
1324 QCOMPARE(model->rowCount(), 4);
1325
1326+ QTest::ignoreMessage(QtWarningMsg, "Only QAbstractItemModel-derived instances are allowed as source models");
1327 model->setSourceModel(0);
1328 QCOMPARE(spy.count(), 1);
1329- QCOMPARE(model->sourceModel(), (HistoryTimeframeModel*) 0);
1330+ QVERIFY(!model->sourceModel().isValid());
1331 QCOMPARE(model->rowCount(), 0);
1332
1333- HistoryTimeframeModel* timeframe2 = new HistoryTimeframeModel(mockHistory);
1334- timeframe2->setSourceModel(mockHistory);
1335- model->setSourceModel(timeframe2);
1336+ HistoryTimeframeModel timeframe2(mockHistory);
1337+ timeframe2.setSourceModel(mockHistory);
1338+ model->setSourceModel(QVariant::fromValue(&timeframe2));
1339 QCOMPARE(spy.count(), 2);
1340- QCOMPARE(model->sourceModel(), timeframe2);
1341+ QCOMPARE(model->sourceModel(), QVariant::fromValue(&timeframe2));
1342 QCOMPARE(model->rowCount(), 4);
1343+
1344+ QTest::ignoreMessage(QtWarningMsg, "Only QAbstractItemModel-derived instances are allowed as source models");
1345+ model->setSourceModel(QVariant::fromValue(QString("not a model")));
1346+ QCOMPARE(spy.count(), 3);
1347+ QVERIFY(!model->sourceModel().isValid());
1348+ QCOMPARE(model->rowCount(), 0);
1349+
1350+ QTest::ignoreMessage(QtWarningMsg, "No results will be returned because the sourceModel does not have a role named \"lastVisitDate\"");
1351+ bookmarks->add(QUrl("http://example.org/"), "Example Domain", QUrl(), "");
1352+ model->setSourceModel(QVariant::fromValue(bookmarks));
1353+ QCOMPARE(model->rowCount(), 0);
1354 }
1355
1356 void shouldKeepLastVisitDatesSorted()
1357
1358=== modified file 'tests/unittests/qml/CMakeLists.txt'
1359--- tests/unittests/qml/CMakeLists.txt 2015-08-11 21:35:55 +0000
1360+++ tests/unittests/qml/CMakeLists.txt 2015-09-17 15:38:35 +0000
1361@@ -28,6 +28,7 @@
1362 ${webbrowser-app_SOURCE_DIR}/limit-proxy-model.cpp
1363 ${webbrowser-app_SOURCE_DIR}/searchengine.cpp
1364 ${webbrowser-app_SOURCE_DIR}/tabs-model.cpp
1365+ ${webbrowser-app_SOURCE_DIR}/text-search-filter-model.cpp
1366 ${webbrowser-app_SOURCE_DIR}/top-sites-model.cpp
1367 tst_QmlTests.cpp
1368 )
1369
1370=== modified file 'tests/unittests/qml/tst_HistoryViewWide.qml'
1371--- tests/unittests/qml/tst_HistoryViewWide.qml 2015-08-18 13:58:19 +0000
1372+++ tests/unittests/qml/tst_HistoryViewWide.qml 2015-09-17 15:38:35 +0000
1373@@ -21,6 +21,7 @@
1374 import Ubuntu.Components 1.3
1375 import Ubuntu.Components.ListItems 1.3 as ListItems
1376 import Ubuntu.Test 1.0
1377+import webbrowsertest.private 0.1
1378 import "../../../src/app/webbrowser"
1379
1380 Item {
1381@@ -29,12 +30,24 @@
1382 width: 700
1383 height: 500
1384
1385- HistoryViewWide {
1386- id: historyViewWide
1387+ property var historyViewWide: historyViewWideLoader.item
1388+ property int ctrlFCaptured: 0
1389+
1390+ Keys.onPressed: {
1391+ if (event.modifiers === Qt.ControlModifier && event.key === Qt.Key_F)
1392+ ctrlFCaptured++
1393+ }
1394+
1395+ Loader {
1396+ id: historyViewWideLoader
1397 anchors.fill: parent
1398- historyModel: HistoryModel {
1399- id: historyMockModel
1400- databasePath: ":memory:"
1401+ active: false
1402+ focus: true
1403+ sourceComponent: HistoryViewWide {
1404+ focus: true
1405+ historyModel: HistoryModelMock {
1406+ databasePath: ":memory:"
1407+ }
1408 }
1409 }
1410
1411@@ -78,16 +91,40 @@
1412 }
1413
1414 function init() {
1415+ historyViewWideLoader.active = true
1416+ waitForRendering(historyViewWideLoader.item)
1417+
1418 for (var i = 0; i < 3; ++i) {
1419- historyMockModel.add("http://example.org/" + i, "Example Domain " + i, "")
1420+ historyViewWide.historyModel.add("http://example.org/" + i, "Example Domain " + i, "")
1421 }
1422 var urlsList = findChild(historyViewWide, "urlsListView")
1423+ waitForRendering(urlsList)
1424 tryCompare(urlsList, "count", 3)
1425- waitForRendering(urlsList)
1426 }
1427
1428 function cleanup() {
1429- historyMockModel.clearAll()
1430+ historyViewWideLoader.active = false
1431+ ctrlFCaptured = 0
1432+ }
1433+
1434+ function getListItems(name, itemName) {
1435+ var list = findChild(historyViewWide, name)
1436+ var items = []
1437+ if (list) {
1438+ // ensure all the delegates are created
1439+ list.cacheBuffer = list.count * 1000
1440+
1441+ // In some cases the ListView might add other children to the
1442+ // contentItem, so we filter the list of children to include
1443+ // only actual delegates
1444+ var children = list.contentItem.children
1445+ for (var i = 0; i < children.length; i++) {
1446+ if (children[i].objectName === itemName) {
1447+ items.push(children[i])
1448+ }
1449+ }
1450+ }
1451+ return items
1452 }
1453
1454 function test_done_button() {
1455@@ -168,28 +205,253 @@
1456 function test_keyboard_navigation_between_lists() {
1457 var lastVisitDateList = findChild(historyViewWide, "lastVisitDateListView")
1458 var urlsList = findChild(historyViewWide, "urlsListView")
1459- verify(!lastVisitDateList.activeFocus)
1460+ verify(urlsList.activeFocus)
1461 keyClick(Qt.Key_Left)
1462- verify(lastVisitDateList.activeFocus)
1463- verify(!urlsList.activeFocus)
1464+ verify(lastVisitDateList.activeFocus)
1465+ verify(!urlsList.activeFocus)
1466 keyClick(Qt.Key_Right)
1467- verify(urlsList.activeFocus)
1468+ verify(urlsList.activeFocus)
1469+ }
1470+
1471+ function test_search_button() {
1472+ var searchQuery = findChild(historyViewWide, "searchQuery")
1473+ verify(!searchQuery.visible)
1474+
1475+ var searchButton = findChild(historyViewWide, "searchButton")
1476+ verify(searchButton.visible)
1477+ clickItem(searchButton)
1478+ verify(!searchButton.visible)
1479+
1480+ verify(searchQuery.visible)
1481+ verify(searchQuery.activeFocus)
1482+ compare(searchQuery.text, "")
1483+
1484+ var urlsList = findChild(historyViewWide, "urlsListView")
1485+ compare(urlsList.count, 3)
1486+ typeString("2")
1487+ compare(urlsList.count, 1)
1488+
1489+ var backButton = findChild(historyViewWide, "backButton")
1490+ verify(backButton.visible)
1491+ clickItem(backButton)
1492+ verify(!backButton.visible)
1493+ verify(!searchQuery.visible)
1494+ verify(searchButton.visible)
1495+ compare(urlsList.count, 3)
1496+
1497+ clickItem(searchButton)
1498+ compare(searchQuery.text, "")
1499+ }
1500+
1501+ function test_keyboard_navigation_for_search() {
1502+ var urlsList = findChild(historyViewWide, "urlsListView")
1503+ verify(urlsList.activeFocus)
1504+ keyClick(Qt.Key_F, Qt.ControlModifier)
1505+
1506+ var searchQuery = findChild(historyViewWide, "searchQuery")
1507+ verify(searchQuery.activeFocus)
1508+
1509+ keyClick(Qt.Key_Escape)
1510+ verify(urlsList.activeFocus)
1511+
1512+ keyClick(Qt.Key_F, Qt.ControlModifier)
1513+ keyClick(Qt.Key_Down)
1514+ verify(urlsList.activeFocus)
1515+ keyClick(Qt.Key_Up)
1516+ verify(searchQuery.activeFocus)
1517+
1518+ keyClick(Qt.Key_Down)
1519+ keyClick(Qt.Key_Left)
1520+ keyClick(Qt.Key_Up)
1521+ verify(searchQuery.activeFocus)
1522+ }
1523+
1524+ function test_ctrl_f_during_search_returns_to_query() {
1525+ var urlsList = findChild(historyViewWide, "urlsListView")
1526+ var datesList = findChild(historyViewWide, "lastVisitDateListView")
1527+ var searchQuery = findChild(historyViewWide, "searchQuery")
1528+
1529+ verify(urlsList.activeFocus)
1530+ verify(!historyViewWide.searchMode)
1531+ keyClick(Qt.Key_F, Qt.ControlModifier)
1532+ verify(searchQuery.activeFocus)
1533+ verify(historyViewWide.searchMode)
1534+
1535+ // CTRL+F jumps back to the search box from the urls list...
1536+ keyClick(Qt.Key_Down)
1537+ verify(urlsList.activeFocus)
1538+ keyClick(Qt.Key_F, Qt.ControlModifier)
1539+ verify(searchQuery.activeFocus)
1540+
1541+ // ...and from the dates list
1542+ keyClick(Qt.Key_Down)
1543+ keyClick(Qt.Key_Left)
1544+ verify(datesList.activeFocus)
1545+ keyClick(Qt.Key_F, Qt.ControlModifier)
1546+ verify(searchQuery.activeFocus)
1547+ }
1548+
1549+ function test_ctrl_f_during_select_is_swallowed() {
1550+ var urlsList = findChild(historyViewWide, "urlsListView")
1551+ longPressItem(urlsList.children[0])
1552+ verify(historyViewWide.selectMode)
1553+
1554+ keyClick(Qt.Key_F, Qt.ControlModifier)
1555+ wait(50) // make sure event loop has processed
1556+ compare(ctrlFCaptured, 0)
1557+ verify(historyViewWide.selectMode)
1558+ }
1559+
1560+ function test_history_entry_activated_by_keyboard() {
1561+ var urlsList = findChild(historyViewWide, "urlsListView")
1562+ compare(urlsList.count, 3)
1563+ historyEntryClickedSpy.clear()
1564+ keyClick(Qt.Key_Enter)
1565+ compare(historyEntryClickedSpy.count, 1)
1566+ var args = historyEntryClickedSpy.signalArguments[0]
1567+ var entry = urlsList.model.get(2)
1568+ compare(String(args[0]), String(entry.url))
1569+
1570+ // now try the same during a search
1571+ historyEntryClickedSpy.clear()
1572+ keyClick(Qt.Key_F, Qt.ControlModifier)
1573+ typeString("dom")
1574+ keyClick(Qt.Key_Down)
1575+ keyClick(Qt.Key_Enter)
1576+ compare(historyEntryClickedSpy.count, 1)
1577+ args = historyEntryClickedSpy.signalArguments[0]
1578+ entry = urlsList.model.get(0)
1579+ compare(String(args[0]), String(entry.url))
1580+ }
1581+
1582+ function test_search_highlight() {
1583+ function wraphtml(text) { return "<html>%1</html>".arg(text) }
1584+ function highlight(term) {
1585+ return "<font color=\"%1\">%2</font>".arg("#752571").arg(term)
1586+ }
1587+
1588+ var searchButton = findChild(historyViewWide, "searchButton")
1589+ var searchQuery = findChild(historyViewWide, "searchQuery")
1590+ var urlsList = findChild(historyViewWide, "urlsListView")
1591+ clickItem(searchButton)
1592+
1593+ var term = "2"
1594+ typeString(term)
1595+ var items = getListItems("urlsListView", "historyDelegate")
1596+ compare(items.length, 1)
1597+ compare(items[0].title, wraphtml("Example Domain " + highlight(term)))
1598+
1599+ var backButton = findChild(historyViewWide, "backButton")
1600+ clickItem(backButton)
1601+ clickItem(searchButton)
1602+
1603+ var terms = ["1", "Example"]
1604+ typeString(terms.join(" "))
1605+ items = getListItems("urlsListView", "historyDelegate")
1606+ compare(items.length, 1)
1607+ compare(items[0].title, wraphtml("%1 Domain %0"
1608+ .arg(highlight(terms[0]))
1609+ .arg(highlight(terms[1]))))
1610+ }
1611+
1612+ function test_search_updates_dates_list() {
1613+ function getDateItem(date) {
1614+ var dates = getListItems("lastVisitDateListView", "lastVisitDateDelegate")
1615+ var items = dates.filter(function(item) {
1616+ return item.lastVisitDate.valueOf() === date.valueOf()
1617+ })
1618+ if (items.length > 0) return items.pop()
1619+ else return null
1620+ }
1621+ function returnToDatesList() {
1622+ keyClick(Qt.Key_Down)
1623+ keyClick(Qt.Key_Left)
1624+ }
1625+
1626+ var searchQuery = findChild(historyViewWide, "searchQuery")
1627+ var today = new Date()
1628+ today = new Date(today.getFullYear(), today.getMonth(), today.getDate())
1629+ var youngest = new Date(1912, 6, 23)
1630+ var model = historyViewWide.historyModel
1631+ model.addByDate("https://en.wikipedia.org/wiki/Alan_Turing", "Alan Turing", youngest)
1632+ model.addByDate("https://en.wikipedia.org/wiki/Alonzo_Church", "Alonzo Church", new Date(1903, 6, 14))
1633+
1634+ var lastVisitDateList = findChild(historyViewWide, "lastVisitDateListView")
1635+ var dates = getListItems("lastVisitDateListView", "lastVisitDateDelegate")
1636+ var urls = getListItems("urlsListView", "historyDelegate")
1637+ compare(dates.length, 4)
1638+ compare(urls.length, 5)
1639+
1640+ // select a date that has search results in it and verify it is
1641+ // still the currently selected one after the search.
1642+ var testItem = getDateItem(youngest)
1643+ clickItem(testItem)
1644+ verify(testItem.activeFocus)
1645+ keyClick(Qt.Key_F, Qt.ControlModifier)
1646+ typeString("Alan")
1647+ urls = getListItems("urlsListView", "historyDelegate")
1648+ compare(urls.length, 1)
1649+ returnToDatesList()
1650+ verify(testItem.activeFocus)
1651+
1652+ // change the search terms so that it will display more items, but
1653+ // since we have a selected date, we will see only one
1654+ keyClick(Qt.Key_F, Qt.ControlModifier)
1655+ keyClick(Qt.Key_Backspace)
1656+ keyClick(Qt.Key_Backspace)
1657+ compare(searchQuery.text, "Al")
1658+ returnToDatesList()
1659+ verify(testItem.activeFocus)
1660+ urls = getListItems("urlsListView", "historyDelegate")
1661+ compare(urls.length, 1)
1662+
1663+ // change the search terms so that the current date will not be
1664+ // included in the results
1665+ keyClick(Qt.Key_F, Qt.ControlModifier)
1666+ typeString("onzo")
1667+ urls = getListItems("urlsListView", "historyDelegate")
1668+ compare(urls.length, 1)
1669+ compare(searchQuery.text, "Alonzo")
1670+ returnToDatesList()
1671+ testItem = getDateItem(youngest)
1672+ compare(testItem, null)
1673+
1674+ // verify that the current item has reverted to the first in the
1675+ // dates list ("all dates")
1676+ compare(lastVisitDateList.currentIndex, 0)
1677+
1678+ // if widen the search again now, we should see both results again
1679+ keyClick(Qt.Key_F, Qt.ControlModifier)
1680+ keyClick(Qt.Key_Backspace)
1681+ keyClick(Qt.Key_Backspace)
1682+ keyClick(Qt.Key_Backspace)
1683+ keyClick(Qt.Key_Backspace)
1684+ compare(searchQuery.text, "Al")
1685+ urls = getListItems("urlsListView", "historyDelegate")
1686+ compare(urls.length, 2)
1687 }
1688
1689 function test_delete_key_at_urls_list_view() {
1690 var urlsList = findChild(historyViewWide, "urlsListView")
1691 keyClick(Qt.Key_Right)
1692- verify(urlsList.activeFocus)
1693+ verify(urlsList.activeFocus)
1694 compare(urlsList.count, 3)
1695 keyClick(Qt.Key_Delete)
1696 compare(urlsList.count, 2)
1697+
1698+ // now try the same while in a search
1699+ keyClick(Qt.Key_F, Qt.ControlModifier)
1700+ typeString("dom")
1701+ keyClick(Qt.Key_Down)
1702+ keyClick(Qt.Key_Delete)
1703+ compare(urlsList.count, 1)
1704 }
1705
1706 function test_delete_key_at_last_visit_date() {
1707 var lastVisitDateList = findChild(historyViewWide, "lastVisitDateListView")
1708 var urlsList = findChild(historyViewWide, "urlsListView")
1709 keyClick(Qt.Key_Left)
1710- verify(lastVisitDateList.activeFocus)
1711+ verify(lastVisitDateList.activeFocus)
1712 compare(lastVisitDateList.currentIndex, 0)
1713 keyClick(Qt.Key_Down)
1714 compare(lastVisitDateList.currentIndex, 1)
1715@@ -202,7 +464,7 @@
1716 var lastVisitDateList = findChild(historyViewWide, "lastVisitDateListView")
1717 var urlsList = findChild(historyViewWide, "urlsListView")
1718 keyClick(Qt.Key_Left)
1719- verify(lastVisitDateList.activeFocus)
1720+ verify(lastVisitDateList.activeFocus)
1721 compare(lastVisitDateList.currentIndex, 0)
1722 compare(urlsList.count, 3)
1723 keyClick(Qt.Key_Delete)
1724
1725=== modified file 'tests/unittests/qml/tst_QmlTests.cpp'
1726--- tests/unittests/qml/tst_QmlTests.cpp 2015-08-11 21:35:55 +0000
1727+++ tests/unittests/qml/tst_QmlTests.cpp 2015-09-17 15:38:35 +0000
1728@@ -36,6 +36,7 @@
1729 #include "limit-proxy-model.h"
1730 #include "searchengine.h"
1731 #include "tabs-model.h"
1732+#include "text-search-filter-model.h"
1733 #include "top-sites-model.h"
1734
1735 static QObject* FileOperations_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine)
1736@@ -109,6 +110,41 @@
1737 QTemporaryDir m_testDir2;
1738 };
1739
1740+class HistoryModelMock : public HistoryModel {
1741+ Q_OBJECT
1742+
1743+public:
1744+ static bool compareHistoryEntries(const HistoryEntry& a, const HistoryEntry& b) {
1745+ return a.lastVisit < b.lastVisit;
1746+ }
1747+
1748+ Q_INVOKABLE int addByDate(const QUrl& url, const QString& title, const QDateTime& date)
1749+ {
1750+ int index = getEntryIndex(url);
1751+ int visitsToAdd = 1;
1752+ if (index == -1) {
1753+ add(url, title, QString());
1754+ index = getEntryIndex(url);
1755+ visitsToAdd = 0;
1756+ }
1757+
1758+ // Since this is useful only for testing and efficiency is not critical
1759+ // we reorder the model and reset it every time we add a new item by date
1760+ // to keep things simple.
1761+ beginResetModel();
1762+ HistoryEntry entry = m_entries.takeAt(index);
1763+ entry.lastVisit = date;
1764+ entry.visits = entry.visits + visitsToAdd;
1765+ m_entries.append(entry);
1766+ std::sort(m_entries.begin(), m_entries.end(), compareHistoryEntries);
1767+ endResetModel();
1768+
1769+ updateExistingEntryInDatabase(entry);
1770+
1771+ return entry.visits;
1772+ }
1773+};
1774+
1775 static QObject* TestContext_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine)
1776 {
1777 Q_UNUSED(engine);
1778@@ -131,10 +167,13 @@
1779 qmlRegisterType<HistoryLastVisitDateListModel>(browserUri, 0, 1, "HistoryLastVisitDateListModel");
1780 qmlRegisterType<HistoryLastVisitDateModel>(browserUri, 0, 1, "HistoryLastVisitDateModel");
1781 qmlRegisterType<LimitProxyModel>(browserUri, 0, 1, "LimitProxyModel");
1782+ qmlRegisterType<TextSearchFilterModel>(browserUri, 0, 1, "TextSearchFilterModel");
1783 qmlRegisterType<TopSitesModel>(browserUri, 0, 1, "TopSitesModel");
1784 qmlRegisterSingletonType<FileOperations>(browserUri, 0, 1, "FileOperations", FileOperations_singleton_factory);
1785
1786- qmlRegisterSingletonType<TestContext>("webbrowsertest.private", 0, 1, "TestContext", TestContext_singleton_factory);
1787+ const char* testUri = "webbrowsertest.private";
1788+ qmlRegisterSingletonType<TestContext>(testUri, 0, 1, "TestContext", TestContext_singleton_factory);
1789+ qmlRegisterType<HistoryModelMock>(testUri, 0, 1, "HistoryModelMock");
1790
1791 return quick_test_main(argc, argv, "QmlTests", nullptr);
1792 }
1793
1794=== renamed directory 'tests/unittests/suggestions-filter-model' => 'tests/unittests/text-search-filter-model'
1795=== modified file 'tests/unittests/text-search-filter-model/CMakeLists.txt'
1796--- tests/unittests/suggestions-filter-model/CMakeLists.txt 2015-06-22 10:29:20 +0000
1797+++ tests/unittests/text-search-filter-model/CMakeLists.txt 2015-09-17 15:38:35 +0000
1798@@ -1,8 +1,8 @@
1799 find_package(Qt5Core REQUIRED)
1800 find_package(Qt5Sql REQUIRED)
1801 find_package(Qt5Test REQUIRED)
1802-set(TEST tst_SuggestionsFilterModelTests)
1803-add_executable(${TEST} tst_SuggestionsFilterModelTests.cpp)
1804+set(TEST tst_TextSearchFilterModelTests)
1805+add_executable(${TEST} tst_TextSearchFilterModelTests.cpp)
1806 include_directories(${webbrowser-app_SOURCE_DIR})
1807 target_link_libraries(${TEST}
1808 Qt5::Core
1809
1810=== renamed file 'tests/unittests/suggestions-filter-model/tst_SuggestionsFilterModelTests.cpp' => 'tests/unittests/text-search-filter-model/tst_TextSearchFilterModelTests.cpp'
1811--- tests/unittests/suggestions-filter-model/tst_SuggestionsFilterModelTests.cpp 2015-04-16 18:45:31 +0000
1812+++ tests/unittests/text-search-filter-model/tst_TextSearchFilterModelTests.cpp 2015-09-17 15:38:35 +0000
1813@@ -1,5 +1,5 @@
1814 /*
1815- * Copyright 2013 Canonical Ltd.
1816+ * Copyright 2013-2015 Canonical Ltd.
1817 *
1818 * This file is part of webbrowser-app.
1819 *
1820@@ -22,22 +22,22 @@
1821
1822 // local
1823 #include "history-model.h"
1824-#include "suggestions-filter-model.h"
1825+#include "text-search-filter-model.h"
1826
1827-class SuggestionsFilterModelTests : public QObject
1828+class TextSearchFilterModelTests : public QObject
1829 {
1830 Q_OBJECT
1831
1832 private:
1833 HistoryModel* model;
1834- SuggestionsFilterModel* matches;
1835+ TextSearchFilterModel* matches;
1836
1837 private Q_SLOTS:
1838 void init()
1839 {
1840 model = new HistoryModel;
1841 model->setDatabasePath(":memory:");
1842- matches = new SuggestionsFilterModel;
1843+ matches = new TextSearchFilterModel;
1844 matches->setSourceModel(QVariant::fromValue(model));
1845 }
1846
1847@@ -67,11 +67,17 @@
1848 delete model2;
1849 }
1850
1851- void shouldBeEmptyWhileTermsAndFieldsEmpty()
1852+ void shouldReturnAllWhileTermsAndOrFieldsEmpty()
1853 {
1854 model->add(QUrl("http://example.org"), "Example Domain", QUrl());
1855 model->add(QUrl("http://example.com"), "Example Domain", QUrl());
1856- QCOMPARE(matches->rowCount(), 0);
1857+ QCOMPARE(matches->rowCount(), 2);
1858+ matches->setTerms(QStringList({"org"}));
1859+ QCOMPARE(matches->rowCount(), 2);
1860+ matches->setSearchFields(QStringList({"url"}));
1861+ QCOMPARE(matches->rowCount(), 1);
1862+ matches->setTerms(QStringList());
1863+ QCOMPARE(matches->rowCount(), 2);
1864 }
1865
1866 void shouldRecordTerms()
1867@@ -132,8 +138,6 @@
1868 QCOMPARE(matches->rowCount(), 2);
1869 matches->setTerms(QStringList({"wiki"}));
1870 QCOMPARE(matches->rowCount(), 1);
1871- matches->setTerms(QStringList());
1872- QCOMPARE(matches->rowCount(), 0);
1873 }
1874
1875 void shouldUpdateResultsWhenSourceModelUpdates()
1876@@ -184,5 +188,5 @@
1877 }
1878 };
1879
1880-QTEST_MAIN(SuggestionsFilterModelTests)
1881-#include "tst_SuggestionsFilterModelTests.moc"
1882+QTEST_MAIN(TextSearchFilterModelTests)
1883+#include "tst_TextSearchFilterModelTests.moc"

Subscribers

People subscribed via source and target branches

to status/vote changes: