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 */