Merge lp:~uriboni/webbrowser-app/topsite-previews into lp:webbrowser-app

Proposed by Ugo Riboni
Status: Merged
Approved by: Olivier Tilloy
Approved revision: 1204
Merged at revision: 1228
Proposed branch: lp:~uriboni/webbrowser-app/topsite-previews
Merge into: lp:webbrowser-app
Diff against target: 1917 lines (+957/-198)
25 files modified
src/app/webbrowser/Browser.qml (+37/-29)
src/app/webbrowser/BrowserTab.qml (+23/-19)
src/app/webbrowser/CMakeLists.txt (+1/-1)
src/app/webbrowser/HistoryModel.qml (+0/-24)
src/app/webbrowser/HistoryView.qml (+12/-4)
src/app/webbrowser/HistoryViewWide.qml (+19/-10)
src/app/webbrowser/NewTabView.qml (+55/-18)
src/app/webbrowser/NewTabViewWide.qml (+15/-28)
src/app/webbrowser/PreviewManager.qml (+91/-0)
src/app/webbrowser/SettingsPage.qml (+2/-3)
src/app/webbrowser/UrlPreviewDelegate.qml (+123/-0)
src/app/webbrowser/UrlPreviewGrid.qml (+90/-0)
src/app/webbrowser/file-operations.cpp (+7/-0)
src/app/webbrowser/file-operations.h (+3/-0)
src/app/webbrowser/qmldir (+1/-0)
src/app/webbrowser/webbrowser-app.cpp (+8/-1)
tests/autopilot/webbrowser_app/emulators/browser.py (+41/-8)
tests/autopilot/webbrowser_app/tests/__init__.py (+12/-6)
tests/autopilot/webbrowser_app/tests/test_new_tab_view.py (+5/-6)
tests/autopilot/webbrowser_app/tests/test_site_previews.py (+168/-0)
tests/unittests/qml/tst_BrowserTab.qml (+64/-1)
tests/unittests/qml/tst_HistoryViewWide.qml (+12/-6)
tests/unittests/qml/tst_NewTabViewWide.qml (+27/-32)
tests/unittests/qml/tst_PreviewManager.qml (+117/-0)
tests/unittests/qml/tst_QmlTests.cpp (+24/-2)
To merge this branch: bzr merge lp:~uriboni/webbrowser-app/topsite-previews
Reviewer Review Type Date Requested Status
Olivier Tilloy Approve
PS Jenkins bot continuous-integration Needs Fixing
Review via email: mp+269771@code.launchpad.net

Commit message

Reimplement the top sites list to use a grid of previews in all form factors.

Description of the change

Reimplement the top sites list to use a grid of previews in all form factors.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

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

Additionally, this branch has conflicts when merging into the latest trunk. Can you please resolve them?

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

A first incomplete round of (mostly) functional review, with comments in no particular order:

In UrlPreviewDelegate.hide_from_history(), the comment mentions bug #1205144, which has been fixed in the UITK, so the hack can now be cleaned up. Same in test_new_tab_view.py.

When launching the app, I’m seeing the following error in the console:
  src/app/webbrowser/Browser.qml:377:30: Unable to assign [undefined] to bool

When opening a new tab, I’m seeing the following error in the console:
  src/app/webbrowser/Browser.qml:518: ReferenceError: UrlUtils is not defined

I don’t think this is a bug introduced by your changes, rather an issue that’s always been there, but it is now more visible: initially, the captures for my top sites grid are blank (expected). If from there I click on one top site to open it, wait for the page to load, then close the tab, and open a new tab again, there is no capture for the page that I just viewed. This is because a capture is not made when closing a tab (only when switching tabs). Not sure how much work this would require to implement, but worth taking a look anyway.

On a related note, can captures be removed when closing tabs? I thought this was implemented already, but it doesn’t seem to work.

The captures in the grid view look very blurry, is this because of downscaling, and if so can we do something to improve it?

In narrow mode, when dragging the grid view upwards, it overlaps with the list of bookmarks above. It needs to be clipped.

The context menu that appears on grid items when long-pressing/right-clicking can be dismissed when clicked outside, but the click is propagated to whatever’s underneath, so if I click anywhere to dismiss the menu I might activate a top site.

Not a big deal as (AFAIK) keyboard navigation was never implemented in narrow views, but since the grid view handles it correctly, can the narrow new tab view be fixed to fully support keyboard nav? If it would require too many changes, I’m fine with leaving it for another branch.

Revision history for this message
Olivier Tilloy (osomon) wrote :

I expressed a concern about app startup performance for the branch where you made the bookmarks model a singleton, and this holds true for the changes done to the history model in this branch. Probably even more so, because the history model is likely to grow substantially over time. Please do some profiling to confirm/invalidate.

In BrowserTab.qml, there are extraneous semi-colons at line ends in JS code, please remove them for consistency.

Revision history for this message
Olivier Tilloy (osomon) wrote :

There used to be a good reason for making the name of the preview file a unique ID as opposed to a hash of the URL, but at the moment I fail to remember that good reason. The relevant revision is http://bazaar.launchpad.net/~phablet-team/webbrowser-app/trunk/revision/823.1.16, unfortunately it doesn’t elaborate on why this was done. I’ll keep thinking about it, I’m pretty sure using the URL was not a good idea for some reason.

Revision history for this message
Olivier Tilloy (osomon) wrote :

I think I remember now why using a hash of the URl wasn’t a good idea: if in a given tab I browse to n different URLs (either by entering a new address or by clicking on links), and if in between each new navigation I switch to another tab and back, this is going to generate n different previews, which are not deleted until the next time the browser is started. The preview cache can easily grow big with unused previews that way, and this is not desirable on devices with a limited amount of storage.

Could the PreviewManager be made more flexible by accepting any string identifier instead of a URL? Previews for top sites could use the corresponding URL, while previews for tabs would use the tab’s unique ID.

Revision history for this message
Olivier Tilloy (osomon) wrote :

In BrowserTab.qml, 'topSites' and 'cachedPreviewUpdated' appear to be unused, can they be removed?

In HistoryViewWide.qml, the diff introduces an extraneous blank line before lastVisitDateDelegate’s onClicked handler, can this be reverted?

In NewTabView.qml, the comment to explain why there is a negative right margin makes sense, but instead of hardcoding it, couldn’t it be bound to "-contentColumn.anchors.rightMargin" ?

In UrlPreviewGrid.qml, do you really need to implement keyboard navigation yourself? I thought GridView already implemented it.

PreviewManager.qml should import Ubuntu.Web 0.2, as it has a reference to cacheLocation.

src/app/webbrowser/qmldir needs to be installed by src/app/webbrowser/CMakeLists.txt

In tests/autopilot/webbrowser_app/tests/__init__.py, it doesn’t look like the result of launch_app() is used anywhere, so the return value could be suppressed.

In test_cleanup_previews_on_startup, is the sleep really necessary? If so, how can we be sure that 0.5 seconds is enough?

In tst_BrowserTab.qml, is the additional import of Ubuntu.Test really needed?

3 out of the 4 tests in webbrowser_app.tests.test_site_previews fail in narrow mode because they silently assume wide mode (in narrow mode, before calling self.open_new_tab(), the test needs to call self.open_tabs_view()).

review: Needs Fixing
Revision history for this message
Ugo Riboni (uriboni) wrote :

> A first incomplete round of (mostly) functional review, with comments in no
> particular order:

Fixed unless noted below:

> In UrlPreviewDelegate.hide_from_history(), the comment mentions bug #1205144,
> which has been fixed in the UITK, so the hack can now be cleaned up. Same in
> test_new_tab_view.py.

We still can't use the CPO's method to click on an action because of another bug.
We can however get the action button by objectName, so I fixed that at least.

> I don’t think this is a bug introduced by your changes, rather an issue that’s
> always been there, but it is now more visible: initially, the captures for my
> top sites grid are blank (expected). If from there I click on one top site to
> open it, wait for the page to load, then close the tab, and open a new tab
> again, there is no capture for the page that I just viewed. This is because a
> capture is not made when closing a tab (only when switching tabs). Not sure
> how much work this would require to implement, but worth taking a look anyway.

The other case when the preview is not captured is when we navigate somewhere else, and while we might be able to delay the unloading of the tab, I don't know if we can't easily delay navigating away on a link click.

This idea of delaying hiding the tab is also making me a bit nervous anyway. Would it be too expensive CPU-wise to periodically call grabToImage without saving to disk, and saving to disk only when the tab stops being visible or the URL changes ?

> On a related note, can captures be removed when closing tabs? I thought this
> was implemented already, but it doesn’t seem to work.

It is implemented and I tested it again and it works here.
Can you give me steps to reproduce this problem ?
Please keep in mind that when closing a tab, if the site is in the top sites its preview will not be deleted.

> The context menu that appears on grid items when long-pressing/right-clicking
> can be dismissed when clicked outside, but the click is propagated to
> whatever’s underneath, so if I click anywhere to dismiss the menu I might
> activate a top site.

This sounds like a bug in the ActionSelectionPopover not setting up the exclusion area correctly.
I will investigate more and file a bug.

> Not a big deal as (AFAIK) keyboard navigation was never implemented in narrow
> views, but since the grid view handles it correctly, can the narrow new tab
> view be fixed to fully support keyboard nav? If it would require too many
> changes, I’m fine with leaving it for another branch.

The real problem with the narrow mode version is that it uses a Column+Repeater setup. I plan to convert that into a proper ListView to get better performance, and keyboard navigation will work almost "for free" after that.
So let's keep it for that other MR.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Ugo Riboni (uriboni) wrote :

> In UrlPreviewGrid.qml, do you really need to implement keyboard navigation
> yourself? I thought GridView already implemented it.

What it does not implement is telling me when we are on item zero and pressing LEFT or when we are on any item on row zero and pressing UP. In these cases we need to signal that we want to give up focus so that it can go back to the address bar.
But the handlers for DOWN and RIGHT were indeed redundant so I removed them.

> PreviewManager.qml should import Ubuntu.Web 0.2, as it has a reference to
> cacheLocation.

I guess the reason why this worked anyway was that the context object exported by the Ubuntu.Web plugin is attached the first time the plugin is imported, and then accessible globally since all QML files in webbrowser-app share the same context.

I personally think that we should re-think our usage of context properties and context objects. They make things harder to unit test, since there does not seem to be a way to set up the same environment (with mocks replacing the original context properties) while setting up a unit test. Wouldn't it be better if instead the plugins would export their globals as part of explicit singletons, which can then easily be extended and/or replaced while unit testing ?
Referencing cacheLocation or globals.cacheLocations or something similarly explicit isn't such a major change in the app, compared to the benefits we get.

Anyway, fixed here, but we should discuss this.

> In test_cleanup_previews_on_startup, is the sleep really necessary? If so, how
> can we be sure that 0.5 seconds is enough?

We really can't. Like all tests that are checking that something does *not* happen, we have to either make a guess, or just decide that we can't test this particular feature.
I am happy to take your guess over mine, if you want to suggest a different value, or have a more rationally explainable value to propose.

> 3 out of the 4 tests in webbrowser_app.tests.test_site_previews fail in narrow
> mode because they silently assume wide mode (in narrow mode, before calling
> self.open_new_tab(), the test needs to call self.open_tabs_view()).

In the process of fixing this

Revision history for this message
Olivier Tilloy (osomon) wrote :

In narrow mode, the highlight for top site previews is cut off on the left hand side for the leftmost column.

Also, keyboard navigation with arrows doesn’t ensure that the currently highlighted preview is visible (if e.g. the last row is out of sight and I navigate down to it with the down arrow key, it should come into view).

review: Needs Fixing
Revision history for this message
Olivier Tilloy (osomon) wrote :

I can reliably reproduce the following issue:

 - ensure that webbrowser-app is not running
 - rm $HOME/.cache/webbrowser-app/captures/*
 - launch webbrowser-app, open a new tab, verify that there are no captures
 - open each top site in a new tab, and ensure that they all have a capture displayed
 - close all tabs, when the last tab is closed, the app quits
 - verify that you have (at least) 10 captures in $HOME/.cache/webbrowser-app/captures
 - launch webbrowser-app again, and open a new tab
 - the captures are gone, for some (wrong) reason they’ve been purged from the cache

review: Needs Fixing
Revision history for this message
Olivier Tilloy (osomon) wrote :

Revision 1195 fixes the issue indeed, but is a bit convoluted. Given that the session restore code is already itself triggered by a Timer with a 1msec interval to ensure it’s out of the critical startup path, maybe the databasePath of the history model could be set in that same timer’s onTriggered (after restoring the open tabs). That would avoid the need for a 'sessionRestoreComplete' property and the getOpenPages() and initialCapturesCleanup() functions.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Ugo Riboni (uriboni) wrote :

> Revision 1195 fixes the issue indeed, but is a bit convoluted. Given that the
> session restore code is already itself triggered by a Timer with a 1msec
> interval to ensure it’s out of the critical startup path, maybe the
> databasePath of the history model could be set in that same timer’s
> onTriggered (after restoring the open tabs). That would avoid the need for a
> 'sessionRestoreComplete' property and the getOpenPages() and
> initialCapturesCleanup() functions.

I fixed this but we still need getOpenPages because the open tabs are a combination of restored tabs and initialTabs (which I think come from the command line)

Revision history for this message
Ugo Riboni (uriboni) wrote :

> In narrow mode, the highlight for top site previews is cut off on the left
> hand side for the leftmost column.

Was due to clipping. Fixed.

> Also, keyboard navigation with arrows doesn’t ensure that the currently
> highlighted preview is visible (if e.g. the last row is out of sight and I
> navigate down to it with the down arrow key, it should come into view).

The issue here is that we are using a flickable around the GridView, and the GridView height equals its contentHeight, so as far as the GridView is concerned there is nothing to scroll.

I will fix this when adding full support for keyboard nav to the narrow view list (and replacing columns+repeaters with listviews) in a separate MR.

Revision history for this message
Olivier Tilloy (osomon) wrote :

> I will fix this when adding full support for keyboard nav to the narrow view
> list (and replacing columns+repeaters with listviews) in a separate MR.

Okay.

Revision history for this message
Olivier Tilloy (osomon) wrote :

A couple of minor comments, we’re definitely getting there!

 - Given that internal.getOpenPages() is called in only one place, its usefulness as a function is low. Can you inline that code in the onTriggered implementation of the timer?

 - The interactive property of the grid view in narrow mode should be set to false, otherwise if I start flicking on the grid only the grid moves, whereas I would expect the entire page to move.

I haven’t tested on a device yet (to ensure no functional regressions in narrow mode on touch), will do when CI generates packages for the latest revision.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

Testing on arale, and I can confirm that the fact that the gridview is flickable inside the containing flickable is annoying, if I scroll all the way down I cannot scroll back up to see the list of bookmarks.

I’m also seeing that the capture images are vertically centered inside the ubuntu shape, which sometimes makes it hard to identify a page by its capture. An extreme example is http://example.org, where all the content is at the top of the page, and by centering the capture the grid item ends up completely white, as the content is clipped off at the top. A similar situation happens with http://google.com, where the preview shows only the links below the search box.
I would expect the captures to be top-aligned, I think it’s generally easier to recognize a page when seeing its topmost part.

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :

LGTM now, thanks!

review: Approve
Revision history for this message
Olivier Tilloy (osomon) wrote :

webbrowser_app.tests.test_new_tab_view.TestNewTabViewContentsWide.test_remove_top_sites is failing on desktop, please fix it.

review: Needs Fixing
Revision history for this message
Olivier Tilloy (osomon) :
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-28 08:15:10 +0000
3+++ src/app/webbrowser/Browser.qml 2015-10-13 11:21:13 +0000
4@@ -25,8 +25,9 @@
5 import webbrowserapp.private 0.1
6 import webbrowsercommon.private 0.1
7 import "../actions" as Actions
8+import "../UrlUtils.js" as UrlUtils
9 import ".."
10-import "../UrlUtils.js" as UrlUtils
11+import "."
12
13 BrowserView {
14 id: browser
15@@ -36,7 +37,6 @@
16
17 currentWebview: tabsModel && tabsModel.currentTab ? tabsModel.currentTab.webview : null
18
19- property var historyModel: (historyModelLoader.status == Loader.Ready) ? historyModelLoader.item : null
20 property var bookmarksModel: (bookmarksModelLoader.status == Loader.Ready) ? bookmarksModelLoader.item : null
21
22 property bool newSession: false
23@@ -99,8 +99,7 @@
24 onTriggered: browser.openUrlInNewTab("", true)
25 },
26 Actions.ClearHistory {
27- enabled: browser.historyModel
28- onTriggered: browser.historyModel.clearAll()
29+ onTriggered: HistoryModel.clearAll()
30 },
31 Actions.FindInPage {
32 enabled: !chrome.findInPageMode && !newTabViewLoader.active
33@@ -242,7 +241,6 @@
34
35 NewTabView {
36 anchors.fill: parent
37- historyModel: browser.historyModel
38 bookmarksModel: browser.bookmarksModel
39 settingsObject: settings
40 focus: true
41@@ -265,7 +263,6 @@
42
43 NewTabViewWide {
44 anchors.fill: parent
45- historyModel: browser.historyModel
46 bookmarksModel: browser.bookmarksModel
47 settingsObject: settings
48 focus: true
49@@ -374,7 +371,6 @@
50 objectName: "history"
51 text: i18n.tr("History")
52 iconName: "history"
53- enabled: browser.historyModel
54 onTriggered: historyViewLoader.active = true
55 },
56 Action {
57@@ -492,7 +488,7 @@
58 readonly property string icon: "history"
59 readonly property bool displayUrl: true
60 sourceModel: TextSearchFilterModel {
61- sourceModel: browser.historyModel
62+ sourceModel: HistoryModel
63 terms: suggestionsList.searchTerms
64 searchFields: ["url", "title"]
65 }
66@@ -771,7 +767,7 @@
67
68 onStatusChanged: {
69 if (status == Loader.Ready) {
70- historyViewTimer.restart()
71+ historyViewLoader.item.loadModel()
72 historyViewLoader.item.forceActiveFocus()
73 } else {
74 internal.resetFocus()
75@@ -782,14 +778,6 @@
76
77 onActiveChanged: if (active) chrome.findInPageMode = false
78
79- Timer {
80- id: historyViewTimer
81- // Set the model asynchronously to ensure
82- // the view is displayed as early as possible.
83- interval: 1
84- onTriggered: historyViewLoader.item.historyModel = browser.historyModel
85- }
86-
87 Component {
88 id: historyViewComponent
89
90@@ -822,7 +810,7 @@
91 if (count == 1) {
92 done()
93 }
94- browser.historyModel.removeEntryByUrl(url)
95+ HistoryModel.removeEntryByUrl(url)
96 }
97 onDone: destroy()
98 }
99@@ -846,6 +834,7 @@
100 browser.openUrlInNewTab(url, true)
101 done()
102 }
103+
104 onNewTabRequested: browser.openUrlInNewTab("", true)
105 onDone: historyViewLoader.active = false
106 }
107@@ -864,7 +853,6 @@
108 SettingsPage {
109 anchors.fill: parent
110 focus: true
111- historyModel: browser.historyModel
112 settingsObject: settings
113 onDone: destroy()
114 Keys.onEscapePressed: {
115@@ -901,12 +889,6 @@
116 }
117
118 Loader {
119- id: historyModelLoader
120- source: "HistoryModel.qml"
121- asynchronous: true
122- }
123-
124- Loader {
125 id: bookmarksModelLoader
126 source: "BookmarksModel.qml"
127 asynchronous: true
128@@ -1128,8 +1110,9 @@
129 return
130 }
131
132- if ((event.type == Oxide.LoadEvent.TypeSucceeded) && browser.historyModel && 300 > event.httpStatusCode && event.httpStatusCode >= 200) {
133- browser.historyModel.add(event.url, title, icon)
134+ if (event.type == Oxide.LoadEvent.TypeSucceeded &&
135+ 300 > event.httpStatusCode && event.httpStatusCode >= 200) {
136+ HistoryModel.add(event.url, title, icon)
137 }
138 }
139
140@@ -1227,6 +1210,15 @@
141 QtObject {
142 id: internal
143
144+ function getOpenPages() {
145+ var urls = [];
146+ for (var i = 0; i < tabsModel.count; i++) {
147+ var url = tabsModel.get(i).url
148+ if (url.length > 0) urls.push(url) // exclude "new tab" tabs
149+ }
150+ return urls;
151+ }
152+
153 function instantiateShareComponent() {
154 var component = Qt.createComponent("../Share.qml")
155 if (component.status == Component.Ready) {
156@@ -1490,8 +1482,18 @@
157 }
158 }
159
160- // Delay instantiation of the first webview by 1 msec to allow initial
161- // rendering to happen. Clumsy workaround for http://pad.lv/1359911.
162+ // Schedule various expensive tasks to a point after the initialization and
163+ // first rendering of the application have already happened.
164+ //
165+ // Scheduling a Timer with the shortest non-zero interval possible (1ms) will
166+ // effectively queue its onTriggered function to run immediately after anything
167+ // that is currently in the event loop queue at the moment the Timer starts.
168+ //
169+ // The tasks are:
170+ // - creating the webviews for all initial tabs. This should ideally be done
171+ // asynchronously via object incubation, but http://pad.lv/1359911 prevents it
172+ // - loading the HistoryModel from the database
173+ // - deleting any page screenshots that are no longer needed
174 Timer {
175 running: true
176 interval: 1
177@@ -1499,6 +1501,7 @@
178 if (!browser.newSession && settings.restoreSession) {
179 session.restore()
180 }
181+
182 // Sanity check
183 console.assert(tabsModel.count <= browser.maxTabsToRestore,
184 "WARNING: too many tabs were restored")
185@@ -1512,6 +1515,11 @@
186 if (!tabsModel.currentTab.url.toString() && !tabsModel.currentTab.restoreState && (formFactor == "desktop")) {
187 internal.focusAddressBar()
188 }
189+
190+ HistoryModel.databasePath = dataLocation + "/history.sqlite"
191+ // Note that the property setter for databasePath won't return until
192+ // the entire model has been loaded, so it is safe to call this here
193+ PreviewManager.cleanUnusedPreviews(internal.getOpenPages())
194 }
195 }
196
197
198=== modified file 'src/app/webbrowser/BrowserTab.qml'
199--- src/app/webbrowser/BrowserTab.qml 2015-09-23 15:53:01 +0000
200+++ src/app/webbrowser/BrowserTab.qml 2015-10-13 11:21:13 +0000
201@@ -20,6 +20,7 @@
202 import Ubuntu.Web 0.2
203 import com.canonical.Oxide 1.4 as Oxide
204 import webbrowserapp.private 0.1
205+import "."
206
207 FocusScope {
208 id: tab
209@@ -40,6 +41,19 @@
210 property bool current: false
211 property bool incognito
212
213+ Connections {
214+ target: PreviewManager
215+ onPreviewSaved: {
216+ if (pageUrl !== url) return
217+ if (preview == previewUrl) {
218+ // Ensure that the preview URL actually changes,
219+ // for the image to be reloaded
220+ preview = ""
221+ }
222+ preview = previewUrl
223+ }
224+ }
225+
226 FocusScope {
227 id: webviewContainer
228 anchors.fill: parent
229@@ -81,9 +95,7 @@
230
231 function close() {
232 unload()
233- if (preview && preview.toString()) {
234- FileOperations.remove(preview)
235- }
236+ PreviewManager.checkDelete(url)
237 destroy()
238 }
239
240@@ -110,6 +122,12 @@
241 visible = false
242 return
243 }
244+
245+ if (url.toString().length === 0) {
246+ visible = false
247+ return
248+ }
249+
250 internal.hiding = true
251 webview.grabToImage(function(result) {
252 if (!internal.hiding) {
253@@ -117,22 +135,8 @@
254 }
255 internal.hiding = false
256 visible = false
257- var capturesDir = cacheLocation + "/captures"
258- if (!FileOperations.exists(Qt.resolvedUrl(capturesDir))) {
259- FileOperations.mkpath(Qt.resolvedUrl(capturesDir))
260- }
261- var filepath = capturesDir + "/" + uniqueId + ".jpg"
262- if (result.saveToFile(filepath)) {
263- var previewUrl = Qt.resolvedUrl(filepath)
264- if (preview == previewUrl) {
265- // Ensure that the preview URL actually changes,
266- // for the image to be reloaded
267- preview = ""
268- }
269- preview = previewUrl
270- } else {
271- preview = ""
272- }
273+
274+ PreviewManager.saveToDisk(result, url)
275 })
276 }
277 }
278
279=== modified file 'src/app/webbrowser/CMakeLists.txt'
280--- src/app/webbrowser/CMakeLists.txt 2015-09-22 01:27:19 +0000
281+++ src/app/webbrowser/CMakeLists.txt 2015-10-13 11:21:13 +0000
282@@ -56,7 +56,7 @@
283 install(TARGETS ${WEBBROWSER_APP}
284 RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
285
286-file(GLOB QML_FILES *.qml *.js)
287+file(GLOB QML_FILES *.qml qmldir *.js)
288 install(FILES ${QML_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/webbrowser-app/webbrowser)
289
290 install(DIRECTORY assets
291
292=== removed file 'src/app/webbrowser/HistoryModel.qml'
293--- src/app/webbrowser/HistoryModel.qml 2015-08-10 15:22:00 +0000
294+++ src/app/webbrowser/HistoryModel.qml 1970-01-01 00:00:00 +0000
295@@ -1,24 +0,0 @@
296-/*
297- * Copyright 2014-2015 Canonical Ltd.
298- *
299- * This file is part of webbrowser-app.
300- *
301- * webbrowser-app is free software; you can redistribute it and/or modify
302- * it under the terms of the GNU General Public License as published by
303- * the Free Software Foundation; version 3.
304- *
305- * webbrowser-app is distributed in the hope that it will be useful,
306- * but WITHOUT ANY WARRANTY; without even the implied warranty of
307- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
308- * GNU General Public License for more details.
309- *
310- * You should have received a copy of the GNU General Public License
311- * along with this program. If not, see <http://www.gnu.org/licenses/>.
312- */
313-
314-import QtQuick 2.4
315-import webbrowserapp.private 0.1
316-
317-HistoryModel {
318- databasePath: dataLocation + "/history.sqlite"
319-}
320
321=== modified file 'src/app/webbrowser/HistoryView.qml'
322--- src/app/webbrowser/HistoryView.qml 2015-08-10 15:22:00 +0000
323+++ src/app/webbrowser/HistoryView.qml 2015-10-13 11:21:13 +0000
324@@ -23,8 +23,6 @@
325 Item {
326 id: historyView
327
328- property alias historyModel: historyTimeframeModel.sourceModel
329-
330 signal seeMoreEntriesClicked(var model)
331 signal done()
332
333@@ -33,6 +31,16 @@
334 color: "#f6f6f6"
335 }
336
337+ Timer {
338+ // Set the model asynchronously to ensure
339+ // the view is displayed as early as possible.
340+ id: loadModelTimer
341+ interval: 1
342+ onTriggered: historyTimeframeModel.sourceModel = HistoryModel
343+ }
344+
345+ function loadModel() { loadModelTimer.restart() }
346+
347 ListView {
348 id: domainsListView
349
350@@ -75,7 +83,7 @@
351 historyView.seeMoreEntriesClicked(model.entries)
352 }
353 }
354- onRemoved: historyView.historyModel.removeEntriesByDomain(model.domain)
355+ onRemoved: HistoryModel.removeEntriesByDomain(model.domain)
356 onPressAndHold: {
357 selectMode = !selectMode
358 if (selectMode) {
359@@ -220,7 +228,7 @@
360 }
361 domainsListView.ViewItems.selectMode = false
362 for (var j in domains) {
363- historyModel.removeEntriesByDomain(domains[j])
364+ HistoryModel.removeEntriesByDomain(domains[j])
365 }
366 }
367 }
368
369=== modified file 'src/app/webbrowser/HistoryViewWide.qml'
370--- src/app/webbrowser/HistoryViewWide.qml 2015-09-18 20:43:26 +0000
371+++ src/app/webbrowser/HistoryViewWide.qml 2015-10-13 11:21:13 +0000
372@@ -25,7 +25,6 @@
373 FocusScope {
374 id: historyViewWide
375
376- property alias historyModel: historySearchModel.sourceModel
377 property bool searchMode: false
378 readonly property bool selectMode: urlsListView.ViewItems.selectMode
379 onSearchModeChanged: {
380@@ -57,15 +56,16 @@
381 internal.removeSelected()
382 } else {
383 if (urlsListView.activeFocus) {
384- historyViewWide.historyModel.removeEntryByUrl(urlsListView.currentItem.siteUrl)
385+ HistoryModel.removeEntryByUrl(urlsListView.currentItem.siteUrl)
386+
387 if (urlsListView.count == 0) {
388 lastVisitDateListView.currentIndex = 0
389 }
390 } else {
391 if (lastVisitDateListView.currentIndex == 0) {
392- historyViewWide.historyModel.clearAll()
393+ HistoryModel.clearAll()
394 } else {
395- historyViewWide.historyModel.removeEntriesByDate(lastVisitDateListView.currentItem.lastVisitDate)
396+ HistoryModel.removeEntriesByDate(lastVisitDateListView.currentItem.lastVisitDate)
397 lastVisitDateListView.currentIndex = 0
398 }
399 }
400@@ -82,6 +82,16 @@
401 anchors.fill: parent
402 }
403
404+ Timer {
405+ // Set the model asynchronously to ensure
406+ // the view is displayed as early as possible.
407+ id: loadModelTimer
408+ interval: 1
409+ onTriggered: historySearchModel.sourceModel = HistoryModel
410+ }
411+
412+ function loadModel() { loadModelTimer.restart() }
413+
414 TextSearchFilterModel {
415 id: historySearchModel
416 searchFields: ["title", "url"]
417@@ -239,8 +249,8 @@
418 // Until a valid HistoryModel is assigned the TextSearchFilterModel
419 // will not report role names, and the HistoryLastVisit*Models will emit warnings
420 // since they need a dateLastVisit role to be present.
421- // We avoid this by assigning the sourceModel only when HistoryModel is ready.
422- sourceModel: historyModel ? historySearchModel : undefined
423+ // We avoid this by delaying assigning the source model until it is ready.
424+ sourceModel: historySearchModel.sourceModel ? historySearchModel : undefined
425 }
426
427 clip: true
428@@ -309,7 +319,7 @@
429 }
430
431 onRemoved: {
432- historyViewWide.historyModel.removeEntryByUrl(model.url)
433+ HistoryModel.removeEntryByUrl(model.url)
434 if (urlsListView.count == 0) {
435 lastVisitDateListView.currentIndex = 0
436 }
437@@ -360,8 +370,7 @@
438 ToolbarAction {
439 objectName: "backButton"
440
441- visible: urlsListView.ViewItems.selectMode ||
442- historyViewWide.searchMode
443+ visible: historyViewWide.selectMode || historyViewWide.searchMode
444
445 anchors {
446 top: parent.top
447@@ -545,7 +554,7 @@
448
449 urlsListView.ViewItems.selectMode = false
450 for (var j in urls) {
451- historyViewWide.historyModel.removeEntryByUrl(urls[j])
452+ HistoryModel.removeEntryByUrl(urls[j])
453 }
454
455 lastVisitDateListView.forceActiveFocus()
456
457=== modified file 'src/app/webbrowser/NewTabView.qml'
458--- src/app/webbrowser/NewTabView.qml 2015-08-10 15:22:00 +0000
459+++ src/app/webbrowser/NewTabView.qml 2015-10-13 11:21:13 +0000
460@@ -21,12 +21,12 @@
461 import Ubuntu.Components 1.3
462 import webbrowserapp.private 0.1
463 import ".."
464+import "."
465
466 Item {
467 id: newTabView
468
469 property QtObject bookmarksModel
470- property alias historyModel: historyTimeframeModel.sourceModel
471 property Settings settingsObject
472
473 signal bookmarkClicked(url url)
474@@ -37,6 +37,7 @@
475 id: topSitesModel
476 sourceModel: HistoryTimeframeModel {
477 id: historyTimeframeModel
478+ sourceModel: HistoryModel
479 }
480 }
481
482@@ -148,7 +149,7 @@
483
484 active: internal.seeMoreBookmarksView
485 sourceComponent: BookmarksFolderListView {
486- model: newTabView.bookmarksModel
487+ model: newTabView.bookmarksModel
488
489 onBookmarkClicked: newTabView.bookmarkClicked(url)
490 onBookmarkRemoved: newTabView.bookmarkRemoved(url)
491@@ -232,7 +233,7 @@
492 height: units.gu(0.1)
493 anchors {
494 left: parent.left
495- leftMargin: units.gu(1.5)
496+ leftMargin: units.gu(2)
497 right: parent.right
498 }
499 color: "#d3d3d3"
500@@ -258,24 +259,60 @@
501 color: UbuntuColors.darkGrey
502 }
503
504- UrlsList {
505- objectName: "topSitesList"
506+ Item {
507 anchors {
508 left: parent.left
509 right: parent.right
510- }
511-
512- opacity: internal.seeMoreBookmarksView ? 0.0 : 1.0
513- Behavior on opacity { UbuntuNumberAnimation {} }
514- visible: opacity > 0
515-
516- limit: 10
517- spacing: 0
518-
519- model: topSitesModel
520-
521- onUrlClicked: newTabView.historyEntryClicked(url)
522- onUrlRemoved: newTabView.historyModel.hide(url)
523+
524+ // The UrlPreviewGrid's highlight extends to the left of the
525+ // grid itself by a margin.
526+ // Since we are clipping the parent we need to prevent the
527+ // highlight from being clipped away at the left edge.
528+ // We do this by shifting the parent left and the contents right
529+ // by an amount equal to the highlight's margin.
530+ leftMargin: units.gu(2) - grid.horizontalMargin
531+
532+ // The right margin should be 2gu, which is set on all cells
533+ // of the UrlPreviewGrid already. However the parent Column
534+ // has 1.5gu right margin, so we are compensating for that
535+ // here instead of removing it from the Column itself and
536+ // reassigning it to all Column children except this one.
537+ rightMargin: - contentColumn.anchors.rightMargin
538+ }
539+ height: childrenRect.height
540+ clip: true
541+
542+ UrlPreviewGrid {
543+ id: grid
544+ objectName: "topSitesList"
545+ anchors {
546+ left: parent.left
547+ leftMargin: grid.horizontalMargin
548+ right: parent.right
549+ top: parent.top
550+ topMargin: units.gu(2)
551+ }
552+
553+ horizontalMargin: units.gu(1)
554+ verticalMargin: units.gu(1)
555+
556+ opacity: internal.seeMoreBookmarksView ? 0.0 : 1.0
557+ Behavior on opacity { UbuntuNumberAnimation {} }
558+ visible: opacity > 0
559+ interactive: false
560+
561+ model: LimitProxyModel {
562+ limit: 10
563+ sourceModel: topSitesModel
564+ }
565+ showFavicons: false
566+
567+ onActivated: newTabView.historyEntryClicked(url)
568+ onRemoved: {
569+ HistoryModel.hide(url)
570+ PreviewManager.checkDelete(url)
571+ }
572+ }
573 }
574 }
575 }
576
577=== modified file 'src/app/webbrowser/NewTabViewWide.qml'
578--- src/app/webbrowser/NewTabViewWide.qml 2015-09-08 09:28:47 +0000
579+++ src/app/webbrowser/NewTabViewWide.qml 2015-10-13 11:21:13 +0000
580@@ -21,12 +21,12 @@
581 import Ubuntu.Components 1.3
582 import webbrowserapp.private 0.1
583 import ".."
584+import "."
585
586 FocusScope {
587 id: newTabViewWide
588
589 property QtObject bookmarksModel
590- property alias historyModel: historyTimeframeModel.sourceModel
591 property QtObject settingsObject
592 property alias selectedIndex: sections.selectedIndex
593 readonly property bool inBookmarksView: newTabViewWide.selectedIndex === 1
594@@ -57,6 +57,7 @@
595 sourceModel: TopSitesModel {
596 sourceModel: HistoryTimeframeModel {
597 id: historyTimeframeModel
598+ sourceModel: HistoryModel
599 }
600 }
601 }
602@@ -233,7 +234,7 @@
603 flickableItem: bookmarksList
604 }
605
606- ListView {
607+ UrlPreviewGrid {
608 id: topSitesList
609 objectName: "topSitesList"
610 anchors {
611@@ -241,36 +242,22 @@
612 bottom: parent.bottom
613 left: parent.left
614 right: parent.right
615- topMargin: units.gu(2)
616+ topMargin: units.gu(3)
617+ leftMargin: units.gu(4)
618 }
619
620 visible: !inBookmarksView
621- currentIndex: 0
622
623 model: topSitesModel
624- delegate: UrlDelegateWide {
625- objectName: "topSiteItem"
626- clip: true
627-
628- title: model.title
629- icon: model.icon
630- url: model.url
631- highlighted: topSitesList.activeFocus && ListView.isCurrentItem
632-
633- onClicked: newTabViewWide.historyEntryClicked(url)
634- onRemoved: newTabViewWide.historyModel.hide(url)
635- }
636-
637- Keys.onReturnPressed: newTabViewWide.historyEntryClicked(currentItem.url)
638- Keys.onDeletePressed: {
639- newTabViewWide.historyModel.hide(currentItem.url)
640- if (topSitesList.model.count === 0) newTabViewWide.releasingKeyboardFocus()
641- }
642- Keys.onDownPressed: currentIndex = Math.min(currentIndex + 1, model.count - 1)
643- Keys.onUpPressed: {
644- if (currentIndex > 0) currentIndex = Math.max(currentIndex - 1, 0)
645- else newTabViewWide.releasingKeyboardFocus()
646- }
647+ showFavicons: true
648+
649+ onActivated: newTabViewWide.historyEntryClicked(url)
650+ onRemoved: {
651+ HistoryModel.hide(url)
652+ if (topSitesModel.count === 0) newTabViewWide.releasingKeyboardFocus()
653+ PreviewManager.checkDelete(url)
654+ }
655+ onReleasingKeyboardFocus: newTabViewWide.releasingKeyboardFocus()
656 }
657
658 Scrollbar {
659@@ -293,7 +280,7 @@
660 anchors {
661 left: parent.left
662 top: parent.top
663- leftMargin: units.gu(1)
664+ leftMargin: units.gu(2)
665 }
666
667 selectedIndex: settingsObject.newTabDefaultSection
668
669=== added file 'src/app/webbrowser/PreviewManager.qml'
670--- src/app/webbrowser/PreviewManager.qml 1970-01-01 00:00:00 +0000
671+++ src/app/webbrowser/PreviewManager.qml 2015-10-13 11:21:13 +0000
672@@ -0,0 +1,91 @@
673+/*
674+ * Copyright 2015 Canonical Ltd.
675+ *
676+ * This file is part of webbrowser-app.
677+ *
678+ * webbrowser-app is free software; you can redistribute it and/or modify
679+ * it under the terms of the GNU General Public License as published by
680+ * the Free Software Foundation; version 3.
681+ *
682+ * webbrowser-app is distributed in the hope that it will be useful,
683+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
684+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
685+ * GNU General Public License for more details.
686+ *
687+ * You should have received a copy of the GNU General Public License
688+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
689+ */
690+
691+pragma Singleton
692+
693+import QtQuick 2.4
694+import Ubuntu.Web 0.2
695+import webbrowserapp.private 0.1
696+
697+Item {
698+ property string capturesDir: cacheLocation + "/captures"
699+ signal previewSaved(url pageUrl, url previewUrl)
700+
701+ LimitProxyModel {
702+ id: topSites
703+ limit: 10
704+ sourceModel: TopSitesModel {
705+ sourceModel: HistoryTimeframeModel {
706+ sourceModel: HistoryModel
707+ }
708+ }
709+ function contains(url) {
710+ for (var i = 0; i < topSites.count; i++) {
711+ if (topSites.get(i).url == url) return true
712+ }
713+ return false
714+ }
715+ function containsHash(hash) {
716+ for (var i = 0; i < topSites.count; i++) {
717+ if (Qt.md5(topSites.get(i).url) == hash) return true
718+ }
719+ return false
720+ }
721+ }
722+
723+ function previewPathFromUrl(url) {
724+ return "%1/%2.jpg".arg(capturesDir).arg(Qt.md5(url))
725+ }
726+
727+ function saveToDisk(data, url) {
728+ if (!FileOperations.exists(Qt.resolvedUrl(capturesDir))) {
729+ FileOperations.mkpath(Qt.resolvedUrl(capturesDir))
730+ }
731+
732+ var filepath = previewPathFromUrl(url)
733+ var previewUrl = ""
734+ if (data.saveToFile(filepath)) previewUrl = Qt.resolvedUrl(filepath)
735+ else console.warn("Failed to save preview to disk for %1 (path is %2)".arg(url).arg(filepath))
736+
737+ previewSaved(url, previewUrl)
738+ }
739+
740+
741+ function checkDelete(url) {
742+ if (!topSites.contains(url)) {
743+ FileOperations.remove(Qt.resolvedUrl(previewPathFromUrl(url)))
744+ }
745+ }
746+
747+ // Remove all previews stored on disk that are not part of the top sites
748+ // and that are not for URLs in the doNotCleanUrls list
749+ function cleanUnusedPreviews(doNotCleanUrls) {
750+ var dir = Qt.resolvedUrl(capturesDir);
751+ var previews = FileOperations.filesInDirectory(dir, ["*.jpg"])
752+ var doNotCleanHashes = doNotCleanUrls.map(function(url) { return Qt.md5(url) })
753+
754+ for (var i = 0; i < previews.length; i++) {
755+ var hash = previews[i].replace('.jpg', '')
756+ if (!topSites.containsHash(hash) &&
757+ doNotCleanHashes.indexOf(hash) === -1) {
758+ var file = Qt.resolvedUrl("%1/%2.jpg".arg(capturesDir).arg(hash))
759+ FileOperations.remove(file)
760+ }
761+ }
762+ }
763+}
764
765=== modified file 'src/app/webbrowser/SettingsPage.qml'
766--- src/app/webbrowser/SettingsPage.qml 2015-08-18 08:49:24 +0000
767+++ src/app/webbrowser/SettingsPage.qml 2015-10-13 11:21:13 +0000
768@@ -29,7 +29,6 @@
769 Item {
770 id: settingsItem
771
772- property QtObject historyModel
773 property Settings settingsObject
774
775 signal done()
776@@ -247,10 +246,10 @@
777 ListItem.Standard {
778 objectName: "privacy.clearHistory"
779 text: i18n.tr("Clear Browsing History")
780- enabled: historyModel.count > 0
781+ enabled: HistoryModel.count > 0
782 onClicked: {
783 var dialog = PopupUtils.open(privacyConfirmDialogComponent, privacyItem, {"title": i18n.tr("Clear Browsing History?")})
784- dialog.confirmed.connect(historyModel.clearAll)
785+ dialog.confirmed.connect(HistoryModel.clearAll)
786 }
787 }
788
789
790=== added file 'src/app/webbrowser/UrlPreviewDelegate.qml'
791--- src/app/webbrowser/UrlPreviewDelegate.qml 1970-01-01 00:00:00 +0000
792+++ src/app/webbrowser/UrlPreviewDelegate.qml 2015-10-13 11:21:13 +0000
793@@ -0,0 +1,123 @@
794+/*
795+ * Copyright 2015 Canonical Ltd.
796+ *
797+ * This file is part of webbrowser-app.
798+ *
799+ * webbrowser-app is free software; you can redistribute it and/or modify
800+ * it under the terms of the GNU General Public License as published by
801+ * the Free Software Foundation; version 3.
802+ *
803+ * webbrowser-app is distributed in the hope that it will be useful,
804+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
805+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
806+ * GNU General Public License for more details.
807+ *
808+ * You should have received a copy of the GNU General Public License
809+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
810+ */
811+
812+import QtQuick 2.4
813+import Ubuntu.Components 1.3
814+import Ubuntu.Components.Popups 1.3
815+import webbrowserapp.private 0.1
816+import ".."
817+import "."
818+
819+AbstractButton {
820+ id: preview
821+
822+ property url icon
823+ property alias title: titleLabel.text
824+ property url url
825+ property bool showFavicon: true
826+
827+ property alias previewHeight: previewShape.height
828+ property alias previewWidth: previewShape.width
829+
830+ signal removed()
831+
832+ onPressAndHold: PopupUtils.open(contextMenuComponent, previewShape)
833+
834+ Column {
835+ id: contentColumn
836+ anchors.left: parent.left
837+ anchors.top: parent.top
838+ spacing: units.gu(1)
839+
840+ Item {
841+ anchors.left: parent.left
842+ anchors.right: parent.right
843+ height: titleLabel.height
844+
845+ Loader {
846+ id: favicon
847+ anchors.left: parent.left
848+ anchors.verticalCenter: parent.verticalCenter
849+ sourceComponent: Favicon {
850+ source: preview.icon
851+ anchors.left: parent.left
852+ anchors.verticalCenter: parent.verticalCenter
853+ }
854+ active: preview.showFavicon
855+ }
856+
857+ Label {
858+ id: titleLabel
859+ anchors.left: favicon.right
860+ anchors.leftMargin: showFavicon ? units.gu(1) : 0
861+ anchors.right: parent.right
862+ anchors.top: parent.top
863+ text: preview.title
864+ elide: Text.ElideRight
865+ fontSize: "small"
866+ }
867+ }
868+
869+ UbuntuShape {
870+ id: previewShape
871+ anchors.left: parent.left
872+ width: units.gu(26)
873+ height: units.gu(16)
874+
875+ source: Image {
876+ id: previewImage
877+ source: FileOperations.exists(previewShape.previewUrl) ? previewShape.previewUrl : ""
878+ sourceSize.width: previewShape.width
879+ cache: false
880+ }
881+ sourceFillMode: UbuntuShape.PreserveAspectCrop
882+ sourceHorizontalAlignment: UbuntuShape.AlignLeft
883+ sourceVerticalAlignment: UbuntuShape.AlignTop
884+
885+ property url previewUrl: Qt.resolvedUrl(PreviewManager.previewPathFromUrl(preview.url))
886+
887+ Connections {
888+ target: PreviewManager
889+ onPreviewSaved: {
890+ if (pageUrl !== preview.url) return
891+ previewImage.source = ""
892+ previewImage.source = previewShape.previewUrl
893+ }
894+ }
895+ }
896+ }
897+
898+ MouseArea {
899+ anchors.fill: contentColumn
900+ acceptedButtons: Qt.RightButton
901+ onClicked: PopupUtils.open(contextMenuComponent, previewShape)
902+ }
903+
904+ Component {
905+ id: contextMenuComponent
906+ ActionSelectionPopover {
907+ actions: ActionList {
908+ Action {
909+ objectName: "delete"
910+ text: i18n.tr("Remove")
911+ onTriggered: preview.removed()
912+ }
913+ }
914+ }
915+ }
916+}
917
918=== added file 'src/app/webbrowser/UrlPreviewGrid.qml'
919--- src/app/webbrowser/UrlPreviewGrid.qml 1970-01-01 00:00:00 +0000
920+++ src/app/webbrowser/UrlPreviewGrid.qml 2015-10-13 11:21:13 +0000
921@@ -0,0 +1,90 @@
922+/*
923+ * Copyright 2015 Canonical Ltd.
924+ *
925+ * This file is part of webbrowser-app.
926+ *
927+ * webbrowser-app is free software; you can redistribute it and/or modify
928+ * it under the terms of the GNU General Public License as published by
929+ * the Free Software Foundation; version 3.
930+ *
931+ * webbrowser-app is distributed in the hope that it will be useful,
932+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
933+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
934+ * GNU General Public License for more details.
935+ *
936+ * You should have received a copy of the GNU General Public License
937+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
938+ */
939+
940+import QtQuick 2.4
941+import Qt.labs.settings 1.0
942+import Ubuntu.Components 1.3
943+import webbrowserapp.private 0.1
944+import ".."
945+
946+GridView {
947+ id: grid
948+
949+ property bool showFavicons: true
950+ property int horizontalMargin: units.gu(3)
951+ property int verticalMargin: units.gu(2.5)
952+ property int previewWidth: units.gu(17)
953+ property int previewHeight: units.gu(10)
954+
955+ signal activated(url url)
956+ signal removed(url url)
957+ signal releasingKeyboardFocus()
958+
959+ currentIndex: 0
960+
961+ cellWidth: previewWidth + horizontalMargin * 2
962+ cellHeight: previewHeight + verticalMargin * 2 + units.gu(4) // height of text + favicon + margins in delegate
963+
964+ implicitHeight: contentItem.childrenRect.height
965+
966+ delegate: UrlPreviewDelegate {
967+ objectName: "topSiteItem"
968+ width: grid.cellWidth
969+ height: grid.cellHeight
970+
971+ title: model.title
972+ icon: model.icon
973+ url: model.url
974+ showFavicon: grid.showFavicons
975+
976+ previewHeight: grid.previewHeight
977+ previewWidth: grid.previewWidth
978+
979+ onClicked: grid.activated(model.url)
980+ onRemoved: grid.removed(model.url)
981+ }
982+
983+ highlight: Component {
984+ Item {
985+ visible: grid.activeFocus
986+ UbuntuShape {
987+ anchors.fill: parent
988+ anchors.leftMargin: - grid.horizontalMargin
989+ anchors.rightMargin: grid.horizontalMargin
990+ anchors.topMargin: - grid.verticalMargin
991+ anchors.bottomMargin: grid.verticalMargin
992+ aspect: UbuntuShape.Flat
993+ backgroundColor: Qt.rgba(0, 0, 0, 0.05)
994+ }
995+ }
996+ }
997+
998+ Keys.onDeletePressed: removed(currentItem.url)
999+
1000+ Keys.onLeftPressed: {
1001+ var i = grid.currentIndex
1002+ grid.moveCurrentIndexLeft()
1003+ if (i === grid.currentIndex) grid.releasingKeyboardFocus()
1004+ }
1005+
1006+ Keys.onUpPressed: {
1007+ var i = grid.currentIndex
1008+ grid.moveCurrentIndexUp()
1009+ if (i === grid.currentIndex) grid.releasingKeyboardFocus()
1010+ }
1011+}
1012
1013=== modified file 'src/app/webbrowser/file-operations.cpp'
1014--- src/app/webbrowser/file-operations.cpp 2015-02-18 20:31:37 +0000
1015+++ src/app/webbrowser/file-operations.cpp 2015-10-13 11:21:13 +0000
1016@@ -43,3 +43,10 @@
1017 {
1018 return QDir::root().mkpath(path.toLocalFile());
1019 }
1020+
1021+QStringList FileOperations::filesInDirectory(const QUrl& directory,
1022+ const QStringList& filters) const
1023+{
1024+ return QDir(directory.toLocalFile()).entryList(filters,
1025+ QDir::Files, QDir::Unsorted);
1026+}
1027
1028=== modified file 'src/app/webbrowser/file-operations.h'
1029--- src/app/webbrowser/file-operations.h 2015-02-18 20:31:37 +0000
1030+++ src/app/webbrowser/file-operations.h 2015-10-13 11:21:13 +0000
1031@@ -20,6 +20,7 @@
1032 #define __FILE_OPERATIONS_H__
1033
1034 #include <QtCore/QObject>
1035+#include <QtCore/QStringList>
1036
1037 class QUrl;
1038
1039@@ -33,6 +34,8 @@
1040 Q_INVOKABLE bool exists(const QUrl& path) const;
1041 Q_INVOKABLE bool remove(const QUrl& file) const;
1042 Q_INVOKABLE bool mkpath(const QUrl& path) const;
1043+ Q_INVOKABLE QStringList filesInDirectory(const QUrl& directory,
1044+ const QStringList& filters) const;
1045 };
1046
1047 #endif // __FILE_OPERATIONS_H__
1048
1049=== added file 'src/app/webbrowser/qmldir'
1050--- src/app/webbrowser/qmldir 1970-01-01 00:00:00 +0000
1051+++ src/app/webbrowser/qmldir 2015-10-13 11:21:13 +0000
1052@@ -0,0 +1,1 @@
1053+singleton PreviewManager 1.0 PreviewManager.qml
1054
1055=== modified file 'src/app/webbrowser/webbrowser-app.cpp'
1056--- src/app/webbrowser/webbrowser-app.cpp 2015-08-19 13:16:06 +0000
1057+++ src/app/webbrowser/webbrowser-app.cpp 2015-10-13 11:21:13 +0000
1058@@ -64,10 +64,17 @@
1059 return new CacheDeleter();
1060 }
1061
1062+static QObject* HistoryModel_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine)
1063+{
1064+ Q_UNUSED(engine);
1065+ Q_UNUSED(scriptEngine);
1066+ return new HistoryModel();
1067+}
1068+
1069 bool WebbrowserApp::initialize()
1070 {
1071 const char* uri = "webbrowserapp.private";
1072- qmlRegisterType<HistoryModel>(uri, 0, 1, "HistoryModel");
1073+ qmlRegisterSingletonType<HistoryModel>(uri, 0, 1, "HistoryModel", HistoryModel_singleton_factory);
1074 qmlRegisterType<HistoryTimeframeModel>(uri, 0, 1, "HistoryTimeframeModel");
1075 qmlRegisterType<TopSitesModel>(uri, 0 , 1, "TopSitesModel");
1076 qmlRegisterType<HistoryDomainListModel>(uri, 0, 1, "HistoryDomainListModel");
1077
1078=== modified file 'tests/autopilot/webbrowser_app/emulators/browser.py'
1079--- tests/autopilot/webbrowser_app/emulators/browser.py 2015-09-29 10:59:50 +0000
1080+++ tests/autopilot/webbrowser_app/emulators/browser.py 2015-10-13 11:21:13 +0000
1081@@ -185,6 +185,18 @@
1082 else:
1083 return self.wait_select_single(ContextMenuMobile)
1084
1085+ def open_item_context_menu_on_item(self, item, menuClass):
1086+ cx = item.globalRect.x + item.globalRect.width // 2
1087+ cy = item.globalRect.y + item.globalRect.height // 2
1088+ self.pointing_device.move(cx, cy)
1089+ if model() == 'Desktop':
1090+ self.pointing_device.click(button=3)
1091+ else:
1092+ self.pointing_device.press()
1093+ time.sleep(1.5)
1094+ self.pointing_device.release()
1095+ return self.wait_select_single(menuClass)
1096+
1097 def open_context_menu(self):
1098 webview = self.get_current_webview()
1099 chrome = self.chrome
1100@@ -519,7 +531,7 @@
1101 return self.select_single(UrlsList, objectName="bookmarksList")
1102
1103 def get_top_sites_list(self):
1104- return self.select_single(UrlsList, objectName="topSitesList")
1105+ return self.select_single(UrlPreviewGrid, objectName="topSitesList")
1106
1107 def get_notopsites_label(self):
1108 return self.select_single(objectName="notopsites")
1109@@ -558,11 +570,7 @@
1110
1111 def get_top_sites_list(self):
1112 self.go_to_section(0)
1113- list = self.select_single(uitk.QQuickListView,
1114- objectName="topSitesList")
1115- return sorted(list.select_many("UrlDelegateWide",
1116- objectName="topSiteItem"),
1117- key=lambda delegate: delegate.globalRect.y)
1118+ return self.select_single(UrlPreviewGrid, objectName="topSitesList")
1119
1120 def get_folders_list(self):
1121 self.go_to_section(1)
1122@@ -572,8 +580,7 @@
1123 key=lambda delegate: delegate.globalRect.y)
1124
1125 def get_top_site_items(self):
1126- self.go_to_section(0)
1127- return self.get_top_sites_list()
1128+ return self.get_top_sites_list().get_delegates()
1129
1130 def get_bookmarks(self, folder_name):
1131 folders = self.get_folders_list()
1132@@ -597,6 +604,16 @@
1133 return [delegate.url for delegate in self.get_delegates()]
1134
1135
1136+class UrlPreviewGrid(uitk.UbuntuUIToolkitCustomProxyObjectBase):
1137+
1138+ def get_delegates(self):
1139+ return sorted(self.select_many("UrlPreviewDelegate"),
1140+ key=lambda delegate: delegate.globalRect.y)
1141+
1142+ def get_urls(self):
1143+ return [delegate.url for delegate in self.get_delegates()]
1144+
1145+
1146 class UrlDelegate(uitk.UCListItem):
1147
1148 pass
1149@@ -607,6 +624,22 @@
1150 pass
1151
1152
1153+class UrlPreviewDelegate(uitk.UbuntuUIToolkitCustomProxyObjectBase):
1154+
1155+ def hide_from_history(self, root):
1156+ menu = root.open_item_context_menu_on_item(self,
1157+ "ActionSelectionPopover")
1158+
1159+ # Note: we can't still use the click_action_button method of
1160+ # ActionSelectionPopover's CPO, because it will crash if we delete the
1161+ # menu as a reaction to the click (which is the case here).
1162+ # However at least we can select the action button by objectName now.
1163+ # See bug http://pad.lv/1504189
1164+ delete_item = menu.wait_select_single(objectName="delete_button")
1165+ self.pointing_device.click_object(delete_item)
1166+ menu.wait_until_destroyed()
1167+
1168+
1169 class DraggableUrlDelegateWide(UrlDelegateWide):
1170
1171 def get_grip(self):
1172
1173=== modified file 'tests/autopilot/webbrowser_app/tests/__init__.py'
1174--- tests/autopilot/webbrowser_app/tests/__init__.py 2015-09-03 09:46:29 +0000
1175+++ tests/autopilot/webbrowser_app/tests/__init__.py 2015-10-13 11:21:13 +0000
1176@@ -82,17 +82,18 @@
1177 if not os.path.exists(self.cache_location):
1178 os.makedirs(self.cache_location)
1179
1180- def setUp(self):
1181+ def setUp(self, launch=True):
1182 self.create_temporary_profile()
1183 self.pointing_device = uitk.get_pointing_device()
1184 super(BrowserTestCaseBase, self).setUp()
1185- self.app = self.launch_app()
1186+ if (launch):
1187+ self.launch_app()
1188
1189 def launch_app(self):
1190 if os.path.exists(self.local_location):
1191- return self.launch_test_local()
1192+ self.app = self.launch_test_local()
1193 else:
1194- return self.launch_test_installed()
1195+ self.app = self.launch_test_installed()
1196 self.main_window.visible.wait_for(True)
1197
1198 def launch_test_local(self):
1199@@ -238,7 +239,7 @@
1200 are executed, thus making them more robust.
1201 """
1202
1203- def setUp(self, path="/test1"):
1204+ def setUp(self, path="/test1", launch=True):
1205 self.http_server = http_server.HTTPServerInAThread()
1206 self.ping_server(self.http_server)
1207 self.addCleanup(self.http_server.cleanup)
1208@@ -249,7 +250,12 @@
1209 self.base_url = "http://" + self.base_domain
1210 self.url = self.base_url + path
1211 self.ARGS = self.ARGS + [self.url]
1212- super(StartOpenRemotePageTestCaseBase, self).setUp()
1213+ super(StartOpenRemotePageTestCaseBase, self).setUp(launch)
1214+ if (launch):
1215+ self.assert_home_page_eventually_loaded()
1216+
1217+ def launch_and_wait_for_page_loaded(self):
1218+ self.launch_app()
1219 self.assert_home_page_eventually_loaded()
1220
1221 def assert_home_page_eventually_loaded(self):
1222
1223=== modified file 'tests/autopilot/webbrowser_app/tests/test_new_tab_view.py'
1224--- tests/autopilot/webbrowser_app/tests/test_new_tab_view.py 2015-09-24 16:01:29 +0000
1225+++ tests/autopilot/webbrowser_app/tests/test_new_tab_view.py 2015-10-13 11:21:13 +0000
1226@@ -406,9 +406,9 @@
1227 Eventually(Equals(1)))
1228 notopsites_label = self.new_tab_view.get_notopsites_label()
1229 self.assertThat(notopsites_label.visible, Eventually(Equals(False)))
1230+
1231 delegate = top_sites.get_delegates()[0]
1232- delegate.trigger_leading_action("leadingAction.delete",
1233- delegate.wait_until_destroyed)
1234+ delegate.hide_from_history(self.main_window)
1235 self.assertThat(lambda: len(top_sites.get_delegates()),
1236 Eventually(Equals(0)))
1237 self.assertThat(notopsites_label.visible, Eventually(Equals(True)))
1238@@ -443,11 +443,10 @@
1239
1240 def test_remove_top_sites(self):
1241 view = self.new_tab_view
1242- topsites = view.get_top_sites_list()
1243+ topsites = view.get_top_site_items()
1244 previous_count = len(topsites)
1245- topsites[0].trigger_leading_action("leadingAction.delete",
1246- topsites[0].wait_until_destroyed)
1247- self.assertThat(len(view.get_top_sites_list()),
1248+ topsites[0].hide_from_history(self.main_window)
1249+ self.assertThat(len(view.get_top_site_items()),
1250 Equals(previous_count - 1))
1251
1252 def test_drag_bookmarks(self):
1253
1254=== added file 'tests/autopilot/webbrowser_app/tests/test_site_previews.py'
1255--- tests/autopilot/webbrowser_app/tests/test_site_previews.py 1970-01-01 00:00:00 +0000
1256+++ tests/autopilot/webbrowser_app/tests/test_site_previews.py 2015-10-13 11:21:13 +0000
1257@@ -0,0 +1,168 @@
1258+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
1259+#
1260+# Copyright 2015 Canonical
1261+#
1262+# This program is free software: you can redistribute it and/or modify it
1263+# under the terms of the GNU General Public License version 3, as published
1264+# by the Free Software Foundation.
1265+#
1266+# This program is distributed in the hope that it will be useful,
1267+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1268+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1269+# GNU General Public License for more details.
1270+#
1271+# You should have received a copy of the GNU General Public License
1272+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1273+
1274+import hashlib
1275+import os
1276+from os import path as path
1277+import sqlite3
1278+import time
1279+
1280+from autopilot.matchers import Eventually
1281+from testtools.matchers import Not, Equals, DirExists, DirContains
1282+
1283+from webbrowser_app.tests import StartOpenRemotePageTestCaseBase
1284+
1285+
1286+class TestSitePreviewsBase(StartOpenRemotePageTestCaseBase):
1287+
1288+ def setUp(self):
1289+ super(TestSitePreviewsBase, self).setUp(launch=False)
1290+ self.captures_dir = path.join(self.cache_location, "captures")
1291+
1292+ def file_in_dir(self, file, dir):
1293+ return path.exists(path.join(dir, file))
1294+
1295+ def capture_file(self, url):
1296+ return hashlib.md5(url.encode()).hexdigest() + ".jpg"
1297+
1298+
1299+class TestSitePreviewsNoLaunch(TestSitePreviewsBase):
1300+
1301+ def populate_captures_dir(self, capture_names):
1302+ # captures dir should not exist on fresh run
1303+ self.assertThat(self.captures_dir, Not(DirExists()))
1304+
1305+ # create some random files and ensure they get cleaned up
1306+ os.mkdir(self.captures_dir)
1307+ for capture_name in capture_names:
1308+ open(path.join(self.captures_dir, capture_name), 'w').close()
1309+ self.assertThat(self.captures_dir,
1310+ DirContains(capture_names))
1311+
1312+ def populate_history(self):
1313+ self.countries = [
1314+ "Japan", "Russia", "France", "Italy", "Argentina",
1315+ "Canada", "Mexico", "Peru", "Congo", "Brazil",
1316+ "China", "Mali", "Morocco"
1317+ ]
1318+ db_path = os.path.join(self.data_location, "history.sqlite")
1319+ connection = sqlite3.connect(db_path)
1320+ connection.execute("""CREATE TABLE IF NOT EXISTS history
1321+ (url VARCHAR, domain VARCHAR, title VARCHAR,
1322+ icon VARCHAR, visits INTEGER,
1323+ lastVisit DATETIME);""")
1324+ visits = 50
1325+ for country in self.countries:
1326+ timestamp = int(time.time())
1327+ query = "INSERT INTO history \
1328+ VALUES ('{}', '{}', '{}', '', {}, {});"
1329+ query = query.format("http://en.wikipedia.org/wiki/" + country,
1330+ "wikipedia.org", country, visits, timestamp)
1331+ connection.execute(query)
1332+ visits -= 1
1333+ connection.commit()
1334+ connection.close()
1335+
1336+ def test_cleanup_previews_on_startup(self):
1337+ self.populate_history()
1338+
1339+ # populate the captures dir with correct thumbnail names for all
1340+ # the sites in history...
1341+ history = ["http://en.wikipedia.org/wiki/" + c for c in self.countries]
1342+ history = [self.capture_file(url) for url in history]
1343+
1344+ # ...plus some other files to verify possible corner cases
1345+ other_url = self.capture_file("http://google.com/")
1346+ not_hash = "not_a_preview.jpg"
1347+ not_image = "not_an_image.xxx"
1348+ self.populate_captures_dir(history + [other_url, not_hash, not_image])
1349+
1350+ self.launch_app()
1351+ time.sleep(0.5) # wait for file system to settle
1352+
1353+ # verify that non-image files and top 10 sites are left alone,
1354+ # everything else is cleaned up
1355+ topsites = history[0:10]
1356+ self.assertThat(self.captures_dir, DirContains(topsites + [not_image]))
1357+
1358+
1359+class TestSitePreviews(TestSitePreviewsBase):
1360+
1361+ def setUp(self):
1362+ super(TestSitePreviews, self).setUp()
1363+ self.launch_and_wait_for_page_loaded()
1364+
1365+ def close_tab(self, index):
1366+ if self.main_window.wide:
1367+ self.main_window.chrome.get_tabs_bar().close_tab(index)
1368+ else:
1369+ tabs_view = self.open_tabs_view()
1370+ tabs_view.get_previews()[index].close()
1371+ toolbar = self.main_window.get_recent_view_toolbar()
1372+ toolbar.click_button("doneButton")
1373+
1374+ def get_captures(self):
1375+ if not path.exists(self.captures_dir):
1376+ return []
1377+
1378+ all = os.listdir(self.captures_dir)
1379+ cap = [f for f in all if path.isfile(path.join(self.captures_dir, f))]
1380+ return cap
1381+
1382+ def remove_top_site(self, new_tab_view, url):
1383+ top_sites = new_tab_view.get_top_site_items()
1384+ top_sites = [d for d in top_sites if d.url == url]
1385+ self.assertThat(len(top_sites), Equals(1))
1386+ delegate = top_sites[0]
1387+ delegate.hide_from_history(self.main_window)
1388+
1389+ def test_save_on_switch_tab_and_not_delete_if_topsite(self):
1390+ previous = self.main_window.get_current_webview().url
1391+
1392+ # switching away from tab should save a capture
1393+ self.open_new_tab(open_tabs_view=True)
1394+ self.assertThat(self.captures_dir,
1395+ DirContains([self.capture_file(previous)]))
1396+
1397+ # closing the captured tab should not delete the capture since it is
1398+ # now part of the top sites (being the only one we opened so far)
1399+ self.close_tab(0)
1400+ time.sleep(0.5) # wait for file system to settle
1401+ self.assertThat(self.captures_dir,
1402+ DirContains([self.capture_file(previous)]))
1403+
1404+ def test_save_on_switch_tab_and_delete_if_not_topsite(self):
1405+ previous = self.main_window.get_current_webview().url
1406+ new_tab_view = self.open_new_tab(open_tabs_view=True)
1407+ self.remove_top_site(new_tab_view, previous)
1408+ self.close_tab(0)
1409+ self.assertThat(lambda: self.file_in_dir(self.capture_file(previous),
1410+ self.captures_dir),
1411+ Eventually(Equals(False)))
1412+
1413+ def test_delete_when_tab_closed_and_removed_from_topsites(self):
1414+ previous = self.main_window.get_current_webview().url
1415+ capture = self.capture_file(previous)
1416+ new_tab_view = self.open_new_tab(open_tabs_view=True)
1417+ self.close_tab(0)
1418+ time.sleep(0.5) # wait for file system to settle
1419+ self.assertThat(self.captures_dir, DirContains([capture]))
1420+
1421+ if not self.main_window.wide:
1422+ new_tab_view = self.open_new_tab(open_tabs_view=True)
1423+ self.remove_top_site(new_tab_view, previous)
1424+ self.assertThat(lambda: self.file_in_dir(capture, self.captures_dir),
1425+ Eventually(Equals(False)))
1426
1427=== modified file 'tests/unittests/qml/tst_BrowserTab.qml'
1428--- tests/unittests/qml/tst_BrowserTab.qml 2015-09-23 15:53:01 +0000
1429+++ tests/unittests/qml/tst_BrowserTab.qml 2015-10-13 11:21:13 +0000
1430@@ -19,6 +19,7 @@
1431 import QtQuick 2.4
1432 import QtTest 1.0
1433 import "../../../src/app/webbrowser"
1434+import webbrowserapp.private 0.1
1435
1436 Item {
1437 id: root
1438@@ -30,13 +31,16 @@
1439 id: tabComponent
1440
1441 BrowserTab {
1442+ id: tab
1443+ anchors.fill: parent
1444 webviewComponent: Item {
1445+ anchors.fill: parent
1446 property url url
1447 property string title
1448 property url icon
1449 property var request
1450 property string currentState
1451-
1452+ property bool incognito: tab.incognito
1453 property int reloaded: 0
1454 function reload() { reloaded++ }
1455 }
1456@@ -44,10 +48,20 @@
1457 }
1458 }
1459
1460+ SignalSpy {
1461+ id: previewSavedSpy
1462+ target: PreviewManager
1463+ signalName: "previewSaved"
1464+ }
1465+
1466 TestCase {
1467 name: "BrowserTab"
1468 when: windowShown
1469
1470+ function init() {
1471+ previewSavedSpy.clear()
1472+ }
1473+
1474 function test_unique_ids() {
1475 var tab = tabComponent.createObject(root)
1476 var tab2 = tabComponent.createObject(root)
1477@@ -100,5 +114,54 @@
1478 compare(tab.webview.request, "foobar")
1479 tab.destroy()
1480 }
1481+
1482+ function test_save_preview() {
1483+ var tab = tabComponent.createObject(root)
1484+ tab.initialUrl = "http://example.org"
1485+ tab.load()
1486+ tryCompare(tab, 'webviewPresent', true)
1487+
1488+ tab.current = true
1489+ tab.current = false
1490+ tryCompare(previewSavedSpy, "count", 1)
1491+ verify(!tab.visible)
1492+ compare(previewSavedSpy.signalArguments[0][0], tab.initialUrl)
1493+ compare(previewSavedSpy.signalArguments[0][1], Qt.resolvedUrl(PreviewManager.previewPathFromUrl(tab.initialUrl)))
1494+ compare(tab.preview, Qt.resolvedUrl(PreviewManager.previewPathFromUrl(tab.initialUrl)))
1495+ tab.destroy()
1496+ }
1497+
1498+ function test_no_save_preview_when_incognito() {
1499+ var tab = tabComponent.createObject(root)
1500+ tab.incognito = true
1501+ tab.initialUrl = "http://example.org"
1502+ tab.load()
1503+ tryCompare(tab, 'webviewPresent', true)
1504+
1505+ tab.current = true
1506+ tab.current = false
1507+ // this does not fully guarantee the event won't be emitted later,
1508+ // but it is a reasonable delay and certainly better than nothing
1509+ wait(250)
1510+ compare(previewSavedSpy.count, 0)
1511+ compare(tab.preview, "")
1512+ tab.destroy()
1513+ }
1514+
1515+ function test_delete_preview_on_close() {
1516+ var url = "http://example.org"
1517+ var path = Qt.resolvedUrl(PreviewManager.previewPathFromUrl(url))
1518+ var tab = tabComponent.createObject(root)
1519+ tab.initialUrl = url
1520+ tab.load()
1521+ tryCompare(tab, 'webviewPresent', true)
1522+
1523+ tab.current = true
1524+ tab.current = false
1525+ tryCompare(previewSavedSpy, "count", 1)
1526+ verify(FileOperations.exists(path))
1527+ tab.close()
1528+ verify(!FileOperations.exists(path))
1529+ }
1530 }
1531 }
1532
1533=== modified file 'tests/unittests/qml/tst_HistoryViewWide.qml'
1534--- tests/unittests/qml/tst_HistoryViewWide.qml 2015-09-18 15:20:09 +0000
1535+++ tests/unittests/qml/tst_HistoryViewWide.qml 2015-10-13 11:21:13 +0000
1536@@ -21,6 +21,7 @@
1537 import Ubuntu.Components 1.3
1538 import Ubuntu.Components.ListItems 1.3 as ListItems
1539 import Ubuntu.Test 1.0
1540+import webbrowserapp.private 0.1
1541 import webbrowsertest.private 0.1
1542 import "../../../src/app/webbrowser"
1543
1544@@ -45,9 +46,6 @@
1545 focus: true
1546 sourceComponent: HistoryViewWide {
1547 focus: true
1548- historyModel: HistoryModelMock {
1549- databasePath: ":memory:"
1550- }
1551 }
1552 }
1553
1554@@ -90,19 +88,25 @@
1555 mouseRelease(item, center.x + 100, center.y, Qt.LeftButton, Qt.NoModifier, 2000)
1556 }
1557
1558+ function initTestCase() {
1559+ HistoryModel.databasePath = ":memory:"
1560+ }
1561+
1562 function init() {
1563 historyViewWideLoader.active = true
1564 waitForRendering(historyViewWideLoader.item)
1565
1566 for (var i = 0; i < 3; ++i) {
1567- historyViewWide.historyModel.add("http://example.org/" + i, "Example Domain " + i, "")
1568+ HistoryModel.add("http://example.org/" + i, "Example Domain " + i, "")
1569 }
1570+ historyViewWide.loadModel()
1571 var urlsList = findChild(historyViewWide, "urlsListView")
1572 waitForRendering(urlsList)
1573 tryCompare(urlsList, "count", 3)
1574 }
1575
1576 function cleanup() {
1577+ HistoryModel.clearAll()
1578 historyViewWideLoader.active = false
1579 ctrlFCaptured = 0
1580 }
1581@@ -205,7 +209,9 @@
1582 function test_keyboard_navigation_between_lists() {
1583 var lastVisitDateList = findChild(historyViewWide, "lastVisitDateListView")
1584 var urlsList = findChild(historyViewWide, "urlsListView")
1585+ verify(!lastVisitDateList.activeFocus)
1586 verify(urlsList.activeFocus)
1587+
1588 keyClick(Qt.Key_Left)
1589 verify(lastVisitDateList.activeFocus)
1590 verify(!urlsList.activeFocus)
1591@@ -309,7 +315,7 @@
1592 keyClick(Qt.Key_Enter)
1593 compare(historyEntryClickedSpy.count, 1)
1594 var args = historyEntryClickedSpy.signalArguments[0]
1595- var entry = urlsList.model.get(2)
1596+ var entry = urlsList.model.get(0)
1597 compare(String(args[0]), String(entry.url))
1598
1599 // now try the same during a search
1600@@ -372,7 +378,7 @@
1601 var today = new Date()
1602 today = new Date(today.getFullYear(), today.getMonth(), today.getDate())
1603 var youngest = new Date(1912, 6, 23)
1604- var model = historyViewWide.historyModel
1605+ var model = HistoryModel
1606 model.addByDate("https://en.wikipedia.org/wiki/Alan_Turing", "Alan Turing", youngest)
1607 model.addByDate("https://en.wikipedia.org/wiki/Alonzo_Church", "Alonzo Church", new Date(1903, 6, 14))
1608
1609
1610=== modified file 'tests/unittests/qml/tst_NewTabViewWide.qml'
1611--- tests/unittests/qml/tst_NewTabViewWide.qml 2015-08-11 14:28:29 +0000
1612+++ tests/unittests/qml/tst_NewTabViewWide.qml 2015-10-13 11:21:13 +0000
1613@@ -29,13 +29,6 @@
1614 height: 600
1615
1616 Component {
1617- id: historyModel
1618- HistoryModel {
1619- databasePath: ":memory:"
1620- }
1621- }
1622-
1623- Component {
1624 id: bookmarksModel
1625 BookmarksModel {
1626 databasePath: ":memory:"
1627@@ -44,7 +37,6 @@
1628
1629 property NewTabViewWide view
1630 property var bookmarks
1631- property var history
1632 property string homepage: "http://example.com/homepage"
1633
1634 Component {
1635@@ -56,7 +48,6 @@
1636 property int newTabDefaultSection: 0
1637 }
1638 bookmarksModel: bookmarks
1639- historyModel: history
1640 }
1641 }
1642
1643@@ -84,9 +75,12 @@
1644 name: "NewTabViewWide"
1645 when: windowShown
1646
1647+ function initTestCase() {
1648+ HistoryModel.databasePath = ":memory:"
1649+ }
1650+
1651 function init() {
1652 bookmarks = bookmarksModel.createObject()
1653- history = historyModel.createObject()
1654 view = viewComponent.createObject(root)
1655 populate()
1656
1657@@ -103,9 +97,9 @@
1658 }
1659
1660 function populate() {
1661- history.add("http://example.com", "Example Com", "")
1662- history.add("http://example.org", "Example Org", "")
1663- history.add("http://example.net", "Example Net", "")
1664+ HistoryModel.add("http://example.com", "Example Com", "")
1665+ HistoryModel.add("http://example.org", "Example Org", "")
1666+ HistoryModel.add("http://example.net", "Example Net", "")
1667 bookmarks.add("http://example.com", "Example Com", "", "")
1668 bookmarks.add("http://example.org/bar", "Example Org Bar", "", "Folder B")
1669 bookmarks.add("http://example.org/foo", "Example Org Foo", "", "Folder B")
1670@@ -114,8 +108,7 @@
1671 }
1672
1673 function cleanup() {
1674- history.destroy()
1675- history = null
1676+ HistoryModel.clearAll()
1677 bookmarks.destroy()
1678 bookmarks = null
1679
1680@@ -155,7 +148,7 @@
1681 function test_topsites_list() {
1682 // add 8 more top sites so that we are beyond the limit of 10
1683 for (var i = 0; i < 8; i++) {
1684- history.add("http://example.com/" + i, "Example Com " + i, "")
1685+ HistoryModel.add("http://example.com/" + i, "Example Com " + i, "")
1686 }
1687
1688 var items = getListItems("topSitesList", "topSiteItem")
1689@@ -208,22 +201,24 @@
1690
1691 function test_navigate_topsites_by_keyboard() {
1692 var items = getListItems("topSitesList", "topSiteItem")
1693- findChild(view, "topSitesList").currentIndex = 0
1694- verify(items[0].highlighted)
1695- keyClick(Qt.Key_Down)
1696- verify(!items[0].highlighted)
1697- verify(items[1].highlighted)
1698- keyClick(Qt.Key_Down)
1699- verify(items[2].highlighted)
1700- keyClick(Qt.Key_Down) // ensure no scrolling past bottom boundary
1701- verify(items[2].highlighted)
1702- keyClick(Qt.Key_Up)
1703- verify(items[1].highlighted)
1704- keyClick(Qt.Key_Up)
1705- verify(items[0].highlighted)
1706- keyClick(Qt.Key_Up)
1707- verify(items[0].highlighted)
1708+ var list = findChild(view, "topSitesList")
1709+ list.currentIndex = 0
1710+ keyClick(Qt.Key_Right)
1711+ compare(list.currentIndex, 1)
1712+ keyClick(Qt.Key_Right)
1713+ compare(list.currentIndex, 2)
1714+ keyClick(Qt.Key_Right) // ensure list does not wrap around
1715+ compare(list.currentIndex, 2)
1716+ keyClick(Qt.Key_Left)
1717+ compare(list.currentIndex, 1)
1718+ keyClick(Qt.Key_Left)
1719+ compare(list.currentIndex, 0)
1720+ keyClick(Qt.Key_Up)
1721+ compare(list.currentIndex, 0)
1722 compare(releasingKeyboardFocusSpy.count, 1)
1723+ keyClick(Qt.Key_Left)
1724+ compare(list.currentIndex, 0)
1725+ compare(releasingKeyboardFocusSpy.count, 2)
1726 }
1727
1728 function test_activate_topsites_by_keyboard() {
1729@@ -231,7 +226,7 @@
1730 keyClick(Qt.Key_Return)
1731 compare(historyEntryClickedSpy.count, 1)
1732 compare(historyEntryClickedSpy.signalArguments[0][0], "http://example.com")
1733- keyClick(Qt.Key_Down)
1734+ keyClick(Qt.Key_Right)
1735 keyClick(Qt.Key_Return)
1736 compare(historyEntryClickedSpy.count, 2)
1737 compare(historyEntryClickedSpy.signalArguments[1][0], "http://example.org")
1738
1739=== added file 'tests/unittests/qml/tst_PreviewManager.qml'
1740--- tests/unittests/qml/tst_PreviewManager.qml 1970-01-01 00:00:00 +0000
1741+++ tests/unittests/qml/tst_PreviewManager.qml 2015-10-13 11:21:13 +0000
1742@@ -0,0 +1,117 @@
1743+/*
1744+ * Copyright 2015 Canonical Ltd.
1745+ *
1746+ * This file is part of webbrowser-app.
1747+ *
1748+ * webbrowser-app is free software; you can redistribute it and/or modify
1749+ * it under the terms of the GNU General Public License as published by
1750+ * the Free Software Foundation; version 3.
1751+ *
1752+ * webbrowser-app is distributed in the hope that it will be useful,
1753+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1754+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1755+ * GNU General Public License for more details.
1756+ *
1757+ * You should have received a copy of the GNU General Public License
1758+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1759+ */
1760+
1761+import QtQuick 2.4
1762+import QtTest 1.0
1763+import Ubuntu.Test 1.0
1764+import "../../../src/app/webbrowser"
1765+import webbrowserapp.private 0.1
1766+import webbrowsertest.private 0.1
1767+
1768+Item {
1769+ id: root
1770+
1771+ width: 800
1772+ height: 600
1773+
1774+ SignalSpy {
1775+ id: previewSavedSpy
1776+ target: PreviewManager
1777+ signalName: "previewSaved"
1778+ }
1779+
1780+ QtObject {
1781+ id: grabResultMock
1782+ function saveToFile(path) {
1783+ TestContext.createFile(path);
1784+ return true
1785+ }
1786+ }
1787+
1788+ QtObject {
1789+ id: grabResultFailMock
1790+ function saveToFile(path) { return false }
1791+ }
1792+
1793+ UbuntuTestCase {
1794+ name: "PreviewManager"
1795+ when: windowShown
1796+
1797+ property string baseUrl: "http://example.com/"
1798+
1799+ function initTestCase() {
1800+ HistoryModel.databasePath = ":memory:"
1801+ }
1802+
1803+ function init() {
1804+ previewSavedSpy.clear()
1805+ verify(TestContext.removeDirectory(PreviewManager.capturesDir))
1806+ }
1807+
1808+ function populate(count, createPreviewFiles) {
1809+ for (var i = 0; i < 11; i++) {
1810+ var url = baseUrl + i
1811+ HistoryModel.add(url, "Example Com" + i, "")
1812+ if (createPreviewFiles) {
1813+ var path = PreviewManager.previewPathFromUrl(url)
1814+ TestContext.createFile(path)
1815+ }
1816+ }
1817+ }
1818+
1819+ function cleanup() {
1820+ HistoryModel.clearAll()
1821+ }
1822+
1823+ function test_topsites_not_deleted() {
1824+ populate(11, true)
1825+ for (var i = 0; i < 11; i++) {
1826+ var url = baseUrl + i
1827+ PreviewManager.checkDelete(url)
1828+ var path = Qt.resolvedUrl(PreviewManager.previewPathFromUrl(url))
1829+
1830+ // verify that only the item that is outside of the top 10 list
1831+ // gets deleted
1832+ if (i < 10) verify(FileOperations.exists(path))
1833+ else verify(!FileOperations.exists(path))
1834+ }
1835+ }
1836+
1837+ function test_save_preview() {
1838+ var file = Qt.resolvedUrl(PreviewManager.previewPathFromUrl(baseUrl))
1839+
1840+ PreviewManager.saveToDisk(grabResultMock, baseUrl)
1841+ verify(FileOperations.exists(file))
1842+ compare(previewSavedSpy.count, 1)
1843+ compare(previewSavedSpy.signalArguments[0][0], baseUrl)
1844+ compare(previewSavedSpy.signalArguments[0][1], file)
1845+ }
1846+
1847+ function test_save_preview_fail() {
1848+ var path = PreviewManager.previewPathFromUrl(baseUrl)
1849+ var file = Qt.resolvedUrl(path)
1850+
1851+ ignoreWarning("Failed to save preview to disk for %1 (path is %2)".arg(baseUrl).arg(path))
1852+ PreviewManager.saveToDisk(grabResultFailMock, baseUrl)
1853+ verify(!FileOperations.exists(file))
1854+ compare(previewSavedSpy.count, 1)
1855+ compare(previewSavedSpy.signalArguments[0][0], baseUrl)
1856+ compare(previewSavedSpy.signalArguments[0][1], "")
1857+ }
1858+ }
1859+}
1860
1861=== modified file 'tests/unittests/qml/tst_QmlTests.cpp'
1862--- tests/unittests/qml/tst_QmlTests.cpp 2015-09-16 17:19:17 +0000
1863+++ tests/unittests/qml/tst_QmlTests.cpp 2015-10-13 11:21:13 +0000
1864@@ -105,6 +105,22 @@
1865 return QFile(QDir(path).absoluteFilePath(QString("%1.xml").arg(filename))).remove();
1866 }
1867
1868+ Q_INVOKABLE bool createFile(const QString& filePath) {
1869+ // create all the directories necessary for the file to be created
1870+ QFileInfo fileInfo(filePath);
1871+ if (!QFileInfo::exists(fileInfo.path())) {
1872+ QDir::root().mkpath(fileInfo.path());
1873+ }
1874+
1875+ QFile file(fileInfo.absoluteFilePath());
1876+ return file.open(QIODevice::WriteOnly | QIODevice::Text);
1877+ }
1878+
1879+ Q_INVOKABLE bool removeDirectory(const QString& path) {
1880+ QDir dir(path);
1881+ return dir.removeRecursively();
1882+ }
1883+
1884 private:
1885 QTemporaryDir m_testDir1;
1886 QTemporaryDir m_testDir2;
1887@@ -152,6 +168,13 @@
1888 return new TestContext();
1889 }
1890
1891+static QObject* HistoryModel_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine)
1892+{
1893+ Q_UNUSED(engine);
1894+ Q_UNUSED(scriptEngine);
1895+ return new HistoryModelMock();
1896+}
1897+
1898 int main(int argc, char** argv)
1899 {
1900 const char* commonUri = "webbrowsercommon.private";
1901@@ -162,7 +185,7 @@
1902 qmlRegisterType<TabsModel>(browserUri, 0, 1, "TabsModel");
1903 qmlRegisterType<BookmarksModel>(browserUri, 0, 1, "BookmarksModel");
1904 qmlRegisterType<BookmarksFolderListModel>(browserUri, 0, 1, "BookmarksFolderListModel");
1905- qmlRegisterType<HistoryModel>(browserUri, 0, 1, "HistoryModel");
1906+ qmlRegisterSingletonType<HistoryModel>(browserUri, 0, 1, "HistoryModel", HistoryModel_singleton_factory);
1907 qmlRegisterType<HistoryTimeframeModel>(browserUri, 0, 1, "HistoryTimeframeModel");
1908 qmlRegisterType<HistoryLastVisitDateListModel>(browserUri, 0, 1, "HistoryLastVisitDateListModel");
1909 qmlRegisterType<HistoryLastVisitDateModel>(browserUri, 0, 1, "HistoryLastVisitDateModel");
1910@@ -173,7 +196,6 @@
1911
1912 const char* testUri = "webbrowsertest.private";
1913 qmlRegisterSingletonType<TestContext>(testUri, 0, 1, "TestContext", TestContext_singleton_factory);
1914- qmlRegisterType<HistoryModelMock>(testUri, 0, 1, "HistoryModelMock");
1915
1916 return quick_test_main(argc, argv, "QmlTests", nullptr);
1917 }

Subscribers

People subscribed via source and target branches

to status/vote changes: