Merge lp:~ahayzen/webbrowser-app/fix-1633040-add-js-dialog-ap-tests into lp:webbrowser-app

Proposed by Andrew Hayzen
Status: Superseded
Proposed branch: lp:~ahayzen/webbrowser-app/fix-1633040-add-js-dialog-ap-tests
Merge into: lp:webbrowser-app
Diff against target: 5186 lines (+2471/-1203)
51 files modified
.bzrignore (+1/-0)
CMakeLists.txt (+3/-6)
debian/control (+1/-2)
make-snap.sh (+4/-0)
po/webbrowser-app.pot (+49/-43)
setup/gui/webbrowser-app.desktop.in (+27/-0)
snap/webbrowser-app.launcher (+10/-0)
snapcraft.yaml (+66/-0)
src/Ubuntu/CMakeLists.txt (+0/-8)
src/Ubuntu/Web/UbuntuWebContext.qml (+7/-1)
src/Ubuntu/Web/ua-overrides-desktop.js.in (+9/-9)
src/Ubuntu/Web/ua-overrides-mobile.js.in (+11/-11)
src/app/AlertDialog.qml (+2/-0)
src/app/BeforeUnloadDialog.qml (+3/-0)
src/app/ChromeBase.qml (+5/-3)
src/app/ConfirmDialog.qml (+3/-0)
src/app/Downloader.qml (+3/-11)
src/app/PromptDialog.qml (+4/-0)
src/app/ThinProgressBar.qml (+3/-6)
src/app/WebViewImpl.qml (+2/-2)
src/app/config.h.in (+3/-2)
src/app/webbrowser/BookmarkOptions.qml (+25/-4)
src/app/webbrowser/Browser.qml (+112/-596)
src/app/webbrowser/BrowserTab.qml (+4/-3)
src/app/webbrowser/Chrome.qml (+30/-5)
src/app/webbrowser/DownloadDelegate.qml (+151/-149)
src/app/webbrowser/DownloadsPage.qml (+10/-2)
src/app/webbrowser/HistoryViewWithExpansion.qml (+67/-0)
src/app/webbrowser/NavigationBar.qml (+3/-11)
src/app/webbrowser/TabComponent.qml (+436/-0)
src/app/webbrowser/TabItem.qml (+16/-4)
src/app/webbrowser/TabsBar.qml (+51/-6)
src/app/webbrowser/downloads-model.cpp (+139/-80)
src/app/webbrowser/downloads-model.h (+7/-9)
src/app/webbrowser/history-model.cpp (+251/-152)
src/app/webbrowser/history-model.h (+64/-7)
src/app/webbrowser/webbrowser-app.qml (+28/-9)
src/app/webcontainer/Chrome.qml (+1/-0)
src/app/webcontainer/WebApp.qml (+2/-1)
tests/autopilot/webapp_container/tests/__init__.py (+9/-6)
tests/autopilot/webapp_container/tests/fake_servers.py (+34/-0)
tests/autopilot/webapp_container/tests/test_js_dialogs.py (+214/-0)
tests/autopilot/webbrowser_app/emulators/browser.py (+64/-2)
tests/autopilot/webbrowser_app/tests/http_server.py (+48/-0)
tests/autopilot/webbrowser_app/tests/test_history.py (+35/-0)
tests/autopilot/webbrowser_app/tests/test_js_dialogs.py (+156/-0)
tests/autopilot/webbrowser_app/tests/test_new_tab_view.py (+1/-8)
tests/unittests/downloads-model/tst_DownloadsModelTests.cpp (+262/-43)
tests/unittests/history-model/tst_HistoryModelTests.cpp (+3/-2)
tests/unittests/qml/CMakeLists.txt (+6/-0)
tests/unittests/qml/tst_TabsBar.qml (+26/-0)
To merge this branch: bzr merge lp:~ahayzen/webbrowser-app/fix-1633040-add-js-dialog-ap-tests
Reviewer Review Type Date Requested Status
system-apps-ci-bot continuous-integration Needs Fixing
Ubuntu Phablet Team Pending
Review via email: mp+308390@code.launchpad.net

Commit message

* Add autopilot tests javascript dialogs to webbrowser and webapp-container - alertDialog, beforeUnloadDialog, confirmDialog and promptDialog

Description of the change

* Add autopilot tests javascript dialogs to webbrowser and webapp-container - alertDialog, beforeUnloadDialog, confirmDialog and promptDialog

TESTING:
Run the following test suites:
webapp_container.tests.test_js_dialogs.TestJSDialogs
webbrowser_app.tests.test_js_dialogs.TestJSDialogs

To post a comment you must log in.
1561. By Andrew Hayzen

* Revert .pot changes

Revision history for this message
system-apps-ci-bot (system-apps-ci-bot) wrote :

FAILED: Continuous integration, rev:1560
https://jenkins.canonical.com/system-apps/job/lp-webbrowser-app-ci/705/
Executed test runs:
    FAILURE: https://jenkins.canonical.com/system-apps/job/build/1837/console
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-0-fetch/1838
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=vivid+overlay/1678
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=vivid+overlay/1678/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=xenial+overlay/1678
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=xenial+overlay/1678/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=yakkety/1678
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=yakkety/1678/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=vivid+overlay/1678
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=vivid+overlay/1678/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=xenial+overlay/1678
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=xenial+overlay/1678/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=yakkety/1678
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=yakkety/1678/artifact/output/*zip*/output.zip
    FAILURE: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=vivid+overlay/1678/console
    FAILURE: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=xenial+overlay/1678/console
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=yakkety/1678
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=yakkety/1678/artifact/output/*zip*/output.zip

Click here to trigger a rebuild:
https://jenkins.canonical.com/system-apps/job/lp-webbrowser-app-ci/705/rebuild

review: Needs Fixing (continuous-integration)
1562. By Andrew Hayzen

* Ensure dialog closes on before unload tests

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '.bzrignore'
--- .bzrignore 2016-06-13 13:41:48 +0000
+++ .bzrignore 2016-10-13 14:41:36 +0000
@@ -31,6 +31,7 @@
31doc/html31doc/html
32click-hooks/webapp-container-hook32click-hooks/webapp-container-hook
33click-hooks/webapp-container.hook33click-hooks/webapp-container.hook
34setup/gui/webbrowser-app.desktop
3435
35obj-*36obj-*
36debian/usr.bin.webbrowser-app37debian/usr.bin.webbrowser-app
3738
=== modified file 'CMakeLists.txt'
--- CMakeLists.txt 2016-07-01 13:09:27 +0000
+++ CMakeLists.txt 2016-10-13 14:41:36 +0000
@@ -11,11 +11,6 @@
11if(NOT INTLTOOL_EXTRACT)11if(NOT INTLTOOL_EXTRACT)
12 message(FATAL_ERROR "Could not find intltool-extract, please install the intltool package")12 message(FATAL_ERROR "Could not find intltool-extract, please install the intltool package")
13endif()13endif()
14find_program(XVFBRUN xvfb-run)
15if(NOT XVFBRUN)
16 message(FATAL_ERROR "Could not find xvfb-run, please install the xvfb package")
17endif()
18set(XVFB_COMMAND ${XVFBRUN} -s "-screen 0 640x480x24" -a)
1914
20# Standard install paths15# Standard install paths
21include(GNUInstallDirs)16include(GNUInstallDirs)
@@ -67,13 +62,15 @@
67add_subdirectory(tests)62add_subdirectory(tests)
6863
69# make non compiled files (QML, JS, images, etc.) visible in QtCreator64# make non compiled files (QML, JS, images, etc.) visible in QtCreator
70file(GLOB NON_COMPILED_ROOT *.png .bzrignore COPYING README)65file(GLOB NON_COMPILED_ROOT *.png .bzrignore COPYING make-snap.sh README snapcraft.yaml)
71file(GLOB_RECURSE NON_COMPILED_SUBDIRS66file(GLOB_RECURSE NON_COMPILED_SUBDIRS
72 debian/*.dirs debian/*.install debian/*.lintian-overrides debian/*.manifest67 debian/*.dirs debian/*.install debian/*.lintian-overrides debian/*.manifest
73 debian/compat debian/control debian/copyright debian/rules debian/source/format68 debian/compat debian/control debian/copyright debian/rules debian/source/format
74 debian/tests/*69 debian/tests/*
75 doc/*.css doc/*.qdoc doc/*.qdocconf70 doc/*.css doc/*.qdoc doc/*.qdocconf
76 po/*.po po/*.pot71 po/*.po po/*.pot
72 setup/gui/*.png setup/gui/webbrowser-app.desktop.in
73 snap/webbrowser-app.launcher
77 src/*.js src/*.qml src/*.sci src/README74 src/*.js src/*.qml src/*.sci src/README
78 tests/*.py tests/*.qml)75 tests/*.py tests/*.qml)
79add_custom_target(NON_COMPILED_TARGET ALL SOURCES ${NON_COMPILED_ROOT} ${NON_COMPILED_SUBDIRS})76add_custom_target(NON_COMPILED_TARGET ALL SOURCES ${NON_COMPILED_ROOT} ${NON_COMPILED_SUBDIRS})
8077
=== modified file 'debian/control'
--- debian/control 2016-08-25 09:52:00 +0000
+++ debian/control 2016-10-13 14:41:36 +0000
@@ -11,7 +11,7 @@
11 dh-translations,11 dh-translations,
12 libapparmor-dev,12 libapparmor-dev,
13 libevdev-dev,13 libevdev-dev,
14 liboxideqt-qmlplugin (>= 1.15),14 liboxideqt-qmlplugin (>= 1.12),
15 libqt5sql5-sqlite,15 libqt5sql5-sqlite,
16 libudev-dev,16 libudev-dev,
17 lsb-release,17 lsb-release,
@@ -23,7 +23,6 @@
23 qml-module-qtquick2 (>= 5.4),23 qml-module-qtquick2 (>= 5.4),
24 qml-module-qtquick-layouts,24 qml-module-qtquick-layouts,
25 qml-module-qttest,25 qml-module-qttest,
26 qmlscene,
27 qt5-default,26 qt5-default,
28 qt5-qmake,27 qt5-qmake,
29 qtbase5-dev (>= 5.4),28 qtbase5-dev (>= 5.4),
3029
=== added file 'make-snap.sh'
--- make-snap.sh 1970-01-01 00:00:00 +0000
+++ make-snap.sh 2016-10-13 14:41:36 +0000
@@ -0,0 +1,4 @@
1#!/bin/sh
2SNAP_DESKTOP_FILE=setup/gui/webbrowser-app.desktop
3intltool-merge -d -u po $SNAP_DESKTOP_FILE.in $SNAP_DESKTOP_FILE
4snapcraft
05
=== modified file 'po/webbrowser-app.pot'
--- po/webbrowser-app.pot 2016-10-04 14:07:17 +0000
+++ po/webbrowser-app.pot 2016-10-13 14:41:36 +0000
@@ -8,7 +8,7 @@
8msgstr ""8msgstr ""
9"Project-Id-Version: webbrowser-app\n"9"Project-Id-Version: webbrowser-app\n"
10"Report-Msgid-Bugs-To: \n"10"Report-Msgid-Bugs-To: \n"
11"POT-Creation-Date: 2016-10-04 16:02+0200\n"11"POT-Creation-Date: 2016-10-13 12:01+0100\n"
12"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"12"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"13"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14"Language-Team: LANGUAGE <LL@li.org>\n"14"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -24,7 +24,7 @@
2424
25#: src/app/AlertDialog.qml:26 src/app/AuthenticationDialog.qml:4725#: src/app/AlertDialog.qml:26 src/app/AuthenticationDialog.qml:47
26#: src/app/ConfirmDialog.qml:26 src/app/HttpAuthenticationDialog.qml:5926#: src/app/ConfirmDialog.qml:26 src/app/HttpAuthenticationDialog.qml:59
27#: src/app/PromptDialog.qml:32 src/app/webbrowser/BookmarkOptions.qml:10127#: src/app/PromptDialog.qml:32 src/app/webbrowser/BookmarkOptions.qml:122
28msgid "OK"28msgid "OK"
29msgstr ""29msgstr ""
3030
@@ -48,10 +48,10 @@
4848
49#: src/app/AuthenticationDialog.qml:53 src/app/ConfirmDialog.qml:3249#: src/app/AuthenticationDialog.qml:53 src/app/ConfirmDialog.qml:32
50#: src/app/HttpAuthenticationDialog.qml:69 src/app/PromptDialog.qml:3850#: src/app/HttpAuthenticationDialog.qml:69 src/app/PromptDialog.qml:38
51#: src/app/webbrowser/BookmarkOptions.qml:14351#: src/app/webbrowser/BookmarkOptions.qml:164
52#: src/app/webbrowser/ContentDownloadDialog.qml:11452#: src/app/webbrowser/ContentDownloadDialog.qml:114
53#: src/app/webbrowser/ContextMenuMobile.qml:13953#: src/app/webbrowser/ContextMenuMobile.qml:139
54#: src/app/webbrowser/DownloadDelegate.qml:18754#: src/app/webbrowser/DownloadDelegate.qml:178
55#: src/app/webbrowser/SettingsPage.qml:25855#: src/app/webbrowser/SettingsPage.qml:258
56#: src/app/webbrowser/SettingsPage.qml:31256#: src/app/webbrowser/SettingsPage.qml:312
57#: src/app/webcontainer/AccountChooserDialog.qml:9657#: src/app/webcontainer/AccountChooserDialog.qml:96
@@ -378,7 +378,7 @@
378msgid "Erase"378msgid "Erase"
379msgstr ""379msgstr ""
380380
381#: src/app/actions/FindInPage.qml:23 src/app/webbrowser/Browser.qml:569381#: src/app/actions/FindInPage.qml:23 src/app/webbrowser/Browser.qml:548
382msgid "Find in page"382msgid "Find in page"
383msgstr ""383msgstr ""
384384
@@ -408,8 +408,8 @@
408msgid "Address;URL;www"408msgid "Address;URL;www"
409msgstr ""409msgstr ""
410410
411#: src/app/actions/NewTab.qml:23 src/app/webbrowser/Browser.qml:440411#: src/app/actions/NewTab.qml:23 src/app/webbrowser/Browser.qml:420
412#: src/app/webbrowser/TabsBar.qml:91412#: src/app/webbrowser/TabsBar.qml:103
413msgid "New Tab"413msgid "New Tab"
414msgstr ""414msgstr ""
415415
@@ -456,7 +456,7 @@
456msgstr ""456msgstr ""
457457
458#: src/app/actions/Reload.qml:23 src/app/webbrowser/SadTab.qml:86458#: src/app/actions/Reload.qml:23 src/app/webbrowser/SadTab.qml:86
459#: src/app/webbrowser/TabsBar.qml:96 src/app/webcontainer/SadPage.qml:51459#: src/app/webbrowser/TabsBar.qml:108 src/app/webcontainer/SadPage.qml:51
460msgid "Reload"460msgid "Reload"
461msgstr ""461msgstr ""
462462
@@ -478,11 +478,11 @@
478msgid "Save video"478msgid "Save video"
479msgstr ""479msgstr ""
480480
481#: src/app/actions/SelectAll.qml:22 src/app/webbrowser/DownloadsPage.qml:83481#: src/app/actions/SelectAll.qml:22 src/app/webbrowser/DownloadsPage.qml:84
482msgid "Select all"482msgid "Select all"
483msgstr ""483msgstr ""
484484
485#: src/app/actions/Share.qml:22 src/app/webbrowser/Browser.qml:549485#: src/app/actions/Share.qml:22 src/app/webbrowser/Browser.qml:528
486msgid "Share"486msgid "Share"
487msgstr ""487msgstr ""
488488
@@ -504,36 +504,36 @@
504msgid "search or enter an address"504msgid "search or enter an address"
505msgstr ""505msgstr ""
506506
507#: src/app/webbrowser/BookmarkOptions.qml:48507#: src/app/webbrowser/BookmarkOptions.qml:66
508msgid "Bookmark Added"508msgid "Bookmark Added"
509msgstr ""509msgstr ""
510510
511#. TRANSLATORS: Field where the title of bookmarked URL can be changed511#. TRANSLATORS: Field where the title of bookmarked URL can be changed
512#: src/app/webbrowser/BookmarkOptions.qml:53512#: src/app/webbrowser/BookmarkOptions.qml:71
513msgid "Name"513msgid "Name"
514msgstr ""514msgstr ""
515515
516#. TRANSLATORS: Field to choose the folder where bookmarked URL will be saved in516#. TRANSLATORS: Field to choose the folder where bookmarked URL will be saved in
517#: src/app/webbrowser/BookmarkOptions.qml:71517#: src/app/webbrowser/BookmarkOptions.qml:89
518msgid "Save in"518msgid "Save in"
519msgstr ""519msgstr ""
520520
521#: src/app/webbrowser/BookmarkOptions.qml:78521#: src/app/webbrowser/BookmarkOptions.qml:96
522#: src/app/webbrowser/BookmarksFoldersView.qml:133522#: src/app/webbrowser/BookmarksFoldersView.qml:133
523#: src/app/webbrowser/BookmarksFoldersViewWide.qml:105523#: src/app/webbrowser/BookmarksFoldersViewWide.qml:105
524msgid "All Bookmarks"524msgid "All Bookmarks"
525msgstr ""525msgstr ""
526526
527#: src/app/webbrowser/BookmarkOptions.qml:93527#: src/app/webbrowser/BookmarkOptions.qml:114
528#: src/app/webbrowser/BookmarkOptions.qml:133528#: src/app/webbrowser/BookmarkOptions.qml:154
529msgid "New Folder"529msgid "New Folder"
530msgstr ""530msgstr ""
531531
532#: src/app/webbrowser/BookmarkOptions.qml:115532#: src/app/webbrowser/BookmarkOptions.qml:136
533msgid "Create new folder"533msgid "Create new folder"
534msgstr ""534msgstr ""
535535
536#: src/app/webbrowser/BookmarkOptions.qml:153536#: src/app/webbrowser/BookmarkOptions.qml:174
537#: src/app/webbrowser/SettingsPage.qml:322537#: src/app/webbrowser/SettingsPage.qml:322
538msgid "Save"538msgid "Save"
539msgstr ""539msgstr ""
@@ -547,14 +547,14 @@
547547
548#: src/app/webbrowser/BookmarksView.qml:32548#: src/app/webbrowser/BookmarksView.qml:32
549#: src/app/webbrowser/BookmarksViewWide.qml:32549#: src/app/webbrowser/BookmarksViewWide.qml:32
550#: src/app/webbrowser/Browser.qml:557 src/app/webbrowser/NewTabView.qml:130550#: src/app/webbrowser/Browser.qml:536 src/app/webbrowser/NewTabView.qml:130
551#: src/app/webbrowser/NewTabViewWide.qml:139551#: src/app/webbrowser/NewTabViewWide.qml:139
552msgid "Bookmarks"552msgid "Bookmarks"
553msgstr ""553msgstr ""
554554
555#: src/app/webbrowser/BookmarksView.qml:76555#: src/app/webbrowser/BookmarksView.qml:76
556#: src/app/webbrowser/BookmarksViewWide.qml:75556#: src/app/webbrowser/BookmarksViewWide.qml:75
557#: src/app/webbrowser/Browser.qml:426 src/app/webbrowser/HistoryView.qml:126557#: src/app/webbrowser/Browser.qml:406 src/app/webbrowser/HistoryView.qml:126
558#: src/app/webbrowser/HistoryViewWide.qml:407558#: src/app/webbrowser/HistoryViewWide.qml:407
559msgid "Done"559msgid "Done"
560msgstr ""560msgstr ""
@@ -563,45 +563,37 @@
563#: src/app/webbrowser/BookmarksViewWide.qml:89563#: src/app/webbrowser/BookmarksViewWide.qml:89
564#: src/app/webbrowser/HistoryView.qml:140564#: src/app/webbrowser/HistoryView.qml:140
565#: src/app/webbrowser/HistoryViewWide.qml:421565#: src/app/webbrowser/HistoryViewWide.qml:421
566#: src/app/webbrowser/TabsBar.qml:153 src/app/webbrowser/TabsList.qml:99566#: src/app/webbrowser/TabsBar.qml:165 src/app/webbrowser/TabsList.qml:99
567msgid "New tab"567msgid "New tab"
568msgstr ""568msgstr ""
569569
570#: src/app/webbrowser/Browser.qml:537570#: src/app/webbrowser/Browser.qml:516
571msgid "New window"571msgid "New window"
572msgstr ""572msgstr ""
573573
574#: src/app/webbrowser/Browser.qml:543574#: src/app/webbrowser/Browser.qml:522
575msgid "New private window"575msgid "New private window"
576msgstr ""576msgstr ""
577577
578#: src/app/webbrowser/Browser.qml:563 src/app/webbrowser/HistoryView.qml:30578#: src/app/webbrowser/Browser.qml:542 src/app/webbrowser/HistoryView.qml:30
579#: src/app/webbrowser/HistoryViewWide.qml:35579#: src/app/webbrowser/HistoryViewWide.qml:35
580msgid "History"580msgid "History"
581msgstr ""581msgstr ""
582582
583#: src/app/webbrowser/Browser.qml:576 src/app/webbrowser/DownloadsPage.qml:45583#: src/app/webbrowser/Browser.qml:555 src/app/webbrowser/DownloadsPage.qml:46
584msgid "Downloads"584msgid "Downloads"
585msgstr ""585msgstr ""
586586
587#: src/app/webbrowser/Browser.qml:583 src/app/webbrowser/SettingsPage.qml:41587#: src/app/webbrowser/Browser.qml:562 src/app/webbrowser/SettingsPage.qml:41
588msgid "Settings"588msgid "Settings"
589msgstr ""589msgstr ""
590590
591#. TRANSLATORS: %1 refers to the current number of tabs opened591#. TRANSLATORS: %1 refers to the current number of tabs opened
592#: src/app/webbrowser/Browser.qml:756 src/app/webbrowser/Browser.qml:794592#: src/app/webbrowser/Browser.qml:735 src/app/webbrowser/Browser.qml:773
593#, qt-format593#, qt-format
594msgid "(%1)"594msgid "(%1)"
595msgstr ""595msgstr ""
596596
597#: src/app/webbrowser/Browser.qml:1337
598msgid "Swipe Up To Exit Full Screen"
599msgstr ""
600
601#: src/app/webbrowser/Browser.qml:1338
602msgid "Press ESC To Exit Full Screen"
603msgstr ""
604
605#: src/app/webbrowser/ContentDownloadDialog.qml:83597#: src/app/webbrowser/ContentDownloadDialog.qml:83
606msgid ""598msgid ""
607"Choose an application to open this file or add it to the downloads folder."599"Choose an application to open this file or add it to the downloads folder."
@@ -615,23 +607,29 @@
615msgid "Download"607msgid "Download"
616msgstr ""608msgstr ""
617609
618#: src/app/webbrowser/DownloadDelegate.qml:159610#: src/app/webbrowser/DownloadDelegate.qml:151
619msgid "Download failed"611msgid "Download failed"
620msgstr ""612msgstr ""
621613
622#: src/app/webbrowser/DownloadDelegate.qml:208614#. TRANSLATORS: %1 is the percentage of the download completed so far
615#: src/app/webbrowser/DownloadDelegate.qml:194
616#, qt-format
617msgid "%1%"
618msgstr ""
619
620#: src/app/webbrowser/DownloadDelegate.qml:199
623msgid "Resume"621msgid "Resume"
624msgstr ""622msgstr ""
625623
626#: src/app/webbrowser/DownloadsPage.qml:59624#: src/app/webbrowser/DownloadsPage.qml:60
627msgid "Confirm selection"625msgid "Confirm selection"
628msgstr ""626msgstr ""
629627
630#: src/app/webbrowser/DownloadsPage.qml:99628#: src/app/webbrowser/DownloadsPage.qml:100
631msgid "Delete"629msgid "Delete"
632msgstr ""630msgstr ""
633631
634#: src/app/webbrowser/DownloadsPage.qml:252632#: src/app/webbrowser/DownloadsPage.qml:260
635msgid "No downloads available"633msgid "No downloads available"
636msgstr ""634msgstr ""
637635
@@ -819,11 +817,19 @@
819msgid "Camera"817msgid "Camera"
820msgstr ""818msgstr ""
821819
820#: src/app/webbrowser/TabComponent.qml:388
821msgid "Swipe Up To Exit Full Screen"
822msgstr ""
823
824#: src/app/webbrowser/TabComponent.qml:389
825msgid "Press ESC To Exit Full Screen"
826msgstr ""
827
822#: src/app/webbrowser/TabPreview.qml:86828#: src/app/webbrowser/TabPreview.qml:86
823msgid "Tap to view"829msgid "Tap to view"
824msgstr ""830msgstr ""
825831
826#: src/app/webbrowser/TabsBar.qml:102832#: src/app/webbrowser/TabsBar.qml:114
827msgid "Close Tab"833msgid "Close Tab"
828msgstr ""834msgstr ""
829835
@@ -832,13 +838,13 @@
832msgstr ""838msgstr ""
833839
834#. TRANSLATORS: %1 refers to the current page’s title840#. TRANSLATORS: %1 refers to the current page’s title
835#: src/app/webbrowser/webbrowser-app.qml:108841#: src/app/webbrowser/webbrowser-app.qml:99
836#: src/app/webcontainer/webapp-container.qml:72842#: src/app/webcontainer/webapp-container.qml:72
837#, qt-format843#, qt-format
838msgid "%1 - Ubuntu Web Browser"844msgid "%1 - Ubuntu Web Browser"
839msgstr ""845msgstr ""
840846
841#: src/app/webbrowser/webbrowser-app.qml:110847#: src/app/webbrowser/webbrowser-app.qml:101
842#: src/app/webcontainer/webapp-container.qml:74848#: src/app/webcontainer/webapp-container.qml:74
843msgid "Ubuntu Web Browser"849msgid "Ubuntu Web Browser"
844msgstr ""850msgstr ""
845851
=== added directory 'setup'
=== added directory 'setup/gui'
=== added symlink 'setup/gui/icon.png'
=== target is u'../../webbrowser-app.png'
=== added symlink 'setup/gui/screenshot.png'
=== target is u'../../screenshot.png'
=== added file 'setup/gui/webbrowser-app.desktop.in'
--- setup/gui/webbrowser-app.desktop.in 1970-01-01 00:00:00 +0000
+++ setup/gui/webbrowser-app.desktop.in 2016-10-13 14:41:36 +0000
@@ -0,0 +1,27 @@
1[Desktop Entry]
2Version=1.0
3_Name=Browser
4_GenericName=Web Browser
5_Comment=Browse the World Wide Web
6_Keywords=Internet;WWW;Browser;Web;Explorer
7Type=Application
8Icon=${SNAP}/meta/gui/icon.png
9Exec=webbrowser-app %u
10Terminal=false
11Categories=Network;WebBrowser;
12MimeType=text/html;text/xml;application/xhtml+xml;x-scheme-handler/http;x-scheme-handler/https;
13X-Ubuntu-Touch=true
14X-Ubuntu-Gettext-Domain=webbrowser-app
15X-Ubuntu-Single-Instance=true
16X-Ubuntu-Default-Department-ID=web-browsers
17X-Screenshot=${SNAP}/meta/gui/screenshot.png
18X-Ubuntu-Splash-Color=#FFFFFF
19Actions=NewWindow;Incognito;
20
21[Desktop Action NewWindow]
22_Name=Open a New Window
23Exec=webbrowser-app --new-window
24
25[Desktop Action Incognito]
26_Name=Open a New Private Window
27Exec=webbrowser-app --incognito
028
=== added directory 'snap'
=== added file 'snap/webbrowser-app.launcher'
--- snap/webbrowser-app.launcher 1970-01-01 00:00:00 +0000
+++ snap/webbrowser-app.launcher 2016-10-13 14:41:36 +0000
@@ -0,0 +1,10 @@
1#!/bin/sh
2
3# Disable the chromium sandbox to work around https://launchpad.net/bugs/1599234.
4# Rely on snapd’s security policy instead.
5export OXIDE_NO_SANDBOX=1
6
7# Explicitly set APP_ID.
8export APP_ID=webbrowser-app
9
10exec "$SNAP/bin/desktop-launch" "webbrowser-app" --desktop_file_hint=unity8 "$@"
011
=== added file 'snapcraft.yaml'
--- snapcraft.yaml 1970-01-01 00:00:00 +0000
+++ snapcraft.yaml 2016-10-13 14:41:36 +0000
@@ -0,0 +1,66 @@
1name: webbrowser-app
2version: 0.23+16.10.20160928-0ubuntu1
3summary: Ubuntu web browser
4description: A lightweight web browser tailored for Ubuntu, based on the Oxide browser engine and using the Ubuntu UI components.
5confinement: strict
6
7apps:
8 webbrowser-app:
9 command: webbrowser-app.launcher
10 plugs:
11 - browser-sandbox
12 - camera
13 - network
14 - network-bind
15 - opengl
16 - pulseaudio
17 - screen-inhibit-control
18 - unity7
19
20plugs:
21 browser-sandbox:
22 interface: browser-support
23 allow-sandbox: true
24
25parts:
26 webbrowser-app:
27 plugin: cmake
28 source: .
29 build-packages:
30 - intltool
31 - libapparmor-dev
32 - libevdev-dev
33 - libudev-dev
34 - lsb-release
35 - pkg-config
36 - qt5-default
37 - qt5-qmake
38 - qtbase5-dev
39 - qtbase5-dev-tools
40 - qtbase5-private-dev
41 - qtdeclarative5-dev
42 - qttools5-dev-tools
43 - xvfb
44 stage-packages:
45 - fonts-liberation
46 - liboxideqt-qmlplugin
47 - libqt5sql5-sqlite
48 - mir-graphics-drivers-desktop
49 - qml-module-qt-labs-folderlistmodel
50 - qml-module-qt-labs-settings
51 - qml-module-qtquick2
52 - qml-module-qtquick-layouts
53 - qml-module-qtquick-window2
54 - qml-module-ubuntu-components
55 - qml-module-ubuntu-thumbnailer0.1
56 - qtdeclarative5-ubuntu-content1
57 - qtdeclarative5-ubuntu-download-manager0.1
58 - qtdeclarative5-unity-action-plugin
59 - qtubuntu-desktop
60 after: [desktop-qt5]
61
62 launcher:
63 plugin: dump
64 source: snap
65 organize:
66 webbrowser-app.launcher: bin/webbrowser-app.launcher
067
=== modified file 'src/Ubuntu/CMakeLists.txt'
--- src/Ubuntu/CMakeLists.txt 2016-07-01 13:06:40 +0000
+++ src/Ubuntu/CMakeLists.txt 2016-10-13 14:41:36 +0000
@@ -23,13 +23,5 @@
23 OUTPUT_VARIABLE UBUNTU_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE)23 OUTPUT_VARIABLE UBUNTU_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE)
24add_definitions(-DUBUNTU_VERSION="${UBUNTU_VERSION}")24add_definitions(-DUBUNTU_VERSION="${UBUNTU_VERSION}")
2525
26execute_process(COMMAND ${XVFB_COMMAND} qmlscene --quit ${CMAKE_CURRENT_SOURCE_DIR}/chromium-version.qml
27 OUTPUT_VARIABLE CHROMIUM_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE)
28string(REGEX MATCH "\\[(.*)\\]" _ ${CHROMIUM_VERSION})
29set(CHROMIUM_VERSION ${CMAKE_MATCH_1})
30if(NOT CHROMIUM_VERSION MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$")
31 message(FATAL_ERROR "Invalid chromium version: '${CHROMIUM_VERSION}'")
32endif()
33
34add_subdirectory(Components)26add_subdirectory(Components)
35add_subdirectory(Web)27add_subdirectory(Web)
3628
=== modified file 'src/Ubuntu/Web/UbuntuWebContext.qml'
--- src/Ubuntu/Web/UbuntuWebContext.qml 2016-09-13 16:07:06 +0000
+++ src/Ubuntu/Web/UbuntuWebContext.qml 2016-10-13 14:41:36 +0000
@@ -91,7 +91,13 @@
91 }91 }
92 if (temp !== null) {92 if (temp !== null) {
93 console.log("Loaded %1 UA override(s) from %2".arg(temp.overrides.length).arg(Qt.resolvedUrl(script)))93 console.log("Loaded %1 UA override(s) from %2".arg(temp.overrides.length).arg(Qt.resolvedUrl(script)))
94 userAgentOverrides = temp.overrides94 var chromiumVersion = Oxide.Oxide.chromiumVersion
95 var overrides = []
96 for (var o in temp.overrides) {
97 var override = temp.overrides[o]
98 overrides.push([override[0], override[1].replace(/\$\{CHROMIUM_VERSION\}/g, chromiumVersion)])
99 }
100 userAgentOverrides = overrides
95 temp.destroy()101 temp.destroy()
96 }102 }
97 }103 }
98104
=== modified file 'src/Ubuntu/Web/ua-overrides-desktop.js.in'
--- src/Ubuntu/Web/ua-overrides-desktop.js.in 2016-09-21 16:15:10 +0000
+++ src/Ubuntu/Web/ua-overrides-desktop.js.in 2016-10-13 14:41:36 +0000
@@ -19,17 +19,17 @@
19.pragma library19.pragma library
2020
21var overrides = [21var overrides = [
22 ["^https?:\/\/.+\.google\.com\/calendar", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chromium/@CHROMIUM_VERSION@ Chrome/@CHROMIUM_VERSION@ Safari/537.36"],22 ["^https?:\/\/.+\.google\.com\/calendar", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chromium/${CHROMIUM_VERSION} Chrome/${CHROMIUM_VERSION} Safari/537.36"],
23 ["^http:\/\/chrome\.angrybirds\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Safari/537.36"], // http://pad.lv/128415823 ["^http:\/\/chrome\.angrybirds\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Safari/537.36"], // http://pad.lv/1284158
24 ["^https?:\/\/(www\.)?youtube\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Safari/537.36"], // http://pad.lv/141288024 ["^https?:\/\/(www\.)?youtube\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Safari/537.36"], // http://pad.lv/1412880
25 ["^https?:\/\/(www\.)?google\..+\/maps", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Safari/537.36"], // http://pad.lv/1503506, http://pad.lv/155164925 ["^https?:\/\/(www\.)?google\..+\/maps", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Safari/537.36"], // http://pad.lv/1503506, http://pad.lv/1551649
26 ["^https?:\/\/mail\.google\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Safari/537.36"], // http://pad.lv/145261626 ["^https?:\/\/mail\.google\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Safari/537.36"], // http://pad.lv/1452616
2727
28 // Google hangouts (https://launchpad.net/bugs/1565055)28 // Google hangouts (https://launchpad.net/bugs/1565055)
29 ["^https?:\/\/hangouts\.google\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Safari/537.36"],29 ["^https?:\/\/hangouts\.google\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Safari/537.36"],
30 ["^https?:\/\/talkgadget\.google\.com\/hangouts\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Safari/537.36"],30 ["^https?:\/\/talkgadget\.google\.com\/hangouts\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Safari/537.36"],
31 ["^https?:\/\/plus\.google\.com\/hangouts\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Safari/537.36"],31 ["^https?:\/\/plus\.google\.com\/hangouts\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Safari/537.36"],
3232
33 // Google recaptcha (https://launchpad.net/bugs/1599146)33 // Google recaptcha (https://launchpad.net/bugs/1599146)
34 ["^https:\/\/www\.google\.com\/recaptcha\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Safari/537.36"],34 ["^https:\/\/www\.google\.com\/recaptcha\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Safari/537.36"],
35];35];
3636
=== modified file 'src/Ubuntu/Web/ua-overrides-mobile.js.in'
--- src/Ubuntu/Web/ua-overrides-mobile.js.in 2016-08-19 10:10:12 +0000
+++ src/Ubuntu/Web/ua-overrides-mobile.js.in 2016-10-13 14:41:36 +0000
@@ -19,18 +19,18 @@
19.pragma library19.pragma library
2020
21var overrides = [21var overrides = [
22 ["^https?:\/\/mail\.google\.com\/", "Mozilla/5.0 (Linux; Android 5.0;) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Mobile Safari/537.36"], // http://pad.lv/137588922 ["^https?:\/\/mail\.google\.com\/", "Mozilla/5.0 (Linux; Android 5.0;) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Mobile Safari/537.36"], // http://pad.lv/1375889
23 ["^https?:\/\/(www|m)\.youtube\.com\/", "Mozilla/5.0 (Linux; Android 5.0;) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Mobile Safari/537.36"], // http://pad.lv/1228415, http://pad.lv/1415107, http://pad.lv/1417258, http://pad.lv/1499394, http://pad.lv/1408760, http://pad.lv/143748523 ["^https?:\/\/(www|m)\.youtube\.com\/", "Mozilla/5.0 (Linux; Android 5.0;) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Mobile Safari/537.36"], // http://pad.lv/1228415, http://pad.lv/1415107, http://pad.lv/1417258, http://pad.lv/1499394, http://pad.lv/1408760, http://pad.lv/1437485
24 ["^http:\/\/chrome\.angrybirds\.com\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@ like Android 4.4;) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Mobile Safari/537.36"], // http://pad.lv/128415824 ["^http:\/\/chrome\.angrybirds\.com\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@ like Android 4.4;) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Mobile Safari/537.36"], // http://pad.lv/1284158
25 ["^https?:\/\/(\w+\.)*hsbc\.com\.br\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@ like Android 4.4;) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Mobile Safari/537.36"], // http://pad.lv/138065725 ["^https?:\/\/(\w+\.)*hsbc\.com\.br\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@ like Android 4.4;) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Mobile Safari/537.36"], // http://pad.lv/1380657
26 ["^http:\/\/(\w+\.)*espn\.(go\.)?com\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@ like Android 4.4;) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Mobile Safari/537.36"], // http://pad.lv/131625926 ["^http:\/\/(\w+\.)*espn\.(go\.)?com\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@ like Android 4.4;) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Mobile Safari/537.36"], // http://pad.lv/1316259
27 ["^https?:\/\/(www|m)\.facebook\.com\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@; Android 5.0; Nexus 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/@CHROMIUM_VERSION@ Mobile Safari/537.36"], // http://pad.lv/1538056, http://pad.lv/145766127 ["^https?:\/\/(www|m)\.facebook\.com\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@; Android 5.0; Nexus 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Mobile Safari/537.36"], // http://pad.lv/1538056, http://pad.lv/1457661
28 ["^https?:\/\/(mobile\.)?nytimes\.com\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@; Android 5.0; Nexus 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/@CHROMIUM_VERSION@ Mobile Safari/537.36"], // http://pad.lv/157362028 ["^https?:\/\/(mobile\.)?nytimes\.com\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@; Android 5.0; Nexus 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Mobile Safari/537.36"], // http://pad.lv/1573620
29 // Google hangouts (https://launchpad.net/bugs/1565055)29 // Google hangouts (https://launchpad.net/bugs/1565055)
30 ["^https?:\/\/hangouts\.google\.com\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@ like Android 4.4;) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Mobile Safari/537.36"],30 ["^https?:\/\/hangouts\.google\.com\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@ like Android 4.4;) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Mobile Safari/537.36"],
31 ["^https?:\/\/talkgadget\.google\.com\/hangouts\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@ like Android 4.4;) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Mobile Safari/537.36"],31 ["^https?:\/\/talkgadget\.google\.com\/hangouts\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@ like Android 4.4;) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Mobile Safari/537.36"],
32 ["^https?:\/\/plus\.google\.com\/hangouts\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@ like Android 4.4;) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Mobile Safari/537.36"],32 ["^https?:\/\/plus\.google\.com\/hangouts\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@ like Android 4.4;) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Mobile Safari/537.36"],
3333
34 // Google recaptcha (https://launchpad.net/bugs/1599146)34 // Google recaptcha (https://launchpad.net/bugs/1599146)
35 ["^https:\/\/www\.google\.com\/recaptcha\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@ like Android 4.4;) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Safari/537.36"],35 ["^https:\/\/www\.google\.com\/recaptcha\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@ like Android 4.4;) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Safari/537.36"],
36];36];
3737
=== modified file 'src/app/AlertDialog.qml'
--- src/app/AlertDialog.qml 2016-05-23 13:25:46 +0000
+++ src/app/AlertDialog.qml 2016-10-13 14:41:36 +0000
@@ -20,11 +20,13 @@
20import Ubuntu.Components 1.320import Ubuntu.Components 1.3
2121
22ModalDialog {22ModalDialog {
23 objectName: "alertDialog"
23 title: i18n.tr("JavaScript Alert")24 title: i18n.tr("JavaScript Alert")
2425
25 Button {26 Button {
26 text: i18n.tr("OK")27 text: i18n.tr("OK")
27 color: theme.palette.normal.positive28 color: theme.palette.normal.positive
29 objectName: "okButton"
28 onClicked: model.accept()30 onClicked: model.accept()
29 }31 }
30}32}
3133
=== modified file 'src/app/BeforeUnloadDialog.qml'
--- src/app/BeforeUnloadDialog.qml 2016-05-23 13:25:46 +0000
+++ src/app/BeforeUnloadDialog.qml 2016-10-13 14:41:36 +0000
@@ -20,15 +20,18 @@
20import Ubuntu.Components 1.320import Ubuntu.Components 1.3
2121
22ModalDialog {22ModalDialog {
23 objectName: "beforeUnloadDialog"
23 title: i18n.tr("Confirm Navigation")24 title: i18n.tr("Confirm Navigation")
2425
25 Button {26 Button {
26 text: i18n.tr("Leave")27 text: i18n.tr("Leave")
27 color: theme.palette.normal.negative28 color: theme.palette.normal.negative
29 objectName: "leaveButton"
28 onClicked: model.accept()30 onClicked: model.accept()
29 }31 }
3032
31 Button {33 Button {
34 objectName: "stayButton"
32 text: i18n.tr("Stay")35 text: i18n.tr("Stay")
33 onClicked: model.reject()36 onClicked: model.reject()
34 }37 }
3538
=== modified file 'src/app/ChromeBase.qml'
--- src/app/ChromeBase.qml 2015-12-02 18:01:18 +0000
+++ src/app/ChromeBase.qml 2016-10-13 14:41:36 +0000
@@ -1,5 +1,5 @@
1/*1/*
2 * Copyright 2014-2015 Canonical Ltd.2 * Copyright 2014-2016 Canonical Ltd.
3 *3 *
4 * This file is part of webbrowser-app.4 * This file is part of webbrowser-app.
5 *5 *
@@ -25,9 +25,11 @@
2525
26 objectName: "chromeBase"26 objectName: "chromeBase"
2727
28 property var webview
29 property alias backgroundColor: backgroundRect.color28 property alias backgroundColor: backgroundRect.color
3029
30 property alias loading: progressBar.visible
31 property alias loadProgress: progressBar.value
32
31 states: [33 states: [
32 State {34 State {
33 name: "shown"35 name: "shown"
@@ -53,7 +55,7 @@
53 }55 }
5456
55 ThinProgressBar {57 ThinProgressBar {
56 webview: chrome.webview58 id: progressBar
5759
58 anchors {60 anchors {
59 left: parent.left61 left: parent.left
6062
=== modified file 'src/app/ConfirmDialog.qml'
--- src/app/ConfirmDialog.qml 2016-05-23 13:25:46 +0000
+++ src/app/ConfirmDialog.qml 2016-10-13 14:41:36 +0000
@@ -20,15 +20,18 @@
20import Ubuntu.Components 1.320import Ubuntu.Components 1.3
2121
22ModalDialog {22ModalDialog {
23 objectName: "confirmDialog"
23 title: i18n.tr("JavaScript Confirmation")24 title: i18n.tr("JavaScript Confirmation")
2425
25 Button {26 Button {
26 text: i18n.tr("OK")27 text: i18n.tr("OK")
27 color: theme.palette.normal.positive28 color: theme.palette.normal.positive
29 objectName: "okButton"
28 onClicked: model.accept()30 onClicked: model.accept()
29 }31 }
3032
31 Button {33 Button {
34 objectName: "cancelButton"
32 text: i18n.tr("Cancel")35 text: i18n.tr("Cancel")
33 onClicked: model.reject()36 onClicked: model.reject()
34 }37 }
3538
=== modified file 'src/app/Downloader.qml'
--- src/app/Downloader.qml 2016-01-12 14:50:27 +0000
+++ src/app/Downloader.qml 2016-10-13 14:41:36 +0000
@@ -34,9 +34,7 @@
3434
35 Component {35 Component {
36 id: metadataComponent36 id: metadataComponent
37 Metadata {37 Metadata {}
38 showInIndicator: true
39 }
40 }38 }
4139
42 Component {40 Component {
@@ -62,14 +60,8 @@
62 singleDownload.download(url)60 singleDownload.download(url)
63 }61 }
6462
65 function downloadPicture(url, headers) {63 function downloadMimeType(url, mimeType, headers, filename, incognito) {
66 var metadata = metadataComponent.createObject(downloadItem)64 var metadata = metadataComponent.createObject(downloadItem, {"showInIndicator": !incognito})
67 downloadItem.mimeType = "image/*"
68 download(url, ContentType.Pictures, headers, metadata)
69 }
70
71 function downloadMimeType(url, mimeType, headers, filename) {
72 var metadata = metadataComponent.createObject(downloadItem)
73 var contentType = MimeTypeMapper.mimeTypeToContentType(mimeType)65 var contentType = MimeTypeMapper.mimeTypeToContentType(mimeType)
74 if (contentType == ContentType.Unknown && filename) {66 if (contentType == ContentType.Unknown && filename) {
75 // If we can't determine the content type from the mime-type67 // If we can't determine the content type from the mime-type
7668
=== modified file 'src/app/PromptDialog.qml'
--- src/app/PromptDialog.qml 2016-05-23 13:25:46 +0000
+++ src/app/PromptDialog.qml 2016-10-13 14:41:36 +0000
@@ -20,10 +20,12 @@
20import Ubuntu.Components 1.320import Ubuntu.Components 1.3
2121
22ModalDialog {22ModalDialog {
23 objectName: "promptDialog"
23 title: i18n.tr("JavaScript Prompt")24 title: i18n.tr("JavaScript Prompt")
2425
25 TextField {26 TextField {
26 id: input27 id: input
28 objectName: "inputTextField"
27 text: model.defaultValue29 text: model.defaultValue
28 onAccepted: model.accept(input.text)30 onAccepted: model.accept(input.text)
29 }31 }
@@ -31,10 +33,12 @@
31 Button {33 Button {
32 text: i18n.tr("OK")34 text: i18n.tr("OK")
33 color: theme.palette.normal.positive35 color: theme.palette.normal.positive
36 objectName: "okButton"
34 onClicked: model.accept(input.text)37 onClicked: model.accept(input.text)
35 }38 }
3639
37 Button {40 Button {
41 objectName: "cancelButton"
38 text: i18n.tr("Cancel")42 text: i18n.tr("Cancel")
39 onClicked: model.reject()43 onClicked: model.reject()
40 }44 }
4145
=== modified file 'src/app/ThinProgressBar.qml'
--- src/app/ThinProgressBar.qml 2015-08-10 15:22:00 +0000
+++ src/app/ThinProgressBar.qml 2016-10-13 14:41:36 +0000
@@ -1,5 +1,5 @@
1/*1/*
2 * Copyright 2014-2015 Canonical Ltd.2 * Copyright 2014-2016 Canonical Ltd.
3 *3 *
4 * This file is part of webbrowser-app.4 * This file is part of webbrowser-app.
5 *5 *
@@ -20,11 +20,8 @@
20import Ubuntu.Components 1.320import Ubuntu.Components 1.3
2121
22ProgressBar {22ProgressBar {
23 property var webview
24
25 height: units.dp(3)23 height: units.dp(3)
26
27 showProgressPercentage: false24 showProgressPercentage: false
28 value: webview ? webview.loadProgress / 100 : 0.025 minimumValue: 0
29 visible: webview ? webview.loading : false26 maximumValue: 100
30}27}
3128
=== modified file 'src/app/WebViewImpl.qml'
--- src/app/WebViewImpl.qml 2015-12-15 12:37:34 +0000
+++ src/app/WebViewImpl.qml 2016-10-13 14:41:36 +0000
@@ -1,5 +1,5 @@
1/*1/*
2 * Copyright 2013-2015 Canonical Ltd.2 * Copyright 2013-2016 Canonical Ltd.
3 *3 *
4 * This file is part of webbrowser-app.4 * This file is part of webbrowser-app.
5 *5 *
@@ -76,7 +76,7 @@
76 mimeType = MimeDatabase.filenameToMimeType(filename)76 mimeType = MimeDatabase.filenameToMimeType(filename)
77 }77 }
78 }78 }
79 downloadLoader.item.downloadMimeType(request.url, mimeType, headers, request.suggestedFilename)79 downloadLoader.item.downloadMimeType(request.url, mimeType, headers, request.suggestedFilename, incognito)
80 } else {80 } else {
81 // Desktop form factor case81 // Desktop form factor case
82 Qt.openUrlExternally(request.url)82 Qt.openUrlExternally(request.url)
8383
=== modified file 'src/app/config.h.in'
--- src/app/config.h.in 2016-05-26 17:00:55 +0000
+++ src/app/config.h.in 2016-10-13 14:41:36 +0000
@@ -22,19 +22,20 @@
22#include <QtCore/QCoreApplication>22#include <QtCore/QCoreApplication>
23#include <QtCore/QDir>23#include <QtCore/QDir>
24#include <QtCore/QString>24#include <QtCore/QString>
25#include <QtCore/QtGlobal>
2526
26#define REMOTE_INSPECTOR_PORT 922127#define REMOTE_INSPECTOR_PORT 9221
2728
28inline bool isRunningInstalled()29inline bool isRunningInstalled()
29{30{
30 static bool installed = (QCoreApplication::applicationDirPath() == QDir("@CMAKE_INSTALL_FULL_BINDIR@").canonicalPath());31 static bool installed = (QCoreApplication::applicationDirPath() == QDir(qgetenv("SNAP").append("@CMAKE_INSTALL_FULL_BINDIR@")).canonicalPath());
31 return installed;32 return installed;
32}33}
3334
34inline QString UbuntuBrowserDirectory()35inline QString UbuntuBrowserDirectory()
35{36{
36 if (isRunningInstalled()) {37 if (isRunningInstalled()) {
37 return QStringLiteral("@CMAKE_INSTALL_FULL_DATADIR@/webbrowser-app");38 return qgetenv("SNAP").append("@CMAKE_INSTALL_FULL_DATADIR@/webbrowser-app");
38 } else {39 } else {
39 return QStringLiteral("@CMAKE_SOURCE_DIR@/src/app");40 return QStringLiteral("@CMAKE_SOURCE_DIR@/src/app");
40 }41 }
4142
=== modified file 'src/app/webbrowser/BookmarkOptions.qml'
--- src/app/webbrowser/BookmarkOptions.qml 2016-05-23 02:52:50 +0000
+++ src/app/webbrowser/BookmarkOptions.qml 2016-10-13 14:41:36 +0000
@@ -19,18 +19,36 @@
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 webbrowserapp.private 0.1
2223
23Popover {24Popover {
24 id: bookmarkOptions25 id: bookmarkOptions
2526
26 property url bookmarkUrl27 property url bookmarkUrl
27 property alias bookmarkTitle: titleTextField.text28 property alias bookmarkTitle: titleTextField.text
28 property alias folderModel: folderOptionSelector.model
2929
30 readonly property string bookmarkFolder: folderModel.get(folderOptionSelector.selectedIndex).folder30 readonly property string bookmarkFolder: folderOptionSelector.model.get(folderOptionSelector.selectedIndex).folder
3131
32 contentHeight: bookmarkOptionsColumn.childrenRect.height + units.gu(2)32 contentHeight: bookmarkOptionsColumn.childrenRect.height + units.gu(2)
3333
34 onVisibleChanged: {
35 if (!visible) {
36 BookmarksModel.remove(bookmarkUrl)
37 }
38 }
39
40 Component.onDestruction: {
41 if (BookmarksModel.contains(bookmarkUrl)) {
42 BookmarksModel.update(bookmarkUrl, bookmarkTitle, bookmarkFolder)
43 }
44 }
45
46 // Fragile workaround for https://launchpad.net/bugs/1546677.
47 // By destroying the popover, its visibility isn’t changed to
48 // false, and thus the bookmark is not removed.
49 Keys.onEnterPressed: bookmarkOptions.destroy()
50 Keys.onReturnPressed: bookmarkOptions.destroy()
51
34 Column {52 Column {
35 id: bookmarkOptionsColumn53 id: bookmarkOptionsColumn
3654
@@ -77,6 +95,9 @@
7795
78 delegate: OptionSelectorDelegate { text: folder === "" ? i18n.tr("All Bookmarks") : folder }96 delegate: OptionSelectorDelegate { text: folder === "" ? i18n.tr("All Bookmarks") : folder }
79 containerHeight: itemHeight * 397 containerHeight: itemHeight * 3
98 model: BookmarksFolderListModel {
99 sourceModel: BookmarksModel
100 }
80 }101 }
81102
82 Item {103 Item {
@@ -120,8 +141,8 @@
120141
121 function createNewFolder(folder) {142 function createNewFolder(folder) {
122 Qt.inputMethod.hide()143 Qt.inputMethod.hide()
123 folderModel.createNewFolder(folder)144 folderOptionSelector.model.createNewFolder(folder)
124 folderOptionSelector.selectedIndex = folderModel.indexOf(folder) 145 folderOptionSelector.selectedIndex = folderOptionSelector.model.indexOf(folder)
125 folderOptionSelector.currentlyExpanded = false146 folderOptionSelector.currentlyExpanded = false
126 PopupUtils.close(dialogue)147 PopupUtils.close(dialogue)
127 }148 }
128149
=== modified file 'src/app/webbrowser/Browser.qml'
--- src/app/webbrowser/Browser.qml 2016-09-28 08:24:06 +0000
+++ src/app/webbrowser/Browser.qml 2016-10-13 14:41:36 +0000
@@ -88,7 +88,7 @@
88 tabs will have their mediaAccessPermissionRequested signal handled by88 tabs will have their mediaAccessPermissionRequested signal handled by
89 creating one of these new dialogs.89 creating one of these new dialogs.
90 */90 */
91 onMediaAccessPermissionRequested: PopupUtils.open(mediaAccessDialogComponent, null, { request: request })91 onMediaAccessPermissionRequested: PopupUtils.open(Qt.resolvedUrl("../MediaAccessDialog.qml"), null, { request: request })
92 }92 }
9393
94 currentWebcontext: SharedWebContext.sharedContext94 currentWebcontext: SharedWebContext.sharedContext
@@ -119,11 +119,6 @@
119 id: keyboardModel119 id: keyboardModel
120 }120 }
121121
122 Component {
123 id: mediaAccessDialogComponent
124 MediaAccessDialog {}
125 }
126
127 actions: [122 actions: [
128 Actions.GoTo {123 Actions.GoTo {
129 onTriggered: currentWebview.url = value124 onTriggered: currentWebview.url = value
@@ -200,12 +195,16 @@
200 fill: tabContainer195 fill: tabContainer
201 topMargin: (chrome.state == "shown") ? chrome.height : 0196 topMargin: (chrome.state == "shown") ? chrome.height : 0
202 }197 }
203 sourceComponent: ErrorSheet {198 Component.onCompleted: setSource("../ErrorSheet.qml", {
204 visible: currentWebview ? currentWebview.lastLoadFailed : false199 "visible": Qt.binding(function(){ return currentWebview ? currentWebview.lastLoadFailed : false }),
205 url: currentWebview ? currentWebview.url : ""200 "url": Qt.binding(function(){ return currentWebview ? currentWebview.url : "" })
201 })
202 Connections {
203 target: errorSheetLoader.item
206 onRefreshClicked: currentWebview.reload()204 onRefreshClicked: currentWebview.reload()
207 }205 }
208 focus: item.visible206
207 focus: item && item.visible
209 asynchronous: true208 asynchronous: true
210 }209 }
211210
@@ -215,9 +214,12 @@
215 fill: tabContainer214 fill: tabContainer
216 topMargin: (chrome.state == "shown") ? chrome.height : 0215 topMargin: (chrome.state == "shown") ? chrome.height : 0
217 }216 }
218 sourceComponent: InvalidCertificateErrorSheet {217 Component.onCompleted: setSource("../InvalidCertificateErrorSheet.qml", {
219 visible: currentWebview && currentWebview.certificateError != null218 "visible": Qt.binding(function(){ return currentWebview && currentWebview.certificateError != null }),
220 certificateError: currentWebview ? currentWebview.certificateError : null219 "certificateError": Qt.binding(function(){ return currentWebview ? currentWebview.certificateError : null })
220 })
221 Connections {
222 target: invalidCertificateErrorSheetLoader.item
221 onAllowed: {223 onAllowed: {
222 // Automatically allow future requests involving this224 // Automatically allow future requests involving this
223 // certificate for the duration of the session.225 // certificate for the duration of the session.
@@ -228,7 +230,7 @@
228 currentWebview.resetCertificateError()230 currentWebview.resetCertificateError()
229 }231 }
230 }232 }
231 focus: item.visible233 focus: item && item.visible
232 asynchronous: true234 asynchronous: true
233 }235 }
234236
@@ -262,60 +264,35 @@
262 newTabViewLoader.active = !tab.url.toString() && !tab.restoreState264 newTabViewLoader.active = !tab.url.toString() && !tab.restoreState
263 }265 }
264 }266 }
265 }267 onWideChanged: newTabViewLoader.selectTabView()
266268 }
267 sourceComponent: browser.incognito ? newPrivateTabView :269 Component.onCompleted: newTabViewLoader.selectTabView()
268 (browser.wide ? newTabViewWide : newTabView)270
269271 function selectTabView() {
270 Component {272 var source = browser.incognito ? "NewPrivateTabView.qml" :
271 id: newTabView273 (browser.wide ? "NewTabViewWide.qml" :
272274 "NewTabView.qml");
273 NewTabView {275 var properties = browser.incognito ? {} : {"settingsObject": settings,
274 anchors.fill: parent276 "focus": true};
275 settingsObject: settings277
276 focus: true278 newTabViewLoader.setSource(source, properties);
277 onBookmarkClicked: {279 }
278 chrome.requestedUrl = url280
279 currentWebview.url = url281 Connections {
280 tabContainer.forceActiveFocus()282 target: newTabViewLoader.item && !browser.incognito ? newTabViewLoader.item : null
281 }283 onBookmarkClicked: {
282 onBookmarkRemoved: BookmarksModel.remove(url)284 chrome.requestedUrl = url
283 onHistoryEntryClicked: {285 currentWebview.url = url
284 chrome.requestedUrl = url286 tabContainer.forceActiveFocus()
285 currentWebview.url = url287 }
286 tabContainer.forceActiveFocus()288 onBookmarkRemoved: BookmarksModel.remove(url)
287 }289 onHistoryEntryClicked: {
288 Keys.onUpPressed: chrome.focus = true290 chrome.requestedUrl = url
289 }291 currentWebview.url = url
290 }292 tabContainer.forceActiveFocus()
291293 }
292 Component {294 }
293 id: newTabViewWide295 Keys.onUpPressed: chrome.focus = true
294
295 NewTabViewWide {
296 anchors.fill: parent
297 settingsObject: settings
298 focus: true
299 onBookmarkClicked: {
300 chrome.requestedUrl = url
301 currentWebview.url = url
302 tabContainer.forceActiveFocus()
303 }
304 onBookmarkRemoved: BookmarksModel.remove(url)
305 onHistoryEntryClicked: {
306 chrome.requestedUrl = url
307 currentWebview.url = url
308 tabContainer.forceActiveFocus()
309 }
310 Keys.onUpPressed: chrome.focus = true
311 }
312 }
313
314 Component {
315 id: newPrivateTabView
316
317 NewPrivateTabView { anchors.fill: parent }
318 }
319 }296 }
320297
321 Loader {298 Loader {
@@ -328,8 +305,11 @@
328 active: webProcessMonitor.crashed || (webProcessMonitor.killed && !currentWebview.loading)305 active: webProcessMonitor.crashed || (webProcessMonitor.killed && !currentWebview.loading)
329 focus: active306 focus: active
330307
331 sourceComponent: SadTab {308 Component.onCompleted: setSource("SadTab.qml", {
332 webview: currentWebview309 "webview": Qt.binding(function () {return browser.currentWebview})
310 })
311 Connections {
312 target: sadTabLoader.item
333 onCloseTabRequested: internal.closeCurrentTab()313 onCloseTabRequested: internal.closeCurrentTab()
334 }314 }
335315
@@ -477,7 +457,6 @@
477 id: chrome457 id: chrome
478458
479 tab: internal.nextTab || tabsModel.currentTab459 tab: internal.nextTab || tabsModel.currentTab
480 webview: tab ? tab.webview : null
481 tabsModel: browser.tabsModel460 tabsModel: browser.tabsModel
482 searchUrl: currentSearchEngine.urlTemplate461 searchUrl: currentSearchEngine.urlTemplate
483462
@@ -800,7 +779,19 @@
800779
801 anchors.fill: parent780 anchors.fill: parent
802 active: false781 active: false
803 sourceComponent: browser.wide ? bookmarksViewWideComponent : bookmarksViewComponent782 asynchronous: true
783 Connections {
784 target: browser
785 onWideChanged: bookmarksViewLoader.selectBookmarksView()
786 }
787 Component.onCompleted: bookmarksViewLoader.selectBookmarksView()
788
789 function selectBookmarksView() {
790 bookmarksViewLoader.setSource(browser.wide ? "BookmarksViewWide.qml" : "BookmarksView.qml",
791 {"focus": true,
792 "homepageUrl": Qt.binding(function () {return settings.homepage})
793 });
794 }
804795
805 onStatusChanged: {796 onStatusChanged: {
806 if (status == Loader.Ready) {797 if (status == Loader.Ready) {
@@ -824,26 +815,6 @@
824 bookmarksViewLoader.active = false815 bookmarksViewLoader.active = false
825 }816 }
826 }817 }
827
828 Component {
829 id: bookmarksViewComponent
830
831 BookmarksView {
832 anchors.fill: parent
833 focus: true
834 homepageUrl: settings.homepage
835 }
836 }
837
838 Component {
839 id: bookmarksViewWideComponent
840
841 BookmarksViewWide {
842 anchors.fill: parent
843 focus: true
844 homepageUrl: settings.homepage
845 }
846 }
847 }818 }
848819
849 Loader {820 Loader {
@@ -851,7 +822,34 @@
851822
852 anchors.fill: parent823 anchors.fill: parent
853 active: false824 active: false
854 sourceComponent: browser.wide ? historyViewWideComponent : historyViewComponent825 asynchronous: true
826 Connections {
827 target: browser
828 onWideChanged: historyViewLoader.selectHistoryView()
829 }
830 Component.onCompleted: historyViewLoader.selectHistoryView()
831
832 function selectHistoryView() {
833 historyViewLoader.setSource(browser.wide ? "HistoryViewWide.qml" : "HistoryViewWithExpansion.qml",
834 {"focus": true});
835 }
836
837 Connections {
838 target: historyViewLoader.item
839 onHistoryEntryClicked: {
840 historyViewLoader.active = false
841 internal.openUrlInNewTab(url, true)
842 }
843 onNewTabRequested: {
844 historyViewLoader.active = false
845 internal.openUrlInNewTab("", true)
846 }
847 onDone: {
848 historyViewLoader.active = false
849 internal.resetFocus()
850 }
851 onBack: historyViewLoader.active = false
852 }
855853
856 onStatusChanged: {854 onStatusChanged: {
857 if (status == Loader.Ready) {855 if (status == Loader.Ready) {
@@ -862,75 +860,6 @@
862 internal.resetFocus()860 internal.resetFocus()
863 }861 }
864 }862 }
865
866 Component {
867 id: historyViewComponent
868
869 FocusScope {
870 focus: true
871
872 signal loadModel()
873 onLoadModel: children[0].loadModel()
874
875 HistoryView {
876 anchors.fill: parent
877 focus: !expandedHistoryViewLoader.focus
878 visible: focus
879 onSeeMoreEntriesClicked: {
880 expandedHistoryViewLoader.model = model
881 expandedHistoryViewLoader.active = true
882 }
883 onNewTabRequested: internal.openUrlInNewTab("", true)
884 onBack: historyViewLoader.active = false
885 }
886
887 Loader {
888 id: expandedHistoryViewLoader
889 asynchronous: true
890 anchors.fill: parent
891 active: false
892 focus: active
893 property var model: null
894 sourceComponent: ExpandedHistoryView {
895 focus: true
896 model: expandedHistoryViewLoader.model
897 onHistoryEntryClicked: {
898 internal.openUrlInNewTab(url, true)
899 historyViewLoader.active = false
900 }
901 onHistoryEntryRemoved: {
902 if (count == 1) {
903 done()
904 }
905 HistoryModel.removeEntryByUrl(url)
906 }
907 onDone: expandedHistoryViewLoader.active = false
908 }
909 }
910 }
911 }
912
913 Component {
914 id: historyViewWideComponent
915
916 HistoryViewWide {
917 anchors.fill: parent
918 focus: true
919
920 onHistoryEntryClicked: {
921 historyViewLoader.active = false
922 internal.openUrlInNewTab(url, true)
923 }
924 onNewTabRequested: {
925 historyViewLoader.active = false
926 internal.openUrlInNewTab("", true)
927 }
928 onDone: {
929 historyViewLoader.active = false
930 internal.resetFocus()
931 }
932 }
933 }
934 }863 }
935864
936 Loader {865 Loader {
@@ -938,6 +867,7 @@
938867
939 anchors.fill: parent868 anchors.fill: parent
940 active: false869 active: false
870 asynchronous: true
941871
942 onStatusChanged: {872 onStatusChanged: {
943 if (status == Loader.Ready) {873 if (status == Loader.Ready) {
@@ -948,10 +878,12 @@
948 }878 }
949 }879 }
950880
951 sourceComponent: SettingsPage {881 Component.onCompleted: setSource("SettingsPage.qml", {
952 anchors.fill: parent882 "focus": true,
953 focus: true883 "settingsObject": settings
954 settingsObject: settings884 })
885 Connections {
886 target: settingsViewLoader.item
955 onDone: settingsViewLoader.active = false887 onDone: settingsViewLoader.active = false
956 }888 }
957 }889 }
@@ -961,18 +893,15 @@
961893
962 anchors.fill: parent894 anchors.fill: parent
963 active: false895 active: false
964 source: "DownloadsPage.qml"896 asynchronous: true
897 Component.onCompleted: {
898 setSource("DownloadsPage.qml", {
899 "downloadManager": Qt.binding(function () {return downloadHandlerLoader.item}),
900 "incognito": incognito,
901 "focus": true
902 })
903 }
965904
966 Binding {
967 target: downloadsViewLoader.item
968 property: "downloadManager"
969 value: downloadHandlerLoader.item
970 }
971 Binding {
972 target: downloadsViewLoader.item
973 property: "focus"
974 value: true
975 }
976 Connections {905 Connections {
977 target: downloadsViewLoader.item906 target: downloadsViewLoader.item
978 onDone: downloadsViewLoader.active = false907 onDone: downloadsViewLoader.active = false
@@ -993,424 +922,10 @@
993 asynchronous: true922 asynchronous: true
994 }923 }
995924
996 Component {925 property Component tabComponent
997 id: tabComponent926 Loader {
998927 source: "TabComponent.qml"
999 BrowserTab {928 onLoaded: tabComponent = item
1000 anchors.fill: parent
1001 incognito: browser.incognito
1002 current: tabsModel && tabsModel.currentTab === this
1003 focus: current
1004
1005 Item {
1006 id: contextualMenuTarget
1007 visible: false
1008 }
1009
1010 webviewComponent: WebViewImpl {
1011 id: webviewimpl
1012
1013 property BrowserTab tab
1014 readonly property bool current: tab.current
1015
1016 currentWebview: browser.currentWebview
1017 filePicker: filePickerLoader.item
1018
1019 anchors.fill: parent
1020
1021 focus: true
1022
1023 enabled: current && !bottomEdgeHandle.dragging && !recentView.visible
1024
1025 locationBarController {
1026 height: chrome.height
1027 mode: chromeController.defaultMode
1028 }
1029
1030 //experimental.preferences.developerExtrasEnabled: developerExtrasEnabled
1031 preferences.localStorageEnabled: true
1032 preferences.appCacheEnabled: true
1033
1034 property QtObject contextModel: null
1035 contextualActions: ActionList {
1036 Actions.OpenLinkInNewTab {
1037 objectName: "OpenLinkInNewTabContextualAction"
1038 enabled: contextModel && contextModel.linkUrl.toString()
1039 onTriggered: internal.openUrlInNewTab(contextModel.linkUrl, true)
1040 }
1041 Actions.OpenLinkInNewBackgroundTab {
1042 objectName: "OpenLinkInNewBackgroundTabContextualAction"
1043 enabled: contextModel && contextModel.linkUrl.toString()
1044 onTriggered: internal.openUrlInNewTab(contextModel.linkUrl, false)
1045 }
1046 Actions.OpenLinkInNewWindow {
1047 objectName: "OpenLinkInNewWindowContextualAction"
1048 enabled: contextModel && contextModel.linkUrl.toString()
1049 onTriggered: browser.openLinkInWindowRequested(contextModel.linkUrl, false)
1050 }
1051 Actions.OpenLinkInPrivateWindow {
1052 objectName: "OpenLinkInPrivateWindowContextualAction"
1053 enabled: contextModel && contextModel.linkUrl.toString()
1054 onTriggered: browser.openLinkInWindowRequested(contextModel.linkUrl, true)
1055 }
1056 Actions.BookmarkLink {
1057 objectName: "BookmarkLinkContextualAction"
1058 enabled: contextModel && contextModel.linkUrl.toString()
1059 && !BookmarksModel.contains(contextModel.linkUrl)
1060 onTriggered: {
1061 // position the menu target with a one-off assignement instead of a binding
1062 // since the contents of the contextModel have meaning only while the context
1063 // menu is active
1064 contextualMenuTarget.x = contextModel.position.x
1065 contextualMenuTarget.y = contextModel.position.y + locationBarController.height + locationBarController.offset
1066 internal.addBookmark(contextModel.linkUrl, contextModel.linkText,
1067 "", contextualMenuTarget)
1068 }
1069 }
1070 Actions.CopyLink {
1071 objectName: "CopyLinkContextualAction"
1072 enabled: contextModel && contextModel.linkUrl.toString()
1073 onTriggered: Clipboard.push(["text/plain", contextModel.linkUrl.toString()])
1074 }
1075 Actions.SaveLink {
1076 objectName: "SaveLinkContextualAction"
1077 enabled: contextModel && contextModel.linkUrl.toString()
1078 onTriggered: contextModel.saveLink()
1079 }
1080 Actions.Share {
1081 objectName: "ShareContextualAction"
1082 enabled: (contentHandlerLoader.status == Loader.Ready) && contextModel &&
1083 (contextModel.linkUrl.toString() || contextModel.selectionText)
1084 onTriggered: {
1085 if (contextModel.linkUrl.toString()) {
1086 internal.shareLink(contextModel.linkUrl.toString(), contextModel.linkText)
1087 } else if (contextModel.selectionText) {
1088 internal.shareText(contextModel.selectionText)
1089 }
1090 }
1091 }
1092 Actions.OpenImageInNewTab {
1093 objectName: "OpenImageInNewTabContextualAction"
1094 enabled: contextModel &&
1095 (contextModel.mediaType === Oxide.WebView.MediaTypeImage) &&
1096 contextModel.srcUrl.toString()
1097 onTriggered: internal.openUrlInNewTab(contextModel.srcUrl, true)
1098 }
1099 Actions.CopyImage {
1100 objectName: "CopyImageContextualAction"
1101 enabled: contextModel &&
1102 (contextModel.mediaType === Oxide.WebView.MediaTypeImage) &&
1103 contextModel.srcUrl.toString()
1104 onTriggered: Clipboard.push(["text/plain", contextModel.srcUrl.toString()])
1105 }
1106 Actions.SaveImage {
1107 objectName: "SaveImageContextualAction"
1108 enabled: contextModel &&
1109 ((contextModel.mediaType === Oxide.WebView.MediaTypeImage) ||
1110 (contextModel.mediaType === Oxide.WebView.MediaTypeCanvas)) &&
1111 contextModel.hasImageContents
1112 onTriggered: contextModel.saveMedia()
1113 }
1114 Actions.OpenVideoInNewTab {
1115 objectName: "OpenVideoInNewTabContextualAction"
1116 enabled: contextModel &&
1117 (contextModel.mediaType === Oxide.WebView.MediaTypeVideo) &&
1118 contextModel.srcUrl.toString()
1119 onTriggered: internal.openUrlInNewTab(contextModel.srcUrl, true)
1120 }
1121 Actions.SaveVideo {
1122 objectName: "SaveVideoContextualAction"
1123 enabled: contextModel &&
1124 (contextModel.mediaType === Oxide.WebView.MediaTypeVideo) &&
1125 contextModel.srcUrl.toString()
1126 onTriggered: contextModel.saveMedia()
1127 }
1128 Actions.Undo {
1129 objectName: "UndoContextualAction"
1130 enabled: contextModel && contextModel.isEditable &&
1131 (contextModel.editFlags & Oxide.WebView.UndoCapability)
1132 onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandUndo)
1133 }
1134 Actions.Redo {
1135 objectName: "RedoContextualAction"
1136 enabled: contextModel && contextModel.isEditable &&
1137 (contextModel.editFlags & Oxide.WebView.RedoCapability)
1138 onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandRedo)
1139 }
1140 Actions.Cut {
1141 objectName: "CutContextualAction"
1142 enabled: contextModel && contextModel.isEditable &&
1143 (contextModel.editFlags & Oxide.WebView.CutCapability)
1144 onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandCut)
1145 }
1146 Actions.Copy {
1147 objectName: "CopyContextualAction"
1148 enabled: contextModel && (contextModel.selectionText ||
1149 (contextModel.isEditable &&
1150 (contextModel.editFlags & Oxide.WebView.CopyCapability)))
1151 onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandCopy)
1152 }
1153 Actions.Paste {
1154 objectName: "PasteContextualAction"
1155 enabled: contextModel && contextModel.isEditable &&
1156 (contextModel.editFlags & Oxide.WebView.PasteCapability)
1157 onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandPaste)
1158 }
1159 Actions.Erase {
1160 objectName: "EraseContextualAction"
1161 enabled: contextModel && contextModel.isEditable &&
1162 (contextModel.editFlags & Oxide.WebView.EraseCapability)
1163 onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandErase)
1164 }
1165 Actions.SelectAll {
1166 objectName: "SelectAllContextualAction"
1167 enabled: contextModel && contextModel.isEditable &&
1168 (contextModel.editFlags & Oxide.WebView.SelectAllCapability)
1169 onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandSelectAll)
1170 }
1171 }
1172
1173 function contextMenuOnCompleted(menu) {
1174 contextModel = menu.contextModel
1175 if (contextModel.linkUrl.toString() ||
1176 contextModel.srcUrl.toString() ||
1177 contextModel.selectionText ||
1178 (contextModel.isEditable && contextModel.editFlags) ||
1179 (((contextModel.mediaType == Oxide.WebView.MediaTypeImage) ||
1180 (contextModel.mediaType == Oxide.WebView.MediaTypeCanvas)) &&
1181 contextModel.hasImageContents)) {
1182 menu.show()
1183 } else {
1184 contextModel.close()
1185 }
1186 }
1187
1188 Component {
1189 id: contextMenuNarrowComponent
1190 ContextMenuMobile {
1191 actions: contextualActions
1192 Component.onCompleted: webviewimpl.contextMenuOnCompleted(this)
1193 }
1194 }
1195 Component {
1196 id: contextMenuWideComponent
1197 ContextMenuWide {
1198 webview: webviewimpl
1199 parent: browser
1200 actions: contextualActions
1201 Component.onCompleted: webviewimpl.contextMenuOnCompleted(this)
1202 }
1203 }
1204 contextMenu: browser.wide ? contextMenuWideComponent : contextMenuNarrowComponent
1205
1206 onNewViewRequested: {
1207 var tab = tabComponent.createObject(tabContainer, {"request": request})
1208 var setCurrent = (request.disposition == Oxide.NewViewRequest.DispositionNewForegroundTab)
1209 internal.addTab(tab, setCurrent)
1210 if (setCurrent) tabContainer.forceActiveFocus()
1211 }
1212
1213 onCloseRequested: prepareToClose()
1214 onPrepareToCloseResponse: {
1215 if (proceed) {
1216 if (tab) {
1217 for (var i = 0; i < tabsModel.count; ++i) {
1218 if (tabsModel.get(i) === tab) {
1219 tabsModel.remove(i)
1220 break
1221 }
1222 }
1223 tab.close()
1224 }
1225 if (tabsModel.count === 0) {
1226 internal.openUrlInNewTab("", true, true)
1227 }
1228 }
1229 }
1230
1231 QtObject {
1232 id: webviewInternal
1233 property url storedUrl: ""
1234 property bool titleSet: false
1235 property string title: ""
1236 }
1237 onLoadEvent: {
1238 if (event.type == Oxide.LoadEvent.TypeCommitted) {
1239 chrome.findInPageMode = false
1240 webviewInternal.titleSet = false
1241 webviewInternal.title = title
1242 }
1243
1244 if (webviewimpl.incognito) {
1245 return
1246 }
1247
1248 if ((event.type == Oxide.LoadEvent.TypeCommitted) &&
1249 !event.isError &&
1250 (300 > event.httpStatusCode) && (event.httpStatusCode >= 200)) {
1251 webviewInternal.storedUrl = event.url
1252 HistoryModel.add(event.url, title, icon)
1253 }
1254 }
1255 onTitleChanged: {
1256 if (!webviewInternal.titleSet && webviewInternal.storedUrl.toString()) {
1257 // Record the title to avoid updating the history database
1258 // every time the page dynamically updates its title.
1259 // We don’t want pages that update their title every second
1260 // to achieve an ugly "scrolling title" effect to flood the
1261 // history database with updates.
1262 webviewInternal.titleSet = true
1263 webviewInternal.title = title
1264 HistoryModel.update(webviewInternal.storedUrl, title, icon)
1265 }
1266 }
1267 onIconChanged: {
1268 if (webviewInternal.storedUrl.toString()) {
1269 HistoryModel.update(webviewInternal.storedUrl, webviewInternal.title, icon)
1270 }
1271 }
1272
1273 onGeolocationPermissionRequested: requestGeolocationPermission(request)
1274
1275 property var certificateError
1276 function resetCertificateError() {
1277 certificateError = null
1278 }
1279 onCertificateError: {
1280 if (!error.isMainFrame || error.isSubresource) {
1281 // Not a main frame document error, just block the content
1282 // (it’s not overridable anyway).
1283 return
1284 }
1285 if (internal.isCertificateErrorAllowed(error)) {
1286 error.allow()
1287 } else {
1288 certificateError = error
1289 error.onCancelled.connect(webviewimpl.resetCertificateError)
1290 }
1291 }
1292
1293 onFullscreenChanged: {
1294 if (fullscreen) {
1295 fullscreenExitHintComponent.createObject(webviewimpl)
1296 }
1297 }
1298 Component {
1299 id: fullscreenExitHintComponent
1300
1301 Rectangle {
1302 id: fullscreenExitHint
1303 objectName: "fullscreenExitHint"
1304
1305 anchors.centerIn: parent
1306 height: units.gu(6)
1307 width: Math.min(units.gu(50), parent.width - units.gu(12))
1308 radius: units.gu(1)
1309 color: "#3e3b39"
1310 opacity: 0.85
1311
1312 Behavior on opacity {
1313 UbuntuNumberAnimation {
1314 duration: UbuntuAnimation.SlowDuration
1315 }
1316 }
1317 onOpacityChanged: {
1318 if (opacity == 0.0) {
1319 fullscreenExitHint.destroy()
1320 }
1321 }
1322
1323 // Delay showing the hint to prevent it from jumping up while the
1324 // webview is being resized (https://launchpad.net/bugs/1454097).
1325 visible: false
1326 Timer {
1327 running: true
1328 interval: 250
1329 onTriggered: fullscreenExitHint.visible = true
1330 }
1331
1332 Label {
1333 color: "white"
1334 font.weight: Font.Light
1335 anchors.centerIn: parent
1336 text: bottomEdgeHandle.enabled
1337 ? i18n.tr("Swipe Up To Exit Full Screen")
1338 : i18n.tr("Press ESC To Exit Full Screen")
1339 }
1340
1341 Timer {
1342 running: fullscreenExitHint.visible
1343 interval: 2000
1344 onTriggered: fullscreenExitHint.opacity = 0
1345 }
1346
1347 Connections {
1348 target: webviewimpl
1349 onFullscreenChanged: {
1350 if (!webviewimpl.fullscreen) {
1351 fullscreenExitHint.destroy()
1352 }
1353 }
1354 }
1355
1356 Component.onCompleted: bottomEdgeHint.forceShow = true
1357 Component.onDestruction: bottomEdgeHint.forceShow = false
1358 }
1359 }
1360
1361 onShowDownloadDialog: {
1362 if (downloadDialogLoader.status === Loader.Ready) {
1363 var downloadDialog = PopupUtils.open(downloadDialogLoader.item, browser, {"contentType" : contentType,
1364 "downloadId" : downloadId,
1365 "singleDownload" : downloader,
1366 "filename" : filename,
1367 "mimeType" : mimeType})
1368 downloadDialog.startDownload.connect(startDownload)
1369 }
1370 }
1371
1372 function showDownloadsPage() {
1373 downloadsViewLoader.active = true
1374 return downloadsViewLoader.item
1375 }
1376
1377 function startDownload(downloadId, download, mimeType) {
1378 DownloadsModel.add(downloadId, download.url, mimeType)
1379 download.start()
1380 downloadsViewLoader.active = true
1381 }
1382
1383 }
1384 }
1385 }
1386
1387 Component {
1388 id: bookmarkOptionsComponent
1389 BookmarkOptions {
1390 folderModel: BookmarksFolderListModel {
1391 sourceModel: BookmarksModel
1392 }
1393
1394 Component.onCompleted: forceActiveFocus()
1395
1396 onVisibleChanged: {
1397 if (!visible) {
1398 BookmarksModel.remove(bookmarkUrl)
1399 }
1400 }
1401
1402 Component.onDestruction: {
1403 if (BookmarksModel.contains(bookmarkUrl)) {
1404 BookmarksModel.update(bookmarkUrl, bookmarkTitle, bookmarkFolder)
1405 }
1406 }
1407
1408 // Fragile workaround for https://launchpad.net/bugs/1546677.
1409 // By destroying the popover, its visibility isn’t changed to
1410 // false, and thus the bookmark is not removed.
1411 Keys.onEnterPressed: destroy()
1412 Keys.onReturnPressed: destroy()
1413 }
1414 }929 }
1415930
1416 QtObject {931 QtObject {
@@ -1513,6 +1028,7 @@
1513 var tabInfo = closedTabHistory.pop()1028 var tabInfo = closedTabHistory.pop()
1514 var tab = restoreTabState(tabInfo.state)1029 var tab = restoreTabState(tabInfo.state)
1515 addTab(tab, true, tabInfo.index)1030 addTab(tab, true, tabInfo.index)
1031 tab.load()
1516 }1032 }
1517 }1033 }
15181034
@@ -1621,7 +1137,7 @@
1621 BookmarksModel.add(url, title, icon, "")1137 BookmarksModel.add(url, title, icon, "")
1622 if (location === undefined) location = chrome.bookmarkTogglePlaceHolder1138 if (location === undefined) location = chrome.bookmarkTogglePlaceHolder
1623 var properties = {"bookmarkUrl": url, "bookmarkTitle": title}1139 var properties = {"bookmarkUrl": url, "bookmarkTitle": title}
1624 currentBookmarkOptionsDialog = PopupUtils.open(bookmarkOptionsComponent,1140 internal.currentBookmarkOptionsDialog = PopupUtils.open(Qt.resolvedUrl("BookmarkOptions.qml"),
1625 location, properties)1141 location, properties)
1626 }1142 }
1627 }1143 }
16281144
=== modified file 'src/app/webbrowser/BrowserTab.qml'
--- src/app/webbrowser/BrowserTab.qml 2016-08-11 11:19:40 +0000
+++ src/app/webbrowser/BrowserTab.qml 2016-10-13 14:41:36 +0000
@@ -193,9 +193,10 @@
193 }193 }
194 }194 }
195 Connections {195 Connections {
196 target: webview196 target: incognito ? null : webview
197 onLoadingStateChanged: {197 onLoadEvent: {
198 if (!webview.loading && !webview.incognito) {198 if ((event.type == Oxide.LoadEvent.TypeSucceeded) ||
199 (event.type == Oxide.LoadEvent.TypeFailed)) {
199 delayedCapture.restart()200 delayedCapture.restart()
200 }201 }
201 }202 }
202203
=== modified file 'src/app/webbrowser/Chrome.qml'
--- src/app/webbrowser/Chrome.qml 2016-02-09 22:01:57 +0000
+++ src/app/webbrowser/Chrome.qml 2016-10-13 14:41:36 +0000
@@ -25,6 +25,7 @@
2525
26 property var tabsModel26 property var tabsModel
27 property alias tab: navigationBar.tab27 property alias tab: navigationBar.tab
28 readonly property var webview: tab ? tab.webview : null
28 property alias searchUrl: navigationBar.searchUrl29 property alias searchUrl: navigationBar.searchUrl
29 property alias text: navigationBar.text30 property alias text: navigationBar.text
30 property alias bookmarked: navigationBar.bookmarked31 property alias bookmarked: navigationBar.bookmarked
@@ -69,15 +70,23 @@
69 Loader {70 Loader {
70 id: tabsBar71 id: tabsBar
7172
72 sourceComponent: TabsBar {73 Component.onCompleted: {
73 model: tabsModel74 tabsBar.setSource("TabsBar.qml", {
74 incognito: chrome.incognito75 "model": Qt.binding(function () {return chrome.tabsModel}),
75 fgColor: navigationBar.fgColor76 "incognito": Qt.binding(function () {return chrome.incognito}),
76 touchEnabled: chrome.touchEnabled77 "fgColor": Qt.binding(function () {return navigationBar.fgColor}),
78 "touchEnabled": Qt.binding(function () {return chrome.touchEnabled})
79 })
80 }
81
82 Connections {
83 target: tabsBar.item
84
77 onSwitchToTab: chrome.switchToTab(index)85 onSwitchToTab: chrome.switchToTab(index)
78 onRequestNewTab: chrome.requestNewTab(index, makeCurrent)86 onRequestNewTab: chrome.requestNewTab(index, makeCurrent)
79 onTabClosed: chrome.tabClosed(index)87 onTabClosed: chrome.tabClosed(index)
80 }88 }
89 asynchronous: true
8190
82 anchors {91 anchors {
83 top: parent.top92 top: parent.top
@@ -90,6 +99,7 @@
90 NavigationBar {99 NavigationBar {
91 id: navigationBar100 id: navigationBar
92101
102 loading: chrome.loading
93 fgColor: "#111111"103 fgColor: "#111111"
94 iconColor: (incognito && !showTabsBar) ? "white" : fgColor104 iconColor: (incognito && !showTabsBar) ? "white" : fgColor
95105
@@ -105,4 +115,19 @@
105 onToggleBookmark: chrome.toggleBookmark()115 onToggleBookmark: chrome.toggleBookmark()
106 }116 }
107 }117 }
118
119 // Delay changing the 'loading' state, to allow for very brief load
120 // sequences to not update the UI, which would result in inelegant
121 // flickering (https://launchpad.net/bugs/1611680).
122 Connections {
123 target: webview
124 onLoadingStateChanged: delayedLoadingNotifier.restart()
125 }
126 Timer {
127 id: delayedLoadingNotifier
128 interval: 100
129 onTriggered: loading = webview.loading
130 }
131
132 loadProgress: (loading && webview) ? webview.loadProgress : 0
108}133}
109134
=== modified file 'src/app/webbrowser/DownloadDelegate.qml'
--- src/app/webbrowser/DownloadDelegate.qml 2016-05-23 02:52:50 +0000
+++ src/app/webbrowser/DownloadDelegate.qml 2016-10-13 14:41:36 +0000
@@ -25,7 +25,7 @@
2525
26 property var downloadManager26 property var downloadManager
2727
28 property alias icon: mimeicon.name28 property string icon
29 property alias image: thumbimage.source29 property alias image: thumbimage.source
30 property alias title: title.text30 property alias title: title.text
31 property alias url: url.text31 property alias url: url.text
@@ -33,15 +33,16 @@
33 property bool incomplete: false33 property bool incomplete: false
34 property string downloadId34 property string downloadId
35 property var download35 property var download
36 property int progress: download ? download.progress : 036 readonly property int progress: download ? download.progress : 0
37 property bool paused37 property bool paused
38 property alias incognito: incognitoIcon.visible
3839
39 divider.visible: false40 divider.visible: false
4041
41 signal removed()42 signal removed()
42 signal cancelled()43 signal cancelled()
4344
44 height: visible ? (incomplete ? (paused ? units.gu(13) : units.gu(10)) : units.gu(7)) : 045 height: visible ? layout.height : 0
4546
46 QtObject {47 QtObject {
47 id: internal48 id: internal
@@ -60,165 +61,167 @@
60 Component.onCompleted: internal.connectToDownloadObject()61 Component.onCompleted: internal.connectToDownloadObject()
61 onDownloadManagerChanged: internal.connectToDownloadObject()62 onDownloadManagerChanged: internal.connectToDownloadObject()
6263
63 Item {64 SlotsLayout {
64 65 id: layout
65 anchors {
66 verticalCenter: parent.verticalCenter
67 left: parent.left
68 leftMargin: units.gu(2)
69 right: parent.right
70 rightMargin: units.gu(2)
71 }
7266
73 Item {67 Item {
74 id: iconContainer68 SlotsLayout.position: SlotsLayout.Leading
75 width: units.gu(3)69 width: units.gu(3)
76 height: width70 height: units.gu(3)
77 anchors.verticalCenter: parent.verticalCenter
78 anchors.verticalCenterOffset: downloadDelegate.incomplete ? -units.gu(1) : 0
7971
80 Image {72 Image {
81 id: thumbimage73 id: thumbimage
82 asynchronous: true74 asynchronous: true
83 width: parent.width75 anchors.fill: parent
84 height: parent.height
85 fillMode: Image.PreserveAspectFit76 fillMode: Image.PreserveAspectFit
86 sourceSize.width: parent.width77 sourceSize.width: width
87 sourceSize.height: parent.height78 sourceSize.height: height
88 anchors.verticalCenter: parent.verticalCenter
89 }79 }
9080
91 Image {81 Image {
92 id: mimeicon
93 asynchronous: true82 asynchronous: true
94 anchors.fill: parent83 anchors.fill: parent
95 anchors.margins: units.gu(0.2)84 anchors.margins: units.gu(0.2)
96 source: "image://theme/%1".arg(name != "" ? name : "save")85 source: "image://theme/%1".arg(downloadDelegate.icon || "save")
97 visible: thumbimage.status !== Image.Ready86 visible: thumbimage.status !== Image.Ready
98 cache: true87 cache: true
99 property string name88 }
100 }89 }
101 }90
10291 mainSlot: Column {
103 Item {92 Label {
104 anchors.top: iconContainer.top93 id: title
105 anchors.left: iconContainer.right94 fontSize: "x-small"
106 anchors.leftMargin: units.gu(2)95 color: "#5d5d5d"
107 anchors.right: parent.right96 elide: Text.ElideRight
10897 anchors {
109 Column {98 left: parent.left
110 id: detailsColumn99 right: parent.right
111 width: parent.width - cancelColumn.width100 }
112 height: parent.height101 }
113102
114 Label {103 Label {
115 id: title104 id: url
116 fontSize: "x-small"105 fontSize: "x-small"
117 color: "#5d5d5d"106 color: "#5d5d5d"
118 elide: Text.ElideRight107 elide: Text.ElideRight
119 width: parent.width108 anchors {
120 }109 left: parent.left
121110 right: parent.right
122 Label {111 }
123 id: url112 }
124 fontSize: "x-small"113
125 color: "#5d5d5d"114 Item {
126 elide: Text.ElideRight115 height: error.visible ? units.gu(1) : units.gu(2)
127 width: parent.width116 anchors {
128 }117 left: parent.left
129118 right: parent.right
130 Item {119 }
131 height: error.visible ? units.gu(1) : units.gu(2)120 visible: incomplete
132 width: parent.width121 }
133 visible: downloadDelegate.incomplete122
134 }123 Item {
135124 id: error
136 Item {125 visible: (incomplete && (download === undefined)) || errorMessage
137 id: error126 height: units.gu(3)
138 visible: incomplete && download === undefined || errorMessage !== ""127 anchors {
139 height: units.gu(3)128 left: parent.left
140 width: parent.width129 right: parent.right
141130 }
142 Icon {131
143 id: errorIcon132 Icon {
144 width: units.gu(2)133 id: errorIcon
145 height: width134 width: units.gu(2)
146 anchors.verticalCenter: parent.verticalCenter135 height: units.gu(2)
147 name: "dialog-warning-symbolic"136 anchors.verticalCenter: parent.verticalCenter
148 color: theme.palette.normal.negative137 name: "dialog-warning-symbolic"
149 }138 color: theme.palette.normal.negative
150139 }
151 Label {140
152 width: parent.width - errorIcon.width141 Label {
153 anchors.left: errorIcon.right142 anchors {
154 anchors.leftMargin: units.gu(1)143 left: errorIcon.right
155 anchors.verticalCenter: errorIcon.verticalCenter144 leftMargin: units.gu(1)
156 fontSize: "x-small"145 right: parent.right
157 color: theme.palette.normal.negative146 verticalCenter: parent.verticalCenter
158 text: errorMessage !== "" ? errorMessage 147 }
159 : (incomplete && download === undefined) ? i18n.tr("Download failed") 148 fontSize: "x-small"
160 : ""149 color: theme.palette.normal.negative
161 elide: Text.ElideRight150 text: errorMessage ||
162 }151 ((incomplete && download === undefined) ? i18n.tr("Download failed") : "")
163 }152 elide: Text.ElideRight
164153 }
165 IndeterminateProgressBar {154 }
166 id: progressBar155
167 width: parent.width156 IndeterminateProgressBar {
168 height: units.gu(0.5)157 id: progressBar
169 visible: downloadDelegate.incomplete && !error.visible158 anchors {
170 progress: downloadDelegate.progress159 left: parent.left
171 // Work around UDM bug #1450144160 right: parent.right
172 indeterminateProgress: downloadDelegate.progress < 0 || downloadDelegate.progress > 100161 }
173 }162 height: units.gu(0.5)
174 }163 visible: incomplete && !error.visible
175164 progress: downloadDelegate.progress
176 Column {165 // Work around UDM bug #1450144
177 id: cancelColumn166 indeterminateProgress: progress < 0 || progress > 100
178 spacing: units.gu(1)167 }
179 anchors.top: detailsColumn.top168 }
180 anchors.left: detailsColumn.right169
181 anchors.leftMargin: units.gu(2)170 Column {
182 width: downloadDelegate.incomplete && !error.visible ? cancelButton.width + units.gu(2) : 0171 SlotsLayout.position: SlotsLayout.Trailing
183172 spacing: units.gu(1)
184 Button {173 width: (incomplete && !error.visible) ? cancelButton.width : 0
185 visible: downloadDelegate.incomplete && !error.visible174
186 id: cancelButton175 Button {
187 text: i18n.tr("Cancel")176 id: cancelButton
188 onClicked: {177 visible: incomplete && !error.visible
189 if (download) {178 text: i18n.tr("Cancel")
190 download.cancel()179 onClicked: {
191 cancelled()180 if (download) {
192 }181 download.cancel()
193 }182 cancelled()
194 }183 }
195184 }
196 Label {185 }
197 visible: !progressBar.indeterminateProgress && downloadDelegate.incomplete186
198 && !error.visible 187 Label {
199 && !downloadDelegate.paused188 visible: !progressBar.indeterminateProgress && incomplete
200 width: cancelButton.width189 && !error.visible && !paused
201 horizontalAlignment: Text.AlignHCenter190 width: cancelButton.width
202 fontSize: "x-small"191 horizontalAlignment: Text.AlignHCenter
203 text: progressBar.progress + "%"192 fontSize: "x-small"
204 }193 // TRANSLATORS: %1 is the percentage of the download completed so far
205194 text: i18n.tr("%1%").arg(progressBar.progress)
206 Button {195 }
207 visible: downloadDelegate.paused196
208 text: i18n.tr("Resume")197 Button {
209 width: cancelButton.width198 visible: paused
210 onClicked: {199 text: i18n.tr("Resume")
211 if (download) {200 width: cancelButton.width
212 download.resume()201 onClicked: {
213 }202 if (download) {
214 }203 download.resume()
215 }204 }
216 }205 }
217206 }
218 }207 }
219 }208 }
220209
221 leadingActions: error.visible || !downloadDelegate.incomplete ? deleteActionList : null210 Icon {
211 id: incognitoIcon
212 anchors {
213 right: parent.right
214 rightMargin: units.gu(2)
215 bottom: parent.bottom
216 bottomMargin: units.gu(1)
217 }
218 width: units.gu(2)
219 height: units.gu(2)
220 asynchronous: true
221 name: "private-browsing"
222 }
223
224 leadingActions: error.visible || !incomplete ? deleteActionList : null
222225
223 ListItemActions {226 ListItemActions {
224 id: deleteActionList227 id: deleteActionList
@@ -226,9 +229,8 @@
226 Action {229 Action {
227 objectName: "leadingAction.delete"230 objectName: "leadingAction.delete"
228 iconName: "delete"231 iconName: "delete"
229 enabled: error.visible || !downloadDelegate.incomplete232 enabled: error.visible || !incomplete
230 onTriggered: error.visible ? downloadDelegate.cancelled() 233 onTriggered: error.visible ? cancelled() : removed()
231 : downloadDelegate.removed()
232 }234 }
233 ]235 ]
234 }236 }
235237
=== modified file 'src/app/webbrowser/DownloadsPage.qml'
--- src/app/webbrowser/DownloadsPage.qml 2016-06-06 09:09:47 +0000
+++ src/app/webbrowser/DownloadsPage.qml 2016-10-13 14:41:36 +0000
@@ -39,6 +39,7 @@
39 property bool pickingMode39 property bool pickingMode
40 property bool multiSelect40 property bool multiSelect
41 property alias mimetypeFilter: downloadModelFilter.pattern41 property alias mimetypeFilter: downloadModelFilter.pattern
42 property bool incognito: false
4243
43 signal done()44 signal done()
4445
@@ -154,8 +155,14 @@
154 focus: !exportPeerPicker.focus155 focus: !exportPeerPicker.focus
155156
156 model: SortFilterModel {157 model: SortFilterModel {
157 model: DownloadsModel158 model: SortFilterModel {
158 filter { 159 model: DownloadsModel
160 filter {
161 property: "incognito"
162 pattern: RegExp(downloadsItem.incognito ? "" : "^false$")
163 }
164 }
165 filter {
159 id: downloadModelFilter166 id: downloadModelFilter
160 property: "mimetype"167 property: "mimetype"
161 }168 }
@@ -197,6 +204,7 @@
197 visible: !(selectMode && incomplete)204 visible: !(selectMode && incomplete)
198 errorMessage: model.error205 errorMessage: model.error
199 paused: model.paused206 paused: model.paused
207 incognito: model.incognito
200208
201 onClicked: {209 onClicked: {
202 if (model.complete && !selectMode) {210 if (model.complete && !selectMode) {
203211
=== added file 'src/app/webbrowser/HistoryViewWithExpansion.qml'
--- src/app/webbrowser/HistoryViewWithExpansion.qml 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/HistoryViewWithExpansion.qml 2016-10-13 14:41:36 +0000
@@ -0,0 +1,67 @@
1/*
2 * Copyright 2014-2016 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
22FocusScope {
23 id: historyViewWithExpansion
24
25 function loadModel() {
26 historyView.loadModel()
27 }
28
29 signal newTabRequested()
30 signal historyEntryClicked(url url)
31 signal done()
32 signal back()
33
34 HistoryView {
35 id: historyView
36 anchors.fill: parent
37 focus: !expandedHistoryViewLoader.focus
38 visible: focus
39 onSeeMoreEntriesClicked: {
40 expandedHistoryViewLoader.model = model
41 expandedHistoryViewLoader.active = true
42 }
43 onNewTabRequested: historyViewWithExpansion.newTabRequested()
44 onBack: historyViewWithExpansion.back()
45 }
46
47 Loader {
48 id: expandedHistoryViewLoader
49 asynchronous: true
50 anchors.fill: parent
51 active: false
52 focus: active
53 property var model: null
54 sourceComponent: ExpandedHistoryView {
55 focus: true
56 model: expandedHistoryViewLoader.model
57 onHistoryEntryClicked: historyViewWithExpansion.historyEntryClicked(url)
58 onHistoryEntryRemoved: {
59 if (count == 1) {
60 done()
61 }
62 HistoryModel.removeEntryByUrl(url)
63 }
64 onDone: expandedHistoryViewLoader.active = false
65 }
66 }
67}
068
=== modified file 'src/app/webbrowser/NavigationBar.qml'
--- src/app/webbrowser/NavigationBar.qml 2016-05-17 17:23:42 +0000
+++ src/app/webbrowser/NavigationBar.qml 2016-10-13 14:41:36 +0000
@@ -24,6 +24,7 @@
24 id: root24 id: root
2525
26 property var tab26 property var tab
27 property alias loading: addressbar.loading
27 property alias searchUrl: addressbar.searchUrl28 property alias searchUrl: addressbar.searchUrl
28 readonly property string text: addressbar.text29 readonly property string text: addressbar.text
29 property alias bookmarked: addressbar.bookmarked30 property alias bookmarked: addressbar.bookmarked
@@ -122,8 +123,6 @@
122123
123 icon: (internal.webview && internal.webview.certificateError) ? "" : tab ? tab.icon : ""124 icon: (internal.webview && internal.webview.certificateError) ? "" : tab ? tab.icon : ""
124125
125 loading: internal.webview ? internal.webview.loading : false
126
127 onValidated: {126 onValidated: {
128 if (!findInPageMode) {127 if (!findInPageMode) {
129 internal.webview.forceActiveFocus()128 internal.webview.forceActiveFocus()
@@ -138,15 +137,8 @@
138 onToggleBookmark: root.toggleBookmark()137 onToggleBookmark: root.toggleBookmark()
139138
140 Connections {139 Connections {
141 target: internal.webview140 target: tab
142 onUrlChanged: {141 onUrlChanged: addressbar.actualUrl = tab.url
143 // ensure that the URL actually changes so that the
144 // address bar is updated in case the user has entered a
145 // new address that redirects to where she previously was
146 // (https://launchpad.net/bugs/1306615)
147 addressbar.actualUrl = ""
148 addressbar.actualUrl = internal.webview.url
149 }
150 }142 }
151 }143 }
152144
153145
=== added file 'src/app/webbrowser/TabComponent.qml'
--- src/app/webbrowser/TabComponent.qml 1970-01-01 00:00:00 +0000
+++ src/app/webbrowser/TabComponent.qml 2016-10-13 14:41:36 +0000
@@ -0,0 +1,436 @@
1/*
2 * Copyright 2014-2016 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 com.canonical.Oxide 1.15 as Oxide
23import webbrowserapp.private 0.1
24import "../actions" as Actions
25import ".."
26
27// FIXME: This component breaks encapsulation: it uses variables not defined in
28// itself. However this is an acceptable tradeoff with regards to
29// startup time performance. Indeed having this component defined as a separate
30// QML file as opposed to inline makes it possible to cache its compiled form.
31
32Component {
33 id: tabComponent
34
35 BrowserTab {
36 anchors.fill: parent
37 incognito: browser.incognito
38 current: tabsModel && tabsModel.currentTab === this
39 focus: current
40
41 Item {
42 id: contextualMenuTarget
43 visible: false
44 }
45
46 webviewComponent: WebViewImpl {
47 id: webviewimpl
48
49 property BrowserTab tab
50 readonly property bool current: tab.current
51
52 currentWebview: browser.currentWebview
53 filePicker: filePickerLoader.item
54
55 anchors.fill: parent
56
57 focus: true
58
59 enabled: current && !bottomEdgeHandle.dragging && !recentView.visible && tabContainer.focus
60
61 locationBarController {
62 height: chrome.height
63 mode: chromeController.defaultMode
64 }
65
66 //experimental.preferences.developerExtrasEnabled: developerExtrasEnabled
67 preferences.localStorageEnabled: true
68 preferences.appCacheEnabled: true
69
70 property QtObject contextModel: null
71 contextualActions: ActionList {
72 Actions.OpenLinkInNewTab {
73 objectName: "OpenLinkInNewTabContextualAction"
74 enabled: contextModel && contextModel.linkUrl.toString()
75 onTriggered: internal.openUrlInNewTab(contextModel.linkUrl, true)
76 }
77 Actions.OpenLinkInNewBackgroundTab {
78 objectName: "OpenLinkInNewBackgroundTabContextualAction"
79 enabled: contextModel && contextModel.linkUrl.toString()
80 onTriggered: internal.openUrlInNewTab(contextModel.linkUrl, false)
81 }
82 Actions.OpenLinkInNewWindow {
83 objectName: "OpenLinkInNewWindowContextualAction"
84 enabled: contextModel && contextModel.linkUrl.toString()
85 onTriggered: browser.openLinkInWindowRequested(contextModel.linkUrl, false)
86 }
87 Actions.OpenLinkInPrivateWindow {
88 objectName: "OpenLinkInPrivateWindowContextualAction"
89 enabled: contextModel && contextModel.linkUrl.toString()
90 onTriggered: browser.openLinkInWindowRequested(contextModel.linkUrl, true)
91 }
92 Actions.BookmarkLink {
93 objectName: "BookmarkLinkContextualAction"
94 enabled: contextModel && contextModel.linkUrl.toString()
95 && !BookmarksModel.contains(contextModel.linkUrl)
96 onTriggered: {
97 // position the menu target with a one-off assignement instead of a binding
98 // since the contents of the contextModel have meaning only while the context
99 // menu is active
100 contextualMenuTarget.x = contextModel.position.x
101 contextualMenuTarget.y = contextModel.position.y + locationBarController.height + locationBarController.offset
102 internal.addBookmark(contextModel.linkUrl, contextModel.linkText,
103 "", contextualMenuTarget)
104 }
105 }
106 Actions.CopyLink {
107 objectName: "CopyLinkContextualAction"
108 enabled: contextModel && contextModel.linkUrl.toString()
109 onTriggered: Clipboard.push(["text/plain", contextModel.linkUrl.toString()])
110 }
111 Actions.SaveLink {
112 objectName: "SaveLinkContextualAction"
113 enabled: contextModel && contextModel.linkUrl.toString()
114 onTriggered: contextModel.saveLink()
115 }
116 Actions.Share {
117 objectName: "ShareContextualAction"
118 enabled: (contentHandlerLoader.status == Loader.Ready) && contextModel &&
119 (contextModel.linkUrl.toString() || contextModel.selectionText)
120 onTriggered: {
121 if (contextModel.linkUrl.toString()) {
122 internal.shareLink(contextModel.linkUrl.toString(), contextModel.linkText)
123 } else if (contextModel.selectionText) {
124 internal.shareText(contextModel.selectionText)
125 }
126 }
127 }
128 Actions.OpenImageInNewTab {
129 objectName: "OpenImageInNewTabContextualAction"
130 enabled: contextModel &&
131 (contextModel.mediaType === Oxide.WebView.MediaTypeImage) &&
132 contextModel.srcUrl.toString()
133 onTriggered: internal.openUrlInNewTab(contextModel.srcUrl, true)
134 }
135 Actions.CopyImage {
136 objectName: "CopyImageContextualAction"
137 enabled: contextModel &&
138 (contextModel.mediaType === Oxide.WebView.MediaTypeImage) &&
139 contextModel.srcUrl.toString()
140 onTriggered: Clipboard.push(["text/plain", contextModel.srcUrl.toString()])
141 }
142 Actions.SaveImage {
143 objectName: "SaveImageContextualAction"
144 enabled: contextModel &&
145 ((contextModel.mediaType === Oxide.WebView.MediaTypeImage) ||
146 (contextModel.mediaType === Oxide.WebView.MediaTypeCanvas)) &&
147 contextModel.hasImageContents
148 onTriggered: contextModel.saveMedia()
149 }
150 Actions.OpenVideoInNewTab {
151 objectName: "OpenVideoInNewTabContextualAction"
152 enabled: contextModel &&
153 (contextModel.mediaType === Oxide.WebView.MediaTypeVideo) &&
154 contextModel.srcUrl.toString()
155 onTriggered: internal.openUrlInNewTab(contextModel.srcUrl, true)
156 }
157 Actions.SaveVideo {
158 objectName: "SaveVideoContextualAction"
159 enabled: contextModel &&
160 (contextModel.mediaType === Oxide.WebView.MediaTypeVideo) &&
161 contextModel.srcUrl.toString()
162 onTriggered: contextModel.saveMedia()
163 }
164 Actions.Undo {
165 objectName: "UndoContextualAction"
166 enabled: contextModel && contextModel.isEditable &&
167 (contextModel.editFlags & Oxide.WebView.UndoCapability)
168 onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandUndo)
169 }
170 Actions.Redo {
171 objectName: "RedoContextualAction"
172 enabled: contextModel && contextModel.isEditable &&
173 (contextModel.editFlags & Oxide.WebView.RedoCapability)
174 onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandRedo)
175 }
176 Actions.Cut {
177 objectName: "CutContextualAction"
178 enabled: contextModel && contextModel.isEditable &&
179 (contextModel.editFlags & Oxide.WebView.CutCapability)
180 onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandCut)
181 }
182 Actions.Copy {
183 objectName: "CopyContextualAction"
184 enabled: contextModel && (contextModel.selectionText ||
185 (contextModel.isEditable &&
186 (contextModel.editFlags & Oxide.WebView.CopyCapability)))
187 onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandCopy)
188 }
189 Actions.Paste {
190 objectName: "PasteContextualAction"
191 enabled: contextModel && contextModel.isEditable &&
192 (contextModel.editFlags & Oxide.WebView.PasteCapability)
193 onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandPaste)
194 }
195 Actions.Erase {
196 objectName: "EraseContextualAction"
197 enabled: contextModel && contextModel.isEditable &&
198 (contextModel.editFlags & Oxide.WebView.EraseCapability)
199 onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandErase)
200 }
201 Actions.SelectAll {
202 objectName: "SelectAllContextualAction"
203 enabled: contextModel && contextModel.isEditable &&
204 (contextModel.editFlags & Oxide.WebView.SelectAllCapability)
205 onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandSelectAll)
206 }
207 }
208
209 function contextMenuOnCompleted(menu) {
210 contextModel = menu.contextModel
211 if (contextModel.linkUrl.toString() ||
212 contextModel.srcUrl.toString() ||
213 contextModel.selectionText ||
214 (contextModel.isEditable && contextModel.editFlags) ||
215 (((contextModel.mediaType == Oxide.WebView.MediaTypeImage) ||
216 (contextModel.mediaType == Oxide.WebView.MediaTypeCanvas)) &&
217 contextModel.hasImageContents)) {
218 menu.show()
219 } else {
220 contextModel.close()
221 }
222 }
223
224 Component {
225 id: contextMenuNarrowComponent
226 ContextMenuMobile {
227 actions: contextualActions
228 Component.onCompleted: webviewimpl.contextMenuOnCompleted(this)
229 }
230 }
231 Component {
232 id: contextMenuWideComponent
233 ContextMenuWide {
234 webview: webviewimpl
235 parent: browser
236 actions: contextualActions
237 Component.onCompleted: webviewimpl.contextMenuOnCompleted(this)
238 }
239 }
240 contextMenu: browser.wide ? contextMenuWideComponent : contextMenuNarrowComponent
241
242 onNewViewRequested: {
243 var tab = tabComponent.createObject(tabContainer, {"request": request})
244 var setCurrent = (request.disposition == Oxide.NewViewRequest.DispositionNewForegroundTab)
245 internal.addTab(tab, setCurrent)
246 if (setCurrent) tabContainer.forceActiveFocus()
247 }
248
249 onCloseRequested: prepareToClose()
250 onPrepareToCloseResponse: {
251 if (proceed) {
252 if (tab) {
253 for (var i = 0; i < tabsModel.count; ++i) {
254 if (tabsModel.get(i) === tab) {
255 tabsModel.remove(i)
256 break
257 }
258 }
259 tab.close()
260 }
261 if (tabsModel.count === 0) {
262 internal.openUrlInNewTab("", true, true)
263 }
264 }
265 }
266
267 QtObject {
268 id: webviewInternal
269 property url storedUrl: ""
270 property bool titleSet: false
271 property string title: ""
272 }
273 onLoadEvent: {
274 if (event.type == Oxide.LoadEvent.TypeCommitted) {
275 chrome.findInPageMode = false
276 webviewInternal.titleSet = false
277 webviewInternal.title = title
278 }
279
280 if (webviewimpl.incognito) {
281 return
282 }
283
284 if ((event.type == Oxide.LoadEvent.TypeCommitted) &&
285 !event.isError &&
286 (300 > event.httpStatusCode) && (event.httpStatusCode >= 200)) {
287 webviewInternal.storedUrl = event.url
288 HistoryModel.add(event.url, title, icon)
289 }
290
291 // If the page has started, stopped, redirected, errored
292 // then clear the cache for the history update
293 // Otherwise if no title change has occurred the next title
294 // change will be the url of the next page causing the
295 // history entry to be incorrect (pad.lv/1603835)
296 if (event.type == Oxide.LoadEvent.TypeFailed ||
297 event.type == Oxide.LoadEvent.TypeRedirected ||
298 event.type == Oxide.LoadEvent.TypeStarted ||
299 event.type == Oxide.LoadEvent.TypeStopped) {
300 webviewInternal.titleSet = true
301 webviewInternal.storedUrl = ""
302 }
303 }
304 onTitleChanged: {
305 if (!webviewInternal.titleSet && webviewInternal.storedUrl.toString()) {
306 // Record the title to avoid updating the history database
307 // every time the page dynamically updates its title.
308 // We don’t want pages that update their title every second
309 // to achieve an ugly "scrolling title" effect to flood the
310 // history database with updates.
311 webviewInternal.titleSet = true
312 if (webviewInternal.title != title) {
313 webviewInternal.title = title
314 HistoryModel.update(webviewInternal.storedUrl, title, icon)
315 }
316 }
317 }
318 onIconChanged: {
319 if (webviewInternal.storedUrl.toString()) {
320 HistoryModel.update(webviewInternal.storedUrl, webviewInternal.title, icon)
321 }
322 }
323
324 onGeolocationPermissionRequested: requestGeolocationPermission(request)
325
326 property var certificateError
327 function resetCertificateError() {
328 certificateError = null
329 }
330 onCertificateError: {
331 if (!error.isMainFrame || error.isSubresource) {
332 // Not a main frame document error, just block the content
333 // (it’s not overridable anyway).
334 return
335 }
336 if (internal.isCertificateErrorAllowed(error)) {
337 error.allow()
338 } else {
339 certificateError = error
340 error.onCancelled.connect(webviewimpl.resetCertificateError)
341 }
342 }
343
344 onFullscreenChanged: {
345 if (fullscreen) {
346 fullscreenExitHintComponent.createObject(webviewimpl)
347 }
348 }
349 Component {
350 id: fullscreenExitHintComponent
351
352 Rectangle {
353 id: fullscreenExitHint
354 objectName: "fullscreenExitHint"
355
356 anchors.centerIn: parent
357 height: units.gu(6)
358 width: Math.min(units.gu(50), parent.width - units.gu(12))
359 radius: units.gu(1)
360 color: "#3e3b39"
361 opacity: 0.85
362
363 Behavior on opacity {
364 UbuntuNumberAnimation {
365 duration: UbuntuAnimation.SlowDuration
366 }
367 }
368 onOpacityChanged: {
369 if (opacity == 0.0) {
370 fullscreenExitHint.destroy()
371 }
372 }
373
374 // Delay showing the hint to prevent it from jumping up while the
375 // webview is being resized (https://launchpad.net/bugs/1454097).
376 visible: false
377 Timer {
378 running: true
379 interval: 250
380 onTriggered: fullscreenExitHint.visible = true
381 }
382
383 Label {
384 color: "white"
385 font.weight: Font.Light
386 anchors.centerIn: parent
387 text: bottomEdgeHandle.enabled
388 ? i18n.tr("Swipe Up To Exit Full Screen")
389 : i18n.tr("Press ESC To Exit Full Screen")
390 }
391
392 Timer {
393 running: fullscreenExitHint.visible
394 interval: 2000
395 onTriggered: fullscreenExitHint.opacity = 0
396 }
397
398 Connections {
399 target: webviewimpl
400 onFullscreenChanged: {
401 if (!webviewimpl.fullscreen) {
402 fullscreenExitHint.destroy()
403 }
404 }
405 }
406
407 Component.onCompleted: bottomEdgeHint.forceShow = true
408 Component.onDestruction: bottomEdgeHint.forceShow = false
409 }
410 }
411
412 onShowDownloadDialog: {
413 if (downloadDialogLoader.status === Loader.Ready) {
414 var downloadDialog = PopupUtils.open(downloadDialogLoader.item, browser, {"contentType" : contentType,
415 "downloadId" : downloadId,
416 "singleDownload" : downloader,
417 "filename" : filename,
418 "mimeType" : mimeType})
419 downloadDialog.startDownload.connect(startDownload)
420 }
421 }
422
423 function showDownloadsPage() {
424 downloadsViewLoader.active = true
425 return downloadsViewLoader.item
426 }
427
428 function startDownload(downloadId, download, mimeType) {
429 DownloadsModel.add(downloadId, download.url, mimeType, incognito)
430 download.start()
431 downloadsViewLoader.active = true
432 }
433
434 }
435 }
436}
0437
=== modified file 'src/app/webbrowser/TabItem.qml'
--- src/app/webbrowser/TabItem.qml 2016-07-01 08:52:37 +0000
+++ src/app/webbrowser/TabItem.qml 2016-10-13 14:41:36 +0000
@@ -22,6 +22,7 @@
2222
23Item {23Item {
24 id: tabItem24 id: tabItem
25 objectName: "tabItem"
2526
26 property bool incognito: false27 property bool incognito: false
27 property bool active: false28 property bool active: false
@@ -38,6 +39,8 @@
38 property color fgColor: Theme.palette.normal.baseText39 property color fgColor: Theme.palette.normal.baseText
3940
40 property bool touchEnabled: true41 property bool touchEnabled: true
42
43 readonly property bool showCloseIcon: closeIcon.x > units.gu(1) + tabItem.width / 2
4144
42 signal selected()45 signal selected()
43 signal closed()46 signal closed()
@@ -54,10 +57,17 @@
5457
55 Favicon {58 Favicon {
56 id: favicon59 id: favicon
57 anchors.verticalCenter: parent.verticalCenter60 anchors {
58 anchors.left: parent.left61 left: tabItem.showCloseIcon ? parent.left : undefined
59 anchors.leftMargin: units.gu(2)62 leftMargin: Math.min(tabItem.width / 4, units.gu(2))
63 horizontalCenter: tabItem.showCloseIcon ? undefined : parent.horizontalCenter
64 verticalCenter: parent.verticalCenter
65 }
60 shouldCache: !incognito66 shouldCache: !incognito
67
68 // Scale width and height of favicon when tabWidth becomes small
69 height: width
70 width: Math.min(units.dp(16), Math.min(tabItem.width - anchors.leftMargin * 2, tabItem.height))
61 }71 }
6272
63 Item {73 Item {
@@ -132,6 +142,7 @@
132 anchors.bottom: touchEnabled ? parent.bottom : undefined142 anchors.bottom: touchEnabled ? parent.bottom : undefined
133 anchors.right: touchEnabled ? parent.right : undefined143 anchors.right: touchEnabled ? parent.right : undefined
134 width: touchEnabled ? units.gu(4) : closeIcon.width144 width: touchEnabled ? units.gu(4) : closeIcon.width
145 visible: closeIcon.visible
135146
136 onClicked: closed()147 onClicked: closed()
137148
@@ -149,9 +160,10 @@
149 anchors.right: parent.right160 anchors.right: parent.right
150 anchors.rightMargin: units.gu(1)161 anchors.rightMargin: units.gu(1)
151 anchors.verticalCenter: parent.verticalCenter162 anchors.verticalCenter: parent.verticalCenter
163 asynchronous: true
152 name: "close"164 name: "close"
153 color: tabItem.fgColor165 color: tabItem.fgColor
154 asynchronous: true166 visible: tabItem.showCloseIcon
155 }167 }
156 }168 }
157}169}
158170
=== modified file 'src/app/webbrowser/TabsBar.qml'
--- src/app/webbrowser/TabsBar.qml 2016-02-05 11:21:32 +0000
+++ src/app/webbrowser/TabsBar.qml 2016-10-13 14:41:36 +0000
@@ -30,6 +30,18 @@
30 property real maxTabWidth: units.gu(20)30 property real maxTabWidth: units.gu(20)
31 property real tabWidth: model ? Math.max(Math.min(tabsContainer.maxWidth / model.count, maxTabWidth), minTabWidth) : 031 property real tabWidth: model ? Math.max(Math.min(tabsContainer.maxWidth / model.count, maxTabWidth), minTabWidth) : 0
3232
33 // Minimum size of the larger tab
34 readonly property real minActiveTabWidth: units.gu(10)
35
36 // When there is a larger tab, calc the smaller tab size
37 readonly property real nonActiveTabWidth: (tabsContainer.maxWidth - minActiveTabWidth) / Math.max(model.count - 1, 1)
38
39 // The size of the right margin of the tab
40 readonly property real rightMargin: units.dp(1)
41
42 // Whether there will be one larger tab or not
43 readonly property bool unevenTabWidth: tabWidth + rightMargin < minActiveTabWidth
44
33 property bool incognito: false45 property bool incognito: false
3446
35 property color fgColor: Theme.palette.normal.baseText47 property color fgColor: Theme.palette.normal.baseText
@@ -130,8 +142,8 @@
130 readonly property int tabIndex: index142 readonly property int tabIndex: index
131143
132 anchors.top: tabsContainer.top144 anchors.top: tabsContainer.top
133 property real rightMargin: units.dp(1)145
134 width: tabWidth + rightMargin146 width: getSize(index)
135 height: tabsContainer.height147 height: tabsContainer.height
136148
137 acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton149 acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
@@ -156,7 +168,7 @@
156168
157 touchEnabled: root.touchEnabled169 touchEnabled: root.touchEnabled
158170
159 rightMargin: tabDelegate.rightMargin171 rightMargin: root.rightMargin
160172
161 onClosed: root.tabClosed(index)173 onClosed: root.tabClosed(index)
162 onSelected: root.switchToTab(index)174 onSelected: root.switchToTab(index)
@@ -168,19 +180,52 @@
168 property: "reordering"180 property: "reordering"
169 value: dragging181 value: dragging
170 }182 }
183
184 Behavior on width { NumberAnimation { duration: 250 } }
171185
172 Binding on x {186 Binding on x {
173 when: !dragging187 when: !dragging
174 value: index * width188 value: getLeftX(index)
175 }189 }
176190
177 Behavior on x { NumberAnimation { duration: 250 } }191 Behavior on x { NumberAnimation { duration: 250 } }
178192
193 function getLeftX(index) {
194 if (unevenTabWidth) {
195 if (index > root.model.currentIndex) {
196 return minActiveTabWidth + (nonActiveTabWidth * (index - 1))
197 } else {
198 return nonActiveTabWidth * index
199 }
200 } else {
201 // Do not depend on width otherwise X updates after
202 // Width causing the animation to be two stage
203 // instead perform same calculation (tabWidth + rightMargin)
204 return index * (tabWidth + rightMargin)
205 }
206 }
207
208 function getSize(index) {
209 if (unevenTabWidth) {
210 // Uneven tabs so use large or small depending which index
211 if (index === root.model.currentIndex) {
212 return minActiveTabWidth
213 } else {
214 return nonActiveTabWidth
215 }
216 } else {
217 return tabWidth + rightMargin
218 }
219 }
220
179 onXChanged: {221 onXChanged: {
180 if (!dragging) return222 if (!dragging) return
181 if (x < (index * width - width / 2)) {223
224 var leftX = getLeftX(index)
225
226 if (x < (leftX - getSize(index - 1) / 2) && index > 0) {
182 root.model.move(index, index - 1)227 root.model.move(index, index - 1)
183 } else if ((x > (index * width + width / 2)) && (index < (root.model.count - 1))) {228 } else if ((x > (leftX + getSize(index + 1) / 2)) && (index < (root.model.count - 1))) {
184 root.model.move(index + 1, index)229 root.model.move(index + 1, index)
185 }230 }
186 }231 }
187232
=== modified file 'src/app/webbrowser/downloads-model.cpp'
--- src/app/webbrowser/downloads-model.cpp 2016-07-06 09:31:57 +0000
+++ src/app/webbrowser/downloads-model.cpp 2016-10-13 14:41:36 +0000
@@ -104,6 +104,7 @@
104 int count = 0; // size() isn't supported on the sqlite backend104 int count = 0; // size() isn't supported on the sqlite backend
105 while (populateQuery.next()) {105 while (populateQuery.next()) {
106 DownloadEntry entry;106 DownloadEntry entry;
107 entry.incognito = false;
107 entry.downloadId = populateQuery.value(0).toString();108 entry.downloadId = populateQuery.value(0).toString();
108 entry.url = populateQuery.value(1).toUrl();109 entry.url = populateQuery.value(1).toUrl();
109 entry.path = populateQuery.value(2).toString();110 entry.path = populateQuery.value(2).toString();
@@ -147,6 +148,7 @@
147 roles[Paused] = "paused";148 roles[Paused] = "paused";
148 roles[Error] = "error";149 roles[Error] = "error";
149 roles[Created] = "created";150 roles[Created] = "created";
151 roles[Incognito] = "incognito";
150 }152 }
151 return roles;153 return roles;
152}154}
@@ -182,6 +184,8 @@
182 return entry.error;184 return entry.error;
183 case Created:185 case Created:
184 return entry.created;186 return entry.created;
187 case Incognito:
188 return entry.incognito;
185 default:189 default:
186 return QVariant();190 return QVariant();
187 }191 }
@@ -218,7 +222,7 @@
218 Add a download to the database. This should happen as soon as the download222 Add a download to the database. This should happen as soon as the download
219 is started.223 is started.
220*/224*/
221void DownloadsModel::add(const QString& downloadId, const QUrl& url, const QString& mimetype)225void DownloadsModel::add(const QString& downloadId, const QUrl& url, const QString& mimetype, bool incognito)
222{226{
223 beginInsertRows(QModelIndex(), 0, 0);227 beginInsertRows(QModelIndex(), 0, 0);
224 DownloadEntry entry;228 DownloadEntry entry;
@@ -227,83 +231,111 @@
227 entry.paused = false;231 entry.paused = false;
228 entry.url = url;232 entry.url = url;
229 entry.mimetype = mimetype;233 entry.mimetype = mimetype;
234 entry.incognito = incognito;
230 m_orderedEntries.prepend(entry);235 m_orderedEntries.prepend(entry);
231 m_numRows++;236 m_numRows++;
232 m_fetchedCount++;
233 endInsertRows();237 endInsertRows();
234 Q_EMIT added(downloadId, url, mimetype);
235 insertNewEntryInDatabase(entry);
236 Q_EMIT rowCountChanged();238 Q_EMIT rowCountChanged();
237}239 if (!incognito) {
238240 insertNewEntryInDatabase(entry);
239void DownloadsModel::setPath(const QString& downloadId, const QString& path)241 m_fetchedCount++;
240{242 }
241 QSqlQuery query(m_database);
242
243 // Override reported mimetype from server with detected mimetype from file once downloaded
244 QMimeDatabase mimeDatabase;
245 QString mimetype = mimeDatabase.mimeTypeForFile(path).name();
246
247 static QString updateStatement = QLatin1String("UPDATE downloads SET mimetype = ?, "
248 "path = ? WHERE downloadId = ?");
249 query.prepare(updateStatement);
250 query.addBindValue(mimetype);
251 query.addBindValue(path);
252 query.addBindValue(downloadId);
253 query.exec();
254 Q_EMIT pathChanged(downloadId, path);
255}243}
256244
257void DownloadsModel::setComplete(const QString& downloadId, const bool complete)245void DownloadsModel::setComplete(const QString& downloadId, const bool complete)
258{246{
259 QSqlQuery query(m_database);247 int index = getIndexForDownloadId(downloadId);
260 static QString updateStatement = QLatin1String("UPDATE downloads SET complete = ? "248 if (index != -1) {
261 "WHERE downloadId = ?");249 DownloadEntry& entry = m_orderedEntries[index];
262 query.prepare(updateStatement);250 if (entry.complete == complete) {
263 query.addBindValue(complete);251 return;
264 query.addBindValue(downloadId);252 }
265 query.exec();253 entry.complete = complete;
266 Q_EMIT completeChanged(downloadId, complete);254 Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), QVector<int>() << Complete);
267 reload();255 if (!entry.incognito) {
256 QSqlQuery query(m_database);
257 static QString updateStatement = QLatin1String("UPDATE downloads SET complete=? WHERE downloadId=?;");
258 query.prepare(updateStatement);
259 query.addBindValue(complete);
260 query.addBindValue(downloadId);
261 query.exec();
262 }
263 }
268}264}
269265
270void DownloadsModel::setError(const QString& downloadId, const QString& error)266void DownloadsModel::setError(const QString& downloadId, const QString& error)
271{267{
272 QSqlQuery query(m_database);268 int index = getIndexForDownloadId(downloadId);
273 static QString updateStatement = QLatin1String("UPDATE downloads SET error = ? "269 if (index != -1) {
274 "WHERE downloadId = ?");270 DownloadEntry& entry = m_orderedEntries[index];
275 query.prepare(updateStatement);271 if (entry.error == error) {
276 query.addBindValue(error);272 return;
277 query.addBindValue(downloadId);273 }
278 query.exec();274 entry.error = error;
279 Q_EMIT errorChanged(downloadId, error);275 Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), QVector<int>() << Error);
280 reload();276 if (!entry.incognito) {
277 QSqlQuery query(m_database);
278 static QString updateStatement = QLatin1String("UPDATE downloads SET error=? WHERE downloadId=?;");
279 query.prepare(updateStatement);
280 query.addBindValue(error);
281 query.addBindValue(downloadId);
282 query.exec();
283 }
284 }
281}285}
282286
283void DownloadsModel::moveToDownloads(const QString& downloadId, const QString& path)287void DownloadsModel::moveToDownloads(const QString& downloadId, const QString& path)
284{288{
289 int index = getIndexForDownloadId(downloadId);
290 if (index == -1) {
291 return;
292 }
285 QFile file(path);293 QFile file(path);
286 if (file.exists()) {294 if (file.exists()) {
287 QFileInfo fi(path);295 QFileInfo fi(path);
288 QString suffix = fi.completeSuffix();296 DownloadEntry& entry = m_orderedEntries[index];
289 QString filename = fi.fileName();297 QVector<int> updatedRoles;
290 QString filenameWithoutSuffix = filename.left(filename.size() - suffix.size());298
299 // Override reported mimetype from server with detected mimetype from file once downloaded
300 QMimeDatabase mimeDatabase;
301 QString mimetype = mimeDatabase.mimeTypeForFile(fi).name();
302 if (mimetype != entry.mimetype) {
303 entry.mimetype = mimetype;
304 updatedRoles.append(Mimetype);
305 }
306
307 // Move file to XDG Downloads folder
291 QDir dir(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));308 QDir dir(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
292 if (!dir.exists()) {309 if (!dir.exists()) {
293 QDir::root().mkpath(dir.absolutePath());310 QDir::root().mkpath(dir.absolutePath());
294 }311 }
295 QString destination = dir.absoluteFilePath(filenameWithoutSuffix + suffix);312 QString baseName = fi.baseName();
313 QString suffix = fi.completeSuffix();
314 QString destination = dir.absoluteFilePath(QString("%1.%2").arg(baseName, suffix));
296 // Avoid filename collision by automatically inserting an incremented315 // Avoid filename collision by automatically inserting an incremented
297 // number into the filename if the original name already exists.316 // number into the filename if the original name already exists.
298 int append = 1;317 int append = 1;
299 while (QFile::exists(destination)) {318 while (QFile::exists(destination)) {
300 destination = dir.absoluteFilePath(QString("%1%2.%3").arg(filenameWithoutSuffix, QString::number(append++), suffix));319 destination = dir.absoluteFilePath(QString("%1.%2.%3").arg(baseName, QString::number(append++), suffix));
301 }320 }
302 if (file.rename(destination)) {321 if (file.rename(destination)) {
303 setPath(downloadId, destination);322 entry.path = destination;
323 updatedRoles.append(Path);
304 } else {324 } else {
305 qWarning() << "Failed moving file from" << path << "to" << destination;325 qWarning() << "Failed moving file from" << path << "to" << destination;
306 }326 }
327
328 Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), updatedRoles);
329 if (!entry.incognito && !updatedRoles.isEmpty()) {
330 QSqlQuery query(m_database);
331 static QString updateStatement = QLatin1String("UPDATE downloads SET mimetype = ?, "
332 "path = ? WHERE downloadId = ?");
333 query.prepare(updateStatement);
334 query.addBindValue(mimetype);
335 query.addBindValue(destination);
336 query.addBindValue(downloadId);
337 query.exec();
338 }
307 } else {339 } else {
308 qWarning() << "Download not found:" << path;340 qWarning() << "Download not found:" << path;
309 }341 }
@@ -331,15 +363,17 @@
331 int index = 0;363 int index = 0;
332 Q_FOREACH(DownloadEntry entry, m_orderedEntries) {364 Q_FOREACH(DownloadEntry entry, m_orderedEntries) {
333 if (entry.path == path) {365 if (entry.path == path) {
366 bool incognito = entry.incognito;
334 beginRemoveRows(QModelIndex(), index, index);367 beginRemoveRows(QModelIndex(), index, index);
335 m_orderedEntries.removeAt(index);368 m_orderedEntries.removeAt(index);
336 endRemoveRows();369 endRemoveRows();
337 Q_EMIT deleted(path);
338 removeExistingEntryFromDatabase(path);
339 m_fetchedCount--;
340 m_numRows--;370 m_numRows--;
341 Q_EMIT rowCountChanged();371 Q_EMIT rowCountChanged();
342 QFile::remove(path);372 QFile::remove(path);
373 if (!incognito) {
374 removeExistingEntryFromDatabase(path);
375 m_fetchedCount--;
376 }
343 return;377 return;
344 } else {378 } else {
345 index++;379 index++;
@@ -352,45 +386,69 @@
352*/386*/
353void DownloadsModel::cancelDownload(const QString& downloadId)387void DownloadsModel::cancelDownload(const QString& downloadId)
354{388{
355 int index=0;389 int index = getIndexForDownloadId(downloadId);
356 Q_FOREACH(DownloadEntry entry, m_orderedEntries) {390 if (index != -1) {
357 if (entry.downloadId == downloadId) {391 const DownloadEntry& entry = m_orderedEntries.at(index);
358 beginRemoveRows(QModelIndex(), index, index);392 bool incognito = entry.incognito;
359 m_orderedEntries.removeAt(index);393 beginRemoveRows(QModelIndex(), index, index);
394 m_orderedEntries.removeAt(index);
395 endRemoveRows();
396 m_numRows--;
397 Q_EMIT rowCountChanged();
398 if (!incognito) {
360 QSqlQuery query(m_database);399 QSqlQuery query(m_database);
361 static QString deleteStatement = QLatin1String("DELETE FROM downloads WHERE downloadId=?;");400 static QString deleteStatement = QLatin1String("DELETE FROM downloads WHERE downloadId=?;");
362 query.prepare(deleteStatement);401 query.prepare(deleteStatement);
363 query.addBindValue(downloadId);402 query.addBindValue(downloadId);
364 query.exec();403 query.exec();
365 endRemoveRows();
366 m_fetchedCount--;404 m_fetchedCount--;
367 m_numRows--;405 }
368 Q_EMIT rowCountChanged();406 }
407}
408
409void DownloadsModel::setPaused(const QString& downloadId, bool paused)
410{
411 int index = getIndexForDownloadId(downloadId);
412 if (index != -1) {
413 DownloadEntry& entry = m_orderedEntries[index];
414 if (entry.paused == paused) {
369 return;415 return;
370 } else {416 }
371 index++;417 entry.paused = paused;
418 Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), QVector<int>() << Paused);
419 if (!entry.incognito) {
420 QSqlQuery query(m_database);
421 static QString pauseStatement = QLatin1String("UPDATE downloads SET paused=? WHERE downloadId=?;");
422 query.prepare(pauseStatement);
423 query.addBindValue(paused);
424 query.addBindValue(downloadId);
425 query.exec();
372 }426 }
373 }427 }
374}428}
375429
376void DownloadsModel::pauseDownload(const QString& downloadId)430void DownloadsModel::pauseDownload(const QString& downloadId)
377{431{
378 QSqlQuery query(m_database);432 setPaused(downloadId, true);
379 static QString pauseStatement = QLatin1String("UPDATE downloads SET paused=1 WHERE downloadId=?;");
380 query.prepare(pauseStatement);
381 query.addBindValue(downloadId);
382 query.exec();
383 reload();
384}433}
385434
386void DownloadsModel::resumeDownload(const QString& downloadId)435void DownloadsModel::resumeDownload(const QString& downloadId)
387{436{
388 QSqlQuery query(m_database);437 setPaused(downloadId, false);
389 static QString resumeStatement = QLatin1String("UPDATE downloads SET paused=0 WHERE downloadId=?;");438}
390 query.prepare(resumeStatement);439
391 query.addBindValue(downloadId);440void DownloadsModel::pruneIncognitoDownloads()
392 query.exec();441{
393 reload();442 for (int i = m_orderedEntries.size() - 1; i >= 0; --i) {
443 const DownloadEntry& entry = m_orderedEntries.at(i);
444 if (entry.incognito) {
445 beginRemoveRows(QModelIndex(), i, i);
446 m_orderedEntries.removeAt(i);
447 endRemoveRows();
448 m_numRows--;
449 Q_EMIT rowCountChanged();
450 }
451 }
394}452}
395453
396void DownloadsModel::removeExistingEntryFromDatabase(const QString& path)454void DownloadsModel::removeExistingEntryFromDatabase(const QString& path)
@@ -409,14 +467,15 @@
409 return m_canFetchMore;467 return m_canFetchMore;
410}468}
411469
412void DownloadsModel::reload()470int DownloadsModel::getIndexForDownloadId(const QString& downloadId) const
413{471{
414 beginResetModel();472 int index = 0;
415 m_orderedEntries.clear();473 Q_FOREACH(const DownloadEntry& entry, m_orderedEntries) {
416 m_canFetchMore = true;474 if (entry.downloadId == downloadId) {
417 m_fetchedCount = 0;475 return index;
418 m_numRows = 0;476 } else {
419 endResetModel();477 ++index;
420 fetchMore();478 }
421 Q_EMIT rowCountChanged();479 }
480 return -1;
422}481}
423482
=== modified file 'src/app/webbrowser/downloads-model.h'
--- src/app/webbrowser/downloads-model.h 2016-01-12 10:37:15 +0000
+++ src/app/webbrowser/downloads-model.h 2016-10-13 14:41:36 +0000
@@ -49,7 +49,8 @@
49 Complete,49 Complete,
50 Paused,50 Paused,
51 Error,51 Error,
52 Created52 Created,
53 Incognito
53 };54 };
5455
55 // reimplemented from QAbstractListModel56 // reimplemented from QAbstractListModel
@@ -63,23 +64,18 @@
63 void setDatabasePath(const QString& path);64 void setDatabasePath(const QString& path);
6465
65 Q_INVOKABLE bool contains(const QString& downloadId) const;66 Q_INVOKABLE bool contains(const QString& downloadId) const;
66 Q_INVOKABLE void add(const QString& downloadId, const QUrl& url, const QString& mimetype);67 Q_INVOKABLE void add(const QString& downloadId, const QUrl& url, const QString& mimetype, bool incognito);
67 Q_INVOKABLE void moveToDownloads(const QString& downloadId, const QString& path);68 Q_INVOKABLE void moveToDownloads(const QString& downloadId, const QString& path);
68 Q_INVOKABLE void setPath(const QString& downloadId, const QString& path);
69 Q_INVOKABLE void setComplete(const QString& downloadId, const bool complete);69 Q_INVOKABLE void setComplete(const QString& downloadId, const bool complete);
70 Q_INVOKABLE void setError(const QString& downloadId, const QString& error);70 Q_INVOKABLE void setError(const QString& downloadId, const QString& error);
71 Q_INVOKABLE void deleteDownload(const QString& path);71 Q_INVOKABLE void deleteDownload(const QString& path);
72 Q_INVOKABLE void cancelDownload(const QString& downloadId);72 Q_INVOKABLE void cancelDownload(const QString& downloadId);
73 Q_INVOKABLE void pauseDownload(const QString& downloadId);73 Q_INVOKABLE void pauseDownload(const QString& downloadId);
74 Q_INVOKABLE void resumeDownload(const QString& downloadId);74 Q_INVOKABLE void resumeDownload(const QString& downloadId);
75 Q_INVOKABLE void pruneIncognitoDownloads();
7576
76Q_SIGNALS:77Q_SIGNALS:
77 void databasePathChanged() const;78 void databasePathChanged() const;
78 void added(const QString& downloadId, const QUrl& url, const QString& mimetype) const;
79 void pathChanged(const QString& downloadId, const QString& path) const;
80 void completeChanged(const QString& downloadId, const bool complete) const;
81 void errorChanged(const QString& downloadId, const QString& error) const;
82 void deleted(const QString& path) const;
83 void rowCountChanged();79 void rowCountChanged();
8480
85private:81private:
@@ -98,6 +94,7 @@
98 bool paused;94 bool paused;
99 QString error;95 QString error;
100 QDateTime created;96 QDateTime created;
97 bool incognito;
101 };98 };
102 QList<DownloadEntry> m_orderedEntries;99 QList<DownloadEntry> m_orderedEntries;
103100
@@ -105,7 +102,8 @@
105 void createOrAlterDatabaseSchema();102 void createOrAlterDatabaseSchema();
106 void insertNewEntryInDatabase(const DownloadEntry& entry);103 void insertNewEntryInDatabase(const DownloadEntry& entry);
107 void removeExistingEntryFromDatabase(const QString& path);104 void removeExistingEntryFromDatabase(const QString& path);
108 void reload();105 void setPaused(const QString& downloadId, bool paused);
106 int getIndexForDownloadId(const QString& downloadId) const;
109};107};
110108
111#endif // __DOWNLOADS_MODEL_H__109#endif // __DOWNLOADS_MODEL_H__
112110
=== modified file 'src/app/webbrowser/history-model.cpp'
--- src/app/webbrowser/history-model.cpp 2016-03-01 09:30:41 +0000
+++ src/app/webbrowser/history-model.cpp 2016-10-13 14:41:36 +0000
@@ -20,10 +20,12 @@
20#include "history-model.h"20#include "history-model.h"
2121
22// Qt22// Qt
23#include <QtCore/QMutexLocker>23#include <QtCore/QTimer>
24#include <QtCore/QWriteLocker>
24#include <QtSql/QSqlQuery>25#include <QtSql/QSqlQuery>
2526
26#define CONNECTION_NAME "webbrowser-app-history"27#define SQL_DRIVER QStringLiteral("QSQLITE")
28#define CONNECTION_NAME QStringLiteral("webbrowser-app-history")
2729
28/*!30/*!
29 \class HistoryModel31 \class HistoryModel
@@ -39,18 +41,31 @@
39 The database is read at startup to populate the model, and whenever a new41 The database is read at startup to populate the model, and whenever a new
40 entry is added to the model the database is updated.42 entry is added to the model the database is updated.
41 However the model doesn’t monitor the database for external changes.43 However the model doesn’t monitor the database for external changes.
44 All database operations are performed on a separate thread in order not to
45 block the UI thread.
42*/46*/
43HistoryModel::HistoryModel(QObject* parent)47HistoryModel::HistoryModel(QObject* parent)
44 : QAbstractListModel(parent)48 : QAbstractListModel(parent)
45{49{
46 m_database = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), CONNECTION_NAME);50 m_dbWorker = new DbWorker;
51 m_dbWorker->moveToThread(&m_dbWorkerThread);
52 connect(m_dbWorker, SIGNAL(hiddenEntryFetched(const QUrl&)),
53 SLOT(onHiddenEntryFetched(const QUrl&)), Qt::QueuedConnection);
54 connect(m_dbWorker,
55 SIGNAL(entryFetched(const QUrl&, const QString&, const QString&,
56 const QUrl&, int, const QDateTime&)),
57 SLOT(onEntryFetched(const QUrl&, const QString&, const QString&,
58 const QUrl&, int, const QDateTime&)),
59 Qt::QueuedConnection);
60 connect(m_dbWorker, SIGNAL(loaded()), SIGNAL(loaded()));
61 m_dbWorkerThread.start(QThread::LowPriority);
47}62}
4863
49HistoryModel::~HistoryModel()64HistoryModel::~HistoryModel()
50{65{
51 m_database.close();66 m_dbWorker->deleteLater();
52 m_database = QSqlDatabase();67 m_dbWorkerThread.quit();
53 QSqlDatabase::removeDatabase(CONNECTION_NAME);68 m_dbWorkerThread.wait();
54}69}
5570
56void HistoryModel::resetDatabase(const QString& databaseName)71void HistoryModel::resetDatabase(const QString& databaseName)
@@ -58,85 +73,35 @@
58 beginResetModel();73 beginResetModel();
59 m_hiddenEntries.clear();74 m_hiddenEntries.clear();
60 m_entries.clear();75 m_entries.clear();
61 m_database.close();76 Q_EMIT m_dbWorker->resetDatabase(databaseName);
62 m_database.setDatabaseName(databaseName);
63 m_database.open();
64 createOrAlterDatabaseSchema();
65 endResetModel();77 endResetModel();
66 populateFromDatabase();78 Q_EMIT m_dbWorker->fetchEntries();
67}79}
6880
69void HistoryModel::createOrAlterDatabaseSchema()81void HistoryModel::onHiddenEntryFetched(const QUrl& url)
70{82{
71 QMutexLocker ml(&m_dbMutex);83 m_hiddenEntries.insert(url);
72 QSqlQuery createQuery(m_database);84}
73 QString query = QLatin1String("CREATE TABLE IF NOT EXISTS history "85
74 "(url VARCHAR, domain VARCHAR, title VARCHAR,"86void HistoryModel::onEntryFetched(const QUrl& url, const QString& domain, const QString& title,
75 " icon VARCHAR, visits INTEGER, lastVisit DATETIME);");87 const QUrl& icon, int visits, const QDateTime& lastVisit)
76 createQuery.prepare(query);88{
77 createQuery.exec();89 HistoryEntry entry;
7890 entry.url = url;
79 // The first version of the database schema didn’t have a 'domain' column91 if (domain.isEmpty()) {
80 QSqlQuery tableInfoQuery(m_database);92 entry.domain = DomainUtils::extractTopLevelDomainName(url);
81 query = QLatin1String("PRAGMA TABLE_INFO(history);");93 } else {
82 tableInfoQuery.prepare(query);94 entry.domain = domain;
83 tableInfoQuery.exec();95 }
84 while (tableInfoQuery.next()) {96 entry.title = title;
85 if (tableInfoQuery.value("name").toString() == "domain") {97 entry.icon = icon;
86 break;98 entry.visits = visits;
87 }99 entry.lastVisit = lastVisit;
88 }100 entry.hidden = m_hiddenEntries.contains(url);
89 if (!tableInfoQuery.isValid()) {101 int index = m_entries.count();
90 QSqlQuery addDomainColumnQuery(m_database);102 beginInsertRows(QModelIndex(), index, index);
91 query = QLatin1String("ALTER TABLE history ADD COLUMN domain VARCHAR;");103 m_entries.append(entry);
92 addDomainColumnQuery.prepare(query);104 endInsertRows();
93 addDomainColumnQuery.exec();
94 // Updating all the entries in the database to add the domain is a
95 // costly operation that would slow down the application startup,
96 // do not do it here.
97 }
98
99 QSqlQuery createHiddenQuery(m_database);
100 query = QLatin1String("CREATE TABLE IF NOT EXISTS history_hidden (url VARCHAR);");
101 createHiddenQuery.prepare(query);
102 createHiddenQuery.exec();
103}
104
105void HistoryModel::populateFromDatabase()
106{
107 QSqlQuery populateQuery(m_database);
108 QString query = QLatin1String("SELECT url, domain, title, icon, visits, lastVisit "
109 "FROM history ORDER BY lastVisit DESC;");
110 populateQuery.prepare(query);
111 populateQuery.exec();
112
113 QSqlQuery populateHiddenQuery(m_database);
114 query = QLatin1String("SELECT url FROM history_hidden;");
115 populateHiddenQuery.prepare(query);
116 populateHiddenQuery.exec();
117
118 while (populateHiddenQuery.next()) {
119 m_hiddenEntries.append(populateHiddenQuery.value(0).toUrl());
120 }
121
122 int count = 0;
123 while (populateQuery.next()) {
124 HistoryEntry entry;
125 entry.url = populateQuery.value(0).toUrl();
126 entry.domain = populateQuery.value(1).toString();
127 if (entry.domain.isEmpty()) {
128 entry.domain = DomainUtils::extractTopLevelDomainName(entry.url);
129 }
130 entry.title = populateQuery.value(2).toString();
131 entry.icon = populateQuery.value(3).toUrl();
132 entry.visits = populateQuery.value(4).toInt();
133 entry.lastVisit = QDateTime::fromTime_t(populateQuery.value(5).toInt());
134 entry.hidden = m_hiddenEntries.contains(entry.url);
135 beginInsertRows(QModelIndex(), count, count);
136 m_entries.append(entry);
137 endInsertRows();
138 ++count;
139 }
140}105}
141106
142QHash<int, QByteArray> HistoryModel::roleNames() const107QHash<int, QByteArray> HistoryModel::roleNames() const
@@ -194,12 +159,13 @@
194159
195const QString HistoryModel::databasePath() const160const QString HistoryModel::databasePath() const
196{161{
197 return m_database.databaseName();162 return m_databasePath;
198}163}
199164
200void HistoryModel::setDatabasePath(const QString& path)165void HistoryModel::setDatabasePath(const QString& path)
201{166{
202 if (path != databasePath()) {167 if (path != m_databasePath) {
168 m_databasePath = path;
203 if (path.isEmpty()) {169 if (path.isEmpty()) {
204 resetDatabase(":memory:");170 resetDatabase(":memory:");
205 } else {171 } else {
@@ -399,86 +365,55 @@
399365
400void HistoryModel::insertNewEntryInDatabase(const HistoryEntry& entry)366void HistoryModel::insertNewEntryInDatabase(const HistoryEntry& entry)
401{367{
402 QMutexLocker ml(&m_dbMutex);368 QVariantList values;
403 QSqlQuery query(m_database);369 values << entry.url.toString();
404 static QString insertStatement = QLatin1String("INSERT INTO history (url, domain, title, icon, "370 values << entry.domain;
405 "visits, lastVisit) VALUES (?, ?, ?, ?, 1, ?);");371 values << entry.title;
406 query.prepare(insertStatement);372 values << entry.icon.toString();
407 query.addBindValue(entry.url.toString());373 values << entry.lastVisit.toTime_t();
408 query.addBindValue(entry.domain);374 Q_EMIT m_dbWorker->enqueue(DbWorker::InsertNewEntry, values);
409 query.addBindValue(entry.title);
410 query.addBindValue(entry.icon.toString());
411 query.addBindValue(entry.lastVisit.toTime_t());
412 query.exec();
413}375}
414376
415void HistoryModel::insertNewEntryInHiddenDatabase(const QUrl& url)377void HistoryModel::insertNewEntryInHiddenDatabase(const QUrl& url)
416{378{
417 QMutexLocker ml(&m_dbMutex);379 Q_EMIT m_dbWorker->enqueue(DbWorker::InsertNewHiddenEntry, QVariantList() << url.toString());
418 QSqlQuery query(m_database);
419 static QString insertStatement = QLatin1String("INSERT INTO history_hidden (url) VALUES (?);");
420 query.prepare(insertStatement);
421 query.addBindValue(url.toString());
422 query.exec();
423}380}
424381
425void HistoryModel::updateExistingEntryInDatabase(const HistoryEntry& entry)382void HistoryModel::updateExistingEntryInDatabase(const HistoryEntry& entry)
426{383{
427 QMutexLocker ml(&m_dbMutex);384 QVariantList values;
428 QSqlQuery query(m_database);385 values << entry.domain;
429 static QString updateStatement = QLatin1String("UPDATE history SET domain=?, title=?, icon=?, "386 values << entry.title;
430 "visits=?, lastVisit=? WHERE url=?;");387 values << entry.icon.toString();
431 query.prepare(updateStatement);388 values << entry.visits;
432 query.addBindValue(entry.domain);389 values << entry.lastVisit.toTime_t();
433 query.addBindValue(entry.title);390 values << entry.url.toString();
434 query.addBindValue(entry.icon.toString());391 Q_EMIT m_dbWorker->enqueue(DbWorker::UpdateExistingEntry, values);
435 query.addBindValue(entry.visits);
436 query.addBindValue(entry.lastVisit.toTime_t());
437 query.addBindValue(entry.url.toString());
438 query.exec();
439}392}
440393
441void HistoryModel::removeEntryFromDatabaseByUrl(const QUrl& url)394void HistoryModel::removeEntryFromDatabaseByUrl(const QUrl& url)
442{395{
443 QMutexLocker ml(&m_dbMutex);396 Q_EMIT m_dbWorker->enqueue(DbWorker::RemoveEntryByUrl, QVariantList() << url.toString());
444 QSqlQuery query(m_database);
445 static QString deleteStatement = QLatin1String("DELETE FROM history WHERE url=?;");
446 query.prepare(deleteStatement);
447 query.addBindValue(url.toString());
448 query.exec();
449}397}
450398
451void HistoryModel::removeEntryFromHiddenDatabaseByUrl(const QUrl& url)399void HistoryModel::removeEntryFromHiddenDatabaseByUrl(const QUrl& url)
452{400{
453 QMutexLocker ml(&m_dbMutex);401 Q_EMIT m_dbWorker->enqueue(DbWorker::RemoveHiddenEntryByUrl, QVariantList() << url.toString());
454 QSqlQuery query(m_database);
455 static QString deleteStatement = QLatin1String("DELETE FROM history_hidden WHERE url=?;");
456 query.prepare(deleteStatement);
457 query.addBindValue(url.toString());
458 query.exec();
459}402}
460403
461void HistoryModel::removeEntriesFromDatabaseByDate(const QDate& date)404void HistoryModel::removeEntriesFromDatabaseByDate(const QDate& date)
462{405{
463 QMutexLocker ml(&m_dbMutex);406 QVariantList values;
464 QSqlQuery query(m_database);
465 static QString deleteStatement = QLatin1String("DELETE FROM history WHERE lastVisit BETWEEN ? AND ?;");
466 query.prepare(deleteStatement);
467 QDateTime dateTime = QDateTime(date);407 QDateTime dateTime = QDateTime(date);
468 query.addBindValue(dateTime.toTime_t());408 values << dateTime.toTime_t();
469 dateTime.setTime(QTime(23, 59, 59, 999));409 dateTime.setTime(QTime(23, 59, 59, 999));
470 query.addBindValue(dateTime.toTime_t());410 values << dateTime.toTime_t();
471 query.exec();411 Q_EMIT m_dbWorker->enqueue(DbWorker::RemoveEntriesByDate, values);
472}412}
473413
474void HistoryModel::removeEntriesFromDatabaseByDomain(const QString& domain)414void HistoryModel::removeEntriesFromDatabaseByDomain(const QString& domain)
475{415{
476 QMutexLocker ml(&m_dbMutex);416 Q_EMIT m_dbWorker->enqueue(DbWorker::RemoveEntriesByDomain, QVariantList() << domain);
477 QSqlQuery query(m_database);
478 static QString deleteStatement = QLatin1String("DELETE FROM history WHERE domain=?;");
479 query.prepare(deleteStatement);
480 query.addBindValue(domain);
481 query.exec();
482}417}
483418
484void HistoryModel::clearAll()419void HistoryModel::clearAll()
@@ -495,16 +430,8 @@
495430
496void HistoryModel::clearDatabase()431void HistoryModel::clearDatabase()
497{432{
498 QMutexLocker ml(&m_dbMutex);433 Q_EMIT m_dbWorker->enqueue(DbWorker::Clear, QVariantList() << QStringLiteral("history"));
499 QSqlQuery deleteQuery(m_database);434 Q_EMIT m_dbWorker->enqueue(DbWorker::Clear, QVariantList() << QStringLiteral("history_hidden"));
500 QString deleteStatement = QLatin1String("DELETE FROM history;");
501 deleteQuery.prepare(deleteStatement);
502 deleteQuery.exec();
503
504 QSqlQuery deleteHiddenQuery(m_database);
505 deleteStatement = QLatin1String("DELETE FROM history_hidden;");
506 deleteHiddenQuery.prepare(deleteStatement);
507 deleteHiddenQuery.exec();
508}435}
509436
510/*!437/*!
@@ -519,7 +446,7 @@
519 return;446 return;
520 }447 }
521448
522 m_hiddenEntries.append(url);449 m_hiddenEntries.insert(url);
523450
524 QVector<int> roles;451 QVector<int> roles;
525 roles << Hidden;452 roles << Hidden;
@@ -547,7 +474,7 @@
547 return;474 return;
548 }475 }
549476
550 m_hiddenEntries.removeAll(url);477 m_hiddenEntries.remove(url);
551478
552 QVector<int> roles;479 QVector<int> roles;
553 roles << Hidden;480 roles << Hidden;
@@ -577,3 +504,175 @@
577 }504 }
578 return item;505 return item;
579}506}
507
508DbWorker::DbWorker()
509 : QObject()
510 , m_flush(nullptr)
511{
512 // Ensure all database operations are performed on the same thread
513 connect(this, SIGNAL(resetDatabase(const QString&)),
514 SLOT(doResetDatabase(const QString&)), Qt::QueuedConnection);
515 connect(this, SIGNAL(fetchEntries()),
516 SLOT(doFetchEntries()), Qt::QueuedConnection);
517 qRegisterMetaType<Operation>("Operation");
518 connect(this, SIGNAL(enqueue(Operation, QVariantList)),
519 SLOT(doEnqueue(Operation, QVariantList)), Qt::QueuedConnection);
520}
521
522DbWorker::~DbWorker()
523{
524 if (m_flush) {
525 m_flush->stop();
526 delete m_flush;
527 m_flush = nullptr;
528 }
529 doFlush();
530 if (m_database.isOpen()) {
531 m_database.close();
532 }
533 m_database = QSqlDatabase();
534 QSqlDatabase::removeDatabase(CONNECTION_NAME);
535}
536
537void DbWorker::doResetDatabase(const QString& databaseName)
538{
539 if (m_flush) {
540 m_flush->stop();
541 delete m_flush;
542 m_flush = nullptr;
543 }
544 doFlush();
545 if (m_database.isOpen()) {
546 m_database.close();
547 }
548 if (!m_database.isValid()) {
549 m_database = QSqlDatabase::addDatabase(SQL_DRIVER, CONNECTION_NAME);
550 }
551 m_database.setDatabaseName(databaseName);
552 m_database.open();
553 doCreateOrAlterDatabaseSchema();
554}
555
556void DbWorker::doCreateOrAlterDatabaseSchema()
557{
558 QSqlQuery createQuery(m_database);
559 QString query = QStringLiteral("CREATE TABLE IF NOT EXISTS history "
560 "(url VARCHAR, domain VARCHAR, title VARCHAR,"
561 " icon VARCHAR, visits INTEGER, lastVisit DATETIME);");
562 createQuery.prepare(query);
563 createQuery.exec();
564
565 // The first version of the database schema didn't have a 'domain' column
566 QSqlQuery tableInfoQuery(m_database);
567 query = QStringLiteral("PRAGMA TABLE_INFO(history);");
568 tableInfoQuery.prepare(query);
569 tableInfoQuery.exec();
570 while (tableInfoQuery.next()) {
571 if (tableInfoQuery.value(QStringLiteral("name")).toString() == QStringLiteral("domain")) {
572 break;
573 }
574 }
575 if (!tableInfoQuery.isValid()) {
576 QSqlQuery addDomainColumnQuery(m_database);
577 query = QStringLiteral("ALTER TABLE history ADD COLUMN domain VARCHAR;");
578 addDomainColumnQuery.prepare(query);
579 addDomainColumnQuery.exec();
580 // Updating all the entries in the database to add the domain is a
581 // costly operation that would slow down the application startup,
582 // do not do it here.
583 }
584
585 QSqlQuery createHiddenQuery(m_database);
586 query = QStringLiteral("CREATE TABLE IF NOT EXISTS history_hidden (url VARCHAR);");
587 createHiddenQuery.prepare(query);
588 createHiddenQuery.exec();
589}
590
591void DbWorker::doFetchEntries()
592{
593 QSqlQuery populateHiddenQuery(m_database);
594 QString query = QStringLiteral("SELECT url FROM history_hidden;");
595 populateHiddenQuery.prepare(query);
596 populateHiddenQuery.exec();
597 while (populateHiddenQuery.next()) {
598 Q_EMIT hiddenEntryFetched(populateHiddenQuery.value(0).toUrl());
599 }
600
601 QSqlQuery populateQuery(m_database);
602 query = QStringLiteral("SELECT url, domain, title, icon, visits, lastVisit "
603 "FROM history ORDER BY lastVisit DESC;");
604 populateQuery.prepare(query);
605 populateQuery.exec();
606 while (populateQuery.next()) {
607 Q_EMIT entryFetched(populateQuery.value(0).toUrl(),
608 populateQuery.value(1).toString(),
609 populateQuery.value(2).toString(),
610 populateQuery.value(3).toUrl(),
611 populateQuery.value(4).toInt(),
612 QDateTime::fromTime_t(populateQuery.value(5).toInt()));
613 }
614 Q_EMIT loaded();
615}
616
617void DbWorker::doEnqueue(DbWorker::Operation operation, QVariantList values)
618{
619 if (!m_flush) {
620 m_flush = new QTimer;
621 m_flush->setInterval(1000);
622 m_flush->setSingleShot(true);
623 connect(m_flush, SIGNAL(timeout()), SLOT(doFlush()));
624 }
625 QWriteLocker locker(&m_lock);
626 m_pending.enqueue(qMakePair(operation, values));
627 m_flush->start();
628}
629
630void DbWorker::doFlush()
631{
632 QWriteLocker locker(&m_lock);
633 while (!m_pending.isEmpty()) {
634 QPair<Operation, QVariantList> args = m_pending.dequeue();
635 QString statement;
636 switch (args.first) {
637 case InsertNewEntry:
638 statement = QStringLiteral("INSERT INTO history (url, domain, title, icon, "
639 "visits, lastVisit) VALUES (?, ?, ?, ?, 1, ?);");
640 break;
641 case InsertNewHiddenEntry:
642 statement = QStringLiteral("INSERT INTO history_hidden (url) VALUES (?);");
643 break;
644 case UpdateExistingEntry:
645 statement = QStringLiteral("UPDATE history SET domain=?, title=?, icon=?, "
646 "visits=?, lastVisit=? WHERE url=?;");
647 break;
648 case RemoveEntryByUrl:
649 statement = QStringLiteral("DELETE FROM history WHERE url=?;");
650 break;
651 case RemoveHiddenEntryByUrl:
652 statement = QStringLiteral("DELETE FROM history_hidden WHERE url=?;");
653 break;
654 case RemoveEntriesByDate:
655 statement = QStringLiteral("DELETE FROM history WHERE lastVisit BETWEEN ? AND ?;");
656 break;
657 case RemoveEntriesByDomain:
658 statement = QStringLiteral("DELETE FROM history WHERE domain=?;");
659 break;
660 case Clear:
661 statement = QStringLiteral("DELETE FROM %1;").arg(args.second.takeFirst().toString());
662 break;
663 default:
664 Q_UNREACHABLE();
665 }
666 if (statement.isEmpty()) {
667 return;
668 }
669 QSqlQuery query(m_database);
670 if (!query.prepare(statement)) {
671 continue;
672 }
673 Q_FOREACH(const QVariant& value, args.second) {
674 query.addBindValue(value);
675 }
676 query.exec();
677 }
678}
580679
=== modified file 'src/app/webbrowser/history-model.h'
--- src/app/webbrowser/history-model.h 2016-02-26 12:26:20 +0000
+++ src/app/webbrowser/history-model.h 2016-10-13 14:41:36 +0000
@@ -23,11 +23,20 @@
23#include <QtCore/QAbstractListModel>23#include <QtCore/QAbstractListModel>
24#include <QtCore/QDateTime>24#include <QtCore/QDateTime>
25#include <QtCore/QList>25#include <QtCore/QList>
26#include <QtCore/QMutex>26#include <QtCore/QPair>
27#include <QtCore/QQueue>
28#include <QtCore/QReadWriteLock>
29#include <QtCore/QSet>
27#include <QtCore/QString>30#include <QtCore/QString>
31#include <QtCore/QThread>
28#include <QtCore/QUrl>32#include <QtCore/QUrl>
33#include <QtCore/QVariant>
29#include <QtSql/QSqlDatabase>34#include <QtSql/QSqlDatabase>
3035
36class QTimer;
37
38class DbWorker;
39
31class HistoryModel : public QAbstractListModel40class HistoryModel : public QAbstractListModel
32{41{
33 Q_OBJECT42 Q_OBJECT
@@ -74,6 +83,7 @@
74Q_SIGNALS:83Q_SIGNALS:
75 void databasePathChanged() const;84 void databasePathChanged() const;
76 void rowCountChanged();85 void rowCountChanged();
86 void loaded() const;
7787
78protected:88protected:
79 struct HistoryEntry {89 struct HistoryEntry {
@@ -89,15 +99,16 @@
89 int getEntryIndex(const QUrl& url) const;99 int getEntryIndex(const QUrl& url) const;
90 void updateExistingEntryInDatabase(const HistoryEntry& entry);100 void updateExistingEntryInDatabase(const HistoryEntry& entry);
91101
102private Q_SLOTS:
103 void onHiddenEntryFetched(const QUrl& url);
104 void onEntryFetched(const QUrl& url, const QString& domain, const QString& title,
105 const QUrl& icon, int visits, const QDateTime& lastVisit);
106
92private:107private:
93 QMutex m_dbMutex;108 QString m_databasePath;
94 QSqlDatabase m_database;109 QSet<QUrl> m_hiddenEntries;
95
96 QList<QUrl> m_hiddenEntries;
97110
98 void resetDatabase(const QString& databaseName);111 void resetDatabase(const QString& databaseName);
99 void createOrAlterDatabaseSchema();
100 void populateFromDatabase();
101 void removeByIndex(int index);112 void removeByIndex(int index);
102 void insertNewEntryInDatabase(const HistoryEntry& entry);113 void insertNewEntryInDatabase(const HistoryEntry& entry);
103 void insertNewEntryInHiddenDatabase(const QUrl& url);114 void insertNewEntryInHiddenDatabase(const QUrl& url);
@@ -106,6 +117,52 @@
106 void removeEntriesFromDatabaseByDate(const QDate& date);117 void removeEntriesFromDatabaseByDate(const QDate& date);
107 void removeEntriesFromDatabaseByDomain(const QString& domain);118 void removeEntriesFromDatabaseByDomain(const QString& domain);
108 void clearDatabase();119 void clearDatabase();
120
121 QThread m_dbWorkerThread;
122 DbWorker* m_dbWorker;
123};
124
125class DbWorker : public QObject {
126 Q_OBJECT
127
128 Q_ENUMS(Operation)
129
130public:
131 DbWorker();
132 ~DbWorker();
133
134 enum Operation {
135 InsertNewEntry,
136 InsertNewHiddenEntry,
137 UpdateExistingEntry,
138 RemoveEntryByUrl,
139 RemoveHiddenEntryByUrl,
140 RemoveEntriesByDate,
141 RemoveEntriesByDomain,
142 Clear,
143 };
144
145Q_SIGNALS:
146 void resetDatabase(const QString& databaseName);
147 void fetchEntries();
148 void hiddenEntryFetched(const QUrl& url);
149 void entryFetched(const QUrl& url, const QString& domain, const QString& title,
150 const QUrl& icon, int visits, const QDateTime& lastVisit);
151 void loaded();
152 void enqueue(Operation operation, QVariantList values);
153
154private Q_SLOTS:
155 void doResetDatabase(const QString& databaseName);
156 void doCreateOrAlterDatabaseSchema();
157 void doFetchEntries();
158 void doEnqueue(Operation operation, QVariantList values);
159 void doFlush();
160
161private:
162 QSqlDatabase m_database;
163 QReadWriteLock m_lock;
164 QQueue<QPair<Operation, QVariantList>> m_pending;
165 QTimer* m_flush;
109};166};
110167
111#endif // __HISTORY_MODEL_H__168#endif // __HISTORY_MODEL_H__
112169
=== modified file 'src/app/webbrowser/webbrowser-app.qml'
--- src/app/webbrowser/webbrowser-app.qml 2016-09-20 19:55:22 +0000
+++ src/app/webbrowser/webbrowser-app.qml 2016-10-13 14:41:36 +0000
@@ -53,15 +53,6 @@
53 BookmarksModel.databasePath = dataLocation + "/bookmarks.sqlite"53 BookmarksModel.databasePath = dataLocation + "/bookmarks.sqlite"
54 HistoryModel.databasePath = dataLocation + "/history.sqlite"54 HistoryModel.databasePath = dataLocation + "/history.sqlite"
55 DownloadsModel.databasePath = dataLocation + "/downloads.sqlite"55 DownloadsModel.databasePath = dataLocation + "/downloads.sqlite"
56
57 var doNotCleanUrls = []
58 for (var x in allWindows) {
59 var tabs = allWindows[x].tabsModel
60 for (var t = 0; t < tabs.count; ++t) {
61 doNotCleanUrls.push(tabs.get(t).url)
62 }
63 }
64 PreviewManager.cleanUnusedPreviews(doNotCleanUrls)
65 }56 }
6657
67 // Array of all windows, sorted chronologically (most recently active last)58 // Array of all windows, sorted chronologically (most recently active last)
@@ -128,6 +119,20 @@
128 session.clear()119 session.clear()
129 }120 }
130 }121 }
122 if (incognito && (allWindows.length > 1)) {
123 // If the last incognito window is being closed,
124 // prune incognito entries from the downloads model
125 var incognitoWindows = 0
126 for (var w in allWindows) {
127 var window = allWindows[w]
128 if ((window !== this) && window.incognito) {
129 ++incognitoWindows
130 }
131 }
132 if (incognitoWindows == 0) {
133 DownloadsModel.pruneIncognitoDownloads()
134 }
135 }
131 destroy()136 destroy()
132 }137 }
133138
@@ -446,4 +451,18 @@
446 }451 }
447 }452 }
448 }453 }
454
455 property var historyModelMonitor: Connections {
456 target: HistoryModel
457 onLoaded: {
458 var doNotCleanUrls = []
459 for (var x in allWindows) {
460 var tabs = allWindows[x].tabsModel
461 for (var t = 0; t < tabs.count; ++t) {
462 doNotCleanUrls.push(tabs.get(t).url)
463 }
464 }
465 PreviewManager.cleanUnusedPreviews(doNotCleanUrls)
466 }
467 }
449}468}
450469
=== modified file 'src/app/webcontainer/Chrome.qml'
--- src/app/webcontainer/Chrome.qml 2016-05-26 17:07:44 +0000
+++ src/app/webcontainer/Chrome.qml 2016-10-13 14:41:36 +0000
@@ -23,6 +23,7 @@
23ChromeBase {23ChromeBase {
24 id: chrome24 id: chrome
2525
26 property var webview: null
26 property bool navigationButtonsVisible: false27 property bool navigationButtonsVisible: false
27 property bool accountSwitcher: false28 property bool accountSwitcher: false
2829
2930
=== modified file 'src/app/webcontainer/WebApp.qml'
--- src/app/webcontainer/WebApp.qml 2016-09-20 15:32:49 +0000
+++ src/app/webcontainer/WebApp.qml 2016-10-13 14:41:36 +0000
@@ -249,7 +249,8 @@
249 id: progressbarComponent249 id: progressbarComponent
250250
251 ThinProgressBar {251 ThinProgressBar {
252 webview: webapp.currentWebview252 visible: webapp.currentWebview && webapp.currentWebview.loading
253 value: visible ? webapp.currentWebview.loadProgress : 0
253254
254 anchors {255 anchors {
255 left: parent.left256 left: parent.left
256257
=== modified file 'tests/autopilot/webapp_container/tests/__init__.py'
--- tests/autopilot/webapp_container/tests/__init__.py 2016-07-13 16:23:18 +0000
+++ tests/autopilot/webapp_container/tests/__init__.py 2016-10-13 14:41:36 +0000
@@ -52,7 +52,8 @@
52 return LOCAL_BROWSER_CONTAINER_PATH_NAME52 return LOCAL_BROWSER_CONTAINER_PATH_NAME
53 return INSTALLED_BROWSER_CONTAINER_PATH_NAME53 return INSTALLED_BROWSER_CONTAINER_PATH_NAME
5454
55 def launch_webcontainer_app(self, args, envvars={}, is_local_app=False):55 def launch_webcontainer_app(self, args, envvars={}, is_local_app=False,
56 ignore_focus=False):
56 if model() != 'Desktop':57 if model() != 'Desktop':
57 args.append(58 args.append(
58 '--desktop_file_hint=/usr/share/applications/'59 '--desktop_file_hint=/usr/share/applications/'
@@ -74,7 +75,7 @@
74 except:75 except:
75 self.app = None76 self.app = None
7677
77 if not is_local_app:78 if not is_local_app and not ignore_focus:
78 webview = self.get_oxide_webview()79 webview = self.get_oxide_webview()
79 self.assertThat(80 self.assertThat(
80 lambda: webview.activeFocus,81 lambda: webview.activeFocus,
@@ -128,10 +129,12 @@
128 'schemeUriHandleFilterResult(QString)')[-1][0]129 'schemeUriHandleFilterResult(QString)')[-1][0]
129 return result130 return result
130131
131 def browse_to(self, url):132 def browse_to(self, url, wait_for_load=True):
132 webview = self.get_oxide_webview()133 webview = self.get_oxide_webview()
133 webview.slots.navigateToUrl(url)134 webview.slots.navigateToUrl(url)
134 self.assert_page_eventually_loaded(url)135
136 if wait_for_load:
137 self.assert_page_eventually_loaded(url)
135138
136 def kill_app(self, signal=signal.SIGKILL):139 def kill_app(self, signal=signal.SIGKILL):
137 os.kill(self.app.pid, signal)140 os.kill(self.app.pid, signal)
@@ -161,9 +164,9 @@
161 return self.base_url[len(self.BASE_URL_SCHEME):]164 return self.base_url[len(self.BASE_URL_SCHEME):]
162165
163 def launch_webcontainer_app_with_local_http_server(166 def launch_webcontainer_app_with_local_http_server(
164 self, args, path='/', envvars={}, homepage=''):167 self, args, path='/', envvars={}, homepage='', ignore_focus=False):
165 self.url = self.base_url + path168 self.url = self.base_url + path
166 if len(homepage) != 0:169 if len(homepage) != 0:
167 self.url = homepage170 self.url = homepage
168 args.append(self.url)171 args.append(self.url)
169 self.launch_webcontainer_app(args, envvars)172 self.launch_webcontainer_app(args, envvars, ignore_focus=ignore_focus)
170173
=== modified file 'tests/autopilot/webapp_container/tests/fake_servers.py'
--- tests/autopilot/webapp_container/tests/fake_servers.py 2016-05-26 17:07:44 +0000
+++ tests/autopilot/webapp_container/tests/fake_servers.py 2016-10-13 14:41:36 +0000
@@ -391,6 +391,40 @@
391 color_url = qs['color_url_part'][0]391 color_url = qs['color_url_part'][0]
392 self.serve_content(392 self.serve_content(
393 self.local_browse_link_chain_content(next, color_url))393 self.local_browse_link_chain_content(next, color_url))
394 elif self.path == "/js-alert-dialog":
395 self.send_response(200)
396 html = '<html><body><script type="text/javascript">'
397 html += 'window.onload = function() {'
398 html += ' window.alert("Alert Dialog")'
399 html += '} </script></body></html>'
400 self.serve_content(html)
401 elif self.path == "/js-before-unload-dialog":
402 self.send_response(200)
403 html = '<html><body><script type="text/javascript">'
404 html += 'window.onbeforeunload = function(e) {'
405 html += ' var dialogText = "Dialog text here";'
406 html += ' e.returnValue = dialogText;'
407 html += ' return dialogText;'
408 html += '}; </script></body></html>'
409 self.serve_content(html)
410 elif self.path == "/js-confirm-dialog":
411 self.send_response(200)
412 html = '<html><body><script type="text/javascript">'
413 html += 'window.onload = function() {'
414 html += ' if (window.confirm("Confirm Dialog") == true) {'
415 html += ' document.title = "OK" } '
416 html += ' else { document.title = "CANCEL" }'
417 html += '} </script></body></html>'
418 self.serve_content(html)
419 elif self.path == "/js-prompt-dialog":
420 self.send_response(200)
421 html = '<html><body><script type="text/javascript">'
422 html += 'window.onload = function() {'
423 html += ' var result = window.prompt("Prompt Dialog", "Default");'
424 html += ' if (result != null) { document.title = result; } '
425 html += ' else { document.title = "CANCEL" }'
426 html += '} </script></body></html>'
427 self.serve_content(html)
394 else:428 else:
395 self.send_error(404)429 self.send_error(404)
396430
397431
=== added file 'tests/autopilot/webapp_container/tests/test_js_dialogs.py'
--- tests/autopilot/webapp_container/tests/test_js_dialogs.py 1970-01-01 00:00:00 +0000
+++ tests/autopilot/webapp_container/tests/test_js_dialogs.py 2016-10-13 14:41:36 +0000
@@ -0,0 +1,214 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#
3# Copyright 2016 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 webapp_container.tests import WebappContainerTestCaseWithLocalContentBase
18
19from testtools.matchers import Equals
20from autopilot.matchers import Eventually
21
22
23class DialogWrapper(object):
24 def __init__(self, dialog):
25 self.dialog = dialog
26
27 self.text = self.dialog.text
28 self.wait_until_destroyed = self.dialog.wait_until_destroyed
29 self.visible = self.dialog.visible
30
31
32class AlertDialog(DialogWrapper):
33 def get_ok_button(self):
34 return self.dialog.select_single("Button", objectName="okButton")
35
36
37class BeforeUnloadDialog(DialogWrapper):
38 def get_leave_button(self):
39 return self.dialog.select_single("Button", objectName="leaveButton")
40
41 def get_stay_button(self):
42 return self.dialog.select_single("Button", objectName="stayButton")
43
44
45class ConfirmDialog(DialogWrapper):
46 def get_cancel_button(self):
47 return self.dialog.select_single("Button", objectName="cancelButton")
48
49 def get_ok_button(self):
50 return self.dialog.select_single("Button", objectName="okButton")
51
52
53class PromptDialog(DialogWrapper):
54 def get_cancel_button(self):
55 return self.dialog.select_single("Button", objectName="cancelButton")
56
57 def get_input_textfield(self):
58 return self.dialog.select_single("TextField",
59 objectName="inputTextField")
60
61 def get_ok_button(self):
62 return self.dialog.select_single("Button", objectName="okButton")
63
64
65class TestJSDialogs(WebappContainerTestCaseWithLocalContentBase):
66
67 def test_alert(self):
68 self.launch_webcontainer_app_with_local_http_server(
69 [], '/js-alert-dialog', ignore_focus=True)
70
71 dialog = AlertDialog(
72 self.app.wait_select_single("Dialog", objectName="alertDialog")
73 )
74 dialog.visible.wait_for(True)
75
76 # Check alert text is correct
77 self.assertThat(dialog.text, Equals("Alert Dialog"))
78
79 # Click OK, check dialog is destroyed
80 self.pointing_device.click_object(dialog.get_ok_button())
81 dialog.wait_until_destroyed()
82
83 def test_before_unload_leave(self):
84 testUrl = self.base_url + "/"
85
86 self.launch_webcontainer_app_with_local_http_server(
87 [], '/js-before-unload-dialog', ignore_focus=True)
88
89 # Change the url to trigger window.onBeforeUnload
90 self.browse_to(testUrl, wait_for_load=False)
91
92 dialog = BeforeUnloadDialog(
93 self.app.wait_select_single("Dialog",
94 objectName="beforeUnloadDialog")
95 )
96 dialog.visible.wait_for(True)
97
98 # Click leave and check that url changes
99 self.pointing_device.click_object(dialog.get_leave_button())
100
101 self.assertThat(self.get_oxide_webview().url,
102 Eventually(Equals(testUrl)))
103
104 def test_before_unload_stay(self):
105 page = '/js-before-unload-dialog'
106 beforeUnloadUrl = self.base_url + page
107 testUrl = self.base_url + "/"
108
109 self.launch_webcontainer_app_with_local_http_server(
110 [], page, ignore_focus=True)
111
112 # Change the url to trigger window.onBeforeUnload
113 self.browse_to(testUrl, wait_for_load=False)
114
115 dialog = BeforeUnloadDialog(
116 self.app.wait_select_single("Dialog",
117 objectName="beforeUnloadDialog")
118 )
119 dialog.visible.wait_for(True)
120
121 # Click stay and check url does not change
122 self.pointing_device.click_object(dialog.get_stay_button())
123
124 self.assertThat(self.get_oxide_webview().url,
125 Eventually(Equals(beforeUnloadUrl)))
126
127 def test_confirm_cancel(self):
128 self.launch_webcontainer_app_with_local_http_server(
129 [], '/js-confirm-dialog', ignore_focus=True)
130
131 dialog = ConfirmDialog(
132 self.app.wait_select_single("Dialog", objectName="confirmDialog")
133 )
134 dialog.visible.wait_for(True)
135
136 # Check that confirm text is correct
137 self.assertThat(dialog.text, Equals("Confirm Dialog"))
138
139 # Click cancel and check that dialog is destroyed
140 self.pointing_device.click_object(dialog.get_cancel_button())
141 dialog.wait_until_destroyed()
142
143 # Check that title changes to cancel
144 self.assertThat(self.get_webcontainer_webview().title,
145 Eventually(Equals("CANCEL")))
146
147 def test_confirm_ok(self):
148 self.launch_webcontainer_app_with_local_http_server(
149 [], '/js-confirm-dialog', ignore_focus=True)
150
151 dialog = ConfirmDialog(
152 self.app.wait_select_single("Dialog", objectName="confirmDialog")
153 )
154 dialog.visible.wait_for(True)
155
156 # Check that confirm text is correct
157 self.assertThat(dialog.text, Equals("Confirm Dialog"))
158
159 # Click OK and check that dialog is destroyed
160 self.pointing_device.click_object(dialog.get_ok_button())
161 dialog.wait_until_destroyed()
162
163 # Check that title changes to OK
164 self.assertThat(self.get_webcontainer_webview().title,
165 Eventually(Equals("OK")))
166
167 def test_prompt_cancel(self):
168 self.launch_webcontainer_app_with_local_http_server(
169 [], '/js-prompt-dialog', ignore_focus=True)
170
171 dialog = PromptDialog(
172 self.app.wait_select_single("Dialog", objectName="promptDialog")
173 )
174 dialog.visible.wait_for(True)
175
176 # Check that prompt text is correct and default textfield
177 self.assertThat(dialog.text, Equals("Prompt Dialog"))
178 self.assertThat(dialog.get_input_textfield().text,
179 Equals("Default"))
180
181 # Click cancel and check that dialog is destroyed
182 self.pointing_device.click_object(dialog.get_cancel_button())
183 dialog.wait_until_destroyed()
184
185 # Check that title changes to cancel
186 self.assertThat(self.get_webcontainer_webview().title,
187 Eventually(Equals("CANCEL")))
188
189 def test_prompt_ok(self):
190 self.launch_webcontainer_app_with_local_http_server(
191 [], '/js-prompt-dialog', ignore_focus=True)
192
193 dialog = PromptDialog(
194 self.app.wait_select_single("Dialog", objectName="promptDialog")
195 )
196 dialog.visible.wait_for(True)
197
198 # Check that prompt text is correct and default textfield
199 self.assertThat(dialog.text, Equals("Prompt Dialog"))
200 self.assertThat(dialog.get_input_textfield().text,
201 Equals("Default"))
202
203 # Enter text into textfield
204 text = "TEST"
205 entry = dialog.get_input_textfield()
206 entry.write(text)
207
208 # Click ok and check that dialog is destroyed
209 self.pointing_device.click_object(dialog.get_ok_button())
210 dialog.wait_until_destroyed()
211
212 # Check that title changes to text entered in textfield
213 self.assertThat(self.get_webcontainer_webview().title,
214 Eventually(Equals(text)))
0215
=== modified file 'tests/autopilot/webbrowser_app/emulators/browser.py'
--- tests/autopilot/webbrowser_app/emulators/browser.py 2016-08-10 15:43:04 +0000
+++ tests/autopilot/webbrowser_app/emulators/browser.py 2016-10-13 14:41:36 +0000
@@ -179,9 +179,9 @@
179 def get_history_view(self):179 def get_history_view(self):
180 try:180 try:
181 if self.wide:181 if self.wide:
182 return self.select_single(HistoryViewWide)182 return self.wait_select_single(HistoryViewWide)
183 else:183 else:
184 return self.select_single(HistoryView)184 return self.wait_select_single(HistoryView)
185 except exceptions.StateNotFoundError:185 except exceptions.StateNotFoundError:
186 return None186 return None
187187
@@ -238,6 +238,26 @@
238 menu.click_cancel_action()238 menu.click_cancel_action()
239 menu.wait_until_destroyed()239 menu.wait_until_destroyed()
240240
241 def get_alert_dialog(self):
242 return AlertDialog(
243 self.wait_select_single("Dialog", objectName="alertDialog")
244 )
245
246 def get_before_unload_dialog(self):
247 return BeforeUnloadDialog(
248 self.wait_select_single("Dialog", objectName="beforeUnloadDialog")
249 )
250
251 def get_confirm_dialog(self):
252 return ConfirmDialog(
253 self.wait_select_single("Dialog", objectName="confirmDialog")
254 )
255
256 def get_prompt_dialog(self):
257 return PromptDialog(
258 self.wait_select_single("Dialog", objectName="promptDialog")
259 )
260
241261
242class Chrome(uitk.UbuntuUIToolkitCustomProxyObjectBase):262class Chrome(uitk.UbuntuUIToolkitCustomProxyObjectBase):
243263
@@ -724,3 +744,45 @@
724 def click_cancel_action(self):744 def click_cancel_action(self):
725 action = self.select_single("Empty", objectName="cancelAction")745 action = self.select_single("Empty", objectName="cancelAction")
726 self.pointing_device.click_object(action)746 self.pointing_device.click_object(action)
747
748
749class DialogWrapper(object):
750 def __init__(self, dialog):
751 self.dialog = dialog
752
753 self.text = self.dialog.text
754 self.wait_until_destroyed = self.dialog.wait_until_destroyed
755 self.visible = self.dialog.visible
756
757
758class AlertDialog(DialogWrapper):
759 def get_ok_button(self):
760 return self.dialog.select_single("Button", objectName="okButton")
761
762
763class BeforeUnloadDialog(DialogWrapper):
764 def get_leave_button(self):
765 return self.dialog.select_single("Button", objectName="leaveButton")
766
767 def get_stay_button(self):
768 return self.dialog.select_single("Button", objectName="stayButton")
769
770
771class ConfirmDialog(DialogWrapper):
772 def get_cancel_button(self):
773 return self.dialog.select_single("Button", objectName="cancelButton")
774
775 def get_ok_button(self):
776 return self.dialog.select_single("Button", objectName="okButton")
777
778
779class PromptDialog(DialogWrapper):
780 def get_cancel_button(self):
781 return self.dialog.select_single("Button", objectName="cancelButton")
782
783 def get_input_textfield(self):
784 return self.dialog.select_single("TextField",
785 objectName="inputTextField")
786
787 def get_ok_button(self):
788 return self.dialog.select_single("Button", objectName="okButton")
727789
=== modified file 'tests/autopilot/webbrowser_app/tests/http_server.py'
--- tests/autopilot/webbrowser_app/tests/http_server.py 2016-03-07 18:29:23 +0000
+++ tests/autopilot/webbrowser_app/tests/http_server.py 2016-10-13 14:41:36 +0000
@@ -252,6 +252,54 @@
252 html += '50%; transform: translate(-50%, -50%); font-size: 500%">'252 html += '50%; transform: translate(-50%, -50%); font-size: 500%">'
253 html += 'Supercalifragilisticexpialidocious</div></body></html>'253 html += 'Supercalifragilisticexpialidocious</div></body></html>'
254 self.send_html(html)254 self.send_html(html)
255 elif self.path == "/redirect-no-title-header":
256 self.send_response(301)
257 self.send_header("Location", "/redirect-destination")
258 self.end_headers()
259 elif self.path == "/redirect-no-title-js":
260 self.send_response(200)
261 html = '<html><body><script type="text/javascript">'
262 html += 'window.location.href = "/redirect-destination"'
263 html += '</script></body></html>'
264 self.send_html(html)
265 elif self.path == "/redirect-destination":
266 self.send_response(200)
267 html = '<html><body><p>redirect-destination</p></body></html>'
268 self.send_html(html)
269 elif self.path == "/js-alert-dialog":
270 self.send_response(200)
271 html = '<html><body><script type="text/javascript">'
272 html += 'window.onload = function() {'
273 html += ' window.alert("Alert Dialog")'
274 html += '} </script></body></html>'
275 self.send_html(html)
276 elif self.path == "/js-before-unload-dialog":
277 self.send_response(200)
278 html = '<html><body><script type="text/javascript">'
279 html += 'window.onbeforeunload = function(e) {'
280 html += ' var dialogText = "Dialog text here";'
281 html += ' e.returnValue = dialogText;'
282 html += ' return dialogText;'
283 html += '}; </script></body></html>'
284 self.send_html(html)
285 elif self.path == "/js-confirm-dialog":
286 self.send_response(200)
287 html = '<html><body><script type="text/javascript">'
288 html += 'window.onload = function() {'
289 html += ' if (window.confirm("Confirm Dialog") == true) {'
290 html += ' document.title = "OK" } '
291 html += ' else { document.title = "CANCEL" }'
292 html += '} </script></body></html>'
293 self.send_html(html)
294 elif self.path == "/js-prompt-dialog":
295 self.send_response(200)
296 html = '<html><body><script type="text/javascript">'
297 html += 'window.onload = function() {'
298 html += ' var result = window.prompt("Prompt Dialog", "Default");'
299 html += ' if (result != null) { document.title = result; } '
300 html += ' else { document.title = "CANCEL" }'
301 html += '} </script></body></html>'
302 self.send_html(html)
255 else:303 else:
256 self.send_error(404)304 self.send_error(404)
257305
258306
=== modified file 'tests/autopilot/webbrowser_app/tests/test_history.py'
--- tests/autopilot/webbrowser_app/tests/test_history.py 2016-02-29 20:42:13 +0000
+++ tests/autopilot/webbrowser_app/tests/test_history.py 2016-10-13 14:41:36 +0000
@@ -124,3 +124,38 @@
124 self.main_window.wait_until_page_loaded(pushed)124 self.main_window.wait_until_page_loaded(pushed)
125 self.open_history()125 self.open_history()
126 self.expect_history_entries([pushed, url, self.url])126 self.expect_history_entries([pushed, url, self.url])
127
128 def test_title_correct_redirect_header(self):
129 # Regression test for https://launchpad.net/bugs/1603835
130 url_redirect = self.base_url + "/redirect-no-title-header"
131 url_destination = self.base_url + "/redirect-destination"
132 url_test = self.base_url + "/test1"
133
134 self.main_window.go_to_url(url_redirect)
135 self.main_window.wait_until_page_loaded(url_destination)
136
137 self.open_history()
138
139 entries = self.expect_history_entries(
140 [url_destination, url_test]
141 )
142 self.assertThat(entries[0].title, Equals("test/redirect-destination"))
143 self.assertThat(entries[1].title, Equals("test page 1"))
144
145 def test_title_correct_redirect_js(self):
146 # Regression test for https://launchpad.net/bugs/1603835
147 url_redirect = self.base_url + "/redirect-no-title-js"
148 url_destination = self.base_url + "/redirect-destination"
149 url_test = self.base_url + "/test1"
150
151 self.main_window.go_to_url(url_redirect)
152 self.main_window.wait_until_page_loaded(url_destination)
153
154 self.open_history()
155
156 entries = self.expect_history_entries(
157 [url_destination, url_redirect, url_test]
158 )
159 self.assertThat(entries[0].title, Equals("test/redirect-destination"))
160 self.assertThat(entries[1].title, Equals("test/redirect-no-title-js"))
161 self.assertThat(entries[2].title, Equals("test page 1"))
127162
=== added file 'tests/autopilot/webbrowser_app/tests/test_js_dialogs.py'
--- tests/autopilot/webbrowser_app/tests/test_js_dialogs.py 1970-01-01 00:00:00 +0000
+++ tests/autopilot/webbrowser_app/tests/test_js_dialogs.py 2016-10-13 14:41:36 +0000
@@ -0,0 +1,156 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#
3# Copyright 2016 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 testtools.matchers import Equals
18from autopilot.matchers import Eventually
19
20from webbrowser_app.tests import StartOpenRemotePageTestCaseBase
21
22
23class TestJSDialogs(StartOpenRemotePageTestCaseBase):
24
25 def test_alert(self):
26 url = self.base_url + "/js-alert-dialog"
27 self.main_window.go_to_url(url)
28
29 dialog = self.main_window.get_alert_dialog()
30 dialog.visible.wait_for(True)
31
32 # Check alert text is correct
33 self.assertThat(dialog.text, Equals("Alert Dialog"))
34
35 # Click OK, check dialog is destroyed
36 self.pointing_device.click_object(dialog.get_ok_button())
37 dialog.wait_until_destroyed()
38
39 def test_before_unload_leave(self):
40 beforeUnloadUrl = self.base_url + "/js-before-unload-dialog"
41 testUrl = self.base_url + "/test1"
42
43 self.main_window.go_to_url(beforeUnloadUrl)
44 self.main_window.wait_until_page_loaded(beforeUnloadUrl)
45
46 # Change the url to trigger window.onBeforeUnload
47 self.main_window.go_to_url(testUrl)
48
49 dialog = self.main_window.get_before_unload_dialog()
50 dialog.visible.wait_for(True)
51
52 # Click leave and check that url changes
53 self.pointing_device.click_object(dialog.get_leave_button())
54
55 self.assertThat(self.main_window.get_current_webview().url,
56 Eventually(Equals(testUrl)))
57
58 def test_before_unload_stay(self):
59 beforeUnloadUrl = self.base_url + "/js-before-unload-dialog"
60 testUrl = self.base_url + "/test1"
61
62 self.main_window.go_to_url(beforeUnloadUrl)
63 self.main_window.wait_until_page_loaded(beforeUnloadUrl)
64
65 # Change the url to trigger window.onBeforeUnload
66 self.main_window.go_to_url(testUrl)
67
68 dialog = self.main_window.get_before_unload_dialog()
69 dialog.visible.wait_for(True)
70
71 # Click stay and check url does not change
72 self.pointing_device.click_object(dialog.get_stay_button())
73
74 self.assertThat(self.main_window.get_current_webview().url,
75 Eventually(Equals(beforeUnloadUrl)))
76
77 def test_confirm_cancel(self):
78 url = self.base_url + "/js-confirm-dialog"
79 self.main_window.go_to_url(url)
80
81 dialog = self.main_window.get_confirm_dialog()
82 dialog.visible.wait_for(True)
83
84 # Check that confirm text is correct
85 self.assertThat(dialog.text, Equals("Confirm Dialog"))
86
87 # Click cancel and check that dialog is destroyed
88 self.pointing_device.click_object(dialog.get_cancel_button())
89 dialog.wait_until_destroyed()
90
91 # Check that title changes to cancel
92 self.assertThat(self.main_window.get_current_webview().title,
93 Eventually(Equals("CANCEL")))
94
95 def test_confirm_ok(self):
96 url = self.base_url + "/js-confirm-dialog"
97 self.main_window.go_to_url(url)
98
99 dialog = self.main_window.get_confirm_dialog()
100 dialog.visible.wait_for(True)
101
102 # Check that confirm text is correct
103 self.assertThat(dialog.text, Equals("Confirm Dialog"))
104
105 # Click OK and check that dialog is destroyed
106 self.pointing_device.click_object(dialog.get_ok_button())
107 dialog.wait_until_destroyed()
108
109 # Check that title changes to OK
110 self.assertThat(self.main_window.get_current_webview().title,
111 Eventually(Equals("OK")))
112
113 def test_prompt_cancel(self):
114 url = self.base_url + "/js-prompt-dialog"
115 self.main_window.go_to_url(url)
116
117 dialog = self.main_window.get_prompt_dialog()
118 dialog.visible.wait_for(True)
119
120 # Check that prompt text is correct and default textfield
121 self.assertThat(dialog.text, Equals("Prompt Dialog"))
122 self.assertThat(dialog.get_input_textfield().text,
123 Equals("Default"))
124
125 # Click cancel and check that dialog is destroyed
126 self.pointing_device.click_object(dialog.get_cancel_button())
127 dialog.wait_until_destroyed()
128
129 # Check that title changes to cancel
130 self.assertThat(self.main_window.get_current_webview().title,
131 Eventually(Equals("CANCEL")))
132
133 def test_prompt_ok(self):
134 url = self.base_url + "/js-prompt-dialog"
135 self.main_window.go_to_url(url)
136
137 dialog = self.main_window.get_prompt_dialog()
138 dialog.visible.wait_for(True)
139
140 # Check that prompt text is correct and default textfield
141 self.assertThat(dialog.text, Equals("Prompt Dialog"))
142 self.assertThat(dialog.get_input_textfield().text,
143 Equals("Default"))
144
145 # Enter text into textfield
146 text = "TEST"
147 entry = dialog.get_input_textfield()
148 entry.write(text)
149
150 # Click ok and check that dialog is destroyed
151 self.pointing_device.click_object(dialog.get_ok_button())
152 dialog.wait_until_destroyed()
153
154 # Check that title changes to text entered in textfield
155 self.assertThat(self.main_window.get_current_webview().title,
156 Eventually(Equals(text)))
0157
=== modified file 'tests/autopilot/webbrowser_app/tests/test_new_tab_view.py'
--- tests/autopilot/webbrowser_app/tests/test_new_tab_view.py 2016-08-10 15:43:04 +0000
+++ tests/autopilot/webbrowser_app/tests/test_new_tab_view.py 2016-10-13 14:41:36 +0000
@@ -440,14 +440,7 @@
440 folder = folders[0]440 folder = folders[0]
441 folder_cx = folder.globalRect.x + folder.width / 2441 folder_cx = folder.globalRect.x + folder.width / 2
442 folder_cy = folder.globalRect.y + folder.height / 2442 folder_cy = folder.globalRect.y + folder.height / 2
443 # Work around https://launchpad.net/bugs/1499437 by dragging downwards443 self.pointing_device.drag(rect.x, rect.y, folder_cx, folder_cy)
444 # a little bit first, then to the target folder.
445 self.pointing_device.move_to_object(grip)
446 pos = self.pointing_device.position()
447 self.pointing_device.press()
448 self.pointing_device.move(pos[0], pos[1] + 20)
449 self.pointing_device.move(folder_cx, folder_cy)
450 self.pointing_device.release()
451 self.assertThat(grip.globalRect, Eventually(Equals(rect)))444 self.assertThat(grip.globalRect, Eventually(Equals(rect)))
452445
453 # Test that dragging an item to another folder removes it from this one446 # Test that dragging an item to another folder removes it from this one
454447
=== modified file 'tests/unittests/downloads-model/tst_DownloadsModelTests.cpp'
--- tests/unittests/downloads-model/tst_DownloadsModelTests.cpp 2016-01-12 10:37:15 +0000
+++ tests/unittests/downloads-model/tst_DownloadsModelTests.cpp 2016-10-13 14:41:36 +0000
@@ -17,6 +17,8 @@
17 */17 */
1818
19#include <QtCore/QDir>19#include <QtCore/QDir>
20#include <QtCore/QFileInfo>
21#include <QtCore/QTemporaryDir>
20#include <QtCore/QTemporaryFile>22#include <QtCore/QTemporaryFile>
21#include <QtTest/QSignalSpy>23#include <QtTest/QSignalSpy>
22#include <QtTest/QtTest>24#include <QtTest/QtTest>
@@ -27,11 +29,16 @@
27 Q_OBJECT29 Q_OBJECT
2830
29private:31private:
32 QTemporaryDir homeDir;
30 DownloadsModel* model;33 DownloadsModel* model;
3134
32private Q_SLOTS:35private Q_SLOTS:
33 void init()36 void init()
34 {37 {
38 // QStandardPaths::setTestModeEnabled() doesn't affect
39 // QStandardPaths::DownloadLocation, so we must override $HOME to
40 // ensure the test won't write data to the user's home directory.
41 qputenv("HOME", homeDir.path().toUtf8());
35 model = new DownloadsModel;42 model = new DownloadsModel;
36 model->setDatabasePath(":memory:");43 model->setDatabasePath(":memory:");
37 }44 }
@@ -39,6 +46,7 @@
39 void cleanup()46 void cleanup()
40 {47 {
41 delete model;48 delete model;
49 qunsetenv("HOME");
42 }50 }
4351
44 void shouldBeInitiallyEmpty()52 void shouldBeInitiallyEmpty()
@@ -58,90 +66,177 @@
58 QVERIFY(roleNames.contains("paused"));66 QVERIFY(roleNames.contains("paused"));
59 QVERIFY(roleNames.contains("error"));67 QVERIFY(roleNames.contains("error"));
60 QVERIFY(roleNames.contains("created"));68 QVERIFY(roleNames.contains("created"));
69 QVERIFY(roleNames.contains("incognito"));
61 }70 }
6271
63 void shouldContainAddedEntries()72 void shouldContainAddedEntries()
64 {73 {
65 QVERIFY(!model->contains(QStringLiteral("testid")));74 QVERIFY(!model->contains(QStringLiteral("testid")));
66 model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/html"));75 model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/html"), false);
67 QVERIFY(model->contains(QStringLiteral("testid")));76 QVERIFY(model->contains(QStringLiteral("testid")));
68 }77 }
6978
70 void shouldAddNewEntries()79 void shouldAddNewEntries()
71 {80 {
72 QSignalSpy spy(model, SIGNAL(added(QString, QUrl, QString)));81 QSignalSpy spy(model, SIGNAL(rowsInserted(const QModelIndex&, int, int)));
7382
74 model->add("testid", QUrl("http://example.org/"), "text/plain");83 model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
75 QCOMPARE(model->rowCount(), 1);84 QCOMPARE(model->rowCount(), 1);
76 QCOMPARE(spy.count(), 1);85 QCOMPARE(spy.count(), 1);
77 QVariantList args = spy.takeFirst();86 QVariantList args = spy.takeFirst();
78 QCOMPARE(args.at(0).toString(), QString("testid"));87 QCOMPARE(args.at(0).toInt(), 0);
79 QCOMPARE(args.at(1).toUrl(), QUrl("http://example.org/"));88 QCOMPARE(args.at(1).toInt(), 0);
80 QCOMPARE(args.at(2).toString(), QString("text/plain"));89 QCOMPARE(model->data(model->index(0), DownloadsModel::DownloadId).toString(), QStringLiteral("testid"));
90 QCOMPARE(model->data(model->index(0), DownloadsModel::Url).toUrl(), QUrl(QStringLiteral("http://example.org/")));
91 QCOMPARE(model->data(model->index(0), DownloadsModel::Mimetype).toString(), QStringLiteral("text/plain"));
8192
82 model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");93 model->add(QStringLiteral("testid2"), QUrl(QStringLiteral("http://example.org/pdf")), QStringLiteral("application/pdf"), false);
83 QCOMPARE(model->rowCount(), 2);94 QCOMPARE(model->rowCount(), 2);
84 QCOMPARE(spy.count(), 1);95 QCOMPARE(spy.count(), 1);
85 args = spy.takeFirst();96 args = spy.takeFirst();
86 QCOMPARE(args.at(0).toString(), QString("testid2"));97 QCOMPARE(args.at(0).toInt(), 0);
87 QCOMPARE(args.at(1).toUrl(), QUrl("http://example.org/pdf"));98 QCOMPARE(args.at(1).toInt(), 0);
88 QCOMPARE(args.at(2).toString(), QString("application/pdf"));99 QCOMPARE(model->data(model->index(0), DownloadsModel::DownloadId).toString(), QStringLiteral("testid2"));
100 QCOMPARE(model->data(model->index(0), DownloadsModel::Url).toUrl(), QUrl(QStringLiteral("http://example.org/pdf")));
101 QCOMPARE(model->data(model->index(0), DownloadsModel::Mimetype).toString(), QStringLiteral("application/pdf"));
89 }102 }
90103
91 void shouldRemoveCancelled()104 void shouldRemoveCancelled()
92 {105 {
93 model->add("testid", QUrl("http://example.org/"), "text/plain");106 model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
94 model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");107 model->add(QStringLiteral("testid2"), QUrl(QStringLiteral("http://example.org/pdf")), QStringLiteral("application/pdf"), false);
95 model->add("testid3", QUrl("https://example.org/secure.png"), "image/png");108 model->add(QStringLiteral("testid3"), QUrl(QStringLiteral("https://example.org/secure.png")), QStringLiteral("image/png"), false);
96 QCOMPARE(model->rowCount(), 3);109 QCOMPARE(model->rowCount(), 3);
97110
98 model->cancelDownload("testid2");111 model->cancelDownload(QStringLiteral("testid2"));
99 QCOMPARE(model->rowCount(), 2);112 QCOMPARE(model->rowCount(), 2);
100113
101 model->cancelDownload("invalid");114 model->cancelDownload(QStringLiteral("invalid"));
102 QCOMPARE(model->rowCount(), 2);115 QCOMPARE(model->rowCount(), 2);
103 }116 }
104117
105 void shouldCompleteDownloads()118 void shouldCompleteDownloads()
106 {119 {
107 QSignalSpy spy(model, SIGNAL(completeChanged(QString, bool)));120 model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
108121 QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Complete).toBool());
109 model->add("testid", QUrl("http://example.org/"), "text/plain");122 QSignalSpy spy(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
110 QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Complete).toBool());123
111 model->setComplete("testid", true);124 model->setComplete(QStringLiteral("testid"), true);
112 QCOMPARE(spy.count(), 1);125 QVERIFY(model->data(model->index(0, 0), DownloadsModel::Complete).toBool());
113 QVariantList args = spy.takeFirst();126 QCOMPARE(spy.count(), 1);
114 QCOMPARE(args.at(0).toString(), QString("testid"));127 QVariantList args = spy.takeFirst();
115 QCOMPARE(args.at(1).toBool(), true);128 QCOMPARE(args.at(0).toModelIndex().row(), 0);
129 QCOMPARE(args.at(1).toModelIndex().row(), 0);
130 QVector<int> roles = args.at(2).value<QVector<int> >();
131 QCOMPARE(roles.size(), 1);
132 QCOMPARE(roles.at(0), (int) DownloadsModel::Complete);
133
134 model->setComplete(QStringLiteral("testid"), true);
135 QVERIFY(model->data(model->index(0, 0), DownloadsModel::Complete).toBool());
136 QVERIFY(spy.isEmpty());
137
138 model->setComplete(QStringLiteral("testid"), false);
139 QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Complete).toBool());
140 QCOMPARE(spy.count(), 1);
141 args = spy.takeFirst();
142 QCOMPARE(args.at(0).toModelIndex().row(), 0);
143 QCOMPARE(args.at(1).toModelIndex().row(), 0);
144 roles = args.at(2).value<QVector<int> >();
145 QCOMPARE(roles.size(), 1);
146 QCOMPARE(roles.at(0), (int) DownloadsModel::Complete);
147 }
148
149 void shouldSetError()
150 {
151 model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
152 QVERIFY(model->data(model->index(0, 0), DownloadsModel::Error).toString().isEmpty());
153 QSignalSpy spy(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
154
155 model->setError(QStringLiteral("testid"), QStringLiteral("foo"));
156 QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Error).toString(), QStringLiteral("foo"));
157 QCOMPARE(spy.count(), 1);
158 QVariantList args = spy.takeFirst();
159 QCOMPARE(args.at(0).toModelIndex().row(), 0);
160 QCOMPARE(args.at(1).toModelIndex().row(), 0);
161 QVector<int> roles = args.at(2).value<QVector<int> >();
162 QCOMPARE(roles.size(), 1);
163 QCOMPARE(roles.at(0), (int) DownloadsModel::Error);
164
165 model->setError(QStringLiteral("testid"), QStringLiteral("foo"));
166 QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Error).toString(), QStringLiteral("foo"));
167 QVERIFY(spy.isEmpty());
168
169 model->setError(QStringLiteral("testid"), QString("bar"));
170 QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Error).toString(), QStringLiteral("bar"));
171 QCOMPARE(spy.count(), 1);
172 args = spy.takeFirst();
173 QCOMPARE(args.at(0).toModelIndex().row(), 0);
174 QCOMPARE(args.at(1).toModelIndex().row(), 0);
175 roles = args.at(2).value<QVector<int> >();
176 QCOMPARE(roles.size(), 1);
177 QCOMPARE(roles.at(0), (int) DownloadsModel::Error);
178 }
179
180 void shouldPauseAndResumeDownload()
181 {
182 model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
183 QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Paused).toBool());
184 QSignalSpy spy(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
185
186 model->pauseDownload(QStringLiteral("testid"));
187 QVERIFY(model->data(model->index(0, 0), DownloadsModel::Paused).toBool());
188 QCOMPARE(spy.count(), 1);
189 QVariantList args = spy.takeFirst();
190 QCOMPARE(args.at(0).toModelIndex().row(), 0);
191 QCOMPARE(args.at(1).toModelIndex().row(), 0);
192 QVector<int> roles = args.at(2).value<QVector<int> >();
193 QCOMPARE(roles.size(), 1);
194 QCOMPARE(roles.at(0), (int) DownloadsModel::Paused);
195
196 model->pauseDownload(QStringLiteral("testid"));
197 QVERIFY(model->data(model->index(0, 0), DownloadsModel::Paused).toBool());
198 QVERIFY(spy.isEmpty());
199
200 model->resumeDownload(QStringLiteral("testid"));
201 QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Paused).toBool());
202 QCOMPARE(spy.count(), 1);
203 args = spy.takeFirst();
204 QCOMPARE(args.at(0).toModelIndex().row(), 0);
205 QCOMPARE(args.at(1).toModelIndex().row(), 0);
206 roles = args.at(2).value<QVector<int> >();
207 QCOMPARE(roles.size(), 1);
208 QCOMPARE(roles.at(0), (int) DownloadsModel::Paused);
116 }209 }
117210
118 void shouldKeepEntriesSortedChronologically()211 void shouldKeepEntriesSortedChronologically()
119 {212 {
120 model->add("testid", QUrl("http://example.org/"), "text/plain");213 model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
121 model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");214 model->add(QStringLiteral("testid2"), QUrl(QStringLiteral("http://example.org/pdf")), QStringLiteral("application/pdf"), false);
122 model->add("testid3", QUrl("https://example.org/secure.png"), "image/png");215 model->add(QStringLiteral("testid3"), QUrl(QStringLiteral("https://example.org/secure.png")), QStringLiteral("image/png"), false);
123216
124 QCOMPARE(model->data(model->index(0, 0), DownloadsModel::DownloadId).toString(), QString("testid3"));217 QCOMPARE(model->data(model->index(0, 0), DownloadsModel::DownloadId).toString(), QStringLiteral("testid3"));
125 QCOMPARE(model->data(model->index(1, 0), DownloadsModel::DownloadId).toString(), QString("testid2"));218 QCOMPARE(model->data(model->index(1, 0), DownloadsModel::DownloadId).toString(), QStringLiteral("testid2"));
126 QCOMPARE(model->data(model->index(2, 0), DownloadsModel::DownloadId).toString(), QString("testid"));219 QCOMPARE(model->data(model->index(2, 0), DownloadsModel::DownloadId).toString(), QStringLiteral("testid"));
127 }220 }
128221
129 void shouldReturnData()222 void shouldReturnData()
130 {223 {
131 model->add("testid", QUrl("http://example.org/"), "text/plain");224 model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
132 QVERIFY(!model->data(QModelIndex(), DownloadsModel::DownloadId).isValid());225 QVERIFY(!model->data(QModelIndex(), DownloadsModel::DownloadId).isValid());
133 QVERIFY(!model->data(model->index(-1, 0), DownloadsModel::DownloadId).isValid());226 QVERIFY(!model->data(model->index(-1, 0), DownloadsModel::DownloadId).isValid());
134 QVERIFY(!model->data(model->index(3, 0), DownloadsModel::DownloadId).isValid());227 QVERIFY(!model->data(model->index(3, 0), DownloadsModel::DownloadId).isValid());
135 QCOMPARE(model->data(model->index(0, 0), DownloadsModel::DownloadId).toString(), QString("testid"));228 QCOMPARE(model->data(model->index(0, 0), DownloadsModel::DownloadId).toString(), QStringLiteral("testid"));
136 QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Url).toUrl(), QUrl("http://example.org/"));229 QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Url).toUrl(), QUrl(QStringLiteral("http://example.org/")));
137 QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Mimetype).toString(), QString("text/plain"));230 QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Mimetype).toString(), QStringLiteral("text/plain"));
138 QVERIFY(model->data(model->index(0, 0), DownloadsModel::Created).toDateTime() <= QDateTime::currentDateTime());231 QVERIFY(model->data(model->index(0, 0), DownloadsModel::Created).toDateTime() <= QDateTime::currentDateTime());
139 QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Complete).toBool());232 QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Complete).toBool());
233 QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Incognito).toBool());
234 QVERIFY(!model->data(model->index(0, 0), -1).isValid());
140 }235 }
141236
142 void shouldReturnDatabasePath()237 void shouldReturnDatabasePath()
143 {238 {
144 QCOMPARE(model->databasePath(), QString(":memory:"));239 QCOMPARE(model->databasePath(), QStringLiteral(":memory:"));
145 }240 }
146241
147 void shouldNotifyWhenSettingDatabasePath()242 void shouldNotifyWhenSettingDatabasePath()
@@ -149,14 +244,14 @@
149 QSignalSpy spyPath(model, SIGNAL(databasePathChanged()));244 QSignalSpy spyPath(model, SIGNAL(databasePathChanged()));
150 QSignalSpy spyReset(model, SIGNAL(modelReset()));245 QSignalSpy spyReset(model, SIGNAL(modelReset()));
151246
152 model->setDatabasePath(":memory:");247 model->setDatabasePath(QStringLiteral(":memory:"));
153 QVERIFY(spyPath.isEmpty());248 QVERIFY(spyPath.isEmpty());
154 QVERIFY(spyReset.isEmpty());249 QVERIFY(spyReset.isEmpty());
155250
156 model->setDatabasePath("");251 model->setDatabasePath(QStringLiteral(""));
157 QCOMPARE(spyPath.count(), 1);252 QCOMPARE(spyPath.count(), 1);
158 QCOMPARE(spyReset.count(), 1);253 QCOMPARE(spyReset.count(), 1);
159 QCOMPARE(model->databasePath(), QString(":memory:"));254 QCOMPARE(model->databasePath(), QStringLiteral(":memory:"));
160 }255 }
161256
162 void shouldSerializeOnDisk()257 void shouldSerializeOnDisk()
@@ -167,8 +262,10 @@
167 delete model;262 delete model;
168 model = new DownloadsModel;263 model = new DownloadsModel;
169 model->setDatabasePath(fileName);264 model->setDatabasePath(fileName);
170 model->add("testid", QUrl("http://example.org/"), "text/plain");265 model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
171 model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");266 model->add(QStringLiteral("testid2"), QUrl(QStringLiteral("http://example.org/pdf")), QStringLiteral("application/pdf"), false);
267 model->add(QStringLiteral("testid3"), QUrl(QStringLiteral("http://example.org/incognito.pdf")), QStringLiteral("application/pdf"), true);
268 QCOMPARE(model->rowCount(), 3);
172 delete model;269 delete model;
173 model = new DownloadsModel;270 model = new DownloadsModel;
174 model->setDatabasePath(fileName);271 model->setDatabasePath(fileName);
@@ -180,17 +277,139 @@
180 {277 {
181 QCOMPARE(model->property("count").toInt(), 0);278 QCOMPARE(model->property("count").toInt(), 0);
182 QCOMPARE(model->rowCount(), 0);279 QCOMPARE(model->rowCount(), 0);
183 model->add("testid", QUrl("http://example.org/"), "text/plain");280 model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
184 QCOMPARE(model->property("count").toInt(), 1);281 QCOMPARE(model->property("count").toInt(), 1);
185 QCOMPARE(model->rowCount(), 1);282 QCOMPARE(model->rowCount(), 1);
186 model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");283 model->add(QStringLiteral("testid2"), QUrl(QStringLiteral("http://example.org/pdf")), QStringLiteral("application/pdf"), false);
187 QCOMPARE(model->property("count").toInt(), 2);284 QCOMPARE(model->property("count").toInt(), 2);
188 QCOMPARE(model->rowCount(), 2);285 QCOMPARE(model->rowCount(), 2);
189 model->add("testid3", QUrl("https://example.org/secure.png"), "image/png");286 model->add(QStringLiteral("testid3"), QUrl(QStringLiteral("https://example.org/secure.png")), QStringLiteral("image/png"), false);
190 QCOMPARE(model->property("count").toInt(), 3);287 QCOMPARE(model->property("count").toInt(), 3);
191 QCOMPARE(model->rowCount(), 3);288 QCOMPARE(model->rowCount(), 3);
192 }289 }
193290
291 void shouldPruneIncognitoDownloads()
292 {
293 model->add(QStringLiteral("testid1"), QUrl(QStringLiteral("http://example.org/1")), QStringLiteral("text/plain"), false);
294 model->add(QStringLiteral("testid2"), QUrl(QStringLiteral("http://example.org/2")), QStringLiteral("text/plain"), true);
295 model->add(QStringLiteral("testid3"), QUrl(QStringLiteral("http://example.org/3")), QStringLiteral("text/plain"), false);
296 model->add(QStringLiteral("testid4"), QUrl(QStringLiteral("http://example.org/4")), QStringLiteral("text/plain"), true);
297 QCOMPARE(model->rowCount(), 4);
298 QSignalSpy spyRowsRemoved(model, SIGNAL(rowsRemoved(const QModelIndex&, int, int)));
299 QSignalSpy spyRowCountChanged(model, SIGNAL(rowCountChanged()));
300 model->pruneIncognitoDownloads();
301 QCOMPARE(model->rowCount(), 2);
302 QCOMPARE(model->data(model->index(0), DownloadsModel::DownloadId).toString(), QStringLiteral("testid3"));
303 QCOMPARE(model->data(model->index(1), DownloadsModel::DownloadId).toString(), QStringLiteral("testid1"));
304 QCOMPARE(spyRowsRemoved.count(), 2);
305 QCOMPARE(spyRowCountChanged.count(), 2);
306 }
307
308 void shouldFailToMoveInvalidDownload()
309 {
310 model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
311 QTemporaryFile tempFile;
312 tempFile.open();
313 QSignalSpy spy(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
314 model->moveToDownloads(QStringLiteral("foobar"), tempFile.fileName());
315 QVERIFY(spy.isEmpty());
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to status/vote changes: