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