summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJüri Valdmann <juri.valdmann@qt.io>2019-04-12 16:44:18 +0200
committerJüri Valdmann <juri.valdmann@qt.io>2019-05-24 16:14:51 +0200
commita69029cf9fcfd0c1fcdaafe5cbcbff2d5dd6b5c5 (patch)
treea38cae2c082ee3c9a0108942b8406947d826a782
parent0f081baa31facec779057de29eec14c6f458f6a6 (diff)
Implement page lifecycle API
[ChangeLog][QtWebEngine][WebEngineView] WebEngineView now supports lifecycle states that can be used for reducing CPU and memory consumption of invisible views. [ChangeLog][QtWebEngineWidgets][QWebEnginePage] QWebEnginePage now supports lifecycle states that can be used for reducing CPU and memory consumption of invisible pages. Fixes: QTBUG-74166 Fixes: QTBUG-55079 Change-Id: I7d70c85dc995bd17c9fe91385a8e2750dbc0a627 Reviewed-by: Leena Miettinen <riitta-leena.miettinen@qt.io> Reviewed-by: Peter Varga <pvarga@inf.u-szeged.hu>
-rw-r--r--examples/webengine/lifecycle/WebBrowser.qml172
-rw-r--r--examples/webengine/lifecycle/WebTab.qml212
-rw-r--r--examples/webengine/lifecycle/WebTabBar.qml100
-rw-r--r--examples/webengine/lifecycle/WebTabButton.qml158
-rw-r--r--examples/webengine/lifecycle/WebTabStack.qml98
-rw-r--r--examples/webengine/lifecycle/WebToolButton.qml63
-rw-r--r--examples/webengine/lifecycle/doc/images/lifecycle-automatic.pngbin0 -> 8148 bytes
-rw-r--r--examples/webengine/lifecycle/doc/images/lifecycle-manual.pngbin0 -> 5855 bytes
-rw-r--r--examples/webengine/lifecycle/doc/images/lifecycle.pngbin0 -> 24494 bytes
-rw-r--r--examples/webengine/lifecycle/doc/src/lifecycle.qdoc167
-rw-r--r--examples/webengine/lifecycle/lifecycle.pro10
-rw-r--r--examples/webengine/lifecycle/main.cpp66
-rw-r--r--examples/webengine/lifecycle/qtquickcontrols2.conf6
-rw-r--r--examples/webengine/lifecycle/resources.qrc11
-rw-r--r--examples/webengine/webengine.pro1
-rw-r--r--src/core/devtools_frontend_qt.cpp4
-rw-r--r--src/core/favicon_manager.cpp5
-rw-r--r--src/core/favicon_manager.h1
-rw-r--r--src/core/media_capture_devices_dispatcher.cpp51
-rw-r--r--src/core/render_widget_host_view_qt.cpp3
-rw-r--r--src/core/renderer/web_channel_ipc_transport.cpp4
-rw-r--r--src/core/web_contents_adapter.cpp320
-rw-r--r--src/core/web_contents_adapter.h30
-rw-r--r--src/core/web_contents_adapter_client.h15
-rw-r--r--src/core/web_contents_delegate_qt.cpp133
-rw-r--r--src/core/web_contents_delegate_qt.h34
-rw-r--r--src/webengine/api/qquickwebengineview.cpp47
-rw-r--r--src/webengine/api/qquickwebengineview_p.h19
-rw-r--r--src/webengine/api/qquickwebengineview_p_p.h3
-rw-r--r--src/webengine/doc/src/webengineview_lgpl.qdoc53
-rw-r--r--src/webengine/plugin/plugin.cpp1
-rw-r--r--src/webenginewidgets/api/qwebenginepage.cpp176
-rw-r--r--src/webenginewidgets/api/qwebenginepage.h24
-rw-r--r--src/webenginewidgets/api/qwebenginepage_p.h6
-rw-r--r--src/webenginewidgets/api/qwebengineview.cpp14
-rw-r--r--src/webenginewidgets/api/qwebengineview.h1
-rw-r--r--tests/auto/quick/publicapi/tst_publicapi.cpp10
-rw-r--r--tests/auto/widgets/qwebenginepage/resources/lifecycle.html17
-rw-r--r--tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp652
-rw-r--r--tests/auto/widgets/qwebenginepage/tst_qwebenginepage.qrc1
-rw-r--r--tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp13
-rw-r--r--tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp33
42 files changed, 2652 insertions, 82 deletions
diff --git a/examples/webengine/lifecycle/WebBrowser.qml b/examples/webengine/lifecycle/WebBrowser.qml
new file mode 100644
index 000000000..23c2500e2
--- /dev/null
+++ b/examples/webengine/lifecycle/WebBrowser.qml
@@ -0,0 +1,172 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Controls.Material 2.12
+import QtQuick.Layouts 1.12
+import QtQuick.Window 2.12
+
+ApplicationWindow {
+ id: root
+
+ readonly property Action newTabAction: Action {
+ text: qsTr("New tab")
+ shortcut: StandardKey.AddTab
+ onTriggered: root.createNewTab({url: "about:blank"})
+ }
+
+ visible: true
+ width: Screen.width * 0.5
+ height: Screen.height * 0.5
+ title: tabStack.currentTab ? tabStack.currentTab.title : ""
+
+ header: WebTabBar {
+ id: tabBar
+
+ z: 1
+
+ newTabAction: root.newTabAction
+ }
+
+ WebTabStack {
+ id: tabStack
+
+ z: 0
+ anchors.fill: parent
+
+ currentIndex: tabBar.currentIndex
+ freezeDelay: freezeSpin.enabled && freezeSpin.value
+ discardDelay: discardSpin.enabled && discardSpin.value
+
+ onCloseRequested: function(index) {
+ root.closeTab(index)
+ }
+
+ onDrawerRequested: drawer.toggle()
+ }
+
+ Drawer {
+ id: drawer
+
+ edge: Qt.RightEdge
+ height: root.height
+
+ Control {
+ padding: 16
+ contentItem: ColumnLayout {
+ Label {
+ Layout.alignment: Qt.AlignHCenter
+ text: qsTr("Settings")
+ font.capitalization: Font.AllUppercase
+ }
+ MenuSeparator {}
+ CheckBox {
+ id: lifecycleCheck
+ text: qsTr("Automatic lifecycle control")
+ checked: true
+ }
+ CheckBox {
+ id: freezeCheck
+ text: qsTr("Freeze after delay (seconds)")
+ enabled: lifecycleCheck.checked
+ checked: true
+ }
+ SpinBox {
+ id: freezeSpin
+ editable: true
+ enabled: freezeCheck.checked
+ value: 60
+ from: 1
+ to: 60*60
+ }
+ CheckBox {
+ id: discardCheck
+ text: qsTr("Discard after delay (seconds)")
+ enabled: lifecycleCheck.checked
+ checked: true
+ }
+ SpinBox {
+ id: discardSpin
+ editable: true
+ enabled: discardCheck.checked
+ value: 60*60
+ from: 1
+ to: 60*60
+ }
+ }
+ }
+
+ function toggle() {
+ if (drawer.visible)
+ drawer.close()
+ else
+ drawer.open()
+ }
+ }
+
+ Component.onCompleted: {
+ createNewTab({url: "https://www.qt.io"})
+ }
+
+ function createNewTab(properties) {
+ const tab = tabStack.createNewTab(properties)
+ tabBar.createNewTab({tab: tab})
+ tabBar.currentIndex = tab.index
+ return tab
+ }
+
+ function closeTab(index) {
+ if (tabStack.count == 1)
+ Qt.quit()
+ tabBar.closeTab(index)
+ tabStack.closeTab(index)
+ }
+}
diff --git a/examples/webengine/lifecycle/WebTab.qml b/examples/webengine/lifecycle/WebTab.qml
new file mode 100644
index 000000000..645758104
--- /dev/null
+++ b/examples/webengine/lifecycle/WebTab.qml
@@ -0,0 +1,212 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQml 2.12
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Controls.Material 2.12
+import QtQuick.Layouts 1.12
+import QtWebEngine 1.11
+
+ColumnLayout {
+ id: root
+
+ signal closeRequested
+ signal drawerRequested
+
+ property int freezeDelay
+ property int discardDelay
+
+ property alias icon : view.icon
+ property alias loading : view.loading
+ property alias url : view.url
+ property alias lifecycleState : view.lifecycleState
+ property alias recommendedState : view.recommendedState
+
+ readonly property string title : {
+ if (view.url == "about:blank")
+ return qsTr("New tab")
+ if (view.title)
+ return view.title
+ return view.url
+ }
+
+ readonly property Action backAction: Action {
+ property WebEngineAction webAction: view.action(WebEngineView.Back)
+ enabled: webAction.enabled
+ text: qsTr("Back")
+ shortcut: root.visible && StandardKey.Back
+ onTriggered: webAction.trigger()
+ }
+
+ readonly property Action forwardAction: Action {
+ property WebEngineAction webAction: view.action(WebEngineView.Forward)
+ enabled: webAction.enabled
+ text: qsTr("Forward")
+ shortcut: root.visible && StandardKey.Forward
+ onTriggered: webAction.trigger()
+ }
+
+ readonly property Action reloadAction: Action {
+ property WebEngineAction webAction: view.action(WebEngineView.Reload)
+ enabled: webAction.enabled
+ text: qsTr("Reload")
+ shortcut: root.visible && StandardKey.Refresh
+ onTriggered: webAction.trigger()
+ }
+
+ readonly property Action stopAction: Action {
+ property WebEngineAction webAction: view.action(WebEngineView.Stop)
+ enabled: webAction.enabled
+ text: qsTr("Stop")
+ shortcut: root.visible && StandardKey.Cancel
+ onTriggered: webAction.trigger()
+ }
+
+ readonly property Action closeAction: Action {
+ text: qsTr("Close")
+ shortcut: root.visible && StandardKey.Close
+ onTriggered: root.closeRequested()
+ }
+
+ readonly property Action activateAction: Action {
+ text: qsTr("Active")
+ checkable: true
+ checked: view.lifecycleState == WebEngineView.LifecycleState.Active
+ enabled: checked || (view.lifecycleState != WebEngineView.LifecycleState.Active)
+ onTriggered: view.lifecycleState = WebEngineView.LifecycleState.Active
+ }
+
+ readonly property Action freezeAction: Action {
+ text: qsTr("Frozen")
+ checkable: true
+ checked: view.lifecycleState == WebEngineView.LifecycleState.Frozen
+ enabled: checked || (!view.visible && view.lifecycleState == WebEngineView.LifecycleState.Active)
+ onTriggered: view.lifecycleState = WebEngineView.LifecycleState.Frozen
+ }
+
+ readonly property Action discardAction: Action {
+ text: qsTr("Discarded")
+ checkable: true
+ checked: view.lifecycleState == WebEngineView.LifecycleState.Discarded
+ enabled: checked || (!view.visible && view.lifecycleState == WebEngineView.LifecycleState.Frozen)
+ onTriggered: view.lifecycleState = WebEngineView.LifecycleState.Discarded
+ }
+
+ spacing: 0
+
+ ToolBar {
+ Layout.fillWidth: true
+ Material.elevation: 0
+ Material.background: Material.color(Material.Grey, Material.Shade800)
+
+ RowLayout {
+ anchors.fill: parent
+ WebToolButton {
+ action: root.backAction
+ text: "←"
+ ToolTip.text: root.backAction.text
+ }
+ WebToolButton {
+ action: root.forwardAction
+ text: "→"
+ ToolTip.text: root.forwardAction.text
+ }
+ WebToolButton {
+ action: root.reloadAction
+ visible: root.reloadAction.enabled
+ text: "↻"
+ ToolTip.text: root.reloadAction.text
+ }
+ WebToolButton {
+ action: root.stopAction
+ visible: root.stopAction.enabled
+ text: "✕"
+ ToolTip.text: root.stopAction.text
+ }
+ TextField {
+ Layout.fillWidth: true
+ Layout.topMargin: 6
+
+ placeholderText: qsTr("Type a URL")
+ text: view.url == "about:blank" ? "" : view.url
+ selectByMouse: true
+
+ onAccepted: { view.url = text }
+ }
+ WebToolButton {
+ text: "⋮"
+ ToolTip.text: qsTr("Settings")
+ onClicked: root.drawerRequested()
+ }
+ }
+ }
+
+ WebEngineView {
+ id: view
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ }
+
+ Timer {
+ interval: {
+ switch (view.recommendedState) {
+ case WebEngineView.LifecycleState.Active:
+ return 1
+ case WebEngineView.LifecycleState.Frozen:
+ return root.freezeDelay * 1000
+ case WebEngineView.LifecycleState.Discarded:
+ return root.discardDelay * 1000
+ }
+ }
+ running: interval && view.lifecycleState != view.recommendedState
+ onTriggered: view.lifecycleState = view.recommendedState
+ }
+}
diff --git a/examples/webengine/lifecycle/WebTabBar.qml b/examples/webengine/lifecycle/WebTabBar.qml
new file mode 100644
index 000000000..326ad39d2
--- /dev/null
+++ b/examples/webengine/lifecycle/WebTabBar.qml
@@ -0,0 +1,100 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Controls.Material 2.12
+import QtQuick.Layouts 1.12
+
+Pane {
+ id: root
+
+ property Action newTabAction
+
+ property alias currentIndex: tabBar.currentIndex
+
+ signal closeRequested(int index)
+
+ Material.background: Material.color(Material.Grey, Material.Shade900)
+ Material.elevation: 4
+ padding: 0
+
+ RowLayout {
+ spacing: 0
+ anchors.fill: parent
+
+ TabBar {
+ id: tabBar
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+ WebToolButton {
+ Layout.bottomMargin: 2
+
+ action: root.newTabAction
+ text: "+"
+ ToolTip.text: root.newTabAction.text
+ }
+ }
+
+ Component {
+ id: factory
+ WebTabButton {}
+ }
+
+ function createNewTab(properties) {
+ return factory.createObject(tabBar, properties)
+ }
+
+ function closeTab(index) {
+ tabBar.takeItem(index).destroy()
+ }
+}
diff --git a/examples/webengine/lifecycle/WebTabButton.qml b/examples/webengine/lifecycle/WebTabButton.qml
new file mode 100644
index 000000000..815e2fb09
--- /dev/null
+++ b/examples/webengine/lifecycle/WebTabButton.qml
@@ -0,0 +1,158 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Controls.Material 2.12
+import QtQuick.Layouts 1.12
+import QtWebEngine 1.11
+
+TabButton {
+ id: root
+
+ property WebTab tab
+
+ text: root.tab.title
+
+ ToolTip.delay: 1000
+ ToolTip.visible: root.hovered
+ ToolTip.text: root.text
+
+ padding: 6
+
+ contentItem: RowLayout {
+ Item {
+ implicitWidth: 16
+ implicitHeight: 16
+ BusyIndicator {
+ visible: root.tab.loading
+ anchors.fill: parent
+ leftInset: 0
+ topInset: 0
+ rightInset: 0
+ bottomInset: 0
+ padding: 0
+ }
+ Image {
+ visible: !root.tab.loading
+ source: root.tab.icon
+ anchors.fill: parent
+ }
+ }
+ Label {
+ Layout.fillWidth: true
+ Layout.leftMargin: 4
+ Layout.rightMargin: 4
+ text: root.text
+ elide: Text.ElideRight
+ color: {
+ switch (root.tab.lifecycleState) {
+ case WebEngineView.LifecycleState.Active:
+ return Material.color(Material.Grey, Material.Shade100)
+ case WebEngineView.LifecycleState.Frozen:
+ return Material.color(Material.Blue, Material.Shade400)
+ case WebEngineView.LifecycleState.Discarded:
+ return Material.color(Material.Red, Material.Shade400)
+ }
+ }
+
+ }
+ WebToolButton {
+ action: root.tab.closeAction
+ text: "✕"
+ ToolTip.text: action.text
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.RightButton
+ propagateComposedEvents: true
+ onClicked: contextMenu.popup()
+ }
+
+ Menu {
+ id: contextMenu
+ Control {
+ contentItem: Label {
+ text: qsTr("Manual lifecycle control")
+ }
+ verticalPadding: 9
+ horizontalPadding: 14
+ }
+ Repeater {
+ model: [root.tab.activateAction, root.tab.freezeAction, root.tab.discardAction]
+ RadioButton {
+ action: modelData
+ verticalPadding: 9
+ horizontalPadding: 14
+ }
+ }
+ Control {
+ contentItem: Label {
+ text: qsTr("Recommended: %1").arg(recommendedStateText)
+ property string recommendedStateText: {
+ switch (root.tab.recommendedState) {
+ case WebEngineView.LifecycleState.Active:
+ return root.tab.activateAction.text
+ case WebEngineView.LifecycleState.Frozen:
+ return root.tab.freezeAction.text
+ case WebEngineView.LifecycleState.Discarded:
+ return root.tab.discardAction.text
+ }
+ }
+ color: Material.hintTextColor
+ }
+ font.pointSize: 8
+ verticalPadding: 9
+ horizontalPadding: 14
+ }
+ }
+}
diff --git a/examples/webengine/lifecycle/WebTabStack.qml b/examples/webengine/lifecycle/WebTabStack.qml
new file mode 100644
index 000000000..75cbba861
--- /dev/null
+++ b/examples/webengine/lifecycle/WebTabStack.qml
@@ -0,0 +1,98 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQml.Models 2.12
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+
+Rectangle {
+ id: root
+
+ signal closeRequested(int index)
+ signal drawerRequested
+
+ property int freezeDelay
+ property int discardDelay
+
+ property int currentIndex
+ property var currentTab: model.children[currentIndex]
+ property alias count: model.count
+
+ color: "white"
+
+ ObjectModel {
+ id: model
+ }
+
+ Component {
+ id: factory
+ WebTab {
+ readonly property int index : ObjectModel.index
+ anchors.fill: parent
+ visible: index == root.currentIndex
+ freezeDelay: root.freezeDelay
+ discardDelay: root.discardDelay
+ onCloseRequested: root.closeRequested(index)
+ onDrawerRequested: root.drawerRequested()
+ }
+ }
+
+ function createNewTab(properties) {
+ const tab = factory.createObject(root, properties)
+ model.append(tab)
+ return tab
+ }
+
+ function closeTab(index) {
+ const tab = model.get(index)
+ model.remove(index)
+ tab.destroy()
+ }
+}
diff --git a/examples/webengine/lifecycle/WebToolButton.qml b/examples/webengine/lifecycle/WebToolButton.qml
new file mode 100644
index 000000000..958c083cd
--- /dev/null
+++ b/examples/webengine/lifecycle/WebToolButton.qml
@@ -0,0 +1,63 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Controls.Material 2.12
+
+ToolButton {
+ id: root
+ font.bold: true
+ font.pointSize: 12
+ ToolTip.delay: 1000
+ ToolTip.visible: hovered
+ implicitWidth: 32
+ implicitHeight: 32
+}
diff --git a/examples/webengine/lifecycle/doc/images/lifecycle-automatic.png b/examples/webengine/lifecycle/doc/images/lifecycle-automatic.png
new file mode 100644
index 000000000..2c62967af
--- /dev/null
+++ b/examples/webengine/lifecycle/doc/images/lifecycle-automatic.png
Binary files differ
diff --git a/examples/webengine/lifecycle/doc/images/lifecycle-manual.png b/examples/webengine/lifecycle/doc/images/lifecycle-manual.png
new file mode 100644
index 000000000..7fb5c5a80
--- /dev/null
+++ b/examples/webengine/lifecycle/doc/images/lifecycle-manual.png
Binary files differ
diff --git a/examples/webengine/lifecycle/doc/images/lifecycle.png b/examples/webengine/lifecycle/doc/images/lifecycle.png
new file mode 100644
index 000000000..87b719022
--- /dev/null
+++ b/examples/webengine/lifecycle/doc/images/lifecycle.png
Binary files differ
diff --git a/examples/webengine/lifecycle/doc/src/lifecycle.qdoc b/examples/webengine/lifecycle/doc/src/lifecycle.qdoc
new file mode 100644
index 000000000..4151d0597
--- /dev/null
+++ b/examples/webengine/lifecycle/doc/src/lifecycle.qdoc
@@ -0,0 +1,167 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the documentation of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:FDL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Free Documentation License Usage
+** Alternatively, this file may be used under the terms of the GNU Free
+** Documentation License version 1.3 as published by the Free Software
+** Foundation and appearing in the file included in the packaging of
+** this file. Please review the following information to ensure
+** the GNU Free Documentation License version 1.3 requirements
+** will be met: https://www.gnu.org/licenses/fdl-1.3.html.
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+/*!
+ \example webengine/lifecycle
+ \title WebEngine Lifecycle Example
+ \ingroup webengine-examples
+ \brief Freezes and discards background tabs to reduce CPU and memory usage.
+
+ \image lifecycle.png
+
+ \e {WebEngine Lifecycle Example} demonstrates how the \l
+ {WebEngineView::}{lifecycleState} and \l {WebEngineView::}{recommendedState}
+ properties of the \l {WebEngineView} can be used to reduce the CPU and
+ memory usage of background tabs in a tabbed browser.
+
+ \include examples-run.qdocinc
+
+ \section1 UI Elements of the Example
+
+ The example uses \l {Qt Quick Controls 2} to implement a traditional tabbed
+ browser in the \l {Material Style} (dark variant). The main application
+ window (\c {WebBrowser.qml}) is divided into a header bar at the top and a
+ main viewing area filling the rest of the window. The header contains the
+ tab bar (\c {WebTabBar.qml}) with one button per tab (\c
+ {WebTabButton.qml}). The main area consists of a stack of tabs (\c
+ {WebTabStack.qml} and \c {WebTab.qml}). Each tab in turn has a tool bar at
+ the top and a \l {WebEngineView} for displaying web pages. Finally, the main
+ window also has a \l {Drawer} for changing settings. The drawer can be
+ opened by clicking the "⋮" button on the tool bar.
+
+ \section1 Overview of Lifecycle States
+
+ Each \l {WebEngineView} item can be in one of three \e {lifecycle states}:
+ active, frozen, or discarded. These states, like the sleep states of a CPU,
+ control the resource usage of web views.
+
+ The \e {active} state is the normal, unrestricted state of a web view. All
+ visible web views are always in the active state, as are all web views that
+ have not yet finished loading. Only invisible, idle web views can be
+ transitioned to other lifecycle states.
+
+ The \e {frozen} state is a low CPU usage state. In this state, most HTML
+ task sources are suspended (frozen) and, as a result, most DOM event
+ processing and JavaScript execution will also be suspended. The web view
+ must be invisible in order to be frozen as rendering is not possible in this
+ state.
+
+ The \e {discarded} state is an extreme resource-saving state. In this state,
+ the browsing context of the web view will be discarded and the corresponding
+ renderer subprocess shut down. CPU and memory usage in this state is reduced
+ virtually to zero. On exiting this state the web page will be automatically
+ reloaded. The process of entering and exiting the discarded state is similar
+ to serializing the browsing history of the web view and destroying the view,
+ then creating a new view and restoring its history.
+
+ See also \l {WebEngineView::LifecycleState}. The equivalent in the Widgets
+ API is \l {QWebEnginePage::LifecycleState}.
+
+ \section2 The \c {lifecycleState} and \c {recommendedState} Properties
+
+ The \l {WebEngineView::}{lifecycleState} property of the \l {WebEngineView}
+ type is a read-write property that controls the current lifecycle state of
+ the web view. This property is designed to place as few restrictions as
+ possible on what states can be transitioned to. For example, it is allowed
+ to freeze a web view that is currently playing music in the background,
+ stopping the music. In order to implement a less aggressive resource-saving
+ strategy that avoids interrupting user-visible background activity, the \l
+ {WebEngineView::} {recommendedState} property must be used.
+
+ The \l {WebEngineView::}{recommendedState} property of the \l
+ {WebEngineView} type is a read-only property that calculates a safe limit on
+ the \l {WebEngineView::}{lifecycleState} property, taking into account the
+ current activity of the web view. So, in the example of a web view playing
+ music in the background, the recommended state will be \c {Active} since a
+ more aggressive state would stop the music. If the application wants to
+ avoid interrupting background activity, then it should avoid putting the web
+ view into a more aggressively resource-saving lifecycle state than what's
+ given by \l {WebEngineView::}{recommendedState}.
+
+ See also \l {WebEngineView::lifecycleState} and \l
+ {WebEngineView::recommendedState}. The equivalents in the Widgets API are \l
+ {QWebEnginePage::lifecycleState} and \l {QWebEnginePage::recommendedState}.
+
+ \section2 The Page Lifecycle API
+
+ The \l {WebEngineView::}{lifecycleState} property is connected to the \l
+ {https://wicg.github.io/page-lifecycle/spec.html}{Page Lifecycle API}, a
+ work-in-progress extension to the HTML standard that specifies two new DOM
+ events, \c {freeze} and \c {resume}, and adds a new \c
+ {Document.wasDiscarded} boolean property. The \c {freeze} and \c {resume}
+ events are fired when transitioning from the \c {Active} to the \c {Frozen
+ state}, and vice-versa. The \c {Document.wasDiscarded} property is set to \c
+ {true} when transition from the \c {Discarded} state to the \c {Active}
+ state.
+
+ \section1 Lifecycle States in the Example
+
+ The example implements two ways of changing the lifecycle state: manual and
+ automatic. The manual way uses the \l {WebEngineView::}{lifecycleState}
+ property directly to change the web view lifecycle state, while the
+ automatic way is timer-based and also takes into account the \l
+ {WebEngineView::}{recommendedState}.
+
+ The tab titles in the tab bar are color coded with frozen tabs shown in blue
+ and discarded in red.
+
+ \section2 Manual Lifecycle Control
+
+ \image lifecycle-manual.png
+
+ Manual control is provided by context menus on the tab bar buttons (\c
+ {WebTabButton.qml}). The menu has three radio buttons, one for each
+ lifecycle state, with the current state checked. Some buttons may be
+ disabled, either because they represent illegal state transitions (for
+ example, a \c {Discarded} view cannot directly transition to the \c {Frozen}
+ state), or because other preconditions are not fulfilled (for example, a
+ visible view can only be in the \c {Active} state).
+
+ \section2 Automatic Lifecycle Control
+
+ \image lifecycle-automatic.png
+
+ Automatic control is implemented with a \l {Timer} in the \c {WebTab}
+ component (\c {WebTab.qml}). The timer is started whenever the \l
+ {WebEngineView::}{lifecycleState} of the web view does not match it's \l
+ {WebEngineView::}{recommendedState}. Once the timer fires, the view's
+ lifecycle state is set to the recommended state.
+
+ The time delay is used to avoid changing the lifecycle state too quickly
+ when the user is switching between tabs. The freezing and discarding delays
+ can be changed in the settings drawer accessed through the "⋮" button on the
+ tool bar.
+
+ This is a rather simple algorithm for automatic lifecycle control, however
+ more sophisticated algorithms could also be conceived and implemented on the
+ basis of the \l {WebEngineView::}{lifecycleState} property. For example, the
+ Chromium browser experimentally uses a pretrained deep neural network to
+ predict the next tab activation time by the user, essentially ranking tabs
+ based on how interesting they are to the user. Implementing such an
+ algorithm is left as an exercise to the reader for now.
+
+*/
diff --git a/examples/webengine/lifecycle/lifecycle.pro b/examples/webengine/lifecycle/lifecycle.pro
new file mode 100644
index 000000000..74fbf23c1
--- /dev/null
+++ b/examples/webengine/lifecycle/lifecycle.pro
@@ -0,0 +1,10 @@
+TEMPLATE = app
+
+QT += quickcontrols2 webengine
+
+SOURCES += main.cpp
+
+RESOURCES += resources.qrc
+
+target.path = $$[QT_INSTALL_EXAMPLES]/webengine/lifecycle
+INSTALLS += target
diff --git a/examples/webengine/lifecycle/main.cpp b/examples/webengine/lifecycle/main.cpp
new file mode 100644
index 000000000..83907cbaf
--- /dev/null
+++ b/examples/webengine/lifecycle/main.cpp
@@ -0,0 +1,66 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QGuiApplication>
+#include <QQmlApplicationEngine>
+#include <QQmlContext>
+#include <QQuickStyle>
+#include <qtwebengineglobal.h>
+
+int main(int argc, char *argv[])
+{
+ QCoreApplication::setOrganizationName("QtExamples");
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+ QGuiApplication app(argc, argv);
+ QtWebEngine::initialize();
+ QQmlApplicationEngine engine;
+ engine.load(QUrl(QStringLiteral("qrc:/WebBrowser.qml")));
+ return app.exec();
+}
diff --git a/examples/webengine/lifecycle/qtquickcontrols2.conf b/examples/webengine/lifecycle/qtquickcontrols2.conf
new file mode 100644
index 000000000..68c77cec5
--- /dev/null
+++ b/examples/webengine/lifecycle/qtquickcontrols2.conf
@@ -0,0 +1,6 @@
+[Controls]
+Style=Material
+
+[Material]
+Theme=Dark
+Variant=Dense
diff --git a/examples/webengine/lifecycle/resources.qrc b/examples/webengine/lifecycle/resources.qrc
new file mode 100644
index 000000000..41e5092ce
--- /dev/null
+++ b/examples/webengine/lifecycle/resources.qrc
@@ -0,0 +1,11 @@
+<RCC>
+ <qresource prefix="/">
+ <file>WebBrowser.qml</file>
+ <file>WebTab.qml</file>
+ <file>WebTabBar.qml</file>
+ <file>WebTabButton.qml</file>
+ <file>WebTabStack.qml</file>
+ <file>WebToolButton.qml</file>
+ <file>qtquickcontrols2.conf</file>
+ </qresource>
+</RCC>
diff --git a/examples/webengine/webengine.pro b/examples/webengine/webengine.pro
index 5ad620390..058868395 100644
--- a/examples/webengine/webengine.pro
+++ b/examples/webengine/webengine.pro
@@ -8,5 +8,6 @@ SUBDIRS += \
qtHaveModule(quickcontrols2) {
SUBDIRS += \
+ lifecycle \
recipebrowser
}
diff --git a/src/core/devtools_frontend_qt.cpp b/src/core/devtools_frontend_qt.cpp
index 9eedee42a..c2d79331f 100644
--- a/src/core/devtools_frontend_qt.cpp
+++ b/src/core/devtools_frontend_qt.cpp
@@ -190,6 +190,8 @@ DevToolsFrontendQt *DevToolsFrontendQt::Show(QSharedPointer<WebContentsAdapter>
frontendAdapter->initialize(site.get());
}
+ frontendAdapter->setInspector(true);
+
content::WebContents *contents = frontendAdapter->webContents();
if (contents == inspectedContents) {
qWarning() << "You can not inspect youself";
@@ -232,6 +234,8 @@ DevToolsFrontendQt::DevToolsFrontendQt(QSharedPointer<WebContentsAdapter> webCon
DevToolsFrontendQt::~DevToolsFrontendQt()
{
+ if (QSharedPointer<WebContentsAdapter> p = m_webContentsAdapter)
+ p->setInspector(false);
}
void DevToolsFrontendQt::Activate()
diff --git a/src/core/favicon_manager.cpp b/src/core/favicon_manager.cpp
index f7ba858c1..489e23d99 100644
--- a/src/core/favicon_manager.cpp
+++ b/src/core/favicon_manager.cpp
@@ -362,6 +362,11 @@ void FaviconManager::generateCandidateIcon(bool touchIconsEnabled)
}
}
+void FaviconManager::copyStateFrom(FaviconManager *source)
+{
+ m_faviconInfoMap = source->m_faviconInfoMap;
+ m_icons = source->m_icons;
+}
FaviconInfo::FaviconInfo()
: url(QUrl())
diff --git a/src/core/favicon_manager.h b/src/core/favicon_manager.h
index 75d6aa75b..df74f6303 100644
--- a/src/core/favicon_manager.h
+++ b/src/core/favicon_manager.h
@@ -118,6 +118,7 @@ public:
QIcon getIcon(const QUrl &url = QUrl()) const;
FaviconInfo getFaviconInfo(const QUrl &) const;
QList<FaviconInfo> getFaviconInfoList(bool) const;
+ void copyStateFrom(FaviconManager *source);
private:
void update(const QList<FaviconInfo> &);
diff --git a/src/core/media_capture_devices_dispatcher.cpp b/src/core/media_capture_devices_dispatcher.cpp
index ecc46f244..55c0bb39b 100644
--- a/src/core/media_capture_devices_dispatcher.cpp
+++ b/src/core/media_capture_devices_dispatcher.cpp
@@ -45,6 +45,7 @@
#include "javascript_dialog_manager_qt.h"
#include "type_conversion.h"
+#include "web_contents_delegate_qt.h"
#include "web_contents_view_qt.h"
#include "web_engine_settings.h"
@@ -166,6 +167,40 @@ WebContentsAdapterClient::MediaRequestFlags mediaRequestFlagsForRequest(const co
return requestFlags;
}
+// Based on MediaStreamCaptureIndicator::UIDelegate
+class MediaStreamUIQt : public content::MediaStreamUI
+{
+public:
+ MediaStreamUIQt(content::WebContents *webContents, const blink::MediaStreamDevices &devices)
+ : m_delegate(static_cast<WebContentsDelegateQt *>(webContents->GetDelegate())->AsWeakPtr())
+ , m_devices(devices)
+ {
+ DCHECK(!m_devices.empty());
+ }
+
+ ~MediaStreamUIQt() override
+ {
+ if (m_started && m_delegate)
+ m_delegate->removeDevices(m_devices);
+ }
+
+private:
+ gfx::NativeViewId OnStarted(base::OnceClosure, base::RepeatingClosure) override
+ {
+ DCHECK(!m_started);
+ m_started = true;
+ if (m_delegate)
+ m_delegate->addDevices(m_devices);
+ return 0;
+ }
+
+ base::WeakPtr<WebContentsDelegateQt> m_delegate;
+ const blink::MediaStreamDevices m_devices;
+ bool m_started = false;
+
+ DISALLOW_COPY_AND_ASSIGN(MediaStreamUIQt);
+};
+
} // namespace
MediaCaptureDevicesDispatcher::PendingAccessRequest::PendingAccessRequest(const content::MediaStreamRequest &request,
@@ -237,8 +272,12 @@ void MediaCaptureDevicesDispatcher::handleMediaAccessPermissionResponse(content:
base::Unretained(this), webContents));
}
- std::move(callback).Run(devices, devices.empty() ? blink::MEDIA_DEVICE_INVALID_STATE : blink::MEDIA_DEVICE_OK,
- std::unique_ptr<content::MediaStreamUI>());
+ if (devices.empty())
+ std::move(callback).Run(devices, blink::MEDIA_DEVICE_INVALID_STATE,
+ std::unique_ptr<content::MediaStreamUI>());
+ else
+ std::move(callback).Run(devices, blink::MEDIA_DEVICE_OK,
+ std::make_unique<MediaStreamUIQt>(webContents, devices));
}
MediaCaptureDevicesDispatcher *MediaCaptureDevicesDispatcher::GetInstance()
@@ -336,8 +375,12 @@ void MediaCaptureDevicesDispatcher::processDesktopCaptureAccessRequest(content::
getDevicesForDesktopCapture(&devices, mediaId, capture_audio);
- std::move(callback).Run(devices, devices.empty() ? blink::MEDIA_DEVICE_INVALID_STATE : blink::MEDIA_DEVICE_OK,
- std::unique_ptr<content::MediaStreamUI>());
+ if (devices.empty())
+ std::move(callback).Run(devices, blink::MEDIA_DEVICE_INVALID_STATE,
+ std::unique_ptr<content::MediaStreamUI>());
+ else
+ std::move(callback).Run(devices, blink::MEDIA_DEVICE_OK,
+ std::make_unique<MediaStreamUIQt>(webContents, devices));
}
void MediaCaptureDevicesDispatcher::enqueueMediaAccessRequest(content::WebContents *webContents,
diff --git a/src/core/render_widget_host_view_qt.cpp b/src/core/render_widget_host_view_qt.cpp
index 994e3a3d6..091171f90 100644
--- a/src/core/render_widget_host_view_qt.cpp
+++ b/src/core/render_widget_host_view_qt.cpp
@@ -634,7 +634,8 @@ void RenderWidgetHostViewQt::ImeCompositionRangeChanged(const gfx::Range&, const
void RenderWidgetHostViewQt::RenderProcessGone(base::TerminationStatus terminationStatus,
int exitCode)
{
- if (m_adapterClient) {
+ // RenderProcessHost::FastShutdownIfPossible results in TERMINATION_STATUS_STILL_RUNNING
+ if (m_adapterClient && terminationStatus != base::TERMINATION_STATUS_STILL_RUNNING) {
m_adapterClient->renderProcessTerminated(
m_adapterClient->renderProcessExitStatus(terminationStatus),
exitCode);
diff --git a/src/core/renderer/web_channel_ipc_transport.cpp b/src/core/renderer/web_channel_ipc_transport.cpp
index 3b9c17b6a..745fe8b1e 100644
--- a/src/core/renderer/web_channel_ipc_transport.cpp
+++ b/src/core/renderer/web_channel_ipc_transport.cpp
@@ -214,9 +214,11 @@ void WebChannelIPCTransport::ResetWorldId()
void WebChannelIPCTransport::DispatchWebChannelMessage(const std::vector<uint8_t> &binaryJson, uint32_t worldId)
{
- DCHECK(m_canUseContext);
DCHECK(m_worldId == worldId);
+ if (!m_canUseContext)
+ return;
+
QJsonDocument doc = QJsonDocument::fromRawData(reinterpret_cast<const char *>(binaryJson.data()),
binaryJson.size(), QJsonDocument::BypassValidation);
DCHECK(doc.isObject());
diff --git a/src/core/web_contents_adapter.cpp b/src/core/web_contents_adapter.cpp
index c4f4591e3..3c6e651a7 100644
--- a/src/core/web_contents_adapter.cpp
+++ b/src/core/web_contents_adapter.cpp
@@ -478,32 +478,7 @@ void WebContentsAdapter::initialize(content::SiteInstance *site)
m_webContents = content::WebContents::Create(create_params);
}
- content::RendererPreferences* rendererPrefs = m_webContents->GetMutableRendererPrefs();
- rendererPrefs->use_custom_colors = true;
- // Qt returns a flash time (the whole cycle) in ms, chromium expects just the interval in seconds
- const int qtCursorFlashTime = QGuiApplication::styleHints()->cursorFlashTime();
- rendererPrefs->caret_blink_interval = base::TimeDelta::FromMillisecondsD(0.5 * static_cast<double>(qtCursorFlashTime));
- rendererPrefs->user_agent_override = m_profileAdapter->httpUserAgent().toStdString();
- rendererPrefs->accept_languages = m_profileAdapter->httpAcceptLanguageWithoutQualities().toStdString();
-#if QT_CONFIG(webengine_webrtc)
- base::CommandLine* commandLine = base::CommandLine::ForCurrentProcess();
- if (commandLine->HasSwitch(switches::kForceWebRtcIPHandlingPolicy))
- rendererPrefs->webrtc_ip_handling_policy = commandLine->GetSwitchValueASCII(switches::kForceWebRtcIPHandlingPolicy);
- else
- rendererPrefs->webrtc_ip_handling_policy = m_adapterClient->webEngineSettings()->testAttribute(WebEngineSettings::WebRTCPublicInterfacesOnly)
- ? content::kWebRTCIPHandlingDefaultPublicInterfaceOnly
- : content::kWebRTCIPHandlingDefault;
-#endif
- // Set web-contents font settings to the default font settings as Chromium constantly overrides
- // the global font defaults with the font settings of the latest web-contents created.
- static const gfx::FontRenderParams params = gfx::GetFontRenderParams(gfx::FontRenderParamsQuery(), nullptr);
- rendererPrefs->should_antialias_text = params.antialiasing;
- rendererPrefs->use_subpixel_positioning = params.subpixel_positioning;
- rendererPrefs->hinting = params.hinting;
- rendererPrefs->use_autohinter = params.autohinter;
- rendererPrefs->use_bitmaps = params.use_bitmaps;
- rendererPrefs->subpixel_rendering = params.subpixel_rendering;
- m_webContents->GetRenderViewHost()->SyncRendererPrefs();
+ initializeRenderPrefs();
// Create and attach observers to the WebContents.
m_webContentsDelegate.reset(new WebContentsDelegateQt(m_webContents.get(), m_adapterClient));
@@ -540,6 +515,40 @@ void WebContentsAdapter::initialize(content::SiteInstance *site)
m_adapterClient->initializationFinished();
}
+void WebContentsAdapter::initializeRenderPrefs()
+{
+ content::RendererPreferences *rendererPrefs = m_webContents->GetMutableRendererPrefs();
+ rendererPrefs->use_custom_colors = true;
+ // Qt returns a flash time (the whole cycle) in ms, chromium expects just the interval in
+ // seconds
+ const int qtCursorFlashTime = QGuiApplication::styleHints()->cursorFlashTime();
+ rendererPrefs->caret_blink_interval =
+ base::TimeDelta::FromMillisecondsD(0.5 * static_cast<double>(qtCursorFlashTime));
+ rendererPrefs->user_agent_override = m_profileAdapter->httpUserAgent().toStdString();
+ rendererPrefs->accept_languages = m_profileAdapter->httpAcceptLanguageWithoutQualities().toStdString();
+#if QT_CONFIG(webengine_webrtc)
+ base::CommandLine* commandLine = base::CommandLine::ForCurrentProcess();
+ if (commandLine->HasSwitch(switches::kForceWebRtcIPHandlingPolicy))
+ rendererPrefs->webrtc_ip_handling_policy =
+ commandLine->GetSwitchValueASCII(switches::kForceWebRtcIPHandlingPolicy);
+ else
+ rendererPrefs->webrtc_ip_handling_policy =
+ m_adapterClient->webEngineSettings()->testAttribute(WebEngineSettings::WebRTCPublicInterfacesOnly)
+ ? content::kWebRTCIPHandlingDefaultPublicInterfaceOnly
+ : content::kWebRTCIPHandlingDefault;
+#endif
+ // Set web-contents font settings to the default font settings as Chromium constantly overrides
+ // the global font defaults with the font settings of the latest web-contents created.
+ static const gfx::FontRenderParams params = gfx::GetFontRenderParams(gfx::FontRenderParamsQuery(), nullptr);
+ rendererPrefs->should_antialias_text = params.antialiasing;
+ rendererPrefs->use_subpixel_positioning = params.subpixel_positioning;
+ rendererPrefs->hinting = params.hinting;
+ rendererPrefs->use_autohinter = params.autohinter;
+ rendererPrefs->use_bitmaps = params.use_bitmaps;
+ rendererPrefs->subpixel_rendering = params.subpixel_rendering;
+ m_webContents->GetRenderViewHost()->SyncRendererPrefs();
+}
+
bool WebContentsAdapter::canGoBack() const
{
CHECK_INITIALIZED(false);
@@ -568,16 +577,22 @@ void WebContentsAdapter::stop()
void WebContentsAdapter::reload()
{
CHECK_INITIALIZED();
+ bool wasDiscarded = (m_lifecycleState == LifecycleState::Discarded);
+ setLifecycleState(LifecycleState::Active);
CHECK_VALID_RENDER_WIDGET_HOST_VIEW(m_webContents->GetRenderViewHost());
- m_webContents->GetController().Reload(content::ReloadType::NORMAL, /*checkRepost = */false);
+ if (!wasDiscarded) // undiscard() already triggers a reload
+ m_webContents->GetController().Reload(content::ReloadType::NORMAL, /*checkRepost = */false);
focusIfNecessary();
}
void WebContentsAdapter::reloadAndBypassCache()
{
CHECK_INITIALIZED();
+ bool wasDiscarded = (m_lifecycleState == LifecycleState::Discarded);
+ setLifecycleState(LifecycleState::Active);
CHECK_VALID_RENDER_WIDGET_HOST_VIEW(m_webContents->GetRenderViewHost());
- m_webContents->GetController().Reload(content::ReloadType::BYPASSING_CACHE, /*checkRepost = */false);
+ if (!wasDiscarded) // undiscard() already triggers a reload
+ m_webContents->GetController().Reload(content::ReloadType::BYPASSING_CACHE, /*checkRepost = */false);
focusIfNecessary();
}
@@ -600,6 +615,8 @@ void WebContentsAdapter::load(const QWebEngineHttpRequest &request)
scoped_refptr<content::SiteInstance> site =
content::SiteInstance::CreateForURL(m_profileAdapter->profile(), gurl);
initialize(site.get());
+ } else {
+ setLifecycleState(LifecycleState::Active);
}
CHECK_VALID_RENDER_WIDGET_HOST_VIEW(m_webContents->GetRenderViewHost());
@@ -691,6 +708,8 @@ void WebContentsAdapter::setContent(const QByteArray &data, const QString &mimeT
{
if (!isInitialized())
loadDefault();
+ else
+ setLifecycleState(LifecycleState::Active);
CHECK_VALID_RENDER_WIDGET_HOST_VIEW(m_webContents->GetRenderViewHost());
@@ -1106,7 +1125,7 @@ void WebContentsAdapter::setAudioMuted(bool muted)
m_webContents->SetAudioMuted(muted);
}
-bool WebContentsAdapter::recentlyAudible()
+bool WebContentsAdapter::recentlyAudible() const
{
CHECK_INITIALIZED(false);
return m_webContents->IsCurrentlyAudible();
@@ -1167,6 +1186,18 @@ bool WebContentsAdapter::hasInspector() const
return false;
}
+bool WebContentsAdapter::isInspector() const
+{
+ return m_inspector;
+}
+
+void WebContentsAdapter::setInspector(bool inspector)
+{
+ m_inspector = inspector;
+ if (inspector)
+ setLifecycleState(LifecycleState::Active);
+}
+
void WebContentsAdapter::openDevToolsFrontend(QSharedPointer<WebContentsAdapter> frontendAdapter)
{
Q_ASSERT(isInitialized());
@@ -1179,7 +1210,10 @@ void WebContentsAdapter::openDevToolsFrontend(QSharedPointer<WebContentsAdapter>
m_devToolsFrontend->Close();
}
+ setLifecycleState(LifecycleState::Active);
+
m_devToolsFrontend = DevToolsFrontendQt::Show(frontendAdapter, m_webContents.get());
+ updateRecommendedState();
}
void WebContentsAdapter::closeDevToolsFrontend()
@@ -1195,6 +1229,7 @@ void WebContentsAdapter::devToolsFrontendDestroyed(DevToolsFrontendQt *frontend)
Q_ASSERT(frontend == m_devToolsFrontend);
Q_UNUSED(frontend);
m_devToolsFrontend = nullptr;
+ updateRecommendedState();
}
void WebContentsAdapter::exitFullScreen()
@@ -1654,8 +1689,7 @@ WebContentsAdapterClient::renderProcessExitStatus(int terminationStatus) {
break;
case base::TERMINATION_STATUS_STILL_RUNNING:
case base::TERMINATION_STATUS_MAX_ENUM:
- // should be unreachable since Chromium asserts status != TERMINATION_STATUS_STILL_RUNNING
- // before calling this method
+ Q_UNREACHABLE();
break;
}
@@ -1680,6 +1714,230 @@ bool WebContentsAdapter::canViewSource()
return m_webContents->GetController().CanViewSource();
}
+WebContentsAdapter::LifecycleState WebContentsAdapter::lifecycleState() const
+{
+ return m_lifecycleState;
+}
+
+void WebContentsAdapter::setLifecycleState(LifecycleState state)
+{
+ CHECK_INITIALIZED();
+
+ LifecycleState from = m_lifecycleState;
+ LifecycleState to = state;
+
+ const auto warn = [from, to](const char *reason) {
+ static const char *names[] { "Active", "Frozen", "Discarded" };
+ qWarning("setLifecycleState: failed to transition from %s to %s state: %s",
+ names[(int)from], names[(int)to], reason);
+ };
+
+ if (from == to)
+ return;
+
+ if (from == LifecycleState::Active) {
+ if (isVisible()) {
+ warn("page is visible");
+ return;
+ }
+ if (hasInspector() || isInspector()) {
+ warn("DevTools open");
+ return;
+ }
+ }
+
+ if (from == LifecycleState::Discarded && to != LifecycleState::Active) {
+ warn("illegal transition");
+ return;
+ }
+
+ // Prevent recursion due to initializationFinished() in undiscard().
+ m_lifecycleState = to;
+
+ switch (to) {
+ case LifecycleState::Active:
+ if (from == LifecycleState::Frozen)
+ unfreeze();
+ else
+ undiscard();
+ break;
+ case LifecycleState::Frozen:
+ freeze();
+ break;
+ case LifecycleState::Discarded:
+ discard();
+ break;
+ }
+
+ m_adapterClient->lifecycleStateChanged(to);
+ updateRecommendedState();
+}
+
+WebContentsAdapter::LifecycleState WebContentsAdapter::recommendedState() const
+{
+ return m_recommendedState;
+}
+
+WebContentsAdapter::LifecycleState WebContentsAdapter::determineRecommendedState() const
+{
+ CHECK_INITIALIZED(LifecycleState::Active);
+
+ if (m_lifecycleState == LifecycleState::Discarded)
+ return LifecycleState::Discarded;
+
+ if (isVisible())
+ return LifecycleState::Active;
+
+ if (m_webContentsDelegate->loadingState() != WebContentsDelegateQt::LoadingState::Loaded)
+ return LifecycleState::Active;
+
+ if (recentlyAudible())
+ return LifecycleState::Active;
+
+ if (m_webContents->GetSiteInstance()->GetRelatedActiveContentsCount() > 1U)
+ return LifecycleState::Active;
+
+ if (hasInspector() || isInspector())
+ return LifecycleState::Active;
+
+ if (m_webContentsDelegate->isCapturingAudio() || m_webContentsDelegate->isCapturingVideo()
+ || m_webContentsDelegate->isMirroring() || m_webContentsDelegate->isCapturingDesktop())
+ return LifecycleState::Active;
+
+ if (m_webContents->IsCrashed())
+ return LifecycleState::Active;
+
+ if (m_lifecycleState == LifecycleState::Active)
+ return LifecycleState::Frozen;
+
+ // Form input is not saved.
+ if (m_webContents->GetPageImportanceSignals().had_form_interaction)
+ return LifecycleState::Frozen;
+
+ // Do not discard PDFs as they might contain entry that is not saved and they
+ // don't remember their scrolling positions. See crbug.com/547286 and
+ // crbug.com/65244.
+ if (m_webContents->GetContentsMimeType() == "application/pdf")
+ return LifecycleState::Frozen;
+
+ return LifecycleState::Discarded;
+}
+
+void WebContentsAdapter::updateRecommendedState()
+{
+ LifecycleState newState = determineRecommendedState();
+ if (m_recommendedState == newState)
+ return;
+
+ m_recommendedState = newState;
+ m_adapterClient->recommendedStateChanged(newState);
+}
+
+bool WebContentsAdapter::isVisible() const
+{
+ CHECK_INITIALIZED(false);
+
+ // Visibility::OCCLUDED is not used
+ return m_webContents->GetVisibility() == content::Visibility::VISIBLE;
+}
+
+void WebContentsAdapter::setVisible(bool visible)
+{
+ CHECK_INITIALIZED();
+
+ if (isVisible() == visible)
+ return;
+
+ if (visible) {
+ setLifecycleState(LifecycleState::Active);
+ wasShown();
+ } else {
+ Q_ASSERT(m_lifecycleState == LifecycleState::Active);
+ wasHidden();
+ }
+
+ m_adapterClient->visibleChanged(visible);
+ updateRecommendedState();
+}
+
+void WebContentsAdapter::freeze()
+{
+ m_webContents->SetPageFrozen(true);
+}
+
+void WebContentsAdapter::unfreeze()
+{
+ m_webContents->SetPageFrozen(false);
+}
+
+void WebContentsAdapter::discard()
+{
+ // Based on TabLifecycleUnitSource::TabLifecycleUnit::FinishDiscard
+
+ if (m_webContents->IsLoading()) {
+ m_webContentsDelegate->didFailLoad(m_webContentsDelegate->url(), net::Error::ERR_ABORTED,
+ QStringLiteral("Discarded"));
+ }
+
+ content::WebContents::CreateParams createParams(m_profileAdapter->profile());
+ createParams.initially_hidden = true;
+ createParams.desired_renderer_state = content::WebContents::CreateParams::kNoRendererProcess;
+ createParams.last_active_time = m_webContents->GetLastActiveTime();
+ std::unique_ptr<content::WebContents> nullContents = content::WebContents::Create(createParams);
+ std::unique_ptr<WebContentsDelegateQt> nullDelegate(new WebContentsDelegateQt(nullContents.get(), m_adapterClient));
+ nullContents->GetController().CopyStateFrom(&m_webContents->GetController(),
+ /* needs_reload */ false);
+ nullDelegate->copyStateFrom(m_webContentsDelegate.get());
+ nullContents->SetWasDiscarded(true);
+
+ // Kill render process if this is the only page it's got.
+ content::RenderProcessHost *renderProcessHost = m_webContents->GetMainFrame()->GetProcess();
+ renderProcessHost->FastShutdownIfPossible(/* page_count */ 1u,
+ /* skip_unload_handlers */ false);
+
+#if QT_CONFIG(webengine_webchannel)
+ if (m_webChannel)
+ m_webChannel->disconnectFrom(m_webChannelTransport.get());
+ m_webChannelTransport.reset();
+ m_webChannel = nullptr;
+ m_webChannelWorld = 0;
+#endif
+ m_renderViewObserverHost.reset();
+ m_webContentsDelegate.reset();
+ m_webContents.reset();
+
+ m_webContents = std::move(nullContents);
+ initializeRenderPrefs();
+ m_webContentsDelegate = std::move(nullDelegate);
+ m_renderViewObserverHost.reset(new RenderViewObserverHostQt(m_webContents.get(), m_adapterClient));
+ WebContentsViewQt *contentsView =
+ static_cast<WebContentsViewQt *>(static_cast<content::WebContentsImpl *>(m_webContents.get())->GetView());
+ contentsView->setClient(m_adapterClient);
+#if QT_CONFIG(webengine_printing_and_pdf)
+ PrintViewManagerQt::CreateForWebContents(webContents());
+#endif
+#if BUILDFLAG(ENABLE_EXTENSIONS)
+ extensions::ExtensionWebContentsObserverQt::CreateForWebContents(webContents());
+#endif
+}
+
+void WebContentsAdapter::undiscard()
+{
+ m_webContents->GetController().SetNeedsReload();
+ m_webContents->GetController().LoadIfNecessary();
+ // Create a RenderView with the initial empty document
+ content::RenderViewHost *rvh = m_webContents->GetRenderViewHost();
+ Q_ASSERT(rvh);
+ if (!rvh->IsRenderViewLive())
+ static_cast<content::WebContentsImpl *>(m_webContents.get())
+ ->CreateRenderViewForRenderManager(rvh, MSG_ROUTING_NONE, MSG_ROUTING_NONE,
+ base::UnguessableToken::Create(),
+ content::FrameReplicationState());
+ m_webContentsDelegate->RenderViewHostChanged(nullptr, rvh);
+ m_adapterClient->initializationFinished();
+ m_adapterClient->selectionChanged();
+}
+
ASSERT_ENUMS_MATCH(WebContentsAdapterClient::UnknownDisposition, WindowOpenDisposition::UNKNOWN)
ASSERT_ENUMS_MATCH(WebContentsAdapterClient::CurrentTabDisposition, WindowOpenDisposition::CURRENT_TAB)
ASSERT_ENUMS_MATCH(WebContentsAdapterClient::SingletonTabDisposition, WindowOpenDisposition::SINGLETON_TAB)
diff --git a/src/core/web_contents_adapter.h b/src/core/web_contents_adapter.h
index 79aa68456..ab9ec5b81 100644
--- a/src/core/web_contents_adapter.h
+++ b/src/core/web_contents_adapter.h
@@ -109,6 +109,14 @@ public:
void load(const QWebEngineHttpRequest &request);
void setContent(const QByteArray &data, const QString &mimeType, const QUrl &baseUrl);
+ using LifecycleState = WebContentsAdapterClient::LifecycleState;
+ LifecycleState lifecycleState() const;
+ void setLifecycleState(LifecycleState state);
+ LifecycleState recommendedState() const;
+
+ bool isVisible() const;
+ void setVisible(bool visible);
+
bool canGoBack() const;
bool canGoForward() const;
void stop();
@@ -155,7 +163,7 @@ public:
ReferrerPolicy referrerPolicy = ReferrerPolicy::Default);
bool isAudioMuted() const;
void setAudioMuted(bool mute);
- bool recentlyAudible();
+ bool recentlyAudible() const;
// Must match blink::WebMediaPlayerAction::Type.
enum MediaPlayerAction {
@@ -171,6 +179,8 @@ public:
void inspectElementAt(const QPoint &location);
bool hasInspector() const;
+ bool isInspector() const;
+ void setInspector(bool inspector);
void exitFullScreen();
void requestClose();
void changedFullScreen();
@@ -178,8 +188,6 @@ public:
void closeDevToolsFrontend();
void devToolsFrontendDestroyed(DevToolsFrontendQt *frontend);
- void wasShown();
- void wasHidden();
void grantMediaAccessPermission(const QUrl &securityOrigin, WebContentsAdapterClient::MediaRequestFlags flags);
void runGeolocationRequestCallback(const QUrl &securityOrigin, bool allowed);
void grantMouseLockPermission(bool granted);
@@ -221,12 +229,25 @@ public:
// meant to be used within WebEngineCore only
void initialize(content::SiteInstance *site);
content::WebContents *webContents() const;
+ void updateRecommendedState();
private:
Q_DISABLE_COPY(WebContentsAdapter)
void waitForUpdateDragActionCalled();
bool handleDropDataFileContents(const content::DropData &dropData, QMimeData *mimeData);
+ void wasShown();
+ void wasHidden();
+
+ LifecycleState determineRecommendedState() const;
+
+ void freeze();
+ void unfreeze();
+ void discard();
+ void undiscard();
+
+ void initializeRenderPrefs();
+
ProfileAdapter *m_profileAdapter;
std::unique_ptr<content::WebContents> m_webContents;
std::unique_ptr<WebContentsDelegateQt> m_webContentsDelegate;
@@ -246,6 +267,9 @@ private:
QPointF m_lastDragScreenPos;
std::unique_ptr<QTemporaryDir> m_dndTmpDir;
DevToolsFrontendQt *m_devToolsFrontend;
+ LifecycleState m_lifecycleState = LifecycleState::Active;
+ LifecycleState m_recommendedState = LifecycleState::Active;
+ bool m_inspector = false;
};
} // namespace QtWebEngineCore
diff --git a/src/core/web_contents_adapter_client.h b/src/core/web_contents_adapter_client.h
index 7ba45aea8..780c14466 100644
--- a/src/core/web_contents_adapter_client.h
+++ b/src/core/web_contents_adapter_client.h
@@ -412,11 +412,26 @@ public:
};
Q_DECLARE_FLAGS(MediaRequestFlags, MediaRequestFlag)
+ enum class LifecycleState {
+ Active,
+ Frozen,
+ Discarded,
+ };
+
+ enum class LoadingState {
+ Unloaded,
+ Loading,
+ Loaded,
+ };
+
virtual ~WebContentsAdapterClient() { }
virtual RenderWidgetHostViewQtDelegate* CreateRenderWidgetHostViewQtDelegate(RenderWidgetHostViewQtDelegateClient *client) = 0;
virtual RenderWidgetHostViewQtDelegate* CreateRenderWidgetHostViewQtDelegateForPopup(RenderWidgetHostViewQtDelegateClient *client) = 0;
virtual void initializationFinished() = 0;
+ virtual void lifecycleStateChanged(LifecycleState) = 0;
+ virtual void recommendedStateChanged(LifecycleState) = 0;
+ virtual void visibleChanged(bool) = 0;
virtual void titleChanged(const QString&) = 0;
virtual void urlChanged(const QUrl&) = 0;
virtual void iconChanged(const QUrl&) = 0;
diff --git a/src/core/web_contents_delegate_qt.cpp b/src/core/web_contents_delegate_qt.cpp
index 021044a71..f4d794de5 100644
--- a/src/core/web_contents_delegate_qt.cpp
+++ b/src/core/web_contents_delegate_qt.cpp
@@ -104,6 +104,8 @@ WebContentsDelegateQt::WebContentsDelegateQt(content::WebContents *webContents,
, m_lastReceivedFindReply(0)
, m_faviconManager(new FaviconManager(webContents, adapterClient))
, m_lastLoadProgress(-1)
+ , m_loadingState(determineLoadingState(webContents))
+ , m_didStartLoadingSeen(m_loadingState == LoadingState::Loading)
{
webContents->SetDelegate(this);
Observe(webContents);
@@ -267,6 +269,18 @@ void WebContentsDelegateQt::RenderFrameDeleted(content::RenderFrameHost *render_
m_loadingErrorFrameList.removeOne(render_frame_host->GetRoutingID());
}
+void WebContentsDelegateQt::RenderProcessGone(base::TerminationStatus status)
+{
+ // Based one TabLoadTracker::RenderProcessGone
+
+ if (status == base::TerminationStatus::TERMINATION_STATUS_NORMAL_TERMINATION
+ || status == base::TerminationStatus::TERMINATION_STATUS_STILL_RUNNING) {
+ return;
+ }
+
+ setLoadingState(LoadingState::Unloaded);
+}
+
void WebContentsDelegateQt::RenderViewHostChanged(content::RenderViewHost *, content::RenderViewHost *newHost)
{
if (newHost && newHost->GetWidget() && newHost->GetWidget()->GetView()) {
@@ -354,6 +368,46 @@ void WebContentsDelegateQt::DidFinishNavigation(content::NavigationHandle *navig
}
}
+void WebContentsDelegateQt::DidStartLoading()
+{
+ // Based on TabLoadTracker::DidStartLoading
+
+ if (!web_contents()->IsLoadingToDifferentDocument())
+ return;
+ if (m_loadingState == LoadingState::Loading) {
+ DCHECK(m_didStartLoadingSeen);
+ return;
+ }
+ m_didStartLoadingSeen = true;
+}
+
+void WebContentsDelegateQt::DidReceiveResponse()
+{
+ // Based on TabLoadTracker::DidReceiveResponse
+
+ if (m_loadingState == LoadingState::Loading) {
+ DCHECK(m_didStartLoadingSeen);
+ return;
+ }
+
+ // A transition to loading requires both DidStartLoading (navigation
+ // committed) and DidReceiveResponse (data has been transmitted over the
+ // network) events to occur. This is because NavigationThrottles can block
+ // actual network requests, but not the rest of the state machinery.
+ if (m_didStartLoadingSeen)
+ setLoadingState(LoadingState::Loading);
+}
+
+void WebContentsDelegateQt::DidStopLoading()
+{
+ // Based on TabLoadTracker::DidStopLoading
+
+ // NOTE: PageAlmostIdle feature not implemented
+
+ if (m_loadingState == LoadingState::Loading)
+ setLoadingState(LoadingState::Loaded);
+}
+
void WebContentsDelegateQt::didFailLoad(const QUrl &url, int errorCode, const QString &errorDescription)
{
m_viewClient->iconChanged(QUrl());
@@ -362,6 +416,9 @@ void WebContentsDelegateQt::didFailLoad(const QUrl &url, int errorCode, const QS
void WebContentsDelegateQt::DidFailLoad(content::RenderFrameHost* render_frame_host, const GURL& validated_url, int error_code, const base::string16& error_description)
{
+ if (m_loadingState == LoadingState::Loading)
+ setLoadingState(LoadingState::Loaded);
+
if (render_frame_host != web_contents()->GetMainFrame())
return;
@@ -724,4 +781,80 @@ WebContentsAdapter *WebContentsDelegateQt::webContentsAdapter() const
return m_viewClient->webContentsAdapter();
}
+void WebContentsDelegateQt::copyStateFrom(WebContentsDelegateQt *source)
+{
+ m_url = source->m_url;
+ m_title = source->m_title;
+ NavigationStateChanged(web_contents(), content::INVALIDATE_TYPE_URL);
+ m_faviconManager->copyStateFrom(source->m_faviconManager.data());
+}
+
+WebContentsDelegateQt::LoadingState WebContentsDelegateQt::determineLoadingState(content::WebContents *contents)
+{
+ // Based on TabLoadTracker::DetermineLoadingState
+
+ if (contents->IsLoadingToDifferentDocument() && !contents->IsWaitingForResponse())
+ return LoadingState::Loading;
+
+ content::NavigationController &controller = contents->GetController();
+ if (controller.GetLastCommittedEntry() != nullptr && !controller.IsInitialNavigation() && !controller.NeedsReload())
+ return LoadingState::Loaded;
+
+ return LoadingState::Unloaded;
+}
+
+void WebContentsDelegateQt::setLoadingState(LoadingState state)
+{
+ if (m_loadingState == state)
+ return;
+
+ m_loadingState = state;
+
+ webContentsAdapter()->updateRecommendedState();
+}
+
+int &WebContentsDelegateQt::streamCount(blink::MediaStreamType type)
+{
+ // Based on MediaStreamCaptureIndicator::WebContentsDeviceUsage::GetStreamCount
+ switch (type) {
+ case blink::MEDIA_DEVICE_AUDIO_CAPTURE:
+ return m_audioStreamCount;
+
+ case blink::MEDIA_DEVICE_VIDEO_CAPTURE:
+ return m_videoStreamCount;
+
+ case blink::MEDIA_GUM_TAB_AUDIO_CAPTURE:
+ case blink::MEDIA_GUM_TAB_VIDEO_CAPTURE:
+ return m_mirroringStreamCount;
+
+ case blink::MEDIA_GUM_DESKTOP_VIDEO_CAPTURE:
+ case blink::MEDIA_GUM_DESKTOP_AUDIO_CAPTURE:
+ case blink::MEDIA_DISPLAY_VIDEO_CAPTURE:
+ return m_desktopStreamCount;
+
+ case blink::MEDIA_NO_SERVICE:
+ case blink::NUM_MEDIA_TYPES:
+ NOTREACHED();
+ return m_videoStreamCount;
+ }
+ NOTREACHED();
+ return m_videoStreamCount;
+}
+
+void WebContentsDelegateQt::addDevices(const blink::MediaStreamDevices &devices)
+{
+ for (const auto &device : devices)
+ ++streamCount(device.type);
+
+ webContentsAdapter()->updateRecommendedState();
+}
+
+void WebContentsDelegateQt::removeDevices(const blink::MediaStreamDevices &devices)
+{
+ for (const auto &device : devices)
+ ++streamCount(device.type);
+
+ webContentsAdapter()->updateRecommendedState();
+}
+
} // namespace QtWebEngineCore
diff --git a/src/core/web_contents_delegate_qt.h b/src/core/web_contents_delegate_qt.h
index 1629222c2..2ef4f22fc 100644
--- a/src/core/web_contents_delegate_qt.h
+++ b/src/core/web_contents_delegate_qt.h
@@ -40,6 +40,7 @@
#ifndef WEB_CONTENTS_DELEGATE_QT_H
#define WEB_CONTENTS_DELEGATE_QT_H
+#include "content/public/browser/media_capture_devices.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/browser/web_contents_observer.h"
#include "third_party/skia/include/core/SkColor.h"
@@ -133,9 +134,13 @@ public:
// WebContentsObserver overrides
void RenderFrameDeleted(content::RenderFrameHost *render_frame_host) override;
+ void RenderProcessGone(base::TerminationStatus status) override;
void RenderViewHostChanged(content::RenderViewHost *old_host, content::RenderViewHost *new_host) override;
void DidStartNavigation(content::NavigationHandle *navigation_handle) override;
void DidFinishNavigation(content::NavigationHandle *navigation_handle) override;
+ void DidStartLoading() override;
+ void DidReceiveResponse() override;
+ void DidStopLoading() override;
void DidFailLoad(content::RenderFrameHost* render_frame_host, const GURL& validated_url, int error_code, const base::string16& error_description) override;
void DidFinishLoad(content::RenderFrameHost *render_frame_host, const GURL &validated_url) override;
void BeforeUnloadFired(bool proceed, const base::TimeTicks& proceed_time) override;
@@ -160,12 +165,32 @@ public:
WebContentsAdapter *webContentsAdapter() const;
WebContentsAdapterClient *adapterClient() const { return m_viewClient; }
+ void copyStateFrom(WebContentsDelegateQt *source);
+
+ using LoadingState = WebContentsAdapterClient::LoadingState;
+ LoadingState loadingState() const { return m_loadingState; }
+
+ void addDevices(const blink::MediaStreamDevices &devices);
+ void removeDevices(const blink::MediaStreamDevices &devices);
+
+ bool isCapturingAudio() const { return m_audioStreamCount > 0; }
+ bool isCapturingVideo() const { return m_videoStreamCount > 0; }
+ bool isMirroring() const { return m_mirroringStreamCount > 0; }
+ bool isCapturingDesktop() const { return m_desktopStreamCount > 0; }
+
+ base::WeakPtr<WebContentsDelegateQt> AsWeakPtr() { return m_weakPtrFactory.GetWeakPtr(); }
+
private:
QWeakPointer<WebContentsAdapter> createWindow(std::unique_ptr<content::WebContents> new_contents, WindowOpenDisposition disposition, const gfx::Rect& initial_pos, bool user_gesture);
void EmitLoadStarted(const QUrl &url, bool isErrorPage = false);
void EmitLoadFinished(bool success, const QUrl &url, bool isErrorPage = false, int errorCode = 0, const QString &errorDescription = QString());
void EmitLoadCommitted();
+ LoadingState determineLoadingState(content::WebContents *contents);
+ void setLoadingState(LoadingState state);
+
+ int &streamCount(blink::MediaStreamType type);
+
WebContentsAdapterClient *m_viewClient;
QString m_lastSearchedString;
int m_lastReceivedFindReply;
@@ -175,9 +200,16 @@ private:
QSharedPointer<FilePickerController> m_filePickerController;
QUrl m_initialTargetUrl;
int m_lastLoadProgress;
-
+ LoadingState m_loadingState;
+ bool m_didStartLoadingSeen;
QUrl m_url;
QString m_title;
+ int m_audioStreamCount = 0;
+ int m_videoStreamCount = 0;
+ int m_mirroringStreamCount = 0;
+ int m_desktopStreamCount = 0;
+
+ base::WeakPtrFactory<WebContentsDelegateQt> m_weakPtrFactory { this };
};
} // namespace QtWebEngineCore
diff --git a/src/webengine/api/qquickwebengineview.cpp b/src/webengine/api/qquickwebengineview.cpp
index 02c9b4d00..c92e8caab 100644
--- a/src/webengine/api/qquickwebengineview.cpp
+++ b/src/webengine/api/qquickwebengineview.cpp
@@ -705,6 +705,25 @@ const QObject *QQuickWebEngineViewPrivate::holdingQObject() const
return q;
}
+void QQuickWebEngineViewPrivate::lifecycleStateChanged(LifecycleState state)
+{
+ Q_Q(QQuickWebEngineView);
+ Q_EMIT q->lifecycleStateChanged(static_cast<QQuickWebEngineView::LifecycleState>(state));
+}
+
+void QQuickWebEngineViewPrivate::recommendedStateChanged(LifecycleState state)
+{
+ Q_Q(QQuickWebEngineView);
+ QTimer::singleShot(0, q, [q, state]() {
+ Q_EMIT q->recommendedStateChanged(static_cast<QQuickWebEngineView::LifecycleState>(state));
+ });
+}
+
+void QQuickWebEngineViewPrivate::visibleChanged(bool visible)
+{
+ Q_UNUSED(visible);
+}
+
#ifndef QT_NO_ACCESSIBILITY
QQuickWebEngineViewAccessible::QQuickWebEngineViewAccessible(QQuickWebEngineView *o)
: QAccessibleObject(o)
@@ -844,8 +863,8 @@ void QQuickWebEngineViewPrivate::initializationFinished()
for (QQuickWebEngineScript *script : qAsConst(m_userScripts))
script->d_func()->bind(profileAdapter()->userResourceController(), adapter.data());
- if (q->window() && q->isVisible())
- adapter->wasShown();
+ if (q->window())
+ adapter->setVisible(q->isVisible());
if (!m_isBeingAdopted)
return;
@@ -1617,10 +1636,8 @@ void QQuickWebEngineView::itemChange(ItemChange change, const ItemChangeData &va
Q_D(QQuickWebEngineView);
if (d && d->profileInitialized() && d->adapter->isInitialized()
&& (change == ItemSceneChange || change == ItemVisibleHasChanged)) {
- if (window() && isVisible())
- d->adapter->wasShown();
- else
- d->adapter->wasHidden();
+ if (window())
+ d->adapter->setVisible(isVisible());
}
QQuickItem::itemChange(change, value);
}
@@ -2132,6 +2149,24 @@ void QQuickWebEngineView::lazyInitialize()
d->ensureContentsAdapter();
}
+QQuickWebEngineView::LifecycleState QQuickWebEngineView::lifecycleState() const
+{
+ Q_D(const QQuickWebEngineView);
+ return static_cast<LifecycleState>(d->adapter->lifecycleState());
+}
+
+void QQuickWebEngineView::setLifecycleState(LifecycleState state)
+{
+ Q_D(QQuickWebEngineView);
+ d->adapter->setLifecycleState(static_cast<WebContentsAdapterClient::LifecycleState>(state));
+}
+
+QQuickWebEngineView::LifecycleState QQuickWebEngineView::recommendedState() const
+{
+ Q_D(const QQuickWebEngineView);
+ return static_cast<LifecycleState>(d->adapter->recommendedState());
+}
+
QQuickWebEngineFullScreenRequest::QQuickWebEngineFullScreenRequest()
: m_viewPrivate(0)
, m_toggleOn(false)
diff --git a/src/webengine/api/qquickwebengineview_p.h b/src/webengine/api/qquickwebengineview_p.h
index efbd0e3d0..3c8e1d9ec 100644
--- a/src/webengine/api/qquickwebengineview_p.h
+++ b/src/webengine/api/qquickwebengineview_p.h
@@ -109,6 +109,7 @@ private:
class Q_WEBENGINE_PRIVATE_EXPORT QQuickWebEngineView : public QQuickItem {
Q_OBJECT
+ Q_CLASSINFO("RegisterEnumClassesUnscoped", "false")
Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged FINAL)
Q_PROPERTY(QUrl icon READ icon NOTIFY iconChanged FINAL)
Q_PROPERTY(bool loading READ isLoading NOTIFY loadingChanged FINAL)
@@ -137,6 +138,9 @@ class Q_WEBENGINE_PRIVATE_EXPORT QQuickWebEngineView : public QQuickItem {
Q_PROPERTY(QQuickWebEngineTestSupport *testSupport READ testSupport WRITE setTestSupport NOTIFY testSupportChanged FINAL)
#endif
+ Q_PROPERTY(LifecycleState lifecycleState READ lifecycleState WRITE setLifecycleState NOTIFY lifecycleStateChanged REVISION 11 FINAL)
+ Q_PROPERTY(LifecycleState recommendedState READ recommendedState NOTIFY recommendedStateChanged REVISION 11 FINAL)
+
public:
QQuickWebEngineView(QQuickItem *parent = 0);
~QQuickWebEngineView();
@@ -461,6 +465,14 @@ public:
};
Q_ENUM(PrintedPageOrientation)
+ // must match WebContentsAdapterClient::LifecycleState
+ enum class LifecycleState {
+ Active,
+ Frozen,
+ Discarded,
+ };
+ Q_ENUM(LifecycleState)
+
// QmlParserStatus
void componentComplete() override;
@@ -492,6 +504,11 @@ public:
void setDevToolsView(QQuickWebEngineView *);
QQuickWebEngineView *devToolsView() const;
+ LifecycleState lifecycleState() const;
+ void setLifecycleState(LifecycleState state);
+
+ LifecycleState recommendedState() const;
+
public Q_SLOTS:
void runJavaScript(const QString&, const QJSValue & = QJSValue());
Q_REVISION(3) void runJavaScript(const QString&, quint32 worldId, const QJSValue & = QJSValue());
@@ -555,6 +572,8 @@ Q_SIGNALS:
Q_REVISION(8) void printRequested();
Q_REVISION(9) void selectClientCertificate(QQuickWebEngineClientCertificateSelection *clientCertSelection);
Q_REVISION(10) void tooltipRequested(QQuickWebEngineTooltipRequest *request);
+ Q_REVISION(11) void lifecycleStateChanged(LifecycleState state);
+ Q_REVISION(11) void recommendedStateChanged(LifecycleState state);
#if QT_CONFIG(webengine_testsupport)
void testSupportChanged();
diff --git a/src/webengine/api/qquickwebengineview_p_p.h b/src/webengine/api/qquickwebengineview_p_p.h
index aa0a765f8..134d8237b 100644
--- a/src/webengine/api/qquickwebengineview_p_p.h
+++ b/src/webengine/api/qquickwebengineview_p_p.h
@@ -101,6 +101,9 @@ public:
QtWebEngineCore::RenderWidgetHostViewQtDelegate* CreateRenderWidgetHostViewQtDelegate(QtWebEngineCore::RenderWidgetHostViewQtDelegateClient *client) override;
QtWebEngineCore::RenderWidgetHostViewQtDelegate* CreateRenderWidgetHostViewQtDelegateForPopup(QtWebEngineCore::RenderWidgetHostViewQtDelegateClient *client) override;
void initializationFinished() override;
+ void lifecycleStateChanged(LifecycleState state) override;
+ void recommendedStateChanged(LifecycleState state) override;
+ void visibleChanged(bool visible) override;
void titleChanged(const QString&) override;
void urlChanged(const QUrl&) override;
void iconChanged(const QUrl&) override;
diff --git a/src/webengine/doc/src/webengineview_lgpl.qdoc b/src/webengine/doc/src/webengineview_lgpl.qdoc
index 9095fac65..55b3b6efd 100644
--- a/src/webengine/doc/src/webengineview_lgpl.qdoc
+++ b/src/webengine/doc/src/webengineview_lgpl.qdoc
@@ -1505,3 +1505,56 @@
\sa TooltipRequest
*/
+
+/*!
+ \qmlproperty enumeration WebEngineView::LifecycleState
+ \since QtWebEngine 1.11
+
+ This enum describes the lifecycle state of the page:
+
+ \value WebEngineView.LifecycleState.Active
+ Normal state.
+ \value WebEngineView.LifecycleState.Frozen
+ Low CPU usage state where most HTML task sources are suspended.
+ \value WebEngineView.LifecycleState.Discarded
+ Very low resource usage state where the entire browsing context is discarded.
+
+ \sa lifecycleState
+*/
+
+/*!
+ \qmlproperty LifecycleState WebEngineView::lifecycleState
+ \since QtWebEngine 1.11
+
+ \brief The current lifecycle state of the page.
+
+ The following restrictions are enforced by the setter:
+
+ \list
+ \li A visible page must remain in the \c{Active} state.
+ \li If the page is being inspected by a \l{devToolsView} then both pages must
+ remain in the \c{Active} states.
+ \li A page in the \c{Discarded} state can only transition to the \c{Active}
+ state. This will cause a reload of the page.
+ \endlist
+
+ These are the only hard limits on the lifecycle state, but see also
+ \l{recommendedState} for the recommended soft limits.
+
+ \sa recommendedState, {WebEngine Lifecycle Example}
+*/
+
+/*!
+ \qmlproperty LifecycleState WebEngineView::recommendedState
+ \since QtWebEngine 1.11
+
+ \brief The recommended limit for the lifecycle state of the page.
+
+ Setting the lifecycle state to a lower resource usage state than the
+ recommended state may cause side-effects such as stopping background audio
+ playback or loss of HTML form input. Setting the lifecycle state to a higher
+ resource state is however completely safe.
+
+ \sa lifecycleState, {WebEngine Lifecycle Example}
+
+*/
diff --git a/src/webengine/plugin/plugin.cpp b/src/webengine/plugin/plugin.cpp
index bd367dea2..ad49d6543 100644
--- a/src/webengine/plugin/plugin.cpp
+++ b/src/webengine/plugin/plugin.cpp
@@ -96,6 +96,7 @@ public:
qmlRegisterType<QQuickWebEngineView, 8>(uri, 1, 8, "WebEngineView");
qmlRegisterType<QQuickWebEngineView, 9>(uri, 1, 9, "WebEngineView");
qmlRegisterType<QQuickWebEngineView, 10>(uri, 1, 10, "WebEngineView");
+ qmlRegisterType<QQuickWebEngineView, 11>(uri, 1, 11, "WebEngineView");
qmlRegisterType<QQuickWebEngineProfile>(uri, 1, 1, "WebEngineProfile");
qmlRegisterType<QQuickWebEngineProfile, 1>(uri, 1, 2, "WebEngineProfile");
qmlRegisterType<QQuickWebEngineProfile, 2>(uri, 1, 3, "WebEngineProfile");
diff --git a/src/webenginewidgets/api/qwebenginepage.cpp b/src/webenginewidgets/api/qwebenginepage.cpp
index 86736d42b..893200826 100644
--- a/src/webenginewidgets/api/qwebenginepage.cpp
+++ b/src/webenginewidgets/api/qwebenginepage.cpp
@@ -171,11 +171,11 @@ QWebEnginePagePrivate::QWebEnginePagePrivate(QWebEngineProfile *_profile)
qRegisterMetaType<QWebEngineQuotaRequest>();
qRegisterMetaType<QWebEngineRegisterProtocolHandlerRequest>();
- // See wasShown() and wasHidden().
+ // See setVisible().
wasShownTimer.setSingleShot(true);
QObject::connect(&wasShownTimer, &QTimer::timeout, [this](){
ensureInitialized();
- wasShown();
+ adapter->setVisible(true);
});
profile->d_ptr->addWebContentsAdapterClient(this);
@@ -214,6 +214,8 @@ void QWebEnginePagePrivate::initializationFinished()
adapter->setAudioMuted(defaultAudioMuted);
if (!qFuzzyCompare(adapter->currentZoomFactor(), defaultZoomFactor))
adapter->setZoomFactor(defaultZoomFactor);
+ if (view)
+ adapter->setVisible(view->isVisible());
scriptCollection.d->initializationFinished(adapter);
@@ -627,8 +629,6 @@ void QWebEnginePagePrivate::recreateFromSerializedHistory(QDataStream &input)
adapter = std::move(newWebContents);
adapter->setClient(this);
adapter->loadDefault();
- if (view && view->isVisible())
- wasShown();
}
}
@@ -1585,31 +1585,6 @@ bool QWebEnginePage::event(QEvent *e)
return QObject::event(e);
}
-void QWebEnginePagePrivate::wasShown()
-{
- if (!adapter->isInitialized()) {
- // On the one hand, it is too early to initialize here. The application
- // may call show() before load(), or it may call show() from
- // createWindow(), and then we would create an unnecessary blank
- // WebContents here. On the other hand, if the application calls show()
- // then it expects something to be shown, so we have to initialize.
- // Therefore we have to delay the initialization via the event loop.
- wasShownTimer.start();
- return;
- }
- adapter->wasShown();
-}
-
-void QWebEnginePagePrivate::wasHidden()
-{
- if (!adapter->isInitialized()) {
- // Cancel timer from wasShown() above.
- wasShownTimer.stop();
- return;
- }
- adapter->wasHidden();
-}
-
void QWebEnginePagePrivate::contextMenuRequested(const WebEngineContextMenuData &data)
{
#if QT_CONFIG(action)
@@ -1820,6 +1795,26 @@ void QWebEnginePagePrivate::printRequested()
});
}
+void QWebEnginePagePrivate::lifecycleStateChanged(LifecycleState state)
+{
+ Q_Q(QWebEnginePage);
+ Q_EMIT q->lifecycleStateChanged(static_cast<QWebEnginePage::LifecycleState>(state));
+}
+
+void QWebEnginePagePrivate::recommendedStateChanged(LifecycleState state)
+{
+ Q_Q(QWebEnginePage);
+ QTimer::singleShot(0, q, [q, state]() {
+ Q_EMIT q->recommendedStateChanged(static_cast<QWebEnginePage::LifecycleState>(state));
+ });
+}
+
+void QWebEnginePagePrivate::visibleChanged(bool visible)
+{
+ Q_Q(QWebEnginePage);
+ Q_EMIT q->visibleChanged(visible);
+}
+
/*!
\since 5.13
@@ -2105,6 +2100,10 @@ void QWebEnginePage::runJavaScript(const QString &scriptSource)
{
Q_D(QWebEnginePage);
d->ensureInitialized();
+ if (d->adapter->lifecycleState() == WebContentsAdapter::LifecycleState::Discarded) {
+ qWarning("runJavaScript: disabled in Discarded state");
+ return;
+ }
d->adapter->runJavaScript(scriptSource, QWebEngineScript::MainWorld);
}
@@ -2112,6 +2111,11 @@ void QWebEnginePage::runJavaScript(const QString& scriptSource, const QWebEngine
{
Q_D(QWebEnginePage);
d->ensureInitialized();
+ if (d->adapter->lifecycleState() == WebContentsAdapter::LifecycleState::Discarded) {
+ qWarning("runJavaScript: disabled in Discarded state");
+ d->m_callbacks.invokeEmpty(resultCallback);
+ return;
+ }
quint64 requestId = d->adapter->runJavaScriptCallbackResult(scriptSource, QWebEngineScript::MainWorld);
d->m_callbacks.registerCallback(requestId, resultCallback);
}
@@ -2490,6 +2494,120 @@ const QWebEngineContextMenuData &QWebEnginePage::contextMenuData() const
return d->contextData;
}
+/*!
+ \enum QWebEnginePage::LifecycleState
+ \since 5.14
+
+ This enum describes the lifecycle state of the page:
+
+ \value Active
+ Normal state.
+ \value Frozen
+ Low CPU usage state where most HTML task sources are suspended.
+ \value Discarded
+ Very low resource usage state where the entire browsing context is discarded.
+
+ \sa lifecycleState, {WebEngine Lifecycle Example}
+*/
+
+/*!
+ \property QWebEnginePage::lifecycleState
+ \since 5.14
+
+ \brief The current lifecycle state of the page.
+
+ The following restrictions are enforced by the setter:
+
+ \list
+ \li A \l{visible} page must remain in the \c{Active} state.
+ \li If the page is being inspected by a \l{devToolsPage} then both pages must
+ remain in the \c{Active} states.
+ \li A page in the \c{Discarded} state can only transition to the \c{Active}
+ state. This will cause a reload of the page.
+ \endlist
+
+ These are the only hard limits on the lifecycle state, but see also
+ \l{recommendedState} for the recommended soft limits.
+
+ \sa recommendedState, {WebEngine Lifecycle Example}
+*/
+
+QWebEnginePage::LifecycleState QWebEnginePage::lifecycleState() const
+{
+ Q_D(const QWebEnginePage);
+ return static_cast<LifecycleState>(d->adapter->lifecycleState());
+}
+
+void QWebEnginePage::setLifecycleState(LifecycleState state)
+{
+ Q_D(QWebEnginePage);
+ d->adapter->setLifecycleState(static_cast<WebContentsAdapterClient::LifecycleState>(state));
+}
+
+/*!
+ \property QWebEnginePage::recommendedState
+ \since 5.14
+
+ \brief The recommended limit for the lifecycle state of the page.
+
+ Setting the lifecycle state to a lower resource usage state than the
+ recommended state may cause side-effects such as stopping background audio
+ playback or loss of HTML form input. Setting the lifecycle state to a higher
+ resource state is however completely safe.
+
+ \sa lifecycleState
+*/
+
+QWebEnginePage::LifecycleState QWebEnginePage::recommendedState() const
+{
+ Q_D(const QWebEnginePage);
+ return static_cast<LifecycleState>(d->adapter->recommendedState());
+}
+
+/*!
+ \property QWebEnginePage::visible
+ \since 5.14
+
+ \brief Whether the page is considered visible in the Page Visibility API.
+
+ Setting this property changes the \c{Document.hidden} and the
+ \c{Document.visibilityState} properties in JavaScript which web sites can use
+ to voluntarily reduce their resource usage if they are not visible to the
+ user.
+
+ If the page is connected to a \l{view} then this property will be managed
+ automatically by the view according to it's own visibility.
+
+ \sa lifecycleState
+*/
+
+bool QWebEnginePage::isVisible() const
+{
+ Q_D(const QWebEnginePage);
+ return d->adapter->isVisible();
+}
+
+void QWebEnginePage::setVisible(bool visible)
+{
+ Q_D(QWebEnginePage);
+
+ if (!d->adapter->isInitialized()) {
+ // On the one hand, it is too early to initialize here. The application
+ // may call show() before load(), or it may call show() from
+ // createWindow(), and then we would create an unnecessary blank
+ // WebContents here. On the other hand, if the application calls show()
+ // then it expects something to be shown, so we have to initialize.
+ // Therefore we have to delay the initialization via the event loop.
+ if (visible)
+ d->wasShownTimer.start();
+ else
+ d->wasShownTimer.stop();
+ return;
+ }
+
+ d->adapter->setVisible(visible);
+}
+
#if QT_CONFIG(action)
QContextMenuBuilder::QContextMenuBuilder(const QtWebEngineCore::WebEngineContextMenuData &data,
QWebEnginePage *page,
diff --git a/src/webenginewidgets/api/qwebenginepage.h b/src/webenginewidgets/api/qwebenginepage.h
index dae41d0ec..736d7ed69 100644
--- a/src/webenginewidgets/api/qwebenginepage.h
+++ b/src/webenginewidgets/api/qwebenginepage.h
@@ -88,6 +88,9 @@ class QWEBENGINEWIDGETS_EXPORT QWebEnginePage : public QObject {
Q_PROPERTY(QPointF scrollPosition READ scrollPosition NOTIFY scrollPositionChanged)
Q_PROPERTY(bool audioMuted READ isAudioMuted WRITE setAudioMuted NOTIFY audioMutedChanged)
Q_PROPERTY(bool recentlyAudible READ recentlyAudible NOTIFY recentlyAudibleChanged)
+ Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged)
+ Q_PROPERTY(LifecycleState lifecycleState READ lifecycleState WRITE setLifecycleState NOTIFY lifecycleStateChanged)
+ Q_PROPERTY(LifecycleState recommendedState READ recommendedState NOTIFY recommendedStateChanged)
public:
enum WebAction {
@@ -222,6 +225,14 @@ public:
};
Q_ENUM(RenderProcessTerminationStatus)
+ // must match WebContentsAdapterClient::LifecycleState
+ enum class LifecycleState {
+ Active,
+ Frozen,
+ Discarded,
+ };
+ Q_ENUM(LifecycleState)
+
explicit QWebEnginePage(QObject *parent = Q_NULLPTR);
QWebEnginePage(QWebEngineProfile *profile, QObject *parent = Q_NULLPTR);
~QWebEnginePage();
@@ -307,6 +318,14 @@ public:
const QWebEngineContextMenuData &contextMenuData() const;
+ LifecycleState lifecycleState() const;
+ void setLifecycleState(LifecycleState state);
+
+ LifecycleState recommendedState() const;
+
+ bool isVisible() const;
+ void setVisible(bool visible);
+
Q_SIGNALS:
void loadStarted();
void loadProgress(int progress);
@@ -345,6 +364,11 @@ Q_SIGNALS:
void pdfPrintingFinished(const QString &filePath, bool success);
void printRequested();
+ void visibleChanged(bool visible);
+
+ void lifecycleStateChanged(LifecycleState state);
+ void recommendedStateChanged(LifecycleState state);
+
protected:
virtual QWebEnginePage *createWindow(WebWindowType type);
virtual QStringList chooseFiles(FileSelectionMode mode, const QStringList &oldFiles, const QStringList &acceptedMimeTypes);
diff --git a/src/webenginewidgets/api/qwebenginepage_p.h b/src/webenginewidgets/api/qwebenginepage_p.h
index 5feefeb0e..059da8e4c 100644
--- a/src/webenginewidgets/api/qwebenginepage_p.h
+++ b/src/webenginewidgets/api/qwebenginepage_p.h
@@ -92,6 +92,9 @@ public:
QtWebEngineCore::RenderWidgetHostViewQtDelegate* CreateRenderWidgetHostViewQtDelegate(QtWebEngineCore::RenderWidgetHostViewQtDelegateClient *client) override;
QtWebEngineCore::RenderWidgetHostViewQtDelegate* CreateRenderWidgetHostViewQtDelegateForPopup(QtWebEngineCore::RenderWidgetHostViewQtDelegateClient *client) override { return CreateRenderWidgetHostViewQtDelegate(client); }
void initializationFinished() override;
+ void lifecycleStateChanged(LifecycleState state) override;
+ void recommendedStateChanged(LifecycleState state) override;
+ void visibleChanged(bool visible) override;
void titleChanged(const QString&) override;
void urlChanged(const QUrl&) override;
void iconChanged(const QUrl&) override;
@@ -166,9 +169,6 @@ public:
void updateAction(QWebEnginePage::WebAction) const;
void _q_webActionTriggered(bool checked);
- void wasShown();
- void wasHidden();
-
QtWebEngineCore::WebContentsAdapter *webContents() { return adapter.data(); }
void recreateFromSerializedHistory(QDataStream &input);
diff --git a/src/webenginewidgets/api/qwebengineview.cpp b/src/webenginewidgets/api/qwebengineview.cpp
index 6c08df343..ac979e766 100644
--- a/src/webenginewidgets/api/qwebengineview.cpp
+++ b/src/webenginewidgets/api/qwebengineview.cpp
@@ -378,7 +378,7 @@ void QWebEngineView::contextMenuEvent(QContextMenuEvent *event)
void QWebEngineView::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
- page()->d_ptr->wasShown();
+ page()->setVisible(true);
}
/*!
@@ -387,7 +387,17 @@ void QWebEngineView::showEvent(QShowEvent *event)
void QWebEngineView::hideEvent(QHideEvent *event)
{
QWidget::hideEvent(event);
- page()->d_ptr->wasHidden();
+ page()->setVisible(false);
+}
+
+/*!
+ * \reimp
+ */
+void QWebEngineView::closeEvent(QCloseEvent *event)
+{
+ QWidget::closeEvent(event);
+ page()->setVisible(false);
+ page()->setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
}
#if QT_CONFIG(draganddrop)
diff --git a/src/webenginewidgets/api/qwebengineview.h b/src/webenginewidgets/api/qwebengineview.h
index e3cb7ad75..63a68f46c 100644
--- a/src/webenginewidgets/api/qwebengineview.h
+++ b/src/webenginewidgets/api/qwebengineview.h
@@ -126,6 +126,7 @@ protected:
bool event(QEvent*) override;
void showEvent(QShowEvent *) override;
void hideEvent(QHideEvent *) override;
+ void closeEvent(QCloseEvent *) override;
#if QT_CONFIG(draganddrop)
void dragEnterEvent(QDragEnterEvent *e) override;
void dragLeaveEvent(QDragLeaveEvent *e) override;
diff --git a/tests/auto/quick/publicapi/tst_publicapi.cpp b/tests/auto/quick/publicapi/tst_publicapi.cpp
index 3d4506b38..8b8eb8636 100644
--- a/tests/auto/quick/publicapi/tst_publicapi.cpp
+++ b/tests/auto/quick/publicapi/tst_publicapi.cpp
@@ -606,6 +606,9 @@ static const QStringList expectedAPI = QStringList()
<< "QQuickWebEngineView.LetterExtra --> PrintedPageSizeId"
<< "QQuickWebEngineView.LetterPlus --> PrintedPageSizeId"
<< "QQuickWebEngineView.LetterSmall --> PrintedPageSizeId"
+ << "QQuickWebEngineView.LifecycleState.Active --> LifecycleState"
+ << "QQuickWebEngineView.LifecycleState.Discarded --> LifecycleState"
+ << "QQuickWebEngineView.LifecycleState.Frozen --> LifecycleState"
<< "QQuickWebEngineView.LinkClickedNavigation --> NavigationType"
<< "QQuickWebEngineView.LoadFailedStatus --> LoadStatus"
<< "QQuickWebEngineView.LoadStartedStatus --> LoadStatus"
@@ -703,6 +706,8 @@ static const QStringList expectedAPI = QStringList()
<< "QQuickWebEngineView.isFullScreenChanged() --> void"
<< "QQuickWebEngineView.javaScriptConsoleMessage(JavaScriptConsoleMessageLevel,QString,int,QString) --> void"
<< "QQuickWebEngineView.javaScriptDialogRequested(QQuickWebEngineJavaScriptDialogRequest*) --> void"
+ << "QQuickWebEngineView.lifecycleState --> LifecycleState"
+ << "QQuickWebEngineView.lifecycleStateChanged(LifecycleState) --> void"
<< "QQuickWebEngineView.linkHovered(QUrl) --> void"
<< "QQuickWebEngineView.loadHtml(QString) --> void"
<< "QQuickWebEngineView.loadHtml(QString,QUrl) --> void"
@@ -726,6 +731,8 @@ static const QStringList expectedAPI = QStringList()
<< "QQuickWebEngineView.quotaRequested(QWebEngineQuotaRequest) --> void"
<< "QQuickWebEngineView.recentlyAudible --> bool"
<< "QQuickWebEngineView.recentlyAudibleChanged(bool) --> void"
+ << "QQuickWebEngineView.recommendedState --> LifecycleState"
+ << "QQuickWebEngineView.recommendedStateChanged(LifecycleState) --> void"
<< "QQuickWebEngineView.registerProtocolHandlerRequested(QWebEngineRegisterProtocolHandlerRequest) --> void"
<< "QQuickWebEngineView.reload() --> void"
<< "QQuickWebEngineView.reloadAndBypassCache() --> void"
@@ -823,8 +830,9 @@ static void checkKnownType(const QByteArray &typeName)
static void gatherAPI(const QString &prefix, const QMetaEnum &metaEnum, QStringList *output)
{
+ const auto format = metaEnum.isScoped() ? "%1%3.%2 --> %3" : "%1%2 --> %3";
for (int i = 0; i < metaEnum.keyCount(); ++i)
- *output << QString::fromLatin1("%1%2 --> %3").arg(prefix).arg(metaEnum.key(i)).arg(metaEnum.name());
+ *output << QString::fromLatin1(format).arg(prefix).arg(metaEnum.key(i)).arg(metaEnum.name());
}
static void gatherAPI(const QString &prefix, const QMetaProperty &property, QStringList *output)
diff --git a/tests/auto/widgets/qwebenginepage/resources/lifecycle.html b/tests/auto/widgets/qwebenginepage/resources/lifecycle.html
new file mode 100644
index 000000000..aa477a359
--- /dev/null
+++ b/tests/auto/widgets/qwebenginepage/resources/lifecycle.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Lifecycle</title>
+ <script>
+ let frozenness = 0;
+ document.addEventListener("freeze", function() {
+ frozenness += 1;
+ });
+ document.addEventListener("resume", function() {
+ frozenness -= 1;
+ });
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp b/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp
index 985d3edf7..90a881ff7 100644
--- a/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp
+++ b/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.cpp
@@ -202,6 +202,19 @@ private Q_SLOTS:
void sendNotification();
void contentsSize();
+ void setLifecycleState();
+ void setVisible();
+ void discardPreservesProperties();
+ void discardBeforeInitialization();
+ void automaticUndiscard();
+ void setLifecycleStateWithDevTools();
+ void discardPreservesCommittedLoad();
+ void discardAbortsPendingLoad();
+ void discardAbortsPendingLoadAndPreservesCommittedLoad();
+ void recommendedState();
+ void recommendedStateAuto();
+ void setLifecycleStateAndReload();
+
private:
static QPoint elementCenter(QWebEnginePage *page, const QString &id);
@@ -3386,6 +3399,645 @@ void tst_QWebEnginePage::contentsSize()
QCOMPARE(m_page->contentsSize().height(), 1216);
}
+void tst_QWebEnginePage::setLifecycleState()
+{
+ qRegisterMetaType<QWebEnginePage::LifecycleState>("LifecycleState");
+
+ QWebEngineProfile profile;
+ QWebEnginePage page(&profile);
+ QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished);
+ QSignalSpy lifecycleSpy(&page, &QWebEnginePage::lifecycleStateChanged);
+ QSignalSpy visibleSpy(&page, &QWebEnginePage::visibleChanged);
+
+ page.load(QStringLiteral("qrc:/resources/lifecycle.html"));
+ QTRY_COMPARE(loadSpy.count(), 1);
+ QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
+ QCOMPARE(lifecycleSpy.count(), 0);
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QCOMPARE(visibleSpy.count(), 0);
+ QCOMPARE(page.isVisible(), false);
+ QCOMPARE(evaluateJavaScriptSync(&page, "document.wasDiscarded"), QVariant(false));
+ QCOMPARE(evaluateJavaScriptSync(&page, "frozenness"), QVariant(0));
+
+ // Active -> Frozen
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
+ QCOMPARE(lifecycleSpy.count(), 1);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Frozen));
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Frozen);
+ QCOMPARE(visibleSpy.count(), 0);
+ QCOMPARE(page.isVisible(), false);
+ QCOMPARE(evaluateJavaScriptSync(&page, "document.wasDiscarded"), QVariant(false));
+ QCOMPARE(evaluateJavaScriptSync(&page, "frozenness"), QVariant(1));
+
+ // Frozen -> Active
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Active);
+ QCOMPARE(lifecycleSpy.count(), 1);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QCOMPARE(visibleSpy.count(), 0);
+ QCOMPARE(page.isVisible(), false);
+ QCOMPARE(evaluateJavaScriptSync(&page, "document.wasDiscarded"), QVariant(false));
+ QCOMPARE(evaluateJavaScriptSync(&page, "frozenness"), QVariant(0));
+
+ // Active -> Discarded
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ QCOMPARE(lifecycleSpy.count(), 1);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Discarded));
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Discarded);
+ QCOMPARE(visibleSpy.count(), 0);
+ QCOMPARE(page.isVisible(), false);
+ QTest::ignoreMessage(QtWarningMsg, "runJavaScript: disabled in Discarded state");
+ QCOMPARE(evaluateJavaScriptSync(&page, "document.wasDiscarded"), QVariant());
+ QTest::ignoreMessage(QtWarningMsg, "runJavaScript: disabled in Discarded state");
+ QCOMPARE(evaluateJavaScriptSync(&page, "frozenness"), QVariant());
+ QCOMPARE(loadSpy.count(), 0);
+
+ // Discarded -> Frozen (illegal!)
+ QTest::ignoreMessage(QtWarningMsg,
+ "setLifecycleState: failed to transition from Discarded to Frozen state: "
+ "illegal transition");
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
+ QCOMPARE(lifecycleSpy.count(), 0);
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Discarded);
+
+ // Discarded -> Active
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Active);
+ QTRY_COMPARE(loadSpy.count(), 1);
+ QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
+ QCOMPARE(lifecycleSpy.count(), 1);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QCOMPARE(visibleSpy.count(), 0);
+ QCOMPARE(page.isVisible(), false);
+ QCOMPARE(evaluateJavaScriptSync(&page, "document.wasDiscarded"), QVariant(true));
+ QCOMPARE(evaluateJavaScriptSync(&page, "frozenness"), QVariant(0));
+
+ // Active -> Frozen -> Discarded -> Active
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Active);
+ QCOMPARE(lifecycleSpy.count(), 3);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Frozen));
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Discarded));
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QCOMPARE(visibleSpy.count(), 0);
+ QCOMPARE(page.isVisible(), false);
+ QTRY_COMPARE(loadSpy.count(), 1);
+ QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
+ QCOMPARE(evaluateJavaScriptSync(&page, "document.wasDiscarded"), QVariant(true));
+ QCOMPARE(evaluateJavaScriptSync(&page, "frozenness"), QVariant(0));
+
+ // Reload clears document.wasDiscarded
+ page.triggerAction(QWebEnginePage::Reload);
+ QTRY_COMPARE(loadSpy.count(), 1);
+ QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
+ QCOMPARE(evaluateJavaScriptSync(&page, "document.wasDiscarded"), QVariant(false));
+}
+
+void tst_QWebEnginePage::setVisible()
+{
+ qRegisterMetaType<QWebEnginePage::LifecycleState>("LifecycleState");
+
+ QWebEngineProfile profile;
+ QWebEnginePage page(&profile);
+ QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished);
+ QSignalSpy lifecycleSpy(&page, &QWebEnginePage::lifecycleStateChanged);
+ QSignalSpy visibleSpy(&page, &QWebEnginePage::visibleChanged);
+
+ page.load(QStringLiteral("about:blank"));
+ QTRY_COMPARE(loadSpy.count(), 1);
+ QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
+ QCOMPARE(lifecycleSpy.count(), 0);
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QCOMPARE(visibleSpy.count(), 0);
+ QCOMPARE(page.isVisible(), false);
+
+ // hidden -> visible
+ page.setVisible(true);
+ QCOMPARE(lifecycleSpy.count(), 0);
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QCOMPARE(visibleSpy.count(), 1);
+ QCOMPARE(visibleSpy.takeFirst().value(0), QVariant(true));
+ QCOMPARE(page.isVisible(), true);
+
+ // Active -> Frozen (illegal)
+ QTest::ignoreMessage(
+ QtWarningMsg,
+ "setLifecycleState: failed to transition from Active to Frozen state: page is visible");
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
+ QCOMPARE(lifecycleSpy.count(), 0);
+
+ // visible -> hidden
+ page.setVisible(false);
+ QCOMPARE(lifecycleSpy.count(), 0);
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QCOMPARE(visibleSpy.count(), 1);
+ QCOMPARE(visibleSpy.takeFirst().value(0), QVariant(false));
+ QCOMPARE(page.isVisible(), false);
+
+ // Active -> Frozen
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
+ QCOMPARE(lifecycleSpy.count(), 1);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Frozen));
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Frozen);
+
+ // hidden -> visible (triggers Frozen -> Active)
+ page.setVisible(true);
+ QCOMPARE(lifecycleSpy.count(), 1);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QCOMPARE(visibleSpy.count(), 1);
+ QCOMPARE(visibleSpy.takeFirst().value(0), QVariant(true));
+ QCOMPARE(page.isVisible(), true);
+
+ // Active -> Discarded (illegal)
+ QTest::ignoreMessage(QtWarningMsg,
+ "setLifecycleState: failed to transition from Active to Discarded state: "
+ "page is visible");
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ QCOMPARE(lifecycleSpy.count(), 0);
+
+ // visible -> hidden
+ page.setVisible(false);
+ QCOMPARE(lifecycleSpy.count(), 0);
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QCOMPARE(visibleSpy.count(), 1);
+ QCOMPARE(visibleSpy.takeFirst().value(0), QVariant(false));
+ QCOMPARE(page.isVisible(), false);
+
+ // Active -> Discarded
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ QCOMPARE(lifecycleSpy.count(), 1);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Discarded));
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Discarded);
+
+ // hidden -> visible (triggers Discarded -> Active)
+ page.setVisible(true);
+ QCOMPARE(lifecycleSpy.count(), 1);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QCOMPARE(visibleSpy.count(), 1);
+ QCOMPARE(visibleSpy.takeFirst().value(0), QVariant(true));
+ QCOMPARE(page.isVisible(), true);
+ QTRY_COMPARE(loadSpy.count(), 1);
+ QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
+}
+
+void tst_QWebEnginePage::discardPreservesProperties()
+{
+ QWebEngineProfile profile;
+ QWebEnginePage page(&profile);
+ QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished);
+
+ page.load(QStringLiteral("about:blank"));
+ QTRY_COMPARE(loadSpy.count(), 1);
+ QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
+
+ // Change as many properties as possible to non-default values
+ bool audioMuted = true;
+ QVERIFY(page.isAudioMuted() != audioMuted);
+ page.setAudioMuted(audioMuted);
+ QColor backgroundColor = Qt::black;
+ QVERIFY(page.backgroundColor() != backgroundColor);
+ page.setBackgroundColor(backgroundColor);
+ qreal zoomFactor = 2;
+ QVERIFY(page.zoomFactor() != zoomFactor);
+ page.setZoomFactor(zoomFactor);
+#if QT_CONFIG(webengine_webchannel)
+ QWebChannel *webChannel = new QWebChannel(&page);
+ page.setWebChannel(webChannel);
+#endif
+
+ // Take snapshot of the rest
+ QSizeF contentsSize = page.contentsSize();
+ QIcon icon = page.icon();
+ QUrl iconUrl = page.iconUrl();
+ QUrl requestedUrl = page.requestedUrl();
+ QString title = page.title();
+ QUrl url = page.url();
+
+ // History should be preserved too
+ int historyCount = page.history()->count();
+ QCOMPARE(historyCount, 1);
+ int historyIndex = page.history()->currentItemIndex();
+ QCOMPARE(historyIndex, 0);
+ QWebEngineHistoryItem historyItem = page.history()->currentItem();
+ QVERIFY(historyItem.isValid());
+
+ // Discard + undiscard
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Active);
+ QTRY_COMPARE(loadSpy.count(), 1);
+ QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
+
+ // Property changes should be preserved
+ QCOMPARE(page.isAudioMuted(), audioMuted);
+ QCOMPARE(page.backgroundColor(), backgroundColor);
+ QCOMPARE(page.contentsSize(), contentsSize);
+ QCOMPARE(page.icon(), icon);
+ QCOMPARE(page.iconUrl(), iconUrl);
+ QCOMPARE(page.requestedUrl(), requestedUrl);
+ QCOMPARE(page.title(), title);
+ QCOMPARE(page.url(), url);
+ QCOMPARE(page.zoomFactor(), zoomFactor);
+#if QT_CONFIG(webengine_webchannel)
+ QCOMPARE(page.webChannel(), webChannel);
+#endif
+ QCOMPARE(page.history()->count(), historyCount);
+ QCOMPARE(page.history()->currentItemIndex(), historyIndex);
+ QCOMPARE(page.history()->currentItem().url(), historyItem.url());
+ QCOMPARE(page.history()->currentItem().originalUrl(), historyItem.originalUrl());
+ QCOMPARE(page.history()->currentItem().title(), historyItem.title());
+}
+
+void tst_QWebEnginePage::discardBeforeInitialization()
+{
+ QWebEngineProfile profile;
+ QWebEnginePage page(&profile);
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ // The call is ignored
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+}
+
+void tst_QWebEnginePage::automaticUndiscard()
+{
+ QWebEngineProfile profile;
+ QWebEnginePage page(&profile);
+ QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished);
+
+ page.load(QStringLiteral("about:blank"));
+ QTRY_COMPARE(loadSpy.count(), 1);
+ QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
+
+ // setUrl
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ page.setUrl(QStringLiteral("qrc:/resources/lifecycle.html"));
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+
+ // setContent
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ page.setContent(QByteArrayLiteral("foo"));
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+}
+
+void tst_QWebEnginePage::setLifecycleStateWithDevTools()
+{
+ QWebEngineProfile profile;
+ QWebEnginePage inspectedPage(&profile);
+ QWebEnginePage devToolsPage(&profile);
+ QSignalSpy devToolsSpy(&devToolsPage, &QWebEnginePage::loadFinished);
+ QSignalSpy inspectedSpy(&inspectedPage, &QWebEnginePage::loadFinished);
+
+ // Ensure pages are initialized
+ inspectedPage.load(QStringLiteral("about:blank"));
+ devToolsPage.load(QStringLiteral("about:blank"));
+ QTRY_COMPARE(inspectedSpy.count(), 1);
+ QCOMPARE(inspectedSpy.takeFirst().value(0), QVariant(true));
+ QTRY_COMPARE(devToolsSpy.count(), 1);
+ QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(true));
+
+ // Open DevTools with Frozen inspectedPage
+ inspectedPage.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
+ inspectedPage.setDevToolsPage(&devToolsPage);
+ QCOMPARE(inspectedPage.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QTRY_COMPARE(devToolsSpy.count(), 1);
+ QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(true));
+ inspectedPage.setDevToolsPage(nullptr);
+
+ // Open DevTools with Discarded inspectedPage
+ inspectedPage.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ inspectedPage.setDevToolsPage(&devToolsPage);
+ QCOMPARE(inspectedPage.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QTRY_COMPARE(devToolsSpy.count(), 1);
+ QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(true));
+ QTRY_COMPARE(inspectedSpy.count(), 1);
+ QCOMPARE(inspectedSpy.takeFirst().value(0), QVariant(true));
+ inspectedPage.setDevToolsPage(nullptr);
+
+ // Open DevTools with Frozen devToolsPage
+ devToolsPage.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
+ devToolsPage.setInspectedPage(&inspectedPage);
+ QCOMPARE(devToolsPage.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QTRY_COMPARE(devToolsSpy.count(), 1);
+ QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(true));
+ devToolsPage.setInspectedPage(nullptr);
+
+ // Open DevTools with Discarded devToolsPage
+ devToolsPage.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ devToolsPage.setInspectedPage(&inspectedPage);
+ QCOMPARE(devToolsPage.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QTRY_COMPARE(devToolsSpy.count(), 2);
+ QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(false));
+ QCOMPARE(devToolsSpy.takeFirst().value(0), QVariant(true));
+ // keep DevTools open
+
+ // Try to change state while DevTools are open
+ QTest::ignoreMessage(
+ QtWarningMsg,
+ "setLifecycleState: failed to transition from Active to Frozen state: DevTools open");
+ inspectedPage.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
+ QCOMPARE(inspectedPage.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QTest::ignoreMessage(QtWarningMsg,
+ "setLifecycleState: failed to transition from Active to Discarded state: "
+ "DevTools open");
+ inspectedPage.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ QCOMPARE(inspectedPage.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QTest::ignoreMessage(
+ QtWarningMsg,
+ "setLifecycleState: failed to transition from Active to Frozen state: DevTools open");
+ devToolsPage.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
+ QCOMPARE(devToolsPage.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QTest::ignoreMessage(QtWarningMsg,
+ "setLifecycleState: failed to transition from Active to Discarded state: "
+ "DevTools open");
+ devToolsPage.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ QCOMPARE(devToolsPage.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+}
+
+void tst_QWebEnginePage::discardPreservesCommittedLoad()
+{
+ QWebEngineProfile profile;
+ QWebEnginePage page(&profile);
+ QSignalSpy loadStartedSpy(&page, &QWebEnginePage::loadStarted);
+ QSignalSpy loadFinishedSpy(&page, &QWebEnginePage::loadFinished);
+ QSignalSpy urlChangedSpy(&page, &QWebEnginePage::urlChanged);
+ QSignalSpy titleChangedSpy(&page, &QWebEnginePage::titleChanged);
+
+ QString url = QStringLiteral("qrc:/resources/lifecycle.html");
+ page.setUrl(url);
+ QTRY_COMPARE(loadStartedSpy.count(), 1);
+ loadStartedSpy.clear();
+ QTRY_COMPARE(loadFinishedSpy.count(), 1);
+ QCOMPARE(loadFinishedSpy.takeFirst().value(0), QVariant(true));
+ QCOMPARE(urlChangedSpy.count(), 1);
+ QCOMPARE(urlChangedSpy.takeFirst().value(0), QVariant(QUrl(url)));
+ QCOMPARE(page.url(), url);
+ QCOMPARE(titleChangedSpy.count(), 2);
+ QCOMPARE(titleChangedSpy.takeFirst().value(0), QVariant(url));
+ QString title = QStringLiteral("Lifecycle");
+ QCOMPARE(titleChangedSpy.takeFirst().value(0), QVariant(title));
+ QCOMPARE(page.title(), title);
+
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ QCOMPARE(loadStartedSpy.count(), 0);
+ QCOMPARE(loadFinishedSpy.count(), 0);
+ QCOMPARE(urlChangedSpy.count(), 0);
+ QCOMPARE(page.url(), QUrl(url));
+ QCOMPARE(titleChangedSpy.count(), 0);
+ QCOMPARE(page.title(), title);
+
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Active);
+ QTRY_COMPARE(loadStartedSpy.count(), 1);
+ loadStartedSpy.clear();
+ QTRY_COMPARE(loadFinishedSpy.count(), 1);
+ QCOMPARE(loadFinishedSpy.takeFirst().value(0), QVariant(true));
+ QCOMPARE(urlChangedSpy.count(), 0);
+ QCOMPARE(page.url(), url);
+ QCOMPARE(titleChangedSpy.count(), 0);
+ QCOMPARE(page.title(), title);
+}
+
+void tst_QWebEnginePage::discardAbortsPendingLoad()
+{
+ QWebEngineProfile profile;
+ QWebEnginePage page(&profile);
+ QSignalSpy loadStartedSpy(&page, &QWebEnginePage::loadStarted);
+ QSignalSpy loadFinishedSpy(&page, &QWebEnginePage::loadFinished);
+ QSignalSpy urlChangedSpy(&page, &QWebEnginePage::urlChanged);
+ QSignalSpy titleChangedSpy(&page, &QWebEnginePage::titleChanged);
+
+ connect(&page, &QWebEnginePage::loadStarted,
+ [&]() { page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded); });
+ QUrl url = QStringLiteral("qrc:/resources/lifecycle.html");
+ page.setUrl(url);
+ QTRY_COMPARE(loadStartedSpy.count(), 1);
+ loadStartedSpy.clear();
+ QTRY_COMPARE(loadFinishedSpy.count(), 1);
+ QCOMPARE(loadFinishedSpy.takeFirst().value(0), QVariant(false));
+ QCOMPARE(urlChangedSpy.count(), 2);
+ QCOMPARE(urlChangedSpy.takeFirst().value(0), QVariant(url));
+ QCOMPARE(urlChangedSpy.takeFirst().value(0), QVariant(QUrl()));
+ QCOMPARE(titleChangedSpy.count(), 0);
+ QCOMPARE(page.url(), QUrl());
+ QCOMPARE(page.title(), QString());
+
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Active);
+ QCOMPARE(loadStartedSpy.count(), 0);
+ QCOMPARE(loadFinishedSpy.count(), 0);
+ QCOMPARE(urlChangedSpy.count(), 0);
+ QCOMPARE(page.url(), QUrl());
+ QCOMPARE(page.title(), QString());
+}
+
+void tst_QWebEnginePage::discardAbortsPendingLoadAndPreservesCommittedLoad()
+{
+ QWebEngineProfile profile;
+ QWebEnginePage page(&profile);
+ QSignalSpy loadStartedSpy(&page, &QWebEnginePage::loadStarted);
+ QSignalSpy loadFinishedSpy(&page, &QWebEnginePage::loadFinished);
+ QSignalSpy urlChangedSpy(&page, &QWebEnginePage::urlChanged);
+ QSignalSpy titleChangedSpy(&page, &QWebEnginePage::titleChanged);
+
+ QString url1 = QStringLiteral("qrc:/resources/lifecycle.html");
+ page.setUrl(url1);
+ QTRY_COMPARE(loadStartedSpy.count(), 1);
+ loadStartedSpy.clear();
+ QTRY_COMPARE(loadFinishedSpy.count(), 1);
+ QCOMPARE(loadFinishedSpy.takeFirst().value(0), QVariant(true));
+ QCOMPARE(urlChangedSpy.count(), 1);
+ QCOMPARE(urlChangedSpy.takeFirst().value(0), QVariant(QUrl(url1)));
+ QCOMPARE(page.url(), url1);
+ QCOMPARE(titleChangedSpy.count(), 2);
+ QCOMPARE(titleChangedSpy.takeFirst().value(0), QVariant(url1));
+ QString title = QStringLiteral("Lifecycle");
+ QCOMPARE(titleChangedSpy.takeFirst().value(0), QVariant(title));
+ QCOMPARE(page.title(), title);
+
+ connect(&page, &QWebEnginePage::loadStarted,
+ [&]() { page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded); });
+ QString url2 = QStringLiteral("about:blank");
+ page.setUrl(url2);
+ QTRY_COMPARE(loadStartedSpy.count(), 1);
+ loadStartedSpy.clear();
+ QTRY_COMPARE(loadFinishedSpy.count(), 1);
+ QCOMPARE(loadFinishedSpy.takeFirst().value(0), QVariant(false));
+ QCOMPARE(urlChangedSpy.count(), 2);
+ QCOMPARE(urlChangedSpy.takeFirst().value(0), QVariant(QUrl(url2)));
+ QCOMPARE(urlChangedSpy.takeFirst().value(0), QVariant(QUrl(url1)));
+ QCOMPARE(titleChangedSpy.count(), 0);
+ QCOMPARE(page.url(), url1);
+ QCOMPARE(page.title(), title);
+
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Active);
+ QCOMPARE(loadStartedSpy.count(), 0);
+ QCOMPARE(loadFinishedSpy.count(), 0);
+ QCOMPARE(urlChangedSpy.count(), 0);
+ QCOMPARE(page.url(), url1);
+ QCOMPARE(page.title(), title);
+}
+
+void tst_QWebEnginePage::recommendedState()
+{
+ qRegisterMetaType<QWebEnginePage::LifecycleState>("LifecycleState");
+
+ QWebEngineProfile profile;
+ QWebEnginePage page(&profile);
+
+ struct Event {
+ enum { StateChange, RecommendationChange } key;
+ QWebEnginePage::LifecycleState value;
+ };
+ std::vector<Event> events;
+ connect(&page, &QWebEnginePage::lifecycleStateChanged, [&](QWebEnginePage::LifecycleState state) {
+ events.push_back(Event { Event::StateChange, state });
+ });
+ connect(&page, &QWebEnginePage::recommendedStateChanged, [&](QWebEnginePage::LifecycleState state) {
+ events.push_back(Event { Event::RecommendationChange, state });
+ });
+
+ page.load(QStringLiteral("qrc:/resources/lifecycle.html"));
+ QTRY_COMPARE(events.size(), 1u);
+ QCOMPARE(events[0].key, Event::RecommendationChange);
+ QCOMPARE(events[0].value, QWebEnginePage::LifecycleState::Frozen);
+ events.clear();
+ QCOMPARE(page.recommendedState(), QWebEnginePage::LifecycleState::Frozen);
+
+ page.setVisible(true);
+ QTRY_COMPARE(events.size(), 1u);
+ QCOMPARE(events[0].key, Event::RecommendationChange);
+ QCOMPARE(events[0].value, QWebEnginePage::LifecycleState::Active);
+ events.clear();
+ QCOMPARE(page.recommendedState(), QWebEnginePage::LifecycleState::Active);
+
+ page.setVisible(false);
+ QTRY_COMPARE(events.size(), 1u);
+ QCOMPARE(events[0].key, Event::RecommendationChange);
+ QCOMPARE(events[0].value, QWebEnginePage::LifecycleState::Frozen);
+ events.clear();
+ QCOMPARE(page.recommendedState(), QWebEnginePage::LifecycleState::Frozen);
+
+ page.triggerAction(QWebEnginePage::Reload);
+ QTRY_COMPARE(events.size(), 2u);
+ QCOMPARE(events[0].key, Event::RecommendationChange);
+ QCOMPARE(events[0].value, QWebEnginePage::LifecycleState::Active);
+ QCOMPARE(events[1].key, Event::RecommendationChange);
+ QCOMPARE(events[1].value, QWebEnginePage::LifecycleState::Frozen);
+ events.clear();
+ QCOMPARE(page.recommendedState(), QWebEnginePage::LifecycleState::Frozen);
+
+ QWebEnginePage devTools;
+ page.setDevToolsPage(&devTools);
+ QTRY_COMPARE(events.size(), 1u);
+ QCOMPARE(events[0].key, Event::RecommendationChange);
+ QCOMPARE(events[0].value, QWebEnginePage::LifecycleState::Active);
+ events.clear();
+ QCOMPARE(page.recommendedState(), QWebEnginePage::LifecycleState::Active);
+
+ page.setDevToolsPage(nullptr);
+ QTRY_COMPARE(events.size(), 1u);
+ QCOMPARE(events[0].key, Event::RecommendationChange);
+ QCOMPARE(events[0].value, QWebEnginePage::LifecycleState::Frozen);
+ events.clear();
+ QCOMPARE(page.recommendedState(), QWebEnginePage::LifecycleState::Frozen);
+
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
+ QTRY_COMPARE(events.size(), 2u);
+ QCOMPARE(events[0].key, Event::StateChange);
+ QCOMPARE(events[0].value, QWebEnginePage::LifecycleState::Frozen);
+ QCOMPARE(events[1].key, Event::RecommendationChange);
+ QCOMPARE(events[1].value, QWebEnginePage::LifecycleState::Discarded);
+ events.clear();
+ QCOMPARE(page.recommendedState(), QWebEnginePage::LifecycleState::Discarded);
+
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ QTRY_COMPARE(events.size(), 1u);
+ QCOMPARE(events[0].key, Event::StateChange);
+ QCOMPARE(events[0].value, QWebEnginePage::LifecycleState::Discarded);
+ events.clear();
+ QCOMPARE(page.recommendedState(), QWebEnginePage::LifecycleState::Discarded);
+}
+
+void tst_QWebEnginePage::recommendedStateAuto()
+{
+ qRegisterMetaType<QWebEnginePage::LifecycleState>("LifecycleState");
+
+ QWebEngineProfile profile;
+ QWebEnginePage page(&profile);
+ QSignalSpy lifecycleSpy(&page, &QWebEnginePage::lifecycleStateChanged);
+ connect(&page, &QWebEnginePage::recommendedStateChanged, &page, &QWebEnginePage::setLifecycleState);
+
+ page.load(QStringLiteral("qrc:/resources/lifecycle.html"));
+ QTRY_COMPARE(lifecycleSpy.count(), 2);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Frozen));
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Discarded));
+
+ page.setVisible(true);
+ QTRY_COMPARE(lifecycleSpy.count(), 1);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
+
+ page.setVisible(false);
+ QTRY_COMPARE(lifecycleSpy.count(), 2);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Frozen));
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Discarded));
+
+ page.triggerAction(QWebEnginePage::Reload);
+ QTRY_COMPARE(lifecycleSpy.count(), 3);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Frozen));
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Discarded));
+
+ QWebEnginePage devTools;
+ page.setDevToolsPage(&devTools);
+ QTRY_COMPARE(lifecycleSpy.count(), 1);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
+
+ page.setDevToolsPage(nullptr);
+ QTRY_COMPARE(lifecycleSpy.count(), 2);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Frozen));
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Discarded));
+}
+
+void tst_QWebEnginePage::setLifecycleStateAndReload()
+{
+ qRegisterMetaType<QWebEnginePage::LifecycleState>("LifecycleState");
+
+ QWebEngineProfile profile;
+ QWebEnginePage page(&profile);
+ QSignalSpy loadSpy(&page, &QWebEnginePage::loadFinished);
+ QSignalSpy lifecycleSpy(&page, &QWebEnginePage::lifecycleStateChanged);
+
+ page.load(QStringLiteral("qrc:/resources/lifecycle.html"));
+ QTRY_COMPARE(loadSpy.count(), 1);
+ QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
+ QCOMPARE(lifecycleSpy.count(), 0);
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Frozen);
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Frozen);
+ QCOMPARE(lifecycleSpy.count(), 1);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Frozen));
+
+ page.triggerAction(QWebEnginePage::Reload);
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QCOMPARE(lifecycleSpy.count(), 1);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
+ QTRY_COMPARE(loadSpy.count(), 1);
+ QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
+
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Discarded);
+ QCOMPARE(lifecycleSpy.count(), 1);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Discarded));
+
+ page.triggerAction(QWebEnginePage::Reload);
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ QCOMPARE(lifecycleSpy.count(), 1);
+ QCOMPARE(lifecycleSpy.takeFirst().value(0), QVariant::fromValue(QWebEnginePage::LifecycleState::Active));
+ QTRY_COMPARE(loadSpy.count(), 1);
+ QCOMPARE(loadSpy.takeFirst().value(0), QVariant(true));
+}
+
static QByteArrayList params = {QByteArrayLiteral("--use-fake-device-for-media-stream")};
W_QTEST_MAIN(tst_QWebEnginePage, params)
diff --git a/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.qrc b/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.qrc
index cf32486e7..013a307de 100644
--- a/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.qrc
+++ b/tests/auto/widgets/qwebenginepage/tst_qwebenginepage.qrc
@@ -23,6 +23,7 @@
<file>resources/foo.txt</file>
<file>resources/bar.txt</file>
<file>resources/path with spaces.txt</file>
+ <file>resources/lifecycle.html</file>
</qresource>
<qresource prefix='/shared'>
<file alias='notification.html'>../../shared/data/notification.html</file>
diff --git a/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp b/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp
index 9a2ee9311..487e70d28 100644
--- a/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp
+++ b/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp
@@ -159,6 +159,14 @@ void tst_QWebEngineScript::loadEvents()
QCOMPARE(page.eval("window.log", QWebEngineScript::MainWorld).toStringList(), expected);
QCOMPARE(page.eval("window.log", QWebEngineScript::ApplicationWorld).toStringList(), expected);
+ // After discard
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Active);
+ QTRY_COMPARE(page.spy.count(), 1);
+ QCOMPARE(page.spy.takeFirst().value(0).toBool(), true);
+ QCOMPARE(page.eval("window.log", QWebEngineScript::MainWorld).toStringList(), expected);
+ QCOMPARE(page.eval("window.log", QWebEngineScript::ApplicationWorld).toStringList(), expected);
+
// Multiple frames
page.load(QUrl("qrc:/resources/test_iframe_main.html"));
QTRY_COMPARE(page.spy.count(), 1);
@@ -531,6 +539,11 @@ void tst_QWebEngineScript::navigation()
page.setUrl(url3);
QTRY_COMPARE(spyTextChanged.count(), 3);
QCOMPARE(testObject.text(), url3);
+
+ page.setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ page.setUrl(url1);
+ QTRY_COMPARE(spyTextChanged.count(), 4);
+ QCOMPARE(testObject.text(), url1);
}
// Try to set TestObject::text to an invalid UTF-16 string.
diff --git a/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp b/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp
index d3ea27fc2..f542c09d9 100644
--- a/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp
+++ b/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp
@@ -201,6 +201,7 @@ private Q_SLOTS:
void setViewDeletesImplicitPage();
void setPagePreservesExplicitPage();
void setViewPreservesExplicitPage();
+ void closeDiscardsPage();
};
// This will be called before the first test function is executed.
@@ -2194,6 +2195,22 @@ void tst_QWebEngineView::textSelectionOutOfInputField()
QVERIFY(!view.hasSelection());
QVERIFY(view.page()->selectedText().isEmpty());
+ // Select text by ctrl+a
+ QTest::keyClick(view.windowHandle(), Qt::Key_A, Qt::ControlModifier);
+ QVERIFY(selectionChangedSpy.wait());
+ QCOMPARE(selectionChangedSpy.count(), 3);
+ QVERIFY(view.hasSelection());
+ QCOMPARE(view.page()->selectedText(), QString("This is a text"));
+
+ // Deselect text via discard+undiscard
+ view.hide();
+ view.page()->setLifecycleState(QWebEnginePage::LifecycleState::Discarded);
+ view.show();
+ QVERIFY(loadFinishedSpy.wait());
+ QCOMPARE(selectionChangedSpy.count(), 4);
+ QVERIFY(!view.hasSelection());
+ QVERIFY(view.page()->selectedText().isEmpty());
+
selectionChangedSpy.clear();
view.setHtml("<html><body>"
" This is a text"
@@ -3258,5 +3275,21 @@ void tst_QWebEngineView::setViewPreservesExplicitPage()
QVERIFY(explicitPage1); // should not be deleted
}
+void tst_QWebEngineView::closeDiscardsPage()
+{
+ QWebEngineProfile profile;
+ QWebEnginePage page(&profile);
+ QWebEngineView view;
+ view.setPage(&page);
+ view.resize(300, 300);
+ view.show();
+ QVERIFY(QTest::qWaitForWindowExposed(&view));
+ QCOMPARE(page.isVisible(), true);
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Active);
+ view.close();
+ QCOMPARE(page.isVisible(), false);
+ QCOMPARE(page.lifecycleState(), QWebEnginePage::LifecycleState::Discarded);
+}
+
QTEST_MAIN(tst_QWebEngineView)
#include "tst_qwebengineview.moc"