Batch transaction notifications, add RPC console filtering, macOS autostart
Prevent UI freezes during sync by batching wallet transaction notifications with a 250ms debounce timer and full-refresh fallback for large batches. Disable dynamic sorting and view updates on overview/transaction pages while syncing. Add request/reply/error filter checkboxes to the RPC console with in-memory message store. Implement macOS LaunchAgents-based autostart. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1085,6 +1085,63 @@ QPushButton:!enabled {
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="filterLayout">
|
||||
<property name="spacing">
|
||||
<number>12</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="showLabel">
|
||||
<property name="text">
|
||||
<string>Show:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="showRequestsCheckBox">
|
||||
<property name="text">
|
||||
<string>Requests</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="showRepliesCheckBox">
|
||||
<property name="text">
|
||||
<string>Replies</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="showErrorsCheckBox">
|
||||
<property name="text">
|
||||
<string>Errors</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacerFilters">
|
||||
<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>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
|
||||
@@ -405,6 +405,96 @@ bool SetStartOnSystemStartup(bool fAutoStart)
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#elif defined(Q_OS_MAC) || defined(MAC_OSX) || defined(__APPLE__)
|
||||
|
||||
boost::filesystem::path static GetLaunchAgentsDir()
|
||||
{
|
||||
const QString homeDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
|
||||
if (homeDir.isEmpty())
|
||||
return boost::filesystem::path();
|
||||
return boost::filesystem::path(homeDir.toStdString()) / "Library" / "LaunchAgents";
|
||||
}
|
||||
|
||||
boost::filesystem::path static GetAutostartFilePath()
|
||||
{
|
||||
return GetLaunchAgentsDir() / "org.triangles.triangles-qt.plist";
|
||||
}
|
||||
|
||||
static std::string PlistEscape(const std::string& value)
|
||||
{
|
||||
std::string escaped;
|
||||
escaped.reserve(value.size());
|
||||
for (std::string::const_iterator it = value.begin(); it != value.end(); ++it)
|
||||
{
|
||||
switch (*it)
|
||||
{
|
||||
case '&': escaped += "&"; break;
|
||||
case '<': escaped += "<"; break;
|
||||
case '>': escaped += ">"; break;
|
||||
case '"': escaped += """; break;
|
||||
case '\'': escaped += "'"; break;
|
||||
default: escaped += *it; break;
|
||||
}
|
||||
}
|
||||
return escaped;
|
||||
}
|
||||
|
||||
bool GetStartOnSystemStartup()
|
||||
{
|
||||
boost::filesystem::ifstream optionFile(GetAutostartFilePath());
|
||||
if (!optionFile.good())
|
||||
return false;
|
||||
|
||||
std::string contents;
|
||||
std::string line;
|
||||
while (getline(optionFile, line))
|
||||
contents += line;
|
||||
optionFile.close();
|
||||
|
||||
return contents.find("<key>RunAtLoad</key>") != std::string::npos &&
|
||||
contents.find("<true/>") != std::string::npos &&
|
||||
contents.find("<string>-min</string>") != std::string::npos;
|
||||
}
|
||||
|
||||
bool SetStartOnSystemStartup(bool fAutoStart)
|
||||
{
|
||||
if (!fAutoStart)
|
||||
return !boost::filesystem::exists(GetAutostartFilePath()) || boost::filesystem::remove(GetAutostartFilePath());
|
||||
|
||||
const QString exePath = QApplication::applicationFilePath();
|
||||
if (exePath.isEmpty())
|
||||
return false;
|
||||
|
||||
const QString workingDir = QFileInfo(exePath).absolutePath();
|
||||
boost::filesystem::create_directories(GetLaunchAgentsDir());
|
||||
|
||||
boost::filesystem::ofstream optionFile(GetAutostartFilePath(), std::ios_base::out|std::ios_base::trunc);
|
||||
if (!optionFile.good())
|
||||
return false;
|
||||
|
||||
optionFile
|
||||
<< "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
<< "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" "
|
||||
<< "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
|
||||
<< "<plist version=\"1.0\">\n"
|
||||
<< "<dict>\n"
|
||||
<< " <key>Label</key>\n"
|
||||
<< " <string>org.triangles.triangles-qt</string>\n"
|
||||
<< " <key>ProgramArguments</key>\n"
|
||||
<< " <array>\n"
|
||||
<< " <string>" << PlistEscape(exePath.toStdString()) << "</string>\n"
|
||||
<< " <string>-min</string>\n"
|
||||
<< " </array>\n"
|
||||
<< " <key>RunAtLoad</key>\n"
|
||||
<< " <true/>\n"
|
||||
<< " <key>WorkingDirectory</key>\n"
|
||||
<< " <string>" << PlistEscape(workingDir.toStdString()) << "</string>\n"
|
||||
<< "</dict>\n"
|
||||
<< "</plist>\n";
|
||||
optionFile.close();
|
||||
|
||||
return optionFile.good();
|
||||
}
|
||||
#else
|
||||
|
||||
// TODO: OSX startup stuff; see:
|
||||
|
||||
@@ -92,6 +92,7 @@ OverviewPage::OverviewPage(QWidget *parent) :
|
||||
currentStake(0),
|
||||
currentUnconfirmedBalance(-1),
|
||||
currentImmatureBalance(-1),
|
||||
walletTransactionSyncing(false),
|
||||
txdelegate(new TxViewDelegate()),
|
||||
filter(0)
|
||||
{
|
||||
@@ -166,8 +167,10 @@ void OverviewPage::setModel(WalletModel *model)
|
||||
// Keep up to date with wallet
|
||||
setBalance(model->getBalance(), model->getStake(), model->getUnconfirmedBalance(), model->getImmatureBalance());
|
||||
connect(model, SIGNAL(balanceChanged(qint64, qint64, qint64, qint64)), this, SLOT(setBalance(qint64, qint64, qint64, qint64)));
|
||||
connect(model, SIGNAL(transactionSyncStateChanged(bool)), this, SLOT(setTransactionSyncState(bool)));
|
||||
|
||||
connect(model->getOptionsModel(), SIGNAL(displayUnitChanged(int)), this, SLOT(updateDisplayUnit()));
|
||||
setTransactionSyncState(model->isTransactionSyncing());
|
||||
}
|
||||
|
||||
// update the display unit, to not use the default ("TRI")
|
||||
@@ -188,6 +191,23 @@ void OverviewPage::updateDisplayUnit()
|
||||
}
|
||||
}
|
||||
|
||||
void OverviewPage::setTransactionSyncState(bool syncing)
|
||||
{
|
||||
walletTransactionSyncing = syncing;
|
||||
if (!filter)
|
||||
return;
|
||||
|
||||
filter->setDynamicSortFilter(!syncing);
|
||||
ui->listTransactions->setUpdatesEnabled(!syncing);
|
||||
|
||||
if (!syncing)
|
||||
{
|
||||
filter->invalidate();
|
||||
filter->sort(TransactionTableModel::Status, Qt::DescendingOrder);
|
||||
ui->listTransactions->viewport()->update();
|
||||
}
|
||||
}
|
||||
|
||||
void OverviewPage::showOutOfSyncWarning(bool fShow)
|
||||
{
|
||||
ui->labelWalletStatus->setVisible(fShow);
|
||||
|
||||
@@ -30,6 +30,7 @@ public:
|
||||
|
||||
public slots:
|
||||
void setBalance(qint64 balance, qint64 stake, qint64 unconfirmedBalance, qint64 immatureBalance);
|
||||
void setTransactionSyncState(bool syncing);
|
||||
|
||||
signals:
|
||||
void transactionClicked(const QModelIndex &index);
|
||||
@@ -42,6 +43,7 @@ private:
|
||||
qint64 currentStake;
|
||||
qint64 currentUnconfirmedBalance;
|
||||
qint64 currentImmatureBalance;
|
||||
bool walletTransactionSyncing;
|
||||
|
||||
TxViewDelegate *txdelegate;
|
||||
TransactionFilterProxy *filter;
|
||||
|
||||
+93
-17
@@ -17,9 +17,6 @@
|
||||
|
||||
#include <openssl/crypto.h>
|
||||
|
||||
// TODO: make it possible to filter out categories (esp debug messages when implemented)
|
||||
// TODO: receive errors and debug messages through ClientModel
|
||||
|
||||
const int CONSOLE_SCROLLBACK = 50;
|
||||
const int CONSOLE_HISTORY = 50;
|
||||
|
||||
@@ -190,6 +187,7 @@ void RPCExecutor::request(const QString &command)
|
||||
RPCConsole::RPCConsole(QWidget *parent) :
|
||||
QDialog(parent),
|
||||
ui(new Ui::RPCConsole),
|
||||
clientModel(0),
|
||||
historyPtr(0)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
@@ -259,12 +257,16 @@ bool RPCConsole::eventFilter(QObject* obj, QEvent *event)
|
||||
|
||||
void RPCConsole::setClientModel(ClientModel *model)
|
||||
{
|
||||
if (clientModel)
|
||||
disconnect(clientModel, 0, this, 0);
|
||||
|
||||
this->clientModel = model;
|
||||
if(model)
|
||||
{
|
||||
// Subscribe to information, replies, messages, errors
|
||||
connect(model, SIGNAL(numConnectionsChanged(int)), this, SLOT(setNumConnections(int)));
|
||||
connect(model, SIGNAL(numBlocksChanged(int,int)), this, SLOT(setNumBlocks(int,int)));
|
||||
connect(model, SIGNAL(error(QString,QString,bool)), this, SLOT(showClientError(QString,QString,bool)));
|
||||
|
||||
// Provide initial values
|
||||
ui->clientVersion->setText(model->formatFullVersion());
|
||||
@@ -285,14 +287,16 @@ static QString categoryClass(int category)
|
||||
{
|
||||
case RPCConsole::CMD_REQUEST: return "cmd-request"; break;
|
||||
case RPCConsole::CMD_REPLY: return "cmd-reply"; break;
|
||||
case RPCConsole::CMD_ERROR: return "cmd-error"; break;
|
||||
case RPCConsole::CMD_ERROR:
|
||||
case RPCConsole::MC_ERROR: return "cmd-error"; break;
|
||||
default: return "misc";
|
||||
}
|
||||
}
|
||||
|
||||
void RPCConsole::clear()
|
||||
{
|
||||
ui->messagesWidget->clear();
|
||||
consoleEntries.clear();
|
||||
refreshMessages();
|
||||
ui->lineEdit->clear();
|
||||
ui->lineEdit->setFocus();
|
||||
|
||||
@@ -323,18 +327,21 @@ void RPCConsole::clear()
|
||||
|
||||
void RPCConsole::message(int category, const QString &message, bool html)
|
||||
{
|
||||
QTime time = QTime::currentTime();
|
||||
QString timeString = time.toString();
|
||||
QString out;
|
||||
out += "<table><tr><td class=\"time\" width=\"65\">" + timeString + "</td>";
|
||||
out += "<td class=\"icon\" width=\"32\"><img src=\"" + categoryClass(category) + "\"></td>";
|
||||
out += "<td class=\"message " + categoryClass(category) + "\" valign=\"middle\">";
|
||||
if(html)
|
||||
out += message;
|
||||
else
|
||||
out += GUIUtil::HtmlEscape(message, true);
|
||||
out += "</td></tr></table>";
|
||||
ui->messagesWidget->append(out);
|
||||
ConsoleEntry entry;
|
||||
entry.category = category;
|
||||
entry.text = message;
|
||||
entry.time = QTime::currentTime().toString();
|
||||
entry.html = html;
|
||||
|
||||
consoleEntries.append(entry);
|
||||
while (consoleEntries.size() > CONSOLE_SCROLLBACK)
|
||||
consoleEntries.removeFirst();
|
||||
|
||||
if (categoryVisible(category))
|
||||
{
|
||||
ui->messagesWidget->append(formatEntry(entry));
|
||||
scrollToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
void RPCConsole::setNumConnections(int count)
|
||||
@@ -439,3 +446,72 @@ void RPCConsole::on_showCLOptionsButton_clicked()
|
||||
GUIUtil::HelpMessageBox help;
|
||||
help.exec();
|
||||
}
|
||||
|
||||
bool RPCConsole::categoryVisible(int category) const
|
||||
{
|
||||
switch (category)
|
||||
{
|
||||
case CMD_REQUEST:
|
||||
return ui->showRequestsCheckBox->isChecked();
|
||||
case CMD_ERROR:
|
||||
case MC_ERROR:
|
||||
return ui->showErrorsCheckBox->isChecked();
|
||||
case CMD_REPLY:
|
||||
case MC_DEBUG:
|
||||
default:
|
||||
return ui->showRepliesCheckBox->isChecked();
|
||||
}
|
||||
}
|
||||
|
||||
QString RPCConsole::formatEntry(const ConsoleEntry &entry) const
|
||||
{
|
||||
QString out;
|
||||
out += "<table><tr><td class=\"time\" width=\"65\">" + entry.time + "</td>";
|
||||
out += "<td class=\"icon\" width=\"32\"><img src=\"" + categoryClass(entry.category) + "\"></td>";
|
||||
out += "<td class=\"message " + categoryClass(entry.category) + "\" valign=\"middle\">";
|
||||
if (entry.html)
|
||||
out += entry.text;
|
||||
else
|
||||
out += GUIUtil::HtmlEscape(entry.text, true);
|
||||
out += "</td></tr></table>";
|
||||
return out;
|
||||
}
|
||||
|
||||
void RPCConsole::refreshMessages()
|
||||
{
|
||||
ui->messagesWidget->clear();
|
||||
for (QList<ConsoleEntry>::const_iterator it = consoleEntries.begin(); it != consoleEntries.end(); ++it)
|
||||
{
|
||||
if (categoryVisible(it->category))
|
||||
ui->messagesWidget->append(formatEntry(*it));
|
||||
}
|
||||
scrollToEnd();
|
||||
}
|
||||
|
||||
void RPCConsole::on_showRequestsCheckBox_toggled(bool checked)
|
||||
{
|
||||
Q_UNUSED(checked);
|
||||
refreshMessages();
|
||||
}
|
||||
|
||||
void RPCConsole::on_showRepliesCheckBox_toggled(bool checked)
|
||||
{
|
||||
Q_UNUSED(checked);
|
||||
refreshMessages();
|
||||
}
|
||||
|
||||
void RPCConsole::on_showErrorsCheckBox_toggled(bool checked)
|
||||
{
|
||||
Q_UNUSED(checked);
|
||||
refreshMessages();
|
||||
}
|
||||
|
||||
void RPCConsole::showClientError(const QString &title, const QString &message, bool modal)
|
||||
{
|
||||
Q_UNUSED(modal);
|
||||
|
||||
if (title.isEmpty())
|
||||
this->message(MC_ERROR, message);
|
||||
else
|
||||
this->message(MC_ERROR, title + ": " + message);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@ private slots:
|
||||
void on_openDebugLogfileButton_clicked();
|
||||
/** display messagebox with program parameters (same as triangles-qt --help) */
|
||||
void on_showCLOptionsButton_clicked();
|
||||
void on_showRequestsCheckBox_toggled(bool checked);
|
||||
void on_showRepliesCheckBox_toggled(bool checked);
|
||||
void on_showErrorsCheckBox_toggled(bool checked);
|
||||
void showClientError(const QString &title, const QString &message, bool modal);
|
||||
|
||||
public slots:
|
||||
void clear();
|
||||
@@ -55,11 +59,23 @@ signals:
|
||||
void cmdRequest(const QString &command);
|
||||
|
||||
private:
|
||||
struct ConsoleEntry
|
||||
{
|
||||
int category;
|
||||
QString text;
|
||||
QString time;
|
||||
bool html;
|
||||
};
|
||||
|
||||
Ui::RPCConsole *ui;
|
||||
ClientModel *clientModel;
|
||||
QStringList history;
|
||||
QList<ConsoleEntry> consoleEntries;
|
||||
int historyPtr;
|
||||
|
||||
bool categoryVisible(int category) const;
|
||||
QString formatEntry(const ConsoleEntry &entry) const;
|
||||
void refreshMessages();
|
||||
void startExecutor();
|
||||
};
|
||||
|
||||
|
||||
@@ -34,7 +34,10 @@ TransactionView::TransactionView(QWidget *parent) :
|
||||
QWidget(parent), model(0), transactionProxyModel(0),
|
||||
ui(new Ui::TransactionsPage),
|
||||
transactionView(0),
|
||||
transactionsSortOrderDown(true)
|
||||
transactionsSortOrderDown(true),
|
||||
walletTransactionSyncing(false),
|
||||
transactionSortColumn(TransactionTableModel::Status),
|
||||
transactionSortOrder(Qt::DescendingOrder)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
setWindowFlags(Qt::CustomizeWindowHint | Qt::FramelessWindowHint | Qt::Window);
|
||||
@@ -150,7 +153,7 @@ void TransactionView::setModel(WalletModel *model)
|
||||
transactionView->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
transactionView->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
transactionView->setSortingEnabled(true);
|
||||
transactionView->sortByColumn(TransactionTableModel::Status, Qt::DescendingOrder);
|
||||
transactionView->sortByColumn(transactionSortColumn, transactionSortOrder);
|
||||
transactionView->verticalHeader()->hide();
|
||||
|
||||
transactionView->horizontalHeader()->resizeSection(TransactionTableModel::Status, 23);
|
||||
@@ -162,6 +165,9 @@ void TransactionView::setModel(WalletModel *model)
|
||||
transactionView->horizontalHeader()->setSectionResizeMode(TransactionTableModel::ToAddress, QHeaderView::Stretch);
|
||||
#endif
|
||||
transactionView->horizontalHeader()->resizeSection(TransactionTableModel::Amount, 100);
|
||||
|
||||
connect(model, SIGNAL(transactionSyncStateChanged(bool)), this, SLOT(setTransactionSyncState(bool)));
|
||||
setTransactionSyncState(model->isTransactionSyncing());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,6 +278,30 @@ void TransactionView::exportClicked()
|
||||
}
|
||||
}
|
||||
|
||||
void TransactionView::setTransactionSyncState(bool syncing)
|
||||
{
|
||||
walletTransactionSyncing = syncing;
|
||||
if (!transactionProxyModel || !transactionView)
|
||||
return;
|
||||
|
||||
if (syncing)
|
||||
{
|
||||
transactionSortColumn = transactionView->horizontalHeader()->sortIndicatorSection();
|
||||
transactionSortOrder = transactionView->horizontalHeader()->sortIndicatorOrder();
|
||||
transactionProxyModel->setDynamicSortFilter(false);
|
||||
transactionView->setSortingEnabled(false);
|
||||
transactionView->setUpdatesEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
transactionProxyModel->setDynamicSortFilter(true);
|
||||
transactionView->setUpdatesEnabled(true);
|
||||
transactionView->setSortingEnabled(true);
|
||||
transactionProxyModel->invalidate();
|
||||
transactionView->sortByColumn(transactionSortColumn, transactionSortOrder);
|
||||
transactionView->viewport()->update();
|
||||
}
|
||||
|
||||
void TransactionView::contextualMenu(const QPoint &point)
|
||||
{
|
||||
QModelIndex index = transactionView->indexAt(point);
|
||||
|
||||
@@ -48,9 +48,12 @@ public:
|
||||
private:
|
||||
WalletModel *model;
|
||||
TransactionFilterProxy *transactionProxyModel;
|
||||
Ui::TransactionsPage *ui;
|
||||
Ui::TransactionsPage *ui;
|
||||
QTableView *transactionView;
|
||||
bool transactionsSortOrderDown;
|
||||
bool walletTransactionSyncing;
|
||||
int transactionSortColumn;
|
||||
Qt::SortOrder transactionSortOrder;
|
||||
|
||||
QComboBox *dateWidget;
|
||||
QComboBox *typeWidget;
|
||||
@@ -74,6 +77,7 @@ private slots:
|
||||
void copyLabel();
|
||||
void copyAmount();
|
||||
void copyTxID();
|
||||
void setTransactionSyncState(bool syncing);
|
||||
|
||||
signals:
|
||||
void doubleClicked(const QModelIndex&);
|
||||
|
||||
+11
-1
@@ -106,7 +106,8 @@ TrianglesGUI::TrianglesGUI(bool fIsTestnet, QWidget *parent):
|
||||
trayIcon(0),
|
||||
notificator(0),
|
||||
rpcConsole(0),
|
||||
prevBlocks(0)
|
||||
prevBlocks(0),
|
||||
walletTransactionSyncing(false)
|
||||
{
|
||||
|
||||
ui->setupUi(this);
|
||||
@@ -592,6 +593,8 @@ void TrianglesGUI::setWalletModel(WalletModel *walletModel)
|
||||
|
||||
setEncryptionStatus(walletModel->getEncryptionStatus());
|
||||
connect(walletModel, SIGNAL(encryptionStatusChanged(int)), this, SLOT(setEncryptionStatus(int)));
|
||||
connect(walletModel, SIGNAL(transactionSyncStateChanged(bool)), this, SLOT(setWalletTransactionSyncState(bool)));
|
||||
setWalletTransactionSyncState(walletModel->isTransactionSyncing());
|
||||
|
||||
// Balloon pop-up for new transaction
|
||||
connect(walletModel->getTransactionTableModel(), SIGNAL(rowsInserted(QModelIndex,int,int)),
|
||||
@@ -1039,6 +1042,8 @@ void TrianglesGUI::incomingTransaction(const QModelIndex & parent, int start, in
|
||||
{
|
||||
if(!walletModel || !clientModel)
|
||||
return;
|
||||
if(walletTransactionSyncing)
|
||||
return;
|
||||
TransactionTableModel *ttm = walletModel->getTransactionTableModel();
|
||||
qint64 amount = ttm->index(start, TransactionTableModel::Amount, parent)
|
||||
.data(Qt::EditRole).toULongLong();
|
||||
@@ -1070,6 +1075,11 @@ void TrianglesGUI::incomingTransaction(const QModelIndex & parent, int start, in
|
||||
}
|
||||
}
|
||||
|
||||
void TrianglesGUI::setWalletTransactionSyncState(bool syncing)
|
||||
{
|
||||
walletTransactionSyncing = syncing;
|
||||
}
|
||||
|
||||
void TrianglesGUI::incomingMessage(const QModelIndex & parent, int start, int end)
|
||||
{
|
||||
if(!messageModel)
|
||||
|
||||
@@ -143,6 +143,7 @@ private:
|
||||
QMovie *syncIconMovie;
|
||||
/** Keep track of previous number of blocks, to detect progress */
|
||||
int prevBlocks;
|
||||
bool walletTransactionSyncing;
|
||||
|
||||
/** Create the main UI actions. */
|
||||
void createActions(bool fIsTestnet);
|
||||
@@ -169,6 +170,7 @@ public slots:
|
||||
@see WalletModel::EncryptionStatus
|
||||
*/
|
||||
void setEncryptionStatus(int status);
|
||||
void setWalletTransactionSyncState(bool syncing);
|
||||
|
||||
/** Notify the user of an error in the network or transaction handling code. */
|
||||
void error(const QString &title, const QString &message, bool modal);
|
||||
|
||||
+141
-5
@@ -8,9 +8,15 @@
|
||||
#include "wallet.h"
|
||||
#include "walletdb.h" // for BackupWallet
|
||||
#include "base58.h"
|
||||
#include "main.h"
|
||||
|
||||
#include <QSet>
|
||||
#include <QTimer>
|
||||
#include <QMutexLocker>
|
||||
|
||||
static const int MODEL_UPDATE_BATCH_THRESHOLD = 128;
|
||||
static const int MODEL_UPDATE_BATCH_DELAY_MS = 250;
|
||||
static const int MODEL_FULL_REFRESH_MIN_INTERVAL_MS = 1500;
|
||||
|
||||
WalletModel::WalletModel(CWallet *wallet, OptionsModel *optionsModel, QObject *parent) :
|
||||
QObject(parent), wallet(wallet), optionsModel(optionsModel), addressTableModel(0),
|
||||
@@ -18,7 +24,12 @@ WalletModel::WalletModel(CWallet *wallet, OptionsModel *optionsModel, QObject *p
|
||||
cachedBalance(0), cachedStake(0), cachedUnconfirmedBalance(0), cachedImmatureBalance(0),
|
||||
cachedNumTransactions(0),
|
||||
cachedEncryptionStatus(Unencrypted),
|
||||
cachedNumBlocks(0)
|
||||
cachedNumBlocks(0),
|
||||
transactionNotificationFlushQueued(false),
|
||||
fullTransactionRefreshQueued(false),
|
||||
transactionSyncing(false),
|
||||
transactionNotificationTimer(0),
|
||||
lastFullTransactionRefreshTime(0)
|
||||
{
|
||||
addressTableModel = new AddressTableModel(wallet, this);
|
||||
transactionTableModel = new TransactionTableModel(wallet, this);
|
||||
@@ -28,6 +39,10 @@ WalletModel::WalletModel(CWallet *wallet, OptionsModel *optionsModel, QObject *p
|
||||
connect(pollTimer, SIGNAL(timeout()), this, SLOT(pollBalanceChanged()));
|
||||
pollTimer->start(MODEL_UPDATE_DELAY);
|
||||
|
||||
transactionNotificationTimer = new QTimer(this);
|
||||
transactionNotificationTimer->setSingleShot(true);
|
||||
connect(transactionNotificationTimer, SIGNAL(timeout()), this, SLOT(flushTransactionNotifications()));
|
||||
|
||||
subscribeToCoreSignals();
|
||||
}
|
||||
|
||||
@@ -66,6 +81,11 @@ int WalletModel::getNumTransactions() const
|
||||
return numTransactions;
|
||||
}
|
||||
|
||||
bool WalletModel::isTransactionSyncing() const
|
||||
{
|
||||
return transactionSyncing;
|
||||
}
|
||||
|
||||
void WalletModel::updateStatus()
|
||||
{
|
||||
EncryptionStatus newEncryptionStatus = getEncryptionStatus();
|
||||
@@ -108,8 +128,126 @@ bool WalletModel::checkBalanceChanged()
|
||||
|
||||
void WalletModel::updateTransaction(const QString &hash, int status)
|
||||
{
|
||||
queueTransactionUpdate(hash, status);
|
||||
}
|
||||
|
||||
void WalletModel::queueTransactionUpdate(const QString &hash, int status)
|
||||
{
|
||||
bool shouldScheduleFlush = false;
|
||||
{
|
||||
QMutexLocker locker(&transactionNotificationMutex);
|
||||
|
||||
int mergedStatus = status;
|
||||
QMap<QString, int>::iterator it = queuedTransactionNotifications.find(hash);
|
||||
if (it != queuedTransactionNotifications.end())
|
||||
{
|
||||
// Preserve insert/delete semantics when multiple updates arrive
|
||||
// for the same transaction before the UI thread drains the queue.
|
||||
if (it.value() == CT_DELETED || status == CT_DELETED)
|
||||
mergedStatus = CT_DELETED;
|
||||
else if (it.value() == CT_NEW || status == CT_NEW)
|
||||
mergedStatus = CT_NEW;
|
||||
else
|
||||
mergedStatus = CT_UPDATED;
|
||||
it.value() = mergedStatus;
|
||||
}
|
||||
else
|
||||
{
|
||||
queuedTransactionNotifications.insert(hash, mergedStatus);
|
||||
}
|
||||
|
||||
if (IsInitialBlockDownload() || queuedTransactionNotifications.size() >= MODEL_UPDATE_BATCH_THRESHOLD)
|
||||
fullTransactionRefreshQueued = true;
|
||||
|
||||
if (!transactionNotificationFlushQueued)
|
||||
{
|
||||
transactionNotificationFlushQueued = true;
|
||||
shouldScheduleFlush = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldScheduleFlush)
|
||||
QMetaObject::invokeMethod(this, "startTransactionNotificationTimer", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void WalletModel::startTransactionNotificationTimer()
|
||||
{
|
||||
if (transactionNotificationTimer)
|
||||
transactionNotificationTimer->start(MODEL_UPDATE_BATCH_DELAY_MS);
|
||||
}
|
||||
|
||||
void WalletModel::flushTransactionNotifications()
|
||||
{
|
||||
QMap<QString, int> pendingNotifications;
|
||||
bool refreshAll = false;
|
||||
bool shouldRescheduleRefresh = false;
|
||||
{
|
||||
QMutexLocker locker(&transactionNotificationMutex);
|
||||
pendingNotifications.swap(queuedTransactionNotifications);
|
||||
refreshAll = fullTransactionRefreshQueued;
|
||||
fullTransactionRefreshQueued = false;
|
||||
transactionNotificationFlushQueued = false;
|
||||
}
|
||||
|
||||
const bool shouldSync = refreshAll ||
|
||||
IsInitialBlockDownload() ||
|
||||
pendingNotifications.size() >= MODEL_UPDATE_BATCH_THRESHOLD;
|
||||
if (transactionSyncing != shouldSync)
|
||||
{
|
||||
transactionSyncing = shouldSync;
|
||||
emit transactionSyncStateChanged(transactionSyncing);
|
||||
}
|
||||
|
||||
if(transactionTableModel)
|
||||
transactionTableModel->updateTransaction(hash, status);
|
||||
{
|
||||
if (refreshAll)
|
||||
{
|
||||
const qint64 now = GetTimeMillis();
|
||||
if (transactionSyncing &&
|
||||
lastFullTransactionRefreshTime != 0 &&
|
||||
now - lastFullTransactionRefreshTime < MODEL_FULL_REFRESH_MIN_INTERVAL_MS)
|
||||
{
|
||||
QMutexLocker locker(&transactionNotificationMutex);
|
||||
fullTransactionRefreshQueued = true;
|
||||
if (!transactionNotificationFlushQueued)
|
||||
{
|
||||
transactionNotificationFlushQueued = true;
|
||||
shouldRescheduleRefresh = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
transactionTableModel->refreshWallet();
|
||||
lastFullTransactionRefreshTime = now;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (QMap<QString, int>::const_iterator it = pendingNotifications.begin(); it != pendingNotifications.end(); ++it)
|
||||
transactionTableModel->updateTransaction(it.key(), it.value());
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRescheduleRefresh && transactionNotificationTimer)
|
||||
transactionNotificationTimer->start(MODEL_FULL_REFRESH_MIN_INTERVAL_MS);
|
||||
|
||||
bool stillPending = false;
|
||||
{
|
||||
QMutexLocker locker(&transactionNotificationMutex);
|
||||
stillPending = transactionNotificationFlushQueued || !queuedTransactionNotifications.isEmpty();
|
||||
}
|
||||
|
||||
if (!transactionSyncing && pendingNotifications.size() >= MODEL_UPDATE_BATCH_THRESHOLD)
|
||||
{
|
||||
transactionSyncing = true;
|
||||
emit transactionSyncStateChanged(true);
|
||||
}
|
||||
|
||||
if (transactionSyncing && !IsInitialBlockDownload() && !stillPending && pendingNotifications.size() < MODEL_UPDATE_BATCH_THRESHOLD)
|
||||
{
|
||||
transactionSyncing = false;
|
||||
emit transactionSyncStateChanged(false);
|
||||
}
|
||||
|
||||
// Don't call checkBalanceChanged() here - it does LOCK(cs_wallet) + iterates
|
||||
// all wallet transactions, blocking the UI thread. The pollBalanceChanged()
|
||||
@@ -392,9 +530,7 @@ static void NotifyAddressBookChanged(WalletModel *walletmodel, CWallet *wallet,
|
||||
static void NotifyTransactionChanged(WalletModel *walletmodel, CWallet *wallet, const uint256 &hash, ChangeType status)
|
||||
{
|
||||
OutputDebugStringF("NotifyTransactionChanged %s status=%i\n", hash.GetHex().c_str(), status);
|
||||
QMetaObject::invokeMethod(walletmodel, "updateTransaction", Qt::QueuedConnection,
|
||||
Q_ARG(QString, QString::fromStdString(hash.GetHex())),
|
||||
Q_ARG(int, status));
|
||||
walletmodel->queueTransactionUpdate(QString::fromStdString(hash.GetHex()), status);
|
||||
}
|
||||
|
||||
void WalletModel::subscribeToCoreSignals()
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include <QObject>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
|
||||
#include "allocators.h" /* for SecureString */
|
||||
|
||||
@@ -72,6 +74,7 @@ public:
|
||||
qint64 getImmatureBalance() const;
|
||||
int getNumTransactions() const;
|
||||
EncryptionStatus getEncryptionStatus() const;
|
||||
bool isTransactionSyncing() const;
|
||||
|
||||
// Check address for validity
|
||||
bool validateAddress(const QString &address);
|
||||
@@ -160,10 +163,16 @@ public slots:
|
||||
void updateStatus();
|
||||
/* New transaction, or transaction changed status */
|
||||
void updateTransaction(const QString &hash, int status);
|
||||
/* Queue a transaction update from a core thread without touching the UI directly */
|
||||
void queueTransactionUpdate(const QString &hash, int status);
|
||||
/* New, updated or removed address book entry */
|
||||
void updateAddressBook(const QString &address, const QString &label, bool isMine, int status);
|
||||
/* Current, immature or unconfirmed balance might have changed - emit 'balanceChanged' if so */
|
||||
void pollBalanceChanged();
|
||||
/* Start or restart the deferred transaction notification flush timer */
|
||||
void startTransactionNotificationTimer();
|
||||
/* Flush queued transaction notifications from core threads */
|
||||
void flushTransactionNotifications();
|
||||
|
||||
signals:
|
||||
// Signal that balance in wallet changed
|
||||
@@ -182,6 +191,16 @@ signals:
|
||||
|
||||
// Asynchronous error notification
|
||||
void error(const QString &title, const QString &message, bool modal);
|
||||
void transactionSyncStateChanged(bool syncing);
|
||||
|
||||
private:
|
||||
QMutex transactionNotificationMutex;
|
||||
QMap<QString, int> queuedTransactionNotifications;
|
||||
bool transactionNotificationFlushQueued;
|
||||
bool fullTransactionRefreshQueued;
|
||||
bool transactionSyncing;
|
||||
QTimer *transactionNotificationTimer;
|
||||
qint64 lastFullTransactionRefreshTime;
|
||||
};
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user