File indexing completed on 2024-12-15 04:54:39
0001 /****************************************************************************** 0002 * 0003 * SPDX-FileCopyrightText: 2008 Szymon Tomasz Stefanek <pragma@kvirc.net> 0004 * 0005 * SPDX-License-Identifier: GPL-2.0-or-later 0006 * 0007 *******************************************************************************/ 0008 0009 // 0010 // This class is a rather huge monster. It's something that resembles a QAbstractItemModel 0011 // (because it has to provide the interface for a QTreeView) but isn't entirely one 0012 // (for optimization reasons). It basically manages a tree of items of two types: 0013 // GroupHeaderItem and MessageItem. Be sure to read the docs for ViewItemJob. 0014 // 0015 // A huge credit here goes to Till Adam which seems to have written most 0016 // (if not all) of the original KMail threading code. The KMHeaders implementation, 0017 // the documentation and his clever ideas were my starting points and essential tools. 0018 // This is why I'm adding his copyright entry (copied from headeritem.cpp) here even if 0019 // he didn't write a byte in this file until now :) 0020 // 0021 // Szymon Tomasz Stefanek, 03 Aug 2008 04:50 (am) 0022 // 0023 // This class contains ideas from: 0024 // 0025 // kmheaders.cpp / kmheaders.h, headeritem.cpp / headeritem.h 0026 // Copyright: (c) 2004 Till Adam < adam at kde dot org > 0027 // 0028 #include "core/model.h" 0029 #include "core/filter.h" 0030 #include "core/groupheaderitem.h" 0031 #include "core/item_p.h" 0032 #include "core/messageitem.h" 0033 #include "core/messageitemsetmanager.h" 0034 #include "core/model_p.h" 0035 #include "core/modelinvariantrowmapper.h" 0036 #include "core/storagemodelbase.h" 0037 #include "core/theme.h" 0038 #include "core/view.h" 0039 #include "messagelist_debug.h" 0040 #include <config-messagelist.h> 0041 0042 #include "MessageCore/StringUtil" 0043 #include <Akonadi/Item> 0044 #include <Akonadi/MessageStatus> 0045 0046 #include <KLocalizedString> 0047 0048 #include <QApplication> 0049 #include <QDateTime> 0050 #include <QElapsedTimer> 0051 #include <QIcon> 0052 #include <QLocale> 0053 #include <QScrollBar> 0054 #include <QTimer> 0055 0056 #include <algorithm> 0057 #include <chrono> 0058 0059 using namespace std::chrono_literals; 0060 0061 namespace MessageList 0062 { 0063 namespace Core 0064 { 0065 Q_GLOBAL_STATIC(QTimer, _k_heartBeatTimer) 0066 0067 /** 0068 * A job in a "View Fill" or "View Cleanup" or "View Update" task. 0069 * 0070 * For a "View Fill" task a job is a set of messages 0071 * that are contiguous in the storage. The set is expressed as a range 0072 * of row indexes. The task "sweeps" the storage in the specified 0073 * range, creates the appropriate Item instances and places them 0074 * in the right position in the tree. 0075 * 0076 * The idea is that in a single instance and for the same StorageModel 0077 * the jobs should never "cover" the same message twice. This assertion 0078 * is enforced all around this source file. 0079 * 0080 * For a "View Cleanup" task the job is a list of ModelInvariantIndex 0081 * objects (that are in fact MessageItem objects) that need to be removed 0082 * from the view. 0083 * 0084 * For a "View Update" task the job is a list of ModelInvariantIndex 0085 * objects (that are in fact MessageItem objects) that need to be updated. 0086 * 0087 * The interesting fact is that all the tasks need 0088 * very similar operations to be performed on the message tree. 0089 * 0090 * For a "View Fill" we have 5 passes. 0091 * 0092 * Pass 1 scans the underlying storage, creates the MessageItem objects 0093 * (which are subclasses of ModelInvariantIndex) and retrieves invariant 0094 * storage indexes for them. It also builds threading caches and 0095 * attempts to do some "easy" threading. If it succeeds in threading 0096 * and some conditions apply then it also attaches the items to the view. 0097 * Any unattached message is placed in a list. 0098 * 0099 * Pass 2 scans the list of messages that haven't been attached in 0100 * the first pass and performs perfect and reference based threading. 0101 * Since grouping of messages may depend on the "shape" of the thread 0102 * then certain threads aren't attached to the view yet. 0103 * Unassigned messages get stuffed into a list waiting for Pass3 0104 * or directly to a list waiting for Pass4 (that is, Pass3 may be skipped 0105 * if there is no hope to find an imperfect parent by subject based threading). 0106 * 0107 * Pass 3 scans the list of messages that haven't been attached in 0108 * the first and second passes and performs subject based threading. 0109 * Since grouping of messages may depend on the "shape" of the thread 0110 * then certain threads aren't attached to the view yet. 0111 * Anything unattached gets stuffed into the list waiting for Pass4. 0112 * 0113 * Pass 4 scans the unattached threads and puts them in the appropriate 0114 * groups. After this pass nothing is unattached. 0115 * 0116 * Pass 5 eventually re-sorts the groups and removes the empty ones. 0117 * 0118 * For a "View Cleanup" we still have 5 passes. 0119 * 0120 * Pass 1 scans the list of invalidated ModelInvariantIndex-es, casts 0121 * them to MessageItem objects and detaches them from the view. 0122 * The orphan children of the destroyed items get stuffed in the list 0123 * of unassigned messages that has been used also in the "View Fill" task above. 0124 * 0125 * Pass 2, 3, 4 and 5: same as "View Fill", just operating on the "orphaned" 0126 * messages that need to be reattached to the view. 0127 * 0128 * For a "View Update" we still have 5 passes. 0129 * 0130 * Pass 1 scans the list of ModelInvariantIndex-es that need an update, casts 0131 * them to MessageItem objects and handles the updates from storage. 0132 * The updates may cause a regrouping so items might be stuffed in one 0133 * of the lists for pass 4 or 5. 0134 * 0135 * Pass 2, 3 and 4 are simply empty. 0136 * 0137 * Pass 5: same as "View Fill", just operating on groups that require updates 0138 * after the messages have been moved in pass 1. 0139 * 0140 * That's why we in fact have Pass1Fill, Pass1Cleanup, Pass1Update, Pass2, Pass3, Pass4 and Pass5 below. 0141 * Pass1Fill, Pass1Cleanup and Pass1Update are exclusive and all of them proceed with Pass2 when finished. 0142 */ 0143 class ViewItemJob 0144 { 0145 public: 0146 enum Pass { 0147 Pass1Fill = 0, ///< Build threading caches, *TRY* to do some threading, try to attach something to the view 0148 Pass1Cleanup = 1, ///< Kill messages, build list of orphans 0149 Pass1Update = 2, ///< Update messages 0150 Pass2 = 3, ///< Thread everything by using caches, try to attach more to the view 0151 Pass3 = 4, ///< Do more threading (this time try to guess), try to attach more to the view 0152 Pass4 = 5, ///< Attach anything is still unattached 0153 Pass5 = 6, ///< Eventually Re-sort group headers and remove the empty ones 0154 LastIndex = 7 ///< Keep this at the end, needed to get the size of the enum 0155 }; 0156 0157 private: 0158 // Data for "View Fill" jobs 0159 int mStartIndex; ///< The first index (in the underlying storage) of this job 0160 int mCurrentIndex; ///< The current index (in the underlying storage) of this job 0161 int mEndIndex; ///< The last index (in the underlying storage) of this job 0162 0163 // Data for "View Cleanup" jobs 0164 QList<ModelInvariantIndex *> *mInvariantIndexList; ///< Owned list of shallow pointers 0165 0166 // Common data 0167 0168 // The maximum time that we can spend "at once" inside viewItemJobStep() (milliseconds) 0169 // The bigger this value, the larger chunks of work we do at once and less the time 0170 // we loose in "breaking and resuming" the job. On the other side large values tend 0171 // to make the view less responsive up to a "freeze" perception if this value is larger 0172 // than 2000. 0173 int mChunkTimeout; 0174 0175 // The interval between two fillView steps. The larger the interval, the more interactivity 0176 // we have. The shorter the interval the more work we get done per second. 0177 int mIdleInterval; 0178 0179 // The minimum number of messages we process in every viewItemJobStep() call 0180 // The larger this value the less time we loose in checking the timeout every N messages. 0181 // On the other side, making this very large may make the view less responsive 0182 // if we're processing very few messages at a time and very high values (say > 10000) may 0183 // eventually make our job unbreakable until the end. 0184 int mMessageCheckCount; 0185 Pass mCurrentPass; 0186 0187 // If this parameter is true then this job uses a "disconnected" UI. 0188 // It's FAR faster since we don't need to call beginInsertRows()/endInsertRows() 0189 // and we simply Q_EMIT a layoutChanged() at the end. It can be done only as the first 0190 // job though: subsequent jobs can't use layoutChanged() as it looses the expanded 0191 // state of items. 0192 bool mDisconnectUI; 0193 0194 public: 0195 /** 0196 * Creates a "View Fill" operation job 0197 */ 0198 ViewItemJob(int startIndex, int endIndex, int chunkTimeout, int idleInterval, int messageCheckCount, bool disconnectUI = false) 0199 : mStartIndex(startIndex) 0200 , mCurrentIndex(startIndex) 0201 , mEndIndex(endIndex) 0202 , mInvariantIndexList(nullptr) 0203 , mChunkTimeout(chunkTimeout) 0204 , mIdleInterval(idleInterval) 0205 , mMessageCheckCount(messageCheckCount) 0206 , mCurrentPass(Pass1Fill) 0207 , mDisconnectUI(disconnectUI) 0208 { 0209 } 0210 0211 /** 0212 * Creates a "View Cleanup" or "View Update" operation job 0213 */ 0214 ViewItemJob(Pass pass, QList<ModelInvariantIndex *> *invariantIndexList, int chunkTimeout, int idleInterval, int messageCheckCount) 0215 : mStartIndex(0) 0216 , mCurrentIndex(0) 0217 , mEndIndex(invariantIndexList->count() - 1) 0218 , mInvariantIndexList(invariantIndexList) 0219 , mChunkTimeout(chunkTimeout) 0220 , mIdleInterval(idleInterval) 0221 , mMessageCheckCount(messageCheckCount) 0222 , mCurrentPass(pass) 0223 , mDisconnectUI(false) 0224 { 0225 } 0226 0227 ~ViewItemJob() 0228 { 0229 delete mInvariantIndexList; 0230 } 0231 0232 public: 0233 [[nodiscard]] int startIndex() const 0234 { 0235 return mStartIndex; 0236 } 0237 0238 void setStartIndex(int startIndex) 0239 { 0240 mStartIndex = startIndex; 0241 mCurrentIndex = startIndex; 0242 } 0243 0244 [[nodiscard]] int currentIndex() const 0245 { 0246 return mCurrentIndex; 0247 } 0248 0249 void setCurrentIndex(int currentIndex) 0250 { 0251 mCurrentIndex = currentIndex; 0252 } 0253 0254 [[nodiscard]] int endIndex() const 0255 { 0256 return mEndIndex; 0257 } 0258 0259 void setEndIndex(int endIndex) 0260 { 0261 mEndIndex = endIndex; 0262 } 0263 0264 [[nodiscard]] Pass currentPass() const 0265 { 0266 return mCurrentPass; 0267 } 0268 0269 void setCurrentPass(Pass pass) 0270 { 0271 mCurrentPass = pass; 0272 } 0273 0274 [[nodiscard]] int idleInterval() const 0275 { 0276 return mIdleInterval; 0277 } 0278 0279 [[nodiscard]] int chunkTimeout() const 0280 { 0281 return mChunkTimeout; 0282 } 0283 0284 [[nodiscard]] int messageCheckCount() const 0285 { 0286 return mMessageCheckCount; 0287 } 0288 0289 [[nodiscard]] QList<ModelInvariantIndex *> *invariantIndexList() const 0290 { 0291 return mInvariantIndexList; 0292 } 0293 0294 [[nodiscard]] bool disconnectUI() const 0295 { 0296 return mDisconnectUI; 0297 } 0298 }; 0299 } // namespace Core 0300 } // namespace MessageList 0301 0302 using namespace MessageList::Core; 0303 0304 Model::Model(View *pParent) 0305 : QAbstractItemModel(pParent) 0306 , d(new ModelPrivate(this)) 0307 { 0308 d->mRecursionCounterForReset = 0; 0309 d->mStorageModel = nullptr; 0310 d->mView = pParent; 0311 d->mAggregation = nullptr; 0312 d->mTheme = nullptr; 0313 d->mSortOrder = nullptr; 0314 d->mFilter = nullptr; 0315 d->mPersistentSetManager = nullptr; 0316 d->mInLengthyJobBatch = false; 0317 d->mLastSelectedMessageInFolder = nullptr; 0318 d->mLoading = false; 0319 0320 d->mRootItem = new Item(Item::InvisibleRoot); 0321 d->mRootItem->setViewable(nullptr, true); 0322 0323 d->mFillStepTimer.setSingleShot(true); 0324 d->mInvariantRowMapper = new ModelInvariantRowMapper(); 0325 d->mModelForItemFunctions = this; 0326 connect(&d->mFillStepTimer, &QTimer::timeout, this, [this]() { 0327 d->viewItemJobStep(); 0328 }); 0329 0330 d->mCachedTodayLabel = i18n("Today"); 0331 d->mCachedYesterdayLabel = i18n("Yesterday"); 0332 d->mCachedUnknownLabel = i18nc("Unknown date", "Unknown"); 0333 d->mCachedLastWeekLabel = i18n("Last Week"); 0334 d->mCachedTwoWeeksAgoLabel = i18n("Two Weeks Ago"); 0335 d->mCachedThreeWeeksAgoLabel = i18n("Three Weeks Ago"); 0336 d->mCachedFourWeeksAgoLabel = i18n("Four Weeks Ago"); 0337 d->mCachedFiveWeeksAgoLabel = i18n("Five Weeks Ago"); 0338 0339 d->mCachedWatchedOrIgnoredStatusBits = Akonadi::MessageStatus::statusIgnored().toQInt32() | Akonadi::MessageStatus::statusWatched().toQInt32(); 0340 0341 connect(_k_heartBeatTimer(), &QTimer::timeout, this, [this]() { 0342 d->checkIfDateChanged(); 0343 }); 0344 0345 if (!_k_heartBeatTimer->isActive()) { // First model starts it 0346 _k_heartBeatTimer->start(1min); // 1 minute 0347 } 0348 } 0349 0350 Model::~Model() 0351 { 0352 setStorageModel(nullptr); 0353 0354 d->clearJobList(); 0355 d->mOldestItem = nullptr; 0356 d->mNewestItem = nullptr; 0357 d->clearUnassignedMessageLists(); 0358 d->clearOrphanChildrenHash(); 0359 d->clearThreadingCacheReferencesIdMD5ToMessageItem(); 0360 d->clearThreadingCacheMessageSubjectMD5ToMessageItem(); 0361 delete d->mPersistentSetManager; 0362 // Delete the invariant row mapper before removing the items. 0363 // It's faster since the items will not need to call the invariant 0364 delete d->mInvariantRowMapper; 0365 delete d->mRootItem; 0366 } 0367 0368 void Model::setAggregation(const Aggregation *aggregation) 0369 { 0370 d->mAggregation = aggregation; 0371 d->mView->setRootIsDecorated((d->mAggregation->grouping() == Aggregation::NoGrouping) && (d->mAggregation->threading() != Aggregation::NoThreading)); 0372 } 0373 0374 void Model::setTheme(const Theme *theme) 0375 { 0376 d->mTheme = theme; 0377 } 0378 0379 void Model::setSortOrder(const SortOrder *sortOrder) 0380 { 0381 d->mSortOrder = sortOrder; 0382 } 0383 0384 const SortOrder *Model::sortOrder() const 0385 { 0386 return d->mSortOrder; 0387 } 0388 0389 void Model::setFilter(const Filter *filter) 0390 { 0391 d->mFilter = filter; 0392 0393 if (d->mFilter) { 0394 connect(d->mFilter, &Filter::finished, this, [this]() { 0395 d->slotApplyFilter(); 0396 }); 0397 } 0398 0399 d->slotApplyFilter(); 0400 } 0401 0402 void ModelPrivate::slotApplyFilter() 0403 { 0404 auto childList = mRootItem->childItems(); 0405 if (!childList) { 0406 return; 0407 } 0408 0409 QModelIndex idx; // invalid 0410 0411 QApplication::setOverrideCursor(Qt::WaitCursor); 0412 for (const auto child : std::as_const(*childList)) { 0413 applyFilterToSubtree(child, idx); 0414 } 0415 0416 QApplication::restoreOverrideCursor(); 0417 } 0418 0419 bool ModelPrivate::applyFilterToSubtree(Item *item, const QModelIndex &parentIndex) 0420 { 0421 // This function applies the current filter (eventually empty) 0422 // to a message tree starting at "item". 0423 0424 if (!mModelForItemFunctions) { 0425 qCWarning(MESSAGELIST_LOG) << "Cannot apply filter, the UI must be not disconnected."; 0426 return true; 0427 } 0428 Q_ASSERT(item); // the item must obviously be valid 0429 Q_ASSERT(item->isViewable()); // the item must be viewable 0430 0431 // Apply to children first 0432 0433 auto childList = item->childItems(); 0434 0435 bool childrenMatch = false; 0436 0437 QModelIndex thisIndex = q->index(item, 0); 0438 0439 if (childList) { 0440 for (const auto child : std::as_const(*childList)) { 0441 if (applyFilterToSubtree(child, thisIndex)) { 0442 childrenMatch = true; 0443 } 0444 } 0445 } 0446 0447 if (!mFilter) { // empty filter always matches (but does not expand items) 0448 mView->setRowHidden(thisIndex.row(), parentIndex, false); 0449 return true; 0450 } 0451 0452 if (childrenMatch) { 0453 mView->setRowHidden(thisIndex.row(), parentIndex, false); 0454 0455 if (!mView->isExpanded(thisIndex)) { 0456 mView->expand(thisIndex); 0457 } 0458 return true; 0459 } 0460 0461 if (item->type() == Item::Message) { 0462 if (mFilter->match((MessageItem *)item)) { 0463 mView->setRowHidden(thisIndex.row(), parentIndex, false); 0464 return true; 0465 } 0466 } // else this is a group header and it never explicitly matches 0467 0468 // filter doesn't match, hide the item 0469 mView->setRowHidden(thisIndex.row(), parentIndex, true); 0470 0471 return false; 0472 } 0473 0474 int Model::columnCount(const QModelIndex &parent) const 0475 { 0476 if (!d->mTheme) { 0477 return 0; 0478 } 0479 if (parent.column() > 0) { 0480 return 0; 0481 } 0482 return d->mTheme->columns().count(); 0483 } 0484 0485 QVariant Model::data(const QModelIndex &index, int role) const 0486 { 0487 /// this is called only when Akonadi is using the selectionmodel 0488 /// for item actions. since akonadi uses the ETM ItemRoles, and the 0489 /// messagelist uses its own internal roles, here we respond 0490 /// to the ETM ones. 0491 0492 auto item = static_cast<Item *>(index.internalPointer()); 0493 0494 switch (role) { 0495 /// taken from entitytreemodel.h 0496 case Qt::UserRole + 1: // EntityTreeModel::ItemIdRole 0497 if (item->type() == MessageList::Core::Item::Message) { 0498 auto mItem = static_cast<MessageItem *>(item); 0499 return QVariant::fromValue(mItem->akonadiItem().id()); 0500 } else { 0501 return {}; 0502 } 0503 break; 0504 case Qt::UserRole + 2: // EntityTreeModel::ItemRole 0505 if (item->type() == MessageList::Core::Item::Message) { 0506 auto mItem = static_cast<MessageItem *>(item); 0507 return QVariant::fromValue(mItem->akonadiItem()); 0508 } else { 0509 return {}; 0510 } 0511 break; 0512 case Qt::UserRole + 3: // EntityTreeModel::MimeTypeRole 0513 if (item->type() == MessageList::Core::Item::Message) { 0514 return QStringLiteral("message/rfc822"); 0515 } else { 0516 return {}; 0517 } 0518 break; 0519 case Qt::AccessibleTextRole: 0520 if (item->type() == MessageList::Core::Item::Message) { 0521 auto mItem = static_cast<MessageItem *>(item); 0522 return mItem->accessibleText(d->mTheme, index.column()); 0523 } else if (item->type() == MessageList::Core::Item::GroupHeader) { 0524 if (index.column() > 0) { 0525 return QString(); 0526 } 0527 auto hItem = static_cast<GroupHeaderItem *>(item); 0528 return hItem->label(); 0529 } 0530 return QString(); 0531 break; 0532 default: 0533 return {}; 0534 } 0535 } 0536 0537 QVariant Model::headerData(int section, Qt::Orientation, int role) const 0538 { 0539 if (!d->mTheme) { 0540 return {}; 0541 } 0542 0543 auto column = d->mTheme->column(section); 0544 if (!column) { 0545 return {}; 0546 } 0547 0548 if (d->mStorageModel && column->isSenderOrReceiver() && (role == Qt::DisplayRole)) { 0549 if (d->mStorageModelContainsOutboundMessages) { 0550 return QVariant(i18n("Receiver")); 0551 } 0552 return QVariant(i18n("Sender")); 0553 } 0554 0555 const bool columnPixmapEmpty(column->pixmapName().isEmpty()); 0556 if ((role == Qt::DisplayRole) && columnPixmapEmpty) { 0557 return QVariant(column->label()); 0558 } else if ((role == Qt::ToolTipRole) && !columnPixmapEmpty) { 0559 return QVariant(column->label()); 0560 } else if ((role == Qt::DecorationRole) && !columnPixmapEmpty) { 0561 return QVariant(QIcon::fromTheme(column->pixmapName())); 0562 } 0563 0564 return {}; 0565 } 0566 0567 QModelIndex Model::index(Item *item, int column) const 0568 { 0569 if (!d->mModelForItemFunctions) { 0570 return {}; // called with disconnected UI: the item isn't known on the Qt side, yet 0571 } 0572 0573 if (!item) { 0574 return {}; 0575 } 0576 // FIXME: This function is a bottleneck (the caching in indexOfChildItem only works 30% of the time) 0577 auto par = item->parent(); 0578 if (!par) { 0579 if (item != d->mRootItem) { 0580 item->dump(QString()); 0581 } 0582 return {}; 0583 } 0584 0585 const int index = par->indexOfChildItem(item); 0586 if (index < 0) { 0587 return {}; // BUG 0588 } 0589 return createIndex(index, column, item); 0590 } 0591 0592 QModelIndex Model::index(int row, int column, const QModelIndex &parent) const 0593 { 0594 if (!d->mModelForItemFunctions) { 0595 return {}; // called with disconnected UI: the item isn't known on the Qt side, yet 0596 } 0597 0598 #ifdef READD_THIS_IF_YOU_WANT_TO_PASS_MODEL_TEST 0599 if (column < 0) { 0600 return QModelIndex(); // senseless column (we could optimize by skipping this check but ModelTest from trolltech is pedantic) 0601 } 0602 #endif 0603 0604 const Item *item; 0605 if (parent.isValid()) { 0606 item = static_cast<const Item *>(parent.internalPointer()); 0607 if (!item) { 0608 return {}; // should never happen 0609 } 0610 } else { 0611 item = d->mRootItem; 0612 } 0613 0614 if (parent.column() > 0) { 0615 return {}; // parent column is not 0: shouldn't have children (as per Qt documentation) 0616 } 0617 0618 Item *child = item->childItem(row); 0619 if (!child) { 0620 return {}; // no such row in parent 0621 } 0622 return createIndex(row, column, child); 0623 } 0624 0625 QModelIndex Model::parent(const QModelIndex &modelIndex) const 0626 { 0627 Q_ASSERT(d->mModelForItemFunctions); // should be never called with disconnected UI 0628 0629 if (!modelIndex.isValid()) { 0630 return {}; // should never happen 0631 } 0632 auto item = static_cast<Item *>(modelIndex.internalPointer()); 0633 if (!item) { 0634 return {}; 0635 } 0636 auto par = item->parent(); 0637 if (!par) { 0638 return {}; // should never happen 0639 } 0640 // return index( par, modelIndex.column() ); 0641 return index(par, 0); // parents are always in column 0 (as per Qt documentation) 0642 } 0643 0644 int Model::rowCount(const QModelIndex &parent) const 0645 { 0646 if (!d->mModelForItemFunctions) { 0647 return 0; // called with disconnected UI 0648 } 0649 0650 const Item *item; 0651 if (parent.isValid()) { 0652 item = static_cast<const Item *>(parent.internalPointer()); 0653 if (!item) { 0654 return 0; // should never happen 0655 } 0656 } else { 0657 item = d->mRootItem; 0658 } 0659 0660 if (!item->isViewable()) { 0661 return 0; 0662 } 0663 0664 return item->childItemCount(); 0665 } 0666 0667 class RecursionPreventer 0668 { 0669 public: 0670 RecursionPreventer(int &counter) 0671 : mCounter(counter) 0672 { 0673 mCounter++; 0674 } 0675 0676 ~RecursionPreventer() 0677 { 0678 mCounter--; 0679 } 0680 0681 [[nodiscard]] bool isRecursive() const 0682 { 0683 return mCounter > 1; 0684 } 0685 0686 private: 0687 int &mCounter; 0688 }; 0689 0690 StorageModel *Model::storageModel() const 0691 { 0692 return d->mStorageModel; 0693 } 0694 0695 void ModelPrivate::clear() 0696 { 0697 q->beginResetModel(); 0698 if (mFillStepTimer.isActive()) { 0699 mFillStepTimer.stop(); 0700 } 0701 0702 // Kill pre-selection at this stage 0703 mPreSelectionMode = PreSelectNone; 0704 mLastSelectedMessageInFolder = nullptr; 0705 mOldestItem = nullptr; 0706 mNewestItem = nullptr; 0707 0708 // Reset the row mapper before removing items 0709 // This is faster since the items don't need to access the mapper. 0710 mInvariantRowMapper->modelReset(); 0711 0712 clearJobList(); 0713 clearUnassignedMessageLists(); 0714 clearOrphanChildrenHash(); 0715 mGroupHeaderItemHash.clear(); 0716 mGroupHeadersThatNeedUpdate.clear(); 0717 mThreadingCacheMessageIdMD5ToMessageItem.clear(); 0718 mThreadingCacheMessageInReplyToIdMD5ToMessageItem.clear(); 0719 clearThreadingCacheReferencesIdMD5ToMessageItem(); 0720 clearThreadingCacheMessageSubjectMD5ToMessageItem(); 0721 mViewItemJobStepChunkTimeout = 100; 0722 mViewItemJobStepIdleInterval = 10; 0723 mViewItemJobStepMessageCheckCount = 10; 0724 delete mPersistentSetManager; 0725 mPersistentSetManager = nullptr; 0726 mCurrentItemToRestoreAfterViewItemJobStep = nullptr; 0727 0728 mTodayDate = QDate::currentDate(); 0729 0730 // FIXME: CLEAR THE FILTER HERE AS WE CAN'T APPLY IT WITH UI DISCONNECTED! 0731 0732 mRootItem->killAllChildItems(); 0733 0734 q->endResetModel(); 0735 // Q_EMIT headerDataChanged(); 0736 0737 mView->selectionModel()->clearSelection(); 0738 } 0739 0740 void Model::setStorageModel(StorageModel *storageModel, PreSelectionMode preSelectionMode) 0741 { 0742 // Prevent a case of recursion when opening a folder that has a message and the folder was 0743 // never opened before. 0744 RecursionPreventer preventer(d->mRecursionCounterForReset); 0745 if (preventer.isRecursive()) { 0746 return; 0747 } 0748 0749 d->clear(); 0750 0751 if (d->mStorageModel) { 0752 // Disconnect all signals from old storageModel 0753 std::for_each(d->mStorageModelConnections.cbegin(), d->mStorageModelConnections.cend(), [](const QMetaObject::Connection &c) -> bool { 0754 return QObject::disconnect(c); 0755 }); 0756 d->mStorageModelConnections.clear(); 0757 } 0758 0759 const bool isReload = (d->mStorageModel == storageModel); 0760 d->mStorageModel = storageModel; 0761 0762 if (!d->mStorageModel) { 0763 return; // no folder: nothing to fill 0764 } 0765 0766 // Save threading cache of the previous folder, but only if the cache was 0767 // enabled and a different folder is being loaded - reload of the same folder 0768 // means change in aggregation in which case we will have to re-build the 0769 // cache so there's no point saving the current threading cache. 0770 if (d->mThreadingCache.isEnabled() && !isReload) { 0771 d->mThreadingCache.save(); 0772 } else { 0773 if (isReload) { 0774 qCDebug(MESSAGELIST_LOG) << "Identical folder reloaded, not saving old threading cache"; 0775 } else { 0776 qCDebug(MESSAGELIST_LOG) << "Threading disabled in previous folder, not saving threading cache"; 0777 } 0778 } 0779 // Load threading cache for the new folder, but only if threading is enabled, 0780 // otherwise we would just be caching a flat list. 0781 if (d->mAggregation->threading() != Aggregation::NoThreading) { 0782 d->mThreadingCache.setEnabled(true); 0783 d->mThreadingCache.load(d->mStorageModel->id(), d->mAggregation); 0784 } else { 0785 // No threading, no cache - don't even bother inserting entries into the 0786 // cache or trying to look them up there 0787 d->mThreadingCache.setEnabled(false); 0788 qCDebug(MESSAGELIST_LOG) << "Threading disabled in folder" << d->mStorageModel->id() << ", not using threading cache"; 0789 } 0790 0791 d->mPreSelectionMode = preSelectionMode; 0792 d->mStorageModelContainsOutboundMessages = d->mStorageModel->containsOutboundMessages(); 0793 0794 d->mStorageModelConnections = {connect(d->mStorageModel, 0795 &StorageModel::rowsInserted, 0796 this, 0797 [this](const QModelIndex &parent, int first, int last) { 0798 d->slotStorageModelRowsInserted(parent, first, last); 0799 }), 0800 connect(d->mStorageModel, 0801 &StorageModel::rowsRemoved, 0802 this, 0803 [this](const QModelIndex &parent, int first, int last) { 0804 d->slotStorageModelRowsRemoved(parent, first, last); 0805 }), 0806 connect(d->mStorageModel, 0807 &StorageModel::layoutChanged, 0808 this, 0809 [this]() { 0810 d->slotStorageModelLayoutChanged(); 0811 }), 0812 connect(d->mStorageModel, 0813 &StorageModel::modelReset, 0814 this, 0815 [this]() { 0816 d->slotStorageModelLayoutChanged(); 0817 }), 0818 connect(d->mStorageModel, 0819 &StorageModel::dataChanged, 0820 this, 0821 [this](const QModelIndex &topLeft, const QModelIndex &bottomRight) { 0822 d->slotStorageModelDataChanged(topLeft, bottomRight); 0823 }), 0824 connect(d->mStorageModel, &StorageModel::headerDataChanged, this, [this](Qt::Orientation orientation, int first, int last) { 0825 d->slotStorageModelHeaderDataChanged(orientation, first, last); 0826 })}; 0827 0828 if (d->mStorageModel->rowCount() == 0) { 0829 return; // folder empty: nothing to fill 0830 } 0831 0832 // Here we use different strategies based on user preference and the folder size. 0833 // The knobs we can tune are: 0834 // 0835 // - The number of jobs used to scan the whole folder and their order 0836 // 0837 // There are basically two approaches to this. One is the "single big job" 0838 // approach. It scans the folder from the beginning to the end in a single job 0839 // entry. The job passes are done only once. It's advantage is that it's simpler 0840 // and it's less likely to generate imperfect parent threadings. The bad 0841 // side is that since the folders are "sort of" date ordered then the most interesting 0842 // messages show up at the end of the work. Not nice for large folders. 0843 // The other approach uses two jobs. This is a bit slower but smarter strategy. 0844 // First we scan the latest 1000 messages and *then* take care of the older ones. 0845 // This will show up the most interesting messages almost immediately. (Well... 0846 // All this assuming that the underlying storage always appends the newly arrived messages) 0847 // The strategy is slower since it generates some imperfect parent threadings which must be 0848 // adjusted by the second job. For instance, in my kernel mailing list folder this "smart" approach 0849 // generates about 150 additional imperfectly threaded children... but the "today" 0850 // messages show up almost immediately. The two-chunk job also makes computing 0851 // the percentage user feedback a little harder and might break some optimization 0852 // in the insertions (we're able to optimize appends and prepends but a chunked 0853 // job is likely to split our work at a boundary where messages are always inserted 0854 // in the middle of the list). 0855 // 0856 // - The maximum time to spend inside a single job step 0857 // 0858 // The larger this time, the greater the number of messages per second that this 0859 // engine can process but also greater time with frozen UI -> less interactivity. 0860 // Reasonable values start at 50 msecs. Values larger than 300 msecs are very likely 0861 // to be perceived by the user as UI non-reactivity. 0862 // 0863 // - The number of messages processed in each job step subchunk. 0864 // 0865 // A job subchunk is processed without checking the maximum time above. This means 0866 // that each job step will process at least the number of messages specified by this value. 0867 // Very low values mean that we respect the maximum time very carefully but we also 0868 // waste time to check if we ran out of time :) 0869 // Very high values are likely to cause the engine to not respect the maximum step time. 0870 // Reasonable values go from 5 to 100. 0871 // 0872 // - The "idle" time between two steps 0873 // 0874 // The lower this time, the greater the number of messages per second that this 0875 // engine can process but also lower time for the UI to process events -> less interactivity. 0876 // A value of 0 here means that Qt will trigger the timer as soon as it has some 0877 // idle time to spend. UI events will be still processed but slowdowns are possible. 0878 // 0 is reasonable though. Values larger than 200 will tend to make the total job 0879 // completion times high. 0880 // 0881 0882 // If we have no filter it seems that we can apply a huge optimization. 0883 // We disconnect the UI for the first huge filling job. This allows us 0884 // to save the extremely expensive beginInsertRows()/endInsertRows() calls 0885 // and call a single layoutChanged() at the end. This slows down a lot item 0886 // expansion. But on the other side if only few items need to be expanded 0887 // then this strategy is better. If filtering is enabled then this strategy 0888 // isn't applicable (because filtering requires interaction with the UI 0889 // while the data is loading). 0890 0891 // So... 0892 0893 // For the very first small chunk it's ok to work with disconnected UI as long 0894 // as we have no filter. The first small chunk is always 1000 messages, so 0895 // even if all of them are expanded, it's still somewhat acceptable. 0896 bool canDoFirstSmallChunkWithDisconnectedUI = !d->mFilter; 0897 0898 // Larger works need a bigger condition: few messages must be expanded in the end. 0899 bool canDoJobWithDisconnectedUI = // we have no filter 0900 !d->mFilter 0901 && ( 0902 // we do no threading at all 0903 (d->mAggregation->threading() == Aggregation::NoThreading) || // or we never expand threads 0904 (d->mAggregation->threadExpandPolicy() == Aggregation::NeverExpandThreads) || // or we expand threads but we'll be going to expand really only a few 0905 ( 0906 // so we don't expand them all 0907 (d->mAggregation->threadExpandPolicy() != Aggregation::AlwaysExpandThreads) && // and we'd expand only a few in fact 0908 (d->mStorageModel->initialUnreadRowCountGuess() < 1000))); 0909 0910 switch (d->mAggregation->fillViewStrategy()) { 0911 case Aggregation::FavorInteractivity: 0912 // favor interactivity 0913 if ((!canDoJobWithDisconnectedUI) && (d->mStorageModel->rowCount() > 3000)) { // empiric value 0914 // First a small job with the most recent messages. Large chunk, small (but non zero) idle interval 0915 // and a larger number of messages to process at once. 0916 auto job1 = 0917 new ViewItemJob(d->mStorageModel->rowCount() - 1000, d->mStorageModel->rowCount() - 1, 200, 20, 100, canDoFirstSmallChunkWithDisconnectedUI); 0918 d->mViewItemJobs.append(job1); 0919 // Then a larger job with older messages. Small chunk, bigger idle interval, small number of messages to 0920 // process at once. 0921 auto job2 = new ViewItemJob(0, d->mStorageModel->rowCount() - 1001, 100, 50, 10, false); 0922 d->mViewItemJobs.append(job2); 0923 0924 // We could even extremize this by splitting the folder in several 0925 // chunks and scanning them from the newest to the oldest... but the overhead 0926 // due to imperfectly threaded children would be probably too big. 0927 } else { 0928 // small folder or can be done with disconnected UI: single chunk work. 0929 // Lag the CPU a bit more but not too much to destroy even the earliest interactivity. 0930 auto job = new ViewItemJob(0, d->mStorageModel->rowCount() - 1, 150, 30, 30, canDoJobWithDisconnectedUI); 0931 d->mViewItemJobs.append(job); 0932 } 0933 break; 0934 case Aggregation::FavorSpeed: 0935 // More batchy jobs, still interactive to a certain degree 0936 if ((!canDoJobWithDisconnectedUI) && (d->mStorageModel->rowCount() > 3000)) { // empiric value 0937 // large folder, but favor speed 0938 auto job1 = 0939 new ViewItemJob(d->mStorageModel->rowCount() - 1000, d->mStorageModel->rowCount() - 1, 250, 0, 100, canDoFirstSmallChunkWithDisconnectedUI); 0940 d->mViewItemJobs.append(job1); 0941 auto job2 = new ViewItemJob(0, d->mStorageModel->rowCount() - 1001, 200, 0, 10, false); 0942 d->mViewItemJobs.append(job2); 0943 } else { 0944 // small folder or can be done with disconnected UI and favor speed: single chunk work. 0945 // Lag the CPU more, get more work done 0946 auto job = new ViewItemJob(0, d->mStorageModel->rowCount() - 1, 250, 0, 100, canDoJobWithDisconnectedUI); 0947 d->mViewItemJobs.append(job); 0948 } 0949 break; 0950 case Aggregation::BatchNoInteractivity: { 0951 // one large job, never interrupt, block UI 0952 auto job = new ViewItemJob(0, d->mStorageModel->rowCount() - 1, 60000, 0, 100000, canDoJobWithDisconnectedUI); 0953 d->mViewItemJobs.append(job); 0954 break; 0955 } 0956 default: 0957 qCWarning(MESSAGELIST_LOG) << "Unrecognized fill view strategy"; 0958 Q_ASSERT(false); 0959 break; 0960 } 0961 0962 d->mLoading = true; 0963 0964 d->viewItemJobStep(); 0965 } 0966 0967 void ModelPrivate::checkIfDateChanged() 0968 { 0969 // This function is called by MessageList::Core::Manager once in a while (every 1 minute or sth). 0970 // It is used to check if the current date has changed (with respect to mTodayDate). 0971 // 0972 // Our message items cache the formatted dates (as formatting them 0973 // on the fly would be too expensive). We also cache the labels of the groups which often display dates. 0974 // When the date changes we would need to fix all these strings. 0975 // 0976 // A dedicated algorithm to refresh the labels of the items would be either too complex 0977 // or would block on large trees. Fixing the labels of the groups is also quite hard... 0978 // 0979 // So to keep the things simple we just reload the view. 0980 0981 if (!mStorageModel) { 0982 return; // nothing to do 0983 } 0984 0985 if (mLoading) { 0986 return; // not now 0987 } 0988 0989 if (!mViewItemJobs.isEmpty()) { 0990 return; // not now 0991 } 0992 0993 if (mTodayDate == QDate::currentDate()) { 0994 return; // date not changed 0995 } 0996 0997 // date changed, reload the view (and try to preserve the current selection) 0998 q->setStorageModel(mStorageModel, PreSelectLastSelected); 0999 } 1000 1001 void Model::setPreSelectionMode(PreSelectionMode preSelect) 1002 { 1003 d->mPreSelectionMode = preSelect; 1004 d->mLastSelectedMessageInFolder = nullptr; 1005 } 1006 1007 // 1008 // The "view fill" algorithm implemented in the functions below is quite smart but also quite complex. 1009 // It's governed by the following goals: 1010 // 1011 // - Be flexible: allow different configurations from "unsorted flat list" to a "grouped and threaded 1012 // list with different sorting algorithms applied to each aggregation level" 1013 // - Be reasonably fast 1014 // - Be non blocking: UI shouldn't freeze while the algorithm is running 1015 // - Be interruptible: user must be able to abort the execution and just switch to another folder in the middle 1016 // 1017 1018 void ModelPrivate::clearUnassignedMessageLists() 1019 { 1020 // This is a bit tricky... 1021 // The three unassigned message lists contain messages that have been created 1022 // but not yet attached to the view. There may be two major cases for a message: 1023 // - it has no parent -> it must be deleted and it will delete its children too 1024 // - it has a parent -> it must NOT be deleted since it will be deleted by its parent. 1025 1026 // Sometimes the things get a little complicated since in Pass2 and Pass3 1027 // we have transitional states in that the MessageItem object can be in two of these lists. 1028 1029 // WARNING: This function does NOT fixup mNewestItem and mOldestItem. If one of these 1030 // two messages is in the lists below, it's deleted and the member becomes a dangling pointer. 1031 // The caller must ensure that both mNewestItem and mOldestItem are set to 0 1032 // and this is enforced in the assert below to avoid errors. This basically means 1033 // that this function should be called only when the storage model changes or 1034 // when the model is destroyed. 1035 Q_ASSERT((mOldestItem == nullptr) && (mNewestItem == nullptr)); 1036 1037 if (!mUnassignedMessageListForPass2.isEmpty()) { 1038 // We're actually in Pass1* or Pass2: everything is mUnassignedMessageListForPass2 1039 // Something may *also* be in mUnassignedMessageListForPass3 and mUnassignedMessageListForPass4 1040 // but that are duplicates for sure. 1041 1042 // We can't just sweep the list and delete parentless items since each delete 1043 // could kill children which are somewhere AFTER in the list: accessing the children 1044 // would then lead to a SIGSEGV. We first sweep the list gathering parentless 1045 // items and *then* delete them without accessing the parented ones. 1046 1047 QList<MessageItem *> parentless; 1048 for (const auto mi : std::as_const(mUnassignedMessageListForPass2)) { 1049 if (!mi->parent()) { 1050 parentless.append(mi); 1051 } 1052 } 1053 1054 for (const auto mi : std::as_const(parentless)) { 1055 delete mi; 1056 } 1057 1058 mUnassignedMessageListForPass2.clear(); 1059 // Any message these list contain was also in mUnassignedMessageListForPass2 1060 mUnassignedMessageListForPass3.clear(); 1061 mUnassignedMessageListForPass4.clear(); 1062 return; 1063 } 1064 1065 // mUnassignedMessageListForPass2 is empty 1066 1067 if (!mUnassignedMessageListForPass3.isEmpty()) { 1068 // We're actually at the very end of Pass2 or inside Pass3 1069 // Pass2 pushes stuff in mUnassignedMessageListForPass3 *or* mUnassignedMessageListForPass4 1070 // Pass3 pushes stuff from mUnassignedMessageListForPass3 to mUnassignedMessageListForPass4 1071 // So if we're in Pass2 then the two lists contain distinct messages but if we're in Pass3 1072 // then the two lists may contain the same messages. 1073 1074 if (!mUnassignedMessageListForPass4.isEmpty()) { 1075 // We're actually in Pass3: the messiest one. 1076 1077 QSet<MessageItem *> itemsToDelete; 1078 for (const auto mi : std::as_const(mUnassignedMessageListForPass3)) { 1079 if (!mi->parent()) { 1080 itemsToDelete.insert(mi); 1081 } 1082 } 1083 for (const auto mi : std::as_const(mUnassignedMessageListForPass4)) { 1084 if (!mi->parent()) { 1085 itemsToDelete.insert(mi); 1086 } 1087 } 1088 for (const auto mi : std::as_const(itemsToDelete)) { 1089 delete mi; 1090 } 1091 1092 mUnassignedMessageListForPass3.clear(); 1093 mUnassignedMessageListForPass4.clear(); 1094 return; 1095 } 1096 1097 // mUnassignedMessageListForPass4 is empty so we must be at the end of a very special kind of Pass2 1098 // We have the same problem as in mUnassignedMessageListForPass2. 1099 QList<MessageItem *> parentless; 1100 for (const auto mi : std::as_const(mUnassignedMessageListForPass3)) { 1101 if (!mi->parent()) { 1102 parentless.append(mi); 1103 } 1104 } 1105 for (const auto mi : std::as_const(parentless)) { 1106 delete mi; 1107 } 1108 1109 mUnassignedMessageListForPass3.clear(); 1110 return; 1111 } 1112 1113 // mUnassignedMessageListForPass3 is empty 1114 if (!mUnassignedMessageListForPass4.isEmpty()) { 1115 // we're in Pass4.. this is easy. 1116 1117 // We have the same problem as in mUnassignedMessageListForPass2. 1118 QList<MessageItem *> parentless; 1119 for (const auto mi : std::as_const(mUnassignedMessageListForPass4)) { 1120 if (!mi->parent()) { 1121 parentless.append(mi); 1122 } 1123 } 1124 for (const auto mi : std::as_const(parentless)) { 1125 delete mi; 1126 } 1127 1128 mUnassignedMessageListForPass4.clear(); 1129 return; 1130 } 1131 } 1132 1133 void ModelPrivate::clearThreadingCacheReferencesIdMD5ToMessageItem() 1134 { 1135 qDeleteAll(mThreadingCacheMessageReferencesIdMD5ToMessageItem); 1136 mThreadingCacheMessageReferencesIdMD5ToMessageItem.clear(); 1137 } 1138 1139 void ModelPrivate::clearThreadingCacheMessageSubjectMD5ToMessageItem() 1140 { 1141 qDeleteAll(mThreadingCacheMessageSubjectMD5ToMessageItem); 1142 mThreadingCacheMessageSubjectMD5ToMessageItem.clear(); 1143 } 1144 1145 void ModelPrivate::clearOrphanChildrenHash() 1146 { 1147 qDeleteAll(mOrphanChildrenHash); 1148 mOrphanChildrenHash.clear(); 1149 } 1150 1151 void ModelPrivate::clearJobList() 1152 { 1153 if (mViewItemJobs.isEmpty()) { 1154 return; 1155 } 1156 1157 if (mInLengthyJobBatch) { 1158 mInLengthyJobBatch = false; 1159 } 1160 1161 qDeleteAll(mViewItemJobs); 1162 mViewItemJobs.clear(); 1163 1164 mModelForItemFunctions = q; // make sure it's true, as there remains no job with disconnected UI 1165 } 1166 1167 void ModelPrivate::attachGroup(GroupHeaderItem *ghi) 1168 { 1169 if (ghi->parent()) { 1170 if (((ghi)->childItemCount() > 0) // has children 1171 && (ghi)->isViewable() // is actually attached to the viewable root 1172 && mModelForItemFunctions // the UI is not disconnected 1173 && mView->isExpanded(q->index(ghi, 0)) // is actually expanded 1174 ) { 1175 saveExpandedStateOfSubtree(ghi); 1176 } 1177 1178 // FIXME: This *WILL* break selection and current index... :/ 1179 1180 ghi->parent()->takeChildItem(mModelForItemFunctions, ghi); 1181 } 1182 1183 ghi->setParent(mRootItem); 1184 1185 // I'm using a macro since it does really improve readability. 1186 // I'm NOT using a helper function since gcc will refuse to inline some of 1187 // the calls because they make this function grow too much. 1188 #define INSERT_GROUP_WITH_COMPARATOR(_ItemComparator) \ 1189 switch (mSortOrder->groupSortDirection()) { \ 1190 case SortOrder::Ascending: \ 1191 mRootItem->d_ptr->insertChildItem<_ItemComparator, true>(mModelForItemFunctions, ghi); \ 1192 break; \ 1193 case SortOrder::Descending: \ 1194 mRootItem->d_ptr->insertChildItem<_ItemComparator, false>(mModelForItemFunctions, ghi); \ 1195 break; \ 1196 default: /* should never happen... */ \ 1197 mRootItem->appendChildItem(mModelForItemFunctions, ghi); \ 1198 break; \ 1199 } 1200 1201 switch (mSortOrder->groupSorting()) { 1202 case SortOrder::SortGroupsByDateTime: 1203 INSERT_GROUP_WITH_COMPARATOR(ItemDateComparator) 1204 break; 1205 case SortOrder::SortGroupsByDateTimeOfMostRecent: 1206 INSERT_GROUP_WITH_COMPARATOR(ItemMaxDateComparator) 1207 break; 1208 case SortOrder::SortGroupsBySenderOrReceiver: 1209 INSERT_GROUP_WITH_COMPARATOR(ItemSenderOrReceiverComparator) 1210 break; 1211 case SortOrder::SortGroupsBySender: 1212 INSERT_GROUP_WITH_COMPARATOR(ItemSenderComparator) 1213 break; 1214 case SortOrder::SortGroupsByReceiver: 1215 INSERT_GROUP_WITH_COMPARATOR(ItemReceiverComparator) 1216 break; 1217 case SortOrder::NoGroupSorting: 1218 mRootItem->appendChildItem(mModelForItemFunctions, ghi); 1219 break; 1220 default: // should never happen 1221 mRootItem->appendChildItem(mModelForItemFunctions, ghi); 1222 break; 1223 } 1224 1225 if (ghi->initialExpandStatus() == Item::ExpandNeeded) { // this actually is a "non viewable expanded state" 1226 if (ghi->childItemCount() > 0) { 1227 if (mModelForItemFunctions) { // the UI is not disconnected 1228 syncExpandedStateOfSubtree(ghi); 1229 } 1230 } 1231 } 1232 1233 // A group header is always viewable, when attached: apply the filter, if we have it. 1234 if (mFilter) { 1235 Q_ASSERT(mModelForItemFunctions); // UI must be NOT disconnected 1236 // apply the filter to subtree 1237 applyFilterToSubtree(ghi, QModelIndex()); 1238 } 1239 } 1240 1241 void ModelPrivate::saveExpandedStateOfSubtree(Item *root) 1242 { 1243 Q_ASSERT(mModelForItemFunctions); // UI must be NOT disconnected here 1244 Q_ASSERT(root); 1245 1246 root->setInitialExpandStatus(Item::ExpandNeeded); 1247 1248 auto children = root->childItems(); 1249 if (!children) { 1250 return; 1251 } 1252 for (const auto mi : std::as_const(*children)) { 1253 if (mi->childItemCount() > 0 // has children 1254 && mi->isViewable() // is actually attached to the viewable root 1255 && mView->isExpanded(q->index(mi, 0))) { // is actually expanded 1256 saveExpandedStateOfSubtree(mi); 1257 } 1258 } 1259 } 1260 1261 void ModelPrivate::syncExpandedStateOfSubtree(Item *root) 1262 { 1263 Q_ASSERT(mModelForItemFunctions); // UI must be NOT disconnected here 1264 1265 // WE ASSUME that: 1266 // - the item is viewable 1267 // - its initialExpandStatus() is Item::ExpandNeeded 1268 // - it has at least one children (well.. this is not a strict requirement, but it's a waste of resources to expand items that don't have children) 1269 1270 QModelIndex idx = q->index(root, 0); 1271 1272 // if ( !mView->isExpanded( idx ) ) // this is O(logN!) in Qt.... very ugly... but it should never happen here 1273 mView->expand(idx); // sync the real state in the view 1274 root->setInitialExpandStatus(Item::ExpandExecuted); 1275 1276 auto children = root->childItems(); 1277 if (!children) { 1278 return; 1279 } 1280 1281 for (const auto mi : std::as_const(*children)) { 1282 if (mi->initialExpandStatus() == Item::ExpandNeeded) { 1283 if (mi->childItemCount() > 0) { 1284 syncExpandedStateOfSubtree(mi); 1285 } 1286 } 1287 } 1288 } 1289 1290 void ModelPrivate::attachMessageToGroupHeader(MessageItem *mi) 1291 { 1292 QString groupLabel; 1293 time_t date; 1294 1295 // compute the group header label and the date 1296 switch (mAggregation->grouping()) { 1297 case Aggregation::GroupByDate: 1298 case Aggregation::GroupByDateRange: { 1299 if (mAggregation->threadLeader() == Aggregation::MostRecentMessage) { 1300 date = mi->maxDate(); 1301 } else { 1302 date = mi->date(); 1303 } 1304 1305 QDateTime dt; 1306 dt.setSecsSinceEpoch(date); 1307 QDate dDate = dt.date(); 1308 int daysAgo = -1; 1309 const int daysInWeek = 7; 1310 if (dDate.isValid() && mTodayDate.isValid()) { 1311 daysAgo = dDate.daysTo(mTodayDate); 1312 } 1313 1314 if ((daysAgo < 0) // In the future 1315 || (static_cast<uint>(date) == static_cast<uint>(-1))) { // Invalid 1316 groupLabel = mCachedUnknownLabel; 1317 } else if (daysAgo == 0) { // Today 1318 groupLabel = mCachedTodayLabel; 1319 } else if (daysAgo == 1) { // Yesterday 1320 groupLabel = mCachedYesterdayLabel; 1321 } else if (daysAgo > 1 && daysAgo < daysInWeek) { // Within last seven days 1322 auto dayName = mCachedDayNameLabel.find(dDate.dayOfWeek()); // non-const call, but non-shared container 1323 if (dayName == mCachedDayNameLabel.end()) { 1324 dayName = mCachedDayNameLabel.insert(dDate.dayOfWeek(), QLocale::system().standaloneDayName(dDate.dayOfWeek())); 1325 } 1326 groupLabel = *dayName; 1327 } else if (mAggregation->grouping() == Aggregation::GroupByDate) { // GroupByDate seven days or more ago 1328 groupLabel = QLocale::system().toString(dDate, QLocale::ShortFormat); 1329 } else if (dDate.month() == mTodayDate.month() // GroupByDateRange within this month 1330 && dDate.year() == mTodayDate.year()) { 1331 const int startOfWeekDaysAgo = (daysInWeek + mTodayDate.dayOfWeek() - QLocale().firstDayOfWeek()) % daysInWeek; 1332 const int weeksAgo = ((daysAgo - startOfWeekDaysAgo) / daysInWeek) + 1; 1333 switch (weeksAgo) { 1334 case 0: // This week 1335 groupLabel = QLocale::system().standaloneDayName(dDate.dayOfWeek()); 1336 break; 1337 case 1: // 1 week ago 1338 groupLabel = mCachedLastWeekLabel; 1339 break; 1340 case 2: 1341 groupLabel = mCachedTwoWeeksAgoLabel; 1342 break; 1343 case 3: 1344 groupLabel = mCachedThreeWeeksAgoLabel; 1345 break; 1346 case 4: 1347 groupLabel = mCachedFourWeeksAgoLabel; 1348 break; 1349 case 5: 1350 groupLabel = mCachedFiveWeeksAgoLabel; 1351 break; 1352 default: // should never happen 1353 groupLabel = mCachedUnknownLabel; 1354 } 1355 } else if (dDate.year() == mTodayDate.year()) { // GroupByDateRange within this year 1356 auto monthName = mCachedMonthNameLabel.find(dDate.month()); // non-const call, but non-shared container 1357 if (monthName == mCachedMonthNameLabel.end()) { 1358 monthName = mCachedMonthNameLabel.insert(dDate.month(), QLocale::system().standaloneMonthName(dDate.month())); 1359 } 1360 groupLabel = *monthName; 1361 } else { // GroupByDateRange in previous years 1362 auto monthName = mCachedMonthNameLabel.find(dDate.month()); // non-const call, but non-shared container 1363 if (monthName == mCachedMonthNameLabel.end()) { 1364 monthName = mCachedMonthNameLabel.insert(dDate.month(), QLocale::system().standaloneMonthName(dDate.month())); 1365 } 1366 groupLabel = i18nc("Message Aggregation Group Header: Month name and Year number", 1367 "%1 %2", 1368 *monthName, 1369 QLocale::system().toString(dDate, QLatin1StringView("yyyy"))); 1370 } 1371 break; 1372 } 1373 1374 case Aggregation::GroupBySenderOrReceiver: 1375 date = mi->date(); 1376 groupLabel = mi->displaySenderOrReceiver(); 1377 break; 1378 1379 case Aggregation::GroupBySender: 1380 date = mi->date(); 1381 groupLabel = mi->displaySender(); 1382 break; 1383 1384 case Aggregation::GroupByReceiver: 1385 date = mi->date(); 1386 groupLabel = mi->displayReceiver(); 1387 break; 1388 1389 case Aggregation::NoGrouping: 1390 // append directly to root 1391 attachMessageToParent(mRootItem, mi); 1392 return; 1393 1394 default: 1395 // should never happen 1396 attachMessageToParent(mRootItem, mi); 1397 return; 1398 } 1399 1400 GroupHeaderItem *ghi; 1401 1402 ghi = mGroupHeaderItemHash.value(groupLabel, nullptr); 1403 if (!ghi) { 1404 // not found 1405 1406 ghi = new GroupHeaderItem(groupLabel); 1407 ghi->initialSetup(date, mi->size(), mi->sender(), mi->receiver(), mi->useReceiver()); 1408 1409 switch (mAggregation->groupExpandPolicy()) { 1410 case Aggregation::NeverExpandGroups: 1411 // nothing to do 1412 break; 1413 case Aggregation::AlwaysExpandGroups: 1414 // expand always 1415 ghi->setInitialExpandStatus(Item::ExpandNeeded); 1416 break; 1417 case Aggregation::ExpandRecentGroups: 1418 // expand only if "close" to today 1419 if (mViewItemJobStepStartTime > ghi->date()) { 1420 if ((mViewItemJobStepStartTime - ghi->date()) < (3600 * 72)) { 1421 ghi->setInitialExpandStatus(Item::ExpandNeeded); 1422 } 1423 } else { 1424 if ((ghi->date() - mViewItemJobStepStartTime) < (3600 * 72)) { 1425 ghi->setInitialExpandStatus(Item::ExpandNeeded); 1426 } 1427 } 1428 break; 1429 default: 1430 // b0rken 1431 break; 1432 } 1433 1434 attachMessageToParent(ghi, mi); 1435 1436 attachGroup(ghi); // this will expand the group if required 1437 1438 mGroupHeaderItemHash.insert(groupLabel, ghi); 1439 } else { 1440 // the group was already there (certainly viewable) 1441 1442 // This function may be also called to re-group a message. 1443 // That is, to eventually find a new group for a message that has changed 1444 // its properties (but was already attached to a group). 1445 // So it may happen that we find out that in fact re-grouping wasn't really 1446 // needed because the message is already in the correct group. 1447 if (mi->parent() == ghi) { 1448 return; // nothing to be done 1449 } 1450 1451 attachMessageToParent(ghi, mi); 1452 } 1453 1454 // Remember this message as a thread leader 1455 mThreadingCache.updateParent(mi, nullptr); 1456 } 1457 1458 MessageItem *ModelPrivate::findMessageParent(MessageItem *mi) 1459 { 1460 Q_ASSERT(mAggregation->threading() != Aggregation::NoThreading); // caller must take care of this 1461 1462 // This function attempts to find a thread parent for the item "mi" 1463 // which actually may already have a children subtree. 1464 1465 // Forged or plain broken message trees are dangerous here. 1466 // For example, a message tree with circular references like 1467 // 1468 // Message mi, Id=1, In-Reply-To=2 1469 // Message childOfMi, Id=2, In-Reply-To=1 1470 // 1471 // is perfectly possible and will cause us to find childOfMi 1472 // as parent of mi. This will then create a loop in the message tree 1473 // (which will then no longer be a tree in fact) and cause us to freeze 1474 // once we attempt to climb the parents. We need to take care of that. 1475 1476 bool bMessageWasThreadable = false; 1477 MessageItem *pParent; 1478 1479 // First of all try to find a "perfect parent", that is the message for that 1480 // we have the ID in the "In-Reply-To" field. This is actually done by using 1481 // MD5 caches of the message ids because of speed. Collisions are very unlikely. 1482 1483 QByteArray md5 = mi->inReplyToIdMD5(); 1484 if (!md5.isEmpty()) { 1485 // have an In-Reply-To field MD5 1486 pParent = mThreadingCacheMessageIdMD5ToMessageItem.value(md5, nullptr); 1487 if (pParent) { 1488 // Take care of circular references 1489 if ((mi == pParent) // self referencing message 1490 || ((mi->childItemCount() > 0) // mi already has children, this is fast to determine 1491 && pParent->hasAncestor(mi) // pParent is in the mi's children tree 1492 )) { 1493 qCWarning(MESSAGELIST_LOG) << "Circular In-Reply-To reference loop detected in the message tree"; 1494 mi->setThreadingStatus(MessageItem::NonThreadable); 1495 return nullptr; // broken message: throw it away 1496 } 1497 mi->setThreadingStatus(MessageItem::PerfectParentFound); 1498 return pParent; // got a perfect parent for this message 1499 } 1500 1501 // got no perfect parent 1502 bMessageWasThreadable = true; // but the message was threadable 1503 } 1504 1505 if (mAggregation->threading() == Aggregation::PerfectOnly) { 1506 mi->setThreadingStatus(bMessageWasThreadable ? MessageItem::ParentMissing : MessageItem::NonThreadable); 1507 return nullptr; // we're doing only perfect parent matches 1508 } 1509 1510 // Try to use the "References" field. In fact we have the MD5 of the 1511 // (n-1)th entry in References. 1512 // 1513 // Original rationale from KMHeaders: 1514 // 1515 // If we don't have a replyToId, or if we have one and the 1516 // corresponding message is not in this folder, as happens 1517 // if you keep your outgoing messages in an OUTBOX, for 1518 // example, try the list of references, because the second 1519 // to last will likely be in this folder. replyToAuxIdMD5 1520 // contains the second to last one. 1521 1522 md5 = mi->referencesIdMD5(); 1523 if (!md5.isEmpty()) { 1524 pParent = mThreadingCacheMessageIdMD5ToMessageItem.value(md5, nullptr); 1525 if (pParent) { 1526 // Take care of circular references 1527 if ((mi == pParent) // self referencing message 1528 || ((mi->childItemCount() > 0) // mi already has children, this is fast to determine 1529 && pParent->hasAncestor(mi) // pParent is in the mi's children tree 1530 )) { 1531 qCWarning(MESSAGELIST_LOG) << "Circular reference loop detected in the message tree"; 1532 mi->setThreadingStatus(MessageItem::NonThreadable); 1533 return nullptr; // broken message: throw it away 1534 } 1535 mi->setThreadingStatus(MessageItem::ImperfectParentFound); 1536 return pParent; // got an imperfect parent for this message 1537 } 1538 1539 auto messagesWithTheSameReferences = mThreadingCacheMessageReferencesIdMD5ToMessageItem.value(md5, nullptr); 1540 if (messagesWithTheSameReferences) { 1541 Q_ASSERT(!messagesWithTheSameReferences->isEmpty()); 1542 1543 pParent = messagesWithTheSameReferences->first(); 1544 if (mi != pParent && (mi->childItemCount() == 0 || !pParent->hasAncestor(mi))) { 1545 mi->setThreadingStatus(MessageItem::ImperfectParentFound); 1546 return pParent; 1547 } 1548 } 1549 1550 // got no imperfect parent 1551 bMessageWasThreadable = true; // but the message was threadable 1552 } 1553 1554 if (mAggregation->threading() == Aggregation::PerfectAndReferences) { 1555 mi->setThreadingStatus(bMessageWasThreadable ? MessageItem::ParentMissing : MessageItem::NonThreadable); 1556 return nullptr; // we're doing only perfect parent matches 1557 } 1558 1559 Q_ASSERT(mAggregation->threading() == Aggregation::PerfectReferencesAndSubject); 1560 1561 // We are supposed to do subject based threading but we can't do it now. 1562 // This is because the subject based threading *may* be wrong and waste 1563 // time by creating circular references (that we'd need to detect and fix). 1564 // We first try the perfect and references based threading on all the messages 1565 // and then run subject based threading only on the remaining ones. 1566 1567 mi->setThreadingStatus((bMessageWasThreadable || mi->subjectIsPrefixed()) ? MessageItem::ParentMissing : MessageItem::NonThreadable); 1568 return nullptr; 1569 } 1570 1571 // Subject threading cache stuff 1572 1573 #if 0 1574 // Debug helpers 1575 void dump_iterator_and_list(QList< MessageItem * >::Iterator &iter, QList< MessageItem * > *list) 1576 { 1577 qCDebug(MESSAGELIST_LOG) << "Threading cache part dump"; 1578 if (iter == list->end()) { 1579 qCDebug(MESSAGELIST_LOG) << "Iterator pointing to end of the list"; 1580 } else { 1581 qCDebug(MESSAGELIST_LOG) << "Iterator pointing to " << *iter << " subject [" << (*iter)->subject() << "] date [" << (*iter)->date() << "]"; 1582 } 1583 1584 for (QList< MessageItem * >::Iterator it = list->begin(); it != list->end(); ++it) { 1585 qCDebug(MESSAGELIST_LOG) << "List element " << *it << " subject [" << (*it)->subject() << "] date [" << (*it)->date() << "]"; 1586 } 1587 1588 qCDebug(MESSAGELIST_LOG) << "End of threading cache part dump"; 1589 } 1590 1591 void dump_list(QList< MessageItem * > *list) 1592 { 1593 qCDebug(MESSAGELIST_LOG) << "Threading cache part dump"; 1594 1595 for (QList< MessageItem * >::Iterator it = list->begin(); it != list->end(); ++it) { 1596 qCDebug(MESSAGELIST_LOG) << "List element " << *it << " subject [" << (*it)->subject() << "] date [" << (*it)->date() << "]"; 1597 } 1598 1599 qCDebug(MESSAGELIST_LOG) << "End of threading cache part dump"; 1600 } 1601 1602 #endif // debug helpers 1603 1604 // a helper class used in a qLowerBound() call below 1605 class MessageLessThanByDate 1606 { 1607 public: 1608 inline bool operator()(const MessageItem *mi1, const MessageItem *mi2) const 1609 { 1610 if (mi1->date() < mi2->date()) { // likely 1611 return true; 1612 } 1613 if (mi1->date() > mi2->date()) { // likely 1614 return false; 1615 } 1616 // dates are equal, compare by pointer 1617 return mi1 < mi2; 1618 } 1619 }; 1620 1621 void ModelPrivate::addMessageToReferencesBasedThreadingCache(MessageItem *mi) 1622 { 1623 // Messages in this cache are sorted by date, and if dates are equal then they are sorted by pointer value. 1624 // Sorting by date is used to optimize the parent lookup in guessMessageParent() below. 1625 1626 // WARNING: If the message date changes for some reason (like in the "update" step) 1627 // then the cache may become unsorted. For this reason the message about to 1628 // be changed must be first removed from the cache and then reinserted. 1629 1630 auto messagesWithTheSameReference = mThreadingCacheMessageReferencesIdMD5ToMessageItem.value(mi->referencesIdMD5(), nullptr); 1631 1632 if (!messagesWithTheSameReference) { 1633 messagesWithTheSameReference = new QList<MessageItem *>(); 1634 mThreadingCacheMessageReferencesIdMD5ToMessageItem.insert(mi->referencesIdMD5(), messagesWithTheSameReference); 1635 messagesWithTheSameReference->append(mi); 1636 return; 1637 } 1638 1639 // Found: assert that we have no duplicates in the cache. 1640 Q_ASSERT(!messagesWithTheSameReference->contains(mi)); 1641 1642 // Ordered insert: first by date then by pointer value. 1643 auto it = std::lower_bound(messagesWithTheSameReference->begin(), messagesWithTheSameReference->end(), mi, MessageLessThanByDate()); 1644 messagesWithTheSameReference->insert(it, mi); 1645 } 1646 1647 void ModelPrivate::removeMessageFromReferencesBasedThreadingCache(MessageItem *mi) 1648 { 1649 // We assume that the caller knows what he is doing and the message is actually in the cache. 1650 // If the message isn't in the cache then we should not be called at all. 1651 1652 auto messagesWithTheSameReference = mThreadingCacheMessageReferencesIdMD5ToMessageItem.value(mi->referencesIdMD5(), nullptr); 1653 1654 // We assume that the message is there so the list must be non null. 1655 Q_ASSERT(messagesWithTheSameReference); 1656 1657 // The cache *MUST* be ordered first by date then by pointer value 1658 auto it = std::lower_bound(messagesWithTheSameReference->begin(), messagesWithTheSameReference->end(), mi, MessageLessThanByDate()); 1659 1660 // The binary based search must have found a message 1661 Q_ASSERT(it != messagesWithTheSameReference->end()); 1662 1663 // and it must have found exactly the message requested 1664 Q_ASSERT(*it == mi); 1665 1666 // Kill it 1667 messagesWithTheSameReference->erase(it); 1668 1669 // And kill the list if it was the last one 1670 if (messagesWithTheSameReference->isEmpty()) { 1671 mThreadingCacheMessageReferencesIdMD5ToMessageItem.remove(mi->referencesIdMD5()); 1672 delete messagesWithTheSameReference; 1673 } 1674 } 1675 1676 void ModelPrivate::addMessageToSubjectBasedThreadingCache(MessageItem *mi) 1677 { 1678 // Messages in this cache are sorted by date, and if dates are equal then they are sorted by pointer value. 1679 // Sorting by date is used to optimize the parent lookup in guessMessageParent() below. 1680 1681 // WARNING: If the message date changes for some reason (like in the "update" step) 1682 // then the cache may become unsorted. For this reason the message about to 1683 // be changed must be first removed from the cache and then reinserted. 1684 1685 // Lookup the list of messages with the same stripped subject 1686 auto messagesWithTheSameStrippedSubject = mThreadingCacheMessageSubjectMD5ToMessageItem.value(mi->strippedSubjectMD5(), nullptr); 1687 1688 if (!messagesWithTheSameStrippedSubject) { 1689 // Not there yet: create it and append. 1690 messagesWithTheSameStrippedSubject = new QList<MessageItem *>(); 1691 mThreadingCacheMessageSubjectMD5ToMessageItem.insert(mi->strippedSubjectMD5(), messagesWithTheSameStrippedSubject); 1692 messagesWithTheSameStrippedSubject->append(mi); 1693 return; 1694 } 1695 1696 // Found: assert that we have no duplicates in the cache. 1697 Q_ASSERT(!messagesWithTheSameStrippedSubject->contains(mi)); 1698 1699 // Ordered insert: first by date then by pointer value. 1700 auto it = std::lower_bound(messagesWithTheSameStrippedSubject->begin(), messagesWithTheSameStrippedSubject->end(), mi, MessageLessThanByDate()); 1701 messagesWithTheSameStrippedSubject->insert(it, mi); 1702 } 1703 1704 void ModelPrivate::removeMessageFromSubjectBasedThreadingCache(MessageItem *mi) 1705 { 1706 // We assume that the caller knows what he is doing and the message is actually in the cache. 1707 // If the message isn't in the cache then we should not be called at all. 1708 // 1709 // The game is called "performance" 1710 1711 // Grab the list of all the messages with the same stripped subject (all potential parents) 1712 auto messagesWithTheSameStrippedSubject = mThreadingCacheMessageSubjectMD5ToMessageItem.value(mi->strippedSubjectMD5(), nullptr); 1713 1714 // We assume that the message is there so the list must be non null. 1715 Q_ASSERT(messagesWithTheSameStrippedSubject); 1716 1717 // The cache *MUST* be ordered first by date then by pointer value 1718 auto it = std::lower_bound(messagesWithTheSameStrippedSubject->begin(), messagesWithTheSameStrippedSubject->end(), mi, MessageLessThanByDate()); 1719 1720 // The binary based search must have found a message 1721 Q_ASSERT(it != messagesWithTheSameStrippedSubject->end()); 1722 1723 // and it must have found exactly the message requested 1724 Q_ASSERT(*it == mi); 1725 1726 // Kill it 1727 messagesWithTheSameStrippedSubject->erase(it); 1728 1729 // And kill the list if it was the last one 1730 if (messagesWithTheSameStrippedSubject->isEmpty()) { 1731 mThreadingCacheMessageSubjectMD5ToMessageItem.remove(mi->strippedSubjectMD5()); 1732 delete messagesWithTheSameStrippedSubject; 1733 } 1734 } 1735 1736 MessageItem *ModelPrivate::guessMessageParent(MessageItem *mi) 1737 { 1738 // This function implements subject based threading 1739 // It attempts to guess a thread parent for the item "mi" 1740 // which actually may already have a children subtree. 1741 1742 // We have all the problems of findMessageParent() plus the fact that 1743 // we're actually guessing (and often we may be *wrong*). 1744 1745 Q_ASSERT(mAggregation->threading() == Aggregation::PerfectReferencesAndSubject); // caller must take care of this 1746 Q_ASSERT(mi->subjectIsPrefixed()); // caller must take care of this 1747 Q_ASSERT(mi->threadingStatus() == MessageItem::ParentMissing); 1748 1749 // Do subject based threading 1750 const QByteArray md5 = mi->strippedSubjectMD5(); 1751 if (!md5.isEmpty()) { 1752 auto messagesWithTheSameStrippedSubject = mThreadingCacheMessageSubjectMD5ToMessageItem.value(md5, nullptr); 1753 1754 if (messagesWithTheSameStrippedSubject) { 1755 Q_ASSERT(!messagesWithTheSameStrippedSubject->isEmpty()); 1756 1757 // Need to find the message with the maximum date lower than the one of this message 1758 1759 auto maxTime = (time_t)0; 1760 MessageItem *pParent = nullptr; 1761 1762 // Here'we re really guessing so circular references are possible 1763 // even on perfectly valid trees. This is why we don't consider it 1764 // an error but just continue searching. 1765 1766 // FIXME: This might be speed up with an initial binary search (?) 1767 // ANSWER: No. We can't rely on date order (as it can be updated on the fly...) 1768 for (const auto it : std::as_const(*messagesWithTheSameStrippedSubject)) { 1769 int delta = mi->date() - it->date(); 1770 1771 // We don't take into account messages with a delta smaller than 120. 1772 // Assuming that our date() values are correct (that is, they take into 1773 // account timezones etc..) then one usually needs more than 120 seconds 1774 // to answer to a message. Better safe than sorry. 1775 1776 // This check also includes negative deltas so messages later than mi aren't considered 1777 1778 if (delta < 120) { 1779 break; // The list is ordered by date (ascending) so we can stop searching here 1780 } 1781 1782 // About the "magic" 3628899 value here comes a Till's comment from the original KMHeaders: 1783 // 1784 // "Parents more than six weeks older than the message are not accepted. The reasoning being 1785 // that if a new message with the same subject turns up after such a long time, the chances 1786 // that it is still part of the same thread are slim. The value of six weeks is chosen as a 1787 // result of a poll conducted on kde-devel, so it's probably bogus. :)" 1788 1789 if (delta < 3628899) { 1790 // Compute the closest. 1791 if ((maxTime < it->date())) { 1792 // This algorithm *can* be (and often is) wrong. 1793 // Take care of circular threading which is really possible at this level. 1794 // If mi contains "it" inside its children subtree then we have 1795 // found such a circular threading problem. 1796 1797 // Note that here we can't have it == mi because of the delta >= 120 check above. 1798 1799 if ((mi->childItemCount() == 0) || !it->hasAncestor(mi)) { 1800 maxTime = it->date(); 1801 pParent = it; 1802 } 1803 } 1804 } 1805 } 1806 1807 if (pParent) { 1808 mi->setThreadingStatus(MessageItem::ImperfectParentFound); 1809 return pParent; // got an imperfect parent for this message 1810 } 1811 } 1812 } 1813 1814 return nullptr; 1815 } 1816 1817 // 1818 // A little template helper, hopefully inlineable. 1819 // 1820 // Return true if the specified message item is in the wrong position 1821 // inside the specified parent and needs re-sorting. Return false otherwise. 1822 // Both parent and messageItem must not be null. 1823 // 1824 // Checking if a message needs re-sorting instead of just re-sorting it 1825 // is very useful since re-sorting is an expensive operation. 1826 // 1827 template<class ItemComparator> 1828 static bool messageItemNeedsReSorting(SortOrder::SortDirection messageSortDirection, ItemPrivate *parent, MessageItem *messageItem) 1829 { 1830 if ((messageSortDirection == SortOrder::Ascending) || (parent->mType == Item::Message)) { 1831 return parent->childItemNeedsReSorting<ItemComparator, true>(messageItem); 1832 } 1833 return parent->childItemNeedsReSorting<ItemComparator, false>(messageItem); 1834 } 1835 1836 bool ModelPrivate::handleItemPropertyChanges(int propertyChangeMask, Item *parent, Item *item) 1837 { 1838 // The facts: 1839 // 1840 // - If dates changed: 1841 // - If we're sorting messages by min/max date then at each level the messages might need resorting. 1842 // - If the thread leader is the most recent message of a thread then the uppermost 1843 // message of the thread might need re-grouping. 1844 // - If the groups are sorted by min/max date then the group might need re-sorting too. 1845 // 1846 // This function explicitly doesn't re-apply the filter when ActionItemStatus changes. 1847 // This is because filters must be re-applied due to a broader range of status variations: 1848 // this is done in viewItemJobStepInternalForJobPass1Update() instead (which is the only 1849 // place in that ActionItemStatus may be set). 1850 1851 if (parent->type() == Item::InvisibleRoot) { 1852 // item is either a message or a group attached to the root. 1853 // It might need resorting. 1854 if (item->type() == Item::GroupHeader) { 1855 // item is a group header attached to the root. 1856 if (( 1857 // max date changed 1858 (propertyChangeMask & MaxDateChanged) && // groups sorted by max date 1859 (mSortOrder->groupSorting() == SortOrder::SortGroupsByDateTimeOfMostRecent)) 1860 || ( 1861 // date changed 1862 (propertyChangeMask & DateChanged) && // groups sorted by date 1863 (mSortOrder->groupSorting() == SortOrder::SortGroupsByDateTime))) { 1864 // This group might need re-sorting. 1865 1866 // Groups are large container of messages so it's likely that 1867 // another message inserted will cause this group to be marked again. 1868 // So we wait until the end to do the grand final re-sorting: it will be done in Pass4. 1869 mGroupHeadersThatNeedUpdate.insert(static_cast<GroupHeaderItem *>(item), static_cast<GroupHeaderItem *>(item)); 1870 } 1871 } else { 1872 // item is a message. It might need re-sorting. 1873 1874 // Since sorting is an expensive operation, we first check if it's *really* needed. 1875 // Re-sorting will actually not change min/max dates at all and 1876 // will not climb up the parent's ancestor tree. 1877 1878 switch (mSortOrder->messageSorting()) { 1879 case SortOrder::SortMessagesByDateTime: 1880 if (propertyChangeMask & DateChanged) { // date changed 1881 if (messageItemNeedsReSorting<ItemDateComparator>(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast<MessageItem *>(item))) { 1882 attachMessageToParent(parent, static_cast<MessageItem *>(item)); 1883 } 1884 } // else date changed, but it doesn't match sorting order: no need to re-sort 1885 break; 1886 case SortOrder::SortMessagesByDateTimeOfMostRecent: 1887 if (propertyChangeMask & MaxDateChanged) { // max date changed 1888 if (messageItemNeedsReSorting<ItemMaxDateComparator>(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast<MessageItem *>(item))) { 1889 attachMessageToParent(parent, static_cast<MessageItem *>(item)); 1890 } 1891 } // else max date changed, but it doesn't match sorting order: no need to re-sort 1892 break; 1893 case SortOrder::SortMessagesByActionItemStatus: 1894 if (propertyChangeMask & ActionItemStatusChanged) { // todo status changed 1895 if (messageItemNeedsReSorting<ItemActionItemStatusComparator>(mSortOrder->messageSortDirection(), 1896 parent->d_ptr, 1897 static_cast<MessageItem *>(item))) { 1898 attachMessageToParent(parent, static_cast<MessageItem *>(item)); 1899 } 1900 } // else to do status changed, but it doesn't match sorting order: no need to re-sort 1901 break; 1902 case SortOrder::SortMessagesByUnreadStatus: 1903 if (propertyChangeMask & UnreadStatusChanged) { // new / unread status changed 1904 if (messageItemNeedsReSorting<ItemUnreadStatusComparator>(mSortOrder->messageSortDirection(), 1905 parent->d_ptr, 1906 static_cast<MessageItem *>(item))) { 1907 attachMessageToParent(parent, static_cast<MessageItem *>(item)); 1908 } 1909 } // else new/unread status changed, but it doesn't match sorting order: no need to re-sort 1910 break; 1911 case SortOrder::SortMessagesByImportantStatus: 1912 if (propertyChangeMask & ImportantStatusChanged) { // important status changed 1913 if (messageItemNeedsReSorting<ItemImportantStatusComparator>(mSortOrder->messageSortDirection(), 1914 parent->d_ptr, 1915 static_cast<MessageItem *>(item))) { 1916 attachMessageToParent(parent, static_cast<MessageItem *>(item)); 1917 } 1918 } // else new/unread status changed, but it doesn't match sorting order: no need to re-sort 1919 break; 1920 case SortOrder::SortMessagesByAttachmentStatus: 1921 if (propertyChangeMask & AttachmentStatusChanged) { // attachment status changed 1922 if (messageItemNeedsReSorting<ItemAttachmentStatusComparator>(mSortOrder->messageSortDirection(), 1923 parent->d_ptr, 1924 static_cast<MessageItem *>(item))) { 1925 attachMessageToParent(parent, static_cast<MessageItem *>(item)); 1926 } 1927 } // else new/unread status changed, but it doesn't match sorting order: no need to re-sort 1928 break; 1929 default: 1930 // this kind of message sorting isn't affected by the property changes: nothing to do. 1931 break; 1932 } 1933 } 1934 1935 return false; // the invisible root isn't affected by any change. 1936 } 1937 1938 if (parent->type() == Item::GroupHeader) { 1939 // item is a message attached to a GroupHeader. 1940 // It might need re-grouping or re-sorting (within the same group) 1941 1942 // Check re-grouping here. 1943 if (( 1944 // max date changed 1945 (propertyChangeMask & MaxDateChanged) && // thread leader is most recent message 1946 (mAggregation->threadLeader() == Aggregation::MostRecentMessage)) 1947 || ( 1948 // date changed 1949 (propertyChangeMask & DateChanged) && // thread leader the topmost message 1950 (mAggregation->threadLeader() == Aggregation::TopmostMessage))) { 1951 // Might really need re-grouping. 1952 // attachMessageToGroupHeader() will find the right group for this message 1953 // and if it's different than the current it will move it. 1954 attachMessageToGroupHeader(static_cast<MessageItem *>(item)); 1955 // Re-grouping fixes the properties of the involved group headers 1956 // so at exit of attachMessageToGroupHeader() the parent can't be affected 1957 // by the change anymore. 1958 return false; 1959 } 1960 1961 // Re-grouping wasn't needed. Re-sorting might be. 1962 } // else item is a message attached to another message and might need re-sorting only. 1963 1964 // Check if message needs re-sorting. 1965 1966 switch (mSortOrder->messageSorting()) { 1967 case SortOrder::SortMessagesByDateTime: 1968 if (propertyChangeMask & DateChanged) { // date changed 1969 if (messageItemNeedsReSorting<ItemDateComparator>(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast<MessageItem *>(item))) { 1970 attachMessageToParent(parent, static_cast<MessageItem *>(item)); 1971 } 1972 } // else date changed, but it doesn't match sorting order: no need to re-sort 1973 break; 1974 case SortOrder::SortMessagesByDateTimeOfMostRecent: 1975 if (propertyChangeMask & MaxDateChanged) { // max date changed 1976 if (messageItemNeedsReSorting<ItemMaxDateComparator>(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast<MessageItem *>(item))) { 1977 attachMessageToParent(parent, static_cast<MessageItem *>(item)); 1978 } 1979 } // else max date changed, but it doesn't match sorting order: no need to re-sort 1980 break; 1981 case SortOrder::SortMessagesByActionItemStatus: 1982 if (propertyChangeMask & ActionItemStatusChanged) { // todo status changed 1983 if (messageItemNeedsReSorting<ItemActionItemStatusComparator>(mSortOrder->messageSortDirection(), 1984 parent->d_ptr, 1985 static_cast<MessageItem *>(item))) { 1986 attachMessageToParent(parent, static_cast<MessageItem *>(item)); 1987 } 1988 } // else to do status changed, but it doesn't match sorting order: no need to re-sort 1989 break; 1990 case SortOrder::SortMessagesByUnreadStatus: 1991 if (propertyChangeMask & UnreadStatusChanged) { // new / unread status changed 1992 if (messageItemNeedsReSorting<ItemUnreadStatusComparator>(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast<MessageItem *>(item))) { 1993 attachMessageToParent(parent, static_cast<MessageItem *>(item)); 1994 } 1995 } // else new/unread status changed, but it doesn't match sorting order: no need to re-sort 1996 break; 1997 case SortOrder::SortMessagesByImportantStatus: 1998 if (propertyChangeMask & ImportantStatusChanged) { // important status changed 1999 if (messageItemNeedsReSorting<ItemImportantStatusComparator>(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast<MessageItem *>(item))) { 2000 attachMessageToParent(parent, static_cast<MessageItem *>(item)); 2001 } 2002 } // else important status changed, but it doesn't match sorting order: no need to re-sort 2003 break; 2004 case SortOrder::SortMessagesByAttachmentStatus: 2005 if (propertyChangeMask & AttachmentStatusChanged) { // attachment status changed 2006 if (messageItemNeedsReSorting<ItemAttachmentStatusComparator>(mSortOrder->messageSortDirection(), 2007 parent->d_ptr, 2008 static_cast<MessageItem *>(item))) { 2009 attachMessageToParent(parent, static_cast<MessageItem *>(item)); 2010 } 2011 } // else important status changed, but it doesn't match sorting order: no need to re-sort 2012 break; 2013 default: 2014 // this kind of message sorting isn't affected by property changes: nothing to do. 2015 break; 2016 } 2017 2018 return true; // parent might be affected too. 2019 } 2020 2021 void ModelPrivate::messageDetachedUpdateParentProperties(Item *oldParent, MessageItem *mi) 2022 { 2023 Q_ASSERT(oldParent); 2024 Q_ASSERT(mi); 2025 Q_ASSERT(oldParent != mRootItem); 2026 2027 // oldParent might have its properties changed because of the child removal. 2028 // propagate the changes up. 2029 for (;;) { 2030 // pParent is not the root item now. This is assured by how we enter this loop 2031 // and by the fact that handleItemPropertyChanges returns false when grandParent 2032 // is Item::InvisibleRoot. We could actually assert it here... 2033 2034 // Check if its dates need an update. 2035 int propertyChangeMask; 2036 2037 if ((mi->maxDate() == oldParent->maxDate()) && oldParent->recomputeMaxDate()) { 2038 propertyChangeMask = MaxDateChanged; 2039 } else { 2040 break; // from the for(;;) loop 2041 } 2042 2043 // One of the oldParent properties has changed for sure 2044 2045 Item *grandParent = oldParent->parent(); 2046 2047 // If there is no grandParent then oldParent isn't attached to the view. 2048 // Re-sorting / re-grouping isn't needed for sure. 2049 if (!grandParent) { 2050 break; // from the for(;;) loop 2051 } 2052 2053 // The following function will return true if grandParent may be affected by the change. 2054 // If the grandParent isn't affected, we stop climbing. 2055 if (!handleItemPropertyChanges(propertyChangeMask, grandParent, oldParent)) { 2056 break; // from the for(;;) loop 2057 } 2058 2059 // Now we need to climb up one level and check again. 2060 oldParent = grandParent; 2061 } // for(;;) loop 2062 2063 // If the last message was removed from a group header then this group will need an update 2064 // for sure. We will need to remove it (unless a message is attached back to it) 2065 if (oldParent->type() == Item::GroupHeader) { 2066 if (oldParent->childItemCount() == 0) { 2067 mGroupHeadersThatNeedUpdate.insert(static_cast<GroupHeaderItem *>(oldParent), static_cast<GroupHeaderItem *>(oldParent)); 2068 } 2069 } 2070 } 2071 2072 void ModelPrivate::propagateItemPropertiesToParent(Item *item) 2073 { 2074 Item *pParent = item->parent(); 2075 Q_ASSERT(pParent); 2076 Q_ASSERT(pParent != mRootItem); 2077 2078 for (;;) { 2079 // pParent is not the root item now. This is assured by how we enter this loop 2080 // and by the fact that handleItemPropertyChanges returns false when grandParent 2081 // is Item::InvisibleRoot. We could actually assert it here... 2082 2083 // Check if its dates need an update. 2084 int propertyChangeMask; 2085 2086 if (item->maxDate() > pParent->maxDate()) { 2087 pParent->setMaxDate(item->maxDate()); 2088 propertyChangeMask = MaxDateChanged; 2089 } else { 2090 // No parent dates have changed: no further work is needed. Stop climbing here. 2091 break; // from the for(;;) loop 2092 } 2093 2094 // One of the pParent properties has changed. 2095 2096 Item *grandParent = pParent->parent(); 2097 2098 // If there is no grandParent then pParent isn't attached to the view. 2099 // Re-sorting / re-grouping isn't needed for sure. 2100 if (!grandParent) { 2101 break; // from the for(;;) loop 2102 } 2103 2104 // The following function will return true if grandParent may be affected by the change. 2105 // If the grandParent isn't affected, we stop climbing. 2106 if (!handleItemPropertyChanges(propertyChangeMask, grandParent, pParent)) { 2107 break; // from the for(;;) loop 2108 } 2109 2110 // Now we need to climb up one level and check again. 2111 pParent = grandParent; 2112 } // for(;;) 2113 } 2114 2115 void ModelPrivate::attachMessageToParent(Item *pParent, MessageItem *mi, AttachOptions attachOptions) 2116 { 2117 Q_ASSERT(pParent); 2118 Q_ASSERT(mi); 2119 2120 // This function may be called to do a simple "re-sort" of the item inside the parent. 2121 // In that case mi->parent() is equal to pParent. 2122 bool oldParentWasTheSame; 2123 2124 if (mi->parent()) { 2125 Item *oldParent = mi->parent(); 2126 2127 // The item already had a parent and this means that we're moving it. 2128 oldParentWasTheSame = oldParent == pParent; // just re-sorting ? 2129 2130 if (mi->isViewable()) { // is actually 2131 // The message is actually attached to the viewable root 2132 2133 // Unfortunately we need to hack the model/view architecture 2134 // since it's somewhat flawed in this. At the moment of writing 2135 // there is simply no way to atomically move a subtree. 2136 // We must detach, call beginRemoveRows()/endRemoveRows(), 2137 // save the expanded state, save the selection, save the current item, 2138 // save the view position (YES! As we are removing items the view 2139 // will hopelessly jump around so we're just FORCED to break 2140 // the isolation from the view)... 2141 // ...*then* reattach, restore the expanded state, restore the selection, 2142 // restore the current item, restore the view position and pray 2143 // that nothing will fail in the (rather complicated) process.... 2144 2145 // Yet more unfortunately, while saving the expanded state might stop 2146 // at a certain (unexpanded) point in the tree, saving the selection 2147 // is hopelessly recursive down to the bare leafs. 2148 2149 // Furthermore the expansion of items is a common case while selection 2150 // in the subtree is rare, so saving it would be a huge cost with 2151 // a low revenue. 2152 2153 // This is why we just let the selection screw up. I hereby refuse to call 2154 // yet another expensive recursive function here :D 2155 2156 // The current item saving can be somewhat optimized doing it once for 2157 // a single job step... 2158 2159 if (((mi)->childItemCount() > 0) // has children 2160 && mModelForItemFunctions // the UI is not actually disconnected 2161 && mView->isExpanded(q->index(mi, 0)) // is actually expanded 2162 ) { 2163 saveExpandedStateOfSubtree(mi); 2164 } 2165 } 2166 2167 // If the parent is viewable (so mi was viewable too) then the beginRemoveRows() 2168 // and endRemoveRows() functions of this model will be called too. 2169 oldParent->takeChildItem(mModelForItemFunctions, mi); 2170 2171 if ((!oldParentWasTheSame) && (oldParent != mRootItem)) { 2172 messageDetachedUpdateParentProperties(oldParent, mi); 2173 } 2174 } else { 2175 // The item had no parent yet. 2176 oldParentWasTheSame = false; 2177 } 2178 2179 // Take care of perfect / imperfect threading. 2180 // Items that are now perfectly threaded, but already have a different parent 2181 // might have been imperfectly threaded before. Remove them from the caches. 2182 // Items that are now imperfectly threaded must be added to the caches. 2183 // 2184 // If we're just re-sorting the item inside the same parent then the threading 2185 // caches don't need to be updated (since they actually depend on the parent). 2186 2187 if (!oldParentWasTheSame) { 2188 switch (mi->threadingStatus()) { 2189 case MessageItem::PerfectParentFound: 2190 if (!mi->inReplyToIdMD5().isEmpty()) { 2191 mThreadingCacheMessageInReplyToIdMD5ToMessageItem.remove(mi->inReplyToIdMD5(), mi); 2192 } 2193 if (attachOptions == StoreInCache && pParent->type() == Item::Message) { 2194 mThreadingCache.updateParent(mi, static_cast<MessageItem *>(pParent)); 2195 } 2196 break; 2197 case MessageItem::ImperfectParentFound: 2198 case MessageItem::ParentMissing: // may be: temporary or just fallback assignment 2199 if (!mi->inReplyToIdMD5().isEmpty()) { 2200 if (!mThreadingCacheMessageInReplyToIdMD5ToMessageItem.contains(mi->inReplyToIdMD5(), mi)) { 2201 mThreadingCacheMessageInReplyToIdMD5ToMessageItem.insert(mi->inReplyToIdMD5(), mi); 2202 } 2203 } 2204 break; 2205 case MessageItem::NonThreadable: // this also happens when we do no threading at all 2206 // make gcc happy 2207 Q_ASSERT(!mThreadingCacheMessageInReplyToIdMD5ToMessageItem.contains(mi->inReplyToIdMD5(), mi)); 2208 break; 2209 } 2210 } 2211 2212 // Set the new parent 2213 mi->setParent(pParent); 2214 2215 // Propagate watched and ignored status 2216 if ((pParent->status().toQInt32() & mCachedWatchedOrIgnoredStatusBits) // unlikely 2217 && (pParent->type() == Item::Message) // likely 2218 ) { 2219 // the parent is either watched or ignored: propagate to the child 2220 if (pParent->status().isWatched()) { 2221 int row = mInvariantRowMapper->modelInvariantIndexToModelIndexRow(mi); 2222 mi->setStatus(Akonadi::MessageStatus::statusWatched()); 2223 mStorageModel->setMessageItemStatus(mi, row, Akonadi::MessageStatus::statusWatched()); 2224 } else if (pParent->status().isIgnored()) { 2225 int row = mInvariantRowMapper->modelInvariantIndexToModelIndexRow(mi); 2226 mi->setStatus(Akonadi::MessageStatus::statusIgnored()); 2227 mStorageModel->setMessageItemStatus(mi, row, Akonadi::MessageStatus::statusIgnored()); 2228 } 2229 } 2230 2231 // And insert into its child list 2232 2233 // If pParent is viewable then the insert/append functions will call this model's 2234 // beginInsertRows() and endInsertRows() functions. This is EXTREMELY 2235 // expensive and ugly but it's the only way with the Qt4 imposed Model/View method. 2236 // Dude... (citation from Lost, if it wasn't clear). 2237 2238 // I'm using a macro since it does really improve readability. 2239 // I'm NOT using a helper function since gcc will refuse to inline some of 2240 // the calls because they make this function grow too much. 2241 #define INSERT_MESSAGE_WITH_COMPARATOR(_ItemComparator) \ 2242 if ((mSortOrder->messageSortDirection() == SortOrder::Ascending) || (pParent->type() == Item::Message)) { \ 2243 pParent->d_ptr->insertChildItem<_ItemComparator, true>(mModelForItemFunctions, mi); \ 2244 } else { \ 2245 pParent->d_ptr->insertChildItem<_ItemComparator, false>(mModelForItemFunctions, mi); \ 2246 } 2247 2248 // If pParent is viewable then the insertion call will also set the child state to viewable. 2249 // Since mi MAY have children, then this call may make them viewable. 2250 switch (mSortOrder->messageSorting()) { 2251 case SortOrder::SortMessagesByDateTime: 2252 INSERT_MESSAGE_WITH_COMPARATOR(ItemDateComparator) 2253 break; 2254 case SortOrder::SortMessagesByDateTimeOfMostRecent: 2255 INSERT_MESSAGE_WITH_COMPARATOR(ItemMaxDateComparator) 2256 break; 2257 case SortOrder::SortMessagesBySize: 2258 INSERT_MESSAGE_WITH_COMPARATOR(ItemSizeComparator) 2259 break; 2260 case SortOrder::SortMessagesBySenderOrReceiver: 2261 INSERT_MESSAGE_WITH_COMPARATOR(ItemSenderOrReceiverComparator) 2262 break; 2263 case SortOrder::SortMessagesBySender: 2264 INSERT_MESSAGE_WITH_COMPARATOR(ItemSenderComparator) 2265 break; 2266 case SortOrder::SortMessagesByReceiver: 2267 INSERT_MESSAGE_WITH_COMPARATOR(ItemReceiverComparator) 2268 break; 2269 case SortOrder::SortMessagesBySubject: 2270 INSERT_MESSAGE_WITH_COMPARATOR(ItemSubjectComparator) 2271 break; 2272 case SortOrder::SortMessagesByActionItemStatus: 2273 INSERT_MESSAGE_WITH_COMPARATOR(ItemActionItemStatusComparator) 2274 break; 2275 case SortOrder::SortMessagesByUnreadStatus: 2276 INSERT_MESSAGE_WITH_COMPARATOR(ItemUnreadStatusComparator) 2277 break; 2278 case SortOrder::SortMessagesByImportantStatus: 2279 INSERT_MESSAGE_WITH_COMPARATOR(ItemImportantStatusComparator) 2280 break; 2281 case SortOrder::SortMessagesByAttachmentStatus: 2282 INSERT_MESSAGE_WITH_COMPARATOR(ItemAttachmentStatusComparator) 2283 break; 2284 case SortOrder::NoMessageSorting: 2285 pParent->appendChildItem(mModelForItemFunctions, mi); 2286 break; 2287 default: // should never happen 2288 pParent->appendChildItem(mModelForItemFunctions, mi); 2289 break; 2290 } 2291 2292 // Decide if we need to expand parents 2293 bool childNeedsExpanding = (mi->initialExpandStatus() == Item::ExpandNeeded); 2294 2295 if (pParent->initialExpandStatus() == Item::NoExpandNeeded) { 2296 switch (mAggregation->threadExpandPolicy()) { 2297 case Aggregation::NeverExpandThreads: 2298 // just do nothing unless this child has children and is already marked for expansion 2299 if (childNeedsExpanding) { 2300 pParent->setInitialExpandStatus(Item::ExpandNeeded); 2301 } 2302 break; 2303 case Aggregation::ExpandThreadsWithNewMessages: // No more new status. fall through to unread if it exists in config 2304 case Aggregation::ExpandThreadsWithUnreadMessages: 2305 // expand only if unread (or it has children marked for expansion) 2306 if (childNeedsExpanding || !mi->status().isRead()) { 2307 pParent->setInitialExpandStatus(Item::ExpandNeeded); 2308 } 2309 break; 2310 case Aggregation::ExpandThreadsWithUnreadOrImportantMessages: 2311 // expand only if unread, important or todo (or it has children marked for expansion) 2312 // FIXME: Wouldn't it be nice to be able to test for bitmasks in MessageStatus ? 2313 if (childNeedsExpanding || !mi->status().isRead() || mi->status().isImportant() || mi->status().isToAct()) { 2314 pParent->setInitialExpandStatus(Item::ExpandNeeded); 2315 } 2316 break; 2317 case Aggregation::AlwaysExpandThreads: 2318 // expand everything 2319 pParent->setInitialExpandStatus(Item::ExpandNeeded); 2320 break; 2321 default: 2322 // BUG 2323 break; 2324 } 2325 } // else it's already marked for expansion or expansion has been already executed 2326 2327 // expand parent first, if possible 2328 if (pParent->initialExpandStatus() == Item::ExpandNeeded) { 2329 // If UI is not disconnected and parent is viewable, go up and expand 2330 if (mModelForItemFunctions && pParent->isViewable()) { 2331 // Now expand parents as needed 2332 Item *parentToExpand = pParent; 2333 while (parentToExpand) { 2334 if (parentToExpand == mRootItem) { 2335 break; // no need to set it expanded 2336 } 2337 // parentToExpand is surely viewable (because this item is) 2338 if (parentToExpand->initialExpandStatus() == Item::ExpandExecuted) { 2339 break; 2340 } 2341 2342 mView->expand(q->index(parentToExpand, 0)); 2343 2344 parentToExpand->setInitialExpandStatus(Item::ExpandExecuted); 2345 parentToExpand = parentToExpand->parent(); 2346 } 2347 } else { 2348 // It isn't viewable or UI is disconnected: climb up marking only 2349 Item *parentToExpand = pParent->parent(); 2350 while (parentToExpand) { 2351 if (parentToExpand == mRootItem) { 2352 break; // no need to set it expanded 2353 } 2354 parentToExpand->setInitialExpandStatus(Item::ExpandNeeded); 2355 parentToExpand = parentToExpand->parent(); 2356 } 2357 } 2358 } 2359 2360 if (mi->isViewable()) { 2361 // mi is now viewable 2362 2363 // sync subtree expanded status 2364 if (childNeedsExpanding) { 2365 if (mi->childItemCount() > 0) { 2366 if (mModelForItemFunctions) { // the UI is not disconnected 2367 syncExpandedStateOfSubtree(mi); // sync the real state in the view 2368 } 2369 } 2370 } 2371 2372 // apply the filter, if needed 2373 if (mFilter) { 2374 Q_ASSERT(mModelForItemFunctions); // the UI must be NOT disconnected here 2375 2376 // apply the filter to subtree 2377 if (applyFilterToSubtree(mi, q->index(pParent, 0))) { 2378 // mi matched, expand parents (unconditionally) 2379 mView->ensureDisplayedWithParentsExpanded(mi); 2380 } 2381 } 2382 } 2383 2384 // Now we need to propagate the property changes the upper levels. 2385 2386 // If we have just inserted a message inside the root then no work needs to be done: 2387 // no grouping is in effect and the message is already in the right place. 2388 if (pParent == mRootItem) { 2389 return; 2390 } 2391 2392 // If we have just removed the item from this parent and re-inserted it 2393 // then this operation was a simple re-sort. The code above didn't update 2394 // the properties when removing the item so we don't actually need 2395 // to make the updates back. 2396 if (oldParentWasTheSame) { 2397 return; 2398 } 2399 2400 // FIXME: OPTIMIZE THIS: First propagate changes THEN syncExpandedStateOfSubtree() 2401 // and applyFilterToSubtree... (needs some thinking though). 2402 2403 // Time to propagate up. 2404 propagateItemPropertiesToParent(mi); 2405 2406 // Aaah.. we're done. Time for a thea ? :) 2407 } 2408 2409 // FIXME: ThreadItem ? 2410 // 2411 // Foo Bar, Joe Thommason, Martin Rox ... Eddie Maiden <date of the thread> 2412 // Title <number of messages>, Last by xxx <inner status> 2413 // 2414 // When messages are added, mark it as dirty only (?) 2415 2416 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass5(ViewItemJob *job, QElapsedTimer elapsedTimer) 2417 { 2418 // In this pass we scan the group headers that are in mGroupHeadersThatNeedUpdate. 2419 // Empty groups get deleted while the other ones are re-sorted. 2420 2421 int curIndex = job->currentIndex(); 2422 2423 auto it = mGroupHeadersThatNeedUpdate.begin(); 2424 auto end = mGroupHeadersThatNeedUpdate.end(); 2425 2426 while (it != end) { 2427 if ((*it)->childItemCount() == 0) { 2428 // group with no children, kill it 2429 (*it)->parent()->takeChildItem(mModelForItemFunctions, *it); 2430 mGroupHeaderItemHash.remove((*it)->label()); 2431 2432 // If we were going to restore its position after the job step, well.. we can't do it anymore. 2433 if (mCurrentItemToRestoreAfterViewItemJobStep == (*it)) { 2434 mCurrentItemToRestoreAfterViewItemJobStep = nullptr; 2435 } 2436 2437 // bye bye 2438 delete *it; 2439 } else { 2440 // Group with children: probably needs re-sorting. 2441 2442 // Re-sorting here is an expensive operation. 2443 // In fact groups have been put in the QHash above on the assumption 2444 // that re-sorting *might* be needed but no real (expensive) check 2445 // has been done yet. Also by sorting a single group we might actually 2446 // put the others in the right place. 2447 // So finally check if re-sorting is *really* needed. 2448 bool needsReSorting; 2449 2450 // A macro really improves readability here. 2451 #define CHECK_IF_GROUP_NEEDS_RESORTING(_ItemDateComparator) \ 2452 switch (mSortOrder->groupSortDirection()) { \ 2453 case SortOrder::Ascending: \ 2454 needsReSorting = (*it)->parent()->d_ptr->childItemNeedsReSorting<_ItemDateComparator, true>(*it); \ 2455 break; \ 2456 case SortOrder::Descending: \ 2457 needsReSorting = (*it)->parent()->d_ptr->childItemNeedsReSorting<_ItemDateComparator, false>(*it); \ 2458 break; \ 2459 default: /* should never happen */ \ 2460 needsReSorting = false; \ 2461 break; \ 2462 } 2463 2464 switch (mSortOrder->groupSorting()) { 2465 case SortOrder::SortGroupsByDateTime: 2466 CHECK_IF_GROUP_NEEDS_RESORTING(ItemDateComparator) 2467 break; 2468 case SortOrder::SortGroupsByDateTimeOfMostRecent: 2469 CHECK_IF_GROUP_NEEDS_RESORTING(ItemMaxDateComparator) 2470 break; 2471 case SortOrder::SortGroupsBySenderOrReceiver: 2472 CHECK_IF_GROUP_NEEDS_RESORTING(ItemSenderOrReceiverComparator) 2473 break; 2474 case SortOrder::SortGroupsBySender: 2475 CHECK_IF_GROUP_NEEDS_RESORTING(ItemSenderComparator) 2476 break; 2477 case SortOrder::SortGroupsByReceiver: 2478 CHECK_IF_GROUP_NEEDS_RESORTING(ItemReceiverComparator) 2479 break; 2480 case SortOrder::NoGroupSorting: 2481 needsReSorting = false; 2482 break; 2483 default: 2484 // Should never happen... just assume re-sorting is not needed 2485 needsReSorting = false; 2486 break; 2487 } 2488 2489 if (needsReSorting) { 2490 attachGroup(*it); // it will first detach and then re-attach in the proper place 2491 } 2492 } 2493 2494 it = mGroupHeadersThatNeedUpdate.erase(it); 2495 2496 curIndex++; 2497 2498 // FIXME: In fact a single update is likely to manipulate 2499 // a subtree with a LOT of messages inside. If interactivity is favored 2500 // we should check the time really more often. 2501 if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) { 2502 if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { 2503 if (it != mGroupHeadersThatNeedUpdate.end()) { 2504 job->setCurrentIndex(curIndex); 2505 return ViewItemJobInterrupted; 2506 } 2507 } 2508 } 2509 } 2510 2511 return ViewItemJobCompleted; 2512 } 2513 2514 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass4(ViewItemJob *job, QElapsedTimer elapsedTimer) 2515 { 2516 // In this pass we scan mUnassignedMessageListForPass4 which now 2517 // contains both items with parents and items without parents. 2518 // We scan mUnassignedMessageList for messages without parent (the ones that haven't been 2519 // attached to the viewable tree yet) and find a suitable group for them. Then we simply 2520 // clear mUnassignedMessageList. 2521 2522 // We call this pass "Grouping" 2523 2524 int curIndex = job->currentIndex(); 2525 int endIndex = job->endIndex(); 2526 2527 while (curIndex <= endIndex) { 2528 MessageItem *mi = mUnassignedMessageListForPass4[curIndex]; 2529 if (!mi->parent()) { 2530 // Unassigned item: thread leader, insert into the proper group. 2531 // Locate the group (or root if no grouping requested) 2532 attachMessageToGroupHeader(mi); 2533 } else { 2534 // A parent was already assigned in Pass3: we have nothing to do here 2535 } 2536 curIndex++; 2537 2538 // FIXME: In fact a single call to attachMessageToGroupHeader() is likely to manipulate 2539 // a subtree with a LOT of messages inside. If interactivity is favored 2540 // we should check the time really more often. 2541 if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) { 2542 if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { 2543 if (curIndex <= endIndex) { 2544 job->setCurrentIndex(curIndex); 2545 return ViewItemJobInterrupted; 2546 } 2547 } 2548 } 2549 } 2550 2551 mUnassignedMessageListForPass4.clear(); 2552 return ViewItemJobCompleted; 2553 } 2554 2555 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass3(ViewItemJob *job, QElapsedTimer elapsedTimer) 2556 { 2557 // In this pass we scan the mUnassignedMessageListForPass3 and try to do construct the threads 2558 // by using subject based threading. If subject based threading is not in effect then 2559 // this pass turns to a nearly-no-op: at the end of Pass2 we have swapped the lists 2560 // and mUnassignedMessageListForPass3 is actually empty. 2561 2562 // We don't shrink the mUnassignedMessageListForPass3 for two reasons: 2563 // - It would mess up this chunked algorithm by shifting indexes 2564 // - mUnassignedMessageList is a QList which is basically an array. It's faster 2565 // to traverse an array of N entries than to remove K>0 entries one by one and 2566 // to traverse the remaining N-K entries. 2567 2568 int curIndex = job->currentIndex(); 2569 int endIndex = job->endIndex(); 2570 2571 while (curIndex <= endIndex) { 2572 // If we're here, then threading is requested for sure. 2573 auto mi = mUnassignedMessageListForPass3[curIndex]; 2574 if ((!mi->parent()) || (mi->threadingStatus() == MessageItem::ParentMissing)) { 2575 // Parent is missing (either "physically" with the item being not attached or "logically" 2576 // with the item being attached to a group or directly to the root. 2577 if (mi->subjectIsPrefixed()) { 2578 // We can try to guess it 2579 auto mparent = guessMessageParent(mi); 2580 2581 if (mparent) { 2582 // imperfect parent found 2583 if (mi->isViewable()) { 2584 // mi was already viewable, we're just trying to re-parent it better... 2585 attachMessageToParent(mparent, mi); 2586 if (!mparent->isViewable()) { 2587 // re-attach it immediately (so current item is not lost) 2588 auto topmost = mparent->topmostMessage(); 2589 Q_ASSERT(!topmost->parent()); // groups are always viewable! 2590 topmost->setThreadingStatus(MessageItem::ParentMissing); 2591 attachMessageToGroupHeader(topmost); 2592 } 2593 } else { 2594 // mi wasn't viewable yet.. no need to attach parent 2595 attachMessageToParent(mparent, mi); 2596 } 2597 // and we're done for now 2598 } else { 2599 // so parent not found, (threadingStatus() is either MessageItem::ParentMissing or MessageItem::NonThreadable) 2600 Q_ASSERT((mi->threadingStatus() == MessageItem::ParentMissing) || (mi->threadingStatus() == MessageItem::NonThreadable)); 2601 mUnassignedMessageListForPass4.append(mi); // this is ~O(1) 2602 // and wait for Pass4 2603 } 2604 } else { 2605 // can't guess the parent as the subject isn't prefixed 2606 Q_ASSERT((mi->threadingStatus() == MessageItem::ParentMissing) || (mi->threadingStatus() == MessageItem::NonThreadable)); 2607 mUnassignedMessageListForPass4.append(mi); // this is ~O(1) 2608 // and wait for Pass4 2609 } 2610 } else { 2611 // Has a parent: either perfect parent already found or non threadable. 2612 // Since we don't end here if mi has status of parent missing then mi must not have imperfect parent. 2613 Q_ASSERT(mi->threadingStatus() != MessageItem::ImperfectParentFound); 2614 Q_ASSERT(mi->isViewable()); 2615 } 2616 2617 curIndex++; 2618 2619 // FIXME: In fact a single call to attachMessageToGroupHeader() is likely to manipulate 2620 // a subtree with a LOT of messages inside. If interactivity is favored 2621 // we should check the time really more often. 2622 if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) { 2623 if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { 2624 if (curIndex <= endIndex) { 2625 job->setCurrentIndex(curIndex); 2626 return ViewItemJobInterrupted; 2627 } 2628 } 2629 } 2630 } 2631 2632 mUnassignedMessageListForPass3.clear(); 2633 return ViewItemJobCompleted; 2634 } 2635 2636 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass2(ViewItemJob *job, QElapsedTimer elapsedTimer) 2637 { 2638 // In this pass we scan the mUnassignedMessageList and try to do construct the threads. 2639 // If some thread leader message got attached to the viewable tree in Pass1Fill then 2640 // we'll also attach all of its children too. The thread leaders we were unable 2641 // to attach in Pass1Fill and their children (which we find here) will make it to the small Pass3 2642 2643 // We don't shrink the mUnassignedMessageList for two reasons: 2644 // - It would mess up this chunked algorithm by shifting indexes 2645 // - mUnassignedMessageList is a QList which is basically an array. It's faster 2646 // to traverse an array of N entries than to remove K>0 entries one by one and 2647 // to traverse the remaining N-K entries. 2648 2649 // We call this pass "Threading" 2650 2651 int curIndex = job->currentIndex(); 2652 int endIndex = job->endIndex(); 2653 2654 while (curIndex <= endIndex) { 2655 // If we're here, then threading is requested for sure. 2656 auto mi = mUnassignedMessageListForPass2[curIndex]; 2657 // The item may or may not have a parent. 2658 // If it has no parent or it has a temporary one (mi->parent() && mi->threadingStatus() == MessageItem::ParentMissing) 2659 // then we attempt to (re-)thread it. Otherwise we just do nothing (the job has already been done by the previous steps). 2660 if ((!mi->parent()) || (mi->threadingStatus() == MessageItem::ParentMissing)) { 2661 qint64 parentId; 2662 auto mparent = mThreadingCache.parentForItem(mi, parentId); 2663 if (mparent && !mparent->hasAncestor(mi)) { 2664 mi->setThreadingStatus(MessageItem::PerfectParentFound); 2665 attachMessageToParent(mparent, mi, SkipCacheUpdate); 2666 } else { 2667 if (parentId > 0) { 2668 // In second pass we have all available Items in mThreadingCache already. If 2669 // mThreadingCache.parentForItem() returns null, but returns valid parentId then 2670 // the Item was removed from Akonadi and our threading cache is out-of-date. 2671 mThreadingCache.expireParent(mi); 2672 mparent = findMessageParent(mi); 2673 } else if (parentId < 0) { 2674 mparent = findMessageParent(mi); 2675 } else { 2676 // parentId = 0: this message is a thread leader so don't 2677 // bother resolving parent, it will be moved directly to 2678 // Pass4 in the code below 2679 } 2680 2681 if (mparent) { 2682 // parent found, either perfect or imperfect 2683 if (mi->isViewable()) { 2684 // mi was already viewable, we're just trying to re-parent it better... 2685 attachMessageToParent(mparent, mi); 2686 if (!mparent->isViewable()) { 2687 // re-attach it immediately (so current item is not lost) 2688 auto topmost = mparent->topmostMessage(); 2689 Q_ASSERT(!topmost->parent()); // groups are always viewable! 2690 topmost->setThreadingStatus(MessageItem::ParentMissing); 2691 attachMessageToGroupHeader(topmost); 2692 } 2693 } else { 2694 // mi wasn't viewable yet.. no need to attach parent 2695 attachMessageToParent(mparent, mi); 2696 } 2697 // and we're done for now 2698 } else { 2699 // so parent not found, (threadingStatus() is either MessageItem::ParentMissing or MessageItem::NonThreadable) 2700 switch (mi->threadingStatus()) { 2701 case MessageItem::ParentMissing: 2702 if (mAggregation->threading() == Aggregation::PerfectReferencesAndSubject) { 2703 // parent missing but still can be found in Pass3 2704 mUnassignedMessageListForPass3.append(mi); // this is ~O(1) 2705 } else { 2706 // We're not doing subject based threading: will never be threaded, go straight to Pass4 2707 mUnassignedMessageListForPass4.append(mi); // this is ~O(1) 2708 } 2709 break; 2710 case MessageItem::NonThreadable: 2711 // will never be threaded, go straight to Pass4 2712 mUnassignedMessageListForPass4.append(mi); // this is ~O(1) 2713 break; 2714 default: 2715 // a bug for sure 2716 qCWarning(MESSAGELIST_LOG) << "ERROR: Invalid message threading status returned by findMessageParent()!"; 2717 Q_ASSERT(false); 2718 break; 2719 } 2720 } 2721 } 2722 } else { 2723 // Has a parent: either perfect parent already found or non threadable. 2724 // Since we don't end here if mi has status of parent missing then mi must not have imperfect parent. 2725 Q_ASSERT(mi->threadingStatus() != MessageItem::ImperfectParentFound); 2726 if (!mi->isViewable()) { 2727 qCWarning(MESSAGELIST_LOG) << "Non viewable message " << mi << " subject " << mi->subject().toUtf8().data(); 2728 Q_ASSERT(mi->isViewable()); 2729 } 2730 } 2731 2732 curIndex++; 2733 2734 // FIXME: In fact a single call to attachMessageToGroupHeader() is likely to manipulate 2735 // a subtree with a LOT of messages inside. If interactivity is favored 2736 // we should check the time really more often. 2737 if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) { 2738 if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { 2739 if (curIndex <= endIndex) { 2740 job->setCurrentIndex(curIndex); 2741 return ViewItemJobInterrupted; 2742 } 2743 } 2744 } 2745 } 2746 2747 mUnassignedMessageListForPass2.clear(); 2748 return ViewItemJobCompleted; 2749 } 2750 2751 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass1Fill(ViewItemJob *job, QElapsedTimer elapsedTimer) 2752 { 2753 // In this pass we scan the a contiguous region of the underlying storage (that is 2754 // assumed to be FLAT) and create the corresponding MessageItem objects. 2755 // The deal is to show items to the user as soon as possible so in this pass we 2756 // *TRY* to attach them to the viewable tree (which is rooted on mRootItem). 2757 // Messages we're unable to attach for some reason (mainly due to threading) get appended 2758 // to mUnassignedMessageList and wait for Pass2. 2759 2760 // We call this pass "Processing" 2761 2762 // Should we use the receiver or the sender field for sorting ? 2763 bool bUseReceiver = mStorageModelContainsOutboundMessages; 2764 2765 // The begin storage index of our work 2766 int curIndex = job->currentIndex(); 2767 // The end storage index of our work. 2768 int endIndex = job->endIndex(); 2769 2770 unsigned long msgToSelect = mPreSelectionMode == PreSelectLastSelected ? mStorageModel->preSelectedMessage() : 0; 2771 2772 MessageItem *mi = nullptr; 2773 2774 while (curIndex <= endIndex) { 2775 // Create the message item with no parent: we'll set it later 2776 if (!mi) { 2777 mi = new MessageItem(); 2778 } else { 2779 // a MessageItem discarded by a previous iteration: reuse it. 2780 Q_ASSERT(mi->parent() == nullptr); 2781 } 2782 2783 if (!mStorageModel->initializeMessageItem(mi, curIndex, bUseReceiver)) { 2784 // ugh 2785 qCWarning(MESSAGELIST_LOG) << "Fill of the MessageItem at storage row index " << curIndex << " failed"; 2786 curIndex++; 2787 continue; 2788 } 2789 2790 // If we're supposed to pre-select a specific message, check if it's this one. 2791 if (msgToSelect != 0 && msgToSelect == mi->uniqueId()) { 2792 // Found, it's this one. 2793 // But actually it's not viewable (so not selectable). We must wait 2794 // until the end of the job to be 100% sure. So here we just translate 2795 // the unique id to a MessageItem pointer and wait. 2796 mLastSelectedMessageInFolder = mi; 2797 msgToSelect = 0; // already found, don't bother checking anymore 2798 } 2799 2800 // Update the newest/oldest message, since we might be supposed to select those later 2801 if (mi->date() != static_cast<uint>(-1)) { 2802 if (!mOldestItem || mOldestItem->date() > mi->date()) { 2803 mOldestItem = mi; 2804 } 2805 if (!mNewestItem || mNewestItem->date() < mi->date()) { 2806 mNewestItem = mi; 2807 } 2808 } 2809 2810 // Ok.. it passed the initial checks: we will not be discarding it. 2811 // Make this message item an invariant index to the underlying model storage. 2812 mInvariantRowMapper->createModelInvariantIndex(curIndex, mi); 2813 2814 // Attempt to do threading as soon as possible (to display items to the user) 2815 if (mAggregation->threading() != Aggregation::NoThreading) { 2816 // Threading is requested 2817 2818 // Fetch the data needed for proper threading 2819 // Add the item to the threading caches 2820 2821 switch (mAggregation->threading()) { 2822 case Aggregation::PerfectReferencesAndSubject: 2823 mStorageModel->fillMessageItemThreadingData(mi, curIndex, StorageModel::PerfectThreadingReferencesAndSubject); 2824 2825 // We also need to build the subject/reference-based threading cache 2826 addMessageToReferencesBasedThreadingCache(mi); 2827 addMessageToSubjectBasedThreadingCache(mi); 2828 break; 2829 case Aggregation::PerfectAndReferences: 2830 mStorageModel->fillMessageItemThreadingData(mi, curIndex, StorageModel::PerfectThreadingPlusReferences); 2831 addMessageToReferencesBasedThreadingCache(mi); 2832 break; 2833 default: 2834 mStorageModel->fillMessageItemThreadingData(mi, curIndex, StorageModel::PerfectThreadingOnly); 2835 break; 2836 } 2837 2838 // Perfect/References threading cache 2839 mThreadingCacheMessageIdMD5ToMessageItem.insert(mi->messageIdMD5(), mi); 2840 2841 // Register the current item into the threading cache 2842 mThreadingCache.addItemToCache(mi); 2843 2844 // First of all look into the persistent cache 2845 qint64 parentId; 2846 Item *pParent = mThreadingCache.parentForItem(mi, parentId); 2847 if (pParent) { 2848 // We already have the parent MessageItem. Attach current message 2849 // to it and mark it as perfect 2850 mi->setThreadingStatus(MessageItem::PerfectParentFound); 2851 attachMessageToParent(pParent, mi); 2852 } else if (parentId > 0) { 2853 // We don't have the parent MessageItem yet, but we do know the 2854 // parent: delay for pass 2 when we will have the parent MessageItem 2855 // for sure. 2856 mi->setThreadingStatus(MessageItem::ParentMissing); 2857 mUnassignedMessageListForPass2.append(mi); 2858 } else if (parentId == 0) { 2859 // Message is a thread leader, skip straight to Pass4 2860 mi->setThreadingStatus(MessageItem::NonThreadable); 2861 mUnassignedMessageListForPass4.append(mi); 2862 } else { 2863 // Check if this item is a perfect parent for some imperfectly threaded 2864 // message (that is actually attached to it, but not necessarily to the 2865 // viewable root). If it is, then remove the imperfect child from its 2866 // current parent rebuild the hierarchy on the fly. 2867 bool needsImmediateReAttach = false; 2868 2869 if (!mThreadingCacheMessageInReplyToIdMD5ToMessageItem.isEmpty()) { // unlikely 2870 const auto lImperfectlyThreaded = mThreadingCacheMessageInReplyToIdMD5ToMessageItem.values(mi->messageIdMD5()); 2871 for (const auto it : lImperfectlyThreaded) { 2872 Q_ASSERT(it->parent()); 2873 Q_ASSERT(it->parent() != mi); 2874 2875 if (!((it->threadingStatus() == MessageItem::ImperfectParentFound) || (it->threadingStatus() == MessageItem::ParentMissing))) { 2876 qCritical() << "Got message " << it << " with threading status" << it->threadingStatus(); 2877 Q_ASSERT_X(false, "ModelPrivate::viewItemJobStepInternalForJobPass1Fill", "Wrong threading status"); 2878 } 2879 2880 // If the item was already attached to the view then 2881 // re-attach it immediately. This will avoid a message 2882 // being displayed for a short while in the view and then 2883 // disappear until a perfect parent isn't found. 2884 if (it->isViewable()) { 2885 needsImmediateReAttach = true; 2886 } 2887 2888 it->setThreadingStatus(MessageItem::PerfectParentFound); 2889 attachMessageToParent(mi, it); 2890 } 2891 } 2892 2893 // FIXME: Might look by "References" too, here... (?) 2894 2895 // Attempt to do threading with anything we already have in caches until now 2896 // Note that this is likely to work since thread-parent messages tend 2897 // to come before thread-children messages in the folders (simply because of 2898 // date of arrival). 2899 2900 // First of all try to find a "perfect parent", that is the message for that 2901 // we have the ID in the "In-Reply-To" field. This is actually done by using 2902 // MD5 caches of the message ids because of speed. Collisions are very unlikely. 2903 2904 const QByteArray md5 = mi->inReplyToIdMD5(); 2905 if (!md5.isEmpty()) { 2906 // Have an In-Reply-To field MD5. 2907 // In well behaved mailing lists 70% of the threadable messages get a parent here :) 2908 pParent = mThreadingCacheMessageIdMD5ToMessageItem.value(md5, nullptr); 2909 2910 if (pParent) { // very likely 2911 // Take care of self-referencing (which is always possible) 2912 // and circular In-Reply-To reference loops which are possible 2913 // in case this item was found to be a perfect parent for some 2914 // imperfectly threaded message just above. 2915 if ((mi == pParent) // self referencing message 2916 || ((mi->childItemCount() > 0) // mi already has children, this is fast to determine 2917 && pParent->hasAncestor(mi) // pParent is in the mi's children tree 2918 )) { 2919 // Bad, bad message.. it has In-Reply-To equal to Message-Id 2920 // or it's in a circular In-Reply-To reference loop. 2921 // Will wait for Pass2 with References-Id only 2922 qCWarning(MESSAGELIST_LOG) << "Circular In-Reply-To reference loop detected in the message tree"; 2923 mUnassignedMessageListForPass2.append(mi); 2924 } else { 2925 // wow, got a perfect parent for this message! 2926 mi->setThreadingStatus(MessageItem::PerfectParentFound); 2927 attachMessageToParent(pParent, mi); 2928 // we're done with this message (also for Pass2) 2929 } 2930 } else { 2931 // got no parent 2932 // will have to wait Pass2 2933 mUnassignedMessageListForPass2.append(mi); 2934 } 2935 } else { 2936 // No In-Reply-To header. 2937 2938 bool mightHaveOtherMeansForThreading; 2939 2940 switch (mAggregation->threading()) { 2941 case Aggregation::PerfectReferencesAndSubject: 2942 mightHaveOtherMeansForThreading = mi->subjectIsPrefixed() || !mi->referencesIdMD5().isEmpty(); 2943 break; 2944 case Aggregation::PerfectAndReferences: 2945 mightHaveOtherMeansForThreading = !mi->referencesIdMD5().isEmpty(); 2946 break; 2947 case Aggregation::PerfectOnly: 2948 mightHaveOtherMeansForThreading = false; 2949 break; 2950 default: 2951 // BUG: there shouldn't be other values (NoThreading is excluded in an upper branch) 2952 Q_ASSERT(false); 2953 mightHaveOtherMeansForThreading = false; // make gcc happy 2954 break; 2955 } 2956 2957 if (mightHaveOtherMeansForThreading) { 2958 // We might have other means for threading this message, wait until Pass2 2959 mUnassignedMessageListForPass2.append(mi); 2960 } else { 2961 // No other means for threading this message. This is either 2962 // a standalone message or a thread leader. 2963 // If there is no grouping in effect or thread leaders are just the "topmost" 2964 // messages then we might be done with this one. 2965 if ((mAggregation->grouping() == Aggregation::NoGrouping) || (mAggregation->threadLeader() == Aggregation::TopmostMessage)) { 2966 // We're done with this message: it will be surely either toplevel (no grouping in effect) 2967 // or a thread leader with a well defined group. Do it :) 2968 // qCDebug(MESSAGELIST_LOG) << "Setting message status from " << mi->threadingStatus() << " to non threadable (1) " << mi; 2969 mi->setThreadingStatus(MessageItem::NonThreadable); 2970 // Locate the parent group for this item 2971 attachMessageToGroupHeader(mi); 2972 // we're done with this message (also for Pass2) 2973 } else { 2974 // Threads belong to the most recent message in the thread. This means 2975 // that we have to wait until Pass2 or Pass3 to assign a group. 2976 mUnassignedMessageListForPass2.append(mi); 2977 } 2978 } 2979 } 2980 2981 if (needsImmediateReAttach && !mi->isViewable()) { 2982 // The item gathered previously viewable children. They must be immediately 2983 // re-shown. So this item must currently be attached to the view. 2984 // This is a temporary measure: it will be probably still moved. 2985 MessageItem *topmost = mi->topmostMessage(); 2986 Q_ASSERT(topmost->threadingStatus() == MessageItem::ParentMissing); 2987 attachMessageToGroupHeader(topmost); 2988 } 2989 } 2990 } else { 2991 // else no threading requested: we don't even need Pass2 2992 // set not threadable status (even if it might be not true, but in this mode we don't care) 2993 // qCDebug(MESSAGELIST_LOG) << "Setting message status from " << mi->threadingStatus() << " to non threadable (2) " << mi; 2994 mi->setThreadingStatus(MessageItem::NonThreadable); 2995 // locate the parent group for this item 2996 if (mAggregation->grouping() == Aggregation::NoGrouping) { 2997 attachMessageToParent(mRootItem, mi); // no groups requested, attach directly to root 2998 } else { 2999 attachMessageToGroupHeader(mi); 3000 } 3001 // we're done with this message (also for Pass2) 3002 } 3003 3004 mi = nullptr; // this item was pushed somewhere, create a new one at next iteration 3005 curIndex++; 3006 3007 if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) { 3008 if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { 3009 if (curIndex <= endIndex) { 3010 job->setCurrentIndex(curIndex); 3011 return ViewItemJobInterrupted; 3012 } 3013 } 3014 } 3015 } 3016 3017 if (mi) { 3018 delete mi; 3019 } 3020 return ViewItemJobCompleted; 3021 } 3022 3023 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass1Cleanup(ViewItemJob *job, QElapsedTimer elapsedTimer) 3024 { 3025 Q_ASSERT(mModelForItemFunctions); // UI must be not disconnected here 3026 // In this pass we remove the MessageItem objects that are present in the job 3027 // and put their children in the unassigned message list. 3028 3029 // Note that this list in fact contains MessageItem objects (we need dynamic_cast<>). 3030 QList<ModelInvariantIndex *> *invalidatedMessages = job->invariantIndexList(); 3031 3032 // We don't shrink the invalidatedMessages because it's basically an array. 3033 // It's faster to traverse an array of N entries than to remove K>0 entries 3034 // one by one and to traverse the remaining N-K entries. 3035 3036 // The begin index of our work 3037 int curIndex = job->currentIndex(); 3038 // The end index of our work. 3039 int endIndex = job->endIndex(); 3040 3041 if (curIndex == job->startIndex()) { 3042 Q_ASSERT(mOrphanChildrenHash.isEmpty()); 3043 } 3044 3045 while (curIndex <= endIndex) { 3046 // Get the underlying storage message data... 3047 auto dyingMessage = dynamic_cast<MessageItem *>(invalidatedMessages->at(curIndex)); 3048 // This MUST NOT be null (otherwise we have a bug somewhere in this file). 3049 Q_ASSERT(dyingMessage); 3050 3051 // If we were going to pre-select this message but we were interrupted 3052 // *before* it was actually made viewable, we just clear the pre-selection pointer 3053 // and unique id (abort pre-selection). 3054 if (dyingMessage == mLastSelectedMessageInFolder) { 3055 mLastSelectedMessageInFolder = nullptr; 3056 mPreSelectionMode = PreSelectNone; 3057 } 3058 3059 // remove the message from any pending user job 3060 if (mPersistentSetManager) { 3061 mPersistentSetManager->removeMessageItemFromAllSets(dyingMessage); 3062 if (mPersistentSetManager->setCount() < 1) { 3063 delete mPersistentSetManager; 3064 mPersistentSetManager = nullptr; 3065 } 3066 } 3067 3068 // Remove the message from threading cache before we start moving up the 3069 // children, so that they don't get mislead by the cache 3070 mThreadingCache.expireParent(dyingMessage); 3071 3072 if (dyingMessage->parent()) { 3073 // Handle saving the current selection: if this item was the current before the step 3074 // then zero it out. We have killed it and it's OK for the current item to change. 3075 3076 if (dyingMessage == mCurrentItemToRestoreAfterViewItemJobStep) { 3077 Q_ASSERT(dyingMessage->isViewable()); 3078 // Try to select the item below the removed one as it helps in doing a "readon" of emails: 3079 // you read a message, decide to delete it and then go to the next. 3080 // Qt tends to select the message above the removed one instead (this is a hardcoded logic in 3081 // QItemSelectionModelPrivate::_q_rowsAboutToBeRemoved()). 3082 mCurrentItemToRestoreAfterViewItemJobStep = mView->messageItemAfter(dyingMessage, MessageTypeAny, false); 3083 3084 if (!mCurrentItemToRestoreAfterViewItemJobStep) { 3085 // There is no item below. Try the item above. 3086 // We still do it better than qt which tends to find the *thread* above 3087 // instead of the item above. 3088 mCurrentItemToRestoreAfterViewItemJobStep = mView->messageItemBefore(dyingMessage, MessageTypeAny, false); 3089 } 3090 3091 Q_ASSERT((!mCurrentItemToRestoreAfterViewItemJobStep) || mCurrentItemToRestoreAfterViewItemJobStep->isViewable()); 3092 } 3093 3094 if (dyingMessage->isViewable() && ((dyingMessage)->childItemCount() > 0) // has children 3095 && mView->isExpanded(q->index(dyingMessage, 0)) // is actually expanded 3096 ) { 3097 saveExpandedStateOfSubtree(dyingMessage); 3098 } 3099 3100 auto oldParent = dyingMessage->parent(); 3101 oldParent->takeChildItem(q, dyingMessage); 3102 3103 // FIXME: This can generate many message movements.. it would be nicer 3104 // to start from messages that are higher in the hierarchy so 3105 // we would need to move less stuff above. 3106 3107 if (oldParent != mRootItem) { 3108 messageDetachedUpdateParentProperties(oldParent, dyingMessage); 3109 } 3110 3111 // We might have already removed its parent from the view, so it 3112 // might already be in the orphan child hash... 3113 if (dyingMessage->threadingStatus() == MessageItem::ParentMissing) { 3114 mOrphanChildrenHash.remove(dyingMessage); // this can turn to a no-op (dyingMessage not present in fact) 3115 } 3116 } else { 3117 // The dying message had no parent: this should happen only if it's already an orphan 3118 3119 Q_ASSERT(dyingMessage->threadingStatus() == MessageItem::ParentMissing); 3120 Q_ASSERT(mOrphanChildrenHash.contains(dyingMessage)); 3121 Q_ASSERT(dyingMessage != mCurrentItemToRestoreAfterViewItemJobStep); 3122 3123 mOrphanChildrenHash.remove(dyingMessage); 3124 } 3125 3126 if (mAggregation->threading() != Aggregation::NoThreading) { 3127 // Threading is requested: remove the message from threading caches. 3128 3129 // Remove from the cache of potential parent items 3130 mThreadingCacheMessageIdMD5ToMessageItem.remove(dyingMessage->messageIdMD5()); 3131 3132 // If we also have a cache for subject/reference-based threading then remove the message from there too 3133 if (mAggregation->threading() == Aggregation::PerfectReferencesAndSubject) { 3134 removeMessageFromReferencesBasedThreadingCache(dyingMessage); 3135 removeMessageFromSubjectBasedThreadingCache(dyingMessage); 3136 } else if (mAggregation->threading() == Aggregation::PerfectAndReferences) { 3137 removeMessageFromReferencesBasedThreadingCache(dyingMessage); 3138 } 3139 3140 // If this message wasn't perfectly parented then it might still be in another cache. 3141 switch (dyingMessage->threadingStatus()) { 3142 case MessageItem::ImperfectParentFound: 3143 case MessageItem::ParentMissing: 3144 if (!dyingMessage->inReplyToIdMD5().isEmpty()) { 3145 mThreadingCacheMessageInReplyToIdMD5ToMessageItem.remove(dyingMessage->inReplyToIdMD5()); 3146 } 3147 break; 3148 default: 3149 Q_ASSERT(!mThreadingCacheMessageInReplyToIdMD5ToMessageItem.contains(dyingMessage->inReplyToIdMD5(), dyingMessage)); 3150 // make gcc happy 3151 break; 3152 } 3153 } 3154 3155 while (auto childItem = dyingMessage->firstChildItem()) { 3156 auto childMessage = dynamic_cast<MessageItem *>(childItem); 3157 Q_ASSERT(childMessage); 3158 3159 dyingMessage->takeChildItem(q, childMessage); 3160 3161 if (mAggregation->threading() != Aggregation::NoThreading) { 3162 if (childMessage->threadingStatus() == MessageItem::PerfectParentFound) { 3163 // If the child message was perfectly parented then now it had 3164 // lost its perfect parent. Add to the cache of imperfectly parented. 3165 if (!childMessage->inReplyToIdMD5().isEmpty()) { 3166 Q_ASSERT(!mThreadingCacheMessageInReplyToIdMD5ToMessageItem.contains(childMessage->inReplyToIdMD5(), childMessage)); 3167 mThreadingCacheMessageInReplyToIdMD5ToMessageItem.insert(childMessage->inReplyToIdMD5(), childMessage); 3168 } 3169 } 3170 } 3171 3172 // Parent is gone 3173 childMessage->setThreadingStatus(MessageItem::ParentMissing); 3174 3175 // If the child (or any message in its subtree) is going to be selected, 3176 // then we must immediately reattach it to a temporary group in order for the 3177 // selection to be preserved across multiple steps. Otherwise we could end 3178 // with the child-to-be-selected being non viewable at the end 3179 // of the view job step. Attach to a temporary group. 3180 if ( 3181 // child is going to be re-selected 3182 (childMessage == mCurrentItemToRestoreAfterViewItemJobStep) 3183 || ( 3184 // there is a message that is going to be re-selected 3185 mCurrentItemToRestoreAfterViewItemJobStep && // that message is in the childMessage subtree 3186 mCurrentItemToRestoreAfterViewItemJobStep->hasAncestor(childMessage))) { 3187 attachMessageToGroupHeader(childMessage); 3188 3189 Q_ASSERT(childMessage->isViewable()); 3190 } 3191 3192 mOrphanChildrenHash.insert(childMessage, childMessage); 3193 } 3194 3195 if (mNewestItem == dyingMessage) { 3196 mNewestItem = nullptr; 3197 } 3198 if (mOldestItem == dyingMessage) { 3199 mOldestItem = nullptr; 3200 } 3201 3202 delete dyingMessage; 3203 3204 curIndex++; 3205 3206 // FIXME: Maybe we should check smaller steps here since the 3207 // code above can generate large message tree movements 3208 // for each single item we sweep in the invalidatedMessages list. 3209 if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) { 3210 if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { 3211 if (curIndex <= endIndex) { 3212 job->setCurrentIndex(curIndex); 3213 return ViewItemJobInterrupted; 3214 } 3215 } 3216 } 3217 } 3218 3219 // We looped over the entire deleted message list. 3220 3221 job->setCurrentIndex(endIndex + 1); 3222 3223 // A quick last cleaning pass: this is usually very fast so we don't have a real 3224 // Pass enumeration for it. We just include it as trailer of Pass1Cleanup to be executed 3225 // when job->currentIndex() > job->endIndex(); 3226 3227 // We move all the messages from the orphan child hash to the unassigned message 3228 // list and get them ready for the standard Pass2. 3229 3230 auto it = mOrphanChildrenHash.begin(); 3231 auto end = mOrphanChildrenHash.end(); 3232 3233 curIndex = 0; 3234 3235 while (it != end) { 3236 mUnassignedMessageListForPass2.append(*it); 3237 3238 it = mOrphanChildrenHash.erase(it); 3239 3240 // This is still interruptible 3241 3242 curIndex++; 3243 3244 // FIXME: We could take "larger" steps here 3245 if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) { 3246 if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { 3247 if (it != mOrphanChildrenHash.end()) { 3248 return ViewItemJobInterrupted; 3249 } 3250 } 3251 } 3252 } 3253 3254 return ViewItemJobCompleted; 3255 } 3256 3257 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass1Update(ViewItemJob *job, QElapsedTimer elapsedTimer) 3258 { 3259 Q_ASSERT(mModelForItemFunctions); // UI must be not disconnected here 3260 3261 // In this pass we simply update the MessageItem objects that are present in the job. 3262 3263 // Note that this list in fact contains MessageItem objects (we need dynamic_cast<>). 3264 auto messagesThatNeedUpdate = job->invariantIndexList(); 3265 3266 // We don't shrink the messagesThatNeedUpdate because it's basically an array. 3267 // It's faster to traverse an array of N entries than to remove K>0 entries 3268 // one by one and to traverse the remaining N-K entries. 3269 3270 // The begin index of our work 3271 int curIndex = job->currentIndex(); 3272 // The end index of our work. 3273 int endIndex = job->endIndex(); 3274 3275 while (curIndex <= endIndex) { 3276 // Get the underlying storage message data... 3277 auto message = dynamic_cast<MessageItem *>(messagesThatNeedUpdate->at(curIndex)); 3278 // This MUST NOT be null (otherwise we have a bug somewhere in this file). 3279 Q_ASSERT(message); 3280 3281 int row = mInvariantRowMapper->modelInvariantIndexToModelIndexRow(message); 3282 3283 if (row < 0) { 3284 // Must have been invalidated (so it's basically about to be deleted) 3285 Q_ASSERT(!message->isValid()); 3286 // Skip it here. 3287 curIndex++; 3288 continue; 3289 } 3290 3291 time_t prevDate = message->date(); 3292 time_t prevMaxDate = message->maxDate(); 3293 bool toDoStatus = message->status().isToAct(); 3294 bool prevUnreadStatus = !message->status().isRead(); 3295 bool prevImportantStatus = message->status().isImportant(); 3296 3297 // The subject/reference based threading cache is sorted by date: we must remove 3298 // the item and re-insert it since updateMessageItemData() may change the date too. 3299 if (mAggregation->threading() == Aggregation::PerfectReferencesAndSubject) { 3300 removeMessageFromReferencesBasedThreadingCache(message); 3301 removeMessageFromSubjectBasedThreadingCache(message); 3302 } else if (mAggregation->threading() == Aggregation::PerfectAndReferences) { 3303 removeMessageFromReferencesBasedThreadingCache(message); 3304 } 3305 3306 // Do update 3307 mStorageModel->updateMessageItemData(message, row); 3308 const QModelIndex idx = q->index(message, 0); 3309 Q_EMIT q->dataChanged(idx, idx); 3310 3311 // Reinsert the item to the cache, if needed 3312 if (mAggregation->threading() == Aggregation::PerfectReferencesAndSubject) { 3313 addMessageToReferencesBasedThreadingCache(message); 3314 addMessageToSubjectBasedThreadingCache(message); 3315 } else if (mAggregation->threading() == Aggregation::PerfectAndReferences) { 3316 addMessageToReferencesBasedThreadingCache(message); 3317 } 3318 3319 int propertyChangeMask = 0; 3320 3321 if (prevDate != message->date()) { 3322 propertyChangeMask |= DateChanged; 3323 } 3324 if (prevMaxDate != message->maxDate()) { 3325 propertyChangeMask |= MaxDateChanged; 3326 } 3327 if (toDoStatus != message->status().isToAct()) { 3328 propertyChangeMask |= ActionItemStatusChanged; 3329 } 3330 if (prevUnreadStatus != (!message->status().isRead())) { 3331 propertyChangeMask |= UnreadStatusChanged; 3332 } 3333 if (prevImportantStatus != (!message->status().isImportant())) { 3334 propertyChangeMask |= ImportantStatusChanged; 3335 } 3336 3337 if (propertyChangeMask) { 3338 // Some message data has changed 3339 // now we need to handle the changes that might cause re-grouping/re-sorting 3340 // and propagate them to the parents. 3341 3342 Item *pParent = message->parent(); 3343 3344 if (pParent && (pParent != mRootItem)) { 3345 // The following function will return true if itemParent may be affected by the change. 3346 // If the itemParent isn't affected, we stop climbing. 3347 if (handleItemPropertyChanges(propertyChangeMask, pParent, message)) { 3348 Q_ASSERT(message->parent()); // handleItemPropertyChanges() must never leave an item detached 3349 3350 // Note that actually message->parent() may be different than pParent since 3351 // handleItemPropertyChanges() may have re-grouped it. 3352 3353 // Time to propagate up. 3354 propagateItemPropertiesToParent(message); 3355 } 3356 } // else there is no parent so the item isn't attached to the view: re-grouping/re-sorting not needed. 3357 } // else message data didn't change an there is nothing interesting to do 3358 3359 // (re-)apply the filter, if needed 3360 if (mFilter && message->isViewable()) { 3361 // In all the other cases we (re-)apply the filter to the topmost subtree that this message is in. 3362 Item *pTopMostNonRoot = message->topmostNonRoot(); 3363 3364 Q_ASSERT(pTopMostNonRoot); 3365 Q_ASSERT(pTopMostNonRoot != mRootItem); 3366 Q_ASSERT(pTopMostNonRoot->parent() == mRootItem); 3367 3368 // FIXME: The call below works, but it's expensive when we are updating 3369 // a lot of items with filtering enabled. This is because the updated 3370 // items are likely to be in the same subtree which we then filter multiple times. 3371 // A point for us is that when filtering there shouldn't be really many 3372 // items in the view so the user isn't going to update a lot of them at once... 3373 // Well... anyway, the alternative would be to write yet another 3374 // specialized routine that would update only the "message" item 3375 // above and climb up eventually hiding parents (without descending the sibling subtrees again). 3376 // If people complain about performance in this particular case I'll consider that solution. 3377 3378 applyFilterToSubtree(pTopMostNonRoot, QModelIndex()); 3379 } // otherwise there is no filter or the item isn't viewable: very likely 3380 // left detached while propagating property changes. Will filter it 3381 // on reattach. 3382 3383 // Done updating this message 3384 3385 curIndex++; 3386 3387 // FIXME: Maybe we should check smaller steps here since the 3388 // code above can generate large message tree movements 3389 // for each single item we sweep in the messagesThatNeedUpdate list. 3390 if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) { 3391 if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { 3392 if (curIndex <= endIndex) { 3393 job->setCurrentIndex(curIndex); 3394 return ViewItemJobInterrupted; 3395 } 3396 } 3397 } 3398 } 3399 3400 return ViewItemJobCompleted; 3401 } 3402 3403 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJob(ViewItemJob *job, QElapsedTimer elapsedTimer) 3404 { 3405 // This function does a timed chunk of work for a single Fill View job. 3406 // It attempts to process messages until a timeout forces it to return to the caller. 3407 3408 // A macro would improve readability here but since this is a good point 3409 // to place debugger breakpoints then we need it explicitly. 3410 // A (template) helper would need to pass many parameters and would not be inlined... 3411 3412 if (job->currentPass() == ViewItemJob::Pass1Fill) { 3413 // We're in Pass1Fill of the job. 3414 switch (viewItemJobStepInternalForJobPass1Fill(job, elapsedTimer)) { 3415 case ViewItemJobInterrupted: 3416 // current job interrupted by timeout: propagate status to caller 3417 return ViewItemJobInterrupted; 3418 break; 3419 case ViewItemJobCompleted: 3420 // pass 1 has been completed 3421 // # TODO: Refactor this, make it virtual or whatever, but switch == bad, code duplication etc 3422 job->setCurrentPass(ViewItemJob::Pass2); 3423 job->setStartIndex(0); 3424 job->setEndIndex(mUnassignedMessageListForPass2.count() - 1); 3425 // take care of small jobs which never timeout by themselves because 3426 // of a small number of messages. At the end of each job check 3427 // the time used and if we're timeoutting and there is another job 3428 // then interrupt. 3429 if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { 3430 return ViewItemJobInterrupted; 3431 } // else proceed with the next pass 3432 break; 3433 default: 3434 // This is *really* a BUG 3435 qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result"; 3436 Q_ASSERT(false); 3437 break; 3438 } 3439 } else if (job->currentPass() == ViewItemJob::Pass1Cleanup) { 3440 // We're in Pass1Cleanup of the job. 3441 switch (viewItemJobStepInternalForJobPass1Cleanup(job, elapsedTimer)) { 3442 case ViewItemJobInterrupted: 3443 // current job interrupted by timeout: propagate status to caller 3444 return ViewItemJobInterrupted; 3445 break; 3446 case ViewItemJobCompleted: 3447 // pass 1 has been completed 3448 job->setCurrentPass(ViewItemJob::Pass2); 3449 job->setStartIndex(0); 3450 job->setEndIndex(mUnassignedMessageListForPass2.count() - 1); 3451 // take care of small jobs which never timeout by themselves because 3452 // of a small number of messages. At the end of each job check 3453 // the time used and if we're timeoutting and there is another job 3454 // then interrupt. 3455 if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { 3456 return ViewItemJobInterrupted; 3457 } // else proceed with the next pass 3458 break; 3459 default: 3460 // This is *really* a BUG 3461 qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result"; 3462 Q_ASSERT(false); 3463 break; 3464 } 3465 } else if (job->currentPass() == ViewItemJob::Pass1Update) { 3466 // We're in Pass1Update of the job. 3467 switch (viewItemJobStepInternalForJobPass1Update(job, elapsedTimer)) { 3468 case ViewItemJobInterrupted: 3469 // current job interrupted by timeout: propagate status to caller 3470 return ViewItemJobInterrupted; 3471 break; 3472 case ViewItemJobCompleted: 3473 // pass 1 has been completed 3474 // Since Pass2, Pass3 and Pass4 are empty for an Update operation 3475 // we simply skip them. (TODO: Triple-verify this assertion...). 3476 job->setCurrentPass(ViewItemJob::Pass5); 3477 job->setStartIndex(0); 3478 job->setEndIndex(mGroupHeadersThatNeedUpdate.count() - 1); 3479 // take care of small jobs which never timeout by themselves because 3480 // of a small number of messages. At the end of each job check 3481 // the time used and if we're timeoutting and there is another job 3482 // then interrupt. 3483 if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { 3484 return ViewItemJobInterrupted; 3485 } // else proceed with the next pass 3486 break; 3487 default: 3488 // This is *really* a BUG 3489 qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result"; 3490 Q_ASSERT(false); 3491 break; 3492 } 3493 } 3494 3495 // Pass1Fill/Pass1Cleanup/Pass1Update has been already completed. 3496 3497 if (job->currentPass() == ViewItemJob::Pass2) { 3498 // We're in Pass2 of the job. 3499 switch (viewItemJobStepInternalForJobPass2(job, elapsedTimer)) { 3500 case ViewItemJobInterrupted: 3501 // current job interrupted by timeout: propagate status to caller 3502 return ViewItemJobInterrupted; 3503 break; 3504 case ViewItemJobCompleted: 3505 // pass 2 has been completed 3506 job->setCurrentPass(ViewItemJob::Pass3); 3507 job->setStartIndex(0); 3508 job->setEndIndex(mUnassignedMessageListForPass3.count() - 1); 3509 // take care of small jobs which never timeout by themselves because 3510 // of a small number of messages. At the end of each job check 3511 // the time used and if we're timeoutting and there is another job 3512 // then interrupt. 3513 if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { 3514 return ViewItemJobInterrupted; 3515 } 3516 // else proceed with the next pass 3517 break; 3518 default: 3519 // This is *really* a BUG 3520 qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result"; 3521 Q_ASSERT(false); 3522 break; 3523 } 3524 } 3525 3526 if (job->currentPass() == ViewItemJob::Pass3) { 3527 // We're in Pass3 of the job. 3528 switch (viewItemJobStepInternalForJobPass3(job, elapsedTimer)) { 3529 case ViewItemJobInterrupted: 3530 // current job interrupted by timeout: propagate status to caller 3531 return ViewItemJobInterrupted; 3532 case ViewItemJobCompleted: 3533 // pass 3 has been completed 3534 job->setCurrentPass(ViewItemJob::Pass4); 3535 job->setStartIndex(0); 3536 job->setEndIndex(mUnassignedMessageListForPass4.count() - 1); 3537 // take care of small jobs which never timeout by themselves because 3538 // of a small number of messages. At the end of each job check 3539 // the time used and if we're timeoutting and there is another job 3540 // then interrupt. 3541 if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { 3542 return ViewItemJobInterrupted; 3543 } 3544 // else proceed with the next pass 3545 break; 3546 default: 3547 // This is *really* a BUG 3548 qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result"; 3549 Q_ASSERT(false); 3550 break; 3551 } 3552 } 3553 3554 if (job->currentPass() == ViewItemJob::Pass4) { 3555 // We're in Pass4 of the job. 3556 switch (viewItemJobStepInternalForJobPass4(job, elapsedTimer)) { 3557 case ViewItemJobInterrupted: 3558 // current job interrupted by timeout: propagate status to caller 3559 return ViewItemJobInterrupted; 3560 case ViewItemJobCompleted: 3561 // pass 4 has been completed 3562 job->setCurrentPass(ViewItemJob::Pass5); 3563 job->setStartIndex(0); 3564 job->setEndIndex(mGroupHeadersThatNeedUpdate.count() - 1); 3565 // take care of small jobs which never timeout by themselves because 3566 // of a small number of messages. At the end of each job check 3567 // the time used and if we're timeoutting and there is another job 3568 // then interrupt. 3569 if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { 3570 return ViewItemJobInterrupted; 3571 } 3572 // else proceed with the next pass 3573 break; 3574 default: 3575 // This is *really* a BUG 3576 qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result"; 3577 Q_ASSERT(false); 3578 break; 3579 } 3580 } 3581 3582 // Pass4 has been already completed. Proceed to Pass5. 3583 return viewItemJobStepInternalForJobPass5(job, elapsedTimer); 3584 } 3585 3586 #ifdef KDEPIM_FOLDEROPEN_PROFILE 3587 3588 // Namespace to collect all the vars and functions for KDEPIM_FOLDEROPEN_PROFILE 3589 namespace Stats 3590 { 3591 // Number of existing jobs/passes 3592 static const int numberOfPasses = ViewItemJob::LastIndex; 3593 3594 // The pass in the last call of viewItemJobStepInternal(), used to detect when 3595 // a new pass starts 3596 static int lastPass = -1; 3597 3598 // Total number of messages in the folder 3599 static int totalMessages; 3600 3601 // Per-Job data 3602 static int numElements[numberOfPasses]; 3603 static int totalTime[numberOfPasses]; 3604 static int chunks[numberOfPasses]; 3605 3606 // Time, in msecs for some special operations 3607 static int expandingTreeTime; 3608 static int layoutChangeTime; 3609 3610 // Descriptions of the job, for nicer debug output 3611 static const char *jobDescription[numberOfPasses] = {"Creating items from messages and simple threading", 3612 "Removing messages", 3613 "Updating messages", 3614 "Additional Threading", 3615 "Subject-Based threading", 3616 "Grouping", 3617 "Group resorting + cleanup"}; 3618 3619 // Timer to track time between start of first job and end of last job 3620 static QTime firstStartTime; 3621 3622 // Timer to track time the current job takes 3623 static QTime currentJobStartTime; 3624 3625 // Zeros the stats, to be called when the first job starts 3626 static void resetStats() 3627 { 3628 totalMessages = 0; 3629 layoutChangeTime = 0; 3630 expandingTreeTime = 0; 3631 lastPass = -1; 3632 for (int i = 0; i < numberOfPasses; ++i) { 3633 numElements[i] = 0; 3634 totalTime[i] = 0; 3635 chunks[i] = 0; 3636 } 3637 } 3638 } // namespace Stats 3639 3640 void ModelPrivate::printStatistics() 3641 { 3642 using namespace Stats; 3643 int totalTotalTime = 0; 3644 int completeTime = firstStartTime.elapsed(); 3645 for (int i = 0; i < numberOfPasses; ++i) { 3646 totalTotalTime += totalTime[i]; 3647 } 3648 3649 float msgPerSecond = totalMessages / (totalTotalTime / 1000.0f); 3650 float msgPerSecondComplete = totalMessages / (completeTime / 1000.0f); 3651 3652 int messagesWithSameSubjectAvg = 0; 3653 int messagesWithSameSubjectMax = 0; 3654 for (const auto messages : std::as_const(mThreadingCacheMessageSubjectMD5ToMessageItem)) { 3655 if (messages->size() > messagesWithSameSubjectMax) { 3656 messagesWithSameSubjectMax = messages->size(); 3657 } 3658 messagesWithSameSubjectAvg += messages->size(); 3659 } 3660 messagesWithSameSubjectAvg = messagesWithSameSubjectAvg / (float)mThreadingCacheMessageSubjectMD5ToMessageItem.size(); 3661 3662 int totalThreads = 0; 3663 if (!mGroupHeaderItemHash.isEmpty()) { 3664 for (const GroupHeaderItem *groupHeader : std::as_const(mGroupHeaderItemHash)) { 3665 totalThreads += groupHeader->childItemCount(); 3666 } 3667 } else { 3668 totalThreads = mRootItem->childItemCount(); 3669 } 3670 3671 qCDebug(MESSAGELIST_LOG) << "Finished filling the view with" << totalMessages << "messages"; 3672 qCDebug(MESSAGELIST_LOG) << "That took" << totalTotalTime << "msecs inside the model and" << completeTime << "in total."; 3673 qCDebug(MESSAGELIST_LOG) << (totalTotalTime / (float)completeTime) * 100.0f << "percent of the time was spent in the model."; 3674 qCDebug(MESSAGELIST_LOG) << "Time for layoutChanged(), in msecs:" << layoutChangeTime << "(" << (layoutChangeTime / (float)totalTotalTime) * 100.0f 3675 << "percent )"; 3676 qCDebug(MESSAGELIST_LOG) << "Time to expand tree, in msecs:" << expandingTreeTime << "(" << (expandingTreeTime / (float)totalTotalTime) * 100.0f 3677 << "percent )"; 3678 qCDebug(MESSAGELIST_LOG) << "Number of messages per second in the model:" << msgPerSecond; 3679 qCDebug(MESSAGELIST_LOG) << "Number of messages per second in total:" << msgPerSecondComplete; 3680 qCDebug(MESSAGELIST_LOG) << "Number of threads:" << totalThreads; 3681 qCDebug(MESSAGELIST_LOG) << "Number of groups:" << mGroupHeaderItemHash.size(); 3682 qCDebug(MESSAGELIST_LOG) << "Messages per thread:" << totalMessages / (float)totalThreads; 3683 qCDebug(MESSAGELIST_LOG) << "Threads per group:" << totalThreads / (float)mGroupHeaderItemHash.size(); 3684 qCDebug(MESSAGELIST_LOG) << "Messages with the same subject:" 3685 << "Max:" << messagesWithSameSubjectMax << "Avg:" << messagesWithSameSubjectAvg; 3686 qCDebug(MESSAGELIST_LOG); 3687 qCDebug(MESSAGELIST_LOG) << "Now follows a breakdown of the jobs."; 3688 qCDebug(MESSAGELIST_LOG); 3689 for (int i = 0; i < numberOfPasses; ++i) { 3690 if (totalTime[i] == 0) { 3691 continue; 3692 } 3693 float elementsPerSecond = numElements[i] / (totalTime[i] / 1000.0f); 3694 float percent = totalTime[i] / (float)totalTotalTime * 100.0f; 3695 qCDebug(MESSAGELIST_LOG) << "----------------------------------------------"; 3696 qCDebug(MESSAGELIST_LOG) << "Job" << i + 1 << "(" << jobDescription[i] << ")"; 3697 qCDebug(MESSAGELIST_LOG) << "Share of complete time:" << percent << "percent"; 3698 qCDebug(MESSAGELIST_LOG) << "Time in msecs:" << totalTime[i]; 3699 qCDebug(MESSAGELIST_LOG) << "Number of elements:" << numElements[i]; // TODO: map of element string 3700 qCDebug(MESSAGELIST_LOG) << "Elements per second:" << elementsPerSecond; 3701 qCDebug(MESSAGELIST_LOG) << "Number of chunks:" << chunks[i]; 3702 qCDebug(MESSAGELIST_LOG); 3703 } 3704 3705 qCDebug(MESSAGELIST_LOG) << "=========================================================="; 3706 resetStats(); 3707 } 3708 3709 #endif 3710 3711 ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternal() 3712 { 3713 // This function does a timed chunk of work in our View Fill operation. 3714 // It attempts to do processing until it either runs out of jobs 3715 // to be done or a timeout forces it to interrupt and jump back to the caller. 3716 3717 QElapsedTimer elapsedTimer; 3718 elapsedTimer.start(); 3719 3720 while (!mViewItemJobs.isEmpty()) { 3721 // Have a job to do. 3722 ViewItemJob *job = mViewItemJobs.constFirst(); 3723 3724 #ifdef KDEPIM_FOLDEROPEN_PROFILE 3725 3726 // Here we check if an old job has just completed or if we are at the start of the 3727 // first job. We then initialize job data stuff and timers based on this. 3728 3729 const int currentPass = job->currentPass(); 3730 const bool firstChunk = currentPass != Stats::lastPass; 3731 if (currentPass != Stats::lastPass && Stats::lastPass != -1) { 3732 Stats::totalTime[Stats::lastPass] = Stats::currentJobStartTime.elapsed(); 3733 } 3734 const bool firstJob = job->currentPass() == ViewItemJob::Pass1Fill && firstChunk; 3735 const int elements = job->endIndex() - job->startIndex(); 3736 if (firstJob) { 3737 Stats::resetStats(); 3738 Stats::totalMessages = elements; 3739 Stats::firstStartTime.restart(); 3740 } 3741 if (firstChunk) { 3742 Stats::numElements[currentPass] = elements; 3743 Stats::currentJobStartTime.restart(); 3744 } 3745 Stats::chunks[currentPass]++; 3746 Stats::lastPass = currentPass; 3747 3748 #endif 3749 3750 mViewItemJobStepIdleInterval = job->idleInterval(); 3751 mViewItemJobStepChunkTimeout = job->chunkTimeout(); 3752 mViewItemJobStepMessageCheckCount = job->messageCheckCount(); 3753 3754 if (job->disconnectUI()) { 3755 mModelForItemFunctions = nullptr; // disconnect the UI for this job 3756 Q_ASSERT(mLoading); // this must be true in the first job 3757 // FIXME: Should assert yet more that this is the very first job for this StorageModel 3758 // Asserting only mLoading is not enough as we could be using a two-jobs loading strategy 3759 // or this could be a job enqueued before the first job has completed. 3760 } else { 3761 // With a connected UI we need to avoid the view to update the scrollbars at EVERY insertion or expansion. 3762 // QTreeViewPrivate::updateScrollBars() is very expensive as it loops through ALL the items in the view every time. 3763 // We can't disable the function directly as it's hidden in the private data object of QTreeView 3764 // but we can disable the parent QTreeView::updateGeometries() instead. 3765 // We will trigger it "manually" at the end of the step. 3766 mView->ignoreUpdateGeometries(true); 3767 3768 // Ok.. I know that this seems unbelieveable but disabling updates actually 3769 // causes a (significant) performance loss in most cases. This is probably because QTreeView 3770 // uses delayed layouts when updates are disabled which should be delayed but in 3771 // fact are "forced" by next item insertions. The delayed layout algorithm, then 3772 // is probably slower than the non-delayed one. 3773 // Disabling the paintEvent() doesn't seem to work either. 3774 // mView->setUpdatesEnabled( false ); 3775 } 3776 3777 switch (viewItemJobStepInternalForJob(job, elapsedTimer)) { 3778 case ViewItemJobInterrupted: 3779 // current job interrupted by timeout: will propagate status to caller 3780 // but before this, give some feedback to the user 3781 3782 // FIXME: This is now inaccurate, think of something else 3783 switch (job->currentPass()) { 3784 case ViewItemJob::Pass1Fill: 3785 case ViewItemJob::Pass1Cleanup: 3786 case ViewItemJob::Pass1Update: 3787 Q_EMIT q->statusMessage(i18np("Processed 1 Message of %2", 3788 "Processed %1 Messages of %2", 3789 job->currentIndex() - job->startIndex(), 3790 job->endIndex() - job->startIndex() + 1)); 3791 break; 3792 case ViewItemJob::Pass2: 3793 Q_EMIT q->statusMessage(i18np("Threaded 1 Message of %2", 3794 "Threaded %1 Messages of %2", 3795 job->currentIndex() - job->startIndex(), 3796 job->endIndex() - job->startIndex() + 1)); 3797 break; 3798 case ViewItemJob::Pass3: 3799 Q_EMIT q->statusMessage(i18np("Threaded 1 Message of %2", 3800 "Threaded %1 Messages of %2", 3801 job->currentIndex() - job->startIndex(), 3802 job->endIndex() - job->startIndex() + 1)); 3803 break; 3804 case ViewItemJob::Pass4: 3805 Q_EMIT q->statusMessage(i18np("Grouped 1 Thread of %2", 3806 "Grouped %1 Threads of %2", 3807 job->currentIndex() - job->startIndex(), 3808 job->endIndex() - job->startIndex() + 1)); 3809 break; 3810 case ViewItemJob::Pass5: 3811 Q_EMIT q->statusMessage(i18np("Updated 1 Group of %2", 3812 "Updated %1 Groups of %2", 3813 job->currentIndex() - job->startIndex(), 3814 job->endIndex() - job->startIndex() + 1)); 3815 break; 3816 default: 3817 break; 3818 } 3819 3820 if (!job->disconnectUI()) { 3821 mView->ignoreUpdateGeometries(false); 3822 // explicit call to updateGeometries() here 3823 mView->updateGeometries(); 3824 } 3825 3826 return ViewItemJobInterrupted; 3827 break; 3828 case ViewItemJobCompleted: 3829 3830 // If this job worked with a disconnected UI, Q_EMIT layoutChanged() 3831 // to reconnect it. We go back to normal operation now. 3832 if (job->disconnectUI()) { 3833 mModelForItemFunctions = q; 3834 // This call would destroy the expanded state of items. 3835 // This is why when mModelForItemFunctions was 0 we didn't actually expand them 3836 // but we just set a "ExpandNeeded" mark... 3837 #ifdef KDEPIM_FOLDEROPEN_PROFILE 3838 QTime layoutChangedTimer; 3839 layoutChangedTimer.start(); 3840 #endif 3841 mView->modelAboutToEmitLayoutChanged(); 3842 Q_EMIT q->layoutChanged(); 3843 mView->modelEmittedLayoutChanged(); 3844 3845 #ifdef KDEPIM_FOLDEROPEN_PROFILE 3846 Stats::layoutChangeTime = layoutChangedTimer.elapsed(); 3847 QTime expandingTime; 3848 expandingTime.start(); 3849 #endif 3850 3851 // expand all the items that need it in a single sweep 3852 3853 // FIXME: This takes quite a lot of time, it could be made an interruptible job 3854 3855 auto rootChildItems = mRootItem->childItems(); 3856 if (rootChildItems) { 3857 for (const auto it : std::as_const(*rootChildItems)) { 3858 if (it->initialExpandStatus() == Item::ExpandNeeded) { 3859 syncExpandedStateOfSubtree(it); 3860 } 3861 } 3862 } 3863 #ifdef KDEPIM_FOLDEROPEN_PROFILE 3864 Stats::expandingTreeTime = expandingTime.elapsed(); 3865 #endif 3866 } else { 3867 mView->ignoreUpdateGeometries(false); 3868 // explicit call to updateGeometries() here 3869 mView->updateGeometries(); 3870 } 3871 3872 // this job has been completed 3873 delete mViewItemJobs.takeFirst(); 3874 3875 #ifdef KDEPIM_FOLDEROPEN_PROFILE 3876 // Last job finished! 3877 Stats::totalTime[currentPass] = Stats::currentJobStartTime.elapsed(); 3878 printStatistics(); 3879 #endif 3880 3881 // take care of small jobs which never timeout by themselves because 3882 // of a small number of messages. At the end of each job check 3883 // the time used and if we're timeoutting and there is another job 3884 // then interrupt. 3885 if ((elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) || (elapsedTimer.elapsed() < 0)) { 3886 if (!mViewItemJobs.isEmpty()) { 3887 return ViewItemJobInterrupted; 3888 } 3889 // else it's completed in fact 3890 } // else proceed with the next job 3891 3892 break; 3893 default: 3894 // This is *really* a BUG 3895 qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result"; 3896 Q_ASSERT(false); 3897 break; 3898 } 3899 } 3900 3901 // no more jobs 3902 3903 Q_EMIT q->statusMessage(i18nc("@info:status Finished view fill", "Ready")); 3904 3905 return ViewItemJobCompleted; 3906 } 3907 3908 void ModelPrivate::viewItemJobStep() 3909 { 3910 // A single step in the View Fill operation. 3911 // This function wraps viewItemJobStepInternal() which does the step job 3912 // and either completes it or stops because of a timeout. 3913 // If the job is stopped then we start a zero-msecs timer to call us 3914 // back and resume the job. Otherwise we're just done. 3915 3916 mViewItemJobStepStartTime = ::time(nullptr); 3917 3918 if (mFillStepTimer.isActive()) { 3919 mFillStepTimer.stop(); 3920 } 3921 3922 if (!mStorageModel) { 3923 return; // nothing more to do 3924 } 3925 3926 // Save the current item in the view as our process may 3927 // cause items to be reparented (and QTreeView will forget the current item in the meantime). 3928 // This machinery is also needed when we're about to remove items from the view in 3929 // a cleanup job: we'll be trying to set as current the item after the one removed. 3930 3931 QModelIndex currentIndexBeforeStep = mView->currentIndex(); 3932 Item *currentItemBeforeStep = currentIndexBeforeStep.isValid() ? static_cast<Item *>(currentIndexBeforeStep.internalPointer()) : nullptr; 3933 3934 // mCurrentItemToRestoreAfterViewItemJobStep will be zeroed out if it's killed 3935 mCurrentItemToRestoreAfterViewItemJobStep = currentItemBeforeStep; 3936 3937 // Save the current item position in the viewport as QTreeView fails to keep 3938 // the current item in the sample place when items are added or removed... 3939 QRect rectBeforeViewItemJobStep; 3940 3941 const bool lockView = mView->isScrollingLocked(); 3942 3943 // This is generally SLOW AS HELL... (so we avoid it if we lock the view and thus don't need it) 3944 if (mCurrentItemToRestoreAfterViewItemJobStep && (!lockView)) { 3945 rectBeforeViewItemJobStep = mView->visualRect(currentIndexBeforeStep); 3946 } 3947 3948 // FIXME: If the current item is NOT in the view, preserve the position 3949 // of the top visible item. This will make the view move yet less. 3950 3951 // Insulate the View from (very likely spurious) "currentChanged()" signals. 3952 mView->ignoreCurrentChanges(true); 3953 3954 // And go to real work. 3955 switch (viewItemJobStepInternal()) { 3956 case ViewItemJobInterrupted: 3957 // Operation timed out, need to resume in a while 3958 if (!mInLengthyJobBatch) { 3959 mInLengthyJobBatch = true; 3960 } 3961 mFillStepTimer.start(mViewItemJobStepIdleInterval); // this is a single shot timer connected to viewItemJobStep() 3962 // and go dealing with current/selection out of the switch. 3963 break; 3964 case ViewItemJobCompleted: 3965 // done :) 3966 3967 Q_ASSERT(mModelForItemFunctions); // UI must be no (longer) disconnected in this state 3968 3969 // Ask the view to remove the eventual busy indications 3970 if (mInLengthyJobBatch) { 3971 mInLengthyJobBatch = false; 3972 } 3973 3974 if (mLoading) { 3975 mLoading = false; 3976 mView->modelFinishedLoading(); 3977 slotApplyFilter(); 3978 } 3979 3980 // Apply pre-selection, if any 3981 if (mPreSelectionMode != PreSelectNone) { 3982 mView->ignoreCurrentChanges(false); 3983 3984 bool bSelectionDone = false; 3985 3986 switch (mPreSelectionMode) { 3987 case PreSelectLastSelected: 3988 // fall down 3989 break; 3990 case PreSelectFirstUnreadCentered: 3991 bSelectionDone = mView->selectFirstMessageItem(MessageTypeUnreadOnly, true); // center 3992 break; 3993 case PreSelectOldestCentered: 3994 mView->setCurrentMessageItem(mOldestItem, true /* center */); 3995 bSelectionDone = true; 3996 break; 3997 case PreSelectNewestCentered: 3998 mView->setCurrentMessageItem(mNewestItem, true /* center */); 3999 bSelectionDone = true; 4000 break; 4001 case PreSelectNone: 4002 // deal with selection below 4003 break; 4004 default: 4005 qCWarning(MESSAGELIST_LOG) << "ERROR: Unrecognized pre-selection mode " << static_cast<int>(mPreSelectionMode); 4006 break; 4007 } 4008 4009 if ((!bSelectionDone) && (mPreSelectionMode != PreSelectNone)) { 4010 // fallback to last selected, if possible 4011 if (mLastSelectedMessageInFolder) { // we found it in the loading process: select and jump out 4012 mView->setCurrentMessageItem(mLastSelectedMessageInFolder); 4013 bSelectionDone = true; 4014 } 4015 } 4016 4017 if (bSelectionDone) { 4018 mLastSelectedMessageInFolder = nullptr; 4019 mPreSelectionMode = PreSelectNone; 4020 return; // already taken care of current / selection 4021 } 4022 } 4023 // deal with current/selection out of the switch 4024 4025 break; 4026 default: 4027 // This is *really* a BUG 4028 qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result"; 4029 Q_ASSERT(false); 4030 break; 4031 } 4032 4033 // Everything else here deals with the selection 4034 4035 // If UI is disconnected then we don't have anything else to do here 4036 if (!mModelForItemFunctions) { 4037 mView->ignoreCurrentChanges(false); 4038 return; 4039 } 4040 4041 // Restore current/selection and/or scrollbar position 4042 4043 if (mCurrentItemToRestoreAfterViewItemJobStep) { 4044 bool stillIgnoringCurrentChanges = true; 4045 4046 // If the assert below fails then the previously current item got detached 4047 // and didn't get reattached in the step: this should never happen. 4048 Q_ASSERT(mCurrentItemToRestoreAfterViewItemJobStep->isViewable()); 4049 4050 // Check if the current item changed 4051 QModelIndex currentIndexAfterStep = mView->currentIndex(); 4052 Item *currentAfterStep = currentIndexAfterStep.isValid() ? static_cast<Item *>(currentIndexAfterStep.internalPointer()) : nullptr; 4053 4054 if (mCurrentItemToRestoreAfterViewItemJobStep != currentAfterStep) { 4055 // QTreeView lost the current item... 4056 if (mCurrentItemToRestoreAfterViewItemJobStep != currentItemBeforeStep) { 4057 // Some view job code expects us to actually *change* the current item. 4058 // This is done by the cleanup step which removes items and tries 4059 // to set as current the item *after* the removed one, if possible. 4060 // We need the view to handle the change though. 4061 stillIgnoringCurrentChanges = false; 4062 mView->ignoreCurrentChanges(false); 4063 } else { 4064 // we just have to restore the old current item. The code 4065 // outside shouldn't have noticed that we lost it (e.g. the message viewer 4066 // still should have the old message opened). So we don't need to 4067 // actually notify the view of the restored setting. 4068 } 4069 // Restore it 4070 qCDebug(MESSAGELIST_LOG) << "Gonna restore current here" << mCurrentItemToRestoreAfterViewItemJobStep->subject(); 4071 mView->setCurrentIndex(q->index(mCurrentItemToRestoreAfterViewItemJobStep, 0)); 4072 } else { 4073 // The item we're expected to set as current is already current 4074 if (mCurrentItemToRestoreAfterViewItemJobStep != currentItemBeforeStep) { 4075 // But we have changed it in the job step. 4076 // This means that: we have deleted the current item and chosen a 4077 // new candidate as current but Qt also has chosen it as candidate 4078 // and already made it current. The problem is that (as of Qt 4.4) 4079 // it probably didn't select it. 4080 if (!mView->selectionModel()->hasSelection()) { 4081 stillIgnoringCurrentChanges = false; 4082 mView->ignoreCurrentChanges(false); 4083 4084 qCDebug(MESSAGELIST_LOG) << "Gonna restore selection here" << mCurrentItemToRestoreAfterViewItemJobStep->subject(); 4085 4086 QItemSelection selection; 4087 selection.append(QItemSelectionRange(q->index(mCurrentItemToRestoreAfterViewItemJobStep, 0))); 4088 mView->selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows); 4089 } 4090 } 4091 } 4092 4093 // FIXME: If it was selected before the change, then re-select it (it may happen that it's not) 4094 if (!lockView) { 4095 // we prefer to keep the currently selected item steady in the view 4096 QRect rectAfterViewItemJobStep = mView->visualRect(q->index(mCurrentItemToRestoreAfterViewItemJobStep, 0)); 4097 if (rectBeforeViewItemJobStep.y() != rectAfterViewItemJobStep.y()) { 4098 // QTreeView lost its position... 4099 mView->verticalScrollBar()->setValue(mView->verticalScrollBar()->value() + rectAfterViewItemJobStep.y() - rectBeforeViewItemJobStep.y()); 4100 } 4101 } 4102 4103 // and kill the insulation, if not yet done 4104 if (stillIgnoringCurrentChanges) { 4105 mView->ignoreCurrentChanges(false); 4106 } 4107 4108 return; 4109 } 4110 4111 // Either there was no current item before, or it was lost in a cleanup step and another candidate for 4112 // current item couldn't be found (possibly empty view) 4113 mView->ignoreCurrentChanges(false); 4114 4115 if (currentItemBeforeStep) { 4116 // lost in a cleanup.. 4117 // tell the view that we have a new current, this time with no insulation 4118 mView->slotSelectionChanged(QItemSelection(), QItemSelection()); 4119 } 4120 } 4121 4122 void ModelPrivate::slotStorageModelRowsInserted(const QModelIndex &parent, int from, int to) 4123 { 4124 if (parent.isValid()) { 4125 return; // ugh... should never happen 4126 } 4127 4128 Q_ASSERT(from <= to); 4129 4130 int count = (to - from) + 1; 4131 4132 mInvariantRowMapper->modelRowsInserted(from, count); 4133 4134 // look if no current job is in the middle 4135 4136 int jobCount = mViewItemJobs.count(); 4137 4138 for (int idx = 0; idx < jobCount; idx++) { 4139 ViewItemJob *job = mViewItemJobs.at(idx); 4140 4141 if (job->currentPass() != ViewItemJob::Pass1Fill) { 4142 // The job is a cleanup or in a later pass: the storage has been already accessed 4143 // and the messages created... no need to care anymore: the invariant row mapper will do the job. 4144 continue; 4145 } 4146 4147 if (job->currentIndex() > job->endIndex()) { 4148 // The job finished the Pass1Fill but still waits for the pass indicator to be 4149 // changed. This is unlikely but still may happen if the job has been interrupted 4150 // and then a call to slotStorageModelRowsRemoved() caused it to be forcibly completed. 4151 continue; 4152 } 4153 4154 // 4155 // The following cases are possible: 4156 // 4157 // from to 4158 // | | -> shift up job 4159 // from to 4160 // | | -> shift up job 4161 // from to 4162 // | | -> shift up job 4163 // from to 4164 // | | -> split job 4165 // from to 4166 // | | -> split job 4167 // from to 4168 // | | -> job unaffected 4169 // 4170 // 4171 // FOLDER 4172 // |-------------------------|---------|--------------| 4173 // 0 currentIndex endIndex count 4174 // +-- job --+ 4175 // 4176 4177 if (from > job->endIndex()) { 4178 // The change is completely above the job, the job is not affected 4179 continue; 4180 } 4181 4182 if (from > job->currentIndex()) { // and from <= job->endIndex() 4183 // The change starts in the middle of the job in a way that it must be split in two. 4184 // The first part is unaffected by the shift and ranges from job->currentIndex() to from - 1. 4185 // The second part ranges from "from" to job->endIndex() that are now shifted up by count steps. 4186 4187 // First add a new job for the second part. 4188 auto newJob = new ViewItemJob(from + count, job->endIndex() + count, job->chunkTimeout(), job->idleInterval(), job->messageCheckCount()); 4189 4190 Q_ASSERT(newJob->currentIndex() <= newJob->endIndex()); 4191 4192 idx++; // we can skip this job in the loop, it's already ok 4193 jobCount++; // and our range increases by one. 4194 mViewItemJobs.insert(idx, newJob); 4195 4196 // Then limit the original job to the first part 4197 job->setEndIndex(from - 1); 4198 4199 Q_ASSERT(job->currentIndex() <= job->endIndex()); 4200 4201 continue; 4202 } 4203 4204 // The change starts below (or exactly on the beginning of) the job. 4205 // The job must be shifted up. 4206 job->setCurrentIndex(job->currentIndex() + count); 4207 job->setEndIndex(job->endIndex() + count); 4208 4209 Q_ASSERT(job->currentIndex() <= job->endIndex()); 4210 } 4211 4212 bool newJobNeeded = true; 4213 4214 // Try to attach to an existing fill job, if any. 4215 // To enforce consistency we can attach only if the Fill job 4216 // is the last one in the list (might be eventually *also* the first, 4217 // and even being already processed but we must make sure that there 4218 // aren't jobs _after_ it). 4219 if (jobCount > 0) { 4220 ViewItemJob *job = mViewItemJobs.at(jobCount - 1); 4221 if (job->currentPass() == ViewItemJob::Pass1Fill) { 4222 if ( 4223 // The job ends just before the added rows 4224 (from == (job->endIndex() + 1)) && // The job didn't reach the end of Pass1Fill yet 4225 (job->currentIndex() <= job->endIndex())) { 4226 // We can still attach this :) 4227 job->setEndIndex(to); 4228 Q_ASSERT(job->currentIndex() <= job->endIndex()); 4229 newJobNeeded = false; 4230 } 4231 } 4232 } 4233 4234 if (newJobNeeded) { 4235 // FIXME: Should take timing options from aggregation here ? 4236 auto job = new ViewItemJob(from, to, 100, 50, 10); 4237 mViewItemJobs.append(job); 4238 } 4239 4240 if (!mFillStepTimer.isActive()) { 4241 mFillStepTimer.start(mViewItemJobStepIdleInterval); 4242 } 4243 } 4244 4245 void ModelPrivate::slotStorageModelRowsRemoved(const QModelIndex &parent, int from, int to) 4246 { 4247 // This is called when the underlying StorageModel emits the rowsRemoved signal. 4248 4249 if (parent.isValid()) { 4250 return; // ugh... should never happen 4251 } 4252 4253 // look if no current job is in the middle 4254 4255 Q_ASSERT(from <= to); 4256 4257 const int count = (to - from) + 1; 4258 4259 int jobCount = mViewItemJobs.count(); 4260 4261 if (mRootItem && from == 0 && count == mRootItem->childItemCount() && jobCount == 0) { 4262 clear(); 4263 return; 4264 } 4265 4266 for (int idx = 0; idx < jobCount; idx++) { 4267 ViewItemJob *job = mViewItemJobs.at(idx); 4268 4269 if (job->currentPass() != ViewItemJob::Pass1Fill) { 4270 // The job is a cleanup or in a later pass: the storage has been already accessed 4271 // and the messages created... no need to care: we will invalidate the messages in a while. 4272 continue; 4273 } 4274 4275 if (job->currentIndex() > job->endIndex()) { 4276 // The job finished the Pass1Fill but still waits for the pass indicator to be 4277 // changed. This is unlikely but still may happen if the job has been interrupted 4278 // and then a call to slotStorageModelRowsRemoved() caused it to be forcibly completed. 4279 continue; 4280 } 4281 4282 // 4283 // The following cases are possible: 4284 // 4285 // from to 4286 // | | -> shift down job 4287 // from to 4288 // | | -> shift down and crop job 4289 // from to 4290 // | | -> kill job 4291 // from to 4292 // | | -> split job, crop and shift 4293 // from to 4294 // | | -> crop job 4295 // from to 4296 // | | -> job unaffected 4297 // 4298 // 4299 // FOLDER 4300 // |-------------------------|---------|--------------| 4301 // 0 currentIndex endIndex count 4302 // +-- job --+ 4303 // 4304 4305 if (from > job->endIndex()) { 4306 // The change is completely above the job, the job is not affected 4307 continue; 4308 } 4309 4310 if (from > job->currentIndex()) { // and from <= job->endIndex() 4311 // The change starts in the middle of the job and ends in the middle or after the job. 4312 // The first part is unaffected by the shift and ranges from job->currentIndex() to from - 1 4313 // We use the existing job for this. 4314 job->setEndIndex(from - 1); // stop before the first removed row 4315 4316 Q_ASSERT(job->currentIndex() <= job->endIndex()); 4317 4318 if (to < job->endIndex()) { 4319 // The change ends inside the job and a part of it can be completed. 4320 4321 // We create a new job for the shifted remaining part. It would actually 4322 // range from to + 1 up to job->endIndex(), but we need to shift it down by count. 4323 // since count = ( to - from ) + 1 so from = to + 1 - count 4324 4325 auto newJob = new ViewItemJob(from, job->endIndex() - count, job->chunkTimeout(), job->idleInterval(), job->messageCheckCount()); 4326 4327 Q_ASSERT(newJob->currentIndex() < newJob->endIndex()); 4328 4329 idx++; // we can skip this job in the loop, it's already ok 4330 jobCount++; // and our range increases by one. 4331 mViewItemJobs.insert(idx, newJob); 4332 } // else the change includes completely the end of the job and no other part of it can be completed. 4333 4334 continue; 4335 } 4336 4337 // The change starts below (or exactly on the beginning of) the job. ( from <= job->currentIndex() ) 4338 if (to >= job->endIndex()) { 4339 // The change completely covers the job: kill it 4340 4341 // We don't delete the job since we want the other passes to be completed 4342 // This is because the Pass1Fill may have already filled mUnassignedMessageListForPass2 4343 // and may have set mOldestItem and mNewestItem. We *COULD* clear the unassigned 4344 // message list with clearUnassignedMessageLists() but mOldestItem and mNewestItem 4345 // could be still dangling pointers. So we just move the current index of the job 4346 // after the end (so storage model scan terminates) and let it complete spontaneously. 4347 job->setCurrentIndex(job->endIndex() + 1); 4348 4349 continue; 4350 } 4351 4352 if (to >= job->currentIndex()) { 4353 // The change partially covers the job. Only a part of it can be completed 4354 // and it must be shifted down. It would actually 4355 // range from to + 1 up to job->endIndex(), but we need to shift it down by count. 4356 // since count = ( to - from ) + 1 so from = to + 1 - count 4357 job->setCurrentIndex(from); 4358 job->setEndIndex(job->endIndex() - count); 4359 4360 Q_ASSERT(job->currentIndex() <= job->endIndex()); 4361 4362 continue; 4363 } 4364 4365 // The change is completely below the job: it must be shifted down. 4366 job->setCurrentIndex(job->currentIndex() - count); 4367 job->setEndIndex(job->endIndex() - count); 4368 } 4369 4370 // This will invalidate the ModelInvariantIndex-es that have been removed and return 4371 // them all in a nice list that we can feed to a view removal job. 4372 auto invalidatedIndexes = mInvariantRowMapper->modelRowsRemoved(from, count); 4373 4374 if (invalidatedIndexes) { 4375 // Try to attach to an existing cleanup job, if any. 4376 // To enforce consistency we can attach only if the Cleanup job 4377 // is the last one in the list (might be eventually *also* the first, 4378 // and even being already processed but we must make sure that there 4379 // aren't jobs _after_ it). 4380 if (jobCount > 0) { 4381 ViewItemJob *job = mViewItemJobs.at(jobCount - 1); 4382 if (job->currentPass() == ViewItemJob::Pass1Cleanup) { 4383 if ((job->currentIndex() <= job->endIndex()) && job->invariantIndexList()) { 4384 // qCDebug(MESSAGELIST_LOG) << "Appending " << invalidatedIndexes->count() << " invalidated indexes to existing cleanup job"; 4385 // We can still attach this :) 4386 *(job->invariantIndexList()) += *invalidatedIndexes; 4387 job->setEndIndex(job->endIndex() + invalidatedIndexes->count()); 4388 delete invalidatedIndexes; 4389 invalidatedIndexes = nullptr; 4390 } 4391 } 4392 } 4393 4394 if (invalidatedIndexes) { 4395 // Didn't append to any existing cleanup job.. create a new one 4396 4397 // qCDebug(MESSAGELIST_LOG) << "Creating new cleanup job for " << invalidatedIndexes->count() << " invalidated indexes"; 4398 // FIXME: Should take timing options from aggregation here ? 4399 auto job = new ViewItemJob(ViewItemJob::Pass1Cleanup, invalidatedIndexes, 100, 50, 10); 4400 mViewItemJobs.append(job); 4401 } 4402 4403 if (!mFillStepTimer.isActive()) { 4404 mFillStepTimer.start(mViewItemJobStepIdleInterval); 4405 } 4406 } 4407 } 4408 4409 void ModelPrivate::slotStorageModelLayoutChanged() 4410 { 4411 qCDebug(MESSAGELIST_LOG) << "Storage model layout changed"; 4412 // need to reset everything... 4413 q->setStorageModel(mStorageModel); 4414 qCDebug(MESSAGELIST_LOG) << "Storage model layout changed done"; 4415 } 4416 4417 void ModelPrivate::slotStorageModelDataChanged(const QModelIndex &fromIndex, const QModelIndex &toIndex) 4418 { 4419 Q_ASSERT(mStorageModel); // must exist (and be the sender of the signal connected to this slot) 4420 4421 int from = fromIndex.row(); 4422 int to = toIndex.row(); 4423 4424 Q_ASSERT(from <= to); 4425 4426 int count = (to - from) + 1; 4427 4428 int jobCount = mViewItemJobs.count(); 4429 4430 // This will find out the ModelInvariantIndex-es that need an update and will return 4431 // them all in a nice list that we can feed to a view removal job. 4432 auto indexesThatNeedUpdate = mInvariantRowMapper->modelIndexRowRangeToModelInvariantIndexList(from, count); 4433 4434 if (indexesThatNeedUpdate) { 4435 // Try to attach to an existing update job, if any. 4436 // To enforce consistency we can attach only if the Update job 4437 // is the last one in the list (might be eventually *also* the first, 4438 // and even being already processed but we must make sure that there 4439 // aren't jobs _after_ it). 4440 if (jobCount > 0) { 4441 ViewItemJob *job = mViewItemJobs.at(jobCount - 1); 4442 if (job->currentPass() == ViewItemJob::Pass1Update) { 4443 if ((job->currentIndex() <= job->endIndex()) && job->invariantIndexList()) { 4444 // We can still attach this :) 4445 *(job->invariantIndexList()) += *indexesThatNeedUpdate; 4446 job->setEndIndex(job->endIndex() + indexesThatNeedUpdate->count()); 4447 delete indexesThatNeedUpdate; 4448 indexesThatNeedUpdate = nullptr; 4449 } 4450 } 4451 } 4452 4453 if (indexesThatNeedUpdate) { 4454 // Didn't append to any existing update job.. create a new one 4455 // FIXME: Should take timing options from aggregation here ? 4456 auto job = new ViewItemJob(ViewItemJob::Pass1Update, indexesThatNeedUpdate, 100, 50, 10); 4457 mViewItemJobs.append(job); 4458 } 4459 4460 if (!mFillStepTimer.isActive()) { 4461 mFillStepTimer.start(mViewItemJobStepIdleInterval); 4462 } 4463 } 4464 } 4465 4466 void ModelPrivate::slotStorageModelHeaderDataChanged(Qt::Orientation, int, int) 4467 { 4468 if (mStorageModelContainsOutboundMessages != mStorageModel->containsOutboundMessages()) { 4469 mStorageModelContainsOutboundMessages = mStorageModel->containsOutboundMessages(); 4470 Q_EMIT q->headerDataChanged(Qt::Horizontal, 0, q->columnCount()); 4471 } 4472 } 4473 4474 Qt::ItemFlags Model::flags(const QModelIndex &index) const 4475 { 4476 if (!index.isValid()) { 4477 return Qt::NoItemFlags; 4478 } 4479 4480 Q_ASSERT(d->mModelForItemFunctions); // UI must be connected if a valid index was queried 4481 4482 Item *it = static_cast<Item *>(index.internalPointer()); 4483 4484 Q_ASSERT(it); 4485 4486 if (it->type() == Item::GroupHeader) { 4487 return Qt::ItemIsEnabled; 4488 } 4489 4490 Q_ASSERT(it->type() == Item::Message); 4491 4492 if (!static_cast<MessageItem *>(it)->isValid()) { 4493 return Qt::NoItemFlags; // not enabled, not selectable 4494 } 4495 4496 if (static_cast<MessageItem *>(it)->aboutToBeRemoved()) { 4497 return Qt::NoItemFlags; // not enabled, not selectable 4498 } 4499 4500 if (static_cast<MessageItem *>(it)->status().isDeleted()) { 4501 return Qt::NoItemFlags; // not enabled, not selectable 4502 } 4503 4504 return Qt::ItemIsEnabled | Qt::ItemIsSelectable; 4505 } 4506 4507 QMimeData *MessageList::Core::Model::mimeData(const QModelIndexList &indexes) const 4508 { 4509 QList<MessageItem *> msgs; 4510 for (const QModelIndex &idx : indexes) { 4511 if (idx.isValid()) { 4512 Item *item = static_cast<Item *>(idx.internalPointer()); 4513 if (item->type() == MessageList::Core::Item::Message) { 4514 msgs << static_cast<MessageItem *>(idx.internalPointer()); 4515 } 4516 } 4517 } 4518 return storageModel()->mimeData(msgs); 4519 } 4520 4521 Item *Model::rootItem() const 4522 { 4523 return d->mRootItem; 4524 } 4525 4526 bool Model::isLoading() const 4527 { 4528 return d->mLoading; 4529 } 4530 4531 MessageItem *Model::messageItemByStorageRow(int row) const 4532 { 4533 if (!d->mStorageModel) { 4534 return nullptr; 4535 } 4536 auto idx = d->mInvariantRowMapper->modelIndexRowToModelInvariantIndex(row); 4537 if (!idx) { 4538 return nullptr; 4539 } 4540 4541 return static_cast<MessageItem *>(idx); 4542 } 4543 4544 MessageItemSetReference Model::createPersistentSet(const QList<MessageItem *> &items) 4545 { 4546 if (!d->mPersistentSetManager) { 4547 d->mPersistentSetManager = new MessageItemSetManager(); 4548 } 4549 4550 MessageItemSetReference ref = d->mPersistentSetManager->createSet(); 4551 for (const auto mi : items) { 4552 d->mPersistentSetManager->addMessageItem(ref, mi); 4553 } 4554 4555 return ref; 4556 } 4557 4558 QList<MessageItem *> Model::persistentSetCurrentMessageItemList(MessageItemSetReference ref) 4559 { 4560 if (d->mPersistentSetManager) { 4561 return d->mPersistentSetManager->messageItems(ref); 4562 } 4563 return {}; 4564 } 4565 4566 void Model::deletePersistentSet(MessageItemSetReference ref) 4567 { 4568 if (!d->mPersistentSetManager) { 4569 return; 4570 } 4571 4572 d->mPersistentSetManager->removeSet(ref); 4573 4574 if (d->mPersistentSetManager->setCount() < 1) { 4575 delete d->mPersistentSetManager; 4576 d->mPersistentSetManager = nullptr; 4577 } 4578 } 4579 4580 #include "moc_model.cpp"