transactionview.cpp raw

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