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
"""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("https://www.qt.io")
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))
app.exec()
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtWebEngine
QtObject {
id: root
property QtObject defaultProfile: WebEngineProfile {
storageName: "Profile"
offTheRecord: false
}
property QtObject otrProfile: WebEngineProfile {
offTheRecord: true
}
property Component browserWindowComponent: BrowserWindow {
applicationRoot: root
}
property Component browserDialogComponent: BrowserDialog {
onClosing: destroy()
}
function createWindow(profile) {
var newWindow = browserWindowComponent.createObject(root);
newWindow.currentWebView.profile = profile;
profile.downloadRequested.connect(newWindow.onDownloadRequested);
return newWindow;
}
function createDialog(profile) {
var newDialog = browserDialogComponent.createObject(root);
newDialog.currentWebView.profile = profile;
return newDialog;
}
function load(url) {
var browserWindow = createWindow(defaultProfile);
browserWindow.currentWebView.url = url;
}
}
// 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
import QtCore
import QtQml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import QtWebEngine
import BrowserUtils
ApplicationWindow {
id: browserWindow
property QtObject applicationRoot
property Item currentWebView: tabBar.currentIndex < tabBar.count ? tabLayout.children[tabBar.currentIndex] : null
property int previousVisibility: Window.Windowed
property int createdTabs: 0
width: 1300
height: 900
visible: true
title: currentWebView && currentWebView.title
// Make sure the Qt.WindowFullscreenButtonHint is set on OS X.
Component.onCompleted: flags = flags | Qt.WindowFullscreenButtonHint
onCurrentWebViewChanged: {
findBar.reset();
}
// When using style "mac", ToolButtons are not supposed to accept focus.
property bool platformIsMac: Qt.platform.os == "osx"
Settings {
id : appSettings
property alias autoLoadImages: loadImages.checked
property alias javaScriptEnabled: javaScriptEnabled.checked
property alias errorPageEnabled: errorPageEnabled.checked
property alias pluginsEnabled: pluginsEnabled.checked
property alias fullScreenSupportEnabled: fullScreenSupportEnabled.checked
property alias autoLoadIconsForPage: autoLoadIconsForPage.checked
property alias touchIconsEnabled: touchIconsEnabled.checked
property alias webRTCPublicInterfacesOnly : webRTCPublicInterfacesOnly.checked
property alias devToolsEnabled: devToolsEnabled.checked
property alias pdfViewerEnabled: pdfViewerEnabled.checked
}
Action {
shortcut: "Ctrl+D"
onTriggered: {
downloadView.visible = !downloadView.visible;
}
}
Action {
id: focus
shortcut: "Ctrl+L"
onTriggered: {
addressBar.forceActiveFocus();
addressBar.selectAll();
}
}
Action {
shortcut: StandardKey.Refresh
onTriggered: {
if (currentWebView)
currentWebView.reload();
}
}
Action {
shortcut: StandardKey.AddTab
onTriggered: {
tabBar.createTab(tabBar.count != 0 ? currentWebView.profile : defaultProfile);
addressBar.forceActiveFocus();
addressBar.selectAll();
}
}
Action {
shortcut: StandardKey.Close
onTriggered: {
currentWebView.triggerWebAction(WebEngineView.RequestClose);
}
}
Action {
shortcut: StandardKey.Quit
onTriggered: browserWindow.close()
}
Action {
shortcut: "Escape"
onTriggered: {
if (currentWebView.state == "FullScreen") {
browserWindow.visibility = browserWindow.previousVisibility;
fullScreenNotification.hide();
currentWebView.triggerWebAction(WebEngineView.ExitFullScreen);
}
if (findBar.visible)
findBar.visible = false;
}
}
Action {
shortcut: "Ctrl+0"
onTriggered: currentWebView.zoomFactor = 1.0
}
Action {
shortcut: StandardKey.ZoomOut
onTriggered: currentWebView.zoomFactor -= 0.1
}
Action {
shortcut: StandardKey.ZoomIn
onTriggered: currentWebView.zoomFactor += 0.1
}
Action {
shortcut: StandardKey.Copy
onTriggered: currentWebView.triggerWebAction(WebEngineView.Copy)
}
Action {
shortcut: StandardKey.Cut
onTriggered: currentWebView.triggerWebAction(WebEngineView.Cut)
}
Action {
shortcut: StandardKey.Paste
onTriggered: currentWebView.triggerWebAction(WebEngineView.Paste)
}
Action {
shortcut: "Shift+"+StandardKey.Paste
onTriggered: currentWebView.triggerWebAction(WebEngineView.PasteAndMatchStyle)
}
Action {
shortcut: StandardKey.SelectAll
onTriggered: currentWebView.triggerWebAction(WebEngineView.SelectAll)
}
Action {
shortcut: StandardKey.Undo
onTriggered: currentWebView.triggerWebAction(WebEngineView.Undo)
}
Action {
shortcut: StandardKey.Redo
onTriggered: currentWebView.triggerWebAction(WebEngineView.Redo)
}
Action {
shortcut: StandardKey.Back
onTriggered: currentWebView.triggerWebAction(WebEngineView.Back)
}
Action {
shortcut: StandardKey.Forward
onTriggered: currentWebView.triggerWebAction(WebEngineView.Forward)
}
Action {
shortcut: StandardKey.Find
onTriggered: {
if (!findBar.visible)
findBar.visible = true;
}
}
Action {
shortcut: StandardKey.FindNext
onTriggered: findBar.findNext()
}
Action {
shortcut: StandardKey.FindPrevious
onTriggered: findBar.findPrevious()
}
menuBar: ToolBar {
id: navigationBar
RowLayout {
anchors.fill: parent
ToolButton {
enabled: currentWebView && (currentWebView.canGoBack || currentWebView.canGoForward)
onClicked: historyMenu.open()
text: qsTr("▼")
Menu {
id: historyMenu
Instantiator {
model: currentWebView && currentWebView.history.items
MenuItem {
text: model.title
onTriggered: currentWebView.goBackOrForward(model.offset)
checkable: !enabled
checked: !enabled
enabled: model.offset
}
onObjectAdded: function(index, object) {
historyMenu.insertItem(index, object)
}
onObjectRemoved: function(index, object) {
historyMenu.removeItem(object)
}
}
}
}
ToolButton {
id: backButton
icon.source: "qrc:/icons/go-previous.png"
onClicked: currentWebView.goBack()
enabled: currentWebView && currentWebView.canGoBack
activeFocusOnTab: !browserWindow.platformIsMac
}
ToolButton {
id: forwardButton
icon.source: "qrc:/icons/go-next.png"
onClicked: currentWebView.goForward()
enabled: currentWebView && currentWebView.canGoForward
activeFocusOnTab: !browserWindow.platformIsMac
}
ToolButton {
id: reloadButton
icon.source: currentWebView && currentWebView.loading ? "qrc:/icons/process-stop.png" : "qrc:/icons/view-refresh.png"
onClicked: currentWebView && currentWebView.loading ? currentWebView.stop() : currentWebView.reload()
activeFocusOnTab: !browserWindow.platformIsMac
}
TextField {
id: addressBar
Image {
anchors.verticalCenter: addressBar.verticalCenter;
x: 5
z: 2
id: faviconImage
width: 16; height: 16
sourceSize: Qt.size(width, height)
source: currentWebView && currentWebView.icon ? currentWebView.icon : ''
}
MouseArea {
id: textFieldMouseArea
acceptedButtons: Qt.RightButton
anchors.fill: parent
onClicked: {
var textSelectionStartPos = addressBar.selectionStart;
var textSelectionEndPos = addressBar.selectionEnd;
textFieldContextMenu.open();
addressBar.select(textSelectionStartPos, textSelectionEndPos);
}
Menu {
id: textFieldContextMenu
x: textFieldMouseArea.mouseX
y: textFieldMouseArea.mouseY
MenuItem {
text: qsTr("Cut")
onTriggered: addressBar.cut()
enabled: addressBar.selectedText.length > 0
}
MenuItem {
text: qsTr("Copy")
onTriggered: addressBar.copy()
enabled: addressBar.selectedText.length > 0
}
MenuItem {
text: qsTr("Paste")
onTriggered: addressBar.paste()
enabled: addressBar.canPaste
}
MenuItem {
text: qsTr("Delete")
onTriggered: addressBar.text = qsTr("")
enabled: addressBar.selectedText.length > 0
}
MenuSeparator {}
MenuItem {
text: qsTr("Select All")
onTriggered: addressBar.selectAll()
enabled: addressBar.text.length > 0
}
}
}
leftPadding: 26
focus: true
Layout.fillWidth: true
Binding on text {
when: currentWebView
value: currentWebView.url
}
onAccepted: currentWebView.url = Utils.fromUserInput(text)
selectByMouse: true
}
ToolButton {
id: settingsMenuButton
text: qsTr("⋮")
onClicked: settingsMenu.open()
Menu {
id: settingsMenu
y: settingsMenuButton.height
MenuItem {
id: loadImages
text: "Autoload images"
checkable: true
checked: WebEngine.settings.autoLoadImages
}
MenuItem {
id: javaScriptEnabled
text: "JavaScript On"
checkable: true
checked: WebEngine.settings.javascriptEnabled
}
MenuItem {
id: errorPageEnabled
text: "ErrorPage On"
checkable: true
checked: WebEngine.settings.errorPageEnabled
}
MenuItem {
id: pluginsEnabled
text: "Plugins On"
checkable: true
checked: true
}
MenuItem {
id: fullScreenSupportEnabled
text: "FullScreen On"
checkable: true
checked: WebEngine.settings.fullScreenSupportEnabled
}
MenuItem {
id: offTheRecordEnabled
text: "Off The Record"
checkable: true
checked: currentWebView && currentWebView.profile === otrProfile
onToggled: function(checked) {
if (currentWebView) {
currentWebView.profile = checked ? otrProfile : defaultProfile;
}
}
}
MenuItem {
id: httpDiskCacheEnabled
text: "HTTP Disk Cache"
checkable: currentWebView && !currentWebView.profile.offTheRecord
checked: currentWebView && (currentWebView.profile.httpCacheType === WebEngineProfile.DiskHttpCache)
onToggled: function(checked) {
if (currentWebView) {
currentWebView.profile.httpCacheType = checked ? WebEngineProfile.DiskHttpCache : WebEngineProfile.MemoryHttpCache;
}
}
}
MenuItem {
id: autoLoadIconsForPage
text: "Icons On"
checkable: true
checked: WebEngine.settings.autoLoadIconsForPage
}
MenuItem {
id: touchIconsEnabled
text: "Touch Icons On"
checkable: true
checked: WebEngine.settings.touchIconsEnabled
enabled: autoLoadIconsForPage.checked
}
MenuItem {
id: webRTCPublicInterfacesOnly
text: "WebRTC Public Interfaces Only"
checkable: true
checked: WebEngine.settings.webRTCPublicInterfacesOnly
}
MenuItem {
id: devToolsEnabled
text: "Open DevTools"
checkable: true
checked: false
}
MenuItem {
id: pdfViewerEnabled
text: "PDF viewer enabled"
checkable: true
checked: WebEngine.settings.pdfViewerEnabled
}
}
}
}
ProgressBar {
id: progressBar
height: 3
anchors {
left: parent.left
top: parent.bottom
right: parent.right
leftMargin: parent.leftMargin
rightMargin: parent.rightMargin
}
background: Item {}
z: -2
from: 0
to: 100
value: (currentWebView && currentWebView.loadProgress < 100) ? currentWebView.loadProgress : 0
}
}
StackLayout {
id: tabLayout
currentIndex: tabBar.currentIndex
anchors.top: tabBar.bottom
anchors.bottom: devToolsView.top
anchors.left: parent.left
anchors.right: parent.right
}
Component {
id: tabButtonComponent
TabButton {
property color frameColor: "#999"
property color fillColor: "#eee"
property color nonSelectedColor: "#ddd"
property string tabTitle: "New Tab"
id: tabButton
contentItem: Rectangle {
id: tabRectangle
color: tabButton.down ? fillColor : nonSelectedColor
border.width: 1
border.color: frameColor
implicitWidth: Math.max(text.width + 30, 80)
implicitHeight: Math.max(text.height + 10, 20)
Rectangle { height: 1 ; width: parent.width ; color: frameColor}
Rectangle { height: parent.height ; width: 1; color: frameColor}
Rectangle { x: parent.width - 2; height: parent.height ; width: 1; color: frameColor}
Text {
id: text
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 6
text: tabButton.tabTitle
elide: Text.ElideRight
color: tabButton.down ? "black" : frameColor
width: parent.width - button.background.width
}
Button {
id: button
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: 4
height: 12
background: Rectangle {
implicitWidth: 12
implicitHeight: 12
color: button.hovered ? "#ccc" : tabRectangle.color
Text {text: "x"; anchors.centerIn: parent; color: "gray"}
}
onClicked: tabButton.closeTab()
}
}
onClicked: addressBar.text = tabLayout.itemAt(TabBar.index).url;
function closeTab() {
tabBar.removeView(TabBar.index);
}
}
}
TabBar {
id: tabBar
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
Component.onCompleted: createTab(defaultProfile)
function createTab(profile, focusOnNewTab = true, url = undefined) {
var webview = tabComponent.createObject(tabLayout, {profile: profile});
var newTabButton = tabButtonComponent.createObject(tabBar, {tabTitle: Qt.binding(function () { return webview.title; })});
tabBar.addItem(newTabButton);
if (focusOnNewTab) {
tabBar.setCurrentIndex(tabBar.count - 1);
}
if (url !== undefined) {
webview.url = url;
}
return webview;
}
function removeView(index) {
tabBar.removeItem(index);
if (tabBar.count > 1) {
tabBar.removeItem(tabBar.itemAt(index));
tabLayout.children[index].destroy();
} else {
browserWindow.close();
}
}
Component {
id: tabComponent
WebEngineView {
id: webEngineView
focus: true
onLinkHovered: function(hoveredUrl) {
if (hoveredUrl == "")
hideStatusText.start();
else {
statusText.text = hoveredUrl;
statusBubble.visible = true;
hideStatusText.stop();
}
}
states: [
State {
name: "FullScreen"
PropertyChanges {
target: tabBar
visible: false
height: 0
}
PropertyChanges {
target: navigationBar
visible: false
}
}
]
settings.localContentCanAccessRemoteUrls: true
settings.localContentCanAccessFileUrls: false
settings.autoLoadImages: appSettings.autoLoadImages
settings.javascriptEnabled: appSettings.javaScriptEnabled
settings.errorPageEnabled: appSettings.errorPageEnabled
settings.pluginsEnabled: appSettings.pluginsEnabled
settings.fullScreenSupportEnabled: appSettings.fullScreenSupportEnabled
settings.autoLoadIconsForPage: appSettings.autoLoadIconsForPage
settings.touchIconsEnabled: appSettings.touchIconsEnabled
settings.webRTCPublicInterfacesOnly: appSettings.webRTCPublicInterfacesOnly
settings.pdfViewerEnabled: appSettings.pdfViewerEnabled
onCertificateError: function(error) {
error.defer();
sslDialog.enqueue(error);
}
onNewWindowRequested: function(request) {
if (!request.userInitiated)
console.warn("Blocked a popup window.");
else if (request.destination === WebEngineNewWindowRequest.InNewTab) {
var tab = tabBar.createTab(currentWebView.profile, true, request.requestedUrl);
tab.acceptAsNewWindow(request);
} else if (request.destination === WebEngineNewWindowRequest.InNewBackgroundTab) {
var backgroundTab = tabBar.createTab(currentWebView.profile, false);
backgroundTab.acceptAsNewWindow(request);
} else if (request.destination === WebEngineNewWindowRequest.InNewDialog) {
var dialog = applicationRoot.createDialog(currentWebView.profile);
dialog.currentWebView.acceptAsNewWindow(request);
} else {
var window = applicationRoot.createWindow(currentWebView.profile);
window.currentWebView.acceptAsNewWindow(request);
}
}
onFullScreenRequested: function(request) {
if (request.toggleOn) {
webEngineView.state = "FullScreen";
browserWindow.previousVisibility = browserWindow.visibility;
browserWindow.showFullScreen();
fullScreenNotification.show();
} else {
webEngineView.state = "";
browserWindow.visibility = browserWindow.previousVisibility;
fullScreenNotification.hide();
}
request.accept();
}
onRegisterProtocolHandlerRequested: function(request) {
console.log("accepting registerProtocolHandler request for "
+ request.scheme + " from " + request.origin);
request.accept();
}
onRenderProcessTerminated: function(terminationStatus, exitCode) {
var status = "";
switch (terminationStatus) {
case WebEngineView.NormalTerminationStatus:
status = "(normal exit)";
break;
case WebEngineView.AbnormalTerminationStatus:
status = "(abnormal exit)";
break;
case WebEngineView.CrashedTerminationStatus:
status = "(crashed)";
break;
case WebEngineView.KilledTerminationStatus:
status = "(killed)";
break;
}
print("Render process exited with code " + exitCode + " " + status);
reloadTimer.running = true;
}
onSelectClientCertificate: function(selection) {
selection.certificates[0].select();
}
onFindTextFinished: function(result) {
if (!findBar.visible)
findBar.visible = true;
findBar.numberOfMatches = result.numberOfMatches;
findBar.activeMatch = result.activeMatch;
}
onLoadingChanged: function(loadRequest) {
if (loadRequest.status == WebEngineView.LoadStartedStatus)
findBar.reset();
}
onFeaturePermissionRequested: function(securityOrigin, feature) {
featurePermissionDialog.securityOrigin = securityOrigin;
featurePermissionDialog.feature = feature;
featurePermissionDialog.visible = true;
}
Timer {
id: reloadTimer
interval: 0
running: false
repeat: false
onTriggered: currentWebView.reload()
}
}
}
}
WebEngineView {
id: devToolsView
visible: devToolsEnabled.checked
height: visible ? 400 : 0
inspectedView: visible && tabBar.currentIndex < tabBar.count ? tabLayout.children[tabBar.currentIndex] : null
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
onNewWindowRequested: function(request) {
var tab = tabBar.createTab(currentWebView.profile);
request.openIn(tab);
}
Timer {
id: hideTimer
interval: 0
running: false
repeat: false
onTriggered: devToolsEnabled.checked = false
}
onWindowCloseRequested: function(request) {
// Delay hiding for keep the inspectedView set to receive the ACK message of close.
hideTimer.running = true;
}
}
Dialog {
id: sslDialog
anchors.centerIn: parent
contentWidth: Math.max(mainTextForSSLDialog.width, detailedTextForSSLDialog.width)
contentHeight: mainTextForSSLDialog.height + detailedTextForSSLDialog.height
property var certErrors: []
// fixme: icon!
// icon: StandardIcon.Warning
standardButtons: Dialog.No | Dialog.Yes
title: "Server's certificate not trusted"
contentItem: Item {
Label {
id: mainTextForSSLDialog
text: "Do you wish to continue?"
}
Text {
id: detailedTextForSSLDialog
anchors.top: mainTextForSSLDialog.bottom
text: "If you wish so, you may continue with an unverified certificate.\n" +
"Accepting an unverified certificate means\n" +
"you may not be connected with the host you tried to connect to.\n" +
"Do you wish to override the security check and continue?"
}
}
onAccepted: {
certErrors.shift().acceptCertificate();
presentError();
}
onRejected: reject()
function reject(){
certErrors.shift().rejectCertificate();
presentError();
}
function enqueue(error){
certErrors.push(error);
presentError();
}
function presentError(){
visible = certErrors.length > 0
}
}
Dialog {
id: featurePermissionDialog
anchors.centerIn: parent
width: Math.min(browserWindow.width, browserWindow.height) / 3 * 2
contentWidth: mainTextForPermissionDialog.width
contentHeight: mainTextForPermissionDialog.height
standardButtons: Dialog.No | Dialog.Yes
title: "Permission Request"
property var feature;
property url securityOrigin;
contentItem: Item {
Label {
id: mainTextForPermissionDialog
text: featurePermissionDialog.questionForFeature()
}
}
onAccepted: currentWebView && currentWebView.grantFeaturePermission(securityOrigin, feature, true)
onRejected: currentWebView && currentWebView.grantFeaturePermission(securityOrigin, feature, false)
onVisibleChanged: {
if (visible)
width = contentWidth + 20;
}
function questionForFeature() {
var question = "Allow " + securityOrigin + " to "
switch (feature) {
case WebEngineView.Geolocation:
question += "access your location information?";
break;
case WebEngineView.MediaAudioCapture:
question += "access your microphone?";
break;
case WebEngineView.MediaVideoCapture:
question += "access your webcam?";
break;
case WebEngineView.MediaVideoCapture:
question += "access your microphone and webcam?";
break;
case WebEngineView.MouseLock:
question += "lock your mouse cursor?";
break;
case WebEngineView.DesktopVideoCapture:
question += "capture video of your desktop?";
break;
case WebEngineView.DesktopAudioVideoCapture:
question += "capture audio and video of your desktop?";
break;
case WebEngineView.Notifications:
question += "show notification on your desktop?";
break;
default:
question += "access unknown or unsupported feature [" + feature + "] ?";
break;
}
return question;
}
}
FullScreenNotification {
id: fullScreenNotification
}
DownloadView {
id: downloadView
visible: false
anchors.fill: parent
}
function onDownloadRequested(download) {
downloadView.visible = true;
downloadView.append(download);
download.accept();
}
FindBar {
id: findBar
visible: false
anchors.right: parent.right
anchors.rightMargin: 10
anchors.top: parent.top
onFindNext: {
if (text)
currentWebView && currentWebView.findText(text);
else if (!visible)
visible = true;
}
onFindPrevious: {
if (text)
currentWebView && currentWebView.findText(text, WebEngineView.FindBackward);
else if (!visible)
visible = true;
}
}
Rectangle {
id: statusBubble
color: "oldlace"
property int padding: 8
visible: false
anchors.left: parent.left
anchors.bottom: parent.bottom
width: statusText.paintedWidth + padding
height: statusText.paintedHeight + padding
Text {
id: statusText
anchors.centerIn: statusBubble
elide: Qt.ElideMiddle
Timer {
id: hideStatusText
interval: 750
onTriggered: {
statusText.text = "";
statusBubble.visible = false;
}
}
}
}
}
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls
import QtWebEngine
import QtQuick.Layouts
Rectangle {
id: downloadView
color: "lightgray"
ListModel {
id: downloadModel
property var downloads: []
}
function append(download) {
downloadModel.append(download);
downloadModel.downloads.push(download);
}
Component {
id: downloadItemDelegate
Rectangle {
width: listView.width
height: childrenRect.height
anchors.margins: 10
radius: 3
color: "transparent"
border.color: "black"
Rectangle {
id: progressBar
property real progress: downloadModel.downloads[index]
? downloadModel.downloads[index].receivedBytes / downloadModel.downloads[index].totalBytes : 0
radius: 3
color: width == listView.width ? "green" : "#2b74c7"
width: listView.width * progress
height: cancelButton.height
Behavior on width {
SmoothedAnimation { duration: 100 }
}
}
Rectangle {
anchors {
left: parent.left
right: parent.right
leftMargin: 20
}
Label {
id: label
text: downloadModel.downloads[index] ? downloadModel.downloads[index].downloadDirectory + "/" + downloadModel.downloads[index].downloadFileName : qsTr("")
anchors {
verticalCenter: cancelButton.verticalCenter
left: parent.left
right: cancelButton.left
}
}
Button {
id: cancelButton
anchors.right: parent.right
icon.source: "qrc:/icons/process-stop.png"
onClicked: {
var download = downloadModel.downloads[index];
download.cancel();
downloadModel.downloads = downloadModel.downloads.filter(function (el) {
return el.id !== download.id;
});
downloadModel.remove(index);
}
}
}
}
}
ListView {
id: listView
anchors {
topMargin: 10
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
width: parent.width - 20
spacing: 5
model: downloadModel
delegate: downloadItemDelegate
Text {
visible: !listView.count
horizontalAlignment: Text.AlignHCenter
height: 30
anchors {
top: parent.top
left: parent.left
right: parent.right
}
font.pixelSize: 20
text: "No active downloads."
}
Rectangle {
color: "gray"
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
}
height: 30
Button {
id: okButton
text: "OK"
anchors.centerIn: parent
onClicked: {
downloadView.visible = false;
}
}
}
}
}
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
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
background: Rectangle {
color: "transparent"
}
onAccepted: root.findNext()
onTextChanged: root.findNext()
onActiveFocusChanged: activeFocus ? selectAll() : deselect()
}
}
Label {
text: activeMatch + "/" + numberOfMatches
visible: findTextField.text != ""
}
Rectangle {
border.width: 1
border.color: "#ddd"
width: 2
height: parent.height
anchors.topMargin: 5
anchors.bottomMargin: 5
}
ToolButton {
text: "<"
enabled: numberOfMatches > 0
onClicked: root.findPrevious()
}
ToolButton {
text: ">"
enabled: numberOfMatches > 0
onClicked: root.findNext()
}
ToolButton {
text: "x"
onClicked: root.visible = false
}
}
}
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
Rectangle {
id: fullScreenNotification
width: 500
height: 40
color: "white"
radius: 7
visible: false
opacity: 0
function show() {
visible = true;
opacity = 1;
reset.start();
}
function hide() {
reset.stop();
opacity = 0;
}
Behavior on opacity {
NumberAnimation {
duration: 750
onStopped: {
if (opacity == 0)
visible = false;
}
}
}
Timer {
id: reset
interval: 5000
onTriggered: hide()
}
anchors.horizontalCenter: parent.horizontalCenter
y: 125
Text {
id: message
width: parent.width
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
elide: Text.ElideNone
clip: true
text: qsTr("You are now in fullscreen mode. Press ESC to quit!")
}
}
<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>