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