Nano Browser Example¶

A web browser implemented using the WebEngineView QML type.

Nano Browser Screenshot

Download this example

# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

"""PySide6 WebEngine QtQuick 2 Example"""

import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from pathlib import Path

from PySide6.QtCore import (QCoreApplication, QFileInfo, QObject,
                            QUrl, Slot)
from PySide6.QtQml import QQmlApplicationEngine, QmlElement, QmlSingleton
from PySide6.QtGui import QGuiApplication
from PySide6.QtWebEngineQuick import QtWebEngineQuick

import rc_resources  # noqa: F401


# To be used on the @QmlElement decorator
# (QML_IMPORT_MINOR_VERSION is optional)
QML_IMPORT_NAME = "BrowserUtils"
QML_IMPORT_MAJOR_VERSION = 1


def url_from_user_input(user_input):
    file_info = QFileInfo(user_input)
    if file_info.exists():
        return QUrl.fromLocalFile(file_info.absoluteFilePath())
    return QUrl.fromUserInput(user_input)


@QmlElement
@QmlSingleton
class Utils(QObject):

    @Slot(str, result=QUrl)
    def fromUserInput(self, user_input):
        return url_from_user_input(user_input)


if __name__ == '__main__':
    QCoreApplication.setApplicationName("Quick Nano Browser")
    QCoreApplication.setOrganizationName("QtProject")

    QtWebEngineQuick.initialize()

    argument_parser = ArgumentParser(description="Quick Nano Browser",
                                     formatter_class=RawTextHelpFormatter)
    argument_parser.add_argument("--single-process", "-s", action="store_true",
                                 help="Run in single process mode (trouble shooting)")
    argument_parser.add_argument("url", help="The URL to open",
                                 nargs='?', type=str)
    options = argument_parser.parse_args()

    url = url_from_user_input(options.url) if options.url else QUrl("chrome://qt")

    app_args = sys.argv
    if options.single_process:
        app_args.extend(["--webEngineArgs", "--single-process"])
    app = QGuiApplication(app_args)
    engine = QQmlApplicationEngine()
    engine.setInitialProperties({"startupUrl": url})
    engine.addImportPath(Path(__file__).parent)
    engine.loadFromModule("BrowserUtils", "ApplicationRoot")
    if not engine.rootObjects():
        sys.exit(-1)

    exit_code = app.exec()
    del engine
    sys.exit(exit_code)
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

pragma ComponentBehavior: Bound

import QtQuick
import BrowserUtils

QtObject {
    id: root
    required property string startupUrl
    Component.onCompleted: BrowserManager.load(startupUrl)
}
// 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
        }
    }
}
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

pragma Singleton

import QtQuick
import QtWebEngine

QtObject {
    id: manager

    property WebEngineProfilePrototype defaultProfilePrototype : WebEngineProfilePrototype {
        storageName: "Profile"
    }

    property WebEngineProfilePrototype otrPrototype : WebEngineProfilePrototype {}

    function createWindow(profile) {
        let browserWindowComponent = Qt.createComponent("BrowserUtils", "BrowserWindow");
        let newWindow = browserWindowComponent.createObject(manager) as BrowserWindow;
        newWindow.currentWebView.profile = profile;
        profile.downloadRequested.connect(newWindow.onDownloadRequested);
        return newWindow;
    }
    function createDialog(profile) {
        let browserDialogComponent = Qt.createComponent("BrowserUtils", "BrowserDialog");
        let newDialog = browserDialogComponent.createObject(manager) as BrowserDialog;
        newDialog.closing.connect(function(){destroy()})
        newDialog.currentWebView.profile = profile;
        return newDialog;
    }
    function load(url) {
        let browserWindow = createWindow(manager.defaultProfilePrototype.instance());
        browserWindow.currentWebView.url = url;
    }

    Component.onCompleted: {
        let fullVersionList = manager.defaultProfilePrototype.instance().clientHints.fullVersionList;
        fullVersionList["QuickNanoBrowser"] = "1.0";
        manager.defaultProfilePrototype.instance().clientHints.fullVersionList = fullVersionList;
    }
}
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

pragma ComponentBehavior: Bound

import QtCore
import QtQml
import QtQuick
import QtQuick.Controls.Fusion
import QtQuick.Dialogs
import QtQuick.Layouts
import QtQuick.Window
import QtWebEngine
import BrowserUtils

ApplicationWindow {
    id: win
    property WebEngineView currentWebView: tabBar.currentIndex < tabBar.count ? tabLayout.children[tabBar.currentIndex] : null
    property int previousVisibility: Window.Windowed
    property bool lastTabClosing: false

    width: 1300
    height: 900
    visible: true
    title: win.currentWebView?.title ?? ""

    // Make sure the Qt.WindowFullscreenButtonHint is set on OS X.
    Component.onCompleted: flags = flags | Qt.WindowFullscreenButtonHint

    onCurrentWebViewChanged: {
        findBar.reset();
    }

    Settings {
        id : appSettings
        property alias autoLoadImages: navigationBar.loadImagesChecked
        property alias javaScriptEnabled: navigationBar.javaScriptEnabledChecked
        property alias errorPageEnabled: navigationBar.errorPageEnabledChecked
        property alias pluginsEnabled: navigationBar.pluginsEnabledChecked
        property alias fullScreenSupportEnabled: navigationBar.fullScreenSupportEnabledChecked
        property alias autoLoadIconsForPage: navigationBar.autoLoadIconsForPageChecked
        property alias touchIconsEnabled: navigationBar.touchIconsEnabledChecked
        property alias webRTCPublicInterfacesOnly : navigationBar.webRTCPublicInterfacesOnlyChecked
        property alias devToolsEnabled: navigationBar.devToolsEnabledChecked
        property alias pdfViewerEnabled: navigationBar.pdfViewerEnabledChecked
        property alias javascriptCanAccessClipboard: navigationBar.javascriptCanAccessClipboardChecked
        property alias javascriptCanPaste: navigationBar.javascriptCanPasteChecked
        property int imageAnimationPolicy: {
           return navigationBar.animateImageOnceChecked ? WebEngineSettings.ImageAnimationPolicy.AnimateOnce :
                  navigationBar.allowImageAnimationChecked ? WebEngineSettings.ImageAnimationPolicy.Allow :
                  navigationBar.disableImageAnimationChecked ? WebEngineSettings.ImageAnimationPolicy.Disallow :
                  WebEngineSettings.ImageAnimationPolicy.AnimateOnce
          }
        }

    Action {
        shortcut: "Ctrl+D"
        onTriggered: {
            downloadView.visible = !downloadView.visible;
        }
    }
    Action {
        id: focusAction
        shortcut: "Ctrl+L"
        onTriggered: {
            navigationBar.addressBar.forceActiveFocus();
            navigationBar.addressBar.selectAll();
        }
    }
    Action {
        shortcut: StandardKey.Refresh
        onTriggered: {
            if (win.currentWebView)
                win.currentWebView.reload();
        }
    }
    Action {
        shortcut: StandardKey.AddTab
        onTriggered: {
            tabBar.createTab(tabBar.count !== 0
                             ? win.currentWebView.profile
                             : BrowserManager.defaultProfilePrototype.instance());
            navigationBar.addressBar.forceActiveFocus();
            navigationBar.addressBar.selectAll();
        }
    }
    Action {
        shortcut: StandardKey.Close
        onTriggered: {
            win.currentWebView.triggerWebAction(WebEngineView.RequestClose);
        }
    }
    Action {
        shortcut: StandardKey.Quit
        onTriggered: win.close()
    }
    Action {
        shortcut: "Escape"
        onTriggered: {
            if (win.currentWebView.state === "FullScreen") {
                win.visibility = win.previousVisibility;
                fullScreenNotification.hide();
                win.currentWebView.triggerWebAction(WebEngineView.ExitFullScreen);
            }

            if (findBar.visible)
                findBar.visible = false;
        }
    }
    Action {
        shortcut: "Ctrl+0"
        onTriggered: win.currentWebView.zoomFactor = 1.0
    }
    Action {
        shortcut: StandardKey.ZoomOut
        onTriggered: win.currentWebView.zoomFactor -= 0.1
    }
    Action {
        shortcut: StandardKey.ZoomIn
        onTriggered: win.currentWebView.zoomFactor += 0.1
    }

    Action {
        shortcut: StandardKey.Copy
        onTriggered: win.currentWebView.triggerWebAction(WebEngineView.Copy)
    }
    Action {
        shortcut: StandardKey.Cut
        onTriggered: win.currentWebView.triggerWebAction(WebEngineView.Cut)
    }
    Action {
        shortcut: StandardKey.Paste
        onTriggered: win.currentWebView.triggerWebAction(WebEngineView.Paste)
    }
    Action {
        shortcut: "Shift+"+StandardKey.Paste
        onTriggered: win.currentWebView.triggerWebAction(WebEngineView.PasteAndMatchStyle)
    }
    Action {
        shortcut: StandardKey.SelectAll
        onTriggered: win.currentWebView.triggerWebAction(WebEngineView.SelectAll)
    }
    Action {
        shortcut: StandardKey.Undo
        onTriggered: win.currentWebView.triggerWebAction(WebEngineView.Undo)
    }
    Action {
        shortcut: StandardKey.Redo
        onTriggered: win.currentWebView.triggerWebAction(WebEngineView.Redo)
    }
    Action {
        shortcut: StandardKey.Back
        onTriggered: win.currentWebView.triggerWebAction(WebEngineView.Back)
    }
    Action {
        shortcut: StandardKey.Forward
        onTriggered: win.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: WebToolBar {
        id: navigationBar
        currentWebView: win.currentWebView
    }

    StackLayout {
        id: tabLayout
        currentIndex: tabBar.currentIndex

        anchors.top: tabBar.bottom
        anchors.bottom: devToolsWebEngineView.top
        anchors.left: parent.left
        anchors.right: parent.right
    }

    Component {
        id: tabButtonComponent

        TabButton {
            id: tabButton
            property color frameColor: "#999999"
            property color fillColor: "#eeeeee"
            property color nonSelectedColor: "#dddddd"
            property string tabTitle: "New Tab"

            contentItem: Rectangle {
                id: tabRectangle
                color: tabButton.down ? tabButton.fillColor : tabButton.nonSelectedColor
                border.width: 1
                border.color: tabButton.frameColor
                implicitWidth: Math.max(tabText.width + 30, 80)
                implicitHeight: Math.max(tabText.height + 10, 20)
                Rectangle { height: 1 ; width: parent.width ; color: tabButton.frameColor}
                Rectangle { height: parent.height ; width: 1; color: tabButton.frameColor}
                Rectangle { x: parent.width - 2; height: parent.height ; width: 1; color: tabButton.frameColor}
                Text {
                    id: tabText
                    anchors.left: parent.left
                    anchors.verticalCenter: parent.verticalCenter
                    anchors.leftMargin: 6
                    text: tabButton.tabTitle
                    elide: Text.ElideRight
                    color: tabButton.down ? "black" : tabButton.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 ? "#cccccc" : tabRectangle.color
                        Text {text: "x"; anchors.centerIn: parent; color: "gray"}
                    }
                    onClicked: tabButton.closeTab()
                }
            }

            onClicked: navigationBar.addressBar.text = (tabLayout.itemAt(TabBar.index) as WebEngineView).url;
            function closeTab() {
                tabBar.tryCloseView(TabBar.index);
            }
        }
    }

    TabBar {
        id: tabBar
        anchors.top: parent.top
        anchors.left: parent.left
        anchors.right: parent.right
        Component.onCompleted: createTab(BrowserManager.defaultProfilePrototype.instance())

        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; })});
            webview.index = Qt.binding(function () { return newTabButton.TabBar.index; })
            tabBar.addItem(newTabButton);
            if (focusOnNewTab) {
                tabBar.setCurrentIndex(tabBar.count - 1);
            }
            if (url !== undefined) {
                webview.url = url;
            }
            return webview;
        }

        function tryCloseView(index) {
            tabLayout.children[index].triggerWebAction(WebEngineView.RequestClose);
        }

        function removeView(index) {
            if (tabBar.count > 1) {
                tabBar.removeItem(tabBar.itemAt(index));
                tabLayout.children[index].destroy();
            } else {
                win.lastTabClosing = true;
                win.close();
            }
        }

        Component {
            id: tabComponent
            WebEngineView {
                id: webEngineView
                property int index: 0
                focus: true

                onLinkHovered: function(hoveredUrl) {
                    if (hoveredUrl === "")
                        hideStatusText.start();
                    else {
                        statusText.text = hoveredUrl;
                        statusBubble.visible = true;
                        hideStatusText.stop();
                    }
                }

                states: [
                    State {
                        name: "FullScreen"
                        PropertyChanges {
                            tabBar.visible: false
                            tabBar.height: 0
                            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
                settings.screenCaptureEnabled: true
                settings.javascriptCanAccessClipboard: appSettings.javascriptCanAccessClipboard
                settings.javascriptCanPaste: appSettings.javascriptCanPaste

                onWindowCloseRequested: function() {
                    tabBar.removeView(webEngineView.index);
                }

                onCertificateError: function(error) {
                    if (!error.isMainFrame) {
                        error.rejectCertificate();
                        return;
                    }

                    error.defer();
                    sslDialog.enqueue(error);
                }

                onNewWindowRequested: function(request) {
                    if (!request.userInitiated)
                        console.warn("Blocked a popup window.");
                    else if (request.destination === WebEngineNewWindowRequest.InNewTab) {
                        let tab = tabBar.createTab(win.currentWebView.profile, true, request.requestedUrl);
                        tab.acceptAsNewWindow(request);
                    } else if (request.destination === WebEngineNewWindowRequest.InNewBackgroundTab) {
                        let backgroundTab = tabBar.createTab(win.currentWebView.profile, false);
                        backgroundTab.acceptAsNewWindow(request);
                    } else if (request.destination === WebEngineNewWindowRequest.InNewDialog) {
                        let dialog = BrowserManager.createDialog(win.currentWebView.profile);
                        dialog.currentWebView.acceptAsNewWindow(request);
                    } else {
                        let window = BrowserManager.createWindow(win.currentWebView.profile);
                        window.currentWebView.acceptAsNewWindow(request);
                    }
                }

                onFullScreenRequested: function(request) {
                    if (request.toggleOn) {
                        webEngineView.state = "FullScreen";
                        win.previousVisibility = win.visibility;
                        win.showFullScreen();
                        fullScreenNotification.show();
                    } else {
                        webEngineView.state = "";
                        win.visibility = win.previousVisibility;
                        fullScreenNotification.hide();
                    }
                    request.accept();
                }

                onRegisterProtocolHandlerRequested: function(request) {
                    console.log("accepting registerProtocolHandler request for "
                                + request.scheme + " from " + request.origin);
                    request.accept();
                }

                onDesktopMediaRequested: function(request) {
                    // select the primary screen
                    request.selectScreen(request.screensModel.index(0, 0));
                }

                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);
                    Qt.callLater(function() { win.currentWebView.reload() })
                }

                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();
                }

                onPermissionRequested: function(permission) {
                    permissionDialog.permission = permission;
                    permissionDialog.visible = true;
                }
                onWebAuthUxRequested: function(request) {
                    webAuthDialog.init(request);
                }
            }
        }
    }
    WebEngineView {
        id: devToolsWebEngineView
        visible: appSettings.devToolsEnabled
        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(win.currentWebView.profile);
            request.openIn(tab);
        }

        onWindowCloseRequested: function() {
            // Delay hiding for keep the inspectedView set to receive the ACK message of close.
            Qt.callLater(function() { appSettings.devToolsEnabled = false })
        }
    }
    SslDialog {
        id: sslDialog
        anchors.centerIn: parent
    }
    PermissionDialog {
        id: permissionDialog
        anchors.centerIn: parent
        width: Math.min(win.width, win.height) / 3 * 2
    }

    FullScreenNotification {
        id: fullScreenNotification
    }

    DownloadView {
        id: downloadView
        visible: false
        anchors.fill: parent
    }

    WebAuthDialog {
        id: webAuthDialog
        visible: false
        width: Math.min(win.width, win.height) / 3 * 2
    }

    MessageDialog {
        id: downloadAcceptDialog
        property var downloadRequest: downloadView.pendingDownloadRequest
        title: "Download requested"
        text: downloadRequest ? downloadRequest.suggestedFileName : ""
        buttons: Dialog.No | Dialog.Yes
        onAccepted: {
            downloadView.visible = true;
            downloadView.append(downloadRequest);
            downloadRequest.accept();
        }
        onRejected: {
            downloadRequest.cancel();
        }
        onButtonClicked: {
            visible = false;
        }
        visible: false
    }

    function onDownloadRequested(download) {
        downloadView.pendingDownloadRequest = download;
        downloadAcceptDialog.visible = true;
    }

    FindBar {
        id: findBar
        visible: false
        anchors.right: parent.right
        anchors.rightMargin: 10
        anchors.top: parent.top

        onFindNext: {
            if (text)
                win.currentWebView?.findText(text);
            else if (!visible)
                visible = true;
        }
        onFindPrevious: {
            if (text)
                win.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;
                }
            }
        }
    }

    onClosing: function(closeEvent) {
       if (lastTabClosing) {
           return;
       }
       closeEvent.accepted = false
       for (let i = 0; i < tabBar.count; i++)  {
           tabBar.tryCloseView(i);
       }
    }
}
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

pragma ComponentBehavior: Bound

import QtQuick
import QtQuick.Controls.Fusion

Rectangle {
    id: downloadView
    color: "lightgray"
    property var pendingDownloadRequest: null

    ListModel {
        id: downloadModel
        property var downloads: []
    }

    function append(download) {
        downloadModel.append(download);
        downloadModel.downloads.push(download);
    }

    Component {
        id: downloadItemDelegate

        Rectangle {
            id: downloadItem
            width: listView.width
            height: childrenRect.height
            anchors.margins: 10
            radius: 3
            color: "transparent"
            border.color: "black"

            required property int index

            Rectangle {
                id: progressBar

                property real progress: {
                    let d = downloadModel.downloads[downloadItem.index]
                    return d ? d.receivedBytes / d.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: {
                        let d = downloadModel.downloads[downloadItem.index]
                        return d ? d.downloadDirectory + "/" + d.downloadFileName : qsTr("")
                    }
                    anchors {
                        verticalCenter: cancelButton.verticalCenter
                        left: parent.left
                        right: cancelButton.left
                    }
                }
                Button {
                    id: cancelButton
                    anchors.right: parent.right
                    icon.source: "qrc:///icons/3rdparty/process-stop.png"
                    onClicked: {
                        var download = downloadModel.downloads[downloadItem.index];

                        download.cancel();

                        downloadModel.downloads = downloadModel.downloads.filter(function (el) {
                            return el.id !== download.id;
                        });
                        downloadModel.remove(downloadItem.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;
                }
            }
        }
    }
}
// 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: root.activeMatch + "/" + root.numberOfMatches
            visible: findTextField.text !== ""
            color: "black"
        }

        Rectangle {
            border.width: 1
            border.color: "#dddddd"
            Layout.preferredWidth: 2
            Layout.preferredHeight: parent.height
        }

        ToolButton {
            id: findBtnLeft
            text: "<"
            enabled: root.numberOfMatches > 0
            onClicked: root.findPrevious()
            contentItem: Text {
                color: "black"
                text: findBtnLeft.text
            }
        }

        ToolButton {
            id: findBtnRight
            text: ">"
            enabled: root.numberOfMatches > 0
            onClicked: root.findNext()
            contentItem: Text {
                color: "black"
                text: findBtnRight.text
            }
        }

        ToolButton {
            id: findBtnClose
            text: "x"
            onClicked: root.visible = false
            contentItem: Text {
                color: "black"
                text: findBtnClose.text
            }
        }
    }
}
// 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 (fullScreenNotification.opacity === 0)
                    fullScreenNotification.visible = false;
            }
        }
    }

    Timer {
        id: reset
        interval: 5000
        onTriggered: fullScreenNotification.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!")
    }
}
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

pragma ComponentBehavior: Bound

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtWebEngine

Dialog {
    id: webAuthDialog
    anchors.centerIn: parent
    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 : selectAccountButtonGroup;
        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 {
                    id: selectAccountRepeaterColumn
                    required property string modelData
                    spacing : 5
                    RadioButton {
                        text: selectAccountRepeaterColumn.modelData
                        ButtonGroup.group : selectAccountButtonGroup;
                        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 (let 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 (requestInfo.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\n"
                     + "Try again with different key or device.";
        case WebEngineWebAuthUxRequest.RequestFailureReason.SoftPinBlock:
            return "The security key is locked because the wrong PIN was entered too many times.\n"
                    + "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.\n"
                    + "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"
    }
}
module BrowserUtils
ApplicationRoot 254.0 ApplicationRoot.qml
BrowserDialog 254.0 BrowserDialog.qml
singleton BrowserManager 254.0 BrowserManager.qml
BrowserWindow 254.0 BrowserWindow.qml
DownloadView 254.0 DownloadView.qml
FindBar 254.0 FindBar.qml
FullScreenNotification 254.0 FullScreenNotification.qml
PermissionDialog 254.0 PermissionDialog.qml
SslDialog 254.0 SslDialog.qml
WebAuthDialog 254.0 WebAuthDialog.qml
WebToolBar 254.0 WebToolBar.qml
depends QtQuick
typeinfo main.qmltypes
<RCC>
    <qresource>
        <file>icons/3rdparty/go-next.png</file>
        <file>icons/3rdparty/go-previous.png</file>
        <file>icons/3rdparty/process-stop.png</file>
        <file>icons/3rdparty/view-refresh.png</file>
    </qresource>
</RCC>