PIVX Core  5.6.99
P2P Digital Currency
governancewidget.cpp
Go to the documentation of this file.
1 // Copyright (c) 2021-2022 The PIVX Core developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4 
5 #include "governancewidget.h"
6 #include "ui_governancewidget.h"
7 
8 #include "createproposaldialog.h"
9 #include "governancemodel.h"
10 #include "mnmodel.h"
11 #include "proposalinfodialog.h"
12 #include "qtutils.h"
13 #include "votedialog.h"
14 
15 #include <QDesktopServices>
16 #include <QGraphicsDropShadowEffect>
17 #include <QScrollBar>
18 #include <QTimer>
19 
20 void initComboView(PWidget* parent, QComboBox* comboBox, const QString& filterHint, const QList<QString>& values)
21 {
22  auto* modelFilter = new QStandardItemModel(parent);
23  Delegate* delegateFilter = new Delegate(parent);
24  for (int i = 0; i < values.size(); ++i) {
25  auto item = new QStandardItem(QString(filterHint+": %1").arg(values.value(i)));
26  item->setData(i);
27  modelFilter->appendRow(item);
28  }
29  delegateFilter->setValues(values);
30  comboBox->setModel(modelFilter);
31  comboBox->setItemDelegate(delegateFilter);
32  comboBox->setCurrentIndex(0);
33 }
34 
36  PWidget(parent),
37  ui(new Ui::governancewidget)
38 {
39  ui->setupUi(this);
40  this->setStyleSheet(parent->styleSheet());
41 
42  setCssProperty(ui->left, "container");
43  ui->left->setContentsMargins(0,20,0,0);
44  setCssProperty(ui->right, "container-right");
45  ui->right->setContentsMargins(20,10,20,20);
46  setCssProperty(ui->scrollArea, "container");
47 
48  /* Title */
49  ui->labelTitle->setText(tr("Governance"));
50  setCssProperty(ui->labelTitle, "text-title-screen");
51  ui->labelSubtitle1->setText(tr("View, follow, vote and submit network budget proposals.\nBe part of the DAO."));
52  setCssProperty(ui->labelSubtitle1, "text-subtitle");
53  setCssProperty(ui->pushImgEmpty, "img-empty-governance");
54  setCssProperty(ui->labelEmpty, "text-empty");
55 
56  // Font
57  QFont font;
58  font.setPointSize(14);
59 
60  // Combo box sort
61  SortEdit* lineEdit = new SortEdit(ui->comboBoxSort);
62  lineEdit->setFont(font);
63  lineEdit->setAlignment(Qt::AlignRight);
64  initComboBox(ui->comboBoxSort, lineEdit, "btn-combo", false);
65  QList<QString> values{tr("Date"), tr("Amount"), tr("Name")};
66  initComboView(this, ui->comboBoxSort, tr("Sort by"), values);
67  ui->comboBoxSort->setVisible(false); // Future: add sort actions
68 
69  // Filter
70  SortEdit* lineEditFilter = new SortEdit(ui->comboBoxFilter);
71  lineEditFilter->setFont(font);
72  initComboBox(ui->comboBoxFilter, lineEditFilter, "btn-filter", false);
73  QList<QString> valuesFilter{tr("All"), tr("Passing"), tr("Not Passing"), tr("Waiting")};
74  initComboView(this, ui->comboBoxFilter, tr("Filter"), valuesFilter);
75  connect(ui->comboBoxFilter, static_cast<void (QComboBox::*)(const QString&)>(&QComboBox::currentTextChanged),
77 
78  // Budget
79  setCssProperty(ui->labelBudget, "btn-title-grey");
80  setCssProperty(ui->labelBudgetSubTitle, "text-subtitle");
81  setCssProperty(ui->labelAvailableTitle, "label-budget-text");
82  setCssProperty(ui->labelAllocatedTitle, "label-budget-text");
83  setCssProperty(ui->labelAvailableAmount, "label-budget-amount");
84  setCssProperty(ui->labelAllocatedAmount, "label-budget-amount-allocated");
85  setCssProperty(ui->iconClock , "ic-time");
86  setCssProperty(ui->labelNextSuperblock, "label-budget-text");
87 
88  // Sync Warning
89  ui->layoutWarning->setVisible(true);
90  ui->lblWarning->setText(tr("Please wait until the node is fully synced to see the correct information"));
91  setCssProperty(ui->lblWarning, "text-warning");
92  setCssProperty(ui->imgWarning, "ic-warning");
93 
94  // Create proposal
95  ui->btnCreateProposal->setTitleClassAndText("btn-title-grey", "Create Proposal");
96  ui->btnCreateProposal->setSubTitleClassAndText("text-subtitle", tr("Prepare and submit a new proposal."));
97  connect(ui->btnCreateProposal, &OptionButton::clicked, this, &GovernanceWidget::onCreatePropClicked);
98  ui->emptyContainer->setVisible(false);
99 }
100 
102 {
103  delete ui;
104 }
105 
106 void GovernanceWidget::onFilterChanged(const QString& value)
107 {
108  int filterByType = ui->comboBoxFilter->currentIndex();
109  switch (filterByType) {
110  case 1:
111  statusFilter = ProposalInfo::Status::PASSING;
112  break;
113  case 2:
114  statusFilter = ProposalInfo::Status::NOT_PASSING;
115  break;
116  case 3:
117  statusFilter = ProposalInfo::Status::WAITING_FOR_APPROVAL;
118  break;
119  default:
120  statusFilter = nullopt;
121  break;
122  }
123  refreshCardsGrid(true);
124 }
125 
127 {
128  if (!governanceModel->isTierTwoSync()) {
129  inform(tr("Please wait until the node is fully synced"));
130  return;
131  }
132 
133  if (proposalInfo.status == ProposalInfo::Status::WAITING_FOR_APPROVAL) {
134  inform(tr("Cannot vote for the proposal yet, wait until it's confirmed by the network"));
135  return;
136  }
137  window->showHide(true);
139  dialog->setProposal(proposalInfo);
140  if (openDialogWithOpaqueBackgroundY(dialog, window, 4.5, 5)) {
141  // future: make this refresh atomic, no need to refresh the entire grid.
142  tryGridRefresh(true);
143  inform(tr("Vote emitted successfully!"));
144  }
145  dialog->deleteLater();
146 }
147 
149 {
150  if (!walletModel || !governanceModel || !clientModel) return;
151 
152  if (!governanceModel->isTierTwoSync()) {
153  inform(tr("Please wait until the node is fully synced"));
154  return;
155  }
156 
157  // Do not allow proposals submission 1440 blocks away (1 day) from the next superblock
158  // The budget finalization could have been submitted and the user would never know it, losing the first superblock.
159  // future: customizable future superblock height selection (for now, we are automatically using the next superblock).
160  const int chainHeight = clientModel->getLastBlockProcessedHeight();
161  const int nextSuperblock = governanceModel->getNextSuperblockHeight();
162  const int acceptedRange = (walletModel->isTestNetwork() || walletModel->isRegTestNetwork()) ? 10 : 1440;
163  if (nextSuperblock - acceptedRange < chainHeight) {
164  inform(tr("Cannot create proposal, superblock is too close. Need to wait %1 blocks").arg(nextSuperblock - chainHeight));
165  return;
166  }
167 
168  auto ptrUnlockedContext = std::make_unique<WalletModel::UnlockContext>(walletModel->requestUnlock());
169  if (!ptrUnlockedContext->isValid()) {
170  inform(tr("Cannot create proposal, wallet locked"));
171  return;
172  }
173 
174  auto balance = walletModel->GetWalletBalances();
175  if (balance.balance <= governanceModel->getProposalFeeAmount()) {
176  inform(tr("Cannot create proposal, need to have at least %1 to pay for the proposal fee").arg(
178  return;
179  }
180 
181  window->showHide(true);
183  if (openDialogWithOpaqueBackgroundY(dialog, window, 4.5, ui->left->height() < 700 ? 12 : 5)) {
184  // future: make this refresh atomic, no need to refresh the entire grid.
185  tryGridRefresh(true);
186  inform(tr("Proposal transaction fee broadcasted!"));
187  }
188  dialog->deleteLater();
189 }
190 
192 {
193  if (!propMenu) {
194  propMenu = new TooltipMenu(window, this);
195  propMenu->setCopyBtnText(tr("Copy Url"));
196  propMenu->setEditBtnText(tr("Open Url"));
197  propMenu->setDeleteBtnText(tr("More Info"));
198  propMenu->setMaximumWidth(propMenu->maximumWidth() + 5);
199  propMenu->setFixedWidth(propMenu->width() + 5);
204  } else {
205  propMenu->hide();
206  }
207  menuCard = card;
208  QRect rect = card->geometry();
209  QPoint pos = rect.topRight();
210  pos.setX(pos.x() - 22);
211  pos.setY(pos.y() + (isSync ? 100 : 140) - ui->scrollArea->verticalScrollBar()->value());
212  propMenu->move(pos);
213  propMenu->show();
214 }
215 
217 {
218  if (!menuCard) return;
219  GUIUtil::setClipboard(QString::fromStdString(menuCard->getProposal().url));
220  inform(tr("Proposal URL copied to clipboard"));
221 }
222 
224 {
225  if (!menuCard) return;
226  if (ask(tr("Open Proposal URL"),
227  tr("The following URL will be opened in the default browser") + "\n\n" +
228  QString::fromStdString(menuCard->getProposal().url) + "\n\n" +
229  tr("Are you sure?\n(Always verify the URL validity before opening it)\n"))) {
230  if (!QDesktopServices::openUrl(QUrl(QString::fromStdString(menuCard->getProposal().url)))) {
231  inform(tr("Failed to open proposal URL"));
232  }
233  }
234 }
235 
237 {
238  window->showHide(true);
240  dialog->setProposal(menuCard->getProposal());
241  openDialogWithOpaqueBackgroundY(dialog, window, 4.5, ui->left->height() < 700 ? 12 : 5);
242  dialog->deleteLater();
243 }
244 
246 {
248 }
249 
251 {
252  if (!isVisible() || clientModel->inInitialBlockDownload()) return;
253  int remainingBlocks = governanceModel->getNextSuperblockHeight() - height;
254  int remainingDays = remainingBlocks / 1440;
255  QString text = remainingDays == 0 ? tr("Next superblock today!\n%2 blocks to go.").arg(remainingBlocks) :
256  tr("Next superblock in %1 days.\n%2 blocks to go.").arg(remainingDays).arg(remainingBlocks);
257  ui->labelNextSuperblock->setText(text);
258 }
259 
261 {
262  governanceModel = _model;
263 }
264 
266 {
267  mnModel = _mnModel;
268 }
269 
271 {
273 }
274 
275 void GovernanceWidget::showEvent(QShowEvent *event)
276 {
278  tryGridRefresh(true); // future: move to background worker
279  if (!refreshTimer) refreshTimer = new QTimer(this);
280  if (!refreshTimer->isActive()) {
281  connect(refreshTimer, &QTimer::timeout, [this]() { tryGridRefresh(true); });
282  refreshTimer->start(1000 * 60 * 3.5); // Try to refresh screen 3.5 minutes
283  }
284 }
285 
286 void GovernanceWidget::hideEvent(QHideEvent *event)
287 {
288  refreshTimer->stop();
290 }
291 
292 void GovernanceWidget::wheelEvent(QWheelEvent* event)
293 {
294  if (propMenu && propMenu->isVisible()) {
295  propMenu->hide();
296  }
297 }
298 
299 void GovernanceWidget::resizeEvent(QResizeEvent *event)
300 {
301  if (!isVisible()) return;
302  tryGridRefresh();
303 }
304 
306 {
307  int _propsPerRow = calculateColumnsPerRow();
308  if (_propsPerRow != propsPerRow || force) {
309  propsPerRow = _propsPerRow;
310  refreshCardsGrid(true);
311 
312  // refresh budget distribution values
314  ui->labelAllocatedAmount->setText(GUIUtil::formatBalance(governanceModel->getBudgetAllocatedAmount()));
315  ui->labelAvailableAmount->setText(GUIUtil::formatBalance(governanceModel->getBudgetAvailableAmount()));
316  }
317 }
318 
319 static void setCardShadow(QWidget* edit)
320 {
321  QGraphicsDropShadowEffect* shadowEffect = new QGraphicsDropShadowEffect();
322  shadowEffect->setColor(QColor(77, 77, 77, 30));
323  shadowEffect->setXOffset(0);
324  shadowEffect->setYOffset(4);
325  shadowEffect->setBlurRadius(6);
326  edit->setGraphicsEffect(shadowEffect);
327 }
328 
330 {
331  ProposalCard* propCard = new ProposalCard(ui->scrollAreaWidgetContents);
333  connect(propCard, &ProposalCard::inform, this, &GovernanceWidget::inform);
335  setCardShadow(propCard);
336  return propCard;
337 }
338 
340 {
341  if (ui->emptyContainer->isVisible() != show) {
342  ui->emptyContainer->setVisible(show);
343  ui->mainContainer->setVisible(!show);
344  }
345 }
346 
347 void GovernanceWidget::refreshCardsGrid(bool forceRefresh)
348 {
349  if (!governanceModel) return;
350  if (!governanceModel->hasProposals()) {
351  showEmptyScreen(true);
352  return;
353  }
354 
355  showEmptyScreen(false);
356  if (!gridLayout) {
357  gridLayout = new QGridLayout();
358  gridLayout->setAlignment(Qt::AlignTop);
359  gridLayout->setHorizontalSpacing(16);
360  gridLayout->setVerticalSpacing(16);
361  ui->scrollArea->setWidgetResizable(true);
362  ui->scrollAreaWidgetContents->setLayout(gridLayout);
363  }
364 
365  // Refresh grid only if needed
366  if (!(forceRefresh || governanceModel->isRefreshNeeded())) return;
367 
368  std::list<ProposalInfo> props = governanceModel->getProposals(statusFilter.get_ptr());
369 
370  // Start marking all the cards
371  for (ProposalCard* card : cards) {
372  card->setNeedsUpdate(true);
373  }
374 
375  // Refresh the card if exists or create a new one.
376  int column = 0;
377  int row = 0;
378  for (const auto& prop : props) {
379  QLayoutItem* item = gridLayout->itemAtPosition(row, column);
380  ProposalCard* card{nullptr};
381  if (item) {
382  card = dynamic_cast<ProposalCard*>(item->widget());
383  card->setNeedsUpdate(false);
384  } else {
385  card = newCard();
386  cards.emplace_back(card);
387  gridLayout->addWidget(card, row, column, 1, 1);
388  }
389  card->setProposal(prop);
390  column++;
391  if (column == propsPerRow) {
392  column = 0;
393  row++;
394  }
395  }
396 
397  // Now delete the not longer needed cards
398  auto it = cards.begin();
399  while (it != cards.end()) {
400  ProposalCard* card = (*it);
401  if (!card->isUpdateNeeded()) {
402  it++;
403  continue;
404  }
405  gridLayout->takeAt(gridLayout->indexOf(card));
406  it = cards.erase(it);
407  if (card == menuCard) menuCard = nullptr;
408  delete card;
409  }
410 }
411 
413 {
414  int widgetWidth = ui->left->width();
415  if (widgetWidth < 785) {
416  return 2;
417  } else if (widgetWidth < 1100){
418  return 3;
419  } else {
420  return 4; // max amount of cards
421  }
422 }
423 
425 {
426  if (isSync != sync) {
427  isSync = sync;
428  ui->layoutWarning->setVisible(!isSync);
429  if (!isVisible()) return;
430  tryGridRefresh();
431  }
432 }
void startMasternodesTimer()
void numBlocksChanged(int count)
void stopMasternodesTimer()
int getNumBlocks()
int getLastBlockProcessedHeight() const
bool inInitialBlockDownload() const
Return true if core is doing initial block download.
void setValues(QList< QString > _values)
int getNextSuperblockHeight() const
CAmount getBudgetAvailableAmount() const
CAmount getProposalFeeAmount() const
CAmount getBudgetAllocatedAmount() const
void setWalletModel(WalletModel *_walletModel)
std::list< ProposalInfo > getProposals(const ProposalInfo::Status *filterByStatus=nullptr, bool filterFinished=true)
GovernanceWidget(PIVXGUI *parent)
void refreshCardsGrid(bool forceRefresh)
void showEvent(QShowEvent *event) override
void tryGridRefresh(bool force=false)
void onMenuClicked(ProposalCard *card)
std::vector< ProposalCard * > cards
void resizeEvent(QResizeEvent *event) override
void showEmptyScreen(bool show)
GovernanceModel * governanceModel
void setGovModel(GovernanceModel *_model)
void onVoteForPropClicked(const ProposalInfo &proposalInfo)
void loadWalletModel() override
void chainHeightChanged(int height)
ProposalCard * menuCard
ProposalCard * newCard()
Optional< ProposalInfo::Status > statusFilter
Ui::governancewidget * ui
TooltipMenu * propMenu
void hideEvent(QHideEvent *event) override
void wheelEvent(QWheelEvent *event) override
void setMNModel(MNModel *_mnModel)
~GovernanceWidget() override
void tierTwoSynced(bool IsSync)
void onFilterChanged(const QString &value)
void loadClientModel() override
QGridLayout * gridLayout
void clicked()
PIVX GUI main class.
Definition: pivxgui.h:46
void showHide(bool show)
Definition: pivxgui.cpp:579
PIVXGUI * window
Definition: pwidget.h:59
void inform(const QString &message)
Definition: pwidget.cpp:45
WalletModel * walletModel
Definition: pwidget.h:61
ClientModel * clientModel
Definition: pwidget.h:60
void message(const QString &title, const QString &body, unsigned int style, bool *ret=nullptr)
bool ask(const QString &title, const QString &message)
Definition: pwidget.cpp:55
void setNeedsUpdate(bool _update)
Definition: proposalcard.h:29
void inform(const QString &text)
void voteClicked(const ProposalInfo &proposalInfo)
ProposalInfo getProposal()
Definition: proposalcard.h:26
void onMenuClicked(ProposalCard *card)
bool isUpdateNeeded() const
Definition: proposalcard.h:30
void setProposal(const ProposalInfo &info)
void setDeleteBtnText(const QString &btnText)
Definition: tooltipmenu.cpp:30
void setEditBtnText(const QString &btnText)
Definition: tooltipmenu.cpp:26
void onDeleteClicked()
void setCopyBtnText(const QString &btnText)
Definition: tooltipmenu.cpp:34
void onEditClicked()
void onCopyClicked()
void setProposal(const ProposalInfo &prop)
Definition: votedialog.cpp:55
CAmount getNetMinFee()
bool isTestNetwork() const
Definition: walletmodel.cpp:76
interfaces::WalletBalances GetWalletBalances()
Definition: walletmodel.h:165
UnlockContext requestUnlock()
bool isRegTestNetwork() const
Definition: walletmodel.cpp:81
void initComboView(PWidget *parent, QComboBox *comboBox, const QString &filterHint, const QList< QString > &values)
QString formatBalance(CAmount amount, int nDisplayUnit, bool isZpiv)
Definition: guiutil.cpp:119
void setClipboard(const QString &str)
Definition: guiutil.cpp:670
void initComboBox(QComboBox *combo, QLineEdit *lineEdit, QString cssClass, bool setView)
Definition: qtutils.cpp:244
bool openDialogWithOpaqueBackgroundY(QDialog *widget, PIVXGUI *gui, double posX, int posY, bool hideOpaqueBackground)
Definition: qtutils.cpp:59
void setCssProperty(std::initializer_list< QWidget * > args, const QString &value)
Definition: qtutils.cpp:334
std::string url
Status status
Proposal state.