From 20d6bcd63777a8fef1916cf2969e0d3d7f99879a 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/23] 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 059a7a253a3a135a2f6d8308daafa7f7c68af28b 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/23] 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 06e95861fd5810273a762c25ddacb820a555db28 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/23] 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 fc6dd3a28dec4ed09d2caebe29d836337de7a460 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/23] 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 5dc3f33108a23107b6c15963ac627f43180ea50f 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/23] 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 e5e5d6a6cc27cae4a5f331c2066acac4af9c526d 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/23] 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 0a0837111c420335abb0948f1fd98d4c0cde5437 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/23] 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 d2bf0469b3..bdb6bb4ba6 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 @@ -132,7 +132,7 @@ Page { Activity { } Send { - onTransactionPrepared: root.sendTransaction() + onTransactionPrepared: root.sendTransaction(multipleRecipientsEnabled) } RequestPayment { } 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 3acfb2191fc3b3a2963b9be6b410f7d8dccbdc1e 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/23] 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 238ce8c5713c734d16852946f75f6980c07f3891 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/23] 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 20f7189f2f6e4ceca866225f9e327cf57387a6c8 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/23] 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 fe5637115e3658ee3802ad10d36e65a38acbe479 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/23] 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 7543860ea673fe9e57d87a41426873c67f25834e 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/23] 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 3b15c2a927cd7e630991b907ee0ebaaf465d503a 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/23] 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 c05e3bce12de09c2c97d390114bbc74c80697cdf Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Tue, 3 Jun 2025 12:08:19 -0400 Subject: [PATCH 14/23] qml: Handle removal of first recipient properly --- src/qml/models/sendrecipientslistmodel.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/qml/models/sendrecipientslistmodel.cpp b/src/qml/models/sendrecipientslistmodel.cpp index f78fccad80..eb58044b5e 100644 --- a/src/qml/models/sendrecipientslistmodel.cpp +++ b/src/qml/models/sendrecipientslistmodel.cpp @@ -93,7 +93,11 @@ void SendRecipientsListModel::remove() endRemoveRows(); Q_EMIT countChanged(); - setCurrentIndex(m_current - 1); + if (m_current > 0) { + setCurrentIndex(m_current - 1); + } else { + Q_EMIT currentRecipientChanged(); + } } SendRecipient* SendRecipientsListModel::currentRecipient() const From 5fd444135a0f118bab0681036875610d637c960e Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Tue, 3 Jun 2025 18:07:18 -0400 Subject: [PATCH 15/23] qml: Restrict Recipients to 25 --- src/qml/pages/wallet/Send.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml index 88820c811a..0203b8f59e 100644 --- a/src/qml/pages/wallet/Send.qml +++ b/src/qml/pages/wallet/Send.qml @@ -131,6 +131,7 @@ PageStack { Layout.preferredHeight: 30 size: 30 iconSource: "image://images/plus-big-filled" + enabled: wallet.recipients.count < 25 onClicked: { wallet.recipients.add() } From a583c6cd8e9f809e488be5feccab1cd6836ccda4 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Wed, 4 Jun 2025 01:11:06 -0400 Subject: [PATCH 16/23] qml: Split amount and display updating for Bitcoin amount input --- src/qml/bitcoinamount.cpp | 8 +++++++- src/qml/bitcoinamount.h | 7 ++++++- src/qml/pages/wallet/Send.qml | 3 ++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/qml/bitcoinamount.cpp b/src/qml/bitcoinamount.cpp index eca63bfbf6..3cc31605d8 100644 --- a/src/qml/bitcoinamount.cpp +++ b/src/qml/bitcoinamount.cpp @@ -66,6 +66,7 @@ void BitcoinAmount::setUnit(const Unit unit) { m_unit = unit; Q_EMIT unitChanged(); + Q_EMIT displayChanged(); } QString BitcoinAmount::unitLabel() const @@ -85,7 +86,7 @@ void BitcoinAmount::flipUnit() m_unit = Unit::BTC; } Q_EMIT unitChanged(); - Q_EMIT amountChanged(); + Q_EMIT displayChanged(); } QString BitcoinAmount::satsToBtcString(qint64 sat) @@ -151,3 +152,8 @@ void BitcoinAmount::fromDisplay(const QString& text) } setSatoshi(newSat); } + +void BitcoinAmount::format() +{ + Q_EMIT displayChanged(); +} diff --git a/src/qml/bitcoinamount.h b/src/qml/bitcoinamount.h index d277d8640f..c451b34fd3 100644 --- a/src/qml/bitcoinamount.h +++ b/src/qml/bitcoinamount.h @@ -15,7 +15,7 @@ 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 display READ toDisplay WRITE fromDisplay NOTIFY amountChanged) + Q_PROPERTY(QString display READ toDisplay WRITE fromDisplay NOTIFY displayChanged) Q_PROPERTY(qint64 satoshi READ satoshi WRITE setSatoshi NOTIFY amountChanged) public: @@ -36,6 +36,10 @@ class BitcoinAmount : public QObject qint64 satoshi() const; void setSatoshi(qint64 new_amount); + bool isSet() const { return m_isSet; } + + Q_INVOKABLE void format(); + static QString satsToBtcString(qint64 sat); public Q_SLOTS: @@ -45,6 +49,7 @@ public Q_SLOTS: Q_SIGNALS: void unitChanged(); void amountChanged(); + void displayChanged(); private: QString sanitize(const QString& text); diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml index 0203b8f59e..33caa3884d 100644 --- a/src/qml/pages/wallet/Send.qml +++ b/src/qml/pages/wallet/Send.qml @@ -194,7 +194,8 @@ PageStack { placeholderText: "0.00000000" selectByMouse: true text: root.recipient.amount.display - onEditingFinished: root.recipient.amount.display = text + onTextEdited: root.recipient.amount.display = text + onEditingFinished: root.recipient.amount.format() onActiveFocusChanged: { if (!activeFocus) { root.recipient.amount.display = text From f673f7c4627be770e73a8be7119bab29f41383d0 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Fri, 6 Jun 2025 00:06:14 -0400 Subject: [PATCH 17/23] qml: When multipleRecipients is disabled, clearToFront of the list --- src/qml/models/sendrecipientslistmodel.cpp | 25 ++++++++++++++++++++++ src/qml/models/sendrecipientslistmodel.h | 1 + src/qml/pages/wallet/Send.qml | 6 ++++++ 3 files changed, 32 insertions(+) diff --git a/src/qml/models/sendrecipientslistmodel.cpp b/src/qml/models/sendrecipientslistmodel.cpp index eb58044b5e..4014bfc450 100644 --- a/src/qml/models/sendrecipientslistmodel.cpp +++ b/src/qml/models/sendrecipientslistmodel.cpp @@ -144,3 +144,28 @@ void SendRecipientsListModel::clear() Q_EMIT currentRecipientChanged(); Q_EMIT currentIndexChanged(); } + +void SendRecipientsListModel::clearToFront() +{ + bool count_changed = false; + while (m_recipients.size() > 1) { + delete m_recipients.at(1); + m_recipients.removeAt(1); + count_changed = true; + } + + if (count_changed) { + Q_EMIT countChanged(); + } + + if (m_totalAmount != m_recipients[0]->amount()->satoshi()) { + m_totalAmount = m_recipients[0]->amount()->satoshi(); + Q_EMIT totalAmountChanged(); + } + + if (m_current != 0) { + m_current = 0; + Q_EMIT currentRecipientChanged(); + Q_EMIT currentIndexChanged(); + } +} diff --git a/src/qml/models/sendrecipientslistmodel.h b/src/qml/models/sendrecipientslistmodel.h index a8ec1c8b3d..0fd9be1f45 100644 --- a/src/qml/models/sendrecipientslistmodel.h +++ b/src/qml/models/sendrecipientslistmodel.h @@ -38,6 +38,7 @@ class SendRecipientsListModel : public QAbstractListModel Q_INVOKABLE void prev(); Q_INVOKABLE void remove(); Q_INVOKABLE void clear(); + Q_INVOKABLE void clearToFront(); int currentIndex() const { return m_current + 1; } void setCurrentIndex(int row); diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml index 33caa3884d..ecf1d6f1b8 100644 --- a/src/qml/pages/wallet/Send.qml +++ b/src/qml/pages/wallet/Send.qml @@ -34,6 +34,12 @@ PageStack { id: settings property alias coinControlEnabled: sendOptionsPopup.coinControlEnabled property alias multipleRecipientsEnabled: sendOptionsPopup.multipleRecipientsEnabled + + onMultipleRecipientsEnabledChanged: { + if (!multipleRecipientsEnabled) { + root.wallet.recipients.clearToFront() + } + } } ScrollView { From 36aecee45422407dfacdd4c99336bb3ed653a41a Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Fri, 6 Jun 2025 00:27:53 -0400 Subject: [PATCH 18/23] qml: New recipients use current's units --- src/qml/models/sendrecipientslistmodel.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/qml/models/sendrecipientslistmodel.cpp b/src/qml/models/sendrecipientslistmodel.cpp index 4014bfc450..b4d508cbd0 100644 --- a/src/qml/models/sendrecipientslistmodel.cpp +++ b/src/qml/models/sendrecipientslistmodel.cpp @@ -53,7 +53,11 @@ void SendRecipientsListModel::add() auto* recipient = new SendRecipient(this); connect(recipient->amount(), &BitcoinAmount::amountChanged, this, &SendRecipientsListModel::updateTotalAmount); + if (m_recipients.size() > 0) { + recipient->amount()->setUnit(m_recipients[m_current]->amount()->unit()); + } m_recipients.append(recipient); + endInsertRows(); Q_EMIT countChanged(); setCurrentIndex(row); From d9c3877c7c8fc0d20fea575d66dd6962de7ef78c Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Fri, 6 Jun 2025 00:46:13 -0400 Subject: [PATCH 19/23] qml: Connect Recipient label to Note to self input --- src/qml/pages/wallet/Send.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml index ecf1d6f1b8..ea0a431d06 100644 --- a/src/qml/pages/wallet/Send.qml +++ b/src/qml/pages/wallet/Send.qml @@ -245,6 +245,7 @@ PageStack { Layout.fillWidth: true labelText: qsTr("Note to self") placeholderText: qsTr("Enter ...") + text: root.recipient.label onTextEdited: root.recipient.label = label.text } From ed4ba2af8704eae88d03079089dc3ad09dd5f7d1 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Fri, 6 Jun 2025 08:33:47 -0400 Subject: [PATCH 20/23] qml: Add a recipient when multiple recipients are enabled --- src/qml/pages/wallet/Send.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml index ea0a431d06..5753b2a99f 100644 --- a/src/qml/pages/wallet/Send.qml +++ b/src/qml/pages/wallet/Send.qml @@ -38,6 +38,8 @@ PageStack { onMultipleRecipientsEnabledChanged: { if (!multipleRecipientsEnabled) { root.wallet.recipients.clearToFront() + } else { + root.wallet.recipients.add() } } } From 0ed6bd9fa1b8f734f5cd83946bb90077225ad4fb Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Sat, 7 Jun 2025 02:52:43 -0400 Subject: [PATCH 21/23] qml: Disable multipleRecipients after clearing the recipients list --- src/qml/models/sendrecipientslistmodel.cpp | 1 + src/qml/models/sendrecipientslistmodel.h | 1 + src/qml/pages/wallet/Send.qml | 8 ++++++++ 3 files changed, 10 insertions(+) diff --git a/src/qml/models/sendrecipientslistmodel.cpp b/src/qml/models/sendrecipientslistmodel.cpp index b4d508cbd0..c4c4d0a683 100644 --- a/src/qml/models/sendrecipientslistmodel.cpp +++ b/src/qml/models/sendrecipientslistmodel.cpp @@ -147,6 +147,7 @@ void SendRecipientsListModel::clear() Q_EMIT totalAmountChanged(); Q_EMIT currentRecipientChanged(); Q_EMIT currentIndexChanged(); + Q_EMIT listCleared(); } void SendRecipientsListModel::clearToFront() diff --git a/src/qml/models/sendrecipientslistmodel.h b/src/qml/models/sendrecipientslistmodel.h index 0fd9be1f45..0e9b77d4b7 100644 --- a/src/qml/models/sendrecipientslistmodel.h +++ b/src/qml/models/sendrecipientslistmodel.h @@ -53,6 +53,7 @@ class SendRecipientsListModel : public QAbstractListModel void currentRecipientChanged(); void countChanged(); void totalAmountChanged(); + void listCleared(); private: void updateTotalAmount(); diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml index 5753b2a99f..2e4841b0a0 100644 --- a/src/qml/pages/wallet/Send.qml +++ b/src/qml/pages/wallet/Send.qml @@ -27,6 +27,14 @@ PageStack { } } + Connections { + target: root.wallet.recipients + function onListCleared() { + settings.multipleRecipientsEnabled = false + } + } + + initialItem: Page { background: null From 564231917b43bc0ab121efd90e7a70ec8761faef Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Sat, 7 Jun 2025 03:02:35 -0400 Subject: [PATCH 22/23] qml: Change IconButton disabled color to neutral4 --- src/qml/controls/IconButton.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qml/controls/IconButton.qml b/src/qml/controls/IconButton.qml index 0cddb36cdf..10df825620 100644 --- a/src/qml/controls/IconButton.qml +++ b/src/qml/controls/IconButton.qml @@ -65,7 +65,7 @@ Button { }, State { name: "DISABLED"; when: !root.enabled - PropertyChanges { target: icon; color: Theme.color.neutral2 } + PropertyChanges { target: icon; color: Theme.color.neutral4 } PropertyChanges { target: bg; color: Theme.color.background } } ] From 1769fa81f96ab67183c002d0f75f1c5824aa4e42 Mon Sep 17 00:00:00 2001 From: Johnny Date: Thu, 26 Jun 2025 16:47:10 -0400 Subject: [PATCH 23/23] Fix typo in MultipleSendReview Co-authored-by: Marnix Croes <93143998+MarnixCroes@users.noreply.github.com> --- src/qml/pages/wallet/MultipleSendReview.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qml/pages/wallet/MultipleSendReview.qml b/src/qml/pages/wallet/MultipleSendReview.qml index 91f6ee2d96..0de43147c8 100644 --- a/src/qml/pages/wallet/MultipleSendReview.qml +++ b/src/qml/pages/wallet/MultipleSendReview.qml @@ -141,7 +141,7 @@ Page { } ContinueButton { - id: confimationButton + id: confirmationButton Layout.fillWidth: true Layout.topMargin: 30 text: qsTr("Send")