File indexing completed on 2024-05-12 16:42:08

0001 /*
0002     SPDX-FileCopyrightText: 2008-2018 Thomas Baumgart <tbaumgart@kde.org>
0003     SPDX-FileCopyrightText: 2017-2018 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "kmymoneysplittable.h"
0008 
0009 // ----------------------------------------------------------------------------
0010 // QT Includes
0011 
0012 #include <QCursor>
0013 #include <QApplication>
0014 #include <QTimer>
0015 #include <QHBoxLayout>
0016 #include <QKeyEvent>
0017 #include <QFrame>
0018 #include <QMouseEvent>
0019 #include <QEvent>
0020 #include <QPushButton>
0021 #include <QMenu>
0022 #include <QIcon>
0023 #include <QHeaderView>
0024 #include <QPointer>
0025 #include <QList>
0026 
0027 // ----------------------------------------------------------------------------
0028 // KDE Includes
0029 
0030 #include <KMessageBox>
0031 #include <KCompletionBox>
0032 #include <KSharedConfig>
0033 #include <KLocalizedString>
0034 
0035 // ----------------------------------------------------------------------------
0036 // Project Includes
0037 
0038 #include "mymoneysplit.h"
0039 #include "mymoneytransaction.h"
0040 #include "mymoneyaccount.h"
0041 #include "mymoneyfile.h"
0042 #include "mymoneyprice.h"
0043 #include "amountedit.h"
0044 #include "kmymoneycategory.h"
0045 #include "kmymoneyaccountselector.h"
0046 #include "kmymoneylineedit.h"
0047 #include "mymoneysecurity.h"
0048 #include "kmymoneysettings.h"
0049 #include "kmymoneymvccombo.h"
0050 #include "mymoneytag.h"
0051 #include "kmymoneytagcombo.h"
0052 #include "ktagcontainer.h"
0053 #include "kcurrencycalculator.h"
0054 #include "mymoneyutils.h"
0055 #include "mymoneytracer.h"
0056 #include "mymoneyexception.h"
0057 #include "icons.h"
0058 #include "mymoneyenums.h"
0059 
0060 using namespace Icons;
0061 
0062 class KMyMoneySplitTablePrivate
0063 {
0064     Q_DISABLE_COPY(KMyMoneySplitTablePrivate)
0065 
0066 public:
0067     KMyMoneySplitTablePrivate()
0068         : m_currentRow(0)
0069         , m_maxRows(0)
0070         , m_precision(2)
0071         , m_contextMenu(nullptr)
0072         , m_contextMenuDelete(nullptr)
0073         , m_contextMenuDuplicate(nullptr)
0074         , m_editCategory(0)
0075         , m_editTag(0)
0076         , m_editMemo(0)
0077         , m_editAmount(0)
0078         , m_readOnly(false)
0079     {
0080     }
0081 
0082     ~KMyMoneySplitTablePrivate()
0083     {
0084     }
0085 
0086     /// the currently selected row (will be printed as selected)
0087     int                 m_currentRow;
0088 
0089     /// the number of rows filled with data
0090     int                 m_maxRows;
0091 
0092     MyMoneyTransaction  m_transaction;
0093     MyMoneyAccount      m_account;
0094     MyMoneySplit        m_split;
0095     MyMoneySplit        m_hiddenSplit;
0096 
0097     /**
0098       * This member keeps the precision for the values
0099       */
0100     int                 m_precision;
0101 
0102     /**
0103       * This member keeps a pointer to the context menu
0104       */
0105     QMenu*         m_contextMenu;
0106 
0107     /// keeps the QAction of the delete entry in the context menu
0108     QAction*       m_contextMenuDelete;
0109 
0110     /// keeps the QAction of the duplicate entry in the context menu
0111     QAction*       m_contextMenuDuplicate;
0112 
0113     /**
0114       * This member contains a pointer to the input widget for the category.
0115       * The widget will be created and destroyed dynamically in createInputWidgets()
0116       * and destroyInputWidgets().
0117       */
0118     QPointer<KMyMoneyCategory> m_editCategory;
0119 
0120     /**
0121       * This member contains a pointer to the tag widget for the memo.
0122       */
0123     QPointer<KTagContainer> m_editTag;
0124 
0125     /**
0126       * This member contains a pointer to the input widget for the memo.
0127       * The widget will be created and destroyed dynamically in createInputWidgets()
0128       * and destroyInputWidgets().
0129       */
0130     QPointer<KMyMoneyLineEdit> m_editMemo;
0131 
0132     /**
0133       * This member contains a pointer to the input widget for the amount.
0134       * The widget will be created and destroyed dynamically in createInputWidgets()
0135       * and destroyInputWidgets().
0136       */
0137     QPointer<AmountEdit>     m_editAmount;
0138 
0139     /**
0140       * This member keeps the tab order for the above widgets
0141       */
0142     QWidgetList         m_tabOrderWidgets;
0143 
0144     QPointer<QFrame>           m_registerButtonFrame;
0145     QPointer<QPushButton>      m_registerEnterButton;
0146     QPointer<QPushButton>      m_registerCancelButton;
0147 
0148     QMap<QString, MyMoneyMoney>  m_priceInfo;
0149 
0150     bool m_readOnly;
0151 };
0152 
0153 KMyMoneySplitTable::KMyMoneySplitTable(QWidget *parent) :
0154     QTableWidget(parent),
0155     d_ptr(new KMyMoneySplitTablePrivate)
0156 {
0157     Q_D(KMyMoneySplitTable);
0158     // used for custom coloring with the help of the application's stylesheet
0159     setObjectName(QLatin1String("splittable"));
0160 
0161     // setup the transactions table
0162     setRowCount(1);
0163     setColumnCount(4);
0164     QStringList labels;
0165     labels << i18n("Category") << i18n("Memo") << i18n("Tag") << i18n("Amount");
0166     setHorizontalHeaderLabels(labels);
0167     setSelectionMode(QAbstractItemView::SingleSelection);
0168     setSelectionBehavior(QAbstractItemView::SelectRows);
0169     int left, top, right, bottom;
0170     getContentsMargins(&left, &top, &right, &bottom);
0171     setContentsMargins(0, top, right, bottom);
0172 
0173     setFont(KMyMoneySettings::listCellFontEx());
0174 
0175     setAlternatingRowColors(true);
0176 
0177     verticalHeader()->hide();
0178     horizontalHeader()->setSectionsMovable(false);
0179     horizontalHeader()->setFont(KMyMoneySettings::listHeaderFontEx());
0180 
0181     KConfigGroup grp = KSharedConfig::openConfig()->group("SplitTable");
0182     QByteArray columns;
0183     columns = grp.readEntry("HeaderState", columns);
0184     horizontalHeader()->restoreState(columns);
0185     horizontalHeader()->setStretchLastSection(true);
0186 
0187     setShowGrid(KMyMoneySettings::showGrid());
0188 
0189     setEditTriggers(QAbstractItemView::NoEditTriggers);
0190 
0191     // setup the context menu
0192     d->m_contextMenu = new QMenu(this);
0193     d->m_contextMenu->setTitle(i18n("Split Options"));
0194     d->m_contextMenu->setIcon(Icons::get(Icon::Transaction));
0195     d->m_contextMenu->addAction(Icons::get(Icon::DocumentEdit), i18n("Edit..."), this, SLOT(slotStartEdit()));
0196     d->m_contextMenuDuplicate = d->m_contextMenu->addAction(Icons::get(Icon::EditCopy), i18nc("To duplicate a split", "Duplicate"), this, SLOT(slotDuplicateSplit()));
0197     d->m_contextMenuDelete = d->m_contextMenu->addAction(Icons::get(Icon::EditDelete),
0198                              i18n("Delete..."),
0199                              this, SLOT(slotDeleteSplit()));
0200 
0201     connect(this, &QAbstractItemView::clicked,
0202             this, static_cast<void (KMyMoneySplitTable::*)(const QModelIndex&)>(&KMyMoneySplitTable::slotSetFocus));
0203 
0204     connect(this, &KMyMoneySplitTable::transactionChanged,
0205             this, &KMyMoneySplitTable::slotUpdateData);
0206 
0207     installEventFilter(this);
0208 }
0209 
0210 KMyMoneySplitTable::~KMyMoneySplitTable()
0211 {
0212     Q_D(KMyMoneySplitTable);
0213     auto grp = KSharedConfig::openConfig()->group("SplitTable");
0214     QByteArray columns = horizontalHeader()->saveState();
0215     grp.writeEntry("HeaderState", columns);
0216     grp.sync();
0217     delete d;
0218 }
0219 
0220 int KMyMoneySplitTable::currentRow() const
0221 {
0222     Q_D(const KMyMoneySplitTable);
0223     return d->m_currentRow;
0224 }
0225 
0226 void KMyMoneySplitTable::setup(const QMap<QString, MyMoneyMoney>& priceInfo, int precision)
0227 {
0228     Q_D(KMyMoneySplitTable);
0229     d->m_priceInfo = priceInfo;
0230     d->m_precision = precision;
0231 }
0232 
0233 bool KMyMoneySplitTable::eventFilter(QObject *o, QEvent *e)
0234 {
0235     Q_D(KMyMoneySplitTable);
0236     // MYMONEYTRACER(tracer);
0237     QKeyEvent *k = static_cast<QKeyEvent *>(e);
0238     bool rc = false;
0239     int row = currentRow();
0240     int lines = viewport()->height() / rowHeight(0);
0241 
0242     if (e->type() == QEvent::KeyPress && !isEditMode()) {
0243         rc = true;
0244         switch (k->key()) {
0245         case Qt::Key_Up:
0246             if (row)
0247                 slotSetFocus(model()->index(row - 1, 0));
0248             break;
0249 
0250         case Qt::Key_Down:
0251             if (row < d->m_transaction.splits().count() - 1)
0252                 slotSetFocus(model()->index(row + 1, 0));
0253             break;
0254 
0255         case Qt::Key_Home:
0256             slotSetFocus(model()->index(0, 0));
0257             break;
0258 
0259         case Qt::Key_End:
0260             slotSetFocus(model()->index(d->m_transaction.splits().count() - 1, 0));
0261             break;
0262 
0263         case Qt::Key_PageUp:
0264             if (lines) {
0265                 while (lines-- > 0 && row)
0266                     --row;
0267                 slotSetFocus(model()->index(row, 0));
0268             }
0269             break;
0270 
0271         case Qt::Key_PageDown:
0272             if (row < d->m_transaction.splits().count() - 1) {
0273                 while (lines-- > 0 && row < d->m_transaction.splits().count() - 1)
0274                     ++row;
0275                 slotSetFocus(model()->index(row, 0));
0276             }
0277             break;
0278 
0279         case Qt::Key_Delete:
0280             slotDeleteSplit();
0281             break;
0282 
0283         case Qt::Key_Return:
0284         case Qt::Key_Enter:
0285             if (row < d->m_transaction.splits().count() - 1
0286                     && KMyMoneySettings::enterMovesBetweenFields()) {
0287                 slotStartEdit();
0288             } else
0289                 emit returnPressed();
0290             break;
0291 
0292         case Qt::Key_Escape:
0293             emit escapePressed();
0294             break;
0295 
0296         case Qt::Key_F2:
0297             slotStartEdit();
0298             break;
0299 
0300         default:
0301             rc = true;
0302 
0303             // duplicate split
0304             if (Qt::Key_C == k->key() && Qt::ControlModifier == k->modifiers()) {
0305                 slotDuplicateSplit();
0306 
0307                 // new split
0308             } else if (Qt::Key_Insert == k->key() && Qt::ControlModifier == k->modifiers()) {
0309                 slotSetFocus(model()->index(d->m_transaction.splits().count() - 1, 0));
0310                 slotStartEdit();
0311 
0312             } else if (k->text()[ 0 ].isPrint()) {
0313                 KMyMoneyCategory* cat = createEditWidgets(false);
0314                 if (cat) {
0315                     KMyMoneyLineEdit *le = qobject_cast<KMyMoneyLineEdit*>(cat->lineEdit());
0316                     if (le) {
0317                         // make sure, the widget receives the key again
0318                         // and does not select the text this time
0319                         le->setText(k->text());
0320                         le->end(false);
0321                         le->deselect();
0322                         le->skipSelectAll(true);
0323                         le->setFocus();
0324                     }
0325                 }
0326             }
0327             break;
0328         }
0329 
0330     } else if (e->type() == QEvent::KeyPress && isEditMode()) {
0331         bool terminate = true;
0332         rc = true;
0333         switch (k->key()) {
0334         // suppress the F2 functionality to start editing in inline edit mode
0335         case Qt::Key_F2:
0336         // suppress the cursor movement in inline edit mode
0337         case Qt::Key_Up:
0338         case Qt::Key_Down:
0339         case Qt::Key_PageUp:
0340         case Qt::Key_PageDown:
0341             break;
0342 
0343         case Qt::Key_Return:
0344         case Qt::Key_Enter:
0345             // we cannot call the slot directly, as it destroys the caller of
0346             // this method :-(  So we let the event handler take care of calling
0347             // the respective slot using a timeout. For a KLineEdit derived object
0348             // it could be, that at this point the user selected a value from
0349             // a completion list. In this case, we close the completion list and
0350             // do not end editing of the transaction.
0351             if (o->inherits("KLineEdit")) {
0352                 if (auto le = dynamic_cast<KLineEdit*>(o)) {
0353                     KCompletionBox* box = le->completionBox(false);
0354                     if (box && box->isVisible()) {
0355                         terminate = false;
0356                         le->completionBox(false)->hide();
0357                     }
0358                 }
0359             }
0360 
0361             // in case we have the 'enter moves focus between fields', we need to simulate
0362             // a TAB key when the object 'o' points to the category or memo field.
0363             if (KMyMoneySettings::enterMovesBetweenFields()) {
0364                 if (o == d->m_editCategory->lineEdit() || o == d->m_editMemo || o == d->m_editTag) {
0365                     terminate = false;
0366                     QKeyEvent evt(e->type(),
0367                                   Qt::Key_Tab, k->modifiers(), QString(),
0368                                   k->isAutoRepeat(), k->count());
0369 
0370                     QApplication::sendEvent(o, &evt);
0371                 }
0372             }
0373 
0374             if (terminate) {
0375                 QTimer::singleShot(0, this, SLOT(slotEndEditKeyboard()));
0376             }
0377             break;
0378 
0379         case Qt::Key_Escape:
0380             // we cannot call the slot directly, as it destroys the caller of
0381             // this method :-(  So we let the event handler take care of calling
0382             // the respective slot using a timeout.
0383             QTimer::singleShot(0, this, SLOT(slotCancelEdit()));
0384             break;
0385 
0386         default:
0387             rc = false;
0388             break;
0389         }
0390     } else if (e->type() == QEvent::KeyRelease && !isEditMode()) {
0391         // for some reason, we only see a KeyRelease event of the Menu key
0392         // here. In other locations (e.g. Register::eventFilter()) we see
0393         // a KeyPress event. Strange. (ipwizard - 2008-05-10)
0394         switch (k->key()) {
0395         case Qt::Key_Menu:
0396             // if the very last entry is selected, the delete
0397             // operation is not available otherwise it is
0398             d->m_contextMenuDelete->setEnabled(
0399                 row < d->m_transaction.splits().count() - 1);
0400             d->m_contextMenuDuplicate->setEnabled(
0401                 row < d->m_transaction.splits().count() - 1);
0402 
0403             // prevent access to context menu in read-only mode
0404             if (!d->m_readOnly) {
0405                 d->m_contextMenu->exec(QCursor::pos());
0406             }
0407             rc = true;
0408             break;
0409         default:
0410             break;
0411         }
0412     }
0413 
0414     // if the event has not been processed here, forward it to
0415     // the base class implementation if it's not a key event
0416     if (rc == false) {
0417         if (e->type() != QEvent::KeyPress
0418                 && e->type() != QEvent::KeyRelease) {
0419             rc = QTableWidget::eventFilter(o, e);
0420         }
0421     }
0422 
0423     return rc;
0424 }
0425 
0426 void KMyMoneySplitTable::slotSetFocus(const QModelIndex& index)
0427 {
0428     slotSetFocus(index, Qt::LeftButton);
0429 }
0430 
0431 void KMyMoneySplitTable::slotSetFocus(const QModelIndex& index, int button)
0432 {
0433     Q_D(KMyMoneySplitTable);
0434     MYMONEYTRACER(tracer);
0435     auto row = index.row();
0436 
0437     // adjust row to used area
0438     if (row > d->m_transaction.splits().count() - 1)
0439         row = d->m_transaction.splits().count() - 1;
0440     if (row < 0)
0441         row = 0;
0442 
0443     // make sure the row will be on the screen
0444     scrollTo(model()->index(row, 0));
0445 
0446     if (isEditMode()) {                   // in edit mode?
0447         if (isEditSplitValid() && KMyMoneySettings::focusChangeIsEnter())
0448             endEdit(false/*keyboard driven*/, false/*set focus to next row*/);
0449         else
0450             slotCancelEdit();
0451     }
0452 
0453     if (button == Qt::LeftButton) {         // left mouse button
0454         if (row != currentRow()) {
0455             // setup new current row and update visible selection
0456             selectRow(row);
0457             slotUpdateData(d->m_transaction);
0458         }
0459     } else if (button == Qt::RightButton) {
0460         // context menu is only available when cursor is on
0461         // an existing transaction or the first line after this area
0462         if (row == index.row()) {
0463             // setup new current row and update visible selection
0464             selectRow(row);
0465             slotUpdateData(d->m_transaction);
0466 
0467             // if the very last entry is selected, the delete
0468             // operation is not available otherwise it is
0469             d->m_contextMenuDelete->setEnabled(
0470                 row < d->m_transaction.splits().count() - 1);
0471             d->m_contextMenuDuplicate->setEnabled(
0472                 row < d->m_transaction.splits().count() - 1);
0473 
0474             // prevent access to context menu in read-only mode
0475             if (!d->m_readOnly) {
0476                 d->m_contextMenu->exec(QCursor::pos());
0477             }
0478         }
0479     }
0480 }
0481 
0482 void KMyMoneySplitTable::mousePressEvent(QMouseEvent* e)
0483 {
0484     slotSetFocus(indexAt(e->pos()), e->button());
0485 }
0486 
0487 /* turn off QTable behaviour */
0488 void KMyMoneySplitTable::mouseReleaseEvent(QMouseEvent* /* e */)
0489 {
0490 }
0491 
0492 void KMyMoneySplitTable::mouseDoubleClickEvent(QMouseEvent *e)
0493 {
0494     Q_D(KMyMoneySplitTable);
0495     MYMONEYTRACER(tracer);
0496 
0497     if (d->m_readOnly) {
0498         return;
0499     }
0500 
0501     int col = columnAt(e->pos().x());
0502     slotSetFocus(model()->index(rowAt(e->pos().y()), col), e->button());
0503     createEditWidgets(false);
0504 
0505     QLineEdit* editWidget = 0;    //krazy:exclude=qmethods
0506     switch (col) {
0507     case 0:
0508         editWidget = d->m_editCategory->lineEdit();
0509         break;
0510 
0511     case 1:
0512         editWidget = d->m_editMemo;
0513         break;
0514 
0515     case 2:
0516         d->m_editTag->tagCombo()->setFocus();
0517         break;
0518 
0519     case 3:
0520         editWidget = d->m_editAmount;
0521         break;
0522 
0523     default:
0524         break;
0525     }
0526     if (editWidget) {
0527         editWidget->setFocus();
0528         editWidget->selectAll();
0529     }
0530 }
0531 
0532 void KMyMoneySplitTable::selectRow(int row)
0533 {
0534     Q_D(KMyMoneySplitTable);
0535     MYMONEYTRACER(tracer);
0536 
0537     if (row > d->m_maxRows)
0538         row = d->m_maxRows;
0539     d->m_currentRow = row;
0540     QTableWidget::selectRow(row);
0541     QList<MyMoneySplit> list = getSplits(d->m_transaction);
0542     if (row < list.count())
0543         d->m_split = list[row];
0544     else
0545         d->m_split = MyMoneySplit();
0546 }
0547 
0548 void KMyMoneySplitTable::setRowCount(int irows)
0549 {
0550     QTableWidget::setRowCount(irows);
0551 
0552     // determine row height according to the edit widgets
0553     // we use the category widget as the base
0554     QFontMetrics fm(KMyMoneySettings::listCellFontEx());
0555     int height = fm.lineSpacing() + 6;
0556 #if 0
0557     // recalculate row height hint
0558     KMyMoneyCategory cat;
0559     height = qMax(cat.sizeHint().height(), height);
0560 #endif
0561 
0562     verticalHeader()->setUpdatesEnabled(false);
0563 
0564     for (auto i = 0; i < irows; ++i)
0565         verticalHeader()->resizeSection(i, height);
0566 
0567     verticalHeader()->setUpdatesEnabled(true);
0568 }
0569 
0570 void KMyMoneySplitTable::setTransaction(const MyMoneyTransaction& t, const MyMoneySplit& s, const MyMoneyAccount& acc)
0571 {
0572     Q_D(KMyMoneySplitTable);
0573     MYMONEYTRACER(tracer);
0574     d->m_transaction = t;
0575     d->m_account = acc;
0576     d->m_hiddenSplit = s;
0577     selectRow(0);
0578     slotUpdateData(d->m_transaction);
0579 }
0580 
0581 MyMoneyTransaction KMyMoneySplitTable::transaction() const
0582 {
0583     Q_D(const KMyMoneySplitTable);
0584     return d->m_transaction;
0585 }
0586 
0587 QList<MyMoneySplit> KMyMoneySplitTable::getSplits(const MyMoneyTransaction& t) const
0588 {
0589     Q_D(const KMyMoneySplitTable);
0590     // get list of splits
0591     QList<MyMoneySplit> list = t.splits();
0592 
0593     // and ignore the one that should be hidden
0594     QList<MyMoneySplit>::Iterator it;
0595     for (it = list.begin(); it != list.end(); ++it) {
0596         if ((*it).id() == d->m_hiddenSplit.id()) {
0597             list.erase(it);
0598             break;
0599         }
0600     }
0601     return list;
0602 }
0603 
0604 void KMyMoneySplitTable::slotUpdateData(const MyMoneyTransaction& t)
0605 {
0606     Q_D(KMyMoneySplitTable);
0607     MYMONEYTRACER(tracer);
0608     unsigned long numRows = 0;
0609     QTableWidgetItem* textItem;
0610 
0611     QList<MyMoneySplit> list = getSplits(t);
0612     updateTransactionTableSize();
0613 
0614     // fill the part that is used by transactions
0615     QList<MyMoneySplit>::Iterator it;
0616     for (it = list.begin(); it != list.end(); ++it) {
0617         QString colText;
0618         MyMoneyMoney value = (*it).value();
0619         if (!(*it).accountId().isEmpty()) {
0620             try {
0621                 colText = MyMoneyFile::instance()->accountToCategory((*it).accountId());
0622             } catch (const MyMoneyException &) {
0623                 qDebug("Unexpected exception in KMyMoneySplitTable::slotUpdateData()");
0624             }
0625         }
0626         QString amountTxt = value.formatMoney(d->m_account.fraction());
0627         if (value == MyMoneyMoney::autoCalc) {
0628             amountTxt = i18n("will be calculated");
0629         }
0630 
0631         if (colText.isEmpty() && (*it).memo().isEmpty() && value.isZero())
0632             amountTxt.clear();
0633 
0634         unsigned width = fontMetrics().width(amountTxt);
0635         AmountEdit* valfield = new AmountEdit();
0636         valfield->setMinimumWidth(width);
0637         width = valfield->minimumSizeHint().width();
0638         delete valfield;
0639 
0640         textItem = item(numRows, 0);
0641         if (textItem)
0642             textItem->setText(colText);
0643         else
0644             setItem(numRows, 0, new QTableWidgetItem(colText));
0645 
0646         textItem = item(numRows, 1);
0647         if (textItem)
0648             textItem->setText((*it).memo());
0649         else
0650             setItem(numRows, 1, new QTableWidgetItem((*it).memo()));
0651 
0652         QList<QString> tl = (*it).tagIdList();
0653         QStringList tagNames;
0654         if (!tl.isEmpty()) {
0655             for (int i = 0; i < tl.size(); i++)
0656                 tagNames.append(MyMoneyFile::instance()->tag(tl[i]).name());
0657         }
0658         setItem(numRows, 2, new QTableWidgetItem(tagNames.join(", ")));
0659 
0660         textItem = item(numRows, 3);
0661         if (textItem)
0662             textItem->setText(amountTxt);
0663         else
0664             setItem(numRows, 3, new QTableWidgetItem(amountTxt));
0665 
0666         item(numRows, 3)->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
0667 
0668         ++numRows;
0669     }
0670 
0671     // now clean out the remainder of the table
0672     while (numRows < static_cast<unsigned long>(rowCount())) {
0673         for (auto i = 0 ; i < 4; ++i) {
0674             textItem = item(numRows, i);
0675             if (textItem)
0676                 textItem->setText("");
0677             else
0678                 setItem(numRows, i, new QTableWidgetItem(""));
0679         }
0680         item(numRows, 3)->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
0681         ++numRows;
0682     }
0683 }
0684 
0685 void KMyMoneySplitTable::updateTransactionTableSize()
0686 {
0687     Q_D(KMyMoneySplitTable);
0688     // get current size of transactions table
0689     int tableHeight = height();
0690     int splitCount = d->m_transaction.splits().count() - 1;
0691 
0692     if (splitCount < 0)
0693         splitCount = 0;
0694 
0695     // see if we need some extra lines to fill the current size with the grid
0696     int numExtraLines = (tableHeight / rowHeight(0)) - splitCount;
0697     if (numExtraLines < 2)
0698         numExtraLines = 2;
0699 
0700     setRowCount(splitCount + numExtraLines);
0701     d->m_maxRows = splitCount;
0702 }
0703 
0704 void KMyMoneySplitTable::resizeEvent(QResizeEvent* ev)
0705 {
0706     QTableWidget::resizeEvent(ev);
0707     if (!isEditMode()) {
0708         // update the size of the transaction table only if a split is not being edited
0709         // otherwise the height of the editors would be altered in an undesired way
0710         updateTransactionTableSize();
0711     }
0712 }
0713 
0714 void KMyMoneySplitTable::slotDuplicateSplit()
0715 {
0716     Q_D(KMyMoneySplitTable);
0717     MYMONEYTRACER(tracer);
0718     QList<MyMoneySplit> list = getSplits(d->m_transaction);
0719     if (d->m_currentRow < list.count()) {
0720         MyMoneySplit split = list[d->m_currentRow];
0721         split.clearId();
0722         try {
0723             d->m_transaction.addSplit(split);
0724             emit transactionChanged(d->m_transaction);
0725         } catch (const MyMoneyException &e) {
0726             qDebug("Cannot duplicate split: %s", e.what());
0727         }
0728     }
0729 }
0730 
0731 void KMyMoneySplitTable::slotDeleteSplit()
0732 {
0733     Q_D(KMyMoneySplitTable);
0734     MYMONEYTRACER(tracer);
0735     QList<MyMoneySplit> list = getSplits(d->m_transaction);
0736     if ((!d->m_readOnly) && (d->m_currentRow < list.count())) {
0737         if (KMessageBox::warningContinueCancel(this,
0738                                                i18n("You are about to delete the selected split. "
0739                                                        "Do you really want to continue?"),
0740                                                i18n("KMyMoney")
0741                                               ) == KMessageBox::Continue) {
0742             try {
0743                 d->m_transaction.removeSplit(list[d->m_currentRow]);
0744                 // if we removed the last split, select the previous
0745                 if (d->m_currentRow && d->m_currentRow == list.count() - 1)
0746                     selectRow(d->m_currentRow - 1);
0747                 else
0748                     selectRow(d->m_currentRow);
0749                 emit transactionChanged(d->m_transaction);
0750             } catch (const MyMoneyException &e) {
0751                 qDebug("Cannot remove split: %s", e.what());
0752             }
0753         }
0754     }
0755 }
0756 
0757 void KMyMoneySplitTable::slotStartEdit()
0758 {
0759     MYMONEYTRACER(tracer);
0760     Q_D(KMyMoneySplitTable);
0761     if (!d->m_readOnly) {
0762         createEditWidgets(true);
0763     }
0764 }
0765 
0766 void KMyMoneySplitTable::slotEndEdit()
0767 {
0768     endEdit(false);
0769 }
0770 
0771 void KMyMoneySplitTable::slotEndEditKeyboard()
0772 {
0773     endEdit(true);
0774 }
0775 
0776 void KMyMoneySplitTable::endEdit(bool keyboardDriven, bool setFocusToNextRow)
0777 {
0778     Q_D(KMyMoneySplitTable);
0779     auto file = MyMoneyFile::instance();
0780 
0781     MYMONEYTRACER(tracer);
0782     MyMoneySplit s1 = d->m_split;
0783 
0784     if (!isEditSplitValid()) {
0785         KMessageBox::information(this, i18n("You need to assign a category to this split before it can be entered."), i18n("Enter split"), "EnterSplitWithEmptyCategory");
0786         d->m_editCategory->setFocus();
0787         return;
0788     }
0789 
0790     bool needUpdate = false;
0791     if (d->m_editCategory->selectedItem() != d->m_split.accountId()) {
0792         s1.setAccountId(d->m_editCategory->selectedItem());
0793         needUpdate = true;
0794     }
0795     if (d->m_editMemo->text() != d->m_split.memo()) {
0796         s1.setMemo(d->m_editMemo->text());
0797         needUpdate = true;
0798     }
0799     if (d->m_editTag->selectedTags() != d->m_split.tagIdList()) {
0800         s1.setTagIdList(d->m_editTag->selectedTags());
0801         needUpdate = true;
0802     }
0803     if (d->m_editAmount->value() != d->m_split.value()) {
0804         s1.setValue(d->m_editAmount->value());
0805         needUpdate = true;
0806     }
0807 
0808     if (needUpdate) {
0809         if (!s1.value().isZero()) {
0810             MyMoneyAccount cat = file->account(s1.accountId());
0811             if (cat.currencyId() != d->m_transaction.commodity()) {
0812 
0813                 MyMoneySecurity fromCurrency, toCurrency;
0814                 MyMoneyMoney fromValue, toValue;
0815                 fromCurrency = file->security(d->m_transaction.commodity());
0816                 toCurrency = file->security(cat.currencyId());
0817 
0818                 // determine the fraction required for this category
0819                 int fract = toCurrency.smallestAccountFraction();
0820                 if (cat.accountType() == eMyMoney::Account::Type::Cash)
0821                     fract = toCurrency.smallestCashFraction();
0822 
0823                 // display only positive values to the user
0824                 fromValue = s1.value().abs();
0825 
0826                 // if we had a price info in the beginning, we use it here
0827                 if (d->m_priceInfo.find(cat.currencyId()) != d->m_priceInfo.end()) {
0828                     toValue = (fromValue * d->m_priceInfo[cat.currencyId()]).convert(fract);
0829                 }
0830 
0831                 // if the shares are still 0, we need to change that
0832                 if (toValue.isZero()) {
0833                     const MyMoneyPrice &price = MyMoneyFile::instance()->price(fromCurrency.id(), toCurrency.id());
0834                     // if the price is valid calculate the shares. If it is invalid
0835                     // assume a conversion rate of 1.0
0836                     if (price.isValid()) {
0837                         toValue = (price.rate(toCurrency.id()) * fromValue).convert(fract);
0838                     } else {
0839                         toValue = fromValue;
0840                     }
0841                 }
0842 
0843                 // now present all that to the user
0844                 QPointer<KCurrencyCalculator> calc =
0845                     new KCurrencyCalculator(fromCurrency,
0846                                             toCurrency,
0847                                             fromValue,
0848                                             toValue,
0849                                             d->m_transaction.postDate(),
0850                                             fract,
0851                                             this);
0852 
0853                 if (calc->exec() == QDialog::Rejected) {
0854                     delete calc;
0855                     return;
0856                 } else {
0857                     s1.setShares((s1.value() * calc->price()).convert(fract));
0858                     delete calc;
0859                 }
0860 
0861             } else {
0862                 s1.setShares(s1.value());
0863             }
0864         } else
0865             s1.setShares(s1.value());
0866 
0867         d->m_split = s1;
0868         try {
0869             if (d->m_split.id().isEmpty()) {
0870                 d->m_transaction.addSplit(d->m_split);
0871             } else {
0872                 d->m_transaction.modifySplit(d->m_split);
0873             }
0874             emit transactionChanged(d->m_transaction);
0875         } catch (const MyMoneyException &e) {
0876             qDebug("Cannot add/modify split: %s", e.what());
0877         }
0878     }
0879     this->setFocus();
0880     destroyEditWidgets();
0881     if (setFocusToNextRow) {
0882         slotSetFocus(model()->index(currentRow() + 1, 0));
0883     }
0884 
0885     // if we still have more splits, we start editing right away
0886     // in case we have selected 'enter moves between fields'
0887     if (keyboardDriven
0888             && currentRow() < d->m_transaction.splits().count() - 1
0889             && KMyMoneySettings::enterMovesBetweenFields()) {
0890         slotStartEdit();
0891     }
0892 
0893 }
0894 
0895 void KMyMoneySplitTable::slotCancelEdit()
0896 {
0897     Q_D(const KMyMoneySplitTable);
0898     MYMONEYTRACER(tracer);
0899     if (isEditMode()) {
0900         /*
0901          * Prevent asking to add a new category which happens if the user entered any text
0902          * caused by emitting signals in KMyMoneyCombo::focusOutEvent() on focus out event.
0903          * (see bug 344409)
0904          */
0905         if (d->m_editCategory)
0906             d->m_editCategory->lineEdit()->setText(QString());
0907         destroyEditWidgets();
0908         this->setFocus();
0909     }
0910 }
0911 
0912 bool KMyMoneySplitTable::isEditMode() const
0913 {
0914     Q_D(const KMyMoneySplitTable);
0915     // while the edit widgets exist we're in edit mode
0916     return d->m_editAmount || d->m_editMemo || d->m_editCategory || d->m_editTag;
0917 }
0918 
0919 bool KMyMoneySplitTable::isEditSplitValid() const
0920 {
0921     Q_D(const KMyMoneySplitTable);
0922     return isEditMode() && !(d->m_editCategory && d->m_editCategory->selectedItem().isEmpty());
0923 }
0924 
0925 void KMyMoneySplitTable::destroyEditWidgets()
0926 {
0927     MYMONEYTRACER(tracer);
0928 
0929     Q_D(KMyMoneySplitTable);
0930     emit editFinished();
0931 
0932     disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, this, &KMyMoneySplitTable::slotLoadEditWidgets);
0933 
0934     destroyEditWidget(d->m_currentRow, 0);
0935     destroyEditWidget(d->m_currentRow, 1);
0936     destroyEditWidget(d->m_currentRow, 2);
0937     destroyEditWidget(d->m_currentRow, 3);
0938     destroyEditWidget(d->m_currentRow + 1, 0);
0939 }
0940 
0941 void KMyMoneySplitTable::destroyEditWidget(int r, int c)
0942 {
0943     if (QWidget* cw = cellWidget(r, c))
0944         cw->hide();
0945     removeCellWidget(r, c);
0946 }
0947 
0948 KMyMoneyCategory* KMyMoneySplitTable::createEditWidgets(bool setFocus)
0949 {
0950     MYMONEYTRACER(tracer);
0951 
0952     emit editStarted();
0953 
0954     Q_D(KMyMoneySplitTable);
0955     auto cellFont = KMyMoneySettings::listCellFontEx();
0956     d->m_tabOrderWidgets.clear();
0957 
0958     // create the widgets
0959     d->m_editAmount = new AmountEdit;
0960     d->m_editAmount->setFont(cellFont);
0961     d->m_editAmount->setCalculatorButtonVisible(true);
0962     d->m_editAmount->setPrecision(d->m_precision);
0963 
0964     d->m_editCategory = new KMyMoneyCategory();
0965     d->m_editCategory->setPlaceholderText(i18n("Category"));
0966     d->m_editCategory->setFont(cellFont);
0967     connect(d->m_editCategory, SIGNAL(createItem(QString,QString&)), this, SIGNAL(createCategory(QString,QString&)));
0968     connect(d->m_editCategory, SIGNAL(objectCreation(bool)), this, SIGNAL(objectCreation(bool)));
0969 
0970     d->m_editMemo = new KMyMoneyLineEdit(0, false, Qt::AlignLeft | Qt::AlignVCenter);
0971     d->m_editMemo->setPlaceholderText(i18n("Memo"));
0972     d->m_editMemo->setFont(cellFont);
0973 
0974     d->m_editTag = new KTagContainer;
0975     d->m_editTag->tagCombo()->setPlaceholderText(i18n("Tag"));
0976     d->m_editTag->tagCombo()->setFont(cellFont);
0977     d->m_editTag->loadTags(MyMoneyFile::instance()->tagList());
0978     connect(d->m_editTag->tagCombo(), SIGNAL(createItem(QString,QString&)), this, SIGNAL(createTag(QString,QString&)));
0979     connect(d->m_editTag->tagCombo(), SIGNAL(objectCreation(bool)), this, SIGNAL(objectCreation(bool)));
0980 
0981     // create buttons for the mouse users
0982     d->m_registerButtonFrame = new QFrame(this);
0983     d->m_registerButtonFrame->setContentsMargins(0, 0, 0, 0);
0984     d->m_registerButtonFrame->setAutoFillBackground(true);
0985 
0986     QHBoxLayout* l = new QHBoxLayout(d->m_registerButtonFrame);
0987     l->setContentsMargins(0, 0, 0, 0);
0988     l->setSpacing(0);
0989     d->m_registerEnterButton = new QPushButton(Icons::get(Icon::DialogOK)
0990             , QString(), d->m_registerButtonFrame);
0991     d->m_registerCancelButton = new QPushButton(Icons::get(Icon::DialogCancel)
0992             , QString(), d->m_registerButtonFrame);
0993 
0994     l->addWidget(d->m_registerEnterButton);
0995     l->addWidget(d->m_registerCancelButton);
0996     l->addStretch(2);
0997 
0998     connect(d->m_registerEnterButton.data(), &QAbstractButton::clicked, this, &KMyMoneySplitTable::slotEndEdit);
0999     connect(d->m_registerCancelButton.data(), &QAbstractButton::clicked, this, &KMyMoneySplitTable::slotCancelEdit);
1000 
1001     // setup tab order
1002     addToTabOrder(d->m_editCategory);
1003     addToTabOrder(d->m_editMemo);
1004     addToTabOrder(d->m_editTag);
1005     addToTabOrder(d->m_editAmount);
1006     addToTabOrder(d->m_registerEnterButton);
1007     addToTabOrder(d->m_registerCancelButton);
1008 
1009     if (!d->m_split.accountId().isEmpty()) {
1010         d->m_editCategory->setSelectedItem(d->m_split.accountId());
1011     } else {
1012         // check if the transaction is balanced or not. If not,
1013         // assign the remainder to the amount.
1014         MyMoneyMoney diff;
1015         QList<MyMoneySplit> list = d->m_transaction.splits();
1016         QList<MyMoneySplit>::ConstIterator it_s;
1017         for (it_s = list.constBegin(); it_s != list.constEnd(); ++it_s) {
1018             if (!(*it_s).accountId().isEmpty())
1019                 diff += (*it_s).value();
1020         }
1021         d->m_split.setValue(-diff);
1022     }
1023 
1024     QList<QString> t = d->m_split.tagIdList();
1025     if (!t.isEmpty()) {
1026         for (int i = 0; i < t.size(); i++)
1027             d->m_editTag->addTagWidget(t[i]);
1028     }
1029 
1030     d->m_editMemo->loadText(d->m_split.memo());
1031     // don't allow automatically calculated values to be modified
1032     if (d->m_split.value() == MyMoneyMoney::autoCalc) {
1033         d->m_editAmount->setEnabled(false);
1034         d->m_editAmount->setText("will be calculated");
1035     } else
1036         d->m_editAmount->setValue(d->m_split.value());
1037 
1038     setCellWidget(d->m_currentRow, 0, d->m_editCategory);
1039     setCellWidget(d->m_currentRow, 1, d->m_editMemo);
1040     setCellWidget(d->m_currentRow, 2, d->m_editTag);
1041     setCellWidget(d->m_currentRow, 3, d->m_editAmount);
1042     setCellWidget(d->m_currentRow + 1, 0, d->m_registerButtonFrame);
1043 
1044     // load e.g. the category widget with the account list
1045     slotLoadEditWidgets();
1046     connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, this, &KMyMoneySplitTable::slotLoadEditWidgets);
1047 
1048     foreach (QWidget* w, d->m_tabOrderWidgets) {
1049         if (w) {
1050             w->installEventFilter(this);
1051         }
1052     }
1053 
1054     if (setFocus) {
1055         d->m_editCategory->lineEdit()->setFocus();
1056         d->m_editCategory->lineEdit()->selectAll();
1057     }
1058 
1059     // resize the rows so the added edit widgets would fit appropriately
1060     resizeRowsToContents();
1061 
1062     return d->m_editCategory;
1063 }
1064 
1065 void KMyMoneySplitTable::slotLoadEditWidgets()
1066 {
1067     Q_D(KMyMoneySplitTable);
1068     // reload category widget
1069     auto categoryId = d->m_editCategory->selectedItem();
1070 
1071     AccountSet aSet;
1072     aSet.addAccountGroup(eMyMoney::Account::Type::Asset);
1073     aSet.addAccountGroup(eMyMoney::Account::Type::Liability);
1074     aSet.addAccountGroup(eMyMoney::Account::Type::Income);
1075     aSet.addAccountGroup(eMyMoney::Account::Type::Expense);
1076     if (KMyMoneySettings::expertMode())
1077         aSet.addAccountGroup(eMyMoney::Account::Type::Equity);
1078 
1079     // remove the accounts with invalid types at this point
1080     aSet.removeAccountType(eMyMoney::Account::Type::CertificateDep);
1081     aSet.removeAccountType(eMyMoney::Account::Type::Investment);
1082     aSet.removeAccountType(eMyMoney::Account::Type::Stock);
1083     aSet.removeAccountType(eMyMoney::Account::Type::MoneyMarket);
1084 
1085     aSet.load(d->m_editCategory->selector());
1086 
1087     // if an account is specified then remove it from the widget so that the user
1088     // cannot create a transfer with from and to account being the same account
1089     if (!d->m_account.id().isEmpty())
1090         d->m_editCategory->selector()->removeItem(d->m_account.id());
1091 
1092     if (!categoryId.isEmpty())
1093         d->m_editCategory->setSelectedItem(categoryId);
1094 }
1095 
1096 void KMyMoneySplitTable::addToTabOrder(QWidget* w)
1097 {
1098     Q_D(KMyMoneySplitTable);
1099     if (w) {
1100         while (w->focusProxy())
1101             w = w->focusProxy();
1102         d->m_tabOrderWidgets.append(w);
1103     }
1104 }
1105 
1106 bool KMyMoneySplitTable::focusNextPrevChild(bool next)
1107 {
1108     MYMONEYTRACER(tracer);
1109     Q_D(KMyMoneySplitTable);
1110     auto rc = false;
1111     if (isEditMode()) {
1112         QWidget *w = 0;
1113 
1114         w = qApp->focusWidget();
1115         int currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w);
1116         while (w && currentWidgetIndex == -1) {
1117             // qDebug("'%s' not in list, use parent", w->className());
1118             w = w->parentWidget();
1119             currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w);
1120         }
1121 
1122         if (currentWidgetIndex != -1) {
1123             // if(w) qDebug("tab order is at '%s'", w->className());
1124             currentWidgetIndex += next ? 1 : -1;
1125             if (currentWidgetIndex < 0)
1126                 currentWidgetIndex = d->m_tabOrderWidgets.size() - 1;
1127             else if (currentWidgetIndex >= d->m_tabOrderWidgets.size())
1128                 currentWidgetIndex = 0;
1129 
1130             w = d->m_tabOrderWidgets[currentWidgetIndex];
1131 
1132             if (((w->focusPolicy() & Qt::TabFocus) == Qt::TabFocus) && w->isVisible() && w->isEnabled()) {
1133                 // qDebug("Selecting '%s' as focus", w->className());
1134                 w->setFocus(next ? Qt::TabFocusReason: Qt::BacktabFocusReason);
1135                 rc = true;
1136             }
1137         }
1138     } else
1139         rc = QTableWidget::focusNextPrevChild(next);
1140     return rc;
1141 }
1142 
1143 void KMyMoneySplitTable::setReadOnlyMode(bool readOnly)
1144 {
1145     Q_D(KMyMoneySplitTable);
1146     d->m_readOnly = readOnly;
1147 }