1 /***************************************************************************
2                           splitdelegate.cpp
3                              -------------------
4     begin                : Wed Apr 6 2016
5     copyright            : (C) 2016 by Thomas Baumgart
6     email                : Thomas Baumgart <tbaumgart@kde.org>
7  ***************************************************************************/
8 
9 /***************************************************************************
10  *                                                                         *
11  *   This program is free software; you can redistribute it and/or modify  *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  ***************************************************************************/
17 
18 #include "splitdelegate.h"
19 
20 // ----------------------------------------------------------------------------
21 // QT Includes
22 
23 #include <QApplication>
24 #include <QScrollBar>
25 #include <QPainter>
26 #include <QDebug>
27 
28 // ----------------------------------------------------------------------------
29 // KDE Includes
30 
31 // ----------------------------------------------------------------------------
32 // Project Includes
33 
34 #include "ledgerview.h"
35 #include "models.h"
36 #include "accountsmodel.h"
37 #include "ledgermodel.h"
38 #include "splitmodel.h"
39 #include "newspliteditor.h"
40 #include "mymoneyaccount.h"
41 #include "modelenums.h"
42 
43 using namespace eLedgerModel;
44 
45 QColor SplitDelegate::m_erroneousColor = QColor(Qt::red);
46 QColor SplitDelegate::m_importedColor = QColor(Qt::yellow);
47 
48 class SplitDelegate::Private
49 {
50 public:
Private()51   Private()
52   : m_editor(0)
53   , m_editorRow(-1)
54   , m_showValuesInverted(false)
55   {}
56 
57   NewSplitEditor*               m_editor;
58   int                           m_editorRow;
59   bool                          m_showValuesInverted;
60 };
61 
62 
SplitDelegate(QObject * parent)63 SplitDelegate::SplitDelegate(QObject* parent)
64   : QStyledItemDelegate(parent)
65   , d(new Private)
66 {
67 }
68 
~SplitDelegate()69 SplitDelegate::~SplitDelegate()
70 {
71   delete d;
72 }
73 
setErroneousColor(const QColor & color)74 void SplitDelegate::setErroneousColor(const QColor& color)
75 {
76   m_erroneousColor = color;
77 }
78 
createEditor(QWidget * parent,const QStyleOptionViewItem & option,const QModelIndex & index) const79 QWidget* SplitDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
80 {
81   Q_UNUSED(option);
82 
83   if(index.isValid()) {
84     Q_ASSERT(parent);
85     LedgerView* view = qobject_cast<LedgerView*>(parent->parentWidget());
86     Q_ASSERT(view != 0);
87 
88     if(view->selectionModel()->selectedRows().count() > 1) {
89       qDebug() << "Editing multiple splits at once is not yet supported";
90 
91       /**
92        * @todo replace the following three lines with the creation of a special
93        * editor that can handle multiple splits at once or show a message to the user
94        * that this is not possible
95        */
96       d->m_editor = 0;
97       SplitDelegate* that = const_cast<SplitDelegate*>(this);
98       emit that->closeEditor(d->m_editor, NoHint);
99 
100     } else {
101       d->m_editor = new NewSplitEditor(parent, view->accountId());
102     }
103 
104     if(d->m_editor) {
105       d->m_editorRow = index.row();
106       connect(d->m_editor, SIGNAL(done()), this, SLOT(endEdit()));
107       emit sizeHintChanged(index);
108     }
109 
110   } else {
111     qFatal("SplitDelegate::createEditor(): we should never end up here");
112   }
113   return d->m_editor;
114 }
115 
editorRow() const116 int SplitDelegate::editorRow() const
117 {
118   return d->m_editorRow;
119 }
120 
paint(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const121 void SplitDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
122 {
123   QStyleOptionViewItem opt = option;
124   initStyleOption(&opt, index);
125 
126   // never change the background of the cell the mouse is hovering over
127   opt.state &= ~QStyle::State_MouseOver;
128 
129   // show the focus only on the detail column
130   opt.state &= ~QStyle::State_HasFocus;
131   if(index.column() == (int)Column::Detail) {
132     QAbstractItemView* view = qobject_cast< QAbstractItemView* >(parent());
133     if(view) {
134       if(view->currentIndex().row() == index.row()) {
135         opt.state |= QStyle::State_HasFocus;
136       }
137     }
138   }
139 
140   painter->save();
141 
142   // Background
143   auto bgOpt = opt;
144   // if selected, always show as active, so that the
145   // background does not change when the editor is shown
146   if (opt.state & QStyle::State_Selected) {
147     bgOpt.state |= QStyle::State_Active;
148   }
149   QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
150   style->drawPrimitive(QStyle::PE_PanelItemViewItem, &bgOpt, painter, opt.widget);
151 
152   // Do not paint text if the edit widget is shown
153   const LedgerView *view = qobject_cast<const LedgerView *>(opt.widget);
154   if (view && view->indexWidget(index)) {
155     painter->restore();
156     return;
157   }
158 
159   const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1;
160   const QRect textArea = QRect(opt.rect.x() + margin, opt.rect.y() + margin, opt.rect.width() - 2 * margin, opt.rect.height() - 2 * margin);
161 
162   QStringList lines;
163   if(index.column() == (int)Column::Detail) {
164     lines << index.model()->data(index, (int)Role::Account).toString();
165     lines << index.model()->data(index, (int)Role::SingleLineMemo).toString();
166     lines.removeAll(QString());
167   }
168 
169   // draw the text items
170   if(!opt.text.isEmpty() || !lines.isEmpty()) {
171 
172     QPalette::ColorGroup cg = (opt.state & QStyle::State_Enabled)
173                               ? QPalette::Normal : QPalette::Disabled;
174 
175     if (cg == QPalette::Normal && !(opt.state & QStyle::State_Active)) {
176       cg = QPalette::Inactive;
177     }
178     if (opt.state & QStyle::State_Selected) {
179       painter->setPen(opt.palette.color(cg, QPalette::HighlightedText));
180     } else {
181       painter->setPen(opt.palette.color(cg, QPalette::Text));
182     }
183     if (opt.state & QStyle::State_Editing) {
184       painter->setPen(opt.palette.color(cg, QPalette::Text));
185       painter->drawRect(textArea.adjusted(0, 0, -1, -1));
186     }
187 
188     // collect data for the various columns
189     if(index.column() == (int)Column::Detail) {
190       for(int i = 0; i < lines.count(); ++i) {
191         painter->drawText(textArea.adjusted(0, (opt.fontMetrics.lineSpacing() + 5) * i, 0, 0), opt.displayAlignment, lines[i]);
192       }
193 
194     } else {
195       painter->drawText(textArea, opt.displayAlignment, opt.text);
196     }
197   }
198 
199   // draw the focus rect
200   if(opt.state & QStyle::State_HasFocus) {
201     QStyleOptionFocusRect o;
202     o.QStyleOption::operator=(opt);
203     o.rect = style->proxy()->subElementRect(QStyle::SE_ItemViewItemFocusRect, &opt, opt.widget);
204     o.state |= QStyle::State_KeyboardFocusChange;
205     o.state |= QStyle::State_Item;
206 
207     QPalette::ColorGroup cg = (opt.state & QStyle::State_Enabled)
208                               ? QPalette::Normal : QPalette::Disabled;
209     o.backgroundColor = opt.palette.color(cg, (opt.state & QStyle::State_Selected)
210                                              ? QPalette::Highlight : QPalette::Window);
211     style->proxy()->drawPrimitive(QStyle::PE_FrameFocusRect, &o, painter, opt.widget);
212   }
213 
214 #if 0
215   if((index.column() == LedgerModel::DetailColumn)
216   && erroneous) {
217     QPixmap attention;
218     attention.loadFromData(attentionSign, sizeof(attentionSign), 0, 0);
219     style->proxy()->drawItemPixmap(painter, option.rect, Qt::AlignRight | Qt::AlignTop, attention);
220   }
221 #endif
222 
223   painter->restore();
224 #if 0
225   const QHeaderView* horizontalHeader = view->horizontalHeader();
226   const QHeaderView* verticalHeader = view->verticalHeader();
227   const QWidget* viewport = view->viewport();
228   const bool showGrid = view->showGrid() && !view->indexWidget(index);
229   const int gridSize = showGrid ? 1 : 0;
230   const int gridHint = style->styleHint(QStyle::SH_Table_GridLineColor, &option, view);
231   const QColor gridColor = static_cast<QRgb>(gridHint);
232   const QPen gridPen = QPen(gridColor, 0, view->gridStyle());
233   const bool rightToLeft = view->isRightToLeft();
234   const int viewportOffset = horizontalHeader->offset();
235 
236 
237   // QStyledItemDelegate::paint(painter, opt, index);
238 
239   if(!horizontalHeader->isSectionHidden(LedgerModel::DateColumn)) {
240     QDate postDate = index.data(LedgerModel::PostDateRole).toDate();
241     if(postDate.isValid()) {
242       int ofs = horizontalHeader->sectionViewportPosition(LedgerModel::DateColumn) + viewportOffset;
243       QRect oRect = opt.rect;
244       opt.displayAlignment = Qt::AlignLeft | Qt::AlignTop;
245       opt.rect.setLeft(opt.rect.left()+ofs);
246       opt.rect.setTop(opt.rect.top()+margin);
247       opt.rect.setWidth(horizontalHeader->sectionSize(LedgerModel::DateColumn));
248       opt.text = KGlobal::locale()->formatDate(postDate, QLocale::ShortFormat);
249       style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);
250       opt.rect = oRect;
251     }
252   }
253 
254   if(!horizontalHeader->isSectionHidden(LedgerModel::DetailColumn)) {
255     QString payee = index.data(LedgerModel::PayeeRole).toString();
256     QString counterAccount = index.data(LedgerModel::CounterAccountRole).toString();
257     QString txt = payee;
258     if(payee.length() > 0)
259       txt += '\n';
260     txt += counterAccount;
261     int ofs = horizontalHeader->sectionViewportPosition(LedgerModel::DetailColumn) + viewportOffset;
262     QRect oRect = opt.rect;
263     opt.displayAlignment = Qt::AlignLeft | Qt::AlignTop;
264     opt.rect.setLeft(opt.rect.left()+ofs);
265     opt.rect.setTop(opt.rect.top()+margin);
266     opt.rect.setWidth(horizontalHeader->sectionSize(LedgerModel::DetailColumn));
267     opt.text = txt;
268     style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);
269     opt.rect = oRect;
270 
271   }
272 #if 0
273   opt.features |= QStyleOptionViewItemV2::HasDisplay;
274   QString txt = QString("%1").arg(index.isValid() ? "true" : "false");
275   if(index.isValid())
276     txt += QString(" %1 - %2").arg(index.row()).arg(view->verticalHeader()->sectionViewportPosition(index.row()));
277   opt.text = displayText(txt, opt.locale);
278 
279   style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);
280 #endif
281 
282   // paint grid
283   if(showGrid) {
284     painter->save();
285     QPen old = painter->pen();
286     painter->setPen(gridPen);
287 
288     // qDebug() << "Paint grid for" << index.row() << "in" << opt.rect;
289     for(int i=0; i < horizontalHeader->count(); ++i) {
290       if(!horizontalHeader->isSectionHidden(i)) {
291         int ofs = horizontalHeader->sectionViewportPosition(i) + viewportOffset;
292         if(!rightToLeft) {
293           ofs += horizontalHeader->sectionSize(i) - gridSize;
294         }
295         if(ofs-viewportOffset < viewport->width()) {
296           // I have no idea, why I need to paint the grid for the selected row and the one below
297           // but it was the only way to get this working correctly. Otherwise the grid was missing
298           // while moving the mouse over the view from bottom to top.
299           painter->drawLine(opt.rect.x()+ofs, opt.rect.y(), opt.rect.x()+ofs, opt.rect.height());
300           painter->drawLine(opt.rect.x()+ofs, opt.rect.y()+verticalHeader->sectionSize(index.row()), opt.rect.x()+ofs, opt.rect.height());
301         }
302       }
303     }
304     painter->setPen(old);
305     painter->restore();
306   }
307 #endif
308 }
309 
sizeHint(const QStyleOptionViewItem & option,const QModelIndex & index) const310 QSize SplitDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const
311 {
312   bool fullDisplay = false;
313   LedgerView* view = qobject_cast<LedgerView*>(parent());
314   if(view) {
315     QModelIndex currentIndex = view->currentIndex();
316     if(currentIndex.isValid()) {
317       QString currentId = currentIndex.model()->data(currentIndex, (int)Role::TransactionSplitId).toString();
318       QString myId = index.model()->data(index, (int)Role::TransactionSplitId).toString();
319       fullDisplay = (currentId == myId);
320     }
321   }
322 
323   QSize size;
324   QStyleOptionViewItem opt = option;
325   if(index.isValid()) {
326     // check if we are showing the edit widget
327     const QAbstractItemView *viewWidget = qobject_cast<const QAbstractItemView *>(opt.widget);
328     if (viewWidget) {
329       QModelIndex editIndex = viewWidget->model()->index(index.row(), 0);
330       if(editIndex.isValid()) {
331         QWidget* editor = viewWidget->indexWidget(editIndex);
332         if(editor) {
333           size = editor->minimumSizeHint();
334           return size;
335         }
336       }
337     }
338   }
339 
340   int rows = 1;
341   if(fullDisplay) {
342     initStyleOption(&opt, index);
343     auto payee = index.data((int)Role::PayeeName).toString();
344     auto account = index.data((int)Role::Account).toString();
345     auto memo = index.data((int)Role::SingleLineMemo).toString();
346 
347     rows = (payee.length() > 0 ? 1 : 0) + (account.length() > 0 ? 1 : 0) + (memo.length() > 0 ? 1 : 0);
348     // make sure we show at least one row
349     if(!rows) {
350       rows = 1;
351     }
352   }
353 
354   // leave a 5 pixel margin for each row
355   size = QSize(100, (opt.fontMetrics.lineSpacing() + 5) * rows);
356   return size;
357 }
358 
updateEditorGeometry(QWidget * editor,const QStyleOptionViewItem & option,const QModelIndex & index) const359 void SplitDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const
360 {
361   Q_UNUSED(index);
362   QStyleOptionViewItem opt = option;
363   int ofs = 8;
364   const LedgerView* view = qobject_cast<const LedgerView*>(opt.widget);
365   if(view) {
366     if(view->verticalScrollBar()->isVisible()) {
367       ofs += view->verticalScrollBar()->width();
368     }
369   }
370 
371   QRect r(opt.rect);
372   r.setWidth(opt.widget->width() - ofs);
373   editor->setGeometry(r);
374   editor->update();
375 }
376 
endEdit()377 void SplitDelegate::endEdit()
378 {
379   if(d->m_editor) {
380     if(d->m_editor->accepted()) {
381       emit commitData(d->m_editor);
382     }
383     emit closeEditor(d->m_editor, NoHint);
384     d->m_editorRow = -1;
385   }
386 }
387 
setEditorData(QWidget * editWidget,const QModelIndex & index) const388 void SplitDelegate::setEditorData(QWidget* editWidget, const QModelIndex& index) const
389 {
390   const SplitModel* model = qobject_cast<const SplitModel*>(index.model());
391   NewSplitEditor* editor = qobject_cast<NewSplitEditor*>(editWidget);
392 
393   if(model && editor) {
394     editor->setShowValuesInverted(d->m_showValuesInverted);
395     editor->setMemo(model->data(index, (int)Role::Memo).toString());
396     editor->setAccountId(model->data(index, (int)Role::AccountId).toString());
397     editor->setAmount(model->data(index, (int)Role::SplitShares).value<MyMoneyMoney>());
398     editor->setCostCenterId(model->data(index, (int)Role::CostCenterId).toString());
399     editor->setNumber(model->data(index, (int)Role::Number).toString());
400   }
401 }
402 
setModelData(QWidget * editor,QAbstractItemModel * model,const QModelIndex & index) const403 void SplitDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
404 {
405   NewSplitEditor* splitEditor = qobject_cast< NewSplitEditor* >(editor);
406   if(splitEditor) {
407     model->setData(index, splitEditor->number(), (int)Role::Number);
408     model->setData(index, splitEditor->memo(), (int)Role::Memo);
409     model->setData(index, splitEditor->accountId(), (int)Role::AccountId);
410     model->setData(index, splitEditor->costCenterId(), (int)Role::CostCenterId);
411     model->setData(index, QVariant::fromValue<MyMoneyMoney>(splitEditor->amount()), (int)Role::SplitShares);
412     model->setData(index, QVariant::fromValue<MyMoneyMoney>(splitEditor->amount()), (int)Role::SplitValue);
413 
414     const QString transactionCommodity = model->data(index, (int)Role::TransactionCommodity).toString();
415     QModelIndex accIndex = Models::instance()->accountsModel()->accountById(splitEditor->accountId());
416     if(accIndex.isValid()) {
417       MyMoneyAccount acc = Models::instance()->accountsModel()->data(accIndex, (int)eAccountsModel::Role::Account).value<MyMoneyAccount>();
418       if(transactionCommodity != acc.currencyId()) {
419 #if 0
420         ///  @todo call KCurrencyConversionDialog and update the model data
421         MyMoneyMoney value;
422         model->setData(index, QVariant::fromValue<MyMoneyMoney>(value), SplitModel::SplitValueRole);
423 #endif
424       }
425     } else {
426       qWarning() << "Unable to get account index in SplitDelegate::setModelData";
427     }
428 
429     // the following forces to send a dataChanged signal
430     model->setData(index, QVariant(), (int)Role::EmitDataChanged);
431 
432     // in case this was a new split, we nned to create a new empty one
433     SplitModel* splitModel = qobject_cast<SplitModel*>(model);
434     if(splitModel) {
435       splitModel->addEmptySplitEntry();
436     }
437   }
438 }
439 
440 /**
441  * This eventfilter seems to do nothing but it prevents that selecting a
442  * different row with the mouse closes the editor
443  */
eventFilter(QObject * o,QEvent * event)444 bool SplitDelegate::eventFilter(QObject* o, QEvent* event)
445 {
446   return QAbstractItemDelegate::eventFilter(o, event);
447 }
448 
setShowValuesInverted(bool inverse)449 void SplitDelegate::setShowValuesInverted(bool inverse)
450 {
451   d->m_showValuesInverted = inverse;
452 }
453 
showValuesInverted()454 bool SplitDelegate::showValuesInverted()
455 {
456   return d->m_showValuesInverted;
457 }
458