diff options
Diffstat (limited to 'src/qt')
46 files changed, 4430 insertions, 0 deletions
diff --git a/src/qt/aboutdialog.cpp b/src/qt/aboutdialog.cpp new file mode 100644 index 0000000000..13347961dd --- /dev/null +++ b/src/qt/aboutdialog.cpp @@ -0,0 +1,22 @@ +#include "aboutdialog.h" +#include "ui_aboutdialog.h" + +#include "util.h" + +AboutDialog::AboutDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::AboutDialog) +{ + ui->setupUi(this); + ui->versionLabel->setText(QString::fromStdString(FormatFullVersion())); +} + +AboutDialog::~AboutDialog() +{ + delete ui; +} + +void AboutDialog::on_buttonBox_accepted() +{ + close(); +} diff --git a/src/qt/aboutdialog.h b/src/qt/aboutdialog.h new file mode 100644 index 0000000000..827cc741c3 --- /dev/null +++ b/src/qt/aboutdialog.h @@ -0,0 +1,25 @@ +#ifndef ABOUTDIALOG_H +#define ABOUTDIALOG_H + +#include <QDialog> + +namespace Ui { + class AboutDialog; +} + +class AboutDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AboutDialog(QWidget *parent = 0); + ~AboutDialog(); + +private: + Ui::AboutDialog *ui; + +private slots: + void on_buttonBox_accepted(); +}; + +#endif // ABOUTDIALOG_H diff --git a/src/qt/addressbookdialog.cpp b/src/qt/addressbookdialog.cpp new file mode 100644 index 0000000000..90950a6441 --- /dev/null +++ b/src/qt/addressbookdialog.cpp @@ -0,0 +1,168 @@ +#include "addressbookdialog.h" +#include "ui_addressbookdialog.h" + +#include "addresstablemodel.h" +#include "editaddressdialog.h" + +#include <QSortFilterProxyModel> +#include <QClipboard> +#include <QDebug> + +AddressBookDialog::AddressBookDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::AddressBookDialog), + model(0) +{ + ui->setupUi(this); +} + +AddressBookDialog::~AddressBookDialog() +{ + delete ui; +} + +void AddressBookDialog::setModel(AddressTableModel *model) +{ + this->model = model; + /* Refresh list from core */ + model->updateList(); + + /* Receive filter */ + QSortFilterProxyModel *receive_model = new QSortFilterProxyModel(this); + receive_model->setSourceModel(model); + receive_model->setDynamicSortFilter(true); + receive_model->setFilterRole(AddressTableModel::TypeRole); + receive_model->setFilterFixedString(AddressTableModel::Receive); + ui->receiveTableView->setModel(receive_model); + + /* Send filter */ + QSortFilterProxyModel *send_model = new QSortFilterProxyModel(this); + send_model->setSourceModel(model); + send_model->setDynamicSortFilter(true); + send_model->setFilterRole(AddressTableModel::TypeRole); + send_model->setFilterFixedString(AddressTableModel::Send); + ui->sendTableView->setModel(send_model); + + /* Set column widths */ + ui->receiveTableView->horizontalHeader()->resizeSection( + AddressTableModel::Address, 320); + ui->receiveTableView->horizontalHeader()->setResizeMode( + AddressTableModel::Label, QHeaderView::Stretch); + ui->sendTableView->horizontalHeader()->resizeSection( + AddressTableModel::Address, 320); + ui->sendTableView->horizontalHeader()->setResizeMode( + AddressTableModel::Label, QHeaderView::Stretch); +} + +void AddressBookDialog::setTab(int tab) +{ + ui->tabWidget->setCurrentIndex(tab); +} + +QTableView *AddressBookDialog::getCurrentTable() +{ + switch(ui->tabWidget->currentIndex()) + { + case SendingTab: + return ui->sendTableView; + case ReceivingTab: + return ui->receiveTableView; + default: + return 0; + } +} + +void AddressBookDialog::on_copyToClipboard_clicked() +{ + /* Copy currently selected address to clipboard + (or nothing, if nothing selected) + */ + QTableView *table = getCurrentTable(); + QModelIndexList indexes = table->selectionModel()->selectedRows(AddressTableModel::Address); + + foreach (QModelIndex index, indexes) + { + QVariant address = index.data(); + QApplication::clipboard()->setText(address.toString()); + } +} + +void AddressBookDialog::on_editButton_clicked() +{ + QModelIndexList indexes = getCurrentTable()->selectionModel()->selectedRows(); + if(indexes.isEmpty()) + { + return; + } + /* Map selected index to source address book model */ + QAbstractProxyModel *proxy_model = static_cast<QAbstractProxyModel*>(getCurrentTable()->model()); + QModelIndex selected = proxy_model->mapToSource(indexes.at(0)); + + /* Double click also triggers edit button */ + EditAddressDialog dlg( + ui->tabWidget->currentIndex() == SendingTab ? + EditAddressDialog::EditSendingAddress : + EditAddressDialog::EditReceivingAddress); + dlg.setModel(model); + dlg.loadRow(selected.row()); + if(dlg.exec()) + { + dlg.saveCurrentRow(); + } +} + +void AddressBookDialog::on_newAddressButton_clicked() +{ + EditAddressDialog dlg( + ui->tabWidget->currentIndex() == SendingTab ? + EditAddressDialog::NewSendingAddress : + EditAddressDialog::NewReceivingAddress); + dlg.setModel(model); + if(dlg.exec()) + { + dlg.saveCurrentRow(); + } +} + +void AddressBookDialog::on_tabWidget_currentChanged(int index) +{ + switch(index) + { + case SendingTab: + ui->deleteButton->setEnabled(true); + break; + case ReceivingTab: + ui->deleteButton->setEnabled(false); + break; + } +} + +void AddressBookDialog::on_deleteButton_clicked() +{ + QTableView *table = getCurrentTable(); + QModelIndexList indexes = table->selectionModel()->selectedRows(); + if(!indexes.isEmpty()) + { + table->model()->removeRow(indexes.at(0).row()); + } +} + +void AddressBookDialog::on_buttonBox_accepted() +{ + QTableView *table = getCurrentTable(); + QModelIndexList indexes = table->selectionModel()->selectedRows(AddressTableModel::Address); + + foreach (QModelIndex index, indexes) + { + QVariant address = table->model()->data(index); + returnValue = address.toString(); + } + if(!returnValue.isEmpty()) + { + accept(); + } + else + { + reject(); + } +} diff --git a/src/qt/addressbookdialog.h b/src/qt/addressbookdialog.h new file mode 100644 index 0000000000..bf7c2a65a6 --- /dev/null +++ b/src/qt/addressbookdialog.h @@ -0,0 +1,47 @@ +#ifndef ADDRESSBOOKDIALOG_H +#define ADDRESSBOOKDIALOG_H + +#include <QDialog> + +namespace Ui { + class AddressBookDialog; +} +class AddressTableModel; + +QT_BEGIN_NAMESPACE +class QTableView; +QT_END_NAMESPACE + +class AddressBookDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AddressBookDialog(QWidget *parent = 0); + ~AddressBookDialog(); + + enum { + SendingTab = 0, + ReceivingTab = 1 + } Tabs; + + void setModel(AddressTableModel *model); + void setTab(int tab); + const QString &getReturnValue() const { return returnValue; } +private: + Ui::AddressBookDialog *ui; + AddressTableModel *model; + QString returnValue; + + QTableView *getCurrentTable(); + +private slots: + void on_buttonBox_accepted(); + void on_deleteButton_clicked(); + void on_tabWidget_currentChanged(int index); + void on_newAddressButton_clicked(); + void on_editButton_clicked(); + void on_copyToClipboard_clicked(); +}; + +#endif // ADDRESSBOOKDIALOG_H diff --git a/src/qt/addresstablemodel.cpp b/src/qt/addresstablemodel.cpp new file mode 100644 index 0000000000..91b87fb7f1 --- /dev/null +++ b/src/qt/addresstablemodel.cpp @@ -0,0 +1,245 @@ +#include "addresstablemodel.h" +#include "guiutil.h" +#include "main.h" + +#include <QFont> + +const QString AddressTableModel::Send = "S"; +const QString AddressTableModel::Receive = "R"; + +struct AddressTableEntry +{ + enum Type { + Sending, + Receiving + }; + + Type type; + QString label; + QString address; + + AddressTableEntry() {} + AddressTableEntry(Type type, const QString &label, const QString &address): + type(type), label(label), address(address) {} +}; + +/* Private implementation */ +struct AddressTablePriv +{ + QList<AddressTableEntry> cachedAddressTable; + + void refreshAddressTable() + { + cachedAddressTable.clear(); + + CRITICAL_BLOCK(cs_mapKeys) + CRITICAL_BLOCK(cs_mapAddressBook) + { + BOOST_FOREACH(const PAIRTYPE(std::string, std::string)& item, mapAddressBook) + { + std::string strAddress = item.first; + std::string strName = item.second; + uint160 hash160; + bool fMine = (AddressToHash160(strAddress, hash160) && mapPubKeys.count(hash160)); + cachedAddressTable.append(AddressTableEntry(fMine ? AddressTableEntry::Receiving : AddressTableEntry::Sending, + QString::fromStdString(strName), + QString::fromStdString(strAddress))); + } + } + } + + int size() + { + return cachedAddressTable.size(); + } + + AddressTableEntry *index(int idx) + { + if(idx >= 0 && idx < cachedAddressTable.size()) + { + return &cachedAddressTable[idx]; + } + else + { + return 0; + } + } +}; + +AddressTableModel::AddressTableModel(QObject *parent) : + QAbstractTableModel(parent),priv(0) +{ + columns << tr("Label") << tr("Address"); + priv = new AddressTablePriv(); + priv->refreshAddressTable(); +} + +AddressTableModel::~AddressTableModel() +{ + delete priv; +} + +int AddressTableModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return priv->size(); +} + +int AddressTableModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return columns.length(); +} + +QVariant AddressTableModel::data(const QModelIndex &index, int role) const +{ + if(!index.isValid()) + return QVariant(); + + AddressTableEntry *rec = static_cast<AddressTableEntry*>(index.internalPointer()); + + if(role == Qt::DisplayRole || role == Qt::EditRole) + { + switch(index.column()) + { + case Label: + return rec->label; + case Address: + return rec->address; + } + } + else if (role == Qt::FontRole) + { + if(index.column() == Address) + { + return GUIUtil::bitcoinAddressFont(); + } + } + else if (role == TypeRole) + { + switch(rec->type) + { + case AddressTableEntry::Sending: + return Send; + case AddressTableEntry::Receiving: + return Receive; + default: break; + } + } + return QVariant(); +} + +bool AddressTableModel::setData(const QModelIndex & index, const QVariant & value, int role) +{ + if(!index.isValid()) + return false; + AddressTableEntry *rec = static_cast<AddressTableEntry*>(index.internalPointer()); + + if(role == Qt::EditRole) + { + switch(index.column()) + { + case Label: + SetAddressBookName(rec->address.toStdString(), value.toString().toStdString()); + rec->label = value.toString(); + break; + case Address: + /* Double-check that we're not overwriting receiving address */ + if(rec->type == AddressTableEntry::Sending) + { + /* Remove old entry */ + CWalletDB().EraseName(rec->address.toStdString()); + /* Add new entry with new address */ + SetAddressBookName(value.toString().toStdString(), rec->label.toStdString()); + + rec->address = value.toString(); + } + break; + } + emit dataChanged(index, index); + + return true; + } + return false; +} + +QVariant AddressTableModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal) + { + if(role == Qt::DisplayRole) + { + return columns[section]; + } + } + return QVariant(); +} + +QModelIndex AddressTableModel::index(int row, int column, const QModelIndex & parent) const +{ + Q_UNUSED(parent); + AddressTableEntry *data = priv->index(row); + if(data) + { + return createIndex(row, column, priv->index(row)); + } + else + { + return QModelIndex(); + } +} + +void AddressTableModel::updateList() +{ + /* Update internal model from Bitcoin core */ + beginResetModel(); + priv->refreshAddressTable(); + endResetModel(); +} + +QString AddressTableModel::addRow(const QString &type, const QString &label, const QString &address) +{ + std::string strLabel = label.toStdString(); + std::string strAddress = address.toStdString(); + + if(type == Send) + { + /* Check for duplicate */ + CRITICAL_BLOCK(cs_mapAddressBook) + { + if(mapAddressBook.count(strAddress)) + { + return QString(); + } + } + } + else if(type == Receive) + { + /* Generate a new address to associate with given label */ + strAddress = PubKeyToAddress(GetKeyFromKeyPool()); + } + else + { + return QString(); + } + /* Add entry and update list */ + SetAddressBookName(strAddress, strLabel); + updateList(); + return QString::fromStdString(strAddress); +} + +bool AddressTableModel::removeRows(int row, int count, const QModelIndex & parent) +{ + Q_UNUSED(parent); + AddressTableEntry *rec = priv->index(row); + if(count != 1 || !rec || rec->type == AddressTableEntry::Receiving) + { + /* Can only remove one row at a time, and cannot remove rows not in model. + Also refuse to remove receiving addresses. + */ + return false; + } + CWalletDB().EraseName(rec->address.toStdString()); + updateList(); + return true; +} diff --git a/src/qt/addresstablemodel.h b/src/qt/addresstablemodel.h new file mode 100644 index 0000000000..8799414334 --- /dev/null +++ b/src/qt/addresstablemodel.h @@ -0,0 +1,54 @@ +#ifndef ADDRESSTABLEMODEL_H +#define ADDRESSTABLEMODEL_H + +#include <QAbstractTableModel> +#include <QStringList> + +class AddressTablePriv; + +class AddressTableModel : public QAbstractTableModel +{ + Q_OBJECT +public: + explicit AddressTableModel(QObject *parent = 0); + ~AddressTableModel(); + + enum ColumnIndex { + Label = 0, /* User specified label */ + Address = 1 /* Bitcoin address */ + }; + + enum { + TypeRole = Qt::UserRole + } RoleIndex; + + static const QString Send; /* Send addres */ + static const QString Receive; /* Receive address */ + + /* Overridden methods from QAbstractTableModel */ + int rowCount(const QModelIndex &parent) const; + int columnCount(const QModelIndex &parent) const; + QVariant data(const QModelIndex &index, int role) const; + bool setData(const QModelIndex & index, const QVariant & value, int role); + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + QModelIndex index(int row, int column, const QModelIndex & parent) const; + bool removeRows(int row, int count, const QModelIndex & parent = QModelIndex()); + + /* Add an address to the model. + Returns the added address on success, and an empty string otherwise. + */ + QString addRow(const QString &type, const QString &label, const QString &address); + + /* Update address list from core. Invalidates any indices. + */ + void updateList(); +private: + AddressTablePriv *priv; + QStringList columns; +signals: + +public slots: + +}; + +#endif // ADDRESSTABLEMODEL_H diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp new file mode 100644 index 0000000000..e003267426 --- /dev/null +++ b/src/qt/bitcoin.cpp @@ -0,0 +1,122 @@ +/* + * W.J. van der Laan 2011 + */ +#include "bitcoingui.h" +#include "clientmodel.h" +#include "util.h" +#include "init.h" +#include "main.h" +#include "externui.h" + +#include <QApplication> +#include <QMessageBox> +#include <QThread> + +// Need a global reference for the notifications to find the GUI +BitcoinGUI *guiref; + +int MyMessageBox(const std::string& message, const std::string& caption, int style, wxWindow* parent, int x, int y) +{ + // Message from main thread + if(guiref) + { + guiref->error(QString::fromStdString(caption), + QString::fromStdString(message)); + } + else + { + QMessageBox::critical(0, QString::fromStdString(caption), + QString::fromStdString(message), + QMessageBox::Ok, QMessageBox::Ok); + } + return 4; +} + +int ThreadSafeMessageBox(const std::string& message, const std::string& caption, int style, wxWindow* parent, int x, int y) +{ + // Message from network thread + if(guiref) + { + QMetaObject::invokeMethod(guiref, "error", Qt::QueuedConnection, + Q_ARG(QString, QString::fromStdString(caption)), + Q_ARG(QString, QString::fromStdString(message))); + } + else + { + printf("%s: %s\n", caption.c_str(), message.c_str()); + fprintf(stderr, "%s: %s\n", caption.c_str(), message.c_str()); + } + return 4; +} + +bool ThreadSafeAskFee(int64 nFeeRequired, const std::string& strCaption, wxWindow* parent) +{ + if(!guiref) + return false; + if(nFeeRequired < MIN_TX_FEE || nFeeRequired <= nTransactionFee || fDaemon) + return true; + bool payFee = false; + + /* Call slot on GUI thread. + If called from another thread, use a blocking QueuedConnection. + */ + Qt::ConnectionType connectionType = Qt::DirectConnection; + if(QThread::currentThread() != QCoreApplication::instance()->thread()) + { + connectionType = Qt::BlockingQueuedConnection; + } + + QMetaObject::invokeMethod(guiref, "askFee", connectionType, + Q_ARG(qint64, nFeeRequired), + Q_ARG(bool*, &payFee)); + + return payFee; +} + +void CalledSetStatusBar(const std::string& strText, int nField) +{ + // Only used for built-in mining, which is disabled, simple ignore +} + +void UIThreadCall(boost::function0<void> fn) +{ + // Only used for built-in mining, which is disabled, simple ignore +} + +void MainFrameRepaint() +{ +} + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + app.setQuitOnLastWindowClosed(false); + + try + { + if(AppInit2(argc, argv)) + { + BitcoinGUI window; + ClientModel model; + guiref = &window; + window.setModel(&model); + + window.show(); + + int retval = app.exec(); + + guiref = 0; + Shutdown(NULL); + + return retval; + } + else + { + return 1; + } + } catch (std::exception& e) { + PrintException(&e, "Runaway exception"); + } catch (...) { + PrintException(NULL, "Runaway exception"); + } +} diff --git a/src/qt/bitcoin.qrc b/src/qt/bitcoin.qrc new file mode 100644 index 0000000000..80904b341c --- /dev/null +++ b/src/qt/bitcoin.qrc @@ -0,0 +1,12 @@ +<RCC> + <qresource prefix="/icons"> + <file alias="address-book">res/icons/address-book.png</file> + <file alias="bitcoin">res/icons/bitcoin.png</file> + <file alias="quit">res/icons/quit.png</file> + <file alias="send">res/icons/send.png</file> + <file alias="toolbar">res/icons/toolbar.png</file> + </qresource> + <qresource prefix="/images"> + <file alias="about">res/images/about.png</file> + </qresource> +</RCC> diff --git a/src/qt/bitcoinaddressvalidator.cpp b/src/qt/bitcoinaddressvalidator.cpp new file mode 100644 index 0000000000..761a266933 --- /dev/null +++ b/src/qt/bitcoinaddressvalidator.cpp @@ -0,0 +1,63 @@ +#include "bitcoinaddressvalidator.h" + +#include <QDebug> + +/* Base58 characters are: + "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + + This is: + - All numbers except for '0' + - All uppercase letters except for 'I' and 'O' + - All lowercase letters except for 'l' + + User friendly Base58 input can map + - 'l' and 'I' to '1' + - '0' and 'O' to 'o' +*/ + +BitcoinAddressValidator::BitcoinAddressValidator(QObject *parent) : + QValidator(parent) +{ +} + +QValidator::State BitcoinAddressValidator::validate(QString &input, int &pos) const +{ + /* Correction */ + for(int idx=0; idx<input.size(); ++idx) + { + switch(input.at(idx).unicode()) + { + case 'l': + case 'I': + input[idx] = QChar('1'); + break; + case '0': + case 'O': + input[idx] = QChar('o'); + break; + default: + break; + } + } + + /* Validation */ + QValidator::State state = QValidator::Acceptable; + for(int idx=0; idx<input.size(); ++idx) + { + int ch = input.at(idx).unicode(); + + if(((ch >= '0' && ch<='9') || + (ch >= 'a' && ch<='z') || + (ch >= 'A' && ch<='Z')) && + ch != 'l' && ch != 'I' && ch != '0' && ch != 'O') + { + /* Alphanumeric and not a 'forbidden' character */ + } + else + { + state = QValidator::Invalid; + } + } + + return state; +} diff --git a/src/qt/bitcoinaddressvalidator.h b/src/qt/bitcoinaddressvalidator.h new file mode 100644 index 0000000000..73f6ea1f61 --- /dev/null +++ b/src/qt/bitcoinaddressvalidator.h @@ -0,0 +1,24 @@ +#ifndef BITCOINADDRESSVALIDATOR_H +#define BITCOINADDRESSVALIDATOR_H + +#include <QRegExpValidator> + +/* Base48 entry widget validator. + Corrects near-miss characters and refuses characters that are no part of base48. + */ +class BitcoinAddressValidator : public QValidator +{ + Q_OBJECT +public: + explicit BitcoinAddressValidator(QObject *parent = 0); + + State validate(QString &input, int &pos) const; + + static const int MaxAddressLength = 34; +signals: + +public slots: + +}; + +#endif // BITCOINADDRESSVALIDATOR_H diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp new file mode 100644 index 0000000000..c08476cec3 --- /dev/null +++ b/src/qt/bitcoingui.cpp @@ -0,0 +1,448 @@ +/* + * Qt4 bitcoin GUI. + * + * W.J. van der Laan 2011 + */ +#include "bitcoingui.h" +#include "transactiontablemodel.h" +#include "addressbookdialog.h" +#include "sendcoinsdialog.h" +#include "optionsdialog.h" +#include "aboutdialog.h" +#include "clientmodel.h" +#include "guiutil.h" +#include "editaddressdialog.h" +#include "optionsmodel.h" +#include "transactiondescdialog.h" + +#include "main.h" + +#include <QApplication> +#include <QMainWindow> +#include <QMenuBar> +#include <QMenu> +#include <QIcon> +#include <QTabWidget> +#include <QVBoxLayout> +#include <QWidget> +#include <QToolBar> +#include <QStatusBar> +#include <QLabel> +#include <QTableView> +#include <QLineEdit> +#include <QPushButton> +#include <QHeaderView> +#include <QLocale> +#include <QSortFilterProxyModel> +#include <QClipboard> +#include <QMessageBox> + +#include <QDebug> + +#include <iostream> + +BitcoinGUI::BitcoinGUI(QWidget *parent): + QMainWindow(parent), trayIcon(0) +{ + resize(850, 550); + setWindowTitle(tr("Bitcoin")); + setWindowIcon(QIcon(":icons/bitcoin")); + + createActions(); + + // Menus + QMenu *file = menuBar()->addMenu("&File"); + file->addAction(sendcoins); + file->addSeparator(); + file->addAction(quit); + + QMenu *settings = menuBar()->addMenu("&Settings"); + settings->addAction(receivingAddresses); + settings->addAction(options); + + QMenu *help = menuBar()->addMenu("&Help"); + help->addAction(about); + + // Toolbar + QToolBar *toolbar = addToolBar("Main toolbar"); + toolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + toolbar->addAction(sendcoins); + toolbar->addAction(addressbook); + + // Address: <address>: New... : Paste to clipboard + QHBoxLayout *hbox_address = new QHBoxLayout(); + hbox_address->addWidget(new QLabel(tr("Your Bitcoin Address:"))); + address = new QLineEdit(); + address->setReadOnly(true); + address->setFont(GUIUtil::bitcoinAddressFont()); + address->setToolTip(tr("Your current default receiving address")); + hbox_address->addWidget(address); + + QPushButton *button_new = new QPushButton(tr("&New...")); + button_new->setToolTip(tr("Create new receiving address")); + QPushButton *button_clipboard = new QPushButton(tr("&Copy to clipboard")); + button_clipboard->setToolTip(tr("Copy current receiving address to the system clipboard")); + hbox_address->addWidget(button_new); + hbox_address->addWidget(button_clipboard); + + // Balance: <balance> + QHBoxLayout *hbox_balance = new QHBoxLayout(); + hbox_balance->addWidget(new QLabel(tr("Balance:"))); + hbox_balance->addSpacing(5);/* Add some spacing between the label and the text */ + + labelBalance = new QLabel(); + labelBalance->setFont(QFont("Monospace")); + labelBalance->setToolTip(tr("Your current balance")); + hbox_balance->addWidget(labelBalance); + hbox_balance->addStretch(1); + + QVBoxLayout *vbox = new QVBoxLayout(); + vbox->addLayout(hbox_address); + vbox->addLayout(hbox_balance); + + vbox->addWidget(createTabs()); + + QWidget *centralwidget = new QWidget(this); + centralwidget->setLayout(vbox); + setCentralWidget(centralwidget); + + // Create status bar + statusBar(); + + labelConnections = new QLabel(); + labelConnections->setFrameStyle(QFrame::Panel | QFrame::Sunken); + labelConnections->setMinimumWidth(130); + labelConnections->setToolTip(tr("Number of connections to other clients")); + + labelBlocks = new QLabel(); + labelBlocks->setFrameStyle(QFrame::Panel | QFrame::Sunken); + labelBlocks->setMinimumWidth(130); + labelBlocks->setToolTip(tr("Number of blocks in the block chain")); + + labelTransactions = new QLabel(); + labelTransactions->setFrameStyle(QFrame::Panel | QFrame::Sunken); + labelTransactions->setMinimumWidth(130); + labelTransactions->setToolTip(tr("Number of transactions in your wallet")); + + statusBar()->addPermanentWidget(labelConnections); + statusBar()->addPermanentWidget(labelBlocks); + statusBar()->addPermanentWidget(labelTransactions); + + // Action bindings + connect(button_new, SIGNAL(clicked()), this, SLOT(newAddressClicked())); + connect(button_clipboard, SIGNAL(clicked()), this, SLOT(copyClipboardClicked())); + + createTrayIcon(); +} + +void BitcoinGUI::createActions() +{ + quit = new QAction(QIcon(":/icons/quit"), tr("&Exit"), this); + quit->setToolTip(tr("Quit application")); + sendcoins = new QAction(QIcon(":/icons/send"), tr("&Send coins"), this); + sendcoins->setToolTip(tr("Send coins to a bitcoin address")); + addressbook = new QAction(QIcon(":/icons/address-book"), tr("&Address Book"), this); + addressbook->setToolTip(tr("Edit the list of stored addresses and labels")); + about = new QAction(QIcon(":/icons/bitcoin"), tr("&About"), this); + about->setToolTip(tr("Show information about Bitcoin")); + receivingAddresses = new QAction(QIcon(":/icons/receiving-addresses"), tr("Your &Receiving Addresses..."), this); + receivingAddresses->setToolTip(tr("Show the list of receiving addresses and edit their labels")); + options = new QAction(QIcon(":/icons/options"), tr("&Options..."), this); + options->setToolTip(tr("Modify configuration options for bitcoin")); + openBitcoin = new QAction(QIcon(":/icons/bitcoin"), "Open &Bitcoin", this); + openBitcoin->setToolTip(tr("Show the Bitcoin window")); + + connect(quit, SIGNAL(triggered()), qApp, SLOT(quit())); + connect(sendcoins, SIGNAL(triggered()), this, SLOT(sendcoinsClicked())); + connect(addressbook, SIGNAL(triggered()), this, SLOT(addressbookClicked())); + connect(receivingAddresses, SIGNAL(triggered()), this, SLOT(receivingAddressesClicked())); + connect(options, SIGNAL(triggered()), this, SLOT(optionsClicked())); + connect(about, SIGNAL(triggered()), this, SLOT(aboutClicked())); + connect(openBitcoin, SIGNAL(triggered()), this, SLOT(show())); +} + +void BitcoinGUI::setModel(ClientModel *model) +{ + this->model = model; + + // Keep up to date with client + setBalance(model->getBalance()); + connect(model, SIGNAL(balanceChanged(qint64)), this, SLOT(setBalance(qint64))); + + setNumConnections(model->getNumConnections()); + connect(model, SIGNAL(numConnectionsChanged(int)), this, SLOT(setNumConnections(int))); + + setNumTransactions(model->getNumTransactions()); + connect(model, SIGNAL(numTransactionsChanged(int)), this, SLOT(setNumTransactions(int))); + + setNumBlocks(model->getNumBlocks()); + connect(model, SIGNAL(numBlocksChanged(int)), this, SLOT(setNumBlocks(int))); + + setAddress(model->getAddress()); + connect(model, SIGNAL(addressChanged(QString)), this, SLOT(setAddress(QString))); + + // Report errors from network/worker thread + connect(model, SIGNAL(error(QString,QString)), this, SLOT(error(QString,QString))); + + // Put transaction list in tabs + setTabsModel(model->getTransactionTableModel()); +} + +void BitcoinGUI::createTrayIcon() +{ + QMenu *trayIconMenu = new QMenu(this); + trayIconMenu->addAction(openBitcoin); + trayIconMenu->addAction(sendcoins); + trayIconMenu->addAction(options); + trayIconMenu->addSeparator(); + trayIconMenu->addAction(quit); + + trayIcon = new QSystemTrayIcon(this); + trayIcon->setContextMenu(trayIconMenu); + trayIcon->setIcon(QIcon(":/icons/toolbar")); + connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), + this, SLOT(trayIconActivated(QSystemTrayIcon::ActivationReason))); + trayIcon->show(); +} + +void BitcoinGUI::trayIconActivated(QSystemTrayIcon::ActivationReason reason) +{ + if(reason == QSystemTrayIcon::DoubleClick) + { + // Doubleclick on system tray icon triggers "open bitcoin" + openBitcoin->trigger(); + } +} + +QWidget *BitcoinGUI::createTabs() +{ + QStringList tab_labels; + tab_labels << tr("All transactions") + << tr("Sent/Received") + << tr("Sent") + << tr("Received"); + + QTabWidget *tabs = new QTabWidget(this); + for(int i = 0; i < tab_labels.size(); ++i) + { + QTableView *view = new QTableView(this); + tabs->addTab(view, tab_labels.at(i)); + + connect(view, SIGNAL(doubleClicked(const QModelIndex&)), this, SLOT(transactionDetails(const QModelIndex&))); + transactionViews.append(view); + } + + return tabs; +} + +void BitcoinGUI::setTabsModel(QAbstractItemModel *transaction_model) +{ + QStringList tab_filters; + tab_filters << "^." + << "^["+TransactionTableModel::Sent+TransactionTableModel::Received+"]" + << "^["+TransactionTableModel::Sent+"]" + << "^["+TransactionTableModel::Received+"]"; + + for(int i = 0; i < transactionViews.size(); ++i) + { + QSortFilterProxyModel *proxy_model = new QSortFilterProxyModel(this); + proxy_model->setSourceModel(transaction_model); + proxy_model->setDynamicSortFilter(true); + proxy_model->setFilterRole(TransactionTableModel::TypeRole); + proxy_model->setFilterRegExp(QRegExp(tab_filters.at(i))); + proxy_model->setSortRole(Qt::EditRole); + + QTableView *transaction_table = transactionViews.at(i); + transaction_table->setModel(proxy_model); + transaction_table->setSelectionBehavior(QAbstractItemView::SelectRows); + transaction_table->setSelectionMode(QAbstractItemView::ExtendedSelection); + transaction_table->setSortingEnabled(true); + transaction_table->sortByColumn(TransactionTableModel::Status, Qt::DescendingOrder); + transaction_table->verticalHeader()->hide(); + + transaction_table->horizontalHeader()->resizeSection( + TransactionTableModel::Status, 120); + transaction_table->horizontalHeader()->resizeSection( + TransactionTableModel::Date, 120); + transaction_table->horizontalHeader()->setResizeMode( + TransactionTableModel::Description, QHeaderView::Stretch); + transaction_table->horizontalHeader()->resizeSection( + TransactionTableModel::Debit, 79); + transaction_table->horizontalHeader()->resizeSection( + TransactionTableModel::Credit, 79); + } + + connect(transaction_model, SIGNAL(rowsInserted(const QModelIndex &, int, int)), + this, SLOT(incomingTransaction(const QModelIndex &, int, int))); +} + +void BitcoinGUI::sendcoinsClicked() +{ + SendCoinsDialog dlg; + dlg.setModel(model); + dlg.exec(); +} + +void BitcoinGUI::addressbookClicked() +{ + AddressBookDialog dlg; + dlg.setModel(model->getAddressTableModel()); + dlg.setTab(AddressBookDialog::SendingTab); + dlg.exec(); +} + +void BitcoinGUI::receivingAddressesClicked() +{ + AddressBookDialog dlg; + dlg.setModel(model->getAddressTableModel()); + dlg.setTab(AddressBookDialog::ReceivingTab); + dlg.exec(); +} + +void BitcoinGUI::optionsClicked() +{ + OptionsDialog dlg; + dlg.setModel(model->getOptionsModel()); + dlg.exec(); +} + +void BitcoinGUI::aboutClicked() +{ + AboutDialog dlg; + dlg.exec(); +} + +void BitcoinGUI::newAddressClicked() +{ + EditAddressDialog dlg(EditAddressDialog::NewReceivingAddress); + dlg.setModel(model->getAddressTableModel()); + if(dlg.exec()) + { + QString newAddress = dlg.saveCurrentRow(); + // Set returned address as new default addres + if(!newAddress.isEmpty()) + { + model->setAddress(newAddress); + } + } +} + +void BitcoinGUI::copyClipboardClicked() +{ + // Copy text in address to clipboard + QApplication::clipboard()->setText(address->text()); +} + +void BitcoinGUI::setBalance(qint64 balance) +{ + labelBalance->setText(QString::fromStdString(FormatMoney(balance))); +} + +void BitcoinGUI::setAddress(const QString &addr) +{ + address->setText(addr); +} + +void BitcoinGUI::setNumConnections(int count) +{ + labelConnections->setText(QLocale::system().toString(count)+" "+tr("connections(s)", "", count)); +} + +void BitcoinGUI::setNumBlocks(int count) +{ + labelBlocks->setText(QLocale::system().toString(count)+" "+tr("block(s)", "", count)); +} + +void BitcoinGUI::setNumTransactions(int count) +{ + labelTransactions->setText(QLocale::system().toString(count)+" "+tr("transaction(s)", "", count)); +} + +void BitcoinGUI::error(const QString &title, const QString &message) +{ + // Report errors from network/worker thread + if(trayIcon->supportsMessages()) + { + // Show as "balloon" message if possible + trayIcon->showMessage(title, message, QSystemTrayIcon::Critical); + } + else + { + // Fall back to old fashioned popup dialog if not + QMessageBox::critical(this, title, + message, + QMessageBox::Ok, QMessageBox::Ok); + } +} + +void BitcoinGUI::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::WindowStateChange) + { + if(model->getOptionsModel()->getMinimizeToTray()) + { + if (isMinimized()) + { + hide(); + e->ignore(); + } + else + { + e->accept(); + } + } + } + QMainWindow::changeEvent(e); +} + +void BitcoinGUI::closeEvent(QCloseEvent *event) +{ + if(!model->getOptionsModel()->getMinimizeToTray() && + !model->getOptionsModel()->getMinimizeOnClose()) + { + qApp->quit(); + } + QMainWindow::closeEvent(event); +} + +void BitcoinGUI::askFee(qint64 nFeeRequired, bool *payFee) +{ + QString strMessage = + tr("This transaction is over the size limit. You can still send it for a fee of %1, " + "which goes to the nodes that process your transaction and helps to support the network. " + "Do you want to pay the fee?").arg(QString::fromStdString(FormatMoney(nFeeRequired))); + QMessageBox::StandardButton retval = QMessageBox::question( + this, tr("Sending..."), strMessage, + QMessageBox::Yes|QMessageBox::Cancel, QMessageBox::Yes); + *payFee = (retval == QMessageBox::Yes); +} + +void BitcoinGUI::transactionDetails(const QModelIndex& idx) +{ + /* A transaction is doubleclicked */ + TransactionDescDialog dlg(idx); + dlg.exec(); +} + +void BitcoinGUI::incomingTransaction(const QModelIndex & parent, int start, int end) +{ + TransactionTableModel *ttm = model->getTransactionTableModel(); + qint64 credit = ttm->index(start, TransactionTableModel::Credit, parent) + .data(Qt::EditRole).toULongLong(); + qint64 debit = ttm->index(start, TransactionTableModel::Debit, parent) + .data(Qt::EditRole).toULongLong(); + if((credit+debit)>0) + { + /* On incoming transaction, make an info balloon */ + QString date = ttm->index(start, TransactionTableModel::Date, parent) + .data().toString(); + QString description = ttm->index(start, TransactionTableModel::Description, parent) + .data().toString(); + + trayIcon->showMessage(tr("Incoming transaction"), + "Date: " + date + "\n" + + "Amount: " + QString::fromStdString(FormatMoney(credit+debit, true)) + "\n" + + description, + QSystemTrayIcon::Information); + } +} diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h new file mode 100644 index 0000000000..96452ef18b --- /dev/null +++ b/src/qt/bitcoingui.h @@ -0,0 +1,88 @@ +#ifndef BITCOINGUI_H +#define BITCOINGUI_H + +#include <QMainWindow> +#include <QSystemTrayIcon> + +class TransactionTableModel; +class ClientModel; + +QT_BEGIN_NAMESPACE +class QLabel; +class QLineEdit; +class QTableView; +class QAbstractItemModel; +class QModelIndex; +QT_END_NAMESPACE + +class BitcoinGUI : public QMainWindow +{ + Q_OBJECT +public: + explicit BitcoinGUI(QWidget *parent = 0); + void setModel(ClientModel *model); + + /* Transaction table tab indices */ + enum { + AllTransactions = 0, + SentReceived = 1, + Sent = 2, + Received = 3 + } TabIndex; + +protected: + void changeEvent(QEvent *e); + void closeEvent(QCloseEvent *event); + +private: + ClientModel *model; + + QLineEdit *address; + QLabel *labelBalance; + QLabel *labelConnections; + QLabel *labelBlocks; + QLabel *labelTransactions; + + QAction *quit; + QAction *sendcoins; + QAction *addressbook; + QAction *about; + QAction *receivingAddresses; + QAction *options; + QAction *openBitcoin; + + QSystemTrayIcon *trayIcon; + QList<QTableView *> transactionViews; + + void createActions(); + QWidget *createTabs(); + void createTrayIcon(); + void setTabsModel(QAbstractItemModel *transaction_model); + +public slots: + void setBalance(qint64 balance); + void setAddress(const QString &address); + void setNumConnections(int count); + void setNumBlocks(int count); + void setNumTransactions(int count); + void error(const QString &title, const QString &message); + /* It is currently not possible to pass a return value to another thread through + BlockingQueuedConnection, so use an indirected pointer. + http://bugreports.qt.nokia.com/browse/QTBUG-10440 + */ + void askFee(qint64 nFeeRequired, bool *payFee); + +private slots: + void sendcoinsClicked(); + void addressbookClicked(); + void optionsClicked(); + void receivingAddressesClicked(); + void aboutClicked(); + void newAddressClicked(); + void copyClipboardClicked(); + void trayIconActivated(QSystemTrayIcon::ActivationReason reason); + void transactionDetails(const QModelIndex& idx); + void incomingTransaction(const QModelIndex & parent, int start, int end); +}; + +#endif diff --git a/src/qt/clientmodel.cpp b/src/qt/clientmodel.cpp new file mode 100644 index 0000000000..97391e0938 --- /dev/null +++ b/src/qt/clientmodel.cpp @@ -0,0 +1,156 @@ +#include "clientmodel.h" +#include "main.h" +#include "guiconstants.h" +#include "optionsmodel.h" +#include "addresstablemodel.h" +#include "transactiontablemodel.h" + +#include <QTimer> + +ClientModel::ClientModel(QObject *parent) : + QObject(parent), optionsModel(0), addressTableModel(0), + transactionTableModel(0) +{ + /* Until signal notifications is built into the bitcoin core, + simply update everything after polling using a timer. + */ + QTimer *timer = new QTimer(this); + connect(timer, SIGNAL(timeout()), this, SLOT(update())); + timer->start(MODEL_UPDATE_DELAY); + + optionsModel = new OptionsModel(this); + addressTableModel = new AddressTableModel(this); + transactionTableModel = new TransactionTableModel(this); +} + +qint64 ClientModel::getBalance() +{ + return GetBalance(); +} + +QString ClientModel::getAddress() +{ + std::vector<unsigned char> vchPubKey; + if (CWalletDB("r").ReadDefaultKey(vchPubKey)) + { + return QString::fromStdString(PubKeyToAddress(vchPubKey)); + } + else + { + return QString(); + } +} + +int ClientModel::getNumConnections() +{ + return vNodes.size(); +} + +int ClientModel::getNumBlocks() +{ + return nBestHeight; +} + +int ClientModel::getNumTransactions() +{ + int numTransactions = 0; + CRITICAL_BLOCK(cs_mapWallet) + { + numTransactions = mapWallet.size(); + } + return numTransactions; +} + +void ClientModel::update() +{ + /* Plainly emit all signals for now. To be precise this should check + wether the values actually changed first. + */ + emit balanceChanged(getBalance()); + emit addressChanged(getAddress()); + emit numConnectionsChanged(getNumConnections()); + emit numBlocksChanged(getNumBlocks()); + emit numTransactionsChanged(getNumTransactions()); +} + +void ClientModel::setAddress(const QString &defaultAddress) +{ + uint160 hash160; + std::string strAddress = defaultAddress.toStdString(); + if (!AddressToHash160(strAddress, hash160)) + return; + if (!mapPubKeys.count(hash160)) + return; + CWalletDB().WriteDefaultKey(mapPubKeys[hash160]); +} + +ClientModel::StatusCode ClientModel::sendCoins(const QString &payTo, qint64 payAmount) +{ + uint160 hash160 = 0; + bool valid = false; + + if(!AddressToHash160(payTo.toUtf8().constData(), hash160)) + { + return InvalidAddress; + } + + if(payAmount <= 0) + { + return InvalidAmount; + } + + if(payAmount > getBalance()) + { + return AmountExceedsBalance; + } + + if((payAmount + nTransactionFee) > getBalance()) + { + return AmountWithFeeExceedsBalance; + } + + CRITICAL_BLOCK(cs_main) + { + // Send to bitcoin address + CWalletTx wtx; + CScript scriptPubKey; + scriptPubKey << OP_DUP << OP_HASH160 << hash160 << OP_EQUALVERIFY << OP_CHECKSIG; + + std::string strError = SendMoney(scriptPubKey, payAmount, wtx, true); + if (strError == "") + { + return OK; + } + else if (strError == "ABORTED") + { + return Aborted; + } + else + { + emit error(tr("Sending..."), QString::fromStdString(strError)); + return MiscError; + } + } + // Add addresses that we've sent to to the address book + std::string strAddress = payTo.toStdString(); + CRITICAL_BLOCK(cs_mapAddressBook) + if (!mapAddressBook.count(strAddress)) + SetAddressBookName(strAddress, ""); + + return OK; +} + +OptionsModel *ClientModel::getOptionsModel() +{ + return optionsModel; +} + +AddressTableModel *ClientModel::getAddressTableModel() +{ + return addressTableModel; +} + +TransactionTableModel *ClientModel::getTransactionTableModel() +{ + return transactionTableModel; +} diff --git a/src/qt/clientmodel.h b/src/qt/clientmodel.h new file mode 100644 index 0000000000..09d1fc921e --- /dev/null +++ b/src/qt/clientmodel.h @@ -0,0 +1,61 @@ +#ifndef CLIENTMODEL_H +#define CLIENTMODEL_H + +#include <QObject> + +class OptionsModel; +class AddressTableModel; +class TransactionTableModel; + +class ClientModel : public QObject +{ + Q_OBJECT +public: + explicit ClientModel(QObject *parent = 0); + + enum StatusCode + { + OK, + InvalidAmount, + InvalidAddress, + AmountExceedsBalance, + AmountWithFeeExceedsBalance, + Aborted, + MiscError + }; + + OptionsModel *getOptionsModel(); + AddressTableModel *getAddressTableModel(); + TransactionTableModel *getTransactionTableModel(); + + qint64 getBalance(); + QString getAddress(); + int getNumConnections(); + int getNumBlocks(); + int getNumTransactions(); + + /* Set default address */ + void setAddress(const QString &defaultAddress); + /* Send coins */ + StatusCode sendCoins(const QString &payTo, qint64 payAmount); +private: + OptionsModel *optionsModel; + AddressTableModel *addressTableModel; + TransactionTableModel *transactionTableModel; + +signals: + void balanceChanged(qint64 balance); + void addressChanged(const QString &address); + void numConnectionsChanged(int count); + void numBlocksChanged(int count); + void numTransactionsChanged(int count); + /* Asynchronous error notification */ + void error(const QString &title, const QString &message); + +public slots: + +private slots: + void update(); +}; + +#endif // CLIENTMODEL_H diff --git a/src/qt/editaddressdialog.cpp b/src/qt/editaddressdialog.cpp new file mode 100644 index 0000000000..dd0541760b --- /dev/null +++ b/src/qt/editaddressdialog.cpp @@ -0,0 +1,84 @@ +#include "editaddressdialog.h" +#include "ui_editaddressdialog.h" +#include "addresstablemodel.h" +#include "guiutil.h" + +#include <QDataWidgetMapper> +#include <QMessageBox> + +EditAddressDialog::EditAddressDialog(Mode mode, QWidget *parent) : + QDialog(parent), + ui(new Ui::EditAddressDialog), mapper(0), mode(mode), model(0) +{ + ui->setupUi(this); + + GUIUtil::setupAddressWidget(ui->addressEdit, this); + + switch(mode) + { + case NewReceivingAddress: + setWindowTitle(tr("New receiving address")); + ui->addressEdit->setEnabled(false); + break; + case NewSendingAddress: + setWindowTitle(tr("New sending address")); + break; + case EditReceivingAddress: + setWindowTitle(tr("Edit receiving address")); + ui->addressEdit->setReadOnly(true); + break; + case EditSendingAddress: + setWindowTitle(tr("Edit sending address")); + break; + } + + mapper = new QDataWidgetMapper(this); + mapper->setSubmitPolicy(QDataWidgetMapper::ManualSubmit); +} + +EditAddressDialog::~EditAddressDialog() +{ + delete ui; +} + +void EditAddressDialog::setModel(AddressTableModel *model) +{ + this->model = model; + mapper->setModel(model); + mapper->addMapping(ui->labelEdit, AddressTableModel::Label); + mapper->addMapping(ui->addressEdit, AddressTableModel::Address); +} + +void EditAddressDialog::loadRow(int row) +{ + mapper->setCurrentIndex(row); +} + +QString EditAddressDialog::saveCurrentRow() +{ + QString address; + switch(mode) + { + case NewReceivingAddress: + case NewSendingAddress: + address = model->addRow( + mode == NewSendingAddress ? AddressTableModel::Send : AddressTableModel::Receive, + ui->labelEdit->text(), + ui->addressEdit->text()); + if(address.isEmpty()) + { + QMessageBox::warning(this, windowTitle(), + tr("The address %1 is already in the address book.").arg(ui->addressEdit->text()), + QMessageBox::Ok, QMessageBox::Ok); + } + break; + case EditReceivingAddress: + case EditSendingAddress: + if(mapper->submit()) + { + address = ui->addressEdit->text(); + } + break; + } + return address; +} diff --git a/src/qt/editaddressdialog.h b/src/qt/editaddressdialog.h new file mode 100644 index 0000000000..6f396d0457 --- /dev/null +++ b/src/qt/editaddressdialog.h @@ -0,0 +1,41 @@ +#ifndef EDITADDRESSDIALOG_H +#define EDITADDRESSDIALOG_H + +#include <QDialog> + +QT_BEGIN_NAMESPACE +class QDataWidgetMapper; +QT_END_NAMESPACE + +namespace Ui { + class EditAddressDialog; +} +class AddressTableModel; + +class EditAddressDialog : public QDialog +{ + Q_OBJECT + +public: + enum Mode { + NewReceivingAddress, + NewSendingAddress, + EditReceivingAddress, + EditSendingAddress + }; + + explicit EditAddressDialog(Mode mode, QWidget *parent = 0); + ~EditAddressDialog(); + + void setModel(AddressTableModel *model); + void loadRow(int row); + QString saveCurrentRow(); + +private: + Ui::EditAddressDialog *ui; + QDataWidgetMapper *mapper; + Mode mode; + AddressTableModel *model; +}; + +#endif // EDITADDRESSDIALOG_H diff --git a/src/qt/forms/aboutdialog.ui b/src/qt/forms/aboutdialog.ui new file mode 100644 index 0000000000..45915c85bf --- /dev/null +++ b/src/qt/forms/aboutdialog.ui @@ -0,0 +1,162 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>AboutDialog</class> + <widget class="QDialog" name="AboutDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>593</width> + <height>319</height> + </rect> + </property> + <property name="windowTitle"> + <string>About Bitcoin</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLabel" name="label_4"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Ignored"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string/> + </property> + <property name="pixmap"> + <pixmap resource="../bitcoin.qrc">:/images/about</pixmap> + </property> + </widget> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string><b>Bitcoin</b> version</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="versionLabel"> + <property name="text"> + <string>0.3.666-beta</string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Copyright © 2009-2011 Bitcoin Developers + +This is experimental software. + +Distributed under the MIT/X11 software license, see the accompanying file license.txt or http://www.opensource.org/licenses/mit-license.php. + +This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit (http://www.openssl.org/) and cryptographic software written by Eric Young (eay@cryptsoft.com) and UPnP software written by Thomas Bernard.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources> + <include location="../bitcoin.qrc"/> + </resources> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>AboutDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>360</x> + <y>308</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>AboutDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>428</x> + <y>308</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/src/qt/forms/addressbookdialog.ui b/src/qt/forms/addressbookdialog.ui new file mode 100644 index 0000000000..2b3c69fb97 --- /dev/null +++ b/src/qt/forms/addressbookdialog.ui @@ -0,0 +1,196 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>AddressBookDialog</class> + <widget class="QDialog" name="AddressBookDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>591</width> + <height>347</height> + </rect> + </property> + <property name="windowTitle"> + <string>Address Book</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTabWidget" name="tabWidget"> + <property name="currentIndex"> + <number>0</number> + </property> + <widget class="QWidget" name="sendTab"> + <property name="toolTip"> + <string/> + </property> + <attribute name="title"> + <string>Sending</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QTableView" name="sendTableView"> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <attribute name="verticalHeaderVisible"> + <bool>false</bool> + </attribute> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="receiveTab"> + <property name="toolTip"> + <string/> + </property> + <attribute name="title"> + <string>Receiving</string> + </attribute> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>These are your Bitcoin addresses for receiving payments. You may want to give a different one to each sender so you can keep track of who is paying you. The highlighted address is displayed in the main window.</string> + </property> + <property name="textFormat"> + <enum>Qt::AutoText</enum> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QTableView" name="receiveTableView"> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <attribute name="verticalHeaderVisible"> + <bool>false</bool> + </attribute> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="newAddressButton"> + <property name="toolTip"> + <string>Create a new address</string> + </property> + <property name="text"> + <string>&New Address...</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="copyToClipboard"> + <property name="toolTip"> + <string>Copy the currently selected address to the system clipboard</string> + </property> + <property name="text"> + <string>&Copy to Clipboard</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="editButton"> + <property name="toolTip"> + <string>Edit the currently selected address</string> + </property> + <property name="text"> + <string>&Edit...</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="deleteButton"> + <property name="toolTip"> + <string>Delete the currently selected address from the list</string> + </property> + <property name="text"> + <string>&Delete</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>receiveTableView</sender> + <signal>doubleClicked(QModelIndex)</signal> + <receiver>editButton</receiver> + <slot>click()</slot> + <hints> + <hint type="sourcelabel"> + <x>334</x> + <y>249</y> + </hint> + <hint type="destinationlabel"> + <x>333</x> + <y>326</y> + </hint> + </hints> + </connection> + <connection> + <sender>sendTableView</sender> + <signal>doubleClicked(QModelIndex)</signal> + <receiver>editButton</receiver> + <slot>click()</slot> + <hints> + <hint type="sourcelabel"> + <x>329</x> + <y>261</y> + </hint> + <hint type="destinationlabel"> + <x>332</x> + <y>326</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/src/qt/forms/editaddressdialog.ui b/src/qt/forms/editaddressdialog.ui new file mode 100644 index 0000000000..f0ba28a854 --- /dev/null +++ b/src/qt/forms/editaddressdialog.ui @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>EditAddressDialog</class> + <widget class="QDialog" name="EditAddressDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>458</width> + <height>113</height> + </rect> + </property> + <property name="windowTitle"> + <string>Edit Address</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QFormLayout" name="formLayout"> + <property name="fieldGrowthPolicy"> + <enum>QFormLayout::AllNonFixedFieldsGrow</enum> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>&Label</string> + </property> + <property name="buddy"> + <cstring>labelEdit</cstring> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>&Address</string> + </property> + <property name="buddy"> + <cstring>addressEdit</cstring> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="labelEdit"> + <property name="toolTip"> + <string>The label associated with this address book entry</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLineEdit" name="addressEdit"> + <property name="toolTip"> + <string>The address associated with this address book entry. This can only be modified for sending addresses.</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>EditAddressDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>EditAddressDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/src/qt/forms/sendcoinsdialog.ui b/src/qt/forms/sendcoinsdialog.ui new file mode 100644 index 0000000000..595b7f40ff --- /dev/null +++ b/src/qt/forms/sendcoinsdialog.ui @@ -0,0 +1,180 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>SendCoinsDialog</class> + <widget class="QDialog" name="SendCoinsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>736</width> + <height>149</height> + </rect> + </property> + <property name="windowTitle"> + <string>Send Coins</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="4" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>&Amount:</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="3" column="1"> + <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 row="4" column="1"> + <widget class="QLineEdit" name="payAmount"> + <property name="maximumSize"> + <size> + <width>145</width> + <height>16777215</height> + </size> + </property> + <property name="toolTip"> + <string>Amount of bitcoins to send (e.g. 0.05)</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="3" column="2"> + <widget class="QPushButton" name="pasteButton"> + <property name="toolTip"> + <string>Paste address from system clipboard</string> + </property> + <property name="text"> + <string>&Paste</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="3" column="3"> + <widget class="QPushButton" name="addressBookButton"> + <property name="toolTip"> + <string>Look up adress in address book</string> + </property> + <property name="text"> + <string>Address &Book...</string> + </property> + <property name="autoDefault"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLabel" name="label_3"> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="text"> + <string>Enter a Bitcoin address (e.g. 1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L)</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="sendButton"> + <property name="toolTip"> + <string>Confirm the send action</string> + </property> + <property name="text"> + <string>&Send</string> + </property> + <property name="icon"> + <iconset resource="../bitcoin.qrc"> + <normaloff>:/icons/send</normaloff>:/icons/send</iconset> + </property> + <property name="default"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Maximum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Abort the send action</string> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources> + <include location="../bitcoin.qrc"/> + </resources> + <connections/> +</ui> diff --git a/src/qt/forms/transactiondescdialog.ui b/src/qt/forms/transactiondescdialog.ui new file mode 100644 index 0000000000..2f70a38214 --- /dev/null +++ b/src/qt/forms/transactiondescdialog.ui @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>TransactionDescDialog</class> + <widget class="QDialog" name="TransactionDescDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Transaction details</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTextEdit" name="detailText"> + <property name="toolTip"> + <string>This pane shows a detailed description of the transaction</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>TransactionDescDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>TransactionDescDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/src/qt/guiconstants.h b/src/qt/guiconstants.h new file mode 100644 index 0000000000..cdd1a74d98 --- /dev/null +++ b/src/qt/guiconstants.h @@ -0,0 +1,11 @@ +#ifndef GUICONSTANTS_H +#define GUICONSTANTS_H + +/* milliseconds between model updates */ +static const int MODEL_UPDATE_DELAY = 250; + +/* size of cache */ +static const unsigned int WALLET_CACHE_SIZE = 100; + + +#endif // GUICONSTANTS_H diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp new file mode 100644 index 0000000000..a81cc14fe8 --- /dev/null +++ b/src/qt/guiutil.cpp @@ -0,0 +1,38 @@ +#include "guiutil.h" +#include "bitcoinaddressvalidator.h" + +#include <QString> +#include <QDateTime> +#include <QDoubleValidator> +#include <QFont> +#include <QLineEdit> + +QString GUIUtil::DateTimeStr(qint64 nTime) +{ + QDateTime date = QDateTime::fromMSecsSinceEpoch(nTime*1000); + return date.date().toString(Qt::SystemLocaleShortDate) + QString(" ") + date.toString("hh:mm"); +} + +QFont GUIUtil::bitcoinAddressFont() +{ + QFont font("Monospace"); + font.setStyleHint(QFont::TypeWriter); + return font; +} + +void GUIUtil::setupAddressWidget(QLineEdit *widget, QWidget *parent) +{ + widget->setMaxLength(BitcoinAddressValidator::MaxAddressLength); + widget->setValidator(new BitcoinAddressValidator(parent)); + widget->setFont(bitcoinAddressFont()); +} + +void GUIUtil::setupAmountWidget(QLineEdit *widget, QWidget *parent) +{ + QDoubleValidator *amountValidator = new QDoubleValidator(parent); + amountValidator->setDecimals(8); + amountValidator->setBottom(0.0); + widget->setValidator(amountValidator); + widget->setAlignment(Qt::AlignRight|Qt::AlignVCenter); +} + diff --git a/src/qt/guiutil.h b/src/qt/guiutil.h new file mode 100644 index 0000000000..748e29bf37 --- /dev/null +++ b/src/qt/guiutil.h @@ -0,0 +1,25 @@ +#ifndef GUIUTIL_H +#define GUIUTIL_H + +#include <QString> + +QT_BEGIN_NAMESPACE +class QFont; +class QLineEdit; +class QWidget; +QT_END_NAMESPACE + +class GUIUtil +{ +public: + static QString DateTimeStr(qint64 nTime); + + /* Render bitcoin addresses in monospace font */ + static QFont bitcoinAddressFont(); + + static void setupAddressWidget(QLineEdit *widget, QWidget *parent); + + static void setupAmountWidget(QLineEdit *widget, QWidget *parent); +}; + +#endif // GUIUTIL_H diff --git a/src/qt/monitoreddatamapper.cpp b/src/qt/monitoreddatamapper.cpp new file mode 100644 index 0000000000..e70aa7ebf6 --- /dev/null +++ b/src/qt/monitoreddatamapper.cpp @@ -0,0 +1,39 @@ +#include "monitoreddatamapper.h" + +#include <QWidget> +#include <QMetaObject> +#include <QMetaProperty> +#include <QDebug> + + +MonitoredDataMapper::MonitoredDataMapper(QObject *parent) : + QDataWidgetMapper(parent) +{ +} + + +void MonitoredDataMapper::addMapping(QWidget *widget, int section) +{ + QDataWidgetMapper::addMapping(widget, section); + addChangeMonitor(widget); +} + +void MonitoredDataMapper::addMapping(QWidget *widget, int section, const QByteArray &propertyName) +{ + QDataWidgetMapper::addMapping(widget, section, propertyName); + addChangeMonitor(widget); +} + +void MonitoredDataMapper::addChangeMonitor(QWidget *widget) +{ + /* Watch user property of widget for changes, and connect + the signal to our viewModified signal. + */ + QMetaProperty prop = widget->metaObject()->userProperty(); + int signal = prop.notifySignalIndex(); + int method = this->metaObject()->indexOfMethod("viewModified()"); + if(signal != -1 && method != -1) + { + QMetaObject::connect(widget, signal, this, method); + } +} diff --git a/src/qt/monitoreddatamapper.h b/src/qt/monitoreddatamapper.h new file mode 100644 index 0000000000..4dd2d1a86a --- /dev/null +++ b/src/qt/monitoreddatamapper.h @@ -0,0 +1,32 @@ +#ifndef MONITOREDDATAMAPPER_H +#define MONITOREDDATAMAPPER_H + +#include <QDataWidgetMapper> + +QT_BEGIN_NAMESPACE +class QWidget; +QT_END_NAMESPACE + +/* Data <-> Widget mapper that watches for changes, + to be able to notify when 'dirty' (for example, to + enable a commit/apply button). + */ +class MonitoredDataMapper : public QDataWidgetMapper +{ + Q_OBJECT +public: + explicit MonitoredDataMapper(QObject *parent=0); + + void addMapping(QWidget *widget, int section); + void addMapping(QWidget *widget, int section, const QByteArray &propertyName); +private: + void addChangeMonitor(QWidget *widget); + +signals: + void viewModified(); + +}; + + + +#endif // MONITOREDDATAMAPPER_H diff --git a/src/qt/optionsdialog.cpp b/src/qt/optionsdialog.cpp new file mode 100644 index 0000000000..d46b48fb29 --- /dev/null +++ b/src/qt/optionsdialog.cpp @@ -0,0 +1,234 @@ +#include "optionsdialog.h" +#include "optionsmodel.h" +#include "monitoreddatamapper.h" +#include "guiutil.h" + +#include <QHBoxLayout> +#include <QVBoxLayout> +#include <QPushButton> +#include <QListWidget> +#include <QStackedWidget> + +#include <QCheckBox> +#include <QLabel> +#include <QLineEdit> +#include <QIntValidator> +#include <QDoubleValidator> +#include <QRegExpValidator> + +/* First (currently only) page of options */ +class MainOptionsPage : public QWidget +{ +public: + explicit MainOptionsPage(QWidget *parent=0); + + void setMapper(MonitoredDataMapper *mapper); +private: + QCheckBox *bitcoin_at_startup; + QCheckBox *minimize_to_tray; + QCheckBox *map_port_upnp; + QCheckBox *minimize_on_close; + QCheckBox *connect_socks4; + QLineEdit *proxy_ip; + QLineEdit *proxy_port; + QLineEdit *fee_edit; + +signals: + +public slots: + +}; + +OptionsDialog::OptionsDialog(QWidget *parent): + QDialog(parent), contents_widget(0), pages_widget(0), + main_options_page(0), model(0) +{ + contents_widget = new QListWidget(); + contents_widget->setMaximumWidth(128); + + pages_widget = new QStackedWidget(); + pages_widget->setMinimumWidth(300); + + QListWidgetItem *item_main = new QListWidgetItem(tr("Main")); + contents_widget->addItem(item_main); + main_options_page = new MainOptionsPage(this); + pages_widget->addWidget(main_options_page); + + contents_widget->setCurrentRow(0); + + QHBoxLayout *main_layout = new QHBoxLayout(); + main_layout->addWidget(contents_widget); + main_layout->addWidget(pages_widget, 1); + + QVBoxLayout *layout = new QVBoxLayout(); + layout->addLayout(main_layout); + + QHBoxLayout *buttons = new QHBoxLayout(); + buttons->addStretch(1); + QPushButton *ok_button = new QPushButton(tr("OK")); + buttons->addWidget(ok_button); + QPushButton *cancel_button = new QPushButton(tr("Cancel")); + buttons->addWidget(cancel_button); + apply_button = new QPushButton(tr("Apply")); + apply_button->setEnabled(false); + buttons->addWidget(apply_button); + + layout->addLayout(buttons); + + setLayout(layout); + setWindowTitle(tr("Options")); + + /* Widget-to-option mapper */ + mapper = new MonitoredDataMapper(this); + mapper->setSubmitPolicy(QDataWidgetMapper::ManualSubmit); + mapper->setOrientation(Qt::Vertical); + /* enable apply button when data modified */ + connect(mapper, SIGNAL(viewModified()), this, SLOT(enableApply())); + /* disable apply button when new data loaded */ + connect(mapper, SIGNAL(currentIndexChanged(int)), this, SLOT(disableApply())); + + /* Event bindings */ + connect(ok_button, SIGNAL(clicked()), this, SLOT(okClicked())); + connect(cancel_button, SIGNAL(clicked()), this, SLOT(cancelClicked())); + connect(apply_button, SIGNAL(clicked()), this, SLOT(applyClicked())); +} + +void OptionsDialog::setModel(OptionsModel *model) +{ + this->model = model; + + mapper->setModel(model); + main_options_page->setMapper(mapper); + + mapper->toFirst(); +} + +void OptionsDialog::changePage(QListWidgetItem *current, QListWidgetItem *previous) +{ + Q_UNUSED(previous); + if(current) + { + pages_widget->setCurrentIndex(contents_widget->row(current)); + } +} + +void OptionsDialog::okClicked() +{ + mapper->submit(); + accept(); +} + +void OptionsDialog::cancelClicked() +{ + reject(); +} + +void OptionsDialog::applyClicked() +{ + mapper->submit(); + apply_button->setEnabled(false); +} + +void OptionsDialog::enableApply() +{ + apply_button->setEnabled(true); +} + +void OptionsDialog::disableApply() +{ + apply_button->setEnabled(false); +} + +MainOptionsPage::MainOptionsPage(QWidget *parent): + QWidget(parent) +{ + QVBoxLayout *layout = new QVBoxLayout(); + + bitcoin_at_startup = new QCheckBox(tr("&Start Bitcoin on window system startup")); + bitcoin_at_startup->setToolTip(tr("Automatically start Bitcoin after the computer is turned on")); + layout->addWidget(bitcoin_at_startup); + + minimize_to_tray = new QCheckBox(tr("&Minimize to the tray instead of the taskbar")); + minimize_to_tray->setToolTip(tr("Show only a tray icon after minimizing the window")); + layout->addWidget(minimize_to_tray); + + map_port_upnp = new QCheckBox(tr("Map port using &UPnP")); + map_port_upnp->setToolTip(tr("Automatically open the Bitcoin client port on the router. This only works when your router supports UPnP and it is enabled.")); + layout->addWidget(map_port_upnp); + + minimize_on_close = new QCheckBox(tr("M&inimize on close")); + minimize_on_close->setToolTip(tr("Minimize instead of exit the application when the window is closed. When this option is enabled, the application will be closed only after selecting Quit in the menu.")); + layout->addWidget(minimize_on_close); + + connect_socks4 = new QCheckBox(tr("&Connect through SOCKS4 proxy:")); + connect_socks4->setToolTip(tr("Connect to the Bitcon network through a SOCKS4 proxy (e.g. when connecting through Tor)")); + layout->addWidget(connect_socks4); + + QHBoxLayout *proxy_hbox = new QHBoxLayout(); + proxy_hbox->addSpacing(18); + QLabel *proxy_ip_label = new QLabel(tr("Proxy &IP: ")); + proxy_hbox->addWidget(proxy_ip_label); + proxy_ip = new QLineEdit(); + proxy_ip->setMaximumWidth(140); + proxy_ip->setEnabled(false); + proxy_ip->setValidator(new QRegExpValidator(QRegExp("[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}"), this)); + proxy_ip->setToolTip(tr("IP address of the proxy (e.g. 127.0.0.1)")); + proxy_ip_label->setBuddy(proxy_ip); + proxy_hbox->addWidget(proxy_ip); + QLabel *proxy_port_label = new QLabel(tr("&Port: ")); + proxy_hbox->addWidget(proxy_port_label); + proxy_port = new QLineEdit(); + proxy_port->setMaximumWidth(55); + proxy_port->setValidator(new QIntValidator(0, 65535, this)); + proxy_port->setEnabled(false); + proxy_port->setToolTip(tr("Port of the proxy (e.g. 1234)")); + proxy_port_label->setBuddy(proxy_port); + proxy_hbox->addWidget(proxy_port); + proxy_hbox->addStretch(1); + + layout->addLayout(proxy_hbox); + QLabel *fee_help = new QLabel(tr("Optional transaction fee per KB that helps make sure your transactions are processed quickly. Most transactions are 1KB. Fee 0.01 recommended.")); + fee_help->setWordWrap(true); + layout->addWidget(fee_help); + + QHBoxLayout *fee_hbox = new QHBoxLayout(); + fee_hbox->addSpacing(18); + QLabel *fee_label = new QLabel(tr("Pay transaction &fee")); + fee_hbox->addWidget(fee_label); + fee_edit = new QLineEdit(); + fee_edit->setMaximumWidth(100); + fee_edit->setToolTip(tr("Optional transaction fee per KB that helps make sure your transactions are processed quickly. Most transactions are 1KB. Fee 0.01 recommended.")); + + GUIUtil::setupAmountWidget(fee_edit, this); + + fee_label->setBuddy(fee_edit); + fee_hbox->addWidget(fee_edit); + fee_hbox->addStretch(1); + + layout->addLayout(fee_hbox); + + layout->addStretch(1); /* Extra space at bottom */ + + setLayout(layout); + + connect(connect_socks4, SIGNAL(toggled(bool)), proxy_ip, SLOT(setEnabled(bool))); + connect(connect_socks4, SIGNAL(toggled(bool)), proxy_port, SLOT(setEnabled(bool))); + +#ifndef USE_UPNP + map_port_upnp->setDisabled(true); +#endif +} + +void MainOptionsPage::setMapper(MonitoredDataMapper *mapper) +{ + /* Map model to widgets */ + mapper->addMapping(bitcoin_at_startup, OptionsModel::StartAtStartup); + mapper->addMapping(minimize_to_tray, OptionsModel::MinimizeToTray); + mapper->addMapping(map_port_upnp, OptionsModel::MapPortUPnP); + mapper->addMapping(minimize_on_close, OptionsModel::MinimizeOnClose); + mapper->addMapping(connect_socks4, OptionsModel::ConnectSOCKS4); + mapper->addMapping(proxy_ip, OptionsModel::ProxyIP); + mapper->addMapping(proxy_port, OptionsModel::ProxyPort); + mapper->addMapping(fee_edit, OptionsModel::Fee); +} + diff --git a/src/qt/optionsdialog.h b/src/qt/optionsdialog.h new file mode 100644 index 0000000000..07e85297d5 --- /dev/null +++ b/src/qt/optionsdialog.h @@ -0,0 +1,45 @@ +#ifndef OPTIONSDIALOG_H +#define OPTIONSDIALOG_H + +#include <QDialog> + +QT_BEGIN_NAMESPACE +class QStackedWidget; +class QListWidget; +class QListWidgetItem; +class QPushButton; +QT_END_NAMESPACE +class OptionsModel; +class MainOptionsPage; +class MonitoredDataMapper; + +class OptionsDialog : public QDialog +{ + Q_OBJECT +public: + explicit OptionsDialog(QWidget *parent=0); + + void setModel(OptionsModel *model); + +signals: + +public slots: + void changePage(QListWidgetItem *current, QListWidgetItem *previous); +private slots: + void okClicked(); + void cancelClicked(); + void applyClicked(); + void enableApply(); + void disableApply(); +private: + QListWidget *contents_widget; + QStackedWidget *pages_widget; + MainOptionsPage *main_options_page; + OptionsModel *model; + MonitoredDataMapper *mapper; + QPushButton *apply_button; + + void setupMainPage(); +}; + +#endif // OPTIONSDIALOG_H diff --git a/src/qt/optionsmodel.cpp b/src/qt/optionsmodel.cpp new file mode 100644 index 0000000000..1528fdf697 --- /dev/null +++ b/src/qt/optionsmodel.cpp @@ -0,0 +1,140 @@ +#include "optionsmodel.h" +#include "main.h" +#include "net.h" + +#include <QDebug> + +OptionsModel::OptionsModel(QObject *parent) : + QAbstractListModel(parent) +{ +} + +int OptionsModel::rowCount(const QModelIndex & parent) const +{ + return OptionIDRowCount; +} + +QVariant OptionsModel::data(const QModelIndex & index, int role) const +{ + if(role == Qt::EditRole) + { + switch(index.row()) + { + case StartAtStartup: + return QVariant(); + case MinimizeToTray: + return QVariant(fMinimizeToTray); + case MapPortUPnP: + return QVariant(fUseUPnP); + case MinimizeOnClose: + return QVariant(fMinimizeOnClose); + case ConnectSOCKS4: + return QVariant(fUseProxy); + case ProxyIP: + return QVariant(QString::fromStdString(addrProxy.ToStringIP())); + case ProxyPort: + return QVariant(QString::fromStdString(addrProxy.ToStringPort())); + case Fee: + return QVariant(QString::fromStdString(FormatMoney(nTransactionFee))); + default: + return QVariant(); + } + } + return QVariant(); +} + +bool OptionsModel::setData(const QModelIndex & index, const QVariant & value, int role) +{ + bool successful = true; /* set to false on parse error */ + if(role == Qt::EditRole) + { + CWalletDB walletdb; + switch(index.row()) + { + case StartAtStartup: + successful = false; /*TODO*/ + break; + case MinimizeToTray: + fMinimizeToTray = value.toBool(); + walletdb.WriteSetting("fMinimizeToTray", fMinimizeToTray); + break; + case MapPortUPnP: + fUseUPnP = value.toBool(); + walletdb.WriteSetting("fUseUPnP", fUseUPnP); +#ifdef USE_UPNP + MapPort(fUseUPnP); +#endif + break; + case MinimizeOnClose: + fMinimizeOnClose = value.toBool(); + walletdb.WriteSetting("fMinimizeOnClose", fMinimizeOnClose); + break; + case ConnectSOCKS4: + fUseProxy = value.toBool(); + walletdb.WriteSetting("fUseProxy", fUseProxy); + break; + case ProxyIP: + { + /* Use CAddress to parse IP */ + CAddress addr(value.toString().toStdString() + ":1"); + if (addr.ip != INADDR_NONE) + { + addrProxy.ip = addr.ip; + walletdb.WriteSetting("addrProxy", addrProxy); + } + else + { + successful = false; + } + } + break; + case ProxyPort: + { + int nPort = atoi(value.toString().toAscii().data()); + if (nPort > 0 && nPort < USHRT_MAX) + { + addrProxy.port = htons(nPort); + walletdb.WriteSetting("addrProxy", addrProxy); + } + else + { + successful = false; + } + } + break; + case Fee: { + int64 retval; + if(ParseMoney(value.toString().toStdString(), retval)) + { + nTransactionFee = retval; + walletdb.WriteSetting("nTransactionFee", nTransactionFee); + } + else + { + successful = false; /* parse error */ + } + } + break; + default: + break; + } + } + emit dataChanged(index, index); + + return successful; +} + +qint64 OptionsModel::getTransactionFee() +{ + return nTransactionFee; +} + +bool OptionsModel::getMinimizeToTray() +{ + return fMinimizeToTray; +} + +bool OptionsModel::getMinimizeOnClose() +{ + return fMinimizeOnClose; +} diff --git a/src/qt/optionsmodel.h b/src/qt/optionsmodel.h new file mode 100644 index 0000000000..0124e2ab47 --- /dev/null +++ b/src/qt/optionsmodel.h @@ -0,0 +1,44 @@ +#ifndef OPTIONSMODEL_H +#define OPTIONSMODEL_H + +#include <QAbstractListModel> + +/* Interface from QT to configuration data structure for bitcoin client. + To QT, the options are presented as a list with the different options + laid out vertically. + This can be changed to a tree once the settings become sufficiently + complex. + */ +class OptionsModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit OptionsModel(QObject *parent = 0); + + enum OptionID { + StartAtStartup, + MinimizeToTray, + MapPortUPnP, + MinimizeOnClose, + ConnectSOCKS4, + ProxyIP, + ProxyPort, + Fee, + OptionIDRowCount + }; + + int rowCount(const QModelIndex & parent = QModelIndex()) const; + QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const; + bool setData(const QModelIndex & index, const QVariant & value, int role = Qt::EditRole); + + /* Explicit getters */ + qint64 getTransactionFee(); + bool getMinimizeToTray(); + bool getMinimizeOnClose(); +signals: + +public slots: + +}; + +#endif // OPTIONSMODEL_H diff --git a/src/qt/res/icons/address-book.png b/src/qt/res/icons/address-book.png Binary files differnew file mode 100644 index 0000000000..abfb3c3a51 --- /dev/null +++ b/src/qt/res/icons/address-book.png diff --git a/src/qt/res/icons/bitcoin.png b/src/qt/res/icons/bitcoin.png Binary files differnew file mode 100644 index 0000000000..09520aca23 --- /dev/null +++ b/src/qt/res/icons/bitcoin.png diff --git a/src/qt/res/icons/quit.png b/src/qt/res/icons/quit.png Binary files differnew file mode 100644 index 0000000000..fb510fbea6 --- /dev/null +++ b/src/qt/res/icons/quit.png diff --git a/src/qt/res/icons/send.png b/src/qt/res/icons/send.png Binary files differnew file mode 100644 index 0000000000..0ba5359d7b --- /dev/null +++ b/src/qt/res/icons/send.png diff --git a/src/qt/res/icons/toolbar.png b/src/qt/res/icons/toolbar.png Binary files differnew file mode 100644 index 0000000000..f2dcb20636 --- /dev/null +++ b/src/qt/res/icons/toolbar.png diff --git a/src/qt/res/images/about.png b/src/qt/res/images/about.png Binary files differnew file mode 100644 index 0000000000..c9ab9511ef --- /dev/null +++ b/src/qt/res/images/about.png diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp new file mode 100644 index 0000000000..721ab14108 --- /dev/null +++ b/src/qt/sendcoinsdialog.cpp @@ -0,0 +1,113 @@ +#include "sendcoinsdialog.h" +#include "ui_sendcoinsdialog.h" +#include "clientmodel.h" +#include "guiutil.h" + +#include "addressbookdialog.h" +#include "optionsmodel.h" + +#include <QApplication> +#include <QClipboard> +#include <QMessageBox> +#include <QLocale> +#include <QDebug> + +#include "util.h" +#include "base58.h" + +SendCoinsDialog::SendCoinsDialog(QWidget *parent, const QString &address) : + QDialog(parent), + ui(new Ui::SendCoinsDialog), + model(0) +{ + ui->setupUi(this); + + GUIUtil::setupAddressWidget(ui->payTo, this); + GUIUtil::setupAmountWidget(ui->payAmount, this); + + /* Set initial address if provided */ + if(!address.isEmpty()) + { + ui->payTo->setText(address); + ui->payAmount->setFocus(); + } +} + +void SendCoinsDialog::setModel(ClientModel *model) +{ + this->model = model; +} + +SendCoinsDialog::~SendCoinsDialog() +{ + delete ui; +} + +void SendCoinsDialog::on_sendButton_clicked() +{ + bool valid; + QString payAmount = ui->payAmount->text(); + qint64 payAmountParsed; + + valid = ParseMoney(payAmount.toStdString(), payAmountParsed); + + if(!valid) + { + QMessageBox::warning(this, tr("Send Coins"), + tr("The amount to pay must be a valid number."), + QMessageBox::Ok, QMessageBox::Ok); + return; + } + + switch(model->sendCoins(ui->payTo->text(), payAmountParsed)) + { + case ClientModel::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 ClientModel::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 ClientModel::AmountExceedsBalance: + QMessageBox::warning(this, tr("Send Coins"), + tr("Amount exceeds your balance"), + QMessageBox::Ok, QMessageBox::Ok); + ui->payAmount->setFocus(); + break; + case ClientModel::AmountWithFeeExceedsBalance: + QMessageBox::warning(this, tr("Send Coins"), + tr("Total exceeds your balance when the %1 transaction fee is included"). + arg(QString::fromStdString(FormatMoney(model->getOptionsModel()->getTransactionFee()))), + QMessageBox::Ok, QMessageBox::Ok); + ui->payAmount->setFocus(); + break; + case ClientModel::OK: + accept(); + break; + } +} + +void SendCoinsDialog::on_pasteButton_clicked() +{ + /* Paste text from clipboard into recipient field */ + ui->payTo->setText(QApplication::clipboard()->text()); +} + +void SendCoinsDialog::on_addressBookButton_clicked() +{ + AddressBookDialog dlg; + dlg.setModel(model->getAddressTableModel()); + dlg.setTab(AddressBookDialog::SendingTab); + dlg.exec(); + ui->payTo->setText(dlg.getReturnValue()); +} + +void SendCoinsDialog::on_buttonBox_rejected() +{ + reject(); +} diff --git a/src/qt/sendcoinsdialog.h b/src/qt/sendcoinsdialog.h new file mode 100644 index 0000000000..f73c38d63a --- /dev/null +++ b/src/qt/sendcoinsdialog.h @@ -0,0 +1,32 @@ +#ifndef SENDCOINSDIALOG_H +#define SENDCOINSDIALOG_H + +#include <QDialog> + +namespace Ui { + class SendCoinsDialog; +} +class ClientModel; + +class SendCoinsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit SendCoinsDialog(QWidget *parent = 0, const QString &address = ""); + ~SendCoinsDialog(); + + void setModel(ClientModel *model); + +private: + Ui::SendCoinsDialog *ui; + ClientModel *model; + +private slots: + void on_buttonBox_rejected(); + void on_addressBookButton_clicked(); + void on_pasteButton_clicked(); + void on_sendButton_clicked(); +}; + +#endif // SENDCOINSDIALOG_H diff --git a/src/qt/transactiondesc.cpp b/src/qt/transactiondesc.cpp new file mode 100644 index 0000000000..4d8a55e99a --- /dev/null +++ b/src/qt/transactiondesc.cpp @@ -0,0 +1,310 @@ +#include <transactiondesc.h> + +#include "guiutil.h" +#include "main.h" + +#include <QString> + +/* Taken straight from ui.cpp + TODO: Convert to use QStrings, Qt::Escape and tr() + */ + +using namespace std; + +static string HtmlEscape(const char* psz, bool fMultiLine=false) +{ + int len = 0; + for (const char* p = psz; *p; p++) + { + if (*p == '<') len += 4; + else if (*p == '>') len += 4; + else if (*p == '&') len += 5; + else if (*p == '"') len += 6; + else if (*p == ' ' && p > psz && p[-1] == ' ' && p[1] == ' ') len += 6; + else if (*p == '\n' && fMultiLine) len += 5; + else + len++; + } + string str; + str.reserve(len); + for (const char* p = psz; *p; p++) + { + if (*p == '<') str += "<"; + else if (*p == '>') str += ">"; + else if (*p == '&') str += "&"; + else if (*p == '"') str += """; + else if (*p == ' ' && p > psz && p[-1] == ' ' && p[1] == ' ') str += " "; + else if (*p == '\n' && fMultiLine) str += "<br>\n"; + else + str += *p; + } + return str; +} + +static string HtmlEscape(const string& str, bool fMultiLine=false) +{ + return HtmlEscape(str.c_str(), fMultiLine); +} + +static string FormatTxStatus(const CWalletTx& wtx) +{ + // Status + if (!wtx.IsFinal()) + { + if (wtx.nLockTime < 500000000) + return strprintf(_("Open for %d blocks"), nBestHeight - wtx.nLockTime); + else + return strprintf(_("Open until %s"), GUIUtil::DateTimeStr(wtx.nLockTime).toStdString().c_str()); + } + else + { + int nDepth = wtx.GetDepthInMainChain(); + if (GetAdjustedTime() - wtx.nTimeReceived > 2 * 60 && wtx.GetRequestCount() == 0) + return strprintf(_("%d/offline?"), nDepth); + else if (nDepth < 6) + return strprintf(_("%d/unconfirmed"), nDepth); + else + return strprintf(_("%d confirmations"), nDepth); + } +} + +string TransactionDesc::toHTML(CWalletTx &wtx) +{ + string strHTML; + CRITICAL_BLOCK(cs_mapAddressBook) + { + strHTML.reserve(4000); + strHTML += "<html><font face='verdana, arial, helvetica, sans-serif'>"; + + int64 nTime = wtx.GetTxTime(); + int64 nCredit = wtx.GetCredit(); + int64 nDebit = wtx.GetDebit(); + int64 nNet = nCredit - nDebit; + + + + strHTML += _("<b>Status:</b> ") + FormatTxStatus(wtx); + int nRequests = wtx.GetRequestCount(); + if (nRequests != -1) + { + if (nRequests == 0) + strHTML += _(", has not been successfully broadcast yet"); + else if (nRequests == 1) + strHTML += strprintf(_(", broadcast through %d node"), nRequests); + else + strHTML += strprintf(_(", broadcast through %d nodes"), nRequests); + } + strHTML += "<br>"; + + strHTML += _("<b>Date:</b> ") + (nTime ? GUIUtil::DateTimeStr(nTime).toStdString() : "") + "<br>"; + + + // + // From + // + if (wtx.IsCoinBase()) + { + strHTML += _("<b>Source:</b> Generated<br>"); + } + else if (!wtx.mapValue["from"].empty()) + { + // Online transaction + if (!wtx.mapValue["from"].empty()) + strHTML += _("<b>From:</b> ") + HtmlEscape(wtx.mapValue["from"]) + "<br>"; + } + else + { + // Offline transaction + if (nNet > 0) + { + // Credit + BOOST_FOREACH(const CTxOut& txout, wtx.vout) + { + if (txout.IsMine()) + { + vector<unsigned char> vchPubKey; + if (ExtractPubKey(txout.scriptPubKey, true, vchPubKey)) + { + string strAddress = PubKeyToAddress(vchPubKey); + if (mapAddressBook.count(strAddress)) + { + strHTML += string() + _("<b>From:</b> ") + _("unknown") + "<br>"; + strHTML += _("<b>To:</b> "); + strHTML += HtmlEscape(strAddress); + if (!mapAddressBook[strAddress].empty()) + strHTML += _(" (yours, label: ") + mapAddressBook[strAddress] + ")"; + else + strHTML += _(" (yours)"); + strHTML += "<br>"; + } + } + break; + } + } + } + } + + + // + // To + // + string strAddress; + if (!wtx.mapValue["to"].empty()) + { + // Online transaction + strAddress = wtx.mapValue["to"]; + strHTML += _("<b>To:</b> "); + if (mapAddressBook.count(strAddress) && !mapAddressBook[strAddress].empty()) + strHTML += mapAddressBook[strAddress] + " "; + strHTML += HtmlEscape(strAddress) + "<br>"; + } + + + // + // Amount + // + if (wtx.IsCoinBase() && nCredit == 0) + { + // + // Coinbase + // + int64 nUnmatured = 0; + BOOST_FOREACH(const CTxOut& txout, wtx.vout) + nUnmatured += txout.GetCredit(); + strHTML += _("<b>Credit:</b> "); + if (wtx.IsInMainChain()) + strHTML += strprintf(_("(%s matures in %d more blocks)"), FormatMoney(nUnmatured).c_str(), wtx.GetBlocksToMaturity()); + else + strHTML += _("(not accepted)"); + strHTML += "<br>"; + } + else if (nNet > 0) + { + // + // Credit + // + strHTML += _("<b>Credit:</b> ") + FormatMoney(nNet) + "<br>"; + } + else + { + bool fAllFromMe = true; + BOOST_FOREACH(const CTxIn& txin, wtx.vin) + fAllFromMe = fAllFromMe && txin.IsMine(); + + bool fAllToMe = true; + BOOST_FOREACH(const CTxOut& txout, wtx.vout) + fAllToMe = fAllToMe && txout.IsMine(); + + if (fAllFromMe) + { + // + // Debit + // + BOOST_FOREACH(const CTxOut& txout, wtx.vout) + { + if (txout.IsMine()) + continue; + + if (wtx.mapValue["to"].empty()) + { + // Offline transaction + uint160 hash160; + if (ExtractHash160(txout.scriptPubKey, hash160)) + { + string strAddress = Hash160ToAddress(hash160); + strHTML += _("<b>To:</b> "); + if (mapAddressBook.count(strAddress) && !mapAddressBook[strAddress].empty()) + strHTML += mapAddressBook[strAddress] + " "; + strHTML += strAddress; + strHTML += "<br>"; + } + } + + strHTML += _("<b>Debit:</b> ") + FormatMoney(-txout.nValue) + "<br>"; + } + + if (fAllToMe) + { + // Payment to self + int64 nChange = wtx.GetChange(); + int64 nValue = nCredit - nChange; + strHTML += _("<b>Debit:</b> ") + FormatMoney(-nValue) + "<br>"; + strHTML += _("<b>Credit:</b> ") + FormatMoney(nValue) + "<br>"; + } + + int64 nTxFee = nDebit - wtx.GetValueOut(); + if (nTxFee > 0) + strHTML += _("<b>Transaction fee:</b> ") + FormatMoney(-nTxFee) + "<br>"; + } + else + { + // + // Mixed debit transaction + // + BOOST_FOREACH(const CTxIn& txin, wtx.vin) + if (txin.IsMine()) + strHTML += _("<b>Debit:</b> ") + FormatMoney(-txin.GetDebit()) + "<br>"; + BOOST_FOREACH(const CTxOut& txout, wtx.vout) + if (txout.IsMine()) + strHTML += _("<b>Credit:</b> ") + FormatMoney(txout.GetCredit()) + "<br>"; + } + } + + strHTML += _("<b>Net amount:</b> ") + FormatMoney(nNet, true) + "<br>"; + + + // + // Message + // + if (!wtx.mapValue["message"].empty()) + strHTML += string() + "<br><b>" + _("Message:") + "</b><br>" + HtmlEscape(wtx.mapValue["message"], true) + "<br>"; + if (!wtx.mapValue["comment"].empty()) + strHTML += string() + "<br><b>" + _("Comment:") + "</b><br>" + HtmlEscape(wtx.mapValue["comment"], true) + "<br>"; + + if (wtx.IsCoinBase()) + strHTML += string() + "<br>" + _("Generated coins must wait 120 blocks before they can be spent. When you generated this block, it was broadcast to the network to be added to the block chain. If it fails to get into the chain, it will change to \"not accepted\" and not be spendable. This may occasionally happen if another node generates a block within a few seconds of yours.") + "<br>"; + + + // + // Debug view + // + if (fDebug) + { + strHTML += "<hr><br>debug print<br><br>"; + BOOST_FOREACH(const CTxIn& txin, wtx.vin) + if (txin.IsMine()) + strHTML += "<b>Debit:</b> " + FormatMoney(-txin.GetDebit()) + "<br>"; + BOOST_FOREACH(const CTxOut& txout, wtx.vout) + if (txout.IsMine()) + strHTML += "<b>Credit:</b> " + FormatMoney(txout.GetCredit()) + "<br>"; + + strHTML += "<br><b>Transaction:</b><br>"; + strHTML += HtmlEscape(wtx.ToString(), true); + + strHTML += "<br><b>Inputs:</b><br>"; + CRITICAL_BLOCK(cs_mapWallet) + { + BOOST_FOREACH(const CTxIn& txin, wtx.vin) + { + COutPoint prevout = txin.prevout; + map<uint256, CWalletTx>::iterator mi = mapWallet.find(prevout.hash); + if (mi != mapWallet.end()) + { + const CWalletTx& prev = (*mi).second; + if (prevout.n < prev.vout.size()) + { + strHTML += HtmlEscape(prev.ToString(), true); + strHTML += " " + FormatTxStatus(prev) + ", "; + strHTML = strHTML + "IsMine=" + (prev.vout[prevout.n].IsMine() ? "true" : "false") + "<br>"; + } + } + } + } + } + + + + strHTML += "</font></html>"; + } + return strHTML; +} diff --git a/src/qt/transactiondesc.h b/src/qt/transactiondesc.h new file mode 100644 index 0000000000..5a85949341 --- /dev/null +++ b/src/qt/transactiondesc.h @@ -0,0 +1,15 @@ +#ifndef TRANSACTIONDESC_H +#define TRANSACTIONDESC_H + +#include <string> + +class CWalletTx; + +class TransactionDesc +{ +public: + /* Provide human-readable extended HTML description of a transaction */ + static std::string toHTML(CWalletTx &wtx); +}; + +#endif // TRANSACTIONDESC_H diff --git a/src/qt/transactiondescdialog.cpp b/src/qt/transactiondescdialog.cpp new file mode 100644 index 0000000000..3bd4808cb6 --- /dev/null +++ b/src/qt/transactiondescdialog.cpp @@ -0,0 +1,20 @@ +#include "transactiondescdialog.h" +#include "ui_transactiondescdialog.h" + +#include "transactiontablemodel.h" + +#include <QModelIndex> + +TransactionDescDialog::TransactionDescDialog(const QModelIndex &idx, QWidget *parent) : + QDialog(parent), + ui(new Ui::TransactionDescDialog) +{ + ui->setupUi(this); + QString desc = idx.data(TransactionTableModel::LongDescriptionRole).toString(); + ui->detailText->setHtml(desc); +} + +TransactionDescDialog::~TransactionDescDialog() +{ + delete ui; +} diff --git a/src/qt/transactiondescdialog.h b/src/qt/transactiondescdialog.h new file mode 100644 index 0000000000..4f8f754b2b --- /dev/null +++ b/src/qt/transactiondescdialog.h @@ -0,0 +1,25 @@ +#ifndef TRANSACTIONDESCDIALOG_H +#define TRANSACTIONDESCDIALOG_H + +#include <QDialog> + +namespace Ui { + class TransactionDescDialog; +} +QT_BEGIN_NAMESPACE +class QModelIndex; +QT_END_NAMESPACE + +class TransactionDescDialog : public QDialog +{ + Q_OBJECT + +public: + explicit TransactionDescDialog(const QModelIndex &idx, QWidget *parent = 0); + ~TransactionDescDialog(); + +private: + Ui::TransactionDescDialog *ui; +}; + +#endif // TRANSACTIONDESCDIALOG_H diff --git a/src/qt/transactionrecord.cpp b/src/qt/transactionrecord.cpp new file mode 100644 index 0000000000..6c1f3a5e58 --- /dev/null +++ b/src/qt/transactionrecord.cpp @@ -0,0 +1,254 @@ +#include "transactionrecord.h" + + +/* Return positive answer if transaction should be shown in list. + */ +bool TransactionRecord::showTransaction(const CWalletTx &wtx) +{ + if (wtx.IsCoinBase()) + { + // Don't show generated coin until confirmed by at least one block after it + // so we don't get the user's hopes up until it looks like it's probably accepted. + // + // It is not an error when generated blocks are not accepted. By design, + // some percentage of blocks, like 10% or more, will end up not accepted. + // This is the normal mechanism by which the network copes with latency. + // + // We display regular transactions right away before any confirmation + // because they can always get into some block eventually. Generated coins + // are special because if their block is not accepted, they are not valid. + // + if (wtx.GetDepthInMainChain() < 2) + { + return false; + } + } + return true; +} + +/* Decompose CWallet transaction to model transaction records. + */ +QList<TransactionRecord> TransactionRecord::decomposeTransaction(const CWalletTx &wtx) +{ + QList<TransactionRecord> parts; + int64 nTime = wtx.nTimeDisplayed = wtx.GetTxTime(); + int64 nCredit = wtx.GetCredit(true); + int64 nDebit = wtx.GetDebit(); + int64 nNet = nCredit - nDebit; + uint256 hash = wtx.GetHash(); + std::map<std::string, std::string> mapValue = wtx.mapValue; + + if (showTransaction(wtx)) + { + if (nNet > 0 || wtx.IsCoinBase()) + { + // + // Credit + // + TransactionRecord sub(hash, nTime); + + sub.credit = nNet; + + if (wtx.IsCoinBase()) + { + // Generated + sub.type = TransactionRecord::Generated; + + if (nCredit == 0) + { + int64 nUnmatured = 0; + BOOST_FOREACH(const CTxOut& txout, wtx.vout) + nUnmatured += txout.GetCredit(); + sub.credit = nUnmatured; + } + } + else if (!mapValue["from"].empty() || !mapValue["message"].empty()) + { + // Received by IP connection + sub.type = TransactionRecord::RecvFromIP; + if (!mapValue["from"].empty()) + sub.address = mapValue["from"]; + } + else + { + // Received by Bitcoin Address + sub.type = TransactionRecord::RecvWithAddress; + BOOST_FOREACH(const CTxOut& txout, wtx.vout) + { + if (txout.IsMine()) + { + std::vector<unsigned char> vchPubKey; + if (ExtractPubKey(txout.scriptPubKey, true, vchPubKey)) + { + sub.address = PubKeyToAddress(vchPubKey); + } + break; + } + } + } + parts.append(sub); + } + else + { + bool fAllFromMe = true; + BOOST_FOREACH(const CTxIn& txin, wtx.vin) + fAllFromMe = fAllFromMe && txin.IsMine(); + + bool fAllToMe = true; + BOOST_FOREACH(const CTxOut& txout, wtx.vout) + fAllToMe = fAllToMe && txout.IsMine(); + + if (fAllFromMe && fAllToMe) + { + // Payment to self + int64 nChange = wtx.GetChange(); + + parts.append(TransactionRecord(hash, nTime, TransactionRecord::SendToSelf, "", + -(nDebit - nChange), nCredit - nChange)); + } + else if (fAllFromMe) + { + // + // Debit + // + int64 nTxFee = nDebit - wtx.GetValueOut(); + + for (int nOut = 0; nOut < wtx.vout.size(); nOut++) + { + const CTxOut& txout = wtx.vout[nOut]; + TransactionRecord sub(hash, nTime); + sub.idx = parts.size(); + + if (txout.IsMine()) + { + // Ignore parts sent to self, as this is usually the change + // from a transaction sent back to our own address. + continue; + } + else if (!mapValue["to"].empty()) + { + // Sent to IP + sub.type = TransactionRecord::SendToIP; + sub.address = mapValue["to"]; + } + else + { + // Sent to Bitcoin Address + sub.type = TransactionRecord::SendToAddress; + uint160 hash160; + if (ExtractHash160(txout.scriptPubKey, hash160)) + sub.address = Hash160ToAddress(hash160); + } + + int64 nValue = txout.nValue; + /* Add fee to first output */ + if (nTxFee > 0) + { + nValue += nTxFee; + nTxFee = 0; + } + sub.debit = -nValue; + + parts.append(sub); + } + } + else + { + // + // Mixed debit transaction, can't break down payees + // + bool fAllMine = true; + BOOST_FOREACH(const CTxOut& txout, wtx.vout) + fAllMine = fAllMine && txout.IsMine(); + BOOST_FOREACH(const CTxIn& txin, wtx.vin) + fAllMine = fAllMine && txin.IsMine(); + + parts.append(TransactionRecord(hash, nTime, TransactionRecord::Other, "", nNet, 0)); + } + } + } + + return parts; +} + +void TransactionRecord::updateStatus(const CWalletTx &wtx) +{ + // Determine transaction status + + // Find the block the tx is in + CBlockIndex* pindex = NULL; + std::map<uint256, CBlockIndex*>::iterator mi = mapBlockIndex.find(wtx.hashBlock); + if (mi != mapBlockIndex.end()) + pindex = (*mi).second; + + // Sort order, unrecorded transactions sort to the top + status.sortKey = strprintf("%010d-%01d-%010u-%03d", + (pindex ? pindex->nHeight : INT_MAX), + (wtx.IsCoinBase() ? 1 : 0), + wtx.nTimeReceived, + idx); + status.confirmed = wtx.IsConfirmed(); + status.depth = wtx.GetDepthInMainChain(); + status.cur_num_blocks = nBestHeight; + + if (!wtx.IsFinal()) + { + if (wtx.nLockTime < 500000000) + { + status.status = TransactionStatus::OpenUntilBlock; + status.open_for = nBestHeight - wtx.nLockTime; + } + else + { + status.status = TransactionStatus::OpenUntilDate; + status.open_for = wtx.nLockTime; + } + } + else + { + if (GetAdjustedTime() - wtx.nTimeReceived > 2 * 60 && wtx.GetRequestCount() == 0) + { + status.status = TransactionStatus::Offline; + } + else if (status.depth < 6) + { + status.status = TransactionStatus::Unconfirmed; + } + else + { + status.status = TransactionStatus::HaveConfirmations; + } + } + + // For generated transactions, determine maturity + if(type == TransactionRecord::Generated) + { + int64 nCredit = wtx.GetCredit(true); + if (nCredit == 0) + { + status.maturity = TransactionStatus::Immature; + + if (wtx.IsInMainChain()) + { + status.matures_in = wtx.GetBlocksToMaturity(); + + // Check if the block was requested by anyone + if (GetAdjustedTime() - wtx.nTimeReceived > 2 * 60 && wtx.GetRequestCount() == 0) + status.maturity = TransactionStatus::MaturesWarning; + } + else + { + status.maturity = TransactionStatus::NotAccepted; + } + } + else + { + status.maturity = TransactionStatus::Mature; + } + } +} + +bool TransactionRecord::statusUpdateNeeded() +{ + return status.cur_num_blocks != nBestHeight; +} diff --git a/src/qt/transactionrecord.h b/src/qt/transactionrecord.h new file mode 100644 index 0000000000..c082fffe9c --- /dev/null +++ b/src/qt/transactionrecord.h @@ -0,0 +1,109 @@ +#ifndef TRANSACTIONRECORD_H +#define TRANSACTIONRECORD_H + +#include "main.h" + +#include <QList> + +class TransactionStatus +{ +public: + TransactionStatus(): + confirmed(false), sortKey(""), maturity(Mature), + matures_in(0), status(Offline), depth(0), open_for(0), cur_num_blocks(-1) + { } + + enum Maturity + { + Immature, + Mature, + MaturesWarning, /* Will likely not mature because no nodes have confirmed */ + NotAccepted + }; + + enum Status { + OpenUntilDate, + OpenUntilBlock, + Offline, + Unconfirmed, + HaveConfirmations + }; + + bool confirmed; + std::string sortKey; + + /* For "Generated" transactions */ + Maturity maturity; + int matures_in; + + /* Reported status */ + Status status; + int64 depth; + int64 open_for; /* Timestamp if status==OpenUntilDate, otherwise number of blocks */ + + /* Current number of blocks (to know whether cached status is still valid. */ + int cur_num_blocks; +}; + +class TransactionRecord +{ +public: + enum Type + { + Other, + Generated, + SendToAddress, + SendToIP, + RecvWithAddress, + RecvFromIP, + SendToSelf + }; + + TransactionRecord(): + hash(), time(0), type(Other), address(""), debit(0), credit(0), idx(0) + { + } + + TransactionRecord(uint256 hash, int64 time): + hash(hash), time(time), type(Other), address(""), debit(0), + credit(0), idx(0) + { + } + + TransactionRecord(uint256 hash, int64 time, + Type type, const std::string &address, + int64 debit, int64 credit): + hash(hash), time(time), type(type), address(address), debit(debit), credit(credit), + idx(0) + { + } + + /* Decompose CWallet transaction to model transaction records. + */ + static bool showTransaction(const CWalletTx &wtx); + static QList<TransactionRecord> decomposeTransaction(const CWalletTx &wtx); + + /* Fixed */ + uint256 hash; + int64 time; + Type type; + std::string address; + int64 debit; + int64 credit; + + /* Subtransaction index, for sort key */ + int idx; + + /* Status: can change with block chain update */ + TransactionStatus status; + + /* Update status from wallet tx. + */ + void updateStatus(const CWalletTx &wtx); + + /* Is a status update needed? + */ + bool statusUpdateNeeded(); +}; + +#endif // TRANSACTIONRECORD_H diff --git a/src/qt/transactiontablemodel.cpp b/src/qt/transactiontablemodel.cpp new file mode 100644 index 0000000000..8fe1839930 --- /dev/null +++ b/src/qt/transactiontablemodel.cpp @@ -0,0 +1,512 @@ +#include "transactiontablemodel.h" +#include "guiutil.h" +#include "transactionrecord.h" +#include "guiconstants.h" +#include "main.h" +#include "transactiondesc.h" + +#include <QLocale> +#include <QDebug> +#include <QList> +#include <QColor> +#include <QTimer> +#include <QtAlgorithms> + +const QString TransactionTableModel::Sent = "s"; +const QString TransactionTableModel::Received = "r"; +const QString TransactionTableModel::Other = "o"; + +/* Comparison operator for sort/binary search of model tx list */ +struct TxLessThan +{ + bool operator()(const TransactionRecord &a, const TransactionRecord &b) const + { + return a.hash < b.hash; + } + bool operator()(const TransactionRecord &a, const uint256 &b) const + { + return a.hash < b; + } + bool operator()(const uint256 &a, const TransactionRecord &b) const + { + return a < b.hash; + } +}; + +/* Private implementation */ +struct TransactionTablePriv +{ + TransactionTablePriv(TransactionTableModel *parent): + parent(parent) + { + } + + TransactionTableModel *parent; + + /* Local cache of wallet. + * As it is in the same order as the CWallet, by definition + * this is sorted by sha256. + */ + QList<TransactionRecord> cachedWallet; + + void refreshWallet() + { + qDebug() << "refreshWallet"; + + /* Query entire wallet from core. + */ + cachedWallet.clear(); + CRITICAL_BLOCK(cs_mapWallet) + { + for(std::map<uint256, CWalletTx>::iterator it = mapWallet.begin(); it != mapWallet.end(); ++it) + { + cachedWallet.append(TransactionRecord::decomposeTransaction(it->second)); + } + } + } + + /* Update our model of the wallet incrementally. + Call with list of hashes of transactions that were added, removed or changed. + */ + void updateWallet(const QList<uint256> &updated) + { + /* Walk through updated transactions, update model as needed. + */ + qDebug() << "updateWallet"; + + /* Sort update list, and iterate through it in reverse, so that model updates + can be emitted from end to beginning (so that earlier updates will not influence + the indices of latter ones). + */ + QList<uint256> updated_sorted = updated; + qSort(updated_sorted); + + CRITICAL_BLOCK(cs_mapWallet) + { + for(int update_idx = updated_sorted.size()-1; update_idx >= 0; --update_idx) + { + const uint256 &hash = updated_sorted.at(update_idx); + /* Find transaction in wallet */ + std::map<uint256, CWalletTx>::iterator mi = mapWallet.find(hash); + bool inWallet = mi != mapWallet.end(); + /* Find bounds of this transaction in model */ + QList<TransactionRecord>::iterator lower = qLowerBound( + cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan()); + QList<TransactionRecord>::iterator upper = qUpperBound( + cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan()); + int lowerIndex = (lower - cachedWallet.begin()); + int upperIndex = (upper - cachedWallet.begin()); + + bool inModel = false; + if(lower != upper) + { + inModel = true; + } + + qDebug() << " " << QString::fromStdString(hash.ToString()) << inWallet << " " << inModel + << lowerIndex << "-" << upperIndex; + + if(inWallet && !inModel) + { + /* Added */ + QList<TransactionRecord> toInsert = + TransactionRecord::decomposeTransaction(mi->second); + if(!toInsert.isEmpty()) /* only if something to insert */ + { + parent->beginInsertRows(QModelIndex(), lowerIndex, lowerIndex+toInsert.size()-1); + int insert_idx = lowerIndex; + foreach(const TransactionRecord &rec, toInsert) + { + cachedWallet.insert(insert_idx, rec); + insert_idx += 1; + } + parent->endInsertRows(); + } + } + else if(!inWallet && inModel) + { + /* Removed */ + parent->beginRemoveRows(QModelIndex(), lowerIndex, upperIndex-1); + cachedWallet.erase(lower, upper); + parent->endRemoveRows(); + } + else if(inWallet && inModel) + { + /* Updated -- nothing to do, status update will take care of this */ + } + } + } + } + + int size() + { + return cachedWallet.size(); + } + + TransactionRecord *index(int idx) + { + if(idx >= 0 && idx < cachedWallet.size()) + { + TransactionRecord *rec = &cachedWallet[idx]; + + /* If a status update is needed (blocks came in since last check), + update the status of this transaction from the wallet. Otherwise, + simply re-use the cached status. + */ + if(rec->statusUpdateNeeded()) + { + CRITICAL_BLOCK(cs_mapWallet) + { + std::map<uint256, CWalletTx>::iterator mi = mapWallet.find(rec->hash); + + if(mi != mapWallet.end()) + { + rec->updateStatus(mi->second); + } + } + } + return rec; + } + else + { + return 0; + } + } + + QString describe(TransactionRecord *rec) + { + CRITICAL_BLOCK(cs_mapWallet) + { + std::map<uint256, CWalletTx>::iterator mi = mapWallet.find(rec->hash); + if(mi != mapWallet.end()) + { + return QString::fromStdString(TransactionDesc::toHTML(mi->second)); + } + } + return QString(""); + } + +}; + +/* Credit and Debit columns are right-aligned as they contain numbers */ +static int column_alignments[] = { + Qt::AlignLeft|Qt::AlignVCenter, + Qt::AlignLeft|Qt::AlignVCenter, + Qt::AlignLeft|Qt::AlignVCenter, + Qt::AlignRight|Qt::AlignVCenter, + Qt::AlignRight|Qt::AlignVCenter, + Qt::AlignLeft|Qt::AlignVCenter + }; + +TransactionTableModel::TransactionTableModel(QObject *parent): + QAbstractTableModel(parent), + priv(new TransactionTablePriv(this)) +{ + columns << tr("Status") << tr("Date") << tr("Description") << tr("Debit") << tr("Credit"); + + priv->refreshWallet(); + + QTimer *timer = new QTimer(this); + connect(timer, SIGNAL(timeout()), this, SLOT(update())); + timer->start(MODEL_UPDATE_DELAY); +} + +TransactionTableModel::~TransactionTableModel() +{ + delete priv; +} + +void TransactionTableModel::update() +{ + QList<uint256> updated; + + /* Check if there are changes to wallet map */ + TRY_CRITICAL_BLOCK(cs_mapWallet) + { + if(!vWalletUpdated.empty()) + { + BOOST_FOREACH(uint256 hash, vWalletUpdated) + { + updated.append(hash); + } + vWalletUpdated.clear(); + } + } + + if(!updated.empty()) + { + priv->updateWallet(updated); + + /* Status (number of confirmations) and (possibly) description + columns changed for all rows. + */ + emit dataChanged(index(0, Status), index(priv->size()-1, Status)); + emit dataChanged(index(0, Description), index(priv->size()-1, Description)); + } +} + +int TransactionTableModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return priv->size(); +} + +int TransactionTableModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return columns.length(); +} + +QVariant TransactionTableModel::formatTxStatus(const TransactionRecord *wtx) const +{ + QString status; + + switch(wtx->status.status) + { + case TransactionStatus::OpenUntilBlock: + status = tr("Open for %n block(s)","",wtx->status.open_for); + break; + case TransactionStatus::OpenUntilDate: + status = tr("Open until ") + GUIUtil::DateTimeStr(wtx->status.open_for); + break; + case TransactionStatus::Offline: + status = tr("%1/offline").arg(wtx->status.depth); + break; + case TransactionStatus::Unconfirmed: + status = tr("%1/unconfirmed").arg(wtx->status.depth); + break; + case TransactionStatus::HaveConfirmations: + status = tr("%1 confirmations").arg(wtx->status.depth); + break; + } + + return QVariant(status); +} + +QVariant TransactionTableModel::formatTxDate(const TransactionRecord *wtx) const +{ + if(wtx->time) + { + return QVariant(GUIUtil::DateTimeStr(wtx->time)); + } + else + { + return QVariant(); + } +} + +/* Look up address in address book, if found return + address[0:12]... (label) + otherwise just return address + */ +std::string lookupAddress(const std::string &address) +{ + std::string description; + CRITICAL_BLOCK(cs_mapAddressBook) + { + std::map<std::string, std::string>::iterator mi = mapAddressBook.find(address); + if (mi != mapAddressBook.end() && !(*mi).second.empty()) + { + std::string label = (*mi).second; + description += address.substr(0,12) + "... "; + description += "(" + label + ")"; + } + else + { + description += address; + } + } + return description; +} + +QVariant TransactionTableModel::formatTxDescription(const TransactionRecord *wtx) const +{ + QString description; + + switch(wtx->type) + { + case TransactionRecord::RecvWithAddress: + description = tr("Received with: ") + QString::fromStdString(lookupAddress(wtx->address)); + break; + case TransactionRecord::RecvFromIP: + description = tr("Received from IP: ") + QString::fromStdString(wtx->address); + break; + case TransactionRecord::SendToAddress: + description = tr("Sent to: ") + QString::fromStdString(lookupAddress(wtx->address)); + break; + case TransactionRecord::SendToIP: + description = tr("Sent to IP: ") + QString::fromStdString(wtx->address); + break; + case TransactionRecord::SendToSelf: + description = tr("Payment to yourself"); + break; + case TransactionRecord::Generated: + switch(wtx->status.maturity) + { + case TransactionStatus::Immature: + description = tr("Generated (matures in %n more blocks)", "", + wtx->status.matures_in); + break; + case TransactionStatus::Mature: + description = tr("Generated"); + break; + case TransactionStatus::MaturesWarning: + description = tr("Generated - Warning: This block was not received by any other nodes and will probably not be accepted!"); + break; + case TransactionStatus::NotAccepted: + description = tr("Generated (not accepted)"); + break; + } + break; + } + return QVariant(description); +} + +QVariant TransactionTableModel::formatTxDebit(const TransactionRecord *wtx) const +{ + if(wtx->debit) + { + QString str = QString::fromStdString(FormatMoney(wtx->debit)); + if(!wtx->status.confirmed || wtx->status.maturity != TransactionStatus::Mature) + { + str = QString("[") + str + QString("]"); + } + return QVariant(str); + } + else + { + return QVariant(); + } +} + +QVariant TransactionTableModel::formatTxCredit(const TransactionRecord *wtx) const +{ + if(wtx->credit) + { + QString str = QString::fromStdString(FormatMoney(wtx->credit)); + if(!wtx->status.confirmed || wtx->status.maturity != TransactionStatus::Mature) + { + str = QString("[") + str + QString("]"); + } + return QVariant(str); + } + else + { + return QVariant(); + } +} + +QVariant TransactionTableModel::data(const QModelIndex &index, int role) const +{ + if(!index.isValid()) + return QVariant(); + TransactionRecord *rec = static_cast<TransactionRecord*>(index.internalPointer()); + + if(role == Qt::DisplayRole) + { + /* Delegate to specific column handlers */ + switch(index.column()) + { + case Status: + return formatTxStatus(rec); + case Date: + return formatTxDate(rec); + case Description: + return formatTxDescription(rec); + case Debit: + return formatTxDebit(rec); + case Credit: + return formatTxCredit(rec); + } + } + else if(role == Qt::EditRole) + { + /* Edit role is used for sorting so return the real values */ + switch(index.column()) + { + case Status: + return QString::fromStdString(rec->status.sortKey); + case Date: + return rec->time; + case Description: + return formatTxDescription(rec); + case Debit: + return rec->debit; + case Credit: + return rec->credit; + } + } + else if (role == Qt::TextAlignmentRole) + { + return column_alignments[index.column()]; + } + else if (role == Qt::ForegroundRole) + { + /* Non-confirmed transactions are grey */ + if(rec->status.confirmed) + { + return QColor(0, 0, 0); + } + else + { + return QColor(128, 128, 128); + } + } + else if (role == TypeRole) + { + /* Role for filtering tabs by type */ + switch(rec->type) + { + case TransactionRecord::RecvWithAddress: + case TransactionRecord::RecvFromIP: + return TransactionTableModel::Received; + case TransactionRecord::SendToAddress: + case TransactionRecord::SendToIP: + case TransactionRecord::SendToSelf: + return TransactionTableModel::Sent; + default: + return TransactionTableModel::Other; + } + } + else if (role == LongDescriptionRole) + { + return priv->describe(rec); + } + return QVariant(); +} + +QVariant TransactionTableModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal) + { + if(role == Qt::DisplayRole) + { + return columns[section]; + } + else if (role == Qt::TextAlignmentRole) + { + return column_alignments[section]; + } + } + return QVariant(); +} + +Qt::ItemFlags TransactionTableModel::flags(const QModelIndex &index) const +{ + return QAbstractTableModel::flags(index); +} + +QModelIndex TransactionTableModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent); + TransactionRecord *data = priv->index(row); + if(data) + { + return createIndex(row, column, priv->index(row)); + } + else + { + return QModelIndex(); + } +} + diff --git a/src/qt/transactiontablemodel.h b/src/qt/transactiontablemodel.h new file mode 100644 index 0000000000..5974c0e7fe --- /dev/null +++ b/src/qt/transactiontablemodel.h @@ -0,0 +1,58 @@ +#ifndef TRANSACTIONTABLEMODEL_H +#define TRANSACTIONTABLEMODEL_H + +#include <QAbstractTableModel> +#include <QStringList> + +class TransactionTablePriv; +class TransactionRecord; + +class TransactionTableModel : public QAbstractTableModel +{ + Q_OBJECT +public: + explicit TransactionTableModel(QObject *parent = 0); + ~TransactionTableModel(); + + enum { + Status = 0, + Date = 1, + Description = 2, + Debit = 3, + Credit = 4 + } ColumnIndex; + + enum { + TypeRole = Qt::UserRole, + LongDescriptionRole = Qt::UserRole+1 + } RoleIndex; + + /* TypeRole values */ + static const QString Sent; + static const QString Received; + static const QString Other; + + int rowCount(const QModelIndex &parent) const; + int columnCount(const QModelIndex &parent) const; + QVariant data(const QModelIndex &index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const; +private: + QStringList columns; + TransactionTablePriv *priv; + + QVariant formatTxStatus(const TransactionRecord *wtx) const; + QVariant formatTxDate(const TransactionRecord *wtx) const; + QVariant formatTxDescription(const TransactionRecord *wtx) const; + QVariant formatTxDebit(const TransactionRecord *wtx) const; + QVariant formatTxCredit(const TransactionRecord *wtx) const; + +private slots: + void update(); + + friend class TransactionTablePriv; +}; + +#endif + |