Nano Browser Example¶
A web browser implemented using the WebEngineView QML type.
# 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();
}
// 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: javaScriptEnabledMenuItem.checked
property alias errorPageEnabled: errorPageEnabledMenuItem.checked
property alias pluginsEnabled: pluginsEnabledMenuItem.checked
property alias fullScreenSupportEnabled: fullScreenSupportEnabledMenuItem.checked
property alias autoLoadIconsForPage: autoLoadIconsForPageMenuItem.checked
property alias touchIconsEnabled: touchIconsEnabledMenuItem.checked
property alias webRTCPublicInterfacesOnly : webRTCPublicInterfacesOnlyMenuItem.checked
property alias devToolsEnabled: devToolsEnabledMenuItem.checked
property alias pdfViewerEnabled: pdfViewerEnabledMenuItem.checked
property int imageAnimationPolicy: WebEngineSettings.ImageAnimationPolicy.Allow
property alias javascriptCanAccessClipboard: javascriptCanAccessClipboardMenuItem.checked
property alias javascriptCanPaste: javascriptCanPasteMenuItem.checked
}
Action {
shortcut: "Ctrl+D"
onTriggered: {
downloadView.visible = !downloadView.visible;
}
}
Action {
id: focusAction
shortcut: "Ctrl+L"
onTriggered: {
addressBar.forceActiveFocus();
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());
addressBar.forceActiveFocus();
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: ToolBar {
id: navigationBar
RowLayout {
anchors.fill: parent
ToolButton {
enabled: win.currentWebView?.canGoBack || win.currentWebView?.canGoForward
onClicked: historyMenu.open()
text: qsTr("â–Ľ")
Menu {
id: historyMenu
Instantiator {
model: win.currentWebView?.history?.items
MenuItem {
required property var model
text: model.title
onTriggered: win.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/3rdparty/go-previous.png"
onClicked: win.currentWebView.goBack()
enabled: win.currentWebView?.canGoBack ?? false
activeFocusOnTab: !win.platformIsMac
}
ToolButton {
id: forwardButton
icon.source: "qrc:///icons/3rdparty/go-next.png"
onClicked: win.currentWebView.goForward()
enabled: win.currentWebView?.canGoForward ?? false
activeFocusOnTab: !win.platformIsMac
}
ToolButton {
id: reloadButton
icon.source: win.currentWebView?.loading
? "qrc:///icons/3rdparty/process-stop.png"
: "qrc:///icons/3rdparty/view-refresh.png"
onClicked: win.currentWebView?.loading ? win.currentWebView.stop() : win.currentWebView.reload()
activeFocusOnTab: !win.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: win.currentWebView?.icon ? win.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: win.currentWebView
value: win.currentWebView.url
}
onAccepted: win.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: javaScriptEnabledMenuItem
text: "JavaScript On"
checkable: true
checked: WebEngine.settings.javascriptEnabled
}
MenuItem {
id: errorPageEnabledMenuItem
text: "ErrorPage On"
checkable: true
checked: WebEngine.settings.errorPageEnabled
}
MenuItem {
id: pluginsEnabledMenuItem
text: "Plugins On"
checkable: true
checked: true
}
MenuItem {
id: fullScreenSupportEnabledMenuItem
text: "FullScreen On"
checkable: true
checked: WebEngine.settings.fullScreenSupportEnabled
}
MenuItem {
id: offTheRecordEnabled
text: "Off The Record"
checkable: true
checked: win.currentWebView?.profile === BrowserManager.otrPrototype.instance()
onToggled: function() {
if (win.currentWebView) {
win.currentWebView.profile = offTheRecordEnabled.checked
? BrowserManager.otrPrototype.instance()
: BrowserManager.defaultProfilePrototype.instance();
}
}
}
MenuItem {
id: httpDiskCacheEnabled
text: "HTTP Disk Cache"
checkable: !win.currentWebView?.profile?.offTheRecord ?? false
checked: win.currentWebView?.profile.httpCacheType === WebEngineProfile.DiskHttpCache
onToggled: function() {
if (win.currentWebView) {
win.currentWebView.profile.httpCacheType = httpDiskCacheEnabled.checked
? WebEngineProfile.DiskHttpCache
: WebEngineProfile.MemoryHttpCache;
}
}
}
MenuItem {
id: autoLoadIconsForPageMenuItem
text: "Icons On"
checkable: true
checked: WebEngine.settings.autoLoadIconsForPage
}
MenuItem {
id: touchIconsEnabledMenuItem
text: "Touch Icons On"
checkable: true
checked: WebEngine.settings.touchIconsEnabled
enabled: autoLoadIconsForPageMenuItem.checked
}
MenuItem {
id: webRTCPublicInterfacesOnlyMenuItem
text: "WebRTC Public Interfaces Only"
checkable: true
checked: WebEngine.settings.webRTCPublicInterfacesOnly
}
MenuItem {
id: devToolsEnabledMenuItem
text: "Open DevTools"
checkable: true
checked: false
}
MenuItem {
id: pdfViewerEnabledMenuItem
text: "PDF Viewer Enabled"
checkable: true
checked: WebEngine.settings.pdfViewerEnabled
}
Menu {
id: imageAnimationPolicyMenu
title: "Image Animation Policy"
MenuItem {
id: disableImageAnimation
text: "Disable All Image Animation"
checkable: true
autoExclusive: true
checked: WebEngine.settings.imageAnimationPolicy === WebEngineSettings.ImageAnimationPolicy.Disallow
onTriggered: {
appSettings.imageAnimationPolicy = WebEngineSettings.ImageAnimationPolicy.Disallow
}
}
MenuItem {
id: allowImageAnimation
text: "Allow All Animated Images"
checkable: true
autoExclusive: true
checked: WebEngine.settings.imageAnimationPolicy === WebEngineSettings.ImageAnimationPolicy.Allow
onTriggered : {
appSettings.imageAnimationPolicy = WebEngineSettings.ImageAnimationPolicy.Allow
}
}
MenuItem {
id: animateImageOnce
text: "Animate Image Once"
checkable: true
autoExclusive: true
checked: WebEngine.settings.imageAnimationPolicy === WebEngineSettings.ImageAnimationPolicy.AnimateOnce
onTriggered : {
appSettings.imageAnimationPolicy = WebEngineSettings.ImageAnimationPolicy.AnimateOnce
}
}
}
MenuItem {
id: javascriptCanAccessClipboardMenuItem
text: "JavaScript can access clipboard"
checkable: true
checked: WebEngine.settings.javascriptCanAccessClipboard
}
MenuItem {
id: javascriptCanPasteMenuItem
text: "JavaScript can paste"
checkable: true
checked: WebEngine.settings.javascriptCanPaste
}
}
}
}
ProgressBar {
id: progressBar
height: 3
anchors {
left: parent.left
top: parent.bottom
right: parent.right
leftMargin: parent.anchors.leftMargin
rightMargin: parent.anchors.rightMargin
}
background: Item {}
z: -2
from: 0
to: 100
value: (win.currentWebView?.loadProgress < 100) ? win.currentWebView.loadProgress : 0
}
}
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: 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: devToolsEnabledMenuItem.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(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() { devToolsEnabledMenuItem.checked = false })
}
}
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: permissionDialog
anchors.centerIn: parent
width: Math.min(win.width, win.height) / 3 * 2
contentWidth: mainTextForPermissionDialog.width
contentHeight: mainTextForPermissionDialog.height
standardButtons: Dialog.No | Dialog.Yes
title: "Permission Request"
property var permission;
contentItem: Item {
Label {
id: mainTextForPermissionDialog
}
}
onAccepted: permission.grant()
onRejected: permission.deny()
onVisibleChanged: {
if (visible) {
mainTextForPermissionDialog.text = questionForPermissionType();
width = contentWidth + 20;
}
}
function questionForPermissionType() {
var question = "Allow " + permission.origin + " to "
switch (permission.permissionType) {
case WebEnginePermission.PermissionType.Geolocation:
question += "access your location information?";
break;
case WebEnginePermission.PermissionType.MediaAudioCapture:
question += "access your microphone?";
break;
case WebEnginePermission.PermissionType.MediaVideoCapture:
question += "access your webcam?";
break;
case WebEnginePermission.PermissionType.MediaAudioVideoCapture:
question += "access your microphone and webcam?";
break;
case WebEnginePermission.PermissionType.MouseLock:
question += "lock your mouse cursor?";
break;
case WebEnginePermission.PermissionType.DesktopVideoCapture:
question += "capture video of your desktop?";
break;
case WebEnginePermission.PermissionType.DesktopAudioVideoCapture:
question += "capture audio and video of your desktop?";
break;
case WebEnginePermission.PermissionType.Notifications:
question += "show notification on your desktop?";
break;
case WebEnginePermission.PermissionType.ClipboardReadWrite:
question += "read from and write to your clipboard?";
break;
case WebEnginePermission.PermissionType.LocalFontsAccess:
question += "access the fonts stored on your machine?";
break;
default:
question += "access unknown or unsupported permission type [" + permission.permissionType + "] ?";
break;
}
return question;
}
}
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
WebAuthDialog 254.0 WebAuthDialog.qml
depends QtQuick
<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>