From 55a35dc2cb23c84614fc8ea32e92de40f4c3958f Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Fri, 28 Mar 2025 13:23:59 -0400 Subject: [PATCH 01/21] qml: Introduce SendRecipientsListModel The SendRecipientsListModel is owned by WalletQmlModel --- src/Makefile.qt.include | 6 +- src/qml/models/sendrecipientslistmodel.cpp | 99 ++++++++++++++++++++++ src/qml/models/sendrecipientslistmodel.h | 55 ++++++++++++ src/qml/models/walletqmlmodel.cpp | 3 + src/qml/models/walletqmlmodel.h | 13 +-- 5 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 src/qml/models/sendrecipientslistmodel.cpp create mode 100644 src/qml/models/sendrecipientslistmodel.h diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 306dbe2aa6..81ab768072 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -44,7 +44,9 @@ QT_MOC_CPP = \ qml/models/moc_nodemodel.cpp \ qml/models/moc_options_model.cpp \ qml/models/moc_peerdetailsmodel.cpp \ - qml/models/moc_peerlistsortproxy.cpp \ + qml/models/moc_peerlistsortproxy.cpp \\ + qml/models/moc_sendrecipient.cpp \ + qml/models/moc_sendrecipientslistmodel.cpp \ qml/models/moc_transaction.cpp \ qml/models/moc_sendrecipient.cpp \ qml/models/moc_walletlistmodel.cpp \ @@ -138,6 +140,7 @@ BITCOIN_QT_H = \ qml/models/peerlistsortproxy.h \ qml/models/transaction.h \ qml/models/sendrecipient.h \ + qml/models/sendrecipientslistmodel.h \ qml/models/walletlistmodel.h \ qml/models/walletqmlmodel.h \ qml/models/walletqmlmodeltransaction.h \ @@ -339,6 +342,7 @@ BITCOIN_QML_BASE_CPP = \ qml/models/peerlistsortproxy.cpp \ qml/models/transaction.cpp \ qml/models/sendrecipient.cpp \ + qml/models/sendrecipientslistmodel.cpp \ qml/models/walletlistmodel.cpp \ qml/models/walletqmlmodel.cpp \ qml/models/walletqmlmodeltransaction.cpp \ diff --git a/src/qml/models/sendrecipientslistmodel.cpp b/src/qml/models/sendrecipientslistmodel.cpp new file mode 100644 index 0000000000..96d1f35656 --- /dev/null +++ b/src/qml/models/sendrecipientslistmodel.cpp @@ -0,0 +1,99 @@ +// Copyright (c) 2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include + +SendRecipientsListModel::SendRecipientsListModel(QObject* parent) + : QAbstractListModel(parent) +{ + m_recipients.append(new SendRecipient(this)); +} + +int SendRecipientsListModel::rowCount(const QModelIndex&) const +{ + return m_recipients.size(); +} + +QVariant SendRecipientsListModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid() || index.row() >= m_recipients.size()) + return {}; + + const auto& r = m_recipients[index.row()]; + switch (role) { + case AddressRole: return r->address(); + case LabelRole: return r->label(); + case AmountRole: return r->amount(); + case MessageRole: return r->message(); + default: return {}; + } + return {}; +} + +QHash SendRecipientsListModel::roleNames() const +{ + return { + {AddressRole, "address"}, + {LabelRole, "label"}, + {AmountRole, "amount"}, + {MessageRole, "message"}, + }; +} + +void SendRecipientsListModel::add() +{ + const int row = m_recipients.size(); + beginInsertRows(QModelIndex(), row, row); + m_recipients.append(new SendRecipient(this)); + endInsertRows(); + Q_EMIT countChanged(); + setCurrentIndex(row); +} + +void SendRecipientsListModel::setCurrentIndex(int row) +{ + if (row < 0 || row >= m_recipients.size()) + return; + + if (row == m_current) + return; + + m_current = row; + + Q_EMIT currentIndexChanged(); + Q_EMIT currentRecipientChanged(); +} + +void SendRecipientsListModel::next() +{ + setCurrentIndex(m_current + 1); +} + +void SendRecipientsListModel::prev() +{ + setCurrentIndex(m_current - 1); +} + +void SendRecipientsListModel::remove() +{ + if (m_recipients.size() == 1) { + return; + } + beginRemoveRows(QModelIndex(), m_current, m_current); + delete m_recipients.takeAt(m_current); + endRemoveRows(); + Q_EMIT countChanged(); + + setCurrentIndex(m_current - 1); +} + +SendRecipient* SendRecipientsListModel::currentRecipient() const +{ + if (m_current < 0 || m_current >= m_recipients.size()) + return nullptr; + + return m_recipients[m_current]; +} diff --git a/src/qml/models/sendrecipientslistmodel.h b/src/qml/models/sendrecipientslistmodel.h new file mode 100644 index 0000000000..b699451dfb --- /dev/null +++ b/src/qml/models/sendrecipientslistmodel.h @@ -0,0 +1,55 @@ +// Copyright (c) 2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QML_MODELS_SENDRECIPIENTSLISTMODEL_H +#define BITCOIN_QML_MODELS_SENDRECIPIENTSLISTMODEL_H + +#include + +#include +#include + +class SendRecipientsListModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int currentIndex READ currentIndex NOTIFY currentIndexChanged) + Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_PROPERTY(SendRecipient* current READ currentRecipient NOTIFY currentRecipientChanged) + +public: + enum Roles { + AddressRole = Qt::UserRole + 1, + LabelRole, + AmountRole, + MessageRole + }; + + explicit SendRecipientsListModel(QObject* parent = nullptr); + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + Q_INVOKABLE void add(); + Q_INVOKABLE void next(); + Q_INVOKABLE void prev(); + Q_INVOKABLE void remove(); + + int currentIndex() const { return m_current + 1; } + void setCurrentIndex(int row); + SendRecipient* currentRecipient() const; + int count() const { return m_recipients.size(); } + QList recipients() const { return m_recipients; } + +Q_SIGNALS: + void currentIndexChanged(); + void currentRecipientChanged(); + void countChanged(); + +private: + QList m_recipients; + int m_current{0}; +}; + +#endif // BITCOIN_QML_MODELS_SENDRECIPIENTSLISTMODEL_H diff --git a/src/qml/models/walletqmlmodel.cpp b/src/qml/models/walletqmlmodel.cpp index cdce215608..7d2528bd6f 100644 --- a/src/qml/models/walletqmlmodel.cpp +++ b/src/qml/models/walletqmlmodel.cpp @@ -25,6 +25,7 @@ WalletQmlModel::WalletQmlModel(std::unique_ptr wallet, QObje m_activity_list_model = new ActivityListModel(this); m_coins_list_model = new CoinsListModel(this); m_current_recipient = new SendRecipient(this); + m_send_recipients = new SendRecipientsListModel(this); } WalletQmlModel::WalletQmlModel(QObject* parent) @@ -33,6 +34,7 @@ WalletQmlModel::WalletQmlModel(QObject* parent) m_activity_list_model = new ActivityListModel(this); m_coins_list_model = new CoinsListModel(this); m_current_recipient = new SendRecipient(this); + m_send_recipients = new SendRecipientsListModel(this); } WalletQmlModel::~WalletQmlModel() @@ -40,6 +42,7 @@ WalletQmlModel::~WalletQmlModel() delete m_activity_list_model; delete m_coins_list_model; delete m_current_recipient; + delete m_send_recipients; if (m_current_transaction) { delete m_current_transaction; } diff --git a/src/qml/models/walletqmlmodel.h b/src/qml/models/walletqmlmodel.h index d97cd0851f..fbaf0f8ffe 100644 --- a/src/qml/models/walletqmlmodel.h +++ b/src/qml/models/walletqmlmodel.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -27,6 +28,7 @@ class WalletQmlModel : public QObject Q_PROPERTY(ActivityListModel* activityListModel READ activityListModel CONSTANT) Q_PROPERTY(CoinsListModel* coinsListModel READ coinsListModel CONSTANT) Q_PROPERTY(SendRecipient* sendRecipient READ sendRecipient CONSTANT) + Q_PROPERTY(SendRecipientsListModel* recipients READ sendRecipientList CONSTANT) Q_PROPERTY(WalletQmlModelTransaction* currentTransaction READ currentTransaction NOTIFY currentTransactionChanged) public: @@ -38,6 +40,11 @@ class WalletQmlModel : public QObject QString balance() const; ActivityListModel* activityListModel() const { return m_activity_list_model; } CoinsListModel* coinsListModel() const { return m_coins_list_model; } + SendRecipient* sendRecipient() const { return m_send_recipients->currentRecipient(); } + SendRecipientsListModel* sendRecipientList() const { return m_send_recipients; } + WalletQmlModelTransaction* currentTransaction() const { return m_current_transaction; } + Q_INVOKABLE bool prepareTransaction(); + Q_INVOKABLE void sendTransaction(); std::set getWalletTxs() const; interfaces::WalletTx getWalletTx(const uint256& hash) const; @@ -46,11 +53,6 @@ class WalletQmlModel : public QObject int& num_blocks, int64_t& block_time) const; - SendRecipient* sendRecipient() const { return m_current_recipient; } - WalletQmlModelTransaction* currentTransaction() const { return m_current_transaction; } - Q_INVOKABLE bool prepareTransaction(); - Q_INVOKABLE void sendTransaction(); - using TransactionChangedFn = std::function; virtual std::unique_ptr handleTransactionChanged(TransactionChangedFn fn); @@ -73,6 +75,7 @@ class WalletQmlModel : public QObject std::unique_ptr m_wallet; ActivityListModel* m_activity_list_model{nullptr}; CoinsListModel* m_coins_list_model{nullptr}; + SendRecipientsListModel* m_send_recipients{nullptr}; SendRecipient* m_current_recipient{nullptr}; WalletQmlModelTransaction* m_current_transaction{nullptr}; wallet::CCoinControl m_coin_control; From ca6dd17a6d42e92f5216e2e24b2a82d0c4295036 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Sat, 29 Mar 2025 23:17:41 -0400 Subject: [PATCH 02/21] qml: Add Multiple Recipients toggle to Send menu --- src/qml/controls/SendOptionsPopup.qml | 28 ++++++++++++++++++++++++--- src/qml/pages/wallet/Send.qml | 5 +++-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/qml/controls/SendOptionsPopup.qml b/src/qml/controls/SendOptionsPopup.qml index f67ff139ec..a7ee8765a1 100644 --- a/src/qml/controls/SendOptionsPopup.qml +++ b/src/qml/controls/SendOptionsPopup.qml @@ -13,14 +13,36 @@ OptionPopup { id: root property alias coinControlEnabled: coinControlToggle.checked + property alias multipleRecipientsEnabled: multipleRecipientsToggle.checked + + implicitWidth: 300 + implicitHeight: 100 clip: true modal: true dim: false - EllipsisMenuToggleItem { - id: coinControlToggle + ColumnLayout { + id: columnLayout anchors.centerIn: parent - text: qsTr("Enable Coin control") + anchors.margins: 10 + spacing: 0 + + EllipsisMenuToggleItem { + id: coinControlToggle + Layout.fillWidth: true + text: qsTr("Enable Coin control") + } + + Separator { + id: separator + Layout.fillWidth: true + } + + EllipsisMenuToggleItem { + id: multipleRecipientsToggle + Layout.fillWidth: true + text: qsTr("Multiple Recipients") + } } } \ No newline at end of file diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml index 9c1905db99..40fd8e3d92 100644 --- a/src/qml/pages/wallet/Send.qml +++ b/src/qml/pages/wallet/Send.qml @@ -33,6 +33,7 @@ PageStack { Settings { id: settings property alias coinControlEnabled: sendOptionsPopup.coinControlEnabled + property alias multipleRecipientsEnabled: sendOptionsPopup.multipleRecipientsEnabled } ScrollView { @@ -55,6 +56,7 @@ PageStack { Layout.fillWidth: true Layout.topMargin: 30 Layout.bottomMargin: 20 + CoreText { id: title anchors.left: parent.left @@ -64,6 +66,7 @@ PageStack { color: Theme.color.neutral9 bold: true } + EllipsisMenuButton { id: menuButton anchors.right: parent.right @@ -78,8 +81,6 @@ PageStack { id: sendOptionsPopup x: menuButton.x - width + menuButton.width y: menuButton.y + menuButton.height - width: 300 - height: 50 } } From e4a2d8a958c045b12c54c211ba456579dc61fa16 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Tue, 1 Apr 2025 23:50:04 -0400 Subject: [PATCH 03/21] qml: Add Multiple Recipients bar to Send form --- src/qml/controls/SendOptionsPopup.qml | 2 +- src/qml/models/walletqmlmodel.cpp | 10 ++++++++++ src/qml/models/walletqmlmodel.h | 7 +++++++ src/qml/pages/wallet/Send.qml | 28 +++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/qml/controls/SendOptionsPopup.qml b/src/qml/controls/SendOptionsPopup.qml index a7ee8765a1..b96ffc5ec8 100644 --- a/src/qml/controls/SendOptionsPopup.qml +++ b/src/qml/controls/SendOptionsPopup.qml @@ -45,4 +45,4 @@ OptionPopup { text: qsTr("Multiple Recipients") } } -} \ No newline at end of file +} diff --git a/src/qml/models/walletqmlmodel.cpp b/src/qml/models/walletqmlmodel.cpp index 7d2528bd6f..245030a77f 100644 --- a/src/qml/models/walletqmlmodel.cpp +++ b/src/qml/models/walletqmlmodel.cpp @@ -208,3 +208,13 @@ std::vector WalletQmlModel::listSelectedCoins() const { return m_coin_control.ListSelected(); } + +int WalletQmlModel::recipientIndex() const +{ + return 1; +} + +int WalletQmlModel::recipientsCount() const +{ + return 1; +} diff --git a/src/qml/models/walletqmlmodel.h b/src/qml/models/walletqmlmodel.h index fbaf0f8ffe..c51faafb65 100644 --- a/src/qml/models/walletqmlmodel.h +++ b/src/qml/models/walletqmlmodel.h @@ -30,6 +30,8 @@ class WalletQmlModel : public QObject Q_PROPERTY(SendRecipient* sendRecipient READ sendRecipient CONSTANT) Q_PROPERTY(SendRecipientsListModel* recipients READ sendRecipientList CONSTANT) Q_PROPERTY(WalletQmlModelTransaction* currentTransaction READ currentTransaction NOTIFY currentTransactionChanged) + Q_PROPERTY(int recipientIndex READ recipientIndex NOTIFY recipientIndexChanged) + Q_PROPERTY(int recipientsCount READ recipientsCount NOTIFY recipientsCountChanged) public: WalletQmlModel(std::unique_ptr wallet, QObject* parent = nullptr); @@ -66,10 +68,15 @@ class WalletQmlModel : public QObject bool isSelectedCoin(const COutPoint& output); std::vector listSelectedCoins() const; + int recipientIndex() const; + int recipientsCount() const; + Q_SIGNALS: void nameChanged(); void balanceChanged(); void currentTransactionChanged(); + void recipientIndexChanged(); + void recipientsCountChanged(); private: std::unique_ptr m_wallet; diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml index 40fd8e3d92..123bd65d33 100644 --- a/src/qml/pages/wallet/Send.qml +++ b/src/qml/pages/wallet/Send.qml @@ -84,6 +84,34 @@ PageStack { } } + RowLayout { + id: selectAndAddRecipients + Layout.fillWidth: true + Layout.topMargin: 10 + Layout.bottomMargin: 10 + visible: settings.multipleRecipientsEnabled + + NavButton { + iconSource: "image://images/caret-left" + } + + NavButton { + iconSource: "image://images/caret-right" + } + + CoreText { + id: selectAndAddRecipientsLabel + text: qsTr("Recipient %1 of %2").arg(wallet.recipientIndex).arg(wallet.recipientsCount) + font.pixelSize: 18 + color: Theme.color.neutral9 + } + } + + Separator { + visible: settings.multipleRecipientsEnabled + Layout.fillWidth: true + } + LabeledTextInput { id: address Layout.fillWidth: true From c259984ab1fa53a3f4c49b309caa14b83f3da63a Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Fri, 2 May 2025 23:04:07 -0400 Subject: [PATCH 04/21] qml: Reduce size of recipient selectors --- src/qml/controls/NavButton.qml | 1 + src/qml/pages/wallet/Send.qml | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/qml/controls/NavButton.qml b/src/qml/controls/NavButton.qml index 965161b983..37e4114a03 100644 --- a/src/qml/controls/NavButton.qml +++ b/src/qml/controls/NavButton.qml @@ -53,6 +53,7 @@ AbstractButton { } contentItem: RowLayout { spacing: 0 + anchors.fill: parent Loader { id: button_background active: root.iconSource.toString().length > 0 diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml index 123bd65d33..1f7c18bcf6 100644 --- a/src/qml/pages/wallet/Send.qml +++ b/src/qml/pages/wallet/Send.qml @@ -92,10 +92,18 @@ PageStack { visible: settings.multipleRecipientsEnabled NavButton { + Layout.preferredWidth: 30 + Layout.preferredHeight: 30 + iconWidth: 30 + iconHeight: 30 iconSource: "image://images/caret-left" } NavButton { + Layout.preferredWidth: 30 + Layout.preferredHeight: 30 + iconWidth: 30 + iconHeight: 30 iconSource: "image://images/caret-right" } From 132e03fc2709bb418928a50be7ddb2527b0e518c Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Thu, 8 May 2025 08:57:40 -0400 Subject: [PATCH 05/21] qml: Add remove button to multiple recipients --- src/qml/models/sendrecipientslistmodel.cpp | 1 + src/qml/models/sendrecipientslistmodel.h | 1 + src/qml/models/walletqmlmodel.cpp | 12 ++------ src/qml/models/walletqmlmodel.h | 7 ----- src/qml/pages/wallet/Send.qml | 34 ++++++++++++++++++++-- 5 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/qml/models/sendrecipientslistmodel.cpp b/src/qml/models/sendrecipientslistmodel.cpp index 96d1f35656..7ffa938fd9 100644 --- a/src/qml/models/sendrecipientslistmodel.cpp +++ b/src/qml/models/sendrecipientslistmodel.cpp @@ -3,6 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include diff --git a/src/qml/models/sendrecipientslistmodel.h b/src/qml/models/sendrecipientslistmodel.h index b699451dfb..f7ed1cef4b 100644 --- a/src/qml/models/sendrecipientslistmodel.h +++ b/src/qml/models/sendrecipientslistmodel.h @@ -9,6 +9,7 @@ #include #include +#include class SendRecipientsListModel : public QAbstractListModel { diff --git a/src/qml/models/walletqmlmodel.cpp b/src/qml/models/walletqmlmodel.cpp index 245030a77f..6fc2fdb200 100644 --- a/src/qml/models/walletqmlmodel.cpp +++ b/src/qml/models/walletqmlmodel.cpp @@ -1,3 +1,4 @@ + // Copyright (c) 2024 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -6,6 +7,7 @@ #include #include +#include #include #include @@ -208,13 +210,3 @@ std::vector WalletQmlModel::listSelectedCoins() const { return m_coin_control.ListSelected(); } - -int WalletQmlModel::recipientIndex() const -{ - return 1; -} - -int WalletQmlModel::recipientsCount() const -{ - return 1; -} diff --git a/src/qml/models/walletqmlmodel.h b/src/qml/models/walletqmlmodel.h index c51faafb65..fbaf0f8ffe 100644 --- a/src/qml/models/walletqmlmodel.h +++ b/src/qml/models/walletqmlmodel.h @@ -30,8 +30,6 @@ class WalletQmlModel : public QObject Q_PROPERTY(SendRecipient* sendRecipient READ sendRecipient CONSTANT) Q_PROPERTY(SendRecipientsListModel* recipients READ sendRecipientList CONSTANT) Q_PROPERTY(WalletQmlModelTransaction* currentTransaction READ currentTransaction NOTIFY currentTransactionChanged) - Q_PROPERTY(int recipientIndex READ recipientIndex NOTIFY recipientIndexChanged) - Q_PROPERTY(int recipientsCount READ recipientsCount NOTIFY recipientsCountChanged) public: WalletQmlModel(std::unique_ptr wallet, QObject* parent = nullptr); @@ -68,15 +66,10 @@ class WalletQmlModel : public QObject bool isSelectedCoin(const COutPoint& output); std::vector listSelectedCoins() const; - int recipientIndex() const; - int recipientsCount() const; - Q_SIGNALS: void nameChanged(); void balanceChanged(); void currentTransactionChanged(); - void recipientIndexChanged(); - void recipientsCountChanged(); private: std::unique_ptr m_wallet; diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml index 1f7c18bcf6..89993ea73d 100644 --- a/src/qml/pages/wallet/Send.qml +++ b/src/qml/pages/wallet/Send.qml @@ -16,7 +16,7 @@ PageStack { vertical: true property WalletQmlModel wallet: walletController.selectedWallet - property SendRecipient recipient: wallet.sendRecipient + property SendRecipient recipient: wallet.recipients.current signal transactionPrepared() @@ -97,6 +97,9 @@ PageStack { iconWidth: 30 iconHeight: 30 iconSource: "image://images/caret-left" + onClicked: { + wallet.recipients.prev() + } } NavButton { @@ -105,14 +108,41 @@ PageStack { iconWidth: 30 iconHeight: 30 iconSource: "image://images/caret-right" + onClicked: { + wallet.recipients.next() + } } CoreText { + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft id: selectAndAddRecipientsLabel - text: qsTr("Recipient %1 of %2").arg(wallet.recipientIndex).arg(wallet.recipientsCount) + text: qsTr("Recipient %1 of %2").arg(wallet.recipients.currentIndex).arg(wallet.recipients.count) font.pixelSize: 18 color: Theme.color.neutral9 } + + NavButton { + Layout.preferredWidth: 30 + Layout.preferredHeight: 30 + iconWidth: 20 + iconHeight: 20 + iconSource: "image://images/plus" + onClicked: { + wallet.recipients.add() + } + } + NavButton { + Layout.preferredWidth: 30 + Layout.preferredHeight: 30 + iconWidth: 20 + iconHeight: 20 + iconSource: "image://images/minus" + visible: wallet.recipients.count > 1 + onClicked: { + wallet.recipients.remove() + } + } } Separator { From 19b9b2f74f2635a29fe7eceddaba3078d9073a02 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Fri, 9 May 2025 23:44:17 -0400 Subject: [PATCH 06/21] qml: Prepare transaction with recipients list --- src/qml/models/walletqmlmodel.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/qml/models/walletqmlmodel.cpp b/src/qml/models/walletqmlmodel.cpp index 6fc2fdb200..45a4f44779 100644 --- a/src/qml/models/walletqmlmodel.cpp +++ b/src/qml/models/walletqmlmodel.cpp @@ -107,16 +107,21 @@ bool WalletQmlModel::prepareTransaction() return false; } - CScript scriptPubKey = GetScriptForDestination(DecodeDestination(m_current_recipient->address().toStdString())); - wallet::CRecipient recipient = {scriptPubKey, m_current_recipient->cAmount(), m_current_recipient->subtractFeeFromAmount()}; - m_coin_control.m_feerate = CFeeRate(1000); + std::vector vecSend; + CAmount total = 0; + for (auto* recipient : m_send_recipients->recipients()) { + CScript scriptPubKey = GetScriptForDestination(DecodeDestination(recipient->address().toStdString())); + wallet::CRecipient c_recipient = {scriptPubKey, recipient->cAmount(), recipient->subtractFeeFromAmount()}; + m_coin_control.m_feerate = CFeeRate(1000); + vecSend.push_back(c_recipient); + total += recipient->cAmount(); + } CAmount balance = m_wallet->getBalance(); - if (balance < recipient.nAmount) { + if (balance < total) { return false; } - std::vector vecSend{recipient}; int nChangePosRet = -1; CAmount nFeeRequired = 0; const auto& res = m_wallet->createTransaction(vecSend, m_coin_control, true, nChangePosRet, nFeeRequired); From b4e421e19dbbf564431a57319e88630edffb820b Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Thu, 29 May 2025 00:34:13 -0400 Subject: [PATCH 07/21] qml: Add MultipleSendReview page --- src/Makefile.qt.include | 1 + src/qml/bitcoin_qml.qrc | 1 + src/qml/pages/main.qml | 20 ++- src/qml/pages/wallet/DesktopWallets.qml | 4 +- src/qml/pages/wallet/MultipleSendReview.qml | 155 ++++++++++++++++++++ src/qml/pages/wallet/Send.qml | 4 +- 6 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 src/qml/pages/wallet/MultipleSendReview.qml diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 81ab768072..eef725ad78 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -488,6 +488,7 @@ QML_RES_QML = \ qml/pages/wallet/CreatePassword.qml \ qml/pages/wallet/CreateWalletWizard.qml \ qml/pages/wallet/DesktopWallets.qml \ + qml/pages/wallet/MultipleSendReview.qml \ qml/pages/wallet/RequestPayment.qml \ qml/pages/wallet/Send.qml \ qml/pages/wallet/SendResult.qml \ diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc index 8557ebe0e6..21982121a2 100644 --- a/src/qml/bitcoin_qml.qrc +++ b/src/qml/bitcoin_qml.qrc @@ -90,6 +90,7 @@ pages/wallet/CreatePassword.qml pages/wallet/CreateWalletWizard.qml pages/wallet/DesktopWallets.qml + pages/wallet/MultipleSendReview.qml pages/wallet/RequestPayment.qml pages/wallet/Send.qml pages/wallet/SendResult.qml diff --git a/src/qml/pages/main.qml b/src/qml/pages/main.qml index 60aa6c2705..7b606de4a7 100644 --- a/src/qml/pages/main.qml +++ b/src/qml/pages/main.qml @@ -85,7 +85,11 @@ ApplicationWindow { main.push(createWalletWizard) } onSendTransaction: { - main.push(sendReviewPage) + if (multipleRecipientsEnabled) { + main.push(multipleSendReviewPage) + } else { + main.push(sendReviewPage) + } } } } @@ -113,6 +117,20 @@ ApplicationWindow { } } + Component { + id: multipleSendReviewPage + MultipleSendReview { + onBack: { + main.pop() + } + onTransactionSent: { + walletController.selectedWallet.sendRecipient.clear() + main.pop() + sendResult.open() + } + } + } + SendResult { id: sendResult closePolicy: Popup.CloseOnPressOutside diff --git a/src/qml/pages/wallet/DesktopWallets.qml b/src/qml/pages/wallet/DesktopWallets.qml index c9fe742993..f1ea7b76da 100644 --- a/src/qml/pages/wallet/DesktopWallets.qml +++ b/src/qml/pages/wallet/DesktopWallets.qml @@ -20,7 +20,7 @@ Page { ButtonGroup { id: navigationTabs } signal addWallet() - signal sendTransaction() + signal sendTransaction(bool multipleRecipientsEnabled) header: NavigationBar2 { id: navBar @@ -136,7 +136,7 @@ Page { } Send { id: sendTab - onTransactionPrepared: root.sendTransaction() + onTransactionPrepared: root.sendTransaction(multipleRecipientsEnabled) } RequestPayment { id: receiveTab diff --git a/src/qml/pages/wallet/MultipleSendReview.qml b/src/qml/pages/wallet/MultipleSendReview.qml new file mode 100644 index 0000000000..91f6ee2d96 --- /dev/null +++ b/src/qml/pages/wallet/MultipleSendReview.qml @@ -0,0 +1,155 @@ +// Copyright (c) 2024 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import org.bitcoincore.qt 1.0 + +import "../../controls" +import "../../components" + +Page { + id: root + background: null + + property WalletQmlModel wallet: walletController.selectedWallet + property WalletQmlModelTransaction transaction: walletController.selectedWallet.currentTransaction + + signal finished() + signal back() + signal transactionSent() + + header: NavigationBar2 { + id: navbar + leftItem: NavButton { + iconSource: "image://images/caret-left" + text: qsTr("Back") + onClicked: { + root.back() + } + } + } + + ScrollView { + clip: true + width: parent.width + height: parent.height + contentWidth: width + + ColumnLayout { + id: columnLayout + width: 450 + anchors.horizontalCenter: parent.horizontalCenter + + spacing: 15 + + CoreText { + id: title + Layout.topMargin: 30 + Layout.bottomMargin: 15 + text: qsTr("Transaction details") + font.pixelSize: 21 + bold: true + } + + ListView { + id: inputsList + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + model: root.wallet.recipients + delegate: Item { + id: delegate + height: 55 + width: ListView.view.width + + required property string address; + required property string label; + required property string amount; + + RowLayout { + spacing: 10 + anchors.fill: parent + CoreText { + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + text: label == "" ? address : label + font.pixelSize: 18 + elide: Text.ElideMiddle + } + + CoreText { + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + text: amount + font.pixelSize: 18 + } + } + + Separator { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + color: Theme.color.neutral3 + } + } + } + + RowLayout { + Layout.topMargin: 20 + CoreText { + text: qsTr("Total amount") + font.pixelSize: 20 + color: Theme.color.neutral9 + horizontalAlignment: Text.AlignLeft + } + Item { + Layout.fillWidth: true + } + CoreText { + text: root.transaction.total + font.pixelSize: 20 + color: Theme.color.neutral9 + } + } + + Separator { + Layout.fillWidth: true + color: Theme.color.neutral3 + } + + RowLayout { + CoreText { + text: qsTr("Fee") + font.pixelSize: 18 + Layout.preferredWidth: 110 + horizontalAlignment: Text.AlignLeft + } + Item { + Layout.fillWidth: true + } + CoreText { + text: root.transaction.fee + font.pixelSize: 15 + } + } + + Separator { + Layout.fillWidth: true + color: Theme.color.neutral3 + } + + ContinueButton { + id: confimationButton + Layout.fillWidth: true + Layout.topMargin: 30 + text: qsTr("Send") + onClicked: { + root.wallet.sendTransaction() + root.transactionSent() + } + } + } + } +} diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml index 89993ea73d..67e882b622 100644 --- a/src/qml/pages/wallet/Send.qml +++ b/src/qml/pages/wallet/Send.qml @@ -18,7 +18,7 @@ PageStack { property WalletQmlModel wallet: walletController.selectedWallet property SendRecipient recipient: wallet.recipients.current - signal transactionPrepared() + signal transactionPrepared(bool multipleRecipientsEnabled) Connections { target: walletController @@ -293,7 +293,7 @@ PageStack { text: qsTr("Review") onClicked: { if (root.wallet.prepareTransaction()) { - root.transactionPrepared() + root.transactionPrepared(settings.multipleRecipientsEnabled); } } } From e583ae214c2f394f34fb31b1e6d9e1a187aee5f2 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Fri, 30 May 2025 23:20:09 -0400 Subject: [PATCH 08/21] qml: Add plus big filled icon --- src/Makefile.qt.include | 1 + src/qml/bitcoin_qml.qrc | 1 + src/qml/res/icons/plus-big-filled.png | Bin 0 -> 315 bytes src/qml/res/src/plus-big-filled.svg | 3 +++ 4 files changed, 5 insertions(+) create mode 100644 src/qml/res/icons/plus-big-filled.png create mode 100644 src/qml/res/src/plus-big-filled.svg diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index eef725ad78..92de9fbb5e 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -383,6 +383,7 @@ QML_RES_ICONS = \ qml/res/icons/network-dark.png \ qml/res/icons/network-light.png \ qml/res/icons/plus.png \ + qml/res/icons/plus-big-filled.png \ qml/res/icons/pending.png \ qml/res/icons/shutdown.png \ qml/res/icons/singlesig-wallet.png \ diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc index 21982121a2..b39af15cb2 100644 --- a/src/qml/bitcoin_qml.qrc +++ b/src/qml/bitcoin_qml.qrc @@ -127,6 +127,7 @@ res/icons/network-dark.png res/icons/network-light.png res/icons/plus.png + res/icons/plus-big-filled.png res/icons/pending.png res/icons/shutdown.png res/icons/singlesig-wallet.png diff --git a/src/qml/res/icons/plus-big-filled.png b/src/qml/res/icons/plus-big-filled.png new file mode 100644 index 0000000000000000000000000000000000000000..365ed049e57d97a55e8696a73a6bdee3592f629d GIT binary patch literal 315 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw1|+Ti+$;i8oCO|{#S9E$svykh8Km+7D9BhG zS6{ zSt(0fo_XoNmf5ZUCs^cL-=TYfH+G*CjnnAsy(iw~zpMM$wNF3lW_o!qv@Zy#4wW%F zxcj@$L#?HvCFc#(b_7_7vvz%KG4{@j{AN?X{F}^to$qqjuNK!`$)4uE^q*MXxzuGX z75vriiSISu$hPiXT)>q2e8>4H%dE5Ux;w#+Iq+a!TcpXhz1sc#Aik%opUXO@geCxR C?t32q literal 0 HcmV?d00001 diff --git a/src/qml/res/src/plus-big-filled.svg b/src/qml/res/src/plus-big-filled.svg new file mode 100644 index 0000000000..2efe7ba2c5 --- /dev/null +++ b/src/qml/res/src/plus-big-filled.svg @@ -0,0 +1,3 @@ + + + From 95fee668fa2b06da965280314a8acaba076f7ae0 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Fri, 30 May 2025 23:21:05 -0400 Subject: [PATCH 09/21] qml: Replace NavButton with IconButton in Send --- src/Makefile.qt.include | 2 +- src/qml/bitcoin_qml.qrc | 2 +- ...{EllipsisMenuButton.qml => IconButton.qml} | 38 ++++++++++---- src/qml/pages/wallet/Send.qml | 50 ++++++++++--------- 4 files changed, 57 insertions(+), 35 deletions(-) rename src/qml/controls/{EllipsisMenuButton.qml => IconButton.qml} (51%) diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 92de9fbb5e..019b2bd15c 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -425,12 +425,12 @@ QML_RES_QML = \ qml/controls/CoreCheckBox.qml \ qml/controls/CoreText.qml \ qml/controls/CoreTextField.qml \ - qml/controls/EllipsisMenuButton.qml \ qml/controls/EllipsisMenuToggleItem.qml \ qml/controls/ExternalLink.qml \ qml/controls/FocusBorder.qml \ qml/controls/Header.qml \ qml/controls/Icon.qml \ + qml/controls/IconButton.qml \ qml/controls/InformationPage.qml \ qml/controls/IPAddressValueInput.qml \ qml/controls/KeyValueRow.qml \ diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc index b39af15cb2..6bcd5f0705 100644 --- a/src/qml/bitcoin_qml.qrc +++ b/src/qml/bitcoin_qml.qrc @@ -30,12 +30,12 @@ controls/FocusBorder.qml controls/Header.qml controls/Icon.qml + controls/IconButton.qml controls/InformationPage.qml controls/IPAddressValueInput.qml controls/KeyValueRow.qml controls/LabeledTextInput.qml controls/LabeledCoinControlButton.qml - controls/EllipsisMenuButton.qml controls/EllipsisMenuToggleItem.qml controls/NavButton.qml controls/NavigationBar.qml diff --git a/src/qml/controls/EllipsisMenuButton.qml b/src/qml/controls/IconButton.qml similarity index 51% rename from src/qml/controls/EllipsisMenuButton.qml rename to src/qml/controls/IconButton.qml index 593ede0902..0cddb36cdf 100644 --- a/src/qml/controls/EllipsisMenuButton.qml +++ b/src/qml/controls/IconButton.qml @@ -11,12 +11,16 @@ import org.bitcoincore.qt 1.0 Button { id: root + property color iconColor: Theme.color.orange property color hoverColor: Theme.color.orange property color activeColor: Theme.color.orange + property int size: 35 + property alias iconSource: icon.source hoverEnabled: AppMode.isDesktop - implicitHeight: 35 - implicitWidth: 35 + height: root.size + width: root.size + padding: 0 MouseArea { anchors.fill: parent @@ -25,28 +29,44 @@ Button { cursorShape: Qt.PointingHandCursor } - background: null + background: Rectangle { + id: bg + anchors.fill: parent + radius: 5 + color: Theme.color.background + + + Behavior on color { + ColorAnimation { duration: 150 } + } + } contentItem: Icon { - id: ellipsisIcon + id: icon anchors.fill: parent source: "image://images/ellipsis" - color: Theme.color.neutral9 - size: 35 + size: root.size + color: iconColor + + Behavior on color { + ColorAnimation { duration: 150 } + } } states: [ State { name: "CHECKED"; when: root.checked - PropertyChanges { target: ellipsisIcon; color: activeColor } + PropertyChanges { target: icon; color: activeColor } }, State { name: "HOVER"; when: root.hovered - PropertyChanges { target: ellipsisIcon; color: hoverColor } + PropertyChanges { target: icon; color: hoverColor } + PropertyChanges { target: bg; color: Theme.color.neutral2 } }, State { name: "DISABLED"; when: !root.enabled - PropertyChanges { target: ellipsisIcon; color: Theme.color.neutral4 } + PropertyChanges { target: icon; color: Theme.color.neutral2 } + PropertyChanges { target: bg; color: Theme.color.background } } ] } diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml index 67e882b622..5655888b5c 100644 --- a/src/qml/pages/wallet/Send.qml +++ b/src/qml/pages/wallet/Send.qml @@ -67,11 +67,12 @@ PageStack { bold: true } - EllipsisMenuButton { + IconButton { id: menuButton anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter checked: sendOptionsPopup.opened + iconSource: "image://images/ellipsis" onClicked: { sendOptionsPopup.open() } @@ -91,54 +92,55 @@ PageStack { Layout.bottomMargin: 10 visible: settings.multipleRecipientsEnabled - NavButton { + CoreText { + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + id: selectAndAddRecipientsLabel + text: qsTr("Recipient %1 of %2").arg(wallet.recipients.currentIndex).arg(wallet.recipients.count) + horizontalAlignment: Text.AlignLeft + font.pixelSize: 18 + color: Theme.color.neutral9 + } + + IconButton { Layout.preferredWidth: 30 Layout.preferredHeight: 30 - iconWidth: 30 - iconHeight: 30 + size: 30 iconSource: "image://images/caret-left" + enabled: wallet.recipients.currentIndex - 1 > 0 onClicked: { wallet.recipients.prev() + } } - NavButton { + IconButton { Layout.preferredWidth: 30 Layout.preferredHeight: 30 - iconWidth: 30 - iconHeight: 30 + size: 30 iconSource: "image://images/caret-right" + enabled: wallet.recipients.currentIndex < wallet.recipients.count onClicked: { wallet.recipients.next() } } - CoreText { - Layout.fillWidth: true - Layout.alignment: Qt.AlignLeft - id: selectAndAddRecipientsLabel - text: qsTr("Recipient %1 of %2").arg(wallet.recipients.currentIndex).arg(wallet.recipients.count) - font.pixelSize: 18 - color: Theme.color.neutral9 - } - - NavButton { + IconButton { Layout.preferredWidth: 30 Layout.preferredHeight: 30 - iconWidth: 20 - iconHeight: 20 - iconSource: "image://images/plus" + size: 30 + iconSource: "image://images/plus-big-filled" onClicked: { wallet.recipients.add() } } - NavButton { + + IconButton { Layout.preferredWidth: 30 Layout.preferredHeight: 30 - iconWidth: 20 - iconHeight: 20 + size: 30 iconSource: "image://images/minus" - visible: wallet.recipients.count > 1 + enabled: wallet.recipients.count > 1 onClicked: { wallet.recipients.remove() } From b357e395b311e141303549c4d15b1b4f8414fd05 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Sat, 31 May 2025 00:46:10 -0400 Subject: [PATCH 10/21] qml: Cleanup BitcoinAmount This merges BitcoinAmount with SendRecipient to simplify the qml logic. By doing so, the conversions can be managed all in c++ against the satoshi member variable. Each recipient having its own BitcoinAmount allows the amounts to be saved independantly when there are multiple recipients. --- src/qml/bitcoinamount.cpp | 155 ++++++++++----------- src/qml/bitcoinamount.h | 31 +++-- src/qml/models/sendrecipient.cpp | 24 +--- src/qml/models/sendrecipient.h | 16 +-- src/qml/models/sendrecipientslistmodel.cpp | 2 +- src/qml/pages/wallet/Send.qml | 22 +-- 6 files changed, 114 insertions(+), 136 deletions(-) diff --git a/src/qml/bitcoinamount.cpp b/src/qml/bitcoinamount.cpp index 153d8fabae..13c00bf5de 100644 --- a/src/qml/bitcoinamount.cpp +++ b/src/qml/bitcoinamount.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2024 The Bitcoin Core developers +// Copyright (c) 2024-2025 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -7,19 +7,9 @@ #include #include - -BitcoinAmount::BitcoinAmount(QObject *parent) : QObject(parent) +BitcoinAmount::BitcoinAmount(QObject* parent) + : QObject(parent) { - m_unit = Unit::BTC; -} - -int BitcoinAmount::decimals(Unit unit) -{ - switch (unit) { - case Unit::BTC: return 8; - case Unit::SAT: return 0; - } // no default case, so the compiler can warn about missing cases - assert(false); } QString BitcoinAmount::sanitize(const QString &text) @@ -43,6 +33,30 @@ QString BitcoinAmount::sanitize(const QString &text) return result; } +qint64 BitcoinAmount::satoshi() const +{ + return m_satoshi; +} + +void BitcoinAmount::setSatoshi(qint64 new_amount) +{ + m_isSet = true; + if (m_satoshi != new_amount) { + m_satoshi = new_amount; + Q_EMIT amountChanged(); + } +} + +void BitcoinAmount::clear() +{ + if (!m_isSet && m_satoshi == 0) { + return; + } + m_satoshi = 0; + m_isSet = false; + Q_EMIT amountChanged(); +} + BitcoinAmount::Unit BitcoinAmount::unit() const { return m_unit; @@ -58,97 +72,82 @@ QString BitcoinAmount::unitLabel() const { switch (m_unit) { case Unit::BTC: return "₿"; - case Unit::SAT: return "Sat"; + case Unit::SAT: return "sat"; } assert(false); } -QString BitcoinAmount::amount() const +void BitcoinAmount::flipUnit() { - return m_amount; + if (m_unit == Unit::BTC) { + m_unit = Unit::SAT; + } else { + m_unit = Unit::BTC; + } + Q_EMIT unitChanged(); + Q_EMIT amountChanged(); } -QString BitcoinAmount::satoshiAmount() const +QString BitcoinAmount::satsToBtc(qint64 sat) { - return toSatoshis(m_amount); -} + const bool negative = sat < 0; + qint64 absSat = negative ? -sat : sat; -void BitcoinAmount::setAmount(const QString& new_amount) -{ - m_amount = sanitize(new_amount); - Q_EMIT amountChanged(); + const qint64 wholePart = absSat / COIN; + const qint64 fracInt = absSat % COIN; + QString fracPart = QString("%1").arg(fracInt, 8, 10, QLatin1Char('0')); + + QString result = QString::number(wholePart) + '.' + fracPart; + if (negative) { + result.prepend('-'); + } + return result; } -QString BitcoinAmount::toSatoshis(const QString& text) const +QString BitcoinAmount::toDisplay() const { + if (!m_isSet) { + return ""; + } if (m_unit == Unit::SAT) { - return text; + return QString::number(m_satoshi); } else { - return convert(text, m_unit); + return satsToBtc(m_satoshi); } } -long long BitcoinAmount::toSatoshis(QString& amount, const Unit unit) +qint64 BitcoinAmount::btcToSats(const QString& btcSanitized) { - int num_decimals = decimals(unit); - - QStringList parts = amount.remove(' ').split("."); + if (btcSanitized.isEmpty() || btcSanitized == ".") return 0; - QString whole = parts[0]; - QString decimals; + QString cleaned = btcSanitized; + if (cleaned.startsWith('.')) cleaned.prepend('0'); - if(parts.size() > 1) - { - decimals = parts[1]; + QStringList parts = cleaned.split('.'); + const qint64 whole = parts[0].isEmpty() ? 0 : parts[0].toLongLong(); + qint64 frac = 0; + if (parts.size() == 2) { + frac = parts[1].leftJustified(8, '0').toLongLong(); } - QString str = whole + decimals.leftJustified(num_decimals, '0', true); - return str.toLongLong(); + return whole * COIN + frac; } -QString BitcoinAmount::convert(const QString& amount, Unit unit) const +void BitcoinAmount::fromDisplay(const QString& text) { - if (amount == "") { - return amount; - } - - QString result = amount; - int decimalPosition = result.indexOf("."); - - if (decimalPosition == -1) { - decimalPosition = result.length(); - result.append("."); + if (text.trimmed().isEmpty()) { + clear(); + return; } - if (unit == Unit::BTC) { - int numDigitsAfterDecimal = result.length() - decimalPosition - 1; - if (numDigitsAfterDecimal < 8) { - result.append(QString(8 - numDigitsAfterDecimal, '0')); - } - result.remove(decimalPosition, 1); - - while (result.startsWith('0') && result.length() > 1) { - result.remove(0, 1); - } - } else if (unit == Unit::SAT) { - result.remove(decimalPosition, 1); - int newDecimalPosition = decimalPosition - 8; - if (newDecimalPosition < 1) { - result = QString("0").repeated(-newDecimalPosition) + result; - newDecimalPosition = 0; - } - result.insert(newDecimalPosition, "."); - - while (result.endsWith('0') && result.contains('.')) { - result.chop(1); - } - if (result.endsWith('.')) { - result.chop(1); - } - if (result.startsWith('.')) { - result.insert(0, "0"); - } + qint64 newSat = 0; + if (m_unit == Unit::BTC) { + QString sanitized = sanitize(text); + newSat = btcToSats(sanitized); + } else { + QString digitsOnly = text; + digitsOnly.remove(QRegExp("[^0-9]")); + newSat = digitsOnly.trimmed().isEmpty() ? 0 : digitsOnly.toLongLong(); } - - return result; + setSatoshi(newSat); } diff --git a/src/qml/bitcoinamount.h b/src/qml/bitcoinamount.h index 0631a05b87..940f5376df 100644 --- a/src/qml/bitcoinamount.h +++ b/src/qml/bitcoinamount.h @@ -1,4 +1,4 @@ -// Copyright (c) 2024 The Bitcoin Core developers +// Copyright (c) 2024-2025 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -15,8 +15,8 @@ class BitcoinAmount : public QObject Q_OBJECT Q_PROPERTY(Unit unit READ unit WRITE setUnit NOTIFY unitChanged) Q_PROPERTY(QString unitLabel READ unitLabel NOTIFY unitChanged) - Q_PROPERTY(QString amount READ amount WRITE setAmount NOTIFY amountChanged) - Q_PROPERTY(QString satoshiAmount READ satoshiAmount NOTIFY amountChanged) + Q_PROPERTY(QString display READ toDisplay WRITE fromDisplay NOTIFY amountChanged) + Q_PROPERTY(qint64 satoshi READ satoshi WRITE setSatoshi NOTIFY amountChanged) public: enum class Unit { @@ -30,27 +30,28 @@ class BitcoinAmount : public QObject Unit unit() const; void setUnit(Unit unit); QString unitLabel() const; - QString amount() const; - void setAmount(const QString& new_amount); - QString satoshiAmount() const; + + QString toDisplay() const; + void fromDisplay(const QString& new_amount); + qint64 satoshi() const; + void setSatoshi(qint64 new_amount); public Q_SLOTS: - QString sanitize(const QString& text); - QString convert(const QString& text, Unit unit) const; - QString toSatoshis(const QString& text) const; + void flipUnit(); + void clear(); Q_SIGNALS: void unitChanged(); - void unitLabelChanged(); void amountChanged(); private: - long long toSatoshis(QString &amount, const Unit unit); - int decimals(Unit unit); + QString sanitize(const QString& text); + static QString satsToBtc(qint64 sat); + static qint64 btcToSats(const QString& btc); - Unit m_unit; - QString m_unitLabel; - QString m_amount; + qint64 m_satoshi{0}; + bool m_isSet{false}; + Unit m_unit{Unit::BTC}; }; #endif // BITCOIN_QML_BITCOINAMOUNT_H diff --git a/src/qml/models/sendrecipient.cpp b/src/qml/models/sendrecipient.cpp index 138bea6559..6ac197a918 100644 --- a/src/qml/models/sendrecipient.cpp +++ b/src/qml/models/sendrecipient.cpp @@ -3,10 +3,11 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include -#include + +#include SendRecipient::SendRecipient(QObject* parent) - : QObject(parent), m_address(""), m_label(""), m_amount(""), m_message("") + : QObject(parent), m_amount(new BitcoinAmount(this)) { } @@ -36,19 +37,11 @@ void SendRecipient::setLabel(const QString& label) } } -QString SendRecipient::amount() const +BitcoinAmount* SendRecipient::amount() const { return m_amount; } -void SendRecipient::setAmount(const QString& amount) -{ - if (m_amount != amount) { - m_amount = amount; - Q_EMIT amountChanged(); - } -} - QString SendRecipient::message() const { return m_message; @@ -69,22 +62,17 @@ bool SendRecipient::subtractFeeFromAmount() const CAmount SendRecipient::cAmount() const { - // TODO: Figure out who owns the parsing of SendRecipient::amount to CAmount - if (m_amount == "") { - return 0; - } - return m_amount.toLongLong(); + return m_amount->satoshi(); } void SendRecipient::clear() { m_address = ""; m_label = ""; - m_amount = ""; + m_amount->setSatoshi(0); m_message = ""; m_subtractFeeFromAmount = false; Q_EMIT addressChanged(); Q_EMIT labelChanged(); - Q_EMIT amountChanged(); Q_EMIT messageChanged(); } diff --git a/src/qml/models/sendrecipient.h b/src/qml/models/sendrecipient.h index 042e97c9de..80af868a1c 100644 --- a/src/qml/models/sendrecipient.h +++ b/src/qml/models/sendrecipient.h @@ -5,17 +5,18 @@ #ifndef BITCOIN_QML_MODELS_SENDRECIPIENT_H #define BITCOIN_QML_MODELS_SENDRECIPIENT_H +#include + #include #include -#include class SendRecipient : public QObject { Q_OBJECT Q_PROPERTY(QString address READ address WRITE setAddress NOTIFY addressChanged) Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged) - Q_PROPERTY(QString amount READ amount WRITE setAmount NOTIFY amountChanged) Q_PROPERTY(QString message READ message WRITE setMessage NOTIFY messageChanged) + Q_PROPERTY(BitcoinAmount* amount READ amount CONSTANT) public: explicit SendRecipient(QObject* parent = nullptr); @@ -26,7 +27,7 @@ class SendRecipient : public QObject QString label() const; void setLabel(const QString& label); - QString amount() const; + BitcoinAmount* amount() const; void setAmount(const QString& amount); QString message() const; @@ -41,14 +42,13 @@ class SendRecipient : public QObject Q_SIGNALS: void addressChanged(); void labelChanged(); - void amountChanged(); void messageChanged(); private: - QString m_address; - QString m_label; - QString m_amount; - QString m_message; + QString m_address{""}; + QString m_label{""}; + QString m_message{""}; + BitcoinAmount* m_amount; bool m_subtractFeeFromAmount{false}; }; diff --git a/src/qml/models/sendrecipientslistmodel.cpp b/src/qml/models/sendrecipientslistmodel.cpp index 7ffa938fd9..68bd16fadc 100644 --- a/src/qml/models/sendrecipientslistmodel.cpp +++ b/src/qml/models/sendrecipientslistmodel.cpp @@ -27,7 +27,7 @@ QVariant SendRecipientsListModel::data(const QModelIndex& index, int role) const switch (role) { case AddressRole: return r->address(); case LabelRole: return r->label(); - case AmountRole: return r->amount(); + case AmountRole: return r->amount()->toDisplay(); case MessageRole: return r->message(); default: return {}; } diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml index 5655888b5c..b3c8179d3f 100644 --- a/src/qml/pages/wallet/Send.qml +++ b/src/qml/pages/wallet/Send.qml @@ -40,6 +40,7 @@ PageStack { clip: true width: parent.width height: parent.height + contentWidth: width ColumnLayout { @@ -166,10 +167,6 @@ PageStack { } Item { - BitcoinAmount { - id: bitcoinAmount - } - height: amountInput.height Layout.fillWidth: true CoreText { @@ -195,10 +192,8 @@ PageStack { background: Item {} placeholderText: "0.00000000" selectByMouse: true - onTextEdited: { - amountInput.text = bitcoinAmount.amount = bitcoinAmount.sanitize(amountInput.text) - root.recipient.amount = bitcoinAmount.satoshiAmount - } + text: root.recipient.amount.display + onEditingFinished: root.recipient.amount.display = text } Item { width: unitLabel.width + flipIcon.width @@ -208,20 +203,15 @@ PageStack { MouseArea { anchors.fill: parent onClicked: { - if (bitcoinAmount.unit == BitcoinAmount.BTC) { - amountInput.text = bitcoinAmount.convert(amountInput.text, BitcoinAmount.BTC) - bitcoinAmount.unit = BitcoinAmount.SAT - } else { - amountInput.text = bitcoinAmount.convert(amountInput.text, BitcoinAmount.SAT) - bitcoinAmount.unit = BitcoinAmount.BTC - } + root.recipient.amount.display = amountInput.text + root.recipient.amount.flipUnit() } } CoreText { id: unitLabel anchors.right: flipIcon.left anchors.verticalCenter: parent.verticalCenter - text: bitcoinAmount.unitLabel + text: root.recipient.amount.unitLabel font.pixelSize: 18 color: enabled ? Theme.color.neutral7 : Theme.color.neutral4 } From 0872c2eab3c6e0421aeff79d836140ecb5eb7240 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Sat, 31 May 2025 11:01:25 -0400 Subject: [PATCH 11/21] qml: Add total calculation to SendRecipientsListModel --- src/qml/bitcoinamount.cpp | 4 +-- src/qml/bitcoinamount.h | 3 ++- src/qml/models/coinslistmodel.cpp | 4 +-- src/qml/models/sendrecipientslistmodel.cpp | 26 +++++++++++++++++--- src/qml/models/sendrecipientslistmodel.h | 7 ++++++ src/qml/models/walletqmlmodel.cpp | 2 +- src/qml/models/walletqmlmodeltransaction.cpp | 5 ++-- src/qml/models/walletqmlmodeltransaction.h | 10 +++----- 8 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/qml/bitcoinamount.cpp b/src/qml/bitcoinamount.cpp index 13c00bf5de..eca63bfbf6 100644 --- a/src/qml/bitcoinamount.cpp +++ b/src/qml/bitcoinamount.cpp @@ -88,7 +88,7 @@ void BitcoinAmount::flipUnit() Q_EMIT amountChanged(); } -QString BitcoinAmount::satsToBtc(qint64 sat) +QString BitcoinAmount::satsToBtcString(qint64 sat) { const bool negative = sat < 0; qint64 absSat = negative ? -sat : sat; @@ -112,7 +112,7 @@ QString BitcoinAmount::toDisplay() const if (m_unit == Unit::SAT) { return QString::number(m_satoshi); } else { - return satsToBtc(m_satoshi); + return satsToBtcString(m_satoshi); } } diff --git a/src/qml/bitcoinamount.h b/src/qml/bitcoinamount.h index 940f5376df..d277d8640f 100644 --- a/src/qml/bitcoinamount.h +++ b/src/qml/bitcoinamount.h @@ -36,6 +36,8 @@ class BitcoinAmount : public QObject qint64 satoshi() const; void setSatoshi(qint64 new_amount); + static QString satsToBtcString(qint64 sat); + public Q_SLOTS: void flipUnit(); void clear(); @@ -46,7 +48,6 @@ public Q_SLOTS: private: QString sanitize(const QString& text); - static QString satsToBtc(qint64 sat); static qint64 btcToSats(const QString& btc); qint64 m_satoshi{0}; diff --git a/src/qml/models/coinslistmodel.cpp b/src/qml/models/coinslistmodel.cpp index 76142e74f3..a34449111f 100644 --- a/src/qml/models/coinslistmodel.cpp +++ b/src/qml/models/coinslistmodel.cpp @@ -122,14 +122,14 @@ QString CoinsListModel::totalSelected() const QString CoinsListModel::changeAmount() const { - CAmount change = m_total_amount - m_wallet_model->sendRecipient()->cAmount(); + CAmount change = m_total_amount - m_wallet_model->sendRecipientList()->totalAmountSatoshi(); change = std::abs(change); return BitcoinUnits::format(BitcoinUnits::Unit::BTC, change); } bool CoinsListModel::overRequiredAmount() const { - return m_total_amount > m_wallet_model->sendRecipient()->cAmount(); + return m_total_amount > m_wallet_model->sendRecipientList()->totalAmountSatoshi(); } int CoinsListModel::coinCount() const diff --git a/src/qml/models/sendrecipientslistmodel.cpp b/src/qml/models/sendrecipientslistmodel.cpp index 68bd16fadc..f1f2659cff 100644 --- a/src/qml/models/sendrecipientslistmodel.cpp +++ b/src/qml/models/sendrecipientslistmodel.cpp @@ -3,14 +3,16 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include -#include #include SendRecipientsListModel::SendRecipientsListModel(QObject* parent) : QAbstractListModel(parent) { - m_recipients.append(new SendRecipient(this)); + auto* recipient = new SendRecipient(this); + connect(recipient->amount(), &BitcoinAmount::amountChanged, + this, &SendRecipientsListModel::updateTotalAmount); + m_recipients.append(recipient); } int SendRecipientsListModel::rowCount(const QModelIndex&) const @@ -48,7 +50,10 @@ void SendRecipientsListModel::add() { const int row = m_recipients.size(); beginInsertRows(QModelIndex(), row, row); - m_recipients.append(new SendRecipient(this)); + auto* recipient = new SendRecipient(this); + connect(recipient->amount(), &BitcoinAmount::amountChanged, + this, &SendRecipientsListModel::updateTotalAmount); + m_recipients.append(recipient); endInsertRows(); Q_EMIT countChanged(); setCurrentIndex(row); @@ -98,3 +103,18 @@ SendRecipient* SendRecipientsListModel::currentRecipient() const return m_recipients[m_current]; } + +void SendRecipientsListModel::updateTotalAmount() +{ + qint64 total = 0; + for (const auto& recipient : m_recipients) { + total += recipient->amount()->satoshi(); + } + m_totalAmount = total; + Q_EMIT totalAmountChanged(); +} + +QString SendRecipientsListModel::totalAmount() const +{ + return BitcoinAmount::satsToBtcString(m_totalAmount); +} diff --git a/src/qml/models/sendrecipientslistmodel.h b/src/qml/models/sendrecipientslistmodel.h index f7ed1cef4b..651b2d7e12 100644 --- a/src/qml/models/sendrecipientslistmodel.h +++ b/src/qml/models/sendrecipientslistmodel.h @@ -17,6 +17,7 @@ class SendRecipientsListModel : public QAbstractListModel Q_PROPERTY(int currentIndex READ currentIndex NOTIFY currentIndexChanged) Q_PROPERTY(int count READ count NOTIFY countChanged) Q_PROPERTY(SendRecipient* current READ currentRecipient NOTIFY currentRecipientChanged) + Q_PROPERTY(QString totalAmount READ totalAmount NOTIFY totalAmountChanged) public: enum Roles { @@ -42,15 +43,21 @@ class SendRecipientsListModel : public QAbstractListModel SendRecipient* currentRecipient() const; int count() const { return m_recipients.size(); } QList recipients() const { return m_recipients; } + QString totalAmount() const; + qint64 totalAmountSatoshi() const { return m_totalAmount; } Q_SIGNALS: void currentIndexChanged(); void currentRecipientChanged(); void countChanged(); + void totalAmountChanged(); private: + void updateTotalAmount(); + QList m_recipients; int m_current{0}; + qint64 m_totalAmount{0}; }; #endif // BITCOIN_QML_MODELS_SENDRECIPIENTSLISTMODEL_H diff --git a/src/qml/models/walletqmlmodel.cpp b/src/qml/models/walletqmlmodel.cpp index 45a4f44779..45c56bbaf4 100644 --- a/src/qml/models/walletqmlmodel.cpp +++ b/src/qml/models/walletqmlmodel.cpp @@ -130,7 +130,7 @@ bool WalletQmlModel::prepareTransaction() delete m_current_transaction; } CTransactionRef newTx = *res; - m_current_transaction = new WalletQmlModelTransaction(m_current_recipient, this); + m_current_transaction = new WalletQmlModelTransaction(m_send_recipients, this); m_current_transaction->setWtx(newTx); m_current_transaction->setTransactionFee(nFeeRequired); Q_EMIT currentTransactionChanged(); diff --git a/src/qml/models/walletqmlmodeltransaction.cpp b/src/qml/models/walletqmlmodeltransaction.cpp index 199103377a..2303606a0e 100644 --- a/src/qml/models/walletqmlmodeltransaction.cpp +++ b/src/qml/models/walletqmlmodeltransaction.cpp @@ -5,10 +5,9 @@ #include #include -#include -WalletQmlModelTransaction::WalletQmlModelTransaction(const SendRecipient* recipient, QObject* parent) - : QObject(parent), m_address(recipient->address()), m_amount(recipient->cAmount()), m_fee(0), m_label(recipient->label()), m_wtx(nullptr) +WalletQmlModelTransaction::WalletQmlModelTransaction(const SendRecipientsListModel* recipient, QObject* parent) + : QObject(parent), m_address(recipient->recipients().at(0)->address()), m_amount(recipient->totalAmountSatoshi()), m_fee(0), m_label(recipient->recipients().at(0)->label()), m_wtx(nullptr) { } diff --git a/src/qml/models/walletqmlmodeltransaction.h b/src/qml/models/walletqmlmodeltransaction.h index 7bf914e06a..35112249de 100644 --- a/src/qml/models/walletqmlmodeltransaction.h +++ b/src/qml/models/walletqmlmodeltransaction.h @@ -5,12 +5,10 @@ #ifndef BITCOIN_QML_MODELS_WALLETQMLMODELTRANSACTION_H #define BITCOIN_QML_MODELS_WALLETQMLMODELTRANSACTION_H -#include -#include +#include #include - -#include +#include class WalletQmlModelTransaction : public QObject @@ -22,7 +20,7 @@ class WalletQmlModelTransaction : public QObject Q_PROPERTY(QString fee READ fee NOTIFY feeChanged) Q_PROPERTY(QString total READ total NOTIFY totalChanged) public: - explicit WalletQmlModelTransaction(const SendRecipient* recipient, QObject* parent = nullptr); + explicit WalletQmlModelTransaction(const SendRecipientsListModel* recipient, QObject* parent = nullptr); QString address() const; QString amount() const; @@ -30,8 +28,6 @@ class WalletQmlModelTransaction : public QObject QString label() const; QString total() const; - QList getRecipients() const; - CTransactionRef& getWtx(); void setWtx(const CTransactionRef&); From 5dc69cd18f1d167330c898bdd137e46ae135a6d2 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Sat, 31 May 2025 11:01:52 -0400 Subject: [PATCH 12/21] qml: Commit Recipient amount when active focus is lost --- src/qml/pages/wallet/Send.qml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml index b3c8179d3f..88820c811a 100644 --- a/src/qml/pages/wallet/Send.qml +++ b/src/qml/pages/wallet/Send.qml @@ -194,6 +194,11 @@ PageStack { selectByMouse: true text: root.recipient.amount.display onEditingFinished: root.recipient.amount.display = text + onActiveFocusChanged: { + if (!activeFocus) { + root.recipient.amount.display = text + } + } } Item { width: unitLabel.width + flipIcon.width @@ -202,10 +207,7 @@ PageStack { anchors.verticalCenter: parent.verticalCenter MouseArea { anchors.fill: parent - onClicked: { - root.recipient.amount.display = amountInput.text - root.recipient.amount.flipUnit() - } + onClicked: root.recipient.amount.flipUnit() } CoreText { id: unitLabel From 2dd6604c36a13ef1287bad16ee3b04d43d74c46b Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Sat, 31 May 2025 13:57:21 -0400 Subject: [PATCH 13/21] qml: Clear Send form after sending transaciton --- src/qml/models/sendrecipient.cpp | 1 + src/qml/models/sendrecipientslistmodel.cpp | 22 ++++++++++++++++++++++ src/qml/models/sendrecipientslistmodel.h | 1 + src/qml/models/walletqmlmodel.cpp | 5 +---- src/qml/models/walletqmlmodel.h | 3 --- src/qml/pages/main.qml | 4 ++-- 6 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/qml/models/sendrecipient.cpp b/src/qml/models/sendrecipient.cpp index 6ac197a918..ce4943d099 100644 --- a/src/qml/models/sendrecipient.cpp +++ b/src/qml/models/sendrecipient.cpp @@ -75,4 +75,5 @@ void SendRecipient::clear() Q_EMIT addressChanged(); Q_EMIT labelChanged(); Q_EMIT messageChanged(); + Q_EMIT amount()->amountChanged(); } diff --git a/src/qml/models/sendrecipientslistmodel.cpp b/src/qml/models/sendrecipientslistmodel.cpp index f1f2659cff..f78fccad80 100644 --- a/src/qml/models/sendrecipientslistmodel.cpp +++ b/src/qml/models/sendrecipientslistmodel.cpp @@ -118,3 +118,25 @@ QString SendRecipientsListModel::totalAmount() const { return BitcoinAmount::satsToBtcString(m_totalAmount); } + +void SendRecipientsListModel::clear() +{ + beginResetModel(); + for (auto* recipient : m_recipients) { + delete recipient; + } + m_recipients.clear(); + m_current = 0; + m_totalAmount = 0; + + auto* recipient = new SendRecipient(this); + connect(recipient->amount(), &BitcoinAmount::amountChanged, + this, &SendRecipientsListModel::updateTotalAmount); + m_recipients.append(recipient); + endResetModel(); + + Q_EMIT countChanged(); + Q_EMIT totalAmountChanged(); + Q_EMIT currentRecipientChanged(); + Q_EMIT currentIndexChanged(); +} diff --git a/src/qml/models/sendrecipientslistmodel.h b/src/qml/models/sendrecipientslistmodel.h index 651b2d7e12..a8ec1c8b3d 100644 --- a/src/qml/models/sendrecipientslistmodel.h +++ b/src/qml/models/sendrecipientslistmodel.h @@ -37,6 +37,7 @@ class SendRecipientsListModel : public QAbstractListModel Q_INVOKABLE void next(); Q_INVOKABLE void prev(); Q_INVOKABLE void remove(); + Q_INVOKABLE void clear(); int currentIndex() const { return m_current + 1; } void setCurrentIndex(int row); diff --git a/src/qml/models/walletqmlmodel.cpp b/src/qml/models/walletqmlmodel.cpp index 45c56bbaf4..22162cd62d 100644 --- a/src/qml/models/walletqmlmodel.cpp +++ b/src/qml/models/walletqmlmodel.cpp @@ -26,7 +26,6 @@ WalletQmlModel::WalletQmlModel(std::unique_ptr wallet, QObje m_wallet = std::move(wallet); m_activity_list_model = new ActivityListModel(this); m_coins_list_model = new CoinsListModel(this); - m_current_recipient = new SendRecipient(this); m_send_recipients = new SendRecipientsListModel(this); } @@ -35,7 +34,6 @@ WalletQmlModel::WalletQmlModel(QObject* parent) { m_activity_list_model = new ActivityListModel(this); m_coins_list_model = new CoinsListModel(this); - m_current_recipient = new SendRecipient(this); m_send_recipients = new SendRecipientsListModel(this); } @@ -43,7 +41,6 @@ WalletQmlModel::~WalletQmlModel() { delete m_activity_list_model; delete m_coins_list_model; - delete m_current_recipient; delete m_send_recipients; if (m_current_transaction) { delete m_current_transaction; @@ -103,7 +100,7 @@ std::unique_ptr WalletQmlModel::handleTransactionChanged(Tr bool WalletQmlModel::prepareTransaction() { - if (!m_wallet || !m_current_recipient) { + if (!m_wallet || !m_send_recipients || m_send_recipients->recipients().empty()) { return false; } diff --git a/src/qml/models/walletqmlmodel.h b/src/qml/models/walletqmlmodel.h index fbaf0f8ffe..de8c5540ac 100644 --- a/src/qml/models/walletqmlmodel.h +++ b/src/qml/models/walletqmlmodel.h @@ -27,7 +27,6 @@ class WalletQmlModel : public QObject Q_PROPERTY(QString balance READ balance NOTIFY balanceChanged) Q_PROPERTY(ActivityListModel* activityListModel READ activityListModel CONSTANT) Q_PROPERTY(CoinsListModel* coinsListModel READ coinsListModel CONSTANT) - Q_PROPERTY(SendRecipient* sendRecipient READ sendRecipient CONSTANT) Q_PROPERTY(SendRecipientsListModel* recipients READ sendRecipientList CONSTANT) Q_PROPERTY(WalletQmlModelTransaction* currentTransaction READ currentTransaction NOTIFY currentTransactionChanged) @@ -40,7 +39,6 @@ class WalletQmlModel : public QObject QString balance() const; ActivityListModel* activityListModel() const { return m_activity_list_model; } CoinsListModel* coinsListModel() const { return m_coins_list_model; } - SendRecipient* sendRecipient() const { return m_send_recipients->currentRecipient(); } SendRecipientsListModel* sendRecipientList() const { return m_send_recipients; } WalletQmlModelTransaction* currentTransaction() const { return m_current_transaction; } Q_INVOKABLE bool prepareTransaction(); @@ -76,7 +74,6 @@ class WalletQmlModel : public QObject ActivityListModel* m_activity_list_model{nullptr}; CoinsListModel* m_coins_list_model{nullptr}; SendRecipientsListModel* m_send_recipients{nullptr}; - SendRecipient* m_current_recipient{nullptr}; WalletQmlModelTransaction* m_current_transaction{nullptr}; wallet::CCoinControl m_coin_control; }; diff --git a/src/qml/pages/main.qml b/src/qml/pages/main.qml index 7b606de4a7..710e2c3bae 100644 --- a/src/qml/pages/main.qml +++ b/src/qml/pages/main.qml @@ -110,7 +110,7 @@ ApplicationWindow { main.pop() } onTransactionSent: { - walletController.selectedWallet.sendRecipient.clear() + walletController.selectedWallet.recipients.clear() main.pop() sendResult.open() } @@ -124,7 +124,7 @@ ApplicationWindow { main.pop() } onTransactionSent: { - walletController.selectedWallet.sendRecipient.clear() + walletController.selectedWallet.recipients.clear() main.pop() sendResult.open() } From c5937ddd5a26488e05bb06668a0ed413295906bd Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Sun, 1 Jun 2025 01:06:56 -0400 Subject: [PATCH 14/21] qml: Add alert icon --- src/Makefile.qt.include | 1 + src/qml/bitcoin_qml.qrc | 1 + src/qml/res/icons/alert-filled.png | Bin 0 -> 531 bytes src/qml/res/src/alert-filled.svg | 3 +++ 4 files changed, 5 insertions(+) create mode 100644 src/qml/res/icons/alert-filled.png create mode 100644 src/qml/res/src/alert-filled.svg diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 019b2bd15c..b37cad3144 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -357,6 +357,7 @@ QML_RES_FONTS = \ QML_RES_ICONS = \ qml/res/icons/add-wallet-dark.png \ + qml/res/icons/alert-filled.png \ qml/res/icons/arrow-down.png \ qml/res/icons/arrow-up.png \ qml/res/icons/bitcoin-circle.png \ diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc index 6bcd5f0705..a4c5b03d01 100644 --- a/src/qml/bitcoin_qml.qrc +++ b/src/qml/bitcoin_qml.qrc @@ -100,6 +100,7 @@ res/icons/add-wallet-dark.png + res/icons/alert-filled.png res/icons/arrow-down.png res/icons/arrow-up.png res/icons/bitcoin-circle.png diff --git a/src/qml/res/icons/alert-filled.png b/src/qml/res/icons/alert-filled.png new file mode 100644 index 0000000000000000000000000000000000000000..a097bea42c3ef90d31cb4eac664331c5cc7e7778 GIT binary patch literal 531 zcmV+u0_^>XP)pRXg5Gi08EfFLAyc92Hl|8G^kM&A$*pmB$wrOmRp4r+gC`e_~;$P@6 zu8ph=U0e VJ+u^Tw50$5002ovPDHLkV1gYO;+Fsb literal 0 HcmV?d00001 diff --git a/src/qml/res/src/alert-filled.svg b/src/qml/res/src/alert-filled.svg new file mode 100644 index 0000000000..f556ef71ca --- /dev/null +++ b/src/qml/res/src/alert-filled.svg @@ -0,0 +1,3 @@ + + + From 1e81bb677c35aa599547507e44a3a9f7d82eee54 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Sun, 1 Jun 2025 01:13:02 -0400 Subject: [PATCH 15/21] qml: Add validation to Send form address and amount --- src/qml/bitcoinamount.h | 2 + src/qml/models/sendrecipient.cpp | 68 +++++++- src/qml/models/sendrecipient.h | 23 ++- src/qml/models/sendrecipientslistmodel.cpp | 8 +- src/qml/models/sendrecipientslistmodel.h | 8 +- src/qml/models/walletqmlmodel.cpp | 8 + src/qml/models/walletqmlmodel.h | 11 +- src/qml/models/walletqmlmodeltransaction.cpp | 3 + src/qml/pages/wallet/Send.qml | 160 ++++++++++++------- test/lint/lint-circular-dependencies.py | 4 + 10 files changed, 225 insertions(+), 70 deletions(-) diff --git a/src/qml/bitcoinamount.h b/src/qml/bitcoinamount.h index d277d8640f..df92d498ee 100644 --- a/src/qml/bitcoinamount.h +++ b/src/qml/bitcoinamount.h @@ -36,6 +36,8 @@ class BitcoinAmount : public QObject qint64 satoshi() const; void setSatoshi(qint64 new_amount); + bool isSet() const { return m_isSet; } + static QString satsToBtcString(qint64 sat); public Q_SLOTS: diff --git a/src/qml/models/sendrecipient.cpp b/src/qml/models/sendrecipient.cpp index ce4943d099..f9b028c59f 100644 --- a/src/qml/models/sendrecipient.cpp +++ b/src/qml/models/sendrecipient.cpp @@ -5,10 +5,14 @@ #include #include +#include -SendRecipient::SendRecipient(QObject* parent) - : QObject(parent), m_amount(new BitcoinAmount(this)) +#include + +SendRecipient::SendRecipient(WalletQmlModel* wallet, QObject* parent) + : QObject(parent), m_wallet(wallet), m_amount(new BitcoinAmount(this)) { + connect(m_amount, &BitcoinAmount::amountChanged, this, &SendRecipient::validateAmount); } QString SendRecipient::address() const @@ -21,6 +25,20 @@ void SendRecipient::setAddress(const QString& address) if (m_address != address) { m_address = address; Q_EMIT addressChanged(); + validateAddress(); + } +} + +QString SendRecipient::addressError() const +{ + return m_addressError; +} + +void SendRecipient::setAddressError(const QString& error) +{ + if (m_addressError != error) { + m_addressError = error; + Q_EMIT addressErrorChanged(); } } @@ -42,6 +60,19 @@ BitcoinAmount* SendRecipient::amount() const return m_amount; } +QString SendRecipient::amountError() const +{ + return m_amountError; +} + +void SendRecipient::setAmountError(const QString& error) +{ + if (m_amountError != error) { + m_amountError = error; + Q_EMIT amountErrorChanged(); + } +} + QString SendRecipient::message() const { return m_message; @@ -77,3 +108,36 @@ void SendRecipient::clear() Q_EMIT messageChanged(); Q_EMIT amount()->amountChanged(); } + +void SendRecipient::validateAddress() +{ + setAddressError(""); + + if (!m_address.isEmpty() && !IsValidDestinationString(m_address.toStdString())) { + setAddressError(tr("Invalid address")); + } + + Q_EMIT isValidChanged(); +} + +void SendRecipient::validateAmount() +{ + setAmountError(""); + + if (m_amount->isSet()) { + if (m_amount->satoshi() <= 0) { + setAmountError(tr("Amount must be greater than zero")); + } else if (m_amount->satoshi() > MAX_MONEY) { + setAmountError(tr("Amount exceeds maximum limit")); + } else if (m_amount->satoshi() > m_wallet->balanceSatoshi()) { + setAmountError(tr("Amount exceeds available balance")); + } + } + + Q_EMIT isValidChanged(); +} + +bool SendRecipient::isValid() const +{ + return m_addressError.isEmpty() && m_amountError.isEmpty() && m_amount->satoshi() > 0 && !m_address.isEmpty(); +} diff --git a/src/qml/models/sendrecipient.h b/src/qml/models/sendrecipient.h index 80af868a1c..965483c854 100644 --- a/src/qml/models/sendrecipient.h +++ b/src/qml/models/sendrecipient.h @@ -10,6 +10,8 @@ #include #include +class WalletQmlModel; + class SendRecipient : public QObject { Q_OBJECT @@ -18,17 +20,25 @@ class SendRecipient : public QObject Q_PROPERTY(QString message READ message WRITE setMessage NOTIFY messageChanged) Q_PROPERTY(BitcoinAmount* amount READ amount CONSTANT) + Q_PROPERTY(QString addressError READ addressError NOTIFY addressErrorChanged) + Q_PROPERTY(QString amountError READ amountError NOTIFY amountErrorChanged) + Q_PROPERTY(bool isValid READ isValid NOTIFY isValidChanged) + public: - explicit SendRecipient(QObject* parent = nullptr); + explicit SendRecipient(WalletQmlModel* wallet, QObject* parent = nullptr); QString address() const; void setAddress(const QString& address); + QString addressError() const; + void setAddressError(const QString& error); QString label() const; void setLabel(const QString& label); BitcoinAmount* amount() const; void setAmount(const QString& amount); + QString amountError() const; + void setAmountError(const QString& error); QString message() const; void setMessage(const QString& message); @@ -37,18 +47,29 @@ class SendRecipient : public QObject bool subtractFeeFromAmount() const; + bool isValid() const; + Q_INVOKABLE void clear(); Q_SIGNALS: void addressChanged(); + void addressErrorChanged(); + void amountErrorChanged(); void labelChanged(); void messageChanged(); + void isValidChanged(); private: + void validateAddress(); + void validateAmount(); + + WalletQmlModel* m_wallet; QString m_address{""}; + QString m_addressError{""}; QString m_label{""}; QString m_message{""}; BitcoinAmount* m_amount; + QString m_amountError{""}; bool m_subtractFeeFromAmount{false}; }; diff --git a/src/qml/models/sendrecipientslistmodel.cpp b/src/qml/models/sendrecipientslistmodel.cpp index f78fccad80..d32aa4c82c 100644 --- a/src/qml/models/sendrecipientslistmodel.cpp +++ b/src/qml/models/sendrecipientslistmodel.cpp @@ -3,13 +3,15 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include SendRecipientsListModel::SendRecipientsListModel(QObject* parent) : QAbstractListModel(parent) { - auto* recipient = new SendRecipient(this); + m_wallet = qobject_cast(parent); + auto* recipient = new SendRecipient(m_wallet, this); connect(recipient->amount(), &BitcoinAmount::amountChanged, this, &SendRecipientsListModel::updateTotalAmount); m_recipients.append(recipient); @@ -50,7 +52,7 @@ void SendRecipientsListModel::add() { const int row = m_recipients.size(); beginInsertRows(QModelIndex(), row, row); - auto* recipient = new SendRecipient(this); + auto* recipient = new SendRecipient(m_wallet, this); connect(recipient->amount(), &BitcoinAmount::amountChanged, this, &SendRecipientsListModel::updateTotalAmount); m_recipients.append(recipient); @@ -129,7 +131,7 @@ void SendRecipientsListModel::clear() m_current = 0; m_totalAmount = 0; - auto* recipient = new SendRecipient(this); + auto* recipient = new SendRecipient(m_wallet, this); connect(recipient->amount(), &BitcoinAmount::amountChanged, this, &SendRecipientsListModel::updateTotalAmount); m_recipients.append(recipient); diff --git a/src/qml/models/sendrecipientslistmodel.h b/src/qml/models/sendrecipientslistmodel.h index a8ec1c8b3d..2e9f2f7643 100644 --- a/src/qml/models/sendrecipientslistmodel.h +++ b/src/qml/models/sendrecipientslistmodel.h @@ -5,11 +5,10 @@ #ifndef BITCOIN_QML_MODELS_SENDRECIPIENTSLISTMODEL_H #define BITCOIN_QML_MODELS_SENDRECIPIENTSLISTMODEL_H -#include - #include -#include -#include + +class SendRecipient; +class WalletQmlModel; class SendRecipientsListModel : public QAbstractListModel { @@ -56,6 +55,7 @@ class SendRecipientsListModel : public QAbstractListModel private: void updateTotalAmount(); + WalletQmlModel* m_wallet; QList m_recipients; int m_current{0}; qint64 m_totalAmount{0}; diff --git a/src/qml/models/walletqmlmodel.cpp b/src/qml/models/walletqmlmodel.cpp index 22162cd62d..dcbbd26d37 100644 --- a/src/qml/models/walletqmlmodel.cpp +++ b/src/qml/models/walletqmlmodel.cpp @@ -55,6 +55,14 @@ QString WalletQmlModel::balance() const return BitcoinUnits::format(BitcoinUnits::Unit::BTC, m_wallet->getBalance()); } +CAmount WalletQmlModel::balanceSatoshi() const +{ + if (!m_wallet) { + return 0; + } + return m_wallet->getBalance(); +} + QString WalletQmlModel::name() const { if (!m_wallet) { diff --git a/src/qml/models/walletqmlmodel.h b/src/qml/models/walletqmlmodel.h index de8c5540ac..6c8e35d09b 100644 --- a/src/qml/models/walletqmlmodel.h +++ b/src/qml/models/walletqmlmodel.h @@ -5,20 +5,21 @@ #ifndef BITCOIN_QML_MODELS_WALLETQMLMODEL_H #define BITCOIN_QML_MODELS_WALLETQMLMODEL_H -#include -#include #include #include #include #include #include + +#include +#include +#include #include -#include #include #include -class ActivityListModel; +#include class WalletQmlModel : public QObject { @@ -37,6 +38,8 @@ class WalletQmlModel : public QObject QString name() const; QString balance() const; + CAmount balanceSatoshi() const; + ActivityListModel* activityListModel() const { return m_activity_list_model; } CoinsListModel* coinsListModel() const { return m_coins_list_model; } SendRecipientsListModel* sendRecipientList() const { return m_send_recipients; } diff --git a/src/qml/models/walletqmlmodeltransaction.cpp b/src/qml/models/walletqmlmodeltransaction.cpp index 2303606a0e..11cc34d08c 100644 --- a/src/qml/models/walletqmlmodeltransaction.cpp +++ b/src/qml/models/walletqmlmodeltransaction.cpp @@ -4,6 +4,9 @@ #include +#include +#include + #include WalletQmlModelTransaction::WalletQmlModelTransaction(const SendRecipientsListModel* recipient, QObject* parent) diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml index 88820c811a..c62be466f2 100644 --- a/src/qml/pages/wallet/Send.qml +++ b/src/qml/pages/wallet/Send.qml @@ -111,7 +111,6 @@ PageStack { enabled: wallet.recipients.currentIndex - 1 > 0 onClicked: { wallet.recipients.prev() - } } @@ -153,77 +152,125 @@ PageStack { Layout.fillWidth: true } - LabeledTextInput { - id: address + ColumnLayout { Layout.fillWidth: true - labelText: qsTr("Send to") - placeholderText: qsTr("Enter address...") - text: root.recipient.address - onTextEdited: root.recipient.address = address.text + + LabeledTextInput { + id: address + Layout.fillWidth: true + labelText: qsTr("Send to") + placeholderText: qsTr("Enter address...") + text: root.recipient.address + onTextEdited: root.recipient.address = address.text + } + + RowLayout { + id: addressIssue + Layout.fillWidth: true + visible: root.recipient.addressError.length > 0 + + Icon { + source: "image://images/alert-filled" + size: 22 + color: Theme.color.red + } + + CoreText { + id: warningText + text: root.recipient.addressError + font.pixelSize: 15 + color: Theme.color.red + horizontalAlignment: Text.AlignLeft + Layout.fillWidth: true + } + } } Separator { Layout.fillWidth: true } - Item { - height: amountInput.height + ColumnLayout { Layout.fillWidth: true - CoreText { - id: amountLabel - width: 110 - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - horizontalAlignment: Text.AlignLeft - text: qsTr("Amount") - font.pixelSize: 18 - } - TextField { - id: amountInput - anchors.left: amountLabel.right - anchors.verticalCenter: parent.verticalCenter - leftPadding: 0 - font.family: "Inter" - font.styleName: "Regular" - font.pixelSize: 18 - color: Theme.color.neutral9 - placeholderTextColor: enabled ? Theme.color.neutral7 : Theme.color.neutral4 - background: Item {} - placeholderText: "0.00000000" - selectByMouse: true - text: root.recipient.amount.display - onEditingFinished: root.recipient.amount.display = text - onActiveFocusChanged: { - if (!activeFocus) { - root.recipient.amount.display = text - } - } - } Item { - width: unitLabel.width + flipIcon.width - height: Math.max(unitLabel.height, flipIcon.height) - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - MouseArea { - anchors.fill: parent - onClicked: root.recipient.amount.flipUnit() - } + height: amountInput.height + Layout.fillWidth: true CoreText { - id: unitLabel - anchors.right: flipIcon.left + id: amountLabel + width: 110 + anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - text: root.recipient.amount.unitLabel + horizontalAlignment: Text.AlignLeft + text: qsTr("Amount") font.pixelSize: 18 - color: enabled ? Theme.color.neutral7 : Theme.color.neutral4 } - Icon { - id: flipIcon + + TextField { + id: amountInput + anchors.left: amountLabel.right + anchors.verticalCenter: parent.verticalCenter + leftPadding: 0 + font.family: "Inter" + font.styleName: "Regular" + font.pixelSize: 18 + color: Theme.color.neutral9 + placeholderTextColor: enabled ? Theme.color.neutral7 : Theme.color.neutral4 + background: Item {} + placeholderText: "0.00000000" + selectByMouse: true + text: root.recipient.amount.display + onEditingFinished: root.recipient.amount.display = text + onActiveFocusChanged: { + if (!activeFocus) { + root.recipient.amount.display = text + } + } + } + Item { + width: unitLabel.width + flipIcon.width + height: Math.max(unitLabel.height, flipIcon.height) anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - source: "image://images/flip-vertical" - icon.color: unitLabel.enabled ? Theme.color.neutral8 : Theme.color.neutral4 - size: 30 + MouseArea { + anchors.fill: parent + onClicked: root.recipient.amount.flipUnit() + } + CoreText { + id: unitLabel + anchors.right: flipIcon.left + anchors.verticalCenter: parent.verticalCenter + text: root.recipient.amount.unitLabel + font.pixelSize: 18 + color: enabled ? Theme.color.neutral7 : Theme.color.neutral4 + } + Icon { + id: flipIcon + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + source: "image://images/flip-vertical" + icon.color: unitLabel.enabled ? Theme.color.neutral8 : Theme.color.neutral4 + size: 30 + } + } + } + + RowLayout { + Layout.fillWidth: true + visible: root.recipient.amountError.length > 0 + + Icon { + source: "image://images/alert-filled" + size: 22 + color: Theme.color.red + } + + CoreText { + text: root.recipient.amountError + font.pixelSize: 15 + color: Theme.color.red + horizontalAlignment: Text.AlignLeft + Layout.fillWidth: true } } } @@ -285,6 +332,7 @@ PageStack { Layout.fillWidth: true Layout.topMargin: 30 text: qsTr("Review") + enabled: root.recipient.isValid onClicked: { if (root.wallet.prepareTransaction()) { root.transactionPrepared(settings.multipleRecipientsEnabled); diff --git a/test/lint/lint-circular-dependencies.py b/test/lint/lint-circular-dependencies.py index fa98b6fd69..b2b7540d97 100755 --- a/test/lint/lint-circular-dependencies.py +++ b/test/lint/lint-circular-dependencies.py @@ -17,6 +17,10 @@ "node/utxo_snapshot -> validation -> node/utxo_snapshot", "qml/models/activitylistmodel -> qml/models/walletqmlmodel -> qml/models/activitylistmodel", "qml/models/coinslistmodel -> qml/models/walletqmlmodel -> qml/models/coinslistmodel", + "qml/models/sendrecipient -> qml/models/walletqmlmodel -> qml/models/sendrecipient", + "qml/models/sendrecipient -> qml/models/walletqmlmodel -> qml/models/walletqmlmodeltransaction -> qml/models/sendrecipient", + "qml/models/sendrecipientslistmodel -> qml/models/walletqmlmodel -> qml/models/sendrecipientslistmodel", + "qml/models/sendrecipientslistmodel -> qml/models/walletqmlmodel -> qml/models/walletqmlmodeltransaction -> qml/models/sendrecipientslistmodel", "qt/addresstablemodel -> qt/walletmodel -> qt/addresstablemodel", "qt/recentrequeststablemodel -> qt/walletmodel -> qt/recentrequeststablemodel", "qt/sendcoinsdialog -> qt/walletmodel -> qt/sendcoinsdialog", From 1705458fd51467e20e8e01800279721757aa1b6f Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Mon, 2 Jun 2025 00:08:11 -0400 Subject: [PATCH 16/21] qml: Extract BitcoinAmountInputField from Send --- src/Makefile.qt.include | 1 + src/qml/bitcoin_qml.qrc | 1 + .../components/BitcoinAmountInputField.qml | 122 ++++++++++++++++++ src/qml/pages/wallet/Send.qml | 86 +----------- 4 files changed, 128 insertions(+), 82 deletions(-) create mode 100644 src/qml/components/BitcoinAmountInputField.qml diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index b37cad3144..a214f33f77 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -404,6 +404,7 @@ QML_RES_QML = \ qml/components/BlockClock.qml \ qml/components/BlockClockDisplayMode.qml \ qml/components/BlockCounter.qml \ + qml/components/BitcoinAmountInputField.qml \ qml/components/ConnectionOptions.qml \ qml/components/ConnectionSettings.qml \ qml/components/DeveloperOptions.qml \ diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc index a4c5b03d01..88d29bc1c2 100644 --- a/src/qml/bitcoin_qml.qrc +++ b/src/qml/bitcoin_qml.qrc @@ -4,6 +4,7 @@ components/BlockClock.qml components/BlockClockDisplayMode.qml components/BlockCounter.qml + components/BitcoinAmountInputField.qml components/ConnectionOptions.qml components/ConnectionSettings.qml components/DeveloperOptions.qml diff --git a/src/qml/components/BitcoinAmountInputField.qml b/src/qml/components/BitcoinAmountInputField.qml new file mode 100644 index 0000000000..b0130ca2f7 --- /dev/null +++ b/src/qml/components/BitcoinAmountInputField.qml @@ -0,0 +1,122 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import org.bitcoincore.qt 1.0 + +import "../controls" + +ColumnLayout { + id: root + + property var amount + property string errorText: "" + property string labelText: qsTr("Amount") + property bool enabled: true + + signal editingFinished(string value) + + Layout.fillWidth: true + spacing: 4 + + Item { + id: inputRow + height: amountInput.height + Layout.fillWidth: true + + CoreText { + id: lbl + width: 110 + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignLeft + text: root.labelText + font.pixelSize: 18 + } + + TextField { + id: amountInput + anchors.left: lbl.right + anchors.verticalCenter: parent.verticalCenter + leftPadding: 0 + enabled: root.enabled + font.family: "Inter" + font.styleName: "Regular" + font.pixelSize: 18 + color: Theme.color.neutral9 + placeholderTextColor: enabled ? Theme.color.neutral7 : Theme.color.neutral4 + background: Item {} + placeholderText: "0.00000000" + selectByMouse: true + + text: root.amount ? root.amount.display : "" + + onEditingFinished: { + if (root.amount) { + root.amount.display = text + } + root.editingFinished(text) + } + + onActiveFocusChanged: { + if (!activeFocus && root.amount) { + root.amount.display = text + root.editingFinished(text) + } + } + } + + Item { + width: unitLabel.width + flipIcon.width + height: Math.max(unitLabel.height, flipIcon.height) + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + opacity: root.enabled ? 1.0 : 0.5 + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: root.enabled && root.amount + onClicked: root.amount.flipUnit() + } + + CoreText { + id: unitLabel + anchors.right: flipIcon.left + anchors.verticalCenter: parent.verticalCenter + text: root.amount ? root.amount.unitLabel : "" + font.pixelSize: 18 + color: enabled ? Theme.color.neutral7 : Theme.color.neutral4 + } + + Icon { + id: flipIcon + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + source: "image://images/flip-vertical" + icon.color: enabled ? Theme.color.neutral8 : Theme.color.neutral4 + size: 30 + } + } + } + + RowLayout { + id: errorRow + Layout.fillWidth: true + visible: root.errorText.length > 0 + + Icon { + source: "image://images/alert-filled" + size: 22 + color: Theme.color.red + } + + CoreText { + text: root.errorText + font.pixelSize: 15 + color: Theme.color.red + horizontalAlignment: Text.AlignLeft + Layout.fillWidth: true + } + } +} diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml index c62be466f2..eed2d33271 100644 --- a/src/qml/pages/wallet/Send.qml +++ b/src/qml/pages/wallet/Send.qml @@ -190,89 +190,11 @@ PageStack { Layout.fillWidth: true } - ColumnLayout { + BitcoinAmountInputField { Layout.fillWidth: true - - Item { - height: amountInput.height - Layout.fillWidth: true - CoreText { - id: amountLabel - width: 110 - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - horizontalAlignment: Text.AlignLeft - text: qsTr("Amount") - font.pixelSize: 18 - } - - TextField { - id: amountInput - anchors.left: amountLabel.right - anchors.verticalCenter: parent.verticalCenter - leftPadding: 0 - font.family: "Inter" - font.styleName: "Regular" - font.pixelSize: 18 - color: Theme.color.neutral9 - placeholderTextColor: enabled ? Theme.color.neutral7 : Theme.color.neutral4 - background: Item {} - placeholderText: "0.00000000" - selectByMouse: true - text: root.recipient.amount.display - onEditingFinished: root.recipient.amount.display = text - onActiveFocusChanged: { - if (!activeFocus) { - root.recipient.amount.display = text - } - } - } - Item { - width: unitLabel.width + flipIcon.width - height: Math.max(unitLabel.height, flipIcon.height) - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - MouseArea { - anchors.fill: parent - onClicked: root.recipient.amount.flipUnit() - } - CoreText { - id: unitLabel - anchors.right: flipIcon.left - anchors.verticalCenter: parent.verticalCenter - text: root.recipient.amount.unitLabel - font.pixelSize: 18 - color: enabled ? Theme.color.neutral7 : Theme.color.neutral4 - } - Icon { - id: flipIcon - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - source: "image://images/flip-vertical" - icon.color: unitLabel.enabled ? Theme.color.neutral8 : Theme.color.neutral4 - size: 30 - } - } - } - - RowLayout { - Layout.fillWidth: true - visible: root.recipient.amountError.length > 0 - - Icon { - source: "image://images/alert-filled" - size: 22 - color: Theme.color.red - } - - CoreText { - text: root.recipient.amountError - font.pixelSize: 15 - color: Theme.color.red - horizontalAlignment: Text.AlignLeft - Layout.fillWidth: true - } - } + enabled: walletController.initialized + amount: root.recipient.amount + errorText: root.recipient.amountError } Separator { From 03042fb90acf62c3801eeb387be2d351bc75a2ff Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Mon, 2 Jun 2025 23:40:00 -0400 Subject: [PATCH 17/21] qml: Add PaymentRequest to WalletQmlModel --- src/Makefile.qt.include | 3 + src/qml/models/paymentrequest.cpp | 107 ++++++++++++++++++++++++++++++ src/qml/models/paymentrequest.h | 63 ++++++++++++++++++ src/qml/models/walletqmlmodel.cpp | 28 +++++++- src/qml/models/walletqmlmodel.h | 7 ++ 5 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 src/qml/models/paymentrequest.cpp create mode 100644 src/qml/models/paymentrequest.h diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index a214f33f77..47264bb62e 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -43,6 +43,7 @@ QT_MOC_CPP = \ qml/models/moc_networktraffictower.cpp \ qml/models/moc_nodemodel.cpp \ qml/models/moc_options_model.cpp \ + qml/models/moc_paymentrequest.cpp \ qml/models/moc_peerdetailsmodel.cpp \ qml/models/moc_peerlistsortproxy.cpp \\ qml/models/moc_sendrecipient.cpp \ @@ -136,6 +137,7 @@ BITCOIN_QT_H = \ qml/models/networktraffictower.h \ qml/models/nodemodel.h \ qml/models/options_model.h \ + qml/models/paymentrequest.h \ qml/models/peerdetailsmodel.h \ qml/models/peerlistsortproxy.h \ qml/models/transaction.h \ @@ -338,6 +340,7 @@ BITCOIN_QML_BASE_CPP = \ qml/models/networktraffictower.cpp \ qml/models/nodemodel.cpp \ qml/models/options_model.cpp \ + qml/models/paymentrequest.cpp \ qml/models/peerdetailsmodel.cpp \ qml/models/peerlistsortproxy.cpp \ qml/models/transaction.cpp \ diff --git a/src/qml/models/paymentrequest.cpp b/src/qml/models/paymentrequest.cpp new file mode 100644 index 0000000000..a73479546a --- /dev/null +++ b/src/qml/models/paymentrequest.cpp @@ -0,0 +1,107 @@ +// Copyright (c) 2024-2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include + +#include +#include + +PaymentRequest::PaymentRequest(QObject* parent) + : QObject(parent) +{ + m_amount = new BitcoinAmount(this); + m_label = ""; + m_message = ""; + m_id = ""; +} + +QString PaymentRequest::address() const +{ + return QString::fromStdString(EncodeDestination(m_destination)); +} + +QString PaymentRequest::label() const +{ + return m_label; +} + +void PaymentRequest::setLabel(const QString& label) +{ + if (m_label == label) + return; + + m_label = label; + Q_EMIT labelChanged(); +} + +QString PaymentRequest::message() const +{ + return m_message; +} + +void PaymentRequest::setMessage(const QString& message) +{ + if (m_message == message) + return; + + m_message = message; + Q_EMIT messageChanged(); +} + +BitcoinAmount* PaymentRequest::amount() const +{ + return m_amount; +} + +QString PaymentRequest::id() const +{ + return m_id; +} + +void PaymentRequest::setId(const unsigned int id) +{ + m_id = QString::number(id); + Q_EMIT idChanged(); +} + +void PaymentRequest::setDestination(const CTxDestination& destination) +{ + m_destination = destination; + Q_EMIT addressChanged(); +} + +CTxDestination PaymentRequest::destination() const +{ + return m_destination; +} + +void PaymentRequest::setAmountError(const QString& error) +{ + if (m_amountError == error) + return; + + m_amountError = error; + Q_EMIT amountErrorChanged(); +} + +QString PaymentRequest::amountError() const +{ + return m_amountError; +} + +void PaymentRequest::clear() +{ + m_destination = CNoDestination(); + m_label.clear(); + m_message.clear(); + m_amount->clear(); + m_amountError.clear(); + m_id.clear(); + Q_EMIT addressChanged(); + Q_EMIT labelChanged(); + Q_EMIT messageChanged(); + Q_EMIT amountErrorChanged(); + Q_EMIT idChanged(); +} diff --git a/src/qml/models/paymentrequest.h b/src/qml/models/paymentrequest.h new file mode 100644 index 0000000000..aa7e26cd15 --- /dev/null +++ b/src/qml/models/paymentrequest.h @@ -0,0 +1,63 @@ +// Copyright (c) 2024-2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QML_MODELS_PAYMENTREQUEST_H +#define BITCOIN_QML_MODELS_PAYMENTREQUEST_H + +#include + +#include + +#include + +class PaymentRequest : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString address READ address NOTIFY addressChanged) + Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged) + Q_PROPERTY(QString message READ message WRITE setMessage NOTIFY messageChanged) + Q_PROPERTY(BitcoinAmount* amount READ amount CONSTANT) + Q_PROPERTY(QString amountError READ amountError NOTIFY amountErrorChanged) + Q_PROPERTY(QString id READ id NOTIFY idChanged) + +public: + explicit PaymentRequest(QObject* parent = nullptr); + + QString address() const; + + QString label() const; + void setLabel(const QString& label); + + QString message() const; + void setMessage(const QString& message); + + BitcoinAmount* amount() const; + QString amountError() const; + void setAmountError(const QString& error); + + QString id() const; + void setId(unsigned int id); + + void setDestination(const CTxDestination& destination); + CTxDestination destination() const; + + Q_INVOKABLE void clear(); + +Q_SIGNALS: + void addressChanged(); + void labelChanged(); + void messageChanged(); + void amountErrorChanged(); + void idChanged(); + +private: + CTxDestination m_destination; + QString m_label; + QString m_message; + QString m_amountError; + BitcoinAmount* m_amount; + QString m_id; +}; + +#endif // BITCOIN_QML_MODELS_PAYMENTREQUEST_H diff --git a/src/qml/models/walletqmlmodel.cpp b/src/qml/models/walletqmlmodel.cpp index dcbbd26d37..e01f336dcf 100644 --- a/src/qml/models/walletqmlmodel.cpp +++ b/src/qml/models/walletqmlmodel.cpp @@ -1,11 +1,12 @@ -// Copyright (c) 2024 The Bitcoin Core developers +// Copyright (c) 2024-2025 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include +#include #include #include #include @@ -20,6 +21,8 @@ #include +unsigned int WalletQmlModel::m_next_payment_request_id{1}; + WalletQmlModel::WalletQmlModel(std::unique_ptr wallet, QObject *parent) : QObject(parent) { @@ -71,6 +74,29 @@ QString WalletQmlModel::name() const return QString::fromStdString(m_wallet->getWalletName()); } +void WalletQmlModel::commitPaymentRequest() +{ + if (!m_current_payment_request) { + return; + } + + if (m_current_payment_request->id().isEmpty()) { + m_current_payment_request->setId(m_next_payment_request_id++); + } + + if (m_current_payment_request->address().isEmpty()) { + // TODO: handle issues with getting the new address (wallet unlock?) + auto destination = m_wallet->getNewDestination(OutputType::BECH32M, + m_current_payment_request->label().toStdString()) + .value(); + std::string address = EncodeDestination(destination); + m_current_payment_request->setDestination(destination); + } + + m_wallet->setAddressReceiveRequest( + m_current_payment_request->destination(), m_current_payment_request->id().toStdString(), m_current_payment_request->message().toStdString()); +} + std::set WalletQmlModel::getWalletTxs() const { if (!m_wallet) { diff --git a/src/qml/models/walletqmlmodel.h b/src/qml/models/walletqmlmodel.h index 6c8e35d09b..33464fba7c 100644 --- a/src/qml/models/walletqmlmodel.h +++ b/src/qml/models/walletqmlmodel.h @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -30,6 +31,7 @@ class WalletQmlModel : public QObject Q_PROPERTY(CoinsListModel* coinsListModel READ coinsListModel CONSTANT) Q_PROPERTY(SendRecipientsListModel* recipients READ sendRecipientList CONSTANT) Q_PROPERTY(WalletQmlModelTransaction* currentTransaction READ currentTransaction NOTIFY currentTransactionChanged) + Q_PROPERTY(PaymentRequest* currentPaymentRequest READ currentPaymentRequest CONSTANT) public: WalletQmlModel(std::unique_ptr wallet, QObject* parent = nullptr); @@ -39,6 +41,8 @@ class WalletQmlModel : public QObject QString name() const; QString balance() const; CAmount balanceSatoshi() const; + Q_INVOKABLE void commitPaymentRequest(); + PaymentRequest* currentPaymentRequest() const { return m_current_payment_request; } ActivityListModel* activityListModel() const { return m_activity_list_model; } CoinsListModel* coinsListModel() const { return m_coins_list_model; } @@ -73,7 +77,10 @@ class WalletQmlModel : public QObject void currentTransactionChanged(); private: + static unsigned int m_next_payment_request_id; + std::unique_ptr m_wallet; + PaymentRequest* m_current_payment_request{nullptr}; ActivityListModel* m_activity_list_model{nullptr}; CoinsListModel* m_coins_list_model{nullptr}; SendRecipientsListModel* m_send_recipients{nullptr}; From fe25d2b84691e3637d765e4b08b9c0122fb254c1 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Mon, 2 Jun 2025 10:22:57 -0400 Subject: [PATCH 18/21] qml: Connect RequestPayment page to WalletQmlModel --- src/qml/bitcoin.cpp | 2 + src/qml/models/sendrecipient.h | 1 - src/qml/models/walletqmlmodel.cpp | 3 + src/qml/pages/wallet/RequestPayment.qml | 85 ++++--------------------- 4 files changed, 17 insertions(+), 74 deletions(-) diff --git a/src/qml/bitcoin.cpp b/src/qml/bitcoin.cpp index f3f78fac71..b74ab001a3 100644 --- a/src/qml/bitcoin.cpp +++ b/src/qml/bitcoin.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -340,6 +341,7 @@ int QmlGuiMain(int argc, char* argv[]) qmlRegisterType("org.bitcoincore.qt", 1, 0, "LineGraph"); qmlRegisterUncreatableType("org.bitcoincore.qt", 1, 0, "PeerDetailsModel", ""); qmlRegisterType("org.bitcoincore.qt", 1, 0, "BitcoinAmount"); + qmlRegisterType("org.bitcoincore.qt", 1, 0, "PaymentRequest"); qmlRegisterUncreatableType("org.bitcoincore.qt", 1, 0, "Transaction", ""); qmlRegisterUncreatableType("org.bitcoincore.qt", 1, 0, "SendRecipient", ""); diff --git a/src/qml/models/sendrecipient.h b/src/qml/models/sendrecipient.h index 965483c854..5338a0b2cb 100644 --- a/src/qml/models/sendrecipient.h +++ b/src/qml/models/sendrecipient.h @@ -36,7 +36,6 @@ class SendRecipient : public QObject void setLabel(const QString& label); BitcoinAmount* amount() const; - void setAmount(const QString& amount); QString amountError() const; void setAmountError(const QString& error); diff --git a/src/qml/models/walletqmlmodel.cpp b/src/qml/models/walletqmlmodel.cpp index e01f336dcf..ad5e33f227 100644 --- a/src/qml/models/walletqmlmodel.cpp +++ b/src/qml/models/walletqmlmodel.cpp @@ -30,6 +30,7 @@ WalletQmlModel::WalletQmlModel(std::unique_ptr wallet, QObje m_activity_list_model = new ActivityListModel(this); m_coins_list_model = new CoinsListModel(this); m_send_recipients = new SendRecipientsListModel(this); + m_current_payment_request = new PaymentRequest(this); } WalletQmlModel::WalletQmlModel(QObject* parent) @@ -38,6 +39,7 @@ WalletQmlModel::WalletQmlModel(QObject* parent) m_activity_list_model = new ActivityListModel(this); m_coins_list_model = new CoinsListModel(this); m_send_recipients = new SendRecipientsListModel(this); + m_current_payment_request = new PaymentRequest(this); } WalletQmlModel::~WalletQmlModel() @@ -45,6 +47,7 @@ WalletQmlModel::~WalletQmlModel() delete m_activity_list_model; delete m_coins_list_model; delete m_send_recipients; + delete m_current_payment_request; if (m_current_transaction) { delete m_current_transaction; } diff --git a/src/qml/pages/wallet/RequestPayment.qml b/src/qml/pages/wallet/RequestPayment.qml index dc8f9b04ea..e4bfcc9df5 100644 --- a/src/qml/pages/wallet/RequestPayment.qml +++ b/src/qml/pages/wallet/RequestPayment.qml @@ -16,6 +16,8 @@ Page { background: null property int requestCounter: 0 + property WalletQmlModel wallet: walletController.selectedWallet + property PaymentRequest request: wallet.currentPaymentRequest ScrollView { clip: true @@ -49,74 +51,11 @@ Page { spacing: 5 - Item { - BitcoinAmount { - id: bitcoinAmount - } - - height: amountInput.height + BitcoinAmountInputField { Layout.fillWidth: true - CoreText { - id: amountLabel - width: 110 - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - horizontalAlignment: Text.AlignLeft - text: "Amount" - font.pixelSize: 18 - } - - TextField { - id: amountInput - anchors.left: amountLabel.right - anchors.verticalCenter: parent.verticalCenter - leftPadding: 0 - font.family: "Inter" - font.styleName: "Regular" - font.pixelSize: 18 - color: Theme.color.neutral9 - placeholderTextColor: enabled ? Theme.color.neutral7 : Theme.color.neutral4 - background: Item {} - placeholderText: "0.00000000" - selectByMouse: true - onTextEdited: { - amountInput.text = bitcoinAmount.sanitize(amountInput.text) - } - } - Item { - width: unitLabel.width + flipIcon.width - height: Math.max(unitLabel.height, flipIcon.height) - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - MouseArea { - anchors.fill: parent - onClicked: { - if (bitcoinAmount.unit == BitcoinAmount.BTC) { - amountInput.text = bitcoinAmount.convert(amountInput.text, BitcoinAmount.BTC) - bitcoinAmount.unit = BitcoinAmount.SAT - } else { - amountInput.text = bitcoinAmount.convert(amountInput.text, BitcoinAmount.SAT) - bitcoinAmount.unit = BitcoinAmount.BTC - } - } - } - CoreText { - id: unitLabel - anchors.right: flipIcon.left - anchors.verticalCenter: parent.verticalCenter - text: bitcoinAmount.unitLabel - font.pixelSize: 18 - color: enabled ? Theme.color.neutral7 : Theme.color.neutral4 - } - Icon { - id: flipIcon - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - source: "image://images/flip-vertical" - color: unitLabel.enabled ? Theme.color.neutral8 : Theme.color.neutral4 - size: 30 - } - } + enabled: walletController.initialized + amount: root.request.amount + errorText: root.request.amountError } Separator { @@ -147,7 +86,7 @@ Page { Item { Layout.fillWidth: true - Layout.minimumHeight: addressLabel.height + copyLabel.height + Layout.minimumHeight: addressLabel.height + copyLabel.height + 20 Layout.topMargin: 10 height: addressLabel.height + copyLabel.height CoreText { @@ -179,11 +118,12 @@ Page { radius: 5 CoreText { id: address + text: root.request.address anchors.fill: parent anchors.leftMargin: 5 horizontalAlignment: Text.AlignLeft font.pixelSize: 18 - wrap: true + wrapMode: Text.WrapAnywhere } } } @@ -197,9 +137,8 @@ Page { if (!clearRequest.visible) { requestCounter = requestCounter + 1 clearRequest.visible = true + wallet.commitPaymentRequest() title.text = qsTr("Payment request #" + requestCounter) - address.text = "bc1q f5xe y2tf 89k9 zy6k gnru wszy 5fsa truy 9te1 bu" - qrImage.code = "bc1qf5xey2tf89k9zy6kgnruwszy5fsatruy9te1bu" continueButton.text = qsTr("Copy payment request") } } @@ -220,8 +159,7 @@ Page { onClicked: { clearRequest.visible = false title.text = qsTr("Request a payment") - address.text = "" - qrImage.code = "" + root.request.clear() continueButton.text = qsTr("Create bitcoin address") } } @@ -240,6 +178,7 @@ Page { id: qrImage backgroundColor: "transparent" foregroundColor: Theme.color.neutral9 + code: root.request.address } } } From 2bb7e517a774c3841d6524a1673c669558d233c5 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Mon, 2 Jun 2025 22:06:30 -0400 Subject: [PATCH 19/21] qml: Base CoreText on Label --- src/qml/controls/CoreText.qml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/qml/controls/CoreText.qml b/src/qml/controls/CoreText.qml index 043e9a1fde..4a5f44aaf3 100644 --- a/src/qml/controls/CoreText.qml +++ b/src/qml/controls/CoreText.qml @@ -5,15 +5,20 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 -Text { +Label { + id: label property bool bold: false property bool wrap: true + color: enabled ? Theme.color.neutral9 : Theme.color.neutral4 + font.family: "Inter" font.styleName: bold ? "Semi Bold" : "Regular" font.pixelSize: 13 + horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter + wrapMode: wrap ? Text.WordWrap : Text.NoWrap Behavior on color { From 84fbd2fb1dbc382a627c32353c224daad281064a Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Mon, 2 Jun 2025 22:10:50 -0400 Subject: [PATCH 20/21] qml: Use CoreText background for PaymentRequest address --- src/qml/pages/wallet/RequestPayment.qml | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/qml/pages/wallet/RequestPayment.qml b/src/qml/pages/wallet/RequestPayment.qml index e4bfcc9df5..61de150850 100644 --- a/src/qml/pages/wallet/RequestPayment.qml +++ b/src/qml/pages/wallet/RequestPayment.qml @@ -109,21 +109,18 @@ Page { color: enabled ? Theme.color.neutral7 : Theme.color.neutral4 } - Rectangle { + CoreText { + id: address anchors.left: addressLabel.right anchors.right: parent.right anchors.top: parent.top - anchors.bottom: parent.bottom - color: Theme.color.neutral2 - radius: 5 - CoreText { - id: address - text: root.request.address - anchors.fill: parent - anchors.leftMargin: 5 - horizontalAlignment: Text.AlignLeft - font.pixelSize: 18 - wrapMode: Text.WrapAnywhere + text: root.request.address + horizontalAlignment: Text.AlignLeft + font.pixelSize: 18 + wrapMode: Text.WrapAnywhere + background: Rectangle { + color: Theme.color.neutral2 + radius: 5 } } } From d512d17e5fd6977abdbdf792cde73977235d6b74 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Mon, 2 Jun 2025 23:08:18 -0400 Subject: [PATCH 21/21] qml: Cleanup Receive layouts --- src/qml/pages/wallet/RequestPayment.qml | 263 ++++++++++++------------ 1 file changed, 131 insertions(+), 132 deletions(-) diff --git a/src/qml/pages/wallet/RequestPayment.qml b/src/qml/pages/wallet/RequestPayment.qml index 61de150850..e7056a62ba 100644 --- a/src/qml/pages/wallet/RequestPayment.qml +++ b/src/qml/pages/wallet/RequestPayment.qml @@ -15,167 +15,166 @@ Page { id: root background: null - property int requestCounter: 0 property WalletQmlModel wallet: walletController.selectedWallet property PaymentRequest request: wallet.currentPaymentRequest ScrollView { clip: true - width: parent.width - height: parent.height + anchors.fill: parent contentWidth: width - CoreText { - id: title - anchors.left: contentRow.left + ColumnLayout { + id: scrollContent + anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: 20 - text: qsTr("Request a payment") - font.pixelSize: 21 - bold: true - } + spacing: 20 + + CoreText { + id: title + Layout.alignment: Qt.AlignLeft + text: root.request.id === "" + ? qsTr("Request a payment") + : qsTr("Payment request #") + root.request.id + font.pixelSize: 21 + bold: true + } - RowLayout { - id: contentRow + RowLayout { + id: contentRow + Layout.fillWidth: true + enabled: walletController.initialized + spacing: 30 - enabled: walletController.initialized + ColumnLayout { + id: formColumn + Layout.minimumWidth: 450 + Layout.maximumWidth: 450 - anchors.top: title.bottom - anchors.topMargin: 40 - anchors.horizontalCenter: parent.horizontalCenter - spacing: 30 - ColumnLayout { - id: columnLayout - Layout.minimumWidth: 450 - Layout.maximumWidth: 470 - - spacing: 5 - - BitcoinAmountInputField { - Layout.fillWidth: true - enabled: walletController.initialized - amount: root.request.amount - errorText: root.request.amountError - } - - Separator { - Layout.fillWidth: true - } + spacing: 10 - LabeledTextInput { - id: label - Layout.fillWidth: true - labelText: qsTr("Label") - placeholderText: qsTr("Enter label...") - } + BitcoinAmountInputField { + Layout.fillWidth: true + enabled: walletController.initialized + amount: root.request.amount + errorText: root.request.amountError + } - Separator { - Layout.fillWidth: true - } + Separator { + Layout.fillWidth: true + } - LabeledTextInput { - id: message - Layout.fillWidth: true - labelText: qsTr("Message") - placeholderText: qsTr("Enter message...") - } + LabeledTextInput { + id: label + Layout.fillWidth: true + labelText: qsTr("Label") + placeholderText: qsTr("Enter label...") + } - Separator { - Layout.fillWidth: true - } + Separator { + Layout.fillWidth: true + } - Item { - Layout.fillWidth: true - Layout.minimumHeight: addressLabel.height + copyLabel.height + 20 - Layout.topMargin: 10 - height: addressLabel.height + copyLabel.height - CoreText { - id: addressLabel - anchors.left: parent.left - anchors.top: parent.top - horizontalAlignment: Text.AlignLeft - width: 110 - text: qsTr("Address") - font.pixelSize: 18 + LabeledTextInput { + id: message + Layout.fillWidth: true + labelText: qsTr("Message") + placeholderText: qsTr("Enter message...") } - CoreText { - id: copyLabel - anchors.left: parent.left - anchors.top: addressLabel.bottom - horizontalAlignment: Text.AlignLeft - width: 110 - text: qsTr("copy") - font.pixelSize: 18 - color: enabled ? Theme.color.neutral7 : Theme.color.neutral4 + + Separator { + Layout.fillWidth: true } - CoreText { - id: address - anchors.left: addressLabel.right - anchors.right: parent.right - anchors.top: parent.top - text: root.request.address - horizontalAlignment: Text.AlignLeft - font.pixelSize: 18 - wrapMode: Text.WrapAnywhere - background: Rectangle { - color: Theme.color.neutral2 - radius: 5 + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 10 + + spacing: 0 + + ColumnLayout { + spacing: 5 + Layout.alignment: Qt.AlignLeft + Layout.minimumWidth: 110 + + CoreText { + id: addressLabel + text: qsTr("Address") + font.pixelSize: 18 + } + CoreText { + id: copyLabel + text: qsTr("copy") + font.pixelSize: 18 + color: enabled ? Theme.color.neutral7 : Theme.color.neutral4 + } + } + + CoreText { + id: address + Layout.fillWidth: true + Layout.minimumHeight: 50 + text: root.request.address + horizontalAlignment: Text.AlignLeft + font.pixelSize: 18 + wrapMode: Text.WrapAnywhere + background: Rectangle { + color: Theme.color.neutral2 + radius: 5 + } } } - } - ContinueButton { - id: continueButton - Layout.fillWidth: true - Layout.topMargin: 30 - text: qsTr("Create bitcoin address") - onClicked: { - if (!clearRequest.visible) { - requestCounter = requestCounter + 1 - clearRequest.visible = true - wallet.commitPaymentRequest() - title.text = qsTr("Payment request #" + requestCounter) - continueButton.text = qsTr("Copy payment request") + ContinueButton { + id: continueButton + Layout.fillWidth: true + Layout.topMargin: 30 + text: qsTr("Create bitcoin address") + onClicked: { + if (!clearRequest.visible) { + clearRequest.visible = true + root.wallet.commitPaymentRequest() + title.text = qsTr("Payment request #" + root.wallet.request.id) + continueButton.text = qsTr("Copy payment request") + } } } - } - ContinueButton { - id: clearRequest - Layout.fillWidth: true - Layout.topMargin: 10 - visible: false - borderColor: Theme.color.neutral6 - borderHoverColor: Theme.color.orangeLight1 - borderPressedColor: Theme.color.orangeLight2 - backgroundColor: "transparent" - backgroundHoverColor: "transparent" - backgroundPressedColor: "transparent" - text: qsTr("Clear") - onClicked: { - clearRequest.visible = false - title.text = qsTr("Request a payment") - root.request.clear() - continueButton.text = qsTr("Create bitcoin address") + ContinueButton { + id: clearRequest + Layout.fillWidth: true + Layout.topMargin: 10 + visible: false + borderColor: Theme.color.neutral6 + borderHoverColor: Theme.color.orangeLight1 + borderPressedColor: Theme.color.orangeLight2 + backgroundColor: "transparent" + backgroundHoverColor: "transparent" + backgroundPressedColor: "transparent" + text: qsTr("Clear") + onClicked: { + clearRequest.visible = false + root.request.clear() + continueButton.text = qsTr("Create bitcoin address") + } } } - } - Pane { - Layout.alignment: Qt.AlignTop - Layout.minimumWidth: 150 - Layout.minimumHeight: 150 - padding: 0 - background: Rectangle { - color: Theme.color.neutral2 - visible: qrImage.code === "" - } - contentItem: QRImage { - id: qrImage - backgroundColor: "transparent" - foregroundColor: Theme.color.neutral9 - code: root.request.address + Pane { + Layout.alignment: Qt.AlignTop + Layout.minimumWidth: 150 + Layout.minimumHeight: 150 + padding: 0 + background: Rectangle { + color: Theme.color.neutral2 + visible: qrImage.code === "" + } + contentItem: QRImage { + id: qrImage + backgroundColor: "transparent" + foregroundColor: Theme.color.neutral9 + code: root.request.address + } } } }