diff --git a/src/qt/addresstablemodel.cpp b/src/qt/addresstablemodel.cpp index c02af16da..2497e2705 100644 --- a/src/qt/addresstablemodel.cpp +++ b/src/qt/addresstablemodel.cpp @@ -1,430 +1,432 @@ // Copyright (c) 2011-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include #include +#include + #include #include const QString AddressTableModel::Send = "S"; const QString AddressTableModel::Receive = "R"; struct AddressTableEntry { enum Type { Sending, Receiving, /* QSortFilterProxyModel will filter these out */ Hidden }; Type type; QString label; QString address; AddressTableEntry() {} AddressTableEntry(Type _type, const QString &_label, const QString &_address) : type(_type), label(_label), address(_address) {} }; struct AddressTableEntryLessThan { bool operator()(const AddressTableEntry &a, const AddressTableEntry &b) const { return a.address < b.address; } bool operator()(const AddressTableEntry &a, const QString &b) const { return a.address < b; } bool operator()(const QString &a, const AddressTableEntry &b) const { return a < b.address; } }; /* Determine address type from address purpose */ static AddressTableEntry::Type translateTransactionType(const QString &strPurpose, bool isMine) { AddressTableEntry::Type addressType = AddressTableEntry::Hidden; // "refund" addresses aren't shown, and change addresses aren't in // mapAddressBook at all. if (strPurpose == "send") { addressType = AddressTableEntry::Sending; } else if (strPurpose == "receive") { addressType = AddressTableEntry::Receiving; } else if (strPurpose == "unknown" || strPurpose == "") { // if purpose not set, guess addressType = (isMine ? AddressTableEntry::Receiving : AddressTableEntry::Sending); } return addressType; } // Private implementation class AddressTablePriv { public: QList cachedAddressTable; AddressTableModel *parent; AddressTablePriv(AddressTableModel *_parent) : parent(_parent) {} void refreshAddressTable(interfaces::Wallet &wallet) { cachedAddressTable.clear(); for (const auto &address : wallet.getAddresses()) { AddressTableEntry::Type addressType = translateTransactionType( QString::fromStdString(address.purpose), address.is_mine); cachedAddressTable.append(AddressTableEntry( addressType, QString::fromStdString(address.name), QString::fromStdString(EncodeCashAddr( address.dest, parent->walletModel->getChainParams())))); } - // qLowerBound() and qUpperBound() require our cachedAddressTable list - // to be sorted in asc order. Even though the map is already sorted this - // re-sorting step is needed because the originating map is sorted by - // binary address, not by base58() address. - qSort(cachedAddressTable.begin(), cachedAddressTable.end(), - AddressTableEntryLessThan()); + // std::lower_bound() and std::upper_bound() require our + // cachedAddressTable list to be sorted in asc order Even though the map + // is already sorted this re-sorting step is needed because the + // originating map is sorted by binary address, not by base58() address. + std::sort(cachedAddressTable.begin(), cachedAddressTable.end(), + AddressTableEntryLessThan()); } void updateEntry(const QString &address, const QString &label, bool isMine, const QString &purpose, int status) { // Find address / label in model - QList::iterator lower = - qLowerBound(cachedAddressTable.begin(), cachedAddressTable.end(), - address, AddressTableEntryLessThan()); - QList::iterator upper = - qUpperBound(cachedAddressTable.begin(), cachedAddressTable.end(), - address, AddressTableEntryLessThan()); + QList::iterator lower = std::lower_bound( + cachedAddressTable.begin(), cachedAddressTable.end(), address, + AddressTableEntryLessThan()); + QList::iterator upper = std::upper_bound( + cachedAddressTable.begin(), cachedAddressTable.end(), address, + AddressTableEntryLessThan()); int lowerIndex = (lower - cachedAddressTable.begin()); int upperIndex = (upper - cachedAddressTable.begin()); bool inModel = (lower != upper); AddressTableEntry::Type newEntryType = translateTransactionType(purpose, isMine); switch (status) { case CT_NEW: if (inModel) { qWarning() << "AddressTablePriv::updateEntry: Warning: Got " "CT_NEW, but entry is already in model"; break; } parent->beginInsertRows(QModelIndex(), lowerIndex, lowerIndex); cachedAddressTable.insert( lowerIndex, AddressTableEntry(newEntryType, label, address)); parent->endInsertRows(); break; case CT_UPDATED: if (!inModel) { qWarning() << "AddressTablePriv::updateEntry: Warning: Got " "CT_UPDATED, but entry is not in model"; break; } lower->type = newEntryType; lower->label = label; parent->emitDataChanged(lowerIndex); break; case CT_DELETED: if (!inModel) { qWarning() << "AddressTablePriv::updateEntry: Warning: Got " "CT_DELETED, but entry is not in model"; break; } parent->beginRemoveRows(QModelIndex(), lowerIndex, upperIndex - 1); cachedAddressTable.erase(lower, upper); parent->endRemoveRows(); break; } } int size() { return cachedAddressTable.size(); } AddressTableEntry *index(int idx) { if (idx >= 0 && idx < cachedAddressTable.size()) { return &cachedAddressTable[idx]; } else { return nullptr; } } }; AddressTableModel::AddressTableModel(WalletModel *parent) : QAbstractTableModel(parent), walletModel(parent) { columns << tr("Label") << tr("Address"); priv = new AddressTablePriv(this); priv->refreshAddressTable(parent->wallet()); } AddressTableModel::~AddressTableModel() { delete priv; } int AddressTableModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return priv->size(); } int AddressTableModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return columns.length(); } QVariant AddressTableModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } AddressTableEntry *rec = static_cast(index.internalPointer()); if (role == Qt::DisplayRole || role == Qt::EditRole) { switch (index.column()) { case Label: if (rec->label.isEmpty() && role == Qt::DisplayRole) { return tr("(no label)"); } else { return rec->label; } case Address: return rec->address; } } else if (role == Qt::FontRole) { QFont font; if (index.column() == Address) { font = GUIUtil::fixedPitchFont(); } return font; } else if (role == TypeRole) { switch (rec->type) { case AddressTableEntry::Sending: return Send; case AddressTableEntry::Receiving: return Receive; default: break; } } return QVariant(); } bool AddressTableModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid()) { return false; } AddressTableEntry *rec = static_cast(index.internalPointer()); std::string strPurpose = (rec->type == AddressTableEntry::Sending ? "send" : "receive"); editStatus = OK; if (role == Qt::EditRole) { CTxDestination curAddress = DecodeDestination( rec->address.toStdString(), walletModel->getChainParams()); if (index.column() == Label) { // Do nothing, if old label == new label if (rec->label == value.toString()) { editStatus = NO_CHANGES; return false; } walletModel->wallet().setAddressBook( curAddress, value.toString().toStdString(), strPurpose); } else if (index.column() == Address) { CTxDestination newAddress = DecodeDestination( value.toString().toStdString(), walletModel->getChainParams()); // Refuse to set invalid address, set error status and return false if (boost::get(&newAddress)) { editStatus = INVALID_ADDRESS; return false; } // Do nothing, if old address == new address else if (newAddress == curAddress) { editStatus = NO_CHANGES; return false; } // Check for duplicate addresses to prevent accidental deletion of // addresses, if you try to paste an existing address over another // address (with a different label) if (walletModel->wallet().getAddress( newAddress, /* name= */ nullptr, /* is_mine= */ nullptr, /* purpose= */ nullptr)) { editStatus = DUPLICATE_ADDRESS; return false; } // Double-check that we're not overwriting a receiving address else if (rec->type == AddressTableEntry::Sending) { // Remove old entry walletModel->wallet().delAddressBook(curAddress); // Add new entry with new address walletModel->wallet().setAddressBook( newAddress, value.toString().toStdString(), strPurpose); } } return true; } return false; } QVariant AddressTableModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { if (role == Qt::DisplayRole && section < columns.size()) { return columns[section]; } } return QVariant(); } Qt::ItemFlags AddressTableModel::flags(const QModelIndex &index) const { if (!index.isValid()) { return 0; } AddressTableEntry *rec = static_cast(index.internalPointer()); Qt::ItemFlags retval = Qt::ItemIsSelectable | Qt::ItemIsEnabled; // Can edit address and label for sending addresses, and only label for // receiving addresses. if (rec->type == AddressTableEntry::Sending || (rec->type == AddressTableEntry::Receiving && index.column() == Label)) { retval |= Qt::ItemIsEditable; } return retval; } QModelIndex AddressTableModel::index(int row, int column, const QModelIndex &parent) const { Q_UNUSED(parent); AddressTableEntry *data = priv->index(row); if (data) { return createIndex(row, column, priv->index(row)); } else { return QModelIndex(); } } void AddressTableModel::updateEntry(const QString &address, const QString &label, bool isMine, const QString &purpose, int status) { // Update address book model from Bitcoin core priv->updateEntry(address, label, isMine, purpose, status); } QString AddressTableModel::addRow(const QString &type, const QString &label, const QString &address, const OutputType address_type) { std::string strLabel = label.toStdString(); std::string strAddress = address.toStdString(); editStatus = OK; if (type == Send) { if (!walletModel->validateAddress(address)) { editStatus = INVALID_ADDRESS; return QString(); } // Check for duplicate addresses if (walletModel->wallet().getAddress( DecodeDestination(strAddress, walletModel->getChainParams()), /* name= */ nullptr, /* is_mine= */ nullptr, /* purpose= */ nullptr)) { editStatus = DUPLICATE_ADDRESS; return QString(); } } else if (type == Receive) { // Generate a new address to associate with given label CPubKey newKey; if (!walletModel->wallet().getKeyFromPool(false /* internal */, newKey)) { WalletModel::UnlockContext ctx(walletModel->requestUnlock()); if (!ctx.isValid()) { // Unlock wallet failed or was cancelled editStatus = WALLET_UNLOCK_FAILURE; return QString(); } if (!walletModel->wallet().getKeyFromPool(false /* internal */, newKey)) { editStatus = KEY_GENERATION_FAILURE; return QString(); } } walletModel->wallet().learnRelatedScripts(newKey, address_type); strAddress = EncodeCashAddr(GetDestinationForKey(newKey, address_type), walletModel->getChainParams()); } else { return QString(); } // Add entry walletModel->wallet().setAddressBook( DecodeDestination(strAddress, walletModel->getChainParams()), strLabel, (type == Send ? "send" : "receive")); return QString::fromStdString(strAddress); } bool AddressTableModel::removeRows(int row, int count, const QModelIndex &parent) { Q_UNUSED(parent); AddressTableEntry *rec = priv->index(row); if (count != 1 || !rec || rec->type == AddressTableEntry::Receiving) { // Can only remove one row at a time, and cannot remove rows not in // model. // Also refuse to remove receiving addresses. return false; } walletModel->wallet().delAddressBook(DecodeDestination( rec->address.toStdString(), walletModel->getChainParams())); return true; } QString AddressTableModel::labelForAddress(const QString &address) const { std::string name; if (getAddressData(address, &name, /* purpose= */ nullptr)) { return QString::fromStdString(name); } return QString(); } QString AddressTableModel::purposeForAddress(const QString &address) const { std::string purpose; if (getAddressData(address, /* name= */ nullptr, &purpose)) { return QString::fromStdString(purpose); } return QString(); } bool AddressTableModel::getAddressData(const QString &address, std::string *name, std::string *purpose) const { CTxDestination destination = DecodeDestination(address.toStdString(), walletModel->getChainParams()); return walletModel->wallet().getAddress(destination, name, /* is_mine= */ nullptr, purpose); } int AddressTableModel::lookupAddress(const QString &address) const { QModelIndexList lst = match(index(0, Address, QModelIndex()), Qt::EditRole, address, 1, Qt::MatchExactly); if (lst.isEmpty()) { return -1; } else { return lst.at(0).row(); } } OutputType AddressTableModel::GetDefaultAddressType() const { return walletModel->wallet().getDefaultAddressType(); }; void AddressTableModel::emitDataChanged(int idx) { Q_EMIT dataChanged(index(idx, 0, QModelIndex()), index(idx, columns.length() - 1, QModelIndex())); } diff --git a/src/qt/bantablemodel.cpp b/src/qt/bantablemodel.cpp index 7572ed97d..646187b52 100644 --- a/src/qt/bantablemodel.cpp +++ b/src/qt/bantablemodel.cpp @@ -1,158 +1,161 @@ // Copyright (c) 2011-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include #include +#include + #include #include bool BannedNodeLessThan::operator()(const CCombinedBan &left, const CCombinedBan &right) const { const CCombinedBan *pLeft = &left; const CCombinedBan *pRight = &right; if (order == Qt::DescendingOrder) std::swap(pLeft, pRight); switch (column) { case BanTableModel::Address: return pLeft->subnet.ToString().compare(pRight->subnet.ToString()) < 0; case BanTableModel::Bantime: return pLeft->banEntry.nBanUntil < pRight->banEntry.nBanUntil; } return false; } // private implementation class BanTablePriv { public: /** Local cache of peer information */ QList cachedBanlist; /** Column to sort nodes by (default to unsorted) */ int sortColumn{-1}; /** Order (ascending or descending) to sort nodes by */ Qt::SortOrder sortOrder; /** Pull a full list of banned nodes from CNode into our cache */ void refreshBanlist(interfaces::Node &node) { banmap_t banMap; node.getBanned(banMap); cachedBanlist.clear(); cachedBanlist.reserve(banMap.size()); for (const auto &entry : banMap) { CCombinedBan banEntry; banEntry.subnet = entry.first; banEntry.banEntry = entry.second; cachedBanlist.append(banEntry); } - if (sortColumn >= 0) + if (sortColumn >= 0) { // sort cachedBanlist (use stable sort to prevent rows jumping // around unnecessarily) - qStableSort(cachedBanlist.begin(), cachedBanlist.end(), - BannedNodeLessThan(sortColumn, sortOrder)); + std::stable_sort(cachedBanlist.begin(), cachedBanlist.end(), + BannedNodeLessThan(sortColumn, sortOrder)); + } } int size() const { return cachedBanlist.size(); } CCombinedBan *index(int idx) { if (idx >= 0 && idx < cachedBanlist.size()) return &cachedBanlist[idx]; return 0; } }; BanTableModel::BanTableModel(interfaces::Node &node, ClientModel *parent) : QAbstractTableModel(parent), m_node(node), clientModel(parent) { columns << tr("IP/Netmask") << tr("Banned Until"); priv.reset(new BanTablePriv()); // load initial data refresh(); } BanTableModel::~BanTableModel() { // Intentionally left empty } int BanTableModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return priv->size(); } int BanTableModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return columns.length(); } QVariant BanTableModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); CCombinedBan *rec = static_cast(index.internalPointer()); if (role == Qt::DisplayRole) { switch (index.column()) { case Address: return QString::fromStdString(rec->subnet.ToString()); case Bantime: QDateTime date = QDateTime::fromMSecsSinceEpoch(0); date = date.addSecs(rec->banEntry.nBanUntil); return date.toString(Qt::SystemLocaleLongDate); } } return QVariant(); } QVariant BanTableModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { if (role == Qt::DisplayRole && section < columns.size()) { return columns[section]; } } return QVariant(); } Qt::ItemFlags BanTableModel::flags(const QModelIndex &index) const { if (!index.isValid()) return 0; Qt::ItemFlags retval = Qt::ItemIsSelectable | Qt::ItemIsEnabled; return retval; } QModelIndex BanTableModel::index(int row, int column, const QModelIndex &parent) const { Q_UNUSED(parent); CCombinedBan *data = priv->index(row); if (data) return createIndex(row, column, data); return QModelIndex(); } void BanTableModel::refresh() { Q_EMIT layoutAboutToBeChanged(); priv->refreshBanlist(m_node); Q_EMIT layoutChanged(); } void BanTableModel::sort(int column, Qt::SortOrder order) { priv->sortColumn = column; priv->sortOrder = order; refresh(); } bool BanTableModel::shouldShow() { return priv->size() > 0; } diff --git a/src/qt/peertablemodel.cpp b/src/qt/peertablemodel.cpp index 3e0f6aeec..7561dc8a1 100644 --- a/src/qt/peertablemodel.cpp +++ b/src/qt/peertablemodel.cpp @@ -1,218 +1,220 @@ // Copyright (c) 2011-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include #include // for cs_main +#include + #include #include #include bool NodeLessThan::operator()(const CNodeCombinedStats &left, const CNodeCombinedStats &right) const { const CNodeStats *pLeft = &(left.nodeStats); const CNodeStats *pRight = &(right.nodeStats); if (order == Qt::DescendingOrder) std::swap(pLeft, pRight); switch (column) { case PeerTableModel::NetNodeId: return pLeft->nodeid < pRight->nodeid; case PeerTableModel::Address: return pLeft->addrName.compare(pRight->addrName) < 0; case PeerTableModel::Subversion: return pLeft->cleanSubVer.compare(pRight->cleanSubVer) < 0; case PeerTableModel::Ping: return pLeft->dMinPing < pRight->dMinPing; case PeerTableModel::Sent: return pLeft->nSendBytes < pRight->nSendBytes; case PeerTableModel::Received: return pLeft->nRecvBytes < pRight->nRecvBytes; } return false; } // private implementation class PeerTablePriv { public: /** Local cache of peer information */ QList cachedNodeStats; /** Column to sort nodes by (default to unsorted) */ int sortColumn{-1}; /** Order (ascending or descending) to sort nodes by */ Qt::SortOrder sortOrder; /** Index of rows by node ID */ std::map mapNodeRows; /** Pull a full list of peers from vNodes into our cache */ void refreshPeers(interfaces::Node &node) { { cachedNodeStats.clear(); interfaces::Node::NodesStats nodes_stats; node.getNodesStats(nodes_stats); cachedNodeStats.reserve(nodes_stats.size()); for (const auto &node_stats : nodes_stats) { CNodeCombinedStats stats; stats.nodeStats = std::get<0>(node_stats); stats.fNodeStateStatsAvailable = std::get<1>(node_stats); stats.nodeStateStats = std::get<2>(node_stats); cachedNodeStats.append(stats); } } if (sortColumn >= 0) { // sort cacheNodeStats (use stable sort to prevent rows jumping // around unnecessarily) - qStableSort(cachedNodeStats.begin(), cachedNodeStats.end(), - NodeLessThan(sortColumn, sortOrder)); + std::stable_sort(cachedNodeStats.begin(), cachedNodeStats.end(), + NodeLessThan(sortColumn, sortOrder)); } // build index map mapNodeRows.clear(); int row = 0; for (const CNodeCombinedStats &stats : cachedNodeStats) { mapNodeRows.insert( std::pair(stats.nodeStats.nodeid, row++)); } } int size() const { return cachedNodeStats.size(); } CNodeCombinedStats *index(int idx) { if (idx >= 0 && idx < cachedNodeStats.size()) return &cachedNodeStats[idx]; return 0; } }; PeerTableModel::PeerTableModel(interfaces::Node &node, ClientModel *parent) : QAbstractTableModel(parent), m_node(node), clientModel(parent), timer(0) { columns << tr("NodeId") << tr("Node/Service") << tr("Ping") << tr("Sent") << tr("Received") << tr("User Agent"); priv.reset(new PeerTablePriv()); // set up timer for auto refresh timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &PeerTableModel::refresh); timer->setInterval(MODEL_UPDATE_DELAY); // load initial data refresh(); } PeerTableModel::~PeerTableModel() { // Intentionally left empty } void PeerTableModel::startAutoRefresh() { timer->start(); } void PeerTableModel::stopAutoRefresh() { timer->stop(); } int PeerTableModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return priv->size(); } int PeerTableModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return columns.length(); } QVariant PeerTableModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); CNodeCombinedStats *rec = static_cast(index.internalPointer()); if (role == Qt::DisplayRole) { switch (index.column()) { case NetNodeId: return (qint64)rec->nodeStats.nodeid; case Address: return QString::fromStdString(rec->nodeStats.addrName); case Subversion: return QString::fromStdString(rec->nodeStats.cleanSubVer); case Ping: return GUIUtil::formatPingTime(rec->nodeStats.dMinPing); case Sent: return GUIUtil::formatBytes(rec->nodeStats.nSendBytes); case Received: return GUIUtil::formatBytes(rec->nodeStats.nRecvBytes); } } else if (role == Qt::TextAlignmentRole) { switch (index.column()) { case Ping: case Sent: case Received: return QVariant(Qt::AlignRight | Qt::AlignVCenter); default: return QVariant(); } } return QVariant(); } QVariant PeerTableModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { if (role == Qt::DisplayRole && section < columns.size()) { return columns[section]; } } return QVariant(); } Qt::ItemFlags PeerTableModel::flags(const QModelIndex &index) const { if (!index.isValid()) return 0; Qt::ItemFlags retval = Qt::ItemIsSelectable | Qt::ItemIsEnabled; return retval; } QModelIndex PeerTableModel::index(int row, int column, const QModelIndex &parent) const { Q_UNUSED(parent); CNodeCombinedStats *data = priv->index(row); if (data) return createIndex(row, column, data); return QModelIndex(); } const CNodeCombinedStats *PeerTableModel::getNodeStats(int idx) { return priv->index(idx); } void PeerTableModel::refresh() { Q_EMIT layoutAboutToBeChanged(); priv->refreshPeers(m_node); Q_EMIT layoutChanged(); } int PeerTableModel::getRowByNodeId(NodeId nodeid) { std::map::iterator it = priv->mapNodeRows.find(nodeid); if (it == priv->mapNodeRows.end()) return -1; return it->second; } void PeerTableModel::sort(int column, Qt::SortOrder order) { priv->sortColumn = column; priv->sortOrder = order; refresh(); } diff --git a/src/qt/recentrequeststablemodel.cpp b/src/qt/recentrequeststablemodel.cpp index b015b8efc..15e38665a 100644 --- a/src/qt/recentrequeststablemodel.cpp +++ b/src/qt/recentrequeststablemodel.cpp @@ -1,226 +1,228 @@ // Copyright (c) 2011-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include +#include #include #include RecentRequestsTableModel::RecentRequestsTableModel(WalletModel *parent) : QAbstractTableModel(parent), walletModel(parent) { // Load entries from wallet std::vector vReceiveRequests; parent->loadReceiveRequests(vReceiveRequests); for (const std::string &request : vReceiveRequests) { addNewRequest(request); } /* These columns must match the indices in the ColumnIndex enumeration */ columns << tr("Date") << tr("Label") << tr("Message") << getAmountTitle(); connect(walletModel->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &RecentRequestsTableModel::updateDisplayUnit); } RecentRequestsTableModel::~RecentRequestsTableModel() { /* Intentionally left empty */ } int RecentRequestsTableModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return list.length(); } int RecentRequestsTableModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return columns.length(); } QVariant RecentRequestsTableModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= list.length()) return QVariant(); if (role == Qt::DisplayRole || role == Qt::EditRole) { const RecentRequestEntry *rec = &list[index.row()]; switch (index.column()) { case Date: return GUIUtil::dateTimeStr(rec->date); case Label: if (rec->recipient.label.isEmpty() && role == Qt::DisplayRole) { return tr("(no label)"); } else { return rec->recipient.label; } case Message: if (rec->recipient.message.isEmpty() && role == Qt::DisplayRole) { return tr("(no message)"); } else { return rec->recipient.message; } case Amount: if (rec->recipient.amount == ::Amount::zero() && role == Qt::DisplayRole) return tr("(no amount requested)"); else if (role == Qt::EditRole) return BitcoinUnits::format( walletModel->getOptionsModel()->getDisplayUnit(), rec->recipient.amount, false, BitcoinUnits::separatorNever); else return BitcoinUnits::format( walletModel->getOptionsModel()->getDisplayUnit(), rec->recipient.amount); } } else if (role == Qt::TextAlignmentRole) { if (index.column() == Amount) return (int)(Qt::AlignRight | Qt::AlignVCenter); } return QVariant(); } bool RecentRequestsTableModel::setData(const QModelIndex &index, const QVariant &value, int role) { return true; } QVariant RecentRequestsTableModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { if (role == Qt::DisplayRole && section < columns.size()) { return columns[section]; } } return QVariant(); } /** Updates the column title to "Amount (DisplayUnit)" and emits * headerDataChanged() signal for table headers to react. */ void RecentRequestsTableModel::updateAmountColumnTitle() { columns[Amount] = getAmountTitle(); Q_EMIT headerDataChanged(Qt::Horizontal, Amount, Amount); } /** Gets title for amount column including current display unit if optionsModel * reference available. */ QString RecentRequestsTableModel::getAmountTitle() { return (this->walletModel->getOptionsModel() != nullptr) ? tr("Requested") + " (" + BitcoinUnits::name(this->walletModel->getOptionsModel() ->getDisplayUnit()) + ")" : ""; } QModelIndex RecentRequestsTableModel::index(int row, int column, const QModelIndex &parent) const { Q_UNUSED(parent); return createIndex(row, column); } bool RecentRequestsTableModel::removeRows(int row, int count, const QModelIndex &parent) { Q_UNUSED(parent); if (count > 0 && row >= 0 && (row + count) <= list.size()) { for (int i = 0; i < count; ++i) { const RecentRequestEntry *rec = &list[row + i]; if (!walletModel->saveReceiveRequest( rec->recipient.address.toStdString(), rec->id, "")) { return false; } } beginRemoveRows(parent, row, row + count - 1); list.erase(list.begin() + row, list.begin() + row + count); endRemoveRows(); return true; } else { return false; } } Qt::ItemFlags RecentRequestsTableModel::flags(const QModelIndex &index) const { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } // called when adding a request from the GUI void RecentRequestsTableModel::addNewRequest( const SendCoinsRecipient &recipient) { RecentRequestEntry newEntry; newEntry.id = ++nReceiveRequestsMaxId; newEntry.date = QDateTime::currentDateTime(); newEntry.recipient = recipient; CDataStream ss(SER_DISK, CLIENT_VERSION); ss << newEntry; if (!walletModel->saveReceiveRequest(recipient.address.toStdString(), newEntry.id, ss.str())) return; addNewRequest(newEntry); } // called from ctor when loading from wallet void RecentRequestsTableModel::addNewRequest(const std::string &recipient) { std::vector data(recipient.begin(), recipient.end()); CDataStream ss(data, SER_DISK, CLIENT_VERSION); RecentRequestEntry entry; ss >> entry; // should not happen if (entry.id == 0) return; if (entry.id > nReceiveRequestsMaxId) nReceiveRequestsMaxId = entry.id; addNewRequest(entry); } // actually add to table in GUI void RecentRequestsTableModel::addNewRequest(RecentRequestEntry &recipient) { beginInsertRows(QModelIndex(), 0, 0); list.prepend(recipient); endInsertRows(); } void RecentRequestsTableModel::sort(int column, Qt::SortOrder order) { - qSort(list.begin(), list.end(), RecentRequestEntryLessThan(column, order)); + std::sort(list.begin(), list.end(), + RecentRequestEntryLessThan(column, order)); Q_EMIT dataChanged( index(0, 0, QModelIndex()), index(list.size() - 1, NUMBER_OF_COLUMNS - 1, QModelIndex())); } void RecentRequestsTableModel::updateDisplayUnit() { updateAmountColumnTitle(); } bool RecentRequestEntryLessThan::operator()(RecentRequestEntry &left, RecentRequestEntry &right) const { RecentRequestEntry *pLeft = &left; RecentRequestEntry *pRight = &right; if (order == Qt::DescendingOrder) std::swap(pLeft, pRight); switch (column) { case RecentRequestsTableModel::Date: return pLeft->date.toTime_t() < pRight->date.toTime_t(); case RecentRequestsTableModel::Label: return pLeft->recipient.label < pRight->recipient.label; case RecentRequestsTableModel::Message: return pLeft->recipient.message < pRight->recipient.message; case RecentRequestsTableModel::Amount: return pLeft->recipient.amount < pRight->recipient.amount; default: return pLeft->id < pRight->id; } } diff --git a/src/qt/transactiontablemodel.cpp b/src/qt/transactiontablemodel.cpp index 981e670e5..4c87c1d94 100644 --- a/src/qt/transactiontablemodel.cpp +++ b/src/qt/transactiontablemodel.cpp @@ -1,771 +1,773 @@ // Copyright (c) 2011-2016 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include + #include #include #include #include #include // Amount column is right-aligned it contains numbers static int column_alignments[] = { Qt::AlignLeft | Qt::AlignVCenter, /* status */ Qt::AlignLeft | Qt::AlignVCenter, /* watchonly */ Qt::AlignLeft | Qt::AlignVCenter, /* date */ Qt::AlignLeft | Qt::AlignVCenter, /* type */ Qt::AlignLeft | Qt::AlignVCenter, /* address */ Qt::AlignRight | Qt::AlignVCenter /* amount */ }; // Comparison operator for sort/binary search of model tx list struct TxLessThan { bool operator()(const TransactionRecord &a, const TransactionRecord &b) const { return a.txid < b.txid; } bool operator()(const TransactionRecord &a, const TxId &b) const { return a.txid < b; } bool operator()(const TxId &a, const TransactionRecord &b) const { return a < b.txid; } }; // Private implementation class TransactionTablePriv { public: TransactionTablePriv(TransactionTableModel *_parent) : parent(_parent) {} TransactionTableModel *parent; /* Local cache of wallet. * As it is in the same order as the CWallet, by definition this is sorted * by sha256. */ QList cachedWallet; /** * Query entire wallet anew from core. */ void refreshWallet(interfaces::Wallet &wallet) { qDebug() << "TransactionTablePriv::refreshWallet"; cachedWallet.clear(); for (const auto &wtx : wallet.getWalletTxs()) { if (TransactionRecord::showTransaction()) { cachedWallet.append( TransactionRecord::decomposeTransaction(wtx)); } } } /** * Update our model of the wallet incrementally, to synchronize our model of * the wallet with that of the core. * Call with transaction that was added, removed or changed. */ void updateWallet(interfaces::Wallet &wallet, const TxId &txid, int status, bool showTransaction) { qDebug() << "TransactionTablePriv::updateWallet: " + QString::fromStdString(txid.ToString()) + " " + QString::number(status); // Find bounds of this transaction in model - QList::iterator lower = qLowerBound( + QList::iterator lower = std::lower_bound( cachedWallet.begin(), cachedWallet.end(), txid, TxLessThan()); - QList::iterator upper = qUpperBound( + QList::iterator upper = std::upper_bound( cachedWallet.begin(), cachedWallet.end(), txid, TxLessThan()); int lowerIndex = (lower - cachedWallet.begin()); int upperIndex = (upper - cachedWallet.begin()); bool inModel = (lower != upper); if (status == CT_UPDATED) { // Not in model, but want to show, treat as new. if (showTransaction && !inModel) { status = CT_NEW; } // In model, but want to hide, treat as deleted. if (!showTransaction && inModel) { status = CT_DELETED; } } qDebug() << " inModel=" + QString::number(inModel) + " Index=" + QString::number(lowerIndex) + "-" + QString::number(upperIndex) + " showTransaction=" + QString::number(showTransaction) + " derivedStatus=" + QString::number(status); switch (status) { case CT_NEW: if (inModel) { qWarning() << "TransactionTablePriv::updateWallet: " "Warning: Got CT_NEW, but transaction is " "already in model"; break; } if (showTransaction) { // Find transaction in wallet interfaces::WalletTx wtx = wallet.getWalletTx(txid); if (!wtx.tx) { qWarning() << "TransactionTablePriv::updateWallet: " "Warning: Got CT_NEW, but transaction is " "not in wallet"; break; } // Added -- insert at the right position QList toInsert = TransactionRecord::decomposeTransaction(wtx); /* only if something to insert */ if (!toInsert.isEmpty()) { parent->beginInsertRows(QModelIndex(), lowerIndex, lowerIndex + toInsert.size() - 1); int insert_idx = lowerIndex; for (const TransactionRecord &rec : toInsert) { cachedWallet.insert(insert_idx, rec); insert_idx += 1; } parent->endInsertRows(); } } break; case CT_DELETED: if (!inModel) { qWarning() << "TransactionTablePriv::updateWallet: " "Warning: Got CT_DELETED, but transaction is " "not in model"; break; } // Removed -- remove entire transaction from table parent->beginRemoveRows(QModelIndex(), lowerIndex, upperIndex - 1); cachedWallet.erase(lower, upper); parent->endRemoveRows(); break; case CT_UPDATED: // Miscellaneous updates -- nothing to do, status update will // take care of this, and is only computed for visible // transactions. break; } } int size() { return cachedWallet.size(); } TransactionRecord *index(interfaces::Wallet &wallet, int idx) { if (idx >= 0 && idx < cachedWallet.size()) { TransactionRecord *rec = &cachedWallet[idx]; // Get required locks upfront. This avoids the GUI from getting // stuck if the core is holding the locks for a longer time - for // example, during a wallet rescan. // // If a status update is needed (blocks came in since last check), // update the status of this transaction from the wallet. Otherwise, // simply re-use the cached status. interfaces::WalletTxStatus wtx; int numBlocks; int64_t block_time; if (wallet.tryGetTxStatus(rec->txid, wtx, numBlocks, block_time) && rec->statusUpdateNeeded(numBlocks)) { rec->updateStatus(wtx, numBlocks, block_time); } return rec; } return 0; } QString describe(interfaces::Node &node, interfaces::Wallet &wallet, TransactionRecord *rec, int unit) { return TransactionDesc::toHTML(node, wallet, rec, unit); } QString getTxHex(interfaces::Wallet &wallet, TransactionRecord *rec) { auto tx = wallet.getTx(rec->txid); if (tx) { std::string strHex = EncodeHexTx(*tx); return QString::fromStdString(strHex); } return QString(); } }; TransactionTableModel::TransactionTableModel( const PlatformStyle *_platformStyle, WalletModel *parent) : QAbstractTableModel(parent), walletModel(parent), priv(new TransactionTablePriv(this)), fProcessingQueuedTransactions(false), platformStyle(_platformStyle) { columns << QString() << QString() << tr("Date") << tr("Type") << tr("Label") << BitcoinUnits::getAmountColumnTitle( walletModel->getOptionsModel()->getDisplayUnit()); priv->refreshWallet(walletModel->wallet()); connect(walletModel->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &TransactionTableModel::updateDisplayUnit); subscribeToCoreSignals(); } TransactionTableModel::~TransactionTableModel() { unsubscribeFromCoreSignals(); delete priv; } /** Updates the column title to "Amount (DisplayUnit)" and emits * headerDataChanged() signal for table headers to react. */ void TransactionTableModel::updateAmountColumnTitle() { columns[Amount] = BitcoinUnits::getAmountColumnTitle( walletModel->getOptionsModel()->getDisplayUnit()); Q_EMIT headerDataChanged(Qt::Horizontal, Amount, Amount); } void TransactionTableModel::updateTransaction(const QString &hash, int status, bool showTransaction) { TxId updated; updated.SetHex(hash.toStdString()); priv->updateWallet(walletModel->wallet(), updated, status, showTransaction); } void TransactionTableModel::updateConfirmations() { // Blocks came in since last poll. // Invalidate status (number of confirmations) and (possibly) description // for all rows. Qt is smart enough to only actually request the data for // the visible rows. Q_EMIT dataChanged(index(0, Status), index(priv->size() - 1, Status)); Q_EMIT dataChanged(index(0, ToAddress), index(priv->size() - 1, ToAddress)); } int TransactionTableModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); return priv->size(); } int TransactionTableModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return columns.length(); } QString TransactionTableModel::formatTxStatus(const TransactionRecord *wtx) const { QString status; switch (wtx->status.status) { case TransactionStatus::OpenUntilBlock: status = tr("Open for %n more block(s)", "", wtx->status.open_for); break; case TransactionStatus::OpenUntilDate: status = tr("Open until %1") .arg(GUIUtil::dateTimeStr(wtx->status.open_for)); break; case TransactionStatus::Unconfirmed: status = tr("Unconfirmed"); break; case TransactionStatus::Abandoned: status = tr("Abandoned"); break; case TransactionStatus::Confirming: status = tr("Confirming (%1 of %2 recommended confirmations)") .arg(wtx->status.depth) .arg(TransactionRecord::RecommendedNumConfirmations); break; case TransactionStatus::Confirmed: status = tr("Confirmed (%1 confirmations)").arg(wtx->status.depth); break; case TransactionStatus::Conflicted: status = tr("Conflicted"); break; case TransactionStatus::Immature: status = tr("Immature (%1 confirmations, will be available after %2)") .arg(wtx->status.depth) .arg(wtx->status.depth + wtx->status.matures_in); break; case TransactionStatus::NotAccepted: status = tr("Generated but not accepted"); break; } return status; } QString TransactionTableModel::formatTxDate(const TransactionRecord *wtx) const { if (wtx->time) { return GUIUtil::dateTimeStr(wtx->time); } return QString(); } /** * Look up address in address book, if found return label (address) otherwise * just return (address) */ QString TransactionTableModel::lookupAddress(const std::string &address, bool tooltip) const { QString label = walletModel->getAddressTableModel()->labelForAddress( QString::fromStdString(address)); QString description; if (!label.isEmpty()) { description += label; } if (label.isEmpty() || tooltip) { description += QString(" (") + QString::fromStdString(address) + QString(")"); } return description; } QString TransactionTableModel::formatTxType(const TransactionRecord *wtx) const { switch (wtx->type) { case TransactionRecord::RecvWithAddress: return tr("Received with"); case TransactionRecord::RecvFromOther: return tr("Received from"); case TransactionRecord::SendToAddress: case TransactionRecord::SendToOther: return tr("Sent to"); case TransactionRecord::SendToSelf: return tr("Payment to yourself"); case TransactionRecord::Generated: return tr("Mined"); default: return QString(); } } QVariant TransactionTableModel::txAddressDecoration(const TransactionRecord *wtx) const { switch (wtx->type) { case TransactionRecord::Generated: return QIcon(":/icons/tx_mined"); case TransactionRecord::RecvWithAddress: case TransactionRecord::RecvFromOther: return QIcon(":/icons/tx_input"); case TransactionRecord::SendToAddress: case TransactionRecord::SendToOther: return QIcon(":/icons/tx_output"); default: return QIcon(":/icons/tx_inout"); } } QString TransactionTableModel::formatTxToAddress(const TransactionRecord *wtx, bool tooltip) const { QString watchAddress; if (tooltip) { // Mark transactions involving watch-only addresses by adding " // (watch-only)" watchAddress = wtx->involvesWatchAddress ? QString(" (") + tr("watch-only") + QString(")") : ""; } switch (wtx->type) { case TransactionRecord::RecvFromOther: return QString::fromStdString(wtx->address) + watchAddress; case TransactionRecord::RecvWithAddress: case TransactionRecord::SendToAddress: case TransactionRecord::Generated: return lookupAddress(wtx->address, tooltip) + watchAddress; case TransactionRecord::SendToOther: return QString::fromStdString(wtx->address) + watchAddress; case TransactionRecord::SendToSelf: default: return tr("(n/a)") + watchAddress; } } QVariant TransactionTableModel::addressColor(const TransactionRecord *wtx) const { // Show addresses without label in a less visible color switch (wtx->type) { case TransactionRecord::RecvWithAddress: case TransactionRecord::SendToAddress: case TransactionRecord::Generated: { QString label = walletModel->getAddressTableModel()->labelForAddress( QString::fromStdString(wtx->address)); if (label.isEmpty()) return COLOR_BAREADDRESS; } break; case TransactionRecord::SendToSelf: return COLOR_BAREADDRESS; default: break; } return QVariant(); } QString TransactionTableModel::formatTxAmount( const TransactionRecord *wtx, bool showUnconfirmed, BitcoinUnits::SeparatorStyle separators) const { QString str = BitcoinUnits::format(walletModel->getOptionsModel()->getDisplayUnit(), wtx->credit + wtx->debit, false, separators); if (showUnconfirmed) { if (!wtx->status.countsForBalance) { str = QString("[") + str + QString("]"); } } return QString(str); } QVariant TransactionTableModel::txStatusDecoration(const TransactionRecord *wtx) const { switch (wtx->status.status) { case TransactionStatus::OpenUntilBlock: case TransactionStatus::OpenUntilDate: return COLOR_TX_STATUS_OPENUNTILDATE; case TransactionStatus::Unconfirmed: return QIcon(":/icons/transaction_0"); case TransactionStatus::Abandoned: return QIcon(":/icons/transaction_abandoned"); case TransactionStatus::Confirming: switch (wtx->status.depth) { case 1: return QIcon(":/icons/transaction_1"); case 2: return QIcon(":/icons/transaction_2"); case 3: return QIcon(":/icons/transaction_3"); case 4: return QIcon(":/icons/transaction_4"); default: return QIcon(":/icons/transaction_5"); }; case TransactionStatus::Confirmed: return QIcon(":/icons/transaction_confirmed"); case TransactionStatus::Conflicted: return QIcon(":/icons/transaction_conflicted"); case TransactionStatus::Immature: { int total = wtx->status.depth + wtx->status.matures_in; int part = (wtx->status.depth * 4 / total) + 1; return QIcon(QString(":/icons/transaction_%1").arg(part)); } case TransactionStatus::NotAccepted: return QIcon(":/icons/transaction_0"); default: return COLOR_BLACK; } } QVariant TransactionTableModel::txWatchonlyDecoration( const TransactionRecord *wtx) const { if (wtx->involvesWatchAddress) { return QIcon(":/icons/eye"); } return QVariant(); } QString TransactionTableModel::formatTooltip(const TransactionRecord *rec) const { QString tooltip = formatTxStatus(rec) + QString("\n") + formatTxType(rec); if (rec->type == TransactionRecord::RecvFromOther || rec->type == TransactionRecord::SendToOther || rec->type == TransactionRecord::SendToAddress || rec->type == TransactionRecord::RecvWithAddress) { tooltip += QString(" ") + formatTxToAddress(rec, true); } return tooltip; } QVariant TransactionTableModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } TransactionRecord *rec = static_cast(index.internalPointer()); switch (role) { case RawDecorationRole: switch (index.column()) { case Status: return txStatusDecoration(rec); case Watchonly: return txWatchonlyDecoration(rec); case ToAddress: return txAddressDecoration(rec); } break; case Qt::DecorationRole: { QIcon icon = qvariant_cast(index.data(RawDecorationRole)); return platformStyle->TextColorIcon(icon); } case Qt::DisplayRole: switch (index.column()) { case Date: return formatTxDate(rec); case Type: return formatTxType(rec); case ToAddress: return formatTxToAddress(rec, false); case Amount: return formatTxAmount(rec, true, BitcoinUnits::separatorAlways); } break; case Qt::EditRole: // Edit role is used for sorting, so return the unformatted values switch (index.column()) { case Status: return QString::fromStdString(rec->status.sortKey); case Date: return rec->time; case Type: return formatTxType(rec); case Watchonly: return (rec->involvesWatchAddress ? 1 : 0); case ToAddress: return formatTxToAddress(rec, true); case Amount: return qint64((rec->credit + rec->debit) / SATOSHI); } break; case Qt::ToolTipRole: return formatTooltip(rec); case Qt::TextAlignmentRole: return column_alignments[index.column()]; case Qt::ForegroundRole: // Use the "danger" color for abandoned transactions if (rec->status.status == TransactionStatus::Abandoned) { return COLOR_TX_STATUS_DANGER; } // Non-confirmed (but not immature) as transactions are grey if (!rec->status.countsForBalance && rec->status.status != TransactionStatus::Immature) { return COLOR_UNCONFIRMED; } if (index.column() == Amount && (rec->credit + rec->debit) < ::Amount::zero()) { return COLOR_NEGATIVE; } if (index.column() == ToAddress) { return addressColor(rec); } break; case TypeRole: return rec->type; case DateRole: return QDateTime::fromTime_t(static_cast(rec->time)); case WatchonlyRole: return rec->involvesWatchAddress; case WatchonlyDecorationRole: return txWatchonlyDecoration(rec); case LongDescriptionRole: return priv->describe( walletModel->node(), walletModel->wallet(), rec, walletModel->getOptionsModel()->getDisplayUnit()); case AddressRole: return QString::fromStdString(rec->address); case LabelRole: return walletModel->getAddressTableModel()->labelForAddress( QString::fromStdString(rec->address)); case AmountRole: return qint64((rec->credit + rec->debit) / SATOSHI); case TxIDRole: return rec->getTxID(); case TxHashRole: return QString::fromStdString(rec->txid.ToString()); case TxHexRole: return priv->getTxHex(walletModel->wallet(), rec); case TxPlainTextRole: { QString details; QDateTime date = QDateTime::fromTime_t(static_cast(rec->time)); QString txLabel = walletModel->getAddressTableModel()->labelForAddress( QString::fromStdString(rec->address)); details.append(date.toString("M/d/yy HH:mm")); details.append(" "); details.append(formatTxStatus(rec)); details.append(". "); if (!formatTxType(rec).isEmpty()) { details.append(formatTxType(rec)); details.append(" "); } if (!rec->address.empty()) { if (txLabel.isEmpty()) { details.append(tr("(no label)") + " "); } else { details.append("("); details.append(txLabel); details.append(") "); } details.append(QString::fromStdString(rec->address)); details.append(" "); } details.append( formatTxAmount(rec, false, BitcoinUnits::separatorNever)); return details; } case ConfirmedRole: return rec->status.countsForBalance; case FormattedAmountRole: // Used for copy/export, so don't include separators return formatTxAmount(rec, false, BitcoinUnits::separatorNever); case StatusRole: return rec->status.status; } return QVariant(); } QVariant TransactionTableModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { if (role == Qt::DisplayRole) { return columns[section]; } else if (role == Qt::TextAlignmentRole) { return column_alignments[section]; } else if (role == Qt::ToolTipRole) { switch (section) { case Status: return tr("Transaction status. Hover over this field to " "show number of confirmations."); case Date: return tr( "Date and time that the transaction was received."); case Type: return tr("Type of transaction."); case Watchonly: return tr("Whether or not a watch-only address is involved " "in this transaction."); case ToAddress: return tr( "User-defined intent/purpose of the transaction."); case Amount: return tr("Amount removed from or added to balance."); } } } return QVariant(); } QModelIndex TransactionTableModel::index(int row, int column, const QModelIndex &parent) const { Q_UNUSED(parent); TransactionRecord *data = priv->index(walletModel->wallet(), row); if (data) { return createIndex(row, column, priv->index(walletModel->wallet(), row)); } return QModelIndex(); } void TransactionTableModel::updateDisplayUnit() { // emit dataChanged to update Amount column with the current unit updateAmountColumnTitle(); Q_EMIT dataChanged(index(0, Amount), index(priv->size() - 1, Amount)); } // queue notifications to show a non freezing progress dialog e.g. for rescan struct TransactionNotification { public: TransactionNotification() {} TransactionNotification(TxId _txid, ChangeType _status, bool _showTransaction) : txid(_txid), status(_status), showTransaction(_showTransaction) {} void invoke(QObject *ttm) { QString strHash = QString::fromStdString(txid.GetHex()); qDebug() << "NotifyTransactionChanged: " + strHash + " status= " + QString::number(status); QMetaObject::invokeMethod(ttm, "updateTransaction", Qt::QueuedConnection, Q_ARG(QString, strHash), Q_ARG(int, status), Q_ARG(bool, showTransaction)); } private: TxId txid; ChangeType status; bool showTransaction; }; static bool fQueueNotifications = false; static std::vector vQueueNotifications; static void NotifyTransactionChanged(TransactionTableModel *ttm, const TxId &txid, ChangeType status) { // Find transaction in wallet // Determine whether to show transaction or not (determine this here so that // no relocking is needed in GUI thread) bool showTransaction = TransactionRecord::showTransaction(); TransactionNotification notification(txid, status, showTransaction); if (fQueueNotifications) { vQueueNotifications.push_back(notification); return; } notification.invoke(ttm); } static void ShowProgress(TransactionTableModel *ttm, const std::string &title, int nProgress) { if (nProgress == 0) { fQueueNotifications = true; } if (nProgress == 100) { fQueueNotifications = false; if (vQueueNotifications.size() > 10) { // prevent balloon spam, show maximum 10 balloons QMetaObject::invokeMethod(ttm, "setProcessingQueuedTransactions", Qt::QueuedConnection, Q_ARG(bool, true)); } for (size_t i = 0; i < vQueueNotifications.size(); ++i) { if (vQueueNotifications.size() - i <= 10) { QMetaObject::invokeMethod( ttm, "setProcessingQueuedTransactions", Qt::QueuedConnection, Q_ARG(bool, false)); } vQueueNotifications[i].invoke(ttm); } // clear std::vector().swap(vQueueNotifications); } } void TransactionTableModel::subscribeToCoreSignals() { // Connect signals to wallet m_handler_transaction_changed = walletModel->wallet().handleTransactionChanged( std::bind(NotifyTransactionChanged, this, std::placeholders::_1, std::placeholders::_2)); m_handler_show_progress = walletModel->wallet().handleShowProgress(std::bind( ShowProgress, this, std::placeholders::_1, std::placeholders::_2)); } void TransactionTableModel::unsubscribeFromCoreSignals() { // Disconnect signals from wallet m_handler_transaction_changed->disconnect(); m_handler_show_progress->disconnect(); }