Skip to content

Commit 8f7f8bd

Browse files
committed
Merge #450: Add Multiple Recipients option to the Send form
1769fa8 Fix typo in MultipleSendReview (Johnny) 5642319 qml: Change IconButton disabled color to neutral4 (johnny9) 0ed6bd9 qml: Disable multipleRecipients after clearing the recipients list (johnny9) ed4ba2a qml: Add a recipient when multiple recipients are enabled (johnny9) d9c3877 qml: Connect Recipient label to Note to self input (johnny9) 36aecee qml: New recipients use current's units (johnny9) f673f7c qml: When multipleRecipients is disabled, clearToFront of the list (johnny9) a583c6c qml: Split amount and display updating for Bitcoin amount input (johnny9) 5fd4441 qml: Restrict Recipients to 25 (johnny9) c05e3bc qml: Handle removal of first recipient properly (johnny9) 3b15c2a qml: Clear Send form after sending transaciton (johnny9) 7543860 qml: Commit Recipient amount when active focus is lost (johnny9) fe56371 qml: Add total calculation to SendRecipientsListModel (johnny9) 20f7189 qml: Cleanup BitcoinAmount (johnny9) 238ce8c qml: Replace NavButton with IconButton in Send (johnny9) 3acfb21 qml: Add plus big filled icon (johnny9) 0a08371 qml: Add MultipleSendReview page (johnny9) e5e5d6a qml: Prepare transaction with recipients list (johnny9) 5dc3f33 qml: Add remove button to multiple recipients (johnny9) fc6dd3a qml: Reduce size of recipient selectors (johnny9) 06e9586 qml: Add Multiple Recipients bar to Send form (johnny9) 059a7a2 qml: Add Multiple Recipients toggle to Send menu (johnny9) 20d6bcd qml: Introduce SendRecipientsListModel (johnny9) Pull request description: This change introduces the multiple recipients controls to the Send form. It is enabled in the ellipses option menu by toggling on "Enable Multiple Recipients". This is stored as a QSetting for the user. This PR depends on #448 which contains the ![Screenshot from 2025-05-08 11-51-34](https://github.com/user-attachments/assets/c1756abd-b485-4cf1-b6a8-2c0fa91ff05a) first implementation of the Send options menu. ![Screenshot from 2025-05-08 11-51-26](https://github.com/user-attachments/assets/6ce9dd39-1b12-49dd-af50-2724fabbb8b9) Top commit has no ACKs. Tree-SHA512: 2f8f9e76eea107a898015966c6f838f303d7364852a26d0ab74dfb76bbf756563a9d29b0d6572c39b78ca23dc012c93af0fdcfa5d30f6bee693f8ac34c3b0c64
2 parents 07093a1 + 1769fa8 commit 8f7f8bd

22 files changed

+742
-190
lines changed

src/Makefile.qt.include

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ QT_MOC_CPP = \
4444
qml/models/moc_nodemodel.cpp \
4545
qml/models/moc_options_model.cpp \
4646
qml/models/moc_peerdetailsmodel.cpp \
47-
qml/models/moc_peerlistsortproxy.cpp \
47+
qml/models/moc_peerlistsortproxy.cpp \\
48+
qml/models/moc_sendrecipient.cpp \
49+
qml/models/moc_sendrecipientslistmodel.cpp \
4850
qml/models/moc_transaction.cpp \
4951
qml/models/moc_sendrecipient.cpp \
5052
qml/models/moc_walletlistmodel.cpp \
@@ -138,6 +140,7 @@ BITCOIN_QT_H = \
138140
qml/models/peerlistsortproxy.h \
139141
qml/models/transaction.h \
140142
qml/models/sendrecipient.h \
143+
qml/models/sendrecipientslistmodel.h \
141144
qml/models/walletlistmodel.h \
142145
qml/models/walletqmlmodel.h \
143146
qml/models/walletqmlmodeltransaction.h \
@@ -339,6 +342,7 @@ BITCOIN_QML_BASE_CPP = \
339342
qml/models/peerlistsortproxy.cpp \
340343
qml/models/transaction.cpp \
341344
qml/models/sendrecipient.cpp \
345+
qml/models/sendrecipientslistmodel.cpp \
342346
qml/models/walletlistmodel.cpp \
343347
qml/models/walletqmlmodel.cpp \
344348
qml/models/walletqmlmodeltransaction.cpp \
@@ -380,6 +384,7 @@ QML_RES_ICONS = \
380384
qml/res/icons/network-dark.png \
381385
qml/res/icons/network-light.png \
382386
qml/res/icons/plus.png \
387+
qml/res/icons/plus-big-filled.png \
383388
qml/res/icons/pending.png \
384389
qml/res/icons/shutdown.png \
385390
qml/res/icons/singlesig-wallet.png \
@@ -422,12 +427,12 @@ QML_RES_QML = \
422427
qml/controls/CoreCheckBox.qml \
423428
qml/controls/CoreText.qml \
424429
qml/controls/CoreTextField.qml \
425-
qml/controls/EllipsisMenuButton.qml \
426430
qml/controls/EllipsisMenuToggleItem.qml \
427431
qml/controls/ExternalLink.qml \
428432
qml/controls/FocusBorder.qml \
429433
qml/controls/Header.qml \
430434
qml/controls/Icon.qml \
435+
qml/controls/IconButton.qml \
431436
qml/controls/InformationPage.qml \
432437
qml/controls/IPAddressValueInput.qml \
433438
qml/controls/KeyValueRow.qml \
@@ -486,6 +491,7 @@ QML_RES_QML = \
486491
qml/pages/wallet/CreatePassword.qml \
487492
qml/pages/wallet/CreateWalletWizard.qml \
488493
qml/pages/wallet/DesktopWallets.qml \
494+
qml/pages/wallet/MultipleSendReview.qml \
489495
qml/pages/wallet/RequestPayment.qml \
490496
qml/pages/wallet/Send.qml \
491497
qml/pages/wallet/SendResult.qml \

src/qml/bitcoin_qml.qrc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@
3131
<file>controls/FocusBorder.qml</file>
3232
<file>controls/Header.qml</file>
3333
<file>controls/Icon.qml</file>
34+
<file>controls/IconButton.qml</file>
3435
<file>controls/InformationPage.qml</file>
3536
<file>controls/IPAddressValueInput.qml</file>
3637
<file>controls/KeyValueRow.qml</file>
3738
<file>controls/LabeledTextInput.qml</file>
3839
<file>controls/LabeledCoinControlButton.qml</file>
39-
<file>controls/EllipsisMenuButton.qml</file>
4040
<file>controls/EllipsisMenuToggleItem.qml</file>
4141
<file>controls/NavButton.qml</file>
4242
<file>controls/NavigationBar.qml</file>
@@ -91,6 +91,7 @@
9191
<file>pages/wallet/CreatePassword.qml</file>
9292
<file>pages/wallet/CreateWalletWizard.qml</file>
9393
<file>pages/wallet/DesktopWallets.qml</file>
94+
<file>pages/wallet/MultipleSendReview.qml</file>
9495
<file>pages/wallet/RequestPayment.qml</file>
9596
<file>pages/wallet/Send.qml</file>
9697
<file>pages/wallet/SendResult.qml</file>
@@ -128,6 +129,7 @@
128129
<file alias="network-dark">res/icons/network-dark.png</file>
129130
<file alias="network-light">res/icons/network-light.png</file>
130131
<file alias="plus">res/icons/plus.png</file>
132+
<file alias="plus-big-filled">res/icons/plus-big-filled.png</file>
131133
<file alias="pending">res/icons/pending.png</file>
132134
<file alias="shutdown">res/icons/shutdown.png</file>
133135
<file alias="singlesig-wallet">res/icons/singlesig-wallet.png</file>

src/qml/bitcoinamount.cpp

Lines changed: 82 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2024 The Bitcoin Core developers
1+
// Copyright (c) 2024-2025 The Bitcoin Core developers
22
// Distributed under the MIT software license, see the accompanying
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

@@ -7,19 +7,9 @@
77
#include <QRegExp>
88
#include <QStringList>
99

10-
11-
BitcoinAmount::BitcoinAmount(QObject *parent) : QObject(parent)
10+
BitcoinAmount::BitcoinAmount(QObject* parent)
11+
: QObject(parent)
1212
{
13-
m_unit = Unit::BTC;
14-
}
15-
16-
int BitcoinAmount::decimals(Unit unit)
17-
{
18-
switch (unit) {
19-
case Unit::BTC: return 8;
20-
case Unit::SAT: return 0;
21-
} // no default case, so the compiler can warn about missing cases
22-
assert(false);
2313
}
2414

2515
QString BitcoinAmount::sanitize(const QString &text)
@@ -43,6 +33,30 @@ QString BitcoinAmount::sanitize(const QString &text)
4333
return result;
4434
}
4535

36+
qint64 BitcoinAmount::satoshi() const
37+
{
38+
return m_satoshi;
39+
}
40+
41+
void BitcoinAmount::setSatoshi(qint64 new_amount)
42+
{
43+
m_isSet = true;
44+
if (m_satoshi != new_amount) {
45+
m_satoshi = new_amount;
46+
Q_EMIT amountChanged();
47+
}
48+
}
49+
50+
void BitcoinAmount::clear()
51+
{
52+
if (!m_isSet && m_satoshi == 0) {
53+
return;
54+
}
55+
m_satoshi = 0;
56+
m_isSet = false;
57+
Q_EMIT amountChanged();
58+
}
59+
4660
BitcoinAmount::Unit BitcoinAmount::unit() const
4761
{
4862
return m_unit;
@@ -52,103 +66,94 @@ void BitcoinAmount::setUnit(const Unit unit)
5266
{
5367
m_unit = unit;
5468
Q_EMIT unitChanged();
69+
Q_EMIT displayChanged();
5570
}
5671

5772
QString BitcoinAmount::unitLabel() const
5873
{
5974
switch (m_unit) {
6075
case Unit::BTC: return "";
61-
case Unit::SAT: return "Sat";
76+
case Unit::SAT: return "sat";
6277
}
6378
assert(false);
6479
}
6580

66-
QString BitcoinAmount::amount() const
81+
void BitcoinAmount::flipUnit()
6782
{
68-
return m_amount;
83+
if (m_unit == Unit::BTC) {
84+
m_unit = Unit::SAT;
85+
} else {
86+
m_unit = Unit::BTC;
87+
}
88+
Q_EMIT unitChanged();
89+
Q_EMIT displayChanged();
6990
}
7091

71-
QString BitcoinAmount::satoshiAmount() const
92+
QString BitcoinAmount::satsToBtcString(qint64 sat)
7293
{
73-
return toSatoshis(m_amount);
74-
}
94+
const bool negative = sat < 0;
95+
qint64 absSat = negative ? -sat : sat;
7596

76-
void BitcoinAmount::setAmount(const QString& new_amount)
77-
{
78-
m_amount = sanitize(new_amount);
79-
Q_EMIT amountChanged();
97+
const qint64 wholePart = absSat / COIN;
98+
const qint64 fracInt = absSat % COIN;
99+
QString fracPart = QString("%1").arg(fracInt, 8, 10, QLatin1Char('0'));
100+
101+
QString result = QString::number(wholePart) + '.' + fracPart;
102+
if (negative) {
103+
result.prepend('-');
104+
}
105+
return result;
80106
}
81107

82-
QString BitcoinAmount::toSatoshis(const QString& text) const
108+
QString BitcoinAmount::toDisplay() const
83109
{
110+
if (!m_isSet) {
111+
return "";
112+
}
84113
if (m_unit == Unit::SAT) {
85-
return text;
114+
return QString::number(m_satoshi);
86115
} else {
87-
return convert(text, m_unit);
116+
return satsToBtcString(m_satoshi);
88117
}
89118
}
90119

91-
long long BitcoinAmount::toSatoshis(QString& amount, const Unit unit)
120+
qint64 BitcoinAmount::btcToSats(const QString& btcSanitized)
92121
{
93-
int num_decimals = decimals(unit);
122+
if (btcSanitized.isEmpty() || btcSanitized == ".") return 0;
94123

95-
QStringList parts = amount.remove(' ').split(".");
124+
QString cleaned = btcSanitized;
125+
if (cleaned.startsWith('.')) cleaned.prepend('0');
96126

97-
QString whole = parts[0];
98-
QString decimals;
99-
100-
if(parts.size() > 1)
101-
{
102-
decimals = parts[1];
127+
QStringList parts = cleaned.split('.');
128+
const qint64 whole = parts[0].isEmpty() ? 0 : parts[0].toLongLong();
129+
qint64 frac = 0;
130+
if (parts.size() == 2) {
131+
frac = parts[1].leftJustified(8, '0').toLongLong();
103132
}
104-
QString str = whole + decimals.leftJustified(num_decimals, '0', true);
105133

106-
return str.toLongLong();
134+
return whole * COIN + frac;
107135
}
108136

109-
QString BitcoinAmount::convert(const QString& amount, Unit unit) const
137+
void BitcoinAmount::fromDisplay(const QString& text)
110138
{
111-
if (amount == "") {
112-
return amount;
113-
}
114-
115-
QString result = amount;
116-
int decimalPosition = result.indexOf(".");
117-
118-
if (decimalPosition == -1) {
119-
decimalPosition = result.length();
120-
result.append(".");
139+
if (text.trimmed().isEmpty()) {
140+
clear();
141+
return;
121142
}
122143

123-
if (unit == Unit::BTC) {
124-
int numDigitsAfterDecimal = result.length() - decimalPosition - 1;
125-
if (numDigitsAfterDecimal < 8) {
126-
result.append(QString(8 - numDigitsAfterDecimal, '0'));
127-
}
128-
result.remove(decimalPosition, 1);
129-
130-
while (result.startsWith('0') && result.length() > 1) {
131-
result.remove(0, 1);
132-
}
133-
} else if (unit == Unit::SAT) {
134-
result.remove(decimalPosition, 1);
135-
int newDecimalPosition = decimalPosition - 8;
136-
if (newDecimalPosition < 1) {
137-
result = QString("0").repeated(-newDecimalPosition) + result;
138-
newDecimalPosition = 0;
139-
}
140-
result.insert(newDecimalPosition, ".");
141-
142-
while (result.endsWith('0') && result.contains('.')) {
143-
result.chop(1);
144-
}
145-
if (result.endsWith('.')) {
146-
result.chop(1);
147-
}
148-
if (result.startsWith('.')) {
149-
result.insert(0, "0");
150-
}
144+
qint64 newSat = 0;
145+
if (m_unit == Unit::BTC) {
146+
QString sanitized = sanitize(text);
147+
newSat = btcToSats(sanitized);
148+
} else {
149+
QString digitsOnly = text;
150+
digitsOnly.remove(QRegExp("[^0-9]"));
151+
newSat = digitsOnly.trimmed().isEmpty() ? 0 : digitsOnly.toLongLong();
151152
}
153+
setSatoshi(newSat);
154+
}
152155

153-
return result;
156+
void BitcoinAmount::format()
157+
{
158+
Q_EMIT displayChanged();
154159
}

src/qml/bitcoinamount.h

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2024 The Bitcoin Core developers
1+
// Copyright (c) 2024-2025 The Bitcoin Core developers
22
// Distributed under the MIT software license, see the accompanying
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

@@ -15,8 +15,8 @@ class BitcoinAmount : public QObject
1515
Q_OBJECT
1616
Q_PROPERTY(Unit unit READ unit WRITE setUnit NOTIFY unitChanged)
1717
Q_PROPERTY(QString unitLabel READ unitLabel NOTIFY unitChanged)
18-
Q_PROPERTY(QString amount READ amount WRITE setAmount NOTIFY amountChanged)
19-
Q_PROPERTY(QString satoshiAmount READ satoshiAmount NOTIFY amountChanged)
18+
Q_PROPERTY(QString display READ toDisplay WRITE fromDisplay NOTIFY displayChanged)
19+
Q_PROPERTY(qint64 satoshi READ satoshi WRITE setSatoshi NOTIFY amountChanged)
2020

2121
public:
2222
enum class Unit {
@@ -30,27 +30,34 @@ class BitcoinAmount : public QObject
3030
Unit unit() const;
3131
void setUnit(Unit unit);
3232
QString unitLabel() const;
33-
QString amount() const;
34-
void setAmount(const QString& new_amount);
35-
QString satoshiAmount() const;
33+
34+
QString toDisplay() const;
35+
void fromDisplay(const QString& new_amount);
36+
qint64 satoshi() const;
37+
void setSatoshi(qint64 new_amount);
38+
39+
bool isSet() const { return m_isSet; }
40+
41+
Q_INVOKABLE void format();
42+
43+
static QString satsToBtcString(qint64 sat);
3644

3745
public Q_SLOTS:
38-
QString sanitize(const QString& text);
39-
QString convert(const QString& text, Unit unit) const;
40-
QString toSatoshis(const QString& text) const;
46+
void flipUnit();
47+
void clear();
4148

4249
Q_SIGNALS:
4350
void unitChanged();
44-
void unitLabelChanged();
4551
void amountChanged();
52+
void displayChanged();
4653

4754
private:
48-
long long toSatoshis(QString &amount, const Unit unit);
49-
int decimals(Unit unit);
55+
QString sanitize(const QString& text);
56+
static qint64 btcToSats(const QString& btc);
5057

51-
Unit m_unit;
52-
QString m_unitLabel;
53-
QString m_amount;
58+
qint64 m_satoshi{0};
59+
bool m_isSet{false};
60+
Unit m_unit{Unit::BTC};
5461
};
5562

5663
#endif // BITCOIN_QML_BITCOINAMOUNT_H

0 commit comments

Comments
 (0)