// Copyright (c) 2011-2013 The Bitcoin developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "transactionview.h" #include "addresstablemodel.h" #include "bitcoinunits.h" #include "csvmodelwriter.h" #include "editaddressdialog.h" #include "guiutil.h" #include "optionsmodel.h" #include "transactiondescdialog.h" #include "transactionfilterproxy.h" #include "transactionrecord.h" #include "transactiontablemodel.h" #include "walletmodel.h" #include "ui_interface.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include TransactionView::TransactionView(QWidget *parent) : QWidget(parent), model(0), transactionProxyModel(0), transactionView(0) { // Build filter row setContentsMargins(0,0,0,0); QHBoxLayout *hlayout = new QHBoxLayout(); hlayout->setContentsMargins(0,0,0,0); #ifdef Q_OS_MAC hlayout->setSpacing(5); hlayout->addSpacing(26); #else hlayout->setSpacing(0); hlayout->addSpacing(23); #endif dateWidget = new QComboBox(this); #ifdef Q_OS_MAC dateWidget->setFixedWidth(121); #else dateWidget->setFixedWidth(120); #endif dateWidget->addItem(tr("All"), All); dateWidget->addItem(tr("Today"), Today); dateWidget->addItem(tr("This week"), ThisWeek); dateWidget->addItem(tr("This month"), ThisMonth); dateWidget->addItem(tr("Last month"), LastMonth); dateWidget->addItem(tr("This year"), ThisYear); dateWidget->addItem(tr("Range..."), Range); hlayout->addWidget(dateWidget); typeWidget = new QComboBox(this); #ifdef Q_OS_MAC typeWidget->setFixedWidth(121); #else typeWidget->setFixedWidth(120); #endif typeWidget->addItem(tr("All"), TransactionFilterProxy::ALL_TYPES); typeWidget->addItem(tr("Received with"), TransactionFilterProxy::TYPE(TransactionRecord::RecvWithAddress) | TransactionFilterProxy::TYPE(TransactionRecord::RecvFromOther)); typeWidget->addItem(tr("Sent to"), TransactionFilterProxy::TYPE(TransactionRecord::SendToAddress) | TransactionFilterProxy::TYPE(TransactionRecord::SendToOther)); typeWidget->addItem(tr("To yourself"), TransactionFilterProxy::TYPE(TransactionRecord::SendToSelf)); typeWidget->addItem(tr("Mined"), TransactionFilterProxy::TYPE(TransactionRecord::Generated)); typeWidget->addItem(tr("Other"), TransactionFilterProxy::TYPE(TransactionRecord::Other)); hlayout->addWidget(typeWidget); addressWidget = new QLineEdit(this); #if QT_VERSION >= 0x040700 addressWidget->setPlaceholderText(tr("Enter address or label to search")); #endif hlayout->addWidget(addressWidget); amountWidget = new QLineEdit(this); #if QT_VERSION >= 0x040700 amountWidget->setPlaceholderText(tr("Min amount")); #endif #ifdef Q_OS_MAC amountWidget->setFixedWidth(97); #else amountWidget->setFixedWidth(100); #endif amountWidget->setValidator(new QDoubleValidator(0, 1e20, 8, this)); hlayout->addWidget(amountWidget); QVBoxLayout *vlayout = new QVBoxLayout(this); vlayout->setContentsMargins(0,0,0,0); vlayout->setSpacing(0); QTableView *view = new QTableView(this); vlayout->addLayout(hlayout); vlayout->addWidget(createDateRangeWidget()); vlayout->addWidget(view); vlayout->setSpacing(0); int width = view->verticalScrollBar()->sizeHint().width(); // Cover scroll bar width with spacing #ifdef Q_OS_MAC hlayout->addSpacing(width+2); #else hlayout->addSpacing(width); #endif // Always show scroll bar view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); view->setTabKeyNavigation(false); view->setContextMenuPolicy(Qt::CustomContextMenu); view->installEventFilter(this); transactionView = view; // Actions QAction *copyAddressAction = new QAction(tr("Copy address"), this); QAction *copyLabelAction = new QAction(tr("Copy label"), this); QAction *copyAmountAction = new QAction(tr("Copy amount"), this); QAction *copyTxIDAction = new QAction(tr("Copy transaction ID"), this); QAction *editLabelAction = new QAction(tr("Edit label"), this); QAction *showDetailsAction = new QAction(tr("Show transaction details"), this); contextMenu = new QMenu(); contextMenu->addAction(copyAddressAction); contextMenu->addAction(copyLabelAction); contextMenu->addAction(copyAmountAction); contextMenu->addAction(copyTxIDAction); contextMenu->addAction(editLabelAction); contextMenu->addAction(showDetailsAction); mapperThirdPartyTxUrls = new QSignalMapper(this); // Connect actions connect(mapperThirdPartyTxUrls, SIGNAL(mapped(QString)), this, SLOT(openThirdPartyTxUrl(QString))); connect(dateWidget, SIGNAL(activated(int)), this, SLOT(chooseDate(int))); connect(typeWidget, SIGNAL(activated(int)), this, SLOT(chooseType(int))); connect(addressWidget, SIGNAL(textChanged(QString)), this, SLOT(changedPrefix(QString))); connect(amountWidget, SIGNAL(textChanged(QString)), this, SLOT(changedAmount(QString))); connect(view, SIGNAL(doubleClicked(QModelIndex)), this, SIGNAL(doubleClicked(QModelIndex))); connect(view, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(contextualMenu(QPoint))); connect(copyAddressAction, SIGNAL(triggered()), this, SLOT(copyAddress())); connect(copyLabelAction, SIGNAL(triggered()), this, SLOT(copyLabel())); connect(copyAmountAction, SIGNAL(triggered()), this, SLOT(copyAmount())); connect(copyTxIDAction, SIGNAL(triggered()), this, SLOT(copyTxID())); connect(editLabelAction, SIGNAL(triggered()), this, SLOT(editLabel())); connect(showDetailsAction, SIGNAL(triggered()), this, SLOT(showDetails())); } void TransactionView::setModel(WalletModel *model) { this->model = model; if(model) { transactionProxyModel = new TransactionFilterProxy(this); transactionProxyModel->setSourceModel(model->getTransactionTableModel()); transactionProxyModel->setDynamicSortFilter(true); transactionProxyModel->setSortCaseSensitivity(Qt::CaseInsensitive); transactionProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); transactionProxyModel->setSortRole(Qt::EditRole); transactionView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); transactionView->setModel(transactionProxyModel); transactionView->setAlternatingRowColors(true); transactionView->setSelectionBehavior(QAbstractItemView::SelectRows); transactionView->setSelectionMode(QAbstractItemView::ExtendedSelection); transactionView->setSortingEnabled(true); transactionView->sortByColumn(TransactionTableModel::Status, Qt::DescendingOrder); transactionView->verticalHeader()->hide(); transactionView->setColumnWidth(TransactionTableModel::Status, STATUS_COLUMN_WIDTH); transactionView->setColumnWidth(TransactionTableModel::Date, DATE_COLUMN_WIDTH); transactionView->setColumnWidth(TransactionTableModel::Type, TYPE_COLUMN_WIDTH); transactionView->setColumnWidth(TransactionTableModel::Amount, AMOUNT_MINIMUM_COLUMN_WIDTH); columnResizingFixer = new GUIUtil::TableViewLastColumnResizingFixer(transactionView, AMOUNT_MINIMUM_COLUMN_WIDTH, MINIMUM_COLUMN_WIDTH); if (model->getOptionsModel()) { // Add third party transaction URLs to context menu QStringList listUrls = model->getOptionsModel()->getThirdPartyTxUrls().split("|", QString::SkipEmptyParts); for (int i = 0; i < listUrls.size(); ++i) { QString host = QUrl(listUrls[i].trimmed(), QUrl::StrictMode).host(); if (!host.isEmpty()) { QAction *thirdPartyTxUrlAction = new QAction(host, this); // use host as menu item label if (i == 0) contextMenu->addSeparator(); contextMenu->addAction(thirdPartyTxUrlAction); connect(thirdPartyTxUrlAction, SIGNAL(triggered()), mapperThirdPartyTxUrls, SLOT(map())); mapperThirdPartyTxUrls->setMapping(thirdPartyTxUrlAction, listUrls[i].trimmed()); } } } } } void TransactionView::chooseDate(int idx) { if(!transactionProxyModel) return; QDate current = QDate::currentDate(); dateRangeWidget->setVisible(false); switch(dateWidget->itemData(idx).toInt()) { case All: transactionProxyModel->setDateRange( TransactionFilterProxy::MIN_DATE, TransactionFilterProxy::MAX_DATE); break; case Today: transactionProxyModel->setDateRange( QDateTime(current), TransactionFilterProxy::MAX_DATE); break; case ThisWeek: { // Find last Monday QDate startOfWeek = current.addDays(-(current.dayOfWeek()-1)); transactionProxyModel->setDateRange( QDateTime(startOfWeek), TransactionFilterProxy::MAX_DATE); } break; case ThisMonth: transactionProxyModel->setDateRange( QDateTime(QDate(current.year(), current.month(), 1)), TransactionFilterProxy::MAX_DATE); break; case LastMonth: transactionProxyModel->setDateRange( QDateTime(QDate(current.year(), current.month()-1, 1)), QDateTime(QDate(current.year(), current.month(), 1))); break; case ThisYear: transactionProxyModel->setDateRange( QDateTime(QDate(current.year(), 1, 1)), TransactionFilterProxy::MAX_DATE); break; case Range: dateRangeWidget->setVisible(true); dateRangeChanged(); break; } } void TransactionView::chooseType(int idx) { if(!transactionProxyModel) return; transactionProxyModel->setTypeFilter( typeWidget->itemData(idx).toInt()); } void TransactionView::changedPrefix(const QString &prefix) { if(!transactionProxyModel) return; transactionProxyModel->setAddressPrefix(prefix); } void TransactionView::changedAmount(const QString &amount) { if(!transactionProxyModel) return; qint64 amount_parsed = 0; if(BitcoinUnits::parse(model->getOptionsModel()->getDisplayUnit(), amount, &amount_parsed)) { transactionProxyModel->setMinAmount(amount_parsed); } else { transactionProxyModel->setMinAmount(0); } } void TransactionView::exportClicked() { // CSV is currently the only supported format QString filename = GUIUtil::getSaveFileName(this, tr("Export Transaction History"), QString(), tr("Comma separated file (*.csv)"), NULL); if (filename.isNull()) return; CSVModelWriter writer(filename); // name, column, role writer.setModel(transactionProxyModel); writer.addColumn(tr("Confirmed"), 0, TransactionTableModel::ConfirmedRole); writer.addColumn(tr("Date"), 0, TransactionTableModel::DateRole); writer.addColumn(tr("Type"), TransactionTableModel::Type, Qt::EditRole); writer.addColumn(tr("Label"), 0, TransactionTableModel::LabelRole); writer.addColumn(tr("Address"), 0, TransactionTableModel::AddressRole); writer.addColumn(tr("Amount"), 0, TransactionTableModel::FormattedAmountRole); writer.addColumn(tr("ID"), 0, TransactionTableModel::TxIDRole); if(!writer.write()) { emit message(tr("Exporting Failed"), tr("There was an error trying to save the transaction history to %1.").arg(filename), CClientUIInterface::MSG_ERROR); } else { emit message(tr("Exporting Successful"), tr("The transaction history was successfully saved to %1.").arg(filename), CClientUIInterface::MSG_INFORMATION); } } void TransactionView::contextualMenu(const QPoint &point) { QModelIndex index = transactionView->indexAt(point); if(index.isValid()) { contextMenu->exec(QCursor::pos()); } } void TransactionView::copyAddress() { GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::AddressRole); } void TransactionView::copyLabel() { GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::LabelRole); } void TransactionView::copyAmount() { GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::FormattedAmountRole); } void TransactionView::copyTxID() { GUIUtil::copyEntryData(transactionView, 0, TransactionTableModel::TxIDRole); } void TransactionView::editLabel() { if(!transactionView->selectionModel() ||!model) return; QModelIndexList selection = transactionView->selectionModel()->selectedRows(); if(!selection.isEmpty()) { AddressTableModel *addressBook = model->getAddressTableModel(); if(!addressBook) return; QString address = selection.at(0).data(TransactionTableModel::AddressRole).toString(); if(address.isEmpty()) { // If this transaction has no associated address, exit return; } // Is address in address book? Address book can miss address when a transaction is // sent from outside the UI. int idx = addressBook->lookupAddress(address); if(idx != -1) { // Edit sending / receiving address QModelIndex modelIdx = addressBook->index(idx, 0, QModelIndex()); // Determine type of address, launch appropriate editor dialog type QString type = modelIdx.data(AddressTableModel::TypeRole).toString(); EditAddressDialog dlg( type == AddressTableModel::Receive ? EditAddressDialog::EditReceivingAddress : EditAddressDialog::EditSendingAddress, this); dlg.setModel(addressBook); dlg.loadRow(idx); dlg.exec(); } else { // Add sending address EditAddressDialog dlg(EditAddressDialog::NewSendingAddress, this); dlg.setModel(addressBook); dlg.setAddress(address); dlg.exec(); } } } void TransactionView::showDetails() { if(!transactionView->selectionModel()) return; QModelIndexList selection = transactionView->selectionModel()->selectedRows(); if(!selection.isEmpty()) { TransactionDescDialog dlg(selection.at(0)); dlg.exec(); } } void TransactionView::openThirdPartyTxUrl(QString url) { if(!transactionView || !transactionView->selectionModel()) return; QModelIndexList selection = transactionView->selectionModel()->selectedRows(0); if(!selection.isEmpty()) QDesktopServices::openUrl(QUrl::fromUserInput(url.replace("%s", selection.at(0).data(TransactionTableModel::TxHashRole).toString()))); } QWidget *TransactionView::createDateRangeWidget() { dateRangeWidget = new QFrame(); dateRangeWidget->setFrameStyle(QFrame::Panel | QFrame::Raised); dateRangeWidget->setContentsMargins(1,1,1,1); QHBoxLayout *layout = new QHBoxLayout(dateRangeWidget); layout->setContentsMargins(0,0,0,0); layout->addSpacing(23); layout->addWidget(new QLabel(tr("Range:"))); dateFrom = new QDateTimeEdit(this); dateFrom->setDisplayFormat("dd/MM/yy"); dateFrom->setCalendarPopup(true); dateFrom->setMinimumWidth(100); dateFrom->setDate(QDate::currentDate().addDays(-7)); layout->addWidget(dateFrom); layout->addWidget(new QLabel(tr("to"))); dateTo = new QDateTimeEdit(this); dateTo->setDisplayFormat("dd/MM/yy"); dateTo->setCalendarPopup(true); dateTo->setMinimumWidth(100); dateTo->setDate(QDate::currentDate()); layout->addWidget(dateTo); layout->addStretch(); // Hide by default dateRangeWidget->setVisible(false); // Notify on change connect(dateFrom, SIGNAL(dateChanged(QDate)), this, SLOT(dateRangeChanged())); connect(dateTo, SIGNAL(dateChanged(QDate)), this, SLOT(dateRangeChanged())); return dateRangeWidget; } void TransactionView::dateRangeChanged() { if(!transactionProxyModel) return; transactionProxyModel->setDateRange( QDateTime(dateFrom->date()), QDateTime(dateTo->date()).addDays(1)); } void TransactionView::focusTransaction(const QModelIndex &idx) { if(!transactionProxyModel) return; QModelIndex targetIdx = transactionProxyModel->mapFromSource(idx); transactionView->scrollTo(targetIdx); transactionView->setCurrentIndex(targetIdx); transactionView->setFocus(); } // We override the virtual resizeEvent of the QWidget to adjust tables column // sizes as the tables width is proportional to the dialogs width. void TransactionView::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); columnResizingFixer->stretchColumnWidth(TransactionTableModel::ToAddress); } // Need to override default Ctrl+C action for amount as default behaviour is just to copy DisplayRole text bool TransactionView::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *ke = static_cast(event); if (ke->key() == Qt::Key_C && ke->modifiers().testFlag(Qt::ControlModifier)) { QModelIndex i = this->transactionView->currentIndex(); if (i.isValid() && i.column() == TransactionTableModel::Amount) { GUIUtil::setClipboard(i.data(TransactionTableModel::FormattedAmountRole).toString()); return true; } } } return QWidget::eventFilter(obj, event); }