diff options
Diffstat (limited to 'examples/webenginequick')
40 files changed, 2767 insertions, 0 deletions
diff --git a/examples/webenginequick/CMakeLists.txt b/examples/webenginequick/CMakeLists.txt new file mode 100644 index 000000000..ec29a1e58 --- /dev/null +++ b/examples/webenginequick/CMakeLists.txt @@ -0,0 +1,5 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +qt_internal_add_example(lifecycle) +qt_internal_add_example(quicknanobrowser) diff --git a/examples/webenginequick/lifecycle/CMakeLists.txt b/examples/webenginequick/lifecycle/CMakeLists.txt new file mode 100644 index 000000000..46478b622 --- /dev/null +++ b/examples/webenginequick/lifecycle/CMakeLists.txt @@ -0,0 +1,55 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(lifecycle LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/webenginequick/lifecycle") + +find_package(Qt6 REQUIRED COMPONENTS Core Gui QuickControls2 WebEngineQuick) + +qt_add_executable(lifecycle + main.cpp + utils.h +) + +set_target_properties(lifecycle PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE +) + +target_link_libraries(lifecycle PUBLIC + Qt::Core + Qt::Gui + Qt::WebEngineQuick +) + +# Resources: +set(resources_resource_files + "WebBrowser.qml" + "WebTab.qml" + "WebTabBar.qml" + "WebTabButton.qml" + "WebTabStack.qml" + "WebToolButton.qml" + "qtquickcontrols2.conf" +) + +qt_add_resources(lifecycle "resources" + PREFIX + "/" + FILES + ${resources_resource_files} +) + +install(TARGETS lifecycle + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) diff --git a/examples/webenginequick/lifecycle/WebBrowser.qml b/examples/webenginequick/lifecycle/WebBrowser.qml new file mode 100644 index 000000000..43edcc537 --- /dev/null +++ b/examples/webenginequick/lifecycle/WebBrowser.qml @@ -0,0 +1,125 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Material +import QtQuick.Layouts +import QtQuick.Window + +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/webenginequick/lifecycle/WebTab.qml b/examples/webenginequick/lifecycle/WebTab.qml new file mode 100644 index 000000000..6fb6cb386 --- /dev/null +++ b/examples/webenginequick/lifecycle/WebTab.qml @@ -0,0 +1,165 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQml +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Material +import QtQuick.Layouts +import QtWebEngine + +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 = utils.fromUserInput(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/webenginequick/lifecycle/WebTabBar.qml b/examples/webenginequick/lifecycle/WebTabBar.qml new file mode 100644 index 000000000..e87380eb1 --- /dev/null +++ b/examples/webenginequick/lifecycle/WebTabBar.qml @@ -0,0 +1,53 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Material +import QtQuick.Layouts + +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/webenginequick/lifecycle/WebTabButton.qml b/examples/webenginequick/lifecycle/WebTabButton.qml new file mode 100644 index 000000000..c26a53f54 --- /dev/null +++ b/examples/webenginequick/lifecycle/WebTabButton.qml @@ -0,0 +1,111 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Material +import QtQuick.Layouts +import QtWebEngine + +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/webenginequick/lifecycle/WebTabStack.qml b/examples/webenginequick/lifecycle/WebTabStack.qml new file mode 100644 index 000000000..1bc16f4c9 --- /dev/null +++ b/examples/webenginequick/lifecycle/WebTabStack.qml @@ -0,0 +1,51 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQml.Models +import QtQuick +import QtQuick.Controls + +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/webenginequick/lifecycle/WebToolButton.qml b/examples/webenginequick/lifecycle/WebToolButton.qml new file mode 100644 index 000000000..bdf94b4ba --- /dev/null +++ b/examples/webenginequick/lifecycle/WebToolButton.qml @@ -0,0 +1,16 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Material + +ToolButton { + id: root + font.bold: true + font.pointSize: 12 + ToolTip.delay: 1000 + ToolTip.visible: hovered + implicitWidth: 32 + implicitHeight: 32 +} diff --git a/examples/webenginequick/lifecycle/doc/images/lifecycle-automatic.png b/examples/webenginequick/lifecycle/doc/images/lifecycle-automatic.png Binary files differnew file mode 100644 index 000000000..2c62967af --- /dev/null +++ b/examples/webenginequick/lifecycle/doc/images/lifecycle-automatic.png diff --git a/examples/webenginequick/lifecycle/doc/images/lifecycle-manual.png b/examples/webenginequick/lifecycle/doc/images/lifecycle-manual.png Binary files differnew file mode 100644 index 000000000..7fb5c5a80 --- /dev/null +++ b/examples/webenginequick/lifecycle/doc/images/lifecycle-manual.png diff --git a/examples/webenginequick/lifecycle/doc/images/lifecycle.png b/examples/webenginequick/lifecycle/doc/images/lifecycle.png Binary files differnew file mode 100644 index 000000000..87b719022 --- /dev/null +++ b/examples/webenginequick/lifecycle/doc/images/lifecycle.png diff --git a/examples/webenginequick/lifecycle/doc/src/lifecycle.qdoc b/examples/webenginequick/lifecycle/doc/src/lifecycle.qdoc new file mode 100644 index 000000000..1580da26d --- /dev/null +++ b/examples/webenginequick/lifecycle/doc/src/lifecycle.qdoc @@ -0,0 +1,84 @@ +// Copyright (C) 2019 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! + \example webenginequick/lifecycle + \title WebEngine Lifecycle Example + \ingroup webengine-examples + \brief Freezes and discards background tabs to reduce CPU and memory usage. + \examplecategory {Web Technologies} + + \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. + + For an overview of the lifecycle feature, see \l {Page Lifecycle API}. + + \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. + + \note Note that \c {WebTab.qml} uses \l {QUrl::} + {fromUserInput} to handle incomplete URLs. + + \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/webenginequick/lifecycle/lifecycle.pro b/examples/webenginequick/lifecycle/lifecycle.pro new file mode 100644 index 000000000..044d025d7 --- /dev/null +++ b/examples/webenginequick/lifecycle/lifecycle.pro @@ -0,0 +1,11 @@ +TEMPLATE = app + +QT += quickcontrols2 webenginequick + +HEADERS += utils.h +SOURCES += main.cpp + +RESOURCES += resources.qrc + +target.path = $$[QT_INSTALL_EXAMPLES]/webenginequick/lifecycle +INSTALLS += target diff --git a/examples/webenginequick/lifecycle/main.cpp b/examples/webenginequick/lifecycle/main.cpp new file mode 100644 index 000000000..1f45ad0ee --- /dev/null +++ b/examples/webenginequick/lifecycle/main.cpp @@ -0,0 +1,21 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "utils.h" + +#include <QGuiApplication> +#include <QQmlApplicationEngine> +#include <QQmlContext> +#include <QtWebEngineQuick/qtwebenginequickglobal.h> + +int main(int argc, char *argv[]) +{ + QCoreApplication::setOrganizationName("QtExamples"); + QtWebEngineQuick::initialize(); + QGuiApplication app(argc, argv); + QQmlApplicationEngine engine; + Utils utils; + engine.rootContext()->setContextProperty("utils", &utils); + engine.load(QUrl(QStringLiteral("qrc:/WebBrowser.qml"))); + return app.exec(); +} diff --git a/examples/webenginequick/lifecycle/qtquickcontrols2.conf b/examples/webenginequick/lifecycle/qtquickcontrols2.conf new file mode 100644 index 000000000..68c77cec5 --- /dev/null +++ b/examples/webenginequick/lifecycle/qtquickcontrols2.conf @@ -0,0 +1,6 @@ +[Controls] +Style=Material + +[Material] +Theme=Dark +Variant=Dense diff --git a/examples/webenginequick/lifecycle/resources.qrc b/examples/webenginequick/lifecycle/resources.qrc new file mode 100644 index 000000000..41e5092ce --- /dev/null +++ b/examples/webenginequick/lifecycle/resources.qrc @@ -0,0 +1,11 @@ +<RCC> + <qresource prefix="/"> + <file>WebBrowser.qml</file> + <file>WebTab.qml</file> + <file>WebTabBar.qml</file> + <file>WebTabButton.qml</file> + <file>WebTabStack.qml</file> + <file>WebToolButton.qml</file> + <file>qtquickcontrols2.conf</file> + </qresource> +</RCC> diff --git a/examples/webenginequick/lifecycle/utils.h b/examples/webenginequick/lifecycle/utils.h new file mode 100644 index 000000000..d9a803907 --- /dev/null +++ b/examples/webenginequick/lifecycle/utils.h @@ -0,0 +1,25 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef UTILS_H +#define UTILS_H + +#include <QtCore/QFileInfo> +#include <QtCore/QUrl> + +class Utils : public QObject +{ + Q_OBJECT +public: + Q_INVOKABLE static QUrl fromUserInput(const QString &userInput); +}; + +inline QUrl Utils::fromUserInput(const QString &userInput) +{ + QFileInfo fileInfo(userInput); + if (fileInfo.exists()) + return QUrl::fromLocalFile(fileInfo.absoluteFilePath()); + return QUrl::fromUserInput(userInput); +} + +#endif // UTILS_H diff --git a/examples/webenginequick/quicknanobrowser/ApplicationRoot.qml b/examples/webenginequick/quicknanobrowser/ApplicationRoot.qml new file mode 100644 index 000000000..55c414409 --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/ApplicationRoot.qml @@ -0,0 +1,40 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtWebEngine + +QtObject { + id: root + + property QtObject defaultProfile: WebEngineProfile { + storageName: "Profile" + offTheRecord: false + } + + property QtObject otrProfile: WebEngineProfile { + offTheRecord: true + } + + property Component browserWindowComponent: BrowserWindow { + applicationRoot: root + } + property Component browserDialogComponent: BrowserDialog { + onClosing: destroy() + } + function createWindow(profile) { + var newWindow = browserWindowComponent.createObject(root); + newWindow.currentWebView.profile = profile; + profile.downloadRequested.connect(newWindow.onDownloadRequested); + return newWindow; + } + function createDialog(profile) { + var newDialog = browserDialogComponent.createObject(root); + newDialog.currentWebView.profile = profile; + return newDialog; + } + function load(url) { + var browserWindow = createWindow(defaultProfile); + browserWindow.currentWebView.url = url; + } +} diff --git a/examples/webenginequick/quicknanobrowser/BrowserDialog.qml b/examples/webenginequick/quicknanobrowser/BrowserDialog.qml new file mode 100644 index 000000000..7af347ec3 --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/BrowserDialog.qml @@ -0,0 +1,27 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Window +import QtWebEngine + +Window { + id: window + property alias currentWebView: webView + flags: Qt.Dialog + width: 800 + height: 600 + visible: true + onClosing: destroy() + WebEngineView { + id: webView + anchors.fill: parent + + onGeometryChangeRequested: function(geometry) { + window.x = geometry.x + window.y = geometry.y + window.width = geometry.width + window.height = geometry.height + } + } +} diff --git a/examples/webenginequick/quicknanobrowser/BrowserWindow.qml b/examples/webenginequick/quicknanobrowser/BrowserWindow.qml new file mode 100644 index 000000000..3b911262b --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/BrowserWindow.qml @@ -0,0 +1,871 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtCore +import QtQml +import QtQuick +import QtQuick.Controls.Fusion +import QtQuick.Layouts +import QtQuick.Window +import QtWebEngine +import BrowserUtils + +ApplicationWindow { + id: browserWindow + property QtObject applicationRoot + property Item currentWebView: tabBar.currentIndex < tabBar.count ? tabLayout.children[tabBar.currentIndex] : null + property int previousVisibility: Window.Windowed + property int createdTabs: 0 + + width: 1300 + height: 900 + visible: true + title: currentWebView && currentWebView.title + + // Make sure the Qt.WindowFullscreenButtonHint is set on OS X. + Component.onCompleted: flags = flags | Qt.WindowFullscreenButtonHint + + onCurrentWebViewChanged: { + findBar.reset(); + } + + // When using style "mac", ToolButtons are not supposed to accept focus. + property bool platformIsMac: Qt.platform.os == "osx" + + Settings { + id : appSettings + property alias autoLoadImages: loadImages.checked + property alias javaScriptEnabled: javaScriptEnabled.checked + property alias errorPageEnabled: errorPageEnabled.checked + property alias pluginsEnabled: pluginsEnabled.checked + property alias fullScreenSupportEnabled: fullScreenSupportEnabled.checked + property alias autoLoadIconsForPage: autoLoadIconsForPage.checked + property alias touchIconsEnabled: touchIconsEnabled.checked + property alias webRTCPublicInterfacesOnly : webRTCPublicInterfacesOnly.checked + property alias devToolsEnabled: devToolsEnabled.checked + property alias pdfViewerEnabled: pdfViewerEnabled.checked + property int imageAnimationPolicy: WebEngineSettings.AllowImageAnimation + } + + Action { + shortcut: "Ctrl+D" + onTriggered: { + downloadView.visible = !downloadView.visible; + } + } + Action { + id: focus + shortcut: "Ctrl+L" + onTriggered: { + addressBar.forceActiveFocus(); + addressBar.selectAll(); + } + } + Action { + shortcut: StandardKey.Refresh + onTriggered: { + if (currentWebView) + currentWebView.reload(); + } + } + Action { + shortcut: StandardKey.AddTab + onTriggered: { + tabBar.createTab(tabBar.count != 0 ? currentWebView.profile : defaultProfile); + addressBar.forceActiveFocus(); + addressBar.selectAll(); + } + } + Action { + shortcut: StandardKey.Close + onTriggered: { + currentWebView.triggerWebAction(WebEngineView.RequestClose); + } + } + Action { + shortcut: StandardKey.Quit + onTriggered: browserWindow.close() + } + Action { + shortcut: "Escape" + onTriggered: { + if (currentWebView.state == "FullScreen") { + browserWindow.visibility = browserWindow.previousVisibility; + fullScreenNotification.hide(); + currentWebView.triggerWebAction(WebEngineView.ExitFullScreen); + } + + if (findBar.visible) + findBar.visible = false; + } + } + Action { + shortcut: "Ctrl+0" + onTriggered: currentWebView.zoomFactor = 1.0 + } + Action { + shortcut: StandardKey.ZoomOut + onTriggered: currentWebView.zoomFactor -= 0.1 + } + Action { + shortcut: StandardKey.ZoomIn + onTriggered: currentWebView.zoomFactor += 0.1 + } + + Action { + shortcut: StandardKey.Copy + onTriggered: currentWebView.triggerWebAction(WebEngineView.Copy) + } + Action { + shortcut: StandardKey.Cut + onTriggered: currentWebView.triggerWebAction(WebEngineView.Cut) + } + Action { + shortcut: StandardKey.Paste + onTriggered: currentWebView.triggerWebAction(WebEngineView.Paste) + } + Action { + shortcut: "Shift+"+StandardKey.Paste + onTriggered: currentWebView.triggerWebAction(WebEngineView.PasteAndMatchStyle) + } + Action { + shortcut: StandardKey.SelectAll + onTriggered: currentWebView.triggerWebAction(WebEngineView.SelectAll) + } + Action { + shortcut: StandardKey.Undo + onTriggered: currentWebView.triggerWebAction(WebEngineView.Undo) + } + Action { + shortcut: StandardKey.Redo + onTriggered: currentWebView.triggerWebAction(WebEngineView.Redo) + } + Action { + shortcut: StandardKey.Back + onTriggered: currentWebView.triggerWebAction(WebEngineView.Back) + } + Action { + shortcut: StandardKey.Forward + onTriggered: currentWebView.triggerWebAction(WebEngineView.Forward) + } + Action { + shortcut: StandardKey.Find + onTriggered: { + if (!findBar.visible) + findBar.visible = true; + } + } + Action { + shortcut: StandardKey.FindNext + onTriggered: findBar.findNext() + } + Action { + shortcut: StandardKey.FindPrevious + onTriggered: findBar.findPrevious() + } + + menuBar: ToolBar { + id: navigationBar + RowLayout { + anchors.fill: parent + ToolButton { + enabled: currentWebView && (currentWebView.canGoBack || currentWebView.canGoForward) + onClicked: historyMenu.open() + text: qsTr("▼") + Menu { + id: historyMenu + Instantiator { + model: currentWebView && currentWebView.history.items + MenuItem { + text: model.title + onTriggered: currentWebView.goBackOrForward(model.offset) + checkable: !enabled + checked: !enabled + enabled: model.offset + } + + onObjectAdded: function(index, object) { + historyMenu.insertItem(index, object) + } + onObjectRemoved: function(index, object) { + historyMenu.removeItem(object) + } + } + } + } + + ToolButton { + id: backButton + icon.source: "qrc:/icons/go-previous.png" + onClicked: currentWebView.goBack() + enabled: currentWebView && currentWebView.canGoBack + activeFocusOnTab: !browserWindow.platformIsMac + } + ToolButton { + id: forwardButton + icon.source: "qrc:/icons/go-next.png" + onClicked: currentWebView.goForward() + enabled: currentWebView && currentWebView.canGoForward + activeFocusOnTab: !browserWindow.platformIsMac + } + ToolButton { + id: reloadButton + icon.source: currentWebView && currentWebView.loading ? "qrc:/icons/process-stop.png" : "qrc:/icons/view-refresh.png" + onClicked: currentWebView && currentWebView.loading ? currentWebView.stop() : currentWebView.reload() + activeFocusOnTab: !browserWindow.platformIsMac + } + TextField { + id: addressBar + Image { + anchors.verticalCenter: addressBar.verticalCenter; + x: 5 + z: 2 + id: faviconImage + width: 16; height: 16 + sourceSize: Qt.size(width, height) + source: currentWebView && currentWebView.icon ? currentWebView.icon : '' + } + MouseArea { + id: textFieldMouseArea + acceptedButtons: Qt.RightButton + anchors.fill: parent + onClicked: { + var textSelectionStartPos = addressBar.selectionStart; + var textSelectionEndPos = addressBar.selectionEnd; + textFieldContextMenu.open(); + addressBar.select(textSelectionStartPos, textSelectionEndPos); + } + Menu { + id: textFieldContextMenu + x: textFieldMouseArea.mouseX + y: textFieldMouseArea.mouseY + MenuItem { + text: qsTr("Cut") + onTriggered: addressBar.cut() + enabled: addressBar.selectedText.length > 0 + } + MenuItem { + text: qsTr("Copy") + onTriggered: addressBar.copy() + enabled: addressBar.selectedText.length > 0 + } + MenuItem { + text: qsTr("Paste") + onTriggered: addressBar.paste() + enabled: addressBar.canPaste + } + MenuItem { + text: qsTr("Delete") + onTriggered: addressBar.text = qsTr("") + enabled: addressBar.selectedText.length > 0 + } + MenuSeparator {} + MenuItem { + text: qsTr("Select All") + onTriggered: addressBar.selectAll() + enabled: addressBar.text.length > 0 + } + } + } + leftPadding: 26 + focus: true + Layout.fillWidth: true + Binding on text { + when: currentWebView + value: currentWebView.url + } + onAccepted: currentWebView.url = Utils.fromUserInput(text) + selectByMouse: true + } + ToolButton { + id: settingsMenuButton + text: qsTr("⋮") + onClicked: settingsMenu.open() + Menu { + id: settingsMenu + y: settingsMenuButton.height + MenuItem { + id: loadImages + text: "Autoload images" + checkable: true + checked: WebEngine.settings.autoLoadImages + } + MenuItem { + id: javaScriptEnabled + text: "JavaScript On" + checkable: true + checked: WebEngine.settings.javascriptEnabled + } + MenuItem { + id: errorPageEnabled + text: "ErrorPage On" + checkable: true + checked: WebEngine.settings.errorPageEnabled + } + MenuItem { + id: pluginsEnabled + text: "Plugins On" + checkable: true + checked: true + } + MenuItem { + id: fullScreenSupportEnabled + text: "FullScreen On" + checkable: true + checked: WebEngine.settings.fullScreenSupportEnabled + } + MenuItem { + id: offTheRecordEnabled + text: "Off The Record" + checkable: true + checked: currentWebView && currentWebView.profile === otrProfile + onToggled: function(checked) { + if (currentWebView) { + currentWebView.profile = checked ? otrProfile : defaultProfile; + } + } + } + MenuItem { + id: httpDiskCacheEnabled + text: "HTTP Disk Cache" + checkable: currentWebView && !currentWebView.profile.offTheRecord + checked: currentWebView && (currentWebView.profile.httpCacheType === WebEngineProfile.DiskHttpCache) + onToggled: function(checked) { + if (currentWebView) { + currentWebView.profile.httpCacheType = checked ? WebEngineProfile.DiskHttpCache : WebEngineProfile.MemoryHttpCache; + } + } + } + MenuItem { + id: autoLoadIconsForPage + text: "Icons On" + checkable: true + checked: WebEngine.settings.autoLoadIconsForPage + } + MenuItem { + id: touchIconsEnabled + text: "Touch Icons On" + checkable: true + checked: WebEngine.settings.touchIconsEnabled + enabled: autoLoadIconsForPage.checked + } + MenuItem { + id: webRTCPublicInterfacesOnly + text: "WebRTC Public Interfaces Only" + checkable: true + checked: WebEngine.settings.webRTCPublicInterfacesOnly + } + MenuItem { + id: devToolsEnabled + text: "Open DevTools" + checkable: true + checked: false + } + MenuItem { + id: pdfViewerEnabled + text: "PDF Viewer Enabled" + checkable: true + checked: WebEngine.settings.pdfViewerEnabled + } + + Menu { + id: imageAnimationPolicy + title: "Image Animation Policy" + + MenuItem { + id: disableImageAnimation + text: "Disable All Image Animation" + checkable: true + autoExclusive: true + checked: WebEngine.settings.imageAnimationPolicy === WebEngineSettings.DisallowImageAnimation + onTriggered: { + appSettings.imageAnimationPolicy = WebEngineSettings.DisallowImageAnimation + } + } + + MenuItem { + id: allowImageAnimation + text: "Allow All Animated Images" + checkable: true + autoExclusive: true + checked: WebEngine.settings.imageAnimationPolicy === WebEngineSettings.AllowImageAnimation + onTriggered : { + appSettings.imageAnimationPolicy = WebEngineSettings.AllowImageAnimation + } + } + + MenuItem { + id: animateImageOnce + text: "Animate Image Once" + checkable: true + autoExclusive: true + checked: WebEngine.settings.imageAnimationPolicy === WebEngineSettings.AnimateImageOnce + onTriggered : { + appSettings.imageAnimationPolicy = WebEngineSettings.AnimateImageOnce + } + } + } + + } + } + } + ProgressBar { + id: progressBar + height: 3 + anchors { + left: parent.left + top: parent.bottom + right: parent.right + leftMargin: parent.leftMargin + rightMargin: parent.rightMargin + } + background: Item {} + z: -2 + from: 0 + to: 100 + value: (currentWebView && currentWebView.loadProgress < 100) ? currentWebView.loadProgress : 0 + } + } + + StackLayout { + id: tabLayout + currentIndex: tabBar.currentIndex + + anchors.top: tabBar.bottom + anchors.bottom: devToolsView.top + anchors.left: parent.left + anchors.right: parent.right + } + + Component { + id: tabButtonComponent + + TabButton { + property color frameColor: "#999" + property color fillColor: "#eee" + property color nonSelectedColor: "#ddd" + property string tabTitle: "New Tab" + + id: tabButton + contentItem: Rectangle { + id: tabRectangle + color: tabButton.down ? fillColor : nonSelectedColor + border.width: 1 + border.color: frameColor + implicitWidth: Math.max(text.width + 30, 80) + implicitHeight: Math.max(text.height + 10, 20) + Rectangle { height: 1 ; width: parent.width ; color: frameColor} + Rectangle { height: parent.height ; width: 1; color: frameColor} + Rectangle { x: parent.width - 2; height: parent.height ; width: 1; color: frameColor} + Text { + id: text + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 6 + text: tabButton.tabTitle + elide: Text.ElideRight + color: tabButton.down ? "black" : frameColor + width: parent.width - button.background.width + } + Button { + id: button + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: 4 + height: 12 + background: Rectangle { + implicitWidth: 12 + implicitHeight: 12 + color: button.hovered ? "#ccc" : tabRectangle.color + Text {text: "x"; anchors.centerIn: parent; color: "gray"} + } + onClicked: tabButton.closeTab() + } + } + + onClicked: addressBar.text = tabLayout.itemAt(TabBar.index).url; + function closeTab() { + tabBar.removeView(TabBar.index); + } + } + } + + TabBar { + id: tabBar + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + Component.onCompleted: createTab(defaultProfile) + + function createTab(profile, focusOnNewTab = true, url = undefined) { + var webview = tabComponent.createObject(tabLayout, {profile: profile}); + var newTabButton = tabButtonComponent.createObject(tabBar, {tabTitle: Qt.binding(function () { return webview.title; })}); + tabBar.addItem(newTabButton); + if (focusOnNewTab) { + tabBar.setCurrentIndex(tabBar.count - 1); + } + if (url !== undefined) { + webview.url = url; + } + return webview; + } + + function removeView(index) { + if (tabBar.count > 1) { + tabBar.removeItem(tabBar.itemAt(index)); + tabLayout.children[index].destroy(); + } else { + browserWindow.close(); + } + } + + Component { + id: tabComponent + WebEngineView { + id: webEngineView + focus: true + + onLinkHovered: function(hoveredUrl) { + if (hoveredUrl == "") + hideStatusText.start(); + else { + statusText.text = hoveredUrl; + statusBubble.visible = true; + hideStatusText.stop(); + } + } + + states: [ + State { + name: "FullScreen" + PropertyChanges { + target: tabBar + visible: false + height: 0 + } + PropertyChanges { + target: navigationBar + visible: false + } + } + ] + settings.localContentCanAccessRemoteUrls: true + settings.localContentCanAccessFileUrls: false + settings.autoLoadImages: appSettings.autoLoadImages + settings.javascriptEnabled: appSettings.javaScriptEnabled + settings.errorPageEnabled: appSettings.errorPageEnabled + settings.pluginsEnabled: appSettings.pluginsEnabled + settings.fullScreenSupportEnabled: appSettings.fullScreenSupportEnabled + settings.autoLoadIconsForPage: appSettings.autoLoadIconsForPage + settings.touchIconsEnabled: appSettings.touchIconsEnabled + settings.webRTCPublicInterfacesOnly: appSettings.webRTCPublicInterfacesOnly + settings.pdfViewerEnabled: appSettings.pdfViewerEnabled + settings.imageAnimationPolicy: appSettings.imageAnimationPolicy + + onCertificateError: function(error) { + error.defer(); + sslDialog.enqueue(error); + } + + onNewWindowRequested: function(request) { + if (!request.userInitiated) + console.warn("Blocked a popup window."); + else if (request.destination === WebEngineNewWindowRequest.InNewTab) { + var tab = tabBar.createTab(currentWebView.profile, true, request.requestedUrl); + tab.acceptAsNewWindow(request); + } else if (request.destination === WebEngineNewWindowRequest.InNewBackgroundTab) { + var backgroundTab = tabBar.createTab(currentWebView.profile, false); + backgroundTab.acceptAsNewWindow(request); + } else if (request.destination === WebEngineNewWindowRequest.InNewDialog) { + var dialog = applicationRoot.createDialog(currentWebView.profile); + dialog.currentWebView.acceptAsNewWindow(request); + } else { + var window = applicationRoot.createWindow(currentWebView.profile); + window.currentWebView.acceptAsNewWindow(request); + } + } + + onFullScreenRequested: function(request) { + if (request.toggleOn) { + webEngineView.state = "FullScreen"; + browserWindow.previousVisibility = browserWindow.visibility; + browserWindow.showFullScreen(); + fullScreenNotification.show(); + } else { + webEngineView.state = ""; + browserWindow.visibility = browserWindow.previousVisibility; + fullScreenNotification.hide(); + } + request.accept(); + } + + onRegisterProtocolHandlerRequested: function(request) { + console.log("accepting registerProtocolHandler request for " + + request.scheme + " from " + request.origin); + request.accept(); + } + + onRenderProcessTerminated: function(terminationStatus, exitCode) { + var status = ""; + switch (terminationStatus) { + case WebEngineView.NormalTerminationStatus: + status = "(normal exit)"; + break; + case WebEngineView.AbnormalTerminationStatus: + status = "(abnormal exit)"; + break; + case WebEngineView.CrashedTerminationStatus: + status = "(crashed)"; + break; + case WebEngineView.KilledTerminationStatus: + status = "(killed)"; + break; + } + + print("Render process exited with code " + exitCode + " " + status); + reloadTimer.running = true; + } + + onSelectClientCertificate: function(selection) { + selection.certificates[0].select(); + } + + onFindTextFinished: function(result) { + if (!findBar.visible) + findBar.visible = true; + + findBar.numberOfMatches = result.numberOfMatches; + findBar.activeMatch = result.activeMatch; + } + + onLoadingChanged: function(loadRequest) { + if (loadRequest.status == WebEngineView.LoadStartedStatus) + findBar.reset(); + } + + onFeaturePermissionRequested: function(securityOrigin, feature) { + featurePermissionDialog.securityOrigin = securityOrigin; + featurePermissionDialog.feature = feature; + featurePermissionDialog.visible = true; + } + onWebAuthUxRequested: function(request) { + webAuthDialog.init(request); + } + + Timer { + id: reloadTimer + interval: 0 + running: false + repeat: false + onTriggered: currentWebView.reload() + } + } + } + } + WebEngineView { + id: devToolsView + visible: devToolsEnabled.checked + height: visible ? 400 : 0 + inspectedView: visible && tabBar.currentIndex < tabBar.count ? tabLayout.children[tabBar.currentIndex] : null + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + onNewWindowRequested: function(request) { + var tab = tabBar.createTab(currentWebView.profile); + request.openIn(tab); + } + + Timer { + id: hideTimer + interval: 0 + running: false + repeat: false + onTriggered: devToolsEnabled.checked = false + } + onWindowCloseRequested: function(request) { + // Delay hiding for keep the inspectedView set to receive the ACK message of close. + hideTimer.running = true; + } + } + Dialog { + id: sslDialog + anchors.centerIn: parent + contentWidth: Math.max(mainTextForSSLDialog.width, detailedTextForSSLDialog.width) + contentHeight: mainTextForSSLDialog.height + detailedTextForSSLDialog.height + property var certErrors: [] + // fixme: icon! + // icon: StandardIcon.Warning + standardButtons: Dialog.No | Dialog.Yes + title: "Server's certificate not trusted" + contentItem: Item { + Label { + id: mainTextForSSLDialog + text: "Do you wish to continue?" + } + Text { + id: detailedTextForSSLDialog + anchors.top: mainTextForSSLDialog.bottom + text: "If you wish so, you may continue with an unverified certificate.\n" + + "Accepting an unverified certificate means\n" + + "you may not be connected with the host you tried to connect to.\n" + + "Do you wish to override the security check and continue?" + } + } + + onAccepted: { + certErrors.shift().acceptCertificate(); + presentError(); + } + onRejected: reject() + + function reject(){ + certErrors.shift().rejectCertificate(); + presentError(); + } + function enqueue(error){ + certErrors.push(error); + presentError(); + } + function presentError(){ + visible = certErrors.length > 0 + } + } + Dialog { + id: featurePermissionDialog + anchors.centerIn: parent + width: Math.min(browserWindow.width, browserWindow.height) / 3 * 2 + contentWidth: mainTextForPermissionDialog.width + contentHeight: mainTextForPermissionDialog.height + standardButtons: Dialog.No | Dialog.Yes + title: "Permission Request" + + property var feature; + property url securityOrigin; + + contentItem: Item { + Label { + id: mainTextForPermissionDialog + text: featurePermissionDialog.questionForFeature() + } + } + + onAccepted: currentWebView && currentWebView.grantFeaturePermission(securityOrigin, feature, true) + onRejected: currentWebView && currentWebView.grantFeaturePermission(securityOrigin, feature, false) + onVisibleChanged: { + if (visible) + width = contentWidth + 20; + } + + function questionForFeature() { + var question = "Allow " + securityOrigin + " to " + + switch (feature) { + case WebEngineView.Geolocation: + question += "access your location information?"; + break; + case WebEngineView.MediaAudioCapture: + question += "access your microphone?"; + break; + case WebEngineView.MediaVideoCapture: + question += "access your webcam?"; + break; + case WebEngineView.MediaVideoCapture: + question += "access your microphone and webcam?"; + break; + case WebEngineView.MouseLock: + question += "lock your mouse cursor?"; + break; + case WebEngineView.DesktopVideoCapture: + question += "capture video of your desktop?"; + break; + case WebEngineView.DesktopAudioVideoCapture: + question += "capture audio and video of your desktop?"; + break; + case WebEngineView.Notifications: + question += "show notification on your desktop?"; + break; + case WebEngineView.ClipboardReadWrite: + question += "read from and write to your clipboard?"; + break; + case WebEngineView.LocalFontsAccess: + question += "access the fonts stored on your machine?"; + break; + default: + question += "access unknown or unsupported feature [" + feature + "] ?"; + break; + } + + return question; + } + } + + FullScreenNotification { + id: fullScreenNotification + } + + DownloadView { + id: downloadView + visible: false + anchors.fill: parent + } + + WebAuthDialog { + id: webAuthDialog + visible: false + } + + function onDownloadRequested(download) { + downloadView.visible = true; + downloadView.append(download); + download.accept(); + } + + FindBar { + id: findBar + visible: false + anchors.right: parent.right + anchors.rightMargin: 10 + anchors.top: parent.top + + onFindNext: { + if (text) + currentWebView && currentWebView.findText(text); + else if (!visible) + visible = true; + } + onFindPrevious: { + if (text) + currentWebView && currentWebView.findText(text, WebEngineView.FindBackward); + else if (!visible) + visible = true; + } + } + + + Rectangle { + id: statusBubble + color: "oldlace" + property int padding: 8 + visible: false + + anchors.left: parent.left + anchors.bottom: parent.bottom + width: statusText.paintedWidth + padding + height: statusText.paintedHeight + padding + + Text { + id: statusText + anchors.centerIn: statusBubble + elide: Qt.ElideMiddle + + Timer { + id: hideStatusText + interval: 750 + onTriggered: { + statusText.text = ""; + statusBubble.visible = false; + } + } + } + } +} diff --git a/examples/webenginequick/quicknanobrowser/CMakeLists.txt b/examples/webenginequick/quicknanobrowser/CMakeLists.txt new file mode 100644 index 000000000..7efb61127 --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/CMakeLists.txt @@ -0,0 +1,111 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(quicknanobrowser LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/webenginequick/quicknanobrowser") + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml Quick WebEngineQuick) + +qt_add_executable(quicknanobrowser + main.cpp + utils.h +) + +if(WIN32) + set_property( + TARGET quicknanobrowser + APPEND PROPERTY + SOURCES quicknanobrowser.exe.manifest) +endif() + +set_target_properties(quicknanobrowser PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_GUI_IDENTIFIER "io.qt.examples.webenginequick.quicknanobrowser" +) + +target_link_libraries(quicknanobrowser PUBLIC + Qt::Core + Qt::Gui + Qt::Qml + Qt::Quick + Qt::WebEngineQuick +) + +qt_add_qml_module(quicknanobrowser + URI BrowserUtils + VERSION 1.0 + RESOURCE_PREFIX / +) + +# Resources: +set(resources_resource_files + "ApplicationRoot.qml" + "BrowserDialog.qml" + "BrowserWindow.qml" + "DownloadView.qml" + "FindBar.qml" + "FullScreenNotification.qml" + "WebAuthDialog.qml" +) + +qt_add_resources(quicknanobrowser "resources" + PREFIX + "/" + FILES + ${resources_resource_files} +) + +set(resources1_resource_files + "icons/3rdparty/go-next.png" + "icons/3rdparty/go-previous.png" + "icons/3rdparty/process-stop.png" + "icons/3rdparty/view-refresh.png" +) + +qt_add_resources(quicknanobrowser "resources1" + PREFIX + "/icons" + BASE + "icons/3rdparty" + FILES + ${resources1_resource_files} +) + +if(TARGET Qt::Widgets) + target_link_libraries(quicknanobrowser PUBLIC + Qt::Widgets + ) +endif() + +if (APPLE) + set_target_properties(quicknanobrowser PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.cmake.macos.plist" + ) + + if (NOT CMAKE_GENERATOR STREQUAL "Xcode") + # Need to sign application for location permissions to work + if(QT_FEATURE_debug_and_release) + set(exe_path "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/") + else() + unset(exe_path) + endif() + add_custom_command(TARGET quicknanobrowser + POST_BUILD COMMAND codesign --force -s - ${exe_path}quicknanobrowser.app + ) + endif() +endif() + +install(TARGETS quicknanobrowser + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) diff --git a/examples/webenginequick/quicknanobrowser/DownloadView.qml b/examples/webenginequick/quicknanobrowser/DownloadView.qml new file mode 100644 index 000000000..421b4f55c --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/DownloadView.qml @@ -0,0 +1,127 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls.Fusion +import QtWebEngine +import QtQuick.Layouts + +Rectangle { + id: downloadView + color: "lightgray" + + ListModel { + id: downloadModel + property var downloads: [] + } + + function append(download) { + downloadModel.append(download); + downloadModel.downloads.push(download); + } + + Component { + id: downloadItemDelegate + + Rectangle { + width: listView.width + height: childrenRect.height + anchors.margins: 10 + radius: 3 + color: "transparent" + border.color: "black" + Rectangle { + id: progressBar + + property real progress: downloadModel.downloads[index] + ? downloadModel.downloads[index].receivedBytes / downloadModel.downloads[index].totalBytes : 0 + + radius: 3 + color: width == listView.width ? "green" : "#2b74c7" + width: listView.width * progress + height: cancelButton.height + + Behavior on width { + SmoothedAnimation { duration: 100 } + } + } + Rectangle { + anchors { + left: parent.left + right: parent.right + leftMargin: 20 + } + Label { + id: label + text: downloadModel.downloads[index] ? downloadModel.downloads[index].downloadDirectory + "/" + downloadModel.downloads[index].downloadFileName : qsTr("") + anchors { + verticalCenter: cancelButton.verticalCenter + left: parent.left + right: cancelButton.left + } + } + Button { + id: cancelButton + anchors.right: parent.right + icon.source: "qrc:/icons/process-stop.png" + onClicked: { + var download = downloadModel.downloads[index]; + + download.cancel(); + + downloadModel.downloads = downloadModel.downloads.filter(function (el) { + return el.id !== download.id; + }); + downloadModel.remove(index); + } + } + } + } + + } + ListView { + id: listView + anchors { + topMargin: 10 + top: parent.top + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + width: parent.width - 20 + spacing: 5 + + model: downloadModel + delegate: downloadItemDelegate + + Text { + visible: !listView.count + horizontalAlignment: Text.AlignHCenter + height: 30 + anchors { + top: parent.top + left: parent.left + right: parent.right + } + font.pixelSize: 20 + text: "No active downloads." + } + + Rectangle { + color: "gray" + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + height: 30 + Button { + id: okButton + text: "OK" + anchors.centerIn: parent + onClicked: { + downloadView.visible = false; + } + } + } + } +} diff --git a/examples/webenginequick/quicknanobrowser/FindBar.qml b/examples/webenginequick/quicknanobrowser/FindBar.qml new file mode 100644 index 000000000..409d8dcff --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/FindBar.qml @@ -0,0 +1,109 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls.Fusion +import QtQuick.Layouts + +Rectangle { + id: root + + property int numberOfMatches: 0 + property int activeMatch: 0 + property alias text: findTextField.text + + function reset() { + numberOfMatches = 0; + activeMatch = 0; + visible = false; + } + + signal findNext() + signal findPrevious() + + width: 250 + height: 35 + radius: 2 + + border.width: 1 + border.color: "black" + color: "white" + + onVisibleChanged: { + if (visible) + findTextField.forceActiveFocus(); + } + + + RowLayout { + anchors.fill: parent + anchors.topMargin: 5 + anchors.bottomMargin: 5 + anchors.leftMargin: 10 + anchors.rightMargin: 10 + + spacing: 5 + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + + TextField { + id: findTextField + anchors.fill: parent + color: "black" + background: Rectangle { + color: "transparent" + } + + onAccepted: root.findNext() + onTextChanged: root.findNext() + onActiveFocusChanged: activeFocus ? selectAll() : deselect() + } + } + + Label { + text: activeMatch + "/" + numberOfMatches + visible: findTextField.text != "" + color: "black" + } + + Rectangle { + border.width: 1 + border.color: "#ddd" + width: 2 + height: parent.height + anchors.topMargin: 5 + anchors.bottomMargin: 5 + } + + ToolButton { + text: "<" + enabled: numberOfMatches > 0 + onClicked: root.findPrevious() + contentItem: Text { + color: "black" + text: parent.text + } + } + + ToolButton { + text: ">" + enabled: numberOfMatches > 0 + onClicked: root.findNext() + contentItem: Text { + color: "black" + text: parent.text + } + } + + ToolButton { + text: "x" + onClicked: root.visible = false + contentItem: Text { + color: "black" + text: parent.text + } + } + } +} diff --git a/examples/webenginequick/quicknanobrowser/FullScreenNotification.qml b/examples/webenginequick/quicknanobrowser/FullScreenNotification.qml new file mode 100644 index 000000000..779406432 --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/FullScreenNotification.qml @@ -0,0 +1,62 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick + +Rectangle { + id: fullScreenNotification + width: 500 + height: 40 + color: "white" + radius: 7 + + visible: false + opacity: 0 + + function show() { + visible = true; + opacity = 1; + reset.start(); + } + + function hide() { + reset.stop(); + opacity = 0; + } + + Behavior on opacity { + NumberAnimation { + duration: 750 + onStopped: { + if (opacity == 0) + visible = false; + } + } + } + + Timer { + id: reset + interval: 5000 + onTriggered: hide() + } + + anchors.horizontalCenter: parent.horizontalCenter + y: 125 + + Text { + id: message + width: parent.width + + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + wrapMode: Text.WordWrap + elide: Text.ElideNone + clip: true + + text: qsTr("You are now in fullscreen mode. Press ESC to quit!") + } +} diff --git a/examples/webenginequick/quicknanobrowser/Info.cmake.macos.plist b/examples/webenginequick/quicknanobrowser/Info.cmake.macos.plist new file mode 100644 index 000000000..cac4fa1f4 --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/Info.cmake.macos.plist @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleName</key> + <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string> + <key>CFBundleExecutable</key> + <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string> + <key>CFBundleVersion</key> + <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string> + <key>CFBundleShortVersionString</key> + <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string> + <key>LSMinimumSystemVersion</key> + <string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string> + <key>NSHumanReadableCopyright</key> + <string>${MACOSX_BUNDLE_COPYRIGHT}</string> + <key>CFBundleIconFile</key> + <string>${MACOSX_BUNDLE_ICON_FILE}</string> + <key>CFBundleDevelopmentRegion</key> + <string>English</string> + <key>NSSupportsAutomaticGraphicsSwitching</key> + <true/> + <key>NSLocationUsageDescription</key> + <string>Quick Nano Browser would like to give web sites access to your location for demo purposes.</string> + <key>NSMicrophoneUsageDescription</key> + <string>Quick Nano Browser would like to give web sites access to your computer's microphone for demo purposes.</string> + <key>NSCameraUsageDescription</key> + <string>Quick Nano Browser would like to give web sites access to your computer's camera for demo purposes.</string> +</dict> +</plist> diff --git a/examples/webenginequick/quicknanobrowser/WebAuthDialog.qml b/examples/webenginequick/quicknanobrowser/WebAuthDialog.qml new file mode 100644 index 000000000..aeb6f5a0f --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/WebAuthDialog.qml @@ -0,0 +1,281 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtWebEngine + +Dialog { + id: webAuthDialog + anchors.centerIn: parent + width: Math.min(browserWindow.width, browserWindow.height) / 3 * 2 + contentWidth: verticalLayout.width +10; + contentHeight: verticalLayout.height +10; + standardButtons: Dialog.Cancel | Dialog.Apply + title: "WebAuth Request" + + property var selectAccount; + property var authrequest: null; + + Connections { + id: webauthConnection + ignoreUnknownSignals: true + function onStateChanged(state) { + webAuthDialog.setupUI(state); + } + } + + onApplied: { + switch (webAuthDialog.authrequest.state) { + case WebEngineWebAuthUxRequest.WebAuthUxState.CollectPin: + webAuthDialog.authrequest.setPin(pinEdit.text); + break; + case WebEngineWebAuthUxRequest.WebAuthUxState.SelectAccount: + webAuthDialog.authrequest.setSelectedAccount(webAuthDialog.selectAccount); + break; + default: + break; + } + } + + onRejected: { + webAuthDialog.authrequest.cancel(); + } + + function init(request) { + pinLabel.visible = false; + pinEdit.visible = false; + confirmPinLabel.visible = false; + confirmPinEdit.visible = false; + selectAccountModel.clear(); + webAuthDialog.authrequest = request; + webauthConnection.target = request; + setupUI(webAuthDialog.authrequest.state) + webAuthDialog.visible = true; + pinEntryError.visible = false; + } + + function setupUI(state) { + switch (state) { + case WebEngineWebAuthUxRequest.WebAuthUxState.SelectAccount: + setupSelectAccountUI(); + break; + case WebEngineWebAuthUxRequest.WebAuthUxState.CollectPin: + setupCollectPin(); + break; + case WebEngineWebAuthUxRequest.WebAuthUxState.FinishTokenCollection: + setupFinishCollectToken(); + break; + case WebEngineWebAuthUxRequest.WebAuthUxState.RequestFailed: + setupErrorUI(); + break; + case WebEngineWebAuthUxRequest.WebAuthUxState.Completed: + webAuthDialog.close(); + break; + } + } + + ButtonGroup { + id : selectAccount; + exclusive: true; + } + + ListModel { + id: selectAccountModel + + } + contentItem: Item { + ColumnLayout { + id : verticalLayout + spacing : 10 + + Label { + id: heading + text: ""; + } + + Label { + id: description + text: ""; + } + + Row { + spacing : 10 + Label { + id: pinLabel + text: "PIN"; + } + TextInput { + id: pinEdit + text: "EnterPin" + enabled: true + focus: true + color: "white" + layer.sourceRect: Qt.rect(0, 0, 20, 20) + } + } + + Row { + spacing : 10 + Label { + id: confirmPinLabel + text: "Confirm PIN"; + } + TextEdit { + id: confirmPinEdit + text: "" + } + } + + Label { + id: pinEntryError + text: ""; + } + + Repeater { + id : selectAccountRepeater + model: selectAccountModel + Column { + spacing : 5 + RadioButton { + text: modelData + ButtonGroup.group : selectAccount; + onClicked: function(){ + webAuthDialog.selectAccount = text; + } + } + } + } + } + } + + function setupSelectAccountUI() { + webAuthDialog.selectAccount = ""; + heading.text = "Choose a passkey"; + description.text = "Which passkey do you want to use for " + webAuthDialog.authrequest.relyingPartyId; + + selectAccountModel.clear(); + var userNames = webAuthDialog.authrequest.userNames; + for (var i = 0; i < userNames.length; i++) { + selectAccountModel.append( {"name" : userNames[i]}); + } + pinLabel.visible = false; + pinEdit.visible = false; + confirmPinLabel.visible = false; + confirmPinEdit.visible = false; + pinEntryError.visible = false; + standardButton(Dialog.Apply).visible = true; + standardButton(Dialog.Cancel).visible = true; + standardButton(Dialog.Cancel).text ="Cancel" + } + + function setupCollectPin() { + var requestInfo = webAuthDialog.authrequest.pinRequest; + + pinEdit.clear(); + + if (requestInfo.reason === WebEngineWebAuthUxRequest.PinEntryReason.Challenge) { + heading.text = "PIN required"; + description.text = "Enter the PIN for your security key"; + pinLabel.visible = true; + pinEdit.visible = true; + confirmPinLabel.visible = false; + confirmPinEdit.visible = false; + } else if (reason === WebEngineWebAuthUxRequest.PinEntryReason.Set) { + heading.text = "Set PIN "; + description.text = "Set new PIN for your security key"; + pinLabel.visible = true; + pinEdit.visible = true; + confirmPinLabel.visible = true; + confirmPinEdit.visible = true; + } + pinEntryError.text = getPINErrorDetails() + " " + requestInfo.remainingAttempts + " attempts reamining"; + pinEntryError.visible = true; + selectAccountModel.clear(); + standardButton(Dialog.Cancel).visible = true; + standardButton(Dialog.Cancel).text ="Cancel" + standardButton(Dialog.Apply).visible = true; + } + + function getPINErrorDetails() { + var requestInfo = webAuthDialog.authrequest.pinRequest; + switch (requestInfo.error) { + case WebEngineWebAuthUxRequest.PinEntryError.NoError: + return ""; + case WebEngineWebAuthUxRequest.PinEntryError.TooShort: + return "Too short"; + case WebEngineWebAuthUxRequest.PinEntryError.InternalUvLocked: + return "Internal Uv locked"; + case WebEngineWebAuthUxRequest.PinEntryError.WrongPin: + return "Wrong PIN"; + case WebEngineWebAuthUxRequest.PinEntryError.InvalidCharacters: + return "Invalid characters"; + case WebEngineWebAuthUxRequest.PinEntryError.SameAsCurrentPin: + return "Same as current PIN"; + } + } + + function getRequestFailureResaon() { + var requestFailureReason = webAuthDialog.authrequest.requestFailureReason; + switch (requestFailureReason) { + case WebEngineWebAuthUxRequest.RequestFailureReason.Timeout: + return " Request Timeout"; + case WebEngineWebAuthUxRequest.RequestFailureReason.KeyNotRegistered: + return "Key not registered"; + case WebEngineWebAuthUxRequest.RequestFailureReason.KeyAlreadyRegistered: + return "You already registered this device. You don't have to register it again + Try agin with different key or device"; + case WebEngineWebAuthUxRequest.RequestFailureReason.SoftPinBlock: + return "The security key is locked because the wrong PIN was entered too many times. + To unlock it, remove and reinsert it."; + case WebEngineWebAuthUxRequest.RequestFailureReason.HardPinBlock: + return "The security key is locked because the wrong PIN was entered too many times. + You'll need to reset the security key."; + case WebEngineWebAuthUxRequest.RequestFailureReason.AuthenticatorRemovedDuringPinEntry: + return "Authenticator removed during verification. Please reinsert and try again"; + case WebEngineWebAuthUxRequest.RequestFailureReason.AuthenticatorMissingResidentKeys: + return "Authenticator doesn't have resident key support"; + case WebEngineWebAuthUxRequest.RequestFailureReason.AuthenticatorMissingUserVerification: + return "Authenticator missing user verification"; + case WebEngineWebAuthUxRequest.RequestFailureReason.AuthenticatorMissingLargeBlob: + return "Authenticator missing Large Blob support"; + case WebEngineWebAuthUxRequest.RequestFailureReason.NoCommonAlgorithms: + return "No common Algorithms"; + case WebEngineWebAuthUxRequest.RequestFailureReason.StorageFull: + return "Storage full"; + case WebEngineWebAuthUxRequest.RequestFailureReason.UserConsentDenied: + return "User consent denied"; + case WebEngineWebAuthUxRequest.RequestFailureReason.WinUserCancelled: + return "User cancelled request"; + } + } + + function setupFinishCollectToken() { + heading.text = "Use your security key with " + webAuthDialog.authrequest.relyingPartyId; + description.text = "Touch your security key again to complete the request."; + pinLabel.visible = false; + pinEdit.visible = false; + confirmPinLabel.visible = false; + confirmPinEdit.visible = false; + selectAccountModel.clear(); + pinEntryError.visible = false; + standardButton(Dialog.Apply).visible = false; + standardButton(Dialog.Cancel).visible = true; + standardButton(Dialog.Cancel).text ="Cancel" + } + + function setupErrorUI() { + heading.text = "Something went wrong"; + description.text = getRequestFailureResaon(); + pinLabel.visible = false; + pinEdit.visible = false; + confirmPinLabel.visible = false; + confirmPinEdit.visible = false; + selectAccountModel.clear(); + pinEntryError.visible = false; + standardButton(Dialog.Apply).visible = false; + standardButton(Dialog.Cancel).visible = true; + standardButton(Dialog.Cancel).text ="Close" + } +} diff --git a/examples/webenginequick/quicknanobrowser/doc/images/quicknanobrowser-demo.jpg b/examples/webenginequick/quicknanobrowser/doc/images/quicknanobrowser-demo.jpg Binary files differnew file mode 100644 index 000000000..12693bb0f --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/doc/images/quicknanobrowser-demo.jpg diff --git a/examples/webenginequick/quicknanobrowser/doc/src/quicknanobrowser.qdoc b/examples/webenginequick/quicknanobrowser/doc/src/quicknanobrowser.qdoc new file mode 100644 index 000000000..1dc209c2e --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/doc/src/quicknanobrowser.qdoc @@ -0,0 +1,187 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! + \example webenginequick/quicknanobrowser + \title WebEngine Quick Nano Browser + \ingroup webengine-examples + \brief A web browser implemented using the WebEngineView QML type. + + \image quicknanobrowser-demo.jpg + \examplecategory {Application Examples} + \examplecategory {Web Technologies} + + \e {Quick Nano Browser} demonstrates how to use the \l{Qt WebEngine QML Types} + {Qt WebEngine QML types} to develop a small web browser application that consists of a browser + window with a title bar, toolbar, tab view, and status bar. The web content is loaded in a web + engine view within the tab view. If certificate errors occur, users are prompted for action in a + message dialog. The status bar pops up to display the URL of a hovered link. + + A web page can issue a request for being displayed in fullscreen mode. Users can allow full + screen mode by using a toolbar button. They can leave fullscreen mode by using a keyboard + shortcut. Additional toolbar buttons enable moving backwards and forwards in the browser + history, reloading tab content, and opening a settings menu for enabling the following features: + JavaScript, plugins, fullscreen mode, off the record, HTTP disk cache, autoloading images, and + ignoring certificate errors. + + \include examples-run.qdocinc + + \section1 Creating the Main Browser Window + + When the browser main window is loaded, it creates an empty tab using the default profile. Each + tab is a web engine view that fills the main window. + + We create the main window in the \e BrowserWindow.qml file using the ApplicationWindow type: + + \quotefromfile webenginequick/quicknanobrowser/BrowserWindow.qml + \skipto ApplicationWindow + \printuntil currentWebView + \dots + \skipto width + \printuntil title + + We use the TabBar Qt Quick control to create a tab bar anchored to the top of the window, and + create a new, empty tab: + + \skipto TabBar { + \printuntil return webview + \printuntil } + + The tab contains a web engine view that loads web content: + + \skipto Component { + \printuntil currentWebView.reload + \printuntil /^\ {8}\}/ + + We use the \l Action type to create new tabs: + + \quotefromfile webenginequick/quicknanobrowser/BrowserWindow.qml + \skipto reload + \skipto Action + \printuntil } + + We use the \l TextField Qt Quick Control within a \l ToolBar to create an address bar that + shows the current URL and where users can enter another URL: + + \quotefromfile webenginequick/quicknanobrowser/BrowserWindow.qml + \skipto menuBar: ToolBar + \printuntil anchors.fill + \dots + \skipto TextField + \printuntil addressBar + \dots + \skipto focus + \printuntil /^\ {12}\}/ + + \section1 Handling Certificate Errors + + If the certificate of the site being loaded triggers a certificate error, we call the + \l{WebEngineCertificateError::}{defer()} QML method to pause the URL request and wait for user + input: + + \quotefromfile webenginequick/quicknanobrowser/BrowserWindow.qml + \skipto onCertificateError + \printuntil } + + We use the Dialog type to prompt users to continue or cancel the loading of the web page. + If users select \uicontrol Yes, we call the + \l{WebEngineCertificateError::}{acceptCertificate()} method to continue loading content from + the URL. If users select \uicontrol No, we call the + \l{WebEngineCertificateError::}{rejectCertificate()} method to reject the request and stop + loading content from the URL: + + \skipto Dialog { + \printuntil /^\ {4}\}/ + + \section1 Handling Feature Permission Requests + + We use the \c onFeaturePermissionRequested() signal handler to handle requests for + accessing a certain feature or device. The \c securityOrigin parameter identifies the + requester web site, and the \c feature parameter is the requested feature. We use these + to construct the message of the dialog: + + \quotefromfile webenginequick/quicknanobrowser/BrowserWindow.qml + \skipto onFeaturePermissionRequested + \printuntil } + + We show a dialog where the user is asked to grant or deny access. The custom + \c questionForFeature() JavaScript function generates a human-readable question about + the request. + If users select \uicontrol Yes, we call the \l{WebEngineView::}{grantFeaturePermission()} + method with a third \c true parameter to grant the \c securityOrigin web site the permission + to access the \c feature. + If users select \uicontrol No, we call the same method but with the \c false parameter to + deny access: + + \skipto id: sslDialog + \skipto Dialog { + \printuntil /^\ {4}\}/ + + + \section1 Entering and Leaving Fullscreen Mode + + We create a menu item for allowing fullscreen mode in a settings menu that we place on the tool + bar. Also, we create an action for leaving fullscreen mode by using a keyboard shortcut. + We call the \l{FullScreenRequest::}{accept()} method to accept the fullscreen request. + The methdod sets the \l{WebEngineView::}{isFullScreen} property to be equal to the + \l{FullScreenRequest::}{toggleOn} property. + + \quotefromfile webenginequick/quicknanobrowser/BrowserWindow.qml + \skipto onFullScreenRequested + \printuntil /^\ {16}\}/ + + When entering fullscreen mode, we display a notification using the FullScreenNotification custom + type that we create in \e FullScreenNotification.qml. + + We use the \l Action type in the settings menu to create a shortcut for leaving fullscreen mode + by pressing the escape key: + + \quotefromfile webenginequick/quicknanobrowser/BrowserWindow.qml + \skipto Settings + \printuntil appSettings + \skipto fullScreenSupportEnabled + \printuntil Action + \skipto Escape + \printuntil /^\ {4}\}/ + + \section1 Handling WebAuth/FIDO UX Requests + + We use the \c onWebAuthUxRequested() signal handler to handle requests for + WebAuth/FIDO UX. The \c request parameter is an instance of WebEngineWebAuthUxRequest + which contains UX request details and APIs required to process the request. + We use it to construct WebAuthUX dialog and initiates the UX request flow. + + \quotefromfile webenginequick/quicknanobrowser/BrowserWindow.qml + \skipto onWebAuthUxRequested + \printuntil } + + The \l WebEngineWebAuthUxRequest object periodically emits the \l + {WebEngineWebAuthUxRequest::}{stateChanged} signal to notify potential + observers of the current WebAuth UX states. The observers update the WebAuth + dialog accordingly. We use onStateChanged() signal handler to handle + state change requests. See \c WebAuthDialog.qml for an example + of how these signals can be handled. + + \quotefromfile webenginequick/quicknanobrowser/WebAuthDialog.qml + \skipto Connections + \printuntil } + \skipto function init(request) + \printuntil } + + \section1 Signing Requirement for macOS + + To allow web sites access to the location, camera, and microphone when running + \e {Quick Nano Browser} on macOS, the application needs to be signed. This is + done automatically when building, but you need to set up a valid signing identity + for the build environment. + + \section1 Files and Attributions + + The example uses icons from the Tango Icon Library: + + \table + \row + \li \l{quicknanobrowser-tango}{Tango Icon Library} + \li Public Domain + \endtable +*/ diff --git a/examples/webenginequick/quicknanobrowser/icons/3rdparty/COPYING b/examples/webenginequick/quicknanobrowser/icons/3rdparty/COPYING new file mode 100644 index 000000000..220881da6 --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/icons/3rdparty/COPYING @@ -0,0 +1 @@ +The icons in this repository are herefore released into the Public Domain. diff --git a/examples/webenginequick/quicknanobrowser/icons/3rdparty/go-next.png b/examples/webenginequick/quicknanobrowser/icons/3rdparty/go-next.png Binary files differnew file mode 100644 index 000000000..6f3f65d33 --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/icons/3rdparty/go-next.png diff --git a/examples/webenginequick/quicknanobrowser/icons/3rdparty/go-previous.png b/examples/webenginequick/quicknanobrowser/icons/3rdparty/go-previous.png Binary files differnew file mode 100644 index 000000000..93be3d1ee --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/icons/3rdparty/go-previous.png diff --git a/examples/webenginequick/quicknanobrowser/icons/3rdparty/process-stop.png b/examples/webenginequick/quicknanobrowser/icons/3rdparty/process-stop.png Binary files differnew file mode 100644 index 000000000..b68290bf1 --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/icons/3rdparty/process-stop.png diff --git a/examples/webenginequick/quicknanobrowser/icons/3rdparty/qt_attribution.json b/examples/webenginequick/quicknanobrowser/icons/3rdparty/qt_attribution.json new file mode 100644 index 000000000..d8d85d6f1 --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/icons/3rdparty/qt_attribution.json @@ -0,0 +1,24 @@ +{ + "Id": "quicknanobrowser-tango", + "Name": "Tango Icon Library", + "QDocModule": "qtwebengine", + "QtUsage": "Used in WebEngine Quick Nano Browser example.", + + "QtParts": [ "examples" ], + "Description": "Selected icons from the Tango Icon Library", + "Homepage": "http://tango.freedesktop.org/Tango_Icon_Library", + "Version": "0.8.90", + "DownloadLocation": "http://tango.freedesktop.org/releases/tango-icon-theme-0.8.90.tar.gz", + "LicenseId": "urn:dje:license:public-domain", + "License": "Public Domain", + "LicenseFile": "COPYING", + "Copyright": ["Ulisse Perusin <uli.peru@gmail.com>", + "Steven Garrity <sgarrity@silverorange.com>", + "Lapo Calamandrei <calamandrei@gmail.com>", + "Ryan Collier <rcollier@novell.com>", + "Rodney Dawes <dobey@novell.com>", + "Andreas Nilsson <nisses.mail@home.se>", + "Tuomas Kuosmanen <tigert@tigert.com>", + "Garrett LeSage <garrett@novell.com>", + "Jakub Steiner <jimmac@novell.com>"] +} diff --git a/examples/webenginequick/quicknanobrowser/icons/3rdparty/view-refresh.png b/examples/webenginequick/quicknanobrowser/icons/3rdparty/view-refresh.png Binary files differnew file mode 100644 index 000000000..cab4d02c7 --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/icons/3rdparty/view-refresh.png diff --git a/examples/webenginequick/quicknanobrowser/main.cpp b/examples/webenginequick/quicknanobrowser/main.cpp new file mode 100644 index 000000000..1e693cbcd --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/main.cpp @@ -0,0 +1,53 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "utils.h" + +#include <QtWebEngineQuick/qtwebenginequickglobal.h> + +#include <QtQml/QQmlApplicationEngine> +#include <QtQml/QQmlContext> + +#include <QtGui/QGuiApplication> + +#include <QtCore/QCommandLineParser> +#include <QtCore/QCommandLineOption> +#include <QtCore/QLoggingCategory> + +static QUrl startupUrl(const QCommandLineParser &parser) +{ + if (!parser.positionalArguments().isEmpty()) { + const QUrl url = Utils::fromUserInput(parser.positionalArguments().constFirst()); + if (url.isValid()) + return url; + } + return QUrl(QStringLiteral("chrome://qt")); +} + +int main(int argc, char **argv) +{ + QCoreApplication::setApplicationName("Quick Nano Browser"); + QCoreApplication::setOrganizationName("QtProject"); + + QtWebEngineQuick::initialize(); + + QGuiApplication app(argc, argv); + QLoggingCategory::setFilterRules(QStringLiteral("qt.webenginecontext.debug=true")); + + QCommandLineParser parser; + parser.addHelpOption(); + parser.addVersionOption(); + parser.addPositionalArgument("url", "The URL to open."); + parser.process(app); + + QQmlApplicationEngine appEngine; + appEngine.load(QUrl("qrc:/ApplicationRoot.qml")); + if (appEngine.rootObjects().isEmpty()) + qFatal("Failed to load sources"); + + const QUrl url = startupUrl(parser); + QMetaObject::invokeMethod(appEngine.rootObjects().constFirst(), + "load", Q_ARG(QVariant, url)); + + return app.exec(); +} diff --git a/examples/webenginequick/quicknanobrowser/quicknanobrowser.exe.manifest b/examples/webenginequick/quicknanobrowser/quicknanobrowser.exe.manifest new file mode 100644 index 000000000..acc401776 --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/quicknanobrowser.exe.manifest @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> +<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <!--The ID below indicates application support for Windows Vista --> + <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/> + <!--The ID below indicates application support for Windows 7 --> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> + <!--The ID below indicates application support for Windows 8 --> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> + <!--The ID below indicates application support for Windows 8.1 --> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> + <!--The ID below indicates application support for Windows 10/11 --> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + </application> +</compatibility> +</assembly> diff --git a/examples/webenginequick/quicknanobrowser/quicknanobrowser.pro b/examples/webenginequick/quicknanobrowser/quicknanobrowser.pro new file mode 100644 index 000000000..bd5427dc5 --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/quicknanobrowser.pro @@ -0,0 +1,27 @@ +requires(qtConfig(accessibility)) + +TEMPLATE = app +TARGET = quicknanobrowser + +HEADERS = utils.h +SOURCES = main.cpp + +win32 { + CONFIG -= embed_manifest_exe + QMAKE_MANIFEST = $$PWD/quicknanobrowser.exe.manifest +} + +RESOURCES += resources.qrc + +QT += qml quick webenginequick + +CONFIG += qmltypes +QML_IMPORT_NAME = BrowserUtils +QML_IMPORT_MAJOR_VERSION = 1 + +qtHaveModule(widgets) { + QT += widgets # QApplication is required to get native styling with QtQuickControls +} + +target.path = $$[QT_INSTALL_EXAMPLES]/webenginequick/quicknanobrowser +INSTALLS += target diff --git a/examples/webenginequick/quicknanobrowser/resources.qrc b/examples/webenginequick/quicknanobrowser/resources.qrc new file mode 100644 index 000000000..0a0b42bbb --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/resources.qrc @@ -0,0 +1,17 @@ +<RCC> + <qresource prefix="/"> + <file>ApplicationRoot.qml</file> + <file>BrowserDialog.qml</file> + <file>BrowserWindow.qml</file> + <file>DownloadView.qml</file> + <file>FindBar.qml</file> + <file>FullScreenNotification.qml</file> + <file>WebAuthDialog.qml</file> + </qresource> + <qresource prefix="/icons"> + <file alias="go-next.png">icons/3rdparty/go-next.png</file> + <file alias="go-previous.png">icons/3rdparty/go-previous.png</file> + <file alias="process-stop.png">icons/3rdparty/process-stop.png</file> + <file alias="view-refresh.png">icons/3rdparty/view-refresh.png</file> + </qresource> +</RCC> diff --git a/examples/webenginequick/quicknanobrowser/utils.h b/examples/webenginequick/quicknanobrowser/utils.h new file mode 100644 index 000000000..6c11e75fb --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/utils.h @@ -0,0 +1,29 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef UTILS_H +#define UTILS_H + +#include <QtQml/qqml.h> + +#include <QtCore/QFileInfo> +#include <QtCore/QUrl> + +class Utils : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON +public: + Q_INVOKABLE static QUrl fromUserInput(const QString &userInput); +}; + +inline QUrl Utils::fromUserInput(const QString &userInput) +{ + QFileInfo fileInfo(userInput); + if (fileInfo.exists()) + return QUrl::fromLocalFile(fileInfo.absoluteFilePath()); + return QUrl::fromUserInput(userInput); +} + +#endif // UTILS_H diff --git a/examples/webenginequick/webenginequick.pro b/examples/webenginequick/webenginequick.pro new file mode 100644 index 000000000..fb44f2c54 --- /dev/null +++ b/examples/webenginequick/webenginequick.pro @@ -0,0 +1,9 @@ +TEMPLATE=subdirs + +SUBDIRS += \ + quicknanobrowser + +qtHaveModule(quickcontrols2) { + SUBDIRS += \ + lifecycle +} |