You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

701 lines
21 KiB

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Controls.Material
import QtQuick.Controls.Material.impl
import QtQuick.Window
import QtQml
import QtMultimedia
import org.electrum 1.0
import "controls"
ApplicationWindow
{
id: app
visible: false // initial value
// dimensions ignored on android
width: 480
height: 800
Material.theme: Material.Dark
Material.primary: Material.Indigo
Material.accent: Material.LightBlue
font.pixelSize: constants.fontSizeMedium
property QtObject constants: appconstants
Constants { id: appconstants }
property alias stack: mainStackView
property alias keyboardFreeZone: _keyboardFreeZone
property variant activeDialogs: []
property var _exceptionDialog
property QtObject appMenu: Menu {
id: menu
parent: Overlay.overlay
dim: true
modal: true
Overlay.modal: Rectangle {
color: "#44000000"
}
property int implicitChildrenWidth: 64
width: implicitChildrenWidth + 60 + constants.paddingLarge
MenuItem {
icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor
icon.source: '../../icons/network.png'
action: Action {
text: qsTr('Network')
onTriggered: menu.openPage(Qt.resolvedUrl('NetworkOverview.qml'))
enabled: stack.currentItem.objectName != 'NetworkOverview'
}
}
MenuItem {
icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor
icon.source: '../../icons/preferences.png'
action: Action {
text: qsTr('Preferences')
onTriggered: menu.openPage(Qt.resolvedUrl('Preferences.qml'))
enabled: stack.currentItem.objectName != 'Properties'
}
}
MenuItem {
icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor
icon.source: '../../icons/electrum.png'
action: Action {
text: qsTr('About');
onTriggered: menu.openPage(Qt.resolvedUrl('About.qml'))
enabled: stack.currentItem.objectName != 'About'
}
}
function openPage(url) {
stack.pushOnRoot(url)
currentIndex = -1
}
// determine widest element and store in implicitChildrenWidth
function updateImplicitWidth() {
for (let i = 0; i < menu.count; i++) {
var item = menu.itemAt(i)
var txt = item.text
var txtwidth = fontMetrics.advanceWidth(txt)
if (txtwidth > menu.implicitChildrenWidth) {
menu.implicitChildrenWidth = txtwidth
}
}
}
FontMetrics {
id: fontMetrics
font: menu.font
}
Component.onCompleted: updateImplicitWidth()
}
function openAppMenu() {
appMenu.open()
appMenu.x = app.width - appMenu.width
appMenu.y = toolbar.height
}
header: ToolBar {
id: toolbar
background: Rectangle {
implicitHeight: 48
color: Material.dialogColor
layer.enabled: true
layer.effect: ElevationEffect {
elevation: 4
fullWidth: true
}
}
ColumnLayout {
spacing: 0
anchors.left: parent.left
anchors.right: parent.right
height: toolbar.height
RowLayout {
id: toolbarTopLayout
Layout.fillWidth: true
Layout.rightMargin: constants.paddingMedium
Layout.alignment: Qt.AlignVCenter
Item {
Layout.fillWidth: true
Layout.preferredHeight: Math.max(implicitHeight, toolbarTopLayout.height)
MouseArea {
anchors.fill: parent
enabled: Daemon.currentWallet &&
(!stack.currentItem || !stack.currentItem.title || stack.currentItem.title == Daemon.currentWallet.name)
onClicked: {
stack.getRoot().menu.open() // open wallet-menu
stack.getRoot().menu.y = toolbar.height
}
}
RowLayout {
width: parent.width
Item {
Layout.preferredWidth: constants.paddingXLarge
Layout.preferredHeight: 1
}
Image {
Layout.preferredWidth: constants.iconSizeSmall
Layout.preferredHeight: constants.iconSizeSmall
visible: Daemon.currentWallet &&
(!stack.currentItem || !stack.currentItem.title || stack.currentItem.title == Daemon.currentWallet.name)
source: '../../icons/wallet.png'
}
Label {
Layout.fillWidth: true
Layout.preferredHeight: Math.max(implicitHeight, toolbarTopLayout.height)
text: stack.currentItem && stack.currentItem.title
? stack.currentItem.title
: Daemon.currentWallet.name
elide: Label.ElideRight
verticalAlignment: Qt.AlignVCenter
font.pixelSize: constants.fontSizeMedium
font.bold: true
}
}
}
Item {
implicitHeight: 48
implicitWidth: statusIconsLayout.width
MouseArea {
anchors.fill: parent
onClicked: openAppMenu() // open global-app-menu
}
RowLayout {
id: statusIconsLayout
anchors.verticalCenter: parent.verticalCenter
Item {
Layout.preferredWidth: constants.paddingLarge
Layout.preferredHeight: 1
}
Item {
visible: Network.isTestNet
width: column.width
height: column.height
ColumnLayout {
id: column
spacing: 0
Image {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: constants.iconSizeSmall
Layout.preferredHeight: constants.iconSizeSmall
source: "../../icons/info.png"
}
Label {
id: networkNameLabel
text: Network.networkName
color: Material.accentColor
font.pixelSize: constants.fontSizeXSmall
}
}
}
LightningNetworkStatusIndicator {
id: lnnsi
}
OnchainNetworkStatusIndicator { }
}
}
}
// hack to force relayout of toolbar
// since qt6 LightningNetworkStatusIndicator.visible doesn't trigger relayout(?)
Item {
Layout.preferredHeight: 1
Layout.topMargin: -1
Layout.preferredWidth: lnnsi.visible
? 1
: 2
}
}
}
StackView {
id: mainStackView
width: parent.width
height: _keyboardFreeZone.height - header.height
initialItem: Component {
WalletMainView {}
}
function getRoot() {
return mainStackView.get(0)
}
function pushOnRoot(item) {
if (mainStackView.depth > 1) {
mainStackView.replace(mainStackView.get(1), item)
} else {
mainStackView.push(item)
}
}
}
Timer {
id: coverTimer
interval: 10
onTriggered: {
app.visible = true
cover.opacity = 0
}
}
Rectangle {
id: cover
parent: Overlay.overlay
anchors.fill: parent
z: 1000
color: 'black'
Behavior on opacity {
enabled: AppController ? AppController.isAndroid() : false
NumberAnimation {
duration: 1000
easing.type: Easing.OutQuad;
}
}
}
Item {
id: _keyboardFreeZone
// Item as first child in Overlay that adjusts its size to the available
// screen space minus the virtual keyboard (e.g. to center dialogs in)
// see also ElDialog.resizeWithKeyboard property
parent: Overlay.overlay
width: parent.width
height: parent.height
states: [
State {
name: 'visible'
when: Qt.inputMethod.keyboardRectangle.y
PropertyChanges {
target: _keyboardFreeZone
height: _keyboardFreeZone.parent.height - (Screen.desktopAvailableHeight - (Qt.inputMethod.keyboardRectangle.y/Screen.devicePixelRatio))
}
}
]
transitions: [
Transition {
from: ''
to: 'visible'
NumberAnimation {
properties: 'height'
duration: 100
easing.type: Easing.OutQuad
}
},
Transition {
from: 'visible'
to: ''
SequentialAnimation {
PauseAnimation {
duration: 200
}
NumberAnimation {
properties: 'height'
duration: 50
easing.type: Easing.OutQuad
}
}
}
]
}
property alias newWalletWizard: _newWalletWizard
Component {
id: _newWalletWizard
NewWalletWizard {
onClosed: destroy()
}
}
property alias serverConnectWizard: _serverConnectWizard
Component {
id: _serverConnectWizard
ServerConnectWizard {
onClosed: destroy()
}
}
property alias messageDialog: _messageDialog
Component {
id: _messageDialog
MessageDialog {
onClosed: destroy()
}
}
property alias helpDialog: _helpDialog
Component {
id: _helpDialog
HelpDialog {
onClosed: destroy()
}
}
property alias passwordDialog: _passwordDialog
Component {
id: _passwordDialog
PasswordDialog {
onClosed: destroy()
}
}
property alias pinDialog: _pinDialog
Component {
id: _pinDialog
Pin {
onClosed: destroy()
}
}
property alias genericShareDialog: _genericShareDialog
Component {
id: _genericShareDialog
GenericShareDialog {
onClosed: destroy()
}
}
property alias openWalletDialog: _openWalletDialog
Component {
id: _openWalletDialog
OpenWalletDialog {
onClosed: destroy()
}
}
property alias loadingWalletDialog: _loadingWalletDialog
Component {
id: _loadingWalletDialog
LoadingWalletDialog {
onClosed: destroy()
}
}
property Component scanDialog // set in Component.onCompleted
Component {
id: _scanDialog
QRScanner {
onFinished: destroy()
}
}
Component {
id: _qtScanDialog
ScanDialog {
onClosed: destroy()
}
}
property alias channelOpenProgressDialog: _channelOpenProgressDialog
ChannelOpenProgressDialog {
id: _channelOpenProgressDialog
}
property alias signVerifyMessageDialog: _signVerifyMessageDialog
Component {
id: _signVerifyMessageDialog
SignVerifyMessageDialog {
onClosed: destroy()
}
}
Component {
id: swapDialog
SwapDialog {
onClosed: destroy()
swaphelper: SwapHelper {
id: _swaphelper
wallet: Daemon.currentWallet
onAuthRequired: (method, authMessage) => {
app.handleAuthRequired(_swaphelper, method, authMessage)
}
onError: (message) => {
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Error'),
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
text: message
})
dialog.open()
}
}
}
}
NotificationPopup {
id: notificationPopup
width: parent.width
}
Component {
id: crashDialog
ExceptionDialog {
z: 1000
}
}
Component.onCompleted: {
coverTimer.start()
if (AppController.isAndroid()) {
app.scanDialog = _scanDialog
} else {
app.scanDialog = _qtScanDialog
}
if (!Config.autoConnectDefined) {
var dialog = serverConnectWizard.createObject(app)
// without completed serverConnectWizard we can't start
dialog.rejected.connect(function() {
app.visible = false
AppController.wantClose = true
Qt.callLater(Qt.quit)
})
dialog.accepted.connect(function() {
Daemon.startNetwork()
var newww = app.newWalletWizard.createObject(app)
newww.walletCreated.connect(function() {
Daemon.availableWallets.reload()
// and load the new wallet
Daemon.loadWallet(newww.path, newww.wizard_data['password'])
})
newww.open()
})
dialog.open()
} else {
Daemon.startNetwork()
if (Daemon.availableWallets.rowCount() > 0) {
Daemon.loadWallet()
} else {
var newww = app.newWalletWizard.createObject(app)
newww.walletCreated.connect(function() {
Daemon.availableWallets.reload()
// and load the new wallet
Daemon.loadWallet(newww.path, newww.wizard_data['password'])
})
newww.open()
}
}
}
onClosing: (close) => {
if (AppController.wantClose) {
// destroy most GUI components so that we don't dump so many null reference warnings on exit
app.header.visible = false
mainStackView.clear()
return
}
if (activeDialogs.length > 0) {
var activeDialog = activeDialogs[activeDialogs.length - 1]
if (activeDialog.allowClose) {
console.log('main: dialog.doClose')
activeDialog.doClose()
} else {
console.log('dialog disallowed close')
}
close.accepted = false
return
}
if (stack.depth > 1) {
close.accepted = false
stack.pop()
} else {
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Close Electrum?'),
yesno: true
})
dialog.accepted.connect(function() {
AppController.wantClose = true
app.close()
})
dialog.open()
close.accepted = false
}
}
property var _opendialog: undefined
function showOpenWalletDialog(name, path) {
if (_opendialog == undefined) {
_opendialog = openWalletDialog.createObject(app, { name: name, path: path })
_opendialog.closed.connect(function() {
_opendialog = undefined
})
_opendialog.open()
}
}
Connections {
target: Daemon
function onWalletRequiresPassword(name, path) {
console.log('wallet requires password')
showOpenWalletDialog(name, path)
}
function onWalletOpenError(error) {
console.log('wallet open error')
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Error'),
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
text: error
})
dialog.open()
}
function onAuthRequired(method, authMessage) {
handleAuthRequired(Daemon, method, authMessage)
}
function onLoadingChanged() {
if (!Daemon.loading)
return
console.log('wallet loading')
var dialog = loadingWalletDialog.createObject(app, { allowClose: false } )
dialog.open()
}
}
Connections {
target: AppController
function onUserNotify(wallet_name, message) {
notificationPopup.show(wallet_name, message)
}
function onShowException(crash_data) {
if (app._exceptionDialog)
return
app._exceptionDialog = crashDialog.createObject(app, {
crashData: crash_data
})
app._exceptionDialog.onClosed.connect(function() {
app._exceptionDialog = null
})
app._exceptionDialog.open()
}
}
Connections {
target: Daemon.currentWallet
function onAuthRequired(method, authMessage) {
handleAuthRequired(Daemon.currentWallet, method, authMessage)
}
// TODO: add to notification queue instead of barging through
function onPaymentSucceeded(key) {
notificationPopup.show(Daemon.currentWallet.name, qsTr('Payment Succeeded'))
}
function onPaymentFailed(key, reason) {
notificationPopup.show(Daemon.currentWallet.name, qsTr('Payment Failed') + ': ' + reason)
}
}
Connections {
target: Config
function onAuthRequired(method, authMessage) {
handleAuthRequired(Config, method, authMessage)
}
}
function handleAuthRequired(qtobject, method, authMessage) {
console.log('auth using method ' + method)
if (method == 'wallet') {
if (Daemon.currentWallet.verifyPassword('')) {
// wallet has no password
qtobject.authProceed()
} else {
var dialog = app.passwordDialog.createObject(app, {'title': qsTr('Enter current password')})
dialog.accepted.connect(function() {
if (Daemon.currentWallet.verifyPassword(dialog.password)) {
qtobject.authProceed()
} else {
qtobject.authCancel()
}
})
dialog.rejected.connect(function() {
qtobject.authCancel()
})
dialog.open()
}
} else if (method == 'pin') {
if (Config.pinCode == '') {
// no PIN configured
handleAuthConfirmationOnly(qtobject, authMessage)
} else {
var dialog = app.pinDialog.createObject(app, {
mode: 'check',
pincode: Config.pinCode,
authMessage: authMessage
})
dialog.accepted.connect(function() {
qtobject.authProceed()
dialog.close()
})
dialog.rejected.connect(function() {
qtobject.authCancel()
})
dialog.open()
}
} else {
console.log('unknown auth method ' + method)
qtobject.authCancel()
}
}
function handleAuthConfirmationOnly(qtobject, authMessage) {
if (!authMessage) {
qtobject.authProceed()
return
}
var dialog = app.messageDialog.createObject(app, {
title: authMessage,
yesno: true
})
dialog.accepted.connect(function() {
qtobject.authProceed()
})
dialog.rejected.connect(function() {
qtobject.authCancel()
})
dialog.open()
}
function startSwap() {
var swapdialog = swapDialog.createObject(app)
swapdialog.open()
}
property var _lastActive: 0 // record time of last activity
property bool _lockDialogShown: false
}