Skip to content

Commit 39eb251

Browse files
committed
Merge bitcoin-core/gui-qml#450: Add Multiple Recipients option to the Send form
676db23 Fix typo in MultipleSendReview (Johnny) 69c8d52 qml: Change IconButton disabled color to neutral4 (johnny9) 01affb5 qml: Disable multipleRecipients after clearing the recipients list (johnny9) 2b1d39d qml: Add a recipient when multiple recipients are enabled (johnny9) 2093017 qml: Connect Recipient label to Note to self input (johnny9) ee08381 qml: New recipients use current's units (johnny9) 1e4159a qml: When multipleRecipients is disabled, clearToFront of the list (johnny9) 5114425 qml: Split amount and display updating for Bitcoin amount input (johnny9) e29622c qml: Restrict Recipients to 25 (johnny9) 4872600 qml: Handle removal of first recipient properly (johnny9) 4d040e9 qml: Clear Send form after sending transaciton (johnny9) 33502e4 qml: Commit Recipient amount when active focus is lost (johnny9) 8a1dbe0 qml: Add total calculation to SendRecipientsListModel (johnny9) 2de4f26 qml: Cleanup BitcoinAmount (johnny9) b94f2bb qml: Replace NavButton with IconButton in Send (johnny9) d7d1d45 qml: Add plus big filled icon (johnny9) 2552e3f qml: Add MultipleSendReview page (johnny9) bd3fac2 qml: Prepare transaction with recipients list (johnny9) fdbaf00 qml: Add remove button to multiple recipients (johnny9) 1a8b696 qml: Reduce size of recipient selectors (johnny9) 34706bb qml: Add Multiple Recipients bar to Send form (johnny9) 191edb7 qml: Add Multiple Recipients toggle to Send menu (johnny9) 0cb338c 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 acd2ef4 + 676db23 commit 39eb251

21 files changed

+734
-188
lines changed

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>

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
}

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

qml/controls/EllipsisMenuButton.qml renamed to qml/controls/IconButton.qml

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@ import org.bitcoincore.qt 1.0
1111
Button {
1212
id: root
1313

14+
property color iconColor: Theme.color.orange
1415
property color hoverColor: Theme.color.orange
1516
property color activeColor: Theme.color.orange
17+
property int size: 35
18+
property alias iconSource: icon.source
1619

1720
hoverEnabled: AppMode.isDesktop
18-
implicitHeight: 35
19-
implicitWidth: 35
21+
height: root.size
22+
width: root.size
23+
padding: 0
2024

2125
MouseArea {
2226
anchors.fill: parent
@@ -25,28 +29,44 @@ Button {
2529
cursorShape: Qt.PointingHandCursor
2630
}
2731

28-
background: null
32+
background: Rectangle {
33+
id: bg
34+
anchors.fill: parent
35+
radius: 5
36+
color: Theme.color.background
37+
38+
39+
Behavior on color {
40+
ColorAnimation { duration: 150 }
41+
}
42+
}
2943

3044
contentItem: Icon {
31-
id: ellipsisIcon
45+
id: icon
3246
anchors.fill: parent
3347
source: "image://images/ellipsis"
34-
color: Theme.color.neutral9
35-
size: 35
48+
size: root.size
49+
color: iconColor
50+
51+
Behavior on color {
52+
ColorAnimation { duration: 150 }
53+
}
3654
}
3755

3856
states: [
3957
State {
4058
name: "CHECKED"; when: root.checked
41-
PropertyChanges { target: ellipsisIcon; color: activeColor }
59+
PropertyChanges { target: icon; color: activeColor }
4260
},
4361
State {
4462
name: "HOVER"; when: root.hovered
45-
PropertyChanges { target: ellipsisIcon; color: hoverColor }
63+
PropertyChanges { target: icon; color: hoverColor }
64+
PropertyChanges { target: bg; color: Theme.color.neutral2 }
4665
},
4766
State {
4867
name: "DISABLED"; when: !root.enabled
49-
PropertyChanges { target: ellipsisIcon; color: Theme.color.neutral4 }
68+
PropertyChanges { target: icon; color: Theme.color.neutral4 }
69+
PropertyChanges { target: bg; color: Theme.color.background }
5070
}
5171
]
5272
}

qml/controls/NavButton.qml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ AbstractButton {
5353
}
5454
contentItem: RowLayout {
5555
spacing: 0
56+
anchors.fill: parent
5657
Loader {
5758
id: button_background
5859
active: root.iconSource.toString().length > 0

0 commit comments

Comments
 (0)