diff options
-rw-r--r-- | bitcoin-qt.pro | 11 | ||||
-rw-r--r-- | doc/assets-attribution.txt | 2 | ||||
-rw-r--r-- | src/qt/addresstablemodel.cpp | 28 | ||||
-rw-r--r-- | src/qt/addresstablemodel.h | 22 | ||||
-rw-r--r-- | src/qt/bitcoin.qrc | 1 | ||||
-rw-r--r-- | src/qt/bitcoinaddressvalidator.cpp | 6 | ||||
-rw-r--r-- | src/qt/bitcoinamountfield.cpp | 33 | ||||
-rw-r--r-- | src/qt/bitcoinamountfield.h | 10 | ||||
-rw-r--r-- | src/qt/editaddressdialog.cpp | 19 | ||||
-rw-r--r-- | src/qt/forms/sendcoinsdialog.ui | 166 | ||||
-rw-r--r-- | src/qt/forms/sendcoinsentry.ui | 175 | ||||
-rw-r--r-- | src/qt/qvalidatedlineedit.cpp | 37 | ||||
-rw-r--r-- | src/qt/qvalidatedlineedit.h | 27 | ||||
-rw-r--r-- | src/qt/sendcoinsdialog.cpp | 176 | ||||
-rw-r--r-- | src/qt/sendcoinsdialog.h | 14 | ||||
-rw-r--r-- | src/qt/sendcoinsentry.cpp | 119 | ||||
-rw-r--r-- | src/qt/sendcoinsentry.h | 45 | ||||
-rw-r--r-- | src/qt/walletmodel.cpp | 91 | ||||
-rw-r--r-- | src/qt/walletmodel.h | 31 |
19 files changed, 735 insertions, 278 deletions
diff --git a/bitcoin-qt.pro b/bitcoin-qt.pro index 9af4c671f1..cdff04b43a 100644 --- a/bitcoin-qt.pro +++ b/bitcoin-qt.pro @@ -84,7 +84,9 @@ HEADERS += src/qt/bitcoingui.h \ src/qt/overviewpage.h \ src/qt/csvmodelwriter.h \ src/qt/qtwin.h \ - src/crypter.h + src/crypter.h \ + src/qt/sendcoinsentry.h \ + src/qt/qvalidatedlineedit.h SOURCES += src/qt/bitcoin.cpp src/qt/bitcoingui.cpp \ src/qt/transactiontablemodel.cpp \ src/qt/addresstablemodel.cpp \ @@ -124,7 +126,9 @@ SOURCES += src/qt/bitcoin.cpp src/qt/bitcoingui.cpp \ src/qt/overviewpage.cpp \ src/qt/csvmodelwriter.cpp \ src/qt/qtwin.cpp \ - src/crypter.cpp + src/crypter.cpp \ + src/qt/sendcoinsentry.cpp \ + src/qt/qvalidatedlineedit.cpp RESOURCES += \ src/qt/bitcoin.qrc @@ -135,7 +139,8 @@ FORMS += \ src/qt/forms/aboutdialog.ui \ src/qt/forms/editaddressdialog.ui \ src/qt/forms/transactiondescdialog.ui \ - src/qt/forms/overviewpage.ui + src/qt/forms/overviewpage.ui \ + src/qt/forms/sendcoinsentry.ui CODECFORTR = UTF-8 TRANSLATIONS = src/qt/locale/bitcoin_nl.ts src/qt/locale/bitcoin_de.ts diff --git a/doc/assets-attribution.txt b/doc/assets-attribution.txt index f58b0da40c..f3900fe0fa 100644 --- a/doc/assets-attribution.txt +++ b/doc/assets-attribution.txt @@ -29,7 +29,7 @@ License: You are free to do with these icons as you wish, including selling, Icon: src/qt/res/icons/configure.png, src/qt/res/icons/quit.png, src/qt/res/icons/editcopy.png, src/qt/res/icons/editpaste.png, src/qt/res/icons/add.png, src/qt/res/icons/edit.png, - src/qt/res/icons/editdelete.png + src/qt/res/icons/editdelete.png, src/qt/res/icons/remove.png (edited) Designer: http://www.everaldo.com Icon Pack: Crystal SVG License: LGPL diff --git a/src/qt/addresstablemodel.cpp b/src/qt/addresstablemodel.cpp index 4578ca740f..125ceebb33 100644 --- a/src/qt/addresstablemodel.cpp +++ b/src/qt/addresstablemodel.cpp @@ -1,5 +1,6 @@ #include "addresstablemodel.h" #include "guiutil.h" +#include "walletmodel.h" #include "headers.h" @@ -72,8 +73,8 @@ struct AddressTablePriv } }; -AddressTableModel::AddressTableModel(CWallet *wallet, QObject *parent) : - QAbstractTableModel(parent),wallet(wallet),priv(0) +AddressTableModel::AddressTableModel(CWallet *wallet, WalletModel *parent) : + QAbstractTableModel(parent),walletModel(parent),wallet(wallet),priv(0) { columns << tr("Label") << tr("Address"); priv = new AddressTablePriv(wallet); @@ -150,6 +151,8 @@ bool AddressTableModel::setData(const QModelIndex & index, const QVariant & valu return false; AddressTableEntry *rec = static_cast<AddressTableEntry*>(index.internalPointer()); + editStatus = OK; + if(role == Qt::EditRole) { switch(index.column()) @@ -160,8 +163,11 @@ bool AddressTableModel::setData(const QModelIndex & index, const QVariant & valu break; case Address: // Refuse to set invalid address - if(!validateAddress(value.toString())) + if(!walletModel->validateAddress(value.toString())) + { + editStatus = INVALID_ADDRESS; return false; + } // Double-check that we're not overwriting receiving address if(rec->type == AddressTableEntry::Sending) { @@ -240,13 +246,22 @@ QString AddressTableModel::addRow(const QString &type, const QString &label, con std::string strLabel = label.toStdString(); std::string strAddress = address.toStdString(); + editStatus = OK; + + if(type == Send) { + if(!walletModel->validateAddress(address)) + { + editStatus = INVALID_ADDRESS; + return QString(); + } // Check for duplicate CRITICAL_BLOCK(wallet->cs_mapAddressBook) { if(wallet->mapAddressBook.count(strAddress)) { + editStatus = DUPLICATE_ADDRESS; return QString(); } } @@ -291,13 +306,6 @@ void AddressTableModel::update() } -bool AddressTableModel::validateAddress(const QString &address) -{ - uint160 hash160 = 0; - - return AddressToHash160(address.toStdString(), hash160); -} - /* Look up label for address in address book, if not found return empty string. */ QString AddressTableModel::labelForAddress(const QString &address) const diff --git a/src/qt/addresstablemodel.h b/src/qt/addresstablemodel.h index d48e786621..296fa58054 100644 --- a/src/qt/addresstablemodel.h +++ b/src/qt/addresstablemodel.h @@ -6,12 +6,13 @@ class AddressTablePriv; class CWallet; +class WalletModel; class AddressTableModel : public QAbstractTableModel { Q_OBJECT public: - explicit AddressTableModel(CWallet *wallet, QObject *parent = 0); + explicit AddressTableModel(CWallet *wallet, WalletModel *parent = 0); ~AddressTableModel(); enum ColumnIndex { @@ -19,9 +20,16 @@ public: Address = 1 /* Bitcoin address */ }; - enum { + enum RoleIndex { TypeRole = Qt::UserRole - } RoleIndex; + }; + + // Return status of last edit/insert operation + enum EditStatus { + OK = 0, + INVALID_ADDRESS = 1, + DUPLICATE_ADDRESS = 2 + }; static const QString Send; /* Send addres */ static const QString Receive; /* Receive address */ @@ -45,10 +53,6 @@ public: */ void updateList(); - /* Check address for validity - */ - bool validateAddress(const QString &address); - /* Look up label for address in address book, if not found return empty string. */ QString labelForAddress(const QString &address) const; @@ -58,10 +62,14 @@ public: */ int lookupAddress(const QString &address) const; + EditStatus getEditStatus() const { return editStatus; } + private: + WalletModel *walletModel; CWallet *wallet; AddressTablePriv *priv; QStringList columns; + EditStatus editStatus; signals: void defaultAddressChanged(const QString &address); diff --git a/src/qt/bitcoin.qrc b/src/qt/bitcoin.qrc index 1522ce61e3..9ef8b2afd5 100644 --- a/src/qt/bitcoin.qrc +++ b/src/qt/bitcoin.qrc @@ -31,6 +31,7 @@ <file alias="export">res/icons/export.png</file> <file alias="synced">res/icons/synced.png</file> <file alias="notsynced">res/icons/notsynced.png</file> + <file>res/icons/remove.png</file> </qresource> <qresource prefix="/images"> <file alias="about">res/images/about.png</file> diff --git a/src/qt/bitcoinaddressvalidator.cpp b/src/qt/bitcoinaddressvalidator.cpp index 4308a893c2..373877808f 100644 --- a/src/qt/bitcoinaddressvalidator.cpp +++ b/src/qt/bitcoinaddressvalidator.cpp @@ -57,5 +57,11 @@ QValidator::State BitcoinAddressValidator::validate(QString &input, int &pos) co } } + // Empty address is "intermediate" input + if(input.isEmpty()) + { + state = QValidator::Intermediate; + } + return state; } diff --git a/src/qt/bitcoinamountfield.cpp b/src/qt/bitcoinamountfield.cpp index 1359a32b87..d545dc52e8 100644 --- a/src/qt/bitcoinamountfield.cpp +++ b/src/qt/bitcoinamountfield.cpp @@ -1,4 +1,5 @@ #include "bitcoinamountfield.h" +#include "qvalidatedlineedit.h" #include <QLabel> #include <QLineEdit> @@ -9,12 +10,12 @@ BitcoinAmountField::BitcoinAmountField(QWidget *parent): QWidget(parent), amount(0), decimals(0) { - amount = new QLineEdit(this); + amount = new QValidatedLineEdit(this); amount->setValidator(new QRegExpValidator(QRegExp("[0-9]+"), this)); amount->setAlignment(Qt::AlignRight|Qt::AlignVCenter); amount->installEventFilter(this); amount->setMaximumWidth(100); - decimals = new QLineEdit(this); + decimals = new QValidatedLineEdit(this); decimals->setValidator(new QRegExpValidator(QRegExp("[0-9]+"), this)); decimals->setMaxLength(8); decimals->setAlignment(Qt::AlignLeft|Qt::AlignVCenter); @@ -29,8 +30,9 @@ BitcoinAmountField::BitcoinAmountField(QWidget *parent): layout->addStretch(1); layout->setContentsMargins(0,0,0,0); - setFocusPolicy(Qt::TabFocus); setLayout(layout); + + setFocusPolicy(Qt::TabFocus); setFocusProxy(amount); // If one if the widgets changes, the combined content changes as well @@ -53,10 +55,28 @@ void BitcoinAmountField::setText(const QString &text) } } +bool BitcoinAmountField::validate() +{ + bool valid = true; + if(amount->text().isEmpty()) + { + amount->setValid(false); + valid = false; + } + if(decimals->text().isEmpty()) + { + decimals->setValid(false); + valid = false; + } + return valid; +} + QString BitcoinAmountField::text() const { if(amount->text().isEmpty() || decimals->text().isEmpty()) + { return QString(); + } return amount->text() + QString(".") + decimals->text(); } @@ -75,3 +95,10 @@ bool BitcoinAmountField::eventFilter(QObject *object, QEvent *event) } return false; } + +QWidget *BitcoinAmountField::setupTabChain(QWidget *prev) +{ + QWidget::setTabOrder(prev, amount); + QWidget::setTabOrder(amount, decimals); + return decimals; +} diff --git a/src/qt/bitcoinamountfield.h b/src/qt/bitcoinamountfield.h index 67304c8b3a..2a0ef4bd99 100644 --- a/src/qt/bitcoinamountfield.h +++ b/src/qt/bitcoinamountfield.h @@ -4,7 +4,7 @@ #include <QWidget> QT_BEGIN_NAMESPACE -class QLineEdit; +class QValidatedLineEdit; QT_END_NAMESPACE // Coin amount entry widget with separate parts for whole @@ -18,6 +18,10 @@ public: void setText(const QString &text); QString text() const; + bool validate(); + // Qt messes up the tab chain by default in some cases (issue http://bugreports.qt.nokia.com/browse/QTBUG-10907) + // Hence we have to set it up manually + QWidget *setupTabChain(QWidget *prev); signals: void textChanged(); @@ -27,8 +31,8 @@ protected: bool eventFilter(QObject *object, QEvent *event); private: - QLineEdit *amount; - QLineEdit *decimals; + QValidatedLineEdit *amount; + QValidatedLineEdit *decimals; }; diff --git a/src/qt/editaddressdialog.cpp b/src/qt/editaddressdialog.cpp index 7ea5638b4b..a0b27e83bb 100644 --- a/src/qt/editaddressdialog.cpp +++ b/src/qt/editaddressdialog.cpp @@ -79,23 +79,22 @@ QString EditAddressDialog::saveCurrentRow() void EditAddressDialog::accept() { - if(mode == NewSendingAddress || mode == EditSendingAddress) + if(saveCurrentRow().isEmpty()) { - // For sending addresses, check validity - // Not needed for receiving addresses, as those are generated - if(!model->validateAddress(ui->addressEdit->text())) + switch(model->getEditStatus()) { + case AddressTableModel::DUPLICATE_ADDRESS: + QMessageBox::warning(this, windowTitle(), + tr("The entered address \"%1\" is already in the address book.").arg(ui->addressEdit->text()), + QMessageBox::Ok, QMessageBox::Ok); + break; + case AddressTableModel::INVALID_ADDRESS: QMessageBox::warning(this, windowTitle(), tr("The entered address \"%1\" is not a valid bitcoin address.").arg(ui->addressEdit->text()), QMessageBox::Ok, QMessageBox::Ok); return; } - } - if(saveCurrentRow().isEmpty()) - { - QMessageBox::warning(this, windowTitle(), - tr("The entered address \"%1\" is already in the address book.").arg(ui->addressEdit->text()), - QMessageBox::Ok, QMessageBox::Ok); + return; } QDialog::accept(); diff --git a/src/qt/forms/sendcoinsdialog.ui b/src/qt/forms/sendcoinsdialog.ui index 8009bd2b94..57b79279af 100644 --- a/src/qt/forms/sendcoinsdialog.ui +++ b/src/qt/forms/sendcoinsdialog.ui @@ -15,145 +15,10 @@ </property> <layout class="QVBoxLayout" name="verticalLayout"> <item> - <spacer name="verticalSpacer_2"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Preferred</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>12</height> - </size> - </property> - </spacer> - </item> - <item> - <layout class="QGridLayout" name="gridLayout"> + <layout class="QVBoxLayout" name="entries"> <property name="spacing"> - <number>12</number> + <number>6</number> </property> - <item row="5" column="0"> - <widget class="QLabel" name="label"> - <property name="text"> - <string>A&mount:</string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - <property name="buddy"> - <cstring>payAmount</cstring> - </property> - </widget> - </item> - <item row="3" column="0"> - <widget class="QLabel" name="label_2"> - <property name="text"> - <string>Pay &To:</string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - <property name="buddy"> - <cstring>payTo</cstring> - </property> - </widget> - </item> - <item row="5" column="1"> - <widget class="BitcoinAmountField" name="payAmount"/> - </item> - <item row="4" column="1"> - <layout class="QHBoxLayout" name="horizontalLayout_2"> - <property name="spacing"> - <number>0</number> - </property> - <item> - <widget class="QLineEdit" name="addAsLabel"> - <property name="enabled"> - <bool>true</bool> - </property> - <property name="toolTip"> - <string>Enter a label for this address to add it to your address book</string> - </property> - </widget> - </item> - </layout> - </item> - <item row="4" column="0"> - <widget class="QLabel" name="label_4"> - <property name="text"> - <string>&Label:</string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - <property name="buddy"> - <cstring>addAsLabel</cstring> - </property> - </widget> - </item> - <item row="3" column="1"> - <layout class="QHBoxLayout" name="horizontalLayout_3"> - <property name="spacing"> - <number>0</number> - </property> - <item> - <widget class="QLineEdit" name="payTo"> - <property name="toolTip"> - <string>The address to send the payment to (e.g. 1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L)</string> - </property> - <property name="maxLength"> - <number>34</number> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="addressBookButton"> - <property name="toolTip"> - <string>Look up adress in address book</string> - </property> - <property name="text"> - <string/> - </property> - <property name="icon"> - <iconset resource="../bitcoin.qrc"> - <normaloff>:/icons/address-book</normaloff>:/icons/address-book</iconset> - </property> - <property name="shortcut"> - <string>Alt+A</string> - </property> - <property name="autoDefault"> - <bool>false</bool> - </property> - <property name="flat"> - <bool>false</bool> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="pasteButton"> - <property name="toolTip"> - <string>Paste address from system clipboard</string> - </property> - <property name="text"> - <string/> - </property> - <property name="icon"> - <iconset resource="../bitcoin.qrc"> - <normaloff>:/icons/editpaste</normaloff>:/icons/editpaste</iconset> - </property> - <property name="shortcut"> - <string>Alt+P</string> - </property> - <property name="autoDefault"> - <bool>false</bool> - </property> - </widget> - </item> - </layout> - </item> </layout> </item> <item> @@ -175,6 +40,17 @@ </spacer> </item> <item> + <widget class="QPushButton" name="addButton"> + <property name="text"> + <string>&Add recipient...</string> + </property> + <property name="icon"> + <iconset resource="../bitcoin.qrc"> + <normaloff>:/icons/add</normaloff>:/icons/add</iconset> + </property> + </widget> + </item> + <item> <widget class="QPushButton" name="sendButton"> <property name="minimumSize"> <size> @@ -214,22 +90,6 @@ </item> </layout> </widget> - <customwidgets> - <customwidget> - <class>BitcoinAmountField</class> - <extends>QLineEdit</extends> - <header>bitcoinamountfield.h</header> - <container>1</container> - </customwidget> - </customwidgets> - <tabstops> - <tabstop>payTo</tabstop> - <tabstop>addressBookButton</tabstop> - <tabstop>pasteButton</tabstop> - <tabstop>addAsLabel</tabstop> - <tabstop>payAmount</tabstop> - <tabstop>sendButton</tabstop> - </tabstops> <resources> <include location="../bitcoin.qrc"/> </resources> diff --git a/src/qt/forms/sendcoinsentry.ui b/src/qt/forms/sendcoinsentry.ui new file mode 100644 index 0000000000..1159ef5389 --- /dev/null +++ b/src/qt/forms/sendcoinsentry.ui @@ -0,0 +1,175 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SendCoinsEntry</class> + <widget class="QFrame" name="SendCoinsEntry"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>729</width> + <height>136</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Sunken</enum> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="spacing"> + <number>12</number> + </property> + <item row="5" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>A&mount:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="buddy"> + <cstring>payAmount</cstring> + </property> + </widget> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Pay &To:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="buddy"> + <cstring>payTo</cstring> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="BitcoinAmountField" name="payAmount"/> + </item> + <item row="4" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <widget class="QValidatedLineEdit" name="addAsLabel"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="toolTip"> + <string>Enter a label for this address to add it to your address book</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>&Label:</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="buddy"> + <cstring>addAsLabel</cstring> + </property> + </widget> + </item> + <item row="3" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <widget class="QValidatedLineEdit" name="payTo"> + <property name="toolTip"> + <string>The address to send the payment to (e.g. 1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L)</string> + </property> + <property name="maxLength"> + <number>34</number> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="addressBookButton"> + <property name="toolTip"> + <string>Look up adress in address book</string> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="../bitcoin.qrc"> + <normaloff>:/icons/address-book</normaloff>:/icons/address-book</iconset> + </property> + <property name="shortcut"> + <string>Alt+A</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + <property name="flat"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="pasteButton"> + <property name="toolTip"> + <string>Paste address from system clipboard</string> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="../bitcoin.qrc"> + <normaloff>:/icons/editpaste</normaloff>:/icons/editpaste</iconset> + </property> + <property name="shortcut"> + <string>Alt+P</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="deleteButton"> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="../bitcoin.qrc"> + <normaloff>:/icons/res/icons/remove.png</normaloff>:/icons/res/icons/remove.png</iconset> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>BitcoinAmountField</class> + <extends>QLineEdit</extends> + <header>bitcoinamountfield.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>QValidatedLineEdit</class> + <extends>QLineEdit</extends> + <header>qvalidatedlineedit.h</header> + </customwidget> + </customwidgets> + <resources> + <include location="../bitcoin.qrc"/> + </resources> + <connections/> +</ui> diff --git a/src/qt/qvalidatedlineedit.cpp b/src/qt/qvalidatedlineedit.cpp new file mode 100644 index 0000000000..4b5acd8b07 --- /dev/null +++ b/src/qt/qvalidatedlineedit.cpp @@ -0,0 +1,37 @@ +#include "qvalidatedlineedit.h" + +QValidatedLineEdit::QValidatedLineEdit(QWidget *parent) : + QLineEdit(parent), valid(true) +{ + connect(this, SIGNAL(textChanged(QString)), this, SLOT(markValid())); +} + +void QValidatedLineEdit::setValid(bool valid) +{ + if(valid == this->valid) + { + return; + } + + if(valid) + { + setStyleSheet(""); + } + else + { + setStyleSheet("background:#FF8080"); + } + this->valid = valid; +} + +void QValidatedLineEdit::focusInEvent(QFocusEvent *evt) +{ + // Clear invalid flag on focus + setValid(true); + QLineEdit::focusInEvent(evt); +} + +void QValidatedLineEdit::markValid() +{ + setValid(true); +} diff --git a/src/qt/qvalidatedlineedit.h b/src/qt/qvalidatedlineedit.h new file mode 100644 index 0000000000..9fc026fab1 --- /dev/null +++ b/src/qt/qvalidatedlineedit.h @@ -0,0 +1,27 @@ +#ifndef QVALIDATEDLINEEDIT_H +#define QVALIDATEDLINEEDIT_H + +#include <QLineEdit> + +// Line edit that can be marked as "invalid". When marked as invalid, +// it will get a red background until it is focused. +class QValidatedLineEdit : public QLineEdit +{ + Q_OBJECT +public: + explicit QValidatedLineEdit(QWidget *parent = 0); + +protected: + void focusInEvent(QFocusEvent *evt); + +private: + bool valid; + +public slots: + void setValid(bool valid); + +private slots: + void markValid(); +}; + +#endif // QVALIDATEDLINEEDIT_H diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index 01d68dc0af..38a0a65520 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -1,43 +1,40 @@ #include "sendcoinsdialog.h" #include "ui_sendcoinsdialog.h" #include "walletmodel.h" -#include "addresstablemodel.h" #include "guiutil.h" - #include "addressbookpage.h" #include "optionsmodel.h" +#include "sendcoinsentry.h" + -#include <QApplication> -#include <QClipboard> #include <QMessageBox> #include <QLocale> #include <QDebug> -#include <QMessageBox> -SendCoinsDialog::SendCoinsDialog(QWidget *parent, const QString &address) : +SendCoinsDialog::SendCoinsDialog(QWidget *parent) : QDialog(parent), ui(new Ui::SendCoinsDialog), model(0) { ui->setupUi(this); -#if QT_VERSION >= 0x040700 - ui->payTo->setPlaceholderText(tr("Enter a Bitcoin address (e.g. 1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L)")); - ui->addAsLabel->setPlaceholderText(tr("Enter a label for this address to add it to your address book")); -#endif - GUIUtil::setupAddressWidget(ui->payTo, this); + addEntry(); - // Set initial send-to address if provided - if(!address.isEmpty()) - { - ui->payTo->setText(address); - ui->payAmount->setFocus(); - } + connect(ui->addButton, SIGNAL(clicked()), this, SLOT(addEntry())); } void SendCoinsDialog::setModel(WalletModel *model) { this->model = model; + + for(int i = 0; i < ui->entries->count(); ++i) + { + SendCoinsEntry *entry = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget()); + if(entry) + { + entry->setModel(model); + } + } } SendCoinsDialog::~SendCoinsDialog() @@ -47,26 +44,38 @@ SendCoinsDialog::~SendCoinsDialog() void SendCoinsDialog::on_sendButton_clicked() { - bool valid; - QString payAmount = ui->payAmount->text(); - QString label; - qint64 payAmountParsed; - - valid = GUIUtil::parseMoney(payAmount, &payAmountParsed); + QList<SendCoinsRecipient> recipients; + bool valid = true; + for(int i = 0; i < ui->entries->count(); ++i) + { + SendCoinsEntry *entry = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget()); + if(entry) + { + if(entry->validate()) + { + recipients.append(entry->getValue()); + } + else + { + valid = false; + } + } + } - if(!valid || payAmount.isEmpty()) + if(!valid || recipients.isEmpty()) { - QMessageBox::warning(this, tr("Send Coins"), - tr("Must fill in an amount to pay."), - QMessageBox::Ok, QMessageBox::Ok); return; } - // Add address to address book under label, if specified - label = ui->addAsLabel->text(); + // Format confirmation message + QStringList formatted; + foreach(const SendCoinsRecipient &rcp, recipients) + { + formatted.append(tr("%1 BTC to %2 (%3)").arg(GUIUtil::formatMoney(rcp.amount), rcp.label, rcp.address)); + } QMessageBox::StandardButton retval = QMessageBox::question(this, tr("Confirm send coins"), - tr("Are you sure you want to send %1 BTC to %2 (%3)?").arg(GUIUtil::formatMoney(payAmountParsed), label, ui->payTo->text()), + tr("Are you sure you want to send %1?").arg(formatted.join(tr(" and "))), QMessageBox::Yes|QMessageBox::Cancel, QMessageBox::Cancel); @@ -75,32 +84,45 @@ void SendCoinsDialog::on_sendButton_clicked() return; } - switch(model->sendCoins(ui->payTo->text(), payAmountParsed, label)) + WalletModel::SendCoinsReturn sendstatus = model->sendCoins(recipients); + switch(sendstatus.status) { case WalletModel::InvalidAddress: QMessageBox::warning(this, tr("Send Coins"), tr("The recepient address is not valid, please recheck."), QMessageBox::Ok, QMessageBox::Ok); - ui->payTo->setFocus(); break; case WalletModel::InvalidAmount: QMessageBox::warning(this, tr("Send Coins"), tr("The amount to pay must be larger than 0."), QMessageBox::Ok, QMessageBox::Ok); - ui->payAmount->setFocus(); break; case WalletModel::AmountExceedsBalance: QMessageBox::warning(this, tr("Send Coins"), tr("Amount exceeds your balance"), QMessageBox::Ok, QMessageBox::Ok); - ui->payAmount->setFocus(); break; case WalletModel::AmountWithFeeExceedsBalance: QMessageBox::warning(this, tr("Send Coins"), tr("Total exceeds your balance when the %1 transaction fee is included"). - arg(GUIUtil::formatMoney(model->getOptionsModel()->getTransactionFee())), + arg(GUIUtil::formatMoney(sendstatus.fee)), + QMessageBox::Ok, QMessageBox::Ok); + break; + case WalletModel::DuplicateAddress: + QMessageBox::warning(this, tr("Send Coins"), + tr("Duplicate address found, can only send to each address once in one send operation"), + QMessageBox::Ok, QMessageBox::Ok); + break; + case WalletModel::TransactionCreationFailed: + QMessageBox::warning(this, tr("Send Coins"), + tr("Error: Transaction creation failed "), + QMessageBox::Ok, QMessageBox::Ok); + break; + break; + case WalletModel::TransactionCommitFailed: + QMessageBox::warning(this, tr("Send Coins"), + 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."), QMessageBox::Ok, QMessageBox::Ok); - ui->payAmount->setFocus(); break; case WalletModel::OK: accept(); @@ -108,34 +130,23 @@ void SendCoinsDialog::on_sendButton_clicked() } } -void SendCoinsDialog::on_pasteButton_clicked() +void SendCoinsDialog::clear() { - // Paste text from clipboard into recipient field - ui->payTo->setText(QApplication::clipboard()->text()); -} + // Remove entries until only one left + while(ui->entries->count() > 1) + { + delete ui->entries->takeAt(0)->widget(); + } -void SendCoinsDialog::on_addressBookButton_clicked() -{ - AddressBookPage dlg(AddressBookPage::ForSending, AddressBookPage::SendingTab, this); - dlg.setModel(model->getAddressTableModel()); - if(dlg.exec()) + // Reset the entry that is left to empty + SendCoinsEntry *entry = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(0)->widget()); + if(entry) { - ui->payTo->setText(dlg.getReturnValue()); - ui->payAmount->setFocus(); + entry->clear(); } -} -void SendCoinsDialog::on_payTo_textChanged(const QString &address) -{ - ui->addAsLabel->setText(model->getAddressTableModel()->labelForAddress(address)); -} + updateRemoveEnabled(); -void SendCoinsDialog::clear() -{ - ui->payTo->setText(QString()); - ui->addAsLabel->setText(QString()); - ui->payAmount->setText(QString()); - ui->payTo->setFocus(); ui->sendButton->setDefault(true); } @@ -148,3 +159,52 @@ void SendCoinsDialog::accept() { clear(); } + +void SendCoinsDialog::addEntry() +{ + SendCoinsEntry *entry = new SendCoinsEntry(this); + entry->setModel(model); + ui->entries->addWidget(entry); + connect(entry, SIGNAL(removeEntry(SendCoinsEntry*)), this, SLOT(removeEntry(SendCoinsEntry*))); + + updateRemoveEnabled(); + + // Focus the field, so that entry can start immediately + entry->clear(); +} + +void SendCoinsDialog::updateRemoveEnabled() +{ + // Remove buttons are enabled as soon as there is more than one send-entry + bool enabled = (ui->entries->count() > 1); + for(int i = 0; i < ui->entries->count(); ++i) + { + SendCoinsEntry *entry = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget()); + if(entry) + { + entry->setRemoveEnabled(enabled); + } + } + setupTabChain(0); +} + +void SendCoinsDialog::removeEntry(SendCoinsEntry* entry) +{ + delete entry; + updateRemoveEnabled(); +} + +QWidget *SendCoinsDialog::setupTabChain(QWidget *prev) +{ + for(int i = 0; i < ui->entries->count(); ++i) + { + SendCoinsEntry *entry = qobject_cast<SendCoinsEntry*>(ui->entries->itemAt(i)->widget()); + if(entry) + { + prev = entry->setupTabChain(prev); + } + } + QWidget::setTabOrder(prev, ui->addButton); + QWidget::setTabOrder(ui->addButton, ui->sendButton); + return ui->sendButton; +} diff --git a/src/qt/sendcoinsdialog.h b/src/qt/sendcoinsdialog.h index 46814af466..0f90be8165 100644 --- a/src/qt/sendcoinsdialog.h +++ b/src/qt/sendcoinsdialog.h @@ -7,31 +7,37 @@ namespace Ui { class SendCoinsDialog; } class WalletModel; +class SendCoinsEntry; class SendCoinsDialog : public QDialog { Q_OBJECT public: - explicit SendCoinsDialog(QWidget *parent = 0, const QString &address = ""); + explicit SendCoinsDialog(QWidget *parent = 0); ~SendCoinsDialog(); void setModel(WalletModel *model); + // Qt messes up the tab chain by default in some cases (issue http://bugreports.qt.nokia.com/browse/QTBUG-10907) + // Hence we have to set it up manually + QWidget *setupTabChain(QWidget *prev); + public slots: void clear(); void reject(); void accept(); + void addEntry(); + void updateRemoveEnabled(); private: Ui::SendCoinsDialog *ui; WalletModel *model; private slots: - void on_payTo_textChanged(const QString &address); - void on_addressBookButton_clicked(); - void on_pasteButton_clicked(); void on_sendButton_clicked(); + + void removeEntry(SendCoinsEntry* entry); }; #endif // SENDCOINSDIALOG_H diff --git a/src/qt/sendcoinsentry.cpp b/src/qt/sendcoinsentry.cpp new file mode 100644 index 0000000000..6e87e9cf99 --- /dev/null +++ b/src/qt/sendcoinsentry.cpp @@ -0,0 +1,119 @@ +#include "sendcoinsentry.h" +#include "ui_sendcoinsentry.h" +#include "guiutil.h" +#include "addressbookpage.h" +#include "walletmodel.h" +#include "addresstablemodel.h" + +#include "qapplication.h" +#include "qclipboard.h" + +#include <QDebug> + +SendCoinsEntry::SendCoinsEntry(QWidget *parent) : + QFrame(parent), + ui(new Ui::SendCoinsEntry), + model(0) +{ + ui->setupUi(this); + +#if QT_VERSION >= 0x040700 + ui->payTo->setPlaceholderText(tr("Enter a Bitcoin address (e.g. 1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L)")); + ui->addAsLabel->setPlaceholderText(tr("Enter a label for this address to add it to your address book")); +#endif + setFocusPolicy(Qt::TabFocus); + setFocusProxy(ui->payTo); + + GUIUtil::setupAddressWidget(ui->payTo, this); +} + +SendCoinsEntry::~SendCoinsEntry() +{ + delete ui; +} + +void SendCoinsEntry::on_pasteButton_clicked() +{ + // Paste text from clipboard into recipient field + ui->payTo->setText(QApplication::clipboard()->text()); +} + +void SendCoinsEntry::on_addressBookButton_clicked() +{ + AddressBookPage dlg(AddressBookPage::ForSending, AddressBookPage::SendingTab, this); + dlg.setModel(model->getAddressTableModel()); + if(dlg.exec()) + { + ui->payTo->setText(dlg.getReturnValue()); + ui->payAmount->setFocus(); + } +} + +void SendCoinsEntry::on_payTo_textChanged(const QString &address) +{ + ui->addAsLabel->setText(model->getAddressTableModel()->labelForAddress(address)); +} + +void SendCoinsEntry::setModel(WalletModel *model) +{ + this->model = model; +} + +void SendCoinsEntry::setRemoveEnabled(bool enabled) +{ + ui->deleteButton->setEnabled(enabled); +} + +void SendCoinsEntry::clear() +{ + ui->payTo->clear(); + ui->addAsLabel->clear(); + ui->payAmount->setText(QString()); + ui->payTo->setFocus(); +} + +void SendCoinsEntry::on_deleteButton_clicked() +{ + emit removeEntry(this); +} + +bool SendCoinsEntry::validate() +{ + // Check input validity + bool retval = true; + + if(!ui->payAmount->validate()) + { + retval = false; + } + + if(!ui->payTo->hasAcceptableInput() || + (model && !model->validateAddress(ui->payTo->text()))) + { + ui->payTo->setValid(false); + retval = false; + } + + return retval; +} + +SendCoinsRecipient SendCoinsEntry::getValue() +{ + SendCoinsRecipient rv; + + rv.address = ui->payTo->text(); + rv.label = ui->addAsLabel->text(); + GUIUtil::parseMoney(ui->payAmount->text(), &rv.amount); + + return rv; +} + +QWidget *SendCoinsEntry::setupTabChain(QWidget *prev) +{ + QWidget::setTabOrder(prev, ui->payTo); + QWidget::setTabOrder(ui->payTo, ui->addressBookButton); + QWidget::setTabOrder(ui->addressBookButton, ui->pasteButton); + QWidget::setTabOrder(ui->pasteButton, ui->deleteButton); + QWidget::setTabOrder(ui->deleteButton, ui->addAsLabel); + return ui->payAmount->setupTabChain(ui->addAsLabel); +} diff --git a/src/qt/sendcoinsentry.h b/src/qt/sendcoinsentry.h new file mode 100644 index 0000000000..55fd12a14b --- /dev/null +++ b/src/qt/sendcoinsentry.h @@ -0,0 +1,45 @@ +#ifndef SENDCOINSENTRY_H +#define SENDCOINSENTRY_H + +#include <QFrame> + +namespace Ui { + class SendCoinsEntry; +} +class WalletModel; +class SendCoinsRecipient; + +class SendCoinsEntry : public QFrame +{ + Q_OBJECT + +public: + explicit SendCoinsEntry(QWidget *parent = 0); + ~SendCoinsEntry(); + + void setModel(WalletModel *model); + bool validate(); + SendCoinsRecipient getValue(); + // Qt messes up the tab chain by default in some cases (issue http://bugreports.qt.nokia.com/browse/QTBUG-10907) + // Hence we have to set it up manually + QWidget *setupTabChain(QWidget *prev); + +public slots: + void setRemoveEnabled(bool enabled); + void clear(); + +signals: + void removeEntry(SendCoinsEntry *entry); + +private slots: + void on_deleteButton_clicked(); + void on_payTo_textChanged(const QString &address); + void on_addressBookButton_clicked(); + void on_pasteButton_clicked(); + +private: + Ui::SendCoinsEntry *ui; + WalletModel *model; +}; + +#endif // SENDCOINSENTRY_H diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index afe095c977..6e4b814d12 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -7,6 +7,7 @@ #include "headers.h" #include <QTimer> +#include <QSet> WalletModel::WalletModel(CWallet *wallet, QObject *parent) : QObject(parent), wallet(wallet), optionsModel(0), addressTableModel(0), @@ -54,63 +55,105 @@ void WalletModel::update() addressTableModel->update(); } -WalletModel::StatusCode WalletModel::sendCoins(const QString &payTo, qint64 payAmount, const QString &addToAddressBookAs) +bool WalletModel::validateAddress(const QString &address) { uint160 hash160 = 0; - bool valid = false; - if(!AddressToHash160(payTo.toUtf8().constData(), hash160)) + return AddressToHash160(address.toStdString(), hash160); +} + +WalletModel::SendCoinsReturn WalletModel::sendCoins(const QList<SendCoinsRecipient> &recipients) +{ + qint64 total = 0; + QSet<QString> setAddress; + QString hex; + + if(recipients.empty()) { - return InvalidAddress; + return OK; } - if(payAmount <= 0) + // Pre-check input data for validity + foreach(const SendCoinsRecipient &rcp, recipients) { - return InvalidAmount; + uint160 hash160 = 0; + + if(!AddressToHash160(rcp.address.toUtf8().constData(), hash160)) + { + return InvalidAddress; + } + setAddress.insert(rcp.address); + + if(rcp.amount <= 0) + { + return InvalidAmount; + } + total += rcp.amount; } - if(payAmount > getBalance()) + if(recipients.size() > setAddress.size()) + { + return DuplicateAddress; + } + + if(total > getBalance()) { return AmountExceedsBalance; } - if((payAmount + nTransactionFee) > getBalance()) + if((total + nTransactionFee) > getBalance()) { - return AmountWithFeeExceedsBalance; + return SendCoinsReturn(AmountWithFeeExceedsBalance, nTransactionFee); } CRITICAL_BLOCK(cs_main) + CRITICAL_BLOCK(wallet->cs_mapWallet) { - // Send to bitcoin address + // Sendmany + std::vector<std::pair<CScript, int64> > vecSend; + foreach(const SendCoinsRecipient &rcp, recipients) + { + CScript scriptPubKey; + scriptPubKey.SetBitcoinAddress(rcp.address.toStdString()); + vecSend.push_back(make_pair(scriptPubKey, rcp.amount)); + } + CWalletTx wtx; - CScript scriptPubKey; - scriptPubKey << OP_DUP << OP_HASH160 << hash160 << OP_EQUALVERIFY << OP_CHECKSIG; + CReserveKey keyChange(wallet); + int64 nFeeRequired = 0; + bool fCreated = wallet->CreateTransaction(vecSend, wtx, keyChange, nFeeRequired); - std::string strError = wallet->SendMoney(scriptPubKey, payAmount, wtx, true); - if (strError == "") + if(!fCreated) { - // OK + if((total + nFeeRequired) > wallet->GetBalance()) + { + return SendCoinsReturn(AmountWithFeeExceedsBalance, nFeeRequired); + } + return TransactionCreationFailed; } - else if (strError == "ABORTED") + if(!ThreadSafeAskFee(nFeeRequired, tr("Sending...").toStdString(), NULL)) { return Aborted; } - else + if(!wallet->CommitTransaction(wtx, keyChange)) { - emit error(tr("Sending..."), QString::fromStdString(strError)); - return MiscError; + return TransactionCommitFailed; } + hex = QString::fromStdString(wtx.GetHash().GetHex()); } // Add addresses that we've sent to to the address book - std::string strAddress = payTo.toStdString(); - CRITICAL_BLOCK(wallet->cs_mapAddressBook) + foreach(const SendCoinsRecipient &rcp, recipients) { - if (!wallet->mapAddressBook.count(strAddress)) - wallet->SetAddressBookName(strAddress, addToAddressBookAs.toStdString()); + std::string strAddress = rcp.address.toStdString(); + CRITICAL_BLOCK(wallet->cs_mapAddressBook) + { + if (!wallet->mapAddressBook.count(strAddress)) + wallet->SetAddressBookName(strAddress, rcp.label.toStdString()); + } } - return OK; + return SendCoinsReturn(OK, 0, hex); } OptionsModel *WalletModel::getOptionsModel() diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index 1105fb03fa..af2cac4b31 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -8,6 +8,13 @@ class AddressTableModel; class TransactionTableModel; class CWallet; +struct SendCoinsRecipient +{ + QString address; + QString label; + qint64 amount; +}; + // Interface to a Bitcoin wallet class WalletModel : public QObject { @@ -22,6 +29,9 @@ public: InvalidAddress, AmountExceedsBalance, AmountWithFeeExceedsBalance, + DuplicateAddress, + TransactionCreationFailed, + TransactionCommitFailed, Aborted, MiscError }; @@ -34,8 +44,25 @@ public: qint64 getUnconfirmedBalance() const; int getNumTransactions() const; - /* Send coins */ - StatusCode sendCoins(const QString &payTo, qint64 payAmount, const QString &addToAddressBookAs=QString()); + // Check address for validity + bool validateAddress(const QString &address); + + // Return status record for SendCoins + // fee is used in case status is "AmountWithFeeExceedsBalance" + // hex is filled with the transaction hash if status is "OK" + struct SendCoinsReturn + { + SendCoinsReturn(StatusCode status, + qint64 fee=0, + QString hex=QString()): + status(status), fee(fee), hex(hex) {} + StatusCode status; + qint64 fee; + QString hex; + }; + + // Send coins to list of recipients + SendCoinsReturn sendCoins(const QList<SendCoinsRecipient> &recipients); private: CWallet *wallet; |