summaryrefslogtreecommitdiffstats
path: root/examples/webenginequick
diff options
context:
space:
mode:
Diffstat (limited to 'examples/webenginequick')
-rw-r--r--examples/webenginequick/CMakeLists.txt5
-rw-r--r--examples/webenginequick/lifecycle/CMakeLists.txt55
-rw-r--r--examples/webenginequick/lifecycle/WebBrowser.qml125
-rw-r--r--examples/webenginequick/lifecycle/WebTab.qml165
-rw-r--r--examples/webenginequick/lifecycle/WebTabBar.qml53
-rw-r--r--examples/webenginequick/lifecycle/WebTabButton.qml111
-rw-r--r--examples/webenginequick/lifecycle/WebTabStack.qml51
-rw-r--r--examples/webenginequick/lifecycle/WebToolButton.qml16
-rw-r--r--examples/webenginequick/lifecycle/doc/images/lifecycle-automatic.pngbin0 -> 8148 bytes
-rw-r--r--examples/webenginequick/lifecycle/doc/images/lifecycle-manual.pngbin0 -> 5855 bytes
-rw-r--r--examples/webenginequick/lifecycle/doc/images/lifecycle.pngbin0 -> 24494 bytes
-rw-r--r--examples/webenginequick/lifecycle/doc/src/lifecycle.qdoc84
-rw-r--r--examples/webenginequick/lifecycle/lifecycle.pro11
-rw-r--r--examples/webenginequick/lifecycle/main.cpp21
-rw-r--r--examples/webenginequick/lifecycle/qtquickcontrols2.conf6
-rw-r--r--examples/webenginequick/lifecycle/resources.qrc11
-rw-r--r--examples/webenginequick/lifecycle/utils.h25
-rw-r--r--examples/webenginequick/quicknanobrowser/ApplicationRoot.qml40
-rw-r--r--examples/webenginequick/quicknanobrowser/BrowserDialog.qml27
-rw-r--r--examples/webenginequick/quicknanobrowser/BrowserWindow.qml871
-rw-r--r--examples/webenginequick/quicknanobrowser/CMakeLists.txt111
-rw-r--r--examples/webenginequick/quicknanobrowser/DownloadView.qml127
-rw-r--r--examples/webenginequick/quicknanobrowser/FindBar.qml109
-rw-r--r--examples/webenginequick/quicknanobrowser/FullScreenNotification.qml62
-rw-r--r--examples/webenginequick/quicknanobrowser/Info.cmake.macos.plist36
-rw-r--r--examples/webenginequick/quicknanobrowser/WebAuthDialog.qml281
-rw-r--r--examples/webenginequick/quicknanobrowser/doc/images/quicknanobrowser-demo.jpgbin0 -> 30156 bytes
-rw-r--r--examples/webenginequick/quicknanobrowser/doc/src/quicknanobrowser.qdoc187
-rw-r--r--examples/webenginequick/quicknanobrowser/icons/3rdparty/COPYING1
-rw-r--r--examples/webenginequick/quicknanobrowser/icons/3rdparty/go-next.pngbin0 -> 930 bytes
-rw-r--r--examples/webenginequick/quicknanobrowser/icons/3rdparty/go-previous.pngbin0 -> 955 bytes
-rw-r--r--examples/webenginequick/quicknanobrowser/icons/3rdparty/process-stop.pngbin0 -> 1272 bytes
-rw-r--r--examples/webenginequick/quicknanobrowser/icons/3rdparty/qt_attribution.json24
-rw-r--r--examples/webenginequick/quicknanobrowser/icons/3rdparty/view-refresh.pngbin0 -> 1364 bytes
-rw-r--r--examples/webenginequick/quicknanobrowser/main.cpp53
-rw-r--r--examples/webenginequick/quicknanobrowser/quicknanobrowser.exe.manifest17
-rw-r--r--examples/webenginequick/quicknanobrowser/quicknanobrowser.pro27
-rw-r--r--examples/webenginequick/quicknanobrowser/resources.qrc17
-rw-r--r--examples/webenginequick/quicknanobrowser/utils.h29
-rw-r--r--examples/webenginequick/webenginequick.pro9
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
new file mode 100644
index 000000000..2c62967af
--- /dev/null
+++ b/examples/webenginequick/lifecycle/doc/images/lifecycle-automatic.png
Binary files differ
diff --git a/examples/webenginequick/lifecycle/doc/images/lifecycle-manual.png b/examples/webenginequick/lifecycle/doc/images/lifecycle-manual.png
new file mode 100644
index 000000000..7fb5c5a80
--- /dev/null
+++ b/examples/webenginequick/lifecycle/doc/images/lifecycle-manual.png
Binary files differ
diff --git a/examples/webenginequick/lifecycle/doc/images/lifecycle.png b/examples/webenginequick/lifecycle/doc/images/lifecycle.png
new file mode 100644
index 000000000..87b719022
--- /dev/null
+++ b/examples/webenginequick/lifecycle/doc/images/lifecycle.png
Binary files differ
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
new file mode 100644
index 000000000..12693bb0f
--- /dev/null
+++ b/examples/webenginequick/quicknanobrowser/doc/images/quicknanobrowser-demo.jpg
Binary files differ
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
new file mode 100644
index 000000000..6f3f65d33
--- /dev/null
+++ b/examples/webenginequick/quicknanobrowser/icons/3rdparty/go-next.png
Binary files differ
diff --git a/examples/webenginequick/quicknanobrowser/icons/3rdparty/go-previous.png b/examples/webenginequick/quicknanobrowser/icons/3rdparty/go-previous.png
new file mode 100644
index 000000000..93be3d1ee
--- /dev/null
+++ b/examples/webenginequick/quicknanobrowser/icons/3rdparty/go-previous.png
Binary files differ
diff --git a/examples/webenginequick/quicknanobrowser/icons/3rdparty/process-stop.png b/examples/webenginequick/quicknanobrowser/icons/3rdparty/process-stop.png
new file mode 100644
index 000000000..b68290bf1
--- /dev/null
+++ b/examples/webenginequick/quicknanobrowser/icons/3rdparty/process-stop.png
Binary files differ
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
new file mode 100644
index 000000000..cab4d02c7
--- /dev/null
+++ b/examples/webenginequick/quicknanobrowser/icons/3rdparty/view-refresh.png
Binary files differ
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
+}