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 }