transactionview.cpp raw

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