diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 7f8bc1c6d..1a473ac4b 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -184,6 +184,7 @@ QT_MOC_CPP = \ qt/moc_transactionfilterproxy.cpp \ qt/moc_transactiontablemodel.cpp \ qt/moc_transactionview.cpp \ + qt/moc_kevaview.cpp \ qt/moc_utilitydialog.cpp \ qt/moc_walletframe.cpp \ qt/moc_walletmodel.cpp \ @@ -258,6 +259,7 @@ BITCOIN_QT_H = \ qt/transactionrecord.h \ qt/transactiontablemodel.h \ qt/transactionview.h \ + qt/kevaview.h \ qt/utilitydialog.h \ qt/walletframe.h \ qt/walletmodel.h \ @@ -372,6 +374,7 @@ BITCOIN_QT_WALLET_CPP = \ qt/transactionrecord.cpp \ qt/transactiontablemodel.cpp \ qt/transactionview.cpp \ + qt/kevaview.cpp \ qt/walletframe.cpp \ qt/walletmodel.cpp \ qt/walletmodeltransaction.cpp \ diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 422c90725..2a07ab6d9 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -313,6 +313,13 @@ void BitcoinGUI::createActions() historyAction->setShortcut(QKeySequence(Qt::ALT + Qt::Key_4)); tabGroup->addAction(historyAction); + kevaAction = new QAction(platformStyle->SingleColorIcon(":/icons/key"), tr("&Keva"), this); + kevaAction->setStatusTip(tr("Keva related operations")); + kevaAction->setToolTip(kevaAction->statusTip()); + kevaAction->setCheckable(true); + kevaAction->setShortcut(QKeySequence(Qt::ALT + Qt::Key_5)); + tabGroup->addAction(kevaAction); + #ifdef ENABLE_WALLET // These showNormalIfMinimized are needed because Send Coins and Receive Coins // can be triggered from the tray menu, and need to show the GUI to be useful. @@ -328,6 +335,8 @@ void BitcoinGUI::createActions() connect(receiveCoinsMenuAction, SIGNAL(triggered()), this, SLOT(gotoReceiveCoinsPage())); connect(historyAction, SIGNAL(triggered()), this, SLOT(showNormalIfMinimized())); connect(historyAction, SIGNAL(triggered()), this, SLOT(gotoHistoryPage())); + connect(kevaAction, SIGNAL(triggered()), this, SLOT(showNormalIfMinimized())); + connect(kevaAction, SIGNAL(triggered()), this, SLOT(gotoKevaPage())); #endif // ENABLE_WALLET quitAction = new QAction(platformStyle->TextColorIcon(":/icons/quit"), tr("E&xit"), this); @@ -462,6 +471,7 @@ void BitcoinGUI::createToolBars() toolbar->addAction(sendCoinsAction); toolbar->addAction(receiveCoinsAction); toolbar->addAction(historyAction); + toolbar->addAction(kevaAction); overviewAction->setChecked(true); } } @@ -498,13 +508,13 @@ void BitcoinGUI::setClientModel(ClientModel *_clientModel) } #endif // ENABLE_WALLET unitDisplayControl->setOptionsModel(_clientModel->getOptionsModel()); - + OptionsModel* optionsModel = _clientModel->getOptionsModel(); if(optionsModel) { // be aware of the tray icon disable state change reported by the OptionsModel object. connect(optionsModel,SIGNAL(hideTrayIconChanged(bool)),this,SLOT(setTrayIconVisible(bool))); - + // initialize the disable state of the tray icon with the current value in the model. setTrayIconVisible(optionsModel->getHideTrayIcon()); } @@ -691,6 +701,12 @@ void BitcoinGUI::gotoHistoryPage() if (walletFrame) walletFrame->gotoHistoryPage(); } +void BitcoinGUI::gotoKevaPage() +{ + kevaAction->setChecked(true); + if (walletFrame) walletFrame->gotoKevaPage(); +} + void BitcoinGUI::gotoReceiveCoinsPage() { receiveCoinsAction->setChecked(true); @@ -1052,7 +1068,7 @@ void BitcoinGUI::setHDStatus(int hdEnabled) labelWalletHDStatusIcon->setPixmap(platformStyle->SingleColorIcon(hdEnabled ? ":/icons/hd_enabled" : ":/icons/hd_disabled").pixmap(STATUSBAR_ICONSIZE,STATUSBAR_ICONSIZE)); labelWalletHDStatusIcon->setToolTip(hdEnabled ? tr("HD key generation is enabled") : tr("HD key generation is disabled")); - // eventually disable the QLabel to set its opacity to 50% + // eventually disable the QLabel to set its opacity to 50% labelWalletHDStatusIcon->setEnabled(hdEnabled); } diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index ddb7ecb76..feaa27a8a 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -91,6 +91,7 @@ private: QMenuBar *appMenuBar; QAction *overviewAction; + QAction *kevaAction; QAction *historyAction; QAction *quitAction; QAction *sendCoinsAction; @@ -195,6 +196,8 @@ private Q_SLOTS: void gotoOverviewPage(); /** Switch to history (transactions) page */ void gotoHistoryPage(); + /** Switch to Keva page */ + void gotoKevaPage(); /** Switch to receive coins page */ void gotoReceiveCoinsPage(); /** Switch to send coins page */ @@ -233,7 +236,7 @@ private Q_SLOTS: /** Show progress dialog e.g. for verifychain */ void showProgress(const QString &title, int nProgress); - + /** When hideTrayIcon setting is changed in OptionsModel hide or show the icon accordingly. */ void setTrayIconVisible(bool); diff --git a/src/qt/bitcoinstrings.cpp b/src/qt/bitcoinstrings.cpp index d6f4e7100..37169f73d 100644 --- a/src/qt/bitcoinstrings.cpp +++ b/src/qt/bitcoinstrings.cpp @@ -9,7 +9,7 @@ #define UNUSED #endif static const char UNUSED *bitcoin_strings[] = { -QT_TRANSLATE_NOOP("bitcoin-core", "Bitcoin Core"), +QT_TRANSLATE_NOOP("bitcoin-core", "Kevacoin Core"), QT_TRANSLATE_NOOP("bitcoin-core", "The %s developers"), QT_TRANSLATE_NOOP("bitcoin-core", "" "(1 = keep tx meta data e.g. account owner and payment request information, 2 " @@ -156,7 +156,7 @@ QT_TRANSLATE_NOOP("bitcoin-core", "" "and enables automatic pruning of old blocks if a target size in MiB is " "provided. This mode is incompatible with -txindex and -rescan. Warning: " "Reverting this setting requires re-downloading the entire blockchain. " -"(default: 0 = disable pruning blocks, 1 = allow manual pruning via RPC, >%u " +"(default: 0 = disable pruning blocks, 1 = allow manual pruning via RPC, >=%u " "= automatically prune block files to stay under the specified target size in " "MiB)"), QT_TRANSLATE_NOOP("bitcoin-core", "" @@ -320,6 +320,9 @@ QT_TRANSLATE_NOOP("bitcoin-core", "Include IP addresses in debug output (default QT_TRANSLATE_NOOP("bitcoin-core", "Incorrect or no genesis block found. Wrong datadir for network?"), QT_TRANSLATE_NOOP("bitcoin-core", "Information"), QT_TRANSLATE_NOOP("bitcoin-core", "Initialization sanity check failed. %s is shutting down."), +QT_TRANSLATE_NOOP("bitcoin-core", "Input tx is not a keva operation"), +QT_TRANSLATE_NOOP("bitcoin-core", "Input tx is not mine"), +QT_TRANSLATE_NOOP("bitcoin-core", "Input tx not found in wallet"), QT_TRANSLATE_NOOP("bitcoin-core", "Insufficient funds"), QT_TRANSLATE_NOOP("bitcoin-core", "Invalid -onion address or hostname: '%s'"), QT_TRANSLATE_NOOP("bitcoin-core", "Invalid -proxy address or hostname: '%s'"), diff --git a/src/qt/kevaview.cpp b/src/qt/kevaview.cpp new file mode 100644 index 000000000..d01eeed49 --- /dev/null +++ b/src/qt/kevaview.cpp @@ -0,0 +1,623 @@ +// Copyright (c) 2011-2017 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +KevaView::KevaView(const PlatformStyle *platformStyle, QWidget *parent) : + QWidget(parent), model(0), transactionProxyModel(0), + kevaView(0), abandonAction(0), bumpFeeAction(0), columnResizingFixer(0) +{ + // Build filter row + setContentsMargins(0,0,0,0); + + QHBoxLayout *hlayout = new QHBoxLayout(); + hlayout->setContentsMargins(0,0,0,0); + + if (platformStyle->getUseExtraSpacing()) { + hlayout->setSpacing(5); + hlayout->addSpacing(26); + } else { + hlayout->setSpacing(0); + hlayout->addSpacing(23); + } + + watchOnlyWidget = new QComboBox(this); + watchOnlyWidget->setFixedWidth(24); + watchOnlyWidget->addItem("", TransactionFilterProxy::WatchOnlyFilter_All); + watchOnlyWidget->addItem(platformStyle->SingleColorIcon(":/icons/eye_plus"), "", TransactionFilterProxy::WatchOnlyFilter_Yes); + watchOnlyWidget->addItem(platformStyle->SingleColorIcon(":/icons/eye_minus"), "", TransactionFilterProxy::WatchOnlyFilter_No); + hlayout->addWidget(watchOnlyWidget); + + dateWidget = new QComboBox(this); + if (platformStyle->getUseExtraSpacing()) { + dateWidget->setFixedWidth(121); + } else { + dateWidget->setFixedWidth(120); + } + 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); + if (platformStyle->getUseExtraSpacing()) { + typeWidget->setFixedWidth(121); + } else { + typeWidget->setFixedWidth(120); + } + + 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); + + search_widget = new QLineEdit(this); +#if QT_VERSION >= 0x040700 + search_widget->setPlaceholderText(tr("Enter address, transaction id, or label to search")); +#endif + hlayout->addWidget(search_widget); + + amountWidget = new QLineEdit(this); +#if QT_VERSION >= 0x040700 + amountWidget->setPlaceholderText(tr("Min amount")); +#endif + if (platformStyle->getUseExtraSpacing()) { + amountWidget->setFixedWidth(97); + } else { + amountWidget->setFixedWidth(100); + } + amountWidget->setValidator(new QDoubleValidator(0, 1e20, 8, this)); + hlayout->addWidget(amountWidget); + + // Delay before filtering transactions in ms + static const int input_filter_delay = 200; + + QTimer* amount_typing_delay = new QTimer(this); + amount_typing_delay->setSingleShot(true); + amount_typing_delay->setInterval(input_filter_delay); + + QTimer* prefix_typing_delay = new QTimer(this); + prefix_typing_delay->setSingleShot(true); + prefix_typing_delay->setInterval(input_filter_delay); + + 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 + if (platformStyle->getUseExtraSpacing()) { + hlayout->addSpacing(width+2); + } else { + hlayout->addSpacing(width); + } + // Always show scroll bar + view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + view->setTabKeyNavigation(false); + view->setContextMenuPolicy(Qt::CustomContextMenu); + + view->installEventFilter(this); + + kevaView = view; + kevaView->setObjectName("KevaView"); + + // Actions + abandonAction = new QAction(tr("Abandon transaction"), this); + bumpFeeAction = new QAction(tr("Increase transaction fee"), this); + bumpFeeAction->setObjectName("bumpFeeAction"); + 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 *copyTxHexAction = new QAction(tr("Copy raw transaction"), this); + QAction *copyTxPlainText = new QAction(tr("Copy full transaction details"), this); + QAction *editLabelAction = new QAction(tr("Edit label"), this); + QAction *showDetailsAction = new QAction(tr("Show transaction details"), this); + + contextMenu = new QMenu(this); + contextMenu->setObjectName("contextMenu"); + contextMenu->addAction(copyAddressAction); + contextMenu->addAction(copyLabelAction); + contextMenu->addAction(copyAmountAction); + contextMenu->addAction(copyTxIDAction); + contextMenu->addAction(copyTxHexAction); + contextMenu->addAction(copyTxPlainText); + contextMenu->addAction(showDetailsAction); + contextMenu->addSeparator(); + contextMenu->addAction(bumpFeeAction); + contextMenu->addAction(abandonAction); + contextMenu->addAction(editLabelAction); + + 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(watchOnlyWidget, SIGNAL(activated(int)), this, SLOT(chooseWatchonly(int))); + connect(amountWidget, SIGNAL(textChanged(QString)), amount_typing_delay, SLOT(start())); + connect(amount_typing_delay, SIGNAL(timeout()), this, SLOT(changedAmount())); + connect(search_widget, SIGNAL(textChanged(QString)), prefix_typing_delay, SLOT(start())); + connect(prefix_typing_delay, SIGNAL(timeout()), this, SLOT(changedSearch())); + + connect(view, SIGNAL(doubleClicked(QModelIndex)), this, SIGNAL(doubleClicked(QModelIndex))); + connect(view, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(contextualMenu(QPoint))); + + connect(bumpFeeAction, SIGNAL(triggered()), this, SLOT(bumpFee())); + connect(abandonAction, SIGNAL(triggered()), this, SLOT(abandonTx())); + 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(copyTxHexAction, SIGNAL(triggered()), this, SLOT(copyTxHex())); + connect(copyTxPlainText, SIGNAL(triggered()), this, SLOT(copyTxPlainText())); + connect(editLabelAction, SIGNAL(triggered()), this, SLOT(editLabel())); + connect(showDetailsAction, SIGNAL(triggered()), this, SLOT(showDetails())); +} + +void KevaView::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); + + kevaView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + kevaView->setModel(transactionProxyModel); + kevaView->setAlternatingRowColors(true); + kevaView->setSelectionBehavior(QAbstractItemView::SelectRows); + kevaView->setSelectionMode(QAbstractItemView::ExtendedSelection); + kevaView->setSortingEnabled(true); + kevaView->sortByColumn(TransactionTableModel::Date, Qt::DescendingOrder); + kevaView->verticalHeader()->hide(); + + kevaView->setColumnWidth(TransactionTableModel::Status, STATUS_COLUMN_WIDTH); + kevaView->setColumnWidth(TransactionTableModel::Watchonly, WATCHONLY_COLUMN_WIDTH); + kevaView->setColumnWidth(TransactionTableModel::Date, DATE_COLUMN_WIDTH); + kevaView->setColumnWidth(TransactionTableModel::Type, TYPE_COLUMN_WIDTH); + kevaView->setColumnWidth(TransactionTableModel::Amount, AMOUNT_MINIMUM_COLUMN_WIDTH); + + columnResizingFixer = new GUIUtil::TableViewLastColumnResizingFixer(kevaView, AMOUNT_MINIMUM_COLUMN_WIDTH, MINIMUM_COLUMN_WIDTH, this); + + 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()); + } + } + } + + // show/hide column Watch-only + updateWatchOnlyColumn(_model->haveWatchOnly()); + + // Watch-only signal + connect(_model, SIGNAL(notifyWatchonlyChanged(bool)), this, SLOT(updateWatchOnlyColumn(bool))); + } +} + +void KevaView::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).addMonths(-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 KevaView::chooseType(int idx) +{ + if(!transactionProxyModel) + return; + transactionProxyModel->setTypeFilter( + typeWidget->itemData(idx).toInt()); +} + +void KevaView::chooseWatchonly(int idx) +{ + if(!transactionProxyModel) + return; + transactionProxyModel->setWatchOnlyFilter( + (TransactionFilterProxy::WatchOnlyFilter)watchOnlyWidget->itemData(idx).toInt()); +} + +void KevaView::changedSearch() +{ + if(!transactionProxyModel) + return; + transactionProxyModel->setSearchString(search_widget->text()); +} + +void KevaView::changedAmount() +{ + if(!transactionProxyModel) + return; + CAmount amount_parsed = 0; + if (BitcoinUnits::parse(model->getOptionsModel()->getDisplayUnit(), amountWidget->text(), &amount_parsed)) { + transactionProxyModel->setMinAmount(amount_parsed); + } + else + { + transactionProxyModel->setMinAmount(0); + } +} + +void KevaView::exportClicked() +{ + if (!model || !model->getOptionsModel()) { + return; + } + + // CSV is currently the only supported format + QString filename = GUIUtil::getSaveFileName(this, + tr("Export Transaction History"), QString(), + tr("Comma separated file (*.csv)"), nullptr); + + if (filename.isNull()) + return; + + CSVModelWriter writer(filename); + + // name, column, role + writer.setModel(transactionProxyModel); + writer.addColumn(tr("Confirmed"), 0, TransactionTableModel::ConfirmedRole); + if (model->haveWatchOnly()) + writer.addColumn(tr("Watch-only"), TransactionTableModel::Watchonly); + 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(BitcoinUnits::getAmountColumnTitle(model->getOptionsModel()->getDisplayUnit()), 0, TransactionTableModel::FormattedAmountRole); + writer.addColumn(tr("ID"), 0, TransactionTableModel::TxIDRole); + + if(!writer.write()) { + Q_EMIT message(tr("Exporting Failed"), tr("There was an error trying to save the transaction history to %1.").arg(filename), + CClientUIInterface::MSG_ERROR); + } + else { + Q_EMIT message(tr("Exporting Successful"), tr("The transaction history was successfully saved to %1.").arg(filename), + CClientUIInterface::MSG_INFORMATION); + } +} + +void KevaView::contextualMenu(const QPoint &point) +{ + QModelIndex index = kevaView->indexAt(point); + QModelIndexList selection = kevaView->selectionModel()->selectedRows(0); + if (selection.empty()) + return; + + // check if transaction can be abandoned, disable context menu action in case it doesn't + uint256 hash; + hash.SetHex(selection.at(0).data(TransactionTableModel::TxHashRole).toString().toStdString()); + abandonAction->setEnabled(model->transactionCanBeAbandoned(hash)); + bumpFeeAction->setEnabled(model->transactionCanBeBumped(hash)); + + if(index.isValid()) + { + contextMenu->popup(kevaView->viewport()->mapToGlobal(point)); + } +} + +void KevaView::abandonTx() +{ + if(!kevaView || !kevaView->selectionModel()) + return; + QModelIndexList selection = kevaView->selectionModel()->selectedRows(0); + + // get the hash from the TxHashRole (QVariant / QString) + uint256 hash; + QString hashQStr = selection.at(0).data(TransactionTableModel::TxHashRole).toString(); + hash.SetHex(hashQStr.toStdString()); + + // Abandon the wallet transaction over the walletModel + model->abandonTransaction(hash); + + // Update the table + model->getTransactionTableModel()->updateTransaction(hashQStr, CT_UPDATED, false); +} + +void KevaView::bumpFee() +{ + if(!kevaView || !kevaView->selectionModel()) + return; + QModelIndexList selection = kevaView->selectionModel()->selectedRows(0); + + // get the hash from the TxHashRole (QVariant / QString) + uint256 hash; + QString hashQStr = selection.at(0).data(TransactionTableModel::TxHashRole).toString(); + hash.SetHex(hashQStr.toStdString()); + + // Bump tx fee over the walletModel + if (model->bumpFee(hash)) { + // Update the table + model->getTransactionTableModel()->updateTransaction(hashQStr, CT_UPDATED, true); + } +} + +void KevaView::copyAddress() +{ + GUIUtil::copyEntryData(kevaView, 0, TransactionTableModel::AddressRole); +} + +void KevaView::copyLabel() +{ + GUIUtil::copyEntryData(kevaView, 0, TransactionTableModel::LabelRole); +} + +void KevaView::copyAmount() +{ + GUIUtil::copyEntryData(kevaView, 0, TransactionTableModel::FormattedAmountRole); +} + +void KevaView::copyTxID() +{ + GUIUtil::copyEntryData(kevaView, 0, TransactionTableModel::TxIDRole); +} + +void KevaView::copyTxHex() +{ + GUIUtil::copyEntryData(kevaView, 0, TransactionTableModel::TxHexRole); +} + +void KevaView::copyTxPlainText() +{ + GUIUtil::copyEntryData(kevaView, 0, TransactionTableModel::TxPlainTextRole); +} + +void KevaView::editLabel() +{ + if(!kevaView->selectionModel() ||!model) + return; + QModelIndexList selection = kevaView->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 KevaView::showDetails() +{ + if(!kevaView->selectionModel()) + return; + QModelIndexList selection = kevaView->selectionModel()->selectedRows(); + if(!selection.isEmpty()) + { + TransactionDescDialog *dlg = new TransactionDescDialog(selection.at(0)); + dlg->setAttribute(Qt::WA_DeleteOnClose); + dlg->show(); + } +} + +void KevaView::openThirdPartyTxUrl(QString url) +{ + if(!kevaView || !kevaView->selectionModel()) + return; + QModelIndexList selection = kevaView->selectionModel()->selectedRows(0); + if(!selection.isEmpty()) + QDesktopServices::openUrl(QUrl::fromUserInput(url.replace("%s", selection.at(0).data(TransactionTableModel::TxHashRole).toString()))); +} + +QWidget *KevaView::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 KevaView::dateRangeChanged() +{ + if(!transactionProxyModel) + return; + transactionProxyModel->setDateRange( + QDateTime(dateFrom->date()), + QDateTime(dateTo->date()).addDays(1)); +} + +void KevaView::focusTransaction(const QModelIndex &idx) +{ + if(!transactionProxyModel) + return; + QModelIndex targetIdx = transactionProxyModel->mapFromSource(idx); + kevaView->scrollTo(targetIdx); + kevaView->setCurrentIndex(targetIdx); + kevaView->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 KevaView::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 KevaView::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)) + { + GUIUtil::copyEntryData(kevaView, 0, TransactionTableModel::TxPlainTextRole); + return true; + } + } + return QWidget::eventFilter(obj, event); +} + +// show/hide column Watch-only +void KevaView::updateWatchOnlyColumn(bool fHaveWatchOnly) +{ + watchOnlyWidget->setVisible(fHaveWatchOnly); + kevaView->setColumnHidden(TransactionTableModel::Watchonly, !fHaveWatchOnly); +} diff --git a/src/qt/kevaview.h b/src/qt/kevaview.h new file mode 100644 index 000000000..dfffeec2a --- /dev/null +++ b/src/qt/kevaview.h @@ -0,0 +1,122 @@ +// Copyright (c) 2011-2017 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_KevaView_H +#define BITCOIN_QT_KevaView_H + +#include + +#include +#include + +class PlatformStyle; +class TransactionFilterProxy; +class WalletModel; + +QT_BEGIN_NAMESPACE +class QComboBox; +class QDateTimeEdit; +class QFrame; +class QLineEdit; +class QMenu; +class QModelIndex; +class QSignalMapper; +class QTableView; +QT_END_NAMESPACE + +/** Widget showing the transaction list for a wallet, including a filter row. + Using the filter row, the user can view or export a subset of the transactions. + */ +class KevaView : public QWidget +{ + Q_OBJECT + +public: + explicit KevaView(const PlatformStyle *platformStyle, QWidget *parent = 0); + + void setModel(WalletModel *model); + + // Date ranges for filter + enum DateEnum + { + All, + Today, + ThisWeek, + ThisMonth, + LastMonth, + ThisYear, + Range + }; + + enum ColumnWidths { + STATUS_COLUMN_WIDTH = 30, + WATCHONLY_COLUMN_WIDTH = 23, + DATE_COLUMN_WIDTH = 120, + TYPE_COLUMN_WIDTH = 113, + AMOUNT_MINIMUM_COLUMN_WIDTH = 120, + MINIMUM_COLUMN_WIDTH = 23 + }; + +private: + WalletModel *model; + TransactionFilterProxy *transactionProxyModel; + QTableView *kevaView; + + QComboBox *dateWidget; + QComboBox *typeWidget; + QComboBox *watchOnlyWidget; + QLineEdit *search_widget; + QLineEdit *amountWidget; + + QMenu *contextMenu; + QSignalMapper *mapperThirdPartyTxUrls; + + QFrame *dateRangeWidget; + QDateTimeEdit *dateFrom; + QDateTimeEdit *dateTo; + QAction *abandonAction; + QAction *bumpFeeAction; + + QWidget *createDateRangeWidget(); + + GUIUtil::TableViewLastColumnResizingFixer *columnResizingFixer; + + virtual void resizeEvent(QResizeEvent* event); + + bool eventFilter(QObject *obj, QEvent *event); + +private Q_SLOTS: + void contextualMenu(const QPoint &); + void dateRangeChanged(); + void showDetails(); + void copyAddress(); + void editLabel(); + void copyLabel(); + void copyAmount(); + void copyTxID(); + void copyTxHex(); + void copyTxPlainText(); + void openThirdPartyTxUrl(QString url); + void updateWatchOnlyColumn(bool fHaveWatchOnly); + void abandonTx(); + void bumpFee(); + +Q_SIGNALS: + void doubleClicked(const QModelIndex&); + + /** Fired when a message should be reported to the user */ + void message(const QString &title, const QString &message, unsigned int style); + +public Q_SLOTS: + void chooseDate(int idx); + void chooseType(int idx); + void chooseWatchonly(int idx); + void changedAmount(); + void changedSearch(); + void exportClicked(); + void focusTransaction(const QModelIndex&); + +}; + +#endif // BITCOIN_QT_KevaView_H diff --git a/src/qt/locale/bitcoin_en.ts b/src/qt/locale/bitcoin_en.ts index 647e8f566..86ae7aa2c 100644 --- a/src/qt/locale/bitcoin_en.ts +++ b/src/qt/locale/bitcoin_en.ts @@ -85,7 +85,7 @@ - These are your Kevacoin addresses for receiving payments. It is recommended to use a new receiving address for each transaction. + These are your Kevaoin addresses for receiving payments. It is recommended to use a new receiving address for each transaction. @@ -304,17 +304,17 @@ BitcoinGUI - + Sign &message... Sign &message... - + Synchronizing with network... Synchronizing with network... - + &Overview &Overview @@ -338,6 +338,16 @@ Browse transaction history Browse transaction history + + + &Keva + + + + + Keva related operations + + E&xit @@ -409,7 +419,7 @@ - + Click to disable network activity. @@ -434,12 +444,12 @@ Reindexing blocks on disk... - + Send coins to a Kevacoin address Send coins to a Kevacoin address - + Backup wallet to another location Backup wallet to another location @@ -464,12 +474,12 @@ &Verify message... - + Kevacoin Kevacoin - + Wallet Wallet @@ -484,7 +494,7 @@ &Receive - + &Show / Hide &Show / Hide @@ -529,12 +539,12 @@ Tabs toolbar - + Request payments (generates QR codes and kevacoin: URIs) - + Show the list of used sending addresses and labels @@ -554,7 +564,7 @@ - + %n active connection(s) to Kevacoin network %n active connection to Kevacoin network @@ -615,12 +625,12 @@ Up to date - + Show the %1 help message to get a list with possible Kevacoin command-line options - + %1 client @@ -635,7 +645,7 @@ Catching up... - + Date: %1 @@ -1671,7 +1681,7 @@ PaymentServer - + @@ -1950,7 +1960,7 @@ - + %1 didn't yet exit safely... @@ -2623,7 +2633,7 @@ SendCoinsDialog - + Send Coins Send Coins @@ -2754,7 +2764,7 @@ - + Send to multiple recipients at once Send to multiple recipients at once @@ -2769,7 +2779,7 @@ - + Dust: @@ -2779,17 +2789,7 @@ - - Enable Replace-By-Fee - - - - - With Replace-By-Fee (BIP-125) you can increase a transaction's fee after it is sent. Without this, a higher fee may be recommended to compensate for increased transaction delay risk. - - - - + Clear &All Clear &All @@ -2809,7 +2809,7 @@ S&end - + Copy quantity @@ -2849,7 +2849,7 @@ - + @@ -2877,22 +2877,12 @@ - - You can increase the fee later (signals Replace-By-Fee, BIP-125). - - - - - Not signalling Replace-By-Fee, BIP-125. - - - - + Confirm send coins - + The recipient address is not valid. Please recheck. @@ -2942,7 +2932,7 @@ - + Estimated to begin confirmation within %n block(s). Estimated to begin confirmation within %n block. @@ -3329,11 +3319,6 @@ conflicted with a transaction with %1 confirmations - - - %1/offline - - 0/unconfirmed, %1 @@ -3370,20 +3355,7 @@ - - , has not been successfully broadcast yet - - - - - , broadcast through %n node(s) - - , broadcast through %n node - , broadcast through %n nodes - - - - + Date Date @@ -3593,11 +3565,6 @@ Open until %1 - - - Offline - - Unconfirmed @@ -3628,11 +3595,6 @@ Immature (%1 confirmations, will be available after %2) - - - This block was not received by any other nodes and will probably not be accepted! - - Generated but not accepted @@ -3674,7 +3636,7 @@ - + (no label) @@ -3927,7 +3889,7 @@ WalletModel - + Send Coins Send Coins @@ -4025,7 +3987,7 @@ bitcoin-core - + Options: Options: @@ -4035,17 +3997,17 @@ Specify data directory - + Connect to a node to retrieve peer addresses, and disconnect Connect to a node to retrieve peer addresses, and disconnect - + Specify your own public address Specify your own public address - + Accept command line and JSON-RPC commands Accept command line and JSON-RPC commands @@ -4085,7 +4047,7 @@ - + Pruning blockstore... @@ -4100,7 +4062,7 @@ - + Kevacoin Core Kevacoin Core @@ -4200,12 +4162,7 @@ - - Reduce storage requirements by enabling pruning (deleting) of old blocks. This allows the pruneblockchain RPC to be called to delete specific blocks, and enables automatic pruning of old blocks if a target size in MiB is provided. This mode is incompatible with -txindex and -rescan. Warning: Reverting this setting requires re-downloading the entire blockchain. (default: 0 = disable pruning blocks, 1 = allow manual pruning via RPC, >%u = automatically prune block files to stay under the specified target size in MiB) - - - - + Set lowest fee rate (in %s/kB) for transactions to be included in block creation. (default: %s) @@ -4449,6 +4406,21 @@ Initialization sanity check failed. %s is shutting down. + + + Input tx is not a keva operation + + + + + Input tx is not mine + + + + + Input tx not found in wallet + + Invalid amount for -%s=<amount>: '%s' @@ -4625,7 +4597,7 @@ - + Allow JSON-RPC connections from specified source. Valid for <ip> are a single IP (e.g. 1.2.3.4), a network/netmask (e.g. 1.2.3.4/255.255.255.0) or a network/CIDR (e.g. 1.2.3.4/24). This option can be specified multiple times @@ -4740,7 +4712,7 @@ Information - + Invalid -onion address or hostname: '%s' @@ -4925,7 +4897,7 @@ Password for JSON-RPC connections - + Execute command when the best block changes (%s in cmd is replaced by block hash) Execute command when the best block changes (%s in cmd is replaced by block hash) @@ -5000,7 +4972,12 @@ - + + Reduce storage requirements by enabling pruning (deleting) of old blocks. This allows the pruneblockchain RPC to be called to delete specific blocks, and enables automatic pruning of old blocks if a target size in MiB is provided. This mode is incompatible with -txindex and -rescan. Warning: Reverting this setting requires re-downloading the entire blockchain. (default: 0 = disable pruning blocks, 1 = allow manual pruning via RPC, >=%u = automatically prune block files to stay under the specified target size in MiB) + + + + Sets the serialization of raw transaction or block hex returned in non-verbose mode, non-segwit(0) or segwit(1) (default: %d) @@ -5115,7 +5092,7 @@ - + Keypool ran out, please call keypoolrefill first @@ -5260,17 +5237,17 @@ Loading wallet... - + Cannot downgrade wallet Cannot downgrade wallet - + Rescanning... Rescanning... - + Done loading Done loading @@ -5280,4 +5257,4 @@ Error - \ No newline at end of file + diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index 7a4ffea03..ae3ac8f71 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include diff --git a/src/qt/walletframe.cpp b/src/qt/walletframe.cpp index c0b9d0426..ad84801fb 100644 --- a/src/qt/walletframe.cpp +++ b/src/qt/walletframe.cpp @@ -124,6 +124,13 @@ void WalletFrame::gotoHistoryPage() i.value()->gotoHistoryPage(); } +void WalletFrame::gotoKevaPage() +{ + QMap::const_iterator i; + for (i = mapWalletViews.constBegin(); i != mapWalletViews.constEnd(); ++i) + i.value()->gotoKevaPage(); +} + void WalletFrame::gotoReceiveCoinsPage() { QMap::const_iterator i; diff --git a/src/qt/walletframe.h b/src/qt/walletframe.h index 42ce69fea..fbc648b2f 100644 --- a/src/qt/walletframe.h +++ b/src/qt/walletframe.h @@ -66,6 +66,8 @@ public Q_SLOTS: void gotoOverviewPage(); /** Switch to history (transactions) page */ void gotoHistoryPage(); + /** Switch to Keva page */ + void gotoKevaPage(); /** Switch to receive coins page */ void gotoReceiveCoinsPage(); /** Switch to send coins page */ diff --git a/src/qt/walletview.cpp b/src/qt/walletview.cpp index 7eced9289..cc30ba29d 100644 --- a/src/qt/walletview.cpp +++ b/src/qt/walletview.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -53,6 +54,25 @@ WalletView::WalletView(const PlatformStyle *_platformStyle, QWidget *parent): vbox->addLayout(hbox_buttons); transactionsPage->setLayout(vbox); +#if 1 +{ + kevaPage = new QWidget(this); + QVBoxLayout *vbox = new QVBoxLayout(); + QHBoxLayout *hbox_buttons = new QHBoxLayout(); + kevaView = new KevaView(platformStyle, this); + vbox->addWidget(kevaView); + QPushButton *exportButton = new QPushButton(tr("&Export"), this); + exportButton->setToolTip(tr("Export the data in the current tab to a file")); + if (platformStyle->getImagesOnButtons()) { + exportButton->setIcon(platformStyle->SingleColorIcon(":/icons/export")); + } + hbox_buttons->addStretch(); + hbox_buttons->addWidget(exportButton); + vbox->addLayout(hbox_buttons); + kevaPage->setLayout(vbox); +} +#endif + receiveCoinsPage = new ReceiveCoinsDialog(platformStyle); sendCoinsPage = new SendCoinsDialog(platformStyle); @@ -61,6 +81,7 @@ WalletView::WalletView(const PlatformStyle *_platformStyle, QWidget *parent): addWidget(overviewPage); addWidget(transactionsPage); + addWidget(kevaPage); addWidget(receiveCoinsPage); addWidget(sendCoinsPage); @@ -100,7 +121,7 @@ void WalletView::setBitcoinGUI(BitcoinGUI *gui) // Pass through transaction notifications connect(this, SIGNAL(incomingTransaction(QString,int,CAmount,QString,QString,QString)), gui, SLOT(incomingTransaction(QString,int,CAmount,QString,QString,QString))); - // Connect HD enabled state signal + // Connect HD enabled state signal connect(this, SIGNAL(hdEnabledStatusChanged(int)), gui, SLOT(setHDStatus(int))); } } @@ -119,6 +140,7 @@ void WalletView::setWalletModel(WalletModel *_walletModel) // Put transaction list in tabs transactionView->setModel(_walletModel); + kevaView->setModel(_walletModel); overviewPage->setWalletModel(_walletModel); receiveCoinsPage->setModel(_walletModel); sendCoinsPage->setModel(_walletModel); @@ -179,6 +201,11 @@ void WalletView::gotoHistoryPage() setCurrentWidget(transactionsPage); } +void WalletView::gotoKevaPage() +{ + setCurrentWidget(kevaPage); +} + void WalletView::gotoReceiveCoinsPage() { setCurrentWidget(receiveCoinsPage); diff --git a/src/qt/walletview.h b/src/qt/walletview.h index 30d68e4ef..784dbefd6 100644 --- a/src/qt/walletview.h +++ b/src/qt/walletview.h @@ -17,6 +17,7 @@ class ReceiveCoinsDialog; class SendCoinsDialog; class SendCoinsRecipient; class TransactionView; +class KevaView; class WalletModel; class AddressBookPage; @@ -60,6 +61,7 @@ private: OverviewPage *overviewPage; QWidget *transactionsPage; + QWidget *kevaPage; ReceiveCoinsDialog *receiveCoinsPage; SendCoinsDialog *sendCoinsPage; AddressBookPage *usedSendingAddressesPage; @@ -67,6 +69,8 @@ private: TransactionView *transactionView; + KevaView *kevaView; + QProgressDialog *progressDialog; const PlatformStyle *platformStyle; @@ -75,6 +79,8 @@ public Q_SLOTS: void gotoOverviewPage(); /** Switch to history (transactions) page */ void gotoHistoryPage(); + /** Switch to Keva page */ + void gotoKevaPage(); /** Switch to receive coins page */ void gotoReceiveCoinsPage(); /** Switch to send coins page */