From 15c76114c6d66bb1bfaaace5f37cc7b5a39e11a6 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Fri, 23 Sep 2022 12:14:19 +0200 Subject: [PATCH] replace swipeview, send & receive now dialogs send mostly working, though no user entered payment yet --- electrum/gui/qml/components/Constants.qml | 2 + electrum/gui/qml/components/History.qml | 5 + electrum/gui/qml/components/ReceiveDialog.qml | 320 ++++++++++++++++++ electrum/gui/qml/components/SendDialog.qml | 58 ++++ .../gui/qml/components/WalletMainView.qml | 172 +++++++--- .../qml/components/controls/FlatButton.qml | 24 ++ .../controls/HistoryItemDelegate.qml | 10 +- electrum/gui/qml/qeinvoice.py | 21 +- 8 files changed, 548 insertions(+), 64 deletions(-) create mode 100644 electrum/gui/qml/components/ReceiveDialog.qml create mode 100644 electrum/gui/qml/components/SendDialog.qml create mode 100644 electrum/gui/qml/components/controls/FlatButton.qml diff --git a/electrum/gui/qml/components/Constants.qml b/electrum/gui/qml/components/Constants.qml index 51de82c92..5ea538488 100644 --- a/electrum/gui/qml/components/Constants.qml +++ b/electrum/gui/qml/components/Constants.qml @@ -27,6 +27,8 @@ Item { property color colorCredit: "#ff80ff80" property color colorDebit: "#ffff8080" property color mutedForeground: 'gray' //Qt.lighter(Material.background, 2) + property color darkerBackground: Qt.darker(Material.background, 1.20) + property color lighterBackground: Qt.lighter(Material.background, 1.10) property color colorMine: "yellow" property color colorError: '#ffff8080' property color colorLightningLocal: "blue" diff --git a/electrum/gui/qml/components/History.qml b/electrum/gui/qml/components/History.qml index bae7cca81..6f541fecc 100644 --- a/electrum/gui/qml/components/History.qml +++ b/electrum/gui/qml/components/History.qml @@ -14,10 +14,15 @@ Pane { padding: 0 clip: true + background: Rectangle { + color: constants.darkerBackground + } + ListView { id: listview width: parent.width height: parent.height + boundsBehavior: Flickable.StopAtBounds model: visualModel diff --git a/electrum/gui/qml/components/ReceiveDialog.qml b/electrum/gui/qml/components/ReceiveDialog.qml new file mode 100644 index 000000000..2b2d56309 --- /dev/null +++ b/electrum/gui/qml/components/ReceiveDialog.qml @@ -0,0 +1,320 @@ +import QtQuick 2.6 +import QtQuick.Layouts 1.0 +import QtQuick.Controls 2.14 +import QtQuick.Controls.Material 2.0 +import QtQml.Models 2.1 + +import org.electrum 1.0 + +import "controls" + +ElDialog { + id: dialog + + property string _bolt11 + property string _bip21uri + property string _address + + property bool _render_qr: false // delay qr rendering until dialog is shown + + parent: Overlay.overlay + modal: true + standardButtons: Dialog.Close + + width: parent.width + height: parent.height + + Overlay.modal: Rectangle { + color: "#aa000000" + } + + ColumnLayout { + id: rootLayout + width: parent.width + spacing: constants.paddingMedium + + states: [ + State { + name: 'bolt11' + PropertyChanges { target: qrloader; sourceComponent: qri_bolt11 } + PropertyChanges { target: bolt11label; font.bold: true } + }, + State { + name: 'bip21uri' + PropertyChanges { target: qrloader; sourceComponent: qri_bip21uri } + PropertyChanges { target: bip21label; font.bold: true } + }, + State { + name: 'address' + PropertyChanges { target: qrloader; sourceComponent: qri_address } + PropertyChanges { target: addresslabel; font.bold: true } + } + ] + + Rectangle { + height: 1 + Layout.fillWidth: true + color: Material.accentColor + } + + Item { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: constants.paddingSmall + Layout.bottomMargin: constants.paddingSmall + + Layout.preferredWidth: qrloader.width + Layout.preferredHeight: qrloader.height + + Loader { + id: qrloader + Component { + id: qri_bolt11 + QRImage { + qrdata: _bolt11 + render: _render_qr + } + } + Component { + id: qri_bip21uri + QRImage { + qrdata: _bip21uri + render: _render_qr + } + } + Component { + id: qri_address + QRImage { + qrdata: _address + render: _render_qr + } + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + if (rootLayout.state == 'bolt11') { + if (_bip21uri != '') + rootLayout.state = 'bip21uri' + else if (_address != '') + rootLayout.state = 'address' + } else if (rootLayout.state == 'bip21uri') { + if (_address != '') + rootLayout.state = 'address' + else if (_bolt11 != '') + rootLayout.state = 'bolt11' + } else if (rootLayout.state == 'address') { + if (_bolt11 != '') + rootLayout.state = 'bolt11' + else if (_bip21uri != '') + rootLayout.state = 'bip21uri' + } + } + } + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: constants.paddingLarge + Label { + id: bolt11label + text: qsTr('BOLT11') + color: _bolt11 ? Material.foreground : constants.mutedForeground + } + Rectangle { + Layout.preferredWidth: constants.paddingXXSmall + Layout.preferredHeight: constants.paddingXXSmall + radius: constants.paddingXXSmall / 2 + color: Material.accentColor + } + Label { + id: bip21label + text: qsTr('BIP21') + color: _bip21uri ? Material.foreground : constants.mutedForeground + } + Rectangle { + Layout.preferredWidth: constants.paddingXXSmall + Layout.preferredHeight: constants.paddingXXSmall + radius: constants.paddingXXSmall / 2 + color: Material.accentColor + } + Label { + id: addresslabel + text: qsTr('ADDRESS') + color: _address ? Material.foreground : constants.mutedForeground + } + } + + Rectangle { + height: 1 + Layout.fillWidth: true + color: Material.accentColor + } + +// aaaaaaaaaaaaaaaaaaaa + + + GridLayout { + id: form + width: parent.width + rowSpacing: constants.paddingSmall + columnSpacing: constants.paddingSmall + columns: 4 + + Label { + text: qsTr('Message') + } + + TextField { + id: message + placeholderText: qsTr('Description of payment request') + Layout.columnSpan: 3 + Layout.fillWidth: true + } + + Label { + text: qsTr('Request') + wrapMode: Text.WordWrap + Layout.rightMargin: constants.paddingXLarge + } + + BtcField { + id: amount + fiatfield: amountFiat + Layout.preferredWidth: parent.width /3 + } + + Label { + text: Config.baseUnit + color: Material.accentColor + } + + Item { width: 1; height: 1; Layout.fillWidth: true } + + Item { visible: Daemon.fx.enabled; width: 1; height: 1 } + + FiatField { + id: amountFiat + btcfield: amount + visible: Daemon.fx.enabled + Layout.preferredWidth: parent.width /3 + } + + Label { + visible: Daemon.fx.enabled + text: Daemon.fx.fiatCurrency + color: Material.accentColor + } + + Item { visible: Daemon.fx.enabled; width: 1; height: 1; Layout.fillWidth: true } + + Label { + text: qsTr('Expires after') + Layout.fillWidth: false + } + + ElComboBox { + id: expires + Layout.columnSpan: 2 + + textRole: 'text' + valueRole: 'value' + + model: ListModel { + id: expiresmodel + Component.onCompleted: { + // we need to fill the model like this, as ListElement can't evaluate script + expiresmodel.append({'text': qsTr('10 minutes'), 'value': 10*60}) + expiresmodel.append({'text': qsTr('1 hour'), 'value': 60*60}) + expiresmodel.append({'text': qsTr('1 day'), 'value': 24*60*60}) + expiresmodel.append({'text': qsTr('1 week'), 'value': 7*24*60*60}) + expiresmodel.append({'text': qsTr('1 month'), 'value': 31*24*60*60}) + expiresmodel.append({'text': qsTr('Never'), 'value': 0}) + expires.currentIndex = 0 + } + } + } + + Item { width: 1; height: 1; Layout.fillWidth: true } + + Button { + Layout.columnSpan: 4 + Layout.alignment: Qt.AlignHCenter + text: qsTr('Create Request') + icon.source: '../../icons/qrcode.png' + onClicked: { + createRequest() + } + } + } + } + + + // make clicking the dialog background move the scope away from textedit fields + // so the keyboard goes away + MouseArea { + anchors.fill: parent + z: -1000 + onClicked: parkFocus.focus = true + FocusScope { id: parkFocus } + } + + Component { + id: requestdialog + RequestDialog { + onClosed: destroy() + } + } + + function createRequest(ignoreGaplimit = false) { + var qamt = Config.unitsToSats(amount.text) + if (qamt.satsInt > Daemon.currentWallet.lightningCanReceive.satsInt) { + console.log('Creating OnChain request') + Daemon.currentWallet.create_request(qamt, message.text, expires.currentValue, false, ignoreGaplimit) + } else { + console.log('Creating Lightning request') + Daemon.currentWallet.create_request(qamt, message.text, expires.currentValue, true) + } + } + + Connections { + target: Daemon.currentWallet + function onRequestCreateSuccess(key) { + message.text = '' + amount.text = '' + var dialog = requestdialog.createObject(app, { key: key }) + dialog.open() + } + function onRequestCreateError(code, error) { + if (code == 'gaplimit') { + var dialog = app.messageDialog.createObject(app, {'text': error, 'yesno': true}) + dialog.yesClicked.connect(function() { + createRequest(true) + }) + } else { + console.log(error) + var dialog = app.messageDialog.createObject(app, {'text': error}) + } + dialog.open() + } + function onRequestStatusChanged(key, status) { + Daemon.currentWallet.requestModel.updateRequest(key, status) + } + } + + Component.onCompleted: { + _address = '1234567890' + rootLayout.state = 'address' + } + + // hack. delay qr rendering until dialog is shown + Connections { + target: dialog.enter + function onRunningChanged() { + if (!dialog.enter.running) { + dialog._render_qr = true + } + } + } + +} diff --git a/electrum/gui/qml/components/SendDialog.qml b/electrum/gui/qml/components/SendDialog.qml new file mode 100644 index 000000000..c3ea45743 --- /dev/null +++ b/electrum/gui/qml/components/SendDialog.qml @@ -0,0 +1,58 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.0 +import QtQuick.Controls.Material 2.0 +import QtQml.Models 2.1 + +import org.electrum 1.0 + +import "controls" + +ElDialog { + id: dialog + + property InvoiceParser invoiceParser + + signal manualInput + + parent: Overlay.overlay + modal: true + standardButtons: Dialog.Close + + width: parent.width + height: parent.height + + Overlay.modal: Rectangle { + color: "#aa000000" + } + + padding: 0 + + onClosed: destroy() + + ColumnLayout { + anchors.fill: parent + + QRScan { + Layout.preferredWidth: parent.width + Layout.fillHeight: true + + onFound: invoiceParser.recipient = scanData + } + + FlatButton { + Layout.fillWidth: true + text: qsTr('Manual input') + onClicked: { + manualInput() + } + } + + FlatButton { + Layout.fillWidth: true + text: qsTr('Paste from clipboard') + onClicked: invoiceParser.recipient = AppController.clipboardToText() + } + } + +} diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 8382a8e0c..a300b3fa7 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -3,8 +3,12 @@ import QtQuick.Controls 2.3 import QtQuick.Layouts 1.0 import QtQml 2.6 +import org.electrum 1.0 + +import "controls" + Item { - id: rootItem + id: mainView property string title: Daemon.currentWallet ? Daemon.currentWallet.name : '' @@ -69,6 +73,8 @@ Item { } } + property var _sendDialog + ColumnLayout { anchors.centerIn: parent width: parent.width @@ -94,71 +100,137 @@ Item { anchors.fill: parent visible: Daemon.currentWallet - SwipeView { - id: swipeview - + History { + id: history + Layout.preferredWidth: parent.width Layout.fillHeight: true - Layout.fillWidth: true - currentIndex: tabbar.currentIndex - - Item { - Loader { - anchors.fill: parent - Receive { - id: receive - anchors.fill: parent - } + } + + RowLayout { + spacing: 0 + + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Send') + onClicked: { + console.log('send') + var comp = Qt.createComponent(Qt.resolvedUrl('SendDialog.qml')) + if (comp.status == Component.Error) + console.log(comp.errorString()) + _sendDialog = comp.createObject(mainView, { invoiceParser: invoiceParser } ) + // dialog. + _sendDialog.open() } } - - Item { - Loader { - anchors.fill: parent - History { - id: history - anchors.fill: parent - } + Rectangle { + Layout.fillWidth: false + Layout.preferredWidth: 2 + Layout.preferredHeight: parent.height * 2/3 + Layout.alignment: Qt.AlignVCenter + color: constants.darkerBackground + } + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Receive') + onClicked: { + var comp = Qt.createComponent(Qt.resolvedUrl('ReceiveDialog.qml')) + var dialog = comp.createObject(mainView) + dialog.open() } } + } + } + InvoiceParser { + id: invoiceParser + wallet: Daemon.currentWallet + onValidationError: { + var dialog = app.messageDialog.createObject(app, {'text': message }) + dialog.open() + } + onValidationWarning: { + if (code == 'no_channels') { + var dialog = app.messageDialog.createObject(app, {'text': message }) + dialog.open() + // TODO: ask user to open a channel, if funds allow + // and maybe store invoice if expiry allows + } + } + onValidationSuccess: { + _sendDialog.close() + // address only -> fill form fields and clear this instance + // else -> show invoice confirmation dialog + if (invoiceType == Invoice.OnchainOnlyAddress) { + recipient.text = invoice.recipient + invoiceParser.clear() + } else { + var dialog = invoiceDialog.createObject(app, {'invoice': invoiceParser}) + // dialog.invoice = invoiceParser + dialog.open() + } + } + onInvoiceCreateError: console.log(code + ' ' + message) - Item { - Loader { - anchors.fill: parent - Send { - anchors.fill: parent + onInvoiceSaved: { + Daemon.currentWallet.invoiceModel.init_model() + } + } + + Component { + id: invoiceDialog + InvoiceDialog { + onDoPay: { + if (invoice.invoiceType == Invoice.OnchainInvoice) { + var dialog = confirmPaymentDialog.createObject(mainView, { + 'address': invoice.address, + 'satoshis': invoice.amount, + 'message': invoice.message + }) + var wo = Daemon.currentWallet.isWatchOnly + dialog.txaccepted.connect(function() { + if (wo) { + showUnsignedTx(dialog.finalizer.serializedTx(false), dialog.finalizer.serializedTx(true)) + } else { + dialog.finalizer.send_onchain() } + }) + dialog.open() + } else if (invoice.invoiceType == Invoice.LightningInvoice) { + console.log('About to pay lightning invoice') + if (invoice.key == '') { + console.log('No invoice key, aborting') + return } + var dialog = lightningPaymentProgressDialog.createObject(mainView, { + invoice_key: invoice.key + }) + dialog.open() + Daemon.currentWallet.pay_lightning_invoice(invoice.key) } - + close() } + // onClosed: destroy() + } + } - TabBar { - id: tabbar - position: TabBar.Footer - Layout.fillWidth: true - currentIndex: swipeview.currentIndex - TabButton { - text: qsTr('Receive') - font.pixelSize: constants.fontSizeLarge - } - TabButton { - text: qsTr('History') - font.pixelSize: constants.fontSizeLarge + Component { + id: confirmPaymentDialog + ConfirmTxDialog { + title: qsTr('Confirm Payment') + finalizer: TxFinalizer { + wallet: Daemon.currentWallet + canRbf: true } - TabButton { - text: qsTr('Send') - font.pixelSize: constants.fontSizeLarge - } - Component.onCompleted: tabbar.setCurrentIndex(1) + onClosed: destroy() } - } - Connections { - target: Daemon - function onWalletLoaded() { - tabbar.setCurrentIndex(1) + Component { + id: lightningPaymentProgressDialog + LightningPaymentProgressDialog { + onClosed: destroy() } } diff --git a/electrum/gui/qml/components/controls/FlatButton.qml b/electrum/gui/qml/components/controls/FlatButton.qml new file mode 100644 index 000000000..80c80eae1 --- /dev/null +++ b/electrum/gui/qml/components/controls/FlatButton.qml @@ -0,0 +1,24 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.15 + +Item { + id: root + + property alias text: buttonLabel.text + property alias font: buttonLabel.font + + signal clicked + + implicitWidth: buttonLabel.width + constants.paddingXXLarge + implicitHeight: buttonLabel.height + constants.paddingXXLarge + + Label { + id: buttonLabel + anchors.centerIn: parent + } + + MouseArea { + anchors.fill: root + onClicked: root.clicked() + } +} diff --git a/electrum/gui/qml/components/controls/HistoryItemDelegate.qml b/electrum/gui/qml/components/controls/HistoryItemDelegate.qml index ae9b49c20..512c24bec 100644 --- a/electrum/gui/qml/components/controls/HistoryItemDelegate.qml +++ b/electrum/gui/qml/components/controls/HistoryItemDelegate.qml @@ -45,7 +45,7 @@ Item { x: constants.paddingSmall width: delegate.width - 2*constants.paddingSmall - Item { Layout.columnSpan: 3; Layout.preferredWidth: 1; Layout.preferredHeight: 1} + Item { Layout.columnSpan: 3; Layout.preferredWidth: 1; Layout.preferredHeight: constants.paddingSmall } Image { readonly property variant tx_icons : [ @@ -113,15 +113,17 @@ Item { } Component.onCompleted: updateText() } - Item { Layout.columnSpan: 3; Layout.preferredWidth: 1; Layout.preferredHeight: 1 } + Item { Layout.columnSpan: 3; Layout.preferredWidth: 1; Layout.preferredHeight: constants.paddingSmall } } } Rectangle { visible: delegate.ListView.section == delegate.ListView.nextSection - Layout.fillWidth: true + // Layout.fillWidth: true + Layout.preferredWidth: parent.width * 2/3 + Layout.alignment: Qt.AlignHCenter Layout.preferredHeight: constants.paddingTiny - color: Qt.rgba(0,0,0,0.10) + color: Material.background //Qt.rgba(0,0,0,0.10) } } diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index 697e8d1df..e0d20177c 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -146,6 +146,7 @@ class QEInvoiceParser(QEInvoice): @recipient.setter def recipient(self, recipient: str): #if self._recipient != recipient: + self.canPay = False self._recipient = recipient if recipient: self.validateRecipient(recipient) @@ -249,24 +250,24 @@ class QEInvoiceParser(QEInvoice): self.userinfo = _('Can\'t pay, insufficient balance') else: self.userinfo = { - PR_EXPIRED: _('Can\'t pay, invoice is expired'), - PR_PAID: _('Can\'t pay, invoice is already paid'), - PR_INFLIGHT: _('Can\'t pay, invoice is already being paid'), - PR_ROUTING: _('Can\'t pay, invoice is already being paid'), - PR_UNKNOWN: _('Can\'t pay, invoice has unknown status'), + PR_EXPIRED: _('Invoice is expired'), + PR_PAID: _('Invoice is already paid'), + PR_INFLIGHT: _('Invoice is already being paid'), + PR_ROUTING: _('Invoice is already being paid'), + PR_UNKNOWN: _('Invoice has unknown status'), }[self.status] elif self.invoiceType == QEInvoice.Type.OnchainInvoice: if self.status in [PR_UNPAID, PR_FAILED]: if self.get_max_spendable_onchain() >= self.amount.satsInt: self.canPay = True else: - self.userinfo = _('Can\'t pay, insufficient balance') + self.userinfo = _('Insufficient balance') else: self.userinfo = { - PR_EXPIRED: _('Can\'t pay, invoice is expired'), - PR_PAID: _('Can\'t pay, invoice is already paid'), - PR_UNCONFIRMED: _('Can\'t pay, invoice is already paid'), - PR_UNKNOWN: _('Can\'t pay, invoice has unknown status'), + PR_EXPIRED: _('Invoice is expired'), + PR_PAID: _('Invoice is already paid'), + PR_UNCONFIRMED: _('Invoice is already paid'), + PR_UNKNOWN: _('Invoice has unknown status'), }[self.status]