Skip to content

Commit 565819d

Browse files
committed
qml: Introduce the Desktop Wallet Activity Page
1 parent c975b88 commit 565819d

21 files changed

+1046
-11
lines changed

qml/bitcoin.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616
#include <noui.h>
1717
#include <qml/appmode.h>
1818
#include <qml/bitcoinamount.h>
19+
#include <qml/clipboard.h>
1920
#ifdef __ANDROID__
2021
#include <qml/androidnotifier.h>
2122
#endif
2223
#include <qml/components/blockclockdial.h>
2324
#include <qml/controls/linegraph.h>
2425
#include <qml/guiconstants.h>
26+
#include <qml/models/activitylistmodel.h>
2527
#include <qml/models/chainmodel.h>
2628
#include <qml/models/networktraffictower.h>
2729
#include <qml/models/nodemodel.h>
@@ -326,12 +328,15 @@ int QmlGuiMain(int argc, char* argv[])
326328
engine.rootContext()->setContextProperty("needOnboarding", need_onboarding);
327329

328330
AppMode app_mode = SetupAppMode();
331+
Clipboard clipboard;
329332

330333
qmlRegisterSingletonInstance<AppMode>("org.bitcoincore.qt", 1, 0, "AppMode", &app_mode);
334+
qmlRegisterSingletonInstance<Clipboard>("org.bitcoincore.qt", 1, 0, "Clipboard", &clipboard);
331335
qmlRegisterType<BlockClockDial>("org.bitcoincore.qt", 1, 0, "BlockClockDial");
332336
qmlRegisterType<LineGraph>("org.bitcoincore.qt", 1, 0, "LineGraph");
333337
qmlRegisterUncreatableType<PeerDetailsModel>("org.bitcoincore.qt", 1, 0, "PeerDetailsModel", "");
334338
qmlRegisterType<BitcoinAmount>("org.bitcoincore.qt", 1, 0, "BitcoinAmount");
339+
qmlRegisterUncreatableType<Transaction>("org.bitcoincore.qt", 1, 0, "Transaction", "");
335340

336341
#ifdef ENABLE_WALLET
337342
qmlRegisterUncreatableType<WalletQmlModel>("org.bitcoincore.qt", 1, 0, "WalletQmlModel",

qml/bitcoin_qml.qrc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,15 @@
3232
<file>controls/InformationPage.qml</file>
3333
<file>controls/IPAddressValueInput.qml</file>
3434
<file>controls/KeyValueRow.qml</file>
35+
<file>controls/LabeledTextInput.qml</file>
3536
<file>controls/NavButton.qml</file>
36-
<file>controls/PageIndicator.qml</file>
3737
<file>controls/NavigationBar.qml</file>
3838
<file>controls/NavigationBar2.qml</file>
3939
<file>controls/NavigationTab.qml</file>
4040
<file>controls/OptionButton.qml</file>
4141
<file>controls/OptionSwitch.qml</file>
4242
<file>controls/OutlineButton.qml</file>
43+
<file>controls/PageIndicator.qml</file>
4344
<file>controls/PageStack.qml</file>
4445
<file>controls/ProgressIndicator.qml</file>
4546
<file>controls/qmldir</file>
@@ -72,6 +73,8 @@
7273
<file>pages/settings/SettingsProxy.qml</file>
7374
<file>pages/settings/SettingsStorage.qml</file>
7475
<file>pages/settings/SettingsTheme.qml</file>
76+
<file>pages/wallet/Activity.qml</file>
77+
<file>pages/wallet/ActivityDetails.qml</file>
7578
<file>pages/wallet/CreateBackup.qml</file>
7679
<file>pages/wallet/CreateConfirm.qml</file>
7780
<file>pages/wallet/CreateIntro.qml</file>
@@ -97,6 +100,7 @@
97100
<file alias="caret-right">res/icons/caret-right.png</file>
98101
<file alias="check">res/icons/check.png</file>
99102
<file alias="copy">res/icons/copy.png</file>
103+
<file alias="coinbase">res/icons/coinbase.png</file>
100104
<file alias="cross">res/icons/cross.png</file>
101105
<file alias="error">res/icons/error.png</file>
102106
<file alias="export">res/icons/export.png</file>
@@ -116,6 +120,8 @@
116120
<file alias="storage-light">res/icons/storage-light.png</file>
117121
<file alias="tooltip-arrow-dark">res/icons/tooltip-arrow-dark.png</file>
118122
<file alias="tooltip-arrow-light">res/icons/tooltip-arrow-light.png</file>
123+
<file alias="triangle-down">res/icons/triangle-down.png</file>
124+
<file alias="triangle-up">res/icons/triangle-up.png</file>
119125
<file alias="wallet">res/icons/wallet.png</file>
120126
<file alias="visible">res/icons/visible.png</file>
121127
</qresource>

qml/clipboard.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) 2024 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+
#ifndef BITCOIN_QML_CLIPBOARD_H
6+
#define BITCOIN_QML_CLIPBOARD_H
7+
8+
#include <QObject>
9+
#include <QClipboard>
10+
#include <QGuiApplication>
11+
12+
class Clipboard : public QObject
13+
{
14+
Q_OBJECT
15+
public:
16+
explicit Clipboard(QObject *parent = nullptr) : QObject(parent) {}
17+
18+
Q_INVOKABLE void setText(const QString &text) {
19+
QGuiApplication::clipboard()->setText(text);
20+
}
21+
22+
Q_INVOKABLE QString text() const {
23+
return QGuiApplication::clipboard()->text();
24+
}
25+
};
26+
27+
#endif // BITCOIN_QML_CLIPBOARD_H

qml/controls/LabeledTextInput.qml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,23 @@ Item {
1818
signal textEdited
1919

2020
id: root
21-
implicitHeight: label.height + input.height
21+
implicitHeight: input.height
2222

2323
CoreText {
2424
id: label
2525
anchors.left: parent.left
26-
anchors.top: parent.top
27-
color: Theme.color.neutral7
28-
font.pixelSize: 15
26+
anchors.verticalCenter: parent.verticalCenter
27+
horizontalAlignment: Text.AlignLeft
28+
width: 110
29+
color: Theme.color.neutral9
30+
font.pixelSize: 18
2931
}
3032

3133
TextField {
3234
id: input
33-
anchors.left: parent.left
35+
anchors.left: label.right
3436
anchors.right: iconContainer.left
35-
anchors.bottom: parent.bottom
37+
anchors.verticalCenter: parent.verticalCenter
3638
leftPadding: 0
3739
font.family: "Inter"
3840
font.styleName: "Regular"

qml/imageprovider.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ QPixmap ImageProvider::requestPixmap(const QString& id, QSize* size, const QSize
7272
return QIcon(":/icons/caret-right").pixmap(requested_size);
7373
}
7474

75+
if (id == "coinbase") {
76+
*size = requested_size;
77+
return QIcon(":/icons/coinbase").pixmap(requested_size);
78+
}
79+
7580
if (id == "check") {
7681
*size = requested_size;
7782
return QIcon(":/icons/check").pixmap(requested_size);
@@ -167,6 +172,16 @@ QPixmap ImageProvider::requestPixmap(const QString& id, QSize* size, const QSize
167172
return QIcon(":/icons/tooltip-arrow-light").pixmap(requested_size);
168173
}
169174

175+
if (id == "triangle-up") {
176+
*size = requested_size;
177+
return QIcon(":/icons/triangle-up").pixmap(requested_size);
178+
}
179+
180+
if (id == "triangle-down") {
181+
*size = requested_size;
182+
return QIcon(":/icons/triangle-down").pixmap(requested_size);
183+
}
184+
170185
if (id == "add-wallet-dark") {
171186
*size = requested_size;
172187
return QIcon(":/icons/add-wallet-dark").pixmap(requested_size);

qml/models/activitylistmodel.cpp

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright (c) 2024-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/activitylistmodel.h>
6+
7+
#include <qml/models/walletqmlmodel.h>
8+
9+
ActivityListModel::ActivityListModel(WalletQmlModel *parent)
10+
: QAbstractListModel(parent)
11+
, m_wallet_model(parent)
12+
{
13+
if (m_wallet_model != nullptr) {
14+
refreshWallet();
15+
subsctribeToCoreSignals();
16+
}
17+
}
18+
19+
ActivityListModel::~ActivityListModel()
20+
{
21+
unsubscribeFromCoreSignals();
22+
}
23+
24+
int ActivityListModel::rowCount(const QModelIndex &parent) const
25+
{
26+
Q_UNUSED(parent)
27+
return m_transactions.size();
28+
}
29+
30+
void ActivityListModel::updateTransactionStatus(QSharedPointer<Transaction> tx) const
31+
{
32+
if (m_wallet_model == nullptr) {
33+
return;
34+
}
35+
interfaces::WalletTxStatus wtx;
36+
int num_blocks;
37+
int64_t block_time;
38+
if (m_wallet_model->tryGetTxStatus(tx->hash, wtx, num_blocks, block_time)) {
39+
tx->updateStatus(wtx, num_blocks, block_time);
40+
} else {
41+
tx->status = Transaction::Status::NotAccepted;
42+
}
43+
}
44+
45+
QVariant ActivityListModel::data(const QModelIndex &index, int role) const
46+
{
47+
if (!index.isValid() || index.row() < 0 || index.row() >= m_transactions.size())
48+
return QVariant();
49+
50+
QSharedPointer<Transaction> tx = m_transactions.at(index.row());
51+
updateTransactionStatus(tx);
52+
53+
switch (role) {
54+
case AddressRole:
55+
return tx->address;
56+
case AmountRole:
57+
return tx->prettyAmount();
58+
case DateTimeRole:
59+
return tx->dateTimeString();
60+
case DepthRole:
61+
return tx->depth;
62+
case LabelRole:
63+
return tx->label;
64+
case StatusRole:
65+
return tx->status;
66+
case TypeRole:
67+
return tx->type;
68+
default:
69+
return QVariant();
70+
}
71+
}
72+
73+
QHash<int, QByteArray> ActivityListModel::roleNames() const
74+
{
75+
QHash<int, QByteArray> roles;
76+
roles[AddressRole] = "address";
77+
roles[AmountRole] = "amount";
78+
roles[DateTimeRole] = "date";
79+
roles[DepthRole] = "depth";
80+
roles[LabelRole] = "label";
81+
roles[StatusRole] = "status";
82+
roles[TypeRole] = "type";
83+
return roles;
84+
}
85+
86+
void ActivityListModel::refreshWallet()
87+
{
88+
if (m_wallet_model == nullptr) {
89+
return;
90+
}
91+
for (const auto &tx : m_wallet_model->getWalletTxs()) {
92+
auto transactions = Transaction::fromWalletTx(tx);
93+
m_transactions.append(transactions);
94+
for (const auto &transaction : transactions) {
95+
updateTransactionStatus(transaction);
96+
}
97+
}
98+
std::sort(m_transactions.begin(), m_transactions.end(),
99+
[](const QSharedPointer<Transaction> &a, const QSharedPointer<Transaction> &b) {
100+
return a->depth < b->depth;
101+
});
102+
}
103+
104+
void ActivityListModel::updateTransaction(const uint256& hash, const interfaces::WalletTxStatus& tx_status, int num_blocks, int64_t block_time)
105+
{
106+
int index = findTransactionIndex(hash);
107+
108+
if (index != -1) {
109+
QSharedPointer<Transaction> tx = m_transactions.at(index);
110+
tx->updateStatus(tx_status, num_blocks, block_time);
111+
Q_EMIT dataChanged(this->index(index), this->index(index));
112+
} else {
113+
// new transaction
114+
interfaces::WalletTx wtx = m_wallet_model->getWalletTx(hash);
115+
auto transactions = Transaction::fromWalletTx(wtx);
116+
for (const auto& tx : transactions) {
117+
tx->updateStatus(tx_status, num_blocks, block_time);
118+
m_transactions.push_front(tx);
119+
}
120+
Q_EMIT dataChanged(this->index(0), this->index(m_transactions.size() - 1));
121+
}
122+
}
123+
124+
125+
int ActivityListModel::findTransactionIndex(const uint256& hash) const
126+
{
127+
auto it = std::find_if(m_transactions.begin(), m_transactions.end(),
128+
[&hash](const QSharedPointer<Transaction>& tx) {
129+
return tx->hash == hash;
130+
});
131+
if (it != m_transactions.end()) {
132+
return std::distance(m_transactions.begin(), it);
133+
}
134+
return -1;
135+
}
136+
137+
void ActivityListModel::subsctribeToCoreSignals()
138+
{
139+
// Connect signals to wallet
140+
m_handler_transaction_changed = m_wallet_model->handleTransactionChanged([this](const uint256& hash, ChangeType status) {
141+
interfaces::WalletTxStatus wtx;
142+
int num_blocks;
143+
int64_t block_time;
144+
if (m_wallet_model->tryGetTxStatus(hash, wtx, num_blocks, block_time)) {
145+
updateTransaction(hash, wtx, num_blocks, block_time);
146+
}
147+
});
148+
}
149+
150+
void ActivityListModel::unsubscribeFromCoreSignals()
151+
{
152+
// Disconnect signals from wallet
153+
if (m_handler_transaction_changed) {
154+
m_handler_transaction_changed->disconnect();
155+
}
156+
}

qml/models/activitylistmodel.h

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) 2024-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+
#ifndef BITCOIN_QML_MODELS_ACTIVITYLISTMODEL_H
6+
#define BITCOIN_QML_MODELS_ACTIVITYLISTMODEL_H
7+
8+
#include <interfaces/handler.h>
9+
#include <interfaces/wallet.h>
10+
#include <qml/models/transaction.h>
11+
12+
#include <memory>
13+
#include <QAbstractListModel>
14+
#include <QList>
15+
#include <QSharedPointer>
16+
#include <QString>
17+
18+
class WalletQmlModel;
19+
20+
class ActivityListModel : public QAbstractListModel
21+
{
22+
Q_OBJECT
23+
24+
public:
25+
explicit ActivityListModel(WalletQmlModel * parent = nullptr);
26+
~ActivityListModel();
27+
28+
enum TransactionRoles {
29+
AddressRole = Qt::UserRole + 1,
30+
AmountRole,
31+
DateTimeRole,
32+
DepthRole,
33+
LabelRole,
34+
StatusRole,
35+
TypeRole
36+
};
37+
38+
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
39+
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
40+
QHash<int, QByteArray> roleNames() const override;
41+
42+
private:
43+
void refreshWallet();
44+
void updateTransactionStatus(QSharedPointer<Transaction> tx) const;
45+
void subsctribeToCoreSignals();
46+
void unsubscribeFromCoreSignals();
47+
void updateTransaction(const uint256& hash, const interfaces::WalletTxStatus& wtx,
48+
int num_blocks, int64_t block_time);
49+
int findTransactionIndex(const uint256& hash) const;
50+
51+
QList<QSharedPointer<Transaction>> m_transactions;
52+
WalletQmlModel* m_wallet_model;
53+
std::unique_ptr<interfaces::Handler> m_handler_transaction_changed;
54+
std::unique_ptr<interfaces::Handler> m_handler_show_progress;
55+
};
56+
57+
#endif // BITCOIN_QML_MODELS_ACTIVITYLISTMODEL_H

0 commit comments

Comments
 (0)