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