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