~mardy/webbrowser-app/plus-1644585

« back to all changes in this revision

Viewing changes to src/app/webbrowser/TabComponent.qml

* Fix for issue where many tabs causes close button to overlap other
  tabs (LP: #1473630)
* When page has started, stopped, redirected or errored clear cache for
  history update - which prevents incorrect titles in being set
  (LP: #1603835)
* Add autopilot tests javascript dialogs to webbrowser and
  webapp-container - alertDialog, beforeUnloadDialog, confirmDialog and
  promptDialog (LP: #1633040)
* Add user-agent override to display the new twitter mobile interface
  (LP: #1577834)
* Improved startup time by 800ms by delaying QML compilation and making
  it asynchronous
* Replace chromium version in UA overrides at runtime, not at build
  time (LP: #1599695)
* Initial support for generating a snap package for webbrowser-app
  (LP: #1629009)
* Do not persist references to incognito downloads on disk
  (LP: #1625519)
* Increase test coverage (to 97.5%) for DownloadsModel (LP: #1534102)
* Various performance optimizations linked to load events
  (LP: #1611680)
* Ensure a tab is loaded when re-opened (LP: #1632246)
* Fix drag'n'drop of bookmarks within the new tab view (LP: #1584868)
* Work around a limitation in the sound and microphone policy groups
  to "fix" sound in yakkety an zesty (LP: #1632620)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/*
 
2
 * Copyright 2014-2016 Canonical Ltd.
 
3
 *
 
4
 * This file is part of webbrowser-app.
 
5
 *
 
6
 * webbrowser-app is free software; you can redistribute it and/or modify
 
7
 * it under the terms of the GNU General Public License as published by
 
8
 * the Free Software Foundation; version 3.
 
9
 *
 
10
 * webbrowser-app is distributed in the hope that it will be useful,
 
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
13
 * GNU General Public License for more details.
 
14
 *
 
15
 * You should have received a copy of the GNU General Public License
 
16
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
17
 */
 
18
 
 
19
import QtQuick 2.4
 
20
import Ubuntu.Components 1.3
 
21
import Ubuntu.Components.Popups 1.3
 
22
import com.canonical.Oxide 1.15 as Oxide
 
23
import webbrowserapp.private 0.1
 
24
import "../actions" as Actions
 
25
import ".."
 
26
 
 
27
// FIXME: This component breaks encapsulation: it uses variables not defined in
 
28
// itself. However this is an acceptable tradeoff with regards to
 
29
// startup time performance. Indeed having this component defined as a separate
 
30
// QML file as opposed to inline makes it possible to cache its compiled form.
 
31
 
 
32
Component {
 
33
    id: tabComponent
 
34
 
 
35
    BrowserTab {
 
36
        anchors.fill: parent
 
37
        incognito: browser.incognito
 
38
        current: tabsModel && tabsModel.currentTab === this
 
39
        focus: current
 
40
 
 
41
        Item {
 
42
            id: contextualMenuTarget
 
43
            visible: false
 
44
        }
 
45
 
 
46
        webviewComponent: WebViewImpl {
 
47
            id: webviewimpl
 
48
 
 
49
            property BrowserTab tab
 
50
            readonly property bool current: tab.current
 
51
 
 
52
            currentWebview: browser.currentWebview
 
53
            filePicker: filePickerLoader.item
 
54
 
 
55
            anchors.fill: parent
 
56
 
 
57
            focus: true
 
58
 
 
59
            enabled: current && !bottomEdgeHandle.dragging && !recentView.visible && tabContainer.focus
 
60
 
 
61
            locationBarController {
 
62
                height: chrome.height
 
63
                mode: chromeController.defaultMode
 
64
            }
 
65
 
 
66
            //experimental.preferences.developerExtrasEnabled: developerExtrasEnabled
 
67
            preferences.localStorageEnabled: true
 
68
            preferences.appCacheEnabled: true
 
69
 
 
70
            property QtObject contextModel: null
 
71
            contextualActions: ActionList {
 
72
                Actions.OpenLinkInNewTab {
 
73
                    objectName: "OpenLinkInNewTabContextualAction"
 
74
                    enabled: contextModel && contextModel.linkUrl.toString()
 
75
                    onTriggered: internal.openUrlInNewTab(contextModel.linkUrl, true)
 
76
                }
 
77
                Actions.OpenLinkInNewBackgroundTab {
 
78
                    objectName: "OpenLinkInNewBackgroundTabContextualAction"
 
79
                    enabled: contextModel && contextModel.linkUrl.toString()
 
80
                    onTriggered: internal.openUrlInNewTab(contextModel.linkUrl, false)
 
81
                }
 
82
                Actions.OpenLinkInNewWindow {
 
83
                    objectName: "OpenLinkInNewWindowContextualAction"
 
84
                    enabled: contextModel && contextModel.linkUrl.toString()
 
85
                    onTriggered: browser.openLinkInWindowRequested(contextModel.linkUrl, false)
 
86
                }
 
87
                Actions.OpenLinkInPrivateWindow {
 
88
                    objectName: "OpenLinkInPrivateWindowContextualAction"
 
89
                    enabled: contextModel && contextModel.linkUrl.toString()
 
90
                    onTriggered: browser.openLinkInWindowRequested(contextModel.linkUrl, true)
 
91
                }
 
92
                Actions.BookmarkLink {
 
93
                    objectName: "BookmarkLinkContextualAction"
 
94
                    enabled: contextModel && contextModel.linkUrl.toString()
 
95
                             && !BookmarksModel.contains(contextModel.linkUrl)
 
96
                    onTriggered: {
 
97
                        // position the menu target with a one-off assignement instead of a binding
 
98
                        // since the contents of the contextModel have meaning only while the context
 
99
                        // menu is active
 
100
                        contextualMenuTarget.x = contextModel.position.x
 
101
                        contextualMenuTarget.y = contextModel.position.y + locationBarController.height + locationBarController.offset
 
102
                        internal.addBookmark(contextModel.linkUrl, contextModel.linkText,
 
103
                                             "", contextualMenuTarget)
 
104
                    }
 
105
                }
 
106
                Actions.CopyLink {
 
107
                    objectName: "CopyLinkContextualAction"
 
108
                    enabled: contextModel && contextModel.linkUrl.toString()
 
109
                    onTriggered: Clipboard.push(["text/plain", contextModel.linkUrl.toString()])
 
110
                }
 
111
                Actions.SaveLink {
 
112
                    objectName: "SaveLinkContextualAction"
 
113
                    enabled: contextModel && contextModel.linkUrl.toString()
 
114
                    onTriggered: contextModel.saveLink()
 
115
                }
 
116
                Actions.Share {
 
117
                    objectName: "ShareContextualAction"
 
118
                    enabled: (contentHandlerLoader.status == Loader.Ready) && contextModel &&
 
119
                             (contextModel.linkUrl.toString() || contextModel.selectionText)
 
120
                    onTriggered: {
 
121
                        if (contextModel.linkUrl.toString()) {
 
122
                            internal.shareLink(contextModel.linkUrl.toString(), contextModel.linkText)
 
123
                        } else if (contextModel.selectionText) {
 
124
                            internal.shareText(contextModel.selectionText)
 
125
                        }
 
126
                    }
 
127
                }
 
128
                Actions.OpenImageInNewTab {
 
129
                    objectName: "OpenImageInNewTabContextualAction"
 
130
                    enabled: contextModel &&
 
131
                             (contextModel.mediaType === Oxide.WebView.MediaTypeImage) &&
 
132
                             contextModel.srcUrl.toString()
 
133
                    onTriggered: internal.openUrlInNewTab(contextModel.srcUrl, true)
 
134
                }
 
135
                Actions.CopyImage {
 
136
                    objectName: "CopyImageContextualAction"
 
137
                    enabled: contextModel &&
 
138
                             (contextModel.mediaType === Oxide.WebView.MediaTypeImage) &&
 
139
                             contextModel.srcUrl.toString()
 
140
                    onTriggered: Clipboard.push(["text/plain", contextModel.srcUrl.toString()])
 
141
                }
 
142
                Actions.SaveImage {
 
143
                    objectName: "SaveImageContextualAction"
 
144
                    enabled: contextModel &&
 
145
                             ((contextModel.mediaType === Oxide.WebView.MediaTypeImage) ||
 
146
                              (contextModel.mediaType === Oxide.WebView.MediaTypeCanvas)) &&
 
147
                             contextModel.hasImageContents
 
148
                    onTriggered: contextModel.saveMedia()
 
149
                }
 
150
                Actions.OpenVideoInNewTab {
 
151
                    objectName: "OpenVideoInNewTabContextualAction"
 
152
                    enabled: contextModel &&
 
153
                             (contextModel.mediaType === Oxide.WebView.MediaTypeVideo) &&
 
154
                             contextModel.srcUrl.toString()
 
155
                    onTriggered: internal.openUrlInNewTab(contextModel.srcUrl, true)
 
156
                }
 
157
                Actions.SaveVideo {
 
158
                    objectName: "SaveVideoContextualAction"
 
159
                    enabled: contextModel &&
 
160
                             (contextModel.mediaType === Oxide.WebView.MediaTypeVideo) &&
 
161
                             contextModel.srcUrl.toString()
 
162
                    onTriggered: contextModel.saveMedia()
 
163
                }
 
164
                Actions.Undo {
 
165
                    objectName: "UndoContextualAction"
 
166
                    enabled: contextModel && contextModel.isEditable &&
 
167
                             (contextModel.editFlags & Oxide.WebView.UndoCapability)
 
168
                    onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandUndo)
 
169
                }
 
170
                Actions.Redo {
 
171
                    objectName: "RedoContextualAction"
 
172
                    enabled: contextModel && contextModel.isEditable &&
 
173
                             (contextModel.editFlags & Oxide.WebView.RedoCapability)
 
174
                    onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandRedo)
 
175
                }
 
176
                Actions.Cut {
 
177
                    objectName: "CutContextualAction"
 
178
                    enabled: contextModel && contextModel.isEditable &&
 
179
                             (contextModel.editFlags & Oxide.WebView.CutCapability)
 
180
                    onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandCut)
 
181
                }
 
182
                Actions.Copy {
 
183
                    objectName: "CopyContextualAction"
 
184
                    enabled: contextModel && (contextModel.selectionText ||
 
185
                                              (contextModel.isEditable &&
 
186
                                               (contextModel.editFlags & Oxide.WebView.CopyCapability)))
 
187
                    onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandCopy)
 
188
                }
 
189
                Actions.Paste {
 
190
                    objectName: "PasteContextualAction"
 
191
                    enabled: contextModel && contextModel.isEditable &&
 
192
                             (contextModel.editFlags & Oxide.WebView.PasteCapability)
 
193
                    onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandPaste)
 
194
                }
 
195
                Actions.Erase {
 
196
                    objectName: "EraseContextualAction"
 
197
                    enabled: contextModel && contextModel.isEditable &&
 
198
                             (contextModel.editFlags & Oxide.WebView.EraseCapability)
 
199
                    onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandErase)
 
200
                }
 
201
                Actions.SelectAll {
 
202
                    objectName: "SelectAllContextualAction"
 
203
                    enabled: contextModel && contextModel.isEditable &&
 
204
                             (contextModel.editFlags & Oxide.WebView.SelectAllCapability)
 
205
                    onTriggered: webviewimpl.executeEditingCommand(Oxide.WebView.EditingCommandSelectAll)
 
206
                }
 
207
            }
 
208
 
 
209
            function contextMenuOnCompleted(menu) {
 
210
                contextModel = menu.contextModel
 
211
                if (contextModel.linkUrl.toString() ||
 
212
                        contextModel.srcUrl.toString() ||
 
213
                        contextModel.selectionText ||
 
214
                        (contextModel.isEditable && contextModel.editFlags) ||
 
215
                        (((contextModel.mediaType == Oxide.WebView.MediaTypeImage) ||
 
216
                          (contextModel.mediaType == Oxide.WebView.MediaTypeCanvas)) &&
 
217
                         contextModel.hasImageContents)) {
 
218
                    menu.show()
 
219
                } else {
 
220
                    contextModel.close()
 
221
                }
 
222
            }
 
223
 
 
224
            Component {
 
225
                id: contextMenuNarrowComponent
 
226
                ContextMenuMobile {
 
227
                    actions: contextualActions
 
228
                    Component.onCompleted: webviewimpl.contextMenuOnCompleted(this)
 
229
                }
 
230
            }
 
231
            Component {
 
232
                id: contextMenuWideComponent
 
233
                ContextMenuWide {
 
234
                    webview: webviewimpl
 
235
                    parent: browser
 
236
                    actions: contextualActions
 
237
                    Component.onCompleted: webviewimpl.contextMenuOnCompleted(this)
 
238
                }
 
239
            }
 
240
            contextMenu: browser.wide ? contextMenuWideComponent : contextMenuNarrowComponent
 
241
 
 
242
            onNewViewRequested: {
 
243
                var tab = tabComponent.createObject(tabContainer, {"request": request})
 
244
                var setCurrent = (request.disposition == Oxide.NewViewRequest.DispositionNewForegroundTab)
 
245
                internal.addTab(tab, setCurrent)
 
246
                if (setCurrent) tabContainer.forceActiveFocus()
 
247
            }
 
248
 
 
249
            onCloseRequested: prepareToClose()
 
250
            onPrepareToCloseResponse: {
 
251
                if (proceed) {
 
252
                    if (tab) {
 
253
                        for (var i = 0; i < tabsModel.count; ++i) {
 
254
                            if (tabsModel.get(i) === tab) {
 
255
                                tabsModel.remove(i)
 
256
                                break
 
257
                            }
 
258
                        }
 
259
                        tab.close()
 
260
                    }
 
261
                    if (tabsModel.count === 0) {
 
262
                        internal.openUrlInNewTab("", true, true)
 
263
                    }
 
264
                }
 
265
            }
 
266
 
 
267
            QtObject {
 
268
                id: webviewInternal
 
269
                property url storedUrl: ""
 
270
                property bool titleSet: false
 
271
                property string title: ""
 
272
            }
 
273
            onLoadEvent: {
 
274
                if (event.type == Oxide.LoadEvent.TypeCommitted) {
 
275
                    chrome.findInPageMode = false
 
276
                    webviewInternal.titleSet = false
 
277
                    webviewInternal.title = title
 
278
                }
 
279
 
 
280
                if (webviewimpl.incognito) {
 
281
                    return
 
282
                }
 
283
 
 
284
                if ((event.type == Oxide.LoadEvent.TypeCommitted) &&
 
285
                        !event.isError &&
 
286
                        (300 > event.httpStatusCode) && (event.httpStatusCode >= 200)) {
 
287
                    webviewInternal.storedUrl = event.url
 
288
                    HistoryModel.add(event.url, title, icon)
 
289
                }
 
290
 
 
291
                // If the page has started, stopped, redirected, errored
 
292
                // then clear the cache for the history update
 
293
                // Otherwise if no title change has occurred the next title
 
294
                // change will be the url of the next page causing the
 
295
                // history entry to be incorrect (pad.lv/1603835)
 
296
                if (event.type == Oxide.LoadEvent.TypeFailed ||
 
297
                        event.type == Oxide.LoadEvent.TypeRedirected ||
 
298
                        event.type == Oxide.LoadEvent.TypeStarted ||
 
299
                        event.type == Oxide.LoadEvent.TypeStopped) {
 
300
                    webviewInternal.titleSet = true
 
301
                    webviewInternal.storedUrl = ""
 
302
                }
 
303
            }
 
304
            onTitleChanged: {
 
305
                if (!webviewInternal.titleSet && webviewInternal.storedUrl.toString()) {
 
306
                    // Record the title to avoid updating the history database
 
307
                    // every time the page dynamically updates its title.
 
308
                    // We don’t want pages that update their title every second
 
309
                    // to achieve an ugly "scrolling title" effect to flood the
 
310
                    // history database with updates.
 
311
                    webviewInternal.titleSet = true
 
312
                    if (webviewInternal.title != title) {
 
313
                        webviewInternal.title = title
 
314
                        HistoryModel.update(webviewInternal.storedUrl, title, icon)
 
315
                    }
 
316
                }
 
317
            }
 
318
            onIconChanged: {
 
319
                if (webviewInternal.storedUrl.toString()) {
 
320
                    HistoryModel.update(webviewInternal.storedUrl, webviewInternal.title, icon)
 
321
                }
 
322
            }
 
323
 
 
324
            onGeolocationPermissionRequested: requestGeolocationPermission(request)
 
325
 
 
326
            property var certificateError
 
327
            function resetCertificateError() {
 
328
                certificateError = null
 
329
            }
 
330
            onCertificateError: {
 
331
                if (!error.isMainFrame || error.isSubresource) {
 
332
                    // Not a main frame document error, just block the content
 
333
                    // (it’s not overridable anyway).
 
334
                    return
 
335
                }
 
336
                if (internal.isCertificateErrorAllowed(error)) {
 
337
                    error.allow()
 
338
                } else {
 
339
                    certificateError = error
 
340
                    error.onCancelled.connect(webviewimpl.resetCertificateError)
 
341
                }
 
342
            }
 
343
 
 
344
            onFullscreenChanged: {
 
345
                if (fullscreen) {
 
346
                    fullscreenExitHintComponent.createObject(webviewimpl)
 
347
                }
 
348
            }
 
349
            Component {
 
350
                id: fullscreenExitHintComponent
 
351
 
 
352
                Rectangle {
 
353
                    id: fullscreenExitHint
 
354
                    objectName: "fullscreenExitHint"
 
355
 
 
356
                    anchors.centerIn: parent
 
357
                    height: units.gu(6)
 
358
                    width: Math.min(units.gu(50), parent.width - units.gu(12))
 
359
                    radius: units.gu(1)
 
360
                    color: "#3e3b39"
 
361
                    opacity: 0.85
 
362
 
 
363
                    Behavior on opacity {
 
364
                        UbuntuNumberAnimation {
 
365
                            duration: UbuntuAnimation.SlowDuration
 
366
                        }
 
367
                    }
 
368
                    onOpacityChanged: {
 
369
                        if (opacity == 0.0) {
 
370
                            fullscreenExitHint.destroy()
 
371
                        }
 
372
                    }
 
373
 
 
374
                    // Delay showing the hint to prevent it from jumping up while the
 
375
                    // webview is being resized (https://launchpad.net/bugs/1454097).
 
376
                    visible: false
 
377
                    Timer {
 
378
                        running: true
 
379
                        interval: 250
 
380
                        onTriggered: fullscreenExitHint.visible = true
 
381
                    }
 
382
 
 
383
                    Label {
 
384
                        color: "white"
 
385
                        font.weight: Font.Light
 
386
                        anchors.centerIn: parent
 
387
                        text: bottomEdgeHandle.enabled
 
388
                              ? i18n.tr("Swipe Up To Exit Full Screen")
 
389
                              : i18n.tr("Press ESC To Exit Full Screen")
 
390
                    }
 
391
 
 
392
                    Timer {
 
393
                        running: fullscreenExitHint.visible
 
394
                        interval: 2000
 
395
                        onTriggered: fullscreenExitHint.opacity = 0
 
396
                    }
 
397
 
 
398
                    Connections {
 
399
                        target: webviewimpl
 
400
                        onFullscreenChanged: {
 
401
                            if (!webviewimpl.fullscreen) {
 
402
                                fullscreenExitHint.destroy()
 
403
                            }
 
404
                        }
 
405
                    }
 
406
 
 
407
                    Component.onCompleted: bottomEdgeHint.forceShow = true
 
408
                    Component.onDestruction: bottomEdgeHint.forceShow = false
 
409
                }
 
410
            }
 
411
 
 
412
            onShowDownloadDialog: {
 
413
                if (downloadDialogLoader.status === Loader.Ready) {
 
414
                    var downloadDialog = PopupUtils.open(downloadDialogLoader.item, browser, {"contentType" : contentType,
 
415
                                                             "downloadId" : downloadId,
 
416
                                                             "singleDownload" : downloader,
 
417
                                                             "filename" : filename,
 
418
                                                             "mimeType" : mimeType})
 
419
                    downloadDialog.startDownload.connect(startDownload)
 
420
                }
 
421
            }
 
422
 
 
423
            function showDownloadsPage() {
 
424
                downloadsViewLoader.active = true
 
425
                return downloadsViewLoader.item
 
426
            }
 
427
 
 
428
            function startDownload(downloadId, download, mimeType) {
 
429
                DownloadsModel.add(downloadId, download.url, mimeType, incognito)
 
430
                download.start()
 
431
                downloadsViewLoader.active = true
 
432
            }
 
433
 
 
434
        }
 
435
    }
 
436
}