-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(qt): UI refresh (2/n, update "Governance" Tab layout, lifecycle icons, description dialogs, resumable proposal creation) #7110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
a773635
5af2bb9
c85e5cb
a7328e9
662703e
8257eac
9bb7b0a
b2b9aa1
c0d8454
f45fe85
4295471
552f591
af65c0e
5f95737
28b67d3
1a1d56e
2d8cffe
16314e7
3ca061f
90e0375
2883527
080c8fb
a0da7de
9fb0c35
d1e67ff
6cb7a40
a5ae10c
3d53afa
ce94c3e
707ce77
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,18 +26,20 @@ | |
|
|
||
| #include <QAbstractItemView> | ||
| #include <QMessageBox> | ||
| #include <QMetaObject> | ||
|
|
||
| // | ||
| // Governance Tab main widget. | ||
| // | ||
|
|
||
| GovernanceList::GovernanceList(QWidget* parent) : | ||
| QWidget(parent), | ||
| ui(std::make_unique<Ui::GovernanceList>()), | ||
| proposalModel(new ProposalModel(this)), | ||
| proposalModelProxy(new QSortFilterProxyModel(this)), | ||
| proposalContextMenu(new QMenu(this)), | ||
| timer(new QTimer(this)) | ||
| ui{new Ui::GovernanceList}, | ||
| proposalModel{new ProposalModel(this)}, | ||
| proposalContextMenu{new QMenu(this)}, | ||
| m_worker(new QObject), | ||
| proposalModelProxy{new QSortFilterProxyModel(this)}, | ||
| m_thread{new QThread(this)}, | ||
| m_timer{new QTimer(this)} | ||
| { | ||
| ui->setupUi(this); | ||
|
|
||
|
|
@@ -77,31 +79,49 @@ GovernanceList::GovernanceList(QWidget* parent) : | |
| connect(ui->btnCreateProposal, &QPushButton::clicked, this, &GovernanceList::showCreateProposalDialog); | ||
| connect(ui->govTableView, &QTableView::doubleClicked, this, &GovernanceList::showAdditionalInfo); | ||
|
|
||
| connect(timer, &QTimer::timeout, this, &GovernanceList::updateProposalList); | ||
|
|
||
| // Initialize masternode count to 0 | ||
| ui->mnCountLabel->setText("0"); | ||
|
|
||
| GUIUtil::updateFonts(); | ||
|
|
||
| // Background thread for calculating proposal list | ||
| m_worker->moveToThread(m_thread); | ||
| // Make sure executor object is deleted in its own thread | ||
| connect(m_thread, &QThread::finished, m_worker, &QObject::deleteLater); | ||
| m_thread->start(); | ||
|
|
||
| // Debounce timer to apply proposal list changes | ||
| m_timer->setSingleShot(true); | ||
| connect(m_timer, &QTimer::timeout, this, &GovernanceList::updateProposalList); | ||
| } | ||
|
|
||
| GovernanceList::~GovernanceList() = default; | ||
| GovernanceList::~GovernanceList() | ||
| { | ||
| m_timer->stop(); | ||
| m_thread->quit(); | ||
| m_thread->wait(); | ||
| delete ui; | ||
| } | ||
|
|
||
| void GovernanceList::setClientModel(ClientModel* model) | ||
| { | ||
| this->clientModel = model; | ||
| if (model != nullptr) { | ||
| connect(model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &GovernanceList::updateDisplayUnit); | ||
|
|
||
| updateProposalList(); | ||
| if (clientModel) { | ||
| connect(clientModel, &ClientModel::governanceChanged, this, &GovernanceList::handleProposalListChanged); | ||
| connect(clientModel->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &GovernanceList::updateDisplayUnit); | ||
| m_timer->start(0); | ||
| } else { | ||
| m_timer->stop(); | ||
| } | ||
| } | ||
|
|
||
| void GovernanceList::setWalletModel(WalletModel* model) | ||
| { | ||
| this->walletModel = model; | ||
| if (model && clientModel) { | ||
| updateVotingCapability(); | ||
| if (walletModel && clientModel) { | ||
| m_timer->start(0); | ||
| } else { | ||
| m_timer->stop(); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -113,35 +133,88 @@ void GovernanceList::updateDisplayUnit() | |
| } | ||
| } | ||
|
|
||
| void GovernanceList::handleProposalListChanged() | ||
| { | ||
| if (!clientModel || m_timer->isActive()) { | ||
| // Too early or already processing, nothing to do | ||
| return; | ||
| } | ||
| int delay{GOVERNANCELIST_UPDATE_SECONDS * 1000}; | ||
| if (!clientModel->masternodeSync().isBlockchainSynced()) { | ||
| // Currently syncing, reduce refreshes | ||
| delay *= 6; | ||
| } | ||
| m_timer->start(delay); | ||
| } | ||
|
|
||
| void GovernanceList::updateProposalList() | ||
| { | ||
| if (this->clientModel) { | ||
| // A proposal is considered passing if (YES votes - NO votes) >= (Total Weight of Masternodes / 10), | ||
| // count total valid (ENABLED) masternodes to determine passing threshold. | ||
| // Need to query number of masternodes here with access to clientModel. | ||
| const int nWeightedMnCount = clientModel->getMasternodeList().first->getValidWeightedMNsCount(); | ||
| const int nAbsVoteReq = std::max(Params().GetConsensus().nGovernanceMinQuorum, nWeightedMnCount / 10); | ||
| proposalModel->setVotingParams(nAbsVoteReq); | ||
|
|
||
| std::vector<CGovernanceObject> govObjList; | ||
| clientModel->getAllGovernanceObjects(govObjList); | ||
| ProposalList newProposals; | ||
| for (const auto& govObj : govObjList) { | ||
| if (govObj.GetObjectType() != GovernanceObject::PROPOSAL) { | ||
| continue; // Skip triggers. | ||
| } | ||
| newProposals.emplace_back(std::make_unique<Proposal>(this->clientModel, govObj)); | ||
| } | ||
| proposalModel->reconcile(std::move(newProposals)); | ||
| if (!clientModel || clientModel->node().shutdownRequested()) { | ||
| return; | ||
| } | ||
|
|
||
| // Update voting capability if we now have both client and wallet models | ||
| if (walletModel) { | ||
| updateVotingCapability(); | ||
| if (m_in_progress.exchange(true)) { | ||
| // Already applying, re-arm for next attempt | ||
| handleProposalListChanged(); | ||
| return; | ||
| } | ||
|
|
||
| QMetaObject::invokeMethod(m_worker, [this] { | ||
| auto result = std::make_shared<CalcProposalList>(calcProposalList()); | ||
| m_in_progress.store(false); | ||
| QTimer::singleShot(0, this, [this, result] { | ||
| setProposalList(std::move(*result)); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| GovernanceList::CalcProposalList GovernanceList::calcProposalList() const | ||
| { | ||
| CalcProposalList ret; | ||
| if (!clientModel || clientModel->node().shutdownRequested()) { | ||
| return ret; | ||
| } | ||
|
|
||
| const auto [dmn, pindex] = clientModel->getMasternodeList(); | ||
| if (!dmn || !pindex) { | ||
| return ret; | ||
| } | ||
|
|
||
| // A proposal is considered passing if (YES votes - NO votes) >= (Total Weight of Masternodes / 10), | ||
| // count total valid (ENABLED) masternodes to determine passing threshold. | ||
| // Need to query number of masternodes here with access to clientModel. | ||
| const int nWeightedMnCount = dmn->getValidWeightedMNsCount(); | ||
| ret.m_abs_vote_req = std::max(Params().GetConsensus().nGovernanceMinQuorum, nWeightedMnCount / 10); | ||
|
|
||
| std::vector<CGovernanceObject> govObjList; | ||
| clientModel->getAllGovernanceObjects(govObjList); | ||
| for (const auto& govObj : govObjList) { | ||
| if (govObj.GetObjectType() != GovernanceObject::PROPOSAL) { | ||
| continue; // Skip triggers. | ||
| } | ||
| ret.m_proposals.emplace_back(std::make_unique<Proposal>(this->clientModel, govObj)); | ||
| } | ||
|
|
||
| // Schedule next update. | ||
| timer->start(GOVERNANCELIST_UPDATE_SECONDS * 1000); | ||
| // Discover voting capability if we now have both client and wallet models | ||
| if (walletModel) { | ||
| dmn->forEachMN(/*only_valid=*/true, [&](const auto& dmn) { | ||
| // Check if wallet owns the voting key using the same logic as RPC | ||
| const auto script = GetScriptForDestination(PKHash(dmn.getKeyIdVoting())); | ||
| if (walletModel->wallet().isSpendable(script)) { | ||
| ret.m_votable_masternodes[dmn.getProTxHash()] = dmn.getKeyIdVoting(); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| return ret; | ||
| } | ||
|
Comment on lines
244
to
+322
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: fd -a 'clientmodel.h|walletmodel.h' src/qtRepository: dashpay/dash Length of output: 135 🏁 Script executed: rg -n "class ClientModel|class WalletModel" src/qt --max-count=5 -A 10Repository: dashpay/dash Length of output: 17105 🏁 Script executed: rg -n "getMasternodeList|getAllGovernanceObjects|isSpendable" src/qt -B 2 -A 5Repository: dashpay/dash Length of output: 7609 🏁 Script executed: cat -n src/qt/clientmodel.h | sed -n '60,80p'Repository: dashpay/dash Length of output: 1193 🏁 Script executed: rg -n "interfaces::Node|interfaces::Wallet" src/qt/clientmodel.h -A 3 | head -40Repository: dashpay/dash Length of output: 703 🏁 Script executed: fd -a 'node.h|wallet.h' src/interfaces | head -5Repository: dashpay/dash Length of output: 139 🏁 Script executed: cat -n src/interfaces/node.h | sed -n '1,100p'Repository: dashpay/dash Length of output: 3607 🏁 Script executed: rg -n "thread|Thread|THREAD" src/interfaces/node.h | head -20Repository: dashpay/dash Length of output: 38 🏁 Script executed: cat -n src/interfaces/node.h | sed -n '100,250p'Repository: dashpay/dash Length of output: 6373 🏁 Script executed: rg -n "gov()" src/interfaces/node.h -A 2 -B 2Repository: dashpay/dash Length of output: 899 🏁 Script executed: cat -n src/interfaces/wallet.h | sed -n '1,80p'Repository: dashpay/dash Length of output: 2579 🏁 Script executed: rg -n "isSpendable" src/interfaces/wallet.h -B 3 -A 3Repository: dashpay/dash Length of output: 465 🏁 Script executed: cat -n src/qt/governancelist.h | sed -n '1,150p'Repository: dashpay/dash Length of output: 4255 🏁 Script executed: rg -n "m_worker|QThread" src/qt/governancelist.h -B 2 -A 5Repository: dashpay/dash Length of output: 682 🏁 Script executed: cat -n src/qt/governancelist.cpp | sed -n '1,50p'Repository: dashpay/dash Length of output: 1622 🏁 Script executed: cat -n src/qt/governancelist.cpp | sed -n '50,150p'Repository: dashpay/dash Length of output: 5157 🏁 Script executed: rg -n "m_worker|m_thread" src/qt/governancelist.cpp -B 2 -A 5 | head -100Repository: dashpay/dash Length of output: 1408 🏁 Script executed: cat -n src/qt/governancelist.cpp | sed -n '240,330p'Repository: dashpay/dash Length of output: 4822 🏁 Script executed: rg -n "queryCollateralDepth|getWalletProposals" src/qt/governancelist.cpp -A 10 | head -60Repository: dashpay/dash Length of output: 2892 Avoid cross-thread access to ClientModel/WalletModel.
🤖 Prompt for AI Agents |
||
|
|
||
| void GovernanceList::setProposalList(CalcProposalList&& data) | ||
| { | ||
| proposalModel->setVotingParams(data.m_abs_vote_req); | ||
| proposalModel->reconcile(std::move(data.m_proposals)); | ||
| votableMasternodes = std::move(data.m_votable_masternodes); | ||
| updateMasternodeCount(); | ||
| } | ||
|
|
||
| void GovernanceList::updateProposalCount() const | ||
|
|
@@ -213,26 +286,6 @@ void GovernanceList::showAdditionalInfo(const QModelIndex& index) | |
| QMessageBox::information(this, windowTitle, json); | ||
| } | ||
|
Comment on lines
426
to
444
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing null check on Line 339 calls Suggested fix- DescriptionDialog* dialog = new DescriptionDialog(
- tr("Details for %1").arg(proposal->title()),
- proposal->toHtml(clientModel->getOptionsModel()->getDisplayUnit()),
- /*parent=*/this);
+ const auto unit = clientModel->getOptionsModel() ? clientModel->getOptionsModel()->getDisplayUnit()
+ : BitcoinUnit::DASH;
+ DescriptionDialog* dialog = new DescriptionDialog(
+ tr("Details for %1").arg(proposal->title()),
+ proposal->toHtml(unit),
+ /*parent=*/this);🤖 Prompt for AI Agents |
||
|
|
||
| void GovernanceList::updateVotingCapability() | ||
| { | ||
| if (!walletModel || !clientModel) return; | ||
|
|
||
| auto [mn_list, pindex] = clientModel->getMasternodeList(); | ||
| if (!pindex) return; | ||
|
|
||
| votableMasternodes.clear(); | ||
| mn_list->forEachMN(/*only_valid=*/true, [&](const auto& dmn) { | ||
| // Check if wallet owns the voting key using the same logic as RPC | ||
| const CScript script = GetScriptForDestination(PKHash(dmn.getKeyIdVoting())); | ||
| if (walletModel->wallet().isSpendable(script)) { | ||
| votableMasternodes[dmn.getProTxHash()] = dmn.getKeyIdVoting(); | ||
| } | ||
| }); | ||
|
|
||
| // Update masternode count display | ||
| updateMasternodeCount(); | ||
| } | ||
|
|
||
| void GovernanceList::updateMasternodeCount() const | ||
| { | ||
| if (ui && ui->mnCountLabel) { | ||
|
|
@@ -340,5 +393,5 @@ void GovernanceList::voteForProposal(vote_outcome_enum_t outcome) | |
| QMessageBox::information(this, tr("Voting Results"), message); | ||
|
|
||
| // Update proposal list to show new vote counts | ||
| updateProposalList(); | ||
| handleProposalListChanged(); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this thread doesn't have name; consider assigning it.
Without it threads are just:
while other do have it:
UPDATE: yes, I opened 10 wallets and got 20 more runnign threads, all with the name
QThread.