Merge coin control features
[novacoin.git] / src / qt / sendcoinsdialog.cpp
1 #include "sendcoinsdialog.h"
2 #include "ui_sendcoinsdialog.h"
3
4 #include "init.h"
5 #include "walletmodel.h"
6 #include "addresstablemodel.h"
7 #include "addressbookpage.h"
8
9 #include "bitcoinunits.h"
10 #include "addressbookpage.h"
11 #include "optionsmodel.h"
12 #include "sendcoinsentry.h"
13 #include "guiutil.h"
14 #include "askpassphrasedialog.h"
15
16 #include "coincontrol.h"
17 #include "coincontroldialog.h"
18
19 #include <QMessageBox>
20 #include <QLocale>
21 #include <QTextDocument>
22 #include <QScrollBar>
23 #include <QClipboard>
24
25 SendCoinsDialog::SendCoinsDialog(QWidget *parent) :
26     QDialog(parent),
27     ui(new Ui::SendCoinsDialog),
28     model(0)
29 {
30     ui->setupUi(this);
31
32 #ifdef Q_OS_MAC // Icons on push buttons are very uncommon on Mac
33     ui->addButton->setIcon(QIcon());
34     ui->clearButton->setIcon(QIcon());
35     ui->sendButton->setIcon(QIcon());
36 #endif
37
38 #if QT_VERSION >= 0x040700
39     /* Do not move this to the XML file, Qt before 4.7 will choke on it */
40     ui->lineEditCoinControlChange->setPlaceholderText(tr("Enter a NovaCoin address (e.g. 4Zo1ga6xuKuQ7JV7M9rGDoxdbYwV5zgQJ5)"));
41 #endif
42
43     addEntry();
44
45     connect(ui->addButton, SIGNAL(clicked()), this, SLOT(addEntry()));
46     connect(ui->clearButton, SIGNAL(clicked()), this, SLOT(clear()));
47
48     // Coin Control
49     ui->lineEditCoinControlChange->setFont(GUIUtil::bitcoinAddressFont());
50     connect(ui->pushButtonCoinControl, SIGNAL(clicked()), this, SLOT(coinControlButtonClicked()));
51     connect(ui->checkBoxCoinControlChange, SIGNAL(stateChanged(int)), this, SLOT(coinControlChangeChecked(int)));
52     connect(ui->lineEditCoinControlChange, SIGNAL(textEdited(const QString &)), this, SLOT(coinControlChangeEdited(const QString &)));
53
54     // Coin Control: clipboard actions
55     QAction *clipboardQuantityAction = new QAction(tr("Copy quantity"), this);
56     QAction *clipboardAmountAction = new QAction(tr("Copy amount"), this);
57     QAction *clipboardFeeAction = new QAction(tr("Copy fee"), this);
58     QAction *clipboardAfterFeeAction = new QAction(tr("Copy after fee"), this);
59     QAction *clipboardBytesAction = new QAction(tr("Copy bytes"), this);
60     QAction *clipboardPriorityAction = new QAction(tr("Copy priority"), this);
61     QAction *clipboardLowOutputAction = new QAction(tr("Copy low output"), this);
62     QAction *clipboardChangeAction = new QAction(tr("Copy change"), this);
63     connect(clipboardQuantityAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardQuantity()));
64     connect(clipboardAmountAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardAmount()));
65     connect(clipboardFeeAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardFee()));
66     connect(clipboardAfterFeeAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardAfterFee()));
67     connect(clipboardBytesAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardBytes()));
68     connect(clipboardPriorityAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardPriority()));
69     connect(clipboardLowOutputAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardLowOutput()));
70     connect(clipboardChangeAction, SIGNAL(triggered()), this, SLOT(coinControlClipboardChange()));
71     ui->labelCoinControlQuantity->addAction(clipboardQuantityAction);
72     ui->labelCoinControlAmount->addAction(clipboardAmountAction);
73     ui->labelCoinControlFee->addAction(clipboardFeeAction);
74     ui->labelCoinControlAfterFee->addAction(clipboardAfterFeeAction);
75     ui->labelCoinControlBytes->addAction(clipboardBytesAction);
76     ui->labelCoinControlPriority->addAction(clipboardPriorityAction);
77     ui->labelCoinControlLowOutput->addAction(clipboardLowOutputAction);
78     ui->labelCoinControlChange->addAction(clipboardChangeAction);
79
80     fNewRecipientAllowed = true;
81 }
82
83 void SendCoinsDialog::setModel(WalletModel *model)
84 {
85     this->model = model;
86
87     for(int i = 0; i < ui->entries->count(); ++i)
88     {
89         SendCoinsEntry *entry = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget());
90         if(entry)
91         {
92             entry->setModel(model);
93         }
94     }
95     if(model && model->getOptionsModel())
96     {
97         setBalance(model->getBalance(), model->getStake(), model->getUnconfirmedBalance(), model->getImmatureBalance());
98         connect(model, SIGNAL(balanceChanged(qint64, qint64, qint64, qint64)), this, SLOT(setBalance(qint64, qint64, qint64, qint64)));
99         connect(model->getOptionsModel(), SIGNAL(displayUnitChanged(int)), this, SLOT(updateDisplayUnit()));
100
101         // Coin Control
102         connect(model->getOptionsModel(), SIGNAL(displayUnitChanged(int)), this, SLOT(coinControlUpdateLabels()));
103         connect(model->getOptionsModel(), SIGNAL(coinControlFeaturesChanged(bool)), this, SLOT(coinControlFeatureChanged(bool)));
104         connect(model->getOptionsModel(), SIGNAL(transactionFeeChanged(qint64)), this, SLOT(coinControlUpdateLabels()));
105         ui->frameCoinControl->setVisible(model->getOptionsModel()->getCoinControlFeatures());
106         coinControlUpdateLabels();
107     }
108 }
109
110 SendCoinsDialog::~SendCoinsDialog()
111 {
112     delete ui;
113 }
114
115 void SendCoinsDialog::on_sendButton_clicked()
116 {
117     QList<SendCoinsRecipient> recipients;
118     bool valid = true;
119
120     if(!model)
121         return;
122
123     for(int i = 0; i < ui->entries->count(); ++i)
124     {
125         SendCoinsEntry *entry = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget());
126         if(entry)
127         {
128             if(entry->validate())
129             {
130                 recipients.append(entry->getValue());
131             }
132             else
133             {
134                 valid = false;
135             }
136         }
137     }
138
139     if(!valid || recipients.isEmpty())
140     {
141         return;
142     }
143
144     // Format confirmation message
145     QStringList formatted;
146     foreach(const SendCoinsRecipient &rcp, recipients)
147     {
148         formatted.append(tr("<b>%1</b> to %2 (%3)").arg(BitcoinUnits::formatWithUnit(BitcoinUnits::BTC, rcp.amount), Qt::escape(rcp.label), rcp.address));
149     }
150
151     fNewRecipientAllowed = false;
152
153     QMessageBox::StandardButton retval = QMessageBox::question(this, tr("Confirm send coins"),
154                           tr("Are you sure you want to send %1?").arg(formatted.join(tr(" and "))),
155           QMessageBox::Yes|QMessageBox::Cancel,
156           QMessageBox::Cancel);
157
158     if(retval != QMessageBox::Yes)
159     {
160         fNewRecipientAllowed = true;
161         return;
162     }
163
164     WalletModel::UnlockContext ctx(model->requestUnlock());
165     if(!ctx.isValid())
166     {
167         // Unlock wallet was cancelled
168         fNewRecipientAllowed = true;
169         return;
170     }
171
172     WalletModel::SendCoinsReturn sendstatus;
173
174     if (!model->getOptionsModel() || !model->getOptionsModel()->getCoinControlFeatures())
175         sendstatus = model->sendCoins(recipients);
176     else
177         sendstatus = model->sendCoins(recipients, CoinControlDialog::coinControl);
178
179     switch(sendstatus.status)
180     {
181     case WalletModel::InvalidAddress:
182         QMessageBox::warning(this, tr("Send Coins"),
183             tr("The recipient address is not valid, please recheck."),
184             QMessageBox::Ok, QMessageBox::Ok);
185         break;
186     case WalletModel::InvalidAmount:
187         QMessageBox::warning(this, tr("Send Coins"),
188             tr("The amount to pay must be larger than 0."),
189             QMessageBox::Ok, QMessageBox::Ok);
190         break;
191     case WalletModel::AmountExceedsBalance:
192         QMessageBox::warning(this, tr("Send Coins"),
193             tr("The amount exceeds your balance."),
194             QMessageBox::Ok, QMessageBox::Ok);
195         break;
196     case WalletModel::AmountWithFeeExceedsBalance:
197         QMessageBox::warning(this, tr("Send Coins"),
198             tr("The total exceeds your balance when the %1 transaction fee is included.").
199             arg(BitcoinUnits::formatWithUnit(BitcoinUnits::BTC, sendstatus.fee)),
200             QMessageBox::Ok, QMessageBox::Ok);
201         break;
202     case WalletModel::DuplicateAddress:
203         QMessageBox::warning(this, tr("Send Coins"),
204             tr("Duplicate address found, can only send to each address once per send operation."),
205             QMessageBox::Ok, QMessageBox::Ok);
206         break;
207     case WalletModel::TransactionCreationFailed:
208         QMessageBox::warning(this, tr("Send Coins"),
209             tr("Error: Transaction creation failed."),
210             QMessageBox::Ok, QMessageBox::Ok);
211         break;
212     case WalletModel::TransactionCommitFailed:
213         QMessageBox::warning(this, tr("Send Coins"),
214             tr("Error: The transaction was rejected. This might happen if some of the coins in your wallet were already spent, such as if you used a copy of wallet.dat and coins were spent in the copy but not marked as spent here."),
215             QMessageBox::Ok, QMessageBox::Ok);
216         break;
217     case WalletModel::Aborted: // User aborted, nothing to do
218         break;
219     case WalletModel::OK:
220         accept();
221         CoinControlDialog::coinControl->UnSelectAll();
222         coinControlUpdateLabels();
223         break;
224     }
225     fNewRecipientAllowed = true;
226 }
227
228 void SendCoinsDialog::clear()
229 {
230     // Remove entries until only one left
231     while(ui->entries->count())
232     {
233         delete ui->entries->takeAt(0)->widget();
234     }
235     addEntry();
236
237     updateRemoveEnabled();
238
239     ui->sendButton->setDefault(true);
240 }
241
242 void SendCoinsDialog::reject()
243 {
244     clear();
245 }
246
247 void SendCoinsDialog::accept()
248 {
249     clear();
250 }
251
252 SendCoinsEntry *SendCoinsDialog::addEntry()
253 {
254     SendCoinsEntry *entry = new SendCoinsEntry(this);
255     entry->setModel(model);
256     ui->entries->addWidget(entry);
257     connect(entry, SIGNAL(removeEntry(SendCoinsEntry*)), this, SLOT(removeEntry(SendCoinsEntry*)));
258     connect(entry, SIGNAL(payAmountChanged()), this, SLOT(coinControlUpdateLabels()));
259
260     updateRemoveEnabled();
261
262     // Focus the field, so that entry can start immediately
263     entry->clear();
264     entry->setFocus();
265     ui->scrollAreaWidgetContents->resize(ui->scrollAreaWidgetContents->sizeHint());
266     QCoreApplication::instance()->processEvents();
267     QScrollBar* bar = ui->scrollArea->verticalScrollBar();
268     if(bar)
269         bar->setSliderPosition(bar->maximum());
270     return entry;
271 }
272
273 void SendCoinsDialog::updateRemoveEnabled()
274 {
275     // Remove buttons are enabled as soon as there is more than one send-entry
276     bool enabled = (ui->entries->count() > 1);
277     for(int i = 0; i < ui->entries->count(); ++i)
278     {
279         SendCoinsEntry *entry = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget());
280         if(entry)
281         {
282             entry->setRemoveEnabled(enabled);
283         }
284     }
285     setupTabChain(0);
286     coinControlUpdateLabels();
287 }
288
289 void SendCoinsDialog::removeEntry(SendCoinsEntry* entry)
290 {
291     delete entry;
292     updateRemoveEnabled();
293 }
294
295 QWidget *SendCoinsDialog::setupTabChain(QWidget *prev)
296 {
297     for(int i = 0; i < ui->entries->count(); ++i)
298     {
299         SendCoinsEntry *entry = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget());
300         if(entry)
301         {
302             prev = entry->setupTabChain(prev);
303         }
304     }
305     QWidget::setTabOrder(prev, ui->addButton);
306     QWidget::setTabOrder(ui->addButton, ui->sendButton);
307     return ui->sendButton;
308 }
309
310 void SendCoinsDialog::pasteEntry(const SendCoinsRecipient &rv)
311 {
312     if(!fNewRecipientAllowed)
313         return;
314
315     SendCoinsEntry *entry = 0;
316     // Replace the first entry if it is still unused
317     if(ui->entries->count() == 1)
318     {
319         SendCoinsEntry *first = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(0)->widget());
320         if(first->isClear())
321         {
322             entry = first;
323         }
324     }
325     if(!entry)
326     {
327         entry = addEntry();
328     }
329
330     entry->setValue(rv);
331 }
332
333 bool SendCoinsDialog::handleURI(const QString &uri)
334 {
335     SendCoinsRecipient rv;
336     // URI has to be valid
337     if (GUIUtil::parseBitcoinURI(uri, &rv))
338     {
339         CBitcoinAddress address(rv.address.toStdString());
340         if (!address.IsValid())
341             return false;
342         pasteEntry(rv);
343         return true;
344     }
345
346     return false;
347 }
348
349 void SendCoinsDialog::setBalance(qint64 balance, qint64 stake, qint64 unconfirmedBalance, qint64 immatureBalance)
350 {
351     Q_UNUSED(stake);
352     Q_UNUSED(unconfirmedBalance);
353     Q_UNUSED(immatureBalance);
354     if(!model || !model->getOptionsModel())
355         return;
356
357     int unit = model->getOptionsModel()->getDisplayUnit();
358     ui->labelBalance->setText(BitcoinUnits::formatWithUnit(unit, balance));
359 }
360
361 void SendCoinsDialog::updateDisplayUnit()
362 {
363     if(model && model->getOptionsModel())
364     {
365         // Update labelBalance with the current balance and the current unit
366         ui->labelBalance->setText(BitcoinUnits::formatWithUnit(model->getOptionsModel()->getDisplayUnit(), model->getBalance()));
367     }
368 }
369
370 // Coin Control: copy label "Quantity" to clipboard
371 void SendCoinsDialog::coinControlClipboardQuantity()
372 {
373     QApplication::clipboard()->setText(ui->labelCoinControlQuantity->text());
374 }
375
376 // Coin Control: copy label "Amount" to clipboard
377 void SendCoinsDialog::coinControlClipboardAmount()
378 {
379     QApplication::clipboard()->setText(ui->labelCoinControlAmount->text().left(ui->labelCoinControlAmount->text().indexOf(" ")));
380 }
381
382 // Coin Control: copy label "Fee" to clipboard
383 void SendCoinsDialog::coinControlClipboardFee()
384 {
385     QApplication::clipboard()->setText(ui->labelCoinControlFee->text().left(ui->labelCoinControlFee->text().indexOf(" ")));
386 }
387
388 // Coin Control: copy label "After fee" to clipboard
389 void SendCoinsDialog::coinControlClipboardAfterFee()
390 {
391     QApplication::clipboard()->setText(ui->labelCoinControlAfterFee->text().left(ui->labelCoinControlAfterFee->text().indexOf(" ")));
392 }
393
394 // Coin Control: copy label "Bytes" to clipboard
395 void SendCoinsDialog::coinControlClipboardBytes()
396 {
397     QApplication::clipboard()->setText(ui->labelCoinControlBytes->text());
398 }
399
400 // Coin Control: copy label "Priority" to clipboard
401 void SendCoinsDialog::coinControlClipboardPriority()
402 {
403     QApplication::clipboard()->setText(ui->labelCoinControlPriority->text());
404 }
405
406 // Coin Control: copy label "Low output" to clipboard
407 void SendCoinsDialog::coinControlClipboardLowOutput()
408 {
409     QApplication::clipboard()->setText(ui->labelCoinControlLowOutput->text());
410 }
411
412 // Coin Control: copy label "Change" to clipboard
413 void SendCoinsDialog::coinControlClipboardChange()
414 {
415     QApplication::clipboard()->setText(ui->labelCoinControlChange->text().left(ui->labelCoinControlChange->text().indexOf(" ")));
416 }
417
418 // Coin Control: settings menu - coin control enabled/disabled by user
419 void SendCoinsDialog::coinControlFeatureChanged(bool checked)
420 {
421     ui->frameCoinControl->setVisible(checked);
422
423     if (!checked && model) // coin control features disabled
424         CoinControlDialog::coinControl->SetNull();
425 }
426
427 // Coin Control: button inputs -> show actual coin control dialog
428 void SendCoinsDialog::coinControlButtonClicked()
429 {
430     CoinControlDialog dlg;
431     dlg.setModel(model);
432     dlg.exec();
433     coinControlUpdateLabels();
434 }
435
436 // Coin Control: checkbox custom change address
437 void SendCoinsDialog::coinControlChangeChecked(int state)
438 {
439     if (model)
440     {
441         if (state == Qt::Checked)
442             CoinControlDialog::coinControl->destChange = CBitcoinAddress(ui->lineEditCoinControlChange->text().toStdString()).Get();
443         else
444             CoinControlDialog::coinControl->destChange = CNoDestination();
445     }
446
447     ui->lineEditCoinControlChange->setEnabled((state == Qt::Checked));
448     ui->labelCoinControlChangeLabel->setEnabled((state == Qt::Checked));
449 }
450
451 // Coin Control: custom change address changed
452 void SendCoinsDialog::coinControlChangeEdited(const QString & text)
453 {
454     if (model)
455     {
456         CoinControlDialog::coinControl->destChange = CBitcoinAddress(text.toStdString()).Get();
457
458         // label for the change address
459         ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:black;}");
460         if (text.isEmpty())
461             ui->labelCoinControlChangeLabel->setText("");
462         else if (!CBitcoinAddress(text.toStdString()).IsValid())
463         {
464             ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:red;}");
465             ui->labelCoinControlChangeLabel->setText(tr("WARNING: Invalid Bitcoin address"));
466         }
467         else
468         {
469             QString associatedLabel = model->getAddressTableModel()->labelForAddress(text);
470             if (!associatedLabel.isEmpty())
471                 ui->labelCoinControlChangeLabel->setText(associatedLabel);
472             else
473             {
474                 CPubKey pubkey;
475                 CKeyID keyid;
476                 CBitcoinAddress(text.toStdString()).GetKeyID(keyid);   
477                 if (model->getPubKey(keyid, pubkey))
478                     ui->labelCoinControlChangeLabel->setText(tr("(no label)"));
479                 else
480                 {
481                     ui->labelCoinControlChangeLabel->setStyleSheet("QLabel{color:red;}");
482                     ui->labelCoinControlChangeLabel->setText(tr("WARNING: unknown change address"));
483                 }
484             }
485         }
486     }
487 }
488
489 // Coin Control: update labels
490 void SendCoinsDialog::coinControlUpdateLabels()
491 {
492     if (!model || !model->getOptionsModel() || !model->getOptionsModel()->getCoinControlFeatures())
493         return;
494
495     // set pay amounts
496     CoinControlDialog::payAmounts.clear();
497     for(int i = 0; i < ui->entries->count(); ++i)
498     {
499         SendCoinsEntry *entry = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget());
500         if(entry)
501             CoinControlDialog::payAmounts.append(entry->getValue().amount);
502     }
503
504     if (CoinControlDialog::coinControl->HasSelected())
505     {
506         // actual coin control calculation
507         CoinControlDialog::updateLabels(model, this);
508
509         // show coin control stats
510         ui->labelCoinControlAutomaticallySelected->hide();
511         ui->widgetCoinControl->show();
512     }
513     else
514     {
515         // hide coin control stats
516         ui->labelCoinControlAutomaticallySelected->show();
517         ui->widgetCoinControl->hide();
518         ui->labelCoinControlInsuffFunds->hide();
519     }
520 }