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 os
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from pathlib import Path
from PySide6.QtCore import (QCoreApplication, QFileInfo, QMetaObject, QObject,
QUrl, Slot, Q_ARG)
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()
qml_file = os.fspath(Path(__file__).resolve().parent / 'ApplicationRoot.qml')
engine.load(QUrl.fromLocalFile(qml_file))
if not engine.rootObjects():
sys.exit(-1)
QMetaObject.invokeMethod(engine.rootObjects()[0], "load", Q_ARG("QVariant", url))
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 QtWebEngine
QtObject {
id: root
property WebEngineProfilePrototype defaultProfilePrototype : WebEngineProfilePrototype {
storageName: "Profile"
Component.onCompleted: {
let fullVersionList = root.defaultProfilePrototype.instance().clientHints.fullVersionList;
fullVersionList["QuickNanoBrowser"] = "1.0";
root.defaultProfilePrototype.instance().clientHints.fullVersionList = fullVersionList;
}
}
property WebEngineProfilePrototype otrPrototype : WebEngineProfilePrototype {
}
property Component browserWindowComponent: BrowserWindow {
applicationRoot: root
}
property Component browserDialogComponent: BrowserDialog {
onClosing: destroy()
}
function createWindow(profile) {
var newWindow = browserWindowComponent.createObject(root) as BrowserWindow;
newWindow.currentWebView.profile = profile;
profile.downloadRequested.connect(newWindow.onDownloadRequested);
return newWindow;
}
function createDialog(profile) {
var newDialog = browserDialogComponent.createObject(root) as BrowserDialog;
newDialog.currentWebView.profile = profile;
return newDialog;
}
function load(url) {
var browserWindow = createWindow(root.defaultProfilePrototype.instance());
browserWindow.currentWebView.url = url;
}
}
// 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) 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
required property QtObject applicationRoot
property WebEngineView currentWebView: tabBar.currentIndex < tabBar.count ? tabLayout.children[tabBar.currentIndex] : null
property int previousVisibility: Window.Windowed
property int createdTabs: 0
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: 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.ImageAnimationPolicy.Allow
property alias javascriptCanAccessClipboard: javascriptCanAccessClipboard.checked
property alias javascriptCanPaste: javascriptCanPaste.checked
}
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 (win.currentWebView)
win.currentWebView.reload();
}
}
Action {
shortcut: StandardKey.AddTab
onTriggered: {
tabBar.createTab(tabBar.count !== 0
? win.currentWebView.profile
: (win.applicationRoot as ApplicationRoot).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: "icons/3rdparty/go-previous.png"
onClicked: win.currentWebView.goBack()
enabled: win.currentWebView?.canGoBack ?? false
activeFocusOnTab: !win.platformIsMac
}
ToolButton {
id: forwardButton
icon.source: "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
? "icons/3rdparty/process-stop.png"
: "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: 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: win.currentWebView?.profile === (win.applicationRoot as ApplicationRoot).otrPrototype.instance()
onToggled: function() {
if (win.currentWebView) {
win.currentWebView.profile = offTheRecordEnabled.checked
? (win.applicationRoot as ApplicationRoot).otrPrototype.instance()
: (win.applicationRoot as ApplicationRoot).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: 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.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: javascriptCanAccessClipboard
text: "JavaScript can access clipboard"
checkable: true
checked: WebEngine.settings.javascriptCanAccessClipboard
}
MenuItem {
id: javascriptCanPaste
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: devToolsView.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(text.width + 30, 80)
implicitHeight: Math.max(text.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: text
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((win.applicationRoot as ApplicationRoot).defaultProfilePrototype.instance())
function createTab(profile, focusOnNewTab = true, url = undefined) {
var webview = tabComponent.createObject(tabLayout, {index: tabBar.count , 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 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;
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
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) {
var tab = tabBar.createTab(win.currentWebView.profile, true, request.requestedUrl);
tab.acceptAsNewWindow(request);
} else if (request.destination === WebEngineNewWindowRequest.InNewBackgroundTab) {
var backgroundTab = tabBar.createTab(win.currentWebView.profile, false);
backgroundTab.acceptAsNewWindow(request);
} else if (request.destination === WebEngineNewWindowRequest.InNewDialog) {
var dialog = (win.applicationRoot as ApplicationRoot).createDialog(win.currentWebView.profile);
dialog.win.currentWebView.acceptAsNewWindow(request);
} else {
var window = (win.applicationRoot as ApplicationRoot).createWindow(win.currentWebView.profile);
window.win.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);
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();
}
onPermissionRequested: function(permission) {
permissionDialog.permission = permission;
permissionDialog.visible = true;
}
onWebAuthUxRequested: function(request) {
webAuthDialog.init(request);
}
Timer {
id: reloadTimer
interval: 0
running: false
repeat: false
onTriggered: win.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(win.currentWebView.profile);
request.openIn(tab);
}
Timer {
id: hideTimer
interval: 0
running: false
repeat: false
onTriggered: devToolsEnabled.checked = false
}
onWindowCloseRequested: function() {
// 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: 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
}
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 (var 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: "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
width: Math.min(parent.parent.width, parent.parent.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 {
id: selectAccountRepeaterColumn
required property string modelData
spacing : 5
RadioButton {
text: selectAccountRepeaterColumn.modelData
ButtonGroup.group : webAuthDialog.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 (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"
}
}
<RCC>
<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>