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

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

Commit message

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

Description of the change

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

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

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

* Revert .pot changes

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

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

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

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

* Ensure dialog closes on before unload tests

Unmerged revisions

Preview Diff

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

Subscribers

People subscribed via source and target branches

to status/vote changes: