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 }