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