Somewhat confident now, tested on GNOME+KDE, with all types of transactions. Next...
[novacoin.git] / src / qt / transactiontablemodel.cpp
1 #include "transactiontablemodel.h"
2 #include "guiutil.h"
3 #include "transactionrecord.h"
4 #include "guiconstants.h"
5 #include "main.h"
6 #include "transactiondesc.h"
7
8 #include <QLocale>
9 #include <QDebug>
10 #include <QList>
11 #include <QColor>
12 #include <QTimer>
13 #include <QtAlgorithms>
14
15 const QString TransactionTableModel::Sent = "s";
16 const QString TransactionTableModel::Received = "r";
17 const QString TransactionTableModel::Other = "o";
18
19 /* Comparison operator for sort/binary search of model tx list */
20 struct TxLessThan
21 {
22     bool operator()(const TransactionRecord &a, const TransactionRecord &b) const
23     {
24         return a.hash < b.hash;
25     }
26     bool operator()(const TransactionRecord &a, const uint256 &b) const
27     {
28         return a.hash < b;
29     }
30     bool operator()(const uint256 &a, const TransactionRecord &b) const
31     {
32         return a < b.hash;
33     }
34 };
35
36 /* Private implementation */
37 struct TransactionTablePriv
38 {
39     TransactionTablePriv(TransactionTableModel *parent):
40             parent(parent)
41     {
42     }
43
44     TransactionTableModel *parent;
45
46     /* Local cache of wallet.
47      * As it is in the same order as the CWallet, by definition
48      * this is sorted by sha256.
49      */
50     QList<TransactionRecord> cachedWallet;
51
52     void refreshWallet()
53     {
54         qDebug() << "refreshWallet";
55
56         /* Query entire wallet from core.
57          */
58         cachedWallet.clear();
59         CRITICAL_BLOCK(cs_mapWallet)
60         {
61             for(std::map<uint256, CWalletTx>::iterator it = mapWallet.begin(); it != mapWallet.end(); ++it)
62             {
63                 cachedWallet.append(TransactionRecord::decomposeTransaction(it->second));
64             }
65         }
66     }
67
68     /* Update our model of the wallet incrementally.
69        Call with list of hashes of transactions that were added, removed or changed.
70      */
71     void updateWallet(const QList<uint256> &updated)
72     {
73         /* Walk through updated transactions, update model as needed.
74          */
75         qDebug() << "updateWallet";
76
77         /* Sort update list, and iterate through it in reverse, so that model updates
78            can be emitted from end to beginning (so that earlier updates will not influence
79            the indices of latter ones).
80          */
81         QList<uint256> updated_sorted = updated;
82         qSort(updated_sorted);
83
84         CRITICAL_BLOCK(cs_mapWallet)
85         {
86             for(int update_idx = updated_sorted.size()-1; update_idx >= 0; --update_idx)
87             {
88                 const uint256 &hash = updated_sorted.at(update_idx);
89                 /* Find transaction in wallet */
90                 std::map<uint256, CWalletTx>::iterator mi = mapWallet.find(hash);
91                 bool inWallet = mi != mapWallet.end();
92                 /* Find bounds of this transaction in model */
93                 QList<TransactionRecord>::iterator lower = qLowerBound(
94                     cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan());
95                 QList<TransactionRecord>::iterator upper = qUpperBound(
96                     cachedWallet.begin(), cachedWallet.end(), hash, TxLessThan());
97                 int lowerIndex = (lower - cachedWallet.begin());
98                 int upperIndex = (upper - cachedWallet.begin());
99
100                 bool inModel = false;
101                 if(lower != upper)
102                 {
103                     inModel = true;
104                 }
105
106                 qDebug() << "  " << QString::fromStdString(hash.ToString()) << inWallet << " " << inModel
107                         << lowerIndex << "-" << upperIndex;
108
109                 if(inWallet && !inModel)
110                 {
111                     /* Added */
112                     QList<TransactionRecord> toInsert =
113                             TransactionRecord::decomposeTransaction(mi->second);
114                     if(!toInsert.isEmpty()) /* only if something to insert */
115                     {
116                         parent->beginInsertRows(QModelIndex(), lowerIndex, lowerIndex+toInsert.size()-1);
117                         int insert_idx = lowerIndex;
118                         foreach(const TransactionRecord &rec, toInsert)
119                         {
120                             cachedWallet.insert(insert_idx, rec);
121                             insert_idx += 1;
122                         }
123                         parent->endInsertRows();
124                     }
125                 }
126                 else if(!inWallet && inModel)
127                 {
128                     /* Removed */
129                     parent->beginRemoveRows(QModelIndex(), lowerIndex, upperIndex-1);
130                     cachedWallet.erase(lower, upper);
131                     parent->endRemoveRows();
132                 }
133                 else if(inWallet && inModel)
134                 {
135                     /* Updated -- nothing to do, status update will take care of this */
136                 }
137             }
138         }
139     }
140
141     int size()
142     {
143         return cachedWallet.size();
144     }
145
146     TransactionRecord *index(int idx)
147     {
148         if(idx >= 0 && idx < cachedWallet.size())
149         {
150             TransactionRecord *rec = &cachedWallet[idx];
151
152             /* If a status update is needed (blocks came in since last check),
153                update the status of this transaction from the wallet. Otherwise,
154                simply re-use the cached status.
155              */
156             if(rec->statusUpdateNeeded())
157             {
158                 CRITICAL_BLOCK(cs_mapWallet)
159                 {
160                     std::map<uint256, CWalletTx>::iterator mi = mapWallet.find(rec->hash);
161
162                     if(mi != mapWallet.end())
163                     {
164                         rec->updateStatus(mi->second);
165                     }
166                 }
167             }
168             return rec;
169         }
170         else
171         {
172             return 0;
173         }
174     }
175
176     QString describe(TransactionRecord *rec)
177     {
178         CRITICAL_BLOCK(cs_mapWallet)
179         {
180             std::map<uint256, CWalletTx>::iterator mi = mapWallet.find(rec->hash);
181             if(mi != mapWallet.end())
182             {
183                 return QString::fromStdString(TransactionDesc::toHTML(mi->second));
184             }
185         }
186         return QString("");
187     }
188
189 };
190
191 /* Credit and Debit columns are right-aligned as they contain numbers */
192 static int column_alignments[] = {
193         Qt::AlignLeft|Qt::AlignVCenter,
194         Qt::AlignLeft|Qt::AlignVCenter,
195         Qt::AlignLeft|Qt::AlignVCenter,
196         Qt::AlignRight|Qt::AlignVCenter,
197         Qt::AlignRight|Qt::AlignVCenter,
198         Qt::AlignLeft|Qt::AlignVCenter
199     };
200
201 TransactionTableModel::TransactionTableModel(QObject *parent):
202         QAbstractTableModel(parent),
203         priv(new TransactionTablePriv(this))
204 {
205     columns << tr("Status") << tr("Date") << tr("Description") << tr("Debit") << tr("Credit");
206
207     priv->refreshWallet();
208
209     QTimer *timer = new QTimer(this);
210     connect(timer, SIGNAL(timeout()), this, SLOT(update()));
211     timer->start(MODEL_UPDATE_DELAY);
212 }
213
214 TransactionTableModel::~TransactionTableModel()
215 {
216     delete priv;
217 }
218
219 void TransactionTableModel::update()
220 {
221     QList<uint256> updated;
222
223     /* Check if there are changes to wallet map */
224     TRY_CRITICAL_BLOCK(cs_mapWallet)
225     {
226         if(!vWalletUpdated.empty())
227         {
228             BOOST_FOREACH(uint256 hash, vWalletUpdated)
229             {
230                 updated.append(hash);
231             }
232             vWalletUpdated.clear();
233         }
234     }
235
236     if(!updated.empty())
237     {
238         priv->updateWallet(updated);
239
240         /* Status (number of confirmations) and (possibly) description
241            columns changed for all rows.
242          */
243         emit dataChanged(index(0, Status), index(priv->size()-1, Status));
244         emit dataChanged(index(0, Description), index(priv->size()-1, Description));
245     }
246 }
247
248 int TransactionTableModel::rowCount(const QModelIndex &parent) const
249 {
250     Q_UNUSED(parent);
251     return priv->size();
252 }
253
254 int TransactionTableModel::columnCount(const QModelIndex &parent) const
255 {
256     Q_UNUSED(parent);
257     return columns.length();
258 }
259
260 QVariant TransactionTableModel::formatTxStatus(const TransactionRecord *wtx) const
261 {
262     QString status;
263
264     switch(wtx->status.status)
265     {
266     case TransactionStatus::OpenUntilBlock:
267         status = tr("Open for %n block(s)","",wtx->status.open_for);
268         break;
269     case TransactionStatus::OpenUntilDate:
270         status = tr("Open until ") + GUIUtil::DateTimeStr(wtx->status.open_for);
271         break;
272     case TransactionStatus::Offline:
273         status = tr("%1/offline").arg(wtx->status.depth);
274         break;
275     case TransactionStatus::Unconfirmed:
276         status = tr("%1/unconfirmed").arg(wtx->status.depth);
277         break;
278     case TransactionStatus::HaveConfirmations:
279         status = tr("%1 confirmations").arg(wtx->status.depth);
280         break;
281     }
282
283     return QVariant(status);
284 }
285
286 QVariant TransactionTableModel::formatTxDate(const TransactionRecord *wtx) const
287 {
288     if(wtx->time)
289     {
290         return QVariant(GUIUtil::DateTimeStr(wtx->time));
291     }
292     else
293     {
294         return QVariant();
295     }
296 }
297
298 /* Look up address in address book, if found return
299      address[0:12]... (label)
300    otherwise just return address
301  */
302 std::string lookupAddress(const std::string &address)
303 {
304     std::string description;
305     CRITICAL_BLOCK(cs_mapAddressBook)
306     {
307         std::map<std::string, std::string>::iterator mi = mapAddressBook.find(address);
308         if (mi != mapAddressBook.end() && !(*mi).second.empty())
309         {
310             std::string label = (*mi).second;
311             description += address.substr(0,12) + "... ";
312             description += "(" + label + ")";
313         }
314         else
315         {
316             description += address;
317         }
318     }
319     return description;
320 }
321
322 QVariant TransactionTableModel::formatTxDescription(const TransactionRecord *wtx) const
323 {
324     QString description;
325
326     switch(wtx->type)
327     {
328     case TransactionRecord::RecvWithAddress:
329         description = tr("Received with: ") + QString::fromStdString(lookupAddress(wtx->address));
330         break;
331     case TransactionRecord::RecvFromIP:
332         description = tr("Received from IP: ") + QString::fromStdString(wtx->address);
333         break;
334     case TransactionRecord::SendToAddress:
335         description = tr("Sent to: ") + QString::fromStdString(lookupAddress(wtx->address));
336         break;
337     case TransactionRecord::SendToIP:
338         description = tr("Sent to IP: ") + QString::fromStdString(wtx->address);
339         break;
340     case TransactionRecord::SendToSelf:
341         description = tr("Payment to yourself");
342         break;
343     case TransactionRecord::Generated:
344         switch(wtx->status.maturity)
345         {
346         case TransactionStatus::Immature:
347             description = tr("Generated (matures in %n more blocks)", "",
348                            wtx->status.matures_in);
349             break;
350         case TransactionStatus::Mature:
351             description = tr("Generated");
352             break;
353         case TransactionStatus::MaturesWarning:
354             description = tr("Generated - Warning: This block was not received by any other nodes and will probably not be accepted!");
355             break;
356         case TransactionStatus::NotAccepted:
357             description = tr("Generated (not accepted)");
358             break;
359         }
360         break;
361     }
362     return QVariant(description);
363 }
364
365 QVariant TransactionTableModel::formatTxDebit(const TransactionRecord *wtx) const
366 {
367     if(wtx->debit)
368     {
369         QString str = QString::fromStdString(FormatMoney(wtx->debit));
370         if(!wtx->status.confirmed || wtx->status.maturity != TransactionStatus::Mature)
371         {
372             str = QString("[") + str + QString("]");
373         }
374         return QVariant(str);
375     }
376     else
377     {
378         return QVariant();
379     }
380 }
381
382 QVariant TransactionTableModel::formatTxCredit(const TransactionRecord *wtx) const
383 {
384     if(wtx->credit)
385     {
386         QString str = QString::fromStdString(FormatMoney(wtx->credit));
387         if(!wtx->status.confirmed || wtx->status.maturity != TransactionStatus::Mature)
388         {
389             str = QString("[") + str + QString("]");
390         }
391         return QVariant(str);
392     }
393     else
394     {
395         return QVariant();
396     }
397 }
398
399 QVariant TransactionTableModel::data(const QModelIndex &index, int role) const
400 {
401     if(!index.isValid())
402         return QVariant();
403     TransactionRecord *rec = static_cast<TransactionRecord*>(index.internalPointer());
404
405     if(role == Qt::DisplayRole)
406     {
407         /* Delegate to specific column handlers */
408         switch(index.column())
409         {
410         case Status:
411             return formatTxStatus(rec);
412         case Date:
413             return formatTxDate(rec);
414         case Description:
415             return formatTxDescription(rec);
416         case Debit:
417             return formatTxDebit(rec);
418         case Credit:
419             return formatTxCredit(rec);
420         }
421     }
422     else if(role == Qt::EditRole)
423     {
424         /* Edit role is used for sorting so return the real values */
425         switch(index.column())
426         {
427         case Status:
428             return QString::fromStdString(rec->status.sortKey);
429         case Date:
430             return rec->time;
431         case Description:
432             return formatTxDescription(rec);
433         case Debit:
434             return rec->debit;
435         case Credit:
436             return rec->credit;
437         }
438     }
439     else if (role == Qt::TextAlignmentRole)
440     {
441         return column_alignments[index.column()];
442     }
443     else if (role == Qt::ForegroundRole)
444     {
445         /* Non-confirmed transactions are grey */
446         if(rec->status.confirmed)
447         {
448             return QColor(0, 0, 0);
449         }
450         else
451         {
452             return QColor(128, 128, 128);
453         }
454     }
455     else if (role == TypeRole)
456     {
457         /* Role for filtering tabs by type */
458         switch(rec->type)
459         {
460         case TransactionRecord::RecvWithAddress:
461         case TransactionRecord::RecvFromIP:
462             return TransactionTableModel::Received;
463         case TransactionRecord::SendToAddress:
464         case TransactionRecord::SendToIP:
465         case TransactionRecord::SendToSelf:
466             return TransactionTableModel::Sent;
467         default:
468             return TransactionTableModel::Other;
469         }
470     }
471     else if (role == LongDescriptionRole)
472     {
473         return priv->describe(rec);
474     }
475     return QVariant();
476 }
477
478 QVariant TransactionTableModel::headerData(int section, Qt::Orientation orientation, int role) const
479 {
480     if(orientation == Qt::Horizontal)
481     {
482         if(role == Qt::DisplayRole)
483         {
484             return columns[section];
485         }
486         else if (role == Qt::TextAlignmentRole)
487         {
488             return column_alignments[section];
489         }
490     }
491     return QVariant();
492 }
493
494 Qt::ItemFlags TransactionTableModel::flags(const QModelIndex &index) const
495 {
496     return QAbstractTableModel::flags(index);
497 }
498
499 QModelIndex TransactionTableModel::index(int row, int column, const QModelIndex &parent) const
500 {
501     Q_UNUSED(parent);
502     TransactionRecord *data = priv->index(row);
503     if(data)
504     {
505         return createIndex(row, column, priv->index(row));
506     }
507     else
508     {
509         return QModelIndex();
510     }
511 }
512