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 <UdjinM6@users.noreply.github.com>
This commit is contained in:
Stefan 2021-10-30 01:14:25 +00:00 committed by GitHub
parent 5781bd5ee3
commit f0d49e4f69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 804 additions and 38 deletions

View File

@ -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 \

View File

@ -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 \

View File

@ -10,6 +10,7 @@
#include <chain.h>
#include <chainparams.h>
#include <evo/deterministicmns.h>
#include <governance/governance.h>
#include <governance/object.h>
#include <init.h>
#include <interfaces/chain.h>
@ -66,6 +67,15 @@ public:
}
};
class GOVImpl : public GOV
{
public:
std::vector<const CGovernanceObject*> 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; }

View File

@ -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<const CGovernanceObject*> 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;

View File

@ -33,9 +33,10 @@
#include <chainparams.h>
#include <interfaces/handler.h>
#include <interfaces/node.h>
#include <qt/governancelist.h>
#include <qt/masternodelist.h>
#include <ui_interface.h>
#include <util/system.h>
#include <qt/masternodelist.h>
#include <iostream>
#include <memory>
@ -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);

View File

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

View File

@ -15,14 +15,15 @@
#include <chain.h>
#include <chainparams.h>
#include <clientversion.h>
#include <governance/object.h>
#include <interfaces/handler.h>
#include <interfaces/node.h>
#include <validation.h>
#include <net.h>
#include <netbase.h>
#include <txmempool.h>
#include <ui_interface.h>
#include <util/system.h>
#include <validation.h>
#include <warnings.h>
#include <stdint.h>
@ -122,6 +123,11 @@ int64_t ClientModel::getHeaderTipTime() const
return cachedBestHeaderTime;
}
std::vector<const CGovernanceObject*> ClientModel::getAllGovernanceObjects()
{
return m_node.gov().getAllNewerThan(0);
}
void ClientModel::updateTimer()
{
// no locking required at this point

View File

@ -40,6 +40,7 @@ enum NumConnections {
};
class CDeterministicMNList;
class CGovernanceObject;
typedef std::shared_ptr<CDeterministicMNList> CDeterministicMNListPtr;
/** Model for Dash network client. */
@ -67,6 +68,8 @@ public:
CDeterministicMNList getMasternodeList() const;
void refreshMasternodeList();
std::vector<const CGovernanceObject*> getAllGovernanceObjects();
//! Returns enum BlockSource of the current importing/syncing state
enum BlockSource getBlockSource() const;
//! Return warnings to be displayed in status bar

View File

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GovernanceList</class>
<widget class="QWidget" name="GovernanceList">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>762</width>
<height>457</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="topLayout">
<property name="leftMargin">
<number>20</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>20</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer0">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_filter_2">
<property name="text">
<string>Filter List:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="filterLineEdit">
<property name="toolTip">
<string>Filter propsal list</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_count_2">
<property name="text">
<string>Proposal Count:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="countLabel">
<property name="text">
<string>0</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QTableView" name="govTableView">
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -328,6 +328,16 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="showGovernanceTab">
<property name="toolTip">
<string>Show additional tab listing governance proposals.</string>
</property>
<property name="text">
<string>Show Governance Tab</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="spendZeroConfChange">
<property name="toolTip">

402
src/qt/governancelist.cpp Normal file
View File

@ -0,0 +1,402 @@
#include <qt/forms/ui_governancelist.h>
#include <qt/governancelist.h>
#include <chainparams.h>
#include <clientversion.h>
#include <coins.h>
#include <evo/deterministicmns.h>
#include <netbase.h>
#include <qt/clientmodel.h>
#include <qt/guiutil.h>
#include <univalue.h>
#include <QAbstractItemView>
#include <QDesktopServices>
#include <QMessageBox>
#include <QTableWidgetItem>
#include <QUrl>
#include <QtGui/QClipboard>
///
/// 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<const Proposal*>& 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<bool> 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<const CGovernanceObject*> govObjList = clientModel->getAllGovernanceObjects();
std::vector<const Proposal*> 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);
}

116
src/qt/governancelist.h Normal file
View File

@ -0,0 +1,116 @@
#ifndef BITCOIN_QT_GOVERNANCELIST_H
#define BITCOIN_QT_GOVERNANCELIST_H
#include <governance/object.h>
#include <primitives/transaction.h>
#include <sync.h>
#include <util/system.h>
#include <QAbstractTableModel>
#include <QDateTime>
#include <QMenu>
#include <QSortFilterProxyModel>
#include <QTimer>
#include <QWidget>
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<const Proposal*> 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<const Proposal*>& 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

View File

@ -247,7 +247,7 @@ void OptionsDialog::setModel(OptionsModel *_model)
connect(ui->threadsScriptVerif, static_cast<void (QSpinBox::*)(int)>(&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);

View File

@ -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());

View File

@ -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,
};

View File

@ -818,6 +818,14 @@ EditAddressDialog
/***** No dark.css specific coloring here yet *****/
/******************************************************
GovernanceList
******************************************************/
GovernanceList QTableView {
color: #c7c7c7;
}
/******************************************************
HelpMessageDialog
******************************************************/

View File

@ -803,6 +803,14 @@ EditAddressDialog
/***** No light.css specific coloring here yet *****/
/******************************************************
GovernanceList
******************************************************/
GovernanceList QTableView {
color: #555;
}
/******************************************************
HelpMessageDialog
******************************************************/

View File

@ -6,6 +6,7 @@
#include <qt/walletmodel.h>
#include <qt/bitcoingui.h>
#include <qt/governancelist.h>
#include <qt/masternodelist.h>
#include <qt/walletview.h>
@ -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<WalletModel*, WalletView*>::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<WalletModel*, WalletView*>::const_iterator i;

View File

@ -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<WalletModel*, WalletView*> 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 */

View File

@ -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<void (TransactionView::*)(const QModelIndex&)>(&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);

View File

@ -6,6 +6,7 @@
#define BITCOIN_QT_WALLETVIEW_H
#include <amount.h>
#include <qt/governancelist.h>
#include <qt/masternodelist.h>
#include <QStackedWidget>
@ -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 */