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 }