diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index ffbfce6837a4..3dc81d225e5b 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -66,6 +66,7 @@ QT_MOC_CPP = \ qt/moc_macdockiconhandler.cpp \ qt/moc_macnotificationhandler.cpp \ qt/moc_masternodelist.cpp \ + qt/moc_masternodemodel.cpp \ qt/moc_mnemonicverificationdialog.cpp \ qt/moc_modaloverlay.cpp \ qt/moc_notificator.cpp \ @@ -147,6 +148,7 @@ BITCOIN_QT_H = \ qt/macnotificationhandler.h \ qt/macos_appnap.h \ qt/masternodelist.h \ + qt/masternodemodel.h \ qt/mnemonicverificationdialog.h \ qt/modaloverlay.h \ qt/networkstyle.h \ @@ -264,6 +266,7 @@ BITCOIN_QT_WALLET_CPP = \ qt/editaddressdialog.cpp \ qt/governancelist.cpp \ qt/masternodelist.cpp \ + qt/masternodemodel.cpp \ qt/mnemonicverificationdialog.cpp \ qt/openuridialog.cpp \ qt/overviewpage.cpp \ diff --git a/src/qt/forms/masternodelist.ui b/src/qt/forms/masternodelist.ui index 672b8199732d..6ad419c8c709 100644 --- a/src/qt/forms/masternodelist.ui +++ b/src/qt/forms/masternodelist.ui @@ -31,19 +31,6 @@ 0 - - - - Qt::Horizontal - - - - 40 - 20 - - - - @@ -52,14 +39,29 @@ 0 - - - Filter List: + + + Filter by masternode type + + + All + + + + + Regular + + + + + Evo + + - + Filter masternode list @@ -69,46 +71,32 @@ - + Show only masternodes this wallet has keys for. - My masternodes only + Owned - - - Qt::Horizontal - - - - 10 - 20 - + + + Hide masternodes that are currently PoSe banned. - - - - - Node Count: + Hide banned - - - - - - 0 + + true - + QAbstractItemView::NoEditTriggers @@ -124,71 +112,42 @@ true - - true - - - - Service - - - - - Type - - - - - Status - - - - - PoSe Score - - - - - Registered - - - - - Last Paid - - - - - Next Payment - - - - - Payout Address - - - - - Operator Reward - - - - - Collateral Address - - - - - Owner Address - - - - - Voting Address - - + + + + 3 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Node Count: + + + + + + + 0 + + + + + diff --git a/src/qt/masternodelist.cpp b/src/qt/masternodelist.cpp index a8e3f57d33d7..ec293e8ce526 100644 --- a/src/qt/masternodelist.cpp +++ b/src/qt/masternodelist.cpp @@ -7,149 +7,187 @@ #include #include +#include + #include +#include #include #include +#include #include -#include - +#include #include -#include -#include +#include + +namespace { +constexpr int MASTERNODELIST_UPDATE_SECONDS{3}; +} // anonymous namespace -template -class CMasternodeListWidgetItem : public QTableWidgetItem +bool MasternodeListSortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { - T itemData; + // "Type" filter + if (m_type_filter != TypeFilter::All) { + QModelIndex idx = sourceModel()->index(source_row, MasternodeModel::TYPE, source_parent); + int type = sourceModel()->data(idx, Qt::EditRole).toInt(); + if (m_type_filter == TypeFilter::Regular && type != static_cast(MnType::Regular)) { + return false; + } + if (m_type_filter == TypeFilter::Evo && type != static_cast(MnType::Evo)) { + return false; + } + } -public: - explicit CMasternodeListWidgetItem(const QString& text, const T& data, int type = Type) : - QTableWidgetItem(text, type), - itemData(data) {} + // Banned filter + if (m_hide_banned) { + QModelIndex idx = sourceModel()->index(source_row, MasternodeModel::STATUS, source_parent); + int banned = sourceModel()->data(idx, Qt::EditRole).toInt(); + if (banned != 0) { + return false; + } + } - bool operator<(const QTableWidgetItem& other) const override - { - return itemData < ((CMasternodeListWidgetItem*)&other)->itemData; + // Text-matching filter + if (const auto& regex = filterRegularExpression(); !regex.pattern().isEmpty()) { + QModelIndex idx = sourceModel()->index(source_row, 0, source_parent); + QString searchText = sourceModel()->data(idx, Qt::UserRole).toString(); + if (!searchText.contains(regex)) { + return false; + } + } + + // "Owned" filter + if (m_show_owned_only) { + QModelIndex idx = sourceModel()->index(source_row, MasternodeModel::PROTX_HASH, source_parent); + QString proTxHash = sourceModel()->data(idx, Qt::DisplayRole).toString(); + if (m_my_mn_hashes.find(proTxHash) == m_my_mn_hashes.end()) { + return false; + } } -}; + + return true; +} MasternodeList::MasternodeList(QWidget* parent) : QWidget(parent), - ui(new Ui::MasternodeList) + ui(new Ui::MasternodeList), + m_model(new MasternodeModel(this)), + m_proxy_model(new MasternodeListSortFilterProxyModel(this)) { ui->setupUi(this); - GUIUtil::setFont({ui->label_count_2, ui->countLabelDIP3}, {GUIUtil::FontWeight::Bold, 14}); - GUIUtil::setFont({ui->label_filter_2}, {GUIUtil::FontWeight::Normal, 15}); - - int columnAddressWidth = 200; - int columnTypeWidth = 160; - int columnStatusWidth = 80; - int columnPoSeScoreWidth = 80; - int columnRegisteredWidth = 80; - int columnLastPaidWidth = 80; - int columnNextPaymentWidth = 100; - int columnPayeeWidth = 130; - int columnOperatorRewardWidth = 130; - int columnCollateralWidth = 130; - int columnOwnerWidth = 130; - int columnVotingWidth = 130; - - ui->tableWidgetMasternodesDIP3->setColumnWidth(COLUMN_SERVICE, columnAddressWidth); - ui->tableWidgetMasternodesDIP3->setColumnWidth(COLUMN_TYPE, columnTypeWidth); - ui->tableWidgetMasternodesDIP3->setColumnWidth(COLUMN_STATUS, columnStatusWidth); - ui->tableWidgetMasternodesDIP3->setColumnWidth(COLUMN_POSE, columnPoSeScoreWidth); - ui->tableWidgetMasternodesDIP3->setColumnWidth(COLUMN_REGISTERED, columnRegisteredWidth); - ui->tableWidgetMasternodesDIP3->setColumnWidth(COLUMN_LAST_PAYMENT, columnLastPaidWidth); - ui->tableWidgetMasternodesDIP3->setColumnWidth(COLUMN_NEXT_PAYMENT, columnNextPaymentWidth); - ui->tableWidgetMasternodesDIP3->setColumnWidth(COLUMN_PAYOUT_ADDRESS, columnPayeeWidth); - ui->tableWidgetMasternodesDIP3->setColumnWidth(COLUMN_OPERATOR_REWARD, columnOperatorRewardWidth); - ui->tableWidgetMasternodesDIP3->setColumnWidth(COLUMN_COLLATERAL_ADDRESS, columnCollateralWidth); - ui->tableWidgetMasternodesDIP3->setColumnWidth(COLUMN_OWNER_ADDRESS, columnOwnerWidth); - ui->tableWidgetMasternodesDIP3->setColumnWidth(COLUMN_VOTING_ADDRESS, columnVotingWidth); - - // dummy column for proTxHash - ui->tableWidgetMasternodesDIP3->insertColumn(COLUMN_PROTX_HASH); - ui->tableWidgetMasternodesDIP3->setColumnHidden(COLUMN_PROTX_HASH, true); - - ui->tableWidgetMasternodesDIP3->setContextMenuPolicy(Qt::CustomContextMenu); - ui->tableWidgetMasternodesDIP3->verticalHeader()->setVisible(false); - - ui->checkBoxMyMasternodesOnly->setEnabled(false); + GUIUtil::setFont({ui->label_count, ui->countLabel}, {GUIUtil::FontWeight::Bold, 14}); + + // Set up proxy model + m_proxy_model->setSourceModel(m_model); + m_proxy_model->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_proxy_model->setSortRole(Qt::EditRole); + + // Set up table view + ui->tableViewMasternodes->setModel(m_proxy_model); + ui->tableViewMasternodes->setContextMenuPolicy(Qt::CustomContextMenu); + ui->tableViewMasternodes->verticalHeader()->setVisible(false); + + // Set column widths + auto* header = ui->tableViewMasternodes->horizontalHeader(); + header->setStretchLastSection(false); + for (int col = 0; col < MasternodeModel::COUNT; ++col) { + if (col == MasternodeModel::SERVICE) { + header->setSectionResizeMode(col, QHeaderView::Stretch); + } else { + header->setSectionResizeMode(col, QHeaderView::ResizeToContents); + } + } + + // Hide ProTx Hash column (used for internal lookup) + ui->tableViewMasternodes->setColumnHidden(MasternodeModel::PROTX_HASH, true); + + // Hide PoSe column by default (since "Hide banned" is checked by default) + ui->tableViewMasternodes->setColumnHidden(MasternodeModel::POSE, true); + + ui->checkBoxOwned->setEnabled(false); contextMenuDIP3 = new QMenu(this); contextMenuDIP3->addAction(tr("Copy ProTx Hash"), this, &MasternodeList::copyProTxHash_clicked); contextMenuDIP3->addAction(tr("Copy Collateral Outpoint"), this, &MasternodeList::copyCollateralOutpoint_clicked); - connect(ui->tableWidgetMasternodesDIP3, &QTableWidget::customContextMenuRequested, this, &MasternodeList::showContextMenuDIP3); - connect(ui->tableWidgetMasternodesDIP3, &QTableWidget::doubleClicked, this, &MasternodeList::extraInfoDIP3_clicked); + QMenu* filterMenu = contextMenuDIP3->addMenu(tr("Filter by")); + filterMenu->addAction(tr("Collateral Address"), this, &MasternodeList::filterByCollateralAddress); + filterMenu->addAction(tr("Payout Address"), this, &MasternodeList::filterByPayoutAddress); + filterMenu->addAction(tr("Owner Address"), this, &MasternodeList::filterByOwnerAddress); + filterMenu->addAction(tr("Voting Address"), this, &MasternodeList::filterByVotingAddress); + + connect(ui->tableViewMasternodes, &QTableView::customContextMenuRequested, this, &MasternodeList::showContextMenuDIP3); + connect(ui->tableViewMasternodes, &QTableView::doubleClicked, this, &MasternodeList::extraInfoDIP3_clicked); + connect(m_proxy_model, &QSortFilterProxyModel::rowsInserted, this, &MasternodeList::updateFilteredCount); + connect(m_proxy_model, &QSortFilterProxyModel::rowsRemoved, this, &MasternodeList::updateFilteredCount); + connect(m_proxy_model, &QSortFilterProxyModel::modelReset, this, &MasternodeList::updateFilteredCount); + connect(m_proxy_model, &QSortFilterProxyModel::layoutChanged, this, &MasternodeList::updateFilteredCount); timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &MasternodeList::updateDIP3ListScheduled); - timer->start(1000); GUIUtil::updateFonts(); } MasternodeList::~MasternodeList() { + timer->stop(); delete ui; } +void MasternodeList::changeEvent(QEvent* event) +{ + QWidget::changeEvent(event); + if (event->type() == QEvent::StyleChange) { + QTimer::singleShot(0, m_model, &MasternodeModel::refreshIcons); + } +} + void MasternodeList::setClientModel(ClientModel* model) { this->clientModel = model; if (model) { // try to update list when masternode count changes connect(clientModel, &ClientModel::masternodeListChanged, this, &MasternodeList::handleMasternodeListChanged); + timer->start(1000); + } else { + timer->stop(); } } void MasternodeList::setWalletModel(WalletModel* model) { this->walletModel = model; - ui->checkBoxMyMasternodesOnly->setEnabled(model != nullptr); + ui->checkBoxOwned->setEnabled(model != nullptr); } void MasternodeList::showContextMenuDIP3(const QPoint& point) { - QTableWidgetItem* item = ui->tableWidgetMasternodesDIP3->itemAt(point); - if (item) contextMenuDIP3->exec(QCursor::pos()); + QModelIndex index = ui->tableViewMasternodes->indexAt(point); + if (index.isValid()) { + contextMenuDIP3->exec(QCursor::pos()); + } } void MasternodeList::handleMasternodeListChanged() { - LOCK(cs_dip3list); - mnListChanged = true; + m_mn_list_changed.store(true, std::memory_order_relaxed); } void MasternodeList::updateDIP3ListScheduled() { - TRY_LOCK(cs_dip3list, fLockAcquired); - if (!fLockAcquired) return; - if (!clientModel || clientModel->node().shutdownRequested()) { return; } - // To prevent high cpu usage update only once in MASTERNODELIST_FILTER_COOLDOWN_SECONDS seconds - // after filter was last changed unless we want to force the update. - if (fFilterUpdatedDIP3) { - int64_t nSecondsToWait = nTimeFilterUpdatedDIP3 - GetTime() + MASTERNODELIST_FILTER_COOLDOWN_SECONDS; - ui->countLabelDIP3->setText(tr("Please wait…") + " " + QString::number(nSecondsToWait)); - - if (nSecondsToWait <= 0) { - updateDIP3List(); - fFilterUpdatedDIP3 = false; - } - } else if (mnListChanged) { + if (m_mn_list_changed.load(std::memory_order_relaxed)) { int64_t nMnListUpdateSecods = clientModel->masternodeSync().isBlockchainSynced() ? MASTERNODELIST_UPDATE_SECONDS : MASTERNODELIST_UPDATE_SECONDS * 10; int64_t nSecondsToWait = nTimeUpdatedDIP3 - GetTime() + nMnListUpdateSecods; if (nSecondsToWait <= 0) { updateDIP3List(); - mnListChanged = false; + m_mn_list_changed.store(false, std::memory_order_relaxed); } } } @@ -185,13 +223,7 @@ void MasternodeList::updateDIP3List() }); } - LOCK(cs_dip3list); - - QString strToFilter; - ui->countLabelDIP3->setText(tr("Updating…")); - ui->tableWidgetMasternodesDIP3->setSortingEnabled(false); - ui->tableWidgetMasternodesDIP3->clearContents(); - ui->tableWidgetMasternodesDIP3->setRowCount(0); + ui->countLabel->setText(tr("Updating…")); nTimeUpdatedDIP3 = GetTime(); @@ -201,198 +233,190 @@ void MasternodeList::updateDIP3List() nextPayments.emplace(dmn->getProTxHash(), mnList->getHeight() + (int)i + 1); } - std::set setOutpts; - if (walletModel && ui->checkBoxMyMasternodesOnly->isChecked()) { - for (const auto& outpt : walletModel->wallet().listProTxCoins()) { - setOutpts.emplace(outpt); - } - } - + MasternodeEntryList entries; mnList->forEachMN(/*only_valid=*/false, [&](const auto& dmn) { - if (walletModel && ui->checkBoxMyMasternodesOnly->isChecked()) { - bool fMyMasternode = setOutpts.count(dmn.getCollateralOutpoint()) || - walletModel->wallet().isSpendable(PKHash(dmn.getKeyIdOwner())) || - walletModel->wallet().isSpendable(PKHash(dmn.getKeyIdVoting())) || - walletModel->wallet().isSpendable(dmn.getScriptPayout()) || - walletModel->wallet().isSpendable(dmn.getScriptOperatorPayout()); - if (!fMyMasternode) return; - } - // populate list - // Address, Protocol, Status, Active Seconds, Last Seen, Pub Key - auto addr_key = dmn.getNetInfoPrimary().GetKey(); - QByteArray addr_ba(reinterpret_cast(addr_key.data()), addr_key.size()); - QTableWidgetItem* addressItem = new CMasternodeListWidgetItem( - QString::fromStdString(dmn.getNetInfoPrimary().ToStringAddrPort()), addr_ba); - QTableWidgetItem* typeItem = new QTableWidgetItem( - QString::fromStdString(std::string(GetMnType(dmn.getType()).description))); - QTableWidgetItem* statusItem = new QTableWidgetItem(dmn.isBanned() ? tr("POSE_BANNED") : tr("ENABLED")); - QTableWidgetItem* PoSeScoreItem = new CMasternodeListWidgetItem(QString::number(dmn.getPoSePenalty()), - dmn.getPoSePenalty()); - QTableWidgetItem* registeredItem = new CMasternodeListWidgetItem(QString::number(dmn.getRegisteredHeight()), - dmn.getRegisteredHeight()); - QTableWidgetItem* lastPaidItem = new CMasternodeListWidgetItem(QString::number(dmn.getLastPaidHeight()), - dmn.getLastPaidHeight()); - - QString strNextPayment = "UNKNOWN"; - int nNextPayment = 0; - if (nextPayments.count(dmn.getProTxHash())) { - nNextPayment = nextPayments[dmn.getProTxHash()]; - strNextPayment = QString::number(nNextPayment); - } - QTableWidgetItem* nextPaymentItem = new CMasternodeListWidgetItem(strNextPayment, nNextPayment); - - CTxDestination payeeDest; - QString payeeStr = tr("UNKNOWN"); - if (ExtractDestination(dmn.getScriptPayout(), payeeDest)) { - payeeStr = QString::fromStdString(EncodeDestination(payeeDest)); - } - QTableWidgetItem* payeeItem = new QTableWidgetItem(payeeStr); - - QString operatorRewardStr = tr("NONE"); - if (dmn.getOperatorReward()) { - operatorRewardStr = QString::number(dmn.getOperatorReward() / 100.0, 'f', 2) + "% "; - - if (dmn.getScriptOperatorPayout() != CScript()) { - CTxDestination operatorDest; - if (ExtractDestination(dmn.getScriptOperatorPayout(), operatorDest)) { - operatorRewardStr += tr("to %1").arg(QString::fromStdString(EncodeDestination(operatorDest))); - } else { - operatorRewardStr += tr("to UNKNOWN"); - } - } else { - operatorRewardStr += tr("but not claimed"); - } - } - QTableWidgetItem* operatorRewardItem = new CMasternodeListWidgetItem(operatorRewardStr, - dmn.getOperatorReward()); - QString collateralStr = tr("UNKNOWN"); auto collateralDestIt = mapCollateralDests.find(dmn.getProTxHash()); if (collateralDestIt != mapCollateralDests.end()) { collateralStr = QString::fromStdString(EncodeDestination(collateralDestIt->second)); } - QTableWidgetItem* collateralItem = new QTableWidgetItem(collateralStr); - - QString ownerStr = QString::fromStdString(EncodeDestination(PKHash(dmn.getKeyIdOwner()))); - QTableWidgetItem* ownerItem = new QTableWidgetItem(ownerStr); - - QString votingStr = QString::fromStdString(EncodeDestination(PKHash(dmn.getKeyIdVoting()))); - QTableWidgetItem* votingItem = new QTableWidgetItem(votingStr); - - QTableWidgetItem* proTxHashItem = new QTableWidgetItem(QString::fromStdString(dmn.getProTxHash().ToString())); - - if (strCurrentFilterDIP3 != "") { - strToFilter = addressItem->text() + " " + - typeItem->text() + " " + - statusItem->text() + " " + - PoSeScoreItem->text() + " " + - registeredItem->text() + " " + - lastPaidItem->text() + " " + - nextPaymentItem->text() + " " + - payeeItem->text() + " " + - operatorRewardItem->text() + " " + - collateralItem->text() + " " + - ownerItem->text() + " " + - votingItem->text() + " " + - proTxHashItem->text(); - if (!strToFilter.contains(strCurrentFilterDIP3)) return; + + int nNextPayment = 0; + auto nextPaymentIt = nextPayments.find(dmn.getProTxHash()); + if (nextPaymentIt != nextPayments.end()) { + nNextPayment = nextPaymentIt->second; } - ui->tableWidgetMasternodesDIP3->insertRow(0); - ui->tableWidgetMasternodesDIP3->setItem(0, COLUMN_SERVICE, addressItem); - ui->tableWidgetMasternodesDIP3->setItem(0, COLUMN_TYPE, typeItem); - ui->tableWidgetMasternodesDIP3->setItem(0, COLUMN_STATUS, statusItem); - ui->tableWidgetMasternodesDIP3->setItem(0, COLUMN_POSE, PoSeScoreItem); - ui->tableWidgetMasternodesDIP3->setItem(0, COLUMN_REGISTERED, registeredItem); - ui->tableWidgetMasternodesDIP3->setItem(0, COLUMN_LAST_PAYMENT, lastPaidItem); - ui->tableWidgetMasternodesDIP3->setItem(0, COLUMN_NEXT_PAYMENT, nextPaymentItem); - ui->tableWidgetMasternodesDIP3->setItem(0, COLUMN_PAYOUT_ADDRESS, payeeItem); - ui->tableWidgetMasternodesDIP3->setItem(0, COLUMN_OPERATOR_REWARD, operatorRewardItem); - ui->tableWidgetMasternodesDIP3->setItem(0, COLUMN_COLLATERAL_ADDRESS, collateralItem); - ui->tableWidgetMasternodesDIP3->setItem(0, COLUMN_OWNER_ADDRESS, ownerItem); - ui->tableWidgetMasternodesDIP3->setItem(0, COLUMN_VOTING_ADDRESS, votingItem); - ui->tableWidgetMasternodesDIP3->setItem(0, COLUMN_PROTX_HASH, proTxHashItem); + entries.push_back(std::make_unique(dmn, collateralStr, nNextPayment)); + }); + + // Update model + m_model->setCurrentHeight(mnList->getHeight()); + m_model->reconcile(std::move(entries)); + + // Update filters + if (walletModel && ui->checkBoxOwned->isChecked()) { + updateMyMasternodeHashes(mnList); + } + + updateFilteredCount(); +} + +void MasternodeList::updateMyMasternodeHashes(const interfaces::MnListPtr& mnList) +{ + if (!walletModel || !mnList) { + return; + } + + std::set setOutpts; + for (const auto& outpt : walletModel->wallet().listProTxCoins()) { + setOutpts.emplace(outpt); + } + + std::set myHashes; + mnList->forEachMN(/*only_valid=*/false, [&](const auto& dmn) { + bool fMyMasternode = setOutpts.count(dmn.getCollateralOutpoint()) || + walletModel->wallet().isSpendable(PKHash(dmn.getKeyIdOwner())) || + walletModel->wallet().isSpendable(PKHash(dmn.getKeyIdVoting())) || + walletModel->wallet().isSpendable(dmn.getScriptPayout()) || + walletModel->wallet().isSpendable(dmn.getScriptOperatorPayout()); + if (fMyMasternode) { + myHashes.insert(QString::fromStdString(dmn.getProTxHash().ToString())); + } }); - ui->countLabelDIP3->setText(QString::number(ui->tableWidgetMasternodesDIP3->rowCount())); - ui->tableWidgetMasternodesDIP3->setSortingEnabled(true); + m_proxy_model->setMyMasternodeHashes(std::move(myHashes)); + m_proxy_model->forceInvalidateFilter(); } -void MasternodeList::on_filterLineEditDIP3_textChanged(const QString& strFilterIn) +void MasternodeList::updateFilteredCount() { - strCurrentFilterDIP3 = strFilterIn; - nTimeFilterUpdatedDIP3 = GetTime(); - fFilterUpdatedDIP3 = true; - ui->countLabelDIP3->setText(tr("Please wait…") + " " + QString::number(MASTERNODELIST_FILTER_COOLDOWN_SECONDS)); + ui->countLabel->setText(QString::number(m_proxy_model->rowCount())); } -void MasternodeList::on_checkBoxMyMasternodesOnly_stateChanged(int state) +void MasternodeList::on_filterText_textChanged(const QString& strFilterIn) { - // no cooldown - nTimeFilterUpdatedDIP3 = GetTime() - MASTERNODELIST_FILTER_COOLDOWN_SECONDS; - fFilterUpdatedDIP3 = true; + m_proxy_model->setFilterRegularExpression( + QRegularExpression(QRegularExpression::escape(strFilterIn), QRegularExpression::CaseInsensitiveOption)); + updateFilteredCount(); } -std::unique_ptr MasternodeList::GetSelectedDIP3MN() +void MasternodeList::on_comboBoxType_currentIndexChanged(int index) { - if (!clientModel) { - return nullptr; + if (index < 0 || index >= static_cast(MasternodeListSortFilterProxyModel::TypeFilter::COUNT)) { + return; } + const auto index_enum{static_cast(index)}; + ui->tableViewMasternodes->setColumnHidden(MasternodeModel::TYPE, index_enum != MasternodeListSortFilterProxyModel::TypeFilter::All); + m_proxy_model->setTypeFilter(index_enum); + m_proxy_model->forceInvalidateFilter(); + updateFilteredCount(); +} - std::string strProTxHash; - { - LOCK(cs_dip3list); +void MasternodeList::on_checkBoxOwned_stateChanged(int state) +{ + m_proxy_model->setShowOwnedOnly(state == Qt::Checked); + if (clientModel && state == Qt::Checked) { + auto [mnList, pindex] = clientModel->getMasternodeList(); + if (mnList) { + updateMyMasternodeHashes(mnList); + } + } else { + m_proxy_model->forceInvalidateFilter(); + } + updateFilteredCount(); +} - QItemSelectionModel* selectionModel = ui->tableWidgetMasternodesDIP3->selectionModel(); - QModelIndexList selected = selectionModel->selectedRows(); +void MasternodeList::on_checkBoxHideBanned_stateChanged(int state) +{ + const bool hide_banned{state == Qt::Checked}; + ui->tableViewMasternodes->setColumnHidden(MasternodeModel::POSE, hide_banned); + m_proxy_model->setHideBanned(hide_banned); + m_proxy_model->forceInvalidateFilter(); + updateFilteredCount(); +} - if (selected.count() == 0) return nullptr; +const MasternodeEntry* MasternodeList::GetSelectedEntry() +{ + if (!m_model) { + return nullptr; + } - QModelIndex index = selected.at(0); - int nSelectedRow = index.row(); - strProTxHash = ui->tableWidgetMasternodesDIP3->item(nSelectedRow, COLUMN_PROTX_HASH)->text().toStdString(); + QItemSelectionModel* selectionModel = ui->tableViewMasternodes->selectionModel(); + if (!selectionModel) { + return nullptr; } - uint256 proTxHash; - proTxHash.SetHex(strProTxHash); + QModelIndexList selected = selectionModel->selectedRows(); + if (selected.count() == 0) { + return nullptr; + } - // Caller is responsible for nullptr checking return value - return clientModel->getMasternodeList().first->getMN(proTxHash); + // Map from proxy to source model + return m_model->getEntryAt(m_proxy_model->mapToSource(selected.at(0))); } void MasternodeList::extraInfoDIP3_clicked() { - auto dmn = GetSelectedDIP3MN(); - if (!dmn) { + const auto* entry = GetSelectedEntry(); + if (!entry) { return; } - UniValue json = dmn->toJson(); - - // Title of popup window - QString strWindowtitle = - tr("Additional information for DIP3 Masternode %1").arg(QString::fromStdString(dmn->getProTxHash().ToString())); - QString strText = QString::fromStdString(json.write(2)); - - QMessageBox::information(this, strWindowtitle, strText); + auto* dialog = new DescriptionDialog(tr("Details for Masternode %1").arg(entry->proTxHash()), entry->toHtml(), /*parent=*/this); + dialog->resize(1000, 500); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); } void MasternodeList::copyProTxHash_clicked() { - auto dmn = GetSelectedDIP3MN(); - if (!dmn) { + const auto* entry = GetSelectedEntry(); + if (!entry) { return; } - QApplication::clipboard()->setText(QString::fromStdString(dmn->getProTxHash().ToString())); + QApplication::clipboard()->setText(entry->proTxHash()); } void MasternodeList::copyCollateralOutpoint_clicked() { - auto dmn = GetSelectedDIP3MN(); - if (!dmn) { + const auto* entry = GetSelectedEntry(); + if (!entry) { return; } - QApplication::clipboard()->setText(QString::fromStdString(dmn->getCollateralOutpoint().ToStringShort())); + QApplication::clipboard()->setText(entry->collateralOutpoint()); +} + +void MasternodeList::filterByCollateralAddress() +{ + const auto* entry = GetSelectedEntry(); + if (entry) { + ui->filterText->setText(entry->collateralAddress()); + } +} + +void MasternodeList::filterByPayoutAddress() +{ + const auto* entry = GetSelectedEntry(); + if (entry) { + ui->filterText->setText(entry->payoutAddress()); + } +} + +void MasternodeList::filterByOwnerAddress() +{ + const auto* entry = GetSelectedEntry(); + if (entry) { + ui->filterText->setText(entry->ownerAddress()); + } +} + +void MasternodeList::filterByVotingAddress() +{ + const auto* entry = GetSelectedEntry(); + if (entry) { + ui->filterText->setText(entry->votingAddress()); + } } diff --git a/src/qt/masternodelist.h b/src/qt/masternodelist.h index c0be342116d9..56929aeaf38b 100644 --- a/src/qt/masternodelist.h +++ b/src/qt/masternodelist.h @@ -5,34 +5,65 @@ #ifndef BITCOIN_QT_MASTERNODELIST_H #define BITCOIN_QT_MASTERNODELIST_H -#include -#include #include -#include - #include +#include +#include #include #include -#define MASTERNODELIST_UPDATE_SECONDS 3 -#define MASTERNODELIST_FILTER_COOLDOWN_SECONDS 3 +#include +#include +#include -namespace Ui -{ +namespace interfaces { +class MnList; +using MnListPtr = std::shared_ptr; +} // namespace interfaces +namespace Ui { class MasternodeList; -} +} // namespace Ui class ClientModel; +class MasternodeEntry; +class MasternodeModel; class WalletModel; QT_BEGIN_NAMESPACE class QModelIndex; QT_END_NAMESPACE -namespace interfaces { -class MnEntry; -} +class MasternodeListSortFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + enum class TypeFilter : uint8_t { + All, + Regular, + Evo, + COUNT + }; + + explicit MasternodeListSortFilterProxyModel(QObject* parent = nullptr) : + QSortFilterProxyModel(parent) {} + + void forceInvalidateFilter() { invalidateFilter(); } + void setHideBanned(bool hide) { m_hide_banned = hide; } + void setMyMasternodeHashes(std::set hashes) { m_my_mn_hashes = std::move(hashes); } + void setShowOwnedOnly(bool show) { m_show_owned_only = show; } + void setTypeFilter(TypeFilter type) { m_type_filter = type; } + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; + +private: + bool m_hide_banned{true}; + bool m_show_owned_only{false}; + std::set m_my_mn_hashes; + TypeFilter m_type_filter{TypeFilter::All}; +}; /** Masternode Manager page widget */ class MasternodeList : public QWidget @@ -43,60 +74,50 @@ class MasternodeList : public QWidget explicit MasternodeList(QWidget* parent = nullptr); ~MasternodeList(); - enum { - COLUMN_SERVICE, - COLUMN_TYPE, - COLUMN_STATUS, - COLUMN_POSE, - COLUMN_REGISTERED, - COLUMN_LAST_PAYMENT, - COLUMN_NEXT_PAYMENT, - COLUMN_PAYOUT_ADDRESS, - COLUMN_OPERATOR_REWARD, - COLUMN_COLLATERAL_ADDRESS, - COLUMN_OWNER_ADDRESS, - COLUMN_VOTING_ADDRESS, - COLUMN_PROTX_HASH, - }; - void setClientModel(ClientModel* clientModel); void setWalletModel(WalletModel* walletModel); +protected: + void changeEvent(QEvent* event) override; + private: QMenu* contextMenuDIP3; - int64_t nTimeFilterUpdatedDIP3{0}; int64_t nTimeUpdatedDIP3{0}; - bool fFilterUpdatedDIP3{true}; QTimer* timer; Ui::MasternodeList* ui; ClientModel* clientModel{nullptr}; WalletModel* walletModel{nullptr}; - // Protects tableWidgetMasternodesDIP3 - RecursiveMutex cs_dip3list; + MasternodeModel* m_model{nullptr}; + MasternodeListSortFilterProxyModel* m_proxy_model{nullptr}; - QString strCurrentFilterDIP3; + std::atomic m_mn_list_changed{true}; - bool mnListChanged{true}; - - std::unique_ptr GetSelectedDIP3MN(); + const MasternodeEntry* GetSelectedEntry(); void updateDIP3List(); + void updateMyMasternodeHashes(const interfaces::MnListPtr& mnList); Q_SIGNALS: void doubleClicked(const QModelIndex&); private Q_SLOTS: - void showContextMenuDIP3(const QPoint&); - void on_filterLineEditDIP3_textChanged(const QString& strFilterIn); - void on_checkBoxMyMasternodesOnly_stateChanged(int state); - - void extraInfoDIP3_clicked(); - void copyProTxHash_clicked(); void copyCollateralOutpoint_clicked(); - + void copyProTxHash_clicked(); + void extraInfoDIP3_clicked(); + void filterByCollateralAddress(); + void filterByPayoutAddress(); + void filterByOwnerAddress(); + void filterByVotingAddress(); void handleMasternodeListChanged(); + void on_checkBoxHideBanned_stateChanged(int state); + void on_checkBoxOwned_stateChanged(int state); + void on_comboBoxType_currentIndexChanged(int index); + void on_filterText_textChanged(const QString& strFilterIn); + void showContextMenuDIP3(const QPoint&); void updateDIP3ListScheduled(); + void updateFilteredCount(); }; + #endif // BITCOIN_QT_MASTERNODELIST_H diff --git a/src/qt/masternodemodel.cpp b/src/qt/masternodemodel.cpp new file mode 100644 index 000000000000..bd2e6ebdb9c9 --- /dev/null +++ b/src/qt/masternodemodel.cpp @@ -0,0 +1,420 @@ +// Copyright (c) 2021-2026 The Dash 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