diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 070f011f42f9..247020dae2cd 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -34,6 +34,7 @@ QT_FORMS_UI = \ qt/forms/optionsdialog.ui \ qt/forms/overviewpage.ui \ qt/forms/proposalcreate.ui \ + qt/forms/proposalinfo.ui \ qt/forms/proposallist.ui \ qt/forms/proposalresume.ui \ qt/forms/psbtoperationsdialog.ui \ @@ -62,6 +63,7 @@ QT_MOC_CPP = \ qt/moc_createwalletdialog.cpp \ qt/moc_csvmodelwriter.cpp \ qt/moc_descriptiondialog.cpp \ + qt/moc_donutchart.cpp \ qt/moc_editaddressdialog.cpp \ qt/moc_guiutil.cpp \ qt/moc_informationwidget.cpp \ @@ -83,6 +85,7 @@ QT_MOC_CPP = \ qt/moc_peertablemodel.cpp \ qt/moc_peertablesortproxy.cpp \ qt/moc_proposalcreate.cpp \ + qt/moc_proposalinfo.cpp \ qt/moc_proposallist.cpp \ qt/moc_proposalmodel.cpp \ qt/moc_proposalresume.cpp \ @@ -145,6 +148,7 @@ BITCOIN_QT_H = \ qt/createwalletdialog.h \ qt/csvmodelwriter.h \ qt/descriptiondialog.h \ + qt/donutchart.h \ qt/editaddressdialog.h \ qt/guiconstants.h \ qt/guiutil_font.h \ @@ -170,6 +174,7 @@ BITCOIN_QT_H = \ qt/peertablemodel.h \ qt/peertablesortproxy.h \ qt/proposalcreate.h \ + qt/proposalinfo.h \ qt/proposallist.h \ qt/proposalmodel.h \ qt/proposalresume.h \ @@ -251,19 +256,19 @@ BITCOIN_QT_BASE_CPP = \ qt/bantablemodel.cpp \ qt/bitcoin.cpp \ qt/bitcoinaddressvalidator.cpp \ - qt/masternodemodel.cpp \ - qt/proposalmodel.cpp \ qt/bitcoinamountfield.cpp \ qt/bitcoingui.cpp \ qt/bitcoinunits.cpp \ qt/clientfeeds.cpp \ qt/clientmodel.cpp \ qt/csvmodelwriter.cpp \ + qt/donutchart.cpp \ qt/guiutil.cpp \ qt/guiutil_font.cpp \ qt/informationwidget.cpp \ qt/initexecutor.cpp \ qt/intro.cpp \ + qt/masternodemodel.cpp \ qt/modaloverlay.cpp \ qt/networkstyle.cpp \ qt/networkwidget.cpp \ @@ -272,6 +277,8 @@ BITCOIN_QT_BASE_CPP = \ qt/optionsmodel.cpp \ qt/peertablemodel.cpp \ qt/peertablesortproxy.cpp \ + qt/proposalinfo.cpp \ + qt/proposalmodel.cpp \ qt/qvalidatedlineedit.cpp \ qt/qvaluecombobox.cpp \ qt/rpcconsole.cpp \ diff --git a/src/governance/object.cpp b/src/governance/object.cpp index 5d78d10dd348..fc99fa4d559e 100644 --- a/src/governance/object.cpp +++ b/src/governance/object.cpp @@ -592,6 +592,27 @@ int CGovernanceObject::GetAbstainCount(const CDeterministicMNList& tip_mn_list, return CountMatchingVotes(tip_mn_list, eVoteSignalIn, VOTE_OUTCOME_ABSTAIN); } +CGovernanceObject::UniqueVoterCount CGovernanceObject::GetUniqueVoterCount(const CDeterministicMNList& tip_mn_list, vote_signal_enum_t eVoteSignalIn) const +{ + LOCK(cs); + UniqueVoterCount result; + for (const auto& [outpoint, recVote] : mapCurrentMNVotes) { + if (recVote.mapInstances.count(eVoteSignalIn) == 0) { + continue; + } + auto dmn = tip_mn_list.GetMNByCollateral(outpoint); + if (!dmn) { + continue; + } + if (dmn->nType == MnType::Evo) { + ++result.m_evo; + } else { + ++result.m_regular; + } + } + return result; +} + bool CGovernanceObject::GetCurrentMNVotes(const COutPoint& mnCollateralOutpoint, vote_rec_t& voteRecord) const { LOCK(cs); diff --git a/src/governance/object.h b/src/governance/object.h index 05eb940bf275..2f2443915835 100644 --- a/src/governance/object.h +++ b/src/governance/object.h @@ -227,6 +227,13 @@ class CGovernanceObject int GetAbstainCount(const CDeterministicMNList& tip_mn_list, vote_signal_enum_t eVoteSignalIn) const EXCLUSIVE_LOCKS_REQUIRED(!cs); + struct UniqueVoterCount { + uint16_t m_regular{0}; + uint16_t m_evo{0}; + }; + UniqueVoterCount GetUniqueVoterCount(const CDeterministicMNList& tip_mn_list, vote_signal_enum_t eVoteSignalIn) const + EXCLUSIVE_LOCKS_REQUIRED(!cs); + bool GetCurrentMNVotes(const COutPoint& mnCollateralOutpoint, vote_rec_t& voteRecord) const EXCLUSIVE_LOCKS_REQUIRED(!cs); diff --git a/src/interfaces/node.h b/src/interfaces/node.h index e91b69e310f4..2baecfb39ca7 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -143,6 +143,11 @@ class GOV int32_t m_yes{0}; }; virtual Votes getObjVotes(const CGovernanceObject& obj, vote_signal_enum_t vote_signal) = 0; + struct UniqueVoters { + uint16_t m_regular{0}; + uint16_t m_evo{0}; + }; + virtual UniqueVoters getObjUniqueVoters(const CGovernanceObject& obj, vote_signal_enum_t vote_signal) = 0; virtual bool getObjLocalValidity(const CGovernanceObject& obj, std::string& error, bool check_collateral) = 0; virtual bool existsObj(const uint256& hash) = 0; virtual bool isEnabled() = 0; diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 8db8a2c610e5..975af2e68e4d 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -244,6 +244,20 @@ class GOVImpl : public GOV } return ret; } + UniqueVoters getObjUniqueVoters(const CGovernanceObject& obj, vote_signal_enum_t vote_signal) override + { + if (context().govman != nullptr && context().dmnman != nullptr) { + const auto& tip_mn_list{context().dmnman->GetListAtChainTip()}; + if (auto govobj{context().govman->FindGovernanceObject(obj.GetHash())}) { + const auto count = govobj->GetUniqueVoterCount(tip_mn_list, vote_signal); + return {.m_regular = count.m_regular, .m_evo = count.m_evo}; + } else { + const auto count = obj.GetUniqueVoterCount(tip_mn_list, vote_signal); + return {.m_regular = count.m_regular, .m_evo = count.m_evo}; + } + } + return {0, 0}; + } bool existsObj(const uint256& hash) override { if (context().govman != nullptr) { diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index af659cc2c708..69fca605a6b1 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -1009,6 +1009,10 @@ void BitcoinGUI::addWallet(WalletModel* walletModel) }); connect(wallet_view, &WalletView::encryptionStatusChanged, this, &BitcoinGUI::updateWalletStatus); connect(wallet_view, &WalletView::incomingTransaction, this, &BitcoinGUI::incomingTransaction); + connect(wallet_view, &WalletView::showProposalInfo, this, [this] { + rpcConsole->setInfoView(RPCConsole::InfoView::Governance); + showDebugWindow(); + }); connect(this, &BitcoinGUI::setPrivacy, wallet_view, &WalletView::setPrivacy); wallet_view->setPrivacy(isPrivacyModeActivated()); const QString display_name = walletModel->getDisplayName(); @@ -1817,26 +1821,35 @@ void BitcoinGUI::updateGovernanceCycleIcon() const auto remaining_blocks{std::max(0, gov_info.nextsuperblock - current_height)}; const auto remaining_str{GUIUtil::formatBlockDuration(remaining_blocks, gov_info.targetSpacing)}; const bool awaiting_superblock{current_height % gov_info.superblockcycle >= gov_info.superblockcycle - gov_info.superblockmaturitywindow}; + // Voting closes superblockmaturitywindow blocks before the superblock + const auto voting_remaining{std::max(0, remaining_blocks - gov_info.superblockmaturitywindow)}; + const auto voting_str{GUIUtil::formatBlockDuration(voting_remaining, gov_info.targetSpacing)}; - QString tooltip1{}; if (awaiting_superblock) { labelGovernanceCycleIcon->setPixmap(m_gov_cycle_pixmaps.at({ToUnderlying(GUIUtil::ThemedColor::BLUE), 0})); - tooltip1 = tr("~%1 (%2 blocks) left for superblock").arg(remaining_str).arg(remaining_blocks); } else { const auto cycle_blocks{gov_info.superblockcycle - gov_info.superblockmaturitywindow}; const auto blocks_elapsed{gov_info.superblockcycle - remaining_blocks - gov_info.superblockmaturitywindow}; const auto progress{static_cast(std::max(0, blocks_elapsed)) / static_cast(std::max(1, cycle_blocks))}; const auto frame{std::clamp(static_cast(progress * (GOV_CYCLE_FRAME_COUNT - 1)), 0, GOV_CYCLE_FRAME_COUNT - 2) + 1}; labelGovernanceCycleIcon->setPixmap(m_gov_cycle_pixmaps.at({ToUnderlying(GUIUtil::ThemedColor::GREEN), frame})); - tooltip1 = tr("~%1 (%2 blocks) left for voting").arg(remaining_str).arg(remaining_blocks); } - const auto allocated_budget{m_node.gov().getFundableProposalHashes().allocated}; - const auto budget_pct{gov_info.governancebudget > 0 - ? static_cast(static_cast(allocated_budget) / static_cast(gov_info.governancebudget) * 100.0) - : 0}; - const auto unit{options_model.getDisplayUnit()}; - const auto tooltip2{tr("~%1% of budget committed (%2 %3).").arg(budget_pct).arg(allocated_budget / BitcoinUnits::factor(unit)).arg(BitcoinUnits::name(unit))}; + const auto tooltip1{remaining_blocks == 0 + ? (awaiting_superblock ? tr("Superblock imminent") : tr("Voting period ended")) + : (awaiting_superblock + ? tr("~%1 (%2 blocks) left for superblock").arg(remaining_str).arg(remaining_blocks) + : tr("~%1 (%2 blocks) left for voting").arg(voting_str).arg(voting_remaining))}; + const auto tooltip2{[&]() { + const auto allocated_budget{m_node.gov().getFundableProposalHashes().allocated}; + const auto budget_pct{gov_info.governancebudget > 0 + ? static_cast(static_cast(allocated_budget) / static_cast(gov_info.governancebudget) * 100.0) + : 0}; + const auto unit{options_model.getDisplayUnit()}; + return tr("~%1% of budget committed (%2 / %3)").arg(budget_pct) + .arg(GUIUtil::formatAmount(unit, allocated_budget, /*is_signed=*/false, /*truncate=*/2)) + .arg(GUIUtil::formatAmount(unit, gov_info.governancebudget, /*is_signed=*/false, /*truncate=*/2)); + }()}; labelGovernanceCycleIcon->setToolTip(QString("%1
%2
").arg(tooltip1).arg(tooltip2)); labelGovernanceCycleIcon->show(); } diff --git a/src/qt/clientfeeds.cpp b/src/qt/clientfeeds.cpp index c9dad9391fcb..95ec4da26e12 100644 --- a/src/qt/clientfeeds.cpp +++ b/src/qt/clientfeeds.cpp @@ -231,10 +231,14 @@ void ProposalFeed::fetch() } ret->m_proposals.emplace_back(std::make_shared(m_client_model, govObj, ret->m_gov_info, ret->m_gov_info.requiredConfs, /*is_broadcast=*/true)); + const auto voters = m_client_model.node().gov().getObjUniqueVoters(govObj, VOTE_SIGNAL_FUNDING); + ret->m_max_regular_voters = std::max(ret->m_max_regular_voters, voters.m_regular); + ret->m_max_evo_voters = std::max(ret->m_max_evo_voters, voters.m_evo); } auto fundable{m_client_model.node().gov().getFundableProposalHashes()}; ret->m_fundable_hashes = std::move(fundable.hashes); + ret->m_allocated = fundable.allocated; setData(std::move(ret)); } diff --git a/src/qt/clientfeeds.h b/src/qt/clientfeeds.h index 08ab6e7fc853..b0c6bcb68f5a 100644 --- a/src/qt/clientfeeds.h +++ b/src/qt/clientfeeds.h @@ -190,9 +190,12 @@ class QuorumFeed : public Feed { using Proposals = std::vector>; struct ProposalData { + CAmount m_allocated{0}; int m_abs_vote_req{0}; interfaces::GOV::GovernanceInfo m_gov_info; Proposals m_proposals; + uint16_t m_max_evo_voters{0}; + uint16_t m_max_regular_voters{0}; Uint256HashSet m_fundable_hashes; }; diff --git a/src/qt/donutchart.cpp b/src/qt/donutchart.cpp new file mode 100644 index 000000000000..ec6bd919d365 --- /dev/null +++ b/src/qt/donutchart.cpp @@ -0,0 +1,228 @@ +// Copyright (c) 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 + +namespace { +constexpr int CHART_MARGIN{10}; +constexpr int TEXT_PADDING{20}; +} // anonymous namespace + +DonutChart::DonutChart(QWidget* parent) + : QWidget(parent) +{ + setMouseTracking(true); + setAttribute(Qt::WA_Hover); +} + +DonutChart::~DonutChart() = default; + +void DonutChart::setData(std::vector&& slices, double total_capacity, CenterText&& default_text) +{ + m_slices = std::move(slices); + m_total_capacity = total_capacity; + m_default_center_text = std::move(default_text); + m_hovered_slice = -1; + update(); +} + +void DonutChart::clear() +{ + m_slices.clear(); + m_total_capacity = 0; + m_hovered_slice = -1; + m_default_center_text = {}; + update(); +} + +QSize DonutChart::sizeHint() const +{ + return QSize(200, 200); +} + +QSize DonutChart::minimumSizeHint() const +{ + return QSize(150, 150); +} + +DonutChart::Geometry DonutChart::chartGeometry() const +{ + const int effective_height{static_cast(height() * 0.8)}; + const int side{qMin(width(), effective_height) - 2 * CHART_MARGIN}; + const int outer_radius{qMax(0, side / 2)}; + const int donut_thickness{qBound(25, outer_radius / 4, 60)}; + return { + .m_inner_radius = qMax(0, outer_radius - donut_thickness), + .m_outer_radius = outer_radius, + .m_center = {width() / 2, height() / 2} + }; +} + +void DonutChart::paintEvent(QPaintEvent* /*event*/) +{ + QPainter painter{this}; + painter.setRenderHint(QPainter::Antialiasing); + + const auto [inner_radius, outer_radius, center] = chartGeometry(); + if (outer_radius <= 0) return; + + // Draw background circle (unallocated portion) + painter.setPen(Qt::NoPen); + painter.setBrush(GUIUtil::getThemedQColor(GUIUtil::ThemedColor::BORDER_WIDGET)); + painter.drawEllipse(center, outer_radius, outer_radius); + + // Draw slices (clamped so total never exceeds 360 degrees) + if (m_total_capacity > 0 && !m_slices.empty()) { + int start_angle{90 * 16}; // Start from top (Qt uses 1/16th of a degree) + int remaining_angle{360 * 16}; + for (size_t idx{0}; idx < m_slices.size(); idx++) { + if (remaining_angle <= 0) break; + const auto& slice = m_slices[idx]; + if (slice.m_value <= 0) continue; + const double fraction{slice.m_value / m_total_capacity}; + const int span_angle{std::max(-(remaining_angle), static_cast(-fraction * 360 * 16))}; // Negative for clockwise + QColor color{slice.m_color}; + if (static_cast(idx) == m_hovered_slice) { + color = color.lighter(130); + } + painter.setBrush(color); + painter.drawPie(center.x() - outer_radius, center.y() - outer_radius, + outer_radius * 2, outer_radius * 2, start_angle, span_angle); + start_angle += span_angle; + remaining_angle += span_angle; // span_angle is negative + } + } + + // Draw inner circle + painter.setBrush(GUIUtil::getThemedQColor(GUIUtil::ThemedColor::BACKGROUND_WIDGET)); + painter.drawEllipse(center, inner_radius, inner_radius); + + // Draw center text + painter.setPen(GUIUtil::getThemedQColor(GUIUtil::ThemedColor::DEFAULT)); + + QString line1, line2, line3; + if (m_hovered_slice >= 0 && m_hovered_slice < static_cast(m_slices.size())) { + const auto& slice = m_slices[m_hovered_slice]; + line1 = slice.m_donut_center_label; + line2 = slice.m_donut_sub_label1; + line3 = slice.m_donut_sub_label2; + } else { + line1 = m_default_center_text.m_donut_center_label; + line2 = m_default_center_text.m_donut_sub_label1; + line3 = m_default_center_text.m_donut_sub_label2; + } + + // Calculate font sizes to fit in the inner circle - scale with widget size + const int max_text_width{inner_radius * 2 - TEXT_PADDING}; + QFont font{painter.font()}; + + // Scale font sizes based on inner radius (min 8pt, max reasonable sizes) + const int primary_font_size{qBound(9, inner_radius * 11 / 60, 20)}; + const int secondary_font_size{qBound(8, inner_radius * 11 / 80, 15)}; + + // Line 1 (name or allocated amount) - larger + font.setPointSize(primary_font_size); + font.setBold(true); + painter.setFont(font); + QFontMetrics fm1{font}; + QString elided_line1{fm1.elidedText(line1, Qt::ElideRight, max_text_width)}; + + // Lines 2 and 3 - smaller + font.setPointSize(secondary_font_size); + font.setBold(false); + painter.setFont(font); + QFontMetrics fm2{font}; + + const int line_height{fm2.height()}; + const int total_height{fm1.height() + line_height * 2 + 4}; + int y{center.y() - total_height / 2}; + + // Draw line 1 + font.setPointSize(primary_font_size); + font.setBold(true); + painter.setFont(font); + painter.drawText(QRect(center.x() - max_text_width / 2, y, max_text_width, fm1.height()), + Qt::AlignCenter, elided_line1); + y += fm1.height() + 2; + + // Draw lines 2 and 3 + font.setPointSize(secondary_font_size); + font.setBold(false); + painter.setFont(font); + const QString elided_line2{fm2.elidedText(line2, Qt::ElideRight, max_text_width)}; + const QString elided_line3{fm2.elidedText(line3, Qt::ElideRight, max_text_width)}; + painter.drawText(QRect(center.x() - max_text_width / 2, y, max_text_width, line_height), + Qt::AlignCenter, elided_line2); + y += line_height; + painter.drawText(QRect(center.x() - max_text_width / 2, y, max_text_width, line_height), + Qt::AlignCenter, elided_line3); +} + +int DonutChart::sliceAtPosition(const QPoint& pos) const +{ + if (m_slices.empty() || m_total_capacity <= 0) { + return -1; + } + + const auto [inner_radius, outer_radius, center] = chartGeometry(); + + // Check if point is within the donut ring + const int dx{pos.x() - center.x()}; + const int dy{pos.y() - center.y()}; + const double dist{qSqrt(dx * dx + dy * dy)}; + + if (dist < inner_radius || dist > outer_radius) { + return -1; + } + + // Calculate angle (0 = top, clockwise) + // Note: swapped and negated for top=0, clockwise + double angle{qAtan2(dx, -dy)}; + if (angle < 0) { + angle += 2 * M_PI; + } + + // Find which slice this angle falls into. + // Mirror paintEvent's clamping so hit-regions match what is painted. + double current_angle{0}; + double remaining{2 * M_PI}; + for (size_t idx{0}; idx < m_slices.size(); idx++) { + if (remaining <= 0) break; + if (m_slices[idx].m_value <= 0) continue; + const double fraction{m_slices[idx].m_value / m_total_capacity}; + const double slice_angle{std::min(fraction * 2 * M_PI, remaining)}; + if (angle >= current_angle && angle < current_angle + slice_angle) { + return static_cast(idx); + } + current_angle += slice_angle; + remaining -= slice_angle; + } + + return -1; +} + +void DonutChart::mouseMoveEvent(QMouseEvent* event) +{ + const int slice{sliceAtPosition(event->pos())}; + if (slice != m_hovered_slice) { + m_hovered_slice = slice; + update(); + } + QWidget::mouseMoveEvent(event); +} + +void DonutChart::leaveEvent(QEvent* event) +{ + if (m_hovered_slice != -1) { + m_hovered_slice = -1; + update(); + } + QWidget::leaveEvent(event); +} diff --git a/src/qt/donutchart.h b/src/qt/donutchart.h new file mode 100644 index 000000000000..4ac5085122f7 --- /dev/null +++ b/src/qt/donutchart.h @@ -0,0 +1,62 @@ +// Copyright (c) 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. + +#ifndef BITCOIN_QT_DONUTCHART_H +#define BITCOIN_QT_DONUTCHART_H + +#include + +#include + +class DonutChart : public QWidget +{ + Q_OBJECT + +public: + struct Slice { + double m_value{0.0}; + QColor m_color; + QString m_donut_center_label; + QString m_donut_sub_label1; + QString m_donut_sub_label2; + }; + + struct CenterText { + QString m_donut_center_label; + QString m_donut_sub_label1; + QString m_donut_sub_label2; + }; + + explicit DonutChart(QWidget* parent = nullptr); + ~DonutChart() override; + + void setData(std::vector&& slices, double total_capacity, CenterText&& default_text); + void clear(); + + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + +protected: + void paintEvent(QPaintEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void leaveEvent(QEvent* event) override; + +private: + struct Geometry { + int m_inner_radius; + int m_outer_radius; + QPoint m_center; + }; + + Geometry chartGeometry() const; + int sliceAtPosition(const QPoint& pos) const; + +private: + CenterText m_default_center_text; + double m_total_capacity{0}; + int m_hovered_slice{-1}; + std::vector m_slices; +}; + +#endif // BITCOIN_QT_DONUTCHART_H diff --git a/src/qt/forms/debugwindow.ui b/src/qt/forms/debugwindow.ui index e1749c2d85af..a669d79602a2 100644 --- a/src/qt/forms/debugwindow.ui +++ b/src/qt/forms/debugwindow.ui @@ -136,6 +136,20 @@ + + + Go&vernance + + + 0 + 0 + 0 + 0 + + + + + @@ -1342,6 +1356,12 @@ + + ProposalInfo + QWidget +
qt/proposalinfo.h
+ 1 +
InformationWidget QWidget diff --git a/src/qt/forms/proposalcreate.ui b/src/qt/forms/proposalcreate.ui index 9cbe85c342c9..f75e7320729d 100644 --- a/src/qt/forms/proposalcreate.ui +++ b/src/qt/forms/proposalcreate.ui @@ -323,7 +323,6 @@ - diff --git a/src/qt/forms/proposalinfo.ui b/src/qt/forms/proposalinfo.ui new file mode 100644 index 000000000000..ad150b2c55e3 --- /dev/null +++ b/src/qt/forms/proposalinfo.ui @@ -0,0 +1,380 @@ + + + ProposalInfo + + + + 0 + 0 + 700 + 400 + + + + + + + + 1 + 0 + + + + + 12 + + + + + General + + + + + + + Voting cycles + + + + + + + IBeamCursor + + + N/A + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Last superblock + + + + + + + IBeamCursor + + + N/A + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Next superblock + + + + + + + IBeamCursor + + + N/A + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Voting cutoff block + + + + + + + IBeamCursor + + + N/A + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Participation + + + + + + + Masternodes voting + + + + + + + IBeamCursor + + + N/A + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + EvoNodes voting + + + + + + + IBeamCursor + + + N/A + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Passing threshold + + + + + + + IBeamCursor + + + N/A + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Node + + + + + + + Masternodes controlled + + + + + + + IBeamCursor + + + N/A + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Votes controlled + + + + + + + IBeamCursor + + + N/A + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Proposals + + + + + + + Proposal Count + + + + + + + IBeamCursor + + + N/A + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Budget allocated + + + + + + + IBeamCursor + + + N/A + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Passing Proposals + + + + + + + IBeamCursor + + + N/A + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Unfunded proposals + + + 10 + + + + + + + IBeamCursor + + + N/A + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Failing Proposals + + + + + + + IBeamCursor + + + N/A + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + 150 + 150 + + + + + 1 + 0 + + + + + + + + + DonutChart + QWidget +
qt/donutchart.h
+
+
+ + +
diff --git a/src/qt/forms/proposallist.ui b/src/qt/forms/proposallist.ui index 67ee680f0a5b..6acc6ca54eaa 100644 --- a/src/qt/forms/proposallist.ui +++ b/src/qt/forms/proposallist.ui @@ -60,21 +60,7 @@
- - - Masternode Count: - - - Number of masternodes this wallet can vote with (masternodes for which this wallet holds the voting key) - - - - - - - 0 - - + diff --git a/src/qt/proposalinfo.cpp b/src/qt/proposalinfo.cpp new file mode 100644 index 000000000000..daa8e211ba81 --- /dev/null +++ b/src/qt/proposalinfo.cpp @@ -0,0 +1,242 @@ +// Copyright (c) 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