receivecoinsdialog.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 <wallet/wallet.h>
   6  
   7  #include <qt/receivecoinsdialog.h>
   8  #include <qt/forms/ui_receivecoinsdialog.h>
   9  
  10  #include <qt/addresstablemodel.h>
  11  #include <qt/guiutil.h>
  12  #include <qt/optionsmodel.h>
  13  #include <qt/platformstyle.h>
  14  #include <qt/receiverequestdialog.h>
  15  #include <qt/recentrequeststablemodel.h>
  16  #include <qt/walletmodel.h>
  17  
  18  #include <QAction>
  19  #include <QCursor>
  20  #include <QMessageBox>
  21  #include <QScrollBar>
  22  #include <QSettings>
  23  #include <QTextDocument>
  24  
  25  #include <ranges>
  26  
  27  ReceiveCoinsDialog::ReceiveCoinsDialog(const PlatformStyle *_platformStyle, QWidget *parent) :
  28      QDialog(parent, GUIUtil::dialog_flags),
  29      ui(new Ui::ReceiveCoinsDialog),
  30      platformStyle(_platformStyle)
  31  {
  32      ui->setupUi(this);
  33  
  34      m_sort_proxy = new QSortFilterProxyModel(this);
  35      m_sort_proxy->setSortRole(Qt::UserRole);
  36  
  37      if (!_platformStyle->getImagesOnButtons()) {
  38          ui->clearButton->setIcon(QIcon());
  39          ui->receiveButton->setIcon(QIcon());
  40          ui->showRequestButton->setIcon(QIcon());
  41          ui->removeRequestButton->setIcon(QIcon());
  42      } else {
  43          ui->clearButton->setIcon(_platformStyle->SingleColorIcon(":/icons/remove"));
  44          ui->receiveButton->setIcon(_platformStyle->SingleColorIcon(":/icons/receiving_addresses"));
  45          ui->showRequestButton->setIcon(_platformStyle->SingleColorIcon(":/icons/eye"));
  46          ui->removeRequestButton->setIcon(_platformStyle->SingleColorIcon(":/icons/remove"));
  47      }
  48  
  49      // context menu
  50      contextMenu = new QMenu(this);
  51      contextMenu->addAction(tr("Copy &URI"), this, &ReceiveCoinsDialog::copyURI);
  52      contextMenu->addAction(tr("&Copy address"), this, &ReceiveCoinsDialog::copyAddress);
  53      copyLabelAction = contextMenu->addAction(tr("Copy &label"), this, &ReceiveCoinsDialog::copyLabel);
  54      copyMessageAction = contextMenu->addAction(tr("Copy &message"), this, &ReceiveCoinsDialog::copyMessage);
  55      copyAmountAction = contextMenu->addAction(tr("Copy &amount"), this, &ReceiveCoinsDialog::copyAmount);
  56      connect(ui->recentRequestsView, &QWidget::customContextMenuRequested, this, &ReceiveCoinsDialog::showMenu);
  57  
  58      connect(ui->clearButton, &QPushButton::clicked, this, &ReceiveCoinsDialog::clear);
  59  }
  60  
  61  void ReceiveCoinsDialog::setModel(WalletModel *_model)
  62  {
  63      this->model = _model;
  64  
  65      if(_model && _model->getOptionsModel())
  66      {
  67          connect(_model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &ReceiveCoinsDialog::updateDisplayUnit);
  68          connect(_model->getOptionsModel(), &OptionsModel::fontForMoneyChanged, this, &ReceiveCoinsDialog::updateFontForMoney);
  69          updateDisplayUnit();
  70  
  71          QTableView* tableView = ui->recentRequestsView;
  72  
  73          tableView->verticalHeader()->hide();
  74          tableView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
  75          tableView->setModel(m_sort_proxy);
  76          m_sort_proxy->setSourceModel(_model->getRecentRequestsTableModel());
  77          tableView->sortByColumn(RecentRequestsTableModel::Date, Qt::DescendingOrder);
  78  
  79          tableView->setAlternatingRowColors(true);
  80          tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
  81          tableView->setSelectionMode(QAbstractItemView::ExtendedSelection);
  82  
  83          QSettings settings;
  84          if (!tableView->horizontalHeader()->restoreState(settings.value("RecentRequestsViewHeaderState").toByteArray())) {
  85              tableView->setColumnWidth(RecentRequestsTableModel::Date, DATE_COLUMN_WIDTH);
  86              tableView->setColumnWidth(RecentRequestsTableModel::Label, LABEL_COLUMN_WIDTH);
  87              tableView->setColumnWidth(RecentRequestsTableModel::Amount, AMOUNT_MINIMUM_COLUMN_WIDTH);
  88          }
  89  
  90          connect(tableView->selectionModel(),
  91              &QItemSelectionModel::selectionChanged, this,
  92              &ReceiveCoinsDialog::recentRequestsView_selectionChanged);
  93          // Last 2 columns are set by the columnResizingFixer, when the table geometry is ready.
  94          columnResizingFixer = new GUIUtil::TableViewLastColumnResizingFixer(tableView, AMOUNT_MINIMUM_COLUMN_WIDTH, DATE_COLUMN_WIDTH, this);
  95  
  96          // Populate address type dropdown and select default
  97          auto add_address_type = [&](OutputType type) {
  98              const auto [text, tooltip] = GetOutputTypeDescription(type);
  99              const auto index = ui->addressType->count();
 100              ui->addressType->addItem(text, (int) type);
 101              ui->addressType->setItemData(index, tooltip, Qt::ToolTipRole);
 102              if (model->wallet().getDefaultAddressType() == type) ui->addressType->setCurrentIndex(index);
 103          };
 104          add_address_type(OutputType::LEGACY);
 105          add_address_type(OutputType::P2SH_SEGWIT);
 106          add_address_type(OutputType::BECH32);
 107          if (model->wallet().taprootEnabled()) {
 108              add_address_type(OutputType::BECH32M);
 109          }
 110  
 111          connect(_model->getOptionsModel(), &OptionsModel::addresstypeChanged, [this](const OutputType type) {
 112              const int index = ui->addressType->findData((int) type);
 113              if (index != -1) ui->addressType->setCurrentIndex(index);
 114          });
 115  
 116          // Set the button to be enabled or disabled based on whether the wallet can give out new addresses.
 117          ui->receiveButton->setEnabled(model->wallet().canGetAddresses());
 118  
 119          // Enable/disable the receive button if the wallet is now able/unable to give out new addresses.
 120          connect(model, &WalletModel::canGetAddressesChanged, [this] {
 121              ui->receiveButton->setEnabled(model->wallet().canGetAddresses());
 122          });
 123      }
 124  }
 125  
 126  ReceiveCoinsDialog::~ReceiveCoinsDialog()
 127  {
 128      QSettings settings;
 129      settings.setValue("RecentRequestsViewHeaderState", ui->recentRequestsView->horizontalHeader()->saveState());
 130      delete ui;
 131  }
 132  
 133  void ReceiveCoinsDialog::clear()
 134  {
 135      ui->reqAmount->clear();
 136      ui->reqLabel->setText("");
 137      ui->reqMessage->setText("");
 138      updateDisplayUnit();
 139  }
 140  
 141  void ReceiveCoinsDialog::reject()
 142  {
 143      clear();
 144  }
 145  
 146  void ReceiveCoinsDialog::accept()
 147  {
 148      clear();
 149  }
 150  
 151  void ReceiveCoinsDialog::updateDisplayUnit()
 152  {
 153      if(model && model->getOptionsModel())
 154      {
 155          ui->reqAmount->setDisplayUnit(model->getOptionsModel()->getDisplayUnit());
 156          updateFontForMoney();
 157      }
 158  }
 159  
 160  void ReceiveCoinsDialog::updateFontForMoney()
 161  {
 162      if(model && model->getOptionsModel())
 163      {
 164          const LimenkaUnit display_unit = model->getOptionsModel()->getDisplayUnit();
 165          const QFont font_for_money = model->getOptionsModel()->getFontForMoney(display_unit);
 166          ui->reqAmount->setFontForMoney(font_for_money);
 167      }
 168  }
 169  
 170  void ReceiveCoinsDialog::on_receiveButton_clicked()
 171  {
 172      if(!model || !model->getOptionsModel() || !model->getAddressTableModel() || !model->getRecentRequestsTableModel())
 173          return;
 174  
 175      QString address;
 176      QString label = ui->reqLabel->text();
 177      /* Generate new receiving address */
 178      const OutputType address_type = (OutputType)ui->addressType->currentData().toInt();
 179      address = model->getAddressTableModel()->addRow(AddressTableModel::Receive, label, "", address_type);
 180  
 181      switch(model->getAddressTableModel()->getEditStatus())
 182      {
 183      case AddressTableModel::EditStatus::OK: {
 184          // Success
 185          SendCoinsRecipient info(address, label,
 186              ui->reqAmount->value(), ui->reqMessage->text());
 187          ReceiveRequestDialog *dialog = new ReceiveRequestDialog(this);
 188          dialog->setAttribute(Qt::WA_DeleteOnClose);
 189          dialog->setModel(model);
 190          dialog->setInfo(info);
 191          dialog->show();
 192  
 193          /* Store request for later reference */
 194          model->getRecentRequestsTableModel()->addNewRequest(info);
 195          break;
 196      }
 197      case AddressTableModel::EditStatus::WALLET_UNLOCK_FAILURE:
 198          QMessageBox::critical(this, windowTitle(),
 199              tr("Could not unlock wallet."),
 200              QMessageBox::Ok, QMessageBox::Ok);
 201          break;
 202      case AddressTableModel::EditStatus::KEY_GENERATION_FAILURE:
 203          QMessageBox::critical(this, windowTitle(),
 204              tr("Could not generate new %1 address").arg(QString::fromStdString(FormatOutputType(address_type))),
 205              QMessageBox::Ok, QMessageBox::Ok);
 206          break;
 207      // These aren't valid return values for our action
 208      case AddressTableModel::EditStatus::INVALID_ADDRESS:
 209      case AddressTableModel::EditStatus::DUPLICATE_ADDRESS:
 210      case AddressTableModel::EditStatus::NO_CHANGES:
 211          assert(false);
 212      }
 213      clear();
 214  }
 215  
 216  void ReceiveCoinsDialog::on_recentRequestsView_doubleClicked(const QModelIndex &index)
 217  {
 218      QModelIndexList selection = SelectedRows();
 219      if (!selection.isEmpty() && selection.at(0).isValid()) {
 220          ShowReceiveRequestDialogForItem(selection.at(0));
 221      }
 222  }
 223  
 224  void ReceiveCoinsDialog::recentRequestsView_selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
 225  {
 226      // Enable Show/Remove buttons only if anything is selected.
 227      bool enable = !ui->recentRequestsView->selectionModel()->selectedRows().isEmpty();
 228      ui->showRequestButton->setEnabled(enable);
 229      ui->removeRequestButton->setEnabled(enable);
 230  }
 231  
 232  void ReceiveCoinsDialog::on_showRequestButton_clicked()
 233  {
 234      QModelIndexList selection = SelectedRows();
 235  
 236      for (const QModelIndex& index : selection) {
 237          ShowReceiveRequestDialogForItem(index);
 238      }
 239  }
 240  
 241  void ReceiveCoinsDialog::on_removeRequestButton_clicked()
 242  {
 243      QModelIndexList selection = SelectedRows();
 244      if(selection.empty())
 245          return;
 246  
 247      // Collect row indices in a set (sorted) and pass in reverse order to removeRows
 248      // to avoid having to keep track of changed source indices after each removal
 249      std::set<int> row_indices;
 250      for (const QModelIndex& ind : selection) {
 251          row_indices.insert(ind.row());
 252      }
 253  
 254      for (auto row_ind : row_indices | std::views::reverse) {
 255          model->getRecentRequestsTableModel()->removeRows(row_ind, 1);
 256      }
 257  }
 258  
 259  // We override the virtual resizeEvent of the QWidget to adjust tables column
 260  // sizes as the tables width is proportional to the dialogs width.
 261  void ReceiveCoinsDialog::resizeEvent(QResizeEvent *event)
 262  {
 263      QWidget::resizeEvent(event);
 264      columnResizingFixer->stretchColumnWidth(RecentRequestsTableModel::Message);
 265  }
 266  
 267  QModelIndexList ReceiveCoinsDialog::SelectedRows()
 268  {
 269      if(!model || !model->getRecentRequestsTableModel() || !ui->recentRequestsView->selectionModel())
 270          return QModelIndexList();
 271      QModelIndexList selection = ui->recentRequestsView->selectionModel()->selectedRows();
 272      QModelIndexList source_mapped;
 273      for (auto row : selection) {
 274          source_mapped.append(m_sort_proxy->mapToSource(row));
 275      }
 276  
 277      return source_mapped;
 278  }
 279  
 280  // copy column of selected row to clipboard
 281  void ReceiveCoinsDialog::copyColumnToClipboard(int column)
 282  {
 283      const QModelIndexList sel = SelectedRows();
 284      if (sel.isEmpty()) {
 285          return;
 286      }
 287  
 288      const RecentRequestsTableModel* const submodel = model->getRecentRequestsTableModel();
 289      QString column_value;
 290      for (int sel_ind = 0; sel_ind < sel.size(); ++sel_ind) {
 291          if (!sel.at(sel_ind).isValid()) {
 292              continue;
 293          }
 294          column_value += submodel->index(sel.at(sel_ind).row(), column).data(Qt::EditRole).toString();
 295          if (sel_ind < sel.size() - 1) {
 296              column_value += QString("\n");
 297          }
 298      }
 299      GUIUtil::setClipboard(column_value);
 300  }
 301  
 302  void ReceiveCoinsDialog::ShowReceiveRequestDialogForItem(const QModelIndex& index)
 303  {
 304      if (!index.isValid()) {
 305          return;
 306      }
 307      const RecentRequestsTableModel* submodel = model->getRecentRequestsTableModel();
 308      ReceiveRequestDialog* dialog = new ReceiveRequestDialog(this);
 309      dialog->setModel(model);
 310      dialog->setInfo(submodel->entry(index.row()).recipient);
 311      dialog->setAttribute(Qt::WA_DeleteOnClose);
 312      dialog->show();
 313  }
 314  
 315  // context menu
 316  void ReceiveCoinsDialog::showMenu(const QPoint &point)
 317  {
 318      const QModelIndexList sel = SelectedRows();
 319      if (sel.isEmpty()) {
 320          return;
 321      }
 322  
 323      if (sel.size() == 1 && sel.at(0).isValid()) {
 324      // disable context menu actions when appropriate
 325      const RecentRequestsTableModel* const submodel = model->getRecentRequestsTableModel();
 326          const RecentRequestEntry& req = submodel->entry(sel.at(0).row());
 327      copyLabelAction->setDisabled(req.recipient.label.isEmpty());
 328      copyMessageAction->setDisabled(req.recipient.message.isEmpty());
 329      copyAmountAction->setDisabled(req.recipient.amount == 0);
 330      } else if (sel.size() > 1) {
 331          // multiple selection
 332  
 333          copyLabelAction->setDisabled(true);
 334          copyMessageAction->setDisabled(true);
 335          copyAmountAction->setDisabled(true);
 336  
 337          // disable context menu actions when appropriate
 338          const RecentRequestsTableModel* const submodel = model->getRecentRequestsTableModel();
 339  
 340          for (auto selection : sel) {
 341              if (!selection.isValid()) {
 342                  continue;
 343              }
 344              const RecentRequestEntry& req = submodel->entry(selection.row());
 345              if (!req.recipient.label.isEmpty()) {
 346                  copyLabelAction->setDisabled(false);
 347              }
 348              if (!req.recipient.message.isEmpty()) {
 349                  copyMessageAction->setDisabled(false);
 350              }
 351              if (req.recipient.amount != 0) {
 352                  copyAmountAction->setDisabled(false);
 353              }
 354          }
 355      }
 356  
 357      contextMenu->exec(QCursor::pos());
 358  }
 359  
 360  // context menu action: copy URI
 361  void ReceiveCoinsDialog::copyURI()
 362  {
 363      const QModelIndexList sel = SelectedRows();
 364      if (sel.isEmpty()) {
 365          return;
 366      }
 367  
 368      const RecentRequestsTableModel * const submodel = model->getRecentRequestsTableModel();
 369      QString uri;
 370      for (int sel_ind = 0; sel_ind < sel.size(); ++sel_ind) {
 371          if (!sel.at(sel_ind).isValid()) {
 372              continue;
 373          }
 374          const RecentRequestEntry& req = submodel->entry(sel.at(sel_ind).row());
 375          uri += GUIUtil::formatLimenkaURI(req.recipient);
 376          if (sel_ind < sel.size() - 1) {
 377              uri += QString("\n");
 378          }
 379      }
 380      GUIUtil::setClipboard(uri);
 381  }
 382  
 383  // context menu action: copy address
 384  void ReceiveCoinsDialog::copyAddress()
 385  {
 386      const QModelIndexList sel = SelectedRows();
 387      if (sel.isEmpty()) {
 388          return;
 389      }
 390  
 391      const RecentRequestsTableModel* const submodel = model->getRecentRequestsTableModel();
 392      QString address;
 393      for (int sel_ind = 0; sel_ind < sel.size(); ++sel_ind) {
 394          if (!sel.at(sel_ind).isValid()) {
 395              continue;
 396          }
 397          const RecentRequestEntry& req = submodel->entry(sel.at(sel_ind).row());
 398          address += req.recipient.address;
 399          if (sel_ind < sel.size() - 1) {
 400              address += QString("\n");
 401          }
 402      }
 403      GUIUtil::setClipboard(address);
 404  }
 405  
 406  // context menu action: copy label
 407  void ReceiveCoinsDialog::copyLabel()
 408  {
 409      copyColumnToClipboard(RecentRequestsTableModel::Label);
 410  }
 411  
 412  // context menu action: copy message
 413  void ReceiveCoinsDialog::copyMessage()
 414  {
 415      copyColumnToClipboard(RecentRequestsTableModel::Message);
 416  }
 417  
 418  // context menu action: copy amount
 419  void ReceiveCoinsDialog::copyAmount()
 420  {
 421      copyColumnToClipboard(RecentRequestsTableModel::Amount);
 422  }
 423