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