overviewpage.cpp raw

   1  // Copyright (c) 2011-2022 The Limenka 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 <qt/overviewpage.h>
   6  #include <qt/forms/ui_overviewpage.h>
   7  
   8  #include <qt/limenkaunits.h>
   9  #include <qt/clientmodel.h>
  10  #include <qt/guiconstants.h>
  11  #include <qt/guiutil.h>
  12  #include <qt/optionsmodel.h>
  13  #include <qt/platformstyle.h>
  14  #include <qt/transactionfilterproxy.h>
  15  #include <qt/transactionoverviewwidget.h>
  16  #include <qt/transactiontablemodel.h>
  17  #include <qt/walletmodel.h>
  18  
  19  #include <QAbstractItemDelegate>
  20  #include <QApplication>
  21  #include <QDateTime>
  22  #include <QPainter>
  23  #include <QStatusTipEvent>
  24  
  25  #include <algorithm>
  26  #include <map>
  27  
  28  #define DECORATION_SIZE 54
  29  #define NUM_ITEMS 5
  30  
  31  Q_DECLARE_METATYPE(interfaces::WalletBalances)
  32  
  33  class TxViewDelegate : public QAbstractItemDelegate
  34  {
  35      Q_OBJECT
  36  public:
  37      explicit TxViewDelegate(const PlatformStyle* _platformStyle, QObject* parent = nullptr)
  38          : QAbstractItemDelegate(parent), platformStyle(_platformStyle)
  39      {
  40          connect(this, &TxViewDelegate::width_changed, this, &TxViewDelegate::sizeHintChanged);
  41      }
  42  
  43      inline void paint(QPainter *painter, const QStyleOptionViewItem &option,
  44                        const QModelIndex &index ) const override
  45      {
  46          painter->save();
  47  
  48          QIcon icon = qvariant_cast<QIcon>(index.data(TransactionTableModel::RawDecorationRole));
  49          QRect mainRect = option.rect;
  50          QRect decorationRect(mainRect.topLeft(), QSize(DECORATION_SIZE, DECORATION_SIZE));
  51          int xspace = DECORATION_SIZE + 8;
  52          int ypad = 6;
  53          int halfheight = (mainRect.height() - 2*ypad)/2;
  54          QRect amountRect(mainRect.left() + xspace, mainRect.top()+ypad, mainRect.width() - xspace, halfheight);
  55          QRect addressRect(mainRect.left() + xspace, mainRect.top()+ypad+halfheight, mainRect.width() - xspace, halfheight);
  56          icon = platformStyle->SingleColorIcon(icon);
  57          icon.paint(painter, decorationRect);
  58  
  59          QDateTime date = index.data(TransactionTableModel::DateRole).toDateTime();
  60          QString address = index.data(Qt::DisplayRole).toString();
  61          qint64 amount = index.data(TransactionTableModel::AmountRole).toLongLong();
  62          bool confirmed = index.data(TransactionTableModel::ConfirmedRole).toBool();
  63          QVariant value = index.data(Qt::ForegroundRole);
  64          QColor foreground = option.palette.color(QPalette::Text);
  65          if(value.canConvert<QBrush>())
  66          {
  67              QBrush brush = qvariant_cast<QBrush>(value);
  68              foreground = brush.color();
  69          }
  70  
  71          if (index.data(TransactionTableModel::WatchonlyRole).toBool()) {
  72              QIcon iconWatchonly = qvariant_cast<QIcon>(index.data(TransactionTableModel::WatchonlyDecorationRole));
  73              QRect watchonlyRect(addressRect.left(), addressRect.top(), 16, addressRect.height());
  74              iconWatchonly = platformStyle->TextColorIcon(iconWatchonly);
  75              iconWatchonly.paint(painter, watchonlyRect);
  76              addressRect.setLeft(addressRect.left() + watchonlyRect.width() + 5);
  77          }
  78  
  79          painter->setPen(foreground);
  80          QRect boundingRect;
  81          painter->drawText(addressRect, Qt::AlignLeft | Qt::AlignVCenter, address, &boundingRect);
  82  
  83          if(amount < 0)
  84          {
  85              foreground = COLOR_NEGATIVE;
  86          }
  87          else if(!confirmed)
  88          {
  89              foreground = COLOR_UNCONFIRMED;
  90          }
  91          else
  92          {
  93              foreground = option.palette.color(QPalette::Text);
  94          }
  95          painter->setPen(foreground);
  96          QFont font_saved = painter->font();
  97          QString amountText = LimenkaUnits::formatWithUnit(unit, amount, true, LimenkaUnits::SeparatorStyle::ALWAYS);
  98          if(!confirmed)
  99          {
 100              amountText = QString("[") + amountText + QString("]");
 101          }
 102  
 103          if (QVariant font_variant = index.siblingAtColumn(TransactionTableModel::Amount).data(Qt::FontRole); font_variant.canConvert<QFont>()) {
 104              painter->setFont(font_variant.value<QFont>());
 105          }
 106          QRect amount_bounding_rect;
 107          painter->drawText(amountRect, Qt::AlignRight | Qt::AlignVCenter, amountText, &amount_bounding_rect);
 108          painter->setFont(font_saved);
 109  
 110          painter->setPen(option.palette.color(QPalette::Text));
 111          QRect date_bounding_rect;
 112          painter->drawText(amountRect, Qt::AlignLeft | Qt::AlignVCenter, GUIUtil::dateTimeStr(date), &date_bounding_rect);
 113  
 114          // 0.4*date_bounding_rect.width() is used to visually distinguish a date from an amount.
 115          const int minimum_width = 1.4 * date_bounding_rect.width() + amount_bounding_rect.width();
 116          const auto search = m_minimum_width.find(index.row());
 117          if (search == m_minimum_width.end() || search->second != minimum_width) {
 118              m_minimum_width[index.row()] = minimum_width;
 119              Q_EMIT width_changed(index);
 120          }
 121  
 122          painter->restore();
 123      }
 124  
 125      inline QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
 126      {
 127          const auto search = m_minimum_width.find(index.row());
 128          const int minimum_text_width = search == m_minimum_width.end() ? 0 : search->second;
 129          return {DECORATION_SIZE + 8 + minimum_text_width, DECORATION_SIZE};
 130      }
 131  
 132      LimenkaUnit unit{LimenkaUnit::BTC};
 133  
 134  Q_SIGNALS:
 135      //! An intermediate signal for emitting from the `paint() const` member function.
 136      void width_changed(const QModelIndex& index) const;
 137  
 138  private:
 139      const PlatformStyle* platformStyle;
 140      mutable std::map<int, int> m_minimum_width;
 141  };
 142  
 143  #include <qt/overviewpage.moc>
 144  
 145  OverviewPage::OverviewPage(const PlatformStyle *platformStyle, QWidget *parent) :
 146      QWidget(parent),
 147      ui(new Ui::OverviewPage),
 148      m_platform_style{platformStyle},
 149      txdelegate(new TxViewDelegate(platformStyle, this))
 150  {
 151      ui->setupUi(this);
 152  
 153      // use a SingleColorIcon for the "out of sync warning" icon
 154      QIcon icon = m_platform_style->SingleColorIcon(QStringLiteral(":/icons/warning"));
 155      ui->labelTransactionsStatus->setIcon(icon);
 156      ui->labelWalletStatus->setIcon(icon);
 157  
 158      // Recent transactions
 159      ui->listTransactions->setItemDelegate(txdelegate);
 160      ui->listTransactions->setIconSize(QSize(DECORATION_SIZE, DECORATION_SIZE));
 161      ui->listTransactions->setMinimumHeight(NUM_ITEMS * (DECORATION_SIZE + 2));
 162      ui->listTransactions->setAttribute(Qt::WA_MacShowFocusRect, false);
 163  
 164      connect(ui->listTransactions, &TransactionOverviewWidget::clicked, this, &OverviewPage::handleTransactionClicked);
 165  
 166      // start with displaying the "out of sync" warnings
 167      showOutOfSyncWarning(true);
 168      connect(ui->labelWalletStatus, &QPushButton::clicked, this, &OverviewPage::outOfSyncWarningClicked);
 169      connect(ui->labelTransactionsStatus, &QPushButton::clicked, this, &OverviewPage::outOfSyncWarningClicked);
 170  }
 171  
 172  void OverviewPage::handleTransactionClicked(const QModelIndex &index)
 173  {
 174      if(filter)
 175          Q_EMIT transactionClicked(filter->mapToSource(index));
 176  }
 177  
 178  void OverviewPage::setPrivacy(bool privacy)
 179  {
 180      m_privacy = privacy;
 181      clientModel->getOptionsModel()->setOption(OptionsModel::OptionID::MaskValues, privacy);
 182      const auto& balances = walletModel->getCachedBalance();
 183      if (balances.balance != -1) {
 184          setBalance(balances);
 185      }
 186  
 187      ui->listTransactions->setVisible(!m_privacy);
 188  
 189      const QString status_tip = m_privacy ? tr("Privacy mode activated for the Overview tab. To unmask the values, uncheck Settings->Mask values.") : "";
 190      setStatusTip(status_tip);
 191      QStatusTipEvent event(status_tip);
 192      QApplication::sendEvent(this, &event);
 193  }
 194  
 195  OverviewPage::~OverviewPage()
 196  {
 197      delete ui;
 198  }
 199  
 200  void OverviewPage::setBalance(const interfaces::WalletBalances& balances)
 201  {
 202      LimenkaUnit unit = walletModel->getOptionsModel()->getDisplayUnit();
 203      if (walletModel->wallet().isLegacy()) {
 204          if (walletModel->wallet().privateKeysDisabled()) {
 205              ui->labelBalance->setText(LimenkaUnits::formatWithPrivacy(unit, balances.watch_only_balance, LimenkaUnits::SeparatorStyle::ALWAYS, m_privacy));
 206              ui->labelUnconfirmed->setText(LimenkaUnits::formatWithPrivacy(unit, balances.unconfirmed_watch_only_balance, LimenkaUnits::SeparatorStyle::ALWAYS, m_privacy));
 207              ui->labelImmature->setText(LimenkaUnits::formatWithPrivacy(unit, balances.immature_watch_only_balance, LimenkaUnits::SeparatorStyle::ALWAYS, m_privacy));
 208              ui->labelTotal->setText(LimenkaUnits::formatWithPrivacy(unit, balances.watch_only_balance + balances.unconfirmed_watch_only_balance + balances.immature_watch_only_balance, LimenkaUnits::SeparatorStyle::ALWAYS, m_privacy));
 209          } else {
 210              ui->labelBalance->setText(LimenkaUnits::formatWithPrivacy(unit, balances.balance, LimenkaUnits::SeparatorStyle::ALWAYS, m_privacy));
 211              ui->labelUnconfirmed->setText(LimenkaUnits::formatWithPrivacy(unit, balances.unconfirmed_balance, LimenkaUnits::SeparatorStyle::ALWAYS, m_privacy));
 212              ui->labelImmature->setText(LimenkaUnits::formatWithPrivacy(unit, balances.immature_balance, LimenkaUnits::SeparatorStyle::ALWAYS, m_privacy));
 213              ui->labelTotal->setText(LimenkaUnits::formatWithPrivacy(unit, balances.balance + balances.unconfirmed_balance + balances.immature_balance, LimenkaUnits::SeparatorStyle::ALWAYS, m_privacy));
 214              ui->labelWatchAvailable->setText(LimenkaUnits::formatWithPrivacy(unit, balances.watch_only_balance, LimenkaUnits::SeparatorStyle::ALWAYS, m_privacy));
 215              ui->labelWatchPending->setText(LimenkaUnits::formatWithPrivacy(unit, balances.unconfirmed_watch_only_balance, LimenkaUnits::SeparatorStyle::ALWAYS, m_privacy));
 216              ui->labelWatchImmature->setText(LimenkaUnits::formatWithPrivacy(unit, balances.immature_watch_only_balance, LimenkaUnits::SeparatorStyle::ALWAYS, m_privacy));
 217              ui->labelWatchTotal->setText(LimenkaUnits::formatWithPrivacy(unit, balances.watch_only_balance + balances.unconfirmed_watch_only_balance + balances.immature_watch_only_balance, LimenkaUnits::SeparatorStyle::ALWAYS, m_privacy));
 218          }
 219      } else {
 220          ui->labelBalance->setText(LimenkaUnits::formatWithPrivacy(unit, balances.balance, LimenkaUnits::SeparatorStyle::ALWAYS, m_privacy));
 221          ui->labelUnconfirmed->setText(LimenkaUnits::formatWithPrivacy(unit, balances.unconfirmed_balance, LimenkaUnits::SeparatorStyle::ALWAYS, m_privacy));
 222          ui->labelImmature->setText(LimenkaUnits::formatWithPrivacy(unit, balances.immature_balance, LimenkaUnits::SeparatorStyle::ALWAYS, m_privacy));
 223          ui->labelTotal->setText(LimenkaUnits::formatWithPrivacy(unit, balances.balance + balances.unconfirmed_balance + balances.immature_balance, LimenkaUnits::SeparatorStyle::ALWAYS, m_privacy));
 224      }
 225      // 128-bit precision: when the wallet holds sub-satoshi fractions or
 226      // confidential outputs, show the full-precision total as a second line
 227      // under the available balance (not in privacy mode, which masks values).
 228      if (!m_privacy) {
 229          const QString precise = walletModel->getPreciseBalance();
 230          if (!precise.isEmpty() && precise != "0") {
 231              const QString existing = ui->labelBalance->text();
 232              ui->labelBalance->setText(existing + "\n" + precise + " \xce\xbb");
 233          }
 234      }
 235  
 236      // only show immature (newly mined) balance if it's non-zero, so as not to complicate things
 237      // for the non-mining users
 238      bool showImmature = balances.immature_balance != 0;
 239      bool showWatchOnlyImmature = balances.immature_watch_only_balance != 0;
 240  
 241      // for symmetry reasons also show immature label when the watch-only one is shown
 242      ui->labelImmature->setVisible(showImmature || showWatchOnlyImmature);
 243      ui->labelImmatureText->setVisible(showImmature || showWatchOnlyImmature);
 244      ui->labelWatchImmature->setVisible(!walletModel->wallet().privateKeysDisabled() && showWatchOnlyImmature); // show watch-only immature balance
 245  }
 246  
 247  // show/hide watch-only labels
 248  void OverviewPage::updateWatchOnlyLabels(bool showWatchOnly)
 249  {
 250      ui->labelSpendable->setVisible(showWatchOnly);      // show spendable label (only when watch-only is active)
 251      ui->labelWatchonly->setVisible(showWatchOnly);      // show watch-only label
 252      ui->lineWatchBalance->setVisible(showWatchOnly);    // show watch-only balance separator line
 253      ui->labelWatchAvailable->setVisible(showWatchOnly); // show watch-only available balance
 254      ui->labelWatchPending->setVisible(showWatchOnly);   // show watch-only pending balance
 255      ui->labelWatchTotal->setVisible(showWatchOnly);     // show watch-only total balance
 256  
 257      if (!showWatchOnly)
 258          ui->labelWatchImmature->hide();
 259  }
 260  
 261  void OverviewPage::setClientModel(ClientModel *model)
 262  {
 263      this->clientModel = model;
 264      if (model) {
 265          // Show warning, for example if this is a prerelease version
 266          connect(model, &ClientModel::alertsChanged, this, &OverviewPage::updateAlerts);
 267          updateAlerts(model->getStatusBarWarnings());
 268  
 269          connect(model->getOptionsModel(), &OptionsModel::fontForMoneyChanged, this, &OverviewPage::setMonospacedFont);
 270          setMonospacedFont(QFont() /* ignored */);
 271      }
 272  }
 273  
 274  void OverviewPage::setWalletModel(WalletModel *model)
 275  {
 276      this->walletModel = model;
 277      if(model && model->getOptionsModel())
 278      {
 279          // Set up transaction list
 280          filter.reset(new TransactionFilterProxy());
 281          filter->setSourceModel(model->getTransactionTableModel());
 282          filter->setDynamicSortFilter(true);
 283          filter->setSortRole(Qt::EditRole);
 284          filter->setShowInactive(false);
 285          filter->sort(TransactionTableModel::Date, Qt::DescendingOrder);
 286  
 287          ui->listTransactions->setModel(filter.get());
 288          ui->listTransactions->setModelColumn(TransactionTableModel::ToAddress);
 289  
 290          connect(filter.get(), &TransactionFilterProxy::rowsInserted, this, &OverviewPage::LimitTransactionRows);
 291          connect(filter.get(), &TransactionFilterProxy::rowsRemoved, this, &OverviewPage::LimitTransactionRows);
 292          connect(filter.get(), &TransactionFilterProxy::rowsMoved, this, &OverviewPage::LimitTransactionRows);
 293          LimitTransactionRows();
 294          // Keep up to date with wallet
 295          setBalance(model->getCachedBalance());
 296          connect(model, &WalletModel::balanceChanged, this, &OverviewPage::setBalance);
 297  
 298          connect(model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &OverviewPage::updateDisplayUnit);
 299  
 300          interfaces::Wallet& wallet = model->wallet();
 301          updateWatchOnlyLabels(wallet.haveWatchOnly() && !wallet.privateKeysDisabled());
 302          connect(model, &WalletModel::notifyWatchonlyChanged, [this](bool showWatchOnly) {
 303              updateWatchOnlyLabels(showWatchOnly && !walletModel->wallet().privateKeysDisabled());
 304          });
 305      }
 306  
 307      // update the display unit, to not use the default ("BTC")
 308      updateDisplayUnit();
 309  }
 310  
 311  void OverviewPage::changeEvent(QEvent* e)
 312  {
 313      if (e->type() == QEvent::PaletteChange) {
 314          QIcon icon = m_platform_style->SingleColorIcon(QStringLiteral(":/icons/warning"));
 315          ui->labelTransactionsStatus->setIcon(icon);
 316          ui->labelWalletStatus->setIcon(icon);
 317      }
 318  
 319      QWidget::changeEvent(e);
 320  }
 321  
 322  // Only show most recent NUM_ITEMS rows
 323  void OverviewPage::LimitTransactionRows()
 324  {
 325      if (filter && ui->listTransactions && ui->listTransactions->model() && filter.get() == ui->listTransactions->model()) {
 326          for (int i = 0; i < filter->rowCount(); ++i) {
 327              ui->listTransactions->setRowHidden(i, i >= NUM_ITEMS);
 328          }
 329      }
 330  }
 331  
 332  void OverviewPage::updateDisplayUnit()
 333  {
 334      if (walletModel && walletModel->getOptionsModel()) {
 335          const auto& balances = walletModel->getCachedBalance();
 336          if (balances.balance != -1) {
 337              setBalance(balances);
 338          }
 339  
 340          // Update txdelegate->unit with the current unit
 341          txdelegate->unit = walletModel->getOptionsModel()->getDisplayUnit();
 342  
 343          ui->listTransactions->update();
 344      }
 345  }
 346  
 347  void OverviewPage::updateAlerts(const QString &warnings)
 348  {
 349      this->ui->labelAlerts->setVisible(!warnings.isEmpty());
 350      this->ui->labelAlerts->setText(warnings);
 351  }
 352  
 353  void OverviewPage::showOutOfSyncWarning(bool fShow)
 354  {
 355      ui->labelWalletStatus->setVisible(fShow);
 356      ui->labelTransactionsStatus->setVisible(fShow);
 357  }
 358  
 359  void OverviewPage::setMonospacedFont(const QFont& dummy)
 360  {
 361      const LimenkaUnit display_unit = clientModel->getOptionsModel()->getDisplayUnit();
 362      const QFont f = clientModel->getOptionsModel()->getFontForMoney(display_unit);
 363      ui->labelBalance->setFont(f);
 364      ui->labelUnconfirmed->setFont(f);
 365      ui->labelImmature->setFont(f);
 366      ui->labelTotal->setFont(f);
 367      ui->labelWatchAvailable->setFont(f);
 368      ui->labelWatchPending->setFont(f);
 369      ui->labelWatchImmature->setFont(f);
 370      ui->labelWatchTotal->setFont(f);
 371  }
 372