From f0d49e4f69d02e9c63d8ba9b943dbb6bf21644df Mon Sep 17 00:00:00 2001 From: Stefan <5tefan@users.noreply.github.com> Date: Sat, 30 Oct 2021 01:14:25 +0000 Subject: [PATCH] Qt: Adds Governance tab (#4351) * Qt: Adds settings option to showGovernanceTab * Qt: Adds not-yet-functional Governance tab to UI * library: adds hook into governance rom node interface * Qt: WIP: puts CGovernanceObject hashes on Governance tab WIP: basically ready to be filled out, most of the infra in place now. * Qt: Populates Governance table values Governance table now contains real values for current columns. Display columns may potentially change. - want url in table if right click option opens url? - What set of cols wanted? Show yes/no votes? - Decide refresh functionality. - Do we even want filter func? (checkbox show: proposal, trigger, active coming) * qt: Shows Voting Status on Governance page Towards DMT like Governance page, now shows voting status, but needs to look at super blocks to determine if proposal period has passed and show final funded or unfunded for past proposals. * Qt: refactor governance tab to use Model-View arch Refactors the QT Governance tab to use a Model-View architecture. The list of Proposals is stored in a QAbstractItemModel, and a QTableView is used to display. This makes a much cleaner separation of concerns in the implementation. Also, can now use the QSortFilterProxyModel to get responsive filtering. Less internal state inside the GovernanceList, critical sections removed. - Needs update loop implemented. - Needs Proposal detail widget implemented. * qt: adds periodic update to gov proposal list Periodically update list of governance related proposals. * qt: populates governancelist voting status column * qt: governancelist Porposal shows extra info on doubleClick * qt: governancelist: reorder .cpp impl to match .h * qt: governancelist: fixup based on CI - Adds LOCK(cs_main) for call to pGovObj->IsValidLocally. - retab, removes trailing whitespace in governancelist.{cpp,h} * qt: clang-format for governancelist * Fixes - drop unused include - use proper univalue "get_" functions - shorter/translatable statuses - shorter dates - add missing css styles * qt: addresses governancelist code review items * qt: use enum to name columns of governancelist * fix: clear context menu before adding new items * fix: skip potential nullptr proposals * improve: show proposal url in context menu * refactor: tweak enum name, make sure it's int starting from 0, use full name * fix: column count * improve: make it sortable, sort by start_date by default * qt: use full col name in voting status col change signal emit * better sorting * use mapToSource to fix data fetching Co-authored-by: UdjinM6 --- contrib/dash-qt.pro | 1 + src/Makefile.qt.include | 4 + src/interfaces/node.cpp | 12 + src/interfaces/node.h | 12 + src/qt/bitcoingui.cpp | 21 +- src/qt/bitcoingui.h | 3 + src/qt/clientmodel.cpp | 8 +- src/qt/clientmodel.h | 3 + src/qt/forms/governancelist.ui | 109 +++++++++ src/qt/forms/optionsdialog.ui | 10 + src/qt/governancelist.cpp | 402 +++++++++++++++++++++++++++++++++ src/qt/governancelist.h | 116 ++++++++++ src/qt/optionsdialog.cpp | 3 +- src/qt/optionsmodel.cpp | 8 + src/qt/optionsmodel.h | 71 +++--- src/qt/res/css/dark.css | 8 + src/qt/res/css/light.css | 8 + src/qt/walletframe.cpp | 20 ++ src/qt/walletframe.h | 4 + src/qt/walletview.cpp | 15 ++ src/qt/walletview.h | 4 + 21 files changed, 804 insertions(+), 38 deletions(-) create mode 100644 src/qt/forms/governancelist.ui create mode 100644 src/qt/governancelist.cpp create mode 100644 src/qt/governancelist.h diff --git a/contrib/dash-qt.pro b/contrib/dash-qt.pro index 708978d1d5..5ce552cedf 100644 --- a/contrib/dash-qt.pro +++ b/contrib/dash-qt.pro @@ -6,6 +6,7 @@ FORMS += \ ../src/qt/forms/coincontroldialog.ui \ ../src/qt/forms/debugwindow.ui \ ../src/qt/forms/editaddressdialog.ui \ + ../src/qt/forms/governancelist.ui \ ../src/qt/forms/helpmessagedialog.ui \ ../src/qt/forms/intro.ui \ ../src/qt/forms/masternodelist.ui \ diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 21456e11c3..cb0ab527a4 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -36,6 +36,7 @@ QT_FORMS_UI = \ qt/forms/askpassphrasedialog.ui \ qt/forms/coincontroldialog.ui \ qt/forms/editaddressdialog.ui \ + qt/forms/governancelist.ui \ qt/forms/helpmessagedialog.ui \ qt/forms/intro.ui \ qt/forms/modaloverlay.ui \ @@ -68,6 +69,7 @@ QT_MOC_CPP = \ qt/moc_csvmodelwriter.cpp \ qt/moc_dash.cpp \ qt/moc_editaddressdialog.cpp \ + qt/moc_governancelist.cpp \ qt/moc_guiutil.cpp \ qt/moc_intro.cpp \ qt/moc_macdockiconhandler.cpp \ @@ -146,6 +148,7 @@ BITCOIN_QT_H = \ qt/csvmodelwriter.h \ qt/dash.h \ qt/editaddressdialog.h \ + qt/governancelist.h \ qt/guiconstants.h \ qt/guiutil.h \ qt/intro.h \ @@ -257,6 +260,7 @@ BITCOIN_QT_WALLET_CPP = \ qt/coincontroldialog.cpp \ qt/coincontroltreewidget.cpp \ qt/editaddressdialog.cpp \ + qt/governancelist.cpp \ qt/masternodelist.cpp \ qt/openuridialog.cpp \ qt/overviewpage.cpp \ diff --git a/src/interfaces/node.cpp b/src/interfaces/node.cpp index 0064731861..18d4b4e3fa 100644 --- a/src/interfaces/node.cpp +++ b/src/interfaces/node.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -66,6 +67,15 @@ public: } }; +class GOVImpl : public GOV +{ +public: + std::vector getAllNewerThan(int64_t nMoreThanTime) override + { + return governance.GetAllNewerThan(nMoreThanTime); + } +}; + class LLMQImpl : public LLMQ { public: @@ -162,6 +172,7 @@ public: NodeImpl() { m_interfaces.chain = MakeChain(); } EVOImpl m_evo; + GOVImpl m_gov; LLMQImpl m_llmq; MasternodeSyncImpl m_masternodeSync; CoinJoinOptionsImpl m_coinjoin; @@ -385,6 +396,7 @@ public: } EVO& evo() override { return m_evo; } + GOV& gov() override { return m_gov; } LLMQ& llmq() override { return m_llmq; } Masternode::Sync& masternodeSync() override { return m_masternodeSync; } CoinJoin::Options& coinJoinOptions() override { return m_coinjoin; } diff --git a/src/interfaces/node.h b/src/interfaces/node.h index c21c2d821f..ec6a41f068 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -22,6 +22,7 @@ class BanMan; class CCoinControl; class CDeterministicMNList; class CFeeRate; +class CGovernanceObject; class CNodeStats; class Coin; class RPCTimerInterface; @@ -41,6 +42,14 @@ public: virtual CDeterministicMNList getListAtChainTip() = 0; }; +//! Interface for the src/governance part of a dash node (dashd process). +class GOV +{ +public: + virtual ~GOV() {} + virtual std::vector getAllNewerThan(int64_t nMoreThanTime) = 0; +}; + //! Interface for the src/llmq part of a dash node (dashd process). class LLMQ { @@ -261,6 +270,9 @@ public: //! Return interface for accessing evo related handler. virtual EVO& evo() = 0; + //! Return interface for accessing governance related handler. + virtual GOV& gov() = 0; + //! Return interface for accessing llmq related handler. virtual LLMQ& llmq() = 0; diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 24ba2be0d5..ca9ce3228c 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -33,9 +33,10 @@ #include #include #include +#include +#include #include #include -#include #include #include @@ -688,6 +689,15 @@ void BitcoinGUI::createToolBars() masternodeButton->setEnabled(true); } + if (settings.value("fShowGovernanceTab").toBool()) { + governanceButton = new QToolButton(this); + governanceButton->setText(tr("&Governance")); + governanceButton->setStatusTip(tr("View Governance Proposals")); + tabGroup->addButton(governanceButton); + connect(governanceButton, &QToolButton::clicked, this, &BitcoinGUI::gotoGovernancePage); + governanceButton->setEnabled(true); + } + connect(overviewButton, &QToolButton::clicked, this, &BitcoinGUI::gotoOverviewPage); connect(sendCoinsButton, &QToolButton::clicked, [this]{ gotoSendCoinsPage(); }); connect(coinJoinCoinsButton, &QToolButton::clicked, [this]{ gotoCoinJoinCoinsPage(); }); @@ -1120,6 +1130,15 @@ void BitcoinGUI::highlightTabButton(QAbstractButton *button, bool checked) GUIUtil::updateFonts(); } +void BitcoinGUI::gotoGovernancePage() +{ + QSettings settings; + if (settings.value("fShowGovernanceTab").toBool() && governanceButton) { + governanceButton->setChecked(true); + if (walletFrame) walletFrame->gotoGovernancePage(); + } +} + void BitcoinGUI::gotoOverviewPage() { overviewButton->setChecked(true); diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index 33c3a84753..adaf9206ea 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -132,6 +132,7 @@ private: QToolButton* receiveCoinsButton = nullptr; QToolButton* historyButton = nullptr; QToolButton* masternodeButton = nullptr; + QToolButton* governanceButton = nullptr; QAction* appToolBarLogoAction = nullptr; QAction* quitAction = nullptr; QAction* sendCoinsMenuAction = nullptr; @@ -298,6 +299,8 @@ private: public Q_SLOTS: #ifdef ENABLE_WALLET + /** Switch to governance page */ + void gotoGovernancePage(); /** Switch to overview (home) page */ void gotoOverviewPage(); /** Switch to history (transactions) page */ diff --git a/src/qt/clientmodel.cpp b/src/qt/clientmodel.cpp index c02265d825..11e459fd8e 100644 --- a/src/qt/clientmodel.cpp +++ b/src/qt/clientmodel.cpp @@ -15,14 +15,15 @@ #include #include #include +#include #include #include -#include #include #include #include #include #include +#include #include #include @@ -122,6 +123,11 @@ int64_t ClientModel::getHeaderTipTime() const return cachedBestHeaderTime; } +std::vector ClientModel::getAllGovernanceObjects() +{ + return m_node.gov().getAllNewerThan(0); +} + void ClientModel::updateTimer() { // no locking required at this point diff --git a/src/qt/clientmodel.h b/src/qt/clientmodel.h index ae5efa4098..69d16b7bba 100644 --- a/src/qt/clientmodel.h +++ b/src/qt/clientmodel.h @@ -40,6 +40,7 @@ enum NumConnections { }; class CDeterministicMNList; +class CGovernanceObject; typedef std::shared_ptr CDeterministicMNListPtr; /** Model for Dash network client. */ @@ -67,6 +68,8 @@ public: CDeterministicMNList getMasternodeList() const; void refreshMasternodeList(); + std::vector getAllGovernanceObjects(); + //! Returns enum BlockSource of the current importing/syncing state enum BlockSource getBlockSource() const; //! Return warnings to be displayed in status bar diff --git a/src/qt/forms/governancelist.ui b/src/qt/forms/governancelist.ui new file mode 100644 index 0000000000..2890c87120 --- /dev/null +++ b/src/qt/forms/governancelist.ui @@ -0,0 +1,109 @@ + + + GovernanceList + + + + 0 + 0 + 762 + 457 + + + + Form + + + + 20 + + + 0 + + + 20 + + + 0 + + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 0 + + + + + Filter List: + + + + + + + Filter propsal list + + + + + + + Qt::Horizontal + + + + 10 + 20 + + + + + + + + Proposal Count: + + + + + + + 0 + + + + + + + + + + + + + + + + + + diff --git a/src/qt/forms/optionsdialog.ui b/src/qt/forms/optionsdialog.ui index c797bdf4bf..8779266381 100644 --- a/src/qt/forms/optionsdialog.ui +++ b/src/qt/forms/optionsdialog.ui @@ -328,6 +328,16 @@ + + + + Show additional tab listing governance proposals. + + + Show Governance Tab + + + diff --git a/src/qt/governancelist.cpp b/src/qt/governancelist.cpp new file mode 100644 index 0000000000..263508dd63 --- /dev/null +++ b/src/qt/governancelist.cpp @@ -0,0 +1,402 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +/// +/// Proposal wrapper +/// + +Proposal::Proposal(const CGovernanceObject* p, QObject* parent) : + QObject(parent), + pGovObj(p) +{ + UniValue prop_data; + if (prop_data.read(pGovObj->GetDataAsPlainString())) { + if (UniValue titleValue = find_value(prop_data, "name"); titleValue.isStr()) { + m_title = QString::fromStdString(titleValue.get_str()); + } + + if (UniValue paymentStartValue = find_value(prop_data, "start_epoch"); paymentStartValue.isNum()) { + m_startDate = QDateTime::fromSecsSinceEpoch(paymentStartValue.get_int64()); + } + + if (UniValue paymentEndValue = find_value(prop_data, "end_epoch"); paymentEndValue.isNum()) { + m_endDate = QDateTime::fromSecsSinceEpoch(paymentEndValue.get_int64()); + } + + if (UniValue amountValue = find_value(prop_data, "payment_amount"); amountValue.isNum()) { + m_paymentAmount = amountValue.get_real(); + } + + if (UniValue urlValue = find_value(prop_data, "url"); urlValue.isStr()) { + m_url = QString::fromStdString(urlValue.get_str()); + } + } +} + +QString Proposal::title() const { return m_title; } + +QString Proposal::hash() const { return QString::fromStdString(pGovObj->GetHash().ToString()); } + +QDateTime Proposal::startDate() const { return m_startDate; } + +QDateTime Proposal::endDate() const { return m_endDate; } + +float Proposal::paymentAmount() const { return m_paymentAmount; } + +QString Proposal::url() const { return m_url; } + +bool Proposal::isActive() const +{ + std::string strError; + LOCK(cs_main); + return pGovObj->IsValidLocally(strError, false); +} + +QString Proposal::votingStatus(const int nAbsVoteReq) const +{ + // Voting status... + // TODO: determine if voting is in progress vs. funded or not funded for past proposals. + // see CSuperblock::GetNearestSuperblocksHeights(nBlockHeight, nLastSuperblock, nNextSuperblock); + const int absYesCount = pGovObj->GetAbsoluteYesCount(VOTE_SIGNAL_FUNDING); + QString qStatusString; + if (absYesCount >= nAbsVoteReq) { + // Could use pGovObj->IsSetCachedFunding here, but need nAbsVoteReq to display numbers anyway. + return tr("Passing +%1").arg(absYesCount - nAbsVoteReq); + } else { + return tr("Needs additional %1 votes").arg(nAbsVoteReq - absYesCount); + } +} + +int Proposal::GetAbsoluteYesCount() const +{ + return pGovObj->GetAbsoluteYesCount(VOTE_SIGNAL_FUNDING); +} + +void Proposal::openUrl() const +{ + QDesktopServices::openUrl(QUrl(m_url)); +} + +QString Proposal::toJson() const +{ + const auto json = pGovObj->ToJson(); + return QString::fromStdString(json.write(2)); +} + +/// +/// Proposal Model +/// + + +int ProposalModel::rowCount(const QModelIndex& index) const +{ + return m_data.count(); +} + +int ProposalModel::columnCount(const QModelIndex& index) const +{ + return Column::_COUNT; +} + +QVariant ProposalModel::data(const QModelIndex& index, int role) const +{ + if (role != Qt::DisplayRole && role != Qt::EditRole) return {}; + const auto proposal = m_data[index.row()]; + switch(role) { + case Qt::DisplayRole: + { + switch (index.column()) { + case Column::HASH: + return proposal->hash(); + case Column::TITLE: + return proposal->title(); + case Column::START_DATE: + return proposal->startDate().date(); + case Column::END_DATE: + return proposal->endDate().date(); + case Column::PAYMENT_AMOUNT: + return proposal->paymentAmount(); + case Column::IS_ACTIVE: + return proposal->isActive() ? "Y" : "N"; + case Column::VOTING_STATUS: + return proposal->votingStatus(nAbsVoteReq); + default: + return {}; + }; + break; + } + case Qt::EditRole: + { + // Edit role is used for sorting, so return the raw values where possible + switch (index.column()) { + case Column::HASH: + return proposal->hash(); + case Column::TITLE: + return proposal->title(); + case Column::START_DATE: + return proposal->startDate(); + case Column::END_DATE: + return proposal->endDate(); + case Column::PAYMENT_AMOUNT: + return proposal->paymentAmount(); + case Column::IS_ACTIVE: + return proposal->isActive(); + case Column::VOTING_STATUS: + return proposal->GetAbsoluteYesCount(); + default: + return {}; + }; + break; + } + }; + return {}; +} + +QVariant ProposalModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation != Qt::Horizontal || role != Qt::DisplayRole) return {}; + switch (section) { + case Column::HASH: + return "Hash"; + case Column::TITLE: + return "Title"; + case Column::START_DATE: + return "Start"; + case Column::END_DATE: + return "End"; + case Column::PAYMENT_AMOUNT: + return "Amount"; + case Column::IS_ACTIVE: + return "Active"; + case Column::VOTING_STATUS: + return "Status"; + default: + return {}; + } +} + +int ProposalModel::columnWidth(int section) +{ + switch (section) { + case Column::HASH: + return 80; + case Column::TITLE: + return 220; + case Column::START_DATE: + case Column::END_DATE: + case Column::PAYMENT_AMOUNT: + return 110; + case Column::IS_ACTIVE: + return 80; + case Column::VOTING_STATUS: + return 220; + default: + return 80; + } +} + +void ProposalModel::append(const Proposal* proposal) +{ + beginInsertRows({}, m_data.count(), m_data.count()); + m_data.append(proposal); + endInsertRows(); +} + +void ProposalModel::remove(int row) +{ + beginRemoveRows({}, row, row); + m_data.removeAt(row); + endRemoveRows(); +} + +void ProposalModel::reconcile(const std::vector& proposals) +{ + // Vector of m_data.count() false values. Going through new proposals, + // set keep_index true for each old proposal found in the new proposals. + // After going through new proposals, remove any existing proposals that + // weren't found (and are still false). + std::vector keep_index(m_data.count(), false); + for (const auto proposal : proposals) { + bool found = false; + for (unsigned int i = 0; i < m_data.count(); ++i) { + if (m_data.at(i)->hash() == proposal->hash()) { + found = true; + keep_index.at(i) = true; + break; + } + } + if (!found) { + append(proposal); + } + } + for (unsigned int i = keep_index.size(); i > 0; --i) { + if (!keep_index.at(i - 1)) { + remove(i - 1); + } + } +} + + +void ProposalModel::setVotingParams(int newAbsVoteReq) +{ + if (this->nAbsVoteReq != newAbsVoteReq) { + this->nAbsVoteReq = newAbsVoteReq; + // Changing either of the voting params may change the voting status + // column. Emit signal to force recalculation. + Q_EMIT dataChanged(createIndex(0, Column::VOTING_STATUS), createIndex(columnCount(), Column::VOTING_STATUS)); + } +} + +const Proposal* ProposalModel::getProposalAt(const QModelIndex& index) const +{ + return m_data[index.row()]; +} + +// +// Governance Tab main widget. +// + +GovernanceList::GovernanceList(QWidget* parent) : + QWidget(parent), + ui(new Ui::GovernanceList), + clientModel(nullptr), + proposalModel(new ProposalModel(this)), + proposalModelProxy(new QSortFilterProxyModel(this)), + proposalContextMenu(new QMenu(this)), + timer(new QTimer(this)) +{ + ui->setupUi(this); + + GUIUtil::setFont({ui->label_count_2, ui->countLabel}, GUIUtil::FontWeight::Bold, 14); + GUIUtil::setFont({ui->label_filter_2}, GUIUtil::FontWeight::Normal, 15); + + proposalModelProxy->setSourceModel(proposalModel); + ui->govTableView->setModel(proposalModelProxy); + ui->govTableView->setSelectionBehavior(QAbstractItemView::SelectRows); + ui->govTableView->horizontalHeader()->setStretchLastSection(true); + + for (int i = 0; i < proposalModel->columnCount(); ++i) { + ui->govTableView->setColumnWidth(i, proposalModel->columnWidth(i)); + } + + // Set up sorting. + proposalModelProxy->setSortRole(Qt::EditRole); + ui->govTableView->setSortingEnabled(true); + ui->govTableView->sortByColumn(ProposalModel::Column::START_DATE, Qt::DescendingOrder); + + // Set up filtering. + proposalModelProxy->setFilterKeyColumn(ProposalModel::Column::TITLE); // filter by title column... + ui->filterLineEdit->setPlaceholderText(tr("Filter by Title")); + connect(ui->filterLineEdit, &QLineEdit::textChanged, proposalModelProxy, &QSortFilterProxyModel::setFilterFixedString); + + // Changes to number of rows should update proposal count display. + connect(proposalModelProxy, &QSortFilterProxyModel::rowsInserted, this, &GovernanceList::updateProposalCount); + connect(proposalModelProxy, &QSortFilterProxyModel::rowsRemoved, this, &GovernanceList::updateProposalCount); + connect(proposalModelProxy, &QSortFilterProxyModel::layoutChanged, this, &GovernanceList::updateProposalCount); + + // Enable CustomContextMenu on the table to make the view emit customContextMenuRequested signal. + ui->govTableView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->govTableView, &QTableView::customContextMenuRequested, this, &GovernanceList::showProposalContextMenu); + connect(ui->govTableView, &QTableView::doubleClicked, this, &GovernanceList::showAdditionalInfo); + + connect(timer, &QTimer::timeout, this, &GovernanceList::updateProposalList); + + GUIUtil::updateFonts(); +} + +GovernanceList::~GovernanceList() +{ + delete ui; +} + +void GovernanceList::setClientModel(ClientModel* model) +{ + this->clientModel = model; + updateProposalList(); +} + +void GovernanceList::updateProposalList() +{ + if (this->clientModel) { + // A proposal is considered passing if (YES votes - NO votes) >= (Total Number 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 nMnCount = clientModel->getMasternodeList().GetValidMNsCount(); + const int nAbsVoteReq = std::max(Params().GetConsensus().nGovernanceMinQuorum, nMnCount / 10); + proposalModel->setVotingParams(nAbsVoteReq); + + const std::vector govObjList = clientModel->getAllGovernanceObjects(); + std::vector newProposals; + for (const auto pGovObj : govObjList) { + if (pGovObj->GetObjectType() != GOVERNANCE_OBJECT_PROPOSAL) { + continue; // Skip triggers. + } + + newProposals.emplace_back(new Proposal(pGovObj, proposalModel)); + } + proposalModel->reconcile(newProposals); + } + + // Schedule next update. + timer->start(GOVERNANCELIST_UPDATE_SECONDS * 1000); +} + +void GovernanceList::updateProposalCount() const +{ + ui->countLabel->setText(QString::number(proposalModelProxy->rowCount())); +} + +void GovernanceList::showProposalContextMenu(const QPoint& pos) +{ + const auto index = ui->govTableView->indexAt(pos); + + if (!index.isValid()) { + return; + } + + const auto proposal = proposalModel->getProposalAt(proposalModelProxy->mapToSource(index)); + if (proposal == nullptr) { + return; + } + + // right click menu with option to open proposal url + QAction* openProposalUrl = new QAction(proposal->url(), this); + proposalContextMenu->clear(); + proposalContextMenu->addAction(openProposalUrl); + connect(openProposalUrl, &QAction::triggered, proposal, &Proposal::openUrl); + proposalContextMenu->exec(QCursor::pos()); +} + +void GovernanceList::showAdditionalInfo(const QModelIndex& index) +{ + if (!index.isValid()) { + return; + } + + const auto proposal = proposalModel->getProposalAt(proposalModelProxy->mapToSource(index)); + if (proposal == nullptr) { + return; + } + + const auto windowTitle = tr("Proposal Info: %1").arg(proposal->title()); + const auto json = proposal->toJson(); + + QMessageBox::information(this, windowTitle, json); +} diff --git a/src/qt/governancelist.h b/src/qt/governancelist.h new file mode 100644 index 0000000000..d352508adb --- /dev/null +++ b/src/qt/governancelist.h @@ -0,0 +1,116 @@ +#ifndef BITCOIN_QT_GOVERNANCELIST_H +#define BITCOIN_QT_GOVERNANCELIST_H + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +inline constexpr int GOVERNANCELIST_UPDATE_SECONDS = 10; + +namespace Ui { +class GovernanceList; +} + +class CDeterministicMNList; +class ClientModel; + +class Proposal : public QObject +{ +private: + Q_OBJECT + + const CGovernanceObject* pGovObj; + QString m_title; + QDateTime m_startDate; + QDateTime m_endDate; + float m_paymentAmount; + QString m_url; + +public: + Proposal(const CGovernanceObject* p, QObject* parent = nullptr); + QString title() const; + QString hash() const; + QDateTime startDate() const; + QDateTime endDate() const; + float paymentAmount() const; + QString url() const; + bool isActive() const; + QString votingStatus(const int nAbsVoteReq) const; + int GetAbsoluteYesCount() const; + + void openUrl() const; + + QString toJson() const; +}; + +class ProposalModel : public QAbstractTableModel +{ +private: + QList m_data; + int nAbsVoteReq = 0; + +public: + explicit ProposalModel(QObject* parent = nullptr) : + QAbstractTableModel(parent){}; + + enum Column : int { + HASH = 0, + TITLE, + START_DATE, + END_DATE, + PAYMENT_AMOUNT, + IS_ACTIVE, + VOTING_STATUS, + _COUNT // for internal use only + }; + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + static int columnWidth(int section); + void append(const Proposal* proposal); + void remove(int row); + void reconcile(const std::vector& proposals); + void setVotingParams(int nAbsVoteReq); + + const Proposal* getProposalAt(const QModelIndex& index) const; +}; + +/** Governance Manager page widget */ +class GovernanceList : public QWidget +{ + Q_OBJECT + +public: + explicit GovernanceList(QWidget* parent = nullptr); + ~GovernanceList() override; + void setClientModel(ClientModel* clientModel); + +private: + ClientModel* clientModel; + + Ui::GovernanceList* ui; + ProposalModel* proposalModel; + QSortFilterProxyModel* proposalModelProxy; + + QMenu* proposalContextMenu; + QTimer* timer; + +private Q_SLOTS: + void updateProposalList(); + void updateProposalCount() const; + void showProposalContextMenu(const QPoint& pos); + void showAdditionalInfo(const QModelIndex& index); +}; + + +#endif // BITCOIN_QT_GOVERNANCELIST_H diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp index 82d79e0229..6f8f4c1a79 100644 --- a/src/qt/optionsdialog.cpp +++ b/src/qt/optionsdialog.cpp @@ -247,7 +247,7 @@ void OptionsDialog::setModel(OptionsModel *_model) connect(ui->threadsScriptVerif, static_cast(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning); /* Wallet */ connect(ui->showMasternodesTab, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning); - connect(ui->spendZeroConfChange, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning); + connect(ui->showGovernanceTab, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning); connect(ui->spendZeroConfChange, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning); /* Network */ connect(ui->allowIncoming, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning); @@ -302,6 +302,7 @@ void OptionsDialog::setMapper() /* Wallet */ mapper->addMapping(ui->coinControlFeatures, OptionsModel::CoinControlFeatures); mapper->addMapping(ui->showMasternodesTab, OptionsModel::ShowMasternodesTab); + mapper->addMapping(ui->showGovernanceTab, OptionsModel::ShowGovernanceTab); mapper->addMapping(ui->showAdvancedCJUI, OptionsModel::ShowAdvancedCJUI); mapper->addMapping(ui->showCoinJoinPopups, OptionsModel::ShowCoinJoinPopups); mapper->addMapping(ui->lowKeysWarning, OptionsModel::LowKeysWarning); diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp index a3b5a1e3bd..5509d29437 100644 --- a/src/qt/optionsmodel.cpp +++ b/src/qt/optionsmodel.cpp @@ -395,6 +395,8 @@ QVariant OptionsModel::data(const QModelIndex & index, int role) const return settings.value("bSpendZeroConfChange"); case ShowMasternodesTab: return settings.value("fShowMasternodesTab"); + case ShowGovernanceTab: + return settings.value("fShowGovernanceTab"); case CoinJoinEnabled: return settings.value("fCoinJoinEnabled"); case ShowAdvancedCJUI: @@ -556,6 +558,12 @@ bool OptionsModel::setData(const QModelIndex & index, const QVariant & value, in setRestartRequired(true); } break; + case ShowGovernanceTab: + if (settings.value("fShowGovernanceTab") != value) { + settings.setValue("fShowGovernanceTab", value); + setRestartRequired(true); + } + break; case CoinJoinEnabled: if (settings.value("fCoinJoinEnabled") != value) { settings.setValue("fCoinJoinEnabled", value.toBool()); diff --git a/src/qt/optionsmodel.h b/src/qt/optionsmodel.h index 541d6385f8..12e5f33982 100644 --- a/src/qt/optionsmodel.h +++ b/src/qt/optionsmodel.h @@ -34,41 +34,42 @@ public: explicit OptionsModel(interfaces::Node& node, QObject *parent = nullptr, bool resetSettings = false); enum OptionID { - StartAtStartup, // bool - HideTrayIcon, // bool - MinimizeToTray, // bool - MapPortUPnP, // bool - MinimizeOnClose, // bool - ProxyUse, // bool - ProxyIP, // QString - ProxyPort, // int - ProxyUseTor, // bool - ProxyIPTor, // QString - ProxyPortTor, // int - DisplayUnit, // BitcoinUnits::Unit - ThirdPartyTxUrls, // QString - Digits, // QString - Theme, // QString - FontFamily, // int - FontScale, // int - FontWeightNormal, // int - FontWeightBold, // int - Language, // QString - CoinControlFeatures, // bool - ThreadsScriptVerif, // int - Prune, // bool - PruneSize, // int - DatabaseCache, // int - SpendZeroConfChange, // bool - ShowMasternodesTab, // bool - CoinJoinEnabled, // bool - ShowAdvancedCJUI, // bool - ShowCoinJoinPopups, // bool - LowKeysWarning, // bool - CoinJoinRounds, // int - CoinJoinAmount, // int - CoinJoinMultiSession,// bool - Listen, // bool + StartAtStartup, // bool + HideTrayIcon, // bool + MinimizeToTray, // bool + MapPortUPnP, // bool + MinimizeOnClose, // bool + ProxyUse, // bool + ProxyIP, // QString + ProxyPort, // int + ProxyUseTor, // bool + ProxyIPTor, // QString + ProxyPortTor, // int + DisplayUnit, // BitcoinUnits::Unit + ThirdPartyTxUrls, // QString + Digits, // QString + Theme, // QString + FontFamily, // int + FontScale, // int + FontWeightNormal, // int + FontWeightBold, // int + Language, // QString + CoinControlFeatures, // bool + ThreadsScriptVerif, // int + Prune, // bool + PruneSize, // int + DatabaseCache, // int + SpendZeroConfChange, // bool + ShowMasternodesTab, // bool + ShowGovernanceTab, // bool + CoinJoinEnabled, // bool + ShowAdvancedCJUI, // bool + ShowCoinJoinPopups, // bool + LowKeysWarning, // bool + CoinJoinRounds, // int + CoinJoinAmount, // int + CoinJoinMultiSession, // bool + Listen, // bool OptionIDRowCount, }; diff --git a/src/qt/res/css/dark.css b/src/qt/res/css/dark.css index b1b1f2289b..d1be65ea46 100644 --- a/src/qt/res/css/dark.css +++ b/src/qt/res/css/dark.css @@ -818,6 +818,14 @@ EditAddressDialog /***** No dark.css specific coloring here yet *****/ +/****************************************************** +GovernanceList +******************************************************/ + +GovernanceList QTableView { + color: #c7c7c7; +} + /****************************************************** HelpMessageDialog ******************************************************/ diff --git a/src/qt/res/css/light.css b/src/qt/res/css/light.css index ffd235e5a0..32cba48760 100644 --- a/src/qt/res/css/light.css +++ b/src/qt/res/css/light.css @@ -803,6 +803,14 @@ EditAddressDialog /***** No light.css specific coloring here yet *****/ +/****************************************************** +GovernanceList +******************************************************/ + +GovernanceList QTableView { + color: #555; +} + /****************************************************** HelpMessageDialog ******************************************************/ diff --git a/src/qt/walletframe.cpp b/src/qt/walletframe.cpp index f48d7881e8..cac55382df 100644 --- a/src/qt/walletframe.cpp +++ b/src/qt/walletframe.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -32,6 +33,9 @@ WalletFrame::WalletFrame(BitcoinGUI* _gui) : masternodeListPage = new MasternodeList(); walletStack->addWidget(masternodeListPage); + + governanceListPage = new GovernanceList(); + walletStack->addWidget(governanceListPage); } WalletFrame::~WalletFrame() @@ -43,6 +47,7 @@ void WalletFrame::setClientModel(ClientModel *_clientModel) this->clientModel = _clientModel; masternodeListPage->setClientModel(_clientModel); + governanceListPage->setClientModel(_clientModel); for (auto i = mapWalletViews.constBegin(); i != mapWalletViews.constEnd(); ++i) { i.value()->setClientModel(_clientModel); @@ -120,6 +125,21 @@ void WalletFrame::showOutOfSyncWarning(bool fShow) i.value()->showOutOfSyncWarning(fShow); } +void WalletFrame::gotoGovernancePage() +{ + QMap::const_iterator i; + + if (mapWalletViews.empty()) { + walletStack->setCurrentWidget(governanceListPage); + return; + } + + for (i = mapWalletViews.constBegin(); i != mapWalletViews.constEnd(); ++i) { + i.value()->gotoGovernancePage(); + } +} + + void WalletFrame::gotoOverviewPage() { QMap::const_iterator i; diff --git a/src/qt/walletframe.h b/src/qt/walletframe.h index d9e5dbdf46..c0d1e38252 100644 --- a/src/qt/walletframe.h +++ b/src/qt/walletframe.h @@ -14,6 +14,7 @@ class SendCoinsRecipient; class WalletModel; class WalletView; class MasternodeList; +class GovernanceList; QT_BEGIN_NAMESPACE class QStackedWidget; @@ -55,6 +56,7 @@ private: ClientModel *clientModel; QMap mapWalletViews; MasternodeList* masternodeListPage; + GovernanceList* governanceListPage; bool bOutOfSync; @@ -63,6 +65,8 @@ public: WalletModel* currentWalletModel() const; public Q_SLOTS: + /** Switch to governance page */ + void gotoGovernancePage(); /** Switch to overview (home) page */ void gotoOverviewPage(); /** Switch to history (transactions) page */ diff --git a/src/qt/walletview.cpp b/src/qt/walletview.cpp index 83d81405f1..2ae9074689 100644 --- a/src/qt/walletview.cpp +++ b/src/qt/walletview.cpp @@ -89,6 +89,10 @@ WalletView::WalletView(QWidget* parent) : masternodeListPage = new MasternodeList(); addWidget(masternodeListPage); } + if (settings.value("fShowGovernanceTab").toBool()) { + governanceListPage = new GovernanceList(); + addWidget(governanceListPage); + } // Clicking on a transaction on the overview pre-selects the transaction on the transaction history page connect(overviewPage, &OverviewPage::transactionClicked, transactionView, static_cast(&TransactionView::focusTransaction)); @@ -156,6 +160,9 @@ void WalletView::setClientModel(ClientModel *_clientModel) if (settings.value("fShowMasternodesTab").toBool()) { masternodeListPage->setClientModel(_clientModel); } + if (settings.value("fShowGovernanceTab").toBool()) { + governanceListPage->setClientModel(_clientModel); + } } void WalletView::setWalletModel(WalletModel *_walletModel) @@ -227,6 +234,14 @@ void WalletView::processNewTransaction(const QModelIndex& parent, int start, int Q_EMIT incomingTransaction(date, walletModel->getOptionsModel()->getDisplayUnit(), amount, type, address, label, walletModel->getWalletName()); } +void WalletView::gotoGovernancePage() +{ + QSettings settings; + if (settings.value("fShowGovernanceTab").toBool()) { + setCurrentWidget(governanceListPage); + } +} + void WalletView::gotoOverviewPage() { setCurrentWidget(overviewPage); diff --git a/src/qt/walletview.h b/src/qt/walletview.h index 30f138d907..544d56b0ae 100644 --- a/src/qt/walletview.h +++ b/src/qt/walletview.h @@ -6,6 +6,7 @@ #define BITCOIN_QT_WALLETVIEW_H #include +#include #include #include @@ -68,6 +69,7 @@ private: AddressBookPage *usedSendingAddressesPage; AddressBookPage *usedReceivingAddressesPage; MasternodeList *masternodeListPage; + GovernanceList* governanceListPage; TransactionView *transactionView; @@ -75,6 +77,8 @@ private: QLabel *transactionSum; public Q_SLOTS: + /** Switch to governance page */ + void gotoGovernancePage(); /** Switch to overview (home) page */ void gotoOverviewPage(); /** Switch to history (transactions) page */