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