Merge lp:~osomon/webbrowser-app/staging-trunk-landing-20161023 into lp:webbrowser-app

Proposed by Olivier Tilloy
Status: Merged
Approved by: Andrew Hayzen
Approved revision: 1565
Merged at revision: 1552
Proposed branch: lp:~osomon/webbrowser-app/staging-trunk-landing-20161023
Merge into: lp:webbrowser-app
Diff against target: 4991 lines (+2469/-1161)
52 files modified
.bzrignore (+1/-0)
CMakeLists.txt (+3/-6)
debian/changelog (+35/-0)
debian/control (+1/-2)
debian/rules (+2/-1)
make-snap.sh (+4/-0)
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 (+10/-9)
src/Ubuntu/Web/ua-overrides-mobile.js.in (+13/-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 (+4/-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 (+216/-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 (+158/-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:~osomon/webbrowser-app/staging-trunk-landing-20161023
Reviewer Review Type Date Requested Status
Andrew Hayzen (community) Approve
system-apps-ci-bot continuous-integration Needs Fixing
Review via email: mp+309094@code.launchpad.net

Commit message

[ Andrew Hayzen ]
* Fix for issue where many tabs causes close button to overlap other tabs (LP: #1473630)
* When page has started, stopped, redirected or errored clear cache for history update - which prevents incorrect titles in being set (LP: #1603835)
* Add autopilot tests javascript dialogs to webbrowser and webapp-container - alertDialog, beforeUnloadDialog, confirmDialog and promptDialog (LP: #1633040)
* Add user-agent override to display the new twitter mobile interface (LP: #1577834)

[ Florian Boucault ]
* Improved startup time by 800ms by delaying QML compilation and making it asynchronous

[ Olivier Tilloy ]
* Replace chromium version in UA overrides at runtime, not at build time (LP: #1599695)
* Initial support for generating a snap package for webbrowser-app (LP: #1629009)
* Do not persist references to incognito downloads on disk (LP: #1625519)
* Increase test coverage (to 97.5%) for DownloadsModel (LP: #1534102)
* Various performance optimizations linked to load events (LP: #1611680)
* Ensure a tab is loaded when re-opened (LP: #1632246)
* Fix drag'n'drop of bookmarks within the new tab view (LP: #1584868)
* Work around a limitation in the sound and microphone policy groups to "fix" sound in yakkety an zesty (LP: #1632620)

To post a comment you must log in.
Revision history for this message
system-apps-ci-bot (system-apps-ci-bot) wrote :

FAILED: Continuous integration, rev:1565
https://jenkins.canonical.com/system-apps/job/lp-webbrowser-app-ci/716/
Executed test runs:
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build/1877
    FAILURE: https://jenkins.canonical.com/system-apps/job/test-0-autopkgtest/label=phone-armhf,release=vivid+overlay,testname=default/454/console
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-0-fetch/1878
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=vivid+overlay/1718
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=vivid+overlay/1718/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=xenial+overlay/1718
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=xenial+overlay/1718/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=yakkety/1718
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=yakkety/1718/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=vivid+overlay/1718
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=vivid+overlay/1718/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=xenial+overlay/1718
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=xenial+overlay/1718/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=yakkety/1718
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=yakkety/1718/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=vivid+overlay/1718
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=vivid+overlay/1718/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=xenial+overlay/1718
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=xenial+overlay/1718/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=yakkety/1718
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=yakkety/1718/artifact/output/*zip*/output.zip

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

review: Needs Fixing (continuous-integration)
Revision history for this message
Andrew Hayzen (ahayzen) wrote :

LGTM :-)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2016-06-13 13:41:48 +0000
3+++ .bzrignore 2016-10-23 20:25:22 +0000
4@@ -31,6 +31,7 @@
5 doc/html
6 click-hooks/webapp-container-hook
7 click-hooks/webapp-container.hook
8+setup/gui/webbrowser-app.desktop
9
10 obj-*
11 debian/usr.bin.webbrowser-app
12
13=== modified file 'CMakeLists.txt'
14--- CMakeLists.txt 2016-07-01 13:09:27 +0000
15+++ CMakeLists.txt 2016-10-23 20:25:22 +0000
16@@ -11,11 +11,6 @@
17 if(NOT INTLTOOL_EXTRACT)
18 message(FATAL_ERROR "Could not find intltool-extract, please install the intltool package")
19 endif()
20-find_program(XVFBRUN xvfb-run)
21-if(NOT XVFBRUN)
22- message(FATAL_ERROR "Could not find xvfb-run, please install the xvfb package")
23-endif()
24-set(XVFB_COMMAND ${XVFBRUN} -s "-screen 0 640x480x24" -a)
25
26 # Standard install paths
27 include(GNUInstallDirs)
28@@ -67,13 +62,15 @@
29 add_subdirectory(tests)
30
31 # make non compiled files (QML, JS, images, etc.) visible in QtCreator
32-file(GLOB NON_COMPILED_ROOT *.png .bzrignore COPYING README)
33+file(GLOB NON_COMPILED_ROOT *.png .bzrignore COPYING make-snap.sh README snapcraft.yaml)
34 file(GLOB_RECURSE NON_COMPILED_SUBDIRS
35 debian/*.dirs debian/*.install debian/*.lintian-overrides debian/*.manifest
36 debian/compat debian/control debian/copyright debian/rules debian/source/format
37 debian/tests/*
38 doc/*.css doc/*.qdoc doc/*.qdocconf
39 po/*.po po/*.pot
40+ setup/gui/*.png setup/gui/webbrowser-app.desktop.in
41+ snap/webbrowser-app.launcher
42 src/*.js src/*.qml src/*.sci src/README
43 tests/*.py tests/*.qml)
44 add_custom_target(NON_COMPILED_TARGET ALL SOURCES ${NON_COMPILED_ROOT} ${NON_COMPILED_SUBDIRS})
45
46=== modified file 'debian/changelog'
47--- debian/changelog 2016-09-28 08:25:12 +0000
48+++ debian/changelog 2016-10-23 20:25:22 +0000
49@@ -1,3 +1,38 @@
50+webbrowser-app (0.23+16.10.20161023-0ubuntu2) UNRELEASED; urgency=medium
51+
52+ [ Andrew Hayzen ]
53+ * Fix for issue where many tabs causes close button to overlap other
54+ tabs (LP: #1473630)
55+ * When page has started, stopped, redirected or errored clear cache for
56+ history update - which prevents incorrect titles in being set
57+ (LP: #1603835)
58+ * Add autopilot tests javascript dialogs to webbrowser and
59+ webapp-container - alertDialog, beforeUnloadDialog, confirmDialog and
60+ promptDialog (LP: #1633040)
61+ * Add user-agent override to display the new twitter mobile interface
62+ (LP: #1577834)
63+
64+ [ Florian Boucault ]
65+ * Improved startup time by 800ms by delaying QML compilation and making
66+ it asynchronous
67+
68+ [ Olivier Tilloy ]
69+ * Replace chromium version in UA overrides at runtime, not at build
70+ time (LP: #1599695)
71+ * Initial support for generating a snap package for webbrowser-app
72+ (LP: #1629009)
73+ * Do not persist references to incognito downloads on disk
74+ (LP: #1625519)
75+ * Increase test coverage (to 97.5%) for DownloadsModel (LP: #1534102)
76+ * Various performance optimizations linked to load events
77+ (LP: #1611680)
78+ * Ensure a tab is loaded when re-opened (LP: #1632246)
79+ * Fix drag'n'drop of bookmarks within the new tab view (LP: #1584868)
80+ * Work around a limitation in the sound and microphone policy groups
81+ to "fix" sound in yakkety an zesty (LP: #1632620)
82+
83+ -- Olivier Tilloy <olivier.tilloy@canonical.com> Sun, 23 Oct 2016 22:18:57 +0200
84+
85 webbrowser-app (0.23+16.10.20160928-0ubuntu1) yakkety; urgency=medium
86
87 [ Andrew Hayzen ]
88
89=== modified file 'debian/control'
90--- debian/control 2016-08-25 09:52:00 +0000
91+++ debian/control 2016-10-23 20:25:22 +0000
92@@ -11,7 +11,7 @@
93 dh-translations,
94 libapparmor-dev,
95 libevdev-dev,
96- liboxideqt-qmlplugin (>= 1.15),
97+ liboxideqt-qmlplugin (>= 1.12),
98 libqt5sql5-sqlite,
99 libudev-dev,
100 lsb-release,
101@@ -23,7 +23,6 @@
102 qml-module-qtquick2 (>= 5.4),
103 qml-module-qtquick-layouts,
104 qml-module-qttest,
105- qmlscene,
106 qt5-default,
107 qt5-qmake,
108 qtbase5-dev (>= 5.4),
109
110=== modified file 'debian/rules'
111--- debian/rules 2016-09-27 16:19:42 +0000
112+++ debian/rules 2016-10-23 20:25:22 +0000
113@@ -18,7 +18,8 @@
114 sed 's#/run/shm/\.org\.chromium\.Chromium\.\*#/{dev,run}/shm/.org.chromium.Chromium.*#g' | \
115 egrep -v 'deny /run/udev/data/\*\* r,' | \
116 sed 's#^}$$#\n /sys/class/ r,\n /sys/class/input/ r,\n /run/udev/data/** r,\n}#g' | \
117- egrep -v '^\s*deny /dev/ r,\s*$$' \
118+ egrep -v '^\s*deny /dev/ r,\s*$$' | \
119+ sed 's#^\(\s*\)deny\(\s\+/{run,dev}/shm/pulse-shm\*\s\+w,\).*$$#\1owner\2#g' \
120 > ./debian/usr.bin.webbrowser-app
121 ifeq ($(DEB_BUILD_GNU_TYPE),$(DEB_HOST_GNU_TYPE))
122 apparmor_parser -QTK ./debian/usr.bin.webbrowser-app
123
124=== added file 'make-snap.sh'
125--- make-snap.sh 1970-01-01 00:00:00 +0000
126+++ make-snap.sh 2016-10-23 20:25:22 +0000
127@@ -0,0 +1,4 @@
128+#!/bin/sh
129+SNAP_DESKTOP_FILE=setup/gui/webbrowser-app.desktop
130+intltool-merge -d -u po $SNAP_DESKTOP_FILE.in $SNAP_DESKTOP_FILE
131+snapcraft
132
133=== added directory 'setup'
134=== added directory 'setup/gui'
135=== added symlink 'setup/gui/icon.png'
136=== target is u'../../webbrowser-app.png'
137=== added symlink 'setup/gui/screenshot.png'
138=== target is u'../../screenshot.png'
139=== added file 'setup/gui/webbrowser-app.desktop.in'
140--- setup/gui/webbrowser-app.desktop.in 1970-01-01 00:00:00 +0000
141+++ setup/gui/webbrowser-app.desktop.in 2016-10-23 20:25:22 +0000
142@@ -0,0 +1,27 @@
143+[Desktop Entry]
144+Version=1.0
145+_Name=Browser
146+_GenericName=Web Browser
147+_Comment=Browse the World Wide Web
148+_Keywords=Internet;WWW;Browser;Web;Explorer
149+Type=Application
150+Icon=${SNAP}/meta/gui/icon.png
151+Exec=webbrowser-app %u
152+Terminal=false
153+Categories=Network;WebBrowser;
154+MimeType=text/html;text/xml;application/xhtml+xml;x-scheme-handler/http;x-scheme-handler/https;
155+X-Ubuntu-Touch=true
156+X-Ubuntu-Gettext-Domain=webbrowser-app
157+X-Ubuntu-Single-Instance=true
158+X-Ubuntu-Default-Department-ID=web-browsers
159+X-Screenshot=${SNAP}/meta/gui/screenshot.png
160+X-Ubuntu-Splash-Color=#FFFFFF
161+Actions=NewWindow;Incognito;
162+
163+[Desktop Action NewWindow]
164+_Name=Open a New Window
165+Exec=webbrowser-app --new-window
166+
167+[Desktop Action Incognito]
168+_Name=Open a New Private Window
169+Exec=webbrowser-app --incognito
170
171=== added directory 'snap'
172=== added file 'snap/webbrowser-app.launcher'
173--- snap/webbrowser-app.launcher 1970-01-01 00:00:00 +0000
174+++ snap/webbrowser-app.launcher 2016-10-23 20:25:22 +0000
175@@ -0,0 +1,10 @@
176+#!/bin/sh
177+
178+# Disable the chromium sandbox to work around https://launchpad.net/bugs/1599234.
179+# Rely on snapd’s security policy instead.
180+export OXIDE_NO_SANDBOX=1
181+
182+# Explicitly set APP_ID.
183+export APP_ID=webbrowser-app
184+
185+exec "$SNAP/bin/desktop-launch" "webbrowser-app" --desktop_file_hint=unity8 "$@"
186
187=== added file 'snapcraft.yaml'
188--- snapcraft.yaml 1970-01-01 00:00:00 +0000
189+++ snapcraft.yaml 2016-10-23 20:25:22 +0000
190@@ -0,0 +1,66 @@
191+name: webbrowser-app
192+version: 0.23+16.10.20160928-0ubuntu1
193+summary: Ubuntu web browser
194+description: A lightweight web browser tailored for Ubuntu, based on the Oxide browser engine and using the Ubuntu UI components.
195+confinement: strict
196+
197+apps:
198+ webbrowser-app:
199+ command: webbrowser-app.launcher
200+ plugs:
201+ - browser-sandbox
202+ - camera
203+ - network
204+ - network-bind
205+ - opengl
206+ - pulseaudio
207+ - screen-inhibit-control
208+ - unity7
209+
210+plugs:
211+ browser-sandbox:
212+ interface: browser-support
213+ allow-sandbox: true
214+
215+parts:
216+ webbrowser-app:
217+ plugin: cmake
218+ source: .
219+ build-packages:
220+ - intltool
221+ - libapparmor-dev
222+ - libevdev-dev
223+ - libudev-dev
224+ - lsb-release
225+ - pkg-config
226+ - qt5-default
227+ - qt5-qmake
228+ - qtbase5-dev
229+ - qtbase5-dev-tools
230+ - qtbase5-private-dev
231+ - qtdeclarative5-dev
232+ - qttools5-dev-tools
233+ - xvfb
234+ stage-packages:
235+ - fonts-liberation
236+ - liboxideqt-qmlplugin
237+ - libqt5sql5-sqlite
238+ - mir-graphics-drivers-desktop
239+ - qml-module-qt-labs-folderlistmodel
240+ - qml-module-qt-labs-settings
241+ - qml-module-qtquick2
242+ - qml-module-qtquick-layouts
243+ - qml-module-qtquick-window2
244+ - qml-module-ubuntu-components
245+ - qml-module-ubuntu-thumbnailer0.1
246+ - qtdeclarative5-ubuntu-content1
247+ - qtdeclarative5-ubuntu-download-manager0.1
248+ - qtdeclarative5-unity-action-plugin
249+ - qtubuntu-desktop
250+ after: [desktop-qt5]
251+
252+ launcher:
253+ plugin: dump
254+ source: snap
255+ organize:
256+ webbrowser-app.launcher: bin/webbrowser-app.launcher
257
258=== modified file 'src/Ubuntu/CMakeLists.txt'
259--- src/Ubuntu/CMakeLists.txt 2016-07-01 13:06:40 +0000
260+++ src/Ubuntu/CMakeLists.txt 2016-10-23 20:25:22 +0000
261@@ -23,13 +23,5 @@
262 OUTPUT_VARIABLE UBUNTU_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE)
263 add_definitions(-DUBUNTU_VERSION="${UBUNTU_VERSION}")
264
265-execute_process(COMMAND ${XVFB_COMMAND} qmlscene --quit ${CMAKE_CURRENT_SOURCE_DIR}/chromium-version.qml
266- OUTPUT_VARIABLE CHROMIUM_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE)
267-string(REGEX MATCH "\\[(.*)\\]" _ ${CHROMIUM_VERSION})
268-set(CHROMIUM_VERSION ${CMAKE_MATCH_1})
269-if(NOT CHROMIUM_VERSION MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$")
270- message(FATAL_ERROR "Invalid chromium version: '${CHROMIUM_VERSION}'")
271-endif()
272-
273 add_subdirectory(Components)
274 add_subdirectory(Web)
275
276=== modified file 'src/Ubuntu/Web/UbuntuWebContext.qml'
277--- src/Ubuntu/Web/UbuntuWebContext.qml 2016-09-13 16:07:06 +0000
278+++ src/Ubuntu/Web/UbuntuWebContext.qml 2016-10-23 20:25:22 +0000
279@@ -91,7 +91,13 @@
280 }
281 if (temp !== null) {
282 console.log("Loaded %1 UA override(s) from %2".arg(temp.overrides.length).arg(Qt.resolvedUrl(script)))
283- userAgentOverrides = temp.overrides
284+ var chromiumVersion = Oxide.Oxide.chromiumVersion
285+ var overrides = []
286+ for (var o in temp.overrides) {
287+ var override = temp.overrides[o]
288+ overrides.push([override[0], override[1].replace(/\$\{CHROMIUM_VERSION\}/g, chromiumVersion)])
289+ }
290+ userAgentOverrides = overrides
291 temp.destroy()
292 }
293 }
294
295=== modified file 'src/Ubuntu/Web/ua-overrides-desktop.js.in'
296--- src/Ubuntu/Web/ua-overrides-desktop.js.in 2016-09-21 16:15:10 +0000
297+++ src/Ubuntu/Web/ua-overrides-desktop.js.in 2016-10-23 20:25:22 +0000
298@@ -19,17 +19,18 @@
299 .pragma library
300
301 var overrides = [
302- ["^https?:\/\/.+\.google\.com\/calendar", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chromium/@CHROMIUM_VERSION@ Chrome/@CHROMIUM_VERSION@ Safari/537.36"],
303- ["^http:\/\/chrome\.angrybirds\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Safari/537.36"], // http://pad.lv/1284158
304- ["^https?:\/\/(www\.)?youtube\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Safari/537.36"], // http://pad.lv/1412880
305- ["^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
306- ["^https?:\/\/mail\.google\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Safari/537.36"], // http://pad.lv/1452616
307+ ["^https?:\/\/.+\.google\.com\/calendar", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chromium/${CHROMIUM_VERSION} Chrome/${CHROMIUM_VERSION} Safari/537.36"],
308+ ["^http:\/\/chrome\.angrybirds\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Safari/537.36"], // http://pad.lv/1284158
309+ ["^https?:\/\/(www\.)?youtube\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Safari/537.36"], // http://pad.lv/1412880
310+ ["^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
311+ ["^https?:\/\/mail\.google\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Safari/537.36"], // http://pad.lv/1452616
312+ ["^https?:\/\/mobile\.twitter\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Safari/537.36"], // http://pad.lv/1577834
313
314 // Google hangouts (https://launchpad.net/bugs/1565055)
315- ["^https?:\/\/hangouts\.google\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Safari/537.36"],
316- ["^https?:\/\/talkgadget\.google\.com\/hangouts\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Safari/537.36"],
317- ["^https?:\/\/plus\.google\.com\/hangouts\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Safari/537.36"],
318+ ["^https?:\/\/hangouts\.google\.com\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Safari/537.36"],
319+ ["^https?:\/\/talkgadget\.google\.com\/hangouts\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Safari/537.36"],
320+ ["^https?:\/\/plus\.google\.com\/hangouts\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Safari/537.36"],
321
322 // Google recaptcha (https://launchpad.net/bugs/1599146)
323- ["^https:\/\/www\.google\.com\/recaptcha\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/@CHROMIUM_VERSION@ Safari/537.36"],
324+ ["^https:\/\/www\.google\.com\/recaptcha\/", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Safari/537.36"],
325 ];
326
327=== modified file 'src/Ubuntu/Web/ua-overrides-mobile.js.in'
328--- src/Ubuntu/Web/ua-overrides-mobile.js.in 2016-08-19 10:10:12 +0000
329+++ src/Ubuntu/Web/ua-overrides-mobile.js.in 2016-10-23 20:25:22 +0000
330@@ -19,18 +19,20 @@
331 .pragma library
332
333 var overrides = [
334- ["^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
335- ["^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
336- ["^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
337- ["^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
338- ["^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
339- ["^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
340- ["^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
341+ ["^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
342+ ["^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
343+ ["^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
344+ ["^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
345+ ["^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
346+ ["^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
347+ ["^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
348+ ["^https?:\/\/mobile\.twitter\.com\/", "Mozilla/5.0 (Linux; Ubuntu @UBUNTU_VERSION@;) AppleWebKit/537.36 Chrome/${CHROMIUM_VERSION} Mobile Safari/537.36"], // http://pad.lv/1577834
349+
350 // Google hangouts (https://launchpad.net/bugs/1565055)
351- ["^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"],
352- ["^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"],
353- ["^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"],
354+ ["^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"],
355+ ["^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"],
356+ ["^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"],
357
358 // Google recaptcha (https://launchpad.net/bugs/1599146)
359- ["^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"],
360+ ["^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"],
361 ];
362
363=== modified file 'src/app/AlertDialog.qml'
364--- src/app/AlertDialog.qml 2016-05-23 13:25:46 +0000
365+++ src/app/AlertDialog.qml 2016-10-23 20:25:22 +0000
366@@ -20,11 +20,13 @@
367 import Ubuntu.Components 1.3
368
369 ModalDialog {
370+ objectName: "alertDialog"
371 title: i18n.tr("JavaScript Alert")
372
373 Button {
374 text: i18n.tr("OK")
375 color: theme.palette.normal.positive
376+ objectName: "okButton"
377 onClicked: model.accept()
378 }
379 }
380
381=== modified file 'src/app/BeforeUnloadDialog.qml'
382--- src/app/BeforeUnloadDialog.qml 2016-05-23 13:25:46 +0000
383+++ src/app/BeforeUnloadDialog.qml 2016-10-23 20:25:22 +0000
384@@ -20,15 +20,18 @@
385 import Ubuntu.Components 1.3
386
387 ModalDialog {
388+ objectName: "beforeUnloadDialog"
389 title: i18n.tr("Confirm Navigation")
390
391 Button {
392 text: i18n.tr("Leave")
393 color: theme.palette.normal.negative
394+ objectName: "leaveButton"
395 onClicked: model.accept()
396 }
397
398 Button {
399+ objectName: "stayButton"
400 text: i18n.tr("Stay")
401 onClicked: model.reject()
402 }
403
404=== modified file 'src/app/ChromeBase.qml'
405--- src/app/ChromeBase.qml 2015-12-02 18:01:18 +0000
406+++ src/app/ChromeBase.qml 2016-10-23 20:25:22 +0000
407@@ -1,5 +1,5 @@
408 /*
409- * Copyright 2014-2015 Canonical Ltd.
410+ * Copyright 2014-2016 Canonical Ltd.
411 *
412 * This file is part of webbrowser-app.
413 *
414@@ -25,9 +25,11 @@
415
416 objectName: "chromeBase"
417
418- property var webview
419 property alias backgroundColor: backgroundRect.color
420
421+ property alias loading: progressBar.visible
422+ property alias loadProgress: progressBar.value
423+
424 states: [
425 State {
426 name: "shown"
427@@ -53,7 +55,7 @@
428 }
429
430 ThinProgressBar {
431- webview: chrome.webview
432+ id: progressBar
433
434 anchors {
435 left: parent.left
436
437=== modified file 'src/app/ConfirmDialog.qml'
438--- src/app/ConfirmDialog.qml 2016-05-23 13:25:46 +0000
439+++ src/app/ConfirmDialog.qml 2016-10-23 20:25:22 +0000
440@@ -20,15 +20,18 @@
441 import Ubuntu.Components 1.3
442
443 ModalDialog {
444+ objectName: "confirmDialog"
445 title: i18n.tr("JavaScript Confirmation")
446
447 Button {
448 text: i18n.tr("OK")
449 color: theme.palette.normal.positive
450+ objectName: "okButton"
451 onClicked: model.accept()
452 }
453
454 Button {
455+ objectName: "cancelButton"
456 text: i18n.tr("Cancel")
457 onClicked: model.reject()
458 }
459
460=== modified file 'src/app/Downloader.qml'
461--- src/app/Downloader.qml 2016-01-12 14:50:27 +0000
462+++ src/app/Downloader.qml 2016-10-23 20:25:22 +0000
463@@ -34,9 +34,7 @@
464
465 Component {
466 id: metadataComponent
467- Metadata {
468- showInIndicator: true
469- }
470+ Metadata {}
471 }
472
473 Component {
474@@ -62,14 +60,8 @@
475 singleDownload.download(url)
476 }
477
478- function downloadPicture(url, headers) {
479- var metadata = metadataComponent.createObject(downloadItem)
480- downloadItem.mimeType = "image/*"
481- download(url, ContentType.Pictures, headers, metadata)
482- }
483-
484- function downloadMimeType(url, mimeType, headers, filename) {
485- var metadata = metadataComponent.createObject(downloadItem)
486+ function downloadMimeType(url, mimeType, headers, filename, incognito) {
487+ var metadata = metadataComponent.createObject(downloadItem, {"showInIndicator": !incognito})
488 var contentType = MimeTypeMapper.mimeTypeToContentType(mimeType)
489 if (contentType == ContentType.Unknown && filename) {
490 // If we can't determine the content type from the mime-type
491
492=== modified file 'src/app/PromptDialog.qml'
493--- src/app/PromptDialog.qml 2016-05-23 13:25:46 +0000
494+++ src/app/PromptDialog.qml 2016-10-23 20:25:22 +0000
495@@ -20,10 +20,12 @@
496 import Ubuntu.Components 1.3
497
498 ModalDialog {
499+ objectName: "promptDialog"
500 title: i18n.tr("JavaScript Prompt")
501
502 TextField {
503 id: input
504+ objectName: "inputTextField"
505 text: model.defaultValue
506 onAccepted: model.accept(input.text)
507 }
508@@ -31,10 +33,12 @@
509 Button {
510 text: i18n.tr("OK")
511 color: theme.palette.normal.positive
512+ objectName: "okButton"
513 onClicked: model.accept(input.text)
514 }
515
516 Button {
517+ objectName: "cancelButton"
518 text: i18n.tr("Cancel")
519 onClicked: model.reject()
520 }
521
522=== modified file 'src/app/ThinProgressBar.qml'
523--- src/app/ThinProgressBar.qml 2015-08-10 15:22:00 +0000
524+++ src/app/ThinProgressBar.qml 2016-10-23 20:25:22 +0000
525@@ -1,5 +1,5 @@
526 /*
527- * Copyright 2014-2015 Canonical Ltd.
528+ * Copyright 2014-2016 Canonical Ltd.
529 *
530 * This file is part of webbrowser-app.
531 *
532@@ -20,11 +20,8 @@
533 import Ubuntu.Components 1.3
534
535 ProgressBar {
536- property var webview
537-
538 height: units.dp(3)
539-
540 showProgressPercentage: false
541- value: webview ? webview.loadProgress / 100 : 0.0
542- visible: webview ? webview.loading : false
543+ minimumValue: 0
544+ maximumValue: 100
545 }
546
547=== modified file 'src/app/WebViewImpl.qml'
548--- src/app/WebViewImpl.qml 2015-12-15 12:37:34 +0000
549+++ src/app/WebViewImpl.qml 2016-10-23 20:25:22 +0000
550@@ -1,5 +1,5 @@
551 /*
552- * Copyright 2013-2015 Canonical Ltd.
553+ * Copyright 2013-2016 Canonical Ltd.
554 *
555 * This file is part of webbrowser-app.
556 *
557@@ -76,7 +76,7 @@
558 mimeType = MimeDatabase.filenameToMimeType(filename)
559 }
560 }
561- downloadLoader.item.downloadMimeType(request.url, mimeType, headers, request.suggestedFilename)
562+ downloadLoader.item.downloadMimeType(request.url, mimeType, headers, request.suggestedFilename, incognito)
563 } else {
564 // Desktop form factor case
565 Qt.openUrlExternally(request.url)
566
567=== modified file 'src/app/config.h.in'
568--- src/app/config.h.in 2016-05-26 17:00:55 +0000
569+++ src/app/config.h.in 2016-10-23 20:25:22 +0000
570@@ -22,19 +22,20 @@
571 #include <QtCore/QCoreApplication>
572 #include <QtCore/QDir>
573 #include <QtCore/QString>
574+#include <QtCore/QtGlobal>
575
576 #define REMOTE_INSPECTOR_PORT 9221
577
578 inline bool isRunningInstalled()
579 {
580- static bool installed = (QCoreApplication::applicationDirPath() == QDir("@CMAKE_INSTALL_FULL_BINDIR@").canonicalPath());
581+ static bool installed = (QCoreApplication::applicationDirPath() == QDir(qgetenv("SNAP").append("@CMAKE_INSTALL_FULL_BINDIR@")).canonicalPath());
582 return installed;
583 }
584
585 inline QString UbuntuBrowserDirectory()
586 {
587 if (isRunningInstalled()) {
588- return QStringLiteral("@CMAKE_INSTALL_FULL_DATADIR@/webbrowser-app");
589+ return qgetenv("SNAP").append("@CMAKE_INSTALL_FULL_DATADIR@/webbrowser-app");
590 } else {
591 return QStringLiteral("@CMAKE_SOURCE_DIR@/src/app");
592 }
593
594=== modified file 'src/app/webbrowser/BookmarkOptions.qml'
595--- src/app/webbrowser/BookmarkOptions.qml 2016-05-23 02:52:50 +0000
596+++ src/app/webbrowser/BookmarkOptions.qml 2016-10-23 20:25:22 +0000
597@@ -19,18 +19,36 @@
598 import QtQuick 2.4
599 import Ubuntu.Components 1.3
600 import Ubuntu.Components.Popups 1.3
601+import webbrowserapp.private 0.1
602
603 Popover {
604 id: bookmarkOptions
605
606 property url bookmarkUrl
607 property alias bookmarkTitle: titleTextField.text
608- property alias folderModel: folderOptionSelector.model
609
610- readonly property string bookmarkFolder: folderModel.get(folderOptionSelector.selectedIndex).folder
611+ readonly property string bookmarkFolder: folderOptionSelector.model.get(folderOptionSelector.selectedIndex).folder
612
613 contentHeight: bookmarkOptionsColumn.childrenRect.height + units.gu(2)
614
615+ onVisibleChanged: {
616+ if (!visible) {
617+ BookmarksModel.remove(bookmarkUrl)
618+ }
619+ }
620+
621+ Component.onDestruction: {
622+ if (BookmarksModel.contains(bookmarkUrl)) {
623+ BookmarksModel.update(bookmarkUrl, bookmarkTitle, bookmarkFolder)
624+ }
625+ }
626+
627+ // Fragile workaround for https://launchpad.net/bugs/1546677.
628+ // By destroying the popover, its visibility isn’t changed to
629+ // false, and thus the bookmark is not removed.
630+ Keys.onEnterPressed: bookmarkOptions.destroy()
631+ Keys.onReturnPressed: bookmarkOptions.destroy()
632+
633 Column {
634 id: bookmarkOptionsColumn
635
636@@ -77,6 +95,9 @@
637
638 delegate: OptionSelectorDelegate { text: folder === "" ? i18n.tr("All Bookmarks") : folder }
639 containerHeight: itemHeight * 3
640+ model: BookmarksFolderListModel {
641+ sourceModel: BookmarksModel
642+ }
643 }
644
645 Item {
646@@ -120,8 +141,8 @@
647
648 function createNewFolder(folder) {
649 Qt.inputMethod.hide()
650- folderModel.createNewFolder(folder)
651- folderOptionSelector.selectedIndex = folderModel.indexOf(folder)
652+ folderOptionSelector.model.createNewFolder(folder)
653+ folderOptionSelector.selectedIndex = folderOptionSelector.model.indexOf(folder)
654 folderOptionSelector.currentlyExpanded = false
655 PopupUtils.close(dialogue)
656 }
657
658=== modified file 'src/app/webbrowser/Browser.qml'
659--- src/app/webbrowser/Browser.qml 2016-09-28 08:24:06 +0000
660+++ src/app/webbrowser/Browser.qml 2016-10-23 20:25:22 +0000
661@@ -88,7 +88,7 @@
662 tabs will have their mediaAccessPermissionRequested signal handled by
663 creating one of these new dialogs.
664 */
665- onMediaAccessPermissionRequested: PopupUtils.open(mediaAccessDialogComponent, null, { request: request })
666+ onMediaAccessPermissionRequested: PopupUtils.open(Qt.resolvedUrl("../MediaAccessDialog.qml"), null, { request: request })
667 }
668
669 currentWebcontext: SharedWebContext.sharedContext
670@@ -119,11 +119,6 @@
671 id: keyboardModel
672 }
673
674- Component {
675- id: mediaAccessDialogComponent
676- MediaAccessDialog {}
677- }
678-
679 actions: [
680 Actions.GoTo {
681 onTriggered: currentWebview.url = value
682@@ -200,12 +195,16 @@
683 fill: tabContainer
684 topMargin: (chrome.state == "shown") ? chrome.height : 0
685 }
686- sourceComponent: ErrorSheet {
687- visible: currentWebview ? currentWebview.lastLoadFailed : false
688- url: currentWebview ? currentWebview.url : ""
689+ Component.onCompleted: setSource("../ErrorSheet.qml", {
690+ "visible": Qt.binding(function(){ return currentWebview ? currentWebview.lastLoadFailed : false }),
691+ "url": Qt.binding(function(){ return currentWebview ? currentWebview.url : "" })
692+ })
693+ Connections {
694+ target: errorSheetLoader.item
695 onRefreshClicked: currentWebview.reload()
696 }
697- focus: item.visible
698+
699+ focus: item && item.visible
700 asynchronous: true
701 }
702
703@@ -215,9 +214,12 @@
704 fill: tabContainer
705 topMargin: (chrome.state == "shown") ? chrome.height : 0
706 }
707- sourceComponent: InvalidCertificateErrorSheet {
708- visible: currentWebview && currentWebview.certificateError != null
709- certificateError: currentWebview ? currentWebview.certificateError : null
710+ Component.onCompleted: setSource("../InvalidCertificateErrorSheet.qml", {
711+ "visible": Qt.binding(function(){ return currentWebview && currentWebview.certificateError != null }),
712+ "certificateError": Qt.binding(function(){ return currentWebview ? currentWebview.certificateError : null })
713+ })
714+ Connections {
715+ target: invalidCertificateErrorSheetLoader.item
716 onAllowed: {
717 // Automatically allow future requests involving this
718 // certificate for the duration of the session.
719@@ -228,7 +230,7 @@
720 currentWebview.resetCertificateError()
721 }
722 }
723- focus: item.visible
724+ focus: item && item.visible
725 asynchronous: true
726 }
727
728@@ -262,60 +264,35 @@
729 newTabViewLoader.active = !tab.url.toString() && !tab.restoreState
730 }
731 }
732- }
733-
734- sourceComponent: browser.incognito ? newPrivateTabView :
735- (browser.wide ? newTabViewWide : newTabView)
736-
737- Component {
738- id: newTabView
739-
740- NewTabView {
741- anchors.fill: parent
742- settingsObject: settings
743- focus: true
744- onBookmarkClicked: {
745- chrome.requestedUrl = url
746- currentWebview.url = url
747- tabContainer.forceActiveFocus()
748- }
749- onBookmarkRemoved: BookmarksModel.remove(url)
750- onHistoryEntryClicked: {
751- chrome.requestedUrl = url
752- currentWebview.url = url
753- tabContainer.forceActiveFocus()
754- }
755- Keys.onUpPressed: chrome.focus = true
756- }
757- }
758-
759- Component {
760- id: newTabViewWide
761-
762- NewTabViewWide {
763- anchors.fill: parent
764- settingsObject: settings
765- focus: true
766- onBookmarkClicked: {
767- chrome.requestedUrl = url
768- currentWebview.url = url
769- tabContainer.forceActiveFocus()
770- }
771- onBookmarkRemoved: BookmarksModel.remove(url)
772- onHistoryEntryClicked: {
773- chrome.requestedUrl = url
774- currentWebview.url = url
775- tabContainer.forceActiveFocus()
776- }
777- Keys.onUpPressed: chrome.focus = true
778- }
779- }
780-
781- Component {
782- id: newPrivateTabView
783-
784- NewPrivateTabView { anchors.fill: parent }
785- }
786+ onWideChanged: newTabViewLoader.selectTabView()
787+ }
788+ Component.onCompleted: newTabViewLoader.selectTabView()
789+
790+ function selectTabView() {
791+ var source = browser.incognito ? "NewPrivateTabView.qml" :
792+ (browser.wide ? "NewTabViewWide.qml" :
793+ "NewTabView.qml");
794+ var properties = browser.incognito ? {} : {"settingsObject": settings,
795+ "focus": true};
796+
797+ newTabViewLoader.setSource(source, properties);
798+ }
799+
800+ Connections {
801+ target: newTabViewLoader.item && !browser.incognito ? newTabViewLoader.item : null
802+ onBookmarkClicked: {
803+ chrome.requestedUrl = url
804+ currentWebview.url = url
805+ tabContainer.forceActiveFocus()
806+ }
807+ onBookmarkRemoved: BookmarksModel.remove(url)
808+ onHistoryEntryClicked: {
809+ chrome.requestedUrl = url
810+ currentWebview.url = url
811+ tabContainer.forceActiveFocus()
812+ }
813+ }
814+ Keys.onUpPressed: chrome.focus = true
815 }
816
817 Loader {
818@@ -328,8 +305,11 @@
819 active: webProcessMonitor.crashed || (webProcessMonitor.killed && !currentWebview.loading)
820 focus: active
821
822- sourceComponent: SadTab {
823- webview: currentWebview
824+ Component.onCompleted: setSource("SadTab.qml", {
825+ "webview": Qt.binding(function () {return browser.currentWebview})
826+ })
827+ Connections {
828+ target: sadTabLoader.item
829 onCloseTabRequested: internal.closeCurrentTab()
830 }
831
832@@ -477,7 +457,6 @@
833 id: chrome
834
835 tab: internal.nextTab || tabsModel.currentTab
836- webview: tab ? tab.webview : null
837 tabsModel: browser.tabsModel
838 searchUrl: currentSearchEngine.urlTemplate
839
840@@ -800,7 +779,19 @@
841
842 anchors.fill: parent
843 active: false
844- sourceComponent: browser.wide ? bookmarksViewWideComponent : bookmarksViewComponent
845+ asynchronous: true
846+ Connections {
847+ target: browser
848+ onWideChanged: bookmarksViewLoader.selectBookmarksView()
849+ }
850+ Component.onCompleted: bookmarksViewLoader.selectBookmarksView()
851+
852+ function selectBookmarksView() {
853+ bookmarksViewLoader.setSource(browser.wide ? "BookmarksViewWide.qml" : "BookmarksView.qml",
854+ {"focus": true,
855+ "homepageUrl": Qt.binding(function () {return settings.homepage})
856+ });
857+ }
858
859 onStatusChanged: {
860 if (status == Loader.Ready) {
861@@ -824,26 +815,6 @@
862 bookmarksViewLoader.active = false
863 }
864 }
865-
866- Component {
867- id: bookmarksViewComponent
868-
869- BookmarksView {
870- anchors.fill: parent
871- focus: true
872- homepageUrl: settings.homepage
873- }
874- }
875-
876- Component {
877- id: bookmarksViewWideComponent
878-
879- BookmarksViewWide {
880- anchors.fill: parent
881- focus: true
882- homepageUrl: settings.homepage
883- }
884- }
885 }
886
887 Loader {
888@@ -851,7 +822,34 @@
889
890 anchors.fill: parent
891 active: false
892- sourceComponent: browser.wide ? historyViewWideComponent : historyViewComponent
893+ asynchronous: true
894+ Connections {
895+ target: browser
896+ onWideChanged: historyViewLoader.selectHistoryView()
897+ }
898+ Component.onCompleted: historyViewLoader.selectHistoryView()
899+
900+ function selectHistoryView() {
901+ historyViewLoader.setSource(browser.wide ? "HistoryViewWide.qml" : "HistoryViewWithExpansion.qml",
902+ {"focus": true});
903+ }
904+
905+ Connections {
906+ target: historyViewLoader.item
907+ onHistoryEntryClicked: {
908+ historyViewLoader.active = false
909+ internal.openUrlInNewTab(url, true)
910+ }
911+ onNewTabRequested: {
912+ historyViewLoader.active = false
913+ internal.openUrlInNewTab("", true)
914+ }
915+ onDone: {
916+ historyViewLoader.active = false
917+ internal.resetFocus()
918+ }
919+ onBack: historyViewLoader.active = false
920+ }
921
922 onStatusChanged: {
923 if (status == Loader.Ready) {
924@@ -862,75 +860,6 @@
925 internal.resetFocus()
926 }
927 }
928-
929- Component {
930- id: historyViewComponent
931-
932- FocusScope {
933- focus: true
934-
935- signal loadModel()
936- onLoadModel: children[0].loadModel()
937-
938- HistoryView {
939- anchors.fill: parent
940- focus: !expandedHistoryViewLoader.focus
941- visible: focus
942- onSeeMoreEntriesClicked: {
943- expandedHistoryViewLoader.model = model
944- expandedHistoryViewLoader.active = true
945- }
946- onNewTabRequested: internal.openUrlInNewTab("", true)
947- onBack: historyViewLoader.active = false
948- }
949-
950- Loader {
951- id: expandedHistoryViewLoader
952- asynchronous: true
953- anchors.fill: parent
954- active: false
955- focus: active
956- property var model: null
957- sourceComponent: ExpandedHistoryView {
958- focus: true
959- model: expandedHistoryViewLoader.model
960- onHistoryEntryClicked: {
961- internal.openUrlInNewTab(url, true)
962- historyViewLoader.active = false
963- }
964- onHistoryEntryRemoved: {
965- if (count == 1) {
966- done()
967- }
968- HistoryModel.removeEntryByUrl(url)
969- }
970- onDone: expandedHistoryViewLoader.active = false
971- }
972- }
973- }
974- }
975-
976- Component {
977- id: historyViewWideComponent
978-
979- HistoryViewWide {
980- anchors.fill: parent
981- focus: true
982-
983- onHistoryEntryClicked: {
984- historyViewLoader.active = false
985- internal.openUrlInNewTab(url, true)
986- }
987- onNewTabRequested: {
988- historyViewLoader.active = false
989- internal.openUrlInNewTab("", true)
990- }
991- onDone: {
992- historyViewLoader.active = false
993- internal.resetFocus()
994- }
995- }
996- }
997 }
998
999 Loader {
1000@@ -938,6 +867,7 @@
1001
1002 anchors.fill: parent
1003 active: false
1004+ asynchronous: true
1005
1006 onStatusChanged: {
1007 if (status == Loader.Ready) {
1008@@ -948,10 +878,12 @@
1009 }
1010 }
1011
1012- sourceComponent: SettingsPage {
1013- anchors.fill: parent
1014- focus: true
1015- settingsObject: settings
1016+ Component.onCompleted: setSource("SettingsPage.qml", {
1017+ "focus": true,
1018+ "settingsObject": settings
1019+ })
1020+ Connections {
1021+ target: settingsViewLoader.item
1022 onDone: settingsViewLoader.active = false
1023 }
1024 }
1025@@ -961,18 +893,15 @@
1026
1027 anchors.fill: parent
1028 active: false
1029- source: "DownloadsPage.qml"
1030+ asynchronous: true
1031+ Component.onCompleted: {
1032+ setSource("DownloadsPage.qml", {
1033+ "downloadManager": Qt.binding(function () {return downloadHandlerLoader.item}),
1034+ "incognito": incognito,
1035+ "focus": true
1036+ })
1037+ }
1038
1039- Binding {
1040- target: downloadsViewLoader.item
1041- property: "downloadManager"
1042- value: downloadHandlerLoader.item
1043- }
1044- Binding {
1045- target: downloadsViewLoader.item
1046- property: "focus"
1047- value: true
1048- }
1049 Connections {
1050 target: downloadsViewLoader.item
1051 onDone: downloadsViewLoader.active = false
1052@@ -993,424 +922,10 @@
1053 asynchronous: true
1054 }
1055
1056- Component {
1057- id: tabComponent
1058-
1059- BrowserTab {
1060- anchors.fill: parent
1061- incognito: browser.incognito
1062- current: tabsModel && tabsModel.currentTab === this
1063- focus: current
1064-
1065- Item {
1066- id: contextualMenuTarget
1067- visible: false
1068- }
1069-
1070- webviewComponent: WebViewImpl {
1071- id: webviewimpl
1072-
1073- property BrowserTab tab
1074- readonly property bool current: tab.current
1075-
1076- currentWebview: browser.currentWebview
1077- filePicker: filePickerLoader.item
1078-
1079- anchors.fill: parent
1080-
1081- focus: true
1082-
1083- enabled: current && !bottomEdgeHandle.dragging && !recentView.visible
1084-
1085- locationBarController {
1086- height: chrome.height
1087- mode: chromeController.defaultMode
1088- }
1089-
1090- //experimental.preferences.developerExtrasEnabled: developerExtrasEnabled
1091- preferences.localStorageEnabled: true
1092- preferences.appCacheEnabled: true
1093-
1094- property QtObject contextModel: null
1095- contextualActions: ActionList {
1096- Actions.OpenLinkInNewTab {
1097- objectName: "OpenLinkInNewTabContextualAction"
1098- enabled: contextModel && contextModel.linkUrl.toString()
1099- onTriggered: internal.openUrlInNewTab(contextModel.linkUrl, true)
1100- }
1101- Actions.OpenLinkInNewBackgroundTab {
1102- objectName: "OpenLinkInNewBackgroundTabContextualAction"
1103- enabled: contextModel && contextModel.linkUrl.toString()
1104- onTriggered: internal.openUrlInNewTab(contextModel.linkUrl, false)
1105- }
1106- Actions.OpenLinkInNewWindow {
1107- objectName: "OpenLinkInNewWindowContextualAction"
1108- enabled: contextModel && contextModel.linkUrl.toString()
1109- onTriggered: browser.openLinkInWindowRequested(contextModel.linkUrl, false)
1110- }
1111- Actions.OpenLinkInPrivateWindow {
1112- objectName: "OpenLinkInPrivateWindowContextualAction"
1113- enabled: contextModel && contextModel.linkUrl.toString()
1114- onTriggered: browser.openLinkInWindowRequested(contextModel.linkUrl, true)
1115- }
1116- Actions.BookmarkLink {
1117- objectName: "BookmarkLinkContextualAction"
1118- enabled: contextModel && contextModel.linkUrl.toString()
1119- && !BookmarksModel.contains(contextModel.linkUrl)
1120- onTriggered: {
1121- // position the menu target with a one-off assignement instead of a binding
1122- // since the contents of the contextModel have meaning only while the context
1123- // menu is active
1124- contextualMenuTarget.x = contextModel.position.x
1125- contextualMenuTarget.y = contextModel.position.y + locationBarController.height + locationBarController.offset
1126- internal.addBookmark(contextModel.linkUrl, contextModel.linkText,
1127- "", contextualMenuTarget)
1128- }
1129- }
1130- Actions.CopyLink {
1131- objectName: "CopyLinkContextualAction"
1132- enabled: contextModel && contextModel.linkUrl.toString()
1133- onTriggered: Clipboard.push(["text/plain", contextModel.linkUrl.toString()])
1134- }
1135- Actions.SaveLink {
1136- objectName: "SaveLinkContextualAction"
1137- enabled: contextModel && contextModel.linkUrl.toString()
1138- onTriggered: contextModel.saveLink()
1139- }
1140- Actions.Share {
1141- objectName: "ShareContextualAction"
1142- enabled: (contentHandlerLoader.status == Loader.Ready) && contextModel &&
1143- (contextModel.linkUrl.toString() || contextModel.selectionText)
1144- onTriggered: {
1145- if (contextModel.linkUrl.toString()) {
1146- internal.shareLink(contextModel.linkUrl.toString(), contextModel.linkText)
1147- } else if (contextModel.selectionText) {
1148- internal.shareText(contextModel.selectionText)
1149- }
1150- }
1151- }
1152- Actions.OpenImageInNewTab {
1153- objectName: "OpenImageInNewTabContextualAction"
1154- enabled: contextModel &&
1155- (contextModel.mediaType === Oxide.WebView.MediaTypeImage) &&
1156- contextModel.srcUrl.toString()
1157- onTriggered: internal.openUrlInNewTab(contextModel.srcUrl, true)
1158- }
1159- Actions.CopyImage {
1160- objectName: "CopyImageContextualAction"
1161- enabled: contextModel &&
1162- (contextModel.mediaType === Oxide.WebView.MediaTypeImage) &&
1163- contextModel.srcUrl.toString()
1164- onTriggered: Clipboard.push(["text/plain", contextModel.srcUrl.toString()])
1165- }
1166- Actions.SaveImage {
1167- objectName: "SaveImageContextualAction"
1168- enabled: contextModel &&
1169- ((contextModel.mediaType === Oxide.WebView.MediaTypeImage) ||
1170- (contextModel.mediaType === Oxide.WebView.MediaTypeCanvas)) &&
1171- contextModel.hasImageContents
1172- onTriggered: contextModel.saveMedia()
1173- }
1174- Actions.OpenVideoInNewTab {
1175- objectName: "OpenVideoInNewTabContextualAction"
1176- enabled: contextModel &&
1177- (contextModel.mediaType === Oxide.WebView.MediaTypeVideo) &&
1178- contextModel.srcUrl.toString()
1179- onTriggered: internal.openUrlInNewTab(contextModel.srcUrl, true)
1180- }
1181- Actions.SaveVideo {
1182- objectName: "SaveVideoContextualAction"
1183- enabled: contextModel &&
1184- (contextModel.mediaType === Oxide.WebView.MediaTypeVideo) &&
1185- contextModel.srcUrl.toString()
1186- onTriggered: contextModel.saveMedia()
1187- }
1188- Actions.Undo {
1189- objectName: "UndoContextualAction"
1190- enabled: contextModel && contextModel.isEditable &&
1191- (contextModel.editFlags & Oxide.WebView.UndoCapability)
1192- onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandUndo)
1193- }
1194- Actions.Redo {
1195- objectName: "RedoContextualAction"
1196- enabled: contextModel && contextModel.isEditable &&
1197- (contextModel.editFlags & Oxide.WebView.RedoCapability)
1198- onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandRedo)
1199- }
1200- Actions.Cut {
1201- objectName: "CutContextualAction"
1202- enabled: contextModel && contextModel.isEditable &&
1203- (contextModel.editFlags & Oxide.WebView.CutCapability)
1204- onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandCut)
1205- }
1206- Actions.Copy {
1207- objectName: "CopyContextualAction"
1208- enabled: contextModel && (contextModel.selectionText ||
1209- (contextModel.isEditable &&
1210- (contextModel.editFlags & Oxide.WebView.CopyCapability)))
1211- onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandCopy)
1212- }
1213- Actions.Paste {
1214- objectName: "PasteContextualAction"
1215- enabled: contextModel && contextModel.isEditable &&
1216- (contextModel.editFlags & Oxide.WebView.PasteCapability)
1217- onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandPaste)
1218- }
1219- Actions.Erase {
1220- objectName: "EraseContextualAction"
1221- enabled: contextModel && contextModel.isEditable &&
1222- (contextModel.editFlags & Oxide.WebView.EraseCapability)
1223- onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandErase)
1224- }
1225- Actions.SelectAll {
1226- objectName: "SelectAllContextualAction"
1227- enabled: contextModel && contextModel.isEditable &&
1228- (contextModel.editFlags & Oxide.WebView.SelectAllCapability)
1229- onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandSelectAll)
1230- }
1231- }
1232-
1233- function contextMenuOnCompleted(menu) {
1234- contextModel = menu.contextModel
1235- if (contextModel.linkUrl.toString() ||
1236- contextModel.srcUrl.toString() ||
1237- contextModel.selectionText ||
1238- (contextModel.isEditable && contextModel.editFlags) ||
1239- (((contextModel.mediaType == Oxide.WebView.MediaTypeImage) ||
1240- (contextModel.mediaType == Oxide.WebView.MediaTypeCanvas)) &&
1241- contextModel.hasImageContents)) {
1242- menu.show()
1243- } else {
1244- contextModel.close()
1245- }
1246- }
1247-
1248- Component {
1249- id: contextMenuNarrowComponent
1250- ContextMenuMobile {
1251- actions: contextualActions
1252- Component.onCompleted: webviewimpl.contextMenuOnCompleted(this)
1253- }
1254- }
1255- Component {
1256- id: contextMenuWideComponent
1257- ContextMenuWide {
1258- webview: webviewimpl
1259- parent: browser
1260- actions: contextualActions
1261- Component.onCompleted: webviewimpl.contextMenuOnCompleted(this)
1262- }
1263- }
1264- contextMenu: browser.wide ? contextMenuWideComponent : contextMenuNarrowComponent
1265-
1266- onNewViewRequested: {
1267- var tab = tabComponent.createObject(tabContainer, {"request": request})
1268- var setCurrent = (request.disposition == Oxide.NewViewRequest.DispositionNewForegroundTab)
1269- internal.addTab(tab, setCurrent)
1270- if (setCurrent) tabContainer.forceActiveFocus()
1271- }
1272-
1273- onCloseRequested: prepareToClose()
1274- onPrepareToCloseResponse: {
1275- if (proceed) {
1276- if (tab) {
1277- for (var i = 0; i < tabsModel.count; ++i) {
1278- if (tabsModel.get(i) === tab) {
1279- tabsModel.remove(i)
1280- break
1281- }
1282- }
1283- tab.close()
1284- }
1285- if (tabsModel.count === 0) {
1286- internal.openUrlInNewTab("", true, true)
1287- }
1288- }
1289- }
1290-
1291- QtObject {
1292- id: webviewInternal
1293- property url storedUrl: ""
1294- property bool titleSet: false
1295- property string title: ""
1296- }
1297- onLoadEvent: {
1298- if (event.type == Oxide.LoadEvent.TypeCommitted) {
1299- chrome.findInPageMode = false
1300- webviewInternal.titleSet = false
1301- webviewInternal.title = title
1302- }
1303-
1304- if (webviewimpl.incognito) {
1305- return
1306- }
1307-
1308- if ((event.type == Oxide.LoadEvent.TypeCommitted) &&
1309- !event.isError &&
1310- (300 > event.httpStatusCode) && (event.httpStatusCode >= 200)) {
1311- webviewInternal.storedUrl = event.url
1312- HistoryModel.add(event.url, title, icon)
1313- }
1314- }
1315- onTitleChanged: {
1316- if (!webviewInternal.titleSet && webviewInternal.storedUrl.toString()) {
1317- // Record the title to avoid updating the history database
1318- // every time the page dynamically updates its title.
1319- // We don’t want pages that update their title every second
1320- // to achieve an ugly "scrolling title" effect to flood the
1321- // history database with updates.
1322- webviewInternal.titleSet = true
1323- webviewInternal.title = title
1324- HistoryModel.update(webviewInternal.storedUrl, title, icon)
1325- }
1326- }
1327- onIconChanged: {
1328- if (webviewInternal.storedUrl.toString()) {
1329- HistoryModel.update(webviewInternal.storedUrl, webviewInternal.title, icon)
1330- }
1331- }
1332-
1333- onGeolocationPermissionRequested: requestGeolocationPermission(request)
1334-
1335- property var certificateError
1336- function resetCertificateError() {
1337- certificateError = null
1338- }
1339- onCertificateError: {
1340- if (!error.isMainFrame || error.isSubresource) {
1341- // Not a main frame document error, just block the content
1342- // (it’s not overridable anyway).
1343- return
1344- }
1345- if (internal.isCertificateErrorAllowed(error)) {
1346- error.allow()
1347- } else {
1348- certificateError = error
1349- error.onCancelled.connect(webviewimpl.resetCertificateError)
1350- }
1351- }
1352-
1353- onFullscreenChanged: {
1354- if (fullscreen) {
1355- fullscreenExitHintComponent.createObject(webviewimpl)
1356- }
1357- }
1358- Component {
1359- id: fullscreenExitHintComponent
1360-
1361- Rectangle {
1362- id: fullscreenExitHint
1363- objectName: "fullscreenExitHint"
1364-
1365- anchors.centerIn: parent
1366- height: units.gu(6)
1367- width: Math.min(units.gu(50), parent.width - units.gu(12))
1368- radius: units.gu(1)
1369- color: "#3e3b39"
1370- opacity: 0.85
1371-
1372- Behavior on opacity {
1373- UbuntuNumberAnimation {
1374- duration: UbuntuAnimation.SlowDuration
1375- }
1376- }
1377- onOpacityChanged: {
1378- if (opacity == 0.0) {
1379- fullscreenExitHint.destroy()
1380- }
1381- }
1382-
1383- // Delay showing the hint to prevent it from jumping up while the
1384- // webview is being resized (https://launchpad.net/bugs/1454097).
1385- visible: false
1386- Timer {
1387- running: true
1388- interval: 250
1389- onTriggered: fullscreenExitHint.visible = true
1390- }
1391-
1392- Label {
1393- color: "white"
1394- font.weight: Font.Light
1395- anchors.centerIn: parent
1396- text: bottomEdgeHandle.enabled
1397- ? i18n.tr("Swipe Up To Exit Full Screen")
1398- : i18n.tr("Press ESC To Exit Full Screen")
1399- }
1400-
1401- Timer {
1402- running: fullscreenExitHint.visible
1403- interval: 2000
1404- onTriggered: fullscreenExitHint.opacity = 0
1405- }
1406-
1407- Connections {
1408- target: webviewimpl
1409- onFullscreenChanged: {
1410- if (!webviewimpl.fullscreen) {
1411- fullscreenExitHint.destroy()
1412- }
1413- }
1414- }
1415-
1416- Component.onCompleted: bottomEdgeHint.forceShow = true
1417- Component.onDestruction: bottomEdgeHint.forceShow = false
1418- }
1419- }
1420-
1421- onShowDownloadDialog: {
1422- if (downloadDialogLoader.status === Loader.Ready) {
1423- var downloadDialog = PopupUtils.open(downloadDialogLoader.item, browser, {"contentType" : contentType,
1424- "downloadId" : downloadId,
1425- "singleDownload" : downloader,
1426- "filename" : filename,
1427- "mimeType" : mimeType})
1428- downloadDialog.startDownload.connect(startDownload)
1429- }
1430- }
1431-
1432- function showDownloadsPage() {
1433- downloadsViewLoader.active = true
1434- return downloadsViewLoader.item
1435- }
1436-
1437- function startDownload(downloadId, download, mimeType) {
1438- DownloadsModel.add(downloadId, download.url, mimeType)
1439- download.start()
1440- downloadsViewLoader.active = true
1441- }
1442-
1443- }
1444- }
1445- }
1446-
1447- Component {
1448- id: bookmarkOptionsComponent
1449- BookmarkOptions {
1450- folderModel: BookmarksFolderListModel {
1451- sourceModel: BookmarksModel
1452- }
1453-
1454- Component.onCompleted: forceActiveFocus()
1455-
1456- onVisibleChanged: {
1457- if (!visible) {
1458- BookmarksModel.remove(bookmarkUrl)
1459- }
1460- }
1461-
1462- Component.onDestruction: {
1463- if (BookmarksModel.contains(bookmarkUrl)) {
1464- BookmarksModel.update(bookmarkUrl, bookmarkTitle, bookmarkFolder)
1465- }
1466- }
1467-
1468- // Fragile workaround for https://launchpad.net/bugs/1546677.
1469- // By destroying the popover, its visibility isn’t changed to
1470- // false, and thus the bookmark is not removed.
1471- Keys.onEnterPressed: destroy()
1472- Keys.onReturnPressed: destroy()
1473- }
1474+ property Component tabComponent
1475+ Loader {
1476+ source: "TabComponent.qml"
1477+ onLoaded: tabComponent = item
1478 }
1479
1480 QtObject {
1481@@ -1513,6 +1028,7 @@
1482 var tabInfo = closedTabHistory.pop()
1483 var tab = restoreTabState(tabInfo.state)
1484 addTab(tab, true, tabInfo.index)
1485+ tab.load()
1486 }
1487 }
1488
1489@@ -1621,7 +1137,7 @@
1490 BookmarksModel.add(url, title, icon, "")
1491 if (location === undefined) location = chrome.bookmarkTogglePlaceHolder
1492 var properties = {"bookmarkUrl": url, "bookmarkTitle": title}
1493- currentBookmarkOptionsDialog = PopupUtils.open(bookmarkOptionsComponent,
1494+ internal.currentBookmarkOptionsDialog = PopupUtils.open(Qt.resolvedUrl("BookmarkOptions.qml"),
1495 location, properties)
1496 }
1497 }
1498
1499=== modified file 'src/app/webbrowser/BrowserTab.qml'
1500--- src/app/webbrowser/BrowserTab.qml 2016-08-11 11:19:40 +0000
1501+++ src/app/webbrowser/BrowserTab.qml 2016-10-23 20:25:22 +0000
1502@@ -193,9 +193,10 @@
1503 }
1504 }
1505 Connections {
1506- target: webview
1507- onLoadingStateChanged: {
1508- if (!webview.loading && !webview.incognito) {
1509+ target: incognito ? null : webview
1510+ onLoadEvent: {
1511+ if ((event.type == Oxide.LoadEvent.TypeSucceeded) ||
1512+ (event.type == Oxide.LoadEvent.TypeFailed)) {
1513 delayedCapture.restart()
1514 }
1515 }
1516
1517=== modified file 'src/app/webbrowser/Chrome.qml'
1518--- src/app/webbrowser/Chrome.qml 2016-02-09 22:01:57 +0000
1519+++ src/app/webbrowser/Chrome.qml 2016-10-23 20:25:22 +0000
1520@@ -25,6 +25,7 @@
1521
1522 property var tabsModel
1523 property alias tab: navigationBar.tab
1524+ readonly property var webview: tab ? tab.webview : null
1525 property alias searchUrl: navigationBar.searchUrl
1526 property alias text: navigationBar.text
1527 property alias bookmarked: navigationBar.bookmarked
1528@@ -69,15 +70,23 @@
1529 Loader {
1530 id: tabsBar
1531
1532- sourceComponent: TabsBar {
1533- model: tabsModel
1534- incognito: chrome.incognito
1535- fgColor: navigationBar.fgColor
1536- touchEnabled: chrome.touchEnabled
1537+ Component.onCompleted: {
1538+ tabsBar.setSource("TabsBar.qml", {
1539+ "model": Qt.binding(function () {return chrome.tabsModel}),
1540+ "incognito": Qt.binding(function () {return chrome.incognito}),
1541+ "fgColor": Qt.binding(function () {return navigationBar.fgColor}),
1542+ "touchEnabled": Qt.binding(function () {return chrome.touchEnabled})
1543+ })
1544+ }
1545+
1546+ Connections {
1547+ target: tabsBar.item
1548+
1549 onSwitchToTab: chrome.switchToTab(index)
1550 onRequestNewTab: chrome.requestNewTab(index, makeCurrent)
1551 onTabClosed: chrome.tabClosed(index)
1552 }
1553+ asynchronous: true
1554
1555 anchors {
1556 top: parent.top
1557@@ -90,6 +99,7 @@
1558 NavigationBar {
1559 id: navigationBar
1560
1561+ loading: chrome.loading
1562 fgColor: "#111111"
1563 iconColor: (incognito && !showTabsBar) ? "white" : fgColor
1564
1565@@ -105,4 +115,19 @@
1566 onToggleBookmark: chrome.toggleBookmark()
1567 }
1568 }
1569+
1570+ // Delay changing the 'loading' state, to allow for very brief load
1571+ // sequences to not update the UI, which would result in inelegant
1572+ // flickering (https://launchpad.net/bugs/1611680).
1573+ Connections {
1574+ target: webview
1575+ onLoadingStateChanged: delayedLoadingNotifier.restart()
1576+ }
1577+ Timer {
1578+ id: delayedLoadingNotifier
1579+ interval: 100
1580+ onTriggered: loading = webview.loading
1581+ }
1582+
1583+ loadProgress: (loading && webview) ? webview.loadProgress : 0
1584 }
1585
1586=== modified file 'src/app/webbrowser/DownloadDelegate.qml'
1587--- src/app/webbrowser/DownloadDelegate.qml 2016-05-23 02:52:50 +0000
1588+++ src/app/webbrowser/DownloadDelegate.qml 2016-10-23 20:25:22 +0000
1589@@ -25,7 +25,7 @@
1590
1591 property var downloadManager
1592
1593- property alias icon: mimeicon.name
1594+ property string icon
1595 property alias image: thumbimage.source
1596 property alias title: title.text
1597 property alias url: url.text
1598@@ -33,15 +33,16 @@
1599 property bool incomplete: false
1600 property string downloadId
1601 property var download
1602- property int progress: download ? download.progress : 0
1603+ readonly property int progress: download ? download.progress : 0
1604 property bool paused
1605+ property alias incognito: incognitoIcon.visible
1606
1607 divider.visible: false
1608
1609 signal removed()
1610 signal cancelled()
1611
1612- height: visible ? (incomplete ? (paused ? units.gu(13) : units.gu(10)) : units.gu(7)) : 0
1613+ height: visible ? layout.height : 0
1614
1615 QtObject {
1616 id: internal
1617@@ -60,165 +61,167 @@
1618 Component.onCompleted: internal.connectToDownloadObject()
1619 onDownloadManagerChanged: internal.connectToDownloadObject()
1620
1621- Item {
1622-
1623- anchors {
1624- verticalCenter: parent.verticalCenter
1625- left: parent.left
1626- leftMargin: units.gu(2)
1627- right: parent.right
1628- rightMargin: units.gu(2)
1629- }
1630+ SlotsLayout {
1631+ id: layout
1632
1633 Item {
1634- id: iconContainer
1635+ SlotsLayout.position: SlotsLayout.Leading
1636 width: units.gu(3)
1637- height: width
1638- anchors.verticalCenter: parent.verticalCenter
1639- anchors.verticalCenterOffset: downloadDelegate.incomplete ? -units.gu(1) : 0
1640+ height: units.gu(3)
1641
1642 Image {
1643 id: thumbimage
1644 asynchronous: true
1645- width: parent.width
1646- height: parent.height
1647+ anchors.fill: parent
1648 fillMode: Image.PreserveAspectFit
1649- sourceSize.width: parent.width
1650- sourceSize.height: parent.height
1651- anchors.verticalCenter: parent.verticalCenter
1652+ sourceSize.width: width
1653+ sourceSize.height: height
1654 }
1655
1656 Image {
1657- id: mimeicon
1658 asynchronous: true
1659 anchors.fill: parent
1660 anchors.margins: units.gu(0.2)
1661- source: "image://theme/%1".arg(name != "" ? name : "save")
1662+ source: "image://theme/%1".arg(downloadDelegate.icon || "save")
1663 visible: thumbimage.status !== Image.Ready
1664 cache: true
1665- property string name
1666- }
1667- }
1668-
1669- Item {
1670- anchors.top: iconContainer.top
1671- anchors.left: iconContainer.right
1672- anchors.leftMargin: units.gu(2)
1673- anchors.right: parent.right
1674-
1675- Column {
1676- id: detailsColumn
1677- width: parent.width - cancelColumn.width
1678- height: parent.height
1679-
1680- Label {
1681- id: title
1682- fontSize: "x-small"
1683- color: "#5d5d5d"
1684- elide: Text.ElideRight
1685- width: parent.width
1686- }
1687-
1688- Label {
1689- id: url
1690- fontSize: "x-small"
1691- color: "#5d5d5d"
1692- elide: Text.ElideRight
1693- width: parent.width
1694- }
1695-
1696- Item {
1697- height: error.visible ? units.gu(1) : units.gu(2)
1698- width: parent.width
1699- visible: downloadDelegate.incomplete
1700- }
1701-
1702- Item {
1703- id: error
1704- visible: incomplete && download === undefined || errorMessage !== ""
1705- height: units.gu(3)
1706- width: parent.width
1707-
1708- Icon {
1709- id: errorIcon
1710- width: units.gu(2)
1711- height: width
1712- anchors.verticalCenter: parent.verticalCenter
1713- name: "dialog-warning-symbolic"
1714- color: theme.palette.normal.negative
1715- }
1716-
1717- Label {
1718- width: parent.width - errorIcon.width
1719- anchors.left: errorIcon.right
1720- anchors.leftMargin: units.gu(1)
1721- anchors.verticalCenter: errorIcon.verticalCenter
1722- fontSize: "x-small"
1723- color: theme.palette.normal.negative
1724- text: errorMessage !== "" ? errorMessage
1725- : (incomplete && download === undefined) ? i18n.tr("Download failed")
1726- : ""
1727- elide: Text.ElideRight
1728- }
1729- }
1730-
1731- IndeterminateProgressBar {
1732- id: progressBar
1733- width: parent.width
1734- height: units.gu(0.5)
1735- visible: downloadDelegate.incomplete && !error.visible
1736- progress: downloadDelegate.progress
1737- // Work around UDM bug #1450144
1738- indeterminateProgress: downloadDelegate.progress < 0 || downloadDelegate.progress > 100
1739- }
1740- }
1741-
1742- Column {
1743- id: cancelColumn
1744- spacing: units.gu(1)
1745- anchors.top: detailsColumn.top
1746- anchors.left: detailsColumn.right
1747- anchors.leftMargin: units.gu(2)
1748- width: downloadDelegate.incomplete && !error.visible ? cancelButton.width + units.gu(2) : 0
1749-
1750- Button {
1751- visible: downloadDelegate.incomplete && !error.visible
1752- id: cancelButton
1753- text: i18n.tr("Cancel")
1754- onClicked: {
1755- if (download) {
1756- download.cancel()
1757- cancelled()
1758- }
1759- }
1760- }
1761-
1762- Label {
1763- visible: !progressBar.indeterminateProgress && downloadDelegate.incomplete
1764- && !error.visible
1765- && !downloadDelegate.paused
1766- width: cancelButton.width
1767- horizontalAlignment: Text.AlignHCenter
1768- fontSize: "x-small"
1769- text: progressBar.progress + "%"
1770- }
1771-
1772- Button {
1773- visible: downloadDelegate.paused
1774- text: i18n.tr("Resume")
1775- width: cancelButton.width
1776- onClicked: {
1777- if (download) {
1778- download.resume()
1779- }
1780- }
1781- }
1782- }
1783-
1784- }
1785- }
1786-
1787- leadingActions: error.visible || !downloadDelegate.incomplete ? deleteActionList : null
1788+ }
1789+ }
1790+
1791+ mainSlot: Column {
1792+ Label {
1793+ id: title
1794+ fontSize: "x-small"
1795+ color: "#5d5d5d"
1796+ elide: Text.ElideRight
1797+ anchors {
1798+ left: parent.left
1799+ right: parent.right
1800+ }
1801+ }
1802+
1803+ Label {
1804+ id: url
1805+ fontSize: "x-small"
1806+ color: "#5d5d5d"
1807+ elide: Text.ElideRight
1808+ anchors {
1809+ left: parent.left
1810+ right: parent.right
1811+ }
1812+ }
1813+
1814+ Item {
1815+ height: error.visible ? units.gu(1) : units.gu(2)
1816+ anchors {
1817+ left: parent.left
1818+ right: parent.right
1819+ }
1820+ visible: incomplete
1821+ }
1822+
1823+ Item {
1824+ id: error
1825+ visible: (incomplete && (download === undefined)) || errorMessage
1826+ height: units.gu(3)
1827+ anchors {
1828+ left: parent.left
1829+ right: parent.right
1830+ }
1831+
1832+ Icon {
1833+ id: errorIcon
1834+ width: units.gu(2)
1835+ height: units.gu(2)
1836+ anchors.verticalCenter: parent.verticalCenter
1837+ name: "dialog-warning-symbolic"
1838+ color: theme.palette.normal.negative
1839+ }
1840+
1841+ Label {
1842+ anchors {
1843+ left: errorIcon.right
1844+ leftMargin: units.gu(1)
1845+ right: parent.right
1846+ verticalCenter: parent.verticalCenter
1847+ }
1848+ fontSize: "x-small"
1849+ color: theme.palette.normal.negative
1850+ text: errorMessage ||
1851+ ((incomplete && download === undefined) ? i18n.tr("Download failed") : "")
1852+ elide: Text.ElideRight
1853+ }
1854+ }
1855+
1856+ IndeterminateProgressBar {
1857+ id: progressBar
1858+ anchors {
1859+ left: parent.left
1860+ right: parent.right
1861+ }
1862+ height: units.gu(0.5)
1863+ visible: incomplete && !error.visible
1864+ progress: downloadDelegate.progress
1865+ // Work around UDM bug #1450144
1866+ indeterminateProgress: progress < 0 || progress > 100
1867+ }
1868+ }
1869+
1870+ Column {
1871+ SlotsLayout.position: SlotsLayout.Trailing
1872+ spacing: units.gu(1)
1873+ width: (incomplete && !error.visible) ? cancelButton.width : 0
1874+
1875+ Button {
1876+ id: cancelButton
1877+ visible: incomplete && !error.visible
1878+ text: i18n.tr("Cancel")
1879+ onClicked: {
1880+ if (download) {
1881+ download.cancel()
1882+ cancelled()
1883+ }
1884+ }
1885+ }
1886+
1887+ Label {
1888+ visible: !progressBar.indeterminateProgress && incomplete
1889+ && !error.visible && !paused
1890+ width: cancelButton.width
1891+ horizontalAlignment: Text.AlignHCenter
1892+ fontSize: "x-small"
1893+ // TRANSLATORS: %1 is the percentage of the download completed so far
1894+ text: i18n.tr("%1%").arg(progressBar.progress)
1895+ }
1896+
1897+ Button {
1898+ visible: paused
1899+ text: i18n.tr("Resume")
1900+ width: cancelButton.width
1901+ onClicked: {
1902+ if (download) {
1903+ download.resume()
1904+ }
1905+ }
1906+ }
1907+ }
1908+ }
1909+
1910+ Icon {
1911+ id: incognitoIcon
1912+ anchors {
1913+ right: parent.right
1914+ rightMargin: units.gu(2)
1915+ bottom: parent.bottom
1916+ bottomMargin: units.gu(1)
1917+ }
1918+ width: units.gu(2)
1919+ height: units.gu(2)
1920+ asynchronous: true
1921+ name: "private-browsing"
1922+ }
1923+
1924+ leadingActions: error.visible || !incomplete ? deleteActionList : null
1925
1926 ListItemActions {
1927 id: deleteActionList
1928@@ -226,9 +229,8 @@
1929 Action {
1930 objectName: "leadingAction.delete"
1931 iconName: "delete"
1932- enabled: error.visible || !downloadDelegate.incomplete
1933- onTriggered: error.visible ? downloadDelegate.cancelled()
1934- : downloadDelegate.removed()
1935+ enabled: error.visible || !incomplete
1936+ onTriggered: error.visible ? cancelled() : removed()
1937 }
1938 ]
1939 }
1940
1941=== modified file 'src/app/webbrowser/DownloadsPage.qml'
1942--- src/app/webbrowser/DownloadsPage.qml 2016-06-06 09:09:47 +0000
1943+++ src/app/webbrowser/DownloadsPage.qml 2016-10-23 20:25:22 +0000
1944@@ -39,6 +39,7 @@
1945 property bool pickingMode
1946 property bool multiSelect
1947 property alias mimetypeFilter: downloadModelFilter.pattern
1948+ property bool incognito: false
1949
1950 signal done()
1951
1952@@ -154,8 +155,14 @@
1953 focus: !exportPeerPicker.focus
1954
1955 model: SortFilterModel {
1956- model: DownloadsModel
1957- filter {
1958+ model: SortFilterModel {
1959+ model: DownloadsModel
1960+ filter {
1961+ property: "incognito"
1962+ pattern: RegExp(downloadsItem.incognito ? "" : "^false$")
1963+ }
1964+ }
1965+ filter {
1966 id: downloadModelFilter
1967 property: "mimetype"
1968 }
1969@@ -197,6 +204,7 @@
1970 visible: !(selectMode && incomplete)
1971 errorMessage: model.error
1972 paused: model.paused
1973+ incognito: model.incognito
1974
1975 onClicked: {
1976 if (model.complete && !selectMode) {
1977
1978=== added file 'src/app/webbrowser/HistoryViewWithExpansion.qml'
1979--- src/app/webbrowser/HistoryViewWithExpansion.qml 1970-01-01 00:00:00 +0000
1980+++ src/app/webbrowser/HistoryViewWithExpansion.qml 2016-10-23 20:25:22 +0000
1981@@ -0,0 +1,67 @@
1982+/*
1983+ * Copyright 2014-2016 Canonical Ltd.
1984+ *
1985+ * This file is part of webbrowser-app.
1986+ *
1987+ * webbrowser-app is free software; you can redistribute it and/or modify
1988+ * it under the terms of the GNU General Public License as published by
1989+ * the Free Software Foundation; version 3.
1990+ *
1991+ * webbrowser-app is distributed in the hope that it will be useful,
1992+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1993+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1994+ * GNU General Public License for more details.
1995+ *
1996+ * You should have received a copy of the GNU General Public License
1997+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1998+ */
1999+
2000+import QtQuick 2.4
2001+import Ubuntu.Components 1.3
2002+
2003+FocusScope {
2004+ id: historyViewWithExpansion
2005+
2006+ function loadModel() {
2007+ historyView.loadModel()
2008+ }
2009+
2010+ signal newTabRequested()
2011+ signal historyEntryClicked(url url)
2012+ signal done()
2013+ signal back()
2014+
2015+ HistoryView {
2016+ id: historyView
2017+ anchors.fill: parent
2018+ focus: !expandedHistoryViewLoader.focus
2019+ visible: focus
2020+ onSeeMoreEntriesClicked: {
2021+ expandedHistoryViewLoader.model = model
2022+ expandedHistoryViewLoader.active = true
2023+ }
2024+ onNewTabRequested: historyViewWithExpansion.newTabRequested()
2025+ onBack: historyViewWithExpansion.back()
2026+ }
2027+
2028+ Loader {
2029+ id: expandedHistoryViewLoader
2030+ asynchronous: true
2031+ anchors.fill: parent
2032+ active: false
2033+ focus: active
2034+ property var model: null
2035+ sourceComponent: ExpandedHistoryView {
2036+ focus: true
2037+ model: expandedHistoryViewLoader.model
2038+ onHistoryEntryClicked: historyViewWithExpansion.historyEntryClicked(url)
2039+ onHistoryEntryRemoved: {
2040+ if (count == 1) {
2041+ done()
2042+ }
2043+ HistoryModel.removeEntryByUrl(url)
2044+ }
2045+ onDone: expandedHistoryViewLoader.active = false
2046+ }
2047+ }
2048+}
2049
2050=== modified file 'src/app/webbrowser/NavigationBar.qml'
2051--- src/app/webbrowser/NavigationBar.qml 2016-05-17 17:23:42 +0000
2052+++ src/app/webbrowser/NavigationBar.qml 2016-10-23 20:25:22 +0000
2053@@ -24,6 +24,7 @@
2054 id: root
2055
2056 property var tab
2057+ property alias loading: addressbar.loading
2058 property alias searchUrl: addressbar.searchUrl
2059 readonly property string text: addressbar.text
2060 property alias bookmarked: addressbar.bookmarked
2061@@ -122,8 +123,6 @@
2062
2063 icon: (internal.webview && internal.webview.certificateError) ? "" : tab ? tab.icon : ""
2064
2065- loading: internal.webview ? internal.webview.loading : false
2066-
2067 onValidated: {
2068 if (!findInPageMode) {
2069 internal.webview.forceActiveFocus()
2070@@ -138,15 +137,8 @@
2071 onToggleBookmark: root.toggleBookmark()
2072
2073 Connections {
2074- target: internal.webview
2075- onUrlChanged: {
2076- // ensure that the URL actually changes so that the
2077- // address bar is updated in case the user has entered a
2078- // new address that redirects to where she previously was
2079- // (https://launchpad.net/bugs/1306615)
2080- addressbar.actualUrl = ""
2081- addressbar.actualUrl = internal.webview.url
2082- }
2083+ target: tab
2084+ onUrlChanged: addressbar.actualUrl = tab.url
2085 }
2086 }
2087
2088
2089=== added file 'src/app/webbrowser/TabComponent.qml'
2090--- src/app/webbrowser/TabComponent.qml 1970-01-01 00:00:00 +0000
2091+++ src/app/webbrowser/TabComponent.qml 2016-10-23 20:25:22 +0000
2092@@ -0,0 +1,436 @@
2093+/*
2094+ * Copyright 2014-2016 Canonical Ltd.
2095+ *
2096+ * This file is part of webbrowser-app.
2097+ *
2098+ * webbrowser-app is free software; you can redistribute it and/or modify
2099+ * it under the terms of the GNU General Public License as published by
2100+ * the Free Software Foundation; version 3.
2101+ *
2102+ * webbrowser-app is distributed in the hope that it will be useful,
2103+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2104+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2105+ * GNU General Public License for more details.
2106+ *
2107+ * You should have received a copy of the GNU General Public License
2108+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2109+ */
2110+
2111+import QtQuick 2.4
2112+import Ubuntu.Components 1.3
2113+import Ubuntu.Components.Popups 1.3
2114+import com.canonical.Oxide 1.15 as Oxide
2115+import webbrowserapp.private 0.1
2116+import "../actions" as Actions
2117+import ".."
2118+
2119+// FIXME: This component breaks encapsulation: it uses variables not defined in
2120+// itself. However this is an acceptable tradeoff with regards to
2121+// startup time performance. Indeed having this component defined as a separate
2122+// QML file as opposed to inline makes it possible to cache its compiled form.
2123+
2124+Component {
2125+ id: tabComponent
2126+
2127+ BrowserTab {
2128+ anchors.fill: parent
2129+ incognito: browser.incognito
2130+ current: tabsModel && tabsModel.currentTab === this
2131+ focus: current
2132+
2133+ Item {
2134+ id: contextualMenuTarget
2135+ visible: false
2136+ }
2137+
2138+ webviewComponent: WebViewImpl {
2139+ id: webviewimpl
2140+
2141+ property BrowserTab tab
2142+ readonly property bool current: tab.current
2143+
2144+ currentWebview: browser.currentWebview
2145+ filePicker: filePickerLoader.item
2146+
2147+ anchors.fill: parent
2148+
2149+ focus: true
2150+
2151+ enabled: current && !bottomEdgeHandle.dragging && !recentView.visible && tabContainer.focus
2152+
2153+ locationBarController {
2154+ height: chrome.height
2155+ mode: chromeController.defaultMode
2156+ }
2157+
2158+ //experimental.preferences.developerExtrasEnabled: developerExtrasEnabled
2159+ preferences.localStorageEnabled: true
2160+ preferences.appCacheEnabled: true
2161+
2162+ property QtObject contextModel: null
2163+ contextualActions: ActionList {
2164+ Actions.OpenLinkInNewTab {
2165+ objectName: "OpenLinkInNewTabContextualAction"
2166+ enabled: contextModel && contextModel.linkUrl.toString()
2167+ onTriggered: internal.openUrlInNewTab(contextModel.linkUrl, true)
2168+ }
2169+ Actions.OpenLinkInNewBackgroundTab {
2170+ objectName: "OpenLinkInNewBackgroundTabContextualAction"
2171+ enabled: contextModel && contextModel.linkUrl.toString()
2172+ onTriggered: internal.openUrlInNewTab(contextModel.linkUrl, false)
2173+ }
2174+ Actions.OpenLinkInNewWindow {
2175+ objectName: "OpenLinkInNewWindowContextualAction"
2176+ enabled: contextModel && contextModel.linkUrl.toString()
2177+ onTriggered: browser.openLinkInWindowRequested(contextModel.linkUrl, false)
2178+ }
2179+ Actions.OpenLinkInPrivateWindow {
2180+ objectName: "OpenLinkInPrivateWindowContextualAction"
2181+ enabled: contextModel && contextModel.linkUrl.toString()
2182+ onTriggered: browser.openLinkInWindowRequested(contextModel.linkUrl, true)
2183+ }
2184+ Actions.BookmarkLink {
2185+ objectName: "BookmarkLinkContextualAction"
2186+ enabled: contextModel && contextModel.linkUrl.toString()
2187+ && !BookmarksModel.contains(contextModel.linkUrl)
2188+ onTriggered: {
2189+ // position the menu target with a one-off assignement instead of a binding
2190+ // since the contents of the contextModel have meaning only while the context
2191+ // menu is active
2192+ contextualMenuTarget.x = contextModel.position.x
2193+ contextualMenuTarget.y = contextModel.position.y + locationBarController.height + locationBarController.offset
2194+ internal.addBookmark(contextModel.linkUrl, contextModel.linkText,
2195+ "", contextualMenuTarget)
2196+ }
2197+ }
2198+ Actions.CopyLink {
2199+ objectName: "CopyLinkContextualAction"
2200+ enabled: contextModel && contextModel.linkUrl.toString()
2201+ onTriggered: Clipboard.push(["text/plain", contextModel.linkUrl.toString()])
2202+ }
2203+ Actions.SaveLink {
2204+ objectName: "SaveLinkContextualAction"
2205+ enabled: contextModel && contextModel.linkUrl.toString()
2206+ onTriggered: contextModel.saveLink()
2207+ }
2208+ Actions.Share {
2209+ objectName: "ShareContextualAction"
2210+ enabled: (contentHandlerLoader.status == Loader.Ready) && contextModel &&
2211+ (contextModel.linkUrl.toString() || contextModel.selectionText)
2212+ onTriggered: {
2213+ if (contextModel.linkUrl.toString()) {
2214+ internal.shareLink(contextModel.linkUrl.toString(), contextModel.linkText)
2215+ } else if (contextModel.selectionText) {
2216+ internal.shareText(contextModel.selectionText)
2217+ }
2218+ }
2219+ }
2220+ Actions.OpenImageInNewTab {
2221+ objectName: "OpenImageInNewTabContextualAction"
2222+ enabled: contextModel &&
2223+ (contextModel.mediaType === Oxide.WebView.MediaTypeImage) &&
2224+ contextModel.srcUrl.toString()
2225+ onTriggered: internal.openUrlInNewTab(contextModel.srcUrl, true)
2226+ }
2227+ Actions.CopyImage {
2228+ objectName: "CopyImageContextualAction"
2229+ enabled: contextModel &&
2230+ (contextModel.mediaType === Oxide.WebView.MediaTypeImage) &&
2231+ contextModel.srcUrl.toString()
2232+ onTriggered: Clipboard.push(["text/plain", contextModel.srcUrl.toString()])
2233+ }
2234+ Actions.SaveImage {
2235+ objectName: "SaveImageContextualAction"
2236+ enabled: contextModel &&
2237+ ((contextModel.mediaType === Oxide.WebView.MediaTypeImage) ||
2238+ (contextModel.mediaType === Oxide.WebView.MediaTypeCanvas)) &&
2239+ contextModel.hasImageContents
2240+ onTriggered: contextModel.saveMedia()
2241+ }
2242+ Actions.OpenVideoInNewTab {
2243+ objectName: "OpenVideoInNewTabContextualAction"
2244+ enabled: contextModel &&
2245+ (contextModel.mediaType === Oxide.WebView.MediaTypeVideo) &&
2246+ contextModel.srcUrl.toString()
2247+ onTriggered: internal.openUrlInNewTab(contextModel.srcUrl, true)
2248+ }
2249+ Actions.SaveVideo {
2250+ objectName: "SaveVideoContextualAction"
2251+ enabled: contextModel &&
2252+ (contextModel.mediaType === Oxide.WebView.MediaTypeVideo) &&
2253+ contextModel.srcUrl.toString()
2254+ onTriggered: contextModel.saveMedia()
2255+ }
2256+ Actions.Undo {
2257+ objectName: "UndoContextualAction"
2258+ enabled: contextModel && contextModel.isEditable &&
2259+ (contextModel.editFlags & Oxide.WebView.UndoCapability)
2260+ onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandUndo)
2261+ }
2262+ Actions.Redo {
2263+ objectName: "RedoContextualAction"
2264+ enabled: contextModel && contextModel.isEditable &&
2265+ (contextModel.editFlags & Oxide.WebView.RedoCapability)
2266+ onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandRedo)
2267+ }
2268+ Actions.Cut {
2269+ objectName: "CutContextualAction"
2270+ enabled: contextModel && contextModel.isEditable &&
2271+ (contextModel.editFlags & Oxide.WebView.CutCapability)
2272+ onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandCut)
2273+ }
2274+ Actions.Copy {
2275+ objectName: "CopyContextualAction"
2276+ enabled: contextModel && (contextModel.selectionText ||
2277+ (contextModel.isEditable &&
2278+ (contextModel.editFlags & Oxide.WebView.CopyCapability)))
2279+ onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandCopy)
2280+ }
2281+ Actions.Paste {
2282+ objectName: "PasteContextualAction"
2283+ enabled: contextModel && contextModel.isEditable &&
2284+ (contextModel.editFlags & Oxide.WebView.PasteCapability)
2285+ onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandPaste)
2286+ }
2287+ Actions.Erase {
2288+ objectName: "EraseContextualAction"
2289+ enabled: contextModel && contextModel.isEditable &&
2290+ (contextModel.editFlags & Oxide.WebView.EraseCapability)
2291+ onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandErase)
2292+ }
2293+ Actions.SelectAll {
2294+ objectName: "SelectAllContextualAction"
2295+ enabled: contextModel && contextModel.isEditable &&
2296+ (contextModel.editFlags & Oxide.WebView.SelectAllCapability)
2297+ onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandSelectAll)
2298+ }
2299+ }
2300+
2301+ function contextMenuOnCompleted(menu) {
2302+ contextModel = menu.contextModel
2303+ if (contextModel.linkUrl.toString() ||
2304+ contextModel.srcUrl.toString() ||
2305+ contextModel.selectionText ||
2306+ (contextModel.isEditable && contextModel.editFlags) ||
2307+ (((contextModel.mediaType == Oxide.WebView.MediaTypeImage) ||
2308+ (contextModel.mediaType == Oxide.WebView.MediaTypeCanvas)) &&
2309+ contextModel.hasImageContents)) {
2310+ menu.show()
2311+ } else {
2312+ contextModel.close()
2313+ }
2314+ }
2315+
2316+ Component {
2317+ id: contextMenuNarrowComponent
2318+ ContextMenuMobile {
2319+ actions: contextualActions
2320+ Component.onCompleted: webviewimpl.contextMenuOnCompleted(this)
2321+ }
2322+ }
2323+ Component {
2324+ id: contextMenuWideComponent
2325+ ContextMenuWide {
2326+ webview: webviewimpl
2327+ parent: browser
2328+ actions: contextualActions
2329+ Component.onCompleted: webviewimpl.contextMenuOnCompleted(this)
2330+ }
2331+ }
2332+ contextMenu: browser.wide ? contextMenuWideComponent : contextMenuNarrowComponent
2333+
2334+ onNewViewRequested: {
2335+ var tab = tabComponent.createObject(tabContainer, {"request": request})
2336+ var setCurrent = (request.disposition == Oxide.NewViewRequest.DispositionNewForegroundTab)
2337+ internal.addTab(tab, setCurrent)
2338+ if (setCurrent) tabContainer.forceActiveFocus()
2339+ }
2340+
2341+ onCloseRequested: prepareToClose()
2342+ onPrepareToCloseResponse: {
2343+ if (proceed) {
2344+ if (tab) {
2345+ for (var i = 0; i < tabsModel.count; ++i) {
2346+ if (tabsModel.get(i) === tab) {
2347+ tabsModel.remove(i)
2348+ break
2349+ }
2350+ }
2351+ tab.close()
2352+ }
2353+ if (tabsModel.count === 0) {
2354+ internal.openUrlInNewTab("", true, true)
2355+ }
2356+ }
2357+ }
2358+
2359+ QtObject {
2360+ id: webviewInternal
2361+ property url storedUrl: ""
2362+ property bool titleSet: false
2363+ property string title: ""
2364+ }
2365+ onLoadEvent: {
2366+ if (event.type == Oxide.LoadEvent.TypeCommitted) {
2367+ chrome.findInPageMode = false
2368+ webviewInternal.titleSet = false
2369+ webviewInternal.title = title
2370+ }
2371+
2372+ if (webviewimpl.incognito) {
2373+ return
2374+ }
2375+
2376+ if ((event.type == Oxide.LoadEvent.TypeCommitted) &&
2377+ !event.isError &&
2378+ (300 > event.httpStatusCode) && (event.httpStatusCode >= 200)) {
2379+ webviewInternal.storedUrl = event.url
2380+ HistoryModel.add(event.url, title, icon)
2381+ }
2382+
2383+ // If the page has started, stopped, redirected, errored
2384+ // then clear the cache for the history update
2385+ // Otherwise if no title change has occurred the next title
2386+ // change will be the url of the next page causing the
2387+ // history entry to be incorrect (pad.lv/1603835)
2388+ if (event.type == Oxide.LoadEvent.TypeFailed ||
2389+ event.type == Oxide.LoadEvent.TypeRedirected ||
2390+ event.type == Oxide.LoadEvent.TypeStarted ||
2391+ event.type == Oxide.LoadEvent.TypeStopped) {
2392+ webviewInternal.titleSet = true
2393+ webviewInternal.storedUrl = ""
2394+ }
2395+ }
2396+ onTitleChanged: {
2397+ if (!webviewInternal.titleSet && webviewInternal.storedUrl.toString()) {
2398+ // Record the title to avoid updating the history database
2399+ // every time the page dynamically updates its title.
2400+ // We don’t want pages that update their title every second
2401+ // to achieve an ugly "scrolling title" effect to flood the
2402+ // history database with updates.
2403+ webviewInternal.titleSet = true
2404+ if (webviewInternal.title != title) {
2405+ webviewInternal.title = title
2406+ HistoryModel.update(webviewInternal.storedUrl, title, icon)
2407+ }
2408+ }
2409+ }
2410+ onIconChanged: {
2411+ if (webviewInternal.storedUrl.toString()) {
2412+ HistoryModel.update(webviewInternal.storedUrl, webviewInternal.title, icon)
2413+ }
2414+ }
2415+
2416+ onGeolocationPermissionRequested: requestGeolocationPermission(request)
2417+
2418+ property var certificateError
2419+ function resetCertificateError() {
2420+ certificateError = null
2421+ }
2422+ onCertificateError: {
2423+ if (!error.isMainFrame || error.isSubresource) {
2424+ // Not a main frame document error, just block the content
2425+ // (it’s not overridable anyway).
2426+ return
2427+ }
2428+ if (internal.isCertificateErrorAllowed(error)) {
2429+ error.allow()
2430+ } else {
2431+ certificateError = error
2432+ error.onCancelled.connect(webviewimpl.resetCertificateError)
2433+ }
2434+ }
2435+
2436+ onFullscreenChanged: {
2437+ if (fullscreen) {
2438+ fullscreenExitHintComponent.createObject(webviewimpl)
2439+ }
2440+ }
2441+ Component {
2442+ id: fullscreenExitHintComponent
2443+
2444+ Rectangle {
2445+ id: fullscreenExitHint
2446+ objectName: "fullscreenExitHint"
2447+
2448+ anchors.centerIn: parent
2449+ height: units.gu(6)
2450+ width: Math.min(units.gu(50), parent.width - units.gu(12))
2451+ radius: units.gu(1)
2452+ color: "#3e3b39"
2453+ opacity: 0.85
2454+
2455+ Behavior on opacity {
2456+ UbuntuNumberAnimation {
2457+ duration: UbuntuAnimation.SlowDuration
2458+ }
2459+ }
2460+ onOpacityChanged: {
2461+ if (opacity == 0.0) {
2462+ fullscreenExitHint.destroy()
2463+ }
2464+ }
2465+
2466+ // Delay showing the hint to prevent it from jumping up while the
2467+ // webview is being resized (https://launchpad.net/bugs/1454097).
2468+ visible: false
2469+ Timer {
2470+ running: true
2471+ interval: 250
2472+ onTriggered: fullscreenExitHint.visible = true
2473+ }
2474+
2475+ Label {
2476+ color: "white"
2477+ font.weight: Font.Light
2478+ anchors.centerIn: parent
2479+ text: bottomEdgeHandle.enabled
2480+ ? i18n.tr("Swipe Up To Exit Full Screen")
2481+ : i18n.tr("Press ESC To Exit Full Screen")
2482+ }
2483+
2484+ Timer {
2485+ running: fullscreenExitHint.visible
2486+ interval: 2000
2487+ onTriggered: fullscreenExitHint.opacity = 0
2488+ }
2489+
2490+ Connections {
2491+ target: webviewimpl
2492+ onFullscreenChanged: {
2493+ if (!webviewimpl.fullscreen) {
2494+ fullscreenExitHint.destroy()
2495+ }
2496+ }
2497+ }
2498+
2499+ Component.onCompleted: bottomEdgeHint.forceShow = true
2500+ Component.onDestruction: bottomEdgeHint.forceShow = false
2501+ }
2502+ }
2503+
2504+ onShowDownloadDialog: {
2505+ if (downloadDialogLoader.status === Loader.Ready) {
2506+ var downloadDialog = PopupUtils.open(downloadDialogLoader.item, browser, {"contentType" : contentType,
2507+ "downloadId" : downloadId,
2508+ "singleDownload" : downloader,
2509+ "filename" : filename,
2510+ "mimeType" : mimeType})
2511+ downloadDialog.startDownload.connect(startDownload)
2512+ }
2513+ }
2514+
2515+ function showDownloadsPage() {
2516+ downloadsViewLoader.active = true
2517+ return downloadsViewLoader.item
2518+ }
2519+
2520+ function startDownload(downloadId, download, mimeType) {
2521+ DownloadsModel.add(downloadId, download.url, mimeType, incognito)
2522+ download.start()
2523+ downloadsViewLoader.active = true
2524+ }
2525+
2526+ }
2527+ }
2528+}
2529
2530=== modified file 'src/app/webbrowser/TabItem.qml'
2531--- src/app/webbrowser/TabItem.qml 2016-07-01 08:52:37 +0000
2532+++ src/app/webbrowser/TabItem.qml 2016-10-23 20:25:22 +0000
2533@@ -22,6 +22,7 @@
2534
2535 Item {
2536 id: tabItem
2537+ objectName: "tabItem"
2538
2539 property bool incognito: false
2540 property bool active: false
2541@@ -38,6 +39,8 @@
2542 property color fgColor: Theme.palette.normal.baseText
2543
2544 property bool touchEnabled: true
2545+
2546+ readonly property bool showCloseIcon: closeIcon.x > units.gu(1) + tabItem.width / 2
2547
2548 signal selected()
2549 signal closed()
2550@@ -54,10 +57,17 @@
2551
2552 Favicon {
2553 id: favicon
2554- anchors.verticalCenter: parent.verticalCenter
2555- anchors.left: parent.left
2556- anchors.leftMargin: units.gu(2)
2557+ anchors {
2558+ left: tabItem.showCloseIcon ? parent.left : undefined
2559+ leftMargin: Math.min(tabItem.width / 4, units.gu(2))
2560+ horizontalCenter: tabItem.showCloseIcon ? undefined : parent.horizontalCenter
2561+ verticalCenter: parent.verticalCenter
2562+ }
2563 shouldCache: !incognito
2564+
2565+ // Scale width and height of favicon when tabWidth becomes small
2566+ height: width
2567+ width: Math.min(units.dp(16), Math.min(tabItem.width - anchors.leftMargin * 2, tabItem.height))
2568 }
2569
2570 Item {
2571@@ -132,6 +142,7 @@
2572 anchors.bottom: touchEnabled ? parent.bottom : undefined
2573 anchors.right: touchEnabled ? parent.right : undefined
2574 width: touchEnabled ? units.gu(4) : closeIcon.width
2575+ visible: closeIcon.visible
2576
2577 onClicked: closed()
2578
2579@@ -149,9 +160,10 @@
2580 anchors.right: parent.right
2581 anchors.rightMargin: units.gu(1)
2582 anchors.verticalCenter: parent.verticalCenter
2583+ asynchronous: true
2584 name: "close"
2585 color: tabItem.fgColor
2586- asynchronous: true
2587+ visible: tabItem.showCloseIcon
2588 }
2589 }
2590 }
2591
2592=== modified file 'src/app/webbrowser/TabsBar.qml'
2593--- src/app/webbrowser/TabsBar.qml 2016-02-05 11:21:32 +0000
2594+++ src/app/webbrowser/TabsBar.qml 2016-10-23 20:25:22 +0000
2595@@ -30,6 +30,18 @@
2596 property real maxTabWidth: units.gu(20)
2597 property real tabWidth: model ? Math.max(Math.min(tabsContainer.maxWidth / model.count, maxTabWidth), minTabWidth) : 0
2598
2599+ // Minimum size of the larger tab
2600+ readonly property real minActiveTabWidth: units.gu(10)
2601+
2602+ // When there is a larger tab, calc the smaller tab size
2603+ readonly property real nonActiveTabWidth: (tabsContainer.maxWidth - minActiveTabWidth) / Math.max(model.count - 1, 1)
2604+
2605+ // The size of the right margin of the tab
2606+ readonly property real rightMargin: units.dp(1)
2607+
2608+ // Whether there will be one larger tab or not
2609+ readonly property bool unevenTabWidth: tabWidth + rightMargin < minActiveTabWidth
2610+
2611 property bool incognito: false
2612
2613 property color fgColor: Theme.palette.normal.baseText
2614@@ -130,8 +142,8 @@
2615 readonly property int tabIndex: index
2616
2617 anchors.top: tabsContainer.top
2618- property real rightMargin: units.dp(1)
2619- width: tabWidth + rightMargin
2620+
2621+ width: getSize(index)
2622 height: tabsContainer.height
2623
2624 acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
2625@@ -156,7 +168,7 @@
2626
2627 touchEnabled: root.touchEnabled
2628
2629- rightMargin: tabDelegate.rightMargin
2630+ rightMargin: root.rightMargin
2631
2632 onClosed: root.tabClosed(index)
2633 onSelected: root.switchToTab(index)
2634@@ -168,19 +180,52 @@
2635 property: "reordering"
2636 value: dragging
2637 }
2638+
2639+ Behavior on width { NumberAnimation { duration: 250 } }
2640
2641 Binding on x {
2642 when: !dragging
2643- value: index * width
2644+ value: getLeftX(index)
2645 }
2646
2647 Behavior on x { NumberAnimation { duration: 250 } }
2648
2649+ function getLeftX(index) {
2650+ if (unevenTabWidth) {
2651+ if (index > root.model.currentIndex) {
2652+ return minActiveTabWidth + (nonActiveTabWidth * (index - 1))
2653+ } else {
2654+ return nonActiveTabWidth * index
2655+ }
2656+ } else {
2657+ // Do not depend on width otherwise X updates after
2658+ // Width causing the animation to be two stage
2659+ // instead perform same calculation (tabWidth + rightMargin)
2660+ return index * (tabWidth + rightMargin)
2661+ }
2662+ }
2663+
2664+ function getSize(index) {
2665+ if (unevenTabWidth) {
2666+ // Uneven tabs so use large or small depending which index
2667+ if (index === root.model.currentIndex) {
2668+ return minActiveTabWidth
2669+ } else {
2670+ return nonActiveTabWidth
2671+ }
2672+ } else {
2673+ return tabWidth + rightMargin
2674+ }
2675+ }
2676+
2677 onXChanged: {
2678 if (!dragging) return
2679- if (x < (index * width - width / 2)) {
2680+
2681+ var leftX = getLeftX(index)
2682+
2683+ if (x < (leftX - getSize(index - 1) / 2) && index > 0) {
2684 root.model.move(index, index - 1)
2685- } else if ((x > (index * width + width / 2)) && (index < (root.model.count - 1))) {
2686+ } else if ((x > (leftX + getSize(index + 1) / 2)) && (index < (root.model.count - 1))) {
2687 root.model.move(index + 1, index)
2688 }
2689 }
2690
2691=== modified file 'src/app/webbrowser/downloads-model.cpp'
2692--- src/app/webbrowser/downloads-model.cpp 2016-07-06 09:31:57 +0000
2693+++ src/app/webbrowser/downloads-model.cpp 2016-10-23 20:25:22 +0000
2694@@ -104,6 +104,7 @@
2695 int count = 0; // size() isn't supported on the sqlite backend
2696 while (populateQuery.next()) {
2697 DownloadEntry entry;
2698+ entry.incognito = false;
2699 entry.downloadId = populateQuery.value(0).toString();
2700 entry.url = populateQuery.value(1).toUrl();
2701 entry.path = populateQuery.value(2).toString();
2702@@ -147,6 +148,7 @@
2703 roles[Paused] = "paused";
2704 roles[Error] = "error";
2705 roles[Created] = "created";
2706+ roles[Incognito] = "incognito";
2707 }
2708 return roles;
2709 }
2710@@ -182,6 +184,8 @@
2711 return entry.error;
2712 case Created:
2713 return entry.created;
2714+ case Incognito:
2715+ return entry.incognito;
2716 default:
2717 return QVariant();
2718 }
2719@@ -218,7 +222,7 @@
2720 Add a download to the database. This should happen as soon as the download
2721 is started.
2722 */
2723-void DownloadsModel::add(const QString& downloadId, const QUrl& url, const QString& mimetype)
2724+void DownloadsModel::add(const QString& downloadId, const QUrl& url, const QString& mimetype, bool incognito)
2725 {
2726 beginInsertRows(QModelIndex(), 0, 0);
2727 DownloadEntry entry;
2728@@ -227,83 +231,111 @@
2729 entry.paused = false;
2730 entry.url = url;
2731 entry.mimetype = mimetype;
2732+ entry.incognito = incognito;
2733 m_orderedEntries.prepend(entry);
2734 m_numRows++;
2735- m_fetchedCount++;
2736 endInsertRows();
2737- Q_EMIT added(downloadId, url, mimetype);
2738- insertNewEntryInDatabase(entry);
2739 Q_EMIT rowCountChanged();
2740-}
2741-
2742-void DownloadsModel::setPath(const QString& downloadId, const QString& path)
2743-{
2744- QSqlQuery query(m_database);
2745-
2746- // Override reported mimetype from server with detected mimetype from file once downloaded
2747- QMimeDatabase mimeDatabase;
2748- QString mimetype = mimeDatabase.mimeTypeForFile(path).name();
2749-
2750- static QString updateStatement = QLatin1String("UPDATE downloads SET mimetype = ?, "
2751- "path = ? WHERE downloadId = ?");
2752- query.prepare(updateStatement);
2753- query.addBindValue(mimetype);
2754- query.addBindValue(path);
2755- query.addBindValue(downloadId);
2756- query.exec();
2757- Q_EMIT pathChanged(downloadId, path);
2758+ if (!incognito) {
2759+ insertNewEntryInDatabase(entry);
2760+ m_fetchedCount++;
2761+ }
2762 }
2763
2764 void DownloadsModel::setComplete(const QString& downloadId, const bool complete)
2765 {
2766- QSqlQuery query(m_database);
2767- static QString updateStatement = QLatin1String("UPDATE downloads SET complete = ? "
2768- "WHERE downloadId = ?");
2769- query.prepare(updateStatement);
2770- query.addBindValue(complete);
2771- query.addBindValue(downloadId);
2772- query.exec();
2773- Q_EMIT completeChanged(downloadId, complete);
2774- reload();
2775+ int index = getIndexForDownloadId(downloadId);
2776+ if (index != -1) {
2777+ DownloadEntry& entry = m_orderedEntries[index];
2778+ if (entry.complete == complete) {
2779+ return;
2780+ }
2781+ entry.complete = complete;
2782+ Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), QVector<int>() << Complete);
2783+ if (!entry.incognito) {
2784+ QSqlQuery query(m_database);
2785+ static QString updateStatement = QLatin1String("UPDATE downloads SET complete=? WHERE downloadId=?;");
2786+ query.prepare(updateStatement);
2787+ query.addBindValue(complete);
2788+ query.addBindValue(downloadId);
2789+ query.exec();
2790+ }
2791+ }
2792 }
2793
2794 void DownloadsModel::setError(const QString& downloadId, const QString& error)
2795 {
2796- QSqlQuery query(m_database);
2797- static QString updateStatement = QLatin1String("UPDATE downloads SET error = ? "
2798- "WHERE downloadId = ?");
2799- query.prepare(updateStatement);
2800- query.addBindValue(error);
2801- query.addBindValue(downloadId);
2802- query.exec();
2803- Q_EMIT errorChanged(downloadId, error);
2804- reload();
2805+ int index = getIndexForDownloadId(downloadId);
2806+ if (index != -1) {
2807+ DownloadEntry& entry = m_orderedEntries[index];
2808+ if (entry.error == error) {
2809+ return;
2810+ }
2811+ entry.error = error;
2812+ Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), QVector<int>() << Error);
2813+ if (!entry.incognito) {
2814+ QSqlQuery query(m_database);
2815+ static QString updateStatement = QLatin1String("UPDATE downloads SET error=? WHERE downloadId=?;");
2816+ query.prepare(updateStatement);
2817+ query.addBindValue(error);
2818+ query.addBindValue(downloadId);
2819+ query.exec();
2820+ }
2821+ }
2822 }
2823
2824 void DownloadsModel::moveToDownloads(const QString& downloadId, const QString& path)
2825 {
2826+ int index = getIndexForDownloadId(downloadId);
2827+ if (index == -1) {
2828+ return;
2829+ }
2830 QFile file(path);
2831 if (file.exists()) {
2832 QFileInfo fi(path);
2833- QString suffix = fi.completeSuffix();
2834- QString filename = fi.fileName();
2835- QString filenameWithoutSuffix = filename.left(filename.size() - suffix.size());
2836+ DownloadEntry& entry = m_orderedEntries[index];
2837+ QVector<int> updatedRoles;
2838+
2839+ // Override reported mimetype from server with detected mimetype from file once downloaded
2840+ QMimeDatabase mimeDatabase;
2841+ QString mimetype = mimeDatabase.mimeTypeForFile(fi).name();
2842+ if (mimetype != entry.mimetype) {
2843+ entry.mimetype = mimetype;
2844+ updatedRoles.append(Mimetype);
2845+ }
2846+
2847+ // Move file to XDG Downloads folder
2848 QDir dir(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
2849 if (!dir.exists()) {
2850 QDir::root().mkpath(dir.absolutePath());
2851 }
2852- QString destination = dir.absoluteFilePath(filenameWithoutSuffix + suffix);
2853+ QString baseName = fi.baseName();
2854+ QString suffix = fi.completeSuffix();
2855+ QString destination = dir.absoluteFilePath(QString("%1.%2").arg(baseName, suffix));
2856 // Avoid filename collision by automatically inserting an incremented
2857 // number into the filename if the original name already exists.
2858 int append = 1;
2859 while (QFile::exists(destination)) {
2860- destination = dir.absoluteFilePath(QString("%1%2.%3").arg(filenameWithoutSuffix, QString::number(append++), suffix));
2861+ destination = dir.absoluteFilePath(QString("%1.%2.%3").arg(baseName, QString::number(append++), suffix));
2862 }
2863 if (file.rename(destination)) {
2864- setPath(downloadId, destination);
2865+ entry.path = destination;
2866+ updatedRoles.append(Path);
2867 } else {
2868 qWarning() << "Failed moving file from" << path << "to" << destination;
2869 }
2870+
2871+ Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), updatedRoles);
2872+ if (!entry.incognito && !updatedRoles.isEmpty()) {
2873+ QSqlQuery query(m_database);
2874+ static QString updateStatement = QLatin1String("UPDATE downloads SET mimetype = ?, "
2875+ "path = ? WHERE downloadId = ?");
2876+ query.prepare(updateStatement);
2877+ query.addBindValue(mimetype);
2878+ query.addBindValue(destination);
2879+ query.addBindValue(downloadId);
2880+ query.exec();
2881+ }
2882 } else {
2883 qWarning() << "Download not found:" << path;
2884 }
2885@@ -331,15 +363,17 @@
2886 int index = 0;
2887 Q_FOREACH(DownloadEntry entry, m_orderedEntries) {
2888 if (entry.path == path) {
2889+ bool incognito = entry.incognito;
2890 beginRemoveRows(QModelIndex(), index, index);
2891 m_orderedEntries.removeAt(index);
2892 endRemoveRows();
2893- Q_EMIT deleted(path);
2894- removeExistingEntryFromDatabase(path);
2895- m_fetchedCount--;
2896 m_numRows--;
2897 Q_EMIT rowCountChanged();
2898 QFile::remove(path);
2899+ if (!incognito) {
2900+ removeExistingEntryFromDatabase(path);
2901+ m_fetchedCount--;
2902+ }
2903 return;
2904 } else {
2905 index++;
2906@@ -352,45 +386,69 @@
2907 */
2908 void DownloadsModel::cancelDownload(const QString& downloadId)
2909 {
2910- int index=0;
2911- Q_FOREACH(DownloadEntry entry, m_orderedEntries) {
2912- if (entry.downloadId == downloadId) {
2913- beginRemoveRows(QModelIndex(), index, index);
2914- m_orderedEntries.removeAt(index);
2915+ int index = getIndexForDownloadId(downloadId);
2916+ if (index != -1) {
2917+ const DownloadEntry& entry = m_orderedEntries.at(index);
2918+ bool incognito = entry.incognito;
2919+ beginRemoveRows(QModelIndex(), index, index);
2920+ m_orderedEntries.removeAt(index);
2921+ endRemoveRows();
2922+ m_numRows--;
2923+ Q_EMIT rowCountChanged();
2924+ if (!incognito) {
2925 QSqlQuery query(m_database);
2926 static QString deleteStatement = QLatin1String("DELETE FROM downloads WHERE downloadId=?;");
2927 query.prepare(deleteStatement);
2928 query.addBindValue(downloadId);
2929 query.exec();
2930- endRemoveRows();
2931 m_fetchedCount--;
2932- m_numRows--;
2933- Q_EMIT rowCountChanged();
2934+ }
2935+ }
2936+}
2937+
2938+void DownloadsModel::setPaused(const QString& downloadId, bool paused)
2939+{
2940+ int index = getIndexForDownloadId(downloadId);
2941+ if (index != -1) {
2942+ DownloadEntry& entry = m_orderedEntries[index];
2943+ if (entry.paused == paused) {
2944 return;
2945- } else {
2946- index++;
2947+ }
2948+ entry.paused = paused;
2949+ Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), QVector<int>() << Paused);
2950+ if (!entry.incognito) {
2951+ QSqlQuery query(m_database);
2952+ static QString pauseStatement = QLatin1String("UPDATE downloads SET paused=? WHERE downloadId=?;");
2953+ query.prepare(pauseStatement);
2954+ query.addBindValue(paused);
2955+ query.addBindValue(downloadId);
2956+ query.exec();
2957 }
2958 }
2959 }
2960
2961 void DownloadsModel::pauseDownload(const QString& downloadId)
2962 {
2963- QSqlQuery query(m_database);
2964- static QString pauseStatement = QLatin1String("UPDATE downloads SET paused=1 WHERE downloadId=?;");
2965- query.prepare(pauseStatement);
2966- query.addBindValue(downloadId);
2967- query.exec();
2968- reload();
2969+ setPaused(downloadId, true);
2970 }
2971
2972 void DownloadsModel::resumeDownload(const QString& downloadId)
2973 {
2974- QSqlQuery query(m_database);
2975- static QString resumeStatement = QLatin1String("UPDATE downloads SET paused=0 WHERE downloadId=?;");
2976- query.prepare(resumeStatement);
2977- query.addBindValue(downloadId);
2978- query.exec();
2979- reload();
2980+ setPaused(downloadId, false);
2981+}
2982+
2983+void DownloadsModel::pruneIncognitoDownloads()
2984+{
2985+ for (int i = m_orderedEntries.size() - 1; i >= 0; --i) {
2986+ const DownloadEntry& entry = m_orderedEntries.at(i);
2987+ if (entry.incognito) {
2988+ beginRemoveRows(QModelIndex(), i, i);
2989+ m_orderedEntries.removeAt(i);
2990+ endRemoveRows();
2991+ m_numRows--;
2992+ Q_EMIT rowCountChanged();
2993+ }
2994+ }
2995 }
2996
2997 void DownloadsModel::removeExistingEntryFromDatabase(const QString& path)
2998@@ -409,14 +467,15 @@
2999 return m_canFetchMore;
3000 }
3001
3002-void DownloadsModel::reload()
3003+int DownloadsModel::getIndexForDownloadId(const QString& downloadId) const
3004 {
3005- beginResetModel();
3006- m_orderedEntries.clear();
3007- m_canFetchMore = true;
3008- m_fetchedCount = 0;
3009- m_numRows = 0;
3010- endResetModel();
3011- fetchMore();
3012- Q_EMIT rowCountChanged();
3013+ int index = 0;
3014+ Q_FOREACH(const DownloadEntry& entry, m_orderedEntries) {
3015+ if (entry.downloadId == downloadId) {
3016+ return index;
3017+ } else {
3018+ ++index;
3019+ }
3020+ }
3021+ return -1;
3022 }
3023
3024=== modified file 'src/app/webbrowser/downloads-model.h'
3025--- src/app/webbrowser/downloads-model.h 2016-01-12 10:37:15 +0000
3026+++ src/app/webbrowser/downloads-model.h 2016-10-23 20:25:22 +0000
3027@@ -49,7 +49,8 @@
3028 Complete,
3029 Paused,
3030 Error,
3031- Created
3032+ Created,
3033+ Incognito
3034 };
3035
3036 // reimplemented from QAbstractListModel
3037@@ -63,23 +64,18 @@
3038 void setDatabasePath(const QString& path);
3039
3040 Q_INVOKABLE bool contains(const QString& downloadId) const;
3041- Q_INVOKABLE void add(const QString& downloadId, const QUrl& url, const QString& mimetype);
3042+ Q_INVOKABLE void add(const QString& downloadId, const QUrl& url, const QString& mimetype, bool incognito);
3043 Q_INVOKABLE void moveToDownloads(const QString& downloadId, const QString& path);
3044- Q_INVOKABLE void setPath(const QString& downloadId, const QString& path);
3045 Q_INVOKABLE void setComplete(const QString& downloadId, const bool complete);
3046 Q_INVOKABLE void setError(const QString& downloadId, const QString& error);
3047 Q_INVOKABLE void deleteDownload(const QString& path);
3048 Q_INVOKABLE void cancelDownload(const QString& downloadId);
3049 Q_INVOKABLE void pauseDownload(const QString& downloadId);
3050 Q_INVOKABLE void resumeDownload(const QString& downloadId);
3051+ Q_INVOKABLE void pruneIncognitoDownloads();
3052
3053 Q_SIGNALS:
3054 void databasePathChanged() const;
3055- void added(const QString& downloadId, const QUrl& url, const QString& mimetype) const;
3056- void pathChanged(const QString& downloadId, const QString& path) const;
3057- void completeChanged(const QString& downloadId, const bool complete) const;
3058- void errorChanged(const QString& downloadId, const QString& error) const;
3059- void deleted(const QString& path) const;
3060 void rowCountChanged();
3061
3062 private:
3063@@ -98,6 +94,7 @@
3064 bool paused;
3065 QString error;
3066 QDateTime created;
3067+ bool incognito;
3068 };
3069 QList<DownloadEntry> m_orderedEntries;
3070
3071@@ -105,7 +102,8 @@
3072 void createOrAlterDatabaseSchema();
3073 void insertNewEntryInDatabase(const DownloadEntry& entry);
3074 void removeExistingEntryFromDatabase(const QString& path);
3075- void reload();
3076+ void setPaused(const QString& downloadId, bool paused);
3077+ int getIndexForDownloadId(const QString& downloadId) const;
3078 };
3079
3080 #endif // __DOWNLOADS_MODEL_H__
3081
3082=== modified file 'src/app/webbrowser/history-model.cpp'
3083--- src/app/webbrowser/history-model.cpp 2016-03-01 09:30:41 +0000
3084+++ src/app/webbrowser/history-model.cpp 2016-10-23 20:25:22 +0000
3085@@ -20,10 +20,12 @@
3086 #include "history-model.h"
3087
3088 // Qt
3089-#include <QtCore/QMutexLocker>
3090+#include <QtCore/QTimer>
3091+#include <QtCore/QWriteLocker>
3092 #include <QtSql/QSqlQuery>
3093
3094-#define CONNECTION_NAME "webbrowser-app-history"
3095+#define SQL_DRIVER QStringLiteral("QSQLITE")
3096+#define CONNECTION_NAME QStringLiteral("webbrowser-app-history")
3097
3098 /*!
3099 \class HistoryModel
3100@@ -39,18 +41,31 @@
3101 The database is read at startup to populate the model, and whenever a new
3102 entry is added to the model the database is updated.
3103 However the model doesn’t monitor the database for external changes.
3104+ All database operations are performed on a separate thread in order not to
3105+ block the UI thread.
3106 */
3107 HistoryModel::HistoryModel(QObject* parent)
3108 : QAbstractListModel(parent)
3109 {
3110- m_database = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), CONNECTION_NAME);
3111+ m_dbWorker = new DbWorker;
3112+ m_dbWorker->moveToThread(&m_dbWorkerThread);
3113+ connect(m_dbWorker, SIGNAL(hiddenEntryFetched(const QUrl&)),
3114+ SLOT(onHiddenEntryFetched(const QUrl&)), Qt::QueuedConnection);
3115+ connect(m_dbWorker,
3116+ SIGNAL(entryFetched(const QUrl&, const QString&, const QString&,
3117+ const QUrl&, int, const QDateTime&)),
3118+ SLOT(onEntryFetched(const QUrl&, const QString&, const QString&,
3119+ const QUrl&, int, const QDateTime&)),
3120+ Qt::QueuedConnection);
3121+ connect(m_dbWorker, SIGNAL(loaded()), SIGNAL(loaded()));
3122+ m_dbWorkerThread.start(QThread::LowPriority);
3123 }
3124
3125 HistoryModel::~HistoryModel()
3126 {
3127- m_database.close();
3128- m_database = QSqlDatabase();
3129- QSqlDatabase::removeDatabase(CONNECTION_NAME);
3130+ m_dbWorker->deleteLater();
3131+ m_dbWorkerThread.quit();
3132+ m_dbWorkerThread.wait();
3133 }
3134
3135 void HistoryModel::resetDatabase(const QString& databaseName)
3136@@ -58,85 +73,35 @@
3137 beginResetModel();
3138 m_hiddenEntries.clear();
3139 m_entries.clear();
3140- m_database.close();
3141- m_database.setDatabaseName(databaseName);
3142- m_database.open();
3143- createOrAlterDatabaseSchema();
3144+ Q_EMIT m_dbWorker->resetDatabase(databaseName);
3145 endResetModel();
3146- populateFromDatabase();
3147-}
3148-
3149-void HistoryModel::createOrAlterDatabaseSchema()
3150-{
3151- QMutexLocker ml(&m_dbMutex);
3152- QSqlQuery createQuery(m_database);
3153- QString query = QLatin1String("CREATE TABLE IF NOT EXISTS history "
3154- "(url VARCHAR, domain VARCHAR, title VARCHAR,"
3155- " icon VARCHAR, visits INTEGER, lastVisit DATETIME);");
3156- createQuery.prepare(query);
3157- createQuery.exec();
3158-
3159- // The first version of the database schema didn’t have a 'domain' column
3160- QSqlQuery tableInfoQuery(m_database);
3161- query = QLatin1String("PRAGMA TABLE_INFO(history);");
3162- tableInfoQuery.prepare(query);
3163- tableInfoQuery.exec();
3164- while (tableInfoQuery.next()) {
3165- if (tableInfoQuery.value("name").toString() == "domain") {
3166- break;
3167- }
3168- }
3169- if (!tableInfoQuery.isValid()) {
3170- QSqlQuery addDomainColumnQuery(m_database);
3171- query = QLatin1String("ALTER TABLE history ADD COLUMN domain VARCHAR;");
3172- addDomainColumnQuery.prepare(query);
3173- addDomainColumnQuery.exec();
3174- // Updating all the entries in the database to add the domain is a
3175- // costly operation that would slow down the application startup,
3176- // do not do it here.
3177- }
3178-
3179- QSqlQuery createHiddenQuery(m_database);
3180- query = QLatin1String("CREATE TABLE IF NOT EXISTS history_hidden (url VARCHAR);");
3181- createHiddenQuery.prepare(query);
3182- createHiddenQuery.exec();
3183-}
3184-
3185-void HistoryModel::populateFromDatabase()
3186-{
3187- QSqlQuery populateQuery(m_database);
3188- QString query = QLatin1String("SELECT url, domain, title, icon, visits, lastVisit "
3189- "FROM history ORDER BY lastVisit DESC;");
3190- populateQuery.prepare(query);
3191- populateQuery.exec();
3192-
3193- QSqlQuery populateHiddenQuery(m_database);
3194- query = QLatin1String("SELECT url FROM history_hidden;");
3195- populateHiddenQuery.prepare(query);
3196- populateHiddenQuery.exec();
3197-
3198- while (populateHiddenQuery.next()) {
3199- m_hiddenEntries.append(populateHiddenQuery.value(0).toUrl());
3200- }
3201-
3202- int count = 0;
3203- while (populateQuery.next()) {
3204- HistoryEntry entry;
3205- entry.url = populateQuery.value(0).toUrl();
3206- entry.domain = populateQuery.value(1).toString();
3207- if (entry.domain.isEmpty()) {
3208- entry.domain = DomainUtils::extractTopLevelDomainName(entry.url);
3209- }
3210- entry.title = populateQuery.value(2).toString();
3211- entry.icon = populateQuery.value(3).toUrl();
3212- entry.visits = populateQuery.value(4).toInt();
3213- entry.lastVisit = QDateTime::fromTime_t(populateQuery.value(5).toInt());
3214- entry.hidden = m_hiddenEntries.contains(entry.url);
3215- beginInsertRows(QModelIndex(), count, count);
3216- m_entries.append(entry);
3217- endInsertRows();
3218- ++count;
3219- }
3220+ Q_EMIT m_dbWorker->fetchEntries();
3221+}
3222+
3223+void HistoryModel::onHiddenEntryFetched(const QUrl& url)
3224+{
3225+ m_hiddenEntries.insert(url);
3226+}
3227+
3228+void HistoryModel::onEntryFetched(const QUrl& url, const QString& domain, const QString& title,
3229+ const QUrl& icon, int visits, const QDateTime& lastVisit)
3230+{
3231+ HistoryEntry entry;
3232+ entry.url = url;
3233+ if (domain.isEmpty()) {
3234+ entry.domain = DomainUtils::extractTopLevelDomainName(url);
3235+ } else {
3236+ entry.domain = domain;
3237+ }
3238+ entry.title = title;
3239+ entry.icon = icon;
3240+ entry.visits = visits;
3241+ entry.lastVisit = lastVisit;
3242+ entry.hidden = m_hiddenEntries.contains(url);
3243+ int index = m_entries.count();
3244+ beginInsertRows(QModelIndex(), index, index);
3245+ m_entries.append(entry);
3246+ endInsertRows();
3247 }
3248
3249 QHash<int, QByteArray> HistoryModel::roleNames() const
3250@@ -194,12 +159,13 @@
3251
3252 const QString HistoryModel::databasePath() const
3253 {
3254- return m_database.databaseName();
3255+ return m_databasePath;
3256 }
3257
3258 void HistoryModel::setDatabasePath(const QString& path)
3259 {
3260- if (path != databasePath()) {
3261+ if (path != m_databasePath) {
3262+ m_databasePath = path;
3263 if (path.isEmpty()) {
3264 resetDatabase(":memory:");
3265 } else {
3266@@ -399,86 +365,55 @@
3267
3268 void HistoryModel::insertNewEntryInDatabase(const HistoryEntry& entry)
3269 {
3270- QMutexLocker ml(&m_dbMutex);
3271- QSqlQuery query(m_database);
3272- static QString insertStatement = QLatin1String("INSERT INTO history (url, domain, title, icon, "
3273- "visits, lastVisit) VALUES (?, ?, ?, ?, 1, ?);");
3274- query.prepare(insertStatement);
3275- query.addBindValue(entry.url.toString());
3276- query.addBindValue(entry.domain);
3277- query.addBindValue(entry.title);
3278- query.addBindValue(entry.icon.toString());
3279- query.addBindValue(entry.lastVisit.toTime_t());
3280- query.exec();
3281+ QVariantList values;
3282+ values << entry.url.toString();
3283+ values << entry.domain;
3284+ values << entry.title;
3285+ values << entry.icon.toString();
3286+ values << entry.lastVisit.toTime_t();
3287+ Q_EMIT m_dbWorker->enqueue(DbWorker::InsertNewEntry, values);
3288 }
3289
3290 void HistoryModel::insertNewEntryInHiddenDatabase(const QUrl& url)
3291 {
3292- QMutexLocker ml(&m_dbMutex);
3293- QSqlQuery query(m_database);
3294- static QString insertStatement = QLatin1String("INSERT INTO history_hidden (url) VALUES (?);");
3295- query.prepare(insertStatement);
3296- query.addBindValue(url.toString());
3297- query.exec();
3298+ Q_EMIT m_dbWorker->enqueue(DbWorker::InsertNewHiddenEntry, QVariantList() << url.toString());
3299 }
3300
3301 void HistoryModel::updateExistingEntryInDatabase(const HistoryEntry& entry)
3302 {
3303- QMutexLocker ml(&m_dbMutex);
3304- QSqlQuery query(m_database);
3305- static QString updateStatement = QLatin1String("UPDATE history SET domain=?, title=?, icon=?, "
3306- "visits=?, lastVisit=? WHERE url=?;");
3307- query.prepare(updateStatement);
3308- query.addBindValue(entry.domain);
3309- query.addBindValue(entry.title);
3310- query.addBindValue(entry.icon.toString());
3311- query.addBindValue(entry.visits);
3312- query.addBindValue(entry.lastVisit.toTime_t());
3313- query.addBindValue(entry.url.toString());
3314- query.exec();
3315+ QVariantList values;
3316+ values << entry.domain;
3317+ values << entry.title;
3318+ values << entry.icon.toString();
3319+ values << entry.visits;
3320+ values << entry.lastVisit.toTime_t();
3321+ values << entry.url.toString();
3322+ Q_EMIT m_dbWorker->enqueue(DbWorker::UpdateExistingEntry, values);
3323 }
3324
3325 void HistoryModel::removeEntryFromDatabaseByUrl(const QUrl& url)
3326 {
3327- QMutexLocker ml(&m_dbMutex);
3328- QSqlQuery query(m_database);
3329- static QString deleteStatement = QLatin1String("DELETE FROM history WHERE url=?;");
3330- query.prepare(deleteStatement);
3331- query.addBindValue(url.toString());
3332- query.exec();
3333+ Q_EMIT m_dbWorker->enqueue(DbWorker::RemoveEntryByUrl, QVariantList() << url.toString());
3334 }
3335
3336 void HistoryModel::removeEntryFromHiddenDatabaseByUrl(const QUrl& url)
3337 {
3338- QMutexLocker ml(&m_dbMutex);
3339- QSqlQuery query(m_database);
3340- static QString deleteStatement = QLatin1String("DELETE FROM history_hidden WHERE url=?;");
3341- query.prepare(deleteStatement);
3342- query.addBindValue(url.toString());
3343- query.exec();
3344+ Q_EMIT m_dbWorker->enqueue(DbWorker::RemoveHiddenEntryByUrl, QVariantList() << url.toString());
3345 }
3346
3347 void HistoryModel::removeEntriesFromDatabaseByDate(const QDate& date)
3348 {
3349- QMutexLocker ml(&m_dbMutex);
3350- QSqlQuery query(m_database);
3351- static QString deleteStatement = QLatin1String("DELETE FROM history WHERE lastVisit BETWEEN ? AND ?;");
3352- query.prepare(deleteStatement);
3353+ QVariantList values;
3354 QDateTime dateTime = QDateTime(date);
3355- query.addBindValue(dateTime.toTime_t());
3356+ values << dateTime.toTime_t();
3357 dateTime.setTime(QTime(23, 59, 59, 999));
3358- query.addBindValue(dateTime.toTime_t());
3359- query.exec();
3360+ values << dateTime.toTime_t();
3361+ Q_EMIT m_dbWorker->enqueue(DbWorker::RemoveEntriesByDate, values);
3362 }
3363
3364 void HistoryModel::removeEntriesFromDatabaseByDomain(const QString& domain)
3365 {
3366- QMutexLocker ml(&m_dbMutex);
3367- QSqlQuery query(m_database);
3368- static QString deleteStatement = QLatin1String("DELETE FROM history WHERE domain=?;");
3369- query.prepare(deleteStatement);
3370- query.addBindValue(domain);
3371- query.exec();
3372+ Q_EMIT m_dbWorker->enqueue(DbWorker::RemoveEntriesByDomain, QVariantList() << domain);
3373 }
3374
3375 void HistoryModel::clearAll()
3376@@ -495,16 +430,8 @@
3377
3378 void HistoryModel::clearDatabase()
3379 {
3380- QMutexLocker ml(&m_dbMutex);
3381- QSqlQuery deleteQuery(m_database);
3382- QString deleteStatement = QLatin1String("DELETE FROM history;");
3383- deleteQuery.prepare(deleteStatement);
3384- deleteQuery.exec();
3385-
3386- QSqlQuery deleteHiddenQuery(m_database);
3387- deleteStatement = QLatin1String("DELETE FROM history_hidden;");
3388- deleteHiddenQuery.prepare(deleteStatement);
3389- deleteHiddenQuery.exec();
3390+ Q_EMIT m_dbWorker->enqueue(DbWorker::Clear, QVariantList() << QStringLiteral("history"));
3391+ Q_EMIT m_dbWorker->enqueue(DbWorker::Clear, QVariantList() << QStringLiteral("history_hidden"));
3392 }
3393
3394 /*!
3395@@ -519,7 +446,7 @@
3396 return;
3397 }
3398
3399- m_hiddenEntries.append(url);
3400+ m_hiddenEntries.insert(url);
3401
3402 QVector<int> roles;
3403 roles << Hidden;
3404@@ -547,7 +474,7 @@
3405 return;
3406 }
3407
3408- m_hiddenEntries.removeAll(url);
3409+ m_hiddenEntries.remove(url);
3410
3411 QVector<int> roles;
3412 roles << Hidden;
3413@@ -577,3 +504,175 @@
3414 }
3415 return item;
3416 }
3417+
3418+DbWorker::DbWorker()
3419+ : QObject()
3420+ , m_flush(nullptr)
3421+{
3422+ // Ensure all database operations are performed on the same thread
3423+ connect(this, SIGNAL(resetDatabase(const QString&)),
3424+ SLOT(doResetDatabase(const QString&)), Qt::QueuedConnection);
3425+ connect(this, SIGNAL(fetchEntries()),
3426+ SLOT(doFetchEntries()), Qt::QueuedConnection);
3427+ qRegisterMetaType<Operation>("Operation");
3428+ connect(this, SIGNAL(enqueue(Operation, QVariantList)),
3429+ SLOT(doEnqueue(Operation, QVariantList)), Qt::QueuedConnection);
3430+}
3431+
3432+DbWorker::~DbWorker()
3433+{
3434+ if (m_flush) {
3435+ m_flush->stop();
3436+ delete m_flush;
3437+ m_flush = nullptr;
3438+ }
3439+ doFlush();
3440+ if (m_database.isOpen()) {
3441+ m_database.close();
3442+ }
3443+ m_database = QSqlDatabase();
3444+ QSqlDatabase::removeDatabase(CONNECTION_NAME);
3445+}
3446+
3447+void DbWorker::doResetDatabase(const QString& databaseName)
3448+{
3449+ if (m_flush) {
3450+ m_flush->stop();
3451+ delete m_flush;
3452+ m_flush = nullptr;
3453+ }
3454+ doFlush();
3455+ if (m_database.isOpen()) {
3456+ m_database.close();
3457+ }
3458+ if (!m_database.isValid()) {
3459+ m_database = QSqlDatabase::addDatabase(SQL_DRIVER, CONNECTION_NAME);
3460+ }
3461+ m_database.setDatabaseName(databaseName);
3462+ m_database.open();
3463+ doCreateOrAlterDatabaseSchema();
3464+}
3465+
3466+void DbWorker::doCreateOrAlterDatabaseSchema()
3467+{
3468+ QSqlQuery createQuery(m_database);
3469+ QString query = QStringLiteral("CREATE TABLE IF NOT EXISTS history "
3470+ "(url VARCHAR, domain VARCHAR, title VARCHAR,"
3471+ " icon VARCHAR, visits INTEGER, lastVisit DATETIME);");
3472+ createQuery.prepare(query);
3473+ createQuery.exec();
3474+
3475+ // The first version of the database schema didn't have a 'domain' column
3476+ QSqlQuery tableInfoQuery(m_database);
3477+ query = QStringLiteral("PRAGMA TABLE_INFO(history);");
3478+ tableInfoQuery.prepare(query);
3479+ tableInfoQuery.exec();
3480+ while (tableInfoQuery.next()) {
3481+ if (tableInfoQuery.value(QStringLiteral("name")).toString() == QStringLiteral("domain")) {
3482+ break;
3483+ }
3484+ }
3485+ if (!tableInfoQuery.isValid()) {
3486+ QSqlQuery addDomainColumnQuery(m_database);
3487+ query = QStringLiteral("ALTER TABLE history ADD COLUMN domain VARCHAR;");
3488+ addDomainColumnQuery.prepare(query);
3489+ addDomainColumnQuery.exec();
3490+ // Updating all the entries in the database to add the domain is a
3491+ // costly operation that would slow down the application startup,
3492+ // do not do it here.
3493+ }
3494+
3495+ QSqlQuery createHiddenQuery(m_database);
3496+ query = QStringLiteral("CREATE TABLE IF NOT EXISTS history_hidden (url VARCHAR);");
3497+ createHiddenQuery.prepare(query);
3498+ createHiddenQuery.exec();
3499+}
3500+
3501+void DbWorker::doFetchEntries()
3502+{
3503+ QSqlQuery populateHiddenQuery(m_database);
3504+ QString query = QStringLiteral("SELECT url FROM history_hidden;");
3505+ populateHiddenQuery.prepare(query);
3506+ populateHiddenQuery.exec();
3507+ while (populateHiddenQuery.next()) {
3508+ Q_EMIT hiddenEntryFetched(populateHiddenQuery.value(0).toUrl());
3509+ }
3510+
3511+ QSqlQuery populateQuery(m_database);
3512+ query = QStringLiteral("SELECT url, domain, title, icon, visits, lastVisit "
3513+ "FROM history ORDER BY lastVisit DESC;");
3514+ populateQuery.prepare(query);
3515+ populateQuery.exec();
3516+ while (populateQuery.next()) {
3517+ Q_EMIT entryFetched(populateQuery.value(0).toUrl(),
3518+ populateQuery.value(1).toString(),
3519+ populateQuery.value(2).toString(),
3520+ populateQuery.value(3).toUrl(),
3521+ populateQuery.value(4).toInt(),
3522+ QDateTime::fromTime_t(populateQuery.value(5).toInt()));
3523+ }
3524+ Q_EMIT loaded();
3525+}
3526+
3527+void DbWorker::doEnqueue(DbWorker::Operation operation, QVariantList values)
3528+{
3529+ if (!m_flush) {
3530+ m_flush = new QTimer;
3531+ m_flush->setInterval(1000);
3532+ m_flush->setSingleShot(true);
3533+ connect(m_flush, SIGNAL(timeout()), SLOT(doFlush()));
3534+ }
3535+ QWriteLocker locker(&m_lock);
3536+ m_pending.enqueue(qMakePair(operation, values));
3537+ m_flush->start();
3538+}
3539+
3540+void DbWorker::doFlush()
3541+{
3542+ QWriteLocker locker(&m_lock);
3543+ while (!m_pending.isEmpty()) {
3544+ QPair<Operation, QVariantList> args = m_pending.dequeue();
3545+ QString statement;
3546+ switch (args.first) {
3547+ case InsertNewEntry:
3548+ statement = QStringLiteral("INSERT INTO history (url, domain, title, icon, "
3549+ "visits, lastVisit) VALUES (?, ?, ?, ?, 1, ?);");
3550+ break;
3551+ case InsertNewHiddenEntry:
3552+ statement = QStringLiteral("INSERT INTO history_hidden (url) VALUES (?);");
3553+ break;
3554+ case UpdateExistingEntry:
3555+ statement = QStringLiteral("UPDATE history SET domain=?, title=?, icon=?, "
3556+ "visits=?, lastVisit=? WHERE url=?;");
3557+ break;
3558+ case RemoveEntryByUrl:
3559+ statement = QStringLiteral("DELETE FROM history WHERE url=?;");
3560+ break;
3561+ case RemoveHiddenEntryByUrl:
3562+ statement = QStringLiteral("DELETE FROM history_hidden WHERE url=?;");
3563+ break;
3564+ case RemoveEntriesByDate:
3565+ statement = QStringLiteral("DELETE FROM history WHERE lastVisit BETWEEN ? AND ?;");
3566+ break;
3567+ case RemoveEntriesByDomain:
3568+ statement = QStringLiteral("DELETE FROM history WHERE domain=?;");
3569+ break;
3570+ case Clear:
3571+ statement = QStringLiteral("DELETE FROM %1;").arg(args.second.takeFirst().toString());
3572+ break;
3573+ default:
3574+ Q_UNREACHABLE();
3575+ }
3576+ if (statement.isEmpty()) {
3577+ return;
3578+ }
3579+ QSqlQuery query(m_database);
3580+ if (!query.prepare(statement)) {
3581+ continue;
3582+ }
3583+ Q_FOREACH(const QVariant& value, args.second) {
3584+ query.addBindValue(value);
3585+ }
3586+ query.exec();
3587+ }
3588+}
3589
3590=== modified file 'src/app/webbrowser/history-model.h'
3591--- src/app/webbrowser/history-model.h 2016-02-26 12:26:20 +0000
3592+++ src/app/webbrowser/history-model.h 2016-10-23 20:25:22 +0000
3593@@ -23,11 +23,20 @@
3594 #include <QtCore/QAbstractListModel>
3595 #include <QtCore/QDateTime>
3596 #include <QtCore/QList>
3597-#include <QtCore/QMutex>
3598+#include <QtCore/QPair>
3599+#include <QtCore/QQueue>
3600+#include <QtCore/QReadWriteLock>
3601+#include <QtCore/QSet>
3602 #include <QtCore/QString>
3603+#include <QtCore/QThread>
3604 #include <QtCore/QUrl>
3605+#include <QtCore/QVariant>
3606 #include <QtSql/QSqlDatabase>
3607
3608+class QTimer;
3609+
3610+class DbWorker;
3611+
3612 class HistoryModel : public QAbstractListModel
3613 {
3614 Q_OBJECT
3615@@ -74,6 +83,7 @@
3616 Q_SIGNALS:
3617 void databasePathChanged() const;
3618 void rowCountChanged();
3619+ void loaded() const;
3620
3621 protected:
3622 struct HistoryEntry {
3623@@ -89,15 +99,16 @@
3624 int getEntryIndex(const QUrl& url) const;
3625 void updateExistingEntryInDatabase(const HistoryEntry& entry);
3626
3627+private Q_SLOTS:
3628+ void onHiddenEntryFetched(const QUrl& url);
3629+ void onEntryFetched(const QUrl& url, const QString& domain, const QString& title,
3630+ const QUrl& icon, int visits, const QDateTime& lastVisit);
3631+
3632 private:
3633- QMutex m_dbMutex;
3634- QSqlDatabase m_database;
3635-
3636- QList<QUrl> m_hiddenEntries;
3637+ QString m_databasePath;
3638+ QSet<QUrl> m_hiddenEntries;
3639
3640 void resetDatabase(const QString& databaseName);
3641- void createOrAlterDatabaseSchema();
3642- void populateFromDatabase();
3643 void removeByIndex(int index);
3644 void insertNewEntryInDatabase(const HistoryEntry& entry);
3645 void insertNewEntryInHiddenDatabase(const QUrl& url);
3646@@ -106,6 +117,52 @@
3647 void removeEntriesFromDatabaseByDate(const QDate& date);
3648 void removeEntriesFromDatabaseByDomain(const QString& domain);
3649 void clearDatabase();
3650+
3651+ QThread m_dbWorkerThread;
3652+ DbWorker* m_dbWorker;
3653+};
3654+
3655+class DbWorker : public QObject {
3656+ Q_OBJECT
3657+
3658+ Q_ENUMS(Operation)
3659+
3660+public:
3661+ DbWorker();
3662+ ~DbWorker();
3663+
3664+ enum Operation {
3665+ InsertNewEntry,
3666+ InsertNewHiddenEntry,
3667+ UpdateExistingEntry,
3668+ RemoveEntryByUrl,
3669+ RemoveHiddenEntryByUrl,
3670+ RemoveEntriesByDate,
3671+ RemoveEntriesByDomain,
3672+ Clear,
3673+ };
3674+
3675+Q_SIGNALS:
3676+ void resetDatabase(const QString& databaseName);
3677+ void fetchEntries();
3678+ void hiddenEntryFetched(const QUrl& url);
3679+ void entryFetched(const QUrl& url, const QString& domain, const QString& title,
3680+ const QUrl& icon, int visits, const QDateTime& lastVisit);
3681+ void loaded();
3682+ void enqueue(Operation operation, QVariantList values);
3683+
3684+private Q_SLOTS:
3685+ void doResetDatabase(const QString& databaseName);
3686+ void doCreateOrAlterDatabaseSchema();
3687+ void doFetchEntries();
3688+ void doEnqueue(Operation operation, QVariantList values);
3689+ void doFlush();
3690+
3691+private:
3692+ QSqlDatabase m_database;
3693+ QReadWriteLock m_lock;
3694+ QQueue<QPair<Operation, QVariantList>> m_pending;
3695+ QTimer* m_flush;
3696 };
3697
3698 #endif // __HISTORY_MODEL_H__
3699
3700=== modified file 'src/app/webbrowser/webbrowser-app.qml'
3701--- src/app/webbrowser/webbrowser-app.qml 2016-09-20 19:55:22 +0000
3702+++ src/app/webbrowser/webbrowser-app.qml 2016-10-23 20:25:22 +0000
3703@@ -53,15 +53,6 @@
3704 BookmarksModel.databasePath = dataLocation + "/bookmarks.sqlite"
3705 HistoryModel.databasePath = dataLocation + "/history.sqlite"
3706 DownloadsModel.databasePath = dataLocation + "/downloads.sqlite"
3707-
3708- var doNotCleanUrls = []
3709- for (var x in allWindows) {
3710- var tabs = allWindows[x].tabsModel
3711- for (var t = 0; t < tabs.count; ++t) {
3712- doNotCleanUrls.push(tabs.get(t).url)
3713- }
3714- }
3715- PreviewManager.cleanUnusedPreviews(doNotCleanUrls)
3716 }
3717
3718 // Array of all windows, sorted chronologically (most recently active last)
3719@@ -128,6 +119,20 @@
3720 session.clear()
3721 }
3722 }
3723+ if (incognito && (allWindows.length > 1)) {
3724+ // If the last incognito window is being closed,
3725+ // prune incognito entries from the downloads model
3726+ var incognitoWindows = 0
3727+ for (var w in allWindows) {
3728+ var window = allWindows[w]
3729+ if ((window !== this) && window.incognito) {
3730+ ++incognitoWindows
3731+ }
3732+ }
3733+ if (incognitoWindows == 0) {
3734+ DownloadsModel.pruneIncognitoDownloads()
3735+ }
3736+ }
3737 destroy()
3738 }
3739
3740@@ -446,4 +451,18 @@
3741 }
3742 }
3743 }
3744+
3745+ property var historyModelMonitor: Connections {
3746+ target: HistoryModel
3747+ onLoaded: {
3748+ var doNotCleanUrls = []
3749+ for (var x in allWindows) {
3750+ var tabs = allWindows[x].tabsModel
3751+ for (var t = 0; t < tabs.count; ++t) {
3752+ doNotCleanUrls.push(tabs.get(t).url)
3753+ }
3754+ }
3755+ PreviewManager.cleanUnusedPreviews(doNotCleanUrls)
3756+ }
3757+ }
3758 }
3759
3760=== modified file 'src/app/webcontainer/Chrome.qml'
3761--- src/app/webcontainer/Chrome.qml 2016-05-26 17:07:44 +0000
3762+++ src/app/webcontainer/Chrome.qml 2016-10-23 20:25:22 +0000
3763@@ -23,9 +23,13 @@
3764 ChromeBase {
3765 id: chrome
3766
3767+ property var webview: null
3768 property bool navigationButtonsVisible: false
3769 property bool accountSwitcher: false
3770
3771+ loading: webview && webview.loading
3772+ loadProgress: loading ? webview.loadProgress : 0
3773+
3774 function updateChromeElementsColor(color) {
3775 chromeTextLabel.color = color
3776
3777
3778=== modified file 'src/app/webcontainer/WebApp.qml'
3779--- src/app/webcontainer/WebApp.qml 2016-09-20 15:32:49 +0000
3780+++ src/app/webcontainer/WebApp.qml 2016-10-23 20:25:22 +0000
3781@@ -249,7 +249,8 @@
3782 id: progressbarComponent
3783
3784 ThinProgressBar {
3785- webview: webapp.currentWebview
3786+ visible: webapp.currentWebview && webapp.currentWebview.loading
3787+ value: visible ? webapp.currentWebview.loadProgress : 0
3788
3789 anchors {
3790 left: parent.left
3791
3792=== modified file 'tests/autopilot/webapp_container/tests/__init__.py'
3793--- tests/autopilot/webapp_container/tests/__init__.py 2016-07-13 16:23:18 +0000
3794+++ tests/autopilot/webapp_container/tests/__init__.py 2016-10-23 20:25:22 +0000
3795@@ -52,7 +52,8 @@
3796 return LOCAL_BROWSER_CONTAINER_PATH_NAME
3797 return INSTALLED_BROWSER_CONTAINER_PATH_NAME
3798
3799- def launch_webcontainer_app(self, args, envvars={}, is_local_app=False):
3800+ def launch_webcontainer_app(self, args, envvars={}, is_local_app=False,
3801+ ignore_focus=False):
3802 if model() != 'Desktop':
3803 args.append(
3804 '--desktop_file_hint=/usr/share/applications/'
3805@@ -74,7 +75,7 @@
3806 except:
3807 self.app = None
3808
3809- if not is_local_app:
3810+ if not is_local_app and not ignore_focus:
3811 webview = self.get_oxide_webview()
3812 self.assertThat(
3813 lambda: webview.activeFocus,
3814@@ -128,10 +129,12 @@
3815 'schemeUriHandleFilterResult(QString)')[-1][0]
3816 return result
3817
3818- def browse_to(self, url):
3819+ def browse_to(self, url, wait_for_load=True):
3820 webview = self.get_oxide_webview()
3821 webview.slots.navigateToUrl(url)
3822- self.assert_page_eventually_loaded(url)
3823+
3824+ if wait_for_load:
3825+ self.assert_page_eventually_loaded(url)
3826
3827 def kill_app(self, signal=signal.SIGKILL):
3828 os.kill(self.app.pid, signal)
3829@@ -161,9 +164,9 @@
3830 return self.base_url[len(self.BASE_URL_SCHEME):]
3831
3832 def launch_webcontainer_app_with_local_http_server(
3833- self, args, path='/', envvars={}, homepage=''):
3834+ self, args, path='/', envvars={}, homepage='', ignore_focus=False):
3835 self.url = self.base_url + path
3836 if len(homepage) != 0:
3837 self.url = homepage
3838 args.append(self.url)
3839- self.launch_webcontainer_app(args, envvars)
3840+ self.launch_webcontainer_app(args, envvars, ignore_focus=ignore_focus)
3841
3842=== modified file 'tests/autopilot/webapp_container/tests/fake_servers.py'
3843--- tests/autopilot/webapp_container/tests/fake_servers.py 2016-05-26 17:07:44 +0000
3844+++ tests/autopilot/webapp_container/tests/fake_servers.py 2016-10-23 20:25:22 +0000
3845@@ -391,6 +391,40 @@
3846 color_url = qs['color_url_part'][0]
3847 self.serve_content(
3848 self.local_browse_link_chain_content(next, color_url))
3849+ elif self.path == "/js-alert-dialog":
3850+ self.send_response(200)
3851+ html = '<html><body><script type="text/javascript">'
3852+ html += 'window.onload = function() {'
3853+ html += ' window.alert("Alert Dialog")'
3854+ html += '} </script></body></html>'
3855+ self.serve_content(html)
3856+ elif self.path == "/js-before-unload-dialog":
3857+ self.send_response(200)
3858+ html = '<html><body><script type="text/javascript">'
3859+ html += 'window.onbeforeunload = function(e) {'
3860+ html += ' var dialogText = "Dialog text here";'
3861+ html += ' e.returnValue = dialogText;'
3862+ html += ' return dialogText;'
3863+ html += '}; </script></body></html>'
3864+ self.serve_content(html)
3865+ elif self.path == "/js-confirm-dialog":
3866+ self.send_response(200)
3867+ html = '<html><body><script type="text/javascript">'
3868+ html += 'window.onload = function() {'
3869+ html += ' if (window.confirm("Confirm Dialog") == true) {'
3870+ html += ' document.title = "OK" } '
3871+ html += ' else { document.title = "CANCEL" }'
3872+ html += '} </script></body></html>'
3873+ self.serve_content(html)
3874+ elif self.path == "/js-prompt-dialog":
3875+ self.send_response(200)
3876+ html = '<html><body><script type="text/javascript">'
3877+ html += 'window.onload = function() {'
3878+ html += ' var result = window.prompt("Prompt Dialog", "Default");'
3879+ html += ' if (result != null) { document.title = result; } '
3880+ html += ' else { document.title = "CANCEL" }'
3881+ html += '} </script></body></html>'
3882+ self.serve_content(html)
3883 else:
3884 self.send_error(404)
3885
3886
3887=== added file 'tests/autopilot/webapp_container/tests/test_js_dialogs.py'
3888--- tests/autopilot/webapp_container/tests/test_js_dialogs.py 1970-01-01 00:00:00 +0000
3889+++ tests/autopilot/webapp_container/tests/test_js_dialogs.py 2016-10-23 20:25:22 +0000
3890@@ -0,0 +1,216 @@
3891+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
3892+#
3893+# Copyright 2016 Canonical
3894+#
3895+# This program is free software: you can redistribute it and/or modify it
3896+# under the terms of the GNU General Public License version 3, as published
3897+# by the Free Software Foundation.
3898+#
3899+# This program is distributed in the hope that it will be useful,
3900+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3901+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3902+# GNU General Public License for more details.
3903+#
3904+# You should have received a copy of the GNU General Public License
3905+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3906+
3907+from webapp_container.tests import WebappContainerTestCaseWithLocalContentBase
3908+
3909+from testtools.matchers import Equals
3910+from autopilot.matchers import Eventually
3911+
3912+
3913+class DialogWrapper(object):
3914+ def __init__(self, dialog):
3915+ self.dialog = dialog
3916+
3917+ self.text = self.dialog.text
3918+ self.wait_until_destroyed = self.dialog.wait_until_destroyed
3919+ self.visible = self.dialog.visible
3920+
3921+
3922+class AlertDialog(DialogWrapper):
3923+ def get_ok_button(self):
3924+ return self.dialog.select_single("Button", objectName="okButton")
3925+
3926+
3927+class BeforeUnloadDialog(DialogWrapper):
3928+ def get_leave_button(self):
3929+ return self.dialog.select_single("Button", objectName="leaveButton")
3930+
3931+ def get_stay_button(self):
3932+ return self.dialog.select_single("Button", objectName="stayButton")
3933+
3934+
3935+class ConfirmDialog(DialogWrapper):
3936+ def get_cancel_button(self):
3937+ return self.dialog.select_single("Button", objectName="cancelButton")
3938+
3939+ def get_ok_button(self):
3940+ return self.dialog.select_single("Button", objectName="okButton")
3941+
3942+
3943+class PromptDialog(DialogWrapper):
3944+ def get_cancel_button(self):
3945+ return self.dialog.select_single("Button", objectName="cancelButton")
3946+
3947+ def get_input_textfield(self):
3948+ return self.dialog.select_single("TextField",
3949+ objectName="inputTextField")
3950+
3951+ def get_ok_button(self):
3952+ return self.dialog.select_single("Button", objectName="okButton")
3953+
3954+
3955+class TestJSDialogs(WebappContainerTestCaseWithLocalContentBase):
3956+
3957+ def test_alert(self):
3958+ self.launch_webcontainer_app_with_local_http_server(
3959+ [], '/js-alert-dialog', ignore_focus=True)
3960+
3961+ dialog = AlertDialog(
3962+ self.app.wait_select_single("Dialog", objectName="alertDialog")
3963+ )
3964+ dialog.visible.wait_for(True)
3965+
3966+ # Check alert text is correct
3967+ self.assertThat(dialog.text, Equals("Alert Dialog"))
3968+
3969+ # Click OK, check dialog is destroyed
3970+ self.pointing_device.click_object(dialog.get_ok_button())
3971+ dialog.wait_until_destroyed()
3972+
3973+ def test_before_unload_leave(self):
3974+ testUrl = self.base_url + "/"
3975+
3976+ self.launch_webcontainer_app_with_local_http_server(
3977+ [], '/js-before-unload-dialog', ignore_focus=True)
3978+
3979+ # Change the url to trigger window.onBeforeUnload
3980+ self.browse_to(testUrl, wait_for_load=False)
3981+
3982+ dialog = BeforeUnloadDialog(
3983+ self.app.wait_select_single("Dialog",
3984+ objectName="beforeUnloadDialog")
3985+ )
3986+ dialog.visible.wait_for(True)
3987+
3988+ # Click leave, wait for dialog to close and check that url changes
3989+ self.pointing_device.click_object(dialog.get_leave_button())
3990+ dialog.wait_until_destroyed()
3991+
3992+ self.assertThat(self.get_oxide_webview().url,
3993+ Eventually(Equals(testUrl)))
3994+
3995+ def test_before_unload_stay(self):
3996+ page = '/js-before-unload-dialog'
3997+ beforeUnloadUrl = self.base_url + page
3998+ testUrl = self.base_url + "/"
3999+
4000+ self.launch_webcontainer_app_with_local_http_server(
4001+ [], page, ignore_focus=True)
4002+
4003+ # Change the url to trigger window.onBeforeUnload
4004+ self.browse_to(testUrl, wait_for_load=False)
4005+
4006+ dialog = BeforeUnloadDialog(
4007+ self.app.wait_select_single("Dialog",
4008+ objectName="beforeUnloadDialog")
4009+ )
4010+ dialog.visible.wait_for(True)
4011+
4012+ # Click stay, wait for dialog to close and check url does not change
4013+ self.pointing_device.click_object(dialog.get_stay_button())
4014+ dialog.wait_until_destroyed()
4015+
4016+ self.assertThat(self.get_oxide_webview().url,
4017+ Eventually(Equals(beforeUnloadUrl)))
4018+
4019+ def test_confirm_cancel(self):
4020+ self.launch_webcontainer_app_with_local_http_server(
4021+ [], '/js-confirm-dialog', ignore_focus=True)
4022+
4023+ dialog = ConfirmDialog(
4024+ self.app.wait_select_single("Dialog", objectName="confirmDialog")
4025+ )
4026+ dialog.visible.wait_for(True)
4027+
4028+ # Check that confirm text is correct
4029+ self.assertThat(dialog.text, Equals("Confirm Dialog"))
4030+
4031+ # Click cancel and check that dialog is destroyed
4032+ self.pointing_device.click_object(dialog.get_cancel_button())
4033+ dialog.wait_until_destroyed()
4034+
4035+ # Check that title changes to cancel
4036+ self.assertThat(self.get_webcontainer_webview().title,
4037+ Eventually(Equals("CANCEL")))
4038+
4039+ def test_confirm_ok(self):
4040+ self.launch_webcontainer_app_with_local_http_server(
4041+ [], '/js-confirm-dialog', ignore_focus=True)
4042+
4043+ dialog = ConfirmDialog(
4044+ self.app.wait_select_single("Dialog", objectName="confirmDialog")
4045+ )
4046+ dialog.visible.wait_for(True)
4047+
4048+ # Check that confirm text is correct
4049+ self.assertThat(dialog.text, Equals("Confirm Dialog"))
4050+
4051+ # Click OK and check that dialog is destroyed
4052+ self.pointing_device.click_object(dialog.get_ok_button())
4053+ dialog.wait_until_destroyed()
4054+
4055+ # Check that title changes to OK
4056+ self.assertThat(self.get_webcontainer_webview().title,
4057+ Eventually(Equals("OK")))
4058+
4059+ def test_prompt_cancel(self):
4060+ self.launch_webcontainer_app_with_local_http_server(
4061+ [], '/js-prompt-dialog', ignore_focus=True)
4062+
4063+ dialog = PromptDialog(
4064+ self.app.wait_select_single("Dialog", objectName="promptDialog")
4065+ )
4066+ dialog.visible.wait_for(True)
4067+
4068+ # Check that prompt text is correct and default textfield
4069+ self.assertThat(dialog.text, Equals("Prompt Dialog"))
4070+ self.assertThat(dialog.get_input_textfield().text,
4071+ Equals("Default"))
4072+
4073+ # Click cancel and check that dialog is destroyed
4074+ self.pointing_device.click_object(dialog.get_cancel_button())
4075+ dialog.wait_until_destroyed()
4076+
4077+ # Check that title changes to cancel
4078+ self.assertThat(self.get_webcontainer_webview().title,
4079+ Eventually(Equals("CANCEL")))
4080+
4081+ def test_prompt_ok(self):
4082+ self.launch_webcontainer_app_with_local_http_server(
4083+ [], '/js-prompt-dialog', ignore_focus=True)
4084+
4085+ dialog = PromptDialog(
4086+ self.app.wait_select_single("Dialog", objectName="promptDialog")
4087+ )
4088+ dialog.visible.wait_for(True)
4089+
4090+ # Check that prompt text is correct and default textfield
4091+ self.assertThat(dialog.text, Equals("Prompt Dialog"))
4092+ self.assertThat(dialog.get_input_textfield().text,
4093+ Equals("Default"))
4094+
4095+ # Enter text into textfield
4096+ text = "TEST"
4097+ entry = dialog.get_input_textfield()
4098+ entry.write(text)
4099+
4100+ # Click ok and check that dialog is destroyed
4101+ self.pointing_device.click_object(dialog.get_ok_button())
4102+ dialog.wait_until_destroyed()
4103+
4104+ # Check that title changes to text entered in textfield
4105+ self.assertThat(self.get_webcontainer_webview().title,
4106+ Eventually(Equals(text)))
4107
4108=== modified file 'tests/autopilot/webbrowser_app/emulators/browser.py'
4109--- tests/autopilot/webbrowser_app/emulators/browser.py 2016-08-10 15:43:04 +0000
4110+++ tests/autopilot/webbrowser_app/emulators/browser.py 2016-10-23 20:25:22 +0000
4111@@ -179,9 +179,9 @@
4112 def get_history_view(self):
4113 try:
4114 if self.wide:
4115- return self.select_single(HistoryViewWide)
4116+ return self.wait_select_single(HistoryViewWide)
4117 else:
4118- return self.select_single(HistoryView)
4119+ return self.wait_select_single(HistoryView)
4120 except exceptions.StateNotFoundError:
4121 return None
4122
4123@@ -238,6 +238,26 @@
4124 menu.click_cancel_action()
4125 menu.wait_until_destroyed()
4126
4127+ def get_alert_dialog(self):
4128+ return AlertDialog(
4129+ self.wait_select_single("Dialog", objectName="alertDialog")
4130+ )
4131+
4132+ def get_before_unload_dialog(self):
4133+ return BeforeUnloadDialog(
4134+ self.wait_select_single("Dialog", objectName="beforeUnloadDialog")
4135+ )
4136+
4137+ def get_confirm_dialog(self):
4138+ return ConfirmDialog(
4139+ self.wait_select_single("Dialog", objectName="confirmDialog")
4140+ )
4141+
4142+ def get_prompt_dialog(self):
4143+ return PromptDialog(
4144+ self.wait_select_single("Dialog", objectName="promptDialog")
4145+ )
4146+
4147
4148 class Chrome(uitk.UbuntuUIToolkitCustomProxyObjectBase):
4149
4150@@ -724,3 +744,45 @@
4151 def click_cancel_action(self):
4152 action = self.select_single("Empty", objectName="cancelAction")
4153 self.pointing_device.click_object(action)
4154+
4155+
4156+class DialogWrapper(object):
4157+ def __init__(self, dialog):
4158+ self.dialog = dialog
4159+
4160+ self.text = self.dialog.text
4161+ self.wait_until_destroyed = self.dialog.wait_until_destroyed
4162+ self.visible = self.dialog.visible
4163+
4164+
4165+class AlertDialog(DialogWrapper):
4166+ def get_ok_button(self):
4167+ return self.dialog.select_single("Button", objectName="okButton")
4168+
4169+
4170+class BeforeUnloadDialog(DialogWrapper):
4171+ def get_leave_button(self):
4172+ return self.dialog.select_single("Button", objectName="leaveButton")
4173+
4174+ def get_stay_button(self):
4175+ return self.dialog.select_single("Button", objectName="stayButton")
4176+
4177+
4178+class ConfirmDialog(DialogWrapper):
4179+ def get_cancel_button(self):
4180+ return self.dialog.select_single("Button", objectName="cancelButton")
4181+
4182+ def get_ok_button(self):
4183+ return self.dialog.select_single("Button", objectName="okButton")
4184+
4185+
4186+class PromptDialog(DialogWrapper):
4187+ def get_cancel_button(self):
4188+ return self.dialog.select_single("Button", objectName="cancelButton")
4189+
4190+ def get_input_textfield(self):
4191+ return self.dialog.select_single("TextField",
4192+ objectName="inputTextField")
4193+
4194+ def get_ok_button(self):
4195+ return self.dialog.select_single("Button", objectName="okButton")
4196
4197=== modified file 'tests/autopilot/webbrowser_app/tests/http_server.py'
4198--- tests/autopilot/webbrowser_app/tests/http_server.py 2016-03-07 18:29:23 +0000
4199+++ tests/autopilot/webbrowser_app/tests/http_server.py 2016-10-23 20:25:22 +0000
4200@@ -252,6 +252,54 @@
4201 html += '50%; transform: translate(-50%, -50%); font-size: 500%">'
4202 html += 'Supercalifragilisticexpialidocious</div></body></html>'
4203 self.send_html(html)
4204+ elif self.path == "/redirect-no-title-header":
4205+ self.send_response(301)
4206+ self.send_header("Location", "/redirect-destination")
4207+ self.end_headers()
4208+ elif self.path == "/redirect-no-title-js":
4209+ self.send_response(200)
4210+ html = '<html><body><script type="text/javascript">'
4211+ html += 'window.location.href = "/redirect-destination"'
4212+ html += '</script></body></html>'
4213+ self.send_html(html)
4214+ elif self.path == "/redirect-destination":
4215+ self.send_response(200)
4216+ html = '<html><body><p>redirect-destination</p></body></html>'
4217+ self.send_html(html)
4218+ elif self.path == "/js-alert-dialog":
4219+ self.send_response(200)
4220+ html = '<html><body><script type="text/javascript">'
4221+ html += 'window.onload = function() {'
4222+ html += ' window.alert("Alert Dialog")'
4223+ html += '} </script></body></html>'
4224+ self.send_html(html)
4225+ elif self.path == "/js-before-unload-dialog":
4226+ self.send_response(200)
4227+ html = '<html><body><script type="text/javascript">'
4228+ html += 'window.onbeforeunload = function(e) {'
4229+ html += ' var dialogText = "Dialog text here";'
4230+ html += ' e.returnValue = dialogText;'
4231+ html += ' return dialogText;'
4232+ html += '}; </script></body></html>'
4233+ self.send_html(html)
4234+ elif self.path == "/js-confirm-dialog":
4235+ self.send_response(200)
4236+ html = '<html><body><script type="text/javascript">'
4237+ html += 'window.onload = function() {'
4238+ html += ' if (window.confirm("Confirm Dialog") == true) {'
4239+ html += ' document.title = "OK" } '
4240+ html += ' else { document.title = "CANCEL" }'
4241+ html += '} </script></body></html>'
4242+ self.send_html(html)
4243+ elif self.path == "/js-prompt-dialog":
4244+ self.send_response(200)
4245+ html = '<html><body><script type="text/javascript">'
4246+ html += 'window.onload = function() {'
4247+ html += ' var result = window.prompt("Prompt Dialog", "Default");'
4248+ html += ' if (result != null) { document.title = result; } '
4249+ html += ' else { document.title = "CANCEL" }'
4250+ html += '} </script></body></html>'
4251+ self.send_html(html)
4252 else:
4253 self.send_error(404)
4254
4255
4256=== modified file 'tests/autopilot/webbrowser_app/tests/test_history.py'
4257--- tests/autopilot/webbrowser_app/tests/test_history.py 2016-02-29 20:42:13 +0000
4258+++ tests/autopilot/webbrowser_app/tests/test_history.py 2016-10-23 20:25:22 +0000
4259@@ -124,3 +124,38 @@
4260 self.main_window.wait_until_page_loaded(pushed)
4261 self.open_history()
4262 self.expect_history_entries([pushed, url, self.url])
4263+
4264+ def test_title_correct_redirect_header(self):
4265+ # Regression test for https://launchpad.net/bugs/1603835
4266+ url_redirect = self.base_url + "/redirect-no-title-header"
4267+ url_destination = self.base_url + "/redirect-destination"
4268+ url_test = self.base_url + "/test1"
4269+
4270+ self.main_window.go_to_url(url_redirect)
4271+ self.main_window.wait_until_page_loaded(url_destination)
4272+
4273+ self.open_history()
4274+
4275+ entries = self.expect_history_entries(
4276+ [url_destination, url_test]
4277+ )
4278+ self.assertThat(entries[0].title, Equals("test/redirect-destination"))
4279+ self.assertThat(entries[1].title, Equals("test page 1"))
4280+
4281+ def test_title_correct_redirect_js(self):
4282+ # Regression test for https://launchpad.net/bugs/1603835
4283+ url_redirect = self.base_url + "/redirect-no-title-js"
4284+ url_destination = self.base_url + "/redirect-destination"
4285+ url_test = self.base_url + "/test1"
4286+
4287+ self.main_window.go_to_url(url_redirect)
4288+ self.main_window.wait_until_page_loaded(url_destination)
4289+
4290+ self.open_history()
4291+
4292+ entries = self.expect_history_entries(
4293+ [url_destination, url_redirect, url_test]
4294+ )
4295+ self.assertThat(entries[0].title, Equals("test/redirect-destination"))
4296+ self.assertThat(entries[1].title, Equals("test/redirect-no-title-js"))
4297+ self.assertThat(entries[2].title, Equals("test page 1"))
4298
4299=== added file 'tests/autopilot/webbrowser_app/tests/test_js_dialogs.py'
4300--- tests/autopilot/webbrowser_app/tests/test_js_dialogs.py 1970-01-01 00:00:00 +0000
4301+++ tests/autopilot/webbrowser_app/tests/test_js_dialogs.py 2016-10-23 20:25:22 +0000
4302@@ -0,0 +1,158 @@
4303+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
4304+#
4305+# Copyright 2016 Canonical
4306+#
4307+# This program is free software: you can redistribute it and/or modify it
4308+# under the terms of the GNU General Public License version 3, as published
4309+# by the Free Software Foundation.
4310+#
4311+# This program is distributed in the hope that it will be useful,
4312+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4313+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4314+# GNU General Public License for more details.
4315+#
4316+# You should have received a copy of the GNU General Public License
4317+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4318+
4319+from testtools.matchers import Equals
4320+from autopilot.matchers import Eventually
4321+
4322+from webbrowser_app.tests import StartOpenRemotePageTestCaseBase
4323+
4324+
4325+class TestJSDialogs(StartOpenRemotePageTestCaseBase):
4326+
4327+ def test_alert(self):
4328+ url = self.base_url + "/js-alert-dialog"
4329+ self.main_window.go_to_url(url)
4330+
4331+ dialog = self.main_window.get_alert_dialog()
4332+ dialog.visible.wait_for(True)
4333+
4334+ # Check alert text is correct
4335+ self.assertThat(dialog.text, Equals("Alert Dialog"))
4336+
4337+ # Click OK, check dialog is destroyed
4338+ self.pointing_device.click_object(dialog.get_ok_button())
4339+ dialog.wait_until_destroyed()
4340+
4341+ def test_before_unload_leave(self):
4342+ beforeUnloadUrl = self.base_url + "/js-before-unload-dialog"
4343+ testUrl = self.base_url + "/test1"
4344+
4345+ self.main_window.go_to_url(beforeUnloadUrl)
4346+ self.main_window.wait_until_page_loaded(beforeUnloadUrl)
4347+
4348+ # Change the url to trigger window.onBeforeUnload
4349+ self.main_window.go_to_url(testUrl)
4350+
4351+ dialog = self.main_window.get_before_unload_dialog()
4352+ dialog.visible.wait_for(True)
4353+
4354+ # Click leave, wait for dialog to close and check that url changes
4355+ self.pointing_device.click_object(dialog.get_leave_button())
4356+ dialog.wait_until_destroyed()
4357+
4358+ self.assertThat(self.main_window.get_current_webview().url,
4359+ Eventually(Equals(testUrl)))
4360+
4361+ def test_before_unload_stay(self):
4362+ beforeUnloadUrl = self.base_url + "/js-before-unload-dialog"
4363+ testUrl = self.base_url + "/test1"
4364+
4365+ self.main_window.go_to_url(beforeUnloadUrl)
4366+ self.main_window.wait_until_page_loaded(beforeUnloadUrl)
4367+
4368+ # Change the url to trigger window.onBeforeUnload
4369+ self.main_window.go_to_url(testUrl)
4370+
4371+ dialog = self.main_window.get_before_unload_dialog()
4372+ dialog.visible.wait_for(True)
4373+
4374+ # Click stay, wait for dialog to close and check url does not change
4375+ self.pointing_device.click_object(dialog.get_stay_button())
4376+ dialog.wait_until_destroyed()
4377+
4378+ self.assertThat(self.main_window.get_current_webview().url,
4379+ Eventually(Equals(beforeUnloadUrl)))
4380+
4381+ def test_confirm_cancel(self):
4382+ url = self.base_url + "/js-confirm-dialog"
4383+ self.main_window.go_to_url(url)
4384+
4385+ dialog = self.main_window.get_confirm_dialog()
4386+ dialog.visible.wait_for(True)
4387+
4388+ # Check that confirm text is correct
4389+ self.assertThat(dialog.text, Equals("Confirm Dialog"))
4390+
4391+ # Click cancel and check that dialog is destroyed
4392+ self.pointing_device.click_object(dialog.get_cancel_button())
4393+ dialog.wait_until_destroyed()
4394+
4395+ # Check that title changes to cancel
4396+ self.assertThat(self.main_window.get_current_webview().title,
4397+ Eventually(Equals("CANCEL")))
4398+
4399+ def test_confirm_ok(self):
4400+ url = self.base_url + "/js-confirm-dialog"
4401+ self.main_window.go_to_url(url)
4402+
4403+ dialog = self.main_window.get_confirm_dialog()
4404+ dialog.visible.wait_for(True)
4405+
4406+ # Check that confirm text is correct
4407+ self.assertThat(dialog.text, Equals("Confirm Dialog"))
4408+
4409+ # Click OK and check that dialog is destroyed
4410+ self.pointing_device.click_object(dialog.get_ok_button())
4411+ dialog.wait_until_destroyed()
4412+
4413+ # Check that title changes to OK
4414+ self.assertThat(self.main_window.get_current_webview().title,
4415+ Eventually(Equals("OK")))
4416+
4417+ def test_prompt_cancel(self):
4418+ url = self.base_url + "/js-prompt-dialog"
4419+ self.main_window.go_to_url(url)
4420+
4421+ dialog = self.main_window.get_prompt_dialog()
4422+ dialog.visible.wait_for(True)
4423+
4424+ # Check that prompt text is correct and default textfield
4425+ self.assertThat(dialog.text, Equals("Prompt Dialog"))
4426+ self.assertThat(dialog.get_input_textfield().text,
4427+ Equals("Default"))
4428+
4429+ # Click cancel and check that dialog is destroyed
4430+ self.pointing_device.click_object(dialog.get_cancel_button())
4431+ dialog.wait_until_destroyed()
4432+
4433+ # Check that title changes to cancel
4434+ self.assertThat(self.main_window.get_current_webview().title,
4435+ Eventually(Equals("CANCEL")))
4436+
4437+ def test_prompt_ok(self):
4438+ url = self.base_url + "/js-prompt-dialog"
4439+ self.main_window.go_to_url(url)
4440+
4441+ dialog = self.main_window.get_prompt_dialog()
4442+ dialog.visible.wait_for(True)
4443+
4444+ # Check that prompt text is correct and default textfield
4445+ self.assertThat(dialog.text, Equals("Prompt Dialog"))
4446+ self.assertThat(dialog.get_input_textfield().text,
4447+ Equals("Default"))
4448+
4449+ # Enter text into textfield
4450+ text = "TEST"
4451+ entry = dialog.get_input_textfield()
4452+ entry.write(text)
4453+
4454+ # Click ok and check that dialog is destroyed
4455+ self.pointing_device.click_object(dialog.get_ok_button())
4456+ dialog.wait_until_destroyed()
4457+
4458+ # Check that title changes to text entered in textfield
4459+ self.assertThat(self.main_window.get_current_webview().title,
4460+ Eventually(Equals(text)))
4461
4462=== modified file 'tests/autopilot/webbrowser_app/tests/test_new_tab_view.py'
4463--- tests/autopilot/webbrowser_app/tests/test_new_tab_view.py 2016-08-10 15:43:04 +0000
4464+++ tests/autopilot/webbrowser_app/tests/test_new_tab_view.py 2016-10-23 20:25:22 +0000
4465@@ -440,14 +440,7 @@
4466 folder = folders[0]
4467 folder_cx = folder.globalRect.x + folder.width / 2
4468 folder_cy = folder.globalRect.y + folder.height / 2
4469- # Work around https://launchpad.net/bugs/1499437 by dragging downwards
4470- # a little bit first, then to the target folder.
4471- self.pointing_device.move_to_object(grip)
4472- pos = self.pointing_device.position()
4473- self.pointing_device.press()
4474- self.pointing_device.move(pos[0], pos[1] + 20)
4475- self.pointing_device.move(folder_cx, folder_cy)
4476- self.pointing_device.release()
4477+ self.pointing_device.drag(rect.x, rect.y, folder_cx, folder_cy)
4478 self.assertThat(grip.globalRect, Eventually(Equals(rect)))
4479
4480 # Test that dragging an item to another folder removes it from this one
4481
4482=== modified file 'tests/unittests/downloads-model/tst_DownloadsModelTests.cpp'
4483--- tests/unittests/downloads-model/tst_DownloadsModelTests.cpp 2016-01-12 10:37:15 +0000
4484+++ tests/unittests/downloads-model/tst_DownloadsModelTests.cpp 2016-10-23 20:25:22 +0000
4485@@ -17,6 +17,8 @@
4486 */
4487
4488 #include <QtCore/QDir>
4489+#include <QtCore/QFileInfo>
4490+#include <QtCore/QTemporaryDir>
4491 #include <QtCore/QTemporaryFile>
4492 #include <QtTest/QSignalSpy>
4493 #include <QtTest/QtTest>
4494@@ -27,11 +29,16 @@
4495 Q_OBJECT
4496
4497 private:
4498+ QTemporaryDir homeDir;
4499 DownloadsModel* model;
4500
4501 private Q_SLOTS:
4502 void init()
4503 {
4504+ // QStandardPaths::setTestModeEnabled() doesn't affect
4505+ // QStandardPaths::DownloadLocation, so we must override $HOME to
4506+ // ensure the test won't write data to the user's home directory.
4507+ qputenv("HOME", homeDir.path().toUtf8());
4508 model = new DownloadsModel;
4509 model->setDatabasePath(":memory:");
4510 }
4511@@ -39,6 +46,7 @@
4512 void cleanup()
4513 {
4514 delete model;
4515+ qunsetenv("HOME");
4516 }
4517
4518 void shouldBeInitiallyEmpty()
4519@@ -58,90 +66,177 @@
4520 QVERIFY(roleNames.contains("paused"));
4521 QVERIFY(roleNames.contains("error"));
4522 QVERIFY(roleNames.contains("created"));
4523+ QVERIFY(roleNames.contains("incognito"));
4524 }
4525
4526 void shouldContainAddedEntries()
4527 {
4528 QVERIFY(!model->contains(QStringLiteral("testid")));
4529- model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/html"));
4530+ model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/html"), false);
4531 QVERIFY(model->contains(QStringLiteral("testid")));
4532 }
4533
4534 void shouldAddNewEntries()
4535 {
4536- QSignalSpy spy(model, SIGNAL(added(QString, QUrl, QString)));
4537+ QSignalSpy spy(model, SIGNAL(rowsInserted(const QModelIndex&, int, int)));
4538
4539- model->add("testid", QUrl("http://example.org/"), "text/plain");
4540+ model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
4541 QCOMPARE(model->rowCount(), 1);
4542 QCOMPARE(spy.count(), 1);
4543 QVariantList args = spy.takeFirst();
4544- QCOMPARE(args.at(0).toString(), QString("testid"));
4545- QCOMPARE(args.at(1).toUrl(), QUrl("http://example.org/"));
4546- QCOMPARE(args.at(2).toString(), QString("text/plain"));
4547+ QCOMPARE(args.at(0).toInt(), 0);
4548+ QCOMPARE(args.at(1).toInt(), 0);
4549+ QCOMPARE(model->data(model->index(0), DownloadsModel::DownloadId).toString(), QStringLiteral("testid"));
4550+ QCOMPARE(model->data(model->index(0), DownloadsModel::Url).toUrl(), QUrl(QStringLiteral("http://example.org/")));
4551+ QCOMPARE(model->data(model->index(0), DownloadsModel::Mimetype).toString(), QStringLiteral("text/plain"));
4552
4553- model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");
4554+ model->add(QStringLiteral("testid2"), QUrl(QStringLiteral("http://example.org/pdf")), QStringLiteral("application/pdf"), false);
4555 QCOMPARE(model->rowCount(), 2);
4556 QCOMPARE(spy.count(), 1);
4557 args = spy.takeFirst();
4558- QCOMPARE(args.at(0).toString(), QString("testid2"));
4559- QCOMPARE(args.at(1).toUrl(), QUrl("http://example.org/pdf"));
4560- QCOMPARE(args.at(2).toString(), QString("application/pdf"));
4561+ QCOMPARE(args.at(0).toInt(), 0);
4562+ QCOMPARE(args.at(1).toInt(), 0);
4563+ QCOMPARE(model->data(model->index(0), DownloadsModel::DownloadId).toString(), QStringLiteral("testid2"));
4564+ QCOMPARE(model->data(model->index(0), DownloadsModel::Url).toUrl(), QUrl(QStringLiteral("http://example.org/pdf")));
4565+ QCOMPARE(model->data(model->index(0), DownloadsModel::Mimetype).toString(), QStringLiteral("application/pdf"));
4566 }
4567
4568 void shouldRemoveCancelled()
4569 {
4570- model->add("testid", QUrl("http://example.org/"), "text/plain");
4571- model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");
4572- model->add("testid3", QUrl("https://example.org/secure.png"), "image/png");
4573+ model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
4574+ model->add(QStringLiteral("testid2"), QUrl(QStringLiteral("http://example.org/pdf")), QStringLiteral("application/pdf"), false);
4575+ model->add(QStringLiteral("testid3"), QUrl(QStringLiteral("https://example.org/secure.png")), QStringLiteral("image/png"), false);
4576 QCOMPARE(model->rowCount(), 3);
4577
4578- model->cancelDownload("testid2");
4579+ model->cancelDownload(QStringLiteral("testid2"));
4580 QCOMPARE(model->rowCount(), 2);
4581
4582- model->cancelDownload("invalid");
4583+ model->cancelDownload(QStringLiteral("invalid"));
4584 QCOMPARE(model->rowCount(), 2);
4585 }
4586
4587 void shouldCompleteDownloads()
4588 {
4589- QSignalSpy spy(model, SIGNAL(completeChanged(QString, bool)));
4590-
4591- model->add("testid", QUrl("http://example.org/"), "text/plain");
4592- QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Complete).toBool());
4593- model->setComplete("testid", true);
4594- QCOMPARE(spy.count(), 1);
4595- QVariantList args = spy.takeFirst();
4596- QCOMPARE(args.at(0).toString(), QString("testid"));
4597- QCOMPARE(args.at(1).toBool(), true);
4598+ model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
4599+ QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Complete).toBool());
4600+ QSignalSpy spy(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
4601+
4602+ model->setComplete(QStringLiteral("testid"), true);
4603+ QVERIFY(model->data(model->index(0, 0), DownloadsModel::Complete).toBool());
4604+ QCOMPARE(spy.count(), 1);
4605+ QVariantList args = spy.takeFirst();
4606+ QCOMPARE(args.at(0).toModelIndex().row(), 0);
4607+ QCOMPARE(args.at(1).toModelIndex().row(), 0);
4608+ QVector<int> roles = args.at(2).value<QVector<int> >();
4609+ QCOMPARE(roles.size(), 1);
4610+ QCOMPARE(roles.at(0), (int) DownloadsModel::Complete);
4611+
4612+ model->setComplete(QStringLiteral("testid"), true);
4613+ QVERIFY(model->data(model->index(0, 0), DownloadsModel::Complete).toBool());
4614+ QVERIFY(spy.isEmpty());
4615+
4616+ model->setComplete(QStringLiteral("testid"), false);
4617+ QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Complete).toBool());
4618+ QCOMPARE(spy.count(), 1);
4619+ args = spy.takeFirst();
4620+ QCOMPARE(args.at(0).toModelIndex().row(), 0);
4621+ QCOMPARE(args.at(1).toModelIndex().row(), 0);
4622+ roles = args.at(2).value<QVector<int> >();
4623+ QCOMPARE(roles.size(), 1);
4624+ QCOMPARE(roles.at(0), (int) DownloadsModel::Complete);
4625+ }
4626+
4627+ void shouldSetError()
4628+ {
4629+ model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
4630+ QVERIFY(model->data(model->index(0, 0), DownloadsModel::Error).toString().isEmpty());
4631+ QSignalSpy spy(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
4632+
4633+ model->setError(QStringLiteral("testid"), QStringLiteral("foo"));
4634+ QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Error).toString(), QStringLiteral("foo"));
4635+ QCOMPARE(spy.count(), 1);
4636+ QVariantList args = spy.takeFirst();
4637+ QCOMPARE(args.at(0).toModelIndex().row(), 0);
4638+ QCOMPARE(args.at(1).toModelIndex().row(), 0);
4639+ QVector<int> roles = args.at(2).value<QVector<int> >();
4640+ QCOMPARE(roles.size(), 1);
4641+ QCOMPARE(roles.at(0), (int) DownloadsModel::Error);
4642+
4643+ model->setError(QStringLiteral("testid"), QStringLiteral("foo"));
4644+ QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Error).toString(), QStringLiteral("foo"));
4645+ QVERIFY(spy.isEmpty());
4646+
4647+ model->setError(QStringLiteral("testid"), QString("bar"));
4648+ QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Error).toString(), QStringLiteral("bar"));
4649+ QCOMPARE(spy.count(), 1);
4650+ args = spy.takeFirst();
4651+ QCOMPARE(args.at(0).toModelIndex().row(), 0);
4652+ QCOMPARE(args.at(1).toModelIndex().row(), 0);
4653+ roles = args.at(2).value<QVector<int> >();
4654+ QCOMPARE(roles.size(), 1);
4655+ QCOMPARE(roles.at(0), (int) DownloadsModel::Error);
4656+ }
4657+
4658+ void shouldPauseAndResumeDownload()
4659+ {
4660+ model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
4661+ QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Paused).toBool());
4662+ QSignalSpy spy(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
4663+
4664+ model->pauseDownload(QStringLiteral("testid"));
4665+ QVERIFY(model->data(model->index(0, 0), DownloadsModel::Paused).toBool());
4666+ QCOMPARE(spy.count(), 1);
4667+ QVariantList args = spy.takeFirst();
4668+ QCOMPARE(args.at(0).toModelIndex().row(), 0);
4669+ QCOMPARE(args.at(1).toModelIndex().row(), 0);
4670+ QVector<int> roles = args.at(2).value<QVector<int> >();
4671+ QCOMPARE(roles.size(), 1);
4672+ QCOMPARE(roles.at(0), (int) DownloadsModel::Paused);
4673+
4674+ model->pauseDownload(QStringLiteral("testid"));
4675+ QVERIFY(model->data(model->index(0, 0), DownloadsModel::Paused).toBool());
4676+ QVERIFY(spy.isEmpty());
4677+
4678+ model->resumeDownload(QStringLiteral("testid"));
4679+ QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Paused).toBool());
4680+ QCOMPARE(spy.count(), 1);
4681+ args = spy.takeFirst();
4682+ QCOMPARE(args.at(0).toModelIndex().row(), 0);
4683+ QCOMPARE(args.at(1).toModelIndex().row(), 0);
4684+ roles = args.at(2).value<QVector<int> >();
4685+ QCOMPARE(roles.size(), 1);
4686+ QCOMPARE(roles.at(0), (int) DownloadsModel::Paused);
4687 }
4688
4689 void shouldKeepEntriesSortedChronologically()
4690 {
4691- model->add("testid", QUrl("http://example.org/"), "text/plain");
4692- model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");
4693- model->add("testid3", QUrl("https://example.org/secure.png"), "image/png");
4694+ model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
4695+ model->add(QStringLiteral("testid2"), QUrl(QStringLiteral("http://example.org/pdf")), QStringLiteral("application/pdf"), false);
4696+ model->add(QStringLiteral("testid3"), QUrl(QStringLiteral("https://example.org/secure.png")), QStringLiteral("image/png"), false);
4697
4698- QCOMPARE(model->data(model->index(0, 0), DownloadsModel::DownloadId).toString(), QString("testid3"));
4699- QCOMPARE(model->data(model->index(1, 0), DownloadsModel::DownloadId).toString(), QString("testid2"));
4700- QCOMPARE(model->data(model->index(2, 0), DownloadsModel::DownloadId).toString(), QString("testid"));
4701+ QCOMPARE(model->data(model->index(0, 0), DownloadsModel::DownloadId).toString(), QStringLiteral("testid3"));
4702+ QCOMPARE(model->data(model->index(1, 0), DownloadsModel::DownloadId).toString(), QStringLiteral("testid2"));
4703+ QCOMPARE(model->data(model->index(2, 0), DownloadsModel::DownloadId).toString(), QStringLiteral("testid"));
4704 }
4705
4706 void shouldReturnData()
4707 {
4708- model->add("testid", QUrl("http://example.org/"), "text/plain");
4709+ model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
4710 QVERIFY(!model->data(QModelIndex(), DownloadsModel::DownloadId).isValid());
4711 QVERIFY(!model->data(model->index(-1, 0), DownloadsModel::DownloadId).isValid());
4712 QVERIFY(!model->data(model->index(3, 0), DownloadsModel::DownloadId).isValid());
4713- QCOMPARE(model->data(model->index(0, 0), DownloadsModel::DownloadId).toString(), QString("testid"));
4714- QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Url).toUrl(), QUrl("http://example.org/"));
4715- QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Mimetype).toString(), QString("text/plain"));
4716+ QCOMPARE(model->data(model->index(0, 0), DownloadsModel::DownloadId).toString(), QStringLiteral("testid"));
4717+ QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Url).toUrl(), QUrl(QStringLiteral("http://example.org/")));
4718+ QCOMPARE(model->data(model->index(0, 0), DownloadsModel::Mimetype).toString(), QStringLiteral("text/plain"));
4719 QVERIFY(model->data(model->index(0, 0), DownloadsModel::Created).toDateTime() <= QDateTime::currentDateTime());
4720 QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Complete).toBool());
4721+ QVERIFY(!model->data(model->index(0, 0), DownloadsModel::Incognito).toBool());
4722+ QVERIFY(!model->data(model->index(0, 0), -1).isValid());
4723 }
4724
4725 void shouldReturnDatabasePath()
4726 {
4727- QCOMPARE(model->databasePath(), QString(":memory:"));
4728+ QCOMPARE(model->databasePath(), QStringLiteral(":memory:"));
4729 }
4730
4731 void shouldNotifyWhenSettingDatabasePath()
4732@@ -149,14 +244,14 @@
4733 QSignalSpy spyPath(model, SIGNAL(databasePathChanged()));
4734 QSignalSpy spyReset(model, SIGNAL(modelReset()));
4735
4736- model->setDatabasePath(":memory:");
4737+ model->setDatabasePath(QStringLiteral(":memory:"));
4738 QVERIFY(spyPath.isEmpty());
4739 QVERIFY(spyReset.isEmpty());
4740
4741- model->setDatabasePath("");
4742+ model->setDatabasePath(QStringLiteral(""));
4743 QCOMPARE(spyPath.count(), 1);
4744 QCOMPARE(spyReset.count(), 1);
4745- QCOMPARE(model->databasePath(), QString(":memory:"));
4746+ QCOMPARE(model->databasePath(), QStringLiteral(":memory:"));
4747 }
4748
4749 void shouldSerializeOnDisk()
4750@@ -167,8 +262,10 @@
4751 delete model;
4752 model = new DownloadsModel;
4753 model->setDatabasePath(fileName);
4754- model->add("testid", QUrl("http://example.org/"), "text/plain");
4755- model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");
4756+ model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
4757+ model->add(QStringLiteral("testid2"), QUrl(QStringLiteral("http://example.org/pdf")), QStringLiteral("application/pdf"), false);
4758+ model->add(QStringLiteral("testid3"), QUrl(QStringLiteral("http://example.org/incognito.pdf")), QStringLiteral("application/pdf"), true);
4759+ QCOMPARE(model->rowCount(), 3);
4760 delete model;
4761 model = new DownloadsModel;
4762 model->setDatabasePath(fileName);
4763@@ -180,17 +277,139 @@
4764 {
4765 QCOMPARE(model->property("count").toInt(), 0);
4766 QCOMPARE(model->rowCount(), 0);
4767- model->add("testid", QUrl("http://example.org/"), "text/plain");
4768+ model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
4769 QCOMPARE(model->property("count").toInt(), 1);
4770 QCOMPARE(model->rowCount(), 1);
4771- model->add("testid2", QUrl("http://example.org/pdf"), "application/pdf");
4772+ model->add(QStringLiteral("testid2"), QUrl(QStringLiteral("http://example.org/pdf")), QStringLiteral("application/pdf"), false);
4773 QCOMPARE(model->property("count").toInt(), 2);
4774 QCOMPARE(model->rowCount(), 2);
4775- model->add("testid3", QUrl("https://example.org/secure.png"), "image/png");
4776+ model->add(QStringLiteral("testid3"), QUrl(QStringLiteral("https://example.org/secure.png")), QStringLiteral("image/png"), false);
4777 QCOMPARE(model->property("count").toInt(), 3);
4778 QCOMPARE(model->rowCount(), 3);
4779 }
4780
4781+ void shouldPruneIncognitoDownloads()
4782+ {
4783+ model->add(QStringLiteral("testid1"), QUrl(QStringLiteral("http://example.org/1")), QStringLiteral("text/plain"), false);
4784+ model->add(QStringLiteral("testid2"), QUrl(QStringLiteral("http://example.org/2")), QStringLiteral("text/plain"), true);
4785+ model->add(QStringLiteral("testid3"), QUrl(QStringLiteral("http://example.org/3")), QStringLiteral("text/plain"), false);
4786+ model->add(QStringLiteral("testid4"), QUrl(QStringLiteral("http://example.org/4")), QStringLiteral("text/plain"), true);
4787+ QCOMPARE(model->rowCount(), 4);
4788+ QSignalSpy spyRowsRemoved(model, SIGNAL(rowsRemoved(const QModelIndex&, int, int)));
4789+ QSignalSpy spyRowCountChanged(model, SIGNAL(rowCountChanged()));
4790+ model->pruneIncognitoDownloads();
4791+ QCOMPARE(model->rowCount(), 2);
4792+ QCOMPARE(model->data(model->index(0), DownloadsModel::DownloadId).toString(), QStringLiteral("testid3"));
4793+ QCOMPARE(model->data(model->index(1), DownloadsModel::DownloadId).toString(), QStringLiteral("testid1"));
4794+ QCOMPARE(spyRowsRemoved.count(), 2);
4795+ QCOMPARE(spyRowCountChanged.count(), 2);
4796+ }
4797+
4798+ void shouldFailToMoveInvalidDownload()
4799+ {
4800+ model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
4801+ QTemporaryFile tempFile;
4802+ tempFile.open();
4803+ QSignalSpy spy(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
4804+ model->moveToDownloads(QStringLiteral("foobar"), tempFile.fileName());
4805+ QVERIFY(spy.isEmpty());
4806+ }
4807+
4808+ void shouldFailToMoveNonExistentFile()
4809+ {
4810+ model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
4811+ QTemporaryFile tempFile;
4812+ tempFile.open();
4813+ QString fileName = tempFile.fileName();
4814+ tempFile.remove();
4815+ QSignalSpy spy(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
4816+ QTest::ignoreMessage(QtWarningMsg, QString("Download not found: \"%1\"").arg(fileName).toUtf8().constData());
4817+ model->moveToDownloads(QStringLiteral("testid"), fileName);
4818+ QVERIFY(spy.isEmpty());
4819+ }
4820+
4821+ void shouldMoveFile()
4822+ {
4823+ model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("application/pdf"), false);
4824+ QTemporaryFile tempFile(QStringLiteral("XXXXXX.txt"));
4825+ tempFile.open();
4826+ tempFile.write(QByteArray("foo bar baz"));
4827+ tempFile.close();
4828+ QString filePath = tempFile.fileName();
4829+ QString fileName = QFileInfo(filePath).fileName();
4830+ QVERIFY(QFile::exists(filePath));
4831+ QSignalSpy spy(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
4832+ model->moveToDownloads(QStringLiteral("testid"), filePath);
4833+ QCOMPARE(spy.count(), 1);
4834+ QVariantList args = spy.takeFirst();
4835+ QCOMPARE(args.at(0).toModelIndex().row(), 0);
4836+ QCOMPARE(args.at(1).toModelIndex().row(), 0);
4837+ QVector<int> roles = args.at(2).value<QVector<int> >();
4838+ QCOMPARE(roles.size(), 2);
4839+ QVERIFY(roles.contains(DownloadsModel::Mimetype));
4840+ QVERIFY(roles.contains(DownloadsModel::Path));
4841+ QCOMPARE(model->data(model->index(0), DownloadsModel::Mimetype).toString(), QStringLiteral("text/plain"));
4842+ QCOMPARE(model->data(model->index(0), DownloadsModel::Path).toString(), QString("%1/Downloads/%2").arg(homeDir.path(), fileName));
4843+ QVERIFY(!QFile::exists(filePath));
4844+ }
4845+
4846+ void shouldRenameFileToAvoidFilenameCollision()
4847+ {
4848+ model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
4849+ QTemporaryFile tempFile(QStringLiteral("XXXXXX.txt"));
4850+ tempFile.open();
4851+ tempFile.write(QByteArray("foo"));
4852+ tempFile.close();
4853+ QString filePath = tempFile.fileName();
4854+ QString fileName = QFileInfo(filePath).fileName();
4855+ QVERIFY(QFile::exists(filePath));
4856+ QSignalSpy spy(model, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
4857+ QString path = QString("%1/Downloads/%2").arg(homeDir.path(), fileName);
4858+ QFile file(path);
4859+ QVERIFY(file.open(QIODevice::WriteOnly));
4860+ QVERIFY(file.write("bar") != -1);
4861+ file.close();
4862+ model->moveToDownloads(QStringLiteral("testid"), filePath);
4863+ QString otherPath = QString("%1/Downloads/%2").arg(homeDir.path(), fileName.replace(QStringLiteral("."), QStringLiteral(".1.")));
4864+ QCOMPARE(model->data(model->index(0), DownloadsModel::Path).toString(), otherPath);
4865+ QVERIFY(!QFile::exists(filePath));
4866+ QVERIFY(QFile::exists(path));
4867+ QVERIFY(QFile::exists(otherPath));
4868+ QVERIFY(file.open(QIODevice::ReadOnly));
4869+ QCOMPARE(file.readAll(), QByteArray("bar"));
4870+ file.close();
4871+ QFile file2(otherPath);
4872+ QVERIFY(file2.open(QIODevice::ReadOnly));
4873+ QCOMPARE(file2.readAll(), QByteArray("foo"));
4874+ file2.close();
4875+ }
4876+
4877+ void shouldDeleteDownload()
4878+ {
4879+ // Need a file saved on disk to allow deleting it
4880+ model->add(QStringLiteral("testid"), QUrl(QStringLiteral("http://example.org/")), QStringLiteral("text/plain"), false);
4881+ QTemporaryFile tempFile(QStringLiteral("XXXXXX.txt"));
4882+ tempFile.open();
4883+ tempFile.write(QByteArray("foo bar baz"));
4884+ tempFile.close();
4885+ QString filePath = tempFile.fileName();
4886+ QString fileName = QFileInfo(filePath).fileName();
4887+ model->moveToDownloads(QStringLiteral("testid"), filePath);
4888+ QString path = model->data(model->index(0), DownloadsModel::Path).toString();
4889+ QVERIFY(QFile::exists(path));
4890+
4891+ QSignalSpy spyRowsRemoved(model, SIGNAL(rowsRemoved(const QModelIndex&, int, int)));
4892+ QSignalSpy spyRowCount(model, SIGNAL(rowCountChanged()));
4893+ model->deleteDownload(path);
4894+ QCOMPARE(spyRowsRemoved.count(), 1);
4895+ QVariantList args = spyRowsRemoved.takeFirst();
4896+ QVERIFY(!args.at(0).toModelIndex().isValid());
4897+ QCOMPARE(args.at(1).toInt(), 0);
4898+ QCOMPARE(args.at(2).toInt(), 0);
4899+ QCOMPARE(spyRowCount.count(), 1);
4900+ QCOMPARE(model->rowCount(), 0);
4901+ QVERIFY(!QFile::exists(path));
4902+ }
4903 };
4904
4905 QTEST_MAIN(DownloadsModelTests)
4906
4907=== modified file 'tests/unittests/history-model/tst_HistoryModelTests.cpp'
4908--- tests/unittests/history-model/tst_HistoryModelTests.cpp 2016-02-26 12:26:20 +0000
4909+++ tests/unittests/history-model/tst_HistoryModelTests.cpp 2016-10-23 20:25:22 +0000
4910@@ -252,7 +252,7 @@
4911 model->setDatabasePath("");
4912 QCOMPARE(spyPath.count(), 1);
4913 QCOMPARE(spyReset.count(), 1);
4914- QCOMPARE(model->databasePath(), QString(":memory:"));
4915+ QCOMPARE(model->databasePath(), QString(""));
4916 }
4917
4918 void shouldSerializeOnDisk()
4919@@ -267,10 +267,11 @@
4920 QTest::qWait(1001);
4921 model->add(QUrl("http://example.com/"), "Example Domain", QUrl());
4922 model->hide(QUrl("http://example.com/"));
4923+ QTest::qWait(100);
4924 delete model;
4925 model = new HistoryModel;
4926 model->setDatabasePath(fileName);
4927- QCOMPARE(model->rowCount(), 2);
4928+ QTRY_COMPARE(model->rowCount(), 2);
4929 QCOMPARE(model->data(model->index(0, 0), HistoryModel::Url).toUrl(), QUrl("http://example.com/"));
4930 QCOMPARE(model->data(model->index(0, 0), HistoryModel::Hidden).toBool(), true);
4931 QCOMPARE(model->data(model->index(1, 0), HistoryModel::Url).toUrl(), QUrl("http://example.org/"));
4932
4933=== modified file 'tests/unittests/qml/CMakeLists.txt'
4934--- tests/unittests/qml/CMakeLists.txt 2016-07-01 12:51:45 +0000
4935+++ tests/unittests/qml/CMakeLists.txt 2016-10-23 20:25:22 +0000
4936@@ -6,6 +6,12 @@
4937 find_package(Qt5QuickTest REQUIRED)
4938 find_package(Qt5Sql REQUIRED)
4939
4940+find_program(XVFBRUN xvfb-run)
4941+if(NOT XVFBRUN)
4942+ message(FATAL_ERROR "Could not find xvfb-run, please install the xvfb package")
4943+endif()
4944+set(XVFB_COMMAND ${XVFBRUN} -s "-screen 0 640x480x24" -a)
4945+
4946 set(TEST tst_QmlTests)
4947 set(SOURCES
4948 ${webbrowser-common_SOURCE_DIR}/favicon-fetcher.cpp
4949
4950=== modified file 'tests/unittests/qml/tst_TabsBar.qml'
4951--- tests/unittests/qml/tst_TabsBar.qml 2015-11-26 13:42:01 +0000
4952+++ tests/unittests/qml/tst_TabsBar.qml 2016-10-23 20:25:22 +0000
4953@@ -104,6 +104,10 @@
4954 }
4955 return null
4956 }
4957+
4958+ function getTabItem(index) {
4959+ return findChild(getTabDelegate(index), "tabItem")
4960+ }
4961
4962 function popupMenuOnTab(index) {
4963 var tab = getTabDelegate(index)
4964@@ -294,5 +298,27 @@
4965 compare(tabsModel.get(1).url, "")
4966 compare(tabsModel.get(2).url, baseUrl + "2")
4967 }
4968+
4969+ function test_close_icon_invisible() {
4970+ var count = 20
4971+
4972+ // Add 2 tabs and check both have showCloseIcon
4973+ tabs.appendTab("", "tab " + 0, "")
4974+ tabs.appendTab("", "tab " + 0, "")
4975+
4976+ compare(getTabItem(0).showCloseIcon, true)
4977+ compare(getTabItem(1).showCloseIcon, true)
4978+
4979+ // Add new tabs and check that both icons are shown
4980+ for (var i = 2; i < count; ++i) {
4981+ tabs.appendTab("", "tab " + i, "")
4982+ compare(tabsModel.currentIndex, i)
4983+
4984+ tryCompare(getTabItem(i), "showCloseIcon", true, 1000)
4985+ }
4986+
4987+ // Check that middle non-selected tab icons are not shown
4988+ compare(getTabItem(count - 10).showCloseIcon, false)
4989+ }
4990 }
4991 }

Subscribers

People subscribed via source and target branches

to status/vote changes: