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