File indexing completed on 2024-05-19 05:08:28
0001 /* 0002 SPDX-FileCopyrightText: 2019-2020 Thomas Baumgart <tbaumgart@kde.org> 0003 SPDX-License-Identifier: GPL-2.0-or-later 0004 */ 0005 0006 #include "splitview.h" 0007 0008 // ---------------------------------------------------------------------------- 0009 // QT Includes 0010 0011 #include <QDate> 0012 #include <QDebug> 0013 #include <QHeaderView> 0014 #include <QMenu> 0015 #include <QPainter> 0016 #include <QResizeEvent> 0017 #include <QScopedPointer> 0018 #include <QScrollBar> 0019 0020 // ---------------------------------------------------------------------------- 0021 // KDE Includes 0022 0023 #include <KLocalizedString> 0024 0025 // ---------------------------------------------------------------------------- 0026 // Project Includes 0027 0028 #include "accountsmodel.h" 0029 #include "columnselector.h" 0030 #include "icons.h" 0031 #include "mymoneyaccount.h" 0032 #include "mymoneyenums.h" 0033 #include "mymoneyfile.h" 0034 #include "mymoneymoney.h" 0035 #include "mymoneysecurity.h" 0036 #include "splitdelegate.h" 0037 #include "splitmodel.h" 0038 0039 class SplitView::Private 0040 { 0041 public: 0042 Private(SplitView* p) 0043 : q(p) 0044 , splitDelegate(nullptr) 0045 , adjustableColumn(SplitModel::Column::Memo) 0046 , adjustingColumn(false) 0047 , showValuesInverted(false) 0048 , balanceCalculationPending(false) 0049 , newTransactionPresent(false) 0050 , firstSelectionAfterCreation(true) 0051 , blockEditorStart(false) 0052 , rightMouseButtonPress(false) 0053 , readOnly(false) 0054 , columnSelector(nullptr) 0055 { 0056 } 0057 0058 void setSingleLineDetailRole(eMyMoney::Model::Roles role) 0059 { 0060 Q_UNUSED(role) 0061 #if 0 0062 auto delegate = qobject_cast<SplitDelegate*>(q->itemDelegate()); 0063 if (delegate) { 0064 delegate->setSingleLineDetailRole(role); 0065 } 0066 #endif 0067 } 0068 0069 void ensureEditorFullyVisible(const QModelIndex& idx) 0070 { 0071 const auto viewportHeight = q->viewport()->height(); 0072 const auto verticalOffset = q->verticalHeader()->offset(); 0073 const auto verticalPosition = q->verticalHeader()->sectionPosition(idx.row()); 0074 const auto cellHeight = q->verticalHeader()->sectionSize(idx.row()); 0075 0076 // in case the idx is displayed passed the viewport 0077 // adjust the position of the scroll area 0078 if (verticalPosition - verticalOffset + cellHeight > viewportHeight) { 0079 q->verticalScrollBar()->setValue(q->verticalScrollBar()->maximum()); 0080 } 0081 } 0082 0083 void setupUnassignedValue(const QModelIndex& targetIdx) 0084 { 0085 if (!totalTransactionValue.isAutoCalc()) { 0086 auto model = const_cast<SplitModel*>(qobject_cast<const SplitModel*>(targetIdx.model())); 0087 MyMoneyMoney splitsTotal; 0088 for (int row = 0; row < model->rowCount(); ++row) { 0089 if (row == targetIdx.row()) { 0090 continue; 0091 } 0092 const auto idx = model->index(row, 0); 0093 if (idx.isValid()) { 0094 splitsTotal += idx.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>(); 0095 } 0096 } 0097 0098 model->setData(targetIdx, QVariant::fromValue<MyMoneyMoney>(totalTransactionValue - splitsTotal), eMyMoney::Model::SplitValueRole); 0099 model->setData(targetIdx, QVariant::fromValue<MyMoneyMoney>(totalTransactionValue - splitsTotal), eMyMoney::Model::SplitSharesRole); 0100 } 0101 } 0102 0103 void executeContextMenu(const QPoint& pos) 0104 { 0105 const auto currentIdx = q->currentIndex(); 0106 const auto currentId = currentIdx.data(eMyMoney::Model::IdRole).toString(); 0107 const auto enableOption = !(currentId.isEmpty() || currentId.endsWith('-')); 0108 0109 QScopedPointer<QMenu> menu(new QMenu(q)); 0110 menu->addSection(i18nc("@title:menu split context menu", "Split Options")); 0111 0112 menu->addAction(Icons::get(Icons::Icon::DocumentNew), i18nc("@item:inmenu Create a split", "New..."), q, [&]() { 0113 // search the empty split and start editing it 0114 const auto rows = q->model()->rowCount(); 0115 for (int row = 0; row < rows; ++row) { 0116 const auto idx = q->model()->index(row, 0); 0117 const auto id = idx.data(eMyMoney::Model::IdRole).toString(); 0118 if (id.isEmpty() || id.endsWith('-')) { 0119 // force a call to currentChanged() to start editing 0120 q->setCurrentIndex(QModelIndex()); 0121 q->setCurrentIndex(idx); 0122 break; 0123 } 0124 } 0125 }); 0126 0127 const auto editItem = menu->addAction(Icons::get(Icons::Icon::DocumentEdit), i18nc("@item:inmenu Edit a split", "Edit..."), q, [&]() { 0128 q->edit(currentIdx); 0129 }); 0130 editItem->setEnabled(enableOption); 0131 0132 const auto duplicateItem = menu->addAction(Icons::get(Icons::Icon::EditCopy), i18nc("@item:inmenu Duplicate selected split(s)", "Duplicate"), q, [&]() { 0133 const auto list = q->selectionModel()->selectedRows(); 0134 QPersistentModelIndex newCurrentIdx; 0135 for (const auto idx : list) { 0136 if (!idx.data(eMyMoney::Model::SplitIsNewRole).toBool()) { 0137 // we can use any model object for the next operation, but we have to use one 0138 const auto baseIdx = MyMoneyFile::instance()->accountsModel()->mapToBaseSource(idx); 0139 auto model = const_cast<SplitModel*>(qobject_cast<const SplitModel*>(baseIdx.model())); 0140 if (model) { 0141 // get the original data 0142 const auto split = model->itemByIndex(baseIdx); 0143 0144 model->appendEmptySplit(); 0145 const auto newIdx = model->emptySplit(); 0146 // prevent update signals 0147 QSignalBlocker block(model); 0148 // the id of the split will be automatically assigned by the model 0149 model->setData(newIdx, split.number(), eMyMoney::Model::SplitNumberRole); 0150 model->setData(newIdx, split.memo(), eMyMoney::Model::SplitMemoRole); 0151 model->setData(newIdx, split.accountId(), eMyMoney::Model::SplitAccountIdRole); 0152 model->setData(newIdx, split.costCenterId(), eMyMoney::Model::SplitCostCenterIdRole); 0153 model->setData(newIdx, split.payeeId(), eMyMoney::Model::SplitPayeeIdRole); 0154 model->setData(newIdx, QVariant::fromValue<MyMoneyMoney>(split.shares()), eMyMoney::Model::SplitSharesRole); 0155 // send out the dataChanged signal with the next (last) setData() 0156 block.unblock(); 0157 model->setData(newIdx, QVariant::fromValue<MyMoneyMoney>(split.value()), eMyMoney::Model::SplitValueRole); 0158 0159 // now add a new empty split at the end 0160 model->appendEmptySplit(); 0161 // and make the new split the current one 0162 if (!newCurrentIdx.isValid()) { 0163 newCurrentIdx = newIdx; 0164 } 0165 } 0166 } 0167 } 0168 if (newCurrentIdx.isValid()) { 0169 q->setCurrentIndex(newCurrentIdx); 0170 } 0171 }); 0172 duplicateItem->setEnabled(enableOption); 0173 0174 const auto deleteItem = menu->addAction(Icons::get(Icons::Icon::EditRemove), i18nc("@item:inmenu Delete selected split(s)", "Delete"), q, [&]() { 0175 Q_EMIT q->deleteSelectedSplits(); 0176 }); 0177 deleteItem->setEnabled(enableOption); 0178 0179 menu->exec(q->viewport()->mapToGlobal(pos)); 0180 } 0181 0182 SplitView* q; 0183 SplitDelegate* splitDelegate; 0184 MyMoneyAccount account; 0185 MyMoneyMoney totalTransactionValue; 0186 int adjustableColumn; 0187 bool adjustingColumn; 0188 bool showValuesInverted; 0189 bool balanceCalculationPending; 0190 bool newTransactionPresent; 0191 bool firstSelectionAfterCreation; 0192 bool blockEditorStart; 0193 bool rightMouseButtonPress; 0194 bool readOnly; 0195 ColumnSelector* columnSelector; 0196 }; 0197 0198 0199 0200 SplitView::SplitView(QWidget* parent) 0201 : QTableView(parent) 0202 , d(new Private(this)) 0203 { 0204 // keep rows as small as possible 0205 verticalHeader()->setMinimumSectionSize(10); 0206 verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); 0207 verticalHeader()->hide(); 0208 0209 horizontalHeader()->setMinimumSectionSize(20); 0210 horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); 0211 0212 // since we don't have a vertical header, it does not make sense 0213 // to use the first column to select all items in the view 0214 setCornerButtonEnabled(false); 0215 0216 // make sure to get informed about resize operations on the columns 0217 connect(horizontalHeader(), &QHeaderView::sectionResized, this, [&]() { 0218 adjustDetailColumn(viewport()->width()); 0219 }); 0220 0221 // we don't need autoscroll as we do not support drag/drop 0222 setAutoScroll(false); 0223 0224 setAlternatingRowColors(true); 0225 0226 setSelectionBehavior(SelectRows); 0227 0228 // setup the context menu 0229 setContextMenuPolicy(Qt::CustomContextMenu); 0230 connect(this, &QWidget::customContextMenuRequested, this, [&](QPoint pos) { 0231 d->executeContextMenu(pos); 0232 }); 0233 0234 setTabKeyNavigation(false); 0235 0236 d->splitDelegate = new SplitDelegate(this); 0237 setItemDelegate(d->splitDelegate); 0238 0239 d->columnSelector = new ColumnSelector(this, QStringLiteral("SplitEditor")); 0240 d->columnSelector->setAlwaysVisible( 0241 QVector<int>({SplitModel::Column::Category, SplitModel::Column::Tags, SplitModel::Column::Payment, SplitModel::Column::Deposit})); 0242 } 0243 0244 SplitView::~SplitView() 0245 { 0246 delete d; 0247 } 0248 0249 void SplitView::setModel(QAbstractItemModel* model) 0250 { 0251 QTableView::setModel(model); 0252 0253 d->columnSelector->setModel(model); 0254 0255 // This will allow the user to move the columns 0256 horizontalHeader()->setSectionsMovable(true); 0257 } 0258 0259 void SplitView::setCommodity(const MyMoneySecurity& commodity) 0260 { 0261 if (d->splitDelegate) { 0262 d->splitDelegate->setCommodity(commodity); 0263 } 0264 } 0265 0266 bool SplitView::showValuesInverted() const 0267 { 0268 return d->showValuesInverted; 0269 } 0270 0271 void SplitView::setColumnsHidden(QVector<int> columns) 0272 { 0273 d->columnSelector->setAlwaysHidden(columns); 0274 } 0275 0276 void SplitView::setColumnsShown(QVector<int> columns) 0277 { 0278 d->columnSelector->setAlwaysVisible(columns); 0279 } 0280 0281 bool SplitView::edit(const QModelIndex& index, QAbstractItemView::EditTrigger trigger, QEvent* event) 0282 { 0283 bool rc = QTableView::edit(index, trigger, event); 0284 0285 if(rc) { 0286 // editing started, but we need the editor to cover all columns 0287 // so we close it, set the span to have a single row and recreate 0288 // the editor in that single cell 0289 closeEditor(indexWidget(index), QAbstractItemDelegate::NoHint); 0290 0291 bool haveEditorInOtherView = false; 0292 /// @todo Here we need to make sure that only a single editor can be started at a time 0293 0294 if(!haveEditorInOtherView) { 0295 Q_EMIT aboutToStartEdit(); 0296 0297 if (index.data(eMyMoney::Model::SplitIsNewRole).toBool()) { 0298 d->setupUnassignedValue(index); 0299 } 0300 setSpan(index.row(), 0, 1, horizontalHeader()->count()); 0301 QModelIndex editIndex = model()->index(index.row(), 0); 0302 rc = QTableView::edit(editIndex, trigger, event); 0303 0304 // make sure that the row gets resized according to the requirements of the editor 0305 // and is completely visible 0306 resizeRowToContents(index.row()); 0307 d->ensureEditorFullyVisible(index); 0308 QMetaObject::invokeMethod(this, "ensureCurrentItemIsVisible", Qt::QueuedConnection); 0309 } else { 0310 rc = false; 0311 } 0312 } 0313 0314 return rc; 0315 } 0316 0317 void SplitView::closeEditor(QWidget* editor, QAbstractItemDelegate::EndEditHint hint) 0318 { 0319 QTableView::closeEditor(editor, hint); 0320 clearSpans(); 0321 0322 // we need to resize the row that contained the editor. 0323 resizeRowsToContents(); 0324 0325 Q_EMIT aboutToFinishEdit(); 0326 0327 QMetaObject::invokeMethod(this, "ensureCurrentItemIsVisible", Qt::QueuedConnection); 0328 } 0329 0330 void SplitView::mousePressEvent(QMouseEvent* event) 0331 { 0332 // qDebug() << "mousePressEvent"; 0333 if(state() != QAbstractItemView::EditingState) { 0334 d->rightMouseButtonPress = (event->button() == Qt::RightButton); 0335 QTableView::mousePressEvent(event); 0336 d->rightMouseButtonPress = false; 0337 } 0338 } 0339 0340 void SplitView::mouseMoveEvent(QMouseEvent* event) 0341 { 0342 Q_UNUSED(event); 0343 // qDebug() << "mouseMoveEvent"; 0344 // QTableView::mouseMoveEvent(event); 0345 } 0346 0347 void SplitView::mouseDoubleClickEvent(QMouseEvent* event) 0348 { 0349 // qDebug() << "mouseDoubleClickEvent"; 0350 QTableView::mouseDoubleClickEvent(event); 0351 } 0352 0353 void SplitView::wheelEvent(QWheelEvent* e) 0354 { 0355 // qDebug() << "wheelEvent"; 0356 QTableView::wheelEvent(e); 0357 } 0358 0359 void SplitView::skipStartEditing() 0360 { 0361 d->firstSelectionAfterCreation = true; 0362 } 0363 0364 void SplitView::blockEditorStart(bool blocked) 0365 { 0366 d->blockEditorStart = blocked; 0367 } 0368 0369 QModelIndex SplitView::moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers) 0370 { 0371 QModelIndex newIndex; 0372 0373 if (!(modifiers & Qt::ControlModifier)) { 0374 // for home and end we need to have the ControlModifier set so 0375 // that the base class implementation works on rows instead of 0376 // columns. 0377 switch (cursorAction) { 0378 case MoveHome: 0379 case MoveEnd: 0380 newIndex = QTableView::moveCursor(cursorAction, modifiers | Qt::ControlModifier); 0381 break; 0382 0383 default: 0384 newIndex = QTableView::moveCursor(cursorAction, modifiers); 0385 break; 0386 } 0387 } 0388 0389 // now make sure that moving the cursor does not hit the empty 0390 // transaction at the bottom or a schedule. 0391 for (auto row = newIndex.row(); row >= 0; --row) { 0392 newIndex = model()->index(row, newIndex.column(), newIndex.parent()); 0393 QString id = newIndex.data(eMyMoney::Model::IdRole).toString(); 0394 // skip the empty transaction at the end of a ledger if 0395 // the movement is not the down arrow 0396 if ((id.isEmpty() || id.endsWith('-')) && (cursorAction != MoveDown)) { 0397 continue; 0398 } 0399 if ((newIndex.flags() & (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) == (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) { 0400 return newIndex; 0401 } 0402 } 0403 return {}; 0404 } 0405 0406 void SplitView::currentChanged(const QModelIndex& current, const QModelIndex& previous) 0407 { 0408 // qDebug() << "currentChanged"; 0409 QTableView::currentChanged(current, previous); 0410 0411 // is it a new selection (a different row)? 0412 if (current.isValid() && (current.row() != previous.row())) { 0413 const auto idx = current.model()->index(current.row(), 0); 0414 const auto id = idx.data(eMyMoney::Model::IdRole).toString(); 0415 // For a new transaction the id is completely empty, for a split view the transaction 0416 // part is filled but the split id is empty and the string ends with a dash 0417 if (!d->blockEditorStart && !d->firstSelectionAfterCreation && (id.isEmpty() || id.endsWith('-')) && !d->rightMouseButtonPress) { 0418 selectionModel()->clearSelection(); 0419 setCurrentIndex(idx); 0420 selectRow(idx.row()); 0421 scrollTo(idx, QAbstractItemView::PositionAtBottom); 0422 edit(idx); 0423 } else { 0424 scrollTo(idx, EnsureVisible); 0425 Q_EMIT transactionSelected(MyMoneyFile::baseModel()->mapToBaseSource(idx)); 0426 } 0427 d->firstSelectionAfterCreation = false; 0428 QMetaObject::invokeMethod(this, "doItemsLayout", Qt::QueuedConnection); 0429 } 0430 } 0431 0432 void SplitView::moveEvent(QMoveEvent* event) 0433 { 0434 // qDebug() << "moveEvent"; 0435 QWidget::moveEvent(event); 0436 } 0437 0438 void SplitView::paintEvent(QPaintEvent* event) 0439 { 0440 QTableView::paintEvent(event); 0441 0442 // the base class implementation paints the regular grid in case there 0443 // is room below the last line and the bottom of the viewport. We check 0444 // here if that is the case and fill that part with the base color to 0445 // remove the false painted grid. 0446 0447 const QHeaderView *verticalHeader = this->verticalHeader(); 0448 if(verticalHeader->count() == 0) 0449 return; 0450 0451 int lastVisualRow = verticalHeader->visualIndexAt(verticalHeader->viewport()->height()); 0452 if (lastVisualRow == -1) 0453 lastVisualRow = model()->rowCount(QModelIndex()) - 1; 0454 0455 while(lastVisualRow >= model()->rowCount(QModelIndex())) 0456 --lastVisualRow; 0457 0458 while ((lastVisualRow > -1) && verticalHeader->isSectionHidden(verticalHeader->logicalIndex(lastVisualRow))) 0459 --lastVisualRow; 0460 0461 int top = 0; 0462 if(lastVisualRow != -1) 0463 top = verticalHeader->sectionViewportPosition(lastVisualRow) + verticalHeader->sectionSize(lastVisualRow); 0464 0465 if(top < viewport()->height()) { 0466 QPainter painter(viewport()); 0467 QRect rect(0, top, viewport()->width(), viewport()->height()-top); 0468 painter.fillRect(rect, QBrush(palette().base())); 0469 } 0470 } 0471 0472 void SplitView::setSingleLineDetailRole(eMyMoney::Model::Roles role) 0473 { 0474 d->setSingleLineDetailRole(role); 0475 } 0476 0477 int SplitView::sizeHintForColumn(int col) const 0478 { 0479 #if 0 0480 if (col == JournalModel::Column::Reconciliation) { 0481 QStyleOptionViewItem opt; 0482 const QModelIndex index = model()->index(0, col); 0483 const auto delegate = itemDelegate(); 0484 if (delegate) { 0485 int hint = delegate->sizeHint(opt, index).width(); 0486 if(showGrid()) 0487 hint += 1; 0488 return hint; 0489 } 0490 } 0491 #endif 0492 return QTableView::sizeHintForColumn(col); 0493 } 0494 0495 int SplitView::sizeHintForRow(int row) const 0496 { 0497 // we can optimize the sizeHintForRow() operation by asking the 0498 // delegate about the height. There's no need to use the std 0499 // method which scans over all items in a column and takes a long 0500 // time in large ledgers. In case the editor is open in the row, we 0501 // use the regular method. 0502 // We always ask for the detail column as this varies in height 0503 ensurePolished(); 0504 0505 if (model()) { 0506 const QModelIndex index = model()->index(row, SplitModel::Column::Memo); 0507 const auto delegate = itemDelegate(); 0508 const auto splitDelegate = qobject_cast<const SplitDelegate*>(delegate); 0509 0510 if(splitDelegate&& (splitDelegate->editorRow() != row)) { 0511 QStyleOptionViewItem opt; 0512 opt.state |= (row == currentIndex().row()) ? QStyle::State_Selected : QStyle::State_None; 0513 int hint = delegate->sizeHint(opt, index).height(); 0514 if(showGrid()) 0515 hint += 1; 0516 return hint; 0517 } 0518 } 0519 0520 return QTableView::sizeHintForRow(row); 0521 } 0522 0523 void SplitView::resizeEvent(QResizeEvent* event) 0524 { 0525 // qDebug() << "resizeEvent, old:" << event->oldSize() << "new:" << event->size() << "viewport:" << viewport()->width(); 0526 QTableView::resizeEvent(event); 0527 adjustDetailColumn(event->size().width()); 0528 } 0529 0530 void SplitView::adjustDetailColumn(int newViewportWidth) 0531 { 0532 // make sure we don't get here recursively 0533 if(d->adjustingColumn) 0534 return; 0535 0536 d->adjustingColumn = true; 0537 0538 QHeaderView* header = horizontalHeader(); 0539 0540 int totalColumnWidth = 0; 0541 for(int i=0; i < header->count(); ++i) { 0542 if(header->isSectionHidden(i)) { 0543 continue; 0544 } 0545 totalColumnWidth += header->sectionSize(i); 0546 } 0547 const int delta = newViewportWidth - totalColumnWidth; 0548 const int newWidth = header->sectionSize(d->adjustableColumn) + delta; 0549 if(newWidth > 10) { 0550 header->resizeSection(d->adjustableColumn, newWidth); 0551 } 0552 0553 // remember that we're done this time 0554 d->adjustingColumn = false; 0555 } 0556 0557 void SplitView::ensureCurrentItemIsVisible() 0558 { 0559 scrollTo(currentIndex(), EnsureVisible); 0560 } 0561 0562 void SplitView::slotSettingsChanged() 0563 { 0564 #if 0 0565 0566 // KMyMoneySettings::showGrid() 0567 // KMyMoneySettings::sortNormalView() 0568 // KMyMoneySettings::ledgerLens() 0569 // KMyMoneySettings::showRegisterDetailed() 0570 d->m_proxyModel->setHideClosedAccounts(!KMyMoneySettings::showAllAccounts()); 0571 d->m_proxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode()); 0572 d->m_proxyModel->setHideFavoriteAccounts(true); 0573 #endif 0574 } 0575 0576 void SplitView::selectMostRecentTransaction() 0577 { 0578 if (model()->rowCount() > 0) { 0579 0580 // we need to check that the last row may contain a scheduled transaction or 0581 // the row that is shown for new transacations or a special entry (e.g. 0582 // online balance or date mark). 0583 // in that case, we need to go back to find the actual last transaction 0584 int row = model()->rowCount()-1; 0585 if(row >= 0) { 0586 const QModelIndex idx = model()->index(row, 0); 0587 setCurrentIndex(idx); 0588 selectRow(idx.row()); 0589 scrollTo(idx, QAbstractItemView::PositionAtBottom); 0590 } 0591 } 0592 } 0593 0594 void SplitView::setTransactionPayeeId(const QString& id) 0595 { 0596 if (d->splitDelegate) { 0597 d->splitDelegate->setTransactionPayeeId(id); 0598 } 0599 } 0600 0601 void SplitView::setReadOnlyMode(bool readOnly) 0602 { 0603 d->readOnly = readOnly; 0604 d->splitDelegate->setReadOnlyMode(readOnly); 0605 } 0606 0607 void SplitView::setTotalTransactionValue(const MyMoneyMoney& transactionValue) 0608 { 0609 d->totalTransactionValue = transactionValue; 0610 }