transactiontablemodel.cpp raw
1 // Copyright (c) 2011-2022 The Limenka 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 <qt/transactiontablemodel.h>
6
7 #include <qt/addresstablemodel.h>
8 #include <qt/limenkaunits.h>
9 #include <qt/clientmodel.h>
10 #include <qt/guiconstants.h>
11 #include <qt/guiutil.h>
12 #include <qt/optionsmodel.h>
13 #include <qt/platformstyle.h>
14 #include <qt/transactiondesc.h>
15 #include <qt/transactionrecord.h>
16 #include <qt/walletmodel.h>
17
18 #include <core_io.h>
19 #include <interfaces/handler.h>
20 #include <tinyformat.h>
21 #include <uint256.h>
22
23 #include <algorithm>
24 #include <functional>
25
26 #include <QColor>
27 #include <QDateTime>
28 #include <QDebug>
29 #include <QIcon>
30 #include <QLatin1Char>
31 #include <QLatin1String>
32 #include <QList>
33
34
35 // Amount column is right-aligned it contains numbers
36 static int column_alignments[] = {
37 Qt::AlignLeft|Qt::AlignVCenter, /*status=*/
38 Qt::AlignLeft|Qt::AlignVCenter, /*watchonly=*/
39 Qt::AlignLeft|Qt::AlignVCenter, /*date=*/
40 Qt::AlignLeft|Qt::AlignVCenter, /*type=*/
41 Qt::AlignLeft|Qt::AlignVCenter, /*address=*/
42 Qt::AlignRight|Qt::AlignVCenter /* amount */
43 };
44
45 // Comparison operator for sort/binary search of model tx list
46 struct TxLessThan
47 {
48 bool operator()(const TransactionRecord &a, const TransactionRecord &b) const
49 {
50 return a.hash < b.hash;
51 }
52 bool operator()(const TransactionRecord &a, const uint256 &b) const
53 {
54 return a.hash < b;
55 }
56 bool operator()(const uint256 &a, const TransactionRecord &b) const
57 {
58 return a < b.hash;
59 }
60 };
61
62 // queue notifications to show a non freezing progress dialog e.g. for rescan
63 struct TransactionNotification
64 {
65 public:
66 TransactionNotification() = default;
67 TransactionNotification(uint256 _hash, ChangeType _status, bool _showTransaction):
68 hash(_hash), status(_status), showTransaction(_showTransaction) {}
69
70 void invoke(QObject *ttm)
71 {
72 QString strHash = QString::fromStdString(hash.GetHex());
73 qDebug() << "NotifyTransactionChanged: " + strHash + " status= " + QString::number(status);
74 bool invoked = QMetaObject::invokeMethod(ttm, "updateTransaction", Qt::QueuedConnection,
75 Q_ARG(QString, strHash),
76 Q_ARG(int, status),
77 Q_ARG(bool, showTransaction));
78 assert(invoked);
79 }
80 private:
81 uint256 hash;
82 ChangeType status;
83 bool showTransaction;
84 };
85
86 // Private implementation
87 class TransactionTablePriv
88 {
89 public:
90 explicit TransactionTablePriv(TransactionTableModel *_parent) :
91 parent(_parent)
92 {
93 }
94
95 TransactionTableModel *parent;
96
97 //! Local cache of wallet sorted by transaction hash
98 QList<TransactionRecord> cachedWallet;
99
100 /** True when model finishes loading all wallet transactions on start */
101 bool m_loaded = false;
102 /** True when transactions are being notified, for instance when scanning */
103 bool m_loading = false;
104 std::vector< TransactionNotification > vQueueNotifications;
105
106 void NotifyTransactionChanged(const uint256 &hash, ChangeType status);
107 void DispatchNotifications();
108
109 /* Query entire wallet anew from core.
110 */
111 void refreshWallet(interfaces::Wallet& wallet)
112 {
113 assert(!m_loaded);
114 {
115 for (const auto& wtx : wallet.getWalletTxs()) {
116 if (TransactionRecord::showTransaction()) {
117 cachedWallet.append(TransactionRecord::decomposeTransaction(wtx));
118 }
119 }
120 }
121 m_loaded = true;
122 DispatchNotifications();
123 }
124
125 /* Update our model of the wallet incrementally, to synchronize our model of the wallet
126 with that of the core.
127
128 Call with transaction that was added, removed or changed.
129 */
130 void updateWallet(interfaces::Wallet& wallet, const uint256 &hash, int status, bool showTransaction)
131 {
132 qDebug() << "TransactionTablePriv::updateWallet: " + QString::fromStdString(hash.ToString()) + " " + QString::number(status);
133
134 // Find bounds of this transaction in model
135 QList<TransactionRecord>::iterator lower = std::lower_bound(
136 cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan());
137 QList<TransactionRecord>::iterator upper = std::upper_bound(
138 cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan());
139 int lowerIndex = (lower - cachedWallet.begin());
140 int upperIndex = (upper - cachedWallet.begin());
141 bool inModel = (lower != upper);
142
143 if(status == CT_UPDATED)
144 {
145 if(showTransaction && !inModel)
146 status = CT_NEW; /* Not in model, but want to show, treat as new */
147 if(!showTransaction && inModel)
148 status = CT_DELETED; /* In model, but want to hide, treat as deleted */
149 }
150
151 qDebug() << " inModel=" + QString::number(inModel) +
152 " Index=" + QString::number(lowerIndex) + "-" + QString::number(upperIndex) +
153 " showTransaction=" + QString::number(showTransaction) + " derivedStatus=" + QString::number(status);
154
155 switch(status)
156 {
157 case CT_NEW:
158 if(inModel)
159 {
160 qWarning() << "TransactionTablePriv::updateWallet: Warning: Got CT_NEW, but transaction is already in model";
161 break;
162 }
163 if(showTransaction)
164 {
165 // Find transaction in wallet
166 interfaces::WalletTx wtx = wallet.getWalletTx(hash);
167 if(!wtx.tx)
168 {
169 qWarning() << "TransactionTablePriv::updateWallet: Warning: Got CT_NEW, but transaction is not in wallet";
170 break;
171 }
172 // Added -- insert at the right position
173 QList<TransactionRecord> toInsert =
174 TransactionRecord::decomposeTransaction(wtx);
175 if(!toInsert.isEmpty()) /* only if something to insert */
176 {
177 parent->beginInsertRows(QModelIndex(), lowerIndex, lowerIndex+toInsert.size()-1);
178 int insert_idx = lowerIndex;
179 for (const TransactionRecord &rec : toInsert)
180 {
181 cachedWallet.insert(insert_idx, rec);
182 insert_idx += 1;
183 }
184 parent->endInsertRows();
185 }
186 }
187 break;
188 case CT_DELETED:
189 if(!inModel)
190 {
191 qWarning() << "TransactionTablePriv::updateWallet: Warning: Got CT_DELETED, but transaction is not in model";
192 break;
193 }
194 // Removed -- remove entire transaction from table
195 parent->beginRemoveRows(QModelIndex(), lowerIndex, upperIndex-1);
196 cachedWallet.erase(lower, upper);
197 parent->endRemoveRows();
198 break;
199 case CT_UPDATED:
200 // Miscellaneous updates -- nothing to do, status update will take care of this, and is only computed for
201 // visible transactions.
202 for (int i = lowerIndex; i < upperIndex; i++) {
203 TransactionRecord *rec = &cachedWallet[i];
204 rec->status.needsUpdate = true;
205 }
206 break;
207 }
208 }
209
210 int size()
211 {
212 return cachedWallet.size();
213 }
214
215 TransactionRecord* index(interfaces::Wallet& wallet, const uint256& cur_block_hash, const int idx)
216 {
217 if (idx >= 0 && idx < cachedWallet.size()) {
218 TransactionRecord *rec = &cachedWallet[idx];
219
220 // If a status update is needed (blocks came in since last check),
221 // try to update the status of this transaction from the wallet.
222 // Otherwise, simply reuse the cached status.
223 interfaces::WalletTxStatus wtx;
224 int numBlocks;
225 int64_t block_time;
226 if (!cur_block_hash.IsNull() && rec->statusUpdateNeeded(cur_block_hash) && wallet.tryGetTxStatus(rec->hash, wtx, numBlocks, block_time)) {
227 rec->updateStatus(wtx, cur_block_hash, numBlocks, block_time);
228 }
229 return rec;
230 }
231 return nullptr;
232 }
233
234 QString describe(interfaces::Node& node, interfaces::Wallet& wallet, TransactionRecord* rec, LimenkaUnit unit, const QFont& font_for_money)
235 {
236 return TransactionDesc::toHTML(node, wallet, rec, unit, font_for_money);
237 }
238
239 QString getTxHex(interfaces::Wallet& wallet, TransactionRecord *rec)
240 {
241 auto tx = wallet.getTx(rec->hash);
242 if (tx) {
243 std::string strHex = EncodeHexTx(*tx);
244 return QString::fromStdString(strHex);
245 }
246 return QString();
247 }
248 };
249
250 TransactionTableModel::TransactionTableModel(const PlatformStyle *_platformStyle, WalletModel *parent):
251 QAbstractTableModel(parent),
252 walletModel(parent),
253 priv(new TransactionTablePriv(this)),
254 platformStyle(_platformStyle)
255 {
256 subscribeToCoreSignals();
257
258 columns << QString() << QString() << tr("Date") << tr("Type") << tr("Label") << LimenkaUnits::getAmountColumnTitle(walletModel->getOptionsModel()->getDisplayUnit());
259 priv->refreshWallet(walletModel->wallet());
260
261 connect(walletModel->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &TransactionTableModel::updateDisplayUnit);
262 connect(walletModel->getOptionsModel(), &OptionsModel::fontForMoneyChanged, this, &TransactionTableModel::updateDisplayUnit);
263 }
264
265 TransactionTableModel::~TransactionTableModel()
266 {
267 unsubscribeFromCoreSignals();
268 delete priv;
269 }
270
271 /** Updates the column title to "Amount (DisplayUnit)" and emits headerDataChanged() signal for table headers to react. */
272 void TransactionTableModel::updateAmountColumnTitle()
273 {
274 columns[Amount] = LimenkaUnits::getAmountColumnTitle(walletModel->getOptionsModel()->getDisplayUnit());
275 Q_EMIT headerDataChanged(Qt::Horizontal,Amount,Amount);
276 }
277
278 void TransactionTableModel::updateTransaction(const QString &hash, int status, bool showTransaction)
279 {
280 uint256 updated;
281 updated.SetHexDeprecated(hash.toStdString());
282
283 priv->updateWallet(walletModel->wallet(), updated, status, showTransaction);
284 }
285
286 void TransactionTableModel::updateConfirmations()
287 {
288 // Blocks came in since last poll.
289 // Invalidate status (number of confirmations) and (possibly) description
290 // for all rows. Qt is smart enough to only actually request the data for the
291 // visible rows.
292 Q_EMIT dataChanged(index(0, Status), index(priv->size()-1, Status));
293 Q_EMIT dataChanged(index(0, ToAddress), index(priv->size()-1, ToAddress));
294 }
295
296 int TransactionTableModel::rowCount(const QModelIndex &parent) const
297 {
298 if (parent.isValid()) {
299 return 0;
300 }
301 return priv->size();
302 }
303
304 int TransactionTableModel::columnCount(const QModelIndex &parent) const
305 {
306 if (parent.isValid()) {
307 return 0;
308 }
309 return columns.length();
310 }
311
312 QString TransactionTableModel::formatTxStatus(const TransactionRecord *wtx) const
313 {
314 QString status;
315
316 switch(wtx->status.status)
317 {
318 case TransactionStatus::Unconfirmed:
319 status = tr("Unconfirmed");
320 break;
321 case TransactionStatus::Abandoned:
322 status = tr("Abandoned");
323 break;
324 case TransactionStatus::AssumedConfirmed:
325 status = tr("Unconfirmed (%1 confirmations pending verification of historical blocks)").arg(wtx->status.depth);
326 break;
327 case TransactionStatus::Confirming:
328 status = tr("Confirming (%1 of %2 recommended confirmations)").arg(wtx->status.depth).arg(TransactionRecord::RecommendedNumConfirmations);
329 break;
330 case TransactionStatus::Confirmed:
331 status = tr("Confirmed (%1 confirmations)").arg(wtx->status.depth);
332 break;
333 case TransactionStatus::Conflicted:
334 status = tr("Conflicted");
335 break;
336 case TransactionStatus::Immature:
337 status = tr("Immature (%1 confirmations, will be available after %2)").arg(wtx->status.depth).arg(wtx->status.depth + wtx->status.matures_in);
338 break;
339 case TransactionStatus::NotAccepted:
340 status = tr("Generated but not accepted");
341 break;
342 }
343
344 return status;
345 }
346
347 QString TransactionTableModel::formatTxDate(const TransactionRecord *wtx) const
348 {
349 if(wtx->time)
350 {
351 return GUIUtil::dateTimeStr(wtx->time);
352 }
353 return QString();
354 }
355
356 /* Look up address in address book, if found return label (address)
357 otherwise just return (address)
358 */
359 QString TransactionTableModel::lookupAddress(const std::string &address, bool tooltip) const
360 {
361 QString label = walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(address));
362 QString description;
363 if(!label.isEmpty())
364 {
365 description += label;
366 }
367 if(label.isEmpty() || walletModel->getOptionsModel()->getDisplayAddresses() || tooltip)
368 {
369 description += QString(" (") + QString::fromStdString(address) + QString(")");
370 }
371 return description;
372 }
373
374 QString TransactionTableModel::formatTxType(const TransactionRecord *wtx) const
375 {
376 switch(wtx->type)
377 {
378 case TransactionRecord::RecvWithAddress:
379 return tr("Received with");
380 case TransactionRecord::RecvFromOther:
381 return tr("Received from");
382 case TransactionRecord::SendToAddress:
383 case TransactionRecord::SendToOther:
384 return tr("Sent to");
385 case TransactionRecord::Generated:
386 return tr("Mined");
387 default:
388 return QString();
389 }
390 }
391
392 QVariant TransactionTableModel::txAddressDecoration(const TransactionRecord *wtx) const
393 {
394 switch(wtx->type)
395 {
396 case TransactionRecord::Generated:
397 return QIcon(":/icons/tx_mined");
398 case TransactionRecord::RecvWithAddress:
399 case TransactionRecord::RecvFromOther:
400 return QIcon(":/icons/tx_input");
401 case TransactionRecord::SendToAddress:
402 case TransactionRecord::SendToOther:
403 return QIcon(":/icons/tx_output");
404 default:
405 return QIcon(":/icons/tx_inout");
406 }
407 }
408
409 QString TransactionTableModel::formatTxToAddress(const TransactionRecord *wtx, bool tooltip) const
410 {
411 QString watchAddress;
412 if (tooltip && wtx->involvesWatchAddress) {
413 // Mark transactions involving watch-only addresses by adding " (watch-only)"
414 watchAddress = QLatin1String(" (") + tr("watch-only") + QLatin1Char(')');
415 }
416
417 switch(wtx->type)
418 {
419 case TransactionRecord::RecvFromOther:
420 return QString::fromStdString(wtx->address) + watchAddress;
421 case TransactionRecord::RecvWithAddress:
422 case TransactionRecord::SendToAddress:
423 case TransactionRecord::Generated:
424 return lookupAddress(wtx->address, tooltip) + watchAddress;
425 case TransactionRecord::SendToOther:
426 return QString::fromStdString(wtx->address) + watchAddress;
427 default:
428 return tr("(n/a)") + watchAddress;
429 }
430 }
431
432 QVariant TransactionTableModel::addressColor(const TransactionRecord *wtx) const
433 {
434 // Show addresses without label in a less visible color
435 switch(wtx->type)
436 {
437 case TransactionRecord::RecvWithAddress:
438 case TransactionRecord::SendToAddress:
439 case TransactionRecord::Generated:
440 {
441 QString label = walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(wtx->address));
442 if(label.isEmpty())
443 return COLOR_BAREADDRESS;
444 } break;
445 default:
446 break;
447 }
448 return QVariant();
449 }
450
451 QString TransactionTableModel::formatTxAmount(const TransactionRecord *wtx, bool showUnconfirmed, LimenkaUnits::SeparatorStyle separators) const
452 {
453 QString str = LimenkaUnits::format(walletModel->getOptionsModel()->getDisplayUnit(), wtx->credit + wtx->debit, false, separators);
454 if(showUnconfirmed)
455 {
456 if(!wtx->status.countsForBalance)
457 {
458 str = QString("[") + str + QString("]");
459 }
460 }
461 return QString(str);
462 }
463
464 QVariant TransactionTableModel::txStatusDecoration(const TransactionRecord *wtx) const
465 {
466 switch(wtx->status.status)
467 {
468 case TransactionStatus::Unconfirmed:
469 case TransactionStatus::AssumedConfirmed:
470 return QIcon(":/icons/transaction_0");
471 case TransactionStatus::Abandoned:
472 return QIcon(":/icons/transaction_abandoned");
473 case TransactionStatus::Confirming:
474 switch (wtx->status.depth * 6 / TransactionRecord::RecommendedNumConfirmations)
475 {
476 case 1: return QIcon(":/icons/transaction_1");
477 case 2: return QIcon(":/icons/transaction_2");
478 case 3: return QIcon(":/icons/transaction_3");
479 case 4: return QIcon(":/icons/transaction_4");
480 default: return QIcon(":/icons/transaction_5");
481 };
482 case TransactionStatus::Confirmed:
483 return QIcon(":/icons/transaction_confirmed");
484 case TransactionStatus::Conflicted:
485 return QIcon(":/icons/transaction_conflicted");
486 case TransactionStatus::Immature: {
487 int total = wtx->status.depth + wtx->status.matures_in;
488 int part = (wtx->status.depth * 4 / total) + 1;
489 return QIcon(QString(":/icons/transaction_%1").arg(part));
490 }
491 case TransactionStatus::NotAccepted:
492 return QIcon(":/icons/transaction_0");
493 default:
494 return COLOR_BLACK;
495 }
496 }
497
498 QVariant TransactionTableModel::txWatchonlyDecoration(const TransactionRecord *wtx) const
499 {
500 if (wtx->involvesWatchAddress)
501 return QIcon(":/icons/eye");
502 else
503 return QVariant();
504 }
505
506 QString TransactionTableModel::formatTooltip(const TransactionRecord *rec) const
507 {
508 QString tooltip = formatTxStatus(rec) + QString("\n") + formatTxType(rec);
509 if(rec->type==TransactionRecord::RecvFromOther || rec->type==TransactionRecord::SendToOther ||
510 rec->type==TransactionRecord::SendToAddress || rec->type==TransactionRecord::RecvWithAddress)
511 {
512 tooltip += QString(" ") + formatTxToAddress(rec, true);
513 }
514 return tooltip;
515 }
516
517 QVariant TransactionTableModel::data(const QModelIndex &index, int role) const
518 {
519 if(!index.isValid())
520 return QVariant();
521 TransactionRecord *rec = static_cast<TransactionRecord*>(index.internalPointer());
522
523 const auto column = static_cast<ColumnIndex>(index.column());
524 switch (role) {
525 case RawDecorationRole:
526 switch (column) {
527 case Status:
528 return txStatusDecoration(rec);
529 case Watchonly:
530 return txWatchonlyDecoration(rec);
531 case Date: return {};
532 case Type: return {};
533 case ToAddress:
534 return txAddressDecoration(rec);
535 case Amount: return {};
536 } // no default case, so the compiler can warn about missing cases
537 assert(false);
538 case Qt::DecorationRole:
539 {
540 QIcon icon = qvariant_cast<QIcon>(index.data(RawDecorationRole));
541 return platformStyle->TextColorIcon(icon);
542 }
543 case Qt::DisplayRole:
544 switch (column) {
545 case Status: return {};
546 case Watchonly: return {};
547 case Date:
548 return formatTxDate(rec);
549 case Type:
550 return formatTxType(rec);
551 case ToAddress:
552 return formatTxToAddress(rec, false);
553 case Amount:
554 return formatTxAmount(rec, true, LimenkaUnits::SeparatorStyle::ALWAYS);
555 } // no default case, so the compiler can warn about missing cases
556 assert(false);
557 case Qt::EditRole:
558 // Edit role is used for sorting, so return the unformatted values
559 switch (column) {
560 case Status:
561 return QString::fromStdString(rec->status.sortKey);
562 case Date:
563 return QString::fromStdString(strprintf("%020s-%s", rec->time, rec->status.sortKey));
564 case Type:
565 return formatTxType(rec);
566 case Watchonly:
567 return (rec->involvesWatchAddress ? 1 : 0);
568 case ToAddress:
569 return formatTxToAddress(rec, true);
570 case Amount:
571 return qint64(rec->credit + rec->debit);
572 } // no default case, so the compiler can warn about missing cases
573 assert(false);
574 case Qt::FontRole:
575 if (column == Amount) {
576 const LimenkaUnit display_unit = walletModel->getOptionsModel()->getDisplayUnit();
577 return walletModel->getOptionsModel()->getFontForMoney(display_unit);
578 }
579 break;
580 case Qt::ToolTipRole:
581 return formatTooltip(rec);
582 case Qt::TextAlignmentRole:
583 return column_alignments[index.column()];
584 case Qt::ForegroundRole:
585 // Use the "danger" color for abandoned transactions
586 if(rec->status.status == TransactionStatus::Abandoned)
587 {
588 return COLOR_TX_STATUS_DANGER;
589 }
590 // Non-confirmed (but not immature) as transactions are grey
591 if(!rec->status.countsForBalance && rec->status.status != TransactionStatus::Immature)
592 {
593 return COLOR_UNCONFIRMED;
594 }
595 if(index.column() == Amount && (rec->credit+rec->debit) < 0)
596 {
597 return COLOR_NEGATIVE;
598 }
599 if(index.column() == ToAddress)
600 {
601 return addressColor(rec);
602 }
603 break;
604 case TypeRole:
605 return rec->type;
606 case DateRole:
607 return QDateTime::fromSecsSinceEpoch(rec->time);
608 case WatchonlyRole:
609 return rec->involvesWatchAddress;
610 case WatchonlyDecorationRole:
611 return txWatchonlyDecoration(rec);
612 case LongDescriptionRole:
613 {
614 const LimenkaUnit display_unit = walletModel->getOptionsModel()->getDisplayUnit();
615 const QFont font_for_money = walletModel->getOptionsModel()->getFontForMoney(display_unit);
616 return priv->describe(walletModel->node(), walletModel->wallet(), rec, display_unit, font_for_money);
617 }
618 case AddressRole:
619 return QString::fromStdString(rec->address);
620 case LabelRole:
621 return walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(rec->address));
622 case AmountRole:
623 return qint64(rec->credit + rec->debit);
624 case TxHashRole:
625 return rec->getTxHash();
626 case TxHexRole:
627 return priv->getTxHex(walletModel->wallet(), rec);
628 case TxPlainTextRole:
629 {
630 QString details;
631 QDateTime date = QDateTime::fromSecsSinceEpoch(rec->time);
632 QString txLabel = walletModel->getAddressTableModel()->labelForAddress(QString::fromStdString(rec->address));
633
634 details.append(date.toString("M/d/yy HH:mm"));
635 details.append(" ");
636 details.append(formatTxStatus(rec));
637 details.append(". ");
638 if(!formatTxType(rec).isEmpty()) {
639 details.append(formatTxType(rec));
640 details.append(" ");
641 }
642 if(!rec->address.empty()) {
643 if(txLabel.isEmpty())
644 details.append(tr("(no label)") + " ");
645 else {
646 details.append("(");
647 details.append(txLabel);
648 details.append(") ");
649 }
650 details.append(QString::fromStdString(rec->address));
651 details.append(" ");
652 }
653 details.append(formatTxAmount(rec, false, LimenkaUnits::SeparatorStyle::NEVER));
654 return details;
655 }
656 case ConfirmedRole:
657 return rec->status.status == TransactionStatus::Status::Confirming || rec->status.status == TransactionStatus::Status::Confirmed;
658 case FormattedAmountRole:
659 // Used for copy/export, so don't include separators
660 return formatTxAmount(rec, false, LimenkaUnits::SeparatorStyle::NEVER);
661 case StatusRole:
662 return rec->status.status;
663 }
664 return QVariant();
665 }
666
667 QVariant TransactionTableModel::headerData(int section, Qt::Orientation orientation, int role) const
668 {
669 if(orientation == Qt::Horizontal)
670 {
671 if(role == Qt::DisplayRole)
672 {
673 return columns[section];
674 }
675 else if (role == Qt::TextAlignmentRole)
676 {
677 return column_alignments[section];
678 } else if (role == Qt::ToolTipRole)
679 {
680 switch(section)
681 {
682 case Status:
683 return tr("Transaction status. Hover over this field to show number of confirmations.");
684 case Date:
685 return tr("Date and time that the transaction was received.");
686 case Type:
687 return tr("Type of transaction.");
688 case Watchonly:
689 return tr("Whether or not a watch-only address is involved in this transaction.");
690 case ToAddress:
691 return tr("User-defined intent/purpose of the transaction.");
692 case Amount:
693 return tr("Amount removed from or added to balance.");
694 }
695 }
696 }
697 return QVariant();
698 }
699
700 QModelIndex TransactionTableModel::index(int row, int column, const QModelIndex &parent) const
701 {
702 Q_UNUSED(parent);
703 TransactionRecord* data = priv->index(walletModel->wallet(), walletModel->getLastBlockProcessed(), row);
704 if(data)
705 {
706 return createIndex(row, column, data);
707 }
708 return QModelIndex();
709 }
710
711 void TransactionTableModel::updateDisplayUnit()
712 {
713 // emit dataChanged to update Amount column with the current unit
714 updateAmountColumnTitle();
715 Q_EMIT dataChanged(index(0, Amount), index(priv->size()-1, Amount));
716 }
717
718 void TransactionTablePriv::NotifyTransactionChanged(const uint256 &hash, ChangeType status)
719 {
720 // Find transaction in wallet
721 // Determine whether to show transaction or not (determine this here so that no relocking is needed in GUI thread)
722 bool showTransaction = TransactionRecord::showTransaction();
723
724 TransactionNotification notification(hash, status, showTransaction);
725
726 if (!m_loaded || m_loading)
727 {
728 vQueueNotifications.push_back(notification);
729 return;
730 }
731 notification.invoke(parent);
732 }
733
734 void TransactionTablePriv::DispatchNotifications()
735 {
736 if (!m_loaded || m_loading) return;
737
738 if (vQueueNotifications.size() > 10) { // prevent balloon spam, show maximum 10 balloons
739 bool invoked = QMetaObject::invokeMethod(parent, "setProcessingQueuedTransactions", Qt::QueuedConnection, Q_ARG(bool, true));
740 assert(invoked);
741 }
742 for (unsigned int i = 0; i < vQueueNotifications.size(); ++i)
743 {
744 if (vQueueNotifications.size() - i <= 10) {
745 bool invoked = QMetaObject::invokeMethod(parent, "setProcessingQueuedTransactions", Qt::QueuedConnection, Q_ARG(bool, false));
746 assert(invoked);
747 }
748
749 vQueueNotifications[i].invoke(parent);
750 }
751 vQueueNotifications.clear();
752 }
753
754 void TransactionTableModel::subscribeToCoreSignals()
755 {
756 // Connect signals to wallet
757 m_handler_transaction_changed = walletModel->wallet().handleTransactionChanged(std::bind(&TransactionTablePriv::NotifyTransactionChanged, priv, std::placeholders::_1, std::placeholders::_2));
758 m_handler_show_progress = walletModel->wallet().handleShowProgress([this](const std::string&, int progress) {
759 priv->m_loading = progress < 100;
760 priv->DispatchNotifications();
761 });
762 }
763
764 void TransactionTableModel::unsubscribeFromCoreSignals()
765 {
766 // Disconnect signals from wallet
767 m_handler_transaction_changed->disconnect();
768 m_handler_show_progress->disconnect();
769 }
770