Skip to content

Commit 6ad8855

Browse files
committed
qml: add BitcoinAddress model and input field components
Add the BitcoinAddress C++ model class that provides: - Address formatting (space-separated groups of 4 characters) - Ellipsis truncation for display - Cursor-aware setAddress() for live editing in text fields - Base58 character filtering and 62-char cap Add two new QML input components: - BitcoinAddressInputField: address input with inline validation error display using alert-filled icon - BitcoinAmountInputField: amount input with unit label, BTC/sat flip toggle, regex validation, and inline error display Register BitcoinAddress as a creatable QML type in bitcoin.cpp and add the new QML components to bitcoin_qml.qrc.
1 parent b9ac893 commit 6ad8855

6 files changed

Lines changed: 380 additions & 0 deletions

File tree

qml/bitcoin.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <qml/guiconstants.h>
3030
#include <qml/imageprovider.h>
3131
#include <qml/models/activitylistmodel.h>
32+
#include <qml/models/bitcoinaddress.h>
3233
#include <qml/models/chainmodel.h>
3334
#include <qml/models/networktraffictower.h>
3435
#include <qml/models/nodemodel.h>
@@ -328,6 +329,7 @@ int QmlGuiMain(int argc, char* argv[])
328329
qmlRegisterType<LineGraph>("org.bitcoincore.qt", 1, 0, "LineGraph");
329330
qmlRegisterUncreatableType<PeerDetailsModel>("org.bitcoincore.qt", 1, 0, "PeerDetailsModel", "");
330331
qmlRegisterType<BitcoinAmount>("org.bitcoincore.qt", 1, 0, "BitcoinAmount");
332+
qmlRegisterType<BitcoinAddress>("org.bitcoincore.qt", 1, 0, "BitcoinAddress");
331333
qmlRegisterUncreatableType<Transaction>("org.bitcoincore.qt", 1, 0, "Transaction", "");
332334
qmlRegisterUncreatableType<SendRecipient>("org.bitcoincore.qt", 1, 0, "SendRecipient", "");
333335

qml/bitcoin_qml.qrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<!DOCTYPE RCC><RCC version="1.0">
22
<qresource prefix="/qml">
33
<file>components/AboutOptions.qml</file>
4+
<file>components/BitcoinAddressInputField.qml</file>
5+
<file>components/BitcoinAmountInputField.qml</file>
46
<file>components/BlockClock.qml</file>
57
<file>components/BlockClockDisplayMode.qml</file>
68
<file>components/BlockCounter.qml</file>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) 2025 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
import QtQuick 2.15
6+
import QtQuick.Controls 2.15
7+
import QtQuick.Layouts 1.15
8+
import org.bitcoincore.qt 1.0
9+
10+
import "../controls"
11+
12+
ColumnLayout {
13+
id: root
14+
15+
property var address
16+
property string errorText: ""
17+
property string labelText: qsTr("Send to")
18+
property bool enabled: true
19+
20+
signal editingFinished()
21+
22+
Layout.fillWidth: true
23+
spacing: 4
24+
25+
LabeledTextInput {
26+
id: addressInput
27+
Layout.fillWidth: true
28+
Layout.alignment: Qt.AlignTop
29+
enabled: root.enabled
30+
labelText: root.labelText
31+
placeholderText: qsTr("Enter address...")
32+
text: root.address.formattedAddress
33+
34+
onTextEdited: {
35+
if (root.address) {
36+
cursorPosition = root.address.setAddress(text, cursorPosition)
37+
}
38+
}
39+
40+
41+
onEditingFinished: {
42+
if (root.address) {
43+
root.address.setAddress(text, cursorPosition)
44+
root.editingFinished()
45+
}
46+
}
47+
48+
49+
onActiveFocusChanged: {
50+
if (!activeFocus && root.address) {
51+
root.address.setAddress(text)
52+
}
53+
}
54+
}
55+
56+
57+
RowLayout {
58+
id: addressIssue
59+
Layout.fillWidth: true
60+
visible: root.errorText.length > 0
61+
spacing: 8
62+
Layout.topMargin: 4
63+
64+
Icon {
65+
source: "image://images/alert-filled"
66+
size: 22
67+
color: Theme.color.red
68+
}
69+
70+
CoreText {
71+
text: root.errorText
72+
font.pixelSize: 15
73+
color: Theme.color.red
74+
horizontalAlignment: Text.AlignLeft
75+
Layout.fillWidth: true
76+
}
77+
}
78+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright (c) 2025 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
import QtQuick 2.15
6+
import QtQuick.Controls 2.15
7+
import QtQuick.Layouts 1.15
8+
import org.bitcoincore.qt 1.0
9+
10+
import "../controls"
11+
12+
ColumnLayout {
13+
id: root
14+
15+
property var amount
16+
property string errorText: ""
17+
property string labelText: qsTr("Amount")
18+
property bool enabled: true
19+
20+
signal editingFinished(string value)
21+
22+
Layout.fillWidth: true
23+
spacing: 4
24+
25+
Item {
26+
id: inputRow
27+
height: amountInput.height
28+
Layout.fillWidth: true
29+
30+
CoreText {
31+
id: lbl
32+
width: 110
33+
anchors.left: parent.left
34+
anchors.verticalCenter: parent.verticalCenter
35+
horizontalAlignment: Text.AlignLeft
36+
text: root.labelText
37+
font.pixelSize: 18
38+
}
39+
40+
TextField {
41+
id: amountInput
42+
anchors.left: lbl.right
43+
anchors.verticalCenter: parent.verticalCenter
44+
leftPadding: 0
45+
enabled: root.enabled
46+
font.family: "Inter"
47+
font.styleName: "Regular"
48+
font.pixelSize: 18
49+
color: Theme.color.neutral9
50+
placeholderTextColor: enabled ? Theme.color.neutral7 : Theme.color.neutral4
51+
background: Item {}
52+
placeholderText: "0.00000000"
53+
selectByMouse: true
54+
55+
text: root.amount ? root.amount.display : ""
56+
57+
onEditingFinished: {
58+
if (root.amount) {
59+
root.amount.display = text
60+
}
61+
root.editingFinished(text)
62+
}
63+
64+
onActiveFocusChanged: {
65+
if (!activeFocus && root.amount) {
66+
root.amount.display = text
67+
root.editingFinished(text)
68+
}
69+
}
70+
71+
validator: RegExpValidator {
72+
regExp: /^(0|[1-9]\d*)(\.\d{0,8})?$/
73+
}
74+
maximumLength: 17
75+
}
76+
77+
Item {
78+
width: unitLabel.width + flipIcon.width
79+
height: Math.max(unitLabel.height, flipIcon.height)
80+
anchors.right: parent.right
81+
anchors.verticalCenter: parent.verticalCenter
82+
opacity: root.enabled ? 1.0 : 0.5
83+
84+
MouseArea {
85+
anchors.fill: parent
86+
hoverEnabled: true
87+
cursorShape: Qt.PointingHandCursor
88+
enabled: root.enabled && root.amount
89+
onClicked: {
90+
root.amount.display = amountInput.text
91+
root.amount.flipUnit()
92+
}
93+
}
94+
95+
CoreText {
96+
id: unitLabel
97+
anchors.right: flipIcon.left
98+
anchors.verticalCenter: parent.verticalCenter
99+
text: root.amount ? root.amount.unitLabel : ""
100+
font.pixelSize: 18
101+
color: enabled ? Theme.color.neutral7 : Theme.color.neutral4
102+
}
103+
104+
Icon {
105+
id: flipIcon
106+
anchors.right: parent.right
107+
anchors.verticalCenter: parent.verticalCenter
108+
source: "image://images/flip-vertical"
109+
icon.color: enabled ? Theme.color.neutral8 : Theme.color.neutral4
110+
size: 30
111+
}
112+
}
113+
}
114+
115+
RowLayout {
116+
id: errorRow
117+
Layout.fillWidth: true
118+
visible: root.errorText.length > 0
119+
120+
Icon {
121+
source: "image://images/alert-filled"
122+
size: 22
123+
color: Theme.color.red
124+
}
125+
126+
CoreText {
127+
text: root.errorText
128+
font.pixelSize: 15
129+
color: Theme.color.red
130+
horizontalAlignment: Text.AlignLeft
131+
Layout.fillWidth: true
132+
}
133+
}
134+
}

qml/models/bitcoinaddress.cpp

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright (c) 2025 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <qml/models/bitcoinaddress.h>
6+
7+
BitcoinAddress::BitcoinAddress(QObject* parent)
8+
: QObject(parent)
9+
{
10+
// Initialize with empty address
11+
setAddress("", 0);
12+
}
13+
14+
BitcoinAddress::BitcoinAddress(const QString& address, QObject* parent)
15+
: QObject(parent)
16+
{
17+
setAddress(address, 0);
18+
}
19+
20+
QString BitcoinAddress::address() const
21+
{
22+
return m_address;
23+
}
24+
25+
bool BitcoinAddress::isEmpty() const
26+
{
27+
return m_address.isEmpty();
28+
}
29+
30+
QString BitcoinAddress::formattedAddress() const
31+
{
32+
return m_formattedAddress;
33+
}
34+
35+
QString BitcoinAddress::ellipsesAddress() const
36+
{
37+
return ellipsesAddress(m_address);
38+
}
39+
40+
int BitcoinAddress::setAddress(const QString& input, int cursorPosition)
41+
{
42+
if (input == m_address) {
43+
return cursorPosition;
44+
}
45+
46+
if (input.isEmpty()) {
47+
m_address = "";
48+
m_formattedAddress = "";
49+
Q_EMIT addressChanged();
50+
Q_EMIT formattedAddressChanged();
51+
Q_EMIT ellipsesAddressChanged();
52+
return 0;
53+
}
54+
55+
// 1) Define the allowed Base58 characters
56+
static const QString base58Chars =
57+
QStringLiteral("0123456789"
58+
"ABCDEFGHJKLMNPQRSTUVWXYZ"
59+
"abcdefghijklmnopqrstuvwxyz");
60+
61+
// 2) Count how many *valid* chars were before the old cursor
62+
int posInClean = 0;
63+
const int lenIn = qMin(cursorPosition, input.length());
64+
for (int i = 0; i < lenIn; ++i) {
65+
if (base58Chars.contains(input[i]))
66+
++posInClean;
67+
}
68+
69+
// 3) Build the raw (no-space) address, filtering and capping at 62 chars
70+
QString raw;
71+
raw.reserve(qMin(input.length(), 62));
72+
for (QChar c : input) {
73+
if (base58Chars.contains(c)) {
74+
raw += c;
75+
if (raw.length() >= 62)
76+
break;
77+
}
78+
}
79+
m_address = raw;
80+
81+
// 4) Format into groups of 4 chars separated by spaces
82+
QString fmt = BitcoinAddress::formattedAddress(raw);
83+
m_formattedAddress = fmt;
84+
85+
// 5) Fire your QML signals
86+
Q_EMIT addressChanged();
87+
Q_EMIT formattedAddressChanged();
88+
Q_EMIT ellipsesAddressChanged();
89+
90+
// 6) Map back to a cursor position in the new formatted string
91+
int newCursor = 0, seen = 0;
92+
while (newCursor < fmt.length() && seen < posInClean) {
93+
if (fmt[newCursor] != QChar(' '))
94+
++seen;
95+
++newCursor;
96+
}
97+
return newCursor;
98+
}
99+
100+
QString BitcoinAddress::formattedAddress(const QString& address)
101+
{
102+
QString fmt;
103+
fmt.reserve(address.length() + address.length() / 4);
104+
for (int i = 0; i < address.length(); ++i) {
105+
if (i > 0 && (i % 4) == 0) {
106+
fmt += QChar(' ');
107+
}
108+
fmt += address[i];
109+
}
110+
return fmt;
111+
}
112+
113+
QString BitcoinAddress::ellipsesAddress(const QString& address)
114+
{
115+
if (address.length() > 8) {
116+
QString left = address.left(4) + ' ' + address.mid(4, 4);
117+
118+
QString right = address.mid(address.length() - 8, 4) + ' ' + address.right(4);
119+
120+
return left + " ... " + right;
121+
} else {
122+
return address;
123+
}
124+
}

0 commit comments

Comments
 (0)