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