Merge lp:~michael-sheldon/webbrowser-app/implement-download-folder into lp:webbrowser-app

Proposed by Michael Sheldon on 2015-08-21
Status: Merged
Approved by: Olivier Tilloy on 2015-12-16
Approved revision: 1224
Merged at revision: 1301
Proposed branch: lp:~michael-sheldon/webbrowser-app/implement-download-folder
Merge into: lp:webbrowser-app
Diff against target: 3136 lines (+2222/-266)
40 files modified
debian/control (+0/-1)
debian/webbrowser-app-apparmor.manifest (+5/-2)
debian/webbrowser-app.install (+1/-0)
src/app/ContentDownloadDialog.qml (+0/-50)
src/app/ContentPickerDialog.qml (+0/-97)
src/app/ContentShareDialog.qml (+1/-1)
src/app/Downloader.qml (+17/-12)
src/app/FilePickerDialog.qml (+0/-45)
src/app/MimeTypeMapper.js (+20/-0)
src/app/Share.qml (+1/-1)
src/app/WebViewImpl.qml (+13/-13)
src/app/mime-database.cpp (+30/-0)
src/app/mime-database.h (+3/-0)
src/app/webbrowser/Browser.qml (+133/-24)
src/app/webbrowser/BrowserPageHeader.qml (+27/-12)
src/app/webbrowser/CMakeLists.txt (+6/-0)
src/app/webbrowser/ContentDownloadDialog.qml (+159/-0)
src/app/webbrowser/ContentHandler.qml (+35/-0)
src/app/webbrowser/ContentPickerDialog.qml (+110/-0)
src/app/webbrowser/DownloadDelegate.qml (+226/-0)
src/app/webbrowser/DownloadHandler.qml (+45/-0)
src/app/webbrowser/DownloadsModel.qml (+24/-0)
src/app/webbrowser/DownloadsPage.qml (+253/-0)
src/app/webbrowser/IndeterminateProgressBar.qml (+51/-0)
src/app/webbrowser/SettingsPage.qml (+4/-4)
src/app/webbrowser/downloads-model.cpp (+412/-0)
src/app/webbrowser/downloads-model.h (+110/-0)
src/app/webbrowser/webbrowser-app-content-hub.json (+5/-0)
src/app/webbrowser/webbrowser-app.cpp (+2/-0)
src/app/webcontainer/ContentDownloadDialog.qml (+67/-0)
src/app/webcontainer/ContentPickerDialog.qml (+96/-0)
src/app/webcontainer/WebViewImplOxide.qml (+25/-0)
tests/autopilot/webbrowser_app/emulators/browser.py (+37/-2)
tests/autopilot/webbrowser_app/tests/__init__.py (+9/-0)
tests/autopilot/webbrowser_app/tests/http_server.py (+12/-0)
tests/autopilot/webbrowser_app/tests/test_downloads.py (+77/-0)
tests/autopilot/webbrowser_app/tests/test_settings.py (+2/-2)
tests/unittests/CMakeLists.txt (+1/-0)
tests/unittests/downloads-model/CMakeLists.txt (+13/-0)
tests/unittests/downloads-model/tst_DownloadsModelTests.cpp (+190/-0)
To merge this branch: bzr merge lp:~michael-sheldon/webbrowser-app/implement-download-folder
Reviewer Review Type Date Requested Status
Olivier Tilloy 2015-08-21 Approve on 2015-12-16
PS Jenkins bot continuous-integration Needs Fixing on 2015-12-16
Review via email: mp+268815@code.launchpad.net

Commit message

Add support for handling downloads internally within the browser.

Description of the change

Add support for handling downloads internally within the browser.

To post a comment you must log in.
1123. By Michael Sheldon on 2015-09-01

Only display 'Choose aplication' button in download dialog when we have an application that can open it installed

1124. By Michael Sheldon on 2015-09-01

Merge from trunk

1125. By Michael Sheldon on 2015-09-02

Remove unused import of urlManagement.js

1126. By Michael Sheldon on 2015-09-03

Attempt to find mimetype from filename if a generic octet-stream mimetype is provided

1127. By Michael Sheldon on 2015-09-03

Move mimetype database querying functions out of DownloadsModel and into new MimeDatabase class

1128. By Michael Sheldon on 2015-09-03

Fix focus when showing downloads page

1129. By Michael Sheldon on 2015-09-03

Update tests to use renamed BrowserPageHeader instead of SettingsPageHeader

1130. By Michael Sheldon on 2015-09-04

Implement download selection

1131. By Michael Sheldon on 2015-09-07

Implement QSortFilterProxyModel for DownloadsModel based on Mimetypes so we only show files matching the requested content type when performing content-hub transfers

1132. By Michael Sheldon on 2015-09-07

Merge from trunk

1133. By Michael Sheldon on 2015-09-08

Handle the case where the user wishes to upload one of their previous downloads internally within the browser

1134. By Michael Sheldon on 2015-09-08

Fix check for whether we're in the browser or a webapp when performing an internal file upload

1135. By Michael Sheldon on 2015-09-09

Fix single item selection from downloads page

1136. By Michael Sheldon on 2015-09-09

Merge from trunk

1137. By Michael Sheldon on 2015-09-09

Fix typo

1138. By Michael Sheldon on 2015-09-10

Capitalize mimetype names

1139. By Michael Sheldon on 2015-09-10

Fix support for downloads in webapps

1140. By Michael Sheldon on 2015-09-17

Add tests for new download dialog

1141. By Michael Sheldon on 2015-09-17

Add support for selection mode and multiple delete on downloads page and provide generic action support to BrowserPageHeader

1142. By Michael Sheldon on 2015-09-17

Fix flake8 tests

1143. By Michael Sheldon on 2015-09-17

Merge from trunk

1144. By Michael Sheldon on 2015-09-17

Fix flake8 tests

1145. By Michael Sheldon on 2015-09-18

Add text/vcard to content-hub mimetype mappings

1146. By Michael Sheldon on 2015-09-20

Add basic empty state to downloads page

1147. By Michael Sheldon on 2015-09-21

Fallback to generic icons for mimetypes that don't have an icon in the current theme

1148. By Michael Sheldon on 2015-09-21

Show filename in download dialog

1149. By Michael Sheldon on 2015-09-21

Merge from trunk

1150. By Michael Sheldon on 2015-09-21

Remove unused signal from BrowserPageHeader and update docs

1151. By Michael Sheldon on 2015-09-22

Bring download dialog style closer to visual spec

1152. By Michael Sheldon on 2015-09-22

Fix download icon fallback when the system theme doesn't have a generic icon

1153. By Michael Sheldon on 2015-09-22

Update download dialog style based on visual spec

1154. By Michael Sheldon on 2015-09-22

Only show internal browser downloads on the downloads page

1155. By Michael Sheldon on 2015-09-22

Remove ubuntu shape from download page icons to match visual spec

1156. By Michael Sheldon on 2015-09-22

Remove unused extension property from downloads model

1157. By Michael Sheldon on 2015-09-22

Use Images instead of Icons in DownloadDelegate for faster load time and smoother list scrolling

1158. By Michael Sheldon on 2015-09-23

Only show download dialog description when content-hub has an application that can open the download directly

1159. By Michael Sheldon on 2015-09-23

Calculate download filename prior to setting metadata title

1160. By Michael Sheldon on 2015-09-23

Merge from trunk

1161. By Michael Sheldon on 2015-09-24

Fix merge error

1162. By Michael Sheldon on 2015-09-25

Remove unecessary check for -x-generic icon now that we provide a fallback in the iconForMimetype method when this isn't available in the theme

1163. By Michael Sheldon on 2015-09-25

Add edit button to downloads page

1164. By Michael Sheldon on 2015-09-25

Fix eliding and spacing on DownloadDelegate labels

1165. By Michael Sheldon on 2015-09-25

Update DownloadDelegate label sizes to match visual design

1166. By Michael Sheldon on 2015-09-25

Update BrowserPageHeader and DownloadsPage styles to match visual spec

1167. By Michael Sheldon on 2015-09-25

Fix multi-selection on DownloadsPage when using the edit button

1168. By Michael Sheldon on 2015-09-29

Fetch download items from database in batches instead of loading all at once

1169. By Michael Sheldon on 2015-09-30

Take the user directly to the downloads page when a download starts

1170. By Michael Sheldon on 2015-09-30

Fix thumbnailing on download page whilst downloading videos or images

1171. By Michael Sheldon on 2015-10-01

Add progress bar and controls for downloads (not yet connected to UDM)

1172. By Michael Sheldon on 2015-10-21

Merge from trunk

1173. By Michael Sheldon on 2015-10-22

Connect progress and cancel buttons to running downloads

1174. By Michael Sheldon on 2015-10-22

Avoid duplication of new downloads at both the bottom and top of the downloads list

1175. By Michael Sheldon on 2015-10-23

Remove downloads that have been cancelled while the browser was closed

1176. By Michael Sheldon on 2015-10-23

Add incomplete downloads to the download model when restoring database on startup

1177. By Michael Sheldon on 2015-10-23

Allow access to ~/Downloads in the app armor manifest

1178. By Michael Sheldon on 2015-10-23

Switch to default apparmor template to allow for thumbnailer support

1179. By Michael Sheldon on 2015-10-23

Move downloads based on signal from global DownloadManager object, so we can handle downloads that finished after an app restart

1180. By Michael Sheldon on 2015-10-26

Merge from trunk

1181. By Michael Sheldon on 2015-10-26

Fix typo in app armor profile

1182. By Michael Sheldon on 2015-10-26

Fix usage of renamed SettingsPageHeader -> BrowserPageHeader

1183. By Michael Sheldon on 2015-10-26

Fix download model index position when removing downloads

1184. By Michael Sheldon on 2015-10-28

Display error messages for failed downloads

1185. By Michael Sheldon on 2015-11-02

Don't display delete action on downloads that are still in progress

1186. By Michael Sheldon on 2015-11-02

Update error display on downloads page to match visual design

1187. By Michael Sheldon on 2015-11-16

Remove progress storage from download database as this is now available via the UDM downloads model

1188. By Michael Sheldon on 2015-11-16

Override server mimetype with detected mimetype for downloads when they complete

1189. By Michael Sheldon on 2015-11-16

Merge from trunk

1190. By Michael Sheldon on 2015-11-17

Update UDM import version

1191. By Michael Sheldon on 2015-11-23

Allow the browser to handle conditions where a download has been paused, cancelled or resumed by another process (e.g. by the transfer indicator)

1192. By Michael Sheldon on 2015-11-27

Fix download tests

1193. By Michael Sheldon on 2015-11-27

Fix flake8 failures

1194. By Michael Sheldon on 2015-11-27

Only allow downloads on non-desktop form factors

1195. By Michael Sheldon on 2015-11-30

Use success of download Loader to determine whether UDM bindings are available on this platform

1196. By Michael Sheldon on 2015-11-30

Merge from trunk

1197. By Michael Sheldon on 2015-11-30

Set download menu option enabled property instead of visibile property when UDM isn't available

1198. By Michael Sheldon on 2015-11-30

Add copyright notice to DownloadHandler

1199. By Michael Sheldon on 2015-12-09

Merge from trunk

1200. By Michael Sheldon on 2015-12-09

Add 'Ctrl-J' as a shortcut to the downloads page and disable shortcuts whilst the download page is visible

Olivier Tilloy (osomon) wrote :
Download full text (3.3 KiB)

Can the autopilot tests detect at run time whether the platform supports downloads, and be skipped where relevant (typically a desktop for udm is not installed)?

What (and how) does the test [typeof(webapp) == "undefined"] check exactly?

In src/app/ContentPickerDialog.qml, this breaks encapsulation:
    var downloadPage = browser.showDownloadsPage()
Can’t we have the dialog emit a signal with the necessary parameters, which WebViewImpl would connect to and forward, and Browser would connect to that signal from the webview and act accordingly?
The same applies to Downloader.qml.

In mimeTypeRegexForContentType(), shouldn’t the regexp for vcards also match "text/vcard" ?
And for documents, shouldn’t it accept the mimetypes listed at https://bazaar.launchpad.net/~ubuntu-docviewer-dev/ubuntu-docviewer-app/lo-viewer/view/head:/src/plugin/file-qml-plugin/docviewerutils.cpp#L52 ?

In MimeDatabase::iconForMimetype() and MimeDatabase::nameForMimetype(), why instantiate a new QMimeDatabase instead of reusing m_database ?

In MimeDatabase::iconForMimetype(), please wrap "save" in a call to QStringLiteral(). On a related note, given that MimeDatabase is a generic helper (not necessarily used only for downloads), wouldn’t it make more sense to return an empty icon, and on the caller’s end replace it by "save" if empty?

In Browser.qml, version 1.0 of Ubuntu.Content is imported. There are other places in the code where version 0.1 is being imported. Can this be made consistent?

On a related note, if Browser.qml imports Ubuntu.Content, then a runtime dependency on qtdeclarative5-ubuntu-content1 needs to be added to the packaging information for webbrowser-app. But qtdeclarative5-ubuntu-content1 is in universe, so we can’t have that dependency. This needs to be decoupled somehow, by having the code that refers to Ubuntu.Content dynamically loaded. Otherwise the browser cannot be started without qtdeclarative5-ubuntu-content1.

DownloadDelegate.qml appears to be breaking encapsulation by referring to 'downloadManager'. The entire Component.onCompleted block can be moved to where this DownloadDelegate is being instantiated, that will probably make it easier to not break encapsulation.

In the file picker when uploading a file, the "select all" button should be disable/not visible if multiple files are not allowed. And how do I actually pick a file to upload? I’m seeing the selection mode, I can tick the checkbox for one given file, but how do I validate (there’s only the selectAll and delete actions in the header)? If there isn’t one already, an autopilot test to validate this use case would be good to have.

Could the DownloadsMimetypeModel be re-implemented purely in QML using a SortFilterModel (https://developer.ubuntu.com/api/apps/qml/sdk-15.04/Ubuntu.Components.SortFilterModel/), by any chance? If not, in DownloadsMimetypeModel::filterAcceptsRow() instead of re-instantiating a QRegExp at each call, it should be cached as a member attribute of the model.

Ideally, the DownloadsModel class should be unit tested, with a decent test coverage. Let’s not block on this, but let’s also plan on adding the missing tests soon. Or as a temporary trade-off, can you...

Read more...

review: Needs Fixing
1201. By Michael Sheldon on 2015-12-10

Reuse existing QMimeDatabase instead of instantiating new ones

1202. By Michael Sheldon on 2015-12-10

Return empty string if we don't have a mimetype icon and replace with the 'save' icon on the caller side

1203. By Michael Sheldon on 2015-12-10

Replace DownloadsMimetypeModel with a SortFilterModel

1204. By Michael Sheldon on 2015-12-10

Fix internal content picking

1205. By Michael Sheldon on 2015-12-10

Expand mimetype regexes for contacts and documents to cover all supported cases

1206. By Michael Sheldon on 2015-12-10

Consistently import the same version of the content hub bindings

1207. By Michael Sheldon on 2015-12-10

Load ContentHub bindings for export handling dynamically to allow for systems where ContentHub isn't installed

1208. By Michael Sheldon on 2015-12-11

Add unit tests for downloads model

Olivier Tilloy (osomon) wrote :

Thanks for adding the unit tests for DownloadsModel. The current coverage is 63.3%, this is not bad but it could easily be improved. The following methods are not covered by the unit tests:
 DownloadsModel::setPath()
 DownloadsModel::setError()
 DownloadsModel::moveToDownloads()
 DownloadsModel::deleteDownload()
 DownloadsModel::pauseDownload()
 DownloadsModel::resumeDownload()
 DownloadsModel::removeExistingEntryFromDatabase()
 DownloadsModel::canFetchMore()

Also, since this is a list model, please consider monitoring the rowsAdded(), rowsRemoved(), rowsMoved() and dataChanged() signals, and verifying that they are emitted when expected, with the correct parameters.

1209. By Michael Sheldon on 2015-12-14

Implement separate ContentDownloadDialogs for webbrowser and webcontainer

Olivier Tilloy (osomon) wrote :

The ContentDownloadDialog.qml implementations don’t need to be an instance of Component, their top-level element can be PopupBase.

The decoupling done at revision 1209 looks good. I suppose you’re aware of it, but just as a reminder to myself: src/app/ContentPickerDialog.qml still breaks encapsulation by referring to 'webapp' and 'browser'.

1210. By Michael Sheldon on 2015-12-15

Split ContentPickerDialog into specific webbrowser and webcontainer implementation and remove FilePickerDialog (no longer functional under confinement)

Olivier Tilloy (osomon) wrote :

In Browser.qml, the condition for the enabled-ness of the keyboard shortcuts could be improved by merging the changes in lp:~osomon/webbrowser-app/keyboard-shortcuts-focus-fixes and ensuring that tabContainer is not visible when downloadsContainer is.

Alternatively, if we’re in a rush to land this, I’ll merge back your changes in my branch after it lands, and will fix it there.

Olivier Tilloy (osomon) wrote :

The runtime dependency on qml-module-qtquick-dialogs should be removed, now that FilePickerDialog.qml was removed.

review: Needs Fixing
1211. By Michael Sheldon on 2015-12-15

Disable download tests on desktop systems

1212. By Michael Sheldon on 2015-12-15

Fix autopilot tests for download dialog after parenting change

1213. By Michael Sheldon on 2015-12-15

Remove no longer required dependence on qml-module-qtquick-dialogs

Olivier Tilloy (osomon) wrote :

src/app/webbrowser/ContentPickerDialog.qml breaks encapsulation by doing this:

    onCancelPressed: webview.focus = true

Instead, you should do:

    onCancelPressed: WebView.view.focus = true

(the 'WebView.view' attached property points to the parent webview).

By the way, is this still needed, and if so shouldn’t it be WebView.view.forceActiveFocus() ?

review: Needs Fixing
1214. By Michael Sheldon on 2015-12-15

Don't show incomplete downloads in picker/selection mode

1215. By Michael Sheldon on 2015-12-15

Remove unnecessary focus changes

1216. By Michael Sheldon on 2015-12-15

Fix flake8 tests

1217. By Michael Sheldon on 2015-12-15

Fix encapsulation for ContentPickerDialog

1218. By Michael Sheldon on 2015-12-15

Simplify download delegate visibility condition

Olivier Tilloy (osomon) wrote :

When uploading a file and selecting the browser from the list of peers, nothing happens, and I’m seeing the following error in the logs:

file:///usr/share/webbrowser-app/webbrowser/ContentPickerDialog.qml:57: ReferenceError: WebView is not defined

If I add an "import com.canonical.Oxide 1.0" statement at the top of that file, the error becomes:

file:///usr/share/webbrowser-app/webbrowser/ContentPickerDialog.qml:58: TypeError: Cannot call method 'showDownloadsPage' of null

This is because the "WebView.view" attached property is valid only in the context of the top-level element (picker). Adding a top level [readonly property var webview: WebView.view] property to picker, and referring to it with [var downloadPage = picker.webview.showDownloadsPage()] fixes the issue.

review: Needs Fixing
Olivier Tilloy (osomon) wrote :

Note that with the suggestions above, the following warning is printed out in the logs, but it’s harmless:

QQmlExpression: Expression file:///usr/share/webbrowser-app/webbrowser/ContentPickerDialog.qml:38:40 depends on non-NOTIFYable properties:
    OxideQQuickWebViewAttached::view

Olivier Tilloy (osomon) wrote :

When showing the downloads page (both from the drawer menu or as a file picker), I’m seeing the following warnings in the console:

file:///usr/share/webbrowser-app/webbrowser/DownloadsPage.qml:54:5: QML BrowserPageHeader: Binding loop detected for property "height"
file:///usr/lib/arm-linux-gnueabihf/qt5/qml/Ubuntu/Content/ContentPeerPicker10.qml:187: TypeError: Cannot read property 'peers' of null
file:///usr/share/webbrowser-app/webbrowser/DownloadsPage.qml:54:5: QML BrowserPageHeader: Cannot specify top, bottom, verticalCenter, fill or centerIn anchors for items inside Column. Column will not function.

Can they be addressed?

Olivier Tilloy (osomon) wrote :

Note: a way to avoid the warning about the non-NOTIFYable property is to not define a top-level property, and instead in onPeerSelected do the following:

    var downloadPage = picker.WebView.view.showDownloadsPage()

1219. By Michael Sheldon on 2015-12-16

Fix access to downloads page when performing internal uploads

1220. By Michael Sheldon on 2015-12-16

Fix warnings from BrowserPageHeader

1221. By Michael Sheldon on 2015-12-16

Move all content hub imports to 1.3

1222. By Michael Sheldon on 2015-12-16

Update content hub imports to 1.3

1223. By Michael Sheldon on 2015-12-16

Don't destroy SingleDownloads when they finish as they're still refered to by the shared downloads model

1224. By Michael Sheldon on 2015-12-16

Fix peer picker autopilot test

1225. By Michael Sheldon on 2015-12-16

Fix icon sizing when thumbnailer returns an invalid icon

Olivier Tilloy (osomon) wrote :

Looks good to me now. I have a bunch of minor comments (on the code itself, no functional issue), but I’ll keep them for a bug report after we manage to land this, let’s not delay this any longer.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/control'
2--- debian/control 2015-09-30 09:26:57 +0000
3+++ debian/control 2015-12-16 16:25:40 +0000
4@@ -44,7 +44,6 @@
5 qml-module-qt-labs-folderlistmodel,
6 qml-module-qt-labs-settings,
7 qml-module-qtquick2 (>= 5.4),
8- qml-module-qtquick-dialogs,
9 qml-module-qtquick-window2 (>= 5.3),
10 qtdeclarative5-ubuntu-ui-toolkit-plugin (>= 1.3) | qtdeclarative5-ubuntu-ui-toolkit-plugin-gles (>= 1.3),
11 qtdeclarative5-ubuntu-web-plugin (= ${binary:Version}),
12
13=== modified file 'debian/webbrowser-app-apparmor.manifest'
14--- debian/webbrowser-app-apparmor.manifest 2015-10-22 15:07:38 +0000
15+++ debian/webbrowser-app-apparmor.manifest 2015-12-16 16:25:40 +0000
16@@ -3,7 +3,6 @@
17 "webbrowser-app": {
18 "binary": "/usr/bin/webbrowser-app",
19 "profile_name": "webbrowser-app",
20- "template": "ubuntu-webapp",
21 "policy_vendor": "ubuntu",
22 "policy_version": 1.3,
23 "policy_groups": [
24@@ -31,8 +30,12 @@
25 },
26 "read_path": [
27 "/usr/share/applications/",
28+ "/custom/vendor/here/location-provider/consent/*.html",
29 "@{HOME}/.local/share/applications/",
30- "/custom/vendor/here/location-provider/consent/*.html"
31+ "@{HOME}/Downloads/"
32+ ],
33+ "write_path": [
34+ "@{HOME}/Downloads/"
35 ]
36 }
37 }
38
39=== modified file 'debian/webbrowser-app.install'
40--- debian/webbrowser-app.install 2015-10-05 09:49:21 +0000
41+++ debian/webbrowser-app.install 2015-12-16 16:25:40 +0000
42@@ -7,4 +7,5 @@
43 usr/share/applications/webbrowser-app.desktop
44 usr/share/locale/*/LC_MESSAGES/webbrowser-app.mo
45 usr/share/url-dispatcher/
46+usr/share/content-hub/peers/webbrowser-app
47 debian/usr.bin.webbrowser-app etc/apparmor.d
48
49=== removed file 'src/app/ContentDownloadDialog.qml'
50--- src/app/ContentDownloadDialog.qml 2015-08-10 15:22:00 +0000
51+++ src/app/ContentDownloadDialog.qml 1970-01-01 00:00:00 +0000
52@@ -1,50 +0,0 @@
53-/*
54- * Copyright 2014-2015 Canonical Ltd.
55- *
56- * This file is part of webbrowser-app.
57- *
58- * webbrowser-app is free software; you can redistribute it and/or modify
59- * it under the terms of the GNU General Public License as published by
60- * the Free Software Foundation; version 3.
61- *
62- * webbrowser-app is distributed in the hope that it will be useful,
63- * but WITHOUT ANY WARRANTY; without even the implied warranty of
64- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
65- * GNU General Public License for more details.
66- *
67- * You should have received a copy of the GNU General Public License
68- * along with this program. If not, see <http://www.gnu.org/licenses/>.
69- */
70-
71-import QtQuick 2.4
72-import Ubuntu.Components 1.3
73-import Ubuntu.Components.Popups 1.3
74-import Ubuntu.Content 0.1
75-
76-PopupBase {
77- id: downloadDialog
78- anchors.fill: parent
79- property var activeTransfer
80- property var downloadId
81- property alias contentType: peerPicker.contentType
82-
83- Rectangle {
84- anchors.fill: parent
85- ContentPeerPicker {
86- id: peerPicker
87- handler: ContentHandler.Destination
88- visible: parent.visible
89-
90- onPeerSelected: {
91- activeTransfer = peer.request()
92- activeTransfer.downloadId = downloadDialog.downloadId
93- activeTransfer.state = ContentTransfer.Downloading
94- PopupUtils.close(downloadDialog)
95- }
96-
97- onCancelPressed: {
98- PopupUtils.close(downloadDialog)
99- }
100- }
101- }
102-}
103
104=== removed file 'src/app/ContentPickerDialog.qml'
105--- src/app/ContentPickerDialog.qml 2015-08-18 07:51:11 +0000
106+++ src/app/ContentPickerDialog.qml 1970-01-01 00:00:00 +0000
107@@ -1,97 +0,0 @@
108-/*
109- * Copyright 2014-2015 Canonical Ltd.
110- *
111- * This file is part of webbrowser-app.
112- *
113- * webbrowser-app is free software; you can redistribute it and/or modify
114- * it under the terms of the GNU General Public License as published by
115- * the Free Software Foundation; version 3.
116- *
117- * webbrowser-app is distributed in the hope that it will be useful,
118- * but WITHOUT ANY WARRANTY; without even the implied warranty of
119- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
120- * GNU General Public License for more details.
121- *
122- * You should have received a copy of the GNU General Public License
123- * along with this program. If not, see <http://www.gnu.org/licenses/>.
124- */
125-
126-import QtQuick 2.4
127-import Ubuntu.Components 1.3
128-import Ubuntu.Components.Popups 1.3 as Popups
129-import Ubuntu.Content 0.1
130-import "MimeTypeMapper.js" as MimeTypeMapper
131-
132-Component {
133- Popups.PopupBase {
134- id: picker
135- objectName: "contentPickerDialog"
136-
137- // Set the parent at construction time, instead of letting show()
138- // set it later on, which for some reason results in the size of
139- // the dialog not being updated.
140- parent: QuickUtils.rootItem(this)
141-
142- property var activeTransfer
143-
144- Rectangle {
145- anchors.fill: parent
146-
147- ContentTransferHint {
148- anchors.fill: parent
149- activeTransfer: picker.activeTransfer
150- }
151-
152- ContentPeerPicker {
153- id: peerPicker
154- anchors.fill: parent
155- visible: true
156- contentType: ContentType.All
157- handler: ContentHandler.Source
158-
159- onPeerSelected: {
160- if (model.allowMultipleFiles) {
161- peer.selectionType = ContentTransfer.Multiple
162- } else {
163- peer.selectionType = ContentTransfer.Single
164- }
165- picker.activeTransfer = peer.request()
166- stateChangeConnection.target = picker.activeTransfer
167- }
168-
169- onCancelPressed: {
170- webview.focus = true
171- model.reject()
172- }
173- }
174- }
175-
176- Connections {
177- id: stateChangeConnection
178- target: null
179- onStateChanged: {
180- if (picker.activeTransfer.state === ContentTransfer.Charged) {
181- var selectedItems = []
182- for(var i in picker.activeTransfer.items) {
183- selectedItems.push(String(picker.activeTransfer.items[i].url).replace("file://", ""))
184- }
185- model.accept(selectedItems)
186- }
187- }
188- }
189-
190- Component.onCompleted: {
191- if(acceptTypes.length === 1) {
192- var contentType = MimeTypeMapper.mimeTypeToContentType(acceptTypes[0])
193- if(contentType == ContentType.Unknown) {
194- // If we don't recognise the type, allow uploads from any app
195- contentType = ContentType.All
196- }
197- peerPicker.contentType = contentType
198- } else {
199- peerPicker.contentType = ContentType.All
200- }
201- show()
202- }
203- }
204-}
205
206=== modified file 'src/app/ContentShareDialog.qml'
207--- src/app/ContentShareDialog.qml 2015-08-10 15:22:00 +0000
208+++ src/app/ContentShareDialog.qml 2015-12-16 16:25:40 +0000
209@@ -19,7 +19,7 @@
210 import QtQuick 2.4
211 import Ubuntu.Components 1.3
212 import Ubuntu.Components.Popups 1.3
213-import Ubuntu.Content 0.1
214+import Ubuntu.Content 1.3
215
216 PopupBase {
217 id: shareDialog
218
219=== modified file 'src/app/Downloader.qml'
220--- src/app/Downloader.qml 2015-08-10 15:22:00 +0000
221+++ src/app/Downloader.qml 2015-12-16 16:25:40 +0000
222@@ -19,18 +19,18 @@
223 import QtQuick 2.4
224 import Ubuntu.Components 1.3
225 import Ubuntu.Components.Popups 1.3
226-import Ubuntu.DownloadManager 0.1
227-import Ubuntu.Content 0.1
228+import Ubuntu.DownloadManager 1.2
229+import Ubuntu.Content 1.3
230 import "MimeTypeMapper.js" as MimeTypeMapper
231 import "FileExtensionMapper.js" as FileExtensionMapper
232
233 Item {
234 id: downloadItem
235
236- Component {
237- id: downloadDialog
238- ContentDownloadDialog { }
239- }
240+ property string filename
241+ property string mimeType
242+
243+ signal showDownloadDialog(string downloadId, var contentType, var downloader, string filename, string mimeType)
244
245 Component {
246 id: metadataComponent
247@@ -42,15 +42,13 @@
248 Component {
249 id: downloadComponent
250 SingleDownload {
251+ id: downloader
252 autoStart: false
253 property var contentType
254+ property string url
255+
256 onDownloadIdChanged: {
257- PopupUtils.open(downloadDialog, downloadItem, {"contentType" : contentType, "downloadId" : downloadId})
258- }
259-
260- onFinished: {
261- metadata.destroy()
262- destroy()
263+ showDownloadDialog(downloadId, contentType, downloader, downloadItem.filename, downloadItem.mimeType)
264 }
265 }
266 }
267@@ -62,11 +60,13 @@
268 singleDownload.headers = headers
269 }
270 singleDownload.metadata = metadata
271+ singleDownload.url = url
272 singleDownload.download(url)
273 }
274
275 function downloadPicture(url, headers) {
276 var metadata = metadataComponent.createObject(downloadItem)
277+ downloadItem.mimeType = "image/*"
278 download(url, ContentType.Pictures, headers, metadata)
279 }
280
281@@ -86,7 +86,12 @@
282 contentType = ContentType.Music
283 metadata.extract = true
284 }
285+ if (!filename) {
286+ filename = url.toString().split("/").pop()
287+ }
288 metadata.title = filename
289+ downloadItem.filename = filename
290+ downloadItem.mimeType = mimeType
291 download(url, contentType, headers, metadata)
292 }
293
294
295=== removed file 'src/app/FilePickerDialog.qml'
296--- src/app/FilePickerDialog.qml 2015-08-25 08:36:06 +0000
297+++ src/app/FilePickerDialog.qml 1970-01-01 00:00:00 +0000
298@@ -1,45 +0,0 @@
299-/*
300- * Copyright 2014-2015 Canonical Ltd.
301- *
302- * This file is part of webbrowser-app.
303- *
304- * webbrowser-app is free software; you can redistribute it and/or modify
305- * it under the terms of the GNU General Public License as published by
306- * the Free Software Foundation; version 3.
307- *
308- * webbrowser-app is distributed in the hope that it will be useful,
309- * but WITHOUT ANY WARRANTY; without even the implied warranty of
310- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
311- * GNU General Public License for more details.
312- *
313- * You should have received a copy of the GNU General Public License
314- * along with this program. If not, see <http://www.gnu.org/licenses/>.
315- */
316-
317-import QtQuick 2.4
318-import QtQuick.Dialogs 1.0
319-import Ubuntu.Components 1.3
320-import Ubuntu.Components.Popups 1.3 as Popups
321-
322-Component {
323- Popups.Dialog {
324- FileDialog {
325- id: fileDialog
326- title: i18n.tr("Please choose a file")
327- selectMultiple: model.allowMultipleFiles
328-
329- onAccepted: {
330- var selectedFiles = []
331- for(var i in fileDialog.fileUrls) {
332- selectedFiles.push(fileDialog.fileUrls[i].replace("file://", ""))
333- }
334- model.accept(selectedFiles)
335- }
336-
337- onRejected: {
338- model.reject()
339- }
340- Component.onCompleted: visible = true
341- }
342- }
343-}
344
345=== modified file 'src/app/MimeTypeMapper.js'
346--- src/app/MimeTypeMapper.js 2015-09-23 14:00:09 +0000
347+++ src/app/MimeTypeMapper.js 2015-12-16 16:25:40 +0000
348@@ -42,3 +42,23 @@
349 return ContentType.Unknown;
350 }
351 }
352+
353+function mimeTypeRegexForContentType(contentType) {
354+ switch (contentType) {
355+ case ContentType.Pictures:
356+ return /image\/.*/;
357+ case ContentType.Music:
358+ return /audio\/.*/;
359+ case ContentType.Videos:
360+ return /video\/.*/;
361+ case ContentType.Contacts:
362+ return /text\/(x-vcard|vcard)/;
363+ case ContentType.EBooks:
364+ return /application\/(epub.*|vnd.amazon.ebook|x-mobipocket-ebook|x-fictionbook+xml|x-ms-reader)/;
365+ case ContentType.Documents:
366+ return /(text\/.*|application\/pdf|application\/x-pdf|application\/vnd\.pdf|application\/vnd\.oasis\.opendocument.*|application\/msword|application\/vnd\.openxmlformats-officedocument\.wordprocessingml\.document|application\/vnd\.openxmlformats-officedocument\.spreadsheetml\.sheet|application\/vnd\.openxmlformats-officedocument\.presentationml\.presentation|application\/vnd\.ms-excel|application\/vnd\.ms-powerpoint)/;
367+ case ContentType.Unknown:
368+ case ContentType.All:
369+ return /.*/;
370+ }
371+}
372
373=== modified file 'src/app/Share.qml'
374--- src/app/Share.qml 2015-09-01 12:41:13 +0000
375+++ src/app/Share.qml 2015-12-16 16:25:40 +0000
376@@ -19,7 +19,7 @@
377 import QtQuick 2.4
378 import Ubuntu.Components 1.3
379 import Ubuntu.Components.Popups 1.3
380-import Ubuntu.Content 0.1
381+import Ubuntu.Content 1.3
382
383 Item {
384 id: shareItem
385
386=== modified file 'src/app/WebViewImpl.qml'
387--- src/app/WebViewImpl.qml 2015-09-01 07:02:13 +0000
388+++ src/app/WebViewImpl.qml 2015-12-16 16:25:40 +0000
389@@ -34,7 +34,8 @@
390 confirmDialog: ConfirmDialog {}
391 promptDialog: PromptDialog {}
392 beforeUnloadDialog: BeforeUnloadDialog {}
393- filePicker: filePickerLoader.item
394+
395+ signal showDownloadDialog(string downloadId, var contentType, var downloader, string filename, string mimeType)
396
397 QtObject {
398 id: internal
399@@ -62,9 +63,10 @@
400 }
401 headers["User-Agent"] = webview.context.userAgent
402 // Work around https://launchpad.net/bugs/1487090 by guessing the mime type
403- // from the suggested filename or URL if oxide hasn’t provided one.
404+ // from the suggested filename or URL if oxide hasn’t provided one, or if
405+ // the server has provided the generic application/octet-stream mime type.
406 var mimeType = request.mimeType
407- if (!mimeType) {
408+ if (!mimeType || mimeType == "application/octet-stream") {
409 mimeType = MimeDatabase.filenameToMimeType(request.suggestedFilename)
410 }
411 if (!mimeType) {
412@@ -87,20 +89,18 @@
413 }
414
415 Loader {
416- id: filePickerLoader
417- source: formFactor == "desktop" ? "FilePickerDialog.qml" : "ContentPickerDialog.qml"
418- asynchronous: true
419- }
420-
421- Loader {
422 id: downloadLoader
423- // TODO: Use the ubuntu download manager on desktop as well
424- // (https://launchpad.net/bugs/1477310). This will require to have
425- // ubuntu-download-manager in main (https://launchpad.net/bugs/1488425).
426- source: formFactor == "desktop" ? "" : "Downloader.qml"
427+ source: "Downloader.qml"
428 asynchronous: true
429 }
430
431+ Connections {
432+ target: downloadLoader.item
433+ onShowDownloadDialog: {
434+ showDownloadDialog(downloadId, contentType, downloader, filename, mimeType)
435+ }
436+ }
437+
438 function requestGeolocationPermission(request) {
439 PopupUtils.open(Qt.resolvedUrl("GeolocationPermissionRequest.qml"),
440 webview.currentWebview, {"request": request})
441
442=== modified file 'src/app/mime-database.cpp'
443--- src/app/mime-database.cpp 2015-08-26 11:29:00 +0000
444+++ src/app/mime-database.cpp 2015-12-16 16:25:40 +0000
445@@ -16,6 +16,8 @@
446 * along with this program. If not, see <http://www.gnu.org/licenses/>.
447 */
448
449+#include <QIcon>
450+
451 #include "mime-database.h"
452
453 MimeDatabase::MimeDatabase(QObject* parent)
454@@ -31,3 +33,31 @@
455 }
456 return QString();
457 }
458+
459+/*!
460+ Provide the system icon name for a given mimetype
461+*/
462+QString MimeDatabase::iconForMimetype(const QString& mimetypeString) const
463+{
464+ QMimeType mimetype = m_database.mimeTypeForName(mimetypeString);
465+ if (mimetype.iconName().isEmpty() || !QIcon::hasThemeIcon(mimetype.iconName())) {
466+ if (QIcon::hasThemeIcon(mimetype.genericIconName())) {
467+ return mimetype.genericIconName();
468+ } else {
469+ return "";
470+ }
471+ } else {
472+ return mimetype.iconName();
473+ }
474+}
475+
476+/*!
477+ Provide the user friendly name for a given mimetype
478+*/
479+QString MimeDatabase::nameForMimetype(const QString& mimetypeString) const
480+{
481+ QMimeType mimetype = m_database.mimeTypeForName(mimetypeString);
482+ return mimetype.comment();
483+}
484+
485+
486
487=== modified file 'src/app/mime-database.h'
488--- src/app/mime-database.h 2015-08-26 11:29:00 +0000
489+++ src/app/mime-database.h 2015-12-16 16:25:40 +0000
490@@ -31,6 +31,9 @@
491 explicit MimeDatabase(QObject* parent=0);
492
493 Q_INVOKABLE QString filenameToMimeType(const QString& filename) const;
494+ Q_INVOKABLE QString iconForMimetype(const QString& mimetypeString) const;
495+ Q_INVOKABLE QString nameForMimetype(const QString& mimetypeString) const;
496+
497
498 private:
499 QMimeDatabase m_database;
500
501=== modified file 'src/app/webbrowser/Browser.qml'
502--- src/app/webbrowser/Browser.qml 2015-12-04 11:06:47 +0000
503+++ src/app/webbrowser/Browser.qml 2015-12-16 16:25:40 +0000
504@@ -37,6 +37,9 @@
505
506 currentWebview: tabsModel && tabsModel.currentTab ? tabsModel.currentTab.webview : null
507
508+ property var downloadsModel: (downloadsModelLoader.status == Loader.Ready) ? downloadsModelLoader.item : null
509+ property var downloadManager: (downloadHandlerLoader.status == Loader.Ready) ? downloadHandlerLoader.item : null
510+
511 property bool newSession: false
512
513 property bool incognito: false
514@@ -168,7 +171,7 @@
515
516 FocusScope {
517 anchors.fill: parent
518- visible: !settingsContainer.visible && !historyViewLoader.active && !bookmarksViewLoader.active
519+ visible: !settingsContainer.visible && !historyViewLoader.active && !bookmarksViewLoader.active && !downloadsContainer.visible
520
521 FocusScope {
522 id: tabContainer
523@@ -537,6 +540,15 @@
524 onTriggered: chrome.findInPageMode = true
525 },
526 Action {
527+ objectName: "downloads"
528+ text: i18n.tr("Downloads")
529+ iconName: "save"
530+ enabled: downloadHandlerLoader.status == Loader.Ready
531+ onTriggered: {
532+ currentWebview.showDownloadsPage()
533+ }
534+ },
535+ Action {
536 objectName: "privatemode"
537 text: browser.incognito ? i18n.tr("Leave Private Mode") : i18n.tr("Private Mode")
538 iconName: "private-browsing"
539@@ -905,6 +917,28 @@
540 }
541 }
542
543+ FocusScope {
544+ id: downloadsContainer
545+
546+ visible: children.length > 0
547+ anchors.fill: parent
548+
549+ Component {
550+ id: downloadsComponent
551+
552+ DownloadsPage {
553+ anchors.fill: parent
554+ focus: true
555+ downloadsModel: browser.downloadsModel
556+ onDone: destroy()
557+ Keys.onEscapePressed: {
558+ destroy()
559+ internal.resetFocus()
560+ }
561+ }
562+ }
563+ }
564+
565 TabsModel {
566 id: publicTabsModel
567 }
568@@ -930,6 +964,17 @@
569 }
570 }
571
572+ Loader {
573+ id: downloadsModelLoader
574+ source: "DownloadsModel.qml"
575+ asynchronous: true
576+ }
577+
578+ Loader {
579+ id: downloadHandlerLoader
580+ source: "DownloadHandler.qml"
581+ }
582+
583 Component {
584 id: tabComponent
585
586@@ -950,6 +995,7 @@
587 readonly property bool current: tab.current
588
589 currentWebview: browser.currentWebview
590+ filePicker: filePickerLoader.item
591
592 anchors.fill: parent
593 focus: true
594@@ -1241,6 +1287,29 @@
595 Component.onDestruction: bottomEdgeHint.forceShow = false
596 }
597 }
598+
599+ onShowDownloadDialog: {
600+ if (downloadDialogLoader.status === Loader.Ready) {
601+ var downloadDialog = PopupUtils.open(downloadDialogLoader.item, browser, {"contentType" : contentType,
602+ "downloadId" : downloadId,
603+ "singleDownload" : downloader,
604+ "filename" : filename,
605+ "mimeType" : mimeType})
606+ downloadDialog.startDownload.connect(startDownload)
607+ }
608+ }
609+
610+ function showDownloadsPage() {
611+ downloadsContainer.focus = true
612+ return downloadsComponent.createObject(downloadsContainer)
613+ }
614+
615+ function startDownload(downloadId, download, mimeType) {
616+ downloadsModel.add(downloadId, download.url, mimeType)
617+ download.start()
618+ showDownloadsPage()
619+ }
620+
621 }
622 }
623 }
624@@ -1743,13 +1812,13 @@
625 KeyboardShortcut {
626 modifiers: Qt.ControlModifier
627 key: Qt.Key_Tab
628- enabled: chrome.visible || recentView.visible
629+ enabled: (chrome.visible || recentView.visible) && !downloadsContainer.visible
630 onTriggered: internal.switchToNextTab()
631 }
632 KeyboardShortcut {
633 modifiers: Qt.ControlModifier
634 key: Qt.Key_PageDown
635- enabled: chrome.visible || recentView.visible
636+ enabled: (chrome.visible || recentView.visible) && !downloadsContainer.visible
637 onTriggered: internal.switchToNextTab()
638 }
639
640@@ -1757,13 +1826,13 @@
641 KeyboardShortcut {
642 modifiers: Qt.ControlModifier
643 key: Qt.Key_Backtab
644- enabled: chrome.visible || recentView.visible
645+ enabled: (chrome.visible || recentView.visible) && !downloadsContainer.visible
646 onTriggered: internal.switchToPreviousTab()
647 }
648 KeyboardShortcut {
649 modifiers: Qt.ControlModifier
650 key: Qt.Key_PageUp
651- enabled: chrome.visible || recentView.visible
652+ enabled: (chrome.visible || recentView.visible) && !downloadsContainer.visible
653 onTriggered: internal.switchToPreviousTab()
654 }
655
656@@ -1771,14 +1840,14 @@
657 KeyboardShortcut {
658 modifiers: Qt.ControlModifier | Qt.ShiftModifier
659 key: Qt.Key_W
660- enabled: chrome.visible || recentView.visible
661+ enabled: (chrome.visible || recentView.visible) && !downloadsContainer.visible
662 onTriggered: internal.undoCloseTab()
663 }
664
665 KeyboardShortcut {
666 modifiers: Qt.ControlModifier | Qt.ShiftModifier
667 key: Qt.Key_T
668- enabled: chrome.visible || recentView.visible
669+ enabled: (chrome.visible || recentView.visible) && !downloadsContainer.visible
670 onTriggered: internal.undoCloseTab()
671 }
672
673@@ -1786,13 +1855,13 @@
674 KeyboardShortcut {
675 modifiers: Qt.ControlModifier
676 key: Qt.Key_W
677- enabled: chrome.visible || recentView.visible
678+ enabled: (chrome.visible || recentView.visible) && !downloadsContainer.visible
679 onTriggered: internal.closeCurrentTab()
680 }
681 KeyboardShortcut {
682 modifiers: Qt.ControlModifier
683 key: Qt.Key_F4
684- enabled: chrome.visible || recentView.visible
685+ enabled: (chrome.visible || recentView.visible) && !downloadsContainer.visible
686 onTriggered: internal.closeCurrentTab()
687 }
688
689@@ -1800,7 +1869,7 @@
690 KeyboardShortcut {
691 modifiers: Qt.ControlModifier
692 key: Qt.Key_T
693- enabled: chrome.visible || recentView.visible || bookmarksViewLoader.active || historyViewLoader.active
694+ enabled: (chrome.visible || recentView.visible || bookmarksViewLoader.active || historyViewLoader.active) && !downloadsContainer.visible
695 onTriggered: {
696 openUrlInNewTab("", true)
697 if (recentView.visible) recentView.reset()
698@@ -1814,18 +1883,18 @@
699 KeyboardShortcut {
700 modifiers: Qt.ControlModifier
701 key: Qt.Key_L
702- enabled: chrome.visible
703+ enabled: chrome.visible && !downloadsContainer.visible
704 onTriggered: internal.focusAddressBar(true)
705 }
706 KeyboardShortcut {
707 modifiers: Qt.AltModifier
708 key: Qt.Key_D
709- enabled: chrome.visible
710+ enabled: chrome.visible && !downloadsContainer.visible
711 onTriggered: internal.focusAddressBar(true)
712 }
713 KeyboardShortcut {
714 key: Qt.Key_F6
715- enabled: chrome.visible
716+ enabled: chrome.visible && !downloadsContainer.visible
717 onTriggered: internal.focusAddressBar(true)
718 }
719
720@@ -1833,7 +1902,7 @@
721 KeyboardShortcut {
722 modifiers: Qt.ControlModifier
723 key: Qt.Key_D
724- enabled: chrome.visible
725+ enabled: chrome.visible && !downloadsContainer.visible
726 onTriggered: {
727 if (currentWebview) {
728 if (BookmarksModel.contains(currentWebview.url)) {
729@@ -1849,7 +1918,7 @@
730 KeyboardShortcut {
731 modifiers: Qt.ControlModifier
732 key: Qt.Key_H
733- enabled: chrome.visible
734+ enabled: chrome.visible && !downloadsContainer.visible
735 onTriggered: historyViewLoader.active = true
736 }
737
738@@ -1857,7 +1926,7 @@
739 KeyboardShortcut {
740 modifiers: Qt.ControlModifier | Qt.ShiftModifier
741 key: Qt.Key_O
742- enabled: chrome.visible
743+ enabled: chrome.visible && !downloadsContainer.visible
744 onTriggered: bookmarksViewLoader.active = true
745 }
746
747@@ -1865,12 +1934,12 @@
748 KeyboardShortcut {
749 modifiers: Qt.AltModifier
750 key: Qt.Key_Left
751- enabled: chrome.visible
752+ enabled: chrome.visible && !downloadsContainer.visible
753 onTriggered: internal.historyGoBack()
754 }
755 KeyboardShortcut {
756 key: Qt.Key_Backspace
757- enabled: chrome.visible
758+ enabled: chrome.visible && !downloadsContainer.visible
759 onTriggered: internal.historyGoBack()
760 }
761
762@@ -1878,26 +1947,26 @@
763 KeyboardShortcut {
764 modifiers: Qt.AltModifier
765 key: Qt.Key_Right
766- enabled: chrome.visible
767+ enabled: chrome.visible && !downloadsContainer.visible
768 onTriggered: internal.historyGoForward()
769 }
770 KeyboardShortcut {
771 modifiers: Qt.ShiftModifier
772 key: Qt.Key_Backspace
773- enabled: chrome.visible
774+ enabled: chrome.visible && !downloadsContainer.visible
775 onTriggered: internal.historyGoForward()
776 }
777
778 // F5 or Ctrl+R: Reload current Tab
779 KeyboardShortcut {
780 key: Qt.Key_F5
781- enabled: chrome.visible
782+ enabled: chrome.visible && !downloadsContainer.visible
783 onTriggered: if (currentWebview) currentWebview.reload()
784 }
785 KeyboardShortcut {
786 modifiers: Qt.ControlModifier
787 key: Qt.Key_R
788- enabled: chrome.visible
789+ enabled: chrome.visible && !downloadsContainer.visible
790 onTriggered: if (currentWebview) currentWebview.reload()
791 }
792
793@@ -1905,8 +1974,48 @@
794 KeyboardShortcut {
795 modifiers: Qt.ControlModifier
796 key: Qt.Key_F
797- enabled: !newTabViewLoader.active && !bookmarksViewLoader.active
798+ enabled: !newTabViewLoader.active && !bookmarksViewLoader.active && !downloadsContainer.visible
799 onTriggered: chrome.findInPageMode = true
800 }
801- }
802+
803+ // Ctrl + J: Show downloads page
804+ KeyboardShortcut {
805+ modifiers: Qt.ControlModifier
806+ key: Qt.Key_J
807+ enabled: chrome.visible && !downloadsContainer.visible
808+ onTriggered: currentWebview.showDownloadsPage()
809+ }
810+ }
811+
812+ Loader {
813+ id: contentHandlerLoader
814+ source: "ContentHandler.qml"
815+ }
816+
817+ Connections {
818+ target: contentHandlerLoader.item
819+ onExportFromDownloads: {
820+ if (downloadHandlerLoader.status == Loader.Ready) {
821+ downloadsContainer.focus = true
822+ var downloadPage = downloadsComponent.createObject(downloadsContainer)
823+ downloadPage.mimetypeFilter = mimetypeFilter
824+ downloadPage.activeTransfer = transfer
825+ downloadPage.multiSelect = multiSelect
826+ downloadPage.pickingMode = true
827+ }
828+ }
829+ }
830+
831+ Loader {
832+ id: downloadDialogLoader
833+ source: "ContentDownloadDialog.qml"
834+ asynchronous: true
835+ }
836+
837+ Loader {
838+ id: filePickerLoader
839+ source: "ContentPickerDialog.qml"
840+ asynchronous: true
841+ }
842+
843 }
844
845=== renamed file 'src/app/webbrowser/SettingsPageHeader.qml' => 'src/app/webbrowser/BrowserPageHeader.qml'
846--- src/app/webbrowser/SettingsPageHeader.qml 2015-08-10 15:22:00 +0000
847+++ src/app/webbrowser/BrowserPageHeader.qml 2015-12-16 16:25:40 +0000
848@@ -21,18 +21,22 @@
849 import Ubuntu.Components.ListItems 1.3 as ListItem
850
851 /*
852- * Component to use as page header in settings page and subpages
853+ * Component to use as page header in settings page, download page and
854+ * subpages
855 *
856- * It has a back() signal fired when back button is pressed and a text
857- * property to set the page title
858+ * It has a back() signal fired when back button is pressed, a text
859+ * property to set the page title and an actions property which
860+ * displays action icons on the right of header.
861 */
862
863-Column {
864+Item {
865 id: root
866 signal back()
867 property string text
868+ property alias actions: actionBar.actions
869+ property alias color: title.color
870
871- height: childrenRect.height
872+ height: title.height + divider.height
873
874 anchors {
875 left: parent.left
876@@ -42,13 +46,9 @@
877 Rectangle {
878 id: title
879
880- height: units.gu(7) - divider.height
881+ height: units.gu(6) - divider.height
882 anchors { left: parent.left; right: parent.right }
883-
884- Rectangle {
885- anchors.fill: parent
886- color: "#f6f6f6"
887- }
888+ color: "#f6f6f6"
889
890 AbstractButton {
891 id: backButton
892@@ -89,9 +89,24 @@
893 text: root.text
894 fontSize: 'x-large'
895 }
896+
897+ ActionBar {
898+ id: actionBar
899+ anchors.right: parent.right
900+ anchors.verticalCenter: parent.verticalCenter
901+ }
902+
903+
904 }
905
906- ListItem.Divider {
907+ Rectangle {
908 id: divider
909+ anchors {
910+ left: parent.left
911+ right: parent.right
912+ bottom: parent.bottom
913+ }
914+ height: units.dp(1)
915+ color: Qt.darker(title.color, 1.1)
916 }
917 }
918
919=== modified file 'src/app/webbrowser/CMakeLists.txt'
920--- src/app/webbrowser/CMakeLists.txt 2015-11-23 09:41:48 +0000
921+++ src/app/webbrowser/CMakeLists.txt 2015-12-16 16:25:40 +0000
922@@ -13,6 +13,7 @@
923 bookmarks-model.cpp
924 bookmarks-folder-model.cpp
925 bookmarks-folderlist-model.cpp
926+ downloads-model.cpp
927 history-domain-model.cpp
928 history-domainlist-model.cpp
929 history-lastvisitdatelist-model.cpp
930@@ -74,3 +75,8 @@
931
932 install(FILES "webbrowser-app.url-dispatcher"
933 DESTINATION ${CMAKE_INSTALL_DATADIR}/url-dispatcher/urls)
934+
935+install(FILES "webbrowser-app-content-hub.json"
936+ DESTINATION ${CMAKE_INSTALL_DATADIR}/content-hub/peers
937+ RENAME webbrowser-app
938+ )
939
940=== added file 'src/app/webbrowser/ContentDownloadDialog.qml'
941--- src/app/webbrowser/ContentDownloadDialog.qml 1970-01-01 00:00:00 +0000
942+++ src/app/webbrowser/ContentDownloadDialog.qml 2015-12-16 16:25:40 +0000
943@@ -0,0 +1,159 @@
944+/*
945+ * Copyright 2014-2015 Canonical Ltd.
946+ *
947+ * This file is part of webbrowser-app.
948+ *
949+ * webbrowser-app is free software; you can redistribute it and/or modify
950+ * it under the terms of the GNU General Public License as published by
951+ * the Free Software Foundation; version 3.
952+ *
953+ * webbrowser-app is distributed in the hope that it will be useful,
954+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
955+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
956+ * GNU General Public License for more details.
957+ *
958+ * You should have received a copy of the GNU General Public License
959+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
960+ */
961+
962+import QtQuick 2.4
963+import Ubuntu.Components 1.3
964+import Ubuntu.Components.Popups 1.3
965+import Ubuntu.Content 1.3
966+import webbrowsercommon.private 0.1
967+
968+Component {
969+ PopupBase {
970+ id: downloadDialog
971+ objectName: "downloadDialog"
972+ anchors.fill: parent
973+ property var activeTransfer
974+ property string downloadId
975+ property var singleDownload
976+ property string mimeType
977+ property string filename
978+ property string icon: MimeDatabase.iconForMimetype(mimeType)
979+ property alias contentType: peerPicker.contentType
980+
981+ signal startDownload(string downloadId, var download, string mimeType)
982+
983+ Component {
984+ id: downloadOptionsComponent
985+ Dialog {
986+ id: downloadOptionsDialog
987+ objectName: "downloadOptionsDialog"
988+ Column {
989+ spacing: units.gu(2)
990+
991+ Item {
992+ width: parent.width
993+ height: mimetypeIcon.height
994+
995+ Icon {
996+ id: mimetypeIcon
997+ name: icon != "" ? icon : "save"
998+ height: units.gu(4.5)
999+ width: height
1000+ }
1001+
1002+ Label {
1003+ id: filenameLabel
1004+ anchors.top: mimetypeIcon.top
1005+ anchors.left: mimetypeIcon.right
1006+ anchors.leftMargin: units.gu(2)
1007+ anchors.right: parent.right
1008+ anchors.rightMargin: units.gu(2)
1009+ elide: Text.ElideMiddle
1010+ text: downloadDialog.filename
1011+ }
1012+
1013+ Label {
1014+ anchors.top: filenameLabel.bottom
1015+ anchors.left: filenameLabel.left
1016+ anchors.right: filenameLabel.right
1017+ elide: Text.ElideRight
1018+ font.capitalization: Font.Capitalize
1019+ text: MimeDatabase.nameForMimetype(downloadDialog.mimeType)
1020+ }
1021+ }
1022+
1023+ Label {
1024+ width: parent.width
1025+ text: i18n.tr("Choose an application to open this file or add it to the downloads folder.")
1026+ wrapMode: Text.Wrap
1027+ visible: peerModel.peers.length > 0
1028+ }
1029+
1030+ Button {
1031+ text: i18n.tr("Choose an application")
1032+ objectName: "chooseAppButton"
1033+ anchors.horizontalCenter: parent.horizontalCenter
1034+ width: units.gu(22)
1035+ height: units.gu(4)
1036+ visible: peerModel.peers.length > 0
1037+ onClicked: {
1038+ PopupUtils.close(downloadOptionsDialog)
1039+ pickerRect.visible = true
1040+ }
1041+ }
1042+
1043+ Button {
1044+ text: i18n.tr("Download")
1045+ objectName: "downloadFileButton"
1046+ anchors.horizontalCenter: parent.horizontalCenter
1047+ width: units.gu(22)
1048+ height: units.gu(4)
1049+ onClicked: {
1050+ startDownload(downloadId, singleDownload, mimeType)
1051+ PopupUtils.close(downloadDialog)
1052+ }
1053+ }
1054+
1055+ Button {
1056+ text: i18n.tr("Cancel")
1057+ objectName: "cancelDownloadButton"
1058+ anchors.horizontalCenter: parent.horizontalCenter
1059+ width: units.gu(22)
1060+ height: units.gu(4)
1061+ onClicked: PopupUtils.close(downloadDialog)
1062+ }
1063+
1064+ }
1065+ }
1066+
1067+ }
1068+
1069+ ContentPeerModel {
1070+ id: peerModel
1071+ handler: ContentHandler.Destination
1072+ contentType: downloadDialog.contentType
1073+ }
1074+
1075+ Rectangle {
1076+ id: pickerRect
1077+ anchors.fill: parent
1078+ visible: false
1079+ ContentPeerPicker {
1080+ id: peerPicker
1081+ handler: ContentHandler.Destination
1082+ objectName: "contentPeerPicker"
1083+ visible: parent.visible
1084+
1085+ onPeerSelected: {
1086+ activeTransfer = peer.request()
1087+ activeTransfer.downloadId = downloadDialog.downloadId
1088+ activeTransfer.state = ContentTransfer.Downloading
1089+ PopupUtils.close(downloadDialog)
1090+ }
1091+
1092+ onCancelPressed: {
1093+ PopupUtils.close(downloadDialog)
1094+ }
1095+ }
1096+ }
1097+
1098+ Component.onCompleted: {
1099+ PopupUtils.open(downloadOptionsComponent, downloadDialog)
1100+ }
1101+ }
1102+}
1103
1104=== added file 'src/app/webbrowser/ContentHandler.qml'
1105--- src/app/webbrowser/ContentHandler.qml 1970-01-01 00:00:00 +0000
1106+++ src/app/webbrowser/ContentHandler.qml 2015-12-16 16:25:40 +0000
1107@@ -0,0 +1,35 @@
1108+/*
1109+ * Copyright 2015 Canonical Ltd.
1110+ *
1111+ * This file is part of webbrowser-app.
1112+ *
1113+ * webbrowser-app is free software; you can redistribute it and/or modify
1114+ * it under the terms of the GNU General Public License as published by
1115+ * the Free Software Foundation; version 3.
1116+ *
1117+ * webbrowser-app is distributed in the hope that it will be useful,
1118+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1119+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1120+ * GNU General Public License for more details.
1121+ *
1122+ * You should have received a copy of the GNU General Public License
1123+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1124+ */
1125+
1126+import QtQuick 2.4
1127+import Ubuntu.Content 1.3
1128+import "../MimeTypeMapper.js" as MimeTypeMapper
1129+
1130+Item {
1131+ signal exportFromDownloads(var transfer, var mimetypeFilter, bool multiSelect)
1132+
1133+ Connections {
1134+ target: ContentHub
1135+ onExportRequested: {
1136+ exportFromDownloads(transfer,
1137+ MimeTypeMapper.mimeTypeRegexForContentType(transfer.contentType),
1138+ transfer.selectionType === ContentTransfer.Multiple)
1139+
1140+ }
1141+ }
1142+}
1143
1144=== added file 'src/app/webbrowser/ContentPickerDialog.qml'
1145--- src/app/webbrowser/ContentPickerDialog.qml 1970-01-01 00:00:00 +0000
1146+++ src/app/webbrowser/ContentPickerDialog.qml 2015-12-16 16:25:40 +0000
1147@@ -0,0 +1,110 @@
1148+/*
1149+ * Copyright 2014-2015 Canonical Ltd.
1150+ *
1151+ * This file is part of webbrowser-app.
1152+ *
1153+ * webbrowser-app is free software; you can redistribute it and/or modify
1154+ * it under the terms of the GNU General Public License as published by
1155+ * the Free Software Foundation; version 3.
1156+ *
1157+ * webbrowser-app is distributed in the hope that it will be useful,
1158+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1159+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1160+ * GNU General Public License for more details.
1161+ *
1162+ * You should have received a copy of the GNU General Public License
1163+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1164+ */
1165+
1166+import QtQuick 2.4
1167+import Ubuntu.Components 1.3
1168+import Ubuntu.Components.Popups 1.3 as Popups
1169+import Ubuntu.Content 1.3
1170+import com.canonical.Oxide 1.8
1171+import "../MimeTypeMapper.js" as MimeTypeMapper
1172+
1173+Component {
1174+ Popups.PopupBase {
1175+ id: picker
1176+ objectName: "contentPickerDialog"
1177+
1178+ // Set the parent at construction time, instead of letting show()
1179+ // set it later on, which for some reason results in the size of
1180+ // the dialog not being updated.
1181+ parent: QuickUtils.rootItem(this)
1182+
1183+ property var activeTransfer
1184+
1185+ Rectangle {
1186+ anchors.fill: parent
1187+
1188+ ContentTransferHint {
1189+ anchors.fill: parent
1190+ activeTransfer: picker.activeTransfer
1191+ }
1192+
1193+ ContentPeerPicker {
1194+ id: peerPicker
1195+ anchors.fill: parent
1196+ visible: true
1197+ contentType: ContentType.All
1198+ handler: ContentHandler.Source
1199+
1200+ onPeerSelected: {
1201+ if (peer.appId == "webbrowser-app") {
1202+ // If we're inside the browser and the user has
1203+ // requested content from the browser then we
1204+ // need to handle the transfer internally
1205+ var downloadPage = picker.WebView.view.showDownloadsPage()
1206+ downloadPage.mimetypeFilter = MimeTypeMapper.mimeTypeRegexForContentType(contentType)
1207+ downloadPage.multiSelect = model.allowMultipleFiles
1208+ downloadPage.selectMode = false
1209+ downloadPage.pickingMode = true
1210+ downloadPage.internalFilePicker = model
1211+ Popups.PopupUtils.close(picker)
1212+ } else {
1213+ if (model.allowMultipleFiles) {
1214+ peer.selectionType = ContentTransfer.Multiple
1215+ } else {
1216+ peer.selectionType = ContentTransfer.Single
1217+ }
1218+ picker.activeTransfer = peer.request()
1219+ stateChangeConnection.target = picker.activeTransfer
1220+ }
1221+ }
1222+
1223+ onCancelPressed: {
1224+ model.reject()
1225+ }
1226+ }
1227+ }
1228+
1229+ Connections {
1230+ id: stateChangeConnection
1231+ target: null
1232+ onStateChanged: {
1233+ if (picker.activeTransfer.state === ContentTransfer.Charged) {
1234+ var selectedItems = []
1235+ for(var i in picker.activeTransfer.items) {
1236+ selectedItems.push(String(picker.activeTransfer.items[i].url).replace("file://", ""))
1237+ }
1238+ model.accept(selectedItems)
1239+ }
1240+ }
1241+ }
1242+
1243+ Component.onCompleted: {
1244+ if(acceptTypes.length === 1) {
1245+ var contentType = MimeTypeMapper.mimeTypeToContentType(acceptTypes[0])
1246+ if(contentType == ContentType.Unknown) {
1247+ // If we don't recognise the type, allow uploads from any app
1248+ contentType = ContentType.All
1249+ }
1250+ peerPicker.contentType = contentType
1251+ } else {
1252+ peerPicker.contentType = ContentType.All
1253+ }
1254+ show()
1255+ }
1256+ }
1257+}
1258
1259=== added file 'src/app/webbrowser/DownloadDelegate.qml'
1260--- src/app/webbrowser/DownloadDelegate.qml 1970-01-01 00:00:00 +0000
1261+++ src/app/webbrowser/DownloadDelegate.qml 2015-12-16 16:25:40 +0000
1262@@ -0,0 +1,226 @@
1263+/*
1264+ * Copyright 2014-2015 Canonical Ltd.
1265+ *
1266+ * This file is part of webbrowser-app.
1267+ *
1268+ * webbrowser-app is free software; you can redistribute it and/or modify
1269+ * it under the terms of the GNU General Public License as published by
1270+ * the Free Software Foundation; version 3.
1271+ *
1272+ * webbrowser-app is distributed in the hope that it will be useful,
1273+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1274+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1275+ * GNU General Public License for more details.
1276+ *
1277+ * You should have received a copy of the GNU General Public License
1278+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1279+ */
1280+
1281+import QtQuick 2.0
1282+import Ubuntu.Components 1.3
1283+import ".."
1284+
1285+ListItem {
1286+ id: downloadDelegate
1287+
1288+ property alias icon: mimeicon.name
1289+ property alias image: thumbimage.source
1290+ property alias title: title.text
1291+ property alias url: url.text
1292+ property string errorMessage
1293+ property bool incomplete: false
1294+ property string downloadId
1295+ property var download
1296+ property int progress: download ? download.progress : 0
1297+ property bool paused
1298+
1299+ divider.visible: false
1300+
1301+ signal removed()
1302+ signal cancelled()
1303+
1304+ height: visible ? (incomplete ? (paused ? units.gu(13) : units.gu(10)) : units.gu(7)) : 0
1305+
1306+ Component.onCompleted: {
1307+ if (incomplete) {
1308+ // Connect to download object
1309+ for(var i = 0; i < downloadManager.downloads.length; i++) {
1310+ if (downloadManager.downloads[i].downloadId == downloadId) {
1311+ download = downloadManager.downloads[i]
1312+ }
1313+ }
1314+ }
1315+ }
1316+
1317+ Item {
1318+
1319+ anchors {
1320+ verticalCenter: parent.verticalCenter
1321+ left: parent.left
1322+ leftMargin: units.gu(2)
1323+ right: parent.right
1324+ }
1325+
1326+ Item {
1327+ id: iconContainer
1328+ width: units.gu(3)
1329+ height: width
1330+ anchors.verticalCenter: parent.verticalCenter
1331+ anchors.verticalCenterOffset: downloadDelegate.incomplete ? -units.gu(1) : 0
1332+
1333+ Image {
1334+ id: thumbimage
1335+ asynchronous: true
1336+ width: parent.width
1337+ height: parent.height
1338+ fillMode: Image.PreserveAspectFit
1339+ sourceSize.width: parent.width
1340+ sourceSize.height: parent.height
1341+ anchors.verticalCenter: parent.verticalCenter
1342+ }
1343+
1344+ Image {
1345+ id: mimeicon
1346+ asynchronous: true
1347+ anchors.fill: parent
1348+ anchors.margins: units.gu(0.2)
1349+ source: "image://theme/%1".arg(name != "" ? name : "save")
1350+ visible: thumbimage.status !== Image.Ready
1351+ cache: true
1352+ property string name
1353+ }
1354+ }
1355+
1356+ Item {
1357+ anchors.top: iconContainer.top
1358+ anchors.left: iconContainer.right
1359+ anchors.leftMargin: units.gu(2)
1360+ anchors.right: parent.right
1361+
1362+ Column {
1363+ id: detailsColumn
1364+ width: parent.width - cancelColumn.width
1365+ height: parent.height
1366+
1367+ Label {
1368+ id: title
1369+ fontSize: "x-small"
1370+ color: "#5d5d5d"
1371+ elide: Text.ElideRight
1372+ width: parent.width
1373+ }
1374+
1375+ Label {
1376+ id: url
1377+ fontSize: "x-small"
1378+ color: "#5d5d5d"
1379+ elide: Text.ElideRight
1380+ width: parent.width
1381+ }
1382+
1383+ Item {
1384+ height: error.visible ? units.gu(1) : units.gu(2)
1385+ width: parent.width
1386+ visible: downloadDelegate.incomplete
1387+ }
1388+
1389+ Item {
1390+ id: error
1391+ visible: incomplete && download === undefined || errorMessage !== ""
1392+ height: units.gu(3)
1393+ width: parent.width
1394+
1395+ Icon {
1396+ id: errorIcon
1397+ width: units.gu(2)
1398+ height: width
1399+ anchors.verticalCenter: parent.verticalCenter
1400+ name: "dialog-warning-symbolic"
1401+ color: UbuntuColors.red
1402+ }
1403+
1404+ Label {
1405+ width: parent.width - errorIcon.width
1406+ anchors.left: errorIcon.right
1407+ anchors.leftMargin: units.gu(1)
1408+ anchors.verticalCenter: errorIcon.verticalCenter
1409+ fontSize: "x-small"
1410+ color: UbuntuColors.red
1411+ text: errorMessage !== "" ? errorMessage
1412+ : (incomplete && download === undefined) ? i18n.tr("Download failed")
1413+ : ""
1414+ elide: Text.ElideRight
1415+ }
1416+ }
1417+
1418+ IndeterminateProgressBar {
1419+ id: progressBar
1420+ width: parent.width
1421+ height: units.gu(0.5)
1422+ visible: downloadDelegate.incomplete && !error.visible
1423+ progress: downloadDelegate.progress
1424+ // Work around UDM bug #1450144
1425+ indeterminateProgress: downloadDelegate.progress < 0 || downloadDelegate.progress > 100
1426+ }
1427+ }
1428+
1429+ Column {
1430+ id: cancelColumn
1431+ spacing: units.gu(1)
1432+ anchors.top: detailsColumn.top
1433+ anchors.left: detailsColumn.right
1434+ anchors.leftMargin: units.gu(2)
1435+ width: downloadDelegate.incomplete && !error.visible ? cancelButton.width + units.gu(2) : 0
1436+
1437+ Button {
1438+ visible: downloadDelegate.incomplete && !error.visible
1439+ id: cancelButton
1440+ text: i18n.tr("Cancel")
1441+ onClicked: {
1442+ if (download) {
1443+ download.cancel()
1444+ cancelled()
1445+ }
1446+ }
1447+ }
1448+
1449+ Label {
1450+ visible: !progressBar.indeterminateProgress && downloadDelegate.incomplete
1451+ && !error.visible
1452+ && !downloadDelegate.paused
1453+ width: cancelButton.width
1454+ horizontalAlignment: Text.AlignHCenter
1455+ fontSize: "x-small"
1456+ text: progressBar.progress + "%"
1457+ }
1458+
1459+ Button {
1460+ visible: downloadDelegate.paused
1461+ text: i18n.tr("Resume")
1462+ width: cancelButton.width
1463+ onClicked: {
1464+ if (download) {
1465+ download.resume()
1466+ }
1467+ }
1468+ }
1469+ }
1470+
1471+ }
1472+ }
1473+
1474+ leadingActions: error.visible || !downloadDelegate.incomplete ? deleteActionList : null
1475+
1476+ ListItemActions {
1477+ id: deleteActionList
1478+ actions: [
1479+ Action {
1480+ objectName: "leadingAction.delete"
1481+ iconName: "delete"
1482+ enabled: error.visible || !downloadDelegate.incomplete
1483+ onTriggered: error.visible ? downloadDelegate.cancelled()
1484+ : downloadDelegate.removed()
1485+ }
1486+ ]
1487+ }
1488+}
1489
1490=== added file 'src/app/webbrowser/DownloadHandler.qml'
1491--- src/app/webbrowser/DownloadHandler.qml 1970-01-01 00:00:00 +0000
1492+++ src/app/webbrowser/DownloadHandler.qml 2015-12-16 16:25:40 +0000
1493@@ -0,0 +1,45 @@
1494+/*
1495+ * Copyright 2015 Canonical Ltd.
1496+ *
1497+ * This file is part of webbrowser-app.
1498+ *
1499+ * webbrowser-app is free software; you can redistribute it and/or modify
1500+ * it under the terms of the GNU General Public License as published by
1501+ * the Free Software Foundation; version 3.
1502+ *
1503+ * webbrowser-app is distributed in the hope that it will be useful,
1504+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1505+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1506+ * GNU General Public License for more details.
1507+ *
1508+ * You should have received a copy of the GNU General Public License
1509+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1510+ */
1511+
1512+import QtQuick 2.4
1513+import Ubuntu.DownloadManager 1.2
1514+
1515+DownloadManager {
1516+ id: downloadManager
1517+
1518+ onDownloadFinished: {
1519+ downloadsModel.moveToDownloads(download.downloadId, path)
1520+ downloadsModel.setComplete(download.downloadId, true)
1521+ }
1522+
1523+ onDownloadPaused: {
1524+ downloadsModel.pauseDownload(download.downloadId)
1525+ }
1526+
1527+ onDownloadResumed: {
1528+ downloadsModel.resumeDownload(download.downloadId)
1529+ }
1530+
1531+ onDownloadCanceled: {
1532+ downloadsModel.cancelDownload(download.downloadId)
1533+ }
1534+
1535+ onErrorFound: {
1536+ downloadsModel.setError(download.downloadId, download.errorMessage)
1537+ }
1538+}
1539
1540=== added file 'src/app/webbrowser/DownloadsModel.qml'
1541--- src/app/webbrowser/DownloadsModel.qml 1970-01-01 00:00:00 +0000
1542+++ src/app/webbrowser/DownloadsModel.qml 2015-12-16 16:25:40 +0000
1543@@ -0,0 +1,24 @@
1544+/*
1545+ * Copyright 2015 Canonical Ltd.
1546+ *
1547+ * This file is part of webbrowser-app.
1548+ *
1549+ * webbrowser-app is free software; you can redistribute it and/or modify
1550+ * it under the terms of the GNU General Public License as published by
1551+ * the Free Software Foundation; version 3.
1552+ *
1553+ * webbrowser-app is distributed in the hope that it will be useful,
1554+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1555+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1556+ * GNU General Public License for more details.
1557+ *
1558+ * You should have received a copy of the GNU General Public License
1559+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1560+ */
1561+
1562+import QtQuick 2.0
1563+import webbrowserapp.private 0.1
1564+
1565+DownloadsModel {
1566+ databasePath: dataLocation + "/downloads.sqlite"
1567+}
1568
1569=== added file 'src/app/webbrowser/DownloadsPage.qml'
1570--- src/app/webbrowser/DownloadsPage.qml 1970-01-01 00:00:00 +0000
1571+++ src/app/webbrowser/DownloadsPage.qml 2015-12-16 16:25:40 +0000
1572@@ -0,0 +1,253 @@
1573+/*
1574+ * Copyright 2015 Canonical Ltd.
1575+ *
1576+ * This file is part of webbrowser-app.
1577+ *
1578+ * webbrowser-app is free software; you can redistribute it and/or modify
1579+ * it under the terms of the GNU General Public License as published by
1580+ * the Free Software Foundation; version 3.
1581+ *
1582+ * webbrowser-app is distributed in the hope that it will be useful,
1583+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1584+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1585+ * GNU General Public License for more details.
1586+ *
1587+ * You should have received a copy of the GNU General Public License
1588+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1589+ */
1590+
1591+import QtQuick 2.0
1592+import Qt.labs.settings 1.0
1593+import Ubuntu.Components 1.3
1594+import Ubuntu.Components.Popups 1.0
1595+import Ubuntu.Thumbnailer 0.1
1596+import Ubuntu.Content 1.3
1597+import Ubuntu.Web 0.2
1598+import webbrowserapp.private 0.1
1599+import webbrowsercommon.private 0.1
1600+
1601+import "../MimeTypeMapper.js" as MimeTypeMapper
1602+
1603+Item {
1604+ id: downloadsItem
1605+
1606+ property QtObject downloadsModel
1607+
1608+ // We can get file picking requests either via content-hub (activeTransfer)
1609+ // Or via the internal oxide file picker (internalFilePicker) in the case
1610+ // where the user wishes to upload a file from their previous downloads.
1611+ property var activeTransfer
1612+ property var internalFilePicker
1613+
1614+ property bool selectMode
1615+ property bool pickingMode
1616+ property bool multiSelect
1617+ property alias mimetypeFilter: downloadModelFilter.pattern
1618+
1619+ signal done()
1620+
1621+ Rectangle {
1622+ anchors.fill: parent
1623+ color: "#fbfbfb"
1624+ }
1625+
1626+ BrowserPageHeader {
1627+ id: title
1628+ text: i18n.tr("Downloads")
1629+ color: "#f7f7f7"
1630+ actions: [
1631+ Action {
1632+ text: i18n.tr("Confirm selection")
1633+ iconName: "tick"
1634+ visible: pickingMode
1635+ enabled: downloadsListView.ViewItems.selectedIndices.length > 0
1636+ onTriggered: {
1637+ var results = []
1638+ if (internalFilePicker) {
1639+ for (var i = 0; i < downloadsListView.ViewItems.selectedIndices.length; i++) {
1640+ var selectedDownload = downloadsListView.model.get(downloadsListView.ViewItems.selectedIndices[i])
1641+ results.push(selectedDownload.path)
1642+ }
1643+ internalFilePicker.accept(results)
1644+ } else {
1645+ for (var i = 0; i < downloadsListView.ViewItems.selectedIndices.length; i++) {
1646+ var selectedDownload = downloadsListView.model.get(downloadsListView.ViewItems.selectedIndices[i])
1647+ results.push(resultComponent.createObject(downloadsItem, {"url": "file://" + selectedDownload.path}))
1648+ }
1649+ activeTransfer.items = results
1650+ activeTransfer.state = ContentTransfer.Charged
1651+ }
1652+ downloadsItem.done()
1653+ }
1654+ },
1655+ Action {
1656+ text: i18n.tr("Select all")
1657+ iconName: "select"
1658+ visible: selectMode
1659+ onTriggered: {
1660+ if (downloadsListView.ViewItems.selectedIndices.length === downloadsListView.count) {
1661+ downloadsListView.ViewItems.selectedIndices = []
1662+ } else {
1663+ var indices = []
1664+ for (var i = 0; i < downloadsListView.count; ++i) {
1665+ indices.push(i)
1666+ }
1667+ downloadsListView.ViewItems.selectedIndices = indices
1668+ }
1669+ }
1670+ },
1671+ Action {
1672+ text: i18n.tr("Delete")
1673+ iconName: "delete"
1674+ visible: selectMode
1675+ onTriggered: {
1676+ var toDelete = []
1677+ for (var i = 0; i < downloadsListView.ViewItems.selectedIndices.length; i++) {
1678+ var selectedDownload = downloadsListView.model.get(downloadsListView.ViewItems.selectedIndices[i])
1679+ toDelete.push(selectedDownload.path)
1680+ }
1681+ for (var i = 0; i < toDelete.length; i++) {
1682+ downloadsModel.deleteDownload(toDelete[i])
1683+ }
1684+ downloadsListView.ViewItems.selectedIndices = []
1685+ downloadsItem.selectMode = false
1686+ }
1687+ },
1688+ Action {
1689+ iconName: "edit"
1690+ visible: !selectMode && !pickingMode
1691+ onTriggered: {
1692+ selectMode = true
1693+ multiSelect = true
1694+ }
1695+ }
1696+ ]
1697+ onBack: {
1698+ if (selectMode) {
1699+ selectMode = false
1700+ } else {
1701+ if (activeTransfer) {
1702+ activeTransfer.state = ContentTransfer.Aborted
1703+ }
1704+ if (internalFilePicker) {
1705+ internalFilePicker.reject()
1706+ }
1707+ downloadsItem.done()
1708+ }
1709+ }
1710+ }
1711+
1712+ Component {
1713+ id: resultComponent
1714+ ContentItem { }
1715+ }
1716+
1717+ ListView {
1718+ id: downloadsListView
1719+ clip: true
1720+
1721+ anchors {
1722+ top: title.bottom
1723+ left: parent.left
1724+ right: parent.right
1725+ bottom: parent.bottom
1726+ rightMargin: units.gu(2)
1727+ }
1728+
1729+ model: SortFilterModel {
1730+ model: downloadsModel
1731+ filter {
1732+ id: downloadModelFilter
1733+ property: "mimetype"
1734+ }
1735+ }
1736+
1737+ delegate: DownloadDelegate {
1738+ downloadId: model.downloadId
1739+ title: model.filename ? model.filename : model.url.toString().split('/').pop().split('?').shift()
1740+ url: model.url
1741+ image: model.complete && (model.mimetype.indexOf("image") === 0 || model.mimetype.indexOf("video") === 0) ? "image://thumbnailer/file://" + model.path : ""
1742+ icon: MimeDatabase.iconForMimetype(model.mimetype)
1743+ incomplete: !model.complete
1744+ selectMode: downloadsItem.selectMode || downloadsItem.pickingMode
1745+ visible: !(selectMode && incomplete)
1746+ errorMessage: model.error
1747+ paused: model.paused
1748+ // Work around bug #1493880
1749+ property bool lastSelected
1750+
1751+ onSelectedChanged: {
1752+ if (!multiSelect && selected && lastSelected != selected) {
1753+ downloadsListView.ViewItems.selectedIndices = [index]
1754+ }
1755+ lastSelected = selected
1756+ }
1757+
1758+ onClicked: {
1759+ if (model.complete) {
1760+ if (selectMode) {
1761+ selected = !selected
1762+ } else {
1763+ exportPeerPicker.contentType = MimeTypeMapper.mimeTypeToContentType(model.mimetype)
1764+ exportPeerPicker.visible = true
1765+ exportPeerPicker.path = model.path
1766+ }
1767+ }
1768+ }
1769+
1770+ onPressAndHold: {
1771+ downloadsItem.selectMode = true
1772+ downloadsItem.multiSelect = true
1773+ if (downloadsItem.selectMode) {
1774+ downloadsListView.ViewItems.selectedIndices = [index]
1775+ }
1776+ }
1777+
1778+ onRemoved: {
1779+ if (model.complete) {
1780+ downloadsModel.deleteDownload(model.path)
1781+ }
1782+ }
1783+
1784+ onCancelled: {
1785+ downloadsModel.cancelDownload(model.downloadId)
1786+ }
1787+ }
1788+
1789+ }
1790+
1791+ Label {
1792+ id: emptyLabel
1793+ anchors.centerIn: parent
1794+ visible: downloadsListView.count == 0
1795+ wrapMode: Text.Wrap
1796+ width: parent.width
1797+ horizontalAlignment: Text.AlignHCenter
1798+ text: i18n.tr("No downloads available")
1799+ }
1800+
1801+ Component {
1802+ id: contentItemComponent
1803+ ContentItem {}
1804+ }
1805+
1806+ ContentPeerPicker {
1807+ id: exportPeerPicker
1808+ visible: false
1809+ anchors.fill: parent
1810+ handler: ContentHandler.Destination
1811+ property string path
1812+ onPeerSelected: {
1813+ var transfer = peer.request()
1814+ if (transfer.state === ContentTransfer.InProgress) {
1815+ transfer.items = [contentItemComponent.createObject(downloadsItem, {"url": path})]
1816+ transfer.state = ContentTransfer.Charged
1817+ }
1818+ visible = false
1819+ }
1820+ onCancelPressed: {
1821+ visible = false
1822+ }
1823+ }
1824+
1825+}
1826
1827=== added file 'src/app/webbrowser/IndeterminateProgressBar.qml'
1828--- src/app/webbrowser/IndeterminateProgressBar.qml 1970-01-01 00:00:00 +0000
1829+++ src/app/webbrowser/IndeterminateProgressBar.qml 2015-12-16 16:25:40 +0000
1830@@ -0,0 +1,51 @@
1831+/*
1832+ * Copyright 2015 Canonical Ltd.
1833+ *
1834+ * This file is part of webbrowser-app.
1835+ *
1836+ * webbrowser-app is free software; you can redistribute it and/or modify
1837+ * it under the terms of the GNU General Public License as published by
1838+ * the Free Software Foundation; version 3.
1839+ *
1840+ * webbrowser-app is distributed in the hope that it will be useful,
1841+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1842+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1843+ * GNU General Public License for more details.
1844+ *
1845+ * You should have received a copy of the GNU General Public License
1846+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1847+ */
1848+
1849+import QtQuick 2.4
1850+import Ubuntu.Components 1.3
1851+
1852+Rectangle {
1853+ id: progressBar
1854+
1855+ property real progress
1856+ property bool indeterminateProgress: false
1857+
1858+ radius: width/3
1859+ color: Theme.palette.normal.base
1860+
1861+ Rectangle {
1862+ id: currentProgress
1863+ height: parent.height
1864+ radius: parent.radius
1865+ anchors.left: parent.left
1866+ anchors.leftMargin: 0
1867+ anchors.top: parent.top
1868+ color: UbuntuColors.orange
1869+ width: indeterminateProgress ? parent.width / 6 : (progress / 100) * parent.width
1870+
1871+ SequentialAnimation {
1872+ running: indeterminateProgress
1873+ onRunningChanged: {
1874+ currentProgress.anchors.leftMargin = 0;
1875+ }
1876+ loops: Animation.Infinite
1877+ PropertyAnimation { target: currentProgress.anchors; property: "leftMargin"; from: 0.0; to: parent.width - parent.width / 6; duration: UbuntuAnimation.SleepyDuration; easing.type: Easing.InOutQuad; }
1878+ PropertyAnimation { target: currentProgress.anchors; property: "leftMargin"; from: parent.width - parent.width / 6; to: 0; duration: UbuntuAnimation.SleepyDuration; easing.type: Easing.InOutQuad; }
1879+ }
1880+ }
1881+}
1882
1883=== modified file 'src/app/webbrowser/SettingsPage.qml'
1884--- src/app/webbrowser/SettingsPage.qml 2015-11-17 16:25:30 +0000
1885+++ src/app/webbrowser/SettingsPage.qml 2015-12-16 16:25:40 +0000
1886@@ -43,7 +43,7 @@
1887 searchPaths: searchEnginesSearchPaths
1888 }
1889
1890- SettingsPageHeader {
1891+ BrowserPageHeader {
1892 id: title
1893
1894 onBack: settingsItem.done()
1895@@ -168,7 +168,7 @@
1896 color: "#f6f6f6"
1897 }
1898
1899- SettingsPageHeader {
1900+ BrowserPageHeader {
1901 id: searchEngineTitle
1902
1903 onBack: searchEngineItem.destroy()
1904@@ -221,7 +221,7 @@
1905 color: "#f6f6f6"
1906 }
1907
1908- SettingsPageHeader {
1909+ BrowserPageHeader {
1910 id: privacyTitle
1911 onBack: privacyItem.destroy()
1912 text: i18n.tr("Privacy & permissions")
1913@@ -379,7 +379,7 @@
1914 color: "#f6f6f6"
1915 }
1916
1917- SettingsPageHeader {
1918+ BrowserPageHeader {
1919 id: mediaAccessTitle
1920
1921 onBack: mediaAccessItem.destroy()
1922
1923=== added file 'src/app/webbrowser/downloads-model.cpp'
1924--- src/app/webbrowser/downloads-model.cpp 1970-01-01 00:00:00 +0000
1925+++ src/app/webbrowser/downloads-model.cpp 2015-12-16 16:25:40 +0000
1926@@ -0,0 +1,412 @@
1927+/*
1928+ * Copyright 2015 Canonical Ltd.
1929+ *
1930+ * This file is part of webbrowser-app.
1931+ *
1932+ * webbrowser-app is free software; you can redistribute it and/or modify
1933+ * it under the terms of the GNU General Public License as published by
1934+ * the Free Software Foundation; version 3.
1935+ *
1936+ * webbrowser-app is distributed in the hope that it will be useful,
1937+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1938+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1939+ * GNU General Public License for more details.
1940+ *
1941+ * You should have received a copy of the GNU General Public License
1942+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1943+ */
1944+
1945+#include "downloads-model.h"
1946+
1947+#include <QtCore/QDebug>
1948+#include <QtCore/QDir>
1949+#include <QtSql/QSqlQuery>
1950+#include <QtCore/QFile>
1951+#include <QtCore/QFileInfo>
1952+#include <QtCore/QStandardPaths>
1953+#include <QtCore/QMimeDatabase>
1954+#include <QtCore/QMimeType>
1955+
1956+#define CONNECTION_NAME "webbrowser-app-downloads"
1957+
1958+/*!
1959+ \class DownloadsModel
1960+ \brief List model that stores information about downloaded files.
1961+
1962+ DownloadsModel is a list model that stores information about files that
1963+ have been downloaded by the browser and stored permanently
1964+ (e.g. in ~/Downloads), as opposed to those that were sent directly to
1965+ another application after download. For each download the original URL, the
1966+ path to the downloaded file, the file mimetype and the download time are
1967+ stored. The model is sorted chronologically to display the most recent
1968+ download first.
1969+
1970+ The information is persistently stored on disk in a SQLite database.
1971+ The database is read at startup to populate the model, and whenever a new
1972+ entry is added to the model or an entry is removed from the model
1973+ the database is updated. Removing a download from the model also results
1974+ in it being deleted from the disk.
1975+ The model doesn’t monitor the database for external changes, but does check
1976+ that downloaded files still exist when first populating.
1977+*/
1978+DownloadsModel::DownloadsModel(QObject* parent)
1979+ : QAbstractListModel(parent)
1980+ , m_numRows(0)
1981+ , m_fetchedCount(0)
1982+ , m_canFetchMore(true)
1983+{
1984+ m_database = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), CONNECTION_NAME);
1985+}
1986+
1987+DownloadsModel::~DownloadsModel()
1988+{
1989+ m_database.close();
1990+ m_database = QSqlDatabase();
1991+ QSqlDatabase::removeDatabase(CONNECTION_NAME);
1992+}
1993+
1994+void DownloadsModel::resetDatabase(const QString& databaseName)
1995+{
1996+ beginResetModel();
1997+ m_orderedEntries.clear();
1998+ m_database.close();
1999+ m_database.setDatabaseName(databaseName);
2000+ m_database.open();
2001+ m_numRows = 0;
2002+ m_fetchedCount = 0;
2003+ m_canFetchMore = true;
2004+ createOrAlterDatabaseSchema();
2005+ endResetModel();
2006+ Q_EMIT rowCountChanged();
2007+}
2008+
2009+void DownloadsModel::createOrAlterDatabaseSchema()
2010+{
2011+ QSqlQuery createQuery(m_database);
2012+ QString query = QLatin1String("CREATE TABLE IF NOT EXISTS downloads "
2013+ "(downloadId VARCHAR, url VARCHAR, path VARCHAR, "
2014+ "mimetype VARCHAR, complete BOOL, paused BOOL, "
2015+ "error VARCHAR, created DATETIME DEFAULT "
2016+ "CURRENT_TIMESTAMP);");
2017+ createQuery.prepare(query);
2018+ createQuery.exec();
2019+}
2020+
2021+void DownloadsModel::fetchMore(const QModelIndex &parent)
2022+{
2023+ QSqlQuery populateQuery(m_database);
2024+ QString query = QLatin1String("SELECT downloadId, url, path, mimetype, "
2025+ "complete, error, created, paused "
2026+ "FROM downloads ORDER BY created DESC LIMIT 100 OFFSET ?;");
2027+ populateQuery.prepare(query);
2028+ populateQuery.addBindValue(m_fetchedCount);
2029+ populateQuery.exec();
2030+ int count = 0; // size() isn't supported on the sqlite backend
2031+ while (populateQuery.next()) {
2032+ DownloadEntry entry;
2033+ entry.downloadId = populateQuery.value(0).toString();
2034+ entry.url = populateQuery.value(1).toUrl();
2035+ entry.path = populateQuery.value(2).toString();
2036+ entry.mimetype = populateQuery.value(3).toString();
2037+ entry.complete = populateQuery.value(4).toBool();
2038+ entry.error = populateQuery.value(5).toString();
2039+ entry.created = QDateTime::fromTime_t(populateQuery.value(6).toInt());
2040+ entry.paused = populateQuery.value(7).toBool();
2041+ QFileInfo fileInfo(entry.path);
2042+ if (fileInfo.exists()) {
2043+ entry.filename = fileInfo.fileName();
2044+ }
2045+
2046+ // Only list a completed entry if its file exists, however we don't
2047+ // remove the entry if the file is missing as it may be stored on a
2048+ // removable medium like an SD card in the future, so could reappear.
2049+ if (!entry.complete || fileInfo.exists()) {
2050+ beginInsertRows(QModelIndex(), m_numRows, m_numRows);
2051+ m_orderedEntries.append(entry);
2052+ endInsertRows();
2053+ m_numRows++;
2054+ }
2055+ count++;
2056+ }
2057+ m_fetchedCount += count;
2058+ if (count == 0) {
2059+ m_canFetchMore = false;
2060+ }
2061+}
2062+
2063+QHash<int, QByteArray> DownloadsModel::roleNames() const
2064+{
2065+ static QHash<int, QByteArray> roles;
2066+ if (roles.isEmpty()) {
2067+ roles[DownloadId] = "downloadId";
2068+ roles[Url] = "url";
2069+ roles[Path] = "path";
2070+ roles[Filename] = "filename";
2071+ roles[Mimetype] = "mimetype";
2072+ roles[Complete] = "complete";
2073+ roles[Paused] = "paused";
2074+ roles[Error] = "error";
2075+ roles[Created] = "created";
2076+ }
2077+ return roles;
2078+}
2079+
2080+int DownloadsModel::rowCount(const QModelIndex& parent) const
2081+{
2082+ Q_UNUSED(parent);
2083+ return m_orderedEntries.count();
2084+}
2085+
2086+QVariant DownloadsModel::data(const QModelIndex& index, int role) const
2087+{
2088+ if (!index.isValid()) {
2089+ return QVariant();
2090+ }
2091+ const DownloadEntry& entry = m_orderedEntries.at(index.row());
2092+ switch (role) {
2093+ case DownloadId:
2094+ return entry.downloadId;
2095+ case Url:
2096+ return entry.url;
2097+ case Path:
2098+ return entry.path;
2099+ case Filename:
2100+ return entry.filename;
2101+ case Mimetype:
2102+ return entry.mimetype;
2103+ case Complete:
2104+ return entry.complete;
2105+ case Paused:
2106+ return entry.paused;
2107+ case Error:
2108+ return entry.error;
2109+ case Created:
2110+ return entry.created;
2111+ default:
2112+ return QVariant();
2113+ }
2114+}
2115+
2116+const QString DownloadsModel::databasePath() const
2117+{
2118+ return m_database.databaseName();
2119+}
2120+
2121+void DownloadsModel::setDatabasePath(const QString& path)
2122+{
2123+ if (path != databasePath()) {
2124+ if (path.isEmpty()) {
2125+ resetDatabase(":memory:");
2126+ } else {
2127+ resetDatabase(path);
2128+ }
2129+ Q_EMIT databasePathChanged();
2130+ }
2131+}
2132+
2133+/*!
2134+ Add a download to the database. This should happen as soon as the download
2135+ is started.
2136+*/
2137+void DownloadsModel::add(const QString& downloadId, const QUrl& url, const QString& mimetype)
2138+{
2139+ beginInsertRows(QModelIndex(), 0, 0);
2140+ DownloadEntry entry;
2141+ entry.downloadId = downloadId;
2142+ entry.complete = false;
2143+ entry.paused = false;
2144+ entry.url = url;
2145+ entry.mimetype = mimetype;
2146+ m_orderedEntries.prepend(entry);
2147+ m_numRows++;
2148+ m_fetchedCount++;
2149+ endInsertRows();
2150+ Q_EMIT added(downloadId, url, mimetype);
2151+ insertNewEntryInDatabase(entry);
2152+ Q_EMIT rowCountChanged();
2153+}
2154+
2155+void DownloadsModel::setPath(const QString& downloadId, const QString& path)
2156+{
2157+ QSqlQuery query(m_database);
2158+
2159+ // Override reported mimetype from server with detected mimetype from file once downloaded
2160+ QMimeDatabase mimeDatabase;
2161+ QString mimetype = mimeDatabase.mimeTypeForFile(path).name();
2162+
2163+ static QString updateStatement = QLatin1String("UPDATE downloads SET mimetype = ?, "
2164+ "path = ? WHERE downloadId = ?");
2165+ query.prepare(updateStatement);
2166+ query.addBindValue(mimetype);
2167+ query.addBindValue(path);
2168+ query.addBindValue(downloadId);
2169+ query.exec();
2170+ Q_EMIT pathChanged(downloadId, path);
2171+}
2172+
2173+void DownloadsModel::setComplete(const QString& downloadId, const bool complete)
2174+{
2175+ QSqlQuery query(m_database);
2176+ static QString updateStatement = QLatin1String("UPDATE downloads SET complete = ? "
2177+ "WHERE downloadId = ?");
2178+ query.prepare(updateStatement);
2179+ query.addBindValue(complete);
2180+ query.addBindValue(downloadId);
2181+ query.exec();
2182+ Q_EMIT completeChanged(downloadId, complete);
2183+ reload();
2184+}
2185+
2186+void DownloadsModel::setError(const QString& downloadId, const QString& error)
2187+{
2188+ QSqlQuery query(m_database);
2189+ static QString updateStatement = QLatin1String("UPDATE downloads SET error = ? "
2190+ "WHERE downloadId = ?");
2191+ query.prepare(updateStatement);
2192+ query.addBindValue(error);
2193+ query.addBindValue(downloadId);
2194+ query.exec();
2195+ Q_EMIT errorChanged(downloadId, error);
2196+ reload();
2197+}
2198+
2199+void DownloadsModel::moveToDownloads(const QString& downloadId, const QString& path)
2200+{
2201+ QFile file(path);
2202+ if (file.exists()) {
2203+ QFileInfo fi(path);
2204+ QString suffix = fi.completeSuffix();
2205+ QString filename = fi.fileName();
2206+ QString filenameWithoutSuffix = filename.left(filename.size() - suffix.size());
2207+ QString dir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
2208+ QString destination = dir + QDir::separator() + filenameWithoutSuffix + suffix;
2209+ // Avoid filename collision by automatically inserting an incremented
2210+ // number into the filename if the original name already exists.
2211+ if (QFile::exists(destination)) {
2212+ int append = 1;
2213+ do {
2214+ destination = QString("%1%2.%3").arg(dir + QDir::separator() + filenameWithoutSuffix, QString::number(append), suffix);
2215+ append++;
2216+ } while (QFile::exists(destination));
2217+ }
2218+ if (file.rename(destination)) {
2219+ setPath(downloadId, destination);
2220+ } else {
2221+ qWarning() << "Failed moving file from " << path << " to " << destination;
2222+ }
2223+ } else {
2224+ qWarning() << "Download not found: " << path;
2225+ }
2226+}
2227+
2228+void DownloadsModel::insertNewEntryInDatabase(const DownloadEntry& entry)
2229+{
2230+ QSqlQuery query(m_database);
2231+ static QString insertStatement = QLatin1String("INSERT INTO downloads (downloadId, url, "
2232+ "mimetype) "
2233+ "VALUES (?, ?, ?);");
2234+ query.prepare(insertStatement);
2235+ query.addBindValue(entry.downloadId);
2236+ query.addBindValue(entry.url);
2237+ query.addBindValue(entry.mimetype);
2238+ query.exec();
2239+}
2240+
2241+/*!
2242+ Remove a downloaded file from the list of downloads and
2243+ delete the file.
2244+*/
2245+void DownloadsModel::deleteDownload(const QString& path)
2246+{
2247+ int index = 0;
2248+ Q_FOREACH(DownloadEntry entry, m_orderedEntries) {
2249+ if (entry.path == path) {
2250+ beginRemoveRows(QModelIndex(), index, index);
2251+ m_orderedEntries.removeAt(index);
2252+ endRemoveRows();
2253+ Q_EMIT deleted(path);
2254+ removeExistingEntryFromDatabase(path);
2255+ m_fetchedCount--;
2256+ m_numRows--;
2257+ Q_EMIT rowCountChanged();
2258+ QFile::remove(path);
2259+ return;
2260+ } else {
2261+ index++;
2262+ }
2263+ }
2264+}
2265+
2266+/*!
2267+ Remove a cancelled download from the model and the database.
2268+*/
2269+void DownloadsModel::cancelDownload(const QString& downloadId)
2270+{
2271+ int index=0;
2272+ Q_FOREACH(DownloadEntry entry, m_orderedEntries) {
2273+ if (entry.downloadId == downloadId) {
2274+ beginRemoveRows(QModelIndex(), index, index);
2275+ m_orderedEntries.removeAt(index);
2276+ QSqlQuery query(m_database);
2277+ static QString deleteStatement = QLatin1String("DELETE FROM downloads WHERE downloadId=?;");
2278+ query.prepare(deleteStatement);
2279+ query.addBindValue(downloadId);
2280+ query.exec();
2281+ endRemoveRows();
2282+ m_fetchedCount--;
2283+ m_numRows--;
2284+ Q_EMIT rowCountChanged();
2285+ return;
2286+ } else {
2287+ index++;
2288+ }
2289+ }
2290+}
2291+
2292+void DownloadsModel::pauseDownload(const QString& downloadId)
2293+{
2294+ QSqlQuery query(m_database);
2295+ static QString pauseStatement = QLatin1String("UPDATE downloads SET paused=1 WHERE downloadId=?;");
2296+ query.prepare(pauseStatement);
2297+ query.addBindValue(downloadId);
2298+ query.exec();
2299+ reload();
2300+}
2301+
2302+void DownloadsModel::resumeDownload(const QString& downloadId)
2303+{
2304+ QSqlQuery query(m_database);
2305+ static QString resumeStatement = QLatin1String("UPDATE downloads SET paused=0 WHERE downloadId=?;");
2306+ query.prepare(resumeStatement);
2307+ query.addBindValue(downloadId);
2308+ query.exec();
2309+ reload();
2310+}
2311+
2312+void DownloadsModel::removeExistingEntryFromDatabase(const QString& path)
2313+{
2314+ QSqlQuery query(m_database);
2315+ static QString deleteStatement = QLatin1String("DELETE FROM downloads WHERE path=?;");
2316+ query.prepare(deleteStatement);
2317+ query.addBindValue(path);
2318+ query.exec();
2319+}
2320+
2321+bool DownloadsModel::canFetchMore(const QModelIndex &parent) const
2322+{
2323+ Q_UNUSED(parent)
2324+
2325+ return m_canFetchMore;
2326+}
2327+
2328+void DownloadsModel::reload()
2329+{
2330+ beginResetModel();
2331+ m_orderedEntries.clear();
2332+ m_canFetchMore = true;
2333+ m_fetchedCount = 0;
2334+ m_numRows = 0;
2335+ endResetModel();
2336+ fetchMore();
2337+ Q_EMIT rowCountChanged();
2338+}
2339
2340=== added file 'src/app/webbrowser/downloads-model.h'
2341--- src/app/webbrowser/downloads-model.h 1970-01-01 00:00:00 +0000
2342+++ src/app/webbrowser/downloads-model.h 2015-12-16 16:25:40 +0000
2343@@ -0,0 +1,110 @@
2344+/*
2345+ * Copyright 2015 Canonical Ltd.
2346+ *
2347+ * This file is part of webbrowser-app.
2348+ *
2349+ * webbrowser-app is free software; you can redistribute it and/or modify
2350+ * it under the terms of the GNU General Public License as published by
2351+ * the Free Software Foundation; version 3.
2352+ *
2353+ * webbrowser-app is distributed in the hope that it will be useful,
2354+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2355+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2356+ * GNU General Public License for more details.
2357+ *
2358+ * You should have received a copy of the GNU General Public License
2359+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2360+ */
2361+
2362+#ifndef __DOWNLOADS_MODEL_H__
2363+#define __DOWNLOADS_MODEL_H__
2364+
2365+#include <QtCore/QAbstractListModel>
2366+#include <QtCore/QDateTime>
2367+#include <QtCore/QList>
2368+#include <QtCore/QSet>
2369+#include <QtCore/QString>
2370+#include <QtCore/QUrl>
2371+#include <QtSql/QSqlDatabase>
2372+
2373+class DownloadsModel : public QAbstractListModel
2374+{
2375+ Q_OBJECT
2376+
2377+ Q_PROPERTY(QString databasePath READ databasePath WRITE setDatabasePath NOTIFY databasePathChanged)
2378+ Q_PROPERTY(int count READ rowCount NOTIFY rowCountChanged)
2379+
2380+ Q_ENUMS(Roles)
2381+
2382+public:
2383+ DownloadsModel(QObject* parent=0);
2384+ ~DownloadsModel();
2385+
2386+ enum Roles {
2387+ DownloadId = Qt::UserRole + 1,
2388+ Url,
2389+ Path,
2390+ Filename,
2391+ Mimetype,
2392+ Complete,
2393+ Paused,
2394+ Error,
2395+ Created
2396+ };
2397+
2398+ // reimplemented from QAbstractListModel
2399+ QHash<int, QByteArray> roleNames() const;
2400+ int rowCount(const QModelIndex& parent=QModelIndex()) const;
2401+ QVariant data(const QModelIndex& index, int role) const;
2402+ bool canFetchMore(const QModelIndex &parent = QModelIndex()) const;
2403+ void fetchMore(const QModelIndex &parent = QModelIndex());
2404+
2405+ const QString databasePath() const;
2406+ void setDatabasePath(const QString& path);
2407+
2408+ Q_INVOKABLE void add(const QString &downloadId, const QUrl& url, const QString& mimetype);
2409+ Q_INVOKABLE void moveToDownloads(const QString& downloadId, const QString& path);
2410+ Q_INVOKABLE void setPath(const QString& downloadId, const QString& path);
2411+ Q_INVOKABLE void setComplete(const QString& downloadId, const bool complete);
2412+ Q_INVOKABLE void setError(const QString& downloadId, const QString& error);
2413+ Q_INVOKABLE void deleteDownload(const QString& path);
2414+ Q_INVOKABLE void cancelDownload(const QString& downloadId);
2415+ Q_INVOKABLE void pauseDownload(const QString& downloadId);
2416+ Q_INVOKABLE void resumeDownload(const QString& downloadId);
2417+
2418+Q_SIGNALS:
2419+ void databasePathChanged() const;
2420+ void added(const QString& downloadId, const QUrl& url, const QString& mimetype) const;
2421+ void pathChanged(const QString& downloadId, const QString& path) const;
2422+ void completeChanged(const QString& downloadId, const bool complete) const;
2423+ void errorChanged(const QString& downloadId, const QString& error) const;
2424+ void deleted(const QString& path) const;
2425+ void rowCountChanged();
2426+
2427+private:
2428+ QSqlDatabase m_database;
2429+ int m_numRows;
2430+ int m_fetchedCount;
2431+ bool m_canFetchMore;
2432+
2433+ struct DownloadEntry {
2434+ QString downloadId;
2435+ QUrl url;
2436+ QString path;
2437+ QString filename;
2438+ QString mimetype;
2439+ bool complete;
2440+ bool paused;
2441+ QString error;
2442+ QDateTime created;
2443+ };
2444+ QList<DownloadEntry> m_orderedEntries;
2445+
2446+ void resetDatabase(const QString& databaseName);
2447+ void createOrAlterDatabaseSchema();
2448+ void insertNewEntryInDatabase(const DownloadEntry& entry);
2449+ void removeExistingEntryFromDatabase(const QString& path);
2450+ void reload();
2451+};
2452+
2453+#endif // __DOWNLOADS_MODEL_H__
2454
2455=== added file 'src/app/webbrowser/webbrowser-app-content-hub.json'
2456--- src/app/webbrowser/webbrowser-app-content-hub.json 1970-01-01 00:00:00 +0000
2457+++ src/app/webbrowser/webbrowser-app-content-hub.json 2015-12-16 16:25:40 +0000
2458@@ -0,0 +1,5 @@
2459+{
2460+ "source": [
2461+ "all"
2462+ ]
2463+}
2464
2465=== modified file 'src/app/webbrowser/webbrowser-app.cpp'
2466--- src/app/webbrowser/webbrowser-app.cpp 2015-11-23 09:41:48 +0000
2467+++ src/app/webbrowser/webbrowser-app.cpp 2015-12-16 16:25:40 +0000
2468@@ -20,6 +20,7 @@
2469 #include "bookmarks-folderlist-model.h"
2470 #include "cache-deleter.h"
2471 #include "config.h"
2472+#include "downloads-model.h"
2473 #include "file-operations.h"
2474 #include "history-domainlist-model.h"
2475 #include "history-lastvisitdatelist-model.h"
2476@@ -87,6 +88,7 @@
2477 qmlRegisterSingletonType<FileOperations>(uri, 0, 1, "FileOperations", FileOperations_singleton_factory);
2478 qmlRegisterType<SearchEngine>(uri, 0, 1, "SearchEngine");
2479 qmlRegisterSingletonType<CacheDeleter>(uri, 0, 1, "CacheDeleter", CacheDeleter_singleton_factory);
2480+ qmlRegisterType<DownloadsModel>(uri, 0, 1, "DownloadsModel");
2481 qmlRegisterType<TextSearchFilterModel>(uri, 0, 1, "TextSearchFilterModel");
2482
2483 if (BrowserApplication::initialize("webbrowser/webbrowser-app.qml")) {
2484
2485=== added file 'src/app/webcontainer/ContentDownloadDialog.qml'
2486--- src/app/webcontainer/ContentDownloadDialog.qml 1970-01-01 00:00:00 +0000
2487+++ src/app/webcontainer/ContentDownloadDialog.qml 2015-12-16 16:25:40 +0000
2488@@ -0,0 +1,67 @@
2489+/*
2490+ * Copyright 2014-2015 Canonical Ltd.
2491+ *
2492+ * This file is part of webbrowser-app.
2493+ *
2494+ * webbrowser-app is free software; you can redistribute it and/or modify
2495+ * it under the terms of the GNU General Public License as published by
2496+ * the Free Software Foundation; version 3.
2497+ *
2498+ * webbrowser-app is distributed in the hope that it will be useful,
2499+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2500+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2501+ * GNU General Public License for more details.
2502+ *
2503+ * You should have received a copy of the GNU General Public License
2504+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2505+ */
2506+
2507+import QtQuick 2.4
2508+import Ubuntu.Components 1.3
2509+import Ubuntu.Components.Popups 1.3
2510+import Ubuntu.Content 1.3
2511+import webbrowsercommon.private 0.1
2512+
2513+Component {
2514+ PopupBase {
2515+ id: downloadDialog
2516+ objectName: "downloadDialog"
2517+ anchors.fill: parent
2518+ property var activeTransfer
2519+ property var downloadId
2520+ property var singleDownload
2521+ property var mimeType
2522+ property var filename
2523+ property var icon: MimeDatabase.iconForMimetype(mimeType)
2524+ property alias contentType: peerPicker.contentType
2525+
2526+ ContentPeerModel {
2527+ id: peerModel
2528+ handler: ContentHandler.Destination
2529+ contentType: downloadDialog.contentType
2530+ }
2531+
2532+ Rectangle {
2533+ id: pickerRect
2534+ anchors.fill: parent
2535+ visible: true
2536+ ContentPeerPicker {
2537+ id: peerPicker
2538+ handler: ContentHandler.Destination
2539+ objectName: "contentPeerPicker"
2540+ visible: parent.visible
2541+
2542+ onPeerSelected: {
2543+ activeTransfer = peer.request()
2544+ activeTransfer.downloadId = downloadDialog.downloadId
2545+ activeTransfer.state = ContentTransfer.Downloading
2546+ PopupUtils.close(downloadDialog)
2547+ }
2548+
2549+ onCancelPressed: {
2550+ PopupUtils.close(downloadDialog)
2551+ }
2552+ }
2553+ }
2554+ }
2555+}
2556
2557=== added file 'src/app/webcontainer/ContentPickerDialog.qml'
2558--- src/app/webcontainer/ContentPickerDialog.qml 1970-01-01 00:00:00 +0000
2559+++ src/app/webcontainer/ContentPickerDialog.qml 2015-12-16 16:25:40 +0000
2560@@ -0,0 +1,96 @@
2561+/*
2562+ * Copyright 2014-2015 Canonical Ltd.
2563+ *
2564+ * This file is part of webbrowser-app.
2565+ *
2566+ * webbrowser-app is free software; you can redistribute it and/or modify
2567+ * it under the terms of the GNU General Public License as published by
2568+ * the Free Software Foundation; version 3.
2569+ *
2570+ * webbrowser-app is distributed in the hope that it will be useful,
2571+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2572+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2573+ * GNU General Public License for more details.
2574+ *
2575+ * You should have received a copy of the GNU General Public License
2576+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2577+ */
2578+
2579+import QtQuick 2.4
2580+import Ubuntu.Components 1.3
2581+import Ubuntu.Components.Popups 1.3 as Popups
2582+import Ubuntu.Content 1.3
2583+import "../MimeTypeMapper.js" as MimeTypeMapper
2584+
2585+Component {
2586+ Popups.PopupBase {
2587+ id: picker
2588+ objectName: "contentPickerDialog"
2589+
2590+ // Set the parent at construction time, instead of letting show()
2591+ // set it later on, which for some reason results in the size of
2592+ // the dialog not being updated.
2593+ parent: QuickUtils.rootItem(this)
2594+
2595+ property var activeTransfer
2596+
2597+ Rectangle {
2598+ anchors.fill: parent
2599+
2600+ ContentTransferHint {
2601+ anchors.fill: parent
2602+ activeTransfer: picker.activeTransfer
2603+ }
2604+
2605+ ContentPeerPicker {
2606+ id: peerPicker
2607+ anchors.fill: parent
2608+ visible: true
2609+ contentType: ContentType.All
2610+ handler: ContentHandler.Source
2611+
2612+ onPeerSelected: {
2613+ if (model.allowMultipleFiles) {
2614+ peer.selectionType = ContentTransfer.Multiple
2615+ } else {
2616+ peer.selectionType = ContentTransfer.Single
2617+ }
2618+ picker.activeTransfer = peer.request()
2619+ stateChangeConnection.target = picker.activeTransfer
2620+ }
2621+
2622+ onCancelPressed: {
2623+ model.reject()
2624+ }
2625+ }
2626+ }
2627+
2628+ Connections {
2629+ id: stateChangeConnection
2630+ target: null
2631+ onStateChanged: {
2632+ if (picker.activeTransfer.state === ContentTransfer.Charged) {
2633+ var selectedItems = []
2634+ for(var i in picker.activeTransfer.items) {
2635+ selectedItems.push(String(picker.activeTransfer.items[i].url).replace("file://", ""))
2636+ }
2637+ model.accept(selectedItems)
2638+ }
2639+ }
2640+ }
2641+
2642+ Component.onCompleted: {
2643+ if(acceptTypes.length === 1) {
2644+ var contentType = MimeTypeMapper.mimeTypeToContentType(acceptTypes[0])
2645+ if(contentType == ContentType.Unknown) {
2646+ // If we don't recognise the type, allow uploads from any app
2647+ contentType = ContentType.All
2648+ }
2649+ peerPicker.contentType = contentType
2650+ } else {
2651+ peerPicker.contentType = ContentType.All
2652+ }
2653+ show()
2654+ }
2655+ }
2656+}
2657
2658=== modified file 'src/app/webcontainer/WebViewImplOxide.qml'
2659--- src/app/webcontainer/WebViewImplOxide.qml 2015-08-20 10:42:09 +0000
2660+++ src/app/webcontainer/WebViewImplOxide.qml 2015-12-16 16:25:40 +0000
2661@@ -53,6 +53,7 @@
2662 property bool runningLocalApplication: false
2663
2664 currentWebview: webview
2665+ filePicker: filePickerLoader.item
2666
2667 context: WebContext {
2668 dataPath: webview.dataPath
2669@@ -253,4 +254,28 @@
2670 request.accept()
2671 }
2672 }
2673+
2674+ onShowDownloadDialog: {
2675+ if (downloadDialogLoader.status === Loader.Ready) {
2676+ var downloadDialog = PopupUtils.open(downloadDialogLoader.item, webview, {"contentType" : contentType,
2677+ "downloadId" : downloadId,
2678+ "singleDownload" : downloader,
2679+ "filename" : filename,
2680+ "mimeType" : mimeType})
2681+ downloadDialog.startDownload.connect(startDownload)
2682+ }
2683+ }
2684+
2685+ Loader {
2686+ id: downloadDialogLoader
2687+ source: "ContentDownloadDialog.qml"
2688+ asynchronous: true
2689+ }
2690+
2691+ Loader {
2692+ id: filePickerLoader
2693+ source: "ContentPickerDialog.qml"
2694+ asynchronous: true
2695+ }
2696+
2697 }
2698
2699=== modified file 'tests/autopilot/webbrowser_app/emulators/browser.py'
2700--- tests/autopilot/webbrowser_app/emulators/browser.py 2015-12-04 10:04:30 +0000
2701+++ tests/autopilot/webbrowser_app/emulators/browser.py 2015-12-16 16:25:40 +0000
2702@@ -149,11 +149,40 @@
2703 def get_settings_page(self):
2704 return self.wait_select_single(SettingsPage, visible=True)
2705
2706+ def get_downloads_page(self):
2707+ return self.wait_select_single(DownloadsPage, visible=True)
2708+
2709 def get_content_picker_dialog(self):
2710 # only on devices
2711 return self.wait_select_single("PopupBase",
2712 objectName="contentPickerDialog")
2713
2714+ def get_download_dialog(self):
2715+ return self.wait_select_single("PopupBase",
2716+ objectName="downloadDialog")
2717+
2718+ def get_peer_picker(self):
2719+ return self.wait_select_single(objectName="contentPeerPicker")
2720+
2721+ def get_download_options_dialog(self):
2722+ return self.wait_select_single("Dialog",
2723+ objectName="downloadOptionsDialog")
2724+
2725+ def click_cancel_download_button(self):
2726+ button = self.select_single("Button",
2727+ objectName="cancelDownloadButton")
2728+ self.pointing_device.click_object(button)
2729+
2730+ def click_choose_app_button(self):
2731+ button = self.select_single("Button",
2732+ objectName="chooseAppButton")
2733+ self.pointing_device.click_object(button)
2734+
2735+ def click_download_file_button(self):
2736+ button = self.select_single("Button",
2737+ objectName="downloadFileButton")
2738+ self.pointing_device.click_object(button)
2739+
2740 def get_bottom_edge_hint(self):
2741 return self.select_single("QQuickImage", objectName="bottomEdgeHint")
2742
2743@@ -473,7 +502,7 @@
2744 class SettingsPage(uitk.UbuntuUIToolkitCustomProxyObjectBase):
2745
2746 def get_header(self):
2747- return self.select_single(SettingsPageHeader)
2748+ return self.select_single(BrowserPageHeader)
2749
2750 def get_searchengine_entry(self):
2751 return self.select_single("Subtitled", objectName="searchengine")
2752@@ -502,7 +531,13 @@
2753 return self.select_single("Standard", objectName="reset")
2754
2755
2756-class SettingsPageHeader(uitk.UbuntuUIToolkitCustomProxyObjectBase):
2757+class DownloadsPage(uitk.UbuntuUIToolkitCustomProxyObjectBase):
2758+
2759+ def get_header(self):
2760+ return self.select_single(BrowserPageHeader)
2761+
2762+
2763+class BrowserPageHeader(uitk.UbuntuUIToolkitCustomProxyObjectBase):
2764
2765 @autopilot.logging.log_action(logger.info)
2766 def click_back_button(self):
2767
2768=== modified file 'tests/autopilot/webbrowser_app/tests/__init__.py'
2769--- tests/autopilot/webbrowser_app/tests/__init__.py 2015-10-15 19:09:59 +0000
2770+++ tests/autopilot/webbrowser_app/tests/__init__.py 2015-12-16 16:25:40 +0000
2771@@ -213,6 +213,15 @@
2772 self.pointing_device.click_object(history_action)
2773 return self.main_window.get_history_view()
2774
2775+ def open_downloads(self):
2776+ chrome = self.main_window.chrome
2777+ drawer_button = chrome.get_drawer_button()
2778+ self.pointing_device.click_object(drawer_button)
2779+ chrome.get_drawer()
2780+ downloads_action = chrome.get_drawer_action("downloads")
2781+ self.pointing_device.click_object(downloads_action)
2782+ return self.main_window.get_downloads_page()
2783+
2784 def assert_number_webviews_eventually(self, count):
2785 self.assertThat(lambda: len(self.main_window.get_webviews()),
2786 Eventually(Equals(count)))
2787
2788=== modified file 'tests/autopilot/webbrowser_app/tests/http_server.py'
2789--- tests/autopilot/webbrowser_app/tests/http_server.py 2015-09-29 20:58:36 +0000
2790+++ tests/autopilot/webbrowser_app/tests/http_server.py 2015-12-16 16:25:40 +0000
2791@@ -180,6 +180,18 @@
2792 self.send_response(200)
2793 name = self.path[len("/tab/"):]
2794 self.send_html('<html><body>' + name + '</body></html>')
2795+ elif self.path.startswith("/downloadpdfgenericmime"):
2796+ self.send_response(200)
2797+ self.send_header("Content-Type", "application/octet-stream")
2798+ self.send_header("Content-Disposition",
2799+ "attachment; filename='test.pdf'")
2800+ self.end_headers()
2801+ elif self.path.startswith("/downloadpdf"):
2802+ self.send_response(200)
2803+ self.send_header("Content-Type", "application/pdf")
2804+ self.send_header("Content-Disposition",
2805+ "attachment; filename='test.pdf'")
2806+ self.end_headers()
2807 elif self.path.startswith("/basicauth"):
2808 login = "user"
2809 password = "pass"
2810
2811=== added file 'tests/autopilot/webbrowser_app/tests/test_downloads.py'
2812--- tests/autopilot/webbrowser_app/tests/test_downloads.py 1970-01-01 00:00:00 +0000
2813+++ tests/autopilot/webbrowser_app/tests/test_downloads.py 2015-12-16 16:25:40 +0000
2814@@ -0,0 +1,77 @@
2815+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2816+#
2817+# Copyright 2015 Canonical
2818+#
2819+# This program is free software: you can redistribute it and/or modify it
2820+# under the terms of the GNU General Public License version 3, as published
2821+# by the Free Software Foundation.
2822+#
2823+# This program is distributed in the hope that it will be useful,
2824+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2825+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2826+# GNU General Public License for more details.
2827+#
2828+# You should have received a copy of the GNU General Public License
2829+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2830+
2831+from webbrowser_app.tests import StartOpenRemotePageTestCaseBase
2832+
2833+from autopilot.matchers import Eventually
2834+from autopilot.platform import model
2835+
2836+from testtools.matchers import Equals
2837+
2838+import testtools
2839+
2840+
2841+@testtools.skipIf(model() == "Desktop", "Don't run on desktop, as "
2842+ "dependencies aren't guaranteed")
2843+class TestDownloads(StartOpenRemotePageTestCaseBase):
2844+
2845+ def test_open_close_downloads_page(self):
2846+ downloads_page = self.open_downloads()
2847+ downloads_page.get_header().click_back_button()
2848+ downloads_page.wait_until_destroyed()
2849+
2850+ def test_mimetype_download(self):
2851+ self.main_window.go_to_url(self.base_url + "/downloadpdf")
2852+ dialog = self.main_window.get_download_dialog()
2853+ options_dialog = self.main_window.get_download_options_dialog()
2854+ self.assertThat(options_dialog.visible, Eventually(Equals(True)))
2855+ self.assertThat(dialog.mimeType, Eventually(Equals("application/pdf")))
2856+
2857+ def test_generic_mimetype_download(self):
2858+ self.main_window.go_to_url(self.base_url + "/downloadpdfgenericmime")
2859+ dialog = self.main_window.get_download_dialog()
2860+ options_dialog = self.main_window.get_download_options_dialog()
2861+ self.assertThat(options_dialog.visible, Eventually(Equals(True)))
2862+ self.assertThat(dialog.mimeType, Eventually(Equals("application/pdf")))
2863+
2864+ def test_filename(self):
2865+ self.main_window.go_to_url(self.base_url + "/downloadpdf")
2866+ dialog = self.main_window.get_download_dialog()
2867+ options_dialog = self.main_window.get_download_options_dialog()
2868+ self.assertThat(options_dialog.visible, Eventually(Equals(True)))
2869+ self.assertThat(dialog.filename, Eventually(Equals("test.pdf")))
2870+
2871+ def test_close_dialog(self):
2872+ self.main_window.go_to_url(self.base_url + "/downloadpdf")
2873+ options_dialog = self.main_window.get_download_options_dialog()
2874+ self.assertThat(options_dialog.visible, Eventually(Equals(True)))
2875+ self.main_window.click_cancel_download_button()
2876+
2877+ def test_picker(self):
2878+ self.main_window.go_to_url(self.base_url + "/downloadpdf")
2879+ options_dialog = self.main_window.get_download_options_dialog()
2880+ self.assertThat(options_dialog.visible, Eventually(Equals(True)))
2881+ self.main_window.click_choose_app_button()
2882+ picker = self.main_window.get_peer_picker()
2883+ self.assertThat(picker.visible, Eventually(Equals(True)))
2884+
2885+ def test_download(self):
2886+ self.main_window.go_to_url(self.base_url + "/downloadpdf")
2887+ options_dialog = self.main_window.get_download_options_dialog()
2888+ self.assertThat(options_dialog.visible, Eventually(Equals(True)))
2889+ self.main_window.click_download_file_button()
2890+ downloads_page = self.main_window.get_downloads_page()
2891+ self.assertThat(downloads_page.visible, Eventually(Equals(True)))
2892
2893=== modified file 'tests/autopilot/webbrowser_app/tests/test_settings.py'
2894--- tests/autopilot/webbrowser_app/tests/test_settings.py 2015-07-06 13:51:36 +0000
2895+++ tests/autopilot/webbrowser_app/tests/test_settings.py 2015-12-16 16:25:40 +0000
2896@@ -50,7 +50,7 @@
2897 self.pointing_device.click_object(searchengine)
2898 searchengine_page = settings.get_searchengine_page()
2899 searchengine_header = searchengine_page.select_single(
2900- browser.SettingsPageHeader)
2901+ browser.BrowserPageHeader)
2902 searchengine_header.click_back_button()
2903 searchengine_page.wait_until_destroyed()
2904 self.assertThat(searchengine.subText, Equals(old_engine))
2905@@ -120,7 +120,7 @@
2906 privacy = settings.get_privacy_entry()
2907 self.pointing_device.click_object(privacy)
2908 privacy_page = settings.get_privacy_page()
2909- privacy_header = privacy_page.select_single(browser.SettingsPageHeader)
2910+ privacy_header = privacy_page.select_single(browser.BrowserPageHeader)
2911 privacy_header.click_back_button()
2912 privacy_page.wait_until_destroyed()
2913
2914
2915=== modified file 'tests/unittests/CMakeLists.txt'
2916--- tests/unittests/CMakeLists.txt 2015-11-23 09:41:48 +0000
2917+++ tests/unittests/CMakeLists.txt 2015-12-16 16:25:40 +0000
2918@@ -20,3 +20,4 @@
2919 add_subdirectory(intent-filter)
2920 add_subdirectory(search-engine)
2921 add_subdirectory(text-search-filter-model)
2922+add_subdirectory(downloads-model)
2923
2924=== added directory 'tests/unittests/downloads-model'
2925=== added file 'tests/unittests/downloads-model/CMakeLists.txt'
2926--- tests/unittests/downloads-model/CMakeLists.txt 1970-01-01 00:00:00 +0000
2927+++ tests/unittests/downloads-model/CMakeLists.txt 2015-12-16 16:25:40 +0000
2928@@ -0,0 +1,13 @@
2929+find_package(Qt5Core REQUIRED)
2930+find_package(Qt5Sql REQUIRED)
2931+find_package(Qt5Test REQUIRED)
2932+set(TEST tst_DownloadsModelTests)
2933+add_executable(${TEST} tst_DownloadsModelTests.cpp)
2934+include_directories(${webbrowser-app_SOURCE_DIR})
2935+target_link_libraries(${TEST}
2936+ Qt5::Core
2937+ Qt5::Sql
2938+ Qt5::Test
2939+ webbrowser-app-models
2940+)
2941+add_test(${TEST} ${CMAKE_CURRENT_BINARY_DIR}/${TEST} -xunitxml -o ${TEST}.xml)
2942
2943=== added file 'tests/unittests/downloads-model/tst_DownloadsModelTests.cpp'
2944--- tests/unittests/downloads-model/tst_DownloadsModelTests.cpp 1970-01-01 00:00:00 +0000
2945+++ tests/unittests/downloads-model/tst_DownloadsModelTests.cpp 2015-12-16 16:25:40 +0000
2946@@ -0,0 +1,190 @@
2947+/*
2948+ * Copyright 2015 Canonical Ltd.
2949+ *
2950+ * This file is part of webbrowser-app.
2951+ *
2952+ * webbrowser-app is free software; you can redistribute it and/or modify
2953+ * it under the terms of the GNU General Public License as published by
2954+ * the Free Software Foundation; version 3.
2955+ *
2956+ * webbrowser-app is distributed in the hope that it will be useful,
2957+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2958+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2959+ * GNU General Public License for more details.
2960+ *
2961+ * You should have received a copy of the GNU General Public License
2962+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2963+ */
2964+
2965+#include <QtCore/QDir>
2966+#include <QtCore/QTemporaryFile>
2967+#include <QtTest/QSignalSpy>
2968+#include <QtTest/QtTest>
2969+#include "downloads-model.h"
2970+
2971+class DownloadsModelTests : public QObject
2972+{
2973+ Q_OBJECT
2974+
2975+private:
2976+ DownloadsModel* model;
2977+
2978+private Q_SLOTS:
2979+ void init()
2980+ {
2981+ model = new DownloadsModel;
2982+ model->setDatabasePath(":memory:");
2983+ }
2984+
2985+ void cleanup()
2986+ {
2987+ delete model;
2988+ }
2989+
2990+ void shouldBeInitiallyEmpty()
2991+ {
2992+ QCOMPARE(model->rowCount(), 0);
2993+ }
2994+
2995+ void shouldExposeRoleNames()
2996+ {
2997+ QList<QByteArray> roleNames = model->roleNames().values();
2998+ QVERIFY(roleNames.contains("downloadId"));
2999+ QVERIFY(roleNames.contains("url"));
3000+ QVERIFY(roleNames.contains("path"));
3001+ QVERIFY(roleNames.contains("filename"));
3002+ QVERIFY(roleNames.contains("mimetype"));
3003+ QVERIFY(roleNames.contains("complete"));
3004+ QVERIFY(roleNames.contains("paused"));
3005+ QVERIFY(roleNames.contains("error"));
3006+ QVERIFY(roleNames.contains("created"));
3007+ }
3008+
3009+ void shouldAddNewEntries()
3010+ {
3011+ QSignalSpy spy(model, SIGNAL(added(QString, QUrl, QString)));
3012+
3013+ model->add("testid", QUrl("http://example.org/"), "text/plain");
3014+ QCOMPARE(model->rowCount(), 1);
3015+ QCOMPARE(spy.count(), 1);
3016+ QVariantList args = spy.takeFirst();
3017+ QCOMPARE(args.at(0).toString(), QString("testid"));
3018+ QCOMPARE(args.at(1).toUrl(), QUrl("http://example.org/"));
3019+ QCOMPARE(args.at(2).toString(), QString("text/plain"));
3020+
3021+ model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");
3022+ QCOMPARE(model->rowCount(), 2);
3023+ QCOMPARE(spy.count(), 1);
3024+ args = spy.takeFirst();
3025+ QCOMPARE(args.at(0).toString(), QString("testid2"));
3026+ QCOMPARE(args.at(1).toUrl(), QUrl("http://example.org/pdf"));
3027+ QCOMPARE(args.at(2).toString(), QString("application/pdf"));
3028+ }
3029+
3030+ void shouldRemoveCancelled()
3031+ {
3032+ model->add("testid", QUrl("http://example.org/"), "text/plain");
3033+ model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");
3034+ model->add("testid3", QUrl("https://example.org/secure.png"), "image/png");
3035+ QCOMPARE(model->rowCount(), 3);
3036+
3037+ model->cancelDownload("testid2");
3038+ QCOMPARE(model->rowCount(), 2);
3039+
3040+ model->cancelDownload("invalid");
3041+ QCOMPARE(model->rowCount(), 2);
3042+ }
3043+
3044+ void shouldCompleteDownloads()
3045+ {
3046+ QSignalSpy spy(model, SIGNAL(completeChanged(QString, bool)));
3047+
3048+ model->add("testid", QUrl("http://example.org/"), "text/plain");
3049+ QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Complete).toBool());
3050+ model->setComplete("testid", true);
3051+ QCOMPARE(spy.count(), 1);
3052+ QVariantList args = spy.takeFirst();
3053+ QCOMPARE(args.at(0).toString(), QString("testid"));
3054+ QCOMPARE(args.at(1).toBool(), true);
3055+ }
3056+
3057+ void shouldKeepEntriesSortedChronologically()
3058+ {
3059+ model->add("testid", QUrl("http://example.org/"), "text/plain");
3060+ model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");
3061+ model->add("testid3", QUrl("https://example.org/secure.png"), "image/png");
3062+
3063+ QCOMPARE(model->data(model->index(0, 0), DownloadsModel::DownloadId).toString(), QString("testid3"));
3064+ QCOMPARE(model->data(model->index(1, 0), DownloadsModel::DownloadId).toString(), QString("testid2"));
3065+ QCOMPARE(model->data(model->index(2, 0), DownloadsModel::DownloadId).toString(), QString("testid"));
3066+ }
3067+
3068+ void shouldReturnData()
3069+ {
3070+ model->add("testid", QUrl("http://example.org/"), "text/plain");
3071+ QVERIFY(!model->data(QModelIndex(), DownloadsModel::DownloadId).isValid());
3072+ QVERIFY(!model->data(model->index(-1, 0), DownloadsModel::DownloadId).isValid());
3073+ QVERIFY(!model->data(model->index(3, 0), DownloadsModel::DownloadId).isValid());
3074+ QCOMPARE(model->data(model->index(0, 0), DownloadsModel::DownloadId).toString(), QString("testid"));
3075+ QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Url).toUrl(), QUrl("http://example.org/"));
3076+ QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Mimetype).toString(), QString("text/plain"));
3077+ QVERIFY(model->data(model->index(0, 0), DownloadsModel::Created).toDateTime() <= QDateTime::currentDateTime());
3078+ QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Complete).toBool());
3079+ }
3080+
3081+ void shouldReturnDatabasePath()
3082+ {
3083+ QCOMPARE(model->databasePath(), QString(":memory:"));
3084+ }
3085+
3086+ void shouldNotifyWhenSettingDatabasePath()
3087+ {
3088+ QSignalSpy spyPath(model, SIGNAL(databasePathChanged()));
3089+ QSignalSpy spyReset(model, SIGNAL(modelReset()));
3090+
3091+ model->setDatabasePath(":memory:");
3092+ QVERIFY(spyPath.isEmpty());
3093+ QVERIFY(spyReset.isEmpty());
3094+
3095+ model->setDatabasePath("");
3096+ QCOMPARE(spyPath.count(), 1);
3097+ QCOMPARE(spyReset.count(), 1);
3098+ QCOMPARE(model->databasePath(), QString(":memory:"));
3099+ }
3100+
3101+ void shouldSerializeOnDisk()
3102+ {
3103+ QTemporaryFile tempFile;
3104+ tempFile.open();
3105+ QString fileName = tempFile.fileName();
3106+ delete model;
3107+ model = new DownloadsModel;
3108+ model->setDatabasePath(fileName);
3109+ model->add("testid", QUrl("http://example.org/"), "text/plain");
3110+ model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");
3111+ delete model;
3112+ model = new DownloadsModel;
3113+ model->setDatabasePath(fileName);
3114+ model->fetchMore();
3115+ QCOMPARE(model->rowCount(), 2);
3116+ }
3117+
3118+ void shouldCountNumberOfEntries()
3119+ {
3120+ QCOMPARE(model->property("count").toInt(), 0);
3121+ QCOMPARE(model->rowCount(), 0);
3122+ model->add("testid", QUrl("http://example.org/"), "text/plain");
3123+ QCOMPARE(model->property("count").toInt(), 1);
3124+ QCOMPARE(model->rowCount(), 1);
3125+ model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");
3126+ QCOMPARE(model->property("count").toInt(), 2);
3127+ QCOMPARE(model->rowCount(), 2);
3128+ model->add("testid3", QUrl("https://example.org/secure.png"), "image/png");
3129+ QCOMPARE(model->property("count").toInt(), 3);
3130+ QCOMPARE(model->rowCount(), 3);
3131+ }
3132+
3133+};
3134+
3135+QTEST_MAIN(DownloadsModelTests)
3136+#include "tst_DownloadsModelTests.moc"

Subscribers

People subscribed via source and target branches

to status/vote changes: