/ src / qt / addressbookpage.cpp
addressbookpage.cpp
  1  // Copyright (c) 2011-2022 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/addressbookpage.h>
  6  #include <qt/forms/ui_addressbookpage.h>
  7  
  8  #include <qt/addresstablemodel.h>
  9  #include <qt/csvmodelwriter.h>
 10  #include <qt/editaddressdialog.h>
 11  #include <qt/guiutil.h>
 12  #include <qt/platformstyle.h>
 13  
 14  #include <QIcon>
 15  #include <QMenu>
 16  #include <QMessageBox>
 17  #include <QSortFilterProxyModel>
 18  #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
 19  #include <QRegularExpression>
 20  #else
 21  #include <QRegExp>
 22  #endif
 23  
 24  class AddressBookSortFilterProxyModel final : public QSortFilterProxyModel
 25  {
 26      const QString m_type;
 27  
 28  public:
 29      AddressBookSortFilterProxyModel(const QString& type, QObject* parent)
 30          : QSortFilterProxyModel(parent)
 31          , m_type(type)
 32      {
 33          setDynamicSortFilter(true);
 34          setFilterCaseSensitivity(Qt::CaseInsensitive);
 35          setSortCaseSensitivity(Qt::CaseInsensitive);
 36      }
 37  
 38  protected:
 39      bool filterAcceptsRow(int row, const QModelIndex& parent) const override
 40      {
 41          auto model = sourceModel();
 42          auto label = model->index(row, AddressTableModel::Label, parent);
 43  
 44          if (model->data(label, AddressTableModel::TypeRole).toString() != m_type) {
 45              return false;
 46          }
 47  
 48          auto address = model->index(row, AddressTableModel::Address, parent);
 49  
 50  #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
 51          const auto pattern = filterRegularExpression();
 52  #else
 53          const auto pattern = filterRegExp();
 54  #endif
 55          return (model->data(address).toString().contains(pattern) ||
 56                  model->data(label).toString().contains(pattern));
 57      }
 58  };
 59  
 60  AddressBookPage::AddressBookPage(const PlatformStyle *platformStyle, Mode _mode, Tabs _tab, QWidget *parent) :
 61      QDialog(parent, GUIUtil::dialog_flags),
 62      ui(new Ui::AddressBookPage),
 63      mode(_mode),
 64      tab(_tab)
 65  {
 66      ui->setupUi(this);
 67  
 68      if (!platformStyle->getImagesOnButtons()) {
 69          ui->newAddress->setIcon(QIcon());
 70          ui->copyAddress->setIcon(QIcon());
 71          ui->deleteAddress->setIcon(QIcon());
 72          ui->exportButton->setIcon(QIcon());
 73      } else {
 74          ui->newAddress->setIcon(platformStyle->SingleColorIcon(":/icons/add"));
 75          ui->copyAddress->setIcon(platformStyle->SingleColorIcon(":/icons/editcopy"));
 76          ui->deleteAddress->setIcon(platformStyle->SingleColorIcon(":/icons/remove"));
 77          ui->exportButton->setIcon(platformStyle->SingleColorIcon(":/icons/export"));
 78      }
 79  
 80      if (mode == ForSelection) {
 81          switch(tab)
 82          {
 83          case SendingTab: setWindowTitle(tr("Choose the address to send coins to")); break;
 84          case ReceivingTab: setWindowTitle(tr("Choose the address to receive coins with")); break;
 85          }
 86          connect(ui->tableView, &QTableView::doubleClicked, this, &QDialog::accept);
 87          ui->tableView->setEditTriggers(QAbstractItemView::NoEditTriggers);
 88          ui->tableView->setFocus();
 89          ui->closeButton->setText(tr("C&hoose"));
 90          ui->exportButton->hide();
 91      }
 92      switch(tab)
 93      {
 94      case SendingTab:
 95          ui->labelExplanation->setText(tr("These are your Bitcoin addresses for sending payments. Always check the amount and the receiving address before sending coins."));
 96          ui->deleteAddress->setVisible(true);
 97          ui->newAddress->setVisible(true);
 98          break;
 99      case ReceivingTab:
100          ui->labelExplanation->setText(tr("These are your Bitcoin addresses for receiving payments. Use the 'Create new receiving address' button in the receive tab to create new addresses.\nSigning is only possible with addresses of the type 'legacy'."));
101          ui->deleteAddress->setVisible(false);
102          ui->newAddress->setVisible(false);
103          break;
104      }
105  
106      // Build context menu
107      contextMenu = new QMenu(this);
108      contextMenu->addAction(tr("&Copy Address"), this, &AddressBookPage::on_copyAddress_clicked);
109      contextMenu->addAction(tr("Copy &Label"), this, &AddressBookPage::onCopyLabelAction);
110      contextMenu->addAction(tr("&Edit"), this, &AddressBookPage::onEditAction);
111  
112      if (tab == SendingTab) {
113          contextMenu->addAction(tr("&Delete"), this, &AddressBookPage::on_deleteAddress_clicked);
114      }
115  
116      connect(ui->tableView, &QWidget::customContextMenuRequested, this, &AddressBookPage::contextualMenu);
117      connect(ui->closeButton, &QPushButton::clicked, this, &QDialog::accept);
118  
119      GUIUtil::handleCloseWindowShortcut(this);
120  }
121  
122  AddressBookPage::~AddressBookPage()
123  {
124      delete ui;
125  }
126  
127  void AddressBookPage::setModel(AddressTableModel *_model)
128  {
129      this->model = _model;
130      if(!_model)
131          return;
132  
133      auto type = tab == ReceivingTab ? AddressTableModel::Receive : AddressTableModel::Send;
134      proxyModel = new AddressBookSortFilterProxyModel(type, this);
135      proxyModel->setSourceModel(_model);
136  
137      connect(ui->searchLineEdit, &QLineEdit::textChanged, proxyModel, &QSortFilterProxyModel::setFilterWildcard);
138  
139      ui->tableView->setModel(proxyModel);
140      ui->tableView->sortByColumn(0, Qt::AscendingOrder);
141  
142      // Set column widths
143      ui->tableView->horizontalHeader()->setSectionResizeMode(AddressTableModel::Label, QHeaderView::Stretch);
144      ui->tableView->horizontalHeader()->setSectionResizeMode(AddressTableModel::Address, QHeaderView::ResizeToContents);
145  
146      connect(ui->tableView->selectionModel(), &QItemSelectionModel::selectionChanged,
147          this, &AddressBookPage::selectionChanged);
148  
149      // Select row for newly created address
150      connect(_model, &AddressTableModel::rowsInserted, this, &AddressBookPage::selectNewAddress);
151  
152      selectionChanged();
153      this->updateWindowsTitleWithWalletName();
154  }
155  
156  void AddressBookPage::on_copyAddress_clicked()
157  {
158      GUIUtil::copyEntryData(ui->tableView, AddressTableModel::Address);
159  }
160  
161  void AddressBookPage::onCopyLabelAction()
162  {
163      GUIUtil::copyEntryData(ui->tableView, AddressTableModel::Label);
164  }
165  
166  void AddressBookPage::onEditAction()
167  {
168      if(!model)
169          return;
170  
171      if(!ui->tableView->selectionModel())
172          return;
173      QModelIndexList indexes = ui->tableView->selectionModel()->selectedRows();
174      if(indexes.isEmpty())
175          return;
176  
177      auto dlg = new EditAddressDialog(
178          tab == SendingTab ?
179          EditAddressDialog::EditSendingAddress :
180          EditAddressDialog::EditReceivingAddress, this);
181      dlg->setModel(model);
182      QModelIndex origIndex = proxyModel->mapToSource(indexes.at(0));
183      dlg->loadRow(origIndex.row());
184      GUIUtil::ShowModalDialogAsynchronously(dlg);
185  }
186  
187  void AddressBookPage::on_newAddress_clicked()
188  {
189      if(!model)
190          return;
191  
192      if (tab == ReceivingTab) {
193          return;
194      }
195  
196      EditAddressDialog dlg(EditAddressDialog::NewSendingAddress, this);
197      dlg.setModel(model);
198      if(dlg.exec())
199      {
200          newAddressToSelect = dlg.getAddress();
201      }
202  }
203  
204  void AddressBookPage::on_deleteAddress_clicked()
205  {
206      QTableView *table = ui->tableView;
207      if(!table->selectionModel())
208          return;
209  
210      QModelIndexList indexes = table->selectionModel()->selectedRows();
211      if(!indexes.isEmpty())
212      {
213          table->model()->removeRow(indexes.at(0).row());
214      }
215  }
216  
217  void AddressBookPage::selectionChanged()
218  {
219      // Set button states based on selected tab and selection
220      QTableView *table = ui->tableView;
221      if(!table->selectionModel())
222          return;
223  
224      if(table->selectionModel()->hasSelection())
225      {
226          switch(tab)
227          {
228          case SendingTab:
229              // In sending tab, allow deletion of selection
230              ui->deleteAddress->setEnabled(true);
231              ui->deleteAddress->setVisible(true);
232              break;
233          case ReceivingTab:
234              // Deleting receiving addresses, however, is not allowed
235              ui->deleteAddress->setEnabled(false);
236              ui->deleteAddress->setVisible(false);
237              break;
238          }
239          ui->copyAddress->setEnabled(true);
240      }
241      else
242      {
243          ui->deleteAddress->setEnabled(false);
244          ui->copyAddress->setEnabled(false);
245      }
246  }
247  
248  void AddressBookPage::done(int retval)
249  {
250      QTableView *table = ui->tableView;
251      if(!table->selectionModel() || !table->model())
252          return;
253  
254      // Figure out which address was selected, and return it
255      QModelIndexList indexes = table->selectionModel()->selectedRows(AddressTableModel::Address);
256  
257      for (const QModelIndex& index : indexes) {
258          QVariant address = table->model()->data(index);
259          returnValue = address.toString();
260      }
261  
262      if(returnValue.isEmpty())
263      {
264          // If no address entry selected, return rejected
265          retval = Rejected;
266      }
267  
268      QDialog::done(retval);
269  }
270  
271  void AddressBookPage::on_exportButton_clicked()
272  {
273      // CSV is currently the only supported format
274      QString filename = GUIUtil::getSaveFileName(this,
275          tr("Export Address List"), QString(),
276          /*: Expanded name of the CSV file format.
277              See: https://en.wikipedia.org/wiki/Comma-separated_values. */
278          tr("Comma separated file") + QLatin1String(" (*.csv)"), nullptr);
279  
280      if (filename.isNull())
281          return;
282  
283      CSVModelWriter writer(filename);
284  
285      // name, column, role
286      writer.setModel(proxyModel);
287      writer.addColumn("Label", AddressTableModel::Label, Qt::EditRole);
288      writer.addColumn("Address", AddressTableModel::Address, Qt::EditRole);
289  
290      if(!writer.write()) {
291          QMessageBox::critical(this, tr("Exporting Failed"),
292              /*: An error message. %1 is a stand-in argument for the name
293                  of the file we attempted to save to. */
294              tr("There was an error trying to save the address list to %1. Please try again.").arg(filename));
295      }
296  }
297  
298  void AddressBookPage::contextualMenu(const QPoint &point)
299  {
300      QModelIndex index = ui->tableView->indexAt(point);
301      if(index.isValid())
302      {
303          contextMenu->exec(QCursor::pos());
304      }
305  }
306  
307  void AddressBookPage::selectNewAddress(const QModelIndex &parent, int begin, int /*end*/)
308  {
309      QModelIndex idx = proxyModel->mapFromSource(model->index(begin, AddressTableModel::Address, parent));
310      if(idx.isValid() && (idx.data(Qt::EditRole).toString() == newAddressToSelect))
311      {
312          // Select row of newly created address, once
313          ui->tableView->setFocus();
314          ui->tableView->selectRow(idx.row());
315          newAddressToSelect.clear();
316      }
317  }
318  
319  void AddressBookPage::updateWindowsTitleWithWalletName()
320  {
321      const QString walletName = this->model->GetWalletDisplayName();
322  
323      if (mode == ForEditing) {
324          switch(tab)
325          {
326          case SendingTab: setWindowTitle(tr("Sending addresses - %1").arg(walletName)); break;
327          case ReceivingTab: setWindowTitle(tr("Receiving addresses - %1").arg(walletName)); break;
328          }
329      }
330  }