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

Proposed by Michael Sheldon
Status: Merged
Approved by: Olivier Tilloy
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 Approve
PS Jenkins bot continuous-integration Needs Fixing
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.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1123. By Michael Sheldon

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

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1124. By Michael Sheldon

Merge from trunk

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1125. By Michael Sheldon

Remove unused import of urlManagement.js

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1126. By Michael Sheldon

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

1127. By Michael Sheldon

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

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1128. By Michael Sheldon

Fix focus when showing downloads page

1129. By Michael Sheldon

Update tests to use renamed BrowserPageHeader instead of SettingsPageHeader

1130. By Michael Sheldon

Implement download selection

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1131. By Michael Sheldon

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

Merge from trunk

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
1133. By Michael Sheldon

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

1134. By Michael Sheldon

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

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1135. By Michael Sheldon

Fix single item selection from downloads page

1136. By Michael Sheldon

Merge from trunk

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1137. By Michael Sheldon

Fix typo

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1138. By Michael Sheldon

Capitalize mimetype names

1139. By Michael Sheldon

Fix support for downloads in webapps

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1140. By Michael Sheldon

Add tests for new download dialog

1141. By Michael Sheldon

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

1142. By Michael Sheldon

Fix flake8 tests

1143. By Michael Sheldon

Merge from trunk

1144. By Michael Sheldon

Fix flake8 tests

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1145. By Michael Sheldon

Add text/vcard to content-hub mimetype mappings

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1146. By Michael Sheldon

Add basic empty state to downloads page

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1147. By Michael Sheldon

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

1148. By Michael Sheldon

Show filename in download dialog

1149. By Michael Sheldon

Merge from trunk

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1150. By Michael Sheldon

Remove unused signal from BrowserPageHeader and update docs

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1151. By Michael Sheldon

Bring download dialog style closer to visual spec

1152. By Michael Sheldon

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

1153. By Michael Sheldon

Update download dialog style based on visual spec

1154. By Michael Sheldon

Only show internal browser downloads on the downloads page

1155. By Michael Sheldon

Remove ubuntu shape from download page icons to match visual spec

1156. By Michael Sheldon

Remove unused extension property from downloads model

1157. By Michael Sheldon

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

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1158. By Michael Sheldon

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

1159. By Michael Sheldon

Calculate download filename prior to setting metadata title

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1160. By Michael Sheldon

Merge from trunk

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1161. By Michael Sheldon

Fix merge error

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1162. By Michael Sheldon

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

Add edit button to downloads page

1164. By Michael Sheldon

Fix eliding and spacing on DownloadDelegate labels

1165. By Michael Sheldon

Update DownloadDelegate label sizes to match visual design

1166. By Michael Sheldon

Update BrowserPageHeader and DownloadsPage styles to match visual spec

1167. By Michael Sheldon

Fix multi-selection on DownloadsPage when using the edit button

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1168. By Michael Sheldon

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

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1169. By Michael Sheldon

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

1170. By Michael Sheldon

Fix thumbnailing on download page whilst downloading videos or images

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1171. By Michael Sheldon

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

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1172. By Michael Sheldon

Merge from trunk

1173. By Michael Sheldon

Connect progress and cancel buttons to running downloads

1174. By Michael Sheldon

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

1175. By Michael Sheldon

Remove downloads that have been cancelled while the browser was closed

1176. By Michael Sheldon

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

1177. By Michael Sheldon

Allow access to ~/Downloads in the app armor manifest

1178. By Michael Sheldon

Switch to default apparmor template to allow for thumbnailer support

1179. By Michael Sheldon

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

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1180. By Michael Sheldon

Merge from trunk

1181. By Michael Sheldon

Fix typo in app armor profile

1182. By Michael Sheldon

Fix usage of renamed SettingsPageHeader -> BrowserPageHeader

1183. By Michael Sheldon

Fix download model index position when removing downloads

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1184. By Michael Sheldon

Display error messages for failed downloads

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1185. By Michael Sheldon

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

1186. By Michael Sheldon

Update error display on downloads page to match visual design

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1187. By Michael Sheldon

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

1188. By Michael Sheldon

Override server mimetype with detected mimetype for downloads when they complete

1189. By Michael Sheldon

Merge from trunk

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1190. By Michael Sheldon

Update UDM import version

1191. By Michael Sheldon

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

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1192. By Michael Sheldon

Fix download tests

1193. By Michael Sheldon

Fix flake8 failures

1194. By Michael Sheldon

Only allow downloads on non-desktop form factors

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1195. By Michael Sheldon

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

1196. By Michael Sheldon

Merge from trunk

1197. By Michael Sheldon

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

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1198. By Michael Sheldon

Add copyright notice to DownloadHandler

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1199. By Michael Sheldon

Merge from trunk

1200. By Michael Sheldon

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

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Olivier Tilloy (osomon) wrote :
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
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1201. By Michael Sheldon

Reuse existing QMimeDatabase instead of instantiating new ones

1202. By Michael Sheldon

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

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1203. By Michael Sheldon

Replace DownloadsMimetypeModel with a SortFilterModel

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1204. By Michael Sheldon

Fix internal content picking

1205. By Michael Sheldon

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

1206. By Michael Sheldon

Consistently import the same version of the content hub bindings

1207. By Michael Sheldon

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

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1208. By Michael Sheldon

Add unit tests for downloads model

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

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

Implement separate ContentDownloadDialogs for webbrowser and webcontainer

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

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

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

Revision history for this message
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.

Revision history for this message
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

Disable download tests on desktop systems

1212. By Michael Sheldon

Fix autopilot tests for download dialog after parenting change

1213. By Michael Sheldon

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

Revision history for this message
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

Don't show incomplete downloads in picker/selection mode

1215. By Michael Sheldon

Remove unnecessary focus changes

1216. By Michael Sheldon

Fix flake8 tests

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1217. By Michael Sheldon

Fix encapsulation for ContentPickerDialog

1218. By Michael Sheldon

Simplify download delegate visibility condition

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

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
Revision history for this message
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

Revision history for this message
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?

Revision history for this message
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

Fix access to downloads page when performing internal uploads

1220. By Michael Sheldon

Fix warnings from BrowserPageHeader

1221. By Michael Sheldon

Move all content hub imports to 1.3

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1222. By Michael Sheldon

Update content hub imports to 1.3

1223. By Michael Sheldon

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

1224. By Michael Sheldon

Fix peer picker autopilot test

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1225. By Michael Sheldon

Fix icon sizing when thumbnailer returns an invalid icon

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

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
=== modified file 'debian/control'
--- debian/control 2015-09-30 09:26:57 +0000
+++ debian/control 2015-12-16 16:25:40 +0000
@@ -44,7 +44,6 @@
44 qml-module-qt-labs-folderlistmodel,44 qml-module-qt-labs-folderlistmodel,
45 qml-module-qt-labs-settings,45 qml-module-qt-labs-settings,
46 qml-module-qtquick2 (>= 5.4),46 qml-module-qtquick2 (>= 5.4),
47 qml-module-qtquick-dialogs,
48 qml-module-qtquick-window2 (>= 5.3),47 qml-module-qtquick-window2 (>= 5.3),
49 qtdeclarative5-ubuntu-ui-toolkit-plugin (>= 1.3) | qtdeclarative5-ubuntu-ui-toolkit-plugin-gles (>= 1.3),48 qtdeclarative5-ubuntu-ui-toolkit-plugin (>= 1.3) | qtdeclarative5-ubuntu-ui-toolkit-plugin-gles (>= 1.3),
50 qtdeclarative5-ubuntu-web-plugin (= ${binary:Version}),49 qtdeclarative5-ubuntu-web-plugin (= ${binary:Version}),
5150
=== modified file 'debian/webbrowser-app-apparmor.manifest'
--- debian/webbrowser-app-apparmor.manifest 2015-10-22 15:07:38 +0000
+++ debian/webbrowser-app-apparmor.manifest 2015-12-16 16:25:40 +0000
@@ -3,7 +3,6 @@
3 "webbrowser-app": {3 "webbrowser-app": {
4 "binary": "/usr/bin/webbrowser-app",4 "binary": "/usr/bin/webbrowser-app",
5 "profile_name": "webbrowser-app",5 "profile_name": "webbrowser-app",
6 "template": "ubuntu-webapp",
7 "policy_vendor": "ubuntu",6 "policy_vendor": "ubuntu",
8 "policy_version": 1.3,7 "policy_version": 1.3,
9 "policy_groups": [8 "policy_groups": [
@@ -31,8 +30,12 @@
31 },30 },
32 "read_path": [31 "read_path": [
33 "/usr/share/applications/",32 "/usr/share/applications/",
33 "/custom/vendor/here/location-provider/consent/*.html",
34 "@{HOME}/.local/share/applications/",34 "@{HOME}/.local/share/applications/",
35 "/custom/vendor/here/location-provider/consent/*.html"35 "@{HOME}/Downloads/"
36 ],
37 "write_path": [
38 "@{HOME}/Downloads/"
36 ]39 ]
37 }40 }
38 }41 }
3942
=== modified file 'debian/webbrowser-app.install'
--- debian/webbrowser-app.install 2015-10-05 09:49:21 +0000
+++ debian/webbrowser-app.install 2015-12-16 16:25:40 +0000
@@ -7,4 +7,5 @@
7usr/share/applications/webbrowser-app.desktop7usr/share/applications/webbrowser-app.desktop
8usr/share/locale/*/LC_MESSAGES/webbrowser-app.mo8usr/share/locale/*/LC_MESSAGES/webbrowser-app.mo
9usr/share/url-dispatcher/9usr/share/url-dispatcher/
10usr/share/content-hub/peers/webbrowser-app
10debian/usr.bin.webbrowser-app etc/apparmor.d11debian/usr.bin.webbrowser-app etc/apparmor.d
1112
=== removed file 'src/app/ContentDownloadDialog.qml'
--- src/app/ContentDownloadDialog.qml 2015-08-10 15:22:00 +0000
+++ src/app/ContentDownloadDialog.qml 1970-01-01 00:00:00 +0000
@@ -1,50 +0,0 @@
1/*
2 * Copyright 2014-2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.4
20import Ubuntu.Components 1.3
21import Ubuntu.Components.Popups 1.3
22import Ubuntu.Content 0.1
23
24PopupBase {
25 id: downloadDialog
26 anchors.fill: parent
27 property var activeTransfer
28 property var downloadId
29 property alias contentType: peerPicker.contentType
30
31 Rectangle {
32 anchors.fill: parent
33 ContentPeerPicker {
34 id: peerPicker
35 handler: ContentHandler.Destination
36 visible: parent.visible
37
38 onPeerSelected: {
39 activeTransfer = peer.request()
40 activeTransfer.downloadId = downloadDialog.downloadId
41 activeTransfer.state = ContentTransfer.Downloading
42 PopupUtils.close(downloadDialog)
43 }
44
45 onCancelPressed: {
46 PopupUtils.close(downloadDialog)
47 }
48 }
49 }
50}
510
=== removed file 'src/app/ContentPickerDialog.qml'
--- src/app/ContentPickerDialog.qml 2015-08-18 07:51:11 +0000
+++ src/app/ContentPickerDialog.qml 1970-01-01 00:00:00 +0000
@@ -1,97 +0,0 @@
1/*
2 * Copyright 2014-2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.4
20import Ubuntu.Components 1.3
21import Ubuntu.Components.Popups 1.3 as Popups
22import Ubuntu.Content 0.1
23import "MimeTypeMapper.js" as MimeTypeMapper
24
25Component {
26 Popups.PopupBase {
27 id: picker
28 objectName: "contentPickerDialog"
29
30 // Set the parent at construction time, instead of letting show()
31 // set it later on, which for some reason results in the size of
32 // the dialog not being updated.
33 parent: QuickUtils.rootItem(this)
34
35 property var activeTransfer
36
37 Rectangle {
38 anchors.fill: parent
39
40 ContentTransferHint {
41 anchors.fill: parent
42 activeTransfer: picker.activeTransfer
43 }
44
45 ContentPeerPicker {
46 id: peerPicker
47 anchors.fill: parent
48 visible: true
49 contentType: ContentType.All
50 handler: ContentHandler.Source
51
52 onPeerSelected: {
53 if (model.allowMultipleFiles) {
54 peer.selectionType = ContentTransfer.Multiple
55 } else {
56 peer.selectionType = ContentTransfer.Single
57 }
58 picker.activeTransfer = peer.request()
59 stateChangeConnection.target = picker.activeTransfer
60 }
61
62 onCancelPressed: {
63 webview.focus = true
64 model.reject()
65 }
66 }
67 }
68
69 Connections {
70 id: stateChangeConnection
71 target: null
72 onStateChanged: {
73 if (picker.activeTransfer.state === ContentTransfer.Charged) {
74 var selectedItems = []
75 for(var i in picker.activeTransfer.items) {
76 selectedItems.push(String(picker.activeTransfer.items[i].url).replace("file://", ""))
77 }
78 model.accept(selectedItems)
79 }
80 }
81 }
82
83 Component.onCompleted: {
84 if(acceptTypes.length === 1) {
85 var contentType = MimeTypeMapper.mimeTypeToContentType(acceptTypes[0])
86 if(contentType == ContentType.Unknown) {
87 // If we don't recognise the type, allow uploads from any app
88 contentType = ContentType.All
89 }
90 peerPicker.contentType = contentType
91 } else {
92 peerPicker.contentType = ContentType.All
93 }
94 show()
95 }
96 }
97}
980
=== modified file 'src/app/ContentShareDialog.qml'
--- src/app/ContentShareDialog.qml 2015-08-10 15:22:00 +0000
+++ src/app/ContentShareDialog.qml 2015-12-16 16:25:40 +0000
@@ -19,7 +19,7 @@
19import QtQuick 2.419import QtQuick 2.4
20import Ubuntu.Components 1.320import Ubuntu.Components 1.3
21import Ubuntu.Components.Popups 1.321import Ubuntu.Components.Popups 1.3
22import Ubuntu.Content 0.122import Ubuntu.Content 1.3
2323
24PopupBase {24PopupBase {
25 id: shareDialog25 id: shareDialog
2626
=== modified file 'src/app/Downloader.qml'
--- src/app/Downloader.qml 2015-08-10 15:22:00 +0000
+++ src/app/Downloader.qml 2015-12-16 16:25:40 +0000
@@ -19,18 +19,18 @@
19import QtQuick 2.419import QtQuick 2.4
20import Ubuntu.Components 1.320import Ubuntu.Components 1.3
21import Ubuntu.Components.Popups 1.321import Ubuntu.Components.Popups 1.3
22import Ubuntu.DownloadManager 0.122import Ubuntu.DownloadManager 1.2
23import Ubuntu.Content 0.123import Ubuntu.Content 1.3
24import "MimeTypeMapper.js" as MimeTypeMapper24import "MimeTypeMapper.js" as MimeTypeMapper
25import "FileExtensionMapper.js" as FileExtensionMapper25import "FileExtensionMapper.js" as FileExtensionMapper
2626
27Item {27Item {
28 id: downloadItem28 id: downloadItem
2929
30 Component {30 property string filename
31 id: downloadDialog31 property string mimeType
32 ContentDownloadDialog { }32
33 }33 signal showDownloadDialog(string downloadId, var contentType, var downloader, string filename, string mimeType)
3434
35 Component {35 Component {
36 id: metadataComponent36 id: metadataComponent
@@ -42,15 +42,13 @@
42 Component {42 Component {
43 id: downloadComponent43 id: downloadComponent
44 SingleDownload {44 SingleDownload {
45 id: downloader
45 autoStart: false46 autoStart: false
46 property var contentType47 property var contentType
48 property string url
49
47 onDownloadIdChanged: {50 onDownloadIdChanged: {
48 PopupUtils.open(downloadDialog, downloadItem, {"contentType" : contentType, "downloadId" : downloadId})51 showDownloadDialog(downloadId, contentType, downloader, downloadItem.filename, downloadItem.mimeType)
49 }
50
51 onFinished: {
52 metadata.destroy()
53 destroy()
54 }52 }
55 }53 }
56 }54 }
@@ -62,11 +60,13 @@
62 singleDownload.headers = headers60 singleDownload.headers = headers
63 }61 }
64 singleDownload.metadata = metadata62 singleDownload.metadata = metadata
63 singleDownload.url = url
65 singleDownload.download(url)64 singleDownload.download(url)
66 }65 }
6766
68 function downloadPicture(url, headers) {67 function downloadPicture(url, headers) {
69 var metadata = metadataComponent.createObject(downloadItem)68 var metadata = metadataComponent.createObject(downloadItem)
69 downloadItem.mimeType = "image/*"
70 download(url, ContentType.Pictures, headers, metadata)70 download(url, ContentType.Pictures, headers, metadata)
71 }71 }
7272
@@ -86,7 +86,12 @@
86 contentType = ContentType.Music86 contentType = ContentType.Music
87 metadata.extract = true87 metadata.extract = true
88 }88 }
89 if (!filename) {
90 filename = url.toString().split("/").pop()
91 }
89 metadata.title = filename92 metadata.title = filename
93 downloadItem.filename = filename
94 downloadItem.mimeType = mimeType
90 download(url, contentType, headers, metadata)95 download(url, contentType, headers, metadata)
91 }96 }
9297
9398
=== removed file 'src/app/FilePickerDialog.qml'
--- src/app/FilePickerDialog.qml 2015-08-25 08:36:06 +0000
+++ src/app/FilePickerDialog.qml 1970-01-01 00:00:00 +0000
@@ -1,45 +0,0 @@
1/*
2 * Copyright 2014-2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.4
20import QtQuick.Dialogs 1.0
21import Ubuntu.Components 1.3
22import Ubuntu.Components.Popups 1.3 as Popups
23
24Component {
25 Popups.Dialog {
26 FileDialog {
27 id: fileDialog
28 title: i18n.tr("Please choose a file")
29 selectMultiple: model.allowMultipleFiles
30
31 onAccepted: {
32 var selectedFiles = []
33 for(var i in fileDialog.fileUrls) {
34 selectedFiles.push(fileDialog.fileUrls[i].replace("file://", ""))
35 }
36 model.accept(selectedFiles)
37 }
38
39 onRejected: {
40 model.reject()
41 }
42 Component.onCompleted: visible = true
43 }
44 }
45}
460
=== modified file 'src/app/MimeTypeMapper.js'
--- src/app/MimeTypeMapper.js 2015-09-23 14:00:09 +0000
+++ src/app/MimeTypeMapper.js 2015-12-16 16:25:40 +0000
@@ -42,3 +42,23 @@
42 return ContentType.Unknown;42 return ContentType.Unknown;
43 }43 }
44}44}
45
46function mimeTypeRegexForContentType(contentType) {
47 switch (contentType) {
48 case ContentType.Pictures:
49 return /image\/.*/;
50 case ContentType.Music:
51 return /audio\/.*/;
52 case ContentType.Videos:
53 return /video\/.*/;
54 case ContentType.Contacts:
55 return /text\/(x-vcard|vcard)/;
56 case ContentType.EBooks:
57 return /application\/(epub.*|vnd.amazon.ebook|x-mobipocket-ebook|x-fictionbook+xml|x-ms-reader)/;
58 case ContentType.Documents:
59 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)/;
60 case ContentType.Unknown:
61 case ContentType.All:
62 return /.*/;
63 }
64}
4565
=== modified file 'src/app/Share.qml'
--- src/app/Share.qml 2015-09-01 12:41:13 +0000
+++ src/app/Share.qml 2015-12-16 16:25:40 +0000
@@ -19,7 +19,7 @@
19import QtQuick 2.419import QtQuick 2.4
20import Ubuntu.Components 1.320import Ubuntu.Components 1.3
21import Ubuntu.Components.Popups 1.321import Ubuntu.Components.Popups 1.3
22import Ubuntu.Content 0.122import Ubuntu.Content 1.3
2323
24Item {24Item {
25 id: shareItem25 id: shareItem
2626
=== modified file 'src/app/WebViewImpl.qml'
--- src/app/WebViewImpl.qml 2015-09-01 07:02:13 +0000
+++ src/app/WebViewImpl.qml 2015-12-16 16:25:40 +0000
@@ -34,7 +34,8 @@
34 confirmDialog: ConfirmDialog {}34 confirmDialog: ConfirmDialog {}
35 promptDialog: PromptDialog {}35 promptDialog: PromptDialog {}
36 beforeUnloadDialog: BeforeUnloadDialog {}36 beforeUnloadDialog: BeforeUnloadDialog {}
37 filePicker: filePickerLoader.item37
38 signal showDownloadDialog(string downloadId, var contentType, var downloader, string filename, string mimeType)
3839
39 QtObject {40 QtObject {
40 id: internal41 id: internal
@@ -62,9 +63,10 @@
62 }63 }
63 headers["User-Agent"] = webview.context.userAgent64 headers["User-Agent"] = webview.context.userAgent
64 // Work around https://launchpad.net/bugs/1487090 by guessing the mime type65 // Work around https://launchpad.net/bugs/1487090 by guessing the mime type
65 // from the suggested filename or URL if oxide hasn’t provided one.66 // from the suggested filename or URL if oxide hasn’t provided one, or if
67 // the server has provided the generic application/octet-stream mime type.
66 var mimeType = request.mimeType68 var mimeType = request.mimeType
67 if (!mimeType) {69 if (!mimeType || mimeType == "application/octet-stream") {
68 mimeType = MimeDatabase.filenameToMimeType(request.suggestedFilename)70 mimeType = MimeDatabase.filenameToMimeType(request.suggestedFilename)
69 }71 }
70 if (!mimeType) {72 if (!mimeType) {
@@ -87,20 +89,18 @@
87 }89 }
8890
89 Loader {91 Loader {
90 id: filePickerLoader
91 source: formFactor == "desktop" ? "FilePickerDialog.qml" : "ContentPickerDialog.qml"
92 asynchronous: true
93 }
94
95 Loader {
96 id: downloadLoader92 id: downloadLoader
97 // TODO: Use the ubuntu download manager on desktop as well93 source: "Downloader.qml"
98 // (https://launchpad.net/bugs/1477310). This will require to have
99 // ubuntu-download-manager in main (https://launchpad.net/bugs/1488425).
100 source: formFactor == "desktop" ? "" : "Downloader.qml"
101 asynchronous: true94 asynchronous: true
102 }95 }
10396
97 Connections {
98 target: downloadLoader.item
99 onShowDownloadDialog: {
100 showDownloadDialog(downloadId, contentType, downloader, filename, mimeType)
101 }
102 }
103
104 function requestGeolocationPermission(request) {104 function requestGeolocationPermission(request) {
105 PopupUtils.open(Qt.resolvedUrl("GeolocationPermissionRequest.qml"),105 PopupUtils.open(Qt.resolvedUrl("GeolocationPermissionRequest.qml"),
106 webview.currentWebview, {"request": request})106 webview.currentWebview, {"request": request})
107107
=== modified file 'src/app/mime-database.cpp'
--- src/app/mime-database.cpp 2015-08-26 11:29:00 +0000
+++ src/app/mime-database.cpp 2015-12-16 16:25:40 +0000
@@ -16,6 +16,8 @@
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */17 */
1818
19#include <QIcon>
20
19#include "mime-database.h"21#include "mime-database.h"
2022
21MimeDatabase::MimeDatabase(QObject* parent)23MimeDatabase::MimeDatabase(QObject* parent)
@@ -31,3 +33,31 @@
31 }33 }
32 return QString();34 return QString();
33}35}
36
37/*!
38 Provide the system icon name for a given mimetype
39*/
40QString MimeDatabase::iconForMimetype(const QString& mimetypeString) const
41{
42 QMimeType mimetype = m_database.mimeTypeForName(mimetypeString);
43 if (mimetype.iconName().isEmpty() || !QIcon::hasThemeIcon(mimetype.iconName())) {
44 if (QIcon::hasThemeIcon(mimetype.genericIconName())) {
45 return mimetype.genericIconName();
46 } else {
47 return "";
48 }
49 } else {
50 return mimetype.iconName();
51 }
52}
53
54/*!
55 Provide the user friendly name for a given mimetype
56*/
57QString MimeDatabase::nameForMimetype(const QString& mimetypeString) const
58{
59 QMimeType mimetype = m_database.mimeTypeForName(mimetypeString);
60 return mimetype.comment();
61}
62
63
3464
=== modified file 'src/app/mime-database.h'
--- src/app/mime-database.h 2015-08-26 11:29:00 +0000
+++ src/app/mime-database.h 2015-12-16 16:25:40 +0000
@@ -31,6 +31,9 @@
31 explicit MimeDatabase(QObject* parent=0);31 explicit MimeDatabase(QObject* parent=0);
3232
33 Q_INVOKABLE QString filenameToMimeType(const QString& filename) const;33 Q_INVOKABLE QString filenameToMimeType(const QString& filename) const;
34 Q_INVOKABLE QString iconForMimetype(const QString& mimetypeString) const;
35 Q_INVOKABLE QString nameForMimetype(const QString& mimetypeString) const;
36
3437
35private:38private:
36 QMimeDatabase m_database;39 QMimeDatabase m_database;
3740
=== modified file 'src/app/webbrowser/Browser.qml'
--- src/app/webbrowser/Browser.qml 2015-12-04 11:06:47 +0000
+++ src/app/webbrowser/Browser.qml 2015-12-16 16:25:40 +0000
@@ -37,6 +37,9 @@
3737
38 currentWebview: tabsModel && tabsModel.currentTab ? tabsModel.currentTab.webview : null38 currentWebview: tabsModel && tabsModel.currentTab ? tabsModel.currentTab.webview : null
3939
40 property var downloadsModel: (downloadsModelLoader.status == Loader.Ready) ? downloadsModelLoader.item : null
41 property var downloadManager: (downloadHandlerLoader.status == Loader.Ready) ? downloadHandlerLoader.item : null
42
40 property bool newSession: false43 property bool newSession: false
4144
42 property bool incognito: false45 property bool incognito: false
@@ -168,7 +171,7 @@
168171
169 FocusScope {172 FocusScope {
170 anchors.fill: parent173 anchors.fill: parent
171 visible: !settingsContainer.visible && !historyViewLoader.active && !bookmarksViewLoader.active174 visible: !settingsContainer.visible && !historyViewLoader.active && !bookmarksViewLoader.active && !downloadsContainer.visible
172175
173 FocusScope {176 FocusScope {
174 id: tabContainer177 id: tabContainer
@@ -537,6 +540,15 @@
537 onTriggered: chrome.findInPageMode = true540 onTriggered: chrome.findInPageMode = true
538 },541 },
539 Action {542 Action {
543 objectName: "downloads"
544 text: i18n.tr("Downloads")
545 iconName: "save"
546 enabled: downloadHandlerLoader.status == Loader.Ready
547 onTriggered: {
548 currentWebview.showDownloadsPage()
549 }
550 },
551 Action {
540 objectName: "privatemode"552 objectName: "privatemode"
541 text: browser.incognito ? i18n.tr("Leave Private Mode") : i18n.tr("Private Mode")553 text: browser.incognito ? i18n.tr("Leave Private Mode") : i18n.tr("Private Mode")
542 iconName: "private-browsing"554 iconName: "private-browsing"
@@ -905,6 +917,28 @@
905 }917 }
906 }918 }
907919
920 FocusScope {
921 id: downloadsContainer
922
923 visible: children.length > 0
924 anchors.fill: parent
925
926 Component {
927 id: downloadsComponent
928
929 DownloadsPage {
930 anchors.fill: parent
931 focus: true
932 downloadsModel: browser.downloadsModel
933 onDone: destroy()
934 Keys.onEscapePressed: {
935 destroy()
936 internal.resetFocus()
937 }
938 }
939 }
940 }
941
908 TabsModel {942 TabsModel {
909 id: publicTabsModel943 id: publicTabsModel
910 }944 }
@@ -930,6 +964,17 @@
930 }964 }
931 }965 }
932966
967 Loader {
968 id: downloadsModelLoader
969 source: "DownloadsModel.qml"
970 asynchronous: true
971 }
972
973 Loader {
974 id: downloadHandlerLoader
975 source: "DownloadHandler.qml"
976 }
977
933 Component {978 Component {
934 id: tabComponent979 id: tabComponent
935980
@@ -950,6 +995,7 @@
950 readonly property bool current: tab.current995 readonly property bool current: tab.current
951996
952 currentWebview: browser.currentWebview997 currentWebview: browser.currentWebview
998 filePicker: filePickerLoader.item
953999
954 anchors.fill: parent1000 anchors.fill: parent
955 focus: true1001 focus: true
@@ -1241,6 +1287,29 @@
1241 Component.onDestruction: bottomEdgeHint.forceShow = false1287 Component.onDestruction: bottomEdgeHint.forceShow = false
1242 }1288 }
1243 }1289 }
1290
1291 onShowDownloadDialog: {
1292 if (downloadDialogLoader.status === Loader.Ready) {
1293 var downloadDialog = PopupUtils.open(downloadDialogLoader.item, browser, {"contentType" : contentType,
1294 "downloadId" : downloadId,
1295 "singleDownload" : downloader,
1296 "filename" : filename,
1297 "mimeType" : mimeType})
1298 downloadDialog.startDownload.connect(startDownload)
1299 }
1300 }
1301
1302 function showDownloadsPage() {
1303 downloadsContainer.focus = true
1304 return downloadsComponent.createObject(downloadsContainer)
1305 }
1306
1307 function startDownload(downloadId, download, mimeType) {
1308 downloadsModel.add(downloadId, download.url, mimeType)
1309 download.start()
1310 showDownloadsPage()
1311 }
1312
1244 }1313 }
1245 }1314 }
1246 }1315 }
@@ -1743,13 +1812,13 @@
1743 KeyboardShortcut {1812 KeyboardShortcut {
1744 modifiers: Qt.ControlModifier1813 modifiers: Qt.ControlModifier
1745 key: Qt.Key_Tab1814 key: Qt.Key_Tab
1746 enabled: chrome.visible || recentView.visible1815 enabled: (chrome.visible || recentView.visible) && !downloadsContainer.visible
1747 onTriggered: internal.switchToNextTab()1816 onTriggered: internal.switchToNextTab()
1748 }1817 }
1749 KeyboardShortcut {1818 KeyboardShortcut {
1750 modifiers: Qt.ControlModifier1819 modifiers: Qt.ControlModifier
1751 key: Qt.Key_PageDown1820 key: Qt.Key_PageDown
1752 enabled: chrome.visible || recentView.visible1821 enabled: (chrome.visible || recentView.visible) && !downloadsContainer.visible
1753 onTriggered: internal.switchToNextTab()1822 onTriggered: internal.switchToNextTab()
1754 }1823 }
17551824
@@ -1757,13 +1826,13 @@
1757 KeyboardShortcut {1826 KeyboardShortcut {
1758 modifiers: Qt.ControlModifier1827 modifiers: Qt.ControlModifier
1759 key: Qt.Key_Backtab1828 key: Qt.Key_Backtab
1760 enabled: chrome.visible || recentView.visible1829 enabled: (chrome.visible || recentView.visible) && !downloadsContainer.visible
1761 onTriggered: internal.switchToPreviousTab()1830 onTriggered: internal.switchToPreviousTab()
1762 }1831 }
1763 KeyboardShortcut {1832 KeyboardShortcut {
1764 modifiers: Qt.ControlModifier1833 modifiers: Qt.ControlModifier
1765 key: Qt.Key_PageUp1834 key: Qt.Key_PageUp
1766 enabled: chrome.visible || recentView.visible1835 enabled: (chrome.visible || recentView.visible) && !downloadsContainer.visible
1767 onTriggered: internal.switchToPreviousTab()1836 onTriggered: internal.switchToPreviousTab()
1768 }1837 }
17691838
@@ -1771,14 +1840,14 @@
1771 KeyboardShortcut {1840 KeyboardShortcut {
1772 modifiers: Qt.ControlModifier | Qt.ShiftModifier1841 modifiers: Qt.ControlModifier | Qt.ShiftModifier
1773 key: Qt.Key_W1842 key: Qt.Key_W
1774 enabled: chrome.visible || recentView.visible1843 enabled: (chrome.visible || recentView.visible) && !downloadsContainer.visible
1775 onTriggered: internal.undoCloseTab()1844 onTriggered: internal.undoCloseTab()
1776 }1845 }
17771846
1778 KeyboardShortcut {1847 KeyboardShortcut {
1779 modifiers: Qt.ControlModifier | Qt.ShiftModifier1848 modifiers: Qt.ControlModifier | Qt.ShiftModifier
1780 key: Qt.Key_T1849 key: Qt.Key_T
1781 enabled: chrome.visible || recentView.visible1850 enabled: (chrome.visible || recentView.visible) && !downloadsContainer.visible
1782 onTriggered: internal.undoCloseTab()1851 onTriggered: internal.undoCloseTab()
1783 }1852 }
17841853
@@ -1786,13 +1855,13 @@
1786 KeyboardShortcut {1855 KeyboardShortcut {
1787 modifiers: Qt.ControlModifier1856 modifiers: Qt.ControlModifier
1788 key: Qt.Key_W1857 key: Qt.Key_W
1789 enabled: chrome.visible || recentView.visible1858 enabled: (chrome.visible || recentView.visible) && !downloadsContainer.visible
1790 onTriggered: internal.closeCurrentTab()1859 onTriggered: internal.closeCurrentTab()
1791 }1860 }
1792 KeyboardShortcut {1861 KeyboardShortcut {
1793 modifiers: Qt.ControlModifier1862 modifiers: Qt.ControlModifier
1794 key: Qt.Key_F41863 key: Qt.Key_F4
1795 enabled: chrome.visible || recentView.visible1864 enabled: (chrome.visible || recentView.visible) && !downloadsContainer.visible
1796 onTriggered: internal.closeCurrentTab()1865 onTriggered: internal.closeCurrentTab()
1797 }1866 }
17981867
@@ -1800,7 +1869,7 @@
1800 KeyboardShortcut {1869 KeyboardShortcut {
1801 modifiers: Qt.ControlModifier1870 modifiers: Qt.ControlModifier
1802 key: Qt.Key_T1871 key: Qt.Key_T
1803 enabled: chrome.visible || recentView.visible || bookmarksViewLoader.active || historyViewLoader.active1872 enabled: (chrome.visible || recentView.visible || bookmarksViewLoader.active || historyViewLoader.active) && !downloadsContainer.visible
1804 onTriggered: {1873 onTriggered: {
1805 openUrlInNewTab("", true)1874 openUrlInNewTab("", true)
1806 if (recentView.visible) recentView.reset()1875 if (recentView.visible) recentView.reset()
@@ -1814,18 +1883,18 @@
1814 KeyboardShortcut {1883 KeyboardShortcut {
1815 modifiers: Qt.ControlModifier1884 modifiers: Qt.ControlModifier
1816 key: Qt.Key_L1885 key: Qt.Key_L
1817 enabled: chrome.visible1886 enabled: chrome.visible && !downloadsContainer.visible
1818 onTriggered: internal.focusAddressBar(true)1887 onTriggered: internal.focusAddressBar(true)
1819 }1888 }
1820 KeyboardShortcut {1889 KeyboardShortcut {
1821 modifiers: Qt.AltModifier1890 modifiers: Qt.AltModifier
1822 key: Qt.Key_D1891 key: Qt.Key_D
1823 enabled: chrome.visible1892 enabled: chrome.visible && !downloadsContainer.visible
1824 onTriggered: internal.focusAddressBar(true)1893 onTriggered: internal.focusAddressBar(true)
1825 }1894 }
1826 KeyboardShortcut {1895 KeyboardShortcut {
1827 key: Qt.Key_F61896 key: Qt.Key_F6
1828 enabled: chrome.visible1897 enabled: chrome.visible && !downloadsContainer.visible
1829 onTriggered: internal.focusAddressBar(true)1898 onTriggered: internal.focusAddressBar(true)
1830 }1899 }
18311900
@@ -1833,7 +1902,7 @@
1833 KeyboardShortcut {1902 KeyboardShortcut {
1834 modifiers: Qt.ControlModifier1903 modifiers: Qt.ControlModifier
1835 key: Qt.Key_D1904 key: Qt.Key_D
1836 enabled: chrome.visible1905 enabled: chrome.visible && !downloadsContainer.visible
1837 onTriggered: {1906 onTriggered: {
1838 if (currentWebview) {1907 if (currentWebview) {
1839 if (BookmarksModel.contains(currentWebview.url)) {1908 if (BookmarksModel.contains(currentWebview.url)) {
@@ -1849,7 +1918,7 @@
1849 KeyboardShortcut {1918 KeyboardShortcut {
1850 modifiers: Qt.ControlModifier1919 modifiers: Qt.ControlModifier
1851 key: Qt.Key_H1920 key: Qt.Key_H
1852 enabled: chrome.visible1921 enabled: chrome.visible && !downloadsContainer.visible
1853 onTriggered: historyViewLoader.active = true1922 onTriggered: historyViewLoader.active = true
1854 }1923 }
18551924
@@ -1857,7 +1926,7 @@
1857 KeyboardShortcut {1926 KeyboardShortcut {
1858 modifiers: Qt.ControlModifier | Qt.ShiftModifier1927 modifiers: Qt.ControlModifier | Qt.ShiftModifier
1859 key: Qt.Key_O1928 key: Qt.Key_O
1860 enabled: chrome.visible1929 enabled: chrome.visible && !downloadsContainer.visible
1861 onTriggered: bookmarksViewLoader.active = true1930 onTriggered: bookmarksViewLoader.active = true
1862 }1931 }
18631932
@@ -1865,12 +1934,12 @@
1865 KeyboardShortcut {1934 KeyboardShortcut {
1866 modifiers: Qt.AltModifier1935 modifiers: Qt.AltModifier
1867 key: Qt.Key_Left1936 key: Qt.Key_Left
1868 enabled: chrome.visible1937 enabled: chrome.visible && !downloadsContainer.visible
1869 onTriggered: internal.historyGoBack()1938 onTriggered: internal.historyGoBack()
1870 }1939 }
1871 KeyboardShortcut {1940 KeyboardShortcut {
1872 key: Qt.Key_Backspace1941 key: Qt.Key_Backspace
1873 enabled: chrome.visible1942 enabled: chrome.visible && !downloadsContainer.visible
1874 onTriggered: internal.historyGoBack()1943 onTriggered: internal.historyGoBack()
1875 }1944 }
18761945
@@ -1878,26 +1947,26 @@
1878 KeyboardShortcut {1947 KeyboardShortcut {
1879 modifiers: Qt.AltModifier1948 modifiers: Qt.AltModifier
1880 key: Qt.Key_Right1949 key: Qt.Key_Right
1881 enabled: chrome.visible1950 enabled: chrome.visible && !downloadsContainer.visible
1882 onTriggered: internal.historyGoForward()1951 onTriggered: internal.historyGoForward()
1883 }1952 }
1884 KeyboardShortcut {1953 KeyboardShortcut {
1885 modifiers: Qt.ShiftModifier1954 modifiers: Qt.ShiftModifier
1886 key: Qt.Key_Backspace1955 key: Qt.Key_Backspace
1887 enabled: chrome.visible1956 enabled: chrome.visible && !downloadsContainer.visible
1888 onTriggered: internal.historyGoForward()1957 onTriggered: internal.historyGoForward()
1889 }1958 }
18901959
1891 // F5 or Ctrl+R: Reload current Tab1960 // F5 or Ctrl+R: Reload current Tab
1892 KeyboardShortcut {1961 KeyboardShortcut {
1893 key: Qt.Key_F51962 key: Qt.Key_F5
1894 enabled: chrome.visible1963 enabled: chrome.visible && !downloadsContainer.visible
1895 onTriggered: if (currentWebview) currentWebview.reload()1964 onTriggered: if (currentWebview) currentWebview.reload()
1896 }1965 }
1897 KeyboardShortcut {1966 KeyboardShortcut {
1898 modifiers: Qt.ControlModifier1967 modifiers: Qt.ControlModifier
1899 key: Qt.Key_R1968 key: Qt.Key_R
1900 enabled: chrome.visible1969 enabled: chrome.visible && !downloadsContainer.visible
1901 onTriggered: if (currentWebview) currentWebview.reload()1970 onTriggered: if (currentWebview) currentWebview.reload()
1902 }1971 }
19031972
@@ -1905,8 +1974,48 @@
1905 KeyboardShortcut {1974 KeyboardShortcut {
1906 modifiers: Qt.ControlModifier1975 modifiers: Qt.ControlModifier
1907 key: Qt.Key_F1976 key: Qt.Key_F
1908 enabled: !newTabViewLoader.active && !bookmarksViewLoader.active1977 enabled: !newTabViewLoader.active && !bookmarksViewLoader.active && !downloadsContainer.visible
1909 onTriggered: chrome.findInPageMode = true1978 onTriggered: chrome.findInPageMode = true
1910 }1979 }
1911 }1980
1981 // Ctrl + J: Show downloads page
1982 KeyboardShortcut {
1983 modifiers: Qt.ControlModifier
1984 key: Qt.Key_J
1985 enabled: chrome.visible && !downloadsContainer.visible
1986 onTriggered: currentWebview.showDownloadsPage()
1987 }
1988 }
1989
1990 Loader {
1991 id: contentHandlerLoader
1992 source: "ContentHandler.qml"
1993 }
1994
1995 Connections {
1996 target: contentHandlerLoader.item
1997 onExportFromDownloads: {
1998 if (downloadHandlerLoader.status == Loader.Ready) {
1999 downloadsContainer.focus = true
2000 var downloadPage = downloadsComponent.createObject(downloadsContainer)
2001 downloadPage.mimetypeFilter = mimetypeFilter
2002 downloadPage.activeTransfer = transfer
2003 downloadPage.multiSelect = multiSelect
2004 downloadPage.pickingMode = true
2005 }
2006 }
2007 }
2008
2009 Loader {
2010 id: downloadDialogLoader
2011 source: "ContentDownloadDialog.qml"
2012 asynchronous: true
2013 }
2014
2015 Loader {
2016 id: filePickerLoader
2017 source: "ContentPickerDialog.qml"
2018 asynchronous: true
2019 }
2020
1912}2021}
19132022
=== renamed file 'src/app/webbrowser/SettingsPageHeader.qml' => 'src/app/webbrowser/BrowserPageHeader.qml'
--- src/app/webbrowser/SettingsPageHeader.qml 2015-08-10 15:22:00 +0000
+++ src/app/webbrowser/BrowserPageHeader.qml 2015-12-16 16:25:40 +0000
@@ -21,18 +21,22 @@
21import Ubuntu.Components.ListItems 1.3 as ListItem21import Ubuntu.Components.ListItems 1.3 as ListItem
2222
23/*23/*
24 * Component to use as page header in settings page and subpages24 * Component to use as page header in settings page, download page and
25 * subpages
25 *26 *
26 * It has a back() signal fired when back button is pressed and a text27 * It has a back() signal fired when back button is pressed, a text
27 * property to set the page title28 * property to set the page title and an actions property which
29 * displays action icons on the right of header.
28 */30 */
2931
30Column {32Item {
31 id: root33 id: root
32 signal back()34 signal back()
33 property string text35 property string text
36 property alias actions: actionBar.actions
37 property alias color: title.color
3438
35 height: childrenRect.height39 height: title.height + divider.height
3640
37 anchors {41 anchors {
38 left: parent.left42 left: parent.left
@@ -42,13 +46,9 @@
42 Rectangle {46 Rectangle {
43 id: title47 id: title
4448
45 height: units.gu(7) - divider.height49 height: units.gu(6) - divider.height
46 anchors { left: parent.left; right: parent.right }50 anchors { left: parent.left; right: parent.right }
4751 color: "#f6f6f6"
48 Rectangle {
49 anchors.fill: parent
50 color: "#f6f6f6"
51 }
5252
53 AbstractButton {53 AbstractButton {
54 id: backButton54 id: backButton
@@ -89,9 +89,24 @@
89 text: root.text89 text: root.text
90 fontSize: 'x-large'90 fontSize: 'x-large'
91 }91 }
92
93 ActionBar {
94 id: actionBar
95 anchors.right: parent.right
96 anchors.verticalCenter: parent.verticalCenter
97 }
98
99
92 }100 }
93101
94 ListItem.Divider {102 Rectangle {
95 id: divider103 id: divider
104 anchors {
105 left: parent.left
106 right: parent.right
107 bottom: parent.bottom
108 }
109 height: units.dp(1)
110 color: Qt.darker(title.color, 1.1)
96 }111 }
97}112}
98113
=== modified file 'src/app/webbrowser/CMakeLists.txt'
--- src/app/webbrowser/CMakeLists.txt 2015-11-23 09:41:48 +0000
+++ src/app/webbrowser/CMakeLists.txt 2015-12-16 16:25:40 +0000
@@ -13,6 +13,7 @@
13 bookmarks-model.cpp13 bookmarks-model.cpp
14 bookmarks-folder-model.cpp14 bookmarks-folder-model.cpp
15 bookmarks-folderlist-model.cpp15 bookmarks-folderlist-model.cpp
16 downloads-model.cpp
16 history-domain-model.cpp17 history-domain-model.cpp
17 history-domainlist-model.cpp18 history-domainlist-model.cpp
18 history-lastvisitdatelist-model.cpp19 history-lastvisitdatelist-model.cpp
@@ -74,3 +75,8 @@
7475
75install(FILES "webbrowser-app.url-dispatcher"76install(FILES "webbrowser-app.url-dispatcher"
76 DESTINATION ${CMAKE_INSTALL_DATADIR}/url-dispatcher/urls)77 DESTINATION ${CMAKE_INSTALL_DATADIR}/url-dispatcher/urls)
78
79install(FILES "webbrowser-app-content-hub.json"
80 DESTINATION ${CMAKE_INSTALL_DATADIR}/content-hub/peers
81 RENAME webbrowser-app
82 )
7783
=== added file 'src/app/webbrowser/ContentDownloadDialog.qml'
--- src/app/webbrowser/ContentDownloadDialog.qml 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/ContentDownloadDialog.qml 2015-12-16 16:25:40 +0000
@@ -0,0 +1,159 @@
1/*
2 * Copyright 2014-2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.4
20import Ubuntu.Components 1.3
21import Ubuntu.Components.Popups 1.3
22import Ubuntu.Content 1.3
23import webbrowsercommon.private 0.1
24
25Component {
26 PopupBase {
27 id: downloadDialog
28 objectName: "downloadDialog"
29 anchors.fill: parent
30 property var activeTransfer
31 property string downloadId
32 property var singleDownload
33 property string mimeType
34 property string filename
35 property string icon: MimeDatabase.iconForMimetype(mimeType)
36 property alias contentType: peerPicker.contentType
37
38 signal startDownload(string downloadId, var download, string mimeType)
39
40 Component {
41 id: downloadOptionsComponent
42 Dialog {
43 id: downloadOptionsDialog
44 objectName: "downloadOptionsDialog"
45 Column {
46 spacing: units.gu(2)
47
48 Item {
49 width: parent.width
50 height: mimetypeIcon.height
51
52 Icon {
53 id: mimetypeIcon
54 name: icon != "" ? icon : "save"
55 height: units.gu(4.5)
56 width: height
57 }
58
59 Label {
60 id: filenameLabel
61 anchors.top: mimetypeIcon.top
62 anchors.left: mimetypeIcon.right
63 anchors.leftMargin: units.gu(2)
64 anchors.right: parent.right
65 anchors.rightMargin: units.gu(2)
66 elide: Text.ElideMiddle
67 text: downloadDialog.filename
68 }
69
70 Label {
71 anchors.top: filenameLabel.bottom
72 anchors.left: filenameLabel.left
73 anchors.right: filenameLabel.right
74 elide: Text.ElideRight
75 font.capitalization: Font.Capitalize
76 text: MimeDatabase.nameForMimetype(downloadDialog.mimeType)
77 }
78 }
79
80 Label {
81 width: parent.width
82 text: i18n.tr("Choose an application to open this file or add it to the downloads folder.")
83 wrapMode: Text.Wrap
84 visible: peerModel.peers.length > 0
85 }
86
87 Button {
88 text: i18n.tr("Choose an application")
89 objectName: "chooseAppButton"
90 anchors.horizontalCenter: parent.horizontalCenter
91 width: units.gu(22)
92 height: units.gu(4)
93 visible: peerModel.peers.length > 0
94 onClicked: {
95 PopupUtils.close(downloadOptionsDialog)
96 pickerRect.visible = true
97 }
98 }
99
100 Button {
101 text: i18n.tr("Download")
102 objectName: "downloadFileButton"
103 anchors.horizontalCenter: parent.horizontalCenter
104 width: units.gu(22)
105 height: units.gu(4)
106 onClicked: {
107 startDownload(downloadId, singleDownload, mimeType)
108 PopupUtils.close(downloadDialog)
109 }
110 }
111
112 Button {
113 text: i18n.tr("Cancel")
114 objectName: "cancelDownloadButton"
115 anchors.horizontalCenter: parent.horizontalCenter
116 width: units.gu(22)
117 height: units.gu(4)
118 onClicked: PopupUtils.close(downloadDialog)
119 }
120
121 }
122 }
123
124 }
125
126 ContentPeerModel {
127 id: peerModel
128 handler: ContentHandler.Destination
129 contentType: downloadDialog.contentType
130 }
131
132 Rectangle {
133 id: pickerRect
134 anchors.fill: parent
135 visible: false
136 ContentPeerPicker {
137 id: peerPicker
138 handler: ContentHandler.Destination
139 objectName: "contentPeerPicker"
140 visible: parent.visible
141
142 onPeerSelected: {
143 activeTransfer = peer.request()
144 activeTransfer.downloadId = downloadDialog.downloadId
145 activeTransfer.state = ContentTransfer.Downloading
146 PopupUtils.close(downloadDialog)
147 }
148
149 onCancelPressed: {
150 PopupUtils.close(downloadDialog)
151 }
152 }
153 }
154
155 Component.onCompleted: {
156 PopupUtils.open(downloadOptionsComponent, downloadDialog)
157 }
158 }
159}
0160
=== added file 'src/app/webbrowser/ContentHandler.qml'
--- src/app/webbrowser/ContentHandler.qml 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/ContentHandler.qml 2015-12-16 16:25:40 +0000
@@ -0,0 +1,35 @@
1/*
2 * Copyright 2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.4
20import Ubuntu.Content 1.3
21import "../MimeTypeMapper.js" as MimeTypeMapper
22
23Item {
24 signal exportFromDownloads(var transfer, var mimetypeFilter, bool multiSelect)
25
26 Connections {
27 target: ContentHub
28 onExportRequested: {
29 exportFromDownloads(transfer,
30 MimeTypeMapper.mimeTypeRegexForContentType(transfer.contentType),
31 transfer.selectionType === ContentTransfer.Multiple)
32
33 }
34 }
35}
036
=== added file 'src/app/webbrowser/ContentPickerDialog.qml'
--- src/app/webbrowser/ContentPickerDialog.qml 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/ContentPickerDialog.qml 2015-12-16 16:25:40 +0000
@@ -0,0 +1,110 @@
1/*
2 * Copyright 2014-2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.4
20import Ubuntu.Components 1.3
21import Ubuntu.Components.Popups 1.3 as Popups
22import Ubuntu.Content 1.3
23import com.canonical.Oxide 1.8
24import "../MimeTypeMapper.js" as MimeTypeMapper
25
26Component {
27 Popups.PopupBase {
28 id: picker
29 objectName: "contentPickerDialog"
30
31 // Set the parent at construction time, instead of letting show()
32 // set it later on, which for some reason results in the size of
33 // the dialog not being updated.
34 parent: QuickUtils.rootItem(this)
35
36 property var activeTransfer
37
38 Rectangle {
39 anchors.fill: parent
40
41 ContentTransferHint {
42 anchors.fill: parent
43 activeTransfer: picker.activeTransfer
44 }
45
46 ContentPeerPicker {
47 id: peerPicker
48 anchors.fill: parent
49 visible: true
50 contentType: ContentType.All
51 handler: ContentHandler.Source
52
53 onPeerSelected: {
54 if (peer.appId == "webbrowser-app") {
55 // If we're inside the browser and the user has
56 // requested content from the browser then we
57 // need to handle the transfer internally
58 var downloadPage = picker.WebView.view.showDownloadsPage()
59 downloadPage.mimetypeFilter = MimeTypeMapper.mimeTypeRegexForContentType(contentType)
60 downloadPage.multiSelect = model.allowMultipleFiles
61 downloadPage.selectMode = false
62 downloadPage.pickingMode = true
63 downloadPage.internalFilePicker = model
64 Popups.PopupUtils.close(picker)
65 } else {
66 if (model.allowMultipleFiles) {
67 peer.selectionType = ContentTransfer.Multiple
68 } else {
69 peer.selectionType = ContentTransfer.Single
70 }
71 picker.activeTransfer = peer.request()
72 stateChangeConnection.target = picker.activeTransfer
73 }
74 }
75
76 onCancelPressed: {
77 model.reject()
78 }
79 }
80 }
81
82 Connections {
83 id: stateChangeConnection
84 target: null
85 onStateChanged: {
86 if (picker.activeTransfer.state === ContentTransfer.Charged) {
87 var selectedItems = []
88 for(var i in picker.activeTransfer.items) {
89 selectedItems.push(String(picker.activeTransfer.items[i].url).replace("file://", ""))
90 }
91 model.accept(selectedItems)
92 }
93 }
94 }
95
96 Component.onCompleted: {
97 if(acceptTypes.length === 1) {
98 var contentType = MimeTypeMapper.mimeTypeToContentType(acceptTypes[0])
99 if(contentType == ContentType.Unknown) {
100 // If we don't recognise the type, allow uploads from any app
101 contentType = ContentType.All
102 }
103 peerPicker.contentType = contentType
104 } else {
105 peerPicker.contentType = ContentType.All
106 }
107 show()
108 }
109 }
110}
0111
=== added file 'src/app/webbrowser/DownloadDelegate.qml'
--- src/app/webbrowser/DownloadDelegate.qml 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/DownloadDelegate.qml 2015-12-16 16:25:40 +0000
@@ -0,0 +1,226 @@
1/*
2 * Copyright 2014-2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.0
20import Ubuntu.Components 1.3
21import ".."
22
23ListItem {
24 id: downloadDelegate
25
26 property alias icon: mimeicon.name
27 property alias image: thumbimage.source
28 property alias title: title.text
29 property alias url: url.text
30 property string errorMessage
31 property bool incomplete: false
32 property string downloadId
33 property var download
34 property int progress: download ? download.progress : 0
35 property bool paused
36
37 divider.visible: false
38
39 signal removed()
40 signal cancelled()
41
42 height: visible ? (incomplete ? (paused ? units.gu(13) : units.gu(10)) : units.gu(7)) : 0
43
44 Component.onCompleted: {
45 if (incomplete) {
46 // Connect to download object
47 for(var i = 0; i < downloadManager.downloads.length; i++) {
48 if (downloadManager.downloads[i].downloadId == downloadId) {
49 download = downloadManager.downloads[i]
50 }
51 }
52 }
53 }
54
55 Item {
56
57 anchors {
58 verticalCenter: parent.verticalCenter
59 left: parent.left
60 leftMargin: units.gu(2)
61 right: parent.right
62 }
63
64 Item {
65 id: iconContainer
66 width: units.gu(3)
67 height: width
68 anchors.verticalCenter: parent.verticalCenter
69 anchors.verticalCenterOffset: downloadDelegate.incomplete ? -units.gu(1) : 0
70
71 Image {
72 id: thumbimage
73 asynchronous: true
74 width: parent.width
75 height: parent.height
76 fillMode: Image.PreserveAspectFit
77 sourceSize.width: parent.width
78 sourceSize.height: parent.height
79 anchors.verticalCenter: parent.verticalCenter
80 }
81
82 Image {
83 id: mimeicon
84 asynchronous: true
85 anchors.fill: parent
86 anchors.margins: units.gu(0.2)
87 source: "image://theme/%1".arg(name != "" ? name : "save")
88 visible: thumbimage.status !== Image.Ready
89 cache: true
90 property string name
91 }
92 }
93
94 Item {
95 anchors.top: iconContainer.top
96 anchors.left: iconContainer.right
97 anchors.leftMargin: units.gu(2)
98 anchors.right: parent.right
99
100 Column {
101 id: detailsColumn
102 width: parent.width - cancelColumn.width
103 height: parent.height
104
105 Label {
106 id: title
107 fontSize: "x-small"
108 color: "#5d5d5d"
109 elide: Text.ElideRight
110 width: parent.width
111 }
112
113 Label {
114 id: url
115 fontSize: "x-small"
116 color: "#5d5d5d"
117 elide: Text.ElideRight
118 width: parent.width
119 }
120
121 Item {
122 height: error.visible ? units.gu(1) : units.gu(2)
123 width: parent.width
124 visible: downloadDelegate.incomplete
125 }
126
127 Item {
128 id: error
129 visible: incomplete && download === undefined || errorMessage !== ""
130 height: units.gu(3)
131 width: parent.width
132
133 Icon {
134 id: errorIcon
135 width: units.gu(2)
136 height: width
137 anchors.verticalCenter: parent.verticalCenter
138 name: "dialog-warning-symbolic"
139 color: UbuntuColors.red
140 }
141
142 Label {
143 width: parent.width - errorIcon.width
144 anchors.left: errorIcon.right
145 anchors.leftMargin: units.gu(1)
146 anchors.verticalCenter: errorIcon.verticalCenter
147 fontSize: "x-small"
148 color: UbuntuColors.red
149 text: errorMessage !== "" ? errorMessage
150 : (incomplete && download === undefined) ? i18n.tr("Download failed")
151 : ""
152 elide: Text.ElideRight
153 }
154 }
155
156 IndeterminateProgressBar {
157 id: progressBar
158 width: parent.width
159 height: units.gu(0.5)
160 visible: downloadDelegate.incomplete && !error.visible
161 progress: downloadDelegate.progress
162 // Work around UDM bug #1450144
163 indeterminateProgress: downloadDelegate.progress < 0 || downloadDelegate.progress > 100
164 }
165 }
166
167 Column {
168 id: cancelColumn
169 spacing: units.gu(1)
170 anchors.top: detailsColumn.top
171 anchors.left: detailsColumn.right
172 anchors.leftMargin: units.gu(2)
173 width: downloadDelegate.incomplete && !error.visible ? cancelButton.width + units.gu(2) : 0
174
175 Button {
176 visible: downloadDelegate.incomplete && !error.visible
177 id: cancelButton
178 text: i18n.tr("Cancel")
179 onClicked: {
180 if (download) {
181 download.cancel()
182 cancelled()
183 }
184 }
185 }
186
187 Label {
188 visible: !progressBar.indeterminateProgress && downloadDelegate.incomplete
189 && !error.visible
190 && !downloadDelegate.paused
191 width: cancelButton.width
192 horizontalAlignment: Text.AlignHCenter
193 fontSize: "x-small"
194 text: progressBar.progress + "%"
195 }
196
197 Button {
198 visible: downloadDelegate.paused
199 text: i18n.tr("Resume")
200 width: cancelButton.width
201 onClicked: {
202 if (download) {
203 download.resume()
204 }
205 }
206 }
207 }
208
209 }
210 }
211
212 leadingActions: error.visible || !downloadDelegate.incomplete ? deleteActionList : null
213
214 ListItemActions {
215 id: deleteActionList
216 actions: [
217 Action {
218 objectName: "leadingAction.delete"
219 iconName: "delete"
220 enabled: error.visible || !downloadDelegate.incomplete
221 onTriggered: error.visible ? downloadDelegate.cancelled()
222 : downloadDelegate.removed()
223 }
224 ]
225 }
226}
0227
=== added file 'src/app/webbrowser/DownloadHandler.qml'
--- src/app/webbrowser/DownloadHandler.qml 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/DownloadHandler.qml 2015-12-16 16:25:40 +0000
@@ -0,0 +1,45 @@
1/*
2 * Copyright 2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.4
20import Ubuntu.DownloadManager 1.2
21
22DownloadManager {
23 id: downloadManager
24
25 onDownloadFinished: {
26 downloadsModel.moveToDownloads(download.downloadId, path)
27 downloadsModel.setComplete(download.downloadId, true)
28 }
29
30 onDownloadPaused: {
31 downloadsModel.pauseDownload(download.downloadId)
32 }
33
34 onDownloadResumed: {
35 downloadsModel.resumeDownload(download.downloadId)
36 }
37
38 onDownloadCanceled: {
39 downloadsModel.cancelDownload(download.downloadId)
40 }
41
42 onErrorFound: {
43 downloadsModel.setError(download.downloadId, download.errorMessage)
44 }
45}
046
=== added file 'src/app/webbrowser/DownloadsModel.qml'
--- src/app/webbrowser/DownloadsModel.qml 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/DownloadsModel.qml 2015-12-16 16:25:40 +0000
@@ -0,0 +1,24 @@
1/*
2 * Copyright 2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.0
20import webbrowserapp.private 0.1
21
22DownloadsModel {
23 databasePath: dataLocation + "/downloads.sqlite"
24}
025
=== added file 'src/app/webbrowser/DownloadsPage.qml'
--- src/app/webbrowser/DownloadsPage.qml 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/DownloadsPage.qml 2015-12-16 16:25:40 +0000
@@ -0,0 +1,253 @@
1/*
2 * Copyright 2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.0
20import Qt.labs.settings 1.0
21import Ubuntu.Components 1.3
22import Ubuntu.Components.Popups 1.0
23import Ubuntu.Thumbnailer 0.1
24import Ubuntu.Content 1.3
25import Ubuntu.Web 0.2
26import webbrowserapp.private 0.1
27import webbrowsercommon.private 0.1
28
29import "../MimeTypeMapper.js" as MimeTypeMapper
30
31Item {
32 id: downloadsItem
33
34 property QtObject downloadsModel
35
36 // We can get file picking requests either via content-hub (activeTransfer)
37 // Or via the internal oxide file picker (internalFilePicker) in the case
38 // where the user wishes to upload a file from their previous downloads.
39 property var activeTransfer
40 property var internalFilePicker
41
42 property bool selectMode
43 property bool pickingMode
44 property bool multiSelect
45 property alias mimetypeFilter: downloadModelFilter.pattern
46
47 signal done()
48
49 Rectangle {
50 anchors.fill: parent
51 color: "#fbfbfb"
52 }
53
54 BrowserPageHeader {
55 id: title
56 text: i18n.tr("Downloads")
57 color: "#f7f7f7"
58 actions: [
59 Action {
60 text: i18n.tr("Confirm selection")
61 iconName: "tick"
62 visible: pickingMode
63 enabled: downloadsListView.ViewItems.selectedIndices.length > 0
64 onTriggered: {
65 var results = []
66 if (internalFilePicker) {
67 for (var i = 0; i < downloadsListView.ViewItems.selectedIndices.length; i++) {
68 var selectedDownload = downloadsListView.model.get(downloadsListView.ViewItems.selectedIndices[i])
69 results.push(selectedDownload.path)
70 }
71 internalFilePicker.accept(results)
72 } else {
73 for (var i = 0; i < downloadsListView.ViewItems.selectedIndices.length; i++) {
74 var selectedDownload = downloadsListView.model.get(downloadsListView.ViewItems.selectedIndices[i])
75 results.push(resultComponent.createObject(downloadsItem, {"url": "file://" + selectedDownload.path}))
76 }
77 activeTransfer.items = results
78 activeTransfer.state = ContentTransfer.Charged
79 }
80 downloadsItem.done()
81 }
82 },
83 Action {
84 text: i18n.tr("Select all")
85 iconName: "select"
86 visible: selectMode
87 onTriggered: {
88 if (downloadsListView.ViewItems.selectedIndices.length === downloadsListView.count) {
89 downloadsListView.ViewItems.selectedIndices = []
90 } else {
91 var indices = []
92 for (var i = 0; i < downloadsListView.count; ++i) {
93 indices.push(i)
94 }
95 downloadsListView.ViewItems.selectedIndices = indices
96 }
97 }
98 },
99 Action {
100 text: i18n.tr("Delete")
101 iconName: "delete"
102 visible: selectMode
103 onTriggered: {
104 var toDelete = []
105 for (var i = 0; i < downloadsListView.ViewItems.selectedIndices.length; i++) {
106 var selectedDownload = downloadsListView.model.get(downloadsListView.ViewItems.selectedIndices[i])
107 toDelete.push(selectedDownload.path)
108 }
109 for (var i = 0; i < toDelete.length; i++) {
110 downloadsModel.deleteDownload(toDelete[i])
111 }
112 downloadsListView.ViewItems.selectedIndices = []
113 downloadsItem.selectMode = false
114 }
115 },
116 Action {
117 iconName: "edit"
118 visible: !selectMode && !pickingMode
119 onTriggered: {
120 selectMode = true
121 multiSelect = true
122 }
123 }
124 ]
125 onBack: {
126 if (selectMode) {
127 selectMode = false
128 } else {
129 if (activeTransfer) {
130 activeTransfer.state = ContentTransfer.Aborted
131 }
132 if (internalFilePicker) {
133 internalFilePicker.reject()
134 }
135 downloadsItem.done()
136 }
137 }
138 }
139
140 Component {
141 id: resultComponent
142 ContentItem { }
143 }
144
145 ListView {
146 id: downloadsListView
147 clip: true
148
149 anchors {
150 top: title.bottom
151 left: parent.left
152 right: parent.right
153 bottom: parent.bottom
154 rightMargin: units.gu(2)
155 }
156
157 model: SortFilterModel {
158 model: downloadsModel
159 filter {
160 id: downloadModelFilter
161 property: "mimetype"
162 }
163 }
164
165 delegate: DownloadDelegate {
166 downloadId: model.downloadId
167 title: model.filename ? model.filename : model.url.toString().split('/').pop().split('?').shift()
168 url: model.url
169 image: model.complete && (model.mimetype.indexOf("image") === 0 || model.mimetype.indexOf("video") === 0) ? "image://thumbnailer/file://" + model.path : ""
170 icon: MimeDatabase.iconForMimetype(model.mimetype)
171 incomplete: !model.complete
172 selectMode: downloadsItem.selectMode || downloadsItem.pickingMode
173 visible: !(selectMode && incomplete)
174 errorMessage: model.error
175 paused: model.paused
176 // Work around bug #1493880
177 property bool lastSelected
178
179 onSelectedChanged: {
180 if (!multiSelect && selected && lastSelected != selected) {
181 downloadsListView.ViewItems.selectedIndices = [index]
182 }
183 lastSelected = selected
184 }
185
186 onClicked: {
187 if (model.complete) {
188 if (selectMode) {
189 selected = !selected
190 } else {
191 exportPeerPicker.contentType = MimeTypeMapper.mimeTypeToContentType(model.mimetype)
192 exportPeerPicker.visible = true
193 exportPeerPicker.path = model.path
194 }
195 }
196 }
197
198 onPressAndHold: {
199 downloadsItem.selectMode = true
200 downloadsItem.multiSelect = true
201 if (downloadsItem.selectMode) {
202 downloadsListView.ViewItems.selectedIndices = [index]
203 }
204 }
205
206 onRemoved: {
207 if (model.complete) {
208 downloadsModel.deleteDownload(model.path)
209 }
210 }
211
212 onCancelled: {
213 downloadsModel.cancelDownload(model.downloadId)
214 }
215 }
216
217 }
218
219 Label {
220 id: emptyLabel
221 anchors.centerIn: parent
222 visible: downloadsListView.count == 0
223 wrapMode: Text.Wrap
224 width: parent.width
225 horizontalAlignment: Text.AlignHCenter
226 text: i18n.tr("No downloads available")
227 }
228
229 Component {
230 id: contentItemComponent
231 ContentItem {}
232 }
233
234 ContentPeerPicker {
235 id: exportPeerPicker
236 visible: false
237 anchors.fill: parent
238 handler: ContentHandler.Destination
239 property string path
240 onPeerSelected: {
241 var transfer = peer.request()
242 if (transfer.state === ContentTransfer.InProgress) {
243 transfer.items = [contentItemComponent.createObject(downloadsItem, {"url": path})]
244 transfer.state = ContentTransfer.Charged
245 }
246 visible = false
247 }
248 onCancelPressed: {
249 visible = false
250 }
251 }
252
253}
0254
=== added file 'src/app/webbrowser/IndeterminateProgressBar.qml'
--- src/app/webbrowser/IndeterminateProgressBar.qml 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/IndeterminateProgressBar.qml 2015-12-16 16:25:40 +0000
@@ -0,0 +1,51 @@
1/*
2 * Copyright 2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.4
20import Ubuntu.Components 1.3
21
22Rectangle {
23 id: progressBar
24
25 property real progress
26 property bool indeterminateProgress: false
27
28 radius: width/3
29 color: Theme.palette.normal.base
30
31 Rectangle {
32 id: currentProgress
33 height: parent.height
34 radius: parent.radius
35 anchors.left: parent.left
36 anchors.leftMargin: 0
37 anchors.top: parent.top
38 color: UbuntuColors.orange
39 width: indeterminateProgress ? parent.width / 6 : (progress / 100) * parent.width
40
41 SequentialAnimation {
42 running: indeterminateProgress
43 onRunningChanged: {
44 currentProgress.anchors.leftMargin = 0;
45 }
46 loops: Animation.Infinite
47 PropertyAnimation { target: currentProgress.anchors; property: "leftMargin"; from: 0.0; to: parent.width - parent.width / 6; duration: UbuntuAnimation.SleepyDuration; easing.type: Easing.InOutQuad; }
48 PropertyAnimation { target: currentProgress.anchors; property: "leftMargin"; from: parent.width - parent.width / 6; to: 0; duration: UbuntuAnimation.SleepyDuration; easing.type: Easing.InOutQuad; }
49 }
50 }
51}
052
=== modified file 'src/app/webbrowser/SettingsPage.qml'
--- src/app/webbrowser/SettingsPage.qml 2015-11-17 16:25:30 +0000
+++ src/app/webbrowser/SettingsPage.qml 2015-12-16 16:25:40 +0000
@@ -43,7 +43,7 @@
43 searchPaths: searchEnginesSearchPaths43 searchPaths: searchEnginesSearchPaths
44 }44 }
4545
46 SettingsPageHeader {46 BrowserPageHeader {
47 id: title47 id: title
4848
49 onBack: settingsItem.done()49 onBack: settingsItem.done()
@@ -168,7 +168,7 @@
168 color: "#f6f6f6"168 color: "#f6f6f6"
169 }169 }
170170
171 SettingsPageHeader {171 BrowserPageHeader {
172 id: searchEngineTitle172 id: searchEngineTitle
173173
174 onBack: searchEngineItem.destroy()174 onBack: searchEngineItem.destroy()
@@ -221,7 +221,7 @@
221 color: "#f6f6f6"221 color: "#f6f6f6"
222 }222 }
223223
224 SettingsPageHeader {224 BrowserPageHeader {
225 id: privacyTitle225 id: privacyTitle
226 onBack: privacyItem.destroy()226 onBack: privacyItem.destroy()
227 text: i18n.tr("Privacy & permissions")227 text: i18n.tr("Privacy & permissions")
@@ -379,7 +379,7 @@
379 color: "#f6f6f6"379 color: "#f6f6f6"
380 }380 }
381381
382 SettingsPageHeader {382 BrowserPageHeader {
383 id: mediaAccessTitle383 id: mediaAccessTitle
384384
385 onBack: mediaAccessItem.destroy()385 onBack: mediaAccessItem.destroy()
386386
=== added file 'src/app/webbrowser/downloads-model.cpp'
--- src/app/webbrowser/downloads-model.cpp 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/downloads-model.cpp 2015-12-16 16:25:40 +0000
@@ -0,0 +1,412 @@
1/*
2 * Copyright 2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19#include "downloads-model.h"
20
21#include <QtCore/QDebug>
22#include <QtCore/QDir>
23#include <QtSql/QSqlQuery>
24#include <QtCore/QFile>
25#include <QtCore/QFileInfo>
26#include <QtCore/QStandardPaths>
27#include <QtCore/QMimeDatabase>
28#include <QtCore/QMimeType>
29
30#define CONNECTION_NAME "webbrowser-app-downloads"
31
32/*!
33 \class DownloadsModel
34 \brief List model that stores information about downloaded files.
35
36 DownloadsModel is a list model that stores information about files that
37 have been downloaded by the browser and stored permanently
38 (e.g. in ~/Downloads), as opposed to those that were sent directly to
39 another application after download. For each download the original URL, the
40 path to the downloaded file, the file mimetype and the download time are
41 stored. The model is sorted chronologically to display the most recent
42 download first.
43
44 The information is persistently stored on disk in a SQLite database.
45 The database is read at startup to populate the model, and whenever a new
46 entry is added to the model or an entry is removed from the model
47 the database is updated. Removing a download from the model also results
48 in it being deleted from the disk.
49 The model doesn’t monitor the database for external changes, but does check
50 that downloaded files still exist when first populating.
51*/
52DownloadsModel::DownloadsModel(QObject* parent)
53 : QAbstractListModel(parent)
54 , m_numRows(0)
55 , m_fetchedCount(0)
56 , m_canFetchMore(true)
57{
58 m_database = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), CONNECTION_NAME);
59}
60
61DownloadsModel::~DownloadsModel()
62{
63 m_database.close();
64 m_database = QSqlDatabase();
65 QSqlDatabase::removeDatabase(CONNECTION_NAME);
66}
67
68void DownloadsModel::resetDatabase(const QString& databaseName)
69{
70 beginResetModel();
71 m_orderedEntries.clear();
72 m_database.close();
73 m_database.setDatabaseName(databaseName);
74 m_database.open();
75 m_numRows = 0;
76 m_fetchedCount = 0;
77 m_canFetchMore = true;
78 createOrAlterDatabaseSchema();
79 endResetModel();
80 Q_EMIT rowCountChanged();
81}
82
83void DownloadsModel::createOrAlterDatabaseSchema()
84{
85 QSqlQuery createQuery(m_database);
86 QString query = QLatin1String("CREATE TABLE IF NOT EXISTS downloads "
87 "(downloadId VARCHAR, url VARCHAR, path VARCHAR, "
88 "mimetype VARCHAR, complete BOOL, paused BOOL, "
89 "error VARCHAR, created DATETIME DEFAULT "
90 "CURRENT_TIMESTAMP);");
91 createQuery.prepare(query);
92 createQuery.exec();
93}
94
95void DownloadsModel::fetchMore(const QModelIndex &parent)
96{
97 QSqlQuery populateQuery(m_database);
98 QString query = QLatin1String("SELECT downloadId, url, path, mimetype, "
99 "complete, error, created, paused "
100 "FROM downloads ORDER BY created DESC LIMIT 100 OFFSET ?;");
101 populateQuery.prepare(query);
102 populateQuery.addBindValue(m_fetchedCount);
103 populateQuery.exec();
104 int count = 0; // size() isn't supported on the sqlite backend
105 while (populateQuery.next()) {
106 DownloadEntry entry;
107 entry.downloadId = populateQuery.value(0).toString();
108 entry.url = populateQuery.value(1).toUrl();
109 entry.path = populateQuery.value(2).toString();
110 entry.mimetype = populateQuery.value(3).toString();
111 entry.complete = populateQuery.value(4).toBool();
112 entry.error = populateQuery.value(5).toString();
113 entry.created = QDateTime::fromTime_t(populateQuery.value(6).toInt());
114 entry.paused = populateQuery.value(7).toBool();
115 QFileInfo fileInfo(entry.path);
116 if (fileInfo.exists()) {
117 entry.filename = fileInfo.fileName();
118 }
119
120 // Only list a completed entry if its file exists, however we don't
121 // remove the entry if the file is missing as it may be stored on a
122 // removable medium like an SD card in the future, so could reappear.
123 if (!entry.complete || fileInfo.exists()) {
124 beginInsertRows(QModelIndex(), m_numRows, m_numRows);
125 m_orderedEntries.append(entry);
126 endInsertRows();
127 m_numRows++;
128 }
129 count++;
130 }
131 m_fetchedCount += count;
132 if (count == 0) {
133 m_canFetchMore = false;
134 }
135}
136
137QHash<int, QByteArray> DownloadsModel::roleNames() const
138{
139 static QHash<int, QByteArray> roles;
140 if (roles.isEmpty()) {
141 roles[DownloadId] = "downloadId";
142 roles[Url] = "url";
143 roles[Path] = "path";
144 roles[Filename] = "filename";
145 roles[Mimetype] = "mimetype";
146 roles[Complete] = "complete";
147 roles[Paused] = "paused";
148 roles[Error] = "error";
149 roles[Created] = "created";
150 }
151 return roles;
152}
153
154int DownloadsModel::rowCount(const QModelIndex& parent) const
155{
156 Q_UNUSED(parent);
157 return m_orderedEntries.count();
158}
159
160QVariant DownloadsModel::data(const QModelIndex& index, int role) const
161{
162 if (!index.isValid()) {
163 return QVariant();
164 }
165 const DownloadEntry& entry = m_orderedEntries.at(index.row());
166 switch (role) {
167 case DownloadId:
168 return entry.downloadId;
169 case Url:
170 return entry.url;
171 case Path:
172 return entry.path;
173 case Filename:
174 return entry.filename;
175 case Mimetype:
176 return entry.mimetype;
177 case Complete:
178 return entry.complete;
179 case Paused:
180 return entry.paused;
181 case Error:
182 return entry.error;
183 case Created:
184 return entry.created;
185 default:
186 return QVariant();
187 }
188}
189
190const QString DownloadsModel::databasePath() const
191{
192 return m_database.databaseName();
193}
194
195void DownloadsModel::setDatabasePath(const QString& path)
196{
197 if (path != databasePath()) {
198 if (path.isEmpty()) {
199 resetDatabase(":memory:");
200 } else {
201 resetDatabase(path);
202 }
203 Q_EMIT databasePathChanged();
204 }
205}
206
207/*!
208 Add a download to the database. This should happen as soon as the download
209 is started.
210*/
211void DownloadsModel::add(const QString& downloadId, const QUrl& url, const QString& mimetype)
212{
213 beginInsertRows(QModelIndex(), 0, 0);
214 DownloadEntry entry;
215 entry.downloadId = downloadId;
216 entry.complete = false;
217 entry.paused = false;
218 entry.url = url;
219 entry.mimetype = mimetype;
220 m_orderedEntries.prepend(entry);
221 m_numRows++;
222 m_fetchedCount++;
223 endInsertRows();
224 Q_EMIT added(downloadId, url, mimetype);
225 insertNewEntryInDatabase(entry);
226 Q_EMIT rowCountChanged();
227}
228
229void DownloadsModel::setPath(const QString& downloadId, const QString& path)
230{
231 QSqlQuery query(m_database);
232
233 // Override reported mimetype from server with detected mimetype from file once downloaded
234 QMimeDatabase mimeDatabase;
235 QString mimetype = mimeDatabase.mimeTypeForFile(path).name();
236
237 static QString updateStatement = QLatin1String("UPDATE downloads SET mimetype = ?, "
238 "path = ? WHERE downloadId = ?");
239 query.prepare(updateStatement);
240 query.addBindValue(mimetype);
241 query.addBindValue(path);
242 query.addBindValue(downloadId);
243 query.exec();
244 Q_EMIT pathChanged(downloadId, path);
245}
246
247void DownloadsModel::setComplete(const QString& downloadId, const bool complete)
248{
249 QSqlQuery query(m_database);
250 static QString updateStatement = QLatin1String("UPDATE downloads SET complete = ? "
251 "WHERE downloadId = ?");
252 query.prepare(updateStatement);
253 query.addBindValue(complete);
254 query.addBindValue(downloadId);
255 query.exec();
256 Q_EMIT completeChanged(downloadId, complete);
257 reload();
258}
259
260void DownloadsModel::setError(const QString& downloadId, const QString& error)
261{
262 QSqlQuery query(m_database);
263 static QString updateStatement = QLatin1String("UPDATE downloads SET error = ? "
264 "WHERE downloadId = ?");
265 query.prepare(updateStatement);
266 query.addBindValue(error);
267 query.addBindValue(downloadId);
268 query.exec();
269 Q_EMIT errorChanged(downloadId, error);
270 reload();
271}
272
273void DownloadsModel::moveToDownloads(const QString& downloadId, const QString& path)
274{
275 QFile file(path);
276 if (file.exists()) {
277 QFileInfo fi(path);
278 QString suffix = fi.completeSuffix();
279 QString filename = fi.fileName();
280 QString filenameWithoutSuffix = filename.left(filename.size() - suffix.size());
281 QString dir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
282 QString destination = dir + QDir::separator() + filenameWithoutSuffix + suffix;
283 // Avoid filename collision by automatically inserting an incremented
284 // number into the filename if the original name already exists.
285 if (QFile::exists(destination)) {
286 int append = 1;
287 do {
288 destination = QString("%1%2.%3").arg(dir + QDir::separator() + filenameWithoutSuffix, QString::number(append), suffix);
289 append++;
290 } while (QFile::exists(destination));
291 }
292 if (file.rename(destination)) {
293 setPath(downloadId, destination);
294 } else {
295 qWarning() << "Failed moving file from " << path << " to " << destination;
296 }
297 } else {
298 qWarning() << "Download not found: " << path;
299 }
300}
301
302void DownloadsModel::insertNewEntryInDatabase(const DownloadEntry& entry)
303{
304 QSqlQuery query(m_database);
305 static QString insertStatement = QLatin1String("INSERT INTO downloads (downloadId, url, "
306 "mimetype) "
307 "VALUES (?, ?, ?);");
308 query.prepare(insertStatement);
309 query.addBindValue(entry.downloadId);
310 query.addBindValue(entry.url);
311 query.addBindValue(entry.mimetype);
312 query.exec();
313}
314
315/*!
316 Remove a downloaded file from the list of downloads and
317 delete the file.
318*/
319void DownloadsModel::deleteDownload(const QString& path)
320{
321 int index = 0;
322 Q_FOREACH(DownloadEntry entry, m_orderedEntries) {
323 if (entry.path == path) {
324 beginRemoveRows(QModelIndex(), index, index);
325 m_orderedEntries.removeAt(index);
326 endRemoveRows();
327 Q_EMIT deleted(path);
328 removeExistingEntryFromDatabase(path);
329 m_fetchedCount--;
330 m_numRows--;
331 Q_EMIT rowCountChanged();
332 QFile::remove(path);
333 return;
334 } else {
335 index++;
336 }
337 }
338}
339
340/*!
341 Remove a cancelled download from the model and the database.
342*/
343void DownloadsModel::cancelDownload(const QString& downloadId)
344{
345 int index=0;
346 Q_FOREACH(DownloadEntry entry, m_orderedEntries) {
347 if (entry.downloadId == downloadId) {
348 beginRemoveRows(QModelIndex(), index, index);
349 m_orderedEntries.removeAt(index);
350 QSqlQuery query(m_database);
351 static QString deleteStatement = QLatin1String("DELETE FROM downloads WHERE downloadId=?;");
352 query.prepare(deleteStatement);
353 query.addBindValue(downloadId);
354 query.exec();
355 endRemoveRows();
356 m_fetchedCount--;
357 m_numRows--;
358 Q_EMIT rowCountChanged();
359 return;
360 } else {
361 index++;
362 }
363 }
364}
365
366void DownloadsModel::pauseDownload(const QString& downloadId)
367{
368 QSqlQuery query(m_database);
369 static QString pauseStatement = QLatin1String("UPDATE downloads SET paused=1 WHERE downloadId=?;");
370 query.prepare(pauseStatement);
371 query.addBindValue(downloadId);
372 query.exec();
373 reload();
374}
375
376void DownloadsModel::resumeDownload(const QString& downloadId)
377{
378 QSqlQuery query(m_database);
379 static QString resumeStatement = QLatin1String("UPDATE downloads SET paused=0 WHERE downloadId=?;");
380 query.prepare(resumeStatement);
381 query.addBindValue(downloadId);
382 query.exec();
383 reload();
384}
385
386void DownloadsModel::removeExistingEntryFromDatabase(const QString& path)
387{
388 QSqlQuery query(m_database);
389 static QString deleteStatement = QLatin1String("DELETE FROM downloads WHERE path=?;");
390 query.prepare(deleteStatement);
391 query.addBindValue(path);
392 query.exec();
393}
394
395bool DownloadsModel::canFetchMore(const QModelIndex &parent) const
396{
397 Q_UNUSED(parent)
398
399 return m_canFetchMore;
400}
401
402void DownloadsModel::reload()
403{
404 beginResetModel();
405 m_orderedEntries.clear();
406 m_canFetchMore = true;
407 m_fetchedCount = 0;
408 m_numRows = 0;
409 endResetModel();
410 fetchMore();
411 Q_EMIT rowCountChanged();
412}
0413
=== added file 'src/app/webbrowser/downloads-model.h'
--- src/app/webbrowser/downloads-model.h 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/downloads-model.h 2015-12-16 16:25:40 +0000
@@ -0,0 +1,110 @@
1/*
2 * Copyright 2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19#ifndef __DOWNLOADS_MODEL_H__
20#define __DOWNLOADS_MODEL_H__
21
22#include <QtCore/QAbstractListModel>
23#include <QtCore/QDateTime>
24#include <QtCore/QList>
25#include <QtCore/QSet>
26#include <QtCore/QString>
27#include <QtCore/QUrl>
28#include <QtSql/QSqlDatabase>
29
30class DownloadsModel : public QAbstractListModel
31{
32 Q_OBJECT
33
34 Q_PROPERTY(QString databasePath READ databasePath WRITE setDatabasePath NOTIFY databasePathChanged)
35 Q_PROPERTY(int count READ rowCount NOTIFY rowCountChanged)
36
37 Q_ENUMS(Roles)
38
39public:
40 DownloadsModel(QObject* parent=0);
41 ~DownloadsModel();
42
43 enum Roles {
44 DownloadId = Qt::UserRole + 1,
45 Url,
46 Path,
47 Filename,
48 Mimetype,
49 Complete,
50 Paused,
51 Error,
52 Created
53 };
54
55 // reimplemented from QAbstractListModel
56 QHash<int, QByteArray> roleNames() const;
57 int rowCount(const QModelIndex& parent=QModelIndex()) const;
58 QVariant data(const QModelIndex& index, int role) const;
59 bool canFetchMore(const QModelIndex &parent = QModelIndex()) const;
60 void fetchMore(const QModelIndex &parent = QModelIndex());
61
62 const QString databasePath() const;
63 void setDatabasePath(const QString& path);
64
65 Q_INVOKABLE void add(const QString &downloadId, const QUrl& url, const QString& mimetype);
66 Q_INVOKABLE void moveToDownloads(const QString& downloadId, const QString& path);
67 Q_INVOKABLE void setPath(const QString& downloadId, const QString& path);
68 Q_INVOKABLE void setComplete(const QString& downloadId, const bool complete);
69 Q_INVOKABLE void setError(const QString& downloadId, const QString& error);
70 Q_INVOKABLE void deleteDownload(const QString& path);
71 Q_INVOKABLE void cancelDownload(const QString& downloadId);
72 Q_INVOKABLE void pauseDownload(const QString& downloadId);
73 Q_INVOKABLE void resumeDownload(const QString& downloadId);
74
75Q_SIGNALS:
76 void databasePathChanged() const;
77 void added(const QString& downloadId, const QUrl& url, const QString& mimetype) const;
78 void pathChanged(const QString& downloadId, const QString& path) const;
79 void completeChanged(const QString& downloadId, const bool complete) const;
80 void errorChanged(const QString& downloadId, const QString& error) const;
81 void deleted(const QString& path) const;
82 void rowCountChanged();
83
84private:
85 QSqlDatabase m_database;
86 int m_numRows;
87 int m_fetchedCount;
88 bool m_canFetchMore;
89
90 struct DownloadEntry {
91 QString downloadId;
92 QUrl url;
93 QString path;
94 QString filename;
95 QString mimetype;
96 bool complete;
97 bool paused;
98 QString error;
99 QDateTime created;
100 };
101 QList<DownloadEntry> m_orderedEntries;
102
103 void resetDatabase(const QString& databaseName);
104 void createOrAlterDatabaseSchema();
105 void insertNewEntryInDatabase(const DownloadEntry& entry);
106 void removeExistingEntryFromDatabase(const QString& path);
107 void reload();
108};
109
110#endif // __DOWNLOADS_MODEL_H__
0111
=== added file 'src/app/webbrowser/webbrowser-app-content-hub.json'
--- src/app/webbrowser/webbrowser-app-content-hub.json 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/webbrowser-app-content-hub.json 2015-12-16 16:25:40 +0000
@@ -0,0 +1,5 @@
1{
2 "source": [
3 "all"
4 ]
5}
06
=== modified file 'src/app/webbrowser/webbrowser-app.cpp'
--- src/app/webbrowser/webbrowser-app.cpp 2015-11-23 09:41:48 +0000
+++ src/app/webbrowser/webbrowser-app.cpp 2015-12-16 16:25:40 +0000
@@ -20,6 +20,7 @@
20#include "bookmarks-folderlist-model.h"20#include "bookmarks-folderlist-model.h"
21#include "cache-deleter.h"21#include "cache-deleter.h"
22#include "config.h"22#include "config.h"
23#include "downloads-model.h"
23#include "file-operations.h"24#include "file-operations.h"
24#include "history-domainlist-model.h"25#include "history-domainlist-model.h"
25#include "history-lastvisitdatelist-model.h"26#include "history-lastvisitdatelist-model.h"
@@ -87,6 +88,7 @@
87 qmlRegisterSingletonType<FileOperations>(uri, 0, 1, "FileOperations", FileOperations_singleton_factory);88 qmlRegisterSingletonType<FileOperations>(uri, 0, 1, "FileOperations", FileOperations_singleton_factory);
88 qmlRegisterType<SearchEngine>(uri, 0, 1, "SearchEngine");89 qmlRegisterType<SearchEngine>(uri, 0, 1, "SearchEngine");
89 qmlRegisterSingletonType<CacheDeleter>(uri, 0, 1, "CacheDeleter", CacheDeleter_singleton_factory);90 qmlRegisterSingletonType<CacheDeleter>(uri, 0, 1, "CacheDeleter", CacheDeleter_singleton_factory);
91 qmlRegisterType<DownloadsModel>(uri, 0, 1, "DownloadsModel");
90 qmlRegisterType<TextSearchFilterModel>(uri, 0, 1, "TextSearchFilterModel");92 qmlRegisterType<TextSearchFilterModel>(uri, 0, 1, "TextSearchFilterModel");
9193
92 if (BrowserApplication::initialize("webbrowser/webbrowser-app.qml")) {94 if (BrowserApplication::initialize("webbrowser/webbrowser-app.qml")) {
9395
=== added file 'src/app/webcontainer/ContentDownloadDialog.qml'
--- src/app/webcontainer/ContentDownloadDialog.qml 1970-01-01 00:00:00 +0000
+++ src/app/webcontainer/ContentDownloadDialog.qml 2015-12-16 16:25:40 +0000
@@ -0,0 +1,67 @@
1/*
2 * Copyright 2014-2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.4
20import Ubuntu.Components 1.3
21import Ubuntu.Components.Popups 1.3
22import Ubuntu.Content 1.3
23import webbrowsercommon.private 0.1
24
25Component {
26 PopupBase {
27 id: downloadDialog
28 objectName: "downloadDialog"
29 anchors.fill: parent
30 property var activeTransfer
31 property var downloadId
32 property var singleDownload
33 property var mimeType
34 property var filename
35 property var icon: MimeDatabase.iconForMimetype(mimeType)
36 property alias contentType: peerPicker.contentType
37
38 ContentPeerModel {
39 id: peerModel
40 handler: ContentHandler.Destination
41 contentType: downloadDialog.contentType
42 }
43
44 Rectangle {
45 id: pickerRect
46 anchors.fill: parent
47 visible: true
48 ContentPeerPicker {
49 id: peerPicker
50 handler: ContentHandler.Destination
51 objectName: "contentPeerPicker"
52 visible: parent.visible
53
54 onPeerSelected: {
55 activeTransfer = peer.request()
56 activeTransfer.downloadId = downloadDialog.downloadId
57 activeTransfer.state = ContentTransfer.Downloading
58 PopupUtils.close(downloadDialog)
59 }
60
61 onCancelPressed: {
62 PopupUtils.close(downloadDialog)
63 }
64 }
65 }
66 }
67}
068
=== added file 'src/app/webcontainer/ContentPickerDialog.qml'
--- src/app/webcontainer/ContentPickerDialog.qml 1970-01-01 00:00:00 +0000
+++ src/app/webcontainer/ContentPickerDialog.qml 2015-12-16 16:25:40 +0000
@@ -0,0 +1,96 @@
1/*
2 * Copyright 2014-2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19import QtQuick 2.4
20import Ubuntu.Components 1.3
21import Ubuntu.Components.Popups 1.3 as Popups
22import Ubuntu.Content 1.3
23import "../MimeTypeMapper.js" as MimeTypeMapper
24
25Component {
26 Popups.PopupBase {
27 id: picker
28 objectName: "contentPickerDialog"
29
30 // Set the parent at construction time, instead of letting show()
31 // set it later on, which for some reason results in the size of
32 // the dialog not being updated.
33 parent: QuickUtils.rootItem(this)
34
35 property var activeTransfer
36
37 Rectangle {
38 anchors.fill: parent
39
40 ContentTransferHint {
41 anchors.fill: parent
42 activeTransfer: picker.activeTransfer
43 }
44
45 ContentPeerPicker {
46 id: peerPicker
47 anchors.fill: parent
48 visible: true
49 contentType: ContentType.All
50 handler: ContentHandler.Source
51
52 onPeerSelected: {
53 if (model.allowMultipleFiles) {
54 peer.selectionType = ContentTransfer.Multiple
55 } else {
56 peer.selectionType = ContentTransfer.Single
57 }
58 picker.activeTransfer = peer.request()
59 stateChangeConnection.target = picker.activeTransfer
60 }
61
62 onCancelPressed: {
63 model.reject()
64 }
65 }
66 }
67
68 Connections {
69 id: stateChangeConnection
70 target: null
71 onStateChanged: {
72 if (picker.activeTransfer.state === ContentTransfer.Charged) {
73 var selectedItems = []
74 for(var i in picker.activeTransfer.items) {
75 selectedItems.push(String(picker.activeTransfer.items[i].url).replace("file://", ""))
76 }
77 model.accept(selectedItems)
78 }
79 }
80 }
81
82 Component.onCompleted: {
83 if(acceptTypes.length === 1) {
84 var contentType = MimeTypeMapper.mimeTypeToContentType(acceptTypes[0])
85 if(contentType == ContentType.Unknown) {
86 // If we don't recognise the type, allow uploads from any app
87 contentType = ContentType.All
88 }
89 peerPicker.contentType = contentType
90 } else {
91 peerPicker.contentType = ContentType.All
92 }
93 show()
94 }
95 }
96}
097
=== modified file 'src/app/webcontainer/WebViewImplOxide.qml'
--- src/app/webcontainer/WebViewImplOxide.qml 2015-08-20 10:42:09 +0000
+++ src/app/webcontainer/WebViewImplOxide.qml 2015-12-16 16:25:40 +0000
@@ -53,6 +53,7 @@
53 property bool runningLocalApplication: false53 property bool runningLocalApplication: false
5454
55 currentWebview: webview55 currentWebview: webview
56 filePicker: filePickerLoader.item
5657
57 context: WebContext {58 context: WebContext {
58 dataPath: webview.dataPath59 dataPath: webview.dataPath
@@ -253,4 +254,28 @@
253 request.accept()254 request.accept()
254 }255 }
255 }256 }
257
258 onShowDownloadDialog: {
259 if (downloadDialogLoader.status === Loader.Ready) {
260 var downloadDialog = PopupUtils.open(downloadDialogLoader.item, webview, {"contentType" : contentType,
261 "downloadId" : downloadId,
262 "singleDownload" : downloader,
263 "filename" : filename,
264 "mimeType" : mimeType})
265 downloadDialog.startDownload.connect(startDownload)
266 }
267 }
268
269 Loader {
270 id: downloadDialogLoader
271 source: "ContentDownloadDialog.qml"
272 asynchronous: true
273 }
274
275 Loader {
276 id: filePickerLoader
277 source: "ContentPickerDialog.qml"
278 asynchronous: true
279 }
280
256}281}
257282
=== modified file 'tests/autopilot/webbrowser_app/emulators/browser.py'
--- tests/autopilot/webbrowser_app/emulators/browser.py 2015-12-04 10:04:30 +0000
+++ tests/autopilot/webbrowser_app/emulators/browser.py 2015-12-16 16:25:40 +0000
@@ -149,11 +149,40 @@
149 def get_settings_page(self):149 def get_settings_page(self):
150 return self.wait_select_single(SettingsPage, visible=True)150 return self.wait_select_single(SettingsPage, visible=True)
151151
152 def get_downloads_page(self):
153 return self.wait_select_single(DownloadsPage, visible=True)
154
152 def get_content_picker_dialog(self):155 def get_content_picker_dialog(self):
153 # only on devices156 # only on devices
154 return self.wait_select_single("PopupBase",157 return self.wait_select_single("PopupBase",
155 objectName="contentPickerDialog")158 objectName="contentPickerDialog")
156159
160 def get_download_dialog(self):
161 return self.wait_select_single("PopupBase",
162 objectName="downloadDialog")
163
164 def get_peer_picker(self):
165 return self.wait_select_single(objectName="contentPeerPicker")
166
167 def get_download_options_dialog(self):
168 return self.wait_select_single("Dialog",
169 objectName="downloadOptionsDialog")
170
171 def click_cancel_download_button(self):
172 button = self.select_single("Button",
173 objectName="cancelDownloadButton")
174 self.pointing_device.click_object(button)
175
176 def click_choose_app_button(self):
177 button = self.select_single("Button",
178 objectName="chooseAppButton")
179 self.pointing_device.click_object(button)
180
181 def click_download_file_button(self):
182 button = self.select_single("Button",
183 objectName="downloadFileButton")
184 self.pointing_device.click_object(button)
185
157 def get_bottom_edge_hint(self):186 def get_bottom_edge_hint(self):
158 return self.select_single("QQuickImage", objectName="bottomEdgeHint")187 return self.select_single("QQuickImage", objectName="bottomEdgeHint")
159188
@@ -473,7 +502,7 @@
473class SettingsPage(uitk.UbuntuUIToolkitCustomProxyObjectBase):502class SettingsPage(uitk.UbuntuUIToolkitCustomProxyObjectBase):
474503
475 def get_header(self):504 def get_header(self):
476 return self.select_single(SettingsPageHeader)505 return self.select_single(BrowserPageHeader)
477506
478 def get_searchengine_entry(self):507 def get_searchengine_entry(self):
479 return self.select_single("Subtitled", objectName="searchengine")508 return self.select_single("Subtitled", objectName="searchengine")
@@ -502,7 +531,13 @@
502 return self.select_single("Standard", objectName="reset")531 return self.select_single("Standard", objectName="reset")
503532
504533
505class SettingsPageHeader(uitk.UbuntuUIToolkitCustomProxyObjectBase):534class DownloadsPage(uitk.UbuntuUIToolkitCustomProxyObjectBase):
535
536 def get_header(self):
537 return self.select_single(BrowserPageHeader)
538
539
540class BrowserPageHeader(uitk.UbuntuUIToolkitCustomProxyObjectBase):
506541
507 @autopilot.logging.log_action(logger.info)542 @autopilot.logging.log_action(logger.info)
508 def click_back_button(self):543 def click_back_button(self):
509544
=== modified file 'tests/autopilot/webbrowser_app/tests/__init__.py'
--- tests/autopilot/webbrowser_app/tests/__init__.py 2015-10-15 19:09:59 +0000
+++ tests/autopilot/webbrowser_app/tests/__init__.py 2015-12-16 16:25:40 +0000
@@ -213,6 +213,15 @@
213 self.pointing_device.click_object(history_action)213 self.pointing_device.click_object(history_action)
214 return self.main_window.get_history_view()214 return self.main_window.get_history_view()
215215
216 def open_downloads(self):
217 chrome = self.main_window.chrome
218 drawer_button = chrome.get_drawer_button()
219 self.pointing_device.click_object(drawer_button)
220 chrome.get_drawer()
221 downloads_action = chrome.get_drawer_action("downloads")
222 self.pointing_device.click_object(downloads_action)
223 return self.main_window.get_downloads_page()
224
216 def assert_number_webviews_eventually(self, count):225 def assert_number_webviews_eventually(self, count):
217 self.assertThat(lambda: len(self.main_window.get_webviews()),226 self.assertThat(lambda: len(self.main_window.get_webviews()),
218 Eventually(Equals(count)))227 Eventually(Equals(count)))
219228
=== modified file 'tests/autopilot/webbrowser_app/tests/http_server.py'
--- tests/autopilot/webbrowser_app/tests/http_server.py 2015-09-29 20:58:36 +0000
+++ tests/autopilot/webbrowser_app/tests/http_server.py 2015-12-16 16:25:40 +0000
@@ -180,6 +180,18 @@
180 self.send_response(200)180 self.send_response(200)
181 name = self.path[len("/tab/"):]181 name = self.path[len("/tab/"):]
182 self.send_html('<html><body>' + name + '</body></html>')182 self.send_html('<html><body>' + name + '</body></html>')
183 elif self.path.startswith("/downloadpdfgenericmime"):
184 self.send_response(200)
185 self.send_header("Content-Type", "application/octet-stream")
186 self.send_header("Content-Disposition",
187 "attachment; filename='test.pdf'")
188 self.end_headers()
189 elif self.path.startswith("/downloadpdf"):
190 self.send_response(200)
191 self.send_header("Content-Type", "application/pdf")
192 self.send_header("Content-Disposition",
193 "attachment; filename='test.pdf'")
194 self.end_headers()
183 elif self.path.startswith("/basicauth"):195 elif self.path.startswith("/basicauth"):
184 login = "user"196 login = "user"
185 password = "pass"197 password = "pass"
186198
=== added file 'tests/autopilot/webbrowser_app/tests/test_downloads.py'
--- tests/autopilot/webbrowser_app/tests/test_downloads.py 1970-01-01 00:00:00 +0000
+++ tests/autopilot/webbrowser_app/tests/test_downloads.py 2015-12-16 16:25:40 +0000
@@ -0,0 +1,77 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#
3# Copyright 2015 Canonical
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17from webbrowser_app.tests import StartOpenRemotePageTestCaseBase
18
19from autopilot.matchers import Eventually
20from autopilot.platform import model
21
22from testtools.matchers import Equals
23
24import testtools
25
26
27@testtools.skipIf(model() == "Desktop", "Don't run on desktop, as "
28 "dependencies aren't guaranteed")
29class TestDownloads(StartOpenRemotePageTestCaseBase):
30
31 def test_open_close_downloads_page(self):
32 downloads_page = self.open_downloads()
33 downloads_page.get_header().click_back_button()
34 downloads_page.wait_until_destroyed()
35
36 def test_mimetype_download(self):
37 self.main_window.go_to_url(self.base_url + "/downloadpdf")
38 dialog = self.main_window.get_download_dialog()
39 options_dialog = self.main_window.get_download_options_dialog()
40 self.assertThat(options_dialog.visible, Eventually(Equals(True)))
41 self.assertThat(dialog.mimeType, Eventually(Equals("application/pdf")))
42
43 def test_generic_mimetype_download(self):
44 self.main_window.go_to_url(self.base_url + "/downloadpdfgenericmime")
45 dialog = self.main_window.get_download_dialog()
46 options_dialog = self.main_window.get_download_options_dialog()
47 self.assertThat(options_dialog.visible, Eventually(Equals(True)))
48 self.assertThat(dialog.mimeType, Eventually(Equals("application/pdf")))
49
50 def test_filename(self):
51 self.main_window.go_to_url(self.base_url + "/downloadpdf")
52 dialog = self.main_window.get_download_dialog()
53 options_dialog = self.main_window.get_download_options_dialog()
54 self.assertThat(options_dialog.visible, Eventually(Equals(True)))
55 self.assertThat(dialog.filename, Eventually(Equals("test.pdf")))
56
57 def test_close_dialog(self):
58 self.main_window.go_to_url(self.base_url + "/downloadpdf")
59 options_dialog = self.main_window.get_download_options_dialog()
60 self.assertThat(options_dialog.visible, Eventually(Equals(True)))
61 self.main_window.click_cancel_download_button()
62
63 def test_picker(self):
64 self.main_window.go_to_url(self.base_url + "/downloadpdf")
65 options_dialog = self.main_window.get_download_options_dialog()
66 self.assertThat(options_dialog.visible, Eventually(Equals(True)))
67 self.main_window.click_choose_app_button()
68 picker = self.main_window.get_peer_picker()
69 self.assertThat(picker.visible, Eventually(Equals(True)))
70
71 def test_download(self):
72 self.main_window.go_to_url(self.base_url + "/downloadpdf")
73 options_dialog = self.main_window.get_download_options_dialog()
74 self.assertThat(options_dialog.visible, Eventually(Equals(True)))
75 self.main_window.click_download_file_button()
76 downloads_page = self.main_window.get_downloads_page()
77 self.assertThat(downloads_page.visible, Eventually(Equals(True)))
078
=== modified file 'tests/autopilot/webbrowser_app/tests/test_settings.py'
--- tests/autopilot/webbrowser_app/tests/test_settings.py 2015-07-06 13:51:36 +0000
+++ tests/autopilot/webbrowser_app/tests/test_settings.py 2015-12-16 16:25:40 +0000
@@ -50,7 +50,7 @@
50 self.pointing_device.click_object(searchengine)50 self.pointing_device.click_object(searchengine)
51 searchengine_page = settings.get_searchengine_page()51 searchengine_page = settings.get_searchengine_page()
52 searchengine_header = searchengine_page.select_single(52 searchengine_header = searchengine_page.select_single(
53 browser.SettingsPageHeader)53 browser.BrowserPageHeader)
54 searchengine_header.click_back_button()54 searchengine_header.click_back_button()
55 searchengine_page.wait_until_destroyed()55 searchengine_page.wait_until_destroyed()
56 self.assertThat(searchengine.subText, Equals(old_engine))56 self.assertThat(searchengine.subText, Equals(old_engine))
@@ -120,7 +120,7 @@
120 privacy = settings.get_privacy_entry()120 privacy = settings.get_privacy_entry()
121 self.pointing_device.click_object(privacy)121 self.pointing_device.click_object(privacy)
122 privacy_page = settings.get_privacy_page()122 privacy_page = settings.get_privacy_page()
123 privacy_header = privacy_page.select_single(browser.SettingsPageHeader)123 privacy_header = privacy_page.select_single(browser.BrowserPageHeader)
124 privacy_header.click_back_button()124 privacy_header.click_back_button()
125 privacy_page.wait_until_destroyed()125 privacy_page.wait_until_destroyed()
126126
127127
=== modified file 'tests/unittests/CMakeLists.txt'
--- tests/unittests/CMakeLists.txt 2015-11-23 09:41:48 +0000
+++ tests/unittests/CMakeLists.txt 2015-12-16 16:25:40 +0000
@@ -20,3 +20,4 @@
20add_subdirectory(intent-filter)20add_subdirectory(intent-filter)
21add_subdirectory(search-engine)21add_subdirectory(search-engine)
22add_subdirectory(text-search-filter-model)22add_subdirectory(text-search-filter-model)
23add_subdirectory(downloads-model)
2324
=== added directory 'tests/unittests/downloads-model'
=== added file 'tests/unittests/downloads-model/CMakeLists.txt'
--- tests/unittests/downloads-model/CMakeLists.txt 1970-01-01 00:00:00 +0000
+++ tests/unittests/downloads-model/CMakeLists.txt 2015-12-16 16:25:40 +0000
@@ -0,0 +1,13 @@
1find_package(Qt5Core REQUIRED)
2find_package(Qt5Sql REQUIRED)
3find_package(Qt5Test REQUIRED)
4set(TEST tst_DownloadsModelTests)
5add_executable(${TEST} tst_DownloadsModelTests.cpp)
6include_directories(${webbrowser-app_SOURCE_DIR})
7target_link_libraries(${TEST}
8 Qt5::Core
9 Qt5::Sql
10 Qt5::Test
11 webbrowser-app-models
12)
13add_test(${TEST} ${CMAKE_CURRENT_BINARY_DIR}/${TEST} -xunitxml -o ${TEST}.xml)
014
=== added file 'tests/unittests/downloads-model/tst_DownloadsModelTests.cpp'
--- tests/unittests/downloads-model/tst_DownloadsModelTests.cpp 1970-01-01 00:00:00 +0000
+++ tests/unittests/downloads-model/tst_DownloadsModelTests.cpp 2015-12-16 16:25:40 +0000
@@ -0,0 +1,190 @@
1/*
2 * Copyright 2015 Canonical Ltd.
3 *
4 * This file is part of webbrowser-app.
5 *
6 * webbrowser-app is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 3.
9 *
10 * webbrowser-app is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19#include <QtCore/QDir>
20#include <QtCore/QTemporaryFile>
21#include <QtTest/QSignalSpy>
22#include <QtTest/QtTest>
23#include "downloads-model.h"
24
25class DownloadsModelTests : public QObject
26{
27 Q_OBJECT
28
29private:
30 DownloadsModel* model;
31
32private Q_SLOTS:
33 void init()
34 {
35 model = new DownloadsModel;
36 model->setDatabasePath(":memory:");
37 }
38
39 void cleanup()
40 {
41 delete model;
42 }
43
44 void shouldBeInitiallyEmpty()
45 {
46 QCOMPARE(model->rowCount(), 0);
47 }
48
49 void shouldExposeRoleNames()
50 {
51 QList<QByteArray> roleNames = model->roleNames().values();
52 QVERIFY(roleNames.contains("downloadId"));
53 QVERIFY(roleNames.contains("url"));
54 QVERIFY(roleNames.contains("path"));
55 QVERIFY(roleNames.contains("filename"));
56 QVERIFY(roleNames.contains("mimetype"));
57 QVERIFY(roleNames.contains("complete"));
58 QVERIFY(roleNames.contains("paused"));
59 QVERIFY(roleNames.contains("error"));
60 QVERIFY(roleNames.contains("created"));
61 }
62
63 void shouldAddNewEntries()
64 {
65 QSignalSpy spy(model, SIGNAL(added(QString, QUrl, QString)));
66
67 model->add("testid", QUrl("http://example.org/"), "text/plain");
68 QCOMPARE(model->rowCount(), 1);
69 QCOMPARE(spy.count(), 1);
70 QVariantList args = spy.takeFirst();
71 QCOMPARE(args.at(0).toString(), QString("testid"));
72 QCOMPARE(args.at(1).toUrl(), QUrl("http://example.org/"));
73 QCOMPARE(args.at(2).toString(), QString("text/plain"));
74
75 model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");
76 QCOMPARE(model->rowCount(), 2);
77 QCOMPARE(spy.count(), 1);
78 args = spy.takeFirst();
79 QCOMPARE(args.at(0).toString(), QString("testid2"));
80 QCOMPARE(args.at(1).toUrl(), QUrl("http://example.org/pdf"));
81 QCOMPARE(args.at(2).toString(), QString("application/pdf"));
82 }
83
84 void shouldRemoveCancelled()
85 {
86 model->add("testid", QUrl("http://example.org/"), "text/plain");
87 model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");
88 model->add("testid3", QUrl("https://example.org/secure.png"), "image/png");
89 QCOMPARE(model->rowCount(), 3);
90
91 model->cancelDownload("testid2");
92 QCOMPARE(model->rowCount(), 2);
93
94 model->cancelDownload("invalid");
95 QCOMPARE(model->rowCount(), 2);
96 }
97
98 void shouldCompleteDownloads()
99 {
100 QSignalSpy spy(model, SIGNAL(completeChanged(QString, bool)));
101
102 model->add("testid", QUrl("http://example.org/"), "text/plain");
103 QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Complete).toBool());
104 model->setComplete("testid", true);
105 QCOMPARE(spy.count(), 1);
106 QVariantList args = spy.takeFirst();
107 QCOMPARE(args.at(0).toString(), QString("testid"));
108 QCOMPARE(args.at(1).toBool(), true);
109 }
110
111 void shouldKeepEntriesSortedChronologically()
112 {
113 model->add("testid", QUrl("http://example.org/"), "text/plain");
114 model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");
115 model->add("testid3", QUrl("https://example.org/secure.png"), "image/png");
116
117 QCOMPARE(model->data(model->index(0, 0), DownloadsModel::DownloadId).toString(), QString("testid3"));
118 QCOMPARE(model->data(model->index(1, 0), DownloadsModel::DownloadId).toString(), QString("testid2"));
119 QCOMPARE(model->data(model->index(2, 0), DownloadsModel::DownloadId).toString(), QString("testid"));
120 }
121
122 void shouldReturnData()
123 {
124 model->add("testid", QUrl("http://example.org/"), "text/plain");
125 QVERIFY(!model->data(QModelIndex(), DownloadsModel::DownloadId).isValid());
126 QVERIFY(!model->data(model->index(-1, 0), DownloadsModel::DownloadId).isValid());
127 QVERIFY(!model->data(model->index(3, 0), DownloadsModel::DownloadId).isValid());
128 QCOMPARE(model->data(model->index(0, 0), DownloadsModel::DownloadId).toString(), QString("testid"));
129 QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Url).toUrl(), QUrl("http://example.org/"));
130 QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Mimetype).toString(), QString("text/plain"));
131 QVERIFY(model->data(model->index(0, 0), DownloadsModel::Created).toDateTime() <= QDateTime::currentDateTime());
132 QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Complete).toBool());
133 }
134
135 void shouldReturnDatabasePath()
136 {
137 QCOMPARE(model->databasePath(), QString(":memory:"));
138 }
139
140 void shouldNotifyWhenSettingDatabasePath()
141 {
142 QSignalSpy spyPath(model, SIGNAL(databasePathChanged()));
143 QSignalSpy spyReset(model, SIGNAL(modelReset()));
144
145 model->setDatabasePath(":memory:");
146 QVERIFY(spyPath.isEmpty());
147 QVERIFY(spyReset.isEmpty());
148
149 model->setDatabasePath("");
150 QCOMPARE(spyPath.count(), 1);
151 QCOMPARE(spyReset.count(), 1);
152 QCOMPARE(model->databasePath(), QString(":memory:"));
153 }
154
155 void shouldSerializeOnDisk()
156 {
157 QTemporaryFile tempFile;
158 tempFile.open();
159 QString fileName = tempFile.fileName();
160 delete model;
161 model = new DownloadsModel;
162 model->setDatabasePath(fileName);
163 model->add("testid", QUrl("http://example.org/"), "text/plain");
164 model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");
165 delete model;
166 model = new DownloadsModel;
167 model->setDatabasePath(fileName);
168 model->fetchMore();
169 QCOMPARE(model->rowCount(), 2);
170 }
171
172 void shouldCountNumberOfEntries()
173 {
174 QCOMPARE(model->property("count").toInt(), 0);
175 QCOMPARE(model->rowCount(), 0);
176 model->add("testid", QUrl("http://example.org/"), "text/plain");
177 QCOMPARE(model->property("count").toInt(), 1);
178 QCOMPARE(model->rowCount(), 1);
179 model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");
180 QCOMPARE(model->property("count").toInt(), 2);
181 QCOMPARE(model->rowCount(), 2);
182 model->add("testid3", QUrl("https://example.org/secure.png"), "image/png");
183 QCOMPARE(model->property("count").toInt(), 3);
184 QCOMPARE(model->rowCount(), 3);
185 }
186
187};
188
189QTEST_MAIN(DownloadsModelTests)
190#include "tst_DownloadsModelTests.moc"

Subscribers

People subscribed via source and target branches

to status/vote changes: