diff --git a/denarius-qt.pro b/denarius-qt.pro index 5b4769e5..1cc24321 100644 --- a/denarius-qt.pro +++ b/denarius-qt.pro @@ -541,6 +541,8 @@ HEADERS += src/qt/bitcoingui.h \ src/sph_types.h \ src/threadsafety.h \ src/eccryptoverify.h \ + src/qt/nametablemodel.h \ + src/qt/managenamespage.h \ src/qt/messagepage.h \ src/qt/messagemodel.h \ src/qt/sendmessagesdialog.h \ @@ -635,6 +637,8 @@ SOURCES += src/qt/bitcoin.cpp src/qt/bitcoingui.cpp \ src/qt/qtipcserver.cpp \ src/qt/rpcconsole.cpp \ src/qt/trafficgraphwidget.cpp \ + src/qt/nametablemodel.cpp \ + src/qt/managenamespage.cpp \ src/qt/messagepage.cpp \ src/qt/messagemodel.cpp \ src/qt/qcustomplot.cpp \ @@ -694,6 +698,7 @@ FORMS += \ src/qt/forms/multisigaddressentry.ui \ src/qt/forms/multisiginputentry.ui \ src/qt/forms/multisigdialog.ui \ + src/qt/forms/managenamespage.ui \ src/qt/forms/sendmessagesentry.ui \ src/qt/forms/sendmessagesdialog.ui \ src/qt/plugins/mrichtexteditor/mrichtextedit.ui diff --git a/src/main.cpp b/src/main.cpp index ab8b25d5..c7a3035d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3112,8 +3112,11 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck, boo return error("ConnectBlock() : WriteBlockIndex failed"); } - // add names to denariusnamesindex.dat - hooks->ConnectBlock(txdb, pindex); + // Check Name Release Height to Connect Blocks + if (pindex->nHeight >= RELEASE_HEIGHT) { + // add names to denariusnamesindex.dat + hooks->ConnectBlock(txdb, pindex); + } // Watch for transactions paying to me BOOST_FOREACH(CTransaction& tx, vtx) diff --git a/src/namecoin.cpp b/src/namecoin.cpp index 710ed317..8f695216 100644 --- a/src/namecoin.cpp +++ b/src/namecoin.cpp @@ -545,7 +545,7 @@ string SendMoneyWithInputTx(CScript scriptPubKey, int64_t nValue, int64_t nNetFe return ""; } -// scans nameindex.dat and return names with their last CNameIndex +// scans denariusnamesindex.dat and return names with their last CNameIndex bool CNameDB::ScanNames( const vector& vchName, unsigned int nMax, @@ -1317,7 +1317,7 @@ Value name_scan(const Array& params, bool fHelp) vector vchName; int nMax = 500; - int mMaxShownValue = -1; + int mMaxShownValue = 10; //-1 for unlimited values if (params.size() > 0) { vchName = vchFromValue(params[0]); @@ -1356,7 +1356,7 @@ Value name_scan(const Array& params, bool fHelp) vector vchValue = txName.vchValue; string value = stringFromVch(vchValue); - oName.push_back(Pair("value", limitString(value, mMaxShownValue, "\n...(value too large - use name_show to see full value)"))); + oName.push_back(Pair("value", limitString(value, mMaxShownValue, "\n...(value redacted - use name_show to see full value)"))); oName.push_back(Pair("expires_in", nExpiresAt - pindexBest->nHeight)); if (nExpiresAt - pindexBest->nHeight <= 0) oName.push_back(Pair("expired", true)); @@ -2380,7 +2380,7 @@ bool CNamecoinHooks::DisconnectInputs(const CTransaction& tx) // be empty, since a reorg cannot go that far back. Be safe anyway and do not try to pop if empty. if (nameRec.vtxPos.size() > 0) { - // check if tx matches last tx in nameindex.dat + // check if tx matches last tx in denariusnamesindex.dat CTransaction lastTx; lastTx.ReadFromDisk(nameRec.vtxPos.back().txPos); assert(lastTx.GetHash() == tx.GetHash()); diff --git a/src/qt/bitcoin.qrc b/src/qt/bitcoin.qrc index 5d8b194e..fb2db60e 100644 --- a/src/qt/bitcoin.qrc +++ b/src/qt/bitcoin.qrc @@ -16,6 +16,7 @@ res/icons/mark.png res/icons/btc.png res/icons/mn.png + res/icons/names.png res/icons/multi.png res/icons/connect0_16.png res/icons/connect1_16.png diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index fc843fba..b94cd65a 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -38,6 +38,7 @@ #include "termsofuse.h" #include "proofofimage.h" #include "jupiter.h" +#include "managenamespage.h" #ifdef Q_OS_MAC #include "macdockiconhandler.h" @@ -191,6 +192,7 @@ BitcoinGUI::BitcoinGUI(QWidget *parent): multisigPage = new MultisigDialog(this); proofOfImagePage = new ProofOfImage(this); jupiterPage = new Jupiter(this); + manageNamesPage = new ManageNamesPage(this); //chatWindow = new ChatWindow(this); transactionsPage = new QWidget(this); @@ -220,6 +222,7 @@ BitcoinGUI::BitcoinGUI(QWidget *parent): centralWidget->addWidget(overviewPage); centralWidget->addWidget(transactionsPage); centralWidget->addWidget(mintingPage); + centralWidget->addWidget(manageNamesPage); centralWidget->addWidget(addressBookPage); centralWidget->addWidget(receiveCoinsPage); centralWidget->addWidget(sendCoinsPage); @@ -353,6 +356,12 @@ void BitcoinGUI::createActions() marketAction->setCheckable(true); tabGroup->addAction(marketAction); + manageNamesAction = new QAction(QIcon(":/icons/names"), tr("&Manage NVS"), this); + manageNamesAction->setToolTip(tr("Manage Denarius NVS")); + manageNamesAction->setCheckable(true); + manageNamesAction->setShortcut(QKeySequence(Qt::ALT + Qt::Key_0)); + tabGroup->addAction(manageNamesAction); + //chatAction = new QAction(QIcon(":/icons/msg"), tr("&Social"), this); //chatAction->setToolTip(tr("View chat")); //chatAction->setCheckable(true); @@ -432,6 +441,8 @@ void BitcoinGUI::createActions() connect(sendCoinsAction, SIGNAL(triggered()), this, SLOT(gotoSendCoinsPage())); connect(receiveCoinsAction, SIGNAL(triggered()), this, SLOT(showNormalIfMinimized())); connect(receiveCoinsAction, SIGNAL(triggered()), this, SLOT(gotoReceiveCoinsPage())); + connect(manageNamesAction, SIGNAL(triggered()), this, SLOT(showNormalIfMinimized())); + connect(manageNamesAction, SIGNAL(triggered()), this, SLOT(gotoManageNamesPage())); connect(mintingAction, SIGNAL(triggered()), this, SLOT(showNormalIfMinimized())); connect(mintingAction, SIGNAL(triggered()), this, SLOT(gotoMintingPage())); connect(fortunastakeManagerAction, SIGNAL(triggered()), this, SLOT(showNormalIfMinimized())); @@ -585,8 +596,9 @@ void BitcoinGUI::createToolBars() mainToolbar->addAction(addressBookAction); mainToolbar->addAction(statisticsAction); mainToolbar->addAction(fortunastakeManagerAction); - mainToolbar->addAction(proofOfImageAction); + mainToolbar->addAction(manageNamesAction); mainToolbar->addAction(jupiterAction); + mainToolbar->addAction(proofOfImageAction); mainToolbar->addAction(marketAction); mainToolbar->addAction(blockAction); mainToolbar->addAction(messageAction); @@ -696,6 +708,7 @@ void BitcoinGUI::setWalletModel(WalletModel *walletModel) marketBrowser->setModel(clientModel); fortunastakeManagerPage->setWalletModel(walletModel); multisigPage->setModel(walletModel); + manageNamesPage->setModel(walletModel); //chatWindow->setModel(clientModel); setEncryptionStatus(walletModel->getEncryptionStatus()); @@ -1157,6 +1170,16 @@ void BitcoinGUI::gotoMarketBrowser() } +void BitcoinGUI::gotoManageNamesPage() +{ + manageNamesAction->setChecked(true); + centralWidget->setCurrentWidget(manageNamesPage); + + exportAction->setEnabled(true); + disconnect(exportAction, SIGNAL(triggered()), 0, 0); + connect(exportAction, SIGNAL(triggered()), manageNamesPage, SLOT(exportClicked())); +} + void BitcoinGUI::gotoProofOfImagePage() { proofOfImageAction->setChecked(true); diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index b97d559d..33e363ec 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -12,6 +12,7 @@ class WalletModel; class MessageModel; class TransactionView; class MintingView; +class ManageNamesPage; class FortunastakeManager; class MultisigDialog; class OverviewPage; @@ -96,6 +97,7 @@ class BitcoinGUI : public QMainWindow OverviewPage *overviewPage; StatisticsPage *statisticsPage; BlockBrowser *blockBrowser; + ManageNamesPage *manageNamesPage; MarketBrowser *marketBrowser; QWidget *transactionsPage; QWidget *mintingPage; @@ -131,6 +133,7 @@ class BitcoinGUI : public QMainWindow QAction *multisigAction; QAction *proofOfImageAction; QAction *jupiterAction; + QAction *manageNamesAction; QAction *fortunastakeManagerAction; QAction *quitAction; QAction *sendCoinsAction; @@ -222,6 +225,8 @@ private slots: void gotoBlockBrowser(); /** Switch to market*/ void gotoMarketBrowser(); + /** Switch to manage names page */ + void gotoManageNamesPage(); /** Switch to minting page */ void gotoMintingPage(); /** Switch to history (transactions) page */ diff --git a/src/qt/forms/managenamespage.ui b/src/qt/forms/managenamespage.ui new file mode 100644 index 00000000..965ffc28 --- /dev/null +++ b/src/qt/forms/managenamespage.ui @@ -0,0 +1,412 @@ + + + ManageNamesPage + + + + 0 + 0 + 715 + 476 + + + + + 0 + 0 + + + + Manage Names + + + + + + + + + 0 + 0 + + + + Name: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + <html><head/><body><p>Enter a Name.</p></body></html> + + + + + + dns:denarius.d or @yourname + + + + + + + Days: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + <html><head/><body><p>Set the number of days that will be added to the Name expire time.</p></body></html> + + + 9999 + + + 30 + + + + + + + 0 + + + + NAME_NEW + + + + + NAME_UPDATE + + + + + NAME_DELETE + + + + + + + + + + + + + + + 0 + 0 + + + + Value(0%): + + + Qt::AlignCenter + + + + + + + Import + + + + + + + + + + 0 + 0 + + + + A=172.168.xx.xx,172.168.xx.xx|CNAME=denarius.io|TTL=600 + + + + + + + + + + + true + + + + 0 + 0 + + + + D Address (leave blank for default): + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + true + + + <html><head/><body><p>Enter a new owner address, or leave blank to keep the current ownership.</p></body></html> + + + + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + <html><head/><body><p>This will send a name_new or name_update transaction (if you already did name_new).</p></body></html> + + + Qt::LeftToRight + + + &Submit + + + + :/icons/send:/icons/send + + + true + + + + + + + + + 1 + + + + My Denarius Names + + + + + + 0 + + + + + + 0 + 0 + + + + Enter part of name to search for + + + + + + + + + + Enter part of value to search for + + + + + + + + + + + 0 + 0 + + + + Enter Namecoin address (or prefix of it) + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 100 + 20 + + + + + + + + + + + 0 + 0 + + + + Qt::CustomContextMenu + + + Double-click name to configure + + + Qt::ScrollBarAlwaysOn + + + false + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + true + + + false + + + + + + + + Global Denarius Names + + + + + + ArrowCursor + + + Refresh + + + false + + + false + + + + + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::SelectRows + + + false + + + 3 + + + false + + + true + + + + Name + + + + + Expires In + + + + + Value + + + + + + + + + + + + + + + + + + + + QValidatedLineEdit + QLineEdit +
qvalidatedlineedit.h
+
+
+ + + + +
diff --git a/src/qt/guiconstants.h b/src/qt/guiconstants.h index daf37d77..c0bbd138 100644 --- a/src/qt/guiconstants.h +++ b/src/qt/guiconstants.h @@ -39,4 +39,8 @@ static const int MAX_URI_LENGTH = 255; #define COLOR_MINT_MATURE QColor(29, 99, 0) #define COLOR_MINT_OLD QColor(99, 0, 23) +// Should be set to MAX_VALUE_LENGTH (from namecoin.h) when it's supported by the network +// (currently due to limitations of CScript the limit is 519 bytes) +static const int GUI_MAX_VALUE_LENGTH = 519; + #endif // GUICONSTANTS_H diff --git a/src/qt/managenamespage.cpp b/src/qt/managenamespage.cpp new file mode 100644 index 00000000..abc980b9 --- /dev/null +++ b/src/qt/managenamespage.cpp @@ -0,0 +1,689 @@ +#include "managenamespage.h" +#include "ui_managenamespage.h" + +#include "walletmodel.h" +#include "nametablemodel.h" +#include "csvmodelwriter.h" +#include "guiutil.h" +#include "base58.h" +#include "main.h" +#include "wallet.h" +#include "guiconstants.h" +#include "ui_interface.h" + +#include +#include +#include +#include +#include +#include + +// +// NameFilterProxyModel +// + +NameFilterProxyModel::NameFilterProxyModel(QObject *parent /* = 0*/) + : QSortFilterProxyModel(parent) +{ +} + +void NameFilterProxyModel::setNameSearch(const QString &search) +{ + nameSearch = search; + invalidateFilter(); +} + +void NameFilterProxyModel::setValueSearch(const QString &search) +{ + valueSearch = search; + invalidateFilter(); +} + +void NameFilterProxyModel::setAddressSearch(const QString &search) +{ + addressSearch = search; + invalidateFilter(); +} + +bool NameFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + + QString name = index.sibling(index.row(), NameTableModel::Name).data(Qt::EditRole).toString(); + QString value = index.sibling(index.row(), NameTableModel::Value).data(Qt::EditRole).toString(); + QString address = index.sibling(index.row(), NameTableModel::Address).data(Qt::EditRole).toString(); + + Qt::CaseSensitivity case_sens = filterCaseSensitivity(); + return name.contains(nameSearch, case_sens) + && value.contains(valueSearch, case_sens) + && address.startsWith(addressSearch, Qt::CaseSensitive); // Address is always case-sensitive +} + +bool NameFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + NameTableEntry *rec1 = static_cast(left.internalPointer()); + NameTableEntry *rec2 = static_cast(right.internalPointer()); + + switch (left.column()) + { + case NameTableModel::Name: + return QString::localeAwareCompare(rec1->name, rec2->name) < 0; + case NameTableModel::Value: + return QString::localeAwareCompare(rec1->value, rec2->value) < 0; + case NameTableModel::Address: + return QString::localeAwareCompare(rec1->address, rec2->address) < 0; + case NameTableModel::ExpiresIn: + return rec1->nExpiresAt < rec2->nExpiresAt; + } + + // should never reach here + return QString::localeAwareCompare(rec1->name, rec2->name) < 0; +} + +// +// ManageNamesPage +// + +const static int COLUMN_WIDTH_NAME = 300, + COLUMN_WIDTH_ADDRESS = 256, + COLUMN_WIDTH_EXPIRES_IN = 100; + +ManageNamesPage::ManageNamesPage(QWidget *parent) : + QDialog(parent), + ui(new Ui::ManageNamesPage), + model(0), + walletModel(0), + proxyModel(0) +{ + ui->setupUi(this); + + ui->tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + ui->tableWidget->setSortingEnabled(true); + + timer = new QTimer(this); + QTimer::singleShot(1000, this, SLOT(updateNames())); + QTimer::singleShot(5000, this, SLOT(updateNames())); + QTimer::singleShot(10000, this, SLOT(updateNames())); + QTimer::singleShot(30000, this, SLOT(updateNames())); // try to load the name list ASAP for the user + QTimer::singleShot(60000, this, SLOT(updateNames())); + + // Context menu actions + QAction *copyNameAction = new QAction(tr("Copy &Name"), this); + QAction *copyValueAction = new QAction(tr("Copy &Value"), this); + QAction *copyAddressAction = new QAction(tr("Copy &Address"), this); + QAction *copyAllAction = new QAction(tr("Copy all to edit boxes"), this); + QAction *saveValueAsBinaryAction = new QAction(tr("Save value as binary file"), this); + + // Build context menu + contextMenu = new QMenu(); + contextMenu->addAction(copyNameAction); + contextMenu->addAction(copyValueAction); + contextMenu->addAction(copyAddressAction); + contextMenu->addAction(copyAllAction); + contextMenu->addAction(saveValueAsBinaryAction); + + // Connect signals for context menu actions + connect(copyNameAction, SIGNAL(triggered()), this, SLOT(onCopyNameAction())); + connect(copyValueAction, SIGNAL(triggered()), this, SLOT(onCopyValueAction())); + connect(copyAddressAction, SIGNAL(triggered()), this, SLOT(onCopyAddressAction())); + connect(copyAllAction, SIGNAL(triggered()), this, SLOT(onCopyAllAction())); + connect(saveValueAsBinaryAction, SIGNAL(triggered()), this, SLOT(onSaveValueAsBinaryAction())); + connect(ui->refreshButton, SIGNAL(pressed()), this, SLOT( updateNames())); + + connect(ui->tableView, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(contextualMenu(QPoint))); + ui->tableView->setEditTriggers(QAbstractItemView::NoEditTriggers); + + // connect(ui->tableView_2, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(contextualMenu(QPoint))); + // ui->tableView_2->setEditTriggers(QAbstractItemView::NoEditTriggers); + + // Reset gui sizes and visibility (for name new) + ui->registerAddress->setEnabled(true); + + ui->tableWidget->setEnabled(true); + + // Catch focus changes to make the appropriate button the default one (Submit or Configure) + ui->registerName->installEventFilter(this); + ui->registerValue->installEventFilter(this); + ui->txTypeSelector->installEventFilter(this); + ui->submitNameButton->installEventFilter(this); + ui->tableView->installEventFilter(this); + ui->nameFilter->installEventFilter(this); + ui->valueFilter->installEventFilter(this); + ui->addressFilter->installEventFilter(this); + + ui->registerName->setMaxLength(MAX_NAME_LENGTH); + + ui->nameFilter->setMaxLength(MAX_NAME_LENGTH); + ui->valueFilter->setMaxLength(GUI_MAX_VALUE_LENGTH); + GUIUtil::setupAddressWidget(ui->addressFilter, this); + +#if QT_VERSION >= 0x040700 + /* Do not move this to the XML file, Qt before 4.7 will choke on it */ + ui->nameFilter->setPlaceholderText(tr("Name filter")); + ui->valueFilter->setPlaceholderText(tr("Value filter")); + ui->addressFilter->setPlaceholderText(tr("Address filter")); +#endif + + ui->nameFilter->setFixedWidth(COLUMN_WIDTH_NAME); + ui->addressFilter->setFixedWidth(COLUMN_WIDTH_ADDRESS); + ui->horizontalSpacer_ExpiresIn->changeSize( + COLUMN_WIDTH_EXPIRES_IN + ui->tableView->verticalScrollBar()->sizeHint().width() + +#ifdef Q_OS_MAC + // Not sure if this is needed, but other Mac code adds 2 pixels to scroll bar width; + // see transactionview.cpp, search for verticalScrollBar()->sizeHint() + + 2 +#endif + + , + ui->horizontalSpacer_ExpiresIn->sizeHint().height(), + QSizePolicy::Fixed); + +} + +ManageNamesPage::~ManageNamesPage() +{ + delete ui; +} + +class SortedWidgetItem : public QTableWidgetItem +{ +public: + bool operator <( const QTableWidgetItem& other ) const + { + return (data(Qt::UserRole) < other.data(Qt::UserRole)); + } +}; + + +void ManageNamesPage::updateNames() +{ + ui->countLabel->setText("Waiting for sync..."); + if (IsInitialBlockDownload()) return; + int nameCount; + + ui->tableWidget->setSortingEnabled(false); + ui->tableWidget->setUpdatesEnabled(false); + ui->tableWidget->clearContents(); + ui->tableWidget->setRowCount(0); + + QString nname; + QString nvalue; + int nexpire = 0; + + vector vchNameUniq; + CNameDB dbName("r"); + int nMax = 500; + NameTxInfo nti; + vector, pair > > nameScan; + + if (!dbName.ScanNames(vchNameUniq, nMax, nameScan)) + throw JSONRPCError(RPC_WALLET_ERROR, "scan failed"); + + pair, pair > pairScan; + + ui->countLabel->setText("Loaded Name DB..."); + BOOST_FOREACH(pairScan, nameScan) + { + + bool bFound = false; + int nodeRow = 0; + for(int i=0; i < ui->tableWidget->rowCount(); i++) + { + if(ui->tableWidget->item(i, 0)->text() == QString::fromStdString(stringFromVch(pairScan.first))) + { + bFound = true; + nodeRow = i; + break; + } + } + + if(nodeRow == 0 && !bFound) + { + ui->tableWidget->insertRow(ui->tableWidget->rowCount()); + nodeRow = ui->tableWidget->rowCount()-1; + } + + CNameIndex txName = pairScan.second.first; + int nExpiresAt = pairScan.second.second; + // bool someMine = false; + // string addy = "denarius"; + + vector vchValue = txName.vchValue; + string value = stringFromVch(vchValue); + + nname = QString::fromStdString(stringFromVch(pairScan.first)); + nvalue = QString::fromStdString(stringFromVch(vchValue)); + nexpire = nExpiresAt; + + QTableWidgetItem *nameItem = new QTableWidgetItem(); + nameItem->setData(Qt::DisplayRole, nname); + SortedWidgetItem *expireItem = new SortedWidgetItem(); + expireItem->setData(Qt::DisplayRole, QString("%2").arg(nexpire)); + QTableWidgetItem *valueItem = new QTableWidgetItem(); + valueItem->setData(Qt::DisplayRole, nvalue); + + ui->tableWidget->setItem(nodeRow, 0, nameItem); + ui->tableWidget->setItem(nodeRow, 1, expireItem); + ui->tableWidget->setItem(nodeRow, 2, valueItem); + // ui->countLabel->setText(QString("%1 names registered in total").arg(nameScan.size())); + } + ui->countLabel->setText(QString("%1 names registered in total").arg(nameScan.size())); + ui->tableWidget->setSortingEnabled(true); + ui->tableWidget->setUpdatesEnabled(true); +} + +void ManageNamesPage::setModel(WalletModel *walletModel) +{ + this->walletModel = walletModel; + model = walletModel->getNameTableModel(); + + proxyModel = new NameFilterProxyModel(this); + proxyModel->setSourceModel(model); + proxyModel->setDynamicSortFilter(true); + proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive); + proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + + ui->tableView->setModel(proxyModel); + ui->tableView->sortByColumn(0, Qt::AscendingOrder); + + ui->tableView->horizontalHeader()->setHighlightSections(false); + + // Set column widths + ui->tableView->horizontalHeader()->resizeSection( + NameTableModel::Name, COLUMN_WIDTH_NAME); + ui->tableView->horizontalHeader()->setResizeMode( + NameTableModel::Value, QHeaderView::Stretch); + ui->tableView->horizontalHeader()->resizeSection( + NameTableModel::Address, COLUMN_WIDTH_ADDRESS); + ui->tableView->horizontalHeader()->resizeSection( + NameTableModel::ExpiresIn, COLUMN_WIDTH_EXPIRES_IN); + + connect(ui->tableView->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), + this, SLOT(selectionChanged())); + + connect(ui->nameFilter, SIGNAL(textChanged(QString)), this, SLOT(changedNameFilter(QString))); + connect(ui->valueFilter, SIGNAL(textChanged(QString)), this, SLOT(changedValueFilter(QString))); + connect(ui->addressFilter, SIGNAL(textChanged(QString)), this, SLOT(changedAddressFilter(QString))); + + selectionChanged(); +} + +void ManageNamesPage::changedNameFilter(const QString &filter) +{ + if (!proxyModel) + return; + proxyModel->setNameSearch(filter); +} + +void ManageNamesPage::changedValueFilter(const QString &filter) +{ + if (!proxyModel) + return; + proxyModel->setValueSearch(filter); +} + +void ManageNamesPage::changedAddressFilter(const QString &filter) +{ + if (!proxyModel) + return; + proxyModel->setAddressSearch(filter); +} + +//TODO finish this +void ManageNamesPage::on_submitNameButton_clicked() +{ + if (!walletModel) + return; + + QString name = ui->registerName->text(); + vector vchValue; // byte-by-byte value, as is + QString displayValue; // for displaying value as unicode string + + if (ui->registerValue->isEnabled()) + { + displayValue = ui->registerValue->toPlainText(); + string strValue = displayValue.toStdString(); + vchValue.assign(strValue.begin(), strValue.end()); + } + else + { + vchValue = importedAsBinaryFile; + displayValue = QString::fromStdString(stringFromVch(vchValue)); + } + + int days = ui->registerDays->text().toInt(); + QString txType = ui->txTypeSelector->currentText(); + QString newAddress = ui->registerAddress->text(); + QString address = ui->registerAddress->text(); + if (txType == "NAME_UPDATE") + newAddress = ui->registerAddress->text(); + if (txType == "NAME_NEW") + address = ui->registerAddress->text(); + + if (name == "") + { + QMessageBox::critical(this, tr("Name is empty"), tr("Enter name please")); + return; + } + + if (vchValue.empty() && (txType == "NAME_NEW" || txType == "NAME_UPDATE")) + { + QMessageBox::critical(this, tr("Value is empty"), tr("Enter value please")); + return; + } + + // TODO: name needs more exhaustive syntax checking, Unicode characters etc. + // TODO: maybe it should be done while the user is typing (e.g. show/hide a red notice below the input box) + if (name != name.simplified() || name.contains(" ")) + { + if (QMessageBox::Yes != QMessageBox::warning(this, tr("Name registration warning"), + tr("The name you entered contains whitespace characters. Are you sure you want to use this name?"), + QMessageBox::Yes | QMessageBox::Cancel, + QMessageBox::Cancel)) + { + return; + } + } + + int64_t txFee = MIN_TX_FEE; + { + string strName = name.toStdString(); + vector vchName(strName.begin(), strName.end()); + + if (txType == "NAME_NEW") + txFee = GetNameOpFee(pindexBest, days, OP_NAME_NEW, vchName, vchValue); + else if (txType == "NAME_UPDATE") + txFee = GetNameOpFee(pindexBest, days, OP_NAME_UPDATE, vchName, vchValue); + } + + if (QMessageBox::Yes != QMessageBox::question(this, tr("Confirm name registration"), + tr("This will issue a %1. TX Fee is AT LEAST %2 D.").arg(txType).arg(txFee / (float)COIN, 0, 'f', 2), + QMessageBox::Yes | QMessageBox::Cancel, + QMessageBox::Cancel)) + { + return; + } + + WalletModel::UnlockContext ctx(walletModel->requestUnlock()); + if (!ctx.isValid()) + return; + + QString err_msg; + + try + { + NameTxReturn res; + int nHeight; + ChangeType status; + if (txType == "NAME_NEW") + { + nHeight = NameTableEntry::NAME_NEW; + status = CT_NEW; + res = walletModel->nameNew(name, vchValue, days, address); + } + else if (txType == "NAME_UPDATE") + { + nHeight = NameTableEntry::NAME_UPDATE; + status = CT_UPDATED; + res = walletModel->nameUpdate(name, vchValue, days, newAddress); + } + else if (txType == "NAME_DELETE") + { + nHeight = NameTableEntry::NAME_DELETE; + status = CT_UPDATED; //we still want to display this name until it is deleted + res = walletModel->nameDelete(name); + } + + importedAsBinaryFile.clear(); + importedAsTextFile.clear(); + + if (res.ok) + { + ui->registerName->setText(""); + ui->registerValue->setEnabled(true); + ui->registerValue->setPlainText(""); + ui->submitNameButton->setDefault(true); // EvgenijM86: is this realy needed here? + + int newRowIndex; + // FIXME: CT_NEW may have been sent from nameNew (via transaction). + // Currently updateEntry is modified so it does not complain + model->updateEntry(name, displayValue, QString::fromStdString(res.address), nHeight, status, &newRowIndex); + ui->tableView->selectRow(newRowIndex); + ui->tableView->setFocus(); + return; + } + + err_msg = QString::fromStdString(res.err_msg); + } + catch (std::exception& e) + { + err_msg = e.what(); + } + + if (err_msg == "ABORTED") + return; + + QMessageBox::warning(this, tr("Name registration failed"), err_msg); +} + +bool ManageNamesPage::eventFilter(QObject *object, QEvent *event) +{ + if (event->type() == QEvent::FocusIn) + { + if (object == ui->registerName || object == ui->submitNameButton) + { + ui->submitNameButton->setDefault(true); + } + else if (object == ui->tableView) + { + ui->submitNameButton->setDefault(false); + } + } + return QDialog::eventFilter(object, event); +} + +void ManageNamesPage::selectionChanged() +{ + // Set button states based on selected tab and selection +// QTableView *table = ui->tableView; +// if(!table->selectionModel()) +// return; +} + +void ManageNamesPage::contextualMenu(const QPoint &point) +{ + QModelIndex index = ui->tableView->indexAt(point); + if (index.isValid()) + contextMenu->exec(QCursor::pos()); +} + +void ManageNamesPage::onCopyNameAction() +{ + GUIUtil::copyEntryData(ui->tableView, NameTableModel::Name); +} + +void ManageNamesPage::onCopyValueAction() +{ + GUIUtil::copyEntryData(ui->tableView, NameTableModel::Value); +} + +void ManageNamesPage::onCopyAddressAction() +{ + GUIUtil::copyEntryData(ui->tableView, NameTableModel::Address); +} + +void ManageNamesPage::onCopyAllAction() +{ + if(!ui->tableView || !ui->tableView->selectionModel()) + return; + + QModelIndexList selection; + + selection = ui->tableView->selectionModel()->selectedRows(NameTableModel::Name); + if (!selection.isEmpty()) + ui->registerName->setText(selection.at(0).data(Qt::EditRole).toString()); + + selection = ui->tableView->selectionModel()->selectedRows(NameTableModel::Value); + if (!selection.isEmpty()) + ui->registerValue->setPlainText(selection.at(0).data(Qt::EditRole).toString()); + + selection = ui->tableView->selectionModel()->selectedRows(NameTableModel::Address); + if (!selection.isEmpty()) + ui->registerAddress->setText(selection.at(0).data(Qt::EditRole).toString()); +} + +void ManageNamesPage::onSaveValueAsBinaryAction() +{ + if(!ui->tableView || !ui->tableView->selectionModel()) + return; + +// get name and value + QModelIndexList selection; + selection = ui->tableView->selectionModel()->selectedRows(NameTableModel::Name); + if (selection.isEmpty()) + return; + + vector vchName; + { + QString tmpName1 = selection.at(0).data(Qt::EditRole).toString(); + string tmpName2 = tmpName1.toStdString(); + vchName.assign(tmpName2.begin(), tmpName2.end()); + } + + vector vchValue; + GetNameValue(vchName, vchValue, true); + + +// select file and save value + QString fileName = QFileDialog::getSaveFileName(this, tr("Export File"), QDir::homePath(), tr("Files (*)")); + QFile file(fileName); + + if (!file.open(QIODevice::WriteOnly)) + return; + + QDataStream in(&file); + BOOST_FOREACH(const unsigned char& uch, vchValue) + in << uch; + file.close(); +} + +void ManageNamesPage::exportClicked() +{ + // CSV is currently the only supported format + QString filename = GUIUtil::getSaveFileName( + this, + tr("Export Registered Names Data"), QString(), + tr("Comma separated file (*.csv)")); + + if (filename.isNull()) + return; + + CSVModelWriter writer(filename); + writer.setModel(proxyModel); + // name, column, role + writer.addColumn("Name", NameTableModel::Name, Qt::EditRole); + writer.addColumn("Value", NameTableModel::Value, Qt::EditRole); + writer.addColumn("Address", NameTableModel::Address, Qt::EditRole); + writer.addColumn("Expires In", NameTableModel::ExpiresIn, Qt::EditRole); + + if(!writer.write()) + { + QMessageBox::critical(this, tr("Error exporting"), tr("Could not write to file %1.").arg(filename), + QMessageBox::Abort, QMessageBox::Abort); + } +} + +void ManageNamesPage::on_txTypeSelector_currentIndexChanged(const QString &txType) +{ + if (txType == "NAME_NEW") + { + ui->registerDays->setEnabled(true); + ui->registerAddress->setEnabled(true); + ui->registerValue->setEnabled(true); + } + else if (txType == "NAME_UPDATE") + { + ui->registerDays->setEnabled(true); + ui->registerAddress->setEnabled(true); + ui->registerValue->setEnabled(true); + } + else if (txType == "NAME_DELETE") + { + ui->registerDays->setDisabled(true); + ui->registerAddress->setDisabled(true); + ui->registerValue->setDisabled(true); + } + return; +} + +void ManageNamesPage::on_cbMyNames_stateChanged(int arg1) +{ + model->fMyNames = true; + model->update(true); +} + +void ManageNamesPage::on_cbOtherNames_stateChanged(int arg1) +{ + model->fOtherNames = true; + model->update(true); +} + +void ManageNamesPage::on_cbExpired_stateChanged(int arg1) +{ + model->fExpired = true; + model->update(true); +} + +void ManageNamesPage::on_importValueButton_clicked() +{ + if (ui->registerValue->isEnabled() == false) + { + ui->registerValue->setEnabled(true); + ui->registerValue->setPlainText(importedAsTextFile); + return; + } + + + QString fileName = QFileDialog::getOpenFileName(this, tr("Import File"), QDir::homePath(), tr("Files (*)")); + + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) return; + QByteArray blob = file.readAll(); + file.close(); + + if (blob.size() > MAX_VALUE_LENGTH) + { + QMessageBox::critical(this, tr("Value too large!"), tr("Value is larger than maximum size: %1 bytes > %2 bytes").arg(importedAsBinaryFile.size()).arg(MAX_VALUE_LENGTH)); + return; + } + + // save textual and binary representation + importedAsBinaryFile.clear(); + importedAsBinaryFile.reserve(blob.size()); + for (int i = 0; i < blob.size(); ++i) + importedAsBinaryFile.push_back(blob.at(i)); + importedAsTextFile = QString::fromStdString(stringFromVch(importedAsBinaryFile)); + + ui->registerValue->setDisabled(true); + ui->registerValue->setPlainText(tr( + "Currently file %1 is imported as binary (byte by byte) into name value. " + "If you wish to import file as unicode string then click on Import buttton again. " + "If you import file as unicode string its data may weight more than original file did." + ).arg(fileName)); +} + +void ManageNamesPage::on_registerValue_textChanged() +{ + float byteSize; + if (ui->registerValue->isEnabled()) + { + string strValue = ui->registerValue->toPlainText().toStdString(); + vector vchValue(strValue.begin(), strValue.end()); + byteSize = vchValue.size(); + } + else + byteSize = importedAsBinaryFile.size(); + + ui->labelValue->setText(tr("value(%1%)").arg(int(100 * byteSize / MAX_VALUE_LENGTH))); +} \ No newline at end of file diff --git a/src/qt/managenamespage.h b/src/qt/managenamespage.h new file mode 100644 index 00000000..0aeea87e --- /dev/null +++ b/src/qt/managenamespage.h @@ -0,0 +1,93 @@ +#ifndef MANAGENAMESPAGE_H +#define MANAGENAMESPAGE_H + +#include +#include +#include +#include + +namespace Ui { + class ManageNamesPage; +} +class WalletModel; +class NameTableModel; + +QT_BEGIN_NAMESPACE +class QTableView; +class QItemSelection; +class QMenu; +class QModelIndex; +QT_END_NAMESPACE + +class NameFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + explicit NameFilterProxyModel(QObject *parent = 0); + + void setNameSearch(const QString &search); + void setValueSearch(const QString &search); + void setAddressSearch(const QString &search); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const; + bool lessThan(const QModelIndex &left, const QModelIndex &right) const; + +private: + QString nameSearch, valueSearch, addressSearch; +}; + +/** Page for managing names */ +class ManageNamesPage : public QDialog +{ + Q_OBJECT + +public: + explicit ManageNamesPage(QWidget *parent = 0); + ~ManageNamesPage(); + + void setModel(WalletModel *walletModel); + std::vector importedAsBinaryFile; + QString importedAsTextFile; + +private: + Ui::ManageNamesPage *ui; + NameTableModel *model; + WalletModel *walletModel; + NameFilterProxyModel *proxyModel; + QMenu *contextMenu; + QTimer *timer; + +public slots: + void updateNames(); + void exportClicked(); + + void changedNameFilter(const QString &filter); + void changedValueFilter(const QString &filter); + void changedAddressFilter(const QString &filter); + +private slots: + void on_submitNameButton_clicked(); + + bool eventFilter(QObject *object, QEvent *event); + void selectionChanged(); + + /** Spawn contextual menu (right mouse menu) for name table entry */ + void contextualMenu(const QPoint &point); + + void onCopyNameAction(); + void onCopyValueAction(); + void onCopyAddressAction(); + void onCopyAllAction(); + void onSaveValueAsBinaryAction(); + + void on_txTypeSelector_currentIndexChanged(const QString &txType); + void on_cbMyNames_stateChanged(int arg1); + void on_cbOtherNames_stateChanged(int arg1); + void on_cbExpired_stateChanged(int arg1); + void on_importValueButton_clicked(); + void on_registerValue_textChanged(); +}; + +#endif // MANAGENAMESPAGE_H \ No newline at end of file diff --git a/src/qt/nametablemodel.cpp b/src/qt/nametablemodel.cpp new file mode 100644 index 00000000..81a26a25 --- /dev/null +++ b/src/qt/nametablemodel.cpp @@ -0,0 +1,378 @@ +#include "nametablemodel.h" + +#include "guiutil.h" +#include "walletmodel.h" +#include "guiconstants.h" + +#include + +#include "../wallet.h" + +#include + +using namespace std; + +// ExpiresIn column is right-aligned as it contains numbers +static int column_alignments[] = { + Qt::AlignLeft|Qt::AlignVCenter, // Name + Qt::AlignLeft|Qt::AlignVCenter, // Value + Qt::AlignLeft|Qt::AlignVCenter, // Address + Qt::AlignRight|Qt::AlignVCenter // Expires in + }; + +struct NameTableEntryLessThan +{ + bool operator()(const NameTableEntry &a, const NameTableEntry &b) const + { + return a.name < b.name; + } + bool operator()(const NameTableEntry &a, const QString &b) const + { + return a.name < b; + } + bool operator()(const QString &a, const NameTableEntry &b) const + { + return a < b.name; + } +}; + +// Private implementation +class NameTablePriv +{ +public: + CWallet *wallet; + QList cachedNameTable; + NameTableModel *parent; + + NameTablePriv(CWallet *wallet, NameTableModel *parent): + wallet(wallet), parent(parent) {} + + void refreshNameTable(bool fMyNames, bool fOtherNames, bool fExpired) + { + parent->beginResetModel(); + cachedNameTable.clear(); + + vector vchNameUniq; + map, NameTxInfo> mapNames, mapPending; + GetNameList(vchNameUniq, mapNames, mapPending); + + // CNameDB dbName("r"); + + // int nMax = 500; + + // NameTxInfo nti; + + // vector, pair > > nameScan; + // if (!dbName.ScanNames(vchNameUniq, nMax, nameScan)) + // throw JSONRPCError(RPC_WALLET_ERROR, "scan failed"); + + // pair, pair > pairScan; + // BOOST_FOREACH(pairScan, nameScan) + // { + // CNameIndex txName = pairScan.second.first; + // int nExpiresAt = pairScan.second.second; + // bool someMine = false; + // string addy = "denarius"; + + // vector vchValue = txName.vchValue; + // string value = stringFromVch(vchValue); + + // NameTableEntryAll nte(stringFromVch(pairScan.first), stringFromVch(vchValue), addy, nExpiresAt, someMine); + // cachedNameTable.append(nte); + // } + + // add info about existing names + BOOST_FOREACH(const PAIRTYPE(vector, NameTxInfo)& item, mapNames) + { + // name is mine and user asked to hide my names + if (item.second.fIsMine && !fMyNames) + continue; + // name is _not_ mine and user asked to hide other names + if (!item.second.fIsMine && !fOtherNames) + continue; + + // name have expired and users asked to hide expired names + if (item.second.nExpiresAt - pindexBest->nHeight <= 0 && !fExpired) + continue; + + NameTableEntry nte(stringFromVch(item.second.vchName), stringFromVch(item.second.vchValue), item.second.strAddress, item.second.nExpiresAt, item.second.fIsMine); + cachedNameTable.append(nte); + + // add pending updates|deletes to existing names or name_new to expired names + if (mapPending.count(item.second.vchName)) + { + NameTxInfo nti = mapPending[item.second.vchName]; + + int nHeightStatus; + if (nti.op == OP_NAME_NEW) + nHeightStatus = NameTableEntry::NAME_NEW; + else if (nti.op == OP_NAME_UPDATE) + nHeightStatus = NameTableEntry::NAME_UPDATE; + else if (nti.op == OP_NAME_DELETE) + nHeightStatus = NameTableEntry::NAME_DELETE; + NameTableEntry nte(stringFromVch(nti.vchName), stringFromVch(nti.vchValue), nti.strAddress, nHeightStatus, item.second.fIsMine); + cachedNameTable.append(nte); + } + } + + // add pending new names that did not previously exist + BOOST_FOREACH(const PAIRTYPE(vector, NameTxInfo)& item, mapPending) + { + if (item.second.fIsMine && !fMyNames) // name is mine and user have asked to hide my names + continue; + if (!item.second.fIsMine && !fOtherNames) // name is not mine and user have asked to hide other names + continue; + + if (mapNames.count(item.second.vchName) == 0 && item.second.op == OP_NAME_NEW) + { + NameTableEntry nte(stringFromVch(item.second.vchName), stringFromVch(item.second.vchValue), item.second.strAddress, NameTableEntry::NAME_NEW, item.second.fIsMine); + cachedNameTable.append(nte); + } + } + + // qLowerBound() and qUpperBound() require our cachedNameTable list to be sorted in asc order + qSort(cachedNameTable.begin(), cachedNameTable.end(), NameTableEntryLessThan()); + parent->endResetModel(); + } + + void updateEntry(const NameTableEntry &nameObj, int status, int *outNewRowIndex = NULL) + { + updateEntry(nameObj.name, nameObj.value, nameObj.address, nameObj.nExpiresAt, status, outNewRowIndex); + } + + void updateEntry(const QString &name, const QString &value, const QString &address, int nExpiresAt, int status, int *outNewRowIndex = NULL) + { + // Find name in model + QList::iterator lower = qLowerBound( + cachedNameTable.begin(), cachedNameTable.end(), name, NameTableEntryLessThan()); + QList::iterator upper = qUpperBound( + cachedNameTable.begin(), cachedNameTable.end(), name, NameTableEntryLessThan()); + int lowerIndex = (lower - cachedNameTable.begin()); + int upperIndex = (upper - cachedNameTable.begin()); + bool inModel = (lower != upper); + + switch(status) + { + case CT_NEW: + if (inModel) + { + if (outNewRowIndex) + { + *outNewRowIndex = parent->index(lowerIndex, 0).row(); + // HACK: ManageNamesPage uses this to ensure updating and get selected row, + // so we do not write warning into the log in this case + } + else + OutputDebugStringF("Warning: NameTablePriv::updateEntry: Got CT_NEW, but entry is already in model\n"); + break; + } + parent->beginInsertRows(QModelIndex(), lowerIndex, lowerIndex); + cachedNameTable.insert(lowerIndex, NameTableEntry(name, value, address, nExpiresAt)); + parent->endInsertRows(); + if (outNewRowIndex) + *outNewRowIndex = parent->index(lowerIndex, 0).row(); + break; + case CT_UPDATED: + if (!inModel) + { + OutputDebugStringF("Warning: NameTablePriv::updateEntry: Got CT_UPDATED, but entry is not in model\n"); + break; + } + lower->name = name; + lower->value = value; + lower->address = address; + lower->nExpiresAt = nExpiresAt; + parent->emitDataChanged(lowerIndex); + break; + case CT_DELETED: + if (!inModel) + { + OutputDebugStringF("Warning: NameTablePriv::updateEntry: Got CT_DELETED, but entry is not in model\n"); + break; + } + parent->beginRemoveRows(QModelIndex(), lowerIndex, upperIndex-1); + cachedNameTable.erase(lower, upper); + parent->endRemoveRows(); + break; + } + } + + int size() + { + return cachedNameTable.size(); + } + + NameTableEntry *index(int idx) + { + if (idx >= 0 && idx < cachedNameTable.size()) + { + return &cachedNameTable[idx]; + } + else + { + return NULL; + } + } +}; + + +NameTableModel::NameTableModel(CWallet *wallet, WalletModel *parent) : + QAbstractTableModel(parent), walletModel(parent), wallet(wallet), priv(0), cachedNumBlocks(0) +{ + columns << tr("Name") << tr("Value") << tr("Address") << tr("Expires in"); + priv = new NameTablePriv(wallet, this); + + fMyNames = true; + fOtherNames = true; + fExpired = false; + priv->refreshNameTable(fMyNames, fOtherNames, fExpired); + + QTimer *timer = new QTimer(this); + connect(timer, SIGNAL(timeout()), this, SLOT(update())); + timer->start(MODEL_UPDATE_DELAY); +} + +NameTableModel::~NameTableModel() +{ + delete priv; +} + +void NameTableModel::update(bool forced) +{ + // just do a complete table refresh, for simplicity sake + if (wallet->vCheckNewNames.size() > 0 || nBestHeight != cachedNumBlocks || forced) + { + priv->refreshNameTable(fMyNames, fOtherNames, fExpired); + wallet->vCheckNewNames.clear(); + cachedNumBlocks = nBestHeight; + } +} + +int NameTableModel::rowCount(const QModelIndex &parent /* = QModelIndex()*/) const +{ + Q_UNUSED(parent); + return priv->size(); +} + +int NameTableModel::columnCount(const QModelIndex &parent /* = QModelIndex()*/) const +{ + Q_UNUSED(parent); + return columns.length(); +} + +QVariant NameTableModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + NameTableEntry *rec = static_cast(index.internalPointer()); + + switch (role) + { + case Qt::DisplayRole: + case Qt::EditRole: + switch (index.column()) + { + case Name: + return rec->name; + case Value: + return rec->value; + case Address: + return rec->address; + case ExpiresIn: + if (!rec->HeightValid()) + { + if (rec->nExpiresAt == NameTableEntry::NAME_NEW) + return QString("pending (new)"); + if (rec->nExpiresAt == NameTableEntry::NAME_UPDATE) + return QString("pending (update)"); + if (rec->nExpiresAt == NameTableEntry::NAME_DELETE) + return QString("pending (delete)"); + } + else + { + float days = (rec->nExpiresAt - pindexBest->nHeight) / 2880.0; // 2880 - number of blocks per day on average + return days < 0 ? QString("%1 hours").arg(days * 24, 0, 'f', 1) : QString("%1 days").arg(days, 0, 'f', 1); + } + } + break; + case Qt::TextAlignmentRole: return column_alignments[index.column()]; + case Qt::FontRole: { + QFont font; + if (index.column() == Address) + font = GUIUtil::bitcoinAddressFont(); + return font; + } + case Qt::BackgroundRole: + if (index.column() == ExpiresIn && rec->nExpiresAt - pindexBest->nHeight <= 0) + return QVariant(QColor(Qt::yellow)); + else if (index.column() != ExpiresIn && !rec->fIsMine) + return QVariant(QColor(255,70,70)); + break; + } + + return QVariant(); +} + +QVariant NameTableModel::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]; + } + else if (role == Qt::ToolTipRole) + { + switch (section) + { + case Name: + return tr("Name registered using Denarius."); + case Value: + return tr("Data associated with the name."); + case Address: + return tr("Denarius address to which the name is registered."); + case ExpiresIn: + return tr("Number of blocks, after which the name will expire. name_update to renew it."); + } + } + } + return QVariant(); +} + +Qt::ItemFlags NameTableModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return 0; + //NameTableEntry *rec = static_cast(index.internalPointer()); + + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; +} + +QModelIndex NameTableModel::index(int row, int column, const QModelIndex &parent /* = QModelIndex()*/) const +{ + Q_UNUSED(parent); + NameTableEntry *data = priv->index(row); + if (data) + { + return createIndex(row, column, priv->index(row)); + } + else + { + return QModelIndex(); + } +} + +void NameTableModel::updateEntry(const QString &name, const QString &value, const QString &address, int nHeight, int status, int *outNewRowIndex /* = NULL*/) +{ + priv->updateEntry(name, value, address, nHeight, status, outNewRowIndex); +} + +void NameTableModel::emitDataChanged(int idx) +{ + emit dataChanged(index(idx, 0), index(idx, columns.length()-1)); +} \ No newline at end of file diff --git a/src/qt/nametablemodel.h b/src/qt/nametablemodel.h new file mode 100644 index 00000000..baa24021 --- /dev/null +++ b/src/qt/nametablemodel.h @@ -0,0 +1,108 @@ +#ifndef NAMETABLEMODEL_H +#define NAMETABLEMODEL_H + +#include +#include + +class NameTablePriv; +class CWallet; +class WalletModel; + +#include +#include "uint256.h" + +/** + Qt model for "Manage Names" page. + */ +class NameTableModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + explicit NameTableModel(CWallet *wallet, WalletModel *parent = 0); + ~NameTableModel(); + + bool fMyNames; + bool fOtherNames; + bool fExpired; + + enum ColumnIndex { + Name = 0, + Value = 1, + Address = 2, + ExpiresIn = 3 + }; + + /** @name Methods overridden from QAbstractTableModel + @{*/ + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + /*@}*/ + +private: + WalletModel *walletModel; + CWallet *wallet; + QStringList columns; + NameTablePriv *priv; + int cachedNumBlocks; + + /** Notify listeners that data changed. */ + void emitDataChanged(int index); + +public slots: + void updateEntry(const QString &name, const QString &value, const QString &address, int nHeight, int status, int *outNewRowIndex = NULL); + void update(bool forced = false); + + friend class NameTablePriv; +}; + +struct NameTableEntry +{ + QString name; + QString value; + QString address; + int nExpiresAt; + bool fIsMine; + + // for pending (not yet in a block) name operations + static const int NAME_NEW = -1; + static const int NAME_UPDATE = -2; + static const int NAME_DELETE = -3; + static const int NAME_NON_EXISTING = -4; //no pending operation, just a blank + + bool HeightValid() { return nExpiresAt >= 0; } + + NameTableEntry() : nExpiresAt(NAME_NON_EXISTING), fIsMine(true) {} + NameTableEntry(const QString &name, const QString &value, const QString &address, int nExpiresAt, bool fIsMine = true) : + name(name), value(value), address(address), nExpiresAt(nExpiresAt), fIsMine(fIsMine) {} + NameTableEntry(const std::string &name, const std::string &value, const std::string &address, int nExpiresAt, bool fIsMine = true) : + name(QString::fromStdString(name)), value(QString::fromStdString(value)), address(QString::fromStdString(address)), nExpiresAt(nExpiresAt), fIsMine(fIsMine) {} +}; + +struct NameTableEntryAll +{ + QString name; + QString value; + int nExpiresAt; + + // for pending (not yet in a block) name operations + static const int NAME_NEW = -1; + static const int NAME_UPDATE = -2; + static const int NAME_DELETE = -3; + static const int NAME_NON_EXISTING = -4; //no pending operation, just a blank + + bool HeightValid() { return nExpiresAt >= 0; } + + NameTableEntryAll() : nExpiresAt(NAME_NON_EXISTING) {} + NameTableEntryAll(const QString &name, const QString &value, int nExpiresAt) : + name(name), value(value), nExpiresAt(nExpiresAt) {} + NameTableEntryAll(const std::string &name, const std::string &value, int nExpiresAt) : + name(QString::fromStdString(name)), value(QString::fromStdString(value)), nExpiresAt(nExpiresAt) {} +}; + + +#endif // NAMETABLEMODEL_H \ No newline at end of file diff --git a/src/qt/res/icons/names.png b/src/qt/res/icons/names.png new file mode 100644 index 00000000..55f0a48b Binary files /dev/null and b/src/qt/res/icons/names.png differ diff --git a/src/qt/transactiontablemodel.cpp b/src/qt/transactiontablemodel.cpp index bd608d52..3b33aebe 100644 --- a/src/qt/transactiontablemodel.cpp +++ b/src/qt/transactiontablemodel.cpp @@ -1,4 +1,5 @@ #include "transactiontablemodel.h" +#include "nametablemodel.h" #include "guiutil.h" #include "transactionrecord.h" #include "guiconstants.h" diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 469e2e91..dc075428 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -4,6 +4,7 @@ #include "addresstablemodel.h" #include "mintingtablemodel.h" #include "transactiontablemodel.h" +#include "nametablemodel.h" #include "ui_interface.h" #include "wallet.h" @@ -29,6 +30,7 @@ WalletModel::WalletModel(CWallet *wallet, OptionsModel *optionsModel, QObject *p addressTableModel = new AddressTableModel(wallet, this); mintingTableModel = new MintingTableModel(wallet, this); + nameTableModel = new NameTableModel(wallet, this); transactionTableModel = new TransactionTableModel(wallet, this); // This timer will be fired repeatedly to update the balance @@ -136,6 +138,9 @@ void WalletModel::pollBalanceChanged() //if(transactionTableModel) //transactionTableModel->updateConfirmations(); + //TODO: perhaps redo this. Currently it rescans all tx available in wallet - idealy we do not need such scan. + if (nameTableModel) + nameTableModel->update(); } fForceBalanceCheck = false; @@ -472,6 +477,27 @@ WalletModel::SendCoinsReturn WalletModel::sendCoins(const QList &vchValue, int nRentalDays, QString address) +{ + string strName = name.toStdString(); + vector vchName(strName.begin(), strName.end()); + return name_new(vchName, vchValue, nRentalDays, address.toStdString()); +} + +NameTxReturn WalletModel::nameUpdate(const QString &name, const vector &vchValue, int nRentalDays, QString newAddress) +{ + string strName = name.toStdString(); + vector vchName(strName.begin(), strName.end()); + return name_update(vchName, vchValue, nRentalDays, newAddress.toStdString()); +} + +NameTxReturn WalletModel::nameDelete(const QString &name) +{ + string strName = name.toStdString(); + vector vchName(strName.begin(), strName.end()); + return name_delete(vchName); +} + OptionsModel *WalletModel::getOptionsModel() { return optionsModel; @@ -482,6 +508,11 @@ AddressTableModel *WalletModel::getAddressTableModel() return addressTableModel; } +NameTableModel *WalletModel::getNameTableModel() +{ + return nameTableModel; +} + MintingTableModel *WalletModel::getMintingTableModel() { return mintingTableModel; diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index 7b7dbf8b..6cdc3a7b 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -7,11 +7,13 @@ #include "allocators.h" /* for SecureString */ #include "wallet.h" +#include "namecoin.h" class OptionsModel; class AddressTableModel; class TransactionTableModel; class MintingTableModel; +class NameTableModel; class CWallet; class CKeyID; class CPubKey; @@ -68,6 +70,7 @@ class WalletModel : public QObject AddressTableModel *getAddressTableModel(); TransactionTableModel *getTransactionTableModel(); MintingTableModel *getMintingTableModel(); + NameTableModel *getNameTableModel(); qint64 getBalance() const; qint64 getLockedBalance() const; @@ -103,6 +106,12 @@ class WalletModel : public QObject // Send coins to a list of recipients SendCoinsReturn sendCoins(const QList &recipients, const CCoinControl *coinControl=NULL); + // Register new name or update it + // Requires unlocked wallet; can throw exception instead of returning error + NameTxReturn nameNew(const QString &name, const std::vector &vchValue, int days, QString address = ""); + NameTxReturn nameUpdate(const QString &name, const std::vector &vchValue, int days, QString newAddress = ""); + NameTxReturn nameDelete(const QString &name); + // Wallet encryption bool setWalletEncrypted(bool encrypted, const SecureString &passphrase); // Passphrase only needed when unlocking @@ -152,7 +161,8 @@ class WalletModel : public QObject AddressTableModel *addressTableModel; TransactionTableModel *transactionTableModel; - MintingTableModel *mintingTableModel; + MintingTableModel *mintingTableModel; + NameTableModel *nameTableModel; //Watch Only bool fHaveWatchOnly;