File indexing completed on 2024-05-12 05:07:46
0001 /* 0002 SPDX-FileCopyrightText: 2015-2019 Thomas Baumgart <tbaumgart@kde.org> 0003 SPDX-License-Identifier: GPL-2.0-or-later 0004 */ 0005 0006 #include "ledgerview.h" 0007 0008 // ---------------------------------------------------------------------------- 0009 // QT Includes 0010 0011 #include <QAction> 0012 #include <QApplication> 0013 #include <QDate> 0014 #include <QDebug> 0015 #include <QHeaderView> 0016 #include <QMenu> 0017 #include <QPainter> 0018 #include <QResizeEvent> 0019 #include <QScrollBar> 0020 #include <QSet> 0021 #include <QSortFilterProxyModel> 0022 #include <QStackedWidget> 0023 #include <QToolTip> 0024 #include <QWidgetAction> 0025 0026 // ---------------------------------------------------------------------------- 0027 // KDE Includes 0028 0029 #include <KDualAction> 0030 #include <KGuiItem> 0031 #include <KLocalizedString> 0032 #include <KMessageBox> 0033 #include <KMessageWidget> 0034 #include <KStandardGuiItem> 0035 0036 // ---------------------------------------------------------------------------- 0037 // Project Includes 0038 0039 #include "accountsmodel.h" 0040 #include "columnselector.h" 0041 #include "delegateproxy.h" 0042 #include "journaldelegate.h" 0043 #include "journalmodel.h" 0044 #include "kmymoneyaccountselector.h" 0045 #include "kmymoneysettings.h" 0046 #include "kmymoneyview.h" 0047 #include "kmymoneyviewbase.h" 0048 #include "ledgersortproxymodel.h" 0049 #include "ledgerviewsettings.h" 0050 #include "menuenums.h" 0051 #include "mymoneyenums.h" 0052 #include "mymoneyfile.h" 0053 #include "mymoneymoney.h" 0054 #include "mymoneysecurity.h" 0055 #include "mymoneyutils.h" 0056 #include "onlinebalancedelegate.h" 0057 #include "reconciliationdelegate.h" 0058 #include "reconciliationmodel.h" 0059 #include "schedulesjournalmodel.h" 0060 #include "securityaccountnamedelegate.h" 0061 #include "securityaccountsproxymodel.h" 0062 #include "selectedobjects.h" 0063 #include "specialdatedelegate.h" 0064 #include "specialdatesmodel.h" 0065 #include "transactioneditorbase.h" 0066 0067 struct GlobalEditData { 0068 LedgerView* detailView = nullptr; 0069 KMyMoneyViewBase* basePage = nullptr; 0070 QString accountId; 0071 QString journalEntryId; 0072 }; 0073 0074 Q_GLOBAL_STATIC(GlobalEditData, s_globalEditData); 0075 0076 class LedgerView::Private 0077 { 0078 Q_DISABLE_COPY_MOVE(Private) 0079 0080 public: 0081 Private(LedgerView* qq) 0082 : q(qq) 0083 , journalDelegate(new JournalDelegate(q)) 0084 , delegateProxy(new DelegateProxy(q)) 0085 , moveToAccountSelector(nullptr) 0086 , columnSelector(nullptr) 0087 , infoMessage(new KMessageWidget(q)) 0088 , editor(nullptr) 0089 , adjustableColumn(JournalModel::Column::Detail) 0090 , adjustingColumn(false) 0091 , showValuesInverted(false) 0092 , newTransactionPresent(false) 0093 , reselectAfterResetPending(false) 0094 { 0095 infoMessage->hide(); 0096 0097 delegateProxy->addDelegate(eMyMoney::Delegates::Types::JournalDelegate, journalDelegate); 0098 delegateProxy->addDelegate(eMyMoney::Delegates::Types::OnlineBalanceDelegate, new OnlineBalanceDelegate(q)); 0099 delegateProxy->addDelegate(eMyMoney::Delegates::Types::SpecialDateDelegate, new SpecialDateDelegate(q)); 0100 delegateProxy->addDelegate(eMyMoney::Delegates::Types::SchedulesDelegate, journalDelegate); 0101 delegateProxy->addDelegate(eMyMoney::Delegates::Types::ReconciliationDelegate, new ReconciliationDelegate(q)); 0102 delegateProxy->addDelegate(eMyMoney::Delegates::Types::SecurityAccountNameDelegate, new SecurityAccountNameDelegate(q)); 0103 0104 q->setItemDelegate(delegateProxy); 0105 } 0106 0107 void setSingleLineDetailRole(eMyMoney::Model::Roles role) 0108 { 0109 journalDelegate->setSingleLineRole(role); 0110 } 0111 0112 void ensureEditorFullyVisible(const QModelIndex& idx) 0113 { 0114 const auto viewportHeight = q->viewport()->height(); 0115 const auto verticalOffset = q->verticalHeader()->offset(); 0116 const auto verticalPosition = q->verticalHeader()->sectionPosition(idx.row()); 0117 const auto cellHeight = q->verticalHeader()->sectionSize(idx.row()); 0118 0119 // in case the idx is displayed past the viewport 0120 // adjust the position of the scroll area 0121 if (verticalPosition - verticalOffset + cellHeight > viewportHeight) { 0122 q->verticalScrollBar()->setValue(q->verticalScrollBar()->maximum()); 0123 } 0124 } 0125 0126 bool haveGlobalEditor() 0127 { 0128 return s_globalEditData()->detailView != nullptr; 0129 } 0130 0131 void registerGlobalEditor(const QModelIndex& idx) 0132 { 0133 if (!haveGlobalEditor()) { 0134 s_globalEditData()->detailView = q; 0135 s_globalEditData()->journalEntryId = idx.data(eMyMoney::Model::IdRole).toString(); 0136 s_globalEditData()->accountId = idx.data(eMyMoney::Model::JournalSplitAccountIdRole).toString(); 0137 0138 // find my base view 0139 // KPageStackedWidget; 0140 auto w = q->parentWidget(); 0141 while (w) { 0142 auto pageStack = qobject_cast<QStackedWidget*>(w); 0143 if (pageStack != nullptr) { 0144 if (qobject_cast<KMyMoneyView*>(pageStack->parentWidget())) { 0145 s_globalEditData()->basePage = qobject_cast<KMyMoneyViewBase*>(pageStack->currentWidget()); 0146 break; 0147 } 0148 } 0149 w = w->parentWidget(); 0150 } 0151 } 0152 } 0153 0154 void unregisterGlobalEditor() 0155 { 0156 s_globalEditData()->detailView = nullptr; 0157 s_globalEditData()->basePage = nullptr; 0158 s_globalEditData()->journalEntryId.clear(); 0159 s_globalEditData()->accountId.clear(); 0160 } 0161 0162 void createMoveToSubMenu() 0163 { 0164 if (!moveToAccountSelector) { 0165 auto menu = pMenus[eMenu::Menu::MoveTransaction]; 0166 if (menu) { 0167 const auto actionList = menu->actions(); 0168 // the account selector is only created the first time this is called. All following calls will 0169 // reuse the created selector. The assumption is, that the first entry is the submenu header and 0170 // the second is the account selector. Everything else is a programming mistake and causes a crash. 0171 switch (actionList.count()) { 0172 case 1: // the header is the only thing in the menu 0173 { 0174 auto accountSelectorAction = new QWidgetAction(menu); 0175 moveToAccountSelector = new KMyMoneyAccountSelector(menu, {}, false); 0176 moveToAccountSelector->setObjectName("transaction_move_menu_selector"); 0177 accountSelectorAction->setDefaultWidget(moveToAccountSelector); 0178 menu->addAction(accountSelectorAction); 0179 q->connect(moveToAccountSelector, &KMyMoneySelector::itemSelected, q, &LedgerView::slotMoveToAccount); 0180 } break; 0181 0182 case 2: // the header and the selector are newTransactionPresent 0183 { 0184 auto accountSelectorAction = qobject_cast<QWidgetAction*>(actionList.at(1)); 0185 if (accountSelectorAction) { 0186 moveToAccountSelector = qobject_cast<KMyMoneyAccountSelector*>(accountSelectorAction->defaultWidget()); 0187 } 0188 } break; 0189 0190 default: 0191 qFatal("Found misalignment with MoveTransaction menu. Please have fixed by developer."); 0192 break; 0193 } 0194 } 0195 } 0196 } 0197 0198 eMenu::Menu updateDynamicActions() 0199 { 0200 eMenu::Menu menuType = eMenu::Menu::Transaction; 0201 0202 const auto indexes = q->selectionModel()->selectedIndexes(); 0203 auto const gotoAccount = pActions[eMenu::Action::GoToAccount]; 0204 auto const gotoPayee = pActions[eMenu::Action::GoToPayee]; 0205 0206 gotoAccount->setText(i18nc("@action:inmenu open account", "Go to account")); 0207 gotoAccount->setEnabled(false); 0208 gotoPayee->setText(i18nc("@action:inmenu open payee", "Go to payee")); 0209 gotoPayee->setEnabled(false); 0210 0211 if (!indexes.isEmpty()) { 0212 const auto baseIdx = MyMoneyFile::instance()->journalModel()->mapToBaseSource(indexes.at(0)); 0213 const auto journalEntry = MyMoneyFile::instance()->journalModel()->itemByIndex(baseIdx); 0214 0215 // if this entry points to the schedules, we switch the menu type 0216 if (baseIdx.model() == MyMoneyFile::instance()->schedulesJournalModel()) { 0217 menuType = eMenu::Menu::Schedule; 0218 } 0219 0220 MyMoneyAccount acc; 0221 if (!q->isColumnHidden(JournalModel::Column::Account)) { 0222 // in case the account column is shown, we jump to that account 0223 acc = MyMoneyFile::instance()->account(journalEntry.split().accountId()); 0224 } else { 0225 // otherwise, we try to find a suitable asset/liability account 0226 for (const auto& split : journalEntry.transaction().splits()) { 0227 acc = MyMoneyFile::instance()->account(split.accountId()); 0228 if (split.id() != journalEntry.split().id()) { 0229 if (!acc.isIncomeExpense()) { 0230 // for stock accounts we show the portfolio account 0231 if (acc.isInvest()) { 0232 acc = MyMoneyFile::instance()->account(acc.parentAccountId()); 0233 } 0234 break; 0235 } 0236 } 0237 acc.clearId(); 0238 } 0239 // try looking for a suitable category in case we 0240 // did not find an account, but we don't support 0241 // jumping to categories when there are more than one 0242 if (acc.id().isEmpty() && (journalEntry.transaction().splitCount() == 2)) { 0243 const auto counterId = baseIdx.data(eMyMoney::Model::TransactionCounterAccountIdRole).toString(); 0244 acc = MyMoneyFile::instance()->account(counterId); 0245 } 0246 } 0247 0248 // found an account, update the action 0249 if (!acc.id().isEmpty()) { 0250 auto name = acc.name(); 0251 name.replace(QRegularExpression(QLatin1String("&(?!&)")), QLatin1String("&&")); 0252 gotoAccount->setEnabled(true); 0253 gotoAccount->setText(i18nc("@action:inmenu open account", "Go to '%1'", name)); 0254 gotoAccount->setData(acc.id()); 0255 } 0256 0257 if (!journalEntry.split().payeeId().isEmpty()) { 0258 auto payeeId = indexes.at(0).data(eMyMoney::Model::SplitPayeeIdRole).toString(); 0259 if (!payeeId.isEmpty()) { 0260 auto name = indexes.at(0).data(eMyMoney::Model::SplitPayeeRole).toString(); 0261 name.replace(QRegularExpression(QLatin1String("&(?!&)")), QLatin1String("&&")); 0262 gotoPayee->setEnabled(true); 0263 gotoPayee->setText(i18nc("@action:inmenu open payee", "Go to '%1'", name)); 0264 gotoPayee->setData(payeeId); 0265 } 0266 } 0267 } 0268 0269 // for a transaction context menu we need to update the 0270 // "move to account" destinations 0271 if (menuType == eMenu::Menu::Transaction) { 0272 const auto file = MyMoneyFile::instance(); 0273 createMoveToSubMenu(); 0274 0275 // in case we were not able to create the selector, we 0276 // better get out of here. Anything else would cause 0277 // a crash later on (accountSet.load) 0278 if (moveToAccountSelector) { 0279 const auto selectedAccountId = selection.firstSelection(SelectedObjects::Account); 0280 const auto accountIdx = file->accountsModel()->indexById(selectedAccountId); 0281 AccountSet accountSet; 0282 if (accountIdx.isValid()) { 0283 if (accountIdx.data(eMyMoney::Model::AccountTypeRole).value<eMyMoney::Account::Type>() == eMyMoney::Account::Type::Investment) { 0284 accountSet.addAccountType(eMyMoney::Account::Type::Investment); 0285 } else if (accountIdx.data(eMyMoney::Model::AccountIsAssetLiabilityRole).toBool()) { 0286 accountSet.addAccountType(eMyMoney::Account::Type::Checkings); 0287 accountSet.addAccountType(eMyMoney::Account::Type::Savings); 0288 accountSet.addAccountType(eMyMoney::Account::Type::Cash); 0289 accountSet.addAccountType(eMyMoney::Account::Type::AssetLoan); 0290 accountSet.addAccountType(eMyMoney::Account::Type::CertificateDep); 0291 accountSet.addAccountType(eMyMoney::Account::Type::MoneyMarket); 0292 accountSet.addAccountType(eMyMoney::Account::Type::Asset); 0293 accountSet.addAccountType(eMyMoney::Account::Type::Currency); 0294 accountSet.addAccountType(eMyMoney::Account::Type::CreditCard); 0295 accountSet.addAccountType(eMyMoney::Account::Type::Loan); 0296 accountSet.addAccountType(eMyMoney::Account::Type::Liability); 0297 } else if (accountIdx.data(eMyMoney::Model::AccountIsIncomeExpenseRole).toBool()) { 0298 accountSet.addAccountType(eMyMoney::Account::Type::Income); 0299 accountSet.addAccountType(eMyMoney::Account::Type::Expense); 0300 } 0301 } else { 0302 accountSet.addAccountType(eMyMoney::Account::Type::Checkings); 0303 accountSet.addAccountType(eMyMoney::Account::Type::Savings); 0304 accountSet.addAccountType(eMyMoney::Account::Type::Cash); 0305 accountSet.addAccountType(eMyMoney::Account::Type::AssetLoan); 0306 accountSet.addAccountType(eMyMoney::Account::Type::CertificateDep); 0307 accountSet.addAccountType(eMyMoney::Account::Type::MoneyMarket); 0308 accountSet.addAccountType(eMyMoney::Account::Type::Asset); 0309 accountSet.addAccountType(eMyMoney::Account::Type::Currency); 0310 accountSet.addAccountType(eMyMoney::Account::Type::CreditCard); 0311 accountSet.addAccountType(eMyMoney::Account::Type::Loan); 0312 accountSet.addAccountType(eMyMoney::Account::Type::Liability); 0313 } 0314 0315 accountSet.load(moveToAccountSelector); 0316 0317 // remove those accounts that we currently reference 0318 // with the selected items 0319 QSet<QString> currencyIds; 0320 for (const auto& journalId : selection.selection(SelectedObjects::JournalEntry)) { 0321 const auto journalIdx = file->journalModel()->indexById(journalId); 0322 moveToAccountSelector->removeItem(journalIdx.data(eMyMoney::Model::JournalSplitAccountIdRole).toString()); 0323 const auto accId = journalIdx.data(eMyMoney::Model::JournalSplitAccountIdRole).toString(); 0324 const auto accIdx = file->accountsModel()->indexById(accId); 0325 currencyIds.insert(accIdx.data(eMyMoney::Model::AccountCurrencyIdRole).toString()); 0326 } 0327 0328 // remove those accounts from the list that are denominated 0329 // in a different currency 0330 const auto list = moveToAccountSelector->accountList(); 0331 for (const auto& accId : list) { 0332 const auto idx = file->accountsModel()->indexById(accId); 0333 if (!currencyIds.contains(idx.data(eMyMoney::Model::AccountCurrencyIdRole).toString())) { 0334 moveToAccountSelector->removeItem(accId); 0335 } 0336 } 0337 0338 // in case we have transactions in multiple currencies selected, 0339 // the move is not supported. 0340 pMenus[eMenu::Menu::MoveTransaction]->setDisabled(currencyIds.count() > 1); 0341 } 0342 } 0343 0344 return menuType; 0345 } 0346 0347 QString createSplitTooltip(const QModelIndex& idx) 0348 { 0349 QString txt; 0350 0351 int splitCount = idx.data(eMyMoney::Model::TransactionSplitCountRole).toInt(); 0352 if ((q->currentIndex().row() != idx.row()) && (splitCount > 1)) { 0353 auto file = MyMoneyFile::instance(); 0354 const auto journalEntryId = idx.data(eMyMoney::Model::IdRole).toString(); 0355 const auto securityId = idx.data(eMyMoney::Model::TransactionCommodityRole).toString(); 0356 const auto security = file->security(securityId); 0357 MyMoneyMoney factor(MyMoneyMoney::ONE); 0358 if (!idx.data(eMyMoney::Model::Roles::SplitSharesRole).value<MyMoneyMoney>().isNegative()) 0359 factor = -factor; 0360 0361 const auto indexes = file->journalModel()->indexesByTransactionId(idx.data(eMyMoney::Model::JournalTransactionIdRole).toString()); 0362 if (!indexes.isEmpty()) { 0363 txt = QLatin1String("<table style='white-space:pre'>"); 0364 for (const auto& tidx : indexes) { 0365 if (tidx.data(eMyMoney::Model::IdRole).toString() == journalEntryId) 0366 continue; 0367 const auto acc = file->accountsModel()->itemById(tidx.data(eMyMoney::Model::SplitAccountIdRole).toString()); 0368 const auto category = file->accountToCategory(acc.id()); 0369 const auto amount = 0370 MyMoneyUtils::formatMoney((tidx.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>() * factor), acc, security, true); 0371 0372 txt += QString("<tr><td>%1</td><td align=right>%2</td></tr>").arg(category, amount); 0373 } 0374 if (splitCount > 2) { 0375 txt += QStringLiteral("<tr><td></td><td><hr/></td></tr>"); 0376 0377 const auto acc = file->accountsModel()->itemById(idx.data(eMyMoney::Model::SplitAccountIdRole).toString()); 0378 const auto amount = 0379 MyMoneyUtils::formatMoney((idx.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>() * (-factor)), acc, security, true); 0380 txt += QString("<tr><td></td><td align=right>%2</td></tr>").arg(amount); 0381 } 0382 txt += QLatin1String("</table>"); 0383 } 0384 } 0385 return txt; 0386 } 0387 0388 QVector<eMyMoney::Model::Roles> statusRoles(const QModelIndex& idx) const 0389 { 0390 QVector<eMyMoney::Model::Roles> status; 0391 if (idx.data(eMyMoney::Model::TransactionErroneousRole).toBool()) { 0392 status.append(eMyMoney::Model::TransactionErroneousRole); 0393 } else if (idx.data(eMyMoney::Model::ScheduleIsOverdueRole).toBool()) { 0394 status.append(eMyMoney::Model::ScheduleIsOverdueRole); 0395 } 0396 0397 // draw the import icon 0398 if (idx.data(eMyMoney::Model::TransactionIsImportedRole).toBool()) { 0399 status.append(eMyMoney::Model::TransactionIsImportedRole); 0400 } 0401 0402 // draw the matched icon 0403 if (idx.data(eMyMoney::Model::JournalSplitIsMatchedRole).toBool()) { 0404 status.append(eMyMoney::Model::JournalSplitIsMatchedRole); 0405 } 0406 return status; 0407 } 0408 0409 int iconClickIndex(const QModelIndex& idx, const QPoint& pos) 0410 { 0411 const auto font = idx.data(Qt::FontRole).value<QFont>(); 0412 const auto metrics = QFontMetrics(font); 0413 const auto iconWidth = (metrics.lineSpacing() + 2) + (2 * q->style()->pixelMetric(QStyle::PM_FocusFrameHMargin)); 0414 const auto cellRect = q->visualRect(idx); 0415 auto iconRect = QRect(cellRect.x() + cellRect.width() - iconWidth, cellRect.y(), iconWidth, iconWidth); 0416 auto iconIndex = -1; 0417 for (int i = 0; i < JournalDelegate::maxIcons(); ++i) { 0418 if (iconRect.contains(pos)) { 0419 iconIndex = i; 0420 break; 0421 } 0422 iconRect.moveLeft(iconRect.left() - iconWidth); 0423 } 0424 return iconIndex; 0425 } 0426 0427 void setFonts() 0428 { 0429 q->horizontalHeader()->setMinimumSectionSize(20); 0430 0431 QFont font = KMyMoneySettings::listHeaderFontEx(); 0432 QFontMetrics fm(font); 0433 int height = fm.lineSpacing() + 6; 0434 q->horizontalHeader()->setMinimumHeight(height); 0435 q->horizontalHeader()->setMaximumHeight(height); 0436 q->horizontalHeader()->setFont(font); 0437 0438 // setup cell font 0439 font = KMyMoneySettings::listCellFontEx(); 0440 q->setFont(font); 0441 0442 journalDelegate->resetLineHeight(); 0443 } 0444 0445 void allowSectionResize() 0446 { 0447 auto header = q->horizontalHeader(); 0448 for (int i = 0; i < header->count(); ++i) { 0449 header->setSectionResizeMode(i, QHeaderView::Interactive); 0450 } 0451 header->setSectionResizeMode(JournalModel::Column::Reconciliation, QHeaderView::ResizeToContents); 0452 } 0453 0454 void blockSectionResize() 0455 { 0456 auto header = q->horizontalHeader(); 0457 for (int i = 0; i < header->count(); ++i) { 0458 header->setSectionResizeMode(i, QHeaderView::Fixed); 0459 } 0460 } 0461 0462 void resetMaxLineCache() 0463 { 0464 auto m = q->LedgerView::model(); 0465 m->setData(QModelIndex(), -1, eMyMoney::Model::JournalSplitMaxLinesCountRole); 0466 } 0467 0468 LedgerView* q; 0469 JournalDelegate* journalDelegate; 0470 DelegateProxy* delegateProxy; 0471 KMyMoneyAccountSelector* moveToAccountSelector; 0472 ColumnSelector* columnSelector; 0473 KMessageWidget* infoMessage; 0474 TransactionEditorBase* editor; 0475 QHash<const QAbstractItemModel*, QStyledItemDelegate*> delegates; 0476 int adjustableColumn; 0477 bool adjustingColumn; 0478 bool showValuesInverted; 0479 bool newTransactionPresent; 0480 bool reselectAfterResetPending; 0481 QString accountId; 0482 QString groupName; 0483 QPersistentModelIndex editIndex; 0484 SelectedObjects selection; 0485 QString firstSelectedId; 0486 LedgerSortOrder sortOrder; 0487 QStringList selectionBeforeReset; 0488 QString currentBeforeReset; 0489 }; 0490 0491 0492 0493 LedgerView::LedgerView(QWidget* parent) 0494 : QTableView(parent) 0495 , d(new Private(this)) 0496 { 0497 // keep rows as small as possible 0498 verticalHeader()->setMinimumSectionSize(1); 0499 verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); 0500 verticalHeader()->hide(); 0501 setSortingEnabled(false); 0502 0503 d->setFonts(); 0504 0505 // since we don't have a vertical header, it does not make sense 0506 // to use the first column to select all items in the view 0507 setCornerButtonEnabled(false); 0508 0509 // make sure to get informed about resize operations on the columns 0510 // but delay the execution of adjustDetailColumn() until we return 0511 // to the main event loop. Also emit information about the change 0512 // so that other views in the same configuration groupcan sync up. 0513 // See LedgerView::resizeSection(). 0514 connect(horizontalHeader(), &QHeaderView::sectionResized, this, [&](int logicalIndex, int oldSize, int newSize) { 0515 Q_EMIT sectionResized(this, d->columnSelector->configGroupName(), logicalIndex, oldSize, newSize); 0516 QMetaObject::invokeMethod(this, "adjustDetailColumn", Qt::QueuedConnection, Q_ARG(int, viewport()->width()), Q_ARG(bool, false)); 0517 }); 0518 0519 connect(horizontalHeader(), &QHeaderView::sectionMoved, this, [&](int logicalIndex, int oldIndex, int newIndex) { 0520 Q_EMIT sectionMoved(this, logicalIndex, oldIndex, newIndex); 0521 }); 0522 0523 // get notifications about setting changes 0524 connect(LedgerViewSettings::instance(), &LedgerViewSettings::settingsChanged, this, &LedgerView::slotSettingsChanged); 0525 0526 // we don't need autoscroll as we do not support drag/drop 0527 setAutoScroll(false); 0528 0529 setAlternatingRowColors(true); 0530 0531 setSelectionBehavior(SelectRows); 0532 0533 // setup context menu 0534 setContextMenuPolicy(Qt::CustomContextMenu); 0535 connect(this, &QWidget::customContextMenuRequested, this, [&](QPoint pos) { 0536 const auto col = columnAt(pos.x()); 0537 const auto row = rowAt(pos.y()); 0538 const auto idx = model()->index(row, col); 0539 if (idx.flags() & Qt::ItemIsSelectable) { 0540 const auto menuType = d->updateDynamicActions(); 0541 Q_EMIT requestCustomContextMenu(menuType, viewport()->mapToGlobal(pos)); 0542 } 0543 }); 0544 0545 connect(d->infoMessage, &KMessageWidget::linkActivated, this, [&](const QString& href) { 0546 Q_UNUSED(href) 0547 d->infoMessage->animatedHide(); 0548 Q_EMIT requestView(s_globalEditData()->basePage, s_globalEditData()->accountId, s_globalEditData()->journalEntryId); 0549 }); 0550 0551 connect(this, &LedgerView::doubleClicked, this, [&](const QModelIndex& index) { 0552 // double click on a schedule causes the schedule editor to be opened 0553 if (MyMoneyModelBase::baseModel(index) == MyMoneyFile::instance()->schedulesJournalModel()) { 0554 pActions[eMenu::Action::EditSchedule]->trigger(); 0555 } 0556 }); 0557 connect(horizontalHeader(), &QHeaderView::sectionClicked, this, [&]() { 0558 Q_EMIT modifySortOrder(); 0559 }); 0560 setTabKeyNavigation(false); 0561 } 0562 0563 LedgerView::~LedgerView() 0564 { 0565 delete d; 0566 } 0567 0568 void LedgerView::setColumnSelectorGroupName(const QString& groupName) 0569 { 0570 if (!d->columnSelector) { 0571 d->groupName = groupName; 0572 } else { 0573 qWarning() << "LedgerView::setColumnSelectorGroupName must be called before model assignment"; 0574 } 0575 } 0576 0577 void LedgerView::setShowPayeeInDetailColumn(bool show) 0578 { 0579 d->journalDelegate->setShowPayeeInDetailColumn(show); 0580 } 0581 0582 void LedgerView::setModel(QAbstractItemModel* model) 0583 { 0584 if (!d->columnSelector) { 0585 d->columnSelector = new ColumnSelector(this, d->groupName); 0586 connect(d->columnSelector, &ColumnSelector::columnsChanged, MyMoneyFile::instance()->journalModel(), &JournalModel::resetRowHeightInformation); 0587 } 0588 QSignalBlocker blocker(this); 0589 QTableView::setModel(model); 0590 0591 d->columnSelector->setModel(model); 0592 0593 d->allowSectionResize(); 0594 0595 horizontalHeader()->setSectionsMovable(true); 0596 0597 connect(model, &QAbstractItemModel::modelAboutToBeReset, this, [&]() { 0598 // keep the current selected ids as the indeces might change 0599 d->selectionBeforeReset = selectedJournalEntryIds(); 0600 d->currentBeforeReset = currentIndex().data(eMyMoney::Model::IdRole).toString(); 0601 0602 // turn off updates of the view to reduce flicker 0603 viewport()->setUpdatesEnabled(false); 0604 }); 0605 0606 connect(model, &QAbstractItemModel::modelReset, this, [&]() { 0607 d->resetMaxLineCache(); 0608 }); 0609 0610 horizontalHeader()->setSortIndicatorShown(false); 0611 horizontalHeader()->setSortIndicator(-1, Qt::AscendingOrder); 0612 0613 horizontalHeader()->setSectionsClickable(true); 0614 } 0615 0616 void LedgerView::reset() 0617 { 0618 QTableView::reset(); 0619 if (d->editor) { 0620 closeEditor(d->editor, QAbstractItemDelegate::NoHint); 0621 d->editor->deleteLater(); 0622 } 0623 0624 // make sure to kick-off re-selection only once 0625 if (!d->reselectAfterResetPending) { 0626 d->reselectAfterResetPending = true; 0627 QMetaObject::invokeMethod(this, &LedgerView::reselectAfterModelReset, Qt::QueuedConnection); 0628 } 0629 } 0630 0631 void LedgerView::reselectAfterModelReset() 0632 { 0633 // make sure the current index is the first in the list 0634 auto objectIds = d->selectionBeforeReset; 0635 if (!d->currentBeforeReset.isEmpty()) { 0636 objectIds.prepend(d->currentBeforeReset); 0637 } 0638 objectIds.removeDuplicates(); 0639 setSelectedJournalEntries(objectIds); 0640 0641 d->reselectAfterResetPending = false; 0642 // turn updates back on 0643 viewport()->setUpdatesEnabled(true); 0644 } 0645 0646 void LedgerView::setAccountId(const QString& id) 0647 { 0648 d->accountId = id; 0649 d->selection.setSelection(SelectedObjects::Account, id); 0650 } 0651 0652 const QString& LedgerView::accountId() const 0653 { 0654 return d->accountId; 0655 } 0656 0657 bool LedgerView::showValuesInverted() const 0658 { 0659 return d->showValuesInverted; 0660 } 0661 0662 void LedgerView::setColumnsHidden(QVector<int> columns) 0663 { 0664 d->columnSelector->setAlwaysHidden(columns); 0665 } 0666 0667 void LedgerView::setColumnsShown(QVector<int> columns) 0668 { 0669 d->columnSelector->setAlwaysVisible(columns); 0670 } 0671 0672 bool LedgerView::edit(const QModelIndex& index, QAbstractItemView::EditTrigger trigger, QEvent* event) 0673 { 0674 bool suppressDuplicateEditorStart = false; 0675 0676 switch(trigger) { 0677 case QAbstractItemView::DoubleClicked: 0678 case QAbstractItemView::EditKeyPressed: 0679 suppressDuplicateEditorStart = true; 0680 break; 0681 default: 0682 break; 0683 } 0684 0685 if(d->haveGlobalEditor() && suppressDuplicateEditorStart) { 0686 if (!d->infoMessage->isVisible() && !d->infoMessage->isShowAnimationRunning()) { 0687 d->infoMessage->resize(viewport()->width(), d->infoMessage->height()); 0688 d->infoMessage->setText( 0689 i18n("You are already editing a transaction in another view. KMyMoney does not support editing two transactions in parallel. <a " 0690 "href=\"jumpToEditor\">Jump to current editor</a>")); 0691 d->infoMessage->setMessageType(KMessageWidget::Warning); 0692 d->infoMessage->animatedShow(); 0693 } 0694 0695 } else { 0696 bool rc = QTableView::edit(index, trigger, event); 0697 0698 if(rc) { 0699 // editing started, but we need the editor to cover all columns 0700 // so we close it, set the span to have a single row and recreate 0701 // the editor in that single cell in case an editor was created at all. 0702 // 0703 // we double check the presence of the editor, because the delegate 0704 // may not have created one but QTableView::edit() nevertheless 0705 // returns true in case it was called with trigger set to 0706 // QAbstractItemView::AllEditTriggers. 0707 d->editor = qobject_cast<TransactionEditorBase*>(indexWidget(index)); 0708 closeEditor(indexWidget(index), QAbstractItemDelegate::NoHint); 0709 0710 // in case the editor was created, we continue to start it. if not 0711 // the journal delegate took care of stopping editing again by 0712 // sending signals 0713 if (d->editor) { 0714 d->registerGlobalEditor(index); 0715 d->infoMessage->animatedHide(); 0716 0717 Q_EMIT aboutToStartEdit(); 0718 setSpan(index.row(), 0, 1, horizontalHeader()->count()); 0719 d->editIndex = model()->index(index.row(), 0); 0720 0721 rc = QTableView::edit(d->editIndex, trigger, event); 0722 0723 // make sure that the row gets resized according to the requirements of the editor 0724 // and is completely visible 0725 d->editor = qobject_cast<TransactionEditorBase*>(indexWidget(d->editIndex)); 0726 connect(d->editor, &TransactionEditorBase::editorLayoutChanged, this, &LedgerView::resizeEditorRow); 0727 connect(this, &LedgerView::settingsChanged, d->editor, &TransactionEditorBase::slotSettingsChanged); 0728 0729 // make sure to unregister the editor in case it is destroyed 0730 connect(d->editor, &TransactionEditorBase::destroyed, this, [&]() { 0731 d->unregisterGlobalEditor(); 0732 d->editor = nullptr; 0733 }); 0734 0735 resizeEditorRow(); 0736 0737 d->blockSectionResize(); 0738 d->columnSelector->setColumnSelectionDisabled(); 0739 } 0740 } 0741 return rc; 0742 } 0743 return false; 0744 } 0745 0746 void LedgerView::showEditor() 0747 { 0748 if (d->haveGlobalEditor()) { 0749 d->ensureEditorFullyVisible(d->editIndex); 0750 QMetaObject::invokeMethod(this, "ensureCurrentItemIsVisible", Qt::QueuedConnection); 0751 } 0752 } 0753 0754 void LedgerView::resizeEditorRow() 0755 { 0756 resizeRowToContents(d->editIndex.row()); 0757 showEditor(); 0758 } 0759 0760 void LedgerView::closeEditor(QWidget* editor, QAbstractItemDelegate::EndEditHint hint) 0761 { 0762 QTableView::closeEditor(editor, hint); 0763 clearSpans(); 0764 0765 d->unregisterGlobalEditor(); 0766 0767 // we need to resize the row that contained the editor. 0768 resizeRowsToContents(); 0769 0770 // and allow the section sizes to be modified again 0771 d->allowSectionResize(); 0772 d->columnSelector->setColumnSelectionEnabled(); 0773 0774 Q_EMIT aboutToFinishEdit(); 0775 0776 d->editIndex = QModelIndex(); 0777 QMetaObject::invokeMethod(this, &LedgerView::ensureCurrentItemIsVisible, Qt::QueuedConnection); 0778 } 0779 0780 QModelIndex LedgerView::editIndex() const 0781 { 0782 return d->editIndex; 0783 } 0784 0785 bool LedgerView::viewportEvent(QEvent* event) 0786 { 0787 if (event->type() == QEvent::ToolTip) { 0788 auto helpEvent = static_cast<QHelpEvent*>(event); 0789 0790 // get the row, if it's the header, then we're done 0791 // otherwise, adjust the row to be 0 based. 0792 const auto col = columnAt(helpEvent->x()); 0793 const auto row = rowAt(helpEvent->y()); 0794 const auto idx = model()->index(row, col); 0795 0796 if (col == JournalModel::Column::Detail) { 0797 bool preventLineBreak(false); 0798 int iconIndex = d->iconClickIndex(idx, helpEvent->pos()); 0799 0800 QVector<QString> tooltips(JournalDelegate::maxIcons()); 0801 if (iconIndex != -1) { 0802 int iconCount(0); 0803 if (idx.data(eMyMoney::Model::TransactionErroneousRole).toBool()) { 0804 if (idx.data(eMyMoney::Model::Roles::TransactionSplitCountRole).toInt() < 2) { 0805 tooltips[iconCount] = i18nc("@info:tooltip icon description", "Transaction is missing a category assignment."); 0806 } else { 0807 const auto acc = MyMoneyFile::instance()->account(d->accountId); 0808 const auto sec = MyMoneyFile::instance()->security(acc.currencyId()); 0809 // don't allow line break between amount and currency symbol 0810 tooltips[iconCount] = 0811 i18nc("@info:tooltip icon description", 0812 "The transaction has a missing assignment of <b>%1</b>.", 0813 MyMoneyUtils::formatMoney(idx.data(eMyMoney::Model::TransactionSplitSumRole).value<MyMoneyMoney>().abs(), acc, sec)); 0814 } 0815 preventLineBreak = true; 0816 ++iconCount; 0817 0818 } else if (idx.data(eMyMoney::Model::ScheduleIsOverdueRole).toBool()) { 0819 const auto overdueSince = MyMoneyUtils::formatDate(idx.data(eMyMoney::Model::ScheduleIsOverdueSinceRole).toDate()); 0820 tooltips[iconCount] = 0821 i18nc("@info:tooltip icon description, param is date", "This schedule is overdue since %1. Click on the icon to enter it.") 0822 .arg(overdueSince); 0823 ++iconCount; 0824 } 0825 0826 if (idx.data(eMyMoney::Model::TransactionIsImportedRole).toBool()) { 0827 tooltips[iconCount] = i18nc("@info:tooltip icon description", "This transaction is imported. Click on the icon to accept it."); 0828 ++iconCount; 0829 } 0830 0831 if (idx.data(eMyMoney::Model::JournalSplitIsMatchedRole).toBool()) { 0832 tooltips[iconCount] = i18nc("@info:tooltip icon description", "This transaction is matched. Click on the icon to accept or un-match it."); 0833 ++iconCount; 0834 } 0835 0836 } else if (!LedgerViewSettings::instance()->showAllSplits()) { 0837 tooltips[0] = d->createSplitTooltip(idx); 0838 iconIndex = 0; 0839 } 0840 0841 if ((iconIndex != -1) && !tooltips[iconIndex].isEmpty()) { 0842 auto text = tooltips[iconIndex]; 0843 if (preventLineBreak) { 0844 text = QString("<p style='white-space:pre'>%1</p>").arg(text); 0845 } 0846 QToolTip::showText(helpEvent->globalPos(), tooltips[iconIndex]); 0847 return true; 0848 } 0849 0850 } else if ((col == JournalModel::Column::Payment) || (col == JournalModel::Column::Deposit)) { 0851 if (!LedgerViewSettings::instance()->showAllSplits()) { 0852 if (!idx.data(Qt::DisplayRole).toString().isEmpty()) { 0853 auto tip = d->createSplitTooltip(idx); 0854 if (!tip.isEmpty()) { 0855 QToolTip::showText(helpEvent->globalPos(), tip); 0856 return true; 0857 } 0858 } 0859 } 0860 } 0861 0862 QToolTip::hideText(); 0863 event->ignore(); 0864 return true; 0865 } 0866 return QTableView::viewportEvent(event); 0867 } 0868 0869 void LedgerView::mousePressEvent(QMouseEvent* event) 0870 { 0871 if (state() != QAbstractItemView::EditingState) { 0872 if (event->button() != Qt::LeftButton) { 0873 QTableView::mousePressEvent(event); 0874 0875 } else { 0876 const auto pos = event->pos(); 0877 const auto column = columnAt(pos.x()); 0878 // call base class (which modifies the selection) in case the reconciliation 0879 // column was not clicked or the current index is not selected. This will 0880 // make sure that if multiple transactions are selected and the reconciliation 0881 // column is clicked that the selection will not change. 0882 if (column != JournalModel::Column::Reconciliation || !selectionModel()->isSelected(indexAt(pos))) { 0883 QTableView::mousePressEvent(event); 0884 } 0885 0886 switch (column) { 0887 case JournalModel::Column::Reconciliation: 0888 // a click on the reconciliation column triggers the Mark transaction action 0889 pActions[eMenu::Action::ToggleReconciliationFlag]->trigger(); 0890 break; 0891 0892 case JournalModel::Column::Detail: { 0893 // check if an icon was clicked in the detail column 0894 const auto col = columnAt(event->x()); 0895 const auto row = rowAt(event->y()); 0896 const auto idx = model()->index(row, col); 0897 const auto iconIndex = d->iconClickIndex(idx, pos); 0898 const auto statusRoles = this->statusRoles(idx); 0899 0900 KGuiItem buttonYes = KMMYesNo::yes(); 0901 KGuiItem buttonNo = KMMYesNo::no(); 0902 KGuiItem buttonCancel = KStandardGuiItem::cancel(); 0903 KMessageBox::ButtonCode result; 0904 0905 if (iconIndex != -1 && (iconIndex < statusRoles.count())) { 0906 switch (statusRoles[iconIndex]) { 0907 case eMyMoney::Model::ScheduleIsOverdueRole: 0908 buttonNo.setToolTip(i18nc("@info:tooltip No button", "Do not enter the overdue scheduled transaction.")); 0909 buttonYes.setToolTip(i18nc("@info:tooltip Yes button", "Enter the overdue scheduled transaction.")); 0910 0911 result = KMessageBox::questionTwoActions(this, 0912 i18nc("Question about the overdue action", "Do you want to enter the overdue schedule now?"), 0913 i18nc("@title:window", "Enter overdue schedule"), 0914 buttonYes, 0915 buttonNo); 0916 if (result == KMessageBox::ButtonCode::PrimaryAction) { 0917 pActions[eMenu::Action::EnterSchedule]->setData(idx.data(eMyMoney::Model::JournalTransactionIdRole).toString()); 0918 pActions[eMenu::Action::EnterSchedule]->trigger(); 0919 } 0920 break; 0921 case eMyMoney::Model::TransactionIsImportedRole: 0922 pActions[eMenu::Action::AcceptTransaction]->trigger(); 0923 break; 0924 case eMyMoney::Model::JournalSplitIsMatchedRole: { 0925 buttonYes.setText(pActions[eMenu::Action::AcceptTransaction]->text()); 0926 buttonYes.setIcon(pActions[eMenu::Action::AcceptTransaction]->icon()); 0927 const auto unmatchAction = qobject_cast<KDualAction*>(pActions[eMenu::Action::MatchTransaction]); 0928 if (unmatchAction) { 0929 unmatchAction->setActive(false); 0930 buttonNo.setText(pActions[eMenu::Action::MatchTransaction]->text()); 0931 buttonNo.setIcon(pActions[eMenu::Action::MatchTransaction]->icon()); 0932 buttonNo.setToolTip(i18nc("@info:tooltip Unmatch button", 0933 "Detach the hidden (matched) transaction from the one shown and enter it into the ledger again.")); 0934 buttonYes.setToolTip( 0935 i18nc("@info:tooltip Accept button", "Accept the match as shown and remove the data of the hidden (matched) transaction.")); 0936 0937 result = KMessageBox::questionTwoActionsCancel( 0938 this, 0939 i18nc("Question about the accept or unmatch action", "Do you want to accept or unmatch the matched transaction now?"), 0940 i18nc("@title:window", "Accept or unmatch transaction"), 0941 buttonYes, 0942 buttonNo, 0943 buttonCancel); 0944 switch (result) { 0945 case KMessageBox::ButtonCode::PrimaryAction: 0946 pActions[eMenu::Action::AcceptTransaction]->trigger(); 0947 break; 0948 case KMessageBox::ButtonCode::SecondaryAction: 0949 pActions[eMenu::Action::MatchTransaction]->trigger(); 0950 break; 0951 default: 0952 break; 0953 } 0954 } 0955 break; 0956 } 0957 default: 0958 break; 0959 } 0960 } 0961 break; 0962 } 0963 default: 0964 break; 0965 } 0966 } 0967 } 0968 } 0969 0970 void LedgerView::mouseMoveEvent(QMouseEvent* event) 0971 { 0972 Q_UNUSED(event); 0973 // qDebug() << "mouseMoveEvent"; 0974 // QTableView::mouseMoveEvent(event); 0975 } 0976 0977 void LedgerView::mouseDoubleClickEvent(QMouseEvent* event) 0978 { 0979 // qDebug() << "mouseDoubleClickEvent"; 0980 QTableView::mouseDoubleClickEvent(event); 0981 } 0982 0983 void LedgerView::wheelEvent(QWheelEvent* e) 0984 { 0985 // qDebug() << "wheelEvent"; 0986 QTableView::wheelEvent(e); 0987 } 0988 0989 void LedgerView::keyPressEvent(QKeyEvent* kev) 0990 { 0991 if ((d->infoMessage->isVisible()) && kev->matches(QKeySequence::Cancel)) { 0992 kev->accept(); 0993 d->infoMessage->animatedHide(); 0994 } else { 0995 #ifndef Q_OS_OSX 0996 // on non OSX operating systems, we turn a return or enter 0997 // key press into an F2 to start editing the transaction. 0998 // This is otherwise suppressed. Comment from QAbstractItemView: 0999 // 1000 // ### we can't open the editor on enter, becuse 1001 // some widgets will forward the enter event back 1002 // to the viewport, starting an endless loop 1003 1004 QKeyEvent evt(kev->type(), Qt::Key_F2, kev->modifiers(), QString(), kev->isAutoRepeat(), kev->count()); 1005 switch (kev->key()) { 1006 case Qt::Key_Return: 1007 case Qt::Key_Enter: 1008 // send out the modified key event 1009 // and don't process this one any further 1010 QApplication::sendEvent(this, &evt); 1011 return; 1012 default: 1013 break; 1014 } 1015 #endif 1016 QTableView::keyPressEvent(kev); 1017 } 1018 } 1019 1020 QModelIndex LedgerView::moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers) 1021 { 1022 QModelIndex newIndex; 1023 bool skipSchedules(false); 1024 1025 if (!(modifiers & Qt::ControlModifier)) { 1026 // for home and end we need to have the ControlModifier set so 1027 // that the base class implementation works on rows instead of 1028 // columns. 1029 switch (cursorAction) { 1030 case MoveHome: 1031 case MoveEnd: 1032 newIndex = QTableView::moveCursor(cursorAction, modifiers | Qt::ControlModifier); 1033 skipSchedules = true; 1034 break; 1035 1036 default: 1037 newIndex = QTableView::moveCursor(cursorAction, modifiers); 1038 break; 1039 } 1040 } 1041 1042 // now make sure that moving the cursor does not hit the empty 1043 // transaction at the bottom or a schedule. 1044 for (auto row = newIndex.row(); row >= 0; --row) { 1045 newIndex = model()->index(row, newIndex.column(), newIndex.parent()); 1046 QString id = newIndex.data(eMyMoney::Model::IdRole).toString(); 1047 // skip the empty transaction at the end of a ledger if 1048 // the movement is not the down arrow 1049 if ((id.isEmpty() || id.endsWith('-')) && (cursorAction != MoveDown)) { 1050 continue; 1051 } 1052 // skip scheduled transactions as well if moving to the end 1053 if (skipSchedules && newIndex.data(eMyMoney::Model::TransactionScheduleRole).toBool()) { 1054 continue; 1055 } 1056 if ((newIndex.flags() & (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) == (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) { 1057 return newIndex; 1058 } 1059 } 1060 return {}; 1061 } 1062 1063 void LedgerView::currentChanged(const QModelIndex& current, const QModelIndex& previous) 1064 { 1065 QTableView::currentChanged(current, previous); 1066 1067 if (current.isValid() && current.row() != previous.row()) { 1068 QModelIndex idx = current.model()->index(current.row(), 0); 1069 QString id = idx.data(eMyMoney::Model::IdRole).toString(); 1070 // For a new transaction the id is completely empty, for a split view the transaction 1071 // part is filled but the split id is empty and the string ends with a dash 1072 if (id.isEmpty() || id.endsWith('-')) { 1073 // the next two lines prevent an endless recursive call of this method 1074 if (idx == previous) { 1075 return; 1076 } 1077 // check for an empty account being opened. we can detect 1078 // that by an invalid previous index and don't start 1079 // editing right away. 1080 if (!previous.isValid()) { 1081 selectRow(idx.row()); 1082 return; 1083 } 1084 selectionModel()->clearSelection(); 1085 setCurrentIndex(idx); 1086 selectRow(idx.row()); 1087 scrollTo(idx, QAbstractItemView::PositionAtBottom); 1088 edit(idx); 1089 } else { 1090 Q_EMIT transactionSelected(idx); 1091 QMetaObject::invokeMethod(this, &LedgerView::ensureCurrentItemIsVisible, Qt::QueuedConnection); 1092 } 1093 QMetaObject::invokeMethod(this, "doItemsLayout", Qt::QueuedConnection); 1094 } 1095 } 1096 1097 void LedgerView::moveEvent(QMoveEvent* event) 1098 { 1099 // qDebug() << "moveEvent"; 1100 QWidget::moveEvent(event); 1101 } 1102 1103 void LedgerView::paintEvent(QPaintEvent* event) 1104 { 1105 QTableView::paintEvent(event); 1106 1107 // the base class implementation paints the regular grid in case there 1108 // is room below the last line and the bottom of the viewport. We check 1109 // here if that is the case and fill that part with the base color to 1110 // remove the false painted grid. 1111 1112 const QHeaderView *verticalHeader = this->verticalHeader(); 1113 if(verticalHeader->count() == 0) 1114 return; 1115 1116 int lastVisualRow = verticalHeader->visualIndexAt(verticalHeader->viewport()->height()); 1117 if (lastVisualRow == -1) 1118 lastVisualRow = model()->rowCount(QModelIndex()) - 1; 1119 1120 while(lastVisualRow >= model()->rowCount(QModelIndex())) 1121 --lastVisualRow; 1122 1123 while ((lastVisualRow > -1) && verticalHeader->isSectionHidden(verticalHeader->logicalIndex(lastVisualRow))) 1124 --lastVisualRow; 1125 1126 int top = 0; 1127 if(lastVisualRow != -1) 1128 top = verticalHeader->sectionViewportPosition(lastVisualRow) + verticalHeader->sectionSize(lastVisualRow); 1129 1130 if(top < viewport()->height()) { 1131 QPainter painter(viewport()); 1132 QRect rect(0, top, viewport()->width(), viewport()->height()-top); 1133 painter.fillRect(rect, QBrush(palette().base())); 1134 } 1135 } 1136 1137 QVector<eMyMoney::Model::Roles> LedgerView::statusRoles(const QModelIndex& idx) const 1138 { 1139 return d->statusRoles(idx); 1140 } 1141 1142 void LedgerView::setSingleLineDetailRole(eMyMoney::Model::Roles role) 1143 { 1144 d->setSingleLineDetailRole(role); 1145 } 1146 1147 int LedgerView::sizeHintForColumn(int col) const 1148 { 1149 if (col == JournalModel::Column::Reconciliation) { 1150 QStyleOptionViewItem opt; 1151 opt.font = font(); 1152 opt.fontMetrics = fontMetrics(); 1153 const QModelIndex index = model()->index(0, col); 1154 const auto delegate = d->delegateProxy->delegate(index); 1155 if (delegate) { 1156 int hint = delegate->sizeHint(opt, index).width(); 1157 if(showGrid()) 1158 hint += 1; 1159 return hint; 1160 } 1161 } 1162 return QTableView::sizeHintForColumn(col); 1163 } 1164 1165 int LedgerView::sizeHintForRow(int row) const 1166 { 1167 // we can optimize the sizeHintForRow() operation by asking the 1168 // delegate about the height. There's no need to use the std 1169 // method which scans over all items in a column and takes a long 1170 // time in large ledgers. In case the editor is open in the row, we 1171 // use the regular method. 1172 // We always ask for the detail column as this varies in height 1173 ensurePolished(); 1174 1175 const auto m = model(); 1176 if (m) { 1177 const QModelIndex index = m->index(row, JournalModel::Column::Detail); 1178 const auto delegate = d->delegateProxy->delegate(index); 1179 const auto journalDelegate = qobject_cast<const JournalDelegate*>(delegate); 1180 1181 if (journalDelegate && (journalDelegate->editorRow() != row)) { 1182 QStyleOptionViewItem opt; 1183 opt.font = font(); 1184 opt.fontMetrics = fontMetrics(); 1185 opt.state |= (row == currentIndex().row()) ? QStyle::State_Selected : QStyle::State_None; 1186 int hint = delegate->sizeHint(opt, index).height(); 1187 if (showGrid()) 1188 hint += 1; 1189 return hint; 1190 } 1191 } 1192 return QTableView::sizeHintForRow(row); 1193 } 1194 1195 void LedgerView::resizeEvent(QResizeEvent* event) 1196 { 1197 // qDebug() << "resizeEvent, old:" << event->oldSize() << "new:" << event->size() << "viewport:" << viewport()->width(); 1198 QTableView::resizeEvent(event); 1199 adjustDetailColumn(event->size().width(), true); 1200 d->infoMessage->resize(viewport()->width(), d->infoMessage->height()); 1201 d->infoMessage->setWordWrap(false); 1202 d->infoMessage->setWordWrap(true); 1203 d->infoMessage->setText(d->infoMessage->text()); 1204 } 1205 1206 void LedgerView::adjustDetailColumn(int newViewportWidth, bool informOtherViews) 1207 { 1208 // make sure we don't get here recursively 1209 if(d->adjustingColumn) 1210 return; 1211 1212 d->adjustingColumn = true; 1213 1214 QHeaderView* header = horizontalHeader(); 1215 // calling length() here seems to be superfluous, but it forces 1216 // the execution of some internally pending operations that would 1217 // otherwise have a negative impact on our operation. 1218 header->length(); 1219 1220 int totalColumnWidth = 0; 1221 for (int i = 0; i < header->count(); ++i) { 1222 if(header->isSectionHidden(i)) { 1223 continue; 1224 } 1225 totalColumnWidth += header->sectionSize(i); 1226 } 1227 const int delta = newViewportWidth - totalColumnWidth; 1228 const int newWidth = header->sectionSize(d->adjustableColumn) + delta; 1229 if(newWidth > 10) { 1230 QSignalBlocker blocker(header); 1231 if (informOtherViews) 1232 blocker.unblock(); 1233 header->resizeSection(d->adjustableColumn, newWidth); 1234 } 1235 1236 // remember that we're done this time 1237 d->adjustingColumn = false; 1238 } 1239 1240 void LedgerView::ensureCurrentItemIsVisible() 1241 { 1242 scrollTo(currentIndex(), EnsureVisible); 1243 } 1244 1245 void LedgerView::slotSettingsChanged() 1246 { 1247 updateGeometries(); 1248 Q_EMIT settingsChanged(); 1249 1250 d->setFonts(); 1251 d->resetMaxLineCache(); 1252 #if 0 1253 // KMyMoneySettings::showGrid() 1254 // KMyMoneySettings::sortNormalView() 1255 // KMyMoneySettings::ledgerLens() 1256 // KMyMoneySettings::showRegisterDetailed() 1257 d->m_proxyModel->setHideClosedAccounts(!KMyMoneySettings::showAllAccounts()); 1258 d->m_proxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode()); 1259 d->m_proxyModel->setHideFavoriteAccounts(true); 1260 #endif 1261 } 1262 1263 void LedgerView::selectMostRecentTransaction() 1264 { 1265 if (model()->rowCount() > 0) { 1266 1267 // we need to check that the last row may contain a scheduled transaction or 1268 // the row that is shown for new transacations or a special entry (e.g. 1269 // online balance or date mark). 1270 // in that case, we need to go back to find the actual last transaction 1271 int row = model()->rowCount()-1; 1272 const auto journalModel = MyMoneyFile::instance()->journalModel(); 1273 while(row >= 0) { 1274 const auto idx = model()->index(row, 0); 1275 if (MyMoneyFile::baseModel()->baseModel(idx) == journalModel) { 1276 setCurrentIndex(idx); 1277 selectRow(idx.row()); 1278 scrollTo(idx, QAbstractItemView::PositionAtBottom); 1279 break; 1280 } 1281 row--; 1282 } 1283 } 1284 } 1285 1286 void LedgerView::editNewTransaction() 1287 { 1288 auto startEditing = [&](const QModelIndex& idx) { 1289 if (idx.data(eMyMoney::Model::IdRole).toString().isEmpty()) { 1290 scrollTo(idx, QAbstractItemView::EnsureVisible); 1291 selectRow(idx.row()); 1292 // if the empty row is already selected, we have to start editing here 1293 // otherwise, it will happen in currentChanged() 1294 const auto currentRow = currentIndex().row(); 1295 setCurrentIndex(idx); 1296 if (idx.row() == currentRow) { 1297 edit(idx); 1298 } 1299 return true; 1300 } 1301 return false; 1302 }; 1303 1304 // sorting takes care that the new transaction 1305 // (the one with an empty id) is either at the 1306 // top or the bottom of the view. So we simply 1307 // look in both locations and start editing if 1308 // we find the transaction. 1309 const auto row = model()->rowCount() - 1; 1310 if (!startEditing(model()->index(row, 0))) { 1311 startEditing(model()->index(0, 0)); 1312 } 1313 } 1314 1315 void LedgerView::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) 1316 { 1317 // call base class implementation 1318 QTableView::selectionChanged(selected, deselected); 1319 1320 QSet<int> allSelectedRows; 1321 QSet<int> selectedRows; 1322 1323 // we need to remember the first item selected as this 1324 // should always be reported as the first item in the 1325 // list of selected journalEntries. We have to divide 1326 // the number of selected indexes by the column count 1327 // to get the number of selected rows. 1328 if (selectionModel() && model() && (model()->columnCount() > 0)) { 1329 switch (selectionModel()->selectedIndexes().count() / model()->columnCount()) { 1330 case 0: 1331 d->firstSelectedId.clear(); 1332 break; 1333 case 1: 1334 d->firstSelectedId = selectionModel()->selectedIndexes().first().data(eMyMoney::Model::IdRole).toString(); 1335 break; 1336 default: 1337 break; 1338 } 1339 } 1340 1341 if (!selected.isEmpty()) { 1342 int lastRow = -1; 1343 for (const auto& idx : selectionModel()->selectedIndexes()) { 1344 if (idx.row() != lastRow) { 1345 lastRow = idx.row(); 1346 allSelectedRows += lastRow; 1347 } 1348 } 1349 lastRow = -1; 1350 for (const auto& idx : selected.indexes()) { 1351 if (idx.row() != lastRow) { 1352 lastRow = idx.row(); 1353 selectedRows += lastRow; 1354 } 1355 } 1356 1357 allSelectedRows -= selectedRows; 1358 // determine the current type of selection by looking at 1359 // the first item in allSelectedRows. In case allSelectedRows 1360 // is empty, a single item was selected and we are good to go 1361 if (!allSelectedRows.isEmpty()) { 1362 const auto baseIdx = model()->index(*allSelectedRows.constBegin(), 0); 1363 const auto isSchedule = baseIdx.data(eMyMoney::Model::TransactionScheduleRole).toBool(); 1364 1365 // now scan all in selected to check if they are of the same type 1366 // and add them to toDeselect if not. 1367 QItemSelection toDeselect; 1368 for (const auto& idx : selected.indexes()) { 1369 if (idx.data(eMyMoney::Model::TransactionScheduleRole).toBool() != isSchedule) { 1370 toDeselect.select(idx, idx); 1371 } 1372 } 1373 if (!toDeselect.isEmpty()) { 1374 selectionModel()->select(toDeselect, QItemSelectionModel::Deselect); 1375 /// @TODO: may be, we should inform the user why we deselect here 1376 } 1377 } 1378 } 1379 1380 // build the list of selected journalEntryIds 1381 // and make sure the first selected is the first listed 1382 QStringList selectedJournalEntries; 1383 QStringList selectedSchedules; 1384 1385 int lastRow = -1; 1386 bool firstSelectedStillPresent(false); 1387 1388 for (const auto& idx : selectionModel()->selectedIndexes()) { 1389 if (idx.row() != lastRow) { 1390 lastRow = idx.row(); 1391 if (d->firstSelectedId != idx.data(eMyMoney::Model::IdRole).toString()) { 1392 selectedJournalEntries += idx.data(eMyMoney::Model::IdRole).toString(); 1393 } else { 1394 firstSelectedStillPresent = true; 1395 } 1396 const auto scheduleId = idx.data(eMyMoney::Model::TransactionScheduleIdRole).toString(); 1397 if (!scheduleId.isEmpty()) { 1398 selectedSchedules += scheduleId; 1399 } 1400 } 1401 } 1402 1403 // in case we still have the first selected id, we prepend 1404 // it to the list. Otherwise, if the list is not empty, we 1405 // use the now first entry to have one in place. 1406 if (firstSelectedStillPresent && !d->firstSelectedId.isEmpty()) { 1407 selectedJournalEntries.prepend(d->firstSelectedId); 1408 } else if (!selectedJournalEntries.isEmpty()) { 1409 d->firstSelectedId = selectedJournalEntries.first(); 1410 } 1411 1412 d->selection.setSelection(SelectedObjects::JournalEntry, selectedJournalEntries); 1413 d->selection.setSelection(SelectedObjects::Schedule, selectedSchedules); 1414 // in case the selection changes when a reselection is pending 1415 // we override the pending information with the updated values 1416 if (d->reselectAfterResetPending) { 1417 d->selectionBeforeReset = selectedJournalEntries + selectedSchedules; 1418 d->currentBeforeReset = d->firstSelectedId; 1419 } 1420 Q_EMIT transactionSelectionChanged(d->selection); 1421 } 1422 1423 QStringList LedgerView::selectedJournalEntryIds() const 1424 { 1425 QStringList selection; 1426 1427 if (selectionModel()) { 1428 int lastRow = -1; 1429 QString id; 1430 for (const auto& idx : selectionModel()->selectedIndexes()) { 1431 // we don't need to process all columns but only the first one 1432 if (idx.row() != lastRow) { 1433 lastRow = idx.row(); 1434 id = idx.data(eMyMoney::Model::IdRole).toString(); 1435 if (!selection.contains(id)) { 1436 selection.append(id); 1437 } 1438 } 1439 } 1440 } 1441 return selection; 1442 } 1443 1444 void LedgerView::reselectJournalEntry(const QString& journalEntryId) 1445 { 1446 const auto journalModel = MyMoneyFile::instance()->journalModel(); 1447 const auto baseIdx = journalModel->indexById(journalEntryId); 1448 auto row = journalModel->mapFromBaseSource(model(), baseIdx).row(); 1449 if (row != -1) { 1450 setSelectedJournalEntries(QStringList(journalEntryId)); 1451 } else { 1452 // it could be, that the journal id changed due to a date 1453 // change. In this case, the transaction id is still the same 1454 // so we check if we find it. 1455 const auto newJournalEntryId = journalModel->updateJournalId(journalEntryId); 1456 if (!newJournalEntryId.isEmpty()) { 1457 setSelectedJournalEntries(QStringList(newJournalEntryId)); 1458 } 1459 } 1460 } 1461 1462 void LedgerView::setSelectedJournalEntries(const QStringList& journalEntryIds) 1463 { 1464 QItemSelection selection; 1465 const auto journalModel = MyMoneyFile::instance()->journalModel(); 1466 const auto lastColumn = model()->columnCount()-1; 1467 int startRow = -1; 1468 int lastRow = -1; 1469 QModelIndex currentIdx; 1470 1471 auto createSelectionRange = [&]() { 1472 if (startRow != -1) { 1473 selection.select(model()->index(startRow, 0), model()->index(lastRow, lastColumn)); 1474 startRow = -1; 1475 } 1476 }; 1477 1478 for (const auto& id : journalEntryIds) { 1479 if (id.isEmpty()) 1480 continue; 1481 const auto baseIdx = journalModel->indexById(id); 1482 auto row = journalModel->mapFromBaseSource(model(), baseIdx).row(); 1483 1484 // the baseIdx may point to a split in a different account which 1485 // we don't see here. In this case, we scan the journal entries 1486 // of the transaction 1487 if ((row == -1) && baseIdx.isValid()) { 1488 const auto indexes = journalModel->indexesByTransactionId(baseIdx.data(eMyMoney::Model::JournalTransactionIdRole).toString()); 1489 for (const auto& idx : indexes) { 1490 if (idx.data(eMyMoney::Model::JournalSplitAccountIdRole).toString() == d->accountId) { 1491 row = journalModel->mapFromBaseSource(model(), idx).row(); 1492 if (row != -1) { 1493 break; 1494 } 1495 } 1496 } 1497 // in case an investment account is selected as destination, 1498 // it may not have been found. In that case, we check if we find the 1499 // parent of one of the accounts and use it instead. 1500 if (row == -1) { 1501 for (const auto& idx : indexes) { 1502 const auto accountId = idx.data(eMyMoney::Model::JournalSplitAccountIdRole).toString(); 1503 const auto account = MyMoneyFile::instance()->accountsModel()->itemById(accountId); 1504 if (account.parentAccountId() == d->accountId) { 1505 row = journalModel->mapFromBaseSource(model(), idx).row(); 1506 if (row != -1) { 1507 break; 1508 } 1509 } 1510 } 1511 } 1512 } 1513 1514 if (row == -1) { 1515 qDebug() << "transaction" << id << "not found anymore for selection. skipped"; 1516 continue; 1517 } 1518 1519 if (startRow == -1) { 1520 startRow = row; 1521 lastRow = row; 1522 // use the first as the current index 1523 if (!currentIdx.isValid()) { 1524 currentIdx = model()->index(startRow, 0); 1525 } 1526 } else { 1527 if (row == lastRow+1) { 1528 lastRow = row; 1529 } else { 1530 // a new range start, so we take care of it 1531 createSelectionRange(); 1532 startRow = row; 1533 lastRow = row; 1534 } 1535 } 1536 } 1537 1538 // if no selection has been setup but we have 1539 // transactions in the ledger, we select the 1540 // last. The very last entry is the empty line, 1541 // so we have to skip that. 1542 if ((lastRow == -1) && (model()->rowCount() > 1)) { 1543 // find the last 'real' transaction 1544 startRow = model()->rowCount()-1; 1545 do { 1546 --startRow; 1547 currentIdx = model()->index(startRow, 0); 1548 } while (startRow > 0 && journalModel->baseModel(currentIdx) != journalModel); 1549 lastRow = startRow; 1550 } 1551 1552 // add a possibly dangling range 1553 createSelectionRange(); 1554 1555 selectionModel()->setCurrentIndex(currentIdx, QItemSelectionModel::NoUpdate); 1556 selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect); 1557 } 1558 1559 void LedgerView::selectAllTransactions() 1560 { 1561 QItemSelection selection; 1562 const auto journalModel = MyMoneyFile::instance()->journalModel(); 1563 int startRow = -1; 1564 int lastRow = -1; 1565 const auto lastColumn = model()->columnCount() - 1; 1566 const auto rows = model()->rowCount(); 1567 1568 auto createSelectionRange = [&]() { 1569 if (startRow != -1) { 1570 selection.select(model()->index(startRow, 0), model()->index(lastRow, lastColumn)); 1571 startRow = -1; 1572 } 1573 }; 1574 1575 for (auto row = 0; row < rows; ++row) { 1576 const auto idx = model()->index(row, 0); 1577 if (!idx.data(eMyMoney::Model::JournalTransactionIdRole).toString().isEmpty()) { 1578 auto baseIdx = journalModel->mapToBaseSource(idx); 1579 if (baseIdx.model() == journalModel) { 1580 if (startRow == -1) { 1581 startRow = row; 1582 lastRow = row; 1583 } else { 1584 if (row == (lastRow + 1)) { 1585 lastRow = row; 1586 } else { 1587 // a new range start, so we take care of it 1588 createSelectionRange(); 1589 } 1590 } 1591 } else { 1592 // a range ends, so we take care of it 1593 createSelectionRange(); 1594 } 1595 } else { 1596 // a range ends, so we take care of it 1597 createSelectionRange(); 1598 } 1599 } 1600 // add a possibly dangling range 1601 createSelectionRange(); 1602 1603 selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect); 1604 } 1605 1606 void LedgerView::slotMoveToAccount(const QString& accountId) 1607 { 1608 // close the menu, if it is still open 1609 if (pMenus[eMenu::Menu::Transaction]->isVisible()) { 1610 pMenus[eMenu::Menu::Transaction]->close(); 1611 } 1612 1613 pActions[eMenu::Action::MoveTransactionTo]->setData(accountId); 1614 pActions[eMenu::Action::MoveTransactionTo]->activate(QAction::Trigger); 1615 } 1616 1617 void LedgerView::resizeSection(QWidget* view, const QString& configGroupName, int section, int oldSize, int newSize) 1618 { 1619 if (view == this) { 1620 return; 1621 } 1622 1623 if (d->columnSelector->configGroupName() == configGroupName) { 1624 if (oldSize == 0 && newSize > 0) { 1625 setColumnHidden(section, false); 1626 } else if (oldSize > 0 && newSize == 0) { 1627 setColumnHidden(section, true); 1628 } 1629 if (newSize > 0) { 1630 QSignalBlocker blocker(horizontalHeader()); 1631 horizontalHeader()->resizeSection(section, newSize); 1632 QMetaObject::invokeMethod(this, "adjustDetailColumn", Qt::QueuedConnection, Q_ARG(int, viewport()->width()), Q_ARG(bool, false)); 1633 } 1634 } 1635 } 1636 1637 void LedgerView::moveSection(QWidget* view, int section, int oldIndex, int newIndex) 1638 { 1639 Q_UNUSED(section) 1640 if (view == this) { 1641 return; 1642 } 1643 1644 QSignalBlocker block(horizontalHeader()); 1645 horizontalHeader()->moveSection(oldIndex, newIndex); 1646 } 1647 1648 void LedgerView::setSortOrder(LedgerSortOrder sortOrder) 1649 { 1650 d->sortOrder = sortOrder; 1651 auto sortModel = qobject_cast<LedgerSortProxyModel*>(model()); 1652 if (sortModel) { 1653 sortModel->setLedgerSortOrder(sortOrder); 1654 } 1655 } 1656 1657 void LedgerView::setFocus() 1658 { 1659 // in case the editor is open, forward the focus 1660 // to the editor. otherwise, we take it. 1661 if (d->editor) { 1662 d->editor->setFocus(); 1663 } else { 1664 QTableView::setFocus(); 1665 } 1666 }