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"