From a69029cf9fcfd0c1fcdaafe5cbcbff2d5dd6b5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCri=20Valdmann?= Date: Fri, 12 Apr 2019 16:44:18 +0200 Subject: 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 Reviewed-by: Peter Varga --- examples/webengine/lifecycle/WebBrowser.qml | 172 ++++++ examples/webengine/lifecycle/WebTab.qml | 212 +++++++ examples/webengine/lifecycle/WebTabBar.qml | 100 ++++ examples/webengine/lifecycle/WebTabButton.qml | 158 +++++ examples/webengine/lifecycle/WebTabStack.qml | 98 ++++ examples/webengine/lifecycle/WebToolButton.qml | 63 ++ .../lifecycle/doc/images/lifecycle-automatic.png | Bin 0 -> 8148 bytes .../lifecycle/doc/images/lifecycle-manual.png | Bin 0 -> 5855 bytes .../webengine/lifecycle/doc/images/lifecycle.png | Bin 0 -> 24494 bytes .../webengine/lifecycle/doc/src/lifecycle.qdoc | 167 ++++++ examples/webengine/lifecycle/lifecycle.pro | 10 + examples/webengine/lifecycle/main.cpp | 66 +++ examples/webengine/lifecycle/qtquickcontrols2.conf | 6 + examples/webengine/lifecycle/resources.qrc | 11 + examples/webengine/webengine.pro | 1 + src/core/devtools_frontend_qt.cpp | 4 + src/core/favicon_manager.cpp | 5 + src/core/favicon_manager.h | 1 + src/core/media_capture_devices_dispatcher.cpp | 51 +- src/core/render_widget_host_view_qt.cpp | 3 +- src/core/renderer/web_channel_ipc_transport.cpp | 4 +- src/core/web_contents_adapter.cpp | 320 +++++++++- src/core/web_contents_adapter.h | 30 +- src/core/web_contents_adapter_client.h | 15 + src/core/web_contents_delegate_qt.cpp | 133 +++++ src/core/web_contents_delegate_qt.h | 34 +- src/webengine/api/qquickwebengineview.cpp | 47 +- src/webengine/api/qquickwebengineview_p.h | 19 + src/webengine/api/qquickwebengineview_p_p.h | 3 + src/webengine/doc/src/webengineview_lgpl.qdoc | 53 ++ src/webengine/plugin/plugin.cpp | 1 + src/webenginewidgets/api/qwebenginepage.cpp | 176 +++++- src/webenginewidgets/api/qwebenginepage.h | 24 + src/webenginewidgets/api/qwebenginepage_p.h | 6 +- src/webenginewidgets/api/qwebengineview.cpp | 14 +- src/webenginewidgets/api/qwebengineview.h | 1 + tests/auto/quick/publicapi/tst_publicapi.cpp | 10 +- .../qwebenginepage/resources/lifecycle.html | 17 + .../widgets/qwebenginepage/tst_qwebenginepage.cpp | 652 +++++++++++++++++++++ .../widgets/qwebenginepage/tst_qwebenginepage.qrc | 1 + .../qwebenginescript/tst_qwebenginescript.cpp | 13 + .../widgets/qwebengineview/tst_qwebengineview.cpp | 33 ++ 42 files changed, 2652 insertions(+), 82 deletions(-) create mode 100644 examples/webengine/lifecycle/WebBrowser.qml create mode 100644 examples/webengine/lifecycle/WebTab.qml create mode 100644 examples/webengine/lifecycle/WebTabBar.qml create mode 100644 examples/webengine/lifecycle/WebTabButton.qml create mode 100644 examples/webengine/lifecycle/WebTabStack.qml create mode 100644 examples/webengine/lifecycle/WebToolButton.qml create mode 100644 examples/webengine/lifecycle/doc/images/lifecycle-automatic.png create mode 100644 examples/webengine/lifecycle/doc/images/lifecycle-manual.png create mode 100644 examples/webengine/lifecycle/doc/images/lifecycle.png create mode 100644 examples/webengine/lifecycle/doc/src/lifecycle.qdoc create mode 100644 examples/webengine/lifecycle/lifecycle.pro create mode 100644 examples/webengine/lifecycle/main.cpp create mode 100644 examples/webengine/lifecycle/qtquickcontrols2.conf create mode 100644 examples/webengine/lifecycle/resources.qrc create mode 100644 tests/auto/widgets/qwebenginepage/resources/lifecycle.html 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 Binary files /dev/null and b/examples/webengine/lifecycle/doc/images/lifecycle-automatic.png 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 Binary files /dev/null and b/examples/webengine/lifecycle/doc/images/lifecycle-manual.png 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 Binary files /dev/null and b/examples/webengine/lifecycle/doc/images/lifecycle.png 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 +#include +#include +#include +#include + +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 @@ + + + WebBrowser.qml + WebTab.qml + WebTabBar.qml + WebTabButton.qml + WebTabStack.qml + WebToolButton.qml + qtquickcontrols2.conf + + 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 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 webCon DevToolsFrontendQt::~DevToolsFrontendQt() { + if (QSharedPointer 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 getFaviconInfoList(bool) const; + void copyStateFrom(FaviconManager *source); private: void update(const QList &); 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(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 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()); + if (devices.empty()) + std::move(callback).Run(devices, blink::MEDIA_DEVICE_INVALID_STATE, + std::unique_ptr()); + else + std::move(callback).Run(devices, blink::MEDIA_DEVICE_OK, + std::make_unique(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()); + if (devices.empty()) + std::move(callback).Run(devices, blink::MEDIA_DEVICE_INVALID_STATE, + std::unique_ptr()); + else + std::move(callback).Run(devices, blink::MEDIA_DEVICE_OK, + std::make_unique(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 &binaryJson, uint32_t worldId) { - DCHECK(m_canUseContext); DCHECK(m_worldId == worldId); + if (!m_canUseContext) + return; + QJsonDocument doc = QJsonDocument::fromRawData(reinterpret_cast(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(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(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 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 frontendAdapter) { Q_ASSERT(isInitialized()); @@ -1179,7 +1210,10 @@ void WebContentsAdapter::openDevToolsFrontend(QSharedPointer 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 nullContents = content::WebContents::Create(createParams); + std::unique_ptr 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(static_cast(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(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 m_webContents; std::unique_ptr m_webContentsDelegate; @@ -246,6 +267,9 @@ private: QPointF m_lastDragScreenPos; std::unique_ptr 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 AsWeakPtr() { return m_weakPtrFactory.GetWeakPtr(); } + private: QWeakPointer createWindow(std::unique_ptr 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 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 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(state)); +} + +void QQuickWebEngineViewPrivate::recommendedStateChanged(LifecycleState state) +{ + Q_Q(QQuickWebEngineView); + QTimer::singleShot(0, q, [q, state]() { + Q_EMIT q->recommendedStateChanged(static_cast(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(d->adapter->lifecycleState()); +} + +void QQuickWebEngineView::setLifecycleState(LifecycleState state) +{ + Q_D(QQuickWebEngineView); + d->adapter->setLifecycleState(static_cast(state)); +} + +QQuickWebEngineView::LifecycleState QQuickWebEngineView::recommendedState() const +{ + Q_D(const QQuickWebEngineView); + return static_cast(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(uri, 1, 8, "WebEngineView"); qmlRegisterType(uri, 1, 9, "WebEngineView"); qmlRegisterType(uri, 1, 10, "WebEngineView"); + qmlRegisterType(uri, 1, 11, "WebEngineView"); qmlRegisterType(uri, 1, 1, "WebEngineProfile"); qmlRegisterType(uri, 1, 2, "WebEngineProfile"); qmlRegisterType(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(); qRegisterMetaType(); - // 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(state)); +} + +void QWebEnginePagePrivate::recommendedStateChanged(LifecycleState state) +{ + Q_Q(QWebEnginePage); + QTimer::singleShot(0, q, [q, state]() { + Q_EMIT q->recommendedStateChanged(static_cast(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(d->adapter->lifecycleState()); +} + +void QWebEnginePage::setLifecycleState(LifecycleState state) +{ + Q_D(QWebEnginePage); + d->adapter->setLifecycleState(static_cast(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(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 @@ + + + + Lifecycle + + + + + 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("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("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("LifecycleState"); + + QWebEngineProfile profile; + QWebEnginePage page(&profile); + + struct Event { + enum { StateChange, RecommendationChange } key; + QWebEnginePage::LifecycleState value; + }; + std::vector 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("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("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 @@ resources/foo.txt resources/bar.txt resources/path with spaces.txt + resources/lifecycle.html ../../shared/data/notification.html 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("" " 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" -- cgit v1.2.3