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 readonly property int statusBarHeight: AppController ? AppController.getStatusBarHeight() : 0 readonly property int navigationBarHeight: AppController ? AppController.getNavigationBarHeight() : 0 // 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 alias infobanner: _infobanner property variant activeDialogs: [] property var _exceptionDialog property var pluginobjects: ({}) 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 // Add top margin for status bar on Android when using edge-to-edge topPadding: app.statusBarHeight 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.availableHeight 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 } } } ColumnLayout { width: parent.width height: _keyboardFreeZone.height - header.height spacing: 0 InfoBanner { id: _infobanner Layout.fillWidth: true } StackView { id: mainStackView Layout.fillHeight: true Layout.fillWidth: true 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) } } } // Add bottom padding for navigation bar on Android when UI is edge-to-edge Item { visible: app.navigationBarHeight > 0 Layout.fillWidth: true Layout.preferredHeight: app.navigationBarHeight } } 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 termsOfUseWizard: _termsOfUseWizard Component { id: _termsOfUseWizard TermsOfUseWizard { 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() } } Component { id: crashDialog ExceptionDialog { onClosed: destroy() } } property alias channelOpenProgressDialog: _channelOpenProgressDialog ChannelOpenProgressDialog { id: _channelOpenProgressDialog } property alias signVerifyMessageDialog: _signVerifyMessageDialog Component { id: _signVerifyMessageDialog SignVerifyMessageDialog { onClosed: destroy() } } property alias nostrSwapServersDialog: _nostrSwapServersDialog Component { id: _nostrSwapServersDialog NostrSwapServersDialog { onClosed: destroy() } } Component { id: swapDialog SwapDialog { id: _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() } onUndefinedNPub: { var dialog = app.nostrSwapServersDialog.createObject(app, { swaphelper: _swaphelper, selectedPubkey: Config.swapServerNPub }) dialog.accepted.connect(function() { Config.swapServerNPub = dialog.selectedPubkey _swaphelper.setReadyState() }) dialog.rejected.connect(function() { _swaphelper.npubSelectionCancelled() }) dialog.open() } } } } NotificationPopup { id: notificationPopup width: parent.width } Component.onCompleted: { coverTimer.start() if (AppController.isAndroid()) { app.scanDialog = _scanDialog } else { app.scanDialog = _qtScanDialog } function continueWithServerConnection() { if (!Network.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() } } } if (!Config.termsOfUseAccepted) { var dialog = termsOfUseWizard.createObject(app) dialog.rejected.connect(function() { app.visible = false AppController.wantClose = true Qt.callLater(Qt.quit) }) dialog.accepted.connect(function() { Config.termsOfUseAccepted = true continueWithServerConnection() }) dialog.open() } else { continueWithServerConnection() } } 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() } function onPluginLoaded(name) { console.log('plugin ' + name + ' loaded') var loader = AppController.plugin(name).loader if (loader == undefined) return var url = Qt.resolvedUrl('../../../plugins/' + name + '/qml/' + loader) var comp = Qt.createComponent(url) if (comp.status == Component.Error) { console.log('Could not find/parse PluginLoader for plugin ' + name) console.log(comp.errorString()) return } var obj = comp.createObject(app) if (obj != null) app.pluginobjects[name] = obj } } function pluginsComponentsByName(comp_name) { // return named QML components from plugins var plugins = AppController.plugins var result = [] for (var i=0; i < plugins.length; i++) { if (!plugins[i].enabled) continue var pluginobject = app.pluginobjects[plugins[i].name] if (!pluginobject) continue if (!(comp_name in pluginobject)) continue var comp = pluginobject[comp_name] if (!comp) continue result.push(comp) } return result } 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_else_pin') { // if there is a loaded wallet and all wallets use the same password, use that // else delegate to pin auth if (Daemon.currentWallet && Daemon.singlePasswordEnabled) { method = 'wallet' } else { method = 'pin' } } 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 }