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