1 // Copyright (c) 2011-2019 The Bitcoin 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 <qt/transactionview.h>
6 
7 #include <qt/addresstablemodel.h>
8 #include <qt/bitcoinunits.h>
9 #include <qt/csvmodelwriter.h>
10 #include <qt/editaddressdialog.h>
11 #include <qt/guiutil.h>
12 #include <qt/optionsmodel.h>
13 #include <qt/platformstyle.h>
14 #include <qt/transactiondescdialog.h>
15 #include <qt/transactionfilterproxy.h>
16 #include <qt/transactionrecord.h>
17 #include <qt/transactiontablemodel.h>
18 #include <qt/walletmodel.h>
19 
20 #include <node/ui_interface.h>
21 
22 #include <QApplication>
23 #include <QComboBox>
24 #include <QDateTimeEdit>
25 #include <QDesktopServices>
26 #include <QDoubleValidator>
27 #include <QHBoxLayout>
28 #include <QHeaderView>
29 #include <QLabel>
30 #include <QLineEdit>
31 #include <QMenu>
32 #include <QPoint>
33 #include <QScrollBar>
34 #include <QTableView>
35 #include <QTimer>
36 #include <QUrl>
37 #include <QVBoxLayout>
38 
TransactionView(const PlatformStyle * platformStyle,QWidget * parent)39 TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *parent) :
40     QWidget(parent)
41 {
42     // Build filter row
43     setContentsMargins(0,0,0,0);
44 
45     QHBoxLayout *hlayout = new QHBoxLayout();
46     hlayout->setContentsMargins(0,0,0,0);
47 
48     if (platformStyle->getUseExtraSpacing()) {
49         hlayout->setSpacing(5);
50         hlayout->addSpacing(26);
51     } else {
52         hlayout->setSpacing(0);
53         hlayout->addSpacing(23);
54     }
55 
56     watchOnlyWidget = new QComboBox(this);
57     watchOnlyWidget->setFixedWidth(24);
58     watchOnlyWidget->addItem("", TransactionFilterProxy::WatchOnlyFilter_All);
59     watchOnlyWidget->addItem(platformStyle->SingleColorIcon(":/icons/eye_plus"), "", TransactionFilterProxy::WatchOnlyFilter_Yes);
60     watchOnlyWidget->addItem(platformStyle->SingleColorIcon(":/icons/eye_minus"), "", TransactionFilterProxy::WatchOnlyFilter_No);
61     hlayout->addWidget(watchOnlyWidget);
62 
63     dateWidget = new QComboBox(this);
64     if (platformStyle->getUseExtraSpacing()) {
65         dateWidget->setFixedWidth(121);
66     } else {
67         dateWidget->setFixedWidth(120);
68     }
69     dateWidget->addItem(tr("All"), All);
70     dateWidget->addItem(tr("Today"), Today);
71     dateWidget->addItem(tr("This week"), ThisWeek);
72     dateWidget->addItem(tr("This month"), ThisMonth);
73     dateWidget->addItem(tr("Last month"), LastMonth);
74     dateWidget->addItem(tr("This year"), ThisYear);
75     dateWidget->addItem(tr("Range..."), Range);
76     hlayout->addWidget(dateWidget);
77 
78     typeWidget = new QComboBox(this);
79     if (platformStyle->getUseExtraSpacing()) {
80         typeWidget->setFixedWidth(121);
81     } else {
82         typeWidget->setFixedWidth(120);
83     }
84 
85     typeWidget->addItem(tr("All"), TransactionFilterProxy::ALL_TYPES);
86     typeWidget->addItem(tr("Received with"), TransactionFilterProxy::TYPE(TransactionRecord::RecvWithAddress) |
87                                         TransactionFilterProxy::TYPE(TransactionRecord::RecvFromOther));
88     typeWidget->addItem(tr("Sent to"), TransactionFilterProxy::TYPE(TransactionRecord::SendToAddress) |
89                                   TransactionFilterProxy::TYPE(TransactionRecord::SendToOther));
90     typeWidget->addItem(tr("To yourself"), TransactionFilterProxy::TYPE(TransactionRecord::SendToSelf));
91     typeWidget->addItem(tr("Mined"), TransactionFilterProxy::TYPE(TransactionRecord::Generated));
92     typeWidget->addItem(tr("Other"), TransactionFilterProxy::TYPE(TransactionRecord::Other));
93 
94     hlayout->addWidget(typeWidget);
95 
96     search_widget = new QLineEdit(this);
97     search_widget->setPlaceholderText(tr("Enter address, transaction id, or label to search"));
98     hlayout->addWidget(search_widget);
99 
100     amountWidget = new QLineEdit(this);
101     amountWidget->setPlaceholderText(tr("Min amount"));
102     if (platformStyle->getUseExtraSpacing()) {
103         amountWidget->setFixedWidth(97);
104     } else {
105         amountWidget->setFixedWidth(100);
106     }
107     QDoubleValidator *amountValidator = new QDoubleValidator(0, 1e20, 8, this);
108     QLocale amountLocale(QLocale::C);
109     amountLocale.setNumberOptions(QLocale::RejectGroupSeparator);
110     amountValidator->setLocale(amountLocale);
111     amountWidget->setValidator(amountValidator);
112     hlayout->addWidget(amountWidget);
113 
114     // Delay before filtering transactions in ms
115     static const int input_filter_delay = 200;
116 
117     QTimer* amount_typing_delay = new QTimer(this);
118     amount_typing_delay->setSingleShot(true);
119     amount_typing_delay->setInterval(input_filter_delay);
120 
121     QTimer* prefix_typing_delay = new QTimer(this);
122     prefix_typing_delay->setSingleShot(true);
123     prefix_typing_delay->setInterval(input_filter_delay);
124 
125     QVBoxLayout *vlayout = new QVBoxLayout(this);
126     vlayout->setContentsMargins(0,0,0,0);
127     vlayout->setSpacing(0);
128 
129     QTableView *view = new QTableView(this);
130     vlayout->addLayout(hlayout);
131     vlayout->addWidget(createDateRangeWidget());
132     vlayout->addWidget(view);
133     vlayout->setSpacing(0);
134     int width = view->verticalScrollBar()->sizeHint().width();
135     // Cover scroll bar width with spacing
136     if (platformStyle->getUseExtraSpacing()) {
137         hlayout->addSpacing(width+2);
138     } else {
139         hlayout->addSpacing(width);
140     }
141     // Always show scroll bar
142     view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
143     view->setTabKeyNavigation(false);
144     view->setContextMenuPolicy(Qt::CustomContextMenu);
145 
146     view->installEventFilter(this);
147 
148     transactionView = view;
149     transactionView->setObjectName("transactionView");
150 
151     // Actions
152     abandonAction = new QAction(tr("Abandon transaction"), this);
153     bumpFeeAction = new QAction(tr("Increase transaction fee"), this);
154     bumpFeeAction->setObjectName("bumpFeeAction");
155     copyAddressAction = new QAction(tr("Copy address"), this);
156     copyLabelAction = new QAction(tr("Copy label"), this);
157     QAction *copyAmountAction = new QAction(tr("Copy amount"), this);
158     QAction *copyTxIDAction = new QAction(tr("Copy transaction ID"), this);
159     QAction *copyTxHexAction = new QAction(tr("Copy raw transaction"), this);
160     QAction *copyTxPlainText = new QAction(tr("Copy full transaction details"), this);
161     QAction *editLabelAction = new QAction(tr("Edit label"), this);
162     QAction *showDetailsAction = new QAction(tr("Show transaction details"), this);
163 
164     contextMenu = new QMenu(this);
165     contextMenu->setObjectName("contextMenu");
166     contextMenu->addAction(copyAddressAction);
167     contextMenu->addAction(copyLabelAction);
168     contextMenu->addAction(copyAmountAction);
169     contextMenu->addAction(copyTxIDAction);
170     contextMenu->addAction(copyTxHexAction);
171     contextMenu->addAction(copyTxPlainText);
172     contextMenu->addAction(showDetailsAction);
173     contextMenu->addSeparator();
174     contextMenu->addAction(bumpFeeAction);
175     contextMenu->addAction(abandonAction);
176     contextMenu->addAction(editLabelAction);
177 
178     connect(dateWidget, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &TransactionView::chooseDate);
179     connect(typeWidget, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &TransactionView::chooseType);
180     connect(watchOnlyWidget, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &TransactionView::chooseWatchonly);
181     connect(amountWidget, &QLineEdit::textChanged, amount_typing_delay, static_cast<void (QTimer::*)()>(&QTimer::start));
182     connect(amount_typing_delay, &QTimer::timeout, this, &TransactionView::changedAmount);
183     connect(search_widget, &QLineEdit::textChanged, prefix_typing_delay, static_cast<void (QTimer::*)()>(&QTimer::start));
184     connect(prefix_typing_delay, &QTimer::timeout, this, &TransactionView::changedSearch);
185 
186     connect(view, &QTableView::doubleClicked, this, &TransactionView::doubleClicked);
187     connect(view, &QTableView::customContextMenuRequested, this, &TransactionView::contextualMenu);
188 
189     connect(bumpFeeAction, &QAction::triggered, this, &TransactionView::bumpFee);
190     connect(abandonAction, &QAction::triggered, this, &TransactionView::abandonTx);
191     connect(copyAddressAction, &QAction::triggered, this, &TransactionView::copyAddress);
192     connect(copyLabelAction, &QAction::triggered, this, &TransactionView::copyLabel);
193     connect(copyAmountAction, &QAction::triggered, this, &TransactionView::copyAmount);
194     connect(copyTxIDAction, &QAction::triggered, this, &TransactionView::copyTxID);
195     connect(copyTxHexAction, &QAction::triggered, this, &TransactionView::copyTxHex);
196     connect(copyTxPlainText, &QAction::triggered, this, &TransactionView::copyTxPlainText);
197     connect(editLabelAction, &QAction::triggered, this, &TransactionView::editLabel);
198     connect(showDetailsAction, &QAction::triggered, this, &TransactionView::showDetails);
199     // Double-clicking on a transaction on the transaction history page shows details
200     connect(this, &TransactionView::doubleClicked, this, &TransactionView::showDetails);
201     // Highlight transaction after fee bump
202     connect(this, &TransactionView::bumpedFee, [this](const uint256& txid) {
203       focusTransaction(txid);
204     });
205 }
206 
setModel(WalletModel * _model)207 void TransactionView::setModel(WalletModel *_model)
208 {
209     this->model = _model;
210     if(_model)
211     {
212         transactionProxyModel = new TransactionFilterProxy(this);
213         transactionProxyModel->setSourceModel(_model->getTransactionTableModel());
214         transactionProxyModel->setDynamicSortFilter(true);
215         transactionProxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
216         transactionProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
217 
218         transactionProxyModel->setSortRole(Qt::EditRole);
219 
220         transactionView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
221         transactionView->setModel(transactionProxyModel);
222         transactionView->setAlternatingRowColors(true);
223         transactionView->setSelectionBehavior(QAbstractItemView::SelectRows);
224         transactionView->setSelectionMode(QAbstractItemView::ExtendedSelection);
225         transactionView->horizontalHeader()->setSortIndicator(TransactionTableModel::Date, Qt::DescendingOrder);
226         transactionView->setSortingEnabled(true);
227         transactionView->verticalHeader()->hide();
228 
229         transactionView->setColumnWidth(TransactionTableModel::Status, STATUS_COLUMN_WIDTH);
230         transactionView->setColumnWidth(TransactionTableModel::Watchonly, WATCHONLY_COLUMN_WIDTH);
231         transactionView->setColumnWidth(TransactionTableModel::Date, DATE_COLUMN_WIDTH);
232         transactionView->setColumnWidth(TransactionTableModel::Type, TYPE_COLUMN_WIDTH);
233         transactionView->setColumnWidth(TransactionTableModel::Amount, AMOUNT_MINIMUM_COLUMN_WIDTH);
234 
235         columnResizingFixer = new GUIUtil::TableViewLastColumnResizingFixer(transactionView, AMOUNT_MINIMUM_COLUMN_WIDTH, MINIMUM_COLUMN_WIDTH, this);
236 
237         if (_model->getOptionsModel())
238         {
239             // Add third party transaction URLs to context menu
240             QStringList listUrls = _model->getOptionsModel()->getThirdPartyTxUrls().split("|", QString::SkipEmptyParts);
241             for (int i = 0; i < listUrls.size(); ++i)
242             {
243                 QString url = listUrls[i].trimmed();
244                 QString host = QUrl(url, QUrl::StrictMode).host();
245                 if (!host.isEmpty())
246                 {
247                     QAction *thirdPartyTxUrlAction = new QAction(host, this); // use host as menu item label
248                     if (i == 0)
249                         contextMenu->addSeparator();
250                     contextMenu->addAction(thirdPartyTxUrlAction);
251                     connect(thirdPartyTxUrlAction, &QAction::triggered, [this, url] { openThirdPartyTxUrl(url); });
252                 }
253             }
254         }
255 
256         // show/hide column Watch-only
257         updateWatchOnlyColumn(_model->wallet().haveWatchOnly());
258 
259         // Watch-only signal
260         connect(_model, &WalletModel::notifyWatchonlyChanged, this, &TransactionView::updateWatchOnlyColumn);
261     }
262 }
263 
chooseDate(int idx)264 void TransactionView::chooseDate(int idx)
265 {
266     if (!transactionProxyModel) return;
267     QDate current = QDate::currentDate();
268     dateRangeWidget->setVisible(false);
269     switch(dateWidget->itemData(idx).toInt())
270     {
271     case All:
272         transactionProxyModel->setDateRange(
273                 TransactionFilterProxy::MIN_DATE,
274                 TransactionFilterProxy::MAX_DATE);
275         break;
276     case Today:
277         transactionProxyModel->setDateRange(
278                 QDateTime(current),
279                 TransactionFilterProxy::MAX_DATE);
280         break;
281     case ThisWeek: {
282         // Find last Monday
283         QDate startOfWeek = current.addDays(-(current.dayOfWeek()-1));
284         transactionProxyModel->setDateRange(
285                 QDateTime(startOfWeek),
286                 TransactionFilterProxy::MAX_DATE);
287 
288         } break;
289     case ThisMonth:
290         transactionProxyModel->setDateRange(
291                 QDateTime(QDate(current.year(), current.month(), 1)),
292                 TransactionFilterProxy::MAX_DATE);
293         break;
294     case LastMonth:
295         transactionProxyModel->setDateRange(
296                 QDateTime(QDate(current.year(), current.month(), 1).addMonths(-1)),
297                 QDateTime(QDate(current.year(), current.month(), 1)));
298         break;
299     case ThisYear:
300         transactionProxyModel->setDateRange(
301                 QDateTime(QDate(current.year(), 1, 1)),
302                 TransactionFilterProxy::MAX_DATE);
303         break;
304     case Range:
305         dateRangeWidget->setVisible(true);
306         dateRangeChanged();
307         break;
308     }
309 }
310 
chooseType(int idx)311 void TransactionView::chooseType(int idx)
312 {
313     if(!transactionProxyModel)
314         return;
315     transactionProxyModel->setTypeFilter(
316         typeWidget->itemData(idx).toInt());
317 }
318 
chooseWatchonly(int idx)319 void TransactionView::chooseWatchonly(int idx)
320 {
321     if(!transactionProxyModel)
322         return;
323     transactionProxyModel->setWatchOnlyFilter(
324         static_cast<TransactionFilterProxy::WatchOnlyFilter>(watchOnlyWidget->itemData(idx).toInt()));
325 }
326 
changedSearch()327 void TransactionView::changedSearch()
328 {
329     if(!transactionProxyModel)
330         return;
331     transactionProxyModel->setSearchString(search_widget->text());
332 }
333 
changedAmount()334 void TransactionView::changedAmount()
335 {
336     if(!transactionProxyModel)
337         return;
338     CAmount amount_parsed = 0;
339     if (BitcoinUnits::parse(model->getOptionsModel()->getDisplayUnit(), amountWidget->text(), &amount_parsed)) {
340         transactionProxyModel->setMinAmount(amount_parsed);
341     }
342     else
343     {
344         transactionProxyModel->setMinAmount(0);
345     }
346 }
347 
exportClicked()348 void TransactionView::exportClicked()
349 {
350     if (!model || !model->getOptionsModel()) {
351         return;
352     }
353 
354     // CSV is currently the only supported format
355     QString filename = GUIUtil::getSaveFileName(this,
356         tr("Export Transaction History"), QString(),
357         tr("Comma separated file (*.csv)"), nullptr);
358 
359     if (filename.isNull())
360         return;
361 
362     CSVModelWriter writer(filename);
363 
364     // name, column, role
365     writer.setModel(transactionProxyModel);
366     writer.addColumn(tr("Confirmed"), 0, TransactionTableModel::ConfirmedRole);
367     if (model->wallet().haveWatchOnly())
368         writer.addColumn(tr("Watch-only"), TransactionTableModel::Watchonly);
369     writer.addColumn(tr("Date"), 0, TransactionTableModel::DateRole);
370     writer.addColumn(tr("Type"), TransactionTableModel::Type, Qt::EditRole);
371     writer.addColumn(tr("Label"), 0, TransactionTableModel::LabelRole);
372     writer.addColumn(tr("Address"), 0, TransactionTableModel::AddressRole);
373     writer.addColumn(BitcoinUnits::getAmountColumnTitle(model->getOptionsModel()->getDisplayUnit()), 0, TransactionTableModel::FormattedAmountRole);
374     writer.addColumn(tr("ID"), 0, TransactionTableModel::TxHashRole);
375 
376     if(!writer.write()) {
377         Q_EMIT message(tr("Exporting Failed"), tr("There was an error trying to save the transaction history to %1.").arg(filename),
378             CClientUIInterface::MSG_ERROR);
379     }
380     else {
381         Q_EMIT message(tr("Exporting Successful"), tr("The transaction history was successfully saved to %1.").arg(filename),
382             CClientUIInterface::MSG_INFORMATION);
383     }
384 }
385 
contextualMenu(const QPoint & point)386 void TransactionView::contextualMenu(const QPoint &point)
387 {
388     QModelIndex index = transactionView->indexAt(point);
389     QModelIndexList selection = transactionView->selectionModel()->selectedRows(0);
390     if (selection.empty())
391         return;
392 
393     // check if transaction can be abandoned, disable context menu action in case it doesn't
394     uint256 hash;
395     hash.SetHex(selection.at(0).data(TransactionTableModel::TxHashRole).toString().toStdString());
396     abandonAction->setEnabled(model->wallet().transactionCanBeAbandoned(hash));
397     bumpFeeAction->setEnabled(model->wallet().transactionCanBeBumped(hash));
398     copyAddressAction->setEnabled(GUIUtil::hasEntryData(transactionView, 0, TransactionTableModel::AddressRole));
399     copyLabelAction->setEnabled(GUIUtil::hasEntryData(transactionView, 0, TransactionTableModel::LabelRole));
400 
401     if (index.isValid()) {
402         GUIUtil::PopupMenu(contextMenu, transactionView->viewport()->mapToGlobal(point));
403     }
404 }
405 
abandonTx()406 void TransactionView::abandonTx()
407 {
408     if(!transactionView || !transactionView->selectionModel())
409         return;
410     QModelIndexList selection = transactionView->selectionModel()->selectedRows(0);
411 
412     // get the hash from the TxHashRole (QVariant / QString)
413     uint256 hash;
414     QString hashQStr = selection.at(0).data(TransactionTableModel::TxHashRole).toString();
415     hash.SetHex(hashQStr.toStdString());
416 
417     // Abandon the wallet transaction over the walletModel
418     model->wallet().abandonTransaction(hash);
419 
420     // Update the table
421     model->getTransactionTableModel()->updateTransaction(hashQStr, CT_UPDATED, false);
422 }
423 
bumpFee()424 void TransactionView::bumpFee()
425 {
426     if(!transactionView || !transactionView->selectionModel())
427         return;
428     QModelIndexList selection = transactionView->selectionModel()->selectedRows(0);
429 
430     // get the hash from the TxHashRole (QVariant / QString)
431     uint256 hash;
432     QString hashQStr = selection.at(0).data(TransactionTableModel::TxHashRole).toString();
433     hash.SetHex(hashQStr.toStdString());
434 
435     // Bump tx fee over the walletModel
436     uint256 newHash;
437     if (model->bumpFee(hash, newHash)) {
438         // Update the table
439         transactionView->selectionModel()->clearSelection();
440         model->getTransactionTableModel()->updateTransaction(hashQStr, CT_UPDATED, true);
441 
442         qApp->processEvents();
443         Q_EMIT bumpedFee(newHash);
444     }
445 }
446 
copyAddress()447 void TransactionView::copyAddress()
448 {
449     GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::AddressRole);
450 }
451 
copyLabel()452 void TransactionView::copyLabel()
453 {
454     GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::LabelRole);
455 }
456 
copyAmount()457 void TransactionView::copyAmount()
458 {
459     GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::FormattedAmountRole);
460 }
461 
copyTxID()462 void TransactionView::copyTxID()
463 {
464     GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::TxHashRole);
465 }
466 
copyTxHex()467 void TransactionView::copyTxHex()
468 {
469     GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::TxHexRole);
470 }
471 
copyTxPlainText()472 void TransactionView::copyTxPlainText()
473 {
474     GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::TxPlainTextRole);
475 }
476 
editLabel()477 void TransactionView::editLabel()
478 {
479     if(!transactionView->selectionModel() ||!model)
480         return;
481     QModelIndexList selection = transactionView->selectionModel()->selectedRows();
482     if(!selection.isEmpty())
483     {
484         AddressTableModel *addressBook = model->getAddressTableModel();
485         if(!addressBook)
486             return;
487         QString address = selection.at(0).data(TransactionTableModel::AddressRole).toString();
488         if(address.isEmpty())
489         {
490             // If this transaction has no associated address, exit
491             return;
492         }
493         // Is address in address book? Address book can miss address when a transaction is
494         // sent from outside the UI.
495         int idx = addressBook->lookupAddress(address);
496         if(idx != -1)
497         {
498             // Edit sending / receiving address
499             QModelIndex modelIdx = addressBook->index(idx, 0, QModelIndex());
500             // Determine type of address, launch appropriate editor dialog type
501             QString type = modelIdx.data(AddressTableModel::TypeRole).toString();
502 
503             EditAddressDialog dlg(
504                 type == AddressTableModel::Receive
505                 ? EditAddressDialog::EditReceivingAddress
506                 : EditAddressDialog::EditSendingAddress, this);
507             dlg.setModel(addressBook);
508             dlg.loadRow(idx);
509             dlg.exec();
510         }
511         else
512         {
513             // Add sending address
514             EditAddressDialog dlg(EditAddressDialog::NewSendingAddress,
515                 this);
516             dlg.setModel(addressBook);
517             dlg.setAddress(address);
518             dlg.exec();
519         }
520     }
521 }
522 
showDetails()523 void TransactionView::showDetails()
524 {
525     if(!transactionView->selectionModel())
526         return;
527     QModelIndexList selection = transactionView->selectionModel()->selectedRows();
528     if(!selection.isEmpty())
529     {
530         TransactionDescDialog *dlg = new TransactionDescDialog(selection.at(0));
531         dlg->setAttribute(Qt::WA_DeleteOnClose);
532         dlg->show();
533     }
534 }
535 
openThirdPartyTxUrl(QString url)536 void TransactionView::openThirdPartyTxUrl(QString url)
537 {
538     if(!transactionView || !transactionView->selectionModel())
539         return;
540     QModelIndexList selection = transactionView->selectionModel()->selectedRows(0);
541     if(!selection.isEmpty())
542          QDesktopServices::openUrl(QUrl::fromUserInput(url.replace("%s", selection.at(0).data(TransactionTableModel::TxHashRole).toString())));
543 }
544 
createDateRangeWidget()545 QWidget *TransactionView::createDateRangeWidget()
546 {
547     dateRangeWidget = new QFrame();
548     dateRangeWidget->setFrameStyle(QFrame::Panel | QFrame::Raised);
549     dateRangeWidget->setContentsMargins(1,1,1,1);
550     QHBoxLayout *layout = new QHBoxLayout(dateRangeWidget);
551     layout->setContentsMargins(0,0,0,0);
552     layout->addSpacing(23);
553     layout->addWidget(new QLabel(tr("Range:")));
554 
555     dateFrom = new QDateTimeEdit(this);
556     dateFrom->setDisplayFormat("dd/MM/yy");
557     dateFrom->setCalendarPopup(true);
558     dateFrom->setMinimumWidth(100);
559     dateFrom->setDate(QDate::currentDate().addDays(-7));
560     layout->addWidget(dateFrom);
561     layout->addWidget(new QLabel(tr("to")));
562 
563     dateTo = new QDateTimeEdit(this);
564     dateTo->setDisplayFormat("dd/MM/yy");
565     dateTo->setCalendarPopup(true);
566     dateTo->setMinimumWidth(100);
567     dateTo->setDate(QDate::currentDate());
568     layout->addWidget(dateTo);
569     layout->addStretch();
570 
571     // Hide by default
572     dateRangeWidget->setVisible(false);
573 
574     // Notify on change
575     connect(dateFrom, &QDateTimeEdit::dateChanged, this, &TransactionView::dateRangeChanged);
576     connect(dateTo, &QDateTimeEdit::dateChanged, this, &TransactionView::dateRangeChanged);
577 
578     return dateRangeWidget;
579 }
580 
dateRangeChanged()581 void TransactionView::dateRangeChanged()
582 {
583     if(!transactionProxyModel)
584         return;
585     transactionProxyModel->setDateRange(
586             QDateTime(dateFrom->date()),
587             QDateTime(dateTo->date()).addDays(1));
588 }
589 
focusTransaction(const QModelIndex & idx)590 void TransactionView::focusTransaction(const QModelIndex &idx)
591 {
592     if(!transactionProxyModel)
593         return;
594     QModelIndex targetIdx = transactionProxyModel->mapFromSource(idx);
595     transactionView->scrollTo(targetIdx);
596     transactionView->setCurrentIndex(targetIdx);
597     transactionView->setFocus();
598 }
599 
focusTransaction(const uint256 & txid)600 void TransactionView::focusTransaction(const uint256& txid)
601 {
602     if (!transactionProxyModel)
603         return;
604 
605     const QModelIndexList results = this->model->getTransactionTableModel()->match(
606         this->model->getTransactionTableModel()->index(0,0),
607         TransactionTableModel::TxHashRole,
608         QString::fromStdString(txid.ToString()), -1);
609 
610     transactionView->setFocus();
611     transactionView->selectionModel()->clearSelection();
612     for (const QModelIndex& index : results) {
613         const QModelIndex targetIndex = transactionProxyModel->mapFromSource(index);
614         transactionView->selectionModel()->select(
615             targetIndex,
616             QItemSelectionModel::Rows | QItemSelectionModel::Select);
617         // Called once per destination to ensure all results are in view, unless
618         // transactions are not ordered by (ascending or descending) date.
619         transactionView->scrollTo(targetIndex);
620         // scrollTo() does not scroll far enough the first time when transactions
621         // are ordered by ascending date.
622         if (index == results[0]) transactionView->scrollTo(targetIndex);
623     }
624 }
625 
626 // We override the virtual resizeEvent of the QWidget to adjust tables column
627 // sizes as the tables width is proportional to the dialogs width.
resizeEvent(QResizeEvent * event)628 void TransactionView::resizeEvent(QResizeEvent* event)
629 {
630     QWidget::resizeEvent(event);
631     columnResizingFixer->stretchColumnWidth(TransactionTableModel::ToAddress);
632 }
633 
634 // Need to override default Ctrl+C action for amount as default behaviour is just to copy DisplayRole text
eventFilter(QObject * obj,QEvent * event)635 bool TransactionView::eventFilter(QObject *obj, QEvent *event)
636 {
637     if (event->type() == QEvent::KeyPress)
638     {
639         QKeyEvent *ke = static_cast<QKeyEvent *>(event);
640         if (ke->key() == Qt::Key_C && ke->modifiers().testFlag(Qt::ControlModifier))
641         {
642              GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::TxPlainTextRole);
643              return true;
644         }
645     }
646     return QWidget::eventFilter(obj, event);
647 }
648 
649 // show/hide column Watch-only
updateWatchOnlyColumn(bool fHaveWatchOnly)650 void TransactionView::updateWatchOnlyColumn(bool fHaveWatchOnly)
651 {
652     watchOnlyWidget->setVisible(fHaveWatchOnly);
653     transactionView->setColumnHidden(TransactionTableModel::Watchonly, !fHaveWatchOnly);
654 }
655