2020-09-22 15:12:48 +00:00
import QtQuick 2.13
2020-09-28 18:33:37 +00:00
import QtQuick . Controls 2.13
import QtQuick . Controls 1.0 as QQC1
2020-09-22 15:12:48 +00:00
import QtQuick . Layouts 1.13
2020-09-22 19:16:44 +00:00
import QtWebEngine 1.10
import QtWebChannel 1.13
2020-09-28 18:33:37 +00:00
import Qt . labs . settings 1.0
import QtQuick . Controls . Styles 1.0
import QtQuick . Dialogs 1.2
2020-09-25 19:05:07 +00:00
import "../../../shared"
import "../../../imports"
2020-09-22 15:12:48 +00:00
2020-09-28 18:33:37 +00:00
// Code based on https://code.qt.io/cgit/qt/qtwebengine.git/tree/examples/webengine/quicknanobrowser/BrowserWindow.qml?h=5.15
// Licensed under BSD
2020-09-22 15:12:48 +00:00
Item {
2020-09-28 18:33:37 +00:00
id: browserWindow
property Item currentWebView: tabs . currentIndex < tabs . count ? tabs . getTab ( tabs . currentIndex ) . item : null
property Component browserDialogComponent: BrowserDialog {
onClosing: destroy ( )
}
2020-09-22 15:12:48 +00:00
Layout.fillHeight: true
Layout.fillWidth: true
2020-09-25 19:05:07 +00:00
property var request: { "hostname" : "" , "permission" : "" }
2020-09-22 19:16:44 +00:00
2020-09-25 19:05:07 +00:00
function postMessage ( isAllowed ) {
request . isAllowed = isAllowed ;
provider . web3Response ( web3Provider . postMessage ( JSON . stringify ( request ) ) ) ;
}
ModalPopup {
id: accessDialog
// TODO: design required
StyledText {
id: siteName
text: request . hostname
anchors.top: parent . top
anchors.topMargin: Style . current . padding
width: parent . width
wrapMode: Text . WordWrap
}
StyledText {
id: permission
text: qsTr ( "Permission requested: %1" ) . arg ( request . permission )
anchors.top: siteName . bottom
anchors.topMargin: Style . current . padding
width: parent . width
wrapMode: Text . WordWrap
}
StyledText {
id: description
anchors.top: permission . bottom
anchors.topMargin: Style . current . padding
width: parent . width
wrapMode: Text . WordWrap
text: {
switch ( request . permission ) {
case Constants.permission_web3: return qsTr ( "Allowing authorizes this DApp to retrieve your wallet address and enable Web3" ) ;
case Constants.permission_contactCode: return qsTr ( "Granting access authorizes this DApp to retrieve your chat key" ) ;
default: return qsTr ( "Unknown permission" ) ;
}
}
}
StyledButton {
anchors.bottom: parent . bottom
anchors.bottomMargin: Style . current . padding
anchors.left: parent . left
anchors.leftMargin: Style . current . padding
label: qsTr ( "Allow" )
onClicked: {
postMessage ( true ) ;
accessDialog . close ( ) ;
}
}
StyledButton {
anchors.bottom: parent . bottom
anchors.bottomMargin: Style . current . padding
anchors.right: parent . right
anchors.rightMargin: Style . current . padding
label: qsTr ( "Deny" )
onClicked: {
postMessage ( false ) ;
accessDialog . close ( ) ;
}
}
}
2020-09-22 19:16:44 +00:00
QtObject {
id: provider
WebChannel.id: "backend"
signal web3Response ( string data ) ;
function postMessage ( data ) {
2020-09-25 19:05:07 +00:00
request = JSON . parse ( data )
if ( request . type === Constants . api_request ) {
// TODO: check if permission has been granted before,
// to not show the dialog
accessDialog . open ( )
} else {
web3Response ( web3Provider . postMessage ( data ) ) ;
}
2020-09-22 19:16:44 +00:00
}
2020-09-24 02:52:41 +00:00
property int networkId: web3Provider . networkId
2020-09-22 19:16:44 +00:00
}
WebChannel {
id: channel
registeredObjects: [ provider ]
}
2020-09-28 18:33:37 +00:00
property QtObject defaultProfile: WebEngineProfile {
storageName: "Profile"
offTheRecord: false
useForGlobalCertificateVerification: true
userScripts: [
WebEngineScript {
injectionPoint: WebEngineScript . DocumentCreation
sourceUrl: Qt . resolvedUrl ( "provider.js" )
worldId: WebEngineScript . MainWorld // TODO: check https://doc.qt.io/qt-5/qml-qtwebengine-webenginescript.html#worldId-prop
}
]
}
property QtObject otrProfile: WebEngineProfile {
offTheRecord: true
persistentCookiesPolicy: WebEngineProfile . NoPersistentCookies
userScripts: [
WebEngineScript {
injectionPoint: WebEngineScript . DocumentCreation
sourceUrl: Qt . resolvedUrl ( "provider.js" )
worldId: WebEngineScript . MainWorld // TODO: check https://doc.qt.io/qt-5/qml-qtwebengine-webenginescript.html#worldId-prop
}
]
}
onCurrentWebViewChanged: {
findBar . reset ( ) ;
}
Settings {
id : browserSettings
property alias autoLoadImages: loadImages . checked
property alias javaScriptEnabled: javaScriptEnabled . checked
property alias errorPageEnabled: errorPageEnabled . checked
property alias pluginsEnabled: pluginsEnabled . 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: {
tabs . createEmptyTab ( tabs . count != 0 ? currentWebView.profile : defaultProfile ) ;
tabs . currentIndex = tabs . count - 1 ;
addressBar . forceActiveFocus ( ) ;
addressBar . selectAll ( ) ;
}
}
Action {
shortcut: StandardKey . Close
onTriggered: {
currentWebView . triggerWebAction ( WebEngineView . RequestClose ) ;
}
}
Action {
shortcut: "Escape"
onTriggered: {
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 ( )
}
Action {
shortcut: "F12"
onTriggered: {
browserSettings . devToolsEnabled = ! browserSettings . devToolsEnabled
}
}
Item {
id: navigationBar
anchors.left: parent . left
anchors.right: parent . right
height: 45
RowLayout {
anchors.fill: parent
Menu {
id: historyMenu
Instantiator {
model: currentWebView && currentWebView . navigationHistory . 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: "../../img/browser/back.png"
onClicked: currentWebView . goBack ( )
onPressAndHold: {
if ( currentWebView && ( currentWebView . canGoBack || currentWebView . canGoForward ) ) {
historyMenu . popup ( backButton . x , backButton . y + backButton . height )
}
}
enabled: currentWebView && currentWebView . canGoBack
}
ToolButton {
id: forwardButton
icon.source: "../../img/browser/forward.png"
onClicked: currentWebView . goForward ( )
enabled: currentWebView && currentWebView . canGoForward
onPressAndHold: {
if ( currentWebView && ( currentWebView . canGoBack || currentWebView . canGoForward ) ) {
historyMenu . popup ( forwardButton . x , forwardButton . y + forwardButton . height )
}
}
}
ToolButton {
id: reloadButton
icon.source: currentWebView && currentWebView . loading ? "../../img/browser/stop.png" : "../../img/browser/refresh.png"
onClicked: currentWebView && currentWebView . loading ? currentWebView . stop ( ) : currentWebView . reload ( )
}
StyledTextField {
id: addressBar
Layout.fillWidth: true
background: Rectangle {
border.color: Style . current . secondaryText
border.width: 1
radius: 2
}
leftPadding: 25
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 : ""
}
focus: true
text: currentWebView && currentWebView . url
Keys.onPressed: {
// TODO: disable browsing local files? file://
if ( event . key === Qt . Key_Enter || event . key === Qt . Key_Return ) {
currentWebView . url = utilsModel . urlFromUserInput ( text ) ;
}
}
}
Menu {
id: settingsMenu
y: settingsMenuButton . height
x: settingsMenuButton . x
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: 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
}
}
ToolButton {
id: settingsMenuButton
text: qsTr ( "⋮" )
onClicked: settingsMenu . open ( )
}
}
}
QQC1 . TabView {
id: tabs
function createEmptyTab ( profile ) {
var tab = addTab ( "" , tabComponent ) ;
// We must do this first to make sure that tab.active gets set so that tab.item gets instantiated immediately.
tab . active = true ;
tab . title = Qt . binding ( function ( ) { return tab . item . title ? tab.item.title : qsTr ( "New Tab" ) } ) ;
tab . item . profile = profile ;
return tab ;
}
function indexOfView ( view ) {
for ( let i = 0 ; i < tabs . count ; ++ i )
if ( tabs . getTab ( i ) . item == view )
return i
return - 1
}
function removeView ( index ) {
if ( tabs . count > 1 )
tabs . removeTab ( index )
}
anchors.top: navigationBar . bottom
anchors.bottom: devToolsView . top
anchors.left: parent . left
anchors.right: parent . right
Component.onCompleted: {
var tab = createEmptyTab ( defaultProfile )
tab . item . url = "https://status-im.github.io/dapp"
}
// Add custom tab view style so we can customize the tabs to include a close button
style: TabViewStyle {
property color frameColor: "#999"
property color fillColor: "#eee"
property color nonSelectedColor: "#ddd"
frameOverlap: 1
frame: Rectangle {
color: "#eee"
border.color: frameColor
}
tab: Rectangle {
id: tabRectangle
color: styleData . selected ? 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: styleData . title
elide: Text . ElideRight
color: styleData . selected ? "black" : frameColor
}
Image {
visible: tabs . count > 1
source: "../../img/browser/close.png"
width: 12
height: 16
anchors.right: parent . right
anchors.verticalCenter: parent . verticalCenter
anchors.rightMargin: 4
MouseArea {
anchors.fill: parent
enabled: tabs . count > 1
onClicked: tabs . removeView ( styleData . index )
}
}
}
}
Component {
id: tabComponent
WebEngineView {
id: webEngineView
focus: true
webChannel: channel
onLinkHovered: function ( hoveredUrl ) {
if ( hoveredUrl == "" )
hideStatusText . start ( ) ;
else {
statusText . text = hoveredUrl ;
statusBubble . visible = true ;
hideStatusText . stop ( ) ;
}
}
settings.autoLoadImages: browserSettings . autoLoadImages
settings.javascriptEnabled: browserSettings . javaScriptEnabled
settings.errorPageEnabled: browserSettings . errorPageEnabled
settings.pluginsEnabled: browserSettings . pluginsEnabled
settings.autoLoadIconsForPage: browserSettings . autoLoadIconsForPage
settings.touchIconsEnabled: browserSettings . touchIconsEnabled
settings.webRTCPublicInterfacesOnly: browserSettings . webRTCPublicInterfacesOnly
settings.pdfViewerEnabled: browserSettings . pdfViewerEnabled
onCertificateError: function ( error ) {
error . defer ( ) ;
sslDialog . enqueue ( error ) ;
}
onNewViewRequested: function ( request ) {
if ( ! request . userInitiated )
print ( "Warning: Blocked a popup window." ) ;
else if ( request . destination === WebEngineView . NewViewInTab ) {
var tab = tabs . createEmptyTab ( currentWebView . profile ) ;
tabs . currentIndex = tabs . count - 1 ;
request . openIn ( tab . item ) ;
} else if ( request . destination === WebEngineView . NewViewInBackgroundTab ) {
var backgroundTab = tabs . createEmptyTab ( currentWebView . profile ) ;
request . openIn ( backgroundTab . item ) ;
} else if ( request . destination === WebEngineView . NewViewInDialog ) {
var dialog = browserDialogComponent . createObject ( ) ;
dialog . currentWebView . profile = currentWebView . profile ;
request . openIn ( dialog . currentWebView ) ;
} else {
// Instead of opening a new window, we open a new tab
// TODO: remove "open in new window" from context menu
var tab = tabs . createEmptyTab ( currentWebView . profile ) ;
tabs . currentIndex = tabs . count - 1 ;
request . openIn ( tab . item ) ;
}
}
onQuotaRequested: function ( request ) {
if ( request . requestedSize <= 5 * 1024 * 1024 )
request . accept ( ) ;
else
request . reject ( ) ;
}
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 ;
}
onWindowCloseRequested: tabs . removeView ( tabs . indexOfView ( webEngineView ) )
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 ( ) ;
}
Timer {
id: reloadTimer
interval: 0
running: false
repeat: false
onTriggered: currentWebView . reload ( )
}
}
}
}
ProgressBar {
id: progressBar
height: 3
from: 0
to: 100
visible: value != 0 && value != 100
value: ( currentWebView && currentWebView . loadProgress < 100 ) ? currentWebView.loadProgress : 0
anchors.bottom: parent . bottom
anchors.bottomMargin: Style . current . padding
anchors.right: parent . right
anchors.rightMargin: Style . current . padding
}
2020-09-22 19:16:44 +00:00
WebEngineView {
2020-09-28 18:33:37 +00:00
id: devToolsView
visible: devToolsEnabled . checked
height: visible ? 400 : 0
inspectedView: visible && tabs . currentIndex < tabs . count ? tabs . getTab ( tabs . currentIndex ) . item : null
anchors.left: parent . left
anchors.right: parent . right
anchors.bottom: parent . bottom
2020-09-22 19:16:44 +00:00
onNewViewRequested: function ( request ) {
2020-09-28 18:33:37 +00:00
var tab = tabs . createEmptyTab ( currentWebView . profile ) ;
tabs . currentIndex = tabs . count - 1 ;
request . openIn ( tab . item ) ;
}
}
MessageDialog {
id: sslDialog
property var certErrors: [ ]
icon: StandardIcon . Warning
standardButtons: StandardButton . No | StandardButton . Yes
title: qsTr ( "Server's certificate not trusted" )
text: qsTr ( "Do you wish to continue?" )
detailedText: qsTr ( "If you wish so, you may continue with an unverified certificate. Accepting an unverified certificate means you may not be connected with the host you tried to connect to.\nDo you wish to override the security check and continue?" )
onYes: {
certErrors . shift ( ) . ignoreCertificateError ( ) ;
presentError ( ) ;
}
onNo: reject ( )
onRejected: reject ( )
function reject ( ) {
certErrors . shift ( ) . rejectCertificate ( ) ;
presentError ( ) ;
}
function enqueue ( error ) {
certErrors . push ( error ) ;
presentError ( ) ;
}
function presentError ( ) {
visible = certErrors . length > 0
}
}
DownloadView {
id: downloadView
visible: false
anchors.fill: parent
}
function onDownloadRequested ( download ) {
console . log ( "DOWNLOAD REQUESTED" )
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 ;
}
}
2020-09-22 19:16:44 +00:00
}
2020-09-22 15:12:48 +00:00
}
}
/ * # # ^ # #
Designer {
D { i: 0 ; autoSize: true ; height: 480 ; width: 640 }
}
# # ^ # # * /