1 /*
2  * Copyright 2008-2018  Thomas Baumgart <tbaumgart@kde.org>
3  * Copyright 2017-2018  Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License as
7  * published by the Free Software Foundation; either version 2 of
8  * the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "kmymoneysplittable.h"
20 
21 // ----------------------------------------------------------------------------
22 // QT Includes
23 
24 #include <QCursor>
25 #include <QApplication>
26 #include <QTimer>
27 #include <QHBoxLayout>
28 #include <QKeyEvent>
29 #include <QFrame>
30 #include <QMouseEvent>
31 #include <QEvent>
32 #include <QPushButton>
33 #include <QMenu>
34 #include <QIcon>
35 #include <QHeaderView>
36 #include <QPointer>
37 #include <QList>
38 
39 // ----------------------------------------------------------------------------
40 // KDE Includes
41 
42 #include <KMessageBox>
43 #include <KCompletionBox>
44 #include <KSharedConfig>
45 #include <KLocalizedString>
46 
47 // ----------------------------------------------------------------------------
48 // Project Includes
49 
50 #include "mymoneysplit.h"
51 #include "mymoneytransaction.h"
52 #include "mymoneyaccount.h"
53 #include "mymoneyfile.h"
54 #include "mymoneyprice.h"
55 #include "amountedit.h"
56 #include "kmymoneycategory.h"
57 #include "kmymoneyaccountselector.h"
58 #include "kmymoneylineedit.h"
59 #include "mymoneysecurity.h"
60 #include "kmymoneysettings.h"
61 #include "kmymoneymvccombo.h"
62 #include "mymoneytag.h"
63 #include "kmymoneytagcombo.h"
64 #include "ktagcontainer.h"
65 #include "kcurrencycalculator.h"
66 #include "mymoneyutils.h"
67 #include "mymoneytracer.h"
68 #include "mymoneyexception.h"
69 #include "icons.h"
70 #include "mymoneyenums.h"
71 
72 using namespace Icons;
73 
74 class KMyMoneySplitTablePrivate
75 {
76   Q_DISABLE_COPY(KMyMoneySplitTablePrivate)
77 
78 public:
KMyMoneySplitTablePrivate()79   KMyMoneySplitTablePrivate() :
80     m_currentRow(0),
81     m_maxRows(0),
82     m_precision(2),
83     m_contextMenu(nullptr),
84     m_contextMenuDelete(nullptr),
85     m_contextMenuDuplicate(nullptr),
86     m_editCategory(0),
87     m_editTag(0),
88     m_editMemo(0),
89     m_editAmount(0)
90   {
91   }
92 
~KMyMoneySplitTablePrivate()93   ~KMyMoneySplitTablePrivate()
94   {
95   }
96 
97   /// the currently selected row (will be printed as selected)
98   int                 m_currentRow;
99 
100   /// the number of rows filled with data
101   int                 m_maxRows;
102 
103   MyMoneyTransaction  m_transaction;
104   MyMoneyAccount      m_account;
105   MyMoneySplit        m_split;
106   MyMoneySplit        m_hiddenSplit;
107 
108   /**
109     * This member keeps the precision for the values
110     */
111   int                 m_precision;
112 
113   /**
114     * This member keeps a pointer to the context menu
115     */
116   QMenu*         m_contextMenu;
117 
118   /// keeps the QAction of the delete entry in the context menu
119   QAction*       m_contextMenuDelete;
120 
121   /// keeps the QAction of the duplicate entry in the context menu
122   QAction*       m_contextMenuDuplicate;
123 
124   /**
125     * This member contains a pointer to the input widget for the category.
126     * The widget will be created and destroyed dynamically in createInputWidgets()
127     * and destroyInputWidgets().
128     */
129   QPointer<KMyMoneyCategory> m_editCategory;
130 
131   /**
132     * This member contains a pointer to the tag widget for the memo.
133     */
134   QPointer<KTagContainer> m_editTag;
135 
136   /**
137     * This member contains a pointer to the input widget for the memo.
138     * The widget will be created and destroyed dynamically in createInputWidgets()
139     * and destroyInputWidgets().
140     */
141   QPointer<KMyMoneyLineEdit> m_editMemo;
142 
143   /**
144     * This member contains a pointer to the input widget for the amount.
145     * The widget will be created and destroyed dynamically in createInputWidgets()
146     * and destroyInputWidgets().
147     */
148   QPointer<AmountEdit>     m_editAmount;
149 
150   /**
151     * This member keeps the tab order for the above widgets
152     */
153   QWidgetList         m_tabOrderWidgets;
154 
155   QPointer<QFrame>           m_registerButtonFrame;
156   QPointer<QPushButton>      m_registerEnterButton;
157   QPointer<QPushButton>      m_registerCancelButton;
158 
159   QMap<QString, MyMoneyMoney>  m_priceInfo;
160 };
161 
KMyMoneySplitTable(QWidget * parent)162 KMyMoneySplitTable::KMyMoneySplitTable(QWidget *parent) :
163     QTableWidget(parent),
164     d_ptr(new KMyMoneySplitTablePrivate)
165 {
166   Q_D(KMyMoneySplitTable);
167   // used for custom coloring with the help of the application's stylesheet
168   setObjectName(QLatin1String("splittable"));
169 
170   // setup the transactions table
171   setRowCount(1);
172   setColumnCount(4);
173   QStringList labels;
174   labels << i18n("Category") << i18n("Memo") << i18n("Tag") << i18n("Amount");
175   setHorizontalHeaderLabels(labels);
176   setSelectionMode(QAbstractItemView::SingleSelection);
177   setSelectionBehavior(QAbstractItemView::SelectRows);
178   int left, top, right, bottom;
179   getContentsMargins(&left, &top, &right, &bottom);
180   setContentsMargins(0, top, right, bottom);
181 
182   setFont(KMyMoneySettings::listCellFontEx());
183 
184   setAlternatingRowColors(true);
185 
186   verticalHeader()->hide();
187   horizontalHeader()->setSectionsMovable(false);
188   horizontalHeader()->setFont(KMyMoneySettings::listHeaderFontEx());
189 
190   KConfigGroup grp = KSharedConfig::openConfig()->group("SplitTable");
191   QByteArray columns;
192   columns = grp.readEntry("HeaderState", columns);
193   horizontalHeader()->restoreState(columns);
194   horizontalHeader()->setStretchLastSection(true);
195 
196   setShowGrid(KMyMoneySettings::showGrid());
197 
198   setEditTriggers(QAbstractItemView::NoEditTriggers);
199 
200   // setup the context menu
201   d->m_contextMenu = new QMenu(this);
202   d->m_contextMenu->setTitle(i18n("Split Options"));
203   d->m_contextMenu->setIcon(Icons::get(Icon::Transaction));
204   d->m_contextMenu->addAction(Icons::get(Icon::DocumentEdit), i18n("Edit..."), this, SLOT(slotStartEdit()));
205   d->m_contextMenuDuplicate = d->m_contextMenu->addAction(Icons::get(Icon::EditCopy), i18nc("To duplicate a split", "Duplicate"), this, SLOT(slotDuplicateSplit()));
206   d->m_contextMenuDelete = d->m_contextMenu->addAction(Icons::get(Icon::EditDelete),
207                         i18n("Delete..."),
208                         this, SLOT(slotDeleteSplit()));
209 
210   connect(this, &QAbstractItemView::clicked,
211           this, static_cast<void (KMyMoneySplitTable::*)(const QModelIndex&)>(&KMyMoneySplitTable::slotSetFocus));
212 
213   connect(this, &KMyMoneySplitTable::transactionChanged,
214           this, &KMyMoneySplitTable::slotUpdateData);
215 
216   installEventFilter(this);
217 }
218 
~KMyMoneySplitTable()219 KMyMoneySplitTable::~KMyMoneySplitTable()
220 {
221   Q_D(KMyMoneySplitTable);
222   auto grp = KSharedConfig::openConfig()->group("SplitTable");
223   QByteArray columns = horizontalHeader()->saveState();
224   grp.writeEntry("HeaderState", columns);
225   grp.sync();
226   delete d;
227 }
228 
currentRow() const229 int KMyMoneySplitTable::currentRow() const
230 {
231   Q_D(const KMyMoneySplitTable);
232   return d->m_currentRow;
233 }
234 
setup(const QMap<QString,MyMoneyMoney> & priceInfo,int precision)235 void KMyMoneySplitTable::setup(const QMap<QString, MyMoneyMoney>& priceInfo, int precision)
236 {
237   Q_D(KMyMoneySplitTable);
238   d->m_priceInfo = priceInfo;
239   d->m_precision = precision;
240 }
241 
eventFilter(QObject * o,QEvent * e)242 bool KMyMoneySplitTable::eventFilter(QObject *o, QEvent *e)
243 {
244   Q_D(KMyMoneySplitTable);
245   // MYMONEYTRACER(tracer);
246   QKeyEvent *k = static_cast<QKeyEvent *>(e);
247   bool rc = false;
248   int row = currentRow();
249   int lines = viewport()->height() / rowHeight(0);
250 
251   if (e->type() == QEvent::KeyPress && !isEditMode()) {
252     rc = true;
253     switch (k->key()) {
254       case Qt::Key_Up:
255         if (row)
256           slotSetFocus(model()->index(row - 1, 0));
257         break;
258 
259       case Qt::Key_Down:
260         if (row < d->m_transaction.splits().count() - 1)
261           slotSetFocus(model()->index(row + 1, 0));
262         break;
263 
264       case Qt::Key_Home:
265         slotSetFocus(model()->index(0, 0));
266         break;
267 
268       case Qt::Key_End:
269         slotSetFocus(model()->index(d->m_transaction.splits().count() - 1, 0));
270         break;
271 
272       case Qt::Key_PageUp:
273         if (lines) {
274           while (lines-- > 0 && row)
275             --row;
276           slotSetFocus(model()->index(row, 0));
277         }
278         break;
279 
280       case Qt::Key_PageDown:
281         if (row < d->m_transaction.splits().count() - 1) {
282           while (lines-- > 0 && row < d->m_transaction.splits().count() - 1)
283             ++row;
284           slotSetFocus(model()->index(row, 0));
285         }
286         break;
287 
288       case Qt::Key_Delete:
289         slotDeleteSplit();
290         break;
291 
292       case Qt::Key_Return:
293       case Qt::Key_Enter:
294         if (row < d->m_transaction.splits().count() - 1
295             && KMyMoneySettings::enterMovesBetweenFields()) {
296           slotStartEdit();
297         } else
298           emit returnPressed();
299         break;
300 
301       case Qt::Key_Escape:
302         emit escapePressed();
303         break;
304 
305       case Qt::Key_F2:
306         slotStartEdit();
307         break;
308 
309       default:
310         rc = true;
311 
312         // duplicate split
313         if (Qt::Key_C == k->key() && Qt::ControlModifier == k->modifiers()) {
314           slotDuplicateSplit();
315 
316           // new split
317         } else if (Qt::Key_Insert == k->key() && Qt::ControlModifier == k->modifiers()) {
318           slotSetFocus(model()->index(d->m_transaction.splits().count() - 1, 0));
319           slotStartEdit();
320 
321         } else if (k->text()[ 0 ].isPrint()) {
322           KMyMoneyCategory* cat = createEditWidgets(false);
323           if (cat) {
324             KMyMoneyLineEdit *le = qobject_cast<KMyMoneyLineEdit*>(cat->lineEdit());
325             if (le) {
326               // make sure, the widget receives the key again
327               // and does not select the text this time
328               le->setText(k->text());
329               le->end(false);
330               le->deselect();
331               le->skipSelectAll(true);
332               le->setFocus();
333             }
334           }
335         }
336         break;
337     }
338 
339   } else if (e->type() == QEvent::KeyPress && isEditMode()) {
340     bool terminate = true;
341     rc = true;
342     switch (k->key()) {
343         // suppress the F2 functionality to start editing in inline edit mode
344       case Qt::Key_F2:
345         // suppress the cursor movement in inline edit mode
346       case Qt::Key_Up:
347       case Qt::Key_Down:
348       case Qt::Key_PageUp:
349       case Qt::Key_PageDown:
350         break;
351 
352       case Qt::Key_Return:
353       case Qt::Key_Enter:
354         // we cannot call the slot directly, as it destroys the caller of
355         // this method :-(  So we let the event handler take care of calling
356         // the respective slot using a timeout. For a KLineEdit derived object
357         // it could be, that at this point the user selected a value from
358         // a completion list. In this case, we close the completion list and
359         // do not end editing of the transaction.
360         if (o->inherits("KLineEdit")) {
361           if (auto le = dynamic_cast<KLineEdit*>(o)) {
362             KCompletionBox* box = le->completionBox(false);
363             if (box && box->isVisible()) {
364               terminate = false;
365               le->completionBox(false)->hide();
366             }
367           }
368         }
369 
370         // in case we have the 'enter moves focus between fields', we need to simulate
371         // a TAB key when the object 'o' points to the category or memo field.
372         if (KMyMoneySettings::enterMovesBetweenFields()) {
373           if (o == d->m_editCategory->lineEdit() || o == d->m_editMemo || o == d->m_editTag) {
374             terminate = false;
375             QKeyEvent evt(e->type(),
376                           Qt::Key_Tab, k->modifiers(), QString(),
377                           k->isAutoRepeat(), k->count());
378 
379             QApplication::sendEvent(o, &evt);
380           }
381         }
382 
383         if (terminate) {
384           QTimer::singleShot(0, this, SLOT(slotEndEditKeyboard()));
385         }
386         break;
387 
388       case Qt::Key_Escape:
389         // we cannot call the slot directly, as it destroys the caller of
390         // this method :-(  So we let the event handler take care of calling
391         // the respective slot using a timeout.
392         QTimer::singleShot(0, this, SLOT(slotCancelEdit()));
393         break;
394 
395       default:
396         rc = false;
397         break;
398     }
399   } else if (e->type() == QEvent::KeyRelease && !isEditMode()) {
400     // for some reason, we only see a KeyRelease event of the Menu key
401     // here. In other locations (e.g. Register::eventFilter()) we see
402     // a KeyPress event. Strange. (ipwizard - 2008-05-10)
403     switch (k->key()) {
404       case Qt::Key_Menu:
405         // if the very last entry is selected, the delete
406         // operation is not available otherwise it is
407         d->m_contextMenuDelete->setEnabled(
408           row < d->m_transaction.splits().count() - 1);
409         d->m_contextMenuDuplicate->setEnabled(
410           row < d->m_transaction.splits().count() - 1);
411 
412         d->m_contextMenu->exec(QCursor::pos());
413         rc = true;
414         break;
415       default:
416         break;
417     }
418   }
419 
420   // if the event has not been processed here, forward it to
421   // the base class implementation if it's not a key event
422   if (rc == false) {
423     if (e->type() != QEvent::KeyPress
424         && e->type() != QEvent::KeyRelease) {
425       rc = QTableWidget::eventFilter(o, e);
426     }
427   }
428 
429   return rc;
430 }
431 
slotSetFocus(const QModelIndex & index)432 void KMyMoneySplitTable::slotSetFocus(const QModelIndex& index)
433 {
434   slotSetFocus(index, Qt::LeftButton);
435 }
436 
slotSetFocus(const QModelIndex & index,int button)437 void KMyMoneySplitTable::slotSetFocus(const QModelIndex& index, int button)
438 {
439   Q_D(KMyMoneySplitTable);
440   MYMONEYTRACER(tracer);
441   auto row = index.row();
442 
443   // adjust row to used area
444   if (row > d->m_transaction.splits().count() - 1)
445     row = d->m_transaction.splits().count() - 1;
446   if (row < 0)
447     row = 0;
448 
449   // make sure the row will be on the screen
450   scrollTo(model()->index(row, 0));
451 
452   if (isEditMode()) {                   // in edit mode?
453     if (isEditSplitValid() && KMyMoneySettings::focusChangeIsEnter())
454       endEdit(false/*keyboard driven*/, false/*set focus to next row*/);
455     else
456       slotCancelEdit();
457   }
458 
459   if (button == Qt::LeftButton) {         // left mouse button
460     if (row != currentRow()) {
461       // setup new current row and update visible selection
462       selectRow(row);
463       slotUpdateData(d->m_transaction);
464     }
465   } else if (button == Qt::RightButton) {
466     // context menu is only available when cursor is on
467     // an existing transaction or the first line after this area
468     if (row == index.row()) {
469       // setup new current row and update visible selection
470       selectRow(row);
471       slotUpdateData(d->m_transaction);
472 
473       // if the very last entry is selected, the delete
474       // operation is not available otherwise it is
475       d->m_contextMenuDelete->setEnabled(
476         row < d->m_transaction.splits().count() - 1);
477       d->m_contextMenuDuplicate->setEnabled(
478         row < d->m_transaction.splits().count() - 1);
479 
480       d->m_contextMenu->exec(QCursor::pos());
481     }
482   }
483 }
484 
mousePressEvent(QMouseEvent * e)485 void KMyMoneySplitTable::mousePressEvent(QMouseEvent* e)
486 {
487   slotSetFocus(indexAt(e->pos()), e->button());
488 }
489 
490 /* turn off QTable behaviour */
mouseReleaseEvent(QMouseEvent *)491 void KMyMoneySplitTable::mouseReleaseEvent(QMouseEvent* /* e */)
492 {
493 }
494 
mouseDoubleClickEvent(QMouseEvent * e)495 void KMyMoneySplitTable::mouseDoubleClickEvent(QMouseEvent *e)
496 {
497   Q_D(KMyMoneySplitTable);
498   MYMONEYTRACER(tracer);
499 
500   int col = columnAt(e->pos().x());
501   slotSetFocus(model()->index(rowAt(e->pos().y()), col), e->button());
502   createEditWidgets(false);
503 
504   QLineEdit* editWidget = 0;    //krazy:exclude=qmethods
505   switch (col) {
506     case 0:
507       editWidget = d->m_editCategory->lineEdit();
508       break;
509 
510     case 1:
511       editWidget = d->m_editMemo;
512       break;
513 
514     case 2:
515      d->m_editTag->tagCombo()->setFocus();
516       break;
517 
518     case 3:
519       editWidget = d->m_editAmount;
520       break;
521 
522     default:
523       break;
524   }
525   if (editWidget) {
526     editWidget->setFocus();
527     editWidget->selectAll();
528   }
529 }
530 
selectRow(int row)531 void KMyMoneySplitTable::selectRow(int row)
532 {
533   Q_D(KMyMoneySplitTable);
534   MYMONEYTRACER(tracer);
535 
536   if (row > d->m_maxRows)
537     row = d->m_maxRows;
538   d->m_currentRow = row;
539   QTableWidget::selectRow(row);
540   QList<MyMoneySplit> list = getSplits(d->m_transaction);
541   if (row < list.count())
542     d->m_split = list[row];
543   else
544     d->m_split = MyMoneySplit();
545 }
546 
setRowCount(int irows)547 void KMyMoneySplitTable::setRowCount(int irows)
548 {
549   QTableWidget::setRowCount(irows);
550 
551   // determine row height according to the edit widgets
552   // we use the category widget as the base
553   QFontMetrics fm(KMyMoneySettings::listCellFontEx());
554   int height = fm.lineSpacing() + 6;
555 #if 0
556   // recalculate row height hint
557   KMyMoneyCategory cat;
558   height = qMax(cat.sizeHint().height(), height);
559 #endif
560 
561   verticalHeader()->setUpdatesEnabled(false);
562 
563   for (auto i = 0; i < irows; ++i)
564     verticalHeader()->resizeSection(i, height);
565 
566   verticalHeader()->setUpdatesEnabled(true);
567 }
568 
setTransaction(const MyMoneyTransaction & t,const MyMoneySplit & s,const MyMoneyAccount & acc)569 void KMyMoneySplitTable::setTransaction(const MyMoneyTransaction& t, const MyMoneySplit& s, const MyMoneyAccount& acc)
570 {
571   Q_D(KMyMoneySplitTable);
572   MYMONEYTRACER(tracer);
573   d->m_transaction = t;
574   d->m_account = acc;
575   d->m_hiddenSplit = s;
576   selectRow(0);
577   slotUpdateData(d->m_transaction);
578 }
579 
transaction() const580 MyMoneyTransaction KMyMoneySplitTable::transaction() const
581 {
582   Q_D(const KMyMoneySplitTable);
583   return d->m_transaction;
584 }
585 
getSplits(const MyMoneyTransaction & t) const586 QList<MyMoneySplit> KMyMoneySplitTable::getSplits(const MyMoneyTransaction& t) const
587 {
588   Q_D(const KMyMoneySplitTable);
589   // get list of splits
590   QList<MyMoneySplit> list = t.splits();
591 
592   // and ignore the one that should be hidden
593   QList<MyMoneySplit>::Iterator it;
594   for (it = list.begin(); it != list.end(); ++it) {
595     if ((*it).id() == d->m_hiddenSplit.id()) {
596       list.erase(it);
597       break;
598     }
599   }
600   return list;
601 }
602 
slotUpdateData(const MyMoneyTransaction & t)603 void KMyMoneySplitTable::slotUpdateData(const MyMoneyTransaction& t)
604 {
605   Q_D(KMyMoneySplitTable);
606   MYMONEYTRACER(tracer);
607   unsigned long numRows = 0;
608   QTableWidgetItem* textItem;
609 
610   QList<MyMoneySplit> list = getSplits(t);
611   updateTransactionTableSize();
612 
613   // fill the part that is used by transactions
614   QList<MyMoneySplit>::Iterator it;
615   for (it = list.begin(); it != list.end(); ++it) {
616     QString colText;
617     MyMoneyMoney value = (*it).value();
618     if (!(*it).accountId().isEmpty()) {
619       try {
620         colText = MyMoneyFile::instance()->accountToCategory((*it).accountId());
621       } catch (const MyMoneyException &) {
622         qDebug("Unexpected exception in KMyMoneySplitTable::slotUpdateData()");
623       }
624     }
625     QString amountTxt = value.formatMoney(d->m_account.fraction());
626     if (value == MyMoneyMoney::autoCalc) {
627       amountTxt = i18n("will be calculated");
628     }
629 
630     if (colText.isEmpty() && (*it).memo().isEmpty() && value.isZero())
631       amountTxt.clear();
632 
633     unsigned width = fontMetrics().width(amountTxt);
634     AmountEdit* valfield = new AmountEdit();
635     valfield->setMinimumWidth(width);
636     width = valfield->minimumSizeHint().width();
637     delete valfield;
638 
639     textItem = item(numRows, 0);
640     if (textItem)
641       textItem->setText(colText);
642     else
643       setItem(numRows, 0, new QTableWidgetItem(colText));
644 
645     textItem = item(numRows, 1);
646     if (textItem)
647       textItem->setText((*it).memo());
648     else
649       setItem(numRows, 1, new QTableWidgetItem((*it).memo()));
650 
651     QList<QString> tl = (*it).tagIdList();
652     QStringList tagNames;
653     if (!tl.isEmpty()) {
654       for (int i = 0; i < tl.size(); i++)
655         tagNames.append(MyMoneyFile::instance()->tag(tl[i]).name());
656     }
657     setItem(numRows, 2, new QTableWidgetItem(tagNames.join(", ")));
658 
659     textItem = item(numRows, 3);
660     if (textItem)
661       textItem->setText(amountTxt);
662     else
663       setItem(numRows, 3, new QTableWidgetItem(amountTxt));
664 
665     item(numRows, 3)->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
666 
667     ++numRows;
668   }
669 
670   // now clean out the remainder of the table
671   while (numRows < static_cast<unsigned long>(rowCount())) {
672     for (auto i = 0 ; i < 4; ++i) {
673       textItem = item(numRows, i);
674       if (textItem)
675         textItem->setText("");
676       else
677         setItem(numRows, i, new QTableWidgetItem(""));
678     }
679     item(numRows, 3)->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
680     ++numRows;
681   }
682 }
683 
updateTransactionTableSize()684 void KMyMoneySplitTable::updateTransactionTableSize()
685 {
686   Q_D(KMyMoneySplitTable);
687   // get current size of transactions table
688   int tableHeight = height();
689   int splitCount = d->m_transaction.splits().count() - 1;
690 
691   if (splitCount < 0)
692     splitCount = 0;
693 
694   // see if we need some extra lines to fill the current size with the grid
695   int numExtraLines = (tableHeight / rowHeight(0)) - splitCount;
696   if (numExtraLines < 2)
697     numExtraLines = 2;
698 
699   setRowCount(splitCount + numExtraLines);
700   d->m_maxRows = splitCount;
701 }
702 
resizeEvent(QResizeEvent * ev)703 void KMyMoneySplitTable::resizeEvent(QResizeEvent* ev)
704 {
705   QTableWidget::resizeEvent(ev);
706   if (!isEditMode()) {
707     // update the size of the transaction table only if a split is not being edited
708     // otherwise the height of the editors would be altered in an undesired way
709     updateTransactionTableSize();
710   }
711 }
712 
slotDuplicateSplit()713 void KMyMoneySplitTable::slotDuplicateSplit()
714 {
715   Q_D(KMyMoneySplitTable);
716   MYMONEYTRACER(tracer);
717   QList<MyMoneySplit> list = getSplits(d->m_transaction);
718   if (d->m_currentRow < list.count()) {
719     MyMoneySplit split = list[d->m_currentRow];
720     split.clearId();
721     try {
722       d->m_transaction.addSplit(split);
723       emit transactionChanged(d->m_transaction);
724     } catch (const MyMoneyException &e) {
725       qDebug("Cannot duplicate split: %s", e.what());
726     }
727   }
728 }
729 
slotDeleteSplit()730 void KMyMoneySplitTable::slotDeleteSplit()
731 {
732   Q_D(KMyMoneySplitTable);
733   MYMONEYTRACER(tracer);
734   QList<MyMoneySplit> list = getSplits(d->m_transaction);
735   if (d->m_currentRow < list.count()) {
736     if (KMessageBox::warningContinueCancel(this,
737                                            i18n("You are about to delete the selected split. "
738                                                 "Do you really want to continue?"),
739                                            i18n("KMyMoney")
740                                           ) == KMessageBox::Continue) {
741       try {
742         d->m_transaction.removeSplit(list[d->m_currentRow]);
743         // if we removed the last split, select the previous
744         if (d->m_currentRow && d->m_currentRow == list.count() - 1)
745           selectRow(d->m_currentRow - 1);
746         else
747           selectRow(d->m_currentRow);
748         emit transactionChanged(d->m_transaction);
749       } catch (const MyMoneyException &e) {
750         qDebug("Cannot remove split: %s", e.what());
751       }
752     }
753   }
754 }
755 
slotStartEdit()756 KMyMoneyCategory* KMyMoneySplitTable::slotStartEdit()
757 {
758   MYMONEYTRACER(tracer);
759   return createEditWidgets(true);
760 }
761 
slotEndEdit()762 void KMyMoneySplitTable::slotEndEdit()
763 {
764   endEdit(false);
765 }
766 
slotEndEditKeyboard()767 void KMyMoneySplitTable::slotEndEditKeyboard()
768 {
769   endEdit(true);
770 }
771 
endEdit(bool keyboardDriven,bool setFocusToNextRow)772 void KMyMoneySplitTable::endEdit(bool keyboardDriven, bool setFocusToNextRow)
773 {
774   Q_D(KMyMoneySplitTable);
775   auto file = MyMoneyFile::instance();
776 
777   MYMONEYTRACER(tracer);
778   MyMoneySplit s1 = d->m_split;
779 
780   if (!isEditSplitValid()) {
781     KMessageBox::information(this, i18n("You need to assign a category to this split before it can be entered."), i18n("Enter split"), "EnterSplitWithEmptyCategory");
782     d->m_editCategory->setFocus();
783     return;
784   }
785 
786   bool needUpdate = false;
787   if (d->m_editCategory->selectedItem() != d->m_split.accountId()) {
788     s1.setAccountId(d->m_editCategory->selectedItem());
789     needUpdate = true;
790   }
791   if (d->m_editMemo->text() != d->m_split.memo()) {
792     s1.setMemo(d->m_editMemo->text());
793     needUpdate = true;
794   }
795   if (d->m_editTag->selectedTags() != d->m_split.tagIdList()) {
796     s1.setTagIdList(d->m_editTag->selectedTags());
797     needUpdate = true;
798   }
799   if (d->m_editAmount->value() != d->m_split.value()) {
800     s1.setValue(d->m_editAmount->value());
801     needUpdate = true;
802   }
803 
804   if (needUpdate) {
805     if (!s1.value().isZero()) {
806       MyMoneyAccount cat = file->account(s1.accountId());
807       if (cat.currencyId() != d->m_transaction.commodity()) {
808 
809         MyMoneySecurity fromCurrency, toCurrency;
810         MyMoneyMoney fromValue, toValue;
811         fromCurrency = file->security(d->m_transaction.commodity());
812         toCurrency = file->security(cat.currencyId());
813 
814         // determine the fraction required for this category
815         int fract = toCurrency.smallestAccountFraction();
816         if (cat.accountType() == eMyMoney::Account::Type::Cash)
817           fract = toCurrency.smallestCashFraction();
818 
819         // display only positive values to the user
820         fromValue = s1.value().abs();
821 
822         // if we had a price info in the beginning, we use it here
823         if (d->m_priceInfo.find(cat.currencyId()) != d->m_priceInfo.end()) {
824           toValue = (fromValue * d->m_priceInfo[cat.currencyId()]).convert(fract);
825         }
826 
827         // if the shares are still 0, we need to change that
828         if (toValue.isZero()) {
829           const MyMoneyPrice &price = MyMoneyFile::instance()->price(fromCurrency.id(), toCurrency.id());
830           // if the price is valid calculate the shares. If it is invalid
831           // assume a conversion rate of 1.0
832           if (price.isValid()) {
833             toValue = (price.rate(toCurrency.id()) * fromValue).convert(fract);
834           } else {
835             toValue = fromValue;
836           }
837         }
838 
839         // now present all that to the user
840         QPointer<KCurrencyCalculator> calc =
841           new KCurrencyCalculator(fromCurrency,
842                                   toCurrency,
843                                   fromValue,
844                                   toValue,
845                                   d->m_transaction.postDate(),
846                                   fract,
847                                   this);
848 
849         if (calc->exec() == QDialog::Rejected) {
850           delete calc;
851           return;
852         } else {
853           s1.setShares((s1.value() * calc->price()).convert(fract));
854           delete calc;
855         }
856 
857       } else {
858         s1.setShares(s1.value());
859       }
860     } else
861       s1.setShares(s1.value());
862 
863     d->m_split = s1;
864     try {
865       if (d->m_split.id().isEmpty()) {
866         d->m_transaction.addSplit(d->m_split);
867       } else {
868         d->m_transaction.modifySplit(d->m_split);
869       }
870       emit transactionChanged(d->m_transaction);
871     } catch (const MyMoneyException &e) {
872       qDebug("Cannot add/modify split: %s", e.what());
873     }
874   }
875   this->setFocus();
876   destroyEditWidgets();
877   if (setFocusToNextRow) {
878     slotSetFocus(model()->index(currentRow() + 1, 0));
879   }
880 
881   // if we still have more splits, we start editing right away
882   // in case we have selected 'enter moves between fields'
883   if (keyboardDriven
884       && currentRow() < d->m_transaction.splits().count() - 1
885       && KMyMoneySettings::enterMovesBetweenFields()) {
886     slotStartEdit();
887   }
888 
889 }
890 
slotCancelEdit()891 void KMyMoneySplitTable::slotCancelEdit()
892 {
893   Q_D(const KMyMoneySplitTable);
894   MYMONEYTRACER(tracer);
895   if (isEditMode()) {
896     /*
897      * Prevent asking to add a new category which happens if the user entered any text
898      * caused by emitting signals in KMyMoneyCombo::focusOutEvent() on focus out event.
899      * (see bug 344409)
900      */
901     if (d->m_editCategory)
902       d->m_editCategory->lineEdit()->setText(QString());
903     destroyEditWidgets();
904     this->setFocus();
905   }
906 }
907 
isEditMode() const908 bool KMyMoneySplitTable::isEditMode() const
909 {
910   Q_D(const KMyMoneySplitTable);
911   // while the edit widgets exist we're in edit mode
912   return d->m_editAmount || d->m_editMemo || d->m_editCategory || d->m_editTag;
913 }
914 
isEditSplitValid() const915 bool KMyMoneySplitTable::isEditSplitValid() const
916 {
917   Q_D(const KMyMoneySplitTable);
918   return isEditMode() && !(d->m_editCategory && d->m_editCategory->selectedItem().isEmpty());
919 }
920 
destroyEditWidgets()921 void KMyMoneySplitTable::destroyEditWidgets()
922 {
923   MYMONEYTRACER(tracer);
924 
925   Q_D(KMyMoneySplitTable);
926   emit editFinished();
927 
928   disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, this, &KMyMoneySplitTable::slotLoadEditWidgets);
929 
930   destroyEditWidget(d->m_currentRow, 0);
931   destroyEditWidget(d->m_currentRow, 1);
932   destroyEditWidget(d->m_currentRow, 2);
933   destroyEditWidget(d->m_currentRow, 3);
934   destroyEditWidget(d->m_currentRow + 1, 0);
935 }
936 
destroyEditWidget(int r,int c)937 void KMyMoneySplitTable::destroyEditWidget(int r, int c)
938 {
939   if (QWidget* cw = cellWidget(r, c))
940     cw->hide();
941   removeCellWidget(r, c);
942 }
943 
createEditWidgets(bool setFocus)944 KMyMoneyCategory* KMyMoneySplitTable::createEditWidgets(bool setFocus)
945 {
946   MYMONEYTRACER(tracer);
947 
948   emit editStarted();
949 
950   Q_D(KMyMoneySplitTable);
951   auto cellFont = KMyMoneySettings::listCellFontEx();
952   d->m_tabOrderWidgets.clear();
953 
954   // create the widgets
955   d->m_editAmount = new AmountEdit;
956   d->m_editAmount->setFont(cellFont);
957   d->m_editAmount->setCalculatorButtonVisible(true);
958   d->m_editAmount->setPrecision(d->m_precision);
959 
960   d->m_editCategory = new KMyMoneyCategory();
961   d->m_editCategory->setPlaceholderText(i18n("Category"));
962   d->m_editCategory->setFont(cellFont);
963   connect(d->m_editCategory, SIGNAL(createItem(QString,QString&)), this, SIGNAL(createCategory(QString,QString&)));
964   connect(d->m_editCategory, SIGNAL(objectCreation(bool)), this, SIGNAL(objectCreation(bool)));
965 
966   d->m_editMemo = new KMyMoneyLineEdit(0, false, Qt::AlignLeft | Qt::AlignVCenter);
967   d->m_editMemo->setPlaceholderText(i18n("Memo"));
968   d->m_editMemo->setFont(cellFont);
969 
970   d->m_editTag = new KTagContainer;
971   d->m_editTag->tagCombo()->setPlaceholderText(i18n("Tag"));
972   d->m_editTag->tagCombo()->setFont(cellFont);
973   d->m_editTag->loadTags(MyMoneyFile::instance()->tagList());
974   connect(d->m_editTag->tagCombo(), SIGNAL(createItem(QString,QString&)), this, SIGNAL(createTag(QString,QString&)));
975   connect(d->m_editTag->tagCombo(), SIGNAL(objectCreation(bool)), this, SIGNAL(objectCreation(bool)));
976 
977   // create buttons for the mouse users
978   d->m_registerButtonFrame = new QFrame(this);
979   d->m_registerButtonFrame->setContentsMargins(0, 0, 0, 0);
980   d->m_registerButtonFrame->setAutoFillBackground(true);
981 
982   QHBoxLayout* l = new QHBoxLayout(d->m_registerButtonFrame);
983   l->setContentsMargins(0, 0, 0, 0);
984   l->setSpacing(0);
985   d->m_registerEnterButton = new QPushButton(Icons::get(Icon::DialogOK)
986                                           , QString(), d->m_registerButtonFrame);
987   d->m_registerCancelButton = new QPushButton(Icons::get(Icon::DialogCancel)
988                                            , QString(), d->m_registerButtonFrame);
989 
990   l->addWidget(d->m_registerEnterButton);
991   l->addWidget(d->m_registerCancelButton);
992   l->addStretch(2);
993 
994   connect(d->m_registerEnterButton.data(), &QAbstractButton::clicked, this, &KMyMoneySplitTable::slotEndEdit);
995   connect(d->m_registerCancelButton.data(), &QAbstractButton::clicked, this, &KMyMoneySplitTable::slotCancelEdit);
996 
997   // setup tab order
998   addToTabOrder(d->m_editCategory);
999   addToTabOrder(d->m_editMemo);
1000   addToTabOrder(d->m_editTag);
1001   addToTabOrder(d->m_editAmount);
1002   addToTabOrder(d->m_registerEnterButton);
1003   addToTabOrder(d->m_registerCancelButton);
1004 
1005   if (!d->m_split.accountId().isEmpty()) {
1006     d->m_editCategory->setSelectedItem(d->m_split.accountId());
1007   } else {
1008     // check if the transaction is balanced or not. If not,
1009     // assign the remainder to the amount.
1010     MyMoneyMoney diff;
1011     QList<MyMoneySplit> list = d->m_transaction.splits();
1012     QList<MyMoneySplit>::ConstIterator it_s;
1013     for (it_s = list.constBegin(); it_s != list.constEnd(); ++it_s) {
1014       if (!(*it_s).accountId().isEmpty())
1015         diff += (*it_s).value();
1016     }
1017     d->m_split.setValue(-diff);
1018   }
1019 
1020   QList<QString> t = d->m_split.tagIdList();
1021   if (!t.isEmpty()) {
1022     for (int i = 0; i < t.size(); i++)
1023       d->m_editTag->addTagWidget(t[i]);
1024   }
1025 
1026   d->m_editMemo->loadText(d->m_split.memo());
1027   // don't allow automatically calculated values to be modified
1028   if (d->m_split.value() == MyMoneyMoney::autoCalc) {
1029     d->m_editAmount->setEnabled(false);
1030     d->m_editAmount->setText("will be calculated");
1031   } else
1032     d->m_editAmount->setValue(d->m_split.value());
1033 
1034   setCellWidget(d->m_currentRow, 0, d->m_editCategory);
1035   setCellWidget(d->m_currentRow, 1, d->m_editMemo);
1036   setCellWidget(d->m_currentRow, 2, d->m_editTag);
1037   setCellWidget(d->m_currentRow, 3, d->m_editAmount);
1038   setCellWidget(d->m_currentRow + 1, 0, d->m_registerButtonFrame);
1039 
1040   // load e.g. the category widget with the account list
1041   slotLoadEditWidgets();
1042   connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, this, &KMyMoneySplitTable::slotLoadEditWidgets);
1043 
1044   foreach (QWidget* w, d->m_tabOrderWidgets) {
1045     if (w) {
1046       w->installEventFilter(this);
1047     }
1048   }
1049 
1050   if (setFocus) {
1051     d->m_editCategory->lineEdit()->setFocus();
1052     d->m_editCategory->lineEdit()->selectAll();
1053   }
1054 
1055   // resize the rows so the added edit widgets would fit appropriately
1056   resizeRowsToContents();
1057 
1058   return d->m_editCategory;
1059 }
1060 
slotLoadEditWidgets()1061 void KMyMoneySplitTable::slotLoadEditWidgets()
1062 {
1063   Q_D(KMyMoneySplitTable);
1064   // reload category widget
1065   auto categoryId = d->m_editCategory->selectedItem();
1066 
1067   AccountSet aSet;
1068   aSet.addAccountGroup(eMyMoney::Account::Type::Asset);
1069   aSet.addAccountGroup(eMyMoney::Account::Type::Liability);
1070   aSet.addAccountGroup(eMyMoney::Account::Type::Income);
1071   aSet.addAccountGroup(eMyMoney::Account::Type::Expense);
1072   if (KMyMoneySettings::expertMode())
1073     aSet.addAccountGroup(eMyMoney::Account::Type::Equity);
1074 
1075   // remove the accounts with invalid types at this point
1076   aSet.removeAccountType(eMyMoney::Account::Type::CertificateDep);
1077   aSet.removeAccountType(eMyMoney::Account::Type::Investment);
1078   aSet.removeAccountType(eMyMoney::Account::Type::Stock);
1079   aSet.removeAccountType(eMyMoney::Account::Type::MoneyMarket);
1080 
1081   aSet.load(d->m_editCategory->selector());
1082 
1083   // if an account is specified then remove it from the widget so that the user
1084   // cannot create a transfer with from and to account being the same account
1085   if (!d->m_account.id().isEmpty())
1086     d->m_editCategory->selector()->removeItem(d->m_account.id());
1087 
1088   if (!categoryId.isEmpty())
1089     d->m_editCategory->setSelectedItem(categoryId);
1090 }
1091 
addToTabOrder(QWidget * w)1092 void KMyMoneySplitTable::addToTabOrder(QWidget* w)
1093 {
1094   Q_D(KMyMoneySplitTable);
1095   if (w) {
1096     while (w->focusProxy())
1097       w = w->focusProxy();
1098     d->m_tabOrderWidgets.append(w);
1099   }
1100 }
1101 
focusNextPrevChild(bool next)1102 bool KMyMoneySplitTable::focusNextPrevChild(bool next)
1103 {
1104   MYMONEYTRACER(tracer);
1105   Q_D(KMyMoneySplitTable);
1106   auto rc = false;
1107   if (isEditMode()) {
1108     QWidget *w = 0;
1109 
1110     w = qApp->focusWidget();
1111     int currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w);
1112     while (w && currentWidgetIndex == -1) {
1113       // qDebug("'%s' not in list, use parent", w->className());
1114       w = w->parentWidget();
1115       currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w);
1116     }
1117 
1118     if (currentWidgetIndex != -1) {
1119       // if(w) qDebug("tab order is at '%s'", w->className());
1120       currentWidgetIndex += next ? 1 : -1;
1121       if (currentWidgetIndex < 0)
1122         currentWidgetIndex = d->m_tabOrderWidgets.size() - 1;
1123       else if (currentWidgetIndex >= d->m_tabOrderWidgets.size())
1124         currentWidgetIndex = 0;
1125 
1126       w = d->m_tabOrderWidgets[currentWidgetIndex];
1127 
1128       if (((w->focusPolicy() & Qt::TabFocus) == Qt::TabFocus) && w->isVisible() && w->isEnabled()) {
1129         // qDebug("Selecting '%s' as focus", w->className());
1130         w->setFocus(next ? Qt::TabFocusReason: Qt::BacktabFocusReason);
1131         rc = true;
1132       }
1133     }
1134   } else
1135     rc = QTableWidget::focusNextPrevChild(next);
1136   return rc;
1137 }
1138